From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mail/components/AboutRedirector.jsm | 124 + comm/mail/components/AppIdleManager.jsm | 45 + comm/mail/components/MailComponents.manifest | 9 + comm/mail/components/MailGlue.jsm | 1380 + comm/mail/components/MessengerContentHandler.jsm | 793 + comm/mail/components/StartupRecorder.jsm | 229 + .../components/about-support/AboutSupportMac.jsm | 16 + .../components/about-support/AboutSupportUnix.jsm | 137 + .../components/about-support/AboutSupportWin32.jsm | 77 + .../about-support/content/aboutSupport.js | 1729 + .../about-support/content/aboutSupport.xhtml | 956 + .../components/about-support/content/accounts.js | 339 + .../components/about-support/content/calendars.js | 77 + comm/mail/components/about-support/content/chat.js | 73 + .../components/about-support/content/export.js | 288 + comm/mail/components/about-support/content/libs.js | 24 + comm/mail/components/about-support/jar.mn | 13 + comm/mail/components/about-support/moz.build | 13 + .../components/accountcreation/AccountConfig.jsm | 463 + .../accountcreation/AccountCreationUtils.jsm | 717 + .../components/accountcreation/ConfigVerifier.jsm | 386 + .../components/accountcreation/CreateInBackend.jsm | 459 + .../accountcreation/ExchangeAutoDiscover.jsm | 676 + .../components/accountcreation/FetchConfig.jsm | 299 + comm/mail/components/accountcreation/FetchHTTP.jsm | 401 + .../components/accountcreation/GuessConfig.jsm | 1317 + comm/mail/components/accountcreation/Sanitizer.jsm | 249 + .../accountcreation/content/accountHub.js | 277 + .../accountcreation/content/accountSetup.js | 3023 ++ .../accountcreation/content/accountSetup.xhtml | 1333 + comm/mail/components/accountcreation/jar.mn | 12 + comm/mail/components/accountcreation/moz.build | 23 + .../components/accountcreation/readFromXML.jsm | 352 + .../templates/accountHubTemplate.inc.xhtml | 158 + .../test/xpcshell/data/example.com.xml | 21 + .../test/xpcshell/test_autoconfigFetchDisk.js | 76 + .../test/xpcshell/test_autoconfigUtils.js | 319 + .../test/xpcshell/test_autoconfigXML.js | 266 + .../accountcreation/test/xpcshell/xpcshell.ini | 9 + .../components/accountcreation/views/container.mjs | 50 + .../components/accountcreation/views/email.mjs | 185 + .../components/accountcreation/views/start.mjs | 163 + comm/mail/components/activity/Activity.jsm | 322 + comm/mail/components/activity/ActivityManager.jsm | 157 + .../mail/components/activity/ActivityManagerUI.jsm | 47 + comm/mail/components/activity/components.conf | 38 + .../activity/content/activity-widgets.js | 384 + comm/mail/components/activity/content/activity.js | 239 + .../components/activity/content/activity.xhtml | 61 + comm/mail/components/activity/jar.mn | 8 + .../activity/modules/activityModules.jsm | 33 + .../mail/components/activity/modules/alertHook.jsm | 101 + comm/mail/components/activity/modules/autosync.jsm | 433 + .../components/activity/modules/glodaIndexer.jsm | 251 + comm/mail/components/activity/modules/moveCopy.jsm | 396 + .../components/activity/modules/pop3Download.jsm | 154 + .../mail/components/activity/modules/sendLater.jsm | 298 + comm/mail/components/activity/moz.build | 34 + comm/mail/components/activity/nsIActivity.idl | 492 + .../components/activity/nsIActivityManager.idl | 135 + .../components/activity/nsIActivityManagerUI.idl | 50 + comm/mail/components/addrbook/content/abCommon.js | 145 + .../components/addrbook/content/abContactsPanel.js | 374 + .../addrbook/content/abContactsPanel.xhtml | 234 + .../addrbook/content/abEditListDialog.xhtml | 99 + .../addrbook/content/abMailListDialog.xhtml | 116 + .../components/addrbook/content/abSearchDialog.js | 408 + .../addrbook/content/abSearchDialog.xhtml | 200 + .../mail/components/addrbook/content/abView-new.js | 577 + .../addrbook/content/aboutAddressBook.js | 4445 +++ .../addrbook/content/aboutAddressBook.xhtml | 460 + .../components/addrbook/content/addressBookTab.js | 172 + .../addrbook/content/menulist-addrbooks.js | 271 + .../components/addrbook/content/vcard-edit/adr.mjs | 149 + .../addrbook/content/vcard-edit/custom.mjs | 60 + .../addrbook/content/vcard-edit/edit.mjs | 1094 + .../addrbook/content/vcard-edit/email.mjs | 135 + .../components/addrbook/content/vcard-edit/fn.mjs | 71 + .../addrbook/content/vcard-edit/id-gen.mjs | 12 + .../addrbook/content/vcard-edit/impp.mjs | 97 + .../components/addrbook/content/vcard-edit/n.mjs | 186 + .../addrbook/content/vcard-edit/nickname.mjs | 59 + .../addrbook/content/vcard-edit/note.mjs | 82 + .../components/addrbook/content/vcard-edit/org.mjs | 197 + .../addrbook/content/vcard-edit/special-date.mjs | 269 + .../components/addrbook/content/vcard-edit/tel.mjs | 83 + .../components/addrbook/content/vcard-edit/tz.mjs | 86 + .../components/addrbook/content/vcard-edit/url.mjs | 89 + .../content/vcard-edit/vCardTemplates.inc.xhtml | 398 + comm/mail/components/addrbook/jar.mn | 35 + comm/mail/components/addrbook/moz.build | 10 + .../components/addrbook/test/browser/browser.ini | 37 + .../addrbook/test/browser/browser_cardDAV_init.js | 664 + .../addrbook/test/browser/browser_cardDAV_oAuth.js | 143 + .../test/browser/browser_cardDAV_properties.js | 245 + .../addrbook/test/browser/browser_cardDAV_sync.js | 138 + .../test/browser/browser_contact_sidebar.js | 470 + .../addrbook/test/browser/browser_contact_tree.js | 1261 + .../test/browser/browser_directory_tree.js | 982 + .../addrbook/test/browser/browser_display_card.js | 1020 + .../test/browser/browser_display_multiple.js | 468 + .../addrbook/test/browser/browser_drag_drop.js | 417 + .../addrbook/test/browser/browser_edit_async.js | 363 + .../addrbook/test/browser/browser_edit_card.js | 3517 ++ .../addrbook/test/browser/browser_edit_photo.js | 866 + .../addrbook/test/browser/browser_ldap_search.js | 180 + .../addrbook/test/browser/browser_mailing_lists.js | 474 + .../addrbook/test/browser/browser_open_actions.js | 157 + .../addrbook/test/browser/browser_search.js | 139 + .../addrbook/test/browser/browser_telemetry.js | 59 + .../addrbook/test/browser/data/addressbook.sjs | 47 + .../addrbook/test/browser/data/addressbooks.sjs | 62 + .../addrbook/test/browser/data/auth_headers.sjs | 26 + .../components/addrbook/test/browser/data/dns.sjs | 48 + .../addrbook/test/browser/data/photo1.jpg | Bin 0 -> 36775 bytes .../addrbook/test/browser/data/photo2.jpg | Bin 0 -> 38826 bytes .../addrbook/test/browser/data/principal.sjs | 38 + .../addrbook/test/browser/data/redirect_auto.sjs | 21 + .../addrbook/test/browser/data/token.sjs | 36 + comm/mail/components/addrbook/test/browser/head.js | 445 + .../components/cloudfile/cloudFileAccounts.jsm | 215 + .../components/cloudfile/content/selectDialog.js | 17 + .../cloudfile/content/selectDialog.xhtml | 32 + comm/mail/components/cloudfile/jar.mn | 7 + comm/mail/components/cloudfile/moz.build | 14 + .../components/cloudfile/test/browser/browser.ini | 13 + .../test/browser/browser_repeat_upload.js | 246 + .../cloudfile/test/browser/files/green_eggs.txt | 1 + .../cloudfile/test/browser/files/icon.svg | 7 + .../cloudfile/test/browser/files/management.html | 10 + .../mail/components/cloudfile/test/browser/head.js | 48 + comm/mail/components/components.conf | 76 + comm/mail/components/compose/composer.js | 65 + .../components/compose/content/ComposerCommands.js | 2261 ++ .../compose/content/MsgComposeCommands.js | 11654 ++++++ .../compose/content/addressingWidgetOverlay.js | 1336 + .../components/compose/content/bigFileObserver.js | 368 + .../compose/content/cloudAttachmentLinkManager.js | 758 + .../compose/content/dialogs/EdAEAttributes.js | 973 + .../compose/content/dialogs/EdAECSSAttributes.js | 146 + .../compose/content/dialogs/EdAEHTMLAttributes.js | 362 + .../compose/content/dialogs/EdAEJSEAttributes.js | 200 + .../compose/content/dialogs/EdAdvancedEdit.js | 342 + .../compose/content/dialogs/EdAdvancedEdit.xhtml | 243 + .../compose/content/dialogs/EdColorPicker.js | 290 + .../compose/content/dialogs/EdColorPicker.xhtml | 103 + .../compose/content/dialogs/EdColorProps.js | 476 + .../compose/content/dialogs/EdColorProps.xhtml | 211 + .../compose/content/dialogs/EdConvertToTable.js | 325 + .../compose/content/dialogs/EdConvertToTable.xhtml | 86 + .../compose/content/dialogs/EdDialogCommon.js | 679 + .../compose/content/dialogs/EdDictionary.js | 138 + .../compose/content/dialogs/EdDictionary.xhtml | 88 + .../compose/content/dialogs/EdHLineProps.js | 227 + .../compose/content/dialogs/EdHLineProps.xhtml | 131 + .../compose/content/dialogs/EdImageDialog.js | 639 + .../compose/content/dialogs/EdImageLinkLoader.js | 144 + .../compose/content/dialogs/EdImageProps.js | 293 + .../compose/content/dialogs/EdImageProps.xhtml | 454 + .../components/compose/content/dialogs/EdInsSrc.js | 162 + .../compose/content/dialogs/EdInsSrc.xhtml | 67 + .../compose/content/dialogs/EdInsertChars.js | 412 + .../compose/content/dialogs/EdInsertChars.xhtml | 92 + .../compose/content/dialogs/EdInsertMath.js | 317 + .../compose/content/dialogs/EdInsertMath.xhtml | 73 + .../compose/content/dialogs/EdInsertTOC.js | 378 + .../compose/content/dialogs/EdInsertTOC.xhtml | 505 + .../compose/content/dialogs/EdInsertTable.js | 258 + .../compose/content/dialogs/EdInsertTable.xhtml | 126 + .../compose/content/dialogs/EdLinkProps.js | 323 + .../compose/content/dialogs/EdLinkProps.xhtml | 112 + .../compose/content/dialogs/EdListProps.js | 455 + .../compose/content/dialogs/EdListProps.xhtml | 101 + .../compose/content/dialogs/EdNamedAnchorProps.js | 159 + .../content/dialogs/EdNamedAnchorProps.xhtml | 67 + .../compose/content/dialogs/EdReplace.js | 380 + .../compose/content/dialogs/EdReplace.xhtml | 126 + .../compose/content/dialogs/EdSpellCheck.js | 496 + .../compose/content/dialogs/EdSpellCheck.xhtml | 209 + .../compose/content/dialogs/EdTableProps.js | 1426 + .../compose/content/dialogs/EdTableProps.xhtml | 472 + .../compose/content/editFormatButtons.inc.xhtml | 282 + comm/mail/components/compose/content/editor.js | 2392 ++ .../components/compose/content/editorUtilities.js | 1015 + .../compose/content/images/tag-anchor.gif | Bin 0 -> 127 bytes .../compose/content/messengercompose.xhtml | 2572 ++ comm/mail/components/compose/jar.mn | 58 + comm/mail/components/compose/moz.build | 8 + comm/mail/components/compose/texzilla/TeXZilla.js | 339 + .../customizableui/CustomizableUI.sys.mjs | 360 + .../customizableui/PanelMultiView.sys.mjs | 1699 + .../customizableui/content/customizeMode.inc.xhtml | 128 + comm/mail/components/customizableui/content/jar.mn | 6 + .../components/customizableui/content/moz.build | 7 + .../customizableui/content/panelUI.inc.xhtml | 606 + .../components/customizableui/content/panelUI.js | 882 + comm/mail/components/customizableui/moz.build | 14 + comm/mail/components/devtools/components.conf | 15 + comm/mail/components/devtools/devtools-loader.jsm | 80 + comm/mail/components/devtools/moz.build | 13 + comm/mail/components/devtools/tb-root-actor.js | 104 + .../components/downloads/content/aboutDownloads.js | 414 + .../downloads/content/aboutDownloads.xhtml | 98 + comm/mail/components/downloads/jar.mn | 7 + comm/mail/components/downloads/moz.build | 5 + .../components/enterprisepolicies/Policies.sys.mjs | 1758 + .../enterprisepolicies/content/aboutPolicies.js | 410 + .../enterprisepolicies/content/aboutPolicies.xhtml | 107 + .../enterprisepolicies/content/policies-active.svg | 6 + .../content/policies-documentation.svg | 6 + .../enterprisepolicies/content/policies-error.svg | 6 + .../helpers/ProxyPolicies.sys.mjs | 111 + .../enterprisepolicies/helpers/moz.build | 12 + comm/mail/components/enterprisepolicies/jar.mn | 10 + comm/mail/components/enterprisepolicies/moz.build | 23 + .../enterprisepolicies/schemas/configuration.json | 10 + .../enterprisepolicies/schemas/moz.build | 12 + .../schemas/policies-schema.json | 634 + .../enterprisepolicies/schemas/schema.sys.mjs | 16 + .../enterprisepolicies/tests/browser/browser.ini | 32 + .../browser/browser_policies_setAndLockPref_API.js | 179 + .../browser/browser_policy_app_auto_update.js | 92 + .../tests/browser/browser_policy_app_update.js | 41 + .../browser_policy_background_app_update.js | 104 + .../tests/browser/browser_policy_block_about.js | 87 + .../browser/browser_policy_cookie_settings.js | 323 + .../browser_policy_disable_masterpassword.js | 90 + .../browser/browser_policy_disable_safemode.js | 49 + .../browser/browser_policy_disable_telemetry.js | 21 + .../tests/browser/browser_policy_downloads.js | 147 + .../tests/browser/browser_policy_extensions.js | 120 + .../browser/browser_policy_extensionsettings.js | 261 + .../browser/browser_policy_extensionsettings2.js | 73 + .../tests/browser/browser_policy_handlers.js | 183 + .../tests/browser/browser_policy_masterpassword.js | 104 + .../browser/browser_policy_passwordmanager.js | 27 + .../tests/browser/disable_app_update/browser.ini | 15 + .../browser_policy_disable_app_update.js | 109 + .../config_disable_app_update.json | 5 + .../browser/disable_developer_tools/browser.ini | 13 + .../browser_policy_disable_developer_tools.js | 55 + .../config_disable_developer_tools.json | 5 + .../tests/browser/extensionsettings.html | 23 + .../browser/hardware_acceleration/browser.ini | 12 + .../browser_policy_hardware_acceleration.js | 9 + .../disable_hardware_acceleration.json | 5 + .../enterprisepolicies/tests/browser/head.js | 103 + .../tests/browser/policytest_v0.1.xpi | Bin 0 -> 305 bytes .../tests/browser/policytest_v0.2.xpi | Bin 0 -> 297 bytes .../components/enterprisepolicies/tests/moz.build | 16 + .../enterprisepolicies/tests/xpcshell/head.js | 140 + .../tests/xpcshell/test_3rdparty.js | 22 + .../tests/xpcshell/test_appupdatepin.js | 80 + .../tests/xpcshell/test_appupdateurl.js | 25 + .../tests/xpcshell/test_bug1658259.js | 44 + .../tests/xpcshell/test_clear_blocked_cookies.js | 118 + .../tests/xpcshell/test_macosparser_unflatten.js | 110 + .../tests/xpcshell/test_policy_search_engine.js | 490 + .../tests/xpcshell/test_preferences.js | 255 + .../tests/xpcshell/test_proxy.js | 122 + .../tests/xpcshell/test_requestedlocales.js | 47 + .../tests/xpcshell/test_runOnce_helper.js | 21 + .../tests/xpcshell/test_simple_pref_policies.js | 378 + .../tests/xpcshell/test_sorted_alphabetically.js | 48 + .../enterprisepolicies/tests/xpcshell/xpcshell.ini | 18 + .../extensions/ExtensionBrowsingData.sys.mjs | 78 + .../components/extensions/ExtensionPopups.sys.mjs | 635 + .../extensions/ExtensionToolbarButtons.jsm | 949 + .../extensions/MailExtensionShortcuts.jsm | 87 + comm/mail/components/extensions/child/.eslintrc.js | 15 + .../extensions/child/ext-extensionScripts.js | 83 + comm/mail/components/extensions/child/ext-mail.js | 28 + comm/mail/components/extensions/child/ext-menus.js | 290 + comm/mail/components/extensions/child/ext-tabs.js | 23 + comm/mail/components/extensions/ext-mail.json | 171 + comm/mail/components/extensions/extension.svg | 19 + comm/mail/components/extensions/extensionPopup.js | 557 + .../components/extensions/extensionPopup.xhtml | 92 + .../components/extensions/extensions-mail.manifest | 4 + comm/mail/components/extensions/jar.mn | 68 + comm/mail/components/extensions/moz.build | 27 + .../mail/components/extensions/parent/.eslintrc.js | 81 + .../components/extensions/parent/ext-accounts.js | 283 + .../extensions/parent/ext-addressBook.js | 1587 + .../extensions/parent/ext-browserAction.js | 329 + .../parent/ext-chrome-settings-overrides.js | 365 + .../components/extensions/parent/ext-cloudFile.js | 804 + .../components/extensions/parent/ext-commands.js | 103 + .../components/extensions/parent/ext-compose.js | 1703 + .../extensions/parent/ext-composeAction.js | 154 + .../extensions/parent/ext-extensionScripts.js | 185 + .../components/extensions/parent/ext-folders.js | 675 + .../components/extensions/parent/ext-identities.js | 360 + comm/mail/components/extensions/parent/ext-mail.js | 2883 ++ .../components/extensions/parent/ext-mailTabs.js | 485 + .../mail/components/extensions/parent/ext-menus.js | 1544 + .../extensions/parent/ext-messageDisplay.js | 348 + .../extensions/parent/ext-messageDisplayAction.js | 251 + .../components/extensions/parent/ext-messages.js | 1563 + .../components/extensions/parent/ext-sessions.js | 62 + .../components/extensions/parent/ext-spaces.js | 364 + .../extensions/parent/ext-spacesToolbar.js | 308 + comm/mail/components/extensions/parent/ext-tabs.js | 822 + .../mail/components/extensions/parent/ext-theme.js | 543 + .../components/extensions/parent/ext-windows.js | 555 + comm/mail/components/extensions/processScript.js | 71 + comm/mail/components/extensions/schemas/LICENSE | 27 + .../components/extensions/schemas/accounts.json | 235 + .../components/extensions/schemas/addressBook.json | 977 + .../extensions/schemas/browserAction.json | 848 + .../schemas/chrome_settings_overrides.json | 194 + .../components/extensions/schemas/cloudFile.json | 501 + .../components/extensions/schemas/commands.json | 279 + .../components/extensions/schemas/compose.json | 937 + .../extensions/schemas/composeAction.json | 722 + .../extensions/schemas/extensionScripts.json | 133 + .../components/extensions/schemas/folders.json | 408 + .../components/extensions/schemas/identities.json | 277 + .../components/extensions/schemas/mailTabs.json | 428 + comm/mail/components/extensions/schemas/menus.json | 757 + .../components/extensions/schemas/menus_child.json | 31 + .../extensions/schemas/messageDisplay.json | 159 + .../extensions/schemas/messageDisplayAction.json | 721 + .../components/extensions/schemas/messages.json | 933 + .../components/extensions/schemas/sessions.json | 76 + .../mail/components/extensions/schemas/spaces.json | 290 + .../extensions/schemas/spacesToolbar.json | 175 + comm/mail/components/extensions/schemas/tabs.json | 989 + comm/mail/components/extensions/schemas/theme.json | 542 + .../components/extensions/schemas/windows.json | 511 + .../extensions/test/AppUiTestDelegate.sys.mjs | 6 + .../extensions/test/browser/.eslintrc.js | 7 + .../components/extensions/test/browser/browser.ini | 135 + .../test/browser/browser_ext_addressBooksUI.js | 116 + .../browser_ext_browserAction_customized.js | 29 + .../browser_ext_browserAction_not_customized.js | 17 + .../browser_ext_browserAction_popup_click.js | 399 + ...xt_browserAction_popup_click_mv3_event_pages.js | 82 + .../browser_ext_browserAction_properties.js | 348 + .../test/browser/browser_ext_bug1812530.js | 200 + .../test/browser/browser_ext_clickHandler.js | 614 + .../test/browser/browser_ext_cloudFile.js | 1444 + .../browser_ext_commands_execute_browser_action.js | 226 + .../browser_ext_commands_execute_compose_action.js | 138 + ..._ext_commands_execute_message_display_action.js | 168 + .../test/browser/browser_ext_commands_getAll.js | 142 + .../test/browser/browser_ext_commands_onChanged.js | 59 + .../test/browser/browser_ext_commands_onCommand.js | 577 + .../browser_ext_commands_onCommand_bug1845236.js | 74 + .../test/browser/browser_ext_commands_update.js | 357 + .../test/browser/browser_ext_composeAction.js | 268 + .../browser_ext_composeAction_popup_click.js | 266 + ...xt_composeAction_popup_click_mv3_event_pages.js | 52 + .../browser_ext_composeAction_properties.js | 125 + .../test/browser/browser_ext_composeScripts.js | 531 + .../browser/browser_ext_compose_attachments.js | 2268 ++ .../browser_ext_compose_begin_attachments.js | 116 + .../test/browser/browser_ext_compose_begin_body.js | 397 + .../browser_ext_compose_begin_bug1691254.js | 141 + .../browser/browser_ext_compose_begin_forward.js | 339 + .../browser/browser_ext_compose_begin_headers.js | 178 + .../browser/browser_ext_compose_begin_identity.js | 102 + .../test/browser/browser_ext_compose_begin_new.js | 136 + .../browser/browser_ext_compose_begin_reply.js | 146 + .../test/browser/browser_ext_compose_bug1692439.js | 160 + .../test/browser/browser_ext_compose_bug1804796.js | 80 + .../test/browser/browser_ext_compose_details.js | 725 + .../browser/browser_ext_compose_details_body.js | 469 + .../browser/browser_ext_compose_details_headers.js | 727 + .../browser/browser_ext_compose_dictionaries.js | 214 + .../browser/browser_ext_compose_onBeforeSend.js | 1010 + .../test/browser/browser_ext_compose_saveDraft.js | 416 + .../browser/browser_ext_compose_saveTemplate.js | 432 + .../browser/browser_ext_compose_sendMessage.js | 733 + .../test/browser/browser_ext_contentScripts.js | 438 + .../test/browser/browser_ext_content_handler.js | 334 + .../browser_ext_content_tabs_navigation_menu.js | 250 + .../test/browser/browser_ext_mailTabs.js | 898 + .../test/browser/browser_ext_mailTabs_mv3.js | 162 + .../browser/browser_ext_menus_context_action.js | 424 + .../browser/browser_ext_menus_context_compose.js | 179 + .../browser/browser_ext_menus_context_content.js | 253 + .../browser_ext_menus_context_folder_pane.js | 97 + .../browser_ext_menus_context_message_panes.js | 180 + .../test/browser/browser_ext_menus_context_tabs.js | 77 + .../browser_ext_menus_context_tools_main_menu.js | 156 + .../browser_ext_menus_message_one_attachment.js | 395 + .../browser_ext_menus_message_two_attachments.js | 397 + .../test/browser/browser_ext_menus_popup_action.js | 405 + .../test/browser/browser_ext_menus_replace_menu.js | 582 + .../browser_ext_menus_replace_menu_context.js | 375 + .../test/browser/browser_ext_messageDisplay.js | 1016 + .../browser/browser_ext_messageDisplayAction.js | 337 + ...browser_ext_messageDisplayAction_popup_click.js | 294 + ...ageDisplayAction_popup_click_mv3_event_pages.js | 113 + .../browser_ext_messageDisplayAction_properties.js | 184 + .../browser/browser_ext_messageDisplayScripts.js | 636 + .../browser_ext_messageDisplay_bug1827032.js | 38 + .../browser_ext_messageDisplay_bug1828056.js | 212 + .../browser_ext_messageDisplay_open_file.js | 221 + ...wser_ext_messageDisplay_open_headerMessageId.js | 221 + .../browser_ext_messageDisplay_open_messageId.js | 221 + .../test/browser/browser_ext_message_external.js | 427 + .../browser_ext_messages_open_attachment.js | 107 + .../test/browser/browser_ext_quickFilter.js | 132 + .../test/browser/browser_ext_sessions.js | 90 + .../extensions/test/browser/browser_ext_spaces.js | 1047 + .../test/browser/browser_ext_spacesToolbar.js | 755 + .../test/browser/browser_ext_tabs_content.js | 336 + .../test/browser/browser_ext_tabs_cookieStoreId.js | 275 + .../test/browser/browser_ext_tabs_events.js | 591 + .../test/browser/browser_ext_tabs_move.js | 306 + .../browser_ext_tabs_onCreated_bug1817872.js | 226 + .../test/browser/browser_ext_tabs_query.js | 113 + .../test/browser/browser_ext_tabs_update_reload.js | 578 + .../test/browser/browser_ext_themes_onUpdated.js | 150 + .../browser_ext_tooltip_in_extension_pages.js | 685 + .../extensions/test/browser/browser_ext_windows.js | 439 + .../test/browser/browser_ext_windows_bug1732559.js | 94 + ...wser_ext_windows_create_normal_cookieStoreId.js | 116 + ...owser_ext_windows_create_popup_cookieStoreId.js | 255 + .../test/browser/browser_ext_windows_events.js | 405 + .../test/browser/browser_ext_windows_types.js | 121 + .../extensions/test/browser/data/cloudFile1.txt | 1 + .../extensions/test/browser/data/cloudFile2.txt | 1 + .../extensions/test/browser/data/content.html | 12 + .../extensions/test/browser/data/content_body.html | 1 + .../extensions/test/browser/data/linktest.html | 11 + .../extensions/test/browser/data/tb-logo.png | Bin 0 -> 6462 bytes .../components/extensions/test/browser/head.js | 1533 + .../extensions/test/browser/head_menus.js | 733 + .../browser/messages/attachedMessageSample.eml | 186 + .../test/browser/messages/messageWithLink.eml | 26 + .../extensions/test/browser/test_browserAction.js | 845 + .../extensions/test/xpcshell/.eslintrc.js | 13 + .../extensions/test/xpcshell/data/utils.js | 124 + .../extensions/test/xpcshell/head-imap.js | 12 + .../extensions/test/xpcshell/head-nntp.js | 12 + .../components/extensions/test/xpcshell/head.js | 298 + .../extensions/test/xpcshell/images/redPixel.png | Bin 0 -> 119 bytes .../extensions/test/xpcshell/images/whitePixel.png | Bin 0 -> 69 bytes .../test/xpcshell/messages/alternative.eml | 23 + .../messages/attachedMessageWithMissingHeaders.eml | 35 + .../test/xpcshell/messages/nestedMessages.eml | 127 + .../extensions/test/xpcshell/messages/sample01.eml | 11 + .../extensions/test/xpcshell/messages/sample02.eml | 121 + .../extensions/test/xpcshell/messages/sample03.eml | 43 + .../extensions/test/xpcshell/messages/sample04.eml | 10 + .../extensions/test/xpcshell/messages/sample05.eml | 10 + .../extensions/test/xpcshell/messages/sample06.eml | 8 + .../extensions/test/xpcshell/messages/sample07.eml | 24 + .../extensions/test/xpcshell/test_ext_accounts.js | 1089 + .../xpcshell/test_ext_accounts_mv3_event_pages.js | 220 + .../test/xpcshell/test_ext_addressBook.js | 2043 + .../test/xpcshell/test_ext_addressBook_provider.js | 139 + .../xpcshell/test_ext_addressBook_quickSearch.js | 238 + .../test/xpcshell/test_ext_addressBook_readonly.js | 148 + .../test/xpcshell/test_ext_addressBook_remote.js | 101 + .../extensions/test/xpcshell/test_ext_alias.js | 123 + ...est_ext_browserAction_unifiedtoolbar_restart.js | 350 + .../test/xpcshell/test_ext_experiments.js | 279 + .../extensions/test/xpcshell/test_ext_folders.js | 560 + .../xpcshell/test_ext_folders_mv3_event_pages.js | 374 + .../test_ext_identities_mv3_event_pages.js | 146 + .../extensions/test/xpcshell/test_ext_messages.js | 730 + .../test/xpcshell/test_ext_messages_attachments.js | 499 + .../test/xpcshell/test_ext_messages_get.js | 1073 + .../test/xpcshell/test_ext_messages_id.js | 256 + .../test/xpcshell/test_ext_messages_import.js | 121 + .../xpcshell/test_ext_messages_move_copy_delete.js | 656 + .../test_ext_messages_onNewMailReceived.js | 153 + .../test/xpcshell/test_ext_messages_query.js | 333 + .../test/xpcshell/test_ext_messages_update.js | 415 + .../extensions/test/xpcshell/xpcshell-imap.ini | 7 + .../extensions/test/xpcshell/xpcshell-local.ini | 23 + .../extensions/test/xpcshell/xpcshell-nntp.ini | 7 + .../extensions/test/xpcshell/xpcshell.ini | 17 + comm/mail/components/im/IMIncomingServer.sys.mjs | 359 + comm/mail/components/im/IMProtocolInfo.sys.mjs | 49 + comm/mail/components/im/all-im.js | 14 + comm/mail/components/im/components.conf | 20 + comm/mail/components/im/content/.eslintrc.js | 22 + comm/mail/components/im/content/addbuddy.js | 58 + comm/mail/components/im/content/addbuddy.xhtml | 59 + comm/mail/components/im/content/am-im.js | 291 + comm/mail/components/im/content/am-im.xhtml | 235 + comm/mail/components/im/content/chat-contact.js | 282 + .../im/content/chat-conversation-info.js | 353 + .../components/im/content/chat-conversation.js | 1760 + comm/mail/components/im/content/chat-group.js | 255 + comm/mail/components/im/content/chat-imconv.js | 366 + .../mail/components/im/content/chat-menu.inc.xhtml | 109 + .../components/im/content/chat-messenger.inc.xhtml | 192 + comm/mail/components/im/content/chat-messenger.js | 2162 ++ comm/mail/components/im/content/imAccountWizard.js | 526 + .../components/im/content/imAccountWizard.xhtml | 180 + comm/mail/components/im/content/imAccounts.js | 663 + comm/mail/components/im/content/imAccounts.xhtml | 250 + comm/mail/components/im/content/imContextMenu.js | 276 + .../mail/components/im/content/imStatusSelector.js | 383 + comm/mail/components/im/content/joinchat.js | 195 + comm/mail/components/im/content/joinchat.xhtml | 58 + .../im/content/toolbarbutton-badge-button.js | 70 + comm/mail/components/im/content/verify.js | 53 + comm/mail/components/im/content/verify.xhtml | 46 + comm/mail/components/im/jar.mn | 199 + .../im/messages/bubbles/Bitmaps/indicator_0.png | Bin 0 -> 581 bytes .../messages/bubbles/Bitmaps/indicator_0_alt.png | Bin 0 -> 658 bytes .../im/messages/bubbles/Bitmaps/indicator_10.png | Bin 0 -> 592 bytes .../im/messages/bubbles/Bitmaps/indicator_100.png | Bin 0 -> 596 bytes .../messages/bubbles/Bitmaps/indicator_100_alt.png | Bin 0 -> 678 bytes .../messages/bubbles/Bitmaps/indicator_10_alt.png | Bin 0 -> 666 bytes .../im/messages/bubbles/Bitmaps/indicator_110.png | Bin 0 -> 600 bytes .../messages/bubbles/Bitmaps/indicator_110_alt.png | Bin 0 -> 676 bytes .../im/messages/bubbles/Bitmaps/indicator_120.png | Bin 0 -> 589 bytes .../messages/bubbles/Bitmaps/indicator_120_alt.png | Bin 0 -> 666 bytes .../im/messages/bubbles/Bitmaps/indicator_130.png | Bin 0 -> 602 bytes .../messages/bubbles/Bitmaps/indicator_130_alt.png | Bin 0 -> 677 bytes .../im/messages/bubbles/Bitmaps/indicator_140.png | Bin 0 -> 597 bytes .../messages/bubbles/Bitmaps/indicator_140_alt.png | Bin 0 -> 678 bytes .../im/messages/bubbles/Bitmaps/indicator_150.png | Bin 0 -> 596 bytes .../messages/bubbles/Bitmaps/indicator_150_alt.png | Bin 0 -> 682 bytes .../im/messages/bubbles/Bitmaps/indicator_160.png | Bin 0 -> 600 bytes .../messages/bubbles/Bitmaps/indicator_160_alt.png | Bin 0 -> 678 bytes .../im/messages/bubbles/Bitmaps/indicator_170.png | Bin 0 -> 593 bytes .../messages/bubbles/Bitmaps/indicator_170_alt.png | Bin 0 -> 669 bytes .../im/messages/bubbles/Bitmaps/indicator_180.png | Bin 0 -> 562 bytes .../messages/bubbles/Bitmaps/indicator_180_alt.png | Bin 0 -> 647 bytes .../im/messages/bubbles/Bitmaps/indicator_190.png | Bin 0 -> 588 bytes .../messages/bubbles/Bitmaps/indicator_190_alt.png | Bin 0 -> 667 bytes .../im/messages/bubbles/Bitmaps/indicator_20.png | Bin 0 -> 593 bytes .../im/messages/bubbles/Bitmaps/indicator_200.png | Bin 0 -> 594 bytes .../messages/bubbles/Bitmaps/indicator_200_alt.png | Bin 0 -> 672 bytes .../messages/bubbles/Bitmaps/indicator_20_alt.png | Bin 0 -> 669 bytes .../im/messages/bubbles/Bitmaps/indicator_210.png | Bin 0 -> 590 bytes .../messages/bubbles/Bitmaps/indicator_210_alt.png | Bin 0 -> 672 bytes .../im/messages/bubbles/Bitmaps/indicator_220.png | Bin 0 -> 591 bytes .../messages/bubbles/Bitmaps/indicator_220_alt.png | Bin 0 -> 676 bytes .../im/messages/bubbles/Bitmaps/indicator_230.png | Bin 0 -> 588 bytes .../messages/bubbles/Bitmaps/indicator_230_alt.png | Bin 0 -> 675 bytes .../im/messages/bubbles/Bitmaps/indicator_240.png | Bin 0 -> 578 bytes .../messages/bubbles/Bitmaps/indicator_240_alt.png | Bin 0 -> 662 bytes .../im/messages/bubbles/Bitmaps/indicator_250.png | Bin 0 -> 590 bytes .../messages/bubbles/Bitmaps/indicator_250_alt.png | Bin 0 -> 677 bytes .../im/messages/bubbles/Bitmaps/indicator_260.png | Bin 0 -> 593 bytes .../messages/bubbles/Bitmaps/indicator_260_alt.png | Bin 0 -> 678 bytes .../im/messages/bubbles/Bitmaps/indicator_270.png | Bin 0 -> 589 bytes .../messages/bubbles/Bitmaps/indicator_270_alt.png | Bin 0 -> 673 bytes .../im/messages/bubbles/Bitmaps/indicator_280.png | Bin 0 -> 585 bytes .../messages/bubbles/Bitmaps/indicator_280_alt.png | Bin 0 -> 670 bytes .../im/messages/bubbles/Bitmaps/indicator_290.png | Bin 0 -> 584 bytes .../messages/bubbles/Bitmaps/indicator_290_alt.png | Bin 0 -> 679 bytes .../im/messages/bubbles/Bitmaps/indicator_30.png | Bin 0 -> 594 bytes .../im/messages/bubbles/Bitmaps/indicator_300.png | Bin 0 -> 561 bytes .../messages/bubbles/Bitmaps/indicator_300_alt.png | Bin 0 -> 653 bytes .../messages/bubbles/Bitmaps/indicator_30_alt.png | Bin 0 -> 674 bytes .../im/messages/bubbles/Bitmaps/indicator_310.png | Bin 0 -> 582 bytes .../messages/bubbles/Bitmaps/indicator_310_alt.png | Bin 0 -> 674 bytes .../im/messages/bubbles/Bitmaps/indicator_320.png | Bin 0 -> 589 bytes .../messages/bubbles/Bitmaps/indicator_320_alt.png | Bin 0 -> 672 bytes .../im/messages/bubbles/Bitmaps/indicator_330.png | Bin 0 -> 592 bytes .../messages/bubbles/Bitmaps/indicator_330_alt.png | Bin 0 -> 678 bytes .../im/messages/bubbles/Bitmaps/indicator_340.png | Bin 0 -> 591 bytes .../messages/bubbles/Bitmaps/indicator_340_alt.png | Bin 0 -> 675 bytes .../im/messages/bubbles/Bitmaps/indicator_350.png | Bin 0 -> 592 bytes .../messages/bubbles/Bitmaps/indicator_350_alt.png | Bin 0 -> 667 bytes .../im/messages/bubbles/Bitmaps/indicator_40.png | Bin 0 -> 599 bytes .../messages/bubbles/Bitmaps/indicator_40_alt.png | Bin 0 -> 683 bytes .../im/messages/bubbles/Bitmaps/indicator_50.png | Bin 0 -> 593 bytes .../messages/bubbles/Bitmaps/indicator_50_alt.png | Bin 0 -> 660 bytes .../im/messages/bubbles/Bitmaps/indicator_60.png | Bin 0 -> 525 bytes .../messages/bubbles/Bitmaps/indicator_60_alt.png | Bin 0 -> 590 bytes .../im/messages/bubbles/Bitmaps/indicator_70.png | Bin 0 -> 596 bytes .../messages/bubbles/Bitmaps/indicator_70_alt.png | Bin 0 -> 661 bytes .../im/messages/bubbles/Bitmaps/indicator_80.png | Bin 0 -> 594 bytes .../messages/bubbles/Bitmaps/indicator_80_alt.png | Bin 0 -> 675 bytes .../im/messages/bubbles/Bitmaps/indicator_90.png | Bin 0 -> 596 bytes .../messages/bubbles/Bitmaps/indicator_90_alt.png | Bin 0 -> 680 bytes .../im/messages/bubbles/Bitmaps/indicator_grey.png | Bin 0 -> 608 bytes .../im/messages/bubbles/Bitmaps/minus-hover.png | Bin 0 -> 620 bytes .../im/messages/bubbles/Bitmaps/minus.png | Bin 0 -> 619 bytes .../im/messages/bubbles/Bitmaps/plus-hover.png | Bin 0 -> 615 bytes .../im/messages/bubbles/Bitmaps/plus.png | Bin 0 -> 614 bytes .../components/im/messages/bubbles/Footer.html | 5 + .../im/messages/bubbles/Incoming/Content.html | 7 + .../im/messages/bubbles/Incoming/Context.html | 7 + .../im/messages/bubbles/Incoming/NextContent.html | 3 + .../mail/components/im/messages/bubbles/Info.plist | 41 + .../components/im/messages/bubbles/NextStatus.html | 3 + .../components/im/messages/bubbles/Status.html | 4 + .../im/messages/bubbles/Variants/Blue_-_Green.css | 36 + .../bubbles/Variants/Blue_-_Green_Alternating.css | 36 + .../im/messages/bubbles/Variants/Blue_-_Pink.css | 36 + .../bubbles/Variants/Blue_-_Pink_Alternating.css | 36 + .../im/messages/bubbles/Variants/Blue_-_Red.css | 36 + .../bubbles/Variants/Blue_-_Red_Alternating.css | 36 + .../im/messages/bubbles/Variants/Green_-_Blue.css | 36 + .../bubbles/Variants/Green_-_Blue_Alternating.css | 36 + .../messages/bubbles/Variants/Green_-_Purple.css | 36 + .../Variants/Green_-_Purple_Alternating.css | 36 + .../im/messages/bubbles/Variants/Green_-_Red.css | 36 + .../bubbles/Variants/Green_-_Red_Alternating.css | 36 + .../im/messages/bubbles/Variants/Grey_-_Blue.css | 36 + .../bubbles/Variants/Grey_-_Blue_Alternating.css | 36 + .../im/messages/bubbles/Variants/Grey_-_Pink.css | 36 + .../bubbles/Variants/Grey_-_Pink_Alternating.css | 36 + .../im/messages/bubbles/Variants/Grey_-_Purple.css | 36 + .../bubbles/Variants/Grey_-_Purple_Alternating.css | 36 + .../im/messages/bubbles/Variants/Grey_-_Red.css | 36 + .../bubbles/Variants/Grey_-_Red_Alternating.css | 36 + .../im/messages/bubbles/Variants/Pink_-_Blue.css | 36 + .../bubbles/Variants/Pink_-_Blue_Alternating.css | 36 + .../im/messages/bubbles/Variants/Pink_-_Purple.css | 36 + .../bubbles/Variants/Pink_-_Purple_Alternating.css | 36 + .../messages/bubbles/Variants/Purple_-_Green.css | 36 + .../Variants/Purple_-_Green_Alternating.css | 36 + .../im/messages/bubbles/Variants/Purple_-_Pink.css | 36 + .../bubbles/Variants/Purple_-_Pink_Alternating.css | 36 + .../im/messages/bubbles/Variants/Red_-_Blue.css | 36 + .../bubbles/Variants/Red_-_Blue_Alternating.css | 36 + .../im/messages/bubbles/Variants/Red_-_Green.css | 36 + .../bubbles/Variants/Red_-_Green_Alternating.css | 36 + comm/mail/components/im/messages/bubbles/inline.js | 330 + comm/mail/components/im/messages/bubbles/main.css | 210 + .../im/messages/dark/Incoming/Content.html | 2 + .../im/messages/dark/Incoming/Context.html | 2 + .../im/messages/dark/Incoming/NextContent.html | 2 + .../im/messages/dark/Incoming/NextContext.html | 2 + comm/mail/components/im/messages/dark/Info.plist | 41 + comm/mail/components/im/messages/dark/Status.html | 1 + .../components/im/messages/dark/Variants/Blue.css | 8 + .../components/im/messages/dark/Variants/Green.css | 8 + .../im/messages/dark/Variants/Purple.css | 8 + .../components/im/messages/dark/Variants/Red.css | 8 + .../im/messages/dark/Variants/Yellow.css | 8 + comm/mail/components/im/messages/dark/inline.js | 60 + comm/mail/components/im/messages/dark/main.css | 127 + comm/mail/components/im/messages/mail/Footer.html | 0 comm/mail/components/im/messages/mail/Header.html | 0 .../im/messages/mail/Incoming/Content.html | 1 + .../im/messages/mail/Incoming/Context.html | 1 + .../im/messages/mail/Incoming/NextContent.html | 1 + .../im/messages/mail/Incoming/NextContext.html | 0 .../im/messages/mail/Incoming/buddy_icon.svg | 6 + comm/mail/components/im/messages/mail/Info.plist | 30 + .../components/im/messages/mail/NextStatus.html | 1 + .../im/messages/mail/Outgoing/Content.html | 0 .../im/messages/mail/Outgoing/Context.html | 0 .../im/messages/mail/Outgoing/NextContent.html | 0 .../im/messages/mail/Outgoing/NextContext.html | 0 comm/mail/components/im/messages/mail/Status.html | 1 + .../components/im/messages/mail/Variants/Dark.css | 49 + .../components/im/messages/mail/Variants/Light.css | 49 + comm/mail/components/im/messages/mail/inline.js | 40 + comm/mail/components/im/messages/mail/main.css | 155 + .../messages/papersheets/Bitmaps/information.png | Bin 0 -> 740 bytes .../im/messages/papersheets/Bitmaps/minus.png | Bin 0 -> 196 bytes .../im/messages/papersheets/Bitmaps/plus.png | Bin 0 -> 196 bytes .../im/messages/papersheets/Incoming/Content.html | 4 + .../im/messages/papersheets/Incoming/Context.html | 4 + .../messages/papersheets/Incoming/NextContent.html | 3 + .../components/im/messages/papersheets/Info.plist | 38 + .../im/messages/papersheets/NextStatus.html | 2 + .../components/im/messages/papersheets/Status.html | 4 + .../im/messages/papersheets/Variants/White.css | 22 + .../components/im/messages/papersheets/inline.js | 81 + .../components/im/messages/papersheets/main.css | 208 + .../im/messages/simple/Incoming/Content.html | 1 + .../im/messages/simple/Incoming/Context.html | 1 + .../im/messages/simple/Incoming/NextContext.html | 1 + comm/mail/components/im/messages/simple/Info.plist | 32 + .../mail/components/im/messages/simple/Status.html | 1 + .../im/messages/simple/Variants/Dark.css | 23 + .../im/messages/simple/Variants/Normal.css | 0 comm/mail/components/im/messages/simple/main.css | 90 + .../components/im/modules/ChatEncryption.sys.mjs | 157 + .../components/im/modules/GlodaIMSearcher.sys.mjs | 352 + .../mail/components/im/modules/chatHandler.sys.mjs | 106 + comm/mail/components/im/modules/chatIcons.sys.mjs | 106 + .../im/modules/chatNotifications.sys.mjs | 262 + comm/mail/components/im/modules/index_im.sys.mjs | 928 + comm/mail/components/im/moz.build | 38 + comm/mail/components/im/smileys/theme.json | 22 + comm/mail/components/im/test/TestProtocol.sys.mjs | 308 + comm/mail/components/im/test/browser/browser.ini | 26 + .../im/test/browser/browser_browserRequest.js | 112 + .../im/test/browser/browser_chatNotifications.js | 101 + .../im/test/browser/browser_chatTelemetry.js | 52 + .../im/test/browser/browser_contextMenu.js | 243 + .../components/im/test/browser/browser_logs.js | 97 + .../im/test/browser/browser_messagesMail.js | 235 + .../im/test/browser/browser_readMessage.js | 49 + .../im/test/browser/browser_removeMessage.js | 54 + .../test/browser/browser_requestNotifications.js | 350 + .../im/test/browser/browser_spacesToolbarChat.js | 255 + .../components/im/test/browser/browser_tooltips.js | 194 + .../im/test/browser/browser_updateMessage.js | 62 + comm/mail/components/im/test/browser/head.js | 132 + comm/mail/components/im/test/components.conf | 14 + .../mail/components/migration/content/migration.js | 464 + .../components/migration/content/migration.xhtml | 89 + comm/mail/components/migration/jar.mn | 7 + comm/mail/components/migration/moz.build | 11 + comm/mail/components/migration/public/moz.build | 12 + .../migration/public/nsIMailProfileMigrator.idl | 70 + .../migration/src/ThunderbirdProfileMigrator.jsm | 869 + comm/mail/components/migration/src/components.conf | 38 + comm/mail/components/migration/src/moz.build | 32 + .../migration/src/nsMailProfileMigratorUtils.cpp | 86 + .../migration/src/nsMailProfileMigratorUtils.h | 54 + .../src/nsNetscapeProfileMigratorBase.cpp | 371 + .../migration/src/nsNetscapeProfileMigratorBase.h | 121 + .../migration/src/nsOutlookProfileMigrator.cpp | 135 + .../migration/src/nsOutlookProfileMigrator.h | 30 + .../components/migration/src/nsProfileMigrator.cpp | 121 + .../components/migration/src/nsProfileMigrator.h | 36 + .../migration/src/nsProfileMigratorBase.cpp | 173 + .../migration/src/nsProfileMigratorBase.h | 40 + .../migration/src/nsSeamonkeyProfileMigrator.cpp | 1175 + .../migration/src/nsSeamonkeyProfileMigrator.h | 84 + comm/mail/components/moz.build | 53 + .../newmailaccount/content/accountProvisioner.js | 892 + .../content/accountProvisioner.xhtml | 226 + .../newmailaccount/content/provisionerCheckout.js | 157 + .../newmailaccount/content/uriListener.js | 281 + comm/mail/components/newmailaccount/jar.mn | 9 + comm/mail/components/newmailaccount/moz.build | 6 + comm/mail/components/preferences/actionsshared.js | 23 + .../components/preferences/applicationManager.js | 112 + .../preferences/applicationManager.xhtml | 76 + .../components/preferences/attachmentReminder.js | 100 + .../preferences/attachmentReminder.xhtml | 54 + comm/mail/components/preferences/chat.inc.xhtml | 198 + comm/mail/components/preferences/chat.js | 193 + comm/mail/components/preferences/colors.js | 15 + comm/mail/components/preferences/colors.xhtml | 90 + comm/mail/components/preferences/compose.inc.xhtml | 354 + comm/mail/components/preferences/compose.js | 776 + comm/mail/components/preferences/connection.js | 597 + comm/mail/components/preferences/connection.xhtml | 264 + comm/mail/components/preferences/cookies.js | 993 + comm/mail/components/preferences/cookies.xhtml | 117 + comm/mail/components/preferences/dockoptions.js | 11 + comm/mail/components/preferences/dockoptions.xhtml | 59 + comm/mail/components/preferences/downloads.js | 132 + .../components/preferences/extensionControlled.js | 129 + comm/mail/components/preferences/findInPage.js | 641 + comm/mail/components/preferences/fonts.js | 196 + comm/mail/components/preferences/fonts.xhtml | 337 + comm/mail/components/preferences/general.inc.xhtml | 1096 + comm/mail/components/preferences/general.js | 2962 ++ comm/mail/components/preferences/jar.mn | 55 + comm/mail/components/preferences/messagestyle.js | 259 + .../components/preferences/messengerLanguages.js | 632 + .../preferences/messengerLanguages.xhtml | 93 + comm/mail/components/preferences/moz.build | 18 + comm/mail/components/preferences/notifications.js | 25 + .../components/preferences/notifications.xhtml | 71 + comm/mail/components/preferences/offline.js | 31 + comm/mail/components/preferences/offline.xhtml | 77 + .../mail/components/preferences/passwordManager.js | 819 + .../components/preferences/passwordManager.xhtml | 186 + comm/mail/components/preferences/permissions.js | 501 + comm/mail/components/preferences/permissions.xhtml | 128 + comm/mail/components/preferences/preferences.js | 453 + comm/mail/components/preferences/preferences.xhtml | 256 + comm/mail/components/preferences/preferencesTab.js | 162 + comm/mail/components/preferences/privacy.inc.xhtml | 597 + comm/mail/components/preferences/privacy.js | 562 + comm/mail/components/preferences/receipts.js | 38 + comm/mail/components/preferences/receipts.xhtml | 120 + .../components/preferences/searchResults.inc.xhtml | 24 + comm/mail/components/preferences/sync.inc.xhtml | 239 + comm/mail/components/preferences/sync.js | 377 + comm/mail/components/preferences/syncDialog.js | 38 + comm/mail/components/preferences/syncDialog.xhtml | 210 + comm/mail/components/preferences/tagDialog.xhtml | 26 + .../preferences/test/browser/browser.ini | 20 + .../preferences/test/browser/browser_chat.js | 74 + .../preferences/test/browser/browser_cloudfile.js | 796 + .../preferences/test/browser/browser_compose.js | 87 + .../preferences/test/browser/browser_general.js | 380 + .../test/browser/browser_openPreferences.js | 37 + .../preferences/test/browser/browser_privacy.js | 454 + .../preferences/test/browser/browser_sync.js | 419 + .../preferences/test/browser/files/avatar.png | Bin 0 -> 11019 bytes .../preferences/test/browser/files/icon.svg | 7 + .../preferences/test/browser/files/management.html | 10 + .../components/preferences/test/browser/head.js | 314 + comm/mail/components/prompts/PromptCollection.jsm | 100 + comm/mail/components/prompts/components.conf | 12 + comm/mail/components/prompts/moz.build | 11 + comm/mail/components/search/SearchIntegration.jsm | 871 + comm/mail/components/search/components.conf | 16 + .../search/content/SpotlightIntegration.js | 240 + .../search/content/WinSearchIntegration.js | 346 + .../extensions/allaannonser-sv-SE/favicon.ico | Bin 0 -> 668 bytes .../extensions/allaannonser-sv-SE/manifest.json | 25 + .../search/extensions/allegro-pl/favicon.ico | Bin 0 -> 1150 bytes .../search/extensions/allegro-pl/manifest.json | 24 + .../extensions/amazon/_locales/au/messages.json | 17 + .../extensions/amazon/_locales/ca/messages.json | 17 + .../extensions/amazon/_locales/de/messages.json | 17 + .../extensions/amazon/_locales/en-GB/messages.json | 17 + .../amazon/_locales/france/messages.json | 17 + .../extensions/amazon/_locales/in/messages.json | 17 + .../extensions/amazon/_locales/it/messages.json | 17 + .../extensions/amazon/_locales/jp/messages.json | 17 + .../extensions/amazon/_locales/mx/messages.json | 17 + .../extensions/amazon/_locales/nl/messages.json | 17 + .../search/extensions/amazon/favicon.ico | Bin 0 -> 1407 bytes .../search/extensions/amazon/manifest.json | 25 + .../search/extensions/amazondotcn/favicon.ico | Bin 0 -> 1407 bytes .../search/extensions/amazondotcn/manifest.json | 24 + .../amazondotcom/_locales/en/messages.json | 20 + .../search/extensions/amazondotcom/favicon.ico | Bin 0 -> 1407 bytes .../search/extensions/amazondotcom/manifest.json | 26 + .../search/extensions/atlas-sk/favicon.ico | Bin 0 -> 818 bytes .../search/extensions/atlas-sk/manifest.json | 24 + .../search/extensions/azerdict/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/azerdict/manifest.json | 26 + .../search/extensions/azet-sk/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/azet-sk/manifest.json | 24 + .../components/search/extensions/baidu/favicon.ico | Bin 0 -> 5686 bytes .../search/extensions/baidu/manifest.json | 26 + .../search/extensions/bbc-alba/favicon.ico | Bin 0 -> 958 bytes .../search/extensions/bbc-alba/manifest.json | 26 + .../components/search/extensions/bing/favicon.ico | Bin 0 -> 3638 bytes .../search/extensions/bing/manifest.json | 26 + .../search/extensions/bok-NO/favicon.png | Bin 0 -> 530 bytes .../search/extensions/bok-NO/manifest.json | 24 + .../extensions/bolcom/_locales/fy-NL/messages.json | 14 + .../extensions/bolcom/_locales/nl/messages.json | 14 + .../search/extensions/bolcom/favicon.ico | Bin 0 -> 1406 bytes .../search/extensions/bolcom/manifest.json | 24 + .../search/extensions/ceneji/favicon.png | Bin 0 -> 283 bytes .../search/extensions/ceneji/manifest.json | 24 + .../search/extensions/chambers-en-GB/favicon.ico | Bin 0 -> 1425 bytes .../search/extensions/chambers-en-GB/manifest.json | 24 + .../search/extensions/coccoc/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/coccoc/manifest.json | 25 + .../search/extensions/daum-kr/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/daum-kr/manifest.json | 26 + .../components/search/extensions/ddg/favicon.ico | Bin 0 -> 5430 bytes .../components/search/extensions/ddg/manifest.json | 26 + .../components/search/extensions/diec2/favicon.png | Bin 0 -> 4070 bytes .../search/extensions/diec2/manifest.json | 25 + .../components/search/extensions/drae/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/drae/manifest.json | 24 + .../search/extensions/ecosia/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/ecosia/manifest.json | 26 + .../search/extensions/eki-ee/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/eki-ee/manifest.json | 26 + .../search/extensions/eudict/favicon.ico | Bin 0 -> 1785 bytes .../search/extensions/eudict/manifest.json | 24 + .../search/extensions/faclair-beag/favicon.ico | Bin 0 -> 1091 bytes .../search/extensions/faclair-beag/manifest.json | 23 + .../components/search/extensions/flip/favicon.png | Bin 0 -> 342 bytes .../search/extensions/flip/manifest.json | 26 + .../search/extensions/freelang/favicon.ico | Bin 0 -> 2280 bytes .../search/extensions/freelang/manifest.json | 24 + .../search/extensions/google/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/google/manifest.json | 25 + .../search/extensions/gulesider-NO/favicon.ico | Bin 0 -> 1150 bytes .../search/extensions/gulesider-NO/manifest.json | 24 + .../search/extensions/heureka-cz/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/heureka-cz/manifest.json | 26 + .../search/extensions/hotline-ua/favicon.ico | Bin 0 -> 1376 bytes .../search/extensions/hotline-ua/manifest.json | 24 + .../search/extensions/kannadastore/favicon.png | Bin 0 -> 827 bytes .../search/extensions/kannadastore/manifest.json | 25 + .../search/extensions/leo_ende_de/favicon.png | Bin 0 -> 749 bytes .../search/extensions/leo_ende_de/manifest.json | 25 + .../search/extensions/list-am/favicon.gif | Bin 0 -> 303 bytes .../search/extensions/list-am/manifest.json | 24 + comm/mail/components/search/extensions/list.json | 1223 + .../search/extensions/longdo/favicon.ico | Bin 0 -> 252 bytes .../search/extensions/longdo/manifest.json | 26 + .../search/extensions/mailru/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/mailru/manifest.json | 26 + .../search/extensions/mapy-cz/favicon.ico | Bin 0 -> 1812 bytes .../search/extensions/mapy-cz/manifest.json | 24 + .../marktplaats/_locales/fy-NL/messages.json | 17 + .../marktplaats/_locales/nl/messages.json | 17 + .../search/extensions/marktplaats/favicon.ico | Bin 0 -> 3054 bytes .../search/extensions/marktplaats/manifest.json | 25 + .../mercadolibre/_locales/ar/messages.json | 17 + .../mercadolibre/_locales/cl/messages.json | 17 + .../mercadolibre/_locales/mx/messages.json | 17 + .../search/extensions/mercadolibre/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/mercadolibre/manifest.json | 25 + .../search/extensions/mercadolivre/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/mercadolivre/manifest.json | 24 + .../search/extensions/morfix-dic/favicon.ico | Bin 0 -> 2286 bytes .../search/extensions/morfix-dic/manifest.json | 24 + .../search/extensions/najdi-si/favicon.png | Bin 0 -> 683 bytes .../search/extensions/najdi-si/manifest.json | 24 + .../search/extensions/naver-kr/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/naver-kr/manifest.json | 26 + .../search/extensions/neti-ee/favicon.ico | Bin 0 -> 2519 bytes .../search/extensions/neti-ee/manifest.json | 24 + .../search/extensions/odpiralni/favicon.png | Bin 0 -> 2639 bytes .../search/extensions/odpiralni/manifest.json | 23 + .../components/search/extensions/olx/favicon.ico | Bin 0 -> 5430 bytes .../components/search/extensions/olx/manifest.json | 26 + .../search/extensions/oshiete-goo/favicon.ico | Bin 0 -> 8348 bytes .../search/extensions/oshiete-goo/manifest.json | 23 + .../search/extensions/osta-ee/favicon.png | Bin 0 -> 328 bytes .../search/extensions/osta-ee/manifest.json | 24 + .../search/extensions/ozonru/favicon.ico | Bin 0 -> 3638 bytes .../search/extensions/ozonru/manifest.json | 27 + .../search/extensions/palasprint/favicon.ico | Bin 0 -> 1406 bytes .../search/extensions/palasprint/manifest.json | 24 + .../search/extensions/pazaruvaj/favicon.ico | Bin 0 -> 2584 bytes .../search/extensions/pazaruvaj/manifest.json | 24 + .../search/extensions/pogodak/favicon.ico | Bin 0 -> 1150 bytes .../search/extensions/pogodak/manifest.json | 24 + .../search/extensions/priberam/favicon.png | Bin 0 -> 790 bytes .../search/extensions/priberam/manifest.json | 25 + .../search/extensions/priceru/favicon.ico | Bin 0 -> 468 bytes .../search/extensions/priceru/manifest.json | 24 + .../search/extensions/prisjakt-sv-SE/favicon.ico | Bin 0 -> 1406 bytes .../search/extensions/prisjakt-sv-SE/manifest.json | 25 + .../search/extensions/pwn-pl/favicon.png | Bin 0 -> 1055 bytes .../search/extensions/pwn-pl/manifest.json | 23 + .../components/search/extensions/qwant/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/qwant/manifest.json | 25 + .../search/extensions/qxl-NO/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/qxl-NO/manifest.json | 24 + .../search/extensions/rakuten/favicon.ico | Bin 0 -> 2053 bytes .../search/extensions/rakuten/manifest.json | 25 + .../search/extensions/readmoo/favicon.ico | Bin 0 -> 2468 bytes .../search/extensions/readmoo/manifest.json | 24 + .../search/extensions/salidzinilv/favicon.ico | Bin 0 -> 3638 bytes .../search/extensions/salidzinilv/manifest.json | 26 + .../search/extensions/seznam-cz/favicon.ico | Bin 0 -> 1743 bytes .../search/extensions/seznam-cz/manifest.json | 26 + .../components/search/extensions/sslv/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/sslv/manifest.json | 24 + .../search/extensions/tearma/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/tearma/manifest.json | 24 + .../search/extensions/tyda-sv-SE/favicon.ico | Bin 0 -> 379 bytes .../search/extensions/tyda-sv-SE/manifest.json | 24 + .../search/extensions/vatera/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/vatera/manifest.json | 25 + .../extensions/wikipedia/_locales/NN/messages.json | 20 + .../extensions/wikipedia/_locales/NO/messages.json | 20 + .../extensions/wikipedia/_locales/af/messages.json | 20 + .../extensions/wikipedia/_locales/an/messages.json | 20 + .../extensions/wikipedia/_locales/ar/messages.json | 20 + .../extensions/wikipedia/_locales/as/messages.json | 20 + .../wikipedia/_locales/ast/messages.json | 20 + .../extensions/wikipedia/_locales/az/messages.json | 20 + .../wikipedia/_locales/be-tarask/messages.json | 20 + .../extensions/wikipedia/_locales/be/messages.json | 20 + .../extensions/wikipedia/_locales/bg/messages.json | 20 + .../extensions/wikipedia/_locales/bn/messages.json | 20 + .../extensions/wikipedia/_locales/br/messages.json | 20 + .../extensions/wikipedia/_locales/bs/messages.json | 20 + .../extensions/wikipedia/_locales/ca/messages.json | 20 + .../wikipedia/_locales/crh/messages.json | 20 + .../extensions/wikipedia/_locales/cy/messages.json | 20 + .../extensions/wikipedia/_locales/cz/messages.json | 20 + .../extensions/wikipedia/_locales/da/messages.json | 20 + .../extensions/wikipedia/_locales/de/messages.json | 20 + .../wikipedia/_locales/dsb/messages.json | 20 + .../extensions/wikipedia/_locales/el/messages.json | 20 + .../extensions/wikipedia/_locales/en/messages.json | 20 + .../extensions/wikipedia/_locales/eo/messages.json | 20 + .../extensions/wikipedia/_locales/es/messages.json | 20 + .../extensions/wikipedia/_locales/et/messages.json | 20 + .../extensions/wikipedia/_locales/eu/messages.json | 20 + .../extensions/wikipedia/_locales/fa/messages.json | 20 + .../extensions/wikipedia/_locales/fi/messages.json | 20 + .../extensions/wikipedia/_locales/fr/messages.json | 20 + .../wikipedia/_locales/fy-NL/messages.json | 20 + .../wikipedia/_locales/ga-IE/messages.json | 20 + .../extensions/wikipedia/_locales/gd/messages.json | 20 + .../extensions/wikipedia/_locales/gl/messages.json | 20 + .../extensions/wikipedia/_locales/gn/messages.json | 20 + .../extensions/wikipedia/_locales/gu/messages.json | 20 + .../extensions/wikipedia/_locales/he/messages.json | 20 + .../extensions/wikipedia/_locales/hi/messages.json | 20 + .../extensions/wikipedia/_locales/hr/messages.json | 20 + .../wikipedia/_locales/hsb/messages.json | 20 + .../extensions/wikipedia/_locales/hu/messages.json | 20 + .../extensions/wikipedia/_locales/hy/messages.json | 20 + .../extensions/wikipedia/_locales/ia/messages.json | 20 + .../extensions/wikipedia/_locales/id/messages.json | 20 + .../extensions/wikipedia/_locales/is/messages.json | 20 + .../extensions/wikipedia/_locales/it/messages.json | 20 + .../extensions/wikipedia/_locales/ja/messages.json | 20 + .../extensions/wikipedia/_locales/ka/messages.json | 20 + .../wikipedia/_locales/kab/messages.json | 20 + .../extensions/wikipedia/_locales/kk/messages.json | 20 + .../extensions/wikipedia/_locales/km/messages.json | 20 + .../extensions/wikipedia/_locales/kn/messages.json | 20 + .../extensions/wikipedia/_locales/kr/messages.json | 20 + .../wikipedia/_locales/lij/messages.json | 20 + .../extensions/wikipedia/_locales/lo/messages.json | 20 + .../extensions/wikipedia/_locales/lt/messages.json | 20 + .../wikipedia/_locales/ltg/messages.json | 20 + .../extensions/wikipedia/_locales/lv/messages.json | 20 + .../extensions/wikipedia/_locales/mk/messages.json | 20 + .../extensions/wikipedia/_locales/ml/messages.json | 20 + .../extensions/wikipedia/_locales/mr/messages.json | 20 + .../extensions/wikipedia/_locales/ms/messages.json | 20 + .../extensions/wikipedia/_locales/my/messages.json | 20 + .../extensions/wikipedia/_locales/ne/messages.json | 20 + .../extensions/wikipedia/_locales/nl/messages.json | 20 + .../extensions/wikipedia/_locales/oc/messages.json | 20 + .../extensions/wikipedia/_locales/or/messages.json | 20 + .../extensions/wikipedia/_locales/pa/messages.json | 20 + .../extensions/wikipedia/_locales/pl/messages.json | 20 + .../extensions/wikipedia/_locales/pt/messages.json | 20 + .../extensions/wikipedia/_locales/rm/messages.json | 20 + .../extensions/wikipedia/_locales/ro/messages.json | 20 + .../extensions/wikipedia/_locales/ru/messages.json | 20 + .../extensions/wikipedia/_locales/si/messages.json | 20 + .../extensions/wikipedia/_locales/sk/messages.json | 20 + .../extensions/wikipedia/_locales/sl/messages.json | 20 + .../extensions/wikipedia/_locales/sq/messages.json | 20 + .../extensions/wikipedia/_locales/sr/messages.json | 20 + .../wikipedia/_locales/sv-SE/messages.json | 20 + .../extensions/wikipedia/_locales/ta/messages.json | 20 + .../extensions/wikipedia/_locales/te/messages.json | 20 + .../extensions/wikipedia/_locales/th/messages.json | 20 + .../extensions/wikipedia/_locales/tl/messages.json | 20 + .../extensions/wikipedia/_locales/tr/messages.json | 20 + .../extensions/wikipedia/_locales/uk/messages.json | 20 + .../extensions/wikipedia/_locales/ur/messages.json | 20 + .../extensions/wikipedia/_locales/uz/messages.json | 20 + .../extensions/wikipedia/_locales/vi/messages.json | 20 + .../extensions/wikipedia/_locales/wo/messages.json | 20 + .../wikipedia/_locales/zh-CN/messages.json | 20 + .../wikipedia/_locales/zh-TW/messages.json | 20 + .../search/extensions/wikipedia/favicon.ico | Bin 0 -> 884 bytes .../search/extensions/wikipedia/manifest.json | 26 + .../wiktionary/_locales/oc/messages.json | 20 + .../wiktionary/_locales/te/messages.json | 20 + .../search/extensions/wiktionary/favicon.ico | Bin 0 -> 318 bytes .../search/extensions/wiktionary/manifest.json | 26 + .../search/extensions/wolnelektury-pl/favicon.png | Bin 0 -> 304 bytes .../extensions/wolnelektury-pl/manifest.json | 24 + .../extensions/yahoo-jp-auctions/favicon.ico | Bin 0 -> 2672 bytes .../extensions/yahoo-jp-auctions/manifest.json | 25 + .../search/extensions/yahoo-jp/favicon.ico | Bin 0 -> 5430 bytes .../search/extensions/yahoo-jp/manifest.json | 24 + .../extensions/yandex/_locales/az/messages.json | 23 + .../extensions/yandex/_locales/by/messages.json | 23 + .../extensions/yandex/_locales/en/messages.json | 23 + .../extensions/yandex/_locales/kk/messages.json | 23 + .../extensions/yandex/_locales/ru/messages.json | 23 + .../extensions/yandex/_locales/tr/messages.json | 23 + .../search/extensions/yandex/manifest.json | 26 + .../search/extensions/yandex/yandex-en.ico | Bin 0 -> 1691 bytes .../search/extensions/yandex/yandex-ru.ico | Bin 0 -> 2034 bytes .../search/extensions/zoznam-sk/favicon.png | Bin 0 -> 222 bytes .../search/extensions/zoznam-sk/manifest.json | 25 + comm/mail/components/search/jar.mn | 15 + .../mdimporter/English.lproj/InfoPlist.strings | Bin 0 -> 456 bytes .../search/mdimporter/English.lproj/schema.strings | Bin 0 -> 1276 bytes .../search/mdimporter/GetMetadataForFile.c | 76 + comm/mail/components/search/mdimporter/Info.plist | 53 + comm/mail/components/search/mdimporter/Makefile.in | 26 + comm/mail/components/search/mdimporter/main.c | 208 + comm/mail/components/search/mdimporter/moz.build | 22 + comm/mail/components/search/mdimporter/schema.xml | 32 + comm/mail/components/search/moz.build | 23 + .../components/search/nsMailWinSearchHelper.cpp | 254 + .../mail/components/search/nsMailWinSearchHelper.h | 34 + comm/mail/components/search/public/moz.build | 10 + .../search/public/nsIMailWinSearchHelper.idl | 58 + comm/mail/components/search/wsenable/Makefile.in | 6 + comm/mail/components/search/wsenable/WSEnable.cpp | 141 + .../search/wsenable/WSEnable.exe.manifest | 37 + comm/mail/components/search/wsenable/WSEnable.rc | 6 + comm/mail/components/search/wsenable/module.ver | 1 + comm/mail/components/search/wsenable/moz.build | 21 + comm/mail/components/shell/components.conf | 37 + comm/mail/components/shell/moz.build | 43 + comm/mail/components/shell/nsGNOMEShellService.cpp | 341 + comm/mail/components/shell/nsGNOMEShellService.h | 49 + comm/mail/components/shell/nsIShellService.idl | 52 + comm/mail/components/shell/nsMacShellService.cpp | 156 + comm/mail/components/shell/nsMacShellService.h | 36 + comm/mail/components/shell/nsToolkitShellService.h | 23 + .../components/shell/nsWindowsShellService.cpp | 329 + comm/mail/components/shell/nsWindowsShellService.h | 51 + .../shell/test/unit/test_shellService.js | 22 + comm/mail/components/shell/test/unit/xpcshell.ini | 2 + comm/mail/components/storybook/.storybook/main.js | 47 + .../storybook/.storybook/preview-head.html | 5 + .../components/storybook/.storybook/preview.mjs | 43 + comm/mail/components/storybook/README.md | 39 + comm/mail/components/storybook/mach_commands.py | 42 + comm/mail/components/storybook/package-lock.json | 37747 +++++++++++++++++++ comm/mail/components/storybook/package.json | 28 + .../storybook/stories/colors.stories.mjs | 89 + .../storybook/stories/pane-splitter.stories.mjs | 60 + .../storybook/stories/search-bar.stories.mjs | 37 + comm/mail/components/telemetry/Events.yaml | 25 + comm/mail/components/telemetry/Histograms.json | 40 + comm/mail/components/telemetry/README.md | 175 + comm/mail/components/telemetry/Scalars.yaml | 591 + .../components/test/unit/head_mailcomponents.js | 20 + .../components/test/unit/test_about_support.js | 219 + .../test/unit/test_telemetry_buildconfig.js | 151 + comm/mail/components/test/unit/xpcshell.ini | 6 + .../content/customizable-element.mjs | 299 + .../content/customization-palette.mjs | 243 + .../content/customization-target.mjs | 333 + .../content/extension-action-button.mjs | 148 + .../content/items/add-to-calendar-button.mjs | 38 + .../unifiedtoolbar/content/items/addons-button.mjs | 19 + .../content/items/compact-folder-button.mjs | 40 + .../unifiedtoolbar/content/items/delete-button.mjs | 44 + .../content/items/folder-location-button.mjs | 81 + .../content/items/global-search-bar.mjs | 223 + .../content/items/mail-go-button.mjs | 183 + .../content/items/quick-filter-bar-toggle.mjs | 32 + .../content/items/reply-list-button.mjs | 15 + .../unifiedtoolbar/content/items/space-button.mjs | 41 + .../content/items/view-picker-button.mjs | 40 + .../unifiedtoolbar/content/list-box-selection.mjs | 549 + .../unifiedtoolbar/content/mail-tab-button.mjs | 153 + .../unifiedtoolbar/content/search-bar.mjs | 121 + .../content/unified-toolbar-button.mjs | 240 + .../content/unified-toolbar-customization-pane.mjs | 264 + .../content/unified-toolbar-customization.mjs | 414 + .../unifiedtoolbar/content/unified-toolbar-tab.mjs | 119 + .../unifiedtoolbar/content/unified-toolbar.mjs | 540 + .../unifiedToolbarCustomizableItems.inc.xhtml | 366 + .../content/unifiedToolbarPopups.inc.xhtml | 133 + .../content/unifiedToolbarTemplates.inc.xhtml | 137 + .../content/unifiedToolbarWebextensions.css | 53 + comm/mail/components/unifiedtoolbar/jar.mn | 29 + .../unifiedtoolbar/modules/ButtonStyle.mjs | 23 + .../modules/CustomizableItems.sys.mjs | 134 + .../modules/CustomizableItemsDetails.mjs | 445 + .../unifiedtoolbar/modules/CustomizationState.mjs | 55 + .../modules/ToolbarMigration.sys.mjs | 419 + comm/mail/components/unifiedtoolbar/moz.build | 22 + .../unifiedtoolbar/test/browser/browser.ini | 16 + .../test/browser/browser_customizableItems.js | 173 + .../test/browser/browser_searchBar.js | 263 + .../test/browser/browser_toolbarMigration.js | 99 + .../test/browser/browser_unifiedToolbarTab.js | 285 + .../test/browser/files/searchBar.xhtml | 21 + .../test/browser/files/unifiedToolbarTab.xhtml | 22 + .../unifiedtoolbar/test/unit/test_buttonStyle.js | 40 + .../test/unit/test_customizableItems.js | 123 + .../test/unit/test_customizableItemsDetails.js | 103 + .../test/unit/test_customizationState.js | 64 + .../test/unit/test_toolbarMigration.js | 431 + .../unifiedtoolbar/test/unit/xpcshell.ini | 8 + 1156 files changed, 265965 insertions(+) create mode 100644 comm/mail/components/AboutRedirector.jsm create mode 100644 comm/mail/components/AppIdleManager.jsm create mode 100644 comm/mail/components/MailComponents.manifest create mode 100644 comm/mail/components/MailGlue.jsm create mode 100644 comm/mail/components/MessengerContentHandler.jsm create mode 100644 comm/mail/components/StartupRecorder.jsm create mode 100644 comm/mail/components/about-support/AboutSupportMac.jsm create mode 100644 comm/mail/components/about-support/AboutSupportUnix.jsm create mode 100644 comm/mail/components/about-support/AboutSupportWin32.jsm create mode 100644 comm/mail/components/about-support/content/aboutSupport.js create mode 100644 comm/mail/components/about-support/content/aboutSupport.xhtml create mode 100644 comm/mail/components/about-support/content/accounts.js create mode 100644 comm/mail/components/about-support/content/calendars.js create mode 100644 comm/mail/components/about-support/content/chat.js create mode 100644 comm/mail/components/about-support/content/export.js create mode 100644 comm/mail/components/about-support/content/libs.js create mode 100644 comm/mail/components/about-support/jar.mn create mode 100644 comm/mail/components/about-support/moz.build create mode 100644 comm/mail/components/accountcreation/AccountConfig.jsm create mode 100644 comm/mail/components/accountcreation/AccountCreationUtils.jsm create mode 100644 comm/mail/components/accountcreation/ConfigVerifier.jsm create mode 100644 comm/mail/components/accountcreation/CreateInBackend.jsm create mode 100644 comm/mail/components/accountcreation/ExchangeAutoDiscover.jsm create mode 100644 comm/mail/components/accountcreation/FetchConfig.jsm create mode 100644 comm/mail/components/accountcreation/FetchHTTP.jsm create mode 100644 comm/mail/components/accountcreation/GuessConfig.jsm create mode 100644 comm/mail/components/accountcreation/Sanitizer.jsm create mode 100644 comm/mail/components/accountcreation/content/accountHub.js create mode 100644 comm/mail/components/accountcreation/content/accountSetup.js create mode 100644 comm/mail/components/accountcreation/content/accountSetup.xhtml create mode 100644 comm/mail/components/accountcreation/jar.mn create mode 100644 comm/mail/components/accountcreation/moz.build create mode 100644 comm/mail/components/accountcreation/readFromXML.jsm create mode 100644 comm/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml create mode 100644 comm/mail/components/accountcreation/test/xpcshell/data/example.com.xml create mode 100644 comm/mail/components/accountcreation/test/xpcshell/test_autoconfigFetchDisk.js create mode 100644 comm/mail/components/accountcreation/test/xpcshell/test_autoconfigUtils.js create mode 100644 comm/mail/components/accountcreation/test/xpcshell/test_autoconfigXML.js create mode 100644 comm/mail/components/accountcreation/test/xpcshell/xpcshell.ini create mode 100644 comm/mail/components/accountcreation/views/container.mjs create mode 100644 comm/mail/components/accountcreation/views/email.mjs create mode 100644 comm/mail/components/accountcreation/views/start.mjs create mode 100644 comm/mail/components/activity/Activity.jsm create mode 100644 comm/mail/components/activity/ActivityManager.jsm create mode 100644 comm/mail/components/activity/ActivityManagerUI.jsm create mode 100644 comm/mail/components/activity/components.conf create mode 100644 comm/mail/components/activity/content/activity-widgets.js create mode 100644 comm/mail/components/activity/content/activity.js create mode 100644 comm/mail/components/activity/content/activity.xhtml create mode 100644 comm/mail/components/activity/jar.mn create mode 100644 comm/mail/components/activity/modules/activityModules.jsm create mode 100644 comm/mail/components/activity/modules/alertHook.jsm create mode 100644 comm/mail/components/activity/modules/autosync.jsm create mode 100644 comm/mail/components/activity/modules/glodaIndexer.jsm create mode 100644 comm/mail/components/activity/modules/moveCopy.jsm create mode 100644 comm/mail/components/activity/modules/pop3Download.jsm create mode 100644 comm/mail/components/activity/modules/sendLater.jsm create mode 100644 comm/mail/components/activity/moz.build create mode 100644 comm/mail/components/activity/nsIActivity.idl create mode 100644 comm/mail/components/activity/nsIActivityManager.idl create mode 100644 comm/mail/components/activity/nsIActivityManagerUI.idl create mode 100644 comm/mail/components/addrbook/content/abCommon.js create mode 100644 comm/mail/components/addrbook/content/abContactsPanel.js create mode 100644 comm/mail/components/addrbook/content/abContactsPanel.xhtml create mode 100644 comm/mail/components/addrbook/content/abEditListDialog.xhtml create mode 100644 comm/mail/components/addrbook/content/abMailListDialog.xhtml create mode 100644 comm/mail/components/addrbook/content/abSearchDialog.js create mode 100644 comm/mail/components/addrbook/content/abSearchDialog.xhtml create mode 100644 comm/mail/components/addrbook/content/abView-new.js create mode 100644 comm/mail/components/addrbook/content/aboutAddressBook.js create mode 100644 comm/mail/components/addrbook/content/aboutAddressBook.xhtml create mode 100644 comm/mail/components/addrbook/content/addressBookTab.js create mode 100644 comm/mail/components/addrbook/content/menulist-addrbooks.js create mode 100644 comm/mail/components/addrbook/content/vcard-edit/adr.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/custom.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/edit.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/email.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/fn.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/id-gen.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/impp.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/n.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/nickname.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/note.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/org.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/special-date.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/tel.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/tz.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/url.mjs create mode 100644 comm/mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml create mode 100644 comm/mail/components/addrbook/jar.mn create mode 100644 comm/mail/components/addrbook/moz.build create mode 100644 comm/mail/components/addrbook/test/browser/browser.ini create mode 100644 comm/mail/components/addrbook/test/browser/browser_cardDAV_init.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_cardDAV_oAuth.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_cardDAV_properties.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_cardDAV_sync.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_contact_sidebar.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_contact_tree.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_directory_tree.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_display_card.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_display_multiple.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_drag_drop.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_edit_async.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_edit_card.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_edit_photo.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_ldap_search.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_mailing_lists.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_open_actions.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_search.js create mode 100644 comm/mail/components/addrbook/test/browser/browser_telemetry.js create mode 100644 comm/mail/components/addrbook/test/browser/data/addressbook.sjs create mode 100644 comm/mail/components/addrbook/test/browser/data/addressbooks.sjs create mode 100644 comm/mail/components/addrbook/test/browser/data/auth_headers.sjs create mode 100644 comm/mail/components/addrbook/test/browser/data/dns.sjs create mode 100644 comm/mail/components/addrbook/test/browser/data/photo1.jpg create mode 100644 comm/mail/components/addrbook/test/browser/data/photo2.jpg create mode 100644 comm/mail/components/addrbook/test/browser/data/principal.sjs create mode 100644 comm/mail/components/addrbook/test/browser/data/redirect_auto.sjs create mode 100644 comm/mail/components/addrbook/test/browser/data/token.sjs create mode 100644 comm/mail/components/addrbook/test/browser/head.js create mode 100644 comm/mail/components/cloudfile/cloudFileAccounts.jsm create mode 100644 comm/mail/components/cloudfile/content/selectDialog.js create mode 100644 comm/mail/components/cloudfile/content/selectDialog.xhtml create mode 100644 comm/mail/components/cloudfile/jar.mn create mode 100644 comm/mail/components/cloudfile/moz.build create mode 100644 comm/mail/components/cloudfile/test/browser/browser.ini create mode 100644 comm/mail/components/cloudfile/test/browser/browser_repeat_upload.js create mode 100644 comm/mail/components/cloudfile/test/browser/files/green_eggs.txt create mode 100644 comm/mail/components/cloudfile/test/browser/files/icon.svg create mode 100644 comm/mail/components/cloudfile/test/browser/files/management.html create mode 100644 comm/mail/components/cloudfile/test/browser/head.js create mode 100644 comm/mail/components/components.conf create mode 100644 comm/mail/components/compose/composer.js create mode 100644 comm/mail/components/compose/content/ComposerCommands.js create mode 100644 comm/mail/components/compose/content/MsgComposeCommands.js create mode 100644 comm/mail/components/compose/content/addressingWidgetOverlay.js create mode 100644 comm/mail/components/compose/content/bigFileObserver.js create mode 100644 comm/mail/components/compose/content/cloudAttachmentLinkManager.js create mode 100644 comm/mail/components/compose/content/dialogs/EdAEAttributes.js create mode 100644 comm/mail/components/compose/content/dialogs/EdAECSSAttributes.js create mode 100644 comm/mail/components/compose/content/dialogs/EdAEHTMLAttributes.js create mode 100644 comm/mail/components/compose/content/dialogs/EdAEJSEAttributes.js create mode 100644 comm/mail/components/compose/content/dialogs/EdAdvancedEdit.js create mode 100644 comm/mail/components/compose/content/dialogs/EdAdvancedEdit.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdColorPicker.js create mode 100644 comm/mail/components/compose/content/dialogs/EdColorPicker.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdColorProps.js create mode 100644 comm/mail/components/compose/content/dialogs/EdColorProps.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdConvertToTable.js create mode 100644 comm/mail/components/compose/content/dialogs/EdConvertToTable.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdDialogCommon.js create mode 100644 comm/mail/components/compose/content/dialogs/EdDictionary.js create mode 100644 comm/mail/components/compose/content/dialogs/EdDictionary.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdHLineProps.js create mode 100644 comm/mail/components/compose/content/dialogs/EdHLineProps.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdImageDialog.js create mode 100644 comm/mail/components/compose/content/dialogs/EdImageLinkLoader.js create mode 100644 comm/mail/components/compose/content/dialogs/EdImageProps.js create mode 100644 comm/mail/components/compose/content/dialogs/EdImageProps.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdInsSrc.js create mode 100644 comm/mail/components/compose/content/dialogs/EdInsSrc.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdInsertChars.js create mode 100644 comm/mail/components/compose/content/dialogs/EdInsertChars.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdInsertMath.js create mode 100644 comm/mail/components/compose/content/dialogs/EdInsertMath.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdInsertTOC.js create mode 100644 comm/mail/components/compose/content/dialogs/EdInsertTOC.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdInsertTable.js create mode 100644 comm/mail/components/compose/content/dialogs/EdInsertTable.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdLinkProps.js create mode 100644 comm/mail/components/compose/content/dialogs/EdLinkProps.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdListProps.js create mode 100644 comm/mail/components/compose/content/dialogs/EdListProps.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.js create mode 100644 comm/mail/components/compose/content/dialogs/EdNamedAnchorProps.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdReplace.js create mode 100644 comm/mail/components/compose/content/dialogs/EdReplace.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdSpellCheck.js create mode 100644 comm/mail/components/compose/content/dialogs/EdSpellCheck.xhtml create mode 100644 comm/mail/components/compose/content/dialogs/EdTableProps.js create mode 100644 comm/mail/components/compose/content/dialogs/EdTableProps.xhtml create mode 100644 comm/mail/components/compose/content/editFormatButtons.inc.xhtml create mode 100644 comm/mail/components/compose/content/editor.js create mode 100644 comm/mail/components/compose/content/editorUtilities.js create mode 100644 comm/mail/components/compose/content/images/tag-anchor.gif create mode 100644 comm/mail/components/compose/content/messengercompose.xhtml create mode 100644 comm/mail/components/compose/jar.mn create mode 100644 comm/mail/components/compose/moz.build create mode 100644 comm/mail/components/compose/texzilla/TeXZilla.js create mode 100644 comm/mail/components/customizableui/CustomizableUI.sys.mjs create mode 100644 comm/mail/components/customizableui/PanelMultiView.sys.mjs create mode 100644 comm/mail/components/customizableui/content/customizeMode.inc.xhtml create mode 100644 comm/mail/components/customizableui/content/jar.mn create mode 100644 comm/mail/components/customizableui/content/moz.build create mode 100644 comm/mail/components/customizableui/content/panelUI.inc.xhtml create mode 100644 comm/mail/components/customizableui/content/panelUI.js create mode 100644 comm/mail/components/customizableui/moz.build create mode 100644 comm/mail/components/devtools/components.conf create mode 100644 comm/mail/components/devtools/devtools-loader.jsm create mode 100644 comm/mail/components/devtools/moz.build create mode 100644 comm/mail/components/devtools/tb-root-actor.js create mode 100644 comm/mail/components/downloads/content/aboutDownloads.js create mode 100644 comm/mail/components/downloads/content/aboutDownloads.xhtml create mode 100644 comm/mail/components/downloads/jar.mn create mode 100644 comm/mail/components/downloads/moz.build create mode 100644 comm/mail/components/enterprisepolicies/Policies.sys.mjs create mode 100644 comm/mail/components/enterprisepolicies/content/aboutPolicies.js create mode 100644 comm/mail/components/enterprisepolicies/content/aboutPolicies.xhtml create mode 100644 comm/mail/components/enterprisepolicies/content/policies-active.svg create mode 100644 comm/mail/components/enterprisepolicies/content/policies-documentation.svg create mode 100644 comm/mail/components/enterprisepolicies/content/policies-error.svg create mode 100644 comm/mail/components/enterprisepolicies/helpers/ProxyPolicies.sys.mjs create mode 100644 comm/mail/components/enterprisepolicies/helpers/moz.build create mode 100644 comm/mail/components/enterprisepolicies/jar.mn create mode 100644 comm/mail/components/enterprisepolicies/moz.build create mode 100644 comm/mail/components/enterprisepolicies/schemas/configuration.json create mode 100644 comm/mail/components/enterprisepolicies/schemas/moz.build create mode 100644 comm/mail/components/enterprisepolicies/schemas/policies-schema.json create mode 100644 comm/mail/components/enterprisepolicies/schemas/schema.sys.mjs create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser.ini create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policies_setAndLockPref_API.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_app_auto_update.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_app_update.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_background_app_update.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_block_about.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_cookie_settings.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_disable_masterpassword.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_disable_safemode.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_disable_telemetry.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_downloads.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_extensions.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_extensionsettings2.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_handlers.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_masterpassword.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/browser_policy_passwordmanager.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/disable_app_update/browser.ini create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/disable_app_update/browser_policy_disable_app_update.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/disable_app_update/config_disable_app_update.json create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/disable_developer_tools/browser.ini create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/disable_developer_tools/browser_policy_disable_developer_tools.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/disable_developer_tools/config_disable_developer_tools.json create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/extensionsettings.html create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/hardware_acceleration/browser.ini create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/hardware_acceleration/browser_policy_hardware_acceleration.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/hardware_acceleration/disable_hardware_acceleration.json create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/head.js create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/policytest_v0.1.xpi create mode 100644 comm/mail/components/enterprisepolicies/tests/browser/policytest_v0.2.xpi create mode 100644 comm/mail/components/enterprisepolicies/tests/moz.build create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/head.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_3rdparty.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_appupdatepin.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_appupdateurl.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_bug1658259.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_clear_blocked_cookies.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_macosparser_unflatten.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_policy_search_engine.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_preferences.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_proxy.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_requestedlocales.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_runOnce_helper.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_simple_pref_policies.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/test_sorted_alphabetically.js create mode 100644 comm/mail/components/enterprisepolicies/tests/xpcshell/xpcshell.ini create mode 100644 comm/mail/components/extensions/ExtensionBrowsingData.sys.mjs create mode 100644 comm/mail/components/extensions/ExtensionPopups.sys.mjs create mode 100644 comm/mail/components/extensions/ExtensionToolbarButtons.jsm create mode 100644 comm/mail/components/extensions/MailExtensionShortcuts.jsm create mode 100644 comm/mail/components/extensions/child/.eslintrc.js create mode 100644 comm/mail/components/extensions/child/ext-extensionScripts.js create mode 100644 comm/mail/components/extensions/child/ext-mail.js create mode 100644 comm/mail/components/extensions/child/ext-menus.js create mode 100644 comm/mail/components/extensions/child/ext-tabs.js create mode 100644 comm/mail/components/extensions/ext-mail.json create mode 100644 comm/mail/components/extensions/extension.svg create mode 100644 comm/mail/components/extensions/extensionPopup.js create mode 100644 comm/mail/components/extensions/extensionPopup.xhtml create mode 100644 comm/mail/components/extensions/extensions-mail.manifest create mode 100644 comm/mail/components/extensions/jar.mn create mode 100644 comm/mail/components/extensions/moz.build create mode 100644 comm/mail/components/extensions/parent/.eslintrc.js create mode 100644 comm/mail/components/extensions/parent/ext-accounts.js create mode 100644 comm/mail/components/extensions/parent/ext-addressBook.js create mode 100644 comm/mail/components/extensions/parent/ext-browserAction.js create mode 100644 comm/mail/components/extensions/parent/ext-chrome-settings-overrides.js create mode 100644 comm/mail/components/extensions/parent/ext-cloudFile.js create mode 100644 comm/mail/components/extensions/parent/ext-commands.js create mode 100644 comm/mail/components/extensions/parent/ext-compose.js create mode 100644 comm/mail/components/extensions/parent/ext-composeAction.js create mode 100644 comm/mail/components/extensions/parent/ext-extensionScripts.js create mode 100644 comm/mail/components/extensions/parent/ext-folders.js create mode 100644 comm/mail/components/extensions/parent/ext-identities.js create mode 100644 comm/mail/components/extensions/parent/ext-mail.js create mode 100644 comm/mail/components/extensions/parent/ext-mailTabs.js create mode 100644 comm/mail/components/extensions/parent/ext-menus.js create mode 100644 comm/mail/components/extensions/parent/ext-messageDisplay.js create mode 100644 comm/mail/components/extensions/parent/ext-messageDisplayAction.js create mode 100644 comm/mail/components/extensions/parent/ext-messages.js create mode 100644 comm/mail/components/extensions/parent/ext-sessions.js create mode 100644 comm/mail/components/extensions/parent/ext-spaces.js create mode 100644 comm/mail/components/extensions/parent/ext-spacesToolbar.js create mode 100644 comm/mail/components/extensions/parent/ext-tabs.js create mode 100644 comm/mail/components/extensions/parent/ext-theme.js create mode 100644 comm/mail/components/extensions/parent/ext-windows.js create mode 100644 comm/mail/components/extensions/processScript.js create mode 100644 comm/mail/components/extensions/schemas/LICENSE create mode 100644 comm/mail/components/extensions/schemas/accounts.json create mode 100644 comm/mail/components/extensions/schemas/addressBook.json create mode 100644 comm/mail/components/extensions/schemas/browserAction.json create mode 100644 comm/mail/components/extensions/schemas/chrome_settings_overrides.json create mode 100644 comm/mail/components/extensions/schemas/cloudFile.json create mode 100644 comm/mail/components/extensions/schemas/commands.json create mode 100644 comm/mail/components/extensions/schemas/compose.json create mode 100644 comm/mail/components/extensions/schemas/composeAction.json create mode 100644 comm/mail/components/extensions/schemas/extensionScripts.json create mode 100644 comm/mail/components/extensions/schemas/folders.json create mode 100644 comm/mail/components/extensions/schemas/identities.json create mode 100644 comm/mail/components/extensions/schemas/mailTabs.json create mode 100644 comm/mail/components/extensions/schemas/menus.json create mode 100644 comm/mail/components/extensions/schemas/menus_child.json create mode 100644 comm/mail/components/extensions/schemas/messageDisplay.json create mode 100644 comm/mail/components/extensions/schemas/messageDisplayAction.json create mode 100644 comm/mail/components/extensions/schemas/messages.json create mode 100644 comm/mail/components/extensions/schemas/sessions.json create mode 100644 comm/mail/components/extensions/schemas/spaces.json create mode 100644 comm/mail/components/extensions/schemas/spacesToolbar.json create mode 100644 comm/mail/components/extensions/schemas/tabs.json create mode 100644 comm/mail/components/extensions/schemas/theme.json create mode 100644 comm/mail/components/extensions/schemas/windows.json create mode 100644 comm/mail/components/extensions/test/AppUiTestDelegate.sys.mjs create mode 100644 comm/mail/components/extensions/test/browser/.eslintrc.js create mode 100644 comm/mail/components/extensions/test/browser/browser.ini create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_addressBooksUI.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_customized.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_not_customized.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_popup_click_mv3_event_pages.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_browserAction_properties.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_bug1812530.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_clickHandler.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_cloudFile.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_execute_browser_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_execute_compose_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_execute_message_display_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_getAll.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_onChanged.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_onCommand_bug1845236.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_commands_update.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeAction.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeAction_popup_click_mv3_event_pages.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeAction_properties.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_composeScripts.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_attachments.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_attachments.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_body.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_bug1691254.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_forward.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_headers.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_identity.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_new.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_begin_reply.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_bug1692439.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_bug1804796.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_details.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_details_body.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_details_headers.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_dictionaries.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_onBeforeSend.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_saveDraft.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_saveTemplate.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_compose_sendMessage.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_contentScripts.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_content_handler.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_content_tabs_navigation_menu.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_mailTabs.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_mailTabs_mv3.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_compose.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_content.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_folder_pane.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_message_panes.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_tabs.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_context_tools_main_menu.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_message_one_attachment.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_message_two_attachments.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_popup_action.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_menus_replace_menu_context.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_popup_click_mv3_event_pages.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayAction_properties.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplayScripts.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1827032.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_bug1828056.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_file.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_headerMessageId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messageDisplay_open_messageId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_message_external.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_messages_open_attachment.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_quickFilter.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_sessions.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_spaces.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_spacesToolbar.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_content.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_cookieStoreId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_events.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_move.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_onCreated_bug1817872.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_query.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tabs_update_reload.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_themes_onUpdated.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_tooltip_in_extension_pages.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_bug1732559.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_create_normal_cookieStoreId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_create_popup_cookieStoreId.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_events.js create mode 100644 comm/mail/components/extensions/test/browser/browser_ext_windows_types.js create mode 100644 comm/mail/components/extensions/test/browser/data/cloudFile1.txt create mode 100644 comm/mail/components/extensions/test/browser/data/cloudFile2.txt create mode 100644 comm/mail/components/extensions/test/browser/data/content.html create mode 100644 comm/mail/components/extensions/test/browser/data/content_body.html create mode 100644 comm/mail/components/extensions/test/browser/data/linktest.html create mode 100644 comm/mail/components/extensions/test/browser/data/tb-logo.png create mode 100644 comm/mail/components/extensions/test/browser/head.js create mode 100644 comm/mail/components/extensions/test/browser/head_menus.js create mode 100644 comm/mail/components/extensions/test/browser/messages/attachedMessageSample.eml create mode 100644 comm/mail/components/extensions/test/browser/messages/messageWithLink.eml create mode 100644 comm/mail/components/extensions/test/browser/test_browserAction.js create mode 100644 comm/mail/components/extensions/test/xpcshell/.eslintrc.js create mode 100644 comm/mail/components/extensions/test/xpcshell/data/utils.js create mode 100644 comm/mail/components/extensions/test/xpcshell/head-imap.js create mode 100644 comm/mail/components/extensions/test/xpcshell/head-nntp.js create mode 100644 comm/mail/components/extensions/test/xpcshell/head.js create mode 100644 comm/mail/components/extensions/test/xpcshell/images/redPixel.png create mode 100644 comm/mail/components/extensions/test/xpcshell/images/whitePixel.png create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/alternative.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/attachedMessageWithMissingHeaders.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/nestedMessages.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/sample01.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/sample02.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/sample03.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/sample04.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/sample05.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/sample06.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/messages/sample07.eml create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_accounts.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_accounts_mv3_event_pages.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_addressBook.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_provider.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_quickSearch.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_readonly.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_addressBook_remote.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_alias.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_browserAction_unifiedtoolbar_restart.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_experiments.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_folders.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_folders_mv3_event_pages.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_identities_mv3_event_pages.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_messages.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_messages_attachments.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_messages_get.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_messages_id.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_messages_import.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_messages_move_copy_delete.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_messages_onNewMailReceived.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_messages_query.js create mode 100644 comm/mail/components/extensions/test/xpcshell/test_ext_messages_update.js create mode 100644 comm/mail/components/extensions/test/xpcshell/xpcshell-imap.ini create mode 100644 comm/mail/components/extensions/test/xpcshell/xpcshell-local.ini create mode 100644 comm/mail/components/extensions/test/xpcshell/xpcshell-nntp.ini create mode 100644 comm/mail/components/extensions/test/xpcshell/xpcshell.ini create mode 100644 comm/mail/components/im/IMIncomingServer.sys.mjs create mode 100644 comm/mail/components/im/IMProtocolInfo.sys.mjs create mode 100644 comm/mail/components/im/all-im.js create mode 100644 comm/mail/components/im/components.conf create mode 100644 comm/mail/components/im/content/.eslintrc.js create mode 100644 comm/mail/components/im/content/addbuddy.js create mode 100644 comm/mail/components/im/content/addbuddy.xhtml create mode 100644 comm/mail/components/im/content/am-im.js create mode 100644 comm/mail/components/im/content/am-im.xhtml create mode 100644 comm/mail/components/im/content/chat-contact.js create mode 100644 comm/mail/components/im/content/chat-conversation-info.js create mode 100644 comm/mail/components/im/content/chat-conversation.js create mode 100644 comm/mail/components/im/content/chat-group.js create mode 100644 comm/mail/components/im/content/chat-imconv.js create mode 100644 comm/mail/components/im/content/chat-menu.inc.xhtml create mode 100644 comm/mail/components/im/content/chat-messenger.inc.xhtml create mode 100644 comm/mail/components/im/content/chat-messenger.js create mode 100644 comm/mail/components/im/content/imAccountWizard.js create mode 100644 comm/mail/components/im/content/imAccountWizard.xhtml create mode 100644 comm/mail/components/im/content/imAccounts.js create mode 100644 comm/mail/components/im/content/imAccounts.xhtml create mode 100644 comm/mail/components/im/content/imContextMenu.js create mode 100644 comm/mail/components/im/content/imStatusSelector.js create mode 100644 comm/mail/components/im/content/joinchat.js create mode 100644 comm/mail/components/im/content/joinchat.xhtml create mode 100644 comm/mail/components/im/content/toolbarbutton-badge-button.js create mode 100644 comm/mail/components/im/content/verify.js create mode 100644 comm/mail/components/im/content/verify.xhtml create mode 100644 comm/mail/components/im/jar.mn create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_0.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_0_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_10.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_100.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_100_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_10_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_110.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_110_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_120.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_120_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_130.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_130_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_140.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_140_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_150.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_150_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_160.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_160_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_170.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_170_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_180.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_180_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_190.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_190_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_20.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_200.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_200_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_20_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_210.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_210_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_220.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_220_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_230.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_230_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_240.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_240_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_250.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_250_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_260.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_260_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_270.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_270_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_280.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_280_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_290.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_290_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_30.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_300.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_300_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_30_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_310.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_310_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_320.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_320_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_330.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_330_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_340.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_340_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_350.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_350_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_40.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_40_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_50.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_50_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_60.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_60_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_70.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_70_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_80.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_80_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_90.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_90_alt.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/indicator_grey.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/minus-hover.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/minus.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/plus-hover.png create mode 100644 comm/mail/components/im/messages/bubbles/Bitmaps/plus.png create mode 100644 comm/mail/components/im/messages/bubbles/Footer.html create mode 100644 comm/mail/components/im/messages/bubbles/Incoming/Content.html create mode 100644 comm/mail/components/im/messages/bubbles/Incoming/Context.html create mode 100644 comm/mail/components/im/messages/bubbles/Incoming/NextContent.html create mode 100644 comm/mail/components/im/messages/bubbles/Info.plist create mode 100644 comm/mail/components/im/messages/bubbles/NextStatus.html create mode 100644 comm/mail/components/im/messages/bubbles/Status.html create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Blue_-_Green.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Blue_-_Green_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Blue_-_Pink.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Blue_-_Pink_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Blue_-_Red.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Blue_-_Red_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Green_-_Blue.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Green_-_Blue_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Green_-_Purple.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Green_-_Purple_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Green_-_Red.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Green_-_Red_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Grey_-_Blue.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Grey_-_Blue_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Grey_-_Pink.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Grey_-_Pink_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Grey_-_Purple.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Grey_-_Purple_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Grey_-_Red.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Grey_-_Red_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Pink_-_Blue.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Pink_-_Blue_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Pink_-_Purple.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Pink_-_Purple_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Purple_-_Green.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Purple_-_Green_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Purple_-_Pink.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Purple_-_Pink_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Red_-_Blue.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Red_-_Blue_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Red_-_Green.css create mode 100644 comm/mail/components/im/messages/bubbles/Variants/Red_-_Green_Alternating.css create mode 100644 comm/mail/components/im/messages/bubbles/inline.js create mode 100644 comm/mail/components/im/messages/bubbles/main.css create mode 100644 comm/mail/components/im/messages/dark/Incoming/Content.html create mode 100644 comm/mail/components/im/messages/dark/Incoming/Context.html create mode 100644 comm/mail/components/im/messages/dark/Incoming/NextContent.html create mode 100644 comm/mail/components/im/messages/dark/Incoming/NextContext.html create mode 100644 comm/mail/components/im/messages/dark/Info.plist create mode 100644 comm/mail/components/im/messages/dark/Status.html create mode 100644 comm/mail/components/im/messages/dark/Variants/Blue.css create mode 100644 comm/mail/components/im/messages/dark/Variants/Green.css create mode 100644 comm/mail/components/im/messages/dark/Variants/Purple.css create mode 100644 comm/mail/components/im/messages/dark/Variants/Red.css create mode 100644 comm/mail/components/im/messages/dark/Variants/Yellow.css create mode 100644 comm/mail/components/im/messages/dark/inline.js create mode 100644 comm/mail/components/im/messages/dark/main.css create mode 100644 comm/mail/components/im/messages/mail/Footer.html create mode 100644 comm/mail/components/im/messages/mail/Header.html create mode 100644 comm/mail/components/im/messages/mail/Incoming/Content.html create mode 100644 comm/mail/components/im/messages/mail/Incoming/Context.html create mode 100644 comm/mail/components/im/messages/mail/Incoming/NextContent.html create mode 100644 comm/mail/components/im/messages/mail/Incoming/NextContext.html create mode 100644 comm/mail/components/im/messages/mail/Incoming/buddy_icon.svg create mode 100644 comm/mail/components/im/messages/mail/Info.plist create mode 100644 comm/mail/components/im/messages/mail/NextStatus.html create mode 100644 comm/mail/components/im/messages/mail/Outgoing/Content.html create mode 100644 comm/mail/components/im/messages/mail/Outgoing/Context.html create mode 100644 comm/mail/components/im/messages/mail/Outgoing/NextContent.html create mode 100644 comm/mail/components/im/messages/mail/Outgoing/NextContext.html create mode 100644 comm/mail/components/im/messages/mail/Status.html create mode 100644 comm/mail/components/im/messages/mail/Variants/Dark.css create mode 100644 comm/mail/components/im/messages/mail/Variants/Light.css create mode 100644 comm/mail/components/im/messages/mail/inline.js create mode 100644 comm/mail/components/im/messages/mail/main.css create mode 100644 comm/mail/components/im/messages/papersheets/Bitmaps/information.png create mode 100644 comm/mail/components/im/messages/papersheets/Bitmaps/minus.png create mode 100644 comm/mail/components/im/messages/papersheets/Bitmaps/plus.png create mode 100644 comm/mail/components/im/messages/papersheets/Incoming/Content.html create mode 100644 comm/mail/components/im/messages/papersheets/Incoming/Context.html create mode 100644 comm/mail/components/im/messages/papersheets/Incoming/NextContent.html create mode 100644 comm/mail/components/im/messages/papersheets/Info.plist create mode 100644 comm/mail/components/im/messages/papersheets/NextStatus.html create mode 100644 comm/mail/components/im/messages/papersheets/Status.html create mode 100644 comm/mail/components/im/messages/papersheets/Variants/White.css create mode 100644 comm/mail/components/im/messages/papersheets/inline.js create mode 100644 comm/mail/components/im/messages/papersheets/main.css create mode 100644 comm/mail/components/im/messages/simple/Incoming/Content.html create mode 100644 comm/mail/components/im/messages/simple/Incoming/Context.html create mode 100644 comm/mail/components/im/messages/simple/Incoming/NextContext.html create mode 100644 comm/mail/components/im/messages/simple/Info.plist create mode 100644 comm/mail/components/im/messages/simple/Status.html create mode 100644 comm/mail/components/im/messages/simple/Variants/Dark.css create mode 100644 comm/mail/components/im/messages/simple/Variants/Normal.css create mode 100644 comm/mail/components/im/messages/simple/main.css create mode 100644 comm/mail/components/im/modules/ChatEncryption.sys.mjs create mode 100644 comm/mail/components/im/modules/GlodaIMSearcher.sys.mjs create mode 100644 comm/mail/components/im/modules/chatHandler.sys.mjs create mode 100644 comm/mail/components/im/modules/chatIcons.sys.mjs create mode 100644 comm/mail/components/im/modules/chatNotifications.sys.mjs create mode 100644 comm/mail/components/im/modules/index_im.sys.mjs create mode 100644 comm/mail/components/im/moz.build create mode 100644 comm/mail/components/im/smileys/theme.json create mode 100644 comm/mail/components/im/test/TestProtocol.sys.mjs create mode 100644 comm/mail/components/im/test/browser/browser.ini create mode 100644 comm/mail/components/im/test/browser/browser_browserRequest.js create mode 100644 comm/mail/components/im/test/browser/browser_chatNotifications.js create mode 100644 comm/mail/components/im/test/browser/browser_chatTelemetry.js create mode 100644 comm/mail/components/im/test/browser/browser_contextMenu.js create mode 100644 comm/mail/components/im/test/browser/browser_logs.js create mode 100644 comm/mail/components/im/test/browser/browser_messagesMail.js create mode 100644 comm/mail/components/im/test/browser/browser_readMessage.js create mode 100644 comm/mail/components/im/test/browser/browser_removeMessage.js create mode 100644 comm/mail/components/im/test/browser/browser_requestNotifications.js create mode 100644 comm/mail/components/im/test/browser/browser_spacesToolbarChat.js create mode 100644 comm/mail/components/im/test/browser/browser_tooltips.js create mode 100644 comm/mail/components/im/test/browser/browser_updateMessage.js create mode 100644 comm/mail/components/im/test/browser/head.js create mode 100644 comm/mail/components/im/test/components.conf create mode 100644 comm/mail/components/migration/content/migration.js create mode 100644 comm/mail/components/migration/content/migration.xhtml create mode 100644 comm/mail/components/migration/jar.mn create mode 100644 comm/mail/components/migration/moz.build create mode 100644 comm/mail/components/migration/public/moz.build create mode 100644 comm/mail/components/migration/public/nsIMailProfileMigrator.idl create mode 100644 comm/mail/components/migration/src/ThunderbirdProfileMigrator.jsm create mode 100644 comm/mail/components/migration/src/components.conf create mode 100644 comm/mail/components/migration/src/moz.build create mode 100644 comm/mail/components/migration/src/nsMailProfileMigratorUtils.cpp create mode 100644 comm/mail/components/migration/src/nsMailProfileMigratorUtils.h create mode 100644 comm/mail/components/migration/src/nsNetscapeProfileMigratorBase.cpp create mode 100644 comm/mail/components/migration/src/nsNetscapeProfileMigratorBase.h create mode 100644 comm/mail/components/migration/src/nsOutlookProfileMigrator.cpp create mode 100644 comm/mail/components/migration/src/nsOutlookProfileMigrator.h create mode 100644 comm/mail/components/migration/src/nsProfileMigrator.cpp create mode 100644 comm/mail/components/migration/src/nsProfileMigrator.h create mode 100644 comm/mail/components/migration/src/nsProfileMigratorBase.cpp create mode 100644 comm/mail/components/migration/src/nsProfileMigratorBase.h create mode 100644 comm/mail/components/migration/src/nsSeamonkeyProfileMigrator.cpp create mode 100644 comm/mail/components/migration/src/nsSeamonkeyProfileMigrator.h create mode 100644 comm/mail/components/moz.build create mode 100644 comm/mail/components/newmailaccount/content/accountProvisioner.js create mode 100644 comm/mail/components/newmailaccount/content/accountProvisioner.xhtml create mode 100644 comm/mail/components/newmailaccount/content/provisionerCheckout.js create mode 100644 comm/mail/components/newmailaccount/content/uriListener.js create mode 100644 comm/mail/components/newmailaccount/jar.mn create mode 100644 comm/mail/components/newmailaccount/moz.build create mode 100644 comm/mail/components/preferences/actionsshared.js create mode 100644 comm/mail/components/preferences/applicationManager.js create mode 100644 comm/mail/components/preferences/applicationManager.xhtml create mode 100644 comm/mail/components/preferences/attachmentReminder.js create mode 100644 comm/mail/components/preferences/attachmentReminder.xhtml create mode 100644 comm/mail/components/preferences/chat.inc.xhtml create mode 100644 comm/mail/components/preferences/chat.js create mode 100644 comm/mail/components/preferences/colors.js create mode 100644 comm/mail/components/preferences/colors.xhtml create mode 100644 comm/mail/components/preferences/compose.inc.xhtml create mode 100644 comm/mail/components/preferences/compose.js create mode 100644 comm/mail/components/preferences/connection.js create mode 100644 comm/mail/components/preferences/connection.xhtml create mode 100644 comm/mail/components/preferences/cookies.js create mode 100644 comm/mail/components/preferences/cookies.xhtml create mode 100644 comm/mail/components/preferences/dockoptions.js create mode 100644 comm/mail/components/preferences/dockoptions.xhtml create mode 100644 comm/mail/components/preferences/downloads.js create mode 100644 comm/mail/components/preferences/extensionControlled.js create mode 100644 comm/mail/components/preferences/findInPage.js create mode 100644 comm/mail/components/preferences/fonts.js create mode 100644 comm/mail/components/preferences/fonts.xhtml create mode 100644 comm/mail/components/preferences/general.inc.xhtml create mode 100644 comm/mail/components/preferences/general.js create mode 100644 comm/mail/components/preferences/jar.mn create mode 100644 comm/mail/components/preferences/messagestyle.js create mode 100644 comm/mail/components/preferences/messengerLanguages.js create mode 100644 comm/mail/components/preferences/messengerLanguages.xhtml create mode 100644 comm/mail/components/preferences/moz.build create mode 100644 comm/mail/components/preferences/notifications.js create mode 100644 comm/mail/components/preferences/notifications.xhtml create mode 100644 comm/mail/components/preferences/offline.js create mode 100644 comm/mail/components/preferences/offline.xhtml create mode 100644 comm/mail/components/preferences/passwordManager.js create mode 100644 comm/mail/components/preferences/passwordManager.xhtml create mode 100644 comm/mail/components/preferences/permissions.js create mode 100644 comm/mail/components/preferences/permissions.xhtml create mode 100644 comm/mail/components/preferences/preferences.js create mode 100644 comm/mail/components/preferences/preferences.xhtml create mode 100644 comm/mail/components/preferences/preferencesTab.js create mode 100644 comm/mail/components/preferences/privacy.inc.xhtml create mode 100644 comm/mail/components/preferences/privacy.js create mode 100644 comm/mail/components/preferences/receipts.js create mode 100644 comm/mail/components/preferences/receipts.xhtml create mode 100644 comm/mail/components/preferences/searchResults.inc.xhtml create mode 100644 comm/mail/components/preferences/sync.inc.xhtml create mode 100644 comm/mail/components/preferences/sync.js create mode 100644 comm/mail/components/preferences/syncDialog.js create mode 100644 comm/mail/components/preferences/syncDialog.xhtml create mode 100644 comm/mail/components/preferences/tagDialog.xhtml create mode 100644 comm/mail/components/preferences/test/browser/browser.ini create mode 100644 comm/mail/components/preferences/test/browser/browser_chat.js create mode 100644 comm/mail/components/preferences/test/browser/browser_cloudfile.js create mode 100644 comm/mail/components/preferences/test/browser/browser_compose.js create mode 100644 comm/mail/components/preferences/test/browser/browser_general.js create mode 100644 comm/mail/components/preferences/test/browser/browser_openPreferences.js create mode 100644 comm/mail/components/preferences/test/browser/browser_privacy.js create mode 100644 comm/mail/components/preferences/test/browser/browser_sync.js create mode 100644 comm/mail/components/preferences/test/browser/files/avatar.png create mode 100644 comm/mail/components/preferences/test/browser/files/icon.svg create mode 100644 comm/mail/components/preferences/test/browser/files/management.html create mode 100644 comm/mail/components/preferences/test/browser/head.js create mode 100644 comm/mail/components/prompts/PromptCollection.jsm create mode 100644 comm/mail/components/prompts/components.conf create mode 100644 comm/mail/components/prompts/moz.build create mode 100644 comm/mail/components/search/SearchIntegration.jsm create mode 100644 comm/mail/components/search/components.conf create mode 100644 comm/mail/components/search/content/SpotlightIntegration.js create mode 100644 comm/mail/components/search/content/WinSearchIntegration.js create mode 100644 comm/mail/components/search/extensions/allaannonser-sv-SE/favicon.ico create mode 100644 comm/mail/components/search/extensions/allaannonser-sv-SE/manifest.json create mode 100644 comm/mail/components/search/extensions/allegro-pl/favicon.ico create mode 100644 comm/mail/components/search/extensions/allegro-pl/manifest.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/au/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/ca/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/de/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/en-GB/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/france/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/in/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/it/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/jp/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/mx/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/_locales/nl/messages.json create mode 100644 comm/mail/components/search/extensions/amazon/favicon.ico create mode 100644 comm/mail/components/search/extensions/amazon/manifest.json create mode 100644 comm/mail/components/search/extensions/amazondotcn/favicon.ico create mode 100644 comm/mail/components/search/extensions/amazondotcn/manifest.json create mode 100644 comm/mail/components/search/extensions/amazondotcom/_locales/en/messages.json create mode 100644 comm/mail/components/search/extensions/amazondotcom/favicon.ico create mode 100644 comm/mail/components/search/extensions/amazondotcom/manifest.json create mode 100644 comm/mail/components/search/extensions/atlas-sk/favicon.ico create mode 100644 comm/mail/components/search/extensions/atlas-sk/manifest.json create mode 100644 comm/mail/components/search/extensions/azerdict/favicon.ico create mode 100644 comm/mail/components/search/extensions/azerdict/manifest.json create mode 100644 comm/mail/components/search/extensions/azet-sk/favicon.ico create mode 100644 comm/mail/components/search/extensions/azet-sk/manifest.json create mode 100644 comm/mail/components/search/extensions/baidu/favicon.ico create mode 100644 comm/mail/components/search/extensions/baidu/manifest.json create mode 100644 comm/mail/components/search/extensions/bbc-alba/favicon.ico create mode 100644 comm/mail/components/search/extensions/bbc-alba/manifest.json create mode 100644 comm/mail/components/search/extensions/bing/favicon.ico create mode 100644 comm/mail/components/search/extensions/bing/manifest.json create mode 100644 comm/mail/components/search/extensions/bok-NO/favicon.png create mode 100644 comm/mail/components/search/extensions/bok-NO/manifest.json create mode 100644 comm/mail/components/search/extensions/bolcom/_locales/fy-NL/messages.json create mode 100644 comm/mail/components/search/extensions/bolcom/_locales/nl/messages.json create mode 100644 comm/mail/components/search/extensions/bolcom/favicon.ico create mode 100644 comm/mail/components/search/extensions/bolcom/manifest.json create mode 100644 comm/mail/components/search/extensions/ceneji/favicon.png create mode 100644 comm/mail/components/search/extensions/ceneji/manifest.json create mode 100644 comm/mail/components/search/extensions/chambers-en-GB/favicon.ico create mode 100644 comm/mail/components/search/extensions/chambers-en-GB/manifest.json create mode 100644 comm/mail/components/search/extensions/coccoc/favicon.ico create mode 100644 comm/mail/components/search/extensions/coccoc/manifest.json create mode 100644 comm/mail/components/search/extensions/daum-kr/favicon.ico create mode 100644 comm/mail/components/search/extensions/daum-kr/manifest.json create mode 100644 comm/mail/components/search/extensions/ddg/favicon.ico create mode 100644 comm/mail/components/search/extensions/ddg/manifest.json create mode 100644 comm/mail/components/search/extensions/diec2/favicon.png create mode 100644 comm/mail/components/search/extensions/diec2/manifest.json create mode 100644 comm/mail/components/search/extensions/drae/favicon.ico create mode 100644 comm/mail/components/search/extensions/drae/manifest.json create mode 100644 comm/mail/components/search/extensions/ecosia/favicon.ico create mode 100644 comm/mail/components/search/extensions/ecosia/manifest.json create mode 100644 comm/mail/components/search/extensions/eki-ee/favicon.ico create mode 100644 comm/mail/components/search/extensions/eki-ee/manifest.json create mode 100644 comm/mail/components/search/extensions/eudict/favicon.ico create mode 100644 comm/mail/components/search/extensions/eudict/manifest.json create mode 100644 comm/mail/components/search/extensions/faclair-beag/favicon.ico create mode 100644 comm/mail/components/search/extensions/faclair-beag/manifest.json create mode 100644 comm/mail/components/search/extensions/flip/favicon.png create mode 100644 comm/mail/components/search/extensions/flip/manifest.json create mode 100644 comm/mail/components/search/extensions/freelang/favicon.ico create mode 100644 comm/mail/components/search/extensions/freelang/manifest.json create mode 100644 comm/mail/components/search/extensions/google/favicon.ico create mode 100644 comm/mail/components/search/extensions/google/manifest.json create mode 100644 comm/mail/components/search/extensions/gulesider-NO/favicon.ico create mode 100644 comm/mail/components/search/extensions/gulesider-NO/manifest.json create mode 100644 comm/mail/components/search/extensions/heureka-cz/favicon.ico create mode 100644 comm/mail/components/search/extensions/heureka-cz/manifest.json create mode 100644 comm/mail/components/search/extensions/hotline-ua/favicon.ico create mode 100644 comm/mail/components/search/extensions/hotline-ua/manifest.json create mode 100644 comm/mail/components/search/extensions/kannadastore/favicon.png create mode 100644 comm/mail/components/search/extensions/kannadastore/manifest.json create mode 100644 comm/mail/components/search/extensions/leo_ende_de/favicon.png create mode 100644 comm/mail/components/search/extensions/leo_ende_de/manifest.json create mode 100644 comm/mail/components/search/extensions/list-am/favicon.gif create mode 100644 comm/mail/components/search/extensions/list-am/manifest.json create mode 100644 comm/mail/components/search/extensions/list.json create mode 100644 comm/mail/components/search/extensions/longdo/favicon.ico create mode 100644 comm/mail/components/search/extensions/longdo/manifest.json create mode 100644 comm/mail/components/search/extensions/mailru/favicon.ico create mode 100644 comm/mail/components/search/extensions/mailru/manifest.json create mode 100644 comm/mail/components/search/extensions/mapy-cz/favicon.ico create mode 100644 comm/mail/components/search/extensions/mapy-cz/manifest.json create mode 100644 comm/mail/components/search/extensions/marktplaats/_locales/fy-NL/messages.json create mode 100644 comm/mail/components/search/extensions/marktplaats/_locales/nl/messages.json create mode 100644 comm/mail/components/search/extensions/marktplaats/favicon.ico create mode 100644 comm/mail/components/search/extensions/marktplaats/manifest.json create mode 100644 comm/mail/components/search/extensions/mercadolibre/_locales/ar/messages.json create mode 100644 comm/mail/components/search/extensions/mercadolibre/_locales/cl/messages.json create mode 100644 comm/mail/components/search/extensions/mercadolibre/_locales/mx/messages.json create mode 100644 comm/mail/components/search/extensions/mercadolibre/favicon.ico create mode 100644 comm/mail/components/search/extensions/mercadolibre/manifest.json create mode 100644 comm/mail/components/search/extensions/mercadolivre/favicon.ico create mode 100644 comm/mail/components/search/extensions/mercadolivre/manifest.json create mode 100644 comm/mail/components/search/extensions/morfix-dic/favicon.ico create mode 100644 comm/mail/components/search/extensions/morfix-dic/manifest.json create mode 100644 comm/mail/components/search/extensions/najdi-si/favicon.png create mode 100644 comm/mail/components/search/extensions/najdi-si/manifest.json create mode 100644 comm/mail/components/search/extensions/naver-kr/favicon.ico create mode 100644 comm/mail/components/search/extensions/naver-kr/manifest.json create mode 100644 comm/mail/components/search/extensions/neti-ee/favicon.ico create mode 100644 comm/mail/components/search/extensions/neti-ee/manifest.json create mode 100644 comm/mail/components/search/extensions/odpiralni/favicon.png create mode 100644 comm/mail/components/search/extensions/odpiralni/manifest.json create mode 100644 comm/mail/components/search/extensions/olx/favicon.ico create mode 100644 comm/mail/components/search/extensions/olx/manifest.json create mode 100644 comm/mail/components/search/extensions/oshiete-goo/favicon.ico create mode 100644 comm/mail/components/search/extensions/oshiete-goo/manifest.json create mode 100644 comm/mail/components/search/extensions/osta-ee/favicon.png create mode 100644 comm/mail/components/search/extensions/osta-ee/manifest.json create mode 100644 comm/mail/components/search/extensions/ozonru/favicon.ico create mode 100644 comm/mail/components/search/extensions/ozonru/manifest.json create mode 100644 comm/mail/components/search/extensions/palasprint/favicon.ico create mode 100644 comm/mail/components/search/extensions/palasprint/manifest.json create mode 100644 comm/mail/components/search/extensions/pazaruvaj/favicon.ico create mode 100644 comm/mail/components/search/extensions/pazaruvaj/manifest.json create mode 100644 comm/mail/components/search/extensions/pogodak/favicon.ico create mode 100644 comm/mail/components/search/extensions/pogodak/manifest.json create mode 100644 comm/mail/components/search/extensions/priberam/favicon.png create mode 100644 comm/mail/components/search/extensions/priberam/manifest.json create mode 100644 comm/mail/components/search/extensions/priceru/favicon.ico create mode 100644 comm/mail/components/search/extensions/priceru/manifest.json create mode 100644 comm/mail/components/search/extensions/prisjakt-sv-SE/favicon.ico create mode 100644 comm/mail/components/search/extensions/prisjakt-sv-SE/manifest.json create mode 100644 comm/mail/components/search/extensions/pwn-pl/favicon.png create mode 100644 comm/mail/components/search/extensions/pwn-pl/manifest.json create mode 100644 comm/mail/components/search/extensions/qwant/favicon.ico create mode 100644 comm/mail/components/search/extensions/qwant/manifest.json create mode 100644 comm/mail/components/search/extensions/qxl-NO/favicon.ico create mode 100644 comm/mail/components/search/extensions/qxl-NO/manifest.json create mode 100644 comm/mail/components/search/extensions/rakuten/favicon.ico create mode 100644 comm/mail/components/search/extensions/rakuten/manifest.json create mode 100644 comm/mail/components/search/extensions/readmoo/favicon.ico create mode 100644 comm/mail/components/search/extensions/readmoo/manifest.json create mode 100644 comm/mail/components/search/extensions/salidzinilv/favicon.ico create mode 100644 comm/mail/components/search/extensions/salidzinilv/manifest.json create mode 100644 comm/mail/components/search/extensions/seznam-cz/favicon.ico create mode 100644 comm/mail/components/search/extensions/seznam-cz/manifest.json create mode 100644 comm/mail/components/search/extensions/sslv/favicon.ico create mode 100644 comm/mail/components/search/extensions/sslv/manifest.json create mode 100644 comm/mail/components/search/extensions/tearma/favicon.ico create mode 100644 comm/mail/components/search/extensions/tearma/manifest.json create mode 100644 comm/mail/components/search/extensions/tyda-sv-SE/favicon.ico create mode 100644 comm/mail/components/search/extensions/tyda-sv-SE/manifest.json create mode 100644 comm/mail/components/search/extensions/vatera/favicon.ico create mode 100644 comm/mail/components/search/extensions/vatera/manifest.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/NN/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/NO/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/af/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/an/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ar/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/as/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ast/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/az/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/be-tarask/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/be/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/bg/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/bn/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/br/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/bs/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ca/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/crh/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/cy/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/cz/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/da/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/de/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/dsb/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/el/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/en/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/eo/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/es/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/et/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/eu/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/fa/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/fi/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/fr/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/fy-NL/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ga-IE/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/gd/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/gl/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/gn/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/gu/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/he/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/hi/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/hr/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/hsb/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/hu/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/hy/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ia/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/id/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/is/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/it/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ja/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ka/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/kab/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/kk/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/km/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/kn/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/kr/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/lij/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/lo/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/lt/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ltg/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/lv/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/mk/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ml/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/mr/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ms/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/my/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ne/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/nl/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/oc/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/or/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/pa/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/pl/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/pt/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/rm/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ro/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ru/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/si/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/sk/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/sl/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/sq/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/sr/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/sv-SE/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ta/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/te/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/th/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/tl/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/tr/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/uk/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/ur/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/uz/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/vi/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/wo/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/zh-CN/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/_locales/zh-TW/messages.json create mode 100644 comm/mail/components/search/extensions/wikipedia/favicon.ico create mode 100644 comm/mail/components/search/extensions/wikipedia/manifest.json create mode 100644 comm/mail/components/search/extensions/wiktionary/_locales/oc/messages.json create mode 100644 comm/mail/components/search/extensions/wiktionary/_locales/te/messages.json create mode 100644 comm/mail/components/search/extensions/wiktionary/favicon.ico create mode 100644 comm/mail/components/search/extensions/wiktionary/manifest.json create mode 100644 comm/mail/components/search/extensions/wolnelektury-pl/favicon.png create mode 100644 comm/mail/components/search/extensions/wolnelektury-pl/manifest.json create mode 100644 comm/mail/components/search/extensions/yahoo-jp-auctions/favicon.ico create mode 100644 comm/mail/components/search/extensions/yahoo-jp-auctions/manifest.json create mode 100644 comm/mail/components/search/extensions/yahoo-jp/favicon.ico create mode 100644 comm/mail/components/search/extensions/yahoo-jp/manifest.json create mode 100644 comm/mail/components/search/extensions/yandex/_locales/az/messages.json create mode 100644 comm/mail/components/search/extensions/yandex/_locales/by/messages.json create mode 100644 comm/mail/components/search/extensions/yandex/_locales/en/messages.json create mode 100644 comm/mail/components/search/extensions/yandex/_locales/kk/messages.json create mode 100644 comm/mail/components/search/extensions/yandex/_locales/ru/messages.json create mode 100644 comm/mail/components/search/extensions/yandex/_locales/tr/messages.json create mode 100644 comm/mail/components/search/extensions/yandex/manifest.json create mode 100644 comm/mail/components/search/extensions/yandex/yandex-en.ico create mode 100644 comm/mail/components/search/extensions/yandex/yandex-ru.ico create mode 100644 comm/mail/components/search/extensions/zoznam-sk/favicon.png create mode 100644 comm/mail/components/search/extensions/zoznam-sk/manifest.json create mode 100644 comm/mail/components/search/jar.mn create mode 100644 comm/mail/components/search/mdimporter/English.lproj/InfoPlist.strings create mode 100644 comm/mail/components/search/mdimporter/English.lproj/schema.strings create mode 100644 comm/mail/components/search/mdimporter/GetMetadataForFile.c create mode 100644 comm/mail/components/search/mdimporter/Info.plist create mode 100644 comm/mail/components/search/mdimporter/Makefile.in create mode 100644 comm/mail/components/search/mdimporter/main.c create mode 100644 comm/mail/components/search/mdimporter/moz.build create mode 100644 comm/mail/components/search/mdimporter/schema.xml create mode 100644 comm/mail/components/search/moz.build create mode 100644 comm/mail/components/search/nsMailWinSearchHelper.cpp create mode 100644 comm/mail/components/search/nsMailWinSearchHelper.h create mode 100644 comm/mail/components/search/public/moz.build create mode 100644 comm/mail/components/search/public/nsIMailWinSearchHelper.idl create mode 100644 comm/mail/components/search/wsenable/Makefile.in create mode 100644 comm/mail/components/search/wsenable/WSEnable.cpp create mode 100644 comm/mail/components/search/wsenable/WSEnable.exe.manifest create mode 100644 comm/mail/components/search/wsenable/WSEnable.rc create mode 100644 comm/mail/components/search/wsenable/module.ver create mode 100644 comm/mail/components/search/wsenable/moz.build create mode 100644 comm/mail/components/shell/components.conf create mode 100644 comm/mail/components/shell/moz.build create mode 100644 comm/mail/components/shell/nsGNOMEShellService.cpp create mode 100644 comm/mail/components/shell/nsGNOMEShellService.h create mode 100644 comm/mail/components/shell/nsIShellService.idl create mode 100644 comm/mail/components/shell/nsMacShellService.cpp create mode 100644 comm/mail/components/shell/nsMacShellService.h create mode 100644 comm/mail/components/shell/nsToolkitShellService.h create mode 100644 comm/mail/components/shell/nsWindowsShellService.cpp create mode 100644 comm/mail/components/shell/nsWindowsShellService.h create mode 100644 comm/mail/components/shell/test/unit/test_shellService.js create mode 100644 comm/mail/components/shell/test/unit/xpcshell.ini create mode 100644 comm/mail/components/storybook/.storybook/main.js create mode 100644 comm/mail/components/storybook/.storybook/preview-head.html create mode 100644 comm/mail/components/storybook/.storybook/preview.mjs create mode 100644 comm/mail/components/storybook/README.md create mode 100644 comm/mail/components/storybook/mach_commands.py create mode 100644 comm/mail/components/storybook/package-lock.json create mode 100644 comm/mail/components/storybook/package.json create mode 100644 comm/mail/components/storybook/stories/colors.stories.mjs create mode 100644 comm/mail/components/storybook/stories/pane-splitter.stories.mjs create mode 100644 comm/mail/components/storybook/stories/search-bar.stories.mjs create mode 100644 comm/mail/components/telemetry/Events.yaml create mode 100644 comm/mail/components/telemetry/Histograms.json create mode 100644 comm/mail/components/telemetry/README.md create mode 100644 comm/mail/components/telemetry/Scalars.yaml create mode 100644 comm/mail/components/test/unit/head_mailcomponents.js create mode 100644 comm/mail/components/test/unit/test_about_support.js create mode 100644 comm/mail/components/test/unit/test_telemetry_buildconfig.js create mode 100644 comm/mail/components/test/unit/xpcshell.ini create mode 100644 comm/mail/components/unifiedtoolbar/content/customizable-element.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/customization-palette.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/customization-target.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/extension-action-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/add-to-calendar-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/addons-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/compact-folder-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/delete-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/folder-location-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/global-search-bar.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/mail-go-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/quick-filter-bar-toggle.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/reply-list-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/space-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/items/view-picker-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/list-box-selection.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/mail-tab-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/search-bar.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/unified-toolbar-button.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/unified-toolbar-customization-pane.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/unified-toolbar-customization.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/unified-toolbar-tab.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/unified-toolbar.mjs create mode 100644 comm/mail/components/unifiedtoolbar/content/unifiedToolbarCustomizableItems.inc.xhtml create mode 100644 comm/mail/components/unifiedtoolbar/content/unifiedToolbarPopups.inc.xhtml create mode 100644 comm/mail/components/unifiedtoolbar/content/unifiedToolbarTemplates.inc.xhtml create mode 100644 comm/mail/components/unifiedtoolbar/content/unifiedToolbarWebextensions.css create mode 100644 comm/mail/components/unifiedtoolbar/jar.mn create mode 100644 comm/mail/components/unifiedtoolbar/modules/ButtonStyle.mjs create mode 100644 comm/mail/components/unifiedtoolbar/modules/CustomizableItems.sys.mjs create mode 100644 comm/mail/components/unifiedtoolbar/modules/CustomizableItemsDetails.mjs create mode 100644 comm/mail/components/unifiedtoolbar/modules/CustomizationState.mjs create mode 100644 comm/mail/components/unifiedtoolbar/modules/ToolbarMigration.sys.mjs create mode 100644 comm/mail/components/unifiedtoolbar/moz.build create mode 100644 comm/mail/components/unifiedtoolbar/test/browser/browser.ini create mode 100644 comm/mail/components/unifiedtoolbar/test/browser/browser_customizableItems.js create mode 100644 comm/mail/components/unifiedtoolbar/test/browser/browser_searchBar.js create mode 100644 comm/mail/components/unifiedtoolbar/test/browser/browser_toolbarMigration.js create mode 100644 comm/mail/components/unifiedtoolbar/test/browser/browser_unifiedToolbarTab.js create mode 100644 comm/mail/components/unifiedtoolbar/test/browser/files/searchBar.xhtml create mode 100644 comm/mail/components/unifiedtoolbar/test/browser/files/unifiedToolbarTab.xhtml create mode 100644 comm/mail/components/unifiedtoolbar/test/unit/test_buttonStyle.js create mode 100644 comm/mail/components/unifiedtoolbar/test/unit/test_customizableItems.js create mode 100644 comm/mail/components/unifiedtoolbar/test/unit/test_customizableItemsDetails.js create mode 100644 comm/mail/components/unifiedtoolbar/test/unit/test_customizationState.js create mode 100644 comm/mail/components/unifiedtoolbar/test/unit/test_toolbarMigration.js create mode 100644 comm/mail/components/unifiedtoolbar/test/unit/xpcshell.ini (limited to 'comm/mail/components') diff --git a/comm/mail/components/AboutRedirector.jsm b/comm/mail/components/AboutRedirector.jsm new file mode 100644 index 0000000000..a652ba5a6d --- /dev/null +++ b/comm/mail/components/AboutRedirector.jsm @@ -0,0 +1,124 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["AboutRedirector"]; + +function AboutRedirector() {} +AboutRedirector.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), + + // Each entry in the map has the key as the part after the "about:" and the + // value as a record with url and flags entries. Note that each addition here + // should be coupled with a corresponding addition in mailComponents.manifest. + _redirMap: { + newserror: { + url: "chrome://messenger/content/newsError.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + rights: { + url: "chrome://messenger/content/aboutRights.xhtml", + flags: + Ci.nsIAboutModule.ALLOW_SCRIPT | + Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT, + }, + support: { + url: "chrome://messenger/content/about-support/aboutSupport.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + preferences: { + url: "chrome://messenger/content/preferences/preferences.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + downloads: { + url: "chrome://messenger/content/downloads/aboutDownloads.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + policies: { + url: "chrome://messenger/content/policies/aboutPolicies.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + accountsettings: { + url: "chrome://messenger/content/AccountManager.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + accountsetup: { + url: "chrome://messenger/content/accountcreation/accountSetup.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + accountprovisioner: { + url: "chrome://messenger/content/newmailaccount/accountProvisioner.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + addressbook: { + url: "chrome://messenger/content/addressbook/aboutAddressBook.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + "3pane": { + url: "chrome://messenger/content/about3Pane.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + message: { + url: "chrome://messenger/content/aboutMessage.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + import: { + url: "chrome://messenger/content/aboutImport.xhtml", + flags: Ci.nsIAboutModule.ALLOW_SCRIPT, + }, + profiling: { + url: "chrome://devtools/content/performance-new/aboutprofiling/index.xhtml", + flags: + Ci.nsIAboutModule.ALLOW_SCRIPT | Ci.nsIAboutModule.IS_SECURE_CHROME_UI, + }, + }, + + /** + * Gets the module name from the given URI. + */ + _getModuleName(aURI) { + // Strip out the first ? or #, and anything following it + let name = /[^?#]+/.exec(aURI.pathQueryRef)[0]; + return name.toLowerCase(); + }, + + getURIFlags(aURI) { + let name = this._getModuleName(aURI); + if (!(name in this._redirMap)) { + throw Components.Exception(`no about:${name}`, Cr.NS_ERROR_ILLEGAL_VALUE); + } + return this._redirMap[name].flags; + }, + + newChannel(aURI, aLoadInfo) { + let name = this._getModuleName(aURI); + if (!(name in this._redirMap)) { + throw Components.Exception(`no about:${name}`, Cr.NS_ERROR_ILLEGAL_VALUE); + } + + let newURI = Services.io.newURI(this._redirMap[name].url); + let channel = Services.io.newChannelFromURIWithLoadInfo(newURI, aLoadInfo); + channel.originalURI = aURI; + + if ( + this._redirMap[name].flags & + Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT + ) { + let principal = Services.scriptSecurityManager.createContentPrincipal( + aURI, + {} + ); + channel.owner = principal; + } + + return channel; + }, + + getChromeURI(aURI) { + let name = this._getModuleName(aURI); + if (!(name in this._redirMap)) { + throw Components.Exception(`no about:${name}`, Cr.NS_ERROR_ILLEGAL_VALUE); + } + return Services.io.newURI(this._redirMap[name].url); + }, +}; diff --git a/comm/mail/components/AppIdleManager.jsm b/comm/mail/components/AppIdleManager.jsm new file mode 100644 index 0000000000..cc42c23178 --- /dev/null +++ b/comm/mail/components/AppIdleManager.jsm @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["appIdleManager"]; + +// This module provides a mechanism to turn window focus and blur events +// into app idle notifications. If we get a blur notification that is not +// followed by a focus notification in less than some small number of seconds, +// then we send a begin app idle notification. +// If we get a focus event, and we're app idle, then we send an end app idle +// notification. +// The notification topic is "mail:appIdle", the values are "idle", and "back" + +var appIdleManager = { + _appIdle: false, + _timerInterval: 5000, // 5 seconds ought to be plenty + get _timer() { + delete this._timer; + return (this._timer = Cc["@mozilla.org/timer;1"].createInstance( + Ci.nsITimer + )); + }, + + _timerCallback() { + appIdleManager._appIdle = true; + Services.obs.notifyObservers(null, "mail:appIdle", "idle"); + }, + + onBlur() { + appIdleManager._timer.initWithCallback( + appIdleManager._timerCallback, + appIdleManager._timerInterval, + Ci.nsITimer.TYPE_ONE_SHOT + ); + }, + + onFocus() { + appIdleManager._timer.cancel(); + if (appIdleManager._appIdle) { + appIdleManager._appIdle = false; + Services.obs.notifyObservers(null, "mail:appIdle", "back"); + } + }, +}; diff --git a/comm/mail/components/MailComponents.manifest b/comm/mail/components/MailComponents.manifest new file mode 100644 index 0000000000..f35c67e263 --- /dev/null +++ b/comm/mail/components/MailComponents.manifest @@ -0,0 +1,9 @@ +# MailGlue.jsm + +# This component must restrict its registration for the app-startup category +# to the specific list of apps that use it so it doesn't get loaded in xpcshell. +# Thus we restrict it to these apps: +# +# mail: {3550f703-e582-4d05-9a08-453d09bdfdc6} + +category app-startup MailGlue @mozilla.org/mail/mailglue;1 application={3550f703-e582-4d05-9a08-453d09bdfdc6} diff --git a/comm/mail/components/MailGlue.jsm b/comm/mail/components/MailGlue.jsm new file mode 100644 index 0000000000..f14237a55a --- /dev/null +++ b/comm/mail/components/MailGlue.jsm @@ -0,0 +1,1380 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["MailGlue", "MailTelemetryForTests"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +const lazy = {}; + +// lazy module getter + +XPCOMUtils.defineLazyGetter(lazy, "gMailBundle", function () { + return Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); +}); + +if (AppConstants.NIGHTLY_BUILD) { + XPCOMUtils.defineLazyGetter( + lazy, + "WeaveService", + () => Cc["@mozilla.org/weave/service;1"].getService().wrappedJSObject + ); +} + +ChromeUtils.defineESModuleGetters(lazy, { + ActorManagerParent: "resource://gre/modules/ActorManagerParent.sys.mjs", + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", + ChatCore: "resource:///modules/chatHandler.sys.mjs", + + LightweightThemeConsumer: + "resource://gre/modules/LightweightThemeConsumer.sys.mjs", + + OsEnvironment: "resource://gre/modules/OsEnvironment.sys.mjs", + PdfJs: "resource://pdf.js/PdfJs.sys.mjs", + + RemoteSecuritySettings: + "resource://gre/modules/psm/RemoteSecuritySettings.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + cal: "resource:///modules/calendar/calUtils.jsm", + ExtensionSupport: "resource:///modules/ExtensionSupport.jsm", + MailMigrator: "resource:///modules/MailMigrator.jsm", + MailServices: "resource:///modules/MailServices.jsm", + MailUsageTelemetry: "resource:///modules/MailUsageTelemetry.jsm", + OAuth2Providers: "resource:///modules/OAuth2Providers.jsm", + TBDistCustomizer: "resource:///modules/TBDistCustomizer.jsm", +}); + +if (AppConstants.MOZ_UPDATER) { + ChromeUtils.defineESModuleGetters(lazy, { + UpdateListener: "resource://gre/modules/UpdateListener.sys.mjs", + }); +} + +const listeners = { + observers: {}, + + observe(subject, topic, data) { + for (let module of this.observers[topic]) { + try { + lazy[module].observe(subject, topic, data); + } catch (e) { + console.error(e); + } + } + }, + + init() { + for (let observer of Object.keys(this.observers)) { + Services.obs.addObserver(this, observer); + } + }, +}; +if (AppConstants.MOZ_UPDATER) { + listeners.observers["update-downloading"] = ["UpdateListener"]; + listeners.observers["update-staged"] = ["UpdateListener"]; + listeners.observers["update-downloaded"] = ["UpdateListener"]; + listeners.observers["update-available"] = ["UpdateListener"]; + listeners.observers["update-error"] = ["UpdateListener"]; + listeners.observers["update-swap"] = ["UpdateListener"]; +} + +const PREF_PDFJS_ISDEFAULT_CACHE_STATE = "pdfjs.enabledCache.state"; + +let JSWINDOWACTORS = { + ChatAction: { + matches: ["chrome://chat/content/conv.html"], + parent: { + esModuleURI: "resource:///actors/ChatActionParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/ChatActionChild.sys.mjs", + events: { + contextmenu: { mozSystemGroup: true }, + }, + }, + }, + + ContextMenu: { + parent: { + esModuleURI: "resource:///actors/ContextMenuParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/ContextMenuChild.sys.mjs", + events: { + contextmenu: { mozSystemGroup: true }, + }, + }, + allFrames: true, + }, + + // As in ActorManagerParent.sys.mjs, but with single-site and single-page + // message manager groups added. + FindBar: { + parent: { + esModuleURI: "resource://gre/actors/FindBarParent.sys.mjs", + }, + child: { + esModuleURI: "resource://gre/actors/FindBarChild.sys.mjs", + events: { + keypress: { mozSystemGroup: true }, + }, + }, + + allFrames: true, + messageManagerGroups: [ + "browsers", + "single-site", + "single-page", + "test", + "", + ], + }, + + LinkClickHandler: { + parent: { + moduleURI: "resource:///actors/LinkClickHandlerParent.jsm", + }, + child: { + moduleURI: "resource:///actors/LinkClickHandlerChild.jsm", + events: { + click: {}, + }, + }, + messageManagerGroups: ["single-site", "webext-browsers"], + allFrames: true, + }, + + LinkHandler: { + parent: { + esModuleURI: "resource:///actors/LinkHandlerParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/LinkHandlerChild.sys.mjs", + events: { + DOMHeadElementParsed: {}, + DOMLinkAdded: {}, + DOMLinkChanged: {}, + pageshow: {}, + // The `pagehide` event is only used to clean up state which will not be + // present if the actor hasn't been created. + pagehide: { createActor: false }, + }, + }, + + messageManagerGroups: ["browsers", "single-site", "single-page"], + }, + + // As in ActorManagerParent.sys.mjs, but with single-site and single-page + // message manager groups added. + LoginManager: { + parent: { + esModuleURI: "resource://gre/modules/LoginManagerParent.sys.mjs", + }, + child: { + esModuleURI: "resource://gre/modules/LoginManagerChild.sys.mjs", + events: { + DOMDocFetchSuccess: {}, + DOMFormBeforeSubmit: {}, + DOMFormHasPassword: {}, + DOMInputPasswordAdded: {}, + }, + }, + + allFrames: true, + messageManagerGroups: [ + "browsers", + "single-site", + "single-page", + "webext-browsers", + "", + ], + }, + + MailLink: { + parent: { + moduleURI: "resource:///actors/MailLinkParent.jsm", + }, + child: { + moduleURI: "resource:///actors/MailLinkChild.jsm", + events: { + click: {}, + }, + }, + allFrames: true, + }, + + Pdfjs: { + parent: { + esModuleURI: "resource://pdf.js/PdfjsParent.sys.mjs", + }, + child: { + esModuleURI: "resource://pdf.js/PdfjsChild.sys.mjs", + }, + enablePreference: PREF_PDFJS_ISDEFAULT_CACHE_STATE, + allFrames: true, + }, + + Prompt: { + parent: { + moduleURI: "resource:///actors/PromptParent.jsm", + }, + includeChrome: true, + allFrames: true, + }, + + StrictLinkClickHandler: { + parent: { + moduleURI: "resource:///actors/LinkClickHandlerParent.jsm", + }, + child: { + moduleURI: "resource:///actors/LinkClickHandlerChild.jsm", + events: { + click: {}, + }, + }, + messageManagerGroups: ["single-page"], + allFrames: true, + }, + + VCard: { + parent: { + moduleURI: "resource:///actors/VCardParent.jsm", + }, + child: { + moduleURI: "resource:///actors/VCardChild.jsm", + events: { + click: {}, + }, + }, + allFrames: true, + }, +}; + +// Seconds of idle time before the late idle tasks will be scheduled. +const LATE_TASKS_IDLE_TIME_SEC = 20; + +// Time after we stop tracking startup crashes. +const STARTUP_CRASHES_END_DELAY_MS = 30 * 1000; + +/** + * Glue code that should be executed before any windows are opened. Any + * window-independent helper methods (a la nsBrowserGlue.js) should go in + * MailUtils.jsm instead. + */ + +function MailGlue() { + XPCOMUtils.defineLazyServiceGetter( + this, + "_userIdleService", + "@mozilla.org/widget/useridleservice;1", + "nsIUserIdleService" + ); + this._init(); +} + +// This should match the constant of the same name in devtools +// (devtools/client/framework/browser-toolbox/Launcher.sys.mjs). Otherwise the logic +// in command-line-startup will fail. We have a test to ensure it matches, at +// mail/base/test/unit/test_devtools_url.js. +MailGlue.BROWSER_TOOLBOX_WINDOW_URL = + "chrome://devtools/content/framework/browser-toolbox/window.html"; + +// A Promise that is resolved by an idle task after most start-up operations. +MailGlue.afterStartUp = new Promise(resolve => { + MailGlue.resolveAfterStartUp = resolve; +}); + +MailGlue.prototype = { + _isNewProfile: undefined, + + // init (called at app startup) + _init() { + // Start-up notifications, in order. + // app-startup happens first, registered in components.conf. + Services.obs.addObserver(this, "command-line-startup"); + Services.obs.addObserver(this, "final-ui-startup"); + Services.obs.addObserver(this, "quit-application-granted"); + Services.obs.addObserver(this, "mail-startup-done"); + + // Shut-down notifications. + Services.obs.addObserver(this, "xpcom-shutdown"); + + // General notifications. + Services.obs.addObserver(this, "intl:app-locales-changed"); + Services.obs.addObserver(this, "handle-xul-text-link"); + Services.obs.addObserver(this, "chrome-document-global-created"); + Services.obs.addObserver(this, "document-element-inserted"); + Services.obs.addObserver(this, "handlersvc-store-initialized"); + + // Call the lazy getter to ensure ActorManagerParent is initialized. + lazy.ActorManagerParent; + + // FindBar and LoginManager actors are included in JSWINDOWACTORS as they + // also apply to the single-site and single-page message manager groups. + // First we must unregister them to avoid errors. + ChromeUtils.unregisterWindowActor("FindBar"); + ChromeUtils.unregisterWindowActor("LoginManager"); + + lazy.ActorManagerParent.addJSWindowActors(JSWINDOWACTORS); + }, + + // cleanup (called at shutdown) + _dispose() { + Services.obs.removeObserver(this, "command-line-startup"); + Services.obs.removeObserver(this, "final-ui-startup"); + Services.obs.removeObserver(this, "quit-application-granted"); + // mail-startup-done is removed by its handler. + + Services.obs.removeObserver(this, "xpcom-shutdown"); + + Services.obs.removeObserver(this, "intl:app-locales-changed"); + Services.obs.removeObserver(this, "handle-xul-text-link"); + Services.obs.removeObserver(this, "chrome-document-global-created"); + Services.obs.removeObserver(this, "document-element-inserted"); + Services.obs.removeObserver(this, "handlersvc-store-initialized"); + + lazy.ExtensionSupport.unregisterWindowListener( + "Thunderbird-internal-BrowserConsole" + ); + + lazy.MailUsageTelemetry.uninit(); + + if (this._lateTasksIdleObserver) { + this._userIdleService.removeIdleObserver( + this._lateTasksIdleObserver, + LATE_TASKS_IDLE_TIME_SEC + ); + delete this._lateTasksIdleObserver; + } + }, + + // nsIObserver implementation + observe(aSubject, aTopic, aData) { + switch (aTopic) { + case "app-startup": + // Record the previously started version. This is used to check for + // extensions that were disabled by an application update. We need to + // read this pref before the Add-Ons Manager changes it. + this.previousVersion = Services.prefs.getCharPref( + "extensions.lastAppVersion", + "0" + ); + break; + case "command-line-startup": + // Check if this process is the developer toolbox process, and if it + // is, stop MailGlue from doing anything more. Also sets a flag that + // can be checked to see if this is the toolbox process. + let isToolboxProcess = false; + let commandLine = aSubject.QueryInterface(Ci.nsICommandLine); + let flagIndex = commandLine.findFlag("chrome", true) + 1; + if ( + flagIndex > 0 && + flagIndex < commandLine.length && + commandLine.getArgument(flagIndex) === + MailGlue.BROWSER_TOOLBOX_WINDOW_URL + ) { + isToolboxProcess = true; + } + + MailGlue.__defineGetter__("isToolboxProcess", () => isToolboxProcess); + + if (isToolboxProcess) { + // Clean up all of the listeners. + this._dispose(); + } + break; + case "final-ui-startup": + // Initialise the permission manager. If this happens before telling + // the folder service that strings are available, it's a *much* less + // expensive operation than if it happens afterwards, because if + // strings are available, some types of mail URL go looking for things + // in message databases, causing massive amounts of I/O. + Services.perms.all; + + Cc["@mozilla.org/msgFolder/msgFolderService;1"] + .getService(Ci.nsIMsgFolderService) + .initializeFolderStrings(); + Cc["@mozilla.org/msgDBView/msgDBViewService;1"] + .getService(Ci.nsIMsgDBViewService) + .initializeDBViewStrings(); + this._beforeUIStartup(); + break; + case "quit-application-granted": + Services.startup.trackStartupCrashEnd(); + if (AppConstants.MOZ_UPDATER) { + lazy.UpdateListener.reset(); + } + break; + case "mail-startup-done": + this._onFirstWindowLoaded(); + Services.obs.removeObserver(this, "mail-startup-done"); + break; + case "xpcom-shutdown": + this._dispose(); + break; + case "intl:app-locales-changed": + Cc["@mozilla.org/msgFolder/msgFolderService;1"] + .getService(Ci.nsIMsgFolderService) + .initializeFolderStrings(); + Cc["@mozilla.org/msgDBView/msgDBViewService;1"] + .getService(Ci.nsIMsgDBViewService) + .initializeDBViewStrings(); + let windows = Services.wm.getEnumerator("mail:3pane"); + while (windows.hasMoreElements()) { + let win = windows.getNext(); + win.document.getElementById("threadTree")?.invalidate(); + } + // Refresh the folder tree. + let fls = Cc["@mozilla.org/mail/folder-lookup;1"].getService( + Ci.nsIFolderLookupService + ); + fls.setPrettyNameFromOriginalAllFolders(); + break; + case "handle-xul-text-link": + this._handleLink(aSubject, aData); + break; + case "chrome-document-global-created": + // Set up lwt, but only if the "lightweightthemes" attr is set on the root + // (i.e. in messenger.xhtml). + aSubject.addEventListener( + "DOMContentLoaded", + () => { + if ( + aSubject.document.documentElement.hasAttribute( + "lightweightthemes" + ) + ) { + new lazy.LightweightThemeConsumer(aSubject.document); + } + }, + { once: true } + ); + break; + case "document-element-inserted": + let doc = aSubject; + if ( + doc.nodePrincipal.isSystemPrincipal && + (doc.contentType == "application/xhtml+xml" || + doc.contentType == "text/html") && + // People shouldn't be using our built-in custom elements in + // system-principal about:blank anyway, and trying to support that + // causes responsiveness regressions. So let's not support it. + doc.URL != "about:blank" + ) { + Services.scriptloader.loadSubScript( + "chrome://messenger/content/customElements.js", + doc.ownerGlobal + ); + } + break; + case "handlersvc-store-initialized": { + // Initialize PdfJs when running in-process and remote. This only + // happens once since PdfJs registers global hooks. If the PdfJs + // extension is installed the init method below will be overridden + // leaving initialization to the extension. + // parent only: configure default prefs, set up pref observers, register + // pdf content handler, and initializes parent side message manager + // shim for privileged api access. + lazy.PdfJs.init(this._isNewProfile); + break; + } + } + }, + + // Runs on startup, before the first command line handler is invoked + // (i.e. before the first window is opened). + _beforeUIStartup() { + lazy.TBDistCustomizer.applyPrefDefaults(); + + const UI_VERSION_PREF = "mail.ui-rdf.version"; + this._isNewProfile = !Services.prefs.prefHasUserValue(UI_VERSION_PREF); + + // handle any migration work that has to happen at profile startup + lazy.MailMigrator.migrateAtProfileStartup(); + + if (!Services.prefs.prefHasUserValue(PREF_PDFJS_ISDEFAULT_CACHE_STATE)) { + lazy.PdfJs.checkIsDefault(this._isNewProfile); + } + + // Inject scripts into some devtools windows. + function _setupBrowserConsole(domWindow) { + // Browser Console is an XHTML document. + domWindow.document.title = + lazy.gMailBundle.GetStringFromName("errorConsoleTitle"); + Services.scriptloader.loadSubScript( + "chrome://global/content/viewSourceUtils.js", + domWindow + ); + } + + lazy.ExtensionSupport.registerWindowListener( + "Thunderbird-internal-BrowserConsole", + { + chromeURLs: ["chrome://devtools/content/webconsole/index.html"], + onLoadWindow: _setupBrowserConsole, + } + ); + + // check if we're in safe mode + if (Services.appinfo.inSafeMode) { + Services.ww.openWindow( + null, + "chrome://messenger/content/troubleshootMode.xhtml", + "_blank", + "chrome,centerscreen,modal,resizable=no", + null + ); + } + + lazy.AddonManager.maybeInstallBuiltinAddon( + "thunderbird-compact-light@mozilla.org", + "1.2", + "resource://builtin-themes/light/" + ); + lazy.AddonManager.maybeInstallBuiltinAddon( + "thunderbird-compact-dark@mozilla.org", + "1.2", + "resource://builtin-themes/dark/" + ); + + if (AppConstants.MOZ_UPDATER) { + listeners.init(); + } + }, + + _checkForOldBuildUpdates() { + // check for update if our build is old + if ( + AppConstants.MOZ_UPDATER && + Services.prefs.getBoolPref("app.update.checkInstallTime") + ) { + let buildID = Services.appinfo.appBuildID; + let today = new Date().getTime(); + /* eslint-disable no-multi-spaces */ + let buildDate = new Date( + buildID.slice(0, 4), // year + buildID.slice(4, 6) - 1, // months are zero-based. + buildID.slice(6, 8), // day + buildID.slice(8, 10), // hour + buildID.slice(10, 12), // min + buildID.slice(12, 14) + ) // ms + .getTime(); + /* eslint-enable no-multi-spaces */ + + const millisecondsIn24Hours = 86400000; + let acceptableAge = + Services.prefs.getIntPref("app.update.checkInstallTime.days") * + millisecondsIn24Hours; + + if (buildDate + acceptableAge < today) { + Cc["@mozilla.org/updates/update-service;1"] + .getService(Ci.nsIApplicationUpdateService) + .checkForBackgroundUpdates(); + } + } + }, + + _onFirstWindowLoaded() { + // Start these services. + Cc["@mozilla.org/chat/logger;1"].getService(Ci.imILogger); + + this._checkForOldBuildUpdates(); + + // On Windows 7 and above, initialize the jump list module. + const WINTASKBAR_CONTRACTID = "@mozilla.org/windows-taskbar;1"; + if ( + WINTASKBAR_CONTRACTID in Cc && + Cc[WINTASKBAR_CONTRACTID].getService(Ci.nsIWinTaskbar).available + ) { + const { WinTaskbarJumpList } = ChromeUtils.import( + "resource:///modules/WindowsJumpLists.jsm" + ); + WinTaskbarJumpList.startup(); + } + + const { ExtensionsUI } = ChromeUtils.import( + "resource:///modules/ExtensionsUI.jsm" + ); + ExtensionsUI.init(); + + // If the application has been updated, check all installed extensions for + // updates. + let currentVersion = Services.appinfo.version; + if (this.previousVersion != "0" && this.previousVersion != currentVersion) { + let { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" + ); + let { XPIDatabase } = ChromeUtils.import( + "resource://gre/modules/addons/XPIDatabase.jsm" + ); + let addons = XPIDatabase.getAddons(); + for (let addon of addons) { + if (addon.permissions() & AddonManager.PERM_CAN_UPGRADE) { + AddonManager.getAddonByID(addon.id).then(addon => { + if (!AddonManager.shouldAutoUpdate(addon)) { + return; + } + addon.findUpdates( + { + onUpdateFinished() {}, + onUpdateAvailable(addon, install) { + install.install(); + }, + }, + AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED + ); + }); + } + } + } + + if (AppConstants.ASAN_REPORTER) { + var { AsanReporter } = ChromeUtils.importESModule( + "resource://gre/modules/AsanReporter.sys.mjs" + ); + AsanReporter.init(); + } + + // Check if Sync is configured + if ( + AppConstants.NIGHTLY_BUILD && + Services.prefs.prefHasUserValue("services.sync.username") + ) { + lazy.WeaveService.init(); + } + + this._scheduleStartupIdleTasks(); + this._lateTasksIdleObserver = (idleService, topic, data) => { + if (topic == "idle") { + idleService.removeIdleObserver( + this._lateTasksIdleObserver, + LATE_TASKS_IDLE_TIME_SEC + ); + delete this._lateTasksIdleObserver; + this._scheduleBestEffortUserIdleTasks(); + } + }; + this._userIdleService.addIdleObserver( + this._lateTasksIdleObserver, + LATE_TASKS_IDLE_TIME_SEC + ); + + lazy.MailUsageTelemetry.init(); + }, + + /** + * Use this function as an entry point to schedule tasks that + * need to run only once after startup, and can be scheduled + * by using an idle callback. + * + * The functions scheduled here will fire from idle callbacks + * once every window has finished being restored by session + * restore, and it's guaranteed that they will run before + * the equivalent per-window idle tasks + * (from _schedulePerWindowIdleTasks in browser.js). + * + * If you have something that can wait even further than the + * per-window initialization, and is okay with not being run in some + * sessions, please schedule them using + * _scheduleBestEffortUserIdleTasks. + * Don't be fooled by thinking that the use of the timeout parameter + * will delay your function: it will just ensure that it potentially + * happens _earlier_ than expected (when the timeout limit has been reached), + * but it will not make it happen later (and out of order) compared + * to the other ones scheduled together. + */ + _scheduleStartupIdleTasks() { + const idleTasks = [ + { + task() { + // This module needs to be loaded so it registers to receive + // FormAutoComplete:GetSelectedIndex messages and respond + // appropriately, otherwise we get error messages like the one + // reported in bug 1635422. + ChromeUtils.importESModule( + "resource://gre/actors/AutoCompleteParent.sys.mjs" + ); + }, + }, + { + task() { + // Make sure Gloda's up and running. + ChromeUtils.import("resource:///modules/gloda/GlodaPublic.jsm"); + }, + }, + { + task() { + MailGlue.resolveAfterStartUp(); + }, + }, + { + task() { + let { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" + ); + setTimeout(function () { + Services.tm.idleDispatchToMainThread( + Services.startup.trackStartupCrashEnd + ); + }, STARTUP_CRASHES_END_DELAY_MS); + }, + }, + { + condition: AppConstants.NIGHTLY_BUILD, + task: async () => { + // Register our sync engines. + await lazy.WeaveService.whenLoaded(); + let Weave = lazy.WeaveService.Weave; + + for (let [moduleName, engineName] of [ + ["accounts", "AccountsEngine"], + ["addressBooks", "AddressBooksEngine"], + ["calendars", "CalendarsEngine"], + ["identities", "IdentitiesEngine"], + ]) { + let ns = ChromeUtils.importESModule( + `resource://services-sync/engines/${moduleName}.sys.mjs` + ); + await Weave.Service.engineManager.register(ns[engineName]); + Weave.Service.engineManager + .get(moduleName.toLowerCase()) + .startTracking(); + } + + if (lazy.WeaveService.enabled) { + // Schedule a sync (if enabled). + Weave.Service.scheduler.autoConnect(); + } + }, + }, + { + condition: Services.prefs.getBoolPref("mail.chat.enabled"), + task() { + lazy.ChatCore.idleStart(); + ChromeUtils.importESModule("resource:///modules/index_im.sys.mjs"); + }, + }, + { + condition: AppConstants.MOZ_UPDATER, + task: () => { + lazy.UpdateListener.maybeShowUnsupportedNotification(); + }, + }, + { + task() { + // Use idleDispatch a second time to run this after the per-window + // idle tasks. + ChromeUtils.idleDispatch(() => { + Services.obs.notifyObservers( + null, + "mail-startup-idle-tasks-finished" + ); + }); + }, + }, + // Do NOT add anything after idle tasks finished. + ]; + + for (let task of idleTasks) { + if ("condition" in task && !task.condition) { + continue; + } + + ChromeUtils.idleDispatch( + () => { + if (!Services.startup.shuttingDown) { + let startTime = Cu.now(); + try { + task.task(); + } catch (ex) { + console.error(ex); + } finally { + ChromeUtils.addProfilerMarker("startupIdleTask", startTime); + } + } + }, + task.timeout ? { timeout: task.timeout } : undefined + ); + } + }, + + /** + * Use this function as an entry point to schedule tasks that we hope + * to run once per session, at any arbitrary point in time, and which we + * are okay with sometimes not running at all. + * + * This function will be called from an idle observer. Check the value of + * LATE_TASKS_IDLE_TIME_SEC to see the current value for this idle + * observer. + * + * Note: this function may never be called if the user is never idle for the + * requisite time (LATE_TASKS_IDLE_TIME_SEC). Be certain before adding + * something here that it's okay that it never be run. + */ + _scheduleBestEffortUserIdleTasks() { + const idleTasks = [ + // Certificates revocation list, etc. + () => lazy.RemoteSecuritySettings.init(), + // If we haven't already, ensure the address book manager is ready. + // This must happen at some point so that CardDAV address books sync. + () => lazy.MailServices.ab.directories, + // Telemetry. + async () => { + lazy.OsEnvironment.reportAllowedAppSources(); + reportAccountTypes(); + reportAddressBookTypes(); + reportAccountSizes(); + await reportCalendars(); + reportPreferences(); + reportUIConfiguration(); + }, + ]; + + for (let task of idleTasks) { + ChromeUtils.idleDispatch(async () => { + if (!Services.startup.shuttingDown) { + let startTime = Cu.now(); + try { + await task(); + } catch (ex) { + console.error(ex); + } finally { + ChromeUtils.addProfilerMarker("startupLateIdleTask", startTime); + } + } + }); + } + }, + + _handleLink(aSubject, aData) { + let linkHandled = aSubject.QueryInterface(Ci.nsISupportsPRBool); + if (!linkHandled.data) { + let win = Services.wm.getMostRecentWindow("mail:3pane"); + aData = JSON.parse(aData); + let tabParams = { url: aData.href, linkHandler: null }; + if (win) { + let tabmail = win.document.getElementById("tabmail"); + if (tabmail) { + tabmail.openTab("contentTab", tabParams); + win.focus(); + linkHandled.data = true; + return; + } + } + + // If we didn't have an open 3 pane window, try and open one. + Services.ww.openWindow( + null, + "chrome://messenger/content/messenger.xhtml", + "_blank", + "chrome,dialog=no,all", + { + type: "contentTab", + tabParams, + } + ); + linkHandled.data = true; + } + }, + + // for XPCOM + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), +}; + +/** + * Report account types to telemetry. For im accounts, use `im_protocol` as + * scalar key name. + */ +function reportAccountTypes() { + // Init all count with 0, so that when an account was set up before but + // removed now, we reset it in telemetry report. + let report = { + pop3: 0, + imap: 0, + nntp: 0, + exchange: 0, + rss: 0, + im_gtalk: 0, + im_irc: 0, + im_jabber: 0, + im_matrix: 0, + im_odnoklassniki: 0, + }; + + const providerReport = { + google: 0, + microsoft: 0, + yahoo_aol: 0, + other: 0, + }; + + for (let account of lazy.MailServices.accounts.accounts) { + const incomingServer = account.incomingServer; + + let type = incomingServer.type; + if (type == "none") { + // Reporting one Local Folders account is not that useful. Skip it. + continue; + } + + if (type === "im") { + let protocol = + incomingServer.wrappedJSObject.imAccount.protocol.normalizedName; + type = `im_${protocol}`; + } + + // It's still possible to report other types not explicitly specified due to + // account types that used to exist, but no longer -- e.g. im_yahoo. + if (!report[type]) { + report[type] = 0; + } + + report[type]++; + + // Collect a rough understanding of the frequency of various OAuth + // providers. + if (incomingServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) { + const hostnameDetails = lazy.OAuth2Providers.getHostnameDetails( + incomingServer.hostName + ); + + if (!hostnameDetails || hostnameDetails.length == 0) { + // Not a valid OAuth2 configuration; skip it + continue; + } + + const host = hostnameDetails[0]; + + switch (host) { + case "accounts.google.com": + providerReport.google++; + break; + case "login.microsoftonline.com": + providerReport.microsoft++; + break; + case "login.yahoo.com": + case "login.aol.com": + providerReport.yahoo_aol++; + break; + default: + providerReport.other++; + } + } + } + + for (let [type, count] of Object.entries(report)) { + Services.telemetry.keyedScalarSet("tb.account.count", type, count); + } + + for (const [provider, count] of Object.entries(providerReport)) { + Services.telemetry.keyedScalarSet( + "tb.account.oauth2_provider_count", + provider, + count + ); + } +} + +/** + * Report size on disk and messages count of each type of folder to telemetry. + */ +function reportAccountSizes() { + let keys = [ + "Inbox", + "Drafts", + "Trash", + "SentMail", + "Templates", + "Junk", + "Archive", + "Queue", + ]; + for (let key of keys) { + Services.telemetry.keyedScalarSet("tb.account.total_messages", key, 0); + } + Services.telemetry.keyedScalarSet("tb.account.total_messages", "Other", 0); + Services.telemetry.keyedScalarSet("tb.account.total_messages", "Total", 0); + + for (let server of lazy.MailServices.accounts.allServers) { + if ( + server instanceof Ci.nsIPop3IncomingServer && + server.deferredToAccount + ) { + // Skip deferred accounts + continue; + } + + for (let folder of server.rootFolder.descendants) { + let key = + keys.find(x => folder.getFlag(Ci.nsMsgFolderFlags[x])) || "Other"; + let totalMessages = folder.getTotalMessages(false); + if (totalMessages > 0) { + Services.telemetry.keyedScalarAdd( + "tb.account.total_messages", + key, + totalMessages + ); + Services.telemetry.keyedScalarAdd( + "tb.account.total_messages", + "Total", + totalMessages + ); + } + let sizeOnDisk = folder.sizeOnDisk; + if (sizeOnDisk > 0) { + Services.telemetry.keyedScalarAdd( + "tb.account.size_on_disk", + key, + sizeOnDisk + ); + Services.telemetry.keyedScalarAdd( + "tb.account.size_on_disk", + "Total", + sizeOnDisk + ); + } + } + } +} + +/** + * Report addressbook count and contact count to telemetry, keyed by addressbook + * type. Type is one of ["jsaddrbook", "jscarddav", "moz-abldapdirectory"], see + * AddrBookManager.jsm for more details. + * + * NOTE: We didn't use `dir.dirType` because it's just an integer, instead we + * use the scheme of `dir.URI` as the type. + */ +function reportAddressBookTypes() { + let report = {}; + for (let dir of lazy.MailServices.ab.directories) { + let type = dir.URI.split(":")[0]; + + if (!report[type]) { + report[type] = { count: 0, contactCount: 0 }; + } + report[type].count++; + + try { + report[type].contactCount += dir.childCardCount; + } catch (ex) { + // Directories may throw NS_ERROR_NOT_IMPLEMENTED. + } + } + + for (let [type, { count, contactCount }] of Object.entries(report)) { + Services.telemetry.keyedScalarSet( + "tb.addressbook.addressbook_count", + type, + count + ); + Services.telemetry.keyedScalarSet( + "tb.addressbook.contact_count", + type, + contactCount + ); + } +} + +/** + * A telemetry probe to report calendar count and read only calendar count. + */ +async function reportCalendars() { + let telemetryReport = {}; + let home = lazy.cal.l10n.getCalString("homeCalendarName"); + + for (let calendar of lazy.cal.manager.getCalendars()) { + if (calendar.name == home && calendar.type == "storage") { + // Ignore the "Home" calendar if it is disabled or unused as it's + // automatically added. + if (calendar.getProperty("disabled")) { + continue; + } + let items = await calendar.getItemsAsArray( + Ci.calICalendar.ITEM_FILTER_ALL_ITEMS, + 1, + null, + null + ); + if (!items.length) { + continue; + } + } + if (!telemetryReport[calendar.type]) { + telemetryReport[calendar.type] = { count: 0, readOnlyCount: 0 }; + } + telemetryReport[calendar.type].count++; + if (calendar.readOnly) { + telemetryReport[calendar.type].readOnlyCount++; + } + } + + for (let [type, { count, readOnlyCount }] of Object.entries( + telemetryReport + )) { + Services.telemetry.keyedScalarSet( + "tb.calendar.calendar_count", + type.toLowerCase(), + count + ); + Services.telemetry.keyedScalarSet( + "tb.calendar.read_only_calendar_count", + type.toLowerCase(), + readOnlyCount + ); + } +} + +function reportPreferences() { + let booleanPrefs = [ + // General + "browser.cache.disk.smart_size.enabled", + "privacy.clearOnShutdown.cache", + "general.autoScroll", + "general.smoothScroll", + "intl.regional_prefs.use_os_locales", + "layers.acceleration.disabled", + "mail.biff.play_sound", + "mail.close_message_window.on_delete", + "mail.delete_matches_sort_order", + "mail.display_glyph", + "mail.mailnews.scroll_to_new_message", + "mail.prompt_purge_threshhold", + "mail.purge.ask", + "mail.showCondensedAddresses", + "mailnews.database.global.indexer.enabled", + "mailnews.mark_message_read.auto", + "mailnews.mark_message_read.delay", + "mailnews.reuse_message_window", + "mailnews.start_page.enabled", + "searchintegration.enable", + + // Fonts + "mail.fixed_width_messages", + + // Colors + "browser.display.use_system_colors", + "browser.underline_anchors", + + // Read receipts + "mail.mdn.report.enabled", + "mail.receipt.request_return_receipt_on", + + // Connection + "network.proxy.share_proxy_settings", + "network.proxy.socks_remote_dns", + "pref.advanced.proxies.disable_button.reload", + "signon.autologin.proxy", + + // Offline + "offline.autoDetect", + + // Compose + "ldap_2.autoComplete.useDirectory", + "mail.collect_email_address_outgoing", + "mail.compose.attachment_reminder", + "mail.compose.autosave", + "mail.compose.big_attachments.notify", + "mail.compose.default_to_paragraph", + "mail.e2ee.auto_enable", + "mail.e2ee.auto_disable", + "mail.e2ee.notify_on_auto_disable", + "mail.enable_autocomplete", + "mail.forward_add_extension", + "mail.SpellCheckBeforeSend", + "mail.spellcheck.inline", + "mail.warn_on_send_accel_key", + "msgcompose.default_colors", + "pref.ldap.disable_button.edit_directories", + + // Send options + "mailnews.sendformat.auto_downgrade", + + // Privacy + "browser.safebrowsing.enabled", + "mail.phishing.detection.enabled", + "mail.spam.logging.enabled", + "mail.spam.manualMark", + "mail.spam.markAsReadOnSpam", + "mailnews.downloadToTempFile", + "mailnews.message_display.disable_remote_image", + "network.cookie.blockFutureCookies", + "places.history.enabled", + "pref.privacy.disable_button.cookie_exceptions", + "pref.privacy.disable_button.view_cookies", + "pref.privacy.disable_button.view_passwords", + "privacy.donottrackheader.enabled", + "security.disable_button.openCertManager", + "security.disable_button.openDeviceManager", + + // Chat + "messenger.options.getAttentionOnNewMessages", + "messenger.status.reportIdle", + "messenger.status.awayWhenIdle", + "mail.chat.enabled", + "mail.chat.play_sound", + "mail.chat.show_desktop_notifications", + "purple.conversations.im.send_typing", + "purple.logging.log_chats", + "purple.logging.log_ims", + "purple.logging.log_system", + + // Calendar views + "calendar.view.showLocation", + "calendar.view-minimonth.showWeekNumber", + "calendar.week.d0sundaysoff", + "calendar.week.d1mondaysoff", + "calendar.week.d2tuesdaysoff", + "calendar.week.d3wednesdaysoff", + "calendar.week.d4thursdaysoff", + "calendar.week.d5fridaysoff", + "calendar.week.d6saturdaysoff", + + // Calendar general + "calendar.item.editInTab", + "calendar.item.promptDelete", + "calendar.timezone.useSystemTimezone", + + // Alarms + "calendar.alarms.playsound", + "calendar.alarms.show", + "calendar.alarms.showmissed", + + // Unlisted + "mail.operate_on_msgs_in_collapsed_threads", + ]; + + let integerPrefs = [ + // Mail UI + "mail.pane_config.dynamic", + "mail.ui.display.dateformat.default", + "mail.ui.display.dateformat.thisweek", + "mail.ui.display.dateformat.today", + ]; + + // Platform-specific preferences + if (AppConstants.platform === "win") { + booleanPrefs.push("mail.biff.show_tray_icon", "mail.minimizeToTray"); + } + + if (AppConstants.platform !== "macosx") { + booleanPrefs.push( + "mail.biff.show_alert", + "mail.biff.use_system_alert", + + // Notifications + "mail.biff.alert.show_preview", + "mail.biff.alert.show_sender", + "mail.biff.alert.show_subject" + ); + } + + // Compile-time flag-dependent preferences + if (AppConstants.HAVE_SHELL_SERVICE) { + booleanPrefs.push("mail.shell.checkDefaultClient"); + } + + if (AppConstants.MOZ_WIDGET_GTK) { + booleanPrefs.push("widget.gtk.overlay-scrollbars.enabled"); + } + + if (AppConstants.MOZ_MAINTENANCE_SERVICE) { + booleanPrefs.push("app.update.service.enabled"); + } + + if (AppConstants.MOZ_DATA_REPORTING) { + booleanPrefs.push("datareporting.healthreport.uploadEnabled"); + } + + if (AppConstants.MOZ_CRASHREPORTER) { + booleanPrefs.push("browser.crashReports.unsubmittedCheck.autoSubmit2"); + } + + // Fetch and report preference values + for (let prefName of booleanPrefs) { + let prefValue = Services.prefs.getBoolPref(prefName, false); + + Services.telemetry.keyedScalarSet( + "tb.preferences.boolean", + prefName, + prefValue + ); + } + + for (let prefName of integerPrefs) { + let prefValue = Services.prefs.getIntPref(prefName, 0); + + Services.telemetry.keyedScalarSet( + "tb.preferences.integer", + prefName, + prefValue + ); + } +} + +function reportUIConfiguration() { + let docURL = "chrome://messenger/content/messenger.xhtml"; + + let folderTreeMode = Services.xulStore.getValue(docURL, "folderTree", "mode"); + if (folderTreeMode) { + let folderTreeCompact = Services.xulStore.getValue( + docURL, + "folderTree", + "compact" + ); + if (folderTreeCompact === "true") { + folderTreeMode += " (compact)"; + } + Services.telemetry.scalarSet( + "tb.ui.configuration.folder_tree_modes", + folderTreeMode + ); + } + + let headerLayout = Services.xulStore.getValue( + docURL, + "messageHeader", + "layout" + ); + if (headerLayout) { + headerLayout = JSON.parse(headerLayout); + for (let [key, value] of Object.entries(headerLayout)) { + if (key == "buttonStyle") { + value = { default: 0, "only-icons": 1, "only-text": 2 }[value]; + } + Services.telemetry.keyedScalarSet( + "tb.ui.configuration.message_header", + key, + value + ); + } + } +} + +/** + * Export these functions so we can test them. This object shouldn't be + * accessed outside of a test (hence the name). + */ +var MailTelemetryForTests = { + reportAccountTypes, + reportAccountSizes, + reportAddressBookTypes, + reportCalendars, + reportPreferences, + reportUIConfiguration, +}; diff --git a/comm/mail/components/MessengerContentHandler.jsm b/comm/mail/components/MessengerContentHandler.jsm new file mode 100644 index 0000000000..06d37a0811 --- /dev/null +++ b/comm/mail/components/MessengerContentHandler.jsm @@ -0,0 +1,793 @@ +/* -*- indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = [ + "MessengerContentHandler", + "MessageDisplayContentHandler", +]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + FeedUtils: "resource:///modules/FeedUtils.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", + MimeParser: "resource:///modules/mimeParser.jsm", + NetUtil: "resource://gre/modules/NetUtil.jsm", +}); + +function resolveURIInternal(aCmdLine, aArgument) { + var uri = aCmdLine.resolveURI(aArgument); + + if (!(uri instanceof Ci.nsIFileURL)) { + return uri; + } + + try { + if (uri.file.exists()) { + return uri; + } + } catch (e) { + console.error(e); + } + + // We have interpreted the argument as a relative file URI, but the file + // doesn't exist. Try URI fixup heuristics: see bug 290782. + + try { + uri = Services.uriFixup.getFixupURIInfo(aArgument, 0).preferredURI; + } catch (e) { + console.error(e); + } + + return uri; +} + +function handleIndexerResult(aFile) { + // Do this here because xpcshell isn't too happy with this at startup + // Make sure the folder tree is initialized + lazy.MailUtils.discoverFolders(); + + // Use the search integration module to convert the indexer result into a + // message header + const { SearchIntegration } = ChromeUtils.import( + "resource:///modules/SearchIntegration.jsm" + ); + let msgHdr = SearchIntegration.handleResult(aFile); + + // If we found a message header, open it, otherwise throw an exception + if (msgHdr) { + getOrOpen3PaneWindow().then(win => { + lazy.MailUtils.displayMessage(msgHdr); + }); + } else { + throw Components.Exception("", Cr.NS_ERROR_FAILURE); + } +} + +async function getOrOpen3PaneWindow() { + let win = Services.wm.getMostRecentWindow("mail:3pane"); + + if (!win) { + const startupPromise = new Promise(resolve => { + Services.obs.addObserver( + { + observe(subject) { + if (subject == win) { + Services.obs.removeObserver(this, "mail-startup-done"); + resolve(); + } + }, + }, + "mail-startup-done" + ); + }); + + // Bug 277798 - we have to pass an argument to openWindow(), or + // else it won't honor the dialog=no instruction. + const argstring = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + win = Services.ww.openWindow( + null, + "chrome://messenger/content/messenger.xhtml", + "_blank", + "chrome,dialog=no,all", + argstring + ); + await startupPromise; + } + + await win.delayedStartupPromise; + return win; +} + +/** + * Open the given uri. + * @param {nsIURI} uri - The uri to open. + */ +function openURI(uri) { + if ( + !Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .isExposedProtocol(uri.scheme) + ) { + throw Components.Exception(`Can't open: ${uri.spec}`, Cr.NS_ERROR_FAILURE); + } + + var channel = Services.io.newChannelFromURI( + uri, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + var loader = Cc["@mozilla.org/uriloader;1"].getService(Ci.nsIURILoader); + + // We cannot load a URI on startup asynchronously without protecting + // the startup + + var loadgroup = Cc["@mozilla.org/network/load-group;1"].createInstance( + Ci.nsILoadGroup + ); + + var loadlistener = { + onStartRequest(aRequest) { + Services.startup.enterLastWindowClosingSurvivalArea(); + }, + + onStopRequest(aRequest, aStatusCode) { + Services.startup.exitLastWindowClosingSurvivalArea(); + }, + + QueryInterface: ChromeUtils.generateQI([ + "nsIRequestObserver", + "nsISupportsWeakReference", + ]), + }; + + loadgroup.groupObserver = loadlistener; + + var listener = { + doContent(ctype, preferred, request, handler) { + var newHandler = Cc[ + "@mozilla.org/uriloader/content-handler;1?type=application/x-message-display" + ].createInstance(Ci.nsIContentHandler); + newHandler.handleContent("application/x-message-display", this, request); + return true; + }, + isPreferred(ctype, desired) { + if (ctype == "message/rfc822") { + return true; + } + return false; + }, + canHandleContent(ctype, preferred, desired) { + return false; + }, + loadCookie: null, + parentContentListener: null, + getInterface(iid) { + if (iid.equals(Ci.nsIURIContentListener)) { + return this; + } + + if (iid.equals(Ci.nsILoadGroup)) { + return loadgroup; + } + + throw Components.Exception("", Cr.NS_ERROR_NO_INTERFACE); + }, + }; + loader.openURI(channel, true, listener); +} + +function MailDefaultHandler() {} + +MailDefaultHandler.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsICommandLineHandler", + "nsICommandLineValidator", + "nsIFactory", + ]), + + /* nsICommandLineHandler */ + + handle(cmdLine) { + if ( + cmdLine.state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH && + Services.startup.wasSilentlyStarted + ) { + // If we are starting up in silent mode, don't open a window. We also need + // to make sure that the application doesn't immediately exit, so stay in + // a LastWindowClosingSurvivalArea until a window opens. + Services.startup.enterLastWindowClosingSurvivalArea(); + Services.obs.addObserver(function windowOpenObserver() { + Services.startup.exitLastWindowClosingSurvivalArea(); + Services.obs.removeObserver(windowOpenObserver, "domwindowopened"); + }, "domwindowopened"); + return; + } + + try { + var remoteCommand = cmdLine.handleFlagWithParam("remote", true); + } catch (e) { + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } + + if (remoteCommand != null) { + try { + var a = /^\s*(\w+)\(([^\)]*)\)\s*$/.exec(remoteCommand); + var remoteVerb = a[1].toLowerCase(); + var remoteParams = a[2].split(","); + + switch (remoteVerb) { + case "openurl": { + let xuri = cmdLine.resolveURI(remoteParams[0]); + openURI(xuri); + break; + } + case "mailto": { + let xuri = cmdLine.resolveURI("mailto:" + remoteParams[0]); + openURI(xuri); + break; + } + case "xfedocommand": + // xfeDoCommand(openBrowser) + switch (remoteParams[0].toLowerCase()) { + case "openinbox": { + getOrOpen3PaneWindow().then(win => win.focus()); + break; + } + case "composemessage": { + let argstring = Cc[ + "@mozilla.org/supports-string;1" + ].createInstance(Ci.nsISupportsString); + remoteParams.shift(); + argstring.data = remoteParams.join(","); + let args = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + args.appendElement(argstring); + args.appendElement(cmdLine); + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://messenger/content/messengercompose/messengercompose.xhtml", + "_blank", + "chrome,dialog=no,all", + args + ) + ); + break; + } + default: + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } + break; + + default: + // Somebody sent us a remote command we don't know how to process: + // just abort. + throw Components.Exception( + `Unrecognized command: ${remoteParams[0]}`, + Cr.NS_ERROR_ABORT + ); + } + + cmdLine.preventDefault = true; + } catch (e) { + // If we had a -remote flag but failed to process it, throw + // NS_ERROR_ABORT so that the xremote code knows to return a failure + // back to the handling code. + dump(e); + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } + } + + var chromeParam = cmdLine.handleFlagWithParam("chrome", false); + if (chromeParam) { + // The parameter specifies the window to open. This code should *not* + // open messenger.xhtml as well. + try { + let argstring = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + let _uri = resolveURIInternal(cmdLine, chromeParam); + // only load URIs which do not inherit chrome privs + if ( + !Services.io.URIChainHasFlags( + _uri, + Ci.nsIProtocolHandler.URI_INHERITS_SECURITY_CONTEXT + ) + ) { + Services.ww.openWindow( + null, + _uri.spec, + "_blank", + "chrome,dialog=no,all", + argstring + ); + cmdLine.preventDefault = true; + } + } catch (e) { + dump(e); + } + } + + if (cmdLine.handleFlag("silent", false)) { + cmdLine.preventDefault = true; + } + + // -MapiStartup + // indicates that this startup is due to MAPI. Don't do anything for now. + cmdLine.handleFlag("MapiStartup", false); + + if (cmdLine.handleFlag("mail", false)) { + getOrOpen3PaneWindow().then(win => win.focusOnMail(0)); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("addressbook", false)) { + getOrOpen3PaneWindow().then(win => win.toAddressBook()); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("options", false)) { + getOrOpen3PaneWindow().then(win => win.openPreferencesTab()); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("calendar", false)) { + getOrOpen3PaneWindow().then(win => win.toCalendar()); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("keymanager", false)) { + getOrOpen3PaneWindow().then(win => win.openKeyManager()); + cmdLine.preventDefault = true; + } + + if (cmdLine.handleFlag("setDefaultMail", false)) { + var shell = Cc["@mozilla.org/mail/shell-service;1"].getService( + Ci.nsIShellService + ); + shell.setDefaultClient(true, Ci.nsIShellService.MAIL); + } + + // The URI might be passed as the argument to the file parameter + let uri = cmdLine.handleFlagWithParam("file", false); + // macOS passes `-url mid:` into the command line, drop the -url flag. + cmdLine.handleFlag("url", false); + + var count = cmdLine.length; + if (count) { + var i = 0; + while (i < count) { + var curarg = cmdLine.getArgument(i); + if (!curarg.startsWith("-")) { + break; + } + + dump("Warning: unrecognized command line flag " + curarg + "\n"); + // To emulate the pre-nsICommandLine behavior, we ignore the + // argument after an unrecognized flag. + i += 2; + // xxxbsmedberg: make me use the console service! + } + + if (i < count) { + uri = cmdLine.getArgument(i); + + // mailto: URIs are frequently passed with spaces in them. They should be + // escaped into %20, but we hack around bad clients, see bug 231032 + if (uri.startsWith("mailto:")) { + while (++i < count) { + var testarg = cmdLine.getArgument(i); + if (testarg.startsWith("-")) { + break; + } + + uri += " " + testarg; + } + } + } + } + + if (!uri && cmdLine.preventDefault) { + return; + } + + if (!uri && cmdLine.state != Ci.nsICommandLine.STATE_INITIAL_LAUNCH) { + try { + for (let window of Services.wm.getEnumerator("mail:3pane")) { + window.focus(); + return; + } + } catch (e) { + dump(e); + } + } + if (uri) { + if (/^file:/i.test(uri)) { + // Turn file URL into a file path so `resolveFile()` will work. + let fileURL = cmdLine.resolveURI(uri); + uri = fileURL.QueryInterface(Ci.nsIFileURL).file.path; + } + // Check for protocols first then look at the file ending. + // Protocols are able to contain file endings like '.ics'. + if (/^https?:/i.test(uri) || /^feed:/i.test(uri)) { + getOrOpen3PaneWindow().then(win => { + lazy.FeedUtils.subscribeToFeed(uri, null); + }); + } else if (/^webcals?:\/\//i.test(uri)) { + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://calendar/content/calendar-creation.xhtml", + "_blank", + "chrome,titlebar,modal,centerscreen", + Services.io.newURI(uri) + ) + ); + } else if (/^mid:/i.test(uri)) { + getOrOpen3PaneWindow().then(win => { + lazy.MailUtils.openMessageByMessageId(uri.slice(4)); + }); + } else if (/^(mailbox|imap|news)-message:\/\//.test(uri)) { + getOrOpen3PaneWindow().then(win => { + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + lazy.MailUtils.displayMessage(messenger.msgHdrFromURI(uri)); + }); + } else if (/^imap:/i.test(uri) || /^s?news:/i.test(uri)) { + getOrOpen3PaneWindow().then(win => { + openURI(cmdLine.resolveURI(uri)); + }); + } else if ( + // While the leading web+ and ext+ identifiers may be case insensitive, + // the protocol identifiers must be lowercase. + /^(web|ext)\+[a-z]+:/i.test(uri) && + /^[a-z]+:/.test(uri.split("+")[1]) + ) { + getOrOpen3PaneWindow().then(win => { + win.gTabmail.openTab("contentTab", { + url: uri, + linkHandler: "single-site", + background: false, + duplicate: true, + }); + }); + } else if ( + uri.toLowerCase().endsWith(".mozeml") || + uri.toLowerCase().endsWith(".wdseml") + ) { + handleIndexerResult(cmdLine.resolveFile(uri)); + cmdLine.preventDefault = true; + } else if (uri.toLowerCase().endsWith(".eml")) { + // Open this eml in a new message window + let file = cmdLine.resolveFile(uri); + // No point in trying to open a file if it doesn't exist or is empty + if (file.exists() && file.fileSize > 0) { + // Read this eml and extract its headers to check for X-Unsent. + let fstream = null; + let headers = new Map(); + try { + fstream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + fstream.init(file, -1, 0, 0); + let data = lazy.NetUtil.readInputStreamToString( + fstream, + fstream.available() + ); + headers = lazy.MimeParser.extractHeaders(data); + } catch (e) { + // Ignore errors on reading the eml or extracting its headers. The + // test for the X-Unsent header below will fail and the message + // window will take care of any error handling. + } finally { + if (fstream) { + fstream.close(); + } + } + + // Get the URL for this file + let fileURL = Services.io + .newFileURI(file) + .QueryInterface(Ci.nsIFileURL); + fileURL = fileURL + .mutate() + .setQuery("type=application/x-message-display") + .finalize(); + + if (headers.get("X-Unsent") == "1") { + getOrOpen3PaneWindow().then(win => { + const msgWindow = Cc[ + "@mozilla.org/messenger/msgwindow;1" + ].createInstance(Ci.nsIMsgWindow); + MailServices.compose.OpenComposeWindow( + win, + {}, + fileURL.spec, + Ci.nsIMsgCompType.Draft, + Ci.nsIMsgCompFormat.Default, + null, + headers.get("from"), + msgWindow + ); + }); + } else { + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://messenger/content/messageWindow.xhtml", + "_blank", + "all,chrome,dialog=no,status,toolbar", + fileURL + ) + ); + } + cmdLine.preventDefault = true; + } else { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + let title, message; + if (!file.exists()) { + title = bundle.GetStringFromName("fileNotFoundTitle"); + message = bundle.formatStringFromName("fileNotFoundMsg", [ + file.path, + ]); + } else { + // The file is empty + title = bundle.GetStringFromName("fileEmptyTitle"); + message = bundle.formatStringFromName("fileEmptyMsg", [file.path]); + } + + Services.prompt.alert(null, title, message); + } + } else if (uri.toLowerCase().endsWith(".ics")) { + // An .ics calendar file! Open the ics file dialog. + let file = cmdLine.resolveFile(uri); + if (file.exists() && file.fileSize > 0) { + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://calendar/content/calendar-ics-file-dialog.xhtml", + "_blank", + "chrome,titlebar,modal,centerscreen", + file + ) + ); + } + } else if (uri.toLowerCase().endsWith(".vcf")) { + // A VCard! Be smart and open the "add contact" dialog. + let file = cmdLine.resolveFile(uri); + if (file.exists() && file.fileSize > 0) { + let winPromise = getOrOpen3PaneWindow(); + let uriSpec = Services.io.newFileURI(file).spec; + lazy.NetUtil.asyncFetch( + { uri: uriSpec, loadUsingSystemPrincipal: true }, + function (inputStream, status) { + if (!Components.isSuccessCode(status)) { + return; + } + + let data = lazy.NetUtil.readInputStreamToString( + inputStream, + inputStream.available() + ); + // Try to detect the character set and decode. Only UTF-8 is + // valid from vCard 4.0, but we support older versions, so other + // charsets are possible. + let charset = Cc["@mozilla.org/messengercompose/computils;1"] + .createInstance(Ci.nsIMsgCompUtils) + .detectCharset(data); + let buffer = new Uint8Array( + Array.from(data, c => c.charCodeAt(0)) + ); + data = new TextDecoder(charset).decode(buffer); + + winPromise.then(win => + win.toAddressBook({ + action: "create", + vCard: decodeURIComponent(data), + }) + ); + } + ); + } + } else { + getOrOpen3PaneWindow().then(win => { + // This must be a regular filename. Use it to create a new message + // with attachment. + let msgParams = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + let composeFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + let attachment = Cc[ + "@mozilla.org/messengercompose/attachment;1" + ].createInstance(Ci.nsIMsgAttachment); + let localFile = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + let fileHandler = Services.io + .getProtocolHandler("file") + .QueryInterface(Ci.nsIFileProtocolHandler); + + try { + // Unescape the URI so that we work with clients that escape spaces. + localFile.initWithPath(unescape(uri)); + attachment.url = fileHandler.getURLSpecFromActualFile(localFile); + composeFields.addAttachment(attachment); + + msgParams.type = Ci.nsIMsgCompType.New; + msgParams.format = Ci.nsIMsgCompFormat.Default; + msgParams.composeFields = composeFields; + + MailServices.compose.OpenComposeWindowWithParams(win, msgParams); + } catch (e) { + // Let protocol handlers try to take care. + openURI(cmdLine.resolveURI(uri)); + } + }); + } + } else { + getOrOpen3PaneWindow(); + } + }, + + /* nsICommandLineValidator */ + validate(cmdLine) { + var osintFlagIdx = cmdLine.findFlag("osint", false); + if (osintFlagIdx == -1) { + return; + } + + // Other handlers may use osint so only handle the osint flag if the mail + // or compose flag is also present and the command line is valid. + var mailFlagIdx = cmdLine.findFlag("mail", false); + var composeFlagIdx = cmdLine.findFlag("compose", false); + if (mailFlagIdx == -1 && composeFlagIdx == -1) { + return; + } + + // If both flags are present use the first flag found so the command line + // length test will fail. + if (mailFlagIdx > -1 && composeFlagIdx > -1) { + var actionFlagIdx = + mailFlagIdx > composeFlagIdx ? composeFlagIdx : mailFlagIdx; + } else { + actionFlagIdx = mailFlagIdx > -1 ? mailFlagIdx : composeFlagIdx; + } + + if (actionFlagIdx && osintFlagIdx > -1) { + var param = cmdLine.getArgument(actionFlagIdx + 1); + if ( + cmdLine.length != actionFlagIdx + 2 || + /thunderbird.url.(mailto|news):/.test(param) + ) { + throw Components.Exception("", Cr.NS_ERROR_ABORT); + } + cmdLine.handleFlag("osint", false); + } + }, + + openInExternal(uri) { + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .loadURI(uri); + }, + + handleContent(aContentType, aWindowContext, aRequest) { + try { + if ( + !Cc["@mozilla.org/webnavigation-info;1"] + .getService(Ci.nsIWebNavigationInfo) + .isTypeSupported(aContentType, null) + ) { + throw Components.Exception("", Cr.NS_ERROR_WONT_HANDLE_CONTENT); + } + } catch (e) { + throw Components.Exception("", Cr.NS_ERROR_WONT_HANDLE_CONTENT); + } + + aRequest.QueryInterface(Ci.nsIChannel); + + // For internal protocols (e.g. imap, mailbox, mailto), we want to handle + // them internally as we know what to do. For http and https we don't + // actually deal with external windows very well, so we redirect them to + // the external browser. + if (!aRequest.URI.schemeIs("http") && !aRequest.URI.schemeIs("https")) { + throw Components.Exception("", Cr.NS_ERROR_WONT_HANDLE_CONTENT); + } + + this.openInExternal(aRequest.URI); + aRequest.cancel(Cr.NS_BINDING_ABORTED); + }, + + helpInfo: + " -mail Go to the mail tab.\n" + + " -addressbook Go to the address book tab.\n" + + " -calendar Go to the calendar tab.\n" + + " -options Go to the settings tab.\n" + + " -file Open the specified email file or ICS calendar file.\n" + + " -setDefaultMail Set this app as the default mail client.\n" + + " -keymanager Open the OpenPGP Key Manager.\n", + + /* nsIFactory */ + + createInstance(iid) { + return this.QueryInterface(iid); + }, +}; + +function MessengerContentHandler() { + if (!gMessengerContentHandler) { + gMessengerContentHandler = this; + } + return gMessengerContentHandler; +} + +MessengerContentHandler.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIContentHandler"]), +}; + +var gMessengerContentHandler = new MailDefaultHandler(); + +/** + * Open a message/rfc822 or eml file in a new msg window. + * + * @implements {nsIContentHandler} + */ +class MessageDisplayContentHandler { + QueryInterface = ChromeUtils.generateQI(["nsIContentHandler"]); + + handleContent(contentType, windowContext, request) { + let channel = request.QueryInterface(Ci.nsIChannel); + if (!channel) { + throw Components.Exception( + "Expecting an nsIChannel", + Cr.NS_ERROR_ILLEGAL_VALUE + ); + } + let uri = channel.URI; + let mailnewsUrl; + try { + mailnewsUrl = uri.QueryInterface(Ci.nsIMsgMailNewsUrl); + } catch (e) {} + if (mailnewsUrl) { + let queryPart = mailnewsUrl.query.replace( + "type=message/rfc822", + "type=application/x-message-display" + ); + uri = mailnewsUrl.mutate().setQuery(queryPart).finalize(); + } else if (uri.scheme == "file") { + uri = uri + .mutate() + .setQuery("type=application/x-message-display") + .finalize(); + } + getOrOpen3PaneWindow().then(win => + Services.ww.openWindow( + win, + "chrome://messenger/content/messageWindow.xhtml", + "_blank", + "all,chrome,dialog=no,status,toolbar", + uri + ) + ); + } +} diff --git a/comm/mail/components/StartupRecorder.jsm b/comm/mail/components/StartupRecorder.jsm new file mode 100644 index 0000000000..f7443b6c57 --- /dev/null +++ b/comm/mail/components/StartupRecorder.jsm @@ -0,0 +1,229 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["StartupRecorder"]; + +const Cm = Components.manager; +Cm.QueryInterface(Ci.nsIServiceManager); + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +let firstPaintNotification = "widget-first-paint"; +// widget-first-paint fires much later than expected on Linux. +if (AppConstants.platform == "linux") { + firstPaintNotification = "xul-window-visible"; +} + +let win, canvas; +let paints = []; +let afterPaintListener = () => { + let width, height; + canvas.width = width = win.innerWidth; + canvas.height = height = win.innerHeight; + if (width < 1 || height < 1) { + return; + } + let ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true }); + + ctx.drawWindow( + win, + 0, + 0, + width, + height, + "white", + ctx.DRAWWINDOW_DO_NOT_FLUSH | + ctx.DRAWWINDOW_DRAW_VIEW | + ctx.DRAWWINDOW_ASYNC_DECODE_IMAGES | + ctx.DRAWWINDOW_USE_WIDGET_LAYERS + ); + paints.push({ + data: ctx.getImageData(0, 0, width, height).data, + width, + height, + }); +}; + +/** + * The StartupRecorder component observes notifications at various stages of + * startup and records the set of JS modules that were already loaded at + * each of these points. + * The records are meant to be used by startup tests in + * browser/base/content/test/performance + * This component only exists in nightly and debug builds, it doesn't ship in + * our release builds. + */ +function StartupRecorder() { + this.wrappedJSObject = this; + this.data = { + images: { + "image-drawing": new Set(), + "image-loading": new Set(), + }, + code: {}, + extras: {}, + prefStats: {}, + }; + this.done = new Promise(resolve => { + this._resolve = resolve; + }); +} +StartupRecorder.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + + record(name) { + ChromeUtils.addProfilerMarker("startupRecorder:" + name); + this.data.code[name] = { + modules: Cu.loadedJSModules.concat(Cu.loadedESModules), + services: Object.keys(Cc).filter(c => { + try { + return Cm.isServiceInstantiatedByContractID(c, Ci.nsISupports); + } catch (e) { + return false; + } + }), + }; + this.data.extras[name] = { + hiddenWindowLoaded: Services.appShell.hasHiddenWindow, + }; + }, + + observe(subject, topic, data) { + if (topic == "app-startup" || topic == "content-process-ready-for-script") { + // Don't do anything in xpcshell. + if (Services.appinfo.ID != "{3550f703-e582-4d05-9a08-453d09bdfdc6}") { + return; + } + + if ( + !Services.prefs.getBoolPref("browser.startup.record", false) && + !Services.prefs.getBoolPref("browser.startup.recordImages", false) + ) { + this._resolve(); + this._resolve = null; + return; + } + + // We can't ensure our observer will be called first or last, so the list of + // topics we observe here should avoid the topics used to trigger things + // during startup (eg. the topics observed by BrowserGlue.jsm). + let topics = [ + "profile-do-change", // This catches stuff loaded during app-startup + "toplevel-window-ready", // Catches stuff from final-ui-startup + firstPaintNotification, + "mail-startup-done", + "mail-startup-idle-tasks-finished", + ]; + + if (Services.prefs.getBoolPref("browser.startup.recordImages", false)) { + // For code simplicify, recording images excludes the other startup + // recorder behaviors, so we can observe only the image topics. + topics = [ + "image-loading", + "image-drawing", + "mail-startup-idle-tasks-finished", + ]; + } + for (let t of topics) { + Services.obs.addObserver(this, t); + } + return; + } + + // We only care about the first paint notification for browser windows, and + // not other types (for example, the gfx sanity test window) + if (topic == firstPaintNotification) { + // In the case we're handling xul-window-visible, we'll have been handed + // an nsIAppWindow instead of an nsIDOMWindow. + if (subject instanceof Ci.nsIAppWindow) { + subject = subject + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIDOMWindow); + } + + if ( + subject.document.documentElement.getAttribute("windowtype") != + "mail:3pane" + ) { + return; + } + } + + if (topic == "image-drawing" || topic == "image-loading") { + this.data.images[topic].add(data); + return; + } + + Services.obs.removeObserver(this, topic); + + if (topic == firstPaintNotification) { + // Because of the check for mail:3pane we made earlier, we know + // that if we got here, then the subject must be the first browser window. + win = subject; + canvas = win.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + canvas.mozOpaque = true; + afterPaintListener(); + win.addEventListener("MozAfterPaint", afterPaintListener); + } + + // TODO: Figure out what can replace this section. + if (topic == "mail-startup-done") { + // We use idleDispatchToMainThread here to record the set of + // loaded scripts after we are fully done with startup and ready + // to react to user events. + Services.tm.dispatchToMainThread( + this.record.bind(this, "before handling user events") + ); + } else if (topic == "mail-startup-idle-tasks-finished") { + if (Services.prefs.getBoolPref("browser.startup.recordImages", false)) { + Services.obs.removeObserver(this, "image-drawing"); + Services.obs.removeObserver(this, "image-loading"); + this._resolve(); + this._resolve = null; + return; + } + + this.record("before becoming idle"); + win.removeEventListener("MozAfterPaint", afterPaintListener); + win = null; + this.data.frames = paints; + this.data.prefStats = {}; + if (AppConstants.DEBUG) { + Services.prefs.readStats( + (key, value) => (this.data.prefStats[key] = value) + ); + } + paints = null; + + if (!Services.env.exists("MOZ_PROFILER_STARTUP_PERFORMANCE_TEST")) { + this._resolve(); + this._resolve = null; + return; + } + + Services.profiler.getProfileDataAsync().then(profileData => { + this.data.profile = profileData; + // There's no equivalent StartProfiler call in this file because the + // profiler is started using the MOZ_PROFILER_STARTUP environment + // variable in browser/base/content/test/performance/browser.ini + Services.profiler.StopProfiler(); + + this._resolve(); + this._resolve = null; + }); + } else { + const topicsToNames = { + "profile-do-change": "before profile selection", + "toplevel-window-ready": "before opening first browser window", + }; + topicsToNames[firstPaintNotification] = "before first paint"; + this.record(topicsToNames[topic]); + } + }, +}; diff --git a/comm/mail/components/about-support/AboutSupportMac.jsm b/comm/mail/components/about-support/AboutSupportMac.jsm new file mode 100644 index 0000000000..16f1c258f8 --- /dev/null +++ b/comm/mail/components/about-support/AboutSupportMac.jsm @@ -0,0 +1,16 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["AboutSupportPlatform"]; + +var AboutSupportPlatform = { + /** + * Given an nsIFile, gets the file system type. The type is returned as a + * string. Possible values are "network", "local", "unknown" and null. + */ + getFileSystemType(aFile) { + // Not implemented + return null; + }, +}; diff --git a/comm/mail/components/about-support/AboutSupportUnix.jsm b/comm/mail/components/about-support/AboutSupportUnix.jsm new file mode 100644 index 0000000000..a27b3c99c5 --- /dev/null +++ b/comm/mail/components/about-support/AboutSupportUnix.jsm @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["AboutSupportPlatform"]; + +// JS ctypes are needed to get at the data we need +var { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" +); +var GFile = ctypes.StructType("GFile"); +var GFileInfo = ctypes.StructType("GFileInfo"); +var GError = ctypes.StructType("GError"); +var GCancellable = ctypes.StructType("GCancellable"); + +var G_FILE_ATTRIBUTE_FILESYSTEM_TYPE = "filesystem::type"; + +var kNetworkFilesystems = ["afs", "cifs", "nfs", "smb"]; + +// These libraries might not be available on all systems. +var gLibsExist = false; +try { + // GC is responsible for closing these libraries if they exist. + var glib = ctypes.open("libglib-2.0.so.0"); + var gobject = ctypes.open("libgobject-2.0.so.0"); + var gio = ctypes.open("libgio-2.0.so.0"); + gLibsExist = true; +} catch (ex) {} + +if (gLibsExist) { + var g_free = glib.declare( + "g_free", + ctypes.default_abi, + ctypes.void_t, + ctypes.voidptr_t + ); + var g_object_unref = gobject.declare( + "g_object_unref", + ctypes.default_abi, + ctypes.void_t, + ctypes.voidptr_t + ); +} + +var AboutSupportPlatform = { + /** + * Given an nsIFile, gets the file system type. The type is returned as a + * string. Possible values are "network", "local", "unknown" and null. + */ + getFileSystemType(aFile) { + // Check if the libs exist. + if (!gLibsExist) { + return "unknown"; + } + + try { + // Given a UTF-8 string, converts it to the current Glib locale. + let g_filename_from_utf8 = glib.declare( + "g_filename_from_utf8", + ctypes.default_abi, + ctypes.char.ptr, // return type: glib locale string + ctypes.char.ptr, // in: utf8string + ctypes.ssize_t, // in: len + ctypes.size_t.ptr, // out: bytes_read + ctypes.size_t.ptr, // out: bytes_written + GError.ptr // out: error + ); + // Yes, we want function scoping for variables we need to free in the + // finally block. I think this is better than declaring lots of variables + // on top. + var filePath = g_filename_from_utf8(aFile.path, -1, null, null, null); + if (filePath.isNull()) { + throw new Error( + "Unable to convert " + aFile.path + " into GLib encoding" + ); + } + + // Given a path, creates a new GFile for it. + let g_file_new_for_path = gio.declare( + "g_file_new_for_path", + ctypes.default_abi, + GFile.ptr, // return type: a newly-allocated GFile + ctypes.char.ptr // in: path + ); + var glibFile = g_file_new_for_path(filePath); + + // Given a GFile, queries the given attributes and returns them + // as a GFileInfo. + let g_file_query_filesystem_info = gio.declare( + "g_file_query_filesystem_info", + ctypes.default_abi, + GFileInfo.ptr, // return type + GFile.ptr, // in: file + ctypes.char.ptr, // in: attributes + GCancellable.ptr, // in: cancellable + GError.ptr // out: error + ); + var glibFileInfo = g_file_query_filesystem_info( + glibFile, + G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, + null, + null + ); + if (glibFileInfo.isNull()) { + throw new Error("Unabled to retrieve GLib file info for " + aFile.path); + } + + let g_file_info_get_attribute_string = gio.declare( + "g_file_info_get_attribute_string", + ctypes.default_abi, + ctypes.char.ptr, // return type: file system type (do not free) + GFileInfo.ptr, // in: info + ctypes.char.ptr // in: attribute + ); + let fsType = g_file_info_get_attribute_string( + glibFileInfo, + G_FILE_ATTRIBUTE_FILESYSTEM_TYPE + ); + if (fsType.isNull()) { + return "unknown"; + } else if (kNetworkFilesystems.includes(fsType.readString())) { + return "network"; + } + return "local"; + } finally { + if (filePath) { + g_free(filePath); + } + if (glibFile && !glibFile.isNull()) { + g_object_unref(glibFile); + } + if (glibFileInfo && !glibFileInfo.isNull()) { + g_object_unref(glibFileInfo); + } + } + }, +}; diff --git a/comm/mail/components/about-support/AboutSupportWin32.jsm b/comm/mail/components/about-support/AboutSupportWin32.jsm new file mode 100644 index 0000000000..4c5e36c5bd --- /dev/null +++ b/comm/mail/components/about-support/AboutSupportWin32.jsm @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["AboutSupportPlatform"]; + +// JS ctypes are needed to get at the data we need +var { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" +); + +var BOOL = ctypes.int32_t; +var DRIVE_UNKNOWN = 0; +var DRIVE_NETWORK = 4; + +var AboutSupportPlatform = { + /** + * Given an nsIFile, gets the file system type. The type is returned as a + * string. Possible values are "network", "local", "unknown" and null. + */ + getFileSystemType(aFile) { + let kernel32 = ctypes.open("kernel32.dll"); + + try { + // Returns the path of the volume a file is on. + let GetVolumePathName = kernel32.declare( + "GetVolumePathNameW", + ctypes.winapi_abi, + BOOL, // return type: 1 indicates success, 0 failure + ctypes.char16_t.ptr, // in: lpszFileName + ctypes.char16_t.ptr, // out: lpszVolumePathName + ctypes.uint32_t // in: cchBufferLength + ); + + // Returns the last error. + let GetLastError = kernel32.declare( + "GetLastError", + ctypes.winapi_abi, + ctypes.uint32_t // return type: the last error + ); + + let filePath = aFile.path; + // The volume path should be at most 1 greater than than the length of the + // path -- add 1 for a trailing backslash if necessary, and 1 for the + // terminating null character. Note that the parentheses around the type are + // necessary for new to apply correctly. + let volumePath = new (ctypes.char16_t.array(filePath.length + 2))(); + + if (!GetVolumePathName(filePath, volumePath, volumePath.length)) { + throw new Error( + "Unable to get volume path for " + + filePath + + ", error " + + GetLastError() + ); + } + + // Returns the type of the drive. + let GetDriveType = kernel32.declare( + "GetDriveTypeW", + ctypes.winapi_abi, + ctypes.uint32_t, // return type: the drive type + ctypes.char16_t.ptr // in: lpRootPathName + ); + let type = GetDriveType(volumePath); + // http://msdn.microsoft.com/en-us/library/aa364939 + if (type == DRIVE_UNKNOWN) { + return "unknown"; + } else if (type == DRIVE_NETWORK) { + return "network"; + } + return "local"; + } finally { + kernel32.close(); + } + }, +}; diff --git a/comm/mail/components/about-support/content/aboutSupport.js b/comm/mail/components/about-support/content/aboutSupport.js new file mode 100644 index 0000000000..fc73b59029 --- /dev/null +++ b/comm/mail/components/about-support/content/aboutSupport.js @@ -0,0 +1,1729 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* This file is a copy of mozilla/toolkit/content/aboutSupport.js with + modifications for TB. */ + +/* globals AboutSupportPlatform, populateAccountsSection, sendViaEmail + populateCalendarsSection, populateChatSection, populateLibrarySection */ + +"use strict"; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { Troubleshoot } = ChromeUtils.importESModule( + "resource://gre/modules/Troubleshoot.sys.mjs" +); +var { ResetProfile } = ChromeUtils.importESModule( + "resource://gre/modules/ResetProfile.sys.mjs" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs", + PlacesDBUtils: "resource://gre/modules/PlacesDBUtils.sys.mjs", + PluralForm: "resource://gre/modules/PluralForm.sys.mjs", + ProcessType: "resource://gre/modules/ProcessType.sys.mjs", +}); + +// added for TB +/* Node classes. All of these are mutually exclusive. */ + +// Any nodes marked with this class will be considered part of the UI only, +// and therefore will not be copied. +var CLASS_DATA_UIONLY = "data-uionly"; + +// Any nodes marked with this class will be considered private and will be +// hidden if the user requests only public data to be shown or copied. +var CLASS_DATA_PRIVATE = "data-private"; + +// Any nodes marked with this class will only be displayed when the user chooses +// to not display private data. +var CLASS_DATA_PUBLIC = "data-public"; +// end of TB addition +window.addEventListener("load", function onload(event) { + try { + window.removeEventListener("load", onload); + Troubleshoot.snapshot().then(async snapshot => { + for (let prop in snapshotFormatters) { + try { + await snapshotFormatters[prop](snapshot[prop]); + } catch (e) { + console.error( + "stack of snapshot error for about:support: ", + e, + ": ", + e.stack + ); + } + } + }, console.error); + populateActionBox(); + setupEventListeners(); + + let hasWinPackageId = false; + try { + hasWinPackageId = Services.sysinfo.getProperty("hasWinPackageId"); + } catch (_ex) { + // The hasWinPackageId property doesn't exist; assume it would be false. + } + if (hasWinPackageId) { + $("update-dir-row").hidden = true; + $("update-history-row").hidden = true; + } + } catch (e) { + console.error( + "stack of load error for about:support: " + e + ": " + e.stack + ); + } + // added for TB + populateAccountsSection(); + populateCalendarsSection(); + populateChatSection(); + populateLibrarySection(); + document + .getElementById("check-show-private-data") + .addEventListener("change", () => onShowPrivateDataChange()); +}); + +function prefsTable(data) { + return sortedArrayFromObject(data).map(function ([name, value]) { + return $.new("tr", [ + $.new("td", name, "pref-name"), + // Very long preference values can cause users problems when they + // copy and paste them into some text editors. Long values generally + // aren't useful anyway, so truncate them to a reasonable length. + $.new("td", String(value).substr(0, 120), "pref-value"), + ]); + }); +} + +// Fluent uses lisp-case IDs so this converts +// the SentenceCase info IDs to lisp-case. +const FLUENT_IDENT_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]*$/; +function toFluentID(str) { + if (!FLUENT_IDENT_REGEX.test(str)) { + return null; + } + return str + .toString() + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .toLowerCase(); +} + +// Each property in this object corresponds to a property in Troubleshoot.jsm's +// snapshot data. Each function is passed its property's corresponding data, +// and it's the function's job to update the page with it. +var snapshotFormatters = { + async application(data) { + $("application-box").textContent = data.name; + $("useragent-box").textContent = data.userAgent; + $("os-box").textContent = data.osVersion; + if (data.osTheme) { + $("os-theme-box").textContent = data.osTheme; + } else { + $("os-theme-row").hidden = true; + } + if (AppConstants.platform == "macosx") { + $("rosetta-box").textContent = data.rosetta; + } + $("binary-box").textContent = Services.dirsvc.get( + "XREExeF", + Ci.nsIFile + ).path; + $("supportLink").href = data.supportURL; + let version = AppConstants.MOZ_APP_VERSION_DISPLAY; + if (data.vendor) { + version += " (" + data.vendor + ")"; + } + $("version-box").textContent = version; + $("buildid-box").textContent = data.buildID; + $("distributionid-box").textContent = data.distributionID; + if (data.updateChannel) { + $("updatechannel-box").textContent = data.updateChannel; + } + if (AppConstants.MOZ_UPDATER) { + $("update-dir-box").textContent = Services.dirsvc.get( + "UpdRootD", + Ci.nsIFile + ).path; + } + + try { + let launcherStatusTextId = "launcher-process-status-unknown"; + switch (data.launcherProcessState) { + case 0: + case 1: + case 2: + launcherStatusTextId = + "launcher-process-status-" + data.launcherProcessState; + break; + } + + document.l10n.setAttributes( + $("launcher-process-box"), + launcherStatusTextId + ); + } catch (e) {} + + const STATUS_STRINGS = { + experimentControl: "fission-status-experiment-control", + experimentTreatment: "fission-status-experiment-treatment", + disabledByE10sEnv: "fission-status-disabled-by-e10s-env", + enabledByEnv: "fission-status-enabled-by-env", + enabledByDefault: "fission-status-enabled-by-default", + disabledByDefault: "fission-status-disabled-by-default", + enabledByUserPref: "fission-status-enabled-by-user-pref", + disabledByUserPref: "fission-status-disabled-by-user-pref", + disabledByE10sOther: "fission-status-disabled-by-e10s-other", + }; + + let statusTextId = STATUS_STRINGS[data.fissionDecisionStatus]; + + document.l10n.setAttributes( + $("multiprocess-box-process-count"), + "multi-process-windows", + { + remoteWindows: data.numRemoteWindows, + totalWindows: data.numTotalWindows, + } + ); + document.l10n.setAttributes( + $("fission-box-process-count"), + "fission-windows", + { + fissionWindows: data.numFissionWindows, + totalWindows: data.numTotalWindows, + } + ); + document.l10n.setAttributes($("fission-box-status"), statusTextId); + + if (Services.policies) { + let policiesStrId = ""; + let aboutPolicies = "about:policies"; + switch (data.policiesStatus) { + case Services.policies.INACTIVE: + policiesStrId = "policies-inactive"; + break; + + case Services.policies.ACTIVE: + policiesStrId = "policies-active"; + aboutPolicies += "#active"; + break; + + default: + policiesStrId = "policies-error"; + aboutPolicies += "#errors"; + break; + } + + if (data.policiesStatus != Services.policies.INACTIVE) { + let activePolicies = $.new("a", null, null, { + href: aboutPolicies, + }); + document.l10n.setAttributes(activePolicies, policiesStrId); + $("policies-status").appendChild(activePolicies); + } else { + document.l10n.setAttributes($("policies-status"), policiesStrId); + } + } else { + $("policies-status-row").hidden = true; + } + + let keyLocationServiceGoogleFound = data.keyLocationServiceGoogleFound + ? "found" + : "missing"; + document.l10n.setAttributes( + $("key-location-service-google-box"), + keyLocationServiceGoogleFound + ); + + let keySafebrowsingGoogleFound = data.keySafebrowsingGoogleFound + ? "found" + : "missing"; + document.l10n.setAttributes( + $("key-safebrowsing-google-box"), + keySafebrowsingGoogleFound + ); + + let keyMozillaFound = data.keyMozillaFound ? "found" : "missing"; + document.l10n.setAttributes($("key-mozilla-box"), keyMozillaFound); + + $("safemode-box").textContent = data.safeMode; + + const formatHumanReadableBytes = (elem, bytes) => { + let size = DownloadUtils.convertByteUnits(bytes); + document.l10n.setAttributes(elem, "app-basics-data-size", { + value: size[0], + unit: size[1], + }); + }; + + formatHumanReadableBytes($("memory-size-box"), data.memorySizeBytes); + formatHumanReadableBytes($("disk-available-box"), data.diskAvailableBytes); + + // added for TB + // Add profile path as private info into the page. + let currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile); + let profElem = document.getElementById("profile-dir-button").parentNode; + let profDirNode = document.getElementById("profile-dir-box"); + profDirNode.setAttribute("class", CLASS_DATA_PRIVATE); + let profLinkNode = document.createElement("a"); + profLinkNode.setAttribute("href", Services.io.newFileURI(currProfD).spec); + profLinkNode.addEventListener("click", function (event) { + openProfileDirectory(); + event.preventDefault(); + }); + let profPathNode = document.createTextNode(currProfD.path); + profLinkNode.appendChild(profPathNode); + profDirNode.appendChild(profLinkNode); + profElem.appendChild(document.createTextNode(" ")); + + // Show type of filesystem detected. + let fsType; + try { + fsType = AboutSupportPlatform.getFileSystemType(currProfD); + if (fsType) { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/aboutSupportMail.properties" + ); + let fsText = bundle.GetStringFromName("fsType." + fsType); + let fsTextNode = document.createElement("span"); + fsTextNode.textContent = fsText; + profElem.appendChild(fsTextNode); + } + } catch (x) { + console.error(x); + } + // end of TB addition + }, + + crashes(data) { + if (!AppConstants.MOZ_CRASHREPORTER) { + return; + } + + let daysRange = Troubleshoot.kMaxCrashAge / (24 * 60 * 60 * 1000); + document.l10n.setAttributes($("crashes-title"), "report-crash-for-days", { + days: daysRange, + }); + let reportURL; + try { + reportURL = Services.prefs.getCharPref("breakpad.reportURL"); + // Ignore any non http/https urls + if (!/^https?:/i.test(reportURL)) { + reportURL = null; + } + } catch (e) {} + if (!reportURL) { + $("crashes-noConfig").style.display = "block"; + $("crashes-noConfig").classList.remove("no-copy"); + return; + } + $("crashes-allReports").style.display = "block"; + + if (data.pending > 0) { + document.l10n.setAttributes( + $("crashes-allReportsWithPending"), + "pending-reports", + { reports: data.pending } + ); + } + + let dateNow = new Date(); + $.append( + $("crashes-tbody"), + data.submitted.map(function (crash) { + let date = new Date(crash.date); + let timePassed = dateNow - date; + let formattedDateStrId; + let formattedDateStrArgs; + if (timePassed >= 24 * 60 * 60 * 1000) { + let daysPassed = Math.round(timePassed / (24 * 60 * 60 * 1000)); + formattedDateStrId = "crashes-time-days"; + formattedDateStrArgs = { days: daysPassed }; + } else if (timePassed >= 60 * 60 * 1000) { + let hoursPassed = Math.round(timePassed / (60 * 60 * 1000)); + formattedDateStrId = "crashes-time-hours"; + formattedDateStrArgs = { hours: hoursPassed }; + } else { + let minutesPassed = Math.max(Math.round(timePassed / (60 * 1000)), 1); + formattedDateStrId = "crashes-time-minutes"; + formattedDateStrArgs = { minutes: minutesPassed }; + } + return $.new("tr", [ + $.new("td", [ + $.new("a", crash.id, null, { href: reportURL + crash.id }), + ]), + $.new("td", null, null, { + "data-l10n-id": formattedDateStrId, + "data-l10n-args": formattedDateStrArgs, + }), + ]); + }) + ); + }, + + addons(data) { + $.append( + $("addons-tbody"), + data.map(function (addon) { + return $.new("tr", [ + $.new("td", addon.name), + $.new("td", addon.type), + $.new("td", addon.version), + $.new("td", addon.isActive), + $.new("td", addon.id), + ]); + }) + ); + }, + + securitySoftware(data) { + if (!AppConstants.isPlatformAndVersionAtLeast("win", "6.2")) { + $("security-software-title").hidden = true; + $("security-software-table").hidden = true; + return; + } + + $("security-software-antivirus").textContent = data.registeredAntiVirus; + $("security-software-antispyware").textContent = data.registeredAntiSpyware; + $("security-software-firewall").textContent = data.registeredFirewall; + }, + + /* Not used by TB + features(data) { + $.append($("features-tbody"), data.map(function(feature) { + return $.new("tr", [ + $.new("td", feature.name), + $.new("td", feature.version), + $.new("td", feature.id), + ]); + })); + }, +*/ + + async processes(data) { + async function buildEntry(name, value) { + const fluentName = ProcessType.fluentNameFromProcessTypeString(name); + let entryName = (await document.l10n.formatValue(fluentName)) || name; + $("processes-tbody").appendChild( + $.new("tr", [$.new("td", entryName), $.new("td", value)]) + ); + } + + let remoteProcessesCount = Object.values(data.remoteTypes).reduce( + (a, b) => a + b, + 0 + ); + document.querySelector("#remoteprocesses-row a").textContent = + remoteProcessesCount; + + // Display the regular "web" process type first in the list, + // and with special formatting. + if (data.remoteTypes.web) { + await buildEntry( + "web", + `${data.remoteTypes.web} / ${data.maxWebContentProcesses}` + ); + delete data.remoteTypes.web; + } + + for (let remoteProcessType in data.remoteTypes) { + await buildEntry(remoteProcessType, data.remoteTypes[remoteProcessType]); + } + }, + + environmentVariables(data) { + if (!data) { + return; + } + $.append( + $("environment-variables-tbody"), + Object.entries(data).map(([name, value]) => { + return $.new("tr", [ + $.new("td", name, "pref-name"), + $.new("td", value, "pref-value"), + ]); + }) + ); + }, + + modifiedPreferences(data) { + $.append($("prefs-tbody"), prefsTable(data)); + }, + + lockedPreferences(data) { + $.append($("locked-prefs-tbody"), prefsTable(data)); + }, + + printingPreferences(data) { + if (AppConstants.platform == "android") { + return; + } + const tbody = $("support-printing-prefs-tbody"); + $.append(tbody, prefsTable(data)); + $("support-printing-clear-settings-button").addEventListener( + "click", + function () { + for (let name in data) { + Services.prefs.clearUserPref(name); + } + tbody.textContent = ""; + } + ); + }, + + /* eslint-disable complexity */ + async graphics(data) { + function localizedMsg(msg) { + if (typeof msg == "object" && msg.key) { + return document.l10n.formatValue(msg.key, msg.args); + } + let msgId = toFluentID(msg); + if (msgId) { + return document.l10n.formatValue(msgId); + } + return ""; + } + + // Read APZ info out of data.info, stripping it out in the process. + let apzInfo = []; + let formatApzInfo = function (info) { + let out = []; + for (let type of [ + "Wheel", + "Touch", + "Drag", + "Keyboard", + "Autoscroll", + "Zooming", + ]) { + let key = "Apz" + type + "Input"; + + if (!(key in info)) { + continue; + } + + delete info[key]; + + out.push(toFluentID(type.toLowerCase() + "Enabled")); + } + + return out; + }; + + // Create a element with key and value columns. + // + // @key Text in the key column. Localized automatically, unless starts with "#". + // @value Fluent ID for text in the value column, or array of children. + function buildRow(key, value) { + let title = key[0] == "#" ? key.substr(1) : key; + let keyStrId = toFluentID(key); + let valueStrId = Array.isArray(value) ? null : toFluentID(value); + let td = $.new("td", value); + td.style["white-space"] = "pre-wrap"; + if (valueStrId) { + document.l10n.setAttributes(td, valueStrId); + } + + let th = $.new("th", title, "column"); + if (!key.startsWith("#")) { + document.l10n.setAttributes(th, keyStrId); + } + return $.new("tr", [th, td]); + } + + // @where The name in "graphics--tbody", of the element to append to. + // @trs Array of row elements. + function addRows(where, trs) { + $.append($("graphics-" + where + "-tbody"), trs); + } + + // Build and append a row. + // + // @where The name in "graphics--tbody", of the element to append to. + function addRow(where, key, value) { + addRows(where, [buildRow(key, value)]); + } + if ("info" in data) { + apzInfo = formatApzInfo(data.info); + + let trs = sortedArrayFromObject(data.info).map(function ([prop, val]) { + let td = $.new("td", String(val)); + td.style["word-break"] = "break-all"; + return $.new("tr", [$.new("th", prop, "column"), td]); + }); + addRows("diagnostics", trs); + + delete data.info; + } + + let windowUtils = window.windowUtils; + let gpuProcessPid = windowUtils.gpuProcessPid; + + if (gpuProcessPid != -1) { + let gpuProcessKillButton = null; + if (AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_DEV_EDITION) { + gpuProcessKillButton = $.new("button"); + + gpuProcessKillButton.addEventListener("click", function () { + windowUtils.terminateGPUProcess(); + }); + + document.l10n.setAttributes( + gpuProcessKillButton, + "gpu-process-kill-button" + ); + } + + addRow("diagnostics", "gpu-process-pid", [new Text(gpuProcessPid)]); + if (gpuProcessKillButton) { + addRow("diagnostics", "gpu-process", [gpuProcessKillButton]); + } + } + + if ( + (AppConstants.NIGHTLY_BUILD || AppConstants.MOZ_DEV_EDITION) && + AppConstants.platform != "macosx" + ) { + let gpuDeviceResetButton = $.new("button"); + + gpuDeviceResetButton.addEventListener("click", function () { + windowUtils.triggerDeviceReset(); + }); + + document.l10n.setAttributes( + gpuDeviceResetButton, + "gpu-device-reset-button" + ); + addRow("diagnostics", "gpu-device-reset", [gpuDeviceResetButton]); + } + + // graphics-failures-tbody tbody + if ("failures" in data) { + // If indices is there, it should be the same length as failures, + // (see Troubleshoot.jsm) but we check anyway: + if ("indices" in data && data.failures.length == data.indices.length) { + let combined = []; + for (let i = 0; i < data.failures.length; i++) { + let assembled = assembleFromGraphicsFailure(i, data); + combined.push(assembled); + } + combined.sort(function (a, b) { + if (a.index < b.index) { + return -1; + } + if (a.index > b.index) { + return 1; + } + return 0; + }); + $.append( + $("graphics-failures-tbody"), + combined.map(function (val) { + return $.new("tr", [ + $.new("th", val.header, "column"), + $.new("td", val.message), + ]); + }) + ); + delete data.indices; + } else { + $.append($("graphics-failures-tbody"), [ + $.new("tr", [ + $.new("th", "LogFailure", "column"), + $.new( + "td", + data.failures.map(function (val) { + return $.new("p", val); + }) + ), + ]), + ]); + } + delete data.failures; + } else { + $("graphics-failures-tbody").style.display = "none"; + } + + // Add a new row to the table, and take the key (or keys) out of data. + // + // @where Table section to add to. + // @key Data key to use. + // @colKey The localization key to use, if different from key. + async function addRowFromKey(where, key, colKey) { + if (!(key in data)) { + return; + } + colKey = colKey || key; + + let value; + let messageKey = key + "Message"; + if (messageKey in data) { + value = await localizedMsg(data[messageKey]); + delete data[messageKey]; + } else { + value = data[key]; + } + delete data[key]; + + if (value) { + addRow(where, colKey, [new Text(value)]); + } + } + + // graphics-features-tbody + let compositor = ""; + if (data.windowLayerManagerRemote) { + compositor = data.windowLayerManagerType; + } else { + let noOMTCString = await document.l10n.formatValue("main-thread-no-omtc"); + compositor = "BasicLayers (" + noOMTCString + ")"; + } + addRow("features", "compositing", [new Text(compositor)]); + delete data.windowLayerManagerRemote; + delete data.windowLayerManagerType; + delete data.numTotalWindows; + delete data.numAcceleratedWindows; + delete data.numAcceleratedWindowsMessage; + + addRow( + "features", + "asyncPanZoom", + apzInfo.length + ? [ + new Text( + ( + await document.l10n.formatValues( + apzInfo.map(id => { + return { id }; + }) + ) + ).join("; ") + ), + ] + : "apz-none" + ); + let featureKeys = [ + "webgl1WSIInfo", + "webgl1Renderer", + "webgl1Version", + "webgl1DriverExtensions", + "webgl1Extensions", + "webgl2WSIInfo", + "webgl2Renderer", + "webgl2Version", + "webgl2DriverExtensions", + "webgl2Extensions", + ["supportsHardwareH264", "hardware-h264"], + ["direct2DEnabled", "#Direct2D"], + ["windowProtocol", "graphics-window-protocol"], + ["desktopEnvironment", "graphics-desktop-environment"], + "usesTiling", + "targetFrameRate", + ]; + for (let feature of featureKeys) { + if (Array.isArray(feature)) { + await addRowFromKey("features", feature[0], feature[1]); + continue; + } + await addRowFromKey("features", feature); + } + + if ("directWriteEnabled" in data) { + let message = data.directWriteEnabled; + if ("directWriteVersion" in data) { + message += " (" + data.directWriteVersion + ")"; + } + await addRow("features", "#DirectWrite", [new Text(message)]); + delete data.directWriteEnabled; + delete data.directWriteVersion; + } + + // Adapter tbodies. + let adapterKeys = [ + ["adapterDescription", "gpu-description"], + ["adapterVendorID", "gpu-vendor-id"], + ["adapterDeviceID", "gpu-device-id"], + ["driverVendor", "gpu-driver-vendor"], + ["driverVersion", "gpu-driver-version"], + ["driverDate", "gpu-driver-date"], + ["adapterDrivers", "gpu-drivers"], + ["adapterSubsysID", "gpu-subsys-id"], + ["adapterRAM", "gpu-ram"], + ]; + + function showGpu(id, suffix) { + function get(prop) { + return data[prop + suffix]; + } + + let trs = []; + for (let [prop, key] of adapterKeys) { + let value = get(prop); + if (value === undefined || value === "") { + continue; + } + trs.push(buildRow(key, [new Text(value)])); + } + + if (trs.length == 0) { + $("graphics-" + id + "-tbody").style.display = "none"; + return; + } + + let active = "yes"; + if ("isGPU2Active" in data && (suffix == "2") != data.isGPU2Active) { + active = "no"; + } + + addRow(id, "gpu-active", active); + addRows(id, trs); + } + showGpu("gpu-1", ""); + showGpu("gpu-2", "2"); + + // Remove adapter keys. + for (let [prop /* key */] of adapterKeys) { + delete data[prop]; + delete data[prop + "2"]; + } + delete data.isGPU2Active; + + let featureLog = data.featureLog; + delete data.featureLog; + + if (featureLog.features.length) { + for (let feature of featureLog.features) { + let trs = []; + for (let entry of feature.log) { + let contents; + if (!entry.hasOwnProperty("message")) { + // This is a default entry. + contents = entry.status + " by " + entry.type; + } else if (entry.message.length && entry.message[0] == "#") { + // This is a failure ID. See nsIGfxInfo.idl. + let m = /#BLOCKLIST_FEATURE_FAILURE_BUG_(\d+)/.exec(entry.message); + if (m) { + let bugSpan = $.new("span"); + + let bugHref = $.new("a"); + bugHref.href = + "https://bugzilla.mozilla.org/show_bug.cgi?id=" + m[1]; + bugHref.setAttribute("data-l10n-name", "bug-link"); + bugSpan.append(bugHref); + document.l10n.setAttributes(bugSpan, "support-blocklisted-bug", { + bugNumber: m[1], + }); + + contents = [bugSpan]; + } else { + let unknownFailure = $.new("span"); + document.l10n.setAttributes(unknownFailure, "unknown-failure", { + failureCode: entry.message.substr(1), + }); + contents = [unknownFailure]; + } + } else { + contents = + entry.status + " by " + entry.type + ": " + entry.message; + } + + trs.push($.new("tr", [$.new("td", contents)])); + } + addRow("decisions", "#" + feature.name, [$.new("table", trs)]); + } + } else { + $("graphics-decisions-tbody").style.display = "none"; + } + + if (featureLog.fallbacks.length) { + for (let fallback of featureLog.fallbacks) { + addRow("workarounds", "#" + fallback.name, [ + new Text(fallback.message), + ]); + } + } else { + $("graphics-workarounds-tbody").style.display = "none"; + } + + let crashGuards = data.crashGuards; + delete data.crashGuards; + + if (crashGuards.length) { + for (let guard of crashGuards) { + let resetButton = $.new("button"); + let onClickReset = function () { + Services.prefs.setIntPref(guard.prefName, 0); + resetButton.removeEventListener("click", onClickReset); + resetButton.disabled = true; + }; + + document.l10n.setAttributes(resetButton, "reset-on-next-restart"); + resetButton.addEventListener("click", onClickReset); + + addRow("crashguards", guard.type + "CrashGuard", [resetButton]); + } + } else { + $("graphics-crashguards-tbody").style.display = "none"; + } + + // Now that we're done, grab any remaining keys in data and drop them into + // the diagnostics section. + for (let key in data) { + let value = data[key]; + addRow("diagnostics", key, [new Text(value)]); + } + }, + /* eslint-enable complexity */ + + media(data) { + function insertBasicInfo(key, value) { + function createRow(key, value) { + let th = $.new("th", null, "column"); + document.l10n.setAttributes(th, key); + let td = $.new("td", value); + td.style["white-space"] = "pre-wrap"; + td.colSpan = 8; + return $.new("tr", [th, td]); + } + $.append($("media-info-tbody"), [createRow(key, value)]); + } + + function createDeviceInfoRow(device) { + let deviceInfo = Ci.nsIAudioDeviceInfo; + + let states = {}; + states[deviceInfo.STATE_DISABLED] = "Disabled"; + states[deviceInfo.STATE_UNPLUGGED] = "Unplugged"; + states[deviceInfo.STATE_ENABLED] = "Enabled"; + + let preferreds = {}; + preferreds[deviceInfo.PREF_NONE] = "None"; + preferreds[deviceInfo.PREF_MULTIMEDIA] = "Multimedia"; + preferreds[deviceInfo.PREF_VOICE] = "Voice"; + preferreds[deviceInfo.PREF_NOTIFICATION] = "Notification"; + preferreds[deviceInfo.PREF_ALL] = "All"; + + let formats = {}; + formats[deviceInfo.FMT_S16LE] = "S16LE"; + formats[deviceInfo.FMT_S16BE] = "S16BE"; + formats[deviceInfo.FMT_F32LE] = "F32LE"; + formats[deviceInfo.FMT_F32BE] = "F32BE"; + + function toPreferredString(preferred) { + if (preferred == deviceInfo.PREF_NONE) { + return preferreds[deviceInfo.PREF_NONE]; + } else if (preferred & deviceInfo.PREF_ALL) { + return preferreds[deviceInfo.PREF_ALL]; + } + let str = ""; + for (let pref of [ + deviceInfo.PREF_MULTIMEDIA, + deviceInfo.PREF_VOICE, + deviceInfo.PREF_NOTIFICATION, + ]) { + if (preferred & pref) { + str += " " + preferreds[pref]; + } + } + return str; + } + + function toFromatString(dev) { + let str = "default: " + formats[dev.defaultFormat] + ", support:"; + for (let fmt of [ + deviceInfo.FMT_S16LE, + deviceInfo.FMT_S16BE, + deviceInfo.FMT_F32LE, + deviceInfo.FMT_F32BE, + ]) { + if (dev.supportedFormat & fmt) { + str += " " + formats[fmt]; + } + } + return str; + } + + function toRateString(dev) { + return ( + "default: " + + dev.defaultRate + + ", support: " + + dev.minRate + + " - " + + dev.maxRate + ); + } + + function toLatencyString(dev) { + return dev.minLatency + " - " + dev.maxLatency; + } + + return $.new("tr", [ + $.new("td", device.name), + $.new("td", device.groupId), + $.new("td", device.vendor), + $.new("td", states[device.state]), + $.new("td", toPreferredString(device.preferred)), + $.new("td", toFromatString(device)), + $.new("td", device.maxChannels), + $.new("td", toRateString(device)), + $.new("td", toLatencyString(device)), + ]); + } + + function insertDeviceInfo(side, devices) { + let rows = []; + for (let dev of devices) { + rows.push(createDeviceInfoRow(dev)); + } + $.append($("media-" + side + "-devices-tbody"), rows); + } + + function insertEnumerateDatabase() { + if ( + !Services.prefs.getBoolPref("media.mediacapabilities.from-database") + ) { + $("media-capabilities-tbody").style.display = "none"; + return; + } + let button = $("enumerate-database-button"); + if (button) { + button.addEventListener("click", function (event) { + let { KeyValueService } = ChromeUtils.importESModule( + "resource://gre/modules/kvstore.sys.mjs" + ); + let currProfDir = Services.dirsvc.get("ProfD", Ci.nsIFile); + currProfDir.append("mediacapabilities"); + let path = currProfDir.path; + + function enumerateDatabase(name) { + KeyValueService.getOrCreate(path, name) + .then(database => { + return database.enumerate(); + }) + .then(enumerator => { + var logs = []; + logs.push(`${name}:`); + for (let { key, value } of enumerator) { + logs.push(`${key}: ${value}`); + } + $("enumerate-database-result").textContent += + logs.join("\n") + "\n"; + }) + .catch(err => { + $("enumerate-database-result").textContent += `${name}:\n`; + }); + } + + $("enumerate-database-result").style.display = "block"; + $("enumerate-database-result").classList.remove("no-copy"); + $("enumerate-database-result").textContent = ""; + + enumerateDatabase("video/av1"); + enumerateDatabase("video/vp8"); + enumerateDatabase("video/vp9"); + enumerateDatabase("video/avc"); + enumerateDatabase("video/theora"); + }); + } + } + + function roundtripAudioLatency() { + insertBasicInfo("roundtrip-latency", "..."); + window.windowUtils + .defaultDevicesRoundTripLatency() + .then(latency => { + var latencyString = `${(latency[0] * 1000).toFixed(2)}ms (${( + latency[1] * 1000 + ).toFixed(2)})`; + data.defaultDevicesRoundTripLatency = latencyString; + document.querySelector( + 'th[data-l10n-id="roundtrip-latency"]' + ).nextSibling.textContent = latencyString; + }) + .catch(e => {}); + } + + // Basic information + insertBasicInfo("audio-backend", data.currentAudioBackend); + insertBasicInfo("max-audio-channels", data.currentMaxAudioChannels); + insertBasicInfo("sample-rate", data.currentPreferredSampleRate); + + if (AppConstants.platform == "macosx") { + var micStatus = {}; + let permission = Cc["@mozilla.org/ospermissionrequest;1"].getService( + Ci.nsIOSPermissionRequest + ); + permission.getAudioCapturePermissionState(micStatus); + if (micStatus.value == permission.PERMISSION_STATE_AUTHORIZED) { + roundtripAudioLatency(); + } + } else { + roundtripAudioLatency(); + } + + // Output devices information + insertDeviceInfo("output", data.audioOutputDevices); + + // Input devices information + insertDeviceInfo("input", data.audioInputDevices); + + // Media Capabilitites + insertEnumerateDatabase(); + }, + + remoteAgent(data) { + if (!AppConstants.ENABLE_WEBDRIVER) { + return; + } + $("remote-debugging-accepting-connections").textContent = data.listening; + $("remote-debugging-url").textContent = data.url; + }, + + accessibility(data) { + $("a11y-activated").textContent = data.isActive; + $("a11y-force-disabled").textContent = data.forceDisabled || 0; + + let a11yHandlerUsed = $("a11y-handler-used"); + if (a11yHandlerUsed) { + a11yHandlerUsed.textContent = data.handlerUsed; + } + + let a11yInstantiator = $("a11y-instantiator"); + if (a11yInstantiator) { + a11yInstantiator.textContent = data.instantiator; + } + }, + + startupCache(data) { + $("startup-cache-disk-cache-path").textContent = data.DiskCachePath; + $("startup-cache-ignore-disk-cache").textContent = data.IgnoreDiskCache; + $("startup-cache-found-disk-cache-on-init").textContent = + data.FoundDiskCacheOnInit; + $("startup-cache-wrote-to-disk-cache").textContent = data.WroteToDiskCache; + }, + + libraryVersions(data) { + let trs = [ + $.new("tr", [ + $.new("th", ""), + $.new("th", null, null, { "data-l10n-id": "min-lib-versions" }), + $.new("th", null, null, { "data-l10n-id": "loaded-lib-versions" }), + ]), + ]; + sortedArrayFromObject(data).forEach(function ([name, val]) { + trs.push( + $.new("tr", [ + $.new("td", name), + $.new("td", val.minVersion), + $.new("td", val.version), + ]) + ); + }); + $.append($("libversions-tbody"), trs); + }, + + userJS(data) { + if (!data.exists) { + return; + } + let userJSFile = Services.dirsvc.get("PrefD", Ci.nsIFile); + userJSFile.append("user.js"); + $("prefs-user-js-link").href = Services.io.newFileURI(userJSFile).spec; + $("prefs-user-js-section").style.display = ""; + // Clear the no-copy class + $("prefs-user-js-section").className = ""; + }, + + sandbox(data) { + if (!AppConstants.MOZ_SANDBOX) { + return; + } + + let tbody = $("sandbox-tbody"); + for (let key in data) { + // Simplify the display a little in the common case. + if ( + key === "hasPrivilegedUserNamespaces" && + data[key] === data.hasUserNamespaces + ) { + continue; + } + if (key === "syscallLog") { + // Not in this table. + continue; + } + let keyStrId = toFluentID(key); + let th = $.new("th", null, "column"); + document.l10n.setAttributes(th, keyStrId); + tbody.appendChild($.new("tr", [th, $.new("td", data[key])])); + } + + if ("syscallLog" in data) { + let syscallBody = $("sandbox-syscalls-tbody"); + let argsHead = $("sandbox-syscalls-argshead"); + for (let syscall of data.syscallLog) { + if (argsHead.colSpan < syscall.args.length) { + argsHead.colSpan = syscall.args.length; + } + let procTypeStrId = toFluentID(syscall.procType); + let cells = [ + $.new("td", syscall.index, "integer"), + $.new("td", syscall.msecAgo / 1000), + $.new("td", syscall.pid, "integer"), + $.new("td", syscall.tid, "integer"), + $.new("td", null, null, { + "data-l10n-id": "sandbox-proc-type-" + procTypeStrId, + }), + $.new("td", syscall.syscall, "integer"), + ]; + for (let arg of syscall.args) { + cells.push($.new("td", arg, "integer")); + } + syscallBody.appendChild($.new("tr", cells)); + } + } + }, + + intl(data) { + $("intl-locale-requested").textContent = JSON.stringify( + data.localeService.requested + ); + $("intl-locale-available").textContent = JSON.stringify( + data.localeService.available + ); + $("intl-locale-supported").textContent = JSON.stringify( + data.localeService.supported + ); + $("intl-locale-regionalprefs").textContent = JSON.stringify( + data.localeService.regionalPrefs + ); + $("intl-locale-default").textContent = JSON.stringify( + data.localeService.defaultLocale + ); + + $("intl-osprefs-systemlocales").textContent = JSON.stringify( + data.osPrefs.systemLocales + ); + $("intl-osprefs-regionalprefs").textContent = JSON.stringify( + data.osPrefs.regionalPrefsLocales + ); + }, +}; + +var $ = document.getElementById.bind(document); + +// eslint-disable-next-line func-names +$.new = function $_new(tag, textContentOrChildren, className, attributes) { + let elt = document.createElement(tag); + if (className) { + elt.className = className; + } + if (attributes) { + if (attributes["data-l10n-id"]) { + let args = attributes.hasOwnProperty("data-l10n-args") + ? attributes["data-l10n-args"] + : undefined; + document.l10n.setAttributes(elt, attributes["data-l10n-id"], args); + delete attributes["data-l10n-id"]; + if (args) { + delete attributes["data-l10n-args"]; + } + } + + for (let attrName in attributes) { + elt.setAttribute(attrName, attributes[attrName]); + } + } + if (Array.isArray(textContentOrChildren)) { + this.append(elt, textContentOrChildren); + } else if (!attributes || !attributes["data-l10n-id"]) { + elt.textContent = String(textContentOrChildren); + } + return elt; +}; + +// eslint-disable-next-line func-names +$.append = function $_append(parent, children) { + children.forEach(c => parent.appendChild(c)); +}; + +function assembleFromGraphicsFailure(i, data) { + // Only cover the cases we have today; for example, we do not have + // log failures that assert and we assume the log level is 1/error. + let message = data.failures[i]; + let index = data.indices[i]; + let what = ""; + if (message.search(/\[GFX1-\]: \(LF\)/) == 0) { + // Non-asserting log failure - the message is substring(14) + what = "LogFailure"; + message = message.substring(14); + } else if (message.search(/\[GFX1-\]: /) == 0) { + // Non-asserting - the message is substring(9) + what = "Error"; + message = message.substring(9); + } else if (message.search(/\[GFX1\]: /) == 0) { + // Asserting - the message is substring(8) + what = "Assert"; + message = message.substring(8); + } + let assembled = { + index, + header: "(#" + index + ") " + what, + message, + }; + return assembled; +} + +function sortedArrayFromObject(obj) { + let tuples = []; + for (let prop in obj) { + tuples.push([prop, obj[prop]]); + } + tuples.sort(([prop1, v1], [prop2, v2]) => prop1.localeCompare(prop2)); + return tuples; +} + +function copyRawDataToClipboard(button) { + if (button) { + button.disabled = true; + } + Troubleshoot.snapshot().then( + async snapshot => { + if (button) { + button.disabled = false; + } + let str = Cc["@mozilla.org/supports-string;1"].createInstance( + Ci.nsISupportsString + ); + str.data = JSON.stringify(snapshot, undefined, 2); + let transferable = Cc[ + "@mozilla.org/widget/transferable;1" + ].createInstance(Ci.nsITransferable); + transferable.init(getLoadContext()); + transferable.addDataFlavor("text/plain"); + transferable.setTransferData("text/plain", str); + Services.clipboard.setData( + transferable, + null, + Ci.nsIClipboard.kGlobalClipboard + ); + }, + err => { + if (button) { + button.disabled = false; + } + console.error(err); + } + ); +} + +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +async function copyContentsToClipboard() { + // Get the HTML and text representations for the important part of the page. + let contentsDiv = $("contents").cloneNode(true); + // Remove the items we don't want to copy from the clone: + contentsDiv.querySelectorAll(".no-copy, [hidden]").forEach(n => n.remove()); + let dataHtml = contentsDiv.innerHTML; + let dataText = createTextForElement(contentsDiv); + + // We can't use plain strings, we have to use nsSupportsString. + let supportsStringClass = Cc["@mozilla.org/supports-string;1"]; + let ssHtml = supportsStringClass.createInstance(Ci.nsISupportsString); + let ssText = supportsStringClass.createInstance(Ci.nsISupportsString); + + let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + transferable.init(getLoadContext()); + + // Add the HTML flavor. + transferable.addDataFlavor("text/html"); + ssHtml.data = dataHtml; + transferable.setTransferData("text/html", ssHtml); + + // Add the plain text flavor. + transferable.addDataFlavor("text/plain"); + ssText.data = dataText; + transferable.setTransferData("text/plain", ssText); + + // Store the data into the clipboard. + Services.clipboard.setData( + transferable, + null, + Services.clipboard.kGlobalClipboard + ); +} + +// Return the plain text representation of an element. Do a little bit +// of pretty-printing to make it human-readable. +function createTextForElement(elem) { + let serializer = new Serializer(); + let text = serializer.serialize(elem); + + // Actual CR/LF pairs are needed for some Windows text editors. + if (AppConstants.platform == "win") { + text = text.replace(/\n/g, "\r\n"); + } + + return text; +} + +function Serializer() {} + +Serializer.prototype = { + serialize(rootElem) { + this._lines = []; + this._startNewLine(); + this._serializeElement(rootElem); + this._startNewLine(); + return this._lines.join("\n").trim() + "\n"; + }, + + // The current line is always the line that writing will start at next. When + // an element is serialized, the current line is updated to be the line at + // which the next element should be written. + get _currentLine() { + return this._lines.length ? this._lines[this._lines.length - 1] : null; + }, + + set _currentLine(val) { + this._lines[this._lines.length - 1] = val; + }, + + _serializeElement(elem) { + // table + if (elem.localName == "table") { + this._serializeTable(elem); + return; + } + + // all other elements + + let hasText = false; + for (let child of elem.childNodes) { + if (child.nodeType == Node.TEXT_NODE) { + let text = this._nodeText(child); + this._appendText(text); + hasText = hasText || !!text.trim(); + } else if (child.nodeType == Node.ELEMENT_NODE) { + this._serializeElement(child); + } + } + + // For headings, draw a "line" underneath them so they stand out. + let isHeader = /^h[0-9]+$/.test(elem.localName); + if (isHeader) { + let headerText = (this._currentLine || "").trim(); + if (headerText) { + this._startNewLine(); + this._appendText("-".repeat(headerText.length)); + } + } + + // Add a blank line underneath elements but only if they contain text. + if (hasText && (isHeader || "p" == elem.localName)) { + this._startNewLine(); + this._startNewLine(); + } + }, + + _startNewLine(lines) { + let currLine = this._currentLine; + if (currLine) { + // The current line is not empty. Trim it. + this._currentLine = currLine.trim(); + if (!this._currentLine) { + // The current line became empty. Discard it. + this._lines.pop(); + } + } + this._lines.push(""); + }, + + _appendText(text, lines) { + this._currentLine += text; + }, + + _isHiddenSubHeading(th) { + return th.parentNode.parentNode.style.display == "none"; + }, + + _serializeTable(table) { + // Collect the table's column headings if in fact there are any. First + // check thead. If there's no thead, check the first tr. + let colHeadings = {}; + let tableHeadingElem = table.querySelector("thead"); + if (!tableHeadingElem) { + tableHeadingElem = table.querySelector("tr"); + } + if (tableHeadingElem) { + let tableHeadingCols = tableHeadingElem.querySelectorAll("th,td"); + // If there's a contiguous run of th's in the children starting from the + // rightmost child, then consider them to be column headings. + for (let i = tableHeadingCols.length - 1; i >= 0; i--) { + let col = tableHeadingCols[i]; + if (col.localName != "th" || col.classList.contains("title-column")) { + break; + } + colHeadings[i] = this._nodeText(col).trim(); + } + } + let hasColHeadings = Object.keys(colHeadings).length > 0; + if (!hasColHeadings) { + tableHeadingElem = null; + } + + let trs = table.querySelectorAll("table > tr, tbody > tr"); + let startRow = + tableHeadingElem && tableHeadingElem.localName == "tr" ? 1 : 0; + + if (startRow >= trs.length) { + // The table's empty. + return; + } + + if (hasColHeadings) { + // Use column headings. Print each tr as a multi-line chunk like: + // Heading 1: Column 1 value + // Heading 2: Column 2 value + for (let i = startRow; i < trs.length; i++) { + let children = trs[i].querySelectorAll("td"); + for (let j = 0; j < children.length; j++) { + let text = ""; + if (colHeadings[j]) { + text += colHeadings[j] + ": "; + } + text += this._nodeText(children[j]).trim(); + this._appendText(text); + this._startNewLine(); + } + this._startNewLine(); + } + return; + } + + // Don't use column headings. Assume the table has only two columns and + // print each tr in a single line like: + // Column 1 value: Column 2 value + for (let i = startRow; i < trs.length; i++) { + let children = trs[i].querySelectorAll("th,td"); + let rowHeading = this._nodeText(children[0]).trim(); + if (children[0].classList.contains("title-column")) { + if (!this._isHiddenSubHeading(children[0])) { + this._appendText(rowHeading); + } + } else if (children.length == 1) { + // This is a single-cell row. + this._appendText(rowHeading); + } else { + let childTables = trs[i].querySelectorAll("table"); + if (childTables.length) { + // If we have child tables, don't use nodeText - its trs are already + // queued up from querySelectorAll earlier. + this._appendText(rowHeading + ": "); + } else { + this._appendText( + rowHeading + ": " + this._nodeText(children[1]).trim() + ); + } + } + this._startNewLine(); + } + this._startNewLine(); + }, + + _nodeText(node) { + return node.textContent.replace(/\s+/g, " "); + }, +}; + +function openProfileDirectory() { + // Get the profile directory. + let currProfD = Services.dirsvc.get("ProfD", Ci.nsIFile); + let profileDir = currProfD.path; + + // Show the profile directory. + let nsLocalFile = Components.Constructor( + "@mozilla.org/file/local;1", + "nsIFile", + "initWithPath" + ); + new nsLocalFile(profileDir).reveal(); +} + +/** + * Profile reset is only supported for the default profile if the appropriate migrator exists. + */ +function populateActionBox() { + if (ResetProfile.resetSupported()) { + $("reset-box").style.display = "block"; + } + if (!Services.appinfo.inSafeMode && AppConstants.platform !== "android") { + $("safe-mode-box").style.display = "block"; + + if (Services.policies && !Services.policies.isAllowed("safeMode")) { + $("restart-in-safe-mode-button").setAttribute("disabled", "true"); + } + } +} + +// Prompt user to restart the browser in safe mode +function safeModeRestart() { + let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + Services.obs.notifyObservers( + cancelQuit, + "quit-application-requested", + "restart" + ); + + if (!cancelQuit.data) { + Services.startup.restartInSafeMode(Ci.nsIAppStartup.eAttemptQuit); + } +} + +// Added for TB. +function onShowPrivateDataChange() { + document + .getElementById("contents") + .classList.toggle( + "show-private-data", + document.getElementById("check-show-private-data").checked + ); +} + +/** + * Set up event listeners for buttons. + */ +function setupEventListeners() { + /* not used by TB + let button = $("reset-box-button"); + if (button) { + button.addEventListener("click", function(event) { + ResetProfile.openConfirmationDialog(window); + }); + } +*/ + let button = $("clear-startup-cache-button"); + if (button) { + button.addEventListener("click", async function (event) { + const [promptTitle, promptBody, restartButtonLabel] = + await document.l10n.formatValues([ + { id: "startup-cache-dialog-title2" }, + { id: "startup-cache-dialog-body2" }, + { id: "restart-button-label" }, + ]); + const buttonFlags = + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL + + Services.prompt.BUTTON_POS_0_DEFAULT; + const result = Services.prompt.confirmEx( + window.docShell.chromeEventHandler.ownerGlobal, + promptTitle, + promptBody, + buttonFlags, + restartButtonLabel, + null, + null, + null, + {} + ); + if (result !== 0) { + return; + } + Services.appinfo.invalidateCachesOnRestart(); + Services.startup.quit( + Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit + ); + }); + } + button = $("restart-in-safe-mode-button"); + if (button) { + button.addEventListener("click", function (event) { + if ( + Services.obs + .enumerateObservers("restart-in-safe-mode") + .hasMoreElements() + ) { + Services.obs.notifyObservers(null, "restart-in-safe-mode"); + } else { + safeModeRestart(); + } + }); + } + if (AppConstants.MOZ_UPDATER) { + button = $("update-dir-button"); + if (button) { + button.addEventListener("click", function (event) { + // Get the update directory. + let updateDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile); + if (!updateDir.exists()) { + updateDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + let updateDirPath = updateDir.path; + // Show the update directory. + let nsLocalFile = Components.Constructor( + "@mozilla.org/file/local;1", + "nsIFile", + "initWithPath" + ); + new nsLocalFile(updateDirPath).reveal(); + }); + } + button = $("show-update-history-button"); + if (button) { + button.addEventListener("click", function (event) { + window.browsingContext.topChromeWindow.openDialog( + "chrome://mozapps/content/update/history.xhtml", + "Update:History", + "centerscreen,resizable=no,titlebar,modal" + ); + }); + } + } + button = $("verify-place-integrity-button"); + if (button) { + button.addEventListener("click", function (event) { + PlacesDBUtils.checkAndFixDatabase().then(tasksStatusMap => { + let logs = []; + for (let [key, value] of tasksStatusMap) { + logs.push(`> Task: ${key}`); + let prefix = value.succeeded ? "+ " : "- "; + logs = logs.concat(value.logs.map(m => `${prefix}${m}`)); + } + $("verify-place-result").style.display = "block"; + $("verify-place-result").classList.remove("no-copy"); + $("verify-place-result").textContent = logs.join("\n"); + }); + }); + } + + // added for TB + $("send-via-email").addEventListener("click", function (event) { + sendViaEmail(); + }); + // end of TB addition + /* not used by TB + $("copy-raw-data-to-clipboard").addEventListener("click", function(event) { + copyRawDataToClipboard(this); + }); +*/ + $("copy-to-clipboard").addEventListener("click", function (event) { + copyContentsToClipboard(); + }); + $("profile-dir-button").addEventListener("click", function (event) { + openProfileDirectory(); + }); +} diff --git a/comm/mail/components/about-support/content/aboutSupport.xhtml b/comm/mail/components/about-support/content/aboutSupport.xhtml new file mode 100644 index 0000000000..8b3f5df2f6 --- /dev/null +++ b/comm/mail/components/about-support/content/aboutSupport.xhtml @@ -0,0 +1,956 @@ + + + + + + %htmlDTD; + %brandDTD; +]> + + + + + + + + <link rel="icon" type="image/png" id="favicon" + href="chrome://branding/content/icon48.png"/> + <link rel="stylesheet" href="chrome://global/skin/aboutSupport.css" + type="text/css"/> +<!-- Added for TB --> + <link rel="stylesheet" href="chrome://messenger/skin/aboutSupport.css" + type="text/css"/> +<!-- End of TB addition --> + <script src="chrome://messenger/content/about-support/aboutSupport.js"/> + <link rel="localization" href="branding/brand.ftl"/> + <link rel="localization" href="toolkit/about/aboutSupport.ftl"/> + <link rel="localization" href="toolkit/global/resetProfile.ftl"/> + <link rel="localization" href="toolkit/global/processTypes.ftl"/> +<!-- Added for TB --> + <link rel="localization" href="messenger/aboutSupportMail.ftl"/> + <link rel="localization" href="messenger/aboutSupportCalendar.ftl"/> + <link rel="localization" href="messenger/aboutSupportChat.ftl"/> + <script src="chrome://messenger/content/about-support/accounts.js"/> + <script src="chrome://messenger/content/about-support/calendars.js"/> + <script src="chrome://messenger/content/about-support/chat.js"/> + <script src="chrome://messenger/content/about-support/libs.js"/> + <script src="chrome://messenger/content/about-support/export.js"/> +<!-- End of TB addition --> + </head> + + <body class="wide-container"> + <h1 data-l10n-id="page-title"/> + <div class="header-flex"> + <div class="content-flex"> + <div class="page-subtitle" data-l10n-id="page-subtitle"> + <a id="supportLink" data-l10n-name="support-link"></a> + </div> + <div id="support-buttons"> + <!-- Not used on TB + <button id="copy-raw-data-to-clipboard" data-l10n-id="copy-raw-data-to-clipboard-label"/> + --> + <button id="copy-to-clipboard" data-l10n-id="copy-text-to-clipboard-label"/> + <!-- Added for TB --> + <button id="send-via-email" data-l10n-id="send-via-email"/> + <div> + <input type="checkbox" + id="check-show-private-data" + class="data-uionly" + role="checkbox"/> + <span> + <label for="check-show-private-data" data-l10n-id="show-private-data-main-text"/> + <span class="gray-text" data-l10n-id="show-private-data-explanation-text"></span> + </span> + </div> + <!-- End of TB addition --> + </div> + </div> + +#ifndef ANDROID + <div class="action-box"> + <div id="reset-box"> + <h3 data-l10n-id="refresh-profile"/> + <button id="reset-box-button" data-l10n-id="refresh-profile-button"/> + </div> + <div id="safe-mode-box"> + <h3 data-l10n-id="troubleshoot-mode-title"/> + <button id="restart-in-safe-mode-button" data-l10n-id="restart-in-troubleshoot-mode-label"/> + </div> + <div id="clear-startup-cache-box"> + <h3 data-l10n-id="clear-startup-cache-title"/> + <button id="clear-startup-cache-button" data-l10n-id="clear-startup-cache-label"/> + </div> + </div> +#endif + </div> + <div id="contents"> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="app-basics-title"/> + + <table> + <tbody> + <tr> + <th class="column" data-l10n-id="app-basics-name"/> + + <td id="application-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-version"/> + + <td id="version-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-build-id"/> + <td id="buildid-box"></td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-distribution-id"/> + <td id="distributionid-box"></td> + </tr> + +#ifndef ANDROID +#ifdef MOZ_UPDATER + <tr id="update-dir-row" class="no-copy"> + <th class="column" data-l10n-id="app-basics-update-dir"/> + + <td> + <button id="update-dir-button" data-l10n-id="show-dir-label"/> + <span id="update-dir-box" dir="ltr"> + </span> + </td> + </tr> + + <tr id="update-history-row" class="no-copy"> + <th class="column" data-l10n-id="app-basics-update-history"/> + + <td> + <button id="show-update-history-button" data-l10n-id="app-basics-show-update-history"/> + </td> + </tr> +#endif +#endif + +#ifdef MOZ_UPDATER + <tr> + <th class="column" data-l10n-id="app-basics-update-channel"/> + <td id="updatechannel-box"></td> + </tr> +#endif + + <tr> + <th class="column" data-l10n-id="app-basics-user-agent"/> + + <td id="useragent-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-os"/> + + <td id="os-box"> + </td> + </tr> + + <tr id="os-theme-row"> + <th class="column" data-l10n-id="app-basics-os-theme"/> + + <td id="os-theme-box"> + </td> + </tr> + +#ifdef XP_MACOSX + <tr> + <th class="column" data-l10n-id="app-basics-rosetta"/> + + <td id="rosetta-box"> + </td> + </tr> +#endif + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-binary"/> + + <td id="binary-box" dir="ltr"> + </td> + </tr> + + <tr id="profile-row" class="no-copy" dir="ltr"> + <th class="column" data-l10n-id="app-basics-profile-dir"/> + + <td> + <button id="profile-dir-button" data-l10n-id="show-dir-label"/> + <span id="profile-dir-box"> + </span> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-build-config"/> + + <td> + <a href="about:buildconfig" target="_blank">about:buildconfig</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-memory-use"/> + + <td> + <a href="about:memory" target="_blank">about:memory</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-cache-use"/> + + <td> + <a href="about:cache" target="_blank">about:cache</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-performance"/> + + <td> + <a href="about:processes" target="_blank">about:processes</a> + </td> + </tr> + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-service-workers"/> + + <td> + <a href="about:serviceworkers" target="_blank">about:serviceworkers</a> + </td> + </tr> + +#if defined(XP_WIN) + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-third-party"/> + + <td> + <a href="about:third-party" target="_blank">about:third-party</a> + </td> + </tr> +#endif + +#if defined(XP_WIN) && defined(MOZ_LAUNCHER_PROCESS) + <tr> + <th class="column" data-l10n-id="app-basics-launcher-process-status"/> + + <td id="launcher-process-box"> + </td> + </tr> +#endif + + <tr> + <th class="column" data-l10n-id="app-basics-multi-process-support"/> + + <td id="multiprocess-box"> + <span id="multiprocess-box-process-count"/> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-fission-support"/> + + <td id="fission-box"> + <span id="fission-box-process-count"/> + <span id="fission-box-status"/> + </td> + </tr> + + <tr id="remoteprocesses-row"> + <th class="column" data-l10n-id="app-basics-remote-processes-count"/> + + <td> + <a href="#remote-processes"></a> + </td> + </tr> + + <tr id="policies-status-row"> + <th class="column" data-l10n-id="app-basics-enterprise-policies"/> + + <td id="policies-status"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-location-service-key-google"/> + + <td id="key-location-service-google-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-safebrowsing-key-google"/> + + <td id="key-safebrowsing-google-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-key-mozilla"/> + + <td id="key-mozilla-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-safe-mode"/> + + <td id="safemode-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-memory-size"/> + + <td id="memory-size-box"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="app-basics-disk-available"/> + + <td id="disk-available-box"> + </td> + </tr> + +#ifndef ANDROID + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-profiles"/> + + <td> + <a href="about:profiles" target="_blank">about:profiles</a> + </td> + </tr> +#endif + + <tr class="no-copy"> + <th class="column" data-l10n-id="app-basics-telemetry"/> + + <td> + <a href="about:telemetry" target="_blank">about:telemetry</a> + </td> + </tr> + + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> +<!-- Added for TB --> + <h2 class="major-section" data-l10n-id="accounts-title"/> + + <table id="accounts-table"> + <thead> + <tr> + <th rowspan="2" data-l10n-id="accounts-ID"/> + + <th rowspan="2" class="data-private" data-l10n-id="accounts-name"/> + + <th colspan="3" data-l10n-id="accounts-incoming-server"/> + + <th colspan="5" data-l10n-id="accounts-outgoing-servers"/> + </tr> + <tr class="thead-level2"> + <!-- Incoming server --> + <th data-l10n-id="accounts-server-name"/> + + <th data-l10n-id="accounts-conn-security"/> + + <th data-l10n-id="accounts-auth-method"/> + + <!-- Outgoing servers --> + <th class="data-private" data-l10n-id="identity-name"/> + + <th data-l10n-id="accounts-server-name"/> + + <th data-l10n-id="accounts-conn-security"/> + + <th data-l10n-id="accounts-auth-method"/> + + <th data-l10n-id="accounts-default"/> + </tr> + </thead> + + <tbody id="accounts-tbody"> + </tbody> + </table> + + <h2 class="major-section" data-l10n-id="mail-libs-title"></h2> + <table class="mail-libs-table"> + <caption></caption> + <thead> + <th data-l10n-id="libs-table-heading-library"></th> + <th data-l10n-id="libs-table-heading-status"></th> + <th data-l10n-id="libs-table-heading-expected-version"></th> + <th data-l10n-id="libs-table-heading-loaded-version"></th> + <th data-l10n-id="libs-table-heading-path"></th> + </thead> + + <tbody> + <tr> + <td>RNP (OpenPGP)</td> + <td id="rnp-status"> + </td> + <td id="rnp-expected-version"> + </td> + <td id="rnp-loaded-version"> + </td> + <td id="rnp-path"> + </td> + </tr> + </tbody> + </table> + + <h2 class="major-section" data-l10n-id="calendars-title"></h2> + + <div id="calendar-tables"></div> + + <template id="calendars-table-template"> + <table class="calendar-table"> + <caption></caption> + <thead> + <th data-l10n-id="calendars-table-heading-property"></th> + <th data-l10n-id="calendars-table-heading-value"></th> + </thead> + <tbody> + </tbody> + </table> + </template> + + <template id="calendars-table-row-template"> + <tr> + <td></td> + <td></td> + </tr> + </template> + + <h2 class="major-section no-copy" data-l10n-id="chat-title"></h2> + + <table class="no-copy" id="chat-table"> + <thead> + <tr> + <th data-l10n-id="chat-table-heading-account"></th> + <th data-l10n-id="chat-table-heading-protocol"></th> + <th data-l10n-id="chat-table-heading-name" class="data-private"></th> + <th data-l10n-id="chat-table-heading-actions"></th> + </tr> + </thead> + <tbody id="chat-tbody"> + </tbody> + </table> + + <template id="chat-table-row-template"> + <tr> + <td></td> + <td></td> + <td class="data-private"></td> + <td><button class="button" type="button" data-l10n-id="chat-table-copy-debug-log"></button></td> + </tr> + </template> + +<!-- End of TB addition --> + + <!-- - - - - - - - - - - - - - - - - - - - - --> +#ifdef MOZ_CRASHREPORTER + + <h2 class="major-section" id="crashes-title" data-l10n-id="crashes-title"/> + + <table id="crashes-table"> + <thead> + <tr> + <th data-l10n-id="crashes-id"/> + <th data-l10n-id="crashes-send-date"/> + </tr> + </thead> + <tbody id="crashes-tbody"> + </tbody> + </table> + <p id="crashes-allReports" class="hidden no-copy"> + <a href="about:crashes" id="crashes-allReportsWithPending" + class="block" data-l10n-id="crashes-all-reports" target="_blank"/> + </p> + <p id="crashes-noConfig" class="hidden no-copy" data-l10n-id="crashes-no-config"/> + +#endif + <!-- - - - - - - - - - - - - - - - - - - - - --> + <!-- Not used by TB + <h2 class="major-section" data-l10n-id="features-title"/> + + <table id="features-table"> + <thead> + <tr> + <th data-l10n-id="features-name"/> + <th data-l10n-id="features-version"/> + <th data-l10n-id="features-id"/> + </tr> + </thead> + <tbody id="features-tbody"> + </tbody> + </table> + --> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="processes-title" id="remote-processes"/> + + <table id="remote-processes-table"> + <thead> + <tr> + <th data-l10n-id="processes-type"/> + <th data-l10n-id="processes-count"/> + </tr> + </thead> + <tbody id="processes-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="support-addons-title"/> + + <table> + <thead> + <tr> + <th data-l10n-id="support-addons-name"/> + <th data-l10n-id="support-addons-type"/> + <th data-l10n-id="support-addons-version"/> + <th data-l10n-id="support-addons-enabled"/> + <th data-l10n-id="support-addons-id"/> + </tr> + </thead> + <tbody id="addons-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" id="security-software-title" data-l10n-id="security-software-title"/> + + <table id="security-software-table"> + <thead> + <tr> + <th data-l10n-id="security-software-type"/> + <th data-l10n-id="security-software-name"/> + </tr> + </thead> + <tbody> + <tr> + <th class="column" data-l10n-id="security-software-antivirus"/> + + <td id="security-software-antivirus"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="security-software-antispyware"/> + + <td id="security-software-antispyware"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="security-software-firewall"/> + + <td id="security-software-firewall"> + </td> + </tr> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="graphics-title"/> + + <table> + <tbody id="graphics-features-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-features-title"/> + </tr> + </tbody> + + <tbody id="graphics-gpu-1-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-gpu1-title"/> + </tr> + </tbody> + + <tbody id="graphics-gpu-2-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-gpu2-title"/> + </tr> + </tbody> + + <tbody id="graphics-diagnostics-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-diagnostics-title"/> + </tr> + </tbody> + + <tbody id="graphics-decisions-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-decision-log-title"/> + </tr> + </tbody> + + <tbody id="graphics-crashguards-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-crash-guards-title"/> + </tr> + </tbody> + + <tbody id="graphics-workarounds-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-workarounds-title"/> + </tr> + </tbody> + + <tbody id="graphics-failures-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="graphics-failure-log-title"/> + </tr> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="media-title"/> + <table> + <tbody id="media-info-tbody"> + </tbody> + + <tbody id="media-output-devices-tbody"> + <tr> + <th colspan="9" class="title-column" data-l10n-id="media-output-devices-title"/> + </tr> + <tr> + <th data-l10n-id="media-device-name"/> + <th data-l10n-id="media-device-group"/> + <th data-l10n-id="media-device-vendor"/> + <th data-l10n-id="media-device-state"/> + <th data-l10n-id="media-device-preferred"/> + <th data-l10n-id="media-device-format"/> + <th data-l10n-id="media-device-channels"/> + <th data-l10n-id="media-device-rate"/> + <th data-l10n-id="media-device-latency"/> + </tr> + </tbody> + + <tbody id="media-input-devices-tbody"> + <tr> + <th colspan="9" class="title-column" data-l10n-id="media-input-devices-title"/> + </tr> + <tr> + <th data-l10n-id="media-device-name"/> + <th data-l10n-id="media-device-group"/> + <th data-l10n-id="media-device-vendor"/> + <th data-l10n-id="media-device-state"/> + <th data-l10n-id="media-device-preferred"/> + <th data-l10n-id="media-device-format"/> + <th data-l10n-id="media-device-channels"/> + <th data-l10n-id="media-device-rate"/> + <th data-l10n-id="media-device-latency"/> + </tr> + </tbody> + + <tbody id="media-capabilities-tbody"> + <tr> + <th colspan="9" class="title-column" data-l10n-id="media-capabilities-title"/> + </tr> + <tr> + <td colspan="9"> + <button id="enumerate-database-button" data-l10n-id="media-capabilities-enumerate"/> + <pre id="enumerate-database-result" class="hidden no-copy"></pre> + </td> + </tr> + </tbody> + + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="environment-variables-title"/> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name" data-l10n-id="environment-variables-name"/> + + <th class="value" data-l10n-id="environment-variables-value"/> + </thead> + + <tbody id="environment-variables-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="modified-key-prefs-title"/> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name" data-l10n-id="modified-prefs-name"/> + + <th class="value" data-l10n-id="modified-prefs-value"/> + </thead> + + <tbody id="prefs-tbody"> + </tbody> + </table> + + <section id="prefs-user-js-section" class="hidden no-copy"> + <h3 data-l10n-id="user-js-title"/> + <p data-l10n-id="user-js-description"> + <a id="prefs-user-js-link" data-l10n-name="user-js-link"></a> + </p> + </section> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="locked-key-prefs-title"/> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name" data-l10n-id="locked-prefs-name"/> + + <th class="value" data-l10n-id="locked-prefs-value"/> + </thead> + + <tbody id="locked-prefs-tbody"> + </tbody> + </table> + +#ifndef ANDROID + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="place-database-title"/> + + <table> + <tr class="no-copy"> + <th class="column" data-l10n-id="place-database-integrity"/> + + <td> + <button id="verify-place-integrity-button" data-l10n-id="place-database-verify-integrity"/> + <pre id="verify-place-result" class="hidden no-copy"></pre> + </td> + </tr> + </table> +#endif + + <!-- - - - - - - - - - - - - - - - - - - - - --> + <h2 class="major-section" data-l10n-id="a11y-title"/> + + <table> + <tbody> + <tr> + <th class="column" data-l10n-id="a11y-activated"/> + + <td id="a11y-activated"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="a11y-force-disabled"/> + + <td id="a11y-force-disabled"> + </td> + </tr> +#if defined(XP_WIN) + <tr> + <th class="column" data-l10n-id="a11y-handler-used"/> + + <td id="a11y-handler-used"> + </td> + </tr> + + <tr> + <th class="column" data-l10n-id="a11y-instantiator"/> + + <td id="a11y-instantiator"> + </td> + </tr> +#endif + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + <h2 class="major-section" data-l10n-id="library-version-title"/> + + <table> + <tbody id="libversions-tbody"> + </tbody> + </table> + + <!-- - - - - - - - - - - - - - - - - - - - - --> + +#if defined(MOZ_SANDBOX) + <h2 class="major-section" id="sandbox" data-l10n-id="sandbox-title"/> + + <table> + <tbody id="sandbox-tbody"> + </tbody> + </table> + +#if defined(XP_LINUX) + <h4 data-l10n-id="sandbox-sys-call-log-title"/> + <table> + <thead> + <tr> + <th data-l10n-id="sandbox-sys-call-index"/> + <th data-l10n-id="sandbox-sys-call-age"/> + <th data-l10n-id="sandbox-sys-call-pid"/> + <th data-l10n-id="sandbox-sys-call-tid"/> + <th data-l10n-id="sandbox-sys-call-proc-type"/> + <th data-l10n-id="sandbox-sys-call-number"/> + <th id="sandbox-syscalls-argshead" data-l10n-id="sandbox-sys-call-args"/> + </tr> + </thead> + <tbody id="sandbox-syscalls-tbody"> + </tbody> + </table> +#endif +#endif + + <h2 class="major-section" data-l10n-id="startup-cache-title"/> + + <table> + <tbody> + <tr> + <th class="column" data-l10n-id="startup-cache-disk-cache-path"/> + + <td id="startup-cache-disk-cache-path"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="startup-cache-ignore-disk-cache"/> + + <td id="startup-cache-ignore-disk-cache"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="startup-cache-found-disk-cache-on-init"/> + + <td id="startup-cache-found-disk-cache-on-init"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="startup-cache-wrote-to-disk-cache"/> + + <td id="startup-cache-wrote-to-disk-cache"> + </td> + </tr> + </tbody> + </table> + + <h2 class="major-section" data-l10n-id="intl-title"/> + + <table> + <tbody id="intl-localeservice-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="intl-app-title"/> + </tr> + <tr> + <th class="column" data-l10n-id="intl-locales-requested"/> + <td id="intl-locale-requested"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-locales-available"/> + <td id="intl-locale-available"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-locales-supported"/> + <td id="intl-locale-supported"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-regional-prefs"/> + <td id="intl-locale-regionalprefs"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-locales-default"/> + <td id="intl-locale-default"> + </td> + </tr> + </tbody> + <tbody id="intl-ospreferences-tbody"> + <tr> + <th colspan="2" class="title-column" data-l10n-id="intl-os-title"/> + </tr> + <tr> + <th class="column" data-l10n-id="intl-os-prefs-system-locales"/> + <td id="intl-osprefs-systemlocales"> + </td> + </tr> + <tr> + <th class="column" data-l10n-id="intl-regional-prefs"/> + <td id="intl-osprefs-regionalprefs"> + </td> + </tr> + </tbody> + </table> + <!-- - - - - - - - - - - - - - - - - - - - - --> + +#if defined(ENABLE_WEBDRIVER) + <h2 class="major-section" data-l10n-id="remote-debugging-title"/> + + <table> + <tbody> + <tr> + <th class="column" data-l10n-id="remote-debugging-accepting-connections"/> + <td id="remote-debugging-accepting-connections"></td> + </tr> + <tr> + <th class="column" data-l10n-id="remote-debugging-url"/> + <td id="remote-debugging-url"></td> + </tr> + </tbody> + </table> +#endif + +#ifndef ANDROID + <!-- - - - - - - - - - - - - - - - - - - - - --> + + <h2 class="major-section" data-l10n-id="support-printing-title"/> + + <table> + <tr class="no-copy"> + <th class="column" data-l10n-id="support-printing-troubleshoot"/> + <td> + <button id="support-printing-clear-settings-button" data-l10n-id="support-printing-clear-settings-button"/> + </td> + </tr> + </table> + + <h3 data-l10n-id="support-printing-modified-settings"/> + + <table class="prefs-table"> + <thead class="no-copy"> + <th class="name" data-l10n-id="support-printing-prefs-name"/> + + <th class="value" data-l10n-id="support-printing-prefs-value"/> + </thead> + + <tbody id="support-printing-prefs-tbody"> + </tbody> + </table> +#endif + + </div> + + </body> + +</html> diff --git a/comm/mail/components/about-support/content/accounts.js b/comm/mail/components/about-support/content/accounts.js new file mode 100644 index 0000000000..33633f4a15 --- /dev/null +++ b/comm/mail/components/about-support/content/accounts.js @@ -0,0 +1,339 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals CLASS_DATA_PRIVATE, CLASS_DATA_PUBLIC */ + +"use strict"; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +// Platform-specific includes +var AboutSupportPlatform; +if ("@mozilla.org/windows-registry-key;1" in Cc) { + let temp = ChromeUtils.import("resource:///modules/AboutSupportWin32.jsm"); + AboutSupportPlatform = temp.AboutSupportPlatform; +} else if ("nsILocalFileMac" in Ci) { + let temp = ChromeUtils.import("resource:///modules/AboutSupportMac.jsm"); + AboutSupportPlatform = temp.AboutSupportPlatform; +} else { + let temp = ChromeUtils.import("resource:///modules/AboutSupportUnix.jsm"); + AboutSupportPlatform = temp.AboutSupportPlatform; +} + +var gMessengerBundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" +); + +var gSocketTypes = {}; +for (let [str, index] of Object.entries(Ci.nsMsgSocketType)) { + gSocketTypes[index] = str; +} + +var gAuthMethods = {}; +for (let [str, index] of Object.entries(Ci.nsMsgAuthMethod)) { + gAuthMethods[index] = str; +} + +// l10n properties in messenger.properties corresponding to each auth method +var gAuthMethodProperties = new Map([ + [0, "authNo"], // Special value defined to be invalid. + // Some accounts without auth report this. + [Ci.nsMsgAuthMethod.none, "authNo"], + [Ci.nsMsgAuthMethod.old, "authOld"], + [Ci.nsMsgAuthMethod.passwordCleartext, "authPasswordCleartextViaSSL"], + [Ci.nsMsgAuthMethod.passwordEncrypted, "authPasswordEncrypted"], + [Ci.nsMsgAuthMethod.GSSAPI, "authKerberos"], + [Ci.nsMsgAuthMethod.NTLM, "authNTLM"], + [Ci.nsMsgAuthMethod.External, "authExternal"], + [Ci.nsMsgAuthMethod.secure, "authAnySecure"], + [Ci.nsMsgAuthMethod.anything, "authAny"], + [Ci.nsMsgAuthMethod.OAuth2, "authOAuth2"], +]); + +var AboutSupport = { + /** + * Gets details about SMTP servers for a given nsIMsgAccount. + * + * @returns An array of records, each record containing the name and other details + * about one SMTP server. + */ + _getSMTPDetails(aAccount) { + let defaultIdentity = aAccount.defaultIdentity; + let smtpDetails = []; + + for (let identity of aAccount.identities) { + let isDefault = identity == defaultIdentity; + let smtpServer = MailServices.smtp.getServerByIdentity(identity); + if (!smtpServer) { + continue; + } + smtpDetails.push({ + identityName: identity.identityName, + name: smtpServer.displayname, + authMethod: smtpServer.authMethod, + socketType: smtpServer.socketType, + isDefault, + }); + } + + return smtpDetails; + }, + + /** + * Returns account details as an array of records. + */ + getAccountDetails() { + let accountDetails = []; + + for (let account of MailServices.accounts.accounts) { + let server = account.incomingServer; + accountDetails.push({ + key: account.key, + name: server.prettyName, + hostDetails: + "(" + + server.type + + ") " + + server.hostName + + (server.port != -1 ? ":" + server.port : ""), + socketType: server.socketType, + authMethod: server.authMethod, + smtpServers: this._getSMTPDetails(account), + }); + } + + function idCompare(accountA, accountB) { + let regex = /^account([0-9]+)$/; + let regexA = regex.exec(accountA.key); + let regexB = regex.exec(accountB.key); + // There's an off chance that the account ID isn't in the standard + // accountN form. If so, use the standard string compare against a fixed + // string ("account") to avoid correctness issues. + if (!regexA || !regexB) { + let keyA = regexA ? "account" : accountA.key; + let keyB = regexB ? "account" : accountB.key; + return keyA.localeCompare(keyB); + } + let idA = parseInt(regexA[1]); + let idB = parseInt(regexB[1]); + return idA - idB; + } + + // Sort accountDetails by account ID. + accountDetails.sort(idCompare); + return accountDetails; + }, + + /** + * Returns the corresponding text for a given socket type index. The text is + * returned as a record with "localized" and "neutral" entries. + */ + getSocketTypeText(aIndex) { + let plainSocketType = + aIndex in gSocketTypes ? gSocketTypes[aIndex] : aIndex; + let prettySocketType; + try { + prettySocketType = gMessengerBundle.GetStringFromName( + "smtpServer-ConnectionSecurityType-" + aIndex + ); + } catch (e) { + if (e.result == Cr.NS_ERROR_FAILURE) { + // The string wasn't found in the bundle. Make do without it. + prettySocketType = plainSocketType; + } else { + throw e; + } + } + return { localized: prettySocketType, neutral: plainSocketType }; + }, + + /** + * Returns the corresponding text for a given authentication method index. The + * text is returned as a record with "localized" and "neutral" entries. + */ + getAuthMethodText(aIndex) { + let prettyAuthMethod; + let plainAuthMethod = + aIndex in gAuthMethods ? gAuthMethods[aIndex] : aIndex; + if (gAuthMethodProperties.has(parseInt(aIndex))) { + prettyAuthMethod = gMessengerBundle.GetStringFromName( + gAuthMethodProperties.get(parseInt(aIndex)) + ); + } else { + prettyAuthMethod = plainAuthMethod; + } + return { localized: prettyAuthMethod, neutral: plainAuthMethod }; + }, +}; + +function createParentElement(tagName, childElems) { + let elem = document.createElement(tagName); + appendChildren(elem, childElems); + return elem; +} + +function createElement(tagName, textContent, opt_attributes, opt_copyData) { + if (opt_attributes == null) { + opt_attributes = {}; + } + let elem = document.createElement(tagName); + elem.textContent = textContent; + for (let key in opt_attributes) { + elem.setAttribute(key, "" + opt_attributes[key]); + } + + if (opt_copyData != null) { + elem.dataset.copyData = opt_copyData; + } + + return elem; +} + +function appendChildren(parentElem, children) { + for (let i = 0; i < children.length; i++) { + parentElem.appendChild(children[i]); + } +} + +/** + * Coerces x into a string. + */ +function toStr(x) { + return "" + x; +} + +/** + * Marks x as private (see below). + */ +function toPrivate(x) { + return { localized: x, neutral: x, isPrivate: true }; +} + +/** + * A list of fields for the incoming server of an account. Each element of the + * list is a pair of [property name, transforming function]. The transforming + * function should take the property and return either a string or an object + * with the following properties: + * - localized: the data in (possibly) localized form + * - neutral: the data in language-neutral form + * - isPrivate (optional): true if the data is private-only, false if public-only, + * not stated otherwise + */ +var gIncomingDetails = [ + ["key", toStr], + ["name", toPrivate], + ["hostDetails", toStr], + ["socketType", AboutSupport.getSocketTypeText.bind(AboutSupport)], + ["authMethod", AboutSupport.getAuthMethodText.bind(AboutSupport)], +]; + +/** + * A list of fields for the outgoing servers associated with an account. This is + * similar to gIncomingDetails above. + */ +var gOutgoingDetails = [ + ["identityName", toPrivate], + ["name", toStr], + ["socketType", AboutSupport.getSocketTypeText.bind(AboutSupport)], + ["authMethod", AboutSupport.getAuthMethodText.bind(AboutSupport)], + ["isDefault", toStr], +]; + +/** + * A list of account details. + */ +var gAccountDetails = AboutSupport.getAccountDetails(); + +function populateAccountsSection() { + let trAccounts = []; + + function createTD(data, rowSpan) { + let text = typeof data == "string" ? data : data.localized; + let copyData = typeof data == "string" ? null : data.neutral; + let attributes = { rowspan: rowSpan }; + if (typeof data == "object" && "isPrivate" in data) { + attributes.class = data.isPrivate + ? CLASS_DATA_PRIVATE + : CLASS_DATA_PUBLIC; + } + + return createElement("td", text, attributes, copyData); + } + + for (let account of gAccountDetails) { + // We want a minimum rowspan of 1 + let rowSpan = account.smtpServers.length || 1; + // incomingTDs is an array of TDs + let incomingTDs = gIncomingDetails.map(([prop, fn]) => + createTD(fn(account[prop]), rowSpan) + ); + // outgoingTDs is an array of arrays of TDs + let outgoingTDs = []; + for (let smtp of account.smtpServers) { + outgoingTDs.push( + gOutgoingDetails.map(([prop, fn]) => createTD(fn(smtp[prop]), 1)) + ); + } + + // If there are no SMTP servers, add a dummy element to make life easier below + if (outgoingTDs.length == 0) { + outgoingTDs = [[]]; + } + + // Add the first SMTP server to this tr. + let tr = createParentElement("tr", incomingTDs.concat(outgoingTDs[0])); + trAccounts.push(tr); + // Add the remaining SMTP servers as separate trs + for (let tds of outgoingTDs.slice(1)) { + trAccounts.push(createParentElement("tr", tds)); + } + } + + appendChildren(document.getElementById("accounts-tbody"), trAccounts); +} + +/** + * Returns a plaintext representation of the accounts data. + */ +function getAccountsText(aHidePrivateData, aIndent) { + let accumulator = []; + + // Given a string or object, converts it into a language-neutral form + function neutralizer(data) { + if (typeof data == "string") { + return data; + } + if ("isPrivate" in data && aHidePrivateData == data.isPrivate) { + return ""; + } + return data.neutral; + } + + for (let account of gAccountDetails) { + accumulator.push(aIndent + account.key + ":"); + // incomingData is an array of strings + let incomingData = gIncomingDetails.map(([prop, fn]) => + neutralizer(fn(account[prop])) + ); + accumulator.push(aIndent + " INCOMING: " + incomingData.join(", ")); + + // outgoingData is an array of arrays of strings + let outgoingData = []; + for (let smtp of account.smtpServers) { + outgoingData.push( + gOutgoingDetails.map(([prop, fn]) => neutralizer(fn(smtp[prop]))) + ); + } + + for (let data of outgoingData) { + accumulator.push(aIndent + " OUTGOING: " + data.join(", ")); + } + + accumulator.push(""); + } + + return accumulator.join("\n"); +} diff --git a/comm/mail/components/about-support/content/calendars.js b/comm/mail/components/about-support/content/calendars.js new file mode 100644 index 0000000000..a55b59572c --- /dev/null +++ b/comm/mail/components/about-support/content/calendars.js @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals CLASS_DATA_PRIVATE */ + +var { cal } = ChromeUtils.import("resource:///modules/calendar/calUtils.jsm"); + +let boolean = val => (!val && val !== false ? "" : val); +let string = val => (val ? String(val) : ""); + +/** + * A list of tuples for each calendar property displayed where each tuple + * contains the following elements: + * 0 - The name of the property passed to getProperty(). + * 1 - A function that accepts the property value and attempts it into a string. + * 2 - Boolean indicating whether the property is private data (optional). + */ +let gCalendarProperties = [ + ["name", string, true], + ["type", string], + ["disabled", boolean], + ["username", string, true], + ["uri", string, true], + ["refreshInterval", string], + ["readOnly", boolean], + ["suppressAlarms", boolean], + ["cache.enabled", boolean], + ["imip.identity", identity => string(identity && identity.key)], + ["imip.identity.disabled", boolean], + ["imip.identity.account", account => string(account && account.key)], + ["organizerId", string, true], + ["forceEmailScheduling", boolean], + ["capabilities.alarms.popup.supported", boolean], + ["capabilities.alarms.oninviations.supported", boolean], + ["capabilities.alarms.maxCount", string], + ["capabilities.attachments.supported", boolean], + ["capabilities.categories.maxCount", string], + ["capabilities.privacy.supported", boolean], + ["capabilities.priority.supported", boolean], + ["capabilities.events.supported", boolean], + ["capabilities.tasks.supported", boolean], + ["capabilities.timezones.floating.supported", boolean], + ["capabilities.timezones.UTC.supported", boolean], + ["capabilities.autoschedule.supported", boolean], +]; + +/** + * Populates the "Calendars" section of the troubleshooting information page + * with the properties of each configured calendar. + */ +function populateCalendarsSection() { + let container = document.getElementById("calendar-tables"); + let tableTmpl = document.getElementById("calendars-table-template"); + let rowTmpl = document.getElementById("calendars-table-row-template"); + + for (let calendar of cal.manager.getCalendars()) { + let table = tableTmpl.content.cloneNode(true).querySelector("table"); + table.firstElementChild.textContent = calendar.name; + + let tbody = table.querySelector("tbody"); + for (let [prop, transform, isPrivate] of gCalendarProperties) { + let tr = rowTmpl.content.cloneNode(true).querySelector("tr"); + let l10nKey = `calendars-table-${prop + .toLowerCase() + .replaceAll(".", "-")}`; + + tr.cells[0].setAttribute("data-l10n-id", l10nKey); + tr.cells[1].textContent = transform(calendar.getProperty(prop)); + if (isPrivate) { + tr.cells[1].setAttribute("class", CLASS_DATA_PRIVATE); + } + tbody.appendChild(tr); + } + container.appendChild(table); + } +} diff --git a/comm/mail/components/about-support/content/chat.js b/comm/mail/components/about-support/content/chat.js new file mode 100644 index 0000000000..50c34b9ba0 --- /dev/null +++ b/comm/mail/components/about-support/content/chat.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { IMServices } = ChromeUtils.importESModule( + "resource:///modules/IMServices.sys.mjs" +); + +/** + * Populates the "Chat" section of the troubleshooting information page with + * the chat accounts. + */ +function populateChatSection() { + let table = document.getElementById("chat-table"); + let rowTmpl = document.getElementById("chat-table-row-template"); + let dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeStyle: "long", + }); + let formatDebugMessage = dbgMsg => { + let m = dbgMsg.message; + let time = new Date(m.timeStamp); + time = dateTimeFormatter.format(time); + let level = dbgMsg.logLevel; + if (!level) { + return "(" + m.errorMessage + ")"; + } + if (level == dbgMsg.LEVEL_ERROR) { + level = "ERROR"; + } else if (level == dbgMsg.LEVEL_WARNING) { + level = "WARN."; + } else if (level == dbgMsg.LEVEL_LOG) { + level = "LOG "; + } else { + level = "DEBUG"; + } + return ( + "[" + + time + + "] " + + level + + " (@ " + + m.sourceLine + + " " + + m.sourceName + + ":" + + m.lineNumber + + ")\n" + + m.errorMessage + ); + }; + + let chatAccounts = IMServices.accounts.getAccounts(); + if (!chatAccounts.length) { + return; + } + table.querySelector("tbody").append( + ...chatAccounts.map(account => { + const row = rowTmpl.content.cloneNode(true).querySelector("tr"); + row.cells[0].textContent = account.id; + row.cells[1].textContent = account.protocol.id; + row.cells[2].textContent = account.name; + row.cells[3].addEventListener("click", () => { + const text = account + .getDebugMessages() + .map(formatDebugMessage) + .join("\n"); + navigator.clipboard.writeText(text); + }); + return row; + }) + ); +} diff --git a/comm/mail/components/about-support/content/export.js b/comm/mail/components/about-support/content/export.js new file mode 100644 index 0000000000..46eb0c6497 --- /dev/null +++ b/comm/mail/components/about-support/content/export.js @@ -0,0 +1,288 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* globals CLASS_DATA_PRIVATE, CLASS_DATA_PUBLIC, CLASS_DATA_UIONLY, createElement, +createParentElement, getAccountsText, getLoadContext, MailServices, Services */ + +"use strict"; + +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +/** + * Create warning text to add to any private data. + * + * @returns A HTML paragraph node containing the warning. + */ +function createWarning() { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/aboutSupportMail.properties" + ); + return createParentElement("p", [ + createElement("strong", bundle.GetStringFromName("warningLabel")), + // Add some whitespace between the label and the text + document.createTextNode(" "), + document.createTextNode(bundle.GetStringFromName("warningText")), + ]); +} + +function getClipboardTransferable() { + // Get the HTML and text representations for the important part of the page. + let hidePrivateData = !document.getElementById("check-show-private-data") + .checked; + let contentsDiv = createCleanedUpContents(hidePrivateData); + let dataHtml = contentsDiv.innerHTML; + let dataText = createTextForElement(contentsDiv, hidePrivateData); + + // We can't use plain strings, we have to use nsSupportsString. + let supportsStringClass = Cc["@mozilla.org/supports-string;1"]; + let ssHtml = supportsStringClass.createInstance(Ci.nsISupportsString); + let ssText = supportsStringClass.createInstance(Ci.nsISupportsString); + + let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + transferable.init(getLoadContext()); + + // Add the HTML flavor. + transferable.addDataFlavor("text/html"); + ssHtml.data = dataHtml; + transferable.setTransferData("text/html", ssHtml); + + // Add the plain text flavor. + transferable.addDataFlavor("text/plain"); + ssText.data = dataText; + transferable.setTransferData("text/plain", ssText); + + return transferable; +} + +// This function intentionally has the same name as the one in aboutSupport.js +// so that the one here is called. +function copyContentsToClipboard() { + let transferable = getClipboardTransferable(); + // Store the data into the clipboard. + Services.clipboard.setData( + transferable, + null, + Services.clipboard.kGlobalClipboard + ); +} + +function sendViaEmail() { + // Get the HTML representation for the important part of the page. + let hidePrivateData = !document.getElementById("check-show-private-data") + .checked; + let contentsDiv = createCleanedUpContents(hidePrivateData); + let dataHtml = contentsDiv.innerHTML; + // The editor considers whitespace to be significant, so replace all + // whitespace with a single space. + dataHtml = dataHtml.replace(/\s+/g, " "); + + // Set up parameters and fields to use for the compose window. + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.type = Ci.nsIMsgCompType.New; + params.format = Ci.nsIMsgCompFormat.HTML; + + let fields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + fields.forcePlainText = false; + fields.body = dataHtml; + // In general we can have non-ASCII characters, and compose's charset + // detection doesn't seem to work when the HTML part is pure ASCII but the + // text isn't. So take the easy way out and force UTF-8. + fields.bodyIsAsciiOnly = false; + params.composeFields = fields; + + // Our params are set up. Now open a compose window. + MailServices.compose.OpenComposeWindowWithParams(null, params); +} + +function createCleanedUpContents(aHidePrivateData) { + // Get the important part of the page. + let contentsDiv = document.getElementById("contents"); + // Deep-clone the entire div. + let clonedDiv = contentsDiv.cloneNode(true); + // Go in and replace text with the text we actually want to copy. + // (this mutates the cloned node) + cleanUpText(clonedDiv, aHidePrivateData); + // Insert a warning if we need to + if (!aHidePrivateData) { + clonedDiv.insertBefore(createWarning(), clonedDiv.firstChild); + } + return clonedDiv; +} + +function cleanUpText(aElem, aHidePrivateData) { + let node = aElem.firstChild; + let copyData = aElem.dataset.copyData; + delete aElem.dataset.copyData; + while (node) { + let classList = "classList" in node && node.classList; + // Delete uionly and no-copy nodes. + if ( + classList && + (classList.contains(CLASS_DATA_UIONLY) || classList.contains("no-copy")) + ) { + // Advance to the next node before removing the current node, since + // node.nextElementSibling is null after remove() + let nextNode = node.nextElementSibling; + node.remove(); + node = nextNode; + continue; + } else if ( + aHidePrivateData && + classList && + classList.contains(CLASS_DATA_PRIVATE) + ) { + // Replace private data with a blank string. + node.textContent = ""; + } else if ( + !aHidePrivateData && + classList && + classList.contains(CLASS_DATA_PUBLIC) + ) { + // Replace public data with a blank string. + node.textContent = ""; + } else if (copyData != null) { + // Replace localized text with non-localized text. + node.textContent = copyData; + copyData = null; + } + + if (node.nodeType == Node.ELEMENT_NODE) { + cleanUpText(node, aHidePrivateData); + } + + // Advance! + node = node.nextSibling; + } +} + +// Return the plain text representation of an element. Do a little bit +// of pretty-printing to make it human-readable. +function createTextForElement(elem, aHidePrivateData) { + // Generate the initial text. + let textFragmentAccumulator = []; + generateTextForElement(elem, aHidePrivateData, "", textFragmentAccumulator); + let text = textFragmentAccumulator.join(""); + + // Trim extraneous whitespace before newlines, then squash extraneous + // blank lines. + text = text.replace(/[ \t]+\n/g, "\n"); + text = text.replace(/\n{3,}/g, "\n\n"); + + // Actual CR/LF pairs are needed for some Windows text editors. + if ("@mozilla.org/windows-registry-key;1" in Cc) { + text = text.replace(/\n/g, "\r\n"); + } + + return text; +} + +/** + * Elements to replace entirely with custom text. Keys are element ids, values + * are functions that return the text. The functions themselves are defined in + * the files for their respective sections. + */ +var gElementsToReplace = { + "accounts-table": getAccountsText, +}; + +function generateTextForElement( + elem, + aHidePrivateData, + indent, + textFragmentAccumulator +) { + // Add a little extra spacing around most elements. + if (!["td", "th", "span", "a"].includes(elem.tagName)) { + textFragmentAccumulator.push("\n"); + } + + // If this element is one of our elements to replace with text, do it. + if (elem.id in gElementsToReplace) { + let replaceFn = gElementsToReplace[elem.id]; + textFragmentAccumulator.push(replaceFn(aHidePrivateData, indent + " ")); + return; + } + + if (AppConstants.MOZ_CRASHREPORTER) { + if (elem.id == "crashes-table") { + textFragmentAccumulator.push(getCrashesText(indent)); + return; + } + } + + let childCount = elem.childElementCount; + + // We're not going to spread a two-column <tr> across multiple lines, so + // handle that separately. + if (elem.tagName == "tr" && childCount == 2) { + textFragmentAccumulator.push(indent); + textFragmentAccumulator.push( + elem.children[0].textContent.trim() + + ": " + + elem.children[1].textContent.trim() + ); + return; + } + + // Generate the text representation for each child node. + let node = elem.firstChild; + while (node) { + if (node.nodeType == Node.TEXT_NODE) { + // Text belonging to this element uses its indentation level. + generateTextForTextNode(node, indent, textFragmentAccumulator); + } else if (node.nodeType == Node.ELEMENT_NODE) { + // Recurse on the child element with an extra level of indentation (but + // only if there's more than one child). + generateTextForElement( + node, + aHidePrivateData, + indent + (childCount > 1 ? " " : ""), + textFragmentAccumulator + ); + } + // Advance! + node = node.nextSibling; + } +} + +function generateTextForTextNode(node, indent, textFragmentAccumulator) { + // If the text node is the first of a run of text nodes, then start + // a new line and add the initial indentation. + let prevNode = node.previousSibling; + if (!prevNode || prevNode.nodeType == Node.TEXT_NODE) { + textFragmentAccumulator.push("\n" + indent); + } + + // Trim the text node's text content and add proper indentation after + // any internal line breaks. + let text = node.textContent.trim().replace(/\n/g, "\n" + indent); + textFragmentAccumulator.push(text); +} + +/** + * Returns a plaintext representation of crashes data. + */ + +function getCrashesText(aIndent) { + let crashesData = ""; + let recentCrashesSubmitted = document.querySelectorAll("#crashes-tbody > tr"); + for (let i = 0; i < recentCrashesSubmitted.length; i++) { + let tds = recentCrashesSubmitted.item(i).querySelectorAll("td"); + crashesData += + aIndent.repeat(2) + + tds.item(0).firstElementChild.href + + " (" + + tds.item(1).textContent + + ")\n"; + } + return crashesData; +} diff --git a/comm/mail/components/about-support/content/libs.js b/comm/mail/components/about-support/content/libs.js new file mode 100644 index 0000000000..1c431596ae --- /dev/null +++ b/comm/mail/components/about-support/content/libs.js @@ -0,0 +1,24 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + BondOpenPGP: "chrome://openpgp/content/BondOpenPGP.jsm", +}); + +/** + * Populates the "Mail Libraries" section of the troubleshooting information page. + */ +function populateLibrarySection() { + let { min_version, loaded_version, status, path } = + BondOpenPGP.getRNPLibStatus(); + + document.getElementById("rnp-expected-version").textContent = min_version; + document.getElementById("rnp-loaded-version").textContent = loaded_version; + document.getElementById("rnp-path").textContent = path; + document.l10n.setAttributes(document.getElementById("rnp-status"), status); +} diff --git a/comm/mail/components/about-support/jar.mn b/comm/mail/components/about-support/jar.mn new file mode 100644 index 0000000000..4dd0375f7f --- /dev/null +++ b/comm/mail/components/about-support/jar.mn @@ -0,0 +1,13 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +messenger.jar: + content/messenger/about-support/accounts.js (content/accounts.js) + content/messenger/about-support/export.js (content/export.js) +* content/messenger/about-support/aboutSupport.xhtml (content/aboutSupport.xhtml) +% override chrome://global/content/aboutSupport.xhtml chrome://messenger/content/about-support/aboutSupport.xhtml + content/messenger/about-support/aboutSupport.js (content/aboutSupport.js) + content/messenger/about-support/calendars.js (content/calendars.js) + content/messenger/about-support/chat.js (content/chat.js) + content/messenger/about-support/libs.js (content/libs.js) diff --git a/comm/mail/components/about-support/moz.build b/comm/mail/components/about-support/moz.build new file mode 100644 index 0000000000..2c16234de4 --- /dev/null +++ b/comm/mail/components/about-support/moz.build @@ -0,0 +1,13 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows": + EXTRA_JS_MODULES += ["AboutSupportWin32.jsm"] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + EXTRA_JS_MODULES += ["AboutSupportMac.jsm"] +else: + EXTRA_JS_MODULES += ["AboutSupportUnix.jsm"] + +JAR_MANIFESTS += ["jar.mn"] diff --git a/comm/mail/components/accountcreation/AccountConfig.jsm b/comm/mail/components/accountcreation/AccountConfig.jsm new file mode 100644 index 0000000000..59b9604725 --- /dev/null +++ b/comm/mail/components/accountcreation/AccountConfig.jsm @@ -0,0 +1,463 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file creates the class AccountConfig, which is a JS object that holds + * a configuration for a certain account. It is *not* created in the backend + * yet (use aw-createAccount.js for that), and it may be incomplete. + * + * Several AccountConfig objects may co-exist, e.g. for autoconfig. + * One AccountConfig object is used to prefill and read the widgets + * in the Wizard UI. + * When we autoconfigure, we autoconfig writes the values into a + * new object and returns that, and the caller can copy these + * values into the object used by the UI. + * + * See also + * <https://wiki.mozilla.org/Thunderbird:Autoconfiguration:ConfigFileFormat> + * for values stored. + */ + +const EXPORTED_SYMBOLS = ["AccountConfig"]; + +const lazy = {}; + +ChromeUtils.defineModuleGetter( + lazy, + "AccountCreationUtils", + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); +ChromeUtils.defineModuleGetter( + lazy, + "Sanitizer", + "resource:///modules/accountcreation/Sanitizer.jsm" +); + +function AccountConfig() { + this.incoming = this.createNewIncoming(); + this.incomingAlternatives = []; + this.outgoing = this.createNewOutgoing(); + this.outgoingAlternatives = []; + this.identity = { + // displayed real name of user + realname: "%REALNAME%", + // email address of user, as shown in From of outgoing mails + emailAddress: "%EMAILADDRESS%", + }; + this.inputFields = []; + this.domains = []; +} +AccountConfig.prototype = { + // @see createNewIncoming() + incoming: null, + // @see createNewOutgoing() + outgoing: null, + /** + * Other servers which can be used instead of |incoming|, + * in order of decreasing preference. + * (|incoming| itself should not be included here.) + * { Array of incoming/createNewIncoming() } + */ + incomingAlternatives: null, + outgoingAlternatives: null, + // just an internal string to refer to this. Do not show to user. + id: null, + // who created the config. + // { one of kSource* } + source: null, + /** + * Used for telemetry purposes. + * - for kSourceXML, subSource is one of xml-from-{disk, db, isp-https, isp-http}. + * - for kSourceExchange, subSource is one of exchange-from-urlN[-guess]. + */ + subSource: null, + displayName: null, + // { Array of { varname (value without %), displayName, exampleValue } } + inputFields: null, + // email address domains for which this config is applicable + // { Array of Strings } + domains: null, + + /** + * Factory function for incoming and incomingAlternatives + */ + createNewIncoming() { + return { + // { String-enum: "pop3", "imap", "nntp", "exchange" } + type: null, + hostname: null, + // { Integer } + port: null, + // May be a placeholder (starts and ends with %). { String } + username: null, + password: null, + // {nsMsgSocketType} @see MailNewsTypes2.idl. -1 means not inited + socketType: -1, + /** + * true when the cert is invalid (and thus SSL useless), because it's + * 1) not from an accepted CA (including self-signed certs) + * 2) for a different hostname or + * 3) expired. + * May go back to false when user explicitly accepted the cert. + */ + badCert: false, + /** + * How to log in to the server: plaintext or encrypted pw, GSSAPI etc. + * Defined by Ci.nsMsgAuthMethod + * Same as server pref "authMethod". + */ + auth: 0, + /** + * Other auth methods that we think the server supports. + * They are ordered by descreasing preference. + * (|auth| itself is not included in |authAlternatives|) + * {Array of Ci.nsMsgAuthMethod} (same as .auth) + */ + authAlternatives: null, + // in minutes { Integer } + checkInterval: 10, + loginAtStartup: true, + // POP3 only: + // Not yet implemented. { Boolean } + useGlobalInbox: false, + leaveMessagesOnServer: true, + daysToLeaveMessagesOnServer: 14, + deleteByAgeFromServer: true, + // When user hits delete, delete from local store and from server + deleteOnServerWhenLocalDelete: true, + downloadOnBiff: true, + // Override `addThisServer` for a specific incoming server + useGlobalPreferredServer: false, + + // OAuth2 configuration, if needed. + oauthSettings: null, + + // for Microsoft Exchange servers. Optional. + owaURL: null, + ewsURL: null, + easURL: null, + // for when an addon overrides the account type. Optional. + addonAccountType: null, + }; + }, + /** + * Factory function for outgoing and outgoingAlternatives + */ + createNewOutgoing() { + return { + type: "smtp", + hostname: null, + port: null, // see incoming + username: null, // see incoming. may be null, if auth is 0. + password: null, // see incoming. may be null, if auth is 0. + socketType: -1, // see incoming + badCert: false, // see incoming + auth: 0, // see incoming + authAlternatives: null, // see incoming + addThisServer: true, // if we already have an SMTP server, add this + // if we already have an SMTP server, use it. + useGlobalPreferredServer: false, + // we should reuse an already configured SMTP server. + // nsISmtpServer.key + existingServerKey: null, + // user display value for existingServerKey + existingServerLabel: null, + + // OAuth2 configuration, if needed. + oauthSettings: null, + }; + }, + + /** + * The configuration needs an addon to handle the account type. + * The addon needs to be installed before the account can be created + * in the backend. + * You can choose one, if there are several addons in the list. + * (Optional) + * + * Array of: + * { + * id: "owl@example.com" {string}, + * + * // already localized string + * name: "Owl" {string}, + * + * // already localized string + * description: "A third party addon that allows you to connect to Exchange servers" {string} + * + * // Minimal version of the addon. Needed in case the addon is already installed, + * // to verify that the installed version is sufficient. + * // The XPI URL below must satisfy this. + * // Must satisfy <https://developer.mozilla.org/en-US/docs/Mozilla/Toolkit_version_format> + * minVersion: "0.2" {string} + * + * xpiURL: "https://live.thunderbird.net/autoconfig/owl.xpi" {URL}, + * websiteURL: "https://www.beonex.com/owl/" {URL}, + * icon32: "https://www.beonex.com/owl/owl-32x32.png" {URL}, + * + * useType : { + * // Type shown as radio button to user in the config result. + * // Users won't understand OWA vs. EWS vs. EAS etc., so this is an abstraction + * // from the end user perspective. + * generalType: "exchange" {string}, + * + * // Protocol + * // Independent of the addon + * protocolType: "owa" {string}, + * + * // Account type in the Thunderbird backend. + * // What nsIMsgAccount.type will be set to when creating the account. + * // This is specific to the addon. + * addonAccountType: "owl-owa" {string}, + * } + * } + */ + addons: null, + + /** + * Returns a deep copy of this object, + * i.e. modifying the copy will not affect the original object. + */ + copy() { + // Workaround: deepCopy() fails to preserve base obj (instanceof) + let result = new AccountConfig(); + for (let prop in this) { + result[prop] = lazy.AccountCreationUtils.deepCopy(this[prop]); + } + + return result; + }, + + isComplete() { + return ( + !!this.incoming.hostname && + !!this.incoming.port && + this.incoming.socketType != -1 && + !!this.incoming.auth && + !!this.incoming.username && + (!!this.outgoing.existingServerKey || + this.outgoing.useGlobalPreferredServer || + (!!this.outgoing.hostname && + !!this.outgoing.port && + this.outgoing.socketType != -1 && + !!this.outgoing.auth && + !!this.outgoing.username)) + ); + }, + + toString() { + function sslToString(socketType) { + switch (socketType) { + case 0: + return "plain"; + case 2: + return "STARTTLS"; + case 3: + return "SSL"; + default: + return "invalid"; + } + } + + function authToString(authMethod) { + switch (authMethod) { + case 0: + return "undefined"; + case 1: + return "none"; + case 2: + return "old plain"; + case 3: + return "plain"; + case 4: + return "encrypted"; + case 5: + return "Kerberos"; + case 6: + return "NTLM"; + case 7: + return "external/SSL"; + case 8: + return "any secure"; + case 10: + return "OAuth2"; + default: + return "invalid"; + } + } + + function passwordToString(password) { + return password ? "set" : "not set"; + } + + function configToString(config) { + return ( + config.type + + ", " + + config.hostname + + ":" + + config.port + + ", " + + sslToString(config.socketType) + + ", auth: " + + authToString(config.auth) + + ", username: " + + (config.username || "(undefined)") + + ", password: " + + passwordToString(config.password) + ); + } + + let result = "Incoming: " + configToString(this.incoming) + "\nOutgoing: "; + if ( + this.outgoing.useGlobalPreferredServer || + this.incoming.useGlobalPreferredServer + ) { + result += "Use global server"; + } else if (this.outgoing.existingServerKey) { + result += "Use existing server " + this.outgoing.existingServerKey; + } else { + result += configToString(this.outgoing); + } + for (let config of this.incomingAlternatives) { + result += "\nIncoming alt: " + configToString(config); + } + for (let config of this.outgoingAlternatives) { + result += "\nOutgoing alt: " + configToString(config); + } + return result; + }, + + /** + * Sort the config alternatives such that exchange is the last of the + * alternatives. + */ + preferStandardProtocols() { + let alternatives = this.incomingAlternatives; + // Add default incoming as one alternative. + alternatives.unshift(this.incoming); + alternatives.sort((a, b) => { + if (a.type == "exchange") { + return 1; + } + if (b.type == "exchange") { + return -1; + } + return 0; + }); + this.incomingAlternatives = alternatives; + this.incoming = alternatives.shift(); + }, +}; + +// enum consts + +// .source +AccountConfig.kSourceUser = "user"; // user manually entered the config +AccountConfig.kSourceXML = "xml"; // config from XML from ISP or Mozilla DB +AccountConfig.kSourceGuess = "guess"; // guessConfig() +AccountConfig.kSourceExchange = "exchange"; // from Microsoft Exchange AutoDiscover + +/** + * Some fields on the account config accept placeholders (when coming from XML). + * + * These are the predefined ones + * %EMAILADDRESS% (full email address of the user, usually entered by user) + * %EMAILLOCALPART% (email address, part before @) + * %EMAILDOMAIN% (email address, part after @) + * %REALNAME% + * as well as those defined in account.inputFields.*.varname, with % added + * before and after. + * + * These must replaced with real values, supplied by the user or app, + * before the account is created. This is done here. You call this function once + * you have all the data - gathered the standard vars mentioned above as well as + * all listed in account.inputFields, and pass them in here. This function will + * insert them in the fields, returning a fully filled-out account ready to be + * created. + * + * @param account {AccountConfig} + * The account data to be modified. It may or may not contain placeholders. + * After this function, it should not contain placeholders anymore. + * This object will be modified in-place. + * + * @param emailfull {String} + * Full email address of this account, e.g. "joe@example.com". + * Empty of incomplete email addresses will/may be rejected. + * + * @param realname {String} + * Real name of user, as will appear in From of outgoing messages + * + * @param password {String} + * The password for the incoming server and (if necessary) the outgoing server + */ +AccountConfig.replaceVariables = function ( + account, + realname, + emailfull, + password +) { + lazy.Sanitizer.nonemptystring(emailfull); + let emailsplit = emailfull.split("@"); + lazy.AccountCreationUtils.assert( + emailsplit.length == 2, + "email address not in expected format: must contain exactly one @" + ); + let emaillocal = lazy.Sanitizer.nonemptystring(emailsplit[0]); + let emaildomain = lazy.Sanitizer.hostname(emailsplit[1]); + lazy.Sanitizer.label(realname); + lazy.Sanitizer.nonemptystring(realname); + + let otherVariables = {}; + otherVariables.EMAILADDRESS = emailfull; + otherVariables.EMAILLOCALPART = emaillocal; + otherVariables.EMAILDOMAIN = emaildomain; + otherVariables.REALNAME = realname; + + if (password) { + account.incoming.password = password; + account.outgoing.password = password; // set member only if auth required? + } + account.incoming.username = _replaceVariable( + account.incoming.username, + otherVariables + ); + account.outgoing.username = _replaceVariable( + account.outgoing.username, + otherVariables + ); + account.incoming.hostname = _replaceVariable( + account.incoming.hostname, + otherVariables + ); + if (account.outgoing.hostname) { + // will be null if user picked existing server. + account.outgoing.hostname = _replaceVariable( + account.outgoing.hostname, + otherVariables + ); + } + account.identity.realname = _replaceVariable( + account.identity.realname, + otherVariables + ); + account.identity.emailAddress = _replaceVariable( + account.identity.emailAddress, + otherVariables + ); + account.displayName = _replaceVariable(account.displayName, otherVariables); +}; + +function _replaceVariable(variable, values) { + let str = variable; + if (typeof str != "string") { + return str; + } + + for (let varname in values) { + str = str.replace("%" + varname + "%", values[varname]); + } + + return str; +} diff --git a/comm/mail/components/accountcreation/AccountCreationUtils.jsm b/comm/mail/components/accountcreation/AccountCreationUtils.jsm new file mode 100644 index 0000000000..f4efb96b2d --- /dev/null +++ b/comm/mail/components/accountcreation/AccountCreationUtils.jsm @@ -0,0 +1,717 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +/** + * Some common, generic functions + */ + +const EXPORTED_SYMBOLS = ["AccountCreationUtils"]; + +const lazy = {}; + +ChromeUtils.defineModuleGetter( + lazy, + "Sanitizer", + "resource:///modules/accountcreation/Sanitizer.jsm" +); + +const { AddonManager } = ChromeUtils.importESModule( + "resource://gre/modules/AddonManager.sys.mjs" +); +const { ConsoleAPI } = ChromeUtils.importESModule( + "resource://gre/modules/Console.sys.mjs" +); +const { clearInterval, clearTimeout, setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +// -------------------------- +// Low level, basic functions + +function assert(test, errorMsg) { + if (!test) { + throw new NotReached( + errorMsg ? errorMsg : "Programming bug. Assertion failed, see log." + ); + } +} + +function makeCallback(obj, func) { + return func.bind(obj); +} + +/** + * Runs the given function sometime later + * + * Currently implemented using setTimeout(), but + * can later be replaced with an nsITimer impl, + * when code wants to use it in a module. + * + * @see |TimeoutAbortable| + */ +function runAsync(func) { + return setTimeout(func, 0); +} + +/** + * Reads UTF8 data from a URL. + * + * @param uri {nsIURI} - what you want to read + * @returns {Array of String} the contents of the file, one string per line + */ +function readURLasUTF8(uri) { + assert(uri instanceof Ci.nsIURI, "uri must be an nsIURI"); + let chan = Services.io.newChannelFromURI( + uri, + null, + Services.scriptSecurityManager.getSystemPrincipal(), + null, + Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + Ci.nsIContentPolicy.TYPE_OTHER + ); + let is = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance( + Ci.nsIConverterInputStream + ); + is.init( + chan.open(), + "UTF-8", + 1024, + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER + ); + + let content = ""; + let strOut = {}; + try { + while (is.readString(1024, strOut) != 0) { + content += strOut.value; + } + } finally { + is.close(); + } + + return content; + // TODO this has a numeric error message. We need to ship translations + // into human language. +} + +/** + * @param bundleURI {String} - chrome URL to properties file + * @returns nsIStringBundle + */ +function getStringBundle(bundleURI) { + try { + return Services.strings.createBundle(bundleURI); + } catch (e) { + throw new Exception( + "Failed to get stringbundle URI <" + bundleURI + ">. Error: " + e + ); + } +} + +// --------- +// Exception + +function Exception(msg) { + this._message = msg; + this.stack = Components.stack.formattedStack; +} +Exception.prototype = { + get message() { + return this._message; + }, + toString() { + return this._message; + }, +}; + +function NotReached(msg) { + Exception.call(this, msg); // call super constructor + console.error(this); +} +// Make NotReached extend Exception. +NotReached.prototype = Object.create(Exception.prototype); +NotReached.prototype.constructor = NotReached; + +// --------- +// Abortable + +/** + * A handle for an async function which you can cancel. + * The async function will return an object of this type (a subtype) + * and you can call cancel() when you feel like killing the function. + */ +function Abortable() {} +Abortable.prototype = { + cancel(e) {}, +}; + +function CancelledException(msg) { + Exception.call(this, msg); +} +CancelledException.prototype = Object.create(Exception.prototype); +CancelledException.prototype.constructor = CancelledException; + +function UserCancelledException(msg) { + // The user knows they cancelled so I don't see a need + // for a message to that effect. + if (!msg) { + msg = "User cancelled"; + } + CancelledException.call(this, msg); +} +UserCancelledException.prototype = Object.create(CancelledException.prototype); +UserCancelledException.prototype.constructor = UserCancelledException; + +/** + * Utility implementation, for waiting for a promise to resolve, + * but allowing its result to be cancelled. + */ +function PromiseAbortable(promise, successCallback, errorCallback) { + Abortable.call(this); // call super constructor + let complete = false; + this.cancel = function (e) { + if (!complete) { + complete = true; + errorCallback(e || new CancelledException()); + } + }; + promise + .then(function (result) { + if (!complete) { + successCallback(result); + complete = true; + } + }) + .catch(function (e) { + if (!complete) { + complete = true; + errorCallback(e); + } + }); +} +PromiseAbortable.prototype = Object.create(Abortable.prototype); +PromiseAbortable.prototype.constructor = PromiseAbortable; + +/** + * Utility implementation, for allowing to abort a setTimeout. + * Use like: return new TimeoutAbortable(setTimeout(function(){ ... }, 0)); + * + * @param setTimeoutID {Integer} - Return value of setTimeout() + */ +function TimeoutAbortable(setTimeoutID) { + Abortable.call(this); // call super constructor + this._id = setTimeoutID; +} +TimeoutAbortable.prototype = Object.create(Abortable.prototype); +TimeoutAbortable.prototype.constructor = TimeoutAbortable; +TimeoutAbortable.prototype.cancel = function () { + clearTimeout(this._id); +}; + +/** + * Utility implementation, for allowing to abort a setTimeout. + * Use like: return new TimeoutAbortable(setTimeout(function(){ ... }, 0)); + * + * @param setIntervalID {Integer} - Return value of setInterval() + */ +function IntervalAbortable(setIntervalID) { + Abortable.call(this); // call super constructor + this._id = setIntervalID; +} +IntervalAbortable.prototype = Object.create(Abortable.prototype); +IntervalAbortable.prototype.constructor = IntervalAbortable; +IntervalAbortable.prototype.cancel = function () { + clearInterval(this._id); +}; + +/** + * Allows you to make several network calls, + * but return only one |Abortable| object. + */ +function SuccessiveAbortable() { + Abortable.call(this); // call super constructor + this._current = null; +} +SuccessiveAbortable.prototype = { + __proto__: Abortable.prototype, + get current() { + return this._current; + }, + set current(abortable) { + assert( + abortable instanceof Abortable || abortable == null, + "need an Abortable object (or null)" + ); + this._current = abortable; + }, + cancel(e) { + if (this._current) { + this._current.cancel(e); + } + }, +}; + +/** + * Allows you to make several network calls in parallel. + */ +function ParallelAbortable() { + Abortable.call(this); // call super constructor + // { Array of ParallelCall } + this._calls = []; + // { Array of Function } + this._finishedObservers = []; +} +ParallelAbortable.prototype = { + __proto__: Abortable.prototype, + /** + * @returns {Array of ParallelCall} + */ + get results() { + return this._calls; + }, + /** + * @returns {ParallelCall} + */ + addCall() { + let call = new ParallelCall(this); + call.position = this._calls.length; + this._calls.push(call); + return call; + }, + /** + * Observers will be called once one of the functions + * finishes, i.e. returns successfully or fails. + * + * @param {Function({ParallelCall} call)} func + */ + addOneFinishedObserver(func) { + assert(typeof func == "function"); + this._finishedObservers.push(func); + }, + /** + * Will be called once *all* of the functions finished, + * It gives you a list of all functions that succeeded or failed, + * respectively. + * + * @param {Function( + * {Array of ParallelCall} succeeded, + * {Array of ParallelCall} failed + * )} func + */ + addAllFinishedObserver(func) { + assert(typeof func == "function"); + this.addOneFinishedObserver(() => { + if (this._calls.some(call => !call.finished)) { + return; + } + let succeeded = this._calls.filter(call => call.succeeded); + let failed = this._calls.filter(call => !call.succeeded); + func(succeeded, failed); + }); + }, + _notifyFinished(call) { + for (let observer of this._finishedObservers) { + try { + observer(call); + } catch (e) { + console.error(e); + } + } + }, + cancel(e) { + for (let call of this._calls) { + if (!call.finished && call.callerAbortable) { + call.callerAbortable.cancel(e); + } + } + }, +}; + +/** + * Returned by ParallelAbortable.addCall(). + * Do not create this object directly + * + * @param {ParallelAbortable} parallelAbortable - The controlling ParallelAbortable + */ +function ParallelCall(parallelAbortable) { + assert(parallelAbortable instanceof ParallelAbortable); + // {ParallelAbortable} the parent + this._parallelAbortable = parallelAbortable; + // {Abortable} Abortable of the caller function that should run in parallel + this.callerAbortable = null; + // {Integer} the order in which the function was added, and its priority + this.position = null; + // {boolean} false = running, pending, false = success or failure + this.finished = false; + // {boolean} if finished: true = returned with success, false = returned with error + this.succeeded = false; + // {Exception} if failed: the error or exception that the caller function returned + this.e = null; + // {Object} if succeeded: the result of the caller function + this.result = null; + + this._time = Date.now(); +} +ParallelCall.prototype = { + /** + * Returns a successCallback(result) function that you pass + * to your function that runs in parallel. + * + * @returns {Function(result)} successCallback + */ + successCallback() { + return result => { + ddump( + "call " + + this.position + + " took " + + (Date.now() - this._time) + + "ms and succeeded" + + (this.callerAbortable && this.callerAbortable._url + ? " at <" + this.callerAbortable._url + ">" + : "") + ); + this.result = result; + this.finished = true; + this.succeeded = true; + this._parallelAbortable._notifyFinished(this); + }; + }, + /** + * Returns an errorCallback(e) function that you pass + * to your function that runs in parallel. + * + * @returns {Function(e)} errorCallback + */ + errorCallback() { + return e => { + ddump( + "call " + + this.position + + " took " + + (Date.now() - this._time) + + "ms and failed with " + + (typeof e.code == "number" ? e.code + " " : "") + + (e.toString() + ? e.toString() + : "unknown error, probably no host connection") + + (this.callerAbortable && this.callerAbortable._url + ? " at <" + this.callerAbortable._url + ">" + : "") + ); + this.e = e; + this.finished = true; + this.succeeded = false; + this._parallelAbortable._notifyFinished(this); + }; + }, + /** + * Call your function that needs to run in parallel + * and pass the resulting |Abortable| of your function here. + * + * @param {Abortable} abortable + */ + setAbortable(abortable) { + assert(abortable instanceof Abortable); + this.callerAbortable = abortable; + }, +}; + +/** + * Runs several calls in parallel. + * Returns the result of the "highest" priority call that succeeds. + * Unlike Promise.race(), does not return the fastest, + * but the first in the order they were added. + * So, the order in which the calls were added determines their priority, + * with the first to be added being the most desirable. + * + * E.g. the first failed, the second is pending, the third succeeded, and the forth is pending. + * It aborts the forth (because the third succeeded), and it waits for the second to return. + * If the second succeeds, it is the result, otherwise the third is the result. + * + * @param {Function( + * {Object} result - Result of winner call + * {ParallelCall} call - Winner call info + * )} successCallback - A call returned successfully + * @param {Function(e, allErrors)} errorCallback - All calls failed. + * {Exception} e - The first CancelledException, and otherwise + * the exception returned by the first call. + * This is just to adhere to the standard API of errorCallback(e). + * {Array of Exception} allErrors - The exceptions from all calls. + */ +function PriorityOrderAbortable(successCallback, errorCallback) { + assert(typeof successCallback == "function"); + assert(typeof errorCallback == "function"); + ParallelAbortable.call(this); // call super constructor + this._successfulCall = null; + + this.addOneFinishedObserver(finishedCall => { + for (let call of this._calls) { + if (!call.finished) { + if (this._successfulCall) { + // abort + if (call.callerAbortable) { + call.callerAbortable.cancel( + new NoLongerNeededException("Another higher call succeeded") + ); + } + continue; + } + // It's pending. do nothing and wait for it. + return; + } + if (!call.succeeded) { + // it failed. ignore it. + continue; + } + if (this._successfulCall) { + // we already have a winner. ignore it. + continue; + } + try { + successCallback(call.result, call); + // This is the winner. + this._successfulCall = call; + } catch (e) { + console.error(e); + // If the handler failed with this data, treat this call as failed. + call.e = e; + call.succeeded = false; + } + } + if (!this._successfulCall) { + // all failed + let allErrors = this._calls.map(call => call.e); + let e = + allErrors.find(e => e instanceof CancelledException) || allErrors[0]; + errorCallback(e, allErrors); // see docs above + } + }); +} +PriorityOrderAbortable.prototype = Object.create(ParallelAbortable.prototype); +PriorityOrderAbortable.prototype.constructor = PriorityOrderAbortable; + +function NoLongerNeededException(msg) { + CancelledException.call(this, msg); +} +NoLongerNeededException.prototype = Object.create(CancelledException.prototype); +NoLongerNeededException.prototype.constructor = NoLongerNeededException; + +// ------------------- +// High level features + +/** + * Allows you to install an addon. + * + * Example: + * var installer = new AddonInstaller({ xpiURL : "https://...xpi", id: "...", ...}); + * installer.install(); + * + * @param {object} args - Contains parameters: + * @param {string} name (Optional) - Name of the addon (not important) + * @param {string} id (Optional) - Addon ID + * If you pass an ID, and the addon is already installed (and the version matches), + * then install() will do nothing. + * After the XPI is downloaded, the ID will be verified. If it doesn't match, the + * install will fail. + * If you don't pass an ID, these checks will be skipped and the addon be installed + * unconditionally. + * It is recommended to pass at least an ID, because it can confuse some addons + * to be reloaded at runtime. + * @param {string} minVersion (Optional) - Minimum version of the addon + * If you pass a minVersion (in addition to ID), and the installed addon is older than this, + * the install will be done anyway. If the downloaded addon has a lower version, + * the install will fail. + * If you do not pass a minVersion, there will be no version check. + * @param {URL} xpiURL - Where to download the XPI from + */ +function AddonInstaller(args) { + Abortable.call(this); + this._name = lazy.Sanitizer.label(args.name); + this._id = lazy.Sanitizer.string(args.id); + this._minVersion = lazy.Sanitizer.string(args.minVersion); + this._url = lazy.Sanitizer.url(args.xpiURL); +} +AddonInstaller.prototype = Object.create(Abortable.prototype); +AddonInstaller.prototype.constructor = AddonInstaller; + +/** + * Checks whether the passed-in addon matches the + * id and minVersion requested by the caller. + * + * @param {nsIAddon} addon + * @returns {boolean} is OK + */ +AddonInstaller.prototype.matches = function (addon) { + return ( + !this._id || + (this._id == addon.id && + (!this._minVersion || + Services.vc.compare(addon.version, this._minVersion) >= 0)) + ); +}; + +/** + * Start the installation + * + * @throws Exception in case of failure + */ +AddonInstaller.prototype.install = async function () { + if (await this.isInstalled()) { + return; + } + await this._installDirect(); +}; + +/** + * Checks whether we already have an addon installed that matches the + * id and minVersion requested by the caller. + * + * @returns {boolean} is already installed and enabled + */ +AddonInstaller.prototype.isInstalled = async function () { + if (!this._id) { + return false; + } + var addon = await AddonManager.getAddonByID(this._id); + return addon && this.matches(addon) && addon.isActive; +}; + +/** + * Checks whether we already have an addon but it is disabled. + * + * @returns {boolean} is already installed but disabled + */ +AddonInstaller.prototype.isDisabled = async function () { + if (!this._id) { + return false; + } + let addon = await AddonManager.getAddonByID(this._id); + return addon && !addon.isActive; +}; + +/** + * Downloads and installs the addon. + * The downloaded XPI will be checked using prompt(). + */ +AddonInstaller.prototype._installDirect = async function () { + var installer = (this._installer = await AddonManager.getInstallForURL( + this._url, + { name: this._name } + )); + installer.promptHandler = makeCallback(this, this.prompt); + await installer.install(); // throws, if failed + + var addon = await AddonManager.getAddonByID(this._id); + await addon.enable(); + + // Wait for addon startup code to finish + // Fixes: verify password fails with NOT_AVAILABLE in createIncomingServer() + if ("startupPromise" in addon) { + await addon.startupPromise; + } + let wait = ms => new Promise(resolve => setTimeout(resolve, ms)); + await wait(1000); +}; + +/** + * Install confirmation. You may override this, if needed. + * + * @throws Exception If you want to cancel install, then throw an exception. + */ +AddonInstaller.prototype.prompt = async function (info) { + if (!this.matches(info.addon)) { + // happens only when we got the wrong XPI + throw new Exception( + "The downloaded addon XPI does not match the minimum requirements" + ); + } +}; + +AddonInstaller.prototype.cancel = function () { + if (this._installer) { + try { + this._installer.cancel(); + } catch (e) { + // if install failed + ddump(e); + } + } +}; + +// ------------ +// Debug output + +function deepCopy(org) { + if (typeof org == "undefined") { + return undefined; + } + if (org == null) { + return null; + } + if (typeof org == "string") { + return org; + } + if (typeof org == "number") { + return org; + } + if (typeof org == "boolean") { + return org; + } + if (typeof org == "function") { + return org; + } + if (typeof org != "object") { + throw new Error("can't copy objects of type " + typeof org + " yet"); + } + + // TODO still instanceof org != instanceof copy + // var result = new org.constructor(); + var result = {}; + if (typeof org.length != "undefined") { + result = []; + } + for (var prop in org) { + result[prop] = deepCopy(org[prop]); + } + return result; +} + +var gAccountSetupLogger = new ConsoleAPI({ + prefix: "mail.setup", + maxLogLevel: "warn", + maxLogLevelPref: "mail.setup.loglevel", +}); + +function ddump(text) { + gAccountSetupLogger.info(text); +} + +function alertPrompt(alertTitle, alertMsg) { + Services.prompt.alert( + Services.wm.getMostRecentWindow(""), + alertTitle, + alertMsg + ); +} + +var AccountCreationUtils = { + Abortable, + AddonInstaller, + alertPrompt, + assert, + CancelledException, + ddump, + deepCopy, + Exception, + gAccountSetupLogger, + getStringBundle, + NotReached, + PriorityOrderAbortable, + PromiseAbortable, + readURLasUTF8, + runAsync, + SuccessiveAbortable, + TimeoutAbortable, + UserCancelledException, +}; diff --git a/comm/mail/components/accountcreation/ConfigVerifier.jsm b/comm/mail/components/accountcreation/ConfigVerifier.jsm new file mode 100644 index 0000000000..cce934a159 --- /dev/null +++ b/comm/mail/components/accountcreation/ConfigVerifier.jsm @@ -0,0 +1,386 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["ConfigVerifier"]; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { OAuth2Providers } = ChromeUtils.import( + "resource:///modules/OAuth2Providers.jsm" +); +const { AccountCreationUtils } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); + +/** + * @implements {nsIUrlListener} + * @implements {nsIInterfaceRequestor} + */ +class ConfigVerifier { + QueryInterface = ChromeUtils.generateQI([ + "nsIInterfaceRequestor", + "nsIUrlListener", + ]); + + // @see {nsIInterfaceRequestor} + getInterface(iid) { + return this.QueryInterface(iid); + } + + constructor(msgWindow) { + this.msgWindow = msgWindow; + this._log = console.createInstance({ + prefix: "mail.setup", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.setup.loglevel", + }); + } + + /** + * @param {nsIURI} url - The URL being processed. + * @see {nsIUrlListener} + */ + OnStartRunningUrl(url) { + this._log.debug(`Starting to verify configuration; + email as username=${ + this.config.incoming.username != this.config.identity.emailAddress + } + savedUsername=${this.config.usernameSaved ? "true" : "false"}, + authMethod=${this.server.authMethod}`); + } + + /** + * @param {nsIURI} url - The URL being processed. + * @param {nsresult} status - A result code of URL processing. + * @see {nsIUrlListener} + */ + OnStopRunningUrl(url, status) { + if (Components.isSuccessCode(status)) { + this._log.debug(`Configuration verified successfully!`); + this.cleanup(); + this.successCallback(this.config); + return; + } + + this._log.debug(`Verifying configuration failed; status=${status}`); + + let certError = false; + try { + let nssErrorsService = Cc["@mozilla.org/nss_errors_service;1"].getService( + Ci.nsINSSErrorsService + ); + let errorClass = nssErrorsService.getErrorClass(status); + if (errorClass == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) { + certError = true; + } + } catch (e) { + // It's not an NSS error. + } + + if (certError) { + let mailNewsUrl = url.QueryInterface(Ci.nsIMsgMailNewsUrl); + let secInfo = mailNewsUrl.failedSecInfo; + this.informUserOfCertError(secInfo, url.asciiHostPort); + } else if (this.alter) { + // Try other variations. + this.server.closeCachedConnections(); + this.tryNextLogon(url); + } else { + // Logon failed, and we aren't supposed to try other variations. + this._failed(url); + } + } + + tryNextLogon(aPreviousUrl) { + this._log.debug("Trying next logon variation"); + // check if we tried full email address as username + if (this.config.incoming.username != this.config.identity.emailAddress) { + this._log.debug("Changing username to email address."); + this.config.usernameSaved = this.config.incoming.username; + this.config.incoming.username = this.config.identity.emailAddress; + this.config.outgoing.username = this.config.identity.emailAddress; + this.server.username = this.config.incoming.username; + this.server.password = this.config.incoming.password; + this.verifyLogon(); + return; + } + + if (this.config.usernameSaved) { + this._log.debug("Re-setting username."); + // If we tried the full email address as the username, then let's go + // back to trying just the username before trying the other cases. + this.config.incoming.username = this.config.usernameSaved; + this.config.outgoing.username = this.config.usernameSaved; + this.config.usernameSaved = null; + this.server.username = this.config.incoming.username; + this.server.password = this.config.incoming.password; + } + + // sec auth seems to have failed, and we've tried both + // varieties of user name, sadly. + // So fall back to non-secure auth, and + // again try the user name and email address as username + if (this.server.socketType == Ci.nsMsgSocketType.SSL) { + this._log.debug("Using SSL"); + } else if (this.server.socketType == Ci.nsMsgSocketType.alwaysSTARTTLS) { + this._log.debug("Using STARTTLS"); + } + if ( + this.config.incoming.authAlternatives && + this.config.incoming.authAlternatives.length + ) { + // We may be dropping back to insecure auth methods here, + // which is not good. But then again, we already warned the user, + // if it is a config without SSL. + + let brokenAuth = this.config.incoming.auth; + // take the next best method (compare chooseBestAuthMethod() in guess) + this.config.incoming.auth = this.config.incoming.authAlternatives.shift(); + this.server.authMethod = this.config.incoming.auth; + // Assume that SMTP server has same methods working as incoming. + // Broken assumption, but we currently have no SMTP verification. + // TODO: implement real SMTP verification + if ( + this.config.outgoing.auth == brokenAuth && + this.config.outgoing.authAlternatives.includes( + this.config.incoming.auth + ) + ) { + this.config.outgoing.auth = this.config.incoming.auth; + } + this._log.debug(`Trying next auth method: ${this.server.authMethod}`); + this.verifyLogon(); + return; + } + + // Tried all variations we can. Give up. + this._log.debug("Have tried all variations. Giving up."); + this._failed(aPreviousUrl); + } + + /** + * Clear out the server we had created for use during testing. + */ + cleanup() { + try { + if (this.server) { + MailServices.accounts.removeIncomingServer(this.server, true); + this.server = null; + } + } catch (e) { + this._log.error(e); + } + } + + /** + * @param {nsIURI} url - The URL being processed. + */ + _failed(url) { + this.cleanup(); + url = url.QueryInterface(Ci.nsIMsgMailNewsUrl); + let code = url.errorCode || "login-error-unknown"; + let msg = url.errorMessage; + // *Only* for known (!) username/password errors, show our message. + // But there are 1000 other reasons why it could have failed, e.g. + // server not reachable, bad auth method, server hiccups, or even + // custom server messages that tell the user to do something, + // so show the backend error message, unless we are certain + // that it's a wrong username or password. + if ( + !msg || // Normal IMAP login error sets no error msg + code == "pop3UsernameFailure" || + code == "pop3PasswordFailed" || + code == "imapOAuth2Error" + ) { + msg = AccountCreationUtils.getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties" + ).GetStringFromName("cannot_login.error"); + } + this.errorCallback(new Error(msg)); + } + + /** + * Inform users that we got a certificate error for the specified location. + * Allow them to add an exception for it. + * + * @param {nsITransportSecurityInfo} secInfo + * @param {string} location - "host:port" that had the problem. + */ + informUserOfCertError(secInfo, location) { + this._log.debug(`Informing user about cert error for ${location}`); + let params = { + exceptionAdded: false, + securityInfo: secInfo, + prefetchCert: true, + location, + }; + Services.wm + .getMostRecentWindow("mail:3pane") + .browsingContext.topChromeWindow.openDialog( + "chrome://pippki/content/exceptionDialog.xhtml", + "exceptionDialog", + "chrome,centerscreen,modal", + params + ); + if (!params.exceptionAdded) { + this._log.debug(`Did not accept exception for ${location}`); + this.cleanup(); + let errorMsg = AccountCreationUtils.getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties" + ).GetStringFromName("cannot_login.error"); + this.errorCallback(new Error(errorMsg)); + } else { + this._log.debug(`Accept exception for ${location} - will retry logon.`); + // Retry the logon now that we've added the cert exception. + this.verifyLogon(); + } + } + + /** + * This checks a given config, by trying a real connection and login, + * with username and password. + * + * @param {AccountConfig} config - The guessed account config. + * username, password, realname, emailaddress etc. are not filled out, + * but placeholders to be filled out via replaceVariables(). + * @param alter {boolean} - Try other usernames and login schemes, until + * login works. Warning: Modifies |config|. + * @returns {Promise<AccountConfig>} the successful configuration. + * @throws {Error} when we could guess not the config, either + * because we have not found anything or because there was an error + * (e.g. no network connection). + * The ex.message will contain a user-presentable message. + */ + async verifyConfig(config, alter) { + this.alter = alter; + return new Promise((resolve, reject) => { + this.config = config; + this.successCallback = resolve; + this.errorCallback = reject; + if ( + MailServices.accounts.findServer( + config.incoming.username, + config.incoming.hostname, + config.incoming.type, + config.incoming.port + ) + ) { + reject(new Error("Incoming server exists")); + return; + } + + // incoming server + if (!this.server) { + this.server = MailServices.accounts.createIncomingServer( + config.incoming.username, + config.incoming.hostname, + config.incoming.type + ); + } + this.server.port = config.incoming.port; + this.server.password = config.incoming.password; + this.server.socketType = config.incoming.socketType; + + this._log.info( + "Setting incoming server authMethod to " + config.incoming.auth + ); + this.server.authMethod = config.incoming.auth; + + try { + // Lookup OAuth2 issuer if needed. + // -- Incoming. + if ( + config.incoming.auth == Ci.nsMsgAuthMethod.OAuth2 && + (!config.incoming.oauthSettings || + !config.incoming.oauthSettings.issuer || + !config.incoming.oauthSettings.scope) + ) { + let details = OAuth2Providers.getHostnameDetails( + config.incoming.hostname + ); + if (!details) { + reject( + new Error( + `Could not get OAuth2 details for hostname=${config.incoming.hostname}.` + ) + ); + } + config.incoming.oauthSettings = { + issuer: details[0], + scope: details[1], + }; + } + // -- Outgoing. + if ( + config.outgoing.auth == Ci.nsMsgAuthMethod.OAuth2 && + (!config.outgoing.oauthSettings || + !config.outgoing.oauthSettings.issuer || + !config.outgoing.oauthSettings.scope) + ) { + let details = OAuth2Providers.getHostnameDetails( + config.outgoing.hostname + ); + if (!details) { + reject( + new Error( + `Could not get OAuth2 details for hostname=${config.outgoing.hostname}.` + ) + ); + } + config.outgoing.oauthSettings = { + issuer: details[0], + scope: details[1], + }; + } + if (config.incoming.owaURL) { + this.server.setUnicharValue("owa_url", config.incoming.owaURL); + } + if (config.incoming.ewsURL) { + this.server.setUnicharValue("ews_url", config.incoming.ewsURL); + } + if (config.incoming.easURL) { + this.server.setUnicharValue("eas_url", config.incoming.easURL); + } + + if ( + this.server.password || + this.server.authMethod == Ci.nsMsgAuthMethod.OAuth2 + ) { + this.verifyLogon(); + } else { + this.cleanup(); + resolve(config); + } + } catch (e) { + this._log.info("verifyConfig failed: " + e); + this.cleanup(); + reject(e); + } + }); + } + + /** + * Verify that the provided credentials can log in to the incoming server. + */ + verifyLogon() { + this._log.info("verifyLogon for server at " + this.server.hostName); + // Save away the old callbacks. + let saveCallbacks = this.msgWindow.notificationCallbacks; + // Set our own callbacks - this works because verifyLogon will + // synchronously create the transport and use the notification callbacks. + // Our listener listens both for the url and cert errors. + this.msgWindow.notificationCallbacks = this; + // try to work around bug where backend is clearing password. + try { + this.server.password = this.config.incoming.password; + let uri = this.server.verifyLogon(this, this.msgWindow); + // clear msgWindow so url won't prompt for passwords. + uri.QueryInterface(Ci.nsIMsgMailNewsUrl).msgWindow = null; + } finally { + // restore them + this.msgWindow.notificationCallbacks = saveCallbacks; + } + } +} diff --git a/comm/mail/components/accountcreation/CreateInBackend.jsm b/comm/mail/components/accountcreation/CreateInBackend.jsm new file mode 100644 index 0000000000..c254bbb44b --- /dev/null +++ b/comm/mail/components/accountcreation/CreateInBackend.jsm @@ -0,0 +1,459 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["CreateInBackend"]; + +const lazy = {}; + +ChromeUtils.defineModuleGetter( + lazy, + "AccountConfig", + "resource:///modules/accountcreation/AccountConfig.jsm" +); +ChromeUtils.defineModuleGetter( + lazy, + "AccountCreationUtils", + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/* eslint-disable complexity */ +/** + * Takes an |AccountConfig| JS object and creates that account in the + * Thunderbird backend (which also writes it to prefs). + * + * @param {AccountConfig} config - The account to create + * @returns {nsIMsgAccount} - the newly created account + */ +function createAccountInBackend(config) { + // incoming server + let inServer = MailServices.accounts.createIncomingServer( + config.incoming.username, + config.incoming.hostname, + config.incoming.type + ); + inServer.port = config.incoming.port; + inServer.authMethod = config.incoming.auth; + inServer.password = config.incoming.password; + // This new CLIENTID is for the outgoing server, and will be applied to the + // incoming only if the incoming username and hostname match the outgoing. + // We must generate this unconditionally because we cannot determine whether + // the outgoing server has clientid enabled yet or not, and we need to do it + // here in order to populate the incoming server if the outgoing matches. + let newOutgoingClientid = Services.uuid + .generateUUID() + .toString() + .replace(/[{}]/g, ""); + // Grab the base domain of both incoming and outgoing hostname in order to + // compare the two to detect if the base domain is the same. + let incomingBaseDomain; + let outgoingBaseDomain; + try { + incomingBaseDomain = Services.eTLD.getBaseDomainFromHost( + config.incoming.hostname + ); + } catch (e) { + incomingBaseDomain = config.incoming.hostname; + } + try { + outgoingBaseDomain = Services.eTLD.getBaseDomainFromHost( + config.outgoing.hostname + ); + } catch (e) { + outgoingBaseDomain = config.outgoing.hostname; + } + if ( + config.incoming.username == config.outgoing.username && + incomingBaseDomain == outgoingBaseDomain + ) { + inServer.clientid = newOutgoingClientid; + } else { + // If the username/hostname are different then generate a new CLIENTID. + inServer.clientid = Services.uuid + .generateUUID() + .toString() + .replace(/[{}]/g, ""); + } + + if (config.rememberPassword && config.incoming.password) { + rememberPassword(inServer, config.incoming.password); + } + + if (inServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) { + inServer.setUnicharValue( + "oauth2.scope", + config.incoming.oauthSettings.scope + ); + inServer.setUnicharValue( + "oauth2.issuer", + config.incoming.oauthSettings.issuer + ); + } + + // SSL + inServer.socketType = config.incoming.socketType; + + // If we already have an account with an identical name, generate a unique + // name for the new account to avoid duplicates. + inServer.prettyName = checkAccountNameAlreadyExists( + config.identity.emailAddress + ) + ? generateUniqueAccountName(config) + : config.identity.emailAddress; + + inServer.doBiff = true; + inServer.biffMinutes = config.incoming.checkInterval; + inServer.setBoolValue("login_at_startup", config.incoming.loginAtStartup); + if (config.incoming.type == "pop3") { + inServer.setBoolValue( + "leave_on_server", + config.incoming.leaveMessagesOnServer + ); + inServer.setIntValue( + "num_days_to_leave_on_server", + config.incoming.daysToLeaveMessagesOnServer + ); + inServer.setBoolValue( + "delete_mail_left_on_server", + config.incoming.deleteOnServerWhenLocalDelete + ); + inServer.setBoolValue( + "delete_by_age_from_server", + config.incoming.deleteByAgeFromServer + ); + inServer.setBoolValue("download_on_biff", config.incoming.downloadOnBiff); + } + if (config.incoming.owaURL) { + inServer.setUnicharValue("owa_url", config.incoming.owaURL); + } + if (config.incoming.ewsURL) { + inServer.setUnicharValue("ews_url", config.incoming.ewsURL); + } + if (config.incoming.easURL) { + inServer.setUnicharValue("eas_url", config.incoming.easURL); + } + inServer.valid = true; + + let username = + config.outgoing.auth != Ci.nsMsgAuthMethod.none + ? config.outgoing.username + : null; + let outServer = MailServices.smtp.findServer( + username, + config.outgoing.hostname + ); + lazy.AccountCreationUtils.assert( + config.outgoing.addThisServer || + config.outgoing.useGlobalPreferredServer || + config.outgoing.existingServerKey, + "No SMTP server: inconsistent flags" + ); + + if ( + config.outgoing.addThisServer && + !outServer && + !config.incoming.useGlobalPreferredServer + ) { + outServer = MailServices.smtp.createServer(); + outServer.hostname = config.outgoing.hostname; + outServer.port = config.outgoing.port; + outServer.authMethod = config.outgoing.auth; + // Populate the clientid if it is enabled for this outgoing server. + if (outServer.clientidEnabled) { + outServer.clientid = newOutgoingClientid; + } + if (config.outgoing.auth != Ci.nsMsgAuthMethod.none) { + outServer.username = username; + outServer.password = config.outgoing.password; + if (config.rememberPassword && config.outgoing.password) { + rememberPassword(outServer, config.outgoing.password); + } + } + + if (outServer.authMethod == Ci.nsMsgAuthMethod.OAuth2) { + let prefBranch = "mail.smtpserver." + outServer.key + "."; + Services.prefs.setCharPref( + prefBranch + "oauth2.scope", + config.outgoing.oauthSettings.scope + ); + Services.prefs.setCharPref( + prefBranch + "oauth2.issuer", + config.outgoing.oauthSettings.issuer + ); + } + + outServer.socketType = config.outgoing.socketType; + outServer.description = config.displayName; + + // If this is the first SMTP server, set it as default + if ( + !MailServices.smtp.defaultServer || + !MailServices.smtp.defaultServer.hostname + ) { + MailServices.smtp.defaultServer = outServer; + } + } + + // identity + // TODO accounts without identity? + let identity = MailServices.accounts.createIdentity(); + identity.fullName = config.identity.realname; + identity.email = config.identity.emailAddress; + + // for new accounts, default to replies being positioned above the quote + // if a default account is defined already, take its settings instead + if (config.incoming.type == "imap" || config.incoming.type == "pop3") { + identity.replyOnTop = 1; + // identity.sigBottom = false; // don't set this until Bug 218346 is fixed + + if ( + MailServices.accounts.accounts.length && + MailServices.accounts.defaultAccount + ) { + let defAccount = MailServices.accounts.defaultAccount; + let defIdentity = defAccount.defaultIdentity; + if ( + defAccount.incomingServer.canBeDefaultServer && + defIdentity && + defIdentity.valid + ) { + identity.replyOnTop = defIdentity.replyOnTop; + identity.sigBottom = defIdentity.sigBottom; + } + } + } + + // due to accepted conventions, news accounts should default to plain text + if (config.incoming.type == "nntp") { + identity.composeHtml = false; + } + + identity.valid = true; + + if ( + !config.outgoing.useGlobalPreferredServer && + !config.incoming.useGlobalPreferredServer + ) { + if (config.outgoing.existingServerKey) { + identity.smtpServerKey = config.outgoing.existingServerKey; + } else { + identity.smtpServerKey = outServer.key; + } + } + + // account and hook up + // Note: Setting incomingServer will cause the AccountManager to refresh + // itself, which could be a problem if we came from it and we haven't set + // the identity (see bug 521955), so make sure everything else on the + // account is set up before you set the incomingServer. + let account = MailServices.accounts.createAccount(); + account.addIdentity(identity); + account.incomingServer = inServer; + if ( + inServer.canBeDefaultServer && + (!MailServices.accounts.defaultAccount || + !MailServices.accounts.defaultAccount.incomingServer.canBeDefaultServer) + ) { + MailServices.accounts.defaultAccount = account; + } + + verifyLocalFoldersAccount(MailServices.accounts); + setFolders(identity, inServer); + + // save + MailServices.accounts.saveAccountInfo(); + try { + Services.prefs.savePrefFile(null); + } catch (ex) { + lazy.AccountCreationUtils.ddump("Could not write out prefs: " + ex); + } + return account; +} +/* eslint-enable complexity */ + +function setFolders(identity, server) { + // TODO: support for local folders for global inbox (or use smart search + // folder instead) + + var baseURI = server.serverURI + "/"; + + // Names will be localized in UI, not in folder names on server/disk + // TODO allow to override these names in the XML config file, + // in case e.g. Google or AOL use different names? + // Workaround: Let user fix it :) + var fccName = "Sent"; + var draftName = "Drafts"; + var templatesName = "Templates"; + + identity.draftFolder = baseURI + draftName; + identity.stationeryFolder = baseURI + templatesName; + identity.fccFolder = baseURI + fccName; + + identity.fccFolderPickerMode = 0; + identity.draftsFolderPickerMode = 0; + identity.tmplFolderPickerMode = 0; +} + +function rememberPassword(server, password) { + let passwordURI; + if (server instanceof Ci.nsIMsgIncomingServer) { + passwordURI = server.localStoreType + "://" + server.hostName; + } else if (server instanceof Ci.nsISmtpServer) { + passwordURI = "smtp://" + server.hostname; + } else { + throw new lazy.AccountCreationUtils.NotReached("Server type not supported"); + } + + let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + login.init(passwordURI, null, passwordURI, server.username, password, "", ""); + try { + Services.logins.addLogin(login); + } catch (e) { + if (e.message.includes("This login already exists")) { + // TODO modify + } else { + throw e; + } + } +} + +/** + * Check whether the user's setup already has an incoming server + * which matches (hostname, port, username) the primary one + * in the config. + * (We also check the email address as username.) + * + * @param config {AccountConfig} filled in (no placeholders) + * @returns {nsIMsgIncomingServer} If it already exists, the server + * object is returned. + * If it's a new server, |null| is returned. + */ +function checkIncomingServerAlreadyExists(config) { + lazy.AccountCreationUtils.assert(config instanceof lazy.AccountConfig); + let incoming = config.incoming; + let existing = MailServices.accounts.findServer( + incoming.username, + incoming.hostname, + incoming.type, + incoming.port + ); + + // if username does not have an '@', also check the e-mail + // address form of the name. + if (!existing && !incoming.username.includes("@")) { + existing = MailServices.accounts.findServer( + config.identity.emailAddress, + incoming.hostname, + incoming.type, + incoming.port + ); + } + return existing; +} + +/** + * Check whether the user's setup already has an outgoing server + * which matches (hostname, port, username) the primary one + * in the config. + * + * @param config {AccountConfig} filled in (no placeholders) + * @returns {nsISmtpServer} If it already exists, the server + * object is returned. + * If it's a new server, |null| is returned. + */ +function checkOutgoingServerAlreadyExists(config) { + lazy.AccountCreationUtils.assert(config instanceof lazy.AccountConfig); + for (let existingServer of MailServices.smtp.servers) { + // TODO check username with full email address, too, like for incoming + if ( + existingServer.hostname == config.outgoing.hostname && + existingServer.port == config.outgoing.port && + existingServer.username == config.outgoing.username + ) { + return existingServer; + } + } + return null; +} + +/** + * Check whether the user's setup already has an account with the same email + * address. This might happen if the user uses the same email for different + * protocols (eg. IMAP and POP3). + * + * @param {string} name - The name or email address of the new account. + * @returns {boolean} True if an account with the same name is found. + */ +function checkAccountNameAlreadyExists(name) { + return MailServices.accounts.accounts.some( + a => a.incomingServer.prettyName == name + ); +} + +/** + * Generate a unique account name by appending the incoming protocol type, and + * a counter if necessary. + * + * @param {AccountConfig} config - The config data of the account being created. + * @returns {string} - The unique account name. + */ +function generateUniqueAccountName(config) { + // Generate a potential unique name. e.g. "foo@bar.com (POP3)". + let name = `${ + config.identity.emailAddress + } (${config.incoming.type.toUpperCase()})`; + + // If this name already exists, append a counter until we find a unique name. + if (checkAccountNameAlreadyExists(name)) { + let counter = 2; + while (checkAccountNameAlreadyExists(`${name}_${counter}`)) { + counter++; + } + // e.g. "foo@bar.com (POP3)_1". + name = `${name}_${counter}`; + } + + return name; +} + +/** + * Check if there already is a "Local Folders". If not, create it. + * Copied from AccountWizard.js with minor updates. + */ +function verifyLocalFoldersAccount(am) { + let localMailServer; + try { + localMailServer = am.localFoldersServer; + } catch (ex) { + localMailServer = null; + } + + try { + if (!localMailServer) { + // creates a copy of the identity you pass in + am.createLocalMailAccount(); + try { + localMailServer = am.localFoldersServer; + } catch (ex) { + lazy.AccountCreationUtils.ddump( + "Error! we should have found the local mail server " + + "after we created it." + ); + } + } + } catch (ex) { + lazy.AccountCreationUtils.ddump("Error in verifyLocalFoldersAccount " + ex); + } +} + +var CreateInBackend = { + checkIncomingServerAlreadyExists, + checkOutgoingServerAlreadyExists, + createAccountInBackend, +}; diff --git a/comm/mail/components/accountcreation/ExchangeAutoDiscover.jsm b/comm/mail/components/accountcreation/ExchangeAutoDiscover.jsm new file mode 100644 index 0000000000..5813aa0240 --- /dev/null +++ b/comm/mail/components/accountcreation/ExchangeAutoDiscover.jsm @@ -0,0 +1,676 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["fetchConfigFromExchange", "getAddonsList"]; + +var { AccountCreationUtils } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + setTimeout: "resource://gre/modules/Timer.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + AccountConfig: "resource:///modules/accountcreation/AccountConfig.jsm", + FetchHTTP: "resource:///modules/accountcreation/FetchHTTP.jsm", + GuessConfig: "resource:///modules/accountcreation/GuessConfig.jsm", + Sanitizer: "resource:///modules/accountcreation/Sanitizer.jsm", +}); + +var { + Abortable, + assert, + ddump, + deepCopy, + Exception, + gAccountSetupLogger, + getStringBundle, + PriorityOrderAbortable, + SuccessiveAbortable, + TimeoutAbortable, +} = AccountCreationUtils; + +/** + * Tries to get a configuration from an MS Exchange server + * using Microsoft AutoDiscover protocol. + * + * Disclaimers: + * - To support domain hosters, we cannot use SSL. That means we + * rely on insecure DNS and http, which means the results may be + * forged when under attack. The same is true for guessConfig(), though. + * + * @param {string} domain - The domain part of the user's email address + * @param {string} emailAddress - The user's email address + * @param {string} username - (Optional) The user's login name. + * If null, email address will be used. + * @param {string} password - The user's password for that email address + * @param {Function(domain, okCallback, cancelCallback)} confirmCallback - A + * callback that will be called to confirm redirection to another domain. + * @param {Function(config {AccountConfig})} successCallback - A callback that + * will be called when we could retrieve a configuration. + * The AccountConfig object will be passed in as first parameter. + * @param {Function(ex)} errorCallback - A callback that + * will be called when we could not retrieve a configuration, + * for whatever reason. This is expected (e.g. when there's no config + * for this domain at this location), + * so do not unconditionally show this to the user. + * The first parameter will be an exception object or error string. + */ +function fetchConfigFromExchange( + domain, + emailAddress, + username, + password, + confirmCallback, + successCallback, + errorCallback +) { + assert(typeof successCallback == "function"); + assert(typeof errorCallback == "function"); + if ( + !Services.prefs.getBoolPref( + "mailnews.auto_config.fetchFromExchange.enabled", + true + ) + ) { + errorCallback("Exchange AutoDiscover disabled per user preference"); + return new Abortable(); + } + + // <https://technet.microsoft.com/en-us/library/bb124251(v=exchg.160).aspx#Autodiscover%20services%20in%20Outlook> + // <https://docs.microsoft.com/en-us/previous-versions/office/developer/exchange-server-interoperability-guidance/hh352638(v%3Dexchg.140)>, search for "The Autodiscover service uses one of these four methods" + let url1 = + "https://autodiscover." + + lazy.Sanitizer.hostname(domain) + + "/autodiscover/autodiscover.xml"; + let url2 = + "https://" + + lazy.Sanitizer.hostname(domain) + + "/autodiscover/autodiscover.xml"; + let url3 = + "http://autodiscover." + + lazy.Sanitizer.hostname(domain) + + "/autodiscover/autodiscover.xml"; + let body = `<?xml version="1.0" encoding="utf-8"?> + <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"> + <Request> + <EMailAddress>${emailAddress}</EMailAddress> + <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema> + </Request> + </Autodiscover>`; + let callArgs = { + uploadBody: body, + post: true, + headers: { + // outlook.com needs this exact string, with space and lower case "utf". + // Compare bug 1454325 comment 15. + "Content-Type": "text/xml; charset=utf-8", + }, + username: username || emailAddress, + password, + allowAuthPrompt: false, + }; + let call; + let fetch; + let fetch3; + + let successive = new SuccessiveAbortable(); + let priority = new PriorityOrderAbortable(function (xml, call) { + // success + readAutoDiscoverResponse( + xml, + successive, + emailAddress, + username, + password, + confirmCallback, + config => { + config.subSource = `exchange-from-${call.foundMsg}`; + return detectStandardProtocols(config, domain, successCallback); + }, + errorCallback + ); + }, errorCallback); // all failed + + call = priority.addCall(); + call.foundMsg = "url1"; + fetch = new lazy.FetchHTTP( + url1, + callArgs, + call.successCallback(), + call.errorCallback() + ); + fetch.start(); + call.setAbortable(fetch); + + call = priority.addCall(); + call.foundMsg = "url2"; + fetch = new lazy.FetchHTTP( + url2, + callArgs, + call.successCallback(), + call.errorCallback() + ); + fetch.start(); + call.setAbortable(fetch); + + call = priority.addCall(); + call.foundMsg = "url3"; + let call3ErrorCallback = call.errorCallback(); + // url3 is HTTP (not HTTPS), so suppress password. Even MS spec demands so. + let call3Args = deepCopy(callArgs); + delete call3Args.username; + delete call3Args.password; + fetch3 = new lazy.FetchHTTP(url3, call3Args, call.successCallback(), ex => { + // url3 is an HTTP URL that will redirect to the real one, usually a + // HTTPS URL of the hoster. XMLHttpRequest unfortunately loses the call + // parameters, drops the auth, drops the body, and turns POST into GET, + // which cause the call to fail. For AutoDiscover mechanism to work, + // we need to repeat the call with the correct parameters again. + let redirectURL = fetch3._request.responseURL; + if (!redirectURL.startsWith("https:")) { + call3ErrorCallback(ex); + return; + } + let redirectURI = Services.io.newURI(redirectURL); + let redirectDomain = Services.eTLD.getBaseDomain(redirectURI); + let originalDomain = Services.eTLD.getBaseDomainFromHost(domain); + + function fetchRedirect() { + let fetchCall = priority.addCall(); + let fetch = new lazy.FetchHTTP( + redirectURL, + callArgs, // now with auth + fetchCall.successCallback(), + fetchCall.errorCallback() + ); + fetchCall.setAbortable(fetch); + fetch.start(); + } + + const kSafeDomains = ["office365.com", "outlook.com"]; + if ( + redirectDomain != originalDomain && + !kSafeDomains.includes(redirectDomain) + ) { + // Given that we received the redirect URL from an insecure HTTP call, + // we ask the user whether he trusts the redirect domain. + gAccountSetupLogger.info("AutoDiscover HTTP redirected to other domain"); + let dialogSuccessive = new SuccessiveAbortable(); + // Because the dialog implements Abortable, the dialog will cancel and + // close automatically, if a slow higher priority call returns late. + let dialogCall = priority.addCall(); + dialogCall.setAbortable(dialogSuccessive); + call3ErrorCallback(new Exception("Redirected")); + dialogSuccessive.current = new TimeoutAbortable( + lazy.setTimeout(() => { + dialogSuccessive.current = confirmCallback( + redirectDomain, + () => { + // User agreed. + fetchRedirect(); + // Remove the dialog from the call stack. + dialogCall.errorCallback()(new Exception("Proceed to fetch")); + }, + ex => { + // User rejected, or action cancelled otherwise. + dialogCall.errorCallback()(ex); + } + ); + // Account for a slow server response. + // This will prevent showing the warning message when not necessary. + // The timeout is just for optics. The Abortable ensures that it works. + }, 2000) + ); + } else { + fetchRedirect(); + call3ErrorCallback(new Exception("Redirected")); + } + }); + fetch3.start(); + call.setAbortable(fetch3); + + successive.current = priority; + return successive; +} + +var gLoopCounter = 0; + +/** + * @param {JXON} xml - The Exchange server AutoDiscover response + * @param {Function(config {AccountConfig})} successCallback - @see accountConfig.js + */ +function readAutoDiscoverResponse( + autoDiscoverXML, + successive, + emailAddress, + username, + password, + confirmCallback, + successCallback, + errorCallback +) { + assert(successive instanceof SuccessiveAbortable); + assert(typeof successCallback == "function"); + assert(typeof errorCallback == "function"); + + // redirect to other email address + if ( + "Account" in autoDiscoverXML.Autodiscover.Response && + "RedirectAddr" in autoDiscoverXML.Autodiscover.Response.Account + ) { + // <https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/49083e77-8dc2-4010-85c6-f40e090f3b17> + let redirectEmailAddress = lazy.Sanitizer.emailAddress( + autoDiscoverXML.Autodiscover.Response.Account.RedirectAddr + ); + let domain = redirectEmailAddress.split("@").pop(); + if (++gLoopCounter > 2) { + throw new Error("Too many redirects in XML response; domain=" + domain); + } + successive.current = fetchConfigFromExchange( + domain, + redirectEmailAddress, + // Per spec, need to authenticate with the original email address, + // not the redirected address (if not already overridden). + username || emailAddress, + password, + confirmCallback, + successCallback, + errorCallback + ); + return; + } + + let config = readAutoDiscoverXML(autoDiscoverXML, username); + if (config.isComplete()) { + successCallback(config); + } else { + errorCallback(new Exception("No valid configs found in AutoDiscover XML")); + } +} + +/* eslint-disable complexity */ +/** + * @param {JXON} xml - The Exchange server AutoDiscover response + * @param {string} username - (Optional) The user's login name + * If null, email address placeholder will be used. + * @returns {AccountConfig} - @see accountConfig.js + * + * @see <https://www.msxfaq.de/exchange/autodiscover/autodiscover_xml.htm> + */ +function readAutoDiscoverXML(autoDiscoverXML, username) { + if ( + typeof autoDiscoverXML != "object" || + !("Autodiscover" in autoDiscoverXML) || + !("Response" in autoDiscoverXML.Autodiscover) || + !("Account" in autoDiscoverXML.Autodiscover.Response) || + !("Protocol" in autoDiscoverXML.Autodiscover.Response.Account) + ) { + let stringBundle = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties" + ); + throw new Exception( + stringBundle.GetStringFromName("no_autodiscover.error") + ); + } + var xml = autoDiscoverXML.Autodiscover.Response.Account; + + function array_or_undef(value) { + return value === undefined ? [] : value; + } + + var config = new lazy.AccountConfig(); + config.source = lazy.AccountConfig.kSourceExchange; + config.incoming.username = username || "%EMAILADDRESS%"; + config.incoming.socketType = Ci.nsMsgSocketType.SSL; // only https supported + config.incoming.port = 443; + config.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext; + config.incoming.authAlternatives = [Ci.nsMsgAuthMethod.OAuth2]; + config.outgoing.addThisServer = false; + config.outgoing.useGlobalPreferredServer = true; + + for (let protocolX of array_or_undef(xml.$Protocol)) { + try { + let type = lazy.Sanitizer.enum( + protocolX.Type, + ["WEB", "EXHTTP", "EXCH", "EXPR", "POP3", "IMAP", "SMTP"], + "unknown" + ); + if (type == "WEB") { + let urlsX; + if ("External" in protocolX) { + urlsX = protocolX.External; + } else if ("Internal" in protocolX) { + urlsX = protocolX.Internal; + } + if (urlsX) { + config.incoming.owaURL = lazy.Sanitizer.url(urlsX.OWAUrl.value); + if ( + !config.incoming.ewsURL && + "Protocol" in urlsX && + "ASUrl" in urlsX.Protocol + ) { + config.incoming.ewsURL = lazy.Sanitizer.url(urlsX.Protocol.ASUrl); + } + config.incoming.type = "exchange"; + let parsedURL = new URL(config.incoming.owaURL); + config.incoming.hostname = lazy.Sanitizer.hostname( + parsedURL.hostname + ); + if (parsedURL.port) { + config.incoming.port = lazy.Sanitizer.integer(parsedURL.port); + } + } + } else if (type == "EXHTTP" || type == "EXCH") { + config.incoming.ewsURL = lazy.Sanitizer.url(protocolX.EwsUrl); + if (!config.incoming.ewsURL) { + config.incoming.ewsURL = lazy.Sanitizer.url(protocolX.ASUrl); + } + config.incoming.type = "exchange"; + let parsedURL = new URL(config.incoming.ewsURL); + config.incoming.hostname = lazy.Sanitizer.hostname(parsedURL.hostname); + if (parsedURL.port) { + config.incoming.port = lazy.Sanitizer.integer(parsedURL.port); + } + } else if (type == "POP3" || type == "IMAP" || type == "SMTP") { + let server; + if (type == "SMTP") { + server = config.createNewOutgoing(); + } else { + server = config.createNewIncoming(); + } + + server.type = lazy.Sanitizer.translate(type, { + POP3: "pop3", + IMAP: "imap", + SMTP: "smtp", + }); + server.hostname = lazy.Sanitizer.hostname(protocolX.Server); + server.port = lazy.Sanitizer.integer(protocolX.Port); + server.socketType = Ci.nsMsgSocketType.plain; + if ( + "SSL" in protocolX && + protocolX.SSL.toLowerCase() == "on" // "On" or "Off" + ) { + // SSL is too unspecific. Do they mean STARTTLS or normal TLS? + // For now, assume normal TLS, unless it's a standard plain port. + switch (server.port) { + case 143: // IMAP standard + case 110: // POP3 standard + case 25: // SMTP standard + case 587: // SMTP standard + server.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + break; + case 993: // IMAP SSL + case 995: // POP3 SSL + case 465: // SMTP SSL + default: + // if non-standard port, assume normal TLS, not STARTTLS + server.socketType = Ci.nsMsgSocketType.SSL; + break; + } + } + server.auth = Ci.nsMsgAuthMethod.passwordCleartext; + if ( + "SPA" in protocolX && + protocolX.SPA.toLowerCase() == "on" // "On" or "Off" + ) { + // Secure Password Authentication = NTLM or GSSAPI/Kerberos + server.auth = Ci.nsMsgAuthMethod.secure; + } + if ("LoginName" in protocolX) { + server.username = lazy.Sanitizer.nonemptystring(protocolX.LoginName); + } else { + server.username = username || "%EMAILADDRESS%"; + } + + if (type == "SMTP") { + if (!config.outgoing.hostname) { + config.outgoing = server; + } else { + config.outgoingAlternatives.push(server); + } + } else if (!config.incoming.hostname) { + // eslint-disable-line no-lonely-if + config.incoming = server; + } else { + config.incomingAlternatives.push(server); + } + } + + // else unknown or unsupported protocol + } catch (e) { + console.error(e); + } + } + + // OAuth2 settings, so that createInBackend() doesn't bail out + if (config.incoming.owaURL || config.incoming.ewsURL) { + config.incoming.oauthSettings = { + issuer: config.incoming.hostname, + scope: config.incoming.owaURL || config.incoming.ewsURL, + }; + config.outgoing.oauthSettings = { + issuer: config.incoming.hostname, + scope: config.incoming.owaURL || config.incoming.ewsURL, + }; + } + + return config; +} +/* eslint-enable complexity */ + +/** + * Ask server which addons can handle this config. + * + * @param {AccountConfig} config + * @param {Function(config {AccountConfig})} successCallback + * @returns {Abortable} + */ +function getAddonsList(config, successCallback, errorCallback) { + let incoming = [config.incoming, ...config.incomingAlternatives].find( + alt => alt.type == "exchange" + ); + if (!incoming) { + successCallback(); + return new Abortable(); + } + let url = Services.prefs.getCharPref("mailnews.auto_config.addons_url"); + if (!url) { + errorCallback(new Exception("no URL for addons list configured")); + return new Abortable(); + } + let fetch = new lazy.FetchHTTP( + url, + { allowCache: true, timeout: 10000 }, + function (json) { + let addons = readAddonsJSON(json); + addons = addons.filter(addon => { + // Find types matching the current config. + // Pick the first in the list as the preferred one and + // tell the UI to use that one. + addon.useType = addon.supportedTypes.find( + type => + (incoming.owaURL && type.protocolType == "owa") || + (incoming.ewsURL && type.protocolType == "ews") || + (incoming.easURL && type.protocolType == "eas") + ); + return !!addon.useType; + }); + if (addons.length == 0) { + errorCallback( + new Exception( + "Config found, but no addons known to handle the config" + ) + ); + return; + } + config.addons = addons; + successCallback(config); + }, + errorCallback + ); + fetch.start(); + return fetch; +} + +/** + * This reads the addons list JSON and makes security validations, + * e.g. that the URLs are not chrome: URLs, which could lead to exploits. + * It also chooses the right language etc.. + * + * @param {JSON} json - the addons.json file contents + * @returns {Array of AddonInfo} - @see AccountConfig.addons + * + * accountTypes are listed in order of decreasing preference. + * Languages are 2-letter codes. If a language is not available, + * the first name or description will be used. + * + * Parse e.g. +[ + { + "id": "owl@beonex.com", + "name": { + "en": "Owl", + "de": "Eule" + }, + "description": { + "en": "Owl is a paid third-party addon that allows you to access your email account on Exchange servers. See the website for prices.", + "de": "Eule ist eine Erweiterung von einem Drittanbieter, die Ihnen erlaubt, Exchange-Server zu benutzen. Sie ist kostenpflichtig. Die Preise finden Sie auf der Website." + }, + "minVersion": "0.2", + "xpiURL": "http://www.beonex.com/owl/latest.xpi", + "websiteURL": "http://www.beonex.com/owl/", + "icon32": "http://www.beonex.com/owl/owl-32.png", + "accountTypes": [ + { + "generalType": "exchange", + "protocolType": "owa", + "addonAccountType": "owl-owa" + }, + { + "generalType": "exchange", + "protocolType": "eas", + "addonAccountType": "owl-eas" + } + ] + } +] + */ +function readAddonsJSON(json) { + let addons = []; + function ensureArray(value) { + return Array.isArray(value) ? value : []; + } + let xulLocale = Services.locale.requestedLocale; + let locale = xulLocale ? xulLocale.substring(0, 5) : "default"; + for (let addonJSON of ensureArray(json)) { + try { + let addon = { + id: addonJSON.id, + minVersion: addonJSON.minVersion, + xpiURL: lazy.Sanitizer.url(addonJSON.xpiURL), + websiteURL: lazy.Sanitizer.url(addonJSON.websiteURL), + icon32: addonJSON.icon32 ? lazy.Sanitizer.url(addonJSON.icon32) : null, + supportedTypes: [], + }; + assert( + new URL(addon.xpiURL).protocol == "https:", + "XPI download URL needs to be https" + ); + addon.name = + locale in addonJSON.name ? addonJSON.name[locale] : addonJSON.name[0]; + addon.description = + locale in addonJSON.description + ? addonJSON.description[locale] + : addonJSON.description[0]; + for (let typeJSON of ensureArray(addonJSON.accountTypes)) { + try { + addon.supportedTypes.push({ + generalType: lazy.Sanitizer.alphanumdash(typeJSON.generalType), + protocolType: lazy.Sanitizer.alphanumdash(typeJSON.protocolType), + addonAccountType: lazy.Sanitizer.alphanumdash( + typeJSON.addonAccountType + ), + }); + } catch (e) { + ddump(e); + } + } + addons.push(addon); + } catch (e) { + ddump(e); + } + } + return addons; +} + +/** + * Probe a found Exchange server for IMAP/POP3 and SMTP support. + * + * @param {AccountConfig} config - The initial detected Exchange configuration. + * @param {string} domain - The domain part of the user's email address + * @param {Function(config {AccountConfig})} successCallback - A callback that + * will be called when we found an appropriate configuration. + * The AccountConfig object will be passed in as first parameter. + */ +function detectStandardProtocols(config, domain, successCallback) { + gAccountSetupLogger.info("Exchange Autodiscover gave some results."); + let alts = [config.incoming, ...config.incomingAlternatives]; + if (alts.find(alt => alt.type == "imap" || alt.type == "pop3")) { + // Autodiscover found an exchange server with advertized IMAP and/or + // POP3 support. We're done then. + config.preferStandardProtocols(); + successCallback(config); + return; + } + + // Autodiscover is known not to advertise all that it supports. Let's see + // if there really isn't any IMAP/POP3 support by probing the Exchange + // server. Use the server hostname already found. + let config2 = new lazy.AccountConfig(); + config2.incoming.hostname = config.incoming.hostname; + config2.incoming.username = config.incoming.username || "%EMAILADDRESS%"; + // For Exchange 2013+ Kerberos/GSSAPI and NTLM options do not work by + // default at least for Linux users, even if support is detected. + config2.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext; + + config2.outgoing.hostname = config.incoming.hostname; + config2.outgoing.username = config.incoming.username || "%EMAILADDRESS%"; + + config2.incomingAlternatives = config.incomingAlternatives; + config2.incomingAlternatives.push(config.incoming); // type=exchange + + config2.outgoingAlternatives = config.outgoingAlternatives; + if (config.outgoing.hostname) { + config2.outgoingAlternatives.push(config.outgoing); + } + + lazy.GuessConfig.guessConfig( + domain, + function (type, hostname, port, ssl, done, config) { + gAccountSetupLogger.info( + `Probing exchange server ${hostname} for ${type} protocol support.` + ); + }, + function (probedConfig) { + // Probing succeeded: found open protocols, yay! + successCallback(probedConfig); + }, + function (e, probedConfig) { + // Probing didn't find any open protocols. + // Let's use the exchange (only) config that was listed then. + config.subSource += "-guess"; + successCallback(config); + }, + config2, + "both" + ); +} diff --git a/comm/mail/components/accountcreation/FetchConfig.jsm b/comm/mail/components/accountcreation/FetchConfig.jsm new file mode 100644 index 0000000000..1bf16ca2ed --- /dev/null +++ b/comm/mail/components/accountcreation/FetchConfig.jsm @@ -0,0 +1,299 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["FetchConfig"]; + +const { AccountCreationUtils } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); +const lazy = {}; +ChromeUtils.defineModuleGetter( + lazy, + "FetchHTTP", + "resource:///modules/accountcreation/FetchHTTP.jsm" +); +ChromeUtils.defineModuleGetter( + lazy, + "readFromXML", + "resource:///modules/accountcreation/readFromXML.jsm" +); +ChromeUtils.defineModuleGetter( + lazy, + "Sanitizer", + "resource:///modules/accountcreation/Sanitizer.jsm" +); + +const { DNS } = ChromeUtils.import("resource:///modules/DNS.jsm"); +const { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm"); + +const { + Abortable, + ddump, + Exception, + PriorityOrderAbortable, + PromiseAbortable, + readURLasUTF8, + runAsync, + SuccessiveAbortable, + TimeoutAbortable, +} = AccountCreationUtils; + +/** + * Tries to find a configuration for this ISP on the local harddisk, in the + * application install directory's "isp" subdirectory. + * Params @see fetchConfigFromISP() + */ +function fetchConfigFromDisk(domain, successCallback, errorCallback) { + return new TimeoutAbortable( + runAsync(function () { + try { + // <TB installdir>/isp/example.com.xml + var configLocation = Services.dirsvc.get("CurProcD", Ci.nsIFile); + configLocation.append("isp"); + configLocation.append(lazy.Sanitizer.hostname(domain) + ".xml"); + + if (!configLocation.exists() || !configLocation.isReadable()) { + errorCallback(new Exception("local file not found")); + return; + } + var contents = readURLasUTF8(Services.io.newFileURI(configLocation)); + let domParser = new DOMParser(); + const xml = JXON.build(domParser.parseFromString(contents, "text/xml")); + successCallback(lazy.readFromXML(xml, "disk")); + } catch (e) { + errorCallback(e); + } + }) + ); +} + +/** + * Tries to get a configuration from the ISP / mail provider directly. + * + * Disclaimers: + * - To support domain hosters, we cannot use SSL. That means we + * rely on insecure DNS and http, which means the results may be + * forged when under attack. The same is true for guessConfig(), though. + * + * @param domain {String} - The domain part of the user's email address + * @param emailAddress {String} - The user's email address + * @param successCallback {Function(config {AccountConfig}})} A callback that + * will be called when we could retrieve a configuration. + * The AccountConfig object will be passed in as first parameter. + * @param errorCallback {Function(ex)} - A callback that + * will be called when we could not retrieve a configuration, + * for whatever reason. This is expected (e.g. when there's no config + * for this domain at this location), + * so do not unconditionally show this to the user. + * The first parameter will be an exception object or error string. + */ +function fetchConfigFromISP( + domain, + emailAddress, + successCallback, + errorCallback +) { + if ( + !Services.prefs.getBoolPref("mailnews.auto_config.fetchFromISP.enabled") + ) { + errorCallback(new Exception("ISP fetch disabled per user preference")); + return new Abortable(); + } + + let conf1 = + "autoconfig." + lazy.Sanitizer.hostname(domain) + "/mail/config-v1.1.xml"; + // .well-known/ <http://tools.ietf.org/html/draft-nottingham-site-meta-04> + let conf2 = + lazy.Sanitizer.hostname(domain) + + "/.well-known/autoconfig/mail/config-v1.1.xml"; + // This list is sorted by decreasing priority + var urls = ["https://" + conf1, "https://" + conf2]; + if ( + !Services.prefs.getBoolPref("mailnews.auto_config.fetchFromISP.sslOnly") + ) { + urls.push("http://" + conf1, "http://" + conf2); + } + let callArgs = { + urlArgs: { + emailaddress: emailAddress, + }, + }; + if ( + !Services.prefs.getBoolPref( + "mailnews.auto_config.fetchFromISP.sendEmailAddress" + ) + ) { + delete callArgs.urlArgs.emailaddress; + } + let call; + let fetch; + + let priority = new PriorityOrderAbortable( + (xml, call) => + successCallback(lazy.readFromXML(xml, `isp-${call.foundMsg}`)), + errorCallback + ); + for (let url of urls) { + call = priority.addCall(); + call.foundMsg = url.startsWith("https") ? "https" : "http"; + fetch = new lazy.FetchHTTP( + url, + callArgs, + call.successCallback(), + call.errorCallback() + ); + call.setAbortable(fetch); + fetch.start(); + } + + return priority; +} + +/** + * Tries to get a configuration for this ISP from a central database at + * Mozilla servers. + * Params @see fetchConfigFromISP() + */ +function fetchConfigFromDB(domain, successCallback, errorCallback) { + let url = Services.prefs.getCharPref("mailnews.auto_config_url"); + if (!url) { + errorCallback(new Exception("no URL for ISP DB configured")); + return new Abortable(); + } + domain = lazy.Sanitizer.hostname(domain); + + // If we don't specify a place to put the domain, put it at the end. + if (!url.includes("{{domain}}")) { + url = url + domain; + } else { + url = url.replace("{{domain}}", domain); + } + + let fetch = new lazy.FetchHTTP( + url, + { timeout: 10000 }, // 10 seconds + function (result) { + successCallback(lazy.readFromXML(result, "db")); + }, + errorCallback + ); + fetch.start(); + return fetch; +} + +/** + * Does a lookup of DNS MX, to get the server that is responsible for + * receiving mail for this domain. Then it takes the domain of that + * server, and does another lookup (in ISPDB and possibly at ISP autoconfig + * server) and if such a config is found, returns that. + * + * Disclaimers: + * - DNS is unprotected, meaning the results could be forged. + * The same is true for fetchConfigFromISP() and guessConfig(), though. + * - DNS MX tells us the incoming server, not the mailbox (IMAP) server. + * They are different. This mechanism is only an approximation + * for hosted domains (yourname.com is served by mx.hoster.com and + * therefore imap.hoster.com - that "therefore" is exactly the + * conclusional jump we make here.) and alternative domains + * (e.g. yahoo.de -> yahoo.com). + * - We make a look up for the base domain. E.g. if MX is + * mx1.incoming.servers.hoster.com, we look up hoster.com. + * Thanks to Services.eTLD, we also get bbc.co.uk right. + * + * Params @see fetchConfigFromISP() + */ +function fetchConfigForMX(domain, successCallback, errorCallback) { + const sanitizedDomain = lazy.Sanitizer.hostname(domain); + const sucAbortable = new SuccessiveAbortable(); + const time = Date.now(); + + sucAbortable.current = getMX( + sanitizedDomain, + function (mxHostname) { + // success + ddump("getmx took " + (Date.now() - time) + "ms"); + let sld = Services.eTLD.getBaseDomainFromHost(mxHostname); + ddump("base domain " + sld + " for " + mxHostname); + if (sld == sanitizedDomain) { + errorCallback( + new Exception("MX lookup would be no different from domain") + ); + return; + } + + // In addition to just the base domain, also check the full domain of the MX server + // to differentiate between Outlook.com/Hotmail and Office365 business domains. + let mxDomain; + try { + mxDomain = Services.eTLD.getNextSubDomain(mxHostname); + } catch (ex) { + // e.g. hostname doesn't have enough components + console.error(ex); // not fatal + } + let priority = new PriorityOrderAbortable(successCallback, errorCallback); + if (mxDomain && sld != mxDomain) { + let call = priority.addCall(); + let fetch = fetchConfigFromDB( + mxDomain, + call.successCallback(), + call.errorCallback() + ); + call.setAbortable(fetch); + } + let call = priority.addCall(); + let fetch = fetchConfigFromDB( + sld, + call.successCallback(), + call.errorCallback() + ); + call.setAbortable(fetch); + sucAbortable.current = priority; + }, + errorCallback + ); + return sucAbortable; +} + +/** + * Queries the DNS MX records for a given domain. Calls `successCallback` with + * the hostname of the MX server. If there are several entries with different + * preference values, only the most preferred (i.e. has the lowest value) + * is used. If there are several most preferred servers (i.e. round robin), + * only one of them is used. + * + * @param {string} sanitizedDomain @see fetchConfigFromISP() + * @param {function(hostname {string})} - successCallback + * Called when we found an MX for the domain. + * For |hostname|, see description above. + * @param {function({Exception|string})} errorCallback @see fetchConfigFromISP() + */ +function getMX(sanitizedDomain, successCallback, errorCallback) { + return new PromiseAbortable( + DNS.mx(sanitizedDomain), + function (records) { + const filteredRecs = records.filter(record => record.host); + + if (filteredRecs.length > 0) { + const sortedRecs = filteredRecs.sort((a, b) => a.prio > b.prio); + const firstHost = sortedRecs[0].host; + successCallback(firstHost); + } else { + errorCallback( + new Exception( + "No hostname found in MX records for sanitizedDomain=" + + sanitizedDomain + ) + ); + } + }, + errorCallback + ); +} + +var FetchConfig = { + forMX: fetchConfigForMX, + fromDB: fetchConfigFromDB, + fromISP: fetchConfigFromISP, + fromDisk: fetchConfigFromDisk, +}; diff --git a/comm/mail/components/accountcreation/FetchHTTP.jsm b/comm/mail/components/accountcreation/FetchHTTP.jsm new file mode 100644 index 0000000000..54b3629906 --- /dev/null +++ b/comm/mail/components/accountcreation/FetchHTTP.jsm @@ -0,0 +1,401 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This is a small wrapper around XMLHttpRequest, which solves various + * inadequacies of the API, e.g. error handling. It is entirely generic and + * can be used for purposes outside of even mail. + * + * It does not provide download progress, but assumes that the + * fetched resource is so small (<1 10 KB) that the roundtrip and + * response generation is far more significant than the + * download time of the response. In other words, it's fine for RPC, + * but not for bigger file downloads. + */ + +const EXPORTED_SYMBOLS = ["FetchHTTP"]; + +const { AccountCreationUtils } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); +const lazy = {}; +ChromeUtils.defineModuleGetter( + lazy, + "Sanitizer", + "resource:///modules/accountcreation/Sanitizer.jsm" +); + +const { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm"); + +const { + Abortable, + alertPrompt, + assert, + ddump, + Exception, + gAccountSetupLogger, + getStringBundle, + UserCancelledException, +} = AccountCreationUtils; + +/** + * Set up a fetch. + * + * @param {string} url - URL of the server function. + * ATTENTION: The caller needs to make sure that the URL is secure to call. + * @param {object} args - Additional parameters as properties, see below + * + * @param {Function({string} result)} successCallback + * Called when the server call worked (no errors). + * |result| will contain the body of the HTTP response, as string. + * @param {Function(ex)} errorCallback + * Called in case of error. ex contains the error + * with a user-displayable but not localized |.message| and maybe a + * |.code|, which can be either + * - an nsresult error code, + * - an HTTP result error code (0...1000) or + * - negative: 0...-100 : + * -2 = can't resolve server in DNS etc. + * -4 = response body (e.g. XML) malformed + * + * The following optional parameters are supported as properties of the |args| object: + * + * @param {Object, associative array} urlArgs - Parameters to add + * to the end of the URL as query string. E.g. + * { foo: "bla", bar: "blub blub" } will add "?foo=bla&bar=blub%20blub" + * to the URL + * (unless the URL already has a "?", then it adds "&foo..."). + * The values will be urlComponentEncoded, so pass them unencoded. + * @param {Object, associative array} headers - HTTP headers to be added + * to the HTTP request. + * { foo: "blub blub" } will add HTTP header "Foo: Blub blub". + * The values will be passed verbatim. + * @param {boolean} post - HTTP GET or POST + * Only influences the HTTP request method, + * i.e. first line of the HTTP request, not the body or parameters. + * Use POST when you modify server state, + * GET when you only request information. + * Default is GET. + * @param {Object, associative array} bodyFormArgs - Like urlArgs, + * just that the params will be sent x-url-encoded in the body, + * like a HTML form post. + * The values will be urlComponentEncoded, so pass them unencoded. + * This cannot be used together with |uploadBody|. + * @param {object} uploadBody - Arbitrary object, which to use as + * body of the HTTP request. Will also set the mimetype accordingly. + * Only supported object types, currently only JXON is supported + * (sending XML). + * Usually, you have nothing to upload, so just pass |null|. + * Only supported object types, currently supported: + * JXON -> sending XML + * JS object -> sending JSON + * string -> sending text/plain + * If you want to override the body mimetype, set header Content-Type below. + * Usually, you have nothing to upload, so just leave it at |null|. + * Default |null|. + * @param {boolean} allowCache (default true) + * @param {string} username (default null = no authentication) + * @param {string} password (default null = no authentication) + * @param {boolean} allowAuthPrompt (default true) + * @param {boolean} requireSecureAuth (default false) + * Ignore the username and password unless we are using https: + * This also applies to both https: to http: and http: to https: redirects. + */ +function FetchHTTP(url, args, successCallback, errorCallback) { + assert(typeof successCallback == "function", "BUG: successCallback"); + assert(typeof errorCallback == "function", "BUG: errorCallback"); + this._url = lazy.Sanitizer.string(url); + if (!args) { + args = {}; + } + if (!args.urlArgs) { + args.urlArgs = {}; + } + if (!args.headers) { + args.headers = {}; + } + + this._args = args; + this._args.post = lazy.Sanitizer.boolean(args.post || false); // default false + this._args.allowCache = + "allowCache" in args ? lazy.Sanitizer.boolean(args.allowCache) : true; // default true + this._args.allowAuthPrompt = lazy.Sanitizer.boolean( + args.allowAuthPrompt || false + ); // default false + this._args.requireSecureAuth = lazy.Sanitizer.boolean( + args.requireSecureAuth || false + ); // default false + this._args.timeout = lazy.Sanitizer.integer(args.timeout || 5000); // default 5 seconds + this._successCallback = successCallback; + this._errorCallback = errorCallback; + this._logger = gAccountSetupLogger; + this._logger.info("Requesting <" + url + ">"); +} +FetchHTTP.prototype = { + __proto__: Abortable.prototype, + _url: null, // URL as passed to ctor, without arguments + _args: null, + _successCallback: null, + _errorCallback: null, + _request: null, // the XMLHttpRequest object + result: null, + + start() { + let url = this._url; + for (let name in this._args.urlArgs) { + url += + (!url.includes("?") ? "?" : "&") + + name + + "=" + + encodeURIComponent(this._args.urlArgs[name]); + } + this._request = new XMLHttpRequest(); + let request = this._request; + request.mozBackgroundRequest = !this._args.allowAuthPrompt; + let username = null, + password = null; + if (url.startsWith("https:") || !this._args.requireSecureAuth) { + username = this._args.username; + password = this._args.password; + } + request.open( + this._args.post ? "POST" : "GET", + url, + true, + username, + password + ); + request.channel.loadGroup = null; + request.timeout = this._args.timeout; + // needs bug 407190 patch v4 (or higher) - uncomment if that lands. + // try { + // var channel = request.channel.QueryInterface(Ci.nsIHttpChannel2); + // channel.connectTimeout = 5; + // channel.requestTimeout = 5; + // } catch (e) { dump(e + "\n"); } + + if (!this._args.allowCache) { + // Disable Mozilla HTTP cache + request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE; + } + + // body + let mimetype = null; + let body = this._args.uploadBody; + if (typeof body == "object" && "nodeType" in body) { + // XML + mimetype = "text/xml; charset=UTF-8"; + body = new XMLSerializer().serializeToString(body); + } else if (typeof body == "object") { + // JSON + mimetype = "text/json; charset=UTF-8"; + body = JSON.stringify(body); + } else if (typeof body == "string") { + // Plaintext + // You can override the mimetype with { headers: {"Content-Type" : "text/foo" } } + mimetype = "text/plain; charset=UTF-8"; + // body already set above + } else if (this._args.bodyFormArgs) { + mimetype = "application/x-www-form-urlencoded; charset=UTF-8"; + body = ""; + for (let name in this._args.bodyFormArgs) { + body += + (body ? "&" : "") + + name + + "=" + + encodeURIComponent(this._args.bodyFormArgs[name]); + } + } + + // Headers + if (mimetype && !("Content-Type" in this._args.headers)) { + request.setRequestHeader("Content-Type", mimetype); + } + if (username && password) { + // workaround, because open(..., username, password) does not work. + request.setRequestHeader( + "Authorization", + "Basic " + + btoa( + // btoa() takes a BinaryString. + String.fromCharCode( + ...new TextEncoder().encode(username + ":" + password) + ) + ) + ); + } + for (let name in this._args.headers) { + request.setRequestHeader(name, this._args.headers[name]); + if (name == "Cookie") { + // Websites are not allowed to set this, but chrome is. + // Nevertheless, the cookie lib later overwrites our header. + // request.channel.setCookie(this._args.headers[name]); -- crashes + // So, deactivate that Firefox cookie lib. + request.channel.loadFlags |= Ci.nsIRequest.LOAD_ANONYMOUS; + } + } + + var me = this; + request.onload = function () { + me._response(true); + }; + request.onerror = function () { + me._response(false); + }; + request.ontimeout = function () { + me._response(false); + }; + request.send(body); + // Store the original stack so we can use it if there is an exception + this._callStack = Error().stack; + }, + _response(success, exStored) { + try { + var errorCode = null; + var errorStr = null; + + if ( + success && + this._request.status >= 200 && + this._request.status < 300 + ) { + // HTTP level success + try { + // response + var mimetype = this._request.getResponseHeader("Content-Type"); + if (!mimetype) { + mimetype = ""; + } + mimetype = mimetype.split(";")[0]; + if ( + mimetype == "text/xml" || + mimetype == "application/xml" || + mimetype == "text/rdf" + ) { + // XML + this.result = JXON.build(this._request.responseXML); + } else if ( + mimetype == "text/json" || + mimetype == "application/json" + ) { + // JSON + this.result = JSON.parse(this._request.responseText); + } else { + // Plaintext (fallback) + // ddump("mimetype: " + mimetype + " only supported as text"); + this.result = this._request.responseText; + } + } catch (e) { + success = false; + errorStr = getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties" + ).GetStringFromName("bad_response_content.error"); + errorCode = -4; + } + } else if ( + this._args.username && + this._request.responseURL.replace(/\/\/.*@/, "//") != this._url && + this._request.responseURL.startsWith( + this._args.requireSecureAuth ? "https" : "http" + ) && + !this._isRetry + ) { + // Redirects lack auth, see <https://stackoverflow.com/a/28411170> + this._logger.info( + "Call to <" + + this._url + + "> was redirected to <" + + this._request.responseURL + + ">, and failed. Re-trying the new URL with authentication again." + ); + this._url = this._request.responseURL; + this._isRetry = true; + this.start(); + return; + } else { + success = false; + try { + errorCode = this._request.status; + errorStr = this._request.statusText; + } catch (e) { + // In case .statusText throws (it's marked as [Throws] in the webidl), + // continue with empty errorStr. + } + if (!errorStr) { + // If we can't resolve the hostname in DNS etc., .statusText is empty. + errorCode = -2; + errorStr = getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties" + ).GetStringFromName("cannot_contact_server.error"); + ddump(errorStr + " on <" + this._url + ">"); + } + } + + // Callbacks + if (success) { + try { + this._successCallback(this.result); + } catch (e) { + e.stack = this._callStack; + this._error(e); + } + } else if (exStored) { + this._error(exStored); + } else { + // Put the caller's stack into the exception + let e = new ServerException(errorStr, errorCode, this._url); + e.stack = this._callStack; + this._error(e); + } + + if (this._finishedCallback) { + try { + this._finishedCallback(this); + } catch (e) { + console.error(e); + } + } + } catch (e) { + // error in our fetchhttp._response() code + this._error(e); + } + }, + _error(e) { + try { + this._errorCallback(e); + } catch (e) { + // error in errorCallback, too! + console.error(e); + alertPrompt("Error in errorCallback for fetchhttp", e); + } + }, + /** + * Call this between start() and finishedCallback fired. + */ + cancel(ex) { + assert(!this.result, "Call already returned"); + + this._request.abort(); + + // Need to manually call error handler + // <https://bugzilla.mozilla.org/show_bug.cgi?id=218236#c11> + this._response(false, ex ? ex : new UserCancelledException()); + }, + /** + * Allows caller or lib to be notified when the call is done. + * This is useful to enable and disable a Cancel button in the UI, + * which allows to cancel the network request. + */ + setFinishedCallback(finishedCallback) { + this._finishedCallback = finishedCallback; + }, +}; + +function ServerException(msg, code, uri) { + Exception.call(this, msg); + this.code = code; + this.uri = uri; +} +ServerException.prototype = Object.create(Exception.prototype); +ServerException.prototype.constructor = ServerException; diff --git a/comm/mail/components/accountcreation/GuessConfig.jsm b/comm/mail/components/accountcreation/GuessConfig.jsm new file mode 100644 index 0000000000..3d590311d9 --- /dev/null +++ b/comm/mail/components/accountcreation/GuessConfig.jsm @@ -0,0 +1,1317 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["GuessConfig"]; + +const { AccountCreationUtils } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); +const lazy = {}; +ChromeUtils.defineModuleGetter( + lazy, + "AccountConfig", + "resource:///modules/accountcreation/AccountConfig.jsm" +); +ChromeUtils.defineModuleGetter( + lazy, + "Sanitizer", + "resource:///modules/accountcreation/Sanitizer.jsm" +); + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +const { + Abortable, + alertPrompt, + assert, + CancelledException, + ddump, + deepCopy, + Exception, + gAccountSetupLogger, + getStringBundle, + NotReached, + UserCancelledException, +} = AccountCreationUtils; + +/** + * Try to guess the config, by: + * - guessing hostnames (pop3.<domain>, pop.<domain>, imap.<domain>, + * mail.<domain> etc.) + * - probing known ports (for IMAP, POP3 etc., with SSL, STARTTLS etc.) + * - opening a connection via the right protocol and checking the + * protocol-specific CAPABILITIES like that the server returns. + * + * Final verification is not done here, but in verifyConfig(). + * + * This function is async. + * + * @param domain {String} the domain part of the email address + * @param progressCallback {function(type, hostname, port, socketType, done)} + * Called when we try a new hostname/port. + * type {String-enum} @see AccountConfig type - "imap", "pop3", "smtp" + * hostname {String} + * port {Integer} + * socketType {nsMsgSocketType} @see MailNewsTypes2.idl + * 0 = plain, 2 = STARTTLS, 3 = SSL + * done {Boolean} false, if we start probing this host/port, true if we're + * done and the host is good. (there is no notification when a host is + * bad, we'll just tell about the next host tried) + * @param successCallback {function(config {AccountConfig})} + * Called when we could guess the config. + * param accountConfig {AccountConfig} The guessed account config. + * username, password, realname, emailaddress etc. are not filled out, + * but placeholders to be filled out via replaceVariables(). + * @param errorCallback function(ex) + * Called when we could guess not the config, either + * because we have not found anything or + * because there was an error (e.g. no network connection). + * The ex.message will contain a user-presentable message. + * @param resultConfig {AccountConfig} (optional) + * A config which may be partially filled in. If so, it will be used as base + * for the guess. + * @param which {String-enum} (optional) "incoming", "outgoing", or "both". + * Default "both". Whether to guess only the incoming or outgoing server. + * @result {Abortable} Allows you to cancel the guess + */ +function guessConfig( + domain, + progressCallback, + successCallback, + errorCallback, + resultConfig, + which +) { + assert(typeof progressCallback == "function", "need progressCallback"); + assert(typeof successCallback == "function", "need successCallback"); + assert(typeof errorCallback == "function", "need errorCallback"); + + // Servers that we know enough that they support OAuth2 do not need guessing. + if (resultConfig.incoming.auth == Ci.nsMsgAuthMethod.OAuth2) { + successCallback(resultConfig); + return new Abortable(); + } + + if (!resultConfig) { + resultConfig = new lazy.AccountConfig(); + } + resultConfig.source = lazy.AccountConfig.kSourceGuess; + + if (!which) { + which = "both"; + } + + if (!Services.prefs.getBoolPref("mailnews.auto_config.guess.enabled")) { + errorCallback("Guessing config disabled per user preference"); + return new Abortable(); + } + + var incomingHostDetector = null; + var outgoingHostDetector = null; + var incomingEx = null; // if incoming had error, store ex here + var outgoingEx = null; // if incoming had error, store ex here + var incomingDone = which == "outgoing"; + var outgoingDone = which == "incoming"; + // If we're offline, we're going to pick the most common settings. + // (Not the "best" settings, but common). + if (Services.io.offline) { + // TODO: don't do this. Bug 599173. + resultConfig.source = lazy.AccountConfig.kSourceUser; + resultConfig.incoming.hostname = "mail." + domain; + resultConfig.incoming.username = resultConfig.identity.emailAddress; + resultConfig.outgoing.username = resultConfig.identity.emailAddress; + resultConfig.incoming.type = "imap"; + resultConfig.incoming.port = 143; + resultConfig.incoming.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + resultConfig.incoming.auth = Ci.nsMsgAuthMethod.passwordCleartext; + resultConfig.outgoing.hostname = "smtp." + domain; + resultConfig.outgoing.socketType = Ci.nsMsgSocketType.alwaysSTARTTLS; + resultConfig.outgoing.port = 587; + resultConfig.outgoing.auth = Ci.nsMsgAuthMethod.passwordCleartext; + resultConfig.incomingAlternatives.push({ + hostname: "mail." + domain, + username: resultConfig.identity.emailAddress, + type: "pop3", + port: 110, + socketType: Ci.nsMsgSocketType.alwaysSTARTTLS, + auth: Ci.nsMsgAuthMethod.passwordCleartext, + }); + successCallback(resultConfig); + return new Abortable(); + } + var progress = function (thisTry) { + progressCallback( + protocolToString(thisTry.protocol), + thisTry.hostname, + thisTry.port, + thisTry.socketType, + false, + resultConfig + ); + }; + + var checkDone = function () { + if (incomingEx) { + try { + errorCallback(incomingEx, resultConfig); + } catch (e) { + console.error(e); + alertPrompt("Error in errorCallback for guessConfig()", e); + } + return; + } + if (outgoingEx) { + try { + errorCallback(outgoingEx, resultConfig); + } catch (e) { + console.error(e); + alertPrompt("Error in errorCallback for guessConfig()", e); + } + return; + } + if (incomingDone && outgoingDone) { + try { + successCallback(resultConfig); + } catch (e) { + try { + errorCallback(e); + } catch (e) { + console.error(e); + alertPrompt("Error in errorCallback for guessConfig()", e); + } + } + } + }; + + var logger = gAccountSetupLogger; + var HostTryToAccountServer = function (thisTry, server) { + server.type = protocolToString(thisTry.protocol); + server.hostname = thisTry.hostname; + server.port = thisTry.port; + server.socketType = thisTry.socketType; + server.auth = + thisTry.authMethod || chooseBestAuthMethod(thisTry.authMethods); + server.authAlternatives = thisTry.authMethods; + // TODO + // cert is also bad when targetSite is set. (Same below for incoming.) + // Fix SSLErrorHandler and security warning dialog in accountSetup.js. + server.badCert = thisTry.selfSignedCert; + server.targetSite = thisTry.targetSite; + logger.info( + "CHOOSING " + + server.type + + " " + + server.hostname + + ":" + + server.port + + ", auth method " + + server.auth + + (server.authAlternatives.length + ? " " + server.authAlternatives.join(",") + : "") + + ", socketType " + + server.socketType + + (server.badCert ? " (bad cert!)" : "") + ); + }; + + var outgoingSuccess = function (thisTry, alternativeTries) { + assert(thisTry.protocol == SMTP, "I only know SMTP for outgoing"); + // Ensure there are no previously saved outgoing errors, if we've got + // success here. + outgoingEx = null; + HostTryToAccountServer(thisTry, resultConfig.outgoing); + + for (let alternativeTry of alternativeTries) { + // resultConfig.createNewOutgoing(); misses username etc., so copy + let altServer = deepCopy(resultConfig.outgoing); + HostTryToAccountServer(alternativeTry, altServer); + assert(resultConfig.outgoingAlternatives); + resultConfig.outgoingAlternatives.push(altServer); + } + + progressCallback( + resultConfig.outgoing.type, + resultConfig.outgoing.hostname, + resultConfig.outgoing.port, + resultConfig.outgoing.socketType, + true, + resultConfig + ); + outgoingDone = true; + checkDone(); + }; + + var incomingSuccess = function (thisTry, alternativeTries) { + // Ensure there are no previously saved incoming errors, if we've got + // success here. + incomingEx = null; + HostTryToAccountServer(thisTry, resultConfig.incoming); + + for (let alternativeTry of alternativeTries) { + // resultConfig.createNewIncoming(); misses username etc., so copy + let altServer = deepCopy(resultConfig.incoming); + HostTryToAccountServer(alternativeTry, altServer); + assert(resultConfig.incomingAlternatives); + resultConfig.incomingAlternatives.push(altServer); + } + + progressCallback( + resultConfig.incoming.type, + resultConfig.incoming.hostname, + resultConfig.incoming.port, + resultConfig.incoming.socketType, + true, + resultConfig + ); + incomingDone = true; + checkDone(); + }; + + var incomingError = function (ex) { + incomingEx = ex; + checkDone(); + incomingHostDetector.cancel(new CancelOthersException()); + outgoingHostDetector.cancel(new CancelOthersException()); + }; + + var outgoingError = function (ex) { + outgoingEx = ex; + checkDone(); + incomingHostDetector.cancel(new CancelOthersException()); + outgoingHostDetector.cancel(new CancelOthersException()); + }; + + incomingHostDetector = new IncomingHostDetector( + progress, + incomingSuccess, + incomingError + ); + outgoingHostDetector = new OutgoingHostDetector( + progress, + outgoingSuccess, + outgoingError + ); + if (which == "incoming" || which == "both") { + incomingHostDetector.start( + resultConfig.incoming.hostname ? resultConfig.incoming.hostname : domain, + !!resultConfig.incoming.hostname, + resultConfig.incoming.type, + resultConfig.incoming.port, + resultConfig.incoming.socketType, + resultConfig.incoming.auth + ); + } + if (which == "outgoing" || which == "both") { + outgoingHostDetector.start( + resultConfig.outgoing.hostname ? resultConfig.outgoing.hostname : domain, + !!resultConfig.outgoing.hostname, + "smtp", + resultConfig.outgoing.port, + resultConfig.outgoing.socketType, + resultConfig.outgoing.auth + ); + } + + return new GuessAbortable(incomingHostDetector, outgoingHostDetector); +} + +function GuessAbortable(incomingHostDetector, outgoingHostDetector) { + Abortable.call(this); + this._incomingHostDetector = incomingHostDetector; + this._outgoingHostDetector = outgoingHostDetector; +} +GuessAbortable.prototype = Object.create(Abortable.prototype); +GuessAbortable.prototype.constructor = GuessAbortable; +GuessAbortable.prototype.cancel = function (ex) { + this._incomingHostDetector.cancel(ex); + this._outgoingHostDetector.cancel(ex); +}; + +// -------------- +// Implementation + +// Objects, functions and constants that follow are not to be used outside +// this file. +var kNotTried = 0; +var kOngoing = 1; +var kFailed = 2; +var kSuccess = 3; + +/** + * Internal object holding one server that we should try or did try. + * Used as |thisTry|. + * + * Note: The consts it uses for protocol is defined towards the end of this file + * and not the same as those used in AccountConfig (type). (fix + * this) + */ +function HostTry() {} +HostTry.prototype = { + // IMAP, POP or SMTP + protocol: UNKNOWN, + // {String} + hostname: undefined, + // {Integer} + port: undefined, + // {nsMsgSocketType} + socketType: UNKNOWN, + // {String} what to send to server + commands: null, + // {Integer-enum} kNotTried, kOngoing, kFailed or kSuccess + status: kNotTried, + // {Abortable} allows to cancel the socket comm + abortable: null, + + // {Array of {Integer-enum}} @see _advertisesAuthMethods() result + // Info about the server, from the protocol and SSL chat + authMethods: null, + // {String} Whether the SSL cert is not from a proper CA + selfSignedCert: false, + // {String} Which host the SSL cert is made for, if not hostname. + // If set, this is an SSL error. + targetSite: null, +}; + +/** + * When the success or errorCallbacks are called to abort the other requests + * which happened in parallel, this ex is used as param for cancel(), so that + * the cancel doesn't trigger another callback. + */ +function CancelOthersException() { + CancelledException.call(this, "we're done, cancelling the other probes"); +} +CancelOthersException.prototype = Object.create(CancelledException.prototype); +CancelOthersException.prototype.constructor = CancelOthersException; + +/** + * @param successCallback {function(result {HostTry}, alts {Array of HostTry})} + * Called when the config is OK + * |result| is the most preferred server. + * |alts| currently exists only for |IncomingHostDetector| and contains + * some servers of the other type (POP3 instead of IMAP), if available. + * @param errorCallback {function(ex)} Called when we could not find a config + * @param progressCallback { function(server {HostTry}) } Called when we tried + * (will try?) a new hostname and port + */ +function HostDetector(progressCallback, successCallback, errorCallback) { + this.mSuccessCallback = successCallback; + this.mProgressCallback = progressCallback; + this.mErrorCallback = errorCallback; + this._cancel = false; + // {Array of {HostTry}}, ordered by decreasing preference + this._hostsToTry = []; + + // init logging + this._log = gAccountSetupLogger; + this._log.info("created host detector"); +} + +HostDetector.prototype = { + cancel(ex) { + this._cancel = true; + // We have to actively stop the network calls, as they may result in + // callbacks e.g. to the cert handler. If the dialog is gone by the time + // this happens, the javascript stack is horked. + for (let i = 0; i < this._hostsToTry.length; i++) { + let thisTry = this._hostsToTry[i]; // {HostTry} + if (thisTry.abortable) { + thisTry.abortable.cancel(ex); + } + thisTry.status = kFailed; // or don't set? Maybe we want to continue. + } + if (ex instanceof CancelOthersException) { + return; + } + if (!ex) { + ex = new CancelledException(); + } + this.mErrorCallback(ex); + }, + + /** + * Start the detection. + * + * @param {string} domain - Domain to be used as base for guessing. + * Should be a domain (e.g. yahoo.co.uk). + * If hostIsPrecise == true, it should be a full hostname. + * @param {boolean} hostIsPrecise - If true, use only this hostname, + * do not guess hostnames. + * @param {"pop3"|"imap"|"exchange"|"smtp"|""} - Account type. + * @param {integer} port - The port to use. 0 to autodetect + * @param {nsMsgSocketType|-1} socketType - Socket type. -1 to autodetect. + * @param {nsMsgAuthMethod|0} authMethod - Authentication method. 0 to autodetect. + */ + start(domain, hostIsPrecise, type, port, socketType, authMethod) { + domain = domain.replace(/\s*/g, ""); // Remove whitespace + if (!hostIsPrecise) { + hostIsPrecise = false; + } + var protocol = lazy.Sanitizer.translate( + type, + { imap: IMAP, pop3: POP, smtp: SMTP }, + UNKNOWN + ); + if (!port) { + port = UNKNOWN; + } + var ssl_only = Services.prefs.getBoolPref( + "mailnews.auto_config.guess.sslOnly" + ); + this._cancel = false; + this._log.info( + `Starting ${protocol} detection on ${ + !hostIsPrecise ? "~ " : "" + }${domain}:${port} with socketType=${socketType} and authMethod=${authMethod}` + ); + + // fill this._hostsToTry + this._hostsToTry = []; + var hostnamesToTry = []; + // if hostIsPrecise is true, it's because that's what the user input + // explicitly, and we'll just try it, nothing else. + if (hostIsPrecise) { + hostnamesToTry.push(domain); + } else { + hostnamesToTry = this._hostnamesToTry(protocol, domain); + } + + for (let i = 0; i < hostnamesToTry.length; i++) { + let hostname = hostnamesToTry[i]; + let hostEntries = this._portsToTry(hostname, protocol, socketType, port); + for (let j = 0; j < hostEntries.length; j++) { + let hostTry = hostEntries[j]; // from getHostEntry() + if (ssl_only && hostTry.socketType == NONE) { + continue; + } + hostTry.hostname = hostname; + hostTry.status = kNotTried; + hostTry.desc = + hostTry.hostname + + ":" + + hostTry.port + + " socketType=" + + hostTry.socketType + + " " + + protocolToString(hostTry.protocol); + hostTry.authMethod = authMethod; + this._hostsToTry.push(hostTry); + } + } + + this._hostsToTry = sortTriesByPreference(this._hostsToTry); + this._tryAll(); + }, + + // We make all host/port combinations run in parallel, store their + // results in an array, and as soon as one finishes successfully and all + // higher-priority ones have failed, we abort all lower-priority ones. + + _tryAll() { + if (this._cancel) { + return; + } + var me = this; + var timeout = Services.prefs.getIntPref( + "mailnews.auto_config.guess.timeout" + ); + // We assume we'll resolve the same proxy for all tries, and + // proceed to use the first resolved proxy for all tries. This + // assumption is generally sound, but not always: mechanisms like + // the pref network.proxy.no_proxies_on can make imap.domain and + // pop.domain resolve differently. + doProxy(this._hostsToTry[0].hostname, function (proxy) { + for (let i = 0; i < me._hostsToTry.length; i++) { + let thisTry = me._hostsToTry[i]; // {HostTry} + if (thisTry.status != kNotTried) { + continue; + } + me._log.info(thisTry.desc + ": initializing probe..."); + if (i == 0) { + // showing 50 servers at once is pointless + me.mProgressCallback(thisTry); + } + + thisTry.abortable = SocketUtil( + thisTry.hostname, + thisTry.port, + thisTry.socketType, + thisTry.commands, + timeout, + proxy, + new SSLErrorHandler(thisTry, me._log), + function (wiredata) { + // result callback + if (me._cancel) { + // Don't use response anymore. + return; + } + me.mProgressCallback(thisTry); + me._processResult(thisTry, wiredata); + me._checkFinished(); + }, + function (e) { + // error callback + if (me._cancel) { + // Who set cancel to true already called mErrorCallback(). + return; + } + me._log.warn(thisTry.desc + ": " + e); + thisTry.status = kFailed; + me._checkFinished(); + } + ); + thisTry.status = kOngoing; + } + }); + }, + + /** + * @param {HostTry} thisTry + * @param {string[]} wiredata - What the server returned in response to our protocol chat. + */ + _processResult(thisTry, wiredata) { + if (thisTry._gotCertError) { + if (thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_MISMATCH) { + thisTry._gotCertError = 0; + thisTry.status = kFailed; + return; + } + + if ( + thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_UNTRUSTED || + thisTry._gotCertError == Ci.nsICertOverrideService.ERROR_TIME + ) { + this._log.info( + thisTry.desc + ": TRYING AGAIN, hopefully with exception recorded" + ); + thisTry._gotCertError = 0; + thisTry.selfSignedCert = true; // _next_ run gets this exception + thisTry.status = kNotTried; // try again (with exception) + this._tryAll(); + return; + } + } + + if (wiredata == null || wiredata === undefined) { + this._log.info(thisTry.desc + ": no data"); + thisTry.status = kFailed; + return; + } + this._log.info(thisTry.desc + ": wiredata: " + wiredata.join("")); + thisTry.authMethods = this._advertisesAuthMethods( + thisTry.protocol, + wiredata + ); + if ( + thisTry.socketType == STARTTLS && + !this._hasSTARTTLS(thisTry, wiredata) + ) { + this._log.info(thisTry.desc + ": STARTTLS wanted, but not offered"); + thisTry.status = kFailed; + return; + } + this._log.info( + thisTry.desc + + ": success" + + (thisTry.selfSignedCert ? " (selfSignedCert)" : "") + ); + thisTry.status = kSuccess; + + if (thisTry.selfSignedCert) { + // eh, ERROR_UNTRUSTED or ERROR_TIME + // We clear the temporary override now after success. If we clear it + // earlier we get into an infinite loop, probably because the cert + // remembering is temporary and the next try gets a new connection which + // isn't covered by that temporariness. + this._log.info( + thisTry.desc + ": clearing validity override for " + thisTry.hostname + ); + Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService) + .clearValidityOverride(thisTry.hostname, thisTry.port, {}); + } + }, + + _checkFinished() { + var successfulTry = null; + var successfulTryAlternative = null; // POP3 + var unfinishedBusiness = false; + // this._hostsToTry is ordered by decreasing preference + for (let i = 0; i < this._hostsToTry.length; i++) { + let thisTry = this._hostsToTry[i]; + if (thisTry.status == kNotTried || thisTry.status == kOngoing) { + unfinishedBusiness = true; + } else if (thisTry.status == kSuccess && !unfinishedBusiness) { + // thisTry is good, and all higher preference tries failed, so use this + if (!successfulTry) { + successfulTry = thisTry; + if (successfulTry.protocol == SMTP) { + break; + } + } else if (successfulTry.protocol != thisTry.protocol) { + successfulTryAlternative = thisTry; + break; + } + } + } + if (successfulTry && (successfulTryAlternative || !unfinishedBusiness)) { + this.mSuccessCallback( + successfulTry, + successfulTryAlternative ? [successfulTryAlternative] : [] + ); + this.cancel(new CancelOthersException()); + } else if (!unfinishedBusiness) { + // all failed + this._log.info("ran out of options"); + var errorMsg = getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties" + ).GetStringFromName("cannot_find_server.error"); + this.mErrorCallback(new Exception(errorMsg)); + // no need to cancel, all failed + } + // else let ongoing calls continue + }, + + /** + * Which auth mechanism the server claims to support. + * That doesn't necessarily reflect reality, it is more an upper bound. + * + * @param {integer} protocol - IMAP, POP or SMTP + * @param {string[]} capaResponse - On the wire data that the server returned. + * May be the full exchange or just capa. + * @returns {nsMsgAuthMethod[]} Advertised authentication methods, + * in decreasing order of preference. + * E.g. [ nsMsgAuthMethod.GSSAPI, nsMsgAuthMethod.passwordEncrypted ] + * for a server that supports only Kerberos and encrypted passwords. + */ + _advertisesAuthMethods(protocol, capaResponse) { + // For IMAP, capabilities include e.g.: + // "AUTH=CRAM-MD5", "AUTH=NTLM", "AUTH=GSSAPI", "AUTH=MSN", "AUTH=PLAIN" + // for POP3, the auth mechanisms are returned in capa as the following: + // "CRAM-MD5", "NTLM", "MSN", "GSSAPI" + // For SMTP, EHLO will return AUTH and then a list of the + // mechanism(s) supported, e.g., + // AUTH LOGIN NTLM MSN CRAM-MD5 GSSAPI + var supported = new Set(); + var line = capaResponse.join("\n").toUpperCase(); + var prefix = ""; + if (protocol == POP) { + prefix = ""; + } else if (protocol == IMAP) { + prefix = "AUTH="; + } else if (protocol == SMTP) { + prefix = "AUTH.*"; + } else { + throw NotReached("must pass protocol"); + } + // add in decreasing order of preference + if (new RegExp(prefix + "GSSAPI").test(line)) { + supported.add(Ci.nsMsgAuthMethod.GSSAPI); + } + if (new RegExp(prefix + "CRAM-MD5").test(line)) { + supported.add(Ci.nsMsgAuthMethod.passwordEncrypted); + } + if (new RegExp(prefix + "(NTLM|MSN)").test(line)) { + supported.add(Ci.nsMsgAuthMethod.NTLM); + } + if (new RegExp(prefix + "LOGIN").test(line)) { + supported.add(Ci.nsMsgAuthMethod.passwordCleartext); + } + if (new RegExp(prefix + "PLAIN").test(line)) { + supported.add(Ci.nsMsgAuthMethod.passwordCleartext); + } + if (protocol != IMAP || !line.includes("LOGINDISABLED")) { + supported.add(Ci.nsMsgAuthMethod.passwordCleartext); + } + // The array elements will be in the Set's order of addition. + return Array.from(supported); + }, + + _hasSTARTTLS(thisTry, wiredata) { + var capa = thisTry.protocol == POP ? "STLS" : "STARTTLS"; + return ( + thisTry.socketType == STARTTLS && + wiredata.join("").toUpperCase().includes(capa) + ); + }, +}; + +/** + * @param {nsMsgAuthMethod[]} authMethods - Authentication methods to choose from. + * See return value of _advertisesAuthMethods() + * Note: the returned auth method will be removed from the array. + * @returns {nsMsgAuthMethod} one of them, the preferred one + * Note: this might be Kerberos, which might not actually work, + * so you might need to try the others, too. + */ +function chooseBestAuthMethod(authMethods) { + if (!authMethods || !authMethods.length) { + return Ci.nsMsgAuthMethod.passwordCleartext; + } + return authMethods.shift(); // take first (= most preferred) +} + +function IncomingHostDetector( + progressCallback, + successCallback, + errorCallback +) { + HostDetector.call(this, progressCallback, successCallback, errorCallback); +} +IncomingHostDetector.prototype = { + __proto__: HostDetector.prototype, + _hostnamesToTry(protocol, domain) { + var hostnamesToTry = []; + if (protocol != POP) { + hostnamesToTry.push("imap." + domain); + } + if (protocol != IMAP) { + hostnamesToTry.push("pop3." + domain); + hostnamesToTry.push("pop." + domain); + } + hostnamesToTry.push("mail." + domain); + hostnamesToTry.push(domain); + return hostnamesToTry; + }, + _portsToTry: getIncomingTryOrder, +}; + +function OutgoingHostDetector( + progressCallback, + successCallback, + errorCallback +) { + HostDetector.call(this, progressCallback, successCallback, errorCallback); +} +OutgoingHostDetector.prototype = { + __proto__: HostDetector.prototype, + _hostnamesToTry(protocol, domain) { + var hostnamesToTry = []; + hostnamesToTry.push("smtp." + domain); + hostnamesToTry.push("mail." + domain); + hostnamesToTry.push(domain); + return hostnamesToTry; + }, + _portsToTry: getOutgoingTryOrder, +}; + +// --------------------------------------------- +// Encode protocol ports and order of preference + +// Protocol Types +var UNKNOWN = -1; +var IMAP = 0; +var POP = 1; +var SMTP = 2; +// Security Types +var NONE = Ci.nsMsgSocketType.plain; +var STARTTLS = Ci.nsMsgSocketType.alwaysSTARTTLS; +var SSL = Ci.nsMsgSocketType.SSL; + +var IMAP_PORTS = {}; +IMAP_PORTS[NONE] = 143; +IMAP_PORTS[STARTTLS] = 143; +IMAP_PORTS[SSL] = 993; + +var POP_PORTS = {}; +POP_PORTS[NONE] = 110; +POP_PORTS[STARTTLS] = 110; +POP_PORTS[SSL] = 995; + +var SMTP_PORTS = {}; +SMTP_PORTS[NONE] = 587; +SMTP_PORTS[STARTTLS] = 587; +SMTP_PORTS[SSL] = 465; + +var CMDS = {}; +CMDS[IMAP] = ["1 CAPABILITY\r\n", "2 LOGOUT\r\n"]; +CMDS[POP] = ["CAPA\r\n", "QUIT\r\n"]; +CMDS[SMTP] = ["EHLO we-guess.mozilla.org\r\n", "QUIT\r\n"]; + +/** + * Sort by preference of SSL, IMAP etc. + * + * @param tries {Array of {HostTry}} + * @returns {Array of {HostTry}} + */ +function sortTriesByPreference(tries) { + return tries.sort(function (a, b) { + // -1 = a is better; 1 = b is better; 0 = equal + // Prefer SSL/STARTTLS above all else + if (a.socketType != NONE && b.socketType == NONE) { + return -1; + } + if (b.socketType != NONE && a.socketType == NONE) { + return 1; + } + // Prefer IMAP over POP + if (a.protocol == IMAP && b.protocol == POP) { + return -1; + } + if (b.protocol == IMAP && a.protocol == POP) { + return 1; + } + // Prefer SSL/TLS over STARTTLS + if (a.socketType == SSL && b.socketType == STARTTLS) { + return -1; + } + if (a.socketType == STARTTLS && b.socketType == SSL) { + return 1; + } + // For hostnames, leave existing sorting, as in _hostnamesToTry() + // For ports, leave existing sorting, as in getOutgoingTryOrder() + return 0; + }); +} + +/** + * @returns {HostTry[]} Hosts to try. + */ +function getIncomingTryOrder(host, protocol, socketType, port) { + var lowerCaseHost = host.toLowerCase(); + + if ( + protocol == UNKNOWN && + (lowerCaseHost.startsWith("pop.") || lowerCaseHost.startsWith("pop3.")) + ) { + protocol = POP; + } else if (protocol == UNKNOWN && lowerCaseHost.startsWith("imap.")) { + protocol = IMAP; + } + + if (protocol != UNKNOWN) { + if (socketType == UNKNOWN) { + return [ + getHostEntry(protocol, STARTTLS, port), + getHostEntry(protocol, SSL, port), + getHostEntry(protocol, NONE, port), + ]; + } + return [getHostEntry(protocol, socketType, port)]; + } + if (socketType == UNKNOWN) { + return [ + getHostEntry(IMAP, STARTTLS, port), + getHostEntry(IMAP, SSL, port), + getHostEntry(POP, STARTTLS, port), + getHostEntry(POP, SSL, port), + getHostEntry(IMAP, NONE, port), + getHostEntry(POP, NONE, port), + ]; + } + return [ + getHostEntry(IMAP, socketType, port), + getHostEntry(POP, socketType, port), + ]; +} + +/** + * @returns {Array of {HostTry}} + */ +function getOutgoingTryOrder(host, protocol, socketType, port) { + assert(protocol == SMTP, "need SMTP as protocol for outgoing"); + if (socketType == UNKNOWN) { + if (port == UNKNOWN) { + // neither SSL nor port known + return [ + getHostEntry(SMTP, STARTTLS, UNKNOWN), + getHostEntry(SMTP, STARTTLS, 25), + getHostEntry(SMTP, SSL, UNKNOWN), + getHostEntry(SMTP, NONE, UNKNOWN), + getHostEntry(SMTP, NONE, 25), + ]; + } + // port known, SSL not + return [ + getHostEntry(SMTP, STARTTLS, port), + getHostEntry(SMTP, SSL, port), + getHostEntry(SMTP, NONE, port), + ]; + } + // SSL known, port not + if (port == UNKNOWN) { + if (socketType == SSL) { + return [getHostEntry(SMTP, SSL, UNKNOWN)]; + } + return [ + getHostEntry(SMTP, socketType, UNKNOWN), + getHostEntry(SMTP, socketType, 25), + ]; + } + // SSL and port known + return [getHostEntry(SMTP, socketType, port)]; +} + +/** + * @returns {HostTry} with proper default port and commands, + * but without hostname. + */ +function getHostEntry(protocol, socketType, port) { + if (!port || port == UNKNOWN) { + switch (protocol) { + case POP: + port = POP_PORTS[socketType]; + break; + case IMAP: + port = IMAP_PORTS[socketType]; + break; + case SMTP: + port = SMTP_PORTS[socketType]; + break; + default: + throw new NotReached("unsupported protocol " + protocol); + } + } + + var r = new HostTry(); + r.protocol = protocol; + r.socketType = socketType; + r.port = port; + r.commands = CMDS[protocol]; + return r; +} + +// here -> AccountConfig +function protocolToString(type) { + if (type == IMAP) { + return "imap"; + } + if (type == POP) { + return "pop3"; + } + if (type == SMTP) { + return "smtp"; + } + throw new NotReached("unexpected protocol"); +} + +// ---------------------- +// SSL cert error handler + +/** + * @param thisTry {HostTry} + * @param logger {ConsoleAPI} + */ +function SSLErrorHandler(thisTry, logger) { + this._try = thisTry; + this._log = logger; + // _ gotCertError will be set to an error code (one of those defined in + // nsICertOverrideService) + this._gotCertError = 0; +} +SSLErrorHandler.prototype = { + processCertError(secInfo, targetSite) { + this._log.error("Got Cert error for " + targetSite); + + if (!secInfo) { + return; + } + + let cert = secInfo.serverCert; + + let parts = targetSite.split(":"); + let host = parts[0]; + let port = parts[1]; + + /* The following 2 cert problems are unfortunately common: + * 1) hostname mismatch: + * user is customer at a domain hoster, he owns yourname.org, + * and the IMAP server is imap.hoster.com (but also reachable as + * imap.yourname.org), and has a cert for imap.hoster.com. + * 2) self-signed: + * a company has an internal IMAP server, and it's only for + * 30 employees, and they didn't want to buy a cert, so + * they use a self-signed cert. + * + * We would like the above to pass, somehow, with user confirmation. + * The following case should *not* pass: + * + * 1) MITM + * User has @gmail.com, and an attacker is between the user and + * the Internet and runs a man-in-the-middle (MITM) attack. + * Attacker controls DNS and sends imap.gmail.com to his own + * imap.attacker.com. He has either a valid, CA-issued + * cert for imap.attacker.com, or a self-signed cert. + * Of course, attacker.com could also be legit-sounding gmailservers.com. + * + * What makes it dangerous is that we (!) propose the server to the user, + * and he cannot judge whether imap.gmailservers.com is correct or not, + * and he will likely approve it. + */ + + if (secInfo.isDomainMismatch) { + this._try._gotCertError = Ci.nsICertOverrideService.ERROR_MISMATCH; + } else if (secInfo.isUntrusted) { + // e.g. self-signed + this._try._gotCertError = Ci.nsICertOverrideService.ERROR_UNTRUSTED; + } else if (secInfo.isNotValidAtThisTime) { + this._try._gotCertError = Ci.nsICertOverrideService.ERROR_TIME; + } else { + this._try._gotCertError = -1; // other + } + + /* We will add a temporary cert exception here, so that + * we can continue and connect and try. + * But we will remove it again as soon as we close the + * connection, in _processResult(). + * _gotCertError will serve as the marker that we + * have to clear the override later. + * + * In verifyConfig(), before we send the password, we *must* + * get another cert exception, this time with dialog to the user + * so that he gets informed about this and can make a choice. + */ + this._try.targetSite = targetSite; + Cc["@mozilla.org/security/certoverride;1"] + .getService(Ci.nsICertOverrideService) + .rememberValidityOverride(host, port, {}, cert, true); // temporary override + this._log.warn(`Added temporary override of bad cert for: ${host}:${port}`); + }, +}; + +// ----------- +// Socket Util + +/** + * @param hostname {String} The DNS hostname to connect to. + * @param port {Integer} The numeric port to connect to on the host. + * @param socketType {nsMsgSocketType} SSL, STARTTLS or NONE + * @param commands {Array of String}: protocol commands + * to send to the server. + * @param timeout {Integer} seconds to wait for a server response, then cancel. + * @param proxy {nsIProxyInfo} The proxy to use (or null to not use any). + * @param sslErrorHandler {SSLErrorHandler} + * @param resultCallback {function(wiredata)} This function will + * be called with the result string array from the server + * or null if no communication occurred. + * @param errorCallback {function(e)} + */ +function SocketUtil( + hostname, + port, + socketType, + commands, + timeout, + proxy, + sslErrorHandler, + resultCallback, + errorCallback +) { + assert(commands && commands.length, "need commands"); + + var index = 0; // commands[index] is next to send to server + var initialized = false; + var aborted = false; + + function _error(e) { + if (aborted) { + return; + } + aborted = true; + errorCallback(e); + } + + function timeoutFunc() { + if (!initialized) { + _error("timeout"); + } + } + + // In case DNS takes too long or does not resolve or another blocking + // issue occurs before the timeout can be set on the socket, this + // ensures that the listener callback will be fired in a timely manner. + // XXX There might to be some clean up needed after the timeout is fired + // for socket and io resources. + + // The timeout value plus 2 seconds + setTimeout(timeoutFunc, timeout * 1000 + 2000); + + var transportService = Cc[ + "@mozilla.org/network/socket-transport-service;1" + ].getService(Ci.nsISocketTransportService); + + // @see NS_NETWORK_SOCKET_CONTRACTID_PREFIX + var socketTypeName; + if (socketType == SSL) { + socketTypeName = ["ssl"]; + } else if (socketType == STARTTLS) { + socketTypeName = ["starttls"]; + } else { + socketTypeName = []; + } + var transport = transportService.createTransport( + socketTypeName, + hostname, + port, + proxy, + null + ); + + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_CONNECT, timeout); + transport.setTimeout(Ci.nsISocketTransport.TIMEOUT_READ_WRITE, timeout); + + var outstream = transport.openOutputStream(0, 0, 0); + var stream = transport.openInputStream(0, 0, 0); + var instream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + instream.init(stream); + + var dataListener = { + data: [], + onStartRequest(request) { + try { + initialized = true; + if (!aborted) { + // Send the first request + let outputData = commands[index++]; + outstream.write(outputData, outputData.length); + } + } catch (e) { + _error(e); + } + }, + async onStopRequest(request, status) { + try { + instream.close(); + outstream.close(); + // Did it fail because of a bad certificate? + let isCertError = false; + if (!Components.isSuccessCode(status)) { + let nssErrorsService = Cc[ + "@mozilla.org/nss_errors_service;1" + ].getService(Ci.nsINSSErrorsService); + try { + let errorType = nssErrorsService.getErrorClass(status); + if (errorType == Ci.nsINSSErrorsService.ERROR_CLASS_BAD_CERT) { + isCertError = true; + } + } catch (e) { + // nsINSSErrorsService.getErrorClass throws if given a non-STARTTLS, + // non-cert error, so ignore this. + } + } + if (isCertError) { + if ( + Services.prefs.getBoolPref( + "mailnews.auto_config.guess.requireGoodCert", + true + ) + ) { + gAccountSetupLogger.info( + `Bad (overridable) certificate for ${hostname}:${port}. Set mailnews.auto_config.guess.requireGoodCert to false to allow detecting this as a valid SSL/TLS configuration` + ); + } else { + let socketTransport = transport.QueryInterface( + Ci.nsISocketTransport + ); + let secInfo = + await socketTransport.tlsSocketControl?.asyncGetSecurityInfo(); + sslErrorHandler.processCertError(secInfo, hostname + ":" + port); + } + } + resultCallback(this.data.length ? this.data : null); + } catch (e) { + _error(e); + } + }, + onDataAvailable(request, inputStream, offset, count) { + try { + if (!aborted) { + let inputData = instream.read(count); + this.data.push(inputData); + if (index < commands.length) { + // Send the next request to the server. + let outputData = commands[index++]; + outstream.write(outputData, outputData.length); + } + } + } catch (e) { + _error(e); + } + }, + }; + + try { + var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( + Ci.nsIInputStreamPump + ); + + pump.init(stream, 0, 0, false); + pump.asyncRead(dataListener); + return new SocketAbortable(transport); + } catch (e) { + _error(e); + } + return null; +} + +function SocketAbortable(transport) { + Abortable.call(this); + assert(transport instanceof Ci.nsITransport, "need transport"); + this._transport = transport; +} +SocketAbortable.prototype = Object.create(Abortable.prototype); +SocketAbortable.prototype.constructor = UserCancelledException; +SocketAbortable.prototype.cancel = function (ex) { + try { + this._transport.close(Cr.NS_ERROR_ABORT); + } catch (e) { + ddump("canceling socket failed: " + e); + } +}; + +/** + * Resolve a proxy for some domain and expose it via a callback. + * + * @param hostname {String} The hostname which a proxy will be resolved for + * @param resultCallback {function(proxyInfo)} + * Called after the proxy has been resolved for hostname. + * proxy {nsIProxyInfo} The resolved proxy, or null if none were found + * for hostname + */ +function doProxy(hostname, resultCallback) { + // This implements the nsIProtocolProxyCallback interface: + function ProxyResolveCallback() {} + ProxyResolveCallback.prototype = { + onProxyAvailable(req, uri, proxy, status) { + // Anything but a SOCKS proxy will be unusable for email. + if (proxy != null && proxy.type != "socks" && proxy.type != "socks4") { + proxy = null; + } + resultCallback(proxy); + }, + }; + var proxyService = Cc[ + "@mozilla.org/network/protocol-proxy-service;1" + ].getService(Ci.nsIProtocolProxyService); + // Use some arbitrary scheme just because it is required... + var uri = Services.io.newURI("http://" + hostname); + // ... we'll ignore it any way. We prefer SOCKS since that's the + // only thing we can use for email protocols. + var proxyFlags = + Ci.nsIProtocolProxyService.RESOLVE_IGNORE_URI_SCHEME | + Ci.nsIProtocolProxyService.RESOLVE_PREFER_SOCKS_PROXY; + if (Services.prefs.getBoolPref("network.proxy.socks_remote_dns")) { + proxyFlags |= Ci.nsIProtocolProxyService.RESOLVE_ALWAYS_TUNNEL; + } + proxyService.asyncResolve(uri, proxyFlags, new ProxyResolveCallback()); +} + +var GuessConfig = { + UNKNOWN, + IMAP, + POP, + SMTP, + NONE, + STARTTLS, + SSL, + getHostEntry, + getIncomingTryOrder, + getOutgoingTryOrder, + guessConfig, +}; diff --git a/comm/mail/components/accountcreation/Sanitizer.jsm b/comm/mail/components/accountcreation/Sanitizer.jsm new file mode 100644 index 0000000000..d6bc3918bc --- /dev/null +++ b/comm/mail/components/accountcreation/Sanitizer.jsm @@ -0,0 +1,249 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["Sanitizer"]; + +const { AccountCreationUtils } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); + +const { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import( + "resource:///modules/hostnameUtils.jsm" +); + +/** + * This is a generic input validation lib. Use it when you process + * data from the network. + * + * Just a few functions which verify, for security purposes, that the + * input variables (strings, if nothing else is noted) are of the expected + * type and syntax. + * + * The functions take a string (unless noted otherwise) and return + * the expected datatype in JS types. If the value is not as expected, + * they throw exceptions. + */ + +// To debug, set mail.setup.loglevel="All" and kDebug = true. +var kDebug = false; + +var Sanitizer = { + integer(unchecked) { + if (typeof unchecked == "number" && !isNaN(unchecked)) { + return unchecked; + } + + var r = parseInt(unchecked); + if (isNaN(r)) { + throw new MalformedException("no_number.error", unchecked); + } + + return r; + }, + + integerRange(unchecked, min, max) { + var int = this.integer(unchecked); + if (int < min) { + throw new MalformedException("number_too_small.error", unchecked); + } + + if (int > max) { + throw new MalformedException("number_too_large.error", unchecked); + } + + return int; + }, + + boolean(unchecked) { + if (typeof unchecked == "boolean") { + return unchecked; + } + + if (unchecked == "true") { + return true; + } + + if (unchecked == "false") { + return false; + } + + throw new MalformedException("boolean.error", unchecked); + }, + + string(unchecked) { + return String(unchecked); + }, + + nonemptystring(unchecked) { + if (!unchecked) { + throw new MalformedException("string_empty.error", unchecked); + } + + return this.string(unchecked); + }, + + /** + * Allow only letters, numbers, "-" and "_". + * + * Empty strings not allowed (good idea?). + */ + alphanumdash(unchecked) { + var str = this.nonemptystring(unchecked); + if (!/^[a-zA-Z0-9\-\_]*$/.test(str)) { + throw new MalformedException("alphanumdash.error", unchecked); + } + + return str; + }, + + /** + * DNS hostnames like foo.bar.example.com + * Allow only letters, numbers, "-" and "." + * Empty strings not allowed. + * Currently does not support IDN (international domain names). + */ + hostname(unchecked) { + let str = cleanUpHostName(this.nonemptystring(unchecked)); + + // Allow placeholders. TODO move to a new hostnameOrPlaceholder() + // The regex is "anything, followed by one or more (placeholders than + // anything)". This doesn't catch the non-placeholder case, but that's + // handled down below. + if (/^[a-zA-Z0-9\-\.]*(%[A-Z0-9]+%[a-zA-Z0-9\-\.]*)+$/.test(str)) { + return str; + } + + if (!isLegalHostNameOrIP(str)) { + throw new MalformedException("hostname_syntax.error", unchecked); + } + + return str.toLowerCase(); + }, + + /** + * A value which resembles an email address. + */ + emailAddress(unchecked) { + let str = this.nonemptystring(unchecked); + if (!/^[a-z0-9\-%+_\.\*]+@[a-z0-9\-\.]+\.[a-z]+$/i.test(str)) { + throw new MalformedException("emailaddress_syntax.error", unchecked); + } + + return str.toLowerCase(); + }, + + /** + * A non-chrome URL that's safe to request. + */ + url(unchecked) { + var str = this.string(unchecked); + + // DANGER ZONE: data:text/javascript or data:text/html can contain + // JavaScript code, run in the caller's security context, and might allow + // arbitrary code execution, so these must be prevented at all costs. + // PNG and JPEG data: URLs are fine. But SVG is again dangerous, + // it can contain javascript, so it would create a critical security hole. + // Talk to BenB or bz before relaxing *any* of the checks in this function. + if ( + str.startsWith("data:image/png;") || + str.startsWith("data:image/jpeg;") + ) { + return new URL(str).href; + } + + if (!str.startsWith("http:") && !str.startsWith("https:")) { + throw new MalformedException("url_scheme.error", unchecked); + } + + var uri; + try { + uri = Services.io.newURI(str); + uri = uri.QueryInterface(Ci.nsIURL); + } catch (e) { + throw new MalformedException("url_parsing.error", unchecked); + } + + if (uri.scheme != "http" && uri.scheme != "https") { + throw new MalformedException("url_scheme.error", unchecked); + } + + return uri.spec; + }, + + /** + * A value which should be shown to the user in the UI as label + */ + label(unchecked) { + return this.string(unchecked); + }, + + /** + * Allows only certain values as input, otherwise throw. + * + * @param unchecked {Any} The value to check + * @param allowedValues {Array} List of values that |unchecked| may have. + * @param defaultValue {Any} (Optional) If |unchecked| does not match + * anything in |mapping|, a |defaultValue| can be returned instead of + * throwing an exception. The latter is the default and happens when + * no |defaultValue| is passed. + * @throws MalformedException + */ + enum(unchecked, allowedValues, defaultValue) { + for (let allowedValue of allowedValues) { + if (allowedValue == unchecked) { + return allowedValue; + } + } + // value is bad + if (typeof defaultValue == "undefined") { + throw new MalformedException("allowed_value.error", unchecked); + } + return defaultValue; + }, + + /** + * Like enum, allows only certain (string) values as input, but allows the + * caller to specify another value to return instead of the input value. E.g., + * if unchecked == "foo", return 1, if unchecked == "bar", return 2, + * otherwise throw. This allows to translate string enums into integer enums. + * + * @param unchecked {Any} The value to check + * @param mapping {Object} Associative array. property name is the input + * value, property value is the output value. E.g. the example above + * would be: { foo: 1, bar : 2 }. + * Use quotes when you need freaky characters: "baz-" : 3. + * @param defaultValue {Any} (Optional) If |unchecked| does not match + * anything in |mapping|, a |defaultValue| can be returned instead of + * throwing an exception. The latter is the default and happens when + * no |defaultValue| is passed. + * @throws MalformedException + */ + translate(unchecked, mapping, defaultValue) { + for (var inputValue in mapping) { + if (inputValue == unchecked) { + return mapping[inputValue]; + } + } + // value is bad + if (typeof defaultValue == "undefined") { + throw new MalformedException("allowed_value.error", unchecked); + } + return defaultValue; + }, +}; + +function MalformedException(msgID, uncheckedBadValue) { + var stringBundle = AccountCreationUtils.getStringBundle( + "chrome://messenger/locale/accountCreationUtil.properties" + ); + var msg = stringBundle.GetStringFromName(msgID); + if (typeof kDebug != "undefined" && kDebug) { + msg += " (bad value: " + uncheckedBadValue + ")"; + } + AccountCreationUtils.Exception.call(this, msg); +} +MalformedException.prototype = Object.create( + AccountCreationUtils.Exception.prototype +); +MalformedException.prototype.constructor = MalformedException; diff --git a/comm/mail/components/accountcreation/content/accountHub.js b/comm/mail/components/accountcreation/content/accountHub.js new file mode 100644 index 0000000000..a703ffce16 --- /dev/null +++ b/comm/mail/components/accountcreation/content/accountHub.js @@ -0,0 +1,277 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/** + * Holds the main controller class. + * + * @type {?AccountHubControllerClass} + */ +var AccountHubController; + +/** + * Controller class to handle the primary views of the account setup flow. + * This class acts as a sort of controller to lazily load the needed views upon + * request. It doesn't handle any data and it should only be used to switch + * between the different setup flows. + * All methods of this class should be private, except for the open() method. + */ +class AccountHubControllerClass { + /** + * The account hub main modal dialog. + * + * @type {?HTMLElement} + */ + #modal = null; + + /** + * The currently visible view inside the dialog. + * + * @type {?HTMLElement} + */ + #currentView = null; + + /** + * Object containing all strings to trigger the needed methods for the various + * views. + */ + #accounts = { + START: () => this.#viewStart(), + MAIL: () => this.#viewEmailSetup(), + CALENDAR: () => this.#viewCalendarSetup(), + ADDRESS_BOOK: () => this.#viewAddressBookSetup(), + CHAT: () => this.#viewChatSetup(), + FEED: () => this.#viewFeedSetup(), + NNTP: () => this.#viewNNTPSetup(), + IMPORT: () => this.#viewImportSetup(), + }; + + constructor() { + this.ready = this.#init(); + } + + async #init() { + await this.#loadScript("container"); + const element = document.createElement("account-hub-container"); + document.body.appendChild(element); + this.#modal = element.modal; + + let closeButton = this.#modal.querySelector("#closeButton"); + closeButton.hidden = !MailServices.accounts.accounts.length; + closeButton.addEventListener("click", () => this.#modal.close()); + + // Listen from closing requests coming from child elements. + this.#modal.addEventListener( + "request-close", + event => { + event.stopPropagation(); + this.#modal.close(); + }, + { + capture: true, + } + ); + + this.#modal.addEventListener( + "open-view", + event => { + event.stopPropagation(); + this.open(event.detail.type); + }, + { + capture: true, + } + ); + + this.#modal.addEventListener("close", event => { + // Don't allow the dialog to be closed if some operations are can't be + // aborted or the UI can't be cleared. + if (!this.#reset()) { + event.preventDefault(); + } + }); + + this.#modal.addEventListener("cancel", event => { + if ( + !MailServices.accounts.accounts.length && + !Services.prefs.getBoolPref("app.use_without_mail_account", false) + ) { + // Prevent closing the modal if no account is currently present and the + // user didn't request using Thunderbird without an email account. + event.preventDefault(); + return; + } + + // Don't allow the dialog to be canceled via the ESC key if some + // operations are in progress and can't be aborted or the UI can't be + // cleared. + if (!this.#reset()) { + event.preventDefault(); + } + }); + } + + /** + * Check if we don't currently have the needed custom element for the + * requested view and load the needed script. We do this to avoid loading all + * the unnecessary account creation files. + * + * @param {string} view - The name of the view to load. + * @returns {Promise<void>} Resolves when custom element of the view is usable. + */ + #loadScript(view) { + if (customElements.get(`account-hub-${view}`)) { + return Promise.resolve(); + } + // eslint-disable-next-line no-unsanitized/method + return import( + `chrome://messenger/content/accountcreation/views/${view}.mjs` + ); + } + + /** + * Create a custom element and append it to the modal inner HTML, or simply + * show it if it was already loaded. + * + * @param {string} id - The ID of the template to clone. + */ + #loadView(id) { + this.#hideViews(); + + let view = this.#modal.querySelector(id); + if (view) { + view.hidden = false; + this.#currentView = view; + // Update the UI to make sure we're refreshing old views. + this.#currentView.initUI(); + return; + } + + view = document.createElement(id); + this.#modal.appendChild(view); + this.#currentView = view; + } + + /** + * Hide all the currently visible views. + */ + #hideViews() { + for (let view of this.#modal.querySelectorAll(".account-hub-view")) { + view.hidden = true; + } + } + + /** + * Open the main modal dialog and load the requested account setup view, or + * fallback to the initial start screen. + * + * @param {?string} type - Which account flow to load when the modal opens. + */ + open(type = "START") { + // Interrupt if something went wrong while cleaning up a previously loaded + // view. + if (!this.#reset()) { + return; + } + + this.#accounts[type].call(); + if (!this.#modal.open) { + this.#modal.showModal(); + } + } + + /** + * Check if we have a current class and try to trigger the rest in order to + * handle abort operations and markup clean up, if possible. + * + * @returns {boolean} - True if the reset process was successful or we didn't + * have anything to reset. + */ + #reset() { + let isClean = this.#currentView?.reset() ?? true; + // If the reset operation was successful, clear the current class. + if (isClean) { + this.#hideViews(); + this.#currentView = null; + } + return isClean; + } + + /** + * Show the initial view of the account hub dialog. + */ + async #viewStart() { + await this.#loadScript("start"); + this.#loadView("account-hub-start"); + } + + /** + * Show the email setup view. + */ + async #viewEmailSetup() { + await this.#loadScript("email"); + this.#loadView("account-hub-email"); + } + + /** + * TODO: Show the calendar setup view. + */ + #viewCalendarSetup() { + console.log("Calendar setup"); + } + + /** + * TODO: Show the address book setup view. + */ + #viewAddressBookSetup() { + console.log("Address Book setup"); + } + + /** + * TODO: Show the chat setup view. + */ + #viewChatSetup() { + console.log("Chat setup"); + } + + /** + * TODO: Show the feed setup view. + */ + #viewFeedSetup() { + console.log("Feed setup"); + } + + /** + * TODO: Show the newsgroup setup view. + */ + #viewNNTPSetup() { + console.log("Newsgroup setup"); + } + + /** + * TODO: Show the import setup view. + */ + #viewImportSetup() { + console.log("Import setup"); + } +} + +/** + * Open the account hub dialog and show the requested view. + * + * @param {?string} type - The type of view that should be loaded when the modal + * is showed. See AccountHubController::#accounts for a list references. + */ +async function openAccountHub(type) { + if (!AccountHubController) { + AccountHubController = new AccountHubControllerClass(); + } + await AccountHubController.ready; + AccountHubController.open(type); +} diff --git a/comm/mail/components/accountcreation/content/accountSetup.js b/comm/mail/components/accountcreation/content/accountSetup.js new file mode 100644 index 0000000000..3a214f2292 --- /dev/null +++ b/comm/mail/components/accountcreation/content/accountSetup.js @@ -0,0 +1,3023 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global MozElements */ + +/* import-globals-from ../../../../mailnews/base/prefs/content/accountUtils.js */ +var { AccountCreationUtils } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); +var { fetchConfigFromExchange, getAddonsList } = ChromeUtils.import( + "resource:///modules/accountcreation/ExchangeAutoDiscover.jsm" +); +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + AddonManager: "resource://gre/modules/AddonManager.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + AccountConfig: "resource:///modules/accountcreation/AccountConfig.jsm", + cal: "resource:///modules/calendar/calUtils.jsm", + CardDAVUtils: "resource:///modules/CardDAVUtils.jsm", + ConfigVerifier: "resource:///modules/accountcreation/ConfigVerifier.jsm", + CreateInBackend: "resource:///modules/accountcreation/CreateInBackend.jsm", + FetchConfig: "resource:///modules/accountcreation/FetchConfig.jsm", + GuessConfig: "resource:///modules/accountcreation/GuessConfig.jsm", + OAuth2Providers: "resource:///modules/OAuth2Providers.jsm", + Sanitizer: "resource:///modules/accountcreation/Sanitizer.jsm", + UIDensity: "resource:///modules/UIDensity.jsm", + UIFontSize: "resource:///modules/UIFontSize.jsm", +}); + +var { + Abortable, + AddonInstaller, + alertPrompt, + assert, + CancelledException, + Exception, + gAccountSetupLogger, + NotReached, + PriorityOrderAbortable, + UserCancelledException, +} = AccountCreationUtils; + +/** + * This is the dialog opened by menu File | New account | Mail... . + * + * It gets the user's realname, email address and password, + * and tries to automatically configure the account from that, + * using various mechanisms. If all fails, the user can enter/edit + * the config, then we create the account. + * + * Steps: + * - User enters realname, email address and password + * - check for config files on disk + * (shipping with Thunderbird, for enterprise deployments) + * - (if fails) try to get the config file from the ISP via a + * fixed URL on the domain of the email address + * - (if fails) try to get the config file from our own database + * at MoMo servers, maintained by the community + * - (if fails) try to guess the config, by guessing hostnames, + * probing ports, checking config via server's CAPS line etc.. + * - verify the setup, by trying to login to the configured servers + * - let user verify and maybe edit the server names and ports + * - If user clicks OK, create the account + */ + +// Keep track of the prefers-reduce-motion media query for JS based animations. +var gReducedMotion; + +// The main 3 Pane Window that we need to define on load in order to properly +// update the UI when a new account is created. +var gMainWindow; + +// Define standard incoming port numbers. +var gStandardPorts = { + imap: [143, 993], + pop3: [110, 995], + smtp: [587, 25, 465], // order matters + exchange: [443], +}; + +// Store all ports into a flat array for greppability. +var gAllStandardPorts = gStandardPorts.smtp + .concat(gStandardPorts.imap) + .concat(gStandardPorts.pop3) + .concat(gStandardPorts.exchange); + +// Define window event listeners. +window.addEventListener("load", () => { + gAccountSetup.onLoad(); +}); +window.addEventListener("unload", () => { + gAccountSetup.onUnload(); +}); + +function onSetupComplete() { + // Post a message to the main window at the end of a successful account setup. + gMainWindow.postMessage("account-created", "*"); +} + +/** + * Prompt a native HTML confirmation dialog for the Exchange auto discover. + * + * @param {string} domain - Text with the question. + * @param {Function} okCallback - Called when the user clicks OK. + * @param {function(ex)} cancelCallback - Called when the user clicks Cancel + * or if you call `Abortable.cancel()`. + * @returns {Abortable} - If `Abortable.cancel()` is called, + * the dialog is closed and the `cancelCallback()` is called. + */ +function confirmExchange(domain, okCallback, cancelCallback) { + let dialog = document.getElementById("exchangeDialog"); + + document.l10n.setAttributes( + document.getElementById("exchangeDialogQuestion"), + "exchange-dialog-question", + { + domain, + } + ); + + document.getElementById("exchangeDialogConfirmButton").onclick = () => { + dialog.close(); + okCallback(); + }; + + document.getElementById("exchangeDialogCancelButton").onclick = () => { + dialog.close(); + cancelCallback(new UserCancelledException()); + }; + + // Show the dialog. + dialog.showModal(); + + let abortable = new Abortable(); + abortable.cancel = ex => { + dialog.close(); + cancelCallback(ex); + }; + return abortable; +} + +/** + * This is our controller for the entire account setup workflow. + */ +var gAccountSetup = { + // Boolean attribute to keep track of the initialization status of the wizard. + isInited: false, + // Attribute to store methods to interrupt abortable operations like testing + // a server configuration or installing an add-on. + _abortable: null, + + tabMonitor: { + monitorName: "accountSetupMonitor", + + onTabTitleChanged() {}, + onTabOpened() {}, + onTabPersist() {}, + onTabRestored() {}, + onTabClosing(tab) { + if (tab?.urlbar?.value == "about:accountsetup") { + gMainWindow?.postMessage("account-setup-dismissed", "*"); + } + }, + onTabSwitched() {}, + }, + + /** + * Initialize the main notification box for the account setup process. + */ + get notificationBox() { + if (!this._notificationBox) { + this._notificationBox = new MozElements.NotificationBox(element => { + element.setAttribute("notificationside", "bottom"); + document.getElementById("accountSetupNotifications").append(element); + }); + } + return this._notificationBox; + }, + + /** + * Initialize the notification box for the calendar and address book sync + * process at the end of the account setup. + */ + get syncingBox() { + if (!this._syncingBox) { + this._syncingBox = new MozElements.NotificationBox(element => { + element.setAttribute("notificationside", "bottom"); + document.getElementById("syncNotifications").append(element); + }); + } + return this._syncingBox; + }, + + clearNotifications() { + this.notificationBox.removeAllNotifications(); + }, + + onLoad() { + // Bail out if it was already initialized. + if (this.isInited) { + return; + } + + gAccountSetupLogger.debug("Initializing setup wizard"); + gReducedMotion = window.matchMedia( + "(prefers-reduced-motion: reduce)" + ).matches; + + // Store the main window. + gMainWindow = Services.wm.getMostRecentWindow("mail:3pane"); + + // this._currentConfig is the config we got either from the XML file or from + // guessing or from the user. Unless it's from the user, it contains + // placeholders like %EMAILLOCALPART% in username and other fields. + // + // The config here must retain these placeholders, to be able to adapt when + // the user enters a different realname, or password or email local part. + // A change of the domain name will trigger a new detection anyways. That + // means, before you actually use the config (e.g. to create an account or + // to show it to the user), you need to run replaceVariables(). + this._currentConfig = null; + this._domain = ""; + this._hostname = ""; + this._email = ""; + this._realname = ""; + if ("@mozilla.org/userinfo;1" in Cc) { + let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo); + // Assume that it's a genuine full name if it includes a space. + if (userInfo.fullname.includes(" ")) { + this._realname = userInfo.fullname; + document.getElementById("realname").value = this._realname; + } + } + + this._password = ""; + // Keep track of the state of the password field, if password or clear text. + this._showPassword = false; + // This is used only for Exchange AutoDiscover and only if needed. + this._exchangeUsername = ""; + // Store the successful callback in this attribute so we can send it around + // the various validation methods. + this._okCallback = onSetupComplete; + this._msgWindow = gMainWindow.msgWindow; + + // If the account provisioner is preffed off, don't display the account + // provisioner button. + if (!Services.prefs.getBoolPref("mail.provider.enabled")) { + document.getElementById("provisionerButton").hidden = true; + } + + // Disable the remember password checkbox if the pref is false. + if (!Services.prefs.getBoolPref("signon.rememberSignons")) { + let passwordCheckbox = document.getElementById("rememberPassword"); + passwordCheckbox.checked = false; + passwordCheckbox.disabled = true; + } + + // Ensure the cursor is on the first input field. + document.getElementById("realname").focus(); + + // In a new profile, the first request to live.thunderbird.net is much + // slower because of one-time overheads like DNS and OCSP. Let's create some + // dummy requests to prime the connections. + let autoconfigURL = Services.prefs.getCharPref("mailnews.auto_config_url"); + fetch(autoconfigURL, { method: "OPTIONS" }).catch(console.error); + + let addonsURL = Services.prefs.getCharPref( + "mailnews.auto_config.addons_url" + ); + if (new URL(autoconfigURL).origin != new URL(addonsURL).origin) { + fetch(addonsURL, { method: "OPTIONS" }).catch(console.error); + } + + // The tab monitor will inform us when this tab is getting closed. + gMainWindow.document + .getElementById("tabmail") + .registerTabMonitor(this.tabMonitor); + + // We did everything, now we can update the variable. + this.isInited = true; + gAccountSetupLogger.debug("Account setup tab loaded."); + + UIDensity.registerWindow(window); + UIFontSize.registerWindow(window); + }, + + /** + * Changes the window configuration to the different modes we have. + * Shows/hides various window parts and buttons. + * + * @param {string} modename + * "start" : Just the realname, email address, password fields + * "find-config" : detection step, adds the loading notification + * "result" : We found a config and display it to the user. + * The user may create the account. + * "manual-edit" : The user wants (or needs) to manually enter their + * the server hostname and other settings. We'll use them as provided. + * Additionally, there are the following sub-modes which can be entered after + * you entered the main mode: + * "manual-edit-have-hostname" : user entered a hostname for both servers + * that we can use + * "manual-edit-testing" : User pressed the [Re-test] button and + * we're currently detecting the "Auto" values + * "manual-edit-complete" : user entered (or we tested) all necessary + * values, and we're ready to create to account + * Currently, this doesn't cover the warning dialogs etc.. It may later. + */ + switchToMode(modename) { + // Bail out if we requested the same mode we're currently viewing. + if (modename == this._currentModename) { + return; + } + + this._currentModename = modename; + gAccountSetupLogger.debug(`switching to UI mode ${modename}`); + + let continueButton = document.getElementById("continueButton"); + let createButton = document.getElementById("createButton"); + let reTestButton = document.getElementById("reTestButton"); + let autoconfigDesc = document.getElementById("manualConfigDescription"); + let setupTitle = document.getElementById("accountSetupTitle"); + + switch (modename) { + case "start": + this.clearNotifications(); + document.getElementById("setupView").hidden = false; + document.getElementById("successView").hidden = true; + + document.l10n.setAttributes(setupTitle, "account-setup-title"); + setupTitle.classList.remove("success"); + document.l10n.setAttributes( + document.getElementById("accountSetupDescription"), + "account-setup-description" + ); + + document.getElementById("resultsArea").hidden = true; + document.getElementById("manualConfigArea").hidden = true; + document.getElementById("manualConfigButton").hidden = true; + document.getElementById("stopButton").hidden = true; + + reTestButton.hidden = true; + autoconfigDesc.hidden = true; + createButton.hidden = true; + continueButton.disabled = true; + continueButton.hidden = false; + break; + case "find-config": + document.getElementById("resultsArea").hidden = true; + document.getElementById("manualConfigArea").hidden = true; + document.getElementById("manualConfigButton").hidden = true; + document.getElementById("stopButton").hidden = false; + + reTestButton.hidden = true; + autoconfigDesc.hidden = true; + createButton.hidden = true; + continueButton.disabled = true; + continueButton.hidden = false; + this.onStop = this.onStopFindConfig; + break; + case "result": + document.getElementById("manualConfigArea").hidden = true; + document.getElementById("stopButton").hidden = true; + document.getElementById("resultsArea").hidden = false; + document.getElementById("manualConfigButton").hidden = false; + + reTestButton.hidden = true; + autoconfigDesc.hidden = true; + continueButton.hidden = true; + createButton.hidden = false; + createButton.disabled = false; + break; + case "manual-edit": + document.getElementById("resultsArea").hidden = true; + document.getElementById("stopButton").hidden = true; + document.getElementById("manualConfigButton").hidden = true; + document.getElementById("manualConfigArea").hidden = false; + + continueButton.hidden = true; + reTestButton.hidden = false; + autoconfigDesc.hidden = false; + reTestButton.disabled = true; + createButton.hidden = false; + createButton.disabled = true; + break; + case "manual-edit-have-hostname": + document.getElementById("resultsArea").hidden = true; + document.getElementById("stopButton").hidden = true; + document.getElementById("manualConfigButton").hidden = true; + document.getElementById("manualConfigArea").hidden = false; + + reTestButton.hidden = false; + autoconfigDesc.hidden = false; + reTestButton.disabled = false; + continueButton.hidden = true; + createButton.hidden = false; + createButton.disabled = true; + break; + case "manual-edit-testing": + document.getElementById("resultsArea").hidden = true; + document.getElementById("manualConfigArea").hidden = false; + document.getElementById("manualConfigButton").hidden = true; + document.getElementById("stopButton").hidden = false; + + reTestButton.hidden = false; + autoconfigDesc.hidden = false; + reTestButton.disabled = true; + continueButton.hidden = true; + createButton.hidden = false; + createButton.disabled = true; + + this.onStop = this.onStopHalfManualTesting; + break; + case "manual-edit-complete": + document.getElementById("resultsArea").hidden = true; + document.getElementById("manualConfigArea").hidden = false; + document.getElementById("manualConfigButton").hidden = true; + document.getElementById("stopButton").hidden = true; + + reTestButton.hidden = false; + autoconfigDesc.hidden = false; + reTestButton.disabled = false; + continueButton.hidden = true; + createButton.disabled = false; + createButton.hidden = false; + + document.getElementById("incomingProtocol").focus(); + break; + case "success": + document.getElementById("setupView").hidden = true; + document.getElementById("successView").hidden = false; + + document.l10n.setAttributes(setupTitle, "account-setup-success-title"); + setupTitle.classList.add("success"); + document.l10n.setAttributes( + document.getElementById("accountSetupDescription"), + "account-setup-success-description" + ); + document.l10n.setAttributes( + document.getElementById("accountSetupDescriptionSecondary"), + "account-setup-success-secondary-description" + ); + break; + default: + throw new NotReached("Unknown mode requested"); + } + + // If we're offline, we're going to disable the create button, but enable + // the advanced config button if we have a current config. + if (Services.io.offline && !this._currentConfig) { + document.getElementById("manualConfigButton").hidden = true; + reTestButton.hidden = true; + autoconfigDesc.hidden = true; + createButton.hidden = true; + } + }, + + /** + * Reset the form and the entire UI of the account setup. + */ + resetSetup() { + document.getElementById("form").reset(); + document.getElementById("realname").focus(); + // Call onStartOver only after resetting the form in order to properly + // update the form buttons. + this.onStartOver(); + }, + + /** + * Start from beginning with possibly new email address. + */ + onStartOver() { + this._currentConfig = null; + if (this._abortable) { + this.onStop(); + } + this.switchToMode("start"); + this.checkValidForm(); + }, + + getConcreteConfig() { + let result = this._currentConfig.copy(); + + AccountConfig.replaceVariables( + result, + this._realname, + this._email, + this._password + ); + result.rememberPassword = + document.getElementById("rememberPassword").checked && !!this._password; + + if (result.incoming.addonAccountType) { + result.incoming.type = result.incoming.addonAccountType; + } + + return result; + }, + + /** + * onInputEmail and onInputRealname are called on input = keypresses, and + * enable/disable the next button based on whether there's a semi-proper + * e-mail address and non-blank realname to start with. + * + * A change to the email address also automatically restarts the + * whole process. + */ + onInputEmail() { + this._email = document.getElementById("email").value; + this.onStartOver(); + }, + + onInputRealname() { + this._realname = document.getElementById("realname").value; + this.checkValidForm(); + }, + + onInputUsername() { + this._exchangeUsername = document.getElementById("usernameEx").value; + this.onStartOver(); + }, + + onInputPassword() { + this._password = document.getElementById("password").value; + this.onStartOver(); + + // Show the password toggle button only if the field is not empty. + let toggleButton = document.getElementById("passwordToggleButton"); + toggleButton.hidden = !this._password; + + if (!this._password) { + // Always reset the field to the proper type. + this.hidePassword(); + } + }, + + /** + * Toggle the type of the password field between password and text to allow + * users reading their own password. + */ + passwordToggle(event) { + // Prevent the form submission if the user presses Enter. + event.preventDefault(); + + // The password field is in plain text, change it back to the proper type. + if (this._showPassword) { + this.hidePassword(); + return; + } + + // Change the password field to plain text to make the text visible. + this.showPassword(); + }, + + /** + * Convert the password field into a plain text field allowing users and + * assistive technologies to read the typed text. + */ + showPassword() { + document.getElementById("password").type = "text"; + document.l10n.setAttributes( + document.getElementById("passwordToggleButton"), + "account-setup-password-toggle-hide" + ); + + let toggleImage = document.getElementById("passwordInfo"); + toggleImage.src = "chrome://messenger/skin/icons/new/compact/eye.svg"; + toggleImage.classList.add("password-toggled"); + + this._showPassword = true; + }, + + /** + * Convert the password field back to its default password type. + */ + hidePassword() { + // No need to reset anything if the password was never shown. + if (!this._showPassword) { + return; + } + + document.getElementById("password").type = "password"; + document.l10n.setAttributes( + document.getElementById("passwordToggleButton"), + "account-setup-password-toggle-show" + ); + + let toggleImage = document.getElementById("passwordInfo"); + toggleImage.src = "chrome://messenger/skin/icons/new/compact/hidden.svg"; + toggleImage.classList.remove("password-toggled"); + + this._showPassword = false; + }, + + /** + * Check whether the user entered the minimum amount of information needed to + * leave the "start" mode (name and email) and is allowed to proceed to the + * detection step. + */ + checkValidForm() { + let email = document.getElementById("email"); + let isValidForm = + email.checkValidity() && + document.getElementById("realname").checkValidity(); + this._domain = isValidForm ? this._email.split("@")[1].toLowerCase() : ""; + + document.getElementById("continueButton").disabled = !isValidForm; + document.getElementById("manualConfigButton").hidden = !isValidForm; + document.getElementById("provisionerButton").hidden = email.value; + }, + + /** + * When the [Continue] button is clicked, we move from the initial account + * information stage to using that information to configure account details. + */ + onContinue() { + this.findConfig(this._domain, this._email); + }, + + // -------------- + // Detection step + + /** + * Try to find an account configuration for this email address. + * This is the function which runs the autoconfig. + */ + findConfig(domain, emailAddress) { + gAccountSetupLogger.debug("findConfig()"); + if (this._abortable) { + this.onStop(); + } + this.switchToMode("find-config"); + this.startLoadingState("account-setup-looking-up-settings"); + + let self = this; + let call = null; + let fetch = null; + + let priority = (this._abortable = new PriorityOrderAbortable( + function (config, call) { + // success + self._abortable = null; + self.stopLoadingState(call.foundMsg); + self.foundConfig(config); + }, + function (e, allErrors) { + // all failed + if (e instanceof CancelledException) { + self.onStartOver(); + return; + } + + // guess config + let initialConfig = new AccountConfig(); + self._prefillConfig(initialConfig); + self._guessConfig(domain, initialConfig); + } + )); + + try { + call = priority.addCall(); + gAccountSetupLogger.debug( + "Looking up configuration: Thunderbird installation…" + ); + call.foundMsg = "account-setup-success-settings-disk"; + fetch = FetchConfig.fromDisk( + domain, + call.successCallback(), + call.errorCallback() + ); + call.setAbortable(fetch); + + call = priority.addCall(); + gAccountSetupLogger.debug("Looking up configuration: Email provider…"); + call.foundMsg = "account-setup-success-settings-isp"; + fetch = FetchConfig.fromISP( + domain, + emailAddress, + call.successCallback(), + call.errorCallback() + ); + call.setAbortable(fetch); + + call = priority.addCall(); + gAccountSetupLogger.debug( + "Looking up configuration: Thunderbird installation…" + ); + call.foundMsg = "account-setup-success-settings-db"; + fetch = FetchConfig.fromDB( + domain, + call.successCallback(), + call.errorCallback() + ); + call.setAbortable(fetch); + + call = priority.addCall(); + gAccountSetupLogger.debug( + "Looking up configuration: Incoming mail domain…" + ); + // "account-setup-success-settings-db" is correct. + // We display the same message for both db and mx cases. + call.foundMsg = "account-setup-success-settings-db"; + fetch = FetchConfig.forMX( + domain, + call.successCallback(), + call.errorCallback() + ); + call.setAbortable(fetch); + + call = priority.addCall(); + gAccountSetupLogger.debug("Looking up configuration: Exchange server…"); + call.foundMsg = "account-setup-success-settings-exchange"; + fetch = fetchConfigFromExchange( + domain, + emailAddress, + this._exchangeUsername, + this._password, + confirmExchange, + call.successCallback(), + (e, allErrors) => { + // Must call error callback in any case to stop the discover mode. + let errorCallback = call.errorCallback(); + if (e instanceof CancelledException) { + errorCallback(e); + } else if (allErrors && allErrors.some(e => e.code == 401)) { + // Auth failed. + // Ask user for username. + this.onStartOver(); + this.stopLoadingState(); // clears status message + document.getElementById("usernameRow").hidden = false; + + this.showErrorNotification( + !this._exchangeUsername + ? "account-setup-credentials-incomplete" + : "account-setup-credentials-wrong" + ); + document.getElementById("manualConfigButton").hidden = false; + errorCallback(new CancelledException()); + } else { + errorCallback(e); + } + } + ); + call.setAbortable(fetch); + } catch (e) { + this.onStop(); + // e.g. when entering an invalid domain like "c@c.-com" + this.showErrorNotification(e, true); + } + }, + + /** + * Just a continuation of findConfig() + */ + _guessConfig(domain, initialConfig) { + this.startLoadingState("account-setup-looking-up-settings-guess"); + let self = this; + self._abortable = GuessConfig.guessConfig( + domain, + function (type, hostname, port, socketType, done, config) { + // progress + gAccountSetupLogger.debug( + `${hostname}:${port} socketType=${socketType} ${type}: progress callback` + ); + }, + function (config) { + // success + self._abortable = null; + self.foundConfig(config); + self.stopLoadingState( + Services.io.offline + ? "account-setup-success-guess-offline" + : "account-setup-success-guess" + ); + }, + function (e, config) { + // guessconfig failed + if (e instanceof CancelledException) { + return; + } + self._abortable = null; + gAccountSetupLogger.warn(`guessConfig failed: ${e}`); + self.showErrorNotification("account-setup-find-settings-failed"); + self.editConfigDetails(); + }, + initialConfig, + "both" + ); + }, + + /** + * Called after findConfig() is successful and displays the data to the user. + * + * @param {AccountConfig} config - The config to present to the user. + */ + foundConfig(config) { + gAccountSetupLogger.debug("found config:\n" + config); + assert( + config instanceof AccountConfig, + "BUG: Arg 'config' needs to be an AccountConfig object" + ); + + this._haveValidConfigForDomain = this._email.split("@")[1]; + + // Bail out if the name and email fields are empty. + if (!this._realname || !this._email) { + return; + } + + config.addons = []; + let successCallback = () => { + this._abortable = null; + this.displayConfigResult(config); + this.switchToMode("result"); + this.ensureVisibleButtons(); + }; + this._abortable = getAddonsList(config, successCallback, e => { + successCallback(); + this.showErrorNotification(e, true); + }); + }, + + /** + * [Stop] button click handler. + * This allows the user to abort any longer operation, esp. network activity. + * We currently have 3 such cases here: + * 1. findConfig(), i.e. fetch config from DB, guessConfig etc. + * 2. testManualConfig(), i.e. the [Retest] button in manual config. + * 3. verifyConfig() - We can't stop this yet, so irrelevant here currently. + * Given that these need slightly different actions, this function will be set + * to a function (i.e. overwritten) by whoever enables the stop button. + * + * We also call this from the code when the user started a different action + * without explicitly clicking [Stop] for the old one first. + */ + onStop() { + throw new NotReached("onStop should be overridden by now"); + }, + + _onStopCommon() { + if (!this._abortable) { + throw new NotReached("onStop called although there's nothing to stop"); + } + gAccountSetupLogger.debug("onStop cancelled _abortable"); + this._abortable.cancel(new UserCancelledException()); + this._abortable = null; + this.stopLoadingState(); + }, + + onStopFindConfig() { + this._onStopCommon(); + this.switchToMode("start"); + this.checkValidForm(); + }, + + onStopHalfManualTesting() { + this._onStopCommon(); + this.validateManualEditComplete(); + }, + + // ----------- Loading area ----------- + /** + * Disable all the input fields of the main form to prevent editing and show + * a notification while a loading or fetching state. + * + * @param {string} stringName - The name of the fluent string that needs to be + * attached to the notification. + */ + async startLoadingState(stringName) { + gAccountSetupLogger.debug(`Loading start: ${stringName}`); + + this.showHelperImage("step2"); + + // Disable all input fields. + for (let input of document.querySelectorAll("#form input")) { + input.disabled = true; + } + + let notificationMessage = await document.l10n.formatValue(stringName); + + gAccountSetupLogger.debug(`Status msg: ${notificationMessage}`); + + let notification = this.notificationBox.getNotificationWithValue( + "accountSetupLoading" + ); + + // If a notification already exists, simply update the message. + if (notification) { + notification.label = notificationMessage; + this.ensureVisibleNotification(); + return; + } + + notification = this.notificationBox.appendNotification( + "accountSetupLoading", + { + label: notificationMessage, + priority: this.notificationBox.PRIORITY_INFO_LOW, + }, + null + ); + notification.setAttribute("align", "center"); + + // Hide the close button to prevent dismissing the notification. + notification.removeAttribute("dismissable"); + + this.ensureVisibleNotification(); + }, + + /** + * Update the text of a currently visible loading notification + * + * @param {string} stringName - The name of the fluent string that needs to be + * attached to the notification. + */ + async updateLoadingState(stringName) { + let notification = this.notificationBox.getNotificationWithValue( + "accountSetupLoading" + ); + // If a notification doesn't already exist, create one. + if (!notification) { + this.startLoadingState(stringName); + return; + } + + let notificationMessage = await document.l10n.formatValue(stringName); + notification.label = notificationMessage; + this.ensureVisibleNotification(); + + gAccountSetupLogger.debug(`Status msg: ${notificationMessage}`); + }, + + /** + * Clear the loading notification and show a successful notification if + * needed. + * + * @param {?string} stringName - The name of the fluent string that needs to + * be attached to the notification, or null if nothing needs to be showed. + */ + async stopLoadingState(stringName) { + // Re-enable all form input fields. + for (let input of document.querySelectorAll("#form input")) { + input.removeAttribute("disabled"); + } + + // Always remove any leftover notification. + this.clearNotifications(); + + // Bail out if we don't need to show anything else. + if (!stringName) { + gAccountSetupLogger.debug("Loading stopped"); + this.showHelperImage("step1"); + return; + } + + gAccountSetupLogger.debug(`Loading stopped: ${stringName}`); + + let notificationMessage = await document.l10n.formatValue(stringName); + + let notification = this.notificationBox.appendNotification( + "accountSetupSuccess", + { + label: notificationMessage, + priority: this.notificationBox.PRIORITY_INFO_HIGH, + }, + null + ); + notification.setAttribute("type", "success"); + + // Hide the close button to prevent dismissing the notification. + notification.removeAttribute("dismissable"); + + this.showHelperImage("step3"); + }, + + /** + * Show an error notification in case something went wrong. + * + * @param {string} stringName - The name of the fluent string that needs to + * be attached to the notification. + * @param {boolean} isMsgError - True if the message comes from a server error + * response or try/catch. + */ + async showErrorNotification(stringName, isMsgError) { + gAccountSetupLogger.debug(`Status error: ${stringName}`); + + this.showHelperImage("step4"); + + // Re-enable all form input fields. + for (let input of document.querySelectorAll("#form input")) { + input.removeAttribute("disabled"); + } + + // Always remove any leftover notification before creating a new one. + this.clearNotifications(); + + // Fetch the fluent string only if this is not an error message coming from + // a previous method. + let notificationMessage = isMsgError + ? stringName + : await document.l10n.formatValue(stringName); + + let notification = this.notificationBox.appendNotification( + "accountSetupError", + { + label: notificationMessage, + priority: this.notificationBox.PRIORITY_WARNING_MEDIUM, + }, + null + ); + + // Hide the close button to prevent dismissing the notification. + notification.removeAttribute("dismissable"); + + this.ensureVisibleNotification(); + }, + + /** + * Hide all the helper images and show the requested one. + * + * @param {string} id - The string ID of the element to show. + */ + showHelperImage(id) { + // Hide all currently visible articles containing helper images in the + // second column. + for (let article of document.querySelectorAll( + ".second-column article:not([hidden])" + )) { + article.hidden = true; + } + + // Simply show the requested helper image if the user specified a reduced + // motion preference. + if (gReducedMotion) { + document.getElementById(id).hidden = false; + return; + } + + // Handle a nice cross fade between steps. + let stepToShow = document.getElementById(id); + // Add the class to let the revealing element start from a proper state. + stepToShow.classList.add("hide"); + stepToShow.hidden = false; + // Timeout to animate after the hidden attribute has been removed. + setTimeout(() => { + stepToShow.classList.remove("hide"); + }); + }, + + /** + * Always ensure the primary button is visible by scrolling the page until the + * button is above the fold. + */ + ensureVisibleButtons() { + // We use the #footDescription element to ensure the buttons are properly + // scrolled above the fold. + document.getElementById("footDescription").scrollIntoView({ + behavior: gReducedMotion ? "auto" : "smooth", + block: "end", + inline: "nearest", + }); + }, + + /** + * Always ensure the notification area is visible when a new notification is + * created. + */ + ensureVisibleNotification() { + document.getElementById("accountSetupNotifications").scrollIntoView({ + behavior: gReducedMotion ? "auto" : "smooth", + block: "start", + inline: "nearest", + }); + }, + + /** + * Populate the results config details area. + * + * @param {AccountConfig} config - The config to present to user. + */ + displayConfigResult(config) { + assert(config instanceof AccountConfig); + this._currentConfig = config; + let configFilledIn = this.getConcreteConfig(); + + // Filter out Protcols we don't currently support + let protocols = config.incomingAlternatives.filter(protocol => + ["imap", "pop3", "exchange"].includes(protocol.type) + ); + protocols.unshift(config.incoming); + protocols = protocols.reduce((found, nextEl) => { + if (!found.some(prevEl => prevEl.type == nextEl.type)) { + found.push(nextEl); + } + return found; + }, []); + + // Hide all the available options in order to start with a clean slate. + for (let row of document.querySelectorAll(".content-blocking-category")) { + row.classList.remove("selected"); + row.hidden = true; + } + + // Remove all previously generated protocol types. + for (let type of document.querySelectorAll(".config-type")) { + type.remove(); + } + + // Reveal all the matching protocols. + for (let protocol of protocols) { + let row = document.getElementById(`resultsOption-${protocol.type}`); + row.hidden = false; + // Attach the protocol to the radio input for later usage. + row.querySelector(`input[type="radio"]`).configIncoming = protocol; + } + + // Preselect the default protocol type. + let selected = document.getElementById( + `resultSelect-${config.incoming.type}` + ); + selected.closest(".content-blocking-category").classList.add("selected"); + selected.checked = true; + + // Update the results area title to match the protocols choice. + document.l10n.setAttributes( + document.getElementById("resultAreaTitle"), + "account-setup-results-area-title", + { + count: protocols.length, + } + ); + + // Ensure by default the "Done" button is enabled. + document.getElementById("createButton").disabled = false; + + // Thunderbird can't handle Exchange server independently, therefore we + // need to prompt the user with the installation of the Owl add-on. + if (config.incoming.type == "exchange") { + let addonsInstallRows = document.getElementById("resultAddonInstallRows"); + + // Remove any pre-existing child element. + while (addonsInstallRows.hasChildNodes()) { + addonsInstallRows.lastChild.remove(); + } + + let container = document.getElementById("resultExchangeHostname"); + _makeHostDisplayString(config.incoming, container); + document + .getElementById("incomingTitle-exchange") + .appendChild(_socketTypeSpan(config.incoming.socketType)); + + (async () => { + try { + for (let addon of config.addons) { + let installer = new AddonInstaller(addon); + addon.isInstalled = await installer.isInstalled(); + addon.isDisabled = await installer.isDisabled(); + } + + let addonInfoArea = document.getElementById("installAddonInfo"); + let installedAddon = config.addons.find(addon => addon.isInstalled); + + // The needed add-on is already installed, no need to show anything. + if (installedAddon) { + config.incoming.addonAccountType = + installedAddon.useType.addonAccountType; + addonInfoArea.hidden = true; + return; + } + // Disable "Done" until add-on is installed, or some other protocol + // is selected. + document.getElementById("createButton").disabled = true; + + addonInfoArea.hidden = false; + + document.l10n.setAttributes( + document.getElementById("resultAddonIntro"), + !config.incomingAlternatives.find(alt => + ["imap", "pop3"].includes(alt.type) + ) + ? "account-setup-addon-install-intro" + : "account-setup-addon-no-protocol" + ); + + for (let addon of config.addons) { + // Creates and addon install section. + // <div><img/><a></a><button></button></div> + let container = document.createElement("div"); + container.classList.add("addon-container"); + + let img = document.createElement("img"); + img.alt = ""; + img.classList.add("icon"); + if (addon.icon32) { + img.setAttribute("src", addon.icon32); + } + + let link = document.createElement("a"); + link.classList.add("link"); + link.setAttribute("href", addon.websiteURL); + link.textContent = addon.description; + + let button = document.createElement("button"); + document.l10n.setAttributes( + button, + "account-setup-addon-install-title" + ); + button.addEventListener("click", () => { + gAccountSetup.addonInstall(addon); + }); + if (addon.isDisabled) { + // If the add on is disabled by user, or due to incompatibility + // - disable install (it won't help, it's already installed) + // - link to the addons manager instead (so they can fix it) + button.disabled = true; + link.setAttribute("href", "about:addons"); + link.setAttribute("target", "_blank"); + + // Trigger an add-on update check. If an update is available, + // enable the install button to (re)install. + AddonManager.getAddonByID(addon.id).then(a => { + if (!a) { + return; + } + let listener = { + onUpdateAvailable(addon, install) { + button.disabled = false; + }, + onNoUpdateAvailable() {}, + }; + a.findUpdates( + listener, + AddonManager.UPDATE_WHEN_USER_REQUESTED + ); + }); + } + + container.appendChild(img); + container.appendChild(link); + container.appendChild(button); + + addonsInstallRows.appendChild(container); + } + } catch (e) { + this.showErrorNotification(e, true); + } + })(); + return; + } + + function _makeHostDisplayString(server, container) { + // Clean up any existing element. + while (container.hasChildNodes()) { + container.lastChild.remove(); + } + + let cert = container.parentNode.querySelector(".cert-status"); + if (cert != null) { + cert.remove(); + } + + let domain = server.hostname; + try { + domain = Services.eTLD.getBaseDomainFromHost(server.hostname); + } catch (ex) { + gAccountSetupLogger.warn(ex); + } + + let hostSpan = document.createElement("span"); + hostSpan.classList.add("host-without-domain"); + hostSpan.textContent = server.hostname.substr( + 0, + server.hostname.length - domain.length + ); + container.appendChild(hostSpan); + + let domainSpan = document.createElement("span"); + domainSpan.classList.add("domain"); + domainSpan.textContent = domain; + container.appendChild(domainSpan); + + if (!gAllStandardPorts.includes(server.port)) { + let portSpan = document.createElement("span"); + portSpan.classList.add("port"); + portSpan.textContent = `:${server.port}`; + container.appendChild(portSpan); + } + + if (server.badCert) { + container.parentNode + .querySelector(".cert-status") + .classList.add("insecure"); + } + } + + /** + * Helper method to create the span protocol element. + * + * @returns {HTMLElement} - The newly created span label. + */ + function _protocolTypeSpan() { + let span = document.createElement("span"); + span.classList.add("protocol-type", "config-type"); + return span; + } + + /** + * Helper method to create the span socket element. + * + * @param {nsMsgSocketType} socket - The value representing the server + * socket type. + * @returns {HTMLElement} - The newly created span label. + */ + function _socketTypeSpan(socket) { + let ssl = Sanitizer.translate(socket, { + 0: "no-encryption", + 2: "starttls", + 3: "ssl", + }); + let span = _protocolTypeSpan(); + document.l10n.setAttributes(span, `account-setup-result-${ssl}`); + span.classList.add("ssl"); + if (socket != 2 && socket != 3) { + // Not an SSL or STARTTLS socket. + span.classList.add("insecure"); + } + return span; + } + + let protocolType = config.incoming.type; + if (configFilledIn.incoming.hostname) { + _makeHostDisplayString( + configFilledIn.incoming, + document.getElementById(`incomingInfo-${protocolType}`) + ); + + let container = document.getElementById(`incomingTitle-${protocolType}`); + + // No need to show the protocol type if it's exchange, and the socket span + // is generated somewhere else specifically for exchange. + if (protocolType != "exchange") { + let span = _protocolTypeSpan(); + span.textContent = configFilledIn.incoming.type; + container.appendChild(span); + container.appendChild(_socketTypeSpan(config.incoming.socketType)); + } + } + + let outgoingInfo = document.getElementById(`outgoingInfo-${protocolType}`); + if (!config.outgoing.existingServerKey) { + if (configFilledIn.outgoing.hostname) { + _makeHostDisplayString(configFilledIn.outgoing, outgoingInfo); + } + let container = document.getElementById(`outgoingTitle-${protocolType}`); + // No need to show the protocol type if it's exchange, and the socket span + // is generated somewhere else specifically for exchange. + if (protocolType != "exchange") { + let span = _protocolTypeSpan(); + span.textContent = configFilledIn.outgoing.type; + container.appendChild(span); + container.appendChild(_socketTypeSpan(config.outgoing.socketType)); + } + } else { + let span = document.createElement("span"); + document.l10n.setAttributes( + span, + "account-setup-result-outgoing-existing" + ); + outgoingInfo.appendChild(span); + } + + let usernameInfo = document.getElementById( + `usernameInfo-${config.incoming.type}` + ); + if (configFilledIn.incoming.username == configFilledIn.outgoing.username) { + usernameInfo.textContent = configFilledIn.incoming.username; + } else { + document.l10n.setAttributes( + usernameInfo, + "account-setup-result-username-different", + { + incoming: configFilledIn.incoming.username, + outgoing: configFilledIn.outgoing.username, + } + ); + } + }, + + /** + * Handle the user switching between IMAP and POP3 settings using the + * radio buttons. + */ + onResultServerTypeChanged() { + let config = this._currentConfig; + // Add current server as best alternative to start of array. + config.incomingAlternatives.unshift(config.incoming); + + // Clear the visually selected radio container. + document + .querySelector(".content-blocking-category.selected") + .classList.remove("selected"); + + // Use selected server (stored as special property on the <input> node). + let selected = document.querySelector( + 'input[name="resultsServerType"]:checked' + ); + selected.closest(".content-blocking-category").classList.add("selected"); + config.incoming = selected.configIncoming; + + // Remove newly selected server from list of alternatives. + config.incomingAlternatives = config.incomingAlternatives.filter( + alt => alt != config.incoming + ); + this.displayConfigResult(config); + }, + + /** + * Install the addon + * Called when user clicks [Install] button. + * + * @param {AddonInfo} addon - @see AccountConfig.addons + */ + async addonInstall(addon) { + let addonInfoArea = document.getElementById("installAddonInfo"); + let createButton = document.getElementById("createButton"); + addonInfoArea.hidden = true; + createButton.disabled = true; + + this.clearNotifications(); + await this.startLoadingState("account-setup-installing-addon"); + + try { + let installer = (this._abortable = new AddonInstaller(addon)); + await installer.install(); + + this._abortable = null; + this.stopLoadingState("account-setup-success-addon"); + createButton.disabled = false; + + this._currentConfig.incoming.addonAccountType = + addon.useType.addonAccountType; + // Remove the note about having to install an add-on. + let rows = document.getElementById("resultAddonInstallRows"); + while (rows.lastChild) { + rows.lastChild.remove(); + } + } catch (e) { + console.error(e); + this.showErrorNotification(e, true); + addonInfoArea.hidden = false; + } + }, + + // ---------------- + // Manual Edit area + + /** + * Gets the values from the user in the manual edit area. Realname and + * password are not part of that area and still placeholders, but hostname and + * username are concrete and no placeholders anymore. + */ + getUserConfig() { + let config = this.getConcreteConfig() || new AccountConfig(); + config.source = AccountConfig.kSourceUser; + + // Incoming server + try { + let inHostnameField = document.getElementById("incomingHostname"); + config.incoming.hostname = Sanitizer.hostname(inHostnameField.value); + inHostnameField.value = config.incoming.hostname; + } catch (e) { + gAccountSetupLogger.warn(e); + } + + try { + config.incoming.port = Sanitizer.integerRange( + document.getElementById("incomingPort").value, + 1, + 65535 + ); + } catch (e) { + config.incoming.port = undefined; // incl. default "Auto" + } + + config.incoming.type = Sanitizer.translate( + document.getElementById("incomingProtocol").value, + { + 1: "imap", + 2: "pop3", + 3: "exchange", + 0: null, + } + ); + config.incoming.socketType = Sanitizer.integer( + document.getElementById("incomingSsl").value + ); + config.incoming.auth = Sanitizer.integer( + document.getElementById("incomingAuthMethod").value + ); + config.incoming.username = + document.getElementById("incomingUsername").value; + + // Outgoing server + + config.outgoing.username = + document.getElementById("outgoingUsername").value; + + // The user specified a custom SMTP server. + config.outgoing.existingServerKey = null; + config.outgoing.addThisServer = true; + config.outgoing.useGlobalPreferredServer = false; + + try { + let input = document.getElementById("outgoingHostname"); + config.outgoing.hostname = Sanitizer.hostname(input.value); + input.value = config.outgoing.hostname; + } catch (e) { + gAccountSetupLogger.warn(e); + } + + try { + config.outgoing.port = Sanitizer.integerRange( + document.getElementById("outgoingPort").value, + 1, + 65535 + ); + } catch (e) { + config.outgoing.port = undefined; // incl. default "Auto" + } + + config.outgoing.socketType = Sanitizer.integer( + document.getElementById("outgoingSsl").value + ); + config.outgoing.auth = Sanitizer.integer( + document.getElementById("outgoingAuthMethod").value + ); + + return config; + }, + + /** + * [Manual Config] button click handler. This turns the config details area + * into an editable form and makes the (Go) button appear. The edit button + * should only be available after the config probing is completely finished, + * replacing what was the (Stop) button. + */ + onManualEdit() { + if (this._abortable) { + this.onStop(); + } + this.editConfigDetails(); + this.showHelperImage("step3"); + }, + + /** + * Setting the config details form so it can be edited. We also disable + * (and hide) the create button during this time because we don't know what + * might have changed. The function called from the button that restarts + * the config check should be enabling the config button as needed. + */ + editConfigDetails() { + gAccountSetupLogger.debug("manual edit"); + + if (!this._currentConfig) { + this._currentConfig = new AccountConfig(); + this._currentConfig.incoming.type = "imap"; + this._currentConfig.incoming.username = "%EMAILADDRESS%"; + this._currentConfig.outgoing.username = "%EMAILADDRESS%"; + this._currentConfig.incoming.hostname = ".%EMAILDOMAIN%"; + this._currentConfig.outgoing.hostname = ".%EMAILDOMAIN%"; + } + // Although we go manual, and we need to display the concrete username, + // however the realname and password is not part of manual config and + // must stay a placeholder in _currentConfig. @see getUserConfig() + + this._fillManualEditFields(this.getConcreteConfig()); + + // _fillManualEditFields() indirectly calls validateManualEditComplete(), + // but it's important to not forget it in case the code is rewritten, + // so calling it explicitly again. Doesn't do harm, speed is irrelevant. + this.validateManualEditComplete(); + }, + + /** + * Fills the manual edit textfields with the provided config. + * + * @param {AccountConfig} config - The config to present to the user. + */ + _fillManualEditFields(config) { + assert(config instanceof AccountConfig); + + let isExchange = config.incoming.type == "exchange"; + + // Incoming server. + document.getElementById("incomingProtocolExchange").hidden = !isExchange; + document.getElementById("incomingProtocol").value = Sanitizer.translate( + config.incoming.type, + { imap: 1, pop3: 2, exchange: 3 }, + 1 + ); + document.getElementById("incomingHostname").value = + config.incoming.hostname; + document.getElementById("incomingSsl").value = Sanitizer.enum( + config.incoming.socketType, + [0, 1, 2, 3], + 0 + ); + document.getElementById("incomingAuthMethod").value = Sanitizer.enum( + config.incoming.auth, + [0, 3, 4, 5, 6, 10], + 0 + ); + document.getElementById("incomingUsername").value = + config.incoming.username; + + // If a port number was specified other than "Auto" + if (config.incoming.port) { + document.getElementById("incomingPort").value = config.incoming.port; + } else { + this.adjustIncomingPortToSSLAndProtocol(config); + } + + // Outgoing server. + + document.getElementById("outgoingHostname").value = + config.outgoing.hostname; + document.getElementById("outgoingUsername").value = + config.outgoing.username; + + // While sameInOutUsernames is true we synchronize values of incoming + // and outgoing username. + this.sameInOutUsernames = true; + document.getElementById("outgoingSsl").value = Sanitizer.enum( + config.outgoing.socketType, + [0, 1, 2, 3], + 0 + ); + document.getElementById("outgoingAuthMethod").value = Sanitizer.enum( + config.outgoing.auth, + [0, 1, 3, 4, 5, 6, 10], + 0 + ); + + // If a port number was specified other than "Auto" + if (config.outgoing.port) { + document.getElementById("outgoingPort").value = config.outgoing.port; + } else { + this.adjustOutgoingPortToSSLAndProtocol(config); + } + + this.adjustOAuth2Visibility(config); + }, + + /** + * Make OAuth2 visible as an authentication method when a hostname that + * OAuth2 can be used with is entered. + * + * @param {AccountConfig} config - The account configuration. + */ + async adjustOAuth2Visibility(config) { + // If the incoming server hostname supports OAuth2, enable it. + let iDetails = OAuth2Providers.getHostnameDetails(config.incoming.hostname); + document.getElementById("in-authMethod-oauth2").hidden = !iDetails; + if (iDetails) { + gAccountSetupLogger.debug( + `OAuth2 details for incoming server ${config.incoming.hostname} is ${iDetails}` + ); + config.incoming.oauthSettings = {}; + [ + config.incoming.oauthSettings.issuer, + config.incoming.oauthSettings.scope, + ] = iDetails; + this._currentConfig.incoming.oauthSettings = + config.incoming.oauthSettings; + } + + // If the smtp hostname supports OAuth2, enable it. + let oDetails = OAuth2Providers.getHostnameDetails(config.outgoing.hostname); + document.getElementById("out-authMethod-oauth2").hidden = !oDetails; + if (oDetails) { + gAccountSetupLogger.debug( + `OAuth2 details for outgoing server ${config.outgoing.hostname} is ${oDetails}` + ); + config.outgoing.oauthSettings = {}; + [ + config.outgoing.oauthSettings.issuer, + config.outgoing.oauthSettings.scope, + ] = oDetails; + this._currentConfig.outgoing.oauthSettings = + config.outgoing.oauthSettings; + } + }, + + /** + * Automatically fill port field in manual edit, unless the user entered a + * non-standard port. + * + * @param {AccountConfig} config - The account configuration. + */ + async adjustIncomingPortToSSLAndProtocol(config) { + let incoming = config.incoming; + + // Bail out if a port number is already defined and it's not part of the + // known ports array. + if (!gAllStandardPorts.includes(incoming.port)) { + return; + } + + let input = document.getElementById("incomingPort"); + + switch (incoming.type) { + case "imap": + input.value = incoming.socketType == Ci.nsMsgSocketType.SSL ? 993 : 143; + break; + + case "pop3": + input.value = incoming.socketType == Ci.nsMsgSocketType.SSL ? 995 : 110; + break; + + case "exchange": + input.value = 443; + break; + } + }, + + /** + * Automatically fill port field in manual edit, unless the user entered a + * non-standard port. + * + * @param {AccountConfig} config - The account configuration. + */ + async adjustOutgoingPortToSSLAndProtocol(config) { + let outgoing = config.outgoing; + + // Bail out if a port number is already defined and it's not part of the + // known ports array. + if (!gAllStandardPorts.includes(outgoing.port)) { + return; + } + + // Implicit TLS for SMTP is on port 465. + if (outgoing.socketType == Ci.nsMsgSocketType.SSL) { + document.getElementById("outgoingPort").value = 465; + return; + } + + // Implicit TLS for SMTP is on port 465. STARTTLS won't work there. + if ( + outgoing.port == 465 && + outgoing.socketType == Ci.nsMsgSocketType.alwaysSTARTTLS + ) { + document.getElementById("outgoingPort").value = 587; + } + }, + + /** + * If the user changed the port manually, adjust the SSL value, + * (only) if the new port is impossible with the old SSL value. + * + * @param config {AccountConfig} + */ + adjustIncomingSSLToPort(config) { + let incoming = config.incoming; + if (!gAllStandardPorts.includes(incoming.port)) { + return; + } + + if (incoming.type == "imap") { + // Implicit TLS for IMAP is on port 993. + if ( + incoming.port == 993 && + incoming.socketType != Ci.nsMsgSocketType.SSL + ) { + document.getElementById("incomingSsl").value = Ci.nsMsgSocketType.SSL; + return; + } + if ( + incoming.port == 143 && + incoming.socketType == Ci.nsMsgSocketType.SSL + ) { + document.getElementById("incomingSsl").value = + Ci.nsMsgSocketType.alwaysSTARTTLS; + return; + } + } + + if (incoming.type == "pop3") { + // Implicit TLS for POP3 is on port 995. + if ( + incoming.port == 995 && + incoming.socketType != Ci.nsMsgSocketType.SSL + ) { + document.getElementById("incomingSsl").value = Ci.nsMsgSocketType.SSL; + return; + } + if ( + incoming.port == 110 && + incoming.socketType == Ci.nsMsgSocketType.SSL + ) { + document.getElementById("incomingSsl").value = + Ci.nsMsgSocketType.alwaysSTARTTLS; + } + } + }, + + /** + * @see adjustIncomingSSLToPort() + */ + adjustOutgoingSSLToPort(config) { + let outgoing = config.outgoing; + if (!gAllStandardPorts.includes(outgoing.port)) { + return; + } + + // Implicit TLS for SMTP is on port 465. + if (outgoing.port == 465 && outgoing.socketType != Ci.nsMsgSocketType.SSL) { + document.getElementById("outgoingSsl").value = Ci.nsMsgSocketType.SSL; + return; + } + + // Port 587 and port 25 are for plain or STARTTLS. Not for Implicit TLS. + if ( + (outgoing.port == 587 || outgoing.port == 25) && + outgoing.socketType == Ci.nsMsgSocketType.SSL + ) { + document.getElementById("outgoingSsl").value = + Ci.nsMsgSocketType.alwaysSTARTTLS; + } + }, + + onChangedProtocolIncoming() { + let config = this.getUserConfig(); + this.adjustIncomingPortToSSLAndProtocol(config); + this.onChangedManualEdit(); + }, + + onChangedPortIncoming() { + gAccountSetupLogger.debug( + "incoming port changed: " + document.getElementById("incomingPort").value + ); + this.adjustIncomingSSLToPort(this.getUserConfig()); + this.onChangedManualEdit(); + }, + + onChangedPortOutgoing() { + gAccountSetupLogger.debug( + "outgoing port changed: " + document.getElementById("outgoingPort").value + ); + this.adjustOutgoingSSLToPort(this.getUserConfig()); + this.onChangedManualEdit(); + }, + + onChangedSSLIncoming() { + this.adjustIncomingPortToSSLAndProtocol(this.getUserConfig()); + this.onChangedManualEdit(); + }, + + onChangedSSLOutgoing() { + this.adjustOutgoingPortToSSLAndProtocol(this.getUserConfig()); + this.onChangedManualEdit(); + }, + + onChangedInAuth() { + this.onChangedManualEdit(); + }, + + onChangedOutAuth(event) { + // Disable the outgoing username field if the "No Authentication" option is + // selected. + document.getElementById("outgoingUsername").disabled = + event.target.selectedOptions[0].id == "outNoAuth"; + this.onChangedManualEdit(); + }, + + onInputInUsername() { + if (this.sameInOutUsernames) { + document.getElementById("outgoingUsername").value = + document.getElementById("incomingUsername").value; + } + this.onChangedManualEdit(); + }, + + onInputOutUsername() { + this.sameInOutUsernames = false; + this.onChangedManualEdit(); + }, + + onChangeHostname() { + this.adjustOAuth2Visibility(this.getUserConfig()); + this.onChangedManualEdit(); + }, + + /** + * A value in the manual configuration area was changed. + */ + onChangedManualEdit() { + // If there's a current operation in progress and is abortable. + if (this._abortable) { + this.onStop(); + } + this.validateManualEditComplete(); + }, + + /** + * The user interacted with an input field in the manual configuration area + * therefore we need to clear previous notifications and disable the "Done" + * button as the current config is not valid until we run again the + * validateManualEditComplete() method, which happens on input blur. + */ + manualConfigChanged() { + this.clearNotifications(); + document.getElementById("createButton").disabled = true; + }, + + /** + * This enables the buttons which allow the user to proceed + * once he has entered enough information. + * + * We can easily and fairly surely autodetect everything apart from the + * hostname (and username). So, once the user has entered + * proper hostnames, change to "manual-edit-have-hostname" mode + * which allows to press [Re-test], which starts the detection + * of the other values. + * Once the user has entered (or we detected) all values, he may + * do [Create Account] (tests login and if successful creates the account) + * or [Advanced Setup] (goes to Account Manager). Esp. in the latter case, + * we will not second-guess his setup and just to as told, so here we make + * sure that he at least entered all values. + */ + validateManualEditComplete() { + // getUserConfig() is expensive, but still OK, not a problem. + let manualConfig = this.getUserConfig(); + this._currentConfig = manualConfig; + + if (manualConfig.isComplete()) { + this.switchToMode("manual-edit-complete"); + return; + } + + if (!!manualConfig.incoming.hostname && !!manualConfig.outgoing.hostname) { + this.switchToMode("manual-edit-have-hostname"); + return; + } + + this.switchToMode("manual-edit"); + }, + + /** + * [Advanced Setup...] button click handler + * Only active in manual edit mode, and goes straight into + * Account Settings (pref UI) dialog. Requires a backend account, + * which requires proper hostname, port and protocol. + */ + async onAdvancedSetup() { + assert(this._currentConfig instanceof AccountConfig); + let configFilledIn = this.getConcreteConfig(); + + if (CreateInBackend.checkIncomingServerAlreadyExists(configFilledIn)) { + let [title, description] = await document.l10n.formatValues([ + "account-setup-creation-error-title", + "account-setup-error-server-exists", + ]); + Services.prompt.alert(null, title, description); + return; + } + + let [title, description] = await document.l10n.formatValues([ + "account-setup-confirm-advanced-title", + "account-setup-confirm-advanced-description", + ]); + + if (!Services.prompt.confirm(null, title, description)) { + return; + } + + gAccountSetupLogger.debug("creating account in backend"); + let newAccount = CreateInBackend.createAccountInBackend(configFilledIn); + + window.close(); + gMainWindow.postMessage("account-created-in-backend", "*"); + MsgAccountManager("am-server.xhtml", newAccount.incomingServer); + }, + + /** + * [Re-test] button click handler. + * Restarts the config guessing process after a person editing the server + * fields. + * It's called "half-manual", because we take the user-entered values + * as given and will not second-guess them, to respect the user wishes. + * (Yes, Sir! Will do as told!) + * The values that the user left empty or on "Auto" will be guessed/probed + * here. We will also check that the user-provided values work. + */ + async testManualConfig() { + this.clearNotifications(); + await this.startLoadingState( + "account-setup-looking-up-settings-half-manual" + ); + + let newConfig = this.getUserConfig(); + gAccountSetupLogger.debug("manual config to test:\n" + newConfig); + + this.switchToMode("manual-edit-testing"); + // if (this._userPickedOutgoingServer) TODO + let self = this; + this._abortable = GuessConfig.guessConfig( + this._domain, + function (type, hostname, port, ssl, done, config) { + // Progress. + gAccountSetupLogger.debug( + `progress callback host: ${hostname}, port: ${port}, type: ${type}` + ); + }, + function (config) { + // Success. + self._abortable = null; + self._fillManualEditFields(config); + self.stopLoadingState("account-setup-success-half-manual"); + self.validateManualEditComplete(); + }, + function (e, config) { + // guessConfig failed. + if (e instanceof CancelledException) { + return; + } + self._abortable = null; + gAccountSetupLogger.warn(`guessConfig failed: ${e}`); + self.showErrorNotification("account-setup-find-settings-failed"); + self.switchToMode("manual-edit-have-hostname"); + }, + newConfig, + newConfig.outgoing.existingServerKey ? "incoming" : "both" + ); + }, + + // ------------------- + // UI helper functions + + _prefillConfig(initialConfig) { + let emailsplit = this._email.split("@"); + assert(emailsplit.length > 1); + let emaillocal = Sanitizer.nonemptystring(emailsplit[0]); + initialConfig.incoming.username = emaillocal; + initialConfig.outgoing.username = emaillocal; + return initialConfig; + }, + + clearError(which) { + document.getElementById(`${which}Warning`).hidden = true; + document.getElementById(`${which}Info`).hidden = false; + }, + + setError(which, msg_name) { + try { + document.getElementById(`${which}Info`).hidden = true; + document.getElementById(`${which}Warning`).hidden = false; + } catch (ex) { + alertPrompt("Missing error string", msg_name); + } + }, + + onFormSubmit(event) { + // Prevent the actual form submission. + event.preventDefault(); + + // Select the only primary button that is visible and enabled. + let currentButton = document.querySelector( + ".buttons-container-last button.primary:not([disabled],[hidden])" + ); + if (currentButton) { + currentButton.click(); + } + }, + + // ------------------------------- + // Finish & dialog close functions + + onCancel() { + // Some tests might close the account setup before it finishes loading, + // therefore the gMainWindow might still be null. If that's the case, do an + // early return since we don't need to run any condition. + if (!gMainWindow) { + window.close(); + return; + } + + // Ask for confirmation if the user never set Thunderbrid to be used without + // an email account, and no account has been configured. + if ( + !Services.prefs.getBoolPref("app.use_without_mail_account", false) && + !MailServices.accounts.accounts.length + ) { + // Abort any possible process before showing the confirmation dialog. + this.checkIfAbortable(); + this.confirmExitDialog(); + return; + } + + window.close(); + }, + + /** + * Ask for confirmation when the account setup is dismissed and the user + * doesn't have any configured account. + */ + confirmExitDialog() { + let dialog = document.getElementById("confirmExitDialog"); + + document.getElementById("exitDialogConfirmButton").onclick = () => { + // Update the pref only if the checkbox was checked since it's FALSE by + // default. We won't expose this checkbox in the UI anymore afterward. + if (document.getElementById("useWithoutAccount").checked) { + Services.prefs.setBoolPref("app.use_without_mail_account", true); + } + + dialog.close(); + window.close(); + }; + + document.getElementById("exitDialogCancelButton").onclick = () => { + dialog.close(); + }; + + dialog.showModal(); + }, + + /** + * Disable the exit dialog button if the user checks the "Use without an email + * account" checkbox. + * + * @param {DOMEvent} event - The checkbox change event. + */ + toggleExitDialogButton(event) { + document.getElementById("exitDialogCancelButton").disabled = + event.target.checked; + }, + + checkIfAbortable() { + if (this._abortable) { + this._abortable.cancel(new UserCancelledException()); + } + }, + + onUnload() { + gMainWindow.document + .getElementById("tabmail") + .unregisterTabMonitor(this.tabMonitor); + this.checkIfAbortable(); + gAccountSetupLogger.debug("Shutting down email config dialog"); + }, + + async onCreate() { + gAccountSetupLogger.debug("Create button clicked"); + + let configFilledIn = this.getConcreteConfig(); + let self = this; + // If the dialog is not needed, it will go straight to OK callback + gSecurityWarningDialog.open( + this._currentConfig, + configFilledIn, + true, + async function () { + // on OK + await self.validateAndFinish(configFilledIn).catch(async ex => { + let errorMessage = await document.l10n.formatValue( + "account-setup-creation-error-title" + ); + gAccountSetupLogger.error(errorMessage + ". " + ex); + + self.clearNotifications(); + let notification = self.notificationBox.appendNotification( + "accountSetupError", + { + label: errorMessage, + priority: self.notificationBox.PRIORITY_CRITICAL_HIGH, + }, + null + ); + + // Hide the close button to prevent dismissing the notification. + notification.removeAttribute("dismissable"); + }); + }, + function () { + // on cancel, do nothing + } + ); + }, + + // called by onCreate() + async validateAndFinish(configFilled) { + let configFilledIn = configFilled || this.getConcreteConfig(); + if ( + configFilledIn.incoming.type == "exchange" && + "addonAccountType" in configFilledIn.incoming + ) { + configFilledIn.incoming.type = configFilledIn.incoming.addonAccountType; + } + + if (CreateInBackend.checkIncomingServerAlreadyExists(configFilledIn)) { + let [title, description] = await document.l10n.formatValues([ + "account-setup-creation-error-title", + "account-setup-error-server-exists", + ]); + Services.prompt.alert(null, title, description); + return; + } + + if (configFilledIn.outgoing.addThisServer) { + let existingServer = + CreateInBackend.checkOutgoingServerAlreadyExists(configFilledIn); + if (existingServer) { + configFilledIn.outgoing.addThisServer = false; + configFilledIn.outgoing.existingServerKey = existingServer.key; + } + } + + let createButton = document.getElementById("createButton"); + let reTestButton = document.getElementById("reTestButton"); + createButton.disabled = true; + reTestButton.disabled = true; + + this.clearNotifications(); + this.startLoadingState("account-setup-checking-password"); + let telemetryKey = + this._currentConfig.source == AccountConfig.kSourceXML || + this._currentConfig.source == AccountConfig.kSourceExchange + ? this._currentConfig.subSource + : this._currentConfig.source; + + let self = this; + let verifier = new ConfigVerifier(this._msgWindow); + window.addEventListener("unload", event => { + verifier.cleanup(); + }); + verifier + .verifyConfig( + configFilledIn, + // guess login config? + configFilledIn.source != AccountConfig.kSourceXML + // TODO Instead, the following line would be correct, but I cannot use it, + // because some other code doesn't adhere to the expectations/specs. + // Find out what it was and fix it. + // concreteConfig.source == AccountConfig.kSourceGuess, + ) + .then(successfulConfig => { + // success + self.stopLoadingState( + successfulConfig.incoming.password + ? "account-setup-success-password" + : null + ); + + // The auth might have changed, so we should update the current config. + self._currentConfig.incoming.auth = successfulConfig.incoming.auth; + self._currentConfig.outgoing.auth = successfulConfig.outgoing.auth; + self._currentConfig.incoming.username = + successfulConfig.incoming.username; + self._currentConfig.outgoing.username = + successfulConfig.outgoing.username; + + // We loaded dynamic client registration, fill this data back in to the + // config set. + if (successfulConfig.incoming.oauthSettings) { + self._currentConfig.incoming.oauthSettings = + successfulConfig.incoming.oauthSettings; + } + if (successfulConfig.outgoing.oauthSettings) { + self._currentConfig.outgoing.oauthSettings = + successfulConfig.outgoing.oauthSettings; + } + self.finish(configFilledIn); + + Services.telemetry.keyedScalarAdd( + "tb.account.successful_email_account_setup", + telemetryKey, + 1 + ); + }) + .catch(e => { + // failed + // Could be a wrong password, but there are 1000 other + // reasons why this failed. Only the backend knows. + // If we got no message, then something other than VerifyLogon failed. + + // For an Exchange server, some known configurations can + // be disabled (per user or domain or server). + // Warn the user if the open protocol we tried didn't work. + if ( + ["imap", "pop3"].includes(configFilledIn.incoming.type) && + configFilledIn.incomingAlternatives.some(i => i.type == "exchange") + ) { + self.showErrorNotification( + "account-setup-exchange-config-unverifiable" + ); + } else { + let msg = e.message || e.toString(); + self.showErrorNotification(msg, true); + } + + // give user something to proceed after fixing + createButton.disabled = false; + // hidden in non-manual mode, so it's fine to enable + reTestButton.disabled = false; + + Services.telemetry.keyedScalarAdd( + "tb.account.failed_email_account_setup", + telemetryKey, + 1 + ); + }); + }, + + /** + * @param {AccountConfig} concreteConfig - The config to use. + */ + finish(concreteConfig) { + gAccountSetupLogger.debug("creating account in backend"); + let newAccount = CreateInBackend.createAccountInBackend(concreteConfig); + + // Trigger the first login to download the folder structure and messages. + newAccount.incomingServer.getNewMessages( + newAccount.incomingServer.rootFolder, + this._msgWindow, + null + ); + + if (this._okCallback) { + this._okCallback(); + } + + this.showSuccessView(newAccount); + }, + + /** + * Toggle the visibility of the list of available services to configure. + */ + toggleSetupContainer(event) { + let container = event.target.closest(".linked-services-section"); + container.classList.toggle("opened"); + container + .querySelector(".linked-services-container") + .toggleAttribute("hidden"); + }, + + /** + * Update the account setup tab to show a successful final view with quick + * links and suggested next steps. + * + * @param {nsIMsgAccount} account - The newly created account. + */ + async showSuccessView(account) { + gAccountSetupLogger.debug("Account creation successful"); + + // Populate the account recap info. + document.getElementById("newAccountName").textContent = this._realname; + document.getElementById("newAccountEmail").textContent = this._email; + document.getElementById("newAccountProtocol").textContent = + account.incomingServer.type; + + // Store the host domain that will be used to look for CardDAV and CalDAV + // services. + this._hostname = this._email.split("@")[1]; + + // Set up event listeners for the quick links. + document.getElementById("settingsButton").addEventListener( + "click", + () => { + MsgAccountManager(null, account.incomingServer); + }, + { once: true } + ); + + // Hide the e2ee button if the current server doesn't support it. + let hasEncryption = + account.incomingServer.type != "rss" && + account.incomingServer.type != "nntp" && + account.incomingServer.protocolInfo?.canGetMessages; + document.getElementById("encryptionButton").hidden = !hasEncryption; + if (hasEncryption) { + document + .getElementById("encryptionButton") + .addEventListener("click", () => { + MsgAccountManager("am-e2e.xhtml", account.incomingServer); + }); + } + + document.getElementById("signatureButton").addEventListener("click", () => { + MsgAccountManager(null, account.incomingServer); + }); + + // Finally, show the success view. + this.switchToMode("success"); + + // Initialize the fetching of possible linked services like address books + // or calendars. + gAccountSetupLogger.debug("Fetching linked address books and calendars"); + + let notification = this.syncingBox.appendNotification( + "accountSetupLoading", + { + label: await document.l10n.formatValue( + "account-setup-looking-up-address-books" + ), + priority: this.syncingBox.PRIORITY_INFO_LOW, + }, + null + ); + notification.setAttribute("align", "center"); + + // Hide the close button to prevent dismissing the notification. + notification.removeAttribute("dismissable"); + + // Detect linked address books. + await this.fetchAddressBooks(); + + // Update the notification and start detecting linked calendars. + document.l10n.setAttributes( + notification.messageText, + "account-setup-looking-up-calendars" + ); + await this.fetchCalendars(); + + // Update the connected services description if we have at least one address + // book or one calendar we can connect to. + document.l10n.setAttributes( + document.getElementById("linkedServicesDescription"), + !this.addressBooks.length && !this.calendars.size + ? "account-setup-no-linked-description" + : "account-setup-linked-services-description" + ); + + // Clear the loading notification. + this.syncingBox.removeAllNotifications(); + this.showHelperImage("step5"); + }, + + /** + * Fetch any available CardDAV address books. + */ + async fetchAddressBooks() { + this.addressBooks = []; + try { + this.addressBooks = await CardDAVUtils.detectAddressBooks( + this._email, + this._password, + `https://${this._hostname}`, + false + ); + } catch (ex) { + gAccountSetupLogger.error(ex); + } + + let hideAddressBookUI = !this.addressBooks.length; + document.getElementById("linkedAddressBooks").hidden = hideAddressBookUI; + + // Clear the UI from any previous list. + let abList = document.querySelector( + "#addressBooksSetup .linked-services-list" + ); + while (abList.hasChildNodes()) { + abList.lastChild.remove(); + } + + // Interrupt if we don't have anything to show. + if (hideAddressBookUI) { + return; + } + + document.l10n.setAttributes( + document.getElementById("addressBooksCountDescription"), + "account-setup-found-address-books-description", + { count: this.addressBooks.length } + ); + + // Collect existing carddav address books to compare with the list of + // recently fetched ones. + let existing = MailServices.ab.directories.map(d => + d.getStringValue("carddav.url", "") + ); + + // Populate the list of available address books. + for (let book of this.addressBooks) { + let provider = document.createElement("span"); + provider.classList.add("protocol-type"); + provider.textContent = "CardDAV"; + + let name = document.createElement("span"); + name.classList.add("list-item-name"); + name.textContent = book.name; + + let button = document.createElement("button"); + button.setAttribute("type", "button"); + + if (existing.includes(book.url.href)) { + // This address book aready exists for some reason, so disable the + // button and mark it as existing. + button.classList.add("existing", "small-button"); + document.l10n.setAttributes( + button, + "account-setup-existing-address-book" + ); + button.disabled = true; + } else { + button.classList.add("small-button"); + document.l10n.setAttributes(button, "account-setup-connect-link"); + button.addEventListener("click", () => { + this._setupAddressBook(button, book); + }); + } + + let row = document.createElement("li"); + row.appendChild(provider); + row.appendChild(name); + row.appendChild(button); + abList.appendChild(row); + } + + // Show a "connect all" button if we have more than one address book. + document.getElementById("addressBooksSetupAll").hidden = + this.addressBooks.length <= 1; + }, + + /** + * Connect to the selected address book. + * + * @param {HTMLElement} button - The clicked button in the list. + * @param {foundBook} book - The address book to configure. + */ + _setupAddressBook(button, book) { + book.create(); + + // Update the button to reflect the creation of the new address book. + button.classList.add("existing"); + document.l10n.setAttributes(button, "account-setup-existing-address-book"); + button.disabled = true; + + // Check if we have any address book left to set up and hide the + // "Connect all" button if not. + document.getElementById("addressBooksSetupAll").hidden = + !document.querySelectorAll( + "#addressBooksSetup .linked-services-list button:not(.existing)" + ).length; + }, + + /** + * Loop through all available address books found and click the connect + * button to trigger the method attached to the onclick listener. + */ + setupAllAddressBooks() { + for (let button of document.querySelectorAll( + "#addressBooksSetup .linked-services-list button" + )) { + button.click(); + } + }, + + /** + * Fetch any available CalDAV calendars. + */ + async fetchCalendars() { + this.calendars = {}; + try { + this.calendars = await cal.provider.detection.detect( + this._email, + this._password, + `https://${this._hostname}`, + document.getElementById("rememberPassword").checked, + [], + {} + ); + } catch (ex) { + gAccountSetupLogger.error(ex); + } + + let hideCalendarUI = !this.calendars.size; + document.getElementById("linkedCalendars").hidden = hideCalendarUI; + + // Clear the UI from any previous list. + let calList = document.querySelector( + "#calendarsSetup .linked-services-list" + ); + while (calList.hasChildNodes()) { + calList.lastChild.remove(); + } + + // Interrupt if we don't have anything to show. + if (hideCalendarUI) { + return; + } + + // Collect existing calendars to compare with the list of recently fetched + // ones. + let existing = new Set( + cal.manager.getCalendars({}).map(calendar => calendar.uri.spec) + ); + + let calendarsCount = 0; + + // Populate the list of available calendars. + for (let [provider, calendars] of this.calendars.entries()) { + for (let calendar of calendars) { + let cal_provider = document.createElement("span"); + cal_provider.classList.add("protocol-type"); + cal_provider.textContent = provider.shortName; + + let cal_name = document.createElement("span"); + cal_name.classList.add("list-item-name"); + cal_name.textContent = calendar.name; + + let button = document.createElement("button"); + button.setAttribute("type", "button"); + + if (existing.has(calendar.uri.spec)) { + // This calendar aready exists for some reason, so disable the button + // and mark it as existing. + button.classList.add("existing", "small-button"); + document.l10n.setAttributes( + button, + "account-setup-existing-calendar" + ); + button.disabled = true; + } else { + button.classList.add("small-button"); + document.l10n.setAttributes(button, "account-setup-connect-link"); + button.addEventListener("click", () => { + // If the button has a specific data attribute it means we want to + // set up the calendar directly without opening the dialog. + if (button.hasAttribute("data-setup-calendar")) { + this._setupCalendar(button, calendar); + return; + } + + this._showCalendarDialog(button, calendar); + }); + } + + let row = document.createElement("li"); + row.appendChild(cal_provider); + row.appendChild(cal_name); + row.appendChild(button); + calList.appendChild(row); + + calendarsCount++; + } + } + + document.l10n.setAttributes( + document.getElementById("calendarsCountDescription"), + "account-setup-found-calendars-description", + { count: calendarsCount } + ); + + // Show a "connect all" button if we have more than one calendar. + document.getElementById("calendarsSetupAll").hidden = calendarsCount <= 1; + }, + + /** + * Show the dialog to connect the selected calendar. This native HTML dialog + * is a streamlined version of the calendar-properties-dialog.xhtml. The two + * dialogs should kept in sync if a property of the calendar changes that + * requires updating any field. + * + * @param {HTMLElement} button - The clicked button in the list. + * @param {calICalendar} calendar - The calendar to configure. + */ + _showCalendarDialog(button, calendar) { + let dialog = document.getElementById("calendarDialog"); + + // Update the calendar info in the dialog. + let nameInput = document.getElementById("calendarName"); + nameInput.value = calendar.name; + + // Some servers provide colors as an 8-character hex string, which the color + // picker can't handle. Strip the alpha component. + let color = calendar.getProperty("color"); + let alpha = color?.match(/^(#[0-9A-Fa-f]{6})[0-9A-Fa-f]{2}$/); + if (alpha) { + calendar.setProperty("color", alpha[1]); + color = alpha[1]; + } + let colorInput = document.getElementById("calendarColor"); + colorInput.value = color || "#A8C2E1"; + + let readOnlyCheckbox = document.getElementById("calendarReadOnly"); + readOnlyCheckbox.checked = calendar.readOnly; + + // Hide the "Show reminders" checkbox if the calendar doesn't support it. + document.getElementById("calendarShowRemindersRow").hidden = + calendar.getProperty("capabilities.alarms.popup.supported") === false; + let remindersCheckbox = document.getElementById("calendarShowReminders"); + remindersCheckbox.checked = !calendar.getProperty("suppressAlarms"); + + // Hide the "Offline support" if the calendar doesn't support it. + let offlineCheckbox = document.getElementById("calendarOfflineSupport"); + let canCache = calendar.getProperty("cache.supported") !== false; + let alwaysCache = calendar.getProperty("cache.always"); + if (!canCache || alwaysCache) { + offlineCheckbox.hidden = true; + offlineCheckbox.disabled = true; + } + offlineCheckbox.checked = + alwaysCache || (canCache && calendar.getProperty("cache.enabled")); + + // Set up the "Refresh calendar" menulist. + let calendarRefresh = document.getElementById("calendarRefresh"); + calendarRefresh.disabled = !calendar.canRefresh; + calendarRefresh.value = calendar.getProperty("refreshInterval") || 30; + + // Set up the dialog's action buttons. + document.getElementById("calendarDialogConfirmButton").onclick = () => { + // Update the attributes of the calendar in case the user changed some + // values. + calendar.name = nameInput.value; + calendar.setProperty("color", colorInput.value); + if (calendar.canRefresh) { + calendar.setProperty("refreshInterval", calendarRefresh.value); + } + + calendar.readOnly = readOnlyCheckbox.checked; + calendar.setProperty("suppressAlarms", !remindersCheckbox.checked); + if (!alwaysCache) { + calendar.setProperty("cache.enabled", offlineCheckbox.checked); + } + + this._setupCalendar(button, calendar); + dialog.close(); + }; + + document.getElementById("calendarDialogCancelButton").onclick = () => { + dialog.close(); + }; + + dialog.showModal(); + }, + + /** + * Connect to the selected calendar. + * + * @param {HTMLElement} button - The clicked button in the list. + * @param {calICalendar} calendar - The calendar to configure. + */ + _setupCalendar(button, calendar) { + cal.manager.registerCalendar(calendar); + + // Update the button to reflect the creation of the new calendar. + button.classList.add("existing"); + document.l10n.setAttributes(button, "account-setup-existing-calendar"); + button.disabled = true; + + // Check if we have any calendar left to set up and hide the "Connect all" + // button if not. + document.getElementById("calendarsSetupAll").hidden = + !document.querySelectorAll( + "#calendarsSetup .linked-services-list button:not(.existing)" + ).length; + }, + + /** + * Loop through all available calendars found and click the connect + * button to trigger the method attached to the onclick listener. + */ + setupAllCalendars() { + for (let button of document.querySelectorAll( + "#calendarsSetup .linked-services-list button:not(.existing)" + )) { + // Set the attribute to skip the opening of the properties dialog. + button.setAttribute("data-setup-calendar", true); + button.click(); + } + }, + + /** + * Called from the very final view of the account setup, when the user decides + * to close the wizard. + */ + onFinish() { + // Send the message to the mail tab in case the UI didn't load during the + // previous setup callback. + gMainWindow.postMessage("account-setup-closed", "*"); + // Close this tab. + window.close(); + }, +}; + +function serverMatches(a, b) { + return ( + a.type == b.type && + a.hostname == b.hostname && + a.port == b.port && + a.socketType == b.socketType && + a.auth == b.auth + ); +} + +/** + * Warning dialog, warning user about lack of, or inappropriate, encryption. + */ +var gSecurityWarningDialog = { + /** + * {Array of {(incoming or outgoing) server part of {AccountConfig}} + * A list of the servers for which we already showed this dialog and the + * user approved the configs. For those, we won't show the warning again. + * (Make sure to store a copy in case the underlying object is changed.) + */ + _acknowledged: [], + + _inSecurityBad: 0x0001, + _inCertBad: 0x0010, + _outSecurityBad: 0x0100, + _outCertBad: 0x1000, + + /** + * Checks whether we need to warn about this config. + * + * We (currently) warn if + * - the mail travels unsecured (no SSL/STARTTLS) + * - (We don't warn about unencrypted passwords specifically, + * because they'd be encrypted with SSL and without SSL, we'd + * warn anyways.) + * + * We may not warn despite these conditions if we had shown the + * warning for that server before and the user acknowledged it. + * (Given that this dialog object is static/global and persistent, + * we can store that approval state here in this object.) + * + * @param configSchema @see open() + * @param configFilledIn @see open() + * @returns {boolean} - True when the dialog should be shown + * (call open()). if false, the dialog can and should be skipped. + */ + needed(configSchema, configFilledIn) { + assert(configSchema instanceof AccountConfig); + assert(configFilledIn instanceof AccountConfig); + assert(configSchema.isComplete()); + assert(configFilledIn.isComplete()); + + let incomingBad = + (configFilledIn.incoming.socketType > 1 ? 0 : this._inSecurityBad) | + (configFilledIn.incoming.badCert ? this._inCertBad : 0); + let outgoingBad = 0; + if (configFilledIn.outgoing.addThisServer) { + outgoingBad = + (configFilledIn.outgoing.socketType > 1 ? 0 : this._outSecurityBad) | + (configFilledIn.outgoing.badCert ? this._outCertBad : 0); + } + + if (incomingBad > 0) { + if ( + this._acknowledged.some(ackServer => { + return serverMatches(ackServer, configFilledIn.incoming); + }) + ) { + incomingBad = 0; + } + } + if (outgoingBad > 0) { + if ( + this._acknowledged.some(ackServer => { + return serverMatches(ackServer, configFilledIn.outgoing); + }) + ) { + outgoingBad = 0; + } + } + + return incomingBad | outgoingBad; + }, + + /** + * Opens the dialog, fills it with values, and shows it to the user. + * + * The function is async: it returns immediately, and when the user clicks + * OK or Cancel, the callbacks are called. There the callers proceed as + * appropriate. + * + * @param configSchema The config, with placeholders not replaced yet. + * This object may be modified to store the user's confirmations, but + * currently that's not the case. + * @param configFilledIn The concrete config with placeholders replaced. + * @param onlyIfNeeded {Boolean} - If there is nothing to warn about, + * call okCallback() immediately (and sync). + * @param okCallback {function(config {AccountConfig})} + * Called when the user clicked OK and approved the config including + * the warnings. |config| is without placeholders replaced. + * @param cancalCallback {function()} + * Called when the user decided to heed the warnings and not approve. + */ + open(configSchema, configFilledIn, onlyIfNeeded, okCallback, cancelCallback) { + assert(typeof okCallback == "function"); + assert(typeof cancelCallback == "function"); + + // needed() also checks the parameters + let needed = this.needed(configSchema, configFilledIn); + if (needed == 0 && onlyIfNeeded) { + okCallback(); + return; + } + + assert(needed > 0, "security dialog opened needlessly"); + + let dialog = document.getElementById("insecureDialog"); + this._currentConfigFilledIn = configFilledIn; + this._okCallback = okCallback; + this._cancelCallback = cancelCallback; + let incoming = configFilledIn.incoming; + let outgoing = configFilledIn.outgoing; + + // Reset the dialog, in case we've shown it before. + document.getElementById("acknowledgeWarning").checked = false; + document.getElementById("insecureConfirmButton").disabled = true; + + // Incoming security is bad. + let insecureIncoming = document.getElementById("insecureSectionIncoming"); + if (needed & this._inSecurityBad) { + document.l10n.setAttributes( + document.getElementById("warningIncoming"), + "account-setup-warning-cleartext", + { + server: incoming.hostname, + } + ); + + document.l10n.setAttributes( + document.getElementById("detailsIncoming"), + "account-setup-warning-cleartext-details" + ); + + insecureIncoming.hidden = false; + } else { + insecureIncoming.hidden = true; + } + + // Outgoing security or certificate is bad. + let insecureOutgoing = document.getElementById("insecureSectionOutgoing"); + if (needed & this._outSecurityBad) { + document.l10n.setAttributes( + document.getElementById("warningOutgoing"), + "account-setup-warning-cleartext", + { + server: outgoing.hostname, + } + ); + + document.l10n.setAttributes( + document.getElementById("detailsOutgoing"), + "account-setup-warning-cleartext-details" + ); + + insecureOutgoing.hidden = false; + } else { + insecureOutgoing.hidden = true; + } + + assert( + !insecureIncoming.hidden || !insecureOutgoing.hidden, + "warning dialog shown for unknown reason" + ); + + // Show the dialog. + dialog.showModal(); + }, + + /** + * User checked checkbox that he understood it and wishes to ignore the + * warning. + */ + toggleAcknowledge() { + document.getElementById("insecureConfirmButton").disabled = + !document.getElementById("acknowledgeWarning").checked; + }, + + /** + * [Cancel] button pressed. Get me out of here! + */ + onCancel() { + document.getElementById("insecureDialog").close(); + document.getElementById("incomingProtocol").focus(); + + this._cancelCallback(); + }, + + /** + * [OK] button pressed. + * Implies that the user toggled the acknowledge checkbox, + * i.e. approved the config and ignored the warnings, + * otherwise the button would have been disabled. + */ + onOK() { + assert(document.getElementById("acknowledgeWarning").checked); + + // Need filled in, in case the hostname is a placeholder. + let storeConfig = this._currentConfigFilledIn.copy(); + this._acknowledged.push(storeConfig.incoming); + this._acknowledged.push(storeConfig.outgoing); + + document.getElementById("insecureDialog").close(); + + this._okCallback(); + }, +}; + +/** + * Helper method to open the dictionaries list in a new tab. + */ +function openDictionariesTab() { + let mailWindow = Services.wm.getMostRecentWindow("mail:3pane"); + let tabmail = mailWindow.document.getElementById("tabmail"); + + let url = Services.urlFormatter.formatURLPref( + "spellchecker.dictionaries.download.url" + ); + + // Open the dictionaries URL. + tabmail.openTab("contentTab", { + url, + }); +} diff --git a/comm/mail/components/accountcreation/content/accountSetup.xhtml b/comm/mail/components/accountcreation/content/accountSetup.xhtml new file mode 100644 index 0000000000..ed1148c561 --- /dev/null +++ b/comm/mail/components/accountcreation/content/accountSetup.xhtml @@ -0,0 +1,1333 @@ +<?xml version="1.0"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> + +<!DOCTYPE html> +<html + id="accountSetup" + xmlns="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + windowtype="mail:accountsetup" +> + <head> + <title data-l10n-id="account-setup-tab-title"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+

+

+
+ +
+ + + + + + + + +
+ + diff --git a/comm/mail/components/accountcreation/jar.mn b/comm/mail/components/accountcreation/jar.mn new file mode 100644 index 0000000000..0a3389020e --- /dev/null +++ b/comm/mail/components/accountcreation/jar.mn @@ -0,0 +1,12 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +messenger.jar: + content/messenger/accountcreation/accountHub.js (content/accountHub.js) + content/messenger/accountcreation/accountSetup.js (content/accountSetup.js) + content/messenger/accountcreation/accountSetup.xhtml (content/accountSetup.xhtml) +# Custom elements + content/messenger/accountcreation/views/container.mjs (views/container.mjs) + content/messenger/accountcreation/views/email.mjs (views/email.mjs) + content/messenger/accountcreation/views/start.mjs (views/start.mjs) diff --git a/comm/mail/components/accountcreation/moz.build b/comm/mail/components/accountcreation/moz.build new file mode 100644 index 0000000000..fa8ce3c258 --- /dev/null +++ b/comm/mail/components/accountcreation/moz.build @@ -0,0 +1,23 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +JAR_MANIFESTS += ["jar.mn"] + +EXTRA_JS_MODULES.accountcreation += [ + "AccountConfig.jsm", + "AccountCreationUtils.jsm", + "ConfigVerifier.jsm", + "CreateInBackend.jsm", + "ExchangeAutoDiscover.jsm", + "FetchConfig.jsm", + "FetchHTTP.jsm", + "GuessConfig.jsm", + "readFromXML.jsm", + "Sanitizer.jsm", +] + +XPCSHELL_TESTS_MANIFESTS += [ + "test/xpcshell/xpcshell.ini", +] diff --git a/comm/mail/components/accountcreation/readFromXML.jsm b/comm/mail/components/accountcreation/readFromXML.jsm new file mode 100644 index 0000000000..b853a81117 --- /dev/null +++ b/comm/mail/components/accountcreation/readFromXML.jsm @@ -0,0 +1,352 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["readFromXML"]; + +const lazy = {}; + +ChromeUtils.defineModuleGetter( + lazy, + "AccountConfig", + "resource:///modules/accountcreation/AccountConfig.jsm" +); +ChromeUtils.defineModuleGetter( + lazy, + "AccountCreationUtils", + "resource:///modules/accountcreation/AccountCreationUtils.jsm" +); +ChromeUtils.defineModuleGetter( + lazy, + "Sanitizer", + "resource:///modules/accountcreation/Sanitizer.jsm" +); + +/* eslint-disable complexity */ +/** + * Takes an XML snipplet (as JXON) and reads the values into + * a new AccountConfig object. + * It does so securely (or tries to), by trying to avoid remote execution + * and similar holes which can appear when reading too naively. + * Of course it cannot tell whether the actual values are correct, + * e.g. it can't tell whether the host name is a good server. + * + * The XML format is documented at + * + * + * @param clientConfigXML {JXON} - The node. + * @param source {String} - Used for the subSource field of AccountConfig. + * @returns AccountConfig object filled with the data from XML + */ +function readFromXML(clientConfigXML, subSource) { + function array_or_undef(value) { + return value === undefined ? [] : value; + } + var exception; + if ( + typeof clientConfigXML != "object" || + !("clientConfig" in clientConfigXML) || + !("emailProvider" in clientConfigXML.clientConfig) + ) { + dump( + `client config xml = ${JSON.stringify(clientConfigXML).substr(0, 50)} \n` + ); + let stringBundle = lazy.AccountCreationUtils.getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties" + ); + throw stringBundle.GetStringFromName("no_emailProvider.error"); + } + var xml = clientConfigXML.clientConfig.emailProvider; + + var d = new lazy.AccountConfig(); + d.source = lazy.AccountConfig.kSourceXML; + d.subSource = `xml-from-${subSource}`; + + d.id = lazy.Sanitizer.hostname(xml["@id"]); + d.displayName = d.id; + try { + d.displayName = lazy.Sanitizer.label(xml.displayName); + } catch (e) { + console.error(e); + } + for (var domain of xml.$domain) { + try { + d.domains.push(lazy.Sanitizer.hostname(domain)); + } catch (e) { + console.error(e); + exception = e; + } + } + if (d.domains.length == 0) { + throw exception ? exception : "need proper in XML"; + } + exception = null; + + // incoming server + for (let iX of array_or_undef(xml.$incomingServer)) { + // input (XML) + let iO = d.createNewIncoming(); // output (object) + try { + // throws if not supported + iO.type = lazy.Sanitizer.enum(iX["@type"], [ + "pop3", + "imap", + "nntp", + "exchange", + ]); + iO.hostname = lazy.Sanitizer.hostname(iX.hostname); + iO.port = lazy.Sanitizer.integerRange(iX.port, 1, 65535); + // We need a username even for Kerberos, need it even internally. + iO.username = lazy.Sanitizer.string(iX.username); // may be a %VARIABLE% + + if ("password" in iX) { + d.rememberPassword = true; + iO.password = lazy.Sanitizer.string(iX.password); + } + + for (let iXsocketType of array_or_undef(iX.$socketType)) { + try { + iO.socketType = lazy.Sanitizer.translate(iXsocketType, { + plain: Ci.nsMsgSocketType.plain, + SSL: Ci.nsMsgSocketType.SSL, + STARTTLS: Ci.nsMsgSocketType.alwaysSTARTTLS, + }); + break; // take first that we support + } catch (e) { + exception = e; + } + } + if (iO.socketType == -1) { + throw exception ? exception : "need proper in XML"; + } + exception = null; + + for (let iXauth of array_or_undef(iX.$authentication)) { + try { + iO.auth = lazy.Sanitizer.translate(iXauth, { + "password-cleartext": Ci.nsMsgAuthMethod.passwordCleartext, + // @deprecated TODO remove + plain: Ci.nsMsgAuthMethod.passwordCleartext, + "password-encrypted": Ci.nsMsgAuthMethod.passwordEncrypted, + // @deprecated TODO remove + secure: Ci.nsMsgAuthMethod.passwordEncrypted, + GSSAPI: Ci.nsMsgAuthMethod.GSSAPI, + NTLM: Ci.nsMsgAuthMethod.NTLM, + OAuth2: Ci.nsMsgAuthMethod.OAuth2, + }); + break; // take first that we support + } catch (e) { + exception = e; + } + } + if (!iO.auth) { + throw exception ? exception : "need proper in XML"; + } + exception = null; + + if (iO.type == "exchange") { + try { + if ("owaURL" in iX) { + iO.owaURL = lazy.Sanitizer.url(iX.owaURL); + } + } catch (e) { + console.error(e); + } + try { + if ("ewsURL" in iX) { + iO.ewsURL = lazy.Sanitizer.url(iX.ewsURL); + } + } catch (e) { + console.error(e); + } + try { + if ("easURL" in iX) { + iO.easURL = lazy.Sanitizer.url(iX.easURL); + } + } catch (e) { + console.error(e); + } + iO.oauthSettings = { + issuer: iO.hostname, + scope: iO.owaURL || iO.ewsURL || iO.easURL, + }; + } + // defaults are in accountConfig.js + if (iO.type == "pop3" && "pop3" in iX) { + try { + if ("leaveMessagesOnServer" in iX.pop3) { + iO.leaveMessagesOnServer = lazy.Sanitizer.boolean( + iX.pop3.leaveMessagesOnServer + ); + } + if ("daysToLeaveMessagesOnServer" in iX.pop3) { + iO.daysToLeaveMessagesOnServer = lazy.Sanitizer.integer( + iX.pop3.daysToLeaveMessagesOnServer + ); + } + } catch (e) { + console.error(e); + } + try { + if ("downloadOnBiff" in iX.pop3) { + iO.downloadOnBiff = lazy.Sanitizer.boolean(iX.pop3.downloadOnBiff); + } + } catch (e) { + console.error(e); + } + } + + try { + if ("useGlobalPreferredServer" in iX) { + iO.useGlobalPreferredServer = lazy.Sanitizer.boolean( + iX.useGlobalPreferredServer + ); + } + } catch (e) { + console.error(e); + } + + // processed successfully, now add to result object + if (!d.incoming.hostname) { + // first valid + d.incoming = iO; + } else { + d.incomingAlternatives.push(iO); + } + } catch (e) { + exception = e; + } + } + if (!d.incoming.hostname) { + // throw exception for last server + throw exception ? exception : "Need proper in XML file"; + } + exception = null; + + // outgoing server + for (let oX of array_or_undef(xml.$outgoingServer)) { + // input (XML) + let oO = d.createNewOutgoing(); // output (object) + try { + if (oX["@type"] != "smtp") { + let stringBundle = lazy.AccountCreationUtils.getStringBundle( + "chrome://messenger/locale/accountCreationModel.properties" + ); + throw stringBundle.GetStringFromName("outgoing_not_smtp.error"); + } + oO.hostname = lazy.Sanitizer.hostname(oX.hostname); + oO.port = lazy.Sanitizer.integerRange(oX.port, 1, 65535); + + for (let oXsocketType of array_or_undef(oX.$socketType)) { + try { + oO.socketType = lazy.Sanitizer.translate(oXsocketType, { + plain: Ci.nsMsgSocketType.plain, + SSL: Ci.nsMsgSocketType.SSL, + STARTTLS: Ci.nsMsgSocketType.alwaysSTARTTLS, + }); + break; // take first that we support + } catch (e) { + exception = e; + } + } + if (oO.socketType == -1) { + throw exception ? exception : "need proper in XML"; + } + exception = null; + + for (let oXauth of array_or_undef(oX.$authentication)) { + try { + oO.auth = lazy.Sanitizer.translate(oXauth, { + // open relay + none: Ci.nsMsgAuthMethod.none, + // inside ISP or corp network + "client-IP-address": Ci.nsMsgAuthMethod.none, + // hope for the best + "smtp-after-pop": Ci.nsMsgAuthMethod.none, + "password-cleartext": Ci.nsMsgAuthMethod.passwordCleartext, + // @deprecated TODO remove + plain: Ci.nsMsgAuthMethod.passwordCleartext, + "password-encrypted": Ci.nsMsgAuthMethod.passwordEncrypted, + // @deprecated TODO remove + secure: Ci.nsMsgAuthMethod.passwordEncrypted, + GSSAPI: Ci.nsMsgAuthMethod.GSSAPI, + NTLM: Ci.nsMsgAuthMethod.NTLM, + OAuth2: Ci.nsMsgAuthMethod.OAuth2, + }); + + break; // take first that we support + } catch (e) { + exception = e; + } + } + if (!oO.auth) { + throw exception ? exception : "need proper in XML"; + } + exception = null; + + if ( + "username" in oX || + // if password-based auth, we need a username, + // so go there anyways and throw. + oO.auth == Ci.nsMsgAuthMethod.passwordCleartext || + oO.auth == Ci.nsMsgAuthMethod.passwordEncrypted + ) { + oO.username = lazy.Sanitizer.string(oX.username); + } + + if ("password" in oX) { + d.rememberPassword = true; + oO.password = lazy.Sanitizer.string(oX.password); + } + + try { + // defaults are in accountConfig.js + if ("addThisServer" in oX) { + oO.addThisServer = lazy.Sanitizer.boolean(oX.addThisServer); + } + if ("useGlobalPreferredServer" in oX) { + oO.useGlobalPreferredServer = lazy.Sanitizer.boolean( + oX.useGlobalPreferredServer + ); + } + } catch (e) { + console.error(e); + } + + // processed successfully, now add to result object + if (!d.outgoing.hostname) { + // first valid + d.outgoing = oO; + } else { + d.outgoingAlternatives.push(oO); + } + } catch (e) { + console.error(e); + exception = e; + } + } + if (!d.outgoing.hostname) { + // throw exception for last server + throw exception ? exception : "Need proper in XML file"; + } + exception = null; + + d.inputFields = []; + for (let inputField of array_or_undef(xml.$inputField)) { + try { + let fieldset = { + varname: lazy.Sanitizer.alphanumdash(inputField["@key"]).toUpperCase(), + displayName: lazy.Sanitizer.label(inputField["@label"]), + exampleValue: lazy.Sanitizer.label(inputField.value), + }; + d.inputFields.push(fieldset); + } catch (e) { + console.error(e); + // For now, don't throw, + // because we don't support custom fields yet anyways. + } + } + + return d; +} +/* eslint-enable complexity */ diff --git a/comm/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml b/comm/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml new file mode 100644 index 0000000000..8b8c7c4ada --- /dev/null +++ b/comm/mail/components/accountcreation/templates/accountHubTemplate.inc.xhtml @@ -0,0 +1,158 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + + + + + + +
+ + +
+ +
+
+#ifdef NIGHTLY_BUILD + +#endif +
+ +
+ +
+
+ + + + diff --git a/comm/mail/components/accountcreation/test/xpcshell/data/example.com.xml b/comm/mail/components/accountcreation/test/xpcshell/data/example.com.xml new file mode 100644 index 0000000000..871ce732e2 --- /dev/null +++ b/comm/mail/components/accountcreation/test/xpcshell/data/example.com.xml @@ -0,0 +1,21 @@ + + + example.com + example.com + example.com + + pop.example.com + 995 + SSL + plain + %EMAILLOCALPART% + + + smtp.example.com + 587 + STARTTLS + %EMAILADDRESS% + plain + + + diff --git a/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigFetchDisk.js b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigFetchDisk.js new file mode 100644 index 0000000000..5b57a42ebb --- /dev/null +++ b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigFetchDisk.js @@ -0,0 +1,76 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests getting a configuration file from the local isp directory and + * reading that file. + */ + +// Globals + +var { AccountConfig } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountConfig.jsm" +); +var { FetchConfig } = ChromeUtils.import( + "resource:///modules/accountcreation/FetchConfig.jsm" +); + +var kXMLFile = "example.com.xml"; +var fetchConfigAbortable; +var copyLocation; + +function onTestSuccess(config) { + // Check that we got the expected config. + AccountConfig.replaceVariables( + config, + "Yamato Nadeshiko", + "yamato.nadeshiko@example.com", + "abc12345" + ); + + Assert.equal(config.incoming.username, "yamato.nadeshiko"); + Assert.equal(config.outgoing.username, "yamato.nadeshiko@example.com"); + Assert.equal(config.incoming.hostname, "pop.example.com"); + Assert.equal(config.outgoing.hostname, "smtp.example.com"); + Assert.equal(config.identity.realname, "Yamato Nadeshiko"); + Assert.equal(config.identity.emailAddress, "yamato.nadeshiko@example.com"); + + Assert.equal(config.subSource, "xml-from-disk"); + + do_test_finished(); +} + +function onTestFailure(e) { + do_throw(e); +} + +function run_test() { + registerCleanupFunction(finish_test); + + // Copy the xml file into place + let file = do_get_file("data/" + kXMLFile); + + copyLocation = Services.dirsvc.get("CurProcD", Ci.nsIFile); + copyLocation.append("isp"); + + file.copyTo(copyLocation, kXMLFile); + + do_test_pending(); + + // Now run the actual test + // Note we keep a global copy of this so that the abortable doesn't get + // garbage collected before the async operation has finished. + fetchConfigAbortable = FetchConfig.fromDisk( + "example.com", + onTestSuccess, + onTestFailure + ); +} + +function finish_test() { + // Remove the test config file + copyLocation.append(kXMLFile); + copyLocation.remove(false); +} diff --git a/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigUtils.js b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigUtils.js new file mode 100644 index 0000000000..763084f750 --- /dev/null +++ b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigUtils.js @@ -0,0 +1,319 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Tests for GuessConfig.jsm + * + * Currently tested: + * - getHostEntry function. + * - getIncomingTryOrder function. + * - getOutgoingTryOrder function. + * + * TODO: + * - Test the returned CMDS. + * - Figure out what else to test. + */ + +// Globals + +var { GuessConfig } = ChromeUtils.import( + "resource:///modules/accountcreation/GuessConfig.jsm" +); + +var { + UNKNOWN, + IMAP, + POP, + SMTP, + NONE, + STARTTLS, + SSL, + getHostEntry, + getIncomingTryOrder, + getOutgoingTryOrder, +} = GuessConfig; + +/* + * UTILITIES + */ + +function assert_equal(aA, aB, aWhy) { + if (aA != aB) { + do_throw(aWhy); + } + Assert.equal(aA, aB); +} + +/** + * Test that two host entries are the same, ignoring the commands. + */ +function assert_equal_host_entries(hostEntry, expected) { + assert_equal(hostEntry.protocol, expected[0], "Protocols are different"); + assert_equal(hostEntry.socketType, expected[1], "SSL values are different"); + assert_equal(hostEntry.port, expected[2], "Port values are different"); +} + +/** + * Assert that the list of tryOrders are the same. + */ +function assert_equal_try_orders(aA, aB) { + assert_equal(aA.length, aB.length, "tryOrders have different length"); + for (let [i, subA] of aA.entries()) { + let subB = aB[i]; + assert_equal_host_entries(subA, subB); + } +} + +/** + * Check that the POP calculations are correct for a given host and + * protocol. + */ +function checkPop(host, protocol) { + // The list of protocol+ssl+port configurations should match + // getIncomingTryOrder() in guessConfig.js. + + // port == UNKNOWN + // [POP, STARTTLS, 110], [POP, SSL, 995], [POP, NONE, 110] + // port != UNKNOWN + // ssl == UNKNOWN + // [POP, STARTTLS, port], [POP, SSL, port], [POP, NONE, port] + // ssl != UNKNOWN + // [POP, ssl, port] + let ssl = UNKNOWN; + let port = UNKNOWN; + let tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [POP, STARTTLS, 110], + [POP, SSL, 995], + [POP, NONE, 110], + ]); + + ssl = STARTTLS; + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[POP, ssl, 110]]); + + ssl = SSL; + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[POP, ssl, 995]]); + + ssl = NONE; + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[POP, ssl, 110]]); + + ssl = UNKNOWN; + port = 31337; + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [POP, STARTTLS, port], + [POP, SSL, port], + [POP, NONE, port], + ]); + + for (ssl in [STARTTLS, SSL, NONE]) { + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[POP, ssl, port]]); + } +} + +/** + * Check that the IMAP calculations are correct for a given host and + * protocol. + */ +function checkImap(host, protocol) { + // The list of protocol+ssl+port configurations should match + // getIncomingTryOrder() in guessConfig.js. + + // port == UNKNOWN + // [IMAP, STARTTLS, 143], [IMAP, SSL, 993], [IMAP, NONE, 143] + // port != UNKNOWN + // ssl == UNKNOWN + // [IMAP, STARTTLS, port], [IMAP, SSL, port], [IMAP, NONE, port] + // ssl != UNKNOWN + // [IMAP, ssl, port]; + + let ssl = UNKNOWN; + let port = UNKNOWN; + let tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [IMAP, STARTTLS, 143], + [IMAP, SSL, 993], + [IMAP, NONE, 143], + ]); + + ssl = STARTTLS; + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[IMAP, ssl, 143]]); + + ssl = SSL; + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[IMAP, ssl, 993]]); + + ssl = NONE; + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[IMAP, ssl, 143]]); + + ssl = UNKNOWN; + port = 31337; + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [IMAP, STARTTLS, port], + [IMAP, SSL, port], + [IMAP, NONE, port], + ]); + + for (ssl in [STARTTLS, SSL, NONE]) { + tryOrder = getIncomingTryOrder(host, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[IMAP, ssl, port]]); + } +} + +/* + * TESTS + */ + +/** + * Test that getHostEntry returns the correct port numbers. + * + * TODO: + * - Test the returned commands as well. + */ +function test_getHostEntry() { + // IMAP port numbers. + assert_equal_host_entries(getHostEntry(IMAP, STARTTLS, UNKNOWN), [ + IMAP, + STARTTLS, + 143, + ]); + assert_equal_host_entries(getHostEntry(IMAP, SSL, UNKNOWN), [IMAP, SSL, 993]); + assert_equal_host_entries(getHostEntry(IMAP, NONE, UNKNOWN), [ + IMAP, + NONE, + 143, + ]); + + // POP port numbers. + assert_equal_host_entries(getHostEntry(POP, STARTTLS, UNKNOWN), [ + POP, + STARTTLS, + 110, + ]); + assert_equal_host_entries(getHostEntry(POP, SSL, UNKNOWN), [POP, SSL, 995]); + assert_equal_host_entries(getHostEntry(POP, NONE, UNKNOWN), [POP, NONE, 110]); + + // SMTP port numbers. + assert_equal_host_entries(getHostEntry(SMTP, STARTTLS, UNKNOWN), [ + SMTP, + STARTTLS, + 587, + ]); + assert_equal_host_entries(getHostEntry(SMTP, SSL, UNKNOWN), [SMTP, SSL, 465]); + assert_equal_host_entries(getHostEntry(SMTP, NONE, UNKNOWN), [ + SMTP, + NONE, + 587, + ]); +} + +/** + * Test the getIncomingTryOrder method. + */ +function test_getIncomingTryOrder() { + // The list of protocol+ssl+port configurations should match + // getIncomingTryOrder() in guessConfig.js. + + // protocol == POP || host starts with pop. || host starts with pop3. + checkPop("example.com", POP); + checkPop("pop.example.com", UNKNOWN); + checkPop("pop3.example.com", UNKNOWN); + checkPop("imap.example.com", POP); + + // protocol == IMAP || host starts with imap. + checkImap("example.com", IMAP); + checkImap("imap.example.com", UNKNOWN); + checkImap("pop.example.com", IMAP); + + let domain = "example.com"; + let protocol = UNKNOWN; + let ssl = UNKNOWN; + let port = UNKNOWN; + let tryOrder = getIncomingTryOrder(domain, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [IMAP, STARTTLS, 143], + [IMAP, SSL, 993], + [POP, STARTTLS, 110], + [POP, SSL, 995], + [IMAP, NONE, 143], + [POP, NONE, 110], + ]); + + ssl = SSL; + tryOrder = getIncomingTryOrder(domain, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [IMAP, SSL, 993], + [POP, SSL, 995], + ]); + + ssl = UNKNOWN; + port = 31337; + tryOrder = getIncomingTryOrder(domain, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [IMAP, STARTTLS, port], + [IMAP, SSL, port], + [POP, STARTTLS, port], + [POP, SSL, port], + [IMAP, NONE, port], + [POP, NONE, port], + ]); + + ssl = SSL; + tryOrder = getIncomingTryOrder(domain, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [IMAP, SSL, port], + [POP, SSL, port], + ]); +} + +/** + * Test the getOutgoingTryOrder method. + */ +function test_getOutgoingTryOrder() { + // The list of protocol+ssl+port configurations should match + // getOutgoingTryOrder() in guessConfig.js. + let domain = "example.com"; + let protocol = SMTP; + let ssl = UNKNOWN; + let port = UNKNOWN; + let tryOrder = getOutgoingTryOrder(domain, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [SMTP, STARTTLS, 587], + [SMTP, STARTTLS, 25], + [SMTP, SSL, 465], + [SMTP, NONE, 587], + [SMTP, NONE, 25], + ]); + + ssl = SSL; + tryOrder = getOutgoingTryOrder(domain, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[SMTP, SSL, 465]]); + + ssl = UNKNOWN; + port = 31337; + tryOrder = getOutgoingTryOrder(domain, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [ + [SMTP, STARTTLS, port], + [SMTP, SSL, port], + [SMTP, NONE, port], + ]); + + ssl = SSL; + tryOrder = getOutgoingTryOrder(domain, protocol, ssl, port); + assert_equal_try_orders(tryOrder, [[SMTP, SSL, port]]); +} + +function run_test() { + test_getHostEntry(); + test_getIncomingTryOrder(); + test_getOutgoingTryOrder(); +} diff --git a/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigXML.js b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigXML.js new file mode 100644 index 0000000000..919b0ffc2f --- /dev/null +++ b/comm/mail/components/accountcreation/test/xpcshell/test_autoconfigXML.js @@ -0,0 +1,266 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Tests accountcreation/readFromXML.js , reading the XML files + * containing a mail configuration. + * + * To allow forwards-compatibility (add new stuff in the future without + * breaking old clients on the new files), we are now fairly tolerant when + * reading and allow fallback mechanisms. This test checks whether that works, + * and of course also whether we can read a normal config and get the proper + * values. + */ + +// Globals + +var { AccountConfig } = ChromeUtils.import( + "resource:///modules/accountcreation/AccountConfig.jsm" +); +var { readFromXML } = ChromeUtils.import( + "resource:///modules/accountcreation/readFromXML.jsm" +); + +var { JXON } = ChromeUtils.import("resource:///modules/JXON.jsm"); + +/* + * UTILITIES + */ + +function assert_equal(aA, aB, aWhy) { + if (aA != aB) { + do_throw(aWhy); + } + Assert.equal(aA, aB); +} + +/** + * Test that two config entries are the same. + */ +function assert_equal_config(aA, aB, field) { + assert_equal(aA, aB, "Configured " + field + " is incorrect."); +} + +/* + * TESTS + */ + +/** + * Test that the xml reader returns a proper config and + * is also forwards-compatible to new additions to the data format. + */ +function test_readFromXML_config1() { + var clientConfigXML = + "" + + '' + + "example.com" + + "example.net" + + "Example" + + "Example Mail" + + // 1. - protocol not supported + '' + + "badprotocol.example.com" + + "993" + + "SSL" + + "%EMAILLOCALPART%" + + "ssl-client-cert" + + "" + + // 2. - socket type not supported + '' + + "badsocket.example.com" + + "993" + + "key-from-DNSSEC" + + "%EMAILLOCALPART%" + + "password-cleartext" + + "" + + // 3. - first supported incoming server + '' + + "imapmail.example.com" + + "993" + + "SSL" + + "%EMAILLOCALPART%" + + "password-cleartext" + + "" + + // 4. - auth method not supported + '' + + "badauth.example.com" + + "993" + + "SSL" + + "%EMAILLOCALPART%" + + "ssl-client-cert" + + // Throw in some elements we don"t support yet + "" + + '' + + '' + + "" + + "" + + // 5. - second supported incoming server + '' + + "popmail.example.com" + + // alternative hostname, not yet supported, should be ignored + "popbackup.example.com" + + "110" + + "7878" + + // unsupported socket type + "GSSAPI2" + + // but fall back + "plain" + + "%EMAILLOCALPART%" + + "%EMAILADDRESS%" + + // unsupported auth method + "GSSAPI2" + + // but fall back + "password-encrypted" + + "" + + "true" + + "999" + + "" + + "" + + // outgoing server with invalid auth method + '' + + "badauth.example.com" + + "587" + + "STARTTLS" + + "%EMAILADDRESS%" + + "smtp-after-imap" + + "" + + // outgoing server - supported + '' + + "smtpout.example.com" + + "smtpfallback.example.com" + + "587" + + "7878" + + "GSSAPI2" + + "STARTTLS" + + "%EMAILADDRESS%" + + "%EMAILLOCALPART%" + + "GSSAPI2" + + "client-IP-address" + + "" + + "" + + // Throw in some more elements we don"t support yet + '' + + '' + + "" + + ""; + + var domParser = new DOMParser(); + var config = readFromXML( + JXON.build(domParser.parseFromString(clientConfigXML, "text/xml")) + ); + + Assert.equal(config instanceof AccountConfig, true); + Assert.equal("example.com", config.id); + Assert.equal("Example", config.displayName); + Assert.notEqual(-1, config.domains.indexOf("example.com")); + // 1. incoming server skipped because of an unsupported protocol + // 2. incoming server skipped because of an so-far unknown auth method + // 3. incoming server is fine for us: IMAP, SSL, cleartext password + let server = config.incoming; + Assert.equal("imapmail.example.com", server.hostname); + Assert.equal("imap", server.type); + Assert.equal(Ci.nsMsgSocketType.SSL, server.socketType); + Assert.equal(3, server.auth); // cleartext password + // only one more supported incoming server + Assert.equal(1, config.incomingAlternatives.length); + // 4. incoming server skipped because of an so-far unknown socketType + // 5. server: POP + server = config.incomingAlternatives[0]; + Assert.equal("popmail.example.com", server.hostname); + Assert.equal("pop3", server.type); + Assert.equal(Ci.nsMsgSocketType.plain, server.socketType); + Assert.equal(4, server.auth); // encrypted password + + // SMTP server, most preferred + server = config.outgoing; + Assert.equal("smtpout.example.com", server.hostname); + Assert.equal("smtp", server.type); + Assert.equal(Ci.nsMsgSocketType.alwaysSTARTTLS, server.socketType); + Assert.equal(1, server.auth); // no auth + // no other SMTP servers + Assert.equal(0, config.outgoingAlternatives.length); +} + +/** + * Test the replaceVariables method. + */ +function test_replaceVariables() { + var clientConfigXML = + "" + + '' + + "example.com" + + "example.com" + + "example.com" + + '' + + "pop.%EMAILDOMAIN%" + + "995" + + "SSL" + + "%EMAILLOCALPART%" + + "plain" + + "" + + "true" + + "999" + + "" + + "" + + '' + + "smtp.example.com" + + "587" + + "STARTTLS" + + "%EMAILADDRESS%" + + "plain" + + "true" + + "false" + + "" + + "" + + ""; + + var domParser = new DOMParser(); + var config = readFromXML( + JXON.build(domParser.parseFromString(clientConfigXML, "text/xml")) + ); + + AccountConfig.replaceVariables( + config, + "Yamato Nadeshiko", + "yamato.nadeshiko@example.com", + "abc12345" + ); + + assert_equal_config( + config.incoming.username, + "yamato.nadeshiko", + "incoming server username" + ); + assert_equal_config( + config.outgoing.username, + "yamato.nadeshiko@example.com", + "outgoing server username" + ); + assert_equal_config( + config.incoming.hostname, + "pop.example.com", + "incoming server hostname" + ); + assert_equal_config( + config.outgoing.hostname, + "smtp.example.com", + "outgoing server hostname" + ); + assert_equal_config( + config.identity.realname, + "Yamato Nadeshiko", + "user real name" + ); + assert_equal_config( + config.identity.emailAddress, + "yamato.nadeshiko@example.com", + "user email address" + ); +} + +function run_test() { + test_readFromXML_config1(); + test_replaceVariables(); +} diff --git a/comm/mail/components/accountcreation/test/xpcshell/xpcshell.ini b/comm/mail/components/accountcreation/test/xpcshell/xpcshell.ini new file mode 100644 index 0000000000..8d42ab2145 --- /dev/null +++ b/comm/mail/components/accountcreation/test/xpcshell/xpcshell.ini @@ -0,0 +1,9 @@ +[DEFAULT] +head = +tail = +support-files = data/* + +[test_autoconfigFetchDisk.js] +skip-if = os == 'win' && msix # MSIX cannot write to application directory +[test_autoconfigUtils.js] +[test_autoconfigXML.js] diff --git a/comm/mail/components/accountcreation/views/container.mjs b/comm/mail/components/accountcreation/views/container.mjs new file mode 100644 index 0000000000..3bf8d9b4dc --- /dev/null +++ b/comm/mail/components/accountcreation/views/container.mjs @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Custom Element containing the main account hub dialog. Used to append the + * needed CSS files to the shadowDom to prevent style leakage. + * NOTE: This could directly extend an HTMLDialogElement if it had a shadowRoot. + */ +class AccountHubContainer extends HTMLElement { + /** @type {HTMLDialogElement} */ + modal; + + /** @type {DOMLocalization} */ + l10n; + + connectedCallback() { + if (this.shadowRoot) { + // Already connected, no need to run it again. + return; + } + + const shadowRoot = this.attachShadow({ mode: "open" }); + + // Load styles in the shadowRoot so we don't leak it. + let style = document.createElement("link"); + style.rel = "stylesheet"; + style.href = "chrome://messenger/skin/accountHub.css"; + shadowRoot.appendChild(style); + + let template = document.getElementById("accountHubDialog"); + let clonedNode = template.content.cloneNode(true); + shadowRoot.appendChild(clonedNode); + this.modal = shadowRoot.querySelector("dialog"); + + // We need to create an internal DOM localization in order to let fluent + // see the IDs inside our shadowRoot. + this.l10n = new DOMLocalization([ + "branding/brand.ftl", + "messenger/accountcreation/accountHub.ftl", + "messenger/accountcreation/accountSetup.ftl", + ]); + this.l10n.connectRoot(shadowRoot); + } + + disconnectedCallback() { + this.l10n.disconnectRoot(this.shadowRoot); + } +} +customElements.define("account-hub-container", AccountHubContainer); diff --git a/comm/mail/components/accountcreation/views/email.mjs b/comm/mail/components/accountcreation/views/email.mjs new file mode 100644 index 0000000000..f5a4d628b7 --- /dev/null +++ b/comm/mail/components/accountcreation/views/email.mjs @@ -0,0 +1,185 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +class AccountHubEmail extends HTMLElement { + /** + * The email setup form. + * + * @type {HTMLFormElement} + */ + #form; + + /** + * The account name field. + * + * @type {HTMLInputElement} + */ + #realName; + + /** + * The email field. + * + * @type {HTMLInputElement} + */ + #email; + + /** + * The password field. + * + * @type {HTMLInputElement} + */ + #password; + + /** + * The password visibility button. + * + * @type {HTMLButtonElement} + */ + #passwordToggleButton; + + /** + * The submit form button. + * + * @type {HTMLButtonElement} + */ + #continueButton; + + /** + * The domain name extrapolated from the email address. + * + * @type {string} + */ + #domain = ""; + + connectedCallback() { + if (this.hasConnected) { + return; + } + this.hasConnected = true; + + this.classList.add("account-hub-view"); + + let template = document.getElementById("accountHubEmailSetup"); + this.appendChild(template.content.cloneNode(true)); + + this.#form = this.querySelector("form"); + this.#realName = this.querySelector("#realName"); + this.#email = this.querySelector("#email"); + this.#password = this.querySelector("#password"); + this.#passwordToggleButton = this.querySelector("#passwordToggleButton"); + this.#continueButton = this.querySelector("#emailContinueButton"); + + this.initUI(); + + this.setupEventListeners(); + } + + /** + * Initialize the UI of the email setup flow. + */ + initUI() { + // Populate the account name if we can get some user info. + if ("@mozilla.org/userinfo;1" in Cc) { + let userInfo = Cc["@mozilla.org/userinfo;1"].getService(Ci.nsIUserInfo); + this.#realName.value = userInfo.fullname; + } + + this.#realName.focus(); + } + + /** + * Set up the event listeners for this workflow only once. + */ + setupEventListeners() { + this.#form.addEventListener("submit", event => { + event.preventDefault(); + event.stopPropagation(); + console.log("submit"); + }); + + this.#realName.addEventListener("input", () => this.#checkValidForm()); + this.#email.addEventListener("input", () => this.#checkValidForm()); + this.#password.addEventListener("input", () => this.#onPasswordInput()); + + this.#passwordToggleButton.addEventListener("click", event => { + this.#togglePasswordInput( + event.target.getAttribute("aria-pressed") === "false" + ); + }); + + // Set the Cancel/Back button. + this.querySelector("#emailGoBackButton").addEventListener("click", () => { + // If in first view, go back to start, otherwise go back in the flow. + this.dispatchEvent( + new CustomEvent("open-view", { + bubbles: true, + composed: true, + detail: { type: "START" }, + }) + ); + }); + } + + /** + * Check whether the user entered the minimum amount of information needed to + * leave the first view and is allowed to proceed to the detection step. + */ + #checkValidForm() { + const isValidForm = + this.#email.checkValidity() && this.#realName.checkValidity(); + this.#domain = isValidForm + ? this.#email.value.split("@")[1].toLowerCase() + : ""; + + this.#continueButton.disabled = !isValidForm; + } + + /** + * Handle the password visibility toggle on password input. + */ + #onPasswordInput() { + if (!this.#password.value) { + this.#togglePasswordInput(false); + } + } + + /** + * Toggle the password field type between `password` and `text` to allow users + * reading their typed password. + * + * @param {boolean} show - If the password field should become a text field. + */ + #togglePasswordInput(show) { + this.#password.type = show ? "text" : "password"; + this.#passwordToggleButton.setAttribute("aria-pressed", show.toString()); + document.l10n.setAttributes( + this.#passwordToggleButton, + show + ? "account-setup-password-toggle-hide" + : "account-setup-password-toggle-show" + ); + } + + /** + * Check if any operation is currently in process and return true only if we + * can leave this view. + * + * @returns {boolean} - If the account hub can remove this view. + */ + reset() { + // TODO + // Check for: + // - Non-abortable operations (autoconfig, email account setup, etc) + + this.#form.reset(); + this.#togglePasswordInput(false); + // TODO + // Before resetting we need to: + // - Clean up the fields. + // - Reset the autoconfig (cached server info). + // - Reset the view to the initial screen. + return true; + } +} +customElements.define("account-hub-email", AccountHubEmail); diff --git a/comm/mail/components/accountcreation/views/start.mjs b/comm/mail/components/accountcreation/views/start.mjs new file mode 100644 index 0000000000..66487cc928 --- /dev/null +++ b/comm/mail/components/accountcreation/views/start.mjs @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global gSync */ + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { UIState } = ChromeUtils.importESModule( + "resource://services-sync/UIState.sys.mjs" +); + +class AccountHubStart extends HTMLElement { + #accounts = [ + { + id: "email", + l10n: "account-hub-email-setup-button", + type: "MAIL", + }, + { + id: "calendar", + l10n: "account-hub-calendar-setup-button", + type: "CALENDAR", + }, + { + id: "addressBook", + l10n: "account-hub-address-book-setup-button", + type: "ADDRESS_BOOK", + }, + { + id: "chat", + l10n: "account-hub-chat-setup-button", + type: "CHAT", + }, + { + id: "feed", + l10n: "account-hub-feed-setup-button", + type: "FEED", + }, + { + id: "newsgroup", + l10n: "account-hub-newsgroup-setup-button", + type: "NNTP", + }, + // TODO: Import/Export of profiles is kinda broken so we don't want to + // expose it so much for now. + // { + // id: "import", + // l10n: "account-hub-import-setup-button", + // type: "IMPORT", + // }, + ]; + + connectedCallback() { + if (this.hasConnected) { + return; + } + this.hasConnected = true; + + this.classList.add("account-hub-view"); + + let template = document.getElementById("accountHubStart"); + this.appendChild(template.content.cloneNode(true)); + + this.initUI(); + + this.setupAccountFlows(); + } + + /** + * Update the UI to reflect reality whenever this view is triggered. + */ + initUI() { + const hasAccounts = MailServices.accounts.accounts.length; + this.querySelector("#welcomeHeader").hidden = hasAccounts; + this.querySelector("#defaultHeader").hidden = !hasAccounts; + + if (AppConstants.NIGHTLY_BUILD) { + this.updateFxAButton(); + } + + // Hide the release notes link for nightly builds since we don't have any. + if (AppConstants.NIGHTLY_BUILD) { + this.querySelector("#hubReleaseNotes").closest("li").hidden = true; + return; + } + + if ( + Services.prefs.getPrefType("app.releaseNotesURL") != + Services.prefs.PREF_INVALID + ) { + let relNotesURL = Services.urlFormatter.formatURLPref( + "app.releaseNotesURL" + ); + if (relNotesURL != "about:blank") { + this.querySelector("#hubReleaseNotes").href = relNotesURL; + return; + } + // Hide the release notes link if we don't have a URL to add. + this.querySelector("#hubReleaseNotes").closest("li").hidden = true; + } + } + + /** + * Populate the main container fo the start view with all the available + * account creation flows. + */ + setupAccountFlows() { + const fragment = new DocumentFragment(); + for (const account of this.#accounts) { + const button = document.createElement("button"); + button.id = `${account.id}Button`; + button.classList.add("button", "button-account"); + document.l10n.setAttributes(button, account.l10n); + button.addEventListener("click", () => { + this.dispatchEvent( + new CustomEvent("open-view", { + bubbles: true, + composed: true, + detail: { + type: account.type, + }, + }) + ); + }); + fragment.append(button); + } + this.querySelector(".hub-body-grid").replaceChildren(fragment); + + if (AppConstants.NIGHTLY_BUILD) { + this.querySelector("#hubSyncButton").addEventListener("click", () => { + // FIXME: Open this in a dialog or browser inside the modal, or find a + // way to close the account hub without an account and open it again in + // case the FxA login fails to set up accounts. + gSync.initFxA(); + }); + } + } + + /** + * Set up the Firefox Sync button. + */ + updateFxAButton() { + const state = UIState.get(); + this.querySelector("#hubSyncButton").hidden = + state.status == UIState.STATUS_SIGNED_IN; + } + + /** + * The start view doesn't have any abortable operation that needs to be + * checked, so we always return true. + * + * @returns {boolean} - Always true. + */ + reset() { + return true; + } +} +customElements.define("account-hub-start", AccountHubStart); diff --git a/comm/mail/components/activity/Activity.jsm b/comm/mail/components/activity/Activity.jsm new file mode 100644 index 0000000000..1b23efe1c7 --- /dev/null +++ b/comm/mail/components/activity/Activity.jsm @@ -0,0 +1,322 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["ActivityProcess", "ActivityEvent", "ActivityWarning"]; + +// Base class for ActivityProcess and ActivityEvent objects + +function Activity() { + this._initLogging(); + this._listeners = []; + this._subjects = []; +} + +Activity.prototype = { + id: -1, + bindingName: "", + iconClass: "", + groupingStyle: Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT, + facet: "", + displayText: "", + initiator: null, + contextType: "", + context: "", + contextObj: null, + + _initLogging() { + this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + }); + }, + + addListener(aListener) { + this._listeners.push(aListener); + }, + + removeListener(aListener) { + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] == aListener) { + this._listeners.splice(i, 1); + break; + } + } + }, + + addSubject(aSubject) { + this._subjects.push(aSubject); + }, + + getSubjects() { + return this._subjects.slice(); + }, +}; + +function ActivityProcess() { + Activity.call(this); + this.bindingName = "activity-process-item"; + this.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; +} + +ActivityProcess.prototype = { + __proto__: Activity.prototype, + + percentComplete: -1, + lastStatusText: "", + workUnitComplete: 0, + totalWorkUnits: 0, + startTime: Date.now(), + _cancelHandler: null, + _pauseHandler: null, + _retryHandler: null, + _state: Ci.nsIActivityProcess.STATE_INPROGRESS, + + init(aDisplayText, aInitiator) { + this.displayText = aDisplayText; + this.initiator = aInitiator; + }, + + get state() { + return this._state; + }, + + set state(val) { + if (val == this._state) { + return; + } + + // test validity of the new state + // + if ( + this._state == Ci.nsIActivityProcess.STATE_INPROGRESS && + !( + val == Ci.nsIActivityProcess.STATE_COMPLETED || + val == Ci.nsIActivityProcess.STATE_CANCELED || + val == Ci.nsIActivityProcess.STATE_WAITINGFORRETRY || + val == Ci.nsIActivityProcess.STATE_WAITINGFORINPUT || + val == Ci.nsIActivityProcess.STATE_PAUSED + ) + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + // we cannot change the state after the activity is completed, + // or it is canceled. + if ( + this._state == Ci.nsIActivityProcess.STATE_COMPLETED || + this._state == Ci.nsIActivityProcess.STATE_CANCELED + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + if ( + this._state == Ci.nsIActivityProcess.STATE_PAUSED && + !( + val == Ci.nsIActivityProcess.STATE_COMPLETED || + val == Ci.nsIActivityProcess.STATE_INPROGRESS || + val == Ci.nsIActivityProcess.STATE_WAITINGFORRETRY || + val == Ci.nsIActivityProcess.STATE_WAITINGFORINPUT || + val == Ci.nsIActivityProcess.STATE_CANCELED + ) + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + if ( + this._state == Ci.nsIActivityProcess.STATE_WAITINGFORINPUT && + !( + val == Ci.nsIActivityProcess.STATE_INPROGRESS || + val == Ci.nsIActivityProcess.STATE_CANCELED + ) + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + if ( + this._state == Ci.nsIActivityProcess.STATE_WAITINGFORRETRY && + !( + val == Ci.nsIActivityProcess.STATE_INPROGRESS || + val == Ci.nsIActivityProcess.STATE_CANCELED + ) + ) { + throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE); + } + + let oldState = this._state; + this._state = val; + + // let the listeners know about the change + this.log.debug("Notifying onStateChanged listeners"); + for (let value of this._listeners) { + try { + value.onStateChanged(this, oldState); + } catch (e) { + this.log.error("Exception thrown by onStateChanged listener: " + e); + } + } + }, + + setProgress(aStatusText, aWorkUnitsComplete, aTotalWorkUnits) { + if (aTotalWorkUnits == 0) { + this.percentComplete = -1; + this.workUnitComplete = 0; + this.totalWorkUnits = 0; + } else { + this.percentComplete = parseInt( + (100.0 * aWorkUnitsComplete) / aTotalWorkUnits + ); + this.workUnitComplete = aWorkUnitsComplete; + this.totalWorkUnits = aTotalWorkUnits; + } + this.lastStatusText = aStatusText; + + // notify listeners + for (let value of this._listeners) { + try { + value.onProgressChanged( + this, + aStatusText, + aWorkUnitsComplete, + aTotalWorkUnits + ); + } catch (e) { + this.log.error("Exception thrown by onProgressChanged listener: " + e); + } + } + }, + + get cancelHandler() { + return this._cancelHandler; + }, + + set cancelHandler(val) { + this._cancelHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + try { + value.onHandlerChanged(this); + } catch (e) { + this.log.error("Exception thrown by onHandlerChanged listener: " + e); + } + } + }, + + get pauseHandler() { + return this._pauseHandler; + }, + + set pauseHandler(val) { + this._pauseHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + value.onHandlerChanged(this); + } + }, + + get retryHandler() { + return this._retryHandler; + }, + + set retryHandler(val) { + this._retryHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + value.onHandlerChanged(this); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityProcess", "nsIActivity"]), +}; + +function ActivityEvent() { + Activity.call(this); + this.bindingName = "activity-event-item"; + this.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE; +} + +ActivityEvent.prototype = { + __proto__: Activity.prototype, + + statusText: "", + startTime: 0, + completionTime: 0, + _undoHandler: null, + + init(aDisplayText, aInitiator, aStatusText, aStartTime, aCompletionTime) { + this.displayText = aDisplayText; + this.statusText = aStatusText; + this.startTime = aStartTime; + if (aCompletionTime) { + this.completionTime = aCompletionTime; + } else { + this.completionTime = Date.now(); + } + this.initiator = aInitiator; + this._completionTime = aCompletionTime; + }, + + get undoHandler() { + return this._undoHandler; + }, + + set undoHandler(val) { + this._undoHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + value.onHandlerChanged(this); + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityEvent", "nsIActivity"]), +}; + +function ActivityWarning() { + Activity.call(this); + this.bindingName = "activity-warning-item"; + this.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; +} + +ActivityWarning.prototype = { + __proto__: Activity.prototype, + + recoveryTipText: "", + _time: 0, + _recoveryHandler: null, + + init(aWarningText, aInitiator, aRecoveryTipText) { + this.displayText = aWarningText; + this.initiator = aInitiator; + this.recoveryTipText = aRecoveryTipText; + this._time = Date.now(); + }, + + get recoveryHandler() { + return this._recoveryHandler; + }, + + set recoveryHandler(val) { + this._recoveryHandler = val; + + // let the listeners know about the change + this.log.debug("Notifying onHandlerChanged listeners"); + for (let value of this._listeners) { + value.onHandlerChanged(this); + } + }, + + get time() { + return this._time; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityWarning", "nsIActivity"]), +}; diff --git a/comm/mail/components/activity/ActivityManager.jsm b/comm/mail/components/activity/ActivityManager.jsm new file mode 100644 index 0000000000..c808cb3354 --- /dev/null +++ b/comm/mail/components/activity/ActivityManager.jsm @@ -0,0 +1,157 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["ActivityManager"]; + +function ActivityManager() {} + +ActivityManager.prototype = { + log: console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + }), + _listeners: [], + _processCount: 0, + _db: null, + _idCounter: 1, + _activities: new Map(), + + get processCount() { + let count = 0; + for (let value of this._activities.values()) { + if (value instanceof Ci.nsIActivityProcess) { + count++; + } + } + + return count; + }, + + getProcessesByContext(aContextType, aContextObj) { + let list = []; + for (let activity of this._activities.values()) { + if ( + activity instanceof Ci.nsIActivityProcess && + activity.contextType == aContextType && + activity.contextObj == aContextObj + ) { + list.push(activity); + } + } + return list; + }, + + get db() { + return null; + }, + + get nextId() { + return this._idCounter++; + }, + + addActivity(aActivity) { + try { + this.log.info("adding Activity"); + // get the next valid id for this activity + let id = this.nextId; + aActivity.id = id; + + // add activity into the activities table + this._activities.set(id, aActivity); + // notify all the listeners + for (let value of this._listeners) { + try { + value.onAddedActivity(id, aActivity); + } catch (e) { + this.log.error("Exception calling onAddedActivity" + e); + } + } + return id; + } catch (e) { + // for some reason exceptions don't end up on the console if we don't + // explicitly log them. + this.log.error("Exception: " + e); + throw e; + } + }, + + removeActivity(aID) { + let activity = this.getActivity(aID); + if (!activity) { + return; // Nothing to remove. + } + + // make sure that the activity is not in-progress state + if ( + activity instanceof Ci.nsIActivityProcess && + activity.state == Ci.nsIActivityProcess.STATE_INPROGRESS + ) { + throw Components.Exception(`Activity in progress`, Cr.NS_ERROR_FAILURE); + } + + // remove the activity + this._activities.delete(aID); + + // notify all the listeners + for (let value of this._listeners) { + try { + value.onRemovedActivity(aID); + } catch (e) { + // ignore the exception + } + } + }, + + cleanUp() { + // Get the list of aIDs. + this.log.info("cleanUp\n"); + for (let [id, activity] of this._activities) { + if (activity instanceof Ci.nsIActivityProcess) { + // Note: The .state property will return undefined if you aren't in + // this if-instanceof block. + let state = activity.state; + if ( + state != Ci.nsIActivityProcess.STATE_INPROGRESS && + state != Ci.nsIActivityProcess.STATE_PAUSED && + state != Ci.nsIActivityProcess.STATE_WAITINGFORINPUT && + state != Ci.nsIActivityProcess.STATE_WAITINGFORRETRY + ) { + this.removeActivity(id); + } + } else { + this.removeActivity(id); + } + } + }, + + getActivity(aID) { + return this._activities.get(aID); + }, + + containsActivity(aID) { + return this._activities.has(aID); + }, + + getActivities() { + return [...this._activities.values()]; + }, + + addListener(aListener) { + this.log.info("addListener\n"); + this._listeners.push(aListener); + }, + + removeListener(aListener) { + this.log.info("removeListener\n"); + for (let i = 0; i < this._listeners.length; i++) { + if (this._listeners[i] == aListener) { + this._listeners.splice(i, 1); + } + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityManager"]), +}; diff --git a/comm/mail/components/activity/ActivityManagerUI.jsm b/comm/mail/components/activity/ActivityManagerUI.jsm new file mode 100644 index 0000000000..b36b9b5a72 --- /dev/null +++ b/comm/mail/components/activity/ActivityManagerUI.jsm @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["ActivityManagerUI"]; + +const ACTIVITY_MANAGER_URL = "chrome://messenger/content/activity.xhtml"; + +function ActivityManagerUI() {} + +ActivityManagerUI.prototype = { + show(aWindowContext, aID) { + // First we see if it is already visible + let window = this.recentWindow; + if (window) { + window.focus(); + return; + } + + let parent = null; + try { + if (aWindowContext) { + parent = aWindowContext.docShell.domWindow; + } + } catch (e) { + /* it's OK to not have a parent window */ + } + + Services.ww.openWindow( + parent, + ACTIVITY_MANAGER_URL, + "ActivityManager", + "chrome,dialog=no,resizable", + {} + ); + }, + + get visible() { + return null != this.recentWindow; + }, + + get recentWindow() { + return Services.wm.getMostRecentWindow("Activity:Manager"); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIActivityManagerUI"]), +}; diff --git a/comm/mail/components/activity/components.conf b/comm/mail/components/activity/components.conf new file mode 100644 index 0000000000..429fd62bb8 --- /dev/null +++ b/comm/mail/components/activity/components.conf @@ -0,0 +1,38 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + 'cid': '{B2C036A3-F7CE-401C-95EE-9C21505167FD}', + 'contract_ids': ['@mozilla.org/activity-process;1'], + 'jsm': 'resource:///modules/Activity.jsm', + 'constructor': 'ActivityProcess', + }, + { + 'cid': '{87AAEB20-89D9-4B95-9542-3BF72405CAB2}', + 'contract_ids': ['@mozilla.org/activity-event;1'], + 'jsm': 'resource:///modules/Activity.jsm', + 'constructor': 'ActivityEvent', + }, + { + 'cid': '{968BAC9E-798B-4952-B384-86B21B8CC71E}', + 'contract_ids': ['@mozilla.org/activity-warning;1'], + 'jsm': 'resource:///modules/Activity.jsm', + 'constructor': 'ActivityWarning', + }, + { + 'cid': '{8aa5972e-19cb-41cc-9696-645f8a8d1a06}', + 'contract_ids': ['@mozilla.org/activity-manager;1'], + 'jsm': 'resource:///modules/ActivityManager.jsm', + 'constructor': 'ActivityManager', + }, + { + 'cid': '{5fa5974e-09cb-40cc-9696-643f8a8d9a06}', + 'contract_ids': ['@mozilla.org/activity-manager-ui;1'], + 'jsm': 'resource:///modules/ActivityManagerUI.jsm', + 'constructor': 'ActivityManagerUI', + }, +] diff --git a/comm/mail/components/activity/content/activity-widgets.js b/comm/mail/components/activity/content/activity-widgets.js new file mode 100644 index 0000000000..44ee16bff8 --- /dev/null +++ b/comm/mail/components/activity/content/activity-widgets.js @@ -0,0 +1,384 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* global MozXULElement, activityManager */ + +// Wrap in a block to prevent leaking to window scope. +{ + const { makeFriendlyDateAgo } = ChromeUtils.import( + "resource:///modules/TemplateUtils.jsm" + ); + + let activityStrings = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + ); + + /** + * The ActivityItemBase widget is the base class for all the activity item. + * It initializes activity details: i.e. id, status, icon, name, progress, + * date etc. for the activity widgets. + * + * @abstract + * @augments HTMLLIElement + */ + class ActivityItemBase extends HTMLLIElement { + connectedCallback() { + if (!this.hasChildNodes()) { + // fetch the activity and set the base attributes + this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + }); + let actID = this.getAttribute("actID"); + this._activity = activityManager.getActivity(actID); + this._activity.QueryInterface(this.constructor.activityInterface); + + // Construct the children. + this.classList.add("activityitem"); + + let icon = document.createElement("img"); + icon.setAttribute( + "src", + this._activity.iconClass + ? `chrome://messenger/skin/icons/new/activity/${this._activity.iconClass}Icon.svg` + : this.constructor.defaultIconSrc + ); + icon.setAttribute("alt", ""); + this.appendChild(icon); + + let display = document.createElement("span"); + display.classList.add("displayText"); + this.appendChild(display); + + if (this.isEvent || this.isWarning) { + let time = document.createElement("time"); + time.classList.add("dateTime"); + this.appendChild(time); + } + + if (this.isProcess) { + let progress = document.createElement("progress"); + progress.setAttribute("value", "0"); + progress.setAttribute("max", "100"); + progress.classList.add("progressmeter"); + this.appendChild(progress); + } + + let statusText = document.createElement("span"); + statusText.setAttribute("role", "note"); + statusText.classList.add("statusText"); + this.appendChild(statusText); + } + // (Re-)Attach the listener. + this.attachToActivity(); + } + + disconnectedCallback() { + this.detachFromActivity(); + } + + get isProcess() { + return this.constructor.activityInterface == Ci.nsIActivityProcess; + } + + get isEvent() { + return this.constructor.activityInterface == Ci.nsIActivityEvent; + } + + get isWarning() { + return this.constructor.activityInterface == Ci.nsIActivityWarning; + } + + get isGroup() { + return false; + } + + get activity() { + return this._activity; + } + + detachFromActivity() { + if (this.activityListener) { + this._activity.removeListener(this.activityListener); + } + } + + attachToActivity() { + if (this.activityListener) { + this._activity.addListener(this.activityListener); + } + } + + static _dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "long", + timeStyle: "short", + }); + + /** + * The time the activity occurred. + * + * @type {number} - The time in milliseconds since the epoch. + */ + set dateTime(time) { + let element = this.querySelector(".dateTime"); + if (!element) { + return; + } + time = new Date(parseInt(time)); + + element.setAttribute("datetime", time.toISOString()); + element.textContent = makeFriendlyDateAgo(time); + element.setAttribute( + "title", + this.constructor._dateTimeFormatter.format(time) + ); + } + + /** + * The text that describes additional information to the user. + * + * @type {string} + */ + set statusText(val) { + this.querySelector(".statusText").textContent = val; + } + + get statusText() { + return this.querySelector(".statusText").textContent; + } + + /** + * The text that describes the activity to the user. + * + * @type {string} + */ + set displayText(val) { + this.querySelector(".displayText").textContent = val; + } + + get displayText() { + return this.querySelector(".displayText").textContent; + } + } + + /** + * The MozActivityEvent widget displays information about events (like + * deleting or moving the message): e.g image, name, date and description. + * It is typically used in Activity Manager window. + * + * @augments ActivityItemBase + */ + class ActivityEventItem extends ActivityItemBase { + static defaultIconSrc = + "chrome://messenger/skin/icons/new/activity/defaultEventIcon.svg"; + static activityInterface = Ci.nsIActivityEvent; + + connectedCallback() { + super.connectedCallback(); + this.setAttribute("is", "activity-event-item"); + + this.displayText = this.activity.displayText; + this.statusText = this.activity.statusText; + this.dateTime = this.activity.completionTime; + } + } + + customElements.define("activity-event-item", ActivityEventItem, { + extends: "li", + }); + + /** + * The ActivityGroupItem widget displays information about the activities of + * the group: e.g. name of the group, list of the activities with their name, + * progress and icon. It is shown in Activity Manager window. It gets removed + * when there is no activities from the group. + * + * @augments HTMLLIElement + */ + class ActivityGroupItem extends HTMLLIElement { + constructor() { + super(); + + let heading = document.createElement("h2"); + heading.classList.add("contextDisplayText"); + this.appendChild(heading); + + let list = document.createElement("ul"); + list.classList.add("activitygroup-list", "activityview"); + this.appendChild(list); + + this.classList.add("activitygroup"); + this.setAttribute("is", "activity-group-item"); + } + + /** + * The text heading for the group, as seen by the user. + * + * @type {string} + */ + set contextDisplayText(val) { + this.querySelector(".contextDisplayText").textContent = val; + } + + get contextDisplayText() { + return this.querySelctor(".contextDisplayText").textContent; + } + + get isGroup() { + return true; + } + } + + customElements.define("activity-group-item", ActivityGroupItem, { + extends: "li", + }); + + /** + * The ActivityProcessItem widget displays information about the internal + * process : e.g image, progress, name, date and description. + * It is typically used in Activity Manager window. + * + * @augments ActivityItemBase + */ + class ActivityProcessItem extends ActivityItemBase { + static defaultIconSrc = + "chrome://messenger/skin/icons/new/activity/deafultProcessIcon.svg"; + static activityInterface = Ci.nsIActivityProcess; + static textMap = { + paused: activityStrings.GetStringFromName("paused2"), + canceled: activityStrings.GetStringFromName("canceled"), + failed: activityStrings.GetStringFromName("failed"), + waitingforinput: activityStrings.GetStringFromName("waitingForInput"), + waitingforretry: activityStrings.GetStringFromName("waitingForRetry"), + }; + + constructor() { + super(); + + this.activityListener = { + onStateChanged: (activity, oldState) => { + // change the view of the element according to the new state + // default states for each item + let hideProgressMeter = false; + let statusText = this.statusText; + + switch (this.activity.state) { + case Ci.nsIActivityProcess.STATE_INPROGRESS: + statusText = ""; + break; + case Ci.nsIActivityProcess.STATE_COMPLETED: + hideProgressMeter = true; + statusText = ""; + break; + case Ci.nsIActivityProcess.STATE_CANCELED: + hideProgressMeter = true; + statusText = this.constructor.textMap.canceled; + break; + case Ci.nsIActivityProcess.STATE_PAUSED: + statusText = this.constructor.textMap.paused; + break; + case Ci.nsIActivityProcess.STATE_WAITINGFORINPUT: + statusText = this.constructor.textMap.waitingforinput; + break; + case Ci.nsIActivityProcess.STATE_WAITINGFORRETRY: + hideProgressMeter = true; + statusText = this.constructor.textMap.waitingforretry; + break; + } + + // Set the visibility + let meter = this.querySelector(".progressmeter"); + meter.hidden = hideProgressMeter; + + // Ensure progress meter not active when hidden + if (hideProgressMeter) { + meter.value = 0; + } + + // Update Status text and Display Text Areas + // In some states we need to modify Display Text area of + // the process (e.g. Failure). + this.statusText = statusText; + }, + onProgressChanged: ( + activity, + statusText, + workUnitsComplete, + totalWorkUnits + ) => { + let element = document.querySelector(".progressmeter"); + if (totalWorkUnits == 0) { + element.removeAttribute("value"); + } else { + let _percentComplete = (100.0 * workUnitsComplete) / totalWorkUnits; + element.value = _percentComplete; + } + this.statusText = statusText; + }, + }; + } + + connectedCallback() { + super.connectedCallback(); + this.setAttribute("is", "activity-process-item"); + + this.displayText = this.activity.displayText; + // make sure that custom element reflects the latest state of the process + this.activityListener.onStateChanged( + this.activity.state, + Ci.nsIActivityProcess.STATE_NOTSTARTED + ); + this.activityListener.onProgressChanged( + this.activity, + this.activity.lastStatusText, + this.activity.workUnitComplete, + this.activity.totalWorkUnits + ); + } + + get inProgress() { + return this.activity.state == Ci.nsIActivityProcess.STATE_INPROGRESS; + } + + get isRemovable() { + return ( + this.activity.state == Ci.nsIActivityProcess.STATE_COMPLETED || + this.activity.state == Ci.nsIActivityProcess.STATE_CANCELED + ); + } + } + + customElements.define("activity-process-item", ActivityProcessItem, { + extends: "li", + }); + + /** + * The ActivityWarningItem widget displays information about + * warnings : e.g image, name, date and description. + * It is typically used in Activity Manager window. + * + * @augments ActivityItemBase + */ + class ActivityWarningItem extends ActivityItemBase { + static defaultIconSrc = + "chrome://messenger/skin/icons/new/activity/warning.svg"; + static activityInterface = Ci.nsIActivityWarning; + + connectedCallback() { + super.connectedCallback(); + this.setAttribute("is", "activity-warning-item"); + + this.displayText = this.activity.displayText; + this.dateTime = this.activity.time; + this.statusText = this.activity.recoveryTipText; + } + } + + customElements.define("activity-warning-item", ActivityWarningItem, { + extends: "li", + }); +} diff --git a/comm/mail/components/activity/content/activity.js b/comm/mail/components/activity/content/activity.js new file mode 100644 index 0000000000..dcaba3d808 --- /dev/null +++ b/comm/mail/components/activity/content/activity.js @@ -0,0 +1,239 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const activityManager = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager +); + +var ACTIVITY_LIMIT = 250; + +var activityObject = { + _activityMgrListener: null, + _activitiesView: null, + _activityLogger: console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + }), + _ignoreNotifications: false, + _groupCache: new Map(), + + // Utility Functions for Activity element management + + /** + * Creates the proper element for the given activity + */ + createActivityWidget(type) { + let element = document.createElement("li", { + is: type.bindingName, + }); + + if (element) { + element.setAttribute("actID", type.id); + } + + return element; + }, + + /** + * Returns the activity group element that matches the context_type + * and context of the given activity, if any. + */ + getActivityGroupElementByContext(aContextType, aContextObj) { + return this._groupCache.get(aContextType + ":" + aContextObj); + }, + + /** + * Inserts the given element into the correct position on the + * activity manager window. + */ + placeActivityElement(element) { + if (element.isGroup || element.isProcess) { + this._activitiesView.insertBefore( + element, + this._activitiesView.firstElementChild + ); + } else { + let next = this._activitiesView.firstElementChild; + while (next && (next.isWarning || next.isProcess || next.isGroup)) { + next = next.nextElementSibling; + } + if (next) { + this._activitiesView.insertBefore(element, next); + } else { + this._activitiesView.appendChild(element); + } + } + if (element.isGroup) { + this._groupCache.set( + element.contextType + ":" + element.contextObj, + element + ); + } + while (this._activitiesView.children.length > ACTIVITY_LIMIT) { + this.removeActivityElement( + this._activitiesView.lastElementChild.getAttribute("actID") + ); + } + }, + + /** + * Adds a new element to activity manager window for the + * given activity. It is called by ActivityMgrListener when + * a new activity is added into the activity manager's internal + * list. + */ + addActivityElement(aID, aActivity) { + try { + this._activityLogger.info(`Adding ActivityElement: ${aID}, ${aActivity}`); + // get |groupingStyle| of the activity. Grouping style determines + // whether we show the activity standalone or grouped by context in + // the activity manager window. + let isGroupByContext = + aActivity.groupingStyle == Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; + + // find out if an activity group has already been created for this context + let group = null; + if (isGroupByContext) { + group = this.getActivityGroupElementByContext( + aActivity.contextType, + aActivity.contextObj + ); + // create a group if it's not already created. + if (!group) { + group = document.createElement("li", { + is: "activity-group-item", + }); + this._activityLogger.info("created group element"); + // Set the context type and object of the newly created group + group.contextType = aActivity.contextType; + group.contextObj = aActivity.contextObj; + group.contextDisplayText = aActivity.contextDisplayText; + + // add group into the list + this.placeActivityElement(group); + } + } + + // create the appropriate element for the activity + let actElement = this.createActivityWidget(aActivity); + this._activityLogger.info("created activity element"); + + if (group) { + // get the inner list element of the group + let groupView = group.querySelector(".activitygroup-list"); + groupView.appendChild(actElement); + } else { + this.placeActivityElement(actElement); + } + } catch (e) { + this._activityLogger.error("addActivityElement: " + e); + throw e; + } + }, + + /** + * Removes the activity element from the activity manager window. + * It is called by ActivityMgrListener when the activity in question + * is removed from the activity manager's internal list. + */ + removeActivityElement(aID) { + this._activityLogger.info("removing Activity ID: " + aID); + let item = this._activitiesView.querySelector(`[actID="${aID}"]`); + + if (item) { + let group = item.closest(".activitygroup"); + item.remove(); + if (group && !group.querySelector(".activityitem")) { + // Empty group is removed. + this._groupCache.delete(group.contextType + ":" + group.contextObj); + group.remove(); + } + } + }, + + // ----------------- + // Startup, Shutdown + + startup() { + try { + this._activitiesView = document.getElementById("activityView"); + + let activities = activityManager.getActivities(); + for ( + let iActivity = Math.max(0, activities.length - ACTIVITY_LIMIT); + iActivity < activities.length; + iActivity++ + ) { + let activity = activities[iActivity]; + this.addActivityElement(activity.id, activity); + } + + // start listening changes in the activity manager's + // internal list + this._activityMgrListener = new this.ActivityMgrListener(); + activityManager.addListener(this._activityMgrListener); + } catch (e) { + this._activityLogger.error("Exception: " + e); + } + }, + + rebuild() { + let activities = activityManager.getActivities(); + for (let activity of activities) { + this.addActivityElement(activity.id, activity); + } + }, + + shutdown() { + activityManager.removeListener(this._activityMgrListener); + }, + + // ----------------- + // Utility Functions + + /** + * Remove all activities not in-progress from the activity list. + */ + clearActivityList() { + this._activityLogger.debug("clearActivityList"); + + this._ignoreNotifications = true; + // If/when we implement search, we'll want to remove just the items + // that are on the search display, however for now, we'll just clear up + // everything. + activityManager.cleanUp(); + + while (this._activitiesView.lastChild) { + this._activitiesView.lastChild.remove(); + } + + this._groupCache.clear(); + this.rebuild(); + this._ignoreNotifications = false; + this._activitiesView.focus(); + }, +}; + +// An object to monitor nsActivityManager operations. This class acts as +// binding layer between nsActivityManager and nsActivityManagerUI objects. +activityObject.ActivityMgrListener = function () {}; +activityObject.ActivityMgrListener.prototype = { + onAddedActivity(aID, aActivity) { + activityObject._activityLogger.info(`added activity: ${aID} ${aActivity}`); + if (!activityObject._ignoreNotifications) { + activityObject.addActivityElement(aID, aActivity); + } + }, + + onRemovedActivity(aID) { + if (!activityObject._ignoreNotifications) { + activityObject.removeActivityElement(aID); + } + }, +}; + +window.addEventListener("load", () => activityObject.startup()); +window.addEventListener("unload", () => activityObject.shutdown()); diff --git a/comm/mail/components/activity/content/activity.xhtml b/comm/mail/components/activity/content/activity.xhtml new file mode 100644 index 0000000000..cdff19cbe6 --- /dev/null +++ b/comm/mail/components/activity/content/activity.xhtml @@ -0,0 +1,61 @@ + + + +#ifdef XP_UNIX +#ifndef XP_MACOSX +#define XP_GNOME 1 +#endif +#endif + + + + + + + + +%activityManagerDTD; +]> + + + + &activity.title; + + + + + + + +#ifdef XP_GNOME + +#else + +#endif + + + +
+
    + +
    + + diff --git a/comm/mail/components/activity/jar.mn b/comm/mail/components/activity/jar.mn new file mode 100644 index 0000000000..babeeac23d --- /dev/null +++ b/comm/mail/components/activity/jar.mn @@ -0,0 +1,8 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +messenger.jar: + content/messenger/activity.js (content/activity.js) + content/messenger/activity-widgets.js (content/activity-widgets.js) +* content/messenger/activity.xhtml (content/activity.xhtml) diff --git a/comm/mail/components/activity/modules/activityModules.jsm b/comm/mail/components/activity/modules/activityModules.jsm new file mode 100644 index 0000000000..945f7473c2 --- /dev/null +++ b/comm/mail/components/activity/modules/activityModules.jsm @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This module is designed to be a central place to initialise activity related +// modules. + +const EXPORTED_SYMBOLS = []; + +const { sendLaterModule } = ChromeUtils.import( + "resource:///modules/activity/sendLater.jsm" +); +sendLaterModule.init(); +const { moveCopyModule } = ChromeUtils.import( + "resource:///modules/activity/moveCopy.jsm" +); +moveCopyModule.init(); +const { glodaIndexerActivity } = ChromeUtils.import( + "resource:///modules/activity/glodaIndexer.jsm" +); +glodaIndexerActivity.init(); +const { autosyncModule } = ChromeUtils.import( + "resource:///modules/activity/autosync.jsm" +); +autosyncModule.init(); +const { alertHook } = ChromeUtils.import( + "resource:///modules/activity/alertHook.jsm" +); +alertHook.init(); +const { pop3DownloadModule } = ChromeUtils.import( + "resource:///modules/activity/pop3Download.jsm" +); +pop3DownloadModule.init(); diff --git a/comm/mail/components/activity/modules/alertHook.jsm b/comm/mail/components/activity/modules/alertHook.jsm new file mode 100644 index 0000000000..b3083aef0a --- /dev/null +++ b/comm/mail/components/activity/modules/alertHook.jsm @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["alertHook"]; + +var nsActWarning = Components.Constructor( + "@mozilla.org/activity-warning;1", + "nsIActivityWarning", + "init" +); + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +// This module provides a link between the send later service and the activity +// manager. +var alertHook = { + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get alertService() { + delete this.alertService; + return (this.alertService = Cc["@mozilla.org/alerts-service;1"].getService( + Ci.nsIAlertsService + )); + }, + + get brandShortName() { + delete this.brandShortName; + return (this.brandShortName = Services.strings + .createBundle("chrome://branding/locale/brand.properties") + .GetStringFromName("brandShortName")); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIMsgUserFeedbackListener"]), + + onAlert(aMessage, aUrl) { + // Create a new warning. + let warning = new nsActWarning(aMessage, this.activityMgr, ""); + + if (aUrl && aUrl.server && aUrl.server.prettyName) { + warning.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; + warning.contextType = "incomingServer"; + warning.contextDisplayText = aUrl.server.prettyName; + warning.contextObj = aUrl.server; + } else { + warning.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE; + } + + this.activityMgr.addActivity(warning); + + // If we have a message window in the url, then show a warning prompt, + // just like the modal code used to. Otherwise, don't. + try { + if (!aUrl || !aUrl.msgWindow) { + return true; + } + } catch (ex) { + // nsIMsgMailNewsUrl.msgWindow will throw on a null pointer, so that's + // what we're handling here. + if ( + ex instanceof Ci.nsIException && + ex.result == Cr.NS_ERROR_INVALID_POINTER + ) { + return true; + } + throw ex; + } + + try { + let alert = Cc["@mozilla.org/alert-notification;1"].createInstance( + Ci.nsIAlertNotification + ); + alert.init( + "", // name + "chrome://branding/content/icon48.png", + this.brandShortName, + aMessage + ); + this.alertService.showAlert(alert); + } catch (ex) { + // XXX On Linux, if libnotify isn't supported, showAlert + // can throw an error, so fall-back to the old method of modal dialogs. + return false; + } + + return true; + }, + + init() { + // We shouldn't need to remove the listener as we're not being held by + // anyone except by the send later instance. + MailServices.mailSession.addUserFeedbackListener(this); + }, +}; diff --git a/comm/mail/components/activity/modules/autosync.jsm b/comm/mail/components/activity/modules/autosync.jsm new file mode 100644 index 0000000000..c2483c4b53 --- /dev/null +++ b/comm/mail/components/activity/modules/autosync.jsm @@ -0,0 +1,433 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["autosyncModule"]; + +var nsActProcess = Components.Constructor( + "@mozilla.org/activity-process;1", + "nsIActivityProcess", + "init" +); +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); + +/** + * This code aims to mediate between the auto-sync code and the activity mgr. + * + * Not every auto-sync activity is directly mapped to a process or event. + * To prevent a possible event overflow, Auto-Sync monitor generates one + * sync'd event per account when after all its _pending_ folders are sync'd, + * rather than generating one event per folder sync. + */ + +var autosyncModule = { + _inQFolderList: [], + _running: false, + _syncInfoPerFolder: new Map(), + _syncInfoPerServer: new Map(), + _lastMessage: new Map(), + + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get autoSyncManager() { + delete this.autoSyncManager; + return (this.autoSyncManager = Cc[ + "@mozilla.org/imap/autosyncmgr;1" + ].getService(Ci.nsIAutoSyncManager)); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + getString(stringName) { + try { + return this.bundle.GetStringFromName(stringName); + } catch (e) { + this.log.error("error trying to get a string called: " + stringName); + throw e; + } + }, + + createSyncMailProcess(folder) { + try { + // create an activity process for this folder + let msg = this.bundle.formatStringFromName("autosyncProcessDisplayText", [ + folder.prettyName, + ]); + let process = new nsActProcess(msg, this.autoSyncManager); + // we want to use default auto-sync icon + process.iconClass = "syncMail"; + process.addSubject(folder); + // group processes under folder's imap account + process.contextType = "account"; + process.contextDisplayText = this.bundle.formatStringFromName( + "autosyncContextDisplayText", + [folder.server.prettyName] + ); + + process.contextObj = folder.server; + + return process; + } catch (e) { + this.log.error("createSyncMailProcess: " + e); + throw e; + } + }, + + createSyncMailEvent(syncItem) { + try { + // extract the relevant parts + let process = syncItem.activity; + let folder = syncItem.syncFolder; + + // create an activity event + + let msg = this.bundle.formatStringFromName("autosyncEventDisplayText", [ + folder.server.prettyName, + ]); + + let statusMsg; + let numOfMessages = this._syncInfoPerServer.get( + folder.server + ).totalDownloads; + if (numOfMessages) { + statusMsg = this.bundle.formatStringFromName( + "autosyncEventStatusText", + [numOfMessages] + ); + } else { + statusMsg = this.getString("autosyncEventStatusTextNoMsgs"); + } + + let event = new nsActEvent( + msg, + this.autoSyncManager, + statusMsg, + this._syncInfoPerServer.get(folder.server).startTime, + Date.now() + ); // completion time + + // since auto-sync events do not have undo option by nature, + // setting these values are informational only. + event.contextType = process.contextType; + event.contextDisplayText = this.bundle.formatStringFromName( + "autosyncContextDisplayText", + [folder.server.prettyName] + ); + event.contextObj = process.contextObj; + event.iconClass = "syncMail"; + + // transfer all subjects. + // same as above, not mandatory + let subjects = process.getSubjects(); + for (let subject of subjects) { + event.addSubject(subject); + } + + return event; + } catch (e) { + this.log.error("createSyncMailEvent: " + e); + throw e; + } + }, + + onStateChanged(running) { + try { + this._running = running; + this.log.info( + "OnStatusChanged: " + (running ? "running" : "sleeping") + "\n" + ); + } catch (e) { + this.log.error("onStateChanged: " + e); + throw e; + } + }, + + onFolderAddedIntoQ(queue, folder) { + try { + if ( + folder instanceof Ci.nsIMsgFolder && + queue == Ci.nsIAutoSyncMgrListener.PriorityQueue + ) { + this._inQFolderList.push(folder); + this.log.info( + "Auto_Sync OnFolderAddedIntoQ [" + + this._inQFolderList.length + + "] " + + folder.prettyName + + " of " + + folder.server.prettyName + ); + // create an activity process for this folder + let process = this.createSyncMailProcess(folder); + + // create a sync object to keep track of the process of this folder + let imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder); + let syncItem = { + syncFolder: folder, + activity: process, + percentComplete: 0, + totalDownloaded: 0, + pendingMsgCount: imapFolder.autoSyncStateObj.pendingMessageCount, + }; + + // if this is the first folder of this server in the queue, then set the sync start time + // for activity event + if (!this._syncInfoPerServer.has(folder.server)) { + this._syncInfoPerServer.set(folder.server, { + startTime: Date.now(), + totalDownloads: 0, + }); + } + + // associate the sync object with the folder in question + // use folder.URI as key + this._syncInfoPerFolder.set(folder.URI, syncItem); + } + } catch (e) { + this.log.error("onFolderAddedIntoQ: " + e); + throw e; + } + }, + onFolderRemovedFromQ(queue, folder) { + try { + if ( + folder instanceof Ci.nsIMsgFolder && + queue == Ci.nsIAutoSyncMgrListener.PriorityQueue + ) { + let i = this._inQFolderList.indexOf(folder); + if (i > -1) { + this._inQFolderList.splice(i, 1); + } + + this.log.info( + "OnFolderRemovedFromQ [" + + this._inQFolderList.length + + "] " + + folder.prettyName + + " of " + + folder.server.prettyName + + "\n" + ); + + let syncItem = this._syncInfoPerFolder.get(folder.URI); + let process = syncItem.activity; + let canceled = false; + if (process instanceof Ci.nsIActivityProcess) { + canceled = process.state == Ci.nsIActivityProcess.STATE_CANCELED; + process.state = Ci.nsIActivityProcess.STATE_COMPLETED; + + try { + this.activityMgr.removeActivity(process.id); + } catch (e) { + // It is OK to end up here; If the folder is queued and the + // message get manually downloaded by the user, we might get + // a folder removed notification even before a download + // started for this folder. This behavior stems from the fact + // that we add activities into the activity manager in + // onDownloadStarted notification rather than onFolderAddedIntoQ. + // This is an expected side effect. + // Log a warning, but do not throw an error. + this.log.warn("onFolderRemovedFromQ: " + e); + } + + // remove the folder/syncItem association from the table + this._syncInfoPerFolder.delete(folder.URI); + } + + // if this is the last folder of this server in the queue + // create a sync event and clean the sync start time + let found = false; + for (let value of this._syncInfoPerFolder.values()) { + if (value.syncFolder.server == folder.server) { + found = true; + break; + } + } + this.log.info( + "Auto_Sync OnFolderRemovedFromQ Last folder of the server: " + !found + ); + if (!found) { + // create an sync event for the completed process if it's not canceled + if (!canceled) { + let key = folder.server.prettyName; + if ( + this._lastMessage.has(key) && + this.activityMgr.containsActivity(this._lastMessage.get(key)) + ) { + this.activityMgr.removeActivity(this._lastMessage.get(key)); + } + this._lastMessage.set( + key, + this.activityMgr.addActivity(this.createSyncMailEvent(syncItem)) + ); + } + this._syncInfoPerServer.delete(folder.server); + } + } + } catch (e) { + this.log.error("onFolderRemovedFromQ: " + e); + throw e; + } + }, + onDownloadStarted(folder, numOfMessages, totalPending) { + try { + if (folder instanceof Ci.nsIMsgFolder) { + this.log.info( + "OnDownloadStarted (" + + numOfMessages + + "/" + + totalPending + + "): " + + folder.prettyName + + " of " + + folder.server.prettyName + + "\n" + ); + + let syncItem = this._syncInfoPerFolder.get(folder.URI); + let process = syncItem.activity; + + // Update the totalPending number. if new messages have been discovered in the folder + // after we added the folder into the q, totalPending might be greater than what we have + // initially set + if (totalPending > syncItem.pendingMsgCount) { + syncItem.pendingMsgCount = totalPending; + } + + if (process instanceof Ci.nsIActivityProcess) { + // if the process has not beed added to activity manager already, add now + if (!this.activityMgr.containsActivity(process.id)) { + this.log.info( + "Auto_Sync OnDownloadStarted: No process, adding a new process" + ); + this.activityMgr.addActivity(process); + } + + syncItem.totalDownloaded += numOfMessages; + + process.state = Ci.nsIActivityProcess.STATE_INPROGRESS; + let percent = + (syncItem.totalDownloaded / syncItem.pendingMsgCount) * 100; + if (percent > syncItem.percentComplete) { + syncItem.percentComplete = percent; + } + + let msg = this.bundle.formatStringFromName( + "autosyncProcessProgress2", + [ + syncItem.totalDownloaded, + syncItem.pendingMsgCount, + folder.prettyName, + folder.server.prettyName, + ] + ); + + process.setProgress( + msg, + syncItem.totalDownloaded, + syncItem.pendingMsgCount + ); + + let serverInfo = this._syncInfoPerServer.get( + syncItem.syncFolder.server + ); + serverInfo.totalDownloads += numOfMessages; + this._syncInfoPerServer.set(syncItem.syncFolder.server, serverInfo); + } + } + } catch (e) { + this.log.error("onDownloadStarted: " + e); + throw e; + } + }, + + onDownloadCompleted(folder) { + try { + if (folder instanceof Ci.nsIMsgFolder) { + this.log.info( + "OnDownloadCompleted: " + + folder.prettyName + + " of " + + folder.server.prettyName + ); + + let process = this._syncInfoPerFolder.get(folder.URI).activity; + if (process instanceof Ci.nsIActivityProcess && !this._running) { + this.log.info( + "OnDownloadCompleted: Auto-Sync Manager is paused, pausing the process" + ); + process.state = Ci.nsIActivityProcess.STATE_PAUSED; + } + } + } catch (e) { + this.log.error("onDownloadCompleted: " + e); + throw e; + } + }, + + onDownloadError(folder) { + if (folder instanceof Ci.nsIMsgFolder) { + this.log.error( + "OnDownloadError: " + + folder.prettyName + + " of " + + folder.server.prettyName + + "\n" + ); + } + }, + + onDiscoveryQProcessed(folder, numOfHdrsProcessed, leftToProcess) { + this.log.info( + "onDiscoveryQProcessed: Processed " + + numOfHdrsProcessed + + "/" + + (leftToProcess + numOfHdrsProcessed) + + " of " + + folder.prettyName + + "\n" + ); + }, + + onAutoSyncInitiated(folder) { + this.log.info( + "onAutoSyncInitiated: " + + folder.prettyName + + " of " + + folder.server.prettyName + + " has been updated.\n" + ); + }, + + init() { + // XXX when do we need to remove ourselves? + this.log.info("initing"); + Cc["@mozilla.org/imap/autosyncmgr;1"] + .getService(Ci.nsIAutoSyncManager) + .addListener(this); + }, +}; diff --git a/comm/mail/components/activity/modules/glodaIndexer.jsm b/comm/mail/components/activity/modules/glodaIndexer.jsm new file mode 100644 index 0000000000..5307d5cefa --- /dev/null +++ b/comm/mail/components/activity/modules/glodaIndexer.jsm @@ -0,0 +1,251 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["glodaIndexerActivity"]; + +var nsActProcess = Components.Constructor( + "@mozilla.org/activity-process;1", + "nsIActivityProcess", + "init" +); +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + PluralForm: "resource://gre/modules/PluralForm.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + Gloda: "resource:///modules/gloda/GlodaPublic.jsm", + GlodaConstants: "resource:///modules/gloda/GlodaConstants.jsm", + GlodaIndexer: "resource:///modules/gloda/GlodaIndexer.jsm", +}); + +/** + * Gloda message indexer feedback. + */ +var glodaIndexerActivity = { + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + getString(stringName) { + try { + return this.bundle.GetStringFromName(stringName); + } catch (e) { + this.log.error("error trying to get a string called: " + stringName); + throw e; + } + }, + + init() { + // Register a listener with the Gloda indexer that receives notifications + // about Gloda indexing status. We wrap the listener in this function so we + // can set |this| to the GlodaIndexerActivity object inside the listener. + function listenerWrapper(...aArgs) { + glodaIndexerActivity.listener(...aArgs); + } + lazy.GlodaIndexer.addListener(listenerWrapper); + }, + + /** + * Information about the current job. An object with these properties: + * + * folder {String} + * the name of the folder being processed by the job + * jobNumber {Number} + * the index of the job in the list of jobs + * process {nsIActivityProcess} + * the activity process corresponding to the current job + * startTime {Date} + * the time at which we were first notified about the job + * totalItemNum {Number} + * the total number of messages being indexed in the job + * jobType {String} + * The IndexinbJob jobType (ex: "folder", "folderCompact") + */ + currentJob: null, + + listener(aStatus, aFolder, aJobNumber, aItemNumber, aTotalItemNum, aJobType) { + this.log.debug("Gloda Indexer Folder/Status: " + aFolder + "/" + aStatus); + this.log.debug("Gloda Indexer Job: " + aJobNumber); + this.log.debug("Gloda Indexer Item: " + aItemNumber + "/" + aTotalItemNum); + + if (aStatus == lazy.GlodaConstants.kIndexerIdle) { + if (this.currentJob) { + this.onJobCompleted(); + } + } else { + // If the job numbers have changed, the indexer has finished the job + // we were previously tracking, so convert the corresponding process + // into an event and start a new process to track the new job. + if (this.currentJob && aJobNumber != this.currentJob.jobNumber) { + this.onJobCompleted(); + } + + // If we aren't tracking a job, either this is the first time we've been + // called or the last job we were tracking was completed. Either way, + // start tracking the new job. + if (!this.currentJob) { + this.onJobBegun(aFolder, aJobNumber, aTotalItemNum, aJobType); + } + + // If there is only one item, don't bother creating a progress item. + if (aTotalItemNum != 1) { + this.onJobProgress(aFolder, aItemNumber, aTotalItemNum); + } + } + }, + + onJobBegun(aFolder, aJobNumber, aTotalItemNum, aJobType) { + let displayText = aFolder + ? this.getString("indexingFolder").replace("#1", aFolder) + : this.getString("indexing"); + let process = new nsActProcess(displayText, lazy.Gloda); + + process.iconClass = "indexMail"; + process.contextType = "account"; + process.contextObj = aFolder; + process.addSubject(aFolder); + + this.currentJob = { + folder: aFolder, + jobNumber: aJobNumber, + process, + startTime: new Date(), + totalItemNum: aTotalItemNum, + jobType: aJobType, + }; + + this.activityMgr.addActivity(process); + }, + + onJobProgress(aFolder, aItemNumber, aTotalItemNum) { + this.currentJob.process.state = Ci.nsIActivityProcess.STATE_INPROGRESS; + // The total number of items being processed in the job can change, as can + // the folder being processed, since we sometimes get notified about a job + // before it has determined these things, so we update them here. + this.currentJob.folder = aFolder; + this.currentJob.totalItemNum = aTotalItemNum; + + let statusText; + if (aTotalItemNum == null) { + statusText = aFolder + ? this.getString("indexingFolderStatusVague").replace("#1", aFolder) + : this.getString("indexingStatusVague"); + } else { + let percentComplete = + aTotalItemNum == 0 + ? 100 + : parseInt((aItemNumber / aTotalItemNum) * 100); + // Note: we must replace the folder name placeholder last; otherwise, + // if the name happens to contain another one of the placeholders, we'll + // hork the name when replacing it. + statusText = this.getString( + aFolder ? "indexingFolderStatusExact" : "indexingStatusExact" + ); + statusText = lazy.PluralForm.get(aTotalItemNum, statusText) + .replace("#1", aItemNumber + 1) + .replace("#2", aTotalItemNum) + .replace("#3", percentComplete) + .replace("#4", aFolder); + } + + this.currentJob.process.setProgress(statusText, aItemNumber, aTotalItemNum); + }, + + onJobCompleted() { + this.currentJob.process.state = Ci.nsIActivityProcess.STATE_COMPLETED; + + this.activityMgr.removeActivity(this.currentJob.process.id); + + // this.currentJob.totalItemNum might still be null at this point + // if we were first notified about the job before the indexer determined + // the number of messages to index and then it didn't find any to index. + let totalItemNum = this.currentJob.totalItemNum || 0; + + // We only create activity events when specific folders get indexed, + // since event-driven indexing jobs are too numerous. We also only create + // them when we ended up indexing something in the folder, since otherwise + // we'd spam the activity manager with too many "indexed 0 messages" items + // that aren't useful enough to justify their presence in the manager. + // TODO: Aggregate event-driven indexing jobs into batches significant + // enough for us to create activity events for them. + if ( + this.currentJob.jobType == "folder" && + this.currentJob.folder && + totalItemNum > 0 + ) { + // Note: we must replace the folder name placeholder last; otherwise, + // if the name happens to contain another one of the placeholders, we'll + // hork the name when replacing it. + let displayText = lazy.PluralForm.get( + totalItemNum, + this.getString("indexedFolder") + ) + .replace("#1", totalItemNum) + .replace("#2", this.currentJob.folder); + + let endTime = new Date(); + let secondsElapsed = parseInt( + (endTime - this.currentJob.startTime) / 1000 + ); + + let statusText = lazy.PluralForm.get( + secondsElapsed, + this.getString("indexedFolderStatus") + ).replace("#1", secondsElapsed); + + let event = new nsActEvent( + displayText, + lazy.Gloda, + statusText, + this.currentJob.startTime, + endTime + ); + event.contextType = this.currentJob.contextType; + event.contextObj = this.currentJob.contextObj; + event.iconClass = "indexMail"; + + // Transfer subjects. + let subjects = this.currentJob.process.getSubjects(); + for (let subject of subjects) { + event.addSubject(subject); + } + + this.activityMgr.addActivity(event); + } + + this.currentJob = null; + }, +}; diff --git a/comm/mail/components/activity/modules/moveCopy.jsm b/comm/mail/components/activity/modules/moveCopy.jsm new file mode 100644 index 0000000000..de3e51d85b --- /dev/null +++ b/comm/mail/components/activity/modules/moveCopy.jsm @@ -0,0 +1,396 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["moveCopyModule"]; + +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); + +// This module provides a link between the move/copy code and the activity +// manager. +var moveCopyModule = { + lastMessage: {}, + lastFolder: {}, + + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + getString(stringName) { + try { + return this.bundle.GetStringFromName(stringName); + } catch (e) { + this.log.error("error trying to get a string called: " + stringName); + throw e; + } + }, + + msgAdded(aMsg) {}, + + msgsDeleted(aMsgList) { + this.log.info("in msgsDeleted"); + + if (aMsgList.length <= 0) { + return; + } + + let displayCount = aMsgList.length; + // get the folder of the deleted messages + let folder = aMsgList[0].folder; + + let activities = this.activityMgr.getActivities(); + if ( + activities.length > 0 && + activities[activities.length - 1].id == this.lastMessage.id && + this.lastMessage.type == "deleteMail" && + this.lastMessage.folder == folder.prettyName + ) { + displayCount += this.lastMessage.count; + this.activityMgr.removeActivity(this.lastMessage.id); + } + + this.lastMessage = {}; + let displayText = PluralForm.get( + displayCount, + this.getString("deletedMessages2") + ); + displayText = displayText.replace("#1", displayCount); + this.lastMessage.count = displayCount; + displayText = displayText.replace("#2", folder.prettyName); + this.lastMessage.folder = folder.prettyName; + + let statusText = folder.server.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + folder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.iconClass = "deleteMail"; + this.lastMessage.type = event.iconClass; + + for (let msgHdr of aMsgList) { + event.addSubject(msgHdr.messageId); + } + + this.lastMessage.id = this.activityMgr.addActivity(event); + }, + + msgsMoveCopyCompleted(aMove, aSrcMsgList, aDestFolder) { + try { + this.log.info("in msgsMoveCopyCompleted"); + + let count = aSrcMsgList.length; + if (count <= 0) { + return; + } + + // get the folder of the moved/copied messages + let folder = aSrcMsgList[0].folder; + this.log.info("got folder"); + + let displayCount = count; + + let activities = this.activityMgr.getActivities(); + if ( + activities.length > 0 && + activities[activities.length - 1].id == this.lastMessage.id && + this.lastMessage.type == (aMove ? "moveMail" : "copyMail") && + this.lastMessage.sourceFolder == folder.prettyName && + this.lastMessage.destFolder == aDestFolder.prettyName + ) { + displayCount += this.lastMessage.count; + this.activityMgr.removeActivity(this.lastMessage.id); + } + + let statusText = ""; + if (folder.server != aDestFolder.server) { + statusText = this.getString("fromServerToServer"); + statusText = statusText.replace("#1", folder.server.prettyName); + statusText = statusText.replace("#2", aDestFolder.server.prettyName); + } else { + statusText = folder.server.prettyName; + } + + this.lastMessage = {}; + let displayText; + if (aMove) { + displayText = PluralForm.get( + displayCount, + this.getString("movedMessages") + ); + } else { + displayText = PluralForm.get( + displayCount, + this.getString("copiedMessages") + ); + } + + displayText = displayText.replace("#1", displayCount); + this.lastMessage.count = displayCount; + displayText = displayText.replace("#2", folder.prettyName); + this.lastMessage.sourceFolder = folder.prettyName; + displayText = displayText.replace("#3", aDestFolder.prettyName); + this.lastMessage.destFolder = aDestFolder.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + folder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + event.iconClass = aMove ? "moveMail" : "copyMail"; + this.lastMessage.type = event.iconClass; + + for (let msgHdr of aSrcMsgList) { + event.addSubject(msgHdr.messageId); + } + this.lastMessage.id = this.activityMgr.addActivity(event); + } catch (e) { + this.log.error("Exception: " + e); + } + }, + + folderAdded(aFolder) {}, + + folderDeleted(aFolder) { + // When a new account is created we get this notification with an empty named + // folder that can't return its server. Ignore it. + // TODO: find out what it is. + let server = aFolder.server; + // If the account has been removed, we're going to ignore this notification. + if ( + !MailServices.accounts.findServer( + server.username, + server.hostName, + server.type + ) + ) { + return; + } + + let displayText; + let statusText = server.prettyName; + + // Display a different message depending on whether we emptied the trash + // or actually deleted a folder + if (aFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Trash, false)) { + displayText = this.getString("emptiedTrash"); + } else { + displayText = this.getString("deletedFolder").replace( + "#1", + aFolder.prettyName + ); + } + + // create an activity event + let event = new nsActEvent( + displayText, + server, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.addSubject(aFolder); + event.iconClass = "deleteMail"; + + // When we rename, we get a delete event as well as a rename, so store + // the last folder we deleted + this.lastFolder = {}; + this.lastFolder.URI = aFolder.URI; + this.lastFolder.event = this.activityMgr.addActivity(event); + }, + + folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) { + this.log.info("in folderMoveCopyCompleted, aMove = " + aMove); + + let displayText; + if (aMove) { + displayText = this.getString("movedFolder"); + } else { + displayText = this.getString("copiedFolder"); + } + + displayText = displayText.replace("#1", aSrcFolder.prettyName); + displayText = displayText.replace("#2", aDestFolder.prettyName); + + let statusText = ""; + if (aSrcFolder.server != aDestFolder.server) { + statusText = this.getString("fromServerToServer"); + statusText = statusText.replace("#1", aSrcFolder.server.prettyName); + statusText = statusText.replace("#2", aDestFolder.server.prettyName); + } else { + statusText = aSrcFolder.server.prettyName; + } + // create an activity event + let event = new nsActEvent( + displayText, + aSrcFolder.server, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.addSubject(aSrcFolder); + event.addSubject(aDestFolder); + event.iconClass = aMove ? "moveMail" : "copyMail"; + + this.activityMgr.addActivity(event); + }, + + folderRenamed(aOrigFolder, aNewFolder) { + this.log.info( + "in folderRenamed, aOrigFolder = " + + aOrigFolder.prettyName + + ", aNewFolder = " + + aNewFolder.prettyName + ); + + let displayText; + let statusText = aNewFolder.server.prettyName; + + // Display a different message depending on whether we moved the folder + // to the trash or actually renamed the folder. + if (aNewFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Trash, true)) { + displayText = this.getString("movedFolderToTrash"); + displayText = displayText.replace("#1", aOrigFolder.prettyName); + } else { + displayText = this.getString("renamedFolder"); + displayText = displayText.replace("#1", aOrigFolder.prettyName); + displayText = displayText.replace("#2", aNewFolder.prettyName); + } + + // When renaming a folder, a delete event is always fired first + if (this.lastFolder.URI == aOrigFolder.URI) { + this.activityMgr.removeActivity(this.lastFolder.event); + } + + // create an activity event + let event = new nsActEvent( + displayText, + aOrigFolder.server, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.addSubject(aOrigFolder); + event.addSubject(aNewFolder); + + this.activityMgr.addActivity(event); + }, + + msgUnincorporatedMoved(srcFolder, msgHdr) { + try { + this.log.info("in msgUnincorporatedMoved"); + + // get the folder of the moved/copied messages + let destFolder = msgHdr.folder; + this.log.info("got folder"); + + let displayCount = 1; + + let activities = this.activityMgr.getActivities(); + if ( + activities.length > 0 && + activities[activities.length - 1].id == this.lastMessage.id && + this.lastMessage.type == "moveMail" && + this.lastMessage.sourceFolder == srcFolder.prettyName && + this.lastMessage.destFolder == destFolder.prettyName + ) { + displayCount += this.lastMessage.count; + this.activityMgr.removeActivity(this.lastMessage.id); + } + + let statusText = ""; + if (srcFolder.server != destFolder.server) { + statusText = this.getString("fromServerToServer"); + statusText = statusText.replace("#1", srcFolder.server.prettyName); + statusText = statusText.replace("#2", destFolder.server.prettyName); + } else { + statusText = srcFolder.server.prettyName; + } + + this.lastMessage = {}; + let displayText; + displayText = PluralForm.get( + displayCount, + this.getString("movedMessages") + ); + + displayText = displayText.replace("#1", displayCount); + this.lastMessage.count = displayCount; + displayText = displayText.replace("#2", srcFolder.prettyName); + this.lastMessage.sourceFolder = srcFolder.prettyName; + displayText = displayText.replace("#3", destFolder.prettyName); + this.lastMessage.destFolder = destFolder.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + srcFolder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.iconClass = "moveMail"; + this.lastMessage.type = event.iconClass; + event.addSubject(msgHdr.messageId); + this.lastMessage.id = this.activityMgr.addActivity(event); + } catch (e) { + this.log.error("Exception: " + e); + } + }, + + init() { + // XXX when do we need to remove ourselves? + MailServices.mfn.addListener( + this, + MailServices.mfn.msgsDeleted | + MailServices.mfn.msgsMoveCopyCompleted | + MailServices.mfn.folderDeleted | + MailServices.mfn.folderMoveCopyCompleted | + MailServices.mfn.folderRenamed | + MailServices.mfn.msgUnincorporatedMoved + ); + }, +}; diff --git a/comm/mail/components/activity/modules/pop3Download.jsm b/comm/mail/components/activity/modules/pop3Download.jsm new file mode 100644 index 0000000000..f203b33212 --- /dev/null +++ b/comm/mail/components/activity/modules/pop3Download.jsm @@ -0,0 +1,154 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["pop3DownloadModule"]; + +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); + +// This module provides a link between the pop3 service code and the activity +// manager. +var pop3DownloadModule = { + // hash table of most recent download items per folder + _mostRecentActivityForFolder: new Map(), + // hash table of prev download items per folder, so we can + // coalesce consecutive no new message events. + _prevActivityForFolder: new Map(), + + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + getString(stringName) { + try { + return this.bundle.GetStringFromName(stringName); + } catch (e) { + this.log.error("error trying to get a string called: " + stringName); + throw e; + } + }, + + onDownloadStarted(aFolder) { + this.log.info("in onDownloadStarted"); + + let displayText = this.bundle.formatStringFromName( + "pop3EventStartDisplayText2", + [ + aFolder.server.prettyName, // account name + aFolder.prettyName, + ] + ); // folder name + // remember the prev activity for this folder, if any. + this._prevActivityForFolder.set( + aFolder.URI, + this._mostRecentActivityForFolder.get(aFolder.URI) + ); + let statusText = aFolder.server.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + aFolder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.iconClass = "syncMail"; + + let downloadItem = {}; + downloadItem.eventID = this.activityMgr.addActivity(event); + this._mostRecentActivityForFolder.set(aFolder.URI, downloadItem); + }, + + onDownloadProgress(aFolder, aNumMsgsDownloaded, aTotalMsgs) { + this.log.info("in onDownloadProgress"); + }, + + onDownloadCompleted(aFolder, aNumMsgsDownloaded) { + this.log.info("in onDownloadCompleted"); + + // Remove activity if there was any. + // It can happen that download never started (e.g. couldn't connect to server), + // with onDownloadStarted, but we still get a onDownloadCompleted event + // when the connection is given up. + let recentActivity = this._mostRecentActivityForFolder.get(aFolder.URI); + if (recentActivity) { + this.activityMgr.removeActivity(recentActivity.eventID); + } + + let displayText; + if (aNumMsgsDownloaded > 0) { + displayText = PluralForm.get( + aNumMsgsDownloaded, + this.getString("pop3EventStatusText") + ); + displayText = displayText.replace("#1", aNumMsgsDownloaded); + } else { + displayText = this.getString("pop3EventStatusTextNoMsgs"); + } + + let statusText = aFolder.server.prettyName; + + // create an activity event + let event = new nsActEvent( + displayText, + aFolder, + statusText, + Date.now(), // start time + Date.now() + ); // completion time + + event.iconClass = "syncMail"; + + let downloadItem = { numMsgsDownloaded: aNumMsgsDownloaded }; + this._mostRecentActivityForFolder.set(aFolder.URI, downloadItem); + downloadItem.eventID = this.activityMgr.addActivity(event); + if (!aNumMsgsDownloaded) { + // If we didn't download any messages this time, and the prev event + // for this folder also didn't download any messages, remove the + // prev event from the activity manager. + let prevItem = this._prevActivityForFolder.get(aFolder.URI); + if (prevItem != undefined && !prevItem.numMsgsDownloaded) { + if (this.activityMgr.containsActivity(prevItem.eventID)) { + this.activityMgr.removeActivity(prevItem.eventID); + } + } + } + }, + init() { + // XXX when do we need to remove ourselves? + MailServices.pop3.addListener(this); + }, +}; diff --git a/comm/mail/components/activity/modules/sendLater.jsm b/comm/mail/components/activity/modules/sendLater.jsm new file mode 100644 index 0000000000..37027d96f1 --- /dev/null +++ b/comm/mail/components/activity/modules/sendLater.jsm @@ -0,0 +1,298 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["sendLaterModule"]; + +var nsActProcess = Components.Constructor( + "@mozilla.org/activity-process;1", + "nsIActivityProcess", + "init" +); +var nsActEvent = Components.Constructor( + "@mozilla.org/activity-event;1", + "nsIActivityEvent", + "init" +); +var nsActWarning = Components.Constructor( + "@mozilla.org/activity-warning;1", + "nsIActivityWarning", + "init" +); + +/** + * This really, really, sucks. Due to mailnews widespread use of + * nsIMsgStatusFeedback we're bound to the UI to get any sensible feedback of + * mail sending operations. The current send later code can't hook into the + * progress listener easily to get the state of messages being sent, so we'll + * just have to do it here. + */ +var sendMsgProgressListener = { + QueryInterface: ChromeUtils.generateQI([ + "nsIMsgStatusFeedback", + "nsISupportsWeakReference", + ]), + + showStatusString(aStatusText) { + sendLaterModule.onMsgStatus(aStatusText); + }, + + startMeteors() {}, + + stopMeteors() {}, + + showProgress(aPercentage) { + sendLaterModule.onMessageSendProgress(0, 0, aPercentage, 0); + }, +}; + +// This module provides a link between the send later service and the activity +// manager. +var sendLaterModule = { + _sendProcess: null, + _copyProcess: null, + _identity: null, + _subject: null, + + get log() { + delete this.log; + return (this.log = console.createInstance({ + prefix: "mail.activity", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.activity.loglevel", + })); + }, + + get activityMgr() { + delete this.activityMgr; + return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService( + Ci.nsIActivityManager + )); + }, + + get bundle() { + delete this.bundle; + return (this.bundle = Services.strings.createBundle( + "chrome://messenger/locale/activity.properties" + )); + }, + + QueryInterface: ChromeUtils.generateQI(["nsIMsgSendLaterListener"]), + + _displayTextForHeader(aLocaleStringBase, aSubject) { + return aSubject + ? this.bundle.formatStringFromName(aLocaleStringBase + "WithSubject", [ + aSubject, + ]) + : this.bundle.GetStringFromName(aLocaleStringBase); + }, + + _newProcess(aLocaleStringBase, aAddSubject) { + let process = new nsActProcess( + this._displayTextForHeader( + aLocaleStringBase, + aAddSubject ? this._subject : "" + ), + this.activityMgr + ); + + process.iconClass = "sendMail"; + process.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; + process.contextObj = this; + process.contextType = "SendLater"; + process.contextDisplayText = + this.bundle.GetStringFromName("sendingMessages"); + + return process; + }, + + // Use this to group an activity by the identity if we have one. + _applyIdentityGrouping(aActivity) { + if (this._identity) { + aActivity.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT; + aActivity.contextType = this._identity.key; + aActivity.contextObj = this._identity; + let contextDisplayText = this._identity.identityName; + if (!contextDisplayText) { + contextDisplayText = this._identity.email; + } + + aActivity.contextDisplayText = contextDisplayText; + } else { + aActivity.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE; + } + }, + + // Replaces the process with an event that reflects a completed process. + _replaceProcessWithEvent(aProcess) { + this.activityMgr.removeActivity(aProcess.id); + + let event = new nsActEvent( + this._displayTextForHeader("sentMessage", this._subject), + this.activityMgr, + "", + aProcess.startTime, + new Date() + ); + + event.iconClass = "sendMail"; + this._applyIdentityGrouping(event); + + this.activityMgr.addActivity(event); + }, + + // Replaces the process with a warning that reflects the failed process. + _replaceProcessWithWarning( + aProcess, + aCopyOrSend, + aStatus, + aMsg, + aMessageHeader + ) { + this.activityMgr.removeActivity(aProcess.id); + + let warning = new nsActWarning( + this._displayTextForHeader("failedTo" + aCopyOrSend, this._subject), + this.activityMgr, + "" + ); + + warning.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE; + this._applyIdentityGrouping(warning); + + this.activityMgr.addActivity(warning); + }, + + onStartSending(aTotalMessageCount) { + if (!aTotalMessageCount) { + this.log.error("onStartSending called with zero messages\n"); + } + }, + + onMessageStartSending( + aCurrentMessage, + aTotalMessageCount, + aMessageHeader, + aIdentity + ) { + // We want to use the identity and subject later, so store them for now. + this._identity = aIdentity; + if (aMessageHeader) { + this._subject = aMessageHeader.mime2DecodedSubject; + } + + // Create the process to display the send activity. + let process = this._newProcess("sendingMessage", true); + this._sendProcess = process; + this.activityMgr.addActivity(process); + + // Now the one for the copy process. + process = this._newProcess("copyMessage", false); + this._copyProcess = process; + this.activityMgr.addActivity(process); + }, + + onMessageSendProgress( + aCurrentMessage, + aTotalMessageCount, + aMessageSendPercent, + aMessageCopyPercent + ) { + if (aMessageSendPercent < 100) { + // Ensure we are in progress... + if (this._sendProcess.state != Ci.nsIActivityProcess.STATE_INPROGRESS) { + this._sendProcess.state = Ci.nsIActivityProcess.STATE_INPROGRESS; + } + + // ... and update the progress. + this._sendProcess.setProgress( + this._sendProcess.lastStatusText, + aMessageSendPercent, + 100 + ); + } else if (aMessageSendPercent == 100) { + if (aMessageCopyPercent == 0) { + // Set send state to completed + if (this._sendProcess.state != Ci.nsIActivityProcess.STATE_COMPLETED) { + this._sendProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED; + } + this._replaceProcessWithEvent(this._sendProcess); + + // Set copy state to in progress. + if (this._copyProcess.state != Ci.nsIActivityProcess.STATE_INPROGRESS) { + this._copyProcess.state = Ci.nsIActivityProcess.STATE_INPROGRESS; + } + + // We don't know the progress of the copy, so just set to 0, and we'll + // display an undetermined progress meter. + this._copyProcess.setProgress(this._copyProcess.lastStatusText, 0, 0); + } else if (aMessageCopyPercent >= 100) { + // We need to set this to completed otherwise activity manager + // complains. + if (this._copyProcess) { + this._copyProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED; + this.activityMgr.removeActivity(this._copyProcess.id); + this._copyProcess = null; + } + + this._sendProcess = null; + } + } + }, + + onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) { + if ( + this._sendProcess && + this._sendProcess.state != Ci.nsIActivityProcess.STATE_COMPLETED + ) { + this._sendProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED; + this._replaceProcessWithWarning( + this._sendProcess, + "SendMessage", + aStatus, + aMsg, + aMessageHeader + ); + this._sendProcess = null; + + if ( + this._copyProcess && + this._copyProcess.state != Ci.nsIActivityProcess.STATE_COMPLETED + ) { + this._copyProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED; + this.activityMgr.removeActivity(this._copyProcess.id); + this._copyProcess = null; + } + } + }, + + onMsgStatus(aStatusText) { + this._sendProcess.setProgress( + aStatusText, + this._sendProcess.workUnitComplete, + this._sendProcess.totalWorkUnits + ); + }, + + onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) {}, + + init() { + // We should need to remove the listener as we're not being held by anyone + // except by the send later instance. + let sendLaterService = Cc[ + "@mozilla.org/messengercompose/sendlater;1" + ].getService(Ci.nsIMsgSendLater); + + sendLaterService.addListener(this); + + // Also add the nsIMsgStatusFeedback object. + let statusFeedback = Cc[ + "@mozilla.org/messenger/statusfeedback;1" + ].createInstance(Ci.nsIMsgStatusFeedback); + + statusFeedback.setWrappedStatusFeedback(sendMsgProgressListener); + + sendLaterService.statusFeedback = statusFeedback; + }, +}; diff --git a/comm/mail/components/activity/moz.build b/comm/mail/components/activity/moz.build new file mode 100644 index 0000000000..efceaacf9f --- /dev/null +++ b/comm/mail/components/activity/moz.build @@ -0,0 +1,34 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +XPIDL_SOURCES += [ + "nsIActivity.idl", + "nsIActivityManager.idl", + "nsIActivityManagerUI.idl", +] + +XPIDL_MODULE = "activity" + +EXTRA_JS_MODULES.activity += [ + "modules/activityModules.jsm", + "modules/alertHook.jsm", + "modules/autosync.jsm", + "modules/glodaIndexer.jsm", + "modules/moveCopy.jsm", + "modules/pop3Download.jsm", + "modules/sendLater.jsm", +] + +EXTRA_JS_MODULES += [ + "Activity.jsm", + "ActivityManager.jsm", + "ActivityManagerUI.jsm", +] + +JAR_MANIFESTS += ["jar.mn"] + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/comm/mail/components/activity/nsIActivity.idl b/comm/mail/components/activity/nsIActivity.idl new file mode 100644 index 0000000000..d80e69088f --- /dev/null +++ b/comm/mail/components/activity/nsIActivity.idl @@ -0,0 +1,492 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" +#include "nsISupportsPrimitives.idl" + +interface nsIActivityListener; +interface nsIActivity; +interface nsIActivityProcess; +interface nsIActivityEvent; +interface nsIActivityWarning; +interface nsIVariant; +interface nsISupportsPRTime; + +/** + * See https://wiki.mozilla.org/Thunderbird:Activity_Manager/Developer for UML + * diagram and sample codes. + */ + +/** + * Background: + * Activity handlers define the behavioral capabilities of the activities. They + * are used by the Activity Manager to change the execution flow of the activity + * based on the user interaction. They are not mandatory, but when set, causes + * behavioral changes on the binding representing the activity, such as showing + * a cancel button, etc. The following handlers are currently supported; + */ + +/** + * The handler to invoke when the recover button is pressed. Used by a Warning + * to recover from the situation causing the warning. For instance, recovery + * action for a "Over Quota Limit" warning, would be to cleanup some disk space, + * and this operation can be implemented and set by the activity developer in + * form of nsIActivityRecoveryHandler component. + */ +[scriptable, uuid(30E0A76F-880A-4093-8F3C-AF2239977A3D)] +interface nsIActivityRecoveryHandler : nsISupports { + nsresult recover(in nsIActivityWarning aActivity); +}; + +/** + * The handler to invoke when the cancel button is pressed. Used by a Process to + * cancel the operation. + */ +[scriptable, uuid(35ee2461-70db-4b3a-90d0-7a68c856e911)] +interface nsIActivityCancelHandler : nsISupports { + nsresult cancel(in nsIActivityProcess aActivity); +}; + +/** + * The handler to invoke when the pause button is pressed. Used by a Process to + * pause/resume the operation. + */ +[scriptable, uuid(9eee22bf-5378-460e-83a7-781cdcc9050b)] +interface nsIActivityPauseHandler : nsISupports { + nsresult pause(in nsIActivityProcess aActivity); + nsresult resume(in nsIActivityProcess aActivity); +}; + +/** + * The handler to invoke when the retry button is pressed. Used by a Process to + * retry the operation in case of failure. + */ +[scriptable, uuid(8ec42517-951f-4bc0-aba5-fde7258b1705)] +interface nsIActivityRetryHandler : nsISupports { + nsresult retry(in nsIActivityProcess aActivity); +}; + +/** + * The handler to invoke when the undo button is pressed. Used by a Event to + * undo the operation generated the event. + */ +[scriptable, uuid(b8632ac7-9d8b-4341-a349-ef000e8c89ac)] +interface nsIActivityUndoHandler : nsISupports { + nsresult undo(in nsIActivityEvent aActivity); +}; + +/** + * Base interface of all activity interfaces. It is abstract in a sense that + * there is no component in the activity management system that solely + * implements this interface. + */ +[scriptable, uuid(6CD33E65-B2D8-4634-9B6D-B80BF1273E99)] +interface nsIActivity : nsISupports { + + /** + * Shows the activity as a standalone item. + */ + const short GROUPING_STYLE_STANDALONE = 1; + + /** + * Groups activity by its context. + */ + const short GROUPING_STYLE_BYCONTEXT = 2; + + /** + * Internal ID given by the activity manager when + * added into the activity list. Not readonly so that + * the activity manager can write to them, but not to be written to + * by anyone else. + */ + attribute unsigned long id; + + // Following attributes change the UI characteristics of the activity + + /** + * A brief description of the activity, to be shown by the + * associated binding (XBL) in the Activity Manager window. + */ + readonly attribute AString displayText; + + /** + * Changes the default icon associated with the activity. Core activity + * icons are declared in |mail/themes//mail/activity/activity.css| + * files. + * + * Extension developers can add and assign their own icons by setting + * this attribute. + */ + attribute AString iconClass; + + /** + * Textual id of the XBL binding that will be used to represent the + * activity in the Activity Manager window. + * + * This attribute allows to associate default activity components + * with custom XBL bindings. See |activity.xml| file for default + * activity XBL bindings, and |activity.css| file for default binding + * associations. + */ + attribute AString bindingName; + + /** + * Defines the grouping style of the activity when being shown in the + * activity manager window: + * GROUPING_STYLE_STANDALONE or GROUPING_STYLE_BYCONTEXT + */ + attribute short groupingStyle; + + /** + * A text value to associate a facet type with the activity. If empty, + * the activity will be shown in the 'Misc' section. + */ + attribute AString facet; + + // UI related attributes end. + + /** + * Gets the initiator of the activity. An initiator represents an object + * that generates and controls the activity. For example, Copy Service can be + * the initiator of the copy, and move activities. Similarly Gloda can be the + * initiator of indexing activity, etc. + * + * This attribute is used mostly by handler components to change the execution + * flow of the activity such as canceling, pausing etc. Since not used by the + * Activity Manager, it is not mandatory to set it. + * + * An initiator can be any JS Object or an XPCOM component that provides an + * nsIVariant interface. + */ + readonly attribute nsIVariant initiator; + + /** + * Adds an object to the activity's internal subject list. Subject list + * provides argument(s) to activity handlers to complete their operation. + * For example, nsIActivityUndoHandler needs the source and destination + * folders to undo a move operation. + * + * Since subjects are not used by the Activity Manager, it is up to the + * activity developer to provide these objects. + * + * A subject can be any JS object or XPCOM component that supports nsIVariant + * interface. + */ + void addSubject(in nsIVariant aSubject); + + /** + * Retrieves all subjects associated with this activity. + * + * @return The list of subject objects associated by the activity. + */ + Array getSubjects(); + + /* + * Background: + * A context is a generic concept that is used to group the processes and + * warnings having similar properties such as same imap server, same smtp + * server etc. + * A context is uniquely identified by its "type" and "object" attributes. + * Each activity that has the same context type and object are considered + * belong to the same logical group, context. + * + * There are 4 predefined context types known by the Activity Manager: + * Account, Smtp, Calendar, and Addressbook. The most common context type + * for activities is the "Account Context" and when combined with an account + * server instance, it allows to group different activities happening on the + * the same account server. + */ + + /** + * Sets and gets the context object of the activity. A context object can be + * any JS object or XPCOM component that supports nsIVariant interface. + */ + attribute nsIVariant contextObj; + + /** + * Sets and gets the context type of the activity. If this is set, then + * the contextDisplayText should also be set. + */ + attribute AString contextType; + + /** + * Return the displayText to be used for the context + **/ + attribute AString contextDisplayText; + + /** + * Adds a listener. See nsIActivityListener below. + */ + void addListener(in nsIActivityListener aListener); + + /** + * Removes the given listener. See nsIActivityListener below. + */ + void removeListener(in nsIActivityListener aListener); +}; + + +/** + * A Process represents an on-going activity. + */ +[scriptable, uuid(9DC7CA67-828D-4AFD-A5C6-3ECE091A98B8)] +interface nsIActivityProcess : nsIActivity { + + /** + * Default state for uninitialized process activity + * object. + */ + const short STATE_NOTSTARTED = -1; + + /** + * Activity is currently in progress. + */ + const short STATE_INPROGRESS = 0; + + /** + * Activity is completed. + */ + const short STATE_COMPLETED = 1; + + /** + * Activity was canceled by the user. + * (same as completed) + */ + const short STATE_CANCELED = 2; + + /** + * Activity was paused by the user. + */ + const short STATE_PAUSED = 3; + + /** + * Activity waits for the user input's to retry. + * (i.e. login password) + */ + const short STATE_WAITINGFORINPUT = 4; + + /** + * Activity is ready for an automatic or manual retry. + */ + const short STATE_WAITINGFORRETRY = 5; + + /** + * The state of the activity. + * See above for possible values. + * @exception NS_ERROR_ILLEGAL_VALUE if the state isn't one of the states + * defined above. + */ + attribute short state; + + /** + * The percentage of activity completed. + * If the max value is unknown it'll be -1 here. + */ + readonly attribute long percentComplete; + + /** + * A brief text about the process' status. + */ + readonly attribute AString lastStatusText; + + /** + * The amount of work units completed so far. + */ + readonly attribute unsigned long workUnitComplete; + + /** + * Total amount of work units. + */ + readonly attribute unsigned long totalWorkUnits; + + /** + * The starting time of the process. + * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970 + * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal + * Time, UTC.). The units of time are in microseconds. + * + * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set + * this value. + */ + readonly attribute long long startTime; + + /** + * The handler to invoke when the cancel button is pressed. If present + * (non-null), the activity can be canceled and a cancel button will be + * displayed to the user. If null, it cannot be canceled and no button will + * be displayed. + */ + attribute nsIActivityCancelHandler cancelHandler; + + /** + * The handler to invoke when the pause button is pressed. If present + * (non-null), the activity can be pauseable and a pause button will be + * displayed to the user. If null, it cannot be paused and no button will + * be displayed. + */ + attribute nsIActivityPauseHandler pauseHandler; + + /** + * The handler to invoke when the retry button is pressed. If present + * (non-null), the activity can be retryable and a retry button will be + * displayed to the user. If null, it cannot be retried and no button will + * be displayed. + */ + attribute nsIActivityRetryHandler retryHandler; + + /** + * Updates the activity progress info. + * + * @param aStatusText A localized text describing the current status of the + * process + * @param aWorkUnitComplete The amount of work units completed. Not used by + * Activity Manager or default binding for any + * purpose. + * @param aTotalWorkUnits Total amount of work units. Not used by + * Activity Manager or default binding for any + * purpose. If set to zero, this indicates that the + * number of work units is unknown, and the percentage + * attribute will be set to -1. + */ + void setProgress(in AString aStatusText, + in unsigned long aWorkUnitComplete, + in unsigned long aTotalWorkUnits); + + /** + * Component initialization method. + * + * @param aDisplayText A localized text to be shown on the Activity Manager + * window + * @param aInitiator The initiator of the process + */ + void init(in AString aDisplayText, in nsIVariant aInitiator); +}; + +/** + * Historical actions performed by the user, by extensions or by the system. + */ +[scriptable, uuid(5B1B0D03-2820-4E37-8BF8-102AFDE4FC45)] +interface nsIActivityEvent : nsIActivity { + + /** + * Any localized textual information related to this event. + * It is shown at the bottom of the displayText area. + */ + readonly attribute AString statusText; + + /** + * The starting time of the event. + * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970 + * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal + * Time, UTC.). The units of time are in microseconds. + * + * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set + * this value. + */ + readonly attribute long long startTime; + + /** + * The completion time of the event in microseconds. + * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970 + * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal + * Time, UTC.). The units of time are in microseconds. + * + * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set + * this value. + */ + readonly attribute long long completionTime; + + /** + * The handler to invoke when the undo button is pressed. If present + * (non-null), the activity can be undoable and an undo button will be + * displayed to the user. If null, it cannot be undone and no button will + * be displayed. + */ + attribute nsIActivityUndoHandler undoHandler; + + /** + * Component initialization method. + * + * @param aDisplayText Any localized text describing the event and its context + * @param aInitiator The initiator of the event + * @param aStatusText Any localized additional information about the event + * @param aStartTime The starting time of the event + * @param aCompletionTime The completion time of the event + */ + void init(in AString aDisplayText, in nsIVariant aInitiator, + in AString aStatusText, in long long aStartTime, + in long long aCompletionTime); +}; + +[scriptable, uuid(8265833e-c604-4585-a43c-a76bd8ed3a8c)] +interface nsIActivityWarning : nsIActivity { + + /** + * Any localized textual information related to this warning. + */ + readonly attribute AString warningText; + + /** + * The time of the warning. + * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970 + * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal + * Time, UTC.). The units of time are in microseconds. + * + * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set + * this value. + */ + readonly attribute long long time; + + /** + * Recovery tip of the warning, localized. + */ + readonly attribute AString recoveryTipText; + + /** + * The handler to invoke when the recover button is pressed. If present + * (non-null), the activity can be recoverable and a recover button will be + * displayed to the user. If null, it cannot be recovered and no button will + * be displayed. + */ + attribute nsIActivityRecoveryHandler recoveryHandler; + + /** + * Component initialization method. + * + * @param aWarningText The localized text that will be shown on the display + * area + * @param aInitiator The initiator of the warning + * @param aRecoveryTip A localized textual information to guide the user in + * order to recover from the warning situation. + */ + void init(in AString aWarningText, in nsIVariant aInitiator, + in AString aRecoveryTip); +}; + +[scriptable, uuid(bd11519f-b297-4b34-a793-1861dc90d5e9)] +interface nsIActivityListener : nsISupports { + /** + * Triggered after activity state is changed. + */ + void onStateChanged(in nsIActivity aActivity, in short aOldState); + + /** + * Triggered after the progress of the process activity is changed. + */ + void onProgressChanged(in nsIActivity aActivity, + in AString aStatusText, + in unsigned long aWorkUnitsCompleted, + in unsigned long aTotalWorkUnits); + + /** + * Triggered after one of the activity handler is set. + * + * This is mostly used to update the UI of the activity when + * one of the handler is set to null after the operation is completed. + * For example after the activity is undone, to make the undo button + * invisible. + */ + void onHandlerChanged(in nsIActivity aActivity); +}; diff --git a/comm/mail/components/activity/nsIActivityManager.idl b/comm/mail/components/activity/nsIActivityManager.idl new file mode 100644 index 0000000000..860b4e1e2b --- /dev/null +++ b/comm/mail/components/activity/nsIActivityManager.idl @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface mozIStorageConnection; +interface nsIActivity; +interface nsIActivityProcess; +interface nsIVariant; + +/** + * See https://wiki.mozilla.org/Thunderbird:Activity_Manager/Developer for UML + * diagram and sample codes. + */ + +/** + * An interface to get notified by the major Activity Manager events. + * Mostly used by UI glue code in activity.js. + */ +[scriptable, uuid(14cfad1c-3401-4c44-ab04-4a11b6662663)] +interface nsIActivityMgrListener : nsISupports { + /** + * Called _after_ activity manager adds an activity into + * the managed list. + */ + void onAddedActivity(in unsigned long aID, in nsIActivity aActivity); + + /** + * Called _after_ activity manager removes an activity from + * the managed list. + */ + void onRemovedActivity(in unsigned long aID); +}; + +/** + * Activity Manager is a simple component that understands how do display a + * combination of user activity and history. The activity manager works in + * conjunction with the 'Interactive Status Bar' to give the user the right + * level of notifications concerning what Thunderbird is doing on it's own and + * how Thunderbird has handled user requests. + * + * There are 3 different classifications of activity items which can be + * displayed in the Activity Manager Window: + * o Process: Processes are transient in the display. They are not written to + * disk as they are always acting on some data that already exists + * locally or remotely. If a process has finished and needs to keep + * some state for the user (like last sync time) it can convert + * itself into an event. + * o Event: Historical actions performed by the user and created by a process + * for the Activity Manager Window. Events can show up in the + * 'Interactive Status Bar' and be displayed to users as they are + * created. + * o Warning: Alerts sent by Thunderbird or servers (i.e. imap server) that need + * attention by the user. For example a Quota Alert from the imap + * server can be represented as a Warning to the user. They are not + * written to disk. + */ +[scriptable, uuid(9BFCC031-50E1-4D30-A35F-23509ABCB8D1)] +interface nsIActivityManager : nsISupports { + + /** + * Adds the given activity into the managed activities list. + * + * @param aActivity The activity that will be added. + * + * @return Unique activity identifier. + */ + unsigned long addActivity(in nsIActivity aActivity); + + /** + * Removes the activity with the given id if it's not currently + * in-progress. + * + * @param aID The unique ID of the activity. + * + * @throws NS_ERROR_FAILURE if the activity is in-progress. + */ + void removeActivity(in unsigned long aID); + + /** + * Retrieves an activity managed by the activity manager. This can be one that + * is in progress, or one that has completed in the past and is stored in the + * persistent store. + * + * @param aID The unique ID of the activity. + * + * @return The activity with the specified ID, or null if not found. + */ + nsIActivity getActivity(in unsigned long aID); + + /** + * Tests whether the activity in question in the activity list or not. + */ + boolean containsActivity(in unsigned long aID); + + /** + * Retrieves all activities managed by the activity manager. This can be one + * that is in progress (process), one that is represented as a warning, or one + * that has completed (event) in the past and is stored in the persistent + * store. + * + * @return A read-only list of activities managed by the activity manager. + */ + Array getActivities(); + + /** + * Retrieves processes with given context type and object. + * + * @return A read-only list of processes matching to given criteria. + */ + Array getProcessesByContext(in AString aContextType, + in nsIVariant aContextObject); + + /** + * Call to remove all activities apart from those that are in progress. + */ + void cleanUp(); + + /** + * The number of processes in the activity list. + */ + readonly attribute long processCount; + + /** + * Adds a listener. + */ + void addListener(in nsIActivityMgrListener aListener); + + /** + * Removes the given listener. + */ + void removeListener(in nsIActivityMgrListener aListener); +}; diff --git a/comm/mail/components/activity/nsIActivityManagerUI.idl b/comm/mail/components/activity/nsIActivityManagerUI.idl new file mode 100644 index 0000000000..07a2a30394 --- /dev/null +++ b/comm/mail/components/activity/nsIActivityManagerUI.idl @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsISupports.idl" + +interface nsIInterfaceRequestor; + +[scriptable, uuid(ae7853b0-2e1f-4dc1-89cd-f8bbfb745d4d)] +interface nsIActivityManagerUI : nsISupports { + /** + * The reason that should be passed when the user requests to show the + * activity manager's UI. + */ + const short REASON_USER_INTERACTED = 0; + + /** + * The reason that should be passed to the show method when we are displaying + * the UI because a new activity is being added to it. + */ + const short REASON_NEW_ACTIVITY = 1; + + /** + * Shows the Activity Manager's UI to the user. + * + * @param [optional] aWindowContext + * The parent window context to show the UI. + * @param [optional] aID + * The id of the activity to be preselected upon opening. + * @param [optional] aReason + * The reason to show the activity manager's UI. This defaults to + * REASON_USER_INTERACTED, and should be one of the previously listed + * constants. + */ + void show([optional] in nsIInterfaceRequestor aWindowContext, + [optional] in unsigned long aID, + [optional] in short aReason); + + /** + * Indicates if the UI is visible or not. + */ + readonly attribute boolean visible; + + /** + * Brings attention to the UI if it is already visible + * + * @throws NS_ERROR_UNEXPECTED if the UI is not visible. + */ + void getAttention(); +}; diff --git a/comm/mail/components/addrbook/content/abCommon.js b/comm/mail/components/addrbook/content/abCommon.js new file mode 100644 index 0000000000..36f251206e --- /dev/null +++ b/comm/mail/components/addrbook/content/abCommon.js @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../../../../mailnews/addrbook/content/abResultsPane.js */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var gAbView = null; + +var kDefaultAscending = "ascending"; +var kDefaultDescending = "descending"; +var kAllDirectoryRoot = "moz-abdirectory://"; +var kPersonalAddressbookURI = "jsaddrbook://abook.sqlite"; + +async function AbDelete() { + let types = GetSelectedCardTypes(); + if (types == kNothingSelected) { + return; + } + + let cards = GetSelectedAbCards(); + + // Determine strings for smart and context-sensitive user prompts + // for confirming deletion. + let action, name, list; + let selectedDir = gAbView.directory; + + switch (types) { + case kListsAndCards: + action = "delete-mixed"; + break; + case kSingleListOnly: + case kMultipleListsOnly: + action = "delete-lists"; + name = cards[0].displayName; + break; + default: { + let nameFormatFromPref = Services.prefs.getIntPref( + "mail.addr_book.lastnamefirst" + ); + name = cards[0].generateName(nameFormatFromPref); + if (selectedDir && selectedDir.isMailList) { + action = "remove-contacts"; + list = selectedDir.dirName; + } else { + action = "delete-contacts"; + } + break; + } + } + + // Adjust strings to match translations. + let actionString; + switch (action) { + case "delete-contacts": + actionString = !cards.length + ? "delete-contacts-single" + : "delete-contacts-multi"; + break; + case "remove-contacts": + actionString = !cards.length + ? "remove-contacts-single" + : "remove-contacts-multi"; + break; + default: + actionString = action; + break; + } + + let [title, message] = await document.l10n.formatValues([ + { + id: `about-addressbook-confirm-${action}-title`, + args: { count: cards.length }, + }, + { + id: `about-addressbook-confirm-${actionString}`, + args: { + count: cards.length, + name, + list, + }, + }, + ]); + + // Finally, show our smart confirmation message, and act upon it! + if (!Services.prompt.confirm(window, title, message)) { + // Deletion cancelled by user. + return; + } + + // Delete cards from address books or mailing lists. + gAbView.deleteSelectedCards(); +} + +function AbNewMessage(address) { + let params = Cc[ + "@mozilla.org/messengercompose/composeparams;1" + ].createInstance(Ci.nsIMsgComposeParams); + params.type = Ci.nsIMsgCompType.New; + params.format = Ci.nsIMsgCompFormat.Default; + params.composeFields = Cc[ + "@mozilla.org/messengercompose/composefields;1" + ].createInstance(Ci.nsIMsgCompFields); + + if (address) { + params.composeFields.to = address; + } else { + params.composeFields.to = GetSelectedAddresses(); + } + MailServices.compose.OpenComposeWindowWithParams(null, params); +} + +/** + * Make a mailbox string from the card, for use in the UI. + * + * @param {nsIAbCard} - The card to use. + * @returns {string} A mailbox representation of the card. + */ +function makeMailboxObjectFromCard(card) { + if (!card) { + return ""; + } + + let email; + if (card.isMailList) { + let directory = GetDirectoryFromURI(card.mailListURI); + email = directory.description || card.displayName; + } else { + email = card.primaryEmail; + } + + return MailServices.headerParser + .makeMailboxObject(card.displayName, email) + .toString(); +} + +function GetDirectoryFromURI(uri) { + if (uri.startsWith("moz-abdirectory://")) { + return null; + } + return MailServices.ab.getDirectory(uri); +} diff --git a/comm/mail/components/addrbook/content/abContactsPanel.js b/comm/mail/components/addrbook/content/abContactsPanel.js new file mode 100644 index 0000000000..c1e3481318 --- /dev/null +++ b/comm/mail/components/addrbook/content/abContactsPanel.js @@ -0,0 +1,374 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../../../../../toolkit/content/editMenuOverlay.js */ +/* import-globals-from ../../../../mailnews/addrbook/content/abResultsPane.js */ +/* import-globals-from ../../../base/content/globalOverlay.js */ +/* import-globals-from abCommon.js */ + +var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm"); +var { getSearchTokens, getModelQuery, generateQueryURI } = ChromeUtils.import( + "resource:///modules/ABQueryUtils.jsm" +); + +// A boolean variable determining whether AB column should be shown +// in Contacts Sidebar in compose window. +var gShowAbColumnInComposeSidebar = false; +var gQueryURIFormat = null; + +UIDensity.registerWindow(window); + +function GetAbViewListener() { + // the ab panel doesn't care if the total changes, or if the selection changes + return null; +} + +/** + * Handle the command event on abContextMenuButton (click, Enter, spacebar). + */ +function abContextMenuButtonOnCommand(event) { + showContextMenu("sidebarAbContextMenu", event, [ + event.target, + "after_end", + 0, + 0, + true, + ]); +} + +/** + * Handle the context menu event of results tree (right-click, context menu key + * press, etc.). Show the respective context menu for selected contact(s) or + * results tree blank space (work around for XUL tree bug 1331377). + * + * @param aEvent a context menu event (right-click, context menu key press, etc.) + */ +function contactsListOnContextMenu(aEvent) { + let target = aEvent.target; + let contextMenuID; + let positionArray; + + // For right-click on column header or column picker, don't show context menu. + if (target.localName == "treecol" || target.localName == "treecolpicker") { + return; + } + + // On treechildren, if there's no selection, show "sidebarAbContextMenu". + if (gAbView.selection.count == 0) { + contextMenuID = gAbResultsTree.getAttribute("contextNoSelection"); + // If "sidebarAbContextMenu" menu was activated by keyboard, + // position it in the topleft corner of gAbResultsTree. + if (!aEvent.button) { + positionArray = [gAbResultsTree, "overlap", 0, 0, true]; + } + // If there's a selection, show "cardProperties" context menu. + } else { + contextMenuID = gAbResultsTree.getAttribute("contextSelection"); + updateCardPropertiesMenu(); + } + showContextMenu(contextMenuID, aEvent, positionArray); +} + +/** + * Update the single row card properties context menu to show or hide the "Edit" + * menu item only depending on the selection type. + */ +function updateCardPropertiesMenu() { + let cards = GetSelectedAbCards(); + + let separator = document.getElementById("abContextBeforeEditContact"); + let menuitem = document.getElementById("abContextEditContact"); + + // Only show the Edit item if one item is selected, is not a mailing list, and + // the contact is not part of a readOnly address book. + if ( + cards.length != 1 || + cards.some(c => c.isMailList) || + MailServices.ab.getDirectoryFromUID(cards[0].directoryUID)?.readOnly + ) { + separator.hidden = true; + menuitem.hidden = true; + return; + } + + separator.hidden = false; + menuitem.hidden = false; +} + +/** + * Handle the click event of the results tree (workaround for XUL tree + * bug 1331377). + * + * @param aEvent a click event + */ +function contactsListOnClick(aEvent) { + CommandUpdate_AddressBook(); + + let target = aEvent.target; + + // Left click on column header: Change sort direction. + if (target.localName == "treecol" && aEvent.button == 0) { + let sortDirection = + target.getAttribute("sortDirection") == kDefaultDescending + ? kDefaultAscending + : kDefaultDescending; + SortAndUpdateIndicators(target.id, sortDirection); + return; + } + // Any click on gAbResultsTree view (rows or blank space). + if (target.localName == "treechildren") { + let row = gAbResultsTree.getRowAt(aEvent.clientX, aEvent.clientY); + if (row < 0 || row >= gAbResultsTree.view.rowCount) { + // Any click on results tree whitespace. + if ((aEvent.detail == 1 && aEvent.button == 0) || aEvent.button == 2) { + // Single left click or any right click on results tree blank space: + // Clear selection. This also triggers on the first click of any + // double-click, but that's ok. MAC OS X doesn't return event.detail==1 + // for single right click, so we also let this trigger for the second + // click of right double-click. + gAbView.selection.clearSelection(); + } + } else if (aEvent.button == 0 && aEvent.detail == 2) { + // Any click on results tree rows. + // Double-click on a row: Go ahead and add the entry. + addSelectedAddresses("addr_to"); + } + } +} + +/** + * Appends the currently selected cards as new recipients in the composed message. + * + * @param recipientType Type of recipient, e.g. "addr_to". + */ +function addSelectedAddresses(recipientType) { + var cards = GetSelectedAbCards(); + + // Turn each card into a properly formatted address. + let addresses = cards.map(makeMailboxObjectFromCard).filter(addr => addr); + parent.addressRowAddRecipientsArray( + parent.document.querySelector( + `.address-row[data-recipienttype="${recipientType}"]` + ), + addresses + ); +} + +/** + * Open the address book tab and trigger the edit of the selected contact. + */ +function editSelectedAddress() { + let cards = GetSelectedAbCards(); + window.top.toAddressBook({ action: "edit", card: cards[0] }); +} + +function AddressBookMenuListChange(aValue) { + let searchInput = document.getElementById("peopleSearchInput"); + if (searchInput.value && !searchInput.showingSearchCriteria) { + onEnterInSearchBar(); + } else { + ChangeDirectoryByURI(aValue); + } + + // Hide the addressbook column if the selected addressbook isn't + // "All address books". Since the column is redundant in all other cases. + let abList = document.getElementById("addressbookList"); + let addrbookColumn = document.getElementById("addrbook"); + if (abList.value.startsWith(kAllDirectoryRoot + "?")) { + addrbookColumn.hidden = !gShowAbColumnInComposeSidebar; + addrbookColumn.removeAttribute("ignoreincolumnpicker"); + } else { + addrbookColumn.hidden = true; + addrbookColumn.setAttribute("ignoreincolumnpicker", "true"); + } + + CommandUpdate_AddressBook(); +} + +var mutationObs = null; + +function AbPanelLoad() { + if (location.search == "?focus") { + document.getElementById("peopleSearchInput").focus(); + } + + document.title = parent.document.getElementById("contactsTitle").value; + + // Get the URI of the directory to display. + let startupURI = Services.prefs.getCharPref("mail.addr_book.view.startupURI"); + // If the URI is a mailing list, use the parent directory instead, since + // mailing lists are not displayed here. + startupURI = startupURI.replace(/^(jsaddrbook:\/\/[\w\.-]*)\/.*$/, "$1"); + + let abPopup = document.getElementById("addressbookList"); + abPopup.value = startupURI; + + // If provided directory is not on abPopup, fall back to All Address Books. + if (!abPopup.selectedItem) { + abPopup.selectedIndex = 0; + } + + // Postpone the slow contacts load so that the sidebar document + // gets a chance to display quickly. + setTimeout(ChangeDirectoryByURI, 0, abPopup.value); + + mutationObs = new MutationObserver(function (aMutations) { + aMutations.forEach(function (mutation) { + if ( + getSelectedDirectoryURI() == kAllDirectoryRoot + "?" && + mutation.type == "attributes" && + mutation.attributeName == "hidden" + ) { + let curState = document.getElementById("addrbook").hidden; + gShowAbColumnInComposeSidebar = !curState; + } + }); + }); + + document.getElementById("addrbook").hidden = !gShowAbColumnInComposeSidebar; + + mutationObs.observe(document.getElementById("addrbook"), { + attributes: true, + childList: true, + }); +} + +function AbPanelUnload() { + mutationObs.disconnect(); + + // If there's no default startupURI, save the last used URI as new startupURI. + if (!Services.prefs.getBoolPref("mail.addr_book.view.startupURIisDefault")) { + Services.prefs.setCharPref( + "mail.addr_book.view.startupURI", + getSelectedDirectoryURI() + ); + } + + CloseAbView(); +} + +function AbResultsPaneDoubleClick(card) { + // double click for ab panel means "send mail to this person / list" + AbNewMessage(); +} + +function CommandUpdate_AddressBook() { + // Toggle disable state of to,cc,bcc buttons. + let disabled = GetNumSelectedCards() == 0 ? "true" : "false"; + document.getElementById("cmd_addrTo").setAttribute("disabled", disabled); + document.getElementById("cmd_addrCc").setAttribute("disabled", disabled); + document.getElementById("cmd_addrBcc").setAttribute("disabled", disabled); + + goUpdateCommand("cmd_delete"); +} + +/** + * Handle the onpopupshowing event of #sidebarAbContextMenu. + * Update the checkmark of #sidebarAbContext-startupDir menuitem when context + * menu opens, so as to always be in sync with changes from the main AB window. + */ +function onAbContextShowing() { + let startupItem = document.getElementById("sidebarAbContext-startupDir"); + if (Services.prefs.getBoolPref("mail.addr_book.view.startupURIisDefault")) { + let startupURI = Services.prefs.getCharPref( + "mail.addr_book.view.startupURI" + ); + startupItem.setAttribute( + "checked", + startupURI == getSelectedDirectoryURI() + ); + } else { + startupItem.setAttribute("checked", "false"); + } +} + +function onEnterInSearchBar() { + if (!gQueryURIFormat) { + // Get model query from pref. We don't want the query starting with "?" + // as we have to prefix "?and" to this format. + /* eslint-disable no-global-assign */ + gQueryURIFormat = getModelQuery("mail.addr_book.quicksearchquery.format"); + /* eslint-enable no-global-assign */ + } + + let searchURI = getSelectedDirectoryURI(); + let searchQuery; + let searchInput = document.getElementById("peopleSearchInput"); + + // Use helper method to split up search query to multi-word search + // query against multiple fields. + if (searchInput) { + let searchWords = getSearchTokens(searchInput.value); + searchQuery = generateQueryURI(gQueryURIFormat, searchWords); + } + + SetAbView(searchURI, searchQuery, searchInput ? searchInput.value : ""); +} + +/** + * Open a menupopup as a context menu + * + * @param aContextMenuID The ID of a menupopup to be shown as context menu + * @param aEvent The event which triggered this. + * @param positionArray An optional array containing the parameters for openPopup() method; + * if omitted, mouse pointer position will be used. + */ +function showContextMenu(aContextMenuID, aEvent, aPositionArray) { + let theContextMenu = document.getElementById(aContextMenuID); + if (!aPositionArray) { + aPositionArray = [null, "", aEvent.clientX, aEvent.clientY, true]; + } + theContextMenu.openPopup(...aPositionArray); +} + +/** + * Get the URI of the selected directory. + * + * @returns The URI of the currently selected directory + */ +function getSelectedDirectoryURI() { + return document.getElementById("addressbookList").value; +} + +function abToggleSelectedDirStartup() { + let selectedDirURI = getSelectedDirectoryURI(); + if (!selectedDirURI) { + return; + } + + let isDefault = Services.prefs.getBoolPref( + "mail.addr_book.view.startupURIisDefault" + ); + let startupURI = Services.prefs.getCharPref("mail.addr_book.view.startupURI"); + + if (isDefault && startupURI == selectedDirURI) { + // The current directory has been the default startup view directory; + // toggle that off now. So there's no default startup view directory any more. + Services.prefs.setBoolPref( + "mail.addr_book.view.startupURIisDefault", + false + ); + } else { + // The current directory will now be the default view + // when starting up the main AB window. + Services.prefs.setCharPref( + "mail.addr_book.view.startupURI", + selectedDirURI + ); + Services.prefs.setBoolPref("mail.addr_book.view.startupURIisDefault", true); + } + + // Update the checkbox in the menuitem. + goUpdateCommand("cmd_abToggleStartupDir"); +} + +function ChangeDirectoryByURI(uri = kPersonalAddressbookURI) { + SetAbView(uri); + + // Actively de-selecting if there are any pre-existing selections + // in the results list. + if (gAbView && gAbView.selection && gAbView.getCardFromRow(0)) { + gAbView.selection.clearSelection(); + } +} diff --git a/comm/mail/components/addrbook/content/abContactsPanel.xhtml b/comm/mail/components/addrbook/content/abContactsPanel.xhtml new file mode 100644 index 0000000000..18163eafda --- /dev/null +++ b/comm/mail/components/addrbook/content/abContactsPanel.xhtml @@ -0,0 +1,234 @@ + + + + + + + + + + + +%abResultsPaneDTD; + +%abContactsPanelDTD; + +%abMainWindowDTD; ]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
      +
    • +
      +
      +
      + +
      +
      +
    • +
    +
    + + +
    +
    +
    +
    + + +
    + + + + + + + + + + +
    + +
    + +
    + + +
    + + + + + + + + + + + +
    + + + + + + +
    + + +