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