1 /* modest-tny-msg-view.c */
3 /* insert (c)/licensing information) */
5 #include "modest-tny-msg-view.h"
6 #include "modest-tny-stream-gtkhtml.h"
7 #include "modest-tny-msg-actions.h"
9 #include <tny-text-buffer-stream.h>
13 #include <glib/gi18n.h>
14 #include <gtkhtml/gtkhtml.h>
15 #include <gtkhtml/gtkhtml-stream.h>
17 /* 'private'/'protected' functions */
18 static void modest_tny_msg_view_class_init (ModestTnyMsgViewClass *klass);
19 static void modest_tny_msg_view_init (ModestTnyMsgView *obj);
20 static void modest_tny_msg_view_finalize (GObject *obj);
23 static GSList* get_url_matches (GString *txt);
24 static gboolean fill_gtkhtml_with_txt (ModestTnyMsgView *self, GtkHTML* gtkhtml, const gchar* txt, TnyMsgIface *msg);
26 static gboolean on_link_clicked (GtkWidget *widget, const gchar *uri,
27 ModestTnyMsgView *msg_view);
28 static gboolean on_url_requested (GtkWidget *widget, const gchar *uri,
29 GtkHTMLStream *stream,
30 ModestTnyMsgView *msg_view);
31 static gchar *construct_virtual_filename(const gchar *filename, const gint position, const gchar *id, const gboolean active);
32 static gchar *construct_virtual_filename_from_mime_part(TnyMsgMimePartIface *msg, const gint position);
34 #define ATTACHMENT_ID_INLINE "attachment-inline"
35 #define ATTACHMENT_ID_LINK "attachment-link"
36 #define PREFIX_LINK_EMAIL "mailto:"
38 gint virtual_filename_get_pos(const gchar *filename);
40 * we need these regexps to find URLs in plain text e-mails
42 typedef struct _UrlMatchPattern UrlMatchPattern;
43 struct _UrlMatchPattern {
49 #define MAIL_VIEWER_URL_MATCH_PATTERNS {\
50 { "(file|http|ftp|https)://[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]+[-A-Za-z0-9_$%&=?/~#]",\
52 { "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
54 { "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
56 { "(voipto|callto|chatto|jabberto|xmpp):[-_a-z@0-9.\\+]+", \
58 { "mailto:[-_a-z0-9.\\+]+@[-_a-z0-9.]+", \
60 { "[-_a-z0-9.\\+]+@[-_a-z0-9.]+",\
67 MAILTO_CLICKED_SIGNAL,
72 typedef struct _ModestTnyMsgViewPrivate ModestTnyMsgViewPrivate;
73 struct _ModestTnyMsgViewPrivate {
76 gboolean show_attachments_inline;
78 #define MODEST_TNY_MSG_VIEW_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), \
79 MODEST_TYPE_TNY_MSG_VIEW, \
80 ModestTnyMsgViewPrivate))
82 static GtkContainerClass *parent_class = NULL;
84 /* uncomment the following if you have defined any signals */
85 static guint signals[LAST_SIGNAL] = {0};
88 modest_tny_msg_view_get_type (void)
90 static GType my_type = 0;
92 static const GTypeInfo my_info = {
93 sizeof(ModestTnyMsgViewClass),
95 NULL, /* base finalize */
96 (GClassInitFunc) modest_tny_msg_view_class_init,
97 NULL, /* class finalize */
98 NULL, /* class data */
99 sizeof(ModestTnyMsgView),
101 (GInstanceInitFunc) modest_tny_msg_view_init,
103 my_type = g_type_register_static (GTK_TYPE_SCROLLED_WINDOW,
111 modest_tny_msg_view_class_init (ModestTnyMsgViewClass *klass)
113 GObjectClass *gobject_class;
114 gobject_class = (GObjectClass*) klass;
116 parent_class = g_type_class_peek_parent (klass);
117 gobject_class->finalize = modest_tny_msg_view_finalize;
119 g_type_class_add_private (gobject_class, sizeof(ModestTnyMsgViewPrivate));
121 signals[MAILTO_CLICKED_SIGNAL] =
122 g_signal_new ("on_mailto_clicked",
123 G_TYPE_FROM_CLASS (gobject_class),
125 G_STRUCT_OFFSET(ModestTnyMsgViewClass, mailto_clicked),
127 g_cclosure_marshal_VOID__STRING,
128 G_TYPE_NONE, 1, G_TYPE_STRING/*, 1, G_TYPE_POINTER*/);
132 modest_tny_msg_view_init (ModestTnyMsgView *obj)
134 ModestTnyMsgViewPrivate *priv;
136 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(obj);
140 priv->gtkhtml = gtk_html_new();
142 priv->show_attachments_inline = FALSE;
144 gtk_html_set_editable (GTK_HTML(priv->gtkhtml), FALSE);
145 gtk_html_allow_selection (GTK_HTML(priv->gtkhtml), TRUE);
146 gtk_html_set_caret_mode (GTK_HTML(priv->gtkhtml), FALSE);
147 gtk_html_set_blocking (GTK_HTML(priv->gtkhtml), FALSE);
148 gtk_html_set_images_blocking (GTK_HTML(priv->gtkhtml), FALSE);
150 g_signal_connect (G_OBJECT(priv->gtkhtml), "link_clicked",
151 G_CALLBACK(on_link_clicked), obj);
153 g_signal_connect (G_OBJECT(priv->gtkhtml), "url_requested",
154 G_CALLBACK(on_url_requested), obj);
159 modest_tny_msg_view_finalize (GObject *obj)
165 modest_tny_msg_view_new (TnyMsgIface *msg, const gboolean show_attachments_inline)
168 ModestTnyMsgView* self;
169 ModestTnyMsgViewPrivate *priv;
171 obj = G_OBJECT(g_object_new(MODEST_TYPE_TNY_MSG_VIEW, NULL));
172 self = MODEST_TNY_MSG_VIEW(obj);
173 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE (self);
175 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self),
176 GTK_POLICY_AUTOMATIC,
177 GTK_POLICY_AUTOMATIC);
180 gtk_container_add (GTK_CONTAINER(obj), priv->gtkhtml);
183 modest_tny_msg_view_set_message (self, msg);
185 modest_tny_msg_view_set_show_attachments_inline_flag(self, show_attachments_inline);
187 return GTK_WIDGET(self);
192 on_link_clicked (GtkWidget *widget, const gchar *uri,
193 ModestTnyMsgView *msg_view)
195 if (g_str_has_prefix(uri, PREFIX_LINK_EMAIL)) {
197 /* skip over "mailto:" */
198 s = g_strdup(uri + strlen(PREFIX_LINK_EMAIL));
199 /* strip ?subject=... and the like */
200 for (p = s; p[0]; p++)
205 g_signal_emit(msg_view, signals[MAILTO_CLICKED_SIGNAL], 0, s);
208 } else if (g_str_has_prefix(uri, ATTACHMENT_ID_LINK)) {
209 /* save or open attachment */
210 g_message ("link-to-save: %s", uri); /* FIXME */
213 g_message ("link clicked: %s", uri); /* FIXME */
219 static TnyMsgMimePartIface *
220 find_cid_image (TnyMsgIface *msg, const gchar *cid)
222 TnyMsgMimePartIface *part = NULL;
225 g_return_val_if_fail (msg, NULL);
226 g_return_val_if_fail (cid, NULL);
228 parts = (GList*) tny_msg_iface_get_parts (msg);
229 while (parts && !part) {
230 const gchar *part_cid;
231 part = TNY_MSG_MIME_PART_IFACE(parts->data);
232 part_cid = tny_msg_mime_part_iface_get_content_id (part);
233 printf("CMP:%s:%s\n", cid, part_cid);
234 if (part_cid && strcmp (cid, part_cid) == 0)
235 return part; /* we found it! */
245 static TnyMsgMimePartIface *
246 find_attachment_by_filename (TnyMsgIface *msg, const gchar *fn)
248 TnyMsgMimePartIface *part = NULL;
253 g_return_val_if_fail (msg, NULL);
254 g_return_val_if_fail (fn, NULL);
256 parts = (GList*) tny_msg_iface_get_parts (msg);
257 pos = virtual_filename_get_pos(fn);
259 if ((pos < 0) || (pos >= g_list_length(parts)))
262 part = g_list_nth_data(parts, pos);
264 dummy = construct_virtual_filename_from_mime_part(part, pos);
265 if (strcmp(dummy, fn) == 0) {
276 on_url_requested (GtkWidget *widget, const gchar *uri,
277 GtkHTMLStream *stream,
278 ModestTnyMsgView *msg_view)
281 ModestTnyMsgViewPrivate *priv;
282 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE (msg_view);
284 g_message ("url requested: %s", uri);
286 if (!modest_tny_msg_view_get_show_attachments_inline_flag(msg_view))
287 return TRUE; /* debatable */
289 if (g_str_has_prefix (uri, "cid:")) {
290 /* +4 ==> skip "cid:" */
292 TnyMsgMimePartIface *part = find_cid_image (priv->msg, uri + 4);
294 g_message ("%s not found", uri + 4);
295 gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
297 TnyStreamIface *tny_stream =
298 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new(stream));
299 tny_msg_mime_part_iface_decode_to_stream (part,tny_stream);
300 gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
302 } else if (g_str_has_prefix (uri, ATTACHMENT_ID_INLINE)) {
303 TnyMsgMimePartIface *part;
304 part = find_attachment_by_filename (priv->msg, uri);
306 g_message ("%s not found", uri);
307 gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
309 TnyStreamIface *tny_stream =
310 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new(stream));
311 tny_msg_mime_part_iface_decode_to_stream (part,tny_stream);
312 gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
327 secure_filename(const gchar *fn)
332 s = g_string_new("");
335 for (p = tmp; p[0] ; p++ ) {
336 p[0] &= 0x5f; /* 01011111 */
337 p[0] |= 0x40; /* 01000000 */
339 g_string_printf(s, "0x%x:%s", g_str_hash(fn), tmp);
341 return g_string_free(s, FALSE);
343 g_string_printf(s, "0x%x", g_str_hash(fn));
344 return g_string_free(s, FALSE);
350 construct_virtual_filename(const gchar *filename,
353 const gboolean active)
359 return g_strdup("AttachmentInvalid");
361 s = g_string_new("");
363 g_string_append(s, ATTACHMENT_ID_INLINE);
365 g_string_append(s, ATTACHMENT_ID_LINK);
366 g_string_append_printf(s, ":%d:", position);
368 g_string_append(s, id);
369 g_string_append_c(s, ':');
371 fn = secure_filename(filename);
373 g_string_append(s, fn);
375 g_string_append_c(s, ':');
376 return g_string_free(s, FALSE);
381 construct_virtual_filename_from_mime_part(TnyMsgMimePartIface *msg, const gint position)
383 const gchar *id, *filename;
384 const gboolean active = TRUE;
386 filename = tny_msg_mime_part_iface_get_filename(
387 TNY_MSG_MIME_PART_IFACE(msg));
389 filename = "[unknown]";
390 id = tny_msg_mime_part_iface_get_content_id(
391 TNY_MSG_MIME_PART_IFACE(msg));
393 return construct_virtual_filename(filename, position, id, active);
397 get_next_token(const gchar *s, gint *len)
414 /* maybe I should use libregexp */
416 virtual_filename_get_pos(const gchar *filename)
418 const gchar *i1, *i2;
423 if ((!g_str_has_prefix(filename, ATTACHMENT_ID_INLINE ":")) &&
424 (!g_str_has_prefix(filename, ATTACHMENT_ID_LINK ":")))
428 i2 = get_next_token(i2, &len);
432 i2 = get_next_token(i2, &len);
435 dummy = g_string_new_len(i1, len);
436 pos = atoi(dummy->str);
437 g_string_free(dummy, FALSE);
443 attachments_as_html(ModestTnyMsgView *self, TnyMsgIface *msg)
445 ModestTnyMsgViewPrivate *priv;
446 gboolean attachments_found = FALSE;
448 const GList *attachment_list, *attachment;
449 const gchar *content_type, *filename, *id;
450 gchar *virtual_filename;
455 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE (self);
457 appendix = g_string_new("");
458 g_string_printf(appendix, "<HTML><BODY>\n<hr><h5>%s:</h5>\n", _("Attachments"));
460 attachment_list = tny_msg_iface_get_parts(msg);
461 attachment = attachment_list;
464 content_type = tny_msg_mime_part_iface_get_content_type(
465 TNY_MSG_MIME_PART_IFACE(attachment->data));
469 if ((strcmp("image/jpeg", content_type) == 0) ||
470 (strcmp("image/gif", content_type) == 0)) {
471 filename = tny_msg_mime_part_iface_get_filename(
472 TNY_MSG_MIME_PART_IFACE(attachment->data));
474 filename = "[unknown]";
476 attachments_found = TRUE;
477 id = tny_msg_mime_part_iface_get_content_id(
478 TNY_MSG_MIME_PART_IFACE(attachment->data));
479 if (modest_tny_msg_view_get_show_attachments_inline_flag(self)) {
480 virtual_filename = construct_virtual_filename(filename,
481 g_list_position((GList *)attachment_list, (GList *) attachment),
483 g_string_append_printf(appendix, "<IMG src=\"%s\">\n<BR>", virtual_filename);
484 g_free(virtual_filename);
486 virtual_filename = construct_virtual_filename(filename,
487 g_list_position((GList *)attachment_list, (GList *) attachment),
489 g_string_append_printf(appendix,
490 "<A href=\"%s\">%s</A>: %s<BR>\n",
491 virtual_filename, filename, content_type);
492 g_free(virtual_filename);
494 attachment = attachment->next;
496 g_string_append(appendix, "</BODY></HTML>");
497 if (!attachments_found)
498 g_string_assign(appendix, "");
499 return g_string_free(appendix, FALSE);
503 hyperlinkify_plain_text (GString *txt)
506 GSList *match_list = get_url_matches (txt);
508 /* we will work backwards, so the offsets stay valid */
509 for (cursor = match_list; cursor; cursor = cursor->next) {
511 url_match_t *match = (url_match_t*) cursor->data;
512 gchar *url = g_strndup (txt->str + match->offset, match->len);
513 gchar *repl = NULL; /* replacement */
515 /* the prefix is NULL: use the one that is already there */
516 repl = g_strdup_printf ("<a href=\"%s%s\">%s</a>",
517 match->prefix ? match->prefix : "", url, url);
519 /* replace the old thing with our hyperlink
520 * replacement thing */
521 g_string_erase (txt, match->offset, match->len);
522 g_string_insert (txt, match->offset, repl);
527 g_free (cursor->data);
530 g_slist_free (match_list);
536 convert_to_html (const gchar *data)
539 gboolean first_space = TRUE;
547 html = g_string_sized_new (len + 100); /* just a guess... */
549 g_string_append_printf (html,
552 "<meta http-equiv=\"content-type\""
553 " content=\"text/html; charset=utf8\">"
557 /* replace with special html chars where needed*/
558 for (i = 0; i != len; ++i) {
562 case 0: break; /* ignore embedded \0s */
563 case '<' : g_string_append (html, "<"); break;
564 case '>' : g_string_append (html, ">"); break;
565 case '&' : g_string_append (html, """); break;
566 case '\n': g_string_append (html, "<br>\n"); break;
569 g_string_append (html, first_space ? " " : " ");
571 } else if (kar == '\t')
572 g_string_append (html, " ");
576 /* optimization trick: accumulate 'normal' chars, then copy */
578 kar = data [++charnum + i];
580 } while ((i + charnum < len) &&
581 (kar > '>' || (kar != '<' && kar != '>'
582 && kar != '&' && kar != ' '
583 && kar != '\n' && kar != '\t')));
584 g_string_append_len (html, &data[i], charnum);
590 g_string_append (html, "</tt></body></html>");
591 hyperlinkify_plain_text (html);
593 return g_string_free (html, FALSE);
600 cmp_offsets_reverse (const url_match_t *match1, const url_match_t *match2)
602 return match2->offset - match1->offset;
608 * check if the match is inside an existing match... */
610 chk_partial_match (const url_match_t *match, int* offset)
612 if (*offset >= match->offset && *offset < match->offset + match->len)
617 get_url_matches (GString *txt)
620 int rv, i, offset = 0;
621 GSList *match_list = NULL;
623 static UrlMatchPattern patterns[] = MAIL_VIEWER_URL_MATCH_PATTERNS;
624 const size_t pattern_num = sizeof(patterns)/sizeof(UrlMatchPattern);
626 /* initalize the regexps */
627 for (i = 0; i != pattern_num; ++i) {
628 patterns[i].preg = g_new0 (regex_t,1);
629 g_assert(regcomp (patterns[i].preg, patterns[i].regex,
630 REG_ICASE|REG_EXTENDED|REG_NEWLINE) == 0);
632 /* find all the matches */
633 for (i = 0; i != pattern_num; ++i) {
637 if ((rv = regexec (patterns[i].preg, txt->str + offset, 1, &rm, 0)) != 0) {
638 g_assert (rv == REG_NOMATCH); /* this should not happen */
639 break; /* try next regexp */
644 /* FIXME: optimize this */
645 /* to avoid partial matches on something that was already found... */
646 /* check_partial_match will put -1 in the data ptr if that is the case */
647 test_offset = offset + rm.rm_so;
648 g_slist_foreach (match_list, (GFunc)chk_partial_match, &test_offset);
650 /* make a list of our matches (<offset, len, prefix> tupels)*/
651 if (test_offset != -1) {
652 url_match_t *match = g_new (url_match_t,1);
653 match->offset = offset + rm.rm_so;
654 match->len = rm.rm_eo - rm.rm_so;
655 match->prefix = patterns[i].prefix;
656 match_list = g_slist_prepend (match_list, match);
662 for (i = 0; i != pattern_num; ++i) {
663 regfree (patterns[i].preg);
664 g_free (patterns[i].preg);
665 } /* don't free patterns itself -- it's static */
667 /* now sort the list, so the matches are in reverse order of occurence.
668 * that way, we can do the replacements starting from the end, so we don't need
669 * to recalculate the offsets
671 match_list = g_slist_sort (match_list,
672 (GCompareFunc)cmp_offsets_reverse);
677 fill_gtkhtml_with_txt (ModestTnyMsgView *self, GtkHTML* gtkhtml, const gchar* txt, TnyMsgIface *msg)
680 gchar *html_attachments;
682 g_return_val_if_fail (gtkhtml, FALSE);
683 g_return_val_if_fail (txt, FALSE);
685 html = g_string_new(convert_to_html (txt));
686 html_attachments = attachments_as_html(self, msg);
687 g_string_append(html, html_attachments);
689 gtk_html_load_from_string (gtkhtml, html->str, html->len);
690 g_string_free (html, TRUE);
691 g_free(html_attachments);
699 set_html_message (ModestTnyMsgView *self, TnyMsgMimePartIface *tny_body, TnyMsgIface *msg)
701 gchar *html_attachments;
702 TnyStreamIface *gtkhtml_stream;
703 ModestTnyMsgViewPrivate *priv;
705 g_return_val_if_fail (self, FALSE);
706 g_return_val_if_fail (tny_body, FALSE);
708 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
711 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new
712 (gtk_html_begin(GTK_HTML(priv->gtkhtml))));
714 tny_stream_iface_reset (gtkhtml_stream);
715 tny_msg_mime_part_iface_decode_to_stream (tny_body, gtkhtml_stream);
716 html_attachments = attachments_as_html(self, msg);
717 tny_stream_iface_write (gtkhtml_stream, html_attachments, strlen(html_attachments));
718 tny_stream_iface_reset (gtkhtml_stream);
720 g_object_unref (G_OBJECT(gtkhtml_stream));
721 g_free (html_attachments);
727 /* this is a hack --> we use the tny_text_buffer_stream to
728 * get the message text, then write to gtkhtml 'by hand' */
730 set_text_message (ModestTnyMsgView *self, TnyMsgMimePartIface *tny_body, TnyMsgIface *msg)
733 GtkTextIter begin, end;
734 TnyStreamIface* txt_stream;
736 ModestTnyMsgViewPrivate *priv;
738 g_return_val_if_fail (self, FALSE);
739 g_return_val_if_fail (tny_body, FALSE);
741 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
743 buf = gtk_text_buffer_new (NULL);
744 txt_stream = TNY_STREAM_IFACE(tny_text_buffer_stream_new (buf));
746 tny_stream_iface_reset (txt_stream);
747 tny_msg_mime_part_iface_decode_to_stream (tny_body, txt_stream);
748 tny_stream_iface_reset (txt_stream);
750 gtk_text_buffer_get_bounds (buf, &begin, &end);
751 txt = gtk_text_buffer_get_text (buf, &begin, &end, FALSE);
753 fill_gtkhtml_with_txt (self, GTK_HTML(priv->gtkhtml), txt, msg);
755 g_object_unref (G_OBJECT(txt_stream));
756 g_object_unref (G_OBJECT(buf));
763 modest_tny_msg_view_get_selected_text (ModestTnyMsgView *self)
765 ModestTnyMsgViewPrivate *priv;
771 g_return_val_if_fail (self, NULL);
772 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
773 html = priv->gtkhtml;
775 /* I'm sure there is a better way to check for selected text */
776 sel = gtk_html_get_selection_html(GTK_HTML(html), &len);
782 clip = gtk_widget_get_clipboard(html, GDK_SELECTION_PRIMARY);
783 return gtk_clipboard_wait_for_text(clip);
787 modest_tny_msg_view_set_message (ModestTnyMsgView *self, TnyMsgIface *msg)
789 TnyMsgMimePartIface *body;
790 ModestTnyMsgViewPrivate *priv;
792 g_return_if_fail (self);
794 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
798 fill_gtkhtml_with_txt (self, GTK_HTML(priv->gtkhtml), "", msg);
802 body = modest_tny_msg_actions_find_body_part (msg, TRUE);
804 if (tny_msg_mime_part_iface_content_type_is (body, "text/html"))
805 set_html_message (self, body, msg);
807 set_text_message (self, body, msg);
810 /* nothing to show */
815 modest_tny_msg_view_redraw (ModestTnyMsgView *self)
817 ModestTnyMsgViewPrivate *priv;
819 g_return_if_fail (self);
820 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
821 modest_tny_msg_view_set_message(self, priv->msg);
825 modest_tny_msg_view_get_show_attachments_inline_flag (ModestTnyMsgView *self)
827 ModestTnyMsgViewPrivate *priv;
829 g_return_val_if_fail (self, FALSE);
830 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
831 return priv->show_attachments_inline;
835 modest_tny_msg_view_set_show_attachments_inline_flag (ModestTnyMsgView *self, gboolean flag)
837 ModestTnyMsgViewPrivate *priv;
840 g_return_val_if_fail (self, FALSE);
841 priv = MODEST_TNY_MSG_VIEW_GET_PRIVATE(self);
842 oldflag = priv->show_attachments_inline;
843 priv->show_attachments_inline = flag;
844 if (priv->show_attachments_inline != oldflag)
845 modest_tny_msg_view_redraw(self);
846 return priv->show_attachments_inline;