1 /* -*-mode:c; c-style:k&r; c-basic-offset:4; -*- */
2 /* Balsa E-Mail Client
3 * Copyright (C) 1997-2013 Stuart Parmenter and others,
4 * See the file AUTHORS for a list.
5 *
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 2, or (at your option)
9 * any later version.
10 *
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.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
19 * 02111-1307, USA.
20 */
21
22 #if defined(HAVE_CONFIG_H) && HAVE_CONFIG_H
23 # include "config.h"
24 #endif /* HAVE_CONFIG_H */
25 #include "balsa-message.h"
26
27 #include <errno.h>
28 #include <stdio.h>
29 #include <string.h>
30 #include <ctype.h>
31 #include <iconv.h>
32 #include <sys/utsname.h>
33
34 #include "balsa-app.h"
35 #include "balsa-icons.h"
36 #include "mime.h"
37 #include "misc.h"
38 #include "html.h"
39 #include <glib/gi18n.h>
40 #include "balsa-mime-widget.h"
41 #include "balsa-mime-widget-callbacks.h"
42 #include "balsa-mime-widget-message.h"
43 #include "balsa-mime-widget-image.h"
44 #include "balsa-mime-widget-crypto.h"
45
46 #include <gdk/gdkkeysyms.h>
47 #include <gdk-pixbuf/gdk-pixbuf.h>
48
49 #include "send.h"
50 #include "quote-color.h"
51 #include "sendmsg-window.h"
52 #include "libbalsa-vfs.h"
53
54 #if HAVE_MACOSX_DESKTOP
55 # include "macosx-helpers.h"
56 #endif
57
58 #ifdef HAVE_GPGME
59 # include "gmime-part-rfc2440.h"
60 #endif
61
62 # define FORWARD_SEARCH(iter, text, match_begin, match_end) \
63 gtk_text_iter_forward_search((iter), (text), \
64 GTK_TEXT_SEARCH_CASE_INSENSITIVE, (match_begin), (match_end), NULL)
65 # define BACKWARD_SEARCH(iter, text, match_begin, match_end) \
66 gtk_text_iter_backward_search((iter), (text), \
67 GTK_TEXT_SEARCH_CASE_INSENSITIVE, (match_begin), (match_end), NULL)
68
69 enum {
70 SELECT_PART,
71 LAST_SIGNAL,
72 };
73
74 enum {
75 PART_INFO_COLUMN = 0,
76 PART_NUM_COLUMN,
77 MIME_ICON_COLUMN,
78 MIME_TYPE_COLUMN,
79 NUM_COLUMNS
80 };
81
82 typedef struct _BalsaPartInfoClass BalsaPartInfoClass;
83
84 struct _BalsaPartInfo {
85 GObject parent_object;
86
87 LibBalsaMessageBody *body;
88
89 /* MIME widget */
90 BalsaMimeWidget *mime_widget;
91
92 /* The contect menu; referenced */
93 GtkWidget *popup_menu;
94
95 /* the path in the tree view */
96 GtkTreePath *path;
97 };
98
99 struct _BalsaPartInfoClass {
100 GObjectClass parent_class;
101 };
102
103 static GType balsa_part_info_get_type();
104
105 #define TYPE_BALSA_PART_INFO \
106 (balsa_part_info_get_type ())
107 #define BALSA_PART_INFO(obj) \
108 (G_TYPE_CHECK_INSTANCE_CAST ((obj), TYPE_BALSA_PART_INFO, BalsaPartInfo))
109 #define IS_BALSA_PART_INFO(obj) \
110 (G_TYPE_CHECK_INSTANCE_TYPE ((obj), TYPE_BALSA_PART_INFO))
111
112 static gint balsa_message_signals[LAST_SIGNAL];
113
114 /* widget */
115 static void balsa_message_class_init(BalsaMessageClass * klass);
116 static void balsa_message_init(BalsaMessage * bm);
117
118 static void balsa_message_destroy(GObject * object);
119
120 static void display_headers(BalsaMessage * bm);
121 static void display_content(BalsaMessage * bm);
122
123 static LibBalsaMessageBody *add_part(BalsaMessage *bm, BalsaPartInfo *info,
124 GtkWidget * container);
125 static LibBalsaMessageBody *add_multipart(BalsaMessage * bm,
126 LibBalsaMessageBody * parent,
127 GtkWidget * container);
128 static void select_part(BalsaMessage * bm, BalsaPartInfo *info);
129 static void tree_activate_row_cb(GtkTreeView *treeview, GtkTreePath *arg1,
130 GtkTreeViewColumn *arg2, gpointer user_data);
131 static gboolean tree_menu_popup_key_cb(GtkWidget *widget, gpointer user_data);
132 static gboolean tree_button_press_cb(GtkWidget * widget, GdkEventButton * event,
133 gpointer data);
134
135 static void part_info_init(BalsaMessage * bm, BalsaPartInfo * info);
136 static void part_context_save_all_cb(GtkWidget * menu_item, GList * info_list);
137 static void part_context_dump_all_cb(GtkWidget * menu_item, GList * info_list);
138 static void part_create_menu (BalsaPartInfo* info);
139
140 static GtkNotebookClass *parent_class = NULL;
141
142 /* stuff needed for sending Message Disposition Notifications */
143 static void handle_mdn_request(GtkWindow *parent, LibBalsaMessage *message);
144 static LibBalsaMessage *create_mdn_reply (const LibBalsaIdentity *mdn_ident,
145 LibBalsaMessage *for_msg,
146 gboolean manual);
147 static GtkWidget* create_mdn_dialog (GtkWindow *parent, gchar *sender,
148 gchar *mdn_to_address,
149 LibBalsaMessage *send_msg,
150 LibBalsaIdentity *mdn_ident);
151 static void mdn_dialog_response(GtkWidget * dialog, gint response,
152 gpointer user_data);
153
154 static void balsa_part_info_init(GObject *object, gpointer data);
155 static BalsaPartInfo* balsa_part_info_new(LibBalsaMessageBody* body);
156 static void balsa_part_info_free(GObject * object);
157
158
159 #ifdef HAVE_GPGME
160 static LibBalsaMsgProtectState balsa_message_scan_signatures(LibBalsaMessageBody *body,
161 LibBalsaMessage * message);
162 static GdkPixbuf * get_crypto_content_icon(LibBalsaMessageBody * body,
163 const gchar * content_type,
164 gchar ** icon_title);
165 static void message_recheck_crypto_cb(GtkWidget * button, BalsaMessage * bm);
166 #endif /* HAVE_GPGME */
167
168
169 static void
balsa_part_info_class_init(BalsaPartInfoClass * klass)170 balsa_part_info_class_init(BalsaPartInfoClass *klass)
171 {
172 GObjectClass *object_class = G_OBJECT_CLASS(klass);
173
174 object_class->finalize = balsa_part_info_free;
175 }
176
177 static GType
balsa_part_info_get_type()178 balsa_part_info_get_type()
179 {
180 static GType balsa_part_info_type = 0 ;
181
182 if (!balsa_part_info_type) {
183 static const GTypeInfo balsa_part_info_info =
184 {
185 sizeof (BalsaPartInfoClass),
186 (GBaseInitFunc) NULL,
187 (GBaseFinalizeFunc) NULL,
188 (GClassInitFunc) balsa_part_info_class_init,
189 (GClassFinalizeFunc) NULL,
190 NULL,
191 sizeof(BalsaPartInfo),
192 0,
193 (GInstanceInitFunc) balsa_part_info_init
194 };
195 balsa_part_info_type =
196 g_type_register_static (G_TYPE_OBJECT, "BalsaPartInfo",
197 &balsa_part_info_info, 0);
198 }
199 return balsa_part_info_type;
200 }
201
202 GType
balsa_message_get_type()203 balsa_message_get_type()
204 {
205 static GType balsa_message_type = 0;
206
207 if (!balsa_message_type) {
208 static const GTypeInfo balsa_message_info = {
209 sizeof(BalsaMessageClass),
210 NULL, /* base_init */
211 NULL, /* base_finalize */
212 (GClassInitFunc) balsa_message_class_init,
213 NULL, /* class_finalize */
214 NULL, /* class_data */
215 sizeof(BalsaMessage),
216 0, /* n_preallocs */
217 (GInstanceInitFunc) balsa_message_init
218 };
219
220 balsa_message_type =
221 g_type_register_static(GTK_TYPE_NOTEBOOK, "BalsaMessage",
222 &balsa_message_info, 0);
223 }
224
225 return balsa_message_type;
226 }
227
228 static void
balsa_message_class_init(BalsaMessageClass * klass)229 balsa_message_class_init(BalsaMessageClass * klass)
230 {
231 GObjectClass *object_class;
232
233 object_class = G_OBJECT_CLASS(klass);
234
235 balsa_message_signals[SELECT_PART] =
236 g_signal_new("select-part",
237 G_TYPE_FROM_CLASS(object_class),
238 G_SIGNAL_RUN_FIRST,
239 G_STRUCT_OFFSET(BalsaMessageClass, select_part),
240 NULL, NULL,
241 g_cclosure_marshal_VOID__VOID,
242 G_TYPE_NONE, 0);
243
244 object_class->dispose = balsa_message_destroy;
245
246 parent_class = g_type_class_peek_parent(klass);
247
248 klass->select_part = NULL;
249
250 }
251
252 /* Helpers for balsa_message_init. */
253 #define BALSA_MESSAGE_ATTACH_BTN "balsa-message-attach-btn"
254 #define bm_header_widget_att_button(balsa_message) \
255 g_object_get_data(G_OBJECT(balsa_message), BALSA_MESSAGE_ATTACH_BTN)
256
257 static void
balsa_headers_attachments_popup(GtkButton * button,BalsaMessage * bm)258 balsa_headers_attachments_popup(GtkButton * button, BalsaMessage * bm)
259 {
260 if (bm->parts_popup)
261 gtk_menu_popup(GTK_MENU(bm->parts_popup), NULL, NULL, NULL, NULL, 0,
262 gtk_get_current_event_time());
263 }
264
265
266 static GtkWidget **
bm_header_tl_buttons(BalsaMessage * bm)267 bm_header_tl_buttons(BalsaMessage * bm)
268 {
269 GPtrArray *array;
270 GtkWidget *button;
271
272 array = g_ptr_array_new();
273
274 #ifdef HAVE_GPGME
275 button = gtk_button_new();
276 gtk_widget_set_tooltip_text(button,
277 _("Check cryptographic signature"));
278 g_signal_connect(G_OBJECT(button), "focus_in_event",
279 G_CALLBACK(balsa_mime_widget_limit_focus),
280 (gpointer) bm);
281 g_signal_connect(G_OBJECT(button), "focus_out_event",
282 G_CALLBACK(balsa_mime_widget_unlimit_focus),
283 (gpointer) bm);
284 gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
285 gtk_container_add(GTK_CONTAINER(button),
286 gtk_image_new_from_stock(BALSA_PIXMAP_GPG_RECHECK,
287 GTK_ICON_SIZE_LARGE_TOOLBAR));
288 g_signal_connect(button, "clicked",
289 G_CALLBACK(message_recheck_crypto_cb), bm);
290 g_ptr_array_add(array, button);
291 #endif
292
293 button = gtk_button_new();
294 gtk_widget_set_tooltip_text(button,
295 _("Select message part to display"));
296 g_signal_connect(G_OBJECT(button), "focus_in_event",
297 G_CALLBACK(balsa_mime_widget_limit_focus),
298 (gpointer) bm);
299 g_signal_connect(G_OBJECT(button), "focus_out_event",
300 G_CALLBACK(balsa_mime_widget_unlimit_focus),
301 (gpointer) bm);
302 gtk_button_set_relief(GTK_BUTTON(button), GTK_RELIEF_NONE);
303 gtk_container_add(GTK_CONTAINER(button),
304 gtk_image_new_from_stock(BALSA_PIXMAP_ATTACHMENT,
305 GTK_ICON_SIZE_LARGE_TOOLBAR));
306 g_signal_connect(button, "clicked",
307 G_CALLBACK(balsa_headers_attachments_popup), bm);
308 g_signal_connect(button, "key_press_event",
309 G_CALLBACK(balsa_mime_widget_key_press_event), bm);
310 g_object_set_data(G_OBJECT(bm), BALSA_MESSAGE_ATTACH_BTN, button);
311 g_ptr_array_add(array, button);
312
313 g_ptr_array_add(array, NULL);
314
315 return (GtkWidget **) g_ptr_array_free(array, FALSE);
316 }
317
318
319 /* Callback for the "style-updated" signal; set the message background to
320 * match the base color of the content in the tree-view. */
321 static void
bm_on_set_style(GtkWidget * widget,BalsaMessage * bm)322 bm_on_set_style(GtkWidget * widget,
323 BalsaMessage * bm)
324 {
325 GtkStyleContext *context;
326 GdkRGBA rgba;
327
328 context = gtk_widget_get_style_context(bm->treeview);
329 gtk_style_context_get_background_color(context, GTK_STATE_FLAG_NORMAL,
330 &rgba);
331 gtk_widget_override_background_color(bm->scroll,
332 GTK_STATE_FLAG_NORMAL, &rgba);
333 }
334
335 static void
on_content_size_alloc(GtkWidget * widget,GtkAllocation * allocation,gpointer user_data)336 on_content_size_alloc(GtkWidget * widget, GtkAllocation * allocation,
337 gpointer user_data)
338 {
339 gtk_container_foreach (GTK_CONTAINER(widget), balsa_mime_widget_image_resize_all, NULL);
340 }
341
342 /*
343 * Callbacks and helpers for the find bar.
344 */
345
346 typedef enum {
347 BM_FIND_STATUS_INIT,
348 BM_FIND_STATUS_FOUND,
349 BM_FIND_STATUS_WRAPPED,
350 BM_FIND_STATUS_NOT_FOUND
351 } BalsaMessageFindStatus;
352
353 static void
bm_find_set_status(BalsaMessage * bm,BalsaMessageFindStatus status)354 bm_find_set_status(BalsaMessage * bm, BalsaMessageFindStatus status)
355 {
356 const gchar *text = "";
357 gboolean sensitive = FALSE;
358
359 switch (status) {
360 default:
361 case BM_FIND_STATUS_INIT:
362 break;
363 case BM_FIND_STATUS_FOUND:
364 /* The widget returned "found"; if it really found a string,
365 * we sensitize the "next" and "previous" buttons, but if
366 * the find-entry was empty, we desensitize them. */
367 if (gtk_entry_get_text(GTK_ENTRY(bm->find_entry))[0])
368 sensitive = TRUE;
369 break;
370 case BM_FIND_STATUS_WRAPPED:
371 text = _("Wrapped");
372 sensitive = TRUE;
373 break;
374 case BM_FIND_STATUS_NOT_FOUND:
375 text = _("Not found");
376 break;
377 }
378
379 gtk_label_set_text(GTK_LABEL(bm->find_label), text);
380 gtk_separator_tool_item_set_draw(GTK_SEPARATOR_TOOL_ITEM
381 (bm->find_sep), text[0] != '\0');
382 gtk_widget_set_sensitive(bm->find_prev, sensitive);
383 gtk_widget_set_sensitive(bm->find_next, sensitive);
384 }
385
386 static void
bm_find_scroll_to_rectangle(BalsaMessage * bm,GtkWidget * widget,GdkRectangle * rectangle)387 bm_find_scroll_to_rectangle(BalsaMessage * bm,
388 GtkWidget * widget,
389 GdkRectangle * rectangle)
390 {
391 gint x, y;
392 GtkAdjustment *hadj, *vadj;
393
394 gtk_widget_translate_coordinates(widget, bm->bm_widget->widget,
395 rectangle->x, rectangle->y,
396 &x, &y);
397
398 g_object_get(G_OBJECT(bm->scroll), "hadjustment", &hadj,
399 "vadjustment", &vadj, NULL);
400 gtk_adjustment_clamp_page(hadj, x, x + rectangle->width);
401 gtk_adjustment_clamp_page(vadj, y, y + rectangle->height);
402 }
403
404 static void
bm_find_scroll_to_selection(BalsaMessage * bm,GtkTextView * text_view,GtkTextIter * begin_iter,GtkTextIter * end_iter)405 bm_find_scroll_to_selection(BalsaMessage * bm,
406 GtkTextView * text_view,
407 GtkTextIter * begin_iter,
408 GtkTextIter * end_iter)
409 {
410 GdkRectangle begin_location, end_location;
411
412 gtk_text_view_get_iter_location(text_view, begin_iter,
413 &begin_location);
414 gtk_text_view_get_iter_location(text_view, end_iter,
415 &end_location);
416 end_location.width = 0;
417 gdk_rectangle_union(&begin_location, &end_location, &begin_location);
418 gtk_text_view_buffer_to_window_coords(text_view,
419 GTK_TEXT_WINDOW_WIDGET,
420 begin_location.x,
421 begin_location.y,
422 &begin_location.x,
423 &begin_location.y);
424
425 bm_find_scroll_to_rectangle(bm, GTK_WIDGET(text_view), &begin_location);
426 }
427
428 #ifdef HAVE_HTML_WIDGET
429 typedef struct {
430 BalsaMessage *bm;
431 GtkWidget *widget;
432 gboolean continuing;
433 gboolean wrapping;
434 } BalsaMessageFindInfo;
435 #define BALSA_MESSAGE_FIND_INFO "BalsaMessageFindInfo"
436
437 static void
bm_find_cb(const gchar * text,gboolean found,gpointer data)438 bm_find_cb(const gchar * text, gboolean found, gpointer data)
439 {
440 BalsaMessageFindInfo *info = data;
441
442 if (!found && info->continuing) {
443 info->wrapping = TRUE;
444 libbalsa_html_search(info->widget, text, info->bm->find_forward,
445 TRUE, bm_find_cb, info);
446 return;
447 }
448
449 if (found && *text) {
450 GdkRectangle selection_bounds;
451 if (libbalsa_html_get_selection_bounds(info->widget,
452 &selection_bounds))
453 bm_find_scroll_to_rectangle(info->bm, info->widget,
454 &selection_bounds);
455 }
456
457 if (info->wrapping) {
458 info->wrapping = FALSE;
459 bm_find_set_status(info->bm, BM_FIND_STATUS_WRAPPED);
460 } else
461 bm_find_set_status(info->bm, found ? BM_FIND_STATUS_FOUND :
462 BM_FIND_STATUS_NOT_FOUND);
463 }
464 #endif /* HAVE_HTML_WIDGET */
465
466 static void
bm_find_entry_changed_cb(GtkEditable * editable,gpointer data)467 bm_find_entry_changed_cb(GtkEditable * editable, gpointer data)
468 {
469 const gchar *text = gtk_entry_get_text(GTK_ENTRY(editable));
470 BalsaMessage *bm = data;
471 GtkWidget *widget = bm->current_part->mime_widget->widget;
472 gboolean found = FALSE;
473
474 if (GTK_IS_TEXT_VIEW(widget)) {
475 GtkTextView *text_view = GTK_TEXT_VIEW(widget);
476 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text_view);
477 GtkTextIter match_begin, match_end;
478
479 if (bm->find_forward) {
480 found = FORWARD_SEARCH(&bm->find_iter, text,
481 &match_begin, &match_end);
482 if (!found) {
483 /* Silently wrap to the top. */
484 gtk_text_buffer_get_start_iter(buffer, &bm->find_iter);
485 found = FORWARD_SEARCH(&bm->find_iter, text,
486 &match_begin, &match_end);
487 }
488 } else {
489 found = BACKWARD_SEARCH(&bm->find_iter, text,
490 &match_begin, &match_end);
491 if (!found) {
492 /* Silently wrap to the bottom. */
493 gtk_text_buffer_get_end_iter(buffer, &bm->find_iter);
494 found = BACKWARD_SEARCH(&bm->find_iter, text,
495 &match_begin, &match_end);
496 }
497 }
498
499 if (found) {
500 gtk_text_buffer_select_range(buffer, &match_begin, &match_end);
501 bm_find_scroll_to_selection(bm, text_view,
502 &match_begin, &match_end);
503 bm->find_iter = match_begin;
504 }
505
506 bm_find_set_status(bm, found ? BM_FIND_STATUS_FOUND :
507 BM_FIND_STATUS_NOT_FOUND);
508 #ifdef HAVE_HTML_WIDGET
509 } else if (libbalsa_html_can_search(widget)) {
510 BalsaMessageFindInfo *info;
511
512 if (!(info = bm->html_find_info)) {
513 bm->html_find_info = info = g_new(BalsaMessageFindInfo, 1);
514 info->bm = bm;
515 }
516 info->widget = widget;
517 info->continuing = FALSE;
518 info->wrapping = FALSE;
519
520 libbalsa_html_search(widget, text, bm->find_forward, TRUE,
521 bm_find_cb, info);
522 #endif /* HAVE_HTML_WIDGET */
523 } else
524 g_assert_not_reached();
525 }
526
527 static void
bm_find_again(BalsaMessage * bm,gboolean find_forward)528 bm_find_again(BalsaMessage * bm, gboolean find_forward)
529 {
530 const gchar *text = gtk_entry_get_text(GTK_ENTRY(bm->find_entry));
531 GtkWidget *widget = bm->current_part->mime_widget->widget;
532 gboolean found;
533
534 bm->find_forward = find_forward;
535
536 if (GTK_IS_TEXT_VIEW(widget)) {
537 GtkTextView *text_view = GTK_TEXT_VIEW(widget);
538 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text_view);
539 GtkTextIter match_begin, match_end;
540
541 if (find_forward) {
542 gtk_text_iter_forward_char(&bm->find_iter);
543 found = FORWARD_SEARCH(&bm->find_iter, text,
544 &match_begin, &match_end);
545 if (!found) {
546 gtk_text_buffer_get_start_iter(buffer, &bm->find_iter);
547 FORWARD_SEARCH(&bm->find_iter, text, &match_begin,
548 &match_end);
549 }
550 } else {
551 gtk_text_iter_backward_char(&bm->find_iter);
552 found = BACKWARD_SEARCH(&bm->find_iter, text,
553 &match_begin, &match_end);
554 if (!found) {
555 gtk_text_buffer_get_end_iter(buffer, &bm->find_iter);
556 BACKWARD_SEARCH(&bm->find_iter, text, &match_begin,
557 &match_end);
558 }
559 }
560
561 gtk_text_buffer_select_range(buffer, &match_begin, &match_end);
562 bm_find_scroll_to_selection(bm, text_view,
563 &match_begin, &match_end);
564 bm->find_iter = match_begin;
565
566 bm_find_set_status(bm, found ?
567 BM_FIND_STATUS_FOUND : BM_FIND_STATUS_WRAPPED);
568 #ifdef HAVE_HTML_WIDGET
569 } else if (libbalsa_html_can_search(widget)) {
570 BalsaMessageFindInfo *info = bm->html_find_info;
571
572 info->continuing = TRUE;
573 libbalsa_html_search(widget, text, find_forward, FALSE,
574 bm_find_cb, info);
575 #endif /* HAVE_HTML_WIDGET */
576 } else
577 g_assert_not_reached();
578 }
579
580 static void
bm_find_prev_cb(GtkToolButton * prev_button,gpointer data)581 bm_find_prev_cb(GtkToolButton * prev_button, gpointer data)
582 {
583 bm_find_again((BalsaMessage *) data, FALSE);
584 }
585
586 static void
bm_find_next_cb(GtkToolButton * prev_button,gpointer data)587 bm_find_next_cb(GtkToolButton * prev_button, gpointer data)
588 {
589 bm_find_again((BalsaMessage *) data, TRUE);
590 }
591
592 static GtkWidget *
bm_find_bar_new(BalsaMessage * bm)593 bm_find_bar_new(BalsaMessage * bm)
594 {
595 GtkWidget *toolbar;
596 GtkWidget *hbox;
597 GtkToolItem *tool_item;
598
599 toolbar = gtk_toolbar_new();
600 gtk_toolbar_set_style(GTK_TOOLBAR(toolbar), GTK_TOOLBAR_BOTH_HORIZ);
601
602 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
603 gtk_box_pack_start(GTK_BOX(hbox), gtk_label_new(_("Find:")),
604 FALSE, FALSE, 0);
605 bm->find_entry = gtk_entry_new();
606 g_signal_connect(bm->find_entry, "changed",
607 G_CALLBACK(bm_find_entry_changed_cb), bm);
608 gtk_box_pack_start(GTK_BOX(hbox), bm->find_entry, FALSE, FALSE, 0);
609
610 tool_item = gtk_tool_item_new();
611 gtk_container_add(GTK_CONTAINER(tool_item), hbox);
612 gtk_toolbar_insert(GTK_TOOLBAR(toolbar), tool_item, -1);
613
614 tool_item =
615 gtk_tool_button_new(gtk_arrow_new(GTK_ARROW_LEFT, GTK_SHADOW_NONE),
616 _("Previous"));
617 bm->find_prev = GTK_WIDGET(tool_item);
618 gtk_tool_item_set_is_important(tool_item, TRUE);
619 g_signal_connect(tool_item, "clicked", G_CALLBACK(bm_find_prev_cb), bm);
620 gtk_toolbar_insert(GTK_TOOLBAR(toolbar), tool_item, -1);
621
622 tool_item =
623 gtk_tool_button_new(gtk_arrow_new(GTK_ARROW_RIGHT, GTK_SHADOW_NONE),
624 _("Next"));
625 bm->find_next = GTK_WIDGET(tool_item);
626 gtk_tool_item_set_is_important(tool_item, TRUE);
627 g_signal_connect(tool_item, "clicked", G_CALLBACK(bm_find_next_cb), bm);
628 gtk_toolbar_insert(GTK_TOOLBAR(toolbar), tool_item, -1);
629
630 bm->find_sep = GTK_WIDGET(gtk_separator_tool_item_new());
631 gtk_toolbar_insert(GTK_TOOLBAR(toolbar), GTK_TOOL_ITEM(bm->find_sep), -1);
632
633 bm->find_label = gtk_label_new("");
634 tool_item = gtk_tool_item_new();
635 gtk_container_add(GTK_CONTAINER(tool_item), bm->find_label);
636 gtk_toolbar_insert(GTK_TOOLBAR(toolbar), tool_item, -1);
637
638 return toolbar;
639 }
640
641 static void bm_disable_find_entry(BalsaMessage * bm);
642
643 static gboolean
bm_find_pass_to_entry(BalsaMessage * bm,GdkEventKey * event)644 bm_find_pass_to_entry(BalsaMessage * bm, GdkEventKey * event)
645 {
646 gboolean res = TRUE;
647
648 switch (event->keyval) {
649 case GDK_KEY_Escape:
650 case GDK_KEY_Return:
651 case GDK_KEY_KP_Enter:
652 bm_disable_find_entry(bm);
653 return res;
654 case GDK_KEY_g:
655 if ((event->state & (GDK_SHIFT_MASK | GDK_CONTROL_MASK)) ==
656 GDK_CONTROL_MASK && gtk_widget_get_sensitive(bm->find_next)) {
657 bm_find_again(bm, bm->find_forward);
658 return res;
659 }
660 default:
661 break;
662 }
663
664 res = FALSE;
665 if (gtk_widget_has_focus(bm->find_entry))
666 g_signal_emit_by_name(bm->find_entry, "key-press-event", event,
667 &res, NULL);
668
669 return res;
670 }
671
672 static void
bm_disable_find_entry(BalsaMessage * bm)673 bm_disable_find_entry(BalsaMessage * bm)
674 {
675 g_signal_handlers_disconnect_by_func
676 (gtk_widget_get_toplevel(GTK_WIDGET(bm)),
677 G_CALLBACK(bm_find_pass_to_entry), bm);
678 gtk_widget_hide(bm->find_bar);
679 }
680
681 /*
682 * End of callbacks and helpers for the find bar.
683 */
684
685 static void
balsa_message_init(BalsaMessage * bm)686 balsa_message_init(BalsaMessage * bm)
687 {
688 GtkWidget *vbox;
689 GtkWidget *scroll;
690 GtkWidget *label;
691 GtkWidget **buttons;
692 GtkTreeStore *model;
693 GtkCellRenderer *renderer;
694 GtkTreeSelection *selection;
695
696 gtk_notebook_set_show_border(GTK_NOTEBOOK(bm), FALSE);
697
698 /* Box to hold the scrolled window and the find bar */
699 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
700 label = gtk_label_new(_("Content"));
701 gtk_notebook_append_page(GTK_NOTEBOOK(bm), vbox, label);
702
703 /* scrolled window for the contents */
704 bm->scroll = scroll = gtk_scrolled_window_new(NULL, NULL);
705 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
706 GTK_POLICY_AUTOMATIC,
707 GTK_POLICY_AUTOMATIC);
708 g_signal_connect(scroll, "key_press_event",
709 G_CALLBACK(balsa_mime_widget_key_press_event), bm);
710 gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0);
711 g_signal_connect_after(bm, "style-updated",
712 G_CALLBACK(bm_on_set_style), bm);
713 g_signal_connect(bm->scroll, "size-allocate",
714 G_CALLBACK(on_content_size_alloc), NULL);
715
716 /* Widget to hold headers */
717 buttons = bm_header_tl_buttons(bm);
718 bm->bm_widget = balsa_mime_widget_new_message_tl(bm, buttons);
719 g_free(buttons);
720
721 /* Widget to hold message */
722 g_signal_connect(G_OBJECT(bm->bm_widget->widget), "focus_in_event",
723 G_CALLBACK(balsa_mime_widget_limit_focus),
724 (gpointer) bm);
725 g_signal_connect(G_OBJECT(bm->bm_widget->widget), "focus_out_event",
726 G_CALLBACK(balsa_mime_widget_unlimit_focus),
727 (gpointer) bm);
728 #if GTK_CHECK_VERSION(3, 8, 0)
729 gtk_container_add(GTK_CONTAINER(bm->scroll), bm->bm_widget->widget);
730 #else /* GTK_CHECK_VERSION(3, 8, 0 */
731 gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(bm->scroll),
732 bm->bm_widget->widget);
733 #endif /* GTK_CHECK_VERSION(3, 8, 0 */
734
735 /* structure view */
736 model = gtk_tree_store_new (NUM_COLUMNS,
737 TYPE_BALSA_PART_INFO,
738 G_TYPE_STRING,
739 GDK_TYPE_PIXBUF,
740 G_TYPE_STRING);
741 bm->treeview = gtk_tree_view_new_with_model (GTK_TREE_MODEL(model));
742 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW (bm->treeview));
743 g_signal_connect(bm->treeview, "row-activated",
744 G_CALLBACK(tree_activate_row_cb), bm);
745 g_signal_connect(bm->treeview, "button_press_event",
746 G_CALLBACK(tree_button_press_cb), bm);
747 g_signal_connect(bm->treeview, "popup-menu",
748 G_CALLBACK(tree_menu_popup_key_cb), bm);
749 g_object_unref (G_OBJECT (model));
750 gtk_tree_view_set_rules_hint (GTK_TREE_VIEW (bm->treeview), TRUE);
751 gtk_tree_selection_set_mode (selection, GTK_SELECTION_MULTIPLE);
752 gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (bm->treeview), FALSE);
753
754 /* column for the part number */
755 renderer = gtk_cell_renderer_text_new ();
756 g_object_set (G_OBJECT (renderer), "xalign", 0.0, NULL);
757 gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (bm->treeview),
758 -1, NULL,
759 renderer, "text",
760 PART_NUM_COLUMN,
761 NULL);
762
763 /* column for type icon */
764 renderer = gtk_cell_renderer_pixbuf_new ();
765 g_object_set (G_OBJECT (renderer), "xalign", 0.0, NULL);
766 gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (bm->treeview),
767 -1, NULL,
768 renderer, "pixbuf",
769 MIME_ICON_COLUMN,
770 NULL);
771
772 /* column for mime type */
773 renderer = gtk_cell_renderer_text_new ();
774 g_object_set (G_OBJECT (renderer), "xalign", 0.0, NULL);
775 gtk_tree_view_insert_column_with_attributes (GTK_TREE_VIEW (bm->treeview),
776 -1, NULL,
777 renderer, "text",
778 MIME_TYPE_COLUMN,
779 NULL);
780
781 /* scrolled window for the tree view */
782 scroll = gtk_scrolled_window_new(NULL, NULL);
783 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll),
784 GTK_POLICY_AUTOMATIC,
785 GTK_POLICY_AUTOMATIC);
786
787 gtk_tree_view_set_expander_column
788 (GTK_TREE_VIEW (bm->treeview), gtk_tree_view_get_column
789 (GTK_TREE_VIEW (bm->treeview), MIME_ICON_COLUMN - 1));
790
791 label = gtk_label_new(_("Message parts"));
792 gtk_notebook_append_page(GTK_NOTEBOOK(bm), scroll, label);
793 gtk_container_add(GTK_CONTAINER(scroll), bm->treeview);
794 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(bm), FALSE);
795
796 bm->current_part = NULL;
797 bm->message = NULL;
798 bm->info_count = 0;
799 bm->save_all_list = NULL;
800 bm->save_all_popup = NULL;
801
802 bm->wrap_text = balsa_app.browse_wrap;
803 bm->shown_headers = balsa_app.shown_headers;
804 bm->close_with_msg = FALSE;
805
806 gtk_widget_show_all(GTK_WIDGET(bm));
807
808 /* Find-in-message toolbar that is hidden by default. */
809 bm->find_bar = bm_find_bar_new(bm);
810 gtk_box_pack_start(GTK_BOX(vbox), bm->find_bar, FALSE, FALSE, 0);
811 }
812
813 static void
balsa_message_destroy(GObject * object)814 balsa_message_destroy(GObject * object)
815 {
816 BalsaMessage* bm = BALSA_MESSAGE(object);
817
818 if (bm->treeview) {
819 balsa_message_set(bm, NULL, 0);
820 gtk_widget_destroy(bm->treeview);
821 bm->treeview = NULL;
822 }
823
824 g_list_free(bm->save_all_list);
825 bm->save_all_list = NULL;
826
827 if (bm->save_all_popup) {
828 g_object_unref(bm->save_all_popup);
829 bm->save_all_popup = NULL;
830 }
831
832 if (bm->parts_popup) {
833 g_object_unref(bm->parts_popup);
834 bm->parts_popup = NULL;
835 }
836
837 #ifdef HAVE_HTML_WIDGET
838 if (bm->html_find_info) {
839 g_free(bm->html_find_info);
840 bm->html_find_info = NULL;
841 }
842 #endif /* HAVE_HTML_WIDGET */
843
844 if (G_OBJECT_CLASS(parent_class)->dispose)
845 (*G_OBJECT_CLASS(parent_class)->dispose) (object);
846 }
847
848 GtkWidget *
balsa_message_new(void)849 balsa_message_new(void)
850 {
851 BalsaMessage *bm;
852
853 bm = g_object_new(BALSA_TYPE_MESSAGE, NULL);
854
855 return GTK_WIDGET(bm);
856 }
857
858 void
balsa_message_set_close(BalsaMessage * bm,gboolean close_with_msg)859 balsa_message_set_close(BalsaMessage * bm, gboolean close_with_msg)
860 {
861 bm->close_with_msg = close_with_msg;
862 }
863
864 /* Returns a BalsaPartInfo with a reference (g_object_unref when done). */
865 static BalsaPartInfo *
tree_next_valid_part_info(GtkTreeModel * model,GtkTreeIter * iter)866 tree_next_valid_part_info(GtkTreeModel * model, GtkTreeIter * iter)
867 {
868 BalsaPartInfo *info = NULL;
869
870 do {
871 GtkTreeIter child;
872
873 /* check if there is a valid info */
874 gtk_tree_model_get(model, iter, PART_INFO_COLUMN, &info, -1);
875 if (info)
876 return info;
877
878 /* if there are children, check the childs */
879 if (gtk_tree_model_iter_children (model, &child, iter))
880 if ((info = tree_next_valid_part_info(model, &child)))
881 return info;
882
883 /* switch to the next iter on the same level */
884 if (!gtk_tree_model_iter_next(model, iter))
885 return NULL;
886 } while (1);
887 /* never reached */
888 return NULL;
889 }
890
891 static void
tree_activate_row_cb(GtkTreeView * treeview,GtkTreePath * arg1,GtkTreeViewColumn * arg2,gpointer user_data)892 tree_activate_row_cb(GtkTreeView *treeview, GtkTreePath *arg1,
893 GtkTreeViewColumn *arg2, gpointer user_data)
894 {
895 BalsaMessage * bm = (BalsaMessage *)user_data;
896 GtkTreeModel * model = gtk_tree_view_get_model(treeview);
897 GtkTreeIter sel_iter;
898 BalsaPartInfo *info = NULL;
899
900 g_return_if_fail(bm);
901
902 /* get the info of the activated part */
903 if (!gtk_tree_model_get_iter(model, &sel_iter, arg1))
904 return;
905 gtk_tree_model_get(model, &sel_iter, PART_INFO_COLUMN, &info, -1);
906
907 /* if it's not displayable (== no info), get the next one... */
908 if (!info) {
909 info = tree_next_valid_part_info(model, &sel_iter);
910
911 if (!info) {
912 gtk_tree_model_get_iter_first(model, &sel_iter);
913 gtk_tree_model_get(model, &sel_iter, PART_INFO_COLUMN, &info, -1);
914 if (!info)
915 info = tree_next_valid_part_info(model, &sel_iter);
916 }
917 }
918
919 gtk_notebook_set_current_page(GTK_NOTEBOOK(bm), 0);
920 select_part(bm, info);
921 if (info)
922 g_object_unref(info);
923 }
924
925 static void
collect_selected_info(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)926 collect_selected_info(GtkTreeModel * model, GtkTreePath * path,
927 GtkTreeIter * iter, gpointer data)
928 {
929 GList **info_list = (GList **)data;
930 BalsaPartInfo *info;
931
932 gtk_tree_model_get(model, iter, PART_INFO_COLUMN, &info, -1);
933 if (info) {
934 g_object_unref(info);
935 *info_list = g_list_append(*info_list, info);
936 }
937 }
938
939 static void
tree_mult_selection_popup(BalsaMessage * bm,GdkEventButton * event,GtkTreeSelection * selection)940 tree_mult_selection_popup(BalsaMessage * bm, GdkEventButton * event,
941 GtkTreeSelection * selection)
942 {
943 gint selected;
944
945 /* destroy left-over select list and popup... */
946 g_list_free(bm->save_all_list);
947 bm->save_all_list = NULL;
948 if (bm->save_all_popup) {
949 g_object_unref(bm->save_all_popup);
950 bm->save_all_popup = NULL;
951 }
952
953 /* collect all selected info blocks */
954 gtk_tree_selection_selected_foreach(selection,
955 collect_selected_info,
956 &bm->save_all_list);
957
958 /* For a single part, display it's popup, for multiple parts a "save all"
959 * popup. If nothing with an info block is selected, do nothing */
960 selected = g_list_length(bm->save_all_list);
961 if (selected == 1) {
962 BalsaPartInfo *info = BALSA_PART_INFO(bm->save_all_list->data);
963 if (info->popup_menu) {
964 if (event)
965 gtk_menu_popup(GTK_MENU(info->popup_menu), NULL, NULL, NULL,
966 NULL, event->button, event->time);
967 else
968 gtk_menu_popup(GTK_MENU(info->popup_menu), NULL, NULL, NULL,
969 NULL, 0, gtk_get_current_event_time());
970 }
971 g_list_free(bm->save_all_list);
972 bm->save_all_list = NULL;
973 } else if (selected > 1) {
974 GtkWidget *menu_item;
975
976 bm->save_all_popup = gtk_menu_new ();
977 g_object_ref_sink(bm->save_all_popup);
978 menu_item =
979 gtk_menu_item_new_with_label (_("Save selected as..."));
980 gtk_widget_show(menu_item);
981 g_signal_connect (G_OBJECT (menu_item), "activate",
982 G_CALLBACK (part_context_save_all_cb),
983 (gpointer) bm->save_all_list);
984 gtk_menu_shell_append (GTK_MENU_SHELL (bm->save_all_popup), menu_item);
985 menu_item =
986 gtk_menu_item_new_with_label (_("Save selected to folder..."));
987 gtk_widget_show(menu_item);
988 g_signal_connect (G_OBJECT (menu_item), "activate",
989 G_CALLBACK (part_context_dump_all_cb),
990 (gpointer) bm->save_all_list);
991 gtk_menu_shell_append (GTK_MENU_SHELL (bm->save_all_popup), menu_item);
992 if (event)
993 gtk_menu_popup(GTK_MENU(bm->save_all_popup), NULL, NULL, NULL,
994 NULL, event->button, event->time);
995 else
996 gtk_menu_popup(GTK_MENU(bm->save_all_popup), NULL, NULL, NULL,
997 NULL, 0, gtk_get_current_event_time());
998 }
999 }
1000
1001 static gboolean
tree_menu_popup_key_cb(GtkWidget * widget,gpointer user_data)1002 tree_menu_popup_key_cb(GtkWidget *widget, gpointer user_data)
1003 {
1004 BalsaMessage * bm = (BalsaMessage *)user_data;
1005
1006 g_return_val_if_fail(bm, FALSE);
1007 tree_mult_selection_popup(bm, NULL,
1008 gtk_tree_view_get_selection(GTK_TREE_VIEW(widget)));
1009 return TRUE;
1010 }
1011
1012 static gboolean
tree_button_press_cb(GtkWidget * widget,GdkEventButton * event,gpointer data)1013 tree_button_press_cb(GtkWidget * widget, GdkEventButton * event,
1014 gpointer data)
1015 {
1016 BalsaMessage * bm = (BalsaMessage *)data;
1017 GtkTreeView *tree_view = GTK_TREE_VIEW(widget);
1018 GtkTreePath *path;
1019
1020 g_return_val_if_fail(bm, FALSE);
1021 g_return_val_if_fail(event, FALSE);
1022 if (event->type != GDK_BUTTON_PRESS || event->button != 3
1023 || event->window != gtk_tree_view_get_bin_window(tree_view))
1024 return FALSE;
1025
1026 /* If the part which received the click is already selected, don't change
1027 * the selection and check if more than on part is selected. Pop up the
1028 * "save all" menu in this case and the "normal" popup otherwise.
1029 * If the receiving part is not selected, select (only) this part and pop
1030 * up its menu.
1031 */
1032 if (gtk_tree_view_get_path_at_pos(tree_view, event->x, event->y,
1033 &path, NULL, NULL, NULL)) {
1034 GtkTreeIter iter;
1035 GtkTreeSelection * selection =
1036 gtk_tree_view_get_selection(tree_view);
1037 GtkTreeModel * model = gtk_tree_view_get_model(tree_view);
1038
1039 if (!gtk_tree_selection_path_is_selected(selection, path)) {
1040 BalsaPartInfo *info = NULL;
1041
1042 gtk_tree_selection_unselect_all(selection);
1043 gtk_tree_selection_select_path(selection, path);
1044 gtk_tree_view_set_cursor(GTK_TREE_VIEW(tree_view), path, NULL,
1045 FALSE);
1046 if (gtk_tree_model_get_iter (model, &iter, path)) {
1047 gtk_tree_model_get(model, &iter, PART_INFO_COLUMN, &info, -1);
1048 if (info) {
1049 if (info->popup_menu)
1050 gtk_menu_popup(GTK_MENU(info->popup_menu), NULL, NULL,
1051 NULL, NULL, event->button, event->time);
1052 g_object_unref(info);
1053 }
1054 }
1055 } else
1056 tree_mult_selection_popup(bm, event, selection);
1057 gtk_tree_path_free(path);
1058 }
1059
1060 return TRUE;
1061 }
1062
1063 /* balsa_message_set:
1064 returns TRUE on success, FALSE on failure (message content could not be
1065 accessed).
1066
1067 if msgno == 0, clears the display and returns TRUE
1068 */
1069
1070 /* Helpers:
1071 */
1072
1073 gchar *
balsa_message_sender_to_gchar(InternetAddressList * list,gint which)1074 balsa_message_sender_to_gchar(InternetAddressList * list, gint which)
1075 {
1076 InternetAddress *ia;
1077
1078 if (!list)
1079 return g_strdup(_("(No sender)"));
1080 if (which < 0)
1081 return internet_address_list_to_string(list, FALSE);
1082 ia = internet_address_list_get_address (list, which);
1083 return internet_address_to_string(ia, FALSE);
1084 }
1085
1086 static void
balsa_message_clear_tree(BalsaMessage * bm)1087 balsa_message_clear_tree(BalsaMessage * bm)
1088 {
1089 GtkTreeModel *model;
1090
1091 g_return_if_fail(bm != NULL);
1092
1093 model = gtk_tree_view_get_model(GTK_TREE_VIEW(bm->treeview));
1094 gtk_tree_store_clear(GTK_TREE_STORE(model));
1095 bm->info_count = 0;
1096 }
1097
1098 gboolean
balsa_message_set(BalsaMessage * bm,LibBalsaMailbox * mailbox,guint msgno)1099 balsa_message_set(BalsaMessage * bm, LibBalsaMailbox * mailbox, guint msgno)
1100 {
1101 gboolean is_new;
1102 GtkTreeIter iter;
1103 BalsaPartInfo *info;
1104 gboolean has_focus = bm->focus_state != BALSA_MESSAGE_FOCUS_STATE_NO;
1105 LibBalsaMessage *message;
1106
1107 g_return_val_if_fail(bm != NULL, FALSE);
1108
1109 bm_disable_find_entry(bm);
1110 balsa_message_clear_tree(bm);
1111 select_part(bm, NULL);
1112 if (bm->message != NULL) {
1113 libbalsa_message_body_unref(bm->message);
1114 g_object_unref(bm->message);
1115 bm->message = NULL;
1116 }
1117
1118 if (mailbox == NULL || msgno == 0) {
1119 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(bm), FALSE);
1120 gtk_notebook_set_current_page(GTK_NOTEBOOK(bm), 0);
1121 return TRUE;
1122 }
1123
1124 bm->message = message = libbalsa_mailbox_get_message(mailbox, msgno);
1125 /* We must not use msgno from now on: an asynchronous expunge may
1126 arrive (in particular between the body_ref() and set_flags()
1127 actions) and change the message numbering. Asynchronous
1128 expunges will update the LibBalsaMailbox::message data but no
1129 message numbers stored in random integer variables. */
1130 if (!message) {
1131 balsa_information(LIBBALSA_INFORMATION_WARNING,
1132 _("Could not access message %u "
1133 "in mailbox \"%s\"."),
1134 msgno, mailbox->name);
1135 return FALSE;
1136 }
1137
1138 is_new = LIBBALSA_MESSAGE_IS_UNREAD(message);
1139 if(!libbalsa_message_body_ref(message, TRUE, TRUE)) {
1140 libbalsa_mailbox_check(mailbox);
1141 g_object_unref(bm->message);
1142 bm->message = NULL;
1143 balsa_information(LIBBALSA_INFORMATION_WARNING,
1144 _("Could not access message %u "
1145 "in mailbox \"%s\"."),
1146 (unsigned int) message->msgno, mailbox->name);
1147 return FALSE;
1148 }
1149
1150 #ifdef HAVE_GPGME
1151 balsa_message_perform_crypto(message,
1152 libbalsa_mailbox_get_crypto_mode(mailbox),
1153 FALSE, 1);
1154 /* calculate the signature summary state if not set earlier */
1155 if(message->prot_state == LIBBALSA_MSG_PROTECT_NONE) {
1156 LibBalsaMsgProtectState prot_state =
1157 balsa_message_scan_signatures(message->body_list, message);
1158 /* update the icon if necessary */
1159 if (message->prot_state != prot_state)
1160 message->prot_state = prot_state;
1161 }
1162 #endif
1163
1164 /* may update the icon */
1165 libbalsa_mailbox_msgno_update_attach(mailbox, message->msgno, message);
1166
1167 display_headers(bm);
1168 display_content(bm);
1169 gtk_widget_show(GTK_WIDGET(bm));
1170
1171 #if defined(ENABLE_TOUCH_UI)
1172 /* hide tabs so that they do not confuse keyboard navigation.
1173 * This could probably be a configuration option. */
1174 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(bm), FALSE);
1175 #else
1176 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(bm), bm->info_count > 1);
1177 #endif /* ENABLE_TOUCH_UI */
1178 gtk_notebook_set_current_page(GTK_NOTEBOOK(bm), 0);
1179
1180 /*
1181 * At this point we check if (a) a message was new (its not new
1182 * any longer) and (b) a Disposition-Notification-To header line is
1183 * present.
1184 *
1185 */
1186 if (is_new && message->headers->dispnotify_to)
1187 handle_mdn_request (balsa_get_parent_window(GTK_WIDGET(bm)), message);
1188
1189 if (!gtk_tree_model_get_iter_first (gtk_tree_view_get_model(GTK_TREE_VIEW(bm->treeview)),
1190 &iter))
1191 /* Not possible? */
1192 return TRUE;
1193
1194 info =
1195 tree_next_valid_part_info(gtk_tree_view_get_model(GTK_TREE_VIEW(bm->treeview)),
1196 &iter);
1197 select_part(bm, info);
1198 if (info)
1199 g_object_unref(info);
1200 /*
1201 * emit read message
1202 */
1203 if (is_new && !mailbox->readonly)
1204 libbalsa_mailbox_msgno_change_flags(mailbox, message->msgno, 0,
1205 LIBBALSA_MESSAGE_FLAG_NEW);
1206
1207 /* restore keyboard focus to the content, if it was there before */
1208 if (has_focus)
1209 balsa_message_grab_focus(bm);
1210
1211 return TRUE;
1212 }
1213
1214 void
balsa_message_save_current_part(BalsaMessage * bm)1215 balsa_message_save_current_part(BalsaMessage * bm)
1216 {
1217 g_return_if_fail(bm != NULL);
1218
1219 if (bm->current_part)
1220 balsa_mime_widget_ctx_menu_save(GTK_WIDGET(bm), bm->current_part->body);
1221 }
1222
1223 static gboolean
balsa_message_set_embedded_hdr(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)1224 balsa_message_set_embedded_hdr(GtkTreeModel * model, GtkTreePath * path,
1225 GtkTreeIter *iter, gpointer data)
1226 {
1227 BalsaPartInfo *info = NULL;
1228 BalsaMessage * bm = BALSA_MESSAGE(data);
1229
1230 gtk_tree_model_get(model, iter, PART_INFO_COLUMN, &info, -1);
1231 if (info) {
1232 if (info->body && info->body->embhdrs && info->mime_widget)
1233 balsa_mime_widget_message_set_headers_d(bm, info->mime_widget,
1234 info->body->embhdrs,
1235 info->body->parts,
1236 info->body->embhdrs->subject);
1237 g_object_unref(G_OBJECT(info));
1238 }
1239
1240 return FALSE;
1241 }
1242
1243 void
balsa_message_set_displayed_headers(BalsaMessage * bmessage,ShownHeaders sh)1244 balsa_message_set_displayed_headers(BalsaMessage * bmessage,
1245 ShownHeaders sh)
1246 {
1247 g_return_if_fail(bmessage != NULL);
1248 g_return_if_fail(sh >= HEADERS_NONE && sh <= HEADERS_ALL);
1249
1250 if (bmessage->shown_headers == sh)
1251 return;
1252
1253 bmessage->shown_headers = sh;
1254
1255 if (bmessage->message) {
1256 if(sh == HEADERS_ALL)
1257 libbalsa_mailbox_set_msg_headers(bmessage->message->mailbox,
1258 bmessage->message);
1259 display_headers(bmessage);
1260 gtk_tree_model_foreach
1261 (gtk_tree_view_get_model(GTK_TREE_VIEW(bmessage->treeview)),
1262 balsa_message_set_embedded_hdr, bmessage);
1263 if (bm_header_widget_att_button(bmessage)) {
1264 if (bmessage->info_count > 1)
1265 gtk_widget_show_all
1266 (GTK_WIDGET(bm_header_widget_att_button(bmessage)));
1267 else
1268 gtk_widget_hide
1269 (GTK_WIDGET(bm_header_widget_att_button(bmessage)));
1270 }
1271 }
1272 }
1273
1274 void
balsa_message_set_wrap(BalsaMessage * bm,gboolean wrap)1275 balsa_message_set_wrap(BalsaMessage * bm, gboolean wrap)
1276 {
1277 g_return_if_fail(bm != NULL);
1278
1279 bm->wrap_text = wrap;
1280
1281 /* This is easier than reformating all the widgets... */
1282 if (bm->message) {
1283 LibBalsaMessage *msg = bm->message;
1284 balsa_message_set(bm, msg->mailbox, msg->msgno);
1285 }
1286 }
1287
1288
1289 static void
display_headers(BalsaMessage * bm)1290 display_headers(BalsaMessage * bm)
1291 {
1292 balsa_mime_widget_message_set_headers_d(bm, bm->bm_widget,
1293 bm->message->headers,
1294 bm->message->body_list,
1295 LIBBALSA_MESSAGE_GET_SUBJECT(bm->message));
1296 }
1297
1298
1299 static void
part_info_init(BalsaMessage * bm,BalsaPartInfo * info)1300 part_info_init(BalsaMessage * bm, BalsaPartInfo * info)
1301 {
1302 g_return_if_fail(bm != NULL);
1303 g_return_if_fail(info != NULL);
1304 g_return_if_fail(info->body != NULL);
1305
1306 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(bm->scroll),
1307 GTK_POLICY_AUTOMATIC,
1308 GTK_POLICY_AUTOMATIC);
1309
1310 info->mime_widget = balsa_mime_widget_new(bm, info->body, info->popup_menu);
1311 }
1312
1313
1314 static inline gchar *
mpart_content_name(const gchar * content_type)1315 mpart_content_name(const gchar *content_type)
1316 {
1317 if (g_ascii_strcasecmp(content_type, "multipart/mixed") == 0)
1318 return g_strdup(_("mixed parts"));
1319 else if (g_ascii_strcasecmp(content_type, "multipart/alternative") == 0)
1320 return g_strdup(_("alternative parts"));
1321 else if (g_ascii_strcasecmp(content_type, "multipart/signed") == 0)
1322 return g_strdup(_("signed parts"));
1323 else if (g_ascii_strcasecmp(content_type, "multipart/encrypted") == 0)
1324 return g_strdup(_("encrypted parts"));
1325 else if (g_ascii_strcasecmp(content_type, "message/rfc822") == 0)
1326 return g_strdup(_("rfc822 message"));
1327 else
1328 return g_strdup_printf(_("\"%s\" parts"),
1329 strchr(content_type, '/') + 1);
1330 }
1331
1332 static void
atattchments_menu_cb(GtkWidget * widget,BalsaPartInfo * info)1333 atattchments_menu_cb(GtkWidget * widget, BalsaPartInfo *info)
1334 {
1335 BalsaMessage * bm = g_object_get_data(G_OBJECT(widget), "balsa-message");
1336
1337 g_return_if_fail(bm);
1338 g_return_if_fail(info);
1339
1340 gtk_notebook_set_current_page(GTK_NOTEBOOK(bm), 0);
1341 select_part(bm, info);
1342 }
1343
1344 static void
add_to_attachments_popup(GtkMenuShell * menu,const gchar * item,BalsaMessage * bm,BalsaPartInfo * info)1345 add_to_attachments_popup(GtkMenuShell * menu, const gchar * item,
1346 BalsaMessage * bm, BalsaPartInfo *info)
1347 {
1348 GtkWidget * menuitem = gtk_menu_item_new_with_label (item);
1349
1350 g_object_set_data(G_OBJECT(menuitem), "balsa-message", bm);
1351 g_signal_connect(G_OBJECT (menuitem), "activate",
1352 G_CALLBACK (atattchments_menu_cb),
1353 (gpointer) info);
1354 gtk_menu_shell_append(menu, menuitem);
1355 }
1356
1357 static void
toggle_all_inline_cb(GtkCheckMenuItem * item,BalsaPartInfo * info)1358 toggle_all_inline_cb(GtkCheckMenuItem * item, BalsaPartInfo *info)
1359 {
1360 BalsaMessage * bm = g_object_get_data(G_OBJECT(item), "balsa-message");
1361
1362 g_return_if_fail(bm);
1363 g_return_if_fail(info);
1364
1365 bm->force_inline = gtk_check_menu_item_get_active(item);
1366
1367 gtk_notebook_set_current_page(GTK_NOTEBOOK(bm), 0);
1368 select_part(bm, info);
1369 }
1370
1371 static void
add_toggle_inline_menu_item(GtkMenuShell * menu,BalsaMessage * bm,BalsaPartInfo * info)1372 add_toggle_inline_menu_item(GtkMenuShell * menu, BalsaMessage * bm,
1373 BalsaPartInfo *info)
1374 {
1375 GtkWidget * menuitem =
1376 gtk_check_menu_item_new_with_label (_("force inline for all parts"));
1377
1378 g_object_set_data(G_OBJECT(menuitem), "balsa-message", bm);
1379 g_signal_connect(G_OBJECT (menuitem), "activate",
1380 G_CALLBACK (toggle_all_inline_cb),
1381 (gpointer) info);
1382 gtk_menu_shell_append(menu, menuitem);
1383
1384 /* Clear force-inline to be consistent with initial FALSE state of
1385 * check-menu-item. */
1386 bm->force_inline = FALSE;
1387 }
1388
1389 static void
display_part(BalsaMessage * bm,LibBalsaMessageBody * body,GtkTreeModel * model,GtkTreeIter * iter,gchar * part_id)1390 display_part(BalsaMessage * bm, LibBalsaMessageBody * body,
1391 GtkTreeModel * model, GtkTreeIter * iter, gchar * part_id)
1392 {
1393 BalsaPartInfo *info = NULL;
1394 gchar *content_type = libbalsa_message_body_get_mime_type(body);
1395 gchar *icon_title = NULL;
1396 gboolean is_multipart=libbalsa_message_body_is_multipart(body);
1397 GdkPixbuf *content_icon;
1398 gchar *content_desc;
1399
1400 content_desc = libbalsa_vfs_content_description(content_type);
1401
1402 if(!is_multipart ||
1403 g_ascii_strcasecmp(content_type, "message/rfc822")==0 ||
1404 g_ascii_strcasecmp(content_type, "multipart/signed")==0 ||
1405 g_ascii_strcasecmp(content_type, "multipart/encrypted")==0 ||
1406 g_ascii_strcasecmp(content_type, "multipart/mixed")==0 ||
1407 g_ascii_strcasecmp(content_type, "multipart/alternative")==0) {
1408
1409 info = balsa_part_info_new(body);
1410 bm->info_count++;
1411
1412 if (g_ascii_strcasecmp(content_type, "message/rfc822") == 0 &&
1413 body->embhdrs) {
1414 gchar *from = balsa_message_sender_to_gchar(body->embhdrs->from, 0);
1415 gchar *subj = g_strdup(body->embhdrs->subject);
1416 libbalsa_utf8_sanitize(&from, balsa_app.convert_unknown_8bit, NULL);
1417 libbalsa_utf8_sanitize(&subj, balsa_app.convert_unknown_8bit, NULL);
1418 icon_title =
1419 g_strdup_printf(_("rfc822 message (from %s, subject \"%s\")"),
1420 from, subj);
1421 g_free(from);
1422 g_free(subj);
1423 } else if (is_multipart) {
1424 icon_title = mpart_content_name(content_type);
1425 if (!strcmp(part_id, "1")) {
1426 add_toggle_inline_menu_item(GTK_MENU_SHELL(bm->parts_popup),
1427 bm, info);
1428 gtk_menu_shell_append(GTK_MENU_SHELL(bm->parts_popup),
1429 gtk_separator_menu_item_new ());
1430 add_to_attachments_popup(GTK_MENU_SHELL(bm->parts_popup),
1431 _("complete message"),
1432 bm, info);
1433 gtk_menu_shell_append(GTK_MENU_SHELL(bm->parts_popup),
1434 gtk_separator_menu_item_new ());
1435 }
1436 } else if (body->filename) {
1437 gchar * filename = g_strdup(body->filename);
1438 gchar * menu_label;
1439
1440 libbalsa_utf8_sanitize(&filename, balsa_app.convert_unknown_8bit,
1441 NULL);
1442 icon_title =
1443 g_strdup_printf("%s (%s)", filename, content_desc);
1444
1445 /* this should neither be a message nor multipart, so add it to the
1446 attachments popup */
1447 menu_label =
1448 g_strdup_printf(_("part %s: %s (file %s)"), part_id,
1449 content_desc, filename);
1450 add_to_attachments_popup(GTK_MENU_SHELL(bm->parts_popup),
1451 menu_label, bm, info);
1452 g_free(menu_label);
1453 g_free(filename);
1454 } else {
1455 gchar * menu_label;
1456
1457 icon_title = g_strdup_printf("%s", content_desc);
1458 menu_label =
1459 g_strdup_printf(_("part %s: %s"), part_id, content_desc);
1460 add_to_attachments_popup(GTK_MENU_SHELL(bm->parts_popup),
1461 menu_label, bm, info);
1462 g_free(menu_label);
1463 }
1464
1465 part_create_menu (info);
1466 info->path = gtk_tree_model_get_path(model, iter);
1467
1468 /* add to the tree view */
1469 #ifdef HAVE_GPGME
1470 content_icon =
1471 get_crypto_content_icon(body, content_type, &icon_title);
1472 if (info->body->was_encrypted) {
1473 gchar * new_title =
1474 g_strconcat(_("encrypted: "), icon_title, NULL);
1475 g_free(icon_title);
1476 icon_title = new_title;
1477 }
1478 #else
1479 content_icon = NULL;
1480 #endif
1481 if (!content_icon)
1482 content_icon =
1483 libbalsa_icon_finder(GTK_WIDGET(bm),
1484 content_type, NULL, NULL,
1485 GTK_ICON_SIZE_LARGE_TOOLBAR);
1486 gtk_tree_store_set (GTK_TREE_STORE(model), iter,
1487 PART_INFO_COLUMN, info,
1488 PART_NUM_COLUMN, part_id,
1489 MIME_ICON_COLUMN, content_icon,
1490 MIME_TYPE_COLUMN, icon_title, -1);
1491
1492 g_object_unref(info);
1493 g_free(icon_title);
1494 } else {
1495 content_icon =
1496 libbalsa_icon_finder(GTK_WIDGET(bm),
1497 content_type, NULL, NULL,
1498 GTK_ICON_SIZE_LARGE_TOOLBAR);
1499 gtk_tree_store_set (GTK_TREE_STORE(model), iter,
1500 PART_INFO_COLUMN, NULL,
1501 PART_NUM_COLUMN, part_id,
1502 MIME_ICON_COLUMN, content_icon,
1503 MIME_TYPE_COLUMN, content_desc, -1);
1504 }
1505
1506 if (content_icon)
1507 g_object_unref(G_OBJECT(content_icon));
1508 g_free(content_desc);
1509 g_free(content_type);
1510 }
1511
1512 static void
display_parts(BalsaMessage * bm,LibBalsaMessageBody * body,GtkTreeIter * parent,gchar * prefix)1513 display_parts(BalsaMessage * bm, LibBalsaMessageBody * body,
1514 GtkTreeIter * parent, gchar * prefix)
1515 {
1516 GtkTreeModel *model =
1517 gtk_tree_view_get_model(GTK_TREE_VIEW(bm->treeview));
1518 GtkTreeIter iter;
1519 gint part_in_level = 1;
1520
1521 while (body) {
1522 gchar * part_id;
1523
1524 if (prefix)
1525 part_id = g_strdup_printf("%s.%d", prefix, part_in_level);
1526 else
1527 part_id = g_strdup_printf("%d", part_in_level);
1528 gtk_tree_store_append(GTK_TREE_STORE(model), &iter, parent);
1529 display_part(bm, body, model, &iter, part_id);
1530 display_parts(bm, body->parts, &iter, part_id);
1531 body = body->next;
1532 part_in_level++;
1533 g_free(part_id);
1534 }
1535 }
1536
1537 /* Display the image in a "Face:" header, if any. */
1538 static void
display_face(BalsaMessage * bm)1539 display_face(BalsaMessage * bm)
1540 {
1541 GtkWidget *face_box;
1542 const gchar *face, *x_face = NULL;
1543 GError *err = NULL;
1544 GtkWidget *image;
1545
1546 face_box = bm->face_box;
1547 gtk_widget_hide(face_box);
1548 gtk_container_foreach(GTK_CONTAINER(face_box),
1549 (GtkCallback) gtk_widget_destroy, NULL);
1550
1551 if (!bm->message
1552 || !((face = libbalsa_message_get_user_header(bm->message, "Face"))
1553 || (x_face =
1554 libbalsa_message_get_user_header(bm->message,
1555 "X-Face")))) {
1556 return;
1557 }
1558
1559 if (face)
1560 image = libbalsa_get_image_from_face_header(face, &err);
1561 else {
1562 #if HAVE_COMPFACE
1563 image = libbalsa_get_image_from_x_face_header(x_face, &err);
1564 #else /* HAVE_COMPFACE */
1565 return;
1566 #endif /* HAVE_COMPFACE */
1567 }
1568 if (err) {
1569 balsa_information(LIBBALSA_INFORMATION_WARNING,
1570 /* Translators: please do not translate Face. */
1571 _("Error loading Face: %s"), err->message);
1572 g_error_free(err);
1573 return;
1574 }
1575
1576 gtk_box_pack_start(GTK_BOX(face_box), image, FALSE, FALSE, 0);
1577 gtk_widget_show_all(face_box);
1578 }
1579
1580 static void
display_content(BalsaMessage * bm)1581 display_content(BalsaMessage * bm)
1582 {
1583 balsa_message_clear_tree(bm);
1584 if (bm->parts_popup)
1585 g_object_unref(bm->parts_popup);
1586 bm->parts_popup = gtk_menu_new();
1587 g_object_ref_sink(bm->parts_popup);
1588 display_parts(bm, bm->message->body_list, NULL, NULL);
1589 if (bm->info_count > 1) {
1590 gtk_widget_show_all(bm->parts_popup);
1591 gtk_widget_show_all
1592 (GTK_WIDGET(bm_header_widget_att_button(bm)));
1593 } else
1594 gtk_widget_hide
1595 (GTK_WIDGET(bm_header_widget_att_button(bm)));
1596 display_face(bm);
1597 gtk_tree_view_columns_autosize(GTK_TREE_VIEW(bm->treeview));
1598 gtk_tree_view_expand_all(GTK_TREE_VIEW(bm->treeview));
1599 }
1600
1601 void
balsa_message_copy_part(const gchar * url,LibBalsaMessageBody * part)1602 balsa_message_copy_part(const gchar *url, LibBalsaMessageBody *part)
1603 {
1604 GError *err = NULL;
1605 LibBalsaMailbox *mailbox = balsa_find_mailbox_by_url(url);
1606 GMimeStream *stream;
1607
1608 g_return_if_fail(mailbox != NULL);
1609
1610 stream = libbalsa_message_body_get_stream(part, &err);
1611
1612 if (!stream) {
1613 libbalsa_information(LIBBALSA_INFORMATION_ERROR,
1614 _("Reading embedded message failed: %s"),
1615 err ? err->message : "?");
1616 g_clear_error(&err);
1617 return;
1618 }
1619
1620 if (!libbalsa_mailbox_add_message(mailbox, stream, 0, &err)) {
1621 libbalsa_information(LIBBALSA_INFORMATION_ERROR,
1622 _("Appending message to %s failed: %s"),
1623 mailbox->name,
1624 err ? err->message : "?");
1625 g_clear_error(&err);
1626 }
1627 g_object_unref(stream);
1628 }
1629
1630 static void
part_create_menu(BalsaPartInfo * info)1631 part_create_menu (BalsaPartInfo* info)
1632 /* Remarks: Will add items in the following order:
1633 1) Default application according to GnomeVFS.
1634 2) GNOME MIME/GnomeVFS key values that don't match default
1635 application or anything on the shortlist.
1636 3) GnomeVFS shortlist applications, with the default one (sometimes
1637 included on shortlist, sometimes not) excluded. */
1638 {
1639 GtkWidget* menu_item;
1640 gchar* content_type;
1641
1642 info->popup_menu = gtk_menu_new ();
1643 g_object_ref_sink(info->popup_menu);
1644
1645 content_type = libbalsa_message_body_get_mime_type (info->body);
1646 libbalsa_vfs_fill_menu_by_content_type(GTK_MENU(info->popup_menu),
1647 content_type,
1648 G_CALLBACK (balsa_mime_widget_ctx_menu_cb),
1649 (gpointer)info->body);
1650
1651 menu_item = gtk_menu_item_new_with_mnemonic (_("_Save..."));
1652 g_signal_connect (G_OBJECT (menu_item), "activate",
1653 G_CALLBACK (balsa_mime_widget_ctx_menu_save), (gpointer) info->body);
1654 gtk_menu_shell_append (GTK_MENU_SHELL (info->popup_menu), menu_item);
1655
1656 if (g_ascii_strcasecmp(content_type, "message/rfc822") == 0) {
1657 GtkWidget *submenu;
1658
1659 menu_item =
1660 gtk_menu_item_new_with_mnemonic(_("_Copy to folder..."));
1661 gtk_menu_shell_append(GTK_MENU_SHELL(info->popup_menu), menu_item);
1662
1663 submenu =
1664 balsa_mblist_mru_menu(GTK_WINDOW(gtk_widget_get_toplevel(info->popup_menu)),
1665 &balsa_app.folder_mru,
1666 G_CALLBACK(balsa_message_copy_part),
1667 info->body);
1668 gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_item), submenu);
1669 }
1670
1671 gtk_widget_show_all (info->popup_menu);
1672 g_free (content_type);
1673 }
1674
1675
1676 static void
balsa_part_info_init(GObject * object,gpointer data)1677 balsa_part_info_init(GObject *object, gpointer data)
1678 {
1679 BalsaPartInfo * info = BALSA_PART_INFO(object);
1680
1681 info->body = NULL;
1682 info->mime_widget = NULL;
1683 info->popup_menu = NULL;
1684 info->path = NULL;
1685 }
1686
1687 static BalsaPartInfo*
balsa_part_info_new(LibBalsaMessageBody * body)1688 balsa_part_info_new(LibBalsaMessageBody* body)
1689 {
1690 BalsaPartInfo * info = g_object_new(TYPE_BALSA_PART_INFO, NULL);
1691 info->body = body;
1692 return info;
1693 }
1694
1695 static void
balsa_part_info_free(GObject * object)1696 balsa_part_info_free(GObject * object)
1697 {
1698 BalsaPartInfo * info;
1699 GObjectClass *parent_class;
1700
1701 g_return_if_fail(object != NULL);
1702 g_return_if_fail(IS_BALSA_PART_INFO(object));
1703 info = BALSA_PART_INFO(object);
1704
1705 if (info->mime_widget) {
1706 g_object_unref(G_OBJECT(info->mime_widget));
1707 info->mime_widget = NULL;
1708 }
1709 if (info->popup_menu)
1710 g_object_unref(info->popup_menu);
1711
1712 gtk_tree_path_free(info->path);
1713
1714 parent_class = g_type_class_peek_parent(G_OBJECT_GET_CLASS(object));
1715 parent_class->finalize(object);
1716 }
1717
1718 static void
part_context_save_all_cb(GtkWidget * menu_item,GList * info_list)1719 part_context_save_all_cb(GtkWidget * menu_item, GList * info_list)
1720 {
1721 while (info_list) {
1722 balsa_mime_widget_ctx_menu_save(menu_item,
1723 BALSA_PART_INFO(info_list->data)->body);
1724 info_list = g_list_next(info_list);
1725 }
1726 }
1727
1728 /*
1729 * Let the user select a folder and save all message parts form info_list in
1730 * this folder, either with their name (if defined) or as localised "content-
1731 * type message part". The function protects files from being overwritten by
1732 * appending "(1)", "(2)", ... to the name to make it unique.
1733 * Sets balsa_app::save_dir to the selected folder.
1734 */
1735 static void
part_context_dump_all_cb(GtkWidget * menu_item,GList * info_list)1736 part_context_dump_all_cb(GtkWidget * menu_item, GList * info_list)
1737 {
1738 GtkWidget *dump_dialog;
1739
1740 g_return_if_fail(info_list);
1741
1742 dump_dialog =
1743 gtk_file_chooser_dialog_new(_("Select folder for saving selected parts"),
1744 balsa_get_parent_window(menu_item),
1745 GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
1746 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
1747 GTK_STOCK_OK, GTK_RESPONSE_OK, NULL);
1748 #if HAVE_MACOSX_DESKTOP
1749 libbalsa_macosx_menu_for_parent(dump_dialog, balsa_get_parent_window(menu_item));
1750 #endif
1751 gtk_dialog_set_default_response(GTK_DIALOG(dump_dialog),
1752 GTK_RESPONSE_CANCEL);
1753 gtk_file_chooser_set_local_only(GTK_FILE_CHOOSER(dump_dialog),
1754 libbalsa_vfs_local_only());
1755 if (balsa_app.save_dir)
1756 gtk_file_chooser_set_current_folder_uri(GTK_FILE_CHOOSER(dump_dialog),
1757 balsa_app.save_dir);
1758
1759 if (gtk_dialog_run(GTK_DIALOG(dump_dialog)) == GTK_RESPONSE_OK) {
1760 gchar *dir_name =
1761 gtk_file_chooser_get_uri(GTK_FILE_CHOOSER(dump_dialog));
1762 LibbalsaVfs * dir_uri;
1763
1764 g_message("store to URI: %s", dir_name);
1765 if (!(dir_uri = libbalsa_vfs_new_from_uri(dir_name)))
1766 balsa_information(LIBBALSA_INFORMATION_ERROR,
1767 _("Could not create URI for %s"),
1768 dir_name);
1769
1770 /* remember the folder */
1771 g_free(balsa_app.save_dir);
1772 balsa_app.save_dir = dir_name;
1773
1774 /* save all parts without further user interaction */
1775 info_list = g_list_first(info_list);
1776 while (dir_uri && info_list) {
1777 BalsaPartInfo *info = BALSA_PART_INFO(info_list->data);
1778 LibbalsaVfs * save_uri;
1779 gboolean result;
1780 GError *err = NULL;
1781
1782 if (info->body->filename)
1783 save_uri =
1784 libbalsa_vfs_dir_append(dir_uri, info->body->filename);
1785 else {
1786 gchar *cont_type =
1787 libbalsa_message_body_get_mime_type(info->body);
1788 gchar *p;
1789
1790 /* be sure to have no '/' in the file name */
1791 g_strdelimit(cont_type, G_DIR_SEPARATOR_S, '-');
1792 p = g_strdup_printf(_("%s message part"), cont_type);
1793 g_free(cont_type);
1794 save_uri = libbalsa_vfs_dir_append(dir_uri, p);
1795 g_free(p);
1796 }
1797 g_message("store to file: %s", libbalsa_vfs_get_uri_utf8(save_uri));
1798
1799 /* don't overwrite existing files, append (1), (2), ... instead */
1800 if (libbalsa_vfs_file_exists(save_uri)) {
1801 gint n = 1;
1802 LibbalsaVfs * base_uri = save_uri;
1803
1804 save_uri = NULL;
1805 do {
1806 gchar * ext = g_strdup_printf(" (%d)", n++);
1807 if (save_uri)
1808 g_object_unref(save_uri);
1809 save_uri = libbalsa_vfs_append(base_uri, ext);
1810 g_free(ext);
1811 } while (libbalsa_vfs_file_exists(save_uri));
1812 g_object_unref(base_uri);
1813 }
1814 g_message("store to file: %s", libbalsa_vfs_get_uri_utf8(save_uri));
1815
1816 /* try to save the file */
1817 result =
1818 libbalsa_message_body_save_vfs(info->body, save_uri,
1819 LIBBALSA_MESSAGE_BODY_UNSAFE,
1820 info->body->body_type ==
1821 LIBBALSA_MESSAGE_BODY_TYPE_TEXT,
1822 &err);
1823 if (!result)
1824 balsa_information(LIBBALSA_INFORMATION_ERROR,
1825 _("Could not save %s: %s"),
1826 libbalsa_vfs_get_uri_utf8(save_uri),
1827 err && err->message ?
1828 err->message : "Unknown error");
1829 g_clear_error(&err);
1830 g_object_unref(save_uri);
1831 info_list = g_list_next(info_list);
1832 }
1833 g_object_unref(dir_uri);
1834 }
1835 gtk_widget_destroy(dump_dialog);
1836 }
1837
1838
1839 typedef struct _selFirst_T {
1840 GtkTreeIter sel_iter;
1841 gboolean found;
1842 } selFirst_T;
1843
1844 static void
tree_selection_get_first(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)1845 tree_selection_get_first(GtkTreeModel * model, GtkTreePath * path,
1846 GtkTreeIter * iter, gpointer data)
1847 {
1848 selFirst_T *sel = (selFirst_T *)data;
1849
1850 if (!sel->found) {
1851 sel->found = TRUE;
1852 memcpy (&sel->sel_iter, iter, sizeof(GtkTreeIter));
1853 }
1854 }
1855
1856 static BalsaPartInfo *
bm_next_part_info(BalsaMessage * bmessage)1857 bm_next_part_info(BalsaMessage * bmessage)
1858 {
1859 selFirst_T sel;
1860 GtkTreeView *gtv;
1861 GtkTreeModel *model;
1862
1863 g_return_val_if_fail(bmessage != NULL, NULL);
1864 g_return_val_if_fail(bmessage->treeview != NULL, NULL);
1865
1866 gtv = GTK_TREE_VIEW(bmessage->treeview);
1867 model = gtk_tree_view_get_model(gtv);
1868
1869 /* get the info of the first selected part */
1870 sel.found = FALSE;
1871 gtk_tree_selection_selected_foreach(gtk_tree_view_get_selection(gtv),
1872 tree_selection_get_first, &sel);
1873 if (!sel.found) {
1874 /* return the first part if nothing is selected */
1875 if (!gtk_tree_model_get_iter_first(model, &sel.sel_iter))
1876 return NULL;
1877 } else {
1878 GtkTreeIter iter;
1879
1880 /* If the first selected iter has a child, select it, otherwise
1881 * take next on the same or higher level. If there is no next,
1882 * return NULL */
1883 if (!gtk_tree_model_iter_children (model, &iter, &sel.sel_iter)) {
1884 GtkTreeIter tmp_iter;
1885
1886 tmp_iter = iter = sel.sel_iter;
1887 while (!gtk_tree_model_iter_next (model, &iter)) {
1888 if (!gtk_tree_model_iter_parent(model, &iter, &tmp_iter))
1889 return NULL;
1890 tmp_iter = iter;
1891 }
1892 }
1893 sel.sel_iter = iter;
1894 }
1895
1896 return tree_next_valid_part_info(model, &sel.sel_iter);
1897 }
1898
1899 void
balsa_message_next_part(BalsaMessage * bmessage)1900 balsa_message_next_part(BalsaMessage * bmessage)
1901 {
1902 BalsaPartInfo *info;
1903 GtkTreeView *gtv;
1904
1905 if (!(info = bm_next_part_info(bmessage)))
1906 return;
1907
1908 gtv = GTK_TREE_VIEW(bmessage->treeview);
1909 gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(gtv));
1910 select_part(bmessage, info);
1911 g_object_unref(info);
1912 }
1913
1914 gboolean
balsa_message_has_next_part(BalsaMessage * bmessage)1915 balsa_message_has_next_part(BalsaMessage * bmessage)
1916 {
1917 BalsaPartInfo *info;
1918
1919 if (bmessage && bmessage->treeview
1920 && (info = bm_next_part_info(bmessage))) {
1921 g_object_unref(info);
1922 return TRUE;
1923 }
1924
1925 return FALSE;
1926 }
1927
1928 static BalsaPartInfo *
bm_previous_part_info(BalsaMessage * bmessage)1929 bm_previous_part_info(BalsaMessage * bmessage)
1930 {
1931 selFirst_T sel;
1932 GtkTreeView *gtv;
1933 GtkTreeModel *model;
1934 BalsaPartInfo *info;
1935
1936 g_return_val_if_fail(bmessage != NULL, NULL);
1937 g_return_val_if_fail(bmessage->treeview != NULL, NULL);
1938
1939 gtv = GTK_TREE_VIEW(bmessage->treeview);
1940 model = gtk_tree_view_get_model(gtv);
1941
1942 /* get the info of the first selected part */
1943 sel.found = FALSE;
1944 gtk_tree_selection_selected_foreach(gtk_tree_view_get_selection(gtv),
1945 tree_selection_get_first, &sel);
1946 if (!sel.found) {
1947 /* return the first part if nothing is selected */
1948 if (!gtk_tree_model_get_iter_first(model, &sel.sel_iter))
1949 return NULL;
1950 gtk_tree_model_get(model, &sel.sel_iter, PART_INFO_COLUMN, &info, -1);
1951 } else {
1952 GtkTreePath * path = gtk_tree_model_get_path(model, &sel.sel_iter);
1953
1954 /* find the previous element with a valid info block */
1955 do {
1956 if (!gtk_tree_path_prev (path)) {
1957 if (gtk_tree_path_get_depth (path) <= 1) {
1958 gtk_tree_path_free(path);
1959 return NULL;
1960 }
1961 gtk_tree_path_up(path);
1962 }
1963 gtk_tree_model_get_iter(model, &sel.sel_iter, path);
1964 gtk_tree_model_get(model, &sel.sel_iter, PART_INFO_COLUMN,
1965 &info, -1);
1966 } while (!info);
1967 gtk_tree_path_free(path);
1968 }
1969
1970 return info;
1971 }
1972
1973 void
balsa_message_previous_part(BalsaMessage * bmessage)1974 balsa_message_previous_part(BalsaMessage * bmessage)
1975 {
1976 BalsaPartInfo *info;
1977 GtkTreeView *gtv;
1978
1979 if (!(info = bm_previous_part_info(bmessage)))
1980 return;
1981
1982 gtv = GTK_TREE_VIEW(bmessage->treeview);
1983 gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(gtv));
1984 select_part(bmessage, info);
1985 g_object_unref(info);
1986 }
1987
1988 gboolean
balsa_message_has_previous_part(BalsaMessage * bmessage)1989 balsa_message_has_previous_part(BalsaMessage * bmessage)
1990 {
1991 BalsaPartInfo *info;
1992
1993 if (bmessage && bmessage->treeview
1994 && (info = bm_previous_part_info(bmessage))) {
1995 g_object_unref(info);
1996 return TRUE;
1997 }
1998
1999 return FALSE;
2000 }
2001
2002 #ifdef HAVE_HTML_WIDGET
2003 static gboolean
libbalsa_can_display(LibBalsaMessageBody * part)2004 libbalsa_can_display(LibBalsaMessageBody *part)
2005 {
2006 gchar *content_type = libbalsa_message_body_get_mime_type(part);
2007 gboolean res = FALSE;
2008 if (!balsa_app.display_alt_plain &&
2009 libbalsa_html_type(content_type))
2010 res = TRUE;
2011 else if(g_ascii_strcasecmp(content_type, "multipart/related") == 0 &&
2012 part->parts)
2013 res = libbalsa_can_display(part->parts);
2014 g_free(content_type);
2015 return res;
2016 }
2017 #endif /* HAVE_HTML_WIDGET */
2018
2019 /** Determines whether given part can be displayed. We display plain
2020 text, parts html/rtf parts unless it has been disabled in the
2021 preferences. We make sure the process is correctly recurrsive, to
2022 display properly messages of following structure:
2023
2024 multiplart/alternative
2025 text/plain "A"
2026 multipart/related
2027 text/plain "B"
2028 image/jpeg "C"
2029
2030 In the case as above, B & C should be displayed.
2031 */
2032 static LibBalsaMessageBody*
preferred_part(LibBalsaMessageBody * parts)2033 preferred_part(LibBalsaMessageBody *parts)
2034 {
2035 LibBalsaMessageBody *body, *preferred = parts;
2036
2037 for (body = parts; body; body = body->next) {
2038 gchar *content_type;
2039
2040 content_type = libbalsa_message_body_get_mime_type(body);
2041
2042 if (g_ascii_strcasecmp(content_type, "text/plain") == 0 ||
2043 g_ascii_strcasecmp(content_type, "text/calendar") == 0)
2044 preferred = body;
2045 #ifdef HAVE_HTML_WIDGET
2046 else if (libbalsa_can_display(body))
2047 preferred = body;
2048 #endif /* HAVE_HTML_WIDGET */
2049
2050 g_free(content_type);
2051 }
2052
2053 return preferred;
2054 }
2055
2056 typedef struct _treeSearchT {
2057 const LibBalsaMessageBody *body;
2058 BalsaPartInfo *info;
2059 } treeSearchT;
2060
2061 static gboolean
treeSearch_Func(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)2062 treeSearch_Func(GtkTreeModel * model, GtkTreePath *path,
2063 GtkTreeIter * iter, gpointer data)
2064 {
2065 treeSearchT *search = (treeSearchT *)data;
2066 BalsaPartInfo *info = NULL;
2067
2068 gtk_tree_model_get(model, iter, PART_INFO_COLUMN, &info, -1);
2069 if (info) {
2070 if (info->body == search->body) {
2071 search->info = info;
2072 return TRUE;
2073 } else
2074 g_object_unref(info);
2075 }
2076
2077 return FALSE;
2078 }
2079
2080 static BalsaPartInfo *
part_info_from_body(BalsaMessage * bm,const LibBalsaMessageBody * body)2081 part_info_from_body(BalsaMessage *bm, const LibBalsaMessageBody *body)
2082 {
2083 treeSearchT search;
2084
2085 search.body = body;
2086 search.info = NULL;
2087
2088 gtk_tree_model_foreach
2089 (gtk_tree_view_get_model(GTK_TREE_VIEW(bm->treeview)),
2090 treeSearch_Func, &search);
2091 return search.info;
2092 }
2093
2094
2095 static LibBalsaMessageBody *
add_body(BalsaMessage * bm,LibBalsaMessageBody * body,GtkWidget * container)2096 add_body(BalsaMessage * bm, LibBalsaMessageBody * body,
2097 GtkWidget * container)
2098 {
2099 if(body) {
2100 BalsaPartInfo *info = part_info_from_body(bm, body);
2101
2102 if (info) {
2103 body = add_part(bm, info, container);
2104 g_object_unref(info);
2105 } else
2106 body = add_multipart(bm, body, container);
2107 }
2108
2109 return body;
2110 }
2111
2112 static LibBalsaMessageBody *
add_multipart_digest(BalsaMessage * bm,LibBalsaMessageBody * body,GtkWidget * container)2113 add_multipart_digest(BalsaMessage * bm, LibBalsaMessageBody * body,
2114 GtkWidget * container)
2115 {
2116 LibBalsaMessageBody *retval = NULL;
2117 /* Add all parts */
2118 retval = add_body(bm, body, container);
2119 for (body = body->next; body; body = body->next)
2120 add_body(bm, body, container);
2121
2122 return retval;
2123 }
2124
2125 static LibBalsaMessageBody *
add_multipart_mixed(BalsaMessage * bm,LibBalsaMessageBody * body,GtkWidget * container)2126 add_multipart_mixed(BalsaMessage * bm, LibBalsaMessageBody * body,
2127 GtkWidget * container)
2128 {
2129 LibBalsaMessageBody * retval = NULL;
2130 /* Add first (main) part + anything else with
2131 Content-Disposition: inline */
2132 if (body) {
2133 retval = add_body(bm, body, container);
2134 for (body = body->next; body; body = body->next) {
2135 #ifdef HAVE_GPGME
2136 GMimeContentType *type =
2137 g_mime_content_type_new_from_string(body->content_type);
2138
2139 if (libbalsa_message_body_is_inline(body) ||
2140 bm->force_inline ||
2141 libbalsa_message_body_is_multipart(body) ||
2142 g_mime_content_type_is_type(type, "application", "pgp-signature") ||
2143 (balsa_app.has_smime &&
2144 (g_mime_content_type_is_type(type, "application", "pkcs7-signature") ||
2145 g_mime_content_type_is_type(type, "application", "x-pkcs7-signature"))))
2146 add_body(bm, body, container);
2147 g_object_unref(type);
2148 #else
2149 if (libbalsa_message_body_is_inline(body) ||
2150 bm->force_inline ||
2151 libbalsa_message_body_is_multipart(body))
2152 add_body(bm, body, container);
2153 #endif
2154 }
2155 }
2156
2157 return retval;
2158 }
2159
2160 static LibBalsaMessageBody *
add_multipart(BalsaMessage * bm,LibBalsaMessageBody * body,GtkWidget * container)2161 add_multipart(BalsaMessage *bm, LibBalsaMessageBody *body,
2162 GtkWidget * container)
2163 /* This function handles multiparts as specified by RFC2046 5.1 and
2164 * message/rfc822 types. */
2165 {
2166 GMimeContentType *type;
2167
2168 if (!body->parts)
2169 return body;
2170
2171 type=g_mime_content_type_new_from_string(body->content_type);
2172
2173 if (g_mime_content_type_is_type(type, "multipart", "related")) {
2174 /* FIXME: more processing required see RFC1872 */
2175 /* Add the first part */
2176 body = add_body(bm, body->parts, container);
2177 } else if (g_mime_content_type_is_type(type, "multipart", "alternative")) {
2178 /* Add the most suitable part. */
2179 body = add_body(bm, preferred_part(body->parts), container);
2180 } else if (g_mime_content_type_is_type(type, "multipart", "digest")) {
2181 body = add_multipart_digest(bm, body->parts, container);
2182 } else { /* default to multipart/mixed */
2183 body = add_multipart_mixed(bm, body->parts, container);
2184 }
2185 g_object_unref(type);
2186
2187 return body;
2188 }
2189
2190 static LibBalsaMessageBody *
add_part(BalsaMessage * bm,BalsaPartInfo * info,GtkWidget * container)2191 add_part(BalsaMessage * bm, BalsaPartInfo * info, GtkWidget * container)
2192 {
2193 GtkTreeSelection *selection;
2194 GtkWidget *widget;
2195 LibBalsaMessageBody *body;
2196
2197 if (!info)
2198 return NULL;
2199
2200 selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(bm->treeview));
2201
2202 if (info->path &&
2203 !gtk_tree_selection_path_is_selected(selection, info->path))
2204 gtk_tree_selection_select_path(selection, info->path);
2205
2206 if (info->mime_widget == NULL)
2207 part_info_init(bm, info);
2208
2209 if ((widget = info->mime_widget->widget))
2210 gtk_box_pack_start(GTK_BOX(container), widget, TRUE, TRUE, 0);
2211
2212 body =
2213 add_multipart(bm, info->body,
2214 info->mime_widget->container ?
2215 info->mime_widget->container : container);
2216
2217 return body;
2218 }
2219
2220
2221 static gboolean
gtk_tree_hide_func(GtkTreeModel * model,GtkTreePath * path,GtkTreeIter * iter,gpointer data)2222 gtk_tree_hide_func(GtkTreeModel * model, GtkTreePath * path,
2223 GtkTreeIter * iter, gpointer data)
2224 {
2225 BalsaPartInfo *info;
2226
2227 gtk_tree_model_get(model, iter, PART_INFO_COLUMN, &info, -1);
2228 if (info) {
2229 GtkWidget *widget, *parent;
2230
2231 if (info->mime_widget && (widget = info->mime_widget->widget)
2232 && (parent = gtk_widget_get_parent(widget)))
2233 gtk_container_remove(GTK_CONTAINER(parent), widget);
2234 g_object_unref(info);
2235 }
2236
2237 return FALSE;
2238 }
2239
2240 static void
hide_all_parts(BalsaMessage * bm)2241 hide_all_parts(BalsaMessage * bm)
2242 {
2243 if (bm->current_part) {
2244 gtk_tree_model_foreach(gtk_tree_view_get_model
2245 (GTK_TREE_VIEW(bm->treeview)),
2246 gtk_tree_hide_func, bm);
2247 gtk_tree_selection_unselect_all(gtk_tree_view_get_selection
2248 (GTK_TREE_VIEW(bm->treeview)));
2249 g_object_unref(bm->current_part);
2250 bm->current_part = NULL;
2251 }
2252
2253 gtk_container_foreach(GTK_CONTAINER(bm->bm_widget->container),
2254 (GtkCallback) gtk_widget_destroy, NULL);
2255 }
2256
2257 /*
2258 * If part == -1 then change to no part
2259 * must release selection before hiding a text widget.
2260 */
2261 static void
select_part(BalsaMessage * bm,BalsaPartInfo * info)2262 select_part(BalsaMessage * bm, BalsaPartInfo *info)
2263 {
2264 LibBalsaMessageBody *body;
2265 GtkAdjustment *hadj, *vadj;
2266
2267 hide_all_parts(bm);
2268
2269 if (bm->current_part)
2270 g_object_unref(bm->current_part);
2271
2272 body = add_part(bm, info, bm->bm_widget->container);
2273 bm->current_part = part_info_from_body(bm, body);
2274
2275 g_signal_emit(G_OBJECT(bm), balsa_message_signals[SELECT_PART], 0);
2276
2277 g_object_get(G_OBJECT(bm->scroll), "hadjustment", &hadj,
2278 "vadjustment", &vadj, NULL);
2279 gtk_adjustment_set_value(hadj, 0);
2280 gtk_adjustment_set_value(vadj, 0);
2281 }
2282
2283 GtkWidget *
balsa_message_current_part_widget(BalsaMessage * bmessage)2284 balsa_message_current_part_widget(BalsaMessage * bmessage)
2285 {
2286 if (bmessage && bmessage->current_part &&
2287 bmessage->current_part->mime_widget)
2288 return bmessage->current_part->mime_widget->widget;
2289 else
2290 return NULL;
2291 }
2292
2293 GtkWindow *
balsa_get_parent_window(GtkWidget * widget)2294 balsa_get_parent_window(GtkWidget * widget)
2295 {
2296 if (widget) {
2297 GtkWidget *toplevel = gtk_widget_get_toplevel(widget);
2298
2299 if (gtk_widget_is_toplevel(toplevel) && GTK_IS_WINDOW(toplevel))
2300 return GTK_WINDOW(toplevel);
2301 }
2302
2303 return GTK_WINDOW(balsa_app.main_window);
2304 }
2305
2306
2307 /*
2308 * This function informs the caller if the currently selected part
2309 * supports selection/copying etc.
2310 */
2311 gboolean
balsa_message_can_select(BalsaMessage * bmessage)2312 balsa_message_can_select(BalsaMessage * bmessage)
2313 {
2314 GtkWidget *w;
2315
2316 g_return_val_if_fail(bmessage != NULL, FALSE);
2317
2318 if (bmessage->current_part == NULL
2319 || (w = bmessage->current_part->mime_widget->widget) == NULL)
2320 return FALSE;
2321
2322 return GTK_IS_EDITABLE(w) || GTK_IS_TEXT_VIEW(w)
2323 #ifdef HAVE_HTML_WIDGET
2324 || libbalsa_html_can_select(w)
2325 #endif /* HAVE_HTML_WIDGET */
2326 ;
2327 }
2328
2329 gboolean
balsa_message_grab_focus(BalsaMessage * bmessage)2330 balsa_message_grab_focus(BalsaMessage * bmessage)
2331 {
2332 GtkWidget *widget;
2333
2334 g_return_val_if_fail(bmessage != NULL, FALSE);
2335 g_return_val_if_fail(bmessage->current_part != NULL, FALSE);
2336
2337 widget = bmessage->current_part->mime_widget->widget;
2338 g_return_val_if_fail(widget != NULL, FALSE);
2339
2340 gtk_widget_set_can_focus(widget, TRUE);
2341 gtk_widget_grab_focus(widget);
2342
2343 return TRUE;
2344 }
2345
2346 static InternetAddress *
bm_get_mailbox(InternetAddressList * list)2347 bm_get_mailbox(InternetAddressList * list)
2348 {
2349 InternetAddress *ia;
2350
2351 if (!list)
2352 return NULL;
2353
2354 ia = internet_address_list_get_address (list, 0);
2355 if (!ia)
2356 return NULL;
2357
2358 while (ia && INTERNET_ADDRESS_IS_GROUP (ia))
2359 ia = internet_address_list_get_address (INTERNET_ADDRESS_GROUP (ia)->members, 0);
2360 if (!ia)
2361 return NULL;
2362
2363 return ia;
2364 }
2365
2366 static void
handle_mdn_request(GtkWindow * parent,LibBalsaMessage * message)2367 handle_mdn_request(GtkWindow *parent, LibBalsaMessage *message)
2368 {
2369 gboolean suspicious;
2370 InternetAddressList *use_from;
2371 InternetAddressList *list;
2372 InternetAddress *from, *dn;
2373 BalsaMDNReply action;
2374 LibBalsaMessage *mdn;
2375 LibBalsaIdentity *mdn_ident = NULL;
2376 gint i, len;
2377
2378 /* Check if the dispnotify_to address is equal to the (in this order,
2379 if present) reply_to, from or sender address. */
2380 if (message->headers->reply_to)
2381 use_from = message->headers->reply_to;
2382 else if (message->headers->from)
2383 use_from = message->headers->from;
2384 else if (message->sender)
2385 use_from = message->sender;
2386 else
2387 use_from = NULL;
2388 /* note: neither Disposition-Notification-To: nor Reply-To:, From: or
2389 Sender: may be address groups */
2390 from = use_from ? internet_address_list_get_address (use_from, 0) : NULL;
2391 dn = internet_address_list_get_address (message->headers->dispnotify_to, 0);
2392 suspicious = !libbalsa_ia_rfc2821_equal(dn, from);
2393
2394 /* Try to find "my" identity first in the to, then in the cc list */
2395 list = message->headers->to_list;
2396 len = list ? internet_address_list_length(list) : 0;
2397 for (i = 0; i < len && !mdn_ident; i++) {
2398 GList * id_list;
2399
2400 for (id_list = balsa_app.identities; !mdn_ident && id_list;
2401 id_list = id_list->next) {
2402 LibBalsaIdentity *ident = LIBBALSA_IDENTITY(id_list->data);
2403
2404 if (libbalsa_ia_rfc2821_equal(ident->ia, bm_get_mailbox(list)))
2405 mdn_ident = ident;
2406 }
2407 }
2408
2409 list = message->headers->cc_list;
2410 for (i = 0; i < len && !mdn_ident; i++) {
2411 GList * id_list;
2412
2413 for (id_list = balsa_app.identities; !mdn_ident && id_list;
2414 id_list = id_list->next) {
2415 LibBalsaIdentity *ident = LIBBALSA_IDENTITY(id_list->data);
2416
2417 if (libbalsa_ia_rfc2821_equal(ident->ia, bm_get_mailbox(list)))
2418 mdn_ident = ident;
2419 }
2420 }
2421
2422
2423 /* Now we decide from the settings of balsa_app.mdn_reply_[not]clean what
2424 to do...
2425 */
2426 if (suspicious || !mdn_ident)
2427 action = balsa_app.mdn_reply_notclean;
2428 else
2429 action = balsa_app.mdn_reply_clean;
2430 if (action == BALSA_MDN_REPLY_NEVER)
2431 return;
2432
2433 /* fall back to the current identity if the requested one is empty */
2434 if (!mdn_ident)
2435 mdn_ident = balsa_app.current_ident;
2436
2437 /* We *may* send a reply, so let's create a message for that... */
2438 mdn = create_mdn_reply (mdn_ident, message, action == BALSA_MDN_REPLY_ASKME);
2439
2440 /* if the user wants to be asked, display a dialog, otherwise send... */
2441 if (action == BALSA_MDN_REPLY_ASKME) {
2442 gchar *sender;
2443 gchar *reply_to;
2444 sender = from ? internet_address_to_string (from, FALSE) : NULL;
2445 reply_to =
2446 internet_address_list_to_string (message->headers->dispnotify_to,
2447 FALSE);
2448 gtk_widget_show_all (create_mdn_dialog (parent, sender, reply_to, mdn,
2449 mdn_ident));
2450 g_free (reply_to);
2451 g_free (sender);
2452 } else {
2453 GError * error = NULL;
2454 LibBalsaMsgCreateResult result;
2455
2456 #if ENABLE_ESMTP
2457 result = libbalsa_message_send(mdn, balsa_app.outbox, NULL,
2458 balsa_find_sentbox_by_url,
2459 mdn_ident->smtp_server,
2460 TRUE, balsa_app.debug, &error);
2461 #else
2462 result = libbalsa_message_send(mdn, balsa_app.outbox, NULL,
2463 balsa_find_sentbox_by_url,
2464 TRUE, balsa_app.debug, &error);
2465 #endif
2466 if (result != LIBBALSA_MESSAGE_CREATE_OK)
2467 libbalsa_information(LIBBALSA_INFORMATION_ERROR,
2468 _("Sending the disposition notification failed: %s"),
2469 error ? error->message : "?");
2470 g_error_free(error);
2471 g_object_unref(G_OBJECT(mdn));
2472 }
2473 }
2474
create_mdn_reply(const LibBalsaIdentity * mdn_ident,LibBalsaMessage * for_msg,gboolean manual)2475 static LibBalsaMessage *create_mdn_reply (const LibBalsaIdentity *mdn_ident,
2476 LibBalsaMessage *for_msg,
2477 gboolean manual)
2478 {
2479 LibBalsaMessage *message;
2480 LibBalsaMessageBody *body;
2481 gchar *date, *dummy;
2482 GString *report;
2483 gchar **params;
2484 struct utsname uts_name;
2485 const gchar *original_rcpt;
2486
2487 /* create a message with the header set from the incoming message */
2488 message = libbalsa_message_new();
2489 message->headers->from = internet_address_list_new();
2490 internet_address_list_add(message->headers->from,
2491 balsa_app.current_ident->ia);
2492 message->headers->date = time(NULL);
2493 libbalsa_message_set_subject(message, "Message Disposition Notification");
2494 message->headers->to_list = internet_address_list_new ();
2495 internet_address_list_append(message->headers->to_list,
2496 for_msg->headers->dispnotify_to);
2497
2498 /* RFC 2298 requests this mime type... */
2499 message->subtype = g_strdup("report");
2500 params = g_new(gchar *, 3);
2501 params[0] = g_strdup("report-type");
2502 params[1] = g_strdup("disposition-notification");
2503 params[2] = NULL;
2504 message->parameters = g_list_prepend(message->parameters, params);
2505
2506 /* the first part of the body is an informational note */
2507 body = libbalsa_message_body_new(message);
2508 date = libbalsa_message_date_to_utf8(for_msg, balsa_app.date_string);
2509 dummy = internet_address_list_to_string(for_msg->headers->to_list, FALSE);
2510 body->buffer = g_strdup_printf(
2511 "The message sent on %s to %s with subject \"%s\" has been displayed.\n"
2512 "There is no guarantee that the message has been read or understood.\n\n",
2513 date, dummy, LIBBALSA_MESSAGE_GET_SUBJECT(for_msg));
2514 g_free (date);
2515 g_free (dummy);
2516 libbalsa_utf8_sanitize(&body->buffer, balsa_app.convert_unknown_8bit, NULL);
2517 if (balsa_app.wordwrap)
2518 libbalsa_wrap_string(body->buffer, balsa_app.wraplength);
2519 dummy = (gchar *)g_mime_charset_best(body->buffer, strlen(body->buffer));
2520 body->charset = g_strdup(dummy ? dummy : "us-ascii");
2521 libbalsa_message_append_part(message, body);
2522
2523 /* the second part is a rfc3798 compliant
2524 message/disposition-notification */
2525 body = libbalsa_message_body_new(message);
2526 report = g_string_new("");
2527 uname(&uts_name);
2528 g_string_printf(report, "Reporting-UA: %s; " PACKAGE " " VERSION "\n",
2529 uts_name.nodename);
2530 /* see rfc 3798, sections 2.3 and 3.2.3 */
2531 if ((original_rcpt =
2532 libbalsa_message_get_user_header(for_msg, "original-recipient")))
2533 g_string_append_printf(report, "Original-Recipient: %s\n",
2534 original_rcpt);
2535 g_string_append_printf(report, "Final-Recipient: rfc822; %s\n",
2536 INTERNET_ADDRESS_MAILBOX(balsa_app.
2537 current_ident->ia)->
2538 addr);
2539 if (for_msg->message_id)
2540 g_string_append_printf(report, "Original-Message-ID: <%s>\n",
2541 for_msg->message_id);
2542 g_string_append_printf(report,
2543 "Disposition: %s-action/MDN-sent-%sly; displayed",
2544 manual ? "manual" : "automatic",
2545 manual ? "manual" : "automatical");
2546 body->buffer = report->str;
2547 g_string_free(report, FALSE);
2548 body->content_type = g_strdup("message/disposition-notification");
2549 body->charset = g_strdup ("US-ASCII");
2550 libbalsa_message_append_part(message, body);
2551 return message;
2552 }
2553
2554 static GtkWidget *
create_mdn_dialog(GtkWindow * parent,gchar * sender,gchar * mdn_to_address,LibBalsaMessage * send_msg,LibBalsaIdentity * mdn_ident)2555 create_mdn_dialog(GtkWindow *parent, gchar * sender, gchar * mdn_to_address,
2556 LibBalsaMessage * send_msg, LibBalsaIdentity *mdn_ident)
2557 {
2558 GtkWidget *mdn_dialog;
2559
2560 mdn_dialog =
2561 gtk_message_dialog_new(parent,
2562 GTK_DIALOG_DESTROY_WITH_PARENT,
2563 GTK_MESSAGE_QUESTION,
2564 GTK_BUTTONS_YES_NO,
2565 _("The sender of this mail, %s, "
2566 "requested \n"
2567 "a Message Disposition Notification"
2568 "(MDN) to be returned to `%s'.\n"
2569 "Do you want to send "
2570 "this notification?"),
2571 sender, mdn_to_address);
2572 #if HAVE_MACOSX_DESKTOP
2573 libbalsa_macosx_menu_for_parent(mdn_dialog, parent);
2574 #endif
2575 gtk_window_set_title(GTK_WINDOW(mdn_dialog), _("Reply to MDN?"));
2576 g_object_set_data(G_OBJECT(mdn_dialog), "balsa-send-msg", send_msg);
2577 g_object_set_data(G_OBJECT(mdn_dialog), "mdn-ident",
2578 g_object_ref(mdn_ident));
2579 g_signal_connect(G_OBJECT(mdn_dialog), "response",
2580 G_CALLBACK(mdn_dialog_response), NULL);
2581
2582 return mdn_dialog;
2583 }
2584
2585 static void
mdn_dialog_response(GtkWidget * dialog,gint response,gpointer user_data)2586 mdn_dialog_response(GtkWidget * dialog, gint response, gpointer user_data)
2587 {
2588 LibBalsaMessage *send_msg =
2589 LIBBALSA_MESSAGE(g_object_get_data(G_OBJECT(dialog),
2590 "balsa-send-msg"));
2591 LibBalsaIdentity *mdn_ident =
2592 LIBBALSA_IDENTITY(g_object_get_data(G_OBJECT(dialog), "mdn-ident"));
2593 GError * error = NULL;
2594 LibBalsaMsgCreateResult result;
2595
2596 g_return_if_fail(send_msg != NULL);
2597 g_return_if_fail(mdn_ident != NULL);
2598
2599 if (response == GTK_RESPONSE_YES) {
2600 #if ENABLE_ESMTP
2601 result = libbalsa_message_send(send_msg, balsa_app.outbox, NULL,
2602 balsa_find_sentbox_by_url,
2603 mdn_ident->smtp_server,
2604 TRUE, balsa_app.debug, &error);
2605 #else
2606 result = libbalsa_message_send(send_msg, balsa_app.outbox, NULL,
2607 balsa_find_sentbox_by_url,
2608 TRUE, balsa_app.debug, &error);
2609 #endif
2610 if (result != LIBBALSA_MESSAGE_CREATE_OK)
2611 libbalsa_information(LIBBALSA_INFORMATION_ERROR,
2612 _("Sending the disposition notification failed: %s"),
2613 error ? error->message : "?");
2614 if (error)
2615 g_error_free(error);
2616 }
2617 g_object_unref(G_OBJECT(send_msg));
2618 g_object_unref(G_OBJECT(mdn_ident));
2619 gtk_widget_destroy(dialog);
2620 }
2621
2622 #ifdef HAVE_HTML_WIDGET
2623 /* Does the current part support zoom? */
2624 gboolean
balsa_message_can_zoom(BalsaMessage * bm)2625 balsa_message_can_zoom(BalsaMessage * bm)
2626 {
2627 return bm->current_part
2628 && libbalsa_html_can_zoom(bm->current_part->mime_widget->widget);
2629 }
2630
2631 /* Zoom an html item. */
2632 void
balsa_message_zoom(BalsaMessage * bm,gint in_out)2633 balsa_message_zoom(BalsaMessage * bm, gint in_out)
2634 {
2635 gint zoom;
2636
2637 if (!balsa_message_can_zoom(bm))
2638 return;
2639
2640 zoom =
2641 GPOINTER_TO_INT(g_object_get_data
2642 (G_OBJECT(bm->message), BALSA_MESSAGE_ZOOM_KEY));
2643 if (in_out)
2644 zoom += in_out;
2645 else
2646 zoom = 0;
2647 g_object_set_data(G_OBJECT(bm->message), BALSA_MESSAGE_ZOOM_KEY,
2648 GINT_TO_POINTER(zoom));
2649
2650 libbalsa_html_zoom(bm->current_part->mime_widget->widget, in_out);
2651
2652 }
2653 #endif /* HAVE_HTML_WIDGET */
2654
2655
2656 #ifdef HAVE_GPGME
2657 /*
2658 * collected GPG(ME) helper stuff to make the source more readable
2659 */
2660
2661
2662 /*
2663 * Calculate and return a "worst case" summary of all checked signatures in a
2664 * message.
2665 */
2666 static LibBalsaMsgProtectState
balsa_message_scan_signatures(LibBalsaMessageBody * body,LibBalsaMessage * message)2667 balsa_message_scan_signatures(LibBalsaMessageBody *body,
2668 LibBalsaMessage * message)
2669 {
2670 LibBalsaMsgProtectState result = LIBBALSA_MSG_PROTECT_NONE;
2671
2672 g_return_val_if_fail(message->headers != NULL, result);
2673
2674 while (body) {
2675 LibBalsaMsgProtectState this_part_state =
2676 libbalsa_message_body_protect_state(body);
2677
2678 /* remember: greater means worse... */
2679 if (this_part_state > result)
2680 result = this_part_state;
2681
2682 /* scan embedded messages */
2683 if (body->parts) {
2684 LibBalsaMsgProtectState sub_result =
2685 balsa_message_scan_signatures(body->parts, message);
2686
2687 if (sub_result >= result)
2688 result = sub_result;
2689 }
2690
2691 body = body->next;
2692 }
2693
2694 return result;
2695 }
2696
2697
2698 /*
2699 * Check if body (of type content_type) is signed and/or encrypted and return
2700 * the proper icon in this case, and NULL otherwise. If the part is signed,
2701 * replace *icon-title by the signature status.
2702 */
2703 static GdkPixbuf *
get_crypto_content_icon(LibBalsaMessageBody * body,const gchar * content_type,gchar ** icon_title)2704 get_crypto_content_icon(LibBalsaMessageBody * body, const gchar * content_type,
2705 gchar ** icon_title)
2706 {
2707 GdkPixbuf *icon;
2708 gchar * new_title;
2709 const gchar * icon_name;
2710
2711 if ((libbalsa_message_body_protection(body) &
2712 (LIBBALSA_PROTECT_ENCRYPT | LIBBALSA_PROTECT_ERROR)) ==
2713 LIBBALSA_PROTECT_ENCRYPT)
2714 return NULL;
2715
2716 icon_name = balsa_mime_widget_signature_icon_name(libbalsa_message_body_protect_state(body));
2717 if (!icon_name)
2718 return NULL;
2719 icon =
2720 gtk_widget_render_icon_pixbuf(GTK_WIDGET(balsa_app.main_window),
2721 icon_name,
2722 GTK_ICON_SIZE_LARGE_TOOLBAR);
2723 if (!icon_title)
2724 return icon;
2725
2726 if (*icon_title &&
2727 g_ascii_strcasecmp(content_type, "application/pgp-signature") &&
2728 g_ascii_strcasecmp(content_type, "application/pkcs7-signature") &&
2729 g_ascii_strcasecmp(content_type, "application/x-pkcs7-signature"))
2730 new_title = g_strconcat(*icon_title, "; ",
2731 libbalsa_gpgme_sig_protocol_name(body->sig_info->protocol),
2732 libbalsa_gpgme_sig_stat_to_gchar(body->sig_info->status),
2733 NULL);
2734 else
2735 new_title = g_strconcat(libbalsa_gpgme_sig_protocol_name(body->sig_info->protocol),
2736 libbalsa_gpgme_sig_stat_to_gchar(body->sig_info->status),
2737 NULL);
2738
2739 if (*icon_title)
2740 g_free(*icon_title);
2741 *icon_title = new_title;
2742
2743 return icon;
2744 }
2745
2746
2747 /* helper functions for libbalsa_msg_perform_crypto below */
2748
2749 /* this is a helper structure to simplify passing a set of parameters */
2750 typedef struct {
2751 LibBalsaChkCryptoMode chk_mode; /* current check mode */
2752 gboolean no_mp_signed; /* don't check multipart/signed */
2753 guint max_ref; /* maximum allowed ref count */
2754 gchar * sender; /* shortcut to sender */
2755 gchar * subject; /* shortcut to subject */
2756 } chk_crypto_t;
2757
2758
2759 /*
2760 * check if body is a multipart/encrypted (RFC 3156) or a signed or encrypted
2761 * application/pkcs7-mime type, try to decrypt it and return the new body
2762 * chain.
2763 */
2764 static LibBalsaMessageBody *
libbalsa_msg_try_decrypt(LibBalsaMessage * message,LibBalsaMessageBody * body,chk_crypto_t * chk_crypto)2765 libbalsa_msg_try_decrypt(LibBalsaMessage * message, LibBalsaMessageBody * body,
2766 chk_crypto_t * chk_crypto)
2767 {
2768 gchar * mime_type;
2769 LibBalsaMessageBody * this_body;
2770
2771 /* Check for multipart/encrypted or application/pkcs7-mime parts and
2772 replace this_body by it's decrypted content. Remember that there may be
2773 weird cases where such parts are nested, so do it in a loop. */
2774 this_body = body;
2775 mime_type = libbalsa_message_body_get_mime_type(this_body);
2776 while (!g_ascii_strcasecmp(mime_type, "multipart/encrypted") ||
2777 !g_ascii_strcasecmp(mime_type, "application/pkcs7-mime")) {
2778 gint encrres;
2779
2780 /* FIXME: not checking for body_ref > 1 (or > 2 when re-checking, which
2781 * adds an extra ref) leads to a crash if we have both the encrypted and
2782 * the unencrypted version open as the body chain of the first one will be
2783 * unref'd. */
2784 if (message->body_ref > chk_crypto->max_ref) {
2785 if (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS) {
2786 libbalsa_information
2787 (LIBBALSA_INFORMATION_ERROR,
2788 _("The decryption cannot be performed because this message "
2789 "is displayed more than once.\n"
2790 "Please close the other instances of this message and try "
2791 "again."));
2792 /* downgrade the check mode to avoid multiple errors popping up */
2793 chk_crypto->chk_mode = LB_MAILBOX_CHK_CRYPT_MAYBE;
2794 }
2795 return body;
2796 }
2797
2798 /* check if the gmime part is present and force loading it if we are
2799 in "always" mode */
2800 if (!this_body->mime_part) {
2801 GError *err = NULL;
2802 if (chk_crypto->chk_mode != LB_MAILBOX_CHK_CRYPT_ALWAYS)
2803 return this_body;
2804
2805 /* force loading the missing part */
2806 if(!libbalsa_mailbox_get_message_part(message, this_body, &err)) {
2807 libbalsa_information
2808 (LIBBALSA_INFORMATION_ERROR,
2809 _("Parsing a message part failed: %s"),
2810 err ? err->message : _("Possible disk space problem."));
2811 g_clear_error(&err);
2812 /* There is nothing more we can do... */
2813 return body;
2814 }
2815 }
2816
2817 encrres = libbalsa_message_body_protection(this_body);
2818
2819 if (encrres & LIBBALSA_PROTECT_ENCRYPT) {
2820 if (encrres & LIBBALSA_PROTECT_ERROR) {
2821 libbalsa_information
2822 (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS ?
2823 LIBBALSA_INFORMATION_ERROR : LIBBALSA_INFORMATION_MESSAGE,
2824 _("The message sent by %s with subject \"%s\" contains "
2825 "an encrypted part, but it's structure is invalid."),
2826 chk_crypto->sender, chk_crypto->subject);
2827 } else if (encrres & LIBBALSA_PROTECT_RFC3156) {
2828 if (!balsa_app.has_openpgp)
2829 libbalsa_information
2830 (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS ?
2831 LIBBALSA_INFORMATION_WARNING : LIBBALSA_INFORMATION_MESSAGE,
2832 _("The message sent by %s with subject \"%s\" "
2833 "contains a PGP encrypted part, but this "
2834 "crypto protocol is not available."),
2835 chk_crypto->sender, chk_crypto->subject);
2836 else
2837 this_body =
2838 libbalsa_body_decrypt(this_body,
2839 GPGME_PROTOCOL_OpenPGP, NULL);
2840 } else if (encrres & LIBBALSA_PROTECT_SMIMEV3) {
2841 if (!balsa_app.has_smime)
2842 libbalsa_information
2843 (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS ?
2844 LIBBALSA_INFORMATION_WARNING : LIBBALSA_INFORMATION_MESSAGE,
2845 _("The message sent by %s with subject \"%s\" "
2846 "contains a S/MIME encrypted part, but this "
2847 "crypto protocol is not available."),
2848 chk_crypto->sender, chk_crypto->subject);
2849 else
2850 this_body =
2851 libbalsa_body_decrypt(this_body, GPGME_PROTOCOL_CMS,
2852 NULL);
2853 }
2854 }
2855
2856 /* has been decrypted? - eject if not, otherwise... */
2857 if (!this_body->was_encrypted)
2858 return this_body;
2859
2860 /* ...check decrypted content again... */
2861 g_free(mime_type);
2862 mime_type = libbalsa_message_body_get_mime_type(this_body);
2863 }
2864 g_free(mime_type);
2865
2866 return this_body;
2867 }
2868
2869
2870 /*
2871 * Treatment of a multipart/signed body with protocol protocol.
2872 */
2873 #define BALSA_MESSAGE_SIGNED_NOTIFIED "balsa-message-signed-notified"
2874 static void
libbalsa_msg_try_mp_signed(LibBalsaMessage * message,LibBalsaMessageBody * body,chk_crypto_t * chk_crypto)2875 libbalsa_msg_try_mp_signed(LibBalsaMessage * message, LibBalsaMessageBody *body,
2876 chk_crypto_t * chk_crypto)
2877 {
2878 gint signres;
2879
2880 if (chk_crypto->no_mp_signed)
2881 return;
2882
2883 /* check if the gmime part is present and force loading it if we are in
2884 "always" mode */
2885 if (!body->mime_part) {
2886 GError *err = NULL;
2887 if (chk_crypto->chk_mode != LB_MAILBOX_CHK_CRYPT_ALWAYS)
2888 return;
2889
2890 /* force loading the missing part */
2891 if(!libbalsa_mailbox_get_message_part(message, body, &err)) {
2892 libbalsa_information
2893 (LIBBALSA_INFORMATION_ERROR,
2894 _("Parsing a message part failed: %s"),
2895 err ? err->message : _("Possible disk space problem."));
2896 g_clear_error(&err);
2897 /* There is nothing more we can do... */
2898 return;
2899 }
2900 }
2901
2902 /* check which type of protection we've got */
2903 signres = libbalsa_message_body_protection(body);
2904 if (!(signres & LIBBALSA_PROTECT_SIGN))
2905 return;
2906
2907 /* eject if the structure is broken */
2908 if (signres & LIBBALSA_PROTECT_ERROR) {
2909 libbalsa_information
2910 (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS ?
2911 LIBBALSA_INFORMATION_ERROR : LIBBALSA_INFORMATION_MESSAGE,
2912 _("The message sent by %s with subject \"%s\" contains a signed "
2913 "part, but its structure is invalid. The signature, if there "
2914 "is any, cannot be checked."),
2915 chk_crypto->sender, chk_crypto->subject);
2916 return;
2917 }
2918
2919 /* check for an unsupported protocol */
2920 if (((signres & LIBBALSA_PROTECT_RFC3156) && !balsa_app.has_openpgp) ||
2921 ((signres & LIBBALSA_PROTECT_SMIMEV3) && !balsa_app.has_smime)) {
2922 libbalsa_information
2923 (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS ?
2924 LIBBALSA_INFORMATION_WARNING : LIBBALSA_INFORMATION_MESSAGE,
2925 _("The message sent by %s with subject \"%s\" contains a %s "
2926 "signed part, but this crypto protocol is not available."),
2927 chk_crypto->sender, chk_crypto->subject,
2928 signres & LIBBALSA_PROTECT_RFC3156 ? _("PGP") : _("S/MIME"));
2929 return;
2930 }
2931
2932 /* force creating the protection info */
2933 if (body->parts->next->sig_info) {
2934 g_object_unref(body->parts->next->sig_info);
2935 body->parts->next->sig_info = NULL;
2936 }
2937 if (!libbalsa_body_check_signature(body,
2938 signres & LIBBALSA_PROTECT_RFC3156 ?
2939 GPGME_PROTOCOL_OpenPGP : GPGME_PROTOCOL_CMS))
2940 return;
2941
2942 /* evaluate the result */
2943 if (g_object_get_data(G_OBJECT(message), BALSA_MESSAGE_SIGNED_NOTIFIED))
2944 return;
2945 g_object_set_data(G_OBJECT(message), BALSA_MESSAGE_SIGNED_NOTIFIED,
2946 GUINT_TO_POINTER(TRUE));
2947
2948 if (body->parts->next->sig_info) {
2949 switch (libbalsa_message_body_protect_state(body->parts->next)) {
2950 case LIBBALSA_MSG_PROTECT_SIGN_GOOD:
2951 libbalsa_information(LIBBALSA_INFORMATION_DEBUG,
2952 _("Detected a good signature"));
2953 break;
2954 case LIBBALSA_MSG_PROTECT_SIGN_NOTRUST:
2955 if (body->parts->next->sig_info->protocol == GPGME_PROTOCOL_CMS)
2956 libbalsa_information
2957 (LIBBALSA_INFORMATION_MESSAGE,
2958 _("Detected a good signature with insufficient "
2959 "validity"));
2960 else
2961 libbalsa_information
2962 (LIBBALSA_INFORMATION_MESSAGE,
2963 _("Detected a good signature with insufficient "
2964 "validity/trust"));
2965 break;
2966 case LIBBALSA_MSG_PROTECT_SIGN_BAD:
2967 libbalsa_information
2968 (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS ?
2969 LIBBALSA_INFORMATION_ERROR : LIBBALSA_INFORMATION_MESSAGE,
2970 _("Checking the signature of the message sent by %s with "
2971 "subject \"%s\" returned:\n%s"),
2972 chk_crypto->sender, chk_crypto->subject,
2973 libbalsa_gpgme_sig_stat_to_gchar(body->parts->next->sig_info->status));
2974 break;
2975 default:
2976 break;
2977 }
2978 } else
2979 libbalsa_information
2980 (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS ?
2981 LIBBALSA_INFORMATION_ERROR : LIBBALSA_INFORMATION_MESSAGE,
2982 _("Checking the signature of the message sent by %s with subject "
2983 "\"%s\" failed with an error!"),
2984 chk_crypto->sender, chk_crypto->subject);
2985 }
2986
2987
2988 /*
2989 * Check if body is has some RFC 2440 protection and try to decrypt and/or
2990 * verify it.
2991 */
2992 static void
libbalsa_msg_part_2440(LibBalsaMessage * message,LibBalsaMessageBody * body,chk_crypto_t * chk_crypto)2993 libbalsa_msg_part_2440(LibBalsaMessage * message, LibBalsaMessageBody * body,
2994 chk_crypto_t * chk_crypto)
2995 {
2996 gpgme_error_t sig_res;
2997 GMimePartRfc2440Mode rfc2440mode;
2998
2999 /* multiparts or complete messages can not be RFC 2440 protected */
3000 if (body->body_type == LIBBALSA_MESSAGE_BODY_TYPE_MESSAGE ||
3001 body->body_type == LIBBALSA_MESSAGE_BODY_TYPE_MULTIPART)
3002 return;
3003
3004 if (!body->mime_part) {
3005 GError *err = NULL;
3006 /* if the user requested to always check everything or if the part is
3007 text (and therefore likely to be displayed anyway) force loading
3008 the missing part */
3009 if (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_MAYBE &&
3010 body->body_type != LIBBALSA_MESSAGE_BODY_TYPE_TEXT)
3011 return;
3012
3013 if(!libbalsa_mailbox_get_message_part(message, body, &err)) {
3014 libbalsa_information
3015 (LIBBALSA_INFORMATION_ERROR,
3016 _("Parsing a message part failed: %s"),
3017 err ? err->message : _("Possible disk space problem."));
3018 g_clear_error(&err);
3019 return;
3020 }
3021 }
3022
3023 /* check if this is a RFC2440 part */
3024 if (!GMIME_IS_PART(body->mime_part))
3025 return;
3026 libbalsa_mailbox_lock_store(body->message->mailbox);
3027 rfc2440mode = g_mime_part_check_rfc2440(GMIME_PART(body->mime_part));
3028 libbalsa_mailbox_unlock_store(body->message->mailbox);
3029
3030 /* if not, or if we have more than one instance of this message open, eject
3031 (see remark for libbalsa_msg_try_decrypt above) - remember that
3032 libbalsa_rfc2440_verify would also replace the stream by the "decrypted"
3033 (i.e. RFC2440 stuff removed) one! */
3034 if (rfc2440mode == GMIME_PART_RFC2440_NONE)
3035 return;
3036 if (message->body_ref > chk_crypto->max_ref) {
3037 if (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS) {
3038 libbalsa_information
3039 (LIBBALSA_INFORMATION_ERROR, "%s\n%s",
3040 rfc2440mode == GMIME_PART_RFC2440_ENCRYPTED ?
3041 _("The decryption cannot be performed because "
3042 "this message is displayed more than once.") :
3043 _("The signature check and removal of the OpenPGP armor "
3044 "cannot be performed because "
3045 "this message is displayed more than once."),
3046 _("Please close the other instances of this message "
3047 "and try again."));
3048 /* downgrade the check mode to avoid multiple errors popping up */
3049 chk_crypto->chk_mode = LB_MAILBOX_CHK_CRYPT_MAYBE;
3050 }
3051 return;
3052 }
3053
3054 /* do the rfc2440 stuff */
3055 libbalsa_mailbox_lock_store(body->message->mailbox);
3056 if (rfc2440mode == GMIME_PART_RFC2440_SIGNED)
3057 sig_res =
3058 libbalsa_rfc2440_verify(GMIME_PART(body->mime_part),
3059 &body->sig_info);
3060 else {
3061 sig_res =
3062 libbalsa_rfc2440_decrypt(GMIME_PART(body->mime_part),
3063 &body->sig_info,
3064 NULL);
3065 body->was_encrypted = (body->sig_info || sig_res == GPG_ERR_NO_ERROR);
3066 if (sig_res == GPG_ERR_NO_ERROR) {
3067 /* decrypting may change the charset, so be sure to use the one
3068 GMimePart reports */
3069 g_free(body->charset);
3070 body->charset = NULL;
3071 }
3072 }
3073 libbalsa_mailbox_unlock_store(body->message->mailbox);
3074
3075 if (body->sig_info && sig_res == GPG_ERR_NO_ERROR) {
3076 if (body->sig_info->validity >= GPGME_VALIDITY_MARGINAL &&
3077 body->sig_info->key->owner_trust >= GPGME_VALIDITY_MARGINAL)
3078 libbalsa_information(LIBBALSA_INFORMATION_DEBUG,
3079 _("Detected a good signature"));
3080 else
3081 libbalsa_information
3082 (LIBBALSA_INFORMATION_MESSAGE,
3083 _("Detected a good signature with insufficient "
3084 "validity/trust"));
3085 } else if (sig_res != GPG_ERR_NO_ERROR && sig_res != GPG_ERR_CANCELED)
3086 libbalsa_information
3087 (chk_crypto->chk_mode == LB_MAILBOX_CHK_CRYPT_ALWAYS ?
3088 LIBBALSA_INFORMATION_ERROR : LIBBALSA_INFORMATION_MESSAGE,
3089 _("Checking the signature of the message sent by %s with "
3090 "subject \"%s\" returned:\n%s"),
3091 chk_crypto->sender, chk_crypto->subject,
3092 libbalsa_gpgme_sig_stat_to_gchar(sig_res));
3093 }
3094
3095
3096 /*
3097 * Scan a body chain for RFC 2440, RFC 2633 and RFC 3156 encrypted and/or
3098 * signed parts. The function is called recursively for nested stuff.
3099 */
3100 static LibBalsaMessageBody *
libbalsa_msg_perform_crypto_real(LibBalsaMessage * message,LibBalsaMessageBody * body,chk_crypto_t * chk_crypto)3101 libbalsa_msg_perform_crypto_real(LibBalsaMessage * message,
3102 LibBalsaMessageBody * body,
3103 chk_crypto_t * chk_crypto)
3104 {
3105 gchar * mime_type;
3106 LibBalsaMessageBody * chk_body;
3107
3108 /* First try do decrypt RFC 3156 multipart/encrypted or any RFC 2633
3109 application/pkcs7-mime stuff. */
3110 body = libbalsa_msg_try_decrypt(message, body, chk_crypto);
3111
3112 /* Check for multipart/signed and check the signature. */
3113 mime_type = libbalsa_message_body_get_mime_type(body);
3114 if (!g_ascii_strcasecmp(mime_type, "multipart/signed"))
3115 libbalsa_msg_try_mp_signed(message, body, chk_crypto);
3116 g_free(mime_type);
3117
3118 /* loop over the parts, checking for RFC 2440 stuff, but ignore
3119 application/octet-stream which might be a detached encrypted part
3120 as well as all detached signatures */
3121 chk_body = body;
3122 while (chk_body) {
3123 mime_type = libbalsa_message_body_get_mime_type(chk_body);
3124
3125 if (g_ascii_strcasecmp(mime_type, "application/octet-stream") &&
3126 g_ascii_strcasecmp(mime_type, "application/pkcs7-signature") &&
3127 g_ascii_strcasecmp(mime_type, "application/x-pkcs7-signature") &&
3128 g_ascii_strcasecmp(mime_type, "application/pgp-signature"))
3129 libbalsa_msg_part_2440(message, chk_body, chk_crypto);
3130 g_free(mime_type);
3131
3132 /* check nested stuff */
3133 if (chk_body->parts)
3134 chk_body->parts =
3135 libbalsa_msg_perform_crypto_real(message, chk_body->parts,
3136 chk_crypto);
3137
3138 chk_body = chk_body->next;
3139 }
3140
3141 return body;
3142 }
3143
3144
3145 /*
3146 * Launches a scan of message with chk_mode and a maximum reference of max_ref.
3147 * If no_mp_signed is TRUE, multipart/signed parts are ignored, i.e. only
3148 * encrypted (either "really" encrypted or armored) parts are used. This is
3149 * useful e.g. for replying.
3150 */
3151 void
balsa_message_perform_crypto(LibBalsaMessage * message,LibBalsaChkCryptoMode chk_mode,gboolean no_mp_signed,guint max_ref)3152 balsa_message_perform_crypto(LibBalsaMessage * message,
3153 LibBalsaChkCryptoMode chk_mode,
3154 gboolean no_mp_signed, guint max_ref)
3155 {
3156 chk_crypto_t chk_crypto;
3157
3158 if (!message->body_list)
3159 return;
3160
3161 /* check if the user requested to ignore any crypto stuff */
3162 if (chk_mode == LB_MAILBOX_CHK_CRYPT_NEVER)
3163 return;
3164
3165 /* set up... */
3166 chk_crypto.chk_mode = chk_mode;
3167 chk_crypto.no_mp_signed = no_mp_signed;
3168 chk_crypto.max_ref = max_ref;
3169 chk_crypto.sender = balsa_message_sender_to_gchar(message->headers->from, -1);
3170 chk_crypto.subject = g_strdup(LIBBALSA_MESSAGE_GET_SUBJECT(message));
3171 libbalsa_utf8_sanitize(&chk_crypto.subject, balsa_app.convert_unknown_8bit,
3172 NULL);
3173
3174 /* do the real work */
3175 message->body_list =
3176 libbalsa_msg_perform_crypto_real(message, message->body_list,
3177 &chk_crypto);
3178
3179 /* clean up */
3180 g_free(chk_crypto.subject);
3181 g_free(chk_crypto.sender);
3182 }
3183
3184
3185 /*
3186 * Callback for the "Check Crypto" button in the message's top-level headers.
3187 * It works roughly like balsa_message_set, but with less overhead and with
3188 * "check always" mode. Note that this routine adds a temporary reference to
3189 * the message.
3190 */
3191 static void
message_recheck_crypto_cb(GtkWidget * button,BalsaMessage * bm)3192 message_recheck_crypto_cb(GtkWidget * button, BalsaMessage * bm)
3193 {
3194 LibBalsaMsgProtectState prot_state;
3195 LibBalsaMessage * message;
3196 GtkTreeIter iter;
3197 BalsaPartInfo * info;
3198 gboolean has_focus = bm->focus_state != BALSA_MESSAGE_FOCUS_STATE_NO;
3199
3200 g_return_if_fail(bm != NULL);
3201
3202 message = bm->message;
3203 g_return_if_fail(message != NULL);
3204
3205 select_part(bm, NULL);
3206 balsa_message_clear_tree(bm);
3207
3208 g_object_ref(G_OBJECT(message));
3209 if (!libbalsa_message_body_ref(message, TRUE, TRUE)) {
3210 g_object_unref(G_OBJECT(message));
3211 return;
3212 }
3213
3214 g_object_set_data(G_OBJECT(message), BALSA_MESSAGE_SIGNED_NOTIFIED, NULL);
3215 balsa_message_perform_crypto(message, LB_MAILBOX_CHK_CRYPT_ALWAYS, FALSE, 2);
3216
3217 /* calculate the signature summary state */
3218 prot_state =
3219 balsa_message_scan_signatures(message->body_list, message);
3220
3221 /* update the icon if necessary */
3222 if (message->prot_state != prot_state)
3223 message->prot_state = prot_state;
3224
3225 /* may update the icon */
3226 libbalsa_mailbox_msgno_update_attach(bm->message->mailbox,
3227 bm->message->msgno, bm->message);
3228
3229 display_headers(bm);
3230 display_content(bm);
3231
3232 #if defined(ENABLE_TOUCH_UI)
3233 /* hide tabs so that they do not confuse keyboard navigation.
3234 * This could probably be a configuration option. */
3235 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(bm), FALSE);
3236 #else
3237 gtk_notebook_set_show_tabs(GTK_NOTEBOOK(bm), bm->info_count > 1);
3238 #endif /* ENABLE_TOUCH_UI */
3239 gtk_notebook_set_current_page(GTK_NOTEBOOK(bm), 0);
3240
3241 if (!gtk_tree_model_get_iter_first (gtk_tree_view_get_model(GTK_TREE_VIEW(bm->treeview)),
3242 &iter)) {
3243 libbalsa_message_body_unref(message);
3244 g_object_unref(G_OBJECT(message));
3245 return;
3246 }
3247
3248 info =
3249 tree_next_valid_part_info(gtk_tree_view_get_model(GTK_TREE_VIEW(bm->treeview)),
3250 &iter);
3251 select_part(bm, info);
3252 if (info)
3253 g_object_unref(info);
3254
3255 /* restore keyboard focus to the content, if it was there before */
3256 if (has_focus)
3257 balsa_message_grab_focus(bm);
3258
3259 libbalsa_message_body_unref(message);
3260 g_object_unref(G_OBJECT(message));
3261 }
3262
3263 #endif /* HAVE_GPGME */
3264
3265 /*
3266 * Public method for find-in-message.
3267 */
3268
3269 void
balsa_message_find_in_message(BalsaMessage * bm)3270 balsa_message_find_in_message(BalsaMessage * bm)
3271 {
3272 GtkWidget *w;
3273
3274 if (bm->current_part
3275 && (w = bm->current_part->mime_widget->widget)
3276 && (GTK_IS_TEXT_VIEW(w)
3277 #ifdef HAVE_HTML_WIDGET
3278 || libbalsa_html_can_search(w)
3279 #endif /* HAVE_HTML_WIDGET */
3280 )) {
3281 if (GTK_IS_TEXT_VIEW(w)) {
3282 GtkTextView *text_view = (GtkTextView *) w;
3283 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text_view);
3284
3285 gtk_text_buffer_get_start_iter(buffer, &bm->find_iter);
3286 }
3287
3288 bm->find_forward = TRUE;
3289 gtk_entry_set_text(GTK_ENTRY(bm->find_entry), "");
3290 g_signal_connect_swapped(gtk_widget_get_toplevel(GTK_WIDGET(bm)),
3291 "key-press-event",
3292 G_CALLBACK(bm_find_pass_to_entry), bm);
3293
3294 bm_find_set_status(bm, BM_FIND_STATUS_INIT);
3295
3296 gtk_widget_show_all(bm->find_bar);
3297 if (gtk_widget_get_window(bm->find_entry))
3298 gtk_widget_grab_focus(bm->find_entry);
3299 }
3300 }
3301