* modest-msg-view:
[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
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                 g_object_unref (G_OBJECT(part));
287         
288                 part = NULL;
289                 tny_iterator_next (iter);
290         }
291         
292         g_object_unref (G_OBJECT(iter));        
293         g_object_unref (G_OBJECT(parts));
294         
295         return part;
296 }
297
298
299 static gboolean
300 on_url_requested (GtkWidget *widget, const gchar *uri,
301                   GtkHTMLStream *stream, ModestMsgView *msg_view)
302 {
303         ModestMsgViewPrivate *priv;
304         priv = MODEST_MSG_VIEW_GET_PRIVATE (msg_view);
305         
306         if (g_str_has_prefix (uri, "cid:")) {
307                 /* +4 ==> skip "cid:" */
308                 TnyMimePart *part = find_cid_image (priv->msg, uri + 4);
309                 if (!part) {
310                         g_printerr ("modest: '%s' not found\n", uri + 4);
311                         gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
312                 } else {
313                         TnyStream *tny_stream =
314                                 TNY_STREAM(modest_tny_stream_gtkhtml_new(stream));
315                         tny_mime_part_decode_to_stream ((TnyMimePart*)part,
316                                                                   tny_stream);
317                         gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
318         
319                         g_object_unref (G_OBJECT(tny_stream));
320                         g_object_unref (G_OBJECT(part));
321                 }
322         }
323
324         return TRUE;
325 }
326
327
328 /* render the attachments as hyperlinks in html */
329 static gchar*
330 attachments_as_html (ModestMsgView *self, TnyMsg *msg)
331 {
332         ModestMsgViewPrivate *priv;
333         GString *appendix;
334         TnyList *parts;
335         TnyIterator *iter;
336         gchar *html;
337         int index = 0;
338         
339         if (!msg)
340                 return NULL;
341
342         priv  = MODEST_MSG_VIEW_GET_PRIVATE (self);
343
344         parts = TNY_LIST(tny_simple_list_new());
345         tny_mime_part_get_parts (TNY_MIME_PART (msg), parts);
346         iter  = tny_list_create_iterator (parts);
347         
348         appendix= g_string_new ("");
349         
350         while (!tny_iterator_is_done(iter)) {
351                 TnyMimePart *part;
352
353                 ++index; /* attachment numbers are 1-based */   
354                 part = TNY_MIME_PART(tny_iterator_get_current (iter));
355
356                 if (tny_mime_part_is_attachment (part)) {
357
358                         const gchar *filename = tny_mime_part_get_filename(part);
359                         if (!filename)
360                                 filename = _("attachment");
361
362                         g_string_append_printf (appendix, "<a href=\"%s%d\">%s</a> \n",
363                                                 ATT_PREFIX, index, filename);                    
364                 }
365                 
366                 g_object_unref (G_OBJECT(part));
367                 tny_iterator_next (iter);
368         }
369         g_object_unref (G_OBJECT(iter));
370         g_object_unref (G_OBJECT(parts));
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 static gchar*
385 get_header_info (TnyMsg *msg, gboolean outgoing)
386 {
387         GString *str;
388         TnyHeader *header;
389         
390         if (!msg)
391                 return NULL;
392         
393         header = tny_msg_get_header (msg);
394         if (!header) {
395                 g_printerr ("modest: cannot get header info for message\n");
396                 return NULL;
397         }
398         
399         str = g_string_new ("<table border=\"0\">\n");
400
401         if (outgoing) {
402                 if (tny_header_get_to(header))
403                         g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("To"), tny_header_get_to(header));
404         } else {
405                 if (tny_header_get_from (header))
406                         g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("From"), tny_header_get_from(header));
407         }
408         
409         if (tny_header_get_subject (header))
410                 g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("Subject"), 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"), received);
420                 g_free (received);
421         }
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 }