conversation-viewer.vala 47.2 KB
Newer Older
1
/* Copyright 2011-2012 Yorba Foundation
Jim Nelson's avatar
Jim Nelson committed
2 3 4 5 6
 *
 * This software is licensed under the GNU Lesser General Public License
 * (version 2.1 or later).  See the COPYING file in this distribution. 
 */

7
public class ConversationViewer : Gtk.Box {
8 9 10 11 12 13
    public const Geary.Email.Field REQUIRED_FIELDS =
        Geary.Email.Field.HEADER
        | Geary.Email.Field.BODY
        | Geary.Email.Field.ORIGINATORS
        | Geary.Email.Field.RECEIVERS
        | Geary.Email.Field.SUBJECT
14
        | Geary.Email.Field.DATE
15
        | Geary.Email.Field.FLAGS
16
        | Geary.Email.Field.PREVIEW;
17
    
18
    private const int ATTACHMENT_PREVIEW_SIZE = 50;
19 20
    private const string MESSAGE_CONTAINER_ID = "message_container";
    private const string SELECTION_COUNTER_ID = "multiple_messages";
21
    
22 23
    // Fired when the user clicks a link.
    public signal void link_selected(string link);
24
    
25
    // Fired when the user clicks "reply" in the message menu.
26
    public signal void reply_to_message(Geary.Email message);
27 28

    // Fired when the user clicks "reply all" in the message menu.
29
    public signal void reply_all_message(Geary.Email message);
30 31

    // Fired when the user clicks "forward" in the message menu.
32
    public signal void forward_message(Geary.Email message);
33

34
    // Fired when the user marks a message.
35
    public signal void mark_message(Geary.Email message, Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove);
36

37 38 39 40 41 42
    // Fired when the user opens an attachment.
    public signal void open_attachment(Geary.Attachment attachment);

    // Fired when the user wants to save one or more attachments.
    public signal void save_attachments(Gee.List<Geary.Attachment> attachment);

43
    // List of emails in this view.
44
    public Gee.TreeSet<Geary.Email> messages { get; private set; default = 
45
        new Gee.TreeSet<Geary.Email>((CompareFunc<Geary.Email>) Geary.Email.compare_date_ascending); }
46
    
47
    // The HTML viewer to view the emails.
48
    public ConversationWebView web_view { get; private set; }
49 50 51 52 53 54 55
    
    // The Info Bar to be shown when an external image is blocked.
    public Gtk.InfoBar external_images_info_bar { get; private set; }
    
    // Label for displaying overlay messages.
    private Gtk.Label message_overlay_label;
    
56 57
    // Maps emails to their corresponding elements.
    private Gee.HashMap<Geary.EmailIdentifier, WebKit.DOM.HTMLElement> email_to_element = new
58 59
        Gee.HashMap<Geary.EmailIdentifier, WebKit.DOM.HTMLElement>(Geary.Hashable.hash_func,
        Geary.Equalable.equal_func);
60
    
61 62
    private string? hover_url = null;
    private Gtk.Menu? context_menu = null;
63
    private Gtk.Menu? message_menu = null;
64
    private Gtk.Menu? attachment_menu = null;
65
    private weak Geary.Folder? current_folder = null;
66
    private Geary.AccountSettings? current_settings = null;
67
    
68
    public ConversationViewer() {
69
        Object(orientation: Gtk.Orientation.VERTICAL, spacing: 0);
70 71 72 73 74 75 76 77 78 79 80 81 82
        
        external_images_info_bar = new Gtk.InfoBar.with_buttons(
            _("_Show Images"), Gtk.ResponseType.OK, _("_Cancel"), Gtk.ResponseType.CANCEL);
        external_images_info_bar.no_show_all = true;
        external_images_info_bar.response.connect(on_external_images_info_bar_response);
        external_images_info_bar.message_type = Gtk.MessageType.WARNING;
        Gtk.Box? external_images_info_bar_content_area =
            external_images_info_bar.get_content_area() as Gtk.Box;
        if (external_images_info_bar_content_area != null) {
            Gtk.Label label = new Gtk.Label(_("This message contains images. Do you want to show them?"));
            external_images_info_bar_content_area.add(label);
            label.show_all();
        }
83
        pack_start(external_images_info_bar, false, false);
84
        
85
        web_view = new ConversationWebView();
86
        
87 88
        web_view.hovering_over_link.connect(on_hovering_over_link);
        
89 90
        web_view.image_load_requested.connect(on_image_load_requested);
        web_view.link_selected.connect((link) => { link_selected(link); });
91
        
92 93 94
        Gtk.ScrolledWindow conversation_viewer_scrolled = new Gtk.ScrolledWindow(null, null);
        conversation_viewer_scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
        conversation_viewer_scrolled.add(web_view);
95 96
        
        Gtk.Overlay message_overlay = new Gtk.Overlay();
97
        message_overlay.add(conversation_viewer_scrolled);
98 99 100 101 102 103 104
        
        message_overlay_label = new Gtk.Label(null);
        message_overlay_label.ellipsize = Pango.EllipsizeMode.MIDDLE;
        message_overlay_label.halign = Gtk.Align.START;
        message_overlay_label.valign = Gtk.Align.END;
        message_overlay.add_overlay(message_overlay_label);
        
105
        pack_start(message_overlay);
106 107
    }
    
108 109
    private void on_image_load_requested() {
        external_images_info_bar.show();
110 111
    }
    
112
    private void on_external_images_info_bar_response(Gtk.InfoBar sender, int response_id) {
113
        web_view.apply_load_external_images(response_id == Gtk.ResponseType.OK);
114
        sender.hide();
115
    }
116 117 118 119 120
    
    public Geary.Email? get_last_message() {
        return messages.is_empty ? null : messages.last();
    }
    
121
    // Removes all displayed e-mails from the view.
122
    public void clear(Geary.Folder? new_folder, Geary.AccountSettings? settings) {
123 124 125 126 127 128 129 130 131 132
        // Remove all messages from DOM.
        try {
            foreach (WebKit.DOM.HTMLElement element in email_to_element.values) {
                if (element.get_parent_element() != null)
                    element.get_parent_element().remove_child(element);
            }
        } catch (Error e) {
            debug("Error clearing message viewer: %s", e.message);
        }
        email_to_element.clear();
133
        messages.clear();
134 135
        
        current_folder = new_folder;
136
        current_settings = settings;
137 138
    }
    
139 140 141 142 143
    // Converts an email ID into HTML ID used by the <div> for the email.
    private string get_div_id(Geary.EmailIdentifier id) {
        return "message_%s".printf(id.to_string());
    }
    
144 145
    public void show_multiple_selected(uint selected_count) {
        // Remove any messages and hide the message container, then show the counter.
146
        clear(current_folder, current_settings);
147
        try {
148 149
            web_view.hide_element_by_id(MESSAGE_CONTAINER_ID);
            web_view.show_element_by_id(SELECTION_COUNTER_ID);
150 151 152
            
            // Update the counter's count.
            WebKit.DOM.HTMLElement counter =
153
                web_view.get_dom_document().get_element_by_id("selection_counter") as WebKit.DOM.HTMLElement;
154 155 156 157 158
            if (selected_count == 0) {
                counter.set_inner_html(_("No conversations selected."));
            } else {
                counter.set_inner_html(_("%u conversations selected.").printf(selected_count));
            }
159
        } catch (Error e) {
160
            debug("Error updating counter: %s", e.message);
161 162 163
        }
    }
    
164
    public void add_message(Geary.Email email) {
165
        web_view.apply_load_external_images(false);
166
        
167 168
        // Make sure the message container is showing and the multi-message counter hidden.
        try {
169 170
            web_view.show_element_by_id(MESSAGE_CONTAINER_ID);
            web_view.hide_element_by_id(SELECTION_COUNTER_ID);
171 172 173 174
        } catch (Error e) {
            debug("Error showing/hiding containers: %s", e.message);
        }

175 176
        if (messages.contains(email))
            return;
177
        
178
        string message_id = get_div_id(email.id);
179
        string header = "";
180
        
181
        WebKit.DOM.Node insert_before = web_view.container.get_last_child();
182
        
183 184 185
        messages.add(email);
        Geary.Email? higher = messages.higher(email);
        if (higher != null)
186
            insert_before = web_view.get_dom_document().get_element_by_id(get_div_id(higher.id));
187
        
188
        WebKit.DOM.HTMLElement div_email_container;
189
        WebKit.DOM.HTMLElement div_message;
190
        try {
191 192
            // The HTML is like this:
            // <div id="$MESSAGE_ID" class="email">
193
            //     <div class="geary_spacer"></div>
194
            //     <div class="email_container">
195 196 197 198 199
            //         <div class="button_bar">
            //             <div class="starred button"><img class="icon" /></div>
            //             <div class="unstarred button"><img class="icon" /></div>
            //             <div class="menu button"><img class="icon" /></div>
            //         </div>
200 201 202
            //         <table>$HEADER</table>
            //         <span>
            //             $EMAIL_BODY
203
            //
204
            //             <div class="signature">$SIGNATURE</div>
205
            //
206 207 208 209 210 211
            //             <div class="quote_container controllable">
            //                 <div class="shower">[show]</div>
            //                 <div class="hider">[hide]</div>
            //                 <div class="quote">$QUOTE</div>
            //             </div>
            //         </span>
212 213
            //     </div>
            // </div>
214
            div_message = Util.DOM.clone_select(web_view.get_dom_document(), "#email_template");
215
            div_message.set_attribute("id", message_id);
216
            web_view.container.insert_before(div_message, insert_before);
217
            div_email_container = Util.DOM.select(div_message, "div.email_container");
218 219 220
            if (email.is_unread() == Geary.Trillian.FALSE) {
                div_message.get_class_list().add("hide");
            }
221 222
        } catch (Error setup_error) {
            warning("Error setting up webkit: %s", setup_error.message);
223 224
            
            return;
225 226
        }
        
227 228
        email_to_element.set(email.id, div_message);
        
229 230
        insert_header_address(ref header, _("From:"), email.from != null ? email.from : email.sender,
            true);
231
        
232
        // Only include to string if it's not just this account.
Eric Gregory's avatar
Eric Gregory committed
233
        // TODO: multiple accounts.
234 235
        if (email.to != null && current_settings != null) {
            if (!(email.to.get_all().size == 1 && email.to.get_all().get(0).address == current_settings.email.address))
236
                 insert_header_address(ref header, _("To:"), email.to);
237
        }
238 239 240 241 242 243 244 245

        if (email.cc != null) {
            insert_header_address(ref header, _("Cc:"), email.cc);
        }

        if (email.bcc != null) {
            insert_header_address(ref header, _("Bcc:"), email.bcc);
        }
246 247
            
        if (email.subject != null)
248
            insert_header(ref header, _("Subject:"), email.get_subject_as_string());
249 250
            
        if (email.date != null)
251
            insert_header_date(ref header, _("Date:"), email.date.value, true);
252 253

        // Add the avatar.
254 255 256 257 258 259 260 261 262 263
        Geary.RFC822.MailboxAddress? primary = email.get_primary_originator();
        if (primary != null) {
            try {
                WebKit.DOM.HTMLImageElement icon = Util.DOM.select(div_message, ".avatar")
                    as WebKit.DOM.HTMLImageElement;
                icon.set_attribute("src",
                    Gravatar.get_image_uri(primary, Gravatar.Default.MYSTERY_MAN, 48));
            } catch (Error error) {
                warning("Failed to load avatar: %s", error.message);
            }
264
        }
265
        
266 267
        // Insert the preview text.
        try {
268 269
            WebKit.DOM.HTMLElement preview =
                Util.DOM.select(div_message, ".header_container .preview");
270 271 272 273 274 275 276
            string preview_str = email.get_preview_as_string();
            if (preview_str.length == Geary.Email.MAX_PREVIEW_BYTES) {
                preview_str += "…";
            }
            preview.set_inner_text(Geary.String.reduce_whitespace(preview_str));
        } catch (Error error) {
            debug("Failed to add preview text: %s", error.message);
277 278
        }

279
        string body_text = "";
280
        try {
281
            body_text = email.get_message().get_first_mime_part_of_content_type("text/html").to_string();
282
            body_text = insert_html_markup(body_text, email);
283
        } catch (Error err) {
284
            try {
285
                body_text = linkify_and_escape_plain_text(email.get_message().
286
                    get_first_mime_part_of_content_type("text/plain").to_string());
287
                body_text = insert_plain_text_markup(body_text);
288 289 290
            } catch (Error err2) {
                debug("Could not get message text. %s", err2.message);
            }
291
        }
292

293
        // Graft header and email body into the email container.
294
        try {
295 296
            WebKit.DOM.HTMLElement table_header =
                Util.DOM.select(div_email_container, ".header_container .header");
297 298
            table_header.set_inner_html(header);
            
299
            WebKit.DOM.HTMLElement span_body = Util.DOM.select(div_email_container, ".body");
300
            span_body.set_inner_html(body_text);
301

302 303 304
        } catch (Error html_error) {
            warning("Error setting HTML for message: %s", html_error.message);
        }
305

306 307
        // Set attachment icon and add the attachments container if we have any attachments.
        set_attachment_icon(div_message, email.attachments.size > 0);
308 309 310 311
        if (email.attachments.size > 0) {
            insert_attachments(div_message, email.attachments);
        }

312
        // Add classes according to the state of the email.
313
        update_flags(email);
314 315

        // Attach to the click events for hiding/showing quotes, opening the menu, and so forth.
316 317 318 319 320 321 322 323 324 325
        bind_event(web_view, ".email", "contextmenu", (Callback) on_context_menu, this);
        bind_event(web_view, ".quote_container > .hider", "click", (Callback) on_hide_quote_clicked);
        bind_event(web_view, ".quote_container > .shower", "click", (Callback) on_show_quote_clicked);
        bind_event(web_view, ".email_container .menu", "click", (Callback) on_menu_clicked, this);
        bind_event(web_view, ".email_container .starred", "click", (Callback) on_unstar_clicked, this);
        bind_event(web_view, ".email_container .unstarred", "click", (Callback) on_star_clicked, this);
        bind_event(web_view, ".header .field .value", "click", (Callback) on_value_clicked, this);
        bind_event(web_view, ".email .header_container", "click", (Callback) on_body_toggle_clicked, this);
        bind_event(web_view, ".attachment_container .attachment", "click", (Callback) on_attachment_clicked, this);
        bind_event(web_view, ".attachment_container .attachment", "contextmenu", (Callback) on_attachment_menu, this);
326
    }
327
    
328
    public void unhide_last_email() {
329
        WebKit.DOM.HTMLElement last_email = (WebKit.DOM.HTMLElement) web_view.container.get_last_child().previous_sibling;
330 331 332 333 334 335 336 337 338
        if (last_email != null) {
            WebKit.DOM.DOMTokenList class_list = last_email.get_class_list();
            try {
                class_list.remove("hide");
            } catch (Error error) {
                // Expected, if not hidden
            }
        }
    }
339
    
340 341
    private Geary.Email? get_email_from_element(WebKit.DOM.Element element) {
        // First get the email container.
342
        WebKit.DOM.Element? email_element = null;
343 344 345 346 347 348 349 350 351 352
        try {
            if (element.webkit_matches_selector(".email")) {
                email_element = element;
            } else {
                email_element = closest_ancestor(element, ".email");
            }
        } catch (Error error) {
            debug("Failed to find div.email from element: %s", error.message);
            return null;
        }
353 354 355 356
        
        if (email_element == null)
            return null;
        
357 358 359 360 361 362 363 364
        // Next find the ID in the email-to-element map.
        Geary.EmailIdentifier? email_id = null;
        foreach (var entry in email_to_element.entries) {
            if (entry.value == email_element) {
                email_id = entry.key;
                break;
            }
        }
365 366 367
        
        if (email_id == null)
            return null;
368 369 370

        // Now lookup the email in our messages set.
        foreach (Geary.Email message in messages) {
371
            if (message.id == email_id)
372 373
                return message;
        }
374
        
375 376
        return null;
    }
377 378 379 380 381 382 383 384 385 386 387 388
    
    private Geary.Attachment? get_attachment_from_element(WebKit.DOM.Element element) {
        Geary.Email? email = get_email_from_element(element);
        if (email == null)
            return null;
         
        try {
            return email.get_attachment(int64.parse(element.get_attribute("data-attachment-id")));
        } catch (Geary.EngineError err) {
            return null;
        }
    }
389

390 391 392 393 394 395 396 397 398
    private void set_attachment_icon(WebKit.DOM.HTMLElement container, bool show) {
        try {
            WebKit.DOM.DOMTokenList class_list = container.get_class_list();
            Util.DOM.toggle_class(class_list, "attachment", show);
        } catch (Error e) {
            warning("Failed to set attachment icon: %s", e.message);
        }
    }

399
    public void update_flags(Geary.Email email) {
400
        // Nothing to do if we aren't displaying this email.
401
        if (!email_to_element.has_key(email.id)) {
402 403 404
            return;
        }

405
        Geary.EmailFlags flags = email.email_flags;
406
        
407
        // Update the flags in our message set.
408
        foreach (Geary.Email message in messages) {
409
            if (message.id.equals(email.id)) {
410 411 412 413
                message.set_flags(flags);
                break;
            }
        }
414
        
415
        // Get the email div and update its state.
416
        WebKit.DOM.HTMLElement container = email_to_element.get(email.id);
417 418
        try {
            WebKit.DOM.DOMTokenList class_list = container.get_class_list();
419 420
            Util.DOM.toggle_class(class_list, "read", !flags.is_unread());
            Util.DOM.toggle_class(class_list, "starred", flags.is_flagged());
421 422 423 424
        } catch (Error e) {
            warning("Failed to set classes on .email: %s", e.message);
        }
    }
425

426
    private static void on_context_menu(WebKit.DOM.Element clicked_element, WebKit.DOM.Event event,
427 428
        ConversationViewer conversation_viewer) {
        Geary.Email email = conversation_viewer.get_email_from_element(clicked_element);
429
        if (email != null)
430
            conversation_viewer.show_context_menu(email);
431 432 433 434 435 436 437 438 439 440 441
    }
    
    private void show_context_menu(Geary.Email email) {
        context_menu = build_context_menu(email);
        context_menu.show_all();
        context_menu.popup(null, null, null, 0, 0);
    }
    
    private Gtk.Menu build_context_menu(Geary.Email email) {
        Gtk.Menu menu = new Gtk.Menu();
        
442
        if (web_view.can_copy_clipboard()) {
443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
            // Add a menu item for copying the current selection.
            Gtk.MenuItem item = new Gtk.MenuItem.with_mnemonic(_("_Copy"));
            item.activate.connect(on_copy_text);
            menu.append(item);
        }
        
        if (hover_url != null) {
            // Add a menu item for copying the link.
            Gtk.MenuItem item = new Gtk.MenuItem.with_mnemonic(_("Copy _Link"));
            item.activate.connect(on_copy_link);
            menu.append(item);
            
            if (Geary.RFC822.MailboxAddress.is_valid_address(hover_url)) {
                // Add a menu item for copying the address.
                item = new Gtk.MenuItem.with_mnemonic(_("Copy _Email Address"));
                item.activate.connect(on_copy_email_address);
459
                menu.append(item);
460 461 462 463 464 465 466 467 468
            }
        }
        
        // Select all.
        Gtk.MenuItem select_all_item = new Gtk.MenuItem.with_mnemonic(_("Select _All"));
        select_all_item.activate.connect(on_select_all);
        menu.append(select_all_item);
        
        return menu;
469 470
    }

471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
    private static void on_hide_quote_clicked(WebKit.DOM.Element element) {
        try {
            WebKit.DOM.Element parent = element.get_parent_element();
            parent.set_attribute("class", "quote_container controllable hide");
        } catch (Error error) {
            warning("Error hiding quote: %s", error.message);
        }
    }

    private static void on_show_quote_clicked(WebKit.DOM.Element element) {
        try {
            WebKit.DOM.Element parent = element.get_parent_element();
            parent.set_attribute("class", "quote_container controllable show");
        } catch (Error error) {
            warning("Error hiding quote: %s", error.message);
        }
    }

489
    private static void on_menu_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
490
        ConversationViewer conversation_viewer) {
491
        event.stop_propagation();
492
        Geary.Email email = conversation_viewer.get_email_from_element(element);
493
        if (email != null)
494
            conversation_viewer.show_message_menu(email);
495
    }
496

497
    private static void on_unstar_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
498
        ConversationViewer conversation_viewer) {
499
        event.stop_propagation();
500
        Geary.Email? email = conversation_viewer.get_email_from_element(element);
501
        if (email != null)
502
            conversation_viewer.unflag_message(email);
503
    }
504

505
    private static void on_star_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
506
        ConversationViewer conversation_viewer) {
507
        event.stop_propagation();
508
        Geary.Email? email = conversation_viewer.get_email_from_element(element);
509
        if (email != null)
510
            conversation_viewer.flag_message(email);
511
    }
512 513

    private static void on_value_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
514 515
        ConversationViewer conversation_viewer) {
        if (!conversation_viewer.is_hidden_email(element))
516 517 518 519 520
            event.stop_propagation();  // Don't allow toggle
    }

    private bool is_hidden_email(WebKit.DOM.Element element) {
        try {
521 522 523 524 525
            WebKit.DOM.HTMLElement? email_element = closest_ancestor(element, ".email");
            if (email_element == null)
                return false;
            
            WebKit.DOM.DOMTokenList class_list = email_element.get_class_list();
526 527 528 529 530 531 532
            return class_list.contains("hide");
        } catch (Error error) {
            warning("Error getting hidden status: %s", error.message);
            return false;
        }
    }

533
    private static void on_body_toggle_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
534 535
        ConversationViewer conversation_viewer) {
        conversation_viewer.on_body_toggle_clicked_self(element);
536 537 538 539
    }

    private void on_body_toggle_clicked_self(WebKit.DOM.Element element) {
        try {
540 541 542 543 544
            WebKit.DOM.HTMLElement? email_element = closest_ancestor(element, ".email");
            if (email_element == null)
                return;
            
            WebKit.DOM.DOMTokenList class_list = email_element.get_class_list();
545
            if (class_list.contains("hide"))
546
                class_list.remove("hide");
547
            else
548 549
                class_list.add("hide");
        } catch (Error error) {
550 551 552 553 554
            warning("Error toggling message: %s", error.message);
        }
    }

    private static void on_attachment_clicked(WebKit.DOM.Element element, WebKit.DOM.Event event,
555 556
        ConversationViewer conversation_viewer) {
        conversation_viewer.on_attachment_clicked_self(element);
557 558 559
    }

    private void on_attachment_clicked_self(WebKit.DOM.Element element) {
560 561 562 563 564 565
        int64 attachment_id = int64.parse(element.get_attribute("data-attachment-id"));
        Geary.Email? email = get_email_from_element(element);
        if (email == null)
            return;
        
        Geary.Attachment? attachment = null;
566
        try {
567
            attachment = email.get_attachment(attachment_id);
568 569 570
        } catch (Error error) {
            warning("Error opening attachment: %s", error.message);
        }
571 572 573
        
        if (attachment != null)
            open_attachment(attachment);
574 575 576
    }

    private static void on_attachment_menu(WebKit.DOM.Element element, WebKit.DOM.Event event,
577
        ConversationViewer conversation_viewer) {
578
        event.stop_propagation();
579 580
        Geary.Email? email = conversation_viewer.get_email_from_element(element);
        Geary.Attachment? attachment = conversation_viewer.get_attachment_from_element(element);
581
        if (email != null && attachment != null)
582
            conversation_viewer.show_attachment_menu(email, attachment);
583
    }
584
    
585 586 587
    private void on_message_menu_selection_done() {
        message_menu = null;
    }
588 589 590 591
    
    private void on_attachment_menu_selection_done() {
        attachment_menu = null;
    }
592

593
    private void save_attachment(Geary.Attachment attachment) {
594
        Gee.List<Geary.Attachment> attachments = new Gee.ArrayList<Geary.Attachment>();
595
        attachments.add(attachment);
596 597
        save_attachments(attachments);
    }
598 599
    
    private void on_mark_read_message(Geary.Email message) {
600 601
        Geary.EmailFlags flags = new Geary.EmailFlags();
        flags.add(Geary.EmailFlags.UNREAD);
602
        mark_message(message, null, flags);
603 604
    }

605
    private void on_mark_unread_message(Geary.Email message) {
606 607
        Geary.EmailFlags flags = new Geary.EmailFlags();
        flags.add(Geary.EmailFlags.UNREAD);
608
        mark_message(message, flags, null);
609 610
    }

611
    private void on_print_message(Geary.Email message) {
Christian Dywan's avatar
Christian Dywan committed
612
        try {
613
            email_to_element.get(message.id).get_class_list().add("print");
614
            web_view.get_main_frame().print();
615
            email_to_element.get(message.id).get_class_list().remove("print");
Christian Dywan's avatar
Christian Dywan committed
616 617 618 619
        } catch (GLib.Error error) {
            debug("Hiding elements for printing failed: %s", error.message);
        }
    }
620 621
    
    private void flag_message(Geary.Email email) {
622 623
        Geary.EmailFlags flags = new Geary.EmailFlags();
        flags.add(Geary.EmailFlags.FLAGGED);
624
        mark_message(email, flags, null);
625 626
    }

627
    private void unflag_message(Geary.Email email) {
628 629
        Geary.EmailFlags flags = new Geary.EmailFlags();
        flags.add(Geary.EmailFlags.FLAGGED);
630
        mark_message(email, null, flags);
631 632
    }

633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657
    private void show_attachment_menu(Geary.Email email, Geary.Attachment attachment) {
        attachment_menu = build_attachment_menu(email, attachment);
        attachment_menu.show_all();
        attachment_menu.popup(null, null, null, 0, 0);
    }
    
    private Gtk.Menu build_attachment_menu(Geary.Email email, Geary.Attachment attachment) {
        Gtk.Menu menu = new Gtk.Menu();
        menu.selection_done.connect(on_attachment_menu_selection_done);
        
        Gtk.MenuItem save_attachment_item = new Gtk.MenuItem.with_mnemonic(_("_Save As..."));
        save_attachment_item.activate.connect(() => save_attachment(attachment));
        menu.append(save_attachment_item);
        
        if (email.attachments.size > 1) {
            Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(_("Save All A_ttachments..."));
            save_all_item.activate.connect(() => save_attachments(email.attachments));
            menu.append(save_all_item);
        }
        
        return menu;
    }
    
    private void show_message_menu(Geary.Email email) {
        message_menu = build_message_menu(email);
658 659 660
        message_menu.show_all();
        message_menu.popup(null, null, null, 0, 0);
    }
661 662
    
    private Gtk.Menu build_message_menu(Geary.Email email) {
663 664
        Gtk.Menu menu = new Gtk.Menu();
        menu.selection_done.connect(on_message_menu_selection_done);
665 666 667 668 669 670 671
        
        if (email.attachments.size > 0) {
            string mnemonic = ngettext("Save A_ttachment...", "Save All A_ttachments...",
                email.attachments.size);
            Gtk.MenuItem save_all_item = new Gtk.MenuItem.with_mnemonic(mnemonic);
            save_all_item.activate.connect(() => save_attachments(email.attachments));
            menu.append(save_all_item);
672
            menu.append(new Gtk.SeparatorMenuItem());
673
        }
674
        
675 676
        // Reply to a message.
        Gtk.MenuItem reply_item = new Gtk.MenuItem.with_mnemonic(_("_Reply"));
677
        reply_item.activate.connect(() => reply_to_message(email));
678
        menu.append(reply_item);
679 680 681

        // Reply to all on a message.
        Gtk.MenuItem reply_all_item = new Gtk.MenuItem.with_mnemonic(_("Reply to _All"));
682
        reply_all_item.activate.connect(() => reply_all_message(email));
683
        menu.append(reply_all_item);
684 685 686

        // Forward a message.
        Gtk.MenuItem forward_item = new Gtk.MenuItem.with_mnemonic(_("_Forward"));
687
        forward_item.activate.connect(() => forward_message(email));
688
        menu.append(forward_item);
689 690

        // Separator.
691
        menu.append(new Gtk.SeparatorMenuItem());
692
        
693
        // Mark as read/unread.
694
        if (current_folder is Geary.FolderSupportsMark) {
695
            if (email.is_unread().to_boolean(false)) {
696
                Gtk.MenuItem mark_read_item = new Gtk.MenuItem.with_mnemonic(_("_Mark as Read"));
697
                mark_read_item.activate.connect(() => on_mark_read_message(email));
698 699 700
                menu.append(mark_read_item);
            } else {
                Gtk.MenuItem mark_unread_item = new Gtk.MenuItem.with_mnemonic(_("_Mark as Unread"));
701
                mark_unread_item.activate.connect(() => on_mark_unread_message(email));
702 703
                menu.append(mark_unread_item);
            }
704
        }
705
        
Christian Dywan's avatar
Christian Dywan committed
706 707
        // Print a message.
        Gtk.MenuItem print_item = new Gtk.ImageMenuItem.from_stock(Gtk.Stock.PRINT, null);
708
        print_item.activate.connect(() => on_print_message(email));
709
        menu.append(print_item);
Christian Dywan's avatar
Christian Dywan committed
710

711
        // Separator.
712
        menu.append(new Gtk.SeparatorMenuItem());
713

714
        // View original message source.
715
        Gtk.MenuItem view_source_item = new Gtk.MenuItem.with_mnemonic(_("_View Source"));
716
        view_source_item.activate.connect(() => on_view_source(email));
717
        menu.append(view_source_item);
718

719
        return menu;
720 721
    }

722
    private WebKit.DOM.HTMLDivElement create_quote_container() throws Error {
723
        WebKit.DOM.HTMLDivElement quote_container = web_view.create_div();
724
        quote_container.set_attribute("class", "quote_container");
725 726
        quote_container.set_inner_html("%s%s%s".printf("<div class=\"shower\">[show]</div>",
            "<div class=\"hider\">[hide]</div>", "<div class=\"quote\"></div>"));
727
        return quote_container;
728
    }
729 730 731

    private string[] split_message_and_signature(string text) {
        try {
732
            Regex signature_regex = new Regex("\\R--\\s*\\R", RegexCompileFlags.MULTILINE);
733 734 735 736 737 738
            return signature_regex.split_full(text, -1, 0, 0, 2);
        } catch (RegexError e) {
            debug("Regex error searching for signature: %s", e.message);
            return new string[0];
        }
    }
739 740 741 742
    
    private string set_up_quotes(string text) {
        try {
            // Extract any quote containers from the signature block and make them controllable.
743
            WebKit.DOM.HTMLElement container = web_view.create_div();
744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763
            container.set_inner_html(text);
            WebKit.DOM.NodeList quote_list = container.query_selector_all(".signature .quote_container");
            for (int i = 0; i < quote_list.length; ++i) {
                WebKit.DOM.Element quote = quote_list.item(i) as WebKit.DOM.Element;
                quote.set_attribute("class", "quote_container controllable hide");
                container.append_child(quote);
            }
            
            // If there is only one quote container in the message, set it up as controllable.
            quote_list = container.query_selector_all(".quote_container");
            if (quote_list.length == 1) {
                ((WebKit.DOM.Element) quote_list.item(0)).set_attribute("class",
                    "quote_container controllable hide");
            }
            return container.get_inner_html();
        } catch (Error error) {
            debug("Error adjusting final quote block: %s", error.message);
            return text;
        }
    }
764

765
    private string insert_plain_text_markup(string text) {
766 767 768 769 770 771 772 773
        // Plain text signature and quote:
        // -- 
        // Nate
        //
        // 2012/3/14 Nate Lillich &lt;nate@yorba.org&gt;#015
        // &gt;
        // &gt;
        //
774 775
        // Wrap all quotes in hide/show controllers.
        string message = "";
776
        try {
777
            WebKit.DOM.HTMLElement container = web_view.create_div();
778 779 780 781
            int offset = 0;
            while (offset < text.length) {
                // Find the beginning of a quote block.
                int quote_start = text.index_of("&gt;") == 0 && message.length == 0 ? 0 :
782
                    text.index_of("\n&gt;", offset);
783 784
                if (quote_start == -1) {
                    break;
785 786 787
                } else if (text.get(quote_start) == '\n') {
                    // Don't include the newline.
                    ++quote_start;
788 789 790 791 792
                }
                
                // Find the end of the quote block.
                int quote_end = quote_start;
                do {
793 794
                    quote_end = text.index_of("\n", quote_end + 1);
                } while (quote_end != -1 && quote_end == text.index_of("\n&gt;", quote_end));
795 796 797 798 799 800
                if (quote_end == -1) {
                    quote_end = text.length;
                }

                // Copy the stuff before the quote, then the wrapped quote.
                WebKit.DOM.Element quote_container = create_quote_container();
801
                Util.DOM.select(quote_container, ".quote").set_inner_html(
802
                    decorate_quotes(text.substring(quote_start, quote_end - quote_start)));
803
                container.append_child(quote_container);
804
                if (quote_start > offset) {
805 806 807 808 809
                    message += text.substring(offset, quote_start - offset);
                }
                message += container.get_inner_html();
                offset = quote_end;
                container.set_inner_html("");
810
            }
811 812 813 814 815 816 817 818
            
            // Append everything that's left.
            if (offset != text.length) {
                message += text.substring(offset);
            }
        } catch (Error error) {
            debug("Error wrapping plaintext quotes: %s", error.message);
            return text;
819 820
        }

821 822 823 824
        // Find the signature marker (--) at the beginning of a line.
        string[] message_chunks = split_message_and_signature(message);
        string signature = "";
        if (message_chunks.length == 2) {
825 826 827
            signature = "<div class=\"signature\">%s</div>".printf(
                message.substring(message_chunks[0].length).strip());
            message = "<div>%s</div>".printf(message_chunks[0]);
828
        }
829
        return "<pre>" + set_up_quotes(message + signature) + "</pre>";
830
    }
831
    
832
    private string insert_html_markup(string text, Geary.Email email) {
833
        try {
834
            // Create a workspace for manipulating the HTML.
835
            WebKit.DOM.HTMLElement container = web_view.create_div();
836
            container.set_inner_html(text);
837 838 839
            
            // Some HTML messages like to wrap themselves in full, proper html, head, and body tags.
            // If we have that here, lets remove it since we are sticking it in our own document.