textview.c (68906B)
1 /* 2 * Claws Mail -- a GTK based, lightweight, and fast e-mail client 3 * Copyright (C) 1999-2025 the Claws Mail team and 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 3 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, see <http://www.gnu.org/licenses/>. 17 */ 18 19 #include "defs.h" 20 21 #include <glib.h> 22 #include <glib/gi18n.h> 23 #include <gdk/gdk.h> 24 #include <gdk/gdkkeysyms.h> 25 #include <gtk/gtk.h> 26 #include <stdio.h> 27 #include <ctype.h> 28 #include <string.h> 29 #include <stdlib.h> 30 #include <errno.h> 31 #include <sys/wait.h> 32 33 #include "main.h" 34 #include "summaryview.h" 35 #include "procheader.h" 36 #include "prefs_common.h" 37 #include "codeconv.h" 38 #include "utils.h" 39 #include "gtkutils.h" 40 #include "procmime.h" 41 #include "html.h" 42 #include "compose.h" 43 #include "addressbook.h" 44 #include "addrindex.h" 45 #include "displayheader.h" 46 #include "account.h" 47 #include "mimeview.h" 48 #include "alertpanel.h" 49 #include "menu.h" 50 #include "filesel.h" 51 #include "inputdialog.h" 52 #include "tags.h" 53 #include "manage_window.h" 54 #include "folder_item_prefs.h" 55 #include "hooks.h" 56 #include "file-utils.h" 57 #include "fence.h" 58 59 static GdkRGBA quote_colors[3] = { 60 {0, 0, 0, 1}, 61 {0, 0, 0, 1}, 62 {0, 0, 0, 1} 63 }; 64 65 static GdkRGBA quote_bgcolors[3] = { 66 {0, 0, 0, 1}, 67 {0, 0, 0, 1}, 68 {0, 0, 0, 1} 69 }; 70 static GdkRGBA signature_color = { 71 0.5, 0.5, 0.5, 1 72 }; 73 74 static GdkRGBA uri_color = { 75 0, 0, 0, 1 76 }; 77 78 static GdkRGBA emphasis_color = { 79 0, 0, 0, 1 80 }; 81 82 static GdkRGBA diff_added_color = { 83 0, 0, 0, 1 84 }; 85 86 static GdkRGBA diff_deleted_color = { 87 0, 0, 0, 1 88 }; 89 90 static GdkRGBA diff_hunk_color = { 91 0, 0, 0, 1 92 }; 93 94 static GdkRGBA tags_bgcolor = { 95 0, 0, 0, 1 96 }; 97 98 static GdkRGBA tags_color = { 99 0, 0, 0, 1 100 }; 101 102 static GdkCursor *hand_cursor = NULL; 103 static GdkCursor *text_cursor = NULL; 104 static GdkCursor *watch_cursor= NULL; 105 106 #define WIDTH 48 107 #define HEIGHT 48 108 109 #define TEXTVIEW_STATUSBAR_PUSH(textview, str) \ 110 { if (textview->messageview->statusbar) \ 111 gtk_statusbar_push(GTK_STATUSBAR(textview->messageview->statusbar), \ 112 textview->messageview->statusbar_cid, str); \ 113 } 114 115 #define TEXTVIEW_STATUSBAR_POP(textview) \ 116 { if (textview->messageview->statusbar) \ 117 gtk_statusbar_pop(GTK_STATUSBAR(textview->messageview->statusbar), \ 118 textview->messageview->statusbar_cid); \ 119 } 120 121 static void textview_add_part (TextView *textview, 122 MimeInfo *mimeinfo); 123 static void textview_add_parts (TextView *textview, 124 MimeInfo *mimeinfo); 125 static void textview_write_body (TextView *textview, 126 MimeInfo *mimeinfo); 127 static void textview_show_html (TextView *textview, 128 FILE *fp, 129 CodeConverter *conv); 130 131 static void textview_write_line (TextView *textview, 132 const gchar *str, 133 CodeConverter *conv, 134 gboolean do_quote_folding); 135 static void textview_write_link (TextView *textview, 136 const gchar *str, 137 const gchar *uri, 138 CodeConverter *conv); 139 140 static GPtrArray *textview_scan_header (TextView *textview, 141 FILE *fp); 142 static void textview_show_header (TextView *textview, 143 GPtrArray *headers); 144 145 static gboolean textview_motion_notify (GtkWidget *widget, 146 GdkEventMotion *motion, 147 TextView *textview); 148 static gboolean textview_leave_notify (GtkWidget *widget, 149 GdkEventCrossing *event, 150 TextView *textview); 151 static gboolean textview_visibility_notify (GtkWidget *widget, 152 GdkEventVisibility *event, 153 TextView *textview); 154 static void textview_uri_update (TextView *textview, 155 gint x, 156 gint y); 157 static gboolean textview_get_uri_range (TextView *textview, 158 GtkTextIter *iter, 159 GtkTextTag *tag, 160 GtkTextIter *start_iter, 161 GtkTextIter *end_iter); 162 static ClickableText *textview_get_uri_from_range (TextView *textview, 163 GtkTextIter *iter, 164 GtkTextTag *tag, 165 GtkTextIter *start_iter, 166 GtkTextIter *end_iter); 167 static ClickableText *textview_get_uri (TextView *textview, 168 GtkTextIter *iter, 169 GtkTextTag *tag); 170 static gboolean textview_uri_button_pressed (GtkTextTag *tag, 171 GObject *obj, 172 GdkEvent *event, 173 GtkTextIter *iter, 174 TextView *textview); 175 176 static void textview_uri_list_remove_all (GSList *uri_list); 177 178 static void textview_toggle_quote (TextView *textview, 179 GSList *start_list, 180 ClickableText *uri, 181 gboolean expand_only); 182 183 static void open_uri_cb (GtkAction *action, 184 TextView *textview); 185 static void copy_uri_cb (GtkAction *action, 186 TextView *textview); 187 static void add_uri_to_addrbook_cb (GtkAction *action, 188 TextView *textview); 189 static void reply_to_uri_cb (GtkAction *action, 190 TextView *textview); 191 static void mail_to_uri_cb (GtkAction *action, 192 TextView *textview); 193 static void copy_mail_to_uri_cb (GtkAction *action, 194 TextView *textview); 195 196 static GtkActionEntry textview_link_popup_entries[] = 197 { 198 {"TextviewPopupLink", NULL, "TextviewPopupLink", NULL, NULL, NULL }, 199 {"TextviewPopupLink/Open", NULL, N_("_Open in web browser"), NULL, NULL, G_CALLBACK(open_uri_cb) }, 200 {"TextviewPopupLink/Copy", NULL, N_("Copy this _link"), NULL, NULL, G_CALLBACK(copy_uri_cb) }, 201 }; 202 203 static GtkActionEntry textview_mail_popup_entries[] = 204 { 205 {"TextviewPopupMail", NULL, "TextviewPopupMail", NULL, NULL, NULL }, 206 {"TextviewPopupMail/Compose", NULL, N_("Write _new message"), NULL, NULL, G_CALLBACK(mail_to_uri_cb) }, 207 {"TextviewPopupMail/ReplyTo", NULL, N_("_Reply to this address"), NULL, NULL, G_CALLBACK(reply_to_uri_cb) }, 208 {"TextviewPopupMail/AddAB", NULL, N_("Add to _Address book"), NULL, NULL, G_CALLBACK(add_uri_to_addrbook_cb) }, 209 {"TextviewPopupMail/Copy", NULL, N_("Copy this add_ress"), NULL, NULL, G_CALLBACK(copy_mail_to_uri_cb) }, 210 }; 211 212 static void textview_size_allocate_cb (GtkWidget *widget, 213 GtkAllocation *allocation, 214 gpointer data) 215 { 216 } 217 218 TextView *textview_create(void) 219 { 220 TextView *textview; 221 GtkWidget *vbox; 222 GtkWidget *scrolledwin; 223 GtkWidget *text; 224 GtkTextBuffer *buffer; 225 GtkClipboard *clipboard; 226 227 debug_print("Creating text view...\n"); 228 textview = g_new0(TextView, 1); 229 230 scrolledwin = gtk_scrolled_window_new(NULL, NULL); 231 gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolledwin), 232 GTK_POLICY_AUTOMATIC, 233 GTK_POLICY_AUTOMATIC); 234 gtk_widget_set_vexpand(GTK_WIDGET(scrolledwin), TRUE); 235 gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolledwin), 236 GTK_SHADOW_IN); 237 238 /* create GtkSText widgets for single-byte and multi-byte character */ 239 text = gtk_text_view_new(); 240 gtk_widget_add_events(text, GDK_LEAVE_NOTIFY_MASK); 241 gtk_widget_show(text); 242 gtk_text_view_set_editable(GTK_TEXT_VIEW(text), FALSE); 243 gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text), GTK_WRAP_WORD_CHAR); 244 gtk_text_view_set_cursor_visible(GTK_TEXT_VIEW(text), FALSE); 245 gtk_text_view_set_left_margin(GTK_TEXT_VIEW(text), 6); 246 gtk_text_view_set_right_margin(GTK_TEXT_VIEW(text), 6); 247 248 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text)); 249 clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY); 250 gtk_text_buffer_add_selection_clipboard(buffer, clipboard); 251 252 g_object_ref(scrolledwin); 253 254 gtk_container_add(GTK_CONTAINER(scrolledwin), text); 255 256 g_signal_connect(G_OBJECT(text), "motion-notify-event", 257 G_CALLBACK(textview_motion_notify), textview); 258 g_signal_connect(G_OBJECT(text), "leave-notify-event", 259 G_CALLBACK(textview_leave_notify), textview); 260 g_signal_connect(G_OBJECT(text), "visibility-notify-event", 261 G_CALLBACK(textview_visibility_notify), textview); 262 g_signal_connect(G_OBJECT(text), "size_allocate", 263 G_CALLBACK(textview_size_allocate_cb), 264 textview); 265 266 gtk_widget_show(scrolledwin); 267 268 vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); 269 gtk_widget_set_name(GTK_WIDGET(vbox), "textview"); 270 gtk_box_pack_start(GTK_BOX(vbox), scrolledwin, TRUE, TRUE, 0); 271 272 gtk_widget_show(vbox); 273 274 textview->ui_manager = gtk_ui_manager_new(); 275 textview->link_action_group = cm_menu_create_action_group_full(textview->ui_manager, 276 "TextviewPopupLink", 277 textview_link_popup_entries, 278 G_N_ELEMENTS(textview_link_popup_entries), (gpointer)textview); 279 textview->mail_action_group = cm_menu_create_action_group_full(textview->ui_manager, 280 "TextviewPopupMail", 281 textview_mail_popup_entries, 282 G_N_ELEMENTS(textview_mail_popup_entries), (gpointer)textview); 283 284 MENUITEM_ADDUI_MANAGER(textview->ui_manager, "/", "Menus", "Menus", GTK_UI_MANAGER_MENUBAR) 285 MENUITEM_ADDUI_MANAGER(textview->ui_manager, 286 "/Menus", "TextviewPopupLink", "TextviewPopupLink", GTK_UI_MANAGER_MENU) 287 MENUITEM_ADDUI_MANAGER(textview->ui_manager, 288 "/Menus", "TextviewPopupMail", "TextviewPopupMail", GTK_UI_MANAGER_MENU) 289 290 MENUITEM_ADDUI_MANAGER(textview->ui_manager, 291 "/Menus/TextviewPopupLink", "Open", "TextviewPopupLink/Open", GTK_UI_MANAGER_MENUITEM) 292 MENUITEM_ADDUI_MANAGER(textview->ui_manager, 293 "/Menus/TextviewPopupLink", "Copy", "TextviewPopupLink/Copy", GTK_UI_MANAGER_MENUITEM) 294 MENUITEM_ADDUI_MANAGER(textview->ui_manager, 295 "/Menus/TextviewPopupMail", "Compose", "TextviewPopupMail/Compose", GTK_UI_MANAGER_MENUITEM) 296 MENUITEM_ADDUI_MANAGER(textview->ui_manager, 297 "/Menus/TextviewPopupMail", "ReplyTo", "TextviewPopupMail/ReplyTo", GTK_UI_MANAGER_MENUITEM) 298 MENUITEM_ADDUI_MANAGER(textview->ui_manager, 299 "/Menus/TextviewPopupMail", "AddAB", "TextviewPopupMail/AddAB", GTK_UI_MANAGER_MENUITEM) 300 MENUITEM_ADDUI_MANAGER(textview->ui_manager, 301 "/Menus/TextviewPopupMail", "Copy", "TextviewPopupMail/Copy", GTK_UI_MANAGER_MENUITEM) 302 303 textview->link_popup_menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM( 304 gtk_ui_manager_get_widget(textview->ui_manager, "/Menus/TextviewPopupLink")) ); 305 textview->mail_popup_menu = gtk_menu_item_get_submenu(GTK_MENU_ITEM( 306 gtk_ui_manager_get_widget(textview->ui_manager, "/Menus/TextviewPopupMail")) ); 307 308 textview->vbox = vbox; 309 textview->scrolledwin = scrolledwin; 310 textview->text = text; 311 textview->uri_list = NULL; 312 textview->body_pos = 0; 313 textview->last_buttonpress = GDK_NOTHING; 314 textview->image = NULL; 315 return textview; 316 } 317 318 static void textview_create_tags(GtkTextView *text, TextView *textview) 319 { 320 GtkTextBuffer *buffer; 321 GtkTextTag *tag, *qtag; 322 323 PangoFontDescription *font_desc = default_font(); 324 PangoFontDescription *bold_font_desc = default_font(); 325 pango_font_description_set_weight(bold_font_desc, PANGO_WEIGHT_BOLD); 326 327 buffer = gtk_text_view_get_buffer(text); 328 329 gtk_text_buffer_create_tag(buffer, "header", 330 "pixels-above-lines", 0, 331 "pixels-above-lines-set", TRUE, 332 "pixels-below-lines", 0, 333 "pixels-below-lines-set", TRUE, 334 "font-desc", font_desc, 335 "left-margin", 3, 336 "left-margin-set", TRUE, 337 NULL); 338 gtk_text_buffer_create_tag(buffer, "header_title", 339 "font-desc", bold_font_desc, 340 NULL); 341 tag = gtk_text_buffer_create_tag(buffer, "hlink", 342 "pixels-above-lines", 0, 343 "pixels-above-lines-set", TRUE, 344 "pixels-below-lines", 0, 345 "pixels-below-lines-set", TRUE, 346 "font-desc", font_desc, 347 "left-margin", 3, 348 "left-margin-set", TRUE, 349 "foreground-rgba", &uri_color, 350 NULL); 351 g_signal_connect(G_OBJECT(tag), "event", 352 G_CALLBACK(textview_uri_button_pressed), textview); 353 if (prefs_common.enable_bgcolor) { 354 gtk_text_buffer_create_tag(buffer, "quote0", 355 "foreground-rgba", "e_colors[0], 356 "paragraph-background-rgba", "e_bgcolors[0], 357 NULL); 358 gtk_text_buffer_create_tag(buffer, "quote1", 359 "foreground-rgba", "e_colors[1], 360 "paragraph-background-rgba", "e_bgcolors[1], 361 NULL); 362 gtk_text_buffer_create_tag(buffer, "quote2", 363 "foreground-rgba", "e_colors[2], 364 "paragraph-background-rgba", "e_bgcolors[2], 365 NULL); 366 } else { 367 gtk_text_buffer_create_tag(buffer, "quote0", 368 "foreground-rgba", "e_colors[0], 369 NULL); 370 gtk_text_buffer_create_tag(buffer, "quote1", 371 "foreground-rgba", "e_colors[1], 372 NULL); 373 gtk_text_buffer_create_tag(buffer, "quote2", 374 "foreground-rgba", "e_colors[2], 375 NULL); 376 } 377 gtk_text_buffer_create_tag(buffer, "tags", 378 "foreground-rgba", &tags_color, 379 "paragraph-background-rgba", &tags_bgcolor, 380 NULL); 381 gtk_text_buffer_create_tag(buffer, "emphasis", 382 "foreground-rgba", &emphasis_color, 383 NULL); 384 gtk_text_buffer_create_tag(buffer, "signature", 385 "foreground-rgba", &signature_color, 386 NULL); 387 tag = gtk_text_buffer_create_tag(buffer, "link", 388 "foreground-rgba", &uri_color, 389 NULL); 390 qtag = gtk_text_buffer_create_tag(buffer, "qlink", 391 NULL); 392 gtk_text_buffer_create_tag(buffer, "link-hover", 393 "underline", PANGO_UNDERLINE_SINGLE, 394 NULL); 395 gtk_text_buffer_create_tag(buffer, "diff-add", 396 "foreground-rgba", &diff_added_color, 397 NULL); 398 gtk_text_buffer_create_tag(buffer, "diff-del", 399 "foreground-rgba", &diff_deleted_color, 400 NULL); 401 gtk_text_buffer_create_tag(buffer, "diff-add-file", 402 "foreground-rgba", &diff_added_color, 403 "weight", PANGO_WEIGHT_BOLD, 404 NULL); 405 gtk_text_buffer_create_tag(buffer, "diff-del-file", 406 "foreground-rgba", &diff_deleted_color, 407 "weight", PANGO_WEIGHT_BOLD, 408 NULL); 409 gtk_text_buffer_create_tag(buffer, "diff-hunk", 410 "foreground-rgba", &diff_hunk_color, 411 "weight", PANGO_WEIGHT_BOLD, 412 NULL); 413 g_signal_connect(G_OBJECT(qtag), "event", 414 G_CALLBACK(textview_uri_button_pressed), textview); 415 g_signal_connect(G_OBJECT(tag), "event", 416 G_CALLBACK(textview_uri_button_pressed), textview); 417 418 pango_font_description_free(font_desc); 419 pango_font_description_free(bold_font_desc); 420 } 421 422 void textview_init(TextView *textview) 423 { 424 if (!hand_cursor) 425 hand_cursor = gdk_cursor_new_for_display( 426 gtk_widget_get_display(textview->vbox), GDK_HAND2); 427 if (!text_cursor) 428 text_cursor = gdk_cursor_new_for_display( 429 gtk_widget_get_display(textview->vbox), GDK_XTERM); 430 if (!watch_cursor) 431 watch_cursor = gdk_cursor_new_for_display( 432 gtk_widget_get_display(textview->vbox), GDK_WATCH); 433 434 textview_reflect_prefs(textview); 435 textview_create_tags(GTK_TEXT_VIEW(textview->text), textview); 436 } 437 438 #define CHANGE_TAG_COLOR(tagname, colorfg, colorbg) { \ 439 tag = gtk_text_tag_table_lookup(tags, tagname); \ 440 if (tag) \ 441 g_object_set(G_OBJECT(tag), "foreground-rgba", colorfg, "paragraph-background-rgba", colorbg, NULL); \ 442 } 443 444 static void textview_update_message_colors(TextView *textview) 445 { 446 GdkRGBA black = {0, 0, 0, 1}; 447 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text)); 448 449 GtkTextTagTable *tags = gtk_text_buffer_get_tag_table(buffer); 450 GtkTextTag *tag = NULL; 451 452 quote_bgcolors[0] = quote_bgcolors[1] = quote_bgcolors[2] = black; 453 quote_colors[0] = quote_colors[1] = quote_colors[2] = black; 454 uri_color = emphasis_color = signature_color = diff_added_color = 455 diff_deleted_color = diff_hunk_color = black; 456 tags_bgcolor = tags_color = black; 457 458 if (prefs_common.enable_color) { 459 /* grab the quote colors, converting from an int to a GdkColor */ 460 quote_colors[0] = prefs_common.color[COL_QUOTE_LEVEL1]; 461 quote_colors[1] = prefs_common.color[COL_QUOTE_LEVEL2]; 462 quote_colors[2] = prefs_common.color[COL_QUOTE_LEVEL3]; 463 uri_color = prefs_common.color[COL_URI]; 464 signature_color = prefs_common.color[COL_SIGNATURE]; 465 emphasis_color = prefs_common.color[COL_EMPHASIS]; 466 diff_added_color = prefs_common.color[COL_DIFF_ADDED]; 467 diff_deleted_color = prefs_common.color[COL_DIFF_DELETED]; 468 diff_hunk_color = prefs_common.color[COL_DIFF_HUNK]; 469 } 470 if (prefs_common.enable_color && prefs_common.enable_bgcolor) { 471 quote_bgcolors[0] = prefs_common.color[COL_QUOTE_LEVEL1_BG]; 472 quote_bgcolors[1] = prefs_common.color[COL_QUOTE_LEVEL2_BG]; 473 quote_bgcolors[2] = prefs_common.color[COL_QUOTE_LEVEL3_BG]; 474 CHANGE_TAG_COLOR("quote0", "e_colors[0], "e_bgcolors[0]); 475 CHANGE_TAG_COLOR("quote1", "e_colors[1], "e_bgcolors[1]); 476 CHANGE_TAG_COLOR("quote2", "e_colors[2], "e_bgcolors[2]); 477 } else { 478 CHANGE_TAG_COLOR("quote0", "e_colors[0], NULL); 479 CHANGE_TAG_COLOR("quote1", "e_colors[1], NULL); 480 CHANGE_TAG_COLOR("quote2", "e_colors[2], NULL); 481 } 482 483 CHANGE_TAG_COLOR("emphasis", &emphasis_color, NULL); 484 CHANGE_TAG_COLOR("signature", &signature_color, NULL); 485 CHANGE_TAG_COLOR("link", &uri_color, NULL); 486 CHANGE_TAG_COLOR("link-hover", &uri_color, NULL); 487 CHANGE_TAG_COLOR("diff-add", &diff_added_color, NULL); 488 CHANGE_TAG_COLOR("diff-del", &diff_deleted_color, NULL); 489 CHANGE_TAG_COLOR("diff-add-file", &diff_added_color, NULL); 490 CHANGE_TAG_COLOR("diff-del-file", &diff_deleted_color, NULL); 491 CHANGE_TAG_COLOR("diff-hunk", &diff_hunk_color, NULL); 492 493 tags_bgcolor = prefs_common.color[COL_TAGS_BG]; 494 tags_color = prefs_common.color[COL_TAGS]; 495 } 496 #undef CHANGE_TAG_COLOR 497 498 void textview_reflect_prefs(TextView *textview) 499 { 500 textview_update_message_colors(textview); 501 } 502 503 void textview_show_part(TextView *textview, MimeInfo *mimeinfo, FILE *fp) 504 { 505 textview->loading = TRUE; 506 textview->stop_loading = FALSE; 507 508 textview_clear(textview); 509 510 if (mimeinfo->type == MIMETYPE_MULTIPART || 511 (mimeinfo->type == MIMETYPE_MESSAGE && !g_ascii_strcasecmp(mimeinfo->subtype, "rfc822"))) { 512 textview_add_parts(textview, mimeinfo); 513 } else { 514 if (fseek(fp, mimeinfo->offset, SEEK_SET) < 0) 515 perror("fseek"); 516 textview_write_body(textview, mimeinfo); 517 } 518 519 textview->loading = FALSE; 520 textview->stop_loading = FALSE; 521 textview_set_position(textview, 0); 522 } 523 524 static void textview_add_part(TextView *textview, MimeInfo *mimeinfo) 525 { 526 if (textview->stop_loading) 527 return; 528 if (mimeinfo->type == MIMETYPE_MULTIPART) 529 return; 530 531 gchar buf[BUFFSIZE]; 532 GPtrArray *headers = NULL; 533 const gchar *name; 534 gchar *content_type; 535 536 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 537 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text); 538 int charcount = gtk_text_buffer_get_char_count(buffer); 539 GtkTextIter iter; 540 gtk_text_buffer_get_end_iter(buffer, &iter); 541 542 textview->prev_quote_level = -1; 543 544 if ((mimeinfo->type == MIMETYPE_MESSAGE) && !g_ascii_strcasecmp(mimeinfo->subtype, "rfc822")) { 545 FILE *fp; 546 if (mimeinfo->content == MIMECONTENT_MEM) 547 fp = str_open_as_stream(mimeinfo->data.mem); 548 else 549 fp = g_fopen(mimeinfo->data.filename, "rb"); 550 if (!fp) { 551 FILE_OP_ERROR(mimeinfo->data.filename, "g_fopen"); 552 return; 553 } 554 if (fseek(fp, mimeinfo->offset, SEEK_SET) < 0) { 555 FILE_OP_ERROR(mimeinfo->data.filename, "fseek"); 556 fclose(fp); 557 return; 558 } 559 headers = textview_scan_header(textview, fp); 560 if (headers) { 561 if (charcount > 0) 562 gtk_text_buffer_insert(buffer, &iter, "\n", 1); 563 textview_show_header(textview, headers); 564 procheader_header_array_destroy(headers); 565 } 566 fclose(fp); 567 return; 568 } 569 570 name = procmime_mimeinfo_get_parameter(mimeinfo, "filename"); 571 content_type = procmime_get_content_type_str(mimeinfo->type, 572 mimeinfo->subtype); 573 if (name == NULL) 574 name = procmime_mimeinfo_get_parameter(mimeinfo, "name"); 575 if (name != NULL) 576 g_snprintf(buf, sizeof(buf), _("[%s %s (%ld bytes)]"), 577 name, content_type, mimeinfo->length); 578 else 579 g_snprintf(buf, sizeof(buf), _("[%s (%ld bytes)]"), 580 content_type, mimeinfo->length); 581 582 g_free(content_type); 583 584 if (mimeinfo->disposition == DISPOSITIONTYPE_ATTACHMENT 585 || (mimeinfo->disposition == DISPOSITIONTYPE_INLINE && 586 mimeinfo->type != MIMETYPE_TEXT)) { 587 gtk_text_buffer_insert(buffer, &iter, "\n", 1); 588 TEXTVIEW_INSERT_LINK(buf, "cm://select_attachment", mimeinfo); 589 gtk_text_buffer_insert(buffer, &iter, " \n", -1); 590 } else if (mimeinfo->type == MIMETYPE_TEXT) { 591 if (prefs_common.display_header && (charcount > 0)) 592 gtk_text_buffer_insert(buffer, &iter, "\n", 1); 593 594 if (!gtk_text_buffer_get_mark(buffer, "body_start")) { 595 gtk_text_buffer_get_end_iter(buffer, &iter); 596 gtk_text_buffer_create_mark(buffer, "body_start", &iter, TRUE); 597 } 598 599 textview_write_body(textview, mimeinfo); 600 601 if (!gtk_text_buffer_get_mark(buffer, "body_end")) { 602 gtk_text_buffer_get_end_iter(buffer, &iter); 603 gtk_text_buffer_create_mark(buffer, "body_end", &iter, TRUE); 604 } 605 } 606 } 607 608 static void recursive_add_parts(TextView *textview, GNode *node) 609 { 610 MimeInfo *mimeinfo = (MimeInfo *) node->data; 611 textview_add_part(textview, mimeinfo); 612 if (mimeinfo->type != MIMETYPE_MULTIPART && mimeinfo->type != MIMETYPE_MESSAGE) { 613 return; 614 } 615 616 GNode *iter; 617 if (g_ascii_strcasecmp(mimeinfo->subtype, "alternative") != 0) { 618 for (iter = g_node_first_child(node); iter != NULL; iter = g_node_next_sibling(iter)) { 619 recursive_add_parts(textview, iter); 620 } 621 return; 622 } 623 624 GNode *preferred_body = NULL; 625 int preferred_score = 0; 626 for (iter = g_node_first_child(node); iter != NULL; iter = g_node_next_sibling(iter)) { 627 int score = 1; 628 MimeInfo *submime = (MimeInfo *) iter->data; 629 if (submime->type == MIMETYPE_TEXT) 630 score++; 631 if (submime->subtype != NULL) { 632 if (g_ascii_strcasecmp(submime->subtype, "plain") == 0) 633 score++; 634 } 635 if (score > preferred_score) { 636 preferred_score = score; 637 preferred_body = iter; 638 } 639 } 640 641 if (preferred_body != NULL) 642 recursive_add_parts(textview, preferred_body); 643 } 644 645 static void textview_add_parts(TextView *textview, MimeInfo *mimeinfo) 646 { 647 recursive_add_parts(textview, mimeinfo->node); 648 } 649 650 void textview_show_error(TextView *textview) 651 { 652 textview_clear(textview); 653 654 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 655 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text); 656 GtkTextIter iter; 657 gtk_text_buffer_get_start_iter(buffer, &iter); 658 659 TEXTVIEW_INSERT(_("\n" 660 " This message can't be displayed.\n" 661 " This is probably due to a network error.\n" 662 "\n" 663 " Use ")); 664 TEXTVIEW_INSERT_LINK(_("'Network Log'"), "cm://view_log", NULL); 665 TEXTVIEW_INSERT(_(" in the Tools menu for more information.")); 666 textview_show_icon(textview, "dialog-error"); 667 } 668 669 void textview_show_info(TextView *textview, const gchar *info_str) 670 { 671 textview_clear(textview); 672 673 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 674 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text); 675 GtkTextIter iter; 676 gtk_text_buffer_get_start_iter(buffer, &iter); 677 678 TEXTVIEW_INSERT(info_str); 679 textview_show_icon(textview, "dialog-information"); 680 textview_cursor_normal(textview); 681 } 682 683 void textview_show_mime_part(TextView *textview, MimeInfo *partinfo) 684 { 685 if (!partinfo) 686 return; 687 688 GtkUIManager *ui_manager; 689 if (textview->messageview->window != NULL) 690 ui_manager = textview->messageview->ui_manager; 691 else 692 ui_manager = textview->messageview->mainwin->ui_manager; 693 694 textview_clear(textview); 695 696 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 697 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text); 698 GtkTextIter iter; 699 gtk_text_buffer_get_start_iter(buffer, &iter); 700 701 TEXTVIEW_INSERT("\n"); 702 703 const char *name = procmime_mimeinfo_get_parameter(partinfo, "filename"); 704 if (name == NULL) 705 name = procmime_mimeinfo_get_parameter(partinfo, "name"); 706 if (name != NULL) { 707 char *content_type = procmime_get_content_type_str(partinfo->type, 708 partinfo->subtype); 709 TEXTVIEW_INSERT(" "); 710 TEXTVIEW_INSERT_BOLD(name); 711 TEXTVIEW_INSERT(" ("); 712 TEXTVIEW_INSERT(content_type); 713 TEXTVIEW_INSERT(", "); 714 TEXTVIEW_INSERT(to_human_readable((goffset)partinfo->length)); 715 TEXTVIEW_INSERT("):\n\n"); 716 free(content_type); 717 } 718 TEXTVIEW_INSERT(_(" The following can be performed on this part\n")); 719 TEXTVIEW_INSERT(_(" by right-clicking the icon or list item:")); 720 TEXTVIEW_INSERT("\n"); 721 722 TEXTVIEW_INSERT(_(" - To save, select ")); 723 TEXTVIEW_INSERT_LINK(_("'Save as...'"), "cm://save_as", NULL); 724 TEXTVIEW_INSERT("\n"); 725 726 TEXTVIEW_INSERT(_(" - To display as text, select ")); 727 TEXTVIEW_INSERT_LINK(_("'Display as text'"), "cm://display_as_text", NULL); 728 729 TEXTVIEW_INSERT(_(" - To open with an external program, select ")); 730 TEXTVIEW_INSERT_LINK(_("'Open'"), "cm://open", NULL); 731 TEXTVIEW_INSERT(_("')\n")); 732 TEXTVIEW_INSERT(_(" (alternately double-click, or click the middle ")); 733 TEXTVIEW_INSERT(_("mouse button)\n")); 734 TEXTVIEW_INSERT(_(" - Or use ")); 735 TEXTVIEW_INSERT_LINK(_("'Open with...'"), "cm://open_with", NULL); 736 TEXTVIEW_INSERT("\n"); 737 738 textview_show_icon(textview, "dialog-information"); 739 } 740 741 static void textview_write_body(TextView *textview, MimeInfo *mimeinfo) 742 { 743 FILE *tmpfp; 744 gchar buf[BUFFSIZE]; 745 CodeConverter *conv; 746 const gchar *charset; 747 const gchar *p, *cmd; 748 GSList *cur; 749 gboolean continue_write = TRUE; 750 glong wrote = 0, i = 0; 751 FolderItem *folder_item = NULL; 752 753 if (textview->messageview->forced_charset) 754 charset = textview->messageview->forced_charset; 755 else { 756 /* use supersets transparently when possible */ 757 charset = procmime_mimeinfo_get_parameter(mimeinfo, "charset"); 758 if (charset && !strcasecmp(charset, CS_ISO_8859_1)) 759 charset = CS_WINDOWS_1252; 760 else if (charset && !strcasecmp(charset, CS_X_GBK)) 761 charset = CS_GB18030; 762 else if (charset && !strcasecmp(charset, CS_GBK)) 763 charset = CS_GB18030; 764 else if (charset && !strcasecmp(charset, CS_GB2312)) 765 charset = CS_GB18030; 766 } 767 768 conv = conv_code_converter_new(charset); 769 770 procmime_force_encoding(textview->messageview->forced_encoding); 771 772 textview->is_in_signature = FALSE; 773 textview->is_diff = FALSE; 774 textview->is_attachment = FALSE; 775 textview->is_in_git_patch = FALSE; 776 777 procmime_decode_content(mimeinfo); 778 779 if (textview->messageview->msginfo && textview->messageview->msginfo->folder) 780 folder_item = textview->messageview->msginfo->folder; 781 782 if (!g_ascii_strcasecmp(mimeinfo->subtype, "html") && 783 ((folder_item && folder_item->prefs && folder_item->prefs->render_html == HTML_RENDER_ALWAYS) 784 || ((folder_item && folder_item->prefs && folder_item->prefs->render_html == HTML_RENDER_DEFAULT) 785 && prefs_common.render_html))) { 786 gchar *filename; 787 788 filename = procmime_get_tmp_file_name(mimeinfo); 789 if (procmime_get_part(filename, mimeinfo) == 0) { 790 tmpfp = g_fopen(filename, "rb"); 791 if (tmpfp) { 792 textview_show_html(textview, tmpfp, conv); 793 fclose(tmpfp); 794 } 795 unlink(filename); 796 } 797 g_free(filename); 798 } else { 799 if (!g_ascii_strcasecmp(mimeinfo->subtype, "x-patch") 800 || !g_ascii_strcasecmp(mimeinfo->subtype, "x-diff")) 801 textview->is_diff = TRUE; 802 803 /* Displayed part is an attachment, but not an attached 804 * e-mail. Set a flag, so that elsewhere in the code we 805 * know not to try making collapsible quotes in it. */ 806 if (mimeinfo->disposition == DISPOSITIONTYPE_ATTACHMENT && 807 mimeinfo->type != MIMETYPE_MESSAGE) 808 textview->is_attachment = TRUE; 809 810 if (mimeinfo->content == MIMECONTENT_MEM) 811 tmpfp = str_open_as_stream(mimeinfo->data.mem); 812 else 813 tmpfp = g_fopen(mimeinfo->data.filename, "rb"); 814 if (!tmpfp) { 815 FILE_OP_ERROR(mimeinfo->data.filename, "g_fopen"); 816 conv_code_converter_destroy(conv); 817 return; 818 } 819 if (fseek(tmpfp, mimeinfo->offset, SEEK_SET) < 0) { 820 FILE_OP_ERROR(mimeinfo->data.filename, "fseek"); 821 fclose(tmpfp); 822 conv_code_converter_destroy(conv); 823 return; 824 } 825 debug_print("Viewing text content of type: %s (length: %ld)\n", mimeinfo->subtype, mimeinfo->length); 826 while (((i = ftell(tmpfp)) < mimeinfo->offset + mimeinfo->length) && 827 (fgets(buf, sizeof(buf), tmpfp) != NULL) 828 && continue_write) { 829 textview_write_line(textview, buf, conv, TRUE); 830 if (textview->stop_loading) { 831 fclose(tmpfp); 832 conv_code_converter_destroy(conv); 833 return; 834 } 835 wrote += ftell(tmpfp)-i; 836 if (mimeinfo->length > 1024*1024 837 && wrote > 1024*1024 838 && !textview->messageview->show_full_text) { 839 continue_write = FALSE; 840 } 841 } 842 fclose(tmpfp); 843 } 844 845 conv_code_converter_destroy(conv); 846 procmime_force_encoding(0); 847 848 textview->uri_list = g_slist_reverse(textview->uri_list); 849 for (cur = textview->uri_list; cur; cur = cur->next) { 850 ClickableText *uri = (ClickableText *)cur->data; 851 if (!uri->is_quote) 852 continue; 853 if (!prefs_common.hide_quotes || 854 uri->quote_level+1 < prefs_common.hide_quotes) { 855 textview_toggle_quote(textview, cur, uri, TRUE); 856 if (textview->stop_loading) { 857 return; 858 } 859 } 860 } 861 862 GTK_EVENTS_FLUSH(); 863 } 864 865 static void textview_show_html(TextView *textview, FILE *fp, 866 CodeConverter *conv) 867 { 868 SC_HTMLParser *parser; 869 gchar *str; 870 gint lines = 0; 871 872 parser = sc_html_parser_new(fp, conv); 873 cm_return_if_fail(parser != NULL); 874 875 while ((str = sc_html_parse(parser)) != NULL) { 876 if (parser->state == SC_HTML_HREF) { 877 /* first time : get and copy the URL */ 878 if (parser->href == NULL) { 879 /* ALF - the claws html parser returns an empty string, 880 * if still inside an <a>, but already parsed past HREF */ 881 str = strtok(str, " "); 882 if (str) { 883 while (str && *str && g_ascii_isspace(*str)) 884 str++; 885 parser->href = g_strdup(str); 886 /* the URL may (or not) be followed by the 887 * referenced text */ 888 str = strtok(NULL, ""); 889 } 890 } 891 if (str != NULL) 892 textview_write_link(textview, str, parser->href, NULL); 893 } else 894 textview_write_line(textview, str, NULL, FALSE); 895 lines++; 896 if (lines % 500 == 0) 897 GTK_EVENTS_FLUSH(); 898 if (textview->stop_loading) { 899 sc_html_parser_destroy(parser); 900 return; 901 } 902 } 903 textview_write_line(textview, "\n", NULL, FALSE); 904 905 sc_html_parser_destroy(parser); 906 } 907 908 #define ADD_TXT_POS(bp_, ep_, pti_) \ 909 if ((last->next = alloca(sizeof(struct txtpos))) != NULL) { \ 910 last = last->next; \ 911 last->bp = (bp_); last->ep = (ep_); last->pti = (pti_); \ 912 last->next = NULL; \ 913 } else { \ 914 g_warning("alloc error scanning URIs"); \ 915 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, \ 916 linebuf, -1, \ 917 fg_tag, NULL); \ 918 return; \ 919 } 920 921 #define ADD_TXT_POS_LATER(bp_, ep_, pti_) \ 922 if ((last->next = alloca(sizeof(struct txtpos))) != NULL) { \ 923 last = last->next; \ 924 last->bp = (bp_); last->ep = (ep_); last->pti = (pti_); \ 925 last->next = NULL; \ 926 } else { \ 927 g_warning("alloc error scanning URIs"); \ 928 } 929 930 /* textview_make_clickable_parts() - colorizes clickable parts */ 931 static void textview_make_clickable_parts(TextView *textview, 932 const gchar *fg_tag, 933 const gchar *uri_tag, 934 const gchar *linebuf, 935 gboolean hdr) 936 { 937 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 938 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text); 939 GtkTextIter iter; 940 gchar *mybuf = g_strdup(linebuf); 941 942 /* parse table - in order of priority */ 943 struct table { 944 const gchar *needle; /* token */ 945 946 /* token search function */ 947 gchar *(*search) (const gchar *haystack, 948 const gchar *needle); 949 /* part parsing function */ 950 gboolean (*parse) (const gchar *start, 951 const gchar *scanpos, 952 const gchar **bp_, 953 const gchar **ep_, 954 gboolean hdr); 955 /* part to URI function */ 956 gchar *(*build_uri) (const gchar *bp, 957 const gchar *ep); 958 }; 959 960 static struct table parser[] = { 961 {"http://", strcasestr, get_uri_part, make_uri_string}, 962 {"https://", strcasestr, get_uri_part, make_uri_string}, 963 {"ftp://", strcasestr, get_uri_part, make_uri_string}, 964 {"ftps://", strcasestr, get_uri_part, make_uri_string}, 965 {"sftp://", strcasestr, get_uri_part, make_uri_string}, 966 {"gopher://",strcasestr, get_uri_part, make_uri_string}, 967 {"www.", strcasestr, get_uri_part, make_http_string}, 968 {"webcal://",strcasestr, get_uri_part, make_uri_string}, 969 {"webcals://",strcasestr, get_uri_part, make_uri_string}, 970 {"mailto:", strcasestr, get_uri_part, make_uri_string}, 971 {"@", strcasestr, get_email_part, make_email_string} 972 }; 973 const gint PARSE_ELEMS = sizeof parser / sizeof parser[0]; 974 975 gint n; 976 const gchar *walk, *bp, *ep; 977 978 struct txtpos { 979 const gchar *bp, *ep; /* text position */ 980 gint pti; /* index in parse table */ 981 struct txtpos *next; /* next */ 982 } head = {NULL, NULL, 0, NULL}, *last = &head; 983 984 if (!g_utf8_validate(linebuf, -1, NULL)) { 985 g_free(mybuf); 986 mybuf = g_malloc(strlen(linebuf)*2 +1); 987 conv_localetodisp(mybuf, strlen(linebuf)*2 +1, linebuf); 988 } 989 990 gtk_text_buffer_get_end_iter(buffer, &iter); 991 992 /* parse for clickable parts, and build a list of begin and end positions */ 993 for (walk = mybuf;;) { 994 gint last_index = PARSE_ELEMS; 995 gchar *scanpos = NULL; 996 997 /* FIXME: this looks phony. scanning for anything in the parse table */ 998 for (n = 0; n < PARSE_ELEMS; n++) { 999 gchar *tmp; 1000 1001 tmp = parser[n].search(walk, parser[n].needle); 1002 if (tmp) { 1003 if (scanpos == NULL || tmp < scanpos) { 1004 scanpos = tmp; 1005 last_index = n; 1006 } 1007 } 1008 } 1009 1010 if (scanpos) { 1011 /* check if URI can be parsed */ 1012 if (parser[last_index].parse(walk, scanpos, &bp, &ep, hdr) 1013 && (size_t) (ep - bp - 1) > strlen(parser[last_index].needle)) { 1014 ADD_TXT_POS(bp, ep, last_index); 1015 walk = ep; 1016 } else 1017 walk = scanpos + 1018 strlen(parser[last_index].needle); 1019 } else 1020 break; 1021 } 1022 1023 /* colorize this line */ 1024 if (head.next) { 1025 const gchar *normal_text = mybuf; 1026 1027 /* insert URIs */ 1028 for (last = head.next; last != NULL; 1029 normal_text = last->ep, last = last->next) { 1030 ClickableText *uri; 1031 uri = g_new0(ClickableText, 1); 1032 if (last->bp - normal_text > 0) 1033 gtk_text_buffer_insert_with_tags_by_name 1034 (buffer, &iter, 1035 normal_text, 1036 last->bp - normal_text, 1037 fg_tag, NULL); 1038 uri->uri = parser[last->pti].build_uri(last->bp, 1039 last->ep); 1040 uri->start = gtk_text_iter_get_offset(&iter); 1041 gtk_text_buffer_insert_with_tags_by_name 1042 (buffer, &iter, last->bp, last->ep - last->bp, 1043 uri_tag, fg_tag, NULL); 1044 uri->end = gtk_text_iter_get_offset(&iter); 1045 uri->filename = NULL; 1046 textview->uri_list = 1047 g_slist_prepend(textview->uri_list, uri); 1048 } 1049 1050 if (*normal_text) 1051 gtk_text_buffer_insert_with_tags_by_name 1052 (buffer, &iter, normal_text, -1, fg_tag, NULL); 1053 } else { 1054 gtk_text_buffer_insert_with_tags_by_name 1055 (buffer, &iter, mybuf, -1, fg_tag, NULL); 1056 } 1057 g_free(mybuf); 1058 } 1059 1060 /* textview_make_clickable_parts() - colorizes clickable parts */ 1061 static void textview_make_clickable_parts_later(TextView *textview, 1062 gint start, gint end) 1063 { 1064 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 1065 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text); 1066 GtkTextIter start_iter, end_iter; 1067 gchar *mybuf; 1068 gint offset = 0; 1069 /* parse table - in order of priority */ 1070 struct table { 1071 const gchar *needle; /* token */ 1072 1073 /* token search function */ 1074 gchar *(*search) (const gchar *haystack, 1075 const gchar *needle); 1076 /* part parsing function */ 1077 gboolean (*parse) (const gchar *start, 1078 const gchar *scanpos, 1079 const gchar **bp_, 1080 const gchar **ep_, 1081 gboolean hdr); 1082 /* part to URI function */ 1083 gchar *(*build_uri) (const gchar *bp, 1084 const gchar *ep); 1085 }; 1086 1087 static struct table parser[] = { 1088 {"http://", strcasestr, get_uri_part, make_uri_string}, 1089 {"https://", strcasestr, get_uri_part, make_uri_string}, 1090 {"ftp://", strcasestr, get_uri_part, make_uri_string}, 1091 {"ftps://", strcasestr, get_uri_part, make_uri_string}, 1092 {"sftp://", strcasestr, get_uri_part, make_uri_string}, 1093 {"www.", strcasestr, get_uri_part, make_http_string}, 1094 {"mailto:", strcasestr, get_uri_part, make_uri_string}, 1095 {"webcal://",strcasestr, get_uri_part, make_uri_string}, 1096 {"webcals://",strcasestr, get_uri_part, make_uri_string}, 1097 {"@", strcasestr, get_email_part, make_email_string} 1098 }; 1099 const gint PARSE_ELEMS = sizeof parser / sizeof parser[0]; 1100 1101 gint n; 1102 const gchar *walk, *bp, *ep; 1103 1104 struct txtpos { 1105 const gchar *bp, *ep; /* text position */ 1106 gint pti; /* index in parse table */ 1107 struct txtpos *next; /* next */ 1108 } head = {NULL, NULL, 0, NULL}, *last = &head; 1109 1110 gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, start); 1111 gtk_text_buffer_get_iter_at_offset(buffer, &end_iter, end); 1112 mybuf = gtk_text_buffer_get_text(buffer, &start_iter, &end_iter, FALSE); 1113 offset = gtk_text_iter_get_offset(&start_iter); 1114 1115 /* parse for clickable parts, and build a list of begin and end positions */ 1116 for (walk = mybuf;;) { 1117 gint last_index = PARSE_ELEMS; 1118 gchar *scanpos = NULL; 1119 1120 /* FIXME: this looks phony. scanning for anything in the parse table */ 1121 for (n = 0; n < PARSE_ELEMS; n++) { 1122 gchar *tmp; 1123 1124 tmp = parser[n].search(walk, parser[n].needle); 1125 if (tmp) { 1126 if (scanpos == NULL || tmp < scanpos) { 1127 scanpos = tmp; 1128 last_index = n; 1129 } 1130 } 1131 } 1132 1133 if (scanpos) { 1134 /* check if URI can be parsed */ 1135 if (parser[last_index].parse(walk, scanpos, &bp, &ep, FALSE) 1136 && (size_t) (ep - bp - 1) > strlen(parser[last_index].needle)) { 1137 ADD_TXT_POS_LATER(bp, ep, last_index); 1138 walk = ep; 1139 } else 1140 walk = scanpos + 1141 strlen(parser[last_index].needle); 1142 } else 1143 break; 1144 } 1145 1146 /* colorize this line */ 1147 if (head.next) { 1148 /* insert URIs */ 1149 for (last = head.next; last != NULL; last = last->next) { 1150 ClickableText *uri; 1151 gint start_offset, end_offset; 1152 gchar *tmp_str; 1153 gchar old_char; 1154 uri = g_new0(ClickableText, 1); 1155 uri->uri = parser[last->pti].build_uri(last->bp, 1156 last->ep); 1157 1158 tmp_str = mybuf; 1159 old_char = tmp_str[last->ep - mybuf]; 1160 tmp_str[last->ep - mybuf] = '\0'; 1161 end_offset = g_utf8_strlen(tmp_str, -1); 1162 tmp_str[last->ep - mybuf] = old_char; 1163 1164 old_char = tmp_str[last->bp - mybuf]; 1165 tmp_str[last->bp - mybuf] = '\0'; 1166 start_offset = g_utf8_strlen(tmp_str, -1); 1167 tmp_str[last->bp - mybuf] = old_char; 1168 1169 gtk_text_buffer_get_iter_at_offset(buffer, &start_iter, start_offset + offset); 1170 gtk_text_buffer_get_iter_at_offset(buffer, &end_iter, end_offset + offset); 1171 1172 uri->start = gtk_text_iter_get_offset(&start_iter); 1173 1174 gtk_text_buffer_apply_tag_by_name(buffer, "link", &start_iter, &end_iter); 1175 1176 uri->end = gtk_text_iter_get_offset(&end_iter); 1177 uri->filename = NULL; 1178 textview->uri_list = 1179 g_slist_prepend(textview->uri_list, uri); 1180 } 1181 } 1182 1183 g_free(mybuf); 1184 } 1185 1186 #undef ADD_TXT_POS 1187 1188 static void textview_write_line(TextView *textview, const gchar *str, 1189 CodeConverter *conv, gboolean do_quote_folding) 1190 { 1191 GtkTextView *text; 1192 GtkTextBuffer *buffer; 1193 GtkTextIter iter; 1194 gchar buf[BUFFSIZE]; 1195 gchar *fg_color; 1196 gint quotelevel = -1, real_quotelevel = -1; 1197 gchar quote_tag_str[10]; 1198 1199 text = GTK_TEXT_VIEW(textview->text); 1200 buffer = gtk_text_view_get_buffer(text); 1201 gtk_text_buffer_get_end_iter(buffer, &iter); 1202 1203 if (!conv) 1204 strncpy2(buf, str, sizeof(buf)); 1205 else if (conv_convert(conv, buf, sizeof(buf), str) < 0) 1206 conv_localetodisp(buf, sizeof(buf), str); 1207 1208 strcrchomp(buf); 1209 fg_color = NULL; 1210 1211 /* change color of quotation 1212 >, foo>, _> ... ok, <foo>, foo bar>, foo-> ... ng 1213 Up to 3 levels of quotations are detected, and each 1214 level is colored using a different color. */ 1215 if (prefs_common.enable_color && !textview->is_attachment && line_has_quote_char(buf)) { 1216 real_quotelevel = quote_depth(buf); 1217 quotelevel = real_quotelevel; 1218 /* set up the correct foreground color */ 1219 if (quotelevel > 2) { 1220 /* recycle colors */ 1221 if (prefs_common.recycle_quote_colors) 1222 quotelevel %= 3; 1223 else 1224 quotelevel = 2; 1225 } 1226 } 1227 1228 if (quotelevel == -1) 1229 fg_color = NULL; 1230 else { 1231 g_snprintf(quote_tag_str, sizeof(quote_tag_str), 1232 "quote%d", quotelevel); 1233 fg_color = quote_tag_str; 1234 } 1235 1236 if (prefs_common.enable_color) { 1237 if (textview->is_diff || textview->is_in_git_patch) { 1238 if (strncmp(buf, "+++ ", 4) == 0) 1239 fg_color = "diff-add-file"; 1240 else if (buf[0] == '+') 1241 fg_color = "diff-add"; 1242 else if (strncmp(buf, "--- ", 4) == 0) 1243 fg_color = "diff-del-file"; 1244 else if (buf[0] == '-') 1245 fg_color = "diff-del"; 1246 else if (strncmp(buf, "@@ ", 3) == 0 && 1247 strstr(&buf[3], " @@")) 1248 fg_color = "diff-hunk"; 1249 } else if (strncmp(buf, "diff --git ", 11) == 0) { 1250 textview->is_in_git_patch = TRUE; 1251 } 1252 } 1253 1254 if (!textview->is_attachment && real_quotelevel > -1 && do_quote_folding) { 1255 if (!g_utf8_validate(buf, -1, NULL)) { 1256 gchar *utf8buf = NULL; 1257 utf8buf = g_malloc(BUFFSIZE); 1258 conv_localetodisp(utf8buf, BUFFSIZE, buf); 1259 strncpy2(buf, utf8buf, BUFFSIZE-1); 1260 g_free(utf8buf); 1261 } 1262 do_quote: 1263 if ( textview->prev_quote_level != real_quotelevel ) { 1264 ClickableText *uri; 1265 uri = g_new0(ClickableText, 1); 1266 uri->uri = g_strdup(""); 1267 uri->data = g_strdup(buf); 1268 uri->data_len = strlen(uri->data); 1269 uri->start = gtk_text_iter_get_offset(&iter); 1270 uri->is_quote = TRUE; 1271 uri->quote_level = real_quotelevel; 1272 uri->fg_color = g_strdup(fg_color); 1273 1274 gtk_text_buffer_insert_with_tags_by_name 1275 (buffer, &iter, " [...]", -1, 1276 "qlink", fg_color, NULL); 1277 uri->end = gtk_text_iter_get_offset(&iter); 1278 gtk_text_buffer_insert(buffer, &iter, " \n", -1); 1279 1280 uri->filename = NULL; 1281 textview->uri_list = 1282 g_slist_prepend(textview->uri_list, uri); 1283 1284 textview->prev_quote_level = real_quotelevel; 1285 } else { 1286 GSList *last = textview->uri_list; 1287 ClickableText *lasturi = NULL; 1288 gint e_len = 0, n_len = 0; 1289 1290 if (textview->uri_list) { 1291 lasturi = (ClickableText *)last->data; 1292 } else { 1293 g_print("oops (%d %d)\n", 1294 real_quotelevel, textview->prev_quote_level); 1295 } 1296 if (lasturi) { 1297 if (lasturi->is_quote == FALSE) { 1298 textview->prev_quote_level = -1; 1299 goto do_quote; 1300 } 1301 e_len = lasturi->data ? lasturi->data_len:0; 1302 n_len = strlen(buf); 1303 lasturi->data = g_realloc((gchar *)lasturi->data, e_len + n_len + 1); 1304 strcpy((gchar *)lasturi->data + e_len, buf); 1305 *((gchar *)lasturi->data + e_len + n_len) = '\0'; 1306 lasturi->data_len += n_len; 1307 } 1308 } 1309 } else { 1310 textview_make_clickable_parts(textview, fg_color, "link", buf, FALSE); 1311 textview->prev_quote_level = -1; 1312 } 1313 } 1314 1315 void textview_write_link(TextView *textview, const gchar *str, 1316 const gchar *uri, CodeConverter *conv) 1317 { 1318 GtkTextView *text; 1319 GtkTextBuffer *buffer; 1320 GtkTextIter iter; 1321 gchar buf[BUFFSIZE]; 1322 gchar *bufp; 1323 ClickableText *r_uri; 1324 1325 if (!str || *str == '\0') 1326 return; 1327 if (!uri) 1328 return; 1329 1330 while (uri && *uri && g_ascii_isspace(*uri)) 1331 uri++; 1332 1333 text = GTK_TEXT_VIEW(textview->text); 1334 buffer = gtk_text_view_get_buffer(text); 1335 gtk_text_buffer_get_end_iter(buffer, &iter); 1336 1337 if (!conv) 1338 strncpy2(buf, str, sizeof(buf)); 1339 else if (conv_convert(conv, buf, sizeof(buf), str) < 0) 1340 conv_utf8todisp(buf, sizeof(buf), str); 1341 1342 if (g_utf8_validate(buf, -1, NULL) == FALSE) 1343 return; 1344 1345 strcrchomp(buf); 1346 1347 gtk_text_buffer_get_end_iter(buffer, &iter); 1348 for (bufp = buf; *bufp != '\0'; bufp = g_utf8_next_char(bufp)) { 1349 gunichar ch; 1350 1351 ch = g_utf8_get_char(bufp); 1352 if (!g_unichar_isspace(ch)) 1353 break; 1354 } 1355 if (bufp > buf) 1356 gtk_text_buffer_insert(buffer, &iter, buf, bufp - buf); 1357 1358 r_uri = g_new0(ClickableText, 1); 1359 r_uri->uri = g_strdup(uri); 1360 r_uri->start = gtk_text_iter_get_offset(&iter); 1361 gtk_text_buffer_insert_with_tags_by_name 1362 (buffer, &iter, bufp, -1, "link", NULL); 1363 r_uri->end = gtk_text_iter_get_offset(&iter); 1364 r_uri->filename = NULL; 1365 textview->uri_list = g_slist_prepend(textview->uri_list, r_uri); 1366 } 1367 1368 static void textview_set_cursor(GdkWindow *window, GdkCursor *cursor) 1369 { 1370 if (GDK_IS_WINDOW(window)) 1371 gdk_window_set_cursor(window, cursor); 1372 } 1373 void textview_clear(TextView *textview) 1374 { 1375 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 1376 GtkTextBuffer *buffer; 1377 GdkWindow *window = gtk_text_view_get_window(text, 1378 GTK_TEXT_WINDOW_TEXT); 1379 1380 buffer = gtk_text_view_get_buffer(text); 1381 gtk_text_buffer_set_text(buffer, "", -1); 1382 if (gtk_text_buffer_get_mark(buffer, "body_start")) 1383 gtk_text_buffer_delete_mark_by_name(buffer, "body_start"); 1384 if (gtk_text_buffer_get_mark(buffer, "body_end")) 1385 gtk_text_buffer_delete_mark_by_name(buffer, "body_end"); 1386 1387 TEXTVIEW_STATUSBAR_POP(textview); 1388 textview_uri_list_remove_all(textview->uri_list); 1389 textview->uri_list = NULL; 1390 textview->uri_hover = NULL; 1391 textview->prev_quote_level = -1; 1392 1393 textview->body_pos = 0; 1394 1395 if (textview->messageview->mainwin->cursor_count == 0) { 1396 textview_set_cursor(window, text_cursor); 1397 } else { 1398 textview_set_cursor(window, watch_cursor); 1399 } 1400 } 1401 1402 void textview_destroy(TextView *textview) 1403 { 1404 GtkTextBuffer *buffer; 1405 GtkClipboard *clipboard; 1406 1407 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text)); 1408 clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY); 1409 gtk_text_buffer_remove_selection_clipboard(buffer, clipboard); 1410 1411 textview_uri_list_remove_all(textview->uri_list); 1412 textview->uri_list = NULL; 1413 textview->prev_quote_level = -1; 1414 1415 g_free(textview); 1416 } 1417 1418 void textview_set_text(TextView *textview, const gchar *text) 1419 { 1420 GtkTextView *view; 1421 GtkTextBuffer *buffer; 1422 1423 cm_return_if_fail(textview != NULL); 1424 cm_return_if_fail(text != NULL); 1425 1426 textview_clear(textview); 1427 1428 view = GTK_TEXT_VIEW(textview->text); 1429 buffer = gtk_text_view_get_buffer(view); 1430 gtk_text_buffer_set_text(buffer, text, strlen(text)); 1431 } 1432 1433 enum 1434 { 1435 H_DATE = 0, 1436 H_FROM = 1, 1437 H_TO = 2, 1438 H_NEWSGROUPS = 3, 1439 H_SUBJECT = 4, 1440 H_CC = 5, 1441 H_REPLY_TO = 6, 1442 H_FOLLOWUP_TO = 7, 1443 H_X_MAILER = 8, 1444 H_X_NEWSREADER = 9, 1445 H_USER_AGENT = 10, 1446 H_ORGANIZATION = 11, 1447 }; 1448 1449 void textview_set_position(TextView *textview, gint pos) 1450 { 1451 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 1452 1453 gtkut_text_view_set_position(text, pos); 1454 } 1455 1456 static GPtrArray *textview_scan_header(TextView *textview, FILE *fp) 1457 { 1458 gchar buf[BUFFSIZE]; 1459 GPtrArray *headers, *sorted_headers; 1460 GSList *disphdr_list; 1461 Header *header; 1462 gint i; 1463 1464 cm_return_val_if_fail(fp != NULL, NULL); 1465 1466 if (prefs_common.show_all_headers) { 1467 headers = procheader_get_header_array(fp); 1468 sorted_headers = g_ptr_array_new(); 1469 for (i = 0; i < headers->len; i++) { 1470 header = g_ptr_array_index(headers, i); 1471 if (!procheader_header_is_internal(header->name)) 1472 g_ptr_array_add(sorted_headers, header); 1473 else 1474 procheader_header_free(header); 1475 } 1476 g_ptr_array_free(headers, TRUE); 1477 return sorted_headers; 1478 } 1479 1480 if (!prefs_common.display_header) { 1481 while (fgets(buf, sizeof(buf), fp) != NULL) 1482 if (buf[0] == '\r' || buf[0] == '\n') break; 1483 return NULL; 1484 } 1485 1486 headers = procheader_get_header_array(fp); 1487 1488 sorted_headers = g_ptr_array_new(); 1489 1490 for (disphdr_list = prefs_common.disphdr_list; disphdr_list != NULL; 1491 disphdr_list = disphdr_list->next) { 1492 DisplayHeaderProp *dp = 1493 (DisplayHeaderProp *)disphdr_list->data; 1494 1495 for (i = 0; i < headers->len; i++) { 1496 header = g_ptr_array_index(headers, i); 1497 if (procheader_headername_equal(header->name, 1498 dp->name)) { 1499 if (dp->hidden) 1500 procheader_header_free(header); 1501 else 1502 g_ptr_array_add(sorted_headers, header); 1503 1504 g_ptr_array_remove_index(headers, i); 1505 i--; 1506 } 1507 } 1508 } 1509 1510 if (prefs_common.show_other_header) { 1511 for (i = 0; i < headers->len; i++) { 1512 header = g_ptr_array_index(headers, i); 1513 unfold_line(header->body); 1514 if (!procheader_header_is_internal(header->name)) { 1515 g_ptr_array_add(sorted_headers, header); 1516 } else { 1517 procheader_header_free(header); 1518 } 1519 } 1520 g_ptr_array_free(headers, TRUE); 1521 } else 1522 procheader_header_array_destroy(headers); 1523 1524 1525 return sorted_headers; 1526 } 1527 1528 void textview_show_icon(TextView *textview, const gchar *stock_id) 1529 { 1530 GtkAllocation allocation; 1531 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 1532 gint x, wx, wy; 1533 gchar *img_sym = NULL; 1534 1535 if (textview->image) 1536 gtk_widget_destroy(textview->image); 1537 1538 img_sym = g_strconcat(stock_id, "-symbolic", NULL); 1539 textview->image = gtk_image_new_from_icon_name(img_sym, GTK_ICON_SIZE_DIALOG); 1540 g_free(img_sym); 1541 cm_return_if_fail(textview->image != NULL); 1542 1543 gtk_widget_set_name(GTK_WIDGET(textview->image), "textview_icon"); 1544 gtk_widget_show(textview->image); 1545 1546 gtk_widget_get_allocation(textview->text, &allocation); 1547 x = allocation.width - WIDTH - 5; 1548 1549 gtk_text_view_buffer_to_window_coords( 1550 GTK_TEXT_VIEW(textview->text), 1551 GTK_TEXT_WINDOW_TEXT, x, 5, &wx, &wy); 1552 1553 gtk_text_view_add_child_in_window(text, textview->image, 1554 GTK_TEXT_WINDOW_TEXT, wx, wy); 1555 1556 gtk_widget_show_all(textview->text); 1557 return; 1558 } 1559 1560 static void textview_show_header(TextView *textview, GPtrArray *headers) 1561 { 1562 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 1563 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text); 1564 GtkTextIter iter; 1565 Header *header; 1566 gint i; 1567 1568 cm_return_if_fail(headers != NULL); 1569 1570 for (i = 0; i < headers->len; i++) { 1571 header = g_ptr_array_index(headers, i); 1572 cm_return_if_fail(header->name != NULL); 1573 1574 gtk_text_buffer_get_end_iter(buffer, &iter); 1575 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, header->name, -1, "header_title", "header", NULL); 1576 if (header->name[strlen(header->name) - 1] != ' ') 1577 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, " ", 1, "header_title", "header", NULL); 1578 1579 if (procheader_headername_equal(header->name, "Subject") || 1580 procheader_headername_equal(header->name, "From") || 1581 procheader_headername_equal(header->name, "To") || 1582 procheader_headername_equal(header->name, "Cc") || 1583 procheader_headername_equal(header->name, "Bcc") || 1584 procheader_headername_equal(header->name, "Reply-To") || 1585 procheader_headername_equal(header->name, "Sender") || 1586 procheader_headername_equal(header->name, "Resent-From") || 1587 procheader_headername_equal(header->name, "Resent-To")) 1588 unfold_line(header->body); 1589 1590 if (procheader_headername_equal(header->name, "Date") && 1591 prefs_common.msgview_date_format) { 1592 gchar hbody[80]; 1593 procheader_date_parse(hbody, header->body, sizeof(hbody)); 1594 gtk_text_buffer_get_end_iter (buffer, &iter); 1595 gtk_text_buffer_insert_with_tags_by_name 1596 (buffer, &iter, hbody, -1, "header", NULL); 1597 } else { 1598 gboolean hdr = 1599 procheader_headername_equal(header->name, "From") || 1600 procheader_headername_equal(header->name, "To") || 1601 procheader_headername_equal(header->name, "Cc") || 1602 procheader_headername_equal(header->name, "Bcc") || 1603 procheader_headername_equal(header->name, "Reply-To") || 1604 procheader_headername_equal(header->name, "Sender") || 1605 procheader_headername_equal(header->name, "Resent-From") || 1606 procheader_headername_equal(header->name, "Resent-To"); 1607 textview_make_clickable_parts(textview, "header", 1608 "hlink", header->body, 1609 hdr); 1610 } 1611 gtk_text_buffer_get_end_iter (buffer, &iter); 1612 gtk_text_buffer_insert_with_tags_by_name(buffer, &iter, "\n", 1, 1613 "header", NULL); 1614 } 1615 } 1616 1617 gboolean textview_search_string(TextView *textview, const gchar *str, 1618 gboolean case_sens) 1619 { 1620 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 1621 1622 return gtkut_text_view_search_string(text, str, case_sens); 1623 } 1624 1625 gboolean textview_search_string_backward(TextView *textview, const gchar *str, 1626 gboolean case_sens) 1627 { 1628 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 1629 1630 return gtkut_text_view_search_string_backward(text, str, case_sens); 1631 } 1632 1633 static gboolean textview_motion_notify(GtkWidget *widget, 1634 GdkEventMotion *event, 1635 TextView *textview) 1636 { 1637 if (textview->loading) 1638 return FALSE; 1639 textview_uri_update(textview, event->x, event->y); 1640 1641 return FALSE; 1642 } 1643 1644 static gboolean textview_leave_notify(GtkWidget *widget, 1645 GdkEventCrossing *event, 1646 TextView *textview) 1647 { 1648 if (textview->loading) 1649 return FALSE; 1650 textview_uri_update(textview, -1, -1); 1651 1652 return FALSE; 1653 } 1654 1655 static gboolean textview_visibility_notify(GtkWidget *widget, 1656 GdkEventVisibility *event, 1657 TextView *textview) 1658 { 1659 gint wx, wy; 1660 GdkWindow *window; 1661 GdkDisplay *display; 1662 GdkSeat *seat; 1663 1664 if (textview->loading) 1665 return FALSE; 1666 1667 window = gtk_text_view_get_window(GTK_TEXT_VIEW(widget), 1668 GTK_TEXT_WINDOW_TEXT); 1669 1670 /* check if occurred for the text window part */ 1671 if (window != event->window) 1672 return FALSE; 1673 1674 display = gdk_window_get_display(window); 1675 seat = gdk_display_get_default_seat(display); 1676 gdk_device_get_position(gdk_seat_get_pointer(seat), 1677 NULL, &wx, &wy); 1678 textview_uri_update(textview, wx, wy); 1679 1680 return FALSE; 1681 } 1682 1683 void textview_cursor_wait(TextView *textview) 1684 { 1685 GdkWindow *window = gtk_text_view_get_window( 1686 GTK_TEXT_VIEW(textview->text), 1687 GTK_TEXT_WINDOW_TEXT); 1688 textview_set_cursor(window, watch_cursor); 1689 } 1690 1691 void textview_cursor_normal(TextView *textview) 1692 { 1693 GdkWindow *window = gtk_text_view_get_window( 1694 GTK_TEXT_VIEW(textview->text), 1695 GTK_TEXT_WINDOW_TEXT); 1696 textview_set_cursor(window, text_cursor); 1697 } 1698 1699 static void textview_uri_update(TextView *textview, gint x, gint y) 1700 { 1701 GtkTextBuffer *buffer; 1702 GtkTextIter start_iter, end_iter; 1703 ClickableText *uri = NULL; 1704 1705 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text)); 1706 1707 if (x != -1 && y != -1) { 1708 gint bx, by; 1709 GtkTextIter iter; 1710 GSList *tags; 1711 GSList *cur; 1712 1713 gtk_text_view_window_to_buffer_coords(GTK_TEXT_VIEW(textview->text), 1714 GTK_TEXT_WINDOW_WIDGET, 1715 x, y, &bx, &by); 1716 gtk_text_view_get_iter_at_location(GTK_TEXT_VIEW(textview->text), 1717 &iter, bx, by); 1718 1719 tags = gtk_text_iter_get_tags(&iter); 1720 for (cur = tags; cur != NULL; cur = cur->next) { 1721 GtkTextTag *tag = cur->data; 1722 char *name; 1723 1724 g_object_get(G_OBJECT(tag), "name", &name, NULL); 1725 1726 if ((!strcmp(name, "link") || !strcmp(name, "hlink")) 1727 && textview_get_uri_range(textview, &iter, tag, 1728 &start_iter, &end_iter)) { 1729 1730 uri = textview_get_uri_from_range(textview, 1731 &iter, tag, 1732 &start_iter, 1733 &end_iter); 1734 } 1735 g_free(name); 1736 if (uri) 1737 break; 1738 } 1739 g_slist_free(tags); 1740 } 1741 1742 if (uri != textview->uri_hover) { 1743 GdkWindow *window; 1744 1745 if (textview->uri_hover) 1746 gtk_text_buffer_remove_tag_by_name(buffer, 1747 "link-hover", 1748 &textview->uri_hover_start_iter, 1749 &textview->uri_hover_end_iter); 1750 1751 textview->uri_hover = uri; 1752 if (uri) { 1753 textview->uri_hover_start_iter = start_iter; 1754 textview->uri_hover_end_iter = end_iter; 1755 } 1756 1757 window = gtk_text_view_get_window(GTK_TEXT_VIEW(textview->text), 1758 GTK_TEXT_WINDOW_TEXT); 1759 if (textview->messageview->mainwin->cursor_count == 0) { 1760 textview_set_cursor(window, uri ? hand_cursor : text_cursor); 1761 } else { 1762 textview_set_cursor(window, watch_cursor); 1763 } 1764 1765 TEXTVIEW_STATUSBAR_POP(textview); 1766 1767 if (uri) { 1768 if (!uri->is_quote) 1769 gtk_text_buffer_apply_tag_by_name(buffer, 1770 "link-hover", 1771 &start_iter, 1772 &end_iter); 1773 TEXTVIEW_STATUSBAR_PUSH(textview, uri->uri); 1774 } 1775 } 1776 } 1777 1778 static gboolean textview_get_uri_range(TextView *textview, 1779 GtkTextIter *iter, 1780 GtkTextTag *tag, 1781 GtkTextIter *start_iter, 1782 GtkTextIter *end_iter) 1783 { 1784 return get_tag_range(iter, tag, start_iter, end_iter); 1785 } 1786 1787 static ClickableText *textview_get_uri_from_range(TextView *textview, 1788 GtkTextIter *iter, 1789 GtkTextTag *tag, 1790 GtkTextIter *start_iter, 1791 GtkTextIter *end_iter) 1792 { 1793 gint start_pos, end_pos, cur_pos; 1794 ClickableText *uri = NULL; 1795 GSList *cur; 1796 1797 start_pos = gtk_text_iter_get_offset(start_iter); 1798 end_pos = gtk_text_iter_get_offset(end_iter); 1799 cur_pos = gtk_text_iter_get_offset(iter); 1800 1801 for (cur = textview->uri_list; cur != NULL; cur = cur->next) { 1802 ClickableText *uri_ = (ClickableText *)cur->data; 1803 if (start_pos == uri_->start && 1804 end_pos == uri_->end) { 1805 uri = uri_; 1806 break; 1807 } 1808 } 1809 for (cur = textview->uri_list; uri == NULL && cur != NULL; cur = cur->next) { 1810 ClickableText *uri_ = (ClickableText *)cur->data; 1811 if (start_pos == uri_->start || 1812 end_pos == uri_->end) { 1813 /* in case of contiguous links, textview_get_uri_range 1814 * returns a broader range (start of 1st link to end 1815 * of last link). 1816 * In that case, correct link is the one covering 1817 * current iter. 1818 */ 1819 if (uri_->start <= cur_pos && cur_pos <= uri_->end) { 1820 uri = uri_; 1821 break; 1822 } 1823 } 1824 } 1825 1826 return uri; 1827 } 1828 1829 static ClickableText *textview_get_uri(TextView *textview, 1830 GtkTextIter *iter, 1831 GtkTextTag *tag) 1832 { 1833 GtkTextIter start_iter, end_iter; 1834 ClickableText *uri = NULL; 1835 1836 if (textview_get_uri_range(textview, iter, tag, &start_iter, 1837 &end_iter)) 1838 uri = textview_get_uri_from_range(textview, iter, tag, 1839 &start_iter, &end_iter); 1840 1841 return uri; 1842 } 1843 1844 static void textview_shift_uris_after(TextView *textview, GSList *start_list, gint start, gint shift) 1845 { 1846 GSList *cur; 1847 if (!start_list) 1848 start_list = textview->uri_list; 1849 1850 for (cur = start_list; cur; cur = cur->next) { 1851 ClickableText *uri = (ClickableText *)cur->data; 1852 if (uri->start <= start) 1853 continue; 1854 uri->start += shift; 1855 uri->end += shift; 1856 } 1857 } 1858 1859 static void textview_remove_uris_in(TextView *textview, gint start, gint end) 1860 { 1861 GSList *cur; 1862 for (cur = textview->uri_list; cur; ) { 1863 ClickableText *uri = (ClickableText *)cur->data; 1864 if (uri->start > start && uri->end < end) { 1865 cur = cur->next; 1866 textview->uri_list = g_slist_remove(textview->uri_list, uri); 1867 g_free(uri->uri); 1868 g_free(uri->filename); 1869 if (uri->is_quote) { 1870 g_free(uri->fg_color); 1871 g_free(uri->data); 1872 /* (only free data in quotes uris) */ 1873 } 1874 g_free(uri); 1875 } else { 1876 cur = cur->next; 1877 } 1878 1879 } 1880 } 1881 1882 static void textview_toggle_quote(TextView *textview, GSList *start_list, ClickableText *uri, gboolean expand_only) 1883 { 1884 GtkTextIter start, end; 1885 GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text)); 1886 1887 if (!uri->is_quote) 1888 return; 1889 1890 if (uri->q_expanded && expand_only) 1891 return; 1892 1893 gtk_text_buffer_get_iter_at_offset(buffer, &start, uri->start); 1894 gtk_text_buffer_get_iter_at_offset(buffer, &end, uri->end); 1895 if (textview->uri_hover) 1896 gtk_text_buffer_remove_tag_by_name(buffer, 1897 "link-hover", 1898 &textview->uri_hover_start_iter, 1899 &textview->uri_hover_end_iter); 1900 textview->uri_hover = NULL; 1901 gtk_text_buffer_remove_tag_by_name(buffer, 1902 "qlink", 1903 &start, 1904 &end); 1905 /* when shifting URIs start and end, we have to do it per-UTF8-char 1906 * so use g_utf8_strlen(). OTOH, when inserting in the text buffer, 1907 * we have to pass a number of bytes, so use strlen(). disturbing. */ 1908 1909 if (!uri->q_expanded) { 1910 gtk_text_buffer_get_iter_at_offset(buffer, &start, uri->start); 1911 gtk_text_buffer_get_iter_at_offset(buffer, &end, uri->end); 1912 textview_shift_uris_after(textview, start_list, uri->start, 1913 g_utf8_strlen((gchar *)uri->data, -1)-strlen(" [...]\n")); 1914 gtk_text_buffer_delete(buffer, &start, &end); 1915 gtk_text_buffer_get_iter_at_offset(buffer, &start, uri->start); 1916 gtk_text_buffer_insert_with_tags_by_name 1917 (buffer, &start, (gchar *)uri->data, 1918 strlen((gchar *)uri->data)-1, 1919 "qlink", (gchar *)uri->fg_color, NULL); 1920 uri->end = gtk_text_iter_get_offset(&start); 1921 textview_make_clickable_parts_later(textview, 1922 uri->start, uri->end); 1923 uri->q_expanded = TRUE; 1924 } else { 1925 gtk_text_buffer_get_iter_at_offset(buffer, &start, uri->start); 1926 gtk_text_buffer_get_iter_at_offset(buffer, &end, uri->end); 1927 textview_remove_uris_in(textview, uri->start, uri->end); 1928 textview_shift_uris_after(textview, start_list, uri->start, 1929 strlen(" [...]\n")-g_utf8_strlen((gchar *)uri->data, -1)); 1930 gtk_text_buffer_delete(buffer, &start, &end); 1931 gtk_text_buffer_get_iter_at_offset(buffer, &start, uri->start); 1932 gtk_text_buffer_insert_with_tags_by_name 1933 (buffer, &start, " [...]", -1, 1934 "qlink", (gchar *)uri->fg_color, NULL); 1935 uri->end = gtk_text_iter_get_offset(&start); 1936 uri->q_expanded = FALSE; 1937 } 1938 if (textview->messageview->mainwin->cursor_count == 0) { 1939 textview_cursor_normal(textview); 1940 } else { 1941 textview_cursor_wait(textview); 1942 } 1943 } 1944 static gboolean textview_uri_button_pressed(GtkTextTag *tag, GObject *obj, 1945 GdkEvent *event, GtkTextIter *iter, 1946 TextView *textview) 1947 { 1948 GdkEventButton *bevent; 1949 ClickableText *uri = NULL; 1950 char *tagname; 1951 gboolean qlink = FALSE; 1952 1953 if (!event) 1954 return FALSE; 1955 1956 if (event->type != GDK_BUTTON_PRESS && event->type != GDK_2BUTTON_PRESS 1957 && event->type != GDK_MOTION_NOTIFY) 1958 return FALSE; 1959 1960 uri = textview_get_uri(textview, iter, tag); 1961 if (!uri) 1962 return FALSE; 1963 1964 g_object_get(G_OBJECT(tag), "name", &tagname, NULL); 1965 1966 if (!strcmp(tagname, "qlink")) 1967 qlink = TRUE; 1968 1969 g_free(tagname); 1970 1971 bevent = (GdkEventButton *) event; 1972 1973 /* doubleclick: open compose / add address / browser */ 1974 if (qlink && event->type == GDK_BUTTON_PRESS && bevent->button != 1) { 1975 /* pass rightclick through */ 1976 return FALSE; 1977 } else if ((event->type == (qlink ? GDK_2BUTTON_PRESS:GDK_BUTTON_PRESS) && bevent->button == 1) || 1978 bevent->button == 2 || bevent->button == 3) { 1979 if (uri->filename && !g_ascii_strncasecmp(uri->filename, "cm://", 5)) { 1980 MimeView *mimeview = 1981 (textview->messageview)? 1982 textview->messageview->mimeview:NULL; 1983 if (mimeview && bevent->button == 1) { 1984 mimeview_handle_cmd(mimeview, uri->filename, NULL, uri->data); 1985 } else if (mimeview && bevent->button == 2 && 1986 !g_ascii_strcasecmp(uri->filename, "cm://select_attachment")) { 1987 mimeview_handle_cmd(mimeview, "cm://open_attachment", NULL, uri->data); 1988 } else if (mimeview && bevent->button == 3 && 1989 !g_ascii_strcasecmp(uri->filename, "cm://select_attachment")) { 1990 mimeview_handle_cmd(mimeview, "cm://menu_attachment", bevent, uri->data); 1991 } 1992 return TRUE; 1993 } else if (qlink && bevent->button == 1) { 1994 if (prefs_common.hide_quoted) { 1995 textview_toggle_quote(textview, NULL, uri, FALSE); 1996 return TRUE; 1997 } else 1998 return FALSE; 1999 } else if (!g_ascii_strncasecmp(uri->uri, "mailto:", 7)) { 2000 if (bevent->button == 3) { 2001 g_object_set_data( 2002 G_OBJECT(textview->mail_popup_menu), 2003 "menu_button", uri); 2004 gtk_menu_popup_at_pointer(GTK_MENU(textview->mail_popup_menu), NULL); 2005 } else { 2006 PrefsAccount *account = NULL; 2007 FolderItem *folder_item = NULL; 2008 2009 if (textview->messageview && textview->messageview->msginfo && 2010 textview->messageview->msginfo->folder) { 2011 folder_item = textview->messageview->msginfo->folder; 2012 if (folder_item->prefs && folder_item->prefs->enable_default_account) 2013 account = account_find_from_id(folder_item->prefs->default_account); 2014 if (!account) 2015 account = account_find_from_item(folder_item); 2016 } 2017 compose_new_with_folderitem(account, folder_item, uri->uri + 7); 2018 } 2019 return TRUE; 2020 } else if (g_ascii_strncasecmp(uri->uri, "file:", 5)) { 2021 if (bevent->button == 1 && 2022 textview_uri_security_check(textview, uri, FALSE) == TRUE) 2023 open_uri(uri->uri, 2024 prefs_common_get_uri_cmd()); 2025 else if (bevent->button == 3 && !qlink) { 2026 g_object_set_data( 2027 G_OBJECT(textview->link_popup_menu), 2028 "menu_button", uri); 2029 gtk_menu_popup_at_pointer(GTK_MENU(textview->link_popup_menu), NULL); 2030 } 2031 return TRUE; 2032 } else { 2033 if (bevent->button == 3 && !qlink) { 2034 g_object_set_data( 2035 G_OBJECT(textview->file_popup_menu), 2036 "menu_button", uri); 2037 gtk_menu_popup_at_pointer(GTK_MENU(textview->file_popup_menu), NULL); 2038 return TRUE; 2039 } 2040 } 2041 } 2042 2043 return FALSE; 2044 } 2045 2046 gchar *textview_get_visible_uri (TextView *textview, 2047 ClickableText *uri) 2048 { 2049 GtkTextBuffer *buffer; 2050 GtkTextIter start, end; 2051 2052 buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(textview->text)); 2053 2054 gtk_text_buffer_get_iter_at_offset(buffer, &start, uri->start); 2055 gtk_text_buffer_get_iter_at_offset(buffer, &end, uri->end); 2056 2057 return gtk_text_buffer_get_text(buffer, &start, &end, FALSE); 2058 } 2059 2060 /*! 2061 *\brief Check to see if a web URL has been disguised as a different 2062 * URL (possible with HTML email). 2063 * 2064 *\param uri The uri to check 2065 * 2066 *\param textview The TextView the URL is contained in 2067 * 2068 *\return gboolean TRUE if the URL is ok, or if the user chose to open 2069 * it anyway, otherwise FALSE 2070 */ 2071 gboolean textview_uri_security_check(TextView *textview, ClickableText *uri, gboolean copied) 2072 { 2073 gchar *visible_str; 2074 gboolean retval = TRUE; 2075 2076 if (is_uri_string(uri->uri) == FALSE) 2077 return FALSE; 2078 2079 visible_str = textview_get_visible_uri(textview, uri); 2080 if (visible_str == NULL) 2081 return TRUE; 2082 2083 g_strstrip(visible_str); 2084 2085 if (strcmp(visible_str, uri->uri) != 0 && is_uri_string(visible_str)) { 2086 gchar *uri_path; 2087 gchar *visible_uri_path; 2088 2089 uri_path = get_uri_path(uri->uri); 2090 visible_uri_path = get_uri_path(visible_str); 2091 if (path_cmp(uri_path, visible_uri_path) != 0) 2092 retval = FALSE; 2093 } 2094 2095 if (retval == FALSE) { 2096 gchar *open_or_cp; 2097 gchar *open_or_cp_btn; 2098 gchar *msg; 2099 AlertValue aval; 2100 2101 open_or_cp = copied? _("Copy it anyway?") : _("Open it anyway?"); 2102 open_or_cp_btn = copied? _("Co_py URL") : _("_Open URL"); 2103 2104 msg = g_markup_printf_escaped("%s\n\n" 2105 "<b>%s</b> %s\n\n" 2106 "<b>%s</b> %s\n\n" 2107 "%s", 2108 _("The real URL is different from the displayed URL."), 2109 _("Displayed URL:"), visible_str, 2110 _("Real URL:"), uri->uri, 2111 open_or_cp); 2112 aval = alertpanel_full(_("Phishing attempt warning"), msg, 2113 NULL, _("_Cancel"), NULL, open_or_cp_btn, NULL, NULL, 2114 ALERTFOCUS_FIRST, FALSE, NULL, ALERT_WARNING); 2115 g_free(msg); 2116 if (aval == G_ALERTALTERNATE) 2117 retval = TRUE; 2118 } 2119 if (strlen(uri->uri) > get_uri_len(uri->uri)) 2120 retval = FALSE; 2121 2122 g_free(visible_str); 2123 2124 return retval; 2125 } 2126 2127 static void textview_uri_list_remove_all(GSList *uri_list) 2128 { 2129 GSList *cur; 2130 2131 for (cur = uri_list; cur != NULL; cur = cur->next) { 2132 if (cur->data) { 2133 g_free(((ClickableText *)cur->data)->uri); 2134 g_free(((ClickableText *)cur->data)->filename); 2135 if (((ClickableText *)cur->data)->is_quote) { 2136 g_free(((ClickableText *)cur->data)->fg_color); 2137 g_free(((ClickableText *)cur->data)->data); 2138 /* (only free data in quotes uris) */ 2139 } 2140 g_free(cur->data); 2141 } 2142 } 2143 2144 g_slist_free(uri_list); 2145 } 2146 2147 static void open_uri_cb (GtkAction *action, TextView *textview) 2148 { 2149 ClickableText *uri = g_object_get_data(G_OBJECT(textview->link_popup_menu), 2150 "menu_button"); 2151 const gchar *raw_url = g_object_get_data(G_OBJECT(textview->link_popup_menu), 2152 "raw_url"); 2153 2154 if (uri) { 2155 if (textview_uri_security_check(textview, uri, FALSE) == TRUE) 2156 open_uri(uri->uri, 2157 prefs_common_get_uri_cmd()); 2158 g_object_set_data(G_OBJECT(textview->link_popup_menu), "menu_button", 2159 NULL); 2160 } 2161 if (raw_url) { 2162 open_uri(raw_url, prefs_common_get_uri_cmd()); 2163 g_object_set_data(G_OBJECT(textview->link_popup_menu), "raw_url", 2164 NULL); 2165 } 2166 } 2167 2168 static void copy_uri_cb (GtkAction *action, TextView *textview) 2169 { 2170 ClickableText *uri = g_object_get_data(G_OBJECT(textview->link_popup_menu), 2171 "menu_button"); 2172 const gchar *raw_url = g_object_get_data(G_OBJECT(textview->link_popup_menu), 2173 "raw_url"); 2174 if (uri) { 2175 if (textview_uri_security_check(textview, uri, TRUE) == TRUE) { 2176 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), uri->uri, -1); 2177 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), uri->uri, -1); 2178 g_object_set_data(G_OBJECT(textview->link_popup_menu), "menu_button", NULL); 2179 } 2180 } 2181 if (raw_url) { 2182 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), raw_url, -1); 2183 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), raw_url, -1); 2184 g_object_set_data(G_OBJECT(textview->link_popup_menu), "raw_url", NULL); 2185 } 2186 } 2187 2188 static void add_uri_to_addrbook_cb (GtkAction *action, TextView *textview) 2189 { 2190 gchar *fromname, *fromaddress; 2191 ClickableText *uri = g_object_get_data(G_OBJECT(textview->mail_popup_menu), 2192 "menu_button"); 2193 2194 if (uri == NULL) 2195 return; 2196 2197 /* extract url */ 2198 fromaddress = g_strdup(uri->uri + 7); 2199 2200 fromname = procheader_get_fromname(fromaddress); 2201 extract_address(fromaddress); 2202 2203 addressbook_add_contact( fromname, fromaddress, NULL, NULL); 2204 2205 g_free(fromaddress); 2206 g_free(fromname); 2207 } 2208 2209 static void reply_to_uri_cb (GtkAction *action, TextView *textview) 2210 { 2211 ClickableText *uri = g_object_get_data(G_OBJECT(textview->mail_popup_menu), 2212 "menu_button"); 2213 if (!textview->messageview || !uri) 2214 return; 2215 2216 compose_reply_to_address (textview->messageview, 2217 textview->messageview->msginfo, uri->uri+7); 2218 } 2219 2220 static void mail_to_uri_cb(GtkAction *action, TextView *textview) 2221 { 2222 fprintf(stderr, "TODO(otl): no-op mail_to_uri_cb called, delete me\n"); 2223 } 2224 2225 static void copy_mail_to_uri_cb (GtkAction *action, TextView *textview) 2226 { 2227 ClickableText *uri = g_object_get_data(G_OBJECT(textview->mail_popup_menu), 2228 "menu_button"); 2229 if (uri == NULL) 2230 return; 2231 2232 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_PRIMARY), uri->uri +7, -1); 2233 gtk_clipboard_set_text(gtk_clipboard_get(GDK_SELECTION_CLIPBOARD), uri->uri +7, -1); 2234 g_object_set_data(G_OBJECT(textview->mail_popup_menu), "menu_button", 2235 NULL); 2236 } 2237 2238 void textview_get_selection_offsets(TextView *textview, gint *sel_start, gint *sel_end) 2239 { 2240 GtkTextView *text = GTK_TEXT_VIEW(textview->text); 2241 GtkTextBuffer *buffer = gtk_text_view_get_buffer(text); 2242 GtkTextIter start, end; 2243 if (gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) { 2244 if (sel_start) 2245 *sel_start = gtk_text_iter_get_offset(&start); 2246 if (sel_end) 2247 *sel_end = gtk_text_iter_get_offset(&end); 2248 } else { 2249 if (sel_start) 2250 *sel_start = -1; 2251 if (sel_end) 2252 *sel_end = -1; 2253 } 2254 }