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