8a57198c456224094271605c758002e1915cc384
[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   NULL, (GAsyncReadyCallback)cm_gdata_query_contacts_ready, NULL);
239
240   g_object_unref(query);
241 }
242
243 static void cm_gdata_query_groups_ready(GDataContactsService *service, GAsyncResult *res, gpointer data)
244 {
245   GDataFeed *feed;
246   GList *walk;
247   GError *error = NULL;
248
249   feed = gdata_service_query_finish(GDATA_SERVICE(service), res, &error);
250   if(error)
251   {
252     g_object_unref(feed);
253     log_error(LOG_PROTOCOL, _("GData plugin: Error querying for groups: %s\n"), error->message);
254     g_error_free(error);
255     return;
256   }
257
258   /* Iterate through the returned groups and search for Contacts group id */
259   for(walk = gdata_feed_get_entries(feed); walk; walk = walk->next) {
260     const gchar *system_group_id;
261     GDataContactsGroup *group = GDATA_CONTACTS_GROUP(walk->data);
262
263     system_group_id = gdata_contacts_group_get_system_group_id(group);
264     if(system_group_id && !strcmp(system_group_id, GDATA_CONTACTS_GROUP_CONTACTS)) {
265       gchar *pos;
266       const gchar *id;
267
268       id = gdata_entry_get_id(GDATA_ENTRY(group));
269
270       /* possibly replace projection "full" by "base" */
271       pos = g_strrstr(id, "/full/");
272       if(pos) {
273         GString *str = g_string_new("\0");
274         int off = pos-id;
275
276         g_string_append_len(str, id, off);
277         g_string_append(str, "/base/");
278         g_string_append(str, id+off+strlen("/full/"));
279         g_string_append_c(str, '\0');
280         contacts_group_id = str->str;
281         g_string_free(str, FALSE);
282       }
283       else
284         contacts_group_id = g_strdup(id);
285       break;
286     }
287   }
288   g_object_unref(feed);
289
290   log_message(LOG_PROTOCOL, _("GData plugin: Groups received\n"));
291
292   query_after_auth(service);
293 }
294
295 static void query_for_contacts_group_id(GDataClientLoginAuthorizer *authorizer)
296 {
297   GDataContactsService *service;
298
299   log_message(LOG_PROTOCOL, _("GData plugin: Starting async groups query\n"));
300
301   service = gdata_contacts_service_new(GDATA_AUTHORIZER(authorizer));
302   gdata_contacts_service_query_groups_async(service, NULL, NULL, NULL, NULL, NULL,
303       (GAsyncReadyCallback)cm_gdata_query_groups_ready, NULL);
304
305   g_object_unref(service);
306 }
307
308 static void cm_gdata_auth_ready(GDataClientLoginAuthorizer *authorizer, GAsyncResult *res, gpointer data)
309 {
310   GError *error = NULL;
311
312   if(gdata_client_login_authorizer_authenticate_finish(authorizer, res, &error) == FALSE)
313   {
314     log_error(LOG_PROTOCOL, _("GData plugin: Authentication error: %s\n"), error->message);
315     g_error_free(error);
316     cm_gdata_contacts_query_running = FALSE;
317     return;
318   }
319
320   log_message(LOG_PROTOCOL, _("GData plugin: Authenticated\n"));
321
322   if(!contacts_group_id)
323   {
324     query_for_contacts_group_id(authorizer);
325   }
326   else {
327     GDataContactsService *service;
328     service = gdata_contacts_service_new(GDATA_AUTHORIZER(authorizer));
329     query_after_auth(service);
330     g_object_unref(service);
331   }
332 }
333
334 static void query()
335 {
336
337   GDataClientLoginAuthorizer *authorizer;
338
339   if(cm_gdata_contacts_query_running)
340   {
341     debug_print("GData plugin: Network query already in progress");
342     return;
343   }
344
345   log_message(LOG_PROTOCOL, _("GData plugin: Starting async authentication\n"));
346
347   authorizer = gdata_client_login_authorizer_new(CM_GDATA_CLIENT_ID, GDATA_TYPE_CONTACTS_SERVICE);
348   gdata_client_login_authorizer_authenticate_async(authorizer, cm_gdata_config.username, cm_gdata_config.password, NULL, (GAsyncReadyCallback)cm_gdata_auth_ready, NULL);
349   cm_gdata_contacts_query_running = TRUE;
350
351   g_object_unref(authorizer);
352 }
353
354
355 static void add_contacts_to_list(GList **address_list, GSList *contacts)
356 {
357   GSList *walk;
358
359   for(walk = contacts; walk; walk = walk->next)
360   {
361     address_entry *ae;
362     Contact *contact = walk->data;
363
364     ae = g_new0(address_entry, 1);
365     ae->name = g_strdup(contact->full_name);
366     ae->address = g_strdup(contact->address);
367     ae->grp_emails = NULL;
368
369     *address_list = g_list_prepend(*address_list, ae);
370     addr_compl_add_address1(ae->address, ae);
371
372     if(contact->given_name && *(contact->given_name) != '\0')
373       addr_compl_add_address1(contact->given_name, ae);
374
375     if(contact->family_name && *(contact->family_name) != '\0')
376       addr_compl_add_address1(contact->family_name, ae);
377   }
378 }
379
380 void cm_gdata_add_contacts(GList **address_list)
381 {
382   add_contacts_to_list(address_list, contacts_cache.contacts);
383 }
384
385 gboolean cm_gdata_update_contacts_cache(void)
386 {
387   if(prefs_common.work_offline)
388   {
389     debug_print("GData plugin: Offline mode\n");
390   }
391   else if(!cm_gdata_config.username || *(cm_gdata_config.username) == '\0' || !cm_gdata_config.password)
392   {
393     /* noop if no credentials are given */
394     debug_print("GData plugin: Empty username or password\n");
395   }
396   else
397   {
398     debug_print("GData plugin: Querying contacts");
399     query();
400   }
401   return TRUE;
402 }
403
404 void cm_gdata_contacts_done(void)
405 {
406   g_free(contacts_group_id);
407   contacts_group_id = NULL;
408
409   write_cache_to_file();
410   if(contacts_cache.contacts && !claws_is_exiting())
411     clear_contacts_cache();
412 }
413
414 void cm_gdata_load_contacts_cache_from_file(void)
415 {
416   gchar *path;
417   GNode *rootnode, *childnode, *contactnode;
418   XMLNode *xmlnode;
419
420   path = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S, GDATA_CONTACTS_FILENAME, NULL);
421   if(!is_file_exist(path)) {
422     g_free(path);
423     return;
424   }
425
426   /* no merging; make sure the cache is empty (this should be a noop, but just to be safe...) */
427   clear_contacts_cache();
428
429   rootnode = xml_parse_file(path);
430   g_free(path);
431   if(!rootnode)
432     return;
433   xmlnode = rootnode->data;
434
435   /* Check that root entry is "gdata" */
436   if(strcmp2(xmlnode->tag->tag, "gdata") != 0) {
437     g_warning("wrong gdata cache file\n");
438     xml_free_tree(rootnode);
439     return;
440   }
441
442   for(childnode = rootnode->children; childnode; childnode = childnode->next) {
443     GList *attributes;
444     xmlnode = childnode->data;
445
446     if(strcmp2(xmlnode->tag->tag, "contacts") != 0)
447       continue;
448
449     for(contactnode = childnode->children; contactnode; contactnode = contactnode->next)
450     {
451       Contact *cached_contact;
452
453       xmlnode = contactnode->data;
454
455       cached_contact = g_new0(Contact, 1);
456       /* Attributes of the branch nodes */
457       for(attributes = xmlnode->tag->attr; attributes; attributes = attributes->next) {
458         XMLAttr *attr = attributes->data;
459
460         if(attr && attr->name && attr->value) {
461           if(!strcmp2(attr->name, "full_name"))
462             cached_contact->full_name = g_strdup(attr->value);
463           else if(!strcmp2(attr->name, "given_name"))
464             cached_contact->given_name = g_strdup(attr->value);
465           else if(!strcmp2(attr->name, "family_name"))
466             cached_contact->family_name = g_strdup(attr->value);
467           else if(!strcmp2(attr->name, "address"))
468             cached_contact->address = g_strdup(attr->value);
469         }
470       }
471
472       if(cached_contact->address)
473       {
474         protect_fields_against_NULL(cached_contact);
475
476         contacts_cache.contacts = g_slist_prepend(contacts_cache.contacts, cached_contact);
477         debug_print("Read contact from cache: %s\n", cached_contact->full_name);
478       }
479       else {
480         debug_print("Ignored contact without email address: %s\n", cached_contact->full_name ? cached_contact->full_name : "(null)");
481       }
482     }
483   }
484
485   /* Free XML tree */
486   xml_free_tree(rootnode);
487
488   contacts_cache.contacts = g_slist_reverse(contacts_cache.contacts);
489 }