1 /* Copyright (c) 2006, Nokia Corporation
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
8 * * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * * Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * * Neither the name of the Nokia Corporation nor the names of its
14 * contributors may be used to endorse or promote products derived from
15 * this software without specific prior written permission.
17 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
18 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
19 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
20 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
21 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 #include <tny-text-buffer-stream.h>
35 #include <glib/gi18n.h>
36 #include <gtkhtml/gtkhtml.h>
37 #include <gtkhtml/gtkhtml-stream.h>
38 #include <tny-list-iface.h>
40 #include <modest-tny-msg-actions.h>
41 #include "modest-msg-view.h"
42 #include "modest-tny-stream-gtkhtml.h"
45 /* 'private'/'protected' functions */
46 static void modest_msg_view_class_init (ModestMsgViewClass *klass);
47 static void modest_msg_view_init (ModestMsgView *obj);
48 static void modest_msg_view_finalize (GObject *obj);
51 static GSList* get_url_matches (GString *txt);
52 static gboolean on_link_clicked (GtkWidget *widget, const gchar *uri,
53 ModestMsgView *msg_view);
54 static gboolean on_url_requested (GtkWidget *widget, const gchar *uri,
55 GtkHTMLStream *stream,
56 ModestMsgView *msg_view);
59 * we need these regexps to find URLs in plain text e-mails
61 typedef struct _UrlMatchPattern UrlMatchPattern;
62 struct _UrlMatchPattern {
69 #define ATT_PREFIX "att:"
71 #define MAIL_VIEWER_URL_MATCH_PATTERNS { \
72 { "(file|http|ftp|https)://[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]+[-A-Za-z0-9_$%&=?/~#]",\
74 { "www\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
76 { "ftp\\.[-a-z0-9.]+[-a-z0-9](:[0-9]*)?(/[-A-Za-z0-9_$.+!*(),;:@%&=?/~#]*[^]}\\),?!;:\"]?)?",\
78 { "(voipto|callto|chatto|jabberto|xmpp):[-_a-z@0-9.\\+]+", \
80 { "mailto:[-_a-z0-9.\\+]+@[-_a-z0-9.]+", \
82 { "[-_a-z0-9.\\+]+@[-_a-z0-9.]+",\
90 ATTACHMENT_CLICKED_SIGNAL,
94 typedef struct _ModestMsgViewPrivate ModestMsgViewPrivate;
95 struct _ModestMsgViewPrivate {
97 const TnyMsgIface *msg;
99 #define MODEST_MSG_VIEW_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), \
100 MODEST_TYPE_MSG_VIEW, \
101 ModestMsgViewPrivate))
103 static GtkContainerClass *parent_class = NULL;
105 /* uncomment the following if you have defined any signals */
106 static guint signals[LAST_SIGNAL] = {0};
109 modest_msg_view_get_type (void)
111 static GType my_type = 0;
113 static const GTypeInfo my_info = {
114 sizeof(ModestMsgViewClass),
115 NULL, /* base init */
116 NULL, /* base finalize */
117 (GClassInitFunc) modest_msg_view_class_init,
118 NULL, /* class finalize */
119 NULL, /* class data */
120 sizeof(ModestMsgView),
122 (GInstanceInitFunc) modest_msg_view_init,
124 my_type = g_type_register_static (GTK_TYPE_SCROLLED_WINDOW,
132 modest_msg_view_class_init (ModestMsgViewClass *klass)
134 GObjectClass *gobject_class;
135 gobject_class = (GObjectClass*) klass;
137 parent_class = g_type_class_peek_parent (klass);
138 gobject_class->finalize = modest_msg_view_finalize;
140 g_type_class_add_private (gobject_class, sizeof(ModestMsgViewPrivate));
143 signals[LINK_CLICKED_SIGNAL] =
144 g_signal_new ("link_clicked",
145 G_TYPE_FROM_CLASS (gobject_class),
147 G_STRUCT_OFFSET(ModestMsgViewClass, link_clicked),
149 g_cclosure_marshal_VOID__STRING,
150 G_TYPE_NONE, 1, G_TYPE_STRING);
152 signals[ATTACHMENT_CLICKED_SIGNAL] =
153 g_signal_new ("attachment_clicked",
154 G_TYPE_FROM_CLASS (gobject_class),
156 G_STRUCT_OFFSET(ModestMsgViewClass, attachment_clicked),
158 g_cclosure_marshal_VOID__POINTER,
159 G_TYPE_NONE, 1, G_TYPE_INT);
164 modest_msg_view_init (ModestMsgView *obj)
166 ModestMsgViewPrivate *priv;
168 priv = MODEST_MSG_VIEW_GET_PRIVATE(obj);
171 priv->gtkhtml = gtk_html_new();
173 gtk_html_set_editable (GTK_HTML(priv->gtkhtml), FALSE);
174 gtk_html_allow_selection (GTK_HTML(priv->gtkhtml), TRUE);
175 gtk_html_set_caret_mode (GTK_HTML(priv->gtkhtml), FALSE);
176 gtk_html_set_blocking (GTK_HTML(priv->gtkhtml), FALSE);
177 gtk_html_set_images_blocking (GTK_HTML(priv->gtkhtml), FALSE);
179 g_signal_connect (G_OBJECT(priv->gtkhtml), "link_clicked",
180 G_CALLBACK(on_link_clicked), obj);
182 g_signal_connect (G_OBJECT(priv->gtkhtml), "url_requested",
183 G_CALLBACK(on_url_requested), obj);
188 modest_msg_view_finalize (GObject *obj)
190 G_OBJECT_CLASS(parent_class)->finalize (obj);
195 modest_msg_view_new (const TnyMsgIface *msg)
199 ModestMsgViewPrivate *priv;
201 obj = G_OBJECT(g_object_new(MODEST_TYPE_MSG_VIEW, NULL));
202 self = MODEST_MSG_VIEW(obj);
203 priv = MODEST_MSG_VIEW_GET_PRIVATE (self);
205 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self),
206 GTK_POLICY_AUTOMATIC,
207 GTK_POLICY_AUTOMATIC);
210 gtk_container_add (GTK_CONTAINER(obj), priv->gtkhtml);
213 modest_msg_view_set_message (self, msg);
215 return GTK_WIDGET(self);
220 on_link_clicked (GtkWidget *widget, const gchar *uri, ModestMsgView *msg_view)
225 g_return_val_if_fail (msg_view, FALSE);
227 /* is it an attachment? */
228 if (g_str_has_prefix(uri, ATT_PREFIX)) {
230 index = atoi (uri + strlen(ATT_PREFIX));
233 /* index is 1-based, so 0 indicates an error */
234 g_printerr ("modest: invalid attachment id: %s\n", uri);
238 g_signal_emit (G_OBJECT(msg_view), signals[ATTACHMENT_CLICKED_SIGNAL],
243 g_signal_emit (G_OBJECT(msg_view), signals[LINK_CLICKED_SIGNAL], 0, uri);
249 static TnyMsgMimePartIface *
250 find_cid_image (const TnyMsgIface *msg, const gchar *cid)
252 TnyMsgMimePartIface *part = NULL;
253 const TnyListIface *parts;
254 TnyIteratorIface *iter;
256 g_return_val_if_fail (msg, NULL);
257 g_return_val_if_fail (cid, NULL);
259 parts = tny_msg_iface_get_parts ((TnyMsgIface*)msg); // FIXME: tinymail
260 iter = tny_list_iface_create_iterator ((TnyListIface*)parts);
262 while (!tny_iterator_iface_is_done(iter)) {
263 const gchar *part_cid;
264 part = TNY_MSG_MIME_PART_IFACE(tny_iterator_iface_current(iter));
265 part_cid = tny_msg_mime_part_iface_get_content_id (part);
267 if (part_cid && strcmp (cid, part_cid) == 0)
271 tny_iterator_iface_next (iter);
274 g_object_unref (G_OBJECT(iter));
280 on_url_requested (GtkWidget *widget, const gchar *uri,
281 GtkHTMLStream *stream,
282 ModestMsgView *msg_view)
284 ModestMsgViewPrivate *priv;
285 priv = MODEST_MSG_VIEW_GET_PRIVATE (msg_view);
287 if (g_str_has_prefix (uri, "cid:")) {
288 /* +4 ==> skip "cid:" */
289 const TnyMsgMimePartIface *part = find_cid_image (priv->msg, uri + 4);
291 g_printerr ("modest: '%s' not found\n", uri + 4);
292 gtk_html_stream_close (stream, GTK_HTML_STREAM_ERROR);
294 TnyStreamIface *tny_stream =
295 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new(stream));
297 tny_msg_mime_part_iface_decode_to_stream ((TnyMsgMimePartIface*)part,
299 gtk_html_stream_close (stream, GTK_HTML_STREAM_OK);
315 /* render the attachments as hyperlinks in html */
317 attachments_as_html (ModestMsgView *self, const TnyMsgIface *msg)
319 ModestMsgViewPrivate *priv;
321 const TnyListIface *parts;
322 TnyIteratorIface *iter;
329 priv = MODEST_MSG_VIEW_GET_PRIVATE (self);
330 parts = tny_msg_iface_get_parts ((TnyMsgIface*)msg);
332 iter = tny_list_iface_create_iterator ((TnyListIface*)parts);
334 appendix= g_string_new ("");
336 while (!tny_iterator_iface_is_done(iter)) {
337 TnyMsgMimePartIface *part;
339 ++index; /* attachment numbers are 1-based */
341 part = TNY_MSG_MIME_PART_IFACE(tny_iterator_iface_current (iter));
343 if (tny_msg_mime_part_iface_is_attachment (part)) {
345 const gchar *filename = tny_msg_mime_part_iface_get_filename(part);
347 filename = _("attachment");
349 g_string_append_printf (appendix, "<a href=\"%s%d\">%s</a> \n",
350 ATT_PREFIX, index, filename);
352 tny_iterator_iface_next (iter);
354 g_object_unref (G_OBJECT(iter));
356 if (appendix->len == 0)
357 return g_string_free (appendix, TRUE);
359 html = g_strdup_printf ("<strong>%s:</strong> %s\n<hr>",
360 _("Attachments"), appendix->str);
361 g_string_free (appendix, TRUE);
369 hyperlinkify_plain_text (GString *txt)
372 GSList *match_list = get_url_matches (txt);
374 /* we will work backwards, so the offsets stay valid */
375 for (cursor = match_list; cursor; cursor = cursor->next) {
377 url_match_t *match = (url_match_t*) cursor->data;
378 gchar *url = g_strndup (txt->str + match->offset, match->len);
379 gchar *repl = NULL; /* replacement */
381 /* the prefix is NULL: use the one that is already there */
382 repl = g_strdup_printf ("<a href=\"%s%s\">%s</a>",
383 match->prefix ? match->prefix : "", url, url);
385 /* replace the old thing with our hyperlink
386 * replacement thing */
387 g_string_erase (txt, match->offset, match->len);
388 g_string_insert (txt, match->offset, repl);
393 g_free (cursor->data);
396 g_slist_free (match_list);
402 convert_to_html (const gchar *data)
405 gboolean first_space = TRUE;
413 html = g_string_sized_new (len + 100); /* just a guess... */
415 g_string_append_printf (html,
418 "<meta http-equiv=\"content-type\""
419 " content=\"text/html; charset=utf8\">"
423 /* replace with special html chars where needed*/
424 for (i = 0; i != len; ++i) {
428 case 0: break; /* ignore embedded \0s */
429 case '<' : g_string_append (html, "<"); break;
430 case '>' : g_string_append (html, ">"); break;
431 case '&' : g_string_append (html, """); break;
432 case '\n': g_string_append (html, "<br>\n"); break;
435 g_string_append (html, first_space ? " " : " ");
437 } else if (kar == '\t')
438 g_string_append (html, " ");
442 /* optimization trick: accumulate 'normal' chars, then copy */
444 kar = data [++charnum + i];
446 } while ((i + charnum < len) &&
447 (kar > '>' || (kar != '<' && kar != '>'
448 && kar != '&' && kar != ' '
449 && kar != '\n' && kar != '\t')));
450 g_string_append_len (html, &data[i], charnum);
456 g_string_append (html, "</tt></body></html>");
457 hyperlinkify_plain_text (html);
459 return g_string_free (html, FALSE);
466 cmp_offsets_reverse (const url_match_t *match1, const url_match_t *match2)
468 return match2->offset - match1->offset;
474 * check if the match is inside an existing match... */
476 chk_partial_match (const url_match_t *match, int* offset)
478 if (*offset >= match->offset && *offset < match->offset + match->len)
483 get_url_matches (GString *txt)
486 int rv, i, offset = 0;
487 GSList *match_list = NULL;
489 static UrlMatchPattern patterns[] = MAIL_VIEWER_URL_MATCH_PATTERNS;
490 const size_t pattern_num = sizeof(patterns)/sizeof(UrlMatchPattern);
492 /* initalize the regexps */
493 for (i = 0; i != pattern_num; ++i) {
494 patterns[i].preg = g_new0 (regex_t,1);
495 g_assert(regcomp (patterns[i].preg, patterns[i].regex,
496 REG_ICASE|REG_EXTENDED|REG_NEWLINE) == 0);
498 /* find all the matches */
499 for (i = 0; i != pattern_num; ++i) {
503 if ((rv = regexec (patterns[i].preg, txt->str + offset, 1, &rm, 0)) != 0) {
504 g_assert (rv == REG_NOMATCH); /* this should not happen */
505 break; /* try next regexp */
510 /* FIXME: optimize this */
511 /* to avoid partial matches on something that was already found... */
512 /* check_partial_match will put -1 in the data ptr if that is the case */
513 test_offset = offset + rm.rm_so;
514 g_slist_foreach (match_list, (GFunc)chk_partial_match, &test_offset);
516 /* make a list of our matches (<offset, len, prefix> tupels)*/
517 if (test_offset != -1) {
518 url_match_t *match = g_new (url_match_t,1);
519 match->offset = offset + rm.rm_so;
520 match->len = rm.rm_eo - rm.rm_so;
521 match->prefix = patterns[i].prefix;
522 match_list = g_slist_prepend (match_list, match);
528 for (i = 0; i != pattern_num; ++i) {
529 regfree (patterns[i].preg);
530 g_free (patterns[i].preg);
531 } /* don't free patterns itself -- it's static */
533 /* now sort the list, so the matches are in reverse order of occurence.
534 * that way, we can do the replacements starting from the end, so we don't need
535 * to recalculate the offsets
537 match_list = g_slist_sort (match_list,
538 (GCompareFunc)cmp_offsets_reverse);
545 set_html_message (ModestMsgView *self, const TnyMsgMimePartIface *tny_body,
546 const TnyMsgIface *msg)
548 gchar *html_attachments;
549 TnyStreamIface *gtkhtml_stream;
550 ModestMsgViewPrivate *priv;
552 g_return_val_if_fail (self, FALSE);
553 g_return_val_if_fail (tny_body, FALSE);
555 priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
558 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new
559 (gtk_html_begin(GTK_HTML(priv->gtkhtml))));
561 tny_stream_iface_reset (gtkhtml_stream);
563 html_attachments = attachments_as_html(self, msg);
564 if (html_attachments) {
565 tny_stream_iface_write (gtkhtml_stream, html_attachments,
566 strlen(html_attachments));
567 tny_stream_iface_reset (gtkhtml_stream);
568 g_free (html_attachments);
572 tny_msg_mime_part_iface_decode_to_stream ((TnyMsgMimePartIface*)tny_body,
575 g_object_unref (G_OBJECT(gtkhtml_stream));
581 /* this is a hack --> we use the tny_text_buffer_stream to
582 * get the message text, then write to gtkhtml 'by hand' */
584 set_text_message (ModestMsgView *self, const TnyMsgMimePartIface *tny_body,
585 const TnyMsgIface *msg)
588 GtkTextIter begin, end;
589 TnyStreamIface* txt_stream, *gtkhtml_stream;
590 gchar *txt, *html_attachments;
591 ModestMsgViewPrivate *priv;
593 g_return_val_if_fail (self, FALSE);
594 g_return_val_if_fail (tny_body, FALSE);
596 priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
598 buf = gtk_text_buffer_new (NULL);
599 txt_stream = TNY_STREAM_IFACE(tny_text_buffer_stream_new (buf));
601 tny_stream_iface_reset (txt_stream);
604 TNY_STREAM_IFACE(modest_tny_stream_gtkhtml_new
605 (gtk_html_begin(GTK_HTML(priv->gtkhtml))));
607 html_attachments = attachments_as_html(self, msg);
608 if (html_attachments) {
609 tny_stream_iface_write (gtkhtml_stream, html_attachments,
610 strlen(html_attachments));
611 tny_stream_iface_reset (gtkhtml_stream);
612 g_free (html_attachments);
616 tny_msg_mime_part_iface_decode_to_stream ((TnyMsgMimePartIface*)tny_body,
618 tny_stream_iface_reset (txt_stream);
620 gtk_text_buffer_get_bounds (buf, &begin, &end);
621 txt = gtk_text_buffer_get_text (buf, &begin, &end, FALSE);
623 gchar *html = convert_to_html (txt);
624 tny_stream_iface_write (gtkhtml_stream, html, strlen(html));
625 tny_stream_iface_reset (gtkhtml_stream);
630 g_object_unref (G_OBJECT(gtkhtml_stream));
631 g_object_unref (G_OBJECT(txt_stream));
632 g_object_unref (G_OBJECT(buf));
639 set_empty_message (ModestMsgView *self)
641 ModestMsgViewPrivate *priv;
643 g_return_val_if_fail (self, FALSE);
644 priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
646 gtk_html_load_from_string (GTK_HTML(priv->gtkhtml),
654 modest_msg_view_get_selected_text (ModestMsgView *self)
656 ModestMsgViewPrivate *priv;
662 g_return_val_if_fail (self, NULL);
663 priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
664 html = priv->gtkhtml;
666 /* I'm sure there is a better way to check for selected text */
667 sel = gtk_html_get_selection_html(GTK_HTML(html), &len);
673 clip = gtk_widget_get_clipboard(html, GDK_SELECTION_PRIMARY);
674 return gtk_clipboard_wait_for_text(clip);
679 modest_msg_view_set_message (ModestMsgView *self, const TnyMsgIface *msg)
681 TnyMsgMimePartIface *body;
682 ModestMsgViewPrivate *priv;
684 g_return_if_fail (self);
686 priv = MODEST_MSG_VIEW_GET_PRIVATE(self);
691 set_empty_message (self);
695 body = modest_tny_msg_actions_find_body_part (msg, TRUE);
697 if (tny_msg_mime_part_iface_content_type_is (body, "text/html"))
698 set_html_message (self, body, msg);
700 set_text_message (self, body, msg);
703 set_empty_message (self);