0d1114702a13e8569be20333825c8b6cf7846776
[claws.git] / src / textview.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999-2001 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/gtktext.h>
32 #include <gtk/gtksignal.h>
33 #include <stdio.h>
34 #include <ctype.h>
35 #include <string.h>
36 #include <stdlib.h>
37
38 #include "intl.h"
39 #include "main.h"
40 #include "summaryview.h"
41 #include "procheader.h"
42 #include "prefs_common.h"
43 #include "codeconv.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
54 #define FONT_LOAD(font, s) \
55 { \
56         gchar *fontstr, *p; \
57  \
58         Xstrdup_a(fontstr, s, ); \
59         if ((p = strchr(fontstr, ',')) != NULL) *p = '\0'; \
60         font = gdk_font_load(fontstr); \
61 }
62
63 typedef struct _RemoteURI       RemoteURI;
64
65 struct _RemoteURI
66 {
67         gchar *uri;
68
69         guint start;
70         guint end;
71 };
72
73 static GdkColor quote_colors[3] = {
74         {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
75         {(gulong)0, (gushort)0, (gushort)0, (gushort)0},
76         {(gulong)0, (gushort)0, (gushort)0, (gushort)0}
77 };
78
79 static GdkColor uri_color = {
80         (gulong)0,
81         (gushort)0,
82         (gushort)0,
83         (gushort)0
84 };
85
86 static GdkColor emphasis_color = {
87         (gulong)0,
88         (gushort)0,
89         (gushort)0,
90         (gushort)0xcfff
91 };
92
93 static GdkColor error_color = {
94         (gulong)0,
95         (gushort)0xefff,
96         (gushort)0,
97         (gushort)0
98 };
99
100 static GdkFont *spacingfont;
101
102 static void textview_show_ertf          (TextView       *textview,
103                                          FILE           *fp,
104                                          CodeConverter  *conv);
105 static void textview_show_html          (TextView       *textview,
106                                          FILE           *fp,
107                                          CodeConverter  *conv);
108 static void textview_write_line         (TextView       *textview,
109                                          const gchar    *str,
110                                          CodeConverter  *conv);
111 static void textview_write_link         (TextView       *textview,
112                                          const gchar    *url,
113                                          const gchar    *str,
114                                          CodeConverter  *conv);
115 static GPtrArray *textview_scan_header  (TextView       *textview,
116                                          FILE           *fp);
117 static void textview_show_header        (TextView       *textview,
118                                          GPtrArray      *headers);
119
120 static void textview_key_pressed        (GtkWidget      *widget,
121                                          GdkEventKey    *event,
122                                          TextView       *textview);
123 static gint textview_button_pressed     (GtkWidget      *widget,
124                                          GdkEventButton *event,
125                                          TextView       *textview);
126 static gint textview_button_released    (GtkWidget      *widget,
127                                          GdkEventButton *event,
128                                          TextView       *textview);
129
130 static void textview_uri_list_remove_all(GSList         *uri_list);
131
132 static void textview_smooth_scroll_do           (TextView       *textview,
133                                                  gfloat          old_value,
134                                                  gfloat          last_value,
135                                                  gint            step);
136 static void textview_smooth_scroll_one_line     (TextView       *textview,
137                                                  gboolean        up);
138 static gboolean textview_smooth_scroll_page     (TextView       *textview,
139                                                  gboolean        up);
140
141
142 TextView *textview_create(void)
143 {
144         TextView *textview;
145         GtkWidget *vbox;
146         GtkWidget *scrolledwin_sb;
147         GtkWidget *scrolledwin_mb;
148         GtkWidget *text_sb;
149         GtkWidget *text_mb;
150
151         debug_print(_("Creating text view...\n"));
152         textview = g_new0(TextView, 1);
153
154         scrolledwin_sb = gtk_scrolled_window_new(NULL, NULL);
155         scrolledwin_mb = gtk_scrolled_window_new(NULL, NULL);
156         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin_sb),
157                                        GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
158         gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin_mb),
159                                        GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
160         gtk_widget_set_usize(scrolledwin_sb, prefs_common.mainview_width, -1);
161         gtk_widget_set_usize(scrolledwin_mb, prefs_common.mainview_width, -1);
162
163         /* create GtkText widgets for single-byte and multi-byte character */
164         text_sb = gtk_text_new(NULL, NULL);
165         text_mb = gtk_text_new(NULL, NULL);
166         GTK_TEXT(text_sb)->default_tab_width = 8;
167         GTK_TEXT(text_mb)->default_tab_width = 8;
168         gtk_widget_show(text_sb);
169         gtk_widget_show(text_mb);
170         gtk_text_set_word_wrap(GTK_TEXT(text_sb), TRUE);
171         gtk_text_set_word_wrap(GTK_TEXT(text_mb), TRUE);
172         gtk_widget_ensure_style(text_sb);
173         gtk_widget_ensure_style(text_mb);
174         if (text_sb->style && text_sb->style->font->type == GDK_FONT_FONTSET) {
175                 GtkStyle *style;
176
177                 style = gtk_style_copy(text_sb->style);
178                 gdk_font_unref(style->font);
179                 FONT_LOAD(style->font, NORMAL_FONT);
180                 gtk_widget_set_style(text_sb, style);
181         }
182         if (text_mb->style && text_mb->style->font->type == GDK_FONT_FONT) {
183                 GtkStyle *style;
184
185                 style = gtk_style_copy(text_mb->style);
186                 gdk_font_unref(style->font);
187                 style->font = gdk_fontset_load(NORMAL_FONT);
188                 gtk_widget_set_style(text_mb, style);
189         }
190         gtk_widget_ref(scrolledwin_sb);
191         gtk_widget_ref(scrolledwin_mb);
192
193         gtk_container_add(GTK_CONTAINER(scrolledwin_sb), text_sb);
194         gtk_container_add(GTK_CONTAINER(scrolledwin_mb), text_mb);
195         gtk_signal_connect(GTK_OBJECT(text_sb), "key_press_event",
196                            GTK_SIGNAL_FUNC(textview_key_pressed),
197                            textview);
198         gtk_signal_connect_after(GTK_OBJECT(text_sb), "button_press_event",
199                                  GTK_SIGNAL_FUNC(textview_button_pressed),
200                                  textview);
201         gtk_signal_connect_after(GTK_OBJECT(text_sb), "button_release_event",
202                                  GTK_SIGNAL_FUNC(textview_button_released),
203                                  textview);
204         gtk_signal_connect(GTK_OBJECT(text_mb), "key_press_event",
205                            GTK_SIGNAL_FUNC(textview_key_pressed),
206                            textview);
207         gtk_signal_connect_after(GTK_OBJECT(text_mb), "button_press_event",
208                                  GTK_SIGNAL_FUNC(textview_button_pressed),
209                                  textview);
210         gtk_signal_connect_after(GTK_OBJECT(text_mb), "button_release_event",
211                                  GTK_SIGNAL_FUNC(textview_button_released),
212                                  textview);
213
214         gtk_widget_show(scrolledwin_sb);
215         gtk_widget_show(scrolledwin_mb);
216
217         vbox = gtk_vbox_new(FALSE, 0);
218         gtk_box_pack_start(GTK_BOX(vbox), scrolledwin_sb, TRUE, TRUE, 0);
219
220         textview->vbox           = vbox;
221         textview->scrolledwin    = scrolledwin_sb;
222         textview->scrolledwin_sb = scrolledwin_sb;
223         textview->scrolledwin_mb = scrolledwin_mb;
224         textview->text           = text_sb;
225         textview->text_sb        = text_sb;
226         textview->text_mb        = text_mb;
227         textview->text_is_mb     = FALSE;
228         textview->uri_list       = NULL;
229         textview->body_pos       = 0;
230         textview->cur_pos        = 0;
231         textview->last_buttonpress = GDK_NOTHING;
232
233         return textview;
234 }
235
236 void textview_init(TextView *textview)
237 {
238         gtkut_widget_disable_theme_engine(textview->text_sb);
239         gtkut_widget_disable_theme_engine(textview->text_mb);
240         textview_update_message_colors();
241         textview_set_font(textview, NULL);
242 }
243
244 void textview_update_message_colors(void)
245 {
246         GdkColor black = {0, 0, 0, 0};
247
248         if (prefs_common.enable_color) {
249                 /* grab the quote colors, converting from an int to a GdkColor */
250                 gtkut_convert_int_to_gdk_color(prefs_common.quote_level1_col,
251                                                &quote_colors[0]);
252                 gtkut_convert_int_to_gdk_color(prefs_common.quote_level2_col,
253                                                &quote_colors[1]);
254                 gtkut_convert_int_to_gdk_color(prefs_common.quote_level3_col,
255                                                &quote_colors[2]);
256                 gtkut_convert_int_to_gdk_color(prefs_common.uri_col,
257                                                &uri_color);
258         } else {
259                 quote_colors[0] = quote_colors[1] = quote_colors[2] = 
260                         uri_color = emphasis_color = black;
261         }
262 }
263
264 void textview_show_message(TextView *textview, MimeInfo *mimeinfo,
265                            const gchar *file)
266 {
267         FILE *fp;
268
269         if ((fp = fopen(file, "r")) == NULL) {
270                 FILE_OP_ERROR(file, "fopen");
271                 return;
272         }
273
274         textview_show_part(textview, mimeinfo, fp);
275
276         fclose(fp);
277 }
278
279 void textview_show_part(TextView *textview, MimeInfo *mimeinfo, FILE *fp)
280 {
281         GtkText *text;
282         gchar buf[BUFFSIZE];
283         const gchar *boundary = NULL;
284         gint boundary_len = 0;
285         const gchar *charset = NULL;
286         FILE *tmpfp;
287         GPtrArray *headers = NULL;
288         CodeConverter *conv;
289
290         g_return_if_fail(mimeinfo != NULL);
291         g_return_if_fail(fp != NULL);
292
293         if (mimeinfo->mime_type == MIME_MULTIPART) {
294                 if (mimeinfo->sub) {
295                         mimeinfo = mimeinfo->sub;
296                         if (fseek(fp, mimeinfo->fpos, SEEK_SET) < 0) {
297                                 perror("fseek");
298                                 return;
299                         }
300                 } else
301                         return;
302         }
303         if (mimeinfo->parent && mimeinfo->parent->boundary) {
304                 boundary = mimeinfo->parent->boundary;
305                 boundary_len = strlen(boundary);
306         }
307
308         if (!boundary && (mimeinfo->mime_type == MIME_TEXT || mimeinfo->mime_type == MIME_TEXT_HTML || mimeinfo->mime_type == MIME_TEXT_ENRICHED)) {
309         
310                 if (fseek(fp, mimeinfo->fpos, SEEK_SET) < 0)
311                         perror("fseek");
312                 headers = textview_scan_header(textview, fp);
313         } else {
314                 if (mimeinfo->mime_type == MIME_TEXT && mimeinfo->parent) {
315                         glong fpos;
316                         MimeInfo *parent = mimeinfo->parent;
317
318                         while (parent->parent) {
319                                 if (parent->main &&
320                                     parent->main->mime_type ==
321                                         MIME_MESSAGE_RFC822)
322                                         break;
323                                 parent = parent->parent;
324                         }
325
326                         if ((fpos = ftell(fp)) < 0)
327                                 perror("ftell");
328                         else if (fseek(fp, parent->fpos, SEEK_SET) < 0)
329                                 perror("fseek");
330                         else {
331                                 headers = textview_scan_header(textview, fp);
332                                 if (fseek(fp, fpos, SEEK_SET) < 0)
333                                         perror("fseek");
334                         }
335                 }
336                 /* skip MIME part headers */
337                 while (fgets(buf, sizeof(buf), fp) != NULL)
338                         if (buf[0] == '\r' || buf[0] == '\n') break;
339         }
340
341         /* display attached RFC822 single text message */
342         if (mimeinfo->parent && mimeinfo->mime_type == MIME_MESSAGE_RFC822) {
343                 if (headers) procheader_header_array_destroy(headers);
344                 if (!mimeinfo->sub || mimeinfo->sub->children) return;
345                 headers = textview_scan_header(textview, fp);
346                 mimeinfo = mimeinfo->sub;
347         } else if (!mimeinfo->parent &&
348                    mimeinfo->mime_type == MIME_MESSAGE_RFC822) {
349                 if (headers) procheader_header_array_destroy(headers);
350                 if (!mimeinfo->sub) return;
351                 headers = textview_scan_header(textview, fp);
352                 mimeinfo = mimeinfo->sub;
353         }
354
355         if (prefs_common.force_charset)
356                 charset = prefs_common.force_charset;
357         else if (mimeinfo->charset)
358                 charset = mimeinfo->charset;
359         textview_set_font(textview, charset);
360
361         conv = conv_code_converter_new(charset);
362
363         textview_clear(textview);
364         text = GTK_TEXT(textview->text);
365         gtk_text_freeze(text);
366
367         textview->body_pos = 0;
368         textview->cur_pos  = 0;
369
370         if (headers) {
371                 textview_show_header(textview, headers);
372                 procheader_header_array_destroy(headers);
373         }
374
375         tmpfp = procmime_decode_content(NULL, fp, mimeinfo);
376         if (tmpfp) {
377                 if (mimeinfo->mime_type == MIME_TEXT_HTML)
378                         textview_show_html(textview, tmpfp, conv);
379                 else if (mimeinfo->mime_type == MIME_TEXT_ENRICHED)
380                         textview_show_ertf(textview, tmpfp, conv);
381                 else
382                         while (fgets(buf, sizeof(buf), tmpfp) != NULL)
383                                 textview_write_line(textview, buf, conv);
384                 fclose(tmpfp);
385         }
386
387         conv_code_converter_destroy(conv);
388
389         gtk_text_thaw(text);
390 }
391
392 #define TEXT_INSERT(str) \
393         gtk_text_insert(text, textview->msgfont, NULL, NULL, str, -1)
394
395 void textview_show_mime_part(TextView *textview, MimeInfo *partinfo)
396 {
397         GtkText *text;
398
399         if (!partinfo) return;
400
401         textview_set_font(textview, NULL);
402         text = GTK_TEXT(textview->text);
403         textview_clear(textview);
404
405         gtk_text_freeze(text);
406
407         TEXT_INSERT(_("To save this part, pop up the context menu with "));
408         TEXT_INSERT(_("right click and select `Save as...', "));
409         TEXT_INSERT(_("or press `y' key.\n\n"));
410
411         TEXT_INSERT(_("To display this part as a text message, select "));
412         TEXT_INSERT(_("`Display as text', or press `t' key.\n\n"));
413
414         TEXT_INSERT(_("To open this part with external program, select "));
415         TEXT_INSERT(_("`Open' or `Open with...', "));
416         TEXT_INSERT(_("or double-click, or click the center button, "));
417         TEXT_INSERT(_("or press `l' key."));
418
419         gtk_text_thaw(text);
420 }
421
422 #if USE_GPGME
423 void textview_show_signature_part(TextView *textview, MimeInfo *partinfo)
424 {
425         GtkText *text;
426
427         if (!partinfo) return;
428
429         textview_set_font(textview, NULL);
430         text = GTK_TEXT(textview->text);
431         textview_clear(textview);
432
433         gtk_text_freeze(text);
434
435         if (partinfo->sigstatus_full == NULL) {
436                 TEXT_INSERT(_("This signature has not been checked yet.\n"));
437                 TEXT_INSERT(_("To check it, pop up the context menu with\n"));
438                 TEXT_INSERT(_("right click and select `Check signature'.\n"));
439         } else {
440                 TEXT_INSERT(partinfo->sigstatus_full);
441         }
442
443         gtk_text_thaw(text);
444 }
445 #endif /* USE_GPGME */
446
447 #undef TEXT_INSERT
448
449 static void textview_show_html(TextView *textview, FILE *fp,
450                                CodeConverter *conv)
451 {
452         HTMLParser *parser;
453         gchar *str;
454         gchar* url = NULL;
455
456         parser = html_parser_new(fp, conv);
457         g_return_if_fail(parser != NULL);
458
459         while ((str = html_parse(parser)) != NULL) {
460                 if (parser->state == HTML_HREF) {
461                         /* first time : get and copy the URL */
462                         if (url == NULL) {
463                                 /* ALF - the sylpheed html parser returns an empty string,
464                                  * if still inside an <a>, but already parsed past HREF */
465                                 str = strtok(str, " ");
466                                 if (str) { 
467                                         url = strdup(str);
468                                         /* the URL may (or not) be followed by the
469                                          * referenced text */
470                                         str = strtok(NULL, "");
471                                 }       
472                         }
473                         if (str != NULL) {
474                                 textview_write_link(textview, url, str, NULL);
475                         }
476                 } else {
477                         if (url != NULL) {
478                                 free(url);
479                                 url = NULL;
480                         }
481                         textview_write_line(textview, str, NULL);
482                 }
483         }
484         html_parser_destroy(parser);
485 }
486
487 static void textview_show_ertf(TextView *textview, FILE *fp,
488                                CodeConverter *conv)
489 {
490         ERTFParser *parser;
491         gchar *str;
492         gchar* url = NULL;
493
494         parser = ertf_parser_new(fp, conv);
495         g_return_if_fail(parser != NULL);
496
497         while ((str = ertf_parse(parser)) != NULL) {
498                 textview_write_line(textview, str, NULL);
499         }
500         ertf_parser_destroy(parser);
501 }
502
503 /* get_uri_part() - retrieves a URI starting from scanpos.
504                     Returns TRUE if succesful */
505 static gboolean get_uri_part(const gchar *start, const gchar *scanpos,
506                              const gchar **bp, const gchar **ep)
507 {
508         const gchar *ep_;
509
510         g_return_val_if_fail(start != NULL, FALSE);
511         g_return_val_if_fail(scanpos != NULL, FALSE);
512         g_return_val_if_fail(bp != NULL, FALSE);
513         g_return_val_if_fail(ep != NULL, FALSE);
514
515         *bp = scanpos;
516
517         /* find end point of URI */
518         for (ep_ = scanpos; *ep_ != '\0'; ep_++) {
519                 if (!isgraph(*ep_) || !isascii(*ep_) || strchr("()<>\"", *ep_))
520                         break;
521         }
522
523         /* no punctuation at end of string */
524
525         /* FIXME: this stripping of trailing punctuations may bite with other URIs.
526          * should pass some URI type to this function and decide on that whether
527          * to perform punctuation stripping */
528
529 #define IS_REAL_PUNCT(ch)       (ispunct(ch) && ((ch) != '/')) 
530
531         for (; ep_ - 1 > scanpos + 1 && IS_REAL_PUNCT(*(ep_ - 1)); ep_--)
532                 ;
533
534 #undef IS_REAL_PUNCT
535
536         *ep = ep_;
537
538         return TRUE;            
539 }
540
541 static gchar *make_uri_string(const gchar *bp, const gchar *ep)
542 {
543         return g_strndup(bp, ep - bp);
544 }
545
546 /* valid mail address characters */
547 #define IS_RFC822_CHAR(ch) \
548         (isascii(ch) && \
549          (ch) > 32   && \
550          (ch) != 127 && \
551          !isspace(ch) && \
552          !strchr("()<>\"", (ch)))
553
554 /* alphabet and number within 7bit ASCII */
555 #define IS_ASCII_ALNUM(ch)      (isascii(ch) && isalnum(ch))
556 #define IS_QUOTE(ch) ((ch) == '\'' || (ch) == '"')
557
558 /* get_email_part() - retrieves an email address. Returns TRUE if succesful */
559 static gboolean get_email_part(const gchar *start, const gchar *scanpos,
560                                const gchar **bp, const gchar **ep)
561 {
562         /* more complex than the uri part because we need to scan back and forward starting from
563          * the scan position. */
564         gboolean result = FALSE;
565         const gchar *bp_ = NULL;
566         const gchar *ep_ = NULL;
567
568         /* the informative part of the email address (describing the name
569          * of the email address owner) may contain quoted parts. the
570          * closure stack stores the last encountered quotes. */
571         gchar closure_stack[128];
572         gchar *ptr = closure_stack;
573
574         g_return_val_if_fail(start != NULL, FALSE);
575         g_return_val_if_fail(scanpos != NULL, FALSE);
576         g_return_val_if_fail(bp != NULL, FALSE);
577         g_return_val_if_fail(ep != NULL, FALSE);
578
579         /* scan start of address */
580         for (bp_ = scanpos - 1; bp_ >= start && IS_RFC822_CHAR(*bp_); bp_--)
581                 ;
582
583         /* TODO: should start with an alnum? */
584         bp_++;
585         for (; bp_ < scanpos && !IS_ASCII_ALNUM(*bp_); bp_++)
586                 ;
587
588         if (bp_ != scanpos) {
589                 /* scan end of address */
590                 for (ep_ = scanpos + 1; *ep_ && IS_RFC822_CHAR(*ep_); ep_++)
591                         ;
592
593                 /* TODO: really should terminate with an alnum? */
594                 for (; ep_ > scanpos && !IS_ASCII_ALNUM(*ep_); --ep_)
595                         ;
596                 ep_++;
597
598                 if (ep_ > scanpos + 1) {
599                         *ep = ep_;
600                         *bp = bp_;
601                         result = TRUE;
602                 }
603         }
604
605         if (!result) return FALSE;
606
607         /* skip if it's between quotes "'alfons@proteus.demon.nl'" <alfons@proteus.demon.nl> */
608         if (bp_ - 1 > start && IS_QUOTE(*(bp_ - 1)) && IS_QUOTE(*ep_)) 
609                 return FALSE;
610
611         /* see if this is <bracketed>; in this case we also scan for the informative part. */
612         if (bp_ - 1 <= start || *(bp_ - 1) != '<' || *ep_ != '>')
613                 return TRUE;
614
615 #define FULL_STACK()    ((size_t) (ptr - closure_stack) >= sizeof closure_stack)
616 #define IN_STACK()      (ptr > closure_stack)
617 /* has underrun check */
618 #define POP_STACK()     if(IN_STACK()) --ptr
619 /* has overrun check */
620 #define PUSH_STACK(c)   if(!FULL_STACK()) *ptr++ = (c); else return TRUE
621 /* has underrun check */
622 #define PEEK_STACK()    (IN_STACK() ? *(ptr - 1) : 0)
623
624         ep_++;
625
626         /* scan for the informative part. */
627         for (bp_ -= 2; bp_ >= start; bp_--) {
628                 /* if closure on the stack keep scanning */
629                 if (PEEK_STACK() == *bp_) {
630                         POP_STACK();
631                         continue;
632                 }
633                 if (*bp_ == '\'' || *bp_ == '"') {
634                         PUSH_STACK(*bp_);
635                         continue;
636                 }
637
638                 /* if nothing in the closure stack, do the special conditions
639                  * the following if..else expression simply checks whether 
640                  * a token is acceptable. if not acceptable, the clause
641                  * should terminate the loop with a 'break' */
642                 if (!PEEK_STACK()) {
643                         if (*bp_ == '-'
644                         && (((bp_ - 1) >= start) && isalnum(*(bp_ - 1)))
645                         && (((bp_ + 1) < ep_)    && isalnum(*(bp_ + 1)))) {
646                                 /* hyphens are allowed, but only in
647                                    between alnums */
648                         } else if (!ispunct(*bp_)) {
649                                 /* but anything not being a punctiation
650                                    is ok */
651                         } else {
652                                 break; /* anything else is rejected */
653                         }
654                 }
655         }
656
657         bp_++;
658
659 #undef PEEK_STACK
660 #undef PUSH_STACK
661 #undef POP_STACK
662 #undef IN_STACK
663 #undef FULL_STACK
664
665         /* scan forward (should start with an alnum) */
666         for (; *bp_ != '<' && isspace(*bp_) && *bp_ != '"'; bp_++)
667                 ;
668
669         *ep = ep_;
670         *bp = bp_;
671
672         return result;
673 }
674
675 #undef IS_QUOTE
676 #undef IS_RFC822_CHAR
677
678 static gchar *make_email_string(const gchar *bp, const gchar *ep)
679 {
680         /* returns a mailto: URI; mailto: is also used to detect the
681          * uri type later on in the button_pressed signal handler */
682         gchar *tmp;
683         gchar *result;
684
685         tmp = g_strndup(bp, ep - bp);
686         result = g_strconcat("mailto:", tmp, NULL);
687         g_free(tmp);
688
689         return result;
690 }
691
692 #define ADD_TXT_POS(bp_, ep_, pti_) \
693         if ((last->next = alloca(sizeof(struct txtpos))) != NULL) { \
694                 last = last->next; \
695                 last->bp = (bp_); last->ep = (ep_); last->pti = (pti_); \
696                 last->next = NULL; \
697         } else { \
698                 g_warning("alloc error scanning URIs\n"); \
699                 gtk_text_insert(text, textview->msgfont, fg_color, NULL, \
700                                 linebuf, -1); \
701                 return; \
702         }
703
704 /* textview_make_clickable_parts() - colorizes clickable parts */
705 static void textview_make_clickable_parts(TextView *textview,
706                                           GdkFont *font,
707                                           GdkColor *fg_color,
708                                           GdkColor *uri_color,
709                                           const gchar *linebuf)
710 {
711         /* parse table - in order of priority */
712         struct table {
713                 const gchar *needle; /* token */
714
715                 /* token search function */
716                 gchar    *(*search)     (const gchar *haystack,
717                                          const gchar *needle);
718                 /* part parsing function */
719                 gboolean  (*parse)      (const gchar *start,
720                                          const gchar *scanpos,
721                                          const gchar **bp_,
722                                          const gchar **ep_);
723                 /* part to URI function */
724                 gchar    *(*build_uri)  (const gchar *bp,
725                                          const gchar *ep);
726         };
727
728         static struct table parser[] = {
729                 {"http://",  strcasestr, get_uri_part,   make_uri_string},
730                 {"https://", strcasestr, get_uri_part,   make_uri_string},
731                 {"ftp://",   strcasestr, get_uri_part,   make_uri_string},
732                 {"mailto:",  strcasestr, get_uri_part,   make_uri_string},
733                 {"@",        strcasestr, get_email_part, make_email_string}
734         };
735         const gint PARSE_ELEMS = sizeof parser / sizeof parser[0];
736
737         gint  n;
738         const gchar *walk, *bp, *ep;
739
740         struct txtpos {
741                 const gchar     *bp, *ep;       /* text position */
742                 gint             pti;           /* index in parse table */
743                 struct txtpos   *next;          /* next */
744         } head = {NULL, NULL, 0,  NULL}, *last = &head;
745
746         GtkText *text = GTK_TEXT(textview->text);
747
748         /* parse for clickable parts, and build a list of begin and end positions  */
749         for (walk = linebuf, n = 0;;) {
750                 gint last_index = PARSE_ELEMS;
751                 gchar *scanpos = NULL;
752
753                 /* FIXME: this looks phony. scanning for anything in the parse table */
754                 for (n = 0; n < PARSE_ELEMS; n++) {
755                         gchar *tmp;
756
757                         tmp = parser[n].search(walk, parser[n].needle);
758                         if (tmp) {
759                                 if (scanpos == NULL || tmp < scanpos) {
760                                         scanpos = tmp;
761                                         last_index = n;
762                                 }
763                         }                                       
764                 }
765
766                 if (scanpos) {
767                         /* check if URI can be parsed */
768                         if (parser[last_index].parse(linebuf, scanpos, &bp, &ep)
769                             && (size_t) (ep - bp - 1) > strlen(parser[last_index].needle)) {
770                                         ADD_TXT_POS(bp, ep, last_index);
771                                         walk = ep;
772                         } else
773                                 walk = scanpos +
774                                         strlen(parser[last_index].needle);
775                 } else
776                         break;
777         }
778
779         /* colorize this line */
780         if (head.next) {
781                 const gchar *normal_text = linebuf;
782
783                 /* insert URIs */
784                 for (last = head.next; last != NULL;
785                      normal_text = last->ep, last = last->next) {
786                         RemoteURI *uri;
787
788                         uri = g_new(RemoteURI, 1);
789                         if (last->bp - normal_text > 0)
790                                 gtk_text_insert(text, font,
791                                                 fg_color, NULL,
792                                                 normal_text,
793                                                 last->bp - normal_text);
794                         uri->uri = parser[last->pti].build_uri(last->bp, 
795                                                                last->ep);
796                         uri->start = gtk_text_get_point(text);
797                         gtk_text_insert(text, font, uri_color,
798                                         NULL, last->bp, last->ep - last->bp);
799                         uri->end = gtk_text_get_point(text);
800                         textview->uri_list =
801                                 g_slist_append(textview->uri_list, uri);
802                 }
803
804                 if (*normal_text)
805                         gtk_text_insert(text, font, fg_color,
806                                         NULL, normal_text, -1);
807         } else
808                 gtk_text_insert(text, font, fg_color, NULL, linebuf, -1);
809 }
810
811 #undef ADD_TXT_POS
812
813 /* This function writes str as a double-clickable link with the given url. */ 
814 static void textview_write_link(TextView *textview, const gchar *url,
815                                 const gchar *str, CodeConverter *conv)
816 {
817     GdkColor *link_color = NULL;
818     RemoteURI* uri;
819     GtkText *text = GTK_TEXT(textview->text);
820     gchar buf[BUFFSIZE];
821
822     /* this part is taken from textview_write_line. Right now the only place
823      * that calls this function passes NULL for conv, but you never know. */
824     if (!conv)
825             strncpy2(buf, str, sizeof(buf));
826     else if (conv_convert(conv, buf, sizeof(buf), str) < 0) {
827             gtk_text_insert(text, textview->msgfont,
828                             prefs_common.enable_color
829                             ? &error_color : NULL, NULL,
830                             "*** Warning: code conversion failed ***\n",
831                             -1);
832             return;
833     }
834
835     /* this part is based on the code in make_clickable_parts */
836     if (prefs_common.enable_color) {
837         link_color = &uri_color;
838     }
839     uri = g_new(RemoteURI, 1);
840     uri->uri = g_strdup(url);
841     uri->start = gtk_text_get_point(text);
842     gtk_text_insert(text, textview->msgfont, link_color, NULL, buf,
843                     strlen(buf));
844     uri->end = gtk_text_get_point(text);
845     textview->uri_list = g_slist_append(textview->uri_list, uri);
846 }
847
848 static void textview_write_line(TextView *textview, const gchar *str,
849                                 CodeConverter *conv)
850 {
851         GtkText *text = GTK_TEXT(textview->text);
852         gchar buf[BUFFSIZE];
853         GdkColor *fg_color;
854         gint quotelevel = -1;
855
856         if (!conv)
857                 strncpy2(buf, str, sizeof(buf));
858         else if (conv_convert(conv, buf, sizeof(buf), str) < 0) {
859                 gtk_text_insert(text, textview->msgfont,
860                                 prefs_common.enable_color
861                                 ? &error_color : NULL, NULL,
862                                 "*** Warning: code conversion failed ***\n",
863                                 -1);
864                 return;
865         }
866
867         strcrchomp(buf);
868         if (prefs_common.conv_mb_alnum) conv_mb_alnum(buf);
869         fg_color = NULL;
870
871         /* change color of quotation
872            >, foo>, _> ... ok, <foo>, foo bar>, foo-> ... ng
873            Up to 3 levels of quotations are detected, and each
874            level is colored using a different color. */
875         if (prefs_common.enable_color && strchr(buf, '>')) {
876                 quotelevel = get_quote_level(buf);
877
878                 /* set up the correct foreground color */
879                 if (quotelevel > 2) {
880                         /* recycle colors */
881                         if (prefs_common.recycle_quote_colors)
882                                 quotelevel %= 3;
883                         else
884                                 quotelevel = 2;
885                 }
886         }
887
888         if (quotelevel == -1)
889                 fg_color = NULL;
890         else
891                 fg_color = &quote_colors[quotelevel];
892
893         if (prefs_common.head_space && spacingfont && buf[0] != '\n')
894                 gtk_text_insert(text, spacingfont, NULL, NULL, " ", 1);
895
896         if (prefs_common.enable_color)
897                 textview_make_clickable_parts(textview, textview->msgfont,
898                                               fg_color, &uri_color, buf);
899         else
900                 textview_make_clickable_parts(textview, textview->msgfont,
901                                               fg_color, NULL, buf);
902 }
903
904 void textview_clear(TextView *textview)
905 {
906         GtkText *text = GTK_TEXT(textview->text);
907
908         gtk_text_freeze(text);
909         gtk_text_set_point(text, 0);
910         gtk_text_forward_delete(text, gtk_text_get_length(text));
911         gtk_text_thaw(text);
912
913         textview_uri_list_remove_all(textview->uri_list);
914         textview->uri_list = NULL;
915 }
916
917 void textview_destroy(TextView *textview)
918 {
919         textview_uri_list_remove_all(textview->uri_list);
920         textview->uri_list = NULL;
921
922         if (!textview->scrolledwin_sb->parent)
923                 gtk_widget_destroy(textview->scrolledwin_sb);
924         if (!textview->scrolledwin_mb->parent)
925                 gtk_widget_destroy(textview->scrolledwin_mb);
926
927         if (textview->msgfont) {
928                 textview->msgfont->ascent = textview->prev_ascent;
929                 textview->msgfont->descent = textview->prev_descent;
930                 gdk_font_unref(textview->msgfont);
931         }
932         if (textview->boldfont)
933                 gdk_font_unref(textview->boldfont);
934
935         g_free(textview);
936 }
937
938 void textview_set_font(TextView *textview, const gchar *codeset)
939 {
940         gboolean use_fontset = TRUE;
941
942         /* In multi-byte mode, GtkText can't display 8bit characters
943            correctly, so it must be single-byte mode. */
944         if (MB_CUR_MAX > 1) {
945                 if (codeset) {
946                         if (!g_strncasecmp(codeset, "ISO-8859-", 9) ||
947                             !g_strcasecmp(codeset, "BALTIC"))
948                                 use_fontset = FALSE;
949                         else if (conv_get_current_charset() != C_EUC_JP &&
950                                  (!g_strncasecmp(codeset, "KOI8-", 5) ||
951                                   !g_strncasecmp(codeset, "CP", 2)    ||
952                                   !g_strncasecmp(codeset, "WINDOWS-", 8)))
953                                 use_fontset = FALSE;
954                 }
955         } else
956                 use_fontset = FALSE;
957
958         if (textview->text_is_mb && !use_fontset) {
959                 GtkWidget *parent;
960
961                 parent = textview->scrolledwin_mb->parent;
962                 gtkut_container_remove(GTK_CONTAINER(parent),
963                                        textview->scrolledwin_mb);
964                 gtk_container_add(GTK_CONTAINER(parent),
965                                   textview->scrolledwin_sb);
966
967                 textview->text = textview->text_sb;
968                 textview->text_is_mb = FALSE;
969         } else if (!textview->text_is_mb && use_fontset) {
970                 GtkWidget *parent;
971
972                 parent = textview->scrolledwin_sb->parent;
973                 gtkut_container_remove(GTK_CONTAINER(parent),
974                                        textview->scrolledwin_sb);
975                 gtk_container_add(GTK_CONTAINER(parent),
976                                   textview->scrolledwin_mb);
977
978                 textview->text = textview->text_mb;
979                 textview->text_is_mb = TRUE;
980         }
981
982         if (prefs_common.textfont) {
983                 if (textview->msgfont) {
984                         textview->msgfont->ascent = textview->prev_ascent;
985                         textview->msgfont->descent = textview->prev_descent;
986                         gdk_font_unref(textview->msgfont);
987                         textview->msgfont = NULL;
988                 }
989                 if (use_fontset)
990                         textview->msgfont =
991                                 gdk_fontset_load(prefs_common.textfont);
992                 else {
993                         if (MB_CUR_MAX > 1) {
994                                 FONT_LOAD(textview->msgfont,
995                                           "-*-courier-medium-r-normal--14-*-*-*-*-*-iso8859-1");
996                         } else {
997                                 FONT_LOAD(textview->msgfont,
998                                           prefs_common.textfont);
999                         }
1000                 }
1001
1002                 if (textview->msgfont) {
1003                         gint ascent, descent;
1004
1005                         textview->prev_ascent = textview->msgfont->ascent;
1006                         textview->prev_descent = textview->msgfont->descent;
1007                         descent = prefs_common.line_space / 2;
1008                         ascent  = prefs_common.line_space - descent;
1009                         textview->msgfont->ascent  += ascent;
1010                         textview->msgfont->descent += descent;
1011                 }
1012         }
1013
1014         if (!textview->boldfont)
1015                 FONT_LOAD(textview->boldfont, BOLD_FONT);
1016         if (!spacingfont)
1017                 spacingfont = gdk_font_load("-*-*-medium-r-normal--6-*");
1018 }
1019
1020 enum
1021 {
1022         H_DATE          = 0,
1023         H_FROM          = 1,
1024         H_TO            = 2,
1025         H_NEWSGROUPS    = 3,
1026         H_SUBJECT       = 4,
1027         H_CC            = 5,
1028         H_REPLY_TO      = 6,
1029         H_FOLLOWUP_TO   = 7,
1030         H_X_MAILER      = 8,
1031         H_X_NEWSREADER  = 9,
1032         H_USER_AGENT    = 10,
1033         H_ORGANIZATION  = 11,
1034 };
1035
1036 void textview_set_position(TextView *textview, gint pos)
1037 {
1038         if (pos < 0) {
1039                 textview->cur_pos =
1040                         gtk_text_get_length(GTK_TEXT(textview->text));
1041         } else {
1042                 textview->cur_pos = pos;
1043         }
1044 }
1045
1046 static GPtrArray *textview_scan_header(TextView *textview, FILE *fp)
1047 {
1048         gchar buf[BUFFSIZE];
1049         GPtrArray *headers, *sorted_headers;
1050         GSList *disphdr_list;
1051         Header *header;
1052         gint i;
1053
1054         g_return_val_if_fail(fp != NULL, NULL);
1055
1056         if (!prefs_common.display_header) {
1057                 while (fgets(buf, sizeof(buf), fp) != NULL)
1058                         if (buf[0] == '\r' || buf[0] == '\n') break;
1059                 return NULL;
1060         }
1061
1062         headers = procheader_get_header_array_asis(fp);
1063
1064         sorted_headers = g_ptr_array_new();
1065
1066         for (disphdr_list = prefs_common.disphdr_list; disphdr_list != NULL;
1067              disphdr_list = disphdr_list->next) {
1068                 DisplayHeaderProp *dp =
1069                         (DisplayHeaderProp *)disphdr_list->data;
1070
1071                 for (i = 0; i < headers->len; i++) {
1072                         header = g_ptr_array_index(headers, i);
1073
1074                         if (procheader_headername_equal(header->name,
1075                                                         dp->name)) {
1076                                 if (dp->hidden)
1077                                         procheader_header_free(header);
1078                                 else
1079                                         g_ptr_array_add(sorted_headers, header);
1080
1081                                 g_ptr_array_remove_index(headers, i);
1082                                 i--;
1083                         }
1084                 }
1085         }
1086
1087         if (prefs_common.show_other_header) {
1088                 for (i = 0; i < headers->len; i++) {
1089                         header = g_ptr_array_index(headers, i);
1090                         g_ptr_array_add(sorted_headers, header);
1091                 }
1092         }
1093
1094         g_ptr_array_free(headers, FALSE);
1095
1096         return sorted_headers;
1097 }
1098
1099 static void textview_show_header(TextView *textview, GPtrArray *headers)
1100 {
1101         GtkText *text = GTK_TEXT(textview->text);
1102         Header *header;
1103         gint i;
1104
1105         g_return_if_fail(headers != NULL);
1106
1107         gtk_text_freeze(text);
1108
1109         for (i = 0; i < headers->len; i++) {
1110                 header = g_ptr_array_index(headers, i);
1111                 g_return_if_fail(header->name != NULL);
1112
1113                 gtk_text_insert(text, textview->boldfont, NULL, NULL,
1114                                 header->name, -1);
1115                 if (header->name[strlen(header->name) - 1] != ' ')
1116                         gtk_text_insert(text, textview->boldfont,
1117                                         NULL, NULL, " ", 1);
1118
1119                 if (procheader_headername_equal(header->name, "Subject") ||
1120                     procheader_headername_equal(header->name, "From")    ||
1121                     procheader_headername_equal(header->name, "To")      ||
1122                     procheader_headername_equal(header->name, "Cc"))
1123                         unfold_line(header->body);
1124
1125                 if (prefs_common.enable_color &&
1126                     (procheader_headername_equal(header->name, "X-Mailer") ||
1127                      procheader_headername_equal(header->name,
1128                                                  "X-Newsreader")) &&
1129                     strstr(header->body, "Sylpheed") != NULL)
1130                         gtk_text_insert(text, NULL, &emphasis_color, NULL,
1131                                         header->body, -1);
1132                 else if (prefs_common.enable_color) {
1133                         textview_make_clickable_parts(textview,
1134                                                       NULL, NULL, &uri_color,
1135                                                       header->body);
1136                 } else {
1137                         textview_make_clickable_parts(textview,
1138                                                       NULL, NULL, NULL,
1139                                                       header->body);
1140                 }
1141                 gtk_text_insert(text, textview->msgfont, NULL, NULL, "\n", 1);
1142         }
1143
1144         gtk_text_insert(text, textview->msgfont, NULL, NULL, "\n", 1);
1145         gtk_text_thaw(text);
1146         textview->body_pos = gtk_text_get_length(text);
1147 }
1148
1149 gboolean textview_search_string(TextView *textview, const gchar *str,
1150                                 gboolean case_sens)
1151 {
1152         GtkText *text = GTK_TEXT(textview->text);
1153         gint pos;
1154         wchar_t *wcs;
1155         gint len;
1156         gint text_len;
1157         gboolean found = FALSE;
1158
1159         g_return_val_if_fail(str != NULL, FALSE);
1160
1161         wcs = strdup_mbstowcs(str);
1162         g_return_val_if_fail(wcs != NULL, FALSE);
1163         len = wcslen(wcs);
1164         pos = textview->cur_pos;
1165         if (pos < textview->body_pos)
1166                 pos = textview->body_pos;
1167         text_len = gtk_text_get_length(text);
1168         if (text_len - pos < len) {
1169                 g_free(wcs);
1170                 return FALSE;
1171         }
1172
1173         for (; pos < text_len; pos++) {
1174                 if (text_len - pos < len) break;
1175                 if (gtkut_text_match_string(GTK_TEXT(text), pos, wcs, len, case_sens)
1176                     == TRUE) {
1177                         gtk_editable_set_position(GTK_EDITABLE(text),
1178                                                   pos + len);
1179                         gtk_editable_select_region(GTK_EDITABLE(text),
1180                                                    pos, pos + len);
1181                         textview_set_position(textview, pos + len);
1182                         found = TRUE;
1183                         break;
1184                 }
1185                 if (text_len - pos == len) break;
1186         }
1187
1188         g_free(wcs);
1189         return found;
1190 }
1191
1192 gboolean textview_search_string_backward(TextView *textview, const gchar *str,
1193                                          gboolean case_sens)
1194 {
1195         GtkText *text = GTK_TEXT(textview->text);
1196         gint pos;
1197         wchar_t *wcs;
1198         gint len;
1199         gint text_len;
1200         gboolean found = FALSE;
1201
1202         g_return_val_if_fail(str != NULL, FALSE);
1203
1204         wcs = strdup_mbstowcs(str);
1205         g_return_val_if_fail(wcs != NULL, FALSE);
1206         len = wcslen(wcs);
1207         pos = textview->cur_pos;
1208         text_len = gtk_text_get_length(text);
1209         if (text_len - textview->body_pos < len) {
1210                 g_free(wcs);
1211                 return FALSE;
1212         }
1213         if (pos <= textview->body_pos || text_len - pos < len)
1214                 pos = text_len - len;
1215
1216         for (; pos >= textview->body_pos; pos--) {
1217                 if (gtkut_text_match_string(GTK_TEXT(text), pos, wcs, len, case_sens)
1218                     == TRUE) {
1219                         gtk_editable_set_position(GTK_EDITABLE(text), pos);
1220                         gtk_editable_select_region(GTK_EDITABLE(text),
1221                                                    pos, pos + len);
1222                         textview_set_position(textview, pos - 1);
1223                         found = TRUE;
1224                         break;
1225                 }
1226                 if (pos == textview->body_pos) break;
1227         }
1228
1229         g_free(wcs);
1230         return found;
1231 }
1232
1233 void textview_scroll_one_line(TextView *textview, gboolean up)
1234 {
1235         GtkText *text = GTK_TEXT(textview->text);
1236         gfloat upper;
1237
1238         if (prefs_common.enable_smooth_scroll) {
1239                 textview_smooth_scroll_one_line(textview, up);
1240                 return;
1241         }
1242
1243         if (!up) {
1244                 upper = text->vadj->upper - text->vadj->page_size;
1245                 if (text->vadj->value < upper) {
1246                         text->vadj->value +=
1247                                 text->vadj->step_increment * 4;
1248                         text->vadj->value =
1249                                 MIN(text->vadj->value, upper);
1250                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1251                                                 "value_changed");
1252                 }
1253         } else {
1254                 if (text->vadj->value > 0.0) {
1255                         text->vadj->value -=
1256                                 text->vadj->step_increment * 4;
1257                         text->vadj->value =
1258                                 MAX(text->vadj->value, 0.0);
1259                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1260                                                 "value_changed");
1261                 }
1262         }
1263 }
1264
1265 gboolean textview_scroll_page(TextView *textview, gboolean up)
1266 {
1267         GtkText *text = GTK_TEXT(textview->text);
1268         gfloat upper;
1269         gfloat page_incr;
1270
1271         if (prefs_common.enable_smooth_scroll)
1272                 return textview_smooth_scroll_page(textview, up);
1273
1274         if (prefs_common.scroll_halfpage)
1275                 page_incr = text->vadj->page_increment / 2;
1276         else
1277                 page_incr = text->vadj->page_increment;
1278
1279         if (!up) {
1280                 upper = text->vadj->upper - text->vadj->page_size;
1281                 if (text->vadj->value < upper) {
1282                         text->vadj->value += page_incr;
1283                         text->vadj->value = MIN(text->vadj->value, upper);
1284                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1285                                                 "value_changed");
1286                 } else
1287                         return FALSE;
1288         } else {
1289                 if (text->vadj->value > 0.0) {
1290                         text->vadj->value -= page_incr;
1291                         text->vadj->value = MAX(text->vadj->value, 0.0);
1292                         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1293                                                 "value_changed");
1294                 } else
1295                         return FALSE;
1296         }
1297
1298         return TRUE;
1299 }
1300
1301 static void textview_smooth_scroll_do(TextView *textview,
1302                                       gfloat old_value, gfloat last_value,
1303                                       gint step)
1304 {
1305         GtkText *text = GTK_TEXT(textview->text);
1306         gint change_value;
1307         gboolean up;
1308         gint i;
1309
1310         if (old_value < last_value) {
1311                 change_value = last_value - old_value;
1312                 up = FALSE;
1313         } else {
1314                 change_value = old_value - last_value;
1315                 up = TRUE;
1316         }
1317
1318         gdk_key_repeat_disable();
1319
1320         for (i = step; i <= change_value; i += step) {
1321                 text->vadj->value = old_value + (up ? -i : i);
1322                 gtk_signal_emit_by_name(GTK_OBJECT(text->vadj),
1323                                         "value_changed");
1324         }
1325
1326         text->vadj->value = last_value;
1327         gtk_signal_emit_by_name(GTK_OBJECT(text->vadj), "value_changed");
1328
1329         gdk_key_repeat_restore();
1330 }
1331
1332 static void textview_smooth_scroll_one_line(TextView *textview, gboolean up)
1333 {
1334         GtkText *text = GTK_TEXT(textview->text);
1335         gfloat upper;
1336         gfloat old_value;
1337         gfloat last_value;
1338
1339         if (!up) {
1340                 upper = text->vadj->upper - text->vadj->page_size;
1341                 if (text->vadj->value < upper) {
1342                         old_value = text->vadj->value;
1343                         last_value = text->vadj->value +
1344                                 text->vadj->step_increment * 4;
1345                         last_value = MIN(last_value, upper);
1346
1347                         textview_smooth_scroll_do(textview, old_value,
1348                                                   last_value,
1349                                                   prefs_common.scroll_step);
1350                 }
1351         } else {
1352                 if (text->vadj->value > 0.0) {
1353                         old_value = text->vadj->value;
1354                         last_value = text->vadj->value -
1355                                 text->vadj->step_increment * 4;
1356                         last_value = MAX(last_value, 0.0);
1357
1358                         textview_smooth_scroll_do(textview, old_value,
1359                                                   last_value,
1360                                                   prefs_common.scroll_step);
1361                 }
1362         }
1363 }
1364
1365 static gboolean textview_smooth_scroll_page(TextView *textview, gboolean up)
1366 {
1367         GtkText *text = GTK_TEXT(textview->text);
1368         gfloat upper;
1369         gfloat page_incr;
1370         gfloat old_value;
1371         gfloat last_value;
1372
1373         if (prefs_common.scroll_halfpage)
1374                 page_incr = text->vadj->page_increment / 2;
1375         else
1376                 page_incr = text->vadj->page_increment;
1377
1378         if (!up) {
1379                 upper = text->vadj->upper - text->vadj->page_size;
1380                 if (text->vadj->value < upper) {
1381                         old_value = text->vadj->value;
1382                         last_value = text->vadj->value + page_incr;
1383                         last_value = MIN(last_value, upper);
1384
1385                         textview_smooth_scroll_do(textview, old_value,
1386                                                   last_value,
1387                                                   prefs_common.scroll_step);
1388                 } else
1389                         return FALSE;
1390         } else {
1391                 if (text->vadj->value > 0.0) {
1392                         old_value = text->vadj->value;
1393                         last_value = text->vadj->value - page_incr;
1394                         last_value = MAX(last_value, 0.0);
1395
1396                         textview_smooth_scroll_do(textview, old_value,
1397                                                   last_value,
1398                                                   prefs_common.scroll_step);
1399                 } else
1400                         return FALSE;
1401         }
1402
1403         return TRUE;
1404 }
1405
1406 static void textview_key_pressed(GtkWidget *widget, GdkEventKey *event,
1407                                  TextView *textview)
1408 {
1409         SummaryView *summaryview = NULL;
1410
1411         if (!event) return;
1412         if (textview->messageview->mainwin)
1413                 summaryview = textview->messageview->mainwin->summaryview;
1414
1415         switch (event->keyval) {
1416         case GDK_Tab:
1417         case GDK_Home:
1418         case GDK_Left:
1419         case GDK_Up:
1420         case GDK_Right:
1421         case GDK_Down:
1422         case GDK_Page_Up:
1423         case GDK_Page_Down:
1424         case GDK_End:
1425         case GDK_Control_L:
1426         case GDK_Control_R:
1427                 break;
1428         case GDK_space:
1429                 if (summaryview)
1430                         summary_pass_key_press_event(summaryview, event);
1431                 else
1432                         textview_scroll_page(textview, FALSE);
1433                 break;
1434         case GDK_BackSpace:
1435                 textview_scroll_page(textview, TRUE);
1436                 break;
1437         case GDK_Return:
1438                 textview_scroll_one_line(textview,
1439                                          (event->state & GDK_MOD1_MASK) != 0);
1440                 break;
1441         default:
1442                 if (summaryview)
1443                         summary_pass_key_press_event(summaryview, event);
1444                 break;
1445         }
1446 }
1447
1448 static gint textview_button_pressed(GtkWidget *widget, GdkEventButton *event,
1449                                     TextView *textview)
1450 {
1451         if (event)
1452                 textview->last_buttonpress = event->type;
1453         return FALSE;
1454 }
1455
1456 static gint textview_button_released(GtkWidget *widget, GdkEventButton *event,
1457                                     TextView *textview)
1458 {
1459         textview->cur_pos = 
1460                 gtk_editable_get_position(GTK_EDITABLE(textview->text));
1461
1462         if (event && 
1463             ((event->button == 1 && textview->last_buttonpress == GDK_2BUTTON_PRESS)
1464              || event->button == 2 || event->button == 3)) {
1465                 GSList *cur;
1466
1467                 /* double click seems to set the cursor after the current
1468                  * word. The cursor position needs fixing, otherwise the
1469                  * last word of a clickable zone will not work */
1470                 if (event->button == 1 && textview->last_buttonpress == GDK_2BUTTON_PRESS) {
1471                         textview->cur_pos--;
1472                 }
1473
1474                 for (cur = textview->uri_list; cur != NULL; cur = cur->next) {
1475                         RemoteURI *uri = (RemoteURI *)cur->data;
1476
1477                         if (textview->cur_pos >= uri->start &&
1478                             textview->cur_pos <  uri->end) {
1479                                 if (!g_strncasecmp(uri->uri, "mailto:", 7)) {
1480                                         if (event->button == 3) {
1481                                                 gchar *fromname, *fromaddress;
1482                                                 GdkEventButton tmpev;   
1483                                                 
1484                                                 /* extract url */
1485                                                 fromaddress = g_strdup(uri->uri + 7);
1486                                                 /* Hiroyuki: please put this function in utils.c! */
1487                                                 fromname = procheader_get_fromname(fromaddress);
1488                                                 extract_address(fromaddress);
1489                                                 g_message("adding from textview %s <%s>", fromname, fromaddress);
1490                                                 /* Add to address book - Match */
1491                                                 addressbook_add_contact( fromname, fromaddress, NULL );
1492                                                 
1493                                                 g_free(fromaddress);
1494                                                 g_free(fromname);
1495                                         } else {
1496                                                 PrefsAccount *account = NULL;
1497                                                 FolderItem   *folder_item;
1498
1499                                                 if (textview->messageview && textview->messageview->mainwin 
1500                                                 &&  textview->messageview->mainwin->summaryview 
1501                                                 &&  textview->messageview->mainwin->summaryview->folder_item) {
1502                                                         folder_item = textview->messageview->mainwin->summaryview->folder_item;
1503                                                         if (folder_item->prefs && folder_item->prefs->enable_default_account)
1504                                                                 account = account_find_from_id(folder_item->prefs->default_account);
1505                                                 }
1506                                                 compose_new_with_recipient
1507                                                         (account, uri->uri + 7);
1508                                         }
1509                                 } else {
1510                                         open_uri(uri->uri,
1511                                                  prefs_common.uri_cmd);
1512                                 }
1513                         }
1514                 }
1515         }
1516         if (event)
1517                 textview->last_buttonpress = event->type;
1518         return FALSE;
1519 }
1520
1521 static void textview_uri_list_remove_all(GSList *uri_list)
1522 {
1523         GSList *cur;
1524
1525         for (cur = uri_list; cur != NULL; cur = cur->next) {
1526                 if (cur->data) {
1527                         g_free(((RemoteURI *)cur->data)->uri);
1528                         g_free(cur->data);
1529                 }
1530         }
1531
1532         g_slist_free(uri_list);
1533 }