geary/src/client/conversation-viewer/conversation-message.vala
Michael James Gratton d0dc003772 Clean up message header UI, tranitions and style a bit.
* ui/conversation-message.ui: Split out preview labels from existing
  header box and create a new preview box for them.

* src/client/components/main-window.vala (MainWindow::set_styling): Use
  style-defined names to allow us to specify message row backgrounds better.

* src/client/conversation-viewer/conversation-message.vala
  (ConversationMessage): Update GTK composite template children
  properties and visible state when showing/hiding the body, set new
  preview/header box label texts.

* src/client/conversation-viewer/conversation-viewer.vala
  (ConversationViewer::do_embedded_composer,
  ConversationViewer::add_message): Don't add 'frame' class to conversation
  list rows, we're styling it explicitly now.
2016-10-04 10:58:17 +11:00

1319 lines
55 KiB
Vala

/*
* Copyright 2011-2015 Yorba Foundation
* Copyright 2016 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* A widget for displaying a message in a conversation.
*/
[GtkTemplate (ui = "/org/gnome/Geary/conversation-message.ui")]
public class ConversationMessage : Gtk.Box {
// Internal class to associate inline image buffers (replaced by rotated scaled versions of
// them) so they can be saved intact if the user requires it
private class ReplacedImage : Geary.BaseObject {
public string id;
public string filename;
public Geary.Memory.Buffer buffer;
public ReplacedImage(int replaced_number, string filename, Geary.Memory.Buffer buffer) {
id = "%X".printf(replaced_number);
this.filename = filename;
this.buffer = buffer;
}
}
private const string[] INLINE_MIME_TYPES = {
"image/png",
"image/gif",
"image/jpeg",
"image/pjpeg",
"image/bmp",
"image/x-icon",
"image/x-xbitmap",
"image/x-xbm"
};
private const int ATTACHMENT_PREVIEW_SIZE = 50;
private const string REPLACED_IMAGE_CLASS = "replaced_inline_image";
private const string DATA_IMAGE_CLASS = "data_inline_image";
private const int MAX_INLINE_IMAGE_MAJOR_DIM = 1024;
private const int QUOTE_SIZE_THRESHOLD = 120;
// The email message being displayed
public Geary.Email email { get; private set; }
// The message being displayed
public Geary.RFC822.Message message { get; private set; }
// The HTML viewer to view the emails.
public ConversationWebView web_view { get; private set; }
[GtkChild]
private Gtk.Image avatar_image;
[GtkChild]
private Gtk.Revealer preview_revealer;
[GtkChild]
private Gtk.Label from_preview;
[GtkChild]
private Gtk.Label body_preview;
[GtkChild]
private Gtk.Revealer header_revealer;
[GtkChild]
private Gtk.Box from_header;
[GtkChild]
private Gtk.Box to_header;
[GtkChild]
private Gtk.Box cc_header;
[GtkChild]
private Gtk.Box bcc_header;
[GtkChild]
private Gtk.Box subject_header;
[GtkChild]
private Gtk.Box date_header;
[GtkChild]
private Gtk.Button flag_button;
[GtkChild]
private Gtk.MenuButton message_menubutton;
[GtkChild]
private Gtk.Revealer body_revealer;
[GtkChild]
public Gtk.Box body_box;
[GtkChild]
private Gtk.Popover link_popover;
[GtkChild]
private Gtk.Label good_link_label;
[GtkChild]
private Gtk.Label bad_link_label;
[GtkChild]
private Gtk.InfoBar remote_images_infobar;
// The folder containing the message
private Geary.Folder containing_folder = null; // XXX weak??
// Contains the current mouse-over'ed link URL, if any
private string? hover_url = null;
private Gee.HashSet<string> inlined_content_ids = new Gee.HashSet<string>();
private int next_replaced_buffer_number = 0;
private Gee.HashMap<string, ReplacedImage> replaced_images = new Gee.HashMap<string, ReplacedImage>();
private Gee.HashSet<string> replaced_content_ids = new Gee.HashSet<string>();
// Fired when the user clicks a link.
public signal void link_selected(string link);
public ConversationMessage(Geary.Email email, Geary.Folder containing_folder) {
this.email = email;
this.containing_folder = containing_folder;
try {
message = email.get_message();
} catch (Error error) {
debug("Error loading message: %s", error.message);
return;
}
// Preview headers
from_preview.set_text(format_addresses(message.from));
string preview_str = message.get_preview();
preview_str = Geary.String.reduce_whitespace(preview_str);
body_preview.set_text(preview_str);
// Full headers
set_header_text(from_header, format_addresses(message.from));
if (message.to != null) {
set_header_text(to_header, format_addresses(message.to));
to_header.get_style_context().remove_class("empty");
}
if (message.cc != null) {
set_header_text(cc_header, format_addresses(message.cc));
cc_header.get_style_context().remove_class("empty");
}
if (message.bcc != null) {
set_header_text(bcc_header, format_addresses(message.bcc));
bcc_header.get_style_context().remove_class("empty");
}
if (message.subject != null) {
set_header_text(subject_header, message.subject.value);
subject_header.get_style_context().remove_class("empty");
}
if (message.date != null) {
Date.ClockFormat clock_format =
GearyApplication.instance.config.clock_format;
set_header_text(
date_header,
Date.pretty_print_verbose(message.date.value, clock_format)
);
date_header.get_style_context().remove_class("empty");
}
message_menubutton.set_menu_model(build_message_menu(email));
message_menubutton.set_sensitive(false);
web_view = new ConversationWebView();
web_view.show();
body_box.pack_end(web_view, true, true, 0);
load_message_body();
//Gtk.ScrolledWindow web_scroller = new Gtk.ScrolledWindow(null, null);
//web_scroller.show();
//web_scroller.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.NEVER);
//web_scroller.add(web_view);
//body_box.pack_end(web_scroller, true, true, 0);
// web_view.context_menu.connect(() => { return true; }); // Suppress default context menu.
// web_view.realize.connect( () => { web_view.get_vadjustment().value_changed.connect(mark_read); });
// web_view.size_allocate.connect(mark_read);
web_view.realize.connect(() => { debug("web_view: realised"); });
web_view.size_allocate.connect(() => { debug("web_view: allocated"); });
body_box.set_has_tooltip(true);
web_view.hovering_over_link.connect(on_hovering_over_link);
web_view.link_selected.connect((link) => { link_selected(link); });
// if (email.from != null && email.from.contains_normalized(current_account_information.email)) {
// // XXX set a RO property?
// get_style_context().add_class("sent");
// }
// // Set attachment icon and add the attachments container if there are displayed attachments.
// int displayed = displayed_attachments(email);
// set_attachment_icon(div_message, displayed > 0);
// if (displayed > 0) {
// insert_attachments(div_message, email.attachments);
// }
// // Look for any attached emails
// Gee.List<Geary.RFC822.Message> sub_messages = message.get_sub_messages();
// foreach (Geary.RFC822.Message sub_message in sub_messages) {
// bool sub_remote_images = false;
// try {
// extra_part = set_message_html(
// sub_message, part_div, out sub_remote_images
// );
// extra_part.get_class_list().add("read");
// extra_part.get_class_list().add("hide");
// remote_images = remote_images || sub_remote_images;
// } catch (Error error) {
// debug("Error adding attached message: %s", error.message);
// }
// }
// // Edit draft button for drafts folder.
// if (in_drafts_folder() && is_in_folder) {
// WebKit.DOM.HTMLElement draft_edit_container = Util.DOM.select(div_message, ".draft_edit");
// WebKit.DOM.HTMLElement draft_edit_button =
// Util.DOM.select(div_message, ".draft_edit_button");
// try {
// draft_edit_container.set_attribute("style", "display:block");
// draft_edit_button.set_inner_html(_("Edit Draft"));
// } catch (Error e) {
// warning("Error setting draft button: %s", e.message);
// }
// }
update_flags(email);
}
public bool is_message_visible() {
return get_style_context().has_class("show-message");
}
public void show_message(bool include_transitions=true) {
get_style_context().add_class("show-message");
avatar_image.set_pixel_size(32); // XXX constant
Gtk.RevealerTransitionType revealer = preview_revealer.get_transition_type();
if (!include_transitions) {
preview_revealer.set_transition_type(Gtk.RevealerTransitionType.NONE);
}
preview_revealer.set_reveal_child(false);
preview_revealer.set_transition_type(revealer);
revealer = header_revealer.get_transition_type();
if (!include_transitions) {
header_revealer.set_transition_type(Gtk.RevealerTransitionType.NONE);
}
header_revealer.set_reveal_child(true);
header_revealer.set_transition_type(revealer);
flag_button.set_sensitive(true);
message_menubutton.set_sensitive(true);
// XXX this is pretty gross
revealer = body_revealer.get_transition_type();
if (!include_transitions) {
body_revealer.set_transition_type(Gtk.RevealerTransitionType.NONE);
}
body_revealer.set_reveal_child(true);
body_revealer.set_transition_type(revealer);
}
public void hide_message() {
get_style_context().remove_class("show-message");
avatar_image.set_pixel_size(24); // XXX constant
preview_revealer.set_reveal_child(true);
header_revealer.set_reveal_child(false);
flag_button.set_sensitive(false);
message_menubutton.set_sensitive(false);
body_revealer.set_reveal_child(false);
}
// Appends email address fields to the header.
private string format_addresses(Geary.RFC822.MailboxAddresses? addresses) {
int i = 0;
string value = "";
Gee.List<Geary.RFC822.MailboxAddress> list = addresses.get_all();
foreach (Geary.RFC822.MailboxAddress a in list) {
value += a.to_string();
if (++i < list.size)
value += ", ";
}
return value;
}
private static void set_header_text(Gtk.Box header, string text) {
((Gtk.Label) header.get_children().nth(1).data).set_text(text);
}
private MenuModel build_message_menu(Geary.Email email) {
Gtk.Builder builder = new Gtk.Builder.from_resource(
"/org/gnome/Geary/conversation-message-menu.ui"
);
MenuModel menu = (MenuModel) builder.get_object("conversation_message_menu");
// menu.selection_done.connect(on_message_menu_selection_done);
// int displayed = displayed_attachments(email);
// if (displayed > 0) {
// string mnemonic = ngettext("Save A_ttachment...", "Save All A_ttachments...",
// displayed);
// 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);
// menu.append(new Gtk.SeparatorMenuItem());
// }
// if (!in_drafts_folder()) {
// // Reply to a message.
// Gtk.MenuItem reply_item = new Gtk.MenuItem.with_mnemonic(_("_Reply"));
// reply_item.activate.connect(() => reply_to_message(email));
// menu.append(reply_item);
// // Reply to all on a message.
// Gtk.MenuItem reply_all_item = new Gtk.MenuItem.with_mnemonic(_("Reply to _All"));
// reply_all_item.activate.connect(() => reply_all_message(email));
// menu.append(reply_all_item);
// // Forward a message.
// Gtk.MenuItem forward_item = new Gtk.MenuItem.with_mnemonic(_("_Forward"));
// forward_item.activate.connect(() => forward_message(email));
// menu.append(forward_item);
// }
// if (menu.get_children().length() > 0) {
// // Separator.
// menu.append(new Gtk.SeparatorMenuItem());
// }
// // Mark as read/unread.
// if (email.is_unread().to_boolean(false)) {
// Gtk.MenuItem mark_read_item = new Gtk.MenuItem.with_mnemonic(_("_Mark as Read"));
// mark_read_item.activate.connect(() => on_mark_read_message(email));
// menu.append(mark_read_item);
// } else {
// Gtk.MenuItem mark_unread_item = new Gtk.MenuItem.with_mnemonic(_("_Mark as Unread"));
// mark_unread_item.activate.connect(() => on_mark_unread_message(email));
// menu.append(mark_unread_item);
// if (messages.size > 1 && messages.last() != email) {
// Gtk.MenuItem mark_unread_from_here_item = new Gtk.MenuItem.with_mnemonic(
// _("Mark Unread From _Here"));
// mark_unread_from_here_item.activate.connect(() => on_mark_unread_from_here(email));
// menu.append(mark_unread_from_here_item);
// }
// }
// // Print a message.
// Gtk.MenuItem print_item = new Gtk.MenuItem.with_mnemonic(Stock._PRINT_MENU);
// print_item.activate.connect(() => on_print_message(email));
// menu.append(print_item);
// // Separator.
// menu.append(new Gtk.SeparatorMenuItem());
// // View original message source.
// Gtk.MenuItem view_source_item = new Gtk.MenuItem.with_mnemonic(_("_View Source"));
// view_source_item.activate.connect(() => on_view_source(email));
// menu.append(view_source_item);
return menu;
}
public void update_flags(Geary.Email email) {
toggle_class("read");
toggle_class("starred");
//if (email.email_flags.is_outbox_sent()) {
// email_warning.set_inner_html(
// _("This message was sent successfully, but could not be saved to %s.").printf(
// Geary.SpecialFolderType.SENT.get_display_name()));
}
public void mark_manual_read() {
get_style_context().add_class("manual_read");
}
private void load_message_body() {
bool remote_images = false;
string? body_text = null;
try {
if (message.has_html_body()) {
body_text = message.get_html_body(inline_image_replacer);
} else {
body_text = message.get_plain_body(true, inline_image_replacer);
}
} catch (Error err) {
debug("Could not get message text. %s", err.message);
}
body_text = clean_html_markup(body_text ?? "", message, out remote_images);
web_view.notify["load-status"].connect((source, param) => {
if (web_view.load_status == WebKit.LoadStatus.FINISHED) {
WebKit.DOM.HTMLElement html = (
web_view.get_dom_document().document_element as
WebKit.DOM.HTMLElement
);
if (html != null) {
try {
unset_controllable_quotes(html);
} catch (Error error) {
warning("Error unsetting controllable_quotes: %s",
error.message);
}
}
bind_event(web_view, "body a", "click",
(Callback) on_link_clicked, this);
bind_event(web_view, ".quote_container > .shower", "click",
(Callback) on_show_quote_clicked, this);
bind_event(web_view, ".quote_container > .hider", "click",
(Callback) on_hide_quote_clicked, this);
}
});
web_view.load_string(body_text, "text/html", "UTF8", "");
if (remote_images) {
Geary.Contact contact = containing_folder.account.get_contact_store().get_by_rfc822(
email.get_primary_originator());
bool always_load = contact != null && contact.always_load_remote_images();
if (always_load || email.load_remote_images().is_certain()) {
web_view.notify["load-status"].connect((source, param) => {
if (web_view.load_status == WebKit.LoadStatus.FINISHED) {
show_images(false);
}
});
} else {
remote_images_infobar.show();
}
}
}
// This delegate is called from within Geary.RFC822.Message.get_body while assembling the plain
// or HTML document when a non-text MIME part is encountered within a multipart/mixed container.
// If this returns null, the MIME part is dropped from the final returned document; otherwise,
// this returns HTML that is placed into the document in the position where the MIME part was
// found
private string? inline_image_replacer(string filename, Geary.Mime.ContentType? content_type,
Geary.Mime.ContentDisposition? disposition, string? content_id, Geary.Memory.Buffer buffer) {
if (content_type == null) {
debug("Not displaying inline: no Content-Type");
return null;
}
if (!is_content_type_supported_inline(content_type)) {
debug("Not displaying %s inline: unsupported Content-Type", content_type.to_string());
return null;
}
// Even if the image doesn't need to be rotated, there's a win here: by reducing the size
// of the image at load time, it reduces the amount of work that has to be done to insert
// it into the HTML and then decoded and displayed for the user ... note that we currently
// have the doucment set up to reduce the size of the image to fit in the viewport, and a
// scaled load-and-deode is always faster than load followed by scale.
Geary.Memory.Buffer rotated_image = buffer;
string mime_type = content_type.get_mime_type();
try {
Gdk.PixbufLoader loader = new Gdk.PixbufLoader();
loader.size_prepared.connect(on_inline_image_size_prepared);
Geary.Memory.UnownedBytesBuffer? unowned_buffer = buffer as Geary.Memory.UnownedBytesBuffer;
if (unowned_buffer != null)
loader.write(unowned_buffer.to_unowned_uint8_array());
else
loader.write(buffer.get_uint8_array());
loader.close();
Gdk.Pixbuf? pixbuf = loader.get_pixbuf();
if (pixbuf != null) {
pixbuf = pixbuf.apply_embedded_orientation();
// trade-off here between how long it takes to compress the data and how long it
// takes to turn it into Base-64 (coupled with how long it takes WebKit to then
// Base-64 decode and uncompress it)
uint8[] image_data;
pixbuf.save_to_buffer(out image_data, "png", "compression", "5");
// Save length before transferring ownership (which frees the array)
int image_length = image_data.length;
rotated_image = new Geary.Memory.ByteBuffer.take((owned) image_data, image_length);
mime_type = "image/png";
}
} catch (Error err) {
debug("Unable to load and rotate image %s for display: %s", filename, err.message);
}
// store so later processing of the message doesn't replace this element with the original
// MIME part
string? escaped_content_id = null;
if (!Geary.String.is_empty(content_id)) {
replaced_content_ids.add(content_id);
escaped_content_id = Geary.HTML.escape_markup(content_id);
}
// Store the original buffer and its filename in a local map so they can be recalled later
// (if the user wants to save it) ... note that Content-ID is optional and there's no
// guarantee that filename will be unique, even in the same message, so need to generate
// a unique identifier for each object
ReplacedImage replaced_image = new ReplacedImage(next_replaced_buffer_number++, filename,
buffer);
replaced_images.set(replaced_image.id, replaced_image);
return "<img alt=\"%s\" class=\"%s %s\" src=\"%s\" replaced-id=\"%s\" %s />".printf(
Geary.HTML.escape_markup(filename),
DATA_IMAGE_CLASS, REPLACED_IMAGE_CLASS,
assemble_data_uri(mime_type, rotated_image),
Geary.HTML.escape_markup(replaced_image.id),
escaped_content_id != null ? @"cid=\"$escaped_content_id\"" : "");
}
// Called by Gdk.PixbufLoader when the image's size has been determined but not loaded yet ...
// this allows us to load the image scaled down, for better performance when manipulating and
// writing the data URI for WebKit
private static void on_inline_image_size_prepared(Gdk.PixbufLoader loader, int width, int height) {
// easier to use as local variable than have the const listed everywhere in the code
// IN ALL SCREAMING CAPS
int scale = MAX_INLINE_IMAGE_MAJOR_DIM;
// Borrowed liberally from Shotwell's Dimensions.get_scaled() method
// check for existing fit
if (width <= scale && height <= scale)
return;
int adj_width, adj_height;
if ((width - scale) > (height - scale)) {
double aspect = (double) scale / (double) width;
adj_width = scale;
adj_height = (int) Math.round((double) height * aspect);
} else {
double aspect = (double) scale / (double) height;
adj_width = (int) Math.round((double) width * aspect);
adj_height = scale;
}
loader.set_size(adj_width, adj_height);
}
// private Gtk.Menu build_context_menu(Geary.Email email, WebKit.DOM.Element clicked_element) {
// Gtk.Menu menu = new Gtk.Menu();
// if (web_view.can_copy_clipboard()) {
// // 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) {
// if (hover_url.has_prefix(Geary.ComposedEmail.MAILTO_SCHEME)) {
// // Add a menu item for copying the address.
// Gtk.MenuItem item = new Gtk.MenuItem.with_mnemonic(_("Copy _Email Address"));
// item.activate.connect(on_copy_email_address);
// menu.append(item);
// } else {
// // 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);
// }
// }
// // Select message.
// if (!is_hidden()) {
// Gtk.MenuItem select_message_item = new Gtk.MenuItem.with_mnemonic(_("Select _Message"));
// select_message_item.activate.connect(() => {on_select_message(clicked_element);});
// menu.append(select_message_item);
// }
// // 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);
// // Inspect.
// if (Args.inspector) {
// Gtk.MenuItem inspect_item = new Gtk.MenuItem.with_mnemonic(_("_Inspect"));
// inspect_item.activate.connect(() => {web_view.web_inspector.inspect_node(clicked_element);});
// menu.append(inspect_item);
// }
// return menu;
// }
// private void on_unstar_clicked() {
// unflag_message();
// }
// private void on_star_clicked() {
// flag_message();
// }
// private bool is_hidden() {
// // XXX
// return false;
// }
// private void on_toggle_hidden() {
// // XXX
// get_viewer().mark_read();
// }
// private void on_attachment_clicked(string attachment_id) {
// Geary.Attachment? attachment = null;
// try {
// attachment = email.get_attachment(attachment_id);
// } catch (Error error) {
// warning("Error opening attachment: %s", error.message);
// }
// if (attachment != null) {
// get_viewer().open_attachment(attachment);
// }
// }
// private void on_data_image_menu(WebKit.DOM.Element element, WebKit.DOM.Event event) {
// event.stop_propagation();
// string? replaced_id = element.get_attribute("replaced-id");
// if (Geary.String.is_empty(replaced_id))
// return;
// ReplacedImage? replaced_image = replaced_images.get(replaced_id);
// if (replaced_image == null)
// return;
// image_menu = new Gtk.Menu();
// image_menu.selection_done.connect(() => {
// image_menu = null;
// });
// Gtk.MenuItem save_image_item = new Gtk.MenuItem.with_mnemonic(_("_Save Image As..."));
// save_image_item.activate.connect(() => {
// save_buffer_to_file(replaced_image.filename, replaced_image.buffer);
// });
// image_menu.append(save_image_item);
// image_menu.show_all();
// image_menu.popup(null, null, null, 0, Gtk.get_current_event_time());
// }
// private void save_attachment(Geary.Attachment attachment) {
// Gee.List<Geary.Attachment> attachments = new Gee.ArrayList<Geary.Attachment>();
// attachments.add(attachment);
// get_viewer().save_attachments(attachments);
// }
// private void on_mark_read_message(Geary.Email message) {
// Geary.EmailFlags flags = new Geary.EmailFlags();
// flags.add(Geary.EmailFlags.UNREAD);
// get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(message.id).to_array_list(), null, flags);
// mark_manual_read(message.id);
// }
// private void on_mark_unread_message(Geary.Email message) {
// Geary.EmailFlags flags = new Geary.EmailFlags();
// flags.add(Geary.EmailFlags.UNREAD);
// get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(message.id).to_array_list(), flags, null);
// mark_manual_read(message.id);
// }
// private void on_mark_unread_from_here(Geary.Email message) {
// Geary.EmailFlags flags = new Geary.EmailFlags();
// flags.add(Geary.EmailFlags.UNREAD);
// Gee.Iterator<Geary.Email>? iter = messages.iterator_at(message);
// if (iter == null) {
// warning("Email not found in message list");
// return;
// }
// // Build a list of IDs to mark.
// Gee.ArrayList<Geary.EmailIdentifier> to_mark = new Gee.ArrayList<Geary.EmailIdentifier>();
// to_mark.add(message.id);
// while (iter.next())
// to_mark.add(iter.get().id);
// get_viewer().mark_messages(to_mark, flags, null);
// foreach(Geary.EmailIdentifier id in to_mark)
// mark_manual_read(id);
// }
// private void on_print_message(Geary.Email message) {
// try {
// email_to_element.get(message.id).get_class_list().add("print");
// web_view.get_main_frame().print();
// email_to_element.get(message.id).get_class_list().remove("print");
// } catch (GLib.Error error) {
// debug("Hiding elements for printing failed: %s", error.message);
// }
// }
// private void flag_message() {
// Geary.EmailFlags flags = new Geary.EmailFlags();
// flags.add(Geary.EmailFlags.FLAGGED);
// get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(), flags, null);
// }
// private void unflag_message() {
// Geary.EmailFlags flags = new Geary.EmailFlags();
// flags.add(Geary.EmailFlags.FLAGGED);
// get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(), null, flags);
// }
// 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, Gtk.get_current_event_time());
// }
// 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 (displayed_attachments(email) > 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 string clean_html_markup(string text, Geary.RFC822.Message message, out bool remote_images) {
remote_images = false;
try {
string inner_text = text;
// If email HTML has a BODY, use only that
GLib.Regex body_regex = new GLib.Regex("<body([^>]*)>(.*)</body>",
GLib.RegexCompileFlags.DOTALL);
GLib.MatchInfo matches;
if (body_regex.match(text, 0, out matches)) {
inner_text = matches.fetch(2);
string attrs = matches.fetch(1);
if (attrs != "")
inner_text = @"<div$attrs>$inner_text</div>";
}
// Create a workspace for manipulating the HTML.
WebKit.DOM.HTMLElement container = web_view.create_div();
container.set_inner_html(inner_text);
// Get all the top level block quotes and stick them into a hide/show controller.
WebKit.DOM.NodeList blockquote_list = container.query_selector_all("blockquote");
for (int i = 0; i < blockquote_list.length; ++i) {
// Get the nodes we need.
WebKit.DOM.Node blockquote_node = blockquote_list.item(i);
WebKit.DOM.Node? next_sibling = blockquote_node.get_next_sibling();
WebKit.DOM.Node parent = blockquote_node.get_parent_node();
// Make sure this is a top level blockquote.
if (node_is_child_of(blockquote_node, "BLOCKQUOTE")) {
continue;
}
// parent
// quote_container
// blockquote
// sibling
WebKit.DOM.Element quote_container = create_quote_container();
Util.DOM.select(quote_container, ".quote").append_child(blockquote_node);
if (next_sibling == null) {
parent.append_child(quote_container);
} else {
parent.insert_before(quote_container, next_sibling);
}
}
// Now look for the signature.
wrap_html_signature(ref container);
// Then look for all <img> tags. Inline images are replaced with
// data URLs.
WebKit.DOM.NodeList inline_list = container.query_selector_all("img");
for (ulong i = 0; i < inline_list.length; ++i) {
// Get the MIME content for the image.
WebKit.DOM.HTMLImageElement img = (WebKit.DOM.HTMLImageElement) inline_list.item(i);
string? src = img.get_attribute("src");
if (Geary.String.is_empty(src))
continue;
// if no Content-ID, then leave as-is, but note if a non-data: URI is being used for
// purposes of detecting remote images
string? content_id = src.has_prefix("cid:") ? src.substring(4) : null;
if (Geary.String.is_empty(content_id)) {
remote_images = remote_images || !src.has_prefix("data:");
continue;
}
// if image has a Content-ID and it's already been replaced by the image replacer,
// drop this tag, otherwise fix up this one with the Base-64 data URI of the image
if (!replaced_content_ids.contains(content_id)) {
string? filename = message.get_content_filename_by_mime_id(content_id);
Geary.Memory.Buffer image_content = message.get_content_by_mime_id(content_id);
Geary.Memory.UnownedBytesBuffer? unowned_buffer =
image_content as Geary.Memory.UnownedBytesBuffer;
// Get the content type.
string guess;
if (unowned_buffer != null)
guess = ContentType.guess(null, unowned_buffer.to_unowned_uint8_array(), null);
else
guess = ContentType.guess(null, image_content.get_uint8_array(), null);
string mimetype = ContentType.get_mime_type(guess);
// Replace the SRC to a data URI, the class to a known label for the popup menu,
// and the ALT to its filename, if supplied
img.remove_attribute("src"); // Work around a WebKitGTK+ crash. Bug 764152
img.set_attribute("src", assemble_data_uri(mimetype, image_content));
img.set_attribute("class", DATA_IMAGE_CLASS);
if (!Geary.String.is_empty(filename))
img.set_attribute("alt", filename);
// stash here so inlined image isn't listed as attachment (esp. if it has no
// Content-Disposition)
inlined_content_ids.add(content_id);
} else {
// replaced by data: URI, remove this tag and let the inserted one shine through
img.parent_element.remove_child(img);
}
}
// Remove any inline images that were referenced through Content-ID
foreach (string cid in inlined_content_ids) {
try {
string escaped_cid = Geary.HTML.escape_markup(cid);
WebKit.DOM.Element? img = container.query_selector(@"[cid='$escaped_cid']");
if (img != null)
img.parent_element.remove_child(img);
} catch (Error error) {
debug("Error removing inlined image: %s", error.message);
}
}
// Now return the whole message.
return container.get_inner_html();
} catch (Error e) {
debug("Error modifying HTML message: %s", e.message);
return text;
}
}
private WebKit.DOM.HTMLDivElement create_quote_container() throws Error {
WebKit.DOM.HTMLDivElement quote_container = web_view.create_div();
quote_container.set_attribute(
"class", "quote_container controllable hide"
);
quote_container.set_inner_html("""
<div class="shower"><input type="button" value="▼ ▼ ▼" /></div>
<div class="hider"><input type="button" value="▲ ▲ ▲" /></div>
<div class="quote"></div>""");
return quote_container;
}
private void wrap_html_signature(ref WebKit.DOM.HTMLElement container) throws Error {
// Most HTML signatures fall into one of these designs which are handled by this method:
//
// 1. GMail: <div>-- </div>$SIGNATURE
// 2. GMail Alternate: <div><span>-- </span></div>$SIGNATURE
// 3. Thunderbird: <div>-- <br>$SIGNATURE</div>
//
WebKit.DOM.NodeList div_list = container.query_selector_all("div,span,p");
int i = 0;
Regex sig_regex = new Regex("^--\\s*$");
Regex alternate_sig_regex = new Regex("^--\\s*(?:<br|\\R)");
for (; i < div_list.length; ++i) {
// Get the div and check that it starts a signature block and is not inside a quote.
WebKit.DOM.HTMLElement div = div_list.item(i) as WebKit.DOM.HTMLElement;
string inner_html = div.get_inner_html();
if ((sig_regex.match(inner_html) || alternate_sig_regex.match(inner_html)) &&
!node_is_child_of(div, "BLOCKQUOTE")) {
break;
}
}
// If we have a signature, move it and all of its following siblings that are not quotes
// inside a signature div.
if (i == div_list.length) {
return;
}
WebKit.DOM.Node elem = div_list.item(i) as WebKit.DOM.Node;
WebKit.DOM.Element parent = elem.get_parent_element();
WebKit.DOM.HTMLElement signature_container = web_view.create_div();
signature_container.set_attribute("class", "signature");
do {
// Get its sibling _before_ we move it into the signature div.
WebKit.DOM.Node? sibling = elem.get_next_sibling();
signature_container.append_child(elem);
elem = sibling;
} while (elem != null);
parent.append_child(signature_container);
}
private void unset_controllable_quotes(WebKit.DOM.HTMLElement element) throws GLib.Error {
WebKit.DOM.NodeList quote_list = element.query_selector_all(".quote_container.controllable");
for (int i = 0; i < quote_list.length; ++i) {
WebKit.DOM.Element quote_container = quote_list.item(i) as WebKit.DOM.Element;
long scroll_height = quote_container.query_selector(".quote").scroll_height;
// If the message is hidden, scroll_height will be 0.
if (scroll_height > 0 && scroll_height < QUOTE_SIZE_THRESHOLD) {
quote_container.class_list.remove("controllable");
quote_container.class_list.remove("hide");
}
}
}
private void show_images(bool remember) {
try {
WebKit.DOM.Element body = Util.DOM.select(
web_view.get_dom_document(), "body"
);
if (body == null) {
warning("Could not find message body");
} else {
WebKit.DOM.NodeList nodes = body.get_elements_by_tag_name("img");
for (ulong i = 0; i < nodes.length; i++) {
WebKit.DOM.Element? element = nodes.item(i) as WebKit.DOM.Element;
if (element == null || !element.has_attribute("src"))
continue;
string src = element.get_attribute("src");
if (!web_view.is_always_loaded(src)) {
// Workaround a WebKitGTK+ 2.4.10 crash. See Bug 763933
element.remove_attribute("src");
element.set_attribute("src", web_view.allow_prefix + src);
}
}
}
} catch (Error error) {
warning("Error showing images: %s", error.message);
}
if (remember) {
// only add flag to load remote images if not already present
if (email != null && !email.load_remote_images().is_certain()) {
Geary.EmailFlags flags = new Geary.EmailFlags();
flags.add(Geary.EmailFlags.LOAD_REMOTE_IMAGES);
// XXX reenable this
//get_viewer().mark_messages(Geary.iterate<Geary.EmailIdentifier>(email.id).to_array_list(), flags, null);
}
}
}
private void always_show_images() {
Geary.ContactStore contact_store =
containing_folder.account.get_contact_store();
Geary.Contact? contact = contact_store.get_by_rfc822(
email.get_primary_originator()
);
if (contact == null) {
debug("Couldn't find contact for %s", email.from.to_string());
return;
}
Geary.ContactFlags flags = new Geary.ContactFlags();
flags.add(Geary.ContactFlags.ALWAYS_LOAD_REMOTE_IMAGES);
Gee.ArrayList<Geary.Contact> contact_list = new Gee.ArrayList<Geary.Contact>();
contact_list.add(contact);
contact_store.mark_contacts_async.begin(contact_list, flags, null);
show_images(false);
// XXX notify something to go load images for the rest of the
// messages in he current convo for this sender
}
// private bool should_show_attachment(Geary.Attachment attachment) {
// // if displayed inline, don't include in attachment list
// if (attachment.content_id in inlined_content_ids)
// return false;
// switch (attachment.content_disposition.disposition_type) {
// case Geary.Mime.DispositionType.ATTACHMENT:
// return true;
// case Geary.Mime.DispositionType.INLINE:
// return !is_content_type_supported_inline(attachment.content_type);
// default:
// assert_not_reached();
// }
// }
// private int displayed_attachments(Geary.Email email) {
// int ret = 0;
// foreach (Geary.Attachment attachment in email.attachments) {
// if (should_show_attachment(attachment)) {
// ret++;
// }
// }
// return ret;
// }
// private void insert_attachments(WebKit.DOM.HTMLElement email_container,
// Gee.List<Geary.Attachment> attachments) {
// // <div class="attachment_container">
// // <div class="top_border"></div>
// // <table class="attachment" data-attachment-id="">
// // <tr>
// // <td class="preview">
// // <img src="" />
// // </td>
// // <td class="info">
// // <div class="filename"></div>
// // <div class="filesize"></div>
// // </td>
// // </tr>
// // </table>
// // </div>
// try {
// // Prepare the dom for our attachments.
// WebKit.DOM.Document document = web_view.get_dom_document();
// WebKit.DOM.HTMLElement attachment_container =
// Util.DOM.clone_select(document, "#attachment_template");
// WebKit.DOM.HTMLElement attachment_template =
// Util.DOM.select(attachment_container, ".attachment");
// attachment_container.remove_attribute("id");
// attachment_container.remove_child(attachment_template);
// // Create an attachment table for each attachment.
// foreach (Geary.Attachment attachment in attachments) {
// if (!should_show_attachment(attachment)) {
// continue;
// }
// // Generate the attachment table.
// WebKit.DOM.HTMLElement attachment_table = Util.DOM.clone_node(attachment_template);
// string filename = !attachment.has_supplied_filename ? _("none") : attachment.file.get_basename();
// Util.DOM.select(attachment_table, ".info .filename")
// .set_inner_text(filename);
// Util.DOM.select(attachment_table, ".info .filesize")
// .set_inner_text(Files.get_filesize_as_string(attachment.filesize));
// attachment_table.set_attribute("data-attachment-id", attachment.id);
// // Set the image preview and insert it into the container.
// WebKit.DOM.HTMLImageElement img =
// Util.DOM.select(attachment_table, ".preview img") as WebKit.DOM.HTMLImageElement;
// web_view.set_attachment_src(img, attachment.content_type, attachment.file.get_path(),
// ATTACHMENT_PREVIEW_SIZE);
// attachment_container.append_child(attachment_table);
// }
// // Append the attachments to the email.
// email_container.append_child(attachment_container);
// } catch (Error error) {
// debug("Failed to insert attachments: %s", error.message);
// }
// }
// private bool in_drafts_folder() {
// return containing_folder.special_folder_type == Geary.SpecialFolderType.DRAFTS;
// }
private void toggle_class(string cls) {
Gtk.StyleContext context = get_style_context();
if (context.has_class(cls)) {
context.add_class(cls);
} else {
context.remove_class(cls);
}
}
private static bool is_content_type_supported_inline(Geary.Mime.ContentType content_type) {
foreach (string mime_type in INLINE_MIME_TYPES) {
try {
if (content_type.is_mime_type(mime_type))
return true;
} catch (Error err) {
debug("Unable to compare MIME type %s: %s", mime_type, err.message);
}
}
return false;
}
/*
* Test whether text looks like a URI that leads somewhere other than href. The text
* will have a scheme prepended if it doesn't already have one, and the short versions
* have the scheme skipped and long paths truncated.
*/
private bool deceptive_text(string href, ref string text, out string href_short,
out string text_short) {
href_short = "";
text_short = "";
// mailto URLs have a different form, and the worst they can do is pop up a composer,
// so we don't trigger on them.
if (href.has_prefix("mailto:"))
return false;
// First, does text look like a URI? Right now, just test whether it has
// <string>.<string> in it. More sophisticated tests are possible.
GLib.MatchInfo text_match, href_match;
try {
GLib.Regex domain = new GLib.Regex(
"([a-z]*://)?" // Optional scheme
+ "([^\\s:/]+\\.[^\\s:/\\.]+)" // Domain
+ "(/[^\\s]*)?" // Optional path
);
if (!domain.match(text, 0, out text_match))
return false;
if (!domain.match(href, 0, out href_match)) {
// If href doesn't look like a URL, something is fishy, so warn the user
href_short = href + _(" (Invalid?)");
text_short = text;
return true;
}
} catch (Error error) {
warning("Error in Regex text for deceptive urls: %s", error.message);
return false;
}
// Second, do the top levels of the two domains match? We compare the top n levels,
// where n is the minimum of the number of levels of the two domains.
string[] href_parts = href_match.fetch_all();
string[] text_parts = text_match.fetch_all();
string[] text_domain = text_parts[2].down().reverse().split(".");
string[] href_domain = href_parts[2].down().reverse().split(".");
for (int i = 0; i < text_domain.length && i < href_domain.length; i++) {
if (text_domain[i] != href_domain[i]) {
if (href_parts[1] == "")
href_parts[1] = "http://";
if (text_parts[1] == "")
text_parts[1] = href_parts[1];
string temp;
assemble_uris(href_parts, out temp, out href_short);
assemble_uris(text_parts, out text, out text_short);
return true;
}
}
return false;
}
private void assemble_uris(string[] parts, out string full, out string short_) {
full = parts[1] + parts[2];
short_ = parts[2];
if (parts.length == 4 && parts[3] != "/") {
full += parts[3];
if (parts[3].length > 20)
short_ += parts[3].substring(0, 20) + "";
else
short_ += parts[3];
}
}
private static void on_show_quote_clicked(WebKit.DOM.Element element,
WebKit.DOM.Event event) {
try {
((WebKit.DOM.HTMLElement) element.parent_node).class_list.remove("hide");
} catch (Error error) {
warning("Error showing quote: %s", error.message);
}
}
private static void on_hide_quote_clicked(WebKit.DOM.Element element,
WebKit.DOM.Event event,
ConversationMessage message) {
try {
((WebKit.DOM.HTMLElement) element.parent_node).class_list.add("hide");
message.web_view.queue_resize();
} catch (Error error) {
warning("Error toggling quote: %s", error.message);
}
}
private static void on_link_clicked(WebKit.DOM.Element element,
WebKit.DOM.Event event,
ConversationMessage message) {
if (message.on_link_clicked_self(element)) {
event.prevent_default();
}
}
// Check for possible phishing links, displays a popover if found.
// If not, lets it go through to the default handler.
private bool on_link_clicked_self(WebKit.DOM.Element element) {
string? href = element.get_attribute("href");
if (Geary.String.is_empty(href))
return false;
string text = ((WebKit.DOM.HTMLElement) element).get_inner_text();
string href_short, text_short;
if (!deceptive_text(href, ref text, out href_short, out text_short))
return false;
// Escape text and especially URLs since we got them from the
// HREF, and Gtk.Label.set_markup is a strict parser.
good_link_label.set_markup(
Markup.printf_escaped("<a href=\"%s\">%s</a>", text, text_short)
);
bad_link_label.set_markup(
Markup.printf_escaped("<a href=\"%s\">%s</a>", href, href_short)
);
// Work out the link's position, update the popover.
Gdk.Rectangle link_rect = Gdk.Rectangle();
web_view.get_allocation(out link_rect);
link_rect.x += (int) element.get_offset_left();
link_rect.y += (int) element.get_offset_top();
WebKit.DOM.Element? offset_parent = element.get_offset_parent();
while (offset_parent != null) {
link_rect.x += (int) offset_parent.get_offset_left();
link_rect.y += (int) offset_parent.get_offset_top();
offset_parent = offset_parent.get_offset_parent();
}
link_rect.width = (int) element.get_offset_width();
link_rect.height = (int) element.get_offset_height();
link_popover.set_pointing_to(link_rect);
link_popover.show();
return true;
}
private void on_hovering_over_link(string? title, string? url) {
// Use tooltip on the containing box since the web_view
// doesn't want to pay ball.
hover_url = (url != null) ? Uri.unescape_string(url) : null;
body_box.set_tooltip_text(hover_url);
body_box.trigger_tooltip_query();
}
[GtkCallback]
private void on_remote_images_response(Gtk.InfoBar info_bar,
int response_id) {
switch (response_id) {
case 1:
show_images(true);
break;
case 2:
always_show_images();
break;
default:
break; // pass
}
remote_images_infobar.hide();
}
// private void on_copy_text() {
// web_view.copy_clipboard();
// }
// private void on_copy_link() {
// // Put the current link in clipboard.
// Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
// clipboard.set_text(hover_url, -1);
// clipboard.store();
// }
// private void on_copy_email_address() {
// // Put the current email address in clipboard.
// Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
// if (hover_url.has_prefix(Geary.ComposedEmail.MAILTO_SCHEME))
// clipboard.set_text(hover_url.substring(Geary.ComposedEmail.MAILTO_SCHEME.length, -1), -1);
// else
// clipboard.set_text(hover_url, -1);
// clipboard.store();
// }
// private void on_select_all() {
// web_view.select_all();
// }
// private void on_select_message(WebKit.DOM.Element email_element) {
// try {
// web_view.get_dom_document().get_default_view().get_selection().select_all_children(email_element);
// } catch (Error error) {
// warning("Could not make selection: %s", error.message);
// }
// }
// private void on_view_source(Geary.Email message) {
// string source = message.header.buffer.to_string() + message.body.buffer.to_string();
// try {
// string temporary_filename;
// int temporary_handle = FileUtils.open_tmp("geary-message-XXXXXX.txt",
// out temporary_filename);
// FileUtils.set_contents(temporary_filename, source);
// FileUtils.close(temporary_handle);
// // ensure this file is only readable by the user ... this needs to be done after the
// // file is closed
// FileUtils.chmod(temporary_filename, (int) (Posix.S_IRUSR | Posix.S_IWUSR));
// string temporary_uri = Filename.to_uri(temporary_filename, null);
// Gtk.show_uri(web_view.get_screen(), temporary_uri, Gdk.CURRENT_TIME);
// } catch (Error error) {
// ErrorDialog dialog = new ErrorDialog(GearyApplication.instance.controller.main_window,
// _("Failed to open default text editor."), error.message);
// dialog.run();
// }
// }
}