* attached images can now be shown when their link is clicked. (will expand and clean...
[modest] / src / modest-tny-msg-view.c
1 /* modest-tny-msg-view.c */
2
3 /* insert (c)/licensing information) */
4
5 #include "modest-tny-msg-view.h"
6 #include "modest-tny-stream-gtkhtml.h"
7 #include "modest-tny-msg-actions.h"
8
9 #include <tny-text-buffer-stream.h>
10 #include <string.h>
11 #include <regex.h>
12 #include <ctype.h>
13 #include <glib/gi18n.h>
14
15 /* 'private'/'protected' functions */
16 static void     modest_tny_msg_view_class_init   (ModestTnyMsgViewClass *klass);
17 static void     modest_tny_msg_view_init         (ModestTnyMsgView *obj);
18 static void     modest_tny_msg_view_finalize     (GObject *obj);
19
20
21 static GSList*  get_url_matches (GString *txt);
22 static gboolean fill_gtkhtml_with_txt (ModestTnyMsgView *self, GtkHTML* gtkhtml, const gchar* txt, TnyMsgIface *msg);
23
24 static gboolean on_link_clicked (GtkWidget *widget, const gchar *uri,
25                                  ModestTnyMsgView *msg_view);
26 static gboolean on_url_requested (GtkWidget *widget, const gchar *uri,
27                                   GtkHTMLStream *stream,
28                                   ModestTnyMsgView *msg_view);
29
30
31 /*
32  * we need these regexps to find URLs in plain text e-mails
33  */
34 typedef struct _UrlMatchPattern UrlMatchPattern;
35 struct _UrlMatchPattern {
36         gchar   *regex;
37         regex_t *preg;
38         gchar   *prefix;
39         
40 };
41 #define MAIL_VIEWER_URL_MATCH_PATTERNS  {\
42         { "(file|http|ftp|https)://[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]+[-A-Za-z0-9_$%&=?/~#]",\
43           NULL, NULL },\
44         { "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
45           NULL, "http://" },\
46         { "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
47           NULL, "ftp://" },\
48         { "(voipto|callto|chatto|jabberto|xmpp):[-_a-z@0-9.\\+]+", \
49            NULL, NULL},                                             \
50         { "mailto:[-_a-z0-9.\\+]+@[-_a-z0-9.]+",                    \
51           NULL, NULL},\
52         { "[-_a-z0-9.\\+]+@[-_a-z0-9.]+",\
53           NULL, "mailto:"}\
54         }
55
56
57 /* list my signals */
58 enum {
59         /* MY_SIGNAL_1, */
60         /* MY_SIGNAL_2, */
61         LAST_SIGNAL
62 };
63
64 typedef struct _ModestTnyMsgViewPrivate ModestTnyMsgViewPrivate;
65 struct _ModestTnyMsgViewPrivate {
66         GtkWidget *gtkhtml;
67         TnyMsgIface *msg;
68         gboolean show_attachments;
69 };
70 #define MODEST_TNY_MSG_VIEW_GET_PRIVATE(o)      (G_TYPE_INSTANCE_GET_PRIVATE((o), \
71                                                  MODEST_TYPE_TNY_MSG_VIEW, \
72                                                  ModestTnyMsgViewPrivate))
73 /* globals */
74 static GtkContainerClass *parent_class = NULL;
75
76 /* uncomment the following if you have defined any signals */
77 /* static guint signals[LAST_SIGNAL] = {0}; */
78
79 GType
80 modest_tny_msg_view_get_type (void)
81 {
82         static GType my_type = 0;
83         if (!my_type) {
84                 static const GTypeInfo my_info = {
85                         sizeof(ModestTnyMsgViewClass),
86                         NULL,           /* base init */
87                         NULL,           /* base finalize */
88                         (GClassInitFunc) modest_tny_msg_view_class_init,
89                         NULL,           /* class finalize */
90                         NULL,           /* class data */
91                         sizeof(ModestTnyMsgView),
92                         1,              /* n_preallocs */
93                         (GInstanceInitFunc) modest_tny_msg_view_init,
94                 };
95                 my_type = g_type_register_static (GTK_TYPE_SCROLLED_WINDOW,
96                                                   "ModestTnyMsgView",
97                                                   &my_info, 0);
98         }
99         return my_type;
100 }
101
102 static void
103 modest_tny_msg_view_class_init (ModestTnyMsgViewClass *klass)
104 {
105         GObjectClass *gobject_class;
106         gobject_class = (GObjectClass*) klass;
107
108         parent_class            = g_type_class_peek_parent (klass);
109         gobject_class->finalize = modest_tny_msg_view_finalize;
110
111         g_type_class_add_private (gobject_class, sizeof(ModestTnyMsgViewPrivate));
112 }
113
114 static void
115 modest_tny_msg_view_init (ModestTnyMsgView *obj)
116 {
117         ModestTnyMsgViewPrivate *priv;
118         
119         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(obj);
120
121         priv->msg = NULL;
122         
123         priv->gtkhtml = gtk_html_new();
124         
125         /* TODO: -> conf-mgr */
126         priv->show_attachments = FALSE;
127
128         gtk_html_set_editable        (GTK_HTML(priv->gtkhtml), FALSE);
129         gtk_html_allow_selection     (GTK_HTML(priv->gtkhtml), TRUE);
130         gtk_html_set_caret_mode      (GTK_HTML(priv->gtkhtml), FALSE);
131         gtk_html_set_blocking        (GTK_HTML(priv->gtkhtml), FALSE);
132         gtk_html_set_images_blocking (GTK_HTML(priv->gtkhtml), FALSE);
133         
134         g_signal_connect (G_OBJECT(priv->gtkhtml), "link_clicked",
135                           G_CALLBACK(on_link_clicked), obj);
136         
137         g_signal_connect (G_OBJECT(priv->gtkhtml), "url_requested",
138                           G_CALLBACK(on_url_requested), obj);
139 }
140         
141
142 static void
143 modest_tny_msg_view_finalize (GObject *obj)
144 {       
145         
146 }
147
148 GtkWidget*
149 modest_tny_msg_view_new (TnyMsgIface *msg)
150 {
151         GObject *obj;
152         ModestTnyMsgView* self;
153         ModestTnyMsgViewPrivate *priv;
154         
155         obj  = G_OBJECT(g_object_new(MODEST_TYPE_TNY_MSG_VIEW, NULL));
156         self = MODEST_TNY_MSG_VIEW(obj);
157         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE (self);
158
159         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self),
160                                        GTK_POLICY_AUTOMATIC,
161                                        GTK_POLICY_AUTOMATIC);
162
163         if (priv->gtkhtml) 
164                 gtk_container_add (GTK_CONTAINER(obj), priv->gtkhtml);  
165         
166         if (msg)
167                 modest_tny_msg_view_set_message (self, msg);
168
169         return GTK_WIDGET(self);
170 }
171
172
173
174 static gboolean
175 on_link_clicked (GtkWidget *widget, const gchar *uri,
176                                  ModestTnyMsgView *msg_view)
177 {
178         ModestTnyMsgViewPrivate *priv;
179         
180         
181         if (g_str_has_prefix(uri, "attachment:")) {
182                 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(msg_view);
183                 priv->show_attachments = !priv->show_attachments;
184                 modest_tny_msg_view_set_message(msg_view, priv->msg);
185         }
186         g_message ("link clicked: %s", uri); /* FIXME */
187 }
188
189
190
191 static TnyMsgMimePartIface *
192 find_cid_image (TnyMsgIface *msg, const gchar *cid)
193 {
194         TnyMsgMimePartIface *part = NULL;
195         GList *parts;
196
197         g_return_val_if_fail (msg, NULL);
198         g_return_val_if_fail (cid, NULL);
199         
200         parts  = (GList*) tny_msg_iface_get_parts (msg);
201         while (parts && !part) {
202                 const gchar *part_cid;
203                 part = TNY_MSG_MIME_PART_IFACE(parts->data);
204                 part_cid = tny_msg_mime_part_iface_get_content_id (part);
205                 printf("CMP:%s:%s\n", cid, part_cid);
206                 if (part_cid && strcmp (cid, part_cid) == 0)
207                         return part; /* we found it! */
208                 
209                 part = NULL;
210                 parts = parts->next;
211         }
212         
213         return part;
214 }
215
216 static TnyMsgMimePartIface *
217 find_attachment_by_filename (TnyMsgIface *msg, const gchar *fn)
218 {
219         TnyMsgMimePartIface *part = NULL;
220         GList *parts;
221
222         g_return_val_if_fail (msg, NULL);
223         g_return_val_if_fail (fn, NULL);
224         
225         parts  = (GList*) tny_msg_iface_get_parts (msg);
226         while (parts && !part) {
227                 const gchar *part_fn;
228                 part = TNY_MSG_MIME_PART_IFACE(parts->data);
229                 part_fn = tny_msg_mime_part_iface_get_filename (part);
230                 if (part_fn && strcmp (fn, part_fn) == 0)
231                         return part; /* we found it! */
232                 
233                 part = NULL;
234                 parts = parts->next;
235         }
236         
237         return part;
238 }
239
240 static gboolean
241 on_url_requested (GtkWidget *widget, const gchar *uri,
242                   GtkHTMLStream *stream,
243                   ModestTnyMsgView *msg_view)
244 {
245         
246         ModestTnyMsgViewPrivate *priv;
247         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE (msg_view);
248
249         g_message ("url requested: %s", uri);
250         
251         if (g_str_has_prefix (uri, "cid:")) {
252                 /* +4 ==> skip "cid:" */
253                 
254                 TnyMsgMimePartIface *part = find_cid_image (priv->msg, uri + 4);
255                 if (!part) {
256                         g_message ("%s not found", uri + 4);
257                         gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
258                 } else {
259                         TnyStreamIface *tny_stream =
260                                 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new(stream));
261                         tny_msg_mime_part_iface_decode_to_stream (part,tny_stream);
262                         gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
263                 }
264         } else if (g_str_has_prefix (uri, "Attachment:")) {
265                 /* +11 ==> skip... */
266                 
267                 TnyMsgMimePartIface *part = find_attachment_by_filename (priv->msg, uri + 11);
268                 if (!part) {
269                         g_message ("%s not found", uri + 11);
270                         gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
271                 } else {
272                         TnyStreamIface *tny_stream =
273                                 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new(stream));
274                         tny_msg_mime_part_iface_decode_to_stream (part,tny_stream);
275                         gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
276                 }
277         }
278         return TRUE;
279 }
280
281
282
283
284 typedef struct  {
285         guint offset;
286         guint len;
287         const gchar* prefix;
288 } url_match_t;
289
290
291 static gchar *
292 attachments_as_html(ModestTnyMsgView *self, TnyMsgIface *msg)
293 {
294         ModestTnyMsgViewPrivate *priv;
295         gboolean attachments_found = FALSE;
296         GString *appendix;
297         const GList *attachment_list, *attachment;
298         const gchar *content_type, *filename;
299         
300         if (!msg)
301                 return g_malloc0(1);
302         
303         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE (self);
304         
305         /* CLEANUP: starting a new HTML may be unsupported */
306         appendix = g_string_new("<HTML><BODY>\n<hr><h5>Attachments:</h5>\n");
307         
308         attachment_list = tny_msg_iface_get_parts(msg);
309         attachment = attachment_list;
310         while (attachment) {
311                 filename = "";
312                 content_type = tny_msg_mime_part_iface_get_content_type(
313                                                                                 TNY_MSG_MIME_PART_IFACE(attachment->data));
314                 g_return_if_fail(content_type);
315                 if (      tny_msg_mime_part_iface_content_type_is(
316                                                                                 TNY_MSG_MIME_PART_IFACE(attachment->data),
317                                                                                 "image/jpeg")
318                            || tny_msg_mime_part_iface_content_type_is(
319                                                                                 TNY_MSG_MIME_PART_IFACE(attachment->data),
320                                                                                 "image/gif")) {
321                         filename = tny_msg_mime_part_iface_get_filename(
322                                                                                 TNY_MSG_MIME_PART_IFACE(attachment->data));
323                         if (!filename)
324                                 filename = "[unknown]";
325                         else
326                                 attachments_found = TRUE;
327                         if (priv->show_attachments) {
328                                 g_string_append_printf(appendix, "<IMG src=\"Attachment:%s\">\n<BR><A href=\"attachment:%s\">%s</A>\n", filename, filename, filename);
329                         } else {
330                                 g_string_append_printf(appendix, "<A href=\"attachment:%s\">%s</A>: %s<BR>\n", filename, filename, content_type);
331                         }
332                 }
333                 attachment = attachment->next;
334         }
335         g_string_append(appendix, "</BODY></HTML>");
336         if (!attachments_found)
337                 g_string_assign(appendix, "");
338         return g_string_free(appendix, FALSE);
339 }
340
341 static void
342 hyperlinkify_plain_text (GString *txt)
343 {
344         GSList *cursor;
345         GSList *match_list = get_url_matches (txt);
346
347         /* we will work backwards, so the offsets stay valid */
348         for (cursor = match_list; cursor; cursor = cursor->next) {
349
350                 url_match_t *match = (url_match_t*) cursor->data;
351                 gchar *url  = g_strndup (txt->str + match->offset, match->len);
352                 gchar *repl = NULL; /* replacement  */
353
354                 /* the prefix is NULL: use the one that is already there */
355                 repl = g_strdup_printf ("<a href=\"%s%s\">%s</a>",
356                                         match->prefix ? match->prefix : "", url, url);
357
358                 /* replace the old thing with our hyperlink
359                  * replacement thing */
360                 g_string_erase  (txt, match->offset, match->len);
361                 g_string_insert (txt, match->offset, repl);
362                 
363                 g_free (url);
364                 g_free (repl);
365
366                 g_free (cursor->data);  
367         }
368         
369         g_slist_free (match_list);
370 }
371
372
373
374 static gchar *
375 convert_to_html (const gchar *data)
376 {
377         int              i;
378         gboolean         first_space = TRUE;
379         GString         *html;      
380         gsize           len;
381
382         if (!data)
383                 return NULL;
384
385         len = strlen (data);
386         html = g_string_sized_new (len + 100);  /* just a  guess... */
387         
388         g_string_append_printf (html,
389                                 "<html>"
390                                 "<head>"
391                                 "<meta http-equiv=\"content-type\""
392                                 " content=\"text/html; charset=utf8\">"
393                                 "</head>"
394                                 "<body><tt>");
395         
396         /* replace with special html chars where needed*/
397         for (i = 0; i != len; ++i)  {
398                 char    kar = data[i]; 
399                 switch (kar) {
400                         
401                 case 0:  break; /* ignore embedded \0s */       
402                 case '<' : g_string_append   (html, "&lt;"); break;
403                 case '>' : g_string_append   (html, "&gt;"); break;
404                 case '&' : g_string_append   (html, "&quot;"); break;
405                 case '\n': g_string_append   (html, "<br>\n"); break;
406                 default:
407                         if (kar == ' ') {
408                                 g_string_append (html, first_space ? " " : "&nbsp;");
409                                 first_space = FALSE;
410                         } else  if (kar == '\t')
411                                 g_string_append (html, "&nbsp; &nbsp;&nbsp;");
412                         else {
413                                 int charnum = 0;
414                                 first_space = TRUE;
415                                 /* optimization trick: accumulate 'normal' chars, then copy */
416                                 do {
417                                         kar = data [++charnum + i];
418                                         
419                                 } while ((i + charnum < len) &&
420                                          (kar > '>' || (kar != '<' && kar != '>'
421                                                         && kar != '&' && kar !=  ' '
422                                                         && kar != '\n' && kar != '\t')));
423                                 g_string_append_len (html, &data[i], charnum);
424                                 i += (charnum  - 1);
425                         }
426                 }
427         }
428         
429         g_string_append (html, "</tt></body></html>");
430         hyperlinkify_plain_text (html);
431
432         return g_string_free (html, FALSE);
433 }
434
435
436
437
438 static gint 
439 cmp_offsets_reverse (const url_match_t *match1, const url_match_t *match2)
440 {
441         return match2->offset - match1->offset;
442 }
443
444
445
446 /*
447  * check if the match is inside an existing match... */
448 static void
449 chk_partial_match (const url_match_t *match, int* offset)
450 {
451         if (*offset >= match->offset && *offset < match->offset + match->len)
452                 *offset = -1;
453 }
454
455 static GSList*
456 get_url_matches (GString *txt)
457 {
458         regmatch_t rm;
459         int rv, i, offset = 0;
460         GSList *match_list = NULL;
461
462         static UrlMatchPattern patterns[] = MAIL_VIEWER_URL_MATCH_PATTERNS;
463         const size_t pattern_num = sizeof(patterns)/sizeof(UrlMatchPattern);
464
465         /* initalize the regexps */
466         for (i = 0; i != pattern_num; ++i) {
467                 patterns[i].preg = g_new0 (regex_t,1);
468                 g_assert(regcomp (patterns[i].preg, patterns[i].regex,
469                                   REG_ICASE|REG_EXTENDED|REG_NEWLINE) == 0);
470         }
471         /* find all the matches */
472         for (i = 0; i != pattern_num; ++i) {
473                 offset     = 0; 
474                 while (1) {
475                         int test_offset;
476                         if ((rv = regexec (patterns[i].preg, txt->str + offset, 1, &rm, 0)) != 0) {
477                                 g_assert (rv == REG_NOMATCH); /* this should not happen */
478                                 break; /* try next regexp */ 
479                         }
480                         if (rm.rm_so == -1)
481                                 break;
482
483                         /* FIXME: optimize this */
484                         /* to avoid partial matches on something that was already found... */
485                         /* check_partial_match will put -1 in the data ptr if that is the case */
486                         test_offset = offset + rm.rm_so;
487                         g_slist_foreach (match_list, (GFunc)chk_partial_match, &test_offset);
488                         
489                         /* make a list of our matches (<offset, len, prefix> tupels)*/
490                         if (test_offset != -1) {
491                                 url_match_t *match = g_new (url_match_t,1);
492                                 match->offset = offset + rm.rm_so;
493                                 match->len    = rm.rm_eo - rm.rm_so;
494                                 match->prefix = patterns[i].prefix;
495                                 match_list = g_slist_prepend (match_list, match);
496                         }
497                         offset += rm.rm_eo;
498                 }
499         }
500
501         for (i = 0; i != pattern_num; ++i) {
502                 regfree (patterns[i].preg);
503                 g_free  (patterns[i].preg);
504         } /* don't free patterns itself -- it's static */
505         
506         /* now sort the list, so the matches are in reverse order of occurence.
507          * that way, we can do the replacements starting from the end, so we don't need
508          * to recalculate the offsets
509          */
510         match_list = g_slist_sort (match_list,
511                                    (GCompareFunc)cmp_offsets_reverse); 
512         return match_list;      
513 }
514
515 static gboolean
516 fill_gtkhtml_with_txt (ModestTnyMsgView *self, GtkHTML* gtkhtml, const gchar* txt, TnyMsgIface *msg)
517 {
518         GString *html;
519         gchar *html_attachments;
520         
521         g_return_val_if_fail (gtkhtml, FALSE);
522         g_return_val_if_fail (txt, FALSE);
523
524         html = g_string_new(convert_to_html (txt));
525         html_attachments = attachments_as_html(self, msg);
526         g_string_append(html, html_attachments);
527
528         gtk_html_load_from_string (gtkhtml, html->str, html->len);
529         g_string_free (html, TRUE);
530         g_free(html_attachments);
531
532         return TRUE;
533 }
534
535
536
537 static gboolean
538 set_html_message (ModestTnyMsgView *self, TnyMsgMimePartIface *tny_body, TnyMsgIface *msg)
539 {
540         gchar *html_attachments;
541         TnyStreamIface *gtkhtml_stream; 
542         ModestTnyMsgViewPrivate *priv;
543         
544         g_return_val_if_fail (self, FALSE);
545         g_return_val_if_fail (tny_body, FALSE);
546         
547         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
548
549         gtkhtml_stream =
550                 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new
551                                  (gtk_html_begin(GTK_HTML(priv->gtkhtml))));
552         
553         tny_stream_iface_reset (gtkhtml_stream);
554         tny_msg_mime_part_iface_decode_to_stream (tny_body, gtkhtml_stream);
555         html_attachments = attachments_as_html(self, msg);
556         /* is this clean? */
557         gtkhtml_write(gtkhtml_stream, html_attachments, strlen(html_attachments));
558         tny_stream_iface_reset (gtkhtml_stream);
559
560         g_object_unref (G_OBJECT(gtkhtml_stream));
561         g_free (html_attachments);
562         
563         return TRUE;
564 }
565
566
567 /* this is a hack --> we use the tny_text_buffer_stream to
568  * get the message text, then write to gtkhtml 'by hand' */
569 static gboolean
570 set_text_message (ModestTnyMsgView *self, TnyMsgMimePartIface *tny_body, TnyMsgIface *msg)
571 {
572         GtkTextBuffer *buf;
573         GtkTextIter begin, end;
574         TnyStreamIface* txt_stream;
575         gchar *txt;
576         ModestTnyMsgViewPrivate *priv;
577                 
578         g_return_val_if_fail (self, FALSE);
579         g_return_val_if_fail (tny_body, FALSE);
580
581         priv           = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
582         
583         buf            = gtk_text_buffer_new (NULL);
584         txt_stream     = TNY_STREAM_IFACE(tny_text_buffer_stream_new (buf));
585                 
586         tny_stream_iface_reset (txt_stream);
587         tny_msg_mime_part_iface_decode_to_stream (tny_body, txt_stream);
588         tny_stream_iface_reset (txt_stream);            
589         
590         gtk_text_buffer_get_bounds (buf, &begin, &end);
591         txt = gtk_text_buffer_get_text (buf, &begin, &end, FALSE);
592         
593         fill_gtkhtml_with_txt (self, GTK_HTML(priv->gtkhtml), txt, msg);
594
595         g_object_unref (G_OBJECT(txt_stream));
596         g_object_unref (G_OBJECT(buf));
597
598         g_free (txt);
599         return TRUE;
600 }
601
602 gchar *
603 modest_tny_msg_view_get_selected_text (ModestTnyMsgView *self)
604 {
605         ModestTnyMsgViewPrivate *priv;
606         gchar *sel;
607         GtkWidget *html;
608         int len;
609         GtkClipboard *clip;
610         gchar *text;
611         GtkTextBuffer *buf;
612
613         g_return_if_fail (self);
614         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
615         html = priv->gtkhtml;
616         
617         /* I'm sure there is a better way to check for selected text */
618         sel = gtk_html_get_selection_html(GTK_HTML(html), &len);
619         if (!sel)
620                 return NULL;
621         
622         g_free(sel);
623         
624         clip = gtk_widget_get_clipboard(html, GDK_SELECTION_PRIMARY);
625         return gtk_clipboard_wait_for_text(clip);
626 }
627
628 void
629 modest_tny_msg_view_set_message (ModestTnyMsgView *self, TnyMsgIface *msg)
630 {
631         TnyMsgMimePartIface *body;
632         ModestTnyMsgViewPrivate *priv;
633
634         g_return_if_fail (self);
635         
636         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
637
638         priv->msg = msg;
639         
640         fill_gtkhtml_with_txt (self, GTK_HTML(priv->gtkhtml), "", msg);
641
642         if (!msg) 
643                 return;
644         
645         body = modest_tny_msg_actions_find_body_part (msg, "text/html");
646         if (body) {
647                 set_html_message (self, body, msg);
648                 return;
649         }
650         
651         body = modest_tny_msg_actions_find_body_part (msg, "text/plain");
652         if (body) {
653                 set_text_message (self, body, msg);
654                 return;
655         }
656
657         /* hmmmmm */
658         fill_gtkhtml_with_txt (self, GTK_HTML(priv->gtkhtml),
659                                 _("Unsupported message type"), msg);
660 }