1/* 2 * Copyright © 2016 Software Freedom Conservancy Inc. 3 * Copyright © 2016-2020 Michael Gratton <mike@vee.net> 4 * 5 * This software is licensed under the GNU Lesser General Public License 6 * (version 2.1 or later). See the COPYING file in this distribution. 7 */ 8 9 10/** 11 * Primary controller for an application instance. 12 * 13 * A single instance of this class is constructed by {@link Client} 14 * when the primary application instance is started. 15 */ 16internal class Application.Controller : 17 Geary.BaseObject, AccountInterface, Composer.ApplicationInterface { 18 19 20 private const uint MAX_AUTH_ATTEMPTS = 3; 21 22 private const uint CLEANUP_CHECK_AFTER_IDLE_BACKGROUND_MINUTES = 5; 23 24 /** Determines if conversations can be trashed from the given folder. */ 25 public static bool does_folder_support_trash(Geary.Folder? target) { 26 return ( 27 target != null && 28 target.used_as != TRASH && 29 !target.properties.is_local_only && 30 (target as Geary.FolderSupport.Move) != null 31 ); 32 } 33 34 /** Determines if folders should be added to main windows. */ 35 private static bool should_add_folder(Gee.Collection<Geary.Folder>? all, 36 Geary.Folder folder) { 37 // if folder is openable, add it 38 if (folder.properties.is_openable != Geary.Trillian.FALSE) 39 return true; 40 else if (folder.properties.has_children == Geary.Trillian.FALSE) 41 return false; 42 43 // if folder contains children, we must ensure that there is 44 // at least one of the same type 45 Geary.Folder.SpecialUse type = folder.used_as; 46 foreach (Geary.Folder other in all) { 47 if (other.used_as == type && other.path.parent == folder.path) 48 return true; 49 } 50 51 return false; 52 } 53 54 55 /** Determines if the controller is open. */ 56 public bool is_open { 57 get { 58 return !this.controller_open.is_cancelled(); 59 } 60 } 61 62 /** The primary application instance that owns this controller. */ 63 public weak Client application { get; private set; } // circular ref 64 65 /** Account management for the application. */ 66 public Accounts.Manager account_manager { get; private set; } 67 68 /** Plugin manager for the application. */ 69 public PluginManager plugins { get; private set; } 70 71 /** Certificate management for the application. */ 72 public Application.CertificateManager certificate_manager { 73 get; private set; 74 } 75 76 // Primary collection of the application's open accounts 77 private Gee.Map<Geary.AccountInformation,AccountContext> accounts = 78 new Gee.HashMap<Geary.AccountInformation,AccountContext>(); 79 private bool is_loading_accounts = true; 80 81 // Cancelled if the controller is closed 82 private GLib.Cancellable controller_open; 83 84 private DatabaseManager database_manager; 85 private Folks.IndividualAggregator folks; 86 87 // List composers that have not yet been closed 88 private Gee.Collection<Composer.Widget> composer_widgets = 89 new Gee.LinkedList<Composer.Widget>(); 90 91 // Requested mailto composers not yet fullfulled 92 private Gee.List<string?> pending_mailtos = new Gee.ArrayList<string>(); 93 94 // Timeout to do work in idle after all windows have been sent to the background 95 private Geary.TimeoutManager all_windows_backgrounded_timeout; 96 97 private GLib.Cancellable? storage_cleanup_cancellable; 98 99 100 /** 101 * Emitted when a composer is registered. 102 * 103 * This will be emitted after a composer is constructed, but 104 * before it is shown. 105 */ 106 public signal void composer_registered(Composer.Widget widget); 107 108 /** 109 * Emitted when a composer is deregistered. 110 * 111 * This will be emitted when a composer has been closed and is 112 * about to be destroyed. 113 */ 114 public signal void composer_deregistered(Composer.Widget widget); 115 116 /** 117 * Constructs a new instance of the controller. 118 */ 119 public async Controller(Client application, 120 GLib.Cancellable cancellable) 121 throws GLib.Error { 122 this.application = application; 123 this.controller_open = cancellable; 124 125 GLib.File config_dir = application.get_home_config_directory(); 126 GLib.File data_dir = application.get_home_data_directory(); 127 128 // This initializes the IconFactory, important to do before 129 // the actions are created (as they refer to some of Geary's 130 // custom icons) 131 IconFactory.init(application.get_resource_directory()); 132 133 // Create DB upgrade dialog. 134 this.database_manager = new DatabaseManager(application); 135 136 // Initialise WebKit and WebViews 137 Components.WebView.init_web_context( 138 this.application.config, 139 this.application.get_web_extensions_dir(), 140 this.application.get_home_cache_directory().get_child( 141 "web-resources" 142 ) 143 ); 144 Components.WebView.load_resources(config_dir); 145 Composer.WebView.load_resources(); 146 ConversationWebView.load_resources(); 147 Accounts.SignatureWebView.load_resources(); 148 149 this.all_windows_backgrounded_timeout = 150 new Geary.TimeoutManager.seconds(CLEANUP_CHECK_AFTER_IDLE_BACKGROUND_MINUTES * 60, on_unfocused_idle); 151 152 this.folks = Folks.IndividualAggregator.dup(); 153 if (!this.folks.is_prepared) { 154 // Do this in the background since it can take a long time 155 // on some systems and the GUI shouldn't be blocked by it 156 this.folks.prepare.begin((obj, res) => { 157 try { 158 this.folks.prepare.end(res); 159 } catch (GLib.Error err) { 160 warning("Error preparing Folks: %s", err.message); 161 } 162 }); 163 164 } 165 166 this.plugins = new PluginManager( 167 this.application, 168 this, 169 this.application.config, 170 this.application.get_app_plugins_dir() 171 ); 172 173 // Create standard config directory 174 try { 175 config_dir.make_directory_with_parents(); 176 } catch (GLib.IOError.EXISTS err) { 177 // fine 178 } 179 180 // Migrate configuration if necessary. 181 Util.Migrate.xdg_config_dir(config_dir, data_dir); 182 Util.Migrate.release_config( 183 application.get_config_search_path(), config_dir 184 ); 185 186 // Hook up cert, accounts and credentials machinery 187 188 this.certificate_manager = yield new Application.CertificateManager( 189 data_dir.get_child("pinned-certs"), 190 cancellable 191 ); 192 // Commit e8061379 mistakenly used config_dir for cert manager 193 // above, so remove it if found. This can be pulled out post 194 // v40. 195 try { 196 yield Geary.Files.recursive_delete_async( 197 config_dir.get_child("pinned-certs") 198 ); 199 } catch (GLib.IOError.NOT_FOUND err) { 200 // exactly as planned 201 } 202 203 SecretMediator? libsecret = yield new SecretMediator(cancellable); 204 205 application.engine.account_available.connect(on_account_available); 206 207 this.account_manager = new Accounts.Manager( 208 libsecret, 209 config_dir, 210 data_dir 211 ); 212 this.account_manager.account_added.connect( 213 on_account_added 214 ); 215 this.account_manager.account_status_changed.connect( 216 on_account_status_changed 217 ); 218 this.account_manager.account_removed.connect( 219 on_account_removed 220 ); 221 this.account_manager.report_problem.connect( 222 on_report_problem 223 ); 224 225 yield this.account_manager.connect_goa(cancellable); 226 227 // Load accounts 228 yield this.account_manager.load_accounts(cancellable); 229 this.is_loading_accounts = false; 230 231 // Expunge any deleted accounts in the background, so we're 232 // not blocking the app continuing to open. 233 this.expunge_accounts.begin(); 234 } 235 236 /** Closes all windows and accounts, releasing held resources. */ 237 public async void close() { 238 // Stop listening for account changes up front so we don't 239 // attempt to add new accounts while shutting down. 240 this.account_manager.account_added.disconnect( 241 on_account_added 242 ); 243 this.account_manager.account_status_changed.disconnect( 244 on_account_status_changed 245 ); 246 this.account_manager.account_removed.disconnect( 247 on_account_removed 248 ); 249 this.application.engine.account_available.disconnect( 250 on_account_available 251 ); 252 253 foreach (MainWindow window in this.application.get_main_windows()) { 254 window.sensitive = false; 255 } 256 257 // Close any open composers up-front before anything else is 258 // shut down so any pending operations have a chance to 259 // complete. 260 var composer_barrier = new Geary.Nonblocking.CountingSemaphore(null); 261 // Take a copy of the collection of composers since 262 // closing any will cause the underlying collection to change. 263 var composers = new Gee.LinkedList<Composer.Widget>(); 264 composers.add_all(this.composer_widgets); 265 foreach (var composer in composers) { 266 if (composer.current_mode != CLOSED) { 267 composer_barrier.acquire(); 268 composer.close.begin( 269 (obj, res) => { 270 composer.close.end(res); 271 composer_barrier.blind_notify(); 272 } 273 ); 274 } 275 } 276 277 try { 278 yield composer_barrier.wait_async(); 279 } catch (GLib.Error err) { 280 warning("Error waiting at composer barrier: %s", err.message); 281 } 282 283 // Now that all composers are closed, we can shut down the 284 // rest of the client and engine. Cancel internal processes 285 // first so they don't block shutdown. 286 this.controller_open.cancel(); 287 288 // Release folder and conversations in main windows before 289 // closing them so we know they are released before closing 290 // the accounts 291 var window_barrier = new Geary.Nonblocking.CountingSemaphore(null); 292 foreach (MainWindow window in this.application.get_main_windows()) { 293 window_barrier.acquire(); 294 window.select_folder.begin( 295 null, 296 false, 297 true, 298 (obj, res) => { 299 window.select_folder.end(res); 300 window.close(); 301 window_barrier.blind_notify(); 302 } 303 ); 304 } 305 try { 306 yield window_barrier.wait_async(); 307 } catch (GLib.Error err) { 308 warning("Error waiting at window barrier: %s", err.message); 309 } 310 311 // Release general resources now there's no more UI 312 try { 313 this.plugins.close(); 314 } catch (GLib.Error err) { 315 warning("Error closing plugin manager: %s", err.message); 316 } 317 this.pending_mailtos.clear(); 318 this.composer_widgets.clear(); 319 320 // Create a copy of known accounts so the loop below does not 321 // explode if accounts are removed while iterating. 322 var closing_accounts = new Gee.LinkedList<AccountContext>(); 323 closing_accounts.add_all(this.accounts.values); 324 var account_barrier = new Geary.Nonblocking.CountingSemaphore(null); 325 foreach (AccountContext context in closing_accounts) { 326 account_barrier.acquire(); 327 this.close_account.begin( 328 context.account.information, 329 true, 330 (obj, ret) => { 331 this.close_account.end(ret); 332 account_barrier.blind_notify(); 333 } 334 ); 335 } 336 try { 337 yield account_barrier.wait_async(); 338 } catch (GLib.Error err) { 339 warning("Error waiting at account barrier: %s", err.message); 340 } 341 342 info("Closed Application.Controller"); 343 } 344 345 /** 346 * Opens a composer for writing a new, blank message. 347 */ 348 public async Composer.Widget compose_blank(AccountContext send_context, 349 Geary.RFC822.MailboxAddress? to = null) { 350 MainWindow main = this.application.get_active_main_window(); 351 Composer.Widget composer = main.conversation_viewer.current_composer; 352 if (composer == null || 353 composer.current_mode != PANED || 354 !composer.is_blank || 355 composer.sender_context != send_context) { 356 composer = new Composer.Widget( 357 this, 358 this.application.config, 359 send_context, 360 null 361 ); 362 register_composer(composer); 363 } 364 try { 365 yield composer.load_empty_body(to); 366 } catch (GLib.Error err) { 367 report_problem(new Geary.ProblemReport(err)); 368 } 369 return composer; 370 } 371 372 /** 373 * Opens new composer with an existing message as context. 374 * 375 * If the given type is {@link Composer.Widget.ContextType.EDIT}, 376 * the context is loaded to be edited (e.g. for drafts, templates, 377 * sending again. Otherwise the context is treated as the email to 378 * be replied to, etc. 379 * 380 * Returns null if there is an existing composer open and the 381 * prompt to close it was declined. 382 */ 383 public async Composer.Widget? compose_with_context(AccountContext send_context, 384 Composer.Widget.ContextType type, 385 Geary.Email context, 386 string? quote) { 387 MainWindow main = this.application.get_active_main_window(); 388 Composer.Widget? composer = null; 389 if (type == EDIT) { 390 // Check all known composers since the context may be open 391 // an existing composer already. 392 foreach (var existing in this.composer_widgets) { 393 if (existing.current_mode != NONE && 394 existing.current_mode != CLOSED && 395 composer.sender_context == send_context && 396 existing.saved_id != null && 397 existing.saved_id.equal_to(context.id)) { 398 composer = existing; 399 break; 400 } 401 } 402 } else { 403 // See whether there is already an inline message in the 404 // current window that is either a reply/forward for that 405 // message, or there is a quote to insert into it. 406 foreach (var existing in this.composer_widgets) { 407 if (existing.get_toplevel() == main && 408 (existing.current_mode == INLINE || 409 existing.current_mode == INLINE_COMPACT) && 410 existing.sender_context == send_context && 411 (context.id in existing.get_referred_ids() || 412 quote != null)) { 413 try { 414 existing.append_to_email(context, quote, type); 415 composer = existing; 416 break; 417 } catch (Geary.EngineError error) { 418 report_problem(new Geary.ProblemReport(error)); 419 } 420 } 421 } 422 423 // Can't re-use an existing composer, so need to create a 424 // new one. Replies must open inline in the main window, 425 // so we need to ensure there are no composers open there 426 // first. 427 if (composer == null && !main.close_composer(true)) { 428 // Prompt to close the existing composer was declined, 429 // so bail out 430 return null; 431 } 432 } 433 434 if (composer == null) { 435 composer = new Composer.Widget( 436 this, 437 this.application.config, 438 send_context, 439 null 440 ); 441 register_composer(composer); 442 443 try { 444 yield composer.load_context(type, context, quote); 445 } catch (GLib.Error err) { 446 report_problem(new Geary.ProblemReport(err)); 447 } 448 } 449 return composer; 450 } 451 452 /** 453 * Opens a composer with the given `mailto:` URL. 454 */ 455 public async void compose_mailto(string mailto) { 456 MainWindow? window = this.application.last_active_main_window; 457 if (window != null && window.selected_account != null) { 458 var context = this.accounts.get(window.selected_account.information); 459 if (context != null) { 460 var composer = new Composer.Widget( 461 this, 462 this.application.config, 463 context 464 ); 465 register_composer(composer); 466 present_composer(composer); 467 468 try { 469 yield composer.load_mailto(mailto); 470 } catch (GLib.Error err) { 471 report_problem(new Geary.ProblemReport(err)); 472 } 473 } 474 } else { 475 // Schedule the send for after we have an account open. 476 this.pending_mailtos.add(mailto); 477 } 478 } 479 480 /** Displays a problem report when an error has been encountered. */ 481 public void report_problem(Geary.ProblemReport report) { 482 debug("Problem reported: %s", report.to_string()); 483 484 if (report.error == null || 485 !(report.error.thrown is IOError.CANCELLED)) { 486 var info_bar = new Components.ProblemReportInfoBar(report); 487 info_bar.retry.connect(on_retry_problem); 488 this.application.get_active_main_window().show_info_bar(info_bar); 489 } 490 491 Geary.ServiceProblemReport? service_report = 492 report as Geary.ServiceProblemReport; 493 if (service_report != null && service_report.service.protocol == SMTP) { 494 this.application.send_error_notification( 495 /// Notification title. 496 _("A problem occurred sending email for %s").printf( 497 service_report.account.display_name 498 ), 499 /// Notification body 500 _("Email will not be sent until re-connected") 501 ); 502 } 503 } 504 505 /** 506 * Updates flags for a collection of conversations. 507 * 508 * If `prefer_adding` is true, this will add the flag if not set 509 * on all conversations or else will remove it. If false, this 510 * will remove the flag if not set on all conversations or else 511 * add it. 512 */ 513 public async void mark_conversations(Geary.Folder location, 514 Gee.Collection<Geary.App.Conversation> conversations, 515 Geary.NamedFlag flag, 516 bool prefer_adding) 517 throws GLib.Error { 518 Geary.Iterable<Geary.App.Conversation> selecting = 519 Geary.traverse(conversations); 520 Geary.EmailFlags flags = new Geary.EmailFlags(); 521 522 if (flag.equal_to(Geary.EmailFlags.UNREAD)) { 523 selecting = selecting.filter(c => prefer_adding ^ c.is_unread()); 524 flags.add(Geary.EmailFlags.UNREAD); 525 } else if (flag.equal_to(Geary.EmailFlags.FLAGGED)) { 526 selecting = selecting.filter(c => prefer_adding ^ c.is_flagged()); 527 flags.add(Geary.EmailFlags.FLAGGED); 528 } else { 529 throw new Geary.EngineError.UNSUPPORTED( 530 "Marking as %s is not supported", flag.to_string() 531 ); 532 } 533 534 Gee.Collection<Geary.EmailIdentifier>? messages = null; 535 Gee.Collection<Geary.App.Conversation> selected = 536 selecting.to_linked_list(); 537 538 bool do_add = prefer_adding ^ selected.is_empty; 539 if (selected.is_empty) { 540 selected = conversations; 541 } 542 543 if (do_add) { 544 // Only apply to the latest in-folder message in 545 // conversations that don't already have the flag, since 546 // we don't want to flag every message in the conversation 547 messages = Geary.traverse(selected).map<Geary.EmailIdentifier>( 548 c => c.get_latest_recv_email(IN_FOLDER_OUT_OF_FOLDER).id 549 ).to_linked_list(); 550 } else { 551 // Remove the flag from those that have it 552 messages = new Gee.LinkedList<Geary.EmailIdentifier>(); 553 foreach (Geary.App.Conversation convo in selected) { 554 foreach (Geary.Email email in 555 convo.get_emails(RECV_DATE_DESCENDING)) { 556 if (email.email_flags != null && 557 email.email_flags.contains(flag)) { 558 messages.add(email.id); 559 } 560 } 561 } 562 } 563 564 yield mark_messages( 565 location, 566 conversations, 567 messages, 568 do_add ? flags : null, 569 do_add ? null : flags 570 ); 571 } 572 573 /** 574 * Updates flags for a collection of email. 575 * 576 * This should only be used when working with specific messages 577 * (for example, marking a specific message in a conversation) 578 * rather than when working with whole conversations. In that 579 * case, use {@link mark_conversations}. 580 */ 581 public async void mark_messages(Geary.Folder location, 582 Gee.Collection<Geary.App.Conversation> conversations, 583 Gee.Collection<Geary.EmailIdentifier> messages, 584 Geary.EmailFlags? to_add, 585 Geary.EmailFlags? to_remove) 586 throws GLib.Error { 587 AccountContext? context = this.accounts.get(location.account.information); 588 if (context != null) { 589 yield context.commands.execute( 590 new MarkEmailCommand( 591 location, 592 conversations, 593 messages, 594 context.emails, 595 to_add, 596 to_remove, 597 /// Translators: Label for in-app notification 598 ngettext( 599 "Conversation marked", 600 "Conversations marked", 601 conversations.size 602 ), 603 /// Translators: Label for in-app notification 604 ngettext( 605 "Conversation un-marked", 606 "Conversations un-marked", 607 conversations.size 608 ) 609 ), 610 context.cancellable 611 ); 612 } 613 } 614 615 public async void move_conversations(Geary.FolderSupport.Move source, 616 Geary.Folder destination, 617 Gee.Collection<Geary.App.Conversation> conversations) 618 throws GLib.Error { 619 AccountContext? context = this.accounts.get(source.account.information); 620 if (context != null) { 621 yield context.commands.execute( 622 new MoveEmailCommand( 623 source, 624 destination, 625 conversations, 626 to_in_folder_email_ids(conversations), 627 /// Translators: Label for in-app 628 /// notification. String substitution is the name 629 /// of the destination folder. 630 ngettext( 631 "Conversation moved to %s", 632 "Conversations moved to %s", 633 conversations.size 634 ).printf(Util.I18n.to_folder_display_name(destination)), 635 /// Translators: Label for in-app 636 /// notification. String substitution is the name 637 /// of the source folder. 638 ngettext( 639 "Conversation restored to %s", 640 "Conversations restored to %s", 641 conversations.size 642 ).printf(Util.I18n.to_folder_display_name(source)) 643 ), 644 context.cancellable 645 ); 646 } 647 } 648 649 public async void move_conversations_special(Geary.Folder source, 650 Geary.Folder.SpecialUse destination, 651 Gee.Collection<Geary.App.Conversation> conversations) 652 throws GLib.Error { 653 AccountContext? context = this.accounts.get(source.account.information); 654 if (context != null) { 655 Command? command = null; 656 Gee.Collection<Geary.EmailIdentifier> messages = 657 to_in_folder_email_ids(conversations); 658 /// Translators: Label for in-app notification. String 659 /// substitution is the name of the destination folder. 660 string undone_tooltip = ngettext( 661 "Conversation restored to %s", 662 "Conversations restored to %s", 663 messages.size 664 ).printf(Util.I18n.to_folder_display_name(source)); 665 666 if (destination == ARCHIVE) { 667 Geary.FolderSupport.Archive? archive_source = ( 668 source as Geary.FolderSupport.Archive 669 ); 670 if (archive_source == null) { 671 throw new Geary.EngineError.UNSUPPORTED( 672 "Folder does not support archiving: %s", 673 source.to_string() 674 ); 675 } 676 command = new ArchiveEmailCommand( 677 archive_source, 678 conversations, 679 messages, 680 /// Translators: Label for in-app notification. 681 ngettext( 682 "Conversation archived", 683 "Conversations archived", 684 messages.size 685 ), 686 undone_tooltip 687 ); 688 } else { 689 Geary.FolderSupport.Move? move_source = ( 690 source as Geary.FolderSupport.Move 691 ); 692 if (move_source == null) { 693 throw new Geary.EngineError.UNSUPPORTED( 694 "Folder does not support moving: %s", 695 source.to_string() 696 ); 697 } 698 Geary.Folder? dest = source.account.get_special_folder( 699 destination 700 ); 701 if (dest == null) { 702 throw new Geary.EngineError.NOT_FOUND( 703 "No folder found for: %s", destination.to_string() 704 ); 705 } 706 command = new MoveEmailCommand( 707 move_source, 708 dest, 709 conversations, 710 messages, 711 /// Translators: Label for in-app 712 /// notification. String substitution is the name 713 /// of the destination folder. 714 ngettext( 715 "Conversation moved to %s", 716 "Conversations moved to %s", 717 messages.size 718 ).printf(Util.I18n.to_folder_display_name(dest)), 719 undone_tooltip 720 ); 721 } 722 723 yield context.commands.execute(command, context.cancellable); 724 } 725 } 726 727 public async void move_messages_special(Geary.Folder source, 728 Geary.Folder.SpecialUse destination, 729 Gee.Collection<Geary.App.Conversation> conversations, 730 Gee.Collection<Geary.EmailIdentifier> messages) 731 throws GLib.Error { 732 AccountContext? context = this.accounts.get(source.account.information); 733 if (context != null) { 734 Command? command = null; 735 /// Translators: Label for in-app notification. String 736 /// substitution is the name of the destination folder. 737 string undone_tooltip = ngettext( 738 "Message restored to %s", 739 "Messages restored to %s", 740 messages.size 741 ).printf(Util.I18n.to_folder_display_name(source)); 742 743 if (destination == ARCHIVE) { 744 Geary.FolderSupport.Archive? archive_source = ( 745 source as Geary.FolderSupport.Archive 746 ); 747 if (archive_source == null) { 748 throw new Geary.EngineError.UNSUPPORTED( 749 "Folder does not support archiving: %s", 750 source.to_string() 751 ); 752 } 753 command = new ArchiveEmailCommand( 754 archive_source, 755 conversations, 756 messages, 757 /// Translators: Label for in-app notification. 758 ngettext( 759 "Message archived", 760 "Messages archived", 761 messages.size 762 ), 763 undone_tooltip 764 ); 765 } else { 766 Geary.FolderSupport.Move? move_source = ( 767 source as Geary.FolderSupport.Move 768 ); 769 if (move_source == null) { 770 throw new Geary.EngineError.UNSUPPORTED( 771 "Folder does not support moving: %s", 772 source.to_string() 773 ); 774 } 775 776 Geary.Folder? dest = source.account.get_special_folder( 777 destination 778 ); 779 if (dest == null) { 780 throw new Geary.EngineError.NOT_FOUND( 781 "No folder found for: %s", destination.to_string() 782 ); 783 } 784 785 command = new MoveEmailCommand( 786 move_source, 787 dest, 788 conversations, 789 messages, 790 /// Translators: Label for in-app 791 /// notification. String substitution is the name 792 /// of the destination folder. 793 ngettext( 794 "Message moved to %s", 795 "Messages moved to %s", 796 messages.size 797 ).printf(Util.I18n.to_folder_display_name(dest)), 798 undone_tooltip 799 ); 800 } 801 802 yield context.commands.execute(command, context.cancellable); 803 } 804 } 805 806 public async void copy_conversations(Geary.FolderSupport.Copy source, 807 Geary.Folder destination, 808 Gee.Collection<Geary.App.Conversation> conversations) 809 throws GLib.Error { 810 AccountContext? context = this.accounts.get(source.account.information); 811 if (context != null) { 812 yield context.commands.execute( 813 new CopyEmailCommand( 814 source, 815 destination, 816 conversations, 817 to_in_folder_email_ids(conversations), 818 /// Translators: Label for in-app 819 /// notification. String substitution is the name 820 /// of the destination folder. 821 ngettext( 822 "Conversation labelled as %s", 823 "Conversations labelled as %s", 824 conversations.size 825 ).printf(Util.I18n.to_folder_display_name(destination)), 826 /// Translators: Label for in-app 827 /// notification. String substitution is the name 828 /// of the destination folder. 829 ngettext( 830 "Conversation un-labelled as %s", 831 "Conversations un-labelled as %s", 832 conversations.size 833 ).printf(Util.I18n.to_folder_display_name(destination)) 834 ), 835 context.cancellable 836 ); 837 } 838 } 839 840 public async void delete_conversations(Geary.FolderSupport.Remove target, 841 Gee.Collection<Geary.App.Conversation> conversations) 842 throws GLib.Error { 843 var messages = target.properties.is_virtual 844 ? to_all_email_ids(conversations) 845 : to_in_folder_email_ids(conversations); 846 yield delete_messages(target, conversations, messages); 847 } 848 849 public async void delete_messages(Geary.FolderSupport.Remove target, 850 Gee.Collection<Geary.App.Conversation> conversations, 851 Gee.Collection<Geary.EmailIdentifier> messages) 852 throws GLib.Error { 853 AccountContext? context = this.accounts.get(target.account.information); 854 if (context != null) { 855 Command command = new DeleteEmailCommand( 856 target, conversations, messages 857 ); 858 command.executed.connect( 859 () => context.controller_stack.email_removed(target, messages) 860 ); 861 yield context.commands.execute(command, context.cancellable); 862 } 863 } 864 865 public async void empty_folder(Geary.Folder target) 866 throws GLib.Error { 867 AccountContext? context = this.accounts.get(target.account.information); 868 if (context != null) { 869 Geary.FolderSupport.Empty? emptyable = ( 870 target as Geary.FolderSupport.Empty 871 ); 872 if (emptyable == null) { 873 throw new Geary.EngineError.UNSUPPORTED( 874 "Emptying folder not supported %s", target.path.to_string() 875 ); 876 } 877 878 Command command = new EmptyFolderCommand(emptyable); 879 command.executed.connect( 880 // Not quite accurate, but close enough 881 () => context.controller_stack.folders_removed( 882 Geary.Collection.single(emptyable) 883 ) 884 ); 885 yield context.commands.execute(command, context.cancellable); 886 } 887 } 888 889 /** Returns a context for an account, if any. */ 890 internal AccountContext? get_context_for_account(Geary.AccountInformation account) { 891 return this.accounts.get(account); 892 } 893 894 /** Returns a read-only collection of contexts each active account. */ 895 internal Gee.Collection<AccountContext> get_account_contexts() { 896 return this.accounts.values.read_only_view; 897 } 898 899 internal void register_window(MainWindow window) { 900 window.retry_service_problem.connect(on_retry_service_problem); 901 } 902 903 internal void unregister_window(MainWindow window) { 904 window.retry_service_problem.disconnect(on_retry_service_problem); 905 } 906 907 /** Opens any pending composers. */ 908 internal async void process_pending_composers() { 909 foreach (string? mailto in this.pending_mailtos) { 910 yield compose_mailto(mailto); 911 } 912 this.pending_mailtos.clear(); 913 } 914 915 /** Queues the email in a composer for delivery. */ 916 internal async void send_composed_email(Composer.Widget composer) { 917 AccountContext context = composer.sender_context; 918 try { 919 yield context.commands.execute( 920 new SendComposerCommand(this.application, context, composer), 921 context.cancellable 922 ); 923 } catch (GLib.Error err) { 924 report_problem(new Geary.ProblemReport(err)); 925 } 926 } 927 928 /** Saves the email in a composer as a draft on the server. */ 929 internal async void save_composed_email(Composer.Widget composer) { 930 // XXX this doesn't actually do what it says on the tin, since 931 // the composer's draft manager is already saving drafts on 932 // the server. Until we get that saving local-only, this will 933 // only be around for pushing the composer onto the undo stack 934 AccountContext context = composer.sender_context; 935 try { 936 yield context.commands.execute( 937 new SaveComposerCommand(this, composer), 938 context.cancellable 939 ); 940 } catch (GLib.Error err) { 941 report_problem(new Geary.ProblemReport(err)); 942 } 943 } 944 945 /** Queues a composer to be discarded. */ 946 internal async void discard_composed_email(Composer.Widget composer) { 947 AccountContext context = composer.sender_context; 948 try { 949 yield context.commands.execute( 950 new DiscardComposerCommand(this, composer), 951 context.cancellable 952 ); 953 } catch (GLib.Error err) { 954 report_problem(new Geary.ProblemReport(err)); 955 } 956 } 957 958 /** Expunges removed accounts while the controller remains open. */ 959 internal async void expunge_accounts() { 960 try { 961 yield this.account_manager.expunge_accounts(this.controller_open); 962 } catch (GLib.Error err) { 963 report_problem(new Geary.ProblemReport(err)); 964 } 965 } 966 967 private void add_account(Geary.AccountInformation added) { 968 try { 969 this.application.engine.add_account(added); 970 } catch (Geary.EngineError.ALREADY_EXISTS err) { 971 // all good 972 } catch (GLib.Error err) { 973 report_problem(new Geary.AccountProblemReport(added, err)); 974 } 975 } 976 977 private async void open_account(Geary.Account account) { 978 AccountContext context = new AccountContext( 979 account, 980 new Geary.App.SearchFolder(account, account.local_folder_root), 981 new Geary.App.EmailStore(account), 982 new Application.ContactStore(account, this.folks) 983 ); 984 this.accounts.set(account.information, context); 985 986 this.database_manager.add_account(account, this.controller_open); 987 988 account.information.authentication_failure.connect( 989 on_authentication_failure 990 ); 991 account.information.untrusted_host.connect(on_untrusted_host); 992 account.notify["current-status"].connect( 993 on_account_status_notify 994 ); 995 account.email_removed.connect(on_account_email_removed); 996 account.folders_available_unavailable.connect(on_folders_available_unavailable); 997 account.report_problem.connect(on_report_problem); 998 999 Geary.Smtp.ClientService? smtp = ( 1000 account.outgoing as Geary.Smtp.ClientService 1001 ); 1002 if (smtp != null) { 1003 smtp.email_sent.connect(on_sent); 1004 smtp.sending_monitor.start.connect(on_sending_started); 1005 smtp.sending_monitor.finish.connect(on_sending_finished); 1006 } 1007 1008 // Notify before opening so that listeners have a chance to 1009 // hook into it before signals start getting fired by folders 1010 // becoming available, etc. 1011 account_available(context, this.is_loading_accounts); 1012 1013 bool retry = false; 1014 do { 1015 try { 1016 yield account.open_async(this.controller_open); 1017 retry = false; 1018 } catch (GLib.Error open_err) { 1019 debug("Unable to open account %s: %s", account.to_string(), open_err.message); 1020 1021 if (open_err is Geary.EngineError.CORRUPT) { 1022 retry = yield account_database_error_async(account); 1023 } 1024 1025 if (!retry) { 1026 report_problem( 1027 new Geary.AccountProblemReport( 1028 account.information, 1029 open_err 1030 ) 1031 ); 1032 1033 this.account_manager.disable_account(account.information); 1034 this.accounts.unset(account.information); 1035 } 1036 } 1037 } while (retry); 1038 1039 update_account_status(); 1040 } 1041 1042 private async void remove_account(Geary.AccountInformation removed) { 1043 yield close_account(removed, false); 1044 try { 1045 this.application.engine.remove_account(removed); 1046 } catch (Geary.EngineError.NOT_FOUND err) { 1047 // all good 1048 } catch (GLib.Error err) { 1049 report_problem( 1050 new Geary.AccountProblemReport(removed, err) 1051 ); 1052 } 1053 } 1054 1055 private async void close_account(Geary.AccountInformation config, 1056 bool is_shutdown) { 1057 AccountContext? context = this.accounts.get(config); 1058 if (context != null) { 1059 debug("Closing account: %s", context.account.information.id); 1060 Geary.Account account = context.account; 1061 1062 account_unavailable(context, is_shutdown); 1063 1064 // Guard against trying to close the account twice 1065 this.accounts.unset(account.information); 1066 1067 this.database_manager.remove_account(account); 1068 1069 // Stop updating status and showing errors when closing 1070 // the account - the user doesn't care any more 1071 account.report_problem.disconnect(on_report_problem); 1072 account.information.authentication_failure.disconnect( 1073 on_authentication_failure 1074 ); 1075 account.information.untrusted_host.disconnect(on_untrusted_host); 1076 account.notify["current-status"].disconnect( 1077 on_account_status_notify 1078 ); 1079 1080 account.email_removed.disconnect(on_account_email_removed); 1081 account.folders_available_unavailable.disconnect(on_folders_available_unavailable); 1082 1083 Geary.Smtp.ClientService? smtp = ( 1084 account.outgoing as Geary.Smtp.ClientService 1085 ); 1086 if (smtp != null) { 1087 smtp.email_sent.disconnect(on_sent); 1088 smtp.sending_monitor.start.disconnect(on_sending_started); 1089 smtp.sending_monitor.finish.disconnect(on_sending_finished); 1090 } 1091 1092 // Now the account is not in the accounts map, reset any 1093 // status notifications for it 1094 update_account_status(); 1095 1096 // Stop any background processes 1097 context.search.clear_query(); 1098 context.contacts.close(); 1099 context.cancellable.cancel(); 1100 1101 // Explicitly close the inbox since we explicitly open it 1102 Geary.Folder? inbox = context.inbox; 1103 if (inbox != null) { 1104 try { 1105 yield inbox.close_async(null); 1106 } catch (Error close_inbox_err) { 1107 debug("Unable to close monitored inbox: %s", close_inbox_err.message); 1108 } 1109 context.inbox = null; 1110 } 1111 1112 try { 1113 yield account.close_async(null); 1114 } catch (Error close_err) { 1115 debug("Unable to close account %s: %s", account.to_string(), close_err.message); 1116 } 1117 1118 debug("Account closed: %s", account.to_string()); 1119 } 1120 } 1121 1122 private void update_account_status() { 1123 // Start off assuming all accounts are online and error free 1124 // (i.e. no status issues to indicate) and proceed until 1125 // proven incorrect. 1126 Geary.Account.Status effective_status = ONLINE; 1127 bool has_auth_error = false; 1128 bool has_cert_error = false; 1129 Geary.Account? service_problem_source = null; 1130 foreach (AccountContext context in this.accounts.values) { 1131 Geary.Account.Status status = context.get_effective_status(); 1132 if (!status.is_online()) { 1133 effective_status &= ~Geary.Account.Status.ONLINE; 1134 } 1135 if (status.has_service_problem()) { 1136 effective_status |= SERVICE_PROBLEM; 1137 if (service_problem_source == null) { 1138 service_problem_source = context.account; 1139 } 1140 } 1141 has_auth_error |= context.authentication_failed; 1142 has_cert_error |= context.tls_validation_failed; 1143 } 1144 1145 foreach (MainWindow window in this.application.get_main_windows()) { 1146 window.update_account_status( 1147 effective_status, 1148 has_auth_error, 1149 has_cert_error, 1150 service_problem_source 1151 ); 1152 } 1153 } 1154 1155 private bool is_currently_prompting() { 1156 return this.accounts.values.fold<bool>( 1157 (ctx, seed) => ( 1158 ctx.authentication_prompting | 1159 ctx.tls_validation_prompting | 1160 seed 1161 ), 1162 false 1163 ); 1164 } 1165 1166 private async void prompt_for_password(AccountContext context, 1167 Geary.ServiceInformation service) { 1168 Geary.AccountInformation account = context.account.information; 1169 bool is_incoming = (service == account.incoming); 1170 Geary.Credentials credentials = is_incoming 1171 ? account.incoming.credentials 1172 : account.get_outgoing_credentials(); 1173 1174 bool handled = true; 1175 if (context.authentication_attempts > MAX_AUTH_ATTEMPTS || 1176 credentials == null) { 1177 // We have run out of authentication attempts or have 1178 // been asked for creds but don't even have a login. So 1179 // just bail out immediately and flag the account as 1180 // needing attention. 1181 handled = false; 1182 } else if (this.account_manager.is_goa_account(account)) { 1183 context.authentication_prompting = true; 1184 try { 1185 yield account.load_incoming_credentials(context.cancellable); 1186 yield account.load_outgoing_credentials(context.cancellable); 1187 } catch (GLib.Error err) { 1188 // Bail out right away, but probably should be opening 1189 // the GOA control panel. 1190 handled = false; 1191 report_problem(new Geary.AccountProblemReport(account, err)); 1192 } 1193 context.authentication_prompting = false; 1194 } else { 1195 context.authentication_prompting = true; 1196 PasswordDialog password_dialog = new PasswordDialog( 1197 this.application.get_active_window(), 1198 account, 1199 service, 1200 credentials 1201 ); 1202 if (password_dialog.run()) { 1203 // The update the credentials for the service that the 1204 // credentials actually came from 1205 Geary.ServiceInformation creds_service = 1206 (credentials == account.incoming.credentials) 1207 ? account.incoming 1208 : account.outgoing; 1209 creds_service.credentials = credentials.copy_with_token( 1210 password_dialog.password 1211 ); 1212 1213 // Update the remember password pref if changed 1214 bool remember = password_dialog.remember_password; 1215 if (creds_service.remember_password != remember) { 1216 creds_service.remember_password = remember; 1217 account.changed(); 1218 } 1219 1220 SecretMediator libsecret = (SecretMediator) account.mediator; 1221 try { 1222 // Update the secret using the service where the 1223 // credentials originated, since the service forms 1224 // part of the key's identity 1225 if (creds_service.remember_password) { 1226 yield libsecret.update_token( 1227 account, creds_service, context.cancellable 1228 ); 1229 } else { 1230 yield libsecret.clear_token( 1231 account, creds_service, context.cancellable 1232 ); 1233 } 1234 } catch (GLib.IOError.CANCELLED err) { 1235 // all good 1236 } catch (GLib.Error err) { 1237 report_problem( 1238 new Geary.ServiceProblemReport(account, service, err) 1239 ); 1240 } 1241 1242 context.authentication_attempts++; 1243 } else { 1244 // User cancelled, bail out unconditionally 1245 handled = false; 1246 } 1247 context.authentication_prompting = false; 1248 } 1249 1250 if (handled) { 1251 try { 1252 yield this.application.engine.update_account_service( 1253 account, service, context.cancellable 1254 ); 1255 } catch (GLib.Error err) { 1256 report_problem( 1257 new Geary.ServiceProblemReport(account, service, err) 1258 ); 1259 } 1260 } else { 1261 context.authentication_attempts = 0; 1262 context.authentication_failed = true; 1263 update_account_status(); 1264 } 1265 } 1266 1267 private async void prompt_untrusted_host(AccountContext context, 1268 Geary.ServiceInformation service, 1269 Geary.Endpoint endpoint, 1270 GLib.TlsConnection cx) { 1271 if (this.application.config.revoke_certs) { 1272 // XXX 1273 } 1274 1275 context.tls_validation_prompting = true; 1276 try { 1277 yield this.certificate_manager.prompt_pin_certificate( 1278 this.application.get_active_main_window(), 1279 context.account.information, 1280 service, 1281 endpoint, 1282 false, 1283 context.cancellable 1284 ); 1285 context.tls_validation_failed = false; 1286 } catch (Application.CertificateManagerError.UNTRUSTED err) { 1287 // Don't report an error here, the user simply declined. 1288 context.tls_validation_failed = true; 1289 } catch (Application.CertificateManagerError err) { 1290 // Assume validation is now good, but report the error 1291 // since the cert may not have been saved 1292 context.tls_validation_failed = false; 1293 report_problem( 1294 new Geary.ServiceProblemReport( 1295 context.account.information, 1296 service, 1297 err 1298 ) 1299 ); 1300 } 1301 1302 context.tls_validation_prompting = false; 1303 update_account_status(); 1304 } 1305 1306 private void on_account_email_removed(Geary.Folder folder, 1307 Gee.Collection<Geary.EmailIdentifier> ids) { 1308 if (folder.used_as == OUTBOX) { 1309 foreach (MainWindow window in this.application.get_main_windows()) { 1310 window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SEND_FAILURE); 1311 window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED); 1312 } 1313 } 1314 } 1315 1316 private void on_sending_started() { 1317 foreach (MainWindow window in this.application.get_main_windows()) { 1318 window.status_bar.activate_message(StatusBar.Message.OUTBOX_SENDING); 1319 } 1320 } 1321 1322 private void on_sending_finished() { 1323 foreach (MainWindow window in this.application.get_main_windows()) { 1324 window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SENDING); 1325 } 1326 } 1327 1328 // Returns true if the caller should try opening the account again 1329 private async bool account_database_error_async(Geary.Account account) { 1330 bool retry = true; 1331 1332 // give the user two options: reset the Account local store, or exit Geary. A third 1333 // could be done to leave the Account in an unopened state, but we don't currently 1334 // have provisions for that. 1335 QuestionDialog dialog = new QuestionDialog( 1336 this.application.get_active_main_window(), 1337 _("Unable to open the database for %s").printf(account.information.id), 1338 _("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\nGeary can rebuild the database and re-synchronize with the server or exit.\n\nRebuilding the database will destroy all local email and its attachments. <b>The mail on the your server will not be affected.</b>") 1339 .printf(account.information.data_dir.get_path()), 1340 _("_Rebuild"), _("E_xit")); 1341 dialog.use_secondary_markup(true); 1342 switch (dialog.run()) { 1343 case Gtk.ResponseType.OK: 1344 // don't use Cancellable because we don't want to interrupt this process 1345 try { 1346 yield account.rebuild_async(); 1347 } catch (Error err) { 1348 ErrorDialog errdialog = new ErrorDialog( 1349 this.application.get_active_main_window(), 1350 _("Unable to rebuild database for “%s”").printf(account.information.id), 1351 _("Error during rebuild:\n\n%s").printf(err.message)); 1352 errdialog.run(); 1353 1354 retry = false; 1355 } 1356 break; 1357 1358 default: 1359 retry = false; 1360 break; 1361 } 1362 1363 return retry; 1364 } 1365 1366 private void on_folders_available_unavailable( 1367 Geary.Account account, 1368 Gee.BidirSortedSet<Geary.Folder>? available, 1369 Gee.BidirSortedSet<Geary.Folder>? unavailable) { 1370 var account_context = this.accounts.get(account.information); 1371 1372 if (available != null && available.size > 0) { 1373 var added_contexts = new Gee.LinkedList<FolderContext>(); 1374 foreach (var folder in available) { 1375 if (Controller.should_add_folder(available, folder)) { 1376 if (folder.used_as == INBOX) { 1377 if (account_context.inbox == null) { 1378 account_context.inbox = folder; 1379 } 1380 folder.open_async.begin( 1381 NO_DELAY, account_context.cancellable 1382 ); 1383 } 1384 1385 var folder_context = new FolderContext(folder); 1386 added_contexts.add(folder_context); 1387 } 1388 } 1389 if (!added_contexts.is_empty) { 1390 account_context.add_folders(added_contexts); 1391 } 1392 } 1393 1394 if (unavailable != null) { 1395 Gee.BidirIterator<Geary.Folder> unavailable_iterator = 1396 unavailable.bidir_iterator(); 1397 bool has_prev = unavailable_iterator.last(); 1398 var removed_contexts = new Gee.LinkedList<FolderContext>(); 1399 while (has_prev) { 1400 Geary.Folder folder = unavailable_iterator.get(); 1401 1402 if (folder.used_as == INBOX) { 1403 account_context.inbox = null; 1404 } 1405 1406 var folder_context = account_context.get_folder(folder); 1407 if (folder_context != null) { 1408 removed_contexts.add(folder_context); 1409 } 1410 1411 has_prev = unavailable_iterator.previous(); 1412 } 1413 if (!removed_contexts.is_empty) { 1414 account_context.remove_folders(removed_contexts); 1415 } 1416 1417 // Notify the command stack that folders have gone away 1418 account_context.controller_stack.folders_removed(unavailable); 1419 } 1420 } 1421 1422 /** Clears new message counts in notification plugin contexts. */ 1423 internal void clear_new_messages(Geary.Folder source, 1424 Gee.Set<Geary.App.Conversation> visible) { 1425 foreach (MainWindow window in this.application.get_main_windows()) { 1426 window.folder_list.set_has_new(source, false); 1427 } 1428 foreach (NotificationPluginContext context in 1429 this.plugins.get_notification_contexts()) { 1430 context.clear_new_messages(source, visible); 1431 } 1432 } 1433 1434 /** Notifies plugins of new email being displayed. */ 1435 internal void email_loaded(Geary.AccountInformation account, 1436 Geary.Email loaded) { 1437 foreach (EmailPluginContext plugin in 1438 this.plugins.get_email_contexts()) { 1439 plugin.email_displayed(account, loaded); 1440 } 1441 } 1442 1443 /** 1444 * Track a window receiving focus, for idle background work. 1445 */ 1446 public void window_focus_in() { 1447 this.all_windows_backgrounded_timeout.reset(); 1448 1449 if (this.storage_cleanup_cancellable != null) { 1450 this.storage_cleanup_cancellable.cancel(); 1451 1452 // Cleanup was still running and we don't know where we got to so 1453 // we'll clear each of these so it runs next time we're in the 1454 // background 1455 foreach (AccountContext context in this.accounts.values) { 1456 context.cancellable.cancelled.disconnect(this.storage_cleanup_cancellable.cancel); 1457 1458 Geary.Account account = context.account; 1459 account.last_storage_cleanup = null; 1460 } 1461 this.storage_cleanup_cancellable = null; 1462 } 1463 } 1464 1465 /** 1466 * Track a window going unfocused, for idle background work. 1467 */ 1468 public void window_focus_out() { 1469 this.all_windows_backgrounded_timeout.start(); 1470 } 1471 1472 /** Attempts to make the composer visible on the active monitor. */ 1473 internal void present_composer(Composer.Widget composer) { 1474 if (composer.current_mode == CLOSED || 1475 composer.current_mode == NONE) { 1476 var target = this.application.get_active_main_window(); 1477 target.show_composer(composer); 1478 } 1479 composer.set_focus(); 1480 composer.present(); 1481 } 1482 1483 internal bool check_open_composers() { 1484 var do_quit = true; 1485 foreach (var composer in this.composer_widgets) { 1486 if (composer.conditional_close(true, true) == CANCELLED) { 1487 do_quit = false; 1488 break; 1489 } 1490 } 1491 return do_quit; 1492 } 1493 1494 internal void register_composer(Composer.Widget widget) { 1495 if (!(widget in this.composer_widgets)) { 1496 debug(@"Registered composer of type $(widget.context_type); " + 1497 @"$(this.composer_widgets.size) composers total"); 1498 widget.destroy.connect_after(this.on_composer_widget_destroy); 1499 this.composer_widgets.add(widget); 1500 composer_registered(widget); 1501 } 1502 } 1503 1504 private void on_composer_widget_destroy(Gtk.Widget sender) { 1505 Composer.Widget? composer = sender as Composer.Widget; 1506 if (composer != null && composer_widgets.remove(composer)) { 1507 debug(@"Composer type $(composer.context_type) destroyed; " + 1508 @"$(this.composer_widgets.size) composers remaining"); 1509 composer_deregistered(composer); 1510 } 1511 } 1512 1513 private void on_sent(Geary.Smtp.ClientService service, 1514 Geary.Email sent) { 1515 /// Translators: The label for an in-app notification. The 1516 /// string substitution is a list of recipients of the email. 1517 string message = _( 1518 "Email sent to %s" 1519 ).printf(Util.Email.to_short_recipient_display(sent)); 1520 Components.InAppNotification notification = 1521 new Components.InAppNotification( 1522 message, application.config.brief_notification_duration 1523 ); 1524 foreach (MainWindow window in this.application.get_main_windows()) { 1525 window.add_notification(notification); 1526 } 1527 1528 AccountContext? context = this.accounts.get(service.account); 1529 if (context != null) { 1530 foreach (EmailPluginContext plugin in 1531 this.plugins.get_email_contexts()) { 1532 plugin.email_sent(context.account.information, sent); 1533 } 1534 } 1535 } 1536 1537 private Gee.Collection<Geary.EmailIdentifier> 1538 to_in_folder_email_ids(Gee.Collection<Geary.App.Conversation> conversations) { 1539 Gee.Collection<Geary.EmailIdentifier> messages = 1540 new Gee.LinkedList<Geary.EmailIdentifier>(); 1541 foreach (Geary.App.Conversation conversation in conversations) { 1542 foreach (Geary.Email email in 1543 conversation.get_emails(RECV_DATE_ASCENDING, IN_FOLDER)) { 1544 messages.add(email.id); 1545 } 1546 } 1547 return messages; 1548 } 1549 1550 private Gee.Collection<Geary.EmailIdentifier> 1551 to_all_email_ids(Gee.Collection<Geary.App.Conversation> conversations) { 1552 Gee.Collection<Geary.EmailIdentifier> messages = 1553 new Gee.LinkedList<Geary.EmailIdentifier>(); 1554 foreach (Geary.App.Conversation conversation in conversations) { 1555 foreach (Geary.Email email in conversation.get_emails(NONE)) { 1556 messages.add(email.id); 1557 } 1558 } 1559 return messages; 1560 } 1561 1562 private void on_account_available(Geary.AccountInformation info) { 1563 Geary.Account? account = null; 1564 try { 1565 account = this.application.engine.get_account(info); 1566 } catch (GLib.Error error) { 1567 report_problem(new Geary.ProblemReport(error)); 1568 warning( 1569 "Error creating account %s instance: %s", 1570 info.id, 1571 error.message 1572 ); 1573 } 1574 1575 if (account != null) { 1576 this.open_account.begin(account); 1577 } 1578 } 1579 1580 private void on_account_added(Geary.AccountInformation added, 1581 Accounts.Manager.Status status) { 1582 if (status == Accounts.Manager.Status.ENABLED) { 1583 this.add_account(added); 1584 } 1585 } 1586 1587 private void on_account_status_changed(Geary.AccountInformation changed, 1588 Accounts.Manager.Status status) { 1589 switch (status) { 1590 case Accounts.Manager.Status.ENABLED: 1591 this.add_account(changed); 1592 break; 1593 1594 case Accounts.Manager.Status.UNAVAILABLE: 1595 case Accounts.Manager.Status.DISABLED: 1596 this.remove_account.begin(changed); 1597 break; 1598 1599 case Accounts.Manager.Status.REMOVED: 1600 // Account is gone, no further action is required 1601 break; 1602 } 1603 } 1604 1605 private void on_account_removed(Geary.AccountInformation removed) { 1606 this.remove_account.begin(removed); 1607 } 1608 1609 private void on_report_problem(Geary.ProblemReport problem) { 1610 report_problem(problem); 1611 } 1612 1613 private void on_retry_problem(Components.ProblemReportInfoBar info_bar) { 1614 Geary.ServiceProblemReport? service_report = 1615 info_bar.report as Geary.ServiceProblemReport; 1616 if (service_report != null) { 1617 AccountContext? context = this.accounts.get(service_report.account); 1618 if (context != null && context.account.is_open()) { 1619 switch (service_report.service.protocol) { 1620 case Geary.Protocol.IMAP: 1621 context.account.incoming.restart.begin(context.cancellable); 1622 break; 1623 1624 case Geary.Protocol.SMTP: 1625 context.account.outgoing.restart.begin(context.cancellable); 1626 break; 1627 } 1628 } 1629 } 1630 } 1631 1632 private void on_account_status_notify() { 1633 update_account_status(); 1634 } 1635 1636 private void on_authentication_failure(Geary.AccountInformation account, 1637 Geary.ServiceInformation service) { 1638 AccountContext? context = this.accounts.get(account); 1639 if (context != null && !is_currently_prompting()) { 1640 this.prompt_for_password.begin(context, service); 1641 } 1642 } 1643 1644 private void on_untrusted_host(Geary.AccountInformation account, 1645 Geary.ServiceInformation service, 1646 Geary.Endpoint endpoint, 1647 TlsConnection cx) { 1648 AccountContext? context = this.accounts.get(account); 1649 if (context != null && !is_currently_prompting()) { 1650 this.prompt_untrusted_host.begin(context, service, endpoint, cx); 1651 } 1652 } 1653 1654 private void on_retry_service_problem(Geary.ClientService.Status type) { 1655 bool has_restarted = false; 1656 foreach (AccountContext context in this.accounts.values) { 1657 Geary.Account account = context.account; 1658 if (account.current_status.has_service_problem() && 1659 (account.incoming.current_status == type || 1660 account.outgoing.current_status == type)) { 1661 1662 Geary.ClientService service = 1663 (account.incoming.current_status == type) 1664 ? account.incoming 1665 : account.outgoing; 1666 1667 bool do_restart = true; 1668 switch (type) { 1669 case AUTHENTICATION_FAILED: 1670 if (has_restarted) { 1671 // Only restart at most one at a time, so we 1672 // don't attempt to re-auth multiple bad 1673 // accounts at once. 1674 do_restart = false; 1675 } else { 1676 // Reset so the infobar does not show up again 1677 context.authentication_failed = false; 1678 } 1679 break; 1680 1681 case TLS_VALIDATION_FAILED: 1682 if (has_restarted) { 1683 // Only restart at most one at a time, so we 1684 // don't attempt to re-pin multiple bad 1685 // accounts at once. 1686 do_restart = false; 1687 } else { 1688 // Reset so the infobar does not show up again 1689 context.tls_validation_failed = false; 1690 } 1691 break; 1692 1693 default: 1694 // No special action required for other statuses 1695 break; 1696 } 1697 1698 if (do_restart) { 1699 has_restarted = true; 1700 service.restart.begin(context.cancellable); 1701 } 1702 } 1703 } 1704 } 1705 1706 private void on_unfocused_idle() { 1707 // Schedule later, catching cases where work should occur later while still in background 1708 this.all_windows_backgrounded_timeout.reset(); 1709 window_focus_out(); 1710 1711 if (this.storage_cleanup_cancellable == null) 1712 do_background_storage_cleanup.begin(); 1713 } 1714 1715 private async void do_background_storage_cleanup() { 1716 debug("Checking for backgrounded idle work"); 1717 this.storage_cleanup_cancellable = new GLib.Cancellable(); 1718 1719 foreach (AccountContext context in this.accounts.values) { 1720 Geary.Account account = context.account; 1721 context.cancellable.cancelled.connect(this.storage_cleanup_cancellable.cancel); 1722 try { 1723 yield account.cleanup_storage(this.storage_cleanup_cancellable); 1724 } catch (GLib.Error err) { 1725 report_problem(new Geary.ProblemReport(err)); 1726 } 1727 context.cancellable.cancelled.disconnect(this.storage_cleanup_cancellable.cancel); 1728 if (this.storage_cleanup_cancellable.is_cancelled()) 1729 break; 1730 } 1731 this.storage_cleanup_cancellable = null; 1732 } 1733 1734} 1735 1736 1737/** Base class for all application controller commands. */ 1738internal class Application.ControllerCommandStack : CommandStack { 1739 1740 1741 private EmailCommand? last_executed = null; 1742 1743 1744 /** {@inheritDoc} */ 1745 public override async void execute(Command target, 1746 GLib.Cancellable? cancellable) 1747 throws GLib.Error { 1748 // Guard against things like Delete being held down by only 1749 // executing a command if it is different to the last one. 1750 if (this.last_executed == null || !this.last_executed.equal_to(target)) { 1751 this.last_executed = target as EmailCommand; 1752 yield base.execute(target, cancellable); 1753 } 1754 } 1755 1756 /** {@inheritDoc} */ 1757 public override async void undo(GLib.Cancellable? cancellable) 1758 throws GLib.Error { 1759 this.last_executed = null; 1760 yield base.undo(cancellable); 1761 } 1762 1763 /** {@inheritDoc} */ 1764 public override async void redo(GLib.Cancellable? cancellable) 1765 throws GLib.Error { 1766 this.last_executed = null; 1767 yield base.redo(cancellable); 1768 } 1769 1770 /** 1771 * Notifies the stack that one or more folders were removed. 1772 * 1773 * This will cause any commands involving the given folder to be 1774 * removed from the stack. It should only be called as a response 1775 * to un-recoverable changes, e.g. when the server notifies that a 1776 * folder has been removed. 1777 */ 1778 internal void folders_removed(Gee.Collection<Geary.Folder> removed) { 1779 Gee.Iterator<Command> commands = this.undo_stack.iterator(); 1780 while (commands.next()) { 1781 EmailCommand? email = commands.get() as EmailCommand; 1782 if (email != null) { 1783 if (email.folders_removed(removed) == REMOVE) { 1784 commands.remove(); 1785 } 1786 } 1787 } 1788 } 1789 1790 /** 1791 * Notifies the stack that email was removed from a folder. 1792 * 1793 * This will cause any commands involving the given email 1794 * identifiers to be removed from commands where they are present, 1795 * potentially also causing the command to be removed from the 1796 * stack. It should only be called as a response to un-recoverable 1797 * changes, e.g. when the server notifies that an email has been 1798 * removed as a result of some other client removing it, or the 1799 * message being deleted completely. 1800 */ 1801 internal void email_removed(Geary.Folder location, 1802 Gee.Collection<Geary.EmailIdentifier> targets) { 1803 Gee.Iterator<Command> commands = this.undo_stack.iterator(); 1804 while (commands.next()) { 1805 EmailCommand? email = commands.get() as EmailCommand; 1806 if (email != null) { 1807 if (email.email_removed(location, targets) == REMOVE) { 1808 commands.remove(); 1809 } 1810 } 1811 } 1812 } 1813 1814} 1815 1816 1817/** Base class for email-related commands. */ 1818public abstract class Application.EmailCommand : Command { 1819 1820 1821 /** Specifies a command's response to external mail state changes. */ 1822 public enum StateChangePolicy { 1823 /** The change can be ignored */ 1824 IGNORE, 1825 1826 /** The command is no longer valid and should be removed */ 1827 REMOVE; 1828 } 1829 1830 1831 /** 1832 * Returns the folder where the command was initially executed. 1833 * 1834 * This is used by the main window to return to the folder where 1835 * the command was first carried out. 1836 */ 1837 public Geary.Folder location { 1838 get; protected set; 1839 } 1840 1841 /** 1842 * Returns the conversations which the command was initially applied to. 1843 * 1844 * This is used by the main window to return to the conversation where 1845 * the command was first carried out. 1846 */ 1847 public Gee.Collection<Geary.App.Conversation> conversations { 1848 get; private set; 1849 } 1850 1851 /** 1852 * Returns the email which the command was initially applied to. 1853 * 1854 * This is used by the main window to return to the conversation where 1855 * the command was first carried out. 1856 */ 1857 public Gee.Collection<Geary.EmailIdentifier> email { 1858 get; private set; 1859 } 1860 1861 private Gee.Collection<Geary.App.Conversation> mutable_conversations; 1862 private Gee.Collection<Geary.EmailIdentifier> mutable_email; 1863 1864 1865 protected EmailCommand(Geary.Folder location, 1866 Gee.Collection<Geary.App.Conversation> conversations, 1867 Gee.Collection<Geary.EmailIdentifier> email) { 1868 this.location = location; 1869 this.conversations = conversations.read_only_view; 1870 this.email = email.read_only_view; 1871 1872 this.mutable_conversations = conversations; 1873 this.mutable_email = email; 1874 } 1875 1876 1877 public override bool equal_to(Command other) { 1878 if (this == other) { 1879 return true; 1880 } 1881 1882 if (this.get_type() != other.get_type()) { 1883 return false; 1884 } 1885 1886 EmailCommand? other_email = other as EmailCommand; 1887 if (other_email == null) { 1888 return false; 1889 } 1890 1891 return ( 1892 this.location == other_email.location && 1893 this.conversations.size == other_email.conversations.size && 1894 this.email.size == other_email.email.size && 1895 this.conversations.contains_all(other_email.conversations) && 1896 this.email.contains_all(other_email.email) 1897 ); 1898 } 1899 1900 /** 1901 * Determines the command's response when a folder is removed. 1902 * 1903 * This is called when some external means (such as another 1904 * command, or another email client altogether) has caused a 1905 * folder to be removed. 1906 * 1907 * The returned policy will determine if the command is unaffected 1908 * by the change and hence can remain on the stack, or is no 1909 * longer valid and hence must be removed. 1910 */ 1911 internal virtual StateChangePolicy folders_removed( 1912 Gee.Collection<Geary.Folder> removed 1913 ) { 1914 return ( 1915 this.location in removed 1916 ? StateChangePolicy.REMOVE 1917 : StateChangePolicy.IGNORE 1918 ); 1919 } 1920 1921 /** 1922 * Determines the command's response when email is removed. 1923 * 1924 * This is called when some external means (such as another 1925 * command, or another email client altogether) has caused a 1926 * email in a folder to be removed. 1927 * 1928 * The returned policy will determine if the command is unaffected 1929 * by the change and hence can remain on the stack, or is no 1930 * longer valid and hence must be removed. 1931 */ 1932 internal virtual StateChangePolicy email_removed( 1933 Geary.Folder location, 1934 Gee.Collection<Geary.EmailIdentifier> targets 1935 ) { 1936 StateChangePolicy ret = IGNORE; 1937 if (this.location == location) { 1938 // Any removed email should have already been removed from 1939 // their conversations by the time we here, so just remove 1940 // any conversations that don't have any messages left. 1941 Gee.Iterator<Geary.App.Conversation> conversations = 1942 this.mutable_conversations.iterator(); 1943 while (conversations.next()) { 1944 var conversation = conversations.get(); 1945 if (!conversation.has_any_non_deleted_email()) { 1946 conversations.remove(); 1947 } 1948 } 1949 1950 // Update message set to remove all removed messages 1951 this.mutable_email.remove_all(targets); 1952 1953 // If we have no more conversations or messages, then the 1954 // command won't be able to do anything and should be 1955 // removed. 1956 if (this.mutable_conversations.is_empty || 1957 this.mutable_email.is_empty) { 1958 ret = REMOVE; 1959 } 1960 } 1961 return ret; 1962 } 1963 1964} 1965 1966 1967/** 1968 * Mixin for trivial application commands. 1969 * 1970 * Trivial commands should not cause a notification to be shown when 1971 * initially executed. 1972 */ 1973public interface Application.TrivialCommand : Command { 1974 1975} 1976 1977 1978private class Application.MarkEmailCommand : TrivialCommand, EmailCommand { 1979 1980 1981 private Geary.App.EmailStore store; 1982 private Geary.EmailFlags? to_add; 1983 private Geary.EmailFlags? to_remove; 1984 1985 1986 public MarkEmailCommand(Geary.Folder location, 1987 Gee.Collection<Geary.App.Conversation> conversations, 1988 Gee.Collection<Geary.EmailIdentifier> messages, 1989 Geary.App.EmailStore store, 1990 Geary.EmailFlags? to_add, 1991 Geary.EmailFlags? to_remove, 1992 string? executed_label = null, 1993 string? undone_label = null) { 1994 base(location, conversations, messages); 1995 this.store = store; 1996 this.to_add = to_add; 1997 this.to_remove = to_remove; 1998 1999 this.executed_label = executed_label; 2000 this.undone_label = undone_label; 2001 } 2002 2003 public override async void execute(GLib.Cancellable? cancellable) 2004 throws GLib.Error { 2005 yield this.store.mark_email_async( 2006 this.email, this.to_add, this.to_remove, cancellable 2007 ); 2008 } 2009 2010 public override async void undo(GLib.Cancellable? cancellable) 2011 throws GLib.Error { 2012 yield this.store.mark_email_async( 2013 this.email, this.to_remove, this.to_add, cancellable 2014 ); 2015 } 2016 2017 public override bool equal_to(Command other) { 2018 if (!base.equal_to(other)) { 2019 return false; 2020 } 2021 2022 MarkEmailCommand other_mark = (MarkEmailCommand) other; 2023 return ( 2024 ((this.to_add == other_mark.to_add) || 2025 (this.to_add != null && 2026 other_mark.to_add != null && 2027 this.to_add.equal_to(other_mark.to_add))) && 2028 ((this.to_remove == other_mark.to_remove) || 2029 (this.to_remove != null && 2030 other_mark.to_remove != null && 2031 this.to_remove.equal_to(other_mark.to_remove))) 2032 ); 2033 } 2034 2035} 2036 2037 2038private abstract class Application.RevokableCommand : EmailCommand { 2039 2040 2041 public override bool can_undo { 2042 get { return this.revokable != null && this.revokable.valid; } 2043 } 2044 2045 private Geary.Revokable? revokable = null; 2046 2047 2048 protected RevokableCommand(Geary.Folder location, 2049 Gee.Collection<Geary.App.Conversation> conversations, 2050 Gee.Collection<Geary.EmailIdentifier> email) { 2051 base(location, conversations, email); 2052 } 2053 2054 public override async void execute(GLib.Cancellable? cancellable) 2055 throws GLib.Error { 2056 set_revokable(yield execute_impl(cancellable)); 2057 if (this.revokable != null && this.revokable.valid) { 2058 yield this.revokable.commit_async(cancellable); 2059 } 2060 } 2061 2062 public override async void undo(GLib.Cancellable? cancellable) 2063 throws GLib.Error { 2064 if (this.revokable == null) { 2065 throw new Geary.EngineError.UNSUPPORTED( 2066 "Cannot undo command, no revokable available" 2067 ); 2068 } 2069 2070 yield this.revokable.revoke_async(cancellable); 2071 set_revokable(null); 2072 } 2073 2074 protected abstract async Geary.Revokable 2075 execute_impl(GLib.Cancellable cancellable) 2076 throws GLib.Error; 2077 2078 private void set_revokable(Geary.Revokable? updated) { 2079 if (this.revokable != null) { 2080 this.revokable.committed.disconnect(on_revokable_committed); 2081 } 2082 2083 this.revokable = updated; 2084 2085 if (this.revokable != null) { 2086 this.revokable.committed.connect(on_revokable_committed); 2087 } 2088 } 2089 2090 private void on_revokable_committed(Geary.Revokable? updated) { 2091 set_revokable(updated); 2092 } 2093 2094} 2095 2096 2097private class Application.MoveEmailCommand : RevokableCommand { 2098 2099 2100 private Geary.FolderSupport.Move source; 2101 private Geary.Folder destination; 2102 2103 2104 public MoveEmailCommand(Geary.FolderSupport.Move source, 2105 Geary.Folder destination, 2106 Gee.Collection<Geary.App.Conversation> conversations, 2107 Gee.Collection<Geary.EmailIdentifier> messages, 2108 string? executed_label = null, 2109 string? undone_label = null) { 2110 base(source, conversations, messages); 2111 2112 this.source = source; 2113 this.destination = destination; 2114 2115 this.executed_label = executed_label; 2116 this.undone_label = undone_label; 2117 } 2118 2119 internal override EmailCommand.StateChangePolicy folders_removed( 2120 Gee.Collection<Geary.Folder> removed 2121 ) { 2122 return ( 2123 this.destination in removed 2124 ? EmailCommand.StateChangePolicy.REMOVE 2125 : base.folders_removed(removed) 2126 ); 2127 } 2128 2129 internal override EmailCommand.StateChangePolicy email_removed( 2130 Geary.Folder location, 2131 Gee.Collection<Geary.EmailIdentifier> targets 2132 ) { 2133 // With the current revokable mechanism we can't determine if 2134 // specific messages removed from the destination are 2135 // affected, so if the dest is the location, just assume they 2136 // are for now. 2137 return ( 2138 location == this.destination 2139 ? EmailCommand.StateChangePolicy.REMOVE 2140 : base.email_removed(location, targets) 2141 ); 2142 } 2143 2144 protected override async Geary.Revokable 2145 execute_impl(GLib.Cancellable cancellable) 2146 throws GLib.Error { 2147 bool open = false; 2148 try { 2149 yield this.source.open_async( 2150 Geary.Folder.OpenFlags.NO_DELAY, cancellable 2151 ); 2152 open = true; 2153 return yield this.source.move_email_async( 2154 this.email, 2155 this.destination.path, 2156 cancellable 2157 ); 2158 } finally { 2159 if (open) { 2160 try { 2161 yield this.source.close_async(null); 2162 } catch (GLib.Error err) { 2163 // ignored 2164 } 2165 } 2166 } 2167 } 2168 2169} 2170 2171 2172private class Application.ArchiveEmailCommand : RevokableCommand { 2173 2174 2175 /** {@inheritDoc} */ 2176 public Geary.Folder command_location { 2177 get; protected set; 2178 } 2179 2180 /** {@inheritDoc} */ 2181 public Gee.Collection<Geary.EmailIdentifier> command_conversations { 2182 get; protected set; 2183 } 2184 2185 /** {@inheritDoc} */ 2186 public Gee.Collection<Geary.EmailIdentifier> command_email { 2187 get; protected set; 2188 } 2189 2190 private Geary.FolderSupport.Archive source; 2191 2192 2193 public ArchiveEmailCommand(Geary.FolderSupport.Archive source, 2194 Gee.Collection<Geary.App.Conversation> conversations, 2195 Gee.Collection<Geary.EmailIdentifier> messages, 2196 string? executed_label = null, 2197 string? undone_label = null) { 2198 base(source, conversations, messages); 2199 this.source = source; 2200 this.executed_label = executed_label; 2201 this.executed_notification_brief = true; 2202 this.undone_label = undone_label; 2203 } 2204 2205 internal override EmailCommand.StateChangePolicy folders_removed( 2206 Gee.Collection<Geary.Folder> removed 2207 ) { 2208 EmailCommand.StateChangePolicy ret = base.folders_removed(removed); 2209 if (ret == IGNORE) { 2210 // With the current revokable mechanism we can't determine 2211 // if specific messages removed from the destination are 2212 // affected, so if the dest is the location, just assume 2213 // they are for now. 2214 foreach (var folder in removed) { 2215 if (folder.used_as == ARCHIVE) { 2216 ret = REMOVE; 2217 break; 2218 } 2219 } 2220 } 2221 return ret; 2222 } 2223 2224 internal override EmailCommand.StateChangePolicy email_removed( 2225 Geary.Folder location, 2226 Gee.Collection<Geary.EmailIdentifier> targets 2227 ) { 2228 // With the current revokable mechanism we can't determine if 2229 // specific messages removed from the destination are 2230 // affected, so if the dest is the location, just assume they 2231 // are for now. 2232 return ( 2233 location.used_as == ARCHIVE 2234 ? EmailCommand.StateChangePolicy.REMOVE 2235 : base.email_removed(location, targets) 2236 ); 2237 } 2238 2239 protected override async Geary.Revokable 2240 execute_impl(GLib.Cancellable cancellable) 2241 throws GLib.Error { 2242 bool open = false; 2243 try { 2244 yield this.source.open_async( 2245 Geary.Folder.OpenFlags.NO_DELAY, cancellable 2246 ); 2247 open = true; 2248 return yield this.source.archive_email_async( 2249 this.email, cancellable 2250 ); 2251 } finally { 2252 if (open) { 2253 try { 2254 yield this.source.close_async(null); 2255 } catch (GLib.Error err) { 2256 // ignored 2257 } 2258 } 2259 } 2260 } 2261 2262} 2263 2264 2265private class Application.CopyEmailCommand : EmailCommand { 2266 2267 2268 public override bool can_undo { 2269 // Engine doesn't yet support it :( 2270 get { return false; } 2271 } 2272 2273 private Geary.FolderSupport.Copy source; 2274 private Geary.Folder destination; 2275 2276 2277 public CopyEmailCommand(Geary.FolderSupport.Copy source, 2278 Geary.Folder destination, 2279 Gee.Collection<Geary.App.Conversation> conversations, 2280 Gee.Collection<Geary.EmailIdentifier> messages, 2281 string? executed_label = null, 2282 string? undone_label = null) { 2283 base(source, conversations, messages); 2284 this.source = source; 2285 this.destination = destination; 2286 2287 this.executed_label = executed_label; 2288 this.undone_label = undone_label; 2289 } 2290 2291 public override async void execute(GLib.Cancellable? cancellable) 2292 throws GLib.Error { 2293 bool open = false; 2294 try { 2295 yield this.source.open_async( 2296 Geary.Folder.OpenFlags.NO_DELAY, cancellable 2297 ); 2298 open = true; 2299 yield this.source.copy_email_async( 2300 this.email, this.destination.path, cancellable 2301 ); 2302 } finally { 2303 if (open) { 2304 try { 2305 yield this.source.close_async(null); 2306 } catch (GLib.Error err) { 2307 // ignored 2308 } 2309 } 2310 } 2311 } 2312 2313 public override async void undo(GLib.Cancellable? cancellable) 2314 throws GLib.Error { 2315 throw new Geary.EngineError.UNSUPPORTED( 2316 "Cannot undo copy, not yet supported" 2317 ); 2318 } 2319 2320 internal override EmailCommand.StateChangePolicy folders_removed( 2321 Gee.Collection<Geary.Folder> removed 2322 ) { 2323 return ( 2324 this.destination in removed 2325 ? EmailCommand.StateChangePolicy.REMOVE 2326 : base.folders_removed(removed) 2327 ); 2328 } 2329 2330 internal override EmailCommand.StateChangePolicy email_removed( 2331 Geary.Folder location, 2332 Gee.Collection<Geary.EmailIdentifier> targets 2333 ) { 2334 // With the current revokable mechanism we can't determine if 2335 // specific messages removed from the destination are 2336 // affected, so if the dest is the location, just assume they 2337 // are for now. 2338 return ( 2339 location == this.destination 2340 ? EmailCommand.StateChangePolicy.REMOVE 2341 : base.email_removed(location, targets) 2342 ); 2343 } 2344 2345} 2346 2347 2348private class Application.DeleteEmailCommand : EmailCommand { 2349 2350 2351 public override bool can_undo { 2352 get { return false; } 2353 } 2354 2355 private Geary.FolderSupport.Remove target; 2356 2357 2358 public DeleteEmailCommand(Geary.FolderSupport.Remove target, 2359 Gee.Collection<Geary.App.Conversation> conversations, 2360 Gee.Collection<Geary.EmailIdentifier> email) { 2361 base(target, conversations, email); 2362 this.target = target; 2363 } 2364 2365 public override async void execute(GLib.Cancellable? cancellable) 2366 throws GLib.Error { 2367 bool open = false; 2368 try { 2369 yield this.target.open_async( 2370 Geary.Folder.OpenFlags.NO_DELAY, cancellable 2371 ); 2372 open = true; 2373 yield this.target.remove_email_async(this.email, cancellable); 2374 } finally { 2375 if (open) { 2376 try { 2377 yield this.target.close_async(null); 2378 } catch (GLib.Error err) { 2379 // ignored 2380 } 2381 } 2382 } 2383 } 2384 2385 public override async void undo(GLib.Cancellable? cancellable) 2386 throws GLib.Error { 2387 throw new Geary.EngineError.UNSUPPORTED( 2388 "Cannot undo emptying a folder: %s", 2389 this.target.path.to_string() 2390 ); 2391 } 2392 2393} 2394 2395 2396private class Application.EmptyFolderCommand : Command { 2397 2398 2399 public override bool can_undo { 2400 get { return false; } 2401 } 2402 2403 private Geary.FolderSupport.Empty target; 2404 2405 2406 public EmptyFolderCommand(Geary.FolderSupport.Empty target) { 2407 this.target = target; 2408 } 2409 2410 public override async void execute(GLib.Cancellable? cancellable) 2411 throws GLib.Error { 2412 bool open = false; 2413 try { 2414 yield this.target.open_async( 2415 Geary.Folder.OpenFlags.NO_DELAY, cancellable 2416 ); 2417 open = true; 2418 yield this.target.empty_folder_async(cancellable); 2419 } finally { 2420 if (open) { 2421 try { 2422 yield this.target.close_async(null); 2423 } catch (GLib.Error err) { 2424 // ignored 2425 } 2426 } 2427 } 2428 } 2429 2430 public override async void undo(GLib.Cancellable? cancellable) 2431 throws GLib.Error { 2432 throw new Geary.EngineError.UNSUPPORTED( 2433 "Cannot undo emptying a folder: %s", 2434 this.target.path.to_string() 2435 ); 2436 } 2437 2438 /** Determines if this command is equal to another. */ 2439 public override bool equal_to(Command other) { 2440 EmptyFolderCommand? other_type = other as EmptyFolderCommand; 2441 return (other_type != null && this.target == other_type.target); 2442 } 2443 2444} 2445 2446 2447private abstract class Application.ComposerCommand : Command { 2448 2449 2450 public override bool can_redo { 2451 get { return false; } 2452 } 2453 2454 protected Composer.Widget? composer { get; private set; } 2455 2456 2457 protected ComposerCommand(Composer.Widget composer) { 2458 this.composer = composer; 2459 } 2460 2461 protected void clear_composer() { 2462 this.composer = null; 2463 } 2464 2465 protected void close_composer() { 2466 // Calling close then immediately erasing the reference looks 2467 // sketchy, but works since Controller still maintains a 2468 // reference to the composer until it destroys itself. 2469 this.composer.close.begin(); 2470 this.composer = null; 2471 } 2472 2473} 2474 2475 2476private class Application.SendComposerCommand : ComposerCommand { 2477 2478 2479 public override bool can_undo { 2480 get { return this.application.config.undo_send_delay > 0; } 2481 } 2482 2483 private Client application; 2484 private AccountContext context; 2485 private Geary.Smtp.ClientService smtp; 2486 private Geary.TimeoutManager commit_timer; 2487 private Geary.EmailIdentifier? saved = null; 2488 2489 2490 public SendComposerCommand(Client application, 2491 AccountContext context, 2492 Composer.Widget composer) { 2493 base(composer); 2494 this.application = application; 2495 this.context = context; 2496 this.smtp = (Geary.Smtp.ClientService) context.account.outgoing; 2497 2498 int send_delay = this.application.config.undo_send_delay; 2499 this.commit_timer = new Geary.TimeoutManager.seconds( 2500 send_delay > 0 ? send_delay : 0, 2501 on_commit_timeout 2502 ); 2503 } 2504 2505 public override async void execute(GLib.Cancellable? cancellable) 2506 throws GLib.Error { 2507 Geary.ComposedEmail email = yield this.composer.to_composed_email(); 2508 if (this.can_undo) { 2509 /// Translators: The label for an in-app notification. The 2510 /// string substitution is a list of recipients of the email. 2511 this.executed_label = _( 2512 "Email to %s queued for delivery" 2513 ).printf(Util.Email.to_short_recipient_display(email)); 2514 2515 this.saved = yield this.smtp.save_email(email, cancellable); 2516 this.commit_timer.start(); 2517 } else { 2518 yield this.smtp.send_email(email, cancellable); 2519 } 2520 } 2521 2522 public override async void undo(GLib.Cancellable? cancellable) 2523 throws GLib.Error { 2524 this.commit_timer.reset(); 2525 yield this.smtp.outbox.remove_email_async( 2526 Geary.Collection.single(this.saved), 2527 cancellable 2528 ); 2529 this.saved = null; 2530 2531 this.composer.set_enabled(true); 2532 this.application.controller.present_composer(this.composer); 2533 clear_composer(); 2534 } 2535 2536 private void on_commit_timeout() { 2537 this.smtp.queue_email(this.saved); 2538 this.saved = null; 2539 close_composer(); 2540 } 2541 2542} 2543 2544 2545private class Application.SaveComposerCommand : ComposerCommand { 2546 2547 2548 private const int DESTROY_TIMEOUT_SEC = 30 * 60; 2549 2550 public override bool can_redo { 2551 get { return false; } 2552 } 2553 2554 private Controller controller; 2555 2556 private Geary.TimeoutManager destroy_timer; 2557 2558 2559 public SaveComposerCommand(Controller controller, 2560 Composer.Widget composer) { 2561 base(composer); 2562 this.controller = controller; 2563 2564 this.destroy_timer = new Geary.TimeoutManager.seconds( 2565 DESTROY_TIMEOUT_SEC, 2566 on_destroy_timeout 2567 ); 2568 } 2569 2570 public override async void execute(GLib.Cancellable? cancellable) 2571 throws GLib.Error { 2572 Geary.ComposedEmail email = yield this.composer.to_composed_email(); 2573 /// Translators: The label for an in-app notification. The 2574 /// string substitution is a list of recipients of the email. 2575 this.executed_label = _( 2576 "Email to %s saved" 2577 ).printf(Util.Email.to_short_recipient_display(email)); 2578 this.destroy_timer.start(); 2579 } 2580 2581 public override async void undo(GLib.Cancellable? cancellable) 2582 throws GLib.Error { 2583 if (this.composer != null) { 2584 this.destroy_timer.reset(); 2585 this.composer.set_enabled(true); 2586 this.controller.present_composer(this.composer); 2587 clear_composer(); 2588 } else { 2589 /// Translators: A label for an in-app notification. 2590 this.undone_label = _( 2591 "Composer could not be restored" 2592 ); 2593 } 2594 } 2595 2596 private void on_destroy_timeout() { 2597 close_composer(); 2598 } 2599 2600} 2601 2602 2603private class Application.DiscardComposerCommand : ComposerCommand { 2604 2605 2606 private const int DESTROY_TIMEOUT_SEC = 30 * 60; 2607 2608 public override bool can_redo { 2609 get { return false; } 2610 } 2611 2612 private Controller controller; 2613 2614 private Geary.TimeoutManager destroy_timer; 2615 2616 2617 public DiscardComposerCommand(Controller controller, 2618 Composer.Widget composer) { 2619 base(composer); 2620 this.controller = controller; 2621 2622 this.destroy_timer = new Geary.TimeoutManager.seconds( 2623 DESTROY_TIMEOUT_SEC, 2624 on_destroy_timeout 2625 ); 2626 } 2627 2628 public override async void execute(GLib.Cancellable? cancellable) 2629 throws GLib.Error { 2630 Geary.ComposedEmail email = yield this.composer.to_composed_email(); 2631 /// Translators: The label for an in-app notification. The 2632 /// string substitution is a list of recipients of the email. 2633 this.executed_label = _( 2634 "Email to %s discarded" 2635 ).printf(Util.Email.to_short_recipient_display(email)); 2636 this.destroy_timer.start(); 2637 } 2638 2639 public override async void undo(GLib.Cancellable? cancellable) 2640 throws GLib.Error { 2641 if (this.composer != null) { 2642 this.destroy_timer.reset(); 2643 this.composer.set_enabled(true); 2644 this.controller.present_composer(this.composer); 2645 clear_composer(); 2646 } else { 2647 /// Translators: A label for an in-app notification. 2648 this.undone_label = _( 2649 "Composer could not be restored" 2650 ); 2651 } 2652 } 2653 2654 private void on_destroy_timeout() { 2655 close_composer(); 2656 } 2657 2658} 2659