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:
Charles Lindsay 2013-06-14 15:50:20 -07:00
commit bc2146dad5
105 changed files with 5990 additions and 4153 deletions

View file

@ -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);
}

View file

@ -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());
}
/**
*
*/

View file

@ -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);

View file

@ -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()) {

View file

@ -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) {

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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));

View file

@ -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);
}

View file

@ -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);

View file

@ -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());
}

View file

@ -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,

View file

@ -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",

View file

@ -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) {

View file

@ -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;

View file

@ -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;

View file

@ -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);
}
}

View file

@ -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() {

View file

@ -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));
}

View file

@ -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();
}

View 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);
}
}

View 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);
}
}

View file

@ -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;
}
}

View file

@ -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.

View file

@ -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);
}
}

View 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 });
}
}

View 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());
}
}

View 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());
}
}

View 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());
}
}

View file

@ -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());
}
}

View 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);
}
}

View 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);
}
}

View 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());
}
}

View 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);
}
}

View 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);
}
}

View file

@ -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...)

View 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);
}
}

View 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());
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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,
}

View file

@ -14,6 +14,7 @@ internal void init() {
MessageFlag.init();
MailboxAttribute.init();
Tag.init();
}
}

View file

@ -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();
}
}

View file

@ -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:

View file

@ -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;
}
}

View file

@ -0,0 +1,69 @@
/* Copyright 2011-2013 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* 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();
}
}

View file

@ -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;
}
}

View 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;
}
}

View file

@ -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);

View 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);
}
}
}

View 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);
}
}

View 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();
}
}

View file

@ -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 {

View file

@ -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);
}

View file

@ -0,0 +1,25 @@
/* Copyright 2013 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/*
* 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);
}
}

View 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();
}
}

View 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);
}
}

View file

@ -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();
}
}

View 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());
}
}

View 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;
}
}

View 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();
}

View 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);
}
}

View file

@ -0,0 +1,74 @@
/* Copyright 2011-2013 Yorba Foundation
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* 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();
}
}

View 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);
}
}

View file

@ -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;
}
}

View file

@ -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; }

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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);
}
}
}

View file

@ -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());
}
}

View file

@ -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());
}

View file

@ -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());
}
}

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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)");
}
}

View file

@ -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;
}
}
}

View file

@ -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());
}
}

View file

@ -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)";
}
}

View file

@ -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);

View file

@ -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

View file

@ -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());
}
}

View file

@ -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();
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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;

View 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);
}
}

View 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
}

View file

@ -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);

View file

@ -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.
*/