GData plugin: Harden against missing fields
[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
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 "cm_gdata_contacts.h"
28 #include "cm_gdata_prefs.h"
29
30 #include <gtk/gtk.h>
31 #include "addr_compl.h"
32 #include "main.h"
33 #include "prefs_common.h"
34 #include "common/log.h"
35 #include "common/xml.h"
36
37 #include <gdata/gdata.h>
38
39 #define GDATA_CONTACTS_FILENAME "gdata_cache.xml"
40
41 typedef struct
42 {
43   const gchar *family_name;
44   const gchar *given_name;
45   const gchar *full_name;
46   const gchar *address;
47 } Contact;
48
49 typedef struct
50 {
51   GSList *contacts;
52 } CmGDataContactsCache;
53
54
55 CmGDataContactsCache contacts_cache;
56 gboolean cm_gdata_contacts_query_running = FALSE;
57 gchar *contacts_group_id = NULL;
58
59 static void protect_fields_against_NULL(Contact *contact)
60 {
61   g_return_if_fail(contact != NULL);
62
63   /* protect fields against being NULL */
64   if(contact->full_name == NULL)
65     contact->full_name = g_strdup("");
66   if(contact->given_name == NULL)
67     contact->given_name = g_strdup("");
68   if(contact->family_name == NULL)
69     contact->family_name = g_strdup("");
70 }
71
72
73 static void write_cache_to_file(void)
74 {
75   gchar *path;
76   PrefFile *pfile;
77   XMLTag *tag;
78   XMLNode *xmlnode;
79   GNode *rootnode;
80   GNode *contactsnode;
81   GSList *walk;
82
83   path = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, GDATA_CONTACTS_FILENAME, NULL);
84   pfile = prefs_write_open(path);
85   g_free(path);
86   if(pfile == NULL) {
87     debug_print("GData plugin error: Cannot open file " GDATA_CONTACTS_FILENAME " for writing\n");
88     return;
89   }
90
91   /* XML declarations */
92   xml_file_put_xml_decl(pfile->fp);
93
94   /* Build up XML tree */
95
96   /* root node */
97   tag = xml_tag_new("gdata");
98   xmlnode = xml_node_new(tag, NULL);
99   rootnode = g_node_new(xmlnode);
100
101   /* contacts node */
102   tag = xml_tag_new("contacts");
103   xmlnode = xml_node_new(tag, NULL);
104   contactsnode = g_node_new(xmlnode);
105   g_node_append(rootnode, contactsnode);
106
107   /* walk contacts cache */
108   for(walk = contacts_cache.contacts; walk; walk = walk->next)
109   {
110     GNode *contactnode;
111     Contact *contact = walk->data;
112     tag = xml_tag_new("contact");
113     xml_tag_add_attr(tag, xml_attr_new("family_name",contact->family_name));
114     xml_tag_add_attr(tag, xml_attr_new("given_name",contact->given_name));
115     xml_tag_add_attr(tag, xml_attr_new("full_name",contact->full_name));
116     xml_tag_add_attr(tag, xml_attr_new("address",contact->address));
117     xmlnode = xml_node_new(tag, NULL);
118     contactnode = g_node_new(xmlnode);
119     g_node_append(contactsnode, contactnode);
120   }
121
122   /* Actual writing and cleanup */
123   xml_write_tree(rootnode, pfile->fp);
124   if(prefs_file_close(pfile) < 0)
125     debug_print("GData plugin error: Failed to write file " GDATA_CONTACTS_FILENAME "\n");
126
127   debug_print("GData plugin error: Wrote cache to file " GDATA_CONTACTS_FILENAME "\n");
128
129   /* Free XML tree */
130   xml_free_tree(rootnode);
131 }
132
133 static int add_gdata_contact_to_cache(GDataContactsContact *contact)
134 {
135   GList *walk;
136   int retval;
137
138   retval = 0;
139   for(walk = gdata_contacts_contact_get_email_addresses(contact); walk; walk = walk->next) {
140     const gchar *email_address;
141     GDataGDEmailAddress *address = GDATA_GD_EMAIL_ADDRESS(walk->data);
142
143     email_address = gdata_gd_email_address_get_address(address);
144     if(email_address && (*email_address != '\0')) {
145       GDataGDName *name;
146       Contact *cached_contact;
147
148       name = gdata_contacts_contact_get_name(contact);
149
150       cached_contact = g_new0(Contact, 1);
151       cached_contact->full_name = g_strdup(gdata_gd_name_get_full_name(name));
152       cached_contact->given_name = g_strdup(gdata_gd_name_get_given_name(name));
153       cached_contact->family_name = g_strdup(gdata_gd_name_get_family_name(name));
154       cached_contact->address = g_strdup(email_address);
155
156       protect_fields_against_NULL(cached_contact);
157
158       contacts_cache.contacts = g_slist_prepend(contacts_cache.contacts, cached_contact);
159
160       debug_print("GData plugin: Added %s <%s>\n", cached_contact->full_name, cached_contact->address);
161       retval = 1;
162     }
163   }
164   if(retval == 0)
165   {
166     debug_print("GData plugin: Skipped received contact \"%s\" because it doesn't have an email address\n",
167         gdata_gd_name_get_full_name(gdata_contacts_contact_get_name(contact)));
168   }
169   return retval;
170 }
171
172 static void free_contact(Contact *contact)
173 {
174   g_free((gpointer)contact->full_name);
175   g_free((gpointer)contact->family_name);
176   g_free((gpointer)contact->given_name);
177   g_free((gpointer)contact->address);
178   g_free(contact);
179 }
180
181 static void clear_contacts_cache(void)
182 {
183   GSList *walk;
184   for(walk = contacts_cache.contacts; walk; walk = walk->next)
185     free_contact(walk->data);
186   g_slist_free(contacts_cache.contacts);
187   contacts_cache.contacts = NULL;
188 }
189
190 static void cm_gdata_query_contacts_ready(GDataContactsService *service, GAsyncResult *res, gpointer data)
191 {
192   GDataFeed *feed;
193   GList *walk;
194   GError *error = NULL;
195   guint num_contacts = 0;
196   guint num_contacts_added = 0;
197         gchar *tmpstr1, *tmpstr2;
198
199   feed = gdata_service_query_finish(GDATA_SERVICE(service), res, &error);
200   cm_gdata_contacts_query_running = FALSE;
201   if(error)
202   {
203     g_object_unref(feed);
204     log_error(LOG_PROTOCOL, _("GData plugin: Error querying for contacts: %s\n"), error->message);
205     g_error_free(error);
206     return;
207   }
208
209   /* clear cache */
210   clear_contacts_cache();
211
212   /* Iterate through the returned contacts and fill the cache */
213   for(walk = gdata_feed_get_entries(feed); walk; walk = walk->next) {
214     num_contacts_added += add_gdata_contact_to_cache(GDATA_CONTACTS_CONTACT(walk->data));
215     num_contacts++;
216   }
217   g_object_unref(feed);
218   contacts_cache.contacts = g_slist_reverse(contacts_cache.contacts);
219         /* i18n: First part of "Added X of Y contacts to cache" */
220   tmpstr1 = g_strdup_printf(ngettext("Added %d of", "Added %d of", num_contacts_added), num_contacts_added);
221         /* i18n: Second part of "Added X of Y contacts to cache" */
222   tmpstr2 = g_strdup_printf(ngettext("1 contact to the cache", "%d contacts to the cache", num_contacts), num_contacts);
223   log_message(LOG_PROTOCOL, "%s %s\n", tmpstr1, tmpstr2);
224         g_free(tmpstr1);
225         g_free(tmpstr2);
226 }
227
228 static void query_after_auth(GDataContactsService *service)
229 {
230   GDataContactsQuery *query;
231
232   log_message(LOG_PROTOCOL, _("GData plugin: Starting async contacts query\n"));
233
234   query = gdata_contacts_query_new(NULL);
235   gdata_contacts_query_set_group(query, contacts_group_id);
236   gdata_query_set_max_results(GDATA_QUERY(query), cm_gdata_config.max_num_results);
237   gdata_contacts_service_query_contacts_async(service, GDATA_QUERY(query), NULL, NULL, NULL,
238 #ifdef HAVE_GDATA_VERSION_0_9_1
239   NULL,
240 #endif
241   (GAsyncReadyCallback)cm_gdata_query_contacts_ready, NULL);
242
243   g_object_unref(query);
244 }
245
246 #ifdef HAVE_GDATA_VERSION_0_9_1
247 static void cm_gdata_query_groups_ready(GDataContactsService *service, GAsyncResult *res, gpointer data)
248 {
249   GDataFeed *feed;
250   GList *walk;
251   GError *error = NULL;
252
253   feed = gdata_service_query_finish(GDATA_SERVICE(service), res, &error);
254   if(error)
255   {
256     g_object_unref(feed);
257     log_error(LOG_PROTOCOL, _("GData plugin: Error querying for groups: %s\n"), error->message);
258     g_error_free(error);
259     return;
260   }
261
262   /* Iterate through the returned groups and search for Contacts group id */
263   for(walk = gdata_feed_get_entries(feed); walk; walk = walk->next) {
264     const gchar *system_group_id;
265     GDataContactsGroup *group = GDATA_CONTACTS_GROUP(walk->data);
266
267     system_group_id = gdata_contacts_group_get_system_group_id(group);
268     if(system_group_id && !strcmp(system_group_id, GDATA_CONTACTS_GROUP_CONTACTS)) {
269       gchar *pos;
270       const gchar *id;
271
272       id = gdata_entry_get_id(GDATA_ENTRY(group));
273
274       /* possibly replace projection "full" by "base" */
275       pos = g_strrstr(id, "/full/");
276       if(pos) {
277         GString *str = g_string_new("\0");
278         int off = pos-id;
279
280         g_string_append_len(str, id, off);
281         g_string_append(str, "/base/");
282         g_string_append(str, id+off+strlen("/full/"));
283         g_string_append_c(str, '\0');
284         contacts_group_id = str->str;
285         g_string_free(str, FALSE);
286       }
287       else
288         contacts_group_id = g_strdup(id);
289       break;
290     }
291   }
292   g_object_unref(feed);
293
294   log_message(LOG_PROTOCOL, _("GData plugin: Groups received\n"));
295
296   query_after_auth(service);
297 }
298 #endif
299
300 #ifdef HAVE_GDATA_VERSION_0_9
301 static void query_for_contacts_group_id(GDataClientLoginAuthorizer *authorizer)
302 {
303   GDataContactsService *service;
304 #ifdef HAVE_GDATA_VERSION_0_9_1
305
306   log_message(LOG_PROTOCOL, _("GData plugin: Starting async groups query\n"));
307
308   service = gdata_contacts_service_new(GDATA_AUTHORIZER(authorizer));
309   gdata_contacts_service_query_groups_async(service, NULL, NULL, NULL, NULL, NULL,
310       (GAsyncReadyCallback)cm_gdata_query_groups_ready, NULL);
311 #else
312   service = gdata_contacts_service_new(GDATA_AUTHORIZER(authorizer));
313   query_after_auth(service);
314 #endif
315   g_object_unref(service);
316 }
317
318 static void cm_gdata_auth_ready(GDataClientLoginAuthorizer *authorizer, GAsyncResult *res, gpointer data)
319 {
320   GError *error = NULL;
321
322   if(gdata_client_login_authorizer_authenticate_finish(authorizer, res, &error) == FALSE)
323   {
324     log_error(LOG_PROTOCOL, _("GData plugin: Authentication error: %s\n"), error->message);
325     g_error_free(error);
326     cm_gdata_contacts_query_running = FALSE;
327     return;
328   }
329
330   log_message(LOG_PROTOCOL, _("GData plugin: Authenticated\n"));
331
332   if(!contacts_group_id)
333   {
334     query_for_contacts_group_id(authorizer);
335   }
336   else {
337     GDataContactsService *service;
338     service = gdata_contacts_service_new(GDATA_AUTHORIZER(authorizer));
339     query_after_auth(service);
340     g_object_unref(service);
341   }
342 }
343 #else
344 static void cm_gdata_auth_ready(GDataContactsService *service, GAsyncResult *res, gpointer data)
345 {
346   GError *error = NULL;
347
348   if(!gdata_service_authenticate_finish(GDATA_SERVICE(service), res, &error))
349   {
350     log_error(LOG_PROTOCOL, _("GData plugin: Authentication error: %s\n"), error->message);
351     g_error_free(error);
352     cm_gdata_contacts_query_running = FALSE;
353     return;
354   }
355
356   log_message(LOG_PROTOCOL, _("GData plugin: Authenticated\n"));
357
358   query_after_auth(service);
359 }
360 #endif
361 static void query()
362 {
363
364 #ifdef HAVE_GDATA_VERSION_0_9
365   GDataClientLoginAuthorizer *authorizer;
366 #else
367   GDataContactsService *service;
368 #endif
369
370   if(cm_gdata_contacts_query_running)
371   {
372     debug_print("GData plugin: Network query already in progress");
373     return;
374   }
375
376   log_message(LOG_PROTOCOL, _("GData plugin: Starting async authentication\n"));
377
378 #ifdef HAVE_GDATA_VERSION_0_9
379   authorizer = gdata_client_login_authorizer_new(CM_GDATA_CLIENT_ID, GDATA_TYPE_CONTACTS_SERVICE);
380   gdata_client_login_authorizer_authenticate_async(authorizer, cm_gdata_config.username, cm_gdata_config.password, NULL, (GAsyncReadyCallback)cm_gdata_auth_ready, NULL);
381   cm_gdata_contacts_query_running = TRUE;
382 #else
383   service = gdata_contacts_service_new(CM_GDATA_CLIENT_ID);
384   cm_gdata_contacts_query_running = TRUE;
385   gdata_service_authenticate_async(GDATA_SERVICE(service), cm_gdata_config.username, cm_gdata_config.password, NULL,
386       (GAsyncReadyCallback)cm_gdata_auth_ready, NULL);
387 #endif
388
389
390 #ifdef HAVE_GDATA_VERSION_0_9
391   g_object_unref(authorizer);
392 #else
393   g_object_unref(service);
394 #endif
395
396 }
397
398
399 static void add_contacts_to_list(GList **address_list, GSList *contacts)
400 {
401   GSList *walk;
402
403   for(walk = contacts; walk; walk = walk->next)
404   {
405     address_entry *ae;
406     Contact *contact = walk->data;
407
408     ae = g_new0(address_entry, 1);
409     ae->name = g_strdup(contact->full_name);
410     ae->address = g_strdup(contact->address);
411     ae->grp_emails = NULL;
412
413     *address_list = g_list_prepend(*address_list, ae);
414     addr_compl_add_address1(ae->address, ae);
415
416     if(contact->given_name && *(contact->given_name) != '\0')
417       addr_compl_add_address1(contact->given_name, ae);
418
419     if(contact->family_name && *(contact->family_name) != '\0')
420       addr_compl_add_address1(contact->family_name, ae);
421   }
422 }
423
424 void cm_gdata_add_contacts(GList **address_list)
425 {
426   add_contacts_to_list(address_list, contacts_cache.contacts);
427 }
428
429 gboolean cm_gdata_update_contacts_cache(void)
430 {
431   if(prefs_common.work_offline)
432   {
433     debug_print("GData plugin: Offline mode\n");
434   }
435   else if(!cm_gdata_config.username || *(cm_gdata_config.username) == '\0' || !cm_gdata_config.password)
436   {
437     /* noop if no credentials are given */
438     debug_print("GData plugin: Empty username or password\n");
439   }
440   else
441   {
442     debug_print("GData plugin: Querying contacts");
443     query();
444   }
445   return TRUE;
446 }
447
448 void cm_gdata_contacts_done(void)
449 {
450   g_free(contacts_group_id);
451   contacts_group_id = NULL;
452
453   write_cache_to_file();
454   if(contacts_cache.contacts && !claws_is_exiting())
455     clear_contacts_cache();
456 }
457
458 void cm_gdata_load_contacts_cache_from_file(void)
459 {
460   gchar *path;
461   GNode *rootnode, *childnode, *contactnode;
462   XMLNode *xmlnode;
463
464   path = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, GDATA_CONTACTS_FILENAME, NULL);
465   if(!is_file_exist(path)) {
466     g_free(path);
467     return;
468   }
469
470   /* no merging; make sure the cache is empty (this should be a noop, but just to be safe...) */
471   clear_contacts_cache();
472
473   rootnode = xml_parse_file(path);
474   g_free(path);
475   if(!rootnode)
476     return;
477   xmlnode = rootnode->data;
478
479   /* Check that root entry is "gdata" */
480   if(strcmp2(xmlnode->tag->tag, "gdata") != 0) {
481     g_warning("wrong gdata cache file\n");
482     xml_free_tree(rootnode);
483     return;
484   }
485
486   for(childnode = rootnode->children; childnode; childnode = childnode->next) {
487     GList *attributes;
488     xmlnode = childnode->data;
489
490     if(strcmp2(xmlnode->tag->tag, "contacts") != 0)
491       continue;
492
493     for(contactnode = childnode->children; contactnode; contactnode = contactnode->next)
494     {
495       Contact *cached_contact;
496
497       xmlnode = contactnode->data;
498
499       cached_contact = g_new0(Contact, 1);
500       /* Attributes of the branch nodes */
501       for(attributes = xmlnode->tag->attr; attributes; attributes = attributes->next) {
502         XMLAttr *attr = attributes->data;
503
504         if(attr && attr->name && attr->value) {
505           if(!strcmp2(attr->name, "full_name"))
506             cached_contact->full_name = g_strdup(attr->value);
507           else if(!strcmp2(attr->name, "given_name"))
508             cached_contact->given_name = g_strdup(attr->value);
509           else if(!strcmp2(attr->name, "family_name"))
510             cached_contact->family_name = g_strdup(attr->value);
511           else if(!strcmp2(attr->name, "address"))
512             cached_contact->address = g_strdup(attr->value);
513         }
514       }
515
516       if(cached_contact->address)
517       {
518         protect_fields_against_NULL(cached_contact);
519
520         contacts_cache.contacts = g_slist_prepend(contacts_cache.contacts, cached_contact);
521         debug_print("Read contact from cache: %s\n", cached_contact->full_name);
522       }
523       else {
524         debug_print("Ignored contact without email address: %s\n", cached_contact->full_name ? cached_contact->full_name : "(null)");
525       }
526     }
527   }
528
529   /* Free XML tree */
530   xml_free_tree(rootnode);
531
532   contacts_cache.contacts = g_slist_reverse(contacts_cache.contacts);
533 }