diff --git a/bindings/vapi/gmime-2.6.vapi b/bindings/vapi/gmime-2.6.vapi
index 9981421e..2f03d5f8 100644
--- a/bindings/vapi/gmime-2.6.vapi
+++ b/bindings/vapi/gmime-2.6.vapi
@@ -682,13 +682,13 @@ namespace GMime {
[CCode (cname = "g_mime_object_encode")]
public virtual void encode (GMime.EncodingConstraint constraint);
[CCode (cname = "g_mime_object_get_content_disposition")]
- public unowned GMime.ContentDisposition get_content_disposition ();
+ public unowned GMime.ContentDisposition? get_content_disposition ();
[CCode (cname = "g_mime_object_get_content_disposition_parameter")]
public unowned string get_content_disposition_parameter (string attribute);
[CCode (cname = "g_mime_object_get_content_id")]
public unowned string get_content_id ();
[CCode (cname = "g_mime_object_get_content_type")]
- public unowned GMime.ContentType get_content_type ();
+ public unowned GMime.ContentType? get_content_type ();
[CCode (cname = "g_mime_object_get_content_type_parameter")]
public unowned string? get_content_type_parameter (string name);
[CCode (cname = "g_mime_object_get_disposition")]
diff --git a/bindings/vapi/gmime-2.6/gmime-2.6.metadata b/bindings/vapi/gmime-2.6/gmime-2.6.metadata
index e03b4ce6..b565b101 100644
--- a/bindings/vapi/gmime-2.6/gmime-2.6.metadata
+++ b/bindings/vapi/gmime-2.6/gmime-2.6.metadata
@@ -7,6 +7,8 @@ g_mime_header_list_get_iter.iter is_out="1"
g_mime_message_get_date.date is_out="1"
g_mime_message_get_date.tz_offset is_out="1"
g_mime_message_get_mime_part is_nullable="1"
+g_mime_object_get_content_disposition nullable="1"
+g_mime_object_get_content_type nullable="1"
g_mime_object_get_content_type_parameter nullable="1"
g_mime_object_to_string transfer_ownership="1"
g_mime_param_next name="get_next"
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 0640163c..6853191c 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -229,6 +229,13 @@ engine/memory/memory-unowned-byte-array-buffer.vala
engine/memory/memory-unowned-bytes-buffer.vala
engine/memory/memory-unowned-string-buffer.vala
+engine/mime/mime-content-disposition.vala
+engine/mime/mime-content-parameters.vala
+engine/mime/mime-content-type.vala
+engine/mime/mime-data-format.vala
+engine/mime/mime-disposition-type.vala
+engine/mime/mime-error.vala
+
engine/nonblocking/nonblocking-abstract-semaphore.vala
engine/nonblocking/nonblocking-batch.vala
engine/nonblocking/nonblocking-concurrent.vala
diff --git a/src/client/views/conversation-viewer.vala b/src/client/views/conversation-viewer.vala
index fe394ef5..5ef8d018 100644
--- a/src/client/views/conversation-viewer.vala
+++ b/src/client/views/conversation-viewer.vala
@@ -15,8 +15,16 @@ public class ConversationViewer : Gtk.Box {
| Geary.Email.Field.FLAGS
| Geary.Email.Field.PREVIEW;
- public const string INLINE_MIME_TYPES =
- "image/png image/gif image/jpeg image/pjpeg image/bmp image/x-icon image/x-xbitmap image/x-xbm";
+ private const string[] INLINE_MIME_TYPES = {
+ "image/png",
+ "image/gif",
+ "image/jpeg",
+ "image/pjpeg",
+ "image/bmp",
+ "image/x-icon",
+ "image/x-xbitmap",
+ "image/x-xbm"
+ };
private const int ATTACHMENT_PREVIEW_SIZE = 50;
private const int SELECT_CONVERSATION_TIMEOUT_MSEC = 100;
@@ -691,9 +699,26 @@ public class ConversationViewer : Gtk.Box {
}
}
- private static string? inline_image_replacer(string filename, string mimetype, Geary.Memory.Buffer buffer) {
- if (!(mimetype in INLINE_MIME_TYPES))
+ private static bool is_content_type_supported_inline(Geary.Mime.ContentType content_type) {
+ foreach (string mime_type in INLINE_MIME_TYPES) {
+ try {
+ if (content_type.is_mime_type(mime_type))
+ return true;
+ } catch (Error err) {
+ debug("Unable to compare MIME type %s: %s", mime_type, err.message);
+ }
+ }
+
+ return false;
+ }
+
+ private static string? inline_image_replacer(string filename, Geary.Mime.ContentType? content_type,
+ Geary.Mime.ContentDisposition? disposition, Geary.Memory.Buffer buffer) {
+ if (content_type == null || !is_content_type_supported_inline(content_type)) {
+ debug("Not displaying %s inline: unsupported Content-Type", content_type.to_string());
+
return null;
+ }
// Even if the image doesn't need to be rotated, there's a win here: by reducing the size
// of the image at load time, it reduces the amount of work that has to be done to insert
@@ -729,7 +754,7 @@ public class ConversationViewer : Gtk.Box {
}
return "
".printf(
- filename, DATA_IMAGE_CLASS, assemble_data_uri(mimetype, rotated_image));
+ filename, DATA_IMAGE_CLASS, assemble_data_uri(content_type.get_mime_type(), rotated_image));
}
// Called by Gdk.PixbufLoader when the image's size has been determined but not loaded yet ...
@@ -1809,12 +1834,12 @@ public class ConversationViewer : Gtk.Box {
}
private static bool should_show_attachment(Geary.Attachment attachment) {
- switch (attachment.disposition) {
- case Geary.Attachment.Disposition.ATTACHMENT:
+ switch (attachment.content_disposition.disposition_type) {
+ case Geary.Mime.DispositionType.ATTACHMENT:
return true;
- case Geary.Attachment.Disposition.INLINE:
- return !(attachment.mime_type in INLINE_MIME_TYPES);
+ case Geary.Mime.DispositionType.INLINE:
+ return !is_content_type_supported_inline(attachment.content_type);
default:
assert_not_reached();
@@ -1876,7 +1901,7 @@ public class ConversationViewer : Gtk.Box {
// Set the image preview and insert it into the container.
WebKit.DOM.HTMLImageElement img =
Util.DOM.select(attachment_table, ".preview img") as WebKit.DOM.HTMLImageElement;
- web_view.set_attachment_src(img, attachment.mime_type, attachment.file.get_path(),
+ web_view.set_attachment_src(img, attachment.content_type, attachment.file.get_path(),
ATTACHMENT_PREVIEW_SIZE);
attachment_container.append_child(attachment_table);
}
diff --git a/src/client/views/conversation-web-view.vala b/src/client/views/conversation-web-view.vala
index 824ecb7e..dc876d40 100644
--- a/src/client/views/conversation-web-view.vala
+++ b/src/client/views/conversation-web-view.vala
@@ -222,8 +222,8 @@ public class ConversationWebView : WebKit.WebView {
}
}
- public void set_attachment_src(WebKit.DOM.HTMLImageElement img, string mime_type, string filename,
- int maxwidth, int maxheight = -1) {
+ public void set_attachment_src(WebKit.DOM.HTMLImageElement img, Geary.Mime.ContentType content_type,
+ string filename, int maxwidth, int maxheight = -1) {
if( maxheight == -1 ){
maxheight = maxwidth;
}
@@ -231,9 +231,9 @@ public class ConversationWebView : WebKit.WebView {
try {
// If the file is an image, use it. Otherwise get the icon for this mime_type.
uint8[] content;
- string content_type = ContentType.from_mime_type(mime_type);
- string icon_mime_type = mime_type;
- if (mime_type.has_prefix("image/")) {
+ string gio_content_type = ContentType.from_mime_type(content_type.get_mime_type());
+ string icon_mime_type = content_type.get_mime_type();
+ if (content_type.has_media_type("image")) {
// Get a thumbnail for the image.
// TODO Generate and save the thumbnail when extracting the attachments rather than
// when showing them in the viewer.
@@ -245,7 +245,7 @@ public class ConversationWebView : WebKit.WebView {
icon_mime_type = "image/png";
} else {
// Load the icon for this mime type.
- ThemedIcon icon = ContentType.get_icon(content_type) as ThemedIcon;
+ ThemedIcon icon = ContentType.get_icon(gio_content_type) as ThemedIcon;
string icon_filename = IconFactory.instance.lookup_icon(icon.names[0], maxwidth)
.get_filename();
FileUtils.get_data(icon_filename, out content);
diff --git a/src/engine/api/geary-attachment.vala b/src/engine/api/geary-attachment.vala
index 5f083f6c..0aec64d1 100644
--- a/src/engine/api/geary-attachment.vala
+++ b/src/engine/api/geary-attachment.vala
@@ -11,42 +11,6 @@
*/
public abstract class Geary.Attachment : BaseObject {
- // NOTE: These values are persisted on disk and should not be modified unless you know what
- // you're doing.
- public enum Disposition {
- ATTACHMENT = 0,
- INLINE = 1;
-
- public static Disposition? from_string(string? str) {
- // Returns null to indicate an unknown disposition
- if (str == null) {
- return null;
- }
-
- switch (str.down()) {
- case "attachment":
- return ATTACHMENT;
-
- case "inline":
- return INLINE;
-
- default:
- return null;
- }
- }
-
- public static Disposition from_int(int i) {
- switch (i) {
- case INLINE:
- return INLINE;
-
- case ATTACHMENT:
- default:
- return ATTACHMENT;
- }
- }
- }
-
/**
* An identifier that can be used to locate the {@link Attachment} in an {@link Email}.
*
@@ -69,9 +33,9 @@ public abstract class Geary.Attachment : BaseObject {
public File file { get; private set; }
/**
- * The MIME type of the {@link Attachment}.
+ * The {@link Mime.ContentType} of the {@link Attachment}.
*/
- public string mime_type { get; private set; }
+ public Mime.ContentType content_type { get; private set; }
/**
* The file size (in bytes) if the {@link file}.
@@ -83,16 +47,16 @@ public abstract class Geary.Attachment : BaseObject {
*
* See [[https://tools.ietf.org/html/rfc2183]]
*/
- public Disposition disposition { get; private set; }
+ public Mime.ContentDisposition content_disposition { get; private set; }
- protected Attachment(string id, File file, bool has_supplied_filename, string mime_type, int64 filesize,
- Disposition disposition) {
+ protected Attachment(string id, File file, bool has_supplied_filename, Mime.ContentType content_type,
+ int64 filesize, Mime.ContentDisposition content_disposition) {
this.id = id;
this.file = file;
this.has_supplied_filename = has_supplied_filename;
- this.mime_type = mime_type;
+ this.content_type = content_type;
this.filesize = filesize;
- this.disposition = disposition;
+ this.content_disposition = content_disposition;
}
}
diff --git a/src/engine/imap-db/imap-db-attachment.vala b/src/engine/imap-db/imap-db-attachment.vala
index 2bf7b8d2..6c8dda70 100644
--- a/src/engine/imap-db/imap-db-attachment.vala
+++ b/src/engine/imap-db/imap-db-attachment.vala
@@ -9,10 +9,10 @@ private class Geary.ImapDB.Attachment : Geary.Attachment {
private const string ATTACHMENTS_DIR = "attachments";
- protected Attachment(File data_dir, string? filename, string mime_type, int64 filesize,
- int64 message_id, int64 attachment_id, Geary.Attachment.Disposition disposition) {
+ protected Attachment(File data_dir, string? filename, Mime.ContentType content_type, int64 filesize,
+ int64 message_id, int64 attachment_id, Mime.ContentDisposition content_disposition) {
base (generate_id(attachment_id),generate_file(data_dir, message_id, attachment_id, filename),
- !String.is_empty(filename), mime_type, filesize, disposition);
+ !String.is_empty(filename), content_type, filesize, content_disposition);
}
private static string generate_id(int64 attachment_id) {
diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala
index 7af66ae0..16bc3918 100644
--- a/src/engine/imap-db/imap-db-database.vala
+++ b/src/engine/imap-db/imap-db-database.vala
@@ -256,9 +256,9 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
try {
Geary.RFC822.Message message = new Geary.RFC822.Message.from_parts(
new RFC822.Header(header), new RFC822.Text(body));
- Geary.Attachment.Disposition? target_disposition = null;
+ Mime.DispositionType target_disposition = Mime.DispositionType.UNSPECIFIED;
if (message.get_sub_messages().is_empty)
- target_disposition = Geary.Attachment.Disposition.INLINE;
+ target_disposition = Mime.DispositionType.INLINE;
Geary.ImapDB.Folder.do_save_attachments_db(cx, id,
message.get_attachments(target_disposition), this, null);
} catch (Error e) {
diff --git a/src/engine/imap-db/imap-db-folder.vala b/src/engine/imap-db/imap-db-folder.vala
index e12f8c86..d9b6bc86 100644
--- a/src/engine/imap-db/imap-db-folder.vala
+++ b/src/engine/imap-db/imap-db-folder.vala
@@ -1874,9 +1874,11 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
Gee.List list = new Gee.ArrayList();
do {
+ Mime.ContentDisposition disposition = new Mime.ContentDisposition.simple(
+ Mime.DispositionType.from_int(results.int_at(4)));
list.add(new ImapDB.Attachment(cx.database.db_file.get_parent(), results.string_at(1),
- results.nonnull_string_at(2), results.int64_at(3), message_id, results.rowid_at(0),
- Geary.Attachment.Disposition.from_int(results.int_at(4))));
+ Mime.ContentType.deserialize(results.nonnull_string_at(2)), results.int64_at(3),
+ message_id, results.rowid_at(0), disposition));
} while (results.next(cancellable));
return list;
@@ -1907,6 +1909,14 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
attachment_data.write_to_stream(stream); // data is null if it's 0 bytes
uint filesize = byte_array.len;
+ // convert into DispositionType enum, which is stored as int
+ // (legacy code stored UNSPECIFIED as NULL, which is zero, which is ATTACHMENT, so preserve
+ // this behavior)
+ Mime.DispositionType disposition_type = Mime.DispositionType.deserialize(disposition,
+ null);
+ if (disposition_type == Mime.DispositionType.UNSPECIFIED)
+ disposition_type = Mime.DispositionType.ATTACHMENT;
+
// Insert it into the database.
Db.Statement stmt = cx.prepare("""
INSERT INTO MessageAttachmentTable (message_id, filename, mime_type, filesize, disposition)
@@ -1916,7 +1926,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
stmt.bind_string(1, filename);
stmt.bind_string(2, mime_type);
stmt.bind_uint(3, filesize);
- stmt.bind_int(4, Geary.Attachment.Disposition.from_string(disposition));
+ stmt.bind_int(4, disposition_type);
int64 attachment_id = stmt.exec_insert(cancellable);
diff --git a/src/engine/mime/mime-content-disposition.vala b/src/engine/mime/mime-content-disposition.vala
new file mode 100644
index 00000000..edcee532
--- /dev/null
+++ b/src/engine/mime/mime-content-disposition.vala
@@ -0,0 +1,108 @@
+/* Copyright 2013 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * A representation of the RFC 2183 Content-Disposition field.
+ *
+ * See [[https://tools.ietf.org/html/rfc2183]]
+ */
+
+public class Geary.Mime.ContentDisposition : Geary.BaseObject {
+ /**
+ * Filename parameter name.
+ *
+ * See [[https://tools.ietf.org/html/rfc2183#section-2.3]]
+ */
+ public const string FILENAME = "filename";
+
+ /**
+ * Creation-Date parameter name.
+ *
+ * See [[https://tools.ietf.org/html/rfc2183#section-2.4]]
+ */
+ public const string CREATION_DATE = "creation-date";
+
+ /**
+ * Modification-Date parameter name.
+ *
+ * See [[https://tools.ietf.org/html/rfc2183#section-2.5]]
+ */
+ public const string MODIFICATION_DATE = "modification-date";
+
+ /**
+ * Read-Date parameter name.
+ *
+ * See [[https://tools.ietf.org/html/rfc2183#section-2.6]]
+ */
+ public const string READ_DATE = "read-date";
+
+ /**
+ * Size parameter name.
+ *
+ * See [[https://tools.ietf.org/html/rfc2183#section-2.7]]
+ */
+ public const string SIZE = "size";
+
+ /**
+ * The {@link DispositionType}, which is {@link DispositionType.NONE} if not specified.
+ */
+ public DispositionType disposition_type { get; private set; }
+
+ /**
+ * True if the original DispositionType was unknown.
+ */
+ public bool is_unknown_disposition_type { get; private set; }
+
+ /**
+ * The original disposition type string.
+ */
+ public string? original_disposition_type_string { get; private set; }
+
+ /**
+ * Various parameters associated with the content's disposition.
+ *
+ * This is never null. Rather, an empty ContentParameters is held if the Content-Type has
+ * no parameters.
+ *
+ * @see FILENAME
+ * @see CREATION_DATE
+ * @see MODIFICATION_DATE
+ * @see READ_DATE
+ * @see SIZE
+ */
+ public ContentParameters params { get; private set; }
+
+ /**
+ * Create a Content-Disposition representation
+ */
+ public ContentDisposition(string? disposition, ContentParameters? params) {
+ bool is_unknown;
+ disposition_type = DispositionType.deserialize(disposition, out is_unknown);
+ is_unknown_disposition_type = is_unknown;
+ original_disposition_type_string = disposition;
+ this.params = params ?? new ContentParameters();
+ }
+
+ /**
+ * Create a simplified Content-Disposition representation.
+ */
+ public ContentDisposition.simple(DispositionType disposition_type) {
+ this.disposition_type = disposition_type;
+ is_unknown_disposition_type = false;
+ original_disposition_type_string = null;
+ this.params = new ContentParameters();
+ }
+
+ internal ContentDisposition.from_gmime(GMime.ContentDisposition content_disposition) {
+ bool is_unknown;
+ disposition_type = DispositionType.deserialize(content_disposition.get_disposition(),
+ out is_unknown);
+ is_unknown_disposition_type = is_unknown;
+ original_disposition_type_string = content_disposition.get_disposition();
+ params = new ContentParameters.from_gmime(content_disposition.get_params());
+ }
+}
+
diff --git a/src/engine/mime/mime-content-parameters.vala b/src/engine/mime/mime-content-parameters.vala
new file mode 100644
index 00000000..21460767
--- /dev/null
+++ b/src/engine/mime/mime-content-parameters.vala
@@ -0,0 +1,114 @@
+/* Copyright 2013 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * Content parameters (for {@link ContentType} and {@link ContentDisposition}).
+ */
+
+public class Geary.Mime.ContentParameters : BaseObject {
+ public int size {
+ get {
+ return params.size;
+ }
+ }
+
+ public Gee.Collection attributes {
+ owned get {
+ return params.keys;
+ }
+ }
+
+ // See get_parameters() for why the keys but not the values are stored case-insensitive
+ private Gee.HashMap params = new Gee.HashMap(
+ String.stri_hash, String.stri_equal);
+
+ /**
+ * Create a mapping of content parameters.
+ *
+ * A Gee.Map may be supplied to initialize the parameter attributes (names) and values.
+ *
+ * Note that params may be any kind of Map, but they will be stored internally in a Map that
+ * uses case-insensitive keys. See {@link get_parameters} for more details.
+ */
+ public ContentParameters(Gee.Map? params = null) {
+ if (params != null && params.size > 0)
+ Collection.map_set_all(this.params, params);
+ }
+
+ internal ContentParameters.from_gmime(GMime.Param? gmime_param) {
+ while (gmime_param != null) {
+ set_parameter(gmime_param.get_name(), gmime_param.get_value());
+ gmime_param = gmime_param.get_next();
+ }
+ }
+
+ /**
+ * A read-only mapping of parameter attributes (names) and values.
+ *
+ * Note that names are stored as case-insensitive tokens. The MIME specification does allow
+ * for some parameter values to be case-sensitive and so they are stored as such. It is up
+ * to the caller to use the right comparison method.
+ *
+ * @see is_parameter_ci
+ * @see is_parameter_cs
+ */
+ public Gee.Map get_parameters() {
+ return params.read_only_view;
+ }
+
+ /**
+ * Returns the parameter value for the attribute name.
+ *
+ * Returns null if not present.
+ */
+ public string? get_value(string attribute) {
+ return params.get(attribute);
+ }
+
+ /**
+ * Returns true if the attribute has the supplied value (case-insensitive comparison).
+ *
+ * @see has_value_ci
+ */
+ public bool has_value_ci(string attribute, string value) {
+ string? stored = params.get(attribute);
+
+ return (stored != null) ? String.stri_equal(stored, value) : false;
+ }
+
+ /**
+ * Returns true if the attribute has the supplied value (case-sensitive comparison).
+ *
+ * @see has_value_cs
+ */
+ public bool has_value_cs(string attribute, string value) {
+ string? stored = params.get(attribute);
+
+ return (stored != null) ? (stored == value) : false;
+ }
+
+ /**
+ * Add or replace the parameter.
+ *
+ * Returns true if the parameter was added, false, otherwise.
+ */
+ public bool set_parameter(string attribute, string value) {
+ bool added = !params.has_key(attribute);
+ params.set(attribute, value);
+
+ return added;
+ }
+
+ /**
+ * Removes the parameter.
+ *
+ * Returns true if the parameter was present.
+ */
+ public bool remove_parameter(string attribute) {
+ return params.unset(attribute);
+ }
+}
+
diff --git a/src/engine/mime/mime-content-type.vala b/src/engine/mime/mime-content-type.vala
new file mode 100644
index 00000000..c2d6da81
--- /dev/null
+++ b/src/engine/mime/mime-content-type.vala
@@ -0,0 +1,199 @@
+/* Copyright 2013 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * A representation of an RFC 2045 MIME Content-Type field.
+ *
+ * See [[https://tools.ietf.org/html/rfc2045#section-5]]
+ */
+
+public class Geary.Mime.ContentType : Geary.BaseObject {
+ /*
+ * MIME wildcard for comparing {@link media_type} and {@link media_subtype}.
+ *
+ * @see is_type
+ */
+ public const string WILDCARD = "*";
+
+ /**
+ * The type (discrete or concrete) portion of the Content-Type field.
+ *
+ * It's highly recommended the caller use the various ''has'' and ''is'' methods when performing
+ * comparisons rather than direct string operations.
+ *
+ * media_type may be {@link WILDCARD}, in which case it matches with any other media_type.
+ *
+ * @see has_media_type
+ */
+ public string media_type { get; private set; }
+
+ /**
+ * The subtype (extension-token or iana-token) portion of the Content-Type field.
+ *
+ * It's highly recommended the caller use the various ''has'' and ''is'' methods when performing
+ * comparisons rather than direct string operations.
+ *
+ * media_subtype may be {@link WILDCARD}, in which case it matches with any other media_subtype.
+ *
+ * @see has_media_subtype
+ */
+ public string media_subtype { get; private set; }
+
+ /**
+ * Content parameters, if any, in the Content-Type field.
+ *
+ * This is never null. Rather, an empty ContentParameters is held if the Content-Type has
+ * no parameters.
+ */
+ public ContentParameters params { get; private set; }
+
+ /**
+ * Create a MIME Content-Type representative object.
+ */
+ public ContentType(string media_type, string media_subtype, ContentParameters? params) {
+ this.media_type = media_type.strip();
+ this.media_subtype = media_subtype.strip();
+ this.params = params ?? new ContentParameters();
+ }
+
+ internal ContentType.from_gmime(GMime.ContentType content_type) {
+ media_type = content_type.get_media_type().strip();
+ media_subtype = content_type.get_media_subtype().strip();
+ params = new ContentParameters.from_gmime(content_type.get_params());
+ }
+
+ public static ContentType deserialize(string str) throws MimeError {
+ // perform a little sanity checking here, as it doesn't appear the GMime constructor has
+ // any error-reporting at all
+ if (String.is_empty(str))
+ throw new MimeError.PARSE("Empty MIME Content-Type");
+
+ if (!str.contains("/"))
+ throw new MimeError.PARSE("Invalid MIME Content-Type: %s", str);
+
+ return new ContentType.from_gmime(new GMime.ContentType.from_string(str));
+ }
+
+ /**
+ * Compares the {@link media_type} with the supplied type.
+ *
+ * An asterisk ("*") or {@link WILDCARD) are accepted, which will always return true.
+ *
+ * @see is_type
+ */
+ public bool has_media_type(string media_type) {
+ return (media_type != WILDCARD) ? String.stri_equal(this.media_type, media_type) : true;
+ }
+
+ /**
+ * Compares the {@link media_subtype} with the supplied subtype.
+ *
+ * An asterisk ("*") or {@link WILDCARD) are accepted, which will always return true.
+ *
+ * @see is_type
+ */
+ public bool has_media_subtype(string media_subtype) {
+ return (media_subtype != WILDCARD) ? String.stri_equal(this.media_subtype, media_subtype) : true;
+ }
+
+ /**
+ * Returns the {@link ContentType}'s media content type (its "MIME type").
+ *
+ * This returns the bare MIME content type description lacking all parameters. For example,
+ * "image/jpeg; name='photo.JPG'" will be returned as "image/jpeg".
+ *
+ * @see serialize
+ */
+ public string get_mime_type() {
+ return "%s/%s".printf(media_type, media_subtype);
+ }
+
+ /**
+ * Compares the supplied type and subtype with this instance's.
+ *
+ * Asterisks (or {@link WILDCARD}) may be supplied for either field.
+ *
+ * @see is_same
+ */
+ public bool is_type(string media_type, string media_subtype) {
+ return has_media_type(media_type) && has_media_subtype(media_subtype);
+ }
+
+ /**
+ * Compares this {@link ContentType} with another instance.
+ *
+ * This is slightly different than the notion of "equal to", as it's possible for
+ * {@link ContentType} to hold {@link WILDCARD}s, which don't imply equality.
+ *
+ * @see is_type
+ */
+ public bool is_same(ContentType other) {
+ return is_type(other.media_type, other.media_subtype);
+ }
+
+ /**
+ * Compares the supplied MIME type (i.e. "image/jpeg") with this instance.
+ *
+ * As in {@link get_mime_type}, this method is only worried about the media type and subtype
+ * in the supplied string. Parameters are ignored.
+ *
+ * Throws {@link MimeError} if the supplied string doesn't look like a MIME type.
+ */
+ public bool is_mime_type(string mime_type) throws MimeError {
+ int index = mime_type.index_of_char('/');
+ if (index < 0)
+ throw new MimeError.PARSE("Invalid MIME type: %s", mime_type);
+
+ string mime_media_type = mime_type.substring(0, index).strip();
+
+ string mime_media_subtype = mime_type.substring(index + 1);
+ index = mime_media_subtype.index_of_char(';');
+ if (index >= 0)
+ mime_media_subtype = mime_media_subtype.substring(0, index);
+ mime_media_subtype = mime_media_subtype.strip();
+
+ if (String.is_empty(mime_media_type) || String.is_empty(mime_media_subtype))
+ throw new MimeError.PARSE("Invalid MIME type: %s", mime_type);
+
+ return is_type(mime_media_type, mime_media_subtype);
+ }
+
+ public string serialize() {
+ StringBuilder builder = new StringBuilder();
+ builder.append_printf("%s/%s", media_type, media_subtype);
+
+ if (params != null && params.size > 0) {
+ foreach (string attribute in params.attributes) {
+ string value = params.get_value(attribute);
+
+ switch (DataFormat.get_encoding_requirement(value)) {
+ case DataFormat.Encoding.QUOTING_OPTIONAL:
+ builder.append_printf("; %s=%s", attribute, value);
+ break;
+
+ case DataFormat.Encoding.QUOTING_REQUIRED:
+ builder.append_printf("; %s=\"%s\"", attribute, value);
+ break;
+
+ case DataFormat.Encoding.UNALLOWED:
+ message("Cannot encode ContentType param value %s=\"%s\": unallowed",
+ attribute, value);
+ break;
+
+ default:
+ assert_not_reached();
+ }
+ }
+ }
+
+ return builder.str;
+ }
+
+ public string to_string() {
+ return serialize();
+ }
+}
+
diff --git a/src/engine/mime/mime-data-format.vala b/src/engine/mime/mime-data-format.vala
new file mode 100644
index 00000000..88ac8553
--- /dev/null
+++ b/src/engine/mime/mime-data-format.vala
@@ -0,0 +1,45 @@
+/* Copyright 2013 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * Utility methods for manipulating and examining data particular to MIME.
+ */
+
+namespace Geary.Mime.DataFormat {
+
+private const char[] CONTENT_TYPE_TOKEN_SPECIALS = {
+ '(', ')', '<', '>', '@', ',', ';', ':', '\\', '"', '/', '[', ']', '?', '='
+};
+
+public enum Encoding {
+ QUOTING_REQUIRED,
+ QUOTING_OPTIONAL,
+ UNALLOWED
+}
+
+public Encoding get_encoding_requirement(string str) {
+ if (String.is_empty(str))
+ return Encoding.QUOTING_REQUIRED;
+
+ Encoding encoding = Encoding.QUOTING_OPTIONAL;
+ int index = 0;
+ for (;;) {
+ char ch = str[index++];
+ if (ch == String.EOS)
+ break;
+
+ if (ch.iscntrl())
+ return Encoding.UNALLOWED;
+
+ // don't return immediately, it's possible unallowed characters may still be ahead
+ if (ch.isspace() || ch in CONTENT_TYPE_TOKEN_SPECIALS)
+ encoding = Encoding.QUOTING_REQUIRED;
+ }
+
+ return encoding;
+}
+
+}
diff --git a/src/engine/mime/mime-disposition-type.vala b/src/engine/mime/mime-disposition-type.vala
new file mode 100644
index 00000000..da71c7ab
--- /dev/null
+++ b/src/engine/mime/mime-disposition-type.vala
@@ -0,0 +1,87 @@
+/* Copyright 2013 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * A representation of a MIME Content-Disposition type field.
+ *
+ * Note that NONE only indicates that the Content-Disposition type field was not present.
+ * RFC 2183 Section 2.8 specifies that unknown type fields should be treated as attachments,
+ * which is true in this code as well.
+ *
+ * These values may be persisted on disk and should not be modified unless you know what
+ * you're doing. (Legacy code requires that NONE be -1.)
+ *
+ * See [[https://tools.ietf.org/html/rfc2183#section-2]]
+ */
+
+public enum Geary.Mime.DispositionType {
+ UNSPECIFIED = -1,
+ ATTACHMENT = 0,
+ INLINE = 1;
+
+ /**
+ * Convert the disposition-type field into an internal representation.
+ *
+ * Empty or blank fields result in {@link UNSPECIFIED}. Unknown fields are converted to
+ * {@link ATTACHMENT} as per RFC 2183 Section 2.8. However, since the caller may want to
+ * make a decision about unknown vs. unspecified type fields, is_unknown is returned as well.
+ */
+ public static DispositionType deserialize(string? str, out bool is_unknown) {
+ is_unknown = false;
+
+ if (String.is_empty_or_whitespace(str))
+ return UNSPECIFIED;
+
+ switch (str.down()) {
+ case "inline":
+ return INLINE;
+
+ case "attachment":
+ return ATTACHMENT;
+
+ default:
+ is_unknown = true;
+
+ return ATTACHMENT;
+ }
+ }
+
+ /**
+ * Returns null if value is {@link UNSPECIFIED}
+ */
+ public string? serialize() {
+ switch (this) {
+ case UNSPECIFIED:
+ return null;
+
+ case ATTACHMENT:
+ return "attachment";
+
+ case INLINE:
+ return "inline";
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ internal static DispositionType from_int(int i) {
+ switch (i) {
+ case INLINE:
+ return INLINE;
+
+ case UNSPECIFIED:
+ return UNSPECIFIED;
+
+ // see note in class description for why unknown content-dispositions are treated as
+ // attachments
+ case ATTACHMENT:
+ default:
+ return ATTACHMENT;
+ }
+ }
+}
+
diff --git a/src/engine/mime/mime-error.vala b/src/engine/mime/mime-error.vala
new file mode 100644
index 00000000..8926e438
--- /dev/null
+++ b/src/engine/mime/mime-error.vala
@@ -0,0 +1,13 @@
+/* Copyright 2013 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * Errors related to {@link Geary.Mime}.
+ */
+
+public errordomain MimeError {
+ PARSE
+}
diff --git a/src/engine/rfc822/rfc822-message-data.vala b/src/engine/rfc822/rfc822-message-data.vala
index b082330a..b8a803f0 100644
--- a/src/engine/rfc822/rfc822-message-data.vala
+++ b/src/engine/rfc822/rfc822-message-data.vala
@@ -330,9 +330,13 @@ public class Geary.RFC822.PreviewText : Geary.RFC822.Text {
GMime.Parser parser = new GMime.Parser.with_stream(header_stream);
GMime.Part? part = parser.construct_part() as GMime.Part;
if (part != null) {
- is_html = (part.get_content_type().to_string() == "text/html");
+ Mime.ContentType? content_type = null;
+ if (part.get_content_type() != null) {
+ content_type = new Mime.ContentType.from_gmime(part.get_content_type());
+ is_html = content_type.is_type("text", "html");
+ charset = content_type.params.get_value("charset");
+ }
- charset = part.get_content_type_parameter("charset");
encoding = part.get_header("Content-Transfer-Encoding");
}
diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala
index 64c6d3b5..7063d67e 100644
--- a/src/engine/rfc822/rfc822-message.vala
+++ b/src/engine/rfc822/rfc822-message.vala
@@ -9,8 +9,8 @@ public class Geary.RFC822.Message : BaseObject {
* This delegate is an optional parameter to the body constructers that allows callers
* to process arbitrary non-text, inline MIME parts.
*/
- public delegate string? InlinePartReplacer(string filename, string mimetype,
- Geary.Memory.Buffer buffer);
+ public delegate string? InlinePartReplacer(string filename, Mime.ContentType? content_type,
+ Mime.ContentDisposition? disposition, Geary.Memory.Buffer buffer);
private const string DEFAULT_ENCODING = "UTF8";
@@ -470,35 +470,41 @@ public class Geary.RFC822.Message : BaseObject {
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
- string? disposition = part.get_disposition();
- if (disposition != null && disposition.down() == "attachment")
+ if (disposition != null && disposition.disposition_type == Mime.DispositionType.ATTACHMENT)
return false;
/* Handle text parts that are not attachments
* They may have inline disposition, or they may have no disposition specified
*/
- GMime.ContentType content_type = part.get_content_type();
- if (String.stri_equal(content_type.get_media_type(), "text")) {
- if (String.stri_equal(content_type.get_media_subtype(), text_subtype)) {
- body = mime_part_to_memory_buffer(part, true, to_html).to_string();
- return true;
+ Mime.ContentType? content_type = null;
+ if (part.get_content_type() != null) {
+ content_type = new Mime.ContentType.from_gmime(part.get_content_type());
+ if (content_type.has_media_type("text")) {
+ if (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;
}
-
- // We were the wrong kind of text part
- return false;
}
// If images have no disposition, they are handled elsewhere; see #7299
- if (disposition == null)
+ if (disposition == null || disposition.disposition_type == Mime.DispositionType.UNSPECIFIED)
return false;
- // Hand off to the replacer for processing
if (replacer == null)
return false;
- string? replaced_part = replacer(RFC822.Utils.get_attachment_filename(part), content_type.to_string(),
- mime_part_to_memory_buffer(part));
+ // Hand off to the replacer for processing
+ string? replaced_part = replacer(RFC822.Utils.get_attachment_filename(part), content_type,
+ disposition, mime_part_to_memory_buffer(part));
if (replaced_part != null)
body = replaced_part;
@@ -642,16 +648,16 @@ public class Geary.RFC822.Message : BaseObject {
return null;
}
- internal Gee.List get_attachments(Geary.Attachment.Disposition? disposition = null)
- throws RFC822Error {
- // A null disposition means "return all Mime parts recognized by Geary.Attachment.Disposition"
+ // UNSPECIFIED disposition means "return all Mime parts"
+ internal Gee.List get_attachments(
+ Mime.DispositionType disposition = Mime.DispositionType.UNSPECIFIED) throws RFC822Error {
Gee.List attachments = new Gee.ArrayList();
get_attachments_recursively(attachments, message.get_mime_part(), disposition);
return attachments;
}
private void get_attachments_recursively(Gee.List attachments, GMime.Object root,
- Geary.Attachment.Disposition? requested_disposition) throws RFC822Error {
+ 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) {
@@ -666,14 +672,15 @@ public class Geary.RFC822.Message : BaseObject {
GMime.MessagePart? messagepart = root as GMime.MessagePart;
if (messagepart != null) {
GMime.Message message = messagepart.get_message();
- Geary.Attachment.Disposition? disposition = Geary.Attachment.Disposition.from_string(
- root.get_disposition());
- if (disposition == null) {
+ bool is_unknown;
+ Mime.DispositionType disposition = Mime.DispositionType.deserialize(root.get_disposition(),
+ out is_unknown);
+ if (disposition == Mime.DispositionType.UNSPECIFIED || is_unknown) {
// This is often the case, and we'll treat these as attached
- disposition = Geary.Attachment.Disposition.ATTACHMENT;
+ disposition = Mime.DispositionType.ATTACHMENT;
}
- if (requested_disposition == null || disposition == requested_disposition) {
+ if (requested_disposition == Mime.DispositionType.UNSPECIFIED || disposition == requested_disposition) {
GMime.Stream stream = new GMime.StreamMem();
message.write_to_stream(stream);
GMime.DataWrapper data = new GMime.DataWrapper.with_stream(stream,
@@ -695,32 +702,24 @@ public class Geary.RFC822.Message : BaseObject {
return;
}
- Geary.Attachment.Disposition? part_disposition = Geary.Attachment.Disposition.from_string(
- part.get_disposition());
- if (part_disposition == null) {
- // The part disposition was unknown to Geary.Attachment.Disposition
+ Mime.DispositionType part_disposition = Mime.DispositionType.deserialize(part.get_disposition(),
+ null);
+ if (part_disposition == Mime.DispositionType.UNSPECIFIED)
return;
- }
- GMime.ContentType content_type = part.get_content_type();
- if (part_disposition == Geary.Attachment.Disposition.INLINE &&
- content_type.get_media_type().down() == "text") {
- string subtype = content_type.get_media_subtype().down();
- if (subtype == "html" || subtype == "plain") {
- // These are 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
+ && content_type.has_media_type("text")
+ && (content_type.has_media_subtype("html") || content_type.has_media_subtype("plain"))) {
+ // these are part of the body
return;
}
}
- if (requested_disposition == null) {
- // Return any attachment whose disposition is recognized by Geary.Attachment.Disposition
+ // Catch remaining disposition-type matches
+ if (requested_disposition == Mime.DispositionType.UNSPECIFIED || part_disposition == requested_disposition)
attachments.add(part);
- return;
- }
-
- if (part_disposition == requested_disposition) {
- attachments.add(part);
- }
}
public Gee.List get_sub_messages() {
@@ -766,11 +765,14 @@ public class Geary.RFC822.Message : BaseObject {
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",
- part.get_content_type().to_string());
+ content_type.to_string());
}
ByteArray byte_array = new ByteArray();
@@ -780,17 +782,17 @@ public class Geary.RFC822.Message : BaseObject {
// Convert encoding to UTF-8.
GMime.StreamFilter stream_filter = new GMime.StreamFilter(stream);
if (to_utf8) {
- string? charset = part.get_content_type_parameter("charset");
+ string? charset = (content_type != null) ? content_type.params.get_value("charset") : null;
if (String.is_empty(charset))
charset = DEFAULT_ENCODING;
stream_filter.add(Geary.RFC822.Utils.create_utf8_filter_charset(charset));
}
- string format = part.get_content_type_parameter("format") ?? "";
- bool flowed = (format.down() == "flowed");
- string delsp_par = part.get_content_type_parameter("DelSp") ?? "no";
- bool delsp = (delsp_par.down() == "yes");
+
+ 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;
if (flowed)
stream_filter.add(new Geary.RFC822.FilterFlowed(to_html, delsp));
+
if (to_html) {
if (!flowed)
stream_filter.add(new Geary.RFC822.FilterPlain());