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