Fix bug #3126 “libravatar: do not collect empty files”
[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
54 static gboolean libravatar_header_update_hook(gpointer source, gpointer data)
55 {
56         AvatarCaptureData *acd = (AvatarCaptureData *)source;
57
58         debug_print("libravatar avatar_header_update invoked\n");
59
60         if (!strcmp(acd->header, "From:")) {
61                 gchar *a, *lower;
62
63                 a = g_strdup(acd->content);
64                 extract_address(a);
65
66                 /* string to lower */
67                 for (lower = a; *lower; lower++)
68                         *lower = g_ascii_tolower(*lower);
69
70                 debug_print("libravatar added '%s'\n", a);
71                 procmsg_msginfo_add_avatar(acd->msginfo, AVATAR_LIBRAVATAR, a);
72         }
73
74         return FALSE; /* keep getting */
75 }
76
77 static gchar *federated_base_url_from_address(const gchar *address)
78 {
79         /*
80            TODO: no federation supported right now
81            Details on http://wiki.libravatar.org/running_your_own/
82          */
83         return g_strdup(libravatarprefs.base_url);
84 }
85
86 static GtkWidget *image_widget_from_filename(const gchar *filename)
87 {
88         GtkWidget *image = NULL;
89         GdkPixbuf *picture = NULL;
90         GError *error = NULL;
91         gint w, h;
92
93         gdk_pixbuf_get_file_info(filename, &w, &h);
94
95         if (w != AVATAR_SIZE || h != AVATAR_SIZE)
96                 /* server can provide a different size from the requested in URL */
97                 picture = gdk_pixbuf_new_from_file_at_scale(
98                                 filename, AVATAR_SIZE, AVATAR_SIZE, TRUE, &error);
99         else    /* exact size */
100                 picture = gdk_pixbuf_new_from_file(filename, &error);
101
102         if (error != NULL) {
103                 g_warning("Failed to load image '%s': %s\n", filename, error->message);
104                 g_error_free(error);
105         } else {
106                 if (picture) {
107                         image = gtk_image_new_from_pixbuf(picture);
108                         g_object_unref(picture);
109                 } else
110                         g_warning("Failed to load image '%s': no error returned!\n", filename);
111         }
112
113         return image;
114 }
115
116 static gchar *cache_name_for_md5(const gchar *md5)
117 {
118         if (libravatarprefs.default_mode >= DEF_MODE_MM
119                         && libravatarprefs.default_mode <= DEF_MODE_RETRO) {
120                 /* cache dir for generated avatars */
121                 return g_strconcat(cache_dir, def_mode[libravatarprefs.default_mode - 10],
122                                    G_DIR_SEPARATOR_S, md5, NULL);
123         }
124         /* default cache dir */
125         return g_strconcat(cache_dir, md5, NULL);
126 }
127
128 static size_t write_image_data_cb(void *ptr, size_t size, size_t nmemb, void *stream)
129 {
130         size_t written = fwrite(ptr, size, nmemb, (FILE *)stream);
131         debug_print("received %zu bytes from avatar server\n", written);
132
133         return written;
134 }
135
136 static GtkWidget *image_widget_from_url(const gchar *url, const gchar *md5)
137 {
138         GtkWidget *image = NULL;
139         gchar *filename;
140         FILE *file;
141         CURL *curl;
142
143         curl = curl_easy_init();
144         if (curl == NULL) {
145                 g_warning("could not initialize curl to get image from url\n");
146                 return NULL;
147         }
148         curl_easy_setopt(curl, CURLOPT_URL, url);
149         curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_image_data_cb);
150
151         filename = cache_name_for_md5(md5);
152         file = fopen(filename, "wb");
153         if (file != NULL) {
154                 long filesize;
155
156                 if (libravatarprefs.allow_redirects) {
157                         long maxredirs = (libravatarprefs.default_mode == DEF_MODE_URL)? 3L
158                                 : ((libravatarprefs.default_mode == DEF_MODE_MM)? 2L: 1L);
159
160                         curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
161                         curl_easy_setopt(curl, CURLOPT_MAXREDIRS, maxredirs);
162                 }
163                 curl_easy_setopt(curl, CURLOPT_FILE, file);
164                 debug_print("retrieving URL to file: %s -> %s\n", url, filename);
165                 curl_easy_perform(curl);
166                 filesize = ftell(file);
167                 fclose(file);
168
169                 if (filesize < MIN_PNG_SIZE)
170                         debug_print("not enough data for an avatar image: %ld bytes\n", filesize);
171                 else
172                         image = image_widget_from_filename(filename);
173
174                 if (!libravatarprefs.cache_icons || filesize == 0) {
175                         if (g_unlink(filename) < 0)
176                                 g_warning("failed to delete cache file %s\n", filename);
177                 }
178
179                 if (filesize == 0)
180                         missing_add_md5(libravatarmisses, md5);
181         } else {
182                 g_warning("could not open '%s' for writting\n", filename);
183         }
184         curl_easy_cleanup(curl);
185         g_free(filename);
186
187         return image;
188 }
189
190 static gboolean is_recent_enough(const gchar *filename)
191 {
192         struct stat s;
193         time_t t;
194
195         if (libravatarprefs.cache_icons) {
196                 t = time(NULL);
197                 if (t != (time_t)-1 && !g_stat(filename, &s)) {
198                         if (t - s.st_ctime <= libravatarprefs.cache_interval * 3600)
199                                 return TRUE;
200                 }
201         }
202
203         return FALSE; /* re-download */
204 }
205
206 static GtkWidget *image_widget_from_cached_md5(const gchar *md5)
207 {
208         GtkWidget *image = NULL;
209         gchar *filename;
210
211         filename = cache_name_for_md5(md5);
212         if (is_file_exist(filename) && is_recent_enough(filename)) {
213                 debug_print("found cached image for %s\n", md5);
214                 image = image_widget_from_filename(filename);
215         }
216         g_free(filename);
217
218         return image;
219 }
220
221 static gchar *libravatar_url_for_md5(const gchar *base, const gchar *md5)
222 {
223         if (libravatarprefs.default_mode >= DEF_MODE_404) {
224                 return g_strdup_printf("%s/%s?s=%u&d=%s",
225                                 base, md5, AVATAR_SIZE,
226                                 def_mode[libravatarprefs.default_mode - 10]);
227         } else if (libravatarprefs.default_mode == DEF_MODE_URL) {
228                 return g_strdup_printf("%s/%s?s=%u&d=%s",
229                                 base, md5, AVATAR_SIZE,
230                                 libravatarprefs.default_mode_url);
231         } else if (libravatarprefs.default_mode == DEF_MODE_NONE) {
232                 return g_strdup_printf("%s/%s?s=%u",
233                                 base, md5, AVATAR_SIZE);
234         }
235
236         g_warning("invalid libravatar default mode: %d\n", libravatarprefs.default_mode);
237         return NULL;
238 }
239
240 static gboolean libravatar_image_render_hook(gpointer source, gpointer data)
241 {
242         AvatarRender *ar = (AvatarRender *)source;
243         GtkWidget *image = NULL;
244         gchar *a = NULL, *url = NULL;
245         gchar md5sum[33];
246
247         debug_print("libravatar avatar_image_render invoked\n");
248
249         a = procmsg_msginfo_get_avatar(ar->full_msginfo, AVATAR_LIBRAVATAR);
250         if (a != NULL) {
251                 gchar *base;
252
253                 md5_hex_digest(md5sum, a);
254                 /* try missing cache */
255                 if (is_missing_md5(libravatarmisses, md5sum)) {
256                         return FALSE;
257                 }
258                 /* try disk cache */
259                 image = image_widget_from_cached_md5(md5sum);
260                 if (image != NULL) {
261                         if (ar->image) /* previous plugin set one */
262                                 gtk_widget_destroy(ar->image);
263                         ar->image = image;
264                         return FALSE;
265                 }
266                 /* not cached copy: try network */
267                 if (prefs_common.work_offline) {
268                         debug_print("working off-line: libravatar network retrieval skipped\n");
269                         return FALSE;
270                 }
271                 base = federated_base_url_from_address(a);
272                 url = libravatar_url_for_md5(base, md5sum);
273                 if (url != NULL) {
274                         image = image_widget_from_url(url, md5sum);
275                         g_free(url);
276                         if (image != NULL) {
277                                 if (ar->image) /* previous plugin set one */
278                                         gtk_widget_destroy(ar->image);
279                                 ar->image = image;
280                         }
281                 }
282                 g_free(base);
283
284                 return TRUE;
285         }
286
287         return FALSE; /* keep rendering */
288 }
289
290 static gint cache_dir_init()
291 {
292         gchar *subdir;
293         int i;
294
295         cache_dir = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
296                                 LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
297                                 NULL);
298         if (!is_dir_exist(cache_dir)) {
299                 if (make_dir(cache_dir) < 0) {
300                         g_free(cache_dir);
301                         return -1;
302                 }
303         }
304         for (i = DEF_MODE_MM; i <= DEF_MODE_RETRO; ++i) {
305                 subdir = g_strconcat(cache_dir, def_mode[i - 10], NULL);
306                 if (!is_dir_exist(subdir)) {
307                         if (make_dir(subdir) < 0) {
308                                 g_warning("cannot create directory %s\n", subdir);
309                                 g_free(subdir);
310                                 return -1;
311                         }
312                 }
313                 g_free(subdir);
314         }
315
316         return 0;
317 }
318
319 static gint missing_cache_init()
320 {
321         gchar *cache_file = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
322                                         LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
323                                         LIBRAVATAR_MISSING_FILE, NULL);
324
325         libravatarmisses = missing_load_from_file(cache_file);
326         g_free(cache_file);
327
328         if (libravatarmisses == NULL)
329                 return -1;
330
331         return 0;
332 }
333
334 static void missing_cache_done()
335 {
336         gchar *cache_file;
337
338         if (libravatarmisses != NULL) {
339                 cache_file = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
340                                         LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
341                                         LIBRAVATAR_MISSING_FILE, NULL);
342                 missing_save_to_file(libravatarmisses, cache_file);
343                 g_free(cache_file);
344                 g_hash_table_destroy(libravatarmisses);
345         }
346 }
347
348 /**
349  * Initialize plugin.
350  *
351  * @param error  For storing the returned error message.
352  *
353  * @return 0 if initialization succeeds, -1 on failure.
354  */
355 gint plugin_init(gchar **error)
356 {
357         if (!check_plugin_version(MAKE_NUMERIC_VERSION(3,9,3,29),
358                                   VERSION_NUMERIC, _("Libravatar"), error))
359                 return -1;
360         /* get info from headers */
361         update_hook_id = hooks_register_hook(AVATAR_HEADER_UPDATE_HOOKLIST,
362                                              libravatar_header_update_hook,
363                                              NULL);
364         if (update_hook_id == -1) {
365                 *error = g_strdup(_("Failed to register avatar header update hook"));
366                 return -1;
367         }
368         /* get image for displaying */
369         render_hook_id = hooks_register_hook(AVATAR_IMAGE_RENDER_HOOKLIST,
370                                              libravatar_image_render_hook,
371                                              NULL);
372         if (render_hook_id == -1) {
373                 *error = g_strdup(_("Failed to register avatar image render hook"));
374                 return -1;
375         }
376         /* cache dir */
377         if (cache_dir_init() == -1) {
378                 *error = g_strdup(_("Failed to create avatar image cache directory"));
379                 return -1;
380         }
381         /* preferences page */
382         libravatar_prefs_init();
383         /* curl library */
384         curl_global_init(CURL_GLOBAL_DEFAULT);
385         /* missing cache */
386         if (missing_cache_init() == -1) {
387                 *error = g_strdup(_("Failed to load missing items cache"));
388                 return -1;
389         }
390         debug_print("Libravatar plugin loaded\n");
391
392         return 0;
393 }
394
395 /**
396  * Destructor for the plugin.
397  * Unregister the callback function and frees matcher.
398  *
399  * @return Always TRUE.
400  */
401 gboolean plugin_done(void)
402 {
403         if (render_hook_id != -1) {
404                 hooks_unregister_hook(AVATAR_IMAGE_RENDER_HOOKLIST,
405                                       render_hook_id);
406                 render_hook_id = -1;
407         }
408         if (update_hook_id != -1) {
409                 hooks_unregister_hook(AVATAR_HEADER_UPDATE_HOOKLIST,
410                                       update_hook_id);
411                 update_hook_id = -1;
412         }
413         libravatar_prefs_done();
414         missing_cache_done();
415         if (cache_dir != NULL)
416                 g_free(cache_dir);
417         debug_print("Libravatar plugin unloaded\n");
418
419         return TRUE;
420 }
421
422 /**
423  * Get the name of the plugin.
424  *
425  * @return The plugin's name, maybe translated.
426  */
427 const gchar *plugin_name(void)
428 {
429         return _("Libravatar");
430 }
431
432 /**
433  * Get the description of the plugin.
434  *
435  * @return The plugin's description, maybe translated.
436  */
437 const gchar *plugin_desc(void)
438 {
439         return _("Display libravatar profiles' images for mail messages. More\n"
440                  "info about libravatar at http://www.libravatar.org/. If you have\n"
441                  "a gravatar.com profile but not a libravatar one, those will also\n"
442                  "be retrieved (when redirections are allowed in plugin config).\n"
443                  "Plugin config page it's available from main window at:\n"
444                  "/Configuration/Preferences/Plugins/Libravatar.\n\n"
445                  "This plugin uses libcurl to retrieve images, so if you're behind a\n"
446                  "proxy please refer to curl(1) manpage for details on 'http_proxy'\n"
447                  "configuration. More details about this and others on README file.\n\n"
448                  "Feedback to <ricardo@mones.org> is welcome.\n");
449 }
450
451 /**
452  * Get the kind of plugin.
453  *
454  * @return The "GTK2" constant.
455  */
456 const gchar *plugin_type(void)
457 {
458         return "GTK2";
459 }
460
461 /**
462  * Get the license acronym the plugin is released under.
463  *
464  * @return The "GPL3+" constant.
465  */
466 const gchar *plugin_licence(void)
467 {
468         return "GPL3+";
469 }
470
471 /**
472  * Get the version of the plugin.
473  *
474  * @return The current version string.
475  */
476 const gchar *plugin_version(void)
477 {
478         return VERSION;
479 }
480
481 /**
482  * Get the features implemented by the plugin.
483  *
484  * @return A constant PluginFeature structure with the features.
485  */
486 struct PluginFeature *plugin_provides(void)
487 {
488         static struct PluginFeature features[] =
489                 { {PLUGIN_OTHER, N_("Libravatar")},
490                   {PLUGIN_NOTHING, NULL}};
491
492         return features;
493 }
494