1 /* GData plugin for Claws-Mail
2 * Copyright (C) 2011 Holger Berndt
3 * Copyright (C) 2011-2015 the Claws Mail team
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
21 # include "claws-features.h"
25 #include <glib/gi18n.h>
29 #include "cm_gdata_contacts.h"
30 #include "cm_gdata_prefs.h"
32 #include "addr_compl.h"
35 #include "prefs_common.h"
36 #include "mainwindow.h"
37 #include "common/log.h"
38 #include "common/xml.h"
39 #include "common/utils.h"
40 #include "common/passcrypt.h"
41 #include "gtk/gtkutils.h"
43 #include <gdata/gdata.h>
45 #define GDATA_CONTACTS_FILENAME "gdata_cache.xml"
47 #define GDATA_C1 "EOnuQt4fxED3WrO//iub3KLQMScIxXiT0VBD8RZUeKjkcm1zEBVMboeWDLYyqjJKZaL4oaZ24umWygbj19T2oJR6ZpjbCw=="
48 #define GDATA_C2 "QYjIgZblg/4RMCnEqNQypcHZba9ePqAN"
49 #define GDATA_C3 "XHEZEgO06YbWfQWOyYhE/ny5Q10aNOZlkQ=="
54 const gchar *family_name;
55 const gchar *given_name;
56 const gchar *full_name;
63 } CmGDataContactsCache;
66 CmGDataContactsCache contacts_cache;
67 gboolean cm_gdata_contacts_query_running = FALSE;
68 gchar *contacts_group_id = NULL;
69 GDataOAuth2Authorizer *authorizer = NULL;
70 GDataContactsService *service = NULL;
73 static void protect_fields_against_NULL(Contact *contact)
75 g_return_if_fail(contact != NULL);
77 /* protect fields against being NULL */
78 if(contact->full_name == NULL)
79 contact->full_name = g_strdup("");
80 if(contact->given_name == NULL)
81 contact->given_name = g_strdup("");
82 if(contact->family_name == NULL)
83 contact->family_name = g_strdup("");
88 const gchar *auth_uri;
90 } AuthCodeQueryButtonData;
93 static void auth_uri_link_button_clicked_cb(GtkButton *button, gpointer data)
95 AuthCodeQueryButtonData *auth_code_query_data = data;
96 open_uri(auth_code_query_data->auth_uri, prefs_common_get_uri_cmd());
97 gtk_widget_grab_focus(auth_code_query_data->entry);
100 static void auth_code_entry_changed_cb(GtkEditable *entry, gpointer data)
102 gtk_widget_set_sensitive(GTK_WIDGET(data), gtk_entry_get_text_length(GTK_ENTRY(entry)) > 0);
106 /* Returns the authorization code as newly allocated string, or NULL */
107 gchar* ask_user_for_auth_code(const gchar *auth_uri)
112 GtkWidget *link_button;
116 gchar *retval = NULL;
120 AuthCodeQueryButtonData *auth_code_query_data;
122 mainwin = mainwindow_get_mainwindow();
123 dialog = gtk_message_dialog_new_with_markup(mainwin ? GTK_WINDOW(mainwin->window) : NULL,
124 GTK_DIALOG_DESTROY_WITH_PARENT,
127 "<span weight=\"bold\" size=\"larger\">%s</span>", _("GData plugin: Authorization required"));
128 gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
129 _("You need to authorize Claws Mail to access your Google contact list to use the GData plugin."
130 "\n\nVisit Google's authorization page by pressing the button below. After you "
131 "confirmed the authorization, you will get an authorization code. Enter that code "
132 "in the field below to grant Claws Mail access to your Google contact list."));
133 gtk_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL);
134 btn_ok = gtk_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_OK, GTK_RESPONSE_OK);
135 gtk_window_set_resizable(GTK_WINDOW(dialog), TRUE);
136 gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER);
138 gtk_widget_set_sensitive(btn_ok, FALSE);
140 table = gtk_table_new(2, 3, FALSE);
141 gtk_table_set_row_spacings(GTK_TABLE(table), 8);
142 gtk_table_set_col_spacings(GTK_TABLE(table), 8);
144 str = g_strconcat("<b>", _("Step 1:"), "</b>", NULL);
145 label = gtk_label_new(str);
147 gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
148 gtk_table_attach(GTK_TABLE(table), label, 0, 1, 0, 1, 0, 0, 0, 0);
150 link_button = gtk_button_new_with_label(_("Click here to open the Google authorization page in a browser"));
151 auth_code_query_data = g_new0(AuthCodeQueryButtonData,1);
152 gtk_table_attach(GTK_TABLE(table), link_button, 1, 3, 0, 1, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
154 str = g_strconcat("<b>", _("Step 2:"), "</b>", NULL);
155 label = gtk_label_new(str);
157 gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
158 gtk_table_attach(GTK_TABLE(table), label, 0, 1, 1, 2, 0, 0, 0, 0);
160 gtk_table_attach(GTK_TABLE(table), gtk_label_new(_("Enter code:")), 1, 2, 1, 2, 0, 0, 0, 0);
162 entry = gtk_entry_new();
163 g_signal_connect(G_OBJECT(entry), "changed", (GCallback)auth_code_entry_changed_cb, (gpointer)btn_ok);
164 gtk_table_attach(GTK_TABLE(table), entry, 2, 3, 1, 2, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
166 auth_code_query_data->auth_uri = auth_uri;
167 auth_code_query_data->entry = entry;
168 g_signal_connect(G_OBJECT(link_button), "clicked", (GCallback)auth_uri_link_button_clicked_cb, (gpointer)auth_code_query_data);
170 vbox = gtk_vbox_new(FALSE, 4);
171 gtk_box_pack_start(GTK_BOX(vbox), table, FALSE, FALSE, 0);
173 gtk_box_pack_start(GTK_BOX(gtk_message_dialog_get_message_area(GTK_MESSAGE_DIALOG(dialog))), vbox, FALSE, FALSE, 0);
175 gtk_widget_show_all(dialog);
177 dlg_res = gtk_dialog_run(GTK_DIALOG(dialog));
180 case GTK_RESPONSE_DELETE_EVENT:
181 case GTK_RESPONSE_CANCEL:
183 case GTK_RESPONSE_OK:
184 retval = g_strdup(gtk_entry_get_text(GTK_ENTRY(entry)));
188 g_free(auth_code_query_data);
189 gtk_widget_destroy(dialog);
195 static void write_cache_to_file(void)
205 path = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, GDATA_CONTACTS_FILENAME, NULL);
206 pfile = prefs_write_open(path);
209 debug_print("GData plugin error: Cannot open file " GDATA_CONTACTS_FILENAME " for writing\n");
213 /* XML declarations */
214 xml_file_put_xml_decl(pfile->fp);
216 /* Build up XML tree */
219 tag = xml_tag_new("gdata");
220 xmlnode = xml_node_new(tag, NULL);
221 rootnode = g_node_new(xmlnode);
224 tag = xml_tag_new("contacts");
225 xmlnode = xml_node_new(tag, NULL);
226 contactsnode = g_node_new(xmlnode);
227 g_node_append(rootnode, contactsnode);
229 /* walk contacts cache */
230 for(walk = contacts_cache.contacts; walk; walk = walk->next)
233 Contact *contact = walk->data;
234 tag = xml_tag_new("contact");
235 xml_tag_add_attr(tag, xml_attr_new("family_name",contact->family_name));
236 xml_tag_add_attr(tag, xml_attr_new("given_name",contact->given_name));
237 xml_tag_add_attr(tag, xml_attr_new("full_name",contact->full_name));
238 xml_tag_add_attr(tag, xml_attr_new("address",contact->address));
239 xmlnode = xml_node_new(tag, NULL);
240 contactnode = g_node_new(xmlnode);
241 g_node_append(contactsnode, contactnode);
244 /* Actual writing and cleanup */
245 xml_write_tree(rootnode, pfile->fp);
246 if (prefs_file_close(pfile) < 0)
247 debug_print("GData plugin error: Failed to write file " GDATA_CONTACTS_FILENAME "\n");
249 debug_print("GData plugin: Wrote cache to file " GDATA_CONTACTS_FILENAME "\n");
252 xml_free_tree(rootnode);
255 static int add_gdata_contact_to_cache(GDataContactsContact *contact)
261 for(walk = gdata_contacts_contact_get_email_addresses(contact); walk; walk = walk->next) {
262 const gchar *email_address;
263 GDataGDEmailAddress *address = GDATA_GD_EMAIL_ADDRESS(walk->data);
265 email_address = gdata_gd_email_address_get_address(address);
266 if(email_address && (*email_address != '\0')) {
268 Contact *cached_contact;
270 name = gdata_contacts_contact_get_name(contact);
272 cached_contact = g_new0(Contact, 1);
273 cached_contact->full_name = g_strdup(gdata_gd_name_get_full_name(name));
274 cached_contact->given_name = g_strdup(gdata_gd_name_get_given_name(name));
275 cached_contact->family_name = g_strdup(gdata_gd_name_get_family_name(name));
276 cached_contact->address = g_strdup(email_address);
278 protect_fields_against_NULL(cached_contact);
280 contacts_cache.contacts = g_slist_prepend(contacts_cache.contacts, cached_contact);
282 debug_print("GData plugin: Added %s <%s>\n", cached_contact->full_name, cached_contact->address);
288 debug_print("GData plugin: Skipped received contact \"%s\" because it doesn't have an email address\n",
289 gdata_gd_name_get_full_name(gdata_contacts_contact_get_name(contact)));
294 static void free_contact(Contact *contact)
296 g_free((gpointer)contact->full_name);
297 g_free((gpointer)contact->family_name);
298 g_free((gpointer)contact->given_name);
299 g_free((gpointer)contact->address);
303 static void clear_contacts_cache(void)
306 for(walk = contacts_cache.contacts; walk; walk = walk->next)
307 free_contact(walk->data);
308 g_slist_free(contacts_cache.contacts);
309 contacts_cache.contacts = NULL;
312 static void cm_gdata_query_contacts_ready(GDataContactsService *service, GAsyncResult *res, gpointer data)
316 GError *error = NULL;
317 guint num_contacts = 0;
318 guint num_contacts_added = 0;
319 gchar *tmpstr1, *tmpstr2;
321 feed = gdata_service_query_finish(GDATA_SERVICE(service), res, &error);
322 cm_gdata_contacts_query_running = FALSE;
325 g_object_unref(feed);
326 log_error(LOG_PROTOCOL, _("GData plugin: Error querying for contacts: %s\n"), error->message);
332 clear_contacts_cache();
334 /* Iterate through the returned contacts and fill the cache */
335 for(walk = gdata_feed_get_entries(feed); walk; walk = walk->next) {
336 num_contacts_added += add_gdata_contact_to_cache(GDATA_CONTACTS_CONTACT(walk->data));
339 g_object_unref(feed);
340 contacts_cache.contacts = g_slist_reverse(contacts_cache.contacts);
341 /* TRANSLATORS: First part of "Added X of Y contacts to cache" */
342 tmpstr1 = g_strdup_printf(ngettext("Added %d of", "Added %d of", num_contacts_added), num_contacts_added);
343 /* TRANSLATORS: Second part of "Added X of Y contacts to cache" */
344 tmpstr2 = g_strdup_printf(ngettext("1 contact to the cache", "%d contacts to the cache", num_contacts), num_contacts);
345 log_message(LOG_PROTOCOL, "%s %s\n", tmpstr1, tmpstr2);
350 static void query_contacts(GDataContactsService *service)
352 GDataContactsQuery *query;
354 log_message(LOG_PROTOCOL, _("GData plugin: Starting async contacts query\n"));
356 query = gdata_contacts_query_new(NULL);
357 gdata_contacts_query_set_group(query, contacts_group_id);
358 gdata_query_set_max_results(GDATA_QUERY(query), cm_gdata_config.max_num_results);
359 gdata_contacts_service_query_contacts_async(service, GDATA_QUERY(query), NULL, NULL, NULL,
360 NULL, (GAsyncReadyCallback)cm_gdata_query_contacts_ready, NULL);
362 g_object_unref(query);
365 static void cm_gdata_query_groups_ready(GDataContactsService *service, GAsyncResult *res, gpointer data)
369 GError *error = NULL;
371 feed = gdata_service_query_finish(GDATA_SERVICE(service), res, &error);
374 g_object_unref(feed);
375 log_error(LOG_PROTOCOL, _("GData plugin: Error querying for groups: %s\n"), error->message);
380 /* Iterate through the returned groups and search for Contacts group id */
381 for(walk = gdata_feed_get_entries(feed); walk; walk = walk->next) {
382 const gchar *system_group_id;
383 GDataContactsGroup *group = GDATA_CONTACTS_GROUP(walk->data);
385 system_group_id = gdata_contacts_group_get_system_group_id(group);
386 if(system_group_id && !strcmp(system_group_id, GDATA_CONTACTS_GROUP_CONTACTS)) {
390 id = gdata_entry_get_id(GDATA_ENTRY(group));
392 /* possibly replace projection "full" by "base" */
393 pos = g_strrstr(id, "/full/");
395 GString *str = g_string_new("\0");
398 g_string_append_len(str, id, off);
399 g_string_append(str, "/base/");
400 g_string_append(str, id+off+strlen("/full/"));
401 g_string_append_c(str, '\0');
402 contacts_group_id = str->str;
403 g_string_free(str, FALSE);
406 contacts_group_id = g_strdup(id);
410 g_object_unref(feed);
412 log_message(LOG_PROTOCOL, _("GData plugin: Groups received\n"));
414 query_contacts(service);
417 static void query_for_contacts_group_id(GDataAuthorizer *authorizer)
419 log_message(LOG_PROTOCOL, _("GData plugin: Starting async groups query\n"));
421 gdata_contacts_service_query_groups_async(service, NULL, NULL, NULL, NULL, NULL,
422 (GAsyncReadyCallback)cm_gdata_query_groups_ready, NULL);
426 static void query_after_auth()
428 if(!contacts_group_id)
429 query_for_contacts_group_id(GDATA_AUTHORIZER(authorizer));
431 query_contacts(service);
435 static void cm_gdata_auth_ready(GDataOAuth2Authorizer *auth, GAsyncResult *res, gpointer data)
437 GError *error = NULL;
439 if(gdata_oauth2_authorizer_request_authorization_finish(auth, res, &error) == FALSE)
441 /* Notify the user of all errors except cancellation errors */
442 if(!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
444 log_error(LOG_PROTOCOL, _("GData plugin: Authorization error: %s\n"), error->message);
447 cm_gdata_contacts_query_running = FALSE;
451 log_message(LOG_PROTOCOL, _("GData plugin: Authorization successful\n"));
456 static void cm_gdata_interactive_auth()
461 log_message(LOG_PROTOCOL, _("GData plugin: Starting interactive authorization\n"));
463 auth_uri = gdata_oauth2_authorizer_build_authentication_uri(authorizer, cm_gdata_config.username, FALSE);
464 g_return_if_fail(auth_uri);
466 auth_code = ask_user_for_auth_code(auth_uri);
470 cm_gdata_contacts_query_running = TRUE;
471 log_message(LOG_PROTOCOL, _("GData plugin: Got authorization code, requesting authorization\n"));
472 gdata_oauth2_authorizer_request_authorization_async(authorizer, auth_code, NULL, (GAsyncReadyCallback)cm_gdata_auth_ready, NULL);
473 memset(auth_code, 0, strlen(auth_code));
478 log_warning(LOG_PROTOCOL, _("GData plugin: No authorization code received, authorization request cancelled\n"));
485 #if GDATA_CHECK_VERSION(0,17,2)
486 static void cm_gdata_refresh_ready(GDataOAuth2Authorizer *auth, GAsyncResult *res, gpointer data)
488 GError *error = NULL;
490 if(gdata_authorizer_refresh_authorization_finish(GDATA_AUTHORIZER(auth), res, &error) == FALSE)
492 /* Notify the user of all errors except cancellation errors */
493 if(!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
495 log_error(LOG_PROTOCOL, _("GData plugin: Authorization refresh error: %s\n"), error->message);
499 cm_gdata_interactive_auth();
504 log_message(LOG_PROTOCOL, _("GData plugin: Authorization refresh successful\n"));
511 /* returns allocated string which must be freed */
512 static guchar* decode(const gchar *in)
517 tmp = g_base64_decode(in, &len);
518 passcrypt_decrypt(tmp, len);
525 if(cm_gdata_contacts_query_running)
527 debug_print("GData plugin: Network query already in progress");
533 gchar *c1 = decode(GDATA_C1);
534 gchar *c2 = decode(GDATA_C2);
535 gchar *c3 = decode(GDATA_C3);
537 authorizer = gdata_oauth2_authorizer_new(c1, c2, c3, GDATA_TYPE_CONTACTS_SERVICE);
543 g_return_if_fail(authorizer);
547 service = gdata_contacts_service_new(GDATA_AUTHORIZER(authorizer));
549 g_return_if_fail(service);
551 if(!gdata_service_is_authorized(GDATA_SERVICE(service)))
553 #if GDATA_CHECK_VERSION(0,17,2)
554 /* Try to restore from saved refresh token.*/
555 if(cm_gdata_config.oauth2_refresh_token)
557 gchar *token = password_decrypt(cm_gdata_config.oauth2_refresh_token, NULL);
558 log_message(LOG_PROTOCOL, _("GData plugin: Trying to refresh authorization\n"));
559 gdata_oauth2_authorizer_set_refresh_token(authorizer, (token != NULL ? token : ""));
561 memset(token, 0, strlen(token));
564 gdata_authorizer_refresh_authorization_async(GDATA_AUTHORIZER(authorizer), NULL, (GAsyncReadyCallback)cm_gdata_refresh_ready, NULL);
568 cm_gdata_interactive_auth();
571 cm_gdata_interactive_auth();
581 static void add_contacts_to_list(GList **address_list, GSList *contacts)
585 for(walk = contacts; walk; walk = walk->next)
588 Contact *contact = walk->data;
590 ae = g_new0(address_entry, 1);
591 ae->name = g_strdup(contact->full_name);
592 ae->address = g_strdup(contact->address);
593 ae->grp_emails = NULL;
595 *address_list = g_list_prepend(*address_list, ae);
596 addr_compl_add_address1(ae->address, ae);
598 if(contact->given_name && *(contact->given_name) != '\0')
599 addr_compl_add_address1(contact->given_name, ae);
601 if(contact->family_name && *(contact->family_name) != '\0')
602 addr_compl_add_address1(contact->family_name, ae);
606 void cm_gdata_add_contacts(GList **address_list)
608 add_contacts_to_list(address_list, contacts_cache.contacts);
611 gboolean cm_gdata_update_contacts_cache(void)
613 if(prefs_common_get_prefs()->work_offline)
615 debug_print("GData plugin: Offline mode\n");
619 debug_print("GData plugin: Querying contacts\n");
625 void cm_gdata_contacts_done(void)
627 g_free(contacts_group_id);
628 contacts_group_id = NULL;
630 write_cache_to_file();
631 if(contacts_cache.contacts && !claws_is_exiting())
632 clear_contacts_cache();
636 #if GDATA_CHECK_VERSION(0,17,2)
637 /* store refresh token */
638 cm_gdata_config.oauth2_refresh_token = gdata_oauth2_authorizer_dup_refresh_token(authorizer);
641 g_object_unref(G_OBJECT(authorizer));
647 g_object_unref(G_OBJECT(service));
652 void cm_gdata_load_contacts_cache_from_file(void)
655 GNode *rootnode, *childnode, *contactnode;
658 path = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, GDATA_CONTACTS_FILENAME, NULL);
659 if(!is_file_exist(path)) {
664 /* no merging; make sure the cache is empty (this should be a noop, but just to be safe...) */
665 clear_contacts_cache();
667 rootnode = xml_parse_file(path);
671 xmlnode = rootnode->data;
673 /* Check that root entry is "gdata" */
674 if(strcmp2(xmlnode->tag->tag, "gdata") != 0) {
675 g_warning("wrong gdata cache file");
676 xml_free_tree(rootnode);
680 for(childnode = rootnode->children; childnode; childnode = childnode->next) {
682 xmlnode = childnode->data;
684 if(strcmp2(xmlnode->tag->tag, "contacts") != 0)
687 for(contactnode = childnode->children; contactnode; contactnode = contactnode->next)
689 Contact *cached_contact;
691 xmlnode = contactnode->data;
693 cached_contact = g_new0(Contact, 1);
694 /* Attributes of the branch nodes */
695 for(attributes = xmlnode->tag->attr; attributes; attributes = attributes->next) {
696 XMLAttr *attr = attributes->data;
698 if(attr && attr->name && attr->value) {
699 if(!strcmp2(attr->name, "full_name"))
700 cached_contact->full_name = g_strdup(attr->value);
701 else if(!strcmp2(attr->name, "given_name"))
702 cached_contact->given_name = g_strdup(attr->value);
703 else if(!strcmp2(attr->name, "family_name"))
704 cached_contact->family_name = g_strdup(attr->value);
705 else if(!strcmp2(attr->name, "address"))
706 cached_contact->address = g_strdup(attr->value);
710 if(cached_contact->address)
712 protect_fields_against_NULL(cached_contact);
714 contacts_cache.contacts = g_slist_prepend(contacts_cache.contacts, cached_contact);
715 debug_print("Read contact from cache: %s\n", cached_contact->full_name);
718 debug_print("Ignored contact without email address: %s\n", cached_contact->full_name ? cached_contact->full_name : "(null)");
724 xml_free_tree(rootnode);
726 contacts_cache.contacts = g_slist_reverse(contacts_cache.contacts);