Merge branch 'master' into feature/search
Conflicts: src/engine/imap-db/imap-db-account.vala src/engine/imap-engine/imap-engine-generic-account.vala src/engine/imap-engine/imap-engine-generic-folder.vala
This commit is contained in:
commit
bc2146dad5
105 changed files with 5990 additions and 4153 deletions
|
|
@ -15,13 +15,13 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account {
|
|||
this.information = information;
|
||||
}
|
||||
|
||||
protected virtual void notify_folders_available_unavailable(Gee.Collection<Geary.Folder>? available,
|
||||
Gee.Collection<Geary.Folder>? unavailable) {
|
||||
protected virtual void notify_folders_available_unavailable(Gee.List<Geary.Folder>? available,
|
||||
Gee.List<Geary.Folder>? unavailable) {
|
||||
folders_available_unavailable(available, unavailable);
|
||||
}
|
||||
|
||||
protected virtual void notify_folders_added_removed(Gee.Collection<Geary.Folder>? added,
|
||||
Gee.Collection<Geary.Folder>? removed) {
|
||||
protected virtual void notify_folders_added_removed(Gee.List<Geary.Folder>? added,
|
||||
Gee.List<Geary.Folder>? removed) {
|
||||
folders_added_removed(added, removed);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,18 +27,31 @@ public interface Geary.Account : BaseObject {
|
|||
|
||||
/**
|
||||
* Fired when folders become available or unavailable in the account.
|
||||
*
|
||||
* Folders become available when the account is first opened or when
|
||||
* they're created later; they become unavailable when the account is
|
||||
* closed or they're deleted later.
|
||||
*
|
||||
* Folders are ordered for the convenience of the caller from the top of the heirarchy to
|
||||
* lower in the heirarchy. In other words, parents are listed before children, assuming the
|
||||
* lists are traversed in natural order.
|
||||
*
|
||||
* @see sort_by_path
|
||||
*/
|
||||
public signal void folders_available_unavailable(Gee.Collection<Geary.Folder>? available,
|
||||
Gee.Collection<Geary.Folder>? unavailable);
|
||||
public signal void folders_available_unavailable(Gee.List<Geary.Folder>? available,
|
||||
Gee.List<Geary.Folder>? unavailable);
|
||||
|
||||
/**
|
||||
* Fired when folders are created or deleted.
|
||||
*
|
||||
* Folders are ordered for the convenience of the caller from the top of the heirarchy to
|
||||
* lower in the heirarchy. In other words, parents are listed before children, assuming the
|
||||
* lists are traversed in natural order.
|
||||
*
|
||||
* @see sort_by_path
|
||||
*/
|
||||
public signal void folders_added_removed(Gee.Collection<Geary.Folder>? added,
|
||||
Gee.Collection<Geary.Folder>? removed);
|
||||
public signal void folders_added_removed(Gee.List<Geary.Folder>? added,
|
||||
Gee.List<Geary.Folder>? removed);
|
||||
|
||||
/**
|
||||
* Fired when a Folder's contents is detected having changed.
|
||||
|
|
@ -68,20 +81,36 @@ public interface Geary.Account : BaseObject {
|
|||
/**
|
||||
* Signal notification method for subclasses to use.
|
||||
*/
|
||||
public abstract void notify_folders_available_unavailable(Gee.Collection<Geary.Folder>? available,
|
||||
Gee.Collection<Geary.Folder>? unavailable);
|
||||
|
||||
protected abstract void notify_folders_available_unavailable(Gee.List<Geary.Folder>? available,
|
||||
Gee.List<Geary.Folder>? unavailable);
|
||||
|
||||
/**
|
||||
* Signal notification method for subclasses to use.
|
||||
*/
|
||||
protected abstract void notify_folders_added_removed(Gee.Collection<Geary.Folder>? added,
|
||||
Gee.Collection<Geary.Folder>? removed);
|
||||
protected abstract void notify_folders_added_removed(Gee.List<Geary.Folder>? added,
|
||||
Gee.List<Geary.Folder>? removed);
|
||||
|
||||
/**
|
||||
* Signal notification method for subclasses to use.
|
||||
*/
|
||||
protected abstract void notify_folders_contents_altered(Gee.Collection<Geary.Folder> altered);
|
||||
|
||||
/**
|
||||
* A utility method to sort a Gee.Collection of {@link Folder}s by their {@link FolderPath}s
|
||||
* to ensure they comport with {@link folders_available_unavailable} and
|
||||
* {@link folders_added_removed} signals' contracts.
|
||||
*/
|
||||
protected Gee.List<Geary.Folder> sort_by_path(Gee.Collection<Geary.Folder> folders) {
|
||||
Gee.TreeSet<Geary.Folder> sorted = new Gee.TreeSet<Geary.Folder>(folder_path_comparator);
|
||||
sorted.add_all(folders);
|
||||
|
||||
return Collection.to_array_list<Geary.Folder>(sorted);
|
||||
}
|
||||
|
||||
private int folder_path_comparator(Geary.Folder a, Geary.Folder b) {
|
||||
return a.get_path().compare_to(b.get_path());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1078,7 +1078,7 @@ public class Geary.ConversationMonitor : BaseObject {
|
|||
return;
|
||||
|
||||
if (!retry_connection) {
|
||||
debug("Folder %s closed due to error, not reestablishing connection", folder.to_string());
|
||||
debug("Folder %s closed normally, not reestablishing connection", folder.to_string());
|
||||
|
||||
stop_monitoring_internal_async.begin(false, false, null);
|
||||
|
||||
|
|
|
|||
|
|
@ -74,10 +74,18 @@ public class Geary.Email : BaseObject {
|
|||
return is_all_set(required_fields);
|
||||
}
|
||||
|
||||
public inline bool fulfills_any(Field required_fields) {
|
||||
return is_any_set(required_fields);
|
||||
}
|
||||
|
||||
public inline bool require(Field required_fields) {
|
||||
return is_all_set(required_fields);
|
||||
}
|
||||
|
||||
public inline bool requires_any(Field required_fields) {
|
||||
return is_any_set(required_fields);
|
||||
}
|
||||
|
||||
public string to_list_string() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (Field f in all()) {
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ public class Geary.Engine : BaseObject {
|
|||
return error_code;
|
||||
|
||||
// validate IMAP, which requires logging in and establishing an AUTHORIZED cx state
|
||||
Geary.Imap.ClientSession? imap_session = new Imap.ClientSession(account.get_imap_endpoint(), true);
|
||||
Geary.Imap.ClientSession? imap_session = new Imap.ClientSession(account.get_imap_endpoint());
|
||||
try {
|
||||
yield imap_session.connect_async(cancellable);
|
||||
} catch (Error err) {
|
||||
|
|
@ -231,7 +231,7 @@ public class Geary.Engine : BaseObject {
|
|||
yield imap_session.initiate_session_async(account.imap_credentials, cancellable);
|
||||
|
||||
// Connected and initiated, still need to be sure connection authorized
|
||||
string current_mailbox;
|
||||
Imap.MailboxSpecifier current_mailbox;
|
||||
if (imap_session.get_context(out current_mailbox) != Imap.ClientSession.Context.AUTHORIZED)
|
||||
error_code |= ValidationResult.IMAP_CREDENTIALS_INVALID;
|
||||
} catch (Error err) {
|
||||
|
|
|
|||
|
|
@ -4,8 +4,22 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A generic structure for representing and maintaining folder paths.
|
||||
*
|
||||
* A FolderPath may have one parent and one child. A FolderPath without a parent is called a
|
||||
* root folder can be be created with {@link FolderRoot}, which is a FolderPath.
|
||||
*
|
||||
* A FolderPath has a delimiter. This delimiter is specified in the FolderRoot.
|
||||
*
|
||||
* @see FolderRoot
|
||||
*/
|
||||
|
||||
public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
|
||||
Gee.Comparable<Geary.FolderPath> {
|
||||
/**
|
||||
* The name of this folder (without any child or parent names or delimiters).
|
||||
*/
|
||||
public string basename { get; private set; }
|
||||
|
||||
private Gee.List<Geary.FolderPath>? path = null;
|
||||
|
|
@ -26,26 +40,53 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
|
|||
this.basename = basename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this {@link FolderPath} is the root folder.
|
||||
*
|
||||
* This means that the FolderPath ''should'' be castable into {@link FolderRoot}, which is
|
||||
* enforced through the constructor and accessor styles of this class. However, this test
|
||||
* merely checks if this FolderPath has any children. A GObject "is" operation is the
|
||||
* reliable way to cast to FolderRoot.
|
||||
*/
|
||||
public bool is_root() {
|
||||
return (path == null || path.size == 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link FolderRoot} of this path.
|
||||
*/
|
||||
public Geary.FolderRoot get_root() {
|
||||
return (FolderRoot) ((path != null && path.size > 0) ? path[0] : this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent {@link FolderPath} of this folder or null if this is the root.
|
||||
*
|
||||
* @see is_root
|
||||
*/
|
||||
public Geary.FolderPath? get_parent() {
|
||||
return (path != null && path.size > 0) ? path.last() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of folders in this path, not including any children of this object.
|
||||
*/
|
||||
public int get_path_length() {
|
||||
// include self, which is not stored in the path list
|
||||
return (path != null) ? path.size + 1 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link FolderPath} object at the index, with this FolderPath object being
|
||||
* the farthest child.
|
||||
*
|
||||
* Root is at index 0 (zero).
|
||||
*
|
||||
* Returns null if index is out of bounds. There is always at least one element in the path,
|
||||
* namely this one.
|
||||
* namely this one, meaning zero is always acceptable and that index[length - 1] will always
|
||||
* return this object.
|
||||
*
|
||||
* @see get_path_length
|
||||
*/
|
||||
public Geary.FolderPath? get_folder_at(int index) {
|
||||
// include self, which is not stored in the path list ... essentially, this logic makes it
|
||||
|
|
@ -63,6 +104,12 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link FolderPath} as a List of {@link basename} strings, this FolderPath's
|
||||
* being the last in the list.
|
||||
*
|
||||
* Thus, the list should have at least one element.
|
||||
*/
|
||||
public Gee.List<string> as_list() {
|
||||
Gee.List<string> list = new Gee.ArrayList<string>();
|
||||
|
||||
|
|
@ -76,6 +123,9 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
|
|||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link FolderPath} object that is a child of this folder.
|
||||
*/
|
||||
public Geary.FolderPath get_child(string basename) {
|
||||
// Build the child's path, which is this node's path plus this node
|
||||
Gee.List<FolderPath> child_path = new Gee.ArrayList<FolderPath>();
|
||||
|
|
@ -86,13 +136,54 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
|
|||
return new FolderPath.child(child_path, basename);
|
||||
}
|
||||
|
||||
public string get_fullpath(string? use_separator = null) {
|
||||
/**
|
||||
* Returns true if this {@link FolderPath} has a default separator.
|
||||
*
|
||||
* It determines this by returning true if its {@link FolderRoot.default_separator} is
|
||||
* non-null and non-empty.
|
||||
*/
|
||||
public bool has_default_separator() {
|
||||
return get_root().default_separator != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the other {@link FolderPath} has the same parent as this one.
|
||||
*
|
||||
* Like {@link equal_to} and {@link compare_to}, this comparison does not account for the
|
||||
* {@link FolderRoot.default_separator}. The comparison is lexiographic, not by reference.
|
||||
*/
|
||||
public bool has_same_parent(FolderPath other) {
|
||||
FolderPath? parent = get_parent();
|
||||
FolderPath? other_parent = other.get_parent();
|
||||
|
||||
if (parent == other_parent)
|
||||
return true;
|
||||
|
||||
if (parent != null && other_parent != null)
|
||||
return parent.equal_to(other_parent);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link FolderPath} as a single string with the supplied separator used as a
|
||||
* delimiter.
|
||||
*
|
||||
* If null is passed in, {@link FolderRoot.default_separator} is used. If the default
|
||||
* separator is null, no fullpath can be produced and this method will return null.
|
||||
*
|
||||
* The separator is not appended to the fullpath.
|
||||
*
|
||||
* @see has_default_separator
|
||||
*/
|
||||
public string? get_fullpath(string? use_separator) {
|
||||
string? separator = use_separator ?? get_root().default_separator;
|
||||
|
||||
// no separator, no hierarchy
|
||||
// no separator, no fullpath
|
||||
if (separator == null)
|
||||
return basename;
|
||||
return null;
|
||||
|
||||
// use cached copy if the stars align
|
||||
if (fullpath != null && fullpath_separator == separator)
|
||||
return fullpath;
|
||||
|
||||
|
|
@ -118,11 +209,19 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
|
|||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Comparisons for Geary.FolderPath is defined as (a) empty paths are less-than non-empty paths
|
||||
* and (b) each element is compared to the corresponding path element of the other FolderPath
|
||||
* following collation rules for casefolded (case-insensitive) compared, and (c) shorter paths
|
||||
* 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
|
||||
* {@link FolderRoot.case_sensitive} does.
|
||||
*
|
||||
* Returns -1 if this path is lexiographically before the other, 1 if its after, and 0 if they
|
||||
* are equal.
|
||||
*/
|
||||
public int compare_to(Geary.FolderPath other) {
|
||||
if (this == other)
|
||||
|
|
@ -146,6 +245,12 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
|
|||
return this_list.size - other_list.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* As with {@link compare_to}, the {@link FolderRoot.default_separator} has no bearing on the
|
||||
* hash, although {@link FolderRoot.case_sensitive} does.
|
||||
*/
|
||||
public uint hash() {
|
||||
if (stored_hash != uint.MAX)
|
||||
return stored_hash;
|
||||
|
|
@ -168,6 +273,9 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
|
|||
return cs ? (basename == cmp) : (basename.down() == cmp.down());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public bool equal_to(Geary.FolderPath other) {
|
||||
int path_length = get_path_length();
|
||||
if (other.get_path_length() != path_length)
|
||||
|
|
@ -188,21 +296,44 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the fullpath using the default separator. Using only for debugging and logging.
|
||||
* Returns the fullpath using the default separator.
|
||||
*
|
||||
* Use only for debugging and logging.
|
||||
*/
|
||||
public string to_string() {
|
||||
return get_fullpath();
|
||||
// use slash if no default separator available
|
||||
return get_fullpath(has_default_separator() ? null : "/");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The root of a folder heirarchy.
|
||||
*
|
||||
* A {@link FolderPath} can only be created by starting with a FolderRoot and adding children
|
||||
* via {@link FolderPath.get_child}. Because all FolderPaths hold references to their parents,
|
||||
* this element can be retrieved with {@link FolderPath.get_root}.
|
||||
*/
|
||||
public class Geary.FolderRoot : Geary.FolderPath {
|
||||
/**
|
||||
* The default separator (delimiter) for this path.
|
||||
*
|
||||
* If null, the separator can be supplied later to {@link FolderPath.get_fullpath}.
|
||||
*
|
||||
* This value will never be empty (i.e. zero-length). A zero-length separator passed to the
|
||||
* constructor will result in this property being null.
|
||||
*/
|
||||
public string? default_separator { get; private set; }
|
||||
/**
|
||||
* Whether this path is lexiographically case-sensitive.
|
||||
*
|
||||
* This has implications, as {@link FolderPath} is Comparable and Hashable.
|
||||
*/
|
||||
public bool case_sensitive { get; private set; }
|
||||
|
||||
public FolderRoot(string basename, string? default_separator, bool case_sensitive) {
|
||||
base (basename);
|
||||
|
||||
this.default_separator = default_separator;
|
||||
this.default_separator = !String.is_empty(default_separator) ? default_separator : null;
|
||||
this.case_sensitive;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ public enum Flag {
|
|||
CONVERSATIONS,
|
||||
PERIODIC,
|
||||
SQL,
|
||||
FOLDER_NORMALIZATION;
|
||||
FOLDER_NORMALIZATION,
|
||||
DESERIALIZER;
|
||||
|
||||
public inline bool is_all_set(Flag flags) {
|
||||
return (flags & this) == flags;
|
||||
|
|
|
|||
|
|
@ -83,11 +83,6 @@ private class Geary.ImapDB.Account : BaseObject {
|
|||
|
||||
// Search folder
|
||||
search_folder = new SearchFolder(account);
|
||||
|
||||
// Need to clear duplicate folders due to old bug that caused multiple folders to be
|
||||
// created in the database ... benign due to other logic, but want to prevent this from
|
||||
// happening if possible
|
||||
clear_duplicate_folders();
|
||||
}
|
||||
|
||||
public async void close_async(Cancellable? cancellable) throws Error {
|
||||
|
|
@ -112,12 +107,8 @@ private class Geary.ImapDB.Account : BaseObject {
|
|||
throws Error {
|
||||
check_open();
|
||||
|
||||
Geary.Imap.FolderProperties? properties = imap_folder.get_properties();
|
||||
|
||||
// properties *must* be available to perform a clone
|
||||
assert(properties != null);
|
||||
|
||||
Geary.FolderPath path = imap_folder.get_path();
|
||||
Geary.Imap.FolderProperties properties = imap_folder.properties;
|
||||
Geary.FolderPath path = imap_folder.path;
|
||||
|
||||
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
||||
// get the parent of this folder, creating parents if necessary ... ok if this fails,
|
||||
|
|
@ -157,8 +148,8 @@ private class Geary.ImapDB.Account : BaseObject {
|
|||
throws Error {
|
||||
check_open();
|
||||
|
||||
Geary.Imap.FolderProperties properties = imap_folder.get_properties();
|
||||
Geary.FolderPath path = imap_folder.get_path();
|
||||
Geary.Imap.FolderProperties properties = imap_folder.properties;
|
||||
Geary.FolderPath path = imap_folder.path;
|
||||
|
||||
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
||||
int64 parent_id;
|
||||
|
|
@ -214,8 +205,8 @@ private class Geary.ImapDB.Account : BaseObject {
|
|||
throws Error {
|
||||
check_open();
|
||||
|
||||
Geary.Imap.FolderProperties properties = imap_folder.get_properties();
|
||||
Geary.FolderPath path = imap_folder.get_path();
|
||||
Geary.Imap.FolderProperties properties = imap_folder.properties;
|
||||
Geary.FolderPath path = imap_folder.path;
|
||||
|
||||
yield db.exec_transaction_async(Db.TransactionType.RW, (cx) => {
|
||||
int64 parent_id;
|
||||
|
|
@ -370,7 +361,7 @@ private class Geary.ImapDB.Account : BaseObject {
|
|||
|
||||
if (id_map.size == 0) {
|
||||
throw new EngineError.NOT_FOUND("No local folders in %s",
|
||||
(parent != null) ? parent.get_fullpath() : "root");
|
||||
(parent != null) ? parent.to_string() : "root");
|
||||
}
|
||||
|
||||
Gee.Collection<Geary.ImapDB.Folder> folders = new Gee.ArrayList<Geary.ImapDB.Folder>();
|
||||
|
|
@ -456,8 +447,19 @@ private class Geary.ImapDB.Account : BaseObject {
|
|||
|
||||
private Geary.ImapDB.Folder? get_local_folder(Geary.FolderPath path) {
|
||||
FolderReference? folder_ref = folder_refs.get(path);
|
||||
if (folder_ref == null)
|
||||
return null;
|
||||
|
||||
return (folder_ref != null) ? (Geary.ImapDB.Folder) folder_ref.get_reference() : null;
|
||||
ImapDB.Folder? folder = (Geary.ImapDB.Folder?) folder_ref.get_reference();
|
||||
if (folder == null)
|
||||
return null;
|
||||
|
||||
// use supplied FolderPath rather than one here; if it came from the server, it has
|
||||
// a usable separator
|
||||
if (path.get_root().default_separator != null)
|
||||
folder.set_path(path);
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
private Geary.ImapDB.Folder create_local_folder(Geary.FolderPath path, int64 folder_id,
|
||||
|
|
@ -698,46 +700,6 @@ private class Geary.ImapDB.Account : BaseObject {
|
|||
}, cancellable);
|
||||
}
|
||||
|
||||
private void clear_duplicate_folders() {
|
||||
int count = 0;
|
||||
|
||||
try {
|
||||
// Find all folders with duplicate names
|
||||
Db.Result result = db.query("SELECT id, name FROM FolderTable WHERE name IN "
|
||||
+ "(SELECT name FROM FolderTable GROUP BY name HAVING (COUNT(name) > 1))");
|
||||
while (!result.finished) {
|
||||
int64 id = result.int64_at(0);
|
||||
|
||||
// see if any folders have this folder as a parent OR if there are messages associated
|
||||
// with this folder
|
||||
Db.Statement child_stmt = db.prepare("SELECT id FROM FolderTable WHERE parent_id=?");
|
||||
child_stmt.bind_int64(0, id);
|
||||
Db.Result child_result = child_stmt.exec();
|
||||
|
||||
Db.Statement message_stmt = db.prepare(
|
||||
"SELECT id FROM MessageLocationTable WHERE folder_id=?");
|
||||
message_stmt.bind_int64(0, id);
|
||||
Db.Result message_result = message_stmt.exec();
|
||||
|
||||
if (child_result.finished && message_result.finished) {
|
||||
// no children, delete it
|
||||
Db.Statement delete_stmt = db.prepare("DELETE FROM FolderTable WHERE id=?");
|
||||
delete_stmt.bind_int64(0, id);
|
||||
|
||||
delete_stmt.exec();
|
||||
count++;
|
||||
}
|
||||
|
||||
result.next();
|
||||
}
|
||||
} catch (Error err) {
|
||||
debug("Error attempting to clear duplicate folders from account: %s", err.message);
|
||||
}
|
||||
|
||||
if (count > 0)
|
||||
debug("Deleted %d duplicate folders", count);
|
||||
}
|
||||
|
||||
public async int get_email_count_async(Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
|
|
@ -1013,10 +975,8 @@ private class Geary.ImapDB.Account : BaseObject {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (parent_id <= 0) {
|
||||
return new Geary.FolderRoot(name,
|
||||
Geary.Imap.Account.ASSUMED_SEPARATOR, Geary.Imap.Folder.CASE_SENSITIVE);
|
||||
}
|
||||
if (parent_id <= 0)
|
||||
return new Geary.FolderRoot(name, null, Geary.Imap.Folder.CASE_SENSITIVE);
|
||||
|
||||
Geary.FolderPath? parent_path = do_find_folder_path(cx, parent_id, cancellable);
|
||||
return (parent_path == null ? null : parent_path.get_child(name));
|
||||
|
|
|
|||
|
|
@ -85,6 +85,12 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
return path;
|
||||
}
|
||||
|
||||
// Use with caution; ImapDB.Account uses this to "improve" the path with one from the server,
|
||||
// which has a usable path delimiter.
|
||||
internal void set_path(FolderPath path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public Geary.Imap.FolderProperties get_properties() {
|
||||
return properties;
|
||||
}
|
||||
|
|
@ -132,6 +138,12 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
}
|
||||
}
|
||||
|
||||
private void clear_marked_removed() {
|
||||
lock (marked_removed) {
|
||||
marked_removed.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private bool is_marked_removed(Geary.EmailIdentifier id) {
|
||||
lock (marked_removed) {
|
||||
return marked_removed.contains(id);
|
||||
|
|
@ -514,6 +526,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
return Imap.UID.is_value_valid(ordering) ? new Imap.UID(ordering) : null;
|
||||
}
|
||||
|
||||
// TODO: Rename to detach_email_async().
|
||||
public async void remove_email_async(Gee.Collection<Geary.EmailIdentifier> ids,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
|
@ -542,6 +555,20 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
}, cancellable);
|
||||
}
|
||||
|
||||
public async void detach_all_emails_async(Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
yield db.exec_transaction_async(Db.TransactionType.WO, (cx) => {
|
||||
Db.Statement stmt = cx.prepare(
|
||||
"DELETE FROM MessageLocationTable WHERE folder_id=?");
|
||||
stmt.bind_rowid(0, folder_id);
|
||||
|
||||
clear_marked_removed();
|
||||
|
||||
return Db.TransactionOutcome.COMMIT;
|
||||
}, cancellable);
|
||||
}
|
||||
|
||||
public async void mark_email_async(Gee.Collection<Geary.EmailIdentifier> to_mark,
|
||||
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove, Cancellable? cancellable)
|
||||
throws Error {
|
||||
|
|
@ -1120,7 +1147,7 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
private void do_set_email_flags(Db.Connection cx, Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> map,
|
||||
Cancellable? cancellable) throws Error {
|
||||
Db.Statement update_stmt = cx.prepare(
|
||||
"UPDATE MessageTable SET flags=? WHERE id=?");
|
||||
"UPDATE MessageTable SET flags=?, fields = fields | ? WHERE id=?");
|
||||
|
||||
foreach (Geary.EmailIdentifier id in map.keys) {
|
||||
int64 message_id = do_find_message(cx, id, ListFlags.NONE, cancellable);
|
||||
|
|
@ -1131,7 +1158,8 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
|
|||
|
||||
update_stmt.reset(Db.ResetScope.CLEAR_BINDINGS);
|
||||
update_stmt.bind_string(0, flags.serialize());
|
||||
update_stmt.bind_rowid(1, message_id);
|
||||
update_stmt.bind_int(1, Geary.Email.Field.FLAGS);
|
||||
update_stmt.bind_rowid(2, message_id);
|
||||
|
||||
update_stmt.exec(cancellable);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,13 +43,13 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
|
|||
if (path_type_map == null) {
|
||||
path_type_map = new Gee.HashMap<Geary.FolderPath, Geary.SpecialFolderType>();
|
||||
|
||||
path_type_map.set(new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR,
|
||||
path_type_map.set(new Geary.FolderRoot(Imap.Account.INBOX_NAME, null,
|
||||
Imap.Folder.CASE_SENSITIVE), SpecialFolderType.INBOX);
|
||||
|
||||
Geary.FolderPath gmail_root = new Geary.FolderRoot(GMAIL_FOLDER,
|
||||
Imap.Account.ASSUMED_SEPARATOR, Imap.Folder.CASE_SENSITIVE);
|
||||
Geary.FolderPath googlemail_root = new Geary.FolderRoot(GOOGLEMAIL_FOLDER,
|
||||
Imap.Account.ASSUMED_SEPARATOR, Imap.Folder.CASE_SENSITIVE);
|
||||
Geary.FolderPath gmail_root = new Geary.FolderRoot(GMAIL_FOLDER, null,
|
||||
Imap.Folder.CASE_SENSITIVE);
|
||||
Geary.FolderPath googlemail_root = new Geary.FolderRoot(GOOGLEMAIL_FOLDER, null,
|
||||
Imap.Folder.CASE_SENSITIVE);
|
||||
|
||||
path_type_map.set(gmail_root.get_child("Drafts"), SpecialFolderType.DRAFTS);
|
||||
path_type_map.set(googlemail_root.get_child("Drafts"), SpecialFolderType.DRAFTS);
|
||||
|
|
|
|||
|
|
@ -92,10 +92,7 @@ private class Geary.ImapEngine.EmailFlagWatcher : BaseObject {
|
|||
int low = -1;
|
||||
bool finished = false;
|
||||
int total = 0;
|
||||
for (;;) {
|
||||
if (finished)
|
||||
break;
|
||||
|
||||
do {
|
||||
// Fetch a chunk of email flags in local folder.
|
||||
Gee.List<Geary.Email>? list_local = yield folder.list_email_async(low, PULL_CHUNK_COUNT,
|
||||
Email.Field.FLAGS, Geary.Folder.ListFlags.LOCAL_ONLY, cancellable);
|
||||
|
|
@ -104,22 +101,20 @@ private class Geary.ImapEngine.EmailFlagWatcher : BaseObject {
|
|||
|
||||
total += list_local.size;
|
||||
|
||||
// if this request's low was 1 or processed the top 2000, then this is the last iteration
|
||||
finished = (low == 1 || total >= MAX_EMAIL_WATCHED);
|
||||
|
||||
// Get all email identifiers in the local folder; also, update the low and count arguments
|
||||
Gee.HashMap<Geary.EmailIdentifier, Geary.EmailFlags> local_map = new Gee.HashMap<
|
||||
Geary.EmailIdentifier, Geary.EmailFlags>();
|
||||
foreach (Geary.Email e in list_local) {
|
||||
if (low == -1)
|
||||
low = e.position;
|
||||
else if (low > e.position)
|
||||
if (low == -1 || e.position < low)
|
||||
low = e.position;
|
||||
|
||||
local_map.set(e.id, e.email_flags);
|
||||
}
|
||||
|
||||
// now roll back PULL_CHUNK_COUNT earlier
|
||||
// if this request's low was 1 or processed the top 2000, then this is the last iteration
|
||||
finished = (low == 1 || total >= MAX_EMAIL_WATCHED);
|
||||
|
||||
// now roll back PULL_CHUNK_COUNT earlier for next iteration
|
||||
low = Numeric.int_floor(low - PULL_CHUNK_COUNT, 1);
|
||||
|
||||
// Fetch e-mail from folder using force update, which will cause the cache to be bypassed
|
||||
|
|
@ -127,7 +122,7 @@ private class Geary.ImapEngine.EmailFlagWatcher : BaseObject {
|
|||
Gee.List<Geary.Email>? list_remote = yield folder.list_email_by_sparse_id_async(local_map.keys,
|
||||
Email.Field.FLAGS, Geary.Folder.ListFlags.FORCE_UPDATE, cancellable);
|
||||
if (list_remote == null || list_remote.size == 0)
|
||||
continue;
|
||||
break;
|
||||
|
||||
// Build map of emails that have changed.
|
||||
Gee.HashMap<Geary.EmailIdentifier, Geary.EmailFlags> changed_map =
|
||||
|
|
@ -142,7 +137,7 @@ private class Geary.ImapEngine.EmailFlagWatcher : BaseObject {
|
|||
|
||||
if (!cancellable.is_cancelled() && changed_map.size > 0)
|
||||
email_flags_changed(changed_map);
|
||||
}
|
||||
} while (!finished);
|
||||
|
||||
Logging.debug(Logging.Flag.PERIODIC, "do_flag_watch_async: completed %s", folder.to_string());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,16 +7,13 @@
|
|||
private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
||||
private const int REFRESH_FOLDER_LIST_SEC = 10 * 60;
|
||||
|
||||
private static Geary.FolderPath? inbox_path = null;
|
||||
private static Geary.FolderPath? outbox_path = null;
|
||||
private static Geary.FolderPath? search_path = null;
|
||||
|
||||
private Imap.Account remote;
|
||||
private ImapDB.Account local;
|
||||
private bool open = false;
|
||||
private Gee.HashMap<FolderPath, Imap.FolderProperties> properties_map = new Gee.HashMap<
|
||||
FolderPath, Imap.FolderProperties>();
|
||||
private Gee.HashMap<FolderPath, GenericFolder> existing_folders = new Gee.HashMap<
|
||||
private Gee.HashMap<FolderPath, GenericFolder> folder_map = new Gee.HashMap<
|
||||
FolderPath, GenericFolder>();
|
||||
private Gee.HashMap<FolderPath, Folder> local_only = new Gee.HashMap<FolderPath, Folder>();
|
||||
private uint refresh_folder_timeout_id = 0;
|
||||
|
|
@ -95,7 +92,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
|
||||
notify_opened();
|
||||
|
||||
notify_folders_available_unavailable(local_only.values, null);
|
||||
notify_folders_available_unavailable(sort_by_path(local_only.values), null);
|
||||
|
||||
// schedule an immediate sweep of the folders; once this is finished, folders will be
|
||||
// regularly enumerated
|
||||
|
|
@ -106,8 +103,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
if (!open)
|
||||
return;
|
||||
|
||||
notify_folders_available_unavailable(null, local_only.values);
|
||||
notify_folders_available_unavailable(null, existing_folders.values);
|
||||
notify_folders_available_unavailable(null, sort_by_path(local_only.values));
|
||||
notify_folders_available_unavailable(null, sort_by_path(folder_map.values));
|
||||
|
||||
local.outbox.report_problem.disconnect(notify_report_problem);
|
||||
|
||||
|
|
@ -125,9 +122,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
} catch (Error rclose_err) {
|
||||
remote_err = rclose_err;
|
||||
}
|
||||
|
||||
properties_map.clear();
|
||||
existing_folders.clear();
|
||||
|
||||
folder_map.clear();
|
||||
local_only.clear();
|
||||
open = false;
|
||||
|
||||
|
|
@ -158,38 +154,38 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
Gee.ArrayList<ImapDB.Folder> folders_to_build = new Gee.ArrayList<ImapDB.Folder>();
|
||||
Gee.ArrayList<GenericFolder> built_folders = new Gee.ArrayList<GenericFolder>();
|
||||
Gee.ArrayList<GenericFolder> return_folders = new Gee.ArrayList<GenericFolder>();
|
||||
|
||||
|
||||
foreach(ImapDB.Folder local_folder in local_folders) {
|
||||
if (existing_folders.has_key(local_folder.get_path()))
|
||||
return_folders.add(existing_folders.get(local_folder.get_path()));
|
||||
if (folder_map.has_key(local_folder.get_path()))
|
||||
return_folders.add(folder_map.get(local_folder.get_path()));
|
||||
else
|
||||
folders_to_build.add(local_folder);
|
||||
}
|
||||
|
||||
|
||||
foreach(ImapDB.Folder folder_to_build in folders_to_build) {
|
||||
GenericFolder folder = new_folder(folder_to_build.get_path(), remote, local, folder_to_build);
|
||||
existing_folders.set(folder.get_path(), folder);
|
||||
folder_map.set(folder.get_path(), folder);
|
||||
built_folders.add(folder);
|
||||
return_folders.add(folder);
|
||||
}
|
||||
|
||||
|
||||
if (built_folders.size > 0)
|
||||
notify_folders_available_unavailable(built_folders, null);
|
||||
notify_folders_available_unavailable(sort_by_path(built_folders), null);
|
||||
|
||||
return return_folders;
|
||||
}
|
||||
|
||||
public override Gee.Collection<Geary.Folder> list_matching_folders(
|
||||
Geary.FolderPath? parent) throws Error {
|
||||
public override Gee.Collection<Geary.Folder> list_matching_folders(Geary.FolderPath? parent)
|
||||
throws Error {
|
||||
check_open();
|
||||
|
||||
Gee.ArrayList<Geary.Folder> matches = new Gee.ArrayList<Geary.Folder>();
|
||||
|
||||
foreach(FolderPath path in existing_folders.keys) {
|
||||
|
||||
foreach(FolderPath path in folder_map.keys) {
|
||||
FolderPath? path_parent = path.get_parent();
|
||||
if ((parent == null && path_parent == null) ||
|
||||
(parent != null && path_parent != null && path_parent.equal_to(parent))) {
|
||||
matches.add(existing_folders.get(path));
|
||||
matches.add(folder_map.get(path));
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
|
|
@ -224,7 +220,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
|
||||
private bool on_refresh_folders() {
|
||||
in_refresh_enumerate = true;
|
||||
enumerate_folders_async.begin(null, refresh_cancellable, on_refresh_completed);
|
||||
enumerate_folders_async.begin(refresh_cancellable, on_refresh_completed);
|
||||
|
||||
refresh_folder_timeout_id = 0;
|
||||
|
||||
|
|
@ -243,29 +239,81 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
reschedule_folder_refresh(false);
|
||||
}
|
||||
|
||||
private async void enumerate_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null)
|
||||
throws Error {
|
||||
private async void enumerate_folders_async(Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
Gee.Collection<ImapDB.Folder>? local_list = null;
|
||||
// get all local folders
|
||||
Gee.HashMap<FolderPath, ImapDB.Folder> local_children = yield enumerate_local_folders_async(null,
|
||||
cancellable);
|
||||
|
||||
// convert to a list of Geary.Folder ... build_folder() also reports new folders, so this
|
||||
// gets the word out quickly
|
||||
Gee.Collection<Geary.Folder> existing_list = new Gee.ArrayList<Geary.Folder>();
|
||||
existing_list.add_all(build_folders(local_children.values));
|
||||
existing_list.add_all(local_only.values);
|
||||
|
||||
Gee.HashMap<FolderPath, Geary.Folder> existing_folders = new Gee.HashMap<FolderPath, Geary.Folder>();
|
||||
foreach (Geary.Folder folder in existing_list)
|
||||
existing_folders.set(folder.get_path(), folder);
|
||||
|
||||
// get all remote (server) folder paths
|
||||
Gee.HashMap<FolderPath, Imap.Folder> remote_folders = yield enumerate_remote_folders_async(null,
|
||||
cancellable);
|
||||
|
||||
// combine the two and make sure everything is up-to-date
|
||||
yield update_folders_async(existing_folders, remote_folders, cancellable);
|
||||
}
|
||||
|
||||
private async Gee.HashMap<FolderPath, ImapDB.Folder> enumerate_local_folders_async(
|
||||
Geary.FolderPath? parent, Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
Gee.Collection<ImapDB.Folder>? local_children = null;
|
||||
try {
|
||||
local_list = yield local.list_folders_async(parent, cancellable);
|
||||
local_children = yield local.list_folders_async(parent, cancellable);
|
||||
} catch (EngineError err) {
|
||||
// don't pass on NOT_FOUND's, that means we need to go to the server for more info
|
||||
if (!(err is EngineError.NOT_FOUND))
|
||||
throw err;
|
||||
}
|
||||
|
||||
Gee.Collection<Geary.Folder> engine_list = new Gee.ArrayList<Geary.Folder>();
|
||||
if (local_list != null && local_list.size > 0) {
|
||||
engine_list.add_all(build_folders(local_list));
|
||||
Gee.HashMap<FolderPath, ImapDB.Folder> result = new Gee.HashMap<FolderPath, ImapDB.Folder>();
|
||||
if (local_children != null) {
|
||||
foreach (ImapDB.Folder local_child in local_children) {
|
||||
result.set(local_child.get_path(), local_child);
|
||||
Collection.map_set_all<FolderPath, ImapDB.Folder>(result,
|
||||
yield enumerate_local_folders_async(local_child.get_path(), cancellable));
|
||||
}
|
||||
}
|
||||
|
||||
// Add local folders (assume that local-only folders always go in root)
|
||||
if (parent == null)
|
||||
engine_list.add_all(local_only.values);
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Gee.HashMap<FolderPath, Imap.Folder> enumerate_remote_folders_async(
|
||||
Geary.FolderPath? parent, Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
background_update_folders.begin(parent, engine_list, cancellable);
|
||||
Gee.List<Imap.Folder>? remote_children = null;
|
||||
try {
|
||||
remote_children = yield remote.list_child_folders_async(parent, cancellable);
|
||||
} catch (Error err) {
|
||||
// ignore everything but I/O errors
|
||||
if (err is IOError)
|
||||
throw err;
|
||||
}
|
||||
|
||||
Gee.HashMap<FolderPath, Imap.Folder> result = new Gee.HashMap<FolderPath, Imap.Folder>();
|
||||
if (remote_children != null) {
|
||||
foreach (Imap.Folder remote_child in remote_children) {
|
||||
result.set(remote_child.path, remote_child);
|
||||
if (remote_child.properties.has_children.is_possible()) {
|
||||
Collection.map_set_all<FolderPath, Imap.Folder>(result,
|
||||
yield enumerate_remote_folders_async(remote_child.path, cancellable));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override Geary.ContactStore get_contact_store() {
|
||||
|
|
@ -317,156 +365,98 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
|
|||
return build_folder((ImapDB.Folder) yield local.fetch_folder_async(path, cancellable));
|
||||
}
|
||||
|
||||
private async void background_update_folders(Geary.FolderPath? parent,
|
||||
Gee.Collection<Geary.Folder> engine_folders, Cancellable? cancellable) {
|
||||
Gee.Collection<Geary.Imap.Folder> remote_folders;
|
||||
try {
|
||||
remote_folders = yield remote.list_folders_async(parent, cancellable);
|
||||
} catch (Error remote_error) {
|
||||
debug("Unable to retrieve folder list from server: %s", remote_error.message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private async void update_folders_async(Gee.Map<FolderPath, Geary.Folder> existing_folders,
|
||||
Gee.Map<FolderPath, Imap.Folder> remote_folders, Cancellable? cancellable) {
|
||||
// update all remote folders properties in the local store and active in the system
|
||||
Gee.HashSet<Geary.FolderPath> altered_paths = new Gee.HashSet<Geary.FolderPath>();
|
||||
foreach (Imap.Folder remote_folder in remote_folders) {
|
||||
foreach (Imap.Folder remote_folder in remote_folders.values) {
|
||||
GenericFolder? generic_folder = existing_folders.get(remote_folder.path)
|
||||
as GenericFolder;
|
||||
if (generic_folder == null)
|
||||
continue;
|
||||
|
||||
// only worry about alterations if the remote is openable
|
||||
if (remote_folder.get_properties().is_openable.is_possible()) {
|
||||
ImapDB.Folder? local_folder = null;
|
||||
try {
|
||||
local_folder = yield local.fetch_folder_async(remote_folder.get_path(), cancellable);
|
||||
} catch (Error err) {
|
||||
if (!(err is EngineError.NOT_FOUND)) {
|
||||
debug("Unable to fetch local folder for remote %s: %s", remote_folder.get_path().to_string(),
|
||||
err.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (local_folder != null) {
|
||||
if (remote_folder.get_properties().have_contents_changed(local_folder.get_properties()).is_possible())
|
||||
altered_paths.add(remote_folder.get_path());
|
||||
}
|
||||
if (remote_folder.properties.is_openable.is_possible()) {
|
||||
ImapDB.Folder local_folder = generic_folder.local_folder;
|
||||
if (remote_folder.properties.have_contents_changed(local_folder.get_properties()).is_possible())
|
||||
altered_paths.add(remote_folder.path);
|
||||
}
|
||||
|
||||
// always update, openable or not
|
||||
try {
|
||||
yield local.update_folder_status_async(remote_folder, cancellable);
|
||||
} catch (Error update_error) {
|
||||
debug("Unable to update local folder %s with remote properties: %s",
|
||||
remote_folder.to_string(), update_error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Get local paths of all engine (local) folders
|
||||
Gee.Set<Geary.FolderPath> local_paths = new Gee.HashSet<Geary.FolderPath>();
|
||||
foreach (Geary.Folder local_folder in engine_folders)
|
||||
local_paths.add(local_folder.get_path());
|
||||
|
||||
// Get remote paths of all remote folders
|
||||
Gee.Set<Geary.FolderPath> remote_paths = new Gee.HashSet<Geary.FolderPath>();
|
||||
foreach (Geary.Imap.Folder remote_folder in remote_folders) {
|
||||
remote_paths.add(remote_folder.get_path());
|
||||
|
||||
// use this iteration to add discovered properties to map
|
||||
properties_map.set(remote_folder.get_path(), remote_folder.get_properties());
|
||||
|
||||
// also use this iteration to set the local folder's special type
|
||||
// set the engine folder's special type
|
||||
// (but only promote, not demote, since getting the special folder type via its
|
||||
// properties relies on the optional XLIST extension)
|
||||
GenericFolder? local_folder = existing_folders.get(remote_folder.get_path());
|
||||
if (local_folder != null && local_folder.get_special_folder_type() == SpecialFolderType.NONE)
|
||||
local_folder.set_special_folder_type(remote_folder.get_properties().attrs.get_special_folder_type());
|
||||
// use this iteration to add discovered properties to map
|
||||
if (generic_folder.get_special_folder_type() == SpecialFolderType.NONE)
|
||||
generic_folder.set_special_folder_type(remote_folder.properties.attrs.get_special_folder_type());
|
||||
}
|
||||
|
||||
// If path in remote but not local, need to add it
|
||||
Gee.List<Geary.Imap.Folder> to_add = new Gee.ArrayList<Geary.Imap.Folder>();
|
||||
foreach (Geary.Imap.Folder folder in remote_folders) {
|
||||
if (!local_paths.contains(folder.get_path()))
|
||||
to_add.add(folder);
|
||||
Gee.List<Imap.Folder>? to_add = new Gee.ArrayList<Imap.Folder>();
|
||||
foreach (Imap.Folder remote_folder in remote_folders.values) {
|
||||
if (!existing_folders.has_key(remote_folder.path))
|
||||
to_add.add(remote_folder);
|
||||
}
|
||||
|
||||
// If path in local but not remote (and isn't local-only, i.e. the Outbox), need to remove
|
||||
// it
|
||||
Gee.List<Geary.Folder>? to_remove = new Gee.ArrayList<Geary.Imap.Folder>();
|
||||
foreach (Geary.Folder folder in engine_folders) {
|
||||
if (!remote_paths.contains(folder.get_path()) && !local_only.keys.contains(folder.get_path()))
|
||||
to_remove.add(folder);
|
||||
// If path in local but not remote (and isn't local-only, i.e. the Outbox), need to remove it
|
||||
Gee.List<Geary.Folder>? to_remove = new Gee.ArrayList<Geary.Folder>();
|
||||
foreach (Geary.FolderPath existing_path in existing_folders.keys) {
|
||||
if (!remote_folders.has_key(existing_path) && !local_only.has_key(existing_path))
|
||||
to_remove.add(existing_folders.get(existing_path));
|
||||
}
|
||||
|
||||
if (to_add.size == 0)
|
||||
to_add = null;
|
||||
|
||||
if (to_remove.size == 0)
|
||||
to_remove = null;
|
||||
|
||||
// For folders to add, clone them and their properties locally
|
||||
if (to_add != null) {
|
||||
foreach (Geary.Imap.Folder folder in to_add) {
|
||||
try {
|
||||
yield local.clone_folder_async(folder, cancellable);
|
||||
} catch (Error err) {
|
||||
debug("Unable to add/remove folder %s: %s", folder.get_path().to_string(),
|
||||
err.message);
|
||||
}
|
||||
foreach (Geary.Imap.Folder remote_folder in to_add) {
|
||||
try {
|
||||
yield local.clone_folder_async(remote_folder, cancellable);
|
||||
} catch (Error err) {
|
||||
debug("Unable to add/remove folder %s to local store: %s", remote_folder.path.to_string(),
|
||||
err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Create Geary.Folder objects for all added folders
|
||||
Gee.Collection<Geary.Folder> engine_added = null;
|
||||
if (to_add != null) {
|
||||
engine_added = new Gee.ArrayList<Geary.Folder>();
|
||||
|
||||
Gee.ArrayList<ImapDB.Folder> folders_to_build = new Gee.ArrayList<ImapDB.Folder>();
|
||||
foreach (Geary.Imap.Folder remote_folder in to_add) {
|
||||
try {
|
||||
folders_to_build.add((ImapDB.Folder) yield local.fetch_folder_async(
|
||||
remote_folder.get_path(), cancellable));
|
||||
} catch (Error convert_err) {
|
||||
// This isn't fatal, but irksome ... in the future, when local folders are
|
||||
// removed, it's possible for one to disappear between cloning it and fetching
|
||||
// it
|
||||
debug("Unable to fetch local folder after cloning: %s", convert_err.message);
|
||||
}
|
||||
Gee.ArrayList<ImapDB.Folder> folders_to_build = new Gee.ArrayList<ImapDB.Folder>();
|
||||
foreach (Geary.Imap.Folder remote_folder in to_add) {
|
||||
try {
|
||||
folders_to_build.add(yield local.fetch_folder_async(remote_folder.path, cancellable));
|
||||
} catch (Error convert_err) {
|
||||
// This isn't fatal, but irksome ... in the future, when local folders are
|
||||
// removed, it's possible for one to disappear between cloning it and fetching
|
||||
// it
|
||||
debug("Unable to fetch local folder after cloning: %s", convert_err.message);
|
||||
}
|
||||
|
||||
engine_added.add_all(build_folders(folders_to_build));
|
||||
}
|
||||
Gee.Collection<Geary.Folder> engine_added = new Gee.ArrayList<Geary.Folder>();
|
||||
engine_added.add_all(build_folders(folders_to_build));
|
||||
|
||||
// TODO: Remove local folders no longer available remotely.
|
||||
if (to_remove != null) {
|
||||
foreach (Geary.Folder folder in to_remove) {
|
||||
debug(@"Need to remove folder $folder");
|
||||
}
|
||||
}
|
||||
foreach (Geary.Folder folder in to_remove)
|
||||
debug(@"Need to remove folder $folder");
|
||||
|
||||
if (engine_added != null)
|
||||
notify_folders_added_removed(engine_added, null);
|
||||
if (engine_added.size > 0)
|
||||
notify_folders_added_removed(sort_by_path(engine_added), null);
|
||||
|
||||
// report all altered folders
|
||||
if (altered_paths.size > 0) {
|
||||
Gee.ArrayList<Geary.Folder> altered = new Gee.ArrayList<Geary.Folder>();
|
||||
foreach (Geary.FolderPath path in altered_paths) {
|
||||
if (existing_folders.has_key(path))
|
||||
altered.add(existing_folders.get(path));
|
||||
foreach (Geary.FolderPath altered_path in altered_paths) {
|
||||
if (existing_folders.has_key(altered_path))
|
||||
altered.add(existing_folders.get(altered_path));
|
||||
else
|
||||
debug("Unable to report %s altered: no local representation", path.to_string());
|
||||
debug("Unable to report %s altered: no local representation", altered_path.to_string());
|
||||
}
|
||||
|
||||
if (altered.size > 0)
|
||||
notify_folders_contents_altered(altered);
|
||||
}
|
||||
|
||||
// enumerate children of each remote folder
|
||||
foreach (Imap.Folder remote_folder in remote_folders) {
|
||||
if (remote_folder.get_properties().has_children.is_possible()) {
|
||||
try {
|
||||
yield enumerate_folders_async(remote_folder.get_path(), cancellable);
|
||||
} catch (Error err) {
|
||||
debug("Unable to enumerate children of %s: %s", remote_folder.get_path().to_string(),
|
||||
err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async void send_email_async(Geary.ComposedEmail composed,
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
// - From open remote folder
|
||||
// - Fetch from local store
|
||||
if (remote_folder != null && get_open_state() == OpenState.BOTH)
|
||||
return remote_folder.get_properties();
|
||||
return remote_folder.properties;
|
||||
|
||||
return local_folder.get_properties();
|
||||
}
|
||||
|
|
@ -113,7 +113,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
debug("normalize_folders %s", to_string());
|
||||
|
||||
Geary.Imap.FolderProperties local_properties = local_folder.get_properties();
|
||||
Geary.Imap.FolderProperties remote_properties = remote_folder.get_properties();
|
||||
Geary.Imap.FolderProperties remote_properties = remote_folder.properties;
|
||||
|
||||
// and both must have their next UID's (it's possible they don't if it's a non-selectable
|
||||
// folder)
|
||||
|
|
@ -133,10 +133,20 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
return false;
|
||||
}
|
||||
|
||||
// If UIDVALIDITY changes, all email in the folder must be removed as the UIDs are now
|
||||
// invalid ... we merely detach the emails (leaving their contents behind) so duplicate
|
||||
// detection can fix them up. But once all UIDs are removed, it's must like the next
|
||||
// if case where no earliest UID available, so simply exit.
|
||||
//
|
||||
// see http://tools.ietf.org/html/rfc3501#section-2.3.1.1
|
||||
if (local_properties.uid_validity.value != remote_properties.uid_validity.value) {
|
||||
// TODO: Don't deal with UID validity changes yet
|
||||
error("UID validity changed: %s -> %s", local_properties.uid_validity.value.to_string(),
|
||||
debug("%s UID validity changed, detaching all email: %s -> %s", get_path().to_string(),
|
||||
local_properties.uid_validity.value.to_string(),
|
||||
remote_properties.uid_validity.value.to_string());
|
||||
|
||||
yield local_folder.detach_all_emails_async(cancellable);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// fetch email from earliest email to last to (a) remove any deletions and (b) update
|
||||
|
|
@ -197,11 +207,12 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
}
|
||||
}
|
||||
|
||||
Geary.Email.Field normalization_fields = is_fast_open ? FAST_NORMALIZATION_FIELDS : NORMALIZATION_FIELDS;
|
||||
|
||||
for (;;) {
|
||||
// Get the local emails in the range ... use PARTIAL_OK to ensure all emails are normalized
|
||||
Gee.List<Geary.Email>? old_local = yield local_folder.list_email_by_id_async(
|
||||
current_start_id, NORMALIZATION_CHUNK_COUNT,
|
||||
is_fast_open ? FAST_NORMALIZATION_FIELDS : NORMALIZATION_FIELDS,
|
||||
current_start_id, NORMALIZATION_CHUNK_COUNT, normalization_fields,
|
||||
ImapDB.Folder.ListFlags.PARTIAL_OK, cancellable);
|
||||
|
||||
// verify still open
|
||||
|
|
@ -222,7 +233,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
// Get the remote emails in the range to either add any not known, remove deleted messages,
|
||||
// and update the flags of the remainder
|
||||
Gee.List<Geary.Email>? old_remote = yield remote_folder.list_email_async(msg_set,
|
||||
NORMALIZATION_FIELDS, cancellable);
|
||||
normalization_fields, cancellable);
|
||||
|
||||
// verify still open after I/O
|
||||
check_open("normalize_folders (list remote)");
|
||||
|
|
@ -491,10 +502,11 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
|
||||
private async void open_remote_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable) {
|
||||
try {
|
||||
debug("Opening remote %s", to_string());
|
||||
Imap.Folder folder = (Imap.Folder) yield remote.fetch_folder_async(local_folder.get_path(),
|
||||
debug("Fetching information for remote folder %s", to_string());
|
||||
Imap.Folder folder = yield remote.fetch_folder_async(local_folder.get_path(),
|
||||
cancellable);
|
||||
|
||||
debug("Opening remote folder %s", folder.to_string());
|
||||
yield folder.open_async(cancellable);
|
||||
|
||||
// allow subclasses to examine the opened folder and resolve any vital
|
||||
|
|
@ -504,17 +516,17 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
yield local.update_folder_select_examine_async(folder, cancellable);
|
||||
|
||||
// signals
|
||||
folder.messages_appended.connect(on_remote_messages_appended);
|
||||
folder.message_at_removed.connect(on_remote_message_at_removed);
|
||||
folder.appended.connect(on_remote_appended);
|
||||
folder.removed.connect(on_remote_removed);
|
||||
folder.disconnected.connect(on_remote_disconnected);
|
||||
|
||||
|
||||
// state
|
||||
remote_count = folder.get_email_count();
|
||||
remote_count = folder.properties.email_total;
|
||||
|
||||
// all set; bless the remote folder as opened
|
||||
remote_folder = folder;
|
||||
} else {
|
||||
debug("Unable to prepare remote folder %s: prepare_opened_file() failed", to_string());
|
||||
debug("Unable to prepare remote folder %s: normalize_folders() failed", to_string());
|
||||
notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, null);
|
||||
|
||||
// schedule immediate close
|
||||
|
|
@ -597,8 +609,8 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
}
|
||||
|
||||
if (closing_remote_folder != null) {
|
||||
closing_remote_folder.messages_appended.disconnect(on_remote_messages_appended);
|
||||
closing_remote_folder.message_at_removed.disconnect(on_remote_message_at_removed);
|
||||
closing_remote_folder.appended.disconnect(on_remote_appended);
|
||||
closing_remote_folder.removed.disconnect(on_remote_removed);
|
||||
closing_remote_folder.disconnected.disconnect(on_remote_disconnected);
|
||||
|
||||
// to avoid keeping the caller waiting while the remote end closes, close it in the
|
||||
|
|
@ -652,8 +664,8 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
remote_semaphore.notify_result(false, null);
|
||||
}
|
||||
|
||||
private void on_remote_messages_appended(int total) {
|
||||
debug("on_remote_messages_appended: total=%d", total);
|
||||
private void on_remote_appended(int total) {
|
||||
debug("on_remote_appended: total=%d", total);
|
||||
replay_queue.schedule(new ReplayAppend(this, total));
|
||||
}
|
||||
|
||||
|
|
@ -668,6 +680,12 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
debug("do_replay_appended_messages %s: remote_count=%d new_remote_count=%d", to_string(),
|
||||
remote_count, new_remote_count);
|
||||
|
||||
if (new_remote_count == remote_count) {
|
||||
debug("do_replay_appended_messages %s: no messages appended", to_string());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Gee.HashSet<Geary.EmailIdentifier> created = new Gee.HashSet<Geary.EmailIdentifier>();
|
||||
Gee.HashSet<Geary.EmailIdentifier> appended = new Gee.HashSet<Geary.EmailIdentifier>();
|
||||
|
||||
|
|
@ -682,8 +700,8 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
|
||||
// normalize starting at the message *after* the highest position of the local store,
|
||||
// which has now changed
|
||||
Imap.MessageSet msg_set = new Imap.MessageSet.range_by_first_last(remote_count + 1,
|
||||
new_remote_count);
|
||||
Imap.MessageSet msg_set = new Imap.MessageSet.range_by_first_last(
|
||||
new Imap.SequenceNumber(remote_count + 1), new Imap.SequenceNumber(new_remote_count));
|
||||
Gee.List<Geary.Email>? list = yield remote_folder.list_email_async(
|
||||
msg_set, ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION, null);
|
||||
if (list != null && list.size > 0) {
|
||||
|
|
@ -715,7 +733,6 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
}
|
||||
|
||||
// save new remote count internally and in local store
|
||||
bool changed = (remote_count != new_remote_count);
|
||||
remote_count = new_remote_count;
|
||||
try {
|
||||
yield local_folder.update_remote_selected_message_count(remote_count, null);
|
||||
|
|
@ -735,9 +752,9 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
debug("do_replay_appended_messages: completed for %s", to_string());
|
||||
}
|
||||
|
||||
private void on_remote_message_at_removed(int position, int total) {
|
||||
debug("on_remote_message_at_removed: position=%d total=%d", position, total);
|
||||
replay_queue.schedule(new ReplayRemoval(this, position, total));
|
||||
private void on_remote_removed(int pos, int total) {
|
||||
debug("on_remote_removed: position=%d total=%d", pos, total);
|
||||
replay_queue.schedule(new ReplayRemoval(this, pos, total));
|
||||
}
|
||||
|
||||
// This MUST only be called from ReplayRemoval.
|
||||
|
|
@ -817,14 +834,16 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
marked.to_string());
|
||||
}
|
||||
|
||||
private void on_remote_disconnected(Geary.Folder.CloseReason reason) {
|
||||
private void on_remote_disconnected(Imap.ClientSession.DisconnectReason reason) {
|
||||
debug("on_remote_disconnected: reason=%s", reason.to_string());
|
||||
replay_queue.schedule(new ReplayDisconnect(this, reason));
|
||||
}
|
||||
|
||||
internal async void do_replay_remote_disconnected(Geary.Folder.CloseReason reason) {
|
||||
internal async void do_replay_remote_disconnected(Imap.ClientSession.DisconnectReason reason) {
|
||||
debug("do_replay_remote_disconnected reason=%s", reason.to_string());
|
||||
assert(reason == CloseReason.REMOTE_CLOSE || reason == CloseReason.REMOTE_ERROR);
|
||||
|
||||
Geary.Folder.CloseReason folder_reason = reason.is_error()
|
||||
? Geary.Folder.CloseReason.REMOTE_ERROR : Geary.Folder.CloseReason.REMOTE_CLOSE;
|
||||
|
||||
// because close_internal_async() issues ReceiveReplayQueue.close_async() (which cannot
|
||||
// be called from within a ReceiveReplayOperation), schedule the close rather than
|
||||
|
|
@ -832,7 +851,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
// the situation, it may not yield until it attempts to close the ReceiveReplayQueue,
|
||||
// which is the problem we're attempting to work around
|
||||
Idle.add(() => {
|
||||
close_internal_async.begin(CloseReason.LOCAL_CLOSE, reason, null);
|
||||
close_internal_async.begin(CloseReason.LOCAL_CLOSE, folder_reason, null);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
|
@ -1139,7 +1158,7 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde
|
|||
// Normalize the local folder by fetching EmailIdentifiers for all missing email as well
|
||||
// as fields for duplicate detection
|
||||
Gee.List<Geary.Email>? list = yield remote_folder.list_email_async(
|
||||
new Imap.MessageSet.range_by_count(high, prefetch_count),
|
||||
new Imap.MessageSet.range_by_count(new Imap.SequenceNumber(high), prefetch_count),
|
||||
ImapDB.Folder.REQUIRED_FOR_DUPLICATE_DETECTION, cancellable);
|
||||
if (list == null || list.size != prefetch_count) {
|
||||
throw new EngineError.BAD_PARAMETERS("Unable to prefetch %d email starting at %d in %s",
|
||||
|
|
|
|||
|
|
@ -301,12 +301,16 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
|
|||
break;
|
||||
}
|
||||
|
||||
// ReplayClose means this queue (and the folder) are closing, so handle errors a little
|
||||
// differently
|
||||
bool is_close_op = op is ReplayClose;
|
||||
|
||||
// wait until the remote folder is opened (or returns false, in which case closed)
|
||||
bool folder_opened = false;
|
||||
try {
|
||||
if (yield remote_reporting_semaphore.wait_for_result_async())
|
||||
folder_opened = true;
|
||||
else
|
||||
else if (!is_close_op)
|
||||
debug("Folder %s closed or failed to open, remote replay queue closing", to_string());
|
||||
} catch (Error remote_err) {
|
||||
debug("Error for remote queue waiting for remote %s to open, remote queue closing: %s", to_string(),
|
||||
|
|
@ -315,7 +319,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
|
|||
// fall through
|
||||
}
|
||||
|
||||
if (op is ReplayClose)
|
||||
if (is_close_op)
|
||||
queue_running = false;
|
||||
|
||||
remotely_executing(op);
|
||||
|
|
@ -331,11 +335,11 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
|
|||
|
||||
remote_err = replay_err;
|
||||
}
|
||||
} else {
|
||||
} else if (!is_close_op) {
|
||||
remote_err = new EngineError.SERVER_UNAVAILABLE("Folder %s not available", to_string());
|
||||
}
|
||||
|
||||
bool has_failed = (status == ReplayOperation.Status.FAILED);
|
||||
bool has_failed = !is_close_op && (status == ReplayOperation.Status.FAILED);
|
||||
|
||||
// COMPLETED == CONTINUE, only FAILED or exception of interest here
|
||||
if (remote_err != null || has_failed) {
|
||||
|
|
|
|||
|
|
@ -318,9 +318,11 @@ private class Geary.ImapEngine.ListEmail : Geary.ImapEngine.SendReplayOperation
|
|||
list = needed_by_position;
|
||||
}
|
||||
|
||||
Imap.SequenceNumber[] seq_list = Imap.SequenceNumber.to_list(list);
|
||||
|
||||
// pull from server
|
||||
Gee.List<Geary.Email>? remote_list = yield engine.remote_folder.list_email_async(
|
||||
new Imap.MessageSet.sparse(list), required_fields, cancellable);
|
||||
new Imap.MessageSet.sparse(seq_list), required_fields, cancellable);
|
||||
if (remote_list == null || remote_list.size == 0)
|
||||
break;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
private class Geary.ImapEngine.ReplayDisconnect : Geary.ImapEngine.ReceiveReplayOperation {
|
||||
public GenericFolder owner;
|
||||
public Geary.Folder.CloseReason reason;
|
||||
public Imap.ClientSession.DisconnectReason reason;
|
||||
|
||||
public ReplayDisconnect(GenericFolder owner, Geary.Folder.CloseReason reason) {
|
||||
public ReplayDisconnect(GenericFolder owner, Imap.ClientSession.DisconnectReason reason) {
|
||||
base ("Disconnect");
|
||||
|
||||
this.owner = owner;
|
||||
|
|
|
|||
|
|
@ -40,16 +40,12 @@ private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount {
|
|||
if (special_map == null) {
|
||||
special_map = new Gee.HashMap<Geary.FolderPath, Geary.SpecialFolderType>();
|
||||
|
||||
special_map.set(new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR, false),
|
||||
special_map.set(new Geary.FolderRoot(Imap.Account.INBOX_NAME, null, false),
|
||||
Geary.SpecialFolderType.INBOX);
|
||||
special_map.set(new Geary.FolderRoot("Sent", Imap.Account.ASSUMED_SEPARATOR, false),
|
||||
Geary.SpecialFolderType.SENT);
|
||||
special_map.set(new Geary.FolderRoot("Draft", Imap.Account.ASSUMED_SEPARATOR, false),
|
||||
Geary.SpecialFolderType.DRAFTS);
|
||||
special_map.set(new Geary.FolderRoot("Bulk Mail", Imap.Account.ASSUMED_SEPARATOR, false),
|
||||
Geary.SpecialFolderType.SPAM);
|
||||
special_map.set(new Geary.FolderRoot("Trash", Imap.Account.ASSUMED_SEPARATOR, false),
|
||||
Geary.SpecialFolderType.TRASH);
|
||||
special_map.set(new Geary.FolderRoot("Sent", null, false), Geary.SpecialFolderType.SENT);
|
||||
special_map.set(new Geary.FolderRoot("Draft", null, false), Geary.SpecialFolderType.DRAFTS);
|
||||
special_map.set(new Geary.FolderRoot("Bulk Mail", null, false), Geary.SpecialFolderType.SPAM);
|
||||
special_map.set(new Geary.FolderRoot("Trash", null, false), Geary.SpecialFolderType.TRASH);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,138 +4,385 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Provides an interface into the IMAP stack that provides a simpler interface for a
|
||||
* Geary.Account implementation.
|
||||
*
|
||||
* Because of the complexities of the IMAP protocol, this private class takes common operations
|
||||
* that a Geary.Account implementation would need (in particular, {@link Geary.ImapEngine.Account}
|
||||
* and makes them into simple async calls.
|
||||
*
|
||||
* Geary.Imap.Account does __no__ management of the {@link Imap.Folder} objects it returns. Thus,
|
||||
* calling a fetch or list operation several times in a row will return separate Folder objects
|
||||
* each time. It is up to the higher layers of the stack to manage these objects.
|
||||
*/
|
||||
|
||||
private class Geary.Imap.Account : BaseObject {
|
||||
// all references to Inbox are converted to this string, purely for sanity sake when dealing
|
||||
// with Inbox's case issues
|
||||
public const string INBOX_NAME = "INBOX";
|
||||
public const string ASSUMED_SEPARATOR = "/";
|
||||
|
||||
public signal void email_sent(Geary.RFC822.Message rfc822);
|
||||
|
||||
private class StatusOperation : Geary.Nonblocking.BatchOperation {
|
||||
public ClientSessionManager session_mgr;
|
||||
public MailboxInformation mbox;
|
||||
public Geary.FolderPath path;
|
||||
|
||||
public StatusOperation(ClientSessionManager session_mgr, MailboxInformation mbox,
|
||||
Geary.FolderPath path) {
|
||||
this.session_mgr = session_mgr;
|
||||
this.mbox = mbox;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public override async Object? execute_async(Cancellable? cancellable) throws Error {
|
||||
return yield session_mgr.status_async(path.get_fullpath(), StatusDataType.all(), cancellable);
|
||||
}
|
||||
}
|
||||
public bool is_open { get; private set; default = false; }
|
||||
|
||||
private string name;
|
||||
private AccountInformation account_information;
|
||||
private ClientSessionManager session_mgr;
|
||||
private Gee.HashMap<string, string?> delims = new Gee.HashMap<string, string?>();
|
||||
private ClientSession? account_session = null;
|
||||
private Nonblocking.Mutex account_session_mutex = new Nonblocking.Mutex();
|
||||
private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex();
|
||||
private Gee.List<MailboxInformation>? list_collector = null;
|
||||
private Gee.List<StatusData>? status_collector = null;
|
||||
|
||||
public signal void email_sent(Geary.RFC822.Message rfc822);
|
||||
|
||||
public signal void login_failed(Geary.Credentials cred);
|
||||
|
||||
public Account(Geary.AccountInformation account_information) {
|
||||
name = "IMAP Account for %s".printf(account_information.imap_credentials.to_string());
|
||||
this.account_information = account_information;
|
||||
this.session_mgr = new ClientSessionManager(account_information);
|
||||
|
||||
session_mgr = new ClientSessionManager(account_information);
|
||||
session_mgr.login_failed.connect(on_login_failed);
|
||||
}
|
||||
|
||||
public async void open_async(Cancellable? cancellable) throws Error {
|
||||
private void check_open() throws Error {
|
||||
if (!is_open)
|
||||
throw new EngineError.OPEN_REQUIRED("Imap.Account not open");
|
||||
}
|
||||
|
||||
public async void open_async(Cancellable? cancellable = null) throws Error {
|
||||
if (is_open)
|
||||
throw new EngineError.ALREADY_OPEN("Imap.Account already open");
|
||||
|
||||
yield session_mgr.open_async(cancellable);
|
||||
|
||||
is_open = true;
|
||||
}
|
||||
|
||||
public async void close_async(Cancellable? cancellable) throws Error {
|
||||
yield session_mgr.close_async(cancellable);
|
||||
}
|
||||
|
||||
public async Gee.Collection<Geary.Imap.Folder> list_folders_async(Geary.FolderPath? parent,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
Geary.FolderPath? processed = process_path(parent, null,
|
||||
(parent != null) ? parent.get_root().default_separator : ASSUMED_SEPARATOR);
|
||||
public async void close_async(Cancellable? cancellable = null) throws Error {
|
||||
if (!is_open)
|
||||
return;
|
||||
|
||||
Gee.Collection<MailboxInformation> mboxes;
|
||||
try {
|
||||
mboxes = (processed == null)
|
||||
? yield session_mgr.list_roots(cancellable)
|
||||
: yield session_mgr.list(processed.get_fullpath(), processed.get_root().default_separator,
|
||||
cancellable);
|
||||
} catch (Error err) {
|
||||
if (err is ImapError.SERVER_ERROR)
|
||||
throw_not_found(parent);
|
||||
else
|
||||
throw err;
|
||||
}
|
||||
int token = yield account_session_mutex.claim_async(cancellable);
|
||||
|
||||
Gee.Collection<Geary.Imap.Folder> folders = new Gee.ArrayList<Geary.Imap.Folder>();
|
||||
|
||||
Geary.Nonblocking.Batch batch = new Geary.Nonblocking.Batch();
|
||||
foreach (MailboxInformation mbox in mboxes) {
|
||||
Geary.FolderPath path = process_path(processed, mbox.get_basename(), mbox.delim);
|
||||
|
||||
// only add to delims map if root-level folder (all sub-folders respect its delimiter)
|
||||
// also use the processed name, not the one reported off the wire
|
||||
if (processed == null)
|
||||
delims.set(path.get_root().basename, mbox.delim);
|
||||
|
||||
if (!mbox.attrs.contains(MailboxAttribute.NO_SELECT))
|
||||
batch.add(new StatusOperation(session_mgr, mbox, path));
|
||||
else
|
||||
folders.add(new Geary.Imap.Folder.unselectable(session_mgr, path, mbox));
|
||||
}
|
||||
|
||||
yield batch.execute_all_async(cancellable);
|
||||
|
||||
foreach (int id in batch.get_ids()) {
|
||||
StatusOperation op = (StatusOperation) batch.get_operation(id);
|
||||
ClientSession? dropped = drop_session();
|
||||
if (dropped != null) {
|
||||
try {
|
||||
folders.add(new Geary.Imap.Folder(session_mgr, op.path,
|
||||
(StatusResults) batch.get_result(id), op.mbox));
|
||||
} catch (Error status_err) {
|
||||
message("Unable to fetch status for %s: %s", op.path.to_string(), status_err.message);
|
||||
yield session_mgr.release_session_async(dropped, cancellable);
|
||||
} catch (Error err) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return folders;
|
||||
}
|
||||
|
||||
public async bool folder_exists_async(Geary.FolderPath path, Cancellable? cancellable = null)
|
||||
throws Error {
|
||||
Geary.FolderPath? processed = process_path(path, null, path.get_root().default_separator);
|
||||
if (processed == null)
|
||||
throw new ImapError.INVALID_PATH("Invalid path %s", path.to_string());
|
||||
|
||||
return yield session_mgr.folder_exists_async(processed.get_fullpath(), cancellable);
|
||||
}
|
||||
|
||||
public async Geary.Imap.Folder fetch_folder_async(Geary.FolderPath path,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
Geary.FolderPath? processed = process_path(path, null, path.get_root().default_separator);
|
||||
if (processed == null)
|
||||
throw new ImapError.INVALID_PATH("Invalid path %s", path.to_string());
|
||||
try {
|
||||
account_session_mutex.release(ref token);
|
||||
} catch (Error err) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
try {
|
||||
MailboxInformation? mbox = yield session_mgr.fetch_async(processed.get_fullpath(),
|
||||
cancellable);
|
||||
if (mbox == null)
|
||||
throw_not_found(path);
|
||||
|
||||
if (mbox.attrs.contains(MailboxAttribute.NO_SELECT))
|
||||
return new Geary.Imap.Folder.unselectable(session_mgr, processed, mbox);
|
||||
|
||||
StatusResults status = yield session_mgr.status_async(processed.get_fullpath(),
|
||||
StatusDataType.all(), cancellable);
|
||||
|
||||
return new Geary.Imap.Folder(session_mgr, processed, status, mbox);
|
||||
} catch (ImapError err) {
|
||||
if (err is ImapError.SERVER_ERROR)
|
||||
throw_not_found(path);
|
||||
else
|
||||
throw err;
|
||||
yield session_mgr.close_async(cancellable);
|
||||
} catch (Error err) {
|
||||
// ignored
|
||||
}
|
||||
|
||||
is_open = false;
|
||||
}
|
||||
|
||||
// Claiming session in open_async() would delay opening, which make take too long ... rather,
|
||||
// this is used by the various calls to put off claiming a session until needed (which
|
||||
// possibly is long enough for ClientSessionManager to get a few ready).
|
||||
private async ClientSession claim_session_async(Cancellable? cancellable) throws Error {
|
||||
int token = yield account_session_mutex.claim_async(cancellable);
|
||||
|
||||
Error? err = null;
|
||||
if (account_session == null) {
|
||||
try {
|
||||
account_session = yield session_mgr.claim_authorized_session_async(cancellable);
|
||||
|
||||
account_session.list.connect(on_list_data);
|
||||
account_session.status.connect(on_status_data);
|
||||
account_session.disconnected.connect(on_disconnected);
|
||||
} catch (Error claim_err) {
|
||||
err = claim_err;
|
||||
}
|
||||
}
|
||||
|
||||
account_session_mutex.release(ref token);
|
||||
|
||||
if (err != null)
|
||||
throw err;
|
||||
|
||||
return account_session;
|
||||
}
|
||||
|
||||
// Can be called locked or unlocked, but only unlocked if you know what you're doing -- i.e.
|
||||
// not yielding.
|
||||
private ClientSession? drop_session() {
|
||||
if (account_session == null)
|
||||
return null;
|
||||
|
||||
account_session.list.disconnect(on_list_data);
|
||||
account_session.status.disconnect(on_status_data);
|
||||
account_session.disconnected.disconnect(on_disconnected);
|
||||
|
||||
ClientSession dropped = account_session;
|
||||
account_session = null;
|
||||
|
||||
return dropped;
|
||||
}
|
||||
|
||||
private void on_list_data(MailboxInformation mailbox_info) {
|
||||
if (list_collector != null)
|
||||
list_collector.add(mailbox_info);
|
||||
}
|
||||
|
||||
private void on_status_data(StatusData status_data) {
|
||||
if (status_collector != null)
|
||||
status_collector.add(status_data);
|
||||
}
|
||||
|
||||
private void on_disconnected() {
|
||||
drop_session();
|
||||
}
|
||||
|
||||
public async bool folder_exists_async(FolderPath path, Cancellable? cancellable) throws Error {
|
||||
try {
|
||||
yield fetch_mailbox_async(path, cancellable);
|
||||
|
||||
return true;
|
||||
} catch (Error err) {
|
||||
if (err is IOError.CANCELLED)
|
||||
throw err;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Imap.Folder fetch_folder_async(FolderPath path, Cancellable? cancellable)
|
||||
throws Error {
|
||||
check_open();
|
||||
|
||||
MailboxInformation mailbox_info = yield fetch_mailbox_async(path, cancellable);
|
||||
|
||||
if (!mailbox_info.attrs.contains(MailboxAttribute.NO_SELECT)) {
|
||||
StatusData status = yield fetch_status_async(path, cancellable);
|
||||
|
||||
return new Imap.Folder(session_mgr, status, mailbox_info);
|
||||
} else {
|
||||
return new Imap.Folder.unselectable(session_mgr, mailbox_info);
|
||||
}
|
||||
}
|
||||
|
||||
private async MailboxInformation fetch_mailbox_async(FolderPath path, Cancellable? cancellable)
|
||||
throws Error {
|
||||
Geary.FolderPath? processed = normalize_inbox(path);
|
||||
if (processed == null)
|
||||
throw new ImapError.INVALID("Invalid path %s", path.to_string());
|
||||
|
||||
ClientSession session = yield claim_session_async(cancellable);
|
||||
bool can_xlist = session.capabilities.has_capability(Capabilities.XLIST);
|
||||
|
||||
Gee.List<MailboxInformation> list_results = new Gee.ArrayList<MailboxInformation>();
|
||||
StatusResponse response = yield send_command_async(
|
||||
new ListCommand(new MailboxSpecifier.from_folder_path(processed, null), can_xlist),
|
||||
list_results, null, cancellable);
|
||||
|
||||
throw_fetch_error(response, processed, list_results.size);
|
||||
|
||||
return list_results[0];
|
||||
}
|
||||
|
||||
private async StatusData fetch_status_async(FolderPath path, Cancellable? cancellable)
|
||||
throws Error {
|
||||
check_open();
|
||||
|
||||
Geary.FolderPath? processed = normalize_inbox(path);
|
||||
if (processed == null)
|
||||
throw new ImapError.INVALID("Invalid path %s", path.to_string());
|
||||
|
||||
Gee.List<StatusData> status_results = new Gee.ArrayList<StatusData>();
|
||||
StatusResponse response = yield send_command_async(
|
||||
new StatusCommand(new MailboxSpecifier.from_folder_path(processed, null), StatusDataType.all()),
|
||||
null, status_results, cancellable);
|
||||
|
||||
throw_fetch_error(response, processed, status_results.size);
|
||||
|
||||
return status_results[0];
|
||||
}
|
||||
|
||||
private void throw_fetch_error(StatusResponse response, FolderPath path, int result_count)
|
||||
throws Error {
|
||||
assert(response.is_completion);
|
||||
|
||||
if (response.status != Status.OK) {
|
||||
throw new ImapError.SERVER_ERROR("Server reports error for path %s: %s", path.to_string(),
|
||||
response.to_string());
|
||||
}
|
||||
|
||||
if (result_count != 1) {
|
||||
throw new ImapError.INVALID("Server reports %d results for fetch of path %s: %s",
|
||||
result_count, path.to_string(), response.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
public async Gee.List<Imap.Folder>? list_child_folders_async(FolderPath? parent, Cancellable? cancellable)
|
||||
throws Error {
|
||||
check_open();
|
||||
|
||||
Geary.FolderPath? processed = normalize_inbox(parent);
|
||||
|
||||
Gee.List<MailboxInformation>? child_info = yield list_children_async(processed, cancellable);
|
||||
if (child_info == null || child_info.size == 0)
|
||||
return null;
|
||||
|
||||
Gee.List<Imap.Folder> child_folders = new Gee.ArrayList<Imap.Folder>();
|
||||
|
||||
Gee.Map<MailboxSpecifier, MailboxInformation> info_map = new Gee.HashMap<
|
||||
MailboxSpecifier, MailboxInformation>();
|
||||
Gee.Map<StatusCommand, MailboxSpecifier> cmd_map = new Gee.HashMap<
|
||||
StatusCommand, MailboxSpecifier>();
|
||||
foreach (MailboxInformation mailbox_info in child_info) {
|
||||
if (mailbox_info.attrs.contains(MailboxAttribute.NO_SELECT)) {
|
||||
child_folders.add(new Imap.Folder.unselectable(session_mgr, mailbox_info));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
info_map.set(mailbox_info.mailbox, mailbox_info);
|
||||
cmd_map.set(new StatusCommand(mailbox_info.mailbox, StatusDataType.all()),
|
||||
mailbox_info.mailbox);
|
||||
}
|
||||
|
||||
Gee.List<StatusData> status_results = new Gee.ArrayList<StatusData>();
|
||||
Gee.Map<Command, StatusResponse> responses = yield send_multiple_async(cmd_map.keys,
|
||||
null, status_results, cancellable);
|
||||
|
||||
foreach (Command cmd in responses.keys) {
|
||||
StatusCommand status_cmd = (StatusCommand) cmd;
|
||||
StatusResponse response = responses.get(cmd);
|
||||
|
||||
MailboxSpecifier mailbox = cmd_map.get(status_cmd);
|
||||
MailboxInformation mailbox_info = info_map.get(mailbox);
|
||||
|
||||
if (response.status != Status.OK) {
|
||||
message("Unable to get STATUS of %s: %s", mailbox.to_string(), response.to_string());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
StatusData? found_status = null;
|
||||
foreach (StatusData status_data in status_results) {
|
||||
if (status_data.mailbox.equal_to(mailbox)) {
|
||||
found_status = status_data;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found_status == null) {
|
||||
message("Unable to get STATUS of %s: not returned from server", mailbox.to_string());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
status_results.remove(found_status);
|
||||
child_folders.add(new Imap.Folder(session_mgr, found_status, mailbox_info));
|
||||
}
|
||||
|
||||
if (status_results.size > 0)
|
||||
debug("%d STATUS results leftover", status_results.size);
|
||||
|
||||
return child_folders;
|
||||
}
|
||||
|
||||
private async Gee.List<MailboxInformation>? list_children_async(FolderPath? parent, Cancellable? cancellable)
|
||||
throws Error {
|
||||
Geary.FolderPath? processed = normalize_inbox(parent);
|
||||
|
||||
ClientSession session = yield claim_session_async(cancellable);
|
||||
bool can_xlist = session.capabilities.has_capability(Capabilities.XLIST);
|
||||
|
||||
ListCommand cmd;
|
||||
if (processed == null) {
|
||||
cmd = new ListCommand.wildcarded("", new MailboxSpecifier("%"), can_xlist);
|
||||
} else {
|
||||
string? specifier = processed.get_fullpath(null);
|
||||
string? delim = processed.get_root().default_separator;
|
||||
if (specifier == null || delim == null) {
|
||||
throw new ImapError.INVALID("Unable to list children of %s: no delimiter specified",
|
||||
processed.to_string());
|
||||
}
|
||||
|
||||
specifier += specifier.has_suffix(delim) ? "%" : (delim + "%");
|
||||
|
||||
cmd = new ListCommand(new MailboxSpecifier(specifier), can_xlist);
|
||||
}
|
||||
|
||||
Gee.List<MailboxInformation> list_results = new Gee.ArrayList<MailboxInformation>();
|
||||
StatusResponse response = yield send_command_async(cmd, list_results, null, cancellable);
|
||||
|
||||
if (response.status != Status.OK)
|
||||
throw_not_found(processed ?? parent);
|
||||
|
||||
// See note at ListCommand about some servers returning the parent's name alongside their
|
||||
// children ... this filters this out
|
||||
if (processed != null) {
|
||||
Gee.Iterator<MailboxInformation> iter = list_results.iterator();
|
||||
while (iter.next()) {
|
||||
FolderPath list_path = iter.get().mailbox.to_folder_path(processed.get_root().default_separator);
|
||||
if (list_path.equal_to(processed)) {
|
||||
debug("Removing parent from LIST results: %s", list_path.to_string());
|
||||
iter.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (list_results.size > 0) ? list_results : null;
|
||||
}
|
||||
|
||||
private async StatusResponse send_command_async(Command cmd,
|
||||
Gee.List<MailboxInformation>? list_results, Gee.List<StatusData>? status_results,
|
||||
Cancellable? cancellable) throws Error {
|
||||
Gee.Map<Command, StatusResponse> responses = yield send_multiple_async(
|
||||
new Geary.Collection.SingleItem<Command>(cmd), list_results, status_results,
|
||||
cancellable);
|
||||
|
||||
assert(responses.size == 1);
|
||||
|
||||
return Geary.Collection.get_first(responses.values);
|
||||
}
|
||||
|
||||
private async Gee.Map<Command, StatusResponse> send_multiple_async(
|
||||
Gee.Collection<Command> cmds, Gee.List<MailboxInformation>? list_results,
|
||||
Gee.List<StatusData>? status_results, Cancellable? cancellable) throws Error {
|
||||
int token = yield cmd_mutex.claim_async(cancellable);
|
||||
|
||||
// set up collectors
|
||||
list_collector = list_results;
|
||||
status_collector = status_results;
|
||||
|
||||
Gee.Map<Command, StatusResponse>? responses = null;
|
||||
Error? err = null;
|
||||
try {
|
||||
ClientSession session = yield claim_session_async(cancellable);
|
||||
responses = yield session.send_multiple_commands_async(cmds, cancellable);
|
||||
} catch (Error send_err) {
|
||||
err = send_err;
|
||||
}
|
||||
|
||||
// disconnect collectors
|
||||
list_collector = null;
|
||||
status_collector = null;
|
||||
|
||||
cmd_mutex.release(ref token);
|
||||
|
||||
if (err != null)
|
||||
throw err;
|
||||
|
||||
assert(responses != null);
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
[NoReturn]
|
||||
|
|
@ -144,38 +391,28 @@ private class Geary.Imap.Account : BaseObject {
|
|||
(path != null) ? path.to_string() : "root", session_mgr.to_string());
|
||||
}
|
||||
|
||||
// This method ensures that Inbox is dealt with in a consistent fashion throughout the
|
||||
// application.
|
||||
private static Geary.FolderPath? process_path(Geary.FolderPath? parent, string? basename,
|
||||
string? delim) throws ImapError {
|
||||
bool empty_basename = String.is_empty(basename);
|
||||
|
||||
// 1. Both null, done
|
||||
if (parent == null && empty_basename)
|
||||
// This method ensures that INBOX is dealt with in a consistent fashion throughout the
|
||||
// application. In IMAP, INBOX is case-insensitive (although there is no specification on
|
||||
// sensitivity of other folders) and must always be recognized as such. Thus, this method
|
||||
// converts all mention of INBOX (or Inbox, or inbox, or InBoX) into a standard string.
|
||||
private static Geary.FolderPath? normalize_inbox(Geary.FolderPath? path) {
|
||||
if (path == null)
|
||||
return null;
|
||||
|
||||
// 2. Parent null but basename not, create FolderRoot for Inbox
|
||||
if (parent == null && !empty_basename && basename.up() == INBOX_NAME)
|
||||
return new Geary.FolderRoot(INBOX_NAME, delim, false);
|
||||
FolderRoot root = path.get_root();
|
||||
if (root.basename.up() != INBOX_NAME)
|
||||
return path;
|
||||
|
||||
// 3. Parent and basename supplied, verify parent is not Inbox, as IMAP does not allow it
|
||||
// to have children
|
||||
if (parent != null && !empty_basename && parent.get_root().basename.up() == INBOX_NAME)
|
||||
throw new ImapError.INVALID_PATH("Inbox may not have children");
|
||||
// create new FolderPath with normalized INBOX at its root
|
||||
FolderPath new_path = new Geary.FolderRoot(INBOX_NAME, root.default_separator,
|
||||
root.case_sensitive);
|
||||
|
||||
// 4. Parent supplied but basename is not; if parent points to Inbox, normalize it
|
||||
if (parent != null && empty_basename && parent.basename.up() == INBOX_NAME)
|
||||
return new Geary.FolderRoot(INBOX_NAME, delim, false);
|
||||
// copy in children starting at 1 (zero is INBOX)
|
||||
Gee.List<string> basenames = path.as_list();
|
||||
for (int ctr = 1; ctr < basenames.size; ctr++)
|
||||
new_path = new_path.get_child(basenames[ctr]);
|
||||
|
||||
// 5. Default behavior: create child of basename or basename as root, otherwise return parent
|
||||
// unmodified
|
||||
if (parent != null && !empty_basename)
|
||||
return parent.get_child(basename);
|
||||
|
||||
if (!empty_basename)
|
||||
return new Geary.FolderRoot(basename, delim, Folder.CASE_SENSITIVE);
|
||||
|
||||
return parent;
|
||||
return new_path;
|
||||
}
|
||||
|
||||
private void on_login_failed() {
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@
|
|||
|
||||
public class Geary.Imap.FolderProperties : Geary.FolderProperties {
|
||||
/**
|
||||
* -1 if the Folder was not opened via SELECT or EXAMINE.
|
||||
* -1 if the Folder was not opened via SELECT or EXAMINE. Updated as EXISTS server data
|
||||
* arrives.
|
||||
*/
|
||||
public int select_examine_messages { get; private set; }
|
||||
/**
|
||||
|
|
@ -71,7 +72,7 @@ public class Geary.Imap.FolderProperties : Geary.FolderProperties {
|
|||
init_flags();
|
||||
}
|
||||
|
||||
public FolderProperties.status(StatusResults status, MailboxAttributes attrs) {
|
||||
public FolderProperties.status(StatusData status, MailboxAttributes attrs) {
|
||||
base (status.messages, status.unseen, Trillian.UNKNOWN, Trillian.UNKNOWN, Trillian.UNKNOWN);
|
||||
|
||||
select_examine_messages = -1;
|
||||
|
|
@ -116,8 +117,6 @@ public class Geary.Imap.FolderProperties : Geary.FolderProperties {
|
|||
}
|
||||
|
||||
private void init_flags() {
|
||||
supports_children = Trillian.from_boolean(!attrs.contains(MailboxAttribute.NO_INFERIORS));
|
||||
|
||||
// \HasNoChildren & \HasChildren are optional attributes (could check for CHILDREN extension,
|
||||
// but unnecessary here)
|
||||
if (attrs.contains(MailboxAttribute.HAS_NO_CHILDREN))
|
||||
|
|
@ -127,6 +126,16 @@ public class Geary.Imap.FolderProperties : Geary.FolderProperties {
|
|||
else
|
||||
has_children = Trillian.UNKNOWN;
|
||||
|
||||
// has_children implies supports_children
|
||||
if (has_children != Trillian.UNKNOWN) {
|
||||
supports_children = has_children;
|
||||
} else {
|
||||
// !supports_children implies !has_children
|
||||
supports_children = Trillian.from_boolean(!attrs.contains(MailboxAttribute.NO_INFERIORS));
|
||||
if (supports_children.is_impossible())
|
||||
has_children = Trillian.FALSE;
|
||||
}
|
||||
|
||||
is_openable = Trillian.from_boolean(!attrs.contains(MailboxAttribute.NO_SELECT));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,164 +7,755 @@
|
|||
private class Geary.Imap.Folder : BaseObject {
|
||||
public const bool CASE_SENSITIVE = true;
|
||||
|
||||
private const Geary.Email.Field BASIC_FETCH_FIELDS = Email.Field.ENVELOPE | Email.Field.DATE
|
||||
| Email.Field.ORIGINATORS | Email.Field.RECEIVERS | Email.Field.REFERENCES
|
||||
| Email.Field.SUBJECT | Email.Field.HEADER;
|
||||
|
||||
public bool is_open { get; private set; default = false; }
|
||||
public FolderPath path { get; private set; }
|
||||
public Imap.FolderProperties properties { get; private set; }
|
||||
public MailboxInformation info { get; private set; }
|
||||
public MessageFlags? permanent_flags { get; private set; default = null; }
|
||||
public Trillian readonly { get; private set; default = Trillian.UNKNOWN; }
|
||||
public Trillian accepts_user_flags { get; private set; default = Trillian.UNKNOWN; }
|
||||
|
||||
private ClientSessionManager session_mgr;
|
||||
private MailboxInformation info;
|
||||
private Geary.FolderPath path;
|
||||
private Imap.FolderProperties properties;
|
||||
private Mailbox? mailbox = null;
|
||||
private ClientSession? session = null;
|
||||
private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex();
|
||||
private Gee.HashMap<SequenceNumber, FetchedData> fetch_accumulator = new Gee.HashMap<
|
||||
SequenceNumber, FetchedData>();
|
||||
|
||||
public signal void messages_appended(int exists);
|
||||
public signal void exists(int total);
|
||||
|
||||
public signal void message_at_removed(int position, int total);
|
||||
public signal void expunge(int position);
|
||||
|
||||
public signal void disconnected(Geary.Folder.CloseReason reason);
|
||||
public signal void fetched(FetchedData fetched_data);
|
||||
|
||||
internal Folder(ClientSessionManager session_mgr, Geary.FolderPath path, StatusResults status,
|
||||
MailboxInformation info) {
|
||||
public signal void recent(int total);
|
||||
|
||||
/**
|
||||
* Fabricated from the IMAP signals and state obtained at open_async().
|
||||
*/
|
||||
public signal void appended(int total);
|
||||
|
||||
/**
|
||||
* Fabricated from the IMAP signals and state obtained at open_async().
|
||||
*/
|
||||
public signal void removed(int pos, int total);
|
||||
|
||||
/**
|
||||
* Note that close_async() still needs to be called after this signal is fired.
|
||||
*/
|
||||
public signal void disconnected(ClientSession.DisconnectReason reason);
|
||||
|
||||
internal Folder(ClientSessionManager session_mgr, StatusData status, MailboxInformation info) {
|
||||
assert(status.mailbox.equal_to(info.mailbox));
|
||||
|
||||
this.session_mgr = session_mgr;
|
||||
this.info = info;
|
||||
this.path = path;
|
||||
path = info.mailbox.to_folder_path(info.delim);
|
||||
|
||||
properties = new Imap.FolderProperties.status(status, info.attrs);
|
||||
}
|
||||
|
||||
internal Folder.unselectable(ClientSessionManager session_mgr, Geary.FolderPath path,
|
||||
MailboxInformation info) {
|
||||
internal Folder.unselectable(ClientSessionManager session_mgr, MailboxInformation info) {
|
||||
this.session_mgr = session_mgr;
|
||||
this.info = info;
|
||||
this.path = path;
|
||||
path = info.mailbox.to_folder_path(info.delim);
|
||||
|
||||
properties = new Imap.FolderProperties(0, 0, 0, null, null, info.attrs);
|
||||
}
|
||||
|
||||
public Geary.FolderPath get_path() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public Geary.Imap.FolderProperties get_properties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
public async void open_async(Cancellable? cancellable = null) throws Error {
|
||||
if (mailbox != null)
|
||||
public async void open_async(Cancellable? cancellable) throws Error {
|
||||
if (is_open)
|
||||
throw new EngineError.ALREADY_OPEN("%s already open", to_string());
|
||||
|
||||
mailbox = yield session_mgr.select_mailbox(path, info.delim, cancellable);
|
||||
fetch_accumulator.clear();
|
||||
|
||||
// connect to signals
|
||||
mailbox.exists_altered.connect(on_exists_altered);
|
||||
mailbox.flags_altered.connect(on_flags_altered);
|
||||
mailbox.expunged.connect(on_expunged);
|
||||
mailbox.disconnected.connect(on_disconnected);
|
||||
session = yield session_mgr.claim_authorized_session_async(cancellable);
|
||||
|
||||
int old_status_messages = properties.status_messages;
|
||||
properties = new Imap.FolderProperties(mailbox.exists, mailbox.recent, properties.unseen,
|
||||
mailbox.uid_validity, mailbox.uid_next, properties.attrs);
|
||||
properties.set_status_message_count(old_status_messages, false);
|
||||
// connect to interesting signals *before* selecting
|
||||
session.exists.connect(on_exists);
|
||||
session.expunge.connect(on_expunge);
|
||||
session.fetch.connect(on_fetch);
|
||||
session.recent.connect(on_recent);
|
||||
session.status_response_received.connect(on_status_response);
|
||||
session.disconnected.connect(on_disconnected);
|
||||
|
||||
StatusResponse response = yield session.select_async(
|
||||
new MailboxSpecifier.from_folder_path(path, info.delim), cancellable);
|
||||
if (response.status != Status.OK) {
|
||||
yield release_session_async(cancellable);
|
||||
|
||||
throw new ImapError.SERVER_ERROR("Unable to SELECT %s: %s", path.to_string(), response.to_string());
|
||||
}
|
||||
|
||||
// if at end of SELECT command accepts_user_flags is still UNKKNOWN, treat as TRUE because,
|
||||
// according to IMAP spec, if PERMANENTFLAGS are not returned, then assume OK
|
||||
if (accepts_user_flags == Trillian.UNKNOWN)
|
||||
accepts_user_flags = Trillian.TRUE;
|
||||
|
||||
is_open = true;
|
||||
}
|
||||
|
||||
public async void close_async(Cancellable? cancellable = null) throws Error {
|
||||
disconnect_mailbox();
|
||||
}
|
||||
|
||||
private void disconnect_mailbox() {
|
||||
if (mailbox == null)
|
||||
public async void close_async(Cancellable? cancellable) throws Error {
|
||||
if (!is_open)
|
||||
return;
|
||||
|
||||
mailbox.exists_altered.disconnect(on_exists_altered);
|
||||
mailbox.flags_altered.disconnect(on_flags_altered);
|
||||
mailbox.expunged.disconnect(on_expunged);
|
||||
mailbox.disconnected.disconnect(on_disconnected);
|
||||
yield release_session_async(cancellable);
|
||||
|
||||
mailbox = null;
|
||||
}
|
||||
|
||||
private void on_exists_altered(int old_exists, int new_exists) {
|
||||
assert(mailbox != null);
|
||||
assert(old_exists != new_exists);
|
||||
fetch_accumulator.clear();
|
||||
|
||||
// only use this signal to notify of additions; removals are handled with the expunged
|
||||
// signal
|
||||
if (new_exists > old_exists)
|
||||
messages_appended(new_exists);
|
||||
}
|
||||
|
||||
private void on_flags_altered(MailboxAttributes flags) {
|
||||
assert(mailbox != null);
|
||||
// TODO: Notify of changes
|
||||
}
|
||||
|
||||
private void on_expunged(MessageNumber expunged, int total) {
|
||||
assert(mailbox != null);
|
||||
readonly = Trillian.UNKNOWN;
|
||||
accepts_user_flags = Trillian.UNKNOWN;
|
||||
|
||||
message_at_removed(expunged.value, total);
|
||||
is_open = false;
|
||||
}
|
||||
|
||||
private void on_disconnected(Geary.Folder.CloseReason reason) {
|
||||
disconnect_mailbox();
|
||||
private async void release_session_async(Cancellable? cancellable) {
|
||||
if (session == null)
|
||||
return;
|
||||
|
||||
session.exists.disconnect(on_exists);
|
||||
session.expunge.disconnect(on_expunge);
|
||||
session.fetch.disconnect(on_fetch);
|
||||
session.recent.disconnect(on_recent);
|
||||
session.status_response_received.disconnect(on_status_response);
|
||||
session.disconnected.disconnect(on_disconnected);
|
||||
|
||||
try {
|
||||
yield session_mgr.release_session_async(session, cancellable);
|
||||
} catch (Error err) {
|
||||
debug("Unable to release session %s: %s", session.to_string(), err.message);
|
||||
}
|
||||
|
||||
session = null;
|
||||
}
|
||||
|
||||
private void on_exists(int total) {
|
||||
debug("%s EXISTS %d", to_string(), total);
|
||||
|
||||
int old_total = properties.select_examine_messages;
|
||||
properties.set_select_examine_message_count(total);
|
||||
|
||||
// don't fire signals until opened
|
||||
if (!is_open)
|
||||
return;
|
||||
|
||||
exists(total);
|
||||
if (old_total < total)
|
||||
appended(total);
|
||||
}
|
||||
|
||||
private void on_expunge(SequenceNumber pos) {
|
||||
debug("%s EXPUNGE %s", to_string(), pos.to_string());
|
||||
|
||||
properties.set_select_examine_message_count(properties.select_examine_messages - 1);
|
||||
|
||||
// don't fire signals until opened
|
||||
if (!is_open)
|
||||
return;
|
||||
|
||||
expunge(pos.value);
|
||||
removed(pos.value, properties.select_examine_messages);
|
||||
}
|
||||
|
||||
private void on_fetch(FetchedData fetched_data) {
|
||||
// add if not found, merge if already received data for this email
|
||||
FetchedData? already_present = fetch_accumulator.get(fetched_data.seq_num);
|
||||
fetch_accumulator.set(fetched_data.seq_num,
|
||||
(already_present != null) ? fetched_data.combine(already_present) : fetched_data);
|
||||
|
||||
// don't fire signal until opened
|
||||
if (is_open)
|
||||
fetched(fetched_data);
|
||||
}
|
||||
|
||||
private void on_recent(int total) {
|
||||
debug("%s RECENT %d", to_string(), total);
|
||||
|
||||
properties.recent = total;
|
||||
|
||||
// don't fire signal until opened
|
||||
if (is_open)
|
||||
recent(total);
|
||||
}
|
||||
|
||||
private void on_status_response(StatusResponse status_response) {
|
||||
// only interested in ResponseCodes here
|
||||
ResponseCode? response_code = status_response.response_code;
|
||||
if (response_code == null)
|
||||
return;
|
||||
|
||||
try {
|
||||
switch (response_code.get_response_code_type()) {
|
||||
case ResponseCodeType.READONLY:
|
||||
readonly = Trillian.TRUE;
|
||||
break;
|
||||
|
||||
case ResponseCodeType.READWRITE:
|
||||
readonly = Trillian.FALSE;
|
||||
break;
|
||||
|
||||
case ResponseCodeType.UIDNEXT:
|
||||
properties.uid_next = response_code.get_uid_next();
|
||||
break;
|
||||
|
||||
case ResponseCodeType.UIDVALIDITY:
|
||||
properties.uid_validity = response_code.get_uid_validity();
|
||||
break;
|
||||
|
||||
case ResponseCodeType.UNSEEN:
|
||||
properties.unseen = response_code.get_unseen();
|
||||
break;
|
||||
|
||||
case ResponseCodeType.PERMANENT_FLAGS:
|
||||
permanent_flags = response_code.get_permanent_flags();
|
||||
accepts_user_flags = Trillian.from_boolean(
|
||||
permanent_flags.contains(MessageFlag.ALLOWS_NEW));
|
||||
break;
|
||||
|
||||
default:
|
||||
debug("%s: Ignoring response code %s", to_string(),
|
||||
response_code.to_string());
|
||||
break;
|
||||
}
|
||||
} catch (ImapError ierr) {
|
||||
debug("Unable to parse ResponseCode %s: %s", response_code.to_string(),
|
||||
ierr.message);
|
||||
}
|
||||
}
|
||||
|
||||
private void on_disconnected(ClientSession.DisconnectReason reason) {
|
||||
debug("%s DISCONNECTED %s", to_string(), reason.to_string());
|
||||
|
||||
disconnected(reason);
|
||||
}
|
||||
|
||||
public int get_email_count() throws Error {
|
||||
if (mailbox == null)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
|
||||
private void check_open() throws Error {
|
||||
if (!is_open)
|
||||
throw new EngineError.OPEN_REQUIRED("Imap.Folder %s not open", to_string());
|
||||
}
|
||||
|
||||
// All commands must executed inside the cmd_mutex; returns FETCH or STORE results
|
||||
private async Gee.HashMap<SequenceNumber, FetchedData>? exec_commands_async(
|
||||
Gee.Collection<Command> cmds, Cancellable? cancellable) throws Error {
|
||||
int token = yield cmd_mutex.claim_async(cancellable);
|
||||
|
||||
return mailbox.exists;
|
||||
// execute commands with mutex locked
|
||||
Gee.Map<Command, StatusResponse>? responses = null;
|
||||
Error? err = null;
|
||||
try {
|
||||
responses = yield session.send_multiple_commands_async(cmds, cancellable);
|
||||
} catch (Error store_fetch_err) {
|
||||
err = store_fetch_err;
|
||||
}
|
||||
|
||||
// swap out results and clear accumulator
|
||||
Gee.HashMap<SequenceNumber, FetchedData>? results = null;
|
||||
if (fetch_accumulator.size > 0) {
|
||||
results = fetch_accumulator;
|
||||
fetch_accumulator = new Gee.HashMap<SequenceNumber, FetchedData>();
|
||||
}
|
||||
|
||||
// unlock after clearing accumulator
|
||||
cmd_mutex.release(ref token);
|
||||
|
||||
if (err != null)
|
||||
throw err;
|
||||
|
||||
assert(responses != null);
|
||||
|
||||
// process response stati after unlocking and clearing accumulator
|
||||
foreach (Command cmd in responses.keys)
|
||||
throw_on_failed_status(responses.get(cmd), cmd);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private void throw_on_failed_status(StatusResponse response, Command cmd) throws Error {
|
||||
assert(response.is_completion);
|
||||
|
||||
switch (response.status) {
|
||||
case Status.OK:
|
||||
return;
|
||||
|
||||
case Status.NO:
|
||||
throw new ImapError.SERVER_ERROR("Request %s failed on %s: %s", cmd.to_string(),
|
||||
to_string(), response.to_string());
|
||||
|
||||
case Status.BAD:
|
||||
throw new ImapError.INVALID("Bad request %s on %s: %s", cmd.to_string(),
|
||||
to_string(), response.to_string());
|
||||
|
||||
default:
|
||||
throw new ImapError.NOT_SUPPORTED("Unknown response status to %s on %s: %s",
|
||||
cmd.to_string(), to_string(), response.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
public async Gee.List<Geary.Email>? list_email_async(MessageSet msg_set, Geary.Email.Field fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
if (mailbox == null)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
|
||||
Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
return yield mailbox.list_set_async(msg_set, fields, cancellable);
|
||||
// getting all the fields can require multiple FETCH commands (some servers don't handle
|
||||
// well putting every required data item into single command), so aggregate FetchCommands
|
||||
Gee.Collection<FetchCommand> cmds = new Gee.ArrayList<FetchCommand>();
|
||||
|
||||
// if not a UID FETCH, request UIDs for all messages so their EmailIdentifier can be
|
||||
// created without going back to the database (assuming the messages have already been
|
||||
// pulled down, not a guarantee)
|
||||
if (!msg_set.is_uid)
|
||||
cmds.add(new FetchCommand.data_type(msg_set, FetchDataType.UID));
|
||||
|
||||
// convert bulk of the "basic" fields into a one or two FETCH commands (some servers have
|
||||
// exhibited bugs or return NO when too many FETCH data types are combined on a single
|
||||
// command)
|
||||
FetchBodyDataIdentifier? partial_header_identifier = null;
|
||||
if (fields.requires_any(BASIC_FETCH_FIELDS)) {
|
||||
Gee.List<FetchDataType> data_types = new Gee.ArrayList<FetchDataType>();
|
||||
FetchBodyDataType? header_body_type;
|
||||
fields_to_fetch_data_types(fields, data_types, out header_body_type);
|
||||
|
||||
// Add all simple data types as one FETCH command
|
||||
if (data_types.size > 0)
|
||||
cmds.add(new FetchCommand(msg_set, data_types, null));
|
||||
|
||||
// Add all body data types as separate FETCH command
|
||||
Gee.List<FetchBodyDataType>? body_data_types = null;
|
||||
if (header_body_type != null) {
|
||||
body_data_types = new Gee.ArrayList<FetchBodyDataType>();
|
||||
body_data_types.add(header_body_type);
|
||||
|
||||
// save identifier for later decoding
|
||||
partial_header_identifier = header_body_type.get_identifier();
|
||||
|
||||
cmds.add(new FetchCommand(msg_set, null, body_data_types));
|
||||
}
|
||||
}
|
||||
|
||||
// RFC822 BODY is a separate command
|
||||
FetchBodyDataIdentifier? body_identifier = null;
|
||||
if (fields.require(Email.Field.BODY)) {
|
||||
FetchBodyDataType body = new FetchBodyDataType.peek(FetchBodyDataType.SectionPart.TEXT,
|
||||
null, -1, -1, null);
|
||||
|
||||
// save identifier for later retrieval from responses
|
||||
body_identifier = body.get_identifier();
|
||||
|
||||
cmds.add(new FetchCommand.body_data_type(msg_set, body));
|
||||
}
|
||||
|
||||
// PREVIEW requires two separate commands
|
||||
FetchBodyDataIdentifier? preview_identifier = null;
|
||||
FetchBodyDataIdentifier? preview_charset_identifier = null;
|
||||
if (fields.require(Email.Field.PREVIEW)) {
|
||||
// Get the preview text (the initial MAX_PREVIEW_BYTES of the first MIME section
|
||||
FetchBodyDataType preview = new FetchBodyDataType.peek(FetchBodyDataType.SectionPart.NONE,
|
||||
{ 1 }, 0, Geary.Email.MAX_PREVIEW_BYTES, null);
|
||||
preview_identifier = preview.get_identifier();
|
||||
cmds.add(new FetchCommand.body_data_type(msg_set, preview));
|
||||
|
||||
// Also get the character set to properly decode it
|
||||
FetchBodyDataType preview_charset = new FetchBodyDataType.peek(
|
||||
FetchBodyDataType.SectionPart.MIME, { 1 }, -1, -1, null);
|
||||
preview_charset_identifier = preview_charset.get_identifier();
|
||||
cmds.add(new FetchCommand.body_data_type(msg_set, preview_charset));
|
||||
}
|
||||
|
||||
// PROPERTIES and FLAGS are a separate command
|
||||
if (fields.requires_any(Email.Field.PROPERTIES | Email.Field.FLAGS)) {
|
||||
Gee.List<FetchDataType> data_types = new Gee.ArrayList<FetchDataType>();
|
||||
|
||||
if (fields.require(Geary.Email.Field.PROPERTIES)) {
|
||||
data_types.add(FetchDataType.INTERNALDATE);
|
||||
data_types.add(FetchDataType.RFC822_SIZE);
|
||||
}
|
||||
|
||||
if (fields.require(Geary.Email.Field.FLAGS))
|
||||
data_types.add(FetchDataType.FLAGS);
|
||||
|
||||
cmds.add(new FetchCommand(msg_set, data_types, null));
|
||||
}
|
||||
|
||||
// Commands prepped, do the fetch and accumulate all the responses
|
||||
Gee.HashMap<SequenceNumber, FetchedData>? fetched = yield exec_commands_async(cmds,
|
||||
cancellable);
|
||||
if (fetched == null || fetched.size == 0)
|
||||
return null;
|
||||
|
||||
// Convert fetched data into Geary.Email objects
|
||||
Gee.List<Geary.Email> email_list = new Gee.ArrayList<Geary.Email>();
|
||||
foreach (SequenceNumber seq_num in fetched.keys) {
|
||||
FetchedData fetched_data = fetched.get(seq_num);
|
||||
|
||||
// the UID should either have been fetched (if using positional addressing) or should
|
||||
// have come back with the response (if using UID addressing)
|
||||
UID? uid = fetched_data.data_map.get(FetchDataType.UID) as UID;
|
||||
if (uid == null) {
|
||||
message("Unable to list message #%s on %s: No UID returned from server",
|
||||
seq_num.to_string(), to_string());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
Geary.Email email = fetched_data_to_email(uid, fetched_data, fields,
|
||||
partial_header_identifier, body_identifier, preview_identifier,
|
||||
preview_charset_identifier);
|
||||
if (!email.fields.fulfills(fields)) {
|
||||
debug("%s: %s missing=%s fetched=%s", to_string(), email.id.to_string(),
|
||||
fields.clear(email.fields).to_list_string(), fetched_data.to_string());
|
||||
}
|
||||
|
||||
email_list.add(email);
|
||||
} catch (Error err) {
|
||||
debug("%s: Unable to convert email for %s %s: %s", to_string(), uid.to_string(),
|
||||
fetched_data.to_string(), err.message);
|
||||
}
|
||||
}
|
||||
|
||||
return (email_list.size > 0) ? email_list : null;
|
||||
}
|
||||
|
||||
public async void remove_email_async(MessageSet msg_set, Cancellable? cancellable = null) throws Error {
|
||||
if (mailbox == null)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
|
||||
public async void remove_email_async(MessageSet msg_set, Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
Gee.List<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
|
||||
flags.add(MessageFlag.DELETED);
|
||||
|
||||
yield mailbox.mark_email_async(msg_set, flags, null, cancellable);
|
||||
Gee.List<Command> cmds = new Gee.ArrayList<Command>();
|
||||
|
||||
// mailbox could've closed during call
|
||||
if (mailbox != null)
|
||||
yield mailbox.expunge_email_async(msg_set, cancellable);
|
||||
StoreCommand store_cmd = new StoreCommand(msg_set, flags, true, false);
|
||||
cmds.add(store_cmd);
|
||||
|
||||
if (session.capabilities.has_capability(Capabilities.UIDPLUS))
|
||||
cmds.add(new ExpungeCommand.uid(msg_set));
|
||||
else
|
||||
cmds.add(new ExpungeCommand());
|
||||
|
||||
yield exec_commands_async(cmds, cancellable);
|
||||
}
|
||||
|
||||
public async void mark_email_async(MessageSet msg_set, Geary.EmailFlags? flags_to_add,
|
||||
Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error {
|
||||
if (mailbox == null)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
|
||||
Geary.EmailFlags? flags_to_remove, Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
Gee.List<MessageFlag> msg_flags_add = new Gee.ArrayList<MessageFlag>();
|
||||
Gee.List<MessageFlag> msg_flags_remove = new Gee.ArrayList<MessageFlag>();
|
||||
MessageFlag.from_email_flags(flags_to_add, flags_to_remove, out msg_flags_add,
|
||||
out msg_flags_remove);
|
||||
|
||||
yield mailbox.mark_email_async(msg_set, msg_flags_add, msg_flags_remove, cancellable);
|
||||
if (msg_flags_add.size == 0 && msg_flags_remove.size == 0)
|
||||
return;
|
||||
|
||||
Gee.Collection<Command> cmds = new Gee.ArrayList<Command>();
|
||||
|
||||
if (msg_flags_add.size > 0)
|
||||
cmds.add(new StoreCommand(msg_set, msg_flags_add, true, false));
|
||||
|
||||
if (msg_flags_remove.size > 0)
|
||||
cmds.add(new StoreCommand(msg_set, msg_flags_remove, false, false));
|
||||
|
||||
yield exec_commands_async(cmds, cancellable);
|
||||
}
|
||||
|
||||
|
||||
public async void copy_email_async(MessageSet msg_set, Geary.FolderPath destination,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
if (mailbox == null)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
|
||||
|
||||
yield mailbox.copy_email_async(msg_set, destination, cancellable);
|
||||
Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
CopyCommand cmd = new CopyCommand(msg_set,
|
||||
new MailboxSpecifier.from_folder_path(destination, null));
|
||||
Gee.Collection<Command> cmds = new Collection.SingleItem<Command>(cmd);
|
||||
|
||||
yield exec_commands_async(cmds, cancellable);
|
||||
}
|
||||
|
||||
|
||||
// TODO: Support MOVE extension
|
||||
public async void move_email_async(MessageSet msg_set, Geary.FolderPath destination,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
if (mailbox == null)
|
||||
throw new EngineError.OPEN_REQUIRED("%s not opened", to_string());
|
||||
|
||||
yield copy_email_async(msg_set, destination, cancellable);
|
||||
yield remove_email_async(msg_set, cancellable);
|
||||
Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
Gee.Collection<Command> cmds = new Gee.ArrayList<Command>();
|
||||
|
||||
// Don't use copy_email_async followed by remove_email_async; this needs to be one
|
||||
// set of commands executed in order without releasing the cmd_mutex; this is especially
|
||||
// vital if positional addressing is used
|
||||
cmds.add(new CopyCommand(msg_set, new MailboxSpecifier.from_folder_path(destination, null)));
|
||||
|
||||
Gee.List<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
|
||||
flags.add(MessageFlag.DELETED);
|
||||
cmds.add(new StoreCommand(msg_set, flags, true, false));
|
||||
|
||||
if (msg_set.is_uid)
|
||||
cmds.add(new ExpungeCommand.uid(msg_set));
|
||||
else
|
||||
cmds.add(new ExpungeCommand());
|
||||
|
||||
yield exec_commands_async(cmds, cancellable);
|
||||
}
|
||||
|
||||
|
||||
// NOTE: If fields are added or removed from this method, BASIC_FETCH_FIELDS *must* be updated
|
||||
// as well
|
||||
private void fields_to_fetch_data_types(Geary.Email.Field fields,
|
||||
Gee.List<FetchDataType> data_types_list, out FetchBodyDataType? header_body_type) {
|
||||
// pack all the needed headers into a single FetchBodyDataType
|
||||
string[] field_names = new string[0];
|
||||
|
||||
// The assumption here is that because ENVELOPE is such a common fetch command, the
|
||||
// server will have optimizations for it, whereas if we called for each header in the
|
||||
// envelope separately, the server has to chunk harder parsing the RFC822 header ... have
|
||||
// to add References because IMAP ENVELOPE doesn't return them for some reason (but does
|
||||
// return Message-ID and In-Reply-To)
|
||||
if (fields.is_all_set(Geary.Email.Field.ENVELOPE)) {
|
||||
data_types_list.add(FetchDataType.ENVELOPE);
|
||||
field_names += "References";
|
||||
|
||||
// remove those flags and process any remaining
|
||||
fields = fields.clear(Geary.Email.Field.ENVELOPE);
|
||||
}
|
||||
|
||||
foreach (Geary.Email.Field field in Geary.Email.Field.all()) {
|
||||
switch (fields & field) {
|
||||
case Geary.Email.Field.DATE:
|
||||
field_names += "Date";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.ORIGINATORS:
|
||||
field_names += "From";
|
||||
field_names += "Sender";
|
||||
field_names += "Reply-To";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.RECEIVERS:
|
||||
field_names += "To";
|
||||
field_names += "Cc";
|
||||
field_names += "Bcc";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.REFERENCES:
|
||||
field_names += "References";
|
||||
field_names += "Message-ID";
|
||||
field_names += "In-Reply-To";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.SUBJECT:
|
||||
field_names += "Subject";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.HEADER:
|
||||
// TODO: If the entire header is being pulled, then no need to pull down partial
|
||||
// headers; simply get them all and decode what is needed directly
|
||||
data_types_list.add(FetchDataType.RFC822_HEADER);
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.NONE:
|
||||
case Geary.Email.Field.BODY:
|
||||
case Geary.Email.Field.PROPERTIES:
|
||||
case Geary.Email.Field.FLAGS:
|
||||
case Geary.Email.Field.PREVIEW:
|
||||
// not set or fetched separately
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
// convert field names into single FetchBodyDataType object
|
||||
if (field_names.length > 0) {
|
||||
header_body_type = new FetchBodyDataType.peek(
|
||||
FetchBodyDataType.SectionPart.HEADER_FIELDS, null, -1, -1, field_names);
|
||||
} else {
|
||||
header_body_type = null;
|
||||
}
|
||||
}
|
||||
|
||||
private Geary.Email fetched_data_to_email(UID uid, FetchedData fetched_data, Geary.Email.Field required_fields,
|
||||
FetchBodyDataIdentifier? partial_header_identifier, FetchBodyDataIdentifier? body_identifier,
|
||||
FetchBodyDataIdentifier? preview_identifier, FetchBodyDataIdentifier? preview_charset_identifier)
|
||||
throws Error {
|
||||
Geary.Email email = new Geary.Email(fetched_data.seq_num.value,
|
||||
new Imap.EmailIdentifier(uid, path));
|
||||
|
||||
// accumulate these to submit Imap.EmailProperties all at once
|
||||
InternalDate? internaldate = null;
|
||||
RFC822.Size? rfc822_size = null;
|
||||
|
||||
// accumulate these to submit References all at once
|
||||
RFC822.MessageID? message_id = null;
|
||||
RFC822.MessageID? in_reply_to = null;
|
||||
RFC822.MessageIDList? references = null;
|
||||
|
||||
// loop through all available FetchDataTypes and gather converted data
|
||||
foreach (FetchDataType data_type in fetched_data.data_map.keys) {
|
||||
MessageData? data = fetched_data.data_map.get(data_type);
|
||||
if (data == null)
|
||||
continue;
|
||||
|
||||
switch (data_type) {
|
||||
case FetchDataType.ENVELOPE:
|
||||
Envelope envelope = (Envelope) data;
|
||||
|
||||
email.set_send_date(envelope.sent);
|
||||
email.set_message_subject(envelope.subject);
|
||||
email.set_originators(envelope.from, envelope.sender, envelope.reply_to);
|
||||
email.set_receivers(envelope.to, envelope.cc, envelope.bcc);
|
||||
|
||||
// store these to add to References all at once
|
||||
message_id = envelope.message_id;
|
||||
in_reply_to = envelope.in_reply_to;
|
||||
break;
|
||||
|
||||
case FetchDataType.RFC822_HEADER:
|
||||
email.set_message_header((RFC822.Header) data);
|
||||
break;
|
||||
|
||||
case FetchDataType.RFC822_TEXT:
|
||||
email.set_message_body((RFC822.Text) data);
|
||||
break;
|
||||
|
||||
case FetchDataType.RFC822_SIZE:
|
||||
rfc822_size = (RFC822.Size) data;
|
||||
break;
|
||||
|
||||
case FetchDataType.FLAGS:
|
||||
email.set_flags(new Imap.EmailFlags((MessageFlags) data));
|
||||
break;
|
||||
|
||||
case FetchDataType.INTERNALDATE:
|
||||
internaldate = (InternalDate) data;
|
||||
break;
|
||||
|
||||
default:
|
||||
// everything else dropped on the floor (not applicable to Geary.Email)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only set PROPERTIES if all have been found
|
||||
if (internaldate != null && rfc822_size != null)
|
||||
email.set_email_properties(new Geary.Imap.EmailProperties(internaldate, rfc822_size));
|
||||
|
||||
// if the header was requested, convert its fields now
|
||||
if (partial_header_identifier != null) {
|
||||
if (!fetched_data.body_data_map.has_key(partial_header_identifier)) {
|
||||
debug("[%s] No partial header identifier \"%s\" found:", to_string(),
|
||||
partial_header_identifier.to_string());
|
||||
foreach (FetchBodyDataIdentifier id in fetched_data.body_data_map.keys)
|
||||
debug("[%s] has %s", to_string(), id.to_string());
|
||||
}
|
||||
assert(fetched_data.body_data_map.has_key(partial_header_identifier));
|
||||
|
||||
RFC822.Header headers = new RFC822.Header(
|
||||
fetched_data.body_data_map.get(partial_header_identifier));
|
||||
|
||||
// DATE
|
||||
if (!email.fields.is_all_set(Geary.Email.Field.DATE)) {
|
||||
string? value = headers.get_header("Date");
|
||||
if (!String.is_empty(value))
|
||||
email.set_send_date(new RFC822.Date(value));
|
||||
}
|
||||
|
||||
// ORIGINATORS
|
||||
if (!email.fields.is_all_set(Geary.Email.Field.ORIGINATORS)) {
|
||||
RFC822.MailboxAddresses? from = null;
|
||||
string? value = headers.get_header("From");
|
||||
if (!String.is_empty(value))
|
||||
from = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
RFC822.MailboxAddresses? sender = null;
|
||||
value = headers.get_header("Sender");
|
||||
if (!String.is_empty(value))
|
||||
sender = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
RFC822.MailboxAddresses? reply_to = null;
|
||||
value = headers.get_header("Reply-To");
|
||||
if (!String.is_empty(value))
|
||||
reply_to = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
if (from != null || sender != null || reply_to != null)
|
||||
email.set_originators(from, sender, reply_to);
|
||||
}
|
||||
|
||||
// RECEIVERS
|
||||
if (!email.fields.is_all_set(Geary.Email.Field.RECEIVERS)) {
|
||||
RFC822.MailboxAddresses? to = null;
|
||||
string? value = headers.get_header("To");
|
||||
if (!String.is_empty(value))
|
||||
to = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
RFC822.MailboxAddresses? cc = null;
|
||||
value = headers.get_header("Cc");
|
||||
if (!String.is_empty(value))
|
||||
cc = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
RFC822.MailboxAddresses? bcc = null;
|
||||
value = headers.get_header("Bcc");
|
||||
if (!String.is_empty(value))
|
||||
bcc = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
if (to != null || cc != null || bcc != null)
|
||||
email.set_receivers(to, cc, bcc);
|
||||
}
|
||||
|
||||
// REFERENCES
|
||||
// (Note that it's possible the request used an IMAP ENVELOPE, in which case only the
|
||||
// References header will be present if REFERENCES were required, which is why
|
||||
// REFERENCES is set at the bottom of the method, when all information has been gathered
|
||||
if (message_id == null) {
|
||||
string? value = headers.get_header("Message-ID");
|
||||
if (!String.is_empty(value))
|
||||
message_id = new RFC822.MessageID(value);
|
||||
}
|
||||
|
||||
if (in_reply_to == null) {
|
||||
string? value = headers.get_header("In-Reply-To");
|
||||
if (!String.is_empty(value))
|
||||
in_reply_to = new RFC822.MessageID(value);
|
||||
}
|
||||
|
||||
if (references == null) {
|
||||
string? value = headers.get_header("References");
|
||||
if (!String.is_empty(value))
|
||||
references = new RFC822.MessageIDList.from_rfc822_string(value);
|
||||
}
|
||||
|
||||
// SUBJECT
|
||||
if (!email.fields.is_all_set(Geary.Email.Field.SUBJECT)) {
|
||||
string? value = headers.get_header("Subject");
|
||||
if (!String.is_empty(value))
|
||||
email.set_message_subject(new RFC822.Subject.decode(value));
|
||||
}
|
||||
}
|
||||
|
||||
// It's possible for all these fields to be null even though they were requested from
|
||||
// the server, so use requested fields for determination
|
||||
if (required_fields.require(Geary.Email.Field.REFERENCES))
|
||||
email.set_full_references(message_id, in_reply_to, references);
|
||||
|
||||
// if body was requested, get it now
|
||||
if (body_identifier != null) {
|
||||
assert(fetched_data.body_data_map.has_key(body_identifier));
|
||||
|
||||
email.set_message_body(new Geary.RFC822.Text(
|
||||
fetched_data.body_data_map.get(body_identifier)));
|
||||
}
|
||||
|
||||
// if preview was requested, get it now ... both identifiers must be supplied if one is
|
||||
if (preview_identifier != null || preview_charset_identifier != null) {
|
||||
assert(preview_identifier != null && preview_charset_identifier != null);
|
||||
assert(fetched_data.body_data_map.has_key(preview_identifier));
|
||||
assert(fetched_data.body_data_map.has_key(preview_charset_identifier));
|
||||
|
||||
email.set_message_preview(new RFC822.PreviewText.with_header(
|
||||
fetched_data.body_data_map.get(preview_identifier),
|
||||
fetched_data.body_data_map.get(preview_charset_identifier)));
|
||||
}
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return path.to_string();
|
||||
}
|
||||
|
|
|
|||
20
src/engine/imap/command/imap-capability-command.vala
Normal file
20
src/engine/imap/command/imap-capability-command.vala
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.1.1]]
|
||||
*
|
||||
* @see Capabilities
|
||||
*/
|
||||
|
||||
public class Geary.Imap.CapabilityCommand : Command {
|
||||
public const string NAME = "capability";
|
||||
|
||||
public CapabilityCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
18
src/engine/imap/command/imap-close-command.vala
Normal file
18
src/engine/imap/command/imap-close-command.vala
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.4.2]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.CloseCommand : Command {
|
||||
public const string NAME = "close";
|
||||
|
||||
public CloseCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,54 +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 class Geary.Imap.CommandResponse : BaseObject {
|
||||
public Gee.List<ServerData> server_data { get; private set; }
|
||||
public StatusResponse? status_response { get; private set; }
|
||||
|
||||
public CommandResponse() {
|
||||
server_data = new Gee.ArrayList<ServerData>();
|
||||
}
|
||||
|
||||
public void add_server_data(ServerData data) {
|
||||
assert(!is_sealed());
|
||||
|
||||
server_data.add(data);
|
||||
}
|
||||
|
||||
public bool remove_server_data(ServerData data) {
|
||||
return server_data.remove(data);
|
||||
}
|
||||
|
||||
public bool remove_many_server_data(Gee.Collection<ServerData> data) {
|
||||
return server_data.remove_all(data);
|
||||
}
|
||||
|
||||
public void seal(StatusResponse status_response) {
|
||||
assert(!is_sealed());
|
||||
|
||||
this.status_response = status_response;
|
||||
}
|
||||
|
||||
public bool is_sealed() {
|
||||
return (status_response != null);
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
foreach (ServerData data in server_data)
|
||||
builder.append("%s\n".printf(data.to_string()));
|
||||
|
||||
if (status_response != null)
|
||||
builder.append(status_response.to_string());
|
||||
|
||||
if (!is_sealed())
|
||||
builder.append("(incomplete command response)");
|
||||
|
||||
return builder.str;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,11 +4,50 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A representation of an IMAP command (request).
|
||||
*
|
||||
* A Command is created by the caller and then submitted to a {@link ClientSession} or
|
||||
* {@link ClientConnection} for transmission to the server. In response, one or more
|
||||
* {@link ServerResponse}s are returned, generally zero or more {@link ServerData}s followed by
|
||||
* a completion {@link StatusResponse}. Untagged {@link StatusResponse}s may also be returned,
|
||||
* depending on the Command.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.Command : RootParameters {
|
||||
/**
|
||||
* All IMAP commands are tagged with an identifier assigned by the client.
|
||||
*
|
||||
* Note that this is not immutable. The general practice is to use an unassigned Tag
|
||||
* up until the {@link Command} is about to be transmitted, at which point a Tag is
|
||||
* assigned. This allows for all commands to be issued in Tag "order". This generally makes
|
||||
* tracing network traffic easier.
|
||||
*
|
||||
* @see Tag.get_unassigned
|
||||
* @see assign_tag
|
||||
*/
|
||||
public Tag tag { get; private set; }
|
||||
|
||||
/**
|
||||
* The name (or "verb") of the {@link Command}.
|
||||
*/
|
||||
public string name { get; private set; }
|
||||
|
||||
/**
|
||||
* Zero or more arguments for the {@link Command}.
|
||||
*
|
||||
* Note that some Commands have require args and others are optional. The format of the
|
||||
* arguments ({@link StringParameter}, {@link ListParameter}, etc.) is sometimes crucial.
|
||||
*/
|
||||
public string[]? args { get; private set; }
|
||||
|
||||
/**
|
||||
* Create a Command with an unassigned Tag.
|
||||
*
|
||||
* @see tag
|
||||
*/
|
||||
public Command(string name, string[]? args = null) {
|
||||
tag = Tag.get_unassigned();
|
||||
this.name = name;
|
||||
|
|
@ -17,6 +56,11 @@ public class Geary.Imap.Command : RootParameters {
|
|||
stock_params();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Command with an assigned Tag.
|
||||
*
|
||||
* @see tag
|
||||
*/
|
||||
public Command.assigned(Tag tag, string name, string[]? args = null)
|
||||
requires (tag.is_tagged() && tag.is_assigned()) {
|
||||
this.tag = tag;
|
||||
|
|
@ -28,14 +72,21 @@ public class Geary.Imap.Command : RootParameters {
|
|||
|
||||
private void stock_params() {
|
||||
add(tag);
|
||||
add(new UnquotedStringParameter(name));
|
||||
add(new AtomParameter(name));
|
||||
if (args != null) {
|
||||
foreach (string arg in args)
|
||||
add(new StringParameter(arg));
|
||||
foreach (string arg in args) {
|
||||
StringParameter? stringp = StringParameter.get_best_for(arg);
|
||||
if (stringp != null)
|
||||
add(stringp);
|
||||
else
|
||||
error("Command continuations currently unsupported");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a {@link Tag} to a {@link Command} with an unassigned placeholder Tag.
|
||||
*
|
||||
* Can only be called on a Command that holds an unassigned Tag. Thus, this can only be called
|
||||
* once at most, and zero times if Command.assigned() was used to generate the Command.
|
||||
* Fires an assertion if either of these cases is true, or if the supplied Tag is unassigned.
|
||||
|
|
|
|||
|
|
@ -1,193 +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 class Geary.Imap.CapabilityCommand : Command {
|
||||
public const string NAME = "capability";
|
||||
|
||||
public CapabilityCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.CompressCommand : Command {
|
||||
public const string NAME = "compress";
|
||||
|
||||
public const string ALGORITHM_DEFLATE = "deflate";
|
||||
|
||||
public CompressCommand(string algorithm) {
|
||||
base (NAME, { algorithm });
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.StarttlsCommand : Command {
|
||||
public const string NAME = "starttls";
|
||||
|
||||
public StarttlsCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.NoopCommand : Command {
|
||||
public const string NAME = "noop";
|
||||
|
||||
public NoopCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.LoginCommand : Command {
|
||||
public const string NAME = "login";
|
||||
|
||||
public LoginCommand(string user, string pass) {
|
||||
base (NAME, { user, pass });
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return "%s %s <user> <pass>".printf(tag.to_string(), name);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.LogoutCommand : Command {
|
||||
public const string NAME = "logout";
|
||||
|
||||
public LogoutCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.ListCommand : Command {
|
||||
public const string NAME = "list";
|
||||
public const string XLIST_NAME = "xlist";
|
||||
|
||||
public ListCommand(Geary.Imap.MailboxParameter mailbox, bool use_xlist) {
|
||||
base (use_xlist ? XLIST_NAME : NAME, { "", mailbox.value });
|
||||
}
|
||||
|
||||
public ListCommand.wildcarded(string reference, Geary.Imap.MailboxParameter mailbox, bool use_xlist) {
|
||||
base (use_xlist ? XLIST_NAME : NAME, { reference, mailbox.value });
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.ExamineCommand : Command {
|
||||
public const string NAME = "examine";
|
||||
|
||||
public ExamineCommand(Geary.Imap.MailboxParameter mailbox) {
|
||||
base (NAME, { mailbox.value });
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.SelectCommand : Command {
|
||||
public const string NAME = "select";
|
||||
|
||||
public SelectCommand(Geary.Imap.MailboxParameter mailbox) {
|
||||
base (NAME, { mailbox.value });
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.CloseCommand : Command {
|
||||
public const string NAME = "close";
|
||||
|
||||
public CloseCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.StatusCommand : Command {
|
||||
public const string NAME = "status";
|
||||
|
||||
public StatusCommand(Geary.Imap.MailboxParameter mailbox, StatusDataType[] data_items) {
|
||||
base (NAME);
|
||||
|
||||
add(mailbox);
|
||||
|
||||
assert(data_items.length > 0);
|
||||
ListParameter data_item_list = new ListParameter(this);
|
||||
foreach (StatusDataType data_item in data_items)
|
||||
data_item_list.add(data_item.to_parameter());
|
||||
|
||||
add(data_item_list);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.StoreCommand : Command {
|
||||
public const string NAME = "store";
|
||||
public const string UID_NAME = "uid store";
|
||||
|
||||
public StoreCommand(MessageSet message_set, Gee.List<MessageFlag> flag_list, bool add_flag,
|
||||
bool silent) {
|
||||
base (message_set.is_uid ? UID_NAME : NAME);
|
||||
|
||||
add(message_set.to_parameter());
|
||||
add(new StringParameter("%sflags%s".printf(add_flag ? "+" : "-", silent ? ".silent" : "")));
|
||||
|
||||
ListParameter list = new ListParameter(this);
|
||||
foreach(MessageFlag flag in flag_list)
|
||||
list.add(new StringParameter(flag.value));
|
||||
|
||||
add(list);
|
||||
}
|
||||
}
|
||||
|
||||
// Results of this command automatically handled by Geary.Imap.UnsolicitedServerData
|
||||
public class Geary.Imap.ExpungeCommand : Command {
|
||||
public const string NAME = "expunge";
|
||||
public const string UID_NAME = "uid expunge";
|
||||
|
||||
public ExpungeCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
|
||||
public ExpungeCommand.uid(MessageSet message_set) {
|
||||
base (UID_NAME);
|
||||
|
||||
assert(message_set.is_uid);
|
||||
|
||||
add(message_set.to_parameter());
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.IdleCommand : Command {
|
||||
public const string NAME = "idle";
|
||||
|
||||
public IdleCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.CopyCommand : Command {
|
||||
public const string NAME = "copy";
|
||||
public const string UID_NAME = "uid copy";
|
||||
|
||||
public CopyCommand(MessageSet message_set, Geary.Imap.MailboxParameter destination) {
|
||||
base (message_set.is_uid ? UID_NAME : NAME);
|
||||
|
||||
add(message_set.to_parameter());
|
||||
add(destination);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.IdCommand : Command {
|
||||
public const string NAME = "id";
|
||||
|
||||
public IdCommand(Gee.HashMap<string, string> fields) {
|
||||
base (NAME);
|
||||
|
||||
ListParameter list = new ListParameter(this);
|
||||
foreach (string key in fields.keys) {
|
||||
list.add(new QuotedStringParameter(key));
|
||||
list.add(new QuotedStringParameter(fields.get(key)));
|
||||
}
|
||||
|
||||
add(list);
|
||||
}
|
||||
|
||||
public IdCommand.nil() {
|
||||
base (NAME);
|
||||
|
||||
add(NilParameter.instance);
|
||||
}
|
||||
}
|
||||
|
||||
20
src/engine/imap/command/imap-compress-command.vala
Normal file
20
src/engine/imap/command/imap-compress-command.vala
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc4978]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.CompressCommand : Command {
|
||||
public const string NAME = "compress";
|
||||
|
||||
public const string ALGORITHM_DEFLATE = "deflate";
|
||||
|
||||
public CompressCommand(string algorithm) {
|
||||
base (NAME, { algorithm });
|
||||
}
|
||||
}
|
||||
|
||||
22
src/engine/imap/command/imap-copy-command.vala
Normal file
22
src/engine/imap/command/imap-copy-command.vala
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.4.7]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.CopyCommand : Command {
|
||||
public const string NAME = "copy";
|
||||
public const string UID_NAME = "uid copy";
|
||||
|
||||
public CopyCommand(MessageSet message_set, MailboxSpecifier destination) {
|
||||
base (message_set.is_uid ? UID_NAME : NAME);
|
||||
|
||||
add(message_set.to_parameter());
|
||||
add(destination.to_parameter());
|
||||
}
|
||||
}
|
||||
|
||||
26
src/engine/imap/command/imap-examine-command.vala
Normal file
26
src/engine/imap/command/imap-examine-command.vala
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.3.2]]
|
||||
*
|
||||
* @see SelectCommand
|
||||
*/
|
||||
|
||||
public class Geary.Imap.ExamineCommand : Command {
|
||||
public const string NAME = "examine";
|
||||
|
||||
public MailboxSpecifier mailbox { get; private set; }
|
||||
|
||||
public ExamineCommand(MailboxSpecifier mailbox) {
|
||||
base (NAME);
|
||||
|
||||
this.mailbox = mailbox;
|
||||
|
||||
add(mailbox.to_parameter());
|
||||
}
|
||||
}
|
||||
|
||||
28
src/engine/imap/command/imap-expunge-command.vala
Normal file
28
src/engine/imap/command/imap-expunge-command.vala
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.4.3]] and
|
||||
* [[http://tools.ietf.org/html/rfc4315#section-2.1]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.ExpungeCommand : Command {
|
||||
public const string NAME = "expunge";
|
||||
public const string UID_NAME = "uid expunge";
|
||||
|
||||
public ExpungeCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
|
||||
public ExpungeCommand.uid(MessageSet message_set) {
|
||||
base (UID_NAME);
|
||||
|
||||
assert(message_set.is_uid);
|
||||
|
||||
add(message_set.to_parameter());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,43 +4,24 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A representation of the IMAP FETCH command.
|
||||
*
|
||||
* FETCH is easily the most complicated IMAP command. It has a large number of parameters, some of
|
||||
* which have a number of variants, others defined as macros combining other fields, and the
|
||||
* returned {@link ServerData} requires involved decoding patterns.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.4.5]]
|
||||
*
|
||||
* @see FetchedData
|
||||
* @see StoreCommand
|
||||
*/
|
||||
|
||||
public class Geary.Imap.FetchCommand : Command {
|
||||
public const string NAME = "fetch";
|
||||
public const string UID_NAME = "uid fetch";
|
||||
|
||||
public FetchCommand(MessageSet msg_set, FetchDataType[]? data_items,
|
||||
Gee.List<FetchBodyDataType>? body_data_items) {
|
||||
base (msg_set.is_uid ? UID_NAME : NAME);
|
||||
|
||||
add(msg_set.to_parameter());
|
||||
|
||||
int data_items_length = (data_items != null) ? data_items.length : 0;
|
||||
int body_items_length = (body_data_items != null) ? body_data_items.size : 0;
|
||||
|
||||
// if only one item being fetched, pass that as a singleton parameter, otherwise pass them
|
||||
// as a list
|
||||
if (data_items_length == 1 && body_items_length == 0) {
|
||||
add(data_items[0].to_parameter());
|
||||
} else if (data_items_length == 0 && body_items_length == 1) {
|
||||
add(body_data_items[0].to_parameter());
|
||||
} else {
|
||||
ListParameter list = new ListParameter(this);
|
||||
|
||||
if (data_items_length > 0) {
|
||||
foreach (FetchDataType data_item in data_items)
|
||||
list.add(data_item.to_parameter());
|
||||
}
|
||||
|
||||
if (body_items_length > 0) {
|
||||
foreach (FetchBodyDataType body_item in body_data_items)
|
||||
list.add(body_item.to_parameter());
|
||||
}
|
||||
|
||||
add(list);
|
||||
}
|
||||
}
|
||||
|
||||
public FetchCommand.from_collection(MessageSet msg_set, Gee.List<FetchDataType>? data_items,
|
||||
public FetchCommand(MessageSet msg_set, Gee.List<FetchDataType>? data_items,
|
||||
Gee.List<FetchBodyDataType>? body_data_items) {
|
||||
base (msg_set.is_uid ? UID_NAME : NAME);
|
||||
|
||||
|
|
@ -53,7 +34,7 @@ public class Geary.Imap.FetchCommand : Command {
|
|||
if (data_items_length == 1 && body_items_length == 0) {
|
||||
add(data_items[0].to_parameter());
|
||||
} else if (data_items_length == 0 && body_items_length == 1) {
|
||||
add(body_data_items[0].to_parameter());
|
||||
add(body_data_items[0].to_request_parameter());
|
||||
} else {
|
||||
ListParameter list = new ListParameter(this);
|
||||
|
||||
|
|
@ -64,11 +45,25 @@ public class Geary.Imap.FetchCommand : Command {
|
|||
|
||||
if (body_items_length > 0) {
|
||||
foreach (FetchBodyDataType body_item in body_data_items)
|
||||
list.add(body_item.to_parameter());
|
||||
list.add(body_item.to_request_parameter());
|
||||
}
|
||||
|
||||
add(list);
|
||||
}
|
||||
}
|
||||
|
||||
public FetchCommand.data_type(MessageSet msg_set, FetchDataType data_type) {
|
||||
base (msg_set.is_uid ? UID_NAME : NAME);
|
||||
|
||||
add(msg_set.to_parameter());
|
||||
add(data_type.to_parameter());
|
||||
}
|
||||
|
||||
public FetchCommand.body_data_type(MessageSet msg_set, FetchBodyDataType body_data_type) {
|
||||
base (msg_set.is_uid ? UID_NAME : NAME);
|
||||
|
||||
add(msg_set.to_parameter());
|
||||
add(body_data_type.to_request_parameter());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
32
src/engine/imap/command/imap-id-command.vala
Normal file
32
src/engine/imap/command/imap-id-command.vala
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://www.ietf.org/rfc/rfc2971.txt]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.IdCommand : Command {
|
||||
public const string NAME = "id";
|
||||
|
||||
public IdCommand(Gee.HashMap<string, string> fields) {
|
||||
base (NAME);
|
||||
|
||||
ListParameter list = new ListParameter(this);
|
||||
foreach (string key in fields.keys) {
|
||||
list.add(new QuotedStringParameter(key));
|
||||
list.add(new QuotedStringParameter(fields.get(key)));
|
||||
}
|
||||
|
||||
add(list);
|
||||
}
|
||||
|
||||
public IdCommand.nil() {
|
||||
base (NAME);
|
||||
|
||||
add(NilParameter.instance);
|
||||
}
|
||||
}
|
||||
|
||||
20
src/engine/imap/command/imap-idle-command.vala
Normal file
20
src/engine/imap/command/imap-idle-command.vala
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc2177]]
|
||||
*
|
||||
* @see NoopCommand
|
||||
*/
|
||||
|
||||
public class Geary.Imap.IdleCommand : Command {
|
||||
public const string NAME = "idle";
|
||||
|
||||
public IdleCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
34
src/engine/imap/command/imap-list-command.vala
Normal file
34
src/engine/imap/command/imap-list-command.vala
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.3.8]]
|
||||
*
|
||||
* Some implementations may return the mailbox name itself when using wildcarding. For example:
|
||||
* LIST "" "Parent/%"
|
||||
* may return "Parent/Child" on most systems, but some will return "Parent" as well. Callers
|
||||
* should be aware of this when processing, especially if performing a recursive decent.
|
||||
*
|
||||
* @see MailboxInformation
|
||||
*/
|
||||
|
||||
public class Geary.Imap.ListCommand : Command {
|
||||
public const string NAME = "list";
|
||||
public const string XLIST_NAME = "xlist";
|
||||
|
||||
public ListCommand(MailboxSpecifier mailbox, bool use_xlist) {
|
||||
base (use_xlist ? XLIST_NAME : NAME, { "" });
|
||||
|
||||
add(mailbox.to_parameter());
|
||||
}
|
||||
|
||||
public ListCommand.wildcarded(string reference, MailboxSpecifier mailbox, bool use_xlist) {
|
||||
base (use_xlist ? XLIST_NAME : NAME, { reference });
|
||||
|
||||
add(mailbox.to_parameter());
|
||||
}
|
||||
}
|
||||
|
||||
22
src/engine/imap/command/imap-login-command.vala
Normal file
22
src/engine/imap/command/imap-login-command.vala
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.2.3]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.LoginCommand : Command {
|
||||
public const string NAME = "login";
|
||||
|
||||
public LoginCommand(string user, string pass) {
|
||||
base (NAME, { user, pass });
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return "%s %s <user> <pass>".printf(tag.to_string(), name);
|
||||
}
|
||||
}
|
||||
|
||||
18
src/engine/imap/command/imap-logout-command.vala
Normal file
18
src/engine/imap/command/imap-logout-command.vala
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.1.3]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.LogoutCommand : Command {
|
||||
public const string NAME = "logout";
|
||||
|
||||
public LogoutCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6,21 +6,35 @@
|
|||
|
||||
extern void qsort(void *base, size_t num, size_t size, CompareFunc compare_func);
|
||||
|
||||
/**
|
||||
* A represenation of an IMAP message range specifier.
|
||||
*
|
||||
* A MessageSet can be for {@link SequenceNumber}s (which use positional addressing) or
|
||||
* {@link UID}s.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-9]], "sequence-set" and "seq-range".
|
||||
*/
|
||||
|
||||
public class Geary.Imap.MessageSet : BaseObject {
|
||||
/**
|
||||
* True if the {@link MessageSet} was created with a UID or a UID range.
|
||||
*
|
||||
* For {@link Command}s that accept MessageSets, they will use a UID variant
|
||||
*/
|
||||
public bool is_uid { get; private set; default = false; }
|
||||
|
||||
private string value { get; private set; }
|
||||
|
||||
public MessageSet(int msg_num) {
|
||||
assert(msg_num > 0);
|
||||
public MessageSet(SequenceNumber seq_num) {
|
||||
assert(seq_num.value > 0);
|
||||
|
||||
value = "%d".printf(msg_num);
|
||||
value = seq_num.serialize();
|
||||
}
|
||||
|
||||
public MessageSet.uid(UID uid) {
|
||||
assert(uid.value > 0);
|
||||
|
||||
value = uid.value.to_string();
|
||||
value = uid.serialize();
|
||||
is_uid = true;
|
||||
}
|
||||
|
||||
|
|
@ -28,43 +42,47 @@ public class Geary.Imap.MessageSet : BaseObject {
|
|||
MessageSet.uid(((Geary.Imap.EmailIdentifier) email_id).uid);
|
||||
}
|
||||
|
||||
public MessageSet.range_by_count(int low_msg_num, int count) {
|
||||
assert(low_msg_num > 0);
|
||||
public MessageSet.range_by_count(SequenceNumber low_seq_num, int count) {
|
||||
assert(low_seq_num.value > 0);
|
||||
assert(count > 0);
|
||||
|
||||
value = (count > 1)
|
||||
? "%d:%d".printf(low_msg_num, low_msg_num + count - 1)
|
||||
: "%d".printf(low_msg_num);
|
||||
? "%d:%d".printf(low_seq_num.value, low_seq_num.value + count - 1)
|
||||
: low_seq_num.serialize();
|
||||
}
|
||||
|
||||
public MessageSet.range_by_first_last(int low_msg_num, int high_msg_num) {
|
||||
assert(low_msg_num > 0);
|
||||
assert(high_msg_num > 0);
|
||||
public MessageSet.range_by_first_last(SequenceNumber low_seq_num, SequenceNumber high_seq_num) {
|
||||
assert(low_seq_num.value > 0);
|
||||
assert(high_seq_num.value > 0);
|
||||
|
||||
// correct range problems (i.e. last before first)
|
||||
if (low_msg_num > high_msg_num) {
|
||||
int swap = low_msg_num;
|
||||
low_msg_num = high_msg_num;
|
||||
high_msg_num = swap;
|
||||
if (low_seq_num.value > high_seq_num.value) {
|
||||
SequenceNumber swap = low_seq_num;
|
||||
low_seq_num = high_seq_num;
|
||||
high_seq_num = swap;
|
||||
}
|
||||
|
||||
value = (low_msg_num != high_msg_num)
|
||||
? "%d:%d".printf(low_msg_num, high_msg_num)
|
||||
: "%d".printf(low_msg_num);
|
||||
value = (!low_seq_num.equal_to(high_seq_num))
|
||||
? "%s:%s".printf(low_seq_num.serialize(), high_seq_num.serialize())
|
||||
: low_seq_num.serialize();
|
||||
}
|
||||
|
||||
public MessageSet.uid_range(UID low, UID high) {
|
||||
assert(low.value > 0);
|
||||
assert(high.value > 0);
|
||||
|
||||
value = "%s:%s".printf(low.value.to_string(), high.value.to_string());
|
||||
if (low.equal_to(high))
|
||||
value = low.serialize();
|
||||
else
|
||||
value = "%s:%s".printf(low.serialize(), high.serialize());
|
||||
|
||||
is_uid = true;
|
||||
}
|
||||
|
||||
public MessageSet.range_to_highest(int low_msg_num) {
|
||||
assert(low_msg_num > 0);
|
||||
public MessageSet.range_to_highest(SequenceNumber low_seq_num) {
|
||||
assert(low_seq_num.value > 0);
|
||||
|
||||
value = "%d:*".printf(low_msg_num);
|
||||
value = "%s:*".printf(low_seq_num.serialize());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -79,34 +97,33 @@ public class Geary.Imap.MessageSet : BaseObject {
|
|||
assert(initial.value > 0);
|
||||
|
||||
if (count == 0) {
|
||||
MessageSet.uid(initial);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int64 low, high;
|
||||
if (count < 0) {
|
||||
high = initial.value;
|
||||
low = (high + count).clamp(1, uint32.MAX);
|
||||
value = initial.serialize();
|
||||
} else {
|
||||
// count > 0
|
||||
low = initial.value;
|
||||
high = (low + count).clamp(1, uint32.MAX);
|
||||
int64 low, high;
|
||||
if (count < 0) {
|
||||
high = initial.value;
|
||||
low = (high + count).clamp(1, uint32.MAX);
|
||||
} else {
|
||||
// count > 0
|
||||
low = initial.value;
|
||||
high = (low + count).clamp(1, uint32.MAX);
|
||||
}
|
||||
|
||||
value = "%s:%s".printf(low.to_string(), high.to_string());
|
||||
}
|
||||
|
||||
value = "%s:%s".printf(low.to_string(), high.to_string());
|
||||
is_uid = true;
|
||||
}
|
||||
|
||||
public MessageSet.uid_range_to_highest(UID low) {
|
||||
assert(low.value > 0);
|
||||
|
||||
value = "%s:*".printf(low.value.to_string());
|
||||
value = "%s:*".printf(low.serialize());
|
||||
is_uid = true;
|
||||
}
|
||||
|
||||
public MessageSet.sparse(int[] msg_nums) {
|
||||
value = build_sparse_range(msg_array_to_int64(msg_nums));
|
||||
public MessageSet.sparse(SequenceNumber[] seq_nums) {
|
||||
value = build_sparse_range(seq_array_to_int64(seq_nums));
|
||||
}
|
||||
|
||||
public MessageSet.uid_sparse(UID[] msg_uids) {
|
||||
|
|
@ -121,8 +138,8 @@ public class Geary.Imap.MessageSet : BaseObject {
|
|||
is_uid = true;
|
||||
}
|
||||
|
||||
public MessageSet.sparse_to_highest(int[] msg_nums) {
|
||||
value = "%s:*".printf(build_sparse_range(msg_array_to_int64(msg_nums)));
|
||||
public MessageSet.sparse_to_highest(SequenceNumber[] seq_nums) {
|
||||
value = "%s:*".printf(build_sparse_range(seq_array_to_int64(seq_nums)));
|
||||
}
|
||||
|
||||
public MessageSet.multirange(MessageSet[] msg_sets) {
|
||||
|
|
@ -165,29 +182,29 @@ public class Geary.Imap.MessageSet : BaseObject {
|
|||
// Builds sparse range of either UID values or message numbers.
|
||||
// NOTE: This method assumes the supplied array is internally allocated, and so an in-place sort
|
||||
// is allowable
|
||||
private static string build_sparse_range(int64[] msg_nums) {
|
||||
assert(msg_nums.length > 0);
|
||||
private static string build_sparse_range(int64[] seq_nums) {
|
||||
assert(seq_nums.length > 0);
|
||||
|
||||
// sort array to search for spans
|
||||
qsort(msg_nums, msg_nums.length, sizeof(int64), Numeric.int64_compare);
|
||||
qsort(seq_nums, seq_nums.length, sizeof(int64), Numeric.int64_compare);
|
||||
|
||||
int64 start_of_span = -1;
|
||||
int64 last_msg_num = -1;
|
||||
int64 last_seq_num = -1;
|
||||
int span_count = 0;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (int64 msg_num in msg_nums) {
|
||||
assert(msg_num >= 0);
|
||||
foreach (int64 seq_num in seq_nums) {
|
||||
assert(seq_num >= 0);
|
||||
|
||||
// the first number is automatically the start of a span, although it may be a span of one
|
||||
// (start_of_span < 0 should only happen on first iteration; can't easily break out of
|
||||
// loop because foreach/Iterator would still require a special case to skip it)
|
||||
if (start_of_span < 0) {
|
||||
// start of first span
|
||||
builder.append(msg_num.to_string());
|
||||
builder.append(seq_num.to_string());
|
||||
|
||||
start_of_span = msg_num;
|
||||
start_of_span = seq_num;
|
||||
span_count = 1;
|
||||
} else if ((start_of_span + span_count) == msg_num) {
|
||||
} else if ((start_of_span + span_count) == seq_num) {
|
||||
// span continues
|
||||
span_count++;
|
||||
} else {
|
||||
|
|
@ -195,40 +212,40 @@ public class Geary.Imap.MessageSet : BaseObject {
|
|||
|
||||
// span ends, another begins
|
||||
if (span_count == 1) {
|
||||
builder.append_printf(",%s", msg_num.to_string());
|
||||
builder.append_printf(",%s", seq_num.to_string());
|
||||
} else if (span_count == 2) {
|
||||
builder.append_printf(",%s,%s", (start_of_span + 1).to_string(),
|
||||
msg_num.to_string());
|
||||
seq_num.to_string());
|
||||
} else {
|
||||
builder.append_printf(":%s,%s", (start_of_span + span_count - 1).to_string(),
|
||||
msg_num.to_string());
|
||||
seq_num.to_string());
|
||||
}
|
||||
|
||||
start_of_span = msg_num;
|
||||
start_of_span = seq_num;
|
||||
span_count = 1;
|
||||
}
|
||||
|
||||
last_msg_num = msg_num;
|
||||
last_seq_num = seq_num;
|
||||
}
|
||||
|
||||
// there should always be one msg_num in sorted, so the loop should exit with some state
|
||||
// there should always be one seq_num in sorted, so the loop should exit with some state
|
||||
assert(start_of_span >= 0);
|
||||
assert(span_count > 0);
|
||||
assert(last_msg_num >= 0);
|
||||
assert(last_seq_num >= 0);
|
||||
|
||||
// look for open-ended span
|
||||
if (span_count == 2)
|
||||
builder.append_printf(",%s", last_msg_num.to_string());
|
||||
else
|
||||
builder.append_printf(":%s", last_msg_num.to_string());
|
||||
builder.append_printf(",%s", last_seq_num.to_string());
|
||||
else if (last_seq_num != start_of_span)
|
||||
builder.append_printf(":%s", last_seq_num.to_string());
|
||||
|
||||
return builder.str;
|
||||
}
|
||||
|
||||
private static int64[] msg_array_to_int64(int[] msg_nums) {
|
||||
private static int64[] seq_array_to_int64(SequenceNumber[] seq_nums) {
|
||||
int64[] ret = new int64[0];
|
||||
foreach (int num in msg_nums)
|
||||
ret += (int64) num;
|
||||
foreach (SequenceNumber seq_num in seq_nums)
|
||||
ret += (int64) seq_num.value;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
@ -249,6 +266,10 @@ public class Geary.Imap.MessageSet : BaseObject {
|
|||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link MessageSet} as a {@link Parameter} suitable for inclusion in a
|
||||
* {@link Command}.
|
||||
*/
|
||||
public Parameter to_parameter() {
|
||||
// Message sets are not quoted, even if they use an atom-special character (this *might*
|
||||
// be a Gmailism...)
|
||||
20
src/engine/imap/command/imap-noop-command.vala
Normal file
20
src/engine/imap/command/imap-noop-command.vala
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.1.2]]
|
||||
*
|
||||
* @see IdleCommand
|
||||
*/
|
||||
|
||||
public class Geary.Imap.NoopCommand : Command {
|
||||
public const string NAME = "noop";
|
||||
|
||||
public NoopCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
26
src/engine/imap/command/imap-select-command.vala
Normal file
26
src/engine/imap/command/imap-select-command.vala
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.3.1]]
|
||||
*
|
||||
* @see ExamineCommand
|
||||
*/
|
||||
|
||||
public class Geary.Imap.SelectCommand : Command {
|
||||
public const string NAME = "select";
|
||||
|
||||
public MailboxSpecifier mailbox { get; private set; }
|
||||
|
||||
public SelectCommand(MailboxSpecifier mailbox) {
|
||||
base (NAME);
|
||||
|
||||
this.mailbox = mailbox;
|
||||
|
||||
add(mailbox.to_parameter());
|
||||
}
|
||||
}
|
||||
|
||||
18
src/engine/imap/command/imap-starttls-command.vala
Normal file
18
src/engine/imap/command/imap-starttls-command.vala
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.2.1]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.StarttlsCommand : Command {
|
||||
public const string NAME = "starttls";
|
||||
|
||||
public StarttlsCommand() {
|
||||
base (NAME);
|
||||
}
|
||||
}
|
||||
|
||||
29
src/engine/imap/command/imap-status-command.vala
Normal file
29
src/engine/imap/command/imap-status-command.vala
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.3.10]]
|
||||
*
|
||||
* @see StatusData
|
||||
*/
|
||||
|
||||
public class Geary.Imap.StatusCommand : Command {
|
||||
public const string NAME = "status";
|
||||
|
||||
public StatusCommand(MailboxSpecifier mailbox, StatusDataType[] data_items) {
|
||||
base (NAME);
|
||||
|
||||
add(mailbox.to_parameter());
|
||||
|
||||
assert(data_items.length > 0);
|
||||
ListParameter data_item_list = new ListParameter(this);
|
||||
foreach (StatusDataType data_item in data_items)
|
||||
data_item_list.add(data_item.to_parameter());
|
||||
|
||||
add(data_item_list);
|
||||
}
|
||||
}
|
||||
|
||||
32
src/engine/imap/command/imap-store-command.vala
Normal file
32
src/engine/imap/command/imap-store-command.vala
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.4.6]]
|
||||
*
|
||||
* @see FetchCommand
|
||||
* @see FetchedData
|
||||
*/
|
||||
|
||||
public class Geary.Imap.StoreCommand : Command {
|
||||
public const string NAME = "store";
|
||||
public const string UID_NAME = "uid store";
|
||||
|
||||
public StoreCommand(MessageSet message_set, Gee.List<MessageFlag> flag_list, bool add_flag,
|
||||
bool silent) {
|
||||
base (message_set.is_uid ? UID_NAME : NAME);
|
||||
|
||||
add(message_set.to_parameter());
|
||||
add(new AtomParameter("%sflags%s".printf(add_flag ? "+" : "-", silent ? ".silent" : "")));
|
||||
|
||||
ListParameter list = new ListParameter(this);
|
||||
foreach(MessageFlag flag in flag_list)
|
||||
list.add(new AtomParameter(flag.value));
|
||||
|
||||
add(list);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
/* Copyright 2012-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 class Geary.Imap.CapabilityResults : Geary.Imap.CommandResults {
|
||||
public Capabilities capabilities { get; private set; }
|
||||
|
||||
private CapabilityResults(StatusResponse status_response, Capabilities capabilities) {
|
||||
base (status_response);
|
||||
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
|
||||
public static bool is_capability_response(CommandResponse response) {
|
||||
if (response.server_data.size < 1)
|
||||
return false;
|
||||
|
||||
StringParameter? cmd = response.server_data[0].get_if_string(1);
|
||||
|
||||
return (cmd != null && cmd.equals_ci(CapabilityCommand.NAME));
|
||||
}
|
||||
|
||||
public static CapabilityResults decode(CommandResponse response, ref int next_revision)
|
||||
throws ImapError {
|
||||
assert(response.is_sealed());
|
||||
|
||||
if (!is_capability_response(response))
|
||||
throw new ImapError.PARSE_ERROR("Unrecognized CAPABILITY response line: \"%s\"", response.to_string());
|
||||
|
||||
ServerData data = response.server_data[0];
|
||||
|
||||
// parse the remaining parameters in the response as capabilities
|
||||
Capabilities capabilities = new Capabilities(next_revision++);
|
||||
for (int ctr = 2; ctr < data.get_count(); ctr++) {
|
||||
StringParameter? param = data.get_if_string(ctr);
|
||||
if (param != null)
|
||||
capabilities.add_parameter(param);
|
||||
}
|
||||
|
||||
return new CapabilityResults(response.status_response, capabilities);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,18 +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.Imap.CommandResults : BaseObject {
|
||||
public StatusResponse status_response { get; private set; }
|
||||
|
||||
public CommandResults(StatusResponse status_response) {
|
||||
this.status_response = status_response;
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return status_response.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,114 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* FetchResults represents the data returned from a FETCH response for each message. Since
|
||||
* FETCH allows for multiple FetchDataItems to be requested, this object can hold all of them.
|
||||
*
|
||||
* decode_command_response() will take a CommandResponse for a FETCH command and return all
|
||||
* results for all messages specified.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.FetchResults : Geary.Imap.CommandResults {
|
||||
public int msg_num { get; private set; }
|
||||
|
||||
private Gee.Map<FetchDataType, MessageData> map = new Gee.HashMap<FetchDataType, MessageData>();
|
||||
private Gee.List<Memory.AbstractBuffer> body_data = new Gee.ArrayList<Memory.AbstractBuffer>();
|
||||
|
||||
public FetchResults(StatusResponse status_response, int msg_num) {
|
||||
base (status_response);
|
||||
|
||||
this.msg_num = msg_num;
|
||||
}
|
||||
|
||||
public static FetchResults decode_data(StatusResponse status_response, ServerData data) throws ImapError {
|
||||
StringParameter msg_num = data.get_as_string(1);
|
||||
StringParameter cmd = data.get_as_string(2);
|
||||
ListParameter list = data.get_as_list(3);
|
||||
|
||||
// verify this is a FETCH response
|
||||
if (!cmd.equals_ci(FetchCommand.NAME)) {
|
||||
throw new ImapError.TYPE_ERROR("Unable to decode fetch response \"%s\": Not marked as fetch response",
|
||||
data.to_string());
|
||||
}
|
||||
|
||||
FetchResults results = new FetchResults(status_response, msg_num.as_int());
|
||||
|
||||
// walk the list for each returned fetch data item, which is paired by its data item name
|
||||
// and the structured data itself
|
||||
for (int ctr = 0; ctr < list.get_count(); ctr += 2) {
|
||||
StringParameter data_item_param = list.get_as_string(ctr);
|
||||
|
||||
// watch for truncated lists, which indicate an empty return value
|
||||
bool has_value = (ctr < (list.get_count() - 1));
|
||||
|
||||
if (FetchBodyDataType.is_fetch_body(data_item_param)) {
|
||||
// FETCH body data items are merely a literal of all requested fields formatted
|
||||
// in RFC822 header format ... watch for empty return values and NIL
|
||||
if (has_value)
|
||||
results.body_data.add(list.get_as_empty_literal(ctr + 1).get_buffer());
|
||||
else
|
||||
results.body_data.add(Memory.EmptyBuffer.instance);
|
||||
} else {
|
||||
FetchDataType data_item = FetchDataType.decode(data_item_param.value);
|
||||
FetchDataDecoder? decoder = data_item.get_decoder();
|
||||
if (decoder == null) {
|
||||
debug("Unable to decode fetch response for \"%s\": No decoder available",
|
||||
data_item.to_string());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// watch for empty return values
|
||||
if (has_value)
|
||||
results.set_data(data_item, decoder.decode(list.get_required(ctr + 1)));
|
||||
else
|
||||
results.set_data(data_item, decoder.decode(NilParameter.instance));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public static FetchResults[] decode(CommandResponse response) {
|
||||
assert(response.is_sealed());
|
||||
|
||||
FetchResults[] array = new FetchResults[0];
|
||||
foreach (ServerData data in response.server_data) {
|
||||
try {
|
||||
array += decode_data(response.status_response, data);
|
||||
} catch (ImapError ierr) {
|
||||
// drop bad data on the ground
|
||||
debug("Dropping FETCH data \"%s\": %s", data.to_string(), ierr.message);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
public Gee.Set<FetchDataType> get_all_types() {
|
||||
return map.keys;
|
||||
}
|
||||
|
||||
private new void set_data(FetchDataType data_item, MessageData primitive) {
|
||||
map.set(data_item, primitive);
|
||||
}
|
||||
|
||||
public new MessageData? get_data(FetchDataType data_item) {
|
||||
return map.get(data_item);
|
||||
}
|
||||
|
||||
public Gee.List<Memory.AbstractBuffer> get_body_data() {
|
||||
return body_data.read_only_view;
|
||||
}
|
||||
|
||||
public int get_count() {
|
||||
return map.size;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,138 +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 class Geary.Imap.MailboxInformation : BaseObject {
|
||||
public string name { get; private set; }
|
||||
public string? delim { get; private set; }
|
||||
public MailboxAttributes attrs { get; private set; }
|
||||
|
||||
public MailboxInformation(string name, string? delim, MailboxAttributes attrs) {
|
||||
this.name = name;
|
||||
this.delim = delim;
|
||||
this.attrs = attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will always return a list with at least one element in it. If no delimiter is specified,
|
||||
* the name is returned as a single element.
|
||||
*/
|
||||
public Gee.List<string> get_path() {
|
||||
Gee.List<string> path = new Gee.ArrayList<string>();
|
||||
|
||||
if (!String.is_empty(delim)) {
|
||||
string[] split = name.split(delim);
|
||||
foreach (string str in split) {
|
||||
if (!String.is_empty(str))
|
||||
path.add(str);
|
||||
}
|
||||
}
|
||||
|
||||
if (path.size == 0)
|
||||
path.add(name);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* If name is non-empty, will return a non-empty value which is the final folder name (i.e.
|
||||
* the parent components are stripped). If no delimiter is specified, the name is returned.
|
||||
*/
|
||||
public string get_basename() {
|
||||
if (String.is_empty(delim))
|
||||
return name;
|
||||
|
||||
int index = name.last_index_of(delim);
|
||||
if (index < 0)
|
||||
return name;
|
||||
|
||||
string basename = name.substring(index + 1);
|
||||
|
||||
return !String.is_empty(basename) ? basename : name;
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.ListResults : Geary.Imap.CommandResults {
|
||||
private Gee.List<MailboxInformation> list;
|
||||
private Gee.Map<string, MailboxInformation> map;
|
||||
|
||||
private ListResults(StatusResponse status_response, Gee.Map<string, MailboxInformation> map,
|
||||
Gee.List<MailboxInformation> list) {
|
||||
base (status_response);
|
||||
|
||||
this.map = map;
|
||||
this.list = list;
|
||||
}
|
||||
|
||||
public static ListResults decode(CommandResponse response) {
|
||||
assert(response.is_sealed());
|
||||
|
||||
Gee.List<MailboxInformation> list = new Gee.ArrayList<MailboxInformation>();
|
||||
Gee.Map<string, MailboxInformation> map = new Gee.HashMap<string, MailboxInformation>();
|
||||
foreach (ServerData data in response.server_data) {
|
||||
try {
|
||||
StringParameter cmd = data.get_as_string(1);
|
||||
ListParameter attrs = data.get_as_list(2);
|
||||
StringParameter? delim = data.get_as_nullable_string(3);
|
||||
MailboxParameter mailbox = new MailboxParameter.from_string_parameter(data.get_as_string(4));
|
||||
|
||||
if (!cmd.equals_ci(ListCommand.NAME) && !cmd.equals_ci(ListCommand.XLIST_NAME)) {
|
||||
debug("Bad list response \"%s\": Not marked as list or xlist response",
|
||||
data.to_string());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
Gee.Collection<MailboxAttribute> attrlist = new Gee.ArrayList<MailboxAttribute>();
|
||||
foreach (Parameter attr in attrs.get_all()) {
|
||||
StringParameter? stringp = attr as StringParameter;
|
||||
if (stringp == null) {
|
||||
debug("Bad list attribute \"%s\": Attribute not a string value",
|
||||
data.to_string());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
attrlist.add(new MailboxAttribute(stringp.value));
|
||||
}
|
||||
|
||||
// Set \Inbox to standard path
|
||||
MailboxInformation info;
|
||||
MailboxAttributes attributes = new MailboxAttributes(attrlist);
|
||||
if (Geary.Imap.MailboxAttribute.SPECIAL_FOLDER_INBOX in attributes) {
|
||||
info = new MailboxInformation(Geary.Imap.Account.INBOX_NAME,
|
||||
(delim != null) ? delim.nullable_value : null, attributes);
|
||||
} else {
|
||||
info = new MailboxInformation(mailbox.decode(),
|
||||
(delim != null) ? delim.nullable_value : null, attributes);
|
||||
}
|
||||
|
||||
map.set(mailbox.decode(), info);
|
||||
list.add(info);
|
||||
} catch (ImapError ierr) {
|
||||
debug("Unable to decode \"%s\": %s", data.to_string(), ierr.message);
|
||||
}
|
||||
}
|
||||
|
||||
return new ListResults(response.status_response, map, list);
|
||||
}
|
||||
|
||||
public int get_count() {
|
||||
return list.size;
|
||||
}
|
||||
|
||||
public Gee.Collection<string> get_names() {
|
||||
return map.keys;
|
||||
}
|
||||
|
||||
public Gee.List<MailboxInformation> get_all() {
|
||||
return list;
|
||||
}
|
||||
|
||||
public MailboxInformation? get_info(string name) {
|
||||
return map.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,135 +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 class Geary.Imap.SelectExamineResults : Geary.Imap.CommandResults {
|
||||
/**
|
||||
* -1 if not specified.
|
||||
*/
|
||||
public int exists { get; private set; }
|
||||
/**
|
||||
* -1 if not specified.
|
||||
*/
|
||||
public int recent { get; private set; }
|
||||
public MessageNumber? unseen_position { get; private set; }
|
||||
public UIDValidity? uid_validity { get; private set; }
|
||||
public UID? uid_next { get; private set; }
|
||||
public Flags? flags { get; private set; }
|
||||
public Flags? permanentflags { get; private set; }
|
||||
public bool readonly { get; private set; }
|
||||
|
||||
private SelectExamineResults(StatusResponse status_response, int exists, int recent, MessageNumber? unseen_position,
|
||||
UIDValidity? uid_validity, UID? uid_next, Flags? flags, Flags? permanentflags, bool readonly) {
|
||||
base (status_response);
|
||||
|
||||
this.exists = exists;
|
||||
this.recent = recent;
|
||||
this.unseen_position = unseen_position;
|
||||
this.uid_validity = uid_validity;
|
||||
this.uid_next = uid_next;
|
||||
this.flags = flags;
|
||||
this.permanentflags = permanentflags;
|
||||
this.readonly = readonly;
|
||||
}
|
||||
|
||||
public static SelectExamineResults decode(CommandResponse response) throws ImapError {
|
||||
assert(response.is_sealed());
|
||||
|
||||
int exists = -1;
|
||||
int recent = -1;
|
||||
MessageNumber? unseen_position = null;
|
||||
UIDValidity? uid_validity = null;
|
||||
UID? uid_next = null;
|
||||
MessageFlags? flags = null;
|
||||
MessageFlags? permanentflags = null;
|
||||
|
||||
bool readonly = true;
|
||||
try {
|
||||
readonly = response.status_response.response_code.get_as_string(0).value.down() != "read-write";
|
||||
} catch (ImapError ierr) {
|
||||
message("Invalid SELECT/EXAMINE read-write indicator: %s",
|
||||
response.status_response.to_string());
|
||||
}
|
||||
|
||||
foreach (ServerData data in response.server_data) {
|
||||
try {
|
||||
StringParameter stringp = data.get_as_string(1);
|
||||
switch (stringp.value.down()) {
|
||||
case "ok":
|
||||
// ok lines are structured like StatusResponses
|
||||
StatusResponse ok_response = new StatusResponse.migrate(data);
|
||||
if (ok_response.response_code == null) {
|
||||
message("Invalid SELECT/EXAMINE response \"%s\": no response code",
|
||||
data.to_string());
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// the ResponseCode is what we're interested in
|
||||
switch (ok_response.response_code.get_code_type()) {
|
||||
case ResponseCodeType.UNSEEN:
|
||||
unseen_position = new MessageNumber(
|
||||
ok_response.response_code.get_as_string(1).as_int(1, int.MAX));
|
||||
break;
|
||||
|
||||
case ResponseCodeType.UIDVALIDITY:
|
||||
uid_validity = new UIDValidity(
|
||||
ok_response.response_code.get_as_string(1).as_int());
|
||||
break;
|
||||
|
||||
case ResponseCodeType.UIDNEXT:
|
||||
uid_next = new UID(ok_response.response_code.get_as_string(1).as_int());
|
||||
break;
|
||||
|
||||
case ResponseCodeType.PERMANENT_FLAGS:
|
||||
permanentflags = MessageFlags.from_list(
|
||||
ok_response.response_code.get_as_list(1));
|
||||
break;
|
||||
|
||||
case ResponseCodeType.MYRIGHTS:
|
||||
// not implemented
|
||||
break;
|
||||
|
||||
default:
|
||||
message("Unknown line in SELECT/EXAMINE response: \"%s\"", data.to_string());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case "flags":
|
||||
flags = MessageFlags.from_list(data.get_as_list(2));
|
||||
break;
|
||||
|
||||
default:
|
||||
// if second parameter is a type descriptor, stringp is an ordinal
|
||||
switch (ServerDataType.from_parameter(data.get_as_string(2))) {
|
||||
case ServerDataType.EXISTS:
|
||||
exists = stringp.as_int(0, int.MAX);
|
||||
break;
|
||||
|
||||
case ServerDataType.RECENT:
|
||||
recent = stringp.as_int(0, int.MAX);
|
||||
break;
|
||||
|
||||
default:
|
||||
message("Unknown line in SELECT/EXAMINE response: \"%s\"", data.to_string());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (ImapError ierr) {
|
||||
debug("SELECT/EXAMINE: unable to decode \"%s\", ignored: %s", data.to_string(), ierr.message);
|
||||
}
|
||||
}
|
||||
|
||||
// flags, exists, and recent are required
|
||||
if (flags == null || exists < 0 || recent < 0)
|
||||
throw new ImapError.PARSE_ERROR("Incomplete SELECT/EXAMINE Response: \"%s\"", response.to_string());
|
||||
|
||||
return new SelectExamineResults(response.status_response, exists, recent, unseen_position,
|
||||
uid_validity, uid_next, flags, permanentflags, readonly);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,16 +4,53 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Possible Errors thrown by various components in the {@link Geary.Imap} namespace.
|
||||
*
|
||||
*/
|
||||
|
||||
public errordomain Geary.ImapError {
|
||||
/**
|
||||
* Indicates a basic parsing error, syntactic in nature.
|
||||
*/
|
||||
PARSE_ERROR,
|
||||
/**
|
||||
* Indicates a type conversion error.
|
||||
*
|
||||
* This largely occurs inside of {@link Imap.ListParameter}, where various
|
||||
* {@link Imap.Parameter}s are retrieved by specific type according to the flavor of the
|
||||
* response.
|
||||
*/
|
||||
TYPE_ERROR,
|
||||
SERVER_ERROR,
|
||||
/**
|
||||
* Indicates an operation failed because a network connection had not been established.
|
||||
*/
|
||||
NOT_CONNECTED,
|
||||
COMMAND_FAILED,
|
||||
/**
|
||||
* Indicates the connection is already established or authentication has been granted.
|
||||
*/
|
||||
ALREADY_CONNECTED,
|
||||
/**
|
||||
* Indicates a request failed according to a returned response.
|
||||
*/
|
||||
SERVER_ERROR,
|
||||
/**
|
||||
* Indicates that an operation could not proceed without prior authentication.
|
||||
*/
|
||||
UNAUTHENTICATED,
|
||||
/**
|
||||
* An operation is not supported by the IMAP stack or by the server.
|
||||
*/
|
||||
NOT_SUPPORTED,
|
||||
NOT_SELECTED,
|
||||
INVALID_PATH,
|
||||
TIMED_OUT
|
||||
/**
|
||||
* Indicates a basic parsing error, semantic in nature.
|
||||
*/
|
||||
INVALID,
|
||||
/**
|
||||
* A network connection of some kind failed due to an expired timer.
|
||||
*
|
||||
* This indicates a local time out, not one reported by the server.
|
||||
*/
|
||||
TIMED_OUT,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ internal void init() {
|
|||
|
||||
MessageFlag.init();
|
||||
MailboxAttribute.init();
|
||||
Tag.init();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,34 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A symbolic representation of IMAP FETCH's BODY section parameter.
|
||||
*
|
||||
* This is only used with {@link FetchCommand}. Most IMAP FETCH calls can be achieved with
|
||||
* plain {@link FetchDataType} specifiers. Some cannot, however, and this more complicated
|
||||
* specifier must be used.
|
||||
*
|
||||
* A fully-qualified specifier looks something like this:
|
||||
*
|
||||
* BODY[part_number.section_part]<subset_start.subset_count>
|
||||
*
|
||||
* or, when headers are specified:
|
||||
*
|
||||
* BODY[part_number.section_part (header_fields)]<subset_start.subset_count>
|
||||
*
|
||||
* Note that Gmail apparently doesn't like BODY[1.TEXT] and instead must be specified with
|
||||
* BODY[1]. Also note that there's a PEEK variant that will not add the /Seen flag to the message.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.4.5]], specifically section on
|
||||
* BODY[<section>]<<partial>>.
|
||||
*
|
||||
* @see FetchDataType
|
||||
*/
|
||||
|
||||
public class Geary.Imap.FetchBodyDataType : BaseObject {
|
||||
/**
|
||||
* Specifies which section (or partial section) is being requested with this identifier.
|
||||
*/
|
||||
public enum SectionPart {
|
||||
NONE,
|
||||
HEADER,
|
||||
|
|
@ -51,24 +78,13 @@ public class Geary.Imap.FetchBodyDataType : BaseObject {
|
|||
private bool is_peek;
|
||||
|
||||
/**
|
||||
* See RFC-3501 6.4.5 for some light beach reading on how the FETCH body data specifier is formed.
|
||||
*
|
||||
* A fully-qualified specifier looks something like this:
|
||||
*
|
||||
* BODY[part_number.section_part]<subset_start.subset_count>
|
||||
*
|
||||
* or, when headers are specified:
|
||||
*
|
||||
* BODY[part_number.section_part (header_fields)]<subset_start.subset_count>
|
||||
*
|
||||
* Note that Gmail apparently doesn't like BODY[1.TEXT] and instead must be specified with
|
||||
* BODY[1].
|
||||
* Create a FetchBodyDataType with the various required and optional parameters specified.
|
||||
*
|
||||
* Set part_number to null to ignore. Set partial_start less than zero to ignore.
|
||||
* partial_count must be greater than zero if partial_start is greater than zero.
|
||||
*
|
||||
* field_names are required for SectionPart.HEADER_FIELDS and SectionPart.HEADER_FIELDS_NOT
|
||||
* and must be null for all other SectionParts.
|
||||
* field_names are required for {@link SectionPart.HEADER_FIELDS} and
|
||||
* {@link SectionPart.HEADER_FIELDS_NOT} and must be null for all other {@link SectionPart}s.
|
||||
*/
|
||||
public FetchBodyDataType(SectionPart section_part, int[]? part_number, int partial_start,
|
||||
int partial_count, string[]? field_names) {
|
||||
|
|
@ -76,7 +92,7 @@ public class Geary.Imap.FetchBodyDataType : BaseObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Like FetchBodyDataType, but the /seen flag will not be set when used on a message.
|
||||
* Like FetchBodyDataType, but the /Seen flag will not be set when used on a message.
|
||||
*/
|
||||
public FetchBodyDataType.peek(SectionPart section_part, int[]? part_number, int partial_start,
|
||||
int partial_count, string[]? field_names) {
|
||||
|
|
@ -107,14 +123,36 @@ public class Geary.Imap.FetchBodyDataType : BaseObject {
|
|||
this.is_peek = is_peek;
|
||||
}
|
||||
|
||||
public string serialize() {
|
||||
return to_string();
|
||||
/**
|
||||
* Returns the {@link FetchBodyDataType} in a string ready for a {@link Command}.
|
||||
*/
|
||||
public string serialize_request() {
|
||||
return (!is_peek ? "body[%s%s%s]%s" : "body.peek[%s%s%s]%s").printf(
|
||||
serialize_part_number(),
|
||||
section_part.serialize(),
|
||||
serialize_field_names(),
|
||||
serialize_partial(true));
|
||||
}
|
||||
|
||||
public Parameter to_parameter() {
|
||||
// Because of the kooky formatting of the Body[section]<partial> fetch field, use an
|
||||
// unquoted string and format it ourselves.
|
||||
return new UnquotedStringParameter(serialize());
|
||||
/**
|
||||
* Returns the {@link FetchBodyDataType} in a string as it might appear in a
|
||||
* {@link ServerResponse}.
|
||||
*
|
||||
* The FetchBodyDataType server response does not include the peek modifier or the span
|
||||
* length if a span was indicated (as the following literal specifies its length).
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.4.2]]
|
||||
*/
|
||||
internal string serialize_response() {
|
||||
return "body[%s%s%s]%s".printf(
|
||||
serialize_part_number(),
|
||||
section_part.serialize(),
|
||||
serialize_field_names(),
|
||||
serialize_partial(false));
|
||||
}
|
||||
|
||||
public Parameter to_request_parameter() {
|
||||
return new AtomParameter(serialize_request());
|
||||
}
|
||||
|
||||
private string serialize_part_number() {
|
||||
|
|
@ -152,22 +190,43 @@ public class Geary.Imap.FetchBodyDataType : BaseObject {
|
|||
return builder.str;
|
||||
}
|
||||
|
||||
private string serialize_partial() {
|
||||
return (partial_start < 0) ? "" : "<%d.%d>".printf(partial_start, partial_count);
|
||||
// See note at serialize_response for reason is_request is necessary.
|
||||
private string serialize_partial(bool is_request) {
|
||||
if (is_request)
|
||||
return (partial_start < 0) ? "" : "<%d.%d>".printf(partial_start, partial_count);
|
||||
else
|
||||
return (partial_start < 0) ? "" : "<%d>".printf(partial_start);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the {@link StringParameter} is formatted like a {@link FetchBodyDataType}.
|
||||
*
|
||||
* Currently this test isn't perfect and should only be used as a guide. There is no
|
||||
* decode or deserialize method for FetchBodyDataType.
|
||||
*
|
||||
* @see get_identifier
|
||||
*/
|
||||
public static bool is_fetch_body(StringParameter items) {
|
||||
string strd = items.value.down();
|
||||
|
||||
return strd.has_prefix("body[") || strd.has_prefix("body.peek[");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link FetchBodyDataIdentifier} than can be used to compare results from
|
||||
* server responses with requested specifiers.
|
||||
*
|
||||
* Because no decode or deserialize method currently exists for {@link FetchBodyDataType},
|
||||
* the easiest way to determine if a response contains requested data is to compare it with
|
||||
* this returned object.
|
||||
*/
|
||||
public FetchBodyDataIdentifier get_identifier() {
|
||||
return new FetchBodyDataIdentifier(this);
|
||||
}
|
||||
|
||||
// Return serialize() because it's a more fuller representation than what the server returns
|
||||
public string to_string() {
|
||||
return (!is_peek ? "body[%s%s%s]%s" : "body.peek[%s%s%s]%s").printf(
|
||||
serialize_part_number(),
|
||||
section_part.serialize(),
|
||||
serialize_field_names(),
|
||||
serialize_partial());
|
||||
return serialize_request();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,18 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A symbolic representation of IMAP FETCH's specifier parameter.
|
||||
*
|
||||
* Most FETCH requests can use this simple specifier to return various parts of the message.
|
||||
* More complicated requests (and requests for partial header or body sections) must use a
|
||||
* {@link FetchBodyDataType} specifier.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.4.5]]
|
||||
*
|
||||
* @see FetchBodyDataType
|
||||
*/
|
||||
|
||||
public enum Geary.Imap.FetchDataType {
|
||||
UID,
|
||||
FLAGS,
|
||||
|
|
@ -65,6 +77,11 @@ public enum Geary.Imap.FetchDataType {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a plain string into a {@link FetchDataType}.
|
||||
*
|
||||
* @throws ImapError.PARSE_ERROR if not a recognized value.
|
||||
*/
|
||||
public static FetchDataType decode(string value) throws ImapError {
|
||||
switch (value.down()) {
|
||||
case "uid":
|
||||
|
|
@ -111,14 +128,30 @@ public enum Geary.Imap.FetchDataType {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns this {@link FetchDataType} into a {@link StringParameter} for transmission.
|
||||
*/
|
||||
public StringParameter to_parameter() {
|
||||
return new StringParameter(to_string());
|
||||
return new AtomParameter(to_string());
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoders a {@link StringParameter} into a {@link FetchDataType} using {@link decode}.
|
||||
*
|
||||
* @see decode
|
||||
*/
|
||||
public static FetchDataType from_parameter(StringParameter strparam) throws ImapError {
|
||||
return decode(strparam.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate {@link FetchDataDecoder} for this {@link FetchDataType}.
|
||||
*
|
||||
* The FetchDataDecoder can then be used to convert the associated {@link Parameter}s into
|
||||
* {@link Imap.MessageData}.
|
||||
*
|
||||
* @return null if no FetchDataDecoder is associated with this value, or an invalid value.
|
||||
*/
|
||||
public FetchDataDecoder? get_decoder() {
|
||||
switch (this) {
|
||||
case UID:
|
||||
|
|
|
|||
|
|
@ -4,6 +4,15 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A generic IMAP message or mailbox flag.
|
||||
*
|
||||
* In IMAP, message and mailbox flags have similar syntax, which is encapsulated here.
|
||||
*
|
||||
* @see MessageFlag
|
||||
* @see MailboxAttribute
|
||||
*/
|
||||
|
||||
public abstract class Geary.Imap.Flag : BaseObject, Gee.Hashable<Geary.Imap.Flag> {
|
||||
public string value { get; private set; }
|
||||
|
||||
|
|
@ -32,256 +41,5 @@ public abstract class Geary.Imap.Flag : BaseObject, Gee.Hashable<Geary.Imap.Flag
|
|||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.MessageFlag : Geary.Imap.Flag {
|
||||
private static MessageFlag? _answered = null;
|
||||
public static MessageFlag ANSWERED { get {
|
||||
if (_answered == null)
|
||||
_answered = new MessageFlag("\\answered");
|
||||
|
||||
return _answered;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _deleted = null;
|
||||
public static MessageFlag DELETED { get {
|
||||
if (_deleted == null)
|
||||
_deleted = new MessageFlag("\\deleted");
|
||||
|
||||
return _deleted;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _draft = null;
|
||||
public static MessageFlag DRAFT { get {
|
||||
if (_draft == null)
|
||||
_draft = new MessageFlag("\\draft");
|
||||
|
||||
return _draft;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _flagged = null;
|
||||
public static MessageFlag FLAGGED { get {
|
||||
if (_flagged == null)
|
||||
_flagged = new MessageFlag("\\flagged");
|
||||
|
||||
return _flagged;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _recent = null;
|
||||
public static MessageFlag RECENT { get {
|
||||
if (_recent == null)
|
||||
_recent = new MessageFlag("\\recent");
|
||||
|
||||
return _recent;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _seen = null;
|
||||
public static MessageFlag SEEN { get {
|
||||
if (_seen == null)
|
||||
_seen = new MessageFlag("\\seen");
|
||||
|
||||
return _seen;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _allows_new = null;
|
||||
public static MessageFlag ALLOWS_NEW { get {
|
||||
if (_allows_new == null)
|
||||
_allows_new = new MessageFlag("\\*");
|
||||
|
||||
return _allows_new;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _load_remote_images = null;
|
||||
public static MessageFlag LOAD_REMOTE_IMAGES { get {
|
||||
if (_load_remote_images == null)
|
||||
_load_remote_images = new MessageFlag("LoadRemoteImages");
|
||||
|
||||
return _load_remote_images;
|
||||
} }
|
||||
|
||||
public MessageFlag(string value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
// Call these at init time to prevent thread issues
|
||||
internal static void init() {
|
||||
MessageFlag to_init = ANSWERED;
|
||||
to_init = DELETED;
|
||||
to_init = DRAFT;
|
||||
to_init = FLAGGED;
|
||||
to_init = RECENT;
|
||||
to_init = SEEN;
|
||||
to_init = ALLOWS_NEW;
|
||||
}
|
||||
|
||||
// Converts a list of email flags to add and remove to a list of message
|
||||
// flags to add and remove.
|
||||
public static void from_email_flags(Geary.EmailFlags? email_flags_add,
|
||||
Geary.EmailFlags? email_flags_remove, out Gee.List<MessageFlag> msg_flags_add,
|
||||
out Gee.List<MessageFlag> msg_flags_remove) {
|
||||
msg_flags_add = new Gee.ArrayList<MessageFlag>();
|
||||
msg_flags_remove = new Gee.ArrayList<MessageFlag>();
|
||||
|
||||
if (email_flags_add != null) {
|
||||
if (email_flags_add.contains(Geary.EmailFlags.UNREAD))
|
||||
msg_flags_remove.add(MessageFlag.SEEN);
|
||||
if (email_flags_add.contains(Geary.EmailFlags.FLAGGED))
|
||||
msg_flags_add.add(MessageFlag.FLAGGED);
|
||||
if (email_flags_add.contains(Geary.EmailFlags.LOAD_REMOTE_IMAGES))
|
||||
msg_flags_add.add(MessageFlag.LOAD_REMOTE_IMAGES);
|
||||
}
|
||||
|
||||
if (email_flags_remove != null) {
|
||||
if (email_flags_remove.contains(Geary.EmailFlags.UNREAD))
|
||||
msg_flags_add.add(MessageFlag.SEEN);
|
||||
if (email_flags_remove.contains(Geary.EmailFlags.FLAGGED))
|
||||
msg_flags_remove.add(MessageFlag.FLAGGED);
|
||||
if (email_flags_remove.contains(Geary.EmailFlags.LOAD_REMOTE_IMAGES))
|
||||
msg_flags_remove.add(MessageFlag.LOAD_REMOTE_IMAGES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.MailboxAttribute : Geary.Imap.Flag {
|
||||
private static MailboxAttribute? _no_inferiors = null;
|
||||
public static MailboxAttribute NO_INFERIORS { get {
|
||||
if (_no_inferiors == null)
|
||||
_no_inferiors = new MailboxAttribute("\\noinferiors");
|
||||
|
||||
return _no_inferiors;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _no_select = null;
|
||||
public static MailboxAttribute NO_SELECT { get {
|
||||
if (_no_select == null)
|
||||
_no_select = new MailboxAttribute("\\noselect");
|
||||
|
||||
return _no_select;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _marked = null;
|
||||
public static MailboxAttribute MARKED { get {
|
||||
if (_marked == null)
|
||||
_marked = new MailboxAttribute("\\marked");
|
||||
|
||||
return _marked;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _unmarked = null;
|
||||
public static MailboxAttribute UNMARKED { get {
|
||||
if (_unmarked == null)
|
||||
_unmarked = new MailboxAttribute("\\unmarked");
|
||||
|
||||
return _unmarked;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _has_no_children = null;
|
||||
public static MailboxAttribute HAS_NO_CHILDREN { get {
|
||||
if (_has_no_children == null)
|
||||
_has_no_children = new MailboxAttribute("\\hasnochildren");
|
||||
|
||||
return _has_no_children;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _has_children = null;
|
||||
public static MailboxAttribute HAS_CHILDREN { get {
|
||||
if (_has_children == null)
|
||||
_has_children = new MailboxAttribute("\\haschildren");
|
||||
|
||||
return _has_children;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _allows_new = null;
|
||||
public static MailboxAttribute ALLOWS_NEW { get {
|
||||
if (_allows_new == null)
|
||||
_allows_new = new MailboxAttribute("\\*");
|
||||
|
||||
return _allows_new;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_inbox = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_INBOX { get {
|
||||
if (_xlist_inbox == null)
|
||||
_xlist_inbox = new MailboxAttribute("\\Inbox");
|
||||
|
||||
return _xlist_inbox;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_all_mail = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_ALL_MAIL { get {
|
||||
if (_xlist_all_mail == null)
|
||||
_xlist_all_mail = new MailboxAttribute("\\AllMail");
|
||||
|
||||
return _xlist_all_mail;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_trash = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_TRASH { get {
|
||||
if (_xlist_trash == null)
|
||||
_xlist_trash = new MailboxAttribute("\\Trash");
|
||||
|
||||
return _xlist_trash;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_drafts = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_DRAFTS { get {
|
||||
if (_xlist_drafts == null)
|
||||
_xlist_drafts = new MailboxAttribute("\\Drafts");
|
||||
|
||||
return _xlist_drafts;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_sent = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_SENT { get {
|
||||
if (_xlist_sent == null)
|
||||
_xlist_sent = new MailboxAttribute("\\Sent");
|
||||
|
||||
return _xlist_sent;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_spam = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_SPAM { get {
|
||||
if (_xlist_spam == null)
|
||||
_xlist_spam = new MailboxAttribute("\\Spam");
|
||||
|
||||
return _xlist_spam;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_starred = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_STARRED { get {
|
||||
if (_xlist_starred == null)
|
||||
_xlist_starred = new MailboxAttribute("\\Starred");
|
||||
|
||||
return _xlist_starred;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_important = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_IMPORTANT { get {
|
||||
if (_xlist_important == null)
|
||||
_xlist_important = new MailboxAttribute("\\Important");
|
||||
|
||||
return _xlist_important;
|
||||
} }
|
||||
|
||||
public MailboxAttribute(string value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
// Call these at init time to prevent thread issues
|
||||
internal static void init() {
|
||||
MailboxAttribute to_init = NO_INFERIORS;
|
||||
to_init = NO_SELECT;
|
||||
to_init = MARKED;
|
||||
to_init = UNMARKED;
|
||||
to_init = HAS_NO_CHILDREN;
|
||||
to_init = HAS_CHILDREN;
|
||||
to_init = ALLOWS_NEW;
|
||||
to_init = SPECIAL_FOLDER_ALL_MAIL;
|
||||
to_init = SPECIAL_FOLDER_DRAFTS;
|
||||
to_init = SPECIAL_FOLDER_IMPORTANT;
|
||||
to_init = SPECIAL_FOLDER_INBOX;
|
||||
to_init = SPECIAL_FOLDER_SENT;
|
||||
to_init = SPECIAL_FOLDER_SPAM;
|
||||
to_init = SPECIAL_FOLDER_STARRED;
|
||||
to_init = SPECIAL_FOLDER_TRASH;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
69
src/engine/imap/message/imap-flags.vala
Normal file
69
src/engine/imap/message/imap-flags.vala
Normal 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A generic collection of {@link Flags}.
|
||||
*/
|
||||
|
||||
public abstract class Geary.Imap.Flags : Geary.MessageData.AbstractMessageData, Geary.Imap.MessageData,
|
||||
Gee.Hashable<Geary.Imap.Flags> {
|
||||
public int size { get { return list.size; } }
|
||||
|
||||
protected Gee.Set<Flag> list;
|
||||
|
||||
public Flags(Gee.Collection<Flag> flags) {
|
||||
list = new Gee.HashSet<Flag>();
|
||||
list.add_all(flags);
|
||||
}
|
||||
|
||||
public bool contains(Flag flag) {
|
||||
return list.contains(flag);
|
||||
}
|
||||
|
||||
public Gee.Set<Flag> get_all() {
|
||||
return list.read_only_view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the flags in serialized form, which is each flag separated by a space (legal in
|
||||
* IMAP, as flags must be atoms and atoms prohibit spaces).
|
||||
*/
|
||||
public virtual string serialize() {
|
||||
return to_string();
|
||||
}
|
||||
|
||||
public bool equal_to(Geary.Imap.Flags other) {
|
||||
if (this == other)
|
||||
return true;
|
||||
|
||||
if (other.size != size)
|
||||
return false;
|
||||
|
||||
foreach (Flag flag in list) {
|
||||
if (!other.contains(flag))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (Flag flag in list) {
|
||||
if (!String.is_empty(builder.str))
|
||||
builder.append_c(' ');
|
||||
|
||||
builder.append(flag.value);
|
||||
}
|
||||
|
||||
return builder.str;
|
||||
}
|
||||
|
||||
public uint hash() {
|
||||
return to_string().down().hash();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5,12 +5,22 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* A StringParameter that holds a mailbox reference (can be wildcarded). Used
|
||||
* to juggle between our internal UTF-8 representation of mailboxes and IMAP's
|
||||
* A {@link StringParameter} that holds a mailbox reference (can be wildcarded).
|
||||
*
|
||||
* Used to juggle between our internal UTF-8 representation of mailboxes and IMAP's
|
||||
* odd "modified UTF-7" representation. The value is stored in IMAP's encoded
|
||||
* format since that's how it comes across the wire.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.MailboxParameter : StringParameter {
|
||||
public MailboxParameter(string mailbox) {
|
||||
base (utf8_to_imap_utf7(mailbox));
|
||||
}
|
||||
|
||||
public MailboxParameter.from_string_parameter(StringParameter string_parameter) {
|
||||
base (string_parameter.value);
|
||||
}
|
||||
|
||||
private static string utf8_to_imap_utf7(string utf8) {
|
||||
try {
|
||||
return Geary.ImapUtf7.utf8_to_imap_utf7(utf8);
|
||||
|
|
@ -29,15 +39,22 @@ public class Geary.Imap.MailboxParameter : StringParameter {
|
|||
}
|
||||
}
|
||||
|
||||
public MailboxParameter(string mailbox) {
|
||||
base (utf8_to_imap_utf7(mailbox));
|
||||
}
|
||||
|
||||
public MailboxParameter.from_string_parameter(StringParameter string_parameter) {
|
||||
base (string_parameter.value);
|
||||
}
|
||||
|
||||
public string decode() {
|
||||
return imap_utf7_to_utf8(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
serialize_string(ser);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override string to_string() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
156
src/engine/imap/message/imap-mailbox-specifier.vala
Normal file
156
src/engine/imap/message/imap-mailbox-specifier.vala
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents an IMAP mailbox name or path (more commonly known as a folder).
|
||||
*
|
||||
* Can also be used to specify a wildcarded name for the {@link ListCommand}.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-5.1]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.MailboxSpecifier : BaseObject, Gee.Hashable<MailboxSpecifier>, Gee.Comparable<MailboxSpecifier> {
|
||||
/**
|
||||
* An instance of an Inbox MailboxSpecifier.
|
||||
*
|
||||
* This is a utility for creating the single IMAP Inbox. {@link compare_to}. {@link hash},
|
||||
* and {@link equal_to} do not rely on this instance for comparison.
|
||||
*/
|
||||
private static MailboxSpecifier? _inbox = null;
|
||||
public static MailboxSpecifier inbox {
|
||||
get {
|
||||
return (_inbox != null) ? _inbox : _inbox = new MailboxSpecifier("Inbox");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decoded mailbox path name.
|
||||
*/
|
||||
public string name { get; private set; }
|
||||
|
||||
/**
|
||||
* Indicates this is the {@link StatusData} for Inbox.
|
||||
*
|
||||
* IMAP guarantees only one mailbox in an account: Inbox.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-5.1]]
|
||||
*/
|
||||
public bool is_inbox { get; private set; }
|
||||
|
||||
public MailboxSpecifier(string name) {
|
||||
init(name);
|
||||
}
|
||||
|
||||
public MailboxSpecifier.from_parameter(MailboxParameter param) {
|
||||
init(param.decode());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a generic {@link FolderPath} into an IMAP mailbox specifier.
|
||||
*
|
||||
* If the delimiter was supplied from a {@link ListCommand} response, it can be supplied here.
|
||||
* Otherwise, the path's {@link FolderRoot.default_separator} will be used. If neither have a
|
||||
* separator, {@link ImapError.INVALID} is thrown.
|
||||
*/
|
||||
public MailboxSpecifier.from_folder_path(FolderPath path, string? delim) throws ImapError {
|
||||
string? fullpath = path.get_fullpath(delim);
|
||||
if (fullpath == null) {
|
||||
throw new ImapError.INVALID("Unable to convert FolderPath to MailboxSpecifier: no delimiter for %s",
|
||||
path.to_string());
|
||||
}
|
||||
|
||||
init(fullpath);
|
||||
}
|
||||
|
||||
private void init(string decoded) {
|
||||
name = decoded;
|
||||
is_inbox = name.down() == "inbox";
|
||||
}
|
||||
|
||||
/**
|
||||
* The mailbox's path as a list of strings.
|
||||
*
|
||||
* Will always return a list with at least one element in it. If no delimiter is specified,
|
||||
* the name is returned as a single element.
|
||||
*/
|
||||
public Gee.List<string> to_list(string? delim) {
|
||||
Gee.List<string> path = new Gee.ArrayList<string>();
|
||||
|
||||
if (!String.is_empty(delim)) {
|
||||
string[] split = name.split(delim);
|
||||
foreach (string str in split) {
|
||||
if (!String.is_empty(str))
|
||||
path.add(str);
|
||||
}
|
||||
}
|
||||
|
||||
if (path.size == 0)
|
||||
path.add(name);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public FolderPath to_folder_path(string? delim = null) {
|
||||
Gee.List<string> list = to_list(delim);
|
||||
FolderPath path = new FolderRoot(list[0], delim, !is_inbox);
|
||||
for (int ctr = 1; ctr < list.size; ctr++)
|
||||
path = path.get_child(list[ctr]);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* The mailbox's name without parent folders.
|
||||
*
|
||||
* If name is non-empty, will return a non-empty value which is the final folder name (i.e.
|
||||
* the parent components are stripped). If no delimiter is specified, the name is returned.
|
||||
*/
|
||||
public string get_basename(string? delim) {
|
||||
if (String.is_empty(delim))
|
||||
return name;
|
||||
|
||||
int index = name.last_index_of(delim);
|
||||
if (index < 0)
|
||||
return name;
|
||||
|
||||
string basename = name.substring(index + 1);
|
||||
|
||||
return !String.is_empty(basename) ? basename : name;
|
||||
}
|
||||
|
||||
public Parameter to_parameter() {
|
||||
return new MailboxParameter(name);
|
||||
}
|
||||
|
||||
public uint hash() {
|
||||
return is_inbox ? String.stri_hash(name) : name.hash();
|
||||
}
|
||||
|
||||
public bool equal_to(MailboxSpecifier other) {
|
||||
if (this == other)
|
||||
return true;
|
||||
|
||||
if (is_inbox)
|
||||
return String.stri_equal(name, other.name);
|
||||
|
||||
return name == other.name;
|
||||
}
|
||||
|
||||
public int compare_to(MailboxSpecifier other) {
|
||||
if (this == other)
|
||||
return 0;
|
||||
|
||||
if (is_inbox && other.is_inbox)
|
||||
return 0;
|
||||
|
||||
return strcmp(name, other.name);
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -6,233 +6,19 @@
|
|||
|
||||
/**
|
||||
* MessageData is an IMAP data structure delivered in some form by the server to the client.
|
||||
* Although the primary use of this object is for FETCH results, other commands return
|
||||
* similarly-structured data (in particulars Flags and Attributes).
|
||||
*
|
||||
*
|
||||
* Note that IMAP specifies that Flags and Attributes are *always* returned as a list, even if only
|
||||
* one is present, which is why these elements are MessageData but not the elements within the
|
||||
* lists (Flag, Attribute).
|
||||
*
|
||||
* Also note that Imap.MessageData requires {@link Geary.MessageData.AbstractMessageData}.
|
||||
*
|
||||
* TODO: Add an abstract to_parameter() method that can be used to serialize the message data.
|
||||
*/
|
||||
|
||||
public interface Geary.Imap.MessageData : Geary.MessageData.AbstractMessageData {
|
||||
}
|
||||
|
||||
public class Geary.Imap.UID : Geary.MessageData.Int64MessageData, Geary.Imap.MessageData, Gee.Comparable<Geary.Imap.UID> {
|
||||
// Using statics because int32.MAX is static, not const (??)
|
||||
public static int64 MIN = 1;
|
||||
public static int64 MAX = int32.MAX;
|
||||
public static int64 INVALID = -1;
|
||||
|
||||
public UID(int64 value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
public bool is_valid() {
|
||||
return is_value_valid(value);
|
||||
}
|
||||
|
||||
public static bool is_value_valid(int64 val) {
|
||||
return Numeric.int64_in_range_inclusive(val, MIN, MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid UID, which means returning MIN or MAX if the value is out of range (either
|
||||
* direction) or MAX if this value is already MAX.
|
||||
*/
|
||||
public UID next() {
|
||||
if (value < MIN)
|
||||
return new UID(MIN);
|
||||
else if (value > MAX)
|
||||
return new UID(MAX);
|
||||
else
|
||||
return new UID(Numeric.int64_ceiling(value + 1, MAX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid UID, which means returning MIN or MAX if the value is out of range (either
|
||||
* direction) or MIN if this value is already MIN.
|
||||
*/
|
||||
public UID previous() {
|
||||
if (value < MIN)
|
||||
return new UID(MIN);
|
||||
else if (value > MAX)
|
||||
return new UID(MAX);
|
||||
else
|
||||
return new UID(Numeric.int64_floor(value - 1, MIN));
|
||||
}
|
||||
|
||||
public virtual int compare_to(Geary.Imap.UID other) {
|
||||
if (value < other.value)
|
||||
return -1;
|
||||
else if (value > other.value)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.UIDValidity : Geary.MessageData.Int64MessageData, Geary.Imap.MessageData {
|
||||
// Using statics because int32.MAX is static, not const (??)
|
||||
public static int64 MIN = 1;
|
||||
public static int64 MAX = int32.MAX;
|
||||
public static int64 INVALID = -1;
|
||||
|
||||
public UIDValidity(int64 value) {
|
||||
base (value);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.MessageNumber : Geary.MessageData.IntMessageData, Geary.Imap.MessageData {
|
||||
public MessageNumber(int value) {
|
||||
base (value);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class Geary.Imap.Flags : Geary.MessageData.AbstractMessageData, Geary.Imap.MessageData,
|
||||
Gee.Hashable<Geary.Imap.Flags> {
|
||||
public int size { get { return list.size; } }
|
||||
|
||||
protected Gee.Set<Flag> list;
|
||||
|
||||
public Flags(Gee.Collection<Flag> flags) {
|
||||
list = new Gee.HashSet<Flag>();
|
||||
list.add_all(flags);
|
||||
}
|
||||
|
||||
public bool contains(Flag flag) {
|
||||
return list.contains(flag);
|
||||
}
|
||||
|
||||
public Gee.Set<Flag> get_all() {
|
||||
return list.read_only_view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the flags in serialized form, which is each flag separated by a space (legal in
|
||||
* IMAP, as flags must be atoms and atoms prohibit spaces).
|
||||
*/
|
||||
public virtual string serialize() {
|
||||
return to_string();
|
||||
}
|
||||
|
||||
public bool equal_to(Geary.Imap.Flags other) {
|
||||
if (this == other)
|
||||
return true;
|
||||
|
||||
if (other.size != size)
|
||||
return false;
|
||||
|
||||
foreach (Flag flag in list) {
|
||||
if (!other.contains(flag))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (Flag flag in list) {
|
||||
if (!String.is_empty(builder.str))
|
||||
builder.append_c(' ');
|
||||
|
||||
builder.append(flag.value);
|
||||
}
|
||||
|
||||
return builder.str;
|
||||
}
|
||||
|
||||
public uint hash() {
|
||||
return to_string().hash();
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.MessageFlags : Geary.Imap.Flags {
|
||||
public MessageFlags(Gee.Collection<MessageFlag> flags) {
|
||||
base (flags);
|
||||
}
|
||||
|
||||
public static MessageFlags from_list(ListParameter listp) throws ImapError {
|
||||
Gee.Collection<MessageFlag> list = new Gee.ArrayList<MessageFlag>();
|
||||
for (int ctr = 0; ctr < listp.get_count(); ctr++)
|
||||
list.add(new MessageFlag(listp.get_as_string(ctr).value));
|
||||
|
||||
return new MessageFlags(list);
|
||||
}
|
||||
|
||||
public static MessageFlags deserialize(string str) {
|
||||
string[] tokens = str.split(" ");
|
||||
|
||||
Gee.Collection<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
|
||||
foreach (string token in tokens)
|
||||
flags.add(new MessageFlag(token));
|
||||
|
||||
return new MessageFlags(flags);
|
||||
}
|
||||
|
||||
internal void add(MessageFlag flag) {
|
||||
list.add(flag);
|
||||
}
|
||||
|
||||
internal void remove(MessageFlag flag) {
|
||||
list.remove(flag);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.MailboxAttributes : Geary.Imap.Flags {
|
||||
public MailboxAttributes(Gee.Collection<MailboxAttribute> attrs) {
|
||||
base (attrs);
|
||||
}
|
||||
|
||||
public static MailboxAttributes from_list(ListParameter listp) throws ImapError {
|
||||
Gee.Collection<MailboxAttribute> list = new Gee.ArrayList<MailboxAttribute>();
|
||||
for (int ctr = 0; ctr < listp.get_count(); ctr++)
|
||||
list.add(new MailboxAttribute(listp.get_as_string(ctr).value));
|
||||
|
||||
return new MailboxAttributes(list);
|
||||
}
|
||||
|
||||
public static MailboxAttributes deserialize(string str) {
|
||||
string[] tokens = str.split(" ");
|
||||
|
||||
Gee.Collection<MailboxAttribute> attrs = new Gee.ArrayList<MailboxAttribute>();
|
||||
foreach (string token in tokens)
|
||||
attrs.add(new MailboxAttribute(token));
|
||||
|
||||
return new MailboxAttributes(attrs);
|
||||
}
|
||||
|
||||
public Geary.SpecialFolderType get_special_folder_type() {
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_INBOX))
|
||||
return Geary.SpecialFolderType.INBOX;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_ALL_MAIL))
|
||||
return Geary.SpecialFolderType.ALL_MAIL;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_TRASH))
|
||||
return Geary.SpecialFolderType.TRASH;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_DRAFTS))
|
||||
return Geary.SpecialFolderType.DRAFTS;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_SENT))
|
||||
return Geary.SpecialFolderType.SENT;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_SPAM))
|
||||
return Geary.SpecialFolderType.SPAM;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_STARRED))
|
||||
return Geary.SpecialFolderType.FLAGGED;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_IMPORTANT))
|
||||
return Geary.SpecialFolderType.IMPORTANT;
|
||||
|
||||
return Geary.SpecialFolderType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.InternalDate : Geary.RFC822.Date, Geary.Imap.MessageData {
|
||||
public InternalDate(string iso8601) throws ImapError {
|
||||
base (iso8601);
|
||||
|
|
|
|||
124
src/engine/imap/message/imap-message-flag.vala
Normal file
124
src/engine/imap/message/imap-message-flag.vala
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
/* 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 IMAP message (email) flag.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-2.3.2]]
|
||||
*
|
||||
* @see StoreCommand
|
||||
* @see FetchCommand
|
||||
* @see FetchedData
|
||||
*/
|
||||
|
||||
public class Geary.Imap.MessageFlag : Geary.Imap.Flag {
|
||||
private static MessageFlag? _answered = null;
|
||||
public static MessageFlag ANSWERED { get {
|
||||
if (_answered == null)
|
||||
_answered = new MessageFlag("\\answered");
|
||||
|
||||
return _answered;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _deleted = null;
|
||||
public static MessageFlag DELETED { get {
|
||||
if (_deleted == null)
|
||||
_deleted = new MessageFlag("\\deleted");
|
||||
|
||||
return _deleted;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _draft = null;
|
||||
public static MessageFlag DRAFT { get {
|
||||
if (_draft == null)
|
||||
_draft = new MessageFlag("\\draft");
|
||||
|
||||
return _draft;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _flagged = null;
|
||||
public static MessageFlag FLAGGED { get {
|
||||
if (_flagged == null)
|
||||
_flagged = new MessageFlag("\\flagged");
|
||||
|
||||
return _flagged;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _recent = null;
|
||||
public static MessageFlag RECENT { get {
|
||||
if (_recent == null)
|
||||
_recent = new MessageFlag("\\recent");
|
||||
|
||||
return _recent;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _seen = null;
|
||||
public static MessageFlag SEEN { get {
|
||||
if (_seen == null)
|
||||
_seen = new MessageFlag("\\seen");
|
||||
|
||||
return _seen;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _allows_new = null;
|
||||
public static MessageFlag ALLOWS_NEW { get {
|
||||
if (_allows_new == null)
|
||||
_allows_new = new MessageFlag("\\*");
|
||||
|
||||
return _allows_new;
|
||||
} }
|
||||
|
||||
private static MessageFlag? _load_remote_images = null;
|
||||
public static MessageFlag LOAD_REMOTE_IMAGES { get {
|
||||
if (_load_remote_images == null)
|
||||
_load_remote_images = new MessageFlag("LoadRemoteImages");
|
||||
|
||||
return _load_remote_images;
|
||||
} }
|
||||
|
||||
public MessageFlag(string value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
// Call these at init time to prevent thread issues
|
||||
internal static void init() {
|
||||
MessageFlag to_init = ANSWERED;
|
||||
to_init = DELETED;
|
||||
to_init = DRAFT;
|
||||
to_init = FLAGGED;
|
||||
to_init = RECENT;
|
||||
to_init = SEEN;
|
||||
to_init = ALLOWS_NEW;
|
||||
}
|
||||
|
||||
// Converts a list of email flags to add and remove to a list of message
|
||||
// flags to add and remove.
|
||||
public static void from_email_flags(Geary.EmailFlags? email_flags_add,
|
||||
Geary.EmailFlags? email_flags_remove, out Gee.List<MessageFlag> msg_flags_add,
|
||||
out Gee.List<MessageFlag> msg_flags_remove) {
|
||||
msg_flags_add = new Gee.ArrayList<MessageFlag>();
|
||||
msg_flags_remove = new Gee.ArrayList<MessageFlag>();
|
||||
|
||||
if (email_flags_add != null) {
|
||||
if (email_flags_add.contains(Geary.EmailFlags.UNREAD))
|
||||
msg_flags_remove.add(MessageFlag.SEEN);
|
||||
if (email_flags_add.contains(Geary.EmailFlags.FLAGGED))
|
||||
msg_flags_add.add(MessageFlag.FLAGGED);
|
||||
if (email_flags_add.contains(Geary.EmailFlags.LOAD_REMOTE_IMAGES))
|
||||
msg_flags_add.add(MessageFlag.LOAD_REMOTE_IMAGES);
|
||||
}
|
||||
|
||||
if (email_flags_remove != null) {
|
||||
if (email_flags_remove.contains(Geary.EmailFlags.UNREAD))
|
||||
msg_flags_add.add(MessageFlag.SEEN);
|
||||
if (email_flags_remove.contains(Geary.EmailFlags.FLAGGED))
|
||||
msg_flags_remove.add(MessageFlag.FLAGGED);
|
||||
if (email_flags_remove.contains(Geary.EmailFlags.LOAD_REMOTE_IMAGES))
|
||||
msg_flags_remove.add(MessageFlag.LOAD_REMOTE_IMAGES);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
55
src/engine/imap/message/imap-message-flags.vala
Normal file
55
src/engine/imap/message/imap-message-flags.vala
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A collection of {@link MessageFlag}s.
|
||||
*
|
||||
* @see StoreCommand
|
||||
* @see FetchCommand
|
||||
* @see FetchedData
|
||||
*/
|
||||
|
||||
public class Geary.Imap.MessageFlags : Geary.Imap.Flags {
|
||||
public MessageFlags(Gee.Collection<MessageFlag> flags) {
|
||||
base (flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link MessageFlags} from a {@link ListParameter} of flag strings.
|
||||
*/
|
||||
public static MessageFlags from_list(ListParameter listp) throws ImapError {
|
||||
Gee.Collection<MessageFlag> list = new Gee.ArrayList<MessageFlag>();
|
||||
for (int ctr = 0; ctr < listp.get_count(); ctr++)
|
||||
list.add(new MessageFlag(listp.get_as_string(ctr).value));
|
||||
|
||||
return new MessageFlags(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link MessageFlags} from a flat string of space-delimited flags.
|
||||
*/
|
||||
public static MessageFlags deserialize(string? str) {
|
||||
if (String.is_empty(str))
|
||||
return new MessageFlags(new Gee.ArrayList<MessageFlag>());
|
||||
|
||||
string[] tokens = str.split(" ");
|
||||
|
||||
Gee.Collection<MessageFlag> flags = new Gee.ArrayList<MessageFlag>();
|
||||
foreach (string token in tokens)
|
||||
flags.add(new MessageFlag(token));
|
||||
|
||||
return new MessageFlags(flags);
|
||||
}
|
||||
|
||||
internal void add(MessageFlag flag) {
|
||||
list.add(flag);
|
||||
}
|
||||
|
||||
internal void remove(MessageFlag flag) {
|
||||
list.remove(flag);
|
||||
}
|
||||
}
|
||||
|
||||
40
src/engine/imap/message/imap-sequence-number.vala
Normal file
40
src/engine/imap/message/imap-sequence-number.vala
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/* 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 IMAP's sequence number, i.e. positional addressing within a mailbox.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-2.3.1.2]]
|
||||
*
|
||||
* @see UID
|
||||
*/
|
||||
|
||||
public class Geary.Imap.SequenceNumber : Geary.MessageData.IntMessageData, Geary.Imap.MessageData,
|
||||
Gee.Comparable<SequenceNumber> {
|
||||
public SequenceNumber(int value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts an array of ints into an array of {@link SequenceNumber}s.
|
||||
*/
|
||||
public static SequenceNumber[] to_list(int[] value_array) {
|
||||
SequenceNumber[] list = new SequenceNumber[0];
|
||||
foreach (int value in value_array)
|
||||
list += new SequenceNumber(value);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public virtual int compare_to(SequenceNumber other) {
|
||||
return value - other.value;
|
||||
}
|
||||
|
||||
public string serialize() {
|
||||
return value.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,6 +4,14 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A representation of the types of data to be found in a STATUS response.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.2.4]]
|
||||
*
|
||||
* @see StatusData
|
||||
*/
|
||||
|
||||
public enum Geary.Imap.StatusDataType {
|
||||
MESSAGES,
|
||||
RECENT,
|
||||
|
|
@ -60,7 +68,7 @@ public enum Geary.Imap.StatusDataType {
|
|||
}
|
||||
|
||||
public StringParameter to_parameter() {
|
||||
return new StringParameter(to_string());
|
||||
return new AtomParameter(to_string());
|
||||
}
|
||||
|
||||
public static StatusDataType from_parameter(StringParameter stringp) throws ImapError {
|
||||
|
|
@ -4,7 +4,18 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.Tag : StringParameter, Gee.Hashable<Geary.Imap.Tag> {
|
||||
/**
|
||||
* A representation of an IMAP command tag.
|
||||
*
|
||||
* Tags are assigned by the client for each {@link Command} it sends to the server. Tags have
|
||||
* a general form of <a-z><000-999>, although that's only by convention and is not required.
|
||||
*
|
||||
* Special tags exist, namely to indicated an untagged response and continuations.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-2.2.1]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.Tag : AtomParameter, Gee.Hashable<Geary.Imap.Tag> {
|
||||
public const string UNTAGGED_VALUE = "*";
|
||||
public const string CONTINUATION_VALUE = "+";
|
||||
public const string UNASSIGNED_VALUE = "----";
|
||||
|
|
@ -21,6 +32,12 @@ public class Geary.Imap.Tag : StringParameter, Gee.Hashable<Geary.Imap.Tag> {
|
|||
base (strparam.value);
|
||||
}
|
||||
|
||||
internal static void init() {
|
||||
get_untagged();
|
||||
get_continuation();
|
||||
get_unassigned();
|
||||
}
|
||||
|
||||
public static Tag get_untagged() {
|
||||
if (untagged == null)
|
||||
untagged = new Tag(UNTAGGED_VALUE);
|
||||
|
|
@ -42,6 +59,30 @@ public class Geary.Imap.Tag : StringParameter, Gee.Hashable<Geary.Imap.Tag> {
|
|||
return unassigned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the StringParameter resembles a tag token: an unquoted non-empty string
|
||||
* that either matches the untagged or continuation special tags or
|
||||
*/
|
||||
public static bool is_tag(StringParameter stringp) {
|
||||
if (stringp is QuotedStringParameter)
|
||||
return false;
|
||||
|
||||
if (String.is_empty(stringp.value))
|
||||
return false;
|
||||
|
||||
if (stringp.value == UNTAGGED_VALUE || stringp.value == CONTINUATION_VALUE)
|
||||
return true;
|
||||
|
||||
int index = 0;
|
||||
unichar ch;
|
||||
while (stringp.value.get_next_char(ref index, out ch)) {
|
||||
if (DataFormat.is_tag_special(ch))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool is_tagged() {
|
||||
return (value != UNTAGGED_VALUE) && (value != CONTINUATION_VALUE);
|
||||
}
|
||||
|
|
|
|||
25
src/engine/imap/message/imap-uid-validity.vala
Normal file
25
src/engine/imap/message/imap-uid-validity.vala
Normal 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* A representation of IMAP's UIDVALIDITY.
|
||||
*
|
||||
* See [[tools.ietf.org/html/rfc3501#section-2.3.1.1]]
|
||||
*
|
||||
* @see UID
|
||||
*/
|
||||
|
||||
public class Geary.Imap.UIDValidity : Geary.MessageData.Int64MessageData, Geary.Imap.MessageData {
|
||||
// Using statics because int32.MAX is static, not const (??)
|
||||
public static int64 MIN = 1;
|
||||
public static int64 MAX = int32.MAX;
|
||||
public static int64 INVALID = -1;
|
||||
|
||||
public UIDValidity(int64 value) {
|
||||
base (value);
|
||||
}
|
||||
}
|
||||
|
||||
73
src/engine/imap/message/imap-uid.vala
Normal file
73
src/engine/imap/message/imap-uid.vala
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An IMAP UID.
|
||||
*
|
||||
* See [[tools.ietf.org/html/rfc3501#section-2.3.1.1]]
|
||||
*
|
||||
* @see SequenceNumber
|
||||
*/
|
||||
|
||||
public class Geary.Imap.UID : Geary.MessageData.Int64MessageData, Geary.Imap.MessageData,
|
||||
Gee.Comparable<Geary.Imap.UID> {
|
||||
// Using statics because int32.MAX is static, not const (??)
|
||||
public static int64 MIN = 1;
|
||||
public static int64 MAX = int32.MAX;
|
||||
public static int64 INVALID = -1;
|
||||
|
||||
public UID(int64 value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
public bool is_valid() {
|
||||
return is_value_valid(value);
|
||||
}
|
||||
|
||||
public static bool is_value_valid(int64 val) {
|
||||
return Numeric.int64_in_range_inclusive(val, MIN, MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid UID, which means returning MIN or MAX if the value is out of range (either
|
||||
* direction) or MAX if this value is already MAX.
|
||||
*/
|
||||
public UID next() {
|
||||
if (value < MIN)
|
||||
return new UID(MIN);
|
||||
else if (value > MAX)
|
||||
return new UID(MAX);
|
||||
else
|
||||
return new UID(Numeric.int64_ceiling(value + 1, MAX));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a valid UID, which means returning MIN or MAX if the value is out of range (either
|
||||
* direction) or MIN if this value is already MIN.
|
||||
*/
|
||||
public UID previous() {
|
||||
if (value < MIN)
|
||||
return new UID(MIN);
|
||||
else if (value > MAX)
|
||||
return new UID(MAX);
|
||||
else
|
||||
return new UID(Numeric.int64_floor(value - 1, MIN));
|
||||
}
|
||||
|
||||
public virtual int compare_to(Geary.Imap.UID other) {
|
||||
if (value < other.value)
|
||||
return -1;
|
||||
else if (value > other.value)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
public string serialize() {
|
||||
return value.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
32
src/engine/imap/parameter/imap-atom-parameter.vala
Normal file
32
src/engine/imap/parameter/imap-atom-parameter.vala
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
/* 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 an IMAP atom.
|
||||
*
|
||||
* This class does not check if quoting is required. Use {@link DataFormat.is_quoting_required}
|
||||
* or {@link StringParameter.get_best_for}.
|
||||
*
|
||||
* See {@link StringParameter} for a note about class heirarchy. In particular, note that
|
||||
* [@link Deserializer} will not create this type of {@link Parameter} because it's unable to
|
||||
* deduce if a string is an atom or a string from syntax alone.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-4.1]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.AtomParameter : Geary.Imap.UnquotedStringParameter {
|
||||
public AtomParameter(string value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_unquoted_string(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,156 +4,11 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
public abstract class Geary.Imap.Parameter : BaseObject, Serializable {
|
||||
public abstract async void serialize(Serializer ser) throws Error;
|
||||
|
||||
/**
|
||||
* to_string() returns a representation of the Parameter suitable for logging and debugging,
|
||||
* but should not be relied upon for wire or persistent representation.
|
||||
*/
|
||||
public abstract string to_string();
|
||||
}
|
||||
|
||||
public class Geary.Imap.NilParameter : Geary.Imap.Parameter {
|
||||
public const string VALUE = "NIL";
|
||||
|
||||
private static NilParameter? _instance = null;
|
||||
|
||||
public static NilParameter instance {
|
||||
get {
|
||||
if (_instance == null)
|
||||
_instance = new NilParameter();
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private NilParameter() {
|
||||
}
|
||||
|
||||
public static bool is_nil(string str) {
|
||||
return String.ascii_equali(VALUE, str);
|
||||
}
|
||||
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_nil();
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.StringParameter : Geary.Imap.Parameter {
|
||||
public string value { get; private set; }
|
||||
public string? nullable_value {
|
||||
get {
|
||||
return String.is_empty(value) ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
public StringParameter(string value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public bool equals_cs(string value) {
|
||||
return this.value == value;
|
||||
}
|
||||
|
||||
public bool equals_ci(string value) {
|
||||
return this.value.down() == value.down();
|
||||
}
|
||||
|
||||
// TODO: This does not check that the value is a properly-formed integer. This should be
|
||||
// added later.
|
||||
public int as_int(int clamp_min = int.MIN, int clamp_max = int.MAX) throws ImapError {
|
||||
return int.parse(value).clamp(clamp_min, clamp_max);
|
||||
}
|
||||
|
||||
// TODO: This does not check that the value is a properly-formed long.
|
||||
public long as_long(int clamp_min = int.MIN, int clamp_max = int.MAX) throws ImapError {
|
||||
return long.parse(value).clamp(clamp_min, clamp_max);
|
||||
}
|
||||
|
||||
// TODO: This does not check that the value is a properly-formed int64.
|
||||
public int64 as_int64(int64 clamp_min = int64.MIN, int64 clamp_max = int64.MAX) throws ImapError {
|
||||
return int64.parse(value).clamp(clamp_min, clamp_max);
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_string(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This delivers the string to the IMAP server with quoting applied whether or not it's required.
|
||||
* (Deserializer will never generate this Parameter.) This is generally legal, but some servers may
|
||||
* not appreciate it.
|
||||
* The representation of an IMAP parenthesized list.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-4.4]]
|
||||
*/
|
||||
public class Geary.Imap.QuotedStringParameter : Geary.Imap.StringParameter {
|
||||
public QuotedStringParameter(string value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_quoted_string(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This delivers the string to the IMAP server with no quoting or formatting applied. (Deserializer
|
||||
* will never generate this Parameter.) This can lead to server errors if misused. Use only if
|
||||
* absolutely necessary.
|
||||
*/
|
||||
public class Geary.Imap.UnquotedStringParameter : Geary.Imap.StringParameter {
|
||||
public UnquotedStringParameter(string value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_unquoted_string(value);
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.LiteralParameter : Geary.Imap.Parameter {
|
||||
private Geary.Memory.AbstractBuffer buffer;
|
||||
|
||||
public LiteralParameter(Geary.Memory.AbstractBuffer buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
public size_t get_size() {
|
||||
return buffer.get_size();
|
||||
}
|
||||
|
||||
public Geary.Memory.AbstractBuffer get_buffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the LiteralParameter as though it had been a StringParameter on the wire. Note
|
||||
* that this does not deal with quoting issues or NIL (which should never be literalized to
|
||||
* begin with). It merely converts the literal data to a UTF-8 string and returns it as a
|
||||
* StringParameter.
|
||||
*/
|
||||
public StringParameter to_string_parameter() {
|
||||
return new StringParameter(buffer.to_valid_utf8());
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return "{literal/%lub}".printf(get_size());
|
||||
}
|
||||
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_string("{%lu}".printf(get_size()));
|
||||
ser.push_eol();
|
||||
yield ser.push_input_stream_literal_data_async(buffer.get_input_stream());
|
||||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
||||
/**
|
||||
|
|
@ -172,21 +27,35 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
add(initial);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if no parent (top-level list).
|
||||
*
|
||||
* In a fully-formed set of {@link Parameter}s, this means this {@link ListParameter} is
|
||||
* probably a {@link RootParameters}.
|
||||
*/
|
||||
public ListParameter? get_parent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public void add(Parameter param) {
|
||||
bool added = list.add(param);
|
||||
assert(added);
|
||||
/**
|
||||
* Returns true if added.
|
||||
*
|
||||
* The same {@link Parameter} can't be added more than once to the same {@link ListParameter}.
|
||||
*/
|
||||
public bool add(Parameter param) {
|
||||
return list.add(param);
|
||||
}
|
||||
|
||||
public int get_count() {
|
||||
return list.size;
|
||||
}
|
||||
|
||||
//
|
||||
// Parameter retrieval
|
||||
//
|
||||
|
||||
/**
|
||||
* Returns the Parameter at the index in the list, null if index is out of range.
|
||||
* Returns the {@link Parameter} at the index in the list, null if index is out of range.
|
||||
*
|
||||
* TODO: This call can cause memory leaks when used with the "as" operator until the following
|
||||
* Vala bug is fixed (probably in version 0.19.1).
|
||||
|
|
@ -216,11 +85,14 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns Paramater at index if in range and of Type type, otherwise throws an
|
||||
* ImapError.TYPE_ERROR. type must be of type Parameter.
|
||||
* Returns {@link Paramater} at index if in range and of Type type, otherwise throws an
|
||||
* {@link ImapError.TYPE_ERROR}.
|
||||
*
|
||||
* type must be of type Parameter.
|
||||
*/
|
||||
public Parameter get_as(int index, Type type) throws ImapError {
|
||||
assert(type.is_a(typeof(Parameter)));
|
||||
if (!type.is_a(typeof(Parameter)))
|
||||
throw new ImapError.TYPE_ERROR("Attempting to cast non-Parameter at index %d", index);
|
||||
|
||||
Parameter param = get_required(index);
|
||||
if (!param.get_type().is_a(type)) {
|
||||
|
|
@ -232,15 +104,25 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Like get_as(), but returns null if the Parameter at index is a NilParameter.
|
||||
* Like {@link get_as}, but returns null if the {@link Parameter} at index is a
|
||||
* {@link NilParameter}.
|
||||
*
|
||||
* type must be of type Parameter.
|
||||
*/
|
||||
public Parameter? get_as_nullable(int index, Type type) throws ImapError {
|
||||
assert(type.is_a(typeof(Parameter)));
|
||||
if (!type.is_a(typeof(Parameter)))
|
||||
throw new ImapError.TYPE_ERROR("Attempting to cast non-Parameter at index %d", index);
|
||||
|
||||
Parameter param = get_required(index);
|
||||
if (param is NilParameter)
|
||||
return null;
|
||||
|
||||
// Because Deserializer doesn't produce NilParameters, check manually if this Parameter
|
||||
// can legally be NIL according to IMAP grammer.
|
||||
StringParameter? stringp = param as StringParameter;
|
||||
if (stringp != null && NilParameter.is_nil(stringp))
|
||||
return null;
|
||||
|
||||
if (!param.get_type().is_a(type)) {
|
||||
throw new ImapError.TYPE_ERROR("Parameter %d is not of type %s (is %s)", index,
|
||||
type.name(), param.get_type().name());
|
||||
|
|
@ -250,11 +132,13 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Like get(), but returns null if Parameter at index is not of the specified type. type must
|
||||
* be of type Parameter.
|
||||
* Like {@link get}, but returns null if {@link Parameter} at index is not of the specified type.
|
||||
*
|
||||
* type must be of type Parameter.
|
||||
*/
|
||||
public Parameter? get_if(int index, Type type) {
|
||||
assert(type.is_a(typeof(Parameter)));
|
||||
if (!type.is_a(typeof(Parameter)))
|
||||
return null;
|
||||
|
||||
Parameter? param = get(index);
|
||||
if (param == null || !param.get_type().is_a(type))
|
||||
|
|
@ -263,44 +147,30 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
return param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a StringParameter only if the Parameter at index is a StringParameter (quoted or
|
||||
* atom string).
|
||||
*/
|
||||
public StringParameter get_only_as_string(int index) throws ImapError {
|
||||
return (StringParameter) get_as(index, typeof(StringParameter));
|
||||
}
|
||||
//
|
||||
// String retrieval
|
||||
//
|
||||
|
||||
/**
|
||||
* Returns a StringParameter only if the Parameter at index is a StringParameter (quoted or
|
||||
* atom string).
|
||||
* Returns a {@link StringParameter} only if the {@link Parameter} at index is a StringParameter.
|
||||
*
|
||||
* Compare to {@link get_if_string_or_literal}.
|
||||
*/
|
||||
public StringParameter? get_only_as_nullable_string(int index) throws ImapError {
|
||||
return (StringParameter?) get_as_nullable(index, typeof(StringParameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a StringParameter only if the Parameter at index is a StringParameter (quoted or
|
||||
* atom string). Returns an empty StringParameter if index is for a NilParameter;
|
||||
*/
|
||||
public StringParameter get_only_as_empty_string(int index) throws ImapError {
|
||||
StringParameter? param = get_only_as_nullable_string(index);
|
||||
|
||||
return param ?? new StringParameter("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a StringParameter only if the Parameter at index is a StringParameter (quoted or
|
||||
* atom string).
|
||||
*/
|
||||
public StringParameter? get_only_if_string(int index) {
|
||||
public StringParameter? get_if_string(int index) {
|
||||
return (StringParameter?) get_if(index, typeof(StringParameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the StringParameter at the index only if the Parameter is a StringParameter or a
|
||||
* LiteralParameter with a length less than or equal to MAX_STRING_LITERAL_LENGTH. Throws an
|
||||
* ImapError.TYPE_ERROR if a literal longer than that value.
|
||||
* Returns a {@link StringParameter} for the value at the index only if the {@link Parameter}
|
||||
* is a StringParameter or a {@link LiteralParameter} with a length less than or equal to
|
||||
* {@link MAX_STRING_LITERAL_LENGTH}.
|
||||
*
|
||||
* Because literal data is being coerced into a StringParameter, the result may not be suitable
|
||||
* for transmission as-is.
|
||||
*
|
||||
* @see get_as_nullable_string
|
||||
* @throws ImapError.TYPE_ERROR if no StringParameter at index or the literal is longer than
|
||||
* MAX_STRING_LITERAL_LENGTH.
|
||||
*/
|
||||
public StringParameter get_as_string(int index) throws ImapError {
|
||||
Parameter param = get_required(index);
|
||||
|
|
@ -311,16 +181,23 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
|
||||
LiteralParameter? literalp = param as LiteralParameter;
|
||||
if (literalp != null && literalp.get_size() <= MAX_STRING_LITERAL_LENGTH)
|
||||
return literalp.to_string_parameter();
|
||||
return literalp.coerce_to_string_parameter();
|
||||
|
||||
throw new ImapError.TYPE_ERROR("Parameter %d not of type string or literal (is %s)", index,
|
||||
param.get_type().name());
|
||||
}
|
||||
|
||||
/**
|
||||
* Much like get_nullable() for StringParameters, but will convert a LiteralParameter to a
|
||||
* StringParameter if its length is less than or equal to MAX_STRING_LITERAL_LENGTH. Throws
|
||||
* an ImapError.TYPE_ERROR if literal is longer than that value.
|
||||
* Returns a {@link StringParameter} for the value at the index only if the {@link Parameter}
|
||||
* is a StringParameter or a {@link LiteralParameter} with a length less than or equal to
|
||||
* {@link MAX_STRING_LITERAL_LENGTH}.
|
||||
*
|
||||
* Because literal data is being coerced into a StringParameter, the result may not be suitable
|
||||
* for transmission as-is.
|
||||
*
|
||||
* @return null if no StringParameter or LiteralParameter at index.
|
||||
* @see get_as_string
|
||||
* @throws ImapError.TYPE_ERROR if literal is longer than MAX_STRING_LITERAL_LENGTH.
|
||||
*/
|
||||
public StringParameter? get_as_nullable_string(int index) throws ImapError {
|
||||
Parameter? param = get_as_nullable(index, typeof(Parameter));
|
||||
|
|
@ -333,7 +210,7 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
|
||||
LiteralParameter? literalp = param as LiteralParameter;
|
||||
if (literalp != null && literalp.get_size() <= MAX_STRING_LITERAL_LENGTH)
|
||||
return literalp.to_string_parameter();
|
||||
return literalp.coerce_to_string_parameter();
|
||||
|
||||
throw new ImapError.TYPE_ERROR("Parameter %d not of type string or literal (is %s)", index,
|
||||
param.get_type().name());
|
||||
|
|
@ -346,66 +223,120 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
public StringParameter get_as_empty_string(int index) throws ImapError {
|
||||
StringParameter? stringp = get_as_nullable_string(index);
|
||||
|
||||
return stringp ?? new StringParameter("");
|
||||
return stringp ?? StringParameter.get_best_for("");
|
||||
}
|
||||
|
||||
//
|
||||
// List retrieval
|
||||
//
|
||||
|
||||
/**
|
||||
* Returns the StringParameter at the index only if the Parameter is a StringParameter or a
|
||||
* LiteralParameter with a length less than or equal to MAX_STRING_LITERAL_LENGTH. Returns null
|
||||
* if either is not true.
|
||||
* Returns a {@link ListParameter} at index.
|
||||
*
|
||||
* @see get_as
|
||||
*/
|
||||
public StringParameter? get_if_string(int index) {
|
||||
Parameter? param = get(index);
|
||||
if (param == null)
|
||||
return null;
|
||||
|
||||
StringParameter? stringp = param as StringParameter;
|
||||
if (stringp != null)
|
||||
return stringp;
|
||||
|
||||
LiteralParameter? literalp = param as LiteralParameter;
|
||||
if (literalp != null && literalp.get_size() <= MAX_STRING_LITERAL_LENGTH)
|
||||
return literalp.to_string_parameter();
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public ListParameter get_as_list(int index) throws ImapError {
|
||||
return (ListParameter) get_as(index, typeof(ListParameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link ListParameter} at index, null if NIL.
|
||||
*
|
||||
* @see get_as_nullable
|
||||
*/
|
||||
public ListParameter? get_as_nullable_list(int index) throws ImapError {
|
||||
return (ListParameter?) get_as_nullable(index, typeof(ListParameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [@link ListParameter} at index, an empty list if NIL.
|
||||
*/
|
||||
public ListParameter get_as_empty_list(int index) throws ImapError {
|
||||
ListParameter? param = get_as_nullable_list(index);
|
||||
|
||||
return param ?? new ListParameter(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link ListParameter} at index, null if not a list.
|
||||
*
|
||||
* @see get_if
|
||||
*/
|
||||
public ListParameter? get_if_list(int index) {
|
||||
return (ListParameter?) get_if(index, typeof(ListParameter));
|
||||
}
|
||||
|
||||
//
|
||||
// Literal retrieval
|
||||
//
|
||||
|
||||
/**
|
||||
* Returns a {@link LiteralParameter} at index.
|
||||
*
|
||||
* @see get_as
|
||||
*/
|
||||
public LiteralParameter get_as_literal(int index) throws ImapError {
|
||||
return (LiteralParameter) get_as(index, typeof(LiteralParameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link LiteralParameter} at index, null if NIL.
|
||||
*
|
||||
* @see get_as_nullable
|
||||
*/
|
||||
public LiteralParameter? get_as_nullable_literal(int index) throws ImapError {
|
||||
return (LiteralParameter?) get_as_nullable(index, typeof(LiteralParameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link LiteralParameter} at index, null if not a list.
|
||||
*
|
||||
* @see get_if
|
||||
*/
|
||||
public LiteralParameter? get_if_literal(int index) {
|
||||
return (LiteralParameter?) get_if(index, typeof(LiteralParameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [@link LiteralParameter} at index, an empty list if NIL.
|
||||
*/
|
||||
public LiteralParameter get_as_empty_literal(int index) throws ImapError {
|
||||
LiteralParameter? param = get_as_nullable_literal(index);
|
||||
|
||||
return param ?? new LiteralParameter(Geary.Memory.EmptyBuffer.instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Memory.AbstractBuffer} 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 {
|
||||
LiteralParameter? literalp = get_if_literal(index);
|
||||
if (literalp != null)
|
||||
return literalp.get_buffer();
|
||||
|
||||
StringParameter? stringp = get_if_string(index);
|
||||
if (stringp != null)
|
||||
return new Memory.StringBuffer(stringp.value);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Memory.AbstractBuffer} 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 {
|
||||
return get_as_nullable_buffer(index) ?? Memory.EmptyBuffer.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a read-only List of {@link Parameter}s.
|
||||
*/
|
||||
public Gee.List<Parameter> get_all() {
|
||||
return list.read_only_view;
|
||||
}
|
||||
|
|
@ -425,10 +356,12 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
}
|
||||
|
||||
/**
|
||||
* Moves all child parameters from the supplied list into this list. The supplied list will be
|
||||
* "stripped" of children.
|
||||
* Moves all child parameters from the supplied list into this list.
|
||||
*
|
||||
* The supplied list will be "stripped" of its children. This ListParameter is cleared prior
|
||||
* to adopting the new children.
|
||||
*/
|
||||
public void move_children(ListParameter src) {
|
||||
public void adopt_children(ListParameter src) {
|
||||
list.clear();
|
||||
|
||||
foreach (Parameter param in src.list) {
|
||||
|
|
@ -455,6 +388,9 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
return builder.str;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override string to_string() {
|
||||
return "(%s)".printf(stringize_list());
|
||||
}
|
||||
|
|
@ -468,6 +404,9 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_ascii('(');
|
||||
yield serialize_list(ser);
|
||||
|
|
@ -475,24 +414,3 @@ public class Geary.Imap.ListParameter : Geary.Imap.Parameter {
|
|||
}
|
||||
}
|
||||
|
||||
public class Geary.Imap.RootParameters : Geary.Imap.ListParameter {
|
||||
public RootParameters(Parameter? initial = null) {
|
||||
base (null, initial);
|
||||
}
|
||||
|
||||
public RootParameters.migrate(RootParameters root) {
|
||||
base (null);
|
||||
|
||||
move_children(root);
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return stringize_list();
|
||||
}
|
||||
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
yield serialize_list(ser);
|
||||
ser.push_eol();
|
||||
}
|
||||
}
|
||||
|
||||
67
src/engine/imap/parameter/imap-literal-parameter.vala
Normal file
67
src/engine/imap/parameter/imap-literal-parameter.vala
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A representation of an IMAP literal parameter.
|
||||
*
|
||||
* Because a literal parameter can hold 8-bit data, this is not a descendent of
|
||||
* {@link StringParameter}, although some times literal data is used to store 8-bit text (for
|
||||
* example, UTF-8).
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-4.3]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.LiteralParameter : Geary.Imap.Parameter {
|
||||
private Geary.Memory.AbstractBuffer buffer;
|
||||
|
||||
public LiteralParameter(Geary.Memory.AbstractBuffer buffer) {
|
||||
this.buffer = buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of bytes in the literal parameter's buffer.
|
||||
*/
|
||||
public size_t get_size() {
|
||||
return buffer.get_size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the literal paremeter's buffer.
|
||||
*/
|
||||
public Geary.Memory.AbstractBuffer get_buffer() {
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the {@link LiteralParameter} as though it had been a {@link StringParameter} on the
|
||||
* wire.
|
||||
*
|
||||
* Note that this does not deal with quoting issues or NIL (which should never be
|
||||
* literalized to begin with). It merely converts the literal data to a UTF-8 string and
|
||||
* returns it as a StringParameter. Hence, the data is being coerced and may be unsuitable
|
||||
* for transmitting on the wire.
|
||||
*/
|
||||
public StringParameter coerce_to_string_parameter() {
|
||||
return new UnquotedStringParameter(buffer.to_valid_utf8());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override string to_string() {
|
||||
return "{literal/%lub}".printf(get_size());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override async void serialize(Serializer ser) 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());
|
||||
}
|
||||
}
|
||||
|
||||
61
src/engine/imap/parameter/imap-nil-parameter.vala
Normal file
61
src/engine/imap/parameter/imap-nil-parameter.vala
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The representation of IMAP's NIL value.
|
||||
*
|
||||
* Note that NIL 'represents the non-existence of a particular data item that is represented as a
|
||||
* string or parenthesized list, as distinct from the empty string "" or the empty parenthesized
|
||||
* list () ... NIL is never used for any data item which takes the form of an atom."
|
||||
*
|
||||
* Since there's only one form of a NilParameter, it should be retrieved via the {@link instance}
|
||||
* property.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-4.5]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.NilParameter : Geary.Imap.Parameter {
|
||||
public const string VALUE = "NIL";
|
||||
|
||||
private static NilParameter? _instance = null;
|
||||
public static NilParameter instance {
|
||||
get {
|
||||
if (_instance == null)
|
||||
_instance = new NilParameter();
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private NilParameter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* See note at {@link NilParameter} for comparison rules of "NIL".
|
||||
*
|
||||
* In particular, this should not be used when expecting an atom. A mailbox name of NIL
|
||||
* means that the mailbox is actually named NIL and does not represent an empty string or empty
|
||||
* list.
|
||||
*/
|
||||
public static bool is_nil(StringParameter stringp) {
|
||||
return String.ascii_equali(VALUE, stringp.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_nil();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override string to_string() {
|
||||
return VALUE;
|
||||
}
|
||||
}
|
||||
|
||||
27
src/engine/imap/parameter/imap-parameter.vala
Normal file
27
src/engine/imap/parameter/imap-parameter.vala
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The basic abstraction of a single IMAP parameter that may be serialized and deserialized to and
|
||||
* from the network.
|
||||
*
|
||||
* @see Serializer
|
||||
* @see Deserializer
|
||||
*/
|
||||
|
||||
public abstract class Geary.Imap.Parameter : BaseObject {
|
||||
/**
|
||||
* Invoked when the {@link Parameter} is to be serialized out to the network.
|
||||
*/
|
||||
public abstract async void serialize(Serializer ser) throws Error;
|
||||
|
||||
/**
|
||||
* Returns a representation of the {@link Parameter} suitable for logging and debugging,
|
||||
* but should not be relied upon for wire or persistent representation.
|
||||
*/
|
||||
public abstract string to_string();
|
||||
}
|
||||
|
||||
37
src/engine/imap/parameter/imap-quoted-string-parameter.vala
Normal file
37
src/engine/imap/parameter/imap-quoted-string-parameter.vala
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/* 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 an IMAP quoted string.
|
||||
*
|
||||
* This class does not check if quoting is required. Use {@link DataFormat.is_quoting_required}
|
||||
* or {@link StringParameter.get_best_for}.
|
||||
*
|
||||
* {@link Deserializer} will never generate this {@link Parameter}.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-4.3]].
|
||||
*/
|
||||
|
||||
public class Geary.Imap.QuotedStringParameter : Geary.Imap.StringParameter {
|
||||
public QuotedStringParameter(string value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override string to_string() {
|
||||
return "\"%s\"".printf(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_quoted_string(value);
|
||||
}
|
||||
}
|
||||
|
||||
74
src/engine/imap/parameter/imap-root-parameters.vala
Normal file
74
src/engine/imap/parameter/imap-root-parameters.vala
Normal 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The base respresentation of an complete IMAP message.
|
||||
*
|
||||
* By definition, a top-level {@link ListParameter}. A RootParameters object should never be
|
||||
* added to another list.
|
||||
*
|
||||
* @see ServerResponse
|
||||
* @see Command
|
||||
*/
|
||||
|
||||
public class Geary.Imap.RootParameters : Geary.Imap.ListParameter {
|
||||
public RootParameters(Parameter? initial = null) {
|
||||
base (null, initial);
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves all contained {@link Parameter} objects inside the supplied RootParameters into a
|
||||
* new RootParameters.
|
||||
*
|
||||
* The supplied root object is stripped clean by this call.
|
||||
*/
|
||||
public RootParameters.migrate(RootParameters root) {
|
||||
base (null);
|
||||
|
||||
adopt_children(root);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if the first parameter is not a StringParameter that resembles a Tag.
|
||||
*/
|
||||
public Tag? get_tag() {
|
||||
StringParameter? strparam = get_if_string(0);
|
||||
if (strparam == null)
|
||||
return null;
|
||||
|
||||
if (!Tag.is_tag(strparam))
|
||||
return null;
|
||||
|
||||
return new Tag.from_parameter(strparam);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the first parameter is a StringParameter that resembles a Tag.
|
||||
*/
|
||||
public bool has_tag() {
|
||||
StringParameter? strparam = get_if_string(0);
|
||||
if (strparam == null)
|
||||
return false;
|
||||
|
||||
return (strparam != null) ? Tag.is_tag(strparam) : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override string to_string() {
|
||||
return stringize_list();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
yield serialize_list(ser);
|
||||
ser.push_eol();
|
||||
}
|
||||
}
|
||||
|
||||
136
src/engine/imap/parameter/imap-string-parameter.vala
Normal file
136
src/engine/imap/parameter/imap-string-parameter.vala
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A base abstract representation of string (text) data in IMAP.
|
||||
*
|
||||
* Although they may be transmitted in various ways, most parameters in IMAP are strings or text
|
||||
* format, possibly with some quoting rules applied. This class handles most issues with these
|
||||
* types of {@link Parameter}s.
|
||||
*
|
||||
* Although the IMAP specification doesn't list an atom as a "string", it is here because of the
|
||||
* common functionality that is needed for comparison and other operations.
|
||||
*
|
||||
* Note that {@link NilParameter} is ''not'' a StringParameter, to avoid type confusion.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-4.3]]
|
||||
*/
|
||||
|
||||
public abstract class Geary.Imap.StringParameter : Geary.Imap.Parameter {
|
||||
/**
|
||||
* The unquoted, decoded string.
|
||||
*/
|
||||
public string value { get; private set; }
|
||||
|
||||
/**
|
||||
* Returns {@link} value or null if value is empty (zero-length).
|
||||
*/
|
||||
public string? nullable_value {
|
||||
get {
|
||||
return String.is_empty(value) ? null : value;
|
||||
}
|
||||
}
|
||||
|
||||
protected StringParameter(string value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link StringParameter} appropriate for the contents of value.
|
||||
*
|
||||
* Will not return an {@link AtomParameter}, but rather an {@link UnquotedStringParameter} if
|
||||
* suitable. Will not return a {@link NilParameter} for empty strings, but rather a
|
||||
* {@link QuotedStringParameter}.
|
||||
*
|
||||
* Because of these restrictions, should only be used when the context or syntax of the
|
||||
* Parameter is unknown or uncertain.
|
||||
*
|
||||
* @return null if the string must be represented with a {@link LiteralParameter}.
|
||||
*/
|
||||
public static StringParameter? get_best_for(string value) {
|
||||
switch (DataFormat.is_quoting_required(value)) {
|
||||
case DataFormat.Quoting.REQUIRED:
|
||||
return new QuotedStringParameter(value);
|
||||
|
||||
case DataFormat.Quoting.OPTIONAL:
|
||||
return new UnquotedStringParameter(value);
|
||||
|
||||
case DataFormat.Quoting.UNALLOWED:
|
||||
return null;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used by subclasses to properly serialize the string value according to quoting rules.
|
||||
*
|
||||
* NOTE: Literal data is not currently supported.
|
||||
*/
|
||||
protected void serialize_string(Serializer ser) throws Error {
|
||||
switch (DataFormat.is_quoting_required(value)) {
|
||||
case DataFormat.Quoting.REQUIRED:
|
||||
ser.push_quoted_string(value);
|
||||
break;
|
||||
|
||||
case DataFormat.Quoting.OPTIONAL:
|
||||
ser.push_unquoted_string(value);
|
||||
break;
|
||||
|
||||
case DataFormat.Quoting.UNALLOWED:
|
||||
error("Unable to serialize literal data");
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Case-sensitive comparison.
|
||||
*/
|
||||
public bool equals_cs(string value) {
|
||||
return this.value == value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Case-insensitive comparison.
|
||||
*/
|
||||
public bool equals_ci(string value) {
|
||||
return this.value.down() == value.down();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link value} to an int, clamped between clamp_min and clamp_max.
|
||||
*
|
||||
* TODO: This does not check that the value is a properly-formed integer. This should be
|
||||
*. added later.
|
||||
*/
|
||||
public int as_int(int clamp_min = int.MIN, int clamp_max = int.MAX) throws ImapError {
|
||||
return int.parse(value).clamp(clamp_min, clamp_max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link value} to a long integer, clamped between clamp_min and clamp_max.
|
||||
*
|
||||
* TODO: This does not check that the value is a properly-formed long integer. This should be
|
||||
*. added later.
|
||||
*/
|
||||
public long as_long(int clamp_min = int.MIN, int clamp_max = int.MAX) throws ImapError {
|
||||
return long.parse(value).clamp(clamp_min, clamp_max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link value} to a 64-bit integer, clamped between clamp_min and clamp_max.
|
||||
*
|
||||
* TODO: This does not check that the value is a properly-formed 64-bit integer. This should be
|
||||
*. added later.
|
||||
*/
|
||||
public int64 as_int64(int64 clamp_min = int64.MIN, int64 clamp_max = int64.MAX) throws ImapError {
|
||||
return int64.parse(value).clamp(clamp_min, clamp_max);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
/* 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 an IMAP string that is not quoted.
|
||||
*
|
||||
* This class does not check if quoting is required. Use {@link DataFormat.is_quoting_required}
|
||||
* or {@link StringParameter.get_best_for}.
|
||||
*
|
||||
* The difference between this class and {@link AtomParameter} is that this can be used in any
|
||||
* circumstance where a string can (or is) represented without quotes or literal data, whereas an
|
||||
* atom has strict definitions about where it's found.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-4.1]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.UnquotedStringParameter : Geary.Imap.StringParameter {
|
||||
public UnquotedStringParameter(string value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override async void serialize(Serializer ser) throws Error {
|
||||
ser.push_unquoted_string(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
public override string to_string() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -7,10 +7,12 @@
|
|||
public class Geary.Imap.Capabilities : Geary.GenericCapabilities {
|
||||
public const string IDLE = "IDLE";
|
||||
public const string STARTTLS = "STARTTLS";
|
||||
public const string XLIST = "XLIST";
|
||||
public const string COMPRESS = "COMPRESS";
|
||||
public const string DEFLATE_SETTING = "DEFLATE";
|
||||
public const string UIDPLUS = "UIDPLUS";
|
||||
|
||||
public const string NAME_SEPARATOR = " ";
|
||||
public const string NAME_SEPARATOR = "=";
|
||||
public const string? VALUE_SEPARATOR = null;
|
||||
|
||||
public int revision { get; private set; }
|
||||
|
|
@ -4,13 +4,41 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A server response indicating that the server is ready to accept more data for the current
|
||||
* command.
|
||||
*
|
||||
* The only requirement for a ContinuationResponse is that its {@link Tag} must be a
|
||||
* {@link Tag.CONTINUATION_VALUE} ("+"). All other information in the response is optional.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.5]] for more information.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.ContinuationResponse : ServerResponse {
|
||||
public ContinuationResponse(Tag tag) {
|
||||
base (tag);
|
||||
private ContinuationResponse() {
|
||||
base (Tag.get_continuation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link RootParameters} into a {@link ContinuationResponse}.
|
||||
*
|
||||
* The supplied root is "stripped" of its children. This may happen even if an exception is
|
||||
* thrown. It's recommended to use {@link is_continuation_response} prior to this call.
|
||||
*/
|
||||
public ContinuationResponse.migrate(RootParameters root) throws ImapError {
|
||||
base.migrate(root);
|
||||
|
||||
if (!tag.is_continuation())
|
||||
throw new ImapError.INVALID("Tag %s is not a continuation", tag.to_string());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the {@link RootParameters}'s {@link Tag} is a continuation character ("+").
|
||||
*/
|
||||
public static bool is_continuation_response(RootParameters root) {
|
||||
Tag? tag = root.get_tag();
|
||||
|
||||
return tag != null ? tag.is_continuation() : false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
/* 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 convenience class to determine if a {@link ServerResponse} contains the result for a
|
||||
* particular {@link FetchBodyDataType}.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.FetchBodyDataIdentifier : BaseObject, Gee.Hashable<FetchBodyDataIdentifier> {
|
||||
private string original;
|
||||
private string munged;
|
||||
|
||||
internal FetchBodyDataIdentifier(FetchBodyDataType body_data_type) {
|
||||
original = body_data_type.serialize_request();
|
||||
munged = munge(body_data_type.serialize_response());
|
||||
}
|
||||
|
||||
internal FetchBodyDataIdentifier.from_parameter(StringParameter stringp) {
|
||||
original = stringp.value;
|
||||
munged = munge(original);
|
||||
}
|
||||
|
||||
// prepare a version of the string modified to properly compare a version in a Command to the
|
||||
// matching result in a ServerResponse.
|
||||
//
|
||||
// Current changes:
|
||||
// * case-insensitive
|
||||
// * leading/trailing whitespace stripped
|
||||
// * BODY.peek[...] is returned as simply BODY[...]
|
||||
// * The span in the returned response is merely the offset ("1.15" becomes "1") because the
|
||||
// associated literal specifies its length
|
||||
// * Remove quoting (some servers return field names quoted, some don't, Geary never uses them
|
||||
// when requesting)
|
||||
//
|
||||
// Some of these changes are reflected by using serialize_response() instead of
|
||||
// serialize_request() in the constructore.
|
||||
private static string munge(string str) {
|
||||
return str.down().replace("\"", "").strip();
|
||||
}
|
||||
|
||||
public bool equal_to(FetchBodyDataIdentifier other) {
|
||||
return munged == other.munged;
|
||||
}
|
||||
|
||||
public uint hash() {
|
||||
return str_hash(munged);
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return "%s/%s".printf(original, munged);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +43,7 @@ public abstract class Geary.Imap.FetchDataDecoder : BaseObject {
|
|||
// reasonably-length literals into StringParameters), do so here manually
|
||||
try {
|
||||
if (literalp.get_size() <= ListParameter.MAX_STRING_LITERAL_LENGTH)
|
||||
return decode_string(literalp.to_string_parameter());
|
||||
return decode_string(literalp.coerce_to_string_parameter());
|
||||
} catch (ImapError imap_err) {
|
||||
// if decode_string() throws a TYPE_ERROR, retry as a LiteralParameter, otherwise
|
||||
// relay the exception to the caller
|
||||
138
src/engine/imap/response/imap-fetched-data.vala
Normal file
138
src/engine/imap/response/imap-fetched-data.vala
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/* Copyright 2011-2012 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The deserialized representation of a FETCH response.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.4.2]]
|
||||
*
|
||||
* @see FetchCommand
|
||||
* @see StoreCommand
|
||||
*/
|
||||
public class Geary.Imap.FetchedData : Object {
|
||||
/**
|
||||
* The positional address of the email in the mailbox.
|
||||
*/
|
||||
public SequenceNumber seq_num { get; private set; }
|
||||
|
||||
/**
|
||||
* A Map of {@link FetchDataType}s to their {@link Imap.MessageData} for this email.
|
||||
*
|
||||
* MessageData should be cast to their appropriate class depending on their FetchDataType.
|
||||
*/
|
||||
public Gee.Map<FetchDataType, MessageData> data_map { get; private set;
|
||||
default = new Gee.HashMap<FetchDataType, MessageData>(); }
|
||||
|
||||
/**
|
||||
* List of {@link FetchBodyDataType} responses.
|
||||
*
|
||||
* Unfortunately, FetchBodyDataType currently doesn't offer a deserialize method, which is
|
||||
* necessary to propertly index or map the buffers with what they represently uniquely. For
|
||||
* 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 FetchedData(SequenceNumber seq_num) {
|
||||
this.seq_num = seq_num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes {@link ServerData} into a FetchedData representation.
|
||||
*
|
||||
* The ServerData must be the response to a FETCH or STORE command.
|
||||
*
|
||||
* @see FetchCommand
|
||||
* @see StoreCommand
|
||||
* @see ServerData.get_fetch
|
||||
*/
|
||||
public static FetchedData decode(ServerData server_data) throws ImapError {
|
||||
if (!server_data.get_as_string(2).equals_ci(FetchCommand.NAME))
|
||||
throw new ImapError.PARSE_ERROR("Not FETCH data: %s", server_data.to_string());
|
||||
|
||||
FetchedData fetched_data = new FetchedData(new SequenceNumber(server_data.get_as_string(1).as_int()));
|
||||
|
||||
// walk the list for each returned fetch data item, which is paired by its data item name
|
||||
// and the structured data itself
|
||||
ListParameter list = server_data.get_as_list(3);
|
||||
for (int ctr = 0; ctr < list.get_count(); ctr += 2) {
|
||||
StringParameter data_item_param = list.get_as_string(ctr);
|
||||
|
||||
// watch for truncated lists, which indicate an empty return value
|
||||
bool has_value = (ctr < (list.get_count() - 1));
|
||||
|
||||
if (FetchBodyDataType.is_fetch_body(data_item_param)) {
|
||||
// "fake" the identifier by merely dropping in the StringParameter wholesale ...
|
||||
// this works because FetchBodyDataIdentifier does case-insensitive comparisons ...
|
||||
// other munging may be required if this isn't sufficient
|
||||
FetchBodyDataIdentifier identifer = new FetchBodyDataIdentifier.from_parameter(data_item_param);
|
||||
|
||||
if (has_value)
|
||||
fetched_data.body_data_map.set(identifer, list.get_as_empty_buffer(ctr + 1));
|
||||
else
|
||||
fetched_data.body_data_map.set(identifer, Memory.EmptyBuffer.instance);
|
||||
} else {
|
||||
FetchDataType data_item = FetchDataType.decode(data_item_param.value);
|
||||
FetchDataDecoder? decoder = data_item.get_decoder();
|
||||
if (decoder == null) {
|
||||
debug("Unable to decode fetch response for \"%s\": No decoder available",
|
||||
data_item.to_string());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// watch for empty return values
|
||||
if (has_value)
|
||||
fetched_data.data_map.set(data_item, decoder.decode(list.get_required(ctr + 1)));
|
||||
else
|
||||
fetched_data.data_map.set(data_item, decoder.decode(NilParameter.instance));
|
||||
}
|
||||
}
|
||||
|
||||
return fetched_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the merge of this {@link FetchedData} and the supplied one.
|
||||
*
|
||||
* The results are undefined if both FetchData objects contain the same {@link FetchDataType}
|
||||
* or {@link FetchBodyDataType}s.
|
||||
*
|
||||
* See warnings at {@link body_data_map} for dealing with multiple FetchBodyDataTypes.
|
||||
*
|
||||
* @return null if the FetchedData do not have the same {@link seq_num}.
|
||||
*/
|
||||
public FetchedData? combine(FetchedData other) {
|
||||
if (!seq_num.equal_to(other.seq_num))
|
||||
return null;
|
||||
|
||||
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,
|
||||
body_data_map);
|
||||
Collection.map_set_all<FetchBodyDataIdentifier, Memory.AbstractBuffer>(combined.body_data_map,
|
||||
other.body_data_map);
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append_printf("[%s] ", seq_num.to_string());
|
||||
|
||||
foreach (FetchDataType data_type in data_map.keys)
|
||||
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());
|
||||
|
||||
return builder.str;
|
||||
}
|
||||
}
|
||||
|
||||
160
src/engine/imap/response/imap-mailbox-attribute.vala
Normal file
160
src/engine/imap/response/imap-mailbox-attribute.vala
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
/* 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 IMAP mailbox attribute (flag).
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.2.2]]
|
||||
*
|
||||
* @see ListCommand
|
||||
* @see MailboxInformation
|
||||
*/
|
||||
|
||||
public class Geary.Imap.MailboxAttribute : Geary.Imap.Flag {
|
||||
private static MailboxAttribute? _no_inferiors = null;
|
||||
public static MailboxAttribute NO_INFERIORS { get {
|
||||
if (_no_inferiors == null)
|
||||
_no_inferiors = new MailboxAttribute("\\noinferiors");
|
||||
|
||||
return _no_inferiors;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _no_select = null;
|
||||
public static MailboxAttribute NO_SELECT { get {
|
||||
if (_no_select == null)
|
||||
_no_select = new MailboxAttribute("\\noselect");
|
||||
|
||||
return _no_select;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _marked = null;
|
||||
public static MailboxAttribute MARKED { get {
|
||||
if (_marked == null)
|
||||
_marked = new MailboxAttribute("\\marked");
|
||||
|
||||
return _marked;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _unmarked = null;
|
||||
public static MailboxAttribute UNMARKED { get {
|
||||
if (_unmarked == null)
|
||||
_unmarked = new MailboxAttribute("\\unmarked");
|
||||
|
||||
return _unmarked;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _has_no_children = null;
|
||||
public static MailboxAttribute HAS_NO_CHILDREN { get {
|
||||
if (_has_no_children == null)
|
||||
_has_no_children = new MailboxAttribute("\\hasnochildren");
|
||||
|
||||
return _has_no_children;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _has_children = null;
|
||||
public static MailboxAttribute HAS_CHILDREN { get {
|
||||
if (_has_children == null)
|
||||
_has_children = new MailboxAttribute("\\haschildren");
|
||||
|
||||
return _has_children;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _allows_new = null;
|
||||
public static MailboxAttribute ALLOWS_NEW { get {
|
||||
if (_allows_new == null)
|
||||
_allows_new = new MailboxAttribute("\\*");
|
||||
|
||||
return _allows_new;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_inbox = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_INBOX { get {
|
||||
if (_xlist_inbox == null)
|
||||
_xlist_inbox = new MailboxAttribute("\\Inbox");
|
||||
|
||||
return _xlist_inbox;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_all_mail = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_ALL_MAIL { get {
|
||||
if (_xlist_all_mail == null)
|
||||
_xlist_all_mail = new MailboxAttribute("\\AllMail");
|
||||
|
||||
return _xlist_all_mail;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_trash = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_TRASH { get {
|
||||
if (_xlist_trash == null)
|
||||
_xlist_trash = new MailboxAttribute("\\Trash");
|
||||
|
||||
return _xlist_trash;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_drafts = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_DRAFTS { get {
|
||||
if (_xlist_drafts == null)
|
||||
_xlist_drafts = new MailboxAttribute("\\Drafts");
|
||||
|
||||
return _xlist_drafts;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_sent = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_SENT { get {
|
||||
if (_xlist_sent == null)
|
||||
_xlist_sent = new MailboxAttribute("\\Sent");
|
||||
|
||||
return _xlist_sent;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_spam = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_SPAM { get {
|
||||
if (_xlist_spam == null)
|
||||
_xlist_spam = new MailboxAttribute("\\Spam");
|
||||
|
||||
return _xlist_spam;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_starred = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_STARRED { get {
|
||||
if (_xlist_starred == null)
|
||||
_xlist_starred = new MailboxAttribute("\\Starred");
|
||||
|
||||
return _xlist_starred;
|
||||
} }
|
||||
|
||||
private static MailboxAttribute? _xlist_important = null;
|
||||
public static MailboxAttribute SPECIAL_FOLDER_IMPORTANT { get {
|
||||
if (_xlist_important == null)
|
||||
_xlist_important = new MailboxAttribute("\\Important");
|
||||
|
||||
return _xlist_important;
|
||||
} }
|
||||
|
||||
public MailboxAttribute(string value) {
|
||||
base (value);
|
||||
}
|
||||
|
||||
// Call these at init time to prevent thread issues
|
||||
internal static void init() {
|
||||
MailboxAttribute to_init = NO_INFERIORS;
|
||||
to_init = NO_SELECT;
|
||||
to_init = MARKED;
|
||||
to_init = UNMARKED;
|
||||
to_init = HAS_NO_CHILDREN;
|
||||
to_init = HAS_CHILDREN;
|
||||
to_init = ALLOWS_NEW;
|
||||
to_init = SPECIAL_FOLDER_ALL_MAIL;
|
||||
to_init = SPECIAL_FOLDER_DRAFTS;
|
||||
to_init = SPECIAL_FOLDER_IMPORTANT;
|
||||
to_init = SPECIAL_FOLDER_INBOX;
|
||||
to_init = SPECIAL_FOLDER_SENT;
|
||||
to_init = SPECIAL_FOLDER_SPAM;
|
||||
to_init = SPECIAL_FOLDER_STARRED;
|
||||
to_init = SPECIAL_FOLDER_TRASH;
|
||||
}
|
||||
}
|
||||
|
||||
78
src/engine/imap/response/imap-mailbox-attributes.vala
Normal file
78
src/engine/imap/response/imap-mailbox-attributes.vala
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A collection of {@link MailboxAttribute}s.
|
||||
*
|
||||
* @see ListCommand
|
||||
* @see MailboxInformation
|
||||
*/
|
||||
|
||||
public class Geary.Imap.MailboxAttributes : Geary.Imap.Flags {
|
||||
public MailboxAttributes(Gee.Collection<MailboxAttribute> attrs) {
|
||||
base (attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link MailboxAttributes} from a {@link ListParameter} of attribute strings.
|
||||
*/
|
||||
public static MailboxAttributes from_list(ListParameter listp) throws ImapError {
|
||||
Gee.Collection<MailboxAttribute> list = new Gee.ArrayList<MailboxAttribute>();
|
||||
for (int ctr = 0; ctr < listp.get_count(); ctr++)
|
||||
list.add(new MailboxAttribute(listp.get_as_string(ctr).value));
|
||||
|
||||
return new MailboxAttributes(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create {@link MailboxAttributes} from a flat string of space-delimited attributes.
|
||||
*/
|
||||
public static MailboxAttributes deserialize(string? str) {
|
||||
if (String.is_empty(str))
|
||||
return new MailboxAttributes(new Gee.ArrayList<MailboxAttribute>());
|
||||
|
||||
string[] tokens = str.split(" ");
|
||||
|
||||
Gee.Collection<MailboxAttribute> attrs = new Gee.ArrayList<MailboxAttribute>();
|
||||
foreach (string token in tokens)
|
||||
attrs.add(new MailboxAttribute(token));
|
||||
|
||||
return new MailboxAttributes(attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the {@link MailboxAttributes} looking for an XLIST-style
|
||||
* {@link Geary.SpecialFolderType}.
|
||||
*/
|
||||
public Geary.SpecialFolderType get_special_folder_type() {
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_INBOX))
|
||||
return Geary.SpecialFolderType.INBOX;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_ALL_MAIL))
|
||||
return Geary.SpecialFolderType.ALL_MAIL;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_TRASH))
|
||||
return Geary.SpecialFolderType.TRASH;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_DRAFTS))
|
||||
return Geary.SpecialFolderType.DRAFTS;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_SENT))
|
||||
return Geary.SpecialFolderType.SENT;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_SPAM))
|
||||
return Geary.SpecialFolderType.SPAM;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_STARRED))
|
||||
return Geary.SpecialFolderType.FLAGGED;
|
||||
|
||||
if (contains(MailboxAttribute.SPECIAL_FOLDER_IMPORTANT))
|
||||
return Geary.SpecialFolderType.IMPORTANT;
|
||||
|
||||
return Geary.SpecialFolderType.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
83
src/engine/imap/response/imap-mailbox-information.vala
Normal file
83
src/engine/imap/response/imap-mailbox-information.vala
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
/* Copyright 2011-2012 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The decoded response to a LIST command.
|
||||
*
|
||||
* This is also the response to an XLIST command.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.2.2]]
|
||||
*
|
||||
* @see ListCommand
|
||||
*/
|
||||
|
||||
public class Geary.Imap.MailboxInformation : Object {
|
||||
/**
|
||||
* Name of the mailbox.
|
||||
*/
|
||||
public MailboxSpecifier mailbox { get; private set; }
|
||||
|
||||
/**
|
||||
* The (optional) delimiter specified by the server.
|
||||
*/
|
||||
public string? delim { get; private set; }
|
||||
|
||||
/**
|
||||
* Folder attributes returned by the server.
|
||||
*/
|
||||
public MailboxAttributes attrs { get; private set; }
|
||||
|
||||
public MailboxInformation(MailboxSpecifier mailbox, string? delim, MailboxAttributes attrs) {
|
||||
this.mailbox = mailbox;
|
||||
this.delim = delim;
|
||||
this.attrs = attrs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decodes {@link ServerData} into a MailboxInformation representation.
|
||||
*
|
||||
* The ServerData must be the response to a LIST or XLIST command.
|
||||
*
|
||||
* @see ListCommand
|
||||
* @see ServerData.get_list
|
||||
*/
|
||||
public static MailboxInformation decode(ServerData server_data) throws ImapError {
|
||||
StringParameter cmd = server_data.get_as_string(1);
|
||||
if (!cmd.equals_ci(ListCommand.NAME) && !cmd.equals_ci(ListCommand.XLIST_NAME))
|
||||
throw new ImapError.PARSE_ERROR("Not LIST or XLIST data: %s", server_data.to_string());
|
||||
|
||||
// Build list of attributes
|
||||
ListParameter attrs = server_data.get_as_list(2);
|
||||
Gee.Collection<MailboxAttribute> attrlist = new Gee.ArrayList<MailboxAttribute>();
|
||||
foreach (Parameter attr in attrs.get_all()) {
|
||||
StringParameter? stringp = attr as StringParameter;
|
||||
if (stringp == null) {
|
||||
debug("Bad list attribute \"%s\": Attribute not a string value",
|
||||
server_data.to_string());
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
attrlist.add(new MailboxAttribute(stringp.value));
|
||||
}
|
||||
|
||||
// decode everything
|
||||
MailboxAttributes attributes = new MailboxAttributes(attrlist);
|
||||
StringParameter? delim = server_data.get_as_nullable_string(3);
|
||||
MailboxParameter mailbox = new MailboxParameter.from_string_parameter(
|
||||
server_data.get_as_string(4));
|
||||
|
||||
// Set \Inbox to standard path
|
||||
if (Geary.Imap.MailboxAttribute.SPECIAL_FOLDER_INBOX in attributes) {
|
||||
return new MailboxInformation(MailboxSpecifier.inbox,
|
||||
(delim != null) ? delim.nullable_value : null, attributes);
|
||||
} else {
|
||||
return new MailboxInformation(new MailboxSpecifier.from_parameter(mailbox),
|
||||
(delim != null) ? delim.nullable_value : null, attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,8 +4,18 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An optional response code accompanying a {@link ServerResponse}.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.1]] for more information.
|
||||
*/
|
||||
|
||||
public enum Geary.Imap.ResponseCodeType {
|
||||
ALERT,
|
||||
AUTHENTICATIONFAILED,
|
||||
AUTHORIZATIONFAILED,
|
||||
BADCHARSET,
|
||||
CAPABILITY,
|
||||
NEWNAME,
|
||||
PARSE,
|
||||
PERMANENT_FLAGS,
|
||||
|
|
@ -15,13 +25,30 @@ public enum Geary.Imap.ResponseCodeType {
|
|||
UIDVALIDITY,
|
||||
UIDNEXT,
|
||||
UNSEEN,
|
||||
MYRIGHTS;
|
||||
MYRIGHTS,
|
||||
UNAVAILABLE,
|
||||
SERVERBUG,
|
||||
CLIENTBUG,
|
||||
ALREADYEXISTS,
|
||||
NONEXISTANT;
|
||||
|
||||
public string to_string() {
|
||||
switch (this) {
|
||||
case ALERT:
|
||||
return "alert";
|
||||
|
||||
case AUTHENTICATIONFAILED:
|
||||
return "authenticationfailed";
|
||||
|
||||
case AUTHORIZATIONFAILED:
|
||||
return "authorizationfailed";
|
||||
|
||||
case BADCHARSET:
|
||||
return "badcharset";
|
||||
|
||||
case CAPABILITY:
|
||||
return "capability";
|
||||
|
||||
case NEWNAME:
|
||||
return "newname";
|
||||
|
||||
|
|
@ -52,6 +79,21 @@ public enum Geary.Imap.ResponseCodeType {
|
|||
case MYRIGHTS:
|
||||
return "myrights";
|
||||
|
||||
case UNAVAILABLE:
|
||||
return "unavailable";
|
||||
|
||||
case SERVERBUG:
|
||||
return "serverbug";
|
||||
|
||||
case CLIENTBUG:
|
||||
return "clientbug";
|
||||
|
||||
case ALREADYEXISTS:
|
||||
return "alreadyexists";
|
||||
|
||||
case NONEXISTANT:
|
||||
return "nonexistant";
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
|
|
@ -62,6 +104,18 @@ public enum Geary.Imap.ResponseCodeType {
|
|||
case "alert":
|
||||
return ALERT;
|
||||
|
||||
case "authenticationfailed":
|
||||
return AUTHENTICATIONFAILED;
|
||||
|
||||
case "authorizationfailed":
|
||||
return AUTHORIZATIONFAILED;
|
||||
|
||||
case "badcharset":
|
||||
return BADCHARSET;
|
||||
|
||||
case "capability":
|
||||
return CAPABILITY;
|
||||
|
||||
case "newname":
|
||||
return NEWNAME;
|
||||
|
||||
|
|
@ -92,6 +146,21 @@ public enum Geary.Imap.ResponseCodeType {
|
|||
case "myrights":
|
||||
return MYRIGHTS;
|
||||
|
||||
case "unavailable":
|
||||
return UNAVAILABLE;
|
||||
|
||||
case "serverbug":
|
||||
return SERVERBUG;
|
||||
|
||||
case "clientbug":
|
||||
return CLIENTBUG;
|
||||
|
||||
case "alreadyexists":
|
||||
return ALREADYEXISTS;
|
||||
|
||||
case "nonexistant":
|
||||
return NONEXISTANT;
|
||||
|
||||
default:
|
||||
throw new ImapError.PARSE_ERROR("Unknown response code \"%s\"", value);
|
||||
}
|
||||
|
|
@ -102,7 +171,7 @@ public enum Geary.Imap.ResponseCodeType {
|
|||
}
|
||||
|
||||
public StringParameter to_parameter() {
|
||||
return new StringParameter(to_string());
|
||||
return new AtomParameter(to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,92 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A response code and additional information that optionally accompanies a {@link StatusResponse}.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.1]] for more information.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.ResponseCode : Geary.Imap.ListParameter {
|
||||
public ResponseCode(ListParameter parent, Parameter? initial = null) {
|
||||
base (parent, initial);
|
||||
}
|
||||
|
||||
public ResponseCodeType get_code_type() throws ImapError {
|
||||
public ResponseCodeType get_response_code_type() throws ImapError {
|
||||
return ResponseCodeType.from_parameter(get_as_string(0));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link ResponseCode} into a UIDNEXT {@link UID}, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not UIDNEXT.
|
||||
*/
|
||||
public UID get_uid_next() throws ImapError {
|
||||
if (get_response_code_type() != ResponseCodeType.UIDNEXT)
|
||||
throw new ImapError.INVALID("Not UIDNEXT: %s", to_string());
|
||||
|
||||
return new UID(get_as_string(1).as_int());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link ResponseCode} into a {@link UIDValidity}, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not UIDVALIDITY.
|
||||
*/
|
||||
public UIDValidity get_uid_validity() throws ImapError {
|
||||
if (get_response_code_type() != ResponseCodeType.UIDVALIDITY)
|
||||
throw new ImapError.INVALID("Not UIDVALIDITY: %s", to_string());
|
||||
|
||||
return new UIDValidity(get_as_string(1).as_int());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link ResponseCode} into an UNSEEN value, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not UNSEEN.
|
||||
*/
|
||||
public int get_unseen() throws ImapError {
|
||||
if (get_response_code_type() != ResponseCodeType.UNSEEN)
|
||||
throw new ImapError.INVALID("Not UNSEEN: %s", to_string());
|
||||
|
||||
return get_as_string(1).as_int(0, int.MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link ResponseCode} into PERMANENTFLAGS {@link MessageFlags}, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not PERMANENTFLAGS.
|
||||
*/
|
||||
public MessageFlags get_permanent_flags() throws ImapError {
|
||||
if (get_response_code_type() != ResponseCodeType.PERMANENT_FLAGS)
|
||||
throw new ImapError.INVALID("Not PERMANENTFLAGS: %s", to_string());
|
||||
|
||||
return MessageFlags.from_list(get_as_list(1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ResponseCode} into {@link Capabilities}, if possible.
|
||||
*
|
||||
* Since Capabilities are revised with various {@link ClientSession} states, this method accepts
|
||||
* a ref to an int that will be incremented after handed to the Capabilities constructor. This
|
||||
* can be used to track the revision of capabilities seen on the connection.
|
||||
*
|
||||
* @throws ImapError.INVALID if Capability was not specified.
|
||||
*/
|
||||
public Capabilities get_capabilities(ref int next_revision) throws ImapError {
|
||||
if (get_response_code_type() != ResponseCodeType.CAPABILITY)
|
||||
throw new ImapError.INVALID("Not CAPABILITY response code: %s", to_string());
|
||||
|
||||
Capabilities capabilities = new Capabilities(next_revision++);
|
||||
for (int ctr = 1; ctr < get_count(); ctr++) {
|
||||
StringParameter? param = get_if_string(ctr);
|
||||
if (param != null)
|
||||
capabilities.add_parameter(param);
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return "[%s]".printf(stringize_list());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A descriptor of what flavor of {@link ServerData} is found in the response.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.2]] for more information.
|
||||
*/
|
||||
|
||||
public enum Geary.Imap.ServerDataType {
|
||||
CAPABILITY,
|
||||
EXISTS,
|
||||
|
|
@ -14,7 +20,8 @@ public enum Geary.Imap.ServerDataType {
|
|||
LSUB,
|
||||
RECENT,
|
||||
SEARCH,
|
||||
STATUS;
|
||||
STATUS,
|
||||
XLIST;
|
||||
|
||||
public string to_string() {
|
||||
switch (this) {
|
||||
|
|
@ -48,6 +55,9 @@ public enum Geary.Imap.ServerDataType {
|
|||
case STATUS:
|
||||
return "status";
|
||||
|
||||
case XLIST:
|
||||
return "xlist";
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
|
|
@ -62,6 +72,7 @@ public enum Geary.Imap.ServerDataType {
|
|||
return EXISTS;
|
||||
|
||||
case "expunge":
|
||||
case "expunged":
|
||||
return EXPUNGE;
|
||||
|
||||
case "fetch":
|
||||
|
|
@ -85,17 +96,89 @@ public enum Geary.Imap.ServerDataType {
|
|||
case "status":
|
||||
return STATUS;
|
||||
|
||||
case "xlist":
|
||||
return XLIST;
|
||||
|
||||
default:
|
||||
throw new ImapError.PARSE_ERROR("\"%s\" is not a valid server data type", value);
|
||||
}
|
||||
}
|
||||
|
||||
public StringParameter to_parameter() {
|
||||
return new StringParameter(to_string());
|
||||
return new AtomParameter(to_string());
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a {@link StringParameter} into a ServerDataType.
|
||||
*
|
||||
* @throws ImapError.PARSE_ERROR if the StringParameter is not recognized as a ServerDataType.
|
||||
*/
|
||||
public static ServerDataType from_parameter(StringParameter param) throws ImapError {
|
||||
return decode(param.value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Examines the {@link RootParameters} looking for a ServerDataType.
|
||||
*
|
||||
* IMAP server responses don't offer a regular format for server data declations. This method
|
||||
* parses for the common patterns and returns the ServerDataType it detects.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.2]] for more information.
|
||||
*/
|
||||
public static ServerDataType from_response(RootParameters root) throws ImapError {
|
||||
StringParameter? firstparam = root.get_if_string(1);
|
||||
if (firstparam != null) {
|
||||
switch (firstparam.value.down()) {
|
||||
case "capability":
|
||||
return CAPABILITY;
|
||||
|
||||
case "flags":
|
||||
return FLAGS;
|
||||
|
||||
case "list":
|
||||
return LIST;
|
||||
|
||||
case "lsub":
|
||||
return LSUB;
|
||||
|
||||
case "search":
|
||||
return SEARCH;
|
||||
|
||||
case "status":
|
||||
return STATUS;
|
||||
|
||||
case "xlist":
|
||||
return XLIST;
|
||||
|
||||
default:
|
||||
// fall-through
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
StringParameter? secondparam = root.get_if_string(2);
|
||||
if (secondparam != null) {
|
||||
switch (secondparam.value.down()) {
|
||||
case "exists":
|
||||
return EXISTS;
|
||||
|
||||
case "expunge":
|
||||
case "expunged":
|
||||
return EXPUNGE;
|
||||
|
||||
case "fetch":
|
||||
return FETCH;
|
||||
|
||||
case "recent":
|
||||
return RECENT;
|
||||
|
||||
default:
|
||||
// fall-through
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ImapError.PARSE_ERROR("\"%s\" unrecognized server data", root.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,13 +4,154 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Email data sent from the server to client in response to a command or unsolicited.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.2]] for more information.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.ServerData : ServerResponse {
|
||||
public ServerData(Tag tag) {
|
||||
public ServerDataType server_data_type { get; private set; }
|
||||
|
||||
private ServerData(Tag tag, ServerDataType server_data_type) {
|
||||
base (tag);
|
||||
|
||||
this.server_data_type = server_data_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link RootParameters} into {@link ServerData}.
|
||||
*
|
||||
* The supplied root is "stripped" of its children. This may happen even if an exception is
|
||||
* thrown. It's recommended to use {@link is_server_data} prior to this call.
|
||||
*/
|
||||
public ServerData.migrate(RootParameters root) throws ImapError {
|
||||
base.migrate(root);
|
||||
|
||||
server_data_type = ServerDataType.from_response(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@link RootParameters} is recognized by {@link ServerDataType.from_response}.
|
||||
*/
|
||||
public static bool is_server_data(RootParameters root) {
|
||||
if (!root.has_tag())
|
||||
return false;
|
||||
|
||||
try {
|
||||
ServerDataType.from_response(root);
|
||||
|
||||
return true;
|
||||
} catch (ImapError ierr) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ServerData} into {@link Capabilities}, if possible.
|
||||
*
|
||||
* Since Capabilities are revised with various {@link ClientSession} states, this method accepts
|
||||
* a ref to an int that will be incremented after handed to the Capabilities constructor. This
|
||||
* can be used to track the revision of capabilities seen on the connection.
|
||||
*
|
||||
* @throws ImapError.INVALID if not a Capability.
|
||||
*/
|
||||
public Capabilities get_capabilities(ref int next_revision) throws ImapError {
|
||||
if (server_data_type != ServerDataType.CAPABILITY)
|
||||
throw new ImapError.INVALID("Not CAPABILITY data: %s", to_string());
|
||||
|
||||
Capabilities capabilities = new Capabilities(next_revision++);
|
||||
for (int ctr = 2; ctr < get_count(); ctr++) {
|
||||
StringParameter? param = get_if_string(ctr);
|
||||
if (param != null)
|
||||
capabilities.add_parameter(param);
|
||||
}
|
||||
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ServerData} into an {@link ServerDataType.EXISTS} value, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not EXISTS.
|
||||
*/
|
||||
public int get_exists() throws ImapError {
|
||||
if (server_data_type != ServerDataType.EXISTS)
|
||||
throw new ImapError.INVALID("Not EXISTS data: %s", to_string());
|
||||
|
||||
return get_as_string(1).as_int(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ServerData} into an expunged {@link SequenceNumber}, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not an expunged MessageNumber.
|
||||
*/
|
||||
public SequenceNumber get_expunge() throws ImapError {
|
||||
if (server_data_type != ServerDataType.EXPUNGE)
|
||||
throw new ImapError.INVALID("Not EXPUNGE data: %s", to_string());
|
||||
|
||||
return new SequenceNumber(get_as_string(1).as_int());
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ServerData} into {@link FetchedData}, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not FetchData.
|
||||
*/
|
||||
public FetchedData get_fetch() throws ImapError {
|
||||
if (server_data_type != ServerDataType.FETCH)
|
||||
throw new ImapError.INVALID("Not FETCH data: %s", to_string());
|
||||
|
||||
return FetchedData.decode(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ServerData} into {@link MailboxAttributes}, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not MailboxAttributes.
|
||||
*/
|
||||
public MailboxAttributes get_flags() throws ImapError {
|
||||
if (server_data_type != ServerDataType.FLAGS)
|
||||
throw new ImapError.INVALID("Not FLAGS data: %s", to_string());
|
||||
|
||||
return MailboxAttributes.from_list(get_as_list(2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ServerData} into {@link MailboxInformation}, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not MailboxInformation.
|
||||
*/
|
||||
public MailboxInformation get_list() throws ImapError {
|
||||
if (server_data_type != ServerDataType.LIST && server_data_type != ServerDataType.XLIST)
|
||||
throw new ImapError.INVALID("Not LIST/XLIST data: %s", to_string());
|
||||
|
||||
return MailboxInformation.decode(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ServerData} into a {@link ServerDataType.RECENT} value, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not a {@link ServerDataType.RECENT} value.
|
||||
*/
|
||||
public int get_recent() throws ImapError {
|
||||
if (server_data_type != ServerDataType.RECENT)
|
||||
throw new ImapError.INVALID("Not RECENT data: %s", to_string());
|
||||
|
||||
return get_as_string(1).as_int(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the {@link ServerData} into {@link StatusData}, if possible.
|
||||
*
|
||||
* @throws ImapError.INVALID if not {@link StatusData}.
|
||||
*/
|
||||
public StatusData get_status() throws ImapError {
|
||||
if (server_data_type != ServerDataType.STATUS)
|
||||
throw new ImapError.INVALID("Not STATUS data: %s", to_string());
|
||||
|
||||
return StatusData.decode(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,50 +4,57 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A response sent from the server to client.
|
||||
*
|
||||
* ServerResponses can take various shapes, including tagged/untagged and some common forms where
|
||||
* status and status text are supplied.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7]] for more information.
|
||||
*/
|
||||
|
||||
public abstract class Geary.Imap.ServerResponse : RootParameters {
|
||||
public enum Type {
|
||||
STATUS_RESPONSE,
|
||||
SERVER_DATA,
|
||||
CONTINUATION_RESPONSE
|
||||
}
|
||||
|
||||
public Tag tag { get; private set; }
|
||||
|
||||
public ServerResponse(Tag tag) {
|
||||
protected ServerResponse(Tag tag) {
|
||||
this.tag = tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link RootParameters} into a ServerResponse.
|
||||
*
|
||||
* The supplied root is "stripped" of its children.
|
||||
*/
|
||||
public ServerResponse.migrate(RootParameters root) throws ImapError {
|
||||
base.migrate(root);
|
||||
|
||||
tag = new Tag.from_parameter((StringParameter) get_as(0, typeof(StringParameter)));
|
||||
if (!has_tag())
|
||||
throw new ImapError.INVALID("Server response does not have a tag token: %s", to_string());
|
||||
|
||||
tag = get_tag();
|
||||
}
|
||||
|
||||
// The RootParameters are migrated and will be stripped upon exit.
|
||||
public static ServerResponse migrate_from_server(RootParameters root, out Type response_type)
|
||||
throws ImapError {
|
||||
Tag tag = new Tag.from_parameter(root.get_as_string(0));
|
||||
if (tag.is_tagged()) {
|
||||
// Attempt to decode second parameter for predefined status codes (piggyback on
|
||||
// Status.decode's exception if this is invalid)
|
||||
StringParameter? statusparam = root.get_if_string(1);
|
||||
if (statusparam != null)
|
||||
Status.decode(statusparam.value);
|
||||
|
||||
// tagged and has proper status, so it's a status response
|
||||
response_type = Type.STATUS_RESPONSE;
|
||||
|
||||
return new StatusResponse.migrate(root);
|
||||
} else if (tag.is_continuation()) {
|
||||
// nothing to decode; everything after the tag is human-readable stuff
|
||||
response_type = Type.CONTINUATION_RESPONSE;
|
||||
|
||||
/**
|
||||
* Migrate the contents of RootParameters into a new, properly-typed ServerResponse.
|
||||
*
|
||||
* The returned ServerResponse may be a {@link ContinuationResponse}, {@link ServerData},
|
||||
* or a generic {@link StatusResponse}.
|
||||
*
|
||||
* The RootParameters will be migrated and stripped clean upon exit.
|
||||
*
|
||||
* @throws ImapError.PARSE_ERROR if not a known form of ServerResponse.
|
||||
*/
|
||||
public static ServerResponse migrate_from_server(RootParameters root) throws ImapError {
|
||||
if (ContinuationResponse.is_continuation_response(root))
|
||||
return new ContinuationResponse.migrate(root);
|
||||
}
|
||||
|
||||
response_type = Type.SERVER_DATA;
|
||||
if (StatusResponse.is_status_response(root))
|
||||
return new StatusResponse.migrate(root);
|
||||
|
||||
return new ServerData.migrate(root);
|
||||
if (ServerData.is_server_data(root))
|
||||
return new ServerData.migrate(root);
|
||||
|
||||
throw new ImapError.PARSE_ERROR("Unknown server response: %s", root.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,27 +4,55 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
public class Geary.Imap.StatusResults : Geary.Imap.CommandResults {
|
||||
public string mailbox { get; private set; }
|
||||
/**
|
||||
* The decoded response to a STATUS command.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.2.4]]
|
||||
*
|
||||
* @see StatusCommand
|
||||
*/
|
||||
|
||||
public class Geary.Imap.StatusData : Object {
|
||||
// NOTE: This must be negative one; other values won't work well due to how the values are
|
||||
// decoded
|
||||
public const int UNSET = -1;
|
||||
|
||||
/**
|
||||
* -1 if not set.
|
||||
* Name of the mailbox.
|
||||
*/
|
||||
public MailboxSpecifier mailbox { get; private set; }
|
||||
|
||||
/**
|
||||
* {@link UNSET} if not set.
|
||||
*/
|
||||
public int messages { get; private set; }
|
||||
|
||||
/**
|
||||
* -1 if not set.
|
||||
* {@link UNSET} if not set.
|
||||
*/
|
||||
public int recent { get; private set; }
|
||||
public UID? uid_next { get; private set; }
|
||||
public UIDValidity? uid_validity { get; private set; }
|
||||
|
||||
/**
|
||||
* -1 if not set.
|
||||
* The UIDNEXT of the mailbox, if returned.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-2.3.1.1]]
|
||||
*/
|
||||
public UID? uid_next { get; private set; }
|
||||
|
||||
/**
|
||||
* The UIDVALIDITY of the mailbox, if returned.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-2.3.1.1]]
|
||||
*/
|
||||
public UIDValidity? uid_validity { get; private set; }
|
||||
|
||||
/**
|
||||
* {@link UNSET} if not set.
|
||||
*/
|
||||
public int unseen { get; private set; }
|
||||
|
||||
private StatusResults(StatusResponse status_response, string mailbox, int messages, int recent,
|
||||
UID? uid_next, UIDValidity? uid_validity, int unseen) {
|
||||
base (status_response);
|
||||
|
||||
public StatusData(MailboxSpecifier mailbox, int messages, int recent, UID? uid_next,
|
||||
UIDValidity? uid_validity, int unseen) {
|
||||
this.mailbox = mailbox;
|
||||
this.messages = messages;
|
||||
this.recent = recent;
|
||||
|
|
@ -33,30 +61,30 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults {
|
|||
this.unseen = unseen;
|
||||
}
|
||||
|
||||
public static StatusResults decode(CommandResponse response) throws ImapError {
|
||||
assert(response.is_sealed());
|
||||
|
||||
// only use the first untagged response of status; zero is a problem, more than one are
|
||||
// ignored
|
||||
if (response.server_data.size == 0)
|
||||
throw new ImapError.PARSE_ERROR("No STATUS response line: \"%s\"", response.to_string());
|
||||
|
||||
ServerData data = response.server_data[0];
|
||||
StringParameter cmd = data.get_as_string(1);
|
||||
MailboxParameter mailbox = new MailboxParameter.from_string_parameter(data.get_as_string(2));
|
||||
ListParameter values = data.get_as_list(3);
|
||||
|
||||
if (!cmd.equals_ci(StatusCommand.NAME)) {
|
||||
/**
|
||||
* Decodes {@link ServerData} into a StatusData representation.
|
||||
*
|
||||
* The ServerData must be the response to a STATUS command.
|
||||
*
|
||||
* @see StatusCommand
|
||||
* @see ServerData.get_status
|
||||
*/
|
||||
public static StatusData decode(ServerData server_data) throws ImapError {
|
||||
if (!server_data.get_as_string(1).equals_ci(StatusCommand.NAME)) {
|
||||
throw new ImapError.PARSE_ERROR("Bad STATUS command name in response \"%s\"",
|
||||
response.to_string());
|
||||
server_data.to_string());
|
||||
}
|
||||
|
||||
int messages = -1;
|
||||
int recent = -1;
|
||||
MailboxParameter mailbox_param = new MailboxParameter.from_string_parameter(
|
||||
server_data.get_as_string(2));
|
||||
|
||||
int messages = UNSET;
|
||||
int recent = UNSET;
|
||||
UID? uid_next = null;
|
||||
UIDValidity? uid_validity = null;
|
||||
int unseen = -1;
|
||||
int unseen = UNSET;
|
||||
|
||||
ListParameter values = server_data.get_as_list(3);
|
||||
for (int ctr = 0; ctr < values.get_count(); ctr += 2) {
|
||||
try {
|
||||
StringParameter typep = values.get_as_string(ctr);
|
||||
|
|
@ -64,10 +92,12 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults {
|
|||
|
||||
switch (StatusDataType.from_parameter(typep)) {
|
||||
case StatusDataType.MESSAGES:
|
||||
// see note at UNSET
|
||||
messages = valuep.as_int(-1, int.MAX);
|
||||
break;
|
||||
|
||||
case StatusDataType.RECENT:
|
||||
// see note at UNSET
|
||||
recent = valuep.as_int(-1, int.MAX);
|
||||
break;
|
||||
|
||||
|
|
@ -80,6 +110,7 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults {
|
|||
break;
|
||||
|
||||
case StatusDataType.UNSEEN:
|
||||
// see note at UNSET
|
||||
unseen = valuep.as_int(-1, int.MAX);
|
||||
break;
|
||||
|
||||
|
|
@ -89,12 +120,18 @@ public class Geary.Imap.StatusResults : Geary.Imap.CommandResults {
|
|||
}
|
||||
} catch (ImapError ierr) {
|
||||
message("Bad value at %d/%d in STATUS response \"%s\": %s", ctr, ctr + 1,
|
||||
response.to_string(), ierr.message);
|
||||
server_data.to_string(), ierr.message);
|
||||
}
|
||||
}
|
||||
|
||||
return new StatusResults(response.status_response, mailbox.decode(), messages, recent, uid_next,
|
||||
uid_validity, unseen);
|
||||
return new StatusData(new MailboxSpecifier.from_parameter(mailbox_param), messages, recent,
|
||||
uid_next, uid_validity, unseen);
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return "%s/%d/UIDNEXT=%s/UIDVALIDITY=%s".printf(mailbox.to_string(), messages,
|
||||
(uid_next != null) ? uid_next.to_string() : "(none)",
|
||||
(uid_validity != null) ? uid_validity.to_string() : "(none)");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4,48 +4,113 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A response line from the server indicating either a result from a command or an unsolicited
|
||||
* change in state.
|
||||
*
|
||||
* StatusResponses may be tagged or untagged, depending on their nature.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.1]] for more information.
|
||||
*
|
||||
* @see ServerResponse.migrate_from_server
|
||||
*/
|
||||
|
||||
public class Geary.Imap.StatusResponse : ServerResponse {
|
||||
public Status status { get; private set; }
|
||||
public ResponseCode? response_code { get; private set; }
|
||||
public string? text { get; private set; }
|
||||
/**
|
||||
* Returns true if this {@link StatusResponse} represents the completion of a {@link Command}.
|
||||
*
|
||||
* This is true if (a) the StatusResponse is tagged and (b) the {@link status} is
|
||||
* {@link Status.OK}, {@link Status.NO}, or {@link Status.BAD}.
|
||||
*/
|
||||
public bool is_completion { get; private set; default = false; }
|
||||
|
||||
public StatusResponse(Tag tag, Status status, ResponseCode? response_code, string? text) {
|
||||
/**
|
||||
* The {@link Status} being reported by the server in this {@link ServerResponse}.
|
||||
*/
|
||||
public Status status { get; private set; }
|
||||
|
||||
/**
|
||||
* An optional {@link ResponseCode} reported by the server in this {@link ServerResponse}.
|
||||
*/
|
||||
public ResponseCode? response_code { get; private set; }
|
||||
|
||||
private StatusResponse(Tag tag, Status status, ResponseCode? response_code) {
|
||||
base (tag);
|
||||
|
||||
this.status = status;
|
||||
this.response_code = response_code;
|
||||
this.text = text;
|
||||
|
||||
add(status.to_parameter());
|
||||
if (response_code != null)
|
||||
add(response_code);
|
||||
if (text != null)
|
||||
add(new StringParameter(text));
|
||||
update_is_completion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the {@link RootParameters} into a {@link StatusResponse}.
|
||||
*
|
||||
* The supplied root is "stripped" of its children. This may happen even if an exception is
|
||||
* thrown. It's recommended to use {@link is_status_response} prior to this call.
|
||||
*/
|
||||
public StatusResponse.migrate(RootParameters root) throws ImapError {
|
||||
base.migrate(root);
|
||||
|
||||
status = Status.from_parameter((StringParameter) get_as(1, typeof(StringParameter)));
|
||||
response_code = (ResponseCode?) get_if(2, typeof(ResponseCode));
|
||||
text = (response_code != null) ? flatten_to_text(3) : flatten_to_text(2);
|
||||
status = Status.from_parameter(get_as_string(1));
|
||||
response_code = get_if_list(2) as ResponseCode;
|
||||
update_is_completion();
|
||||
}
|
||||
|
||||
private string? flatten_to_text(int start_index) throws ImapError {
|
||||
private void update_is_completion() {
|
||||
// TODO: Is this too stringent? It means a faulty server could send back a completion
|
||||
// with another Status code and cause the client to treat the command as "unanswered",
|
||||
// requiring a timeout.
|
||||
is_completion = false;
|
||||
if (tag.is_tagged()) {
|
||||
switch (status) {
|
||||
case Status.OK:
|
||||
case Status.NO:
|
||||
case Status.BAD:
|
||||
is_completion = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
// fall through
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns optional text provided by the server. Note that this text is not internationalized
|
||||
* and probably in English, and is not standard or uniformly declared. It's not recommended
|
||||
* this text be displayed to the user.
|
||||
*/
|
||||
public string? get_text() {
|
||||
// build text from all StringParameters ... this will skip any ResponseCode or ListParameter
|
||||
// (or NilParameter, for that matter)
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
while (start_index < get_count()) {
|
||||
StringParameter? strparam = get_if_string(start_index);
|
||||
for (int index = 2; index < get_count(); index++) {
|
||||
StringParameter? strparam = get_if_string(index);
|
||||
if (strparam != null) {
|
||||
builder.append(strparam.value);
|
||||
if (start_index < (get_count() - 1))
|
||||
if (index < (get_count() - 1))
|
||||
builder.append_c(' ');
|
||||
}
|
||||
|
||||
start_index++;
|
||||
}
|
||||
|
||||
return !String.is_empty(builder.str) ? builder.str : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if {@link RootParameters} holds a {@link Status} parameter.
|
||||
*/
|
||||
public static bool is_status_response(RootParameters root) {
|
||||
if (!root.has_tag())
|
||||
return false;
|
||||
|
||||
try {
|
||||
Status.from_parameter(root.get_as_string(1));
|
||||
|
||||
return true;
|
||||
} catch (ImapError err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,12 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An optional status code accompanying a {@link ServerResponse}.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.1]] for more information.
|
||||
*/
|
||||
|
||||
public enum Geary.Imap.Status {
|
||||
OK,
|
||||
NO,
|
||||
|
|
@ -60,11 +66,7 @@ public enum Geary.Imap.Status {
|
|||
}
|
||||
|
||||
public Parameter to_parameter() {
|
||||
return new StringParameter(to_string());
|
||||
}
|
||||
|
||||
public void serialize(Serializer ser) throws Error {
|
||||
ser.push_string(to_string());
|
||||
return new AtomParameter(to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,133 +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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Some ServerData returned by the server may be unsolicited and not an expected part of the command.
|
||||
* "Unsolicited" is contextual, since these fields may be returned as a natural part of a command
|
||||
* (SELECT/EXAMINE or EXPUNGE) or expected (NOOP). In other situations, they must be dealt with
|
||||
* out-of-band and the unsolicited ServerData not considered as part of the normal CommandResponse.
|
||||
*
|
||||
* Note that only one of the fields (exists, recent, expunge, or flags) will be valid for any
|
||||
* ServerData; it's impossible that more than one will be valid.
|
||||
*/
|
||||
public class Geary.Imap.UnsolicitedServerData : BaseObject {
|
||||
/**
|
||||
* -1 means not found in ServerData
|
||||
*/
|
||||
public int exists { get; private set; }
|
||||
/**
|
||||
* -1 means not found in ServerData
|
||||
*/
|
||||
public int recent { get; private set; }
|
||||
/**
|
||||
* null means not found in ServerData
|
||||
*/
|
||||
public MessageNumber? expunge { get; private set; }
|
||||
/**
|
||||
* null means not found in ServerData
|
||||
*/
|
||||
public MailboxAttributes? flags { get; private set; }
|
||||
|
||||
private UnsolicitedServerData(int exists, int recent, MessageNumber? expunge, MailboxAttributes? flags) {
|
||||
this.exists = exists;
|
||||
this.recent = recent;
|
||||
this.expunge = expunge;
|
||||
this.flags = flags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns null if not recognized as unsolicited server data.
|
||||
*/
|
||||
public static UnsolicitedServerData? from_server_data(ServerData data) {
|
||||
// Note that unsolicited server data is formatted the same save for FLAGS:
|
||||
//
|
||||
// * 47 EXISTS
|
||||
// * 3 EXPUNGE
|
||||
// * FLAGS (\answered \flagged \deleted \seen)
|
||||
// * 15 RECENT
|
||||
//
|
||||
// Also note that these server data are *not* unsolicited if they're associated with their
|
||||
// "natural" command (i.e. SELECT/EXAMINE, NOOP) although the NOOP decoder uses this object
|
||||
// to do its decoding.
|
||||
//
|
||||
// Also note that the unsolicited data is EXPUNGE while the EXPUNGE command expects
|
||||
// EXPUNGED (past tense) server data to be returned
|
||||
|
||||
// first unsolicited param is always a string
|
||||
StringParameter? first_string = data.get_if_string(1);
|
||||
if (first_string == null)
|
||||
return null;
|
||||
|
||||
// second might be a string or a list
|
||||
StringParameter? second_string = data.get_if_string(2);
|
||||
ListParameter? second_list = data.get_if_list(2);
|
||||
if (second_string == null && second_list == null)
|
||||
return null;
|
||||
|
||||
// determine command and value by types
|
||||
StringParameter? cmdparam = null;
|
||||
StringParameter? strparam = null;
|
||||
ListParameter? listparam = null;
|
||||
if (second_list != null) {
|
||||
cmdparam = first_string;
|
||||
listparam = second_list;
|
||||
} else {
|
||||
cmdparam = second_string;
|
||||
strparam = first_string;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (cmdparam.value.down()) {
|
||||
case "exists":
|
||||
return (strparam != null)
|
||||
? new UnsolicitedServerData(strparam.as_int(), -1, null, null)
|
||||
: null;
|
||||
|
||||
case "recent":
|
||||
return (strparam != null)
|
||||
? new UnsolicitedServerData(-1, strparam.as_int(), null, null)
|
||||
: null;
|
||||
|
||||
case "expunge":
|
||||
case "expunged": // Automatically handles ExpungeCommand results
|
||||
return (strparam != null)
|
||||
? new UnsolicitedServerData(-1, -1, new MessageNumber(strparam.as_int()), null)
|
||||
: null;
|
||||
|
||||
case "flags":
|
||||
return (listparam != null)
|
||||
? new UnsolicitedServerData(-1, -1, null, MailboxAttributes.from_list(listparam))
|
||||
: null;
|
||||
|
||||
default:
|
||||
// an unrecognized parameter
|
||||
return null;
|
||||
}
|
||||
} catch (ImapError err) {
|
||||
debug("Unable to decode unsolicited data \"%s\": %s", data.to_string(), err.message);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
if (exists >= 0)
|
||||
return "EXISTS %d".printf(exists);
|
||||
|
||||
if (recent >= 0)
|
||||
return "RECENT %d".printf(recent);
|
||||
|
||||
if (expunge != null)
|
||||
return "EXPUNGE %s".printf(expunge.to_string());
|
||||
|
||||
if (flags != null)
|
||||
return "FLAGS %s".printf(flags.to_string());
|
||||
|
||||
return "(invalid unsolicited data)";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -25,7 +25,8 @@ public class Geary.Imap.ClientConnection : BaseObject {
|
|||
|
||||
/**
|
||||
* The default timeout for an issued command to result in a response code from the server.
|
||||
* A timed-out command will result in the connection being forcibly closed.
|
||||
*
|
||||
* @see command_timeout_sec
|
||||
*/
|
||||
public const uint DEFAULT_COMMAND_TIMEOUT_SEC = 15;
|
||||
|
||||
|
|
@ -75,10 +76,23 @@ public class Geary.Imap.ClientConnection : BaseObject {
|
|||
// Used solely for debugging
|
||||
private static int next_cx_id = 0;
|
||||
|
||||
/**
|
||||
* The timeout in seconds before an uncompleted {@link Command} is considered abandoned.
|
||||
*
|
||||
* ClientConnection does not time out the initial greeting from the server (as there's no
|
||||
* command associated with it). That's the responsibility of the caller.
|
||||
*
|
||||
* A timed-out command will result in the connection being forcibly closed.
|
||||
*/
|
||||
public uint command_timeout_sec { get; set; default = DEFAULT_COMMAND_TIMEOUT_SEC; }
|
||||
|
||||
/**
|
||||
* This identifier is used only for debugging, to differentiate connections from one another
|
||||
* in logs and debug output.
|
||||
*/
|
||||
public int cx_id { get; private set; }
|
||||
|
||||
private Geary.Endpoint endpoint;
|
||||
private int cx_id;
|
||||
private Geary.State.Machine fsm;
|
||||
private SocketConnection? cx = null;
|
||||
private IOStream? ios = null;
|
||||
|
|
@ -326,6 +340,8 @@ public class Geary.Imap.ClientConnection : BaseObject {
|
|||
cx = null;
|
||||
ios = null;
|
||||
|
||||
receive_failure(err);
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
|
@ -374,8 +390,8 @@ public class Geary.Imap.ClientConnection : BaseObject {
|
|||
|
||||
// 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(ios.output_stream);
|
||||
des = new Deserializer(ios.input_stream);
|
||||
ser = new Serializer(to_string(), ios.output_stream);
|
||||
des = new Deserializer(to_string(), ios.input_stream);
|
||||
|
||||
des.parameters_ready.connect(on_parameters_ready);
|
||||
des.bytes_received.connect(on_bytes_received);
|
||||
|
|
@ -429,29 +445,38 @@ public class Geary.Imap.ClientConnection : BaseObject {
|
|||
}
|
||||
|
||||
private void on_parameters_ready(RootParameters root) {
|
||||
ServerResponse response;
|
||||
try {
|
||||
ServerResponse.Type response_type;
|
||||
ServerResponse response = ServerResponse.migrate_from_server(root, out response_type);
|
||||
|
||||
switch (response_type) {
|
||||
case ServerResponse.Type.STATUS_RESPONSE:
|
||||
fsm.issue(Event.RECVD_STATUS_RESPONSE, null, response);
|
||||
break;
|
||||
|
||||
case ServerResponse.Type.SERVER_DATA:
|
||||
fsm.issue(Event.RECVD_SERVER_DATA, null, response);
|
||||
break;
|
||||
|
||||
case ServerResponse.Type.CONTINUATION_RESPONSE:
|
||||
fsm.issue(Event.RECVD_CONTINUATION_RESPONSE, null, response);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
response = ServerResponse.migrate_from_server(root);
|
||||
} catch (ImapError err) {
|
||||
received_bad_response(root, err);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
StatusResponse? status_response = response as StatusResponse;
|
||||
if (status_response != null) {
|
||||
fsm.issue(Event.RECVD_STATUS_RESPONSE, null, status_response);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ServerData? server_data = response as ServerData;
|
||||
if (server_data != null) {
|
||||
fsm.issue(Event.RECVD_SERVER_DATA, null, server_data);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ContinuationResponse? continuation_response = response as ContinuationResponse;
|
||||
if (continuation_response != null) {
|
||||
fsm.issue(Event.RECVD_CONTINUATION_RESPONSE, null, continuation_response);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
error("[%s] Unknown ServerResponse of type %s received: %s:", to_string(), response.get_type().name(),
|
||||
response.to_string());
|
||||
}
|
||||
|
||||
private void on_bytes_received(size_t bytes) {
|
||||
|
|
@ -693,12 +718,13 @@ public class Geary.Imap.ClientConnection : BaseObject {
|
|||
}
|
||||
|
||||
private void signal_status_response(void *user, Object? object) {
|
||||
StatusResponse status_response = (StatusResponse) object;
|
||||
StatusResponse? status_response = object as StatusResponse;
|
||||
if (status_response != null && status_response.is_completion) {
|
||||
// stop the countdown timer on the associated command
|
||||
cmd_completed_timeout();
|
||||
}
|
||||
|
||||
// stop the countdown timer on the associated command
|
||||
cmd_completed_timeout();
|
||||
|
||||
received_status_response(status_response);
|
||||
received_status_response((StatusResponse) object);
|
||||
}
|
||||
|
||||
private void signal_continuation(void *user, Object? object) {
|
||||
|
|
@ -796,7 +822,7 @@ public class Geary.Imap.ClientConnection : BaseObject {
|
|||
|
||||
try {
|
||||
Logging.debug(Logging.Flag.NETWORK, "[%s S] %s", to_string(), "done");
|
||||
ser.push_string("done");
|
||||
ser.push_unquoted_string("done");
|
||||
ser.push_eol();
|
||||
} catch (Error err) {
|
||||
debug("[%s] Unable to close IDLE: %s", to_string(), err.message);
|
||||
|
|
|
|||
|
|
@ -9,29 +9,57 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
|
|||
|
||||
public bool is_open { get; private set; default = false; }
|
||||
|
||||
/**
|
||||
* Set to zero or negative value if keepalives should be disabled when a connection has not
|
||||
* selected a mailbox. (This is not recommended.)
|
||||
*
|
||||
* This only affects newly created sessions or sessions leaving the selected/examined state
|
||||
* and returning to an authorized state.
|
||||
*/
|
||||
public uint unselected_keepalive_sec { get; set; default = ClientSession.DEFAULT_UNSELECTED_KEEPALIVE_SEC; }
|
||||
|
||||
/**
|
||||
* Set to zero or negative value if keepalives should be disabled when a mailbox is selected
|
||||
* or examined. (This is not recommended.)
|
||||
*
|
||||
* This only affects newly selected/examined sessions.
|
||||
*/
|
||||
public uint selected_keepalive_sec { get; set; default = ClientSession.DEFAULT_SELECTED_KEEPALIVE_SEC; }
|
||||
|
||||
/**
|
||||
* Set to zero or negative value if keepalives should be disabled when a mailbox is selected
|
||||
* or examined and IDLE is supported. (This is not recommended.)
|
||||
*
|
||||
* This only affects newly selected/examined sessions.
|
||||
*/
|
||||
public uint selected_with_idle_keepalive_sec { get; set; default = ClientSession.DEFAULT_SELECTED_WITH_IDLE_KEEPALIVE_SEC; }
|
||||
|
||||
/**
|
||||
* ClientSessionManager attempts to maintain a minimum number of open sessions with the server
|
||||
* so they're immediately ready for use.
|
||||
*
|
||||
* Setting this does not immediately adjust the pool size in either direction. Adjustment will
|
||||
* happen as connections are needed or closed.
|
||||
*/
|
||||
public int min_pool_size { get; set; default = DEFAULT_MIN_POOL_SIZE; }
|
||||
|
||||
private AccountInformation account_information;
|
||||
private int min_pool_size;
|
||||
private Gee.HashSet<ClientSession> sessions = new Gee.HashSet<ClientSession>();
|
||||
private Geary.Nonblocking.Mutex sessions_mutex = new Geary.Nonblocking.Mutex();
|
||||
private Gee.HashSet<SelectedContext> examined_contexts = new Gee.HashSet<SelectedContext>();
|
||||
private Gee.HashSet<SelectedContext> selected_contexts = new Gee.HashSet<SelectedContext>();
|
||||
private uint unselected_keepalive_sec = ClientSession.DEFAULT_UNSELECTED_KEEPALIVE_SEC;
|
||||
private uint selected_keepalive_sec = ClientSession.DEFAULT_SELECTED_KEEPALIVE_SEC;
|
||||
private uint selected_with_idle_keepalive_sec = ClientSession.DEFAULT_SELECTED_WITH_IDLE_KEEPALIVE_SEC;
|
||||
private Nonblocking.Mutex sessions_mutex = new Nonblocking.Mutex();
|
||||
private Gee.HashSet<ClientSession> reserved_sessions = new Gee.HashSet<ClientSession>();
|
||||
private bool authentication_failed = false;
|
||||
|
||||
public signal void login_failed();
|
||||
|
||||
public ClientSessionManager(AccountInformation account_information,
|
||||
int min_pool_size = DEFAULT_MIN_POOL_SIZE) {
|
||||
public ClientSessionManager(AccountInformation account_information) {
|
||||
this.account_information = account_information;
|
||||
this.min_pool_size = min_pool_size;
|
||||
|
||||
account_information.notify["imap-credentials"].connect(on_imap_credentials_notified);
|
||||
}
|
||||
|
||||
~ClientSessionManager() {
|
||||
account_information.notify["imap-credentials"].disconnect(on_imap_credentials_notified);
|
||||
if (is_open)
|
||||
warning("Destroying opened ClientSessionManager");
|
||||
}
|
||||
|
||||
public async void open_async(Cancellable? cancellable) throws Error {
|
||||
|
|
@ -123,204 +151,23 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to zero or negative value if keepalives should be disabled when a connection has not
|
||||
* selected a mailbox. (This is not recommended.)
|
||||
*
|
||||
* This only affects newly created sessions or sessions leaving the selected/examined state
|
||||
* and returning to an authorized state.
|
||||
*/
|
||||
public void set_unselected_keepalive(int unselected_keepalive_sec) {
|
||||
// set for future connections
|
||||
this.unselected_keepalive_sec = unselected_keepalive_sec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to zero or negative value if keepalives should be disabled when a mailbox is selected
|
||||
* or examined. (This is not recommended.)
|
||||
*
|
||||
* This only affects newly selected/examined sessions.
|
||||
*/
|
||||
public void set_selected_keepalive(int selected_keepalive_sec) {
|
||||
this.selected_keepalive_sec = selected_keepalive_sec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set to zero or negative value if keepalives should be disabled when a mailbox is selected
|
||||
* or examined and IDLE is supported. (This is not recommended.)
|
||||
*
|
||||
* This only affects newly selected/examined sessions.
|
||||
*/
|
||||
public void set_selected_with_idle_keepalive(int selected_with_idle_keepalive_sec) {
|
||||
this.selected_with_idle_keepalive_sec = selected_with_idle_keepalive_sec;
|
||||
}
|
||||
|
||||
public async Gee.Collection<Geary.Imap.MailboxInformation> list_roots(
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
ClientSession session = yield get_authorized_session_async(cancellable);
|
||||
|
||||
ListResults results = ListResults.decode(yield session.send_command_async(
|
||||
new ListCommand.wildcarded("", new Geary.Imap.MailboxParameter("%"),
|
||||
session.get_capabilities().has_capability("XLIST")),
|
||||
cancellable));
|
||||
|
||||
if (results.status_response.status != Status.OK)
|
||||
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
|
||||
|
||||
return results.get_all();
|
||||
}
|
||||
|
||||
public async Gee.Collection<Geary.Imap.MailboxInformation> list(string parent,
|
||||
string delim, Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
// build a proper IMAP specifier
|
||||
string specifier = parent;
|
||||
specifier += specifier.has_suffix(delim) ? "%" : (delim + "%");
|
||||
|
||||
ClientSession session = yield get_authorized_session_async(cancellable);
|
||||
|
||||
ListResults results = ListResults.decode(yield session.send_command_async(
|
||||
new ListCommand(new Geary.Imap.MailboxParameter(specifier),
|
||||
session.get_capabilities().has_capability("XLIST")),
|
||||
cancellable));
|
||||
|
||||
if (results.status_response.status != Status.OK)
|
||||
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
|
||||
|
||||
return results.get_all();
|
||||
}
|
||||
|
||||
public async bool folder_exists_async(string path, Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
ClientSession session = yield get_authorized_session_async(cancellable);
|
||||
|
||||
ListResults results = ListResults.decode(yield session.send_command_async(
|
||||
new ListCommand(new Geary.Imap.MailboxParameter(path),
|
||||
session.get_capabilities().has_capability("XLIST")),
|
||||
cancellable));
|
||||
|
||||
return (results.status_response.status == Status.OK) && (results.get_count() == 1);
|
||||
}
|
||||
|
||||
public async Geary.Imap.MailboxInformation? fetch_async(string path,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
ClientSession session = yield get_authorized_session_async(cancellable);
|
||||
|
||||
ListResults results = ListResults.decode(yield session.send_command_async(
|
||||
new ListCommand(new Geary.Imap.MailboxParameter(path),
|
||||
session.get_capabilities().has_capability("XLIST")),
|
||||
cancellable));
|
||||
|
||||
if (results.status_response.status != Status.OK)
|
||||
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
|
||||
|
||||
return (results.get_count() > 0) ? results.get_all()[0] : null;
|
||||
}
|
||||
|
||||
public async Geary.Imap.StatusResults status_async(string path, StatusDataType[] types,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
ClientSession session = yield get_authorized_session_async(cancellable);
|
||||
|
||||
StatusResults results = StatusResults.decode(yield session.send_command_async(
|
||||
new StatusCommand(new Geary.Imap.MailboxParameter(path), types), cancellable));
|
||||
|
||||
if (results.status_response.status != Status.OK)
|
||||
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public async Mailbox select_mailbox(Geary.FolderPath path, string? delim,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
return yield select_examine_mailbox(path, delim, true, cancellable);
|
||||
}
|
||||
|
||||
public async Mailbox examine_mailbox(Geary.FolderPath path, string? delim,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
return yield select_examine_mailbox(path, delim, false, cancellable);
|
||||
}
|
||||
|
||||
public async Mailbox select_examine_mailbox(Geary.FolderPath path, string? delim,
|
||||
bool is_select, Cancellable? cancellable = null) throws Error {
|
||||
check_open();
|
||||
|
||||
Gee.HashSet<SelectedContext> contexts = is_select ? selected_contexts : examined_contexts;
|
||||
SelectedContext new_context = yield select_examine_async(
|
||||
path.get_fullpath(delim), is_select, cancellable);
|
||||
|
||||
if (!contexts.contains(new_context)) {
|
||||
// Can't use the ternary operator due to this bug:
|
||||
// https://bugzilla.gnome.org/show_bug.cgi?id=599349
|
||||
if (is_select)
|
||||
new_context.freed.connect(on_selected_context_freed);
|
||||
else
|
||||
new_context.freed.connect(on_examined_context_freed);
|
||||
|
||||
bool added = contexts.add(new_context);
|
||||
assert(added);
|
||||
}
|
||||
|
||||
return new Mailbox(new_context, path);
|
||||
}
|
||||
|
||||
private void on_selected_context_freed(Geary.ReferenceSemantics semantics) {
|
||||
on_context_freed(semantics, selected_contexts);
|
||||
}
|
||||
|
||||
private void on_examined_context_freed(Geary.ReferenceSemantics semantics) {
|
||||
on_context_freed(semantics, examined_contexts);
|
||||
}
|
||||
|
||||
private void on_context_freed(Geary.ReferenceSemantics semantics,
|
||||
Gee.HashSet<SelectedContext> contexts) {
|
||||
SelectedContext context = (SelectedContext) semantics;
|
||||
|
||||
// last reference to the Mailbox has been dropped, so drop the mailbox and move the
|
||||
// ClientSession back to the authorized state
|
||||
bool removed = contexts.remove(context);
|
||||
assert(removed);
|
||||
|
||||
do_close_mailbox_async.begin(context);
|
||||
}
|
||||
|
||||
private async void do_close_mailbox_async(SelectedContext context) {
|
||||
try {
|
||||
if (context.session != null)
|
||||
yield context.session.close_mailbox_async();
|
||||
} catch (Error err) {
|
||||
debug("Error closing IMAP mailbox: %s", err.message);
|
||||
|
||||
if (context.session != null)
|
||||
remove_session(context.session);
|
||||
}
|
||||
}
|
||||
|
||||
// This should only be called when sessions_mutex is locked.
|
||||
private async ClientSession create_new_authorized_session(Cancellable? cancellable) throws Error {
|
||||
if (authentication_failed)
|
||||
throw new ImapError.UNAUTHENTICATED("Invalid ClientSessionManager credentials");
|
||||
|
||||
ClientSession new_session = new ClientSession(account_information.get_imap_endpoint(),
|
||||
account_information.imap_server_pipeline);
|
||||
ClientSession new_session = new ClientSession(account_information.get_imap_endpoint());
|
||||
|
||||
// add session to pool before launching all the connect activity so error cases can properly
|
||||
// back it out
|
||||
add_session(new_session);
|
||||
locked_add_session(new_session);
|
||||
|
||||
try {
|
||||
yield new_session.connect_async(cancellable);
|
||||
} catch (Error err) {
|
||||
debug("[%s] Connect failure: %s", new_session.to_string(), err.message);
|
||||
|
||||
bool removed = remove_session(new_session);
|
||||
bool removed = locked_remove_session(new_session);
|
||||
assert(removed);
|
||||
|
||||
throw err;
|
||||
|
|
@ -340,7 +187,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
|
|||
new_session.to_string(), disconnect_err.message);
|
||||
}
|
||||
|
||||
bool removed = remove_session(new_session);
|
||||
bool removed = locked_remove_session(new_session);
|
||||
assert(removed);
|
||||
|
||||
throw err;
|
||||
|
|
@ -359,78 +206,154 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
|
|||
return new_session;
|
||||
}
|
||||
|
||||
private async ClientSession get_authorized_session_async(Cancellable? cancellable) throws Error {
|
||||
public async ClientSession claim_authorized_session_async(Cancellable? cancellable) throws Error {
|
||||
check_open();
|
||||
|
||||
int token = yield sessions_mutex.claim_async(cancellable);
|
||||
|
||||
ClientSession? found_session = null;
|
||||
foreach (ClientSession session in sessions) {
|
||||
string? mailbox;
|
||||
if (session.get_context(out mailbox) == ClientSession.Context.AUTHORIZED) {
|
||||
MailboxSpecifier? mailbox;
|
||||
if (!reserved_sessions.contains(session) &&
|
||||
(session.get_context(out mailbox) == ClientSession.Context.AUTHORIZED)) {
|
||||
found_session = session;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Error? c = null;
|
||||
Error? err = null;
|
||||
try {
|
||||
if (found_session == null)
|
||||
found_session = yield create_new_authorized_session(cancellable);
|
||||
} catch (Error e2) {
|
||||
debug("Error creating session: %s", e2.message);
|
||||
c = e2;
|
||||
} finally {
|
||||
try {
|
||||
sessions_mutex.release(ref token);
|
||||
} catch (Error e) {
|
||||
debug("Error releasing mutex: %s", e.message);
|
||||
c = e;
|
||||
}
|
||||
} catch (Error create_err) {
|
||||
debug("Error creating session: %s", create_err.message);
|
||||
err = create_err;
|
||||
}
|
||||
|
||||
if (c != null)
|
||||
throw c;
|
||||
// claim it now
|
||||
if (found_session != null) {
|
||||
bool added = reserved_sessions.add(found_session);
|
||||
assert(added);
|
||||
}
|
||||
|
||||
try {
|
||||
sessions_mutex.release(ref token);
|
||||
} catch (Error release_err) {
|
||||
debug("Error releasing sessions table mutex: %s", release_err.message);
|
||||
}
|
||||
|
||||
if (err != null)
|
||||
throw err;
|
||||
|
||||
return found_session;
|
||||
}
|
||||
|
||||
private async SelectedContext select_examine_async(string folder, bool is_select,
|
||||
Cancellable? cancellable) throws Error {
|
||||
ClientSession.Context needed_context = (is_select) ? ClientSession.Context.SELECTED
|
||||
: ClientSession.Context.EXAMINED;
|
||||
public async void release_session_async(ClientSession session, Cancellable? cancellable)
|
||||
throws Error {
|
||||
check_open();
|
||||
|
||||
Gee.HashSet<SelectedContext> contexts = is_select ? selected_contexts : examined_contexts;
|
||||
foreach (SelectedContext c in contexts) {
|
||||
string? mailbox;
|
||||
if (c.session != null && (c.session.get_context(out mailbox) == needed_context &&
|
||||
mailbox == folder))
|
||||
return c;
|
||||
MailboxSpecifier? mailbox;
|
||||
ClientSession.Context context = session.get_context(out mailbox);
|
||||
|
||||
bool unreserve = false;
|
||||
switch (context) {
|
||||
case ClientSession.Context.AUTHORIZED:
|
||||
// keep as-is, but remove from the reserved list
|
||||
unreserve = true;
|
||||
break;
|
||||
|
||||
case ClientSession.Context.UNAUTHORIZED:
|
||||
yield force_disconnect_async(session, true);
|
||||
break;
|
||||
|
||||
case ClientSession.Context.UNCONNECTED:
|
||||
yield force_disconnect_async(session, false);
|
||||
break;
|
||||
|
||||
case ClientSession.Context.IN_PROGRESS:
|
||||
case ClientSession.Context.EXAMINED:
|
||||
case ClientSession.Context.SELECTED:
|
||||
// always close mailbox to return to authorized state
|
||||
try {
|
||||
yield session.close_mailbox_async(cancellable);
|
||||
} catch (ImapError imap_error) {
|
||||
debug("Error attempting to close released session %s: %s", session.to_string(),
|
||||
imap_error.message);
|
||||
}
|
||||
|
||||
// if not in authorized state now, drop it, otherwise remove from reserved list
|
||||
if (session.get_context(out mailbox) == ClientSession.Context.AUTHORIZED)
|
||||
unreserve = true;
|
||||
else
|
||||
yield force_disconnect_async(session, true);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
|
||||
ClientSession authd = yield get_authorized_session_async(cancellable);
|
||||
if (unreserve) {
|
||||
try {
|
||||
// don't respect Cancellable because this *must* happen; don't want this lingering
|
||||
// on the reserved list forever
|
||||
int token = yield sessions_mutex.claim_async();
|
||||
|
||||
bool removed = reserved_sessions.remove(session);
|
||||
assert(removed);
|
||||
|
||||
sessions_mutex.release(ref token);
|
||||
} catch (Error err) {
|
||||
message("Unable to remove %s from reserved list: %s", session.to_string(), err.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// It's possible this will be called more than once on the same session, especially in the case of a
|
||||
// remote close on reserved ClientSession, so this code is forgiving.
|
||||
private async void force_disconnect_async(ClientSession session, bool do_disconnect) {
|
||||
int token;
|
||||
try {
|
||||
token = yield sessions_mutex.claim_async();
|
||||
} catch (Error err) {
|
||||
debug("Unable to acquire sessions mutex: %s", err.message);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
SelectExamineResults results = yield authd.select_examine_async(folder, is_select, cancellable);
|
||||
locked_remove_session(session);
|
||||
|
||||
if (results.status_response.status != Status.OK)
|
||||
throw new ImapError.SERVER_ERROR("Server error: %s", results.to_string());
|
||||
if (do_disconnect) {
|
||||
try {
|
||||
yield session.disconnect_async();
|
||||
} catch (Error err) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
return new SelectedContext(authd, results);
|
||||
try {
|
||||
sessions_mutex.release(ref token);
|
||||
} catch (Error err) {
|
||||
debug("Unable to release sessions mutex: %s", err.message);
|
||||
}
|
||||
|
||||
adjust_session_pool.begin();
|
||||
}
|
||||
|
||||
private void on_disconnected(ClientSession session, ClientSession.DisconnectReason reason) {
|
||||
bool removed = remove_session(session);
|
||||
assert(removed);
|
||||
|
||||
adjust_session_pool.begin();
|
||||
force_disconnect_async.begin(session, false);
|
||||
}
|
||||
|
||||
private void on_login_failed(ClientSession session) {
|
||||
authentication_failed = true;
|
||||
|
||||
login_failed();
|
||||
|
||||
session.disconnect_async.begin();
|
||||
}
|
||||
|
||||
private void add_session(ClientSession session) {
|
||||
// Only call with sessions mutex locked
|
||||
private void locked_add_session(ClientSession session) {
|
||||
sessions.add(session);
|
||||
|
||||
// See create_new_authorized_session() for why the "disconnected" signal is not subscribed
|
||||
|
|
@ -438,13 +361,16 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
|
|||
session.login_failed.connect(on_login_failed);
|
||||
}
|
||||
|
||||
private bool remove_session(ClientSession session) {
|
||||
// Only call with sessions mutex locked
|
||||
private bool locked_remove_session(ClientSession session) {
|
||||
bool removed = sessions.remove(session);
|
||||
if (removed) {
|
||||
session.disconnected.disconnect(on_disconnected);
|
||||
session.login_failed.disconnect(on_login_failed);
|
||||
}
|
||||
|
||||
reserved_sessions.remove(session);
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -6,13 +6,15 @@
|
|||
|
||||
/**
|
||||
* The Deserializer performs asynchronous I/O on a supplied input stream and transforms the raw
|
||||
* bytes into IMAP Parameters (which can then be converted into ServerResponses or ServerData).
|
||||
* The Deserializer will only begin reading from the stream when start_async() is called. Calling
|
||||
* stop_async() will halt reading without closing the stream itself. A Deserializer may not be
|
||||
* reused once stop_async() has been invoked.
|
||||
* bytes into IMAP {@link Parameter}s (which can then be converted into {@link ServerResponse}s or
|
||||
* {@link ServerData}).
|
||||
*
|
||||
* The Deserializer will only begin reading from the stream when {@link start_async} is called.
|
||||
* Calling {@link stop_async} will halt reading without closing the stream itself. A Deserializer
|
||||
* may not be reused once stop_async has been invoked.
|
||||
*
|
||||
* Since all results from the Deserializer are reported via signals, those signals should be
|
||||
* connected to prior to calling start_async(), or the caller risks missing early messages. (Note
|
||||
* connected to prior to calling start_async, or the caller risks missing early messages. (Note
|
||||
* 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.)
|
||||
|
|
@ -32,6 +34,7 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
TAG,
|
||||
START_PARAM,
|
||||
ATOM,
|
||||
SYSTEM_FLAG,
|
||||
QUOTED,
|
||||
QUOTED_ESCAPE,
|
||||
PARTIAL_BODY_ATOM,
|
||||
|
|
@ -65,6 +68,7 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
"Geary.Imap.Deserializer", State.TAG, State.COUNT, Event.COUNT,
|
||||
state_to_string, event_to_string);
|
||||
|
||||
private string identifier;
|
||||
private ConverterInputStream cins;
|
||||
private DataInputStream dins;
|
||||
private Geary.State.Machine fsm;
|
||||
|
|
@ -80,20 +84,35 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
private int ins_priority = Priority.DEFAULT;
|
||||
private char[] atom_specials_exceptions = { ' ', ' ', '\0' };
|
||||
|
||||
/**
|
||||
* Fired when a complete set of IMAP {@link Parameter}s have been received.
|
||||
*
|
||||
* Note that {@link RootParameters} may contain {@link QuotedStringParameter}s,
|
||||
* {@link UnquotedStringParameter}s, {@link ResponseCode}, and {@link ListParameter}s.
|
||||
* Deserializer does not produce any other kind of Parameter due to its inability to deduce
|
||||
* them from syntax alone. ResponseCode, however, can be.
|
||||
*/
|
||||
public signal void parameters_ready(RootParameters root);
|
||||
|
||||
/**
|
||||
* "eos" is fired when the underlying InputStream is closed, whether due to normal EOS or input
|
||||
* error. Subscribe to "receive-failure" to be notified of errors.
|
||||
* Fired when the underlying InputStream is closed, whether due to normal EOS or input error.
|
||||
*
|
||||
* @see receive_failure
|
||||
*/
|
||||
public signal void eos();
|
||||
|
||||
/**
|
||||
* Fired when an Error is trapped on the input stream.
|
||||
*
|
||||
* This is nonrecoverable and means the stream should be closed and this Deserializer destroyed.
|
||||
*/
|
||||
public signal void receive_failure(Error err);
|
||||
|
||||
/**
|
||||
* "data-received" is fired as data blocks are received during download. The bytes themselves
|
||||
* may be partial and unusable out of context, so they're not provided, but their size is, to allow
|
||||
* monitoring of speed and such.
|
||||
* Fired as data blocks are received during download.
|
||||
*
|
||||
* The bytes themselves may be partial and unusable out of context, so they're not provided,
|
||||
* but their size is, to allow monitoring of speed and such.
|
||||
*
|
||||
* Note that this is fired for both line data (i.e. responses, status, etc.) and literal data
|
||||
* (block transfers).
|
||||
|
|
@ -103,9 +122,17 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
*/
|
||||
public signal void bytes_received(size_t bytes);
|
||||
|
||||
/**
|
||||
* Fired when a syntax error has occurred.
|
||||
*
|
||||
* This generally means the data looks like garbage and further deserialization is unlikely
|
||||
* or impossible.
|
||||
*/
|
||||
public signal void deserialize_failure();
|
||||
|
||||
public Deserializer(InputStream ins) {
|
||||
public Deserializer(string identifier, InputStream ins) {
|
||||
this.identifier = identifier;
|
||||
|
||||
cins = new ConverterInputStream(ins, midstream);
|
||||
cins.set_close_base_stream(false);
|
||||
dins = new DataInputStream(cins);
|
||||
|
|
@ -115,7 +142,7 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
context = root;
|
||||
|
||||
Geary.State.Mapping[] mappings = {
|
||||
new Geary.State.Mapping(State.TAG, Event.CHAR, on_tag_or_atom_char),
|
||||
new Geary.State.Mapping(State.TAG, Event.CHAR, on_tag_char),
|
||||
new Geary.State.Mapping(State.TAG, Event.EOS, on_eos),
|
||||
new Geary.State.Mapping(State.TAG, Event.ERROR, on_error),
|
||||
|
||||
|
|
@ -124,11 +151,16 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
new Geary.State.Mapping(State.START_PARAM, Event.EOS, on_eos),
|
||||
new Geary.State.Mapping(State.START_PARAM, Event.ERROR, on_error),
|
||||
|
||||
new Geary.State.Mapping(State.ATOM, Event.CHAR, on_tag_or_atom_char),
|
||||
new Geary.State.Mapping(State.ATOM, Event.CHAR, on_atom_char),
|
||||
new Geary.State.Mapping(State.ATOM, Event.EOL, on_atom_eol),
|
||||
new Geary.State.Mapping(State.ATOM, Event.EOS, on_eos),
|
||||
new Geary.State.Mapping(State.ATOM, Event.ERROR, on_error),
|
||||
|
||||
new Geary.State.Mapping(State.SYSTEM_FLAG, Event.CHAR, on_system_flag_char),
|
||||
new Geary.State.Mapping(State.SYSTEM_FLAG, Event.EOL, on_atom_eol),
|
||||
new Geary.State.Mapping(State.SYSTEM_FLAG, Event.EOS, on_eos),
|
||||
new Geary.State.Mapping(State.SYSTEM_FLAG, Event.ERROR, on_error),
|
||||
|
||||
new Geary.State.Mapping(State.QUOTED, Event.CHAR, on_quoted_char),
|
||||
new Geary.State.Mapping(State.QUOTED, Event.EOS, on_eos),
|
||||
new Geary.State.Mapping(State.QUOTED, Event.ERROR, on_error),
|
||||
|
|
@ -168,10 +200,20 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
fsm = new Geary.State.Machine(machine_desc, mappings, on_bad_transition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Install a custom Converter into the input stream.
|
||||
*
|
||||
* Can be used for decompression, decryption, and so on.
|
||||
*/
|
||||
public bool install_converter(Converter converter) {
|
||||
return midstream.install(converter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin deserializing IMAP responses from the input stream.
|
||||
*
|
||||
* Subscribe to the various signals before starting to ensure that all responses are trapped.
|
||||
*/
|
||||
public async void start_async(int priority = GLib.Priority.DEFAULT) throws Error {
|
||||
if (cancellable != null)
|
||||
throw new EngineError.ALREADY_OPEN("Deserializer already open");
|
||||
|
|
@ -239,11 +281,15 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
size_t bytes_read;
|
||||
string? line = dins.read_line_async.end(result, out bytes_read);
|
||||
if (line == null) {
|
||||
Logging.debug(Logging.Flag.DESERIALIZER, "[%s] line EOS", to_string());
|
||||
|
||||
push_eos();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Logging.debug(Logging.Flag.DESERIALIZER, "[%s] line %s", to_string(), line);
|
||||
|
||||
bytes_received(bytes_read);
|
||||
|
||||
push_line(line);
|
||||
|
|
@ -262,11 +308,15 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
// happens when actually pulling data
|
||||
size_t bytes_read = dins.read_async.end(result);
|
||||
if (bytes_read == 0 && literal_length_remaining > 0) {
|
||||
Logging.debug(Logging.Flag.DESERIALIZER, "[%s] block EOS", to_string());
|
||||
|
||||
push_eos();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Logging.debug(Logging.Flag.DESERIALIZER, "[%s] block %lub", to_string(), bytes_read);
|
||||
|
||||
bytes_received(bytes_read);
|
||||
|
||||
// adjust the current buffer's size to the amount that was actually read in
|
||||
|
|
@ -375,14 +425,18 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
current_string.append_unichar(ch);
|
||||
}
|
||||
|
||||
private void save_string_parameter() {
|
||||
if (is_current_string_empty())
|
||||
private void save_string_parameter(bool quoted) {
|
||||
// deal with empty quoted strings
|
||||
if (!quoted && is_current_string_empty())
|
||||
return;
|
||||
|
||||
if (NilParameter.is_nil(current_string.str))
|
||||
save_parameter(NilParameter.instance);
|
||||
// deal with empty quoted strings
|
||||
string str = (quoted && current_string == null) ? "" : current_string.str;
|
||||
|
||||
if (quoted)
|
||||
save_parameter(new QuotedStringParameter(str));
|
||||
else
|
||||
save_parameter(new StringParameter(current_string.str));
|
||||
save_parameter(new UnquotedStringParameter(str));
|
||||
|
||||
current_string = null;
|
||||
}
|
||||
|
|
@ -448,6 +502,10 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
return State.TAG;
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return "des:%s/%s".printf(identifier, fsm.get_state_string(fsm.get_state()));
|
||||
}
|
||||
|
||||
//
|
||||
// Transition handlers
|
||||
//
|
||||
|
|
@ -483,24 +541,37 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
if (ch == get_current_context_terminator())
|
||||
return pop();
|
||||
else
|
||||
return on_tag_or_atom_char(State.ATOM, event, user);
|
||||
return on_atom_char(state, event, user);
|
||||
}
|
||||
}
|
||||
|
||||
private uint on_eol(uint state, uint event, void *user) {
|
||||
return flush_params();
|
||||
private uint on_tag_char(uint state, uint event, void *user) {
|
||||
unichar ch = *((unichar *) user);
|
||||
|
||||
// drop if not allowed for tags (allowing for continuations and watching for spaces, which
|
||||
// indicate a change of state)
|
||||
if (DataFormat.is_tag_special(ch, " +"))
|
||||
return State.TAG;
|
||||
|
||||
// space indicates end of tag
|
||||
if (ch == ' ') {
|
||||
save_string_parameter(false);
|
||||
|
||||
return State.START_PARAM;
|
||||
}
|
||||
|
||||
append_to_string(ch);
|
||||
|
||||
return State.TAG;
|
||||
}
|
||||
|
||||
private uint on_tag_or_atom_char(uint state, uint event, void *user) {
|
||||
assert(state == State.TAG || state == State.ATOM);
|
||||
|
||||
private uint on_atom_char(uint state, uint event, void *user) {
|
||||
unichar ch = *((unichar *) user);
|
||||
|
||||
// The partial body fetch results ("BODY[section]" or "BODY[section]<partial>" and their
|
||||
// .peek variants) offer so many exceptions to the decoding process they're given their own
|
||||
// state
|
||||
if (state == State.ATOM && ch == '['
|
||||
&& (has_current_string_prefix("body") || has_current_string_prefix("body.peek"))) {
|
||||
if (ch == '[' && (has_current_string_prefix("body") || has_current_string_prefix("body.peek"))) {
|
||||
append_to_string(ch);
|
||||
|
||||
return State.PARTIAL_BODY_ATOM;
|
||||
|
|
@ -512,42 +583,74 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
char terminator = get_current_context_terminator();
|
||||
atom_specials_exceptions[1] = terminator;
|
||||
|
||||
// Atom specials includes space and close-parens, but those are handled in particular ways
|
||||
// while in the ATOM state, so they're excluded here. Like atom specials, the space is
|
||||
// treated in a particular way for tags, but unlike atom, the close-parens character is not.
|
||||
// The + symbol indicates a continuation and needs to be excepted when searching for a tag.
|
||||
if (state == State.TAG && DataFormat.is_tag_special(ch, " +"))
|
||||
return state;
|
||||
else if (state == State.ATOM && DataFormat.is_atom_special(ch, (string) atom_specials_exceptions))
|
||||
return state;
|
||||
// drop if not allowed for atoms, barring specials which indicate special state changes
|
||||
if (DataFormat.is_atom_special(ch, (string) atom_specials_exceptions))
|
||||
return State.ATOM;
|
||||
|
||||
// message flag indicator is only legal at start of atom
|
||||
if (state == State.ATOM && ch == '\\' && !is_current_string_empty())
|
||||
return state;
|
||||
if (ch == '\\' && is_current_string_empty()) {
|
||||
append_to_string(ch);
|
||||
|
||||
return State.SYSTEM_FLAG;
|
||||
}
|
||||
|
||||
// space indicates end-of-atom or end-of-tag
|
||||
// space indicates end-of-atom
|
||||
if (ch == ' ') {
|
||||
save_string_parameter();
|
||||
save_string_parameter(false);
|
||||
|
||||
return State.START_PARAM;
|
||||
}
|
||||
|
||||
// close-parens/close-square-bracket after an atom indicates end-of-list/end-of-response
|
||||
// code
|
||||
if (state == State.ATOM && ch == terminator) {
|
||||
save_string_parameter();
|
||||
if (ch == get_current_context_terminator()) {
|
||||
save_string_parameter(false);
|
||||
|
||||
return pop();
|
||||
}
|
||||
|
||||
append_to_string(ch);
|
||||
|
||||
return state;
|
||||
return State.ATOM;
|
||||
}
|
||||
|
||||
private uint on_system_flag_char(uint state, uint event, void *user) {
|
||||
unichar ch = *((unichar *) user);
|
||||
|
||||
// see note in on_atom_char for why/how this works
|
||||
char terminator = get_current_context_terminator();
|
||||
atom_specials_exceptions[1] = terminator;
|
||||
|
||||
// drop if not allowed for atoms, barring specials which indicate state changes
|
||||
// note that asterisk is allowed for flags
|
||||
if (ch != '*' && DataFormat.is_atom_special(ch, (string) atom_specials_exceptions))
|
||||
return State.SYSTEM_FLAG;
|
||||
|
||||
// space indicates end-of-system-flag
|
||||
if (ch == ' ') {
|
||||
save_string_parameter(false);
|
||||
|
||||
return State.START_PARAM;
|
||||
}
|
||||
|
||||
// close-parens/close-square-bracket after a system flag indicates end-of-list/end-of-response
|
||||
// code
|
||||
if (ch == terminator) {
|
||||
save_string_parameter(false);
|
||||
|
||||
return pop();
|
||||
}
|
||||
|
||||
append_to_string(ch);
|
||||
|
||||
return State.SYSTEM_FLAG;
|
||||
}
|
||||
|
||||
private uint on_eol(uint state, uint event, void *user) {
|
||||
return flush_params();
|
||||
}
|
||||
|
||||
private uint on_atom_eol(uint state, uint event, void *user) {
|
||||
// clean up final atom
|
||||
save_string_parameter();
|
||||
save_string_parameter(false);
|
||||
|
||||
return flush_params();
|
||||
}
|
||||
|
|
@ -565,7 +668,7 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
|
||||
// DQUOTE ends quoted string and return to parsing atoms
|
||||
if (ch == '\"') {
|
||||
save_string_parameter();
|
||||
save_string_parameter(true);
|
||||
|
||||
return State.START_PARAM;
|
||||
}
|
||||
|
|
@ -622,7 +725,7 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
if (ch != ' ')
|
||||
return on_partial_body_atom_char(State.PARTIAL_BODY_ATOM, event, user);
|
||||
|
||||
save_string_parameter();
|
||||
save_string_parameter(false);
|
||||
|
||||
return State.START_PARAM;
|
||||
}
|
||||
|
|
@ -707,9 +810,5 @@ public class Geary.Imap.Deserializer : BaseObject {
|
|||
|
||||
return State.FAILED;
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return "%s/%s".printf(fsm.to_string(), get_mode().to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,771 +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 class Geary.Imap.Mailbox : Geary.SmartReference {
|
||||
private class MailboxOperation : Nonblocking.BatchOperation {
|
||||
public SelectedContext context;
|
||||
public Command cmd;
|
||||
|
||||
public MailboxOperation(SelectedContext context, Command cmd) {
|
||||
this.context = context;
|
||||
this.cmd = cmd;
|
||||
}
|
||||
|
||||
public override async Object? execute_async(Cancellable? cancellable) throws Error {
|
||||
return yield context.session.send_command_async(cmd, cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
public string name { get { return context.name; } }
|
||||
public int exists { get { return context.exists; } }
|
||||
public int recent { get { return context.recent; } }
|
||||
public bool is_readonly { get { return context.is_readonly; } }
|
||||
public UIDValidity? uid_validity { get { return context.uid_validity; } }
|
||||
public UID? uid_next { get { return context.uid_next; } }
|
||||
|
||||
private SelectedContext context;
|
||||
private Geary.FolderPath folder_path;
|
||||
|
||||
public signal void exists_altered(int old_exists, int new_exists);
|
||||
|
||||
public signal void recent_altered(int recent);
|
||||
|
||||
public signal void flags_altered(MailboxAttributes flags);
|
||||
|
||||
public signal void expunged(MessageNumber msg_num, int total);
|
||||
|
||||
public signal void closed();
|
||||
|
||||
public signal void disconnected(Geary.Folder.CloseReason reason);
|
||||
|
||||
internal Mailbox(SelectedContext context, Geary.FolderPath folder_path) {
|
||||
base (context);
|
||||
|
||||
this.context = context;
|
||||
this.folder_path = folder_path;
|
||||
|
||||
context.closed.connect(on_closed);
|
||||
context.disconnected.connect(on_disconnected);
|
||||
context.exists_altered.connect(on_exists_altered);
|
||||
context.expunged.connect(on_expunged);
|
||||
context.flags_altered.connect(on_flags_altered);
|
||||
context.recent_altered.connect(on_recent_altered);
|
||||
}
|
||||
|
||||
~Mailbox() {
|
||||
context.closed.disconnect(on_closed);
|
||||
context.disconnected.disconnect(on_disconnected);
|
||||
context.exists_altered.disconnect(on_exists_altered);
|
||||
context.expunged.disconnect(on_expunged);
|
||||
context.flags_altered.disconnect(on_flags_altered);
|
||||
context.recent_altered.disconnect(on_recent_altered);
|
||||
}
|
||||
|
||||
// This helper function is tightly tied to list_set_async(). It assumes that if a new Email
|
||||
// must be created from the FETCH results, a UID is available in the results, either because it
|
||||
// was queried for or because UID addressing was used. It adds the new email to the msgs list
|
||||
// and maps it into the positional map. fields_to_fetch_data_types() is a key part of this
|
||||
// arrangement.
|
||||
private Geary.Email accumulate_email(FetchResults results, Gee.List<Email> msgs,
|
||||
Gee.HashMap<int, Email> pos_map) {
|
||||
// TODO: It's always assumed that the FetchResults will have a UID (due to UID addressing
|
||||
// or because it was requested as part of the FETCH command); however; some servers
|
||||
// (i.e. Dovecot) may split up their FetchResults to multiple lines. If the UID comes in
|
||||
// first, no problem here, otherwise this scheme will fail
|
||||
UID? uid = results.get_data(FetchDataType.UID) as UID;
|
||||
assert(uid != null);
|
||||
|
||||
Geary.Email email = new Geary.Email(results.msg_num,
|
||||
new Geary.Imap.EmailIdentifier(uid, folder_path));
|
||||
msgs.add(email);
|
||||
pos_map.set(email.position, email);
|
||||
|
||||
return email;
|
||||
}
|
||||
|
||||
public async Gee.List<Geary.Email>? list_set_async(MessageSet msg_set, Geary.Email.Field fields,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
if (context.is_closed())
|
||||
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
|
||||
|
||||
if (fields == Geary.Email.Field.NONE)
|
||||
throw new EngineError.BAD_PARAMETERS("No email fields specified");
|
||||
|
||||
Nonblocking.Batch batch = new Nonblocking.Batch();
|
||||
|
||||
Gee.List<FetchDataType> data_type_list = new Gee.ArrayList<FetchDataType>();
|
||||
Gee.List<FetchBodyDataType> body_data_type_list = new Gee.ArrayList<FetchBodyDataType>();
|
||||
fields_to_fetch_data_types(msg_set.is_uid, fields, data_type_list, body_data_type_list);
|
||||
|
||||
// if nothing else, should always fetch the UID, which is gotten via data_type_list
|
||||
// (necessary to create the EmailIdentifier, also provides mappings of position -> UID)
|
||||
// *unless* MessageSet is UID addressing
|
||||
int plain_id = Nonblocking.Batch.INVALID_ID;
|
||||
if (data_type_list.size > 0 || body_data_type_list.size > 0) {
|
||||
FetchCommand fetch_cmd = new FetchCommand.from_collection(msg_set, data_type_list,
|
||||
body_data_type_list);
|
||||
plain_id = batch.add(new MailboxOperation(context, fetch_cmd));
|
||||
}
|
||||
|
||||
int body_id = Nonblocking.Batch.INVALID_ID;
|
||||
if (fields.require(Geary.Email.Field.BODY)) {
|
||||
// Fetch the body.
|
||||
Gee.List<FetchBodyDataType> types = new Gee.ArrayList<FetchBodyDataType>();
|
||||
types.add(new FetchBodyDataType.peek(
|
||||
FetchBodyDataType.SectionPart.TEXT, null, -1, -1, null));
|
||||
FetchCommand fetch_body = new FetchCommand(msg_set, null, types);
|
||||
|
||||
body_id = batch.add(new MailboxOperation(context, fetch_body));
|
||||
}
|
||||
|
||||
int preview_id = Nonblocking.Batch.INVALID_ID;
|
||||
int preview_charset_id = Nonblocking.Batch.INVALID_ID;
|
||||
if (fields.require(Geary.Email.Field.PREVIEW)) {
|
||||
// Preview text.
|
||||
FetchBodyDataType fetch_preview = new FetchBodyDataType.peek(FetchBodyDataType.SectionPart.NONE,
|
||||
{ 1 }, 0, Geary.Email.MAX_PREVIEW_BYTES, null);
|
||||
Gee.List<FetchBodyDataType> list = new Gee.ArrayList<FetchBodyDataType>();
|
||||
list.add(fetch_preview);
|
||||
|
||||
FetchCommand preview_cmd = new FetchCommand(msg_set, null, list);
|
||||
|
||||
preview_id = batch.add(new MailboxOperation(context, preview_cmd));
|
||||
|
||||
// Preview character set.
|
||||
FetchBodyDataType fetch_preview_charset = new FetchBodyDataType.peek(
|
||||
FetchBodyDataType.SectionPart.MIME,
|
||||
{ 1 }, -1, -1, null);
|
||||
Gee.List<FetchBodyDataType> list_charset = new Gee.ArrayList<FetchBodyDataType>();
|
||||
list_charset.add(fetch_preview_charset);
|
||||
|
||||
FetchCommand preview_charset_cmd = new FetchCommand(msg_set, null, list_charset);
|
||||
|
||||
preview_charset_id = batch.add(new MailboxOperation(context, preview_charset_cmd));
|
||||
}
|
||||
|
||||
int properties_id = Nonblocking.Batch.INVALID_ID;
|
||||
if (fields.is_any_set(Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS)) {
|
||||
// Properties and flags.
|
||||
Gee.List<FetchDataType> properties_data_types_list = new Gee.ArrayList<FetchDataType>();
|
||||
|
||||
if (fields.require(Geary.Email.Field.PROPERTIES)) {
|
||||
properties_data_types_list.add(FetchDataType.INTERNALDATE);
|
||||
properties_data_types_list.add(FetchDataType.RFC822_SIZE);
|
||||
}
|
||||
|
||||
if (fields.require(Geary.Email.Field.FLAGS))
|
||||
properties_data_types_list.add(FetchDataType.FLAGS);
|
||||
|
||||
FetchCommand properties_cmd = new FetchCommand.from_collection(msg_set,
|
||||
properties_data_types_list, null);
|
||||
|
||||
properties_id = batch.add(new MailboxOperation(context, properties_cmd));
|
||||
}
|
||||
|
||||
yield batch.execute_all_async(cancellable);
|
||||
|
||||
// Keep list of generated messages (which are returned) and a map of the messages according
|
||||
// to their position addressing (which is built up as results are processed)
|
||||
Gee.List<Geary.Email> msgs = new Gee.ArrayList<Geary.Email>();
|
||||
Gee.HashMap<int, Geary.Email> pos_map = new Gee.HashMap<int, Geary.Email>();
|
||||
|
||||
// process "plain" fetch results (i.e. simple IMAP data)
|
||||
if (plain_id != Nonblocking.Batch.INVALID_ID) {
|
||||
MailboxOperation plain_op = (MailboxOperation) batch.get_operation(plain_id);
|
||||
CommandResponse plain_resp = (CommandResponse) batch.get_result(plain_id);
|
||||
|
||||
if (plain_resp.status_response.status != Status.OK) {
|
||||
throw new ImapError.SERVER_ERROR("Server error for %s: %s", plain_op.cmd.to_string(),
|
||||
plain_resp.to_string());
|
||||
}
|
||||
|
||||
FetchResults[] plain_results = FetchResults.decode(plain_resp);
|
||||
foreach (FetchResults plain_res in plain_results) {
|
||||
// even though msgs and pos_map are empty before this loop, it's possible the server
|
||||
// will send back multiple FetchResults for the same message, so always merge results
|
||||
// whenever possible
|
||||
Geary.Email? email = pos_map.get(plain_res.msg_num);
|
||||
if (email == null)
|
||||
email = accumulate_email(plain_res, msgs, pos_map);
|
||||
|
||||
fetch_results_to_email(plain_res, fields, email);
|
||||
}
|
||||
}
|
||||
|
||||
// Process body results.
|
||||
if (body_id != Nonblocking.Batch.INVALID_ID) {
|
||||
MailboxOperation body_op = (MailboxOperation) batch.get_operation(body_id);
|
||||
CommandResponse body_resp = (CommandResponse) batch.get_result(body_id);
|
||||
|
||||
if (body_resp.status_response.status != Status.OK) {
|
||||
throw new ImapError.SERVER_ERROR("Server error for %s: %s",
|
||||
body_op.cmd.to_string(), body_resp.to_string());
|
||||
}
|
||||
|
||||
FetchResults[] body_results = FetchResults.decode(body_resp);
|
||||
foreach (FetchResults body_res in body_results) {
|
||||
Geary.Email? body_email = pos_map.get(body_res.msg_num);
|
||||
if (body_email == null)
|
||||
body_email = accumulate_email(body_res, msgs, pos_map);
|
||||
|
||||
body_email.set_message_body(new Geary.RFC822.Text(body_res.get_body_data().get(0)));
|
||||
}
|
||||
}
|
||||
|
||||
// Process properties results.
|
||||
if (properties_id != Nonblocking.Batch.INVALID_ID) {
|
||||
MailboxOperation properties_op = (MailboxOperation) batch.get_operation(properties_id);
|
||||
CommandResponse properties_resp = (CommandResponse) batch.get_result(properties_id);
|
||||
|
||||
if (properties_resp.status_response.status != Status.OK) {
|
||||
throw new ImapError.SERVER_ERROR("Server error for %s: %s",
|
||||
properties_op.cmd.to_string(), properties_resp.to_string());
|
||||
}
|
||||
|
||||
FetchResults[] properties_results = FetchResults.decode(properties_resp);
|
||||
foreach (FetchResults properties_res in properties_results) {
|
||||
Geary.Email? properties_email = pos_map.get(properties_res.msg_num);
|
||||
if (properties_email == null)
|
||||
properties_email = accumulate_email(properties_res, msgs, pos_map);
|
||||
|
||||
fetch_results_to_email(properties_res,
|
||||
fields & (Geary.Email.Field.PROPERTIES | Geary.Email.Field.FLAGS), properties_email);
|
||||
}
|
||||
}
|
||||
|
||||
// process preview FETCH results
|
||||
if (preview_id != Nonblocking.Batch.INVALID_ID &&
|
||||
preview_charset_id != Nonblocking.Batch.INVALID_ID) {
|
||||
|
||||
MailboxOperation preview_op = (MailboxOperation) batch.get_operation(preview_id);
|
||||
CommandResponse preview_resp = (CommandResponse) batch.get_result(preview_id);
|
||||
|
||||
MailboxOperation preview_charset_op = (MailboxOperation)
|
||||
batch.get_operation(preview_charset_id);
|
||||
CommandResponse preview_charset_resp = (CommandResponse)
|
||||
batch.get_result(preview_charset_id);
|
||||
|
||||
if (preview_resp.status_response.status != Status.OK) {
|
||||
throw new ImapError.SERVER_ERROR("Server error for %s: %s", preview_op.cmd.to_string(),
|
||||
preview_resp.to_string());
|
||||
}
|
||||
|
||||
if (preview_charset_resp.status_response.status != Status.OK) {
|
||||
throw new ImapError.SERVER_ERROR("Server error for %s: %s",
|
||||
preview_charset_op.cmd.to_string(), preview_charset_resp.to_string());
|
||||
}
|
||||
|
||||
FetchResults[] preview_results = FetchResults.decode(preview_resp);
|
||||
FetchResults[] preview_header_results = FetchResults.decode(preview_charset_resp);
|
||||
int i = 0;
|
||||
foreach (FetchResults preview_res in preview_results) {
|
||||
Geary.Email? preview_email = pos_map.get(preview_res.msg_num);
|
||||
if (preview_email == null)
|
||||
preview_email = accumulate_email(preview_res, msgs, pos_map);
|
||||
|
||||
preview_email.set_message_preview(new RFC822.PreviewText.with_header(
|
||||
preview_res.get_body_data()[0], preview_header_results[i].get_body_data()[0]));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
return (msgs.size > 0) ? msgs : null;
|
||||
}
|
||||
|
||||
private void on_closed() {
|
||||
closed();
|
||||
}
|
||||
|
||||
private void on_disconnected(Geary.Folder.CloseReason reason) {
|
||||
disconnected(reason);
|
||||
}
|
||||
|
||||
private void on_exists_altered(int old_exists, int new_exists) {
|
||||
exists_altered(old_exists, new_exists);
|
||||
}
|
||||
|
||||
private void on_recent_altered(int recent) {
|
||||
recent_altered(recent);
|
||||
}
|
||||
|
||||
private void on_expunged(MessageNumber msg_num, int total) {
|
||||
expunged(msg_num, total);
|
||||
}
|
||||
|
||||
private void on_flags_altered(MailboxAttributes flags) {
|
||||
flags_altered(flags);
|
||||
}
|
||||
|
||||
private void fields_to_fetch_data_types(bool is_uid, Geary.Email.Field fields,
|
||||
Gee.List<FetchDataType> data_types_list, Gee.List<FetchBodyDataType> body_data_types_list) {
|
||||
// always fetch UID because it's needed for EmailIdentifier UNLESS UID addressing is being
|
||||
// used, in which case UID will return with the response
|
||||
if (!is_uid)
|
||||
data_types_list.add(FetchDataType.UID);
|
||||
|
||||
// pack all the needed headers into a single FetchBodyDataType
|
||||
string[] field_names = new string[0];
|
||||
|
||||
// The assumption here is that because ENVELOPE is such a common fetch command, the
|
||||
// server will have optimizations for it, whereas if we called for each header in the
|
||||
// envelope separately, the server has to chunk harder parsing the RFC822 header ... have
|
||||
// to add References because IMAP ENVELOPE doesn't return them for some reason (but does
|
||||
// return Message-ID and In-Reply-To)
|
||||
if (fields.is_all_set(Geary.Email.Field.ENVELOPE)) {
|
||||
data_types_list.add(FetchDataType.ENVELOPE);
|
||||
field_names += "References";
|
||||
|
||||
// remove those flags and process any remaining
|
||||
fields = fields.clear(Geary.Email.Field.ENVELOPE);
|
||||
}
|
||||
|
||||
foreach (Geary.Email.Field field in Geary.Email.Field.all()) {
|
||||
switch (fields & field) {
|
||||
case Geary.Email.Field.DATE:
|
||||
field_names += "Date";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.ORIGINATORS:
|
||||
field_names += "From";
|
||||
field_names += "Sender";
|
||||
field_names += "Reply-To";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.RECEIVERS:
|
||||
field_names += "To";
|
||||
field_names += "Cc";
|
||||
field_names += "Bcc";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.REFERENCES:
|
||||
field_names += "References";
|
||||
field_names += "Message-ID";
|
||||
field_names += "In-Reply-To";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.SUBJECT:
|
||||
field_names += "Subject";
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.HEADER:
|
||||
data_types_list.add(FetchDataType.RFC822_HEADER);
|
||||
break;
|
||||
|
||||
case Geary.Email.Field.BODY:
|
||||
case Geary.Email.Field.PROPERTIES:
|
||||
case Geary.Email.Field.FLAGS:
|
||||
case Geary.Email.Field.NONE:
|
||||
case Geary.Email.Field.PREVIEW:
|
||||
// not set (or, for body previews and properties, fetched separately)
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
if (field_names.length > 0) {
|
||||
body_data_types_list.add(new FetchBodyDataType.peek(
|
||||
FetchBodyDataType.SectionPart.HEADER_FIELDS, null, -1, -1, field_names));
|
||||
}
|
||||
}
|
||||
|
||||
private static void fetch_results_to_email(FetchResults res, Geary.Email.Field fields,
|
||||
Geary.Email email) throws Error {
|
||||
Geary.Imap.MessageFlags? flags = null;
|
||||
|
||||
// accumulate these to submit Imap.EmailProperties all at once
|
||||
InternalDate? internaldate = null;
|
||||
RFC822.Size? rfc822_size = null;
|
||||
|
||||
// accumulate these to submit References all at once
|
||||
RFC822.MessageID? message_id = null;
|
||||
RFC822.MessageID? in_reply_to = null;
|
||||
RFC822.MessageIDList? references = null;
|
||||
|
||||
foreach (FetchDataType data_type in res.get_all_types()) {
|
||||
MessageData? data = res.get_data(data_type);
|
||||
if (data == null)
|
||||
continue;
|
||||
|
||||
switch (data_type) {
|
||||
case FetchDataType.ENVELOPE:
|
||||
Envelope envelope = (Envelope) data;
|
||||
|
||||
if ((fields & Geary.Email.Field.DATE) != 0)
|
||||
email.set_send_date(envelope.sent);
|
||||
|
||||
if ((fields & Geary.Email.Field.SUBJECT) != 0)
|
||||
email.set_message_subject(envelope.subject);
|
||||
|
||||
if ((fields & Geary.Email.Field.ORIGINATORS) != 0)
|
||||
email.set_originators(envelope.from, envelope.sender, envelope.reply_to);
|
||||
|
||||
if ((fields & Geary.Email.Field.RECEIVERS) != 0)
|
||||
email.set_receivers(envelope.to, envelope.cc, envelope.bcc);
|
||||
|
||||
if ((fields & Geary.Email.Field.REFERENCES) != 0) {
|
||||
message_id = envelope.message_id;
|
||||
in_reply_to = envelope.in_reply_to;
|
||||
}
|
||||
break;
|
||||
|
||||
case FetchDataType.RFC822_HEADER:
|
||||
email.set_message_header((RFC822.Header) data);
|
||||
break;
|
||||
|
||||
case FetchDataType.RFC822_TEXT:
|
||||
email.set_message_body((RFC822.Text) data);
|
||||
break;
|
||||
|
||||
case FetchDataType.RFC822_SIZE:
|
||||
rfc822_size = (RFC822.Size) data;
|
||||
break;
|
||||
|
||||
case FetchDataType.FLAGS:
|
||||
flags = (MessageFlags) data;
|
||||
break;
|
||||
|
||||
case FetchDataType.INTERNALDATE:
|
||||
internaldate = (InternalDate) data;
|
||||
break;
|
||||
|
||||
default:
|
||||
// everything else dropped on the floor (not applicable to Geary.Email)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Only set PROPERTIES if all have been found
|
||||
if (internaldate != null && rfc822_size != null)
|
||||
email.set_email_properties(new Geary.Imap.EmailProperties(internaldate, rfc822_size));
|
||||
|
||||
if (flags != null)
|
||||
email.set_flags(new Geary.Imap.EmailFlags(flags));
|
||||
|
||||
// fields_to_fetch_data_types() will always generate a single FetchBodyDataType for all
|
||||
// the header fields it needs
|
||||
Gee.List<Memory.AbstractBuffer> body_data = res.get_body_data();
|
||||
if (body_data.size > 0) {
|
||||
assert(body_data.size == 1);
|
||||
RFC822.Header headers = new RFC822.Header(body_data[0]);
|
||||
|
||||
// DATE
|
||||
if (!email.fields.is_all_set(Geary.Email.Field.DATE) && fields.require(Geary.Email.Field.DATE)) {
|
||||
string? value = headers.get_header("Date");
|
||||
email.set_send_date(!String.is_empty(value) ? new RFC822.Date(value) : null);
|
||||
}
|
||||
|
||||
// ORIGINATORS
|
||||
if (!email.fields.is_all_set(Geary.Email.Field.ORIGINATORS) && fields.require(Geary.Email.Field.ORIGINATORS)) {
|
||||
RFC822.MailboxAddresses? from = null;
|
||||
RFC822.MailboxAddresses? sender = null;
|
||||
RFC822.MailboxAddresses? reply_to = null;
|
||||
|
||||
string? value = headers.get_header("From");
|
||||
if (!String.is_empty(value))
|
||||
from = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
value = headers.get_header("Sender");
|
||||
if (!String.is_empty(value))
|
||||
sender = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
value = headers.get_header("Reply-To");
|
||||
if (!String.is_empty(value))
|
||||
reply_to = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
email.set_originators(from, sender, reply_to);
|
||||
}
|
||||
|
||||
// RECEIVERS
|
||||
if (!email.fields.is_all_set(Geary.Email.Field.RECEIVERS) && fields.require(Geary.Email.Field.RECEIVERS)) {
|
||||
RFC822.MailboxAddresses? to = null;
|
||||
RFC822.MailboxAddresses? cc = null;
|
||||
RFC822.MailboxAddresses? bcc = null;
|
||||
|
||||
string? value = headers.get_header("To");
|
||||
if (!String.is_empty(value))
|
||||
to = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
value = headers.get_header("Cc");
|
||||
if (!String.is_empty(value))
|
||||
cc = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
value = headers.get_header("Bcc");
|
||||
if (!String.is_empty(value))
|
||||
bcc = new RFC822.MailboxAddresses.from_rfc822_string(value);
|
||||
|
||||
email.set_receivers(to, cc, bcc);
|
||||
}
|
||||
|
||||
// REFERENCES
|
||||
// (Note that it's possible the request used an IMAP ENVELOPE, in which case only the
|
||||
// References header will be present if REFERENCES were required, which is why
|
||||
// REFERENCES is set at the bottom of the method, when all information has been gathered
|
||||
if (message_id == null) {
|
||||
string? value = headers.get_header("Message-ID");
|
||||
if (!String.is_empty(value))
|
||||
message_id = new RFC822.MessageID(value);
|
||||
}
|
||||
|
||||
if (in_reply_to == null) {
|
||||
string? value = headers.get_header("In-Reply-To");
|
||||
if (!String.is_empty(value))
|
||||
in_reply_to = new RFC822.MessageID(value);
|
||||
}
|
||||
|
||||
if (references == null) {
|
||||
string? value = headers.get_header("References");
|
||||
if (!String.is_empty(value))
|
||||
references = new RFC822.MessageIDList.from_rfc822_string(value);
|
||||
}
|
||||
|
||||
// SUBJECT
|
||||
if (!email.fields.is_all_set(Geary.Email.Field.SUBJECT) && fields.require(Geary.Email.Field.SUBJECT)) {
|
||||
string? value = headers.get_header("Subject");
|
||||
email.set_message_subject(!String.is_empty(value) ? new RFC822.Subject.decode(value) : null);
|
||||
}
|
||||
}
|
||||
|
||||
if (fields.require(Geary.Email.Field.REFERENCES))
|
||||
email.set_full_references(message_id, in_reply_to, references);
|
||||
}
|
||||
|
||||
public async Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> mark_email_async(
|
||||
MessageSet to_mark, Gee.List<MessageFlag>? flags_to_add, Gee.List<MessageFlag>? flags_to_remove,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
|
||||
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> ret =
|
||||
new Gee.HashMap<Geary.EmailIdentifier, Geary.EmailFlags>();
|
||||
|
||||
if (context.is_closed())
|
||||
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
|
||||
|
||||
Nonblocking.Batch batch = new Nonblocking.Batch();
|
||||
int add_flags_id = Nonblocking.Batch.INVALID_ID;
|
||||
int remove_flags_id = Nonblocking.Batch.INVALID_ID;
|
||||
|
||||
if (flags_to_add != null && flags_to_add.size > 0)
|
||||
add_flags_id = batch.add(new MailboxOperation(context, new StoreCommand(
|
||||
to_mark, flags_to_add, true, false)));
|
||||
|
||||
if (flags_to_remove != null && flags_to_remove.size > 0)
|
||||
remove_flags_id = batch.add(new MailboxOperation(context, new StoreCommand(
|
||||
to_mark, flags_to_remove, false, false)));
|
||||
|
||||
yield batch.execute_all_async(cancellable);
|
||||
|
||||
if (add_flags_id != Nonblocking.Batch.INVALID_ID) {
|
||||
gather_flag_results((MailboxOperation) batch.get_operation(add_flags_id),
|
||||
(CommandResponse) batch.get_result(add_flags_id), ref ret);
|
||||
}
|
||||
|
||||
if (remove_flags_id != Nonblocking.Batch.INVALID_ID) {
|
||||
gather_flag_results((MailboxOperation) batch.get_operation(remove_flags_id),
|
||||
(CommandResponse) batch.get_result(remove_flags_id), ref ret);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Helper function for building results for mark_email_async
|
||||
private void gather_flag_results(MailboxOperation operation, CommandResponse response,
|
||||
ref Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> map) throws Error {
|
||||
|
||||
if (response.status_response == null)
|
||||
throw new ImapError.SERVER_ERROR("Server error. Command: %s No status response. %s",
|
||||
operation.cmd.to_string(), response.to_string());
|
||||
|
||||
if (response.status_response.status != Status.OK)
|
||||
throw new ImapError.SERVER_ERROR("Server error. Command: %s Response: %s Error: %s",
|
||||
operation.cmd.to_string(), response.to_string(),
|
||||
response.status_response.status.to_string());
|
||||
|
||||
FetchResults[] results = FetchResults.decode(response);
|
||||
foreach (FetchResults res in results) {
|
||||
UID? uid = res.get_data(FetchDataType.UID) as UID;
|
||||
assert(uid != null);
|
||||
|
||||
Geary.Imap.MessageFlags? msg_flags = res.get_data(FetchDataType.FLAGS) as MessageFlags;
|
||||
if (msg_flags != null) {
|
||||
Geary.Imap.EmailFlags email_flags = new Geary.Imap.EmailFlags(msg_flags);
|
||||
|
||||
map.set(new Geary.Imap.EmailIdentifier(uid, folder_path) , email_flags);
|
||||
} else {
|
||||
debug("No flags returned");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void copy_email_async(MessageSet msg_set, Geary.FolderPath destination,
|
||||
Cancellable? cancellable = null) throws Error {
|
||||
|
||||
if (context.is_closed())
|
||||
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
|
||||
|
||||
yield context.session.send_command_async(
|
||||
new CopyCommand(msg_set, new Geary.Imap.MailboxParameter(destination.to_string())),
|
||||
cancellable);
|
||||
}
|
||||
|
||||
public async void expunge_email_async(MessageSet? msg_set, Cancellable? cancellable = null) throws Error {
|
||||
if (context.is_closed())
|
||||
throw new ImapError.NOT_SELECTED("Mailbox %s closed", name);
|
||||
|
||||
// Response automatically handled by unsolicited server data. ... use UID EXPUNGE whenever
|
||||
// possible
|
||||
if (msg_set == null || !context.session.get_capabilities().has_capability("uidplus"))
|
||||
yield context.session.send_command_async(new ExpungeCommand(), cancellable);
|
||||
else
|
||||
yield context.session.send_command_async(new ExpungeCommand.uid(msg_set), cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
// A SelectedContext is a ReferenceSemantics object wrapping a ClientSession that is in a SELECTED
|
||||
// or EXAMINED state (i.e. it has "cd'd" into a folder). Multiple Mailbox objects may be created
|
||||
// that refer to this SelectedContext. When they're all destroyed, the session is returned to
|
||||
// the AUTHORIZED state by the ClientSessionManager.
|
||||
//
|
||||
// This means there is some duplication between the SelectedContext and the Mailbox. In particular
|
||||
// signals must be reflected to ensure order-of-operation is preserved (i.e. when the ClientSession
|
||||
// "unsolicited-exists" signal is fired, a signal subscriber may then query SelectedContext for
|
||||
// its exists count before it has received the notification).
|
||||
//
|
||||
// All this fancy stepping should not be exposed to a user of the IMAP portion of Geary, who should
|
||||
// only see Geary.Imap.Mailbox, nor should it be exposed to the user of Geary.Engine, where all this
|
||||
// should only be exposed via Geary.Folder.
|
||||
private class Geary.Imap.SelectedContext : BaseObject, Geary.ReferenceSemantics {
|
||||
public ClientSession? session { get; private set; }
|
||||
|
||||
public string name { get; protected set; }
|
||||
public int exists { get; protected set; }
|
||||
public int recent { get; protected set; }
|
||||
public bool is_readonly { get; protected set; }
|
||||
public UIDValidity? uid_validity { get; protected set; }
|
||||
public UID? uid_next { get; protected set; }
|
||||
|
||||
protected int manual_ref_count { get; protected set; }
|
||||
|
||||
public signal void exists_altered(int old_exists, int new_exists);
|
||||
|
||||
public signal void recent_altered(int recent);
|
||||
|
||||
public signal void expunged(MessageNumber msg_num, int total);
|
||||
|
||||
public signal void flags_altered(MailboxAttributes flags);
|
||||
|
||||
public signal void closed();
|
||||
|
||||
public signal void disconnected(Geary.Folder.CloseReason reason);
|
||||
|
||||
public signal void login_failed();
|
||||
|
||||
internal SelectedContext(ClientSession session, SelectExamineResults results) {
|
||||
this.session = session;
|
||||
|
||||
name = session.get_current_mailbox();
|
||||
is_readonly = results.readonly;
|
||||
exists = results.exists;
|
||||
recent = results.recent;
|
||||
uid_validity = results.uid_validity;
|
||||
uid_next = results.uid_next;
|
||||
|
||||
session.current_mailbox_changed.connect(on_session_mailbox_changed);
|
||||
session.unsolicited_exists.connect(on_unsolicited_exists);
|
||||
session.unsolicited_recent.connect(on_unsolicited_recent);
|
||||
session.unsolicited_expunged.connect(on_unsolicited_expunged);
|
||||
session.unsolicited_flags.connect(on_unsolicited_flags);
|
||||
session.logged_out.connect(on_session_logged_out);
|
||||
session.disconnected.connect(on_session_disconnected);
|
||||
session.login_failed.connect(on_login_failed);
|
||||
}
|
||||
|
||||
~SelectedContext() {
|
||||
if (session != null) {
|
||||
session.current_mailbox_changed.disconnect(on_session_mailbox_changed);
|
||||
session.unsolicited_exists.disconnect(on_unsolicited_exists);
|
||||
session.unsolicited_recent.disconnect(on_unsolicited_recent);
|
||||
session.unsolicited_recent.disconnect(on_unsolicited_recent);
|
||||
session.unsolicited_expunged.disconnect(on_unsolicited_expunged);
|
||||
session.logged_out.disconnect(on_session_logged_out);
|
||||
session.disconnected.disconnect(on_session_disconnected);
|
||||
session.login_failed.disconnect(on_login_failed);
|
||||
}
|
||||
}
|
||||
|
||||
public bool is_closed() {
|
||||
return (session == null);
|
||||
}
|
||||
|
||||
private void on_unsolicited_exists(int exists) {
|
||||
// only report if changed; note that on_solicited_expunged also fires this signal
|
||||
if (this.exists == exists)
|
||||
return;
|
||||
|
||||
int old_exists = this.exists;
|
||||
this.exists = exists;
|
||||
|
||||
exists_altered(old_exists, this.exists);
|
||||
}
|
||||
|
||||
private void on_unsolicited_recent(int recent) {
|
||||
this.recent = recent;
|
||||
|
||||
recent_altered(recent);
|
||||
}
|
||||
|
||||
private void on_unsolicited_expunged(MessageNumber msg_num) {
|
||||
assert(exists > 0);
|
||||
|
||||
// update exists count along with reporting the deletion
|
||||
int old_exists = exists;
|
||||
exists--;
|
||||
|
||||
exists_altered(old_exists, exists);
|
||||
expunged(msg_num, exists);
|
||||
}
|
||||
|
||||
private void on_unsolicited_flags(MailboxAttributes flags) {
|
||||
flags_altered(flags);
|
||||
}
|
||||
|
||||
private void on_session_mailbox_changed(string? old_mailbox, string? new_mailbox, bool readonly) {
|
||||
session = null;
|
||||
closed();
|
||||
}
|
||||
|
||||
private void on_session_logged_out() {
|
||||
session = null;
|
||||
disconnected(Geary.Folder.CloseReason.REMOTE_CLOSE);
|
||||
}
|
||||
|
||||
private void on_session_disconnected(ClientSession.DisconnectReason reason) {
|
||||
if (session == null)
|
||||
return;
|
||||
|
||||
session = null;
|
||||
|
||||
switch (reason) {
|
||||
case ClientSession.DisconnectReason.LOCAL_CLOSE:
|
||||
case ClientSession.DisconnectReason.REMOTE_CLOSE:
|
||||
disconnected(Geary.Folder.CloseReason.REMOTE_CLOSE);
|
||||
break;
|
||||
|
||||
case ClientSession.DisconnectReason.LOCAL_ERROR:
|
||||
case ClientSession.DisconnectReason.REMOTE_ERROR:
|
||||
disconnected(Geary.Folder.CloseReason.REMOTE_ERROR);
|
||||
break;
|
||||
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
private void on_login_failed() {
|
||||
login_failed();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1,10 +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 interface Geary.Imap.Serializable {
|
||||
public abstract async void serialize(Serializer ser) throws Error;
|
||||
}
|
||||
|
||||
|
|
@ -20,13 +20,15 @@
|
|||
*/
|
||||
|
||||
public class Geary.Imap.Serializer : BaseObject {
|
||||
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");
|
||||
|
||||
public Serializer(OutputStream outs) {
|
||||
public Serializer(string identifier, OutputStream outs) {
|
||||
this.identifier = identifier;
|
||||
this.outs = outs;
|
||||
|
||||
couts = new ConverterOutputStream(outs, midstream);
|
||||
|
|
@ -44,25 +46,6 @@ public class Geary.Imap.Serializer : BaseObject {
|
|||
douts.put_byte(ch, null);
|
||||
}
|
||||
|
||||
public void push_string(string str) throws Error {
|
||||
// see if need to convert to quoted string, only emitting it if required
|
||||
switch (DataFormat.is_quoting_required(str)) {
|
||||
case DataFormat.Quoting.OPTIONAL:
|
||||
douts.put_string(str);
|
||||
break;
|
||||
|
||||
case DataFormat.Quoting.REQUIRED:
|
||||
bool required = push_quoted_string(str);
|
||||
assert(required);
|
||||
break;
|
||||
|
||||
case DataFormat.Quoting.UNALLOWED:
|
||||
default:
|
||||
// TODO: Not handled currently
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes the string to the IMAP server with quoting applied whether required or not. Returns
|
||||
* true if quoting was required.
|
||||
|
|
@ -119,7 +102,7 @@ public class Geary.Imap.Serializer : BaseObject {
|
|||
for (size_t ctr = 0; ctr < length; ctr++)
|
||||
builder.append_c((char) mouts.get_data()[ctr]);
|
||||
|
||||
Logging.debug(Logging.Flag.SERIALIZER, "COMMIT:\n%s", builder.str);
|
||||
Logging.debug(Logging.Flag.SERIALIZER, "[%s] send %s", to_string(), builder.str.strip());
|
||||
}
|
||||
|
||||
ssize_t index = 0;
|
||||
|
|
@ -139,5 +122,9 @@ public class Geary.Imap.Serializer : BaseObject {
|
|||
yield couts.flush_async(priority, cancellable);
|
||||
yield outs.flush_async(priority, cancellable);
|
||||
}
|
||||
|
||||
public string to_string() {
|
||||
return "ser:%s".printf(identifier);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,47 +5,56 @@
|
|||
*/
|
||||
|
||||
/**
|
||||
* A Nonblocking.BatchOperation is an abstract base class used by Nonblocking.Batch. It represents
|
||||
* a single task of asynchronous work. Nonblocking.Batch will execute it one time only.
|
||||
* An abstract base class representing a single task of asynchronous work.
|
||||
*/
|
||||
public abstract class Geary.Nonblocking.BatchOperation : BaseObject {
|
||||
/**
|
||||
* Called by {@link Nonblocking.Batch} when execution should start.
|
||||
*
|
||||
* This will be called once and only once by Nonblocking.Batch.
|
||||
*
|
||||
* @return An optional Object. This will be referenced and stored by Nonblocking.Batch.
|
||||
*/
|
||||
public abstract async Object? execute_async(Cancellable? cancellable) throws Error;
|
||||
}
|
||||
|
||||
/**
|
||||
* NonblockingBatch allows for multiple asynchronous tasks to be executed in parallel and for their
|
||||
* results to be examined after all have completed. It's designed specifically with Vala's async
|
||||
* keyword in mind.
|
||||
* Allows for multiple asynchronous tasks to be executed in parallel and for their results to be
|
||||
* examined after all have completed.
|
||||
*
|
||||
* Nonblocking.Batch is designed specifically with Vala's async keyword in mind.
|
||||
* Although the yield keyword allows for async tasks to execute, it only allows them to performed
|
||||
* in serial. In a loop, for example, the next task in the loop won't execute until the current
|
||||
* one has completed. The thread of execution won't block waiting for it, but this can be
|
||||
* suboptiminal and certain cases.
|
||||
*
|
||||
* NonblockingBatch allows for multiple async tasks to be gathered (via the add() method) into a
|
||||
* single batch. Each task must subclass from NonblockingBatchOperation. It's expected that the
|
||||
* subclass will maintain state particular to the operation, although NonblockingBatch does gather
|
||||
* two types of a results the task may generate: a result object (which descends from Object) or
|
||||
* a thrown exception. Other results should be stored by the subclass.
|
||||
* Nonblocking.Batch allows for multiple async tasks to be gathered (via the {@link add} method)
|
||||
* into a single batch. Each task must subclass from {@link BatchOperation}. It's expected that
|
||||
* the subclass will maintain state particular to the operation, although Nonblocking.Batch does
|
||||
* gather two types of a results the task may generate: a result object (which descends from Object)
|
||||
* or a thrown exception. Other results should be stored by the subclass.
|
||||
*
|
||||
* To use, create a NonblockingBatch and populate it via the add() method. When all
|
||||
* NonblockingBatchOperations have been added, call execute_all_async(). NonblockingBatch will fire off
|
||||
* all at once and only complete execute_all_async() when all of them have finished. As mentioned
|
||||
* earlier, it's also gather their returned objects and thrown exceptions while they run. See
|
||||
* get_result() and throw_first_exception() for more information.
|
||||
* To use, create a Nonblocking.Batch and populate it via the add() method. When all
|
||||
* {@link BatchOperation}s have been added, call {@link execute_all_async}. NonblockingBatch will
|
||||
* execute all BatchOperations at once and only complete their execute_all_async when all have
|
||||
* finished. As mentioned earlier, it's also gather their returned objects and thrown exceptions
|
||||
* while they run. See {@link get_result} and {@link throw_first_exception} for more information.
|
||||
*
|
||||
* The caller will want to call *either* get_result() or throw_first_exception() to ensure that
|
||||
* The caller will want to call either get_result or throw_first_exception to ensure that
|
||||
* errors are propagated. It's not necessary to call both.
|
||||
*
|
||||
* After execute_all_async() has completed, the results may be examined. The NonblockingBatch object
|
||||
* can *not* be reused.
|
||||
* After execute_all_async has completed, the results may be examined. The Nonblocking.Batch
|
||||
* object can ''not'' be reused.
|
||||
*
|
||||
* Currently NonblockingBatch will fire off all operations at once and let them complete. It does
|
||||
* Nonblocking.Batch will fire off all operations at once and let them complete. It does
|
||||
* not attempt to stop the others if one throws exception. Also, there's no algorithm to submit the
|
||||
* operations in smaller chunks (to avoid flooding the thread's MainLoop). These may be added in
|
||||
* the future.
|
||||
*/
|
||||
public class Geary.Nonblocking.Batch : BaseObject {
|
||||
/**
|
||||
* An invalid {@link BatchOperation} identifier.
|
||||
*/
|
||||
public const int INVALID_ID = -1;
|
||||
|
||||
private const int START_ID = 1;
|
||||
|
|
@ -87,15 +96,14 @@ public class Geary.Nonblocking.Batch : BaseObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the number of NonblockingBatchOperations added.
|
||||
* Returns the number of {@link BatchOperation}s added to the batch.
|
||||
*/
|
||||
public int size {
|
||||
get { return contexts.size; }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first exception encountered after completing execute_all_async(). Will be null
|
||||
* before that.
|
||||
* Returns the first exception encountered after completing {@link execute_all_async}.
|
||||
*/
|
||||
public Error? first_exception { get; private set; default = null; }
|
||||
|
||||
|
|
@ -105,26 +113,40 @@ public class Geary.Nonblocking.Batch : BaseObject {
|
|||
private bool locked = false;
|
||||
private int completed_ops = 0;
|
||||
|
||||
/**
|
||||
* Fired when a {@link BatchOperation} is added to the batch.
|
||||
*/
|
||||
public signal void added(Nonblocking.BatchOperation op, int id);
|
||||
|
||||
/**
|
||||
* Fired when batch execution has started.
|
||||
*/
|
||||
public signal void started(int count);
|
||||
|
||||
/**
|
||||
* Fired when a {@link BatchOperation} has completed.
|
||||
*/
|
||||
public signal void operation_completed(Nonblocking.BatchOperation op, Object? returned,
|
||||
Error? threw);
|
||||
|
||||
/**
|
||||
* Fired when all {@link BatchOperation}s have completed.
|
||||
*/
|
||||
public signal void completed(int count, Error? first_error);
|
||||
|
||||
public Batch() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a NonblockingBatchOperation to the batch. INVALID_ID is returned if the batch is
|
||||
* executing or has already executed. Otherwise, returns an ID that can be used to fetch
|
||||
* results of this particular NonblockingBatchOperation after execute_all() completes.
|
||||
* Adds a {@link BatchOperation} for later execution.
|
||||
*
|
||||
* The returned ID is only good for this NonblockingBatch. Since each instance uses the
|
||||
* {@link INVALID_ID} is returned if the batch is executing or has already executed. Otherwise,
|
||||
* returns an ID that can be used to fetch results of this particular BatchOperation after
|
||||
* {@link execute_all_async} completes.
|
||||
*
|
||||
* The returned ID is only good for this {@link Batch}. Since each instance uses the
|
||||
* same algorithm, different instances will likely return the same ID, so they must be
|
||||
* associated with the NonblockingBatch they originated from.
|
||||
* associated with the Batch they originated from.
|
||||
*/
|
||||
public int add(Nonblocking.BatchOperation op) {
|
||||
if (locked) {
|
||||
|
|
@ -142,12 +164,16 @@ public class Geary.Nonblocking.Batch : BaseObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Executes all the NonblockingBatchOperations added to the batch. The supplied Cancellable
|
||||
* will be passed to each operation.
|
||||
* Executes all the {@link BatchOperation}s added to the batch.
|
||||
*
|
||||
* The supplied Cancellable will be passed to each {@link BatchOperation.execute_async}.
|
||||
*
|
||||
* If the batch is executing or already executed, IOError.PENDING will be thrown. If the
|
||||
* Cancellable is already cancelled, IOError.CANCELLED is thrown. Other errors may be thrown
|
||||
* as well; see NonblockingAbstractSemaphore.wait_async().
|
||||
* as well; see {@link AbstractSemaphore.wait_async}.
|
||||
*
|
||||
* Batch will launch each BatchOperation in the order added. Depending on the BatchOperation,
|
||||
* this does not guarantee that they'll complete in any particular order.
|
||||
*
|
||||
* If there are no operations added to the batch, the method quietly exits.
|
||||
*/
|
||||
|
|
@ -167,8 +193,8 @@ public class Geary.Nonblocking.Batch : BaseObject {
|
|||
|
||||
started(contexts.size);
|
||||
|
||||
// although they should technically be able to execute in any order, fire them off in the
|
||||
// order they were submitted; this may hide bugs, but it also makes other bugs reproducible
|
||||
// fire them off in order they were submitted; this may hide bugs, but it also makes other
|
||||
// bugs reproducible
|
||||
int count = 0;
|
||||
for (int id = START_ID; id < next_result_id; id++) {
|
||||
BatchContext? context = contexts.get(id);
|
||||
|
|
@ -184,15 +210,16 @@ public class Geary.Nonblocking.Batch : BaseObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns a Set of IDs for all added NonblockingBatchOperations.
|
||||
* Returns a Set of identifiers for all added {@link BatchOperation}s.
|
||||
*/
|
||||
public Gee.Set<int> get_ids() {
|
||||
return contexts.keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the NonblockingBatchOperation for the supplied ID. Returns null if the ID is invalid
|
||||
* or unknown.
|
||||
* Returns the NonblockingBatchOperation for the supplied identifier.
|
||||
*
|
||||
* @return null if the identifier is invalid or unknown.
|
||||
*/
|
||||
public Nonblocking.BatchOperation? get_operation(int id) {
|
||||
BatchContext? context = contexts.get(id);
|
||||
|
|
@ -201,14 +228,15 @@ public class Geary.Nonblocking.Batch : BaseObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the resulting Object from the operation for the supplied ID. If the ID is invalid
|
||||
* or unknown, or the operation returned null, null is returned.
|
||||
* Returns the resulting Object from the operation for the supplied identifier.
|
||||
*
|
||||
* If the operation threw an exception, it will be thrown here. If all the operations' results
|
||||
* are examined with this method, there is no need to call throw_first_exception().
|
||||
*
|
||||
* If the operation has not completed, IOError.BUSY will be thrown. It *is* legal to query
|
||||
* If the operation has not completed, IOError.BUSY will be thrown. It is legal to query
|
||||
* the result of a completed operation while others are executing.
|
||||
*
|
||||
* @return The resulting Object for the executed {@link BatchOperation}, which may be null.
|
||||
*/
|
||||
public Object? get_result(int id) throws Error {
|
||||
BatchContext? context = contexts.get(id);
|
||||
|
|
@ -225,8 +253,8 @@ public class Geary.Nonblocking.Batch : BaseObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* If no results are examined via get_result(), this method can be used to manually throw the
|
||||
* first seen Error from the operations.
|
||||
* If no results are examined via {@link get_result}, this method can be used to manually throw
|
||||
* the first seen Error from the operations.
|
||||
*/
|
||||
public void throw_first_exception() throws Error {
|
||||
if (first_exception != null)
|
||||
|
|
@ -234,7 +262,7 @@ public class Geary.Nonblocking.Batch : BaseObject {
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns the message if an exception was encountered, null otherwise.
|
||||
* Returns the Error message if an exception was encountered, null otherwise.
|
||||
*/
|
||||
public string? get_first_exception_message() {
|
||||
return (first_exception != null) ? first_exception.message : null;
|
||||
|
|
|
|||
60
src/engine/nonblocking/nonblocking-counting-semaphore.vala
Normal file
60
src/engine/nonblocking/nonblocking-counting-semaphore.vala
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* 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 nonblocking semaphore which allows for any number of tasks to run, but only signalling
|
||||
* completion when all have finished.
|
||||
*
|
||||
* Unlike the other {@link AbstractSemaphore} variants, a task must {@link acquire} before it
|
||||
* can {@link notify}. The number of acquired tasks is kept in the {@link count} property.
|
||||
*/
|
||||
public class Geary.Nonblocking.CountingSemaphore : Geary.Nonblocking.AbstractSemaphore {
|
||||
/**
|
||||
* The number of tasks which have {@link acquire} the semaphore.
|
||||
*/
|
||||
public int count { get; private set; default = 0; }
|
||||
|
||||
public CountingSemaphore(Cancellable? cancellable) {
|
||||
base (true, false, cancellable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by a task to acquire (and, hence, lock) the semaphore.
|
||||
*
|
||||
* @return Number of acquired tasks, including the one that made this call.
|
||||
*/
|
||||
public int acquire() {
|
||||
return ++count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by a task which has previously {@link acquire}d the semaphore.
|
||||
*
|
||||
* When the number of acquired tasks reaches zero, the semaphore is unlocked and all waiting
|
||||
* tasks will resume.
|
||||
*
|
||||
* @see wait_async
|
||||
* @throws NonblockingError.INVALID if called when {@link count} is zero.
|
||||
*/
|
||||
public override void notify() throws Error {
|
||||
if (count == 0)
|
||||
throw new NonblockingError.INVALID("notify() on a zeroed CountingSemaphore");
|
||||
|
||||
if (count-- == 0)
|
||||
base.notify();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for all tasks which have {@link acquire}d this semaphore to release it.
|
||||
*
|
||||
* If no tasks have acquired the semaphore, this call will complete immediately.
|
||||
*/
|
||||
public async override void wait_async(Cancellable? cancellable = null) throws Error {
|
||||
if (count != 0)
|
||||
yield wait_async(cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
14
src/engine/nonblocking/nonblocking-error.vala
Normal file
14
src/engine/nonblocking/nonblocking-error.vala
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
/* 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.
|
||||
*/
|
||||
|
||||
public errordomain NonblockingError {
|
||||
/**
|
||||
* Indicates a call was made when it shouldn't have been; that the primitive was in such a
|
||||
* state that it cannot properly respond or account for the requested change.
|
||||
*/
|
||||
INVALID
|
||||
}
|
||||
|
||||
|
|
@ -4,6 +4,13 @@
|
|||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A task primitive for creating critical sections inside of asynchronous code.
|
||||
*
|
||||
* Like other primitives in {@link Nonblocking}, Mutex is ''not'' designed for a threaded
|
||||
* environment.
|
||||
*/
|
||||
|
||||
public class Geary.Nonblocking.Mutex : BaseObject {
|
||||
public const int INVALID_TOKEN = -1;
|
||||
|
||||
|
|
@ -15,6 +22,21 @@ public class Geary.Nonblocking.Mutex : BaseObject {
|
|||
public Mutex() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the {@link Mutex} has been claimed by a task.
|
||||
*/
|
||||
public bool is_locked() {
|
||||
return locked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Claim (i.e. lock) the {@link Mutex} and begin execution inside a critical section.
|
||||
*
|
||||
* claim_async will block asynchronously waiting for the Mutex to be released, if it's already
|
||||
* claimed.
|
||||
*
|
||||
* @return A token which must be used to {@link release} the Mutex.
|
||||
*/
|
||||
public async int claim_async(Cancellable? cancellable = null) throws Error {
|
||||
for (;;) {
|
||||
if (!locked) {
|
||||
|
|
@ -30,6 +52,14 @@ public class Geary.Nonblocking.Mutex : BaseObject {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Release (i.e. unlock) the {@link Mutex} and end execution inside a critical section.
|
||||
*
|
||||
* The token returned by {@link claim_async} must be supplied as a parameter. It will be
|
||||
* modified by this call so it can't be reused.
|
||||
*
|
||||
* Throws IOError.INVALID_ARGUMENT if the token was not the one returned by claim_async.
|
||||
*/
|
||||
public void release(ref int token) throws Error {
|
||||
if (token != locked_token || token == INVALID_TOKEN)
|
||||
throw new IOError.INVALID_ARGUMENT("Token %d is not the lock token", token);
|
||||
|
|
|
|||
|
|
@ -31,6 +31,14 @@ public bool are_sets_equal<G>(Gee.Set<G> a, Gee.Set<G> b) {
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dest Map with all keys and values in src.
|
||||
*/
|
||||
public void map_set_all<K, V>(Gee.Map<K, V> dest, Gee.Map<K, V> src) {
|
||||
foreach (K key in src.keys)
|
||||
dest.set(key, src.get(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* To be used by a Hashable's to_hash() method.
|
||||
*/
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue