b0d7d6f3f36e4b50263955e5555bb555642a0e86
[claws.git] / src / textview.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2003 Hiroyuki Yamamoto
4  *
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.
9  *
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.
14  *
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.
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #  include "config.h"
22 #endif
23
24 #include "defs.h"
25
26 #include <glib.h>
27 #include <gdk/gdk.h>
28 #include <gdk/gdkkeysyms.h>
29 #include <gtk/gtkvbox.h>
30 #include <gtk/gtkscrolledwindow.h>
31 #include <gtk/gtksignal.h>
32 #include <stdio.h>
33 #include <ctype.h>
34 #include <string.h>
35 #include <stdlib.h>
36
37 #include "intl.h"
38 #include "main.h"
39 #include "summaryview.h"
40 #include "procheader.h"
41 #include "prefs_common.h"
42 #include "codeconv.h"
43 #include "gtkstext.h"
44 #include "utils.h"
45 #include "gtkutils.h"
46 #include "procmime.h"
47 #include "html.h"
48 #include "enriched.h"
49 #include "compose.h"
50 #include "addressbook.h"
51 #include "displayheader.h"
52 #include "account.h"
53 #include "mimeview.h"
54
55 typedef struct _RemoteURI       RemoteURI;
56
57 struct _RemoteURI
58 {
59         gchar *uri;
60
61         guint start;
62         guint end;
63 };
64
65 static GdkColor quote_colors[3] = {
66         {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
67         {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
68         {(gulong)0, (gushort)0, (gushort)0, (gushort)0}
69 };
70
71 static GdkColor signature_color = {
72         (gulong)0,
73         (gushort)0x7fff,
74         (gushort)0x7fff,
75         (gushort)0x7fff
76 };
77         
78 static GdkColor uri_color = {
79         (gulong)0,
80         (gushort)0,
81         (gushort)0,
82         (gushort)0
83 };
84
85 static GdkColor emphasis_color = {
86         (gulong)0,
87         (gushort)0,
88         (gushort)0,
89         (gushort)0xcfff
90 };
91
92 #if 0
93 static GdkColor error_color = {
94         (gulong)0,
95         (gushort)0xefff,
96         (gushort)0,
97         (gushort)0
98 };
99 #endif
100
101 static GdkColor good_sig_color = {
102         (gulong)0,
103         (gushort)0,
104         (gushort)0xbfff,
105         (gushort)0
106 };
107
108 static GdkColor nocheck_sig_color = {
109         (gulong)0,
110         (gushort)0,
111         (gushort)0,
112         (gushort)0xcfff
113 };
114
115 static GdkColor bad_sig_color = {
116         (gulong)0,
117         (gushort)0xefff,
118         (gushort)0,
119         (gushort)0
120 };
121
122 static GdkFont *text_sb_font;
123 static GdkFont *text_mb_font;
124 static gint text_sb_font_orig_ascent;
125 static gint text_sb_font_orig_descent;
126 static gint text_mb_font_orig_ascent;
127 static gint text_mb_font_orig_descent;
128 static GdkFont *spacingfont;
129
130 static void textview_show_ertf          (TextView       *textview,
131                                          FILE           *fp,
132                                          CodeConverter  *conv);
133 static void textview_add_part           (TextView       *textview,
134                                          MimeInfo       *mimeinfo);
135 static void textview_add_parts          (TextView       *textview,
136                                          MimeInfo       *mimeinfo);
137 static void textview_write_body         (TextView       *textview,
138                                          MimeInfo       *mimeinfo,
139                                          const gchar    *charset);
140 static void textview_show_html          (TextView       *textview,
141                                          FILE           *fp,
142                                          CodeConverter  *conv);
143 static void textview_write_line         (TextView       *textview,
144                                          const gchar    *str,
145                                          CodeConverter  *conv);
146 static void textview_write_link         (TextView       *textview,
147                                          const gchar    *str,
148                                          const gchar    *uri,
149                                          CodeConverter  *conv);
150 static GPtrArray *textview_scan_header  (TextView       *textview,
151                                          FILE           *fp);
152 static void textview_show_header        (TextView       *textview,
153                                          GPtrArray      *headers);
154
155 static gint textview_key_pressed        (GtkWidget      *widget,
156                                          GdkEventKey    *event,
157                                          TextView       *textview);
158 static gint textview_button_pressed     (GtkWidget      *widget,
159                                          GdkEventButton *event,
160                                          TextView       *textview);
161 static gint textview_button_released    (GtkWidget      *widget,
162                                          GdkEventButton *event,
163                                          TextView       *textview);
164
165 static void textview_uri_list_remove_all(GSList         *uri_list);
166
167 static void textview_smooth_scroll_do           (TextView       *textview,
168                                                  gfloat          old_value,
169                                                  gfloat          last_value,
170                                                  gint            step);
171 static void textview_smooth_scroll_one_line     (TextView       *textview,
172                                                  gboolean        up);
173 static gboolean textview_smooth_scroll_page     (TextView       *textview,
174                                                  gboolean        up);
175
176
177 TextView *textview_create(void)
178 {
179         TextView *textview;
180         GtkWidget *vbox;
181         GtkWidget *scrolledwin_sb;
182         GtkWidget *scrolledwin_mb;
183         GtkWidget *text_sb;
184         GtkWidget *text_mb;
185
186         debug_print("Creating text view...\n");
187         textview = g_new0(TextView, 1);
188
189         scrolledwin_sb = gtk_scrolled_window_new(NULL, NULL);
190         scrolledwin_mb = gtk_scrolled_window_new(NULL, NULL);
191         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin_sb),
192                                        GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
193         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin_mb),
194                                        GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
195         gtk_widget_set_usize(scrolledwin_sb, prefs_common.mainview_width, -1);
196         gtk_widget_set_usize(scrolledwin_mb, prefs_common.mainview_width, -1);
197
198         /* create GtkSText widgets for single-byte and multi-byte character */
199         text_sb = gtk_stext_new(NULL, NULL);
200         text_mb = gtk_stext_new(NULL, NULL);
201         GTK_STEXT(text_sb)->default_tab_width = 8;
202         GTK_STEXT(text_mb)->default_tab_width = 8;
203         gtk_widget_show(text_sb);
204         gtk_widget_show(text_mb);
205         gtk_stext_set_word_wrap(GTK_STEXT(text_sb), TRUE);
206         gtk_stext_set_word_wrap(GTK_STEXT(text_mb), TRUE);
207         gtk_widget_ensure_style(text_sb);
208         gtk_widget_ensure_style(text_mb);
209         if (text_sb->style && text_sb->style->font->type == GDK_FONT_FONTSET) {
210                 GtkStyle *style;
211                 GdkFont *font;
212
213                 font = gtkut_font_load_from_fontset(prefs_common.normalfont);
214                 if (font) {
215                         style = gtk_style_copy(text_sb->style);
216                         gdk_font_unref(style->font);
217                         style->font = font;
218                         gtk_widget_set_style(text_sb, style);
219                 }
220         }
221         if (text_mb->style && text_mb->style->font->type == GDK_FONT_FONT) {
222                 GtkStyle *style;
223                 GdkFont *font;
224
225                 font = gdk_fontset_load(prefs_common.normalfont);
226                 if (font) {
227                         style = gtk_style_copy(text_mb->style);
228                         gdk_font_unref(style->font);
229                         style->font = font;
230                         gtk_widget_set_style(text_mb, style);
231                 }
232         }
233         gtk_widget_ref(scrolledwin_sb);
234         gtk_widget_ref(scrolledwin_mb);
235
236         gtk_container_add(GTK_CONTAINER(scrolledwin_sb), text_sb);
237         gtk_container_add(GTK_CONTAINER(scrolledwin_mb), text_mb);
238         gtk_signal_connect(GTK_OBJECT(text_sb), "key_press_event",
239                            GTK_SIGNAL_FUNC(textview_key_pressed),
240                            textview);
241         gtk_signal_connect_after(GTK_OBJECT(text_sb), "button_press_event",
242                                  GTK_SIGNAL_FUNC(textview_button_pressed),
243                                  textview);
244         gtk_signal_connect_after(GTK_OBJECT(text_sb), "button_release_event",
245                                  GTK_SIGNAL_FUNC(textview_button_released),
246                                  textview);
247         gtk_signal_connect(GTK_OBJECT(text_mb), "key_press_event",
248                            GTK_SIGNAL_FUNC(textview_key_pressed),
249                            textview);
250         gtk_signal_connect_after(GTK_OBJECT(text_mb), "button_press_event",
251                                  GTK_SIGNAL_FUNC(textview_button_pressed),
252                                  textview);
253         gtk_signal_connect_after(GTK_OBJECT(text_mb), "button_release_event",
254                                  GTK_SIGNAL_FUNC(textview_button_released),
255                                  textview);
256
257         gtk_widget_show(scrolledwin_sb);
258         gtk_widget_show(scrolledwin_mb);
259
260         vbox = gtk_vbox_new(FALSE, 0);
261         gtk_box_pack_start(GTK_BOX(vbox), scrolledwin_sb, TRUE, TRUE, 0);
262
263         gtk_widget_show(vbox);
264
265         textview->vbox             = vbox;
266         textview->scrolledwin      = scrolledwin_sb;
267         textview->scrolledwin_sb   = scrolledwin_sb;
268         textview->scrolledwin_mb   = scrolledwin_mb;
269         textview->text             = text_sb;
270         textview->text_sb          = text_sb;
271         textview->text_mb          = text_mb;
272         textview->text_is_mb       = FALSE;
273         textview->uri_list         = NULL;
274         textview->body_pos         = 0;
275         textview->cur_pos          = 0;
276         textview->show_all_headers = FALSE;
277         textview->last_buttonpress = GDK_NOTHING;
278         textview->show_url_msgid   = 0;
279
280         return textview;
281 }
282
283 void textview_init(TextView *textview)
284 {
285         gtkut_widget_disable_theme_engine(textview->text_sb);
286         gtkut_widget_disable_theme_engine(textview->text_mb);
287         textview_update_message_colors();
288         textview_set_all_headers(textview, FALSE);
289         textview_set_font(textview, NULL);
290 }
291
292 void textview_update_message_colors(void)
293 {
294         GdkColor black = {0, 0, 0, 0};
295
296         if (prefs_common.enable_color) {
297                 /* grab the quote colors, converting from an int to a GdkColor */
298                 gtkut_convert_int_to_gdk_color(prefs_common.quote_level1_col,
299                                                &quote_colors[0]);
300                 gtkut_convert_int_to_gdk_color(prefs_common.quote_level2_col,
301                                                &quote_colors[1]);
302                 gtkut_convert_int_to_gdk_color(prefs_common.quote_level3_col,
303                                                &quote_colors[2]);
304                 gtkut_convert_int_to_gdk_color(prefs_common.uri_col,
305                                                &uri_color);
306                 gtkut_convert_int_to_gdk_color(prefs_common.signature_col,
307                                                &signature_color);
308         } else {
309                 quote_colors[0] = quote_colors[1] = quote_colors[2] = 
310                         uri_color = emphasis_color = signature_color = black;
311         }
312 }
313
314 void textview_show_message(TextView *textview, MimeInfo *mimeinfo,
315                            const gchar *file)
316 {
317         GtkSText *text;
318         FILE *fp;
319         const gchar *charset = NULL;
320
321         if ((fp = fopen(file, "rb")) == NULL) {
322                 FILE_OP_ERROR(file, "fopen");
323                 return;
324         }
325
326         if (textview->messageview->forced_charset)
327                 charset = textview->messageview->forced_charset;
328         else if (prefs_common.force_charset)
329                 charset = prefs_common.force_charset;
330         else if (mimeinfo->charset)
331                 charset = mimeinfo->charset;
332
333         textview_set_font(textview, charset);
334         textview_clear(textview);
335
336         text = GTK_STEXT(textview->text);
337
338         gtk_stext_freeze(text);
339
340 /*
341         if (fseek(fp, mimeinfo->offset, SEEK_SET) < 0) perror("fseek");
342         headers = textview_scan_header(textview, fp);
343         if (headers) {
344                 textview_show_header(textview, headers);
345                 procheader_header_array_destroy(headers);
346                 textview->body_pos = gtk_stext_get_length(text);
347         }
348 */
349         textview_add_parts(textview, mimeinfo);
350
351         gtk_stext_thaw(text);
352
353         fclose(fp);
354 }
355
356 void textview_show_part(TextView *textview, MimeInfo *mimeinfo, FILE *fp)
357 {
358         GtkSText *text;
359         const gchar *charset = NULL;
360
361         g_return_if_fail(mimeinfo != NULL);
362         g_return_if_fail(fp != NULL);
363
364         if ((mimeinfo->type == MIMETYPE_MULTIPART) ||
365             ((mimeinfo->type == MIMETYPE_MESSAGE) && !g_strcasecmp(mimeinfo->subtype, "rfc822"))) {
366                 textview_clear(textview);
367                 textview_add_parts(textview, mimeinfo);
368                 return;
369         }
370
371         if (fseek(fp, mimeinfo->offset, SEEK_SET) < 0)
372                 perror("fseek");
373 /*
374         headers = textview_scan_header(textview, fp);
375 */
376         if (textview->messageview->forced_charset)
377                 charset = textview->messageview->forced_charset;
378         else if (prefs_common.force_charset)
379                 charset = prefs_common.force_charset;
380         else if (mimeinfo->charset)
381                 charset = mimeinfo->charset;
382
383         textview_set_font(textview, charset);
384
385         text = GTK_STEXT(textview->text);
386
387         gtk_stext_freeze(text);
388         textview_clear(textview);
389
390 /*
391         if (headers) {
392                 textview_show_header(textview, headers);
393                 procheader_header_array_destroy(headers);
394                 textview->body_pos = gtk_stext_get_length(text);
395                 if (!mimeinfo->main)
396                         gtk_stext_insert(text, NULL, NULL, NULL, "\n", 1);
397         }
398 */
399         if (mimeinfo->type == MIMETYPE_MULTIPART)
400                 textview_add_parts(textview, mimeinfo);
401         else
402                 textview_write_body(textview, mimeinfo, charset);
403
404         gtk_stext_thaw(text);
405 }
406
407 static void textview_add_part(TextView *textview, MimeInfo *mimeinfo)
408 {
409         GtkSText *text = GTK_STEXT(textview->text);
410         gchar buf[BUFFSIZE];
411         const gchar *charset = NULL;
412         GPtrArray *headers = NULL;
413
414         g_return_if_fail(mimeinfo != NULL);
415
416         if (mimeinfo->type == MIMETYPE_MULTIPART) return;
417
418         if ((mimeinfo->type == MIMETYPE_MESSAGE) && !g_strcasecmp(mimeinfo->subtype, "rfc822")) {
419                 FILE *fp;
420
421                 fp = fopen(mimeinfo->filename, "rb");
422                 fseek(fp, mimeinfo->offset, SEEK_SET);
423                 headers = textview_scan_header(textview, fp);
424                 if (headers) {
425                         gtk_stext_freeze(text);
426                         if (gtk_stext_get_length(text) > 0)
427                                 gtk_stext_insert(text, NULL, NULL, NULL, "\n", 1);
428                         textview_show_header(textview, headers);
429                         procheader_header_array_destroy(headers);
430                         gtk_stext_thaw(text);
431                 }
432                 fclose(fp);
433                 return;
434         }
435
436         gtk_stext_freeze(text);
437
438         if (g_hash_table_lookup(mimeinfo->parameters, "name") != NULL)
439                 g_snprintf(buf, sizeof(buf), "\n[%s  %s/%s (%d bytes)]\n",
440                            (gchar *) g_hash_table_lookup(mimeinfo->parameters, "name"),
441                            procmime_get_type_str(mimeinfo->type),
442                            mimeinfo->subtype, mimeinfo->length);
443         else
444                 g_snprintf(buf, sizeof(buf), "\n[%s/%s (%d bytes)]\n",
445                            procmime_get_type_str(mimeinfo->type),
446                            mimeinfo->subtype, mimeinfo->length);
447
448         if (mimeinfo->type != MIMETYPE_TEXT) {
449                 gtk_stext_insert(text, NULL, NULL, NULL, buf, -1);
450         } else {
451                 if (prefs_common.display_header && (gtk_stext_get_length(text) > 0))
452                         gtk_stext_insert(text, NULL, NULL, NULL, "\n", 1);
453                 if (textview->messageview->forced_charset)
454                         charset = textview->messageview->forced_charset;
455                 else if (prefs_common.force_charset)
456                         charset = prefs_common.force_charset;
457                 else if (mimeinfo->charset)
458                         charset = mimeinfo->charset;
459                 textview_write_body(textview, mimeinfo, charset);
460         }
461
462         gtk_stext_thaw(text);
463 }
464
465 static gboolean add_parts_func(GNode *node, gpointer data)
466 {
467         MimeInfo *mimeinfo = (MimeInfo *) node->data;
468         TextView *textview = (TextView *) data;
469
470         g_return_val_if_fail(mimeinfo != NULL, FALSE);
471
472         textview_add_part(textview, mimeinfo);
473
474         return FALSE;
475 }
476
477 static void textview_add_parts(TextView *textview, MimeInfo *mimeinfo)
478 {
479         g_return_if_fail(mimeinfo != NULL);
480
481         g_node_traverse(mimeinfo->node, G_PRE_ORDER, G_TRAVERSE_ALL, -1, add_parts_func, textview);
482 }
483
484 #define TEXT_INSERT(str) \
485         gtk_stext_insert(text, textview->msgfont, NULL, NULL, str, -1)
486
487 void textview_show_mime_part(TextView *textview, MimeInfo *partinfo)
488 {
489         GtkSText *text;
490
491         if (!partinfo) return;
492
493         textview_set_font(textview, NULL);
494         text = GTK_STEXT(textview->text);
495         textview_clear(textview);
496
497         gtk_stext_freeze(text);
498
499         TEXT_INSERT(_("To save this part, pop up the context menu with "));
500         TEXT_INSERT(_("right click and select `Save as...', "));
501         TEXT_INSERT(_("or press `y' key.\n\n"));
502
503         TEXT_INSERT(_("To display this part as a text message, select "));
504         TEXT_INSERT(_("`Display as text', or press `t' key.\n\n"));
505
506         TEXT_INSERT(_("To open this part with external program, select "));
507         TEXT_INSERT(_("`Open' or `Open with...', "));
508         TEXT_INSERT(_("or double-click, or click the center button, "));
509         TEXT_INSERT(_("or press `l' key."));
510
511         gtk_stext_thaw(text);
512 }
513
514 #undef TEXT_INSERT
515
516 static void textview_write_body(TextView *textview, MimeInfo *mimeinfo,
517                                 const gchar *charset)
518 {
519         FILE *tmpfp;
520         gchar buf[BUFFSIZE];
521         CodeConverter *conv;
522
523         conv = conv_code_converter_new(charset);
524
525         textview->is_in_signature = FALSE;
526
527         if(mimeinfo->encoding_type != ENC_BINARY && 
528            mimeinfo->encoding_type != ENC_7BIT && 
529            mimeinfo->encoding_type != ENC_8BIT)
530                 procmime_decode_content(mimeinfo);
531
532         if (!g_strcasecmp(mimeinfo->subtype, "html")) {
533                 gchar *filename;
534                 
535                 filename = procmime_get_tmp_file_name(mimeinfo);
536                 if (procmime_get_part(filename, mimeinfo) == 0) {
537                         tmpfp = fopen(filename, "rb");
538                         textview_show_html(textview, tmpfp, conv);
539                         fclose(tmpfp);
540                         unlink(filename);
541                 }
542                 g_free(filename);
543         } else if (!g_strcasecmp(mimeinfo->subtype, "enriched")) {
544                 gchar *filename;
545                 
546                 filename = procmime_get_tmp_file_name(mimeinfo);
547                 if (procmime_get_part(filename, mimeinfo) == 0) {
548                         tmpfp = fopen(filename, "rb");
549                         textview_show_ertf(textview, tmpfp, conv);
550                         fclose(tmpfp);
551                         unlink(filename);
552                 }
553                 g_free(filename);
554         } else {
555                 tmpfp = fopen(mimeinfo->filename, "rb");
556                 fseek(tmpfp, mimeinfo->offset, SEEK_SET);
557                 debug_print("Viewing text content of type: %s (length: %d)\n", mimeinfo->subtype, mimeinfo->length);
558                 while ((fgets(buf, sizeof(buf), tmpfp) != NULL) && 
559                        (ftell(tmpfp) <= mimeinfo->offset + mimeinfo->length))
560                         textview_write_line(textview, buf, conv);
561                 fclose(tmpfp);
562         }
563
564         conv_code_converter_destroy(conv);
565 }
566
567 static void textview_show_html(TextView *textview, FILE *fp,
568                                CodeConverter *conv)
569 {
570         HTMLParser *parser;
571         gchar *str;
572
573         parser = html_parser_new(fp, conv);
574         g_return_if_fail(parser != NULL);
575
576         while ((str = html_parse(parser)) != NULL) {
577                 if (parser->state == HTML_HREF) {
578                         /* first time : get and copy the URL */
579                         if (parser->href == NULL) {
580                                 /* ALF - the sylpheed html parser returns an empty string,
581                                  * if still inside an <a>, but already parsed past HREF */
582                                 str = strtok(str, " ");
583                                 if (str) { 
584                                         parser->href = strdup(str);
585                                         /* the URL may (or not) be followed by the
586                                          * referenced text */
587                                         str = strtok(NULL, "");
588                                 }       
589                         }
590                         if (str != NULL)
591                                 textview_write_link(textview, str, parser->href, NULL);
592                 } else
593                         textview_write_line(textview, str, NULL);
594         }
595         html_parser_destroy(parser);
596 }
597
598 static void textview_show_ertf(TextView *textview, FILE *fp,
599                                CodeConverter *conv)
600 {
601         ERTFParser *parser;
602         gchar *str;
603
604         parser = ertf_parser_new(fp, conv);
605         g_return_if_fail(parser != NULL);
606
607         while ((str = ertf_parse(parser)) != NULL) {
608                 textview_write_line(textview, str, NULL);
609         }
610         
611         ertf_parser_destroy(parser);
612 }
613
614 /* get_uri_part() - retrieves a URI starting from scanpos.
615                     Returns TRUE if succesful */
616 static gboolean get_uri_part(const gchar *start, const gchar *scanpos,
617                              const gchar **bp, const gchar **ep)
618 {
619         const gchar *ep_;
620
621         g_return_val_if_fail(start != NULL, FALSE);
622         g_return_val_if_fail(scanpos != NULL, FALSE);
623         g_return_val_if_fail(bp != NULL, FALSE);
624         g_return_val_if_fail(ep != NULL, FALSE);
625
626         *bp = scanpos;
627
628         /* find end point of URI */
629         for (ep_ = scanpos; *ep_ != '\0'; ep_++) {
630                 if (!isgraph(*ep_) || !isascii(*ep_) || strchr("()<>\"", *ep_))
631                         break;
632         }
633
634         /* no punctuation at end of string */
635
636         /* FIXME: this stripping of trailing punctuations may bite with other URIs.
637          * should pass some URI type to this function and decide on that whether
638          * to perform punctuation stripping */
639
640 #define IS_REAL_PUNCT(ch)       (ispunct(ch) && ((ch) != '/')) 
641
642         for (; ep_ - 1 > scanpos + 1 && IS_REAL_PUNCT(*(ep_ - 1)); ep_--)
643                 ;
644
645 #undef IS_REAL_PUNCT
646
647         *ep = ep_;
648
649         return TRUE;            
650 }
651
652 static gchar *make_uri_string(const gchar *bp, const gchar *ep)
653 {
654         return g_strndup(bp, ep - bp);
655 }
656
657 /* valid mail address characters */
658 #define IS_RFC822_CHAR(ch) \
659         (isascii(ch) && \
660          (ch) > 32   && \
661          (ch) != 127 && \
662          !isspace(ch) && \
663          !strchr("(),;<>\"", (ch)))
664
665 /* alphabet and number within 7bit ASCII */
666 #define IS_ASCII_ALNUM(ch)      (isascii(ch) && isalnum(ch))
667 #define IS_QUOTE(ch) ((ch) == '\'' || (ch) == '"')
668
669 static GHashTable *create_domain_tab(void)
670 {
671         static const gchar *toplvl_domains [] = {
672             "museum", "aero",
673             "arpa", "coop", "info", "name", "biz", "com", "edu", "gov",
674             "int", "mil", "net", "org", "ac", "ad", "ae", "af", "ag",
675             "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au",
676             "aw", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh", "bi",
677             "bj", "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by",
678             "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl",
679             "cm", "cn", "co", "cr", "cu", "cv", "cx", "cy", "cz", "de",
680             "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "er",
681             "es", "et", "fi", "fj", "fk", "fm", "fo", "fr", "ga", "gd",
682             "ge", "gf", "gg", "gh", "gi", "gl", "gm", "gn", "gp", "gq",
683             "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr",
684             "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir",
685             "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki",
686             "km", "kn", "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc",
687             "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc",
688             "md", "mg", "mh", "mk", "ml", "mm", "mn", "mo", "mp", "mq",
689             "mr", "ms", "mt", "mu", "mv", "mw", "mx", "my", "mz", "na",
690             "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nu",
691             "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm",
692             "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "ru",
693             "rw", "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj",
694             "sk", "sl", "sm", "sn", "so", "sr", "st", "sv", "sy", "sz",
695             "tc", "td", "tf", "tg", "th", "tj", "tk", "tm", "tn", "to",
696             "tp", "tr", "tt", "tv", "tw", "tz", "ua", "ug", "uk", "um",
697             "us", "uy", "uz", "va", "vc", "ve", "vg", "vi", "vn", "vu",
698             "wf", "ws", "ye", "yt", "yu", "za", "zm", "zw" 
699         };
700         gint n;
701         GHashTable *htab = g_hash_table_new(g_stricase_hash, g_stricase_equal);
702         
703         g_return_val_if_fail(htab, NULL);
704         for (n = 0; n < sizeof toplvl_domains / sizeof toplvl_domains[0]; n++) 
705                 g_hash_table_insert(htab, (gpointer) toplvl_domains[n], (gpointer) toplvl_domains[n]);
706         return htab;
707 }
708
709 static gboolean is_toplvl_domain(GHashTable *tab, const gchar *first, const gchar *last)
710 {
711         const gint MAX_LVL_DOM_NAME_LEN = 6;
712         gchar buf[MAX_LVL_DOM_NAME_LEN + 1];
713         const gchar *m = buf + MAX_LVL_DOM_NAME_LEN + 1;
714         register gchar *p;
715         
716         if (last - first > MAX_LVL_DOM_NAME_LEN || first > last)
717                 return FALSE;
718
719         for (p = buf; p < m &&  first < last; *p++ = *first++)
720                 ;
721         *p = 0;
722
723         return g_hash_table_lookup(tab, buf) != NULL;
724 }
725
726 /* get_email_part() - retrieves an email address. Returns TRUE if succesful */
727 static gboolean get_email_part(const gchar *start, const gchar *scanpos,
728                                const gchar **bp, const gchar **ep)
729 {
730         /* more complex than the uri part because we need to scan back and forward starting from
731          * the scan position. */
732         gboolean result = FALSE;
733         const gchar *bp_ = NULL;
734         const gchar *ep_ = NULL;
735         static GHashTable *dom_tab;
736         const gchar *last_dot = NULL;
737         const gchar *prelast_dot = NULL;
738         const gchar *last_tld_char = NULL;
739
740         /* the informative part of the email address (describing the name
741          * of the email address owner) may contain quoted parts. the
742          * closure stack stores the last encountered quotes. */
743         gchar closure_stack[128];
744         gchar *ptr = closure_stack;
745
746         g_return_val_if_fail(start != NULL, FALSE);
747         g_return_val_if_fail(scanpos != NULL, FALSE);
748         g_return_val_if_fail(bp != NULL, FALSE);
749         g_return_val_if_fail(ep != NULL, FALSE);
750
751         if (!dom_tab)
752                 dom_tab = create_domain_tab();
753         g_return_val_if_fail(dom_tab, FALSE);   
754
755         /* scan start of address */
756         for (bp_ = scanpos - 1; bp_ >= start && IS_RFC822_CHAR(*bp_); bp_--)
757                 ;
758
759         /* TODO: should start with an alnum? */
760         bp_++;
761         for (; bp_ < scanpos && !IS_ASCII_ALNUM(*bp_); bp_++)
762                 ;
763
764         if (bp_ != scanpos) {
765                 /* scan end of address */
766                 for (ep_ = scanpos + 1; *ep_ && IS_RFC822_CHAR(*ep_); ep_++)
767                         if (*ep_ == '.') {
768                                 prelast_dot = last_dot;
769                                 last_dot = ep_;
770                                 if (*(last_dot + 1) == '.') {
771                                         if (prelast_dot == NULL)
772                                                 return FALSE;
773                                         last_dot = prelast_dot;
774                                         break;
775                                 }
776                         }
777
778                 /* TODO: really should terminate with an alnum? */
779                 for (; ep_ > scanpos && !IS_ASCII_ALNUM(*ep_); --ep_)
780                         ;
781                 ep_++;
782
783                 if (last_dot == NULL)
784                         return FALSE;
785                 if (last_dot >= ep_)
786                         last_dot = prelast_dot;
787                 if (last_dot == NULL || (scanpos + 1 >= last_dot))
788                         return FALSE;
789                 last_dot++;
790
791                 for (last_tld_char = last_dot; last_tld_char < ep_; last_tld_char++)
792                         if (*last_tld_char == '?')
793                                 break;
794
795                 if (is_toplvl_domain(dom_tab, last_dot, last_tld_char))
796                         result = TRUE;
797
798                 *ep = ep_;
799                 *bp = bp_;
800         }
801
802         if (!result) return FALSE;
803
804         /* skip if it's between quotes "'alfons@proteus.demon.nl'" <alfons@proteus.demon.nl> */
805         if (bp_ - 1 > start && IS_QUOTE(*(bp_ - 1)) && IS_QUOTE(*ep_)) 
806                 return FALSE;
807
808         /* see if this is <bracketed>; in this case we also scan for the informative part. */
809         if (bp_ - 1 <= start || *(bp_ - 1) != '<' || *ep_ != '>')
810                 return TRUE;
811
812 #define FULL_STACK()    ((size_t) (ptr - closure_stack) >= sizeof closure_stack)
813 #define IN_STACK()      (ptr > closure_stack)
814 /* has underrun check */
815 #define POP_STACK()     if(IN_STACK()) --ptr
816 /* has overrun check */
817 #define PUSH_STACK(c)   if(!FULL_STACK()) *ptr++ = (c); else return TRUE
818 /* has underrun check */
819 #define PEEK_STACK()    (IN_STACK() ? *(ptr - 1) : 0)
820
821         ep_++;
822
823         /* scan for the informative part. */
824         for (bp_ -= 2; bp_ >= start; bp_--) {
825                 /* if closure on the stack keep scanning */
826                 if (PEEK_STACK() == *bp_) {
827                         POP_STACK();
828                         continue;
829                 }
830                 if (*bp_ == '\'' || *bp_ == '"') {
831                         PUSH_STACK(*bp_);
832                         continue;
833                 }
834
835                 /* if nothing in the closure stack, do the special conditions
836                  * the following if..else expression simply checks whether 
837                  * a token is acceptable. if not acceptable, the clause
838                  * should terminate the loop with a 'break' */
839                 if (!PEEK_STACK()) {
840                         if (*bp_ == '-'
841                         && (((bp_ - 1) >= start) && isalnum(*(bp_ - 1)))
842                         && (((bp_ + 1) < ep_)    && isalnum(*(bp_ + 1)))) {
843                                 /* hyphens are allowed, but only in
844                                    between alnums */
845                         } else if (!ispunct(*bp_)) {
846                                 /* but anything not being a punctiation
847                                    is ok */
848                         } else {
849                                 break; /* anything else is rejected */
850                         }
851                 }
852         }
853
854         bp_++;
855
856 #undef PEEK_STACK
857 #undef PUSH_STACK
858 #undef POP_STACK
859 #undef IN_STACK
860 #undef FULL_STACK
861
862         /* scan forward (should start with an alnum) */
863         for (; *bp_ != '<' && isspace(*bp_) && *bp_ != '"'; bp_++)
864                 ;
865
866         *ep = ep_;
867         *bp = bp_;
868
869         return result;
870 }
871
872 #undef IS_QUOTE
873 #undef IS_RFC822_CHAR
874
875 static gchar *make_email_string(const gchar *bp, const gchar *ep)
876 {
877         /* returns a mailto: URI; mailto: is also used to detect the
878          * uri type later on in the button_pressed signal handler */
879         gchar *tmp;
880         gchar *result;
881
882         tmp = g_strndup(bp, ep - bp);
883         result = g_strconcat("mailto:", tmp, NULL);
884         g_free(tmp);
885
886         return result;
887 }
888
889 #define ADD_TXT_POS(bp_, ep_, pti_) \
890         if ((last->next = alloca(sizeof(struct txtpos))) != NULL) { \
891                 last = last->next; \
892                 last->bp = (bp_); last->ep = (ep_); last->pti = (pti_); \
893                 last->next = NULL; \
894         } else { \
895                 g_warning("alloc error scanning URIs\n"); \
896                 gtk_stext_insert(text, textview->msgfont, fg_color, NULL, \
897                                 linebuf, -1); \
898                 return; \
899         }
900
901 /* textview_make_clickable_parts() - colorizes clickable parts */
902 static void textview_make_clickable_parts(TextView *textview,
903                                           GdkFont *font,
904                                           GdkColor *fg_color,
905                                           GdkColor *uri_color,
906                                           const gchar *linebuf)
907 {
908         /* parse table - in order of priority */
909         struct table {
910                 const gchar *needle; /* token */
911
912                 /* token search function */
913                 gchar    *(*search)     (const gchar *haystack,
914                                          const gchar *needle);
915                 /* part parsing function */
916                 gboolean  (*parse)      (const gchar *start,
917                                          const gchar *scanpos,
918                                          const gchar **bp_,
919                                          const gchar **ep_);
920                 /* part to URI function */
921                 gchar    *(*build_uri)  (const gchar *bp,
922                                          const gchar *ep);
923         };
924
925         static struct table parser[] = {
926                 {"http://",  strcasestr, get_uri_part,   make_uri_string},
927                 {"https://", strcasestr, get_uri_part,   make_uri_string},
928                 {"ftp://",   strcasestr, get_uri_part,   make_uri_string},
929                 {"www.",     strcasestr, get_uri_part,   make_uri_string},
930                 {"mailto:",  strcasestr, get_uri_part,   make_uri_string},
931                 {"@",        strcasestr, get_email_part, make_email_string}
932         };
933         const gint PARSE_ELEMS = sizeof parser / sizeof parser[0];
934
935         gint  n;
936         const gchar *walk, *bp, *ep;
937
938         struct txtpos {
939                 const gchar     *bp, *ep;       /* text position */
940                 gint             pti;           /* index in parse table */
941                 struct txtpos   *next;          /* next */
942         } head = {NULL, NULL, 0,  NULL}, *last = &head;
943
944         GtkSText *text = GTK_STEXT(textview->text);
945
946         /* parse for clickable parts, and build a list of begin and end positions  */
947         for (walk = linebuf, n = 0;;) {
948                 gint last_index = PARSE_ELEMS;
949                 gchar *scanpos = NULL;
950
951                 /* FIXME: this looks phony. scanning for anything in the parse table */
952                 for (n = 0; n < PARSE_ELEMS; n++) {
953                         gchar *tmp;
954
955                         tmp = parser[n].search(walk, parser[n].needle);
956                         if (tmp) {
957                                 if (scanpos == NULL || tmp < scanpos) {
958                                         scanpos = tmp;
959                                         last_index = n;
960                                 }
961                         }                                       
962                 }
963
964                 if (scanpos) {
965                         /* check if URI can be parsed */
966                         if (parser[last_index].parse(walk, scanpos, &bp, &ep)
967                             && (size_t) (ep - bp - 1) > strlen(parser[last_index].needle)) {
968                                         ADD_TXT_POS(bp, ep, last_index);
969                                         walk = ep;
970                         } else
971                                 walk = scanpos +
972                                         strlen(parser[last_index].needle);
973                 } else
974                         break;
975         }
976
977         /* colorize this line */
978         if (head.next) {
979                 const gchar *normal_text = linebuf;
980
981                 /* insert URIs */
982                 for (last = head.next; last != NULL;
983                      normal_text = last->ep, last = last->next) {
984                         RemoteURI *uri;
985
986                         uri = g_new(RemoteURI, 1);
987                         if (last->bp - normal_text > 0)
988                                 gtk_stext_insert(text, font,
989                                                 fg_color, NULL,
990                                                 normal_text,
991                                                 last->bp - normal_text);
992                         uri->uri = parser[last->pti].build_uri(last->bp,
993                                                                last->ep);
994                         uri->start = gtk_stext_get_point(text);
995                         gtk_stext_insert(text, font, uri_color,
996                                         NULL, last->bp, last->ep - last->bp);
997                         uri->end = gtk_stext_get_point(text);
998                         textview->uri_list =
999                                 g_slist_append(textview->uri_list, uri);
1000                 }
1001
1002                 if (*normal_text)
1003                         gtk_stext_insert(text, font, fg_color,
1004                                         NULL, normal_text, -1);
1005         } else
1006                 gtk_stext_insert(text, font, fg_color, NULL, linebuf, -1);
1007 }
1008
1009 #undef ADD_TXT_POS
1010
1011 static void textview_write_line(TextView *textview, const gchar *str,
1012                                 CodeConverter *conv)
1013 {
1014         GtkSText *text = GTK_STEXT(textview->text);
1015         gchar buf[BUFFSIZE];
1016         GdkColor *fg_color;
1017         gint quotelevel = -1;
1018
1019         if (!conv) {
1020                 if (textview->text_is_mb)
1021                         conv_localetodisp(buf, sizeof(buf), str);
1022                 else
1023                         strncpy2(buf, str, sizeof(buf));
1024         } else if (conv_convert(conv, buf, sizeof(buf), str) < 0)
1025                 conv_localetodisp(buf, sizeof(buf), str);
1026         else if (textview->text_is_mb)
1027                 conv_unreadable_locale(buf);
1028
1029         strcrchomp(buf);
1030         if (prefs_common.conv_mb_alnum) conv_mb_alnum(buf);
1031         fg_color = NULL;
1032
1033         /* change color of quotation
1034            >, foo>, _> ... ok, <foo>, foo bar>, foo-> ... ng
1035            Up to 3 levels of quotations are detected, and each
1036            level is colored using a different color. */
1037         if (prefs_common.enable_color 
1038             && line_has_quote_char(buf, prefs_common.quote_chars)) {
1039                 quotelevel = get_quote_level(buf, prefs_common.quote_chars);
1040
1041                 /* set up the correct foreground color */
1042                 if (quotelevel > 2) {
1043                         /* recycle colors */
1044                         if (prefs_common.recycle_quote_colors)
1045                                 quotelevel %= 3;
1046                         else
1047                                 quotelevel = 2;
1048                 }
1049         }
1050
1051         if (quotelevel == -1)
1052                 fg_color = NULL;
1053         else
1054                 fg_color = &quote_colors[quotelevel];
1055
1056         if (prefs_common.enable_color && (strcmp(buf,"-- \n") == 0 || textview->is_in_signature)) {
1057                 fg_color = &signature_color;
1058                 textview->is_in_signature = TRUE;
1059         }
1060         
1061         if (prefs_common.head_space && spacingfont && buf[0] != '\n')
1062                 gtk_stext_insert(text, spacingfont, NULL, NULL, " ", 1);
1063
1064         if (prefs_common.enable_color)
1065                 textview_make_clickable_parts(textview, textview->msgfont,
1066                                               fg_color, &uri_color, buf);
1067         else
1068                 textview_make_clickable_parts(textview, textview->msgfont,
1069                                               fg_color, NULL, buf);
1070 }
1071
1072 void textview_write_link(TextView *textview, const gchar *str,
1073                          const gchar *uri, CodeConverter *conv)
1074 {
1075         GdkColor *link_color = NULL;
1076         GtkSText *text = GTK_STEXT(textview->text);
1077         gchar buf[BUFFSIZE];
1078         gchar *bufp;
1079         RemoteURI *r_uri;
1080
1081         if (*str == '\0')
1082                 return;
1083
1084         if (!conv) {
1085                 if (textview->text_is_mb)
1086                         conv_localetodisp(buf, sizeof(buf), str);
1087                 else
1088                         strncpy2(buf, str, sizeof(buf));
1089         } else if (conv_convert(conv, buf, sizeof(buf), str) < 0)
1090                 conv_localetodisp(buf, sizeof(buf), str);
1091         else if (textview->text_is_mb)
1092                 conv_unreadable_locale(buf);
1093
1094         strcrchomp(buf);
1095
1096         for (bufp = buf; isspace(*bufp); bufp++)
1097                 gtk_stext_insert(text, textview->msgfont, NULL, NULL, bufp, 1);
1098
1099         if (prefs_common.enable_color) {
1100                 link_color = &uri_color;
1101         }
1102         r_uri = g_new(RemoteURI, 1);
1103         r_uri->uri = g_strdup(uri);
1104         r_uri->start = gtk_stext_get_point(text);
1105         gtk_stext_insert(text, textview->msgfont, link_color, NULL, bufp, -1);
1106         r_uri->end = gtk_stext_get_point(text);
1107         textview->uri_list = g_slist_append(textview->uri_list, r_uri);
1108 }
1109
1110 void textview_clear(TextView *textview)
1111 {
1112         GtkSText *text = GTK_STEXT(textview->text);
1113
1114         gtk_stext_freeze(text);
1115         gtk_stext_set_point(text, 0);
1116         gtk_stext_forward_delete(text, gtk_stext_get_length(text));
1117         gtk_stext_thaw(text);
1118
1119         textview_uri_list_remove_all(textview->uri_list);
1120         textview->uri_list = NULL;
1121
1122         textview->body_pos = 0;
1123         textview->cur_pos  = 0;
1124 }
1125
1126 void textview_destroy(TextView *textview)
1127 {
1128         textview_uri_list_remove_all(textview->uri_list);
1129         textview->uri_list = NULL;
1130
1131         if (!textview->scrolledwin_sb->parent)
1132                 gtk_widget_destroy(textview->scrolledwin_sb);
1133         if (!textview->scrolledwin_mb->parent)
1134                 gtk_widget_destroy(textview->scrolledwin_mb);
1135
1136         if (textview->msgfont)
1137                 gdk_font_unref(textview->msgfont);
1138         if (textview->boldfont)
1139                 gdk_font_unref(textview->boldfont);
1140
1141         g_free(textview);
1142 }
1143
1144 void textview_set_all_headers(TextView *textview, gboolean all_headers)
1145 {
1146         textview->show_all_headers = all_headers;
1147 }
1148
1149 void textview_set_font(TextView *textview, const gchar *codeset)
1150 {
1151         gboolean use_fontset = TRUE;
1152
1153         /* In multi-byte mode, GtkSText can't display 8bit characters
1154            correctly, so it must be single-byte mode. */
1155         if (MB_CUR_MAX > 1) {
1156                 if (codeset && conv_get_current_charset() != C_UTF_8) {
1157                         if (!g_strncasecmp(codeset, "ISO-8859-", 9) ||
1158                             !g_strcasecmp(codeset, "BALTIC"))
1159                                 use_fontset = FALSE;
1160                         else if (conv_get_current_charset() != C_EUC_JP &&
1161                                  (!g_strncasecmp(codeset, "KOI8-", 5) ||
1162                                   !g_strncasecmp(codeset, "CP", 2)    ||
1163                                   !g_strncasecmp(codeset, "WINDOWS-", 8)))
1164                                 use_fontset = FALSE;
1165                 }
1166         } else
1167                 use_fontset = FALSE;
1168
1169         if (textview->text_is_mb && !use_fontset) {
1170                 GtkWidget *parent;
1171
1172                 parent = textview->scrolledwin_mb->parent;
1173                 gtkut_container_remove(GTK_CONTAINER(parent),
1174                                        textview->scrolledwin_mb);
1175                 gtk_container_add(GTK_CONTAINER(parent),
1176                                   textview->scrolledwin_sb);
1177
1178                 textview->text = textview->text_sb;
1179                 textview->text_is_mb = FALSE;
1180         } else if (!textview->text_is_mb && use_fontset) {
1181                 GtkWidget *parent;
1182
1183                 parent = textview->scrolledwin_sb->parent;
1184                 gtkut_container_remove(GTK_CONTAINER(parent),
1185                                        textview->scrolledwin_sb);
1186                 gtk_container_add(GTK_CONTAINER(parent),
1187                                   textview->scrolledwin_mb);
1188
1189                 textview->text = textview->text_mb;
1190                 textview->text_is_mb = TRUE;
1191         }
1192
1193         if (prefs_common.textfont) {
1194                 GdkFont *font;
1195
1196                 if (use_fontset) {
1197                         if (text_mb_font) {
1198                                 text_mb_font->ascent = text_mb_font_orig_ascent;
1199                                 text_mb_font->descent = text_mb_font_orig_descent;
1200                         }
1201                         font = gdk_fontset_load(prefs_common.textfont);
1202                         if (font && text_mb_font != font) {
1203                                 if (text_mb_font)
1204                                         gdk_font_unref(text_mb_font);
1205                                 text_mb_font = font;
1206                                 text_mb_font_orig_ascent = font->ascent;
1207                                 text_mb_font_orig_descent = font->descent;
1208                         }
1209                 } else {
1210                         if (text_sb_font) {
1211                                 text_sb_font->ascent = text_sb_font_orig_ascent;
1212                                 text_sb_font->descent = text_sb_font_orig_descent;
1213                         }
1214                         if (MB_CUR_MAX > 1)
1215                                 font = gdk_font_load("-*-courier-medium-r-normal--14-*-*-*-*-*-iso8859-1");
1216                         else
1217                                 font = gtkut_font_load_from_fontset
1218                                         (prefs_common.textfont);
1219                         if (font && text_sb_font != font) {
1220                                 if (text_sb_font)
1221                                         gdk_font_unref(text_sb_font);
1222                                 text_sb_font = font;
1223                                 text_sb_font_orig_ascent = font->ascent;
1224                                 text_sb_font_orig_descent = font->descent;
1225                         }
1226                 }
1227
1228                 if (font) {
1229                         gint ascent, descent;
1230
1231                         descent = prefs_common.line_space / 2;
1232                         ascent  = prefs_common.line_space - descent;
1233                         font->ascent  += ascent;
1234                         font->descent += descent;
1235
1236                         if (textview->msgfont)
1237                                 gdk_font_unref(textview->msgfont);
1238                         textview->msgfont = font;
1239                         gdk_font_ref(font);
1240                 }
1241         }
1242
1243         if (!textview->boldfont && prefs_common.boldfont)
1244                 textview->boldfont = gtkut_font_load(prefs_common.boldfont);
1245         if (!spacingfont)
1246                 spacingfont = gdk_font_load("-*-*-medium-r-normal--6-*");
1247 }
1248
1249 enum
1250 {
1251         H_DATE          = 0,
1252         H_FROM          = 1,
1253         H_TO            = 2,
1254         H_NEWSGROUPS    = 3,
1255         H_SUBJECT       = 4,
1256         H_CC            = 5,
1257         H_REPLY_TO      = 6,
1258         H_FOLLOWUP_TO   = 7,
1259         H_X_MAILER      = 8,
1260         H_X_NEWSREADER  = 9,
1261         H_USER_AGENT    = 10,
1262         H_ORGANIZATION  = 11,
1263 };
1264
1265 void textview_set_position(TextView *textview, gint pos)
1266 {
1267         if (pos < 0) {
1268                 textview->cur_pos =
1269                         gtk_stext_get_length(GTK_STEXT(textview->text));
1270         } else {
1271                 textview->cur_pos = pos;
1272         }
1273 }
1274
1275 static GPtrArray *textview_scan_header(TextView *textview, FILE *fp)
1276 {
1277         gchar buf[BUFFSIZE];
1278         GPtrArray *headers, *sorted_headers;
1279         GSList *disphdr_list;
1280         Header *header;
1281         gint i;
1282
1283         g_return_val_if_fail(fp != NULL, NULL);
1284
1285         if (textview->show_all_headers)
1286                 return procheader_get_header_array_asis(fp);
1287
1288         if (!prefs_common.display_header) {
1289                 while (fgets(buf, sizeof(buf), fp) != NULL)
1290                         if (buf[0] == '\r' || buf[0] == '\n') break;
1291                 return NULL;
1292         }
1293
1294         headers = procheader_get_header_array_asis(fp);
1295
1296         sorted_headers = g_ptr_array_new();
1297
1298         for (disphdr_list = prefs_common.disphdr_list; disphdr_list != NULL;
1299              disphdr_list = disphdr_list->next) {
1300                 DisplayHeaderProp *dp =
1301                         (DisplayHeaderProp *)disphdr_list->data;
1302
1303                 for (i = 0; i < headers->len; i++) {
1304                         header = g_ptr_array_index(headers, i);
1305
1306                         if (procheader_headername_equal(header->name,
1307                                                         dp->name)) {
1308                                 if (dp->hidden)
1309                                         procheader_header_free(header);
1310                                 else
1311                                         g_ptr_array_add(sorted_headers, header);
1312
1313                                 g_ptr_array_remove_index(headers, i);
1314                                 i--;
1315                         }
1316                 }
1317         }
1318
1319         if (prefs_common.show_other_header) {
1320                 for (i = 0; i < headers->len; i++) {
1321                         header = g_ptr_array_index(headers, i);
1322                         g_ptr_array_add(sorted_headers, header);
1323                 }
1324                 g_ptr_array_free(headers, TRUE);
1325         } else
1326                 procheader_header_array_destroy(headers);
1327
1328
1329         return sorted_headers;
1330 }
1331
1332 static void textview_show_header(TextView *textview, GPtrArray *headers)
1333 {
1334         GtkSText *text = GTK_STEXT(textview->text);
1335         Header *header;
1336         gint i;
1337
1338         g_return_if_fail(headers != NULL);
1339
1340         gtk_stext_freeze(text);
1341
1342         for (i = 0; i < headers->len; i++) {
1343                 header = g_ptr_array_index(headers, i);
1344                 g_return_if_fail(header->name != NULL);
1345
1346                 gtk_stext_insert(text, textview->boldfont, NULL, NULL,
1347                                 header->name, -1);
1348                 if (header->name[strlen(header->name) - 1] != ' ')
1349                         gtk_stext_insert(text, textview->boldfont,
1350                                         NULL, NULL, " ", 1);
1351
1352                 if (procheader_headername_equal(header->name, "Subject") ||
1353                     procheader_headername_equal(header->name, "From")    ||
1354                     procheader_headername_equal(header->name, "To")      ||
1355                     procheader_headername_equal(header->name, "Cc"))
1356                         unfold_line(header->body);
1357
1358                 if (textview->text_is_mb == TRUE)
1359                         conv_unreadable_locale(header->body);
1360
1361                 if (prefs_common.enable_color &&
1362                     (procheader_headername_equal(header->name, "X-Mailer") ||
1363                      procheader_headername_equal(header->name,
1364                                                  "X-Newsreader")) &&
1365                     strstr(header->body, "Sylpheed") != NULL)
1366                         gtk_stext_insert(text, NULL, &emphasis_color, NULL,
1367                                         header->body, -1);
1368                 else if (prefs_common.enable_color) {
1369                         textview_make_clickable_parts(textview,
1370                                                       NULL, NULL, &uri_color,
1371                                                       header->body);
1372                 } else {
1373                         textview_make_clickable_parts(textview,
1374                                                       NULL, NULL, NULL,
1375                                                       header->body);
1376                 }
1377                 gtk_stext_insert(text, textview->msgfont, NULL, NULL, "\n", 1);
1378         }
1379
1380         gtk_stext_thaw(text);
1381 }
1382
1383 gboolean textview_search_string(TextView *textview, const gchar *str,
1384                                 gboolean case_sens)
1385 {
1386         GtkSText *text = GTK_STEXT(textview->text);
1387         gint pos;
1388         gint len;
1389
1390         g_return_val_if_fail(str != NULL, FALSE);
1391
1392         len = get_mbs_len(str);
1393         g_return_val_if_fail(len >= 0, FALSE);
1394
1395         pos = textview->cur_pos;
1396         if (pos < textview->body_pos)
1397                 pos = textview->body_pos;
1398
1399         if ((pos = gtkut_stext_find(text, pos, str, case_sens)) != -1) {
1400                 gtk_editable_set_position(GTK_EDITABLE(text), pos + len);
1401                 gtk_editable_select_region(GTK_EDITABLE(text), pos, pos + len);
1402                 textview_set_position(textview, pos + len);
1403                 return TRUE;
1404         }
1405
1406         return FALSE;
1407 }
1408
1409 gboolean textview_search_string_backward(TextView *textview, const gchar *str,
1410                                          gboolean case_sens)
1411 {
1412         GtkSText *text = GTK_STEXT(textview->text);
1413         gint pos;
1414         wchar_t *wcs;
1415         gint len;
1416         gint text_len;
1417         gboolean found = FALSE;
1418
1419         g_return_val_if_fail(str != NULL, FALSE);
1420
1421         wcs = strdup_mbstowcs(str);
1422         g_return_val_if_fail(wcs != NULL, FALSE);
1423         len = wcslen(wcs);
1424         pos = textview->cur_pos;
1425         text_len = gtk_stext_get_length(text);
1426         if (text_len - textview->body_pos < len) {
1427                 g_free(wcs);
1428                 return FALSE;
1429         }
1430         if (pos <= textview->body_pos || text_len - pos < len)
1431                 pos = text_len - len;
1432
1433         for (; pos >= textview->body_pos; pos--) {
1434                 if (gtk_stext_match_string(text, pos, wcs, len, case_sens)
1435                     == TRUE) {
1436                         gtk_editable_set_position(GTK_EDITABLE(text), pos);
1437                         gtk_editable_select_region(GTK_EDITABLE(text),
1438                                                    pos, pos + len);
1439                         textview_set_position(textview, pos - 1);
1440                         found = TRUE;
1441                         break;
1442                 }
1443                 if (pos == textview->body_pos) break;
1444         }
1445
1446         g_free(wcs);
1447         return found;
1448 }
1449
1450 void textview_scroll_one_line(TextView *textview, gboolean up)
1451 {
1452         GtkSText *text = GTK_STEXT(textview->text);
1453         gfloat upper;
1454
1455         if (prefs_common.enable_smooth_scroll) {
1456                 textview_smooth_scroll_one_line(textview, up);
1457                 return;
1458         }
1459
1460         if (!up) {
1461                 upper = text->vadj->upper - text->vadj->page_size;
1462                 if (text->vadj->value < upper) {
1463                         text->vadj->value +=
1464                                 text->vadj->step_increment * 4;
1465                         text->vadj->value =
1466                                 MIN(text->vadj->value, upper);
1467                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1468                                                 "value_changed");
1469                 }
1470         } else {
1471                 if (text->vadj->value > 0.0) {
1472                         text->vadj->value -=
1473                                 text->vadj->step_increment * 4;
1474                         text->vadj->value =
1475                                 MAX(text->vadj->value, 0.0);
1476                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1477                                                 "value_changed");
1478                 }
1479         }
1480 }
1481
1482 gboolean textview_scroll_page(TextView *textview, gboolean up)
1483 {
1484         GtkSText *text = GTK_STEXT(textview->text);
1485         gfloat upper;
1486         gfloat page_incr;
1487
1488         if (prefs_common.enable_smooth_scroll)
1489                 return textview_smooth_scroll_page(textview, up);
1490
1491         if (prefs_common.scroll_halfpage)
1492                 page_incr = text->vadj->page_increment / 2;
1493         else
1494                 page_incr = text->vadj->page_increment;
1495
1496         if (!up) {
1497                 upper = text->vadj->upper - text->vadj->page_size;
1498                 if (text->vadj->value < upper) {
1499                         text->vadj->value += page_incr;
1500                         text->vadj->value = MIN(text->vadj->value, upper);
1501                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1502                                                 "value_changed");
1503                 } else
1504                         return FALSE;
1505         } else {
1506                 if (text->vadj->value > 0.0) {
1507                         text->vadj->value -= page_incr;
1508                         text->vadj->value = MAX(text->vadj->value, 0.0);
1509                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1510                                                 "value_changed");
1511                 } else
1512                         return FALSE;
1513         }
1514
1515         return TRUE;
1516 }
1517
1518 static void textview_smooth_scroll_do(TextView *textview,
1519                                       gfloat old_value, gfloat last_value,
1520                                       gint step)
1521 {
1522         GtkSText *text = GTK_STEXT(textview->text);
1523         gint change_value;
1524         gboolean up;
1525         gint i;
1526
1527         if (old_value < last_value) {
1528                 change_value = last_value - old_value;
1529                 up = FALSE;
1530         } else {
1531                 change_value = old_value - last_value;
1532                 up = TRUE;
1533         }
1534
1535         gdk_key_repeat_disable();
1536
1537         for (i = step; i <= change_value; i += step) {
1538                 text->vadj->value = old_value + (up ? -i : i);
1539                 gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1540                                         "value_changed");
1541         }
1542
1543         text->vadj->value = last_value;
1544         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj), "value_changed");
1545
1546         gdk_key_repeat_restore();
1547 }
1548
1549 static void textview_smooth_scroll_one_line(TextView *textview, gboolean up)
1550 {
1551         GtkSText *text = GTK_STEXT(textview->text);
1552         gfloat upper;
1553         gfloat old_value;
1554         gfloat last_value;
1555
1556         if (!up) {
1557                 upper = text->vadj->upper - text->vadj->page_size;
1558                 if (text->vadj->value < upper) {
1559                         old_value = text->vadj->value;
1560                         last_value = text->vadj->value +
1561                                 text->vadj->step_increment * 4;
1562                         last_value = MIN(last_value, upper);
1563
1564                         textview_smooth_scroll_do(textview, old_value,
1565                                                   last_value,
1566                                                   prefs_common.scroll_step);
1567                 }
1568         } else {
1569                 if (text->vadj->value > 0.0) {
1570                         old_value = text->vadj->value;
1571                         last_value = text->vadj->value -
1572                                 text->vadj->step_increment * 4;
1573                         last_value = MAX(last_value, 0.0);
1574
1575                         textview_smooth_scroll_do(textview, old_value,
1576                                                   last_value,
1577                                                   prefs_common.scroll_step);
1578                 }
1579         }
1580 }
1581
1582 static gboolean textview_smooth_scroll_page(TextView *textview, gboolean up)
1583 {
1584         GtkSText *text = GTK_STEXT(textview->text);
1585         gfloat upper;
1586         gfloat page_incr;
1587         gfloat old_value;
1588         gfloat last_value;
1589
1590         if (prefs_common.scroll_halfpage)
1591                 page_incr = text->vadj->page_increment / 2;
1592         else
1593                 page_incr = text->vadj->page_increment;
1594
1595         if (!up) {
1596                 upper = text->vadj->upper - text->vadj->page_size;
1597                 if (text->vadj->value < upper) {
1598                         old_value = text->vadj->value;
1599                         last_value = text->vadj->value + page_incr;
1600                         last_value = MIN(last_value, upper);
1601
1602                         textview_smooth_scroll_do(textview, old_value,
1603                                                   last_value,
1604                                                   prefs_common.scroll_step);
1605                 } else
1606                         return FALSE;
1607         } else {
1608                 if (text->vadj->value > 0.0) {
1609                         old_value = text->vadj->value;
1610                         last_value = text->vadj->value - page_incr;
1611                         last_value = MAX(last_value, 0.0);
1612
1613                         textview_smooth_scroll_do(textview, old_value,
1614                                                   last_value,
1615                                                   prefs_common.scroll_step);
1616                 } else
1617                         return FALSE;
1618         }
1619
1620         return TRUE;
1621 }
1622
1623 #define KEY_PRESS_EVENT_STOP() \
1624         if (gtk_signal_n_emissions_by_name \
1625                 (GTK_OBJECT(widget), "key_press_event") > 0) { \
1626                 gtk_signal_emit_stop_by_name(GTK_OBJECT(widget), \
1627                                              "key_press_event"); \
1628         }
1629
1630 static gint textview_key_pressed(GtkWidget *widget, GdkEventKey *event,
1631                                  TextView *textview)
1632 {
1633         SummaryView *summaryview = NULL;
1634         MessageView *messageview = textview->messageview;
1635
1636         if (!event) return FALSE;
1637         if (messageview->mainwin)
1638                 summaryview = messageview->mainwin->summaryview;
1639
1640         switch (event->keyval) {
1641         case GDK_Tab:
1642         case GDK_Home:
1643         case GDK_Left:
1644         case GDK_Up:
1645         case GDK_Right:
1646         case GDK_Down:
1647         case GDK_Page_Up:
1648         case GDK_Page_Down:
1649         case GDK_End:
1650         case GDK_Control_L:
1651         case GDK_Control_R:
1652                 break;
1653         case GDK_space:
1654                 if (summaryview)
1655                         summary_pass_key_press_event(summaryview, event);
1656                 else
1657                         textview_scroll_page(textview, FALSE);
1658                 break;
1659         case GDK_BackSpace:
1660                 textview_scroll_page(textview, TRUE);
1661                 break;
1662         case GDK_Return:
1663                 textview_scroll_one_line(textview,
1664                                          (event->state & GDK_MOD1_MASK) != 0);
1665                 break;
1666         case GDK_Delete:
1667                 if (summaryview)
1668                         summary_pass_key_press_event(summaryview, event);
1669                 break;
1670         case GDK_n:
1671         case GDK_N:
1672         case GDK_p:
1673         case GDK_P:
1674         case GDK_y:
1675         case GDK_t:
1676         case GDK_l:
1677                 KEY_PRESS_EVENT_STOP();
1678                 mimeview_pass_key_press_event(messageview->mimeview,
1679                                               event);
1680                 /* fall through */
1681         default:
1682                 if (summaryview &&
1683                     event->window != messageview->mainwin->window->window) {
1684                         GdkEventKey tmpev = *event;
1685
1686                         tmpev.window = messageview->mainwin->window->window;
1687                         KEY_PRESS_EVENT_STOP();
1688                         gtk_widget_event(messageview->mainwin->window,
1689                                          (GdkEvent *)&tmpev);
1690                 }
1691                 break;
1692         }
1693
1694         return TRUE;
1695 }
1696
1697 static gint show_url_timeout_cb(gpointer data)
1698 {
1699         TextView *textview = (TextView *)data;
1700         
1701         if (textview->messageview->mainwin)
1702                 if (textview->show_url_msgid)
1703                         gtk_statusbar_remove(GTK_STATUSBAR(
1704                                 textview->messageview->mainwin->statusbar),
1705                                 textview->messageview->mainwin->folderview_cid,
1706                                 textview->show_url_msgid);
1707                 return FALSE;
1708 }
1709
1710 static gint textview_button_pressed(GtkWidget *widget, GdkEventButton *event,
1711                                     TextView *textview)
1712 {
1713         if (event)
1714                 textview->last_buttonpress = event->type;
1715         return FALSE;
1716 }
1717
1718 static gint textview_button_released(GtkWidget *widget, GdkEventButton *event,
1719                                     TextView *textview)
1720 {
1721         textview->cur_pos = 
1722                 gtk_editable_get_position(GTK_EDITABLE(textview->text));
1723
1724         if (event && 
1725             ((event->button == 1)
1726              || event->button == 2 || event->button == 3)) {
1727                 GSList *cur;
1728
1729                 /* double click seems to set the cursor after the current
1730                  * word. The cursor position needs fixing, otherwise the
1731                  * last word of a clickable zone will not work */
1732                 if (event->button == 1 && textview->last_buttonpress == GDK_2BUTTON_PRESS) {
1733                         textview->cur_pos--;
1734                 }
1735
1736                 for (cur = textview->uri_list; cur != NULL; cur = cur->next) {
1737                         RemoteURI *uri = (RemoteURI *)cur->data;
1738
1739                         if (textview->cur_pos >= uri->start &&
1740                             textview->cur_pos <= uri->end) {
1741                                 gchar *trimmed_uri;
1742                                 
1743                                 trimmed_uri = trim_string(uri->uri, 60);
1744                                 /* single click: display url in statusbar */
1745                                 if (event->button == 1 && textview->last_buttonpress != GDK_2BUTTON_PRESS) {
1746                                         if (textview->messageview->mainwin) {
1747                                                 if (textview->show_url_msgid) {
1748                                                         gtk_timeout_remove(textview->show_url_timeout_tag);
1749                                                         gtk_statusbar_remove(GTK_STATUSBAR(
1750                                                                 textview->messageview->mainwin->statusbar),
1751                                                                 textview->messageview->mainwin->folderview_cid,
1752                                                                 textview->show_url_msgid);
1753                                                         textview->show_url_msgid = 0;
1754                                                 }
1755                                                 textview->show_url_msgid = gtk_statusbar_push(
1756                                                                 GTK_STATUSBAR(textview->messageview->mainwin->statusbar),
1757                                                                 textview->messageview->mainwin->folderview_cid,
1758                                                                 trimmed_uri);
1759                                                 textview->show_url_timeout_tag = gtk_timeout_add( 4000, show_url_timeout_cb, textview );
1760                                                 gtkut_widget_wait_for_draw(textview->messageview->mainwin->hbox_stat);
1761                                         }
1762                                 } else
1763                                 if (!g_strncasecmp(uri->uri, "mailto:", 7)) {
1764                                         if (event->button == 3) {
1765                                                 gchar *fromname, *fromaddress;
1766                                                 
1767                                                 /* extract url */
1768                                                 fromaddress = g_strdup(uri->uri + 7);
1769                                                 /* Hiroyuki: please put this function in utils.c! */
1770                                                 fromname = procheader_get_fromname(fromaddress);
1771                                                 extract_address(fromaddress);
1772                                                 g_message("adding from textview %s <%s>", fromname, fromaddress);
1773                                                 /* Add to address book - Match */
1774                                                 addressbook_add_contact( fromname, fromaddress, NULL );
1775                                                 
1776                                                 g_free(fromaddress);
1777                                                 g_free(fromname);
1778                                         } else {
1779                                                 PrefsAccount *account = NULL;
1780                                                 FolderItem   *folder_item;
1781
1782                                                 if (textview->messageview && textview->messageview->mainwin 
1783                                                 &&  textview->messageview->mainwin->summaryview 
1784                                                 &&  textview->messageview->mainwin->summaryview->folder_item) {
1785                                                         folder_item = textview->messageview->mainwin->summaryview->folder_item;
1786                                                         if (folder_item->prefs && folder_item->prefs->enable_default_account)
1787                                                                 account = account_find_from_id(folder_item->prefs->default_account);
1788                                                 }
1789                                                 compose_new(account, uri->uri + 7, NULL);
1790                                         }
1791                                 } else {
1792                                         open_uri(uri->uri,
1793                                                  prefs_common.uri_cmd);
1794                                 }
1795                                 g_free(trimmed_uri);
1796                         }
1797                 }
1798         }
1799         if (event)
1800                 textview->last_buttonpress = event->type;
1801         return FALSE;
1802 }
1803
1804 static void textview_uri_list_remove_all(GSList *uri_list)
1805 {
1806         GSList *cur;
1807
1808         for (cur = uri_list; cur != NULL; cur = cur->next) {
1809                 if (cur->data) {
1810                         g_free(((RemoteURI *)cur->data)->uri);
1811                         g_free(cur->data);
1812                 }
1813         }
1814
1815         g_slist_free(uri_list);
1816 }