* 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
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 static gchar*
384 get_header_info (TnyMsg *msg, gboolean outgoing)
385 {
386         GString *str;
387         TnyHeader *header;
388         
389         if (!msg)
390                 return NULL;
391         
392         header = tny_msg_get_header (msg);
393         if (!header) {
394                 g_printerr ("modest: cannot get header info for message\n");
395                 return NULL;
396         }
397         
398         str = g_string_new ("<table border=\"0\">\n");
399
400         if (outgoing) {
401                 if (tny_header_get_to(header))
402                         g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("To"), tny_header_get_to(header));
403         } else {
404                 if (tny_header_get_from (header))
405                         g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("From"), 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"), tny_header_get_subject(header));
410
411
412         if (outgoing) {
413                 gchar *sent =   modest_text_utils_get_display_date (tny_header_get_date_sent (header));
414                 g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("Sent"), sent);
415                 g_free (sent);
416         } else {
417                 gchar *received = modest_text_utils_get_display_date (tny_header_get_date_received (header));
418                 g_string_append_printf (str, "<tr><td><b>%s</b>:</td><td>%s</td></tr>\n", _("Received"), received);
419                 g_free (received);
420         }
421
422         g_string_append (str, "</table>\n<hr>\n");
423         
424         g_object_unref (G_OBJECT(header));
425         return g_string_free (str, FALSE);
426 }
427
428
429
430 static gboolean
431 set_html_message (ModestMsgView *self, const gchar* header_info, TnyMimePart *tny_body, TnyMsg *msg)
432 {
433         gchar *html_attachments;
434         GtkHTMLStream *gtkhtml_stream;
435         TnyStream *tny_stream;  
436         ModestMsgViewPrivate *priv;
437         
438         g_return_val_if_fail (self, FALSE);
439         g_return_val_if_fail (tny_body, FALSE);
440         
441         priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
442
443         gtkhtml_stream = gtk_html_begin(GTK_HTML(priv->gtkhtml));
444
445         tny_stream     = TNY_STREAM(modest_tny_stream_gtkhtml_new (gtkhtml_stream));
446         tny_stream_reset (tny_stream);
447
448         if (header_info) {
449                 tny_stream_write (tny_stream, header_info, strlen(header_info));
450                 tny_stream_reset (tny_stream);
451         }
452         
453         html_attachments = attachments_as_html(self, msg);
454         if (html_attachments) {
455                 tny_stream_write (tny_stream, html_attachments, strlen(html_attachments));
456                 tny_stream_reset (tny_stream);
457                 g_free (html_attachments);
458         }
459
460         tny_mime_part_decode_to_stream ((TnyMimePart*)tny_body, tny_stream);
461         g_object_unref (G_OBJECT(tny_stream));
462         
463         gtk_html_stream_destroy (gtkhtml_stream);
464         
465         return TRUE;
466 }
467
468
469 /* FIXME: this is a hack --> we use the tny_text_buffer_stream to
470  * get the message text, then write to gtkhtml 'by hand' */
471 static gboolean
472 set_text_message (ModestMsgView *self, const gchar* header_info, TnyMimePart *tny_body, TnyMsg *msg)
473 {
474         GtkTextBuffer *buf;
475         GtkTextIter begin, end;
476         TnyStream* txt_stream, *tny_stream;
477         GtkHTMLStream *gtkhtml_stream;
478         gchar *txt, *html_attachments;
479         ModestMsgViewPrivate *priv;
480                 
481         g_return_val_if_fail (self, FALSE);
482         g_return_val_if_fail (tny_body, FALSE);
483
484         priv           = MODEST_MSG_VIEW_GET_PRIVATE(self);
485         
486         buf            = gtk_text_buffer_new (NULL);
487         txt_stream     = TNY_STREAM(tny_gtk_text_buffer_stream_new (buf));
488                 
489         tny_stream_reset (txt_stream);
490
491         gtkhtml_stream = gtk_html_begin(GTK_HTML(priv->gtkhtml)); 
492         tny_stream =  TNY_STREAM(modest_tny_stream_gtkhtml_new (gtkhtml_stream));
493         
494         if (header_info) {
495                 tny_stream_write (tny_stream, header_info, strlen(header_info));
496                 tny_stream_reset (tny_stream);
497         }
498         
499         html_attachments = attachments_as_html(self, msg);
500         if (html_attachments) {
501                 tny_stream_write (tny_stream, html_attachments,
502                                         strlen(html_attachments));
503                 tny_stream_reset (tny_stream);
504                 g_free (html_attachments);
505         }
506
507         // FIXME: tinymail
508         tny_mime_part_decode_to_stream ((TnyMimePart*)tny_body, txt_stream);
509         tny_stream_reset (txt_stream);          
510         
511         gtk_text_buffer_get_bounds (buf, &begin, &end);
512         txt = gtk_text_buffer_get_text (buf, &begin, &end, FALSE);
513         if (txt) {
514                 gchar *html = modest_text_utils_convert_to_html (txt);
515                 tny_stream_write (tny_stream, html, strlen(html));
516                 tny_stream_reset (tny_stream);
517                 g_free (txt);
518                 g_free (html);
519         }
520         
521         g_object_unref (G_OBJECT(tny_stream));
522         g_object_unref (G_OBJECT(txt_stream));
523         g_object_unref (G_OBJECT(buf));
524         
525         gtk_html_stream_destroy (gtkhtml_stream);
526         
527         return TRUE;
528 }
529
530
531 static gboolean
532 set_empty_message (ModestMsgView *self)
533 {
534         ModestMsgViewPrivate *priv;
535         
536         g_return_val_if_fail (self, FALSE);
537         priv           = MODEST_MSG_VIEW_GET_PRIVATE(self);
538
539         gtk_html_load_from_string (GTK_HTML(priv->gtkhtml),
540                                    "", 1);
541         
542         return TRUE;
543 }
544
545
546 void
547 modest_msg_view_set_message (ModestMsgView *self, TnyMsg *msg)
548 {
549         TnyMimePart *body;
550         ModestMsgViewPrivate *priv;
551         gchar *header_info;
552         
553         g_return_if_fail (self);
554         
555         priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
556
557         if (msg != priv->msg) {
558                 if (priv->msg)
559                         g_object_unref (G_OBJECT(priv->msg));
560                 if (msg)
561                         g_object_ref   (G_OBJECT(msg));
562                 priv->msg = msg;
563         }
564         
565         if (!msg) {
566                 set_empty_message (self);
567                 return;
568         }
569
570         header_info = get_header_info (msg, TRUE);
571         
572         body = modest_tny_msg_find_body_part (msg,TRUE);
573         if (body) {
574                 if (tny_mime_part_content_type_is (body, "text/html"))
575                         set_html_message (self, header_info, body, msg);
576                 else
577                         set_text_message (self, header_info, body, msg);
578         } else 
579                 set_empty_message (self);
580
581         g_free (header_info);
582 }