* all:
[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
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         GtkWidget   *gtkhtml;
70         TnyMsg      *msg;
71
72         gulong  sig1, sig2, sig3;
73 };
74 #define MODEST_MSG_VIEW_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
75                                                  MODEST_TYPE_MSG_VIEW, \
76                                                  ModestMsgViewPrivate))
77 /* globals */
78 static GtkContainerClass *parent_class = NULL;
79
80 /* uncomment the following if you have defined any signals */
81 static guint signals[LAST_SIGNAL] = {0};
82
83 GType
84 modest_msg_view_get_type (void)
85 {
86         static GType my_type = 0;
87         if (!my_type) {
88                 static const GTypeInfo my_info = {
89                         sizeof(ModestMsgViewClass),
90                         NULL,           /* base init */
91                         NULL,           /* base finalize */
92                         (GClassInitFunc) modest_msg_view_class_init,
93                         NULL,           /* class finalize */
94                         NULL,           /* class data */
95                         sizeof(ModestMsgView),
96                         1,              /* n_preallocs */
97                         (GInstanceInitFunc) modest_msg_view_init,
98                         NULL
99                 };
100                 my_type = g_type_register_static (GTK_TYPE_SCROLLED_WINDOW,
101                                                   "ModestMsgView",
102                                                   &my_info, 0);
103         }
104         return my_type;
105 }
106
107 static void
108 modest_msg_view_class_init (ModestMsgViewClass *klass)
109 {
110         GObjectClass *gobject_class;
111         gobject_class = (GObjectClass*) klass;
112
113         parent_class            = g_type_class_peek_parent (klass);
114         gobject_class->finalize = modest_msg_view_finalize;
115
116         g_type_class_add_private (gobject_class, sizeof(ModestMsgViewPrivate));
117
118                 
119         signals[LINK_CLICKED_SIGNAL] =
120                 g_signal_new ("link_clicked",
121                               G_TYPE_FROM_CLASS (gobject_class),
122                               G_SIGNAL_RUN_FIRST,
123                               G_STRUCT_OFFSET(ModestMsgViewClass, link_clicked),
124                               NULL, NULL,
125                               g_cclosure_marshal_VOID__STRING,
126                               G_TYPE_NONE, 1, G_TYPE_STRING);
127         
128         signals[ATTACHMENT_CLICKED_SIGNAL] =
129                 g_signal_new ("attachment_clicked",
130                               G_TYPE_FROM_CLASS (gobject_class),
131                               G_SIGNAL_RUN_FIRST,
132                               G_STRUCT_OFFSET(ModestMsgViewClass, attachment_clicked),
133                               NULL, NULL,
134                               g_cclosure_marshal_VOID__POINTER,
135                               G_TYPE_NONE, 1, G_TYPE_INT);
136         
137         signals[LINK_HOVER_SIGNAL] =
138                 g_signal_new ("link_hover",
139                               G_TYPE_FROM_CLASS (gobject_class),
140                               G_SIGNAL_RUN_FIRST,
141                               G_STRUCT_OFFSET(ModestMsgViewClass, link_hover),
142                               NULL, NULL,
143                               g_cclosure_marshal_VOID__STRING,
144                               G_TYPE_NONE, 1, G_TYPE_STRING);
145 }
146
147 static void
148 modest_msg_view_init (ModestMsgView *obj)
149 {
150         ModestMsgViewPrivate *priv;
151         
152         priv = MODEST_MSG_VIEW_GET_PRIVATE(obj);
153
154         priv->msg                     = NULL;
155         priv->gtkhtml                 = gtk_html_new();
156         
157         gtk_html_set_editable        (GTK_HTML(priv->gtkhtml), FALSE);
158         gtk_html_allow_selection     (GTK_HTML(priv->gtkhtml), TRUE);
159         gtk_html_set_caret_mode      (GTK_HTML(priv->gtkhtml), FALSE);
160         gtk_html_set_blocking        (GTK_HTML(priv->gtkhtml), FALSE);
161         gtk_html_set_images_blocking (GTK_HTML(priv->gtkhtml), FALSE);
162
163         priv->sig1 = g_signal_connect (G_OBJECT(priv->gtkhtml), "link_clicked",
164                                        G_CALLBACK(on_link_clicked), obj);
165         priv->sig2 = g_signal_connect (G_OBJECT(priv->gtkhtml), "url_requested",
166                                        G_CALLBACK(on_url_requested), obj);
167         priv->sig3 = g_signal_connect (G_OBJECT(priv->gtkhtml), "on_url",
168                                        G_CALLBACK(on_link_hover), obj);
169 }
170         
171
172 static void
173 modest_msg_view_finalize (GObject *obj)
174 {       
175         ModestMsgViewPrivate *priv;
176         priv = MODEST_MSG_VIEW_GET_PRIVATE (obj);
177
178         if (priv->msg) {
179                 g_object_unref (G_OBJECT(priv->msg));
180                 priv->msg = NULL;
181         }
182         
183         /* we cannot disconnect sigs, because priv->gtkhtml is
184          * already dead */
185         
186         priv->gtkhtml = NULL;
187         
188         G_OBJECT_CLASS(parent_class)->finalize (obj);           
189 }
190
191
192 GtkWidget*
193 modest_msg_view_new (TnyMsg *msg)
194 {
195         GObject *obj;
196         ModestMsgView* self;
197         ModestMsgViewPrivate *priv;
198         
199         obj  = G_OBJECT(g_object_new(MODEST_TYPE_MSG_VIEW, NULL));
200         self = MODEST_MSG_VIEW(obj);
201         priv = MODEST_MSG_VIEW_GET_PRIVATE (self);
202
203         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self),
204                                        GTK_POLICY_AUTOMATIC,
205                                        GTK_POLICY_AUTOMATIC);
206         if (priv->gtkhtml) 
207                 gtk_container_add (GTK_CONTAINER(obj), priv->gtkhtml);
208         
209         if (msg)
210                 modest_msg_view_set_message (self, msg);
211         
212         return GTK_WIDGET(self);
213 }
214
215
216 static gboolean
217 on_link_clicked (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view)
218 {
219         int index;
220
221         g_return_val_if_fail (msg_view, FALSE);
222         
223         /* is it an attachment? */
224         if (g_str_has_prefix(uri, ATT_PREFIX)) {
225
226                 index = atoi (uri + strlen(ATT_PREFIX));
227                 
228                 if (index == 0) {
229                         /* index is 1-based, so 0 indicates an error */
230                         g_printerr ("modest: invalid attachment id: %s\n", uri);
231                         return FALSE;
232                 }
233
234                 g_signal_emit (G_OBJECT(msg_view), signals[ATTACHMENT_CLICKED_SIGNAL],
235                                0, index);
236                 return FALSE;
237         }
238
239         g_signal_emit (G_OBJECT(msg_view), signals[LINK_CLICKED_SIGNAL],
240                        0, uri);
241
242         return FALSE;
243 }
244
245
246
247 static gboolean
248 on_link_hover (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view)
249 {
250         if (uri && g_str_has_prefix (uri, ATT_PREFIX))
251                 return FALSE;
252
253         g_signal_emit (G_OBJECT(msg_view), signals[LINK_HOVER_SIGNAL],
254                        0, uri);
255
256         return FALSE;
257 }
258
259
260
261 static TnyMimePart *
262 find_cid_image (TnyMsg *msg, const gchar *cid)
263 {
264         TnyMimePart *part = NULL;
265         TnyList *parts;
266         TnyIterator *iter;
267         
268         g_return_val_if_fail (msg, NULL);
269         g_return_val_if_fail (cid, NULL);
270         
271         parts  = TNY_LIST (tny_simple_list_new());
272
273         tny_mime_part_get_parts (TNY_MIME_PART (msg), parts); 
274         iter   = tny_list_create_iterator (parts);
275         
276         while (!tny_iterator_is_done(iter)) {
277                 const gchar *part_cid;
278                 part = TNY_MIME_PART(tny_iterator_get_current(iter));
279                 part_cid = tny_mime_part_get_content_id (part);
280
281                 if (part_cid && strcmp (cid, part_cid) == 0)
282                         break;
283
284                 g_object_unref (G_OBJECT(part));
285         
286                 part = NULL;
287                 tny_iterator_next (iter);
288         }
289         
290         g_object_unref (G_OBJECT(iter));        
291         g_object_unref (G_OBJECT(parts));
292         
293         return part;
294 }
295
296
297 static gboolean
298 on_url_requested (GtkWidget *widget, const gchar *uri,
299                   GtkHTMLStream *stream, ModestMsgView *msg_view)
300 {
301         ModestMsgViewPrivate *priv;
302         priv = MODEST_MSG_VIEW_GET_PRIVATE (msg_view);
303         
304         if (g_str_has_prefix (uri, "cid:")) {
305                 /* +4 ==> skip "cid:" */
306                 TnyMimePart *part = find_cid_image (priv->msg, uri + 4);
307                 if (!part) {
308                         g_printerr ("modest: '%s' not found\n", uri + 4);
309                         gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
310                 } else {
311                         TnyStream *tny_stream =
312                                 TNY_STREAM(modest_tny_stream_gtkhtml_new(stream));
313                         tny_mime_part_decode_to_stream ((TnyMimePart*)part,
314                                                                   tny_stream);
315                         gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
316         
317                         g_object_unref (G_OBJECT(tny_stream));
318                         g_object_unref (G_OBJECT(part));
319                 }
320         }
321
322         return TRUE;
323 }
324
325
326 /* render the attachments as hyperlinks in html */
327 static gchar*
328 attachments_as_html (ModestMsgView *self, TnyMsg *msg)
329 {
330         ModestMsgViewPrivate *priv;
331         GString *appendix;
332         TnyList *parts;
333         TnyIterator *iter;
334         gchar *html;
335         int index = 0;
336         
337         if (!msg)
338                 return NULL;
339
340         priv  = MODEST_MSG_VIEW_GET_PRIVATE (self);
341
342         parts = TNY_LIST(tny_simple_list_new());
343         tny_mime_part_get_parts (TNY_MIME_PART (msg), parts);
344         iter  = tny_list_create_iterator (parts);
345         
346         appendix= g_string_new ("");
347         
348         while (!tny_iterator_is_done(iter)) {
349                 TnyMimePart *part;
350
351                 ++index; /* attachment numbers are 1-based */   
352                 part = TNY_MIME_PART(tny_iterator_get_current (iter));
353
354                 if (tny_mime_part_is_attachment (part)) {
355
356                         const gchar *filename = tny_mime_part_get_filename(part);
357                         if (!filename)
358                                 filename = _("attachment");
359
360                         g_string_append_printf (appendix, "<a href=\"%s%d\">%s</a> \n",
361                                                 ATT_PREFIX, index, filename);                    
362                 }
363                 
364                 g_object_unref (G_OBJECT(part));
365                 tny_iterator_next (iter);
366         }
367         g_object_unref (G_OBJECT(iter));
368         g_object_unref (G_OBJECT(parts));
369         
370         if (appendix->len == 0) 
371                 return g_string_free (appendix, TRUE);
372
373         html = g_strdup_printf ("<strong>%s:</strong> %s\n<hr>",
374                                 _("Attachments"), appendix->str);                        
375         g_string_free (appendix, TRUE);
376         
377         return html;
378 }
379
380
381 static gchar*
382 get_header_info (TnyMsg *msg, gboolean outgoing)
383 {
384         GString *str;
385         TnyHeader *header;
386         
387         if (!msg)
388                 return NULL;
389         
390         header = tny_msg_get_header (msg);
391         if (!header) {
392                 g_printerr ("modest: cannot get header info for message\n");
393                 return NULL;
394         }
395         
396         str = g_string_new ("<table border=\"0\">\n");
397
398         if (outgoing) {
399                 if (tny_header_get_to(header))
400                         g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("To"),
401                                                 tny_header_get_to(header));
402         } else {
403                 if (tny_header_get_from (header))
404                         g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("From"),
405                                                 tny_header_get_from(header));
406         }
407         
408         if (tny_header_get_subject (header))
409                 g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("Subject"),
410                                         tny_header_get_subject(header));
411
412
413         if (outgoing) {
414                 gchar *sent =   modest_text_utils_get_display_date (tny_header_get_date_sent (header));
415                 g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("Sent"), sent);
416                 g_free (sent);
417         } else {
418                 gchar *received = modest_text_utils_get_display_date (tny_header_get_date_received (header));
419                 g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("Received"),
420                                         received);
421                 g_free (received);
422         }
423         g_string_append (str, "</table>\n<hr>\n");
424         
425         g_object_unref (G_OBJECT(header));
426         return g_string_free (str, FALSE);
427 }
428
429
430
431 static gboolean
432 set_html_message (ModestMsgView *self, const gchar* header_info, TnyMimePart *tny_body, TnyMsg *msg)
433 {
434         gchar *html_attachments;
435         GtkHTMLStream *gtkhtml_stream;
436         TnyStream *tny_stream;  
437         ModestMsgViewPrivate *priv;
438         
439         g_return_val_if_fail (self, FALSE);
440         g_return_val_if_fail (tny_body, FALSE);
441         
442         priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
443
444         gtkhtml_stream = gtk_html_begin(GTK_HTML(priv->gtkhtml));
445
446         tny_stream     = TNY_STREAM(modest_tny_stream_gtkhtml_new (gtkhtml_stream));
447         tny_stream_reset (tny_stream);
448
449         if (header_info) {
450                 tny_stream_write (tny_stream, header_info, strlen(header_info));
451                 tny_stream_reset (tny_stream);
452         }
453         
454         html_attachments = attachments_as_html(self, msg);
455         if (html_attachments) {
456                 tny_stream_write (tny_stream, html_attachments, strlen(html_attachments));
457                 tny_stream_reset (tny_stream);
458                 g_free (html_attachments);
459         }
460
461         tny_mime_part_decode_to_stream ((TnyMimePart*)tny_body, tny_stream);
462         g_object_unref (G_OBJECT(tny_stream));
463         
464         gtk_html_stream_destroy (gtkhtml_stream);
465         
466         return TRUE;
467 }
468
469
470 /* FIXME: this is a hack --> we use the tny_text_buffer_stream to
471  * get the message text, then write to gtkhtml 'by hand' */
472 static gboolean
473 set_text_message (ModestMsgView *self, const gchar* header_info, TnyMimePart *tny_body, TnyMsg *msg)
474 {
475         GtkTextBuffer *buf;
476         GtkTextIter begin, end;
477         TnyStream* txt_stream, *tny_stream;
478         GtkHTMLStream *gtkhtml_stream;
479         gchar *txt, *html_attachments;
480         ModestMsgViewPrivate *priv;
481                 
482         g_return_val_if_fail (self, FALSE);
483         g_return_val_if_fail (tny_body, FALSE);
484
485         priv           = MODEST_MSG_VIEW_GET_PRIVATE(self);
486         
487         buf            = gtk_text_buffer_new (NULL);
488         txt_stream     = TNY_STREAM(tny_gtk_text_buffer_stream_new (buf));
489                 
490         tny_stream_reset (txt_stream);
491
492         gtkhtml_stream = gtk_html_begin(GTK_HTML(priv->gtkhtml)); 
493         tny_stream =  TNY_STREAM(modest_tny_stream_gtkhtml_new (gtkhtml_stream));
494         
495         if (header_info) {
496                 tny_stream_write (tny_stream, header_info, strlen(header_info));
497                 tny_stream_reset (tny_stream);
498         }
499         
500         html_attachments = attachments_as_html(self, msg);
501         if (html_attachments) {
502                 tny_stream_write (tny_stream, html_attachments,
503                                         strlen(html_attachments));
504                 tny_stream_reset (tny_stream);
505                 g_free (html_attachments);
506         }
507
508         // FIXME: tinymail
509         tny_mime_part_decode_to_stream ((TnyMimePart*)tny_body, txt_stream);
510         tny_stream_reset (txt_stream);          
511         
512         gtk_text_buffer_get_bounds (buf, &begin, &end);
513         txt = gtk_text_buffer_get_text (buf, &begin, &end, FALSE);
514         if (txt) {
515                 gchar *html = modest_text_utils_convert_to_html (txt);
516                 tny_stream_write (tny_stream, html, strlen(html));
517                 tny_stream_reset (tny_stream);
518                 g_free (txt);
519                 g_free (html);
520         }
521         
522         g_object_unref (G_OBJECT(tny_stream));
523         g_object_unref (G_OBJECT(txt_stream));
524         g_object_unref (G_OBJECT(buf));
525         
526         gtk_html_stream_destroy (gtkhtml_stream);
527         
528         return TRUE;
529 }
530
531
532 static gboolean
533 set_empty_message (ModestMsgView *self)
534 {
535         ModestMsgViewPrivate *priv;
536         
537         g_return_val_if_fail (self, FALSE);
538         priv           = MODEST_MSG_VIEW_GET_PRIVATE(self);
539
540         gtk_html_load_from_string (GTK_HTML(priv->gtkhtml),
541                                    "", 1);
542         
543         return TRUE;
544 }
545
546
547 void
548 modest_msg_view_set_message (ModestMsgView *self, TnyMsg *msg)
549 {
550         TnyMimePart *body;
551         ModestMsgViewPrivate *priv;
552         gchar *header_info;
553         
554         g_return_if_fail (self);
555         
556         priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
557
558         if (msg != priv->msg) {
559                 if (priv->msg)
560                         g_object_unref (G_OBJECT(priv->msg));
561                 if (msg)
562                         g_object_ref   (G_OBJECT(msg));
563                 priv->msg = msg;
564         }
565         
566         if (!msg) {
567                 set_empty_message (self);
568                 return;
569         }
570
571         header_info = get_header_info (msg, TRUE);
572         
573         body = modest_tny_msg_find_body_part (msg,TRUE);
574         if (body) {
575                 if (tny_mime_part_content_type_is (body, "text/html"))
576                         set_html_message (self, header_info, body, msg);
577                 else
578                         set_text_message (self, header_info, body, msg);
579         } else 
580                 set_empty_message (self);
581
582         g_free (header_info);
583 }
584
585
586 TnyMsg*
587 modest_msg_view_get_message (ModestMsgView *self)
588 {
589         g_return_val_if_fail (self, NULL);
590         
591         return MODEST_MSG_VIEW_GET_PRIVATE(self)->msg;
592 }