77bbc2b5818621115145e605227005e5f1634230
[modest] / src / widgets / modest-msg-view.c
1 /* Copyright (c) 2006, Nokia Corporation
2  * All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are
6  * met:
7  *
8  * * Redistributions of source code must retain the above copyright
9  *   notice, this list of conditions and the following disclaimer.
10  * * Redistributions in binary form must reproduce the above copyright
11  *   notice, this list of conditions and the following disclaimer in the
12  *   documentation and/or other materials provided with the distribution.
13  * * Neither the name of the Nokia Corporation nor the names of its
14  *   contributors may be used to endorse or promote products derived from
15  *   this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
18  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19  * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20  * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
21  * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28  */
29
30 #include <tny-gtk-text-buffer-stream.h>
31 #include <string.h>
32 #include <regex.h>
33 #include <ctype.h>
34 #include <stdlib.h>
35 #include <glib/gi18n.h>
36 #include <gtkhtml/gtkhtml.h>
37 #include <gtkhtml/gtkhtml-stream.h>
38 #include <tny-list.h>
39 #include <tny-simple-list.h>
40
41 #include <modest-tny-msg-actions.h>
42 #include <modest-text-utils.h>
43 #include "modest-msg-view.h"
44 #include "modest-tny-stream-gtkhtml.h"
45
46
47 /* 'private'/'protected' functions */
48 static void     modest_msg_view_class_init   (ModestMsgViewClass *klass);
49 static void     modest_msg_view_init         (ModestMsgView *obj);
50 static void     modest_msg_view_finalize     (GObject *obj);
51
52 static gboolean on_link_clicked (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view);
53 static gboolean on_url_requested (GtkWidget *widget, const gchar *uri, GtkHTMLStream *stream,
54                                   ModestMsgView *msg_view);
55 static gboolean on_link_hover (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view);
56
57 #define ATT_PREFIX "att:"
58
59 /* list my signals */
60 enum {
61         LINK_CLICKED_SIGNAL,
62         LINK_HOVER_SIGNAL,
63         ATTACHMENT_CLICKED_SIGNAL,
64         LAST_SIGNAL
65 };
66
67 typedef struct _ModestMsgViewPrivate ModestMsgViewPrivate;
68 struct _ModestMsgViewPrivate {
69
70         GtkWidget   *gtkhtml;
71         TnyMsg      *msg;
72
73         gulong  sig1, sig2, sig3;
74 };
75 #define MODEST_MSG_VIEW_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
76                                                  MODEST_TYPE_MSG_VIEW, \
77                                                  ModestMsgViewPrivate))
78 /* globals */
79 static GtkContainerClass *parent_class = NULL;
80
81 /* uncomment the following if you have defined any signals */
82 static guint signals[LAST_SIGNAL] = {0};
83
84 GType
85 modest_msg_view_get_type (void)
86 {
87         static GType my_type = 0;
88         if (!my_type) {
89                 static const GTypeInfo my_info = {
90                         sizeof(ModestMsgViewClass),
91                         NULL,           /* base init */
92                         NULL,           /* base finalize */
93                         (GClassInitFunc) modest_msg_view_class_init,
94                         NULL,           /* class finalize */
95                         NULL,           /* class data */
96                         sizeof(ModestMsgView),
97                         1,              /* n_preallocs */
98                         (GInstanceInitFunc) modest_msg_view_init,
99                         NULL
100                 };
101                 my_type = g_type_register_static (GTK_TYPE_SCROLLED_WINDOW,
102                                                   "ModestMsgView",
103                                                   &my_info, 0);
104         }
105         return my_type;
106 }
107
108 static void
109 modest_msg_view_class_init (ModestMsgViewClass *klass)
110 {
111         GObjectClass *gobject_class;
112         gobject_class = (GObjectClass*) klass;
113
114         parent_class            = g_type_class_peek_parent (klass);
115         gobject_class->finalize = modest_msg_view_finalize;
116
117         g_type_class_add_private (gobject_class, sizeof(ModestMsgViewPrivate));
118
119                 
120         signals[LINK_CLICKED_SIGNAL] =
121                 g_signal_new ("link_clicked",
122                               G_TYPE_FROM_CLASS (gobject_class),
123                               G_SIGNAL_RUN_FIRST,
124                               G_STRUCT_OFFSET(ModestMsgViewClass, link_clicked),
125                               NULL, NULL,
126                               g_cclosure_marshal_VOID__STRING,
127                               G_TYPE_NONE, 1, G_TYPE_STRING);
128         
129         signals[ATTACHMENT_CLICKED_SIGNAL] =
130                 g_signal_new ("attachment_clicked",
131                               G_TYPE_FROM_CLASS (gobject_class),
132                               G_SIGNAL_RUN_FIRST,
133                               G_STRUCT_OFFSET(ModestMsgViewClass, attachment_clicked),
134                               NULL, NULL,
135                               g_cclosure_marshal_VOID__POINTER,
136                               G_TYPE_NONE, 1, G_TYPE_INT);
137         
138         signals[LINK_HOVER_SIGNAL] =
139                 g_signal_new ("link_hover",
140                               G_TYPE_FROM_CLASS (gobject_class),
141                               G_SIGNAL_RUN_FIRST,
142                               G_STRUCT_OFFSET(ModestMsgViewClass, link_hover),
143                               NULL, NULL,
144                               g_cclosure_marshal_VOID__STRING,
145                               G_TYPE_NONE, 1, G_TYPE_STRING);
146 }
147
148 static void
149 modest_msg_view_init (ModestMsgView *obj)
150 {
151         ModestMsgViewPrivate *priv;
152         
153         priv = MODEST_MSG_VIEW_GET_PRIVATE(obj);
154
155         priv->msg                     = NULL;
156         priv->gtkhtml                 = gtk_html_new();
157         
158         gtk_html_set_editable        (GTK_HTML(priv->gtkhtml), FALSE);
159         gtk_html_allow_selection     (GTK_HTML(priv->gtkhtml), TRUE);
160         gtk_html_set_caret_mode      (GTK_HTML(priv->gtkhtml), FALSE);
161         gtk_html_set_blocking        (GTK_HTML(priv->gtkhtml), FALSE);
162         gtk_html_set_images_blocking (GTK_HTML(priv->gtkhtml), FALSE);
163
164         priv->sig1 = g_signal_connect (G_OBJECT(priv->gtkhtml), "link_clicked",
165                                        G_CALLBACK(on_link_clicked), obj);
166         priv->sig2 = g_signal_connect (G_OBJECT(priv->gtkhtml), "url_requested",
167                                        G_CALLBACK(on_url_requested), obj);
168         priv->sig3 = g_signal_connect (G_OBJECT(priv->gtkhtml), "on_url",
169                                        G_CALLBACK(on_link_hover), obj);
170 }
171         
172
173 static void
174 modest_msg_view_finalize (GObject *obj)
175 {       
176         ModestMsgViewPrivate *priv;
177         priv = MODEST_MSG_VIEW_GET_PRIVATE (obj);
178
179         if (priv->msg) {
180                 g_object_unref (G_OBJECT(priv->msg));
181                 priv->msg = NULL;
182         }
183         
184         /* we cannot disconnect sigs, because priv->gtkhtml is
185          * already dead */
186         
187         priv->gtkhtml = NULL;
188         
189         G_OBJECT_CLASS(parent_class)->finalize (obj);           
190 }
191
192
193 GtkWidget*
194 modest_msg_view_new (TnyMsg *msg)
195 {
196         GObject *obj;
197         ModestMsgView* self;
198         ModestMsgViewPrivate *priv;
199         
200         obj  = G_OBJECT(g_object_new(MODEST_TYPE_MSG_VIEW, NULL));
201         self = MODEST_MSG_VIEW(obj);
202         priv = MODEST_MSG_VIEW_GET_PRIVATE (self);
203
204         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self),
205                                        GTK_POLICY_AUTOMATIC,
206                                        GTK_POLICY_AUTOMATIC);
207
208         if (priv->gtkhtml) 
209                 gtk_container_add (GTK_CONTAINER(obj), priv->gtkhtml);
210         
211         if (msg)
212                 modest_msg_view_set_message (self, msg);
213         
214         return GTK_WIDGET(self);
215 }
216
217
218 static gboolean
219 on_link_clicked (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view)
220 {
221         int index;
222
223         g_return_val_if_fail (msg_view, FALSE);
224         
225         /* is it an attachment? */
226         if (g_str_has_prefix(uri, ATT_PREFIX)) {
227
228                 index = atoi (uri + strlen(ATT_PREFIX));
229                 
230                 if (index == 0) {
231                         /* index is 1-based, so 0 indicates an error */
232                         g_printerr ("modest: invalid attachment id: %s\n", uri);
233                         return FALSE;
234                 }
235
236                 g_signal_emit (G_OBJECT(msg_view), signals[ATTACHMENT_CLICKED_SIGNAL],
237                                0, index);
238                 return FALSE;
239         }
240
241         g_signal_emit (G_OBJECT(msg_view), signals[LINK_CLICKED_SIGNAL],
242                        0, uri);
243
244         return FALSE;
245 }
246
247
248
249 static gboolean
250 on_link_hover (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view)
251 {
252         if (uri && g_str_has_prefix (uri, ATT_PREFIX))
253                 return FALSE;
254
255         g_signal_emit (G_OBJECT(msg_view), signals[LINK_HOVER_SIGNAL],
256                        0, uri);
257
258         return FALSE;
259 }
260
261
262
263 static TnyMimePart *
264 find_cid_image (TnyMsg *msg, const gchar *cid)
265 {
266         TnyMimePart *part = NULL;
267         TnyList *parts;
268         TnyIterator *iter;
269         
270         g_return_val_if_fail (msg, NULL);
271         g_return_val_if_fail (cid, NULL);
272         
273         parts  = TNY_LIST (tny_simple_list_new());
274
275         tny_mime_part_get_parts (TNY_MIME_PART (msg), parts); 
276         iter   = tny_list_create_iterator (parts);
277         
278         while (!tny_iterator_is_done(iter)) {
279                 const gchar *part_cid;
280                 part = TNY_MIME_PART(tny_iterator_get_current(iter));
281                 part_cid = tny_mime_part_get_content_id (part);
282
283                 if (part_cid && strcmp (cid, part_cid) == 0)
284                         break;
285
286                 part = NULL;
287                 tny_iterator_next (iter);
288         }
289         
290         if (part)
291                 g_object_ref (G_OBJECT(part));
292
293         g_object_unref (G_OBJECT(iter));        
294         g_object_unref (G_OBJECT(parts));
295         
296         return part;
297 }
298
299
300 static gboolean
301 on_url_requested (GtkWidget *widget, const gchar *uri,
302                   GtkHTMLStream *stream,
303                   ModestMsgView *msg_view)
304 {
305         ModestMsgViewPrivate *priv;
306         priv = MODEST_MSG_VIEW_GET_PRIVATE (msg_view);
307         
308         if (g_str_has_prefix (uri, "cid:")) {
309                 /* +4 ==> skip "cid:" */
310                 TnyMimePart *part = find_cid_image (priv->msg, uri + 4);
311                 if (!part) {
312                         g_printerr ("modest: '%s' not found\n", uri + 4);
313                         gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
314                 } else {
315                         TnyStream *tny_stream =
316                                 TNY_STREAM(modest_tny_stream_gtkhtml_new(stream));
317                         tny_mime_part_decode_to_stream ((TnyMimePart*)part,
318                                                                   tny_stream);
319                         gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
320         
321                         g_object_unref (G_OBJECT(tny_stream));
322                         g_object_unref (G_OBJECT(part));
323                 }
324         }
325
326         return TRUE;
327 }
328
329
330 /* render the attachments as hyperlinks in html */
331 static gchar*
332 attachments_as_html (ModestMsgView *self, TnyMsg *msg)
333 {
334         ModestMsgViewPrivate *priv;
335         GString *appendix;
336         TnyList *parts;
337         TnyIterator *iter;
338         gchar *html;
339         int index = 0;
340         
341         if (!msg)
342                 return NULL;
343
344         priv  = MODEST_MSG_VIEW_GET_PRIVATE (self);
345
346         parts = TNY_LIST(tny_simple_list_new());
347         tny_mime_part_get_parts (TNY_MIME_PART (msg), parts);
348         iter  = tny_list_create_iterator (parts);
349         
350         appendix= g_string_new ("");
351         
352         while (!tny_iterator_is_done(iter)) {
353                 TnyMimePart *part;
354
355                 ++index; /* attachment numbers are 1-based */
356                 
357                 part = TNY_MIME_PART(tny_iterator_get_current (iter));
358
359                 if (tny_mime_part_is_attachment (part)) {
360
361                         const gchar *filename = tny_mime_part_get_filename(part);
362                         if (!filename)
363                                 filename = _("attachment");
364
365                         g_string_append_printf (appendix, "<a href=\"%s%d\">%s</a> \n",
366                                                 ATT_PREFIX, index, filename);                    
367                 }
368                 tny_iterator_next (iter);
369         }
370         g_object_unref (G_OBJECT(iter));
371         
372         if (appendix->len == 0) 
373                 return g_string_free (appendix, TRUE);
374
375         html = g_strdup_printf ("<strong>%s:</strong> %s\n<hr>",
376                                 _("Attachments"), appendix->str);                        
377         g_string_free (appendix, TRUE);
378         
379         return html;
380 }
381
382
383
384
385 static gboolean
386 set_html_message (ModestMsgView *self, TnyMimePart *tny_body, TnyMsg *msg)
387 {
388         gchar *html_attachments;
389         TnyStream *gtkhtml_stream;      
390         ModestMsgViewPrivate *priv;
391         
392         g_return_val_if_fail (self, FALSE);
393         g_return_val_if_fail (tny_body, FALSE);
394         
395         priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
396
397         gtkhtml_stream =
398                 TNY_STREAM(modest_tny_stream_gtkhtml_new
399                                  (gtk_html_begin(GTK_HTML(priv->gtkhtml))));
400         
401         tny_stream_reset (gtkhtml_stream);
402         
403         html_attachments = attachments_as_html(self, msg);
404         if (html_attachments) {
405                 tny_stream_write (gtkhtml_stream, html_attachments,
406                                         strlen(html_attachments));
407                 tny_stream_reset (gtkhtml_stream);
408                 g_free (html_attachments);
409         }
410
411         // FIXME: tinymail
412         tny_mime_part_decode_to_stream ((TnyMimePart*)tny_body,
413                                                   gtkhtml_stream);
414
415         g_object_unref (G_OBJECT(gtkhtml_stream));
416         
417         return TRUE;
418 }
419
420
421 /* FIXME: this is a hack --> we use the tny_text_buffer_stream to
422  * get the message text, then write to gtkhtml 'by hand' */
423 static gboolean
424 set_text_message (ModestMsgView *self, TnyMimePart *tny_body, TnyMsg *msg)
425 {
426         GtkTextBuffer *buf;
427         GtkTextIter begin, end;
428         TnyStream* txt_stream, *gtkhtml_stream;
429         gchar *txt, *html_attachments;
430         ModestMsgViewPrivate *priv;
431                 
432         g_return_val_if_fail (self, FALSE);
433         g_return_val_if_fail (tny_body, FALSE);
434
435         priv           = MODEST_MSG_VIEW_GET_PRIVATE(self);
436         
437         buf            = gtk_text_buffer_new (NULL);
438         txt_stream     = TNY_STREAM(tny_gtk_text_buffer_stream_new (buf));
439                 
440         tny_stream_reset (txt_stream);
441         
442         gtkhtml_stream =
443                 TNY_STREAM(modest_tny_stream_gtkhtml_new
444                                  (gtk_html_begin(GTK_HTML(priv->gtkhtml))));
445
446         html_attachments = attachments_as_html(self, msg);
447         if (html_attachments) {
448                 tny_stream_write (gtkhtml_stream, html_attachments,
449                                         strlen(html_attachments));
450                 tny_stream_reset (gtkhtml_stream);
451                 g_free (html_attachments);
452         }
453
454         // FIXME: tinymail
455         tny_mime_part_decode_to_stream ((TnyMimePart*)tny_body,
456                                                   txt_stream);
457         tny_stream_reset (txt_stream);          
458         
459         gtk_text_buffer_get_bounds (buf, &begin, &end);
460         txt = gtk_text_buffer_get_text (buf, &begin, &end, FALSE);
461         if (txt) {
462                 gchar *html = modest_text_utils_convert_to_html (txt);
463                 tny_stream_write (gtkhtml_stream, html, strlen(html));
464                 tny_stream_reset (gtkhtml_stream);
465                 g_free (txt);
466                 g_free (html);
467         }
468         
469         g_object_unref (G_OBJECT(gtkhtml_stream));
470         g_object_unref (G_OBJECT(txt_stream));
471         g_object_unref (G_OBJECT(buf));
472
473         return TRUE;
474 }
475
476
477 static gboolean
478 set_empty_message (ModestMsgView *self)
479 {
480         ModestMsgViewPrivate *priv;
481         
482         g_return_val_if_fail (self, FALSE);
483         priv           = MODEST_MSG_VIEW_GET_PRIVATE(self);
484
485         gtk_html_load_from_string (GTK_HTML(priv->gtkhtml),
486                                    "", 1);
487         
488         return TRUE;
489 }
490
491
492 void
493 modest_msg_view_set_message (ModestMsgView *self, TnyMsg *msg)
494 {
495         TnyMimePart *body;
496         ModestMsgViewPrivate *priv;
497
498         g_return_if_fail (self);
499         
500         priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
501
502
503         if (msg != priv->msg) {
504                 if (priv->msg)
505                         g_object_unref (G_OBJECT(priv->msg));
506                 if (msg)
507                         g_object_ref   (G_OBJECT(msg));
508                 priv->msg = msg;
509         }
510         
511         if (!msg) {
512                 set_empty_message (self);
513                 return;
514         }
515                 
516         body = modest_tny_msg_actions_find_body_part (msg, TRUE);
517         if (body) {
518                 if (tny_mime_part_content_type_is (body, "text/html"))
519                         set_html_message (self, body, msg);
520                 else
521                         set_text_message (self, body, msg);
522                 return;
523         } else 
524                 set_empty_message (self);
525 }