2 * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3 * Copyright (C) 1999-2004 Hiroyuki Yamamoto
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
28 #include <gdk/gdkkeysyms.h>
29 #include <gtk/gtkvbox.h>
30 #include <gtk/gtkscrolledwindow.h>
31 #include <gtk/gtksignal.h>
39 #include "summaryview.h"
40 #include "procheader.h"
41 #include "prefs_common.h"
49 #include "addressbook.h"
50 #include "displayheader.h"
53 #include "alertpanel.h"
63 static GdkColor quote_colors[3] = {
64 {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
65 {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
66 {(gulong)0, (gushort)0, (gushort)0, (gushort)0}
69 static GdkColor signature_color = {
76 static GdkColor uri_color = {
83 static GdkColor emphasis_color = {
91 static GdkColor error_color = {
100 static GdkCursor *hand_cursor = NULL;
101 static GdkCursor *text_cursor = NULL;
103 #define TEXTVIEW_STATUSBAR_PUSH(textview, str) \
105 gtk_statusbar_push(GTK_STATUSBAR(textview->messageview->statusbar), \
106 textview->messageview->statusbar_cid, str); \
109 #define TEXTVIEW_STATUSBAR_POP(textview) \
111 gtk_statusbar_pop(GTK_STATUSBAR(textview->messageview->statusbar), \
112 textview->messageview->statusbar_cid); \
115 static void textview_show_ertf (TextView *textview,
117 CodeConverter *conv);
118 static void textview_add_part (TextView *textview,
120 static void textview_add_parts (TextView *textview,
122 static void textview_write_body (TextView *textview,
124 static void textview_show_html (TextView *textview,
126 CodeConverter *conv);
128 static void textview_write_line (TextView *textview,
130 CodeConverter *conv);
131 static void textview_write_link (TextView *textview,
134 CodeConverter *conv);
136 static GPtrArray *textview_scan_header (TextView *textview,
138 static void textview_show_header (TextView *textview,
141 static gint textview_key_pressed (GtkWidget *widget,
144 static gboolean textview_motion_notify (GtkWidget *widget,
145 GdkEventMotion *motion,
147 static gboolean textview_leave_notify (GtkWidget *widget,
148 GdkEventCrossing *event,
150 static gboolean textview_visibility_notify (GtkWidget *widget,
151 GdkEventVisibility *event,
153 static void textview_uri_update (TextView *textview,
156 static gboolean textview_get_uri_range (TextView *textview,
159 GtkTextIter *start_iter,
160 GtkTextIter *end_iter);
161 static RemoteURI *textview_get_uri_from_range (TextView *textview,
164 GtkTextIter *start_iter,
165 GtkTextIter *end_iter);
166 static RemoteURI *textview_get_uri (TextView *textview,
169 static gboolean textview_uri_button_pressed (GtkTextTag *tag,
175 static void textview_smooth_scroll_do (TextView *textview,
179 static void textview_smooth_scroll_one_line (TextView *textview,
181 static gboolean textview_smooth_scroll_page (TextView *textview,
184 static gboolean textview_uri_security_check (TextView *textview,
186 static void textview_uri_list_remove_all (GSList *uri_list);
189 static void populate_popup(GtkTextView *textview, GtkMenu *menu,
192 gtk_menu_detach(menu);
195 TextView *textview_create(void)
199 GtkWidget *scrolledwin;
201 GtkTextBuffer *buffer;
202 GtkClipboard *clipboard;
203 PangoFontDescription *font_desc = NULL;
205 debug_print("Creating text view...\n");
206 textview = g_new0(TextView, 1);
208 scrolledwin = gtk_scrolled_window_new(NULL, NULL);
209 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin),
210 GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
211 gtk_widget_set_size_request(scrolledwin, prefs_common.mainview_width, -1);
212 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwin),
215 /* create GtkSText widgets for single-byte and multi-byte character */
216 text = gtk_text_view_new();
217 gtk_widget_add_events(text, GDK_LEAVE_NOTIFY_MASK);
218 gtk_widget_show(text);
219 gtk_text_view_set_editable(GTK_TEXT_VIEW(text), FALSE);
220 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text), GTK_WRAP_WORD_CHAR);
221 gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(text), FALSE);
222 g_signal_connect(G_OBJECT(text), "populate-popup",
223 G_CALLBACK(populate_popup), NULL);
226 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text));
227 clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
228 gtk_text_buffer_add_selection_clipboard(buffer, clipboard);
230 gtk_widget_ensure_style(text);
232 if (prefs_common.normalfont)
233 font_desc = pango_font_description_from_string
234 (prefs_common.normalfont);
236 gtk_widget_modify_font(text, font_desc);
238 pango_font_description_free(font_desc);
240 gtk_widget_ref(scrolledwin);
242 gtk_container_add(GTK_CONTAINER(scrolledwin), text);
245 hand_cursor = gdk_cursor_new(GDK_HAND2);
247 text_cursor = gdk_cursor_new(GDK_XTERM);
249 g_signal_connect(G_OBJECT(text), "key_press_event",
250 G_CALLBACK(textview_key_pressed),
252 g_signal_connect(G_OBJECT(text), "motion_notify_event",
253 G_CALLBACK(textview_motion_notify),
255 g_signal_connect(G_OBJECT(text), "leave_notify_event",
256 G_CALLBACK(textview_leave_notify),
258 g_signal_connect(G_OBJECT(text), "visibility_notify_event",
259 G_CALLBACK(textview_visibility_notify),
262 gtk_widget_show(scrolledwin);
264 vbox = gtk_vbox_new(FALSE, 0);
265 gtk_box_pack_start(GTK_BOX(vbox), scrolledwin, TRUE, TRUE, 0);
267 gtk_widget_show(vbox);
269 textview->vbox = vbox;
270 textview->scrolledwin = scrolledwin;
271 textview->text = text;
272 textview->uri_list = NULL;
273 textview->body_pos = 0;
274 textview->show_all_headers = FALSE;
275 textview->last_buttonpress = GDK_NOTHING;
280 static void textview_create_tags(GtkTextView *text, TextView *textview)
282 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
285 gtk_text_buffer_create_tag(buffer, "header",
286 "pixels-above-lines", 0,
287 "pixels-above-lines-set", TRUE,
288 "pixels-below-lines", 0,
289 "pixels-below-lines-set", TRUE,
291 "left-margin-set", TRUE,
293 gtk_text_buffer_create_tag(buffer, "header_title",
294 "font", prefs_common.boldfont,
296 gtk_text_buffer_create_tag(buffer, "quote0",
297 "foreground-gdk", "e_colors[0],
299 gtk_text_buffer_create_tag(buffer, "quote1",
300 "foreground-gdk", "e_colors[1],
302 gtk_text_buffer_create_tag(buffer, "quote2",
303 "foreground-gdk", "e_colors[2],
305 gtk_text_buffer_create_tag(buffer, "emphasis",
306 "foreground-gdk", &emphasis_color,
308 gtk_text_buffer_create_tag(buffer, "signature",
309 "foreground-gdk", &signature_color,
311 tag = gtk_text_buffer_create_tag(buffer, "link",
312 "foreground-gdk", &uri_color,
314 gtk_text_buffer_create_tag(buffer, "link-hover",
315 "underline", PANGO_UNDERLINE_SINGLE,
318 g_signal_connect(G_OBJECT(tag), "event",
319 G_CALLBACK(textview_uri_button_pressed), textview);
322 void textview_init(TextView *textview)
324 gtkut_widget_disable_theme_engine(textview->text);
325 textview_update_message_colors();
326 textview_set_all_headers(textview, FALSE);
327 textview_set_font(textview, NULL);
329 textview_create_tags(GTK_TEXT_VIEW(textview->text), textview);
332 void textview_update_message_colors(void)
334 GdkColor black = {0, 0, 0, 0};
336 if (prefs_common.enable_color) {
337 /* grab the quote colors, converting from an int to a GdkColor */
338 gtkut_convert_int_to_gdk_color(prefs_common.quote_level1_col,
340 gtkut_convert_int_to_gdk_color(prefs_common.quote_level2_col,
342 gtkut_convert_int_to_gdk_color(prefs_common.quote_level3_col,
344 gtkut_convert_int_to_gdk_color(prefs_common.uri_col,
346 gtkut_convert_int_to_gdk_color(prefs_common.signature_col,
349 quote_colors[0] = quote_colors[1] = quote_colors[2] =
350 uri_color = emphasis_color = signature_color = black;
354 void textview_show_message(TextView *textview, MimeInfo *mimeinfo,
359 if ((fp = fopen(file, "rb")) == NULL) {
360 FILE_OP_ERROR(file, "fopen");
364 textview_clear(textview);
366 textview_add_parts(textview, mimeinfo);
370 textview_set_position(textview, 0);
373 void textview_show_part(TextView *textview, MimeInfo *mimeinfo, FILE *fp)
377 g_return_if_fail(mimeinfo != NULL);
378 g_return_if_fail(fp != NULL);
380 if ((mimeinfo->type == MIMETYPE_MULTIPART) ||
381 ((mimeinfo->type == MIMETYPE_MESSAGE) && !g_ascii_strcasecmp(mimeinfo->subtype, "rfc822"))) {
382 textview_clear(textview);
383 textview_add_parts(textview, mimeinfo);
387 if (fseek(fp, mimeinfo->offset, SEEK_SET) < 0)
390 textview_clear(textview);
392 if (mimeinfo->type == MIMETYPE_MULTIPART)
393 textview_add_parts(textview, mimeinfo);
395 textview_write_body(textview, mimeinfo);
399 static void textview_add_part(TextView *textview, MimeInfo *mimeinfo)
402 GtkTextBuffer *buffer;
405 GPtrArray *headers = NULL;
410 g_return_if_fail(mimeinfo != NULL);
411 text = GTK_TEXT_VIEW(textview->text);
412 buffer = gtk_text_view_get_buffer(text);
413 charcount = gtk_text_buffer_get_char_count(buffer);
414 gtk_text_buffer_get_end_iter(buffer, &iter);
416 if (mimeinfo->type == MIMETYPE_MULTIPART) return;
418 if ((mimeinfo->type == MIMETYPE_MESSAGE) && !g_ascii_strcasecmp(mimeinfo->subtype, "rfc822")) {
421 fp = fopen(mimeinfo->data.filename, "rb");
422 fseek(fp, mimeinfo->offset, SEEK_SET);
423 headers = textview_scan_header(textview, fp);
426 gtk_text_buffer_insert(buffer, &iter, "\n", 1);
427 textview_show_header(textview, headers);
428 procheader_header_array_destroy(headers);
434 name = procmime_mimeinfo_get_parameter(mimeinfo, "filename");
435 content_type = procmime_get_content_type_str(mimeinfo->type,
438 name = procmime_mimeinfo_get_parameter(mimeinfo, "name");
440 g_snprintf(buf, sizeof(buf), "\n[%s %s (%d bytes)]\n",
441 name, content_type, mimeinfo->length);
443 g_snprintf(buf, sizeof(buf), "\n[%s (%d bytes)]\n",
444 content_type, mimeinfo->length);
446 g_free(content_type);
448 if (mimeinfo->type != MIMETYPE_TEXT) {
449 gtk_text_buffer_insert(buffer, &iter, buf, -1);
450 } else if (mimeinfo->disposition != DISPOSITIONTYPE_ATTACHMENT) {
451 if (prefs_common.display_header && (charcount > 0))
452 gtk_text_buffer_insert(buffer, &iter, "\n", 1);
454 textview_write_body(textview, mimeinfo);
458 static void recursive_add_parts(TextView *textview, GNode *node)
463 mimeinfo = (MimeInfo *) node->data;
465 textview_add_part(textview, mimeinfo);
467 if ((mimeinfo->type != MIMETYPE_MULTIPART) &&
468 (mimeinfo->type != MIMETYPE_MESSAGE))
471 if (g_ascii_strcasecmp(mimeinfo->subtype, "alternative") == 0) {
472 GNode * prefered_body;
480 prefered_body = NULL;
483 for(iter = g_node_first_child(node) ; iter != NULL ;
484 iter = g_node_next_sibling(iter)) {
489 submime = (MimeInfo *) iter->data;
490 if (submime->type == MIMETYPE_TEXT)
493 if (submime->subtype != NULL) {
494 if (g_ascii_strcasecmp(submime->subtype, "plain") == 0)
498 if (score > prefered_score) {
499 prefered_score = score;
500 prefered_body = iter;
504 if (prefered_body != NULL) {
505 recursive_add_parts(textview, prefered_body);
509 for(iter = g_node_first_child(node) ; iter != NULL ;
510 iter = g_node_next_sibling(iter)) {
511 recursive_add_parts(textview, iter);
516 static void textview_add_parts(TextView *textview, MimeInfo *mimeinfo)
518 g_return_if_fail(mimeinfo != NULL);
520 recursive_add_parts(textview, mimeinfo->node);
523 #define TEXT_INSERT(str) \
524 gtk_text_buffer_insert(buffer, &iter, str, -1)
526 void textview_show_error(TextView *textview)
529 GtkTextBuffer *buffer;
532 textview_set_font(textview, NULL);
533 textview_clear(textview);
535 text = GTK_TEXT_VIEW(textview->text);
536 buffer = gtk_text_view_get_buffer(text);
537 gtk_text_buffer_get_start_iter(buffer, &iter);
539 TEXT_INSERT(_("This message can't be displayed.\n"));
543 void textview_show_mime_part(TextView *textview, MimeInfo *partinfo)
546 GtkTextBuffer *buffer;
549 if (!partinfo) return;
551 textview_set_font(textview, NULL);
552 textview_clear(textview);
554 text = GTK_TEXT_VIEW(textview->text);
555 buffer = gtk_text_view_get_buffer(text);
556 gtk_text_buffer_get_start_iter(buffer, &iter);
558 TEXT_INSERT(_("The following can be performed on this part by "));
559 TEXT_INSERT(_("right-clicking the icon or list item:\n"));
561 TEXT_INSERT(_(" To save select 'Save as...' (Shortcut key: 'y')\n"));
562 TEXT_INSERT(_(" To display as text select 'Display as text' "));
563 TEXT_INSERT(_("(Shortcut key: 't')\n"));
564 TEXT_INSERT(_(" To open with an external program select 'Open' "));
565 TEXT_INSERT(_("(Shortcut key: 'l'),\n"));
566 TEXT_INSERT(_(" (alternately double-click, or click the middle "));
567 TEXT_INSERT(_("mouse button),\n"));
568 TEXT_INSERT(_(" or 'Open with...' (Shortcut key: 'o')\n"));
574 static void textview_write_body(TextView *textview, MimeInfo *mimeinfo)
579 const gchar *charset;
581 if (textview->messageview->forced_charset)
582 charset = textview->messageview->forced_charset;
584 charset = procmime_mimeinfo_get_parameter(mimeinfo, "charset");
586 textview_set_font(textview, charset);
588 conv = conv_code_converter_new(charset);
590 procmime_force_encoding(textview->messageview->forced_encoding);
592 textview->is_in_signature = FALSE;
594 procmime_decode_content(mimeinfo);
596 if (!g_ascii_strcasecmp(mimeinfo->subtype, "html")) {
599 filename = procmime_get_tmp_file_name(mimeinfo);
600 if (procmime_get_part(filename, mimeinfo) == 0) {
601 tmpfp = fopen(filename, "rb");
602 textview_show_html(textview, tmpfp, conv);
607 } else if (!g_ascii_strcasecmp(mimeinfo->subtype, "enriched")) {
610 filename = procmime_get_tmp_file_name(mimeinfo);
611 if (procmime_get_part(filename, mimeinfo) == 0) {
612 tmpfp = fopen(filename, "rb");
613 textview_show_ertf(textview, tmpfp, conv);
619 tmpfp = fopen(mimeinfo->data.filename, "rb");
620 fseek(tmpfp, mimeinfo->offset, SEEK_SET);
621 debug_print("Viewing text content of type: %s (length: %d)\n", mimeinfo->subtype, mimeinfo->length);
622 while ((fgets(buf, sizeof(buf), tmpfp) != NULL) &&
623 (ftell(tmpfp) <= mimeinfo->offset + mimeinfo->length))
624 textview_write_line(textview, buf, conv);
628 conv_code_converter_destroy(conv);
629 procmime_force_encoding(0);
632 static void textview_show_html(TextView *textview, FILE *fp,
638 parser = html_parser_new(fp, conv);
639 g_return_if_fail(parser != NULL);
641 while ((str = html_parse(parser)) != NULL) {
642 if (parser->state == HTML_HREF) {
643 /* first time : get and copy the URL */
644 if (parser->href == NULL) {
645 /* ALF - the sylpheed html parser returns an empty string,
646 * if still inside an <a>, but already parsed past HREF */
647 str = strtok(str, " ");
649 parser->href = g_strdup(str);
650 /* the URL may (or not) be followed by the
652 str = strtok(NULL, "");
656 textview_write_link(textview, str, parser->href, NULL);
658 textview_write_line(textview, str, NULL);
660 html_parser_destroy(parser);
663 static void textview_show_ertf(TextView *textview, FILE *fp,
669 parser = ertf_parser_new(fp, conv);
670 g_return_if_fail(parser != NULL);
672 while ((str = ertf_parse(parser)) != NULL) {
673 textview_write_line(textview, str, NULL);
676 ertf_parser_destroy(parser);
679 /* get_uri_part() - retrieves a URI starting from scanpos.
680 Returns TRUE if succesful */
681 static gboolean get_uri_part(const gchar *start, const gchar *scanpos,
682 const gchar **bp, const gchar **ep)
686 g_return_val_if_fail(start != NULL, FALSE);
687 g_return_val_if_fail(scanpos != NULL, FALSE);
688 g_return_val_if_fail(bp != NULL, FALSE);
689 g_return_val_if_fail(ep != NULL, FALSE);
693 /* find end point of URI */
694 for (ep_ = scanpos; *ep_ != '\0'; ep_++) {
695 if (!isgraph(*(const guchar *)ep_) ||
696 !IS_ASCII(*(const guchar *)ep_) ||
697 strchr("()<>\"", *ep_))
701 /* no punctuation at end of string */
703 /* FIXME: this stripping of trailing punctuations may bite with other URIs.
704 * should pass some URI type to this function and decide on that whether
705 * to perform punctuation stripping */
707 #define IS_REAL_PUNCT(ch) (ispunct(ch) && ((ch) != '/'))
709 for (; ep_ - 1 > scanpos + 1 &&
710 IS_REAL_PUNCT(*(const guchar *)(ep_ - 1));
721 static gchar *make_uri_string(const gchar *bp, const gchar *ep)
723 return g_strndup(bp, ep - bp);
726 /* valid mail address characters */
727 #define IS_RFC822_CHAR(ch) \
732 !strchr("(),;<>\"", (ch)))
734 /* alphabet and number within 7bit ASCII */
735 #define IS_ASCII_ALNUM(ch) (IS_ASCII(ch) && isalnum(ch))
736 #define IS_QUOTE(ch) ((ch) == '\'' || (ch) == '"')
738 static GHashTable *create_domain_tab(void)
740 static const gchar *toplvl_domains [] = {
742 "arpa", "coop", "info", "name", "biz", "com", "edu", "gov",
743 "int", "mil", "net", "org", "ac", "ad", "ae", "af", "ag",
744 "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au",
745 "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi",
746 "bj", "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by",
747 "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl",
748 "cm", "cn", "co", "cr", "cu", "cv", "cx", "cy", "cz", "de",
749 "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er",
750 "es", "et", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gd",
751 "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq",
752 "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr",
753 "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir",
754 "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki",
755 "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc",
756 "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc",
757 "md", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq",
758 "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na",
759 "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu",
760 "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm",
761 "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "ru",
762 "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj",
763 "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", "sz",
764 "tc", "td", "tf", "tg", "th", "tj", "tk", "tm", "tn", "to",
765 "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "um",
766 "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu",
767 "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw"
770 GHashTable *htab = g_hash_table_new(g_stricase_hash, g_stricase_equal);
772 g_return_val_if_fail(htab, NULL);
773 for (n = 0; n < sizeof toplvl_domains / sizeof toplvl_domains[0]; n++)
774 g_hash_table_insert(htab, (gpointer) toplvl_domains[n], (gpointer) toplvl_domains[n]);
778 static gboolean is_toplvl_domain(GHashTable *tab, const gchar *first, const gchar *last)
780 const gint MAX_LVL_DOM_NAME_LEN = 6;
781 gchar buf[MAX_LVL_DOM_NAME_LEN + 1];
782 const gchar *m = buf + MAX_LVL_DOM_NAME_LEN + 1;
785 if (last - first > MAX_LVL_DOM_NAME_LEN || first > last)
788 for (p = buf; p < m && first < last; *p++ = *first++)
792 return g_hash_table_lookup(tab, buf) != NULL;
795 /* get_email_part() - retrieves an email address. Returns TRUE if succesful */
796 static gboolean get_email_part(const gchar *start, const gchar *scanpos,
797 const gchar **bp, const gchar **ep)
799 /* more complex than the uri part because we need to scan back and forward starting from
800 * the scan position. */
801 gboolean result = FALSE;
802 const gchar *bp_ = NULL;
803 const gchar *ep_ = NULL;
804 static GHashTable *dom_tab;
805 const gchar *last_dot = NULL;
806 const gchar *prelast_dot = NULL;
807 const gchar *last_tld_char = NULL;
809 /* the informative part of the email address (describing the name
810 * of the email address owner) may contain quoted parts. the
811 * closure stack stores the last encountered quotes. */
812 gchar closure_stack[128];
813 gchar *ptr = closure_stack;
815 g_return_val_if_fail(start != NULL, FALSE);
816 g_return_val_if_fail(scanpos != NULL, FALSE);
817 g_return_val_if_fail(bp != NULL, FALSE);
818 g_return_val_if_fail(ep != NULL, FALSE);
821 dom_tab = create_domain_tab();
822 g_return_val_if_fail(dom_tab, FALSE);
824 /* scan start of address */
825 for (bp_ = scanpos - 1;
826 bp_ >= start && IS_RFC822_CHAR(*(const guchar *)bp_); bp_--)
829 /* TODO: should start with an alnum? */
831 for (; bp_ < scanpos && !IS_ASCII_ALNUM(*(const guchar *)bp_); bp_++)
834 if (bp_ != scanpos) {
835 /* scan end of address */
836 for (ep_ = scanpos + 1;
837 *ep_ && IS_RFC822_CHAR(*(const guchar *)ep_); ep_++)
839 prelast_dot = last_dot;
841 if (*(last_dot + 1) == '.') {
842 if (prelast_dot == NULL)
844 last_dot = prelast_dot;
849 /* TODO: really should terminate with an alnum? */
850 for (; ep_ > scanpos && !IS_ASCII_ALNUM(*(const guchar *)ep_);
855 if (last_dot == NULL)
858 last_dot = prelast_dot;
859 if (last_dot == NULL || (scanpos + 1 >= last_dot))
863 for (last_tld_char = last_dot; last_tld_char < ep_; last_tld_char++)
864 if (*last_tld_char == '?')
867 if (is_toplvl_domain(dom_tab, last_dot, last_tld_char))
874 if (!result) return FALSE;
876 if (*(bp_ - 1) == '"' && *(ep_) == '"'
877 && *(ep_ + 1) == ' ' && *(ep_ + 2) == '<'
878 && IS_RFC822_CHAR(*(ep_ + 3))) {
879 /* this informative part with an @ in it is
880 * followed by the email address */
883 /* go to matching '>' (or next non-rfc822 char, like \n) */
884 for (; *ep_ != '>' && *ep != '\0' && IS_RFC822_CHAR(*ep_); ep_++)
887 /* include the bracket */
888 if (*ep_ == '>') ep_++;
890 /* include the leading quote */
898 /* skip if it's between quotes "'alfons@proteus.demon.nl'" <alfons@proteus.demon.nl> */
899 if (bp_ - 1 > start && IS_QUOTE(*(bp_ - 1)) && IS_QUOTE(*ep_))
902 /* see if this is <bracketed>; in this case we also scan for the informative part. */
903 if (bp_ - 1 <= start || *(bp_ - 1) != '<' || *ep_ != '>')
906 #define FULL_STACK() ((size_t) (ptr - closure_stack) >= sizeof closure_stack)
907 #define IN_STACK() (ptr > closure_stack)
908 /* has underrun check */
909 #define POP_STACK() if(IN_STACK()) --ptr
910 /* has overrun check */
911 #define PUSH_STACK(c) if(!FULL_STACK()) *ptr++ = (c); else return TRUE
912 /* has underrun check */
913 #define PEEK_STACK() (IN_STACK() ? *(ptr - 1) : 0)
917 /* scan for the informative part. */
918 for (bp_ -= 2; bp_ >= start; bp_--) {
919 /* if closure on the stack keep scanning */
920 if (PEEK_STACK() == *bp_) {
924 if (*bp_ == '\'' || *bp_ == '"') {
929 /* if nothing in the closure stack, do the special conditions
930 * the following if..else expression simply checks whether
931 * a token is acceptable. if not acceptable, the clause
932 * should terminate the loop with a 'break' */
935 && (((bp_ - 1) >= start) && isalnum(*(bp_ - 1)))
936 && (((bp_ + 1) < ep_) && isalnum(*(bp_ + 1)))) {
937 /* hyphens are allowed, but only in
939 } else if (!ispunct(*bp_)) {
940 /* but anything not being a punctiation
943 break; /* anything else is rejected */
956 /* scan forward (should start with an alnum) */
957 for (; *bp_ != '<' && isspace(*bp_) && *bp_ != '"'; bp_++)
967 #undef IS_ASCII_ALNUM
968 #undef IS_RFC822_CHAR
970 static gchar *make_email_string(const gchar *bp, const gchar *ep)
972 /* returns a mailto: URI; mailto: is also used to detect the
973 * uri type later on in the button_pressed signal handler */
977 tmp = g_strndup(bp, ep - bp);
978 result = g_strconcat("mailto:", tmp, NULL);
984 static gchar *make_http_string(const gchar *bp, const gchar *ep)
986 /* returns an http: URI; */
990 tmp = g_strndup(bp, ep - bp);
991 result = g_strconcat("http://", tmp, NULL);
997 #define ADD_TXT_POS(bp_, ep_, pti_) \
998 if ((last->next = alloca(sizeof(struct txtpos))) != NULL) { \
1000 last->bp = (bp_); last->ep = (ep_); last->pti = (pti_); \
1001 last->next = NULL; \
1003 g_warning("alloc error scanning URIs\n"); \
1004 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, \
1010 /* textview_make_clickable_parts() - colorizes clickable parts */
1011 static void textview_make_clickable_parts(TextView *textview,
1012 const gchar *fg_tag,
1013 const gchar *uri_tag,
1014 const gchar *linebuf)
1016 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1017 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1020 /* parse table - in order of priority */
1022 const gchar *needle; /* token */
1024 /* token search function */
1025 gchar *(*search) (const gchar *haystack,
1026 const gchar *needle);
1027 /* part parsing function */
1028 gboolean (*parse) (const gchar *start,
1029 const gchar *scanpos,
1032 /* part to URI function */
1033 gchar *(*build_uri) (const gchar *bp,
1037 static struct table parser[] = {
1038 {"http://", strcasestr, get_uri_part, make_uri_string},
1039 {"https://", strcasestr, get_uri_part, make_uri_string},
1040 {"ftp://", strcasestr, get_uri_part, make_uri_string},
1041 {"www.", strcasestr, get_uri_part, make_http_string},
1042 {"mailto:", strcasestr, get_uri_part, make_uri_string},
1043 {"@", strcasestr, get_email_part, make_email_string}
1045 const gint PARSE_ELEMS = sizeof parser / sizeof parser[0];
1048 const gchar *walk, *bp, *ep;
1051 const gchar *bp, *ep; /* text position */
1052 gint pti; /* index in parse table */
1053 struct txtpos *next; /* next */
1054 } head = {NULL, NULL, 0, NULL}, *last = &head;
1056 gtk_text_buffer_get_end_iter(buffer, &iter);
1058 /* parse for clickable parts, and build a list of begin and end positions */
1059 for (walk = linebuf, n = 0;;) {
1060 gint last_index = PARSE_ELEMS;
1061 gchar *scanpos = NULL;
1063 /* FIXME: this looks phony. scanning for anything in the parse table */
1064 for (n = 0; n < PARSE_ELEMS; n++) {
1067 tmp = parser[n].search(walk, parser[n].needle);
1069 if (scanpos == NULL || tmp < scanpos) {
1077 /* check if URI can be parsed */
1078 if (parser[last_index].parse(walk, scanpos, &bp, &ep)
1079 && (size_t) (ep - bp - 1) > strlen(parser[last_index].needle)) {
1080 ADD_TXT_POS(bp, ep, last_index);
1084 strlen(parser[last_index].needle);
1089 /* colorize this line */
1091 const gchar *normal_text = linebuf;
1094 for (last = head.next; last != NULL;
1095 normal_text = last->ep, last = last->next) {
1097 uri = g_new(RemoteURI, 1);
1098 if (last->bp - normal_text > 0)
1099 gtk_text_buffer_insert_with_tags_by_name
1102 last->bp - normal_text,
1104 uri->uri = parser[last->pti].build_uri(last->bp,
1106 uri->start = gtk_text_iter_get_offset(&iter);
1107 gtk_text_buffer_insert_with_tags_by_name
1109 last->bp, last->ep - last->bp,
1111 uri->end = gtk_text_iter_get_offset(&iter);
1112 textview->uri_list =
1113 g_slist_append(textview->uri_list, uri);
1117 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter,
1121 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter,
1129 static void textview_write_line(TextView *textview, const gchar *str,
1130 CodeConverter *conv)
1133 GtkTextBuffer *buffer;
1135 gchar buf[BUFFSIZE];
1137 gint quotelevel = -1;
1138 gchar quote_tag_str[10];
1140 text = GTK_TEXT_VIEW(textview->text);
1141 buffer = gtk_text_view_get_buffer(text);
1142 gtk_text_buffer_get_end_iter(buffer, &iter);
1145 strncpy2(buf, str, sizeof(buf));
1146 } else if (conv_convert(conv, buf, sizeof(buf), str) < 0) {
1147 conv_localetodisp(buf, sizeof(buf), str);
1151 if (prefs_common.conv_mb_alnum) conv_mb_alnum(buf);
1154 /* change color of quotation
1155 >, foo>, _> ... ok, <foo>, foo bar>, foo-> ... ng
1156 Up to 3 levels of quotations are detected, and each
1157 level is colored using a different color. */
1158 if (prefs_common.enable_color
1159 && line_has_quote_char(buf, prefs_common.quote_chars)) {
1160 quotelevel = get_quote_level(buf, prefs_common.quote_chars);
1162 /* set up the correct foreground color */
1163 if (quotelevel > 2) {
1164 /* recycle colors */
1165 if (prefs_common.recycle_quote_colors)
1172 if (quotelevel == -1)
1175 g_snprintf(quote_tag_str, sizeof(quote_tag_str),
1176 "quote%d", quotelevel);
1177 fg_color = quote_tag_str;
1180 if (prefs_common.enable_color && (strcmp(buf,"-- \n") == 0 || textview->is_in_signature)) {
1181 fg_color = "signature";
1182 textview->is_in_signature = TRUE;
1185 if (prefs_common.enable_color)
1186 textview_make_clickable_parts(textview, fg_color, "link", buf);
1188 textview_make_clickable_parts(textview, fg_color, NULL, buf);
1191 void textview_write_link(TextView *textview, const gchar *str,
1192 const gchar *uri, CodeConverter *conv)
1194 GdkColor *link_color = NULL;
1196 GtkTextBuffer *buffer;
1198 gchar buf[BUFFSIZE];
1205 text = GTK_TEXT_VIEW(textview->text);
1206 buffer = gtk_text_view_get_buffer(text);
1207 gtk_text_buffer_get_end_iter(buffer, &iter);
1212 if (textview->text_is_mb)
1213 conv_localetodisp(buf, sizeof(buf), str);
1215 strncpy2(buf, str, sizeof(buf));
1216 } else if (conv_convert(conv, buf, sizeof(buf), str) < 0)
1217 conv_localetodisp(buf, sizeof(buf), str);
1218 else if (textview->text_is_mb)
1219 conv_unreadable_locale(buf);
1222 strncpy2(buf, str, sizeof(buf));
1223 else if (conv_convert(conv, buf, sizeof(buf), str) < 0)
1224 conv_localetodisp(buf, sizeof(buf), str);
1229 gtk_text_buffer_get_end_iter(buffer, &iter);
1231 for (bufp = buf; isspace(*(guchar *)bufp); bufp++)
1232 gtk_text_buffer_insert(buffer, &iter, bufp, 1);
1234 if (prefs_common.enable_color) {
1235 link_color = &uri_color;
1237 r_uri = g_new(RemoteURI, 1);
1238 r_uri->uri = g_strdup(uri);
1239 r_uri->start = gtk_text_iter_get_offset(&iter);
1240 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, bufp, -1,
1242 r_uri->end = gtk_text_iter_get_offset(&iter);
1243 textview->uri_list = g_slist_append(textview->uri_list, r_uri);
1246 void textview_clear(TextView *textview)
1248 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1249 GtkTextBuffer *buffer;
1251 buffer = gtk_text_view_get_buffer(text);
1252 gtk_text_buffer_set_text(buffer, "\0", -1);
1254 TEXTVIEW_STATUSBAR_POP(textview);
1255 textview_uri_list_remove_all(textview->uri_list);
1256 textview->uri_list = NULL;
1258 textview->body_pos = 0;
1261 void textview_destroy(TextView *textview)
1263 textview_uri_list_remove_all(textview->uri_list);
1264 textview->uri_list = NULL;
1269 void textview_set_all_headers(TextView *textview, gboolean all_headers)
1271 textview->show_all_headers = all_headers;
1274 void textview_set_font(TextView *textview, const gchar *codeset)
1276 if (prefs_common.textfont) {
1277 PangoFontDescription *font_desc = NULL;
1279 if (prefs_common.textfont)
1280 font_desc = pango_font_description_from_string
1281 (prefs_common.textfont);
1283 gtk_widget_modify_font(textview->text, font_desc);
1284 pango_font_description_free(font_desc);
1287 gtk_text_view_set_pixels_above_lines(GTK_TEXT_VIEW(textview->text),
1288 prefs_common.line_space / 2);
1289 gtk_text_view_set_pixels_below_lines(GTK_TEXT_VIEW(textview->text),
1290 prefs_common.line_space / 2);
1291 if (prefs_common.head_space) {
1292 gtk_text_view_set_left_margin(GTK_TEXT_VIEW(textview->text), 6);
1294 gtk_text_view_set_left_margin(GTK_TEXT_VIEW(textview->text), 0);
1298 void textview_set_text(TextView *textview, const gchar *text)
1301 GtkTextBuffer *buffer;
1303 g_return_if_fail(textview != NULL);
1304 g_return_if_fail(text != NULL);
1306 textview_clear(textview);
1308 view = GTK_TEXT_VIEW(textview->text);
1309 buffer = gtk_text_view_get_buffer(view);
1310 gtk_text_buffer_set_text(buffer, text, strlen(text));
1326 H_ORGANIZATION = 11,
1329 void textview_set_position(TextView *textview, gint pos)
1331 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1332 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1335 gtk_text_buffer_get_iter_at_offset(buffer, &iter, pos);
1336 gtk_text_buffer_place_cursor(buffer, &iter);
1337 gtk_text_view_scroll_to_iter(text, &iter, 0.0, FALSE, 0.0, 0.0);
1340 static GPtrArray *textview_scan_header(TextView *textview, FILE *fp)
1342 gchar buf[BUFFSIZE];
1343 GPtrArray *headers, *sorted_headers;
1344 GSList *disphdr_list;
1348 g_return_val_if_fail(fp != NULL, NULL);
1350 if (textview->show_all_headers)
1351 return procheader_get_header_array_asis(fp);
1353 if (!prefs_common.display_header) {
1354 while (fgets(buf, sizeof(buf), fp) != NULL)
1355 if (buf[0] == '\r' || buf[0] == '\n') break;
1359 headers = procheader_get_header_array_asis(fp);
1361 sorted_headers = g_ptr_array_new();
1363 for (disphdr_list = prefs_common.disphdr_list; disphdr_list != NULL;
1364 disphdr_list = disphdr_list->next) {
1365 DisplayHeaderProp *dp =
1366 (DisplayHeaderProp *)disphdr_list->data;
1368 for (i = 0; i < headers->len; i++) {
1369 header = g_ptr_array_index(headers, i);
1371 if (procheader_headername_equal(header->name,
1374 procheader_header_free(header);
1376 g_ptr_array_add(sorted_headers, header);
1378 g_ptr_array_remove_index(headers, i);
1384 if (prefs_common.show_other_header) {
1385 for (i = 0; i < headers->len; i++) {
1386 header = g_ptr_array_index(headers, i);
1387 g_ptr_array_add(sorted_headers, header);
1389 g_ptr_array_free(headers, TRUE);
1391 procheader_header_array_destroy(headers);
1394 return sorted_headers;
1397 static void textview_show_header(TextView *textview, GPtrArray *headers)
1399 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1400 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1405 g_return_if_fail(headers != NULL);
1407 for (i = 0; i < headers->len; i++) {
1408 header = g_ptr_array_index(headers, i);
1409 g_return_if_fail(header->name != NULL);
1411 gtk_text_buffer_get_end_iter (buffer, &iter);
1412 gtk_text_buffer_insert_with_tags_by_name
1413 (buffer, &iter, header->name, -1,
1414 "header_title", "header", NULL);
1415 if (header->name[strlen(header->name) - 1] != ' ')
1416 gtk_text_buffer_insert_with_tags_by_name
1417 (buffer, &iter, " ", 1,
1418 "header_title", "header", NULL);
1420 if (procheader_headername_equal(header->name, "Subject") ||
1421 procheader_headername_equal(header->name, "From") ||
1422 procheader_headername_equal(header->name, "To") ||
1423 procheader_headername_equal(header->name, "Cc"))
1424 unfold_line(header->body);
1428 if (textview->text_is_mb == TRUE)
1429 conv_unreadable_locale(header->body);
1432 if (prefs_common.enable_color &&
1433 (procheader_headername_equal(header->name, "X-Mailer") ||
1434 procheader_headername_equal(header->name,
1436 strstr(header->body, "Sylpheed") != NULL) {
1437 gtk_text_buffer_get_end_iter (buffer, &iter);
1438 gtk_text_buffer_insert_with_tags_by_name
1439 (buffer, &iter, header->body, -1,
1440 "header", "emphasis", NULL);
1441 } else if (prefs_common.enable_color) {
1442 textview_make_clickable_parts(textview, "header", "link",
1445 textview_make_clickable_parts(textview, "header", NULL,
1448 gtk_text_buffer_get_end_iter (buffer, &iter);
1449 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, "\n", 1,
1454 gboolean textview_search_string(TextView *textview, const gchar *str,
1457 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1458 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1460 GtkTextIter iter, start, end, real_end, *pos;
1461 gboolean found = FALSE;
1462 gint insert_offset, selbound_offset;
1464 /* reset selection */
1465 mark = gtk_text_buffer_get_mark(buffer, "insert");
1466 gtk_text_buffer_get_iter_at_mark(buffer, &start, mark);
1467 insert_offset = gtk_text_iter_get_offset(&start);
1468 mark = gtk_text_buffer_get_mark(buffer, "selection_bound");
1469 gtk_text_buffer_get_iter_at_mark(buffer, &end, mark);
1470 selbound_offset = gtk_text_iter_get_offset(&end);
1472 pos = insert_offset > selbound_offset ? &start : &end;
1473 gtk_text_buffer_place_cursor(buffer, pos);
1476 mark = gtk_text_buffer_get_insert(buffer);
1477 gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark);
1479 found = gtk_text_iter_forward_search(&iter, str,
1480 GTK_TEXT_SEARCH_VISIBLE_ONLY,
1481 &start, &end, NULL);
1485 gtk_text_buffer_get_end_iter(buffer, &real_end);
1486 text = gtk_text_buffer_get_text(buffer, &iter,
1489 while (!found && i++ < strlen(text) - 1) {
1490 found = (strncasecmp(text+i, str, strlen(str)) == 0);
1493 i += gtk_text_iter_get_offset(&end);
1496 gtk_text_buffer_get_iter_at_offset(buffer, &start, i);
1497 gtk_text_buffer_get_iter_at_offset(buffer, &end,
1505 gtk_text_buffer_place_cursor(buffer, &start);
1506 gtk_text_buffer_move_mark_by_name(buffer, "selection_bound",
1508 mark = gtk_text_buffer_get_mark(buffer, "insert");
1509 gtk_text_view_scroll_mark_onscreen(text, mark);
1515 gboolean textview_search_string_backward(TextView *textview, const gchar *str,
1518 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1519 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text);
1521 GtkTextIter iter, start, real_start, end, *pos;
1522 gboolean found = FALSE;
1523 gint insert_offset, selbound_offset;
1525 /* reset selection */
1526 mark = gtk_text_buffer_get_mark(buffer, "insert");
1527 gtk_text_buffer_get_iter_at_mark(buffer, &start, mark);
1528 insert_offset = gtk_text_iter_get_offset(&start);
1529 mark = gtk_text_buffer_get_mark(buffer, "selection_bound");
1530 gtk_text_buffer_get_iter_at_mark(buffer, &end, mark);
1531 selbound_offset = gtk_text_iter_get_offset(&end);
1533 pos = insert_offset < selbound_offset ? &start : &end;
1534 gtk_text_buffer_place_cursor(buffer, pos);
1537 mark = gtk_text_buffer_get_insert(buffer);
1538 gtk_text_buffer_get_iter_at_mark(buffer, &iter, mark);
1540 found = gtk_text_iter_backward_search(&iter, str,
1541 GTK_TEXT_SEARCH_VISIBLE_ONLY,
1542 &start, &end, NULL);
1546 if (gtk_text_iter_get_offset(&iter) == 0)
1547 gtk_text_buffer_get_end_iter(buffer, &iter);
1549 i = gtk_text_iter_get_offset(&iter) - strlen(str) - 1;
1550 gtk_text_buffer_get_start_iter(buffer, &real_start);
1552 text = gtk_text_buffer_get_text(buffer, &real_start,
1555 while (!found && i-- > 0) {
1556 found = (strncasecmp(text+i, str, strlen(str)) == 0);
1560 gtk_text_buffer_get_iter_at_offset(buffer, &start, i);
1561 gtk_text_buffer_get_iter_at_offset(buffer, &end,
1569 gtk_text_buffer_place_cursor(buffer, &end);
1570 gtk_text_buffer_move_mark_by_name(buffer, "selection_bound",
1572 mark = gtk_text_buffer_get_mark(buffer, "insert");
1573 gtk_text_view_scroll_mark_onscreen(text, mark);
1579 void textview_scroll_one_line(TextView *textview, gboolean up)
1581 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1582 GtkAdjustment *vadj = text->vadjustment;
1585 if (prefs_common.enable_smooth_scroll) {
1586 textview_smooth_scroll_one_line(textview, up);
1591 upper = vadj->upper - vadj->page_size;
1592 if (vadj->value < upper) {
1594 vadj->step_increment * 4;
1596 MIN(vadj->value, upper);
1597 g_signal_emit_by_name(G_OBJECT(vadj),
1598 "value_changed", 0);
1601 if (vadj->value > 0.0) {
1603 vadj->step_increment * 4;
1605 MAX(vadj->value, 0.0);
1606 g_signal_emit_by_name(G_OBJECT(vadj),
1607 "value_changed", 0);
1612 gboolean textview_scroll_page(TextView *textview, gboolean up)
1614 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1615 GtkAdjustment *vadj = text->vadjustment;
1619 if (prefs_common.enable_smooth_scroll)
1620 return textview_smooth_scroll_page(textview, up);
1622 if (prefs_common.scroll_halfpage)
1623 page_incr = vadj->page_increment / 2;
1625 page_incr = vadj->page_increment;
1628 upper = vadj->upper - vadj->page_size;
1629 if (vadj->value < upper) {
1630 vadj->value += page_incr;
1631 vadj->value = MIN(vadj->value, upper);
1632 g_signal_emit_by_name(G_OBJECT(vadj),
1633 "value_changed", 0);
1637 if (vadj->value > 0.0) {
1638 vadj->value -= page_incr;
1639 vadj->value = MAX(vadj->value, 0.0);
1640 g_signal_emit_by_name(G_OBJECT(vadj),
1641 "value_changed", 0);
1649 static void textview_smooth_scroll_do(TextView *textview,
1650 gfloat old_value, gfloat last_value,
1653 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1654 GtkAdjustment *vadj = text->vadjustment;
1659 if (old_value < last_value) {
1660 change_value = last_value - old_value;
1663 change_value = old_value - last_value;
1667 for (i = step; i <= change_value; i += step) {
1668 vadj->value = old_value + (up ? -i : i);
1669 g_signal_emit_by_name(G_OBJECT(vadj),
1670 "value_changed", 0);
1673 vadj->value = last_value;
1674 g_signal_emit_by_name(G_OBJECT(vadj), "value_changed", 0);
1677 static void textview_smooth_scroll_one_line(TextView *textview, gboolean up)
1679 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1680 GtkAdjustment *vadj = text->vadjustment;
1686 upper = vadj->upper - vadj->page_size;
1687 if (vadj->value < upper) {
1688 old_value = vadj->value;
1689 last_value = vadj->value +
1690 vadj->step_increment * 4;
1691 last_value = MIN(last_value, upper);
1693 textview_smooth_scroll_do(textview, old_value,
1695 prefs_common.scroll_step);
1698 if (vadj->value > 0.0) {
1699 old_value = vadj->value;
1700 last_value = vadj->value -
1701 vadj->step_increment * 4;
1702 last_value = MAX(last_value, 0.0);
1704 textview_smooth_scroll_do(textview, old_value,
1706 prefs_common.scroll_step);
1711 static gboolean textview_smooth_scroll_page(TextView *textview, gboolean up)
1713 GtkTextView *text = GTK_TEXT_VIEW(textview->text);
1714 GtkAdjustment *vadj = text->vadjustment;
1720 if (prefs_common.scroll_halfpage)
1721 page_incr = vadj->page_increment / 2;
1723 page_incr = vadj->page_increment;
1726 upper = vadj->upper - vadj->page_size;
1727 if (vadj->value < upper) {
1728 old_value = vadj->value;
1729 last_value = vadj->value + page_incr;
1730 last_value = MIN(last_value, upper);
1732 textview_smooth_scroll_do(textview, old_value,
1734 prefs_common.scroll_step);
1738 if (vadj->value > 0.0) {
1739 old_value = vadj->value;
1740 last_value = vadj->value - page_incr;
1741 last_value = MAX(last_value, 0.0);
1743 textview_smooth_scroll_do(textview, old_value,
1745 prefs_common.scroll_step);
1753 #define KEY_PRESS_EVENT_STOP() \
1754 g_signal_stop_emission_by_name(G_OBJECT(widget), \
1757 static gint textview_key_pressed(GtkWidget *widget, GdkEventKey *event,
1760 SummaryView *summaryview = NULL;
1761 MessageView *messageview = textview->messageview;
1763 if (!event) return FALSE;
1764 if (messageview->mainwin)
1765 summaryview = messageview->mainwin->summaryview;
1767 switch (event->keyval) {
1782 summary_pass_key_press_event(summaryview, event);
1784 textview_scroll_page(textview, FALSE);
1787 textview_scroll_page(textview, TRUE);
1790 textview_scroll_one_line(textview,
1791 (event->state & GDK_MOD1_MASK) != 0);
1795 summary_pass_key_press_event(summaryview, event);
1800 if ((event->state & (GDK_MOD1_MASK|GDK_CONTROL_MASK)) == 0) {
1801 KEY_PRESS_EVENT_STOP();
1802 mimeview_pass_key_press_event(messageview->mimeview,
1806 /* possible fall through */
1809 event->window != messageview->mainwin->window->window) {
1810 GdkEventKey tmpev = *event;
1812 tmpev.window = messageview->mainwin->window->window;
1813 KEY_PRESS_EVENT_STOP();
1814 gtk_widget_event(messageview->mainwin->window,
1815 (GdkEvent *)&tmpev);
1824 *\brief Check to see if a web URL has been disguised as a different
1825 * URL (possible with HTML email).
1827 *\param uri The uri to check
1829 *\param textview The TextView the URL is contained in
1831 *\return gboolean TRUE if the URL is ok, or if the user chose to open
1832 * it anyway, otherwise FALSE
1834 static gboolean uri_security_check(RemoteURI *uri, TextView *textview)
1837 gboolean retval = TRUE;
1839 if (g_ascii_strncasecmp(uri->uri, "http:", 5) &&
1840 g_ascii_strncasecmp(uri->uri, "https:", 6) &&
1841 g_ascii_strncasecmp(uri->uri, "www.", 4))
1844 clicked_str = gtk_editable_get_chars(GTK_EDITABLE(textview->text),
1847 if (clicked_str == NULL)
1850 if (strcmp(clicked_str, uri->uri) &&
1851 (!g_ascii_strncasecmp(clicked_str, "http:", 5) ||
1852 !g_ascii_strncasecmp(clicked_str, "https:", 6) ||
1853 !g_ascii_strncasecmp(clicked_str, "www.", 4))) {
1857 /* allow uri->uri == http://somewhere.com
1858 and clicked_str == somewhere.com */
1859 str = g_strconcat("http://", clicked_str, NULL);
1861 if (!g_ascii_strcasecmp(str, uri->uri))
1866 if (retval == FALSE) {
1870 msg = g_strdup_printf(_("The real URL (%s) is different from\n"
1871 "the apparent URL (%s). \n"
1873 uri->uri, clicked_str);
1874 resp = alertpanel_with_type(_("Warning"),
1878 NULL, NULL, ALERT_WARNING);
1880 if (resp == G_ALERTDEFAULT)
1883 g_free(clicked_str);
1887 static gboolean textview_motion_notify(GtkWidget *widget,
1888 GdkEventMotion *event,
1891 textview_uri_update(textview, event->x, event->y);
1892 gdk_window_get_pointer(widget->window, NULL, NULL, NULL);
1897 static gboolean textview_leave_notify(GtkWidget *widget,
1898 GdkEventCrossing *event,
1901 textview_uri_update(textview, -1, -1);
1906 static gboolean textview_visibility_notify(GtkWidget *widget,
1907 GdkEventVisibility *event,
1912 gdk_window_get_pointer(widget->window, &wx, &wy, NULL);
1913 textview_uri_update(textview, wx, wy);
1918 static void textview_uri_update(TextView *textview, gint x, gint y)
1920 GtkTextBuffer *buffer;
1921 GtkTextIter start_iter, end_iter;
1922 RemoteURI *uri = NULL;
1924 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text));
1926 if (x != -1 && y != -1) {
1932 gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(textview->text),
1933 GTK_TEXT_WINDOW_WIDGET,
1935 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(textview->text),
1938 tags = gtk_text_iter_get_tags(&iter);
1939 for (cur = tags; cur != NULL; cur = cur->next) {
1940 GtkTextTag *tag = cur->data;
1943 g_object_get(G_OBJECT(tag), "name", &name, NULL);
1944 if (!strcmp(name, "link")
1945 && textview_get_uri_range(textview, &iter, tag,
1946 &start_iter, &end_iter))
1947 uri = textview_get_uri_from_range(textview,
1959 if (uri != textview->uri_hover) {
1962 if (textview->uri_hover)
1963 gtk_text_buffer_remove_tag_by_name(buffer,
1965 &textview->uri_hover_start_iter,
1966 &textview->uri_hover_end_iter);
1968 textview->uri_hover = uri;
1970 textview->uri_hover_start_iter = start_iter;
1971 textview->uri_hover_end_iter = end_iter;
1974 window = gtk_text_view_get_window(GTK_TEXT_VIEW(textview->text),
1975 GTK_TEXT_WINDOW_TEXT);
1976 gdk_window_set_cursor(window, uri ? hand_cursor : text_cursor);
1978 TEXTVIEW_STATUSBAR_POP(textview);
1983 gtk_text_buffer_apply_tag_by_name(buffer,
1988 trimmed_uri = trim_string(uri->uri, 60);
1989 TEXTVIEW_STATUSBAR_PUSH(textview, trimmed_uri);
1990 g_free(trimmed_uri);
1995 static gboolean textview_get_uri_range(TextView *textview,
1998 GtkTextIter *start_iter,
1999 GtkTextIter *end_iter)
2001 GtkTextIter _start_iter, _end_iter;
2004 if(!gtk_text_iter_forward_to_tag_toggle(&_end_iter, tag)) {
2005 debug_print("Can't find end");
2009 _start_iter = _end_iter;
2010 if(!gtk_text_iter_backward_to_tag_toggle(&_start_iter, tag)) {
2011 debug_print("Can't find start.");
2015 *start_iter = _start_iter;
2016 *end_iter = _end_iter;
2021 static RemoteURI *textview_get_uri_from_range(TextView *textview,
2024 GtkTextIter *start_iter,
2025 GtkTextIter *end_iter)
2027 gint start_pos, end_pos, cur_pos;
2028 RemoteURI *uri = NULL;
2031 start_pos = gtk_text_iter_get_offset(start_iter);
2032 end_pos = gtk_text_iter_get_offset(end_iter);
2033 cur_pos = gtk_text_iter_get_offset(iter);
2035 for (cur = textview->uri_list; cur != NULL; cur = cur->next) {
2036 RemoteURI *uri_ = (RemoteURI *)cur->data;
2037 if (start_pos == uri_->start &&
2038 end_pos == uri_->end) {
2041 } else if (start_pos == uri_->start ||
2042 end_pos == uri_->end) {
2043 /* in case of contiguous links, textview_get_uri_range
2044 * returns a broader range (start of 1st link to end
2046 * In that case, correct link is the one covering
2049 if (uri_->start <= cur_pos && cur_pos <= uri_->end) {
2059 static RemoteURI *textview_get_uri(TextView *textview,
2063 GtkTextIter start_iter, end_iter;
2064 RemoteURI *uri = NULL;
2066 if (textview_get_uri_range(textview, iter, tag, &start_iter,
2068 uri = textview_get_uri_from_range(textview, iter, tag,
2069 &start_iter, &end_iter);
2074 static gboolean textview_uri_button_pressed(GtkTextTag *tag, GObject *obj,
2075 GdkEvent *event, GtkTextIter *iter,
2078 GdkEventButton *bevent;
2079 RemoteURI *uri = NULL;
2086 if (event->type != GDK_BUTTON_PRESS && event->type != GDK_2BUTTON_PRESS
2087 && event->type != GDK_MOTION_NOTIFY)
2090 uri = textview_get_uri(textview, iter, tag);
2094 bevent = (GdkEventButton *) event;
2096 /* doubleclick: open compose / add address / browser */
2097 if ((event->type == GDK_BUTTON_PRESS && bevent->button == 1) ||
2098 bevent->button == 2 || bevent->button == 3) {
2099 if (!g_ascii_strncasecmp(uri->uri, "mailto:", 7)) {
2100 if (bevent->button == 3) {
2101 gchar *fromname, *fromaddress;
2104 fromaddress = g_strdup(uri->uri + 7);
2105 /* Hiroyuki: please put this function in utils.c! */
2106 fromname = procheader_get_fromname(fromaddress);
2107 extract_address(fromaddress);
2108 g_message("adding from textview %s <%s>", fromname, fromaddress);
2109 /* Add to address book - Match */
2110 addressbook_add_contact( fromname, fromaddress, NULL );
2112 g_free(fromaddress);
2115 PrefsAccount *account = NULL;
2117 if (textview->messageview && textview->messageview->msginfo &&
2118 textview->messageview->msginfo->folder) {
2119 FolderItem *folder_item;
2121 folder_item = textview->messageview->msginfo->folder;
2122 if (folder_item->prefs && folder_item->prefs->enable_default_account)
2123 account = account_find_from_id(folder_item->prefs->default_account);
2125 compose_new(account, uri->uri + 7, NULL);
2129 if (textview_uri_security_check(textview, uri) == TRUE)
2131 prefs_common.uri_cmd);
2140 *\brief Check to see if a web URL has been disguised as a different
2141 * URL (possible with HTML email).
2143 *\param uri The uri to check
2145 *\param textview The TextView the URL is contained in
2147 *\return gboolean TRUE if the URL is ok, or if the user chose to open
2148 * it anyway, otherwise FALSE
2150 static gboolean textview_uri_security_check(TextView *textview, RemoteURI *uri)
2153 gboolean retval = TRUE;
2155 if (is_uri_string(uri->uri) == FALSE)
2158 visible_str = gtk_editable_get_chars(GTK_EDITABLE(textview->text),
2159 uri->start, uri->end);
2160 if (visible_str == NULL)
2163 if (strcmp(visible_str, uri->uri) != 0 && is_uri_string(visible_str)) {
2165 gchar *visible_uri_path;
2167 uri_path = get_uri_path(uri->uri);
2168 visible_uri_path = get_uri_path(visible_str);
2169 if (strcmp(uri_path, visible_uri_path) != 0)
2173 if (retval == FALSE) {
2177 msg = g_strdup_printf(_("The real URL (%s) is different from\n"
2178 "the apparent URL (%s).\n"
2180 uri->uri, visible_str);
2181 aval = alertpanel_with_type(_("Warning"), msg, _("Yes"), _("No"), NULL,
2182 NULL, ALERT_WARNING);
2184 if (aval == G_ALERTDEFAULT)
2188 g_free(visible_str);
2193 static void textview_uri_list_remove_all(GSList *uri_list)
2197 for (cur = uri_list; cur != NULL; cur = cur->next) {
2199 g_free(((RemoteURI *)cur->data)->uri);
2204 g_slist_free(uri_list);