iMerge branch 'wip/90-304-image-dnd-paste' into 'mainline'
Inline image drag and drop #90 and image paste from clipboard #304 See merge request GNOME/geary!343
This commit is contained in:
commit
8f2563bc09
12 changed files with 433 additions and 41 deletions
|
|
@ -14,7 +14,7 @@ public class ComposerWebView : ClientWebView {
|
|||
|
||||
// WebKit message handler names
|
||||
private const string CURSOR_CONTEXT_CHANGED = "cursorContextChanged";
|
||||
|
||||
private const string DRAG_DROP_RECEIVED = "dragDropReceived";
|
||||
|
||||
/**
|
||||
* Encapsulates editing-related state for a specific DOM node.
|
||||
|
|
@ -108,6 +108,9 @@ public class ComposerWebView : ClientWebView {
|
|||
/** Emitted when the cursor's edit context has changed. */
|
||||
public signal void cursor_context_changed(EditContext cursor_context);
|
||||
|
||||
/** Emitted when an image file has been dropped on the composer */
|
||||
public signal void image_file_dropped(string filename, string type, uint8[] contents);
|
||||
|
||||
/** Workaround for WebView eating the button event */
|
||||
internal signal bool button_release_event_done(Gdk.Event event);
|
||||
|
||||
|
|
@ -121,6 +124,7 @@ public class ComposerWebView : ClientWebView {
|
|||
this.user_content_manager.add_script(ComposerWebView.app_script);
|
||||
|
||||
register_message_handler(CURSOR_CONTEXT_CHANGED, on_cursor_context_changed);
|
||||
register_message_handler(DRAG_DROP_RECEIVED, on_drag_drop_received);
|
||||
|
||||
// XXX this is a bit of a hack given the docs for is_empty,
|
||||
// above
|
||||
|
|
@ -521,4 +525,40 @@ public class ComposerWebView : ClientWebView {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a dropped image
|
||||
*/
|
||||
private void on_drag_drop_received(WebKit.JavascriptResult result) {
|
||||
|
||||
try {
|
||||
JSC.Value object = result.get_js_value();
|
||||
string filename = Util.JS.to_string(
|
||||
Util.JS.get_property(object, "fileName")
|
||||
);
|
||||
string filename_unescaped = GLib.Uri.unescape_string(filename);
|
||||
|
||||
string file_type = Util.JS.to_string(
|
||||
Util.JS.get_property(object, "fileType")
|
||||
);
|
||||
|
||||
string content_base64 = Util.JS.to_string(
|
||||
Util.JS.get_property(object, "content")
|
||||
);
|
||||
uint8[] image = GLib.Base64.decode(content_base64);
|
||||
|
||||
if (image.length == 0) {
|
||||
warning("%s is empty", filename);
|
||||
return;
|
||||
}
|
||||
|
||||
// A simple check to see if the file looks like an image. A problem here
|
||||
// will be this accepting types which won't be supported by WebKit
|
||||
// or recipients.
|
||||
if (file_type.index_of("image/") == 0) {
|
||||
image_file_dropped(filename_unescaped, file_type, image);
|
||||
}
|
||||
} catch (Util.JS.Error err) {
|
||||
debug("Could not get deceptive link param: %s", err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,6 +179,8 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
_("attach|attaching|attaches|attachment|attachments|attached|enclose|enclosed|enclosing|encloses|enclosure|enclosures");
|
||||
|
||||
|
||||
private const string PASTED_IMAGE_FILENAME_TEMPLATE = "geary-pasted-image-%u.png";
|
||||
|
||||
public Geary.Account account { get; private set; }
|
||||
private Gee.Map<string, Geary.AccountInformation> accounts;
|
||||
|
||||
|
|
@ -367,8 +369,8 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
private AttachPending pending_include = AttachPending.INLINE_ONLY;
|
||||
private Gee.Set<File> attached_files = new Gee.HashSet<File>(Geary.Files.nullable_hash,
|
||||
Geary.Files.nullable_equal);
|
||||
private Gee.Map<string,File> inline_files = new Gee.HashMap<string,File>();
|
||||
private Gee.Map<string,File> cid_files = new Gee.HashMap<string,File>();
|
||||
private Gee.Map<string,Geary.Memory.Buffer> inline_files = new Gee.HashMap<string,Geary.Memory.Buffer>();
|
||||
private Gee.Map<string,Geary.Memory.Buffer> cid_files = new Gee.HashMap<string,Geary.Memory.Buffer>();
|
||||
|
||||
private Geary.App.DraftManager? draft_manager = null;
|
||||
private GLib.Cancellable? draft_manager_opening = null;
|
||||
|
|
@ -524,6 +526,12 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
this.application.engine.account_unavailable.connect(
|
||||
on_account_unavailable
|
||||
);
|
||||
|
||||
// Listen for drag and dropped image file
|
||||
this.editor.image_file_dropped.connect(
|
||||
on_image_file_dropped
|
||||
);
|
||||
|
||||
// TODO: also listen for account updates to allow adding identities while writing an email
|
||||
|
||||
this.from = new Geary.RFC822.MailboxAddresses.single(account.information.primary_mailbox);
|
||||
|
|
@ -1623,9 +1631,10 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
// using a cid: URL anyway, so treat it as an
|
||||
// attachment instead.
|
||||
if (content_id != null) {
|
||||
this.cid_files[content_id] = file;
|
||||
Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true);
|
||||
this.cid_files[content_id] = file_buffer;
|
||||
this.editor.add_internal_resource(
|
||||
content_id, new Geary.Memory.FileBuffer(file, true)
|
||||
content_id, file_buffer
|
||||
);
|
||||
} else {
|
||||
type = Geary.Mime.DispositionType.ATTACHMENT;
|
||||
|
|
@ -1641,7 +1650,10 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
!this.attached_files.contains(file) &&
|
||||
!this.inline_files.has_key(content_id)) {
|
||||
if (type == Geary.Mime.DispositionType.INLINE) {
|
||||
add_inline_part(file, content_id);
|
||||
check_attachment_file(file);
|
||||
Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true);
|
||||
string unused;
|
||||
add_inline_part(file_buffer, content_id, out unused);
|
||||
} else {
|
||||
add_attachment_part(file);
|
||||
}
|
||||
|
|
@ -1690,18 +1702,41 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
update_attachments_view();
|
||||
}
|
||||
|
||||
private void add_inline_part(File target, string content_id)
|
||||
private void add_inline_part(Geary.Memory.Buffer target, string content_id, out string unique_contentid)
|
||||
throws AttachmentError {
|
||||
check_attachment_file(target);
|
||||
this.inline_files[content_id] = target;
|
||||
try {
|
||||
this.editor.add_internal_resource(
|
||||
content_id, new Geary.Memory.FileBuffer(target, true)
|
||||
|
||||
const string UNIQUE_RENAME_TEMPLATE = "%s_%02u";
|
||||
|
||||
if (target.size == 0)
|
||||
throw new AttachmentError.FILE(
|
||||
_("“%s” is an empty file.").printf(content_id)
|
||||
);
|
||||
} catch (Error err) {
|
||||
// unlikely
|
||||
debug("Failed to re-open file for attachment: %s", err.message);
|
||||
|
||||
// Avoid filename conflicts
|
||||
unique_contentid = content_id;
|
||||
int suffix_index = 0;
|
||||
string unsuffixed_filename = "";
|
||||
while (this.inline_files.has_key(unique_contentid)) {
|
||||
string[] filename_parts = unique_contentid.split(".");
|
||||
|
||||
// Handle no file extension
|
||||
int partindex;
|
||||
if (filename_parts.length > 1) {
|
||||
partindex = filename_parts.length-2;
|
||||
} else {
|
||||
partindex = 0;
|
||||
}
|
||||
if (unsuffixed_filename == "")
|
||||
unsuffixed_filename = filename_parts[partindex];
|
||||
filename_parts[partindex] = UNIQUE_RENAME_TEMPLATE.printf(unsuffixed_filename, suffix_index++);
|
||||
|
||||
unique_contentid = string.joinv(".", filename_parts);
|
||||
}
|
||||
|
||||
this.inline_files[unique_contentid] = target;
|
||||
this.editor.add_internal_resource(
|
||||
unique_contentid, target
|
||||
);
|
||||
}
|
||||
|
||||
private FileInfo check_attachment_file(File target)
|
||||
|
|
@ -1873,7 +1908,14 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
private void on_paste(SimpleAction action, Variant? param) {
|
||||
if (this.container.get_focus() == this.editor) {
|
||||
if (this.editor.is_rich_text) {
|
||||
this.editor.paste_rich_text();
|
||||
// Check for pasted image in clipboard
|
||||
Gtk.Clipboard clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD);
|
||||
bool has_image = clipboard.wait_is_image_available();
|
||||
if (has_image) {
|
||||
paste_image();
|
||||
} else {
|
||||
this.editor.paste_rich_text();
|
||||
}
|
||||
} else {
|
||||
this.editor.paste_plain_text();
|
||||
}
|
||||
|
|
@ -1882,6 +1924,39 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a pasted image, adding it as an inline attachment
|
||||
*/
|
||||
private void paste_image() {
|
||||
// The slow operations here are creating the PNG and, to a lesser extent,
|
||||
// requesting the image from the clipboard
|
||||
this.container.top_window.application.mark_busy();
|
||||
|
||||
get_clipboard(Gdk.SELECTION_CLIPBOARD).request_image((clipboard, pixbuf) => {
|
||||
if (pixbuf != null) {
|
||||
try {
|
||||
uint8[] buffer;
|
||||
pixbuf.save_to_buffer(out buffer, "png");
|
||||
Geary.Memory.ByteBuffer byte_buffer = new Geary.Memory.ByteBuffer(buffer, buffer.length);
|
||||
|
||||
GLib.DateTime time_now = new GLib.DateTime.now();
|
||||
string filename = PASTED_IMAGE_FILENAME_TEMPLATE.printf(time_now.hash());
|
||||
|
||||
string unique_filename;
|
||||
add_inline_part(byte_buffer, filename, out unique_filename);
|
||||
this.editor.insert_image(
|
||||
ClientWebView.INTERNAL_URL_PREFIX + unique_filename
|
||||
);
|
||||
} catch (Error error) {
|
||||
warning("Failed to paste image %s", error.message);
|
||||
}
|
||||
} else {
|
||||
warning("Failed to get image from clipboard");
|
||||
}
|
||||
this.container.top_window.application.unmark_busy();
|
||||
});
|
||||
}
|
||||
|
||||
private void on_paste_without_formatting(SimpleAction action, Variant? param) {
|
||||
if (this.container.get_focus() == this.editor)
|
||||
this.editor.paste_plain_text();
|
||||
|
|
@ -2496,10 +2571,13 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
dialog.hide();
|
||||
foreach (File file in dialog.get_files()) {
|
||||
try {
|
||||
check_attachment_file(file);
|
||||
Geary.Memory.FileBuffer file_buffer = new Geary.Memory.FileBuffer(file, true);
|
||||
string path = file.get_path();
|
||||
add_inline_part(file, path);
|
||||
string unique_filename;
|
||||
add_inline_part(file_buffer, path, out unique_filename);
|
||||
this.editor.insert_image(
|
||||
ClientWebView.INTERNAL_URL_PREFIX + path
|
||||
ClientWebView.INTERNAL_URL_PREFIX + unique_filename
|
||||
);
|
||||
} catch (Error err) {
|
||||
attachment_failed(err.message);
|
||||
|
|
@ -2555,4 +2633,21 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a dropped image file, adding it as an inline attachment
|
||||
*/
|
||||
private void on_image_file_dropped(string filename, string file_type, uint8[] contents) {
|
||||
Geary.Memory.ByteBuffer buffer = new Geary.Memory.ByteBuffer(contents, contents.length);
|
||||
string unique_filename;
|
||||
try {
|
||||
add_inline_part(buffer, filename, out unique_filename);
|
||||
} catch (AttachmentError err) {
|
||||
warning("Couldn't attach dropped empty file %s", filename);
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor.insert_image(
|
||||
ClientWebView.INTERNAL_URL_PREFIX + unique_filename
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@ public void webkit_web_extension_initialize_with_user_data(WebKit.WebExtension e
|
|||
*/
|
||||
public class GearyWebExtension : Object {
|
||||
|
||||
|
||||
private const string[] ALLOWED_SCHEMES = { "cid", "geary", "data" };
|
||||
private const string[] ALLOWED_SCHEMES = { "cid", "geary", "data", "blob" };
|
||||
|
||||
private WebKit.WebExtension extension;
|
||||
|
||||
|
|
|
|||
|
|
@ -38,10 +38,10 @@ public class Geary.ComposedEmail : BaseObject {
|
|||
|
||||
public Gee.Set<File> attached_files { get; private set;
|
||||
default = new Gee.HashSet<File>(Geary.Files.nullable_hash, Geary.Files.nullable_equal); }
|
||||
public Gee.Map<string,File> inline_files { get; private set;
|
||||
default = new Gee.HashMap<string,File>(); }
|
||||
public Gee.Map<string,File> cid_files { get; private set;
|
||||
default = new Gee.HashMap<string,File>(); }
|
||||
public Gee.Map<string,Memory.Buffer> inline_files { get; private set;
|
||||
default = new Gee.HashMap<string,Memory.Buffer>(); }
|
||||
public Gee.Map<string,Memory.Buffer> cid_files { get; private set;
|
||||
default = new Gee.HashMap<string,Memory.Buffer>(); }
|
||||
|
||||
public string img_src_prefix { get; set; default = ""; }
|
||||
|
||||
|
|
@ -88,18 +88,17 @@ public class Geary.ComposedEmail : BaseObject {
|
|||
public bool replace_inline_img_src(string orig, string replacement) {
|
||||
// XXX This and contains_inline_img_src are pretty
|
||||
// hacky. Should probably be working with a DOM tree.
|
||||
bool ret = false;
|
||||
int index = -1;
|
||||
if (this.body_html != null) {
|
||||
string old_body = this.body_html;
|
||||
this.body_html = old_body.replace(
|
||||
IMG_SRC_TEMPLATE.printf(this.img_src_prefix + orig),
|
||||
IMG_SRC_TEMPLATE.printf(replacement)
|
||||
);
|
||||
// Avoid doing a proper comparison so we don't need to scan
|
||||
// the whole string again.
|
||||
ret = this.body_html.length != old_body.length;
|
||||
string prefixed_orig = IMG_SRC_TEMPLATE.printf(this.img_src_prefix + orig);
|
||||
index = this.body_html.index_of(prefixed_orig);
|
||||
if (index != -1) {
|
||||
this.body_html = this.body_html.substring(0, index) +
|
||||
IMG_SRC_TEMPLATE.printf(replacement) +
|
||||
this.body_html.substring(index + prefixed_orig.length);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
return index != -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
|
|||
new Gee.LinkedList<GMime.Object>();
|
||||
|
||||
// The files that need to have Content IDs assigned
|
||||
Gee.Map<string,File> inline_files = new Gee.HashMap<string,File>();
|
||||
Gee.Map<string,Memory.Buffer> inline_files = new Gee.HashMap<string,Memory.Buffer>();
|
||||
inline_files.set_all(email.inline_files);
|
||||
|
||||
// Create parts for inline images, if any, and updating
|
||||
|
|
@ -248,18 +248,18 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
|
|||
// assigned
|
||||
foreach (string cid in email.cid_files.keys) {
|
||||
if (email.contains_inline_img_src(CID_URL_PREFIX + cid)) {
|
||||
File file = email.cid_files[cid];
|
||||
GMime.Object? inline_part = null;
|
||||
try {
|
||||
inline_part = yield get_file_part(
|
||||
file,
|
||||
inline_part = yield get_buffer_part(
|
||||
email.cid_files[cid],
|
||||
GLib.Path.get_basename(cid),
|
||||
Geary.Mime.DispositionType.INLINE,
|
||||
cancellable
|
||||
);
|
||||
} catch (GLib.Error err) {
|
||||
warning(
|
||||
"Error creating CID part %s: %s",
|
||||
file.get_path(),
|
||||
cid,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
|
|
@ -288,15 +288,16 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
|
|||
CID_URL_PREFIX + cid)) {
|
||||
GMime.Object? inline_part = null;
|
||||
try {
|
||||
inline_part = yield get_file_part(
|
||||
inline_part = yield get_buffer_part(
|
||||
inline_files[name],
|
||||
GLib.Path.get_basename(name),
|
||||
Geary.Mime.DispositionType.INLINE,
|
||||
cancellable
|
||||
);
|
||||
} catch (GLib.Error err) {
|
||||
warning(
|
||||
"Error creating inline file part %s: %s",
|
||||
inline_files[name].get_path(),
|
||||
name,
|
||||
err.message
|
||||
);
|
||||
}
|
||||
|
|
@ -441,6 +442,56 @@ public class Geary.RFC822.Message : BaseObject, EmailHeaderSet {
|
|||
GMime.StreamGIO stream = new GMime.StreamGIO(file);
|
||||
stream.set_owner(false);
|
||||
|
||||
return yield finalise_attachment_part(stream, part, content_type, cancellable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a GMime part for the provided attachment buffer
|
||||
*/
|
||||
private async GMime.Part? get_buffer_part(Memory.Buffer buffer,
|
||||
string basename,
|
||||
Geary.Mime.DispositionType disposition,
|
||||
GLib.Cancellable cancellable)
|
||||
throws Error {
|
||||
|
||||
Mime.ContentType? mime_type = Mime.ContentType.guess_type(
|
||||
basename,
|
||||
buffer
|
||||
);
|
||||
|
||||
if (mime_type == null) {
|
||||
throw new RFC822Error.INVALID(
|
||||
_("Could not determine mime type for “%s”.").printf(basename)
|
||||
);
|
||||
}
|
||||
|
||||
GMime.ContentType? content_type = new GMime.ContentType.from_string(mime_type.get_mime_type());
|
||||
|
||||
if (content_type == null) {
|
||||
throw new RFC822Error.INVALID(
|
||||
_("Could not determine content type for mime type “%s” on “%s”.").printf(mime_type.to_string(), basename)
|
||||
);
|
||||
}
|
||||
|
||||
GMime.Part part = new GMime.Part();
|
||||
part.set_disposition(disposition.serialize());
|
||||
part.set_filename(basename);
|
||||
part.set_content_type(content_type);
|
||||
|
||||
GMime.StreamMem stream = Utils.create_stream_mem(buffer);
|
||||
|
||||
return yield finalise_attachment_part(stream, part, content_type, cancellable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set encoding and content object on GMime part
|
||||
*/
|
||||
private async GMime.Part finalise_attachment_part(GMime.Stream stream,
|
||||
GMime.Part part,
|
||||
GMime.ContentType content_type,
|
||||
GLib.Cancellable cancellable)
|
||||
throws Error {
|
||||
|
||||
// Text parts should be scanned fully to determine best
|
||||
// (i.e. most compact) transport encoding to use, but
|
||||
// that's usually fine since they tend to be
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@
|
|||
<file>basic-multipart-alternative.eml</file>
|
||||
<file>basic-multipart-tnef.eml</file>
|
||||
<file>geary-0.6-db.tar.xz</file>
|
||||
<file>test-attachment-image.png</file>
|
||||
</gresource>
|
||||
</gresources>
|
||||
|
|
|
|||
BIN
test/data/test-attachment-image.png
Normal file
BIN
test/data/test-attachment-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7 KiB |
53
test/engine/api/geary-composed-email-test.vala
Normal file
53
test/engine/api/geary-composed-email-test.vala
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2016-2018 Michael Gratton <mike@vee.net>
|
||||
*
|
||||
* 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.ComposedEmailTest: TestCase {
|
||||
|
||||
private const string IMG_CONTAINING_HTML_BODY = "<img src=\"test.png\" />";
|
||||
|
||||
public ComposedEmailTest() {
|
||||
base("Geary.ComposedEmailTest");
|
||||
add_test("contains_inline_img_src", contains_inline_img_src);
|
||||
add_test("replace_inline_img_src", replace_inline_img_src);
|
||||
}
|
||||
|
||||
public void contains_inline_img_src() throws Error {
|
||||
ComposedEmail composed = build_composed_with_img_src();
|
||||
assert_true(composed.contains_inline_img_src("test.png"), "Expected matched image source");
|
||||
assert_false(composed.contains_inline_img_src("missing.png"), "Expected missing image");
|
||||
}
|
||||
|
||||
public void replace_inline_img_src() throws Error {
|
||||
ComposedEmail composed = build_composed_with_img_src();
|
||||
assert_true(composed.replace_inline_img_src("test.png", "updated.png"), "Expected replacement success");
|
||||
assert_false(composed.replace_inline_img_src("missing.png", "updated.png"), "Expected replacement failure");
|
||||
assert_true(composed.contains_inline_img_src("updated.png"), "Expected new image source");
|
||||
|
||||
assert_true(composed.replace_inline_img_src("updated.png", "1234567.png"), "Expected replacement success for same length filename");
|
||||
assert_true(composed.contains_inline_img_src("1234567.png"), "Expected new same length image source");
|
||||
}
|
||||
|
||||
private ComposedEmail build_composed_with_img_src() {
|
||||
RFC822.MailboxAddress to = new RFC822.MailboxAddress(
|
||||
"Test", "test@example.com"
|
||||
);
|
||||
RFC822.MailboxAddress from = new RFC822.MailboxAddress(
|
||||
"Sender", "sender@example.com"
|
||||
);
|
||||
|
||||
return new Geary.ComposedEmail(
|
||||
new GLib.DateTime.now_local(),
|
||||
new Geary.RFC822.MailboxAddresses.single(from),
|
||||
new Geary.RFC822.MailboxAddresses.single(to),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
IMG_CONTAINING_HTML_BODY
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -29,6 +29,11 @@ This is the second line.
|
|||
|
||||
""";
|
||||
|
||||
private static string SIMPLE_MULTIRECIPIENT_TO_CC_BCC = "From: Jill Smith <jill@somewhere.tld>\r\nTo: Jane Doe <jdoe@somewhere.tld>\r\nCc: Jane Doe CC <jdoe_cc@somewhere.tld>\r\nBcc: Jane Doe BCC <jdoe_bcc@somewhere.tld>\r\nSubject: Re: Saying Hello\r\nDate: Fri, 21 Nov 1997 10:01:10 -0600\r\n\r\nThis is a reply to your hello.\r\n\r\n";
|
||||
private static string NETWORK_BUFFER_EXPECTED = "From: Alice <alice@example.net>\r\nSender: Bob <bob@example.net>\r\nTo: Charlie <charlie@example.net>\r\nCC: Dave <dave@example.net>\r\nBCC: Eve <eve@example.net>\r\nReply-To: \"Alice: Personal Account\" <alice@example.org>\r\nSubject: Re: Basic text/plain message\r\nDate: Fri, 21 Nov 1997 10:01:10 -0600\r\nMessage-ID: <3456@example.net>\r\nIn-Reply-To: <1234@local.machine.example>\r\nReferences: <1234@local.machine.example>\r\nX-Mailer: Geary Test Suite 1.0\r\n\r\nThis is the first line.\r\n\r\nThis is the second line.\r\n\r\n";
|
||||
|
||||
private static string TEST_ATTACHMENT_IMAGE_FILENAME = "test-attachment-image.png";
|
||||
|
||||
public MessageTest() {
|
||||
base("Geary.RFC822.MessageTest");
|
||||
add_test("basic_message_from_buffer", basic_message_from_buffer);
|
||||
|
|
@ -49,6 +54,11 @@ This is the second line.
|
|||
add_test("multipart_alternative_as_html",
|
||||
multipart_alternative_as_html);
|
||||
add_test("get_preview", get_preview);
|
||||
add_test("get_recipients", get_recipients);
|
||||
add_test("get_searchable_body", get_searchable_body);
|
||||
add_test("get_searchable_recipients", get_searchable_recipients);
|
||||
add_test("get_network_buffer", get_network_buffer);
|
||||
add_test("from_composed_email_inline_attachments", from_composed_email_inline_attachments);
|
||||
}
|
||||
|
||||
public void basic_message_from_buffer() throws Error {
|
||||
|
|
@ -169,6 +179,106 @@ This is the second line.
|
|||
assert(multipart_signed.get_preview() == MULTIPART_SIGNED_MESSAGE_PREVIEW);
|
||||
}
|
||||
|
||||
public void get_recipients() throws Error {
|
||||
Message test = string_to_message(SIMPLE_MULTIRECIPIENT_TO_CC_BCC);
|
||||
|
||||
Gee.List<RFC822.MailboxAddress>? addresses = test.get_recipients();
|
||||
|
||||
Gee.List<string> verify_list = new Gee.ArrayList<string>();
|
||||
verify_list.add("Jane Doe <jdoe@somewhere.tld>");
|
||||
verify_list.add("Jane Doe CC <jdoe_cc@somewhere.tld>");
|
||||
verify_list.add("Jane Doe BCC <jdoe_bcc@somewhere.tld>");
|
||||
|
||||
assert_addresses_list(addresses, verify_list, "get_recipients");
|
||||
}
|
||||
|
||||
public void get_searchable_body() throws Error {
|
||||
Message test = resource_to_message(BASIC_TEXT_HTML);
|
||||
string searchable = test.get_searchable_body();
|
||||
assert_true(searchable.contains("This is the first line"), "Expected body text");
|
||||
assert_false(searchable.contains("<P>"), "Expected html removed");
|
||||
}
|
||||
|
||||
public void get_searchable_recipients() throws Error {
|
||||
Message test = string_to_message(SIMPLE_MULTIRECIPIENT_TO_CC_BCC);
|
||||
string searchable = test.get_searchable_recipients();
|
||||
assert_true(searchable.contains("Jane Doe <jdoe@somewhere.tld>"), "Expected to address");
|
||||
assert_true(searchable.contains("Jane Doe CC <jdoe_cc@somewhere.tld>"), "Expected cc address");
|
||||
assert_true(searchable.contains("Jane Doe BCC <jdoe_bcc@somewhere.tld>"), "Expected bcc address");
|
||||
}
|
||||
|
||||
public void get_network_buffer() throws Error {
|
||||
Message test = resource_to_message(BASIC_TEXT_PLAIN);
|
||||
Memory.Buffer buffer = test.get_network_buffer(true);
|
||||
assert_true(buffer.to_string() == NETWORK_BUFFER_EXPECTED, "Network buffer differs");
|
||||
}
|
||||
|
||||
public void from_composed_email_inline_attachments() throws Error {
|
||||
RFC822.MailboxAddress to = new RFC822.MailboxAddress(
|
||||
"Test", "test@example.com"
|
||||
);
|
||||
RFC822.MailboxAddress from = new RFC822.MailboxAddress(
|
||||
"Sender", "sender@example.com"
|
||||
);
|
||||
|
||||
Geary.ComposedEmail composed = new Geary.ComposedEmail(
|
||||
new GLib.DateTime.now_local(),
|
||||
new Geary.RFC822.MailboxAddresses.single(from),
|
||||
new Geary.RFC822.MailboxAddresses.single(to),
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
"<img src=\"cid:test-attachment-image.png\" /><img src=\"needing_cid.png\" />"
|
||||
);
|
||||
|
||||
GLib.File resource =
|
||||
GLib.File.new_for_uri(RESOURCE_URI).resolve_relative_path(TEST_ATTACHMENT_IMAGE_FILENAME);
|
||||
uint8[] contents;
|
||||
resource.load_contents(null, out contents, null);
|
||||
Geary.Memory.ByteBuffer buffer = new Geary.Memory.ByteBuffer(contents, contents.length);
|
||||
composed.cid_files[TEST_ATTACHMENT_IMAGE_FILENAME] = buffer;
|
||||
Geary.Memory.ByteBuffer buffer2 = new Geary.Memory.ByteBuffer(contents, contents.length);
|
||||
composed.inline_files["needing_cid.png"] = buffer2;
|
||||
|
||||
this.message_from_composed_email.begin(
|
||||
composed,
|
||||
async_complete_full
|
||||
);
|
||||
Geary.RFC822.Message message = message_from_composed_email.end(async_result());
|
||||
|
||||
Gee.List<Part> attachments = message.get_attachments();
|
||||
|
||||
bool found_first = false;
|
||||
bool found_second = false;
|
||||
bool second_id_renamed = false;
|
||||
foreach (Part part in attachments) {
|
||||
if (part.get_clean_filename() == TEST_ATTACHMENT_IMAGE_FILENAME) {
|
||||
found_first = true;
|
||||
} else if (part.get_clean_filename() == "needing_cid.png") {
|
||||
found_second = true;
|
||||
second_id_renamed = part.content_id != "needing_cid.png";
|
||||
}
|
||||
}
|
||||
assert_true(found_first, "Expected CID attachment");
|
||||
assert_true(found_second, "Expected inline attachment");
|
||||
assert_true(second_id_renamed, "Expected inline attachment renamed");
|
||||
|
||||
string html_body = message.get_html_body(null);
|
||||
assert_false(html_body.contains("src=\"needing_cid.png\""), "Expected updated attachment content ID");
|
||||
|
||||
Memory.Buffer out_buffer = message.get_native_buffer();
|
||||
assert_true(out_buffer.size > (buffer.size+buffer2.size), "Expected sizeable message");
|
||||
}
|
||||
|
||||
private async Geary.RFC822.Message message_from_composed_email(Geary.ComposedEmail composed) {
|
||||
return yield new Geary.RFC822.Message.from_composed_email(
|
||||
composed,
|
||||
GMime.utils_generate_message_id(composed.from.get(0).domain),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private Message resource_to_message(string path) throws Error {
|
||||
GLib.File resource =
|
||||
GLib.File.new_for_uri(RESOURCE_URI).resolve_relative_path(path);
|
||||
|
|
@ -208,6 +318,17 @@ This is the second line.
|
|||
assert_string(expected, addresses.to_rfc822_string());
|
||||
}
|
||||
|
||||
private void assert_addresses_list(Gee.List<RFC822.MailboxAddress>? addresses,
|
||||
Gee.List<string> expected,
|
||||
string context)
|
||||
throws Error {
|
||||
assert_non_null(addresses, context + " not null");
|
||||
assert_true(addresses.size == expected.size, context + " size");
|
||||
foreach (RFC822.MailboxAddress address in addresses) {
|
||||
assert_true(expected.contains(address.to_rfc822_string()), context + " missing");
|
||||
}
|
||||
}
|
||||
|
||||
private void assert_message_id_list(Geary.RFC822.MessageIDList? ids,
|
||||
string expected)
|
||||
throws Error {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ geary_test_engine_sources = [
|
|||
'engine/api/geary-engine-test.vala',
|
||||
'engine/api/geary-folder-path-test.vala',
|
||||
'engine/api/geary-service-information-test.vala',
|
||||
'engine/api/geary-composed-email-test.vala',
|
||||
'engine/app/app-conversation-test.vala',
|
||||
'engine/app/app-conversation-monitor-test.vala',
|
||||
'engine/app/app-conversation-set-test.vala',
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ int main(string[] args) {
|
|||
engine.add_suite(new Geary.RFC822.PartTest().get_suite());
|
||||
engine.add_suite(new Geary.RFC822.Utils.Test().get_suite());
|
||||
engine.add_suite(new Geary.String.Test().get_suite());
|
||||
engine.add_suite(new Geary.ComposedEmailTest().get_suite());
|
||||
|
||||
/*
|
||||
* Run the tests
|
||||
|
|
|
|||
|
|
@ -93,6 +93,12 @@ ComposerPageState.prototype = {
|
|||
}
|
||||
}, true);
|
||||
|
||||
// Handle file drag & drop
|
||||
document.body.addEventListener("drop", state.handleFileDrop, true);
|
||||
document.body.addEventListener("allowDrop", function(e) {
|
||||
ev.preventDefault();
|
||||
}, true);
|
||||
|
||||
// Search for and remove a particular styling when we quote
|
||||
// text. If that style exists in the quoted text, we alter it
|
||||
// slightly so we don't mess with it later.
|
||||
|
|
@ -380,6 +386,31 @@ ComposerPageState.prototype = {
|
|||
}
|
||||
}
|
||||
return inPart;
|
||||
},
|
||||
handleFileDrop: function(dropEvent) {
|
||||
dropEvent.preventDefault();
|
||||
|
||||
for (var i = 0; i < dropEvent.dataTransfer.files.length; i++) {
|
||||
const file = dropEvent.dataTransfer.files[i];
|
||||
|
||||
if (!file.type.startsWith('image/'))
|
||||
continue;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (function(filename, imageType) { return function(loadEvent) {
|
||||
// Remove prefixed file type and encoding type
|
||||
var parts = loadEvent.target.result.split(",");
|
||||
if (parts.length < 2)
|
||||
return;
|
||||
|
||||
window.webkit.messageHandlers.dragDropReceived.postMessage({
|
||||
fileName: encodeURIComponent(filename),
|
||||
fileType: imageType,
|
||||
content: parts[1]
|
||||
});
|
||||
}; })(file.name, file.type);
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue