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
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.
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.
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/>.
22 #include "claws-features.h"
26 #include <glib/gi18n.h>
28 #include <curl/curl.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"
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 */
51 static guint update_hook_id;
52 static guint render_hook_id;
53 static gchar *cache_dir = NULL; /* dir-separator terminated */
55 static gboolean libravatar_header_update_hook(gpointer source, gpointer data)
57 AvatarCaptureData *acd = (AvatarCaptureData *)source;
59 debug_print("libravatar avatar_header_update invoked\n");
61 if (!strcmp(acd->header, "From:")) {
64 a = g_strdup(acd->content);
68 for (lower = a; *lower; lower++)
69 *lower = g_ascii_tolower(*lower);
71 debug_print("libravatar added '%s'\n", a);
72 procmsg_msginfo_add_avatar(acd->msginfo, AVATAR_LIBRAVATAR, a);
76 return FALSE; /* keep getting */
79 static gchar *federated_base_url_from_address(const gchar *address)
81 #if (defined USE_GNUTLS && GLIB_CHECK_VERSION(2,22,0))
82 gchar *base_url = NULL;
84 if (!libravatarprefs.allow_federated) {
85 debug_print("federated domains disabled by configuration\n");
89 base_url = federated_url_for_address(address);
90 if (base_url != NULL) {
96 return g_strdup(libravatarprefs.base_url);
99 static GtkWidget *image_widget_from_filename(const gchar *filename)
101 GtkWidget *image = NULL;
102 GdkPixbuf *picture = NULL;
103 GError *error = NULL;
106 gdk_pixbuf_get_file_info(filename, &w, &h);
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);
116 g_warning("Failed to load image '%s': %s\n", filename, error->message);
120 image = gtk_image_new_from_pixbuf(picture);
121 g_object_unref(picture);
123 g_warning("Failed to load image '%s': no error returned!\n", filename);
129 static gchar *cache_name_for_md5(const gchar *md5)
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);
137 /* default cache dir */
138 return g_strconcat(cache_dir, md5, NULL);
141 static size_t write_image_data_cb(void *ptr, size_t size, size_t nmemb, void *stream)
143 size_t written = fwrite(ptr, size, nmemb, (FILE *)stream);
144 debug_print("received %zu bytes from avatar server\n", written);
149 static GtkWidget *image_widget_from_url(const gchar *url, const gchar *md5)
151 GtkWidget *image = NULL;
156 curl = curl_easy_init();
158 g_warning("could not initialize curl to get image from url\n");
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);
172 filename = cache_name_for_md5(md5);
173 file = fopen(filename, "wb");
177 if (libravatarprefs.allow_redirects) {
178 long maxredirs = (libravatarprefs.default_mode == DEF_MODE_URL)? 3L
179 : ((libravatarprefs.default_mode == DEF_MODE_MM)? 2L: 1L);
181 curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
182 curl_easy_setopt(curl, CURLOPT_MAXREDIRS, maxredirs);
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);
190 if (filesize < MIN_PNG_SIZE)
191 debug_print("not enough data for an avatar image: %ld bytes\n", filesize);
193 image = image_widget_from_filename(filename);
195 if (!libravatarprefs.cache_icons || filesize == 0) {
196 if (g_unlink(filename) < 0)
197 g_warning("failed to delete cache file %s\n", filename);
201 missing_add_md5(libravatarmisses, md5);
203 g_warning("could not open '%s' for writing\n", filename);
205 curl_easy_cleanup(curl);
211 static gboolean is_recent_enough(const gchar *filename)
216 if (libravatarprefs.cache_icons) {
218 if (t != (time_t)-1 && !g_stat(filename, &s)) {
219 if (t - s.st_ctime <= libravatarprefs.cache_interval * 3600)
224 return FALSE; /* re-download */
227 static GtkWidget *image_widget_from_cached_md5(const gchar *md5)
229 GtkWidget *image = NULL;
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);
242 static gchar *libravatar_url_for_md5(const gchar *base, const gchar *md5)
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);
257 g_warning("invalid libravatar default mode: %d\n", libravatarprefs.default_mode);
261 static gboolean libravatar_image_render_hook(gpointer source, gpointer data)
263 AvatarRender *ar = (AvatarRender *)source;
264 GtkWidget *image = NULL;
265 gchar *a = NULL, *url = NULL;
268 debug_print("libravatar avatar_image_render invoked\n");
270 a = procmsg_msginfo_get_avatar(ar->full_msginfo, AVATAR_LIBRAVATAR);
274 md5_hex_digest(md5sum, a);
275 /* try missing cache */
276 if (is_missing_md5(libravatarmisses, md5sum)) {
280 image = image_widget_from_cached_md5(md5sum);
282 if (ar->image) /* previous plugin set one */
283 gtk_widget_destroy(ar->image);
285 ar->type = AVATAR_LIBRAVATAR;
288 /* not cached copy: try network */
289 if (prefs_common.work_offline) {
290 debug_print("working off-line: libravatar network retrieval skipped\n");
293 base = federated_base_url_from_address(a);
294 url = libravatar_url_for_md5(base, md5sum);
296 image = image_widget_from_url(url, md5sum);
299 if (ar->image) /* previous plugin set one */
300 gtk_widget_destroy(ar->image);
302 ar->type = AVATAR_LIBRAVATAR;
310 return FALSE; /* keep rendering */
313 static gint cache_dir_init()
318 cache_dir = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
319 LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
321 if (!is_dir_exist(cache_dir)) {
322 if (make_dir(cache_dir) < 0) {
327 for (i = DEF_MODE_MM; i <= DEF_MODE_RETRO; ++i) {
328 subdir = g_strconcat(cache_dir, def_mode[i - 10], NULL);
329 if (!is_dir_exist(subdir)) {
330 if (make_dir(subdir) < 0) {
331 g_warning("cannot create directory %s\n", subdir);
342 static gint missing_cache_init()
344 gchar *cache_file = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
345 LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
346 LIBRAVATAR_MISSING_FILE, NULL);
348 libravatarmisses = missing_load_from_file(cache_file);
351 if (libravatarmisses == NULL)
357 static void missing_cache_done()
361 if (libravatarmisses != NULL) {
362 cache_file = g_strconcat(get_rc_dir(), G_DIR_SEPARATOR_S,
363 LIBRAVATAR_CACHE_DIR, G_DIR_SEPARATOR_S,
364 LIBRAVATAR_MISSING_FILE, NULL);
365 missing_save_to_file(libravatarmisses, cache_file);
367 g_hash_table_destroy(libravatarmisses);
374 * @param error For storing the returned error message.
376 * @return 0 if initialization succeeds, -1 on failure.
378 gint plugin_init(gchar **error)
380 if (!check_plugin_version(MAKE_NUMERIC_VERSION(3,9,3,29),
381 VERSION_NUMERIC, _("Libravatar"), error))
383 /* get info from headers */
384 update_hook_id = hooks_register_hook(AVATAR_HEADER_UPDATE_HOOKLIST,
385 libravatar_header_update_hook,
387 if (update_hook_id == -1) {
388 *error = g_strdup(_("Failed to register avatar header update hook"));
391 /* get image for displaying */
392 render_hook_id = hooks_register_hook(AVATAR_IMAGE_RENDER_HOOKLIST,
393 libravatar_image_render_hook,
395 if (render_hook_id == -1) {
396 *error = g_strdup(_("Failed to register avatar image render hook"));
400 if (cache_dir_init() == -1) {
401 *error = g_strdup(_("Failed to create avatar image cache directory"));
404 /* preferences page */
405 libravatar_prefs_init();
407 curl_global_init(CURL_GLOBAL_DEFAULT);
409 if (missing_cache_init() == -1) {
410 *error = g_strdup(_("Failed to load missing items cache"));
413 debug_print("Libravatar plugin loaded\n");
419 * Destructor for the plugin.
420 * Unregister the callback function and frees matcher.
422 * @return Always TRUE.
424 gboolean plugin_done(void)
426 if (render_hook_id != -1) {
427 hooks_unregister_hook(AVATAR_IMAGE_RENDER_HOOKLIST,
431 if (update_hook_id != -1) {
432 hooks_unregister_hook(AVATAR_HEADER_UPDATE_HOOKLIST,
436 libravatar_prefs_done();
437 missing_cache_done();
438 if (cache_dir != NULL)
440 debug_print("Libravatar plugin unloaded\n");
446 * Get the name of the plugin.
448 * @return The plugin's name, maybe translated.
450 const gchar *plugin_name(void)
452 return _("Libravatar");
456 * Get the description of the plugin.
458 * @return The plugin's description, maybe translated.
460 const gchar *plugin_desc(void)
462 return _("Display libravatar profiles' images for mail messages. More\n"
463 "info about libravatar at http://www.libravatar.org/. If you have\n"
464 "a gravatar.com profile but not a libravatar one, those will also\n"
465 "be retrieved (when redirections are allowed in plugin config).\n"
466 "Plugin config page is available from main window at:\n"
467 "/Configuration/Preferences/Plugins/Libravatar.\n\n"
468 "This plugin uses libcurl to retrieve images, so if you're behind a\n"
469 "proxy please refer to curl(1) manpage for details on 'http_proxy'\n"
470 "configuration. More details about this and others on README file.\n\n"
471 "Feedback to <ricardo@mones.org> is welcome.\n");
475 * Get the kind of plugin.
477 * @return The "GTK2" constant.
479 const gchar *plugin_type(void)
485 * Get the license acronym the plugin is released under.
487 * @return The "GPL3+" constant.
489 const gchar *plugin_licence(void)
495 * Get the version of the plugin.
497 * @return The current version string.
499 const gchar *plugin_version(void)
505 * Get the features implemented by the plugin.
507 * @return A constant PluginFeature structure with the features.
509 struct PluginFeature *plugin_provides(void)
511 static struct PluginFeature features[] =
512 { {PLUGIN_OTHER, N_("Libravatar")},
513 {PLUGIN_NOTHING, NULL}};