geary/src/engine/api/geary-account.vala
Michael Gratton b3488f6cf9 Add Geary.Account::list_local_email_async
Add new method and implementation to support breaking search folder
impl out of the ImapDb package.
2019-12-12 10:47:52 +11:00

598 lines
23 KiB
Vala

/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2018-2019 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.
*/
/**
* Account is the basic interface to the user's email account, {@link Folder}s, and various signals,
* monitors, and notifications of account activity.
*
* In addition to providing an interface to the various Folders, Account offers aggregation signals
* indicating changes to all Folders as they're discovered. Because some mail interfaces don't
* provide account-wide notification, these signals may only be fired when the Folder is open,
* which sometimes happens in the background as background synchronization occurs.
*
* Accounts must be opened and closed with {@link open_async} and {@link close_async}. Most
* methods only work if the Account is opened (if not, that will be mentioned in their
* documentation).
*
* A list of all Accounts may be retrieved from the {@link Engine} singleton.
*/
public abstract class Geary.Account : BaseObject, Logging.Source {
/** Number of times to attempt re-authentication. */
internal const uint AUTH_ATTEMPTS_MAX = 3;
/**
* Denotes the account's current status.
*
* @see Account.current_status
* @see ClientService.current_status
*/
[Flags]
public enum Status {
/**
* The account is currently online and operating normally.
*
* This flags will be set when the account's {@link incoming}
* service's {@link ClientService.current_status} is {@link
* ClientService.Status.CONNECTED}.
*/
ONLINE,
/**
* One or of the account's services is degraded.
*
* This flag will be set when one or both of its services has
* encountered a problem. Consult the {@link
* ClientService.current_status} to determine which and the
* exact problem.
*/
SERVICE_PROBLEM;
/** Determines if the {@link ONLINE} flag is set. */
public bool is_online() {
return (this & ONLINE) == ONLINE;
}
/** Determines if the {@link SERVICE_PROBLEM} flag is set. */
public bool has_service_problem() {
return (this & SERVICE_PROBLEM) == SERVICE_PROBLEM;
}
}
/**
* 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}, {@link folders_created}, {@link
* folders_deleted} signals' contracts.
*/
public static Gee.BidirSortedSet<Folder>
sort_by_path(Gee.Collection<Folder> folders) {
Gee.TreeSet<Folder> sorted =
new Gee.TreeSet<Folder>(Account.folder_path_comparator);
sorted.add_all(folders);
return sorted;
}
/**
* Comparator used to sort folders.
*
* @see sort_by_path
*/
public static int folder_path_comparator(Geary.Folder a, Geary.Folder b) {
return a.path.compare_to(b.path);
}
/**
* The account's current configuration.
*/
public AccountInformation information { get; protected set; }
/**
* The account's current status.
*
* This property's value is set based on the {@link
* ClientService.current_status} of the account's {@link incoming}
* and {@link outgoing} services. See {@link Status} for more
* information.
*
* The initial value for this property is {@link Status.ONLINE},
* which may or may not be incorrect. However the once the account
* has been opened, its services will begin checking connectivity
* and the value will be updated to match in due course.
*
* @see ClientService.current_status
*/
public Status current_status { get; protected set; default = ONLINE; }
/**
* The service manager for the incoming email service.
*/
public ClientService incoming { get; private set; }
/**
* The service manager for the outgoing email service.
*/
public ClientService outgoing { get; private set; }
/**
* The contact information store for this account.
*/
public Geary.ContactStore contact_store { get; protected set; }
public ProgressMonitor background_progress { get; protected set; }
public ProgressMonitor search_upgrade_monitor { get; protected set; }
public ProgressMonitor db_upgrade_monitor { get; protected set; }
public ProgressMonitor db_vacuum_monitor { get; protected set; }
public signal void opened();
public signal void closed();
/**
* Emitted to notify the client that some problem has occurred.
*
* The engine uses this signal to report internal errors and other
* issues that the client should notify the user about. The {@link
* ProblemReport} class provides context about the nature of the
* problem itself.
*/
public signal void report_problem(Geary.ProblemReport problem);
/**
* 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 hierarchy to lower in the hierarchy. In other
* words, parents are listed before children, assuming the
* collections are traversed in natural order.
*
* @see sort_by_path
*/
public signal void
folders_available_unavailable(Gee.BidirSortedSet<Folder>? available,
Gee.BidirSortedSet<Folder>? unavailable);
/**
* Fired when new folders have been created.
*
* This is fired in response to new folders appearing, for example
* the user created a new folder. It will be fired after {@link
* folders_available_unavailable} has been fired to mark the
* folders as having been made available.
*
* Folders are ordered for the convenience of the caller from the
* top of the hierarchy to lower in the hierarchy. In other
* words, parents are listed before children, assuming the
* collection is traversed in natural order.
*/
public signal void folders_created(Gee.BidirSortedSet<Geary.Folder> created);
/**
* Fired when existing folders are deleted.
*
* This is fired in response to existing folders being removed,
* for example if the user deleted a folder. it will be fired
* after {@link folders_available_unavailable} has been fired to
* mark the folders as having been made unavailable.
*
* Folders are ordered for the convenience of the caller from the
* top of the hierarchy to lower in the hierarchy. In other
* words, parents are listed before children, assuming the
* collection is traversed in natural order.
*/
public signal void folders_deleted(Gee.BidirSortedSet<Geary.Folder> deleted);
/**
* Fired when a Folder's contents is detected having changed.
*/
public signal void folders_contents_altered(Gee.Collection<Geary.Folder> altered);
/**
* Fired when a Folder's type is detected having changed.
*/
public signal void folders_special_type(Gee.Collection<Geary.Folder> altered);
/**
* Fired when emails are appended to a folder in this account.
*/
public signal void email_appended(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids);
/**
* Fired when emails are inserted to a folder in this account.
*
* @see Folder.email_inserted
*/
public signal void email_inserted(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids);
/**
* Fired when emails are removed from a folder in this account.
*/
public signal void email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids);
/**
* Fired when one or more emails have been locally saved to a folder with
* the full set of Fields.
*/
public signal void email_locally_complete(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids);
/**
* Fired when one or more emails have been discovered (added) to the Folder, but not necessarily
* appended (i.e. old email pulled down due to user request or background fetching).
*/
public signal void email_discovered(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids);
/**
* Fired when the supplied email flags have changed from any folder.
*/
public signal void email_flags_changed(Geary.Folder folder,
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> map);
/** {@inheritDoc} */
public Logging.Flag logging_flags {
get; protected set; default = Logging.Flag.ALL;
}
/** {@inheritDoc} */
public Logging.Source? logging_parent { get { return null; } }
protected Account(AccountInformation information,
ClientService incoming,
ClientService outgoing) {
this.information = information;
this.incoming = incoming;
this.outgoing = outgoing;
incoming.notify["current-status"].connect(
on_service_status_notify
);
outgoing.notify["current-status"].connect(
on_service_status_notify
);
}
/**
* Opens the {@link Account} and makes it and its {@link Folder}s available for use.
*
* @throws EngineError.CORRUPT if the local store is corrupt or unusable
* @throws EngineError.PERMISSIONS if the local store is inaccessible
* @throws EngineError.VERSION if the local store was created or updated for a different
* version of Geary.
*/
public abstract async void open_async(Cancellable? cancellable = null) throws Error;
/**
* Closes the {@link Account}, which makes most its operations unavailable.
*
* This does not delete the Account, merely closes database and network channels.
*
* Returns without error if the Account is already closed.
*/
public abstract async void close_async(Cancellable? cancellable = null) throws Error;
/**
* Returns true if this account is open, else false.
*/
public abstract bool is_open();
/**
* Rebuild the local data stores for this {@link Account}.
*
* This should only be used if {@link open_async} throws {@link EngineError.CORRUPT},
* indicating that the local data store is corrupted and cannot be used.
*
* ''rebuild_async() will delete all local data''.
*
* If the Account is backed by a synchronized copy on the network, it will rebuild its local
* mail store. If not, the data is forever deleted. Hence, it's best to query the user before
* calling this method.
*
* Unlike most methods in Account, this should only be called when the Account is closed.
*/
public abstract async void rebuild_async(Cancellable? cancellable = null) throws Error;
/**
* Returns an email identifier from its serialised form.
*
* This is useful for converting a string representation of a
* email id back into an actual instance of an id. This does not
* guarantee that the email represented by the id will exist.
*
* @see EmailIdentifier.to_variant
* @throws EngineError.BAD_PARAMETERS when the variant is not the
* have the correct type.
*/
public abstract EmailIdentifier to_email_identifier(GLib.Variant serialised)
throws EngineError.BAD_PARAMETERS;
/**
* Returns the folder path from its serialised form.
*
* This is useful for converting a string representation of a
* folder path back into an actual instance of a path. This does
* not guarantee that the folder represented by the path will
* exist.
*
* @see FolderPath.to_variant
* @throws EngineError.BAD_PARAMETERS when the variant is not the
* have the correct type or if no folder root with an appropriate
* label exists.
*/
public abstract FolderPath to_folder_path(GLib.Variant serialised)
throws EngineError.BAD_PARAMETERS;
/**
* Determines if a folder is known to the engine.
*
* This method only considers currently known folders, it does not
* check the remote to see if a previously folder exists.
*/
public virtual bool has_folder(FolderPath path) {
try {
get_folder(path);
return true;
} catch (EngineError.NOT_FOUND err) {
return false;
}
}
/**
* Returns the folder represented by a specific path.
*
* This method only considers currently known folders, it does not
* check the remote to see if a previously folder exists.
*
* @throws EngineError.NOT_FOUND if the folder does not exist.
*/
public abstract Folder get_folder(FolderPath path)
throws EngineError.NOT_FOUND;
/**
* Lists all currently-available folders.
*
* @see list_matching_folders
*/
public abstract Gee.Collection<Folder> list_folders();
/**
* Lists all currently-available folders found a under parent.
*
* If the parent path cannot be found, EngineError.NOT_FOUND is
* thrown. However, the caller should be prepared to deal with an
* empty list being returned instead.
*/
public abstract Gee.Collection<Folder> list_matching_folders(FolderPath? parent)
throws EngineError.NOT_FOUND;
/**
* Returns a folder for the given special folder type, it is exists.
*/
public virtual Geary.Folder? get_special_folder(Geary.SpecialFolderType type){
return traverse<Folder>(list_folders())
.first_matching(f => f.special_folder_type == type);
}
/**
* Returns the Folder object with the given special folder type. The folder will be
* created on the server if it doesn't already exist. An error will be thrown if the
* folder doesn't exist and can't be created. The only valid special folder types that
* can be required are: DRAFTS, SENT, SPAM, and TRASH.
*/
public abstract async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
Cancellable? cancellable = null) throws Error;
/**
* Search the local account for emails referencing a Message-ID value
* (which can appear in the Message-ID header itself, as well as the
* In-Reply-To header, and maybe more places). Fetch the requested fields,
* optionally ignoring emails that don't have the requested fields set.
* Don't include emails that appear in any of the blacklisted folders in
* the result. If null is included in the blacklist, omit emails appearing
* in no folders. Return a map of Email object to a list of FolderPaths
* it's in, which can be null if it's in no folders.
*/
public abstract async Gee.MultiMap<Geary.Email, Geary.FolderPath?>? local_search_message_id_async(
Geary.RFC822.MessageID message_id, Geary.Email.Field requested_fields, bool partial_ok,
Gee.Collection<Geary.FolderPath?>? folder_blacklist, Geary.EmailFlags? flag_blacklist,
Cancellable? cancellable = null) throws Error;
/**
* Return a single email fulfilling the required fields. The email to pull
* is identified by an EmailIdentifier from a previous call to
* local_search_message_id_async() or local_search_async(). Throw
* EngineError.NOT_FOUND if the email isn't found and
* EngineError.INCOMPLETE_MESSAGE if the fields aren't available.
*/
public abstract async Geary.Email local_fetch_email_async(Geary.EmailIdentifier email_id,
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error;
/**
* Return a collection of email with the given identifiers.
*
* The returned collection will be in the same order as the
* natural ordering of the given identifiers.
*
* Throws {@link EngineError.NOT_FOUND} if any email is not found
* and {@link EngineError.INCOMPLETE_MESSAGE} if the fields aren't
* available.
*/
public abstract async Gee.List<Email> list_local_email_async(
Gee.Collection<EmailIdentifier> ids,
Email.Field required_fields,
GLib.Cancellable? cancellable = null
) throws GLib.Error;
/**
* Create a new {@link SearchQuery} for this {@link Account}.
*
* See {@link Geary.SearchQuery.Strategy} for more information about how its interpreted by the
* Engine. In particular, note that it's an advisory parameter only and may have no effect,
* especially on server searches. However, it may also have a dramatic effect on what search
* results are returned and so should be used with some caution. Whether this parameter is
* user-configurable, available through GSettings or another configuration mechanism, or simply
* baked into the caller's code is up to the caller. CONSERVATIVE is designed to be a good
* default.
*
* The resulting object can only be used with calls into this
* account instance.
*/
public abstract async SearchQuery new_search_query(string query,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
throws GLib.Error;
/**
* Performs a search with the given query. Optionally, a list of folders not to search
* can be passed as well as a list of email identifiers to restrict the search to only those messages.
* Returns a list of EmailIdentifiers, or null if there are no results.
* The list is limited to a maximum number of results and starting offset,
* so you can walk the table. limit can be negative to mean "no limit" but
* offset must not be negative.
*/
public abstract async Gee.Collection<Geary.EmailIdentifier>? local_search_async(Geary.SearchQuery query,
int limit = 100, int offset = 0, Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws Error;
/**
* Given a list of mail IDs, returns a set of casefolded words that match for the query.
*/
public abstract async Gee.Set<string>? get_search_matches_async(Geary.SearchQuery query,
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error;
/**
* Return a map of each passed-in email identifier to the set of folders
* that contain it. If an email id doesn't appear in the resulting map,
* it isn't contained in any folders. Return null if the resulting map
* would be empty. Only throw database errors et al., not errors due to
* the email id not being found.
*/
public abstract async Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? get_containing_folders_async(
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error;
/** {@inheritDoc} */
public virtual Logging.State to_logging_state() {
return new Logging.State(this, this.information.id);
}
/** Fires a {@link opened} signal. */
protected virtual void notify_opened() {
opened();
}
/** Fires a {@link closed} signal. */
protected virtual void notify_closed() {
closed();
}
/** Fires a {@link folders_available_unavailable} signal. */
protected virtual void
notify_folders_available_unavailable(Gee.BidirSortedSet<Folder>? available,
Gee.BidirSortedSet<Folder>? unavailable) {
folders_available_unavailable(available, unavailable);
}
/** Fires a {@link folders_created} signal. */
protected virtual void notify_folders_created(Gee.BidirSortedSet<Geary.Folder> created) {
folders_created(created);
}
/** Fires a {@link folders_deleted} signal. */
protected virtual void notify_folders_deleted(Gee.BidirSortedSet<Geary.Folder> deleted) {
folders_deleted(deleted);
}
/** Fires a {@link folders_contents_altered} signal. */
protected virtual void notify_folders_contents_altered(Gee.Collection<Geary.Folder> altered) {
folders_contents_altered(altered);
}
/** Fires a {@link email_appended} signal. */
protected virtual void notify_email_appended(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_appended(folder, ids);
}
/** Fires a {@link email_inserted} signal. */
protected virtual void notify_email_inserted(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_inserted(folder, ids);
}
/** Fires a {@link email_removed} signal. */
protected virtual void notify_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
email_removed(folder, ids);
}
/** Fires a {@link email_locally_complete} signal. */
protected virtual void notify_email_locally_complete(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids) {
email_locally_complete(folder, ids);
}
/** Fires a {@link email_discovered} signal. */
protected virtual void notify_email_discovered(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids) {
email_discovered(folder, ids);
}
/** Fires a {@link email_flags_changed} signal. */
protected virtual void notify_email_flags_changed(Geary.Folder folder,
Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> flag_map) {
email_flags_changed(folder, flag_map);
}
/** Fires a {@link report_problem} signal for this account. */
protected virtual void notify_report_problem(ProblemReport report) {
report_problem(report);
}
/**
* Fires a {@link report_problem} signal for this account.
*/
protected virtual void notify_account_problem(Error? err) {
report_problem(new AccountProblemReport(this.information, err));
}
/** Fires a {@link report_problem} signal for a service for this account. */
protected virtual void notify_service_problem(ServiceInformation service,
Error? err) {
report_problem(
new ServiceProblemReport(this.information, service, err)
);
}
private void on_service_status_notify() {
Status new_status = 0;
// Don't consider service status UNKNOWN to indicate being
// offline, since clients will indicate offline status, but
// not indicate online status. So when at startup, or when
// restarting services, we don't want to cause them to
// spuriously indicate being offline.
if (incoming.current_status != UNREACHABLE) {
new_status |= ONLINE;
}
if (incoming.current_status.is_error() ||
outgoing.current_status.is_error()) {
new_status |= SERVICE_PROBLEM;
}
this.current_status = new_status;
}
}