1 /*
2  * text-highlight.c
3  *
4  * This program is free software; you can redistribute it and/or modify it
5  * under the terms of the GNU Lesser General Public License as published by
6  * the Free Software Foundation.
7  *
8  * This program is distributed in the hope that it will be useful, but
9  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
10  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
11  * for more details.
12  *
13  * You should have received a copy of the GNU Lesser General Public License
14  * along with this program; if not, see <http://www.gnu.org/licenses/>.
15  *
16  */
17 
18 #include "evolution-config.h"
19 
20 #include "e-mail-formatter-text-highlight.h"
21 #include "languages.h"
22 
23 #include <em-format/e-mail-formatter-extension.h>
24 #include <em-format/e-mail-formatter.h>
25 #include <em-format/e-mail-part-utils.h>
26 #include <e-util/e-util.h>
27 
28 #include <libebackend/libebackend.h>
29 #include <libedataserver/libedataserver.h>
30 
31 #include <glib/gi18n-lib.h>
32 #include <camel/camel.h>
33 
34 typedef EMailFormatterExtension EMailFormatterTextHighlight;
35 typedef EMailFormatterExtensionClass EMailFormatterTextHighlightClass;
36 
37 typedef EExtension EMailFormatterTextHighlightLoader;
38 typedef EExtensionClass EMailFormatterTextHighlightLoaderClass;
39 
40 typedef struct _TextHighlightClosure TextHighlightClosure;
41 
42 struct _TextHighlightClosure {
43 	gboolean wrote_anything;
44 	CamelStream *read_stream;
45 	GOutputStream *output_stream;
46 	GCancellable *cancellable;
47 	GError *error;
48 };
49 
50 GType e_mail_formatter_text_highlight_get_type (void);
51 
G_DEFINE_DYNAMIC_TYPE(EMailFormatterTextHighlight,e_mail_formatter_text_highlight,E_TYPE_MAIL_FORMATTER_EXTENSION)52 G_DEFINE_DYNAMIC_TYPE (
53 	EMailFormatterTextHighlight,
54 	e_mail_formatter_text_highlight,
55 	E_TYPE_MAIL_FORMATTER_EXTENSION)
56 
57 static gboolean
58 emfe_text_highlight_formatter_is_enabled (void)
59 {
60 	GSettings *settings;
61 	gboolean enabled;
62 
63 	settings = e_util_ref_settings ("org.gnome.evolution.text-highlight");
64 	enabled = g_settings_get_boolean (settings, "enabled");
65 	g_object_unref (settings);
66 
67 	return enabled;
68 }
69 
70 static gchar *
get_syntax(EMailPart * part,const gchar * uri)71 get_syntax (EMailPart *part,
72             const gchar *uri)
73 {
74 	gchar *syntax = NULL;
75 	CamelContentType *ct = NULL;
76 	CamelMimePart *mime_part;
77 
78 	mime_part = e_mail_part_ref_mime_part (part);
79 
80 	if (uri) {
81 		SoupURI *soup_uri = soup_uri_new (uri);
82 		GHashTable *query = soup_form_decode (soup_uri->query);
83 
84 		syntax = g_hash_table_lookup (query, "__formatas");
85 		if (syntax) {
86 			syntax = g_strdup (syntax);
87 		}
88 		g_hash_table_destroy (query);
89 		soup_uri_free (soup_uri);
90 	}
91 
92 	/* Try to detect syntax by content-type first */
93 	if (syntax == NULL) {
94 		ct = camel_mime_part_get_content_type (mime_part);
95 		if (ct) {
96 			gchar *mime_type = camel_content_type_simple (ct);
97 
98 			syntax = (gchar *) get_syntax_for_mime_type (mime_type);
99 			syntax = syntax ? g_strdup (syntax) : NULL;
100 			g_free (mime_type);
101 		}
102 	}
103 
104 	/* If it fails or the content type too generic, try to detect it by
105 	 * filename extension */
106 	if (syntax == NULL ||
107 	    (ct != NULL &&
108 	     (camel_content_type_is (ct, "application", "octet-stream") ||
109 	     (camel_content_type_is (ct, "text", "plain"))))) {
110 		const gchar *filename;
111 
112 		filename = camel_mime_part_get_filename (mime_part);
113 		if (filename != NULL) {
114 			gchar *ext = g_strrstr (filename, ".");
115 			if (ext != NULL) {
116 				g_free (syntax);
117 				syntax = (gchar *) get_syntax_for_ext (ext + 1);
118 				syntax = syntax ? g_strdup (syntax) : NULL;
119 			}
120 		}
121 	}
122 
123 	/* Out of ideas - use plain text */
124 	if (syntax == NULL) {
125 		syntax = g_strdup ("txt");
126 	}
127 
128 	g_object_unref (mime_part);
129 
130 	return syntax;
131 }
132 
133 static gpointer
text_hightlight_read_data_thread(gpointer user_data)134 text_hightlight_read_data_thread (gpointer user_data)
135 {
136 	TextHighlightClosure *closure = user_data;
137 	gint nbuffer = 10240;
138 	gchar *buffer;
139 
140 	g_return_val_if_fail (closure != NULL, NULL);
141 
142 	buffer = g_new (gchar, nbuffer);
143 
144 	while (!camel_stream_eos (closure->read_stream) &&
145 	       !g_cancellable_set_error_if_cancelled (closure->cancellable, &closure->error)) {
146 		gssize read;
147 		gsize wrote = 0;
148 
149 		read = camel_stream_read (closure->read_stream, buffer, nbuffer, closure->cancellable, &closure->error);
150 		if (read < 0 || closure->error)
151 			break;
152 
153 		closure->wrote_anything = closure->wrote_anything || read > 0;
154 
155 		if (!g_output_stream_write_all (closure->output_stream, buffer, read, &wrote, closure->cancellable, &closure->error) ||
156 		    (gssize) wrote != read || closure->error)
157 			break;
158 	}
159 
160 	g_free (buffer);
161 
162 	return NULL;
163 }
164 
165 static gboolean
text_highlight_feed_data(GOutputStream * output_stream,CamelDataWrapper * data_wrapper,gint pipe_stdin,gint pipe_stdout,GCancellable * cancellable,GError ** error)166 text_highlight_feed_data (GOutputStream *output_stream,
167                           CamelDataWrapper *data_wrapper,
168                           gint pipe_stdin,
169                           gint pipe_stdout,
170                           GCancellable *cancellable,
171                           GError **error)
172 {
173 	TextHighlightClosure closure;
174 	CamelContentType *content_type;
175 	CamelStream *write_stream;
176 	gboolean success = TRUE;
177 	GThread *thread;
178 
179 	closure.wrote_anything = FALSE;
180 	closure.read_stream = camel_stream_fs_new_with_fd (pipe_stdout);
181 	closure.output_stream = output_stream;
182 	closure.cancellable = cancellable;
183 	closure.error = NULL;
184 
185 	write_stream = camel_stream_fs_new_with_fd (pipe_stdin);
186 
187 	thread = g_thread_new (NULL, text_hightlight_read_data_thread, &closure);
188 
189 	content_type = camel_data_wrapper_get_mime_type_field (data_wrapper);
190 	if (content_type) {
191 		const gchar *charset = camel_content_type_param (content_type, "charset");
192 
193 		/* Convert to UTF-8 charset, if needed, which the 'highlight' expects;
194 		   it can cope with non-UTF-8 letters, thus no need for a content UTF-8-validation */
195 		if (charset && g_ascii_strcasecmp (charset, "utf-8") != 0) {
196 			CamelMimeFilter *filter;
197 
198 			filter = camel_mime_filter_charset_new (charset, "UTF-8");
199 			if (filter != NULL) {
200 				CamelStream *filtered = camel_stream_filter_new (write_stream);
201 
202 				if (filtered) {
203 					camel_stream_filter_add (CAMEL_STREAM_FILTER (filtered), filter);
204 					g_object_unref (write_stream);
205 					write_stream = filtered;
206 				}
207 
208 				g_object_unref (filter);
209 			}
210 		}
211 	}
212 
213 	if (camel_data_wrapper_decode_to_stream_sync (data_wrapper, write_stream, cancellable, error) < 0) {
214 		g_cancellable_cancel (cancellable);
215 		success = FALSE;
216 	} else {
217 		/* Close the stream, thus the highlight knows no more data will come */
218 		g_clear_object (&write_stream);
219 	}
220 
221 	g_thread_join (thread);
222 
223 	g_clear_object (&closure.read_stream);
224 	g_clear_object (&write_stream);
225 
226 	if (closure.error) {
227 		if (error && !*error)
228 			g_propagate_error (error, closure.error);
229 		else
230 			g_clear_error (&closure.error);
231 
232 		return FALSE;
233 	}
234 
235 	return success && closure.wrote_anything;
236 }
237 
238 static gboolean
emfe_text_highlight_format(EMailFormatterExtension * extension,EMailFormatter * formatter,EMailFormatterContext * context,EMailPart * part,GOutputStream * stream,GCancellable * cancellable)239 emfe_text_highlight_format (EMailFormatterExtension *extension,
240                             EMailFormatter *formatter,
241                             EMailFormatterContext *context,
242                             EMailPart *part,
243                             GOutputStream *stream,
244                             GCancellable *cancellable)
245 {
246 	CamelMimePart *mime_part;
247 	CamelContentType *ct;
248 	gboolean success = FALSE;
249 
250 	mime_part = e_mail_part_ref_mime_part (part);
251 	ct = camel_mime_part_get_content_type (mime_part);
252 
253 	/* Don't format text/html unless it's an attachment */
254 	if (ct && camel_content_type_is (ct, "text", "html")) {
255 		const CamelContentDisposition *disp;
256 
257 		disp = camel_mime_part_get_content_disposition (mime_part);
258 
259 		if (disp == NULL)
260 			goto exit;
261 
262 		if (g_strcmp0 (disp->disposition, "attachment") != 0)
263 			goto exit;
264 	}
265 
266 	if (context->mode == E_MAIL_FORMATTER_MODE_PRINTING) {
267 		/* Don't interfere with printing. */
268 		goto exit;
269 
270 	} else if (context->mode == E_MAIL_FORMATTER_MODE_RAW) {
271 		gint pipe_stdin, pipe_stdout;
272 		GPid pid;
273 		CamelDataWrapper *dw;
274 		gchar *font_family, *font_size, *syntax, *theme;
275 		PangoFontDescription *fd;
276 		GSettings *settings;
277 		gchar *font = NULL;
278 
279 		const gchar *argv[] = {
280 			HIGHLIGHT_COMMAND,
281 			NULL,	/* --font= */
282 			NULL,   /* --font-size= */
283 			NULL,   /* --syntax= */
284 			NULL,   /* --style= */
285 			"--out-format=html",
286 			"--include-style",
287 			"--inline-css",
288 			"--encoding=none",
289 			"--failsafe",
290 			NULL };
291 
292 		if (!emfe_text_highlight_formatter_is_enabled ()) {
293 			gboolean can_process = FALSE;
294 
295 			if (context->uri) {
296 				SoupURI *soup_uri;
297 
298 				soup_uri = soup_uri_new (context->uri);
299 				if (soup_uri) {
300 					GHashTable *query;
301 
302 					query = soup_form_decode (soup_uri->query);
303 					can_process = query && g_strcmp0 (g_hash_table_lookup (query, "__force_highlight"), "true") == 0;
304 					if (query)
305 						g_hash_table_destroy (query);
306 					soup_uri_free (soup_uri);
307 				}
308 			}
309 
310 			if (!can_process) {
311 				success = e_mail_formatter_format_as (
312 					formatter, context, part, stream,
313 					"application/vnd.evolution.plaintext",
314 					cancellable);
315 				goto exit;
316 			}
317 		}
318 
319 		dw = camel_medium_get_content (CAMEL_MEDIUM (mime_part));
320 		if (dw == NULL)
321 			goto exit;
322 
323 		syntax = get_syntax (part, context->uri);
324 
325 		/* Use the traditional text/plain formatter for plain-text */
326 		if (g_strcmp0 (syntax, "txt") == 0) {
327 			g_free (syntax);
328 			goto exit;
329 		}
330 
331 		settings = e_util_ref_settings ("org.gnome.evolution.mail");
332 		if (g_settings_get_boolean (settings, "use-custom-font"))
333 			font = g_settings_get_string (
334 				settings, "monospace-font");
335 		g_object_unref (settings);
336 
337 		if (font == NULL) {
338 			settings = e_util_ref_settings ("org.gnome.desktop.interface");
339 			font = g_settings_get_string (
340 				settings, "monospace-font-name");
341 			g_object_unref (settings);
342 		}
343 
344 		if (font == NULL)
345 			font = g_strdup ("monospace 10");
346 
347 		fd = pango_font_description_from_string (font);
348 
349 		g_free (font);
350 
351 		font_family = g_strdup_printf (
352 			"--font='%s'",
353 			pango_font_description_get_family (fd));
354 		font_size = g_strdup_printf (
355 			"--font-size=%d",
356 			pango_font_description_get_size (fd) / PANGO_SCALE);
357 
358 		settings = e_util_ref_settings ("org.gnome.evolution.text-highlight");
359 		theme = g_settings_get_string (settings, "theme");
360 		g_object_unref (settings);
361 
362 		if (!theme || !*theme) {
363 			g_free (theme);
364 			theme = g_strdup ("bclear");
365 		}
366 
367 		argv[1] = font_family;
368 		argv[2] = font_size;
369 		argv[3] = g_strdup_printf ("--syntax=%s", syntax);
370 		argv[4] = g_strdup_printf ("--style=%s", theme);
371 		g_free (syntax);
372 		g_free (theme);
373 
374 		success = g_spawn_async_with_pipes (
375 			NULL, (gchar **) argv, NULL, 0, NULL, NULL,
376 			&pid, &pipe_stdin, &pipe_stdout, NULL, NULL);
377 
378 		if (success) {
379 			GError *local_error = NULL;
380 
381 			success = text_highlight_feed_data (
382 				stream, dw,
383 				pipe_stdin, pipe_stdout,
384 				cancellable, &local_error);
385 
386 			if (g_error_matches (
387 				local_error, G_IO_ERROR,
388 				G_IO_ERROR_CANCELLED)) {
389 				/* Do nothing. */
390 
391 			} else if (local_error != NULL) {
392 				g_warning (
393 					"%s: %s", G_STRFUNC,
394 					local_error->message);
395 			}
396 
397 			g_clear_error (&local_error);
398 
399 			g_spawn_close_pid (pid);
400 		}
401 
402 		if (!success) {
403 			/* We can't call e_mail_formatter_format_as on text/plain,
404 			 * because text-highlight is registered as a handler for
405 			 * text/plain, so we would end up in an endless recursion.
406 			 *
407 			 * Just return FALSE here and EMailFormatter will automatically
408 			 * fall back to the default text/plain formatter */
409 			if (!camel_content_type_is (ct, "text", "plain")) {
410 				/* In case of any other content, force use of
411 				 * text/plain formatter, because returning FALSE
412 				 * for text/x-patch or application/php would show
413 				 * an error, as there is no other handler registered
414 				 * for these */
415 				success = e_mail_formatter_format_as (
416 					formatter, context, part, stream,
417 					"application/vnd.evolution.plaintext",
418 					cancellable);
419 			}
420 		}
421 
422 		g_free (font_family);
423 		g_free (font_size);
424 		g_free ((gchar *) argv[3]);
425 		g_free ((gchar *) argv[4]);
426 		pango_font_description_free (fd);
427 
428 		if (!success)
429 			goto exit;
430 	} else {
431 		CamelFolder *folder;
432 		const gchar *message_uid;
433 		const gchar *default_charset, *charset;
434 		gchar *uri, *str;
435 		gchar *syntax;
436 
437 		folder = e_mail_part_list_get_folder (context->part_list);
438 		message_uid = e_mail_part_list_get_message_uid (context->part_list);
439 		default_charset = e_mail_formatter_get_default_charset (formatter);
440 		charset = e_mail_formatter_get_charset (formatter);
441 
442 		if (!default_charset)
443 			default_charset = "";
444 		if (!charset)
445 			charset = "";
446 
447 		syntax = get_syntax (part, NULL);
448 
449 		uri = e_mail_part_build_uri (
450 			folder, message_uid,
451 			"part_id", G_TYPE_STRING, e_mail_part_get_id (part),
452 			"mode", G_TYPE_INT, E_MAIL_FORMATTER_MODE_RAW,
453 			"__formatas", G_TYPE_STRING, syntax,
454 			"formatter_default_charset", G_TYPE_STRING, default_charset,
455 			"formatter_charset", G_TYPE_STRING, charset,
456 			NULL);
457 
458 		g_free (syntax);
459 
460 		str = g_strdup_printf (
461 			"<div class=\"part-container-nostyle\" >"
462 			"<iframe width=\"100%%\" height=\"10\""
463 			" id=\"%s\" name=\"%s\" "
464 			" class=\"-e-mail-formatter-frame-color %s -e-web-view-background-color\" "
465 			" frameborder=\"0\" src=\"%s\" "
466 			" >"
467 			"</iframe>"
468 			"</div>",
469 			e_mail_part_get_id (part),
470 			e_mail_part_get_id (part),
471 			e_mail_part_get_frame_security_style (part),
472 			uri);
473 
474 		g_output_stream_write_all (
475 			stream, str, strlen (str),
476 			NULL, cancellable, NULL);
477 
478 		g_free (str);
479 		g_free (uri);
480 	}
481 
482 	success = TRUE;
483 
484 exit:
485 	g_object_unref (mime_part);
486 
487 	return success;
488 }
489 
490 static void
e_mail_formatter_text_highlight_class_init(EMailFormatterExtensionClass * class)491 e_mail_formatter_text_highlight_class_init (EMailFormatterExtensionClass *class)
492 {
493 	class->display_name = _("Text Highlight");
494 	class->description = _("Syntax highlighting of mail parts");
495 	class->mime_types = get_mime_types ();
496 	class->format = emfe_text_highlight_format;
497 }
498 
499 static void
e_mail_formatter_text_highlight_class_finalize(EMailFormatterExtensionClass * class)500 e_mail_formatter_text_highlight_class_finalize (EMailFormatterExtensionClass *class)
501 {
502 }
503 
504 static void
e_mail_formatter_text_highlight_init(EMailFormatterExtension * extension)505 e_mail_formatter_text_highlight_init (EMailFormatterExtension *extension)
506 {
507 }
508 
509 void
e_mail_formatter_text_highlight_type_register(GTypeModule * type_module)510 e_mail_formatter_text_highlight_type_register (GTypeModule *type_module)
511 {
512 	e_mail_formatter_text_highlight_register_type (type_module);
513 }
514 
515