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