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