93
77
external_images_info_bar.get_content_area() as Gtk.Box;
94
78
if (external_images_info_bar_content_area != null) {
95
79
Gtk.Label label = new Gtk.Label(_("This message contains images. Do you want to show them?"));
80
label.set_line_wrap(true);
96
81
external_images_info_bar_content_area.add(label);
99
box.pack_start(external_images_info_bar, false, false);
84
pack_start(external_images_info_bar, false, false);
101
86
web_view = new ConversationWebView();
103
web_view.set_border_width(0);
105
web_view.navigation_policy_decision_requested.connect(on_navigation_policy_decision_requested);
106
web_view.new_window_policy_decision_requested.connect(on_navigation_policy_decision_requested);
107
88
web_view.hovering_over_link.connect(on_hovering_over_link);
108
web_view.resource_request_starting.connect(on_resource_request_starting);
110
WebKit.WebSettings settings = new WebKit.WebSettings();
111
settings.enable_default_context_menu = false;
112
settings.enable_scripts = false;
113
settings.enable_java_applet = false;
114
settings.enable_plugins = false;
115
web_view.settings = settings;
117
// Load the HTML into WebKit.
118
web_view.load_finished.connect(on_load_finished);
119
string html_text = GearyApplication.instance.read_theme_file("message-viewer.html") ?? "";
120
web_view.load_string(html_text, "text/html", "UTF8", "");
89
web_view.realize.connect( () => { web_view.get_vadjustment().value_changed.connect(mark_read); });
90
web_view.size_allocate.connect(mark_read);
92
web_view.image_load_requested.connect(on_image_load_requested);
93
web_view.link_selected.connect((link) => { link_selected(link); });
122
95
Gtk.ScrolledWindow conversation_viewer_scrolled = new Gtk.ScrolledWindow(null, null);
123
96
conversation_viewer_scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC);
132
105
message_overlay_label.valign = Gtk.Align.END;
133
106
message_overlay.add_overlay(message_overlay_label);
135
box.pack_start(message_overlay);
108
pack_start(message_overlay);
111
private void on_image_load_requested() {
112
external_images_info_bar.show();
139
115
private void on_external_images_info_bar_response(Gtk.InfoBar sender, int response_id) {
140
set_load_external_images(response_id == Gtk.ResponseType.OK);
116
web_view.apply_load_external_images(response_id == Gtk.ResponseType.OK);
144
private void on_load_finished(WebKit.WebFrame frame) {
147
WebKit.DOM.Document document = web_view.get_dom_document();
148
WebKit.DOM.Element style_element = document.create_element(STYLE_NAME);
150
string css_text = GearyApplication.instance.read_theme_file("message-viewer.css") ?? "";
151
WebKit.DOM.Text text_node = document.create_text_node(css_text);
152
style_element.append_child(text_node);
154
WebKit.DOM.HTMLHeadElement head_element = document.get_head();
155
head_element.append_child(style_element);
156
} catch (Error error) {
157
debug("Unable to load message-viewer document from files: %s", error.message);
162
// Grab the HTML container.
163
WebKit.DOM.Element? _container = web_view.get_dom_document().get_element_by_id("message_container");
164
assert(_container != null);
165
container = _container as WebKit.DOM.HTMLDivElement;
166
assert(container != null);
169
set_icon_src("#email_template .menu .icon", "go-down");
170
set_icon_src("#email_template .starred .icon", "starred");
171
set_icon_src("#email_template .unstarred .icon", "non-starred-grey");
172
set_icon_src("#email_template .attachment.icon", "mail-attachment");
175
private void on_resource_request_starting(WebKit.WebFrame web_frame,
176
WebKit.WebResource web_resource, WebKit.NetworkRequest request,
177
WebKit.NetworkResponse? response) {
179
string? uri = request.get_uri();
180
bool uri_is_image = is_image(uri);
181
if (uri_is_image && !load_external_images)
182
external_images_info_bar.show();
183
if (!is_always_loaded(uri) && !(uri_is_image && load_external_images))
184
request.set_uri("about:blank");
187
private bool is_always_loaded(string? uri) {
191
foreach (string prefix in always_loaded_prefixes) {
192
if (uri.has_prefix(prefix))
199
private bool is_image(string? uri) {
204
Regex regex = new Regex("(?:jpe?g|gif|png)$", RegexCompileFlags.CASELESS);
205
return regex.match(uri);
206
} catch (RegexError err) {
207
debug("Error creating image-matching regex: %s", err.message);
212
private void set_load_external_images(bool load_external_images) {
213
this.load_external_images = load_external_images;
215
// Refreshing the images would do nothing in this case--the resource has already been
216
// loaded, so no additional resource request will be sent.
217
if (load_external_images == false)
220
// We can't simply set load_external_images to true before refreshing, then set it back to
221
// false afterwards. If one of the images' sources is redirected, an additional resource
222
// request will come after we reset load_external_images to false.
224
WebKit.DOM.Document document = web_view.get_dom_document();
225
WebKit.DOM.NodeList nodes = document.query_selector_all("img");
226
for (ulong i = 0; i < nodes.length; i++) {
227
WebKit.DOM.Element? element = nodes.item(i) as WebKit.DOM.Element;
231
if (!element.has_attribute("src"))
234
string src = element.get_attribute("src");
235
if (Geary.String.is_empty_or_whitespace(src) || is_always_loaded(src))
238
// Refresh the image source. Requests are denied when load_external_images
239
// is false, so we need to force webkit to send the request again.
240
element.set_attribute("src", src);
242
} catch (Error err) {
243
debug("Error refreshing images: %s", err.message);
247
private void load_user_style() {
249
WebKit.DOM.Document document = web_view.get_dom_document();
250
WebKit.DOM.Element style_element = document.create_element(STYLE_NAME);
251
style_element.set_attribute("id", "user_style");
252
WebKit.DOM.HTMLHeadElement head_element = document.get_head();
253
head_element.append_child(style_element);
255
File user_style = GearyApplication.instance.get_user_config_directory().get_child(USER_CSS);
256
user_style_monitor = user_style.monitor_file(FileMonitorFlags.NONE, null);
257
user_style_monitor.changed.connect(on_user_style_changed);
259
// And call it once to load the initial user style
260
on_user_style_changed(user_style, null, FileMonitorEvent.CREATED);
261
} catch (Error error) {
262
debug("Error setting up user style: %s", error.message);
266
private void on_user_style_changed(File user_style, File? other_file, FileMonitorEvent event_type) {
267
// Changing a file produces 1 created signal, 3 changes done hints, and 0 changed
268
if (event_type != FileMonitorEvent.CHANGED && event_type != FileMonitorEvent.CREATED
269
&& event_type != FileMonitorEvent.DELETED) {
273
debug("Loading new message viewer style from %s...", user_style.get_path());
275
WebKit.DOM.Document document = web_view.get_dom_document();
276
WebKit.DOM.Element style_element = document.get_element_by_id("user_style");
277
ulong n = style_element.child_nodes.length;
279
for (int i = 0; i < n; i++)
280
style_element.remove_child(style_element.first_child);
281
} catch (Error error) {
282
debug("Error removing old user style: %s", error.message);
286
DataInputStream data_input_stream = new DataInputStream(user_style.read());
288
string user_css = data_input_stream.read_upto("\0", 1, out length);
289
WebKit.DOM.Text text_node = document.create_text_node(user_css);
290
style_element.append_child(text_node);
291
} catch (Error error) {
292
// Expected if file was deleted.
296
private void set_icon_src(string selector, string icon_name) {
299
string icon_filename = IconFactory.instance.lookup_icon(icon_name, 16).get_filename();
300
uint8[] icon_content;
301
FileUtils.get_data(icon_filename, out icon_content);
303
// Fetch its mime type.
304
bool uncertain_content_type;
305
string icon_mimetype = ContentType.get_mime_type(ContentType.guess(icon_filename,
306
icon_content, out uncertain_content_type));
308
// Then set the source to a data url.
309
WebKit.DOM.HTMLImageElement img = Util.DOM.select(web_view.get_dom_document(), selector)
310
as WebKit.DOM.HTMLImageElement;
311
set_data_url(img, icon_mimetype, icon_content);
312
} catch (Error error) {
313
warning("Failed to load icon '%s': %s", icon_name, error.message);
317
private void set_image_src(WebKit.DOM.HTMLImageElement img, string mime_type, string filename,
318
int maxwidth, int maxheight = -1) {
319
if( maxheight == -1 ){
320
maxheight = maxwidth;
324
// If the file is an image, use it. Otherwise get the icon for this mime_type.
326
string content_type = ContentType.from_mime_type(mime_type);
327
string icon_mime_type = mime_type;
328
if (mime_type.has_prefix("image/")) {
329
// Get a thumbnail for the image.
330
// TODO Generate and save the thumbnail when extracting the attachments rather than
331
// when showing them in the viewer.
332
img.get_class_list().add("thumbnail");
333
Gdk.Pixbuf image = new Gdk.Pixbuf.from_file_at_scale(filename, maxwidth, maxheight,
335
image.save_to_buffer(out content, "png");
336
icon_mime_type = "image/png";
338
// Load the icon for this mime type.
339
ThemedIcon icon = ContentType.get_icon(content_type) as ThemedIcon;
340
string icon_filename = IconFactory.instance.lookup_icon(icon.names[0], maxwidth)
342
FileUtils.get_data(icon_filename, out content);
343
icon_mime_type = ContentType.get_mime_type(ContentType.guess(icon_filename, content,
347
// Then set the source to a data url.
348
set_data_url(img, icon_mime_type, content);
349
} catch (Error error) {
350
warning("Failed to load image '%s': %s", filename, error.message);
354
private void set_data_url(WebKit.DOM.HTMLImageElement img, string mime_type, uint8[] content)
356
img.set_attribute("src", "data:%s;base64,%s".printf(mime_type, Base64.encode(content)));
359
120
public Geary.Email? get_last_message() {
360
121
return messages.is_empty ? null : messages.last();
363
124
// Removes all displayed e-mails from the view.
364
public void clear(Geary.Folder? new_folder, Geary.AccountSettings? settings) {
125
public void clear(Geary.Folder? new_folder, Geary.AccountInformation? account_information) {
365
126
// Remove all messages from DOM.
367
128
foreach (WebKit.DOM.HTMLElement element in email_to_element.values) {
589
private WebKit.DOM.HTMLElement? closest_ancestor(WebKit.DOM.Element element, string selector) {
591
WebKit.DOM.Element? parent = element.get_parent_element();
592
while (parent != null && !parent.webkit_matches_selector(selector)) {
593
parent = parent.get_parent_element();
595
return parent as WebKit.DOM.HTMLElement;
596
} catch (Error error) {
597
warning("Failed to find ancestor: %s", error.message);
350
public void compress_emails() {
351
if (messages.size == 0)
354
WebKit.DOM.Document document = web_view.get_dom_document();
355
WebKit.DOM.Element first_compressed = null;
356
int compress_count = 0;
357
bool prev_hidden = false, curr_hidden = false, next_hidden = false;
359
next_hidden = document.get_element_by_id(get_div_id(messages.first().id)).get_class_list().contains("hide");
360
} catch (Error error) {
361
debug("Error checking hidden status: %s", error.message);
364
foreach (Geary.Email message in messages) {
366
WebKit.DOM.Element message_element = document.get_element_by_id(get_div_id(message.id));
367
prev_hidden = curr_hidden;
368
curr_hidden = next_hidden;
369
next_hidden = (message_element.next_element_sibling != null)
370
&& message_element.next_element_sibling.get_class_list().contains("hide");
371
if (curr_hidden && prev_hidden && next_hidden) {
372
message_element.get_class_list().add("compressed");
374
if (first_compressed == null)
375
first_compressed = message_element;
376
} else if (compress_count > 0) {
377
if (compress_count == 1) {
378
message_element.previous_element_sibling.get_class_list().remove("compressed");
380
WebKit.DOM.HTMLElement span =
381
first_compressed.first_element_child.first_element_child
382
as WebKit.DOM.HTMLElement;
383
span.set_inner_html(_("%u read messages").printf(compress_count));
384
// We need to set the display to get an accurate offset_height
385
span.set_attribute("style", "display:inline-block;");
386
span.set_attribute("style", "display:inline-block; top:%ipx".printf(
387
(int) (message_element.offset_top - first_compressed.offset_top
388
- span.offset_height) / 2));
391
first_compressed = null;
393
} catch (Error error) {
394
debug("Error compressing emails: %s", error.message);
399
public void decompress_emails(WebKit.DOM.Element email_element) {
400
WebKit.DOM.Element iter_element = email_element;
402
while ((iter_element != null) && iter_element.get_class_list().contains("compressed")) {
403
iter_element.get_class_list().remove("compressed");
404
iter_element.first_element_child.first_element_child.set_attribute("style", "display:none");
405
iter_element = iter_element.previous_element_sibling;
407
} catch (Error error) {
408
debug("Error decompressing emails: %s", error.message);
410
iter_element = email_element.next_element_sibling;
412
while ((iter_element != null) && iter_element.get_class_list().contains("compressed")) {
413
iter_element.get_class_list().remove("compressed");
414
iter_element.first_element_child.first_element_child.set_attribute("style", "display:none");
415
iter_element = iter_element.next_element_sibling;
417
} catch (Error error) {
418
debug("Error decompressing emails: %s", error.message);
602
422
private Geary.Email? get_email_from_element(WebKit.DOM.Element element) {
603
423
// First get the email container.
604
424
WebKit.DOM.Element? email_element = null;
1162
977
// Now look for the signature.
1163
978
wrap_html_signature(ref container);
1165
// Then get all inline images and replace them with data URLs.
1166
WebKit.DOM.NodeList inline_list = container.query_selector_all("img[src^=\"cid:\"]");
1167
for (int i = 0; i < inline_list.length; ++i) {
980
// Then look for all <img> tags. Inline images are replaced with
981
// data URLs, while external images are added to
982
// external_images_uri (to be used later by is_image()).
983
Gee.ArrayList<string> external_images_uri = new Gee.ArrayList<string>();
984
WebKit.DOM.NodeList inline_list = container.query_selector_all("img");
985
for (ulong i = 0; i < inline_list.length; ++i) {
1168
986
// Get the MIME content for the image.
1169
987
WebKit.DOM.HTMLImageElement img = (WebKit.DOM.HTMLImageElement) inline_list.item(i);
1170
string mime_id = img.get_attribute("src").substring(4);
1171
Geary.Memory.AbstractBuffer image_content =
1172
email.get_message().get_content_by_mime_id(mime_id);
1173
uint8[] image_data = image_content.get_array();
1175
// Get the content type.
1176
bool uncertain_content_type;
1177
string mimetype = ContentType.get_mime_type(ContentType.guess(null, image_data,
1178
out uncertain_content_type));
1180
// Then set the source to a data url.
1181
set_data_url(img, mimetype, image_data);
988
string? src = img.get_attribute("src");
989
if (Geary.String.is_empty(src)) {
991
} else if (src.has_prefix("cid:")) {
992
string mime_id = src.substring(4);
993
Geary.Memory.AbstractBuffer image_content =
994
email.get_message().get_content_by_mime_id(mime_id);
995
uint8[] image_data = image_content.get_array();
997
// Get the content type.
998
bool uncertain_content_type;
999
string mimetype = ContentType.get_mime_type(ContentType.guess(null, image_data,
1000
out uncertain_content_type));
1002
// Then set the source to a data url.
1003
web_view.set_data_url(img, mimetype, image_data);
1004
} else if (!src.has_prefix("data:")) {
1005
external_images_uri.add(src);
1006
if (!web_view.load_external_images)
1007
external_images_info_bar.show();
1011
web_view.set_external_images_uris(external_images_uri);
1184
1013
// Now return the whole message.
1185
1014
return set_up_quotes(container.get_inner_html());
1186
1015
} catch (Error e) {