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