diff --git a/bindings/vapi/gmime-2.6.vapi b/bindings/vapi/gmime-2.6.vapi index 5694a1aa..9981421e 100644 --- a/bindings/vapi/gmime-2.6.vapi +++ b/bindings/vapi/gmime-2.6.vapi @@ -804,19 +804,19 @@ namespace GMime { [CCode (cname = "g_mime_part_get_best_content_encoding")] public GMime.ContentEncoding get_best_content_encoding (GMime.EncodingConstraint constraint); [CCode (cname = "g_mime_part_get_content_description")] - public unowned string get_content_description (); + public unowned string? get_content_description (); [CCode (cname = "g_mime_part_get_content_encoding")] public GMime.ContentEncoding get_content_encoding (); [CCode (cname = "g_mime_part_get_content_id")] - public unowned string get_content_id (); + public unowned string? get_content_id (); [CCode (cname = "g_mime_part_get_content_location")] - public unowned string get_content_location (); + public unowned string? get_content_location (); [CCode (cname = "g_mime_part_get_content_md5")] - public unowned string get_content_md5 (); + public unowned string? get_content_md5 (); [CCode (cname = "g_mime_part_get_content_object")] - public unowned GMime.DataWrapper get_content_object (); + public unowned GMime.DataWrapper? get_content_object (); [CCode (cname = "g_mime_part_get_filename")] - public unowned string get_filename (); + public unowned string? get_filename (); [CCode (cname = "g_mime_part_set_content_description")] public void set_content_description (string description); [CCode (cname = "g_mime_part_set_content_encoding")] diff --git a/bindings/vapi/gmime-2.6/gmime-2.6.metadata b/bindings/vapi/gmime-2.6/gmime-2.6.metadata index f56d5243..e03b4ce6 100644 --- a/bindings/vapi/gmime-2.6/gmime-2.6.metadata +++ b/bindings/vapi/gmime-2.6/gmime-2.6.metadata @@ -11,7 +11,13 @@ g_mime_object_get_content_type_parameter nullable="1" g_mime_object_to_string transfer_ownership="1" g_mime_param_next name="get_next" g_mime_parser_construct_message nullable="1" +g_mime_part_get_content_description nullable="1" +g_mime_part_get_content_location nullable="1" +g_mime_part_get_content_id nullable="1" +g_mime_part_get_content_md5 nullable="1" +g_mime_part_get_content_object nullable="1" g_mime_part_get_content_part nullable="1" +g_mime_part_get_filename nullable="1" g_mime_signer_next name="get_next" g_mime_stream_mem_new_with_buffer.buffer is_array="1" array_length_pos="1.0" type_name="uint8[]" g_mime_stream_mem_new_with_buffer.len hidden="1" diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index 9fbffcd5..0c2e7a84 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -1492,12 +1492,13 @@ public class GearyController : Geary.BaseObject { } } - private void on_save_buffer_to_file(string filename, Geary.Memory.Buffer buffer) { + private void on_save_buffer_to_file(string? filename, Geary.Memory.Buffer buffer) { Gtk.FileChooserDialog dialog = new Gtk.FileChooserDialog(null, main_window, Gtk.FileChooserAction.SAVE, Stock._CANCEL, Gtk.ResponseType.CANCEL, Stock._SAVE, Gtk.ResponseType.ACCEPT, null); if (last_save_directory != null) dialog.set_current_folder(last_save_directory.get_path()); - dialog.set_current_name(filename); + if (!Geary.String.is_empty(filename)) + dialog.set_current_name(filename); dialog.set_do_overwrite_confirmation(true); dialog.confirm_overwrite.connect(on_confirm_overwrite); dialog.set_create_folders(true); diff --git a/src/client/util/util-webkit.vala b/src/client/util/util-webkit.vala index 9567d647..3d1d0f50 100644 --- a/src/client/util/util-webkit.vala +++ b/src/client/util/util-webkit.vala @@ -400,3 +400,50 @@ public string resolve_nesting(string text, string[] values) { } } +// Returns a URI suitable for an IMG SRC attribute (or elsewhere, potentially) that is the +// memory buffer unpacked into a Base-64 encoded data: URI +public string assemble_data_uri(string mimetype, Geary.Memory.Buffer buffer) { + // attempt to use UnownedBytesBuffer to avoid memcpying a potentially huge buffer only to + // free it when the encoding operation is completed + string base64; + Geary.Memory.UnownedBytesBuffer? unowned_bytes = buffer as Geary.Memory.UnownedBytesBuffer; + if (unowned_bytes != null) + base64 = Base64.encode(unowned_bytes.to_unowned_uint8_array()); + else + base64 = Base64.encode(buffer.get_uint8_array()); + + return "data:%s;base64,%s".printf(mimetype, base64); +} + +// Turns the data: URI created by assemble_data_uri() back into its components. The returned +// buffer is decoded. +// +// TODO: Return mimetype +public bool dissasemble_data_uri(string uri, out Geary.Memory.Buffer? buffer) { + buffer = null; + + if (!uri.has_prefix("data:")) + return false; + + // count from semicolon past encoding type specifier + int start_index = uri.index_of(";"); + if (start_index <= 0) + return false; + + // watch for string termination to avoid overflow + int base64_len = "base64,".length; + for (int ctr = 0; ctr < base64_len; ctr++) { + if (uri[start_index++] == Geary.String.EOS) + return false; + } + + // avoid a memory copy of the substring by manually calculating the start address + uint8[] bytes = Base64.decode((string) (((char *) uri) + start_index)); + + // transfer ownership of the byte array directly to the Buffer; this prevents an + // unnecessary copy + buffer = new Geary.Memory.ByteBuffer.take((owned) bytes, bytes.length); + + return true; +} + diff --git a/src/client/views/conversation-viewer.vala b/src/client/views/conversation-viewer.vala index cc6ff3eb..c3e3d84d 100644 --- a/src/client/views/conversation-viewer.vala +++ b/src/client/views/conversation-viewer.vala @@ -23,7 +23,7 @@ public class ConversationViewer : Gtk.Box { private const string MESSAGE_CONTAINER_ID = "message_container"; private const string SELECTION_COUNTER_ID = "multiple_messages"; private const string SPINNER_ID = "spinner"; - private const string REPLACED_IMAGE_CLASS = "replaced_inline_image"; + private const string DATA_IMAGE_CLASS = "data_inline_image"; private enum SearchState { // Search/find states. @@ -94,7 +94,7 @@ public class ConversationViewer : Gtk.Box { public signal void save_attachments(Gee.List attachment); // Fired when the user wants to save an image buffer to disk - public signal void save_buffer_to_file(string filename, Geary.Memory.Buffer buffer); + public signal void save_buffer_to_file(string? filename, Geary.Memory.Buffer buffer); // Fired when the user clicks the edit draft button. public signal void edit_draft(Geary.Email message); @@ -563,7 +563,7 @@ public class ConversationViewer : Gtk.Box { bind_event(web_view, ".email .compressed_note", "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); - bind_event(web_view, "." + REPLACED_IMAGE_CLASS, "contextmenu", (Callback) on_replaced_image_menu, this); + bind_event(web_view, "." + DATA_IMAGE_CLASS, "contextmenu", (Callback) on_data_image_menu, this); bind_event(web_view, ".remote_images .show_images", "click", (Callback) on_show_images, this); bind_event(web_view, ".remote_images .show_from", "click", (Callback) on_show_images_from, this); bind_event(web_view, ".remote_images .close_show_images", "click", (Callback) on_close_show_images, this); @@ -693,46 +693,7 @@ public class ConversationViewer : Gtk.Box { return null; return "\"%s\"".printf( - filename, REPLACED_IMAGE_CLASS, assemble_replaced_image_uri(mimetype, buffer)); - } - - private static string assemble_replaced_image_uri(string mimetype, Geary.Memory.Buffer buffer) { - // attempt to use UnownedBytesBuffer to avoid memcpying a potentially huge buffer only to - // free it when the encoding operation is completed - string base64; - Geary.Memory.UnownedBytesBuffer? unowned_bytes = buffer as Geary.Memory.UnownedBytesBuffer; - if (unowned_bytes != null) - base64 = Base64.encode(unowned_bytes.to_unowned_uint8_array()); - else - base64 = Base64.encode(buffer.get_uint8_array()); - - return "data:%s;base64,%s".printf(mimetype, base64); - } - - // Turns the data: URI created by assemble_replaced_image_uri() back into its components. The - // returned buffer is decoded. - // - // TODO: return mimetype - private static bool dissasemble_replaced_image_uri(string uri, out Geary.Memory.Buffer? buffer) { - buffer = null; - - if (!uri.has_prefix("data:")) - return false; - - // count from semicolon past encoding type specifier - int start_index = uri.index_of(";"); - if (start_index <= 0) - return false; - start_index += "base64,".length; - - // avoid a memory copy of the substring by manually calculating the start address - uint8[] bytes = Base64.decode((string) (((char *) uri) + start_index)); - - // transfer ownership of the byte array directly to the Buffer; this prevents an - // unnecessary copy - buffer = new Geary.Memory.ByteBuffer.take((owned) bytes, bytes.length); - - return true; + filename, DATA_IMAGE_CLASS, assemble_data_uri(mimetype, buffer)); } private void unhide_last_email() { @@ -1328,17 +1289,17 @@ public class ConversationViewer : Gtk.Box { conversation_viewer.show_attachment_menu(email, attachment); } - private static void on_replaced_image_menu(WebKit.DOM.Element element, WebKit.DOM.Event event, + private static void on_data_image_menu(WebKit.DOM.Element element, WebKit.DOM.Event event, ConversationViewer conversation_viewer) { event.stop_propagation(); Geary.Memory.Buffer? buffer; - if (!dissasemble_replaced_image_uri(element.get_attribute("src"), out buffer)) + if (!dissasemble_data_uri(element.get_attribute("src"), out buffer)) return; - string filename = element.get_attribute("alt"); + string? filename = element.get_attribute("alt"); - if (buffer != null && buffer.size > 0 && !Geary.String.is_empty(filename)) + if (buffer != null && buffer.size > 0) conversation_viewer.show_replaced_image_menu(filename, buffer); } @@ -1448,7 +1409,7 @@ public class ConversationViewer : Gtk.Box { return menu; } - private void show_replaced_image_menu(string filename, Geary.Memory.Buffer buffer) { + private void show_replaced_image_menu(string? filename, Geary.Memory.Buffer buffer) { image_menu = new Gtk.Menu(); image_menu.selection_done.connect(() => { image_menu = null; @@ -1634,16 +1595,27 @@ public class ConversationViewer : Gtk.Box { continue; } else if (src.has_prefix("cid:")) { string mime_id = src.substring(4); + + string? filename = message.get_content_filename_by_mime_id(mime_id); Geary.Memory.Buffer image_content = message.get_content_by_mime_id(mime_id); - uint8[] image_data = image_content.get_uint8_array(); - + Geary.Memory.UnownedBytesBuffer? unowned_buffer = + image_content as Geary.Memory.UnownedBytesBuffer; + // Get the content type. - bool uncertain_content_type; - string mimetype = ContentType.get_mime_type(ContentType.guess(null, image_data, - out uncertain_content_type)); - - // Then set the source to a data url. - web_view.set_data_url(img, mimetype, image_data); + 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 URIm the class to a known label for the popup menu, + // and the ALT to its filename, if supplied + 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); } else if (!src.has_prefix("data:")) { remote_images = true; } @@ -1838,7 +1810,8 @@ public class ConversationViewer : Gtk.Box { // 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_image_src(img, attachment.mime_type, attachment.file.get_path(), ATTACHMENT_PREVIEW_SIZE); + web_view.set_attachment_src(img, attachment.mime_type, attachment.file.get_path(), + ATTACHMENT_PREVIEW_SIZE); attachment_container.append_child(attachment_table); } diff --git a/src/client/views/conversation-web-view.vala b/src/client/views/conversation-web-view.vala index 47489357..824ecb7e 100644 --- a/src/client/views/conversation-web-view.vala +++ b/src/client/views/conversation-web-view.vala @@ -202,21 +202,27 @@ public class ConversationWebView : WebKit.WebView { private void set_icon_src(string selector, string icon_name) { try { // Load icon. - uint8[] icon_content = null; + uint8[]? icon_content = null; Gdk.Pixbuf? pixbuf = IconFactory.instance.load_symbolic_colored(icon_name, 16); if (pixbuf != null) pixbuf.save_to_buffer(out icon_content, "png"); // Load as PNG. + if (icon_content == null || icon_content.length == 0) + return; + + Geary.Memory.ByteBuffer buffer = new Geary.Memory.ByteBuffer.take((owned) icon_content, + icon_content.length); + // Then set the source to a data url. WebKit.DOM.HTMLImageElement img = Util.DOM.select(get_dom_document(), selector) as WebKit.DOM.HTMLImageElement; - set_data_url(img, "image/png", icon_content); + img.set_attribute("src", assemble_data_uri("image/png", buffer)); } catch (Error error) { warning("Failed to load icon '%s': %s", icon_name, error.message); } } - public void set_image_src(WebKit.DOM.HTMLImageElement img, string mime_type, string filename, + public void set_attachment_src(WebKit.DOM.HTMLImageElement img, string mime_type, string filename, int maxwidth, int maxheight = -1) { if( maxheight == -1 ){ maxheight = maxwidth; @@ -248,17 +254,14 @@ public class ConversationWebView : WebKit.WebView { } // Then set the source to a data url. - set_data_url(img, icon_mime_type, content); + Geary.Memory.Buffer buffer = new Geary.Memory.ByteBuffer.take((owned) content, + content.length); + img.set_attribute("src", assemble_data_uri(icon_mime_type, buffer)); } catch (Error error) { warning("Failed to load image '%s': %s", filename, error.message); } } - public void set_data_url(WebKit.DOM.HTMLImageElement img, string mime_type, uint8[] content) - throws Error { - img.set_attribute("src", "data:%s;base64,%s".printf(mime_type, Base64.encode(content))); - } - private bool on_navigation_policy_decision_requested(WebKit.WebFrame frame, WebKit.NetworkRequest request, WebKit.WebNavigationAction navigation_action, WebKit.WebPolicyDecision policy_decision) { diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala index fa856984..a05db10f 100644 --- a/src/engine/rfc822/rfc822-message.vala +++ b/src/engine/rfc822/rfc822-message.vala @@ -5,6 +5,13 @@ */ public class Geary.RFC822.Message : BaseObject { + /** + * This delegate is an optional parameter to the body constructers that allows callers + * to process arbitrary non-text, inline MIME parts. + */ + public delegate string? InlinePartReplacer(string filename, string mimetype, + Geary.Memory.Buffer buffer); + private const string DEFAULT_ENCODING = "UTF8"; private const string HEADER_IN_REPLY_TO = "In-Reply-To"; @@ -419,13 +426,6 @@ public class Geary.RFC822.Message : BaseObject { return message_to_memory_buffer(true, dotstuffed); } - /** - * This delegate is an optional parameter to the body constructers that allows callers - * to process arbitrary non-text, inline MIME parts. - */ - public delegate string? InlinePartReplacer(string filename, string mimetype, - Geary.Memory.Buffer buffer); - /** * This method is the main utility method used by the other body constructors. It calls itself * recursively via the last argument ("node"). @@ -605,13 +605,20 @@ public class Geary.RFC822.Message : BaseObject { public Memory.Buffer get_content_by_mime_id(string mime_id) throws RFC822Error { GMime.Part? part = find_mime_part_by_mime_id(message.get_mime_part(), mime_id); - if (part == null) { - throw new RFC822Error.NOT_FOUND("Could not find a MIME part with content-id %s", - mime_id); - } + if (part == null) + throw new RFC822Error.NOT_FOUND("Could not find a MIME part with Content-ID %s", mime_id); + return mime_part_to_memory_buffer(part); } - + + public string? get_content_filename_by_mime_id(string mime_id) throws RFC822Error { + GMime.Part? part = find_mime_part_by_mime_id(message.get_mime_part(), mime_id); + if (part == null) + throw new RFC822Error.NOT_FOUND("Could not find a MIME part with Content-ID %s", mime_id); + + return part.get_filename(); + } + private GMime.Part? find_mime_part_by_mime_id(GMime.Object root, string mime_id) { // If this is a multipart container, check each of its children. if (root is GMime.Multipart) { diff --git a/theming/message-viewer.css b/theming/message-viewer.css index 223af814..926b39b0 100644 --- a/theming/message-viewer.css +++ b/theming/message-viewer.css @@ -203,7 +203,7 @@ body:not(.nohide) .email.hide .header_container .avatar { margin-right: -0.67em; } -.email .replaced_inline_image { +.email .data_inline_image { max-width: 100%; display: block; margin-top: 1em;