[geary/wip/726281-text-attachment-crlf: 10/13] Ensure we always use the one, same codepath when decoding text content.



commit f1c797650fb9b112fd11be6b8a7b2be61765112e
Author: Michael James Gratton <mike vee net>
Date:   Thu May 10 13:47:47 2018 +1000

    Ensure we always use the one, same codepath when decoding text content.
    
    This introduces the Geary.RFC822.Part class, which provides a place to
    MIME entity body decoding code so it can be reused when needed. It also
    provides a place to put common GMime to Geary object conversion, and
    apply some common policy decisions, such as what is the default content
    type if none is specified.
    
    * src/engine/rfc822/rfc822-part.vala: New Part class that represents a
      MIME entity. Move code for both decoding entity body from
      RFC822.Message and code for cleaning content filename from RFC822.Util
      to here. Convert GMime entity header objects into their Geary
      equivalents and make available as properties. Provide a common means of
      determining the content type of the part if not explicitly set.
    
    * src/engine/rfc822/rfc822-message-data.vala (PreviewText.with_header):
      Construct a RFC822.Part and use that for decoding preview text. Swap
      args to make some more sense and update call sites.
    
    * src/engine/rfc822/rfc822-message.vala (InlinePartReplacer): Simply pass
      through an instance of a RFC822.Part rather than the multi-arg list,
      since that has all the data needed by replacers.
    
    * src/engine/imap-db/imap-db-attachment.vala (Attachment): Require and
      use RFC822.Part instances for obtaining attachment bodies rather than
      GMime.Part instances. Update call sites.

 po/POTFILES.in                                     |    1 +
 src/CMakeLists.txt                                 |    1 +
 .../conversation-viewer/conversation-message.vala  |   35 ++-
 src/engine/imap-db/imap-db-attachment.vala         |   76 ++---
 src/engine/imap-db/imap-db-database.vala           |    2 +-
 src/engine/imap/api/imap-folder-session.vala       |    4 +-
 src/engine/meson.build                             |    1 +
 src/engine/rfc822/rfc822-message-data.vala         |   65 ++--
 src/engine/rfc822/rfc822-message.vala              |  319 ++++++-------------
 src/engine/rfc822/rfc822-part.vala                 |  195 ++++++++++++
 src/engine/rfc822/rfc822-utils.vala                |   14 -
 test/CMakeLists.txt                                |    1 +
 test/engine/imap-db/imap-db-attachment-test.vala   |   24 ++-
 test/engine/rfc822-message-data-test.vala          |   24 +-
 test/engine/rfc822-part-test.vala                  |   71 +++++
 test/meson.build                                   |    1 +
 16 files changed, 487 insertions(+), 347 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 02f5e59..d795f84 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 4404bde..8d0174a 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 75a28ad..877155c 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. <IMG ALT="Image" ...
         string UNKNOWN_FILENAME_ALT_TEXT = _("Image");
         string clean_filename = Geary.HTML.escape_markup(
-            filename ?? UNKNOWN_FILENAME_ALT_TEXT
+            part.get_clean_filename() ?? UNKNOWN_FILENAME_ALT_TEXT
         );
 
         return "<img alt=\"%s\" class=\"%s\" src=\"%s%s\" />".printf(
diff --git a/src/engine/imap-db/imap-db-attachment.vala b/src/engine/imap-db/imap-db-attachment.vala
index eecfd9d..f840aeb 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<Attachment> save_attachments(Db.Connection cx,
                                                           GLib.File attachments_path,
                                                           int64 message_id,
-                                                          Gee.List<GMime.Part> attachments,
+                                                          Gee.List<RFC822.Part> attachments,
                                                           Cancellable? cancellable)
         throws Error {
         Gee.List<Attachment> list = new Gee.LinkedList<Attachment>();
-        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 1fd86cf..807b6c0 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<GMime.Part> msg_attachments =
+                    Gee.List<RFC822.Part> 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 926edd1..aee6317 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 717ea20..f759ec8 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 adb9da9..a643e0f 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");
-            }
-
-            encoding = part.get_header("Content-Transfer-Encoding");
-        }
+        GMime.Part? gpart = parser.construct_part() as GMime.Part;
+        if (gpart != null) {
+            Part part = new Part(gpart);
 
-        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);
+            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");
 
-            // 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));
+            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);
 
-            filter.add(Geary.RFC822.Utils.create_utf8_filter_charset(charset));
-            filter.add(new GMime.FilterCRLF(false, false));
+                ByteArray output = new ByteArray();
+                GMime.StreamMem output_stream =
+                    new GMime.StreamMem.with_byte_array(output);
+                output_stream.set_owner(false);
 
-            input_stream.write_to_stream(filter);
-            uint8[] data = output.data;
-            data += (uint8) '\0';
+                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);
+                    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);
+                }
+            }
         }
 
         base(new Geary.Memory.StringBuffer(preview_text));
diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala
index a0e0709..15c9027 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;
-
-        // 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()
-            );
-        }
+        Part part = new Part(node);
+        bool is_matching_part = false;
 
-        GMime.Multipart? multipart = node as GMime.Multipart;
-        if (multipart != null) {
+        if (node is GMime.Multipart) {
+            GMime.Multipart multipart = (GMime.Multipart) node;
             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);
+            for (int i = 0; i < count && !is_matching_part; i++) {
+                is_matching_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;
-                    }
-                }
+        } 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)
+            );
         }
-        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<GMime.Part> get_attachments(
+    internal Gee.List<Part> get_attachments(
         Mime.DispositionType disposition = Mime.DispositionType.UNSPECIFIED) throws RFC822Error {
-        Gee.List<GMime.Part> attachments = new Gee.ArrayList<GMime.Part>();
+        Gee.List<Part> attachments = new Gee.LinkedList<Part>();
         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<GMime.Part> 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<Part> 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<Geary.RFC822.Message> get_sub_messages() {
         Gee.List<Geary.RFC822.Message> messages = new Gee.ArrayList<Geary.RFC822.Message>();
         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 0000000..53bf846
--- /dev/null
+++ b/src/engine/rfc822/rfc822-part.vala
@@ -0,0 +1,195 @@
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2018 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.
+ */
+
+/**
+ * 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 8611e64..44acbc4 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 2841073..fc276ce 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 ae11db8..0ecfa27 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<GMime.Part>.wrap({ part }),
+            new Gee.ArrayList<Geary.RFC822.Part>.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<GMime.Part>.wrap({ part }),
+            new Gee.ArrayList<Geary.RFC822.Part>.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<GMime.Part>.wrap({ part }),
+            new Gee.ArrayList<Geary.RFC822.Part>.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 156fe48..5251fd9 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 0000000..513232d
--- /dev/null
+++ b/test/engine/rfc822-part-test.vala
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2018 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.
+ */
+
+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 aeeeeda..108f048 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',


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]