Commit e29a9c80 authored by Jim Nelson's avatar Jim Nelson

Convert all MIME handling to Engine classes: Closes #6530

We've had numerous bugs due to improper MIME comparisons and dealing
with Content-Type and Content-Disposition (or their lack of presence
in a message).  Now the Engine offers MIME classes that better deal
with these issues without exporting the GMime structures, which
are not as easy to manage and don't offer some of the things that
have bitten us in the past (such as case-insensitive comparisons).
parent eed221bf
......@@ -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")]
......
......@@ -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"
......
......@@ -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
......
......@@ -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 "<img alt=\"%s\" class=\"%s\" src=\"%s\" />".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);
}
......
......@@ -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);
......
......@@ -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;
}
}
......@@ -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) {
......
......@@ -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) {
......
......@@ -1874,9 +1874,11 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
Gee.List<Geary.Attachment> list = new Gee.ArrayList<Geary.Attachment>();
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);
......
/* 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());
}
}
/* 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<string> attributes {
owned get {
return params.keys;
}
}
// See get_parameters() for why the keys but not the values are stored case-insensitive
private Gee.HashMap<string, string> params = new Gee.HashMap<string, string>(
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<string, string>? params = null) {
if (params != null && params.size > 0)
Collection.map_set_all<string, string>(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<string, string> 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);
}
}
/* 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();
}
}