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