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