fd508984c22705b720848c332c6ef0eec7e6722d
[claws.git] / src / plugins / gdata / cm_gdata_contacts.c
1 /* GData plugin for Claws-Mail
2  * Copyright (C) 2011 Holger Berndt
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17
18 #ifdef HAVE_CONFIG_H
19 #  include "config.h"
20 #  include "claws-features.h"
21 #endif
22
23 #include <glib.h>
24 #include <glib/gi18n.h>
25
26 #include <gtk/gtk.h>
27
28 #include "cm_gdata_contacts.h"
29 #include "cm_gdata_prefs.h"
30
31 #include "addr_compl.h"
32 #include "main.h"
33 #include "prefs_common.h"
34 #include "mainwindow.h"
35 #include "common/log.h"
36 #include "common/xml.h"
37 #include "common/utils.h"
38 #include "common/passcrypt.h"
39 #include "gtk/gtkutils.h"
40
41 #include <gdata/gdata.h>
42
43 #define GDATA_CONTACTS_FILENAME "gdata_cache.xml"
44
45 #define GDATA_C1 "EOnuQt4fxED3WrO//iub3KLQMScIxXiT0VBD8RZUeKjkcm1zEBVMboeWDLYyqjJKZaL4oaZ24umWygbj19T2oJR6ZpjbCw=="
46 #define GDATA_C2 "QYjIgZblg/4RMCnEqNQypcHZba9ePqAN"
47 #define GDATA_C3 "XHEZEgO06YbWfQWOyYhE/ny5Q10aNOZlkQ=="
48
49
50 typedef struct
51 {
52   const gchar *family_name;
53   const gchar *given_name;
54   const gchar *full_name;
55   const gchar *address;
56 } Contact;
57
58 typedef struct
59 {
60   GSList *contacts;
61 } CmGDataContactsCache;
62
63
64 CmGDataContactsCache contacts_cache;
65 gboolean cm_gdata_contacts_query_running = FALSE;
66 gchar *contacts_group_id = NULL;
67 GDataOAuth2Authorizer *authorizer = NULL;
68 GDataContactsService *service = NULL;
69
70
71 static void protect_fields_against_NULL(Contact *contact)
72 {
73   g_return_if_fail(contact != NULL);
74
75   /* protect fields against being NULL */
76   if(contact->full_name == NULL)
77     contact->full_name = g_strdup("");
78   if(contact->given_name == NULL)
79     contact->given_name = g_strdup("");
80   if(contact->family_name == NULL)
81     contact->family_name = g_strdup("");
82 }
83
84 typedef struct
85 {
86   const gchar *auth_uri;
87   GtkWidget *entry;
88 } AuthCodeQueryButtonData;
89
90
91 static void auth_uri_link_button_clicked_cb(GtkButton *button, gpointer data)
92 {
93   AuthCodeQueryButtonData *auth_code_query_data = data;
94   open_uri(auth_code_query_data->auth_uri, prefs_common_get_uri_cmd());
95   gtk_widget_grab_focus(auth_code_query_data->entry);
96 }
97
98 static void auth_code_entry_changed_cb(GtkEditable *entry, gpointer data)
99 {
100   gtk_widget_set_sensitive(GTK_WIDGET(data), gtk_entry_get_text_length(GTK_ENTRY(entry)) > 0);
101 }
102
103
104 /* Returns the authorization code as newly allocated string, or NULL */
105 gchar* ask_user_for_auth_code(const gchar *auth_uri)
106 {
107   GtkWidget *dialog;
108   GtkWidget *vbox;
109   GtkWidget *table;
110   GtkWidget *link_button;
111   GtkWidget *label;
112   GtkWidget *entry;
113   gchar *str;
114   gchar *retval = NULL;
115   MainWindow *mainwin;
116   gint dlg_res;
117   GtkWidget *btn_ok;
118   AuthCodeQueryButtonData *auth_code_query_data;
119
120   mainwin = mainwindow_get_mainwindow();
121   dialog = gtk_message_dialog_new_with_markup(mainwin ? GTK_WINDOW(mainwin->window) : NULL,
122       GTK_DIALOG_DESTROY_WITH_PARENT,
123       GTK_MESSAGE_INFO,
124       GTK_BUTTONS_NONE,
125       "<span weight=\"bold\" size=\"larger\">%s</span>", _("GData plugin: Authorization required"));
126   gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
127       _("You need to authorize Claws Mail to access your Google contact list to use the GData plugin."
128       "\n\nVisit Google's authorization page by pressing the button below. After you "
129       "confirmed the authorization, you will get an authorization code. Enter that code "
130       "in the field below to grant Claws Mail access to your Google contact list."));
131   gtk_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL);
132   btn_ok = gtk_dialog_add_button(GTK_DIALOG(dialog), GTK_STOCK_OK, GTK_RESPONSE_OK);
133   gtk_window_set_resizable(GTK_WINDOW(dialog), TRUE);
134   gtk_window_set_position(GTK_WINDOW(dialog), GTK_WIN_POS_CENTER);
135
136   gtk_widget_set_sensitive(btn_ok, FALSE);
137
138   table = gtk_table_new(2, 3, FALSE);
139   gtk_table_set_row_spacings(GTK_TABLE(table), 8);
140   gtk_table_set_col_spacings(GTK_TABLE(table), 8);
141
142   str = g_strconcat("<b>", _("Step 1:"), "</b>", NULL);
143   label = gtk_label_new(str);
144   g_free(str);
145   gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
146   gtk_table_attach(GTK_TABLE(table), label, 0, 1, 0, 1, 0, 0, 0, 0);
147
148   link_button = gtk_button_new_with_label(_("Click here to open the Google authorization page in a browser"));
149   auth_code_query_data = g_new0(AuthCodeQueryButtonData,1);
150   gtk_table_attach(GTK_TABLE(table), link_button, 1, 3, 0, 1, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
151
152   str = g_strconcat("<b>", _("Step 2:"), "</b>", NULL);
153   label = gtk_label_new(str);
154   g_free(str);
155   gtk_label_set_use_markup(GTK_LABEL(label), TRUE);
156   gtk_table_attach(GTK_TABLE(table), label, 0, 1, 1, 2, 0, 0, 0, 0);
157
158   gtk_table_attach(GTK_TABLE(table), gtk_label_new(_("Enter code:")), 1, 2, 1, 2, 0, 0, 0, 0);
159
160   entry = gtk_entry_new();
161   g_signal_connect(G_OBJECT(entry), "changed", (GCallback)auth_code_entry_changed_cb, (gpointer)btn_ok);
162   gtk_table_attach(GTK_TABLE(table), entry, 2, 3, 1, 2, GTK_EXPAND | GTK_FILL, GTK_EXPAND | GTK_FILL, 0, 0);
163
164   auth_code_query_data->auth_uri = auth_uri;
165   auth_code_query_data->entry = entry;
166   g_signal_connect(G_OBJECT(link_button), "clicked", (GCallback)auth_uri_link_button_clicked_cb, (gpointer)auth_code_query_data);
167
168   vbox = gtk_vbox_new(FALSE, 4);
169   gtk_box_pack_start(GTK_BOX(vbox), table, FALSE, FALSE, 0);
170
171   gtk_box_pack_start(GTK_BOX(gtk_message_dialog_get_message_area(GTK_MESSAGE_DIALOG(dialog))), vbox, FALSE, FALSE, 0);
172
173   gtk_widget_show_all(dialog);
174
175   dlg_res = gtk_dialog_run(GTK_DIALOG(dialog));
176   switch(dlg_res)
177   {
178   case GTK_RESPONSE_DELETE_EVENT:
179   case GTK_RESPONSE_CANCEL:
180     break;
181   case GTK_RESPONSE_OK:
182     retval = g_strdup(gtk_entry_get_text(GTK_ENTRY(entry)));
183     break;
184   }
185
186   g_free(auth_code_query_data);
187   gtk_widget_destroy(dialog);
188
189   return retval;
190 }
191
192
193 static void write_cache_to_file(void)
194 {
195   gchar *path;
196   PrefFile *pfile;
197   XMLTag *tag;
198   XMLNode *xmlnode;
199   GNode *rootnode;
200   GNode *contactsnode;
201   GSList *walk;
202
203   path = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, GDATA_CONTACTS_FILENAME, NULL);
204   pfile = prefs_write_open(path);
205   g_free(path);
206   if(pfile == NULL) {
207     debug_print("GData plugin error: Cannot open file " GDATA_CONTACTS_FILENAME " for writing\n");
208     return;
209   }
210
211   /* XML declarations */
212   xml_file_put_xml_decl(pfile->fp);
213
214   /* Build up XML tree */
215
216   /* root node */
217   tag = xml_tag_new("gdata");
218   xmlnode = xml_node_new(tag, NULL);
219   rootnode = g_node_new(xmlnode);
220
221   /* contacts node */
222   tag = xml_tag_new("contacts");
223   xmlnode = xml_node_new(tag, NULL);
224   contactsnode = g_node_new(xmlnode);
225   g_node_append(rootnode, contactsnode);
226
227   /* walk contacts cache */
228   for(walk = contacts_cache.contacts; walk; walk = walk->next)
229   {
230     GNode *contactnode;
231     Contact *contact = walk->data;
232     tag = xml_tag_new("contact");
233     xml_tag_add_attr(tag, xml_attr_new("family_name",contact->family_name));
234     xml_tag_add_attr(tag, xml_attr_new("given_name",contact->given_name));
235     xml_tag_add_attr(tag, xml_attr_new("full_name",contact->full_name));
236     xml_tag_add_attr(tag, xml_attr_new("address",contact->address));
237     xmlnode = xml_node_new(tag, NULL);
238     contactnode = g_node_new(xmlnode);
239     g_node_append(contactsnode, contactnode);
240   }
241
242   /* Actual writing and cleanup */
243   xml_write_tree(rootnode, pfile->fp);
244   if(prefs_file_close(pfile) < 0)
245     debug_print("GData plugin error: Failed to write file " GDATA_CONTACTS_FILENAME "\n");
246
247   debug_print("GData plugin error: Wrote cache to file " GDATA_CONTACTS_FILENAME "\n");
248
249   /* Free XML tree */
250   xml_free_tree(rootnode);
251 }
252
253 static int add_gdata_contact_to_cache(GDataContactsContact *contact)
254 {
255   GList *walk;
256   int retval;
257
258   retval = 0;
259   for(walk = gdata_contacts_contact_get_email_addresses(contact); walk; walk = walk->next) {
260     const gchar *email_address;
261     GDataGDEmailAddress *address = GDATA_GD_EMAIL_ADDRESS(walk->data);
262
263     email_address = gdata_gd_email_address_get_address(address);
264     if(email_address && (*email_address != '\0')) {
265       GDataGDName *name;
266       Contact *cached_contact;
267
268       name = gdata_contacts_contact_get_name(contact);
269
270       cached_contact = g_new0(Contact, 1);
271       cached_contact->full_name = g_strdup(gdata_gd_name_get_full_name(name));
272       cached_contact->given_name = g_strdup(gdata_gd_name_get_given_name(name));
273       cached_contact->family_name = g_strdup(gdata_gd_name_get_family_name(name));
274       cached_contact->address = g_strdup(email_address);
275
276       protect_fields_against_NULL(cached_contact);
277
278       contacts_cache.contacts = g_slist_prepend(contacts_cache.contacts, cached_contact);
279
280       debug_print("GData plugin: Added %s <%s>\n", cached_contact->full_name, cached_contact->address);
281       retval = 1;
282     }
283   }
284   if(retval == 0)
285   {
286     debug_print("GData plugin: Skipped received contact \"%s\" because it doesn't have an email address\n",
287         gdata_gd_name_get_full_name(gdata_contacts_contact_get_name(contact)));
288   }
289   return retval;
290 }
291
292 static void free_contact(Contact *contact)
293 {
294   g_free((gpointer)contact->full_name);
295   g_free((gpointer)contact->family_name);
296   g_free((gpointer)contact->given_name);
297   g_free((gpointer)contact->address);
298   g_free(contact);
299 }
300
301 static void clear_contacts_cache(void)
302 {
303   GSList *walk;
304   for(walk = contacts_cache.contacts; walk; walk = walk->next)
305     free_contact(walk->data);
306   g_slist_free(contacts_cache.contacts);
307   contacts_cache.contacts = NULL;
308 }
309
310 static void cm_gdata_query_contacts_ready(GDataContactsService *service, GAsyncResult *res, gpointer data)
311 {
312   GDataFeed *feed;
313   GList *walk;
314   GError *error = NULL;
315   guint num_contacts = 0;
316   guint num_contacts_added = 0;
317         gchar *tmpstr1, *tmpstr2;
318
319   feed = gdata_service_query_finish(GDATA_SERVICE(service), res, &error);
320   cm_gdata_contacts_query_running = FALSE;
321   if(error)
322   {
323     g_object_unref(feed);
324     log_error(LOG_PROTOCOL, _("GData plugin: Error querying for contacts: %s\n"), error->message);
325     g_error_free(error);
326     return;
327   }
328
329   /* clear cache */
330   clear_contacts_cache();
331
332   /* Iterate through the returned contacts and fill the cache */
333   for(walk = gdata_feed_get_entries(feed); walk; walk = walk->next) {
334     num_contacts_added += add_gdata_contact_to_cache(GDATA_CONTACTS_CONTACT(walk->data));
335     num_contacts++;
336   }
337   g_object_unref(feed);
338   contacts_cache.contacts = g_slist_reverse(contacts_cache.contacts);
339         /* i18n: First part of "Added X of Y contacts to cache" */
340   tmpstr1 = g_strdup_printf(ngettext("Added %d of", "Added %d of", num_contacts_added), num_contacts_added);
341         /* i18n: Second part of "Added X of Y contacts to cache" */
342   tmpstr2 = g_strdup_printf(ngettext("1 contact to the cache", "%d contacts to the cache", num_contacts), num_contacts);
343   log_message(LOG_PROTOCOL, "%s %s\n", tmpstr1, tmpstr2);
344         g_free(tmpstr1);
345         g_free(tmpstr2);
346 }
347
348 static void query_contacts(GDataContactsService *service)
349 {
350   GDataContactsQuery *query;
351
352   log_message(LOG_PROTOCOL, _("GData plugin: Starting async contacts query\n"));
353
354   query = gdata_contacts_query_new(NULL);
355   gdata_contacts_query_set_group(query, contacts_group_id);
356   gdata_query_set_max_results(GDATA_QUERY(query), cm_gdata_config.max_num_results);
357   gdata_contacts_service_query_contacts_async(service, GDATA_QUERY(query), NULL, NULL, NULL,
358   NULL, (GAsyncReadyCallback)cm_gdata_query_contacts_ready, NULL);
359
360   g_object_unref(query);
361 }
362
363 static void cm_gdata_query_groups_ready(GDataContactsService *service, GAsyncResult *res, gpointer data)
364 {
365   GDataFeed *feed;
366   GList *walk;
367   GError *error = NULL;
368
369   feed = gdata_service_query_finish(GDATA_SERVICE(service), res, &error);
370   if(error)
371   {
372     g_object_unref(feed);
373     log_error(LOG_PROTOCOL, _("GData plugin: Error querying for groups: %s\n"), error->message);
374     g_error_free(error);
375     return;
376   }
377
378   /* Iterate through the returned groups and search for Contacts group id */
379   for(walk = gdata_feed_get_entries(feed); walk; walk = walk->next) {
380     const gchar *system_group_id;
381     GDataContactsGroup *group = GDATA_CONTACTS_GROUP(walk->data);
382
383     system_group_id = gdata_contacts_group_get_system_group_id(group);
384     if(system_group_id && !strcmp(system_group_id, GDATA_CONTACTS_GROUP_CONTACTS)) {
385       gchar *pos;
386       const gchar *id;
387
388       id = gdata_entry_get_id(GDATA_ENTRY(group));
389
390       /* possibly replace projection "full" by "base" */
391       pos = g_strrstr(id, "/full/");
392       if(pos) {
393         GString *str = g_string_new("\0");
394         int off = pos-id;
395
396         g_string_append_len(str, id, off);
397         g_string_append(str, "/base/");
398         g_string_append(str, id+off+strlen("/full/"));
399         g_string_append_c(str, '\0');
400         contacts_group_id = str->str;
401         g_string_free(str, FALSE);
402       }
403       else
404         contacts_group_id = g_strdup(id);
405       break;
406     }
407   }
408   g_object_unref(feed);
409
410   log_message(LOG_PROTOCOL, _("GData plugin: Groups received\n"));
411
412   query_contacts(service);
413 }
414
415 static void query_for_contacts_group_id(GDataAuthorizer *authorizer)
416 {
417   log_message(LOG_PROTOCOL, _("GData plugin: Starting async groups query\n"));
418
419   gdata_contacts_service_query_groups_async(service, NULL, NULL, NULL, NULL, NULL,
420       (GAsyncReadyCallback)cm_gdata_query_groups_ready, NULL);
421 }
422
423
424 static void query_after_auth()
425 {
426   if(!contacts_group_id)
427     query_for_contacts_group_id(GDATA_AUTHORIZER(authorizer));
428   else
429     query_contacts(service);
430 }
431
432
433 static void cm_gdata_auth_ready(GDataOAuth2Authorizer *auth, GAsyncResult *res, gpointer data)
434 {
435   GError *error = NULL;
436
437   if(gdata_oauth2_authorizer_request_authorization_finish(auth, res, &error) == FALSE)
438   {
439     /* Notify the user of all errors except cancellation errors */
440     if(!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
441     {
442       log_error(LOG_PROTOCOL, _("GData plugin: Authorization error: %s\n"), error->message);
443     }
444     g_error_free(error);
445     cm_gdata_contacts_query_running = FALSE;
446     return;
447   }
448
449   log_message(LOG_PROTOCOL, _("GData plugin: Authorization successful\n"));
450
451   query_after_auth();
452 }
453
454 static void cm_gdata_interactive_auth()
455 {
456   gchar *auth_uri;
457   gchar *auth_code;
458
459   log_message(LOG_PROTOCOL, _("GData plugin: Starting interactive authorization\n"));
460
461   auth_uri = gdata_oauth2_authorizer_build_authentication_uri(authorizer, cm_gdata_config.username, FALSE);
462   g_return_if_fail(auth_uri);
463
464   auth_code = ask_user_for_auth_code(auth_uri);
465
466   if(auth_code)
467   {
468     cm_gdata_contacts_query_running = TRUE;
469     log_message(LOG_PROTOCOL, _("GData plugin: Got authorization code, requesting authorization\n"));
470     gdata_oauth2_authorizer_request_authorization_async(authorizer, auth_code, NULL, (GAsyncReadyCallback)cm_gdata_auth_ready, NULL);
471     memset(auth_code, 0, strlen(auth_code));
472     g_free(auth_code);
473   }
474   else
475   {
476     log_warning(LOG_PROTOCOL, _("GData plugin: No authorization code received, authorization request cancelled\n"));
477   }
478
479   g_free(auth_uri);
480 }
481
482
483 #if GDATA_CHECK_VERSION(0,17,2)
484 static void cm_gdata_refresh_ready(GDataOAuth2Authorizer *auth, GAsyncResult *res, gpointer data)
485 {
486   GError *error = NULL;
487
488   if(gdata_authorizer_refresh_authorization_finish(GDATA_AUTHORIZER(auth), res, &error) == FALSE)
489   {
490     /* Notify the user of all errors except cancellation errors */
491     if(!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
492     {
493       log_error(LOG_PROTOCOL, _("GData plugin: Authorization refresh error: %s\n"), error->message);
494     }
495     g_error_free(error);
496
497     cm_gdata_interactive_auth();
498
499     return;
500   }
501
502   log_message(LOG_PROTOCOL, _("GData plugin: Authorization refresh successful\n"));
503
504   query_after_auth();
505 }
506 #endif
507
508
509 /* returns allocated string which must be freed */
510 static guchar* decode(const gchar *in)
511 {
512   guchar *tmp;
513   gsize len;
514
515   tmp = g_base64_decode(in, &len);
516   passcrypt_decrypt(tmp, len);
517   return tmp;
518 }
519
520
521 static void query()
522 {
523   if(cm_gdata_contacts_query_running)
524   {
525     debug_print("GData plugin: Network query already in progress");
526     return;
527   }
528
529   if(!authorizer)
530   {
531     gchar *c1 = decode(GDATA_C1);
532     gchar *c2 = decode(GDATA_C2);
533     gchar *c3 = decode(GDATA_C3);
534
535     authorizer = gdata_oauth2_authorizer_new(c1, c2, c3, GDATA_TYPE_CONTACTS_SERVICE);
536
537     g_free(c1);
538     g_free(c2);
539     g_free(c3);
540   }
541   g_return_if_fail(authorizer);
542
543   if(!service)
544   {
545     service = gdata_contacts_service_new(GDATA_AUTHORIZER(authorizer));
546   }
547   g_return_if_fail(service);
548
549   if(!gdata_service_is_authorized(GDATA_SERVICE(service)))
550   {
551 #if GDATA_CHECK_VERSION(0,17,2)
552     /* Try to restore from saved refresh token.*/
553     if(cm_gdata_config.oauth2_refresh_token)
554     {
555       log_message(LOG_PROTOCOL, _("GData plugin: Trying to refresh authorization\n"));
556       gdata_oauth2_authorizer_set_refresh_token(authorizer, cm_gdata_config.oauth2_refresh_token);
557       gdata_authorizer_refresh_authorization_async(GDATA_AUTHORIZER(authorizer), NULL, (GAsyncReadyCallback)cm_gdata_refresh_ready, NULL);
558     }
559     else
560     {
561       cm_gdata_interactive_auth();
562     }
563 #else
564     cm_gdata_interactive_auth();
565 #endif
566   }
567   else
568   {
569     query_after_auth();
570   }
571 }
572
573
574 static void add_contacts_to_list(GList **address_list, GSList *contacts)
575 {
576   GSList *walk;
577
578   for(walk = contacts; walk; walk = walk->next)
579   {
580     address_entry *ae;
581     Contact *contact = walk->data;
582
583     ae = g_new0(address_entry, 1);
584     ae->name = g_strdup(contact->full_name);
585     ae->address = g_strdup(contact->address);
586     ae->grp_emails = NULL;
587
588     *address_list = g_list_prepend(*address_list, ae);
589     addr_compl_add_address1(ae->address, ae);
590
591     if(contact->given_name && *(contact->given_name) != '\0')
592       addr_compl_add_address1(contact->given_name, ae);
593
594     if(contact->family_name && *(contact->family_name) != '\0')
595       addr_compl_add_address1(contact->family_name, ae);
596   }
597 }
598
599 void cm_gdata_add_contacts(GList **address_list)
600 {
601   add_contacts_to_list(address_list, contacts_cache.contacts);
602 }
603
604 gboolean cm_gdata_update_contacts_cache(void)
605 {
606   if(prefs_common_get_prefs()->work_offline)
607   {
608     debug_print("GData plugin: Offline mode\n");
609   }
610   else
611   {
612     debug_print("GData plugin: Querying contacts\n");
613     query();
614   }
615   return TRUE;
616 }
617
618 void cm_gdata_contacts_done(void)
619 {
620   g_free(contacts_group_id);
621   contacts_group_id = NULL;
622
623   write_cache_to_file();
624   if(contacts_cache.contacts && !claws_is_exiting())
625     clear_contacts_cache();
626
627   if(authorizer)
628   {
629 #if GDATA_CHECK_VERSION(0,17,2)
630     /* store refresh token */
631     cm_gdata_config.oauth2_refresh_token = gdata_oauth2_authorizer_dup_refresh_token(authorizer);
632 #endif
633
634     g_object_unref(G_OBJECT(authorizer));
635     authorizer = NULL;
636   }
637
638   if(service)
639   {
640     g_object_unref(G_OBJECT(service));
641     service = NULL;
642   }
643 }
644
645 void cm_gdata_load_contacts_cache_from_file(void)
646 {
647   gchar *path;
648   GNode *rootnode, *childnode, *contactnode;
649   XMLNode *xmlnode;
650
651   path = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, GDATA_CONTACTS_FILENAME, NULL);
652   if(!is_file_exist(path)) {
653     g_free(path);
654     return;
655   }
656
657   /* no merging; make sure the cache is empty (this should be a noop, but just to be safe...) */
658   clear_contacts_cache();
659
660   rootnode = xml_parse_file(path);
661   g_free(path);
662   if(!rootnode)
663     return;
664   xmlnode = rootnode->data;
665
666   /* Check that root entry is "gdata" */
667   if(strcmp2(xmlnode->tag->tag, "gdata") != 0) {
668     g_warning("wrong gdata cache file");
669     xml_free_tree(rootnode);
670     return;
671   }
672
673   for(childnode = rootnode->children; childnode; childnode = childnode->next) {
674     GList *attributes;
675     xmlnode = childnode->data;
676
677     if(strcmp2(xmlnode->tag->tag, "contacts") != 0)
678       continue;
679
680     for(contactnode = childnode->children; contactnode; contactnode = contactnode->next)
681     {
682       Contact *cached_contact;
683
684       xmlnode = contactnode->data;
685
686       cached_contact = g_new0(Contact, 1);
687       /* Attributes of the branch nodes */
688       for(attributes = xmlnode->tag->attr; attributes; attributes = attributes->next) {
689         XMLAttr *attr = attributes->data;
690
691         if(attr && attr->name && attr->value) {
692           if(!strcmp2(attr->name, "full_name"))
693             cached_contact->full_name = g_strdup(attr->value);
694           else if(!strcmp2(attr->name, "given_name"))
695             cached_contact->given_name = g_strdup(attr->value);
696           else if(!strcmp2(attr->name, "family_name"))
697             cached_contact->family_name = g_strdup(attr->value);
698           else if(!strcmp2(attr->name, "address"))
699             cached_contact->address = g_strdup(attr->value);
700         }
701       }
702
703       if(cached_contact->address)
704       {
705         protect_fields_against_NULL(cached_contact);
706
707         contacts_cache.contacts = g_slist_prepend(contacts_cache.contacts, cached_contact);
708         debug_print("Read contact from cache: %s\n", cached_contact->full_name);
709       }
710       else {
711         debug_print("Ignored contact without email address: %s\n", cached_contact->full_name ? cached_contact->full_name : "(null)");
712       }
713     }
714   }
715
716   /* Free XML tree */
717   xml_free_tree(rootnode);
718
719   contacts_cache.contacts = g_slist_reverse(contacts_cache.contacts);
720 }