TNEF (winmail.dat) parsing support via libytnef

This commit is contained in:
Oliver Giles 2018-06-15 11:38:10 +03:00
parent ef8f97628e
commit 18fcf0e18f
11 changed files with 1870 additions and 6 deletions

View file

@ -27,7 +27,7 @@ variables:
gtk3-devel iso-codes-devel json-glib-devel itstool gtk3-devel iso-codes-devel json-glib-devel itstool
libcanberra-devel libgee-devel libhandy-devel libcanberra-devel libgee-devel libhandy-devel
libnotify-devel libsecret-devel libunwind-devel libnotify-devel libsecret-devel libunwind-devel
libxml2-devel sqlite-devel webkitgtk4-devel libxml2-devel libytnef-devel sqlite-devel webkitgtk4-devel
FEDORA_TEST_DEPS: Xvfb tar xz FEDORA_TEST_DEPS: Xvfb tar xz
# Ubuntu packages # Ubuntu packages
@ -39,7 +39,7 @@ variables:
libhandy-0.0-dev libjson-glib-dev libmessaging-menu-dev libhandy-0.0-dev libjson-glib-dev libmessaging-menu-dev
libnotify-dev libsecret-1-dev libsqlite3-dev libnotify-dev libsecret-1-dev libsqlite3-dev
libunity-dev libunwind-dev libwebkit2gtk-4.0-dev libunity-dev libunwind-dev libwebkit2gtk-4.0-dev
libxml2-dev libxml2-dev libytnef0-dev
UBUNTU_TEST_DEPS: xauth xvfb UBUNTU_TEST_DEPS: xauth xvfb
fedora: fedora:

View file

@ -45,7 +45,8 @@ Install them by running this command:
glib2-devel gmime-devel gnome-online-accounts-devel gtk3-devel \ glib2-devel gmime-devel gnome-online-accounts-devel gtk3-devel \
iso-codes-devel json-glib-devel libcanberra-devel \ iso-codes-devel json-glib-devel libcanberra-devel \
libgee-devel libhandy-devel libnotify-devel libsecret-devel \ libgee-devel libhandy-devel libnotify-devel libsecret-devel \
libunwind-devel libxml2-devel sqlite-devel webkitgtk4-devel libunwind-devel libxml2-devel libytnef-devel sqlite-devel \
webkitgtk4-devel
Installing dependencies on Ubuntu/Debian Installing dependencies on Ubuntu/Debian
---------------------------------------- ----------------------------------------
@ -58,7 +59,7 @@ Install them by running this command:
libglib2.0-dev libgmime-2.6-dev libgoa-1.0-dev libgtk-3-dev \ libglib2.0-dev libgmime-2.6-dev libgoa-1.0-dev libgtk-3-dev \
libjson-glib-dev libhandy-dev libnotify-dev libsecret-1-dev \ libjson-glib-dev libhandy-dev libnotify-dev libsecret-1-dev \
libsqlite3-dev libunwind-dev libwebkit2gtk-4.0-dev \ libsqlite3-dev libunwind-dev libwebkit2gtk-4.0-dev \
libxml2-dev libxml2-dev libytnef0-dev
And for Ubuntu Unity integration: And for Ubuntu Unity integration:

View file

@ -0,0 +1,56 @@
/*
* Copyright (c) 2018 Oliver Giles <ohw.giles@gmail.com>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
[CCode (cheader_filename = "ytnef.h")]
namespace Ytnef {
[CCode (cname ="variableLength", has_type_id = false)]
public struct VariableLength {
[CCode (array_length_cname = "size")]
uint8[] data;
}
[CCode (cname = "MAPI_UNDEFINED")]
public VariableLength* MAPI_UNDEFINED;
[CCode (cname = "int", cprefix = "PT_", has_type_id = false)]
public enum PropType {
STRING8
}
[CCode (cname = "int", cprefix = "PR_", has_type_id = false)]
public enum PropID {
DISPLAY_NAME,
ATTACH_LONG_FILENAME
}
[CCode (cname = "PROP_TAG")]
public static int PROP_TAG(PropType type, PropID id);
[CCode (cname = "MAPIProps", has_type_id = false)]
public struct MAPIProps {
}
[CCode (cname = "Attachment", has_type_id = false)]
public struct Attachment {
VariableLength Title;
VariableLength FileData;
MAPIProps MAPI;
Attachment? next;
}
[CCode (cname = "TNEFStruct", destroy_function="TNEFFree", has_type_id = false)]
public struct TNEFStruct {
Attachment starting_attach;
}
[CCode (cname = "TNEFParseMemory", has_type_id = false)]
public static int ParseMemory(uint8[] data, out TNEFStruct tnef);
[CCode (cname = "MAPIFindProperty")]
public static unowned VariableLength* MAPIFindProperty(MAPIProps MAPI, uint tag);
}

View file

@ -78,6 +78,7 @@ libunwind_generic_dep = dependency(
'libunwind-generic', version: '>= 1.1', required: not get_option('libunwind_optional') 'libunwind-generic', version: '>= 1.1', required: not get_option('libunwind_optional')
) )
libxml = dependency('libxml-2.0', version: '>= 2.7.8') libxml = dependency('libxml-2.0', version: '>= 2.7.8')
libytnef = dependency('libytnef', version: '>= 1.9.3', required: get_option('tnef-support'))
posix = valac.find_library('posix') posix = valac.find_library('posix')
webkit2gtk_web_extension = dependency('webkit2gtk-web-extension-4.0', version: '>=' + target_webkit) webkit2gtk_web_extension = dependency('webkit2gtk-web-extension-4.0', version: '>=' + target_webkit)

View file

@ -5,3 +5,4 @@ option('ref_tracking', type: 'boolean', value: false, description: 'Whether to u
option('iso_639_xml', type: 'string', value: '', description: 'Full path to the ISO 639 XML file.') option('iso_639_xml', type: 'string', value: '', description: 'Full path to the ISO 639 XML file.')
option('iso_3166_xml', type: 'string', value: '', description: 'Full path to the ISO 3166 XML file.') option('iso_3166_xml', type: 'string', value: '', description: 'Full path to the ISO 3166 XML file.')
option('libunwind_optional', type: 'boolean', value: false, description: 'Determines if libunwind is required.') option('libunwind_optional', type: 'boolean', value: false, description: 'Determines if libunwind is required.')
option('tnef-support', type: 'boolean', value: true, description: 'Whether to support TNEF attachments (requires libytnef).')

View file

@ -205,6 +205,16 @@
} }
] ]
}, },
{
"name": "libytnef",
"sources": [
{
"type": "git",
"url": "https://github.com/Yeraze/ytnef.git",
"branch": "master"
}
]
},
{ {
"name": "geary", "name": "geary",
"buildsystem": "meson", "buildsystem": "meson",

View file

@ -333,6 +333,7 @@ geary_engine_dependencies = [
gmime, gmime,
javascriptcoregtk, javascriptcoregtk,
libxml, libxml,
libytnef,
posix, posix,
sqlite sqlite
] ]
@ -358,6 +359,13 @@ if libunwind_dep.found()
] ]
endif endif
if get_option('tnef-support')
geary_engine_dependencies += libytnef
geary_engine_vala_options += [
'-D', 'WITH_TNEF_SUPPORT'
]
endif
geary_engine_lib = static_library('geary-engine', geary_engine_lib = static_library('geary-engine',
geary_engine_sources, geary_engine_sources,
dependencies: geary_engine_dependencies, dependencies: geary_engine_dependencies,

View file

@ -880,17 +880,50 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
Mime.ContentType content_type = Mime.ContentType content_type =
part.get_effective_content_type(); part.get_effective_content_type();
// Skip text/plain and text/html parts that are INLINE #if WITH_TNEF_SUPPORT
// or UNSPECIFIED, as they will be included in the body if (content_type.is_type("application", "vnd.ms-tnef")) {
GMime.StreamMem stream = new GMime.StreamMem();
((GMime.Part) root).get_content_object().write_to_stream(stream);
ByteArray tnef_data = stream.get_byte_array();
Ytnef.TNEFStruct tn;
if (Ytnef.ParseMemory(tnef_data.data, out tn) == 0) {
for (unowned Ytnef.Attachment? a = tn.starting_attach.next; a != null; a = a.next) {
attachments.add(new Part(tnef_attachment_to_gmime_part(a)));
}
}
} else
#endif // WITH_TNEF_SUPPORT
if (actual_disposition == Mime.DispositionType.ATTACHMENT || if (actual_disposition == Mime.DispositionType.ATTACHMENT ||
(!content_type.is_type("text", "plain") && (!content_type.is_type("text", "plain") &&
!content_type.is_type("text", "html"))) { !content_type.is_type("text", "html"))) {
// Skip text/plain and text/html parts that are INLINE
// or UNSPECIFIED, as they will be included in the body
attachments.add(part); attachments.add(part);
} }
} }
} }
} }
#if WITH_TNEF_SUPPORT
private GMime.Part tnef_attachment_to_gmime_part(Ytnef.Attachment a) {
Ytnef.VariableLength* filenameProp = Ytnef.MAPIFindProperty(a.MAPI, Ytnef.PROP_TAG(Ytnef.PropType.STRING8, Ytnef.PropID.ATTACH_LONG_FILENAME));
if (filenameProp == Ytnef.MAPI_UNDEFINED) {
filenameProp = Ytnef.MAPIFindProperty(a.MAPI, Ytnef.PROP_TAG(Ytnef.PropType.STRING8, Ytnef.PropID.DISPLAY_NAME));
if (filenameProp == Ytnef.MAPI_UNDEFINED) {
filenameProp = &a.Title;
}
}
string filename = (string) filenameProp.data;
uint8[] data = Bytes.unref_to_data(new Bytes(a.FileData.data));
GMime.Part part = new GMime.Part();
part.set_filename(filename);
part.set_content_type(new GMime.ContentType.from_string(GLib.ContentType.guess(filename, data, null)));
part.set_content_object(new GMime.DataWrapper.with_stream(new GMime.StreamMem.with_buffer(data), GMime.ContentEncoding.BINARY));
return part;
}
#endif
public Gee.List<Geary.RFC822.Message> get_sub_messages() { public Gee.List<Geary.RFC822.Message> get_sub_messages() {
Gee.List<Geary.RFC822.Message> messages = new Gee.ArrayList<Geary.RFC822.Message>(); Gee.List<Geary.RFC822.Message> messages = new Gee.ArrayList<Geary.RFC822.Message>();
find_sub_messages(messages, message.get_mime_part()); find_sub_messages(messages, message.get_mime_part());

File diff suppressed because it is too large Load diff

View file

@ -4,6 +4,7 @@
<file>basic-text-plain.eml</file> <file>basic-text-plain.eml</file>
<file>basic-text-html.eml</file> <file>basic-text-html.eml</file>
<file>basic-multipart-alternative.eml</file> <file>basic-multipart-alternative.eml</file>
<file>basic-multipart-tnef.eml</file>
<file>geary-0.6-db.tar.xz</file> <file>geary-0.6-db.tar.xz</file>
</gresource> </gresource>
</gresources> </gresources>

View file

@ -12,6 +12,7 @@ class Geary.RFC822.MessageTest : TestCase {
private const string BASIC_TEXT_HTML = "basic-text-html.eml"; private const string BASIC_TEXT_HTML = "basic-text-html.eml";
private const string BASIC_MULTIPART_ALTERNATIVE = private const string BASIC_MULTIPART_ALTERNATIVE =
"basic-multipart-alternative.eml"; "basic-multipart-alternative.eml";
private const string BASIC_MULTIPART_TNEF = "basic-multipart-tnef.eml";
private const string HTML_CONVERSION_TEMPLATE = private const string HTML_CONVERSION_TEMPLATE =
"<div class=\"plaintext\" style=\"white-space: pre-wrap;\">%s</div>"; "<div class=\"plaintext\" style=\"white-space: pre-wrap;\">%s</div>";
@ -38,6 +39,7 @@ This is the second line.
add_test("text_plain_as_html", text_plain_as_html); add_test("text_plain_as_html", text_plain_as_html);
add_test("text_html_as_html", text_html_as_html); add_test("text_html_as_html", text_html_as_html);
add_test("text_html_as_plain", text_html_as_plain); add_test("text_html_as_plain", text_html_as_plain);
add_test("tnef_extract_attachments", tnef_extract_attachments);
add_test("multipart_alternative_as_plain", add_test("multipart_alternative_as_plain",
multipart_alternative_as_plain); multipart_alternative_as_plain);
add_test("multipart_alternative_as_converted_html", add_test("multipart_alternative_as_converted_html",
@ -124,6 +126,14 @@ This is the second line.
assert_string(BASIC_HTML_BODY, test.get_html_body(null)); assert_string(BASIC_HTML_BODY, test.get_html_body(null));
} }
public void tnef_extract_attachments() throws Error {
Message test = resource_to_message(BASIC_MULTIPART_TNEF);
Gee.List<Part> attachments = test.get_attachments();
assert_true(attachments.size == 2);
assert_true(attachments[0].get_clean_filename() == "zappa_av1.jpg");
assert_true(attachments[1].get_clean_filename() == "bookmark.htm");
}
public void multipart_alternative_as_plain() throws Error { public void multipart_alternative_as_plain() throws Error {
Message test = resource_to_message(BASIC_MULTIPART_ALTERNATIVE); Message test = resource_to_message(BASIC_MULTIPART_ALTERNATIVE);