diff --git a/po/POTFILES.in b/po/POTFILES.in index 02f5e599..d795f840 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -354,6 +354,7 @@ src/engine/rfc822/rfc822-mailbox-address.vala src/engine/rfc822/rfc822-mailbox-addresses.vala src/engine/rfc822/rfc822-message-data.vala src/engine/rfc822/rfc822-message.vala +src/engine/rfc822/rfc822-part.vala src/engine/rfc822/rfc822-utils.vala src/engine/rfc822/rfc822.vala src/engine/smtp/smtp-authenticator.vala diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4404bde7..8d0174a0 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -275,6 +275,7 @@ engine/rfc822/rfc822-mailbox-addresses.vala engine/rfc822/rfc822-mailbox-address.vala engine/rfc822/rfc822-message.vala engine/rfc822/rfc822-message-data.vala +engine/rfc822/rfc822-part.vala engine/rfc822/rfc822-utils.vala engine/smtp/smtp-authenticator.vala diff --git a/src/client/conversation-viewer/conversation-message.vala b/src/client/conversation-viewer/conversation-message.vala index 75a28adc..877155c1 100644 --- a/src/client/conversation-viewer/conversation-message.vala +++ b/src/client/conversation-viewer/conversation-message.vala @@ -656,37 +656,40 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface { } } - // 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; - } - + // 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(Geary.RFC822.Part part) { + Geary.Mime.ContentType content_type = part.get_effective_content_type(); if (content_type.media_type != "image" || !this.web_view.can_show_mime_type(content_type.to_string())) { - debug("Not displaying %s inline: unsupported Content-Type", content_type.to_string()); + debug("Not displaying %s inline: unsupported Content-Type", + content_type.to_string()); return null; } - string id = content_id; + string? id = part.content_id; if (id == null) { id = REPLACED_CID_TEMPLATE.printf(this.next_replaced_buffer_number++); } - this.web_view.add_internal_resource(id, buffer); + try { + this.web_view.add_internal_resource(id, part.write_to_buffer()); + } catch (Geary.RFC822Error err) { + debug("Failed to get inline buffer: %s", err.message); + return null; + } // Translators: This string is used as the HTML IMG ALT // attribute value when displaying an inline image in an email // that did not specify a file name. E.g. Image".printf( diff --git a/src/engine/imap-db/imap-db-attachment.vala b/src/engine/imap-db/imap-db-attachment.vala index eecfd9d3..f840aeb8 100644 --- a/src/engine/imap-db/imap-db-attachment.vala +++ b/src/engine/imap-db/imap-db-attachment.vala @@ -15,11 +15,10 @@ private class Geary.ImapDB.Attachment : Geary.Attachment { internal int64 message_id { get; private set; } - private int64 attachment_id; + private int64 attachment_id = -1; private Attachment(int64 message_id, - int64 attachment_id, Mime.ContentType content_type, string? content_id, string? content_description, @@ -34,31 +33,24 @@ private class Geary.ImapDB.Attachment : Geary.Attachment { ); this.message_id = message_id; - this.attachment_id = attachment_id; } - internal Attachment.from_part(int64 message_id, GMime.Part part) + internal Attachment.from_part(int64 message_id, RFC822.Part part) throws Error { - GMime.ContentType? part_type = part.get_content_type(); - Mime.ContentType type = (part_type != null) - ? new Mime.ContentType.from_gmime(part_type) - : Mime.ContentType.ATTACHMENT_DEFAULT; - - GMime.ContentDisposition? part_disposition = part.get_content_disposition(); - Mime.ContentDisposition disposition = (part_disposition != null) - ? new Mime.ContentDisposition.from_gmime(part_disposition) - : new Mime.ContentDisposition.simple( + Mime.ContentDisposition? disposition = part.content_disposition; + if (disposition == null) { + disposition = new Mime.ContentDisposition.simple( Geary.Mime.DispositionType.UNSPECIFIED ); + } this( message_id, - -1, // This gets set only after saving - type, - part.get_content_id(), - part.get_content_description(), + part.get_effective_content_type(), + part.content_id, + part.content_description, disposition, - RFC822.Utils.get_clean_attachment_filename(part) + part.get_clean_filename() ); } @@ -79,7 +71,6 @@ private class Geary.ImapDB.Attachment : Geary.Attachment { this( result.rowid_for("message_id"), - result.rowid_for("id"), Mime.ContentType.deserialize(result.nonnull_string_for("mime_type")), result.string_for("content_id"), result.string_for("description"), @@ -87,13 +78,15 @@ private class Geary.ImapDB.Attachment : Geary.Attachment { content_filename ); + this.attachment_id = result.rowid_for("id"); + set_file_info( generate_file(attachments_dir), result.int64_for("filesize") ); } internal void save(Db.Connection cx, - GMime.Part part, + RFC822.Part part, GLib.File attachments_dir, Cancellable? cancellable) throws Error { @@ -155,7 +148,7 @@ private class Geary.ImapDB.Attachment : Geary.Attachment { // This isn't async since its only callpaths are via db async // transactions, which run in independent threads - private void save_file(GMime.Part part, + private void save_file(RFC822.Part part, GLib.File attachments_dir, Cancellable? cancellable) throws Error { @@ -182,31 +175,26 @@ private class Geary.ImapDB.Attachment : Geary.Attachment { // All good } - // Save the data to disk if there is any. - GMime.DataWrapper? attachment_data = part.get_content_object(); - if (attachment_data != null) { - GLib.OutputStream target_stream = target.create( - FileCreateFlags.NONE, cancellable - ); - GMime.Stream stream = new Geary.Stream.MimeOutputStream( - target_stream - ); - stream = new GMime.StreamBuffer( - stream, GMime.StreamBufferMode.BLOCK_WRITE - ); + GLib.OutputStream target_stream = target.create( + FileCreateFlags.NONE, cancellable + ); + GMime.Stream stream = new Geary.Stream.MimeOutputStream( + target_stream + ); + stream = new GMime.StreamBuffer( + stream, GMime.StreamBufferMode.BLOCK_WRITE + ); - attachment_data.write_to_stream(stream); + part.write_to_stream(stream); - // Using the stream's length is a bit of a hack, but at - // least on one system we are getting 0 back for the file - // size if we use target.query_info(). - stream.flush(); - int64 file_size = stream.length(); + // Using the stream's length is a bit of a hack, but at + // least on one system we are getting 0 back for the file + // size if we use target.query_info(). + int64 file_size = stream.length(); - stream.close(); + stream.close(); - set_file_info(target, file_size); - } + set_file_info(target, file_size); } private void update_db(Db.Connection cx, Cancellable? cancellable) @@ -234,11 +222,11 @@ private class Geary.ImapDB.Attachment : Geary.Attachment { internal static Gee.List save_attachments(Db.Connection cx, GLib.File attachments_path, int64 message_id, - Gee.List attachments, + Gee.List attachments, Cancellable? cancellable) throws Error { Gee.List list = new Gee.LinkedList(); - foreach (GMime.Part part in attachments) { + foreach (RFC822.Part part in attachments) { Attachment attachment = new Attachment.from_part(message_id, part); attachment.save(cx, part, attachments_path, cancellable); list.add(attachment); diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala index 1fd86cf2..807b6c06 100644 --- a/src/engine/imap-db/imap-db-database.vala +++ b/src/engine/imap-db/imap-db-database.vala @@ -496,7 +496,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase { } // build a list of attachments in the message itself - Gee.List msg_attachments = + Gee.List msg_attachments = message.get_attachments(); try { diff --git a/src/engine/imap/api/imap-folder-session.vala b/src/engine/imap/api/imap-folder-session.vala index 926edd1c..aee63178 100644 --- a/src/engine/imap/api/imap-folder-session.vala +++ b/src/engine/imap/api/imap-folder-session.vala @@ -996,8 +996,8 @@ private class Geary.Imap.FolderSession : Geary.Imap.SessionObject { if (fetched_data.body_data_map.has_key(preview_specifier) && fetched_data.body_data_map.has_key(preview_charset_specifier)) { email.set_message_preview(new RFC822.PreviewText.with_header( - fetched_data.body_data_map.get(preview_specifier), - fetched_data.body_data_map.get(preview_charset_specifier))); + fetched_data.body_data_map.get(preview_charset_specifier), + fetched_data.body_data_map.get(preview_specifier))); } else { message("[%s] No preview specifiers \"%s\" and \"%s\" found", folder_name, preview_specifier.to_string(), preview_charset_specifier.to_string()); diff --git a/src/engine/meson.build b/src/engine/meson.build index 717ea20b..f759ec8a 100644 --- a/src/engine/meson.build +++ b/src/engine/meson.build @@ -272,6 +272,7 @@ geary_engine_vala_sources = files( 'rfc822/rfc822-mailbox-address.vala', 'rfc822/rfc822-message.vala', 'rfc822/rfc822-message-data.vala', + 'rfc822/rfc822-part.vala', 'rfc822/rfc822-utils.vala', 'smtp/smtp-authenticator.vala', diff --git a/src/engine/rfc822/rfc822-message-data.vala b/src/engine/rfc822/rfc822-message-data.vala index adb9da97..a643e0fa 100644 --- a/src/engine/rfc822/rfc822-message-data.vala +++ b/src/engine/rfc822/rfc822-message-data.vala @@ -373,49 +373,46 @@ public class Geary.RFC822.PreviewText : Geary.RFC822.Text { base (_buffer); } - public PreviewText.with_header(Memory.Buffer preview, Memory.Buffer preview_header) { - string? charset = null; - string? encoding = null; - bool is_plain = false; - bool is_html = false; + public PreviewText.with_header(Memory.Buffer preview_header, Memory.Buffer preview) { + string preview_text = ""; // Parse the header. GMime.Stream header_stream = Utils.create_stream_mem(preview_header); GMime.Parser parser = new GMime.Parser.with_stream(header_stream); - GMime.Part? part = parser.construct_part() as GMime.Part; - if (part != null) { - Mime.ContentType? content_type = null; - if (part.get_content_type() != null) { - content_type = new Mime.ContentType.from_gmime(part.get_content_type()); - is_plain = content_type.is_type("text", "plain"); - is_html = content_type.is_type("text", "html"); - charset = content_type.params.get_value("charset"); + GMime.Part? gpart = parser.construct_part() as GMime.Part; + if (gpart != null) { + Part part = new Part(gpart); + + Mime.ContentType content_type = part.get_effective_content_type(); + bool is_plain = content_type.is_type("text", "plain"); + bool is_html = content_type.is_type("text", "html"); + + if (is_plain || is_html) { + // Parse the partial body + GMime.DataWrapper body = new GMime.DataWrapper.with_stream( + new GMime.StreamMem.with_buffer(preview.get_uint8_array()), + gpart.get_content_encoding() + ); + gpart.set_content_object(body); + + ByteArray output = new ByteArray(); + GMime.StreamMem output_stream = + new GMime.StreamMem.with_byte_array(output); + output_stream.set_owner(false); + + try { + part.write_to_stream(output_stream); + uint8[] data = output.data; + data += (uint8) '\0'; + + preview_text = Geary.RFC822.Utils.to_preview_text( + (string) data, + is_html ? TextFormat.HTML : TextFormat.PLAIN + ); + } catch (RFC822Error err) { + debug("Failed to parse preview body: %s", err.message); + } } - - encoding = part.get_header("Content-Transfer-Encoding"); - } - - string preview_text = ""; - if (is_plain || is_html) { - // Parse the preview - GMime.StreamMem input_stream = Utils.create_stream_mem(preview); - ByteArray output = new ByteArray(); - GMime.StreamMem output_stream = new GMime.StreamMem.with_byte_array(output); - output_stream.set_owner(false); - - // Convert the encoding and character set. - GMime.StreamFilter filter = new GMime.StreamFilter(output_stream); - if (encoding != null) - filter.add(new GMime.FilterBasic(GMime.content_encoding_from_string(encoding), false)); - - filter.add(Geary.RFC822.Utils.create_utf8_filter_charset(charset)); - filter.add(new GMime.FilterCRLF(false, false)); - - input_stream.write_to_stream(filter); - uint8[] data = output.data; - data += (uint8) '\0'; - - preview_text = Geary.RFC822.Utils.to_preview_text((string) data, is_html ? TextFormat.HTML : TextFormat.PLAIN); } base(new Geary.Memory.StringBuffer(preview_text)); diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala index a0e07095..15c90277 100644 --- a/src/engine/rfc822/rfc822-message.vala +++ b/src/engine/rfc822/rfc822-message.vala @@ -16,15 +16,18 @@ 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. + * Callback for including non-text MIME entities in message bodies. * - * This is only called for non-text MIME parts in mixed multipart sections. Inline parts - * referred to by rich text in alternative or related documents must be located by the caller - * and appropriately presented. + * This delegate is an optional parameter to the body constructors + * that allows callers to process arbitrary non-text, inline MIME + * parts. + * + * This is only called for non-text MIME parts in mixed multipart + * sections. Inline parts referred to by rich text in alternative + * or related documents must be located by the caller and + * appropriately presented. */ - public delegate string? InlinePartReplacer(string? filename, Mime.ContentType? content_type, - Mime.ContentDisposition? disposition, string? content_id, Geary.Memory.Buffer buffer); + public delegate string? InlinePartReplacer(Part part); private const string HEADER_SENDER = "Sender"; private const string HEADER_IN_REPLY_TO = "In-Reply-To"; @@ -485,43 +488,29 @@ public class Geary.RFC822.Message : BaseObject { * construct_body_from_mime_parts. */ private bool has_body_parts(GMime.Object node, string text_subtype) { - bool has_part = false; + Part part = new Part(node); + bool is_matching_part = false; - // RFC 2045 Section 5.2 allows us to assume - // text/plain US-ASCII if no content type is - // otherwise specified. - Mime.ContentType this_content_type = Mime.ContentType.DISPLAY_DEFAULT; - if (node.get_content_type() != null) { - this_content_type = new Mime.ContentType.from_gmime( - node.get_content_type() + if (node is GMime.Multipart) { + GMime.Multipart multipart = (GMime.Multipart) node; + int count = multipart.get_count(); + for (int i = 0; i < count && !is_matching_part; i++) { + is_matching_part = has_body_parts( + multipart.get_part(i), text_subtype + ); + } + } else if (node is GMime.Part) { + Mime.DispositionType disposition = Mime.DispositionType.UNSPECIFIED; + if (part.content_disposition != null) { + disposition = part.content_disposition.disposition_type; + } + + is_matching_part = ( + disposition != Mime.DispositionType.ATTACHMENT && + part.get_effective_content_type().is_type("text", text_subtype) ); } - - GMime.Multipart? multipart = node as GMime.Multipart; - if (multipart != null) { - int count = multipart.get_count(); - for (int i = 0; i < count && !has_part; ++i) { - has_part = has_body_parts(multipart.get_part(i), text_subtype); - } - } else { - GMime.Part? part = node as GMime.Part; - if (part != null) { - Mime.ContentDisposition? disposition = null; - if (part.get_content_disposition() != null) - disposition = new Mime.ContentDisposition.from_gmime( - part.get_content_disposition() - ); - - if (disposition == null || - disposition.disposition_type != Mime.DispositionType.ATTACHMENT) { - if (this_content_type.has_media_type("text") && - this_content_type.has_media_subtype(text_subtype)) { - has_part = true; - } - } - } - } - return has_part; + return is_matching_part; } /** @@ -540,23 +529,22 @@ public class Geary.RFC822.Message : BaseObject { * * @return Whether a text part with the desired text_subtype was found */ - private bool construct_body_from_mime_parts(GMime.Object node, Mime.MultipartSubtype container_subtype, - string text_subtype, bool to_html, InlinePartReplacer? replacer, ref string? body) throws RFC822Error { - // RFC 2045 Section 5.2 allows us to assume text/plain - // US-ASCII if no content type is otherwise specified. - Mime.ContentType this_content_type = Mime.ContentType.DISPLAY_DEFAULT; - if (node.get_content_type() != null) { - this_content_type = new Mime.ContentType.from_gmime( - node.get_content_type() - ); - } + private bool construct_body_from_mime_parts(GMime.Object node, + Mime.MultipartSubtype container_subtype, + string text_subtype, + bool to_html, + InlinePartReplacer? replacer, + ref string? body) + throws RFC822Error { + Part part = new Part(node); + Mime.ContentType content_type = part.get_effective_content_type(); // If this is a multipart, call ourselves recursively on the children GMime.Multipart? multipart = node as GMime.Multipart; if (multipart != null) { - Mime.MultipartSubtype this_subtype = Mime.MultipartSubtype.from_content_type(this_content_type, - null); - + Mime.MultipartSubtype this_subtype = + Mime.MultipartSubtype.from_content_type(content_type, null); + bool found_text_subtype = false; StringBuilder builder = new StringBuilder(); @@ -576,45 +564,33 @@ public class Geary.RFC822.Message : BaseObject { return found_text_subtype; } - - // Only process inline leaf parts - GMime.Part? part = node as GMime.Part; - if (part == null) - return false; - - Mime.ContentDisposition? disposition = null; - if (part.get_content_disposition() != null) - disposition = new Mime.ContentDisposition.from_gmime(part.get_content_disposition()); - - // Stop processing if the part is an attachment - if (disposition != null && disposition.disposition_type == Mime.DispositionType.ATTACHMENT) - return false; - - // Assemble body from text parts that are not attachments - if (this_content_type != null && this_content_type.has_media_type("text")) { - if (this_content_type.has_media_subtype(text_subtype)) { - body = mime_part_to_memory_buffer(part, true, to_html).to_string(); - - return true; - } - - // We were the wrong kind of text part - return false; + + Mime.DispositionType disposition = Mime.DispositionType.UNSPECIFIED; + if (part.content_disposition != null) { + disposition = part.content_disposition.disposition_type; } - // Use inline part replacer *only* for inline parts and if in - // a mixed multipart where each element is to be presented to - // the user as structure dictates; For alternative and - // related, the inline part is referred to elsewhere in the - // document and it's the callers responsibility to locate them - if (replacer != null && disposition != null && - disposition.disposition_type == Mime.DispositionType.INLINE && - container_subtype == Mime.MultipartSubtype.MIXED) { - body = replacer(RFC822.Utils.get_clean_attachment_filename(part), - this_content_type, - disposition, - part.get_content_id(), - mime_part_to_memory_buffer(part)); + // Process inline leaf parts + if (node is GMime.Part && + disposition != Mime.DispositionType.ATTACHMENT) { + + // Assemble body from matching text parts, else use inline + // part replacer *only* for inline parts and if in a mixed + // multipart where each element is to be presented to the + // user as structure dictates; For alternative and + // related, the inline part is referred to elsewhere in + // the document and it's the callers responsibility to + // locate them + + if (content_type.is_type("text", text_subtype)) { + body = part.write_to_buffer( + to_html ? Part.BodyFormatting.HTML : Part.BodyFormatting.NONE + ).to_string(); + } else if (replacer != null && + disposition == Mime.DispositionType.INLINE && + container_subtype == Mime.MultipartSubtype.MIXED) { + body = replacer(part); + } } return body != null; @@ -751,47 +727,10 @@ public class Geary.RFC822.Message : BaseObject { return searchable; } - 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); - - 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) { - GMime.Multipart multipart = root as GMime.Multipart; - int count = multipart.get_count(); - for (int i = 0; i < count; ++i) { - GMime.Part? child_part = find_mime_part_by_mime_id(multipart.get_part(i), mime_id); - if (child_part != null) { - return child_part; - } - } - } - - // Otherwise, check this part's content id. - GMime.Part? part = root as GMime.Part; - if (part != null && part.get_content_id() == mime_id) { - return part; - } - return null; - } - // UNSPECIFIED disposition means "return all Mime parts" - internal Gee.List get_attachments( + internal Gee.List get_attachments( Mime.DispositionType disposition = Mime.DispositionType.UNSPECIFIED) throws RFC822Error { - Gee.List attachments = new Gee.ArrayList(); + Gee.List attachments = new Gee.LinkedList(); get_attachments_recursively(attachments, message.get_mime_part(), disposition); return attachments; } @@ -875,21 +814,19 @@ public class Geary.RFC822.Message : BaseObject { return ids; } - private void get_attachments_recursively(Gee.List attachments, GMime.Object root, - Mime.DispositionType requested_disposition) throws RFC822Error { - // If this is a multipart container, dive into each of its children. - GMime.Multipart? multipart = root as GMime.Multipart; - if (multipart != null) { + private void get_attachments_recursively(Gee.List attachments, + GMime.Object root, + Mime.DispositionType requested_disposition) + throws RFC822Error { + + if (root is GMime.Multipart) { + GMime.Multipart multipart = (GMime.Multipart) root; int count = multipart.get_count(); for (int i = 0; i < count; ++i) { get_attachments_recursively(attachments, multipart.get_part(i), requested_disposition); } - return; - } - - // If this is an attached message, go through it. - GMime.MessagePart? messagepart = root as GMime.MessagePart; - if (messagepart != null) { + } else if (root is GMime.MessagePart) { + GMime.MessagePart messagepart = (GMime.MessagePart) root; GMime.Message message = messagepart.get_message(); bool is_unknown; Mime.DispositionType disposition = Mime.DispositionType.deserialize(root.get_disposition(), @@ -907,40 +844,37 @@ public class Geary.RFC822.Message : BaseObject { GMime.Part part = new GMime.Part.with_type("message", "rfc822"); part.set_content_object(data); part.set_filename((message.get_subject() ?? _("(no subject)")) + ".eml"); - attachments.add(part); + attachments.add(new Part(part)); } - + get_attachments_recursively(attachments, message.get_mime_part(), requested_disposition); - return; - } - - // Otherwise, check if this part should be an attachment - GMime.Part? part = root as GMime.Part; - if (part == null) { - return; - } - - // If requested disposition is not UNSPECIFIED, check if this part matches the requested deposition - Mime.DispositionType part_disposition = Mime.DispositionType.deserialize(part.get_disposition(), - null); - if (requested_disposition != Mime.DispositionType.UNSPECIFIED && requested_disposition != part_disposition) - return; - - // skip text/plain and text/html parts that are INLINE or UNSPECIFIED, as they will be used - // as part of the body - if (part.get_content_type() != null) { - Mime.ContentType content_type = new Mime.ContentType.from_gmime(part.get_content_type()); - if ((part_disposition == Mime.DispositionType.INLINE || part_disposition == Mime.DispositionType.UNSPECIFIED) - && content_type.has_media_type("text") - && (content_type.has_media_subtype("html") || content_type.has_media_subtype("plain"))) { - return; + } else if (root is GMime.Part) { + Part part = new Part(root); + + Mime.DispositionType actual_disposition = + Mime.DispositionType.UNSPECIFIED; + if (part.content_disposition != null) { + actual_disposition = part.content_disposition.disposition_type; + } + + if (requested_disposition == Mime.DispositionType.UNSPECIFIED || + actual_disposition == requested_disposition) { + + Mime.ContentType content_type = + part.get_effective_content_type(); + + // Skip text/plain and text/html parts that are INLINE + // or UNSPECIFIED, as they will be included in the body + if (actual_disposition == Mime.DispositionType.ATTACHMENT || + (!content_type.is_type("text", "plain") && + !content_type.is_type("text", "html"))) { + attachments.add(part); + } } } - - attachments.add(part); } - + public Gee.List get_sub_messages() { Gee.List messages = new Gee.ArrayList(); find_sub_messages(messages, message.get_mime_part()); @@ -985,57 +919,6 @@ public class Geary.RFC822.Message : BaseObject { return new Memory.ByteBuffer.from_byte_array(byte_array); } - - private Memory.Buffer mime_part_to_memory_buffer(GMime.Part part, - bool to_utf8 = false, bool to_html = false) throws RFC822Error { - Mime.ContentType? content_type = null; - if (part.get_content_type() != null) - content_type = new Mime.ContentType.from_gmime(part.get_content_type()); - - GMime.DataWrapper? wrapper = part.get_content_object(); - if (wrapper == null) { - throw new RFC822Error.INVALID("Could not get the content wrapper for content-type %s", - content_type.to_string()); - } - - ByteArray byte_array = new ByteArray(); - GMime.StreamMem stream = new GMime.StreamMem.with_byte_array(byte_array); - stream.set_owner(false); - - if (to_utf8) { - // Assume encoded text, convert to unencoded UTF-8 - GMime.StreamFilter stream_filter = new GMime.StreamFilter(stream); - string? charset = (content_type != null) ? content_type.params.get_value("charset") : null; - stream_filter.add(Geary.RFC822.Utils.create_utf8_filter_charset(charset)); - - bool flowed = (content_type != null) ? content_type.params.has_value_ci("format", "flowed") : false; - bool delsp = (content_type != null) ? content_type.params.has_value_ci("DelSp", "yes") : false; - - // Unconditionally remove the CR's in any CRLF sequence, since - // they are effectively a wire encoding. - stream_filter.add(new GMime.FilterCRLF(false, false)); - - if (flowed) - stream_filter.add(new Geary.RFC822.FilterFlowed(to_html, delsp)); - - if (to_html) { - if (!flowed) - stream_filter.add(new Geary.RFC822.FilterPlain()); - stream_filter.add(new GMime.FilterHTML( - GMime.FILTER_HTML_CONVERT_URLS | GMime.FILTER_HTML_CONVERT_ADDRESSES, 0)); - stream_filter.add(new Geary.RFC822.FilterBlockquotes()); - } - - wrapper.write_to_stream(stream_filter); - stream_filter.flush(); - } else { - // Keep as binary - wrapper.write_to_stream(stream); - stream.flush(); - } - - return new Geary.Memory.ByteBuffer.from_byte_array(byte_array); - } public string to_string() { return message.to_string(); diff --git a/src/engine/rfc822/rfc822-part.vala b/src/engine/rfc822/rfc822-part.vala new file mode 100644 index 00000000..53bf8468 --- /dev/null +++ b/src/engine/rfc822/rfc822-part.vala @@ -0,0 +1,195 @@ +/* + * Copyright 2016 Software Freedom Conservancy Inc. + * Copyright 2018 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * An RFC-2045 style MIME entity. + * + * This object provides a convenient means accessing the high-level + * MIME entity header field values that are useful to applications and + * decoded forms of the entity body. + */ +public class Geary.RFC822.Part : Object { + + + /** Specifies a format to apply to body data when writing it. */ + public enum BodyFormatting { + + /** No formatting will be applied. */ + NONE, + + /** Plain text bodies will be formatted as HTML. */ + HTML; + } + + /** + * The entity's Content-Type. + * + * See [[https://tools.ietf.org/html/rfc2045#section-5]] + */ + public Mime.ContentType? content_type { get; private set; } + + /** + * The entity's Content-ID. + * + * See [[https://tools.ietf.org/html/rfc2045#section-5]], + * [[https://tools.ietf.org/html/rfc2111]] and {@link + * Email.get_attachment_by_content_id}. + */ + public string? content_id { get; private set; } + + /** + * The entity's Content-Description. + * + * See [[https://tools.ietf.org/html/rfc2045#section-8]] + */ + public string? content_description { get; private set; } + + /** + * The entity's Content-Disposition. + * + * See [[https://tools.ietf.org/html/rfc2183]] + */ + public Mime.ContentDisposition? content_disposition { get; private set; } + + private GMime.Object source_object; + private GMime.Part? source_part; + + + internal Part(GMime.Object source) { + this.source_object = source; + this.source_part = source as GMime.Part; + + GMime.ContentType? part_type = source.get_content_type(); + if (part_type != null) { + this.content_type = new Mime.ContentType.from_gmime(part_type); + } + + this.content_id = source.get_content_id(); + + this.content_description = (this.source_part != null) + ? source_part.get_content_description() : null; + + GMime.ContentDisposition? part_disposition = source.get_content_disposition(); + if (part_disposition != null) { + this.content_disposition = new Mime.ContentDisposition.from_gmime( + part_disposition + ); + } + } + + /** + * The entity's effective Content-Type. + * + * This returns the entity's content type if set, else returns + * {@link Geary.Mime.ContentType.DISPLAY_DEFAULT} this is a + * displayable (i.e. non-attachment) entity, or {@link + * Geary.Mime.ContentType.} + */ + public Mime.ContentType get_effective_content_type() { + Mime.ContentType? type = this.content_type; + if (type == null) { + Mime.DispositionType disposition = Mime.DispositionType.UNSPECIFIED; + if (this.content_disposition != null) { + disposition = this.content_disposition.disposition_type; + } + type = (disposition != Mime.DispositionType.ATTACHMENT) + ? Mime.ContentType.DISPLAY_DEFAULT + : Mime.ContentType.ATTACHMENT_DEFAULT; + } + return type; + } + + /** + * Returns the entity's filename, cleaned for use in the file system. + */ + public string? get_clean_filename() { + string? filename = (this.source_part != null) + ? this.source_part.get_filename() : null; + if (filename != null) { + try { + filename = invalid_filename_character_re.replace_literal( + filename, filename.length, 0, "_" + ); + } catch (RegexError e) { + debug("Error sanitizing attachment filename: %s", e.message); + } + } + return filename; + } + + public Memory.Buffer write_to_buffer(BodyFormatting format = BodyFormatting.NONE) + throws RFC822Error { + ByteArray byte_array = new ByteArray(); + GMime.StreamMem stream = new GMime.StreamMem.with_byte_array(byte_array); + stream.set_owner(false); + + write_to_stream(stream, format); + + return new Geary.Memory.ByteBuffer.from_byte_array(byte_array); + } + + internal void write_to_stream(GMime.Stream destination, + BodyFormatting format = BodyFormatting.NONE) + throws RFC822Error { + GMime.DataWrapper? wrapper = (this.source_part != null) + ? this.source_part.get_content_object() : null; + if (wrapper == null) { + throw new RFC822Error.INVALID( + "Could not get the content wrapper for content-type %s", + content_type.to_string() + ); + } + + Mime.ContentType content_type = this.get_effective_content_type(); + if (content_type.is_type("text", Mime.ContentType.WILDCARD)) { + // Assume encoded text, convert to unencoded UTF-8 + GMime.StreamFilter filter = new GMime.StreamFilter(destination); + string? charset = content_type.params.get_value("charset"); + filter.add( + Geary.RFC822.Utils.create_utf8_filter_charset(charset) + ); + + bool flowed = content_type.params.has_value_ci("format", "flowed"); + bool delsp = content_type.params.has_value_ci("DelSp", "yes"); + + // Unconditionally remove the CR's in any CRLF sequence, since + // they are effectively a wire encoding. + filter.add(new GMime.FilterCRLF(false, false)); + + if (flowed) { + filter.add( + new Geary.RFC822.FilterFlowed( + format == BodyFormatting.HTML, delsp + ) + ); + } + + if (format == BodyFormatting.HTML) { + if (!flowed) { + filter.add(new Geary.RFC822.FilterPlain()); + } + filter.add( + new GMime.FilterHTML( + GMime.FILTER_HTML_CONVERT_URLS | + GMime.FILTER_HTML_CONVERT_ADDRESSES, + 0 + ) + ); + filter.add(new Geary.RFC822.FilterBlockquotes()); + } + + wrapper.write_to_stream(filter); + filter.flush(); + } else { + // Keep as binary + wrapper.write_to_stream(destination); + destination.flush(); + } + } + +} diff --git a/src/engine/rfc822/rfc822-utils.vala b/src/engine/rfc822/rfc822-utils.vala index 8611e645..44acbc49 100644 --- a/src/engine/rfc822/rfc822-utils.vala +++ b/src/engine/rfc822/rfc822-utils.vala @@ -433,19 +433,5 @@ public GMime.ContentEncoding get_best_encoding(GMime.Stream in_stream) { return filter.encoding(GMime.EncodingConstraint.7BIT); } -public string? get_clean_attachment_filename(GMime.Part part) { - string? filename = part.get_filename(); - if (filename != null) { - try { - filename = invalid_filename_character_re.replace_literal( - filename, filename.length, 0, "_" - ); - } catch (RegexError e) { - debug("Error sanitizing attachment filename: %s", e.message); - } - } - return filename; -} - } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2841073b..fc276ce4 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -39,6 +39,7 @@ set(TEST_ENGINE_SRC engine/rfc822-mailbox-addresses-test.vala engine/rfc822-message-test.vala engine/rfc822-message-data-test.vala + engine/rfc822-part-test.vala engine/rfc822-utils-test.vala engine/util-html-test.vala engine/util-idle-manager-test.vala diff --git a/test/engine/imap-db/imap-db-attachment-test.vala b/test/engine/imap-db/imap-db-attachment-test.vala index ae11db83..0ecfa276 100644 --- a/test/engine/imap-db/imap-db-attachment-test.vala +++ b/test/engine/imap-db/imap-db-attachment-test.vala @@ -22,7 +22,9 @@ class Geary.ImapDB.AttachmentTest : TestCase { GMime.Part part = new_part(null, TEXT_ATTACHMENT.data); part.set_header("Content-Type", ""); - Attachment test = new Attachment.from_part(1, part); + Attachment test = new Attachment.from_part( + 1, new Geary.RFC822.Part(part) + ); assert_string( Geary.Mime.ContentType.ATTACHMENT_DEFAULT.to_string(), test.content_type.to_string() @@ -53,7 +55,9 @@ class Geary.ImapDB.AttachmentTest : TestCase { ) ); - Attachment test = new Attachment.from_part(1, part); + Attachment test = new Attachment.from_part( + 1, new Geary.RFC822.Part(part) + ); assert_string(TYPE, test.content_type.to_string()); assert_string(ID, test.content_id); @@ -72,7 +76,9 @@ class Geary.ImapDB.AttachmentTest : TestCase { new GMime.ContentDisposition.from_string("inline") ); - Attachment test = new Attachment.from_part(1, part); + Attachment test = new Attachment.from_part( + 1, new Geary.RFC822.Part(part) + ); assert_int( Geary.Mime.DispositionType.INLINE, @@ -149,7 +155,9 @@ CREATE TABLE MessageAttachmentTable ( this.db.get_master_connection(), this.tmp_dir, 1, - new Gee.ArrayList.wrap({ part }), + new Gee.ArrayList.wrap({ + new Geary.RFC822.Part(part) + }), null ); @@ -201,7 +209,9 @@ CREATE TABLE MessageAttachmentTable ( this.db.get_master_connection(), this.tmp_dir, 1, - new Gee.ArrayList.wrap({ part }), + new Gee.ArrayList.wrap({ + new Geary.RFC822.Part(part) + }), null ); @@ -269,7 +279,9 @@ VALUES (2, 'text/plain'); this.db.get_master_connection(), this.tmp_dir, 1, - new Gee.ArrayList.wrap({ part }), + new Gee.ArrayList.wrap({ + new Geary.RFC822.Part(part) + }), null ); diff --git a/test/engine/rfc822-message-data-test.vala b/test/engine/rfc822-message-data-test.vala index 156fe48f..5251fd9d 100644 --- a/test/engine/rfc822-message-data-test.vala +++ b/test/engine/rfc822-message-data-test.vala @@ -14,30 +14,30 @@ class Geary.RFC822.MessageDataTest : TestCase { public void preview_text_with_header() throws Error { PreviewText plain_preview1 = new PreviewText.with_header( - new Geary.Memory.StringBuffer(PLAIN_BODY1_ENCODED), - new Geary.Memory.StringBuffer(PLAIN_BODY1_HEADERS) + new Geary.Memory.StringBuffer(PLAIN_BODY1_HEADERS), + new Geary.Memory.StringBuffer(PLAIN_BODY1_ENCODED) ); - assert(plain_preview1.buffer.to_string() == PLAIN_BODY1_EXPECTED); + assert_string(PLAIN_BODY1_EXPECTED, plain_preview1.buffer.to_string()); PreviewText base64_preview = new PreviewText.with_header( - new Geary.Memory.StringBuffer(BASE64_BODY_ENCODED), - new Geary.Memory.StringBuffer(BASE64_BODY_HEADERS) + new Geary.Memory.StringBuffer(BASE64_BODY_HEADERS), + new Geary.Memory.StringBuffer(BASE64_BODY_ENCODED) ); - assert(base64_preview.buffer.to_string() == BASE64_BODY_EXPECTED); + assert_string(BASE64_BODY_EXPECTED, base64_preview.buffer.to_string()); string html_part_headers = "Content-Type: text/html; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n"; PreviewText html_preview1 = new PreviewText.with_header( - new Geary.Memory.StringBuffer(HTML_BODY1_ENCODED), - new Geary.Memory.StringBuffer(html_part_headers) + new Geary.Memory.StringBuffer(html_part_headers), + new Geary.Memory.StringBuffer(HTML_BODY1_ENCODED) ); - assert(html_preview1.buffer.to_string() == HTML_BODY1_EXPECTED); + assert_string(HTML_BODY1_EXPECTED, html_preview1.buffer.to_string()); PreviewText html_preview2 = new PreviewText.with_header( - new Geary.Memory.StringBuffer(HTML_BODY2_ENCODED), - new Geary.Memory.StringBuffer(html_part_headers) + new Geary.Memory.StringBuffer(html_part_headers), + new Geary.Memory.StringBuffer(HTML_BODY2_ENCODED) ); - assert(html_preview2.buffer.to_string() == HTML_BODY2_EXPECTED); + assert_string(HTML_BODY2_EXPECTED, html_preview2.buffer.to_string()); } public static string PLAIN_BODY1_HEADERS = "Content-Type: text/plain; charset=\"us-ascii\"\r\nContent-Transfer-Encoding: 7bit\r\n"; diff --git a/test/engine/rfc822-part-test.vala b/test/engine/rfc822-part-test.vala new file mode 100644 index 00000000..513232d0 --- /dev/null +++ b/test/engine/rfc822-part-test.vala @@ -0,0 +1,71 @@ +/* + * Copyright 2018 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +class Geary.RFC822.PartTest : TestCase { + + private const string BODY = "This is an attachment.\n"; + + + public PartTest() { + base("Geary.RFC822.PartTest"); + add_test("new_from_empty_mime_part", new_from_empty_mime_part); + add_test("new_from_complete_mime_part", new_from_complete_mime_part); + } + + public void new_from_empty_mime_part() throws Error { + GMime.Part part = new_part(null, BODY.data); + part.set_header("Content-Type", ""); + + Part test = new Part(part); + + assert_null(test.content_type, "content_type"); + assert_null_string(test.content_id, "content_id"); + assert_null_string(test.content_description, "content_description"); + assert_null(test.content_disposition, "content_disposition"); + } + + public void new_from_complete_mime_part() throws Error { + const string TYPE = "text/plain"; + const string ID = "test-id"; + const string DESC = "test description"; + + GMime.Part part = new_part(TYPE, BODY.data); + part.set_content_id(ID); + part.set_content_description(DESC); + part.set_content_disposition( + new GMime.ContentDisposition.from_string("inline") + ); + + Part test = new Part(part); + + assert_string(TYPE, test.content_type.to_string()); + assert_string(ID, test.content_id); + assert_string(DESC, test.content_description); + assert_non_null(test.content_disposition, "content_disposition"); + assert_int( + Geary.Mime.DispositionType.INLINE, + test.content_disposition.disposition_type + ); + } + + private GMime.Part new_part(string? mime_type, + uint8[] body, + GMime.ContentEncoding encoding = GMime.ContentEncoding.DEFAULT) { + GMime.Part part = new GMime.Part(); + if (mime_type != null) { + part.set_content_type(new GMime.ContentType.from_string(mime_type)); + } + GMime.DataWrapper body_wrapper = new GMime.DataWrapper.with_stream( + new GMime.StreamMem.with_buffer(body), + encoding + ); + part.set_content_object(body_wrapper); + part.encode(GMime.EncodingConstraint.7BIT); + return part; + } + +} diff --git a/test/meson.build b/test/meson.build index aeeeeda7..108f048c 100644 --- a/test/meson.build +++ b/test/meson.build @@ -37,6 +37,7 @@ geary_test_engine_sources = [ 'engine/rfc822-mailbox-addresses-test.vala', 'engine/rfc822-message-test.vala', 'engine/rfc822-message-data-test.vala', + 'engine/rfc822-part-test.vala', 'engine/rfc822-utils-test.vala', 'engine/util-html-test.vala', 'engine/util-idle-manager-test.vala',