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