TNEF (winmail.dat) parsing support via libytnef
This commit is contained in:
parent
ef8f97628e
commit
18fcf0e18f
11 changed files with 1870 additions and 6 deletions
|
|
@ -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:
|
||||||
|
|
|
||||||
5
INSTALL
5
INSTALL
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
||||||
56
bindings/vapi/libytnef.vapi
Normal file
56
bindings/vapi/libytnef.vapi
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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).')
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
1743
test/data/basic-multipart-tnef.eml
Normal file
1743
test/data/basic-multipart-tnef.eml
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue