Only create IMAP account and folder sessions when ready, not otherwise.

This commit makes the Imap.Account and Imap.Folder classes work somewhat
more like Imap.ClientSession, in that they have become higher-level
wrappers around ClientSession which come and go as the client session
does (i.e. as the connection to the IMAP server comes and goes). Further,
partly decouple account session lifecycle in ImapEngine.GenericAccount
and the folder session in ImapEngine.MinimalFolder from those objects
being opened/closed, so that sessions are created only when open /and/
the IMAP server is available, and disconnected on close /or/ when the
underlying connection goes away.

As a result, GenericAccount and MinimalFolder no longer claims a client
session on open and try to keep it forever. Instead if needed, they wait
for the server to become contactable.

This makes Geary much more robust in the face of changing network
connections - when working offline, resuming after sleep, and so on.
This commit is contained in:
Michael James Gratton 2018-01-26 09:52:20 +10:30
parent 59a52bde3e
commit ea891a39cd
27 changed files with 2497 additions and 2475 deletions

View file

@ -172,12 +172,14 @@ src/engine/db/db-versioned-database.vala
src/engine/db/db.vala
src/engine/imap/imap.vala
src/engine/imap/imap-error.vala
src/engine/imap/api/imap-account.vala
src/engine/imap/api/imap-account-session.vala
src/engine/imap/api/imap-email-flags.vala
src/engine/imap/api/imap-email-properties.vala
src/engine/imap/api/imap-folder.vala
src/engine/imap/api/imap-folder-properties.vala
src/engine/imap/api/imap-folder-root.vala
src/engine/imap/api/imap-folder.vala
src/engine/imap/api/imap-folder-session.vala
src/engine/imap/api/imap-session-object.vala
src/engine/imap/command/imap-append-command.vala
src/engine/imap/command/imap-capability-command.vala
src/engine/imap/command/imap-close-command.vala

View file

@ -86,12 +86,14 @@ engine/db/db-versioned-database.vala
engine/imap/imap.vala
engine/imap/imap-error.vala
engine/imap/api/imap-account.vala
engine/imap/api/imap-account-session.vala
engine/imap/api/imap-email-flags.vala
engine/imap/api/imap-email-properties.vala
engine/imap/api/imap-folder-properties.vala
engine/imap/api/imap-folder.vala
engine/imap/api/imap-folder-properties.vala
engine/imap/api/imap-folder-root.vala
engine/imap/api/imap-folder-session.vala
engine/imap/api/imap-session-object.vala
engine/imap/command/imap-append-command.vala
engine/imap/command/imap-capability-command.vala
engine/imap/command/imap-close-command.vala

View file

@ -32,12 +32,12 @@ public abstract class Geary.AbstractLocalFolder : Geary.Folder {
protected bool is_open() {
return open_count > 0;
}
public override async void wait_for_open_async(Cancellable? cancellable = null) throws Error {
public override async void wait_for_remote_async(Cancellable? cancellable = null) throws Error {
if (open_count == 0)
throw new EngineError.OPEN_REQUIRED("%s not open", to_string());
}
public override async bool open_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable = null)
throws Error {
if (open_count++ > 0)

View file

@ -398,28 +398,38 @@ public class Geary.Engine : BaseObject {
return account_instances.get(account_information.id);
ImapDB.Account local_account = new ImapDB.Account(account_information);
Imap.Account remote_account = new Imap.Account(account_information);
Geary.Account account;
switch (account_information.service_provider) {
case ServiceProvider.GMAIL:
account = new ImapEngine.GmailAccount("Gmail:%s".printf(account_information.id),
account_information, remote_account, local_account);
account = new ImapEngine.GmailAccount(
"Gmail:%s".printf(account_information.id),
account_information,
local_account
);
break;
case ServiceProvider.YAHOO:
account = new ImapEngine.YahooAccount("Yahoo:%s".printf(account_information.id),
account_information, remote_account, local_account);
account = new ImapEngine.YahooAccount(
"Yahoo:%s".printf(account_information.id),
account_information,
local_account
);
break;
case ServiceProvider.OUTLOOK:
account = new ImapEngine.OutlookAccount("Outlook:%s".printf(account_information.id),
account_information, remote_account, local_account);
account = new ImapEngine.OutlookAccount(
"Outlook:%s".printf(account_information.id),
account_information,
local_account
);
break;
case ServiceProvider.OTHER:
account = new ImapEngine.OtherAccount("Other:%s".printf(account_information.id),
account_information, remote_account, local_account);
account = new ImapEngine.OtherAccount(
"Other:%s".printf(account_information.id),
account_information,
local_account
);
break;
default:

View file

@ -63,13 +63,12 @@ public abstract class Geary.Folder : BaseObject {
LOCAL,
BOTH
}
public enum OpenFailed {
LOCAL_FAILED,
REMOTE_FAILED,
CANCELLED
LOCAL_ERROR,
REMOTE_ERROR,
}
/**
* Provides the reason why the folder is closing or closed when the {@link closed} signal
* is fired.
@ -195,51 +194,68 @@ public abstract class Geary.Folder : BaseObject {
public abstract Geary.SpecialFolderType special_folder_type { get; }
public abstract Geary.ProgressMonitor opening_monitor { get; }
/**
* Fired when the folder is successfully opened by a caller.
* Fired when the folder moves through stages of being opened.
*
* It will only fire once until the Folder is closed, with the {@link OpenState} indicating what
* has been opened and the count indicating the number of messages in the folder. In the case
* of {@link OpenState.BOTH} or {@link OpenState.REMOTE}, it refers to the authoritative number.
* For {@link OpenState.LOCAL}, it refers to the number of messages in the local store.
* It will fire at least once if the folder successfully opens,
* with the {@link OpenState} indicating what has been opened and
* the count indicating the number of messages in the folder. it
* may fire additional times as remote sessions are established
* and re-established after being lost.
*
* {@link OpenState.REMOTE} will only be passed if there's no local store, indicating that it's
* not a synchronized folder but rather one entirely backed by a network server. Geary
* currently has no such folder implemented like this.
* If //state// is {@link OpenState.LOCAL}, the local store for
* the folder has opened and the count reflects the number of
* messages in the local store.
*
* This signal will never fire with {@link OpenState.CLOSED} as a parameter.
* If //state// is {@link OpenState.BOTH}, it indicates both the
* local store and a remote session has been established, and the
* count reflects the number of messages on the remote.
*
* If //state// is {@link OpenState.REMOTE}, it indicates a folder
* is not synchronized locally but rather one entirely backed by a
* network server.
*
* In the case of {@link OpenState.BOTH} or {@link
* OpenState.REMOTE}, it refers to the authoritative count.
*
* This signal will never fire with {@link OpenState.CLOSED} as a
* parameter.
*
* @see get_open_state
*/
public signal void opened(OpenState state, int count);
/**
* Fired when {@link open_async} fails for one or more reasons.
*
* See open_async and {@link opened} for more information on how opening a Folder works, i particular
* how open_async may return immediately although the remote has not completely opened.
* This signal may be called in the context of, or after completion of, open_async. It will
* ''not'' be called after {@link close_async} has completed, however.
* See open_async and {@link opened} for more information on how
* opening a Folder works, in particular how open_async may return
* immediately although the remote has not completely opened.
* This signal may be called in the context of, or after
* completion of, open_async. It will ''not'' be called after
* {@link close_async} has completed, however.
*
* Note that this signal may be fired ''and'' open_async throw an Error.
* Note that this signal may be fired ''and'' open_async throw an
* Error.
*
* This signal may be fired more than once before the Folder is closed. It will only fire once
* for each type of failure, however.
* This signal may be fired more than once before the Folder is
* closed, especially in the case of a remote session
*/
public signal void open_failed(OpenFailed failure, Error? err);
/**
* Fired when the Folder is closed, either by the caller or due to errors in the local
* or remote store(s).
* Fired when the Folder is closed, either by the caller or due to
* errors in the local or remote store(s).
*
* It will fire three times: to report how the local store closed
* (gracefully or due to error), how the remote closed (similarly) and finally with
* {@link CloseReason.FOLDER_CLOSED}. The first two may come in either order; the third is
* always the last.
* It will fire a number of times: to report how the local store
* closed (gracefully or due to error), how the remote closed
* (similarly) and finally with {@link CloseReason.FOLDER_CLOSED}.
* The first two may come in either order; the third is always the
* last.
*/
public signal void closed(CloseReason reason);
/**
* Fired when email has been appended to the list of messages in the folder.
*
@ -384,7 +400,7 @@ public abstract class Geary.Folder : BaseObject {
protected virtual void notify_email_locally_complete(Gee.Collection<Geary.EmailIdentifier> ids) {
email_locally_complete(ids);
}
/**
* In its default implementation, this will also call {@link notify_display_name_changed} since
* that's often the case; if not, subclasses should override.
@ -392,13 +408,12 @@ public abstract class Geary.Folder : BaseObject {
protected virtual void notify_special_folder_type_changed(Geary.SpecialFolderType old_type,
Geary.SpecialFolderType new_type) {
special_folder_type_changed(old_type, new_type);
// in default implementation, this may also mean the display name changed; subclasses may
// override this behavior, but no way to detect this, so notify
if (special_folder_type != Geary.SpecialFolderType.NONE)
notify_display_name_changed();
notify_display_name_changed();
}
protected virtual void notify_display_name_changed() {
display_name_changed();
}
@ -418,8 +433,10 @@ public abstract class Geary.Folder : BaseObject {
* Returns the state of the Folder's connections to the local and remote stores.
*/
public abstract OpenState get_open_state();
/**
* Marks the folder's operations as being required for use.
*
* The Folder must be opened before most operations may be performed on it. Depending on the
* implementation this might entail opening a network connection or setting the connection to
* a particular state, opening a file or database, and so on.
@ -444,7 +461,7 @@ public abstract class Geary.Folder : BaseObject {
* accessing the remote store before OpenState.BOTH has been signalled will result in that
* call blocking until the remote is open or an error state has occurred. It's also possible for
* the command to return early without waiting, depending on prior information of the folder.
* See list_email_async() for special notes on its operation. Also see wait_for_open_async().
* See list_email_async() for special notes on its operation. Also see wait_for_remote_async().
*
* If there's an error while opening, "open-failed" will be fired. (See that signal for more
* information on how many times it may fire, and when.) To prevent the Folder from going into
@ -462,18 +479,20 @@ public abstract class Geary.Folder : BaseObject {
* Returns false if already opened.
*/
public abstract async bool open_async(OpenFlags open_flags, Cancellable? cancellable = null) throws Error;
/**
* Wait for the Folder to become fully open or fails to open due to error. If not opened
* due to error, throws EngineError.ALREADY_CLOSED.
* Blocks waiting for the folder to establish a remote session.
*
* NOTE: The current implementation requirements are only that should be work after an
* open_async() call has completed (i.e. an open is in progress). Calling this method
* otherwise will throw an EngineError.OPEN_REQUIRED.
* @throws EngineError.OPEN_REQUIRED if the folder has not already
* been opened.
* @throws EngineError.ALREADY_CLOSED if not opened due to error.
*/
public abstract async void wait_for_open_async(Cancellable? cancellable = null) throws Error;
public abstract async void wait_for_remote_async(Cancellable? cancellable = null)
throws Error;
/**
* Marks one use of the folder's operations as being completed.
*
* The Folder should be closed when operations on it are concluded. Depending on the
* implementation this might entail closing a network connection or reverting it to another
* state, or closing file handles or database connections.
@ -487,13 +506,15 @@ public abstract class Geary.Folder : BaseObject {
* {@link wait_for_close_async} to block until the folder is completely closed. Otherwise,
* returns false. Note that this semantic is slightly different than the result code for
* {@link open_async}.
*
* @see open_async
*/
public abstract async bool close_async(Cancellable? cancellable = null) throws Error;
/**
* Wait for the {@link Folder} to fully close.
*
* Unlike {@link wait_for_open_async}, this will ''always'' block until a {@link Folder} is
* Unlike {@link wait_for_remote_async}, this will ''always'' block until a {@link Folder} is
* closed, even if it's not open.
*/
public abstract async void wait_for_close_async(Cancellable? cancellable = null) throws Error;

View file

@ -65,7 +65,6 @@ private class Geary.ImapDB.Account : BaseObject {
// Only available when the Account is opened
public SmtpOutboxFolder? outbox { get; private set; default = null; }
public Geary.SearchFolder? search_folder { get; private set; default = null; }
public ImapEngine.ContactStore contact_store { get; private set; }
public IntervalProgressMonitor search_index_monitor { get; private set;
default = new IntervalProgressMonitor(ProgressType.SEARCH_INDEX, 0, 0); }
@ -340,11 +339,8 @@ private class Geary.ImapDB.Account : BaseObject {
// ImapDB.Account holds the Outbox, which is tied to the database it maintains
outbox = new SmtpOutboxFolder(db, account, sending_monitor);
outbox.email_sent.connect(on_outbox_email_sent);
// Search folder
search_folder = ((ImapEngine.GenericAccount) account).new_search_folder();
}
public async void close_async(Cancellable? cancellable) throws Error {
if (db == null)
return;
@ -361,9 +357,8 @@ private class Geary.ImapDB.Account : BaseObject {
outbox.email_sent.disconnect(on_outbox_email_sent);
outbox = null;
search_folder = null;
}
private void on_outbox_email_sent(Geary.RFC822.Message rfc822) {
email_sent(rfc822);
}
@ -519,12 +514,14 @@ private class Geary.ImapDB.Account : BaseObject {
Geary.FolderPath path = (parent != null)
? parent.get_child(basename)
: new Imap.FolderRoot(basename);
Geary.Imap.FolderProperties properties = new Geary.Imap.FolderProperties(
result.int_for("last_seen_total"), result.int_for("unread_count"), 0,
Geary.Imap.FolderProperties properties = new Geary.Imap.FolderProperties.from_imapdb(
Geary.Imap.MailboxAttributes.deserialize(result.string_for("attributes")),
result.int_for("last_seen_total"),
result.int_for("unread_count"),
new Imap.UIDValidity(result.int64_for("uid_validity")),
new Imap.UID(result.int64_for("uid_next")),
Geary.Imap.MailboxAttributes.deserialize(result.string_for("attributes")));
new Imap.UID(result.int64_for("uid_next"))
);
// due to legacy code, can't set last_seen_total to -1 to indicate that the folder
// hasn't been SELECT/EXAMINE'd yet, so the STATUS count should be used as the
// authoritative when the other is zero ... this is important when first creating a
@ -608,17 +605,21 @@ private class Geary.ImapDB.Account : BaseObject {
Db.Result results = stmt.exec(cancellable);
if (!results.finished) {
properties = new Imap.FolderProperties(results.int_for("last_seen_total"),
results.int_for("unread_count"), 0,
properties = new Imap.FolderProperties.from_imapdb(
Geary.Imap.MailboxAttributes.deserialize(results.string_for("attributes")),
results.int_for("last_seen_total"),
results.int_for("unread_count"),
new Imap.UIDValidity(results.int64_for("uid_validity")),
new Imap.UID(results.int64_for("uid_next")),
Geary.Imap.MailboxAttributes.deserialize(results.string_for("attributes")));
new Imap.UID(results.int64_for("uid_next"))
);
// due to legacy code, can't set last_seen_total to -1 to indicate that the folder
// hasn't been SELECT/EXAMINE'd yet, so the STATUS count should be used as the
// authoritative when the other is zero ... this is important when first creating a
// folder, as the STATUS is the count that is known first
properties.set_status_message_count(results.int_for("last_seen_status_total"),
(properties.select_examine_messages == 0));
properties.set_status_message_count(
results.int_for("last_seen_status_total"),
(properties.select_examine_messages == 0)
);
}
return Db.TransactionOutcome.DONE;

View file

@ -271,48 +271,6 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
properties.set_select_examine_message_count(count);
}
public async Imap.StatusData fetch_status_data(ListFlags flags, Cancellable? cancellable) throws Error {
Imap.StatusData? status_data = null;
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
Db.Statement stmt = cx.prepare("""
SELECT uid_next, uid_validity, unread_count
FROM FolderTable
WHERE id = ?
""");
stmt.bind_rowid(0, folder_id);
Db.Result result = stmt.exec(cancellable);
if (result.finished)
return Db.TransactionOutcome.DONE;
int messages = do_get_email_count(cx, flags, cancellable);
Imap.UID? uid_next = !result.is_null_for("uid_next")
? new Imap.UID(result.int64_for("uid_next"))
: null;
Imap.UIDValidity? uid_validity = !result.is_null_for("uid_validity")
? new Imap.UIDValidity(result.int64_for("uid_validity"))
: null;
// Note that recent is not stored
status_data = new Imap.StatusData(
// XXX using to_string here very sketchy
new Imap.MailboxSpecifier(this.path.to_string()),
messages,
0,
uid_next,
uid_validity,
result.int_for("unread_count")
);
return Db.TransactionOutcome.DONE;
}, cancellable);
if (status_data == null)
throw new EngineError.NOT_FOUND("%s STATUS not found in database", path.to_string());
return status_data;
}
// Returns a Map with the created or merged email as the key and the result of the operation
// (true if created, false if merged) as the value. Note that every email
// object passed in's EmailIdentifier will be fully filled out by this

View file

@ -21,7 +21,7 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
Geary.Endpoint.Flags.SSL,
Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC);
}
public static Geary.Endpoint generate_smtp_endpoint() {
return new Geary.Endpoint(
"smtp.gmail.com",
@ -30,9 +30,10 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC);
}
public GmailAccount(string name, Geary.AccountInformation account_information,
Imap.Account remote, ImapDB.Account local) {
base (name, account_information, remote, local);
public GmailAccount(string name,
Geary.AccountInformation account_information,
ImapDB.Account local) {
base(name, account_information, local);
}
protected override Geary.SpecialFolderType[] get_supported_special_folders() {

View file

@ -65,27 +65,21 @@ private class Geary.ImapEngine.GmailFolder : MinimalFolder, FolderSupport.Archiv
return;
}
// For speed reasons, use a detached Imap.Folder object to delete moved emails; this is a
// For speed reasons, use a standalone Imap.Folder object to delete moved emails; this is a
// separate connection and is not synchronized with the database, but also avoids a full
// folder normalization, which can be a heavyweight operation
Imap.Folder imap_trash = yield ((GenericAccount) folder.account).fetch_detached_folder_async(
trash.path, cancellable);
yield imap_trash.open_async(cancellable);
GenericAccount account = (GenericAccount) folder.account;
Imap.FolderSession imap_trash = yield account.open_folder_session(
trash.path, cancellable
);
try {
yield imap_trash.remove_email_async(Imap.MessageSet.uid_sparse(uids), cancellable);
} finally {
try {
// don't use cancellable, need to close this connection no matter what
yield imap_trash.close_async(null);
} catch (Error err) {
// ignored
}
account.release_folder_session(imap_trash);
}
debug("%s: Successfully true-removed %d/%d emails", folder.to_string(), uids.size,
email_ids.size);
}
}

View file

@ -106,7 +106,7 @@ private class Geary.ImapEngine.RefreshFolderSync : FolderOperation {
try {
yield this.folder.open_async(Folder.OpenFlags.FAST_OPEN, cancellable);
opened = true;
yield this.folder.wait_for_open_async(cancellable);
yield this.folder.wait_for_remote_async(cancellable);
yield sync_folder(cancellable);
} finally {
if (opened) {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -491,7 +491,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
// wait until the remote folder is opened (or throws an exception, in which case closed)
try {
if (!is_close_op && folder_opened && state == State.OPEN)
yield owner.wait_for_open_async();
yield owner.wait_for_remote_async();
} catch (Error remote_err) {
debug("Folder %s closed or failed to open, remote replay queue closing: %s",
to_string(), remote_err.message);

View file

@ -22,42 +22,34 @@ private class Geary.ImapEngine.RevokableCommittedMove : Revokable {
this.destination = destination;
this.destination_uids = destination_uids;
}
protected override async void internal_revoke_async(Cancellable? cancellable) throws Error {
Imap.Folder? detached_destination = null;
Imap.FolderSession? session = null;
try {
// use a detached folder to quickly open, issue command, and leave, without full
// normalization that MinimalFolder requires
detached_destination = yield account.fetch_detached_folder_async(destination, cancellable);
yield detached_destination.open_async(cancellable);
session = yield this.account.open_folder_session(destination, cancellable);
foreach (Imap.MessageSet msg_set in Imap.MessageSet.uid_sparse(destination_uids)) {
// don't use Cancellable to try to make operations atomic
yield detached_destination.copy_email_async(msg_set, source, null);
yield detached_destination.remove_email_async(msg_set.to_list(), null);
yield session.copy_email_async(msg_set, source, null);
yield session.remove_email_async(msg_set.to_list(), null);
if (cancellable != null && cancellable.is_cancelled())
throw new IOError.CANCELLED("Revoke cancelled");
}
notify_revoked();
Geary.Folder target = yield this.account.fetch_folder_async(this.destination);
this.account.update_folder(target);
} finally {
if (detached_destination != null) {
try {
yield detached_destination.close_async(cancellable);
} catch (Error err) {
// ignored
}
if (session != null) {
this.account.release_folder_session(session);
}
set_invalid();
}
}
protected override async void internal_commit_async(Cancellable? cancellable) throws Error {
// pretty simple: already committed, so done
notify_committed(null);

View file

@ -25,4 +25,21 @@ private static bool is_hard_failure(Error err) {
|| err is EngineError.SERVER_UNAVAILABLE;
}
/**
* Determines if this IOError related to a remote host or not.
*/
private static bool is_remote_error(GLib.Error err) {
return err is ImapError
|| err is IOError.CONNECTION_CLOSED
|| err is IOError.CONNECTION_REFUSED
|| err is IOError.HOST_UNREACHABLE
|| err is IOError.MESSAGE_TOO_LARGE
|| err is IOError.NETWORK_UNREACHABLE
|| err is IOError.NOT_CONNECTED
|| err is IOError.PROXY_AUTH_FAILED
|| err is IOError.PROXY_FAILED
|| err is IOError.PROXY_NEED_AUTH
|| err is IOError.PROXY_NOT_ALLOWED;
}
}

View file

@ -5,9 +5,11 @@
*/
private class Geary.ImapEngine.OtherAccount : Geary.ImapEngine.GenericAccount {
public OtherAccount(string name, AccountInformation account_information,
Imap.Account remote, ImapDB.Account local) {
base (name, account_information, remote, local);
public OtherAccount(string name,
AccountInformation account_information,
ImapDB.Account local) {
base (name, account_information, local);
}
protected override MinimalFolder new_folder(ImapDB.Folder local_folder) {
@ -20,4 +22,5 @@ private class Geary.ImapEngine.OtherAccount : Geary.ImapEngine.GenericAccount {
return new OtherFolder(this, local_folder, type);
}
}

View file

@ -5,6 +5,7 @@
*/
private class Geary.ImapEngine.OutlookAccount : Geary.ImapEngine.GenericAccount {
public static Geary.Endpoint generate_imap_endpoint() {
Geary.Endpoint endpoint = new Geary.Endpoint(
"imap-mail.outlook.com",
@ -30,9 +31,10 @@ private class Geary.ImapEngine.OutlookAccount : Geary.ImapEngine.GenericAccount
Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC);
}
public OutlookAccount(string name, AccountInformation account_information, Imap.Account remote,
ImapDB.Account local) {
base (name, account_information, remote, local);
public OutlookAccount(string name,
AccountInformation account_information,
ImapDB.Account local) {
base(name, account_information, local);
}
protected override MinimalFolder new_folder(ImapDB.Folder local_folder) {

View file

@ -31,25 +31,15 @@ private class Geary.ImapEngine.ReplayDisconnect : Geary.ImapEngine.ReplayOperati
public override async ReplayOperation.Status replay_local_async() throws Error {
debug("%s ReplayDisconnect reason=%s", owner.to_string(), reason.to_string());
Geary.Folder.CloseReason remote_reason = reason.is_error()
? Geary.Folder.CloseReason.REMOTE_ERROR : Geary.Folder.CloseReason.REMOTE_CLOSE;
// because close_internal_async() may schedule a ReplayOperation before its first yield,
// that means a ReplayOperation is scheduling a ReplayOperation, which isn't something
// we want to encourage, so use the Idle queue to schedule close_internal_async
Idle.add(() => {
// ReplayDisconnect is only used when remote disconnects, so never flush pending, the
// connection is down or going down
owner.close_internal_async.begin(Geary.Folder.CloseReason.LOCAL_CLOSE, remote_reason,
flush_pending, cancellable);
return false;
});
? Geary.Folder.CloseReason.REMOTE_ERROR
: Geary.Folder.CloseReason.REMOTE_CLOSE;
this.owner.close_remote_session.begin(remote_reason);
return ReplayOperation.Status.COMPLETED;
}
public override async void backout_local_async() throws Error {
}
@ -62,4 +52,3 @@ private class Geary.ImapEngine.ReplayDisconnect : Geary.ImapEngine.ReplayOperati
return "reason=%s".printf(reason.to_string());
}
}

View file

@ -5,6 +5,7 @@
*/
private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount {
public static Geary.Endpoint generate_imap_endpoint() {
return new Geary.Endpoint(
"imap.mail.yahoo.com",
@ -12,7 +13,7 @@ private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount {
Geary.Endpoint.Flags.SSL,
Imap.ClientConnection.RECOMMENDED_TIMEOUT_SEC);
}
public static Geary.Endpoint generate_smtp_endpoint() {
return new Geary.Endpoint(
"smtp.mail.yahoo.com",
@ -20,16 +21,17 @@ private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount {
Geary.Endpoint.Flags.SSL,
Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC);
}
private static Gee.HashMap<Geary.FolderPath, Geary.SpecialFolderType>? special_map = null;
public YahooAccount(string name, AccountInformation account_information,
Imap.Account remote, ImapDB.Account local) {
base (name, account_information, remote, local);
public YahooAccount(string name,
AccountInformation account_information,
ImapDB.Account local) {
base(name, account_information, local);
if (special_map == null) {
special_map = new Gee.HashMap<Geary.FolderPath, Geary.SpecialFolderType>();
special_map.set(Imap.MailboxSpecifier.inbox.to_folder_path(null, null), Geary.SpecialFolderType.INBOX);
special_map.set(new Imap.FolderRoot("Sent"), Geary.SpecialFolderType.SENT);
special_map.set(new Imap.FolderRoot("Draft"), Geary.SpecialFolderType.DRAFTS);

View file

@ -14,12 +14,6 @@
* (in particular, {@link Geary.ImapEngine.GenericAccount}) and makes
* them into simple async calls.
*
* This class maintains an {@link ClientSessionManager} instance to
* maintain a pool of connections to an account's IMAP endpoint. On
* opening, it will open the pool, then claim an IMAP session and
* maintain it in a non-selected state for executing
* non-mailbox-specific operations.
*
* Geary.Imap.Account manages the {@link Imap.Folder} objects it
* returns, but only in the sense that it will not create new
* instances repeatedly. Otherwise, it does not refresh or update the
@ -27,100 +21,24 @@
* Imap.StatusData} periodically). That's the responsibility of the
* higher layers of the stack.
*/
private class Geary.Imap.Account : BaseObject {
internal class Geary.Imap.AccountSession : Geary.Imap.SessionObject {
/** Default IMAP session pool size. */
private const int IMAP_MIN_POOL_SIZE = 2;
private Gee.HashMap<FolderPath,Imap.Folder> folders =
new Gee.HashMap<FolderPath,Imap.Folder>();
/** Determines if the IMAP account has been opened. */
public bool is_open { get; private set; default = false; }
/**
* Determines if the IMAP account has a working connection.
*
* See {@link ClientSessionManager.is_open} for more details.
*/
public bool is_ready { get { return this.session_mgr.is_ready; } }
private string name;
private AccountInformation account;
private ClientSessionManager session_mgr;
private uint authentication_failures = 0;
private ClientSession? account_session = null;
private Nonblocking.Mutex account_session_mutex = new Nonblocking.Mutex();
private Nonblocking.Mutex cmd_mutex = new Nonblocking.Mutex();
private Gee.HashMap<FolderPath, Imap.Folder> folders = new Gee.HashMap<FolderPath, Imap.Folder>();
private Gee.List<MailboxInformation>? list_collector = null;
private Gee.List<StatusData>? status_collector = null;
private Gee.List<ServerData>? server_data_collector = null;
/**
* Fired after opening when the account has a working connection.
*
* This may be fired multiple times, see @{link
* ClientSessionManager.ready} for details.
*/
public signal void ready();
internal AccountSession(string account_id,
ClientSession session) {
base("%s:account".printf(account_id), session);
/** Fired if a user-notifiable problem occurs. */
public signal void report_problem(ProblemReport report);
public Account(Geary.AccountInformation account) {
this.name = account.id + ":imap";
this.account = account;
this.session_mgr = new ClientSessionManager(account);
this.session_mgr.min_pool_size = IMAP_MIN_POOL_SIZE;
this.session_mgr.ready.connect(on_session_ready);
this.session_mgr.connection_failed.connect(on_connection_failed);
this.session_mgr.login_failed.connect(on_login_failed);
}
/**
* Prepares the account for use.
*
* Opening the account will kick off at establishing least one
* connection to the IMAP server, if accessible.
*/
public async void open_async(Cancellable? cancellable = null) throws Error {
if (is_open)
throw new EngineError.ALREADY_OPEN("Imap.Account already open");
// Reset this so we start trying to authenticate again
this.authentication_failures = 0;
// This will cause the session manager to open at least one
// connection. We can't attempt to claim one straight away
// since we might not be online.
yield session_mgr.open_async(cancellable);
is_open = true;
}
/**
* Notifies the account that the engine is preparing to exit.
*/
public void prepare_to_close() {
this.session_mgr.discard_returned_sessions = true;
}
/**
* Closes the account, releasing its IMAP session and session pool.
*/
public async void close_async(Cancellable? cancellable = null) throws Error {
if (!is_open)
return;
yield drop_session_async(cancellable);
try {
yield session_mgr.close_async(cancellable);
} catch (Error err) {
// ignored
}
is_open = false;
session.list.connect(on_list_data);
session.status.connect(on_status_data);
session.server_data_received.connect(on_server_data_received);
}
/**
@ -128,7 +46,7 @@ private class Geary.Imap.Account : BaseObject {
*/
public async FolderPath get_default_personal_namespace(Cancellable? cancellable)
throws Error {
ClientSession session = yield claim_session_async(cancellable);
ClientSession session = claim_session();
if (session.personal_namespaces.is_empty) {
throw new ImapError.INVALID("No personal namespace found");
}
@ -145,7 +63,7 @@ private class Geary.Imap.Account : BaseObject {
public async bool folder_exists_async(FolderPath path, Cancellable? cancellable)
throws Error {
ClientSession session = yield claim_session_async(cancellable);
ClientSession session = claim_session();
Gee.List<MailboxInformation> mailboxes = yield send_list_async(session, path, false, cancellable);
bool exists = mailboxes.is_empty;
if (!exists) {
@ -171,7 +89,7 @@ private class Geary.Imap.Account : BaseObject {
Geary.SpecialFolderType? type,
Cancellable? cancellable)
throws Error {
ClientSession session = yield claim_session_async(cancellable);
ClientSession session = claim_session();
MailboxSpecifier mailbox = session.get_mailbox_for_path(path);
bool can_create_special = session.capabilities.has_capability(Capabilities.CREATE_SPECIAL_USE);
CreateCommand cmd = (type != null && can_create_special)
@ -199,14 +117,16 @@ private class Geary.Imap.Account : BaseObject {
*/
public async Imap.Folder fetch_folder_async(FolderPath path, Cancellable? cancellable)
throws Error {
ClientSession session = yield claim_session_async(cancellable);
ClientSession session = claim_session();
Gee.List<MailboxInformation>? mailboxes = yield send_list_async(session, path, false, cancellable);
Gee.List<MailboxInformation>? mailboxes = yield send_list_async(
session, path, false, cancellable
);
if (mailboxes.is_empty)
throw_not_found(path);
Imap.Folder? folder = null;
MailboxInformation mailbox_info = mailboxes.get(0);
Imap.FolderProperties? props = null;
if (!mailbox_info.attrs.is_no_select) {
StatusData status = yield send_status_async(
session,
@ -214,12 +134,16 @@ private class Geary.Imap.Account : BaseObject {
StatusDataType.all(),
cancellable
);
folder = new_selectable_folder(path, status, mailbox_info.attrs);
props = new Imap.FolderProperties.selectable(
mailbox_info.attrs,
status,
session.capabilities
);
} else {
folder = new_unselectable_folder(path, mailbox_info.attrs);
props = new Imap.FolderProperties.not_selectable(mailbox_info.attrs);
}
return folder;
return new Imap.Folder(path, props);
}
/**
@ -230,33 +154,22 @@ private class Geary.Imap.Account : BaseObject {
* server and cached for future use.
*/
public async Imap.Folder fetch_folder_cached_async(FolderPath path,
bool refresh_counts,
bool refresh_status,
Cancellable? cancellable)
throws Error {
check_open();
throws Error {
ClientSession session = claim_session();
Imap.Folder? folder = this.folders.get(path);
if (folder == null) {
folder = yield fetch_folder_async(path, cancellable);
this.folders.set(path, folder);
} else {
if (refresh_counts && !folder.properties.attrs.is_no_select) {
try {
ClientSession session = yield claim_session_async(cancellable);
StatusData data = yield send_status_async(
session,
session.get_mailbox_for_path(path),
{ StatusDataType.UNSEEN, StatusDataType.MESSAGES },
cancellable
);
folder.properties.set_status_unseen(data.unseen);
folder.properties.set_status_message_count(data.messages, false);
} catch (ImapError e) {
this.folders.unset(path);
// XXX notify someone
throw_not_found(path);
}
}
} else if (refresh_status && !folder.properties.attrs.is_no_select) {
StatusData status = yield send_status_async(
session,
session.get_mailbox_for_path(path),
{ StatusDataType.UNSEEN, StatusDataType.MESSAGES },
cancellable
);
folder.properties.update_status(status);
}
return folder;
}
@ -272,7 +185,7 @@ private class Geary.Imap.Account : BaseObject {
*/
public async Gee.List<Imap.Folder> fetch_child_folders_async(FolderPath? parent, Cancellable? cancellable)
throws Error {
ClientSession session = yield claim_session_async(cancellable);
ClientSession session = claim_session();
Gee.List<Imap.Folder> children = new Gee.ArrayList<Imap.Folder>();
Gee.List<MailboxInformation> mailboxes = yield send_list_async(session, parent, true, cancellable);
if (mailboxes.size == 0) {
@ -300,7 +213,10 @@ private class Geary.Imap.Account : BaseObject {
FolderPath path = session.get_path_for_mailbox(mailbox_info.mailbox);
Folder? child = this.folders.get(path);
if (child == null) {
child = new_unselectable_folder(path, mailbox_info.attrs);
child = new Imap.Folder(
path,
new Imap.FolderProperties.not_selectable(mailbox_info.attrs)
);
this.folders.set(path, child);
}
children.add(child);
@ -347,10 +263,18 @@ private class Geary.Imap.Account : BaseObject {
FolderPath child_path = session.get_path_for_mailbox(mailbox_info.mailbox);
Imap.Folder? child = this.folders.get(child_path);
if (child != null) {
child.properties.update_status(status);
} else {
child = new_selectable_folder(child_path, status, mailbox_info.attrs);
child = new Imap.Folder(
child_path,
new Imap.FolderProperties.selectable(
mailbox_info.attrs,
status,
session.capabilities
)
);
this.folders.set(child_path, child);
}
@ -364,12 +288,6 @@ private class Geary.Imap.Account : BaseObject {
return children;
}
internal Imap.Folder new_selectable_folder(FolderPath path, StatusData status, MailboxAttributes attrs) {
return new Imap.Folder(
path, new Imap.FolderProperties.status(status, attrs), this.session_mgr
);
}
internal void folders_removed(Gee.Collection<FolderPath> paths) {
foreach (FolderPath path in paths) {
if (folders.has_key(path))
@ -377,88 +295,15 @@ private class Geary.Imap.Account : BaseObject {
}
}
// 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 {
check_open();
// check if available session is in good state
if (account_session != null
&& account_session.get_protocol_state(null) != ClientSession.ProtocolState.AUTHORIZED) {
yield drop_session_async(cancellable);
/** {@inheritDoc} */
public override ClientSession? close() {
ClientSession old_session = base.close();
if (old_session != null) {
old_session.list.disconnect(on_list_data);
old_session.status.disconnect(on_status_data);
old_session.server_data_received.disconnect(on_server_data_received);
}
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.server_data_received.connect(on_server_data_received);
account_session.disconnected.connect(on_disconnected);
} catch (Error claim_err) {
err = claim_err;
}
}
account_session_mutex.release(ref token);
if (err != null) {
if (account_session != null)
yield drop_session_async(null);
throw err;
}
return account_session;
}
private async void drop_session_async(Cancellable? cancellable) {
debug("[%s] Dropping account session...", to_string());
int token;
try {
token = yield account_session_mutex.claim_async(cancellable);
} catch (Error err) {
debug("Unable to claim Imap.Account session mutex: %s", err.message);
return;
}
string desc = account_session != null ? account_session.to_string() : "(none)";
if (account_session != null) {
// disconnect signals before releasing (in particular, "disconnected" will in turn
// reenter this method, so avoid that)
account_session.list.disconnect(on_list_data);
account_session.status.disconnect(on_status_data);
account_session.server_data_received.disconnect(on_server_data_received);
account_session.disconnected.disconnect(on_disconnected);
debug("[%s] Releasing account session %s", to_string(), desc);
try {
yield session_mgr.release_session_async(account_session, cancellable);
} catch (Error err) {
// ignored
}
debug("[%s] Released account session %s", to_string(), desc);
account_session = null;
}
try {
account_session_mutex.release(ref token);
} catch (Error err) {
// ignored
}
debug("[%s] Dropped account session (%s)", to_string(), desc);
return old_session;
}
// Performs a LIST against the server, returning the results
@ -566,61 +411,49 @@ private class Geary.Imap.Account : BaseObject {
return Geary.Collection.get_first(responses.values);
}
private async Gee.Map<Command, StatusResponse> send_multiple_async(
ClientSession session,
Gee.Collection<Command> cmds,
Gee.List<MailboxInformation>? list_results,
Gee.List<StatusData>? status_results,
Cancellable? cancellable)
private async Gee.Map<Command, StatusResponse>
send_multiple_async(ClientSession session,
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;
int token = yield this.cmd_mutex.claim_async(cancellable);
// set up collectors
this.list_collector = list_results;
this.status_collector = status_results;
Error? cmd_err = null;
try {
responses = yield session.send_multiple_commands_async(cmds, cancellable);
} catch (Error send_err) {
err = send_err;
responses = yield session.send_multiple_commands_async(
cmds, cancellable
);
} catch (Error err) {
cmd_err = err;
}
// disconnect collectors
list_collector = null;
status_collector = null;
cmd_mutex.release(ref token);
if (err != null)
throw err;
assert(responses != null);
// tear down collectors
this.list_collector = null;
this.status_collector = null;
this.cmd_mutex.release(ref token);
if (cmd_err != null) {
throw cmd_err;
}
return responses;
}
private void check_open() throws Error {
if (!is_open)
throw new EngineError.OPEN_REQUIRED("Imap.Account not open");
}
private inline Imap.Folder new_unselectable_folder(FolderPath path, MailboxAttributes attrs) {
return new Imap.Folder(
path, new Imap.FolderProperties(0, 0, 0, null, null, attrs), this.session_mgr
);
}
private void notify_report_problem(ProblemType problem, Error? err) {
report_problem(new ServiceProblemReport(problem, this.account, Service.IMAP, err));
}
[NoReturn]
private void throw_not_found(Geary.FolderPath? path) throws EngineError {
throw new EngineError.NOT_FOUND("Folder %s not found on %s",
(path != null) ? path.to_string() : "root", session_mgr.to_string());
throw new EngineError.NOT_FOUND(
"Folder not found: %s",
(path != null) ? path.to_string() : "[root]"
);
}
private void on_list_data(MailboxInformation mailbox_info) {
@ -638,80 +471,4 @@ private class Geary.Imap.Account : BaseObject {
server_data_collector.add(server_data);
}
private void on_disconnected() {
drop_session_async.begin(null);
}
private void on_session_ready() {
// Now have a valid session, so credentials must be good
this.authentication_failures = 0;
ready();
}
private void on_connection_failed(Error error) {
// There was an error connecting to the IMAP host
this.authentication_failures = 0;
if (error is ImapError.UNAUTHENTICATED) {
// This is effectively a login failure
on_login_failed(null);
} else {
notify_report_problem(ProblemType.CONNECTION_ERROR, error);
}
}
private void on_login_failed(Geary.Imap.StatusResponse? response) {
this.authentication_failures++;
if (this.authentication_failures >= Geary.Account.AUTH_ATTEMPTS_MAX) {
// We have tried auth too many times, so bail out
notify_report_problem(ProblemType.LOGIN_FAILED, null);
} else {
// login can fail due to an invalid password hence we
// should re-ask it but it can also fail due to server
// inaccessibility, for instance "[UNAVAILABLE] / Maximum
// number of connections from user+IP exceeded". In that
// case, resetting password seems unneeded.
bool reask_password = false;
Error? login_error = null;
try {
reask_password = (
response == null ||
response.response_code == null ||
response.response_code.get_response_code_type().value != Geary.Imap.ResponseCodeType.UNAVAILABLE
);
} catch (ImapError err) {
login_error = err;
debug("Unable to parse ResponseCode %s: %s", response.response_code.to_string(),
err.message);
}
if (!reask_password) {
// Either the server was unavailable, or we were unable to
// parse the login response. Either way, indicate a
// non-login error.
notify_report_problem(ProblemType.SERVER_ERROR, login_error);
} else {
// Now, we should ask the user for their password
this.account.fetch_passwords_async.begin(
ServiceFlag.IMAP, true,
(obj, ret) => {
try {
if (this.account.fetch_passwords_async.end(ret)) {
// Have a new password, so try that
this.session_mgr.credentials_updated();
} else {
// User cancelled, so indicate a login problem
notify_report_problem(ProblemType.LOGIN_FAILED, null);
}
} catch (Error err) {
notify_report_problem(ProblemType.GENERIC_ERROR, err);
}
});
}
}
}
public string to_string() {
return name;
}
}

View file

@ -56,45 +56,93 @@ public class Geary.Imap.FolderProperties : Geary.FolderProperties {
public UIDValidity? uid_validity { get; internal set; }
public UID? uid_next { get; internal set; }
public MailboxAttributes attrs { get; internal set; }
/**
* Note that unseen from SELECT/EXAMINE is the *position* of the first unseen message,
* not the total unseen count, so it's not be passed in here, but rather only from the unseen
* count from a STATUS command
* Constructs properties for an IMAP folder that can be selected.
*/
public FolderProperties(int messages, int email_unread, int recent, UIDValidity? uid_validity,
UID? uid_next, MailboxAttributes attrs) {
// give the base class a zero email_unread, as the notion of "unknown" doesn't exist in
// its contract
base (messages, email_unread, Trillian.UNKNOWN, Trillian.UNKNOWN, Trillian.UNKNOWN, false,
false, false);
select_examine_messages = messages;
status_messages = -1;
this.recent = recent;
public FolderProperties.selectable(MailboxAttributes attrs,
StatusData status,
Capabilities capabilities) {
this(
attrs,
status.messages,
status.unseen,
capabilities.supports_uidplus()
);
this.select_examine_messages = -1;
this.status_messages = status.messages;
this.recent = status.recent;
this.unseen = status.unseen;
this.uid_validity = status.uid_validity;
this.uid_next = status.uid_next;
}
/**
* Constructs properties for an IMAP folder that can not be selected.
*/
public FolderProperties.not_selectable(MailboxAttributes attrs) {
this(attrs, 0, 0, false);
this.select_examine_messages = 0;
this.status_messages = -1;
this.recent = 0;
this.unseen = -1;
this.uid_validity = null;
this.uid_next = null;
}
/**
* Reconstitutes properties for an IMAP folder from the database
*/
internal FolderProperties.from_imapdb(MailboxAttributes attrs,
int email_total,
int email_unread,
UIDValidity? uid_validity,
UID? uid_next) {
this(attrs, email_total, email_unread, false);
this.select_examine_messages = email_total;
this.status_messages = -1;
this.recent = 0;
this.unseen = -1;
this.uid_validity = uid_validity;
this.uid_next = uid_next;
this.attrs = attrs;
init_flags();
}
public FolderProperties.status(StatusData status, MailboxAttributes attrs) {
base (status.messages, status.unseen, Trillian.UNKNOWN, Trillian.UNKNOWN, Trillian.UNKNOWN,
false, false, false);
select_examine_messages = -1;
status_messages = status.messages;
recent = status.recent;
unseen = status.unseen;
uid_validity = status.uid_validity;
uid_next = status.uid_next;
protected FolderProperties(MailboxAttributes attrs,
int email_total,
int email_unread,
bool supports_uid) {
Trillian has_children = Trillian.UNKNOWN;
if (attrs.contains(MailboxAttribute.HAS_NO_CHILDREN))
has_children = Trillian.FALSE;
else if (attrs.contains(MailboxAttribute.HAS_CHILDREN))
has_children = Trillian.TRUE;
Trillian supports_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;
}
Trillian is_openable = Trillian.from_boolean(!attrs.is_no_select);
base(email_total, email_unread,
has_children, supports_children, is_openable,
false, // not local
false, // not virtual
!supports_uid);
this.attrs = attrs;
init_flags();
}
/**
* Use with {@link FolderProperties} of the *same folder* seen at different times (i.e. after
* SELECTing versus data stored locally). Only compares fields that suggest the contents of
@ -145,30 +193,7 @@ public class Geary.Imap.FolderProperties : Geary.FolderProperties {
return false;
}
private void init_flags() {
// \HasNoChildren & \HasChildren are optional attributes (could check for CHILDREN extension,
// but unnecessary here)
if (attrs.contains(MailboxAttribute.HAS_NO_CHILDREN))
has_children = Trillian.FALSE;
else if (attrs.contains(MailboxAttribute.HAS_CHILDREN))
has_children = Trillian.TRUE;
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.is_no_select);
}
/**
* Update an existing {@link FolderProperties} with fresh {@link StatusData}.
*

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,102 @@
/*
* Copyright 2018 Michael Gratton <mike@vee.net>.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Base class for IMAP client session objects.
*
* Since a client session can come and go as the server and network
* changes, IMAP client objects need to be sensitive to the state of
* the connection. This abstract class manages access to an IMAP
* client session for objects that use connections to an IMAP server,
* ensuring it is no longer available if the client session is
* disconnected.
*
* This class is ''not'' thread safe.
*/
public abstract class Geary.Imap.SessionObject : BaseObject {
/** Determines if this object has a valid session or not. */
public bool is_valid { get { return this.session != null; } }
private string id;
private ClientSession? session;
/** Fired if the object's connection to the server is lost. */
public signal void disconnected(ClientSession.DisconnectReason reason);
/**
* Constructs a new IMAP object with the given session.
*/
protected SessionObject(string id, ClientSession session) {
this.id = id;
this.session = session;
this.session.disconnected.connect(on_disconnected);
}
~SessionObject() {
if (close() != null) {
debug("%s: destroyed without releasing its session".printf(this.id));
}
}
/**
* Drops this object's association with its client session.
*
* Calling this method unhooks the object from its session, and
* makes it unavailable for further use. This does //not//
* disconnect the client session from its server.
*
* @return the old IMAP client session, for returning to the pool,
* etc, if any.
*/
public virtual ClientSession? close() {
ClientSession? old_session = this.session;
this.session = null;
if (old_session != null) {
old_session.disconnected.disconnect(on_disconnected);
}
return old_session;
}
/**
* Returns a string representation of this object for debugging.
*/
public virtual string to_string() {
return "%s:%s".printf(
this.id,
this.session != null ? this.session.to_string() : "(session dropped)"
);
}
/**
* Obtains IMAP session the server for use by this object.
*
* @throws ImapError.NOT_CONNECTED if the session with the server
* server has been dropped via {@link close}, or because
* the connection was lost.
*/
protected ClientSession claim_session()
throws ImapError {
if (this.session == null) {
throw new ImapError.NOT_CONNECTED("IMAP object has no session");
}
return this.session;
}
private void on_disconnected(ClientSession.DisconnectReason reason) {
debug("%s: DISCONNECTED %s", to_string(), reason.to_string());
close();
disconnected(reason);
}
}

View file

@ -78,8 +78,9 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
*/
public bool discard_returned_sessions = false;
private AccountInformation account_information;
private string id;
private Endpoint endpoint;
private Credentials credentials;
private Nonblocking.Mutex sessions_mutex = new Nonblocking.Mutex();
private Gee.Set<ClientSession> all_sessions =
@ -110,16 +111,17 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
public signal void login_failed(StatusResponse? response);
public ClientSessionManager(AccountInformation account_information) {
this.account_information = account_information;
public ClientSessionManager(string id,
Endpoint imap_endpoint,
Credentials imap_credentials) {
this.id = "%s:%s".printf(id, imap_endpoint.to_string());
// NOTE: This works because AccountInformation guarantees the IMAP endpoint not to change
// for the lifetime of the AccountInformation object; if this ever changes, will need to
// refactor for that
this.endpoint = account_information.get_imap_endpoint();
this.endpoint = imap_endpoint;
this.endpoint.notify[Endpoint.PROP_TRUST_UNTRUSTED_HOST].connect(on_imap_trust_untrusted_host);
this.endpoint.untrusted_host.connect(on_imap_untrusted_host);
this.credentials = imap_credentials;
this.pool_start = new TimeoutManager.seconds(
POOL_START_TIMEOUT_SEC,
() => { this.check_pool.begin(); }
@ -133,7 +135,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
~ClientSessionManager() {
if (is_open)
warning("[%s] Destroying opened ClientSessionManager", to_string());
warning("[%s] Destroying opened ClientSessionManager", this.id);
this.endpoint.untrusted_host.disconnect(on_imap_untrusted_host);
this.endpoint.notify[Endpoint.PROP_TRUST_UNTRUSTED_HOST].disconnect(on_imap_trust_untrusted_host);
@ -176,7 +178,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
// for now
int attempts = 0;
while (this.all_sessions.size > 0) {
debug("[%s] Waiting for client sessions to disconnect...", to_string());
debug("[%s] Waiting for client sessions to disconnect...", this.id);
Timeout.add(250, close_async.callback);
yield;
@ -192,8 +194,9 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
* This will reset the manager's authentication state and if open,
* attempt to open a connection to the server.
*/
public void credentials_updated() {
public void credentials_updated(Credentials new_creds) {
this.authentication_failed = false;
this.credentials = new_creds;
if (this.is_open) {
this.check_pool.begin();
}
@ -220,7 +223,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
throws Error {
check_open();
debug("[%s] Claiming session from %d of %d free",
to_string(), this.free_queue.size, this.all_sessions.size);
this.id, this.free_queue.size, this.all_sessions.size);
if (this.authentication_failed)
throw new ImapError.UNAUTHENTICATED("Invalid ClientSessionManager credentials");
@ -253,13 +256,13 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
return claimed;
}
public async void release_session_async(ClientSession session, Cancellable? cancellable)
public async void release_session_async(ClientSession session)
throws Error {
// Don't check_open(), it's valid for this to be called when
// is_open is false, that happens during mop-up
debug("[%s] Returning session with %d of %d free",
to_string(), this.free_queue.size, this.all_sessions.size);
this.id, this.free_queue.size, this.all_sessions.size);
if (!this.is_open || this.discard_returned_sessions) {
yield force_disconnect(session);
@ -271,17 +274,12 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
// adding it back to the pool
if (proto == ClientSession.ProtocolState.SELECTED ||
proto == ClientSession.ProtocolState.SELECTING) {
debug("[%s] Closing %s for released session %s",
to_string(),
mailbox != null ? mailbox.to_string() : "(unknown)",
session.to_string());
// always close mailbox to return to authorized state
try {
yield session.close_mailbox_async(cancellable);
yield session.close_mailbox_async(pool_cancellable);
} catch (ImapError imap_error) {
debug("[%s] Error attempting to close released session %s: %s",
to_string(), session.to_string(), imap_error.message);
this.id, session.to_string(), imap_error.message);
free = false;
}
@ -294,13 +292,19 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
}
if (free) {
debug("[%s] Unreserving session %s",
to_string(), session.to_string());
debug("[%s] Unreserving session %s", this.id, session.to_string());
this.free_queue.send(session);
}
}
}
/**
* Returns a string representation of this object for debugging.
*/
public string to_string() {
return this.id;
}
private void check_open() throws Error {
if (!is_open)
throw new EngineError.OPEN_REQUIRED("ClientSessionManager is not open");
@ -308,7 +312,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
private async void check_pool() {
debug("[%s] Checking session pool with %d of %d free",
to_string(), this.free_queue.size, this.all_sessions.size);
this.id, this.free_queue.size, this.all_sessions.size);
while (this.is_open &&
!this.authentication_failed &&
@ -326,8 +330,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
this.free_queue.send(free);
} catch (Error err) {
debug("[%s] Error adding free session pool: %s",
to_string(),
err.message);
this.id, err.message);
break;
}
@ -361,7 +364,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
yield remove_session_async(target);
} catch (Error err) {
debug("[%s] Error removing unconnected session: %s",
to_string(), err.message);
this.id, err.message);
}
break;
@ -374,6 +377,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
}
private async ClientSession create_new_authorized_session(Cancellable? cancellable) throws Error {
debug("[%s] Opening new session", this.id);
ClientSession new_session = new ClientSession(endpoint);
// Listen for auth failures early so the client is notified if
@ -390,7 +394,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
}
try {
yield new_session.initiate_session_async(account_information.imap_credentials, cancellable);
yield new_session.initiate_session_async(this.credentials, cancellable);
} catch (Error err) {
debug("[%s] Initiate session failure: %s", new_session.to_string(), err.message);
@ -417,6 +421,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
// We now have a good connection, so signal us as ready if not
// already done so.
if (!this.is_ready) {
debug("[%s] Became ready", this.id);
this.is_ready = true;
ready();
}
@ -428,7 +433,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
private async void force_disconnect_all()
throws Error {
debug("[%s] Dropping and disconnecting %d sessions",
to_string(), this.all_sessions.size);
this.id, this.all_sessions.size);
// Take a copy and work off that while scheduling disconnects,
// since as they disconnect they'll remove themselves from the
@ -447,12 +452,12 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
}
private async void force_disconnect(ClientSession session) {
debug("[%s] Dropping session %s", to_string(), session.to_string());
debug("[%s] Dropping session %s", this.id, session.to_string());
try {
yield remove_session_async(session);
} catch (Error err) {
debug("[%s] Error removing session: %s", to_string(), err.message);
debug("[%s] Error removing session: %s", this.id, err.message);
}
// Don't wait for this to finish because we don't want to
@ -485,8 +490,7 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
this.remove_session_async.end(res);
} catch (Error err) {
debug("[%s] Error removing disconnected session: %s",
to_string(),
err.message);
this.id, err.message);
}
}
);
@ -536,13 +540,4 @@ public class Geary.Imap.ClientSessionManager : BaseObject {
connection_failed(error);
}
/**
* Use only for debugging and logging.
*/
public string to_string() {
return "%s:%s".printf(
this.account_information.id,
endpoint.to_string()
);
}
}

View file

@ -83,12 +83,14 @@ geary_engine_vala_sources = files(
'imap/imap.vala',
'imap/imap-error.vala',
'imap/api/imap-account.vala',
'imap/api/imap-account-session.vala',
'imap/api/imap-email-flags.vala',
'imap/api/imap-email-properties.vala',
'imap/api/imap-folder-properties.vala',
'imap/api/imap-folder.vala',
'imap/api/imap-folder-properties.vala',
'imap/api/imap-folder-root.vala',
'imap/api/imap-folder-session.vala',
'imap/api/imap-session-object.vala',
'imap/command/imap-append-command.vala',
'imap/command/imap-capability-command.vala',
'imap/command/imap-close-command.vala',

View file

@ -56,7 +56,7 @@ public class Geary.MockFolder : Folder {
throw new EngineError.UNSUPPORTED("Mock method");
}
public override async void wait_for_open_async(Cancellable? cancellable = null)
public override async void wait_for_remote_async(Cancellable? cancellable = null)
throws Error {
throw new EngineError.UNSUPPORTED("Mock method");
}