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.
".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',