New libravatar plugin
[claws.git] / src / plugins / libravatar / libravatar.c
1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2014 Hiroyuki Yamamoto and the Claws Mail Team
4  * Copyright (C) 2014 Ricardo Mones
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #  include "config.h"
22 #include "claws-features.h"
23 #endif
24
25 #include <glib.h>
26 #include <glib/gi18n.h>
27
28 #include <curl/curl.h>
29
30 #include "version.h"
31 #include "libravatar.h"
32 #include "libravatar_prefs.h"
33 #include "libravatar_missing.h"
34 #include "prefs_common.h"
35 #include "procheader.h"
36 #include "procmsg.h"
37 #include "utils.h"
38 #include "md5.h"
39
40 /* indexes of keys are default_mode - 10 if applicable */
41 static const char *def_mode[] = {
42         "404",  /* not used, only useful in web pages */
43         "mm",
44         "identicon",
45         "monsterid",
46         "wavatar",
47         "retro"
48 };
49
50 static guint update_hook_id;
51 static guint render_hook_id;
52 static gchar *cache_dir = NULL; /* dir-separator terminated */
53 static GHashTable *misses;
54
55 static gboolean libravatar_header_update_hook(gpointer source, gpointer data)
56 {
57         AvatarCaptureData *acd = (AvatarCaptureData *)source;
58
59         debug_print("libravatar avatar_header_update invoked\n");
60
61         if (!strcmp(acd->header, "From:")) {
62                 gchar *a = g_strdup(acd->content);
63                 extract_address(a);
64                 debug_print("libravatar added '%s'\n", a);
65                 procmsg_msginfo_add_avatar(acd->msginfo, AVATAR_LIBRAVATAR, a);
66         }
67
68         return FALSE; /* keep getting */
69 }
70
71 static gchar *federated_base_url_from_address(const gchar *address)
72 {
73         /*
74            TODO: no federation supported right now
75            Details on http://wiki.libravatar.org/running_your_own/
76          */
77         return g_strdup(libravatarprefs.base_url);
78 }
79
80 static GtkWidget *image_widget_from_filename(const gchar *filename)
81 {
82         GtkWidget *image = NULL;
83         GdkPixbuf *picture = NULL;
84         GError *error = NULL;
85         gint w, h;
86
87         gdk_pixbuf_get_file_info(filename, &w, &h);
88
89         if (w != AVATAR_SIZE || h != AVATAR_SIZE)
90                 /* server can provide a different size from the requested in URL */
91                 picture = gdk_pixbuf_new_from_file_at_scale(
92                                 filename, AVATAR_SIZE, AVATAR_SIZE, TRUE, &error);
93         else    /* exact size */
94                 picture = gdk_pixbuf_new_from_file(filename, &error);
95
96         if (error != NULL) {
97                 g_warning("Failed to load image '%s': %s\n", filename, error->message);
98                 g_error_free(error);
99         } else {
100                 if (picture) {
101                         image = gtk_image_new_from_pixbuf(picture);
102                         g_object_unref(picture);
103                 } else
104                         g_warning("Failed to load image '%s': no error returned!\n", filename);
105         }
106
107         return image;
108 }
109
110 static gchar *cache_name_for_md5(const gchar *md5)
111 {
112         if (libravatarprefs.default_mode >= DEF_MODE_MM
113                         && libravatarprefs.default_mode <= DEF_MODE_RETRO) {
114                 /* cache dir for generated avatars */
115                 return g_strconcat(cache_dir, def_mode[libravatarprefs.default_mode - 10],
116                                    G_DIR_SEPARATOR_S, md5, NULL);
117         }
118         /* default cache dir */
119         return g_strconcat(cache_dir, md5, NULL);
120 }
121
122 static size_t write_image_data_cb(void *ptr, size_t size, size_t nmemb, void *stream)
123 {
124         size_t written = fwrite(ptr, size, nmemb, (FILE *)stream);
125         debug_print("received %zu bytes from avatar server\n", written);
126
127         return written;
128 }
129
130 static GtkWidget *image_widget_from_url(const gchar *url, const gchar *md5)
131 {
132         GtkWidget *image = NULL;
133         gchar *filename;
134         FILE *file;
135         CURL *curl;
136
137         curl = curl_easy_init();
138         if (curl == NULL) {
139                 g_warning("could not initialize curl to get image from url\n");
140                 return NULL;
141         }
142         curl_easy_setopt(curl, CURLOPT_URL, url);
143         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_image_data_cb);
144
145         filename = cache_name_for_md5(md5);
146         file = fopen(filename, "wb");
147         if (file != NULL) {
148                 long filesize;
149
150                 if (libravatarprefs.allow_redirects) {
151                         long maxredirs = (libravatarprefs.default_mode == DEF_MODE_MM)? 2L: 1L;
152
153                         curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
154                         curl_easy_setopt(curl, CURLOPT_MAXREDIRS, maxredirs);
155                 }
156                 curl_easy_setopt(curl, CURLOPT_FILE, file);
157                 debug_print("retrieving URL to file: %s -> %s\n", url, filename);
158                 curl_easy_perform(curl);
159                 filesize = ftell(file);
160                 fclose(file);
161
162                 if (filesize < MIN_PNG_SIZE)
163                         debug_print("not enough data for an avatar image: %ld bytes\n", filesize);
164                 else
165                         image = image_widget_from_filename(filename);
166
167                 if (!libravatarprefs.cache_icons) {
168                         if (g_unlink(filename) < 0)
169                                 g_warning("failed to delete cache file %s\n", filename);
170                 }
171
172                 if (filesize == 0)
173                         missing_add_md5(misses, md5);
174         } else {
175                 g_warning("could not open '%s' for writting\n", filename);
176         }
177         curl_easy_cleanup(curl);
178         g_free(filename);
179
180         return image;
181 }
182
183 static gboolean is_recent_enough(const gchar *filename)
184 {
185         struct stat s;
186         time_t t;
187
188         if (libravatarprefs.cache_icons) {
189                 t = time(NULL);
190                 if (t != (time_t)-1 && !g_stat(filename, &s)) {
191                         if (t - s.st_ctime <= libravatarprefs.cache_interval * 3600)
192                                 return TRUE;
193                 }
194         }
195
196         return FALSE; /* re-download */
197 }
198
199 static GtkWidget *image_widget_from_cached_md5(const gchar *md5)
200 {
201         GtkWidget *image = NULL;
202         gchar *filename;
203
204         filename = cache_name_for_md5(md5);
205         if (is_file_exist(filename) && is_recent_enough(filename)) {
206                 debug_print("found cached image for %s\n", md5);
207                 image = image_widget_from_filename(filename);
208         }
209         g_free(filename);
210
211         return image;
212 }
213
214 static gchar *libravatar_url_for_md5(const gchar *base, const gchar *md5)
215 {
216         if (libravatarprefs.default_mode >= DEF_MODE_404) {
217                 return g_strdup_printf("%s/%s?s=%u&d=%s",
218                                 base, md5, AVATAR_SIZE,
219                                 def_mode[libravatarprefs.default_mode - 10]);
220         } else if (libravatarprefs.default_mode == DEF_MODE_URL) {
221                 return g_strdup_printf("%s/%s?s=%u&d=%s",
222                                 base, md5, AVATAR_SIZE,
223                                 libravatarprefs.default_mode_url);
224         } else if (libravatarprefs.default_mode == DEF_MODE_NONE) {
225                 return g_strdup_printf("%s/%s?s=%u",
226                                 base, md5, AVATAR_SIZE);
227         }
228
229         g_warning("invalid libravatar default mode: %d\n", libravatarprefs.default_mode);
230         return NULL;
231 }
232
233 static gboolean libravatar_image_render_hook(gpointer source, gpointer data)
234 {
235         AvatarRender *ar = (AvatarRender *)source;
236         GtkWidget *image = NULL;
237         gchar *a = NULL, *url = NULL;
238         gchar md5sum[33];
239
240         debug_print("libravatar avatar_image_render invoked\n");
241
242         a = procmsg_msginfo_get_avatar(ar->full_msginfo, AVATAR_LIBRAVATAR);
243         if (a != NULL) {
244                 gchar *base;
245
246                 md5_hex_digest(md5sum, a);
247                 /* try missing cache */
248                 if (is_missing_md5(misses, md5sum)) {
249                         return FALSE;
250                 }
251                 /* try disk cache */
252                 image = image_widget_from_cached_md5(md5sum);
253                 if (image != NULL) {
254                         if (ar->image) /* previous plugin set one */
255                                 gtk_widget_destroy(ar->image);
256                         ar->image = image;
257                         return FALSE;
258                 }
259                 /* not cached copy: try network */
260                 if (prefs_common.work_offline) {
261                         debug_print("working off-line: libravatar network retrieval skipped\n");
262                         return FALSE;
263                 }
264                 base = federated_base_url_from_address(a);
265                 url = libravatar_url_for_md5(base, md5sum);
266                 if (url != NULL) {
267                         image = image_widget_from_url(url, md5sum);
268                         g_free(url);
269                         if (image != NULL) {
270                                 if (ar->image) /* previous plugin set one */
271                                         gtk_widget_destroy(ar->image);
272                                 ar->image = image;
273                         }
274                 }
275                 g_free(base);
276
277                 return TRUE;
278         }
279
280         return FALSE; /* keep rendering */
281 }
282
283 static gint cache_dir_init()
284 {
285         gchar *subdir;
286         int i;
287
288         cache_dir = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
289                                 LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
290                                 NULL);
291         if (!is_dir_exist(cache_dir)) {
292                 if (make_dir(cache_dir) < 0) {
293                         g_free(cache_dir);
294                         return -1;
295                 }
296         }
297         for (i = DEF_MODE_MM; i <= DEF_MODE_RETRO; ++i) {
298                 subdir = g_strconcat(cache_dir, def_mode[i - 10], NULL);
299                 if (!is_dir_exist(subdir)) {
300                         if (make_dir(subdir) < 0) {
301                                 g_warning("cannot create directory %s\n", subdir);
302                                 g_free(subdir);
303                                 return -1;
304                         }
305                 }
306                 g_free(subdir);
307         }
308
309         return 0;
310 }
311
312 static gint missing_cache_init()
313 {
314         gchar *cache_file = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
315                                         LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
316                                         LIBRAVATAR_MISSING_FILE, NULL);
317
318         misses = missing_load_from_file(cache_file);
319         g_free(cache_file);
320
321         if (misses == NULL)
322                 return -1;
323
324         return 0;
325 }
326
327 static void missing_cache_done()
328 {
329         gchar *cache_file;
330
331         if (misses != NULL) {
332                 cache_file = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
333                                         LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
334                                         LIBRAVATAR_MISSING_FILE, NULL);
335                 missing_save_to_file(misses, cache_file);
336                 g_free(cache_file);
337                 g_hash_table_destroy(misses);
338         }
339 }
340
341 /**
342  * Initialize plugin.
343  *
344  * @param error  For storing the returned error message.
345  *
346  * @return 0 if initialization succeeds, -1 on failure.
347  */
348 gint plugin_init(gchar **error)
349 {
350         if (!check_plugin_version(MAKE_NUMERIC_VERSION(3,9,3,29),
351                                   VERSION_NUMERIC, _("Libravatar"), error))
352                 return -1;
353         /* get info from headers */
354         update_hook_id = hooks_register_hook(AVATAR_HEADER_UPDATE_HOOKLIST,
355                                              libravatar_header_update_hook,
356                                              NULL);
357         if (update_hook_id == -1) {
358                 *error = g_strdup(_("Failed to register avatar header update hook"));
359                 return -1;
360         }
361         /* get image for displaying */
362         render_hook_id = hooks_register_hook(AVATAR_IMAGE_RENDER_HOOKLIST,
363                                              libravatar_image_render_hook,
364                                              NULL);
365         if (render_hook_id == -1) {
366                 *error = g_strdup(_("Failed to register avatar image render hook"));
367                 return -1;
368         }
369         /* cache dir */
370         if (cache_dir_init() == -1) {
371                 *error = g_strdup(_("Failed to create avatar image cache directory"));
372                 return -1;
373         }
374         /* preferences page */
375         libravatar_prefs_init();
376         /* curl library */
377         curl_global_init(CURL_GLOBAL_DEFAULT);
378         /* missing cache */
379         if (missing_cache_init() == -1) {
380                 *error = g_strdup(_("Failed to load missing items cache"));
381                 return -1;
382         }
383         debug_print("Libravatar plugin loaded\n");
384
385         return 0;
386 }
387
388 /**
389  * Destructor for the plugin.
390  * Unregister the callback function and frees matcher.
391  *
392  * @return Always TRUE.
393  */
394 gboolean plugin_done(void)
395 {
396         if (render_hook_id != -1) {
397                 hooks_unregister_hook(AVATAR_IMAGE_RENDER_HOOKLIST,
398                                       render_hook_id);
399                 render_hook_id = -1;
400         }
401         if (update_hook_id != -1) {
402                 hooks_unregister_hook(AVATAR_HEADER_UPDATE_HOOKLIST,
403                                       update_hook_id);
404                 update_hook_id = -1;
405         }
406         libravatar_prefs_done();
407         missing_cache_done();
408         if (cache_dir != NULL)
409                 g_free(cache_dir);
410         debug_print("Libravatar plugin unloaded\n");
411
412         return TRUE;
413 }
414
415 /**
416  * Get the name of the plugin.
417  *
418  * @return The plugin's name, maybe translated.
419  */
420 const gchar *plugin_name(void)
421 {
422         return _("Libravatar");
423 }
424
425 /**
426  * Get the description of the plugin.
427  *
428  * @return The plugin's description, maybe translated.
429  */
430 const gchar *plugin_desc(void)
431 {
432         return _("Get and display libravatar images for mail messages.\n\n"
433                  "Info about libravatar at http://www.libravatar.org/\n\n"
434                  "Feedback to <ricardo@mones.org> is welcome.\n");
435 }
436
437 /**
438  * Get the kind of plugin.
439  *
440  * @return The "GTK2" constant.
441  */
442 const gchar *plugin_type(void)
443 {
444         return "GTK2";
445 }
446
447 /**
448  * Get the license acronym the plugin is released under.
449  *
450  * @return The "GPL3+" constant.
451  */
452 const gchar *plugin_licence(void)
453 {
454         return "GPL3+";
455 }
456
457 /**
458  * Get the version of the plugin.
459  *
460  * @return The current version string.
461  */
462 const gchar *plugin_version(void)
463 {
464         return VERSION;
465 }
466
467 /**
468  * Get the features implemented by the plugin.
469  *
470  * @return A constant PluginFeature structure with the features.
471  */
472 struct PluginFeature *plugin_provides(void)
473 {
474         static struct PluginFeature features[] =
475                 { {PLUGIN_OTHER, N_("Libravatar")},
476                   {PLUGIN_NOTHING, NULL}};
477
478         return features;
479 }
480