Commit fca993fe authored by Charles Lindsay's avatar Charles Lindsay

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.
parents c4ef5eec a4ce7899
<?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>
--
-- 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;
--
-- 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);
--
-- 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);
......@@ -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
......
......@@ -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,
......
......@@ -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());
}
}
......@@ -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() {
......
......@@ -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() {
......
......@@ -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);
}
......
......@@ -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,
......
......@@ -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;
......
......@@ -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();
......
......@@ -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();
......
......@@ -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;
}
......
/* 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;
}
}
......@@ -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
......
......@@ -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.
*/
......
......@@ -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.
*/
......
......@@ -30,11 +30,12 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder {