1 /*
2  * This program is free software; you can redistribute it and/or modify it
3  * under the terms of the GNU Lesser General Public License as published by
4  * the Free Software Foundation.
5  *
6  * This program is distributed in the hope that it will be useful, but
7  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
8  * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
9  * for more details.
10  *
11  * You should have received a copy of the GNU Lesser General Public License
12  * along with this program; if not, see <http://www.gnu.org/licenses/>.
13  *
14  *
15  * Authors:
16  *		Iain Holmes <iain@ximian.com>
17  *	    Michael Zucchi <notzed@ximian.com>
18  *
19  * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
20  *
21  */
22 
23 #include "evolution-config.h"
24 
25 #include <sys/types.h>
26 #include <sys/stat.h>
27 #include <fcntl.h>
28 #include <unistd.h>
29 
30 #include <stdio.h>
31 #include <ctype.h>
32 #include <string.h>
33 #include <errno.h>
34 
35 #include <gtk/gtk.h>
36 #include <glib/gi18n.h>
37 #include <glib/gstdio.h>
38 
39 #include "shell/e-shell.h"
40 #include "shell/e-shell-window.h"
41 #include "shell/e-shell-view.h"
42 #include "shell/e-shell-sidebar.h"
43 
44 #include "mail/e-mail-backend.h"
45 #include "mail/em-folder-selection-button.h"
46 #include "mail/em-folder-tree-model.h"
47 #include "mail/em-folder-tree.h"
48 
49 #include "mail-importer.h"
50 
51 typedef struct {
52 	EImport *import;
53 	EImportTarget *target;
54 
55 	GMutex status_lock;
56 	gchar *status_what;
57 	gint status_pc;
58 	gint status_timeout_id;
59 	GCancellable *cancellable;	/* cancel/status port */
60 
61 	gchar *uri;
62 } MboxImporter;
63 
64 static void
folder_selected(EMFolderSelectionButton * button,EImportTargetURI * target)65 folder_selected (EMFolderSelectionButton *button,
66                  EImportTargetURI *target)
67 {
68 	g_free (target->uri_dest);
69 	target->uri_dest = g_strdup (em_folder_selection_button_get_folder_uri (button));
70 }
71 
72 static GtkWidget *
mbox_getwidget(EImport * ei,EImportTarget * target,EImportImporter * im)73 mbox_getwidget (EImport *ei,
74                 EImportTarget *target,
75                 EImportImporter *im)
76 {
77 	EShell *shell;
78 	EShellBackend *shell_backend;
79 	EMailBackend *backend;
80 	EMailSession *session;
81 	GtkWindow *window;
82 	GtkWidget *hbox, *w;
83 	GtkLabel *label;
84 	gchar *select_uri = NULL;
85 
86 	/* XXX Dig up the mail backend from the default EShell.
87 	 *     Since the EImport framework doesn't allow for user
88 	 *     data, I don't see how else to get to it. */
89 	shell = e_shell_get_default ();
90 	shell_backend = e_shell_get_backend_by_name (shell, "mail");
91 
92 	backend = E_MAIL_BACKEND (shell_backend);
93 	session = e_mail_backend_get_session (backend);
94 
95 	/* preselect the folder selected in a mail view */
96 	window = e_shell_get_active_window (shell);
97 	if (E_IS_SHELL_WINDOW (window)) {
98 		EShellWindow *shell_window;
99 		const gchar *view;
100 
101 		shell_window = E_SHELL_WINDOW (window);
102 		view = e_shell_window_get_active_view (shell_window);
103 
104 		if (view && g_str_equal (view, "mail")) {
105 			EShellView *shell_view;
106 			EShellSidebar *shell_sidebar;
107 			EMFolderTree *folder_tree = NULL;
108 
109 			shell_view = e_shell_window_get_shell_view (
110 				shell_window, view);
111 
112 			shell_sidebar =
113 				e_shell_view_get_shell_sidebar (shell_view);
114 
115 			g_object_get (
116 				shell_sidebar, "folder-tree",
117 				&folder_tree, NULL);
118 
119 			select_uri =
120 				em_folder_tree_get_selected_uri (folder_tree);
121 
122 			g_object_unref (folder_tree);
123 		}
124 	}
125 
126 	if (!select_uri) {
127 		const gchar *uri;
128 		uri = e_mail_session_get_local_folder_uri (
129 			session, E_MAIL_LOCAL_FOLDER_INBOX);
130 		select_uri = g_strdup (uri);
131 	}
132 
133 	hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 0);
134 
135 	w = gtk_label_new_with_mnemonic (_("_Destination folder:"));
136 	gtk_box_pack_start ((GtkBox *) hbox, w, FALSE, TRUE, 6);
137 
138 	label = GTK_LABEL (w);
139 
140 	w = em_folder_selection_button_new (
141 		session, _("Select folder"),
142 		_("Select folder to import into"));
143 	gtk_label_set_mnemonic_widget (label, w);
144 	em_folder_selection_button_set_folder_uri (
145 		EM_FOLDER_SELECTION_BUTTON (w), select_uri);
146 	folder_selected (
147 		EM_FOLDER_SELECTION_BUTTON (w), (EImportTargetURI *) target);
148 	g_signal_connect (
149 		w, "selected",
150 		G_CALLBACK (folder_selected), target);
151 	gtk_box_pack_start ((GtkBox *) hbox, w, FALSE, TRUE, 6);
152 
153 	w = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
154 	gtk_box_pack_start ((GtkBox *) w, hbox, FALSE, FALSE, 0);
155 	gtk_widget_show_all (w);
156 
157 	g_free (select_uri);
158 
159 	return w;
160 }
161 
162 static gboolean
mbox_supported(EImport * ei,EImportTarget * target,EImportImporter * im)163 mbox_supported (EImport *ei,
164                 EImportTarget *target,
165                 EImportImporter *im)
166 {
167 	gchar signature[1024];
168 	gboolean ret = FALSE;
169 	gint fd, n;
170 	EImportTargetURI *s;
171 	gchar *filename;
172 
173 	if (target->type != E_IMPORT_TARGET_URI)
174 		return FALSE;
175 
176 	s = (EImportTargetURI *) target;
177 	if (s->uri_src == NULL)
178 		return TRUE;
179 
180 	if (strncmp (s->uri_src, "file:///", strlen ("file:///")) != 0)
181 		return FALSE;
182 
183 	filename = g_filename_from_uri (s->uri_src, NULL, NULL);
184 	fd = g_open (filename, O_RDONLY, 0);
185 	if (fd != -1) {
186 		n = read (fd, signature, 1024);
187 		ret = n >= 5 && memcmp (signature, "From ", 5) == 0;
188 		close (fd);
189 
190 		/* An artificial number, at least 256 bytes message
191 		   to be able to try to import it as an MBOX */
192 		if (!ret && n >= 256) {
193 			gint ii;
194 
195 			ret = (signature[0] >= 'a' && signature[0] <= 'z') ||
196 			      (signature[0] >= 'A' && signature[0] <= 'Z');
197 
198 			for (ii = 0; ii < n && ret; ii++) {
199 				ret = signature[ii] == '-' ||
200 				      signature[ii] == ' ' ||
201 				      signature[ii] == '\t' ||
202 				     (signature[ii] >= 'a' && signature[ii] <= 'z') ||
203 				     (signature[ii] >= 'A' && signature[ii] <= 'Z') ||
204 				     (signature[ii] >= '0' && signature[ii] <= '9');
205 			}
206 
207 			/* It's probably a header name which starts with ASCII letter and
208 			   contains only [a..z][A..Z][\t, ,-] and the read stopped on ':'. */
209 			if (ii > 0 && ii < n && !ret && signature[ii - 1] == ':') {
210 				CamelStream *stream;
211 
212 				stream = camel_stream_fs_new_with_name (filename, O_RDONLY, 0, NULL);
213 				if (stream) {
214 					CamelMimeMessage *msg;
215 
216 					msg = camel_mime_message_new ();
217 
218 					/* Check whether the message can be parsed and whether
219 					   it contains any mandatory fields. */
220 					ret = camel_data_wrapper_construct_from_stream_sync ((CamelDataWrapper *) msg, stream, NULL, NULL) &&
221 					      camel_mime_message_get_message_id (msg) &&
222 					      camel_mime_message_get_subject (msg) &&
223 					      camel_mime_message_get_from (msg) &&
224 					      (camel_mime_message_get_recipients (msg, CAMEL_RECIPIENT_TYPE_TO) ||
225 					       camel_mime_message_get_recipients (msg, CAMEL_RECIPIENT_TYPE_RESENT_TO));
226 
227 					g_object_unref (msg);
228 					g_object_unref (stream);
229 				}
230 			}
231 		}
232 	}
233 
234 	g_free (filename);
235 
236 	return ret;
237 }
238 
239 static void
mbox_status(CamelOperation * op,const gchar * what,gint pc,gpointer data)240 mbox_status (CamelOperation *op,
241              const gchar *what,
242              gint pc,
243              gpointer data)
244 {
245 	MboxImporter *importer = data;
246 
247 	g_mutex_lock (&importer->status_lock);
248 	g_free (importer->status_what);
249 	importer->status_what = g_strdup (what);
250 	importer->status_pc = pc;
251 	g_mutex_unlock (&importer->status_lock);
252 }
253 
254 static gboolean
mbox_status_timeout(gpointer data)255 mbox_status_timeout (gpointer data)
256 {
257 	MboxImporter *importer = data;
258 	gint pc;
259 	gchar *what;
260 
261 	if (importer->status_what) {
262 		g_mutex_lock (&importer->status_lock);
263 		what = importer->status_what;
264 		importer->status_what = NULL;
265 		pc = importer->status_pc;
266 		g_mutex_unlock (&importer->status_lock);
267 
268 		e_import_status (
269 			importer->import, (EImportTarget *)
270 			importer->target, what, pc);
271 	}
272 
273 	return TRUE;
274 }
275 
276 static void
mbox_import_done(gpointer data,GError ** error)277 mbox_import_done (gpointer data,
278                   GError **error)
279 {
280 	MboxImporter *importer = data;
281 
282 	g_source_remove (importer->status_timeout_id);
283 	g_free (importer->status_what);
284 	g_mutex_clear (&importer->status_lock);
285 	g_object_unref (importer->cancellable);
286 
287 	e_import_complete (importer->import, importer->target, error ? *error : NULL);
288 	g_free (importer);
289 }
290 
291 static void
mbox_import(EImport * ei,EImportTarget * target,EImportImporter * im)292 mbox_import (EImport *ei,
293              EImportTarget *target,
294              EImportImporter *im)
295 {
296 	EShell *shell;
297 	EShellBackend *shell_backend;
298 	EMailSession *session;
299 	MboxImporter *importer;
300 	gchar *filename;
301 
302 	/* XXX Dig up the EMailSession from the default EShell.
303 	 *     Since the EImport framework doesn't allow for user
304 	 *     data, I don't see how else to get to it. */
305 	shell = e_shell_get_default ();
306 	shell_backend = e_shell_get_backend_by_name (shell, "mail");
307 	session = e_mail_backend_get_session (E_MAIL_BACKEND (shell_backend));
308 
309 	/* TODO: do we validate target? */
310 
311 	importer = g_malloc0 (sizeof (*importer));
312 	g_datalist_set_data (&target->data, "mbox-data", importer);
313 	importer->import = ei;
314 	importer->target = target;
315 	g_mutex_init (&importer->status_lock);
316 	importer->status_timeout_id =
317 		e_named_timeout_add (100, mbox_status_timeout, importer);
318 	importer->cancellable = camel_operation_new ();
319 
320 	g_signal_connect (
321 		importer->cancellable, "status",
322 		G_CALLBACK (mbox_status), importer);
323 
324 	filename = g_filename_from_uri (
325 		((EImportTargetURI *) target)->uri_src, NULL, NULL);
326 	mail_importer_import_mbox (
327 		session, filename, ((EImportTargetURI *) target)->uri_dest,
328 		importer->cancellable, mbox_import_done, importer);
329 	g_free (filename);
330 }
331 
332 static void
mbox_cancel(EImport * ei,EImportTarget * target,EImportImporter * im)333 mbox_cancel (EImport *ei,
334              EImportTarget *target,
335              EImportImporter *im)
336 {
337 	MboxImporter *importer = g_datalist_get_data (&target->data, "mbox-data");
338 
339 	if (importer)
340 		g_cancellable_cancel (importer->cancellable);
341 }
342 
343 static MboxImporterCreatePreviewFunc create_preview_func = NULL;
344 static MboxImporterFillPreviewFunc fill_preview_func = NULL;
345 
346 void
mbox_importer_set_preview_funcs(MboxImporterCreatePreviewFunc create_func,MboxImporterFillPreviewFunc fill_func)347 mbox_importer_set_preview_funcs (MboxImporterCreatePreviewFunc create_func,
348                                  MboxImporterFillPreviewFunc fill_func)
349 {
350 	create_preview_func = create_func;
351 	fill_preview_func = fill_func;
352 }
353 
354 static void
preview_selection_changed_cb(GtkTreeSelection * selection,EWebViewPreview * preview)355 preview_selection_changed_cb (GtkTreeSelection *selection,
356                               EWebViewPreview *preview)
357 {
358 	GtkTreeIter iter;
359 	GtkTreeModel *model = NULL;
360 	gboolean found = FALSE;
361 
362 	g_return_if_fail (selection != NULL);
363 	g_return_if_fail (preview != NULL);
364 	g_return_if_fail (fill_preview_func != NULL);
365 
366 	if (gtk_tree_selection_get_selected (selection, &model, &iter) && model) {
367 		CamelMimeMessage *msg = NULL;
368 
369 		gtk_tree_model_get (model, &iter, 2, &msg, -1);
370 
371 		if (msg) {
372 			found = TRUE;
373 			fill_preview_func (G_OBJECT (preview), msg);
374 			g_object_unref (msg);
375 		}
376 	}
377 
378 	if (!found) {
379 		e_web_view_preview_begin_update (preview);
380 		e_web_view_preview_end_update (preview);
381 	}
382 }
383 
384 static void
mbox_preview_add_message(CamelMimeMessage * msg,GtkListStore ** pstore)385 mbox_preview_add_message (CamelMimeMessage *msg,
386 			  GtkListStore **pstore)
387 {
388 	GtkTreeIter iter;
389 	gchar *from;
390 
391 	g_return_if_fail (CAMEL_IS_MIME_MESSAGE (msg));
392 	g_return_if_fail (pstore != NULL);
393 
394 	if (!*pstore)
395 		*pstore = gtk_list_store_new (
396 			3, G_TYPE_STRING, G_TYPE_STRING,
397 			CAMEL_TYPE_MIME_MESSAGE);
398 
399 	from = NULL;
400 
401 	if (camel_mime_message_get_from (msg))
402 		from = camel_address_encode (CAMEL_ADDRESS (camel_mime_message_get_from (msg)));
403 
404 	gtk_list_store_append (*pstore, &iter);
405 	gtk_list_store_set (
406 		*pstore, &iter,
407 		0, camel_mime_message_get_subject (msg) ?
408 		camel_mime_message_get_subject (msg) : "",
409 		1, from ? from : "", 2, msg, -1);
410 
411 	g_free (from);
412 }
413 
414 static GtkWidget *
mbox_get_preview(EImport * ei,EImportTarget * target,EImportImporter * im)415 mbox_get_preview (EImport *ei,
416                   EImportTarget *target,
417                   EImportImporter *im)
418 {
419 	GtkWidget *preview = NULL;
420 	EImportTargetURI *s = (EImportTargetURI *) target;
421 	gchar *filename;
422 	gint fd;
423 	CamelMimeParser *mp;
424 	GtkListStore *store = NULL;
425 	GtkTreeIter iter;
426 	GtkWidget *preview_widget = NULL;
427 	gboolean any_read = FALSE;
428 
429 	if (!create_preview_func || !fill_preview_func)
430 		return NULL;
431 
432 	filename = g_filename_from_uri (s->uri_src, NULL, NULL);
433 	if (!filename) {
434 		g_message (G_STRLOC ": Couldn't get filename from URI '%s'", s->uri_src);
435 		return NULL;
436 	}
437 
438 	fd = g_open (filename, O_RDONLY | O_BINARY, 0);
439 	if (fd == -1) {
440 		g_warning (
441 			"Cannot find source file to import '%s': %s",
442 			filename, g_strerror (errno));
443 		g_free (filename);
444 		return NULL;
445 	}
446 
447 	mp = camel_mime_parser_new ();
448 	camel_mime_parser_scan_from (mp, TRUE);
449 	if (camel_mime_parser_init_with_fd (mp, fd) == -1) {
450 		goto cleanup;
451 	}
452 
453 	while (camel_mime_parser_step (mp, NULL, NULL) == CAMEL_MIME_PARSER_STATE_FROM) {
454 		CamelMimeMessage *msg;
455 
456 		any_read = TRUE;
457 
458 		msg = camel_mime_message_new ();
459 		if (!camel_mime_part_construct_from_parser_sync (
460 			(CamelMimePart *) msg, mp, NULL, NULL)) {
461 			g_object_unref (msg);
462 			break;
463 		}
464 
465 		mbox_preview_add_message (msg, &store);
466 
467 		g_object_unref (msg);
468 
469 		camel_mime_parser_step (mp, NULL, NULL);
470 	}
471 
472 	if (!any_read) {
473 		CamelStream *stream;
474 
475 		stream = camel_stream_fs_new_with_name (filename, O_RDONLY, 0, NULL);
476 		if (stream) {
477 			CamelMimeMessage *msg;
478 
479 			msg = camel_mime_message_new ();
480 
481 			if (camel_data_wrapper_construct_from_stream_sync ((CamelDataWrapper *) msg, stream, NULL, NULL))
482 				mbox_preview_add_message (msg, &store);
483 
484 			g_object_unref (msg);
485 			g_object_unref (stream);
486 		}
487 	}
488 
489 	if (store) {
490 		GtkTreeView *tree_view;
491 		GtkTreeSelection *selection;
492 
493 		preview = e_web_view_preview_new ();
494 		gtk_widget_show (preview);
495 
496 		tree_view = e_web_view_preview_get_tree_view (
497 			E_WEB_VIEW_PREVIEW (preview));
498 		if (!tree_view) {
499 			g_warn_if_reached ();
500 			gtk_widget_destroy (preview);
501 			preview = NULL;
502 			goto cleanup;
503 		}
504 
505 		gtk_tree_view_set_model (tree_view, GTK_TREE_MODEL (store));
506 		g_object_unref (store);
507 
508 		gtk_tree_view_insert_column_with_attributes (
509 			/* Translators: Column header for a message subject */
510 			tree_view, -1, C_("mboxImp", "Subject"),
511 			gtk_cell_renderer_text_new (), "text", 0, NULL);
512 
513 		gtk_tree_view_insert_column_with_attributes (
514 			/* Translators: Column header for a message From address */
515 			tree_view, -1, C_("mboxImp", "From"),
516 			gtk_cell_renderer_text_new (), "text", 1, NULL);
517 
518 		if (gtk_tree_model_iter_n_children (GTK_TREE_MODEL (store), NULL) > 1)
519 			e_web_view_preview_show_tree_view (
520 				E_WEB_VIEW_PREVIEW (preview));
521 
522 		create_preview_func (G_OBJECT (preview), &preview_widget);
523 		if (!preview_widget) {
524 			g_warn_if_reached ();
525 			goto cleanup;
526 		}
527 
528 		e_web_view_preview_set_preview (
529 			E_WEB_VIEW_PREVIEW (preview), preview_widget);
530 		gtk_widget_show (preview_widget);
531 
532 		selection = gtk_tree_view_get_selection (tree_view);
533 		if (!gtk_tree_model_get_iter_first (GTK_TREE_MODEL (store), &iter)) {
534 			g_warn_if_reached ();
535 			goto cleanup;
536 		}
537 		gtk_tree_selection_select_iter (selection, &iter);
538 
539 		g_signal_connect (
540 			selection, "changed",
541 			G_CALLBACK (preview_selection_changed_cb), preview);
542 
543 		preview_selection_changed_cb (
544 			selection, E_WEB_VIEW_PREVIEW (preview));
545 	}
546 
547  cleanup:
548 	g_object_unref (mp);
549 	g_free (filename);
550 
551 	/* 'fd' is freed together with 'mp' */
552 	/* coverity[leaked_handle] */
553 	return preview;
554 }
555 
556 static EImportImporter mbox_importer = {
557 	E_IMPORT_TARGET_URI,
558 	0,
559 	mbox_supported,
560 	mbox_getwidget,
561 	mbox_import,
562 	mbox_cancel,
563 	mbox_get_preview,
564 };
565 
566 EImportImporter *
mbox_importer_peek(void)567 mbox_importer_peek (void)
568 {
569 	mbox_importer.name = _("Berkeley Mailbox (mbox)");
570 	mbox_importer.description = _("Importer Berkeley Mailbox format folders");
571 
572 	return &mbox_importer;
573 }
574