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