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