diff options
author | David Zeuthen <davidz@redhat.com> | 2011-05-10 16:30:44 -0400 |
---|---|---|
committer | David Zeuthen <davidz@redhat.com> | 2011-05-10 16:30:44 -0400 |
commit | ab5f088b98cf63f03ddf7df710f0e9c96214cb70 (patch) | |
tree | 74db105509273a1db1ae80e471167f8b0cc6ebbe | |
parent | 631d30c53597f3536aadbd572e71c5b7cadc053c (diff) |
Add an simple IMAP client and assorted D-Bus interfaces to expose it
I've also written code for gnome-shell to use it, see
http://people.freedesktop.org/~david/mail-notif-1.png
http://people.freedesktop.org/~david/mail-notif-2.png
http://people.freedesktop.org/~david/mail-notif-3.png
Signed-off-by: David Zeuthen <davidz@redhat.com>
30 files changed, 4935 insertions, 2 deletions
diff --git a/configure.ac b/configure.ac index 0b7c15b..e79ae2c 100644 --- a/configure.ac +++ b/configure.ac @@ -63,6 +63,8 @@ PKG_CHECK_MODULES(REST, [rest-0.7]) AC_SUBST(REST_CFLAGS) AC_SUBST(REST_LIBS) +GOBJECT_INTROSPECTION_CHECK([0.6.2]) + # Internationalization # @@ -82,6 +84,7 @@ src/Makefile src/goa/Makefile src/daemon/Makefile src/panel/Makefile +src/examples/Makefile po/Makefile.in doc/Makefile doc/version.xml @@ -104,6 +107,7 @@ echo " compiler: ${CC} cflags: ${CFLAGS} cppflags: ${CPPFLAGS} + introspection: ${found_introspection} Maintainer mode: ${USE_MAINTAINER_MODE} Building api docs: ${enable_gtk_doc} diff --git a/data/dbus-interfaces.xml b/data/dbus-interfaces.xml index 0e6a012..ed483cf 100644 --- a/data/dbus-interfaces.xml +++ b/data/dbus-interfaces.xml @@ -275,4 +275,142 @@ </interface> + + <!-- + org.gnome.OnlineAccounts.Mail: + + An account object implements this interface if it provides + email-like messaging capabilities. + + TODO: have a way to open an user agent for a message guid + obtained from a MailQuery. Also have a way to download the full + message (multipart message? ugh). + --> + <interface name="org.gnome.OnlineAccounts.Mail"> + <!-- + CreateQuery: + @criteria: The criteria of the query. + @max_size: The maximum number of messages to query for. + @query_object: Object path of the resulting query object. + + Creates a new #org.gnome.OnlineAccounts.Mail.Query object for + querying the first @max_size messages matching @criteria. The + returned object will stay alive until + org.gnome.OnlineAccounts.Mail.Query.Close() is called or the + caller vanishes from the message bus. + + See the #org.gnome.OnlineAccounts.Mail.Query:Criteria property + on the #org.gnome.OnlineAccounts.Mail.Query interface for the + values that can be used for the @criteria parameter. + --> + <method name="CreateQuery"> + <arg name="criteria" type="s" direction="in"/> + <arg name="max_size" type="i" direction="in"/> + <arg name="query_object" type="o" direction="out"/> + </method> + </interface> + + <!-- + org.gnome.OnlineAccounts.Mail.Query: + + An interface used for querying for messages. Use the + org.gnome.OnlineAccounts.Mail.CreateQuery() method on the + #org.gnome.OnlineAccounts.Mail interface to create an object + with this interface. + --> + <interface name="org.gnome.OnlineAccounts.Mail.Query"> + + <!-- Criteria: + The criteria used for what to include in the + #org.gnome.OnlineAccounts.Mail.Query:Result array. + + If this is the empty string, all messages are included. Other + valid criteria includes the string <literal>unread</literal> to only + include unread messages. + + TODO: add other criteria, e.g. "contains:some string". + --> + <property name="Criteria" type="s" access="read"/> + + <!-- MaxSize: + The maximum number of messages to return in the + #org.gnome.OnlineAccounts.Mail.Query:Result property. + --> + <property name="MaxSize" type="u" access="read"/> + + <!-- + Result: + + An array of messages in the window being monitored - this + array contains up to #org.gnome.OnlineAccounts.Mail.Query:MaxSize + messages. Each element in the array is described in <xref linkend="goa-mail-query-struct"/>. + +<table frame='all' id='goa-mail-query-struct'> + <title>Elements in the #org.gnome.OnlineAccounts.Mail.Query:Result structure</title> + <tgroup cols='2' align='left' colsep='1' rowsep='1'> + <tbody> + <row><entry><literal>s</literal> guid</entry><entry>Unique ID</entry></row> + <row><entry><literal>t</literal> date</entry><entry>Date (seconds since the Epoch)</entry></row> + <row><entry><literal>s</literal> from</entry><entry>Name of sender</entry></row> + <row><entry><literal>s</literal> subject</entry><entry>Subject</entry></row> + <row><entry><literal>s</literal> excerpt</entry><entry>Plain-text excerpt</entry></row> + <row><entry><literal>i</literal> flags</entry><entry>Flags (bit 0: unread)</entry></row> + <row><entry><literal>a{sv}</literal> extras</entry><entry>Other information (currently unused)</entry></row> + </tbody> + </tgroup> +</table> + --> + <property name="Result" type="a(stsssia{sv})" access="read"/> + + <!-- NumUnread: + The total number of unread messages (not influenced by the + #org.gnome.OnlineAccounts.Mail.Query:Criteria property) or -1 + if unknown. + --> + <property name="NumUnread" type="i" access="read"/> + + <!-- NumMessages: + The total number of messages (not influenced by the + #org.gnome.OnlineAccounts.Mail.Query:Criteria property) or -1 + if unknown. + --> + <property name="NumMessages" type="i" access="read"/> + + <!-- Connected: + TRUE if currently connected to the server, FALSE if not. + + If FALSE, a client should periodically call the + org.gnome.OnlineAccounts.Mail.Query.Refresh() method when + network connectivity is available. + --> + <property name="Connected" type="b" access="read"/> + + <!-- Refresh: + Forcibly does a server roundtrip and updates e.g. the + #org.gnome.OnlineAccounts.Mail.Query:Result property + + Normally this isn't needed as implementations typically uses + techniques such as <ulink + url="http://en.wikipedia.org/wiki/IMAP_IDLE">IMAP + IDLE</ulink> to keep the current. + + As a side-effect, this also updates the + #org.gnome.OnlineAccounts.Mail.Query:Connected property - + specifically this property might bet se to FALSE (in case + network connectivity was lost) or TRUE (in case network + connectivity was acquired). + + This method won't return until the check is done so it is + appropriate to e.g. show a spinner while the operation is + pending. + --> + <method name="Refresh"/> + + <!-- Close: + Method that can be used to close the query and release all + resources used for it. + --> + <method name="Close"/> + </interface> + </node> diff --git a/doc/goa-docs.xml b/doc/goa-docs.xml index d635870..930d0bb 100644 --- a/doc/goa-docs.xml +++ b/doc/goa-docs.xml @@ -106,6 +106,8 @@ <xi:include href="../src/goa/goa-generated-doc-org.gnome.OnlineAccounts.TwitterAccount.xml"/> <xi:include href="../src/goa/goa-generated-doc-org.gnome.OnlineAccounts.OAuthBased.xml"/> <xi:include href="../src/goa/goa-generated-doc-org.gnome.OnlineAccounts.OAuth2Based.xml"/> + <xi:include href="../src/goa/goa-generated-doc-org.gnome.OnlineAccounts.Mail.xml"/> + <xi:include href="../src/goa/goa-generated-doc-org.gnome.OnlineAccounts.Mail.Query.xml"/> </chapter> </part> @@ -123,6 +125,8 @@ <xi:include href="xml/GoaTwitterAccount.xml"/> <xi:include href="xml/GoaOAuthBased.xml"/> <xi:include href="xml/GoaOAuth2Based.xml"/> + <xi:include href="xml/GoaMail.xml"/> + <xi:include href="xml/GoaMailQuery.xml"/> </part> <part id="ref-backend-library"> @@ -134,6 +138,11 @@ <xi:include href="xml/goabackendfacebookprovider.xml"/> <xi:include href="xml/goabackendyahooprovider.xml"/> <xi:include href="xml/goabackendtwitterprovider.xml"/> + <xi:include href="xml/goabackendimapauth.xml"/> + <xi:include href="xml/goabackendimapauthoauth.xml"/> + <xi:include href="xml/goabackendimapclient.xml"/> + <xi:include href="xml/goabackendimapmessage.xml"/> + <xi:include href="xml/goabackendimapmail.xml"/> </part> <part id="tools-fileformats"> diff --git a/doc/goa-sections.txt b/doc/goa-sections.txt index a210f07..be2ef71 100644 --- a/doc/goa-sections.txt +++ b/doc/goa-sections.txt @@ -55,6 +55,8 @@ goa_object_get_yahoo_account goa_object_get_twitter_account goa_object_get_oauth_based goa_object_get_oauth2_based +goa_object_get_mail +goa_object_get_mail_query goa_object_peek_manager goa_object_peek_account goa_object_peek_google_account @@ -63,6 +65,8 @@ goa_object_peek_yahoo_account goa_object_peek_twitter_account goa_object_peek_oauth_based goa_object_peek_oauth2_based +goa_object_peek_mail +goa_object_peek_mail_query GoaObjectProxy GoaObjectProxyClass goa_object_proxy_new @@ -77,6 +81,8 @@ goa_object_skeleton_set_yahoo_account goa_object_skeleton_set_twitter_account goa_object_skeleton_set_oauth_based goa_object_skeleton_set_oauth2_based +goa_object_skeleton_set_mail +goa_object_skeleton_set_mail_query <SUBSECTION Standard> goa_object_get_type goa_object_proxy_get_type @@ -492,6 +498,7 @@ goa_backend_provider_lookup_credentials goa_backend_provider_lookup_credentials_finish goa_backend_provider_ensure_credentials goa_backend_provider_ensure_credentials_finish +goa_backend_provider_ensure_credentials_sync GOA_BACKEND_PROVIDER_EXTENSION_POINT_NAME goa_backend_provider_get_all goa_backend_provider_get_for_provider_type @@ -549,6 +556,7 @@ goa_backend_oauth_provider_get_identity goa_backend_oauth_provider_get_identity_finish goa_backend_oauth_provider_get_access_token goa_backend_oauth_provider_get_access_token_finish +goa_backend_oauth_provider_get_access_token_sync goa_backend_oauth_provider_get_use_external_browser <SUBSECTION Standard> GOA_BACKEND_OAUTH_PROVIDER @@ -600,3 +608,189 @@ GOA_IS_BACKEND_TWITTER_PROVIDER GOA_TYPE_BACKEND_TWITTER_PROVIDER goa_backend_twitter_provider_get_type </SECTION> + +<SECTION> +<FILE>GoaMail</FILE> +GoaMail +GoaMailIface +goa_mail_interface_info +goa_mail_call_create_query +goa_mail_call_create_query_finish +goa_mail_call_create_query_sync +goa_mail_complete_create_query +GoaMailProxy +GoaMailProxyClass +goa_mail_proxy_new +goa_mail_proxy_new_finish +goa_mail_proxy_new_sync +goa_mail_proxy_new_for_bus +goa_mail_proxy_new_for_bus_finish +goa_mail_proxy_new_for_bus_sync +GoaMailSkeleton +GoaMailSkeletonClass +goa_mail_skeleton_new +<SUBSECTION Standard> +GOA_TYPE_MAIL +GOA_IS_MAIL +GOA_MAIL +GOA_MAIL_GET_IFACE +GOA_TYPE_MAIL_PROXY +GOA_IS_MAIL_PROXY +GOA_IS_MAIL_PROXY_CLASS +GOA_MAIL_PROXY +GOA_MAIL_PROXY_CLASS +GOA_MAIL_PROXY_GET_CLASS +GOA_TYPE_MAIL_SKELETON +GOA_IS_MAIL_SKELETON +GOA_IS_MAIL_SKELETON_CLASS +GOA_MAIL_SKELETON +GOA_MAIL_SKELETON_CLASS +GOA_MAIL_SKELETON_GET_CLASS +GoaMailProxyPrivate +GoaMailSkeletonPrivate +goa_mail_get_type +goa_mail_proxy_get_type +goa_mail_skeleton_get_type +</SECTION> + +<SECTION> +<FILE>GoaMailQuery</FILE> +GoaMailQuery +GoaMailQueryIface +goa_mail_query_interface_info +goa_mail_query_override_properties +goa_mail_query_get_connected +goa_mail_query_get_criteria +goa_mail_query_get_max_size +goa_mail_query_get_num_messages +goa_mail_query_get_num_unread +goa_mail_query_get_result +goa_mail_query_set_connected +goa_mail_query_set_criteria +goa_mail_query_set_max_size +goa_mail_query_set_num_messages +goa_mail_query_set_num_unread +goa_mail_query_set_result +goa_mail_query_call_refresh +goa_mail_query_call_refresh_finish +goa_mail_query_call_refresh_sync +goa_mail_query_complete_refresh +goa_mail_query_call_close +goa_mail_query_call_close_finish +goa_mail_query_call_close_sync +goa_mail_query_complete_close +GoaMailQueryProxy +GoaMailQueryProxyClass +goa_mail_query_proxy_new +goa_mail_query_proxy_new_finish +goa_mail_query_proxy_new_sync +goa_mail_query_proxy_new_for_bus +goa_mail_query_proxy_new_for_bus_finish +goa_mail_query_proxy_new_for_bus_sync +GoaMailQuerySkeleton +GoaMailQuerySkeletonClass +goa_mail_query_skeleton_new +<SUBSECTION Standard> +GOA_TYPE_MAIL_QUERY +GOA_IS_MAIL_QUERY +GOA_MAIL_QUERY +GOA_MAIL_QUERY_GET_IFACE +GOA_TYPE_MAIL_QUERY_PROXY +GOA_IS_MAIL_QUERY_PROXY +GOA_IS_MAIL_QUERY_PROXY_CLASS +GOA_MAIL_QUERY_PROXY +GOA_MAIL_QUERY_PROXY_CLASS +GOA_MAIL_QUERY_PROXY_GET_CLASS +GOA_TYPE_MAIL_QUERY_SKELETON +GOA_IS_MAIL_QUERY_SKELETON +GOA_IS_MAIL_QUERY_SKELETON_CLASS +GOA_MAIL_QUERY_SKELETON +GOA_MAIL_QUERY_SKELETON_CLASS +GOA_MAIL_QUERY_SKELETON_GET_CLASS +GoaMailQueryProxyPrivate +GoaMailQuerySkeletonPrivate +goa_mail_query_get_type +goa_mail_query_proxy_get_type +goa_mail_query_skeleton_get_type +</SECTION> + +<SECTION> +<FILE>goabackendimapclient</FILE> +GoaBackendImapClient +goa_backend_imap_client_new +goa_backend_imap_client_new_finish +goa_backend_imap_client_new_sync +goa_backend_imap_client_get_messages +goa_backend_imap_client_get_num_messages +goa_backend_imap_client_get_num_unread +goa_backend_imap_client_refresh +goa_backend_imap_client_refresh_finish +goa_backend_imap_client_refresh_sync +goa_backend_imap_client_get_closed +goa_backend_imap_client_close +<SUBSECTION Standard> +GOA_TYPE_BACKEND_IMAP_MESSAGE_FLAGS +goa_backend_imap_message_flags_get_type +GOA_BACKEND_IMAP_CLIENT +GOA_IS_BACKEND_IMAP_CLIENT +GOA_TYPE_BACKEND_IMAP_CLIENT +goa_backend_imap_client_get_type +</SECTION> + +<SECTION> +<FILE>goabackendimapmessage</FILE> +GoaBackendImapMessage +GoaBackendImapMessageFlags +goa_backend_imap_message_ref +goa_backend_imap_message_unref +goa_backend_imap_message_get_uid +goa_backend_imap_message_get_internal_date +goa_backend_imap_message_get_flags +goa_backend_imap_message_get_excerpt +goa_backend_imap_message_get_headers +goa_backend_imap_message_lookup_header +<SUBSECTION Standard> +GOA_TYPE_BACKEND_IMAP_MESSAGE +goa_backend_imap_message_get_type +goa_backend_imap_message_compare_seqnum_reverse +goa_backend_imap_message_flags_from_strv +goa_backend_imap_message_new +</SECTION> + +<SECTION> +<FILE>goabackendimapmail</FILE> +GoaBackendImapMail +goa_backend_imap_mail_new +<SUBSECTION Standard> +GOA_BACKEND_IMAP_MAIL +GOA_IS_BACKEND_IMAP_MAIL +GOA_TYPE_BACKEND_IMAP_MAIL +goa_backend_imap_mail_get_type +</SECTION> + +<SECTION> +<FILE>goabackendimapauth</FILE> +GoaBackendImapAuth +GoaBackendImapAuthClass +goa_backend_imap_auth_run_sync +<SUBSECTION Standard> +GoaBackendImapAuthPrivate +GOA_BACKEND_IMAP_AUTH +GOA_IS_BACKEND_IMAP_AUTH +GOA_TYPE_BACKEND_IMAP_AUTH +GOA_BACKEND_IMAP_AUTH_CLASS +GOA_IS_BACKEND_IMAP_AUTH_CLASS +GOA_BACKEND_IMAP_AUTH_GET_CLASS +goa_backend_imap_auth_get_type +</SECTION> + +<SECTION> +<FILE>goabackendimapauthoauth</FILE> +GoaBackendImapAuthOAuth +goa_backend_imap_auth_oauth_new +<SUBSECTION Standard> +GOA_BACKEND_IMAP_AUTH_OAUTH +GOA_IS_BACKEND_IMAP_AUTH_OAUTH +GOA_TYPE_BACKEND_IMAP_AUTH_OAUTH +goa_backend_imap_auth_oauth_get_type +</SECTION> diff --git a/doc/goa.types b/doc/goa.types index b9f0b2a..aa069c6 100644 --- a/doc/goa.types +++ b/doc/goa.types @@ -30,3 +30,14 @@ goa_object_proxy_get_type goa_object_skeleton_get_type goa_oauth_based_get_type goa_oauth2_based_get_type +goa_mail_get_type +goa_mail_proxy_get_type +goa_mail_skeleton_get_type +goa_mail_query_get_type +goa_mail_query_proxy_get_type +goa_mail_query_skeleton_get_type +goa_backend_imap_message_get_type +goa_backend_imap_auth_get_type +goa_backend_imap_auth_oauth_get_type +goa_backend_imap_client_get_type +goa_backend_imap_mail_get_type diff --git a/src/Makefile.am b/src/Makefile.am index 03c88ef..6027402 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,4 +1,4 @@ NULL = -SUBDIRS = goa daemon panel +SUBDIRS = goa daemon panel examples diff --git a/src/examples/Makefile.am b/src/examples/Makefile.am new file mode 100644 index 0000000..a3135b3 --- /dev/null +++ b/src/examples/Makefile.am @@ -0,0 +1,34 @@ + +NULL = + +INCLUDES = \ + -I$(top_builddir)/src -I$(top_srcdir)/src \ + -DPACKAGE_LIBEXEC_DIR=\""$(libexecdir)"\" \ + -DPACKAGE_SYSCONF_DIR=\""$(sysconfdir)"\" \ + -DPACKAGE_DATA_DIR=\""$(datadir)"\" \ + -DPACKAGE_BIN_DIR=\""$(bindir)"\" \ + -DPACKAGE_LOCALSTATE_DIR=\""$(localstatedir)"\" \ + -DPACKAGE_LOCALE_DIR=\""$(localedir)"\" \ + -DPACKAGE_LIB_DIR=\""$(libdir)"\" \ + -D_POSIX_PTHREAD_SEMANTICS -D_REENTRANT \ + -DGOA_API_IS_SUBJECT_TO_CHANGE \ + $(WARN_CFLAGS) \ + $(NULL) + +noinst_PROGRAMS = mail-query + +mail_query_SOURCES = \ + mail-query.c \ + $(NULL) + +mail_query_CFLAGS = \ + $(GLIB_CFLAGS) \ + $(NULL) + +mail_query_LDADD = \ + $(GLIB_LIBS) \ + $(top_builddir)/src/goa/libgoa.la \ + $(NULL) + +clean-local : + rm -f *~ diff --git a/src/examples/mail-query.c b/src/examples/mail-query.c new file mode 100644 index 0000000..81298c5 --- /dev/null +++ b/src/examples/mail-query.c @@ -0,0 +1,273 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#include "config.h" +#include <glib/gi18n.h> +#include <stdlib.h> + +#include "goa/goa.h" + +static gchar * +fix_up_excerpt (const gchar *s, + gsize max_len) +{ + GString *str; + guint n; + + str = g_string_new (NULL); + for (n = 0; s[n] != '\0'; n++) + { + gint c = s[n]; + if (c == '\r') + g_string_append_c (str, ' '); + else if (c == '\n') + g_string_append_c (str, ' '); + else + g_string_append_c (str, c); + if (str->len > max_len) + break; + } + if (s[n] != '\0') + g_string_append (str, "..."); + + return g_string_free (str, FALSE); +} + +static void +print_result (GoaMailQuery *query) +{ + GVariantIter iter; + GVariant *result; + const gchar *guid; + gint64 date; + const gchar *from; + const gchar *subject; + const gchar *excerpt; + gint flags; + guint n; + + result = goa_mail_query_get_result (query); + if (result != NULL) + { + g_print ("Mailbox: NumMessages=%d NumUnread=%d Connected=%d\n" + "Query: NumResults=%d MaxSize=%d\n" + "===========================================\n", + goa_mail_query_get_num_messages (query), + goa_mail_query_get_num_unread (query), + goa_mail_query_get_connected (query), + (gint) g_variant_n_children (result), + goa_mail_query_get_max_size (query)); + if (g_variant_n_children (result) == 0) + g_print ("\n"); + + n = 0; + g_variant_iter_init (&iter, result); + while (g_variant_iter_next (&iter, + "(st&s&s&si@a{sv})", + &guid, &date, &from, &subject, &excerpt, &flags, NULL)) + { + gchar *excerpt_fixed_up; + excerpt_fixed_up = fix_up_excerpt (excerpt, 50); + g_print ("Message %d: guid %s, flags %d\n" + " Date: %" G_GINT64_FORMAT "\n" + " From: %s\n" + " Subject: %s\n" + " Excerpt: %s\n" + "\n", + n, guid, flags, + date, + from, + subject, + excerpt_fixed_up); + g_free (excerpt_fixed_up); + n++; + } + } + else + { + g_print ("result is NULL so proxy must be stale\n" + "=======================================\n" + "\n"); + } +} + +static void +on_notify (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + GoaMailQuery *query = GOA_MAIL_QUERY (object); + print_result (query); +} + +int +main (int argc, + char *argv[]) +{ + gint ret; + GMainLoop *loop; + GError *error; + GoaClient *client; + GList *accounts; + GList *l; + GoaMailQuery *query; + gchar *opt_account; + gchar *opt_query; + gint opt_size; + GOptionEntry opt_entries[] = + { + { "account", 'a', 0, G_OPTION_ARG_STRING, &opt_account, "The account to do a query for", NULL }, + { "query", 'q', 0, G_OPTION_ARG_STRING, &opt_query, "The query to perform (blank if not set)", NULL }, + { "size", 's', 0, G_OPTION_ARG_INT, &opt_size, "The size of the query (5 if not set)", NULL }, + { NULL} + }; + GOptionContext *opt_context; + gchar *query_object_path; + + ret = 1; + client = NULL; + accounts = NULL; + query = NULL; + opt_account = NULL; + opt_query = NULL; + opt_size = 5; + query_object_path = NULL; + + g_type_init (); + + opt_context = g_option_context_new ("goa mail query example"); + error = NULL; + g_option_context_add_main_entries (opt_context, opt_entries, NULL); + if (!g_option_context_parse (opt_context, &argc, &argv, &error)) + { + g_printerr ("Error parsing options: %s\n", error->message); + g_error_free (error); + goto out; + } + if (opt_account == NULL) + { + g_printerr ("Incorrect usage, try --help.\n"); + goto out; + } + + if (opt_query == NULL) + opt_query = g_strdup(""); + + loop = g_main_loop_new (NULL, FALSE); + + error = NULL; + client = goa_client_new_sync (NULL, /* GCancellable */ + &error); + if (client == NULL) + { + g_printerr ("Error creating a GOA client: %s (%s, %d)\n", + error->message, g_quark_to_string (error->domain), error->code); + g_error_free (error); + goto out; + } + + accounts = goa_client_get_accounts (client); + for (l = accounts; l != NULL; l = l->next) + { + GoaObject *object = GOA_OBJECT (l->data); + GoaAccount *account; + GoaMail *mail; + + account = goa_object_peek_account (object); + if (account == NULL) + continue; + + if (!(g_strcmp0 (goa_account_get_id (account), opt_account) == 0 || + g_strcmp0 (g_dbus_object_get_object_path (G_DBUS_OBJECT (object)), opt_account) == 0)) + continue; + + mail = goa_object_peek_mail (object); + if (mail == NULL) + { + g_printerr ("Given account does not implement the Mail interface\n"); + goto out; + } + + /* Start querying account */ + g_print ("Querying mail for %s with query string \"%s\"\n", + goa_account_get_id (account), + opt_query); + error = NULL; + if (!goa_mail_call_create_query_sync (mail, + opt_query, /* query string */ + opt_size, /* return no more than N messages */ + &query_object_path, + NULL, /* GCancellable */ + &error)) + { + g_printerr ("Error creating mail query: %s (%s, %d)\n", + error->message, g_quark_to_string (error->domain), error->code); + g_error_free (error); + goto out; + } + + error = NULL; + query = goa_mail_query_proxy_new_for_bus_sync (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "org.gnome.OnlineAccounts", + query_object_path, + NULL, /* GCancellable */ + &error); + if (query == NULL) + { + g_printerr ("Error creating query proxy: %s (%s, %d)\n", + error->message, g_quark_to_string (error->domain), error->code); + g_error_free (error); + goto out; + } + + print_result (query); + g_signal_connect (query, + "notify", + G_CALLBACK (on_notify), + NULL); + break; + } + + if (query_object_path == NULL) + { + g_printerr ("Didn't find requested account.\n"); + goto out; + } + + g_main_loop_run (loop); + + ret = 0; + + out: + if (query != NULL) + g_object_unref (query); + if (client != NULL) + g_object_unref (client); + g_free (query_object_path); + g_list_foreach (accounts, (GFunc) g_object_unref, NULL); + g_list_free (accounts); + g_free (opt_account); + g_free (opt_query); + g_option_context_free (opt_context); + return ret; +} diff --git a/src/goa/Makefile.am b/src/goa/Makefile.am index 2a62c9c..2d05710 100644 --- a/src/goa/Makefile.am +++ b/src/goa/Makefile.am @@ -1,5 +1,6 @@ NULL = +CLEANFILES = INCLUDES = \ -I$(top_builddir)/src -I$(top_srcdir)/src \ @@ -41,8 +42,19 @@ goaenumtypes.c: goaenums.h goaenumtypes.c.template cd $(srcdir) && glib-mkenums --template goaenumtypes.c.template goaenums.h ) > \ goaenumtypes.c.tmp && mv goaenumtypes.c.tmp goaenumtypes.c +goabackendenumtypes.h: goabackendenums.h goabackendenumtypes.h.template + ( top_builddir=`cd $(top_builddir) && pwd`; \ + cd $(srcdir) && glib-mkenums --template goabackendenumtypes.h.template goabackendenums.h ) > \ + goabackendenumtypes.h.tmp && mv goabackendenumtypes.h.tmp goabackendenumtypes.h + +goabackendenumtypes.c: goabackendenums.h goabackendenumtypes.c.template + ( top_builddir=`cd $(top_builddir) && pwd`; \ + cd $(srcdir) && glib-mkenums --template goabackendenumtypes.c.template goabackendenums.h ) > \ + goabackendenumtypes.c.tmp && mv goabackendenumtypes.c.tmp goabackendenumtypes.c + enum_built_sources = \ goaenumtypes.h goaenumtypes.c \ + goabackendenumtypes.h goabackendenumtypes.c \ $(NULL) # ---------------------------------------------------------------------------------------------------- @@ -79,6 +91,38 @@ libgoa_la_LIBADD = \ $(GLIB_LIBS) \ $(NULL) +if HAVE_INTROSPECTION +girdir = $(INTROSPECTION_GIRDIR) +gir_DATA = Goa-1.0.gir + +typelibsdir = $(INTROSPECTION_TYPELIBDIR) +typelibs_DATA = Goa-1.0.typelib + +Goa-1.0.gir: libgoa.la $(INTROSPECTION_SCANNER) Makefile.am + $(INTROSPECTION_SCANNER) -v \ + --warn-all \ + --namespace Goa \ + --nsversion=1.0 \ + --include=Gio-2.0 \ + --library=goa \ + --output $@ \ + --pkg=glib-2.0 \ + --pkg=gobject-2.0 \ + --pkg=gio-2.0 \ + --libtool=$(top_builddir)/libtool \ + --c-include='goa/goa.h' \ + -I$(top_srcdir)/src \ + -DGOA_COMPILATION \ + $(libgoa_la_SOURCES) \ + $(NULL) + +Goa-1.0.typelib: Goa-1.0.gir $(INTROSPECTION_COMPILER) + $(INTROSPECTION_COMPILER) $< -o $@ + +CLEANFILES += $(gir_DATA) $(typelibs_DATA) + +endif # HAVE_INTROSPECTION + # ---------------------------------------------------------------------------------------------------- lib_LTLIBRARIES += libgoa-backend.la @@ -89,6 +133,18 @@ libgoa_backend_la_HEADERS = \ goabackend.h \ goabackendtypes.h \ goabackendprovider.h \ + goabackendoauthprovider.h \ + goabackendoauth2provider.h \ + goabackendgoogleprovider.h \ + goabackendfacebookprovider.h \ + goabackendyahooprovider.h \ + goabackendtwitterprovider.h \ + goabackendimapauth.h \ + goabackendimapauthoauth.h \ + goabackendimapclient.h \ + goabackendimapmessage.h \ + goabackendenums.h \ + goabackendenumtypes.h \ $(NULL) libgoa_backend_la_SOURCES = \ @@ -101,6 +157,13 @@ libgoa_backend_la_SOURCES = \ goabackendfacebookprovider.h goabackendfacebookprovider.c \ goabackendyahooprovider.h goabackendyahooprovider.c \ goabackendtwitterprovider.h goabackendtwitterprovider.c \ + goabackendimapprivate.h \ + goabackendimapauth.h goabackendimapauth.c \ + goabackendimapauthoauth.h goabackendimapauthoauth.c \ + goabackendimapclient.h goabackendimapclient.c \ + goabackendimapmessage.h goabackendimapmessage.c \ + goabackendimapmail.h goabackendimapmail.c \ + goabackendenumtypes.h goabackendenumtypes.c \ $(NULL) libgoa_backend_la_CFLAGS = \ diff --git a/src/goa/goabackend.h b/src/goa/goabackend.h index 9ebad88..f7e1dce 100644 --- a/src/goa/goabackend.h +++ b/src/goa/goabackend.h @@ -35,6 +35,12 @@ #include <goa/goabackendgoogleprovider.h> #include <goa/goabackendfacebookprovider.h> #include <goa/goabackendyahooprovider.h> +#include <goa/goabackendtwitterprovider.h> +#include <goa/goabackendimapauth.h> +#include <goa/goabackendimapauthoauth.h> +#include <goa/goabackendimapclient.h> +#include <goa/goabackendimapmessage.h> +#include <goa/goabackendimapmail.h> #undef __GOA_BACKEND_INSIDE_GOA_BACKEND_H__ #endif /* __GOA_BACKEND_H__ */ diff --git a/src/goa/goabackendenums.h b/src/goa/goabackendenums.h new file mode 100644 index 0000000..2b08547 --- /dev/null +++ b/src/goa/goabackendenums.h @@ -0,0 +1,59 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#if !defined (__GOA_BACKEND_INSIDE_GOA_BACKEND_H__) && !defined (GOA_BACKEND_COMPILATION) +#error "Only <goa/goabackend.h> can be included directly." +#endif + +#ifndef __GOA_BACKEND_ENUMS_H__ +#define __GOA_BACKEND_ENUMS_H__ + +#include <gio/gio.h> + +G_BEGIN_DECLS + +/** + * GoaBackendImapMessageFlags: + * @GOA_BACKEND_IMAP_MESSAGE_FLAGS_NONE: No flags set. + * @GOA_BACKEND_IMAP_MESSAGE_FLAGS_SEEN: Corresponds to the <literal>\Seen</literal> flag. + * @GOA_BACKEND_IMAP_MESSAGE_FLAGS_ANSWERED: Corresponds to the <literal>\Answered</literal> flag. + * @GOA_BACKEND_IMAP_MESSAGE_FLAGS_FLAGGED: Corresponds to the <literal>\Flagged</literal> flag. + * @GOA_BACKEND_IMAP_MESSAGE_FLAGS_DELETED: Corresponds to the <literal>\Deleted</literal> flag. + * @GOA_BACKEND_IMAP_MESSAGE_FLAGS_DRAFT: Corresponds to the <literal>\Draft</literal> flag. + * @GOA_BACKEND_IMAP_MESSAGE_FLAGS_RECENT: Corresponds to the <literal>\Recent</literal> flag. + * + * Flag enumeration corresponding to <ulink url="http://tools.ietf.org/html/rfc3501#section-2.3.2">IMAP flags</ulink>. + */ +typedef enum +{ + GOA_BACKEND_IMAP_MESSAGE_FLAGS_NONE = 0, + GOA_BACKEND_IMAP_MESSAGE_FLAGS_SEEN = (1<<0), + GOA_BACKEND_IMAP_MESSAGE_FLAGS_ANSWERED = (1<<1), + GOA_BACKEND_IMAP_MESSAGE_FLAGS_FLAGGED = (1<<2), + GOA_BACKEND_IMAP_MESSAGE_FLAGS_DELETED = (1<<3), + GOA_BACKEND_IMAP_MESSAGE_FLAGS_DRAFT = (1<<4), + GOA_BACKEND_IMAP_MESSAGE_FLAGS_RECENT = (1<<5) +} GoaBackendImapMessageFlags; + +G_END_DECLS + +#endif /* __GOA_BACKEND_ENUMS_H__ */ diff --git a/src/goa/goabackendenumtypes.c.template b/src/goa/goabackendenumtypes.c.template new file mode 100644 index 0000000..7a76459 --- /dev/null +++ b/src/goa/goabackendenumtypes.c.template @@ -0,0 +1,40 @@ +/*** BEGIN file-header ***/ +#include "goabackendenums.h" +#include "goabackendenumtypes.h" + +/*** END file-header ***/ + +/*** BEGIN file-production ***/ +/* enumerations from "@filename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType +@enum_name@_get_type (void) +{ + static volatile gsize g_define_type_id__volatile = 0; + + if (g_once_init_enter (&g_define_type_id__volatile)) + { + static const G@Type@Value values[] = { +/*** END value-header ***/ + +/*** BEGIN value-production ***/ + { @VALUENAME@, "@VALUENAME@", "@valuenick@" }, +/*** END value-production ***/ + +/*** BEGIN value-tail ***/ + { 0, NULL, NULL } + }; + GType g_define_type_id = + g_@type@_register_static (g_intern_static_string ("@EnumName@"), values); + g_once_init_leave (&g_define_type_id__volatile, g_define_type_id); + } + + return g_define_type_id__volatile; +} + +/*** END value-tail ***/ + +/*** BEGIN file-tail ***/ +/*** END file-tail ***/ diff --git a/src/goa/goabackendenumtypes.h.template b/src/goa/goabackendenumtypes.h.template new file mode 100644 index 0000000..7321076 --- /dev/null +++ b/src/goa/goabackendenumtypes.h.template @@ -0,0 +1,24 @@ +/*** BEGIN file-header ***/ +#ifndef __GOA_BACKEND_ENUM_TYPES_H__ +#define __GOA_BACKEND_ENUM_TYPES_H__ + +#include <glib-object.h> + +G_BEGIN_DECLS +/*** END file-header ***/ + +/*** BEGIN file-production ***/ + +/* enumerations from "@filename@" */ +/*** END file-production ***/ + +/*** BEGIN value-header ***/ +GType @enum_name@_get_type (void) G_GNUC_CONST; +#define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) +/*** END value-header ***/ + +/*** BEGIN file-tail ***/ +G_END_DECLS + +#endif /* __GOA_BACKEND_ENUM_TYPES_H__ */ +/*** END file-tail ***/ diff --git a/src/goa/goabackendgoogleprovider.c b/src/goa/goabackendgoogleprovider.c index 95353f0..4b78749 100644 --- a/src/goa/goabackendgoogleprovider.c +++ b/src/goa/goabackendgoogleprovider.c @@ -30,6 +30,9 @@ #include "goabackendoauthprovider.h" #include "goabackendgoogleprovider.h" +#include "goabackendimapmail.h" +#include "goabackendimapauthoauth.h" + /** * GoaBackendGoogleProvider: * @@ -328,12 +331,14 @@ goa_backend_google_provider_build_object (GoaBackendProvider *provider, { GoaAccount *account; GoaGoogleAccount *google_account; + GoaMail *mail; gboolean ret; gchar *email_address; email_address = NULL; account = NULL; google_account = NULL; + mail = NULL; ret = FALSE; /* Chain up */ @@ -366,10 +371,27 @@ goa_backend_google_provider_build_object (GoaBackendProvider *provider, goa_google_account_set_email_address (google_account, email_address); + mail = goa_object_get_mail (GOA_OBJECT (object)); + if (mail == NULL) + { + GoaBackendImapAuth *auth; + gchar *request_uri; + request_uri = g_strdup_printf ("https://mail.google.com/mail/b/%s/imap/", email_address); + auth = goa_backend_imap_auth_oauth_new (GOA_BACKEND_OAUTH_PROVIDER (provider), + GOA_OBJECT (object), + request_uri); + mail = goa_backend_imap_mail_new ("imap.gmail.com", TRUE, auth); + goa_object_skeleton_set_mail (object, mail); + g_object_unref (auth); + g_free (request_uri); + } + ret = TRUE; out: g_free (email_address); + if (mail != NULL) + g_object_unref (mail); if (google_account != NULL) g_object_unref (google_account); if (account != NULL) @@ -415,3 +437,5 @@ goa_backend_google_provider_class_init (GoaBackendGoogleProviderClass *klass) oauth_class->get_callback_uri = get_callback_uri; oauth_class->get_use_external_browser = get_use_external_browser; } + +/* ---------------------------------------------------------------------------------------------------- */ diff --git a/src/goa/goabackendimapauth.c b/src/goa/goabackendimapauth.c new file mode 100644 index 0000000..2f8f02b --- /dev/null +++ b/src/goa/goabackendimapauth.c @@ -0,0 +1,84 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#include "config.h" +#include <glib/gi18n-lib.h> +#include <stdlib.h> + +#include "goabackendimapauth.h" + +/** + * SECTION:goabackendimapauth + * @title: GoaBackendImapAuth + * @short_description: Helper type for authenticating IMAP connections + * + * #GoaBackendImapAuth is an abstract type used for authenticating + * IMAP connections. See #GoaBackendImapAuthOAuth for a concrete + * implementation. + */ + +G_DEFINE_ABSTRACT_TYPE (GoaBackendImapAuth, goa_backend_imap_auth, G_TYPE_OBJECT); + +/* ---------------------------------------------------------------------------------------------------- */ + +static void +goa_backend_imap_auth_init (GoaBackendImapAuth *client) +{ +} + +static void +goa_backend_imap_auth_class_init (GoaBackendImapAuthClass *klass) +{ +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/** + * goa_backend_imap_auth_run_sync: + * @auth: A #GoaBackendImapAuth. + * @input: A valid #GDataInputStream. + * @output: A valid #GDataOutputStream. + * @cancellable: (allow-none): A #GCancellable or %NULL. + * @error: Return location for error or %NULL. + * + * Authenticates the IMAP connection represented by @input and + * @output. This method blocks the calling thread until authentication + * is done. + * + * Returns: %TRUE if authentication succeeded, %FALSE if @error is + * set. + */ +gboolean +goa_backend_imap_auth_run_sync (GoaBackendImapAuth *auth, + GDataInputStream *input, + GDataOutputStream *output, + GCancellable *cancellable, + GError **error) +{ + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_AUTH (auth), FALSE); + g_return_val_if_fail (G_IS_DATA_INPUT_STREAM (input), FALSE); + g_return_val_if_fail (G_IS_DATA_OUTPUT_STREAM (output), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + return GOA_BACKEND_IMAP_AUTH_GET_CLASS (auth)->run_sync (auth, input, output, cancellable, error); +} + +/* ---------------------------------------------------------------------------------------------------- */ diff --git a/src/goa/goabackendimapauth.h b/src/goa/goabackendimapauth.h new file mode 100644 index 0000000..37ac521 --- /dev/null +++ b/src/goa/goabackendimapauth.h @@ -0,0 +1,88 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#if !defined (__GOA_BACKEND_INSIDE_GOA_BACKEND_H__) && !defined (GOA_BACKEND_COMPILATION) +#error "Only <goa/goabackend.h> can be included directly." +#endif + +#ifndef __GOA_BACKEND_IMAP_AUTH_H__ +#define __GOA_BACKEND_IMAP_AUTH_H__ + +#include <goa/goabackendtypes.h> + +G_BEGIN_DECLS + +#define GOA_TYPE_BACKEND_IMAP_AUTH (goa_backend_imap_auth_get_type ()) +#define GOA_BACKEND_IMAP_AUTH(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOA_TYPE_BACKEND_IMAP_AUTH, GoaBackendImapAuth)) +#define GOA_BACKEND_IMAP_AUTH_CLASS(k) (G_TYPE_CHECK_CLASS_CAST ((k), GOA_TYPE_BACKEND_IMAP_AUTH, GoaBackendImapAuthClass)) +#define GOA_BACKEND_IMAP_AUTH_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), GOA_TYPE_BACKEND_IMAP_AUTH, GoaBackendImapAuthClass)) +#define GOA_IS_BACKEND_IMAP_AUTH(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOA_TYPE_BACKEND_IMAP_AUTH)) +#define GOA_IS_BACKEND_IMAP_AUTH_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE ((k), GOA_TYPE_BACKEND_IMAP_AUTH)) + +struct _GoaBackendImapAuthClass; +struct _GoaBackendImapAuthPrivate; +typedef struct _GoaBackendImapAuthClass GoaBackendImapAuthClass; +typedef struct _GoaBackendImapAuthPrivate GoaBackendImapAuthPrivate; + +/** + * GoaBackendImapAuth: + * + * The #GoaBackendImapAuth structure contains only private data and + * should only be accessed using the provided API. + */ +struct _GoaBackendImapAuth +{ + /*< private >*/ + GObject parent_instance; + GoaBackendImapAuthPrivate *priv; +}; + +/** + * GoaBackendImapAuthClass: + * @parent_class: The parent class + * @run_sync: Virtual function for the goa_backend_imap_auth_run_sync() method. + * + * Class structure for #GoaBackendImapAuth. + */ +struct _GoaBackendImapAuthClass +{ + GObjectClass parent_class; + gboolean (*run_sync) (GoaBackendImapAuth *auth, + GDataInputStream *input, + GDataOutputStream *output, + GCancellable *cancellable, + GError **error); + /*< private >*/ + /* Padding for future expansion */ + gpointer goa_reserved[8]; +}; + +GType goa_backend_imap_auth_get_type (void) G_GNUC_CONST; +gboolean goa_backend_imap_auth_run_sync (GoaBackendImapAuth *auth, + GDataInputStream *input, + GDataOutputStream *output, + GCancellable *cancellable, + GError **error); + +G_END_DECLS + +#endif /* __GOA_BACKEND_IMAP_AUTH_H__ */ diff --git a/src/goa/goabackendimapauthoauth.c b/src/goa/goabackendimapauthoauth.c new file mode 100644 index 0000000..0abb9f8 --- /dev/null +++ b/src/goa/goabackendimapauthoauth.c @@ -0,0 +1,572 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#include "config.h" +#include <glib/gi18n-lib.h> +#include <stdlib.h> + +#include "goabackendimapauth.h" +#include "goabackendimapauthoauth.h" +#include "goabackendoauthprovider.h" + +/** + * SECTION:goabackendimapauthoauth + * @title: GoaBackendImapAuthOAuth + * @short_description: XOAUTH authentication method for IMAP + * + * #GoaBackendImapAuthOAuth implements the <ulink + * url="http://code.google.com/apis/gmail/oauth/protocol.html">XOAUTH</ulink> + * authentication method for IMAP. + */ + +/** + * GoaBackendImapAuthOAuth: + * + * The #GoaBackendImapAuthOAuth structure contains only private data + * and should only be accessed using the provided API. + */ +struct _GoaBackendImapAuthOAuth +{ + GoaBackendImapAuth parent_instance; + + GoaBackendOAuthProvider *provider; + GoaObject *object; + gchar *request_uri; +}; + +typedef struct +{ + GoaBackendImapAuthClass parent_class; + +} GoaBackendImapAuthOAuthClass; + +enum +{ + PROP_0, + PROP_PROVIDER, + PROP_OBJECT, + PROP_REQUEST_URI +}; + +static gboolean goa_backend_imap_auth_oauth_run_sync (GoaBackendImapAuth *_auth, + GDataInputStream *input, + GDataOutputStream *output, + GCancellable *cancellable, + GError **error); + +G_DEFINE_TYPE (GoaBackendImapAuthOAuth, goa_backend_imap_auth_oauth, GOA_TYPE_BACKEND_IMAP_AUTH); + +/* ---------------------------------------------------------------------------------------------------- */ + +static void +goa_backend_imap_auth_oauth_finalize (GObject *object) +{ + GoaBackendImapAuthOAuth *auth = GOA_BACKEND_IMAP_AUTH_OAUTH (object); + + g_object_unref (auth->provider); + g_object_unref (auth->object); + + G_OBJECT_CLASS (goa_backend_imap_auth_oauth_parent_class)->finalize (object); +} + +static void +goa_backend_imap_auth_oauth_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GoaBackendImapAuthOAuth *auth = GOA_BACKEND_IMAP_AUTH_OAUTH (object); + + switch (prop_id) + { + case PROP_PROVIDER: + g_value_set_object (value, auth->provider); + break; + + case PROP_OBJECT: + g_value_set_object (value, auth->object); + break; + + case PROP_REQUEST_URI: + g_value_set_string (value, auth->request_uri); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +goa_backend_imap_auth_oauth_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GoaBackendImapAuthOAuth *auth = GOA_BACKEND_IMAP_AUTH_OAUTH (object); + + switch (prop_id) + { + case PROP_PROVIDER: + auth->provider = g_value_dup_object (value); + break; + + case PROP_OBJECT: + auth->object = g_value_dup_object (value); + break; + + case PROP_REQUEST_URI: + auth->request_uri = g_value_dup_string (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/* ---------------------------------------------------------------------------------------------------- */ + + +static void +goa_backend_imap_auth_oauth_init (GoaBackendImapAuthOAuth *client) +{ +} + +static void +goa_backend_imap_auth_oauth_class_init (GoaBackendImapAuthOAuthClass *klass) +{ + GObjectClass *gobject_class; + GoaBackendImapAuthClass *auth_class; + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = goa_backend_imap_auth_oauth_finalize; + gobject_class->get_property = goa_backend_imap_auth_oauth_get_property; + gobject_class->set_property = goa_backend_imap_auth_oauth_set_property; + + auth_class = GOA_BACKEND_IMAP_AUTH_CLASS (klass); + auth_class->run_sync = goa_backend_imap_auth_oauth_run_sync; + + /** + * GoaBackendImapAuthOAuth:provider: + * + * The #GoaBackendOAuthProvider object to use when calculating the XOAUTH mechanism parameter. + */ + g_object_class_install_property (gobject_class, + PROP_PROVIDER, + g_param_spec_object ("provider", + "provider", + "provider", + GOA_TYPE_BACKEND_OAUTH_PROVIDER, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * GoaBackendImapAuthOAuth:object: + * + * The #GoaObject object to use when calculating the XOAUTH mechanism parameter. + */ + g_object_class_install_property (gobject_class, + PROP_OBJECT, + g_param_spec_object ("object", + "object", + "object", + GOA_TYPE_OBJECT, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * GoaBackendImapAuthOAuth:request-uri: + * + * The request URI to use when calculating the XOAUTH mechanism parameter. + */ + g_object_class_install_property (gobject_class, + PROP_REQUEST_URI, + g_param_spec_string ("request-uri", + "request-uri", + "request-uri", + NULL, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/** + * goa_backend_imap_auth_oauth_new: + * @provider: A #GoaBackendOAuthProvider. + * @object: An account object. + * @request_uri: The request URI to use. + * + * Creates a new #GoaBackendImapAuth to be used for XOAUTH authentication. + * + * Returns: (type GoaBackendImapAuthOAuth): A #GoaBackendImapAuthOAuth. Free with g_object_unref(). + */ +GoaBackendImapAuth * +goa_backend_imap_auth_oauth_new (GoaBackendOAuthProvider *provider, + GoaObject *object, + const gchar *request_uri) +{ + g_return_val_if_fail (GOA_IS_BACKEND_OAUTH_PROVIDER (provider), NULL); + g_return_val_if_fail (GOA_IS_OBJECT (object), NULL); + return GOA_BACKEND_IMAP_AUTH (g_object_new (GOA_TYPE_BACKEND_IMAP_AUTH_OAUTH, + "provider", provider, + "object", object, + "request-uri", request_uri, + NULL)); +} + +/* ---------------------------------------------------------------------------------------------------- */ + + +#include <libsoup/soup.h> + +#define OAUTH_ENCODE_STRING(x_) (x_ ? soup_uri_encode( (x_), "!$&'()*+,;=@") : g_strdup ("")) + +#define SHA1_BLOCK_SIZE 64 +#define SHA1_LENGTH 20 + +/* + * hmac_sha1: + * @key: The key + * @message: The message + * + * Given the key and message, compute the HMAC-SHA1 hash and return the base-64 + * encoding of it. This is very geared towards OAuth, and as such both key and + * message must be NULL-terminated strings, and the result is base-64 encoded. + */ +static char * +hmac_sha1 (const char *key, const char *message) +{ + GChecksum *checksum; + char *real_key; + guchar ipad[SHA1_BLOCK_SIZE]; + guchar opad[SHA1_BLOCK_SIZE]; + guchar inner[SHA1_LENGTH]; + guchar digest[SHA1_LENGTH]; + gsize key_length, inner_length, digest_length; + int i; + + g_return_val_if_fail (key, NULL); + g_return_val_if_fail (message, NULL); + + checksum = g_checksum_new (G_CHECKSUM_SHA1); + + /* If the key is longer than the block size, hash it first */ + if (strlen (key) > SHA1_BLOCK_SIZE) { + guchar new_key[SHA1_LENGTH]; + + key_length = sizeof (new_key); + + g_checksum_update (checksum, (guchar*)key, strlen (key)); + g_checksum_get_digest (checksum, new_key, &key_length); + g_checksum_reset (checksum); + + real_key = g_memdup (new_key, key_length); + } else { + real_key = g_strdup (key); + key_length = strlen (key); + } + + /* Sanity check the length */ + g_assert (key_length <= SHA1_BLOCK_SIZE); + + /* Protect against use of the provided key by NULLing it */ + key = NULL; + + /* Stage 1 */ + memset (ipad, 0, sizeof (ipad)); + memset (opad, 0, sizeof (opad)); + + memcpy (ipad, real_key, key_length); + memcpy (opad, real_key, key_length); + + /* Stage 2 and 5 */ + for (i = 0; i < sizeof (ipad); i++) { + ipad[i] ^= 0x36; + opad[i] ^= 0x5C; + } + + /* Stage 3 and 4 */ + g_checksum_update (checksum, ipad, sizeof (ipad)); + g_checksum_update (checksum, (guchar*)message, strlen (message)); + inner_length = sizeof (inner); + g_checksum_get_digest (checksum, inner, &inner_length); + g_checksum_reset (checksum); + + /* Stage 6 and 7 */ + g_checksum_update (checksum, opad, sizeof (opad)); + g_checksum_update (checksum, inner, inner_length); + + digest_length = sizeof (digest); + g_checksum_get_digest (checksum, digest, &digest_length); + + g_checksum_free (checksum); + g_free (real_key); + + return g_base64_encode (digest, digest_length); +} + +static char * +sign_plaintext (const gchar *consumer_secret, + const gchar *token_secret) +{ + char *cs; + char *ts; + char *rv; + + cs = OAUTH_ENCODE_STRING (consumer_secret); + ts = OAUTH_ENCODE_STRING (token_secret); + rv = g_strconcat (cs, "&", ts, NULL); + + g_free (cs); + g_free (ts); + + return rv; +} + +static char * +sign_hmac (const gchar *consumer_secret, + const gchar *token_secret, + const gchar *http_method, + const gchar *request_uri, + const gchar *encoded_params) +{ + GString *text; + + text = g_string_new (NULL); + g_string_append (text, http_method); + g_string_append_c (text, '&'); + g_string_append_uri_escaped (text, request_uri, NULL, FALSE); + g_string_append_c (text, '&'); + g_string_append_uri_escaped (text, encoded_params, NULL, FALSE); + + /* PLAINTEXT signature value is the HMAC-SHA1 key value */ + gchar *key; + key = sign_plaintext (consumer_secret, token_secret); + + gchar *signature; + signature = hmac_sha1 (key, text->str); + + g_free (key); + g_string_free (text, TRUE); + + return signature; +} + +static GHashTable * +calculate_xoauth_params (const gchar *request_uri, + const gchar *consumer_key, + const gchar *consumer_secret, + const gchar *access_token, + const gchar *access_token_secret) +{ + GHashTable *params; + gchar *nonce; + gchar *timestamp; + GList *keys; + GList *l; + GString *normalized; + + nonce = g_strdup_printf ("%u", g_random_int ()); + timestamp = g_strdup_printf ("%" G_GINT64_FORMAT, (gint64) time (NULL)); + + params = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, g_free); + g_hash_table_insert (params, "oauth_consumer_key", g_strdup (consumer_key)); + g_hash_table_insert (params, "oauth_nonce", nonce); /* takes ownership */ + g_hash_table_insert (params, "oauth_timestamp", timestamp); /* takes ownership */ + g_hash_table_insert (params, "oauth_version", g_strdup ("1.0")); + g_hash_table_insert (params, "oauth_signature_method", g_strdup ("HMAC-SHA1")); + g_hash_table_insert (params, "oauth_token", g_strdup (access_token)); + + normalized = g_string_new (NULL); + keys = g_hash_table_get_keys (params); + keys = g_list_sort (keys, (GCompareFunc) g_strcmp0); /* TODO: locale specific? */ + for (l = keys; l != NULL; l = l->next) + { + const gchar *key = l->data; + const gchar *value; + gchar *k; + gchar *v; + + value = g_hash_table_lookup (params, key); + if (normalized->len > 0) + g_string_append_c (normalized, '&'); + + k = OAUTH_ENCODE_STRING (key); + v = OAUTH_ENCODE_STRING (value); + + g_string_append_printf (normalized, "%s=%s", k, v); + + g_free (k); + g_free (v); + + g_print ("key %s=`%s'\n", key, value); + } + g_list_free (keys); + + g_print ("normalized: `%s'\n", normalized->str); + + gchar *signature; + signature = sign_hmac (consumer_secret, + access_token_secret, + "GET", + request_uri, + normalized->str); + g_hash_table_insert (params, "oauth_signature", signature); /* takes ownership */ + + g_string_free (normalized, TRUE); + return params; +} + +static gchar * +calculate_xoauth_param (const gchar *request_uri, + const gchar *consumer_key, + const gchar *consumer_secret, + const gchar *access_token, + const gchar *access_token_secret, + GError **error) +{ + gchar *ret; + GString *str; + GHashTable *params; + GList *keys; + GList *l; + + params = calculate_xoauth_params (request_uri, + consumer_key, + consumer_secret, + access_token, + access_token_secret); + str = g_string_new ("GET "); + g_string_append (str, request_uri); + g_string_append_c (str, ' '); + keys = g_hash_table_get_keys (params); + keys = g_list_sort (keys, (GCompareFunc) g_strcmp0); /* TODO: locale specific? */ + for (l = keys; l != NULL; l = l->next) + { + const gchar *key = l->data; + const gchar *value; + gchar *k; + gchar *v; + + value = g_hash_table_lookup (params, key); + if (l != keys) + g_string_append_c (str, ','); + + k = OAUTH_ENCODE_STRING (key); + v = OAUTH_ENCODE_STRING (value); + g_string_append_printf (str, "%s=\"%s\"", k, v); + g_free (k); + g_free (v); + } + g_list_free (keys); + + ret = g_base64_encode ((const guchar *) str->str, str->len); + g_string_free (str, TRUE); + g_hash_table_unref (params); + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gboolean +goa_backend_imap_auth_oauth_run_sync (GoaBackendImapAuth *_auth, + GDataInputStream *input, + GDataOutputStream *output, + GCancellable *cancellable, + GError **error) +{ + GoaBackendImapAuthOAuth *auth = GOA_BACKEND_IMAP_AUTH_OAUTH (_auth); + gchar *access_token; + gchar *access_token_secret; + gchar *xoauth_param; + gchar *request; + gchar *response; + gboolean ret; + + access_token = NULL; + access_token_secret = NULL; + xoauth_param = NULL; + request = NULL; + response = NULL; + ret = FALSE; + + access_token = goa_backend_oauth_provider_get_access_token_sync (auth->provider, + auth->object, + FALSE, /* force_refresh */ + &access_token_secret, + NULL, /* out_access_token_expires_in */ + NULL, /* GCancellable */ + error); /* GError */ + if (access_token == NULL) + goto out; + + xoauth_param = calculate_xoauth_param (auth->request_uri, + goa_backend_oauth_provider_get_consumer_key (auth->provider), + goa_backend_oauth_provider_get_consumer_secret (auth->provider), + access_token, + access_token_secret, + error); + if (xoauth_param == NULL) + goto out; + + request = g_strdup_printf ("A001 AUTHENTICATE XOAUTH %s\r\n", xoauth_param); + if (!g_data_output_stream_put_string (output, request, cancellable, error)) + goto out; + + again: + response = g_data_input_stream_read_line (input, NULL, cancellable, error); + if (response == NULL) + goto out; + /* ignore untagged responses */ + if (g_str_has_prefix (response, "*")) + { + g_free (response); + goto again; + } + if (!g_str_has_prefix (response, "A001 OK")) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + "Unexpected response `%s' while doing XOAUTH authentication", + response); + goto out; + } + + ret = TRUE; + + out: + g_free (response); + g_free (request); + g_free (xoauth_param); + g_free (access_token); + g_free (access_token_secret); + return ret; +} diff --git a/src/goa/goabackendimapauthoauth.h b/src/goa/goabackendimapauthoauth.h new file mode 100644 index 0000000..3899117 --- /dev/null +++ b/src/goa/goabackendimapauthoauth.h @@ -0,0 +1,46 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#if !defined (__GOA_BACKEND_INSIDE_GOA_BACKEND_H__) && !defined (GOA_BACKEND_COMPILATION) +#error "Only <goa/goabackend.h> can be included directly." +#endif + +#ifndef __GOA_BACKEND_IMAP_AUTH_OAUTH_H__ +#define __GOA_BACKEND_IMAP_AUTH_OAUTH_H__ + +#include <goa/goabackendtypes.h> + +G_BEGIN_DECLS + +#define GOA_TYPE_BACKEND_IMAP_AUTH_OAUTH (goa_backend_imap_auth_oauth_get_type ()) +#define GOA_BACKEND_IMAP_AUTH_OAUTH(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOA_TYPE_BACKEND_IMAP_AUTH_OAUTH, GoaBackendImapAuthOAuth)) +#define GOA_IS_BACKEND_IMAP_AUTH_OAUTH(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOA_TYPE_BACKEND_IMAP_AUTH_OAUTH)) + + +GType goa_backend_imap_auth_oauth_get_type (void) G_GNUC_CONST; +GoaBackendImapAuth *goa_backend_imap_auth_oauth_new (GoaBackendOAuthProvider *provider, + GoaObject *object, + const gchar *request_uri); + +G_END_DECLS + +#endif /* __GOA_BACKEND_IMAP_AUTH_OAUTH_H__ */ diff --git a/src/goa/goabackendimapclient.c b/src/goa/goabackendimapclient.c new file mode 100644 index 0000000..0b50b5a --- /dev/null +++ b/src/goa/goabackendimapclient.c @@ -0,0 +1,2023 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#include "config.h" +#include <glib/gi18n-lib.h> +#include <stdlib.h> + +#include "goabackendimapauth.h" +#include "goabackendimapclient.h" +#include "goabackendimapmessage.h" +#include "goabackendimapprivate.h" + +typedef enum +{ + CAPABILITY_FLAGS_NONE = 0, + CAPABILITY_FLAGS_IMAP4REV1 = (1<<0), + CAPABILITY_FLAGS_IDLE = (1<<1), + CAPABILITY_FLAGS_X_GM_EXT_1 = (1<<2), +} CapabilityFlags; + +static CapabilityFlags +capability_flags_from_strv (const gchar *const *strings) +{ + CapabilityFlags ret; + guint n; + + g_return_val_if_fail (strings != NULL, CAPABILITY_FLAGS_NONE); + + ret = CAPABILITY_FLAGS_NONE; + for (n = 0; strings[n] != NULL; n++) + { + if (g_strcmp0 (strings[n], "IMAP4rev1") == 0) + ret |= CAPABILITY_FLAGS_IMAP4REV1; + else if (g_strcmp0 (strings[n], "IDLE") == 0) + ret |= CAPABILITY_FLAGS_IDLE; + else if (g_strcmp0 (strings[n], "X-GM-EXT-1") == 0) + ret |= CAPABILITY_FLAGS_X_GM_EXT_1; + } + return ret; +} + +/** + * GoaBackendImapClient: + * + * The #GoaBackendImapClient structure contains only private data and should + * only be accessed using the provided API. + */ +struct _GoaBackendImapClient +{ + /*< private >*/ + GObject parent_instance; + + /* set at init / object construction */ + GMainContext *context; + gboolean closed; + GThread *worker_thread; + GCancellable *worker_cancellable; + + /* set at object construction */ + GoaBackendImapAuth *auth; + gchar *host_and_port; + guint use_tls; + gchar *criteria; + guint query_offset; + guint query_size; + + /* The remaining data members are already related to the running + * session + */ + gboolean is_running; + CapabilityFlags caps; + GSocketClient *sc; + GSocketConnection *c; + GDataInputStream *dis; + GDataOutputStream *dos; + gint num_search_result; + gint *search_result; + + /* used for generating command tags */ + guint tag; + + /* the number of messages in the selected mailbox */ + gint exists; + + /* the number of unseen messages in the selected mailbox */ + gint unseen; + + /* the 32-bit uidvalidity */ + gint uidvalidity; + + /* The number of times we've fetched data */ + gint num_completed_fetches; + GCond *num_completed_fetches_cond; + GMutex *num_completed_fetches_mutex; + + /* A list of results of the query */ + GList *messages; + /* A list currently being built */ + GList *messages_buildup; +}; + +typedef struct _GoaBackendImapClientClass GoaBackendImapClientClass; + +struct _GoaBackendImapClientClass +{ + GObjectClass parent_class; + void (*updated) (GoaBackendImapClient *client); + void (*closed) (GoaBackendImapClient *client, + const GError **error); +}; + +/** + * SECTION:goabackendimapclient + * @title: GoaBackendImapClient + * @short_description: A simple IMAP client + * + * #GoaBackendImapClient is a type used for obtaining information via the + * <ulink url="http://tools.ietf.org/html/rfc3501">IMAP</ulink> + * protocol. + */ + +enum +{ + UPDATED_SIGNAL, + CLOSED_SIGNAL, + LAST_SIGNAL +}; + +enum +{ + PROP_0, + PROP_HOST_AND_PORT, + PROP_USE_TLS, + PROP_CRITERIA, + PROP_QUERY_OFFSET, + PROP_QUERY_SIZE, + PROP_AUTH, + PROP_CLOSED +}; + +static guint signals[LAST_SIGNAL] = {0}; + +static void goa_backend_imap_client__g_initable_iface_init (GInitableIface *iface); + +static void goa_backend_imap_client__g_async_initable_iface_init (GAsyncInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (GoaBackendImapClient, goa_backend_imap_client, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, goa_backend_imap_client__g_initable_iface_init) + G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, goa_backend_imap_client__g_async_initable_iface_init)); + +G_LOCK_DEFINE_STATIC (messages_lock); + +/* ---------------------------------------------------------------------------------------------------- */ + +static void +goa_backend_imap_client_finalize (GObject *object) +{ + GoaBackendImapClient *client = GOA_BACKEND_IMAP_CLIENT (object); + + g_free (client->search_result); + if (client->sc != NULL) + g_object_unref (client->sc); + if (client->c != NULL) + g_object_unref (client->c); + if (client->dis != NULL) + g_object_unref (client->dis); + if (client->dos != NULL) + g_object_unref (client->dos); + + g_free (client->host_and_port); + g_free (client->criteria); + g_object_unref (client->auth); + + g_mutex_free (client->num_completed_fetches_mutex); + g_cond_free (client->num_completed_fetches_cond); + + g_object_unref (client->worker_cancellable); + if (client->context != NULL) + g_main_context_unref (client->context); + + G_OBJECT_CLASS (goa_backend_imap_client_parent_class)->finalize (object); +} + +static void +goa_backend_imap_client_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GoaBackendImapClient *client = GOA_BACKEND_IMAP_CLIENT (object); + + switch (prop_id) + { + case PROP_HOST_AND_PORT: + g_value_set_string (value, client->host_and_port); + break; + + case PROP_USE_TLS: + g_value_set_boolean (value, client->use_tls); + break; + + case PROP_CRITERIA: + g_value_set_string (value, client->criteria); + break; + + case PROP_QUERY_OFFSET: + g_value_set_uint (value, client->query_offset); + break; + + case PROP_QUERY_SIZE: + g_value_set_uint (value, client->query_size); + break; + + case PROP_AUTH: + g_value_set_object (value, client->auth); + break; + + case PROP_CLOSED: + g_value_set_boolean (value, client->closed); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +goa_backend_imap_client_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GoaBackendImapClient *client = GOA_BACKEND_IMAP_CLIENT (object); + + switch (prop_id) + { + case PROP_HOST_AND_PORT: + client->host_and_port = g_value_dup_string (value); + break; + + case PROP_USE_TLS: + client->use_tls = g_value_get_boolean (value); + break; + + case PROP_CRITERIA: + client->criteria = g_value_dup_string (value); + break; + + case PROP_QUERY_OFFSET: + client->query_offset = g_value_get_uint (value); + break; + + case PROP_QUERY_SIZE: + client->query_size = g_value_get_uint (value); + break; + + case PROP_AUTH: + client->auth = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +goa_backend_imap_client_init (GoaBackendImapClient *client) +{ + client->num_completed_fetches_cond = g_cond_new (); + client->num_completed_fetches_mutex = g_mutex_new (); + + client->context = g_main_context_get_thread_default (); + if (client->context != NULL) + g_main_context_ref (client->context); + client->worker_cancellable = g_cancellable_new (); +} + +static void +goa_backend_imap_client_class_init (GoaBackendImapClientClass *klass) +{ + GObjectClass *gobject_class; + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = goa_backend_imap_client_finalize; + gobject_class->set_property = goa_backend_imap_client_set_property; + gobject_class->get_property = goa_backend_imap_client_get_property; + + /** + * GoaBackendImapClient:host-and-port: + * + * The host to connect to. + */ + g_object_class_install_property (gobject_class, + PROP_HOST_AND_PORT, + g_param_spec_string ("host-and-port", + "host-and-port", + "host-and-port", + NULL, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * GoaBackendImapClient:use-tls: + * + * Whether TLS should be used when establishing the connection. + */ + g_object_class_install_property (gobject_class, + PROP_USE_TLS, + g_param_spec_boolean ("use-tls", + "use-tls", + "use-tls", + TRUE, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * GoaBackendImapClient:criteria: + * + * The criteria used for selecting messages. See + * goa_backend_imap_client_new_sync() for an explanation of what + * strings are valid. + */ + g_object_class_install_property (gobject_class, + PROP_CRITERIA, + g_param_spec_string ("criteria", + "criteria", + "criteria", + NULL, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * GoaBackendImapClient:query-offset: + * + * Offset into array of matching messages used when returning messages. + */ + g_object_class_install_property (gobject_class, + PROP_QUERY_OFFSET, + g_param_spec_uint ("query-offset", + "query-offset", + "query-offset", + 0, G_MAXUINT, 0, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * GoaBackendImapClient:query-size: + * + * Maximum number of messages to return. + */ + g_object_class_install_property (gobject_class, + PROP_QUERY_SIZE, + g_param_spec_uint ("query-size", + "query-size", + "query-size", + 1, G_MAXUINT, 10, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * GoaBackendImapClient:auth: + * + * The #GoaBackendImapAuth object used for authentication the connection. + */ + g_object_class_install_property (gobject_class, + PROP_AUTH, + g_param_spec_object ("auth", + "auth", + "auth", + GOA_TYPE_BACKEND_IMAP_AUTH, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + /** + * GoaBackendImapClient:closed: + * + * Whether the connection to the IMAP server has been closed. + */ + g_object_class_install_property (gobject_class, + PROP_CLOSED, + g_param_spec_boolean ("closed", + "closed", + "closed", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * GoaBackendImapClient::updated: + * @client: The #GoaBackendImapClient emitting the signal. + * + * Signal emitted every time messages has been fetched from the + * server. This does not neccesarily mean that the messages returned + * by goa_backend_imap_client_get_messages() has changed. + */ + signals[UPDATED_SIGNAL] = + g_signal_new ("updated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GoaBackendImapClientClass, updated), + NULL, + NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, + 0); + + /** + * GoaBackendImapClient::closed: + * @client: The #GoaBackendImapClient emitting the signal. + * @error: A #GError or %NULL. + * + * Emitted when the connection has been closed. If the connection + * was closed because of an error condition, @error will be + * non-%NULL. + * + * After this signal has been emitted, the object is no longer useful. + */ + signals[CLOSED_SIGNAL] = + g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GoaBackendImapClientClass, closed), + NULL, + NULL, + g_cclosure_marshal_VOID__BOXED, + G_TYPE_NONE, + 1, + G_TYPE_ERROR); +} + +static gboolean +emit_updated_in_idle_cb (gpointer user_data) +{ + GoaBackendImapClient *client = GOA_BACKEND_IMAP_CLIENT (user_data); + g_signal_emit (client, signals[UPDATED_SIGNAL], 0); + return FALSE; +} + +static void +emit_updated_in_idle (GoaBackendImapClient *client) +{ + GSource *source; + + g_return_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client)); + + source = g_idle_source_new (); + g_source_set_priority (source, G_PRIORITY_DEFAULT); + g_source_set_callback (source, emit_updated_in_idle_cb, g_object_ref (client), g_object_unref); + g_source_attach (source, client->context); + g_source_unref (source); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +typedef struct +{ + GoaBackendImapClient *client; + GError *error; +} EmitClosedData; + +static void +emit_closed_data_free (EmitClosedData *data) +{ + g_object_unref (data->client); + if (data->error != NULL) + g_error_free (data->error); + g_free (data); +} + +static gboolean +emit_closed_in_idle_cb (gpointer user_data) +{ + EmitClosedData *data = user_data; + g_signal_emit (data->client, signals[CLOSED_SIGNAL], 0, data->error); + g_object_notify (G_OBJECT (data->client), "closed"); + return FALSE; +} + +static void +emit_closed_in_idle (GoaBackendImapClient *client, + const GError *error) +{ + GSource *source; + EmitClosedData *data; + + g_return_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client)); + + data = g_new0 (EmitClosedData, 1); + data->client = g_object_ref (client); + data->error = error != NULL ? g_error_copy (error) : NULL; + + source = g_idle_source_new (); + g_source_set_priority (source, G_PRIORITY_DEFAULT); + g_source_set_callback (source, emit_closed_in_idle_cb, data, (GDestroyNotify) emit_closed_data_free); + g_source_attach (source, client->context); + g_source_unref (source); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/** + * goa_backend_imap_client_new: + * @host_and_port: The name and optionally port to connect to. + * @use_tls: Whether TLS should be used. + * @auth: Object used for authenticating the connection. + * @criteria: Criteria string used to filter returned messages. + * @query_offset: Offset of result window in the array of matching messages. + * @query_size: Size of result window in the array of matching messages. + * @cancellable: (allow-none): A #GCancellable or %NULL. + * @callback: Function to call when the request has been satisfied. + * @user_data: Data to pass to @callback. + * + * Async version of goa_backend_imap_client_new_sync(). + * + * When the result is ready, @callback will be called in the <link + * linkend="g-main-context-push-thread-default">thread-default main + * loop</link>. You can then use goa_backend_imap_client_new_finish() + * to get the result. + */ +void +goa_backend_imap_client_new (const gchar *host_and_port, + gboolean use_tls, + GoaBackendImapAuth *auth, + const gchar *criteria, + guint query_offset, + guint query_size, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (GOA_IS_BACKEND_IMAP_AUTH (auth)); + g_return_if_fail (host_and_port != NULL); + g_return_if_fail (criteria != NULL); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + g_async_initable_new_async (GOA_TYPE_BACKEND_IMAP_CLIENT, + G_PRIORITY_DEFAULT, + cancellable, + callback, + user_data, + "host-and-port", host_and_port, + "use-tls", use_tls, + "auth", auth, + "criteria", criteria, + "query-offset", query_offset, + "query-size", query_size, + NULL); +} + +/** + * goa_backend_imap_client_new_finish: + * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to goa_backend_imap_client_new(). + * @error: Return location for error or %NULL. + * + * Finishes an operation started with goa_backend_imap_client_new(). + * + * Returns: A #GoaBackendImapClient or %NULL if @error is set. + */ +GoaBackendImapClient * +goa_backend_imap_client_new_finish (GAsyncResult *res, + GError **error) +{ + GObject *ret; + GObject *source_object; + source_object = g_async_result_get_source_object (res); + ret = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), res, error); + g_object_unref (source_object); + if (ret != NULL) + return GOA_BACKEND_IMAP_CLIENT (ret); + else + return NULL; +} + +/** + * goa_backend_imap_client_new_sync: + * @host_and_port: The name and optionally port to connect to. + * @use_tls: Whether TLS should be used. + * @auth: Object used for authenticating the connection. + * @criteria: Criteria string used to filter returned messages. + * @query_offset: Offset of result window in the array of matching messages. + * @query_size: Size of result window in the array of matching messages. + * @cancellable: (allow-none): A #GCancellable or %NULL. + * @error: Return location for error or %NULL. + * + * Creates a new IMAP client connecting to the mailbox INBOX at the + * server represented by @host_and_port using @auth to authenticate + * the IMAP connection. If authentication fails or messages can't be + * downloaded, then this constructor returns %NULL and @error will be + * set. + * + * The returned object is used to obtain #GoaBackendImapMessage + * instances representing objects on the IMAP server. The messages are + * selected by requesting the IMAP server to match all messages in the + * mailbox against the @criteria string. Of the matching messages, + * @query_size messages starting at offset @query_offset will be + * downloaded. Note that the entire message is not downloaded - only + * the data represented by #GoaBackendImapMessage (e.g. typically only + * hundreds of bytes per message). Use + * goa_backend_imap_client_get_messages() to obtain the matching + * messages. + * + * The connection to the IMAP server is live insofar the <ulink + * url="http://en.wikipedia.org/wiki/IMAP_IDLE">IMAP IDLE</ulink> (or + * polling, if IDLE is not available) command is used to get notified + * of new messages - the #GoaBackendImapClient::updated signal is + * emitted every time new messages are fetched. Use + * goa_backend_imap_client_refresh() to force a refresh. + * + * Note that the connection can be broken at any time - the + * #GoaBackendImapClient::closed signal is emitted this happens. The + * object is no longer useful when this happens. Use + * goa_backend_imap_client_close() to close the connection manually. + * + * Also note that this constructor blocks the calling thread - see + * goa_backend_imap_client_new() for the async non-blocking version. + * + * All signals are emitted in the <link + * linkend="g-main-context-push-thread-default">thread-default main + * loop</link> of the thread you are calling this constructor from. + * + * Valid strings for @criteria includes the empty string (to match all + * messages) and <literal>unread</literal> (to match all unread + * messages). More options may be added in the future. + * + * Returns: A #GoaBackendImapClient or %NULL if @error is set. + */ +GoaBackendImapClient * +goa_backend_imap_client_new_sync (const gchar *host_and_port, + gboolean use_tls, + GoaBackendImapAuth *auth, + const gchar *criteria, + guint query_offset, + guint query_size, + GCancellable *cancellable, + GError **error) +{ + GInitable *ret; + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_AUTH (auth), NULL); + g_return_val_if_fail (host_and_port != NULL, NULL); + g_return_val_if_fail (criteria != NULL, NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + ret = g_initable_new (GOA_TYPE_BACKEND_IMAP_CLIENT, + cancellable, + error, + "host-and-port", host_and_port, + "use-tls", use_tls, + "auth", auth, + "criteria", criteria, + "query-offset", query_offset, + "query-size", query_size, + NULL); + if (ret != NULL) + return GOA_BACKEND_IMAP_CLIENT (ret); + else + return NULL; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gboolean +parse_int (const gchar *s, + gint *out_result) +{ + gboolean ret; + gchar *endp; + gint result; + + g_return_val_if_fail (s != NULL, FALSE); + + ret = FALSE; + result = strtol (s, &endp, 0); + if (result == 0 && endp == s) + goto out; + + if (out_result != NULL) + *out_result = result; + + ret = TRUE; + + out: + return ret; +} + +static gboolean +fetch_check (const gchar *data, + guint *pos, + const gchar *key) +{ + gsize key_len; + gboolean ret; + + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (pos != NULL, FALSE); + g_return_val_if_fail (key != NULL, FALSE); + + ret = FALSE; + + key_len = strlen (key); + if (strncmp (data + *pos, key, key_len) == 0 && data[*pos + key_len] == ' ') + { + ret = TRUE; + *pos += key_len + 1; + goto out; + } + out: + return ret; +} + +static gchar ** +fetch_parenthesized_list (const gchar *data, + guint *pos) +{ + gchar **ret; + gchar *s; + guint start_pos; + + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (pos != NULL, FALSE); + + ret = NULL; + + if (data[*pos] != '(') + goto out; + *pos += 1; + start_pos = *pos; + + while (data[*pos] != ')' && data[*pos] != '\0') + *pos += 1; + if (data[*pos] != ')') + goto out; + + s = g_strndup (data + start_pos, *pos - start_pos); + ret = g_strsplit (s, " ", -1); + g_free (s); + + *pos += 1; + + out: + return ret; +} + +static gchar * +fetch_string (const gchar *data, + guint *pos) +{ + gchar *ret; + guint start_pos; + + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (pos != NULL, FALSE); + + ret = NULL; + + start_pos = *pos; + + while (data[*pos] != ' ' && data[*pos] != ')' && data[*pos] != '\0') + *pos += 1; + + ret = g_strndup (data + start_pos, *pos - start_pos); + + return ret; +} + +static gchar * +fetch_quoted_string (const gchar *data, + guint *pos) +{ + gchar *ret; + guint start_pos; + + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (pos != NULL, FALSE); + + ret = NULL; + + if (data[*pos] != '"') + goto out; + *pos += 1; + + start_pos = *pos; + + while (data[*pos] != '"' && data[*pos] != '\0') + *pos += 1; + + ret = g_strndup (data + start_pos, *pos - start_pos); + + *pos += 1; + + out: + return ret; +} + +static guint +lookup_month (const gchar *str) +{ + static const gchar *months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + guint n; + for (n = 0; months[n] != NULL; n++) + if (g_strcmp0 (str, months[n]) == 0) + return n + 1; + return 0; +} + +static gboolean +fetch_date_time (const gchar *data, + guint *pos, + gint64 *out_value) +{ + gchar *str_value; + GTimeZone *tz; + GDateTime *dt; + gchar month[4]; + gint mon; + gint day, year, hour, min, sec, offset; + gboolean ret; + + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (pos != NULL, FALSE); + g_return_val_if_fail (out_value != NULL, FALSE); + + str_value = NULL; + tz = NULL; + dt = NULL; + ret = FALSE; + + str_value = fetch_quoted_string (data, pos); + if (str_value == NULL) + goto out; + + if (sscanf (str_value, + "%02d-%03s-%04d %02d:%02d:%02d %04d", + &day, month, &year, &hour, &min, &sec, &offset) != 7) + goto out; + + tz = g_time_zone_new (str_value + strlen (str_value) - 5); + if (tz == NULL) + goto out; + mon = lookup_month (month); + if (mon == 0) + goto out; + + dt = g_date_time_new (tz, year, mon, day, hour, min, (gdouble) sec); + if (out_value != NULL) + *out_value = g_date_time_to_unix (dt); + + ret = TRUE; + + out: + if (dt != NULL) + g_date_time_unref (dt); + if (tz != NULL) + g_time_zone_unref (tz); + g_free (str_value); + return ret; +} + +static gboolean +fetch_int (const gchar *data, + guint *pos, + gint *out_value) +{ + gchar *str_value; + gboolean ret; + + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (pos != NULL, FALSE); + g_return_val_if_fail (out_value != NULL, FALSE); + + str_value = NULL; + ret = FALSE; + + str_value = fetch_string (data, pos); + if (str_value == NULL) + goto out; + + if (!parse_int (str_value, out_value)) + goto out; + + ret = TRUE; + + out: + g_free (str_value); + return ret; +} + +static gchar * +fetch_literal_string (const gchar *data, + guint *pos, + guint *out_len) +{ + gchar *ret; + guint start_pos; + guint len; + + g_return_val_if_fail (data != NULL, FALSE); + g_return_val_if_fail (pos != NULL, FALSE); + + ret = NULL; + + start_pos = *pos; + + if (data[*pos] != '{') + goto out; + *pos += 1; + while (g_ascii_isdigit (data[*pos])) + *pos += 1; + if (strncmp (data + *pos, "}\r\n", 3) != 0) + goto out; + *pos += 3; + + if (!parse_int (data + start_pos + 1, (gint*) &len)) + goto out; + + ret = g_strndup (data + *pos, len); + *pos += len; + + if (out_len != NULL) + *out_len = len; + + out: + return ret; +} + +/* Simple FETCH response parser only handling a subset of FETCH + * responses, see + * + * http://tools.ietf.org/html/rfc3501#section-7.4.2 + * + * for more details. + */ +static void +handle_fetch_response (GoaBackendImapClient *client, + guint message_seqnum, + const gchar *data) +{ + guint n; + gchar **flags_strv; + gboolean parsed; + gboolean has_uid; + gint uid; + gint64 internal_date; + gchar *thread_id; + gchar *rfc822_headers; + guint rfc822_headers_len; + gchar *excerpt; + guint excerpt_len; + GoaBackendImapMessageFlags flags; + GoaBackendImapMessage *message; + + g_return_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client)); + g_return_if_fail (message_seqnum >= 1); + g_return_if_fail (data != NULL); + + uid = 0; + has_uid = FALSE; + thread_id = NULL; + flags_strv = NULL; + excerpt = NULL; + rfc822_headers = NULL; + parsed = FALSE; + + if (data[0] != '(') + goto out; + n = 1; + while (data[n] != ')' && data[n] != '\0') + { + if (fetch_check (data, &n, "X-GM-THRID")) + { + thread_id = fetch_string (data, &n); + if (thread_id == NULL) + goto out; + + } + else if (fetch_check (data, &n, "UID")) + { + if (!fetch_int (data, &n, &uid)) + goto out; + has_uid = TRUE; + } + else if (fetch_check (data, &n, "INTERNALDATE")) + { + if (!fetch_date_time (data, &n, &internal_date)) + goto out; + } + else if (fetch_check (data, &n, "FLAGS")) + { + flags_strv = fetch_parenthesized_list (data, &n); + if (flags_strv == NULL) + goto out; + } + else if (fetch_check (data, &n, "BODY[HEADER.FIELDS (Date From To Cc Subject)]")) + { + rfc822_headers = fetch_literal_string (data, &n, &rfc822_headers_len); + if (rfc822_headers == NULL) + goto out; + } + else if (fetch_check (data, &n, "BODY[TEXT]<0>")) + { + excerpt = fetch_literal_string (data, &n, &excerpt_len); + if (excerpt == NULL) + goto out; + } + else + { + /* Don't know how to handle unknown data so fail completely */ + goto out; + } + /* advance to next value in FETCH response list, if any */ + while (data[n] == ' ') + n++; + } + + /* thread_id is optional (it's a GMail extension) */ + if (!has_uid || flags_strv == NULL || rfc822_headers == NULL || excerpt == NULL) + goto out; + + flags = goa_backend_imap_message_flags_from_strv ((const gchar* const *) flags_strv); + + /* OK, message is valid */ + parsed = TRUE; + + message = goa_backend_imap_message_new (); + message->seqnum = message_seqnum; + message->uid = ((guint64) client->uidvalidity << 32) | ((guint64) uid); + message->flags = flags; + message->internal_date = internal_date; + /* steal the rfc822_headers string */ + message->rfc822_headers = rfc822_headers; + rfc822_headers = NULL; + /* steal the excerpt string */ + message->excerpt = excerpt; + excerpt = NULL; + + client->messages_buildup = g_list_prepend (client->messages_buildup, message); + + out: + if (!parsed) + { + g_debug ("Was unable to parse FETCH response for msg %d with data `%s'", message_seqnum, data); + } + g_free (rfc822_headers); + g_free (excerpt); + g_free (thread_id); + g_strfreev (flags_strv); +} + + +static void +handle_expunge_response (GoaBackendImapClient *client, + guint message_seqnum) +{ + /* We are currently utterly uninterested in this since we + * currently re-request everything in the query window + */ +} + +static void +handle_status_response (GoaBackendImapClient *client, + const gchar *mailbox, + const gchar *data) +{ + gchar **items; + guint n; + + items = NULL; + + n = 0; + items = fetch_parenthesized_list (data, &n); + if (items == NULL) + goto out; + + for (n = 0; items[n] != NULL && items[n+1] != NULL; n += 2) + { + gint value; + if (g_strcmp0 (items[n], "MESSAGES") == 0 && parse_int (items[n+1], &value)) + { + client->exists = value; + } + else if (g_strcmp0 (items[n], "UNSEEN") == 0 && parse_int (items[n+1], &value)) + { + client->unseen = value; + } + else if (g_strcmp0 (items[n], "UIDVALIDITY") == 0 && parse_int (items[n+1], &value)) + { + client->uidvalidity = value; + } + } + + out: + g_strfreev (items); +} + +static gint +sort_by_seqnum_reverse (gint *a, gint *b) +{ + return *b - *a; +} + +static void +handle_search_response (GoaBackendImapClient *client, + const gchar *data) +{ + gchar **tokens; + GArray *array; + guint n; + + array = g_array_sized_new (FALSE, + FALSE, + sizeof (gint), + 100); + + /* TODO: this could be done more efficiently but I'm pretty lazy */ + tokens = g_strsplit (data, " ", -1); + for (n = 0; tokens[n] != NULL; n++) + { + gint seqnum; + seqnum = atoi (tokens[n]); + g_array_append_val (array, seqnum); + } + g_strfreev (tokens); + + /* Sort resulting messages by seqnum so the newest ones are first */ + g_array_sort (array, (GCompareFunc) sort_by_seqnum_reverse); + + if (client->search_result != NULL) + g_free (client->search_result); + client->num_search_result = array->len; + client->search_result = (gint *) g_array_free (array, FALSE); +} + +static void +handle_untagged_response (GoaBackendImapClient *client, + const gchar *response) +{ + gint n; + gchar *s; + gchar **tokens; + + s = NULL; + tokens = NULL; + + s = g_strdup (response + 1); /* skip leading asterix */ + g_strchug (s); /* and leading whitespace, if any */ + + /* special case */ + if (g_str_has_prefix (s, "CAPABILITY ")) + { + gchar **strv; + strv = g_strsplit (s + sizeof "CAPABILITY " - 1, " ", -1); + client->caps = capability_flags_from_strv ((const gchar *const *) strv); + g_strfreev (strv); + goto out; + } + + tokens = g_strsplit (s, " ", 3); + if (g_strv_length (tokens) < 2) + { + g_debug ("ignoring short response `%s'", response); + goto out; + } + + if (g_strcmp0 (tokens[1], "EXISTS") == 0 && parse_int (tokens[0], &n)) + { + client->exists = n; + } + else if (g_strcmp0 (tokens[1], "FETCH") == 0 && parse_int (tokens[0], &n)) + { + if (tokens[2] != NULL) + handle_fetch_response (client, n, tokens[2]); + else + g_debug ("ignoring fetch response with no data `%s'", response); + } + else if (g_strcmp0 (tokens[1], "EXPUNGE") == 0 && parse_int (tokens[0], &n)) + { + handle_expunge_response (client, n); + } + else if (g_strcmp0 (tokens[0], "STATUS") == 0) + { + handle_status_response (client, tokens[1], tokens[2]); + } + else if (g_strcmp0 (tokens[0], "SEARCH") == 0) + { + handle_search_response (client, s + sizeof "SEARCH " - 1); + } + else + { + g_debug ("TODO: unhandled untagged response `%s'", response); + } + + out: + g_strfreev (tokens); + g_free (s); +} + +static gchar * +goa_backend_imap_client_run_command_sync (GoaBackendImapClient *client, + const gchar *command, + GCancellable *cancellable, + GError **error) +{ + gchar *s; + gchar *tag; + gchar *ret; + GString *response; + gsize len; + gboolean is_idle_command; + gboolean idle_has_sent_done; + GError *local_error; + + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client), NULL); + g_return_val_if_fail (command != NULL, NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + s = NULL; + tag = NULL; + response = NULL; + ret = NULL; + is_idle_command = FALSE; + idle_has_sent_done = FALSE; + + if (g_strcmp0 (command, "IDLE") == 0) + is_idle_command = TRUE; + + tag = g_strdup_printf ("T%05d ", client->tag++); + s = g_strconcat (tag, command, "\r\n", NULL); + if (!g_data_output_stream_put_string (client->dos, s, cancellable, error)) + { + g_prefix_error (error, "Error putting string: "); + goto out; + } + g_free (s); + + response = g_string_new (NULL); + again: + local_error = NULL; + s = g_data_input_stream_read_line (client->dis, NULL, cancellable, &local_error); + if (s == NULL) + { + if (local_error != NULL) + { + g_prefix_error (&local_error, "Error reading line: "); + /* if doing an IDLE that was cancelled, write the continuation string + * anyway, ignoring the cancellable + */ + if ((local_error->domain == G_IO_ERROR && local_error->code == G_IO_ERROR_CANCELLED) && is_idle_command) + { + if (!g_data_output_stream_put_string (client->dos, "DONE\r\n", NULL, error)) + { + /* if this fails, ignore the cancelled error */ + g_error_free (local_error); + g_prefix_error (error, "Error putting IDLE continuation string: "); + goto out; + } + /* TODO: this way we're ignoring the response to the IDLE command we just + * fired off.. it's not a problem per se, but it's annoying to see in + * debug output... we could sit around and wait for the response but there's + * really no point in doing so + */ + } + g_propagate_error (error, local_error); + } + else + { + g_set_error (error, GOA_ERROR, GOA_ERROR_FAILED, "No content to read"); + } + goto out; + } + len = strlen (s); + /* So far so good */ + g_string_append_len (response, s, len); + + /* Could be it's a literal string */ + if (len >= 3 && s[len-1] == '}') + { + gint n; + n = len - 2; + while (g_ascii_isdigit (s[n]) && n >= 0) + n--; + if (s[n] == '{') + { + gsize num_read; + gsize lit_len; + gchar *lit; + lit_len = atoi (s + n + 1); + /* Don't blindly allocate any big number of bytes */ + if (lit_len > 10*1024*1024) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + "Refusing to read an additional %" G_GSIZE_FORMAT " bytes for literal string", + lit_len); + g_free (s); + goto out; + } + lit = g_malloc0 (lit_len + 1); + if (!g_input_stream_read_all (G_INPUT_STREAM (client->dis), + lit, + lit_len, + &num_read, + cancellable, + error)) + { + g_free (lit); + g_prefix_error (error, + "Requested %" G_GSIZE_FORMAT " bytes for literal string " + "but only read %" G_GSSIZE_FORMAT ": ", + lit_len, num_read); + g_free (s); + goto out; + } + /* include the original CRLF, then the literal string */ + g_string_append (response, "\r\n"); + g_string_append_len (response, lit, lit_len); + g_free (lit); + g_free (s); + /* then keep reading */ + goto again; + } + } + + if (g_str_has_prefix (response->str, tag)) + { + gint tag_len; + tag_len = strlen (tag); + if (g_str_has_prefix (response->str + tag_len, "BAD")) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + "BAD response to `%s': %s", + command, + response->str + tag_len + 4); + goto out; + } + ret = g_strdup (response->str + tag_len); + /* TODO: return additional response? */ + goto out; + } + else if (g_str_has_prefix (response->str, "*")) + { + /* untagged */ + handle_untagged_response (client, response->str); + } + else + { + /* otherwise other unhandled response */ + g_debug ("unhandled response `%s'", response->str); + } + + /* If idling, when we receive real data, put the DONE continuation + * string so the IDLE command will terminate + */ + if (is_idle_command && !g_str_has_prefix (response->str, "+") && !idle_has_sent_done) + { + idle_has_sent_done = TRUE; + if (!g_data_output_stream_put_string (client->dos, "DONE\r\n", cancellable, error)) + { + g_prefix_error (error, "Error putting IDLE continuation string: "); + goto out; + } + } + + /* reset */ + g_string_set_size (response, 0); + goto again; + + out: + if (response != NULL) + g_string_free (response, TRUE); + g_free (tag); + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/** + * goa_backend_imap_client_get_num_unread: + * @client: A #GoaBackendImapClient. + * + * The amount of unread messages in the mailbox. + * + * Returns: The number of unread messages or -1 if the connection has been closed. + */ +gint +goa_backend_imap_client_get_num_unread (GoaBackendImapClient *client) +{ + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client), -1); + if (client->closed) + return -1; + else + return client->unseen; +} + +/** + * goa_backend_imap_client_get_num_messages: + * @client: A #GoaBackendImapClient. + * + * The amount of messages in the mailbox. + * + * Returns: The number of messages or -1 if the connection has been closed. + */ +gint +goa_backend_imap_client_get_num_messages (GoaBackendImapClient *client) +{ + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client), -1); + if (client->closed) + return -1; + else + return client->exists; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/** + * goa_backend_imap_client_get_messages: + * @client: A #GoaBackendImapClient. + * + * Gets all messages in the mailbox matching the + * #GoaBackendImapClient:criteria, #GoaBackendImapClient:query-offset + * and #GoaBackendImapClient:query-size properties as described in the + * documentation for goa_backend_imap_client_new_sync(). + * + * Returns: (transfer full) (element-type GoaBackendImapMessage): A + * list of messages that should be freed with g_list_free() after each + * element has been freed with goa_imap_message_unref(). + */ +GList * +goa_backend_imap_client_get_messages (GoaBackendImapClient *client) +{ + GList *ret; + + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client), NULL); + + G_LOCK (messages_lock); + ret = g_list_copy (client->messages); + g_list_foreach (client->messages, (GFunc) goa_backend_imap_message_ref, NULL); + G_UNLOCK (messages_lock); + + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/** + * goa_backend_imap_client_refresh_sync: + * @client: A #GoaBackendImapClient. + * @cancellable: A #GCancellable or %NULL. + * @error: Return location for error or %NULL. + * + * Forcibly refreshes the messages by doing a server roundtrip. The + * calling thread is blocked while this is happening. + * + * Returns: %TRUE if the operation succeeded, %FALSE if @error is set. + */ +gboolean +goa_backend_imap_client_refresh_sync (GoaBackendImapClient *client, + GCancellable *cancellable, + GError **error) +{ + gboolean ret; + gint num_completed_fetches_before; + + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + g_object_ref (client); + + ret = FALSE; + + if (client->closed) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + "Client is not running"); + goto out; + } + + g_mutex_lock (client->num_completed_fetches_mutex); + num_completed_fetches_before = client->num_completed_fetches; + g_cancellable_cancel (client->worker_cancellable); + while (client->num_completed_fetches < num_completed_fetches_before + 1) + g_cond_wait (client->num_completed_fetches_cond, client->num_completed_fetches_mutex); + if (client->num_completed_fetches == G_MAXINT) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + "Client stopped while refreshing"); + } + else + { + ret = TRUE; + } + //g_debug ("yay client->num_completed_fetches=%d", client->num_completed_fetches); + g_mutex_unlock (client->num_completed_fetches_mutex); + + out: + g_object_unref (client); + return ret; +} + +static void +refresh_in_thread_func (GSimpleAsyncResult *res, + GObject *object, + GCancellable *cancellable) +{ + GError *error; + error = NULL; + if (!goa_backend_imap_client_refresh_sync (GOA_BACKEND_IMAP_CLIENT (object), + cancellable, + &error)) + g_simple_async_result_take_error (res, error); +} + +/** + * goa_backend_imap_client_refresh: + * @client: A #GoaBackendImapClient. + * @cancellable: A #GCancellable or %NULL. + * @callback: Function to call when the request has been satisfied. + * @user_data: Data to pass to @callback. + * + * Async version of goa_backend_imap_client_refresh_sync(). + * + * When the result is ready, @callback will be called in the <link + * linkend="g-main-context-push-thread-default">thread-default main + * loop</link>. You can then use goa_backend_imap_client_refresh_finish() + * to get the result. + */ +void +goa_backend_imap_client_refresh (GoaBackendImapClient *client, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GSimpleAsyncResult *simple; + + g_return_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client)); + g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable)); + + simple = g_simple_async_result_new (G_OBJECT (client), + callback, + user_data, + goa_backend_imap_client_refresh); + g_simple_async_result_run_in_thread (simple, + refresh_in_thread_func, + G_PRIORITY_DEFAULT, + cancellable); + g_object_unref (simple); +} + +/** + * goa_backend_imap_client_refresh_finish: + * @client: A #GoaBackendImapClient. + * @res: The #GAsyncResult obtained from the #GAsyncReadyCallback passed to goa_backend_imap_client_refresh(). + * @error: Return location for error or %NULL. + * + * Finishes an operation started with goa_backend_imap_client_refresh(). + * + * Returns: %TRUE if the operation succeeded, %FALSE if @error is set. + */ +gboolean +goa_backend_imap_client_refresh_finish (GoaBackendImapClient *client, + GAsyncResult *res, + GError **error) +{ + GSimpleAsyncResult *simple = G_SIMPLE_ASYNC_RESULT (res); + gboolean ret; + + ret = FALSE; + + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client), FALSE); + g_return_val_if_fail (G_IS_ASYNC_RESULT (res), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + g_warn_if_fail (g_simple_async_result_get_source_tag (simple) == goa_backend_imap_client_refresh); + + if (g_simple_async_result_propagate_error (simple, error)) + goto out; + + ret = TRUE; + + out: + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gboolean +goa_backend_imap_client_do_login (GoaBackendImapClient *client, + GCancellable *cancellable, + GError **error) +{ + gboolean ret; + gchar *response; + + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + ret = FALSE; + + client->sc = g_socket_client_new (); + if (client->use_tls) + g_socket_client_set_tls (client->sc, TRUE); + + /* TODO: TLS validation etc etc */ + + client->c = g_socket_client_connect_to_host (client->sc, + client->host_and_port, + client->use_tls ? 993 : 143, + cancellable, + error); + if (client->c == NULL) + goto out; + + /* fail quickly */ + g_socket_set_timeout (g_socket_connection_get_socket (client->c), 30); + + client->dis = g_data_input_stream_new (g_io_stream_get_input_stream (G_IO_STREAM (client->c))); + client->dos = g_data_output_stream_new (g_io_stream_get_output_stream (G_IO_STREAM (client->c))); + g_filter_input_stream_set_close_base_stream (G_FILTER_INPUT_STREAM (client->dis), FALSE); + g_filter_output_stream_set_close_base_stream (G_FILTER_OUTPUT_STREAM (client->dos), FALSE); + g_data_input_stream_set_newline_type (client->dis, G_DATA_STREAM_NEWLINE_TYPE_CR_LF); + + /* Authenticate via the passed in auth helper */ + if (!goa_backend_imap_auth_run_sync (client->auth, + client->dis, + client->dos, + cancellable, + error)) + goto out; + + /* Finally, select the mailbox + * TODO: make it possible to select other inboxes + */ + response = goa_backend_imap_client_run_command_sync (client, + "SELECT INBOX", + cancellable, + error); + if (response == NULL) + goto out; + g_free (response); + + ret = TRUE; + + out: + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gboolean +goa_backend_imap_client_do_refresh (GoaBackendImapClient *client, + GCancellable *cancellable, + GError **error) +{ + gchar *response; + gboolean ret; + GString *request_str; + guint num_fetches; + + ret = FALSE; + + /* fail quickly */ + g_socket_set_timeout (g_socket_connection_get_socket (client->c), 30); + + /* First get an overall status */ + response = goa_backend_imap_client_run_command_sync (client, + "STATUS INBOX (MESSAGES UNSEEN UIDVALIDITY)", + cancellable, + error); + if (response == NULL) + goto out; + g_free (response); + + if (client->exists == 0) + { + ret = TRUE; /* that was easy */ + goto out; + } + + num_fetches = 0; + /* Calculate FETCH string */ + request_str = g_string_new ("FETCH "); + if (g_strcmp0 (client->criteria, "") == 0) + { + gint message_seqnum_high; + gint message_seqnum_low; + + /* No search criteria => get latest messages */ + + /* TODO: use client->query_offset */ + message_seqnum_high = client->exists; + message_seqnum_low = client->exists - client->query_size + 1; + if (message_seqnum_low < 1) + message_seqnum_low = 1; + g_string_append_printf (request_str, + "%d:%d", + message_seqnum_low, + message_seqnum_high); + num_fetches = message_seqnum_high - message_seqnum_low + 1; + } + else + { + guint n; + if (client->search_result != NULL) + { + client->search_result = NULL; + g_free (client->search_result); + } + response = goa_backend_imap_client_run_command_sync (client, + "SEARCH UNSEEN", + cancellable, + error); + if (response == NULL) + goto out; + g_free (response); + for (n = client->query_offset; n < client->num_search_result && n < client->query_size; n++) + { + /* TODO: maybe compress into ranges - not sure it matters + * as the query is really small + */ + if (n > 0) + g_string_append_c (request_str, ','); + g_string_append_printf (request_str, "%d", client->search_result[n]); + num_fetches++; + } + } + + /* build up messages */ + g_assert (client->messages_buildup == NULL); + if (num_fetches > 0) + { + g_string_append (request_str, + " (FLAGS INTERNALDATE UID X-GM-THRID BODY.PEEK[HEADER.FIELDS (Date From To Cc Subject)] BODY.PEEK[TEXT]<0.200>)"); + response = goa_backend_imap_client_run_command_sync (client, + request_str->str, + cancellable, + error); + g_string_free (request_str, TRUE); + if (response == NULL) + goto out; + g_free (response); + } + else + { + g_string_free (request_str, FALSE); + } + + /* Sort resulting messages by seqnum */ + client->messages_buildup = g_list_sort (client->messages_buildup, + (GCompareFunc) goa_backend_imap_message_compare_seqnum_reverse); + /* replace messages */ + G_LOCK (messages_lock); + g_list_foreach (client->messages, (GFunc) goa_backend_imap_message_unref, NULL); + g_list_free (client->messages); + client->messages = client->messages_buildup; + G_UNLOCK (messages_lock); + client->messages_buildup = NULL; + + ret = TRUE; + + out: + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/* runs in the thread that runs the default GMainContext */ +static gboolean +refresh_while_idling_cb (gpointer user_data) +{ + GoaBackendImapClient *client = GOA_BACKEND_IMAP_CLIENT (user_data); + goa_backend_imap_client_refresh (client, + NULL, /* GCancellable */ + NULL, /* GAsyncReadyCallback */ + NULL); /* user_data */ + return FALSE; +} + +static gboolean +goa_backend_imap_client_do_idle (GoaBackendImapClient *client, + GError **error) +{ + GError *local_error; + gboolean ret; + gchar *response; + guint refresh_timeout_id; + + ret = FALSE; + + /* OK, sit around and wait until the mailbox changes (e.g. new mail arriving)... For + * we use the IMAP IDLE command, see http://tools.ietf.org/html/rfc2177, if available + * + * (TODO: handle IDLE not being available) + * + * Note that this operation can be interrupted by another thread + * cancelling + * + * client->worker_cancellable + * + * If this happens then we reset the cancellable (so it can be cancelled + * again) and then our caller can do another loop (e.g. SEARCH, FETCH all + * messages of interest), increasing client->num_completed_fetches (and waking up + * waiters) when done. + */ + local_error = NULL; + g_cancellable_reset (client->worker_cancellable); + + /* We want a nice long timeout here to conserve battery power so + * set socket timeout to infinite and schedule our own refresh. + * + * Note that IMAP IDLE suggests that we should not sit and wait + * too long so we pick 25 minutes here. + */ + g_socket_set_timeout (g_socket_connection_get_socket (client->c), 0); + + /* TODO: hmm.. safe to use the default GMainContext? The only alternative + * is to create our own thread + */ + refresh_timeout_id = g_timeout_add (25 * 60 * 1000, + refresh_while_idling_cb, + client); + response = goa_backend_imap_client_run_command_sync (client, + "IDLE", + client->worker_cancellable, + &local_error); + g_source_remove (refresh_timeout_id); + if (response == NULL) + { + if ((local_error->domain == G_IO_ERROR && local_error->code == G_IO_ERROR_CANCELLED)) + { + g_error_free (local_error); + } + else if ((local_error->domain == G_IO_ERROR && local_error->code == G_IO_ERROR_TIMED_OUT)) + { + g_error_free (local_error); + } + else + { + g_propagate_error (error, local_error); + goto out; + } + } + else + { + g_free (response); + } + + ret = TRUE; + + out: + return ret; +} + + +/* ---------------------------------------------------------------------------------------------------- */ + +/* worker thread */ +static gpointer +goa_backend_imap_client_worker_thread_func (gpointer user_data) +{ + GoaBackendImapClient *client = GOA_BACKEND_IMAP_CLIENT (user_data); + GError *error; + + /* This is the main loop where we idle, then refresh, then idle, + * then refresh again and around and around she goes... + */ + while (TRUE) + { + //g_debug ("idling in worker thread"); + error = NULL; + if (!goa_backend_imap_client_do_idle (client, &error)) + goto out; + + /* exit worker thread if being asked to close */ + if (client->closed) + { + error = NULL; + goto out; + } + + //g_debug ("refreshing in worker thread"); + error = NULL; + if (!goa_backend_imap_client_do_refresh (client, NULL, &error)) + goto out; + + /* Done with a loop, notify threads waiting on us (if any) */ + //g_debug ("notifying waiters"); + g_mutex_lock (client->num_completed_fetches_mutex); + client->num_completed_fetches += 1; + g_cond_broadcast (client->num_completed_fetches_cond); + g_mutex_unlock (client->num_completed_fetches_mutex); + + /* let the user know that we completed a loop and we're now idling */ + //g_debug ("emitting ::updated signal"); + emit_updated_in_idle (client); + } + + out: + //g_debug ("worker done"); + emit_closed_in_idle (client, error); + if (error != NULL) + g_error_free (error); + g_object_unref (client); + return NULL; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/** + * goa_backend_imap_client_get_closed: + * @client: A #GoaBackendImapClient. + * + * Gets if @client is closed. + * + * Returns: %TRUE if the connection to the IMAP server has been closed, %FALSE otherwise. + */ +gboolean +goa_backend_imap_client_get_closed (GoaBackendImapClient *client) +{ + g_return_val_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client), FALSE); + return client->closed; +} + +/** + * goa_backend_imap_client_close: + * @client: A #GoaBackendImapClient. + * + * Closes the connection used by @client. + */ +void +goa_backend_imap_client_close (GoaBackendImapClient *client) +{ + g_return_if_fail (GOA_IS_BACKEND_IMAP_CLIENT (client)); + client->closed = TRUE; + g_cancellable_cancel (client->worker_cancellable); + /* TODO: could join on client->worker_thread ... */ +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static gboolean +goa_backend_imap_client_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + GoaBackendImapClient *client = GOA_BACKEND_IMAP_CLIENT (initable); + gboolean ret; + + ret = FALSE; + + if (!goa_backend_imap_client_do_login (client, cancellable, error)) + goto out; + + if (!goa_backend_imap_client_do_refresh (client, cancellable, error)) + goto out; + + /* OK, made it this far - create a worker thread for idling and refreshing */ + client->worker_thread = g_thread_create (goa_backend_imap_client_worker_thread_func, + g_object_ref (client), + TRUE, /* joinable */ + error); + if (client->worker_thread == NULL) + goto out; + + ret = TRUE; + + out: + return ret; +} + +static void +goa_backend_imap_client__g_initable_iface_init (GInitableIface *iface) +{ + iface->init = goa_backend_imap_client_initable_init; +} + +static void +goa_backend_imap_client__g_async_initable_iface_init (GAsyncInitableIface *iface) +{ + /* use default implementation that runs the GInitable code in a thread */ +} + +/* ---------------------------------------------------------------------------------------------------- */ diff --git a/src/goa/goabackendimapclient.h b/src/goa/goabackendimapclient.h new file mode 100644 index 0000000..45000f9 --- /dev/null +++ b/src/goa/goabackendimapclient.h @@ -0,0 +1,80 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#if !defined (__GOA_BACKEND_INSIDE_GOA_BACKEND_H__) && !defined (GOA_BACKEND_COMPILATION) +#error "Only <goa/goabackend.h> can be included directly." +#endif + +#ifndef __GOA_BACKEND_IMAP_CLIENT_H__ +#define __GOA_BACKEND_IMAP_CLIENT_H__ + +#include <goa/goabackendtypes.h> + +G_BEGIN_DECLS + +#define GOA_TYPE_BACKEND_IMAP_CLIENT (goa_backend_imap_client_get_type ()) +#define GOA_BACKEND_IMAP_CLIENT(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOA_TYPE_BACKEND_IMAP_CLIENT, GoaBackendImapClient)) +#define GOA_IS_BACKEND_IMAP_CLIENT(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOA_TYPE_BACKEND_IMAP_CLIENT)) + +GType goa_backend_imap_client_get_type (void) G_GNUC_CONST; +void goa_backend_imap_client_new (const gchar *host_and_port, + gboolean use_tls, + GoaBackendImapAuth *auth, + const gchar *criteria, + guint query_offset, + guint query_size, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GoaBackendImapClient *goa_backend_imap_client_new_finish (GAsyncResult *res, + GError **error); +GoaBackendImapClient *goa_backend_imap_client_new_sync (const gchar *host_and_port, + gboolean use_tls, + GoaBackendImapAuth *auth, + const gchar *criteria, + guint query_offset, + guint query_size, + GCancellable *cancellable, + GError **error); + +gboolean goa_backend_imap_client_get_closed (GoaBackendImapClient *client); +void goa_backend_imap_client_close (GoaBackendImapClient *client); + +gint goa_backend_imap_client_get_num_unread (GoaBackendImapClient *client); +gint goa_backend_imap_client_get_num_messages (GoaBackendImapClient *client); +GList *goa_backend_imap_client_get_messages (GoaBackendImapClient *client); + +void goa_backend_imap_client_refresh (GoaBackendImapClient *client, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean goa_backend_imap_client_refresh_finish (GoaBackendImapClient *client, + GAsyncResult *res, + GError **error); +gboolean goa_backend_imap_client_refresh_sync (GoaBackendImapClient *client, + GCancellable *cancellable, + GError **error); + + +G_END_DECLS + +#endif /* __GOA_BACKEND_IMAP_CLIENT_H__ */ diff --git a/src/goa/goabackendimapmail.c b/src/goa/goabackendimapmail.c new file mode 100644 index 0000000..bf812e5 --- /dev/null +++ b/src/goa/goabackendimapmail.c @@ -0,0 +1,551 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#include "config.h" +#include <glib/gi18n-lib.h> + +#include <rest/oauth-proxy.h> +#include <json-glib/json-glib.h> + +#include "goabackendimapauth.h" +#include "goabackendimapclient.h" +#include "goabackendimapmessage.h" +#include "goabackendimapmail.h" + +/** + * GoaBackendImapMail: + * + * The #GoaBackendImapMail structure contains only private data and should + * only be accessed using the provided API. + */ +struct _GoaBackendImapMail +{ + /*< private >*/ + GoaMailSkeleton parent_instance; + + gchar *host_and_port; + gboolean use_tls; + GoaBackendImapAuth *auth; +}; + +typedef struct _GoaBackendImapMailClass GoaBackendImapMailClass; + +struct _GoaBackendImapMailClass +{ + GoaMailSkeletonClass parent_class; +}; + +enum +{ + PROP_0, + PROP_HOST_AND_PORT, + PROP_USE_TLS, + PROP_AUTH +}; + +/** + * SECTION:goabackendimapmail + * @title: GoaBackendImapMail + * @short_description: Implementation of the #GoaMail interface for IMAP servers + * + * #GoaBackendImapMail is an implementation of the #GoaMail D-Bus + * interface that uses a #GoaBackendImapClient instance to speak to a + * remote IMAP server. + */ + +static void goa_backend_imap_mail__goa_mail_iface_init (GoaMailIface *iface); + +G_DEFINE_TYPE_WITH_CODE (GoaBackendImapMail, goa_backend_imap_mail, GOA_TYPE_MAIL_SKELETON, + G_IMPLEMENT_INTERFACE (GOA_TYPE_MAIL, goa_backend_imap_mail__goa_mail_iface_init)); + +/* ---------------------------------------------------------------------------------------------------- */ + +static void +goa_backend_imap_mail_finalize (GObject *object) +{ + GoaBackendImapMail *mail = GOA_BACKEND_IMAP_MAIL (object); + + g_free (mail->host_and_port); + g_object_unref (mail->auth); + + G_OBJECT_CLASS (goa_backend_imap_mail_parent_class)->finalize (object); +} + +static void +goa_backend_imap_mail_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GoaBackendImapMail *mail = GOA_BACKEND_IMAP_MAIL (object); + + switch (prop_id) + { + case PROP_HOST_AND_PORT: + g_value_set_string (value, mail->host_and_port); + break; + + case PROP_USE_TLS: + g_value_set_boolean (value, mail->use_tls); + break; + + case PROP_AUTH: + g_value_set_object (value, mail->auth); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +goa_backend_imap_mail_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GoaBackendImapMail *mail = GOA_BACKEND_IMAP_MAIL (object); + + switch (prop_id) + { + case PROP_HOST_AND_PORT: + mail->host_and_port = g_value_dup_string (value); + break; + + case PROP_USE_TLS: + mail->use_tls = g_value_get_boolean (value); + break; + + case PROP_AUTH: + mail->auth = g_value_dup_object (value); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +goa_backend_imap_mail_init (GoaBackendImapMail *mail) +{ + /* Ensure D-Bus method invocations run in their own thread */ + g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (mail), + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); +} + +static void +goa_backend_imap_mail_class_init (GoaBackendImapMailClass *klass) +{ + GObjectClass *gobject_class; + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = goa_backend_imap_mail_finalize; + gobject_class->set_property = goa_backend_imap_mail_set_property; + gobject_class->get_property = goa_backend_imap_mail_get_property; + + g_object_class_install_property (gobject_class, + PROP_HOST_AND_PORT, + g_param_spec_string ("host-and-port", + "host-and-port", + "host-and-port", + NULL, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, + PROP_USE_TLS, + g_param_spec_boolean ("use-tls", + "use-tls", + "use-tls", + TRUE, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); + + g_object_class_install_property (gobject_class, + PROP_AUTH, + g_param_spec_object ("auth", + "auth", + "auth", + GOA_TYPE_BACKEND_IMAP_AUTH, + G_PARAM_READABLE | + G_PARAM_WRITABLE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS)); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +/** + * goa_backend_imap_mail_new: + * @host_and_port: The name and optionally port to connect to. + * @use_tls: Whether TLS should be used. + * @auth: Object used for authenticating the connection. + * + * Creates a new #GoaMail object. + * + * Returns: (type GoaBackendImapMail): A new #GoaMail instance. + */ +GoaMail * +goa_backend_imap_mail_new (const gchar *host_and_port, + gboolean use_tls, + GoaBackendImapAuth *auth) +{ + g_return_val_if_fail (host_and_port != NULL, NULL); + return GOA_MAIL (g_object_new (GOA_TYPE_BACKEND_IMAP_MAIL, + "host-and-port", host_and_port, + "use-tls", use_tls, + "auth", auth, + NULL)); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static void on_imap_client_updated (GoaBackendImapClient *client, + gpointer user_data); + +static void on_imap_client_closed (GoaBackendImapClient *client, + gpointer user_data); + +typedef struct +{ + volatile gint ref_count; + + GoaBackendImapMail *mail; + GoaMailQuery *query; + GCancellable *cancellable; + GoaObject *object; + GoaBackendImapClient *imap_client; + + guint name_watcher_id; +} QueryData; + +#if 0 +static QueryData * +query_data_ref (QueryData *data) +{ + g_atomic_int_inc (&data->ref_count); + return data; +} +#endif + +static void +query_data_unref (QueryData *data) +{ + if (g_atomic_int_dec_and_test (&data->ref_count)) + { + if (data->name_watcher_id) + g_bus_unwatch_name (data->name_watcher_id); + g_object_unref (data->mail); + g_object_unref (data->query); + g_object_unref (data->cancellable); + if (data->imap_client != NULL) + { + g_signal_handlers_disconnect_by_func (data->imap_client, G_CALLBACK (on_imap_client_updated), data); + g_signal_handlers_disconnect_by_func (data->imap_client, G_CALLBACK (on_imap_client_closed), data); + g_object_unref (data->imap_client); + } + g_slice_free (QueryData, data); + } +} + +static void +nuke_query (QueryData *data) +{ + /* yippee ki yay motherfucker */ + + /* unexport the D-Bus object */ + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (data->query)); + /* shutdown the IMAP client */ + if (data->imap_client != NULL) + goa_backend_imap_client_close (data->imap_client); + query_data_unref (data); +} + +static void +on_query_owner_vanished (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + QueryData *data = user_data; + nuke_query (data); +} + +static gboolean +mail_query_create_imap_client_sync (QueryData *data, + GError **error); + +/* runs in thread dedicated to the method invocation */ +static gboolean +mail_query_on_handle_close (GoaMailQuery *query, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + QueryData *data = user_data; + nuke_query (data); + return TRUE; +} + +/* runs in thread dedicated to the method invocation */ +static gboolean +mail_query_on_handle_refresh (GoaMailQuery *query, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + QueryData *data = user_data; + GError *error; + + if (data->imap_client == NULL) + { + error = NULL; + if (!mail_query_create_imap_client_sync (data, &error)) + { + g_prefix_error (&error, "Error creating IMAP client: "); + g_dbus_method_invocation_return_gerror (invocation, error); + g_error_free (error); + goto out; + } + goa_mail_query_complete_refresh (query, invocation); + goto out; + } + + error = NULL; + if (!goa_backend_imap_client_refresh_sync (data->imap_client, + NULL, /* GCancellable */ + &error)) + { + g_dbus_method_invocation_return_gerror (invocation, error); + g_error_free (error); + } + else + { + goa_mail_query_complete_refresh (query, invocation); + } + out: + return TRUE; +} + + +static void +on_imap_client_closed (GoaBackendImapClient *client, + gpointer user_data) +{ + QueryData *data = user_data; + + //g_debug ("##### on_imap_client_closed"); + + goa_mail_query_set_connected (data->query, FALSE); + g_signal_handlers_disconnect_by_func (data->imap_client, G_CALLBACK (on_imap_client_updated), data); + g_signal_handlers_disconnect_by_func (data->imap_client, G_CALLBACK (on_imap_client_closed), data); + g_object_unref (data->imap_client); + data->imap_client = NULL; +} + +static void +on_imap_client_updated (GoaBackendImapClient *client, + gpointer user_data) +{ + QueryData *data = user_data; + GList *messages; + GList *l; + GVariantBuilder builder; + + //g_debug ("##### on_imap_client_updated"); + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(stsssia{sv})")); + + messages = goa_backend_imap_client_get_messages (data->imap_client); + for (l = messages; l != NULL; l = l->next) + { + GoaBackendImapMessage *message = l->data; + gchar guid[20]; + const gchar *from; + const gchar *subject; + GoaBackendImapMessageFlags imap_flags; + gint flags; + GVariantBuilder extras_builder; + + from = goa_backend_imap_message_lookup_header (message, "From"); + subject = goa_backend_imap_message_lookup_header (message, "Subject"); + imap_flags = goa_backend_imap_message_get_flags (message); + + flags = 0; + if (!(imap_flags & GOA_BACKEND_IMAP_MESSAGE_FLAGS_SEEN)) + flags |= 1; + + g_variant_builder_init (&extras_builder, G_VARIANT_TYPE_VARDICT); + + g_snprintf (guid, sizeof guid, "%" G_GUINT64_FORMAT, + goa_backend_imap_message_get_uid (message)); + + g_variant_builder_add (&builder, "(stsssia{sv})", + guid, + goa_backend_imap_message_get_internal_date (message), + from != NULL ? from : _("No Sender"), + subject != NULL ? subject : _("No Subject"), + goa_backend_imap_message_get_excerpt (message), + flags, + &extras_builder); + } + goa_mail_query_set_result (data->query, g_variant_builder_end (&builder)); + goa_mail_query_set_num_unread (data->query, goa_backend_imap_client_get_num_unread (data->imap_client)); + goa_mail_query_set_num_messages (data->query, goa_backend_imap_client_get_num_messages (data->imap_client)); + g_dbus_interface_skeleton_flush (G_DBUS_INTERFACE_SKELETON (data->query)); + + g_list_foreach (messages, (GFunc) goa_backend_imap_message_ref, NULL); + g_list_free (messages); +} + +static gboolean +mail_query_create_imap_client_sync (QueryData *data, + GError **error) +{ + gboolean ret; + + g_return_val_if_fail (data->imap_client == NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + ret = FALSE; + + /* gets ourselves an IMAP client - it's not running yet */ + data->imap_client = goa_backend_imap_client_new_sync (data->mail->host_and_port, + data->mail->use_tls, + data->mail->auth, + goa_mail_query_get_criteria (data->query), + 0, /* offset */ + goa_mail_query_get_max_size (data->query), + NULL, /* GCancellable */ + error); + if (data->imap_client == NULL) + goto out; + + /* initial update */ + on_imap_client_updated (data->imap_client, data); + goa_mail_query_set_connected (data->query, TRUE); + + /* subsequent updates */ + g_signal_connect (data->imap_client, + "updated", + G_CALLBACK (on_imap_client_updated), + data); + + g_signal_connect (data->imap_client, + "closed", + G_CALLBACK (on_imap_client_closed), + data); + + ret = TRUE; + + out: + return ret; +} + +/* runs in thread dedicated to the method invocation */ +static gboolean +handle_create_query (GoaMail *_mail, + GDBusMethodInvocation *invocation, + const gchar *criteria, + gint max_size) +{ + GoaBackendImapMail *mail = GOA_BACKEND_IMAP_MAIL (_mail); + gchar *query_object_path; + GError *error; + QueryData *data; + static gint _g_query_count = 0; + + query_object_path = NULL; + + data = g_slice_new0 (QueryData); + data->ref_count = 1; + data->mail = g_object_ref (mail); + data->query = goa_mail_query_skeleton_new (); + data->cancellable = g_cancellable_new (); + + goa_mail_query_set_criteria (data->query, criteria); + goa_mail_query_set_max_size (data->query, max_size); + + /* Create the IMAP client - it's not fatal if the client is not + * working (could be the case if there is no network) since the user + * can call Refresh() to bring it up again. + */ + error = NULL; + if (!mail_query_create_imap_client_sync (data, &error)) + { + g_error_free (error); + } + + g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (data->query), + G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); + g_signal_connect (data->query, + "handle-refresh", + G_CALLBACK (mail_query_on_handle_refresh), + data); + g_signal_connect (data->query, + "handle-close", + G_CALLBACK (mail_query_on_handle_close), + data); + + query_object_path = g_strdup_printf ("/org/gnome/OnlineAccounts/mail_queries/%d", _g_query_count++); + + error = NULL; + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (data->query), + g_dbus_method_invocation_get_connection (invocation), + query_object_path, + &error)) + { + g_prefix_error (&error, "Error exporting mail query: "); + g_dbus_method_invocation_return_gerror (invocation, error); + query_data_unref (data); + goto out; + } + + data->name_watcher_id = g_bus_watch_name_on_connection (g_dbus_method_invocation_get_connection (invocation), + g_dbus_method_invocation_get_sender (invocation), + G_BUS_NAME_WATCHER_FLAGS_NONE, + NULL, /* name_appeared_handler */ + on_query_owner_vanished, + data, + NULL); + + /* TODO: set up things so only caller can access the created object? */ + + goa_mail_complete_create_query (GOA_MAIL (mail), invocation, query_object_path); + + out: + g_free (query_object_path); + return TRUE; /* invocation was handled */ +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static void +goa_backend_imap_mail__goa_mail_iface_init (GoaMailIface *iface) +{ + iface->handle_create_query = handle_create_query; +} + +/* ---------------------------------------------------------------------------------------------------- */ diff --git a/src/goa/goabackendimapmail.h b/src/goa/goabackendimapmail.h new file mode 100644 index 0000000..2d87aaf --- /dev/null +++ b/src/goa/goabackendimapmail.h @@ -0,0 +1,45 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#if !defined (__GOA_BACKEND_INSIDE_GOA_BACKEND_H__) && !defined (GOA_BACKEND_COMPILATION) +#error "Only <goa/goabackend.h> can be included directly." +#endif + +#ifndef __GOA_BACKEND_IMAP_MAIL_H__ +#define __GOA_BACKEND_IMAP_MAIL_H__ + +#include <goa/goabackendtypes.h> + +G_BEGIN_DECLS + +#define GOA_TYPE_BACKEND_IMAP_MAIL (goa_backend_imap_mail_get_type ()) +#define GOA_BACKEND_IMAP_MAIL(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GOA_TYPE_BACKEND_IMAP_MAIL, GoaBackendImapMail)) +#define GOA_IS_BACKEND_IMAP_MAIL(o) (G_TYPE_CHECK_INSTANCE_TYPE ((o), GOA_TYPE_BACKEND_IMAP_MAIL)) + +GType goa_backend_imap_mail_get_type (void) G_GNUC_CONST; +GoaMail *goa_backend_imap_mail_new (const gchar *host_and_port, + gboolean use_tls, + GoaBackendImapAuth *auth); + +G_END_DECLS + +#endif /* __GOA_BACKEND_IMAP_MAIL_H__ */ diff --git a/src/goa/goabackendimapmessage.c b/src/goa/goabackendimapmessage.c new file mode 100644 index 0000000..10e710c --- /dev/null +++ b/src/goa/goabackendimapmessage.c @@ -0,0 +1,263 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#include "config.h" +#include <glib/gi18n-lib.h> +#include <stdlib.h> + +#include "goabackendimapprivate.h" +#include "goabackendimapmessage.h" + +G_LOCK_DEFINE_STATIC (message_lock); + +/** + * SECTION:goabackendimapmessage + * @title: GoaBackendImapMessage + * @short_description: Message stored on an IMAP server + * + * The #GoaBackendImapMessage type is a boxed type representing a + * message stored on an IMAP server. See the #GoaBackendImapClient + * type for more details. + */ + +G_DEFINE_BOXED_TYPE (GoaBackendImapMessage, goa_backend_imap_message, + goa_backend_imap_message_ref, + goa_backend_imap_message_unref); + +/* ---------------------------------------------------------------------------------------------------- */ + +GoaBackendImapMessageFlags +goa_backend_imap_message_flags_from_strv (const gchar *const *strings) +{ + GoaBackendImapMessageFlags ret; + guint n; + + g_return_val_if_fail (strings != NULL, GOA_BACKEND_IMAP_MESSAGE_FLAGS_NONE); + + ret = GOA_BACKEND_IMAP_MESSAGE_FLAGS_NONE; + for (n = 0; strings[n] != NULL; n++) + { + if (g_strcmp0 (strings[n], "\\Seen") == 0) + ret |= GOA_BACKEND_IMAP_MESSAGE_FLAGS_SEEN; + else if (g_strcmp0 (strings[n], "\\Answered") == 0) + ret |= GOA_BACKEND_IMAP_MESSAGE_FLAGS_ANSWERED; + else if (g_strcmp0 (strings[n], "\\Flagged") == 0) + ret |= GOA_BACKEND_IMAP_MESSAGE_FLAGS_FLAGGED; + else if (g_strcmp0 (strings[n], "\\Deleted") == 0) + ret |= GOA_BACKEND_IMAP_MESSAGE_FLAGS_DELETED; + else if (g_strcmp0 (strings[n], "\\Draft") == 0) + ret |= GOA_BACKEND_IMAP_MESSAGE_FLAGS_DRAFT; + else if (g_strcmp0 (strings[n], "\\Recent") == 0) + ret |= GOA_BACKEND_IMAP_MESSAGE_FLAGS_RECENT; + else + g_debug ("TODO: unhandled flag `%s'", strings[n]); + } + return ret; +} + +gint +goa_backend_imap_message_compare_seqnum_reverse (const GoaBackendImapMessage *a, + const GoaBackendImapMessage *b) +{ + return b->seqnum - a->seqnum; +} + +/** + * goa_backend_imap_message_get_flags: + * @message: A #GoaBackendImapMessage. + * + * Gets the flags for @message. + * + * Returns: Flags from the #GoaBackendImapMessageFlags enumeration. + */ +GoaBackendImapMessageFlags +goa_backend_imap_message_get_flags (GoaBackendImapMessage *message) +{ + return message->flags; +} + +/** + * goa_backend_imap_message_get_uid: + * @message: A #GoaBackendImapMessage. + * + * Gets the unique id for @message. + * + * This includes the <ulink + * url="http://tools.ietf.org/html/rfc3501#section-2.3.1.1">unique + * identifier validity value</ulink> in the upper 32 bits. + * + * Returns: Unique id. + */ +guint64 +goa_backend_imap_message_get_uid (GoaBackendImapMessage *message) +{ + return message->uid; +} + +/** + * goa_backend_imap_message_get_internal_date: + * @message: A #GoaBackendImapMessage. + * + * Gets the <ulink + * url="http://tools.ietf.org/html/rfc3501#section-2.3.3">internal + * date</ulink> for @message. + * + * Returns: The internal date represented as seconds since the Epoch, Jan 1 1970 0:00 UTC. + */ +gint64 +goa_backend_imap_message_get_internal_date (GoaBackendImapMessage *message) +{ + return message->internal_date; +} + +/** + * goa_backend_imap_message_get_headers: + * @message: A #GoaBackendImapMessage. + * + * Gets the subset of <ulink + * url="http://tools.ietf.org/html/rfc2822">RFC 2822</ulink> headers + * extracted with the message. See also + * goa_backend_imap_message_lookup_header(). + * + * Returns: A string representing the headers. + */ +const gchar * +goa_backend_imap_message_get_headers (GoaBackendImapMessage *message) +{ + return message->rfc822_headers; +} + +/** + * goa_backend_imap_message_get_excerpt: + * @message: A #GoaBackendImapMessage. + * + * Gets an excerpt of the body of @message - the excerpt typically + * doesn't exceed 200 characters. + * + * Returns: An excerpt of the message body. + */ +const gchar * +goa_backend_imap_message_get_excerpt (GoaBackendImapMessage *message) +{ + return message->excerpt; +} + +static GHashTable * +parse_rfc822_headers (const gchar *rfc822_headers) +{ + GHashTable *ret; + gchar **lines; + guint n; + + ret = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL); + lines = g_strsplit (rfc822_headers, "\r\n", -1); + for (n = 0; lines[n] != NULL; n++) + { + const gchar *line = lines[n]; + const gchar *s; + + if (line[0] == '\0') + continue; + + s = strstr (line, ": "); + if (s != NULL) + { + gchar *key; + gchar *value; + key = g_strndup (line, s - line); + value = g_strdup (s + 2); + g_hash_table_insert (ret, key, value); + } + else + g_debug ("%s: mysterious line `%s'", G_STRFUNC, line); + } + g_strfreev (lines); + + return ret; +} + +/** + * goa_backend_imap_message_lookup_header: + * @message: A #GoaBackendImapMessage. + * @header: Header to lookup. + * + * Convenience function to lookup a header on @message. + * + * Returns: The value corresponding to @header or %NULL if not found. + */ +const gchar * +goa_backend_imap_message_lookup_header (GoaBackendImapMessage *message, + const gchar *header) +{ + g_return_val_if_fail (header != NULL, NULL); + + G_LOCK (message_lock); + if (message->headers_hash == NULL) + message->headers_hash = parse_rfc822_headers (message->rfc822_headers); + G_UNLOCK (message_lock); + + return g_hash_table_lookup (message->headers_hash, header); +} + +GoaBackendImapMessage * +goa_backend_imap_message_new (void) +{ + GoaBackendImapMessage *message; + message = g_slice_new0 (GoaBackendImapMessage); + message->ref_count = 1; + return message; +} + +/** + * goa_backend_imap_message_ref: + * @message: A #GoaBackendImapMessage. + * + * Increases the reference count of @message. + * + * Returns: @message. + */ +GoaBackendImapMessage * +goa_backend_imap_message_ref (GoaBackendImapMessage *message) +{ + g_atomic_int_inc (&message->ref_count); + return message; +} + +/** + * goa_backend_imap_message_unref: + * @message: A #GoaBackendImapMessage. + * + * Decreases the reference count of @message. When the reference count + * hits 0, the resources used by @message are freed. + */ +void +goa_backend_imap_message_unref (GoaBackendImapMessage *message) +{ + if (g_atomic_int_dec_and_test (&message->ref_count)) + { + g_free (message->excerpt); + g_free (message->rfc822_headers); + if (message->headers_hash != NULL) + g_hash_table_unref (message->headers_hash); + g_slice_free (GoaBackendImapMessage, message); + } +} diff --git a/src/goa/goabackendimapmessage.h b/src/goa/goabackendimapmessage.h new file mode 100644 index 0000000..7e5648a --- /dev/null +++ b/src/goa/goabackendimapmessage.h @@ -0,0 +1,49 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#if !defined (__GOA_BACKEND_INSIDE_GOA_BACKEND_H__) && !defined (GOA_BACKEND_COMPILATION) +#error "Only <goa/goabackend.h> can be included directly." +#endif + +#ifndef __GOA_BACKEND_IMAP_MESSAGE_H__ +#define __GOA_BACKEND_IMAP_MESSAGE_H__ + +#include <goa/goabackendtypes.h> + +G_BEGIN_DECLS + +#define GOA_TYPE_BACKEND_IMAP_MESSAGE (goa_backend_imap_message_get_type) + +GType goa_backend_imap_message_get_type (void) G_GNUC_CONST; +GoaBackendImapMessage *goa_backend_imap_message_ref (GoaBackendImapMessage *message); +void goa_backend_imap_message_unref (GoaBackendImapMessage *message); +GoaBackendImapMessageFlags goa_backend_imap_message_get_flags (GoaBackendImapMessage *message); +guint64 goa_backend_imap_message_get_uid (GoaBackendImapMessage *message); +gint64 goa_backend_imap_message_get_internal_date (GoaBackendImapMessage *message); +const gchar *goa_backend_imap_message_get_headers (GoaBackendImapMessage *message); +const gchar *goa_backend_imap_message_get_excerpt (GoaBackendImapMessage *message); +const gchar *goa_backend_imap_message_lookup_header (GoaBackendImapMessage *message, + const gchar *header); + +G_END_DECLS + +#endif /* __GOA_BACKEND_IMAP_MESSAGE_H__ */ diff --git a/src/goa/goabackendimapprivate.h b/src/goa/goabackendimapprivate.h new file mode 100644 index 0000000..005fcd1 --- /dev/null +++ b/src/goa/goabackendimapprivate.h @@ -0,0 +1,63 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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 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., 59 Temple Place, Suite 330, + * Boston, MA 02111-1307, USA. + * + * Author: David Zeuthen <davidz@redhat.com> + */ + +#if !defined (GOA_BACKEND_COMPILATION) +#error "This is a private header." +#endif + +#ifndef __GOA_BACKEND_IMAP_PRIVATE_H__ +#define __GOA_BACKEND_IMAP_PRIVATE_H__ + +#include <goa/goabackendtypes.h> + +G_BEGIN_DECLS + +/** + * GoaBackendImapMessage: + * + * The #GoaBackendImapMessage structure contains only private data and + * should only be accessed using the provided API. + */ +struct _GoaBackendImapMessage +{ + volatile gint ref_count; + gint seqnum; + guint64 uid; + GoaBackendImapMessageFlags flags; + gint64 internal_date; + gchar *rfc822_headers; + gchar *excerpt; + + /* calculated on-demand */ + GHashTable *headers_hash; +}; + +GoaBackendImapMessage *goa_backend_imap_message_new (void); + +gint goa_backend_imap_message_compare_seqnum_reverse (const GoaBackendImapMessage *a, + const GoaBackendImapMessage *b); + +GoaBackendImapMessageFlags goa_backend_imap_message_flags_from_strv (const gchar *const *strings); + +G_END_DECLS + +#endif /* __GOA_BACKEND_IMAP_PRIVATE_H__ */ diff --git a/src/goa/goabackendoauthprovider.c b/src/goa/goabackendoauthprovider.c index 2e66aab..a6ef272 100644 --- a/src/goa/goabackendoauthprovider.c +++ b/src/goa/goabackendoauthprovider.c @@ -1596,6 +1596,9 @@ goa_backend_oauth_provider_get_access_token_do_one (GetAccessTokenData *data) * loop</link> this method was called from. You can then call * goa_backend_oauth_provider_get_access_token_finish() to get the * result of the operation. + * + * See goa_backend_oauth_provider_get_access_token_sync() for the + * synchronous, blocking version of this method. */ void goa_backend_oauth_provider_get_access_token (GoaBackendOAuthProvider *provider, @@ -1712,6 +1715,90 @@ goa_backend_oauth_provider_get_access_token_finish (GoaBackendOAuthProvider *p /* ---------------------------------------------------------------------------------------------------- */ +typedef struct +{ + GAsyncResult *res; + GMainContext *context; + GMainLoop *loop; +} GetAccessTokenSyncData; + +static void +get_access_token_sync_cb (GoaBackendOAuthProvider *provider, + GAsyncResult *res, + gpointer user_data) +{ + GetAccessTokenSyncData *data = user_data; + data->res = g_object_ref (res); + g_main_loop_quit (data->loop); +} + +/** + * goa_backend_oauth_provider_get_access_token_sync: + * @provider: A #GoaBackendOAuthProvider. + * @object: A #GoaObject. + * @force_refresh: If set to %TRUE, forces a refresh of the access token, if possible. + * @out_access_token_secret: (out): The secret for the return access token. + * @out_access_token_expires_in: (out): Return location for how many seconds the returned token is valid for (0 if unknown) or %NULL. + * @cancellable: (allow-none): A #GCancellable or %NULL. + * @error: Return location for error or %NULL. + * + * Synchronously gets an access token for @object. The calling thread + * is blocked while the operation is pending. + * + * See goa_backend_oauth_provider_get_access_token() for the + * asynchronous non-blocking version and more details. + * + * Returns: The access token or %NULL if error is set. The returned + * string must be freed with g_free(). + */ +gchar * +goa_backend_oauth_provider_get_access_token_sync (GoaBackendOAuthProvider *provider, + GoaObject *object, + gboolean force_refresh, + gchar **out_access_token_secret, + gint *out_access_token_expires_in, + GCancellable *cancellable, + GError **error) +{ + GetAccessTokenSyncData *data; + gchar *ret; + + g_return_val_if_fail (GOA_IS_BACKEND_OAUTH_PROVIDER (provider), NULL); + g_return_val_if_fail (GOA_IS_OBJECT (object), NULL); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + data = g_new0 (GetAccessTokenSyncData, 1); + data->context = g_main_context_new (); + data->loop = g_main_loop_new (data->context, FALSE); + + g_main_context_push_thread_default (data->context); + + goa_backend_oauth_provider_get_access_token (provider, + object, + force_refresh, + cancellable, + (GAsyncReadyCallback) get_access_token_sync_cb, + data); + g_main_loop_run (data->loop); + ret = goa_backend_oauth_provider_get_access_token_finish (provider, + out_access_token_secret, + out_access_token_expires_in, + data->res, + error); + + g_main_context_pop_thread_default (data->context); + + g_main_context_unref (data->context); + g_main_loop_unref (data->loop); + g_object_unref (data->res); + g_free (data); + + return ret; +} + +/* ---------------------------------------------------------------------------------------------------- */ + static gboolean on_handle_get_access_token (GoaOAuthBased *object, GDBusMethodInvocation *invocation, gpointer user_data); diff --git a/src/goa/goabackendoauthprovider.h b/src/goa/goabackendoauthprovider.h index 3fc4a01..22c00b9 100644 --- a/src/goa/goabackendoauthprovider.h +++ b/src/goa/goabackendoauthprovider.h @@ -28,6 +28,7 @@ #define __GOA_BACKEND_OAUTH_PROVIDER_H__ #include <goa/goabackendtypes.h> +#include <goa/goabackendprovider.h> G_BEGIN_DECLS @@ -136,6 +137,13 @@ gchar *goa_backend_oauth_provider_get_access_token_finish (GoaBackendOAut gint *out_access_token_expires_in, GAsyncResult *res, GError **error); +gchar *goa_backend_oauth_provider_get_access_token_sync (GoaBackendOAuthProvider *provider, + GoaObject *object, + gboolean force_refresh, + gchar **out_access_token_secret, + gint *out_access_token_expires_in, + GCancellable *cancellable, + GError **error); /* ---------------------------------------------------------------------------------------------------- */ diff --git a/src/goa/goabackendprovider.c b/src/goa/goabackendprovider.c index 7b60515..02e3053 100644 --- a/src/goa/goabackendprovider.c +++ b/src/goa/goabackendprovider.c @@ -301,6 +301,79 @@ goa_backend_provider_ensure_credentials_finish (GoaBackendProvider *provider, return GOA_BACKEND_PROVIDER_GET_CLASS (provider)->ensure_credentials_finish (provider, out_expires_in, res, error); } +/* ---------------------------------------------------------------------------------------------------- */ + +typedef struct +{ + GAsyncResult *res; + GMainContext *context; + GMainLoop *loop; +} EnsureCredentialsSyncData; + +static void +ensure_credentials_sync_cb (GoaBackendOAuthProvider *provider, + GAsyncResult *res, + gpointer user_data) +{ + EnsureCredentialsSyncData *data = user_data; + data->res = g_object_ref (res); + g_main_loop_quit (data->loop); +} + +/** + * goa_backend_provider_ensure_credentials_sync: + * @provider: A #GoaBackendProvider. + * @object: A #GoaObject with a #GoaAccount interface. + * @out_expires_in: (out): Return location for how long the expired credentials are good for (0 if unknown) or %NULL. + * @cancellable: (allow-none): A #GCancellable or %NULL. + * @error: Return location for error or %NULL. + * + * Like goa_backend_provider_ensure_credentials() but blocks the + * calling thread until an answer is received. + * + * Returns: %TRUE if the credentials for the passed #GoaObject are valid, %FALSE if @error is set. + */ +gboolean +goa_backend_provider_ensure_credentials_sync (GoaBackendProvider *provider, + GoaObject *object, + gint *out_expires_in, + GCancellable *cancellable, + GError **error) +{ + EnsureCredentialsSyncData *data; + gboolean ret; + + g_return_val_if_fail (GOA_IS_BACKEND_PROVIDER (provider), FALSE); + g_return_val_if_fail (GOA_IS_OBJECT (object), FALSE); + g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + data = g_new0 (EnsureCredentialsSyncData, 1); + data->context = g_main_context_new (); + data->loop = g_main_loop_new (data->context, FALSE); + + g_main_context_push_thread_default (data->context); + + goa_backend_provider_ensure_credentials (provider, + object, + cancellable, + (GAsyncReadyCallback) ensure_credentials_sync_cb, + data); + g_main_loop_run (data->loop); + ret = goa_backend_provider_ensure_credentials_finish (provider, + out_expires_in, + data->res, + error); + + g_main_context_pop_thread_default (data->context); + + g_main_context_unref (data->context); + g_main_loop_unref (data->loop); + g_object_unref (data->res); + g_free (data); + return ret; +} + static void goa_backend_provider_ensure_credentials_real (GoaBackendProvider *provider, GoaObject *object, diff --git a/src/goa/goabackendprovider.h b/src/goa/goabackendprovider.h index d8902bf..955d311 100644 --- a/src/goa/goabackendprovider.h +++ b/src/goa/goabackendprovider.h @@ -90,7 +90,7 @@ struct _GoaBackendProviderClass const gchar *group, GError **error); - /* virtual but with defaut implementation */ + /* virtual but with default implementation */ void (*ensure_credentials) (GoaBackendProvider *provider, GoaObject *object, GCancellable *cancellable, @@ -158,6 +158,12 @@ gboolean goa_backend_provider_ensure_credentials_finish (GoaBackendProvider *pr GAsyncResult *res, GError **error); +gboolean goa_backend_provider_ensure_credentials_sync (GoaBackendProvider *provider, + GoaObject *object, + gint *out_expires_in, + GCancellable *cancellable, + GError **error); + /** * GOA_BACKEND_PROVIDER_EXTENSION_POINT_NAME: diff --git a/src/goa/goabackendtypes.h b/src/goa/goabackendtypes.h index 4fba4ab..d94df65 100644 --- a/src/goa/goabackendtypes.h +++ b/src/goa/goabackendtypes.h @@ -28,6 +28,7 @@ #define __GOA_BACKEND_TYPES_H__ #include <goa/goa.h> +#include <goa/goabackendenums.h> #include <gtk/gtk.h> G_BEGIN_DECLS @@ -53,6 +54,21 @@ typedef struct _GoaBackendYahooProvider GoaBackendYahooProvider; struct _GoaBackendTwitterProvider; typedef struct _GoaBackendTwitterProvider GoaBackendTwitterProvider; +struct _GoaBackendImapAuth; +typedef struct _GoaBackendImapAuth GoaBackendImapAuth; + +struct _GoaBackendImapAuthOAuth; +typedef struct _GoaBackendImapAuthOAuth GoaBackendImapAuthOAuth; + +struct _GoaBackendImapClient; +typedef struct _GoaBackendImapClient GoaBackendImapClient; + +struct _GoaBackendImapMessage; +typedef struct _GoaBackendImapMessage GoaBackendImapMessage; + +struct _GoaBackendImapMail; +typedef struct _GoaBackendImapMail GoaBackendImapMail; + G_END_DECLS #endif /* __GOA_BACKEND_TYPES_H__ */ |