* moded find_body_part from modest-tny-msg-view to modest-tny-msg-actions
[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);
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                 if (part_cid && strcmp (cid, part_cid) == 0)
194                         return part; /* we found it! */
195                 
196                 part = NULL;
197                 parts = parts->next;
198         }
199         
200         return part;
201 }
202
203
204 static gboolean
205 on_url_requested (GtkWidget *widget, const gchar *uri,
206                   GtkHTMLStream *stream,
207                   ModestTnyMsgView *msg_view)
208 {
209         
210         ModestTnyMsgViewPrivate *priv;
211         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE (msg_view);
212
213         g_message ("url requested: %s", uri);
214         
215         if (g_str_has_prefix (uri, "cid:")) {
216                 /* +4 ==> skip "cid:" */
217                 
218                 TnyMsgMimePartIface *part = find_cid_image (priv->msg, uri + 4);
219                 if (!part) {
220                         g_message ("%s not found", uri + 4);
221                         gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
222                 } else {
223                         TnyStreamIface *tny_stream =
224                                 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new(stream));
225                         tny_msg_mime_part_iface_decode_to_stream (part,tny_stream);
226                         gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
227                 }
228         }
229         return TRUE;
230 }
231
232
233
234
235 typedef struct  {
236         guint offset;
237         guint len;
238         const gchar* prefix;
239 } url_match_t;
240
241
242 static void
243 hyperlinkify_plain_text (GString *txt)
244 {
245         GSList *cursor;
246         GSList *match_list = get_url_matches (txt);
247
248         /* we will work backwards, so the offsets stay valid */
249         for (cursor = match_list; cursor; cursor = cursor->next) {
250
251                 url_match_t *match = (url_match_t*) cursor->data;
252                 gchar *url  = g_strndup (txt->str + match->offset, match->len);
253                 gchar *repl = NULL; /* replacement  */
254
255                 /* the prefix is NULL: use the one that is already there */
256                 repl = g_strdup_printf ("<a href=\"%s%s\">%s</a>",
257                                         match->prefix ? match->prefix : "", url, url);
258
259                 /* replace the old thing with our hyperlink
260                  * replacement thing */
261                 g_string_erase  (txt, match->offset, match->len);
262                 g_string_insert (txt, match->offset, repl);
263                 
264                 g_free (url);
265                 g_free (repl);
266
267                 g_free (cursor->data);  
268         }
269         
270         g_slist_free (match_list);
271 }
272
273
274
275 static gchar *
276 convert_to_html (const gchar *data)
277 {
278         int              i;
279         gboolean         first_space = TRUE;
280         GString         *html;      
281         gsize           len;
282
283         if (!data)
284                 return NULL;
285
286         len = strlen (data);
287         html = g_string_sized_new (len + 100);  /* just a  guess... */
288         
289         g_string_append_printf (html,
290                                 "<html>"
291                                 "<head>"
292                                 "<meta http-equiv=\"content-type\""
293                                 " content=\"text/html; charset=utf8\">"
294                                 "</head>"
295                                 "<body><tt>");
296         
297         /* replace with special html chars where needed*/
298         for (i = 0; i != len; ++i)  {
299                 char    kar = data[i]; 
300                 switch (kar) {
301                         
302                 case 0:  break; /* ignore embedded \0s */       
303                 case '<' : g_string_append   (html, "&lt;"); break;
304                 case '>' : g_string_append   (html, "&gt;"); break;
305                 case '&' : g_string_append   (html, "&quot;"); break;
306                 case '\n': g_string_append   (html, "<br>\n"); break;
307                 default:
308                         if (kar == ' ') {
309                                 g_string_append (html, first_space ? " " : "&nbsp;");
310                                 first_space = FALSE;
311                         } else  if (kar == '\t')
312                                 g_string_append (html, "&nbsp; &nbsp;&nbsp;");
313                         else {
314                                 int charnum = 0;
315                                 first_space = TRUE;
316                                 /* optimization trick: accumulate 'normal' chars, then copy */
317                                 do {
318                                         kar = data [++charnum + i];
319                                         
320                                 } while ((i + charnum < len) &&
321                                          (kar > '>' || (kar != '<' && kar != '>'
322                                                         && kar != '&' && kar !=  ' '
323                                                         && kar != '\n' && kar != '\t')));
324                                 g_string_append_len (html, &data[i], charnum);
325                                 i += (charnum  - 1);
326                         }
327                 }
328         }
329
330         g_string_append (html, "</tt></body></html>");
331         hyperlinkify_plain_text (html);
332
333         return g_string_free (html, FALSE);
334 }
335
336
337
338
339 static gint 
340 cmp_offsets_reverse (const url_match_t *match1, const url_match_t *match2)
341 {
342         return match2->offset - match1->offset;
343 }
344
345
346
347 /*
348  * check if the match is inside an existing match... */
349 static void
350 chk_partial_match (const url_match_t *match, int* offset)
351 {
352         if (*offset >= match->offset && *offset < match->offset + match->len)
353                 *offset = -1;
354 }
355
356 static GSList*
357 get_url_matches (GString *txt)
358 {
359         regmatch_t rm;
360         int rv, i, offset = 0;
361         GSList *match_list = NULL;
362
363         static UrlMatchPattern patterns[] = MAIL_VIEWER_URL_MATCH_PATTERNS;
364         const size_t pattern_num = sizeof(patterns)/sizeof(UrlMatchPattern);
365
366         /* initalize the regexps */
367         for (i = 0; i != pattern_num; ++i) {
368                 patterns[i].preg = g_new0 (regex_t,1);
369                 g_assert(regcomp (patterns[i].preg, patterns[i].regex,
370                                   REG_ICASE|REG_EXTENDED|REG_NEWLINE) == 0);
371         }
372         /* find all the matches */
373         for (i = 0; i != pattern_num; ++i) {
374                 offset     = 0; 
375                 while (1) {
376                         int test_offset;
377                         if ((rv = regexec (patterns[i].preg, txt->str + offset, 1, &rm, 0)) != 0) {
378                                 g_assert (rv == REG_NOMATCH); /* this should not happen */
379                                 break; /* try next regexp */ 
380                         }
381                         if (rm.rm_so == -1)
382                                 break;
383
384                         /* FIXME: optimize this */
385                         /* to avoid partial matches on something that was already found... */
386                         /* check_partial_match will put -1 in the data ptr if that is the case */
387                         test_offset = offset + rm.rm_so;
388                         g_slist_foreach (match_list, (GFunc)chk_partial_match, &test_offset);
389                         
390                         /* make a list of our matches (<offset, len, prefix> tupels)*/
391                         if (test_offset != -1) {
392                                 url_match_t *match = g_new (url_match_t,1);
393                                 match->offset = offset + rm.rm_so;
394                                 match->len    = rm.rm_eo - rm.rm_so;
395                                 match->prefix = patterns[i].prefix;
396                                 match_list = g_slist_prepend (match_list, match);
397                         }
398                         offset += rm.rm_eo;
399                 }
400         }
401
402         for (i = 0; i != pattern_num; ++i) {
403                 regfree (patterns[i].preg);
404                 g_free  (patterns[i].preg);
405         } /* don't free patterns itself -- it's static */
406         
407         /* now sort the list, so the matches are in reverse order of occurence.
408          * that way, we can do the replacements starting from the end, so we don't need
409          * to recalculate the offsets
410          */
411         match_list = g_slist_sort (match_list,
412                                    (GCompareFunc)cmp_offsets_reverse); 
413         return match_list;      
414 }
415
416 static gboolean
417 fill_gtkhtml_with_txt (GtkHTML* gtkhtml, const gchar* txt)
418 {
419         gchar *html;
420         
421         g_return_val_if_fail (gtkhtml, FALSE);
422         g_return_val_if_fail (txt, FALSE);
423
424         html = convert_to_html (txt);
425         gtk_html_load_from_string (gtkhtml, html,  strlen(html));
426         g_free (html);
427
428         return TRUE;
429 }
430
431
432
433 static gboolean
434 set_html_message (ModestTnyMsgView *self, TnyMsgMimePartIface *tny_body)
435 {
436         TnyStreamIface *gtkhtml_stream; 
437         ModestTnyMsgViewPrivate *priv;
438         
439         g_return_val_if_fail (self, FALSE);
440         g_return_val_if_fail (tny_body, FALSE);
441         
442         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
443
444         gtkhtml_stream =
445                 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new
446                                  (gtk_html_begin(GTK_HTML(priv->gtkhtml))));
447         
448         tny_stream_iface_reset (gtkhtml_stream);
449         tny_msg_mime_part_iface_decode_to_stream (tny_body, gtkhtml_stream);
450         tny_stream_iface_reset (gtkhtml_stream);
451
452         g_object_unref (G_OBJECT(gtkhtml_stream));
453         
454         return TRUE;
455 }
456
457
458 /* this is a hack --> we use the tny_text_buffer_stream to
459  * get the message text, then write to gtkhtml 'by hand' */
460 static gboolean
461 set_text_message (ModestTnyMsgView *self, TnyMsgMimePartIface *tny_body)
462 {
463         GtkTextBuffer *buf;
464         GtkTextIter begin, end;
465         TnyStreamIface* txt_stream;
466         gchar *txt;
467         ModestTnyMsgViewPrivate *priv;
468                 
469         g_return_val_if_fail (self, FALSE);
470         g_return_val_if_fail (tny_body, FALSE);
471
472         priv           = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
473         
474         buf            = gtk_text_buffer_new (NULL);
475         txt_stream     = TNY_STREAM_IFACE(tny_text_buffer_stream_new (buf));
476                 
477         tny_stream_iface_reset (txt_stream);
478         tny_msg_mime_part_iface_decode_to_stream (tny_body, txt_stream);
479         tny_stream_iface_reset (txt_stream);            
480         
481         gtk_text_buffer_get_bounds (buf, &begin, &end);
482         txt = gtk_text_buffer_get_text (buf, &begin, &end, FALSE);
483         
484         fill_gtkhtml_with_txt (GTK_HTML(priv->gtkhtml), txt);
485         
486         g_object_unref (G_OBJECT(txt_stream));
487         g_object_unref (G_OBJECT(buf));
488
489         g_free (txt);
490         return TRUE;
491 }
492
493 gchar *
494 modest_tny_msg_view_get_selected_text (ModestTnyMsgView *self)
495 {
496         ModestTnyMsgViewPrivate *priv;
497         gchar *sel;
498         GtkWidget *html;
499         int len;
500         GtkClipboard *clip;
501         gchar *text;
502         GtkTextBuffer *buf;
503
504         g_return_if_fail (self);
505         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
506         html = priv->gtkhtml;
507         
508         /* I'm sure there is a better way to check for selected text */
509         sel = gtk_html_get_selection_html(GTK_HTML(html), &len);
510         if (!sel)
511                 return NULL;
512         
513         g_free(sel);
514         
515         clip = gtk_widget_get_clipboard(html, GDK_SELECTION_PRIMARY);
516         return gtk_clipboard_wait_for_text(clip);
517 }
518
519 void
520 modest_tny_msg_view_set_message (ModestTnyMsgView *self, TnyMsgIface *msg)
521 {
522         TnyMsgMimePartIface *body;
523         ModestTnyMsgViewPrivate *priv;
524
525         g_return_if_fail (self);
526         
527         priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
528
529         priv->msg = msg;
530         
531         fill_gtkhtml_with_txt (GTK_HTML(priv->gtkhtml), "");
532
533         if (!msg) 
534                 return;
535         
536         body = modest_tny_msg_actions_find_body_part (msg, "text/html");
537         if (body) {
538                 set_html_message (self, body);
539                 return;
540         }
541         
542         body = modest_tny_msg_actions_find_body_part (msg, "text/plain");
543         if (body) {
544                 set_text_message (self, body);
545                 return;
546         }
547
548         /* hmmmmm */
549         fill_gtkhtml_with_txt (GTK_HTML(priv->gtkhtml),
550                                 _("Unsupported message type"));
551 }