Reenable displaying sub-messages.
Geary currently displays RFC 822 attachments inline, below the email's
primary message body, using the same HTML chrome for the headers and
email body as for the primary body. Taking the same approach but using
GTK+ widgets meant splitting ConversationMessage up into a
ConversationEmail class that manages the UI for displaying an email in
its entirety, and a ConversationMessage to manage the only header widgets
and webview for displaying an individual RFC 822 message, usable for both
the primary body and any sub-messages. Thus, this is a big change.
One behavioural change is that each sub-message with remote images now
requires individual approval, rather than being dependant on the
containing message's sender and/or approval. This prevents some attacks
e.g. a trusted sender forwarding a spam/malware message, but does not
prevent it if the message is forwarded inline, obviosuly.
* src/client/conversation-viewer/conversation-email.vala (ConversationEmail):
New class for managing the UI for an overall email message. This
replaces the old ConversationMessage and contains much of it's code and
widgets - anything from that class which does not directly support
displaying headers or a message body.
* src/client/conversation-viewer/conversation-message.vala:
(ConversationMessage): Same class as before, but now with its scope
narrowed to only display message headers and body. The draft infobar
remains here rather than being put ConversationEmail where it belongs
since it's bit of a pain to insert in the right place and doesn't
really hurt.
(::email): Moved this property and any code that depends on it to
ConversationEmail.
(::always_load_remote_images): New property passed in via the ctor,
allowing one dependency on the old ::email property to be removed.
(::inlined_content_ids): Moved to ConversationEmail, since that is the
class that keeps track of attachments to display. Add the signal
attachment_displayed_inline to allow ConversationEmail to be notified
of inlined attachments instead.
(::flag_remote_images, ::remember_remote_images): New signals to notify
ConversationEmail that the user has flagged this message or the
message's sender for loading remote images. This is passed through
since in the former's case we may need to set flags on the email
itself, the latter because it is one less use of the contact_store
property, which should be removed from this class at some point.
* src/client/conversation-viewer/conversation-viewer.vala: Chase API
changes from the above. In general, replace use of the term "message"
with "email" since this class is now mostly dealing with
ConversationEmail instances, rather than ConversationMessage instances.
(ConversationViewer::check_mark_read): Only consider the
ConversationEmail's primary message body when checking for visibility
rather than that and any submessages to keep things simple.
(ConversationViewer::show_message, ::hide_message): Renamed to
expand_email/collapse_email respectively since we don't ever actually
hide it. Carry that change on to same methods on ConversationEmail.
* src/engine/rfc822/rfc822-message.vala (Geary.RFC822.Message): Add
get_primary_originator(), almost vermatim from Geary.Email, to support
determining the sender for remembering remote message loading for
senders of sub-emails.
* src/client/components/main-window.vala (MainWindow::set_styling): Fix
background transition for collapsed emails.
* src/client/application/geary-controller.vala: Chase API name changes.
* src/CMakeLists.txt: Include new ConversationEmail source file.
* ui/conversation-email.ui: New UI for ConversationEmail, move the email
action box, attachments box amd sub-messages box here from
conversation-message.ui.
* ui/CMakeLists.txt: Include new UI in compiled resources.
* po/POTFILES.in: Add new UI for transation.
2016-04-19 16:52:34 +10:00
/ *
* Copyright 2016 Software Freedom Conservancy Inc .
2019-03-23 14:22:44 +11:00
* Copyright 2016 - 2019 Michael Gratton < mike @ vee . net >
2011-11-22 18:55:07 -08:00
*
* This software is licensed under the GNU Lesser General Public License
Reenable displaying sub-messages.
Geary currently displays RFC 822 attachments inline, below the email's
primary message body, using the same HTML chrome for the headers and
email body as for the primary body. Taking the same approach but using
GTK+ widgets meant splitting ConversationMessage up into a
ConversationEmail class that manages the UI for displaying an email in
its entirety, and a ConversationMessage to manage the only header widgets
and webview for displaying an individual RFC 822 message, usable for both
the primary body and any sub-messages. Thus, this is a big change.
One behavioural change is that each sub-message with remote images now
requires individual approval, rather than being dependant on the
containing message's sender and/or approval. This prevents some attacks
e.g. a trusted sender forwarding a spam/malware message, but does not
prevent it if the message is forwarded inline, obviosuly.
* src/client/conversation-viewer/conversation-email.vala (ConversationEmail):
New class for managing the UI for an overall email message. This
replaces the old ConversationMessage and contains much of it's code and
widgets - anything from that class which does not directly support
displaying headers or a message body.
* src/client/conversation-viewer/conversation-message.vala:
(ConversationMessage): Same class as before, but now with its scope
narrowed to only display message headers and body. The draft infobar
remains here rather than being put ConversationEmail where it belongs
since it's bit of a pain to insert in the right place and doesn't
really hurt.
(::email): Moved this property and any code that depends on it to
ConversationEmail.
(::always_load_remote_images): New property passed in via the ctor,
allowing one dependency on the old ::email property to be removed.
(::inlined_content_ids): Moved to ConversationEmail, since that is the
class that keeps track of attachments to display. Add the signal
attachment_displayed_inline to allow ConversationEmail to be notified
of inlined attachments instead.
(::flag_remote_images, ::remember_remote_images): New signals to notify
ConversationEmail that the user has flagged this message or the
message's sender for loading remote images. This is passed through
since in the former's case we may need to set flags on the email
itself, the latter because it is one less use of the contact_store
property, which should be removed from this class at some point.
* src/client/conversation-viewer/conversation-viewer.vala: Chase API
changes from the above. In general, replace use of the term "message"
with "email" since this class is now mostly dealing with
ConversationEmail instances, rather than ConversationMessage instances.
(ConversationViewer::check_mark_read): Only consider the
ConversationEmail's primary message body when checking for visibility
rather than that and any submessages to keep things simple.
(ConversationViewer::show_message, ::hide_message): Renamed to
expand_email/collapse_email respectively since we don't ever actually
hide it. Carry that change on to same methods on ConversationEmail.
* src/engine/rfc822/rfc822-message.vala (Geary.RFC822.Message): Add
get_primary_originator(), almost vermatim from Geary.Email, to support
determining the sender for remembering remote message loading for
senders of sub-emails.
* src/client/components/main-window.vala (MainWindow::set_styling): Fix
background transition for collapsed emails.
* src/client/application/geary-controller.vala: Chase API name changes.
* src/CMakeLists.txt: Include new ConversationEmail source file.
* ui/conversation-email.ui: New UI for ConversationEmail, move the email
action box, attachments box amd sub-messages box here from
conversation-message.ui.
* ui/CMakeLists.txt: Include new UI in compiled resources.
* po/POTFILES.in: Add new UI for transation.
2016-04-19 16:52:34 +10:00
* ( version 2.1 or later ) . See the COPYING file in this distribution .
2011-11-22 18:55:07 -08:00
* /
2014-08-28 17:27:50 -07:00
2016-04-22 15:57:07 +10:00
/ * *
2019-06-26 03:35:27 +00:00
* Primary controller for an application instance .
2019-04-19 14:59:35 +10:00
*
2019-10-06 11:12:55 +11:00
* A single instance of this class is constructed by { @ link
* GearyAplication } when the primary application instance is started .
2016-04-22 15:57:07 +10:00
* /
2019-11-13 14:08:37 +11:00
internal class Application . Controller : Geary . BaseObject {
Reimplement in-conversation find.
* src/client/application/geary-controller.vala (GearyController): Remove
ACTION_FIND_NEXT_IN_CONVERSATION and
ACTION_FIND_PREVIOUS_IN_CONVERSATION arctions and callbacks since they
will be taken care of by the search entry & search bar buttons, and
remove from accelerators.ui. Add ACTION_TOGGLE_FIND action to handle
toggling find bar in the same way as the search bar.
* src/client/components/main-toolbar.vala (MainToolbar): Add new button
and infrastrcuture for toggling the find bar.
* src/client/conversation-viewer/conversation-viewer.vala
(ConversationViewer): Convert ::conversation_page to be grid, add new
::conversation_scroller property for the scrollbar, update call
sites. Add props for accessing find widgets, remove old find methods
and add callbacks for handling find start, change, etc.
* src/client/conversation-viewer/conversation-email.vala,
src/client/conversation-viewer/conversation-message.vala: Add methods
for accessing selected text for find.
* src/client/conversation-viewer/conversation-listbox.vala
(ConversationListBox::highlight_search_terms): Updated to return a flag
specifiying whether any search results were found, and to
expand/collapse messsages depending on whether they have any.
* src/client/conversation-viewer/conversation-message.vala
(ConversationMessage::highlight_search_terms): Keep track of how many
results were found, and return that.
* ui/conversation-viewer.ui: Convert conversation_page to be a grid, add
a search bar and search widgets to it, and move conversation
ScrolledWindow to it.
2016-08-22 11:42:54 +10:00
2017-05-15 23:21:32 +02:00
2019-01-01 22:19:24 +11:00
private const uint MAX_AUTH_ATTEMPTS = 3 ;
2018-02-21 16:43:56 +11:00
2020-01-08 14:49:53 +01:00
private const uint CLEANUP_CHECK_AFTER_IDLE_BACKGROUND_MINUTES = 5 ;
2018-10-24 02:14:19 +11:00
2019-10-30 14:26:05 +11:00
/** Determines if conversations can be trashed from the given folder. */
public static bool does_folder_support_trash ( Geary . Folder ? target ) {
return (
target ! = null & &
target . special_folder_type ! = TRASH & &
! target . properties . is_local_only & &
( target as Geary . FolderSupport . Move ) ! = null
) ;
}
2019-11-14 12:17:05 +11:00
/** Determines if folders should be added to main windows. */
public static bool should_add_folder ( Gee . Collection < Geary . Folder > ? all ,
Geary . Folder folder ) {
// if folder is openable, add it
if ( folder . properties . is_openable ! = Geary . Trillian . FALSE )
return true ;
else if ( folder . properties . has_children = = Geary . Trillian . FALSE )
return false ;
// if folder contains children, we must ensure that there is at least one of the same type
Geary . SpecialFolderType type = folder . special_folder_type ;
foreach ( Geary . Folder other in all ) {
if ( other . special_folder_type = = type & & other . path . parent = = folder . path )
return true ;
}
return false ;
}
2019-10-30 14:26:05 +11:00
2019-04-19 14:59:35 +10:00
/** Determines if the controller is open. */
2019-04-10 18:26:22 +10:00
public bool is_open {
get {
2019-11-13 15:19:38 +11:00
return ! this . controller_open . is_cancelled ( ) ;
2019-04-10 18:26:22 +10:00
}
}
2019-10-06 11:12:55 +11:00
/** The primary application instance that owns this controller. */
2019-11-13 10:54:11 +11:00
public weak Client application { get ; private set ; } // circular ref
2016-10-11 01:40:12 +11:00
2019-04-19 14:59:35 +10:00
/** Account management for the application. */
public Accounts . Manager account_manager { get ; private set ; }
2018-05-24 11:14:53 +10:00
2019-04-19 14:59:35 +10:00
/** Certificate management for the application. */
public Application . CertificateManager certificate_manager {
get ; private set ;
2019-01-08 23:38:22 +11:00
}
2019-04-19 14:59:35 +10:00
/** Avatar store for the application. */
public Application . AvatarStore avatars {
2019-03-17 13:51:21 +11:00
get ; private set ; default = new Application . AvatarStore ( ) ;
2018-10-07 13:18:23 +11:00
}
2016-04-22 14:55:40 +10:00
2019-04-19 14:59:35 +10:00
// Primary collection of the application's open accounts
2018-02-21 16:43:56 +11:00
private Gee . Map < Geary . AccountInformation , AccountContext > accounts =
new Gee . HashMap < Geary . AccountInformation , AccountContext > ( ) ;
2019-04-19 14:59:35 +10:00
// Cancelled if the controller is closed
2019-11-13 15:19:38 +11:00
private GLib . Cancellable controller_open ;
2019-04-13 12:00:15 +10:00
2019-04-19 14:59:35 +10:00
private UpgradeDialog upgrade_dialog ;
private Folks . IndividualAggregator folks ;
2019-04-13 12:00:15 +10:00
2019-09-26 23:25:19 +10:00
private PluginManager plugin_manager ;
2019-11-17 11:46:31 +11:00
// List composers that have not yet been closed
2019-11-10 09:35:58 +11:00
private Gee . Collection < Composer . Widget > composer_widgets =
new Gee . LinkedList < Composer . Widget > ( ) ;
// Requested mailto composers not yet fullfulled
private Gee . List < string ? > pending_mailtos = new Gee . ArrayList < string > ( ) ;
2017-05-15 23:21:32 +02:00
2020-01-08 11:00:01 +01:00
// Timeout to do work in idle after all windows have been sent to the background
2020-01-09 12:36:47 +01:00
private Geary . TimeoutManager all_windows_backgrounded_timeout ;
2020-01-08 11:00:01 +01:00
2020-01-08 14:22:51 +01:00
// Whether we're fully in the background
public bool all_windows_backgrounded { get ; private set ; default = false ; }
2016-10-11 01:40:12 +11:00
2019-11-13 15:19:38 +11:00
/ * *
* Emitted when an account is added or is enabled .
*
* This will be emitted after an account is opened and added to
* the controller .
* /
public signal void account_available ( AccountContext context ) ;
/ * *
* Emitted when an account is removed or is disabled .
*
* This will be emitted after the account is removed from the
* controller ' s collection of accounts , but before the { @ link
* AccountContext . cancellable } is cancelled and before the account
* itself is closed .
*
* The ` is_shutdown ` argument will be true if the application is
* in the middle of quitting , otherwise if the account was simply
* removed but the application will keep running , then it will be
* false .
* /
public signal void account_unavailable ( AccountContext context ,
bool is_shutdown ) ;
2016-10-11 01:40:12 +11:00
/ * *
* Constructs a new instance of the controller .
* /
2019-11-13 10:54:11 +11:00
public async Controller ( Client application ,
2019-12-02 20:01:19 +08:00
GLib . Cancellable cancellable )
throws GLib . Error {
2016-10-11 01:40:12 +11:00
this . application = application ;
2019-11-13 15:19:38 +11:00
this . controller_open = cancellable ;
2016-10-11 01:40:12 +11:00
2016-04-27 11:56:25 +10:00
// This initializes the IconFactory, important to do before
// the actions are created (as they refer to some of Geary's
// custom icons)
2019-11-13 10:32:00 +11:00
IconFactory . init ( application . get_resource_directory ( ) ) ;
2016-04-27 11:56:25 +10:00
2013-07-08 14:22:23 -07:00
// Create DB upgrade dialog.
2019-11-18 21:44:48 +11:00
this . upgrade_dialog = new UpgradeDialog ( application ) ;
2016-04-22 14:55:40 +10:00
2016-12-31 14:43:35 +11:00
// Initialise WebKit and WebViews
2019-11-25 21:58:48 +11:00
Components . WebView . init_web_context (
2017-01-04 03:32:50 +11:00
this . application . config ,
2016-12-31 14:43:35 +11:00
this . application . get_web_extensions_dir ( ) ,
2016-12-20 18:21:19 +01:00
this . application . get_user_cache_directory ( ) . get_child ( " web-resources " )
2016-12-31 14:43:35 +11:00
) ;
2019-12-02 20:01:19 +08:00
Components . WebView . load_resources (
this . application . get_user_config_directory ( )
) ;
Composer . WebView . load_resources ( ) ;
ConversationWebView . load_resources ( ) ;
Accounts . SignatureWebView . load_resources ( ) ;
2016-10-16 21:42:48 +11:00
2020-01-09 12:36:47 +01:00
this . all_windows_backgrounded_timeout =
new Geary . TimeoutManager . seconds ( CLEANUP_CHECK_AFTER_IDLE_BACKGROUND_MINUTES * 60 , on_unfocused_idle ) ;
2019-03-15 09:00:52 +11:00
this . folks = Folks . IndividualAggregator . dup ( ) ;
if ( ! this . folks . is_prepared ) {
2019-03-29 17:18:21 +11:00
// Do this in the background since it can take a long time
// on some systems and the GUI shouldn't be blocked by it
2019-03-15 09:00:52 +11:00
this . folks . prepare . begin ( ( obj , res ) = > {
2019-03-29 17:18:21 +11:00
try {
2019-03-15 09:00:52 +11:00
this . folks . prepare . end ( res ) ;
2019-03-29 17:18:21 +11:00
} catch ( GLib . Error err ) {
warning ( " Error preparing Folks: %s " , err . message ) ;
}
} ) ;
2019-02-26 23:55:27 +11:00
}
2016-04-22 14:55:40 +10:00
2019-09-27 02:51:05 +10:00
this . plugin_manager = new PluginManager ( application ) ;
2019-09-27 00:43:08 +10:00
this . plugin_manager . notifications = new NotificationContext (
this . avatars ,
this . get_contact_store_for_account ,
this . should_notify_new_messages
) ;
2019-09-27 01:03:34 +10:00
this . plugin_manager . load ( ) ;
2019-09-26 23:25:19 +10:00
2016-07-08 03:34:00 +02:00
// Migrate configuration if necessary.
2019-12-02 20:01:19 +08:00
Migrate . xdg_config_dir ( this . application . get_user_data_directory ( ) ,
this . application . get_user_config_directory ( ) ) ;
2016-07-08 03:34:00 +02:00
2019-01-08 23:38:22 +11:00
// Hook up cert, accounts and credentials machinery
2019-01-10 17:33:37 +11:00
this . certificate_manager = yield new Application . CertificateManager (
this . application . get_user_data_directory ( ) . get_child ( " pinned-certs " ) ,
cancellable
2019-01-10 13:58:56 +11:00
) ;
2019-01-08 23:38:22 +11:00
2019-12-02 20:01:19 +08:00
SecretMediator ? libsecret = yield new SecretMediator ( cancellable ) ;
2018-12-26 15:27:47 +10:30
2019-11-14 12:17:05 +11:00
application . engine . account_available . connect ( on_account_available ) ;
2018-07-23 14:38:57 +10:00
this . account_manager = new Accounts . Manager (
2018-12-26 15:27:47 +10:30
libsecret ,
2018-05-24 15:31:21 +10:00
this . application . get_user_config_directory ( ) ,
this . application . get_user_data_directory ( )
) ;
2018-06-02 01:04:59 +02:00
this . account_manager . account_added . connect (
on_account_added
2018-05-28 00:54:43 +10:00
) ;
2018-06-02 01:04:59 +02:00
this . account_manager . account_status_changed . connect (
on_account_status_changed
2018-05-28 00:54:43 +10:00
) ;
2018-06-17 18:03:09 +10:00
this . account_manager . account_removed . connect (
on_account_removed
) ;
2018-12-09 16:46:00 +11:00
this . account_manager . report_problem . connect (
on_report_problem
) ;
2017-10-27 16:19:35 +02:00
2019-12-02 20:01:19 +08:00
yield this . account_manager . connect_goa ( cancellable ) ;
2017-10-27 16:19:35 +02:00
2019-11-19 00:49:15 +11:00
// Start loading accounts
2019-12-02 20:01:19 +08:00
yield this . account_manager . load_accounts ( cancellable ) ;
2018-06-17 18:03:09 +10:00
// Expunge any deleted accounts in the background, so we're
// not blocking the app continuing to open.
this . expunge_accounts . begin ( ) ;
2011-11-22 18:55:07 -08:00
}
2018-05-24 11:22:29 +10:00
2019-10-05 19:15:47 +10:00
/** Returns a context for an account, if any. */
2019-11-13 14:04:05 +11:00
internal AccountContext ? get_context_for_account ( Geary . AccountInformation account ) {
2019-10-05 19:15:47 +10:00
return this . accounts . get ( account ) ;
}
2019-11-13 14:04:05 +11:00
/** Returns a read-only collection of contexts each active account. */
internal Gee . Collection < AccountContext > get_account_contexts ( ) {
return this . accounts . values . read_only_view ;
}
2019-11-17 11:46:31 +11:00
/** Closes all windows and accounts, releasing held resources. */
public async void close ( ) {
2019-11-18 11:46:50 +11:00
// Stop listening for account changes up front so we don't
// attempt to add new accounts while shutting down.
this . account_manager . account_added . disconnect (
on_account_added
) ;
this . account_manager . account_status_changed . disconnect (
on_account_status_changed
) ;
this . account_manager . account_removed . disconnect (
on_account_removed
) ;
2019-11-17 11:46:31 +11:00
this . application . engine . account_available . disconnect (
on_account_available
) ;
2019-11-14 12:17:05 +11:00
foreach ( MainWindow window in this . application . get_main_windows ( ) ) {
2019-11-18 11:46:50 +11:00
window . sensitive = false ;
2019-11-14 12:17:05 +11:00
}
2019-11-17 11:46:31 +11:00
// Close any open composers up-front before anything else is
// shut down so any pending operations have a chance to
// complete.
var composer_barrier = new Geary . Nonblocking . CountingSemaphore ( null ) ;
// Take a copy of the collection of composers since
// closing any will cause the underlying collection to change.
var composers = new Gee . LinkedList < Composer . Widget > ( ) ;
composers . add_all ( this . composer_widgets ) ;
foreach ( var composer in composers ) {
if ( composer . current_mode ! = CLOSED ) {
composer_barrier . acquire ( ) ;
composer . close . begin (
( obj , res ) = > {
composer . close . end ( res ) ;
composer_barrier . blind_notify ( ) ;
}
) ;
}
}
try {
yield composer_barrier . wait_async ( ) ;
} catch ( GLib . Error err ) {
debug ( " Error waiting at composer barrier: %s " , err . message ) ;
}
2018-06-17 18:03:09 +10:00
2019-11-17 11:46:31 +11:00
// Now that all composers are closed, we can shut down the
// rest of the client and engine. Cancel internal processes
// first so they don't block shutdown.
2019-11-13 15:19:38 +11:00
this . controller_open . cancel ( ) ;
2018-02-21 16:04:19 +11:00
2019-11-18 11:46:50 +11:00
// Release folder and conversations in main windows before
// closing them so we know they are released before closing
// the accounts
var window_barrier = new Geary . Nonblocking . CountingSemaphore ( null ) ;
2019-11-14 12:17:05 +11:00
foreach ( MainWindow window in this . application . get_main_windows ( ) ) {
2019-11-18 11:46:50 +11:00
window_barrier . acquire ( ) ;
window . select_folder . begin (
null ,
false ,
2019-11-18 20:27:08 +11:00
true ,
2019-11-18 11:46:50 +11:00
( obj , res ) = > {
window . select_folder . end ( res ) ;
window . close ( ) ;
window_barrier . blind_notify ( ) ;
}
) ;
}
try {
yield window_barrier . wait_async ( ) ;
} catch ( GLib . Error err ) {
debug ( " Error waiting at window barrier: %s " , err . message ) ;
2019-11-14 12:17:05 +11:00
}
2018-02-21 16:04:19 +11:00
2019-11-18 11:46:50 +11:00
// Release general resources now there's no more UI
2019-09-27 00:43:08 +10:00
this . plugin_manager . notifications . clear_folders ( ) ;
2019-11-18 11:46:50 +11:00
this . avatars . close ( ) ;
this . pending_mailtos . clear ( ) ;
this . composer_widgets . clear ( ) ;
2018-02-21 16:04:19 +11:00
2019-11-12 12:31:42 +11:00
// Create a copy of known accounts so the loop below does not
2018-02-21 16:43:56 +11:00
// explode if accounts are removed while iterating.
2019-11-12 12:31:42 +11:00
var closing_accounts = new Gee . LinkedList < AccountContext > ( ) ;
closing_accounts . add_all ( this . accounts . values ) ;
2019-11-18 11:46:50 +11:00
var account_barrier = new Geary . Nonblocking . CountingSemaphore ( null ) ;
2019-11-12 12:31:42 +11:00
foreach ( AccountContext context in closing_accounts ) {
2019-11-18 11:46:50 +11:00
account_barrier . acquire ( ) ;
2018-05-24 15:49:52 +10:00
this . close_account . begin (
context . account . information ,
2019-11-05 08:26:45 +11:00
true ,
2018-05-24 15:49:52 +10:00
( obj , ret ) = > {
this . close_account . end ( ret ) ;
2019-11-18 11:46:50 +11:00
account_barrier . blind_notify ( ) ;
2018-05-24 15:49:52 +10:00
}
) ;
2017-11-21 01:13:21 +11:00
}
try {
2019-11-18 11:46:50 +11:00
yield account_barrier . wait_async ( ) ;
} catch ( GLib . Error err ) {
debug ( " Error waiting at account barrier: %s " , err . message ) ;
2017-11-21 01:13:21 +11:00
}
2016-04-22 14:55:40 +10:00
2019-04-19 14:59:35 +10:00
debug ( " Closed Application.Controller " ) ;
2011-11-22 18:55:07 -08:00
}
2016-09-25 01:10:07 +10:00
/ * *
* Opens or queues a new composer addressed to a specific email address .
* /
2016-12-20 18:21:19 +01:00
public void compose ( string ? mailto = null ) {
2019-11-14 12:17:05 +11:00
MainWindow ? window = this . application . last_active_main_window ;
if ( window ! = null & & window . selected_account ! = null ) {
2019-11-09 16:46:41 +11:00
create_compose_widget (
2019-11-14 12:17:05 +11:00
window ,
window . selected_account ,
NEW_MESSAGE ,
mailto ,
null ,
null ,
false
2019-11-09 16:46:41 +11:00
) ;
2019-11-14 12:17:05 +11:00
} else {
// Schedule the send for after we have an account open.
this . pending_mailtos . add ( mailto ) ;
2016-09-25 01:10:07 +10:00
}
}
2019-10-05 10:52:04 +10:00
/ * *
* Opens new composer with an existing message as context .
* /
2019-11-14 12:17:05 +11:00
public void compose_with_context_email ( MainWindow to_show ,
Geary . Account account ,
2019-11-10 09:35:58 +11:00
Composer . Widget . ComposeType type ,
2019-10-05 10:52:04 +10:00
Geary . Email context ,
2019-11-09 16:46:41 +11:00
string ? quote ,
bool is_draft ) {
2019-11-14 12:17:05 +11:00
create_compose_widget (
to_show , account , type , null , context , quote , is_draft
) ;
2019-10-05 10:52:04 +10:00
}
2019-03-14 18:57:47 +11:00
/** Adds a new composer to be kept track of. */
2019-11-10 09:35:58 +11:00
public void add_composer ( Composer . Widget widget ) {
2019-03-14 18:57:47 +11:00
debug ( @" Added composer of type $(widget.compose_type); $(this.composer_widgets.size) composers total " ) ;
2019-11-12 12:28:24 +11:00
widget . destroy . connect_after ( this . on_composer_widget_destroy ) ;
2019-03-14 18:57:47 +11:00
this . composer_widgets . add ( widget ) ;
}
2019-11-03 06:34:36 +11:00
/** Returns a read-only collection of currently open composers .*/
2019-11-10 09:35:58 +11:00
public Gee . Collection < Composer . Widget > get_composers ( ) {
2019-11-03 06:34:36 +11:00
return this . composer_widgets . read_only_view ;
}
/** Opens any pending composers. */
public void process_pending_composers ( ) {
foreach ( string ? mailto in this . pending_mailtos ) {
compose ( mailto ) ;
}
this . pending_mailtos . clear ( ) ;
}
2019-11-12 12:34:50 +11:00
/** Queues the email in a composer for delivery. */
2019-11-12 14:31:53 +11:00
public async void send_composed_email ( Composer . Widget composer ) {
2019-11-12 12:34:50 +11:00
AccountContext ? context = this . accounts . get (
composer . account . information
) ;
if ( context ! = null ) {
try {
yield context . commands . execute (
new SendComposerCommand ( this . application , context , composer ) ,
context . cancellable
) ;
} catch ( GLib . Error err ) {
report_problem ( new Geary . ProblemReport ( err ) ) ;
}
}
}
2019-11-12 15:21:26 +11:00
/** Saves the email in a composer as a draft on the server. */
public async void save_composed_email ( Composer . Widget composer ) {
// XXX this doesn't actually do what it says on the tin, since
// the composer's draft manager is already saving drafts on
// the server. Until we get that saving local-only, this will
// only be around for pushing the composer onto the undo stack
AccountContext ? context = this . accounts . get (
composer . account . information
) ;
if ( context ! = null ) {
try {
yield context . commands . execute (
new SaveComposerCommand ( this , composer ) ,
context . cancellable
) ;
} catch ( GLib . Error err ) {
report_problem ( new Geary . ProblemReport ( err ) ) ;
}
}
}
2019-11-12 14:34:50 +11:00
/** Queues a composer to be discarded. */
public async void discard_composed_email ( Composer . Widget composer ) {
AccountContext ? context = this . accounts . get (
composer . account . information
) ;
if ( context ! = null ) {
try {
yield context . commands . execute (
new DiscardComposerCommand ( this , composer ) ,
context . cancellable
) ;
} catch ( GLib . Error err ) {
report_problem ( new Geary . ProblemReport ( err ) ) ;
}
}
}
2019-04-21 13:48:36 +10:00
/** Displays a problem report when an error has been encountered. */
public void report_problem ( Geary . ProblemReport report ) {
debug ( " Problem reported: %s " , report . to_string ( ) ) ;
if ( report . error = = null | |
! ( report . error . thrown is IOError . CANCELLED ) ) {
MainWindowInfoBar info_bar = new MainWindowInfoBar . for_problem ( report ) ;
info_bar . retry . connect ( on_retry_problem ) ;
2019-11-14 12:17:05 +11:00
this . application . get_active_main_window ( ) . show_infobar ( info_bar ) ;
2019-04-21 13:48:36 +10:00
}
Geary . ServiceProblemReport ? service_report =
report as Geary . ServiceProblemReport ;
if ( service_report ! = null & & service_report . service . protocol = = SMTP ) {
2019-09-27 02:51:05 +10:00
this . application . send_error_notification (
2019-04-21 13:48:36 +10:00
/// Notification title.
_ ( " A problem occurred sending email for %s " ) . printf (
service_report . account . display_name
) ,
/// Notification body
_ ( " Email will not be sent until re-connected " )
) ;
}
}
2019-06-15 16:47:34 +10:00
/** Returns the contact store for an account, if any. */
public Application . ContactStore ?
get_contact_store_for_account ( Geary . Account target ) {
AccountContext ? context = this . accounts . get ( target . information ) ;
return ( context ! = null ) ? context . contacts : null ;
}
2019-10-05 19:50:50 +10:00
/ * *
* Updates flags for a collection of conversations .
*
* If ` prefer_adding ` is true , this will add the flag if not set
* on all conversations or else will remove it . If false , this
* will remove the flag if not set on all conversations or else
* add it .
* /
2019-10-30 22:38:33 +11:00
public async void mark_conversations ( Geary . Folder location ,
2019-10-05 19:50:50 +10:00
Gee . Collection < Geary . App . Conversation > conversations ,
Geary . NamedFlag flag ,
bool prefer_adding )
throws GLib . Error {
Geary . Iterable < Geary . App . Conversation > selecting =
Geary . traverse ( conversations ) ;
Geary . EmailFlags flags = new Geary . EmailFlags ( ) ;
if ( flag . equal_to ( Geary . EmailFlags . UNREAD ) ) {
selecting = selecting . filter ( c = > prefer_adding ^ c . is_unread ( ) ) ;
flags . add ( Geary . EmailFlags . UNREAD ) ;
} else if ( flag . equal_to ( Geary . EmailFlags . FLAGGED ) ) {
selecting = selecting . filter ( c = > prefer_adding ^ c . is_flagged ( ) ) ;
flags . add ( Geary . EmailFlags . FLAGGED ) ;
} else {
throw new Geary . EngineError . UNSUPPORTED (
" Marking as %s is not supported " , flag . to_string ( )
) ;
}
Gee . Collection < Geary . EmailIdentifier > ? messages = null ;
Gee . Collection < Geary . App . Conversation > selected =
selecting . to_linked_list ( ) ;
bool do_add = prefer_adding ^ selected . is_empty ;
if ( selected . is_empty ) {
selected = conversations ;
}
if ( do_add ) {
// Only apply to the latest in-folder message in
// conversations that don't already have the flag, since
// we don't want to flag every message in the conversation
messages = Geary . traverse ( selected ) . map < Geary . EmailIdentifier > (
c = > c . get_latest_recv_email ( IN_FOLDER_OUT_OF_FOLDER ) . id
) . to_linked_list ( ) ;
} else {
// Remove the flag from those that have it
messages = new Gee . LinkedList < Geary . EmailIdentifier > ( ) ;
foreach ( Geary . App . Conversation convo in selected ) {
foreach ( Geary . Email email in
convo . get_emails ( RECV_DATE_DESCENDING ) ) {
if ( email . email_flags ! = null & &
email . email_flags . contains ( flag ) ) {
messages . add ( email . id ) ;
}
}
}
}
2019-10-24 12:50:52 +11:00
yield mark_messages (
2019-10-30 22:38:33 +11:00
location ,
2019-10-24 12:50:52 +11:00
conversations ,
messages ,
do_add ? flags : null ,
do_add ? null : flags
) ;
2019-10-05 19:50:50 +10:00
}
/ * *
* Updates flags for a collection of email .
*
* This should only be used when working with specific messages
* ( for example , marking a specific message in a conversation )
* rather than when working with whole conversations . In that
* case , use { @ link mark_conversations } .
* /
2019-10-30 22:38:33 +11:00
public async void mark_messages ( Geary . Folder location ,
2019-10-24 12:50:52 +11:00
Gee . Collection < Geary . App . Conversation > conversations ,
2019-10-05 19:50:50 +10:00
Gee . Collection < Geary . EmailIdentifier > messages ,
Geary . EmailFlags ? to_add ,
Geary . EmailFlags ? to_remove )
throws GLib . Error {
2019-10-30 22:38:33 +11:00
AccountContext ? context = this . accounts . get ( location . account . information ) ;
2019-10-05 19:50:50 +10:00
if ( context ! = null ) {
2019-10-23 01:17:17 +11:00
yield context . commands . execute (
2019-10-05 19:50:50 +10:00
new MarkEmailCommand (
2019-10-30 22:38:33 +11:00
location ,
conversations ,
2019-10-05 19:50:50 +10:00
messages ,
2019-10-30 22:38:33 +11:00
context . emails ,
2019-10-05 19:50:50 +10:00
to_add ,
to_remove ,
2019-10-24 12:50:52 +11:00
/// Translators: Label for in-app notification
2019-10-05 19:50:50 +10:00
ngettext (
2019-10-24 12:50:52 +11:00
" Conversation marked " ,
" Conversations marked " ,
conversations . size
2019-10-05 19:50:50 +10:00
) ,
2019-10-24 12:50:52 +11:00
/// Translators: Label for in-app notification
2019-10-05 19:50:50 +10:00
ngettext (
2019-10-24 12:50:52 +11:00
" Conversation un-marked " ,
" Conversations un-marked " ,
conversations . size
2019-10-05 19:50:50 +10:00
)
) ,
context . cancellable
) ;
}
}
2019-10-06 10:30:19 +11:00
public async void move_conversations ( Geary . FolderSupport . Move source ,
Geary . Folder destination ,
Gee . Collection < Geary . App . Conversation > conversations )
throws GLib . Error {
AccountContext ? context = this . accounts . get ( source . account . information ) ;
if ( context ! = null ) {
2019-10-23 01:17:17 +11:00
yield context . commands . execute (
2019-10-06 10:30:19 +11:00
new MoveEmailCommand (
source ,
destination ,
2019-10-30 22:38:33 +11:00
conversations ,
2019-10-06 10:30:19 +11:00
to_in_folder_email_ids ( conversations ) ,
2019-10-24 12:53:38 +11:00
/// Translators: Label for in-app
2019-10-06 10:30:19 +11:00
/// notification. String substitution is the name
/// of the destination folder.
ngettext (
" Conversation moved to %s " ,
" Conversations moved to %s " ,
conversations . size
) . printf ( destination . get_display_name ( ) ) ,
2019-10-24 12:53:38 +11:00
/// Translators: Label for in-app
2019-10-06 10:30:19 +11:00
/// notification. String substitution is the name
/// of the source folder.
ngettext (
" Conversation restored to %s " ,
" Conversations restored to %s " ,
conversations . size
) . printf ( source . get_display_name ( ) )
) ,
context . cancellable
) ;
}
}
2019-10-24 12:46:33 +11:00
public async void move_conversations_special ( Geary . Folder source ,
2019-10-06 10:30:19 +11:00
Geary . SpecialFolderType destination ,
Gee . Collection < Geary . App . Conversation > conversations )
throws GLib . Error {
AccountContext ? context = this . accounts . get ( source . account . information ) ;
if ( context ! = null ) {
2019-10-24 12:46:33 +11:00
Command ? command = null ;
Gee . Collection < Geary . EmailIdentifier > messages =
to_in_folder_email_ids ( conversations ) ;
/// Translators: Label for in-app notification. String
/// substitution is the name of the destination folder.
string undone_tooltip = ngettext (
" Conversation restored to %s " ,
" Conversations restored to %s " ,
messages . size
) . printf ( source . get_display_name ( ) ) ;
if ( destination = = ARCHIVE ) {
Geary . FolderSupport . Archive ? archive_source = (
source as Geary . FolderSupport . Archive
2019-10-06 10:30:19 +11:00
) ;
2019-10-24 12:46:33 +11:00
if ( archive_source = = null ) {
throw new Geary . EngineError . UNSUPPORTED (
" Folder does not support archiving: %s " ,
source . to_string ( )
) ;
}
command = new ArchiveEmailCommand (
archive_source ,
2019-10-30 22:38:33 +11:00
conversations ,
2019-10-24 12:46:33 +11:00
messages ,
/// Translators: Label for in-app notification.
ngettext (
" Conversation archived " ,
" Conversations archived " ,
messages . size
) ,
undone_tooltip
) ;
} else {
Geary . FolderSupport . Move ? move_source = (
source as Geary . FolderSupport . Move
) ;
if ( move_source = = null ) {
throw new Geary . EngineError . UNSUPPORTED (
" Folder does not support moving: %s " ,
source . to_string ( )
) ;
}
Geary . Folder ? dest = source . account . get_special_folder (
destination
) ;
if ( dest = = null ) {
throw new Geary . EngineError . NOT_FOUND (
" No folder found for: %s " , destination . to_string ( )
) ;
}
command = new MoveEmailCommand (
move_source ,
2019-10-06 10:30:19 +11:00
dest ,
2019-10-30 22:38:33 +11:00
conversations ,
2019-10-24 12:46:33 +11:00
messages ,
/// Translators: Label for in-app
2019-10-06 10:30:19 +11:00
/// notification. String substitution is the name
/// of the destination folder.
ngettext (
" Conversation moved to %s " ,
" Conversations moved to %s " ,
2019-10-24 12:46:33 +11:00
messages . size
) . printf ( destination . get_display_name ( ) ) ,
undone_tooltip
) ;
}
yield context . commands . execute ( command , context . cancellable ) ;
2019-10-06 10:30:19 +11:00
}
}
2019-10-24 12:46:33 +11:00
public async void move_messages_special ( Geary . Folder source ,
2019-10-06 10:30:19 +11:00
Geary . SpecialFolderType destination ,
2019-10-30 22:38:33 +11:00
Gee . Collection < Geary . App . Conversation > conversations ,
2019-10-06 10:30:19 +11:00
Gee . Collection < Geary . EmailIdentifier > messages )
throws GLib . Error {
AccountContext ? context = this . accounts . get ( source . account . information ) ;
if ( context ! = null ) {
2019-10-24 12:46:33 +11:00
Command ? command = null ;
/// Translators: Label for in-app notification. String
/// substitution is the name of the destination folder.
string undone_tooltip = ngettext (
" Message restored to %s " ,
" Messages restored to %s " ,
messages . size
) . printf ( source . get_display_name ( ) ) ;
if ( destination = = ARCHIVE ) {
Geary . FolderSupport . Archive ? archive_source = (
source as Geary . FolderSupport . Archive
2019-10-06 10:30:19 +11:00
) ;
2019-10-24 12:46:33 +11:00
if ( archive_source = = null ) {
throw new Geary . EngineError . UNSUPPORTED (
" Folder does not support archiving: %s " ,
source . to_string ( )
) ;
}
command = new ArchiveEmailCommand (
archive_source ,
2019-10-30 22:38:33 +11:00
conversations ,
2019-10-24 12:46:33 +11:00
messages ,
/// Translators: Label for in-app notification.
ngettext (
" Message archived " ,
" Messages archived " ,
messages . size
) ,
undone_tooltip
) ;
} else {
Geary . FolderSupport . Move ? move_source = (
source as Geary . FolderSupport . Move
) ;
if ( move_source = = null ) {
throw new Geary . EngineError . UNSUPPORTED (
" Folder does not support moving: %s " ,
source . to_string ( )
) ;
}
2019-10-06 10:30:19 +11:00
2019-10-24 12:46:33 +11:00
Geary . Folder ? dest = source . account . get_special_folder (
destination
) ;
if ( dest = = null ) {
throw new Geary . EngineError . NOT_FOUND (
" No folder found for: %s " , destination . to_string ( )
) ;
}
command = new MoveEmailCommand (
move_source ,
2019-10-06 10:30:19 +11:00
dest ,
2019-10-30 22:38:33 +11:00
conversations ,
2019-10-06 10:30:19 +11:00
messages ,
2019-10-24 12:46:33 +11:00
/// Translators: Label for in-app
2019-10-06 10:30:19 +11:00
/// notification. String substitution is the name
/// of the destination folder.
ngettext (
" Message moved to %s " ,
" Messages moved to %s " ,
messages . size
) . printf ( destination . get_display_name ( ) ) ,
2019-10-24 12:46:33 +11:00
undone_tooltip
) ;
}
yield context . commands . execute ( command , context . cancellable ) ;
2019-10-05 19:50:50 +10:00
}
}
2019-10-06 10:42:03 +11:00
public async void copy_conversations ( Geary . FolderSupport . Copy source ,
Geary . Folder destination ,
Gee . Collection < Geary . App . Conversation > conversations )
throws GLib . Error {
AccountContext ? context = this . accounts . get ( source . account . information ) ;
if ( context ! = null ) {
2019-10-23 01:17:17 +11:00
yield context . commands . execute (
2019-10-06 10:42:03 +11:00
new CopyEmailCommand (
source ,
destination ,
2019-10-30 22:38:33 +11:00
conversations ,
2019-10-06 10:42:03 +11:00
to_in_folder_email_ids ( conversations ) ,
2019-10-24 12:53:38 +11:00
/// Translators: Label for in-app
2019-10-06 10:42:03 +11:00
/// notification. String substitution is the name
/// of the destination folder.
ngettext (
" Conversation labelled as %s " ,
" Conversations labelled as %s " ,
conversations . size
) . printf ( destination . get_display_name ( ) ) ,
2019-10-24 12:53:38 +11:00
/// Translators: Label for in-app
2019-10-06 10:42:03 +11:00
/// notification. String substitution is the name
/// of the destination folder.
ngettext (
" Conversation un-labelled as %s " ,
" Conversations un-labelled as %s " ,
conversations . size
) . printf ( destination . get_display_name ( ) )
) ,
context . cancellable
) ;
}
}
2019-10-06 10:54:52 +11:00
public async void delete_conversations ( Geary . FolderSupport . Remove target ,
Gee . Collection < Geary . App . Conversation > conversations )
throws GLib . Error {
2019-10-30 22:38:33 +11:00
yield delete_messages (
target , conversations , to_in_folder_email_ids ( conversations )
) ;
2019-10-06 10:54:52 +11:00
}
public async void delete_messages ( Geary . FolderSupport . Remove target ,
2019-10-30 22:38:33 +11:00
Gee . Collection < Geary . App . Conversation > conversations ,
2019-10-06 10:54:52 +11:00
Gee . Collection < Geary . EmailIdentifier > messages )
throws GLib . Error {
AccountContext ? context = this . accounts . get ( target . account . information ) ;
if ( context ! = null ) {
2019-10-30 22:38:33 +11:00
Command command = new DeleteEmailCommand (
target , conversations , messages
) ;
2019-10-24 12:42:54 +11:00
command . executed . connect (
( ) = > context . controller_stack . email_removed ( target , messages )
2019-10-06 10:54:52 +11:00
) ;
2019-10-24 12:42:54 +11:00
yield context . commands . execute ( command , context . cancellable ) ;
2019-10-06 10:54:52 +11:00
}
}
2019-10-06 10:56:09 +11:00
public async void empty_folder_special ( Geary . Account source ,
Geary . SpecialFolderType type )
throws GLib . Error {
AccountContext ? context = this . accounts . get ( source . information ) ;
if ( context ! = null ) {
Geary . FolderSupport . Empty ? emptyable = (
source . get_special_folder ( type )
as Geary . FolderSupport . Empty
) ;
if ( emptyable = = null ) {
throw new Geary . EngineError . UNSUPPORTED (
" Special folder type not supported %s " , type . to_string ( )
) ;
}
2019-10-24 12:42:54 +11:00
Command command = new EmptyFolderCommand ( emptyable ) ;
command . executed . connect (
// Not quite accurate, but close enough
( ) = > context . controller_stack . folders_removed (
Geary . Collection . single ( emptyable )
)
2019-10-06 10:56:09 +11:00
) ;
2019-10-24 12:42:54 +11:00
yield context . commands . execute ( command , context . cancellable ) ;
2019-10-06 10:56:09 +11:00
}
}
2019-11-14 12:17:05 +11:00
internal void register_window ( MainWindow window ) {
window . retry_service_problem . connect ( on_retry_service_problem ) ;
window . folder_list . set_new_messages_monitor (
this . plugin_manager . notifications
) ;
}
internal void unregister_window ( MainWindow window ) {
window . retry_service_problem . disconnect ( on_retry_service_problem ) ;
}
2018-06-17 18:03:09 +10:00
/** Expunges removed accounts while the controller remains open. */
internal async void expunge_accounts ( ) {
2018-02-21 16:43:56 +11:00
try {
2019-11-13 15:19:38 +11:00
yield this . account_manager . expunge_accounts ( this . controller_open ) ;
2018-06-17 18:03:09 +10:00
} catch ( GLib . Error err ) {
2019-03-23 14:22:44 +11:00
report_problem ( new Geary . ProblemReport ( err ) ) ;
2018-02-21 16:43:56 +11:00
}
2012-04-17 13:15:46 -07:00
}
2019-11-13 15:19:38 +11:00
private async void open_account ( Geary . Account account ) {
AccountContext context = new AccountContext (
account ,
2019-12-13 12:12:07 +11:00
new Geary . App . SearchFolder ( account , account . local_folder_root ) ,
2019-11-13 15:19:38 +11:00
new Geary . App . EmailStore ( account ) ,
new Application . ContactStore ( account , this . folks )
) ;
this . accounts . set ( account . information , context ) ;
this . upgrade_dialog . add_account ( account , this . controller_open ) ;
2019-01-01 22:19:24 +11:00
account . information . authentication_failure . connect (
on_authentication_failure
) ;
2018-12-27 16:56:11 +11:00
account . information . untrusted_host . connect ( on_untrusted_host ) ;
2018-12-30 22:40:04 +11:00
account . notify [ " current-status " ] . connect (
on_account_status_notify
) ;
2019-11-13 15:19:38 +11:00
account . email_removed . connect ( on_account_email_removed ) ;
account . folders_available_unavailable . connect ( on_folders_available_unavailable ) ;
2013-06-07 16:23:45 -07:00
account . report_problem . connect ( on_report_problem ) ;
2019-11-13 15:19:38 +11:00
Geary . Smtp . ClientService ? smtp = (
account . outgoing as Geary . Smtp . ClientService
) ;
if ( smtp ! = null ) {
smtp . email_sent . connect ( on_sent ) ;
smtp . sending_monitor . start . connect ( on_sending_started ) ;
smtp . sending_monitor . finish . connect ( on_sending_finished ) ;
}
bool retry = false ;
do {
try {
yield account . open_async ( this . controller_open ) ;
retry = false ;
2019-11-18 21:44:48 +11:00
} catch ( GLib . Error open_err ) {
2019-11-13 15:19:38 +11:00
debug ( " Unable to open account %s: %s " , account . to_string ( ) , open_err . message ) ;
if ( open_err is Geary . EngineError . CORRUPT ) {
retry = yield account_database_error_async ( account ) ;
}
if ( ! retry ) {
report_problem (
new Geary . AccountProblemReport (
account . information ,
open_err
)
) ;
this . account_manager . disable_account ( account . information ) ;
this . accounts . unset ( account . information ) ;
}
}
} while ( retry ) ;
account_available ( context ) ;
update_account_status ( ) ;
2013-06-07 16:23:45 -07:00
}
2016-12-11 14:30:59 +01:00
2019-11-05 08:26:45 +11:00
private async void close_account ( Geary . AccountInformation config ,
bool is_shutdown ) {
2018-12-30 22:40:04 +11:00
AccountContext ? context = this . accounts . get ( config ) ;
2018-02-21 16:43:56 +11:00
if ( context ! = null ) {
2019-11-05 08:26:45 +11:00
debug ( " Closing account: %s " , context . account . information . id ) ;
2018-12-30 22:40:04 +11:00
Geary . Account account = context . account ;
2019-11-05 08:26:45 +11:00
// Guard against trying to close the account twice
this . accounts . unset ( account . information ) ;
2018-02-21 16:43:56 +11:00
2019-11-13 15:19:38 +11:00
this . upgrade_dialog . remove_account ( account ) ;
2018-12-30 22:40:04 +11:00
// Stop updating status and showing errors when closing
// the account - the user doesn't care any more
account . report_problem . disconnect ( on_report_problem ) ;
2019-01-01 22:19:24 +11:00
account . information . authentication_failure . disconnect (
on_authentication_failure
) ;
2018-12-27 16:56:11 +11:00
account . information . untrusted_host . disconnect ( on_untrusted_host ) ;
2018-12-30 22:40:04 +11:00
account . notify [ " current-status " ] . disconnect (
on_account_status_notify
) ;
2018-02-21 16:43:56 +11:00
2019-11-05 08:26:45 +11:00
account . email_removed . disconnect ( on_account_email_removed ) ;
account . folders_available_unavailable . disconnect ( on_folders_available_unavailable ) ;
2019-11-08 10:29:31 +11:00
Geary . Smtp . ClientService ? smtp = (
account . outgoing as Geary . Smtp . ClientService
) ;
if ( smtp ! = null ) {
smtp . email_sent . disconnect ( on_sent ) ;
smtp . sending_monitor . start . disconnect ( on_sending_started ) ;
smtp . sending_monitor . finish . disconnect ( on_sending_finished ) ;
}
2019-11-05 08:26:45 +11:00
// Now the account is not in the accounts map, reset any
// status notifications for it
update_account_status ( ) ;
2019-11-13 15:19:38 +11:00
account_unavailable ( context , is_shutdown ) ;
2019-11-05 08:26:45 +11:00
2019-12-13 12:12:07 +11:00
// Stop any background processes
context . search . clear ( ) ;
2019-11-05 08:26:45 +11:00
context . contacts . close ( ) ;
2019-12-13 12:12:07 +11:00
context . cancellable . cancel ( ) ;
2019-11-05 08:26:45 +11:00
// Explicitly close the inbox since we explicitly open it
Geary . Folder ? inbox = context . inbox ;
if ( inbox ! = null ) {
try {
yield inbox . close_async ( null ) ;
} catch ( Error close_inbox_err ) {
debug ( " Unable to close monitored inbox: %s " , close_inbox_err . message ) ;
}
context . inbox = null ;
}
try {
yield account . close_async ( null ) ;
} catch ( Error close_err ) {
debug ( " Unable to close account %s: %s " , account . to_string ( ) , close_err . message ) ;
}
debug ( " Account closed: %s " , account . to_string ( ) ) ;
2013-06-07 16:23:45 -07:00
}
}
2018-02-21 16:43:56 +11:00
2019-01-01 23:22:54 +11:00
private void update_account_status ( ) {
2019-01-24 18:48:30 +11:00
// Start off assuming all accounts are online and error free
// (i.e. no status issues to indicate) and proceed until
// proven incorrect.
Geary . Account . Status effective_status = ONLINE ;
2019-01-07 23:13:30 +11:00
bool has_auth_error = false ;
2019-01-08 23:34:45 +11:00
bool has_cert_error = false ;
2019-01-07 23:13:30 +11:00
Geary . Account ? service_problem_source = null ;
2019-01-01 23:22:54 +11:00
foreach ( AccountContext context in this . accounts . values ) {
2019-01-24 18:48:30 +11:00
Geary . Account . Status status = context . get_effective_status ( ) ;
if ( ! status . is_online ( ) ) {
effective_status & = ~ Geary . Account . Status . ONLINE ;
}
if ( status . has_service_problem ( ) ) {
effective_status | = SERVICE_PROBLEM ;
if ( service_problem_source = = null ) {
service_problem_source = context . account ;
}
2019-01-07 23:13:30 +11:00
}
has_auth_error | = context . authentication_failed ;
2019-01-08 23:34:45 +11:00
has_cert_error | = context . tls_validation_failed ;
2019-01-01 23:22:54 +11:00
}
2019-11-14 12:17:05 +11:00
foreach ( MainWindow window in this . application . get_main_windows ( ) ) {
window . update_account_status (
effective_status ,
has_auth_error ,
has_cert_error ,
service_problem_source
) ;
2017-11-08 18:13:51 +11:00
}
}
2016-10-26 10:27:02 +02:00
2019-01-01 22:19:24 +11:00
private bool is_currently_prompting ( ) {
return this . accounts . values . fold < bool > (
2019-01-08 23:34:45 +11:00
( ctx , seed ) = > (
ctx . authentication_prompting |
ctx . tls_validation_prompting |
seed
) ,
2019-01-01 22:19:24 +11:00
false
) ;
}
private async void prompt_for_password ( AccountContext context ,
Geary . ServiceInformation service ) {
Geary . AccountInformation account = context . account . information ;
bool is_incoming = ( service = = account . incoming ) ;
Geary . Credentials credentials = is_incoming
? account . incoming . credentials
: account . get_outgoing_credentials ( ) ;
bool handled = true ;
2019-02-16 16:33:44 +11:00
if ( context . authentication_attempts > MAX_AUTH_ATTEMPTS | |
2019-01-01 22:19:24 +11:00
credentials = = null ) {
2019-02-16 16:33:44 +11:00
// We have run out of authentication attempts or have
// been asked for creds but don't even have a login. So
// just bail out immediately and flag the account as
// needing attention.
2019-01-01 22:19:24 +11:00
handled = false ;
2019-02-16 16:33:44 +11:00
} else if ( this . account_manager . is_goa_account ( account ) ) {
context . authentication_prompting = true ;
try {
2019-02-18 12:06:06 +11:00
yield account . load_incoming_credentials ( context . cancellable ) ;
yield account . load_outgoing_credentials ( context . cancellable ) ;
2019-02-16 16:33:44 +11:00
} catch ( GLib . Error err ) {
// Bail out right away, but probably should be opening
// the GOA control panel.
handled = false ;
2019-03-23 14:22:44 +11:00
report_problem ( new Geary . AccountProblemReport ( account , err ) ) ;
2019-02-16 16:33:44 +11:00
}
context . authentication_prompting = false ;
2019-01-01 22:19:24 +11:00
} else {
context . authentication_prompting = true ;
PasswordDialog password_dialog = new PasswordDialog (
this . application . get_active_window ( ) ,
account ,
2019-03-06 23:09:03 +11:00
service ,
credentials
2018-12-30 22:40:04 +11:00
) ;
2019-01-01 22:19:24 +11:00
if ( password_dialog . run ( ) ) {
// The update the credentials for the service that the
// credentials actually came from
Geary . ServiceInformation creds_service =
2019-03-06 23:09:03 +11:00
( credentials = = account . incoming . credentials )
2019-01-01 22:19:24 +11:00
? account . incoming
: account . outgoing ;
2019-03-06 23:09:03 +11:00
creds_service . credentials = credentials . copy_with_token (
password_dialog . password
) ;
2019-03-06 23:56:56 +11:00
// Update the remember password pref if changed
bool remember = password_dialog . remember_password ;
if ( creds_service . remember_password ! = remember ) {
creds_service . remember_password = remember ;
account . changed ( ) ;
}
2019-01-01 22:19:24 +11:00
SecretMediator libsecret = ( SecretMediator ) account . mediator ;
try {
2019-03-06 23:09:03 +11:00
// Update the secret using the service where the
// credentials originated, since the service forms
// part of the key's identity
2019-03-06 23:56:56 +11:00
if ( creds_service . remember_password ) {
yield libsecret . update_token (
account , creds_service , context . cancellable
) ;
} else {
yield libsecret . clear_token (
account , creds_service , context . cancellable
) ;
}
2019-01-01 22:19:24 +11:00
} catch ( GLib . IOError . CANCELLED err ) {
// all good
} catch ( GLib . Error err ) {
report_problem (
2019-03-23 14:22:44 +11:00
new Geary . ServiceProblemReport ( account , service , err )
2019-01-01 22:19:24 +11:00
) ;
}
2019-03-06 23:56:56 +11:00
2019-01-01 22:19:24 +11:00
context . authentication_attempts + + ;
} else {
// User cancelled, bail out unconditionally
handled = false ;
}
context . authentication_prompting = false ;
}
2019-03-06 23:53:50 +11:00
if ( handled ) {
2019-03-09 20:15:16 +11:00
try {
yield this . application . engine . update_account_service (
account , service , context . cancellable
) ;
} catch ( GLib . Error err ) {
report_problem (
2019-03-23 14:22:44 +11:00
new Geary . ServiceProblemReport ( account , service , err )
2019-03-09 20:15:16 +11:00
) ;
}
2019-03-06 23:53:50 +11:00
} else {
2019-01-01 22:19:24 +11:00
context . authentication_attempts = 0 ;
context . authentication_failed = true ;
update_account_status ( ) ;
}
}
2019-01-08 23:34:45 +11:00
private async void prompt_untrusted_host ( AccountContext context ,
Geary . ServiceInformation service ,
Geary . Endpoint endpoint ,
GLib . TlsConnection cx ) {
2016-12-20 18:21:19 +01:00
if ( this . application . config . revoke_certs ) {
2019-01-08 23:34:45 +11:00
// XXX
}
context . tls_validation_prompting = true ;
2019-01-08 23:38:22 +11:00
try {
yield this . certificate_manager . prompt_pin_certificate (
2019-11-14 12:17:05 +11:00
this . application . get_active_main_window ( ) ,
2019-01-08 23:38:22 +11:00
context . account . information ,
service ,
endpoint ,
2019-01-09 13:02:06 +11:00
false ,
2019-01-08 23:38:22 +11:00
context . cancellable
) ;
context . tls_validation_failed = false ;
} catch ( Application . CertificateManagerError . UNTRUSTED err ) {
// Don't report an error here, the user simply declined.
context . tls_validation_failed = true ;
} catch ( Application . CertificateManagerError err ) {
// Assume validation is now good, but report the error
// since the cert may not have been saved
context . tls_validation_failed = false ;
report_problem (
new Geary . ServiceProblemReport (
context . account . information ,
service ,
err
)
) ;
}
2019-01-08 23:34:45 +11:00
context . tls_validation_prompting = false ;
update_account_status ( ) ;
}
2019-11-14 12:17:05 +11:00
private void on_account_email_removed ( Geary . Folder folder ,
Gee . Collection < Geary . EmailIdentifier > ids ) {
if ( folder . special_folder_type = = OUTBOX ) {
foreach ( MainWindow window in this . application . get_main_windows ( ) ) {
window . status_bar . deactivate_message ( StatusBar . Message . OUTBOX_SEND_FAILURE ) ;
window . status_bar . deactivate_message ( StatusBar . Message . OUTBOX_SAVE_SENT_MAIL_FAILED ) ;
}
2013-09-17 16:55:03 -07:00
}
}
2019-02-28 09:38:56 +01:00
2013-09-17 16:55:03 -07:00
private void on_sending_started ( ) {
2019-11-14 12:17:05 +11:00
foreach ( MainWindow window in this . application . get_main_windows ( ) ) {
window . status_bar . activate_message ( StatusBar . Message . OUTBOX_SENDING ) ;
}
2013-09-17 16:55:03 -07:00
}
2019-02-28 09:38:56 +01:00
2013-09-17 16:55:03 -07:00
private void on_sending_finished ( ) {
2019-11-14 12:17:05 +11:00
foreach ( MainWindow window in this . application . get_main_windows ( ) ) {
window . status_bar . deactivate_message ( StatusBar . Message . OUTBOX_SENDING ) ;
}
2013-09-17 16:55:03 -07:00
}
2018-02-21 16:43:56 +11:00
2013-09-09 14:44:36 -07:00
// Returns true if the caller should try opening the account again
private async bool account_database_error_async ( Geary . Account account ) {
bool retry = true ;
2019-02-28 09:38:56 +01:00
2013-09-09 14:44:36 -07:00
// give the user two options: reset the Account local store, or exit Geary. A third
// could be done to leave the Account in an unopened state, but we don't currently
// have provisions for that.
2019-11-14 12:17:05 +11:00
QuestionDialog dialog = new QuestionDialog (
this . application . get_active_main_window ( ) ,
2016-07-14 02:22:09 +10:00
_ ( " Unable to open the database for %s " ) . printf ( account . information . id ) ,
2013-09-09 14:44:36 -07:00
_ ( " There was an error opening the local mail database for this account. This is possibly due to corruption of the database file in this directory: \n \n %s \n \n Geary can rebuild the database and re-synchronize with the server or exit. \n \n Rebuilding the database will destroy all local email and its attachments. <b>The mail on the your server will not be affected.</b> " )
2016-07-12 15:55:27 +10:00
. printf ( account . information . data_dir . get_path ( ) ) ,
2013-09-09 14:44:36 -07:00
_ ( " _Rebuild " ) , _ ( " E_xit " ) ) ;
dialog . use_secondary_markup ( true ) ;
switch ( dialog . run ( ) ) {
case Gtk . ResponseType . OK :
// don't use Cancellable because we don't want to interrupt this process
try {
yield account . rebuild_async ( ) ;
} catch ( Error err ) {
2019-11-14 12:17:05 +11:00
ErrorDialog errdialog = new ErrorDialog (
this . application . get_active_main_window ( ) ,
2016-12-15 17:09:24 +01:00
_ ( " Unable to rebuild database for “%s” " ) . printf ( account . information . id ) ,
2013-09-09 14:44:36 -07:00
_ ( " Error during rebuild: \n \n %s " ) . printf ( err . message ) ) ;
2016-10-07 15:03:12 +02:00
errdialog . run ( ) ;
2019-02-28 09:38:56 +01:00
2013-09-09 14:44:36 -07:00
retry = false ;
}
break ;
2019-02-28 09:38:56 +01:00
2013-09-09 14:44:36 -07:00
default :
retry = false ;
break ;
}
2016-10-11 01:40:12 +11:00
2013-09-09 14:44:36 -07:00
return retry ;
}
2016-10-11 01:40:12 +11:00
2018-01-27 16:22:45 +10:30
private bool is_inbox_descendant ( Geary . Folder target ) {
bool is_descendent = false ;
Geary . Account account = target . account ;
2019-10-03 07:52:43 +10:00
Geary . Folder ? inbox = account . get_special_folder ( Geary . SpecialFolderType . INBOX ) ;
2018-01-27 16:22:45 +10:30
if ( inbox ! = null ) {
is_descendent = inbox . path . is_descendant ( target . path ) ;
}
return is_descendent ;
}
private void on_special_folder_type_changed ( Geary . Folder folder ,
Geary . SpecialFolderType old_type ,
Geary . SpecialFolderType new_type ) {
// Update notifications
2019-09-27 00:43:08 +10:00
this . plugin_manager . notifications . remove_folder ( folder ) ;
2018-01-27 16:22:45 +10:30
if ( folder . special_folder_type = = Geary . SpecialFolderType . INBOX | |
( folder . special_folder_type = = Geary . SpecialFolderType . NONE & &
is_inbox_descendant ( folder ) ) ) {
2019-11-14 12:17:05 +11:00
Geary . AccountInformation info = folder . account . information ;
2019-09-27 00:43:08 +10:00
this . plugin_manager . notifications . add_folder (
2018-05-24 11:25:30 +10:00
folder , this . accounts . get ( info ) . cancellable
2018-02-21 16:43:56 +11:00
) ;
2013-04-01 12:50:49 -07:00
}
2013-02-14 16:01:51 -08:00
}
2018-01-27 16:22:45 +10:30
2019-02-19 16:39:11 +11:00
private void on_folders_available_unavailable (
Geary . Account account ,
Gee . BidirSortedSet < Geary . Folder > ? available ,
Gee . BidirSortedSet < Geary . Folder > ? unavailable ) {
2018-02-21 16:43:56 +11:00
AccountContext context = this . accounts . get ( account . information ) ;
2013-01-17 12:50:30 -08:00
if ( available ! = null & & available . size > 0 ) {
foreach ( Geary . Folder folder in available ) {
2019-11-14 12:17:05 +11:00
if ( ! Controller . should_add_folder ( available , folder ) ) {
2017-03-02 12:35:39 +01:00
continue ;
}
2019-11-14 12:17:05 +11:00
folder . special_folder_type_changed . connect (
on_special_folder_type_changed
) ;
2018-01-26 17:12:28 +10:30
2018-02-21 16:43:56 +11:00
GLib . Cancellable cancellable = context . cancellable ;
2018-01-27 16:22:45 +10:30
switch ( folder . special_folder_type ) {
case Geary . SpecialFolderType . INBOX :
2018-02-21 16:43:56 +11:00
if ( context . inbox = = null ) {
context . inbox = folder ;
2013-02-14 16:01:51 -08:00
}
2019-11-14 12:17:05 +11:00
folder . open_async . begin ( NO_DELAY , cancellable ) ;
2018-01-27 16:22:45 +10:30
// Always notify for new messages in the Inbox
2019-09-27 00:43:08 +10:00
this . plugin_manager . notifications . add_folder (
folder , cancellable
) ;
2018-01-27 16:22:45 +10:30
break ;
2018-02-07 15:03:23 +11:00
2018-01-27 16:22:45 +10:30
case Geary . SpecialFolderType . NONE :
2018-02-07 15:03:23 +11:00
// Only notify for new messages in non-special
// descendants of the Inbox
2018-01-27 16:22:45 +10:30
if ( is_inbox_descendant ( folder ) ) {
2019-09-27 00:43:08 +10:00
this . plugin_manager . notifications . add_folder (
folder , cancellable
) ;
2018-01-27 16:22:45 +10:30
}
break ;
2012-05-22 13:44:57 -07:00
}
2011-11-22 18:55:07 -08:00
}
}
2018-02-21 16:43:56 +11:00
2013-02-12 16:17:16 -08:00
if ( unavailable ! = null ) {
2019-02-19 16:39:11 +11:00
Gee . BidirIterator < Geary . Folder > unavailable_iterator =
unavailable . bidir_iterator ( ) ;
2019-07-17 15:06:41 +10:00
bool has_prev = unavailable_iterator . last ( ) ;
while ( has_prev ) {
2019-02-19 16:39:11 +11:00
Geary . Folder folder = unavailable_iterator . get ( ) ;
2019-11-14 12:17:05 +11:00
folder . special_folder_type_changed . disconnect (
on_special_folder_type_changed
) ;
2018-02-21 16:43:56 +11:00
switch ( folder . special_folder_type ) {
case Geary . SpecialFolderType . INBOX :
context . inbox = null ;
2019-09-27 00:43:08 +10:00
this . plugin_manager . notifications . remove_folder ( folder ) ;
2018-02-21 16:43:56 +11:00
break ;
case Geary . SpecialFolderType . NONE :
// Only notify for new messages in non-special
// descendants of the Inbox
if ( is_inbox_descendant ( folder ) ) {
2019-09-27 00:43:08 +10:00
this . plugin_manager . notifications . remove_folder ( folder ) ;
2018-02-21 16:43:56 +11:00
}
break ;
2013-02-12 16:17:16 -08:00
}
2018-02-21 16:43:56 +11:00
2019-07-17 15:06:41 +10:00
has_prev = unavailable_iterator . previous ( ) ;
2013-02-12 16:17:16 -08:00
}
2019-10-24 12:42:54 +11:00
// Notify the command stack that folders have gone away
context . controller_stack . folders_removed ( unavailable ) ;
2013-02-12 16:17:16 -08:00
}
2011-11-22 18:55:07 -08:00
}
2018-02-21 16:43:56 +11:00
2013-03-19 11:47:40 -07:00
private bool should_notify_new_messages ( Geary . Folder folder ) {
2013-02-12 16:17:16 -08:00
// A monitored folder must be selected to squelch notifications;
2012-09-27 19:22:34 -07:00
// if conversation list is at top of display, don't display
// and don't display if main window has top-level focus
2019-11-14 12:17:05 +11:00
MainWindow ? window = this . application . last_active_main_window ;
2019-11-03 06:34:36 +11:00
return (
2019-11-14 12:17:05 +11:00
window ! = null & &
( folder ! = window . selected_folder | |
window . conversation_list_view . vadjustment . value ! = 0.0 | |
! window . has_toplevel_focus )
2019-11-03 06:34:36 +11:00
) ;
2012-09-27 19:22:34 -07:00
}
2019-02-28 09:38:56 +01:00
2012-09-27 19:22:34 -07:00
// Clears messages if conditions are true: anything in should_notify_new_messages() is
2013-03-19 14:03:54 -07:00
// false and the supplied visible messages are visible in the conversation list view
2019-11-03 06:34:36 +11:00
public void clear_new_messages ( string caller ,
Gee . Set < Geary . App . Conversation > ? supplied ) {
2019-11-14 12:17:05 +11:00
MainWindow ? window = this . application . last_active_main_window ;
Geary . Folder ? selected = (
( window ! = null ) ? window . selected_folder : null
) ;
2019-09-27 00:43:08 +10:00
NotificationContext notifications = this . plugin_manager . notifications ;
2019-11-03 06:34:36 +11:00
if ( selected ! = null & & (
! notifications . get_folders ( ) . contains ( selected ) | |
should_notify_new_messages ( selected ) ) ) {
2019-02-28 09:38:56 +01:00
2019-09-27 00:43:08 +10:00
Gee . Set < Geary . App . Conversation > visible =
2019-11-14 12:17:05 +11:00
supplied ? ? window . conversation_list_view . get_visible_conversations ( ) ;
2019-02-28 09:38:56 +01:00
2019-09-27 00:43:08 +10:00
foreach ( Geary . App . Conversation conversation in visible ) {
try {
2019-11-03 06:34:36 +11:00
if ( notifications . are_any_new_messages ( selected ,
2019-09-27 00:43:08 +10:00
conversation . get_email_ids ( ) ) ) {
debug ( " Clearing new messages: %s " , caller ) ;
2019-11-03 06:34:36 +11:00
notifications . clear_new_messages ( selected ) ;
2019-09-27 00:43:08 +10:00
break ;
}
} catch ( Geary . EngineError . NOT_FOUND err ) {
// all good
}
2012-09-27 19:22:34 -07:00
}
}
}
2018-07-07 11:45:29 +10:00
2020-01-08 11:00:01 +01:00
// Track a window receiving focus, for idle background work
public void window_focus_in ( ) {
2020-01-09 12:36:47 +01:00
this . all_windows_backgrounded_timeout . reset ( ) ;
2020-01-08 11:00:01 +01:00
}
// Track a window going unfocused, for idle background work
public void window_focus_out ( ) {
this . all_windows_backgrounded_timeout . start ( ) ;
}
private void on_unfocused_idle ( ) {
// Schedule later, catching cases where work should occur later while still in background
2020-01-09 12:36:47 +01:00
this . all_windows_backgrounded_timeout . reset ( ) ;
2020-01-08 14:22:51 +01:00
this . all_windows_backgrounded = true ;
2020-01-08 11:00:01 +01:00
window_focus_out ( ) ;
debug ( " Checking for backgrounded idle work " ) ;
foreach ( AccountContext context in this . accounts . values ) {
2020-01-08 14:08:52 +01:00
Geary . Account account = context . account ;
account . app_backgrounded_cleanup . begin ( context . cancellable ) ;
2020-01-08 11:00:01 +01:00
}
}
2019-11-12 11:52:49 +11:00
/** Displays a composer on the last active main window. */
internal void show_composer ( Composer . Widget composer ,
2019-11-14 12:17:05 +11:00
Gee . Collection < Geary . EmailIdentifier > ? refers_to ,
MainWindow ? show_on ) {
var target = show_on ;
if ( target = = null ) {
target = this . application . get_active_main_window ( ) ;
}
target . show_composer ( composer , refers_to ) ;
2019-11-12 11:52:49 +11:00
composer . set_focus ( ) ;
}
2019-11-17 11:46:31 +11:00
internal bool check_open_composers ( ) {
var do_quit = true ;
foreach ( var composer in this . composer_widgets ) {
if ( composer . conditional_close ( true , true ) = = CANCELLED ) {
do_quit = false ;
break ;
2013-08-08 18:47:23 -07:00
}
}
2019-11-17 11:46:31 +11:00
return do_quit ;
2012-05-22 14:04:24 -07:00
}
2016-06-21 14:48:24 +10:00
2016-08-22 11:09:25 +02:00
/ * *
2019-08-02 11:02:17 +10:00
* Creates a composer widget .
*
* Depending on the arguments , this can be inline in the
2016-08-22 11:09:25 +02:00
* conversation or as a new window .
2019-08-02 11:02:17 +10:00
*
* @ param compose_type - Whether it ' s a new message , a reply , a
* forwarded mail , . . .
* @ param referred - The mail of which we should copy the from / to / . . .
* addresses
2016-08-22 11:09:25 +02:00
* @ param quote - The quote after the mail body
* @ param mailto - A " mailto: " - link
2019-08-02 11:02:17 +10:00
* @ param is_draft - Whether we ' re starting from a draft ( true ) or
* a new mail ( false )
2016-08-22 11:09:25 +02:00
* /
2019-11-14 12:17:05 +11:00
private void create_compose_widget ( MainWindow show_on ,
Geary . Account account ,
2019-11-10 09:35:58 +11:00
Composer . Widget . ComposeType compose_type ,
2019-11-09 16:46:41 +11:00
string ? mailto ,
Geary . Email ? referred ,
string ? quote ,
bool is_draft ) {
2019-08-02 09:58:04 +10:00
// There's a few situations where we can re-use an existing
// composer, check for these first.
if ( compose_type = = NEW_MESSAGE & & ! is_draft ) {
// We're creating a new message that isn't a draft, if
2019-11-14 12:17:05 +11:00
// there's already an empty composer open, just use
// that
foreach ( Composer . Widget existing in this . composer_widgets ) {
if ( existing ! = null & &
existing . current_mode = = PANED & &
existing . is_blank ) {
existing . present ( ) ;
return ;
}
2019-08-02 09:58:04 +10:00
}
2019-11-09 16:46:41 +11:00
} else if ( compose_type ! = NEW_MESSAGE & & referred ! = null ) {
// A reply/forward was requested, see whether there is
2019-11-14 12:17:05 +11:00
// already an inline message in the target window that is
// either a reply/forward for that message, or there is a
// quote to insert into it.
2019-11-10 09:35:58 +11:00
foreach ( Composer . Widget existing in this . composer_widgets ) {
2019-11-14 12:17:05 +11:00
if ( existing . get_toplevel ( ) = = show_on & &
( existing . current_mode = = INLINE | |
2019-11-12 11:52:49 +11:00
existing . current_mode = = INLINE_COMPACT ) & &
2019-11-09 16:46:41 +11:00
( referred . id in existing . get_referred_ids ( ) | |
2019-08-02 09:58:04 +10:00
quote ! = null ) ) {
2019-11-09 16:46:41 +11:00
try {
existing . append_to_email ( referred , quote , compose_type ) ;
existing . present ( ) ;
return ;
} catch ( Geary . EngineError error ) {
report_problem ( new Geary . ProblemReport ( error ) ) ;
}
2019-08-02 09:58:04 +10:00
}
}
// Can't re-use an existing composer, so need to create a
// new one. Replies must open inline in the main window,
// so we need to ensure there are no composers open there
// first.
2019-11-14 12:17:05 +11:00
if ( ! show_on . close_composer ( true ) ) {
2019-08-02 09:58:04 +10:00
return ;
}
}
2016-07-25 10:33:42 +10:00
2019-11-10 09:35:58 +11:00
Composer . Widget widget ;
2013-08-19 16:39:53 -07:00
if ( mailto ! = null ) {
2019-11-10 09:35:58 +11:00
widget = new Composer . Widget . from_mailto (
2019-10-23 01:00:38 +11:00
this . application , account , mailto
2019-06-13 16:54:13 +10:00
) ;
2013-08-19 16:39:53 -07:00
} else {
2019-11-10 09:35:58 +11:00
widget = new Composer . Widget (
2019-11-09 16:46:41 +11:00
this . application , account , compose_type
2019-06-13 16:54:13 +10:00
) ;
2017-01-16 15:55:38 +11:00
}
2016-09-19 12:20:47 +10:00
2019-03-14 18:57:47 +11:00
add_composer ( widget ) ;
2019-11-12 11:52:49 +11:00
show_composer (
widget ,
2019-11-14 12:17:05 +11:00
referred ! = null ? Geary . Collection . single ( referred . id ) : null ,
show_on
2019-11-12 11:52:49 +11:00
) ;
2016-09-23 23:31:01 +10:00
2019-08-02 11:02:17 +10:00
this . load_composer . begin (
2019-10-23 01:00:38 +11:00
account ,
2019-08-02 11:02:17 +10:00
widget ,
referred ,
2019-11-09 16:46:41 +11:00
is_draft ,
2019-11-03 06:34:36 +11:00
quote
2019-08-02 11:02:17 +10:00
) ;
}
private async void load_composer ( Geary . Account account ,
2019-11-10 09:35:58 +11:00
Composer . Widget widget ,
2019-08-02 11:02:17 +10:00
Geary . Email ? referred = null ,
2019-11-09 16:46:41 +11:00
bool is_draft ,
2019-11-03 06:34:36 +11:00
string ? quote = null ) {
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
Geary . Email ? full = null ;
2019-11-03 06:34:36 +11:00
GLib . Cancellable ? cancellable = null ;
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
if ( referred ! = null ) {
2019-08-02 11:02:17 +10:00
AccountContext ? context = this . accounts . get ( account . information ) ;
if ( context ! = null ) {
2019-11-03 06:34:36 +11:00
cancellable = context . cancellable ;
2019-01-15 12:19:22 +11:00
try {
2019-08-02 11:02:17 +10:00
full = yield context . emails . fetch_email_async (
2019-01-15 12:19:22 +11:00
referred . id ,
2019-11-05 10:59:51 +11:00
Geary . ComposedEmail . REQUIRED_REPLY_FIELDS |
2019-11-10 09:35:58 +11:00
Composer . Widget . REQUIRED_FIELDS ,
2019-11-05 10:59:51 +11:00
NONE ,
2019-08-02 11:02:17 +10:00
cancellable
2019-01-15 12:19:22 +11:00
) ;
} catch ( Error e ) {
message ( " Could not load full message: %s " , e . message ) ;
}
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
}
2016-09-23 23:31:01 +10:00
}
2019-11-05 10:59:51 +11:00
try {
2019-11-09 16:46:41 +11:00
yield widget . load ( full , is_draft , quote , cancellable ) ;
2019-11-05 10:59:51 +11:00
} catch ( GLib . Error err ) {
report_problem ( new Geary . ProblemReport ( err ) ) ;
}
2014-05-13 12:31:26 -07:00
}
Clean up how composer loads content into its web view.
The main gist of this is to ensure that the composer's widgets are
constructed seperately to loading its content, and that we only ever call
ComposerWebView::load_html precisely once per composer instance.
* src/client/composer/composer-widget.vala: Remove referred message,
quote text and draft flag param from constructor signature, move any
calls that loaded data from them to new load method. Don't load
anything into the editor here. Make loading the signature file async,
and call new ComposerWebView::updateSignature method on the editor to
update it.
(ComposerWidget::load): New async message for loading content into the
composer. Move related code from the constructor and GearyController
here, make methods that were previously public for that private
again. Tidy up calls a bit now that we have a single place from which
to do it all, and can understand the process a bit better.
(ComposerWidget::on_editor_key_press_event): Don't reload the editor to
remove the quoted text, call new ComposerWebView::delete_quoted_message
method on it instead.
* src/client/composer/composer-web-view.vala
(ComposerWebView): Add ::delete_quoted_message ::update_signature
methods, thunk to JS.
(ComposerWebView::load_html): Add quote and is_draft parameters,
construct HTML for the composer using apporporate spacing here, instead
of relying on all the disparate parts from doing the right thing.
* src/client/application/geary-controller.vala
(GearyController::create_compose_widget_async): Load composer content
after adding it to the widget hierarchy, set focus only after
everything is set up.
* src/engine/rfc822/rfc822-utils.vala (quote_email_for_reply,
quote_email_for_forward): Don't add extra padding around quoted parts -
let callers manage their own whitespace.
* test/client/components/client-web-view-test-case.vala
(TestCase:load_body_fixture): Make HTML param non-nullable, update
subclasses.
* ui/composer-web-view.js (ComposerPageState): Add ::updateSignature and
::deleteQuotedMessage method stubs.
2017-01-25 11:16:20 +11:00
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Separate composer widget from composer window
In anticipation of inline composition, we need the composer widget to be
separate from the window in which it lives. We introduce a new
interface, ComposerContainer, that the thing that holds to
ComposerWidget must implement.
Basic inline composition
Many of the details don't work, or don't work well, but the basics are
in place.
Allow only a single inline composition at a time
With this, we introduce a dialog when you would try to add another. We
also use this when changing the selected conversation with a composer
open.
Compose new messages inline, with no conversation selected
Hook up composer accelerators only when focus is in composer editor
It would be nice to only activate these accelerators when the composer
has focus generally, but that doesn't seem to be easy to detect.
Only disconnect accelerators if they're connected
Maintain focus when composer is popped out
The selection isn't, though.
Fix Tab focus for embedded composer
There are two things that needed to be fixed: The tab key doesn't
usually advance focus for embedded widgets (huh?), so we handle tab
presses by hand for ComposerWidgets. Also, the EmailEntrys do their own
tab handling, which needs to know about the composer widget, not the
toplevel widget in the embedded case.
Remove close() from ComposerContainer interface
I don't think it was actually doing anything, and it conflicts with the new close() method of Gtk.Window.
2014-02-11 00:26:14 -05:00
private void on_composer_widget_destroy ( Gtk . Widget sender ) {
2019-11-17 11:46:31 +11:00
Composer . Widget ? composer = sender as Composer . Widget ;
if ( composer ! = null ) {
composer_widgets . remove ( ( Composer . Widget ) sender ) ;
debug ( @" Composer type $(composer.compose_type) destroyed; " +
@" $(this.composer_widgets.size) composers remaining " ) ;
2013-08-08 18:47:23 -07:00
}
2012-05-22 14:04:24 -07:00
}
2019-02-24 21:44:03 +11:00
2019-11-08 10:29:31 +11:00
private void on_sent ( Geary . Smtp . ClientService service ,
Geary . RFC822 . Message sent ) {
2019-11-12 12:34:50 +11:00
/// Translators: The label for an in-app notification. The
/// string substitution is a list of recipients of the email.
2017-05-27 14:29:45 +02:00
string message = _ (
2019-11-12 12:34:50 +11:00
" Email sent to %s "
2019-11-08 10:14:04 +11:00
) . printf ( Util . Email . to_short_recipient_display ( sent ) ) ;
2019-09-27 19:29:17 +10:00
Components . InAppNotification notification =
new Components . InAppNotification ( message ) ;
2019-11-14 12:17:05 +11:00
foreach ( MainWindow window in this . application . get_main_windows ( ) ) {
window . add_notification ( notification ) ;
}
2019-11-08 10:29:31 +11:00
AccountContext ? context = this . accounts . get ( service . account ) ;
if ( context ! = null ) {
this . plugin_manager . notifications . email_sent ( context . account , sent ) ;
}
2012-06-25 16:30:23 -07:00
}
2016-04-18 10:17:21 +10:00
2019-10-06 10:30:19 +11:00
private Gee . Collection < Geary . EmailIdentifier >
to_in_folder_email_ids ( Gee . Collection < Geary . App . Conversation > conversations ) {
Gee . Collection < Geary . EmailIdentifier > messages =
new Gee . LinkedList < Geary . EmailIdentifier > ( ) ;
foreach ( Geary . App . Conversation conversation in conversations ) {
foreach ( Geary . Email email in
conversation . get_emails ( RECV_DATE_ASCENDING , IN_FOLDER ) ) {
messages . add ( email . id ) ;
}
}
return messages ;
}
2018-02-21 16:43:56 +11:00
private void on_account_available ( Geary . AccountInformation info ) {
Geary . Account ? account = null ;
2013-09-19 17:58:04 -07:00
try {
2019-11-14 14:44:30 +11:00
account = this . application . engine . get_account ( info ) ;
} catch ( GLib . Error error ) {
report_problem ( new Geary . ProblemReport ( error ) ) ;
warning (
" Error creating account %s instance: %s " ,
info . id ,
error . message
) ;
2018-02-21 16:43:56 +11:00
}
if ( account ! = null ) {
2019-11-13 15:19:38 +11:00
this . open_account . begin ( account ) ;
2013-09-19 17:58:04 -07:00
}
}
2016-10-11 01:40:12 +11:00
2018-06-02 01:04:59 +02:00
private void on_account_added ( Geary . AccountInformation added ,
2018-07-23 14:38:57 +10:00
Accounts . Manager . Status status ) {
if ( status = = Accounts . Manager . Status . ENABLED ) {
2018-06-02 01:04:59 +02:00
try {
this . application . engine . add_account ( added ) ;
} catch ( GLib . Error err ) {
2019-03-23 14:22:44 +11:00
report_problem ( new Geary . AccountProblemReport ( added , err ) ) ;
2018-06-02 01:04:59 +02:00
}
2018-05-28 00:54:43 +10:00
}
}
2018-06-02 01:04:59 +02:00
private void on_account_status_changed ( Geary . AccountInformation changed ,
2018-07-23 14:38:57 +10:00
Accounts . Manager . Status status ) {
2018-06-02 01:04:59 +02:00
switch ( status ) {
2018-07-23 14:38:57 +10:00
case Accounts . Manager . Status . ENABLED :
2019-11-14 14:44:30 +11:00
if ( ! this . application . engine . has_account ( changed ) ) {
2018-06-02 01:04:59 +02:00
try {
this . application . engine . add_account ( changed ) ;
} catch ( GLib . Error err ) {
2019-03-23 14:22:44 +11:00
report_problem ( new Geary . AccountProblemReport ( changed , err ) ) ;
2018-06-02 01:04:59 +02:00
}
}
break ;
2018-07-23 14:38:57 +10:00
case Accounts . Manager . Status . UNAVAILABLE :
case Accounts . Manager . Status . DISABLED :
2019-11-14 14:44:30 +11:00
if ( this . application . engine . has_account ( changed ) ) {
2018-06-02 01:04:59 +02:00
this . close_account . begin (
changed ,
2019-11-05 08:26:45 +11:00
false ,
2018-06-02 01:04:59 +02:00
( obj , res ) = > {
this . close_account . end ( res ) ;
try {
this . application . engine . remove_account ( changed ) ;
} catch ( GLib . Error err ) {
report_problem (
2019-03-23 14:22:44 +11:00
new Geary . AccountProblemReport ( changed , err )
2018-06-02 01:04:59 +02:00
) ;
}
}
) ;
}
break ;
}
2018-05-28 00:54:43 +10:00
}
2018-06-17 18:03:09 +10:00
private void on_account_removed ( Geary . AccountInformation removed ) {
debug ( " %s: Closing account for removal " , removed . id ) ;
this . close_account . begin (
removed ,
2019-11-05 08:26:45 +11:00
false ,
2018-06-17 18:03:09 +10:00
( obj , res ) = > {
this . close_account . end ( res ) ;
debug ( " %s: Account closed " , removed . id ) ;
try {
this . application . engine . remove_account ( removed ) ;
debug ( " %s: Account removed from engine " , removed . id ) ;
} catch ( GLib . Error err ) {
report_problem (
2019-03-23 14:22:44 +11:00
new Geary . AccountProblemReport ( removed , err )
2018-06-17 18:03:09 +10:00
) ;
}
}
) ;
}
2019-01-01 23:22:54 +11:00
private void on_report_problem ( Geary . ProblemReport problem ) {
report_problem ( problem ) ;
}
private void on_retry_problem ( MainWindowInfoBar info_bar ) {
Geary . ServiceProblemReport ? service_report =
info_bar . report as Geary . ServiceProblemReport ;
if ( service_report ! = null ) {
AccountContext ? context = this . accounts . get ( service_report . account ) ;
if ( context ! = null & & context . account . is_open ( ) ) {
switch ( service_report . service . protocol ) {
case Geary . Protocol . IMAP :
context . account . incoming . restart . begin ( context . cancellable ) ;
break ;
case Geary . Protocol . SMTP :
2019-03-03 22:07:40 +11:00
context . account . outgoing . restart . begin ( context . cancellable ) ;
2019-01-01 23:22:54 +11:00
break ;
}
}
}
}
private void on_account_status_notify ( ) {
update_account_status ( ) ;
}
private void on_authentication_failure ( Geary . AccountInformation account ,
Geary . ServiceInformation service ) {
AccountContext ? context = this . accounts . get ( account ) ;
if ( context ! = null & & ! is_currently_prompting ( ) ) {
this . prompt_for_password . begin ( context , service ) ;
}
}
2018-12-27 16:56:11 +11:00
private void on_untrusted_host ( Geary . AccountInformation account ,
Geary . ServiceInformation service ,
Geary . Endpoint endpoint ,
TlsConnection cx ) {
2019-01-08 23:34:45 +11:00
AccountContext ? context = this . accounts . get ( account ) ;
if ( context ! = null & & ! is_currently_prompting ( ) ) {
this . prompt_untrusted_host . begin ( context , service , endpoint , cx ) ;
}
2018-12-27 16:56:11 +11:00
}
2019-01-01 23:22:54 +11:00
private void on_retry_service_problem ( Geary . ClientService . Status type ) {
2019-01-08 23:34:45 +11:00
bool has_restarted = false ;
2019-01-07 23:47:11 +11:00
foreach ( AccountContext context in this . accounts . values ) {
2019-01-01 23:22:54 +11:00
Geary . Account account = context . account ;
2019-01-07 23:47:11 +11:00
if ( account . current_status . has_service_problem ( ) & &
( account . incoming . current_status = = type | |
account . outgoing . current_status = = type ) ) {
Geary . ClientService service =
( account . incoming . current_status = = type )
? account . incoming
: account . outgoing ;
2019-01-08 23:34:45 +11:00
bool do_restart = true ;
2019-01-07 23:47:11 +11:00
switch ( type ) {
case AUTHENTICATION_FAILED :
2019-01-08 23:34:45 +11:00
if ( has_restarted ) {
2019-01-07 23:47:11 +11:00
// Only restart at most one at a time, so we
// don't attempt to re-auth multiple bad
// accounts at once.
2019-01-08 23:34:45 +11:00
do_restart = false ;
2019-01-07 23:47:11 +11:00
} else {
// Reset so the infobar does not show up again
context . authentication_failed = false ;
2019-01-08 23:34:45 +11:00
}
break ;
case TLS_VALIDATION_FAILED :
if ( has_restarted ) {
// Only restart at most one at a time, so we
// don't attempt to re-pin multiple bad
// accounts at once.
do_restart = false ;
} else {
// Reset so the infobar does not show up again
context . tls_validation_failed = false ;
2019-01-07 23:47:11 +11:00
}
break ;
}
2019-01-01 23:22:54 +11:00
2019-01-08 23:34:45 +11:00
if ( do_restart ) {
has_restarted = true ;
2019-01-07 23:47:11 +11:00
service . restart . begin ( context . cancellable ) ;
}
2019-01-01 23:22:54 +11:00
}
}
}
Make ConversationMonitor more robust with no/changing connectivity.
Conversation monitor was built around older assumptions of how a folder's
remote connections work - that once a folder opens it will likely also
eventually establish a remote connection, that once the connection is up
it will hang around, and so on.
This patch removes any public notion of (re)seeding, since it can't be
relied to actually happen over the course of the session, ensures that
all folder operations are local-only when the folder does not have a
working remote connection so it doesn't block, and take the opportunity
to reorganise and clean up the monitor API and documentation comments.
* src/engine/app/app-conversation-monitor.vala (ConversationMonitor):
Remove seed signals, and don't bother running an initial reseed if the
folder is already open, since the fill operation will cause any locally
incomplete messages to be filled out from the report. Manage and use an
internal Cancellable for cancelling internal operations when shutting
down. Construct a queue only when starting to monitor conversations,
delete it when stopping. Move as much operation-specific code into the
operations themselves as reasonably possible, making some methods
internal so thy can be accessed from the ops. Ensure all folder listing
operations specify LOCAL_ONLY when the remote is not open. Removed
LocalLoadOperation since that is now redundant. Update the API for
accessing conversations to match Gee conventions and update call
sites. Update documentation comments. Hook back up to locally-complete
signals so we don't miss emails being filled out by the prefetcher, for
now.
* src/engine/app/conversation-monitor/app-conversation-set.vala
(ConversationSet): Rename conversations property to match Gee
conventions, update call sites.
* src/engine/app/conversation-monitor/app-conversation-operation.vala
(ConversationOperation): Allow operations to specify if they should
allow duplicates, and allow the execution method to throw errors, so
they can be handled in a uniform way.
* src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
(ConversationOperationQueue): Accept progress monitor property as a
ctor arg rather than constructing on itself, so it is tied to the
life-cycle of the ConversationMonitor rather than the queue. Add a
signal for notifying of errors thrown when running operations, and use
the new operation-independent support for determining if duplicates
should be queued.
* src/engine/app/conversation-monitor/app-fill-window-operation.vala
(FillWindowOperation): Enforce a maximum window size as well as minimum
to keep loading large windows semi-responsive. Remove code to handle
inserts now that they are handled by their own op.
* src/engine/app/conversation-monitor/app-insert-operation.vala
(InsertOperation): New operation to manage inserts, handle it them by
simply adding them to the conversation if they are newer than the
oldest message, rather that relisting all loaded messages.
2018-03-03 10:56:29 +11:00
2017-11-08 18:13:51 +11:00
}
2019-10-05 19:50:50 +10:00
2019-11-13 14:04:05 +11:00
/ * *
* Collects application state related to a single open account .
* /
internal class Application . AccountContext : Geary . BaseObject {
/** The account for this context. */
public Geary . Account account { get ; private set ; }
/** The account's Inbox folder */
public Geary . Folder ? inbox = null ;
2019-12-13 12:12:07 +11:00
/** The account's search folder */
public Geary . App . SearchFolder search = null ;
2019-11-13 14:04:05 +11:00
/** The account's email store */
public Geary . App . EmailStore emails { get ; private set ; }
/** The account's contact store */
public ContactStore contacts { get ; private set ; }
/** The account's application command stack. */
public CommandStack commands {
get { return this . controller_stack ; }
}
/** A cancellable tied to the life-cycle of the account. */
public Cancellable cancellable {
get ; private set ; default = new Cancellable ( ) ;
}
/** The account's application command stack. */
internal ControllerCommandStack controller_stack {
get ; protected set ; default = new ControllerCommandStack ( ) ;
}
/** Determines if the account has an authentication problem. */
internal bool authentication_failed {
get ; private set ; default = false ;
}
/** Determines if the account is prompting for a pasword. */
internal bool authentication_prompting {
get ; private set ; default = false ;
}
/** Determines if currently prompting for a password. */
internal uint authentication_attempts {
get ; private set ; default = 0 ;
}
/** Determines if any TLS certificate errors have been seen. */
internal bool tls_validation_failed {
get ; private set ; default = false ;
}
/** Determines if currently prompting about TLS certificate errors. */
internal bool tls_validation_prompting {
get ; private set ; default = false ;
}
public AccountContext ( Geary . Account account ,
2019-12-13 12:12:07 +11:00
Geary . App . SearchFolder search ,
2019-11-13 14:04:05 +11:00
Geary . App . EmailStore emails ,
Application . ContactStore contacts ) {
this . account = account ;
2019-12-13 12:12:07 +11:00
this . search = search ;
2019-11-13 14:04:05 +11:00
this . emails = emails ;
this . contacts = contacts ;
}
/** Returns the current effective status for the account. */
public Geary . Account . Status get_effective_status ( ) {
Geary . Account . Status current = this . account . current_status ;
Geary . Account . Status effective = 0 ;
if ( current . is_online ( ) ) {
effective | = ONLINE ;
}
if ( current . has_service_problem ( ) ) {
// Only retain this flag if the problem isn't auth or
// cert related, that is handled elsewhere.
Geary . ClientService . Status incoming =
account . incoming . current_status ;
Geary . ClientService . Status outgoing =
account . outgoing . current_status ;
if ( incoming ! = AUTHENTICATION_FAILED & &
incoming ! = TLS_VALIDATION_FAILED & &
outgoing ! = AUTHENTICATION_FAILED & &
outgoing ! = TLS_VALIDATION_FAILED ) {
effective | = SERVICE_PROBLEM ;
}
}
return effective ;
}
}
2019-10-24 12:42:54 +11:00
/** Base class for all application controller commands. */
internal class Application . ControllerCommandStack : CommandStack {
2019-10-31 16:13:37 +11:00
private EmailCommand ? last_executed = null ;
/** {@inheritDoc} */
public override async void execute ( Command target ,
GLib . Cancellable ? cancellable )
throws GLib . Error {
// Guard against things like Delete being held down by only
// executing a command if it is different to the last one.
if ( this . last_executed = = null | | ! this . last_executed . equal_to ( target ) ) {
this . last_executed = target as EmailCommand ;
yield base . execute ( target , cancellable ) ;
}
}
/** {@inheritDoc} */
public override async void undo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
this . last_executed = null ;
yield base . undo ( cancellable ) ;
}
/** {@inheritDoc} */
public override async void redo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
this . last_executed = null ;
yield base . redo ( cancellable ) ;
}
2019-10-24 12:42:54 +11:00
/ * *
* Notifies the stack that one or more folders were removed .
*
* This will cause any commands involving the given folder to be
* removed from the stack . It should only be called as a response
* to un - recoverable changes , e . g . when the server notifies that a
* folder has been removed .
* /
internal void folders_removed ( Gee . Collection < Geary . Folder > removed ) {
Gee . Iterator < Command > commands = this . undo_stack . iterator ( ) ;
while ( commands . next ( ) ) {
EmailCommand ? email = commands . get ( ) as EmailCommand ;
if ( email ! = null ) {
if ( email . folders_removed ( removed ) = = REMOVE ) {
commands . remove ( ) ;
}
}
}
}
/ * *
* Notifies the stack that email was removed from a folder .
*
* This will cause any commands involving the given email
* identifiers to be removed from commands where they are present ,
* potentially also causing the command to be removed from the
* stack . It should only be called as a response to un - recoverable
* changes , e . g . when the server notifies that an email has been
* removed as a result of some other client removing it , or the
* message being deleted completely .
* /
internal void email_removed ( Geary . Folder location ,
Gee . Collection < Geary . EmailIdentifier > targets ) {
Gee . Iterator < Command > commands = this . undo_stack . iterator ( ) ;
while ( commands . next ( ) ) {
EmailCommand ? email = commands . get ( ) as EmailCommand ;
if ( email ! = null ) {
if ( email . email_removed ( location , targets ) = = REMOVE ) {
commands . remove ( ) ;
}
}
}
}
}
2019-10-30 22:38:33 +11:00
/** Base class for email-related commands. */
public abstract class Application . EmailCommand : Command {
2019-10-24 12:42:54 +11:00
/** Specifies a command's response to external mail state changes. */
public enum StateChangePolicy {
/** The change can be ignored */
IGNORE ,
/** The command is no longer valid and should be removed */
REMOVE ;
}
2019-10-30 22:38:33 +11:00
/ * *
* Returns the folder where the command was initially executed .
*
* This is used by the main window to return to the folder where
* the command was first carried out .
* /
public Geary . Folder location {
get ; protected set ;
}
/ * *
* Returns the conversations which the command was initially applied to .
*
* This is used by the main window to return to the conversation where
* the command was first carried out .
* /
public Gee . Collection < Geary . App . Conversation > conversations {
get ; private set ;
}
/ * *
* Returns the email which the command was initially applied to .
*
* This is used by the main window to return to the conversation where
* the command was first carried out .
* /
public Gee . Collection < Geary . EmailIdentifier > email {
get ; private set ;
}
private Gee . Collection < Geary . App . Conversation > mutable_conversations ;
private Gee . Collection < Geary . EmailIdentifier > mutable_email ;
protected EmailCommand ( Geary . Folder location ,
Gee . Collection < Geary . App . Conversation > conversations ,
Gee . Collection < Geary . EmailIdentifier > email ) {
this . location = location ;
this . conversations = conversations . read_only_view ;
this . email = email . read_only_view ;
this . mutable_conversations = conversations ;
this . mutable_email = email ;
}
2019-10-31 16:13:37 +11:00
public override bool equal_to ( Command other ) {
if ( this = = other ) {
return true ;
}
if ( this . get_type ( ) ! = other . get_type ( ) ) {
return false ;
}
EmailCommand ? other_email = other as EmailCommand ;
if ( other_email = = null ) {
return false ;
}
return (
this . location = = other_email . location & &
this . conversations . size = = other_email . conversations . size & &
this . email . size = = other_email . email . size & &
this . conversations . contains_all ( other_email . conversations ) & &
this . email . contains_all ( other_email . email )
) ;
}
2019-10-24 12:42:54 +11:00
/ * *
* Determines the command ' s response when a folder is removed .
*
* This is called when some external means ( such as another
* command , or another email client altogether ) has caused a
* folder to be removed .
*
* The returned policy will determine if the command is unaffected
* by the change and hence can remain on the stack , or is no
* longer valid and hence must be removed .
* /
2019-10-30 22:38:33 +11:00
internal virtual StateChangePolicy folders_removed (
2019-10-24 12:42:54 +11:00
Gee . Collection < Geary . Folder > removed
2019-10-30 22:38:33 +11:00
) {
return (
this . location in removed
? StateChangePolicy . REMOVE
: StateChangePolicy . IGNORE
) ;
}
2019-10-24 12:42:54 +11:00
/ * *
* Determines the command ' s response when email is removed .
*
* This is called when some external means ( such as another
* command , or another email client altogether ) has caused a
* email in a folder to be removed .
*
* The returned policy will determine if the command is unaffected
* by the change and hence can remain on the stack , or is no
* longer valid and hence must be removed .
* /
2019-10-30 22:38:33 +11:00
internal virtual StateChangePolicy email_removed (
2019-10-24 12:42:54 +11:00
Geary . Folder location ,
Gee . Collection < Geary . EmailIdentifier > targets
2019-10-30 22:38:33 +11:00
) {
StateChangePolicy ret = IGNORE ;
if ( this . location = = location ) {
// Any removed email should have already been removed from
// their conversations by the time we here, so just remove
// any conversations that don't have any messages left.
Gee . Iterator < Geary . App . Conversation > conversations =
this . mutable_conversations . iterator ( ) ;
while ( conversations . next ( ) ) {
var conversation = conversations . get ( ) ;
if ( ! conversation . has_any_non_deleted_email ( ) ) {
conversations . remove ( ) ;
}
}
// Update message set to remove all removed messages
this . mutable_email . remove_all ( targets ) ;
// If we have no more conversations or messages, then the
// command won't be able to do anything and should be
// removed.
if ( this . mutable_conversations . is_empty | |
this . mutable_email . is_empty ) {
ret = REMOVE ;
}
}
return ret ;
}
2019-10-24 12:42:54 +11:00
}
2019-10-22 23:40:31 +11:00
/ * *
* Mixin for trivial application commands .
*
* Trivial commands should not cause a notification to be shown when
* initially executed .
* /
public interface Application . TrivialCommand : Command {
}
2019-10-30 22:38:33 +11:00
private class Application . MarkEmailCommand : TrivialCommand , EmailCommand {
2019-10-05 19:50:50 +10:00
private Geary . App . EmailStore store ;
private Geary . EmailFlags ? to_add ;
private Geary . EmailFlags ? to_remove ;
2019-10-30 22:38:33 +11:00
public MarkEmailCommand ( Geary . Folder location ,
Gee . Collection < Geary . App . Conversation > conversations ,
2019-10-05 19:50:50 +10:00
Gee . Collection < Geary . EmailIdentifier > messages ,
2019-10-30 22:38:33 +11:00
Geary . App . EmailStore store ,
2019-10-05 19:50:50 +10:00
Geary . EmailFlags ? to_add ,
Geary . EmailFlags ? to_remove ,
string ? executed_label = null ,
string ? undone_label = null ) {
2019-10-30 22:38:33 +11:00
base ( location , conversations , messages ) ;
2019-10-05 19:50:50 +10:00
this . store = store ;
this . to_add = to_add ;
this . to_remove = to_remove ;
this . executed_label = executed_label ;
this . undone_label = undone_label ;
}
public override async void execute ( GLib . Cancellable ? cancellable )
throws GLib . Error {
yield this . store . mark_email_async (
2019-10-30 22:38:33 +11:00
this . email , this . to_add , this . to_remove , cancellable
2019-10-05 19:50:50 +10:00
) ;
}
public override async void undo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
yield this . store . mark_email_async (
2019-10-30 22:38:33 +11:00
this . email , this . to_remove , this . to_add , cancellable
2019-10-24 12:42:54 +11:00
) ;
}
2019-10-31 16:13:37 +11:00
public override bool equal_to ( Command other ) {
if ( ! base . equal_to ( other ) ) {
return false ;
}
MarkEmailCommand other_mark = ( MarkEmailCommand ) other ;
return (
( ( this . to_add = = other_mark . to_add ) | |
( this . to_add ! = null & &
other_mark . to_add ! = null & &
this . to_add . equal_to ( other_mark . to_add ) ) ) & &
( ( this . to_remove = = other_mark . to_remove ) | |
( this . to_remove ! = null & &
other_mark . to_remove ! = null & &
this . to_remove . equal_to ( other_mark . to_remove ) ) )
) ;
}
2019-10-05 19:50:50 +10:00
}
2019-10-30 22:38:33 +11:00
private abstract class Application . RevokableCommand : EmailCommand {
2019-10-06 10:30:19 +11:00
public override bool can_undo {
get { return this . revokable ! = null & & this . revokable . valid ; }
}
private Geary . Revokable ? revokable = null ;
2019-10-30 22:38:33 +11:00
protected RevokableCommand ( Geary . Folder location ,
Gee . Collection < Geary . App . Conversation > conversations ,
Gee . Collection < Geary . EmailIdentifier > email ) {
base ( location , conversations , email ) ;
}
2019-10-06 10:30:19 +11:00
public override async void execute ( GLib . Cancellable ? cancellable )
throws GLib . Error {
set_revokable ( yield execute_impl ( cancellable ) ) ;
if ( this . revokable ! = null & & this . revokable . valid ) {
yield this . revokable . commit_async ( cancellable ) ;
}
}
public override async void undo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
if ( this . revokable = = null ) {
throw new Geary . EngineError . UNSUPPORTED (
" Cannot undo command, no revokable available "
) ;
}
yield this . revokable . revoke_async ( cancellable ) ;
set_revokable ( null ) ;
}
protected abstract async Geary . Revokable
execute_impl ( GLib . Cancellable cancellable )
throws GLib . Error ;
private void set_revokable ( Geary . Revokable ? updated ) {
if ( this . revokable ! = null ) {
this . revokable . committed . disconnect ( on_revokable_committed ) ;
}
this . revokable = updated ;
if ( this . revokable ! = null ) {
this . revokable . committed . connect ( on_revokable_committed ) ;
}
}
private void on_revokable_committed ( Geary . Revokable ? updated ) {
set_revokable ( updated ) ;
}
}
2019-10-30 22:38:33 +11:00
private class Application . MoveEmailCommand : RevokableCommand {
2019-10-06 10:30:19 +11:00
private Geary . FolderSupport . Move source ;
private Geary . Folder destination ;
public MoveEmailCommand ( Geary . FolderSupport . Move source ,
Geary . Folder destination ,
2019-10-30 22:38:33 +11:00
Gee . Collection < Geary . App . Conversation > conversations ,
2019-10-06 10:30:19 +11:00
Gee . Collection < Geary . EmailIdentifier > messages ,
string ? executed_label = null ,
string ? undone_label = null ) {
2019-10-30 22:38:33 +11:00
base ( source , conversations , messages ) ;
2019-10-06 10:30:19 +11:00
this . source = source ;
this . destination = destination ;
this . executed_label = executed_label ;
this . undone_label = undone_label ;
}
2019-10-24 12:42:54 +11:00
internal override EmailCommand . StateChangePolicy folders_removed (
Gee . Collection < Geary . Folder > removed
) {
return (
2019-10-30 22:38:33 +11:00
this . destination in removed
2019-10-24 12:42:54 +11:00
? EmailCommand . StateChangePolicy . REMOVE
2019-10-30 22:38:33 +11:00
: base . folders_removed ( removed )
2019-10-24 12:42:54 +11:00
) ;
}
internal override EmailCommand . StateChangePolicy email_removed (
Geary . Folder location ,
Gee . Collection < Geary . EmailIdentifier > targets
) {
2019-10-30 22:38:33 +11:00
// With the current revokable mechanism we can't determine if
// specific messages removed from the destination are
// affected, so if the dest is the location, just assume they
// are for now.
return (
location = = this . destination
? EmailCommand . StateChangePolicy . REMOVE
: base . email_removed ( location , targets )
) ;
2019-10-24 12:42:54 +11:00
}
2019-10-06 10:30:19 +11:00
protected override async Geary . Revokable
execute_impl ( GLib . Cancellable cancellable )
throws GLib . Error {
bool open = false ;
try {
yield this . source . open_async (
Geary . Folder . OpenFlags . NO_DELAY , cancellable
) ;
open = true ;
return yield this . source . move_email_async (
2019-10-30 22:38:33 +11:00
this . email ,
2019-10-06 10:30:19 +11:00
this . destination . path ,
cancellable
) ;
} finally {
if ( open ) {
try {
yield this . source . close_async ( null ) ;
} catch ( GLib . Error err ) {
// ignored
}
}
}
}
}
2019-10-30 22:38:33 +11:00
private class Application . ArchiveEmailCommand : RevokableCommand {
2019-10-24 12:46:33 +11:00
2019-10-30 22:38:33 +11:00
/** {@inheritDoc} */
public Geary . Folder command_location {
get ; protected set ;
}
/** {@inheritDoc} */
public Gee . Collection < Geary . EmailIdentifier > command_conversations {
get ; protected set ;
}
/** {@inheritDoc} */
public Gee . Collection < Geary . EmailIdentifier > command_email {
get ; protected set ;
}
2019-10-24 12:46:33 +11:00
private Geary . FolderSupport . Archive source ;
2019-10-30 22:38:33 +11:00
2019-10-24 12:46:33 +11:00
public ArchiveEmailCommand ( Geary . FolderSupport . Archive source ,
2019-10-30 22:38:33 +11:00
Gee . Collection < Geary . App . Conversation > conversations ,
2019-10-24 12:46:33 +11:00
Gee . Collection < Geary . EmailIdentifier > messages ,
string ? executed_label = null ,
string ? undone_label = null ) {
2019-10-30 22:38:33 +11:00
base ( source , conversations , messages ) ;
2019-10-24 12:46:33 +11:00
this . source = source ;
this . executed_label = executed_label ;
this . undone_label = undone_label ;
}
internal override EmailCommand . StateChangePolicy folders_removed (
Gee . Collection < Geary . Folder > removed
) {
2019-10-30 22:38:33 +11:00
EmailCommand . StateChangePolicy ret = base . folders_removed ( removed ) ;
if ( ret = = IGNORE ) {
// With the current revokable mechanism we can't determine
// if specific messages removed from the destination are
// affected, so if the dest is the location, just assume
// they are for now.
foreach ( var folder in removed ) {
if ( folder . special_folder_type = = ARCHIVE ) {
ret = REMOVE ;
break ;
}
}
2019-10-24 12:46:33 +11:00
}
return ret ;
}
internal override EmailCommand . StateChangePolicy email_removed (
Geary . Folder location ,
Gee . Collection < Geary . EmailIdentifier > targets
) {
2019-10-30 22:38:33 +11:00
// With the current revokable mechanism we can't determine if
// specific messages removed from the destination are
// affected, so if the dest is the location, just assume they
// are for now.
return (
location . special_folder_type = = ARCHIVE
? EmailCommand . StateChangePolicy . REMOVE
: base . email_removed ( location , targets )
) ;
2019-10-24 12:46:33 +11:00
}
protected override async Geary . Revokable
execute_impl ( GLib . Cancellable cancellable )
throws GLib . Error {
bool open = false ;
try {
yield this . source . open_async (
Geary . Folder . OpenFlags . NO_DELAY , cancellable
) ;
open = true ;
return yield this . source . archive_email_async (
2019-10-30 22:38:33 +11:00
this . email , cancellable
2019-10-24 12:46:33 +11:00
) ;
} finally {
if ( open ) {
try {
yield this . source . close_async ( null ) ;
} catch ( GLib . Error err ) {
// ignored
}
}
}
}
}
2019-10-30 22:38:33 +11:00
private class Application . CopyEmailCommand : EmailCommand {
2019-10-06 10:42:03 +11:00
public override bool can_undo {
// Engine doesn't yet support it :(
get { return false ; }
}
private Geary . FolderSupport . Copy source ;
private Geary . Folder destination ;
public CopyEmailCommand ( Geary . FolderSupport . Copy source ,
Geary . Folder destination ,
2019-10-30 22:38:33 +11:00
Gee . Collection < Geary . App . Conversation > conversations ,
2019-10-06 10:42:03 +11:00
Gee . Collection < Geary . EmailIdentifier > messages ,
string ? executed_label = null ,
string ? undone_label = null ) {
2019-10-30 22:38:33 +11:00
base ( source , conversations , messages ) ;
2019-10-06 10:42:03 +11:00
this . source = source ;
this . destination = destination ;
this . executed_label = executed_label ;
this . undone_label = undone_label ;
}
public override async void execute ( GLib . Cancellable ? cancellable )
throws GLib . Error {
bool open = false ;
try {
yield this . source . open_async (
Geary . Folder . OpenFlags . NO_DELAY , cancellable
) ;
open = true ;
yield this . source . copy_email_async (
2019-10-30 22:38:33 +11:00
this . email , this . destination . path , cancellable
2019-10-06 10:42:03 +11:00
) ;
} finally {
if ( open ) {
try {
yield this . source . close_async ( null ) ;
} catch ( GLib . Error err ) {
// ignored
}
}
}
}
public override async void undo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
throw new Geary . EngineError . UNSUPPORTED (
" Cannot undo copy, not yet supported "
2019-10-06 10:54:52 +11:00
) ;
}
2019-10-24 12:42:54 +11:00
internal override EmailCommand . StateChangePolicy folders_removed (
Gee . Collection < Geary . Folder > removed
) {
return (
2019-10-30 22:38:33 +11:00
this . destination in removed
2019-10-24 12:42:54 +11:00
? EmailCommand . StateChangePolicy . REMOVE
2019-10-30 22:38:33 +11:00
: base . folders_removed ( removed )
2019-10-24 12:42:54 +11:00
) ;
}
internal override EmailCommand . StateChangePolicy email_removed (
Geary . Folder location ,
Gee . Collection < Geary . EmailIdentifier > targets
) {
2019-10-30 22:38:33 +11:00
// With the current revokable mechanism we can't determine if
// specific messages removed from the destination are
// affected, so if the dest is the location, just assume they
// are for now.
return (
location = = this . destination
? EmailCommand . StateChangePolicy . REMOVE
: base . email_removed ( location , targets )
) ;
2019-10-24 12:42:54 +11:00
}
2019-10-06 10:54:52 +11:00
}
2019-10-30 22:38:33 +11:00
private class Application . DeleteEmailCommand : EmailCommand {
2019-10-06 10:54:52 +11:00
public override bool can_undo {
get { return false ; }
}
private Geary . FolderSupport . Remove target ;
public DeleteEmailCommand ( Geary . FolderSupport . Remove target ,
2019-10-30 22:38:33 +11:00
Gee . Collection < Geary . App . Conversation > conversations ,
Gee . Collection < Geary . EmailIdentifier > email ) {
base ( target , conversations , email ) ;
2019-10-06 10:54:52 +11:00
this . target = target ;
}
public override async void execute ( GLib . Cancellable ? cancellable )
throws GLib . Error {
bool open = false ;
try {
yield this . target . open_async (
Geary . Folder . OpenFlags . NO_DELAY , cancellable
) ;
open = true ;
2019-10-30 22:38:33 +11:00
yield this . target . remove_email_async ( this . email , cancellable ) ;
2019-10-06 10:54:52 +11:00
} finally {
if ( open ) {
try {
yield this . target . close_async ( null ) ;
} catch ( GLib . Error err ) {
// ignored
}
}
2019-10-06 10:42:03 +11:00
}
}
2019-10-06 10:54:52 +11:00
public override async void undo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
throw new Geary . EngineError . UNSUPPORTED (
" Cannot undo emptying a folder: %s " ,
this . target . path . to_string ( )
) ;
}
2019-10-06 10:42:03 +11:00
}
2019-10-06 10:56:09 +11:00
private class Application . EmptyFolderCommand : Command {
public override bool can_undo {
get { return false ; }
}
private Geary . FolderSupport . Empty target ;
public EmptyFolderCommand ( Geary . FolderSupport . Empty target ) {
this . target = target ;
}
public override async void execute ( GLib . Cancellable ? cancellable )
throws GLib . Error {
bool open = false ;
try {
yield this . target . open_async (
Geary . Folder . OpenFlags . NO_DELAY , cancellable
) ;
open = true ;
yield this . target . empty_folder_async ( cancellable ) ;
} finally {
if ( open ) {
try {
yield this . target . close_async ( null ) ;
} catch ( GLib . Error err ) {
// ignored
}
}
}
}
public override async void undo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
throw new Geary . EngineError . UNSUPPORTED (
" Cannot undo emptying a folder: %s " ,
this . target . path . to_string ( )
) ;
}
2019-10-31 16:13:37 +11:00
/** Determines if this command is equal to another. */
public override bool equal_to ( Command other ) {
EmptyFolderCommand ? other_type = other as EmptyFolderCommand ;
return ( other_type ! = null & & this . target = = other_type . target ) ;
}
2019-10-06 10:56:09 +11:00
}
2019-11-12 12:34:50 +11:00
2019-11-12 14:31:53 +11:00
private abstract class Application . ComposerCommand : Command {
2019-11-12 12:34:50 +11:00
public override bool can_redo {
get { return false ; }
}
2019-11-12 14:31:53 +11:00
protected Composer . Widget ? composer { get ; private set ; }
protected ComposerCommand ( Composer . Widget composer ) {
this . composer = composer ;
}
protected void clear_composer ( ) {
this . composer = null ;
}
protected void close_composer ( ) {
// Calling close then immediately erasing the reference looks
// sketchy, but works since Controller still maintains a
// reference to the composer until it destroys itself.
this . composer . close . begin ( ) ;
this . composer = null ;
}
}
private class Application . SendComposerCommand : ComposerCommand {
public override bool can_undo {
get { return this . application . config . undo_send_delay > 0 ; }
}
2019-11-13 10:54:11 +11:00
private Client application ;
2019-11-13 14:04:05 +11:00
private AccountContext context ;
2019-11-12 12:34:50 +11:00
private Geary . Smtp . ClientService smtp ;
private Geary . TimeoutManager commit_timer ;
private Geary . EmailIdentifier ? saved = null ;
2019-11-13 10:54:11 +11:00
public SendComposerCommand ( Client application ,
2019-11-13 14:04:05 +11:00
AccountContext context ,
2019-11-12 12:34:50 +11:00
Composer . Widget composer ) {
2019-11-12 14:31:53 +11:00
base ( composer ) ;
2019-11-12 12:34:50 +11:00
this . application = application ;
this . context = context ;
this . smtp = ( Geary . Smtp . ClientService ) context . account . outgoing ;
int send_delay = this . application . config . undo_send_delay ;
this . commit_timer = new Geary . TimeoutManager . seconds (
send_delay > 0 ? send_delay : 0 ,
on_commit_timeout
) ;
}
public override async void execute ( GLib . Cancellable ? cancellable )
throws GLib . Error {
Geary . ComposedEmail email = yield this . composer . get_composed_email ( ) ;
/// Translators: The label for an in-app notification. The
/// string substitution is a list of recipients of the email.
this . executed_label = _ (
" Email to %s queued for delivery "
) . printf ( Util . Email . to_short_recipient_display ( email ) ) ;
if ( this . can_undo ) {
this . saved = yield this . smtp . save_email ( email , cancellable ) ;
this . commit_timer . start ( ) ;
} else {
yield this . smtp . send_email ( email , cancellable ) ;
}
}
public override async void undo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
this . commit_timer . reset ( ) ;
yield this . smtp . outbox . remove_email_async (
Geary . Collection . single ( this . saved ) ,
cancellable
) ;
2019-11-12 14:31:53 +11:00
this . saved = null ;
2019-11-12 12:34:50 +11:00
this . composer . set_enabled ( true ) ;
2019-11-14 12:17:05 +11:00
this . application . controller . show_composer ( this . composer , null , null ) ;
2019-11-12 14:31:53 +11:00
clear_composer ( ) ;
2019-11-12 12:34:50 +11:00
}
private void on_commit_timeout ( ) {
this . smtp . queue_email ( this . saved ) ;
this . saved = null ;
2019-11-12 14:31:53 +11:00
close_composer ( ) ;
2019-11-12 12:34:50 +11:00
}
}
2019-11-12 14:34:50 +11:00
2019-11-12 15:21:26 +11:00
private class Application . SaveComposerCommand : ComposerCommand {
private const int DESTROY_TIMEOUT_SEC = 30 * 60 ;
public override bool can_redo {
get { return false ; }
}
private Controller controller ;
private Geary . TimeoutManager destroy_timer ;
public SaveComposerCommand ( Controller controller ,
Composer . Widget composer ) {
base ( composer ) ;
this . controller = controller ;
this . destroy_timer = new Geary . TimeoutManager . seconds (
DESTROY_TIMEOUT_SEC ,
on_destroy_timeout
) ;
}
public override async void execute ( GLib . Cancellable ? cancellable )
throws GLib . Error {
Geary . ComposedEmail email = yield this . composer . get_composed_email ( ) ;
/// Translators: The label for an in-app notification. The
/// string substitution is a list of recipients of the email.
this . executed_label = _ (
" Email to %s saved "
) . printf ( Util . Email . to_short_recipient_display ( email ) ) ;
this . destroy_timer . start ( ) ;
}
public override async void undo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
if ( this . composer ! = null ) {
this . destroy_timer . reset ( ) ;
this . composer . set_enabled ( true ) ;
2019-11-14 12:17:05 +11:00
this . controller . show_composer ( this . composer , null , null ) ;
2019-11-12 15:21:26 +11:00
clear_composer ( ) ;
} else {
/// Translators: A label for an in-app notification.
this . undone_label = _ (
" Composer could not be restored "
) ;
}
}
private void on_destroy_timeout ( ) {
close_composer ( ) ;
}
}
2019-11-12 14:34:50 +11:00
private class Application . DiscardComposerCommand : ComposerCommand {
private const int DESTROY_TIMEOUT_SEC = 30 * 60 ;
public override bool can_redo {
get { return false ; }
}
private Controller controller ;
private Geary . TimeoutManager destroy_timer ;
public DiscardComposerCommand ( Controller controller ,
Composer . Widget composer ) {
base ( composer ) ;
this . controller = controller ;
this . destroy_timer = new Geary . TimeoutManager . seconds (
DESTROY_TIMEOUT_SEC ,
on_destroy_timeout
) ;
}
public override async void execute ( GLib . Cancellable ? cancellable )
throws GLib . Error {
Geary . ComposedEmail email = yield this . composer . get_composed_email ( ) ;
/// Translators: The label for an in-app notification. The
/// string substitution is a list of recipients of the email.
this . executed_label = _ (
" Email to %s discarded "
) . printf ( Util . Email . to_short_recipient_display ( email ) ) ;
this . destroy_timer . start ( ) ;
}
public override async void undo ( GLib . Cancellable ? cancellable )
throws GLib . Error {
if ( this . composer ! = null ) {
this . destroy_timer . reset ( ) ;
this . composer . set_enabled ( true ) ;
2019-11-14 12:17:05 +11:00
this . controller . show_composer ( this . composer , null , null ) ;
2019-11-12 14:34:50 +11:00
clear_composer ( ) ;
} else {
2019-11-12 15:21:26 +11:00
/// Translators: A label for an in-app notification.
2019-11-12 14:34:50 +11:00
this . undone_label = _ (
" Composer could not be restored "
) ;
}
}
private void on_destroy_timeout ( ) {
close_composer ( ) ;
}
}