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-mime-widget-image.h"
26
27 #include <string.h>
28 #include "balsa-icons.h"
29 #include "mime-stream-shared.h"
30 #include "html.h"
31 #include <glib/gi18n.h>
32 #include "balsa-mime-widget-message.h"
33 #include "balsa-mime-widget-multipart.h"
34 #include "balsa-mime-widget-text.h"
35 #include "balsa-mime-widget-vcalendar.h"
36 #include "balsa-mime-widget-callbacks.h"
37 #include "balsa-mime-widget-crypto.h"
38 #include "balsa-mime-widget.h"
39
40
41 /* object related functions */
42 static void balsa_mime_widget_init (GTypeInstance *instance, gpointer g_class);
43 static void balsa_mime_widget_class_init(BalsaMimeWidgetClass * klass);
44
45
46 /* fall-back widget (unknown/unsupported mime type) */
47 static BalsaMimeWidget *balsa_mime_widget_new_unknown(BalsaMessage * bm,
48 LibBalsaMessageBody *
49 mime_body,
50 const gchar *
51 content_type);
52
53 static void vadj_change_cb(GtkAdjustment *vadj, GtkWidget *widget);
54
55
56 static GObjectClass *parent_class = NULL;
57
58
59 GType
balsa_mime_widget_get_type()60 balsa_mime_widget_get_type()
61 {
62 static GType balsa_mime_widget_type = 0;
63
64 if (!balsa_mime_widget_type) {
65 static const GTypeInfo balsa_mime_widget_info = {
66 sizeof(BalsaMimeWidgetClass),
67 NULL, /* base_init */
68 NULL, /* base_finalize */
69 (GClassInitFunc) balsa_mime_widget_class_init,
70 NULL, /* class_finalize */
71 NULL, /* class_data */
72 sizeof(BalsaMimeWidget),
73 0, /* n_preallocs */
74 (GInstanceInitFunc) balsa_mime_widget_init
75 };
76
77 balsa_mime_widget_type =
78 g_type_register_static(G_TYPE_OBJECT, "BalsaMimeWidget",
79 &balsa_mime_widget_info, 0);
80 }
81
82 return balsa_mime_widget_type;
83 }
84
85
86 static void
balsa_mime_widget_init(GTypeInstance * instance,gpointer g_class)87 balsa_mime_widget_init (GTypeInstance *instance, gpointer g_class)
88 {
89 BalsaMimeWidget *self = (BalsaMimeWidget *)instance;
90
91 self->widget = NULL;
92 self->container = NULL;
93 }
94
95
96 static void
balsa_mime_widget_class_init(BalsaMimeWidgetClass * klass)97 balsa_mime_widget_class_init(BalsaMimeWidgetClass * klass)
98 {
99 GObjectClass *object_class = G_OBJECT_CLASS (klass);
100
101 parent_class = g_type_class_ref(G_TYPE_OBJECT);
102 object_class->finalize = balsa_mime_widget_destroy;
103 }
104
105
106 /* wildcard defined whether the mime_type matches all subtypes */
107 typedef struct _mime_delegate_t {
108 gboolean wildcard;
109 const gchar *mime_type;
110 BalsaMimeWidget *(*handler) (BalsaMessage * bm,
111 LibBalsaMessageBody * mime_body,
112 const gchar * content_type,
113 gpointer data);
114 } mime_delegate_t;
115
116 static mime_delegate_t mime_delegate[] =
117 { {FALSE, "message/delivery-status", balsa_mime_widget_new_text},
118 {TRUE, "message/", balsa_mime_widget_new_message},
119 {FALSE, "text/calendar", balsa_mime_widget_new_vcalendar},
120 {TRUE, "text/", balsa_mime_widget_new_text},
121 {TRUE, "multipart/", balsa_mime_widget_new_multipart},
122 {TRUE, "image/", balsa_mime_widget_new_image},
123 #ifdef HAVE_GPGME
124 {FALSE, "application/pgp-signature", balsa_mime_widget_new_signature},
125 {FALSE, "application/pkcs7-signature", balsa_mime_widget_new_signature},
126 {FALSE, "application/x-pkcs7-signature", balsa_mime_widget_new_signature},
127 #endif
128 {FALSE, NULL, NULL}
129 };
130
131
132 BalsaMimeWidget *
balsa_mime_widget_new(BalsaMessage * bm,LibBalsaMessageBody * mime_body,gpointer data)133 balsa_mime_widget_new(BalsaMessage * bm, LibBalsaMessageBody * mime_body, gpointer data)
134 {
135 BalsaMimeWidget *mw = NULL;
136 gchar *content_type;
137 mime_delegate_t *delegate;
138
139 g_return_val_if_fail(bm != NULL, NULL);
140 g_return_val_if_fail(mime_body != NULL, NULL);
141
142 /* determine the content type of the passed MIME body */
143 content_type = libbalsa_message_body_get_mime_type(mime_body);
144 delegate = mime_delegate;
145 while (delegate->handler &&
146 ((delegate->wildcard &&
147 g_ascii_strncasecmp(delegate->mime_type, content_type,
148 strlen(delegate->mime_type))) ||
149 (!delegate->wildcard &&
150 g_ascii_strcasecmp(delegate->mime_type, content_type))))
151 delegate++;
152
153 if (delegate->handler)
154 mw = (delegate->handler) (bm, mime_body, content_type, data);
155 /* fall back to default if no handler is present */
156 if (!mw)
157 mw = balsa_mime_widget_new_unknown(bm, mime_body, content_type);
158
159 if (mw) {
160 if (mw->widget) {
161 g_signal_connect(G_OBJECT(mw->widget), "focus_in_event",
162 G_CALLBACK(balsa_mime_widget_limit_focus),
163 (gpointer) bm);
164 g_signal_connect(G_OBJECT(mw->widget), "focus_out_event",
165 G_CALLBACK(balsa_mime_widget_unlimit_focus),
166 (gpointer) bm);
167 #ifdef HAVE_GPGME
168 if (mime_body->sig_info &&
169 g_ascii_strcasecmp("application/pgp-signature", content_type) &&
170 g_ascii_strcasecmp("application/pkcs7-signature", content_type) &&
171 g_ascii_strcasecmp("application/x-pkcs7-signature", content_type)) {
172 GtkWidget * signature =
173 balsa_mime_widget_signature_widget(mime_body, content_type);
174 mw->widget = balsa_mime_widget_crypto_frame(mime_body, mw->widget,
175 mime_body->was_encrypted,
176 FALSE, signature);
177 } else if (mime_body->was_encrypted &&
178 g_ascii_strcasecmp("multipart/signed", content_type)) {
179 mw->widget = balsa_mime_widget_crypto_frame(mime_body, mw->widget,
180 TRUE, TRUE, NULL);
181 }
182 #endif
183 g_object_ref_sink(mw->widget);
184
185 if (GTK_IS_LAYOUT(mw->widget)) {
186 GtkAdjustment *vadj;
187
188 g_object_get(G_OBJECT(mw->widget), "vadjustment", &vadj,
189 NULL);
190 g_signal_connect(vadj, "changed",
191 G_CALLBACK(vadj_change_cb), mw->widget);
192 }
193
194 gtk_widget_show_all(mw->widget);
195 }
196 }
197 g_free(content_type);
198
199 return mw;
200 }
201
202
203 void
balsa_mime_widget_destroy(GObject * object)204 balsa_mime_widget_destroy(GObject * object)
205 {
206 BalsaMimeWidget * mime_widget = BALSA_MIME_WIDGET(object);
207
208 if (mime_widget->container && mime_widget->container != mime_widget->widget)
209 gtk_widget_destroy(mime_widget->container);
210 mime_widget->container = NULL;
211 if (mime_widget->widget) {
212 g_object_unref(mime_widget->widget);
213 mime_widget->widget = NULL;
214 }
215
216 G_OBJECT_CLASS(parent_class)->finalize(object);
217 }
218
219
220 static BalsaMimeWidget *
balsa_mime_widget_new_unknown(BalsaMessage * bm,LibBalsaMessageBody * mime_body,const gchar * content_type)221 balsa_mime_widget_new_unknown(BalsaMessage * bm,
222 LibBalsaMessageBody * mime_body,
223 const gchar * content_type)
224 {
225 GtkWidget *hbox;
226 GtkWidget *button = NULL;
227 gchar *msg;
228 GtkWidget *msg_label;
229 gchar *content_desc;
230 BalsaMimeWidget *mw;
231 gchar *use_content_type;
232
233 g_return_val_if_fail(mime_body, NULL);
234 mw = g_object_new(BALSA_TYPE_MIME_WIDGET, NULL);
235
236 mw->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, BMW_VBOX_SPACE);
237 gtk_container_set_border_width(GTK_CONTAINER(mw->widget),
238 BMW_CONTAINER_BORDER);
239
240 if (mime_body->filename) {
241 msg = g_strdup_printf(_("File name: %s"), mime_body->filename);
242 gtk_box_pack_start(GTK_BOX(mw->widget), gtk_label_new(msg), FALSE,
243 FALSE, 0);
244 g_free(msg);
245 }
246
247 /* guess content_type if not specified or if generic app/octet-stream */
248 /* on local mailboxes only, to avoid possibly long downloads */
249 if ((content_type == NULL ||
250 g_ascii_strcasecmp(content_type, "application/octet-stream") == 0)
251 && LIBBALSA_IS_MAILBOX_LOCAL(mime_body->message->mailbox)) {
252 GError *err = NULL;
253 gpointer buffer;
254 GMimeStream *stream =
255 libbalsa_message_body_get_stream(mime_body, &err);
256 if(!stream) {
257 libbalsa_information(LIBBALSA_INFORMATION_ERROR,
258 _("Error reading message part: %s"),
259 err ? err->message : "Unknown error");
260 g_clear_error(&err);
261 use_content_type = g_strdup(content_type);
262 } else {
263 ssize_t length = 1024 /* g_mime_stream_length(stream) */ ;
264 ssize_t size;
265
266 buffer = g_malloc(length);
267 libbalsa_mime_stream_shared_lock(stream);
268 size = g_mime_stream_read(stream, buffer, length);
269 libbalsa_mime_stream_shared_unlock(stream);
270 g_object_unref(stream);
271 use_content_type = libbalsa_vfs_content_type_of_buffer(buffer, size);
272 if (g_ascii_strncasecmp(use_content_type, "text", 4) == 0
273 && libbalsa_text_attr_string(buffer) & LIBBALSA_TEXT_HI_BIT) {
274 /* Hmmm...better stick with application/octet-stream. */
275 g_free(use_content_type);
276 use_content_type = g_strdup("application/octet-stream");
277 }
278 g_free(buffer);
279 }
280 } else
281 use_content_type = g_strdup(content_type);
282
283 content_desc = libbalsa_vfs_content_description(use_content_type);
284 if (content_desc) {
285 msg = g_strdup_printf(_("Type: %s (%s)"), content_desc,
286 use_content_type);
287 g_free(content_desc);
288 } else
289 msg = g_strdup_printf(_("Content Type: %s"), use_content_type);
290 msg_label = gtk_label_new(msg);
291 g_free(msg);
292 gtk_label_set_ellipsize(GTK_LABEL(msg_label), PANGO_ELLIPSIZE_END);
293 gtk_box_pack_start(GTK_BOX(mw->widget), msg_label, FALSE, FALSE, 0);
294
295 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, BMW_HBOX_SPACE);
296 gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);
297 if ((button = libbalsa_vfs_mime_button(mime_body, use_content_type,
298 G_CALLBACK(balsa_mime_widget_ctx_menu_cb),
299 (gpointer) mime_body)))
300 gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
301 else
302 gtk_box_pack_start(GTK_BOX(mw->widget),
303 gtk_label_new(_("No open or view action "
304 "defined for this content type")),
305 FALSE, FALSE, 0);
306 g_free(use_content_type);
307
308 button = gtk_button_new_with_mnemonic(_("S_ave part"));
309 gtk_box_pack_start(GTK_BOX(hbox), button, TRUE, TRUE, 0);
310 g_signal_connect(G_OBJECT(button), "clicked",
311 G_CALLBACK(balsa_mime_widget_ctx_menu_save),
312 (gpointer) mime_body);
313
314 gtk_box_pack_start(GTK_BOX(mw->widget), hbox, FALSE, FALSE, 0);
315
316 return mw;
317 }
318
319
320 static gint resize_idle_id;
321
322 static GtkWidget *old_widget, *new_widget;
323 static gdouble old_upper, new_upper;
324
325 static gboolean
resize_idle(GtkWidget * widget)326 resize_idle(GtkWidget * widget)
327 {
328 gdk_threads_enter();
329 resize_idle_id = 0;
330 gtk_widget_queue_resize(widget);
331 old_widget = new_widget;
332 old_upper = new_upper;
333 gdk_threads_leave();
334
335 return FALSE;
336 }
337
338
339 void
balsa_mime_widget_schedule_resize(GtkWidget * widget)340 balsa_mime_widget_schedule_resize(GtkWidget * widget)
341 {
342 g_object_ref(widget);
343 resize_idle_id = g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
344 (GSourceFunc) resize_idle,
345 widget, g_object_unref);
346 }
347
348
349 static void
vadj_change_cb(GtkAdjustment * vadj,GtkWidget * widget)350 vadj_change_cb(GtkAdjustment *vadj, GtkWidget *widget)
351 {
352 gdouble upper = gtk_adjustment_get_upper(vadj);
353
354 /* do nothing if it's the same widget and the height hasn't changed
355 *
356 * an HtmlView widget seems to grow by 4 pixels each time we resize
357 * it, whence the following unobvious test: */
358 if (widget == old_widget
359 && upper >= old_upper && upper <= old_upper + 4)
360 return;
361 new_widget = widget;
362 new_upper = upper;
363 if (resize_idle_id)
364 g_source_remove(resize_idle_id);
365 balsa_mime_widget_schedule_resize(widget);
366 }
367