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