Merge branch 'master' into feature/search

Conflicts:
	sql/version-010.sql
	src/client/folder-list/folder-list-folder-entry.vala
	src/engine/rfc822/rfc822-message.vala

Also, I had to manually fix some compile errors introduced due to
interfaces changing on master.
This commit is contained in:
Charles Lindsay 2013-06-24 17:54:41 -07:00
commit fca993fec7
70 changed files with 1751 additions and 507 deletions

76
icons/edit-symbolic.svg Normal file
View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
width="16"
height="16"
id="svg7384"
inkscape:version="0.48.3.1 r9886"
sodipodi:docname="list-add-symbolic.svg">
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1360"
inkscape:window-height="724"
id="namedview3045"
showgrid="false"
showborder="false"
inkscape:showpageshadow="false"
inkscape:zoom="1"
inkscape:cx="10.018511"
inkscape:cy="10.058708"
inkscape:window-x="0"
inkscape:window-y="22"
inkscape:window-maximized="1"
inkscape:current-layer="svg7384">
<inkscape:grid
type="xygrid"
id="grid3047"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" />
</sodipodi:namedview>
<title
id="title9167">Gnome Symbolic Icon Theme</title>
<defs
id="defs9" />
<metadata
id="metadata90">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>Gnome Symbolic Icon Theme</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<path
style="fill:#bebebe;fill-opacity:1;stroke:none"
d="M 1,15 4,14 13,5 C 12.728018,3.8509406 12.021543,3.2329291 11,3 l -9,9 z"
id="path3049"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccc" />
<path
style="fill:#bebebe;fill-opacity:1;stroke:none"
d="m 12,2 1,-1 c 1.048085,0.2307602 1.775924,0.827515 2,2 L 14,4 C 13.745812,2.9503172 13.079146,2.2836505 12,2 z"
id="path3819"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,5 +1,5 @@
--
-- Dummy database upgrade to add MessageSearchTable, whose parameters depend on
-- things we need at run-time. See src/engine/imap-db/imap-db-database.vala in
-- post_upgrade() for the code that runs the upgrade.
-- Add unread count column to the FolderTable
--
ALTER TABLE FolderTable ADD COLUMN unread_count INTEGER DEFAULT 0;

View file

@ -1,7 +1,5 @@
--
-- Add the internaldate column as a time_t value so we can sort on it.
-- Dummy database upgrade to add MessageSearchTable, whose parameters depend on
-- things we need at run-time. See src/engine/imap-db/imap-db-database.vala in
-- post_upgrade() for the code that runs the upgrade.
--
ALTER TABLE MessageTable ADD COLUMN internaldate_time_t INTEGER;
CREATE INDEX MessageTableInternalDateTimeTIndex ON MessageTable(internaldate_time_t);

7
sql/version-012.sql Normal file
View file

@ -0,0 +1,7 @@
--
-- Add the internaldate column as a time_t value so we can sort on it.
--
ALTER TABLE MessageTable ADD COLUMN internaldate_time_t INTEGER;
CREATE INDEX MessageTableInternalDateTimeTIndex ON MessageTable(internaldate_time_t);

View file

@ -15,6 +15,7 @@ engine/abstract/geary-abstract-local-folder.vala
engine/api/geary-account.vala
engine/api/geary-account-information.vala
engine/api/geary-aggregated-folder-properties.vala
engine/api/geary-attachment.vala
engine/api/geary-base-object.vala
engine/api/geary-composed-email.vala
@ -73,6 +74,7 @@ engine/imap/api/imap-email-identifier.vala
engine/imap/api/imap-email-properties.vala
engine/imap/api/imap-folder-properties.vala
engine/imap/api/imap-folder.vala
engine/imap/command/imap-append-command.vala
engine/imap/command/imap-capability-command.vala
engine/imap/command/imap-close-command.vala
engine/imap/command/imap-command.vala
@ -184,6 +186,16 @@ engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala
engine/memory/memory-buffer.vala
engine/memory/memory-byte-buffer.vala
engine/memory/memory-empty-buffer.vala
engine/memory/memory-file-buffer.vala
engine/memory/memory-growable-buffer.vala
engine/memory/memory-string-buffer.vala
engine/memory/memory-unowned-byte-array-buffer.vala
engine/memory/memory-unowned-bytes-buffer.vala
engine/memory/memory-unowned-string-buffer.vala
engine/nonblocking/nonblocking-abstract-semaphore.vala
engine/nonblocking/nonblocking-batch.vala
engine/nonblocking/nonblocking-counting-semaphore.vala
@ -231,8 +243,8 @@ engine/util/util-generic-capabilities.vala
engine/util/util-html.vala
engine/util/util-imap-utf7.vala
engine/util/util-inet.vala
engine/util/util-memory.vala
engine/util/util-numeric.vala
engine/util/util-object.vala
engine/util/util-reference-semantics.vala
engine/util/util-scheduler.vala
engine/util/util-single-item.vala

View file

@ -80,6 +80,10 @@ public class ComposerWindow : Gtk.Window {
</style>
</head><body id="message-body"></body></html>""";
public const string ATTACHMENT_KEYWORDS_GENERIC = ".doc|.pdf|.xls|.ppt|.rtf|.pps";
/// A list of keywords, separated by pipe ("|") characters, that suggest an attachment
public const string ATTACHMENT_KEYWORDS_LOCALIZED = _("attach|enclosed|enclosing|cover letter");
// Signal sent when the "Send" button is clicked.
public signal void send(ComposerWindow composer);
@ -304,7 +308,7 @@ public class ComposerWindow : Gtk.Window {
try {
body_html = referred.get_message().get_body(true);
} catch (Error error) {
debug("Error getting messae body: %s", error.message);
debug("Error getting message body: %s", error.message);
}
add_attachments(referred.attachments);
break;
@ -659,9 +663,56 @@ public class ComposerWindow : Gtk.Window {
on_discard();
}
private bool email_contains_attachment_keywords() {
// Filter out all content contained in block quotes
string filtered = @"$subject\n";
filtered += Util.DOM.get_text_representation(editor.get_dom_document(), "blockquote");
Regex url_regex = null;
try {
// Prepare to ignore urls later
url_regex = new Regex(URL_REGEX, RegexCompileFlags.CASELESS);
} catch (Error error) {
debug("Error building regex in keyword checker: %s", error.message);
}
string[] keys = ATTACHMENT_KEYWORDS_GENERIC.casefold().split("|");
foreach (string key in ATTACHMENT_KEYWORDS_LOCALIZED.casefold().split("|")) {
keys += key;
}
string folded;
foreach (string line in filtered.split("\n")) {
// Stop looking once we hit forwarded content
if (line.has_prefix("--")) {
break;
}
folded = line.casefold();
foreach (string key in keys) {
if (key in folded) {
try {
// Make sure the match isn't coming from a url
if (key in url_regex.replace(folded, -1, 0, "")) {
return true;
}
} catch (Error error) {
debug("Regex replacement error in keyword checker: %s", error.message);
return true;
}
}
}
}
return false;
}
private bool should_send() {
bool has_subject = !Geary.String.is_empty(subject.strip());
bool has_body_or_attachment = !Geary.String.is_empty(get_html()) || attachment_files.size > 0;
bool has_body = !Geary.String.is_empty(get_html());
bool has_attachment = attachment_files.size > 0;
bool has_body_or_attachment = has_body || has_attachment;
string? confirmation = null;
if (!has_subject && !has_body_or_attachment) {
confirmation = _("Send message with an empty subject and body?");
@ -669,6 +720,8 @@ public class ComposerWindow : Gtk.Window {
confirmation = _("Send message with an empty subject?");
} else if (!has_body_or_attachment) {
confirmation = _("Send message with an empty body?");
} else if (!has_attachment && email_contains_attachment_keywords()) {
confirmation = _("Send message without an attachment?");
}
if (confirmation != null) {
ConfirmationDialog dialog = new ConfirmationDialog(this,

View file

@ -8,24 +8,30 @@
public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.InternalDropTargetEntry,
Sidebar.EmphasizableEntry {
private bool has_new;
private int unread_count;
public FolderEntry(Geary.Folder folder) {
base(folder);
has_new = false;
unread_count = 0;
folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_UNDREAD].connect(
on_email_unread_count_changed);
}
~FolderEntry() {
folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_UNDREAD].disconnect(
on_email_unread_count_changed);
}
public override string get_sidebar_name() {
return (unread_count == 0 ? folder.get_display_name() :
return (folder.properties.email_unread == 0 ? folder.get_display_name() :
/// This string gets the folder name and the unread messages count,
/// e.g. All Mail (5).
_("%s (%d)").printf(folder.get_display_name(), unread_count));
_("%s (%d)").printf(folder.get_display_name(), folder.properties.email_unread));
}
public override string? get_sidebar_tooltip() {
return (unread_count == 0 ? null :
ngettext("%d unread message", "%d unread messages", unread_count).printf(unread_count));
return (folder.properties.email_unread == 0 ? null :
ngettext("%d unread message", "%d unread messages", folder.properties.email_unread).
printf(folder.properties.email_unread));
}
public override Icon? get_sidebar_icon() {
@ -81,15 +87,6 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In
is_emphasized_changed(has_new);
}
public void set_unread_count(int unread_count) {
if (this.unread_count == unread_count)
return;
this.unread_count = unread_count;
sidebar_name_changed(get_sidebar_name());
sidebar_tooltip_changed(get_sidebar_tooltip());
}
public bool internal_drop_received(Gdk.DragContext context, Gtk.SelectionData data) {
// Copy or move?
Gdk.ModifierType mask;
@ -104,4 +101,9 @@ public class FolderList.FolderEntry : FolderList.AbstractFolderEntry, Sidebar.In
return true;
}
private void on_email_unread_count_changed() {
sidebar_name_changed(get_sidebar_name());
sidebar_tooltip_changed(get_sidebar_tooltip());
}
}

View file

@ -16,7 +16,10 @@ public class FolderList.InboxFolderEntry : FolderList.FolderEntry {
}
public override string get_sidebar_name() {
return folder.account.information.nickname;
return (folder.properties.email_unread == 0 ? folder.account.information.nickname :
/// This string gets the account nickname and the unread messages count,
/// e.g. Work (5).
_("%s (%d)").printf(folder.account.information.nickname, folder.properties.email_unread));
}
public Geary.AccountInformation get_account_information() {

View file

@ -36,7 +36,7 @@ public class FolderList.SearchEntry : FolderList.AbstractFolderEntry {
}
public override string? get_sidebar_tooltip() {
return _("%d results").printf(folder.get_properties().email_total);
return _("%d results").printf(folder.properties.email_total);
}
public override Icon? get_sidebar_icon() {

View file

@ -122,7 +122,7 @@ public string pretty_print(DateTime datetime, ClockFormat clock_format) {
if (diff < TimeSpan.HOUR) {
return _("%dm ago").printf(diff / TimeSpan.MINUTE);
}
if (diff < 6 * TimeSpan.HOUR) {
if (diff < 12 * TimeSpan.HOUR) {
return _("%dh ago").printf(diff / TimeSpan.HOUR);
}

View file

@ -42,6 +42,69 @@ namespace Util.DOM {
class_list.remove(clas);
}
}
// Returns the text contained in the DOM document, after ignoring tags of type "exclude"
// and padding newlines where appropriate. Used to scan for attachment keywords.
public string get_text_representation(WebKit.DOM.Document doc, string exclude) {
WebKit.DOM.HTMLElement? copy = Util.DOM.clone_node(doc.get_body());
if (copy == null) {
return "";
}
// Keep deleting the next excluded element until there are none left
while (true) {
WebKit.DOM.HTMLElement? current = Util.DOM.select(copy, exclude);
if (current == null) {
break;
}
WebKit.DOM.Node parent = current.get_parent_node();
try {
parent.remove_child(current);
} catch (Error error) {
debug("Error removing blockquotes: %s", error.message);
break;
}
}
WebKit.DOM.NodeList node_list;
try {
node_list = copy.query_selector_all("br");
} catch (Error error) {
debug("Error finding <br>s: %s", error.message);
return copy.get_inner_text();
}
// Replace <br> tags with newlines
for (int i = 0; i < node_list.length; ++i) {
WebKit.DOM.Node br = node_list.item(i);
WebKit.DOM.Node parent = br.get_parent_node();
try {
parent.replace_child(doc.create_text_node("\n"), br);
} catch (Error error) {
debug("Error replacing <br>: %s", error.message);
}
}
try {
node_list = copy.query_selector_all("div");
} catch (Error error) {
debug("Error finding <div>s: %s", error.message);
return copy.get_inner_text();
}
// Pad each <div> with newlines
for (int i = 0; i < node_list.length; ++i) {
WebKit.DOM.Node div = node_list.item(i);
try {
div.insert_before(doc.create_text_node("\n"), div.first_child);
div.append_child(doc.create_text_node("\n"));
} catch (Error error) {
debug("Error padding <div> with newlines: %s", error.message);
}
}
return copy.get_inner_text();
}
}
public void bind_event(WebKit.WebView view, string selector, string event, Callback callback,

View file

@ -219,6 +219,7 @@ public class ConversationViewer : Gtk.Box {
current_conversation.appended.disconnect(on_conversation_appended);
current_conversation.trimmed.disconnect(on_conversation_trimmed);
current_conversation.email_flags_changed.disconnect(update_flags);
current_conversation = null;
}
// Disable message buttons until conversation loads.
@ -226,15 +227,13 @@ public class ConversationViewer : Gtk.Box {
if (conversations == null || conversations.size == 0 || current_folder == null) {
show_multiple_selected(0);
current_conversation = null;
return;
}
// Clear view before we yield, to make sure it happens.
clear(current_folder, current_folder.account.information);
web_view.scroll_reset();
if (conversations.size == 1) {
clear(current_folder, current_folder.account.information);
web_view.scroll_reset();
current_conversation = Geary.Collection.get_first(conversations);
select_conversation_async.begin(current_conversation, current_folder,
@ -256,10 +255,13 @@ public class ConversationViewer : Gtk.Box {
Geary.Folder current_folder) throws Error {
Gee.Collection<Geary.Email> messages = conversation.get_emails(Geary.Conversation.Ordering.DATE_ASCENDING);
// Load this once, so if it's cancelled, we cancel the WHOLE load.
Cancellable cancellable = cancellable_fetch;
// Fetch full messages.
Gee.Collection<Geary.Email> messages_to_add = new Gee.HashSet<Geary.Email>();
foreach (Geary.Email email in messages)
messages_to_add.add(yield fetch_full_message_async(email));
messages_to_add.add(yield fetch_full_message_async(email, cancellable));
// Add messages.
foreach (Geary.Email email in messages_to_add)
@ -316,17 +318,18 @@ public class ConversationViewer : Gtk.Box {
}
// Given an email, fetch the full version with all required fields.
private async Geary.Email fetch_full_message_async(Geary.Email email) throws Error {
private async Geary.Email fetch_full_message_async(Geary.Email email,
Cancellable? cancellable) throws Error {
Geary.Email.Field required_fields = ConversationViewer.REQUIRED_FIELDS |
Geary.ComposedEmail.REQUIRED_REPLY_FIELDS;
Geary.Email full_email;
if (email.id.get_folder_path() == null) {
full_email = yield current_folder.account.local_fetch_email_async(
email.id, required_fields, cancellable_fetch);
email.id, required_fields, cancellable);
} else {
full_email = yield current_folder.fetch_email_async(email.id,
required_fields, Geary.Folder.ListFlags.NONE, cancellable_fetch);
required_fields, Geary.Folder.ListFlags.NONE, cancellable);
}
return full_email;
@ -345,7 +348,7 @@ public class ConversationViewer : Gtk.Box {
}
private async void on_conversation_appended_async(Geary.Email email) throws Error {
add_message(yield fetch_full_message_async(email));
add_message(yield fetch_full_message_async(email, cancellable_fetch));
}
private void on_conversation_appended_complete(Object? source, AsyncResult result) {
@ -1415,9 +1418,8 @@ public class ConversationViewer : Gtk.Box {
continue;
} else if (src.has_prefix("cid:")) {
string mime_id = src.substring(4);
Geary.Memory.AbstractBuffer image_content =
message.get_content_by_mime_id(mime_id);
uint8[] image_data = image_content.get_array();
Geary.Memory.Buffer image_content = message.get_content_by_mime_id(mime_id);
uint8[] image_data = image_content.get_bytes().get_data();
// Get the content type.
bool uncertain_content_type;

View file

@ -83,6 +83,7 @@ class ImapConsole : Gtk.Window {
"capabililties",
"caps",
"connect",
"unsecure",
"disconnect",
"login",
"logout",
@ -94,6 +95,7 @@ class ImapConsole : Gtk.Window {
"fetch",
"uid-fetch",
"fetch-fields",
"append",
"help",
"exit",
"quit",
@ -138,6 +140,7 @@ class ImapConsole : Gtk.Window {
break;
case "connect":
case "unsecure":
connect_cmd(cmd, args);
break;
@ -181,6 +184,10 @@ class ImapConsole : Gtk.Window {
fetch_fields(cmd, args);
break;
case "append":
append(cmd, args);
break;
case "help":
foreach (string cmdname in cmdnames)
print_console_line(cmdname);
@ -285,10 +292,13 @@ class ImapConsole : Gtk.Window {
check_args(cmd, args, 1, "hostname[:port]");
Geary.Endpoint.Flags flags = Geary.Endpoint.Flags.GRACEFUL_DISCONNECT;
if (cmd != "unsecure")
flags |= Geary.Endpoint.Flags.SSL;
cx = new Geary.Imap.ClientConnection(
new Geary.Endpoint(args[0], Geary.Imap.ClientConnection.DEFAULT_PORT_SSL,
Geary.Endpoint.Flags.SSL | Geary.Endpoint.Flags.GRACEFUL_DISCONNECT,
Geary.Imap.ClientConnection.DEFAULT_TIMEOUT_SEC));
flags, Geary.Imap.ClientConnection.DEFAULT_TIMEOUT_SEC));
cx.sent_command.connect(on_sent_command);
cx.received_status_response.connect(on_received_status_response);
@ -462,6 +472,25 @@ class ImapConsole : Gtk.Window {
}
}
private void append(string cmd, string[] args) throws Error {
check_connected(cmd, args, 2, "<mailbox> <filename>");
status("Appending %s to %s".printf(args[1], args[0]));
cx.send_async.begin(new Geary.Imap.AppendCommand(new Geary.Imap.MailboxSpecifier(args[0]),
null, null, new Geary.Memory.FileBuffer(File.new_for_path(args[1]), true)), null,
on_appended);
}
private void on_appended(Object? source, AsyncResult result) {
try {
cx.send_async.end(result);
status("Appended");
} catch (Error err) {
exception(err);
}
}
private void close(string cmd, string[] args) throws Error {
check_connected(cmd, args, 0, null);
@ -601,7 +630,7 @@ class ImapConsole : Gtk.Window {
void main(string[] args) {
Gtk.init(ref args);
Geary.Logging.set_flags(Geary.Logging.Flag.NETWORK);
Geary.Logging.enable_flags(Geary.Logging.Flag.NETWORK);
Geary.Logging.log_to(stdout);
ImapConsole console = new ImapConsole();

View file

@ -50,9 +50,9 @@ public abstract class Geary.AbstractFolder : BaseObject, Geary.Folder {
public abstract Geary.Account account { get; }
public abstract Geary.FolderPath get_path();
public abstract Geary.FolderProperties properties { get; }
public abstract Geary.FolderProperties get_properties();
public abstract Geary.FolderPath get_path();
public abstract Geary.SpecialFolderType get_special_folder_type();

View file

@ -33,7 +33,7 @@ public abstract class Geary.AbstractLocalFolder : Geary.AbstractFolder {
if (open_count++ > 0)
return false;
notify_opened(Geary.Folder.OpenState.LOCAL, get_properties().email_total);
notify_opened(Geary.Folder.OpenState.LOCAL, properties.email_total);
return true;
}

View file

@ -0,0 +1,54 @@
/* 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.
*/
/**
* Aggregates multiple FolderProperties into one. This way a Geary.Folder can
* present one stable FolderProperties object that the client can register
* change listeners on, etc. despite most Geary.Folders having both a local
* and remote version of FolderProperties.
*
* The class relies on GObject bindings and the fact that FolderProperties
* contains only propertiess.
*/
private class Geary.AggregatedFolderProperties : Geary.FolderProperties {
// Map of child FolderProperties to their bindings.
private Gee.Map<FolderProperties, Gee.List<Binding>> child_bindings
= new Gee.HashMap<FolderProperties, Gee.List<Binding>>();
/**
* Creates an aggregate FolderProperties.
*/
public AggregatedFolderProperties() {
// Set defaults.
base(0, 0, Trillian.UNKNOWN, Trillian.UNKNOWN, Trillian.UNKNOWN);
}
/**
* Adds a child FolderProperties. The child's property values will overwrite
* this class's property values.
*/
public void add(FolderProperties child) {
// Create a binding for all properties.
Gee.List<Binding>? bindings = Geary.ObjectUtils.mirror_properties(child, this);
assert(bindings != null);
child_bindings.set(child, bindings);
}
/**
* Removes a child FolderProperties.
*/
public bool remove(FolderProperties child) {
Gee.List<Binding> bindings;
if (child_bindings.unset(child, out bindings)) {
Geary.ObjectUtils.unmirror_properties(bindings);
return true;
}
return false;
}
}

View file

@ -217,7 +217,7 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
* are less-than longer paths, assuming the path elements are equal up to the shorter path's
* length.
*
* Note that the {@ link FolderRoot.default_separator} has no bearing on comparisons, although
* Note that the {@link FolderRoot.default_separator} has no bearing on comparisons, although
* {@link FolderRoot.case_sensitive} does.
*
* Returns -1 if this path is lexiographically before the other, 1 if its after, and 0 if they

View file

@ -5,6 +5,12 @@
*/
public abstract class Geary.FolderProperties : BaseObject {
public const string PROP_NAME_EMAIL_TOTAL = "email-total";
public const string PROP_NAME_EMAIL_UNDREAD = "email-unread";
public const string PROP_NAME_HAS_CHILDREN = "has-children";
public const string PROP_NAME_SUPPORTS_CHILDREN = "supports-children";
public const string PROP_NAME_IS_OPENABLE = "is-openable";
/**
* The total count of email in the Folder.
*/

View file

@ -109,6 +109,8 @@ public interface Geary.Folder : BaseObject {
public abstract Geary.Account account { get; }
public abstract Geary.FolderProperties properties { get; }
/**
* Fired when the folder is successfully opened by a caller.
*
@ -236,19 +238,6 @@ public interface Geary.Folder : BaseObject {
public abstract Geary.FolderPath get_path();
/**
* Returns a FolderProperties that represents, if fully open, accurate values for this Folder,
* and if not, values that represent the last time the Folder was opened or examined by the
* Engine.
*
* The returned object is not guaranteed to be long-lived. If the Folder's state changes, it's
* possible a new FolderProperties will be set in its place. Instead of monitoring the fields
* of the FolderProperties for changes, use Account.folders_contents_changed() to be notified
* of changes and use the (potentially new) FolderProperties returned by this method at that
* point.
*/
public abstract Geary.FolderProperties get_properties();
/**
* Returns the special folder type of the folder.
*/

View file

@ -30,11 +30,12 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder {
public static const int MAX_RESULT_EMAILS = 1000;
public override Account account { get { return _account; } }
public override FolderProperties properties { get { return _properties; } }
private static FolderRoot? path = null;
private weak Account _account;
private SearchFolderProperties properties = new SearchFolderProperties(0, 0);
private SearchFolderProperties _properties = new SearchFolderProperties(0, 0);
private Gee.HashSet<Geary.FolderPath?> exclude_folders = new Gee.HashSet<Geary.FolderPath?>();
private Geary.SpecialFolderType[] exclude_types = {
Geary.SpecialFolderType.SPAM,
@ -169,10 +170,6 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder {
return Geary.SpecialFolderType.SEARCH;
}
public override Geary.FolderProperties get_properties() {
return properties;
}
public override async Gee.List<Geary.Email>? list_email_async(int low, int count,
Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null)
throws Error {

View file

@ -142,15 +142,15 @@ public abstract class Geary.MessageData.Int64MessageData : AbstractMessageData,
public abstract class Geary.MessageData.BlockMessageData : AbstractMessageData {
public string data_name { get; private set; }
public Geary.Memory.AbstractBuffer buffer { get; private set; }
public Geary.Memory.Buffer buffer { get; private set; }
public BlockMessageData(string data_name, Geary.Memory.AbstractBuffer buffer) {
public BlockMessageData(string data_name, Geary.Memory.Buffer buffer) {
this.data_name = data_name;
this.buffer = buffer;
}
public override string to_string() {
return "%s (%lub)".printf(data_name, buffer.get_size());
return "%s (%lub)".printf(data_name, buffer.size);
}
}

View file

@ -124,6 +124,18 @@ public class Geary.Db.Result : Geary.Db.Context {
return s;
}
/**
* column is zero-based.
*/
public Memory.Buffer string_buffer_at(int column) throws DatabaseError {
// Memory.StringBuffer is not entirely suited for this, as it can result in extra copies
// internally ... GrowableBuffer is better for large blocks
Memory.GrowableBuffer buffer = new Memory.GrowableBuffer();
buffer.append(string_at(column).data);
return buffer;
}
private void verify_at(int column) throws DatabaseError {
if (finished)
throw new DatabaseError.FINISHED("Query finished");
@ -204,6 +216,14 @@ public class Geary.Db.Result : Geary.Db.Context {
return string_at(convert_for(name));
}
/**
* name is the name of the column in the result set. See Statement.get_column_index() for name
* matching rules.
*/
public Memory.Buffer string_buffer_for(string name) throws DatabaseError {
return string_buffer_at(convert_for(name));
}
private int convert_for(string name) throws DatabaseError {
if (finished)
throw new DatabaseError.FINISHED("Query finished");

View file

@ -31,6 +31,8 @@ public class Geary.Db.Statement : Geary.Db.Context {
*/
public signal void bindings_cleared();
private Gee.HashSet<Memory.Buffer> held_buffers = new Gee.HashSet<Memory.Buffer>();
internal Statement(Connection connection, string sql) throws DatabaseError {
this.connection = connection;
// save for logging in case prepare_v2() fails
@ -229,6 +231,38 @@ public class Geary.Db.Statement : Geary.Db.Context {
return this;
}
/**
* Binds the string representation of a {@link Memory.Buffer} to the replacement value
* in the {@link Statement}.
*
* If buffer supports {@link Memory.UnownedStringBuffer}, the unowned string will be used
* to avoid a memory copy. However, this means the Statement will hold a reference to the
* buffer until the Statement is destroyed.
*
* index is zero-based.
*/
public Statement bind_string_buffer(int index, Memory.Buffer? buffer) throws DatabaseError {
if (buffer == null)
return bind_string(index, null);
Memory.UnownedStringBuffer? unowned_buffer = buffer as Memory.UnownedStringBuffer;
if (unowned_buffer == null) {
throw_on_error("Statement.bind_string_buffer", stmt.bind_text(index + 1, buffer.to_string()));
return this;
}
// hold on to buffer for lifetime of Statement, SQLite's callback isn't enough for us to
// selectively unref each Buffer as it's done with it
held_buffers.add(unowned_buffer);
// note use of _bind_text, which is for static and other strings with their own memory
// management
stmt._bind_text(index + 1, unowned_buffer.to_unowned_string());
return this;
}
public override Statement? get_statement() {
return this;
}

View file

@ -135,7 +135,7 @@ private class Geary.ImapDB.Account : BaseObject {
// create the folder object
Db.Statement stmt = cx.prepare(
"INSERT INTO FolderTable (name, parent_id, last_seen_total, last_seen_status_total, "
+ "uid_validity, uid_next, attributes) VALUES (?, ?, ?, ?, ?, ?, ?)");
+ "uid_validity, uid_next, attributes, unread_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
stmt.bind_string(0, path.basename);
stmt.bind_rowid(1, parent_id);
stmt.bind_int(2, Numeric.int_floor(properties.select_examine_messages, 0));
@ -145,6 +145,7 @@ private class Geary.ImapDB.Account : BaseObject {
stmt.bind_int64(5, (properties.uid_next != null) ? properties.uid_next.value
: Imap.UID.INVALID);
stmt.bind_string(6, properties.attrs.serialize());
stmt.bind_int(7, properties.email_unread);
stmt.exec(cancellable);
@ -174,15 +175,17 @@ private class Geary.ImapDB.Account : BaseObject {
Db.Statement stmt;
if (parent_id != Db.INVALID_ROWID) {
stmt = cx.prepare(
"UPDATE FolderTable SET attributes=? WHERE parent_id=? AND name=?");
"UPDATE FolderTable SET attributes=?, unread_count=? WHERE parent_id=? AND name=?");
stmt.bind_string(0, properties.attrs.serialize());
stmt.bind_rowid(1, parent_id);
stmt.bind_string(2, path.basename);
stmt.bind_int(1, properties.email_unread);
stmt.bind_rowid(2, parent_id);
stmt.bind_string(3, path.basename);
} else {
stmt = cx.prepare(
"UPDATE FolderTable SET attributes=? WHERE parent_id IS NULL AND name=?");
"UPDATE FolderTable SET attributes=?, unread_count=? WHERE parent_id IS NULL AND name=?");
stmt.bind_string(0, properties.attrs.serialize());
stmt.bind_string(1, path.basename);
stmt.bind_int(1, properties.email_unread);
stmt.bind_string(2, path.basename);
}
stmt.exec();
@ -200,7 +203,7 @@ private class Geary.ImapDB.Account : BaseObject {
if (db_folder != null) {
Imap.FolderProperties local_properties = db_folder.get_properties();
local_properties.unseen = properties.unseen;
local_properties.set_status_unseen(properties.unseen);
local_properties.recent = properties.recent;
local_properties.attrs = properties.attrs;
@ -264,7 +267,7 @@ private class Geary.ImapDB.Account : BaseObject {
if (db_folder != null) {
Imap.FolderProperties local_properties = db_folder.get_properties();
local_properties.unseen = properties.unseen;
local_properties.set_status_unseen(properties.unseen);
local_properties.recent = properties.recent;
local_properties.uid_validity = properties.uid_validity;
local_properties.uid_next = properties.uid_next;
@ -349,7 +352,7 @@ private class Geary.ImapDB.Account : BaseObject {
: new Geary.FolderRoot(basename, "/", Geary.Imap.Folder.CASE_SENSITIVE);
Geary.Imap.FolderProperties properties = new Geary.Imap.FolderProperties(
result.int_for("last_seen_total"), 0, 0,
result.int_for("last_seen_total"), 0,
new Imap.UIDValidity(result.int64_for("uid_validity")),
new Imap.UID(result.int64_for("uid_next")),
Geary.Imap.MailboxAttributes.deserialize(result.string_for("attributes")));
@ -436,7 +439,7 @@ private class Geary.ImapDB.Account : BaseObject {
Db.Result results = stmt.exec(cancellable);
if (!results.finished) {
properties = new Imap.FolderProperties(results.int_for("last_seen_total"), 0, 0,
properties = new Imap.FolderProperties(results.int_for("last_seen_total"), 0,
new Imap.UIDValidity(results.int64_for("uid_validity")),
new Imap.UID(results.int64_for("uid_next")),
Geary.Imap.MailboxAttributes.deserialize(results.string_for("attributes")));

View file

@ -35,11 +35,11 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
post_upgrade_encode_folder_names();
break;
case 10:
case 11:
post_upgrade_add_search_table();
break;
case 11:
case 12:
post_upgrade_populate_internal_date_time_t();
break;
}
@ -87,7 +87,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
}
}
// Version 10.
// Version 11.
private void post_upgrade_add_search_table() {
try {
string stemmer = find_appropriate_search_stemmer();
@ -149,7 +149,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
return "english";
}
// Version 11.
// Version 12.
private void post_upgrade_populate_internal_date_time_t() {
try {
exec_transaction(Db.TransactionType.RW, (cx) => {

View file

@ -920,8 +920,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
stmt.bind_string(10, row.in_reply_to);
stmt.bind_string(11, row.references);
stmt.bind_string(12, row.subject);
stmt.bind_string(13, row.header);
stmt.bind_string(14, row.body);
stmt.bind_string_buffer(13, row.header);
stmt.bind_string_buffer(14, row.body);
stmt.bind_string(15, row.preview);
stmt.bind_string(16, row.email_flags);
stmt.bind_string(17, row.internaldate);
@ -1256,7 +1256,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
if (new_fields.is_any_set(Geary.Email.Field.HEADER)) {
Db.Statement stmt = cx.prepare(
"UPDATE MessageTable SET header=? WHERE id=?");
stmt.bind_string(0, row.header);
stmt.bind_string_buffer(0, row.header);
stmt.bind_rowid(1, row.id);
stmt.exec(cancellable);
@ -1265,7 +1265,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
if (new_fields.is_any_set(Geary.Email.Field.BODY)) {
Db.Statement stmt = cx.prepare(
"UPDATE MessageTable SET body=? WHERE id=?");
stmt.bind_string(0, row.body);
stmt.bind_string_buffer(0, row.body);
stmt.bind_rowid(1, row.id);
stmt.exec(cancellable);

View file

@ -25,9 +25,9 @@ private class Geary.ImapDB.MessageRow {
public string? subject { get; set; default = null; }
public string? header { get; set; default = null; }
public Memory.Buffer? header { get; set; default = null; }
public string? body { get; set; default = null; }
public Memory.Buffer? body { get; set; default = null; }
public string? preview { get; set; default = null; }
@ -79,10 +79,10 @@ private class Geary.ImapDB.MessageRow {
subject = results.string_for("subject");
if (fields.is_all_set(Geary.Email.Field.HEADER))
header = results.string_for("header");
header = results.string_buffer_for("header");
if (fields.is_all_set(Geary.Email.Field.BODY))
body = results.string_for("body");
body = results.string_buffer_for("body");
if (fields.is_all_set(Geary.Email.Field.PREVIEW))
preview = results.string_for("preview");
@ -127,10 +127,10 @@ private class Geary.ImapDB.MessageRow {
email.set_message_subject(new RFC822.Subject.decode(subject ?? ""));
if (fields.is_all_set(Geary.Email.Field.HEADER))
email.set_message_header(new RFC822.Header(new Geary.Memory.StringBuffer(header ?? "")));
email.set_message_header(new RFC822.Header(header ?? Memory.EmptyBuffer.instance));
if (fields.is_all_set(Geary.Email.Field.BODY))
email.set_message_body(new RFC822.Text(new Geary.Memory.StringBuffer(body ?? "")));
email.set_message_body(new RFC822.Text(body ?? Memory.EmptyBuffer.instance));
if (fields.is_all_set(Geary.Email.Field.PREVIEW))
email.set_message_preview(new RFC822.PreviewText(new Geary.Memory.StringBuffer(preview ?? "")));
@ -217,13 +217,13 @@ private class Geary.ImapDB.MessageRow {
}
if (email.fields.is_all_set(Geary.Email.Field.HEADER)) {
header = (email.header != null) ? email.header.buffer.to_string() : null;
header = (email.header != null) ? email.header.buffer : null;
fields = fields.set(Geary.Email.Field.HEADER);
}
if (email.fields.is_all_set(Geary.Email.Field.BODY)) {
body = (email.body != null) ? email.body.buffer.to_string() : null;
body = (email.body != null) ? email.body.buffer : null;
fields = fields.set(Geary.Email.Field.BODY);
}

View file

@ -17,10 +17,10 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
public int64 id;
public int position;
public int64 ordering;
public string? message;
public Memory.Buffer? message;
public SmtpOutboxEmailIdentifier outbox_id;
public OutboxRow(int64 id, int position, int64 ordering, string? message) {
public OutboxRow(int64 id, int position, int64 ordering, Memory.Buffer? message) {
assert(position >= 1);
this.id = id;
@ -44,10 +44,12 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
private weak Account _account;
private Geary.Smtp.ClientSession smtp;
private Nonblocking.Mailbox<OutboxRow> outbox_queue = new Nonblocking.Mailbox<OutboxRow>();
private SmtpOutboxFolderProperties properties = new SmtpOutboxFolderProperties(0, 0);
private SmtpOutboxFolderProperties _properties = new SmtpOutboxFolderProperties(0, 0);
public override Account account { get { return _account; } }
public override FolderProperties properties { get { return _properties; } }
// Requires the Database from the get-go because it runs a background task that access it
// whether open or not
public SmtpOutboxFolder(ImapDB.Database db, Account account) {
@ -81,7 +83,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
int position = 1;
while (!results.finished) {
list.add(new OutboxRow(results.rowid_at(0), position++, results.int64_at(1),
results.string_at(2)));
results.string_buffer_at(2)));
results.next(cancellable);
}
@ -90,7 +92,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
if (list.size > 0) {
// set properties now (can't do yield in ctor)
properties.set_total(list.size);
_properties.set_total(list.size);
debug("Priming outbox postman with %d stored messages", list.size);
foreach (OutboxRow row in list)
@ -115,7 +117,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
// Convert row into RFC822 message suitable for sending or framing
RFC822.Message message;
try {
message = new RFC822.Message.from_string(row.message);
message = new RFC822.Message.from_buffer(row.message);
} catch (RFC822Error msg_err) {
// TODO: This needs to be reported to the user
debug("Outbox postman message error: %s", msg_err.message);
@ -177,7 +179,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
// update properties
try {
properties.set_total(yield get_email_count_async(null));
_properties.set_total(yield get_email_count_async(null));
} catch (Error err) {
debug("Outbox postman: Unable to fetch updated email count for properties: %s",
err.message);
@ -197,10 +199,6 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
return path;
}
public override Geary.FolderProperties get_properties() {
return properties;
}
public override Geary.SpecialFolderType get_special_folder_type() {
return Geary.SpecialFolderType.OUTBOX;
}
@ -243,7 +241,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
assert(!results.finished);
int64 ordering = results.int64_at(0);
string message = results.string_at(1);
Memory.Buffer message = results.string_buffer_at(1);
int position = do_get_position_by_ordering(cx, ordering, cancellable);
@ -257,7 +255,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
assert(row != null);
// update properties
properties.set_total(yield get_email_count_async(cancellable));
_properties.set_total(yield get_email_count_async(cancellable));
// immediately add to outbox queue for delivery
outbox_queue.send(row);
@ -309,7 +307,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
int position = low;
do {
list.add(row_to_email(new OutboxRow(results.rowid_at(0), position++, results.int64_at(1),
results.string_at(2))));
results.string_buffer_at(2))));
} while (results.next());
return Db.TransactionOutcome.DONE;
@ -354,7 +352,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
}
list.add(row_to_email(new OutboxRow(results.rowid_at(0), position++, ordering,
results.string_at(2))));
results.string_buffer_at(2))));
} while (results.next());
return Db.TransactionOutcome.DONE;
@ -489,7 +487,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
// Utility for getting an email object back from an outbox row.
private Geary.Email row_to_email(OutboxRow row) throws Error {
RFC822.Message message = new RFC822.Message.from_string(row.message);
RFC822.Message message = new RFC822.Message.from_buffer(row.message);
Geary.Email email = message.get_email(row.position, row.outbox_id);
// TODO: Determine message's total size (header + body) to store in Properties.
@ -574,7 +572,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
if (position < 1)
return null;
return new OutboxRow(results.rowid_at(0), position, ordering, results.string_at(1));
return new OutboxRow(results.rowid_at(0), position, ordering, results.string_buffer_at(1));
}
private bool do_remove_email(Db.Connection cx, SmtpOutboxEmailIdentifier id, Cancellable? cancellable)

View file

@ -227,7 +227,7 @@ private class Geary.ImapEngine.AccountSynchronizer : Geary.BaseObject {
debug("Oldest local email in %s not old enough (%s vs. %s), synchronizing...",
folder.to_string(), oldest_local.to_string(), epoch.to_string());
}
} else if (folder.get_properties().email_total == 0) {
} else if (folder.properties.email_total == 0) {
// no local messages, no remote messages -- this is as good as having everything up
// to the epoch
return true;

View file

@ -15,12 +15,14 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
Geary.Email.Field.PROPERTIES | ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION;
public override Account account { get { return _account; } }
public override FolderProperties properties { get { return _properties; } }
internal ImapDB.Folder local_folder { get; protected set; }
internal Imap.Folder? remote_folder { get; protected set; default = null; }
internal EmailPrefetcher email_prefetcher { get; private set; }
internal EmailFlagWatcher email_flag_watcher;
private weak GenericAccount _account;
private Geary.AggregatedFolderProperties _properties = new Geary.AggregatedFolderProperties();
private Imap.Account remote;
private ImapDB.Account local;
private SpecialFolderType special_folder_type;
@ -37,6 +39,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
this.local = local;
this.local_folder = local_folder;
this.special_folder_type = special_folder_type;
_properties.add(local_folder.get_properties());
email_flag_watcher = new EmailFlagWatcher(this);
email_flag_watcher.email_flags_changed.connect(on_email_flags_changed);
@ -53,16 +56,6 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
return local_folder.get_path();
}
public override Geary.FolderProperties get_properties() {
// Get properties in order of authoritativeness:
// - From open remote folder
// - Fetch from local store
if (remote_folder != null && get_open_state() == OpenState.BOTH)
return remote_folder.properties;
return local_folder.get_properties();
}
public override Geary.SpecialFolderType get_special_folder_type() {
return special_folder_type;
}
@ -380,7 +373,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
CreateLocalEmailOperation? create_op = null;
if (to_create_or_merge.size > 0) {
create_op = new CreateLocalEmailOperation(local_folder, to_create_or_merge,
NORMALIZATION_FIELDS);
normalization_fields);
batch.add(create_op);
}
@ -581,6 +574,8 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
return;
}
_properties.add(remote_folder.properties);
// notify any subscribers with similar information
notify_opened(
(remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL,
@ -591,6 +586,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
if (open_count == 0 || --open_count > 0)
return;
_properties.remove(remote_folder.properties);
yield close_internal_async(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, cancellable);
}

View file

@ -45,26 +45,33 @@ public class Geary.Imap.FolderProperties : Geary.FolderProperties {
*/
public int select_examine_messages { get; private set; }
/**
* -1 if the FolderProperties were not obtained via a STATUS command
* -1 if the FolderProperties were not obtained or updated via a STATUS command
*/
public int status_messages { get; private set; }
public int unseen { get; internal set; }
/**
* -1 if the FolderProperties were not obtained or updated via a STATUS command
*/
public int unseen { get; private set; }
public int recent { get; internal set; }
public UIDValidity? uid_validity { get; internal set; }
public UID? uid_next { get; internal set; }
public MailboxAttributes attrs { get; internal set; }
// Note that unseen from SELECT/EXAMINE is the *position* of the first unseen message,
// not the total unseen count, so it should not be passed in here, but rather the unseen
// count from a STATUS command
public FolderProperties(int messages, int recent, int unseen, UIDValidity? uid_validity,
/**
* Note that unseen from SELECT/EXAMINE is the *position* of the first unseen message,
* not the total unseen count, so it's not be passed in here, but rather only from the unseen
* count from a STATUS command
*/
public FolderProperties(int messages, int recent, UIDValidity? uid_validity,
UID? uid_next, MailboxAttributes attrs) {
base (messages, unseen, Trillian.UNKNOWN, Trillian.UNKNOWN, Trillian.UNKNOWN);
// give the base class a zero email_unread, as the notion of "unknown" doesn't exist in
// its contract
base (messages, 0, Trillian.UNKNOWN, Trillian.UNKNOWN, Trillian.UNKNOWN);
select_examine_messages = messages;
status_messages = -1;
this.recent = recent;
this.unseen = unseen;
this.unseen = -1;
this.uid_validity = uid_validity;
this.uid_next = uid_next;
this.attrs = attrs;
@ -159,5 +166,16 @@ public class Geary.Imap.FolderProperties : Geary.FolderProperties {
// select/examine more authoritative than status
email_total = messages;
}
public void set_status_unseen(int count) {
// drop unknown counts, especially if known is held here
if (count < 0)
return;
unseen = count;
// update base class value (which clients see)
email_unread = count;
}
}

View file

@ -63,7 +63,7 @@ private class Geary.Imap.Folder : BaseObject {
this.info = info;
path = info.mailbox.to_folder_path(info.delim);
properties = new Imap.FolderProperties(0, 0, 0, null, null, info.attrs);
properties = new Imap.FolderProperties(0, 0, null, null, info.attrs);
}
public async void open_async(Cancellable? cancellable) throws Error {
@ -206,7 +206,9 @@ private class Geary.Imap.Folder : BaseObject {
break;
case ResponseCodeType.UNSEEN:
properties.unseen = response_code.get_unseen();
// do NOT update properties.unseen, as the UNSEEN response code (here) means
// the sequence number of the first unseen message, not the total count of
// unseen messages
break;
case ResponseCodeType.PERMANENT_FLAGS:

View file

@ -0,0 +1,31 @@
/* 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 IMAP APPEND command.
*
* See [[http://tools.ietf.org/html/rfc3501#section-6.3.11]]
*/
public class Geary.Imap.AppendCommand : Command {
public const string NAME = "append";
public AppendCommand(MailboxSpecifier mailbox, MessageFlags? flags, InternalDate? internal_date,
Memory.Buffer message) {
base (NAME);
add(mailbox.to_parameter());
if (flags != null && flags.size > 0)
add(flags.to_parameter());
if (internal_date != null)
add(internal_date.to_parameter());
add(new LiteralParameter(message));
}
}

View file

@ -79,7 +79,7 @@ public class Geary.Imap.Command : RootParameters {
if (stringp != null)
add(stringp);
else
error("Command continuations currently unsupported");
add(new LiteralParameter(new Memory.StringBuffer(arg)));
}
}
}
@ -110,10 +110,11 @@ public class Geary.Imap.Command : RootParameters {
return this.name.down() == name.down();
}
public override async void serialize(Serializer ser) throws Error {
public override void serialize(Serializer ser, Tag tag) throws Error {
assert(tag.is_assigned());
yield base.serialize(ser);
base.serialize(ser, tag);
ser.push_end_of_message();
}
}

View file

@ -32,6 +32,13 @@ public abstract class Geary.Imap.Flag : BaseObject, Gee.Hashable<Geary.Imap.Flag
return (flag == this) ? true : flag.equals_string(value);
}
/**
* Returns the {@link Flag} as an appropriate {@link Parameter}.
*/
public Parameter to_parameter() {
return StringParameter.get_best_for(value);
}
public uint hash() {
return str_hash(value.down());
}

View file

@ -35,6 +35,19 @@ public abstract class Geary.Imap.Flags : Geary.MessageData.AbstractMessageData,
return to_string();
}
/**
* Returns a {@link ListParameter} representation of these flags.
*
* If empty, this returns an empty ListParameter.
*/
public virtual Parameter to_parameter() {
ListParameter listp = new ListParameter(null);
foreach (Flag flag in list)
listp.add(flag.to_parameter());
return listp;
}
public bool equal_to(Geary.Imap.Flags other) {
if (this == other)
return true;

View file

@ -46,7 +46,7 @@ public class Geary.Imap.MailboxParameter : StringParameter {
/**
* {@inheritDoc}
*/
public override async void serialize(Serializer ser) throws Error {
public override void serialize(Serializer ser, Tag tag) throws Error {
serialize_string(ser);
}

View file

@ -19,10 +19,23 @@
public interface Geary.Imap.MessageData : Geary.MessageData.AbstractMessageData {
}
/**
* A representations of IMAP's INTERNALDATE field.
*
* See [[http://tools.ietf.org/html/rfc3501#section-2.3.3]]
*/
public class Geary.Imap.InternalDate : Geary.RFC822.Date, Geary.Imap.MessageData {
public InternalDate(string iso8601) throws ImapError {
base (iso8601);
}
/**
* Returns the {@link InternalDate} as a {@link Parameter}.
*/
public Parameter to_parameter() {
return StringParameter.get_best_for(serialize());
}
}
public class Geary.Imap.RFC822Size : Geary.RFC822.Size, Geary.Imap.MessageData {
@ -67,19 +80,19 @@ public class Geary.Imap.Envelope : Geary.MessageData.AbstractMessageData, Geary.
}
public class Geary.Imap.RFC822Header : Geary.RFC822.Header, Geary.Imap.MessageData {
public RFC822Header(Geary.Memory.AbstractBuffer buffer) {
public RFC822Header(Memory.Buffer buffer) {
base (buffer);
}
}
public class Geary.Imap.RFC822Text : Geary.RFC822.Text, Geary.Imap.MessageData {
public RFC822Text(Geary.Memory.AbstractBuffer buffer) {
public RFC822Text(Memory.Buffer buffer) {
base (buffer);
}
}
public class Geary.Imap.RFC822Full : Geary.RFC822.Full, Geary.Imap.MessageData {
public RFC822Full(Geary.Memory.AbstractBuffer buffer) {
public RFC822Full(Memory.Buffer buffer) {
base (buffer);
}
}

View file

@ -25,7 +25,7 @@ public class Geary.Imap.AtomParameter : Geary.Imap.UnquotedStringParameter {
/**
* {@inheritDoc}
*/
public override async void serialize(Serializer ser) throws Error {
public override void serialize(Serializer ser, Tag tag) throws Error {
ser.push_unquoted_string(value);
}
}

View file

@ -85,7 +85,7 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
}
/**
* Returns {@link Paramater} at index if in range and of Type type, otherwise throws an
* Returns {@link Parameter} at index if in range and of Type type, otherwise throws an
* {@link ImapError.TYPE_ERROR}.
*
* type must be of type Parameter.
@ -154,7 +154,7 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
/**
* Returns a {@link StringParameter} only if the {@link Parameter} at index is a StringParameter.
*
* Compare to {@link get_if_string_or_literal}.
* Compare to {@link get_as_nullable_string}.
*/
public StringParameter? get_if_string(int index) {
return (StringParameter?) get_if(index, typeof(StringParameter));
@ -307,12 +307,12 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
}
/**
* Returns a {@link Memory.AbstractBuffer} for the {@link Parameter} at position index.
* Returns a {@link Memory.Buffer} for the {@link Parameter} at position index.
*
* Only converts {@link StringParameter} and {@link LiteralParameter}. All other types return
* null.
*/
public Memory.AbstractBuffer? get_as_nullable_buffer(int index) throws ImapError {
public Memory.Buffer? get_as_nullable_buffer(int index) throws ImapError {
LiteralParameter? literalp = get_if_literal(index);
if (literalp != null)
return literalp.get_buffer();
@ -325,12 +325,12 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
}
/**
* Returns a {@link Memory.AbstractBuffer} for the {@link Parameter} at position index.
* Returns a {@link Memory.Buffer} for the {@link Parameter} at position index.
*
* Only converts {@link StringParameter} and {@link LiteralParameter}. All other types return
* as an empty buffer.
*/
public Memory.AbstractBuffer get_as_empty_buffer(int index) throws ImapError {
public Memory.Buffer get_as_empty_buffer(int index) throws ImapError {
return get_as_nullable_buffer(index) ?? Memory.EmptyBuffer.instance;
}
@ -395,10 +395,10 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
return "(%s)".printf(stringize_list());
}
protected async void serialize_list(Serializer ser) throws Error {
protected void serialize_list(Serializer ser, Tag tag) throws Error {
int length = list.size;
for (int ctr = 0; ctr < length; ctr++) {
yield list[ctr].serialize(ser);
list[ctr].serialize(ser, tag);
if (ctr < (length - 1))
ser.push_space();
}
@ -407,9 +407,9 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
/**
* {@inheritDoc}
*/
public override async void serialize(Serializer ser) throws Error {
public override void serialize(Serializer ser, Tag tag) throws Error {
ser.push_ascii('(');
yield serialize_list(ser);
serialize_list(ser, tag);
ser.push_ascii(')');
}
}

View file

@ -15,9 +15,9 @@
*/
public class Geary.Imap.LiteralParameter : Geary.Imap.Parameter {
private Geary.Memory.AbstractBuffer buffer;
private Memory.Buffer buffer;
public LiteralParameter(Geary.Memory.AbstractBuffer buffer) {
public LiteralParameter(Memory.Buffer buffer) {
this.buffer = buffer;
}
@ -25,13 +25,13 @@ public class Geary.Imap.LiteralParameter : Geary.Imap.Parameter {
* Returns the number of bytes in the literal parameter's buffer.
*/
public size_t get_size() {
return buffer.get_size();
return buffer.size;
}
/**
* Returns the literal paremeter's buffer.
*/
public Geary.Memory.AbstractBuffer get_buffer() {
public Memory.Buffer get_buffer() {
return buffer;
}
@ -45,7 +45,7 @@ public class Geary.Imap.LiteralParameter : Geary.Imap.Parameter {
* for transmitting on the wire.
*/
public StringParameter coerce_to_string_parameter() {
return new UnquotedStringParameter(buffer.to_valid_utf8());
return new UnquotedStringParameter(buffer.get_valid_utf8());
}
/**
@ -58,10 +58,10 @@ public class Geary.Imap.LiteralParameter : Geary.Imap.Parameter {
/**
* {@inheritDoc}
*/
public override async void serialize(Serializer ser) throws Error {
public override void serialize(Serializer ser, Tag tag) throws Error {
ser.push_unquoted_string("{%lu}".printf(get_size()));
ser.push_eol();
yield ser.push_input_stream_literal_data_async(buffer.get_input_stream());
ser.push_synchronized_literal_data(tag, buffer);
}
}

View file

@ -47,7 +47,7 @@ public class Geary.Imap.NilParameter : Geary.Imap.Parameter {
/**
* {@inheritDoc}
*/
public override async void serialize(Serializer ser) throws Error {
public override void serialize(Serializer ser, Tag tag) throws Error {
ser.push_nil();
}

View file

@ -15,8 +15,12 @@
public abstract class Geary.Imap.Parameter : BaseObject {
/**
* Invoked when the {@link Parameter} is to be serialized out to the network.
*
* The supplied Tag will have (or will be) assigned to the message, so it should be passed
* to all serialize() calls this call may make. The {@link Parameter} should not use its own
* internal Tag object, if it has a reference to one.
*/
public abstract async void serialize(Serializer ser) throws Error;
public abstract void serialize(Serializer ser, Tag tag) throws Error;
/**
* Returns a representation of the {@link Parameter} suitable for logging and debugging,

View file

@ -30,7 +30,7 @@ public class Geary.Imap.QuotedStringParameter : Geary.Imap.StringParameter {
/**
* {@inheritDoc}
*/
public override async void serialize(Serializer ser) throws Error {
public override void serialize(Serializer ser, Tag tag) throws Error {
ser.push_quoted_string(value);
}
}

View file

@ -66,8 +66,8 @@ public class Geary.Imap.RootParameters : Geary.Imap.ListParameter {
/**
* {@inheritDoc}
*/
public override async void serialize(Serializer ser) throws Error {
yield serialize_list(ser);
public override void serialize(Serializer ser, Tag tag) throws Error {
serialize_list(ser, tag);
ser.push_eol();
}
}

View file

@ -26,7 +26,7 @@ public abstract class Geary.Imap.StringParameter : Geary.Imap.Parameter {
public string value { get; private set; }
/**
* Returns {@link} value or null if value is empty (zero-length).
* Returns {@link value} or null if value is empty (zero-length).
*/
public string? nullable_value {
get {

View file

@ -25,7 +25,7 @@ public class Geary.Imap.UnquotedStringParameter : Geary.Imap.StringParameter {
/**
* {@inheritDoc}
*/
public override async void serialize(Serializer ser) throws Error {
public override void serialize(Serializer ser, Tag tag) throws Error {
ser.push_unquoted_string(value);
}

View file

@ -34,8 +34,8 @@ public class Geary.Imap.FetchedData : Object {
* now, these buffers are indexed with {@link FetchBodyDataIdentifier}s. This means the results
* can only be accessed against the original request's identifier.
*/
public Gee.Map<FetchBodyDataIdentifier, Memory.AbstractBuffer> body_data_map { get; private set;
default = new Gee.HashMap<FetchBodyDataIdentifier, Memory.AbstractBuffer>(); }
public Gee.Map<FetchBodyDataIdentifier, Memory.Buffer> body_data_map { get; private set;
default = new Gee.HashMap<FetchBodyDataIdentifier, Memory.Buffer>(); }
public FetchedData(SequenceNumber seq_num) {
this.seq_num = seq_num;
@ -113,9 +113,9 @@ public class Geary.Imap.FetchedData : Object {
FetchedData combined = new FetchedData(seq_num);
Collection.map_set_all<FetchDataType, MessageData>(combined.data_map, data_map);
Collection.map_set_all<FetchDataType, MessageData>(combined.data_map, other.data_map);
Collection.map_set_all<FetchBodyDataIdentifier, Memory.AbstractBuffer>(combined.body_data_map,
Collection.map_set_all<FetchBodyDataIdentifier, Memory.Buffer>(combined.body_data_map,
body_data_map);
Collection.map_set_all<FetchBodyDataIdentifier, Memory.AbstractBuffer>(combined.body_data_map,
Collection.map_set_all<FetchBodyDataIdentifier, Memory.Buffer>(combined.body_data_map,
other.body_data_map);
return combined;
@ -130,7 +130,7 @@ public class Geary.Imap.FetchedData : Object {
builder.append_printf("%s=%s ", data_type.to_string(), data_map.get(data_type).to_string());
foreach (FetchBodyDataIdentifier identifier in body_data_map.keys)
builder.append_printf("%s=%lu ", identifier.to_string(), body_data_map.get(identifier).get_size());
builder.append_printf("%s=%lu ", identifier.to_string(), body_data_map.get(identifier).size);
return builder.str;
}

View file

@ -94,9 +94,9 @@ public class Geary.Imap.ResponseCode : Geary.Imap.ListParameter {
return "[%s]".printf(stringize_list());
}
public override async void serialize(Serializer ser) throws Error {
public override void serialize(Serializer ser, Tag tag) throws Error {
ser.push_ascii('[');
yield serialize_list(ser);
serialize_list(ser, tag);
ser.push_ascii(']');
}
}

View file

@ -38,6 +38,8 @@ public class Geary.Imap.ClientConnection : BaseObject {
IDLING,
IDLE,
DEIDLING,
DEIDLING_SYNCHRONIZING,
SYNCHRONIZING,
DISCONNECTED,
COUNT
@ -56,6 +58,9 @@ public class Geary.Imap.ClientConnection : BaseObject {
SEND,
SEND_IDLE,
// To initiate a command continuation request
SYNCHRONIZE,
// RECVD_* will emit appropriate signals inside their transition handlers; do *not* use
// issue_conditional_event() for these events
RECVD_STATUS_RESPONSE,
@ -99,11 +104,14 @@ public class Geary.Imap.ClientConnection : BaseObject {
private Serializer? ser = null;
private Deserializer? des = null;
private Geary.Nonblocking.Mutex send_mutex = new Geary.Nonblocking.Mutex();
private Geary.Nonblocking.Spinlock synchronized_notifier = new Geary.Nonblocking.Spinlock();
private int tag_counter = 0;
private char tag_prefix = 'a';
private uint flush_timeout_id = 0;
private bool idle_when_quiet = false;
private Gee.HashSet<Tag> posted_idle_tags = new Gee.HashSet<Tag>();
private Tag? posted_synchronization_tag = null;
private StatusResponse? synchronization_status_response = null;
private uint timeout_id = 0;
private uint timeout_cmd_count = 0;
@ -173,10 +181,11 @@ public class Geary.Imap.ClientConnection : BaseObject {
Geary.State.Mapping[] mappings = {
new Geary.State.Mapping(State.UNCONNECTED, Event.CONNECTED, on_connected),
new Geary.State.Mapping(State.UNCONNECTED, Event.DISCONNECTED, Geary.State.nop),
new Geary.State.Mapping(State.UNCONNECTED, Event.DISCONNECTED, on_disconnected),
new Geary.State.Mapping(State.CONNECTED, Event.SEND, on_proceed),
new Geary.State.Mapping(State.CONNECTED, Event.SEND_IDLE, on_send_idle),
new Geary.State.Mapping(State.CONNECTED, Event.SYNCHRONIZE, on_synchronize),
new Geary.State.Mapping(State.CONNECTED, Event.RECVD_STATUS_RESPONSE, on_status_response),
new Geary.State.Mapping(State.CONNECTED, Event.RECVD_SERVER_DATA, on_server_data),
new Geary.State.Mapping(State.CONNECTED, Event.RECVD_CONTINUATION_RESPONSE, on_continuation),
@ -198,11 +207,30 @@ public class Geary.Imap.ClientConnection : BaseObject {
new Geary.State.Mapping(State.DEIDLING, Event.SEND, on_proceed),
new Geary.State.Mapping(State.DEIDLING, Event.SEND_IDLE, on_send_idle),
new Geary.State.Mapping(State.DEIDLING, Event.SYNCHRONIZE, on_deidling_synchronize),
new Geary.State.Mapping(State.DEIDLING, Event.RECVD_STATUS_RESPONSE, on_idle_status_response),
new Geary.State.Mapping(State.DEIDLING, Event.RECVD_SERVER_DATA, on_server_data),
new Geary.State.Mapping(State.DEIDLING, Event.RECVD_CONTINUATION_RESPONSE, on_idling_continuation),
new Geary.State.Mapping(State.DEIDLING, Event.DISCONNECTED, on_disconnected),
new Geary.State.Mapping(State.DEIDLING_SYNCHRONIZING, Event.SEND, on_no_proceed),
new Geary.State.Mapping(State.DEIDLING_SYNCHRONIZING, Event.SEND_IDLE, on_no_proceed),
new Geary.State.Mapping(State.DEIDLING_SYNCHRONIZING, Event.RECVD_STATUS_RESPONSE,
on_deidling_synchronizing_status_response),
new Geary.State.Mapping(State.DEIDLING_SYNCHRONIZING, Event.RECVD_SERVER_DATA, on_server_data),
new Geary.State.Mapping(State.DEIDLING_SYNCHRONIZING, Event.RECVD_CONTINUATION_RESPONSE,
on_synchronize_continuation),
new Geary.State.Mapping(State.DEIDLING_SYNCHRONIZING, Event.DISCONNECTED, on_disconnected),
new Geary.State.Mapping(State.SYNCHRONIZING, Event.SEND, on_no_proceed),
new Geary.State.Mapping(State.SYNCHRONIZING, Event.SEND_IDLE, on_no_proceed),
new Geary.State.Mapping(State.SYNCHRONIZING, Event.RECVD_STATUS_RESPONSE,
on_synchronize_status_response),
new Geary.State.Mapping(State.SYNCHRONIZING, Event.RECVD_SERVER_DATA, on_server_data),
new Geary.State.Mapping(State.SYNCHRONIZING, Event.RECVD_CONTINUATION_RESPONSE,
on_synchronize_continuation),
new Geary.State.Mapping(State.SYNCHRONIZING, Event.DISCONNECTED, on_disconnected),
// TODO: A DISCONNECTING state would be helpful here, allowing for responses and data
// received from the server after a send error caused a disconnect to be signalled to
// subscribers before moving to the DISCONNECTED state. That would require more work,
@ -210,6 +238,7 @@ public class Geary.Imap.ClientConnection : BaseObject {
// everything to flush out before it shifted to a DISCONNECTED state as well.
new Geary.State.Mapping(State.DISCONNECTED, Event.SEND, on_no_proceed),
new Geary.State.Mapping(State.DISCONNECTED, Event.SEND_IDLE, on_no_proceed),
new Geary.State.Mapping(State.DISCONNECTED, Event.SYNCHRONIZE, on_no_proceed),
new Geary.State.Mapping(State.DISCONNECTED, Event.RECVD_STATUS_RESPONSE, Geary.State.nop),
new Geary.State.Mapping(State.DISCONNECTED, Event.RECVD_SERVER_DATA, Geary.State.nop),
new Geary.State.Mapping(State.DISCONNECTED, Event.RECVD_CONTINUATION_RESPONSE, Geary.State.nop),
@ -388,9 +417,8 @@ public class Geary.Imap.ClientConnection : BaseObject {
assert(ser == null);
assert(des == null);
// not buffering the Serializer because it buffers using a MemoryOutputStream and not
// buffering the Deserializer because it uses a DataInputStream, which is buffered
ser = new Serializer(to_string(), ios.output_stream);
// Not buffering the Deserializer because it uses a DataInputStream, which is buffered
ser = new Serializer(to_string(), new BufferedOutputStream(ios.output_stream));
des = new Deserializer(to_string(), ios.input_stream);
des.parameters_ready.connect(on_parameters_ready);
@ -415,7 +443,6 @@ public class Geary.Imap.ClientConnection : BaseObject {
yield des.stop_async();
}
// TODO: May need to commit Serializer before disconnecting
ser = null;
des = null;
}
@ -500,7 +527,6 @@ public class Geary.Imap.ClientConnection : BaseObject {
recv_closed();
}
// TODO: Guard against reentrancy
public async void send_async(Command cmd, Cancellable? cancellable = null) throws Error {
check_for_connection();
@ -511,8 +537,8 @@ public class Geary.Imap.ClientConnection : BaseObject {
fsm.get_state_string(fsm.get_state()));
}
// need to run this in critical section because OutputStreams can only be written to
// serially
// need to run this in critical section because Serializer requires it (don't want to be
// pushing data while a flush_async() is occurring)
int token = yield send_mutex.claim_async(cancellable);
// Always assign a new tag; Commands with pre-assigned Tags should not be re-sent.
@ -528,9 +554,7 @@ public class Geary.Imap.ClientConnection : BaseObject {
try {
// watch for disconnect while waiting for mutex
if (ser != null) {
// TODO: Make serialize non-blocking; this would also remove the need for a send_mutex
// (although reentrancy should still be checked for)
yield cmd.serialize(ser);
cmd.serialize(ser, cmd.tag);
} else {
ser_err = new ImapError.NOT_CONNECTED("Send not allowed: connection in %s state",
fsm.get_state_string(fsm.get_state()));
@ -582,7 +606,7 @@ public class Geary.Imap.ClientConnection : BaseObject {
// need to signal when the IDLE command is sent, for completeness
IdleCommand? idle_cmd = null;
// Like send_async(), need to use mutex when flushing as OutputStream must be accessed in
// Like send_async(), need to use mutex when flushing as Serializer must be accessed in
// serialized fashion
//
// NOTE: Because this is happening in the background, it's possible for ser to go to null
@ -595,8 +619,53 @@ public class Geary.Imap.ClientConnection : BaseObject {
// Dovecot will hang the connection (not send any replies) if IDLE is sent in the
// same buffer as normal commands, so flush the buffer first, enqueue IDLE, and
// flush that behind the first
if (ser != null)
yield ser.flush_async();
bool is_synchronized = false;
while (ser != null) {
// prepare for upcoming synchronization point (continuation response could be
// recv'd before flush_async() completes) and reset prior synchronization response
posted_synchronization_tag = ser.next_synchronized_message();
synchronization_status_response = null;
Tag? synchronize_tag;
yield ser.flush_async(is_synchronized, out synchronize_tag);
// if no tag returned, all done, otherwise synchronization required
if (synchronize_tag == null)
break;
// no longer synchronized
is_synchronized = false;
// synchronization is not always possible
if (!issue_conditional_event(Event.SYNCHRONIZE)) {
debug("[%s] Unable to synchronize, exiting do_flush_async", to_string());
return;
}
// wait for synchronization point to be reached
debug("[%s] Synchronizing...", to_string());
yield synchronized_notifier.wait_async();
// watch for the synchronization request to be thwarted
if (synchronization_status_response != null) {
debug("[%s]: Failed to synchronize command continuation: %s", to_string(),
synchronization_status_response.to_string());
// skip pass current message, this one's done
if (ser != null)
ser.fast_forward_queue();
} else {
debug("[%s] Synchronized, ready to continue", to_string());
// now synchronized, ready to continue
is_synchronized = true;
}
}
// reset synchronization state
posted_synchronization_tag = null;
synchronization_status_response = null;
// as connection is "quiet" (haven't seen new command in n msec), go into IDLE state
// if (a) allowed by owner and (b) allowed by state machine
@ -611,11 +680,14 @@ public class Geary.Imap.ClientConnection : BaseObject {
Logging.debug(Logging.Flag.NETWORK, "[%s] Initiating IDLE: %s", to_string(),
idle_cmd.to_string());
yield idle_cmd.serialize(ser);
}
idle_cmd.serialize(ser, idle_cmd.tag);
if (ser != null)
yield ser.flush_async();
Tag? synchronize_tag;
yield ser.flush_async(false, out synchronize_tag);
// flushing IDLE should never require synchronization
assert(synchronize_tag == null);
}
} catch (Error err) {
idle_cmd = null;
send_failure(err);
@ -779,6 +851,14 @@ public class Geary.Imap.ClientConnection : BaseObject {
return do_proceed(State.IDLING, user);
}
private uint on_synchronize(uint state, uint event, void *user) {
return do_proceed(State.SYNCHRONIZING, user);
}
private uint on_deidling_synchronize(uint state, uint event, void *user) {
return do_proceed(State.DEIDLING_SYNCHRONIZING, user);
}
private uint on_status_response(uint state, uint event, void *user, Object? object) {
fsm.do_post_transition(signal_status_response, user, object);
@ -812,7 +892,7 @@ public class Geary.Imap.ClientConnection : BaseObject {
private uint on_idle_send(uint state, uint event, void *user) {
Logging.debug(Logging.Flag.NETWORK, "[%s] Closing IDLE", to_string());
// TODO: Because there is not DISCONNECTING state, need to watch for the Serializer
// TODO: Because there is no DISCONNECTING state, need to watch for the Serializer
// disappearing during a disconnect while in a "normal" state
if (ser == null) {
debug("[%s] Unable to close IDLE: no serializer", to_string());
@ -880,6 +960,55 @@ public class Geary.Imap.ClientConnection : BaseObject {
return state;
}
private uint on_deidling_synchronizing_status_response(uint state, uint event, void *user,
Object? object) {
// piggyback on on_idle_status_response, but instead of jumping to CONNECTED, jump to
// SYNCHRONIZING (because IDLE has completed)
return (on_idle_status_response(state, event, user, object) == State.CONNECTED)
? State.SYNCHRONIZING : state;
}
private uint on_synchronize_status_response(uint state, uint event, void *user, Object? object) {
StatusResponse status_response = (StatusResponse) object;
// waiting for status response to synchronization message, treat others normally
if (posted_synchronization_tag == null || !posted_synchronization_tag.equal_to(status_response.tag)) {
fsm.do_post_transition(signal_status_response, user, object);
return state;
}
// receive status response while waiting for synchronization of a command; this means the
// server has rejected it
debug("[%s] Command continuation rejected: %s", to_string(), status_response.to_string());
// save result and notify sleeping flush_async()
synchronization_status_response = status_response;
synchronized_notifier.blind_notify();
return State.CONNECTED;
}
private uint on_synchronize_continuation(uint state, uint event, void *user, Object? object) {
ContinuationResponse continuation = (ContinuationResponse) object;
if (posted_synchronization_tag == null) {
debug("[%s] Bad command continuation received: %s", to_string(),
continuation.to_string());
} else {
debug("[%s] Command continuation received for %s: %s", to_string(),
posted_synchronization_tag.to_string(), continuation.to_string());
}
// wake up the sleeping flush_async() call so it will continue
synchronization_status_response = null;
synchronized_notifier.blind_notify();
// There is no SYNCHRONIZED state, which is kind of fleeting; the moment the flush_async()
// call continues, no longer synchronized
return State.CONNECTED;
}
private uint on_bad_transition(uint state, uint event, void *user) {
debug("[%s] Bad cx state transition %s", to_string(), fsm.get_event_issued_string(state, event));

View file

@ -18,6 +18,9 @@
* that since Deserializer uses async I/O, this isn't technically possible unless the signals are
* connected after the Idle loop has a chance to run; however, this is an implementation detail and
* shouldn't be relied upon.)
*
* Internally Deserializer uses a DataInputStream to help decode the data. Since DataInputStream
* is buffered, there's no need to buffer the InputStream passed to Deserializer's constructor.
*/
public class Geary.Imap.Deserializer : BaseObject {
@ -320,7 +323,7 @@ public class Geary.Imap.Deserializer : BaseObject {
bytes_received(bytes_read);
// adjust the current buffer's size to the amount that was actually read in
block_buffer.adjust(current_buffer, bytes_read);
block_buffer.trim(current_buffer, bytes_read);
push_data(bytes_read);
} catch (Error err) {

View file

@ -5,34 +5,54 @@
*/
/**
* The Serializer asynchronously writes serialized IMAP commands to the supplied output stream.
* Serializer asynchronously writes serialized IMAP commands to the supplied output stream via a
* queue of buffers.
*
* Since most IMAP commands are small in size (one line of data, often under 64 bytes), the
* Serializer writes them to a temporary buffer, only writing to the actual stream when literal data
* is written (which can often be large and coming off of disk) or commit_async() is called, which
* should be invoked when convenient, to prevent the buffer from growing too large.
* Serializer writes them to a queue of temporary buffers (interspersed with user-supplied buffers
* that are intended to be literal data). The data is only written when {@link flush_async} is
* invoked.
*
* Because of this situation, the serialized commands will not necessarily reach the output stream
* unless commit_async() is called, which pushes the in-memory bytes to it. Since the
* output stream itself may be buffered, flush_async() should be called to verify the bytes have
* reached the wire.
* This means that if the caller wants some buffer beyond the steps described above, they should
* pass in a BufferedOutputStream (or one of its subclasses). flush_async() will flush the user's
* OutputStream after writing to it.
*
* flush_async() implies commit_async(), but the reverse is not true.
* Command continuation requires some synchronization between the Serializer and the
* {@link Deserializer}. It also requires some queue management. See {@link fast_forward_queue}
* and {@link next_synchronized_message}.
*
* @see Deserializer
*/
public class Geary.Imap.Serializer : BaseObject {
private class SerializedData {
public Memory.Buffer buffer;
public Tag? literal_data_tag;
public SerializedData(Memory.Buffer buffer, Tag? literal_data_tag) {
this.buffer = buffer;
this.literal_data_tag = literal_data_tag;
}
}
private string identifier;
private OutputStream outs;
private ConverterOutputStream couts;
private MemoryOutputStream mouts;
private DataOutputStream douts;
private Geary.Stream.MidstreamConverter midstream = new Geary.Stream.MidstreamConverter("Serializer");
private Gee.Queue<SerializedData?> datastream = new Gee.LinkedList<SerializedData?>();
public Serializer(string identifier, OutputStream outs) {
this.identifier = identifier;
this.outs = outs;
// prepare the ConverterOutputStream (which wraps the caller's OutputStream and allows for
// midstream conversion)
couts = new ConverterOutputStream(outs, midstream);
couts.set_close_base_stream(false);
// prepare the DataOutputStream (which generates buffers for the queue)
mouts = new MemoryOutputStream(null, realloc, free);
douts = new DataOutputStream(mouts);
douts.set_close_base_stream(false);
@ -78,49 +98,135 @@ public class Geary.Imap.Serializer : BaseObject {
douts.put_string("\r\n", null);
}
public async void push_input_stream_literal_data_async(InputStream ins,
int priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null) throws Error {
// commit the in-memory buffer to the output stream
yield commit_async(priority, cancellable);
// splice the literal data directly to the output stream
yield couts.splice_async(ins, OutputStreamSpliceFlags.NONE, priority, cancellable);
}
// commit_async() takes the stored (in-memory) serialized data and writes it asynchronously
// to the wrapped OutputStream. Note that this is *not* a flush, as it's possible the
// serialized data will be stored in a buffer in the OutputStream. Use flush_async() to force
// data onto the wire.
public async void commit_async(int priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null)
throws Error {
private void enqueue_current_stream() throws IOError {
size_t length = mouts.get_data_size();
if (length == 0)
if (length <= 0)
return;
if (Logging.are_all_flags_set(Logging.Flag.SERIALIZER)) {
StringBuilder builder = new StringBuilder();
for (size_t ctr = 0; ctr < length; ctr++)
builder.append_c((char) mouts.get_data()[ctr]);
// close before converting to Memory.ByteBuffer
mouts.close();
Logging.debug(Logging.Flag.SERIALIZER, "[%s] send %s", to_string(), builder.str.strip());
}
ssize_t index = 0;
do {
index += yield couts.write_async(mouts.get_data()[index:length], priority, cancellable);
} while (index < length);
SerializedData data = new SerializedData(
new Memory.ByteBuffer.from_memory_output_stream(mouts), null);
datastream.add(data);
mouts = new MemoryOutputStream(null, realloc, free);
douts = new DataOutputStream(mouts);
douts.set_close_base_stream(false);
}
// This pushes all serialized data onto the wire. This calls commit_async() before
// flushing.
public async void flush_async(int priority = GLib.Priority.DEFAULT, Cancellable? cancellable = null)
throws Error {
yield commit_async(priority, cancellable);
yield couts.flush_async(priority, cancellable);
yield outs.flush_async(priority, cancellable);
/*
* Pushes an {link Memory.Buffer} to the serialized stream that must be synchronized
* with the server before transmission.
*
* Literal data may require synchronization with the server and so should only be used when
* necessary. See {link DataFormat.is_quoting_required} to test data.
*
* The supplied buffer must not be mutated once submitted to the {@link Serializer}.
*
* See [[http://tools.ietf.org/html/rfc3501#section-4.3]] and
* [[http://tools.ietf.org/html/rfc3501#section-7.5]]
*/
public void push_synchronized_literal_data(Tag tag, Memory.Buffer buffer) throws Error {
enqueue_current_stream();
datastream.add(new SerializedData(buffer, tag));
}
/**
* Indicates that a complete message has been pushed to the {@link Serializer}.
*
* It's important to delineate messages for the Serializer, as it aids in queue management
* and command continuation (synchronization).
*/
public void push_end_of_message() throws Error {
enqueue_current_stream();
datastream.add(null);
}
/**
* Returns the {@link Tag} for the message with the next synchronization message Tag.
*
* This can be used to prepare for receiving a command continuation failure before sending
* the request via {@link flush_async}, as the response could return before that call completes.
*/
public Tag? next_synchronized_message() {
foreach (SerializedData? data in datastream) {
if (data != null && data.literal_data_tag != null)
return data.literal_data_tag;
}
return null;
}
/**
* Discards all buffers associated with the current message and moves the queue forward to the
* next one.
*
* This is useful when a command continuation is refused by the server and the command must be
* aborted.
*
* Any data currently in the buffer is *not* enqueued, as by definition it has not been marked
* with {@link push_end_of_message}.
*/
public void fast_forward_queue() {
while (!datastream.is_empty) {
if (datastream.poll() == null)
break;
}
}
/**
* Push all serialized data and buffers onto the wire.
*
* Caller should pass is_synchronized=true if the connection has been synchronized for a command
* continuation.
*
* If synchronize_tag returns non-null, then the flush has not completed. The connection must
* wait for the server to send a continuation response before continuing. When ready, call
* flush_async() again with is_synchronized set to true. The tag is supplied to watch for
* an error condition from the server (which may reject the synchronization request).
*/
public async void flush_async(bool is_synchronized, out Tag? synchronize_tag,
Cancellable? cancellable = null) throws Error {
synchronize_tag = null;
// commit the last buffer to the queue (although this is best done with push_end_message)
enqueue_current_stream();
// walk the SerializedData queue, pushing each out to the wire unless a synchronization
// point is encountered
while (!datastream.is_empty) {
// see if next data buffer is synchronized
SerializedData? data = datastream.peek();
if (data != null && data.literal_data_tag != null && !is_synchronized) {
// report the Tag that is associated with the continuation
synchronize_tag = data.literal_data_tag;
// break out to ensure pipe is flushed
break;
}
// if not, remove and process
data = datastream.poll();
if (data == null) {
// end of message, move on
continue;
}
Logging.debug(Logging.Flag.SERIALIZER, "[%s] %s", to_string(), data.buffer.to_string());
// splice buffer's InputStream directly into OutputStream
yield couts.splice_async(data.buffer.get_input_stream(), OutputStreamSpliceFlags.NONE,
Priority.DEFAULT, cancellable);
// if synchronized before, not any more
is_synchronized = false;
}
// make sure everything is flushed out now ... some trouble with BufferedOutputStreams
// here, so flush ConverterOutputStream and its base stream
yield couts.flush_async(Priority.DEFAULT, cancellable);
yield couts.base_stream.flush_async(Priority.DEFAULT, cancellable);
}
public string to_string() {

View file

@ -0,0 +1,119 @@
/* Copyright 2011-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.
*/
/**
* Represents an interface to a variety of backing buffers.
*
* A Buffer may be an in-memory or on-disk block of bytes. Buffer allows for a
* uniform interface to these blocks and makes it easy to move them around and avoiding copies.
*
* Questions of mutability are left to the implementation and users of Buffer. In general,
* AbstractBuffers should be built and modified before allowing other callers to access it.
*
* @see ByteBuffer
* @see EmptyBuffer
* @see GrowableBuffer
* @see StringBuffer
* @see UnownedStringBuffer
* @see UnownedBytesBuffer
*/
public abstract class Geary.Memory.Buffer : BaseObject {
/**
* Returns the number of valid (usable) bytes in the buffer.
*/
public abstract size_t size { get; }
/**
* Returns the number of bytes allocated (usable and unusable) for the buffer.
*/
public abstract size_t allocated_size { get; }
/**
* Returns a Bytes object holding the buffer's contents.
*
* Since Bytes is immutable, the caller will need to make its own copy if it wants to modify
* the data.
*/
public abstract Bytes get_bytes();
/**
* Returns an InputStream that can read the buffer in its current entirety.
*
* Note that the InputStream may share its memory buffer(s) with the Buffer but does
* not hold references to them or the Buffer itself. Thus, the Buffer should
* only be destroyed after all InputStreams are destroyed or exhausted.
*
* The base class implementation uses {@link get_bytes} to create the InputStream. Subclasses
* should look for more optimal implementations.
*/
public virtual InputStream get_input_stream() {
return new MemoryInputStream.from_bytes(get_bytes());
}
/**
* Returns a ByteArray storing the buffer in its entirety.
*
* A copy of the backing buffer is returned.
*
* The base class implementation uses {@link get_bytes} to create the InputStream. Subclasses
* should look for more optimal implementations.
*/
public virtual ByteArray get_byte_array() {
ByteArray byte_array = new ByteArray();
byte_array.append(get_bytes().get_data());
return byte_array;
}
/**
* Returns an array of uint8 storing the buffer in its entirety.
*
* A copy of the backing buffer is returned.
*
* The base class implementation uses {@link get_bytes} to create the InputStream. Subclasses
* should look for more optimal implementations.
*
* @see UnownedBytesBuffer
*/
public virtual uint8[] get_uint8_array() {
return get_bytes().get_data();
}
/**
* Returns a copy of the contents of the buffer as though it was a null terminated string.
*
* The base class implementation uses {@link get_bytes} to create the InputStream. Subclasses
* should look for more optimal implementations.
*
* No validation is made on the string. See {@link get_valid_utf8}.
*
* @see UnownedStringBuffer
*/
public virtual string to_string() {
uint8[] buffer = get_uint8_array();
buffer += (uint8) '\0';
return (string) buffer;
}
/**
* Returns a copy of the contents of the buffer as though it was a UTF-8 string.
*
* The base class implementation uses {@link get_bytes} to create the InputStream. Subclasses
* should look for more optimal implementations.
*
* If the conversion fails or decodes as invalid UTF-8, an empty string is returned.
*
* @see UnownedStringBuffer.get_unowned_valid_utf8
*/
public virtual string get_valid_utf8() {
string str = to_string();
return str.validate() ? str : "";
}
}

View file

@ -0,0 +1,98 @@
/* Copyright 2011-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.
*/
/**
* Allows for a plain block of bytes to be represented as an {@link Buffer}.
*/
public class Geary.Memory.ByteBuffer : Memory.Buffer, Memory.UnownedBytesBuffer {
/**
* {@inheritDoc}
*/
public override size_t size {
get {
return bytes.length;
}
}
/**
* {@inheritDoc}
*/
public override size_t allocated_size {
get {
return allocated_bytes;
}
}
private Bytes bytes;
private size_t allocated_bytes;
/**
* filled is the number of usable bytes in the supplied buffer, allocated is the total size
* of the buffer.
*
* filled must be less than or equal to the allocated size of the buffer.
*
* A copy of the data buffer is made. See {@link ByteBuffer.ByteBuffer.take} for a no-copy
* alternative.
*/
public ByteBuffer(uint8[] data, size_t filled) {
assert(filled <= data.length);
bytes = new Bytes(data[0:filled]);
allocated_bytes = bytes.length;
}
/**
* filled is the number of usable bytes in the supplied buffer, allocated is the total size
* of the buffer.
*
* filled must be less than or equal to the allocated size of the buffer.
*/
public ByteBuffer.take(owned uint8[] data, size_t filled) {
assert(filled <= data.length);
bytes = new Bytes.take(data[0:filled]);
allocated_bytes = data.length;
}
/**
* Takes ownership and converts a ByteArray to a {@link ByteBuffer}.
*
* The ByteArray is freed after this call and should not be used.
*/
public ByteBuffer.from_byte_array(ByteArray byte_array) {
bytes = ByteArray.free_to_bytes(byte_array);
allocated_bytes = bytes.length;
}
/**
* Takes ownership and converts a MemoryOutputStream to a {@link ByteBuffer}.
*
* The MemoryOutputStream ''must'' be closed before this call.
*/
public ByteBuffer.from_memory_output_stream(MemoryOutputStream mouts) {
assert(mouts.is_closed());
bytes = mouts.steal_as_bytes();
allocated_bytes = bytes.length;
}
/**
* {@inheritDoc}
*/
public override Bytes get_bytes() {
return bytes;
}
/**
* {@inheritDoc}
*/
public unowned uint8[] to_unowned_uint8_array() {
return bytes.get_data();
}
}

View file

@ -0,0 +1,74 @@
/* Copyright 2011-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.
*/
/**
* An EmptyBuffer fulfills the interface of {@link Buffer} for a zero-length block.
*
* Because all EmptyBuffers are the same and immutable, only a single may be used: {@link instance}.
*/
public class Geary.Memory.EmptyBuffer : Memory.Buffer, Memory.UnownedStringBuffer,
Memory.UnownedBytesBuffer, Memory.UnownedByteArrayBuffer {
private static EmptyBuffer? _instance = null;
public static EmptyBuffer instance {
get {
return (_instance != null) ? _instance : _instance = new EmptyBuffer();
}
}
/**
* {@inheritDoc}
*/
public override size_t size {
get {
return 0;
}
}
/**
* {@inheritDoc}
*/
public override size_t allocated_size {
get {
return 0;
}
}
private Bytes bytes = new Bytes(new uint8[0]);
private ByteArray byte_array = new ByteArray();
private EmptyBuffer() {
}
/**
* {@inheritDoc}
*/
public override Bytes get_bytes() {
return bytes;
}
/**
* {@inheritDoc}
*/
public unowned uint8[] to_unowned_uint8_array() {
return bytes.get_data();
}
/**
* {@inheritDoc}
*/
public unowned string to_unowned_string() {
return "";
}
/**
* {@inheritDoc}
*/
public unowned ByteArray to_unowned_byte_array() {
return byte_array;
}
}

View file

@ -0,0 +1,51 @@
/* 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.
*/
//extern Bytes *g_bytes_new_with_free_func(void *data, size_t size, DestroyNotify destroy, void *user);
/**
* Makes a file available as a {@link Memory.Buffer}.
*/
public class Geary.Memory.FileBuffer : Memory.Buffer, Memory.UnownedBytesBuffer {
private File file;
private MappedFile mmap;
public override size_t size {
get {
return mmap.get_length();
}
}
public override size_t allocated_size {
get {
return mmap.get_length();
}
}
/**
* The File is immediately opened when this is called.
*/
public FileBuffer(File file, bool readonly) throws Error {
if (file.get_path() == null)
throw new IOError.NOT_FOUND("File for Geary.Memory.FileBuffer not found");
this.file = file;
mmap = new MappedFile(file.get_path(), !readonly);
}
public override Bytes get_bytes() {
return Bytes.new_with_owner(to_unowned_uint8_array(), mmap);
}
public unowned uint8[] to_unowned_uint8_array() {
unowned uint8[] buffer = (uint8[]) mmap.get_contents();
buffer.length = (int) mmap.get_length();
return buffer;
}
}

View file

@ -0,0 +1,237 @@
/* Copyright 2011-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.
*/
/**
* An {@link Buffer} that can be grown by appending additional buffer fragments.
*
* A buffer can be grown by appending data to it ({@link append}), or by allocating additional space
* in the internal buffer ({@link allocate}) which can then be trimmed back if not entirely used
* ({@link trim}).
*/
public class Geary.Memory.GrowableBuffer : Memory.Buffer, Memory.UnownedBytesBuffer,
Memory.UnownedStringBuffer {
private static uint8[] NUL_ARRAY = { '\0' };
private ByteArray? byte_array = new ByteArray();
private Bytes? bytes = null;
public override size_t size {
// account for trailing NUL, which is always kept in place for UnownedStringBuffer
get {
if (bytes != null)
return bytes.length - 1;
assert(byte_array != null);
return byte_array.len - 1;
}
}
public override size_t allocated_size {
get {
return size;
}
}
public GrowableBuffer() {
// add NUL for UnownedStringBuffer
byte_array.append(NUL_ARRAY);
}
private Bytes to_bytes() {
if (bytes != null) {
assert(byte_array == null);
return bytes;
}
assert(byte_array != null);
bytes = ByteArray.free_to_bytes(byte_array);
byte_array = null;
return bytes;
}
private unowned uint8[] get_bytes_no_nul() {
assert(bytes != null);
assert(bytes.get_size() > 0);
return bytes.get_data()[0:bytes.get_size() - 1];
}
private ByteArray to_byte_array() {
if (byte_array != null) {
assert(bytes == null);
return byte_array;
}
assert(bytes != null);
byte_array = Bytes.unref_to_array(bytes);
bytes = null;
return byte_array;
}
private unowned uint8[] get_byte_array_no_nul() {
assert(byte_array != null);
assert(byte_array.len > 0);
return byte_array.data[0:byte_array.len - 1];
}
/**
* Appends the data to the existing GrowableBuffer.
*
* It's unwise to append to a GrowableBuffer while outstanding ByteArrays and InputStreams
* (from {@link get_byte_array} or {@link Buffer.get_input_stream}) are outstanding.
*/
public void append(uint8[] buffer) {
if (buffer.length <= 0)
return;
to_byte_array();
// account for existing NUL
assert(byte_array.len > 0);
byte_array.set_size(byte_array.len - 1);
// append buffer and new NUL for UnownedStringBuffer
byte_array.append(buffer);
byte_array.append(NUL_ARRAY);
}
/**
* Allocate data within the backing buffer for writing.
*
* Any usused bytes in the returned buffer should be returned to the {@link GrowableBuffer}
* via {@link trim}.
*
* It's unwise to write to a GrowableBuffer while outstanding ByteArrays and InputStreams
* (from {@link get_byte_array} or {@link Buffer.get_input_stream}) are outstanding. Likewise,
* it's dangerous to be writing to a GrowableBuffer and in the process call get_bytes() and
* such.
*/
public unowned uint8[] allocate(size_t requested_bytes) {
to_byte_array();
// existing NUL must be there already
assert(byte_array.len > 0);
uint original_bytes = byte_array.len;
uint new_size = original_bytes + (uint) requested_bytes;
byte_array.set_size(new_size);
byte_array.data[new_size - 1] = String.EOS;
// only return portion request, not including new NUL, but overwriting existing NUL
unowned uint8[] buffer = byte_array.data[(original_bytes - 1):(new_size - 1)];
assert(buffer.length == requested_bytes);
return buffer;
}
/**
* Trim a previously allocated buffer.
*
* {@link allocate} returns an internal buffer that may be used for writing. If the entire
* buffer is not filled, it should be trimmed with this call.
*
* filled_bytes is the number of bytes in the supplied buffer that are used (filled). The
* remainder are to be trimmed.
*
* trim() can only trim back the last allocation; there is no facility for having multiple
* outstanding allocations and trimming each back randomly.
*
* WARNING: No other call to the GrowableBuffer should be made between allocate() and trim().
* Requesting {@link get_bytes} and other calls may shift the buffer in memory.
*/
public void trim(uint8[] allocation, size_t filled_bytes) {
// TODO: pointer arithmetic to verify that this allocation actually belongs to the
// ByteArray
assert(byte_array != null);
assert(filled_bytes <= allocation.length);
// don't need to worry about the NUL byte here (unless caller overran buffer, then we
// have bigger problems)
byte_array.set_size(byte_array.len - (uint) (allocation.length - filled_bytes));
}
/**
* {@inheritDoc}
*/
public override Bytes get_bytes() {
to_bytes();
assert(bytes.get_size() > 0);
// don't return trailing nul
return new Bytes.from_bytes(bytes, 0, bytes.get_size() - 1);
}
/**
* {@inheritDoc}
*/
public override ByteArray get_byte_array() {
ByteArray copy = new ByteArray();
// don't copy trailing NUL
if (bytes != null) {
copy.append(get_bytes_no_nul());
} else {
assert(byte_array != null);
copy.append(get_byte_array_no_nul());
}
return copy;
}
/**
* {@inheritDoc}
*/
public override uint8[] get_uint8_array() {
// because returned array is not unowned, Vala will make a copy
return to_unowned_uint8_array();
}
/**
* {@inheritDoc}
*/
public unowned uint8[] to_unowned_uint8_array() {
// in any case, don't return trailing NUL
if (bytes != null)
return get_bytes_no_nul();
assert(byte_array != null);
return get_byte_array_no_nul();
}
/**
* {@inheritDoc}
*/
public override string to_string() {
// because returned string is not unowned, Vala will make a copy
return to_unowned_string();
}
/**
* {@inheritDoc}
*/
public unowned string to_unowned_string() {
// because of how append() and allocate() ensure a trailing NUL, can convert data to a
// string without copy-and-append
if (bytes != null)
return (string) bytes.get_data();
assert(byte_array != null);
return (string) byte_array.data;
}
}

View file

@ -0,0 +1,69 @@
/* Copyright 2011-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.
*/
/**
* Allows for a common string to be represented as an {@link Memory.Buffer}.
*/
public class Geary.Memory.StringBuffer : Memory.Buffer, Memory.UnownedStringBuffer,
Memory.UnownedBytesBuffer {
public override size_t size {
get {
return length;
}
}
public override size_t allocated_size {
get {
return length;
}
}
private string str;
private size_t length;
private Bytes? bytes = null;
public StringBuffer(string str) {
this.str = str;
length = str.data.length;
}
/**
* {@inheritDoc}
*/
public override Bytes get_bytes() {
return (bytes != null) ? bytes : bytes = new Bytes(str.data);
}
/**
* {@inheritDoc}
*/
public override string to_string() {
return str;
}
/**
* {@inheritDoc}
*/
public override string get_valid_utf8() {
return str.validate() ? str : "";
}
/**
* {@inheritDoc}
*/
public unowned string to_unowned_string() {
return str;
}
/**
* {@inheritDoc}
*/
public unowned uint8[] to_unowned_uint8_array() {
return str.data;
}
}

View file

@ -0,0 +1,25 @@
/* 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.
*/
/**
* Interface which allows access to a backing ByteArray of {@link Memory.Buffer} without
* transferring ownership, i.e. copying the ByteArray.
*
* The presence of this interface indicates that obtaining access to the backing buffer is cheap.
*
* Not all AbstractBuffers can support this call, but if they can, this interface allows for it.
* The ByteArray that is returned should ''not'' be modified or freed by the caller.
*/
public interface Geary.Memory.UnownedByteArrayBuffer : Memory.Buffer {
/**
* Returns an unowned pointer to the backing ByteArray.
*
* The returned ByteArray should not be modified or freed.
*/
public abstract unowned ByteArray to_unowned_byte_array();
}

View file

@ -0,0 +1,27 @@
/* 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.
*/
/**
* Interface which allows access to backing data of {@link Memory.Buffer} without transferring
* ownership, i.e. copying the buffer.
*
* The presence of this interface indicates that obtaining access to the backing buffer is cheap
* (i.e. get_bytes().get_data() will do the same thing, but generating a Bytes object might have
* a cost).
*
* Not all AbstractBuffers can support this call, but if they can, this interface allows for it.
* The buffers that are returned should ''not'' be modified or freed by the caller.
*/
public interface Geary.Memory.UnownedBytesBuffer : Memory.Buffer {
/**
* Returns an unowned pointer of the backing buffer.
*
* The returned array should not be modified or freed.
*/
public abstract unowned uint8[] to_unowned_uint8_array();
}

View file

@ -0,0 +1,34 @@
/* 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.
*/
/**
* Interface which allows access to backing string of {@link Memory.Buffer} without transferring
* ownership, i.e. copying the string.
*
* The presence of this interface indicates that obtaining access to the backing string is cheap.
*
* Not all AbstractBuffers can support this call, but if they can, this interface allows for it.
* The buffers that are returned should ''not'' be modified or freed by the caller.
*/
public interface Geary.Memory.UnownedStringBuffer : Memory.Buffer {
/**
* Returns an unowned string version of the backing buffer.
*
* The returned string should not be modified or freed.
*/
public abstract unowned string to_unowned_string();
/**
* An unowned version of {@link Memory.Buffer.get_valid_utf8}.
*/
public virtual unowned string get_unowned_valid_utf8() {
string str = to_unowned_string();
return str.validate() ? str : "";
}
}

View file

@ -119,6 +119,13 @@ public class Geary.RFC822.Date : Geary.RFC822.MessageData, Geary.MessageData.Abs
return value.equal(other.value);
}
/**
* Returns the {@link Date} in ISO-8601 format.
*/
public virtual string serialize() {
return GMime.utils_header_format_date(as_time_t, 0);
}
public virtual uint hash() {
return value.hash();
}
@ -181,7 +188,7 @@ public class Geary.RFC822.Header : Geary.MessageData.BlockMessageData, Geary.RFC
private GMime.Message? message = null;
private string[]? names = null;
public Header(Geary.Memory.AbstractBuffer buffer) {
public Header(Memory.Buffer buffer) {
base ("RFC822.Header", buffer);
}
@ -189,8 +196,7 @@ public class Geary.RFC822.Header : Geary.MessageData.BlockMessageData, Geary.RFC
if (message != null)
return message.get_header_list();
GMime.Parser parser = new GMime.Parser.with_stream(
new GMime.StreamMem.with_buffer(buffer.get_array()));
GMime.Parser parser = new GMime.Parser.with_stream(Utils.create_stream_mem(buffer));
parser.set_respect_content_length(false);
parser.set_scan_from(false);
@ -224,32 +230,30 @@ public class Geary.RFC822.Header : Geary.MessageData.BlockMessageData, Geary.RFC
}
public class Geary.RFC822.Text : Geary.MessageData.BlockMessageData, Geary.RFC822.MessageData {
public Text(Geary.Memory.AbstractBuffer buffer) {
public Text(Memory.Buffer buffer) {
base ("RFC822.Text", buffer);
}
}
public class Geary.RFC822.Full : Geary.MessageData.BlockMessageData, Geary.RFC822.MessageData {
public Full(Geary.Memory.AbstractBuffer buffer) {
public Full(Memory.Buffer buffer) {
base ("RFC822.Full", buffer);
}
}
// Used for decoding preview text.
public class Geary.RFC822.PreviewText : Geary.RFC822.Text {
public PreviewText(Geary.Memory.AbstractBuffer _buffer) {
public PreviewText(Memory.Buffer _buffer) {
base (_buffer);
}
public PreviewText.with_header(Geary.Memory.AbstractBuffer buffer, Geary.Memory.AbstractBuffer
preview_header) {
public PreviewText.with_header(Memory.Buffer preview, Memory.Buffer preview_header) {
string? charset = null;
string? encoding = null;
bool is_html = false;
// Parse the header.
GMime.Stream header_stream = new GMime.StreamMem.with_buffer(
preview_header.get_array());
GMime.Stream header_stream = Utils.create_stream_mem(preview_header);
GMime.Parser parser = new GMime.Parser.with_stream(header_stream);
GMime.Part? part = parser.construct_part() as GMime.Part;
if (part != null) {
@ -259,8 +263,7 @@ public class Geary.RFC822.PreviewText : Geary.RFC822.Text {
encoding = part.get_header("Content-Transfer-Encoding");
}
GMime.StreamMem input_stream = new GMime.StreamMem.with_buffer(buffer.get_array());
GMime.StreamMem input_stream = Utils.create_stream_mem(preview);
ByteArray output = new ByteArray();
GMime.StreamMem output_stream = new GMime.StreamMem.with_byte_array(output);
output_stream.set_owner(false);

View file

@ -27,8 +27,7 @@ public class Geary.RFC822.Message : BaseObject {
private GMime.Message message;
public Message(Full full) throws RFC822Error {
GMime.Parser parser = new GMime.Parser.with_stream(
new GMime.StreamMem.with_buffer(full.buffer.get_array()));
GMime.Parser parser = new GMime.Parser.with_stream(Utils.create_stream_mem(full.buffer));
message = parser.construct_message();
if (message == null)
@ -42,8 +41,8 @@ public class Geary.RFC822.Message : BaseObject {
stock_from_gmime();
}
public Message.from_string(string full_email) throws RFC822Error {
this(new Geary.RFC822.Full(new Geary.Memory.StringBuffer(full_email)));
public Message.from_buffer(Memory.Buffer full_email) throws RFC822Error {
this(new Geary.RFC822.Full(full_email));
}
public Message.from_parts(Header header, Text body) throws RFC822Error {
@ -52,12 +51,12 @@ public class Geary.RFC822.Message : BaseObject {
// http://redmine.yorba.org/issues/7034
// and
// https://bugzilla.gnome.org/show_bug.cgi?id=701572
//
// TODO: When fixed in GMime, return to original behavior of streaming each buffer in
uint8[] buffer = new uint8[header.buffer.get_size() + body.buffer.get_size()];
//
// TODO: When fixed in GMime, return to original behavior of streaming each buffer in
uint8[] buffer = new uint8[header.buffer.size + body.buffer.size];
uint8* ptr = buffer;
GLib.Memory.copy(ptr, header.buffer.get_array(), header.buffer.get_size());
GLib.Memory.copy(ptr + header.buffer.get_size(), body.buffer.get_array(), body.buffer.get_size());
GLib.Memory.copy(ptr, header.buffer.get_bytes().get_data(), header.buffer.size);
GLib.Memory.copy(ptr + header.buffer.size, body.buffer.get_bytes().get_data(), body.buffer.size);
GMime.Parser parser = new GMime.Parser.with_stream(new GMime.StreamMem.with_buffer(buffer));
message = parser.construct_message();
@ -401,12 +400,11 @@ public class Geary.RFC822.Message : BaseObject {
return (addrs.size > 0) ? addrs : null;
}
public Geary.Memory.AbstractBuffer get_body_rfc822_buffer() {
public Memory.Buffer get_body_rfc822_buffer() {
return new Geary.Memory.StringBuffer(message.to_string());
}
public Geary.Memory.AbstractBuffer get_first_mime_part_of_content_type(string content_type,
bool to_html = false)
public Memory.Buffer get_first_mime_part_of_content_type(string content_type, bool to_html = false)
throws RFC822Error {
// search for content type starting from the root
GMime.Part? part = find_first_mime_part(message.get_mime_part(), content_type);
@ -534,7 +532,7 @@ public class Geary.RFC822.Message : BaseObject {
return RFC822.MailboxAddress.list_to_string(recipients, "", (a) => a.to_searchable_string());
}
public Geary.Memory.AbstractBuffer get_content_by_mime_id(string mime_id) throws RFC822Error {
public Memory.Buffer get_content_by_mime_id(string mime_id) throws RFC822Error {
GMime.Part? part = find_mime_part_by_mime_id(message.get_mime_part(), mime_id);
if (part == null) {
throw new RFC822Error.NOT_FOUND("Could not find a MIME part with content-id %s",
@ -613,7 +611,7 @@ public class Geary.RFC822.Message : BaseObject {
}
}
private Geary.Memory.AbstractBuffer mime_part_to_memory_buffer(GMime.Part part,
private Memory.Buffer mime_part_to_memory_buffer(GMime.Part part,
bool to_utf8 = false, bool to_html = false) throws RFC822Error {
GMime.DataWrapper? wrapper = part.get_content_object();
@ -652,7 +650,7 @@ public class Geary.RFC822.Message : BaseObject {
wrapper.write_to_stream(stream_filter);
stream_filter.flush();
return new Geary.Memory.Buffer(byte_array.data, byte_array.len);
return new Geary.Memory.ByteBuffer.from_byte_array(byte_array);
}
public string to_string() {

View file

@ -21,6 +21,34 @@ public GMime.FilterCharset create_utf8_filter_charset(string from_charset) {
return filter_charset;
}
/**
* Uses the best-possible transfer of bytes from the Memory.Buffer to the GMime.StreamMem object.
* The StreamMem object should be destroyed *before* the Memory.Buffer object, since this method
* will use unowned variants whenever possible.
*/
public GMime.StreamMem create_stream_mem(Memory.Buffer buffer) {
Memory.UnownedByteArrayBuffer? unowned_bytes_array_buffer = buffer as Memory.UnownedByteArrayBuffer;
if (unowned_bytes_array_buffer != null) {
// set_byte_array doesn't do any copying and doesn't take ownership -- perfect, this is
// the best of all possible worlds, assuming the Memory.Buffer is not destroyed first
GMime.StreamMem stream = new GMime.StreamMem();
stream.set_byte_array(unowned_bytes_array_buffer.to_unowned_byte_array());
return stream;
}
Memory.UnownedBytesBuffer? unowned_bytes_buffer = buffer as Memory.UnownedBytesBuffer;
if (unowned_bytes_buffer != null) {
// StreamMem.with_buffer does do a buffer copy (there's not set_buffer() call like
// set_byte_array() for some reason), but don't do a buffer copy when it comes out of the
// Memory.Buffer
return new GMime.StreamMem.with_buffer(unowned_bytes_buffer.to_unowned_uint8_array());
}
// do plain-old buffer copy
return new GMime.StreamMem.with_buffer(buffer.get_uint8_array());
}
public string create_subject_for_reply(Geary.Email email) {
return (email.subject ?? new Geary.RFC822.Subject("")).create_reply().value;
}

View file

@ -173,7 +173,8 @@ public class Geary.Smtp.ClientSession {
// DATA
Geary.RFC822.Message email_copy = new Geary.RFC822.Message.without_bcc(email);
response = yield cx.send_data_async(email_copy.get_body_rfc822_buffer().get_array(), cancellable);
response = yield cx.send_data_async(email_copy.get_body_rfc822_buffer().get_bytes().get_data(),
cancellable);
if (!response.code.is_success_completed())
response.throw_error("Unable to send message");

View file

@ -31,7 +31,7 @@ public class Geary.Smtp.PlainAuthenticator : Geary.Smtp.AbstractAuthenticator {
growable.append(nul);
growable.append((credentials.pass ?? "").data);
return Base64.encode(growable.get_array()).data;
return Base64.encode(growable.get_bytes().get_data()).data;
}
}

View file

@ -13,6 +13,11 @@ public Gee.ArrayList<G> to_array_list<G>(Gee.Collection<G> c) {
return list;
}
public void add_all_array<G>(Gee.Collection<G> c, G[] ar) {
foreach (G g in ar)
c.add(g);
}
public G? get_first<G>(Gee.Collection<G> c) {
Gee.Iterator<G> iter = c.iterator();

View file

@ -1,238 +0,0 @@
/* Copyright 2011-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.
*/
public abstract class Geary.Memory.AbstractBuffer : BaseObject {
public abstract size_t get_size();
public abstract size_t get_allocated_size();
public abstract uint8[] get_array();
/**
* Returns an InputStream that can read the buffer in its current entirety. Note that the
* InputStream may share its memory buffer(s) with the AbstractBuffer but does not hold
* references to them or the AbstractBuffer itself. Thus, the AbstractBuffer should only be
* destroyed after all InputStreams are destroyed or exhausted.
*/
public abstract InputStream get_input_stream();
/**
* Returns the contents of the buffer as though it was a null terminated string. Note that this
* involves reading the entire buffer into memory.
*
* If the conversion fails or decodes as invalid UTF-8, an empty string is returned.
*/
public string to_string() {
uint8[] buffer = get_array();
buffer += (uint8) '\0';
return (string) buffer;
}
/**
* Returns the contents of the buffer as though it was a UTF-8 string. Note that this involves
* reading the entire buffer into memory.
*
* If the conversion fails or decodes as invalid UTF-8, an empty string is returned.
*/
public string to_valid_utf8() {
string str = to_string();
return str.validate() ? str : "";
}
}
public class Geary.Memory.EmptyBuffer : Geary.Memory.AbstractBuffer {
private static EmptyBuffer? _instance = null;
public static EmptyBuffer instance {
get {
if (_instance == null)
_instance = new EmptyBuffer();
return _instance;
}
}
private uint8[]? empty = null;
private EmptyBuffer() {
}
public override size_t get_size() {
return 0;
}
public override size_t get_allocated_size() {
return 0;
}
public override uint8[] get_array() {
if (empty == null)
empty = new uint8[0];
return empty;
}
public override InputStream get_input_stream() {
return new MemoryInputStream.from_data(get_array(), null);
}
}
public class Geary.Memory.StringBuffer : Geary.Memory.AbstractBuffer {
private string str;
public StringBuffer(string str) {
this.str = str;
}
public override size_t get_size() {
return str.data.length;
}
public override size_t get_allocated_size() {
return str.data.length;
}
public override uint8[] get_array() {
return str.data;
}
public override InputStream get_input_stream() {
return new MemoryInputStream.from_data(str.data, null);
}
}
public class Geary.Memory.Buffer : Geary.Memory.AbstractBuffer {
private uint8[] buffer;
private size_t filled;
public Buffer(uint8[] buffer, size_t filled) {
this.buffer = buffer;
this.filled = filled;
}
public override size_t get_size() {
return filled;
}
public override size_t get_allocated_size() {
return buffer.length;
}
public override uint8[] get_array() {
return buffer[0:filled];
}
public override InputStream get_input_stream() {
return new MemoryInputStream.from_data(buffer[0:filled], null);
}
}
public class Geary.Memory.GrowableBuffer : Geary.Memory.AbstractBuffer {
private class BufferFragment {
public uint8[] buffer;
public size_t reserved_bytes = 0;
public unowned uint8[]? active = null;
public BufferFragment(size_t bytes) {
buffer = new uint8[bytes];
}
public unowned uint8[]? reserve(size_t requested_bytes) {
if((reserved_bytes + requested_bytes) > buffer.length)
return null;
active = buffer[reserved_bytes:reserved_bytes + requested_bytes];
reserved_bytes += requested_bytes;
return active;
}
public void adjust(uint8[] active, size_t adjusted_bytes) {
assert(this.active == active);
assert(active.length >= adjusted_bytes);
size_t freed = active.length - adjusted_bytes;
assert(reserved_bytes >= freed);
reserved_bytes -= freed;
active = null;
}
}
private size_t min_fragment_bytes;
private Gee.ArrayList<BufferFragment> fragments = new Gee.ArrayList<BufferFragment>();
public GrowableBuffer(size_t min_fragment_bytes = 1024) {
this.min_fragment_bytes = min_fragment_bytes;
}
public unowned uint8[] allocate(size_t bytes) {
if (fragments.size > 0) {
unowned uint8[]? buffer = fragments[fragments.size - 1].reserve(bytes);
if (buffer != null)
return buffer;
}
BufferFragment next = new BufferFragment(
(bytes < min_fragment_bytes) ? min_fragment_bytes : bytes);
fragments.add(next);
unowned uint8[]? buffer = next.reserve(bytes);
assert(buffer != null);
return buffer;
}
public void adjust(uint8[] buffer, size_t adjusted_bytes) {
assert(fragments.size > 0);
fragments[fragments.size - 1].adjust(buffer, adjusted_bytes);
}
public void append(uint8[] buffer) {
unowned uint8[] dest = allocate(buffer.length);
assert(dest.length == buffer.length);
GLib.Memory.copy(dest, buffer, buffer.length);
}
public override size_t get_size() {
size_t size = 0;
foreach (BufferFragment fragment in fragments)
size += fragment.reserved_bytes;
return size;
}
public override size_t get_allocated_size() {
size_t size = 0;
foreach (BufferFragment fragment in fragments)
size += fragment.buffer.length;
return size;
}
public override uint8[] get_array() {
uint8[] buffer = new uint8[get_size()];
uint8 *buffer_ptr = (uint8 *) buffer;
foreach (BufferFragment fragment in fragments) {
GLib.Memory.copy(buffer_ptr, fragment.buffer, fragment.reserved_bytes);
buffer_ptr += fragment.reserved_bytes;
}
return buffer;
}
public override InputStream get_input_stream() {
// TODO: add_data() copies the buffer, hence the optimization doesn't work yet.
MemoryInputStream mins = new MemoryInputStream();
foreach (BufferFragment fragment in fragments)
mins.add_data(fragment.buffer[0:fragment.reserved_bytes], null);
return mins;
}
}

View file

@ -0,0 +1,44 @@
/* 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.
*/
namespace Geary.ObjectUtils {
/**
* Creates a set of property bindings from source to dest with the given binding flags.
*/
public Gee.List<Binding>? mirror_properties(Object source, Object dest, BindingFlags
flags = GLib.BindingFlags.DEFAULT | GLib.BindingFlags.SYNC_CREATE) {
// Make sets of both object's properties.
Gee.HashSet<ParamSpec> source_properties = new Gee.HashSet<ParamSpec>();
Collection.add_all_array<ParamSpec>(source_properties,
source.get_class().list_properties());
Gee.HashSet<ParamSpec> dest_properties = new Gee.HashSet<ParamSpec>();
Collection.add_all_array<ParamSpec>(dest_properties,
dest.get_class().list_properties());
// Remove properties from source_properties that are not in both sets.
source_properties.retain_all(dest_properties);
// Create all bindings.
Gee.List<Binding> bindings = new Gee.ArrayList<Binding>();
foreach(ParamSpec ps in source_properties)
bindings.add(source.bind_property(ps.name, dest, ps.name, flags));
return bindings.size > 0 ? bindings : null;
}
/**
* Removes a property mirror created by mirror_properties
*/
public void unmirror_properties(Gee.List<Binding> bindings) {
foreach(Binding b in bindings)
b.unref();
bindings.clear();
}
}

View file

@ -14,7 +14,7 @@
</child>
<child>
<object class="GtkAction" id="edit_account">
<property name="icon_name">gtk-edit</property>
<property name="icon_name">edit-symbolic</property>
</object>
</child>
<child>