ea0bd233851c7cfc6abaa7a2ec1a6cb124a0eaa2
[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.h>
42 #include <modest-text-utils.h>
43 #include "modest-msg-view.h"
44 #include "modest-tny-stream-gtkhtml.h"
45 #include <modest-mail-header-view.h>
46 #include <modest-attachments-view.h>
47
48
49 /* 'private'/'protected' functions */
50 static void     modest_msg_view_class_init   (ModestMsgViewClass *klass);
51 static void     modest_msg_view_init         (ModestMsgView *obj);
52 static void     modest_msg_view_finalize     (GObject *obj);
53
54 static void on_recpt_activated (ModestMailHeaderView *header_view, const gchar *address, ModestMsgView *msg_view);
55 static gboolean on_link_clicked (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view);
56 static gboolean on_url_requested (GtkWidget *widget, const gchar *uri, GtkHTMLStream *stream,
57                                   ModestMsgView *msg_view);
58 static gboolean on_link_hover (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view);
59
60 #define ATT_PREFIX "att:"
61
62 /* list my signals */
63 enum {
64         LINK_CLICKED_SIGNAL,
65         LINK_HOVER_SIGNAL,
66         ATTACHMENT_CLICKED_SIGNAL,
67         RECPT_ACTIVATED_SIGNAL,
68         LAST_SIGNAL
69 };
70
71 typedef struct _ModestMsgViewPrivate ModestMsgViewPrivate;
72 struct _ModestMsgViewPrivate {
73         GtkWidget   *gtkhtml;
74         GtkWidget   *mail_header_view;
75         GtkWidget   *attachments_view;
76
77         TnyMsg      *msg;
78
79         gulong  sig1, sig2, sig3;
80 };
81 #define MODEST_MSG_VIEW_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
82                                                  MODEST_TYPE_MSG_VIEW, \
83                                                  ModestMsgViewPrivate))
84 /* globals */
85 static GtkContainerClass *parent_class = NULL;
86
87 /* uncomment the following if you have defined any signals */
88 static guint signals[LAST_SIGNAL] = {0};
89
90 GType
91 modest_msg_view_get_type (void)
92 {
93         static GType my_type = 0;
94         if (!my_type) {
95                 static const GTypeInfo my_info = {
96                         sizeof(ModestMsgViewClass),
97                         NULL,           /* base init */
98                         NULL,           /* base finalize */
99                         (GClassInitFunc) modest_msg_view_class_init,
100                         NULL,           /* class finalize */
101                         NULL,           /* class data */
102                         sizeof(ModestMsgView),
103                         1,              /* n_preallocs */
104                         (GInstanceInitFunc) modest_msg_view_init,
105                         NULL
106                 };
107                 my_type = g_type_register_static (GTK_TYPE_VBOX,
108                                                   "ModestMsgView",
109                                                   &my_info, 0);
110         }
111         return my_type;
112 }
113
114 static void
115 modest_msg_view_class_init (ModestMsgViewClass *klass)
116 {
117         GObjectClass *gobject_class;
118         gobject_class = (GObjectClass*) klass;
119
120         parent_class            = g_type_class_peek_parent (klass);
121         gobject_class->finalize = modest_msg_view_finalize;
122
123         g_type_class_add_private (gobject_class, sizeof(ModestMsgViewPrivate));
124
125                 
126         signals[LINK_CLICKED_SIGNAL] =
127                 g_signal_new ("link_clicked",
128                               G_TYPE_FROM_CLASS (gobject_class),
129                               G_SIGNAL_RUN_FIRST,
130                               G_STRUCT_OFFSET(ModestMsgViewClass, link_clicked),
131                               NULL, NULL,
132                               g_cclosure_marshal_VOID__STRING,
133                               G_TYPE_NONE, 1, G_TYPE_STRING);
134         
135         signals[ATTACHMENT_CLICKED_SIGNAL] =
136                 g_signal_new ("attachment_clicked",
137                               G_TYPE_FROM_CLASS (gobject_class),
138                               G_SIGNAL_RUN_FIRST,
139                               G_STRUCT_OFFSET(ModestMsgViewClass, attachment_clicked),
140                               NULL, NULL,
141                               g_cclosure_marshal_VOID__POINTER,
142                               G_TYPE_NONE, 1, G_TYPE_INT);
143         
144         signals[LINK_HOVER_SIGNAL] =
145                 g_signal_new ("link_hover",
146                               G_TYPE_FROM_CLASS (gobject_class),
147                               G_SIGNAL_RUN_FIRST,
148                               G_STRUCT_OFFSET(ModestMsgViewClass, link_hover),
149                               NULL, NULL,
150                               g_cclosure_marshal_VOID__STRING,
151                               G_TYPE_NONE, 1, G_TYPE_STRING);
152
153         signals[RECPT_ACTIVATED_SIGNAL] =
154                 g_signal_new ("recpt_activated",
155                               G_TYPE_FROM_CLASS (gobject_class),
156                               G_SIGNAL_RUN_FIRST,
157                               G_STRUCT_OFFSET(ModestMsgViewClass, recpt_activated),
158                               NULL, NULL,
159                               g_cclosure_marshal_VOID__STRING,
160                               G_TYPE_NONE, 1, G_TYPE_STRING);
161 }
162
163 static void
164 modest_msg_view_init (ModestMsgView *obj)
165 {
166         ModestMsgViewPrivate *priv;
167         
168         priv = MODEST_MSG_VIEW_GET_PRIVATE(obj);
169
170         priv->msg                     = NULL;
171
172         priv->gtkhtml                 = gtk_html_new();
173         gtk_html_set_editable        (GTK_HTML(priv->gtkhtml), FALSE);
174         gtk_html_allow_selection     (GTK_HTML(priv->gtkhtml), TRUE);
175         gtk_html_set_caret_mode      (GTK_HTML(priv->gtkhtml), FALSE);
176         gtk_html_set_blocking        (GTK_HTML(priv->gtkhtml), FALSE);
177         gtk_html_set_images_blocking (GTK_HTML(priv->gtkhtml), FALSE);
178
179         priv->mail_header_view        = GTK_WIDGET(modest_mail_header_view_new ());
180         gtk_widget_set_no_show_all (priv->mail_header_view, TRUE);
181
182         priv->attachments_view        = GTK_WIDGET(modest_attachments_view_new (NULL));
183         gtk_widget_set_no_show_all (priv->attachments_view, TRUE);
184         
185         priv->sig1 = g_signal_connect (G_OBJECT(priv->gtkhtml), "link_clicked",
186                                        G_CALLBACK(on_link_clicked), obj);
187         priv->sig2 = g_signal_connect (G_OBJECT(priv->gtkhtml), "url_requested",
188                                        G_CALLBACK(on_url_requested), obj);
189         priv->sig3 = g_signal_connect (G_OBJECT(priv->gtkhtml), "on_url",
190                                        G_CALLBACK(on_link_hover), obj);
191         
192         g_signal_connect (G_OBJECT (priv->mail_header_view), "recpt-activated", 
193                           G_CALLBACK (on_recpt_activated), obj);
194 }
195         
196
197 static void
198 modest_msg_view_finalize (GObject *obj)
199 {       
200         ModestMsgViewPrivate *priv;
201         priv = MODEST_MSG_VIEW_GET_PRIVATE (obj);
202
203         if (priv->msg) {
204                 g_object_unref (G_OBJECT(priv->msg));
205                 priv->msg = NULL;
206         }
207         
208         /* we cannot disconnect sigs, because priv->gtkhtml is
209          * already dead */
210         
211         priv->gtkhtml = NULL;
212         priv->attachments_view = NULL;
213         
214         G_OBJECT_CLASS(parent_class)->finalize (obj);           
215 }
216
217
218 GtkWidget*
219 modest_msg_view_new (TnyMsg *msg)
220 {
221         GObject *obj;
222         ModestMsgView* self;
223         ModestMsgViewPrivate *priv;
224         GtkWidget *scrolled_window;
225         
226         obj  = G_OBJECT(g_object_new(MODEST_TYPE_MSG_VIEW, NULL));
227         self = MODEST_MSG_VIEW(obj);
228         priv = MODEST_MSG_VIEW_GET_PRIVATE (self);
229
230         gtk_box_set_spacing (GTK_BOX (self), 0);
231         gtk_box_set_homogeneous (GTK_BOX (self), FALSE);
232
233         if (priv->mail_header_view)
234                 gtk_box_pack_start (GTK_BOX(self), priv->mail_header_view, FALSE, FALSE, 0);
235         
236         if (priv->attachments_view)
237                 gtk_box_pack_start (GTK_BOX(self), priv->attachments_view, FALSE, FALSE, 0);
238
239         if (priv->gtkhtml) {
240                 scrolled_window = gtk_scrolled_window_new (NULL, NULL);
241                 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled_window),
242                                                GTK_POLICY_AUTOMATIC,
243                                                GTK_POLICY_AUTOMATIC);
244                 gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW(scrolled_window),
245                                                      GTK_SHADOW_IN);
246                 gtk_container_add (GTK_CONTAINER (scrolled_window), priv->gtkhtml);
247                 gtk_box_pack_start (GTK_BOX(self), scrolled_window, TRUE, TRUE, 0);
248         }
249
250         modest_msg_view_set_message (self, msg);
251         
252         return GTK_WIDGET(self);
253 }
254
255 static void
256 on_recpt_activated (ModestMailHeaderView *header_view, 
257                     const gchar *address,
258                     ModestMsgView * view)
259 {
260         g_signal_emit (G_OBJECT (view), signals[RECPT_ACTIVATED_SIGNAL], 0, address);
261 }
262
263 static gboolean
264 on_link_clicked (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view)
265 {
266         int index;
267
268         g_return_val_if_fail (msg_view, FALSE);
269         
270         /* is it an attachment? */
271         if (g_str_has_prefix(uri, ATT_PREFIX)) {
272
273                 index = atoi (uri + strlen(ATT_PREFIX));
274                 
275                 if (index == 0) {
276                         /* index is 1-based, so 0 indicates an error */
277                         g_printerr ("modest: invalid attachment id: %s\n", uri);
278                         return FALSE;
279                 }
280
281                 g_signal_emit (G_OBJECT(msg_view), signals[ATTACHMENT_CLICKED_SIGNAL],
282                                0, index);
283                 return FALSE;
284         }
285
286         g_signal_emit (G_OBJECT(msg_view), signals[LINK_CLICKED_SIGNAL],
287                        0, uri);
288
289         return FALSE;
290 }
291
292
293
294 static gboolean
295 on_link_hover (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view)
296 {
297         if (uri && g_str_has_prefix (uri, ATT_PREFIX))
298                 return FALSE;
299
300         g_signal_emit (G_OBJECT(msg_view), signals[LINK_HOVER_SIGNAL],
301                        0, uri);
302
303         return FALSE;
304 }
305
306
307
308 static TnyMimePart *
309 find_cid_image (TnyMsg *msg, const gchar *cid)
310 {
311         TnyMimePart *part = NULL;
312         TnyList *parts;
313         TnyIterator *iter;
314         
315         g_return_val_if_fail (msg, NULL);
316         g_return_val_if_fail (cid, NULL);
317         
318         parts  = TNY_LIST (tny_simple_list_new());
319
320         tny_mime_part_get_parts (TNY_MIME_PART (msg), parts); 
321         iter   = tny_list_create_iterator (parts);
322         
323         while (!tny_iterator_is_done(iter)) {
324                 const gchar *part_cid;
325                 part = TNY_MIME_PART(tny_iterator_get_current(iter));
326                 part_cid = tny_mime_part_get_content_id (part);
327
328                 if (part_cid && strcmp (cid, part_cid) == 0)
329                         break;
330
331                 g_object_unref (G_OBJECT(part));
332         
333                 part = NULL;
334                 tny_iterator_next (iter);
335         }
336         
337         g_object_unref (G_OBJECT(iter));        
338         g_object_unref (G_OBJECT(parts));
339         
340         return part;
341 }
342
343
344 static gboolean
345 on_url_requested (GtkWidget *widget, const gchar *uri,
346                   GtkHTMLStream *stream, ModestMsgView *msg_view)
347 {
348         ModestMsgViewPrivate *priv;
349         priv = MODEST_MSG_VIEW_GET_PRIVATE (msg_view);
350         
351         if (g_str_has_prefix (uri, "cid:")) {
352                 /* +4 ==> skip "cid:" */
353                 TnyMimePart *part = find_cid_image (priv->msg, uri + 4);
354                 if (!part) {
355                         g_printerr ("modest: '%s' not found\n", uri + 4);
356                         gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
357                 } else {
358                         TnyStream *tny_stream =
359                                 TNY_STREAM(modest_tny_stream_gtkhtml_new(stream));
360                         tny_mime_part_decode_to_stream ((TnyMimePart*)part,
361                                                                   tny_stream);
362                         gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
363         
364                         g_object_unref (G_OBJECT(tny_stream));
365                         g_object_unref (G_OBJECT(part));
366                 }
367         }
368
369         return TRUE;
370 }
371
372 static gboolean
373 set_html_message (ModestMsgView *self, TnyMimePart *tny_body, TnyMsg *msg)
374 {
375         GtkHTMLStream *gtkhtml_stream;
376         TnyStream *tny_stream;  
377         ModestMsgViewPrivate *priv;
378         
379         g_return_val_if_fail (self, FALSE);
380         g_return_val_if_fail (tny_body, FALSE);
381         
382         priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
383
384         gtkhtml_stream = gtk_html_begin(GTK_HTML(priv->gtkhtml));
385
386         tny_stream     = TNY_STREAM(modest_tny_stream_gtkhtml_new (gtkhtml_stream));
387         tny_stream_reset (tny_stream);
388
389         tny_mime_part_decode_to_stream ((TnyMimePart*)tny_body, tny_stream);
390         g_object_unref (G_OBJECT(tny_stream));
391         
392         gtk_html_stream_destroy (gtkhtml_stream);
393         
394         return TRUE;
395 }
396
397
398 /* FIXME: this is a hack --> we use the tny_text_buffer_stream to
399  * get the message text, then write to gtkhtml 'by hand' */
400 static gboolean
401 set_text_message (ModestMsgView *self, TnyMimePart *tny_body, TnyMsg *msg)
402 {
403         GtkTextBuffer *buf;
404         GtkTextIter begin, end;
405         TnyStream* txt_stream, *tny_stream;
406         GtkHTMLStream *gtkhtml_stream;
407         gchar *txt;
408         ModestMsgViewPrivate *priv;
409                 
410         g_return_val_if_fail (self, FALSE);
411         g_return_val_if_fail (tny_body, FALSE);
412
413         priv           = MODEST_MSG_VIEW_GET_PRIVATE(self);
414         
415         buf            = gtk_text_buffer_new (NULL);
416         txt_stream     = TNY_STREAM(tny_gtk_text_buffer_stream_new (buf));
417                 
418         tny_stream_reset (txt_stream);
419
420         gtkhtml_stream = gtk_html_begin(GTK_HTML(priv->gtkhtml)); 
421         tny_stream =  TNY_STREAM(modest_tny_stream_gtkhtml_new (gtkhtml_stream));
422         
423         // FIXME: tinymail
424         tny_mime_part_decode_to_stream ((TnyMimePart*)tny_body, txt_stream);
425         tny_stream_reset (txt_stream);          
426         
427         gtk_text_buffer_get_bounds (buf, &begin, &end);
428         txt = gtk_text_buffer_get_text (buf, &begin, &end, FALSE);
429         if (txt) {
430                 gchar *html = modest_text_utils_convert_to_html (txt);
431                 tny_stream_write (tny_stream, html, strlen(html));
432                 tny_stream_reset (tny_stream);
433                 g_free (txt);
434                 g_free (html);
435         }
436         
437         g_object_unref (G_OBJECT(tny_stream));
438         g_object_unref (G_OBJECT(txt_stream));
439         g_object_unref (G_OBJECT(buf));
440         
441         gtk_html_stream_destroy (gtkhtml_stream);
442         
443         return TRUE;
444 }
445
446
447 static gboolean
448 set_empty_message (ModestMsgView *self)
449 {
450         ModestMsgViewPrivate *priv;
451         
452         g_return_val_if_fail (self, FALSE);
453         priv           = MODEST_MSG_VIEW_GET_PRIVATE(self);
454
455         gtk_html_load_from_string (GTK_HTML(priv->gtkhtml),
456                                    "", 1);
457         
458         return TRUE;
459 }
460
461
462 void
463 modest_msg_view_set_message (ModestMsgView *self, TnyMsg *msg)
464 {
465         TnyMimePart *body;
466         ModestMsgViewPrivate *priv;
467         TnyHeader *header;
468         
469         g_return_if_fail (self);
470         
471         priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
472         gtk_widget_set_no_show_all (priv->mail_header_view, FALSE);
473
474         if (msg != priv->msg) {
475                 if (priv->msg)
476                         g_object_unref (G_OBJECT(priv->msg));
477                 if (msg)
478                         g_object_ref   (G_OBJECT(msg));
479                 priv->msg = msg;
480         }
481         
482         if (!msg) {
483                 tny_header_view_clear (TNY_HEADER_VIEW (priv->mail_header_view));
484                 gtk_widget_hide_all (priv->mail_header_view);
485                 gtk_widget_set_no_show_all (priv->mail_header_view, TRUE);
486                 set_empty_message (self);
487                 return;
488         }
489
490         header = tny_msg_get_header (msg);
491         tny_header_view_set_header (TNY_HEADER_VIEW (priv->mail_header_view), header);
492         g_object_unref (header);
493
494         modest_attachments_view_set_message (MODEST_ATTACHMENTS_VIEW(priv->attachments_view),
495                                              msg);
496         
497         body = modest_tny_msg_find_body_part (msg,TRUE);
498         if (body) {
499                 if (tny_mime_part_content_type_is (body, "text/html"))
500                         set_html_message (self, body, msg);
501                 else
502                         set_text_message (self, body, msg);
503         } else 
504                 set_empty_message (self);
505         
506         gtk_widget_show_all (priv->mail_header_view);
507         gtk_widget_set_no_show_all (priv->mail_header_view, TRUE);
508 }
509
510
511 TnyMsg*
512 modest_msg_view_get_message (ModestMsgView *self)
513 {
514         g_return_val_if_fail (self, NULL);
515         
516         return MODEST_MSG_VIEW_GET_PRIVATE(self)->msg;
517 }