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/mailnews/base/.eslintrc.js | 5 + comm/mailnews/base/content/.eslintrc.js | 5 + comm/mailnews/base/content/dateFormat.js | 227 + comm/mailnews/base/content/folder-menupopup.js | 1238 ++++ comm/mailnews/base/content/folderProps.js | 480 ++ comm/mailnews/base/content/folderProps.xhtml | 338 + comm/mailnews/base/content/jsTreeView.js | 239 + comm/mailnews/base/content/junkCommands.js | 449 ++ comm/mailnews/base/content/junkLog.js | 48 + comm/mailnews/base/content/junkLog.xhtml | 57 + comm/mailnews/base/content/markByDate.js | 120 + comm/mailnews/base/content/markByDate.xhtml | 79 + .../base/content/menulist-charsetpicker.js | 86 + comm/mailnews/base/content/msgAccountCentral.js | 238 + comm/mailnews/base/content/msgAccountCentral.xhtml | 309 + .../base/content/msgSelectOfflineFolders.js | 189 + .../base/content/msgSelectOfflineFolders.xhtml | 89 + comm/mailnews/base/content/msgSynchronize.js | 192 + comm/mailnews/base/content/msgSynchronize.xhtml | 76 + comm/mailnews/base/content/newFolderDialog.js | 82 + comm/mailnews/base/content/newFolderDialog.xhtml | 107 + comm/mailnews/base/content/newmailalert.js | 109 + comm/mailnews/base/content/newmailalert.xhtml | 55 + comm/mailnews/base/content/newsError.js | 48 + comm/mailnews/base/content/newsError.xhtml | 57 + comm/mailnews/base/content/renameFolderDialog.js | 43 + .../mailnews/base/content/renameFolderDialog.xhtml | 63 + comm/mailnews/base/content/retention.js | 52 + comm/mailnews/base/content/shutdownWindow.js | 97 + comm/mailnews/base/content/shutdownWindow.xhtml | 53 + comm/mailnews/base/content/subscribe.js | 496 ++ comm/mailnews/base/content/subscribe.xhtml | 235 + .../mailnews/base/content/virtualFolderListEdit.js | 206 + .../base/content/virtualFolderListEdit.xhtml | 84 + .../base/content/virtualFolderProperties.js | 383 + .../base/content/virtualFolderProperties.xhtml | 110 + comm/mailnews/base/moz.build | 11 + comm/mailnews/base/prefs/.eslintrc.js | 5 + comm/mailnews/base/prefs/content/AccountManager.js | 1949 +++++ .../base/prefs/content/AccountManager.xhtml | 171 + comm/mailnews/base/prefs/content/AccountWizard.js | 605 ++ .../base/prefs/content/AccountWizard.xhtml | 161 + comm/mailnews/base/prefs/content/SmtpServerEdit.js | 241 + .../base/prefs/content/SmtpServerEdit.xhtml | 208 + comm/mailnews/base/prefs/content/accountUtils.js | 369 + .../base/prefs/content/am-addressing.inc.xhtml | 125 + comm/mailnews/base/prefs/content/am-addressing.js | 68 + .../base/prefs/content/am-addressing.xhtml | 32 + .../base/prefs/content/am-archiveoptions.js | 74 + .../base/prefs/content/am-archiveoptions.xhtml | 136 + .../base/prefs/content/am-copies.inc.xhtml | 308 + comm/mailnews/base/prefs/content/am-copies.js | 555 ++ comm/mailnews/base/prefs/content/am-copies.xhtml | 35 + .../base/prefs/content/am-identities-list.js | 206 + .../base/prefs/content/am-identities-list.xhtml | 101 + .../base/prefs/content/am-identity-edit.js | 577 ++ .../base/prefs/content/am-identity-edit.xhtml | 239 + comm/mailnews/base/prefs/content/am-junk.js | 335 + comm/mailnews/base/prefs/content/am-junk.xhtml | 293 + comm/mailnews/base/prefs/content/am-main.js | 110 + comm/mailnews/base/prefs/content/am-main.xhtml | 344 + comm/mailnews/base/prefs/content/am-offline.js | 435 ++ comm/mailnews/base/prefs/content/am-offline.xhtml | 350 + comm/mailnews/base/prefs/content/am-prefs.js | 142 + .../base/prefs/content/am-server-advanced.js | 151 + .../base/prefs/content/am-server-advanced.xhtml | 222 + comm/mailnews/base/prefs/content/am-server.js | 615 ++ comm/mailnews/base/prefs/content/am-server.xhtml | 674 ++ .../prefs/content/am-serverwithnoidentities.js | 94 + .../prefs/content/am-serverwithnoidentities.xhtml | 152 + comm/mailnews/base/prefs/content/am-smtp.js | 277 + comm/mailnews/base/prefs/content/am-smtp.xhtml | 121 + comm/mailnews/base/prefs/content/amUtils.js | 275 + comm/mailnews/base/prefs/content/aw-accname.js | 28 + comm/mailnews/base/prefs/content/aw-done.js | 39 + comm/mailnews/base/prefs/content/aw-identity.js | 77 + comm/mailnews/base/prefs/content/aw-incoming.js | 42 + .../mailnews/base/prefs/content/converterDialog.js | 385 + .../base/prefs/content/converterDialog.xhtml | 53 + comm/mailnews/base/prefs/content/removeAccount.js | 168 + .../base/prefs/content/removeAccount.xhtml | 84 + comm/mailnews/base/public/MailNewsTypes.h | 40 + comm/mailnews/base/public/MailNewsTypes2.idl | 93 + comm/mailnews/base/public/moz.build | 80 + comm/mailnews/base/public/mozINewMailListener.idl | 22 + .../base/public/mozINewMailNotificationService.idl | 58 + comm/mailnews/base/public/msgCore.h | 210 + comm/mailnews/base/public/msgIOAuth2Module.idl | 56 + .../base/public/nsICopyMessageListener.idl | 24 + .../base/public/nsICopyMessageStreamListener.idl | 20 + comm/mailnews/base/public/nsIFolderListener.idl | 70 + .../base/public/nsIFolderLookupService.idl | 52 + .../base/public/nsIIncomingServerListener.idl | 32 + comm/mailnews/base/public/nsIMailAuthModule.idl | 40 + comm/mailnews/base/public/nsIMailChannel.idl | 109 + comm/mailnews/base/public/nsIMapiRegistry.idl | 50 + comm/mailnews/base/public/nsIMessenger.idl | 127 + comm/mailnews/base/public/nsIMessengerMigrator.idl | 14 + .../base/public/nsIMessengerOSIntegration.idl | 26 + .../base/public/nsIMessengerWindowService.idl | 17 + .../base/public/nsIMessengerWindowsIntegration.idl | 16 + comm/mailnews/base/public/nsIMsgAccount.idl | 86 + comm/mailnews/base/public/nsIMsgAccountManager.idl | 256 + comm/mailnews/base/public/nsIMsgAsyncPrompter.idl | 79 + comm/mailnews/base/public/nsIMsgBiffManager.idl | 18 + comm/mailnews/base/public/nsIMsgContentPolicy.idl | 35 + comm/mailnews/base/public/nsIMsgCopyService.idl | 116 + .../base/public/nsIMsgCopyServiceListener.idl | 55 + .../base/public/nsIMsgCustomColumnHandler.idl | 40 + comm/mailnews/base/public/nsIMsgDBView.idl | 558 ++ comm/mailnews/base/public/nsIMsgEnumerator.idl | 94 + comm/mailnews/base/public/nsIMsgFolder.idl | 877 +++ comm/mailnews/base/public/nsIMsgFolderCache.idl | 48 + .../base/public/nsIMsgFolderCacheElement.idl | 35 + .../mailnews/base/public/nsIMsgFolderCompactor.idl | 34 + comm/mailnews/base/public/nsIMsgFolderListener.idl | 227 + .../public/nsIMsgFolderNotificationService.idl | 119 + comm/mailnews/base/public/nsIMsgHdr.idl | 109 + comm/mailnews/base/public/nsIMsgIdentity.idl | 311 + comm/mailnews/base/public/nsIMsgIncomingServer.idl | 596 ++ comm/mailnews/base/public/nsIMsgMailNewsUrl.idl | 211 + comm/mailnews/base/public/nsIMsgMailSession.idl | 78 + comm/mailnews/base/public/nsIMsgMdnGenerator.idl | 70 + comm/mailnews/base/public/nsIMsgMessageService.idl | 226 + comm/mailnews/base/public/nsIMsgOfflineManager.idl | 22 + comm/mailnews/base/public/nsIMsgPluggableStore.idl | 335 + comm/mailnews/base/public/nsIMsgProgress.idl | 38 + .../mailnews/base/public/nsIMsgProtocolHandler.idl | 13 + comm/mailnews/base/public/nsIMsgProtocolInfo.idl | 97 + comm/mailnews/base/public/nsIMsgPurgeService.idl | 13 + comm/mailnews/base/public/nsIMsgShutdown.idl | 67 + comm/mailnews/base/public/nsIMsgStatusFeedback.idl | 18 + comm/mailnews/base/public/nsIMsgTagService.idl | 67 + comm/mailnews/base/public/nsIMsgThread.idl | 35 + .../base/public/nsIMsgUserFeedbackListener.idl | 28 + comm/mailnews/base/public/nsIMsgWindow.idl | 64 + comm/mailnews/base/public/nsISpamSettings.idl | 97 + .../base/public/nsIStatusBarBiffManager.idl | 13 + comm/mailnews/base/public/nsIStopwatch.idl | 44 + .../mailnews/base/public/nsISubscribableServer.idl | 74 + comm/mailnews/base/public/nsIUrlListener.idl | 32 + comm/mailnews/base/public/nsIUserInfo.idl | 32 + comm/mailnews/base/public/nsMsgFolderFlags.idl | 117 + comm/mailnews/base/public/nsMsgGroupnameFlags.h | 48 + comm/mailnews/base/public/nsMsgHeaderMasks.h | 53 + comm/mailnews/base/public/nsMsgLocalFolderHdrs.h | 47 + comm/mailnews/base/public/nsMsgMessageFlags.idl | 183 + comm/mailnews/base/src/ABQueryUtils.jsm | 159 + comm/mailnews/base/src/FolderLookupService.jsm | 163 + comm/mailnews/base/src/FolderUtils.jsm | 364 + comm/mailnews/base/src/HeaderReader.h | 305 + comm/mailnews/base/src/JXON.jsm | 159 + comm/mailnews/base/src/LineReader.h | 188 + comm/mailnews/base/src/LineReader.jsm | 68 + comm/mailnews/base/src/MailAuthenticator.jsm | 468 ++ comm/mailnews/base/src/MailChannel.sys.mjs | 71 + comm/mailnews/base/src/MailCryptoUtils.jsm | 76 + comm/mailnews/base/src/MailNewsDLF.cpp | 84 + comm/mailnews/base/src/MailNewsDLF.h | 37 + comm/mailnews/base/src/MailNotificationManager.jsm | 478 ++ comm/mailnews/base/src/MailNotificationService.jsm | 375 + comm/mailnews/base/src/MailServices.jsm | 169 + comm/mailnews/base/src/MailStringUtils.jsm | 102 + comm/mailnews/base/src/MailnewsLoadContextInfo.cpp | 51 + comm/mailnews/base/src/MailnewsLoadContextInfo.h | 32 + comm/mailnews/base/src/MailnewsMigrator.jsm | 352 + comm/mailnews/base/src/MsgAsyncPrompter.jsm | 621 ++ comm/mailnews/base/src/MsgDBCacheManager.jsm | 185 + comm/mailnews/base/src/MsgIncomingServer.jsm | 1268 ++++ comm/mailnews/base/src/MsgKeySet.jsm | 132 + comm/mailnews/base/src/MsgProtocolInfo.sys.mjs | 53 + comm/mailnews/base/src/OAuth2.jsm | 364 + comm/mailnews/base/src/OAuth2Module.jsm | 203 + comm/mailnews/base/src/OAuth2Providers.jsm | 259 + comm/mailnews/base/src/TemplateUtils.jsm | 90 + comm/mailnews/base/src/UrlListener.cpp | 22 + comm/mailnews/base/src/UrlListener.h | 72 + comm/mailnews/base/src/VirtualFolderWrapper.jsm | 257 + comm/mailnews/base/src/WinUnreadBadge.jsm | 246 + comm/mailnews/base/src/components.conf | 359 + comm/mailnews/base/src/converterWorker.js | 533 ++ comm/mailnews/base/src/hostnameUtils.jsm | 366 + comm/mailnews/base/src/mailstoreConverter.jsm | 339 + comm/mailnews/base/src/moz.build | 154 + comm/mailnews/base/src/nsCidProtocolHandler.cpp | 49 + comm/mailnews/base/src/nsCidProtocolHandler.h | 25 + .../base/src/nsCopyMessageStreamListener.cpp | 106 + .../base/src/nsCopyMessageStreamListener.h | 29 + comm/mailnews/base/src/nsImapMoveCoalescer.cpp | 198 + comm/mailnews/base/src/nsImapMoveCoalescer.h | 71 + comm/mailnews/base/src/nsMailAuthModule.cpp | 85 + comm/mailnews/base/src/nsMailAuthModule.h | 27 + comm/mailnews/base/src/nsMailChannel.cpp | 139 + comm/mailnews/base/src/nsMailChannel.h | 30 + comm/mailnews/base/src/nsMailDirProvider.cpp | 160 + comm/mailnews/base/src/nsMailDirProvider.h | 42 + comm/mailnews/base/src/nsMailDirServiceDefs.h | 31 + comm/mailnews/base/src/nsMessenger.cpp | 2446 +++++++ comm/mailnews/base/src/nsMessenger.h | 118 + comm/mailnews/base/src/nsMessengerBootstrap.cpp | 84 + comm/mailnews/base/src/nsMessengerBootstrap.h | 30 + comm/mailnews/base/src/nsMessengerOSXIntegration.h | 24 + .../mailnews/base/src/nsMessengerOSXIntegration.mm | 63 + .../base/src/nsMessengerUnixIntegration.cpp | 24 + .../mailnews/base/src/nsMessengerUnixIntegration.h | 22 + .../base/src/nsMessengerWinIntegration.cpp | 379 + comm/mailnews/base/src/nsMessengerWinIntegration.h | 39 + comm/mailnews/base/src/nsMsgAccount.cpp | 413 ++ comm/mailnews/base/src/nsMsgAccount.h | 34 + comm/mailnews/base/src/nsMsgAccountManager.cpp | 3546 ++++++++++ comm/mailnews/base/src/nsMsgAccountManager.h | 211 + comm/mailnews/base/src/nsMsgBiffManager.cpp | 341 + comm/mailnews/base/src/nsMsgBiffManager.h | 52 + comm/mailnews/base/src/nsMsgCompressIStream.cpp | 203 + comm/mailnews/base/src/nsMsgCompressIStream.h | 33 + comm/mailnews/base/src/nsMsgCompressOStream.cpp | 128 + comm/mailnews/base/src/nsMsgCompressOStream.h | 26 + comm/mailnews/base/src/nsMsgContentPolicy.cpp | 928 +++ comm/mailnews/base/src/nsMsgContentPolicy.h | 95 + comm/mailnews/base/src/nsMsgCopyService.cpp | 587 ++ comm/mailnews/base/src/nsMsgCopyService.h | 91 + comm/mailnews/base/src/nsMsgDBFolder.cpp | 5573 +++++++++++++++ comm/mailnews/base/src/nsMsgDBFolder.h | 366 + comm/mailnews/base/src/nsMsgDBView.cpp | 7411 ++++++++++++++++++++ comm/mailnews/base/src/nsMsgDBView.h | 558 ++ comm/mailnews/base/src/nsMsgEnumerator.cpp | 138 + comm/mailnews/base/src/nsMsgEnumerator.h | 45 + comm/mailnews/base/src/nsMsgFileStream.cpp | 190 + comm/mailnews/base/src/nsMsgFileStream.h | 35 + comm/mailnews/base/src/nsMsgFolderCache.cpp | 570 ++ comm/mailnews/base/src/nsMsgFolderCache.h | 60 + comm/mailnews/base/src/nsMsgFolderCompactor.cpp | 1391 ++++ comm/mailnews/base/src/nsMsgFolderCompactor.h | 48 + .../base/src/nsMsgFolderNotificationService.cpp | 174 + .../base/src/nsMsgFolderNotificationService.h | 46 + comm/mailnews/base/src/nsMsgGroupThread.cpp | 731 ++ comm/mailnews/base/src/nsMsgGroupThread.h | 88 + comm/mailnews/base/src/nsMsgGroupView.cpp | 941 +++ comm/mailnews/base/src/nsMsgGroupView.h | 78 + comm/mailnews/base/src/nsMsgI18N.cpp | 403 ++ comm/mailnews/base/src/nsMsgI18N.h | 138 + comm/mailnews/base/src/nsMsgIdentity.cpp | 645 ++ comm/mailnews/base/src/nsMsgIdentity.h | 87 + comm/mailnews/base/src/nsMsgIncomingServer.cpp | 2142 ++++++ comm/mailnews/base/src/nsMsgIncomingServer.h | 103 + comm/mailnews/base/src/nsMsgKeySet.cpp | 1412 ++++ comm/mailnews/base/src/nsMsgKeySet.h | 108 + comm/mailnews/base/src/nsMsgLineBuffer.cpp | 351 + comm/mailnews/base/src/nsMsgLineBuffer.h | 123 + comm/mailnews/base/src/nsMsgMailNewsUrl.cpp | 1070 +++ comm/mailnews/base/src/nsMsgMailNewsUrl.h | 142 + comm/mailnews/base/src/nsMsgMailSession.cpp | 671 ++ comm/mailnews/base/src/nsMsgMailSession.h | 110 + comm/mailnews/base/src/nsMsgOfflineManager.cpp | 352 + comm/mailnews/base/src/nsMsgOfflineManager.h | 79 + comm/mailnews/base/src/nsMsgProgress.cpp | 250 + comm/mailnews/base/src/nsMsgProgress.h | 45 + comm/mailnews/base/src/nsMsgProtocol.cpp | 1512 ++++ comm/mailnews/base/src/nsMsgProtocol.h | 263 + comm/mailnews/base/src/nsMsgPurgeService.cpp | 496 ++ comm/mailnews/base/src/nsMsgPurgeService.h | 51 + comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp | 806 +++ comm/mailnews/base/src/nsMsgQuickSearchDBView.h | 99 + comm/mailnews/base/src/nsMsgReadStateTxn.cpp | 43 + comm/mailnews/base/src/nsMsgReadStateTxn.h | 44 + comm/mailnews/base/src/nsMsgSearchDBView.cpp | 1344 ++++ comm/mailnews/base/src/nsMsgSearchDBView.h | 182 + comm/mailnews/base/src/nsMsgSpecialViews.cpp | 163 + comm/mailnews/base/src/nsMsgSpecialViews.h | 82 + comm/mailnews/base/src/nsMsgStatusFeedback.cpp | 261 + comm/mailnews/base/src/nsMsgStatusFeedback.h | 48 + comm/mailnews/base/src/nsMsgTagService.cpp | 458 ++ comm/mailnews/base/src/nsMsgTagService.h | 50 + comm/mailnews/base/src/nsMsgThreadedDBView.cpp | 899 +++ comm/mailnews/base/src/nsMsgThreadedDBView.h | 62 + comm/mailnews/base/src/nsMsgTxn.cpp | 247 + comm/mailnews/base/src/nsMsgTxn.h | 77 + comm/mailnews/base/src/nsMsgUtils.cpp | 1926 +++++ comm/mailnews/base/src/nsMsgUtils.h | 462 ++ comm/mailnews/base/src/nsMsgWindow.cpp | 327 + comm/mailnews/base/src/nsMsgWindow.h | 52 + comm/mailnews/base/src/nsMsgXFViewThread.cpp | 444 ++ comm/mailnews/base/src/nsMsgXFViewThread.h | 51 + .../base/src/nsMsgXFVirtualFolderDBView.cpp | 514 ++ .../mailnews/base/src/nsMsgXFVirtualFolderDBView.h | 70 + comm/mailnews/base/src/nsNewMailnewsURI.cpp | 155 + comm/mailnews/base/src/nsNewMailnewsURI.h | 15 + .../base/src/nsQuarantinedOutputStream.cpp | 234 + comm/mailnews/base/src/nsQuarantinedOutputStream.h | 72 + comm/mailnews/base/src/nsSpamSettings.cpp | 806 +++ comm/mailnews/base/src/nsSpamSettings.h | 68 + comm/mailnews/base/src/nsStatusBarBiffManager.cpp | 254 + comm/mailnews/base/src/nsStatusBarBiffManager.h | 37 + comm/mailnews/base/src/nsStopwatch.cpp | 164 + comm/mailnews/base/src/nsStopwatch.h | 51 + comm/mailnews/base/src/nsSubscribableServer.cpp | 867 +++ comm/mailnews/base/src/nsSubscribableServer.h | 59 + comm/mailnews/base/src/nsUserInfo.h | 23 + comm/mailnews/base/src/nsUserInfoMac.mm | 70 + comm/mailnews/base/src/nsUserInfoUnix.cpp | 124 + comm/mailnews/base/src/nsUserInfoWin.cpp | 99 + comm/mailnews/base/test/.eslintrc.js | 5 + comm/mailnews/base/test/TestMsgStripRE.cpp | 91 + comm/mailnews/base/test/gtest/TestHeaderReader.cpp | 205 + comm/mailnews/base/test/gtest/TestLineReader.cpp | 200 + comm/mailnews/base/test/gtest/moz.build | 15 + comm/mailnews/base/test/moz.build | 19 + comm/mailnews/base/test/unit/data/folderCache.json | 206 + comm/mailnews/base/test/unit/data/panacea.dat | 70 + .../mailnews/base/test/unit/data/panacea_empty.dat | 1 + .../mailnews/base/test/unit/data/remoteContent.sql | 41 + comm/mailnews/base/test/unit/head_mailbase.js | 23 + comm/mailnews/base/test/unit/nodelist_test.xml | 7 + .../base/test/unit/test_MsgIncomingServer.js | 224 + comm/mailnews/base/test/unit/test_MsgKeySet.js | 85 + comm/mailnews/base/test/unit/test_accountMgr.js | 102 + comm/mailnews/base/test/unit/test_accountMgr2.js | 199 + .../base/test/unit/test_accountMgrCustomTypes.js | 94 + .../test/unit/test_accountMgrMovedLocalFolders.js | 43 + .../base/test/unit/test_accountMgrRemoveDefault.js | 55 + .../base/test/unit/test_accountMigration.js | 184 + comm/mailnews/base/test/unit/test_acctRepair.js | 56 + comm/mailnews/base/test/unit/test_bccInDatabase.js | 54 + comm/mailnews/base/test/unit/test_bug428427.js | 249 + comm/mailnews/base/test/unit/test_bug434810.js | 27 + comm/mailnews/base/test/unit/test_bug471682.js | 118 + comm/mailnews/base/test/unit/test_bug514945.js | 40 + comm/mailnews/base/test/unit/test_closedDB.js | 104 + .../mailnews/base/test/unit/test_compactFailure.js | 134 + .../test/unit/test_converterDeferredAccount.js | 206 + comm/mailnews/base/test/unit/test_copyChaining.js | 109 + .../base/test/unit/test_copyToInvalidDB.js | 106 + comm/mailnews/base/test/unit/test_detachToFile.js | 159 + comm/mailnews/base/test/unit/test_emptyTrash.js | 174 + .../base/test/unit/test_fix_deferred_accounts.js | 59 + comm/mailnews/base/test/unit/test_folderCompact.js | 335 + .../mailnews/base/test/unit/test_folderCompact2.js | 300 + .../base/test/unit/test_folderLookupService.js | 83 + .../base/test/unit/test_folderStringProperties.js | 41 + .../mailnews/base/test/unit/test_formatFileSize.js | 144 + .../base/test/unit/test_getMsgTextFromStream.js | 88 + .../base/test/unit/test_headerFoldingInDatabase.js | 58 + comm/mailnews/base/test/unit/test_hostnameUtils.js | 276 + comm/mailnews/base/test/unit/test_identity.js | 69 + comm/mailnews/base/test/unit/test_imapPump.js | 81 + .../mailnews/base/test/unit/test_incomingServer.js | 99 + .../test/unit/test_inheritedFolderProperties.js | 183 + .../base/test/unit/test_junkingWhenDisabled.js | 176 + .../base/test/unit/test_loadVirtualFolders.js | 64 + comm/mailnews/base/test/unit/test_mailServices.js | 67 + .../base/test/unit/test_mailstoreConverter.js | 376 + .../mailnews/base/test/unit/test_mimemaltdetach.js | 160 + .../base/test/unit/test_newMailNotification.js | 203 + .../base/test/unit/test_nsIFolderListener.js | 45 + .../base/test/unit/test_nsIMsgContentPolicy.js | 68 + comm/mailnews/base/test/unit/test_nsIMsgFolder.js | 102 + .../base/test/unit/test_nsIMsgFolderCache.js | 228 + .../base/test/unit/test_nsIMsgFolderListener.js | 214 + .../test/unit/test_nsIMsgFolderListenerLocal.js | 444 ++ .../base/test/unit/test_nsIMsgTagService.js | 113 + .../base/test/unit/test_nsMailDirProvider.js | 23 + comm/mailnews/base/test/unit/test_nsMsgDBView.js | 1212 ++++ .../test/unit/test_nsMsgDBView_headerValues.js | 110 + .../base/test/unit/test_nsMsgMailSession_Alerts.js | 217 + .../test/unit/test_nsMsgMailSession_Listeners.js | 161 + .../base/test/unit/test_nsMsgTraitService.js | 130 + .../base/test/unit/test_postPluginFilters.js | 223 + comm/mailnews/base/test/unit/test_retention.js | 66 + comm/mailnews/base/test/unit/test_saveAs.js | 172 + .../base/test/unit/test_testsuite_base64.js | 22 + .../test/unit/test_testsuite_fakeserverAuth.js | 58 + .../unit/test_testsuite_fakeserver_imapd_gmail.js | 92 + ...est_testsuite_fakeserver_imapd_list-extended.js | 150 + .../base/test/unit/test_viewSortByAddresses.js | 144 + .../base/test/unit/test_virtualFolders1.js | 205 + .../base/test/unit/test_virtualFolders2.js | 90 + .../base/test/unit/test_virtualFolders3.js | 229 + comm/mailnews/base/test/unit/xpcshell-imap.ini | 8 + comm/mailnews/base/test/unit/xpcshell.ini | 77 + 379 files changed, 97165 insertions(+) create mode 100644 comm/mailnews/base/.eslintrc.js create mode 100644 comm/mailnews/base/content/.eslintrc.js create mode 100644 comm/mailnews/base/content/dateFormat.js create mode 100644 comm/mailnews/base/content/folder-menupopup.js create mode 100644 comm/mailnews/base/content/folderProps.js create mode 100644 comm/mailnews/base/content/folderProps.xhtml create mode 100644 comm/mailnews/base/content/jsTreeView.js create mode 100644 comm/mailnews/base/content/junkCommands.js create mode 100644 comm/mailnews/base/content/junkLog.js create mode 100644 comm/mailnews/base/content/junkLog.xhtml create mode 100644 comm/mailnews/base/content/markByDate.js create mode 100644 comm/mailnews/base/content/markByDate.xhtml create mode 100644 comm/mailnews/base/content/menulist-charsetpicker.js create mode 100644 comm/mailnews/base/content/msgAccountCentral.js create mode 100644 comm/mailnews/base/content/msgAccountCentral.xhtml create mode 100644 comm/mailnews/base/content/msgSelectOfflineFolders.js create mode 100644 comm/mailnews/base/content/msgSelectOfflineFolders.xhtml create mode 100644 comm/mailnews/base/content/msgSynchronize.js create mode 100644 comm/mailnews/base/content/msgSynchronize.xhtml create mode 100644 comm/mailnews/base/content/newFolderDialog.js create mode 100644 comm/mailnews/base/content/newFolderDialog.xhtml create mode 100644 comm/mailnews/base/content/newmailalert.js create mode 100644 comm/mailnews/base/content/newmailalert.xhtml create mode 100644 comm/mailnews/base/content/newsError.js create mode 100644 comm/mailnews/base/content/newsError.xhtml create mode 100644 comm/mailnews/base/content/renameFolderDialog.js create mode 100644 comm/mailnews/base/content/renameFolderDialog.xhtml create mode 100644 comm/mailnews/base/content/retention.js create mode 100644 comm/mailnews/base/content/shutdownWindow.js create mode 100644 comm/mailnews/base/content/shutdownWindow.xhtml create mode 100644 comm/mailnews/base/content/subscribe.js create mode 100644 comm/mailnews/base/content/subscribe.xhtml create mode 100644 comm/mailnews/base/content/virtualFolderListEdit.js create mode 100644 comm/mailnews/base/content/virtualFolderListEdit.xhtml create mode 100644 comm/mailnews/base/content/virtualFolderProperties.js create mode 100644 comm/mailnews/base/content/virtualFolderProperties.xhtml create mode 100644 comm/mailnews/base/moz.build create mode 100644 comm/mailnews/base/prefs/.eslintrc.js create mode 100644 comm/mailnews/base/prefs/content/AccountManager.js create mode 100644 comm/mailnews/base/prefs/content/AccountManager.xhtml create mode 100644 comm/mailnews/base/prefs/content/AccountWizard.js create mode 100644 comm/mailnews/base/prefs/content/AccountWizard.xhtml create mode 100644 comm/mailnews/base/prefs/content/SmtpServerEdit.js create mode 100644 comm/mailnews/base/prefs/content/SmtpServerEdit.xhtml create mode 100644 comm/mailnews/base/prefs/content/accountUtils.js create mode 100644 comm/mailnews/base/prefs/content/am-addressing.inc.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-addressing.js create mode 100644 comm/mailnews/base/prefs/content/am-addressing.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-archiveoptions.js create mode 100644 comm/mailnews/base/prefs/content/am-archiveoptions.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-copies.inc.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-copies.js create mode 100644 comm/mailnews/base/prefs/content/am-copies.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-identities-list.js create mode 100644 comm/mailnews/base/prefs/content/am-identities-list.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-identity-edit.js create mode 100644 comm/mailnews/base/prefs/content/am-identity-edit.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-junk.js create mode 100644 comm/mailnews/base/prefs/content/am-junk.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-main.js create mode 100644 comm/mailnews/base/prefs/content/am-main.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-offline.js create mode 100644 comm/mailnews/base/prefs/content/am-offline.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-prefs.js create mode 100644 comm/mailnews/base/prefs/content/am-server-advanced.js create mode 100644 comm/mailnews/base/prefs/content/am-server-advanced.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-server.js create mode 100644 comm/mailnews/base/prefs/content/am-server.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-serverwithnoidentities.js create mode 100644 comm/mailnews/base/prefs/content/am-serverwithnoidentities.xhtml create mode 100644 comm/mailnews/base/prefs/content/am-smtp.js create mode 100644 comm/mailnews/base/prefs/content/am-smtp.xhtml create mode 100644 comm/mailnews/base/prefs/content/amUtils.js create mode 100644 comm/mailnews/base/prefs/content/aw-accname.js create mode 100644 comm/mailnews/base/prefs/content/aw-done.js create mode 100644 comm/mailnews/base/prefs/content/aw-identity.js create mode 100644 comm/mailnews/base/prefs/content/aw-incoming.js create mode 100644 comm/mailnews/base/prefs/content/converterDialog.js create mode 100644 comm/mailnews/base/prefs/content/converterDialog.xhtml create mode 100644 comm/mailnews/base/prefs/content/removeAccount.js create mode 100644 comm/mailnews/base/prefs/content/removeAccount.xhtml create mode 100644 comm/mailnews/base/public/MailNewsTypes.h create mode 100644 comm/mailnews/base/public/MailNewsTypes2.idl create mode 100644 comm/mailnews/base/public/moz.build create mode 100644 comm/mailnews/base/public/mozINewMailListener.idl create mode 100644 comm/mailnews/base/public/mozINewMailNotificationService.idl create mode 100644 comm/mailnews/base/public/msgCore.h create mode 100644 comm/mailnews/base/public/msgIOAuth2Module.idl create mode 100644 comm/mailnews/base/public/nsICopyMessageListener.idl create mode 100644 comm/mailnews/base/public/nsICopyMessageStreamListener.idl create mode 100644 comm/mailnews/base/public/nsIFolderListener.idl create mode 100644 comm/mailnews/base/public/nsIFolderLookupService.idl create mode 100644 comm/mailnews/base/public/nsIIncomingServerListener.idl create mode 100644 comm/mailnews/base/public/nsIMailAuthModule.idl create mode 100644 comm/mailnews/base/public/nsIMailChannel.idl create mode 100644 comm/mailnews/base/public/nsIMapiRegistry.idl create mode 100644 comm/mailnews/base/public/nsIMessenger.idl create mode 100644 comm/mailnews/base/public/nsIMessengerMigrator.idl create mode 100644 comm/mailnews/base/public/nsIMessengerOSIntegration.idl create mode 100644 comm/mailnews/base/public/nsIMessengerWindowService.idl create mode 100644 comm/mailnews/base/public/nsIMessengerWindowsIntegration.idl create mode 100644 comm/mailnews/base/public/nsIMsgAccount.idl create mode 100644 comm/mailnews/base/public/nsIMsgAccountManager.idl create mode 100644 comm/mailnews/base/public/nsIMsgAsyncPrompter.idl create mode 100644 comm/mailnews/base/public/nsIMsgBiffManager.idl create mode 100644 comm/mailnews/base/public/nsIMsgContentPolicy.idl create mode 100644 comm/mailnews/base/public/nsIMsgCopyService.idl create mode 100644 comm/mailnews/base/public/nsIMsgCopyServiceListener.idl create mode 100644 comm/mailnews/base/public/nsIMsgCustomColumnHandler.idl create mode 100644 comm/mailnews/base/public/nsIMsgDBView.idl create mode 100644 comm/mailnews/base/public/nsIMsgEnumerator.idl create mode 100644 comm/mailnews/base/public/nsIMsgFolder.idl create mode 100644 comm/mailnews/base/public/nsIMsgFolderCache.idl create mode 100644 comm/mailnews/base/public/nsIMsgFolderCacheElement.idl create mode 100644 comm/mailnews/base/public/nsIMsgFolderCompactor.idl create mode 100644 comm/mailnews/base/public/nsIMsgFolderListener.idl create mode 100644 comm/mailnews/base/public/nsIMsgFolderNotificationService.idl create mode 100644 comm/mailnews/base/public/nsIMsgHdr.idl create mode 100644 comm/mailnews/base/public/nsIMsgIdentity.idl create mode 100644 comm/mailnews/base/public/nsIMsgIncomingServer.idl create mode 100644 comm/mailnews/base/public/nsIMsgMailNewsUrl.idl create mode 100644 comm/mailnews/base/public/nsIMsgMailSession.idl create mode 100644 comm/mailnews/base/public/nsIMsgMdnGenerator.idl create mode 100644 comm/mailnews/base/public/nsIMsgMessageService.idl create mode 100644 comm/mailnews/base/public/nsIMsgOfflineManager.idl create mode 100644 comm/mailnews/base/public/nsIMsgPluggableStore.idl create mode 100644 comm/mailnews/base/public/nsIMsgProgress.idl create mode 100644 comm/mailnews/base/public/nsIMsgProtocolHandler.idl create mode 100644 comm/mailnews/base/public/nsIMsgProtocolInfo.idl create mode 100644 comm/mailnews/base/public/nsIMsgPurgeService.idl create mode 100644 comm/mailnews/base/public/nsIMsgShutdown.idl create mode 100644 comm/mailnews/base/public/nsIMsgStatusFeedback.idl create mode 100644 comm/mailnews/base/public/nsIMsgTagService.idl create mode 100644 comm/mailnews/base/public/nsIMsgThread.idl create mode 100644 comm/mailnews/base/public/nsIMsgUserFeedbackListener.idl create mode 100644 comm/mailnews/base/public/nsIMsgWindow.idl create mode 100644 comm/mailnews/base/public/nsISpamSettings.idl create mode 100644 comm/mailnews/base/public/nsIStatusBarBiffManager.idl create mode 100644 comm/mailnews/base/public/nsIStopwatch.idl create mode 100644 comm/mailnews/base/public/nsISubscribableServer.idl create mode 100644 comm/mailnews/base/public/nsIUrlListener.idl create mode 100644 comm/mailnews/base/public/nsIUserInfo.idl create mode 100644 comm/mailnews/base/public/nsMsgFolderFlags.idl create mode 100644 comm/mailnews/base/public/nsMsgGroupnameFlags.h create mode 100644 comm/mailnews/base/public/nsMsgHeaderMasks.h create mode 100644 comm/mailnews/base/public/nsMsgLocalFolderHdrs.h create mode 100644 comm/mailnews/base/public/nsMsgMessageFlags.idl create mode 100644 comm/mailnews/base/src/ABQueryUtils.jsm create mode 100644 comm/mailnews/base/src/FolderLookupService.jsm create mode 100644 comm/mailnews/base/src/FolderUtils.jsm create mode 100644 comm/mailnews/base/src/HeaderReader.h create mode 100644 comm/mailnews/base/src/JXON.jsm create mode 100644 comm/mailnews/base/src/LineReader.h create mode 100644 comm/mailnews/base/src/LineReader.jsm create mode 100644 comm/mailnews/base/src/MailAuthenticator.jsm create mode 100644 comm/mailnews/base/src/MailChannel.sys.mjs create mode 100644 comm/mailnews/base/src/MailCryptoUtils.jsm create mode 100644 comm/mailnews/base/src/MailNewsDLF.cpp create mode 100644 comm/mailnews/base/src/MailNewsDLF.h create mode 100644 comm/mailnews/base/src/MailNotificationManager.jsm create mode 100644 comm/mailnews/base/src/MailNotificationService.jsm create mode 100644 comm/mailnews/base/src/MailServices.jsm create mode 100644 comm/mailnews/base/src/MailStringUtils.jsm create mode 100644 comm/mailnews/base/src/MailnewsLoadContextInfo.cpp create mode 100644 comm/mailnews/base/src/MailnewsLoadContextInfo.h create mode 100644 comm/mailnews/base/src/MailnewsMigrator.jsm create mode 100644 comm/mailnews/base/src/MsgAsyncPrompter.jsm create mode 100644 comm/mailnews/base/src/MsgDBCacheManager.jsm create mode 100644 comm/mailnews/base/src/MsgIncomingServer.jsm create mode 100644 comm/mailnews/base/src/MsgKeySet.jsm create mode 100644 comm/mailnews/base/src/MsgProtocolInfo.sys.mjs create mode 100644 comm/mailnews/base/src/OAuth2.jsm create mode 100644 comm/mailnews/base/src/OAuth2Module.jsm create mode 100644 comm/mailnews/base/src/OAuth2Providers.jsm create mode 100644 comm/mailnews/base/src/TemplateUtils.jsm create mode 100644 comm/mailnews/base/src/UrlListener.cpp create mode 100644 comm/mailnews/base/src/UrlListener.h create mode 100644 comm/mailnews/base/src/VirtualFolderWrapper.jsm create mode 100644 comm/mailnews/base/src/WinUnreadBadge.jsm create mode 100644 comm/mailnews/base/src/components.conf create mode 100644 comm/mailnews/base/src/converterWorker.js create mode 100644 comm/mailnews/base/src/hostnameUtils.jsm create mode 100644 comm/mailnews/base/src/mailstoreConverter.jsm create mode 100644 comm/mailnews/base/src/moz.build create mode 100644 comm/mailnews/base/src/nsCidProtocolHandler.cpp create mode 100644 comm/mailnews/base/src/nsCidProtocolHandler.h create mode 100644 comm/mailnews/base/src/nsCopyMessageStreamListener.cpp create mode 100644 comm/mailnews/base/src/nsCopyMessageStreamListener.h create mode 100644 comm/mailnews/base/src/nsImapMoveCoalescer.cpp create mode 100644 comm/mailnews/base/src/nsImapMoveCoalescer.h create mode 100644 comm/mailnews/base/src/nsMailAuthModule.cpp create mode 100644 comm/mailnews/base/src/nsMailAuthModule.h create mode 100644 comm/mailnews/base/src/nsMailChannel.cpp create mode 100644 comm/mailnews/base/src/nsMailChannel.h create mode 100644 comm/mailnews/base/src/nsMailDirProvider.cpp create mode 100644 comm/mailnews/base/src/nsMailDirProvider.h create mode 100644 comm/mailnews/base/src/nsMailDirServiceDefs.h create mode 100644 comm/mailnews/base/src/nsMessenger.cpp create mode 100644 comm/mailnews/base/src/nsMessenger.h create mode 100644 comm/mailnews/base/src/nsMessengerBootstrap.cpp create mode 100644 comm/mailnews/base/src/nsMessengerBootstrap.h create mode 100644 comm/mailnews/base/src/nsMessengerOSXIntegration.h create mode 100644 comm/mailnews/base/src/nsMessengerOSXIntegration.mm create mode 100644 comm/mailnews/base/src/nsMessengerUnixIntegration.cpp create mode 100644 comm/mailnews/base/src/nsMessengerUnixIntegration.h create mode 100644 comm/mailnews/base/src/nsMessengerWinIntegration.cpp create mode 100644 comm/mailnews/base/src/nsMessengerWinIntegration.h create mode 100644 comm/mailnews/base/src/nsMsgAccount.cpp create mode 100644 comm/mailnews/base/src/nsMsgAccount.h create mode 100644 comm/mailnews/base/src/nsMsgAccountManager.cpp create mode 100644 comm/mailnews/base/src/nsMsgAccountManager.h create mode 100644 comm/mailnews/base/src/nsMsgBiffManager.cpp create mode 100644 comm/mailnews/base/src/nsMsgBiffManager.h create mode 100644 comm/mailnews/base/src/nsMsgCompressIStream.cpp create mode 100644 comm/mailnews/base/src/nsMsgCompressIStream.h create mode 100644 comm/mailnews/base/src/nsMsgCompressOStream.cpp create mode 100644 comm/mailnews/base/src/nsMsgCompressOStream.h create mode 100644 comm/mailnews/base/src/nsMsgContentPolicy.cpp create mode 100644 comm/mailnews/base/src/nsMsgContentPolicy.h create mode 100644 comm/mailnews/base/src/nsMsgCopyService.cpp create mode 100644 comm/mailnews/base/src/nsMsgCopyService.h create mode 100644 comm/mailnews/base/src/nsMsgDBFolder.cpp create mode 100644 comm/mailnews/base/src/nsMsgDBFolder.h create mode 100644 comm/mailnews/base/src/nsMsgDBView.cpp create mode 100644 comm/mailnews/base/src/nsMsgDBView.h create mode 100644 comm/mailnews/base/src/nsMsgEnumerator.cpp create mode 100644 comm/mailnews/base/src/nsMsgEnumerator.h create mode 100644 comm/mailnews/base/src/nsMsgFileStream.cpp create mode 100644 comm/mailnews/base/src/nsMsgFileStream.h create mode 100644 comm/mailnews/base/src/nsMsgFolderCache.cpp create mode 100644 comm/mailnews/base/src/nsMsgFolderCache.h create mode 100644 comm/mailnews/base/src/nsMsgFolderCompactor.cpp create mode 100644 comm/mailnews/base/src/nsMsgFolderCompactor.h create mode 100644 comm/mailnews/base/src/nsMsgFolderNotificationService.cpp create mode 100644 comm/mailnews/base/src/nsMsgFolderNotificationService.h create mode 100644 comm/mailnews/base/src/nsMsgGroupThread.cpp create mode 100644 comm/mailnews/base/src/nsMsgGroupThread.h create mode 100644 comm/mailnews/base/src/nsMsgGroupView.cpp create mode 100644 comm/mailnews/base/src/nsMsgGroupView.h create mode 100644 comm/mailnews/base/src/nsMsgI18N.cpp create mode 100644 comm/mailnews/base/src/nsMsgI18N.h create mode 100644 comm/mailnews/base/src/nsMsgIdentity.cpp create mode 100644 comm/mailnews/base/src/nsMsgIdentity.h create mode 100644 comm/mailnews/base/src/nsMsgIncomingServer.cpp create mode 100644 comm/mailnews/base/src/nsMsgIncomingServer.h create mode 100644 comm/mailnews/base/src/nsMsgKeySet.cpp create mode 100644 comm/mailnews/base/src/nsMsgKeySet.h create mode 100644 comm/mailnews/base/src/nsMsgLineBuffer.cpp create mode 100644 comm/mailnews/base/src/nsMsgLineBuffer.h create mode 100644 comm/mailnews/base/src/nsMsgMailNewsUrl.cpp create mode 100644 comm/mailnews/base/src/nsMsgMailNewsUrl.h create mode 100644 comm/mailnews/base/src/nsMsgMailSession.cpp create mode 100644 comm/mailnews/base/src/nsMsgMailSession.h create mode 100644 comm/mailnews/base/src/nsMsgOfflineManager.cpp create mode 100644 comm/mailnews/base/src/nsMsgOfflineManager.h create mode 100644 comm/mailnews/base/src/nsMsgProgress.cpp create mode 100644 comm/mailnews/base/src/nsMsgProgress.h create mode 100644 comm/mailnews/base/src/nsMsgProtocol.cpp create mode 100644 comm/mailnews/base/src/nsMsgProtocol.h create mode 100644 comm/mailnews/base/src/nsMsgPurgeService.cpp create mode 100644 comm/mailnews/base/src/nsMsgPurgeService.h create mode 100644 comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp create mode 100644 comm/mailnews/base/src/nsMsgQuickSearchDBView.h create mode 100644 comm/mailnews/base/src/nsMsgReadStateTxn.cpp create mode 100644 comm/mailnews/base/src/nsMsgReadStateTxn.h create mode 100644 comm/mailnews/base/src/nsMsgSearchDBView.cpp create mode 100644 comm/mailnews/base/src/nsMsgSearchDBView.h create mode 100644 comm/mailnews/base/src/nsMsgSpecialViews.cpp create mode 100644 comm/mailnews/base/src/nsMsgSpecialViews.h create mode 100644 comm/mailnews/base/src/nsMsgStatusFeedback.cpp create mode 100644 comm/mailnews/base/src/nsMsgStatusFeedback.h create mode 100644 comm/mailnews/base/src/nsMsgTagService.cpp create mode 100644 comm/mailnews/base/src/nsMsgTagService.h create mode 100644 comm/mailnews/base/src/nsMsgThreadedDBView.cpp create mode 100644 comm/mailnews/base/src/nsMsgThreadedDBView.h create mode 100644 comm/mailnews/base/src/nsMsgTxn.cpp create mode 100644 comm/mailnews/base/src/nsMsgTxn.h create mode 100644 comm/mailnews/base/src/nsMsgUtils.cpp create mode 100644 comm/mailnews/base/src/nsMsgUtils.h create mode 100644 comm/mailnews/base/src/nsMsgWindow.cpp create mode 100644 comm/mailnews/base/src/nsMsgWindow.h create mode 100644 comm/mailnews/base/src/nsMsgXFViewThread.cpp create mode 100644 comm/mailnews/base/src/nsMsgXFViewThread.h create mode 100644 comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp create mode 100644 comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h create mode 100644 comm/mailnews/base/src/nsNewMailnewsURI.cpp create mode 100644 comm/mailnews/base/src/nsNewMailnewsURI.h create mode 100644 comm/mailnews/base/src/nsQuarantinedOutputStream.cpp create mode 100644 comm/mailnews/base/src/nsQuarantinedOutputStream.h create mode 100644 comm/mailnews/base/src/nsSpamSettings.cpp create mode 100644 comm/mailnews/base/src/nsSpamSettings.h create mode 100644 comm/mailnews/base/src/nsStatusBarBiffManager.cpp create mode 100644 comm/mailnews/base/src/nsStatusBarBiffManager.h create mode 100644 comm/mailnews/base/src/nsStopwatch.cpp create mode 100644 comm/mailnews/base/src/nsStopwatch.h create mode 100644 comm/mailnews/base/src/nsSubscribableServer.cpp create mode 100644 comm/mailnews/base/src/nsSubscribableServer.h create mode 100644 comm/mailnews/base/src/nsUserInfo.h create mode 100644 comm/mailnews/base/src/nsUserInfoMac.mm create mode 100644 comm/mailnews/base/src/nsUserInfoUnix.cpp create mode 100644 comm/mailnews/base/src/nsUserInfoWin.cpp create mode 100644 comm/mailnews/base/test/.eslintrc.js create mode 100644 comm/mailnews/base/test/TestMsgStripRE.cpp create mode 100644 comm/mailnews/base/test/gtest/TestHeaderReader.cpp create mode 100644 comm/mailnews/base/test/gtest/TestLineReader.cpp create mode 100644 comm/mailnews/base/test/gtest/moz.build create mode 100644 comm/mailnews/base/test/moz.build create mode 100644 comm/mailnews/base/test/unit/data/folderCache.json create mode 100644 comm/mailnews/base/test/unit/data/panacea.dat create mode 100644 comm/mailnews/base/test/unit/data/panacea_empty.dat create mode 100644 comm/mailnews/base/test/unit/data/remoteContent.sql create mode 100644 comm/mailnews/base/test/unit/head_mailbase.js create mode 100644 comm/mailnews/base/test/unit/nodelist_test.xml create mode 100644 comm/mailnews/base/test/unit/test_MsgIncomingServer.js create mode 100644 comm/mailnews/base/test/unit/test_MsgKeySet.js create mode 100644 comm/mailnews/base/test/unit/test_accountMgr.js create mode 100644 comm/mailnews/base/test/unit/test_accountMgr2.js create mode 100644 comm/mailnews/base/test/unit/test_accountMgrCustomTypes.js create mode 100644 comm/mailnews/base/test/unit/test_accountMgrMovedLocalFolders.js create mode 100644 comm/mailnews/base/test/unit/test_accountMgrRemoveDefault.js create mode 100644 comm/mailnews/base/test/unit/test_accountMigration.js create mode 100644 comm/mailnews/base/test/unit/test_acctRepair.js create mode 100644 comm/mailnews/base/test/unit/test_bccInDatabase.js create mode 100644 comm/mailnews/base/test/unit/test_bug428427.js create mode 100644 comm/mailnews/base/test/unit/test_bug434810.js create mode 100644 comm/mailnews/base/test/unit/test_bug471682.js create mode 100644 comm/mailnews/base/test/unit/test_bug514945.js create mode 100644 comm/mailnews/base/test/unit/test_closedDB.js create mode 100644 comm/mailnews/base/test/unit/test_compactFailure.js create mode 100644 comm/mailnews/base/test/unit/test_converterDeferredAccount.js create mode 100644 comm/mailnews/base/test/unit/test_copyChaining.js create mode 100644 comm/mailnews/base/test/unit/test_copyToInvalidDB.js create mode 100644 comm/mailnews/base/test/unit/test_detachToFile.js create mode 100644 comm/mailnews/base/test/unit/test_emptyTrash.js create mode 100644 comm/mailnews/base/test/unit/test_fix_deferred_accounts.js create mode 100644 comm/mailnews/base/test/unit/test_folderCompact.js create mode 100644 comm/mailnews/base/test/unit/test_folderCompact2.js create mode 100644 comm/mailnews/base/test/unit/test_folderLookupService.js create mode 100644 comm/mailnews/base/test/unit/test_folderStringProperties.js create mode 100644 comm/mailnews/base/test/unit/test_formatFileSize.js create mode 100644 comm/mailnews/base/test/unit/test_getMsgTextFromStream.js create mode 100644 comm/mailnews/base/test/unit/test_headerFoldingInDatabase.js create mode 100644 comm/mailnews/base/test/unit/test_hostnameUtils.js create mode 100644 comm/mailnews/base/test/unit/test_identity.js create mode 100644 comm/mailnews/base/test/unit/test_imapPump.js create mode 100644 comm/mailnews/base/test/unit/test_incomingServer.js create mode 100644 comm/mailnews/base/test/unit/test_inheritedFolderProperties.js create mode 100644 comm/mailnews/base/test/unit/test_junkingWhenDisabled.js create mode 100644 comm/mailnews/base/test/unit/test_loadVirtualFolders.js create mode 100644 comm/mailnews/base/test/unit/test_mailServices.js create mode 100644 comm/mailnews/base/test/unit/test_mailstoreConverter.js create mode 100644 comm/mailnews/base/test/unit/test_mimemaltdetach.js create mode 100644 comm/mailnews/base/test/unit/test_newMailNotification.js create mode 100644 comm/mailnews/base/test/unit/test_nsIFolderListener.js create mode 100644 comm/mailnews/base/test/unit/test_nsIMsgContentPolicy.js create mode 100644 comm/mailnews/base/test/unit/test_nsIMsgFolder.js create mode 100644 comm/mailnews/base/test/unit/test_nsIMsgFolderCache.js create mode 100644 comm/mailnews/base/test/unit/test_nsIMsgFolderListener.js create mode 100644 comm/mailnews/base/test/unit/test_nsIMsgFolderListenerLocal.js create mode 100644 comm/mailnews/base/test/unit/test_nsIMsgTagService.js create mode 100644 comm/mailnews/base/test/unit/test_nsMailDirProvider.js create mode 100644 comm/mailnews/base/test/unit/test_nsMsgDBView.js create mode 100644 comm/mailnews/base/test/unit/test_nsMsgDBView_headerValues.js create mode 100644 comm/mailnews/base/test/unit/test_nsMsgMailSession_Alerts.js create mode 100644 comm/mailnews/base/test/unit/test_nsMsgMailSession_Listeners.js create mode 100644 comm/mailnews/base/test/unit/test_nsMsgTraitService.js create mode 100644 comm/mailnews/base/test/unit/test_postPluginFilters.js create mode 100644 comm/mailnews/base/test/unit/test_retention.js create mode 100644 comm/mailnews/base/test/unit/test_saveAs.js create mode 100644 comm/mailnews/base/test/unit/test_testsuite_base64.js create mode 100644 comm/mailnews/base/test/unit/test_testsuite_fakeserverAuth.js create mode 100644 comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_gmail.js create mode 100644 comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_list-extended.js create mode 100644 comm/mailnews/base/test/unit/test_viewSortByAddresses.js create mode 100644 comm/mailnews/base/test/unit/test_virtualFolders1.js create mode 100644 comm/mailnews/base/test/unit/test_virtualFolders2.js create mode 100644 comm/mailnews/base/test/unit/test_virtualFolders3.js create mode 100644 comm/mailnews/base/test/unit/xpcshell-imap.ini create mode 100644 comm/mailnews/base/test/unit/xpcshell.ini (limited to 'comm/mailnews/base') diff --git a/comm/mailnews/base/.eslintrc.js b/comm/mailnews/base/.eslintrc.js new file mode 100644 index 0000000000..5816519fbb --- /dev/null +++ b/comm/mailnews/base/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/valid-jsdoc"], +}; diff --git a/comm/mailnews/base/content/.eslintrc.js b/comm/mailnews/base/content/.eslintrc.js new file mode 100644 index 0000000000..5816519fbb --- /dev/null +++ b/comm/mailnews/base/content/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/valid-jsdoc"], +}; diff --git a/comm/mailnews/base/content/dateFormat.js b/comm/mailnews/base/content/dateFormat.js new file mode 100644 index 0000000000..600aa873fa --- /dev/null +++ b/comm/mailnews/base/content/dateFormat.js @@ -0,0 +1,227 @@ +/* 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/. */ + +/* Utilities to show and parse user-entered date values used in filter and search rules. */ + +"use strict"; + +const formatYMD = 1; +const formatYDM = 2; +const formatMDY = 3; +const formatMYD = 4; +const formatDMY = 5; +const formatDYM = 6; +const formatMIN = 1; +const formatMAX = 6; + +var gSearchDateFormat = 0; +var gSearchDateSeparator; +var gSearchDateLeadingZeros; + +/** + * Get the short date format option of the current locale. + * This supports the common case which the date separator is + * either '/', '-', '.' and using Christian year. + */ +function initLocaleShortDateFormat() { + try { + const dateFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", + }); + var aDate = new Date(1999, 11, 2); + // Short formats can be space-separated, like 02 Dec 1999. + var dateString = dateFormatter + .format(aDate) + .replace(" 2", "2") + .replace(/ /g, "/"); + + // find out the separator + var possibleSeparators = "/-."; + var arrayOfStrings; + for (let i = 0; i < possibleSeparators.length; ++i) { + arrayOfStrings = dateString.split(possibleSeparators[i]); + if (arrayOfStrings.length == 3) { + gSearchDateSeparator = possibleSeparators[i]; + break; + } + } + + // check the format option + if (arrayOfStrings.length != 3) { + // no successful split + console.error( + `initLocaleShortDateFormat: could not analyze date format of ${dateString}, defaulting to yyyy/mm/dd` + ); + } else { + // The date will contain a zero if the system settings include leading zeros. + gSearchDateLeadingZeros = dateString.includes("0"); + + // Match 2 as number, since that will match both "2" and "02". + // Let's not look for 12 since it could be Dec instead. + if (arrayOfStrings[0] == 2) { + // 02.12.1999 or 02.1999.12 + gSearchDateFormat = arrayOfStrings[1] == "1999" ? formatDYM : formatDMY; + } else if (arrayOfStrings[1] == 2) { + // 12.02.1999 or 1999.02.12 + gSearchDateFormat = arrayOfStrings[0] == "1999" ? formatYDM : formatMDY; + } else { + // implies arrayOfStrings[2] == 2 + // 12.1999.02 or 1999.12.02 + gSearchDateFormat = arrayOfStrings[0] == "1999" ? formatYMD : formatMYD; + } + } + } catch (e) { + console.error("initLocaleShortDateFormat: caught an exception: " + e); + gSearchDateFormat = 0; + } +} + +function initializeSearchDateFormat() { + if (gSearchDateFormat > 0) { + return; + } + + // get a search date format option and a separator + try { + gSearchDateFormat = Services.prefs.getComplexValue( + "mailnews.search_date_format", + Ci.nsIPrefLocalizedString + ).data; + + gSearchDateFormat = parseInt(gSearchDateFormat); + + // if the option is 0 then try to use the format of the current locale + if (gSearchDateFormat == 0) { + initLocaleShortDateFormat(); + } else { + // initialize the search date format based on preferences + if (gSearchDateFormat < formatMIN || gSearchDateFormat > formatMAX) { + gSearchDateFormat = formatYMD; + } + + gSearchDateSeparator = Services.prefs.getComplexValue( + "mailnews.search_date_separator", + Ci.nsIPrefLocalizedString + ).data; + + gSearchDateLeadingZeros = + Services.prefs.getComplexValue( + "mailnews.search_date_leading_zeros", + Ci.nsIPrefLocalizedString + ).data == "true"; + } + } catch (e) { + console.error("initializeSearchDateFormat: caught an exception: " + e); + gSearchDateFormat = 0; + } + + if (gSearchDateFormat == 0) { + // Set to yyyy/mm/dd in case we couldn't determine in any way. + gSearchDateFormat = formatYMD; + gSearchDateSeparator = "/"; + gSearchDateLeadingZeros = true; + } +} + +function convertPRTimeToString(tm) { + var time = new Date(); + // PRTime is in microseconds, JavaScript time is in milliseconds + // so divide by 1000 when converting + time.setTime(tm / 1000); + + return convertDateToString(time); +} + +function convertDateToString(time) { + initializeSearchDateFormat(); + + var year = time.getFullYear(); + var month = time.getMonth() + 1; // since js month is 0-11 + if (gSearchDateLeadingZeros && month < 10) { + month = "0" + month; + } + var date = time.getDate(); // day + if (gSearchDateLeadingZeros && date < 10) { + date = "0" + date; + } + + var dateStr; + var sep = gSearchDateSeparator; + + switch (gSearchDateFormat) { + case formatYMD: + dateStr = year + sep + month + sep + date; + break; + case formatYDM: + dateStr = year + sep + date + sep + month; + break; + case formatMDY: + dateStr = month + sep + date + sep + year; + break; + case formatMYD: + dateStr = month + sep + year + sep + date; + break; + case formatDMY: + dateStr = date + sep + month + sep + year; + break; + case formatDYM: + dateStr = date + sep + year + sep + month; + break; + default: + dump("valid search date format option is 1-6\n"); + } + + return dateStr; +} + +function convertStringToPRTime(str) { + initializeSearchDateFormat(); + + var arrayOfStrings = str.split(gSearchDateSeparator); + var year, month, date; + + // set year, month, date based on the format option + switch (gSearchDateFormat) { + case formatYMD: + year = arrayOfStrings[0]; + month = arrayOfStrings[1]; + date = arrayOfStrings[2]; + break; + case formatYDM: + year = arrayOfStrings[0]; + month = arrayOfStrings[2]; + date = arrayOfStrings[1]; + break; + case formatMDY: + year = arrayOfStrings[2]; + month = arrayOfStrings[0]; + date = arrayOfStrings[1]; + break; + case formatMYD: + year = arrayOfStrings[1]; + month = arrayOfStrings[0]; + date = arrayOfStrings[2]; + break; + case formatDMY: + year = arrayOfStrings[2]; + month = arrayOfStrings[1]; + date = arrayOfStrings[0]; + break; + case formatDYM: + year = arrayOfStrings[1]; + month = arrayOfStrings[2]; + date = arrayOfStrings[0]; + break; + default: + dump("valid search date format option is 1-6\n"); + } + + month -= 1; // since js month is 0-11 + + var time = new Date(year, month, date); + + // JavaScript time is in milliseconds, PRTime is in microseconds + // so multiply by 1000 when converting + return time.getTime() * 1000; +} diff --git a/comm/mailnews/base/content/folder-menupopup.js b/comm/mailnews/base/content/folder-menupopup.js new file mode 100644 index 0000000000..61a912a4fd --- /dev/null +++ b/comm/mailnews/base/content/folder-menupopup.js @@ -0,0 +1,1238 @@ +/* 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"; + +/* globals MozElements MozXULElement PanelUI */ + +// This file implements `folder-menupopup` custom elements used in traditional +// menus. + +// Wrap in a block to prevent leaking to window scope. +{ + const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" + ); + + const LazyModules = {}; + + ChromeUtils.defineModuleGetter( + LazyModules, + "FeedUtils", + "resource:///modules/FeedUtils.jsm" + ); + ChromeUtils.defineModuleGetter( + LazyModules, + "FolderUtils", + "resource:///modules/FolderUtils.jsm" + ); + ChromeUtils.defineModuleGetter( + LazyModules, + "MailUtils", + "resource:///modules/MailUtils.jsm" + ); + + /** + * Creates an element, sets attributes on it, including always setting the + * "generated" attribute to "true", and returns the element. The "generated" + * attribute is used to determine which elements to remove when clearing + * the menu. + * + * @param {string} tagName - The tag name of the element to generate. + * @param {object} [attributes] - Optional attributes to set on the element. + * @param {object} [isObject] - The optional "is" object to use when creating + * the element, typically `{is: "folder-menupopup"}`. + */ + function generateElement(tagName, attributes, isObject) { + const element = document.createXULElement(tagName, isObject); + element.setAttribute("generated", "true"); + + if (attributes) { + Object.entries(attributes).forEach(([key, value]) => { + element.setAttribute(key, value); + }); + } + return element; + } + + /** + * A function to add shared code to the classes for the `folder-menupopup` + * custom element. Takes a "Base" class, and returns a class that extends + * the "Base" class. + * + * @param {Class} Base - A class to be extended with shared functionality. + * @returns {Class} A class that extends the first class. + */ + let FolderMenu = Base => + class extends Base { + constructor() { + super(); + + window.addEventListener( + "unload", + () => { + // Clean up when being destroyed. + this._removeListener(); + this._teardown(); + }, + { once: true } + ); + + // If non-null, the subFolders of this nsIMsgFolder will be used to + // populate this menu. If this is null, the menu will be populated + // using the root-folders for all accounts. + this._parentFolder = null; + + this._stringBundle = Services.strings.createBundle( + "chrome://messenger/locale/folderWidgets.properties" + ); + + // Various filtering modes can be used with this menu-binding. To use + // one of them, append the mode="foo" attribute to the element. When + // building the menu, we will then use this._filters[mode] as a filter + // function to eliminate folders that should not be shown. + // note: extensions should feel free to plug in here. + this._filters = { + // Returns true if messages can be filed in the folder. + filing(folder) { + if (!folder.server.canFileMessagesOnServer) { + return false; + } + + return folder.canFileMessages || folder.hasSubFolders; + }, + + // Returns true if we can get mail for this folder. (usually this just + // means the "root" fake folder). + getMail(folder) { + if (folder.isServer && folder.server.type != "none") { + return true; + } + if (folder.server.type == "nntp" || folder.server.type == "rss") { + return true; + } + return false; + }, + + // Returns true if we can add filters to this folder/account. + filters(folder) { + // We can always filter news. + if (folder.server.type == "nntp") { + return true; + } + + return folder.server.canHaveFilters; + }, + + subscribe(folder) { + return folder.canSubscribe; + }, + + newFolder(folder) { + return ( + folder.canCreateSubfolders && + folder.server.canCreateFoldersOnServer + ); + }, + + deferred(folder) { + return ( + folder.server.canCreateFoldersOnServer && !folder.supportsOffline + ); + }, + + // Folders that are not in a deferred account. + notDeferred(folder) { + let server = folder.server; + return !( + server instanceof Ci.nsIPop3IncomingServer && + server.deferredToAccount + ); + }, + + // Folders that can be searched. + search(folder) { + if ( + !folder.server.canSearchMessages || + folder.getFlag(Ci.nsMsgFolderFlags.Virtual) + ) { + return false; + } + return true; + }, + + // Folders that can subscribe feeds. + feeds(folder) { + if ( + folder.server.type != "rss" || + folder.getFlag(Ci.nsMsgFolderFlags.Trash) || + folder.getFlag(Ci.nsMsgFolderFlags.Virtual) + ) { + return false; + } + return true; + }, + + junk(folder) { + // Don't show servers (nntp & any others) which do not allow search or filing + // I don't really understand why canSearchMessages is needed, but it was included in + // earlier code, so I include it as well. + if ( + !folder.server.canFileMessagesOnServer || + !folder.server.canSearchMessages + ) { + return false; + } + // Show parents that might have usable subfolders, or usable folders. + return folder.hasSubFolders || folder.canFileMessages; + }, + }; + + // Is this list containing only servers (accounts) and no real folders? + this._serversOnly = true; + + /** + * Our listener to let us know when folders change/appear/disappear so + * we can know to rebuild ourselves. + * + * @implements {nsIFolderListener} + */ + this._listener = { + _menu: this, + _clearMenu(menu) { + // I'm not quite sure why this isn't always a function (bug 514445). + if (menu._teardown) { + menu._teardown(); + } + }, + + _setCssSelectorsForItem(item) { + const child = this._getChildForItem(item); + if (child) { + this._menu._setCssSelectors(child._folder, child); + } + }, + + _folderAddedOrRemoved(folder) { + if (this._filterFunction && !this._filterFunction(folder)) { + return; + } + // xxx we can optimize this later + this._clearMenu(this._menu); + }, + + onFolderAdded(parentFolder, child) { + this._folderAddedOrRemoved(child); + }, + onMessageAdded(parentFolder, msg) {}, + onFolderRemoved(parentFolder, child) { + this._folderAddedOrRemoved(child); + }, + onMessageRemoved(parentFolder, msg) {}, + + // xxx I stole this listener list from nsMsgFolderDatasource.cpp, but + // someone should really document what events are fired when, so that + // we make sure we're updating at the right times. + onFolderPropertyChanged(item, property, old, newItem) {}, + onFolderIntPropertyChanged(item, property, old, aNew) { + if (item instanceof Ci.nsIMsgFolder) { + if (property == "FolderFlag") { + if ( + this._menu.getAttribute("showFavorites") != "true" || + !this._menu._initializedSpecials.has("favorites") + ) { + return; + } + + if ( + (old & Ci.nsMsgFolderFlags.Favorite) != + (aNew & Ci.nsMsgFolderFlags.Favorite) + ) { + setTimeout(this._clearMenu, 0, this._menu); + } + } + } + this._setCssSelectorsForItem(item); + }, + onFolderBoolPropertyChanged(item, property, old, newItem) { + this._setCssSelectorsForItem(item); + }, + onFolderUnicharPropertyChanged(item, property, old, newItem) { + this._setCssSelectorsForItem(item); + }, + onFolderPropertyFlagChanged(item, property, old, newItem) {}, + + onFolderEvent(folder, eventName) { + if (eventName == "MRMTimeChanged") { + if ( + this._menu.getAttribute("showRecent") != "true" || + !this._menu._initializedSpecials.has("recent") || + !this._menu.childWrapper.firstElementChild + ) { + return; + } + + const recentMenuItem = this._menu.childWrapper.firstElementChild; + const recentSubMenu = + this._menu._getSubMenuForMenuItem(recentMenuItem); + + // If this folder is already in the recent menu, return. + if ( + !recentSubMenu || + this._getChildForItem(folder, recentSubMenu) + ) { + return; + } + } else if (eventName == "RenameCompleted") { + // Special casing folder renames here, since they require more work + // since sort-order may have changed. + if (!this._getChildForItem(folder)) { + return; + } + } else { + return; + } + // Folder renamed, or new recent folder, so rebuild. + setTimeout(this._clearMenu, 0, this._menu); + }, + + /** + * Helper function to check and see whether we have a menuitem for this + * particular nsIMsgFolder. + * + * @param {nsIMsgFolder} item - The folder to check. + * @param {Element} [menu] - Optional menu to look in, defaults to this._menu. + * @returns {Element|null} The menuitem for that folder, or null if no + * child for that folder exists. + */ + _getChildForItem(item, menu = this._menu) { + if ( + !menu || + !menu.childWrapper.hasChildNodes() || + !(item instanceof Ci.nsIMsgFolder) + ) { + return null; + } + for (let child of menu.childWrapper.children) { + if (child._folder && child._folder.URI == item.URI) { + return child; + } + } + return null; + }, + }; + + // True if we have already built our menu items and are now just + // listening for changes. + this._initialized = false; + + // A Set listing which of our special menus are already built. + // E.g. "recent", "favorites". + this._initializedSpecials = new Set(); + + // The format for displaying names of folders. + this._displayformat = null; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + // Call the connectedCallback of the "base" class this mixin class is extending. + super.connectedCallback(); + + // Get the displayformat if set. + if (this.parentNode && this.parentNode.localName == "menulist") { + this._displayformat = this.parentNode.getAttribute("displayformat"); + } + } + + set parentFolder(val) { + this._parentFolder = val; + this._teardown(); + } + + get parentFolder() { + return this._parentFolder; + } + + /** + * Make sure we remove our listener when the window is being destroyed + * or the widget torn down. + */ + _removeListener() { + if (!this._initialized) { + return; + } + MailServices.mailSession.RemoveFolderListener(this._listener); + } + + /** + * Call this if you do not know whether the menu items have been built, + * but know that they need to be built now if they haven't been yet. + */ + _ensureInitialized() { + if (this._initialized) { + return; + } + + // The excludeServers attribute is a comma separated list of server keys. + const excludeServers = this.hasAttribute("excludeServers") + ? this.getAttribute("excludeServers").split(",") + : []; + + // Extensions and other consumers can add to these modes too, see the + // note on the _filters field. (Note: empty strings ("") are falsy in JS.) + const mode = this.getAttribute("mode"); + + const filterFunction = mode ? this._filters[mode] : folder => true; + + const folders = this._getFolders( + this._parentFolder, + excludeServers, + mode ? filterFunction : null + ); + + this._listener._filterFunction = filterFunction; + + this._build(folders, mode); + + // Lastly, we add a listener to get notified of changes in the folder + // structure. + MailServices.mailSession.AddFolderListener( + this._listener, + Ci.nsIFolderListener.all + ); + + this._initialized = true; + } + + /** + * Get the folders that will appear in the menu. + * + * @param {Element} parentFolder - The parent menu popup/view element. + * @param {string[]} excludeServers - Server keys for the servers to exclude. + * @param {Function} [filterFunction] - Function for filtering the folders. + */ + _getFolders(parentFolder, excludeServers, filterFunction) { + let folders; + + // If we have a parent folder, just get the subFolders for that parent. + if (parentFolder) { + folders = parentFolder.subFolders; + } else { + // If we don't have a parent, then we assume we should build the + // top-level accounts. (Actually we build the fake root folders for + // those accounts.) + let accounts = LazyModules.FolderUtils.allAccountsSorted(true); + + // Now generate our folder list. Note that we'll special case this + // situation elsewhere, to avoid destroying the sort order we just made. + folders = accounts.map(acct => acct.incomingServer.rootFolder); + } + + if (filterFunction) { + folders = folders.filter(filterFunction); + } + + if (excludeServers.length > 0) { + folders = folders.filter( + folder => !excludeServers.includes(folder.server.key) + ); + } + return folders; + } + + /** + * Actually constructs the menu items based on the folders given. + * + * @param {nsIMsgFolder[]} folders - An array of nsIMsgFolders to use for building. + * @param {string} [mode] - The filtering mode. See comment on _filters field. + */ + _build(folders, mode) { + let globalInboxFolder = null; + + // See if this is the toplevel menu (usually with accounts). + if (!this._parentFolder) { + this._addTopLevelMenuItems(); + + // If we are showing the accounts for deferring, move Local Folders to the top. + if (mode == "deferred") { + globalInboxFolder = + MailServices.accounts.localFoldersServer.rootFolder; + let localFoldersIndex = folders.indexOf(globalInboxFolder); + if (localFoldersIndex != -1) { + folders.splice(localFoldersIndex, 1); + folders.unshift(globalInboxFolder); + } + } + // If we're the root of the folder hierarchy, then we actually don't + // want to sort the folders, but rather the accounts to which the + // folders belong. Since that sorting was already done, we don't need + // to do anything for that case here. + } else { + this._maybeAddParentFolderMenuItem(mode); + + // Sort the list of folders. We give first priority to the sortKey + // property if it is available, otherwise a case-insensitive + // comparison of names. + folders = folders.sort((a, b) => a.compareSortKeys(b)); + } + + this._addFoldersMenuItems(folders, mode, globalInboxFolder); + if (!this._parentFolder) { + this._addTopLevelBottomMenuItems(); + } + } + + /** + * Add menu items that only appear at top level, like "Recent". + */ + _addTopLevelMenuItems() { + const showRecent = this.getAttribute("showRecent") == "true"; + const showFavorites = this.getAttribute("showFavorites") == "true"; + + if (showRecent) { + this.childWrapper.appendChild( + this._buildSpecialMenu({ + special: "recent", + label: this.getAttribute("recentLabel"), + accessKey: this.getAttribute("recentAccessKey"), + }) + ); + } + if (showFavorites) { + this.childWrapper.appendChild( + this._buildSpecialMenu({ + special: "favorites", + label: this.getAttribute("favoritesLabel"), + accessKey: this.getAttribute("favoritesAccessKey"), + }) + ); + } + if (showRecent || showFavorites) { + this.childWrapper.appendChild(this._buildSeparator()); + } + } + + /** + * Add menu items that only appear at top level (but last), like "". + */ + _addTopLevelBottomMenuItems() { + if (this.getAttribute("showLast") != "true") { + return; + } + const folderURI = Services.prefs.getStringPref( + "mail.last_msg_movecopy_target_uri" + ); + const folder = + folderURI && LazyModules.MailUtils.getExistingFolder(folderURI); + if (!folder) { + return; + } + + this.childWrapper.appendChild(this._buildSeparator()); + const attributes = { + label: `${folder.prettyName} - ${folder.server.prettyName}`, + ...this._getCssSelectorAttributes(folder), + }; + this.childWrapper.appendChild(this._buildMenuItem(attributes, folder)); + } + + /** + * Populate a "recent" or "favorites" special submenu with either the + * recently used or favorite folders, to allow for easy access. + * + * @param {Element} menu - The menu or toolbarbutton element for which one + * wants to populate the special sub menu. + * @param {Element} submenu - The submenu element, typically a menupopup. + */ + _populateSpecialSubmenu(menu, submenu) { + let specialType = menu.getAttribute("special"); + if (this._initializedSpecials.has(specialType)) { + return; + } + + // Iterate through all folders in all accounts matching the current filter. + let specialFolders = MailServices.accounts.allFolders; + if (this._listener._filterFunction) { + specialFolders = specialFolders.filter( + this._listener._filterFunction + ); + } + + switch (specialType) { + case "recent": + // Find the most recently modified ones. + specialFolders = LazyModules.FolderUtils.getMostRecentFolders( + specialFolders, + Services.prefs.getIntPref("mail.folder_widget.max_recent"), + "MRMTime" + ); + break; + case "favorites": + specialFolders = specialFolders.filter(folder => + folder.getFlag(Ci.nsMsgFolderFlags.Favorite) + ); + break; + } + + // Cache the pretty names so that they do not need to be fetched + // with quadratic complexity when sorting by name. + let specialFoldersMap = specialFolders.map(folder => { + return { + folder, + name: folder.prettyName, + }; + }); + + // Because we're scanning across multiple accounts, we can end up with + // several folders with the same name. Find those dupes. + let dupeNames = new Set(); + for (let i = 0; i < specialFoldersMap.length; i++) { + for (let j = i + 1; j < specialFoldersMap.length; j++) { + if (specialFoldersMap[i].name == specialFoldersMap[j].name) { + dupeNames.add(specialFoldersMap[i].name); + } + } + } + + for (let folderItem of specialFoldersMap) { + // If this folder name appears multiple times in the recent list, + // append the server name to disambiguate. + // TODO: + // - maybe this could use verboseFolderFormat from messenger.properties + // instead of hardcoded " - ". + // - disambiguate folders with same name in same account + // (in different subtrees). + let label = folderItem.name; + if (dupeNames.has(label)) { + label += " - " + folderItem.folder.server.prettyName; + } + + folderItem.label = label; + } + + // Make sure the entries are sorted alphabetically. + specialFoldersMap.sort((a, b) => + LazyModules.FolderUtils.folderNameCompare(a.label, b.label) + ); + + // Create entries for each of the recent folders. + for (let folderItem of specialFoldersMap) { + let attributes = { + label: folderItem.label, + ...this._getCssSelectorAttributes(folderItem.folder), + }; + + submenu.childWrapper.appendChild( + this._buildMenuItem(attributes, folderItem.folder) + ); + } + + if (specialFoldersMap.length == 0) { + menu.setAttribute("disabled", "true"); + } + + this._initializedSpecials.add(specialType); + } + + /** + * Add a menu item that refers back to the parent folder when there is a + * showFileHereLabel attribute or no mode attribute. However don't + * add such a menu item if one of the following conditions is met: + * (-) There is no parent folder. + * (-) Folder is server and showAccountsFileHere is explicitly false. + * (-) Current folder has a mode, the parent folder can be selected, + * no messages can be filed into the parent folder (e.g. when the + * parent folder is a news group or news server) and the folder + * mode is not equal to newFolder. + * The menu item will have the value of the fileHereLabel attribute as + * label or if the attribute does not exist the name of the parent + * folder instead. + * + * @param {string} mode - The mode attribute. + */ + _maybeAddParentFolderMenuItem(mode) { + let folder = this._parentFolder; + if ( + folder && + (this.getAttribute("showFileHereLabel") == "true" || !mode) + ) { + let showAccountsFileHere = this.getAttribute("showAccountsFileHere"); + if ( + (!folder.isServer || showAccountsFileHere != "false") && + (!mode || + mode == "newFolder" || + folder.noSelect || + folder.canFileMessages || + showAccountsFileHere == "true") + ) { + let attributes = {}; + + if (this.hasAttribute("fileHereLabel")) { + attributes.label = this.getAttribute("fileHereLabel"); + attributes.accesskey = this.getAttribute("fileHereAccessKey"); + } else { + attributes.label = folder.prettyName; + Object.assign(attributes, this._getCssSelectorAttributes(folder)); + } + + if (folder.noSelect) { + attributes.disabled = "true"; + } + + this.childWrapper.appendChild( + this._buildMenuItem(attributes, folder) + ); + this.childWrapper.appendChild(this._buildSeparator()); + } + } + } + + /** + * Add menu items, one for each folder. + * + * @param {nsIMsgFolder[]} folders - Array of folder objects. + * @param {string} mode - The mode attribute. + * @param {nsIMsgFolder} globalInboxFolder - The root/global inbox folder. + */ + _addFoldersMenuItems(folders, mode, globalInboxFolder) { + // disableServers attribute is a comma separated list of server keys. + const disableServers = this.hasAttribute("disableServers") + ? this.getAttribute("disableServers").split(",") + : []; + + // We need to call this, or hasSubFolders will always return false. + // Remove this workaround when Bug 502900 is fixed. + LazyModules.MailUtils.discoverFolders(); + this._serversOnly = true; + + let [shouldExpand, labels] = this._getShouldExpandAndLabels(); + + for (let folder of folders) { + if (!folder.isServer) { + this._serversOnly = false; + } + + let attributes = { + label: this._getFolderLabel(mode, globalInboxFolder, folder), + ...this._getCssSelectorAttributes(folder), + }; + + if (disableServers.includes(folder.server.key)) { + attributes.disabled = "true"; + } + + if (!folder.hasSubFolders || !shouldExpand(folder.server.type)) { + // There are no subfolders, create a simple menu item. + this.childWrapper.appendChild( + this._buildMenuItem(attributes, folder) + ); + } else { + // There are subfolders, create a menu item with a submenu. + // xxx this is slightly problematic in that we haven't confirmed + // whether any of the subfolders will pass the filter. + + this._serversOnly = false; + + let submenuAttributes = {}; + + [ + "class", + "type", + "fileHereLabel", + "showFileHereLabel", + "oncommand", + "showAccountsFileHere", + "mode", + "disableServers", + "position", + ].forEach(attribute => { + if (this.hasAttribute(attribute)) { + submenuAttributes[attribute] = this.getAttribute(attribute); + } + }); + + const [menuItem, submenu] = this._buildMenuItemWithSubmenu( + attributes, + true, + folder, + submenuAttributes + ); + + // If there are labels, we add an item and separator to the submenu. + if (labels) { + const serverAttributes = { label: labels[folder.server.type] }; + + submenu.childWrapper.appendChild( + this._buildMenuItem(serverAttributes, folder, this) + ); + + submenu.childWrapper.appendChild(this._buildSeparator()); + } + + this.childWrapper.appendChild(menuItem); + } + } + } + + /** + * Return the label to use for a folder. + * + * @param {string} mode - The mode, e.g. "deferred". + * @param {nsIMsgFolder} globalInboxFolder - The root/global inbox folder. + * @param {nsIMsgFolder} folder - The folder for which we are getting a label. + * @returns {string} The label to use for the folder. + */ + _getFolderLabel(mode, globalInboxFolder, folder) { + if ( + mode == "deferred" && + folder.isServer && + folder.server.rootFolder == globalInboxFolder + ) { + return this._stringBundle.formatStringFromName("globalInbox", [ + folder.prettyName, + ]); + } + return folder.prettyName; + } + + /** + * Let the user have a list of subfolders for all account types, none of + * them, or only some of them. Returns an array containing a function that + * determines whether to show subfolders for a given account type, and an + * object mapping account types to label names (may be null). + * + * @returns {any[]} - An array; [0] is the shouldExpand function, [1] is + * the labels object. + */ + _getShouldExpandAndLabels() { + let shouldExpand; + let labels = null; + if ( + this.getAttribute("expandFolders") == "true" || + !this.hasAttribute("expandFolders") + ) { + shouldExpand = () => true; + } else if (this.getAttribute("expandFolders") == "false") { + shouldExpand = () => false; + } else { + // We want a subfolder list for only some servers. We also may need + // to create headers to select the servers. If so, then headlabels + // is a comma-delimited list of labels corresponding to the server + // types specified in expandFolders. + let types = this.getAttribute("expandFolders").split(/ *, */); + // Set the labels. labels[type] = label + if (this.hasAttribute("headlabels")) { + let labelNames = this.getAttribute("headlabels").split(/ *, */); + labels = {}; + // If the length isn't equal, don't give them any of the labels, + // since any combination will probably be wrong. + if (labelNames.length == types.length) { + for (let index in types) { + labels[types[index]] = labelNames[index]; + } + } + } + shouldExpand = e => types.includes(e); + } + return [shouldExpand, labels]; + } + + /** + * Set attributes on a menu, menuitem, or toolbarbutton element to allow + * for CSS styling. + * + * @param {nsIMsgFolder} folder - The folder that corresponds to the menu/menuitem. + * @param {Element} menuNode - The actual DOM node to set attributes on. + */ + _setCssSelectors(folder, menuNode) { + const cssAttributes = this._getCssSelectorAttributes(folder); + + Object.entries(cssAttributes).forEach(([key, value]) => + menuNode.setAttribute(key, value) + ); + } + + /** + * Returns attributes to be set on a menu, menuitem, or toolbarbutton + * element to allow for CSS styling. + * + * @param {nsIMsgFolder} folder - The folder that corresponds to the menu item. + * @returns {object} Contains the CSS selector attributes. + */ + _getCssSelectorAttributes(folder) { + let attributes = {}; + + // First the SpecialFolder attribute. + attributes.SpecialFolder = + LazyModules.FolderUtils.getSpecialFolderString(folder); + + // Now the biffState. + let biffStates = ["NewMail", "NoMail", "UnknownMail"]; + for (let state of biffStates) { + if (folder.biffState == Ci.nsIMsgFolder["nsMsgBiffState_" + state]) { + attributes.BiffState = state; + break; + } + } + + attributes.IsServer = folder.isServer; + attributes.IsSecure = folder.server.isSecure; + attributes.ServerType = folder.server.type; + attributes.IsFeedFolder = + !!LazyModules.FeedUtils.getFeedUrlsInFolder(folder); + + return attributes; + } + + /** + * This function returns a formatted display name for a menulist + * selected folder. The desired format is set as the 'displayformat' + * attribute of the folderpicker's , one of: + * 'name' (default) - Folder + * 'verbose' - Folder on Account + * 'path' - Account/Folder/Subfolder + * + * @param {nsIMsgFolder} folder - The folder that corresponds to the menu/menuitem. + * @returns {string} The display name. + */ + getDisplayName(folder) { + if (folder.isServer) { + return folder.prettyName; + } + + if (this._displayformat == "verbose") { + return this._stringBundle.formatStringFromName( + "verboseFolderFormat", + [folder.prettyName, folder.server.prettyName] + ); + } + + if (this._displayformat == "path") { + return ( + LazyModules.FeedUtils.getFolderPrettyPath(folder) || folder.name + ); + } + + return folder.name; + } + + /** + * Makes a given folder selected. + * TODO: This function does not work yet for the appmenu. However, as of + * June 2019, this functionality is not used in the appmenu. + * + * @param {nsIMsgFolder} inputFolder - The folder to select (if none, + * then Choose Folder). If inputFolder is not in this popup, but is + * instead a descendant of a member of the popup, that ancestor will be + * selected. + * @returns {boolean} Is true if any usable folder was found, otherwise false. + */ + selectFolder(inputFolder) { + // Set the label of the menulist element as if folder had been selected. + function setupParent(folder, menulist, noFolders) { + let menupopup = menulist.menupopup; + if (folder) { + menulist.setAttribute("label", menupopup.getDisplayName(folder)); + } else if (noFolders) { + menulist.setAttribute( + "label", + menupopup._stringBundle.GetStringFromName("noFolders") + ); + } else if (menupopup._serversOnly) { + menulist.setAttribute( + "label", + menupopup._stringBundle.GetStringFromName("chooseAccount") + ); + } else { + menulist.setAttribute( + "label", + menupopup._stringBundle.GetStringFromName("chooseFolder") + ); + } + menulist.setAttribute("value", folder ? folder.URI : ""); + menulist.setAttribute("IsServer", folder ? folder.isServer : false); + menulist.setAttribute( + "IsSecure", + folder ? folder.server.isSecure : false + ); + menulist.setAttribute( + "ServerType", + folder ? folder.server.type : "none" + ); + menulist.setAttribute( + "SpecialFolder", + folder + ? LazyModules.FolderUtils.getSpecialFolderString(folder) + : "none" + ); + menulist.setAttribute( + "IsFeedFolder", + Boolean(folder && LazyModules.FeedUtils.getFeedUrlsInFolder(folder)) + ); + } + + let folder; + if (inputFolder) { + for (let child of this.children) { + if ( + child && + child._folder && + !child.disabled && + (child._folder.URI == inputFolder.URI || + (child.tagName == "menu" && + child._folder.isAncestorOf(inputFolder))) + ) { + if (child._folder.URI == inputFolder.URI) { + this.parentNode.selectedItem = child; + } + folder = inputFolder; + break; + } + } + } + + // If the caller specified a folder to select and it was not + // found, or if the caller didn't pass a folder (meaning a logical + // and valid folder wasn't determined), don't blow up but reset + // attributes and set a nice Choose Folder label so the user may + // select a valid folder per the filter for this picker. If there are + // no children, then no folder passed the filter; disable the menulist + // as there's nothing to choose from. + let noFolders; + if (!this.childElementCount) { + this.parentNode.setAttribute("disabled", true); + noFolders = true; + } else { + this.parentNode.removeAttribute("disabled"); + noFolders = false; + } + + setupParent(folder, this.parentNode, noFolders); + return !!folder; + } + + /** + * Removes all menu items from this menu, removes their submenus. This + * function is called when a change that affects this menu is detected + * by the listener. + */ + _teardown() { + if (!this._initialized) { + return; + } + const children = this.childWrapper.children; + // We iterate in reverse order because children is live so it changes + // as we remove child nodes. + for (let i = children.length - 1; i >= 0; i--) { + const item = children[i]; + if (item.getAttribute("generated") != "true") { + continue; + } + const submenu = this._getSubMenuForMenuItem(item); + + if (submenu && "_teardown" in submenu) { + submenu._teardown(); + submenu.remove(); + } + item.remove(); + } + + this._removeListener(); + + this._initialized = false; + this._initializedSpecials.clear(); + } + }; + + /** + * The MozFolderMenupopup widget is used as a menupopup that contains menu + * items and submenus for all folders from every account (or some subset of + * folders and accounts). It is also used to provide a menu with a menuitem + * for each account. Each menu item gets displayed with the folder or + * account name and icon. + * + * @augments {MozElements.MozMenuPopup} + */ + let MozFolderMenuPopup = FolderMenu( + class extends MozElements.MozMenuPopup { + constructor() { + super(); + + // To improve performance, only build the menu when it is shown. + this.addEventListener( + "popupshowing", + event => { + this._ensureInitialized(); + }, + true + ); + + // Because the menu items in a panelview go inside a child vbox but are + // direct children of a menupopup, we set up a consistent way to append + // and access menu items for both cases. + this.childWrapper = this; + } + + connectedCallback() { + if (this.delayConnectedCallback()) { + return; + } + + this.setAttribute("is", "folder-menupopup"); + + // Find out if we are in a wrapper (customize toolbars mode is active). + let inWrapper = false; + let node = this; + while (XULElement.isInstance(node)) { + if (node.id.startsWith("wrapper-")) { + inWrapper = true; + break; + } + node = node.parentNode; + } + + if (!inWrapper) { + if (this.hasAttribute("original-width")) { + // If we were in a wrapper before and have a width stored, restore it now. + if (this.getAttribute("original-width") == "none") { + this.removeAttribute("width"); + } else { + this.setAttribute("width", this.getAttribute("original-width")); + } + + this.removeAttribute("original-width"); + } + + // If we are a child of a menulist, and we aren't in a wrapper, we + // need to build our content right away, otherwise the menulist + // won't have proper sizing. + if (this.parentNode && this.parentNode.localName == "menulist") { + this._ensureInitialized(); + } + } else { + // But if we're in a wrapper, remove our children, because we're + // getting re-created when the toolbar customization closes. + this._teardown(); + + // Store our current width and set a safe small width when we show + // in a wrapper. + if (!this.hasAttribute("original-width")) { + this.setAttribute( + "original-width", + this.hasAttribute("width") ? this.getAttribute("width") : "none" + ); + this.setAttribute("width", "100"); + } + } + } + + /** + * Given a menu item, return the menupopup that it opens. + * + * @param {Element} menu - The menu item, typically a `menu` element. + * @returns {Element|null} The `menupopup` element or null if none found. + */ + _getSubMenuForMenuItem(menu) { + return menu.querySelector("menupopup"); + } + + /** + * Returns a `menuseparator` element for use in a `menupopup`. + */ + _buildSeparator() { + return generateElement("menuseparator"); + } + + /** + * Builds a menu item (`menuitem`) element that does not open a submenu + * (i.e. not a `menu` element). + * + * @param {object} [attributes] - Attributes to set on the element. + * @param {nsIMsgFolder} folder - The folder associated with the menu item. + * @returns {Element} A `menuitem`. + */ + _buildMenuItem(attributes, folder) { + const menuitem = generateElement("menuitem", attributes); + menuitem.classList.add("folderMenuItem", "menuitem-iconic"); + menuitem._folder = folder; + return menuitem; + } + + /** + * Builds a menu item (`menu`) element and an associated submenu + * (`menupopup`) element. + * + * @param {object} attributes - Attributes to set on the `menu` element. + * @param {boolean} folderSubmenu - Whether the submenu is to be a + * `folder-menupopup` element. + * @param {nsIMsgFolder} [folder] - The folder associated with the menu item. + * @param {object} submenuAttributes - Attributes to set on the `menupopup` element. + * @returns {Element[]} Array containing the `menu` and + * `menupopup` elements. + */ + _buildMenuItemWithSubmenu( + attributes, + folderSubmenu, + folder, + submenuAttributes + ) { + const menu = generateElement("menu", attributes); + menu.classList.add("folderMenuItem", "menu-iconic"); + + const isObject = folderSubmenu ? { is: "folder-menupopup" } : null; + + const menupopup = generateElement( + "menupopup", + submenuAttributes, + isObject + ); + + if (folder) { + menu._folder = folder; + menupopup._parentFolder = folder; + } + + if (!menupopup.childWrapper) { + menupopup.childWrapper = menupopup; + } + + menu.appendChild(menupopup); + + return [menu, menupopup]; + } + + /** + * Build a special menu item (`menu`) and an empty submenu (`menupopup`) + * for it. The submenu is populated just before it is shown by + * `_populateSpecialSubmenu`. + * + * The submenu (`menupopup`) is just a standard element, not a custom + * element (`folder-menupopup`). + * + * @param {object} [attributes] - Attributes to set on the menu item element. + * @returns {Element} The menu item (`menu`) element. + */ + _buildSpecialMenu(attributes) { + const [menu, menupopup] = this._buildMenuItemWithSubmenu(attributes); + + menupopup.addEventListener( + "popupshowing", + event => { + this._populateSpecialSubmenu(menu, menupopup); + }, + { once: true } + ); + + return menu; + } + } + ); + + customElements.define("folder-menupopup", MozFolderMenuPopup, { + extends: "menupopup", + }); +} diff --git a/comm/mailnews/base/content/folderProps.js b/comm/mailnews/base/content/folderProps.js new file mode 100644 index 0000000000..324bfcac40 --- /dev/null +++ b/comm/mailnews/base/content/folderProps.js @@ -0,0 +1,480 @@ +/* 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 retention.js */ +/* global BigInt */ + +var { FolderTreeProperties } = ChromeUtils.import( + "resource:///modules/FolderTreeProperties.jsm" +); +var { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm"); + +var gMsgFolder; +var gLockedPref = null; + +var gDefaultColor = ""; + +window.addEventListener("load", folderPropsOnLoad); +document.addEventListener("dialogaccept", folderPropsOKButton); +document.addEventListener("dialogcancel", folderCancelButton); + +/** + * The folderPropsSink is the class that gets notified of an imap folder's + * properties. + * + * @implements {nsIMsgImapFolderProps} + */ +var gFolderPropsSink = { + setFolderType(folderTypeString) { + var typeLabel = document.getElementById("folderType.text"); + if (typeLabel) { + typeLabel.setAttribute("value", folderTypeString); + } + // get the element for the folder type label and set value on it. + }, + + setFolderTypeDescription(folderDescription) { + var folderTypeLabel = document.getElementById("folderDescription.text"); + if (folderTypeLabel) { + folderTypeLabel.setAttribute("value", folderDescription); + } + }, + + setFolderPermissions(folderPermissions) { + var permissionsLabel = document.getElementById("folderPermissions.text"); + var descTextNode = document.createTextNode(folderPermissions); + permissionsLabel.appendChild(descTextNode); + }, + + serverDoesntSupportACL() { + var typeLabel = document.getElementById("folderTypeLabel"); + if (typeLabel) { + typeLabel.setAttribute("hidden", "true"); + } + var permissionsLabel = document.getElementById("permissionsDescLabel"); + if (permissionsLabel) { + permissionsLabel.setAttribute("hidden", "true"); + } + }, + + setQuotaStatus(folderQuotaStatus) { + var quotaStatusLabel = document.getElementById("folderQuotaStatus"); + if (quotaStatusLabel) { + quotaStatusLabel.setAttribute("value", folderQuotaStatus); + } + }, + + showQuotaData(showData) { + var quotaStatusLabel = document.getElementById("folderQuotaStatus"); + var folderQuotaData = document.getElementById("folderQuotaData"); + + if (quotaStatusLabel && folderQuotaData) { + quotaStatusLabel.hidden = showData; + folderQuotaData.hidden = !showData; + } + }, + + setQuotaData(folderQuota) { + let quotaDetails = document.getElementById("quotaDetails"); + let messenger = Cc["@mozilla.org/messenger;1"].createInstance( + Ci.nsIMessenger + ); + + for (let quota of folderQuota) { + let li = document.createElement("li"); + let name = document.createElement("span"); + name.textContent = quota.name; + li.appendChild(name); + + let progress = document.createElement("progress"); + progress.classList.add("quota-percentage"); + progress.setAttribute("value", quota.usage); + progress.setAttribute("max", quota.limit); + + li.appendChild(progress); + + let percentage = document.createElement("span"); + document.l10n.setAttributes(percentage, "quota-percent-used", { + percent: Number((100n * BigInt(quota.usage)) / BigInt(quota.limit)), + }); + li.appendChild(percentage); + + li.appendChild(document.createTextNode(" — ")); + + let details = document.createElement("span"); + if (/STORAGE/i.test(quota.name)) { + let usage = messenger.formatFileSize(quota.usage * 1024); + let limit = messenger.formatFileSize(quota.limit * 1024); + details.textContent = `${usage} / ${limit}`; + } else { + details.textContent = `${quota.usage} / ${quota.limit}`; + } + li.appendChild(details); + + quotaDetails.appendChild(li); + } + }, +}; + +function doEnabling() { + var nameTextbox = document.getElementById("name"); + document.querySelector("dialog").getButton("accept").disabled = + !nameTextbox.value; +} + +function folderPropsOKButton(event) { + if (gMsgFolder) { + if ( + document.getElementById("offline.selectForOfflineFolder").checked || + document.getElementById("offline.selectForOfflineNewsgroup").checked + ) { + gMsgFolder.setFlag(Ci.nsMsgFolderFlags.Offline); + } else { + gMsgFolder.clearFlag(Ci.nsMsgFolderFlags.Offline); + } + + if (document.getElementById("folderCheckForNewMessages").checked) { + gMsgFolder.setFlag(Ci.nsMsgFolderFlags.CheckNew); + } else { + gMsgFolder.clearFlag(Ci.nsMsgFolderFlags.CheckNew); + } + + let glodaCheckbox = document.getElementById("folderIncludeInGlobalSearch"); + if (!glodaCheckbox.hidden) { + if (glodaCheckbox.checked) { + // We pass true here so that folders such as trash and junk can still + // have a priority set. + Gloda.resetFolderIndexingPriority(gMsgFolder, true); + } else { + Gloda.setFolderIndexingPriority( + gMsgFolder, + Gloda.getFolderForFolder(gMsgFolder).kIndexingNeverPriority + ); + } + } + + var retentionSettings = saveCommonRetentionSettings( + gMsgFolder.retentionSettings + ); + retentionSettings.useServerDefaults = document.getElementById( + "retention.useDefault" + ).checked; + gMsgFolder.retentionSettings = retentionSettings; + + let color = document.getElementById("color").value; + if (color == gDefaultColor) { + color = undefined; + } + FolderTreeProperties.setColor(gMsgFolder.URI, color); + // Tell 3-pane tabs to update the folder's color. + Services.obs.notifyObservers(gMsgFolder, "folder-color-changed", color); + } + + try { + // This throws an exception when an illegal folder name was entered. + top.okCallback( + document.getElementById("name").value, + window.arguments[0].name + ); + } catch (e) { + event.preventDefault(); + } +} + +function folderCancelButton(event) { + // Clear any previewed color. + Services.obs.notifyObservers(gMsgFolder, "folder-color-preview"); +} + +function folderPropsOnLoad() { + let styles = getComputedStyle(document.body); + let folderColors = { + Inbox: styles.getPropertyValue("--folder-color-inbox"), + Sent: styles.getPropertyValue("--folder-color-sent"), + Outbox: styles.getPropertyValue("--folder-color-outbox"), + Drafts: styles.getPropertyValue("--folder-color-draft"), + Trash: styles.getPropertyValue("--folder-color-trash"), + Archive: styles.getPropertyValue("--folder-color-archive"), + Templates: styles.getPropertyValue("--folder-color-template"), + Spam: styles.getPropertyValue("--folder-color-spam"), + Virtual: styles.getPropertyValue("--folder-color-folder-filter"), + RSS: styles.getPropertyValue("--folder-color-rss"), + Newsgroup: styles.getPropertyValue("--folder-color-newsletter"), + }; + gDefaultColor = styles.getPropertyValue("--folder-color-folder"); + + // look in arguments[0] for parameters + if (window.arguments && window.arguments[0]) { + if (window.arguments[0].title) { + document.title = window.arguments[0].title; + } + if (window.arguments[0].okCallback) { + top.okCallback = window.arguments[0].okCallback; + } + } + + if (window.arguments[0].folder) { + // Fill in folder name, based on what they selected in the folder pane. + gMsgFolder = window.arguments[0].folder; + } + + if (window.arguments[0].name) { + // Initialize name textbox with the given name and remember this + // value so we can tell whether the folder needs to be renamed + // when the dialog is accepted. + var nameTextbox = document.getElementById("name"); + nameTextbox.value = window.arguments[0].name; + } + + const serverType = window.arguments[0].serverType; + + // Do this first, because of gloda we may want to override some of the hidden + // statuses. + hideShowControls(serverType); + + if (gMsgFolder) { + // We really need a functioning database, so we'll detect problems + // and create one if we have to. + try { + gMsgFolder.msgDatabase; + } catch (e) { + gMsgFolder.updateFolder(window.arguments[0].msgWindow); + } + + // Check the current folder name against known folder names to set the + // correct default color, if needed. + let selectedFolderName = ""; + + switch (window.arguments[0].serverType) { + case "rss": + selectedFolderName = "RSS"; + break; + case "nntp": + selectedFolderName = "Newsgroup"; + break; + default: + selectedFolderName = window.arguments[0].name; + break; + } + + if (Object.keys(folderColors).includes(selectedFolderName)) { + gDefaultColor = folderColors[selectedFolderName]; + } + + let colorInput = document.getElementById("color"); + colorInput.value = + FolderTreeProperties.getColor(gMsgFolder.URI) || gDefaultColor; + colorInput.addEventListener("input", event => { + // Preview the chosen color. + Services.obs.notifyObservers( + gMsgFolder, + "folder-color-preview", + colorInput.value + ); + }); + let resetColorButton = document.getElementById("resetColor"); + resetColorButton.addEventListener("click", function () { + colorInput.value = gDefaultColor; + // Preview the default color. + Services.obs.notifyObservers( + gMsgFolder, + "folder-color-preview", + gDefaultColor + ); + }); + + var locationTextbox = document.getElementById("location"); + + // Decode the displayed mailbox:// URL as it's useful primarily for debugging, + // whereas imap and news urls are sent around. + locationTextbox.value = + serverType == "imap" || serverType == "nntp" + ? gMsgFolder.folderURL + : decodeURI(gMsgFolder.folderURL); + + if (gMsgFolder.canRename) { + document.getElementById("name").removeAttribute("readonly"); + } + + if (gMsgFolder.getFlag(Ci.nsMsgFolderFlags.Offline)) { + if (serverType == "imap" || serverType == "pop3") { + document.getElementById( + "offline.selectForOfflineFolder" + ).checked = true; + } + + if (serverType == "nntp") { + document.getElementById( + "offline.selectForOfflineNewsgroup" + ).checked = true; + } + } else { + if (serverType == "imap" || serverType == "pop3") { + document.getElementById( + "offline.selectForOfflineFolder" + ).checked = false; + } + + if (serverType == "nntp") { + document.getElementById( + "offline.selectForOfflineNewsgroup" + ).checked = false; + } + } + + // set check for new mail checkbox + document.getElementById("folderCheckForNewMessages").checked = + gMsgFolder.getFlag(Ci.nsMsgFolderFlags.CheckNew); + + // if gloda indexing is off, hide the related checkbox + var glodaCheckbox = document.getElementById("folderIncludeInGlobalSearch"); + var glodaEnabled = Services.prefs.getBoolPref( + "mailnews.database.global.indexer.enabled" + ); + if ( + !glodaEnabled || + gMsgFolder.flags & + (Ci.nsMsgFolderFlags.Queue | Ci.nsMsgFolderFlags.Newsgroup) + ) { + glodaCheckbox.hidden = true; + } else { + // otherwise, the user can choose whether this file gets indexed + let glodaFolder = Gloda.getFolderForFolder(gMsgFolder); + glodaCheckbox.checked = + glodaFolder.indexingPriority != glodaFolder.kIndexingNeverPriority; + } + } + + if (serverType == "imap") { + let imapFolder = gMsgFolder.QueryInterface(Ci.nsIMsgImapMailFolder); + imapFolder.fillInFolderProps(gFolderPropsSink); + + let users = [...imapFolder.getOtherUsersWithAccess()]; + if (users.length) { + document.getElementById("folderOtherUsers").hidden = false; + document.getElementById("folderOtherUsersText").textContent = + users.join(", "); + } + + // Disable "Repair Folder" when offline as that would cause the offline store + // to get deleted and redownloaded. + document.getElementById("folderRebuildSummaryButton").disabled = + gMsgFolder.supportsOffline && Services.io.offline; + } + + var retentionSettings = gMsgFolder.retentionSettings; + initCommonRetentionSettings(retentionSettings); + document.getElementById("retention.useDefault").checked = + retentionSettings.useServerDefaults; + + // set folder sizes + let numberOfMsgs = gMsgFolder.getTotalMessages(false); + if (numberOfMsgs >= 0) { + document.getElementById("numberOfMessages").value = numberOfMsgs; + } + + try { + let sizeOnDisk = Cc["@mozilla.org/messenger;1"] + .createInstance(Ci.nsIMessenger) + .formatFileSize(gMsgFolder.sizeOnDisk, true); + document.getElementById("sizeOnDisk").value = sizeOnDisk; + } catch (e) {} + + onCheckKeepMsg(); + onUseDefaultRetentionSettings(); + + // select the initial tab + if (window.arguments[0].tabID) { + document.getElementById("folderPropTabBox").selectedTab = + document.getElementById(window.arguments[0].tabID); + } +} + +function hideShowControls(serverType) { + let controls = document.querySelectorAll("[hidefor]"); + var len = controls.length; + for (var i = 0; i < len; i++) { + var control = controls[i]; + var hideFor = control.getAttribute("hidefor"); + if (!hideFor) { + throw new Error("hidefor empty"); + } + + // hide unsupported server type + // adding support for hiding multiple server types using hideFor="server1,server2" + var hideForBool = false; + var hideForTokens = hideFor.split(","); + for (var j = 0; j < hideForTokens.length; j++) { + if (hideForTokens[j] == serverType) { + hideForBool = true; + break; + } + } + control.hidden = hideForBool; + } + + // hide the privileges button if the imap folder doesn't have an admin url + // maybe should leave this hidden by default and only show it in this case instead + try { + var imapFolder = gMsgFolder.QueryInterface(Ci.nsIMsgImapMailFolder); + if (imapFolder) { + var privilegesButton = document.getElementById("imap.FolderPrivileges"); + if (privilegesButton) { + if (!imapFolder.hasAdminUrl) { + privilegesButton.setAttribute("hidden", "true"); + } + } + } + } catch (ex) {} + + if (gMsgFolder) { + // Hide "check for new mail" checkbox if this is an Inbox. + if (gMsgFolder.getFlag(Ci.nsMsgFolderFlags.Inbox)) { + document.getElementById("folderCheckForNewMessages").hidden = true; + } + // Retention policy doesn't apply to Drafts/Templates/Outbox. + if ( + gMsgFolder.isSpecialFolder( + Ci.nsMsgFolderFlags.Drafts | + Ci.nsMsgFolderFlags.Templates | + Ci.nsMsgFolderFlags.Queue, + true + ) + ) { + document.getElementById("Retention").hidden = true; + } + } +} + +function onOfflineFolderDownload() { + // we need to create a progress window and pass that in as the second parameter here. + gMsgFolder.downloadAllForOffline(null, window.arguments[0].msgWindow); +} + +function onFolderPrivileges() { + var imapFolder = gMsgFolder.QueryInterface(Ci.nsIMsgImapMailFolder); + if (imapFolder) { + imapFolder.folderPrivileges(window.arguments[0].msgWindow); + } + // let's try closing the modal dialog to see if it fixes the various problems running this url + window.close(); +} + +function onUseDefaultRetentionSettings() { + var useDefault = document.getElementById("retention.useDefault").checked; + document.getElementById("retention.keepMsg").disabled = useDefault; + document.getElementById("retention.keepNewMsgMinLabel").disabled = useDefault; + document.getElementById("retention.keepOldMsgMinLabel").disabled = useDefault; + + var keepMsg = document.getElementById("retention.keepMsg").value; + const nsIMsgRetentionSettings = Ci.nsIMsgRetentionSettings; + document.getElementById("retention.keepOldMsgMin").disabled = + useDefault || keepMsg != nsIMsgRetentionSettings.nsMsgRetainByAge; + document.getElementById("retention.keepNewMsgMin").disabled = + useDefault || keepMsg != nsIMsgRetentionSettings.nsMsgRetainByNumHeaders; +} + +function RebuildSummaryInformation() { + window.arguments[0].rebuildSummaryCallback(); +} diff --git a/comm/mailnews/base/content/folderProps.xhtml b/comm/mailnews/base/content/folderProps.xhtml new file mode 100644 index 0000000000..2c0376cc2a --- /dev/null +++ b/comm/mailnews/base/content/folderProps.xhtml @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + &folderProps.windowtitle.label; + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/jsTreeView.js b/comm/mailnews/base/content/jsTreeView.js new file mode 100644 index 0000000000..704fd2475f --- /dev/null +++ b/comm/mailnews/base/content/jsTreeView.js @@ -0,0 +1,239 @@ +/* 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/. */ + +/* exported PROTO_TREE_VIEW */ + +/** + * This file contains a prototype object designed to make the implementation of + * nsITreeViews in javascript simpler. This object requires that consumers + * override the _rebuild function. This function must set the _rowMap object to + * an array of objects fitting the following interface: + * + * readonly attribute string id - a unique identifier for the row/object + * readonly attribute integer level - the hierarchy level of the row + * attribute boolean open - whether or not this item's children are exposed + * string getText(aColName) - return the text to display for this row in the + * specified column + * string getProperties() - return the css-selectors + * attribute array children - return an array of child-objects also meeting this + * interface + */ + +function PROTO_TREE_VIEW() { + this._tree = null; + this._rowMap = []; + this._persistOpenMap = []; +} + +PROTO_TREE_VIEW.prototype = { + get rowCount() { + return this._rowMap.length; + }, + + /** + * CSS files will cue off of these. Note that we reach into the rowMap's + * items so that custom data-displays can define their own properties + */ + getCellProperties(aRow, aCol) { + return this._rowMap[aRow].getProperties(aCol); + }, + + /** + * The actual text to display in the tree + */ + getCellText(aRow, aCol) { + return this._rowMap[aRow].getText(aCol.id); + }, + + getCellValue(aRow, aCol) { + return this._rowMap[aRow].getValue(aCol.id); + }, + + /** + * The jstv items take care of assigning this when building children lists + */ + getLevel(aIndex) { + return this._rowMap[aIndex].level; + }, + + /** + * This is easy since the jstv items assigned the _parent property when making + * the child lists + */ + getParentIndex(aIndex) { + return this._rowMap.indexOf(this._rowMap[aIndex]._parent); + }, + + /** + * This is duplicative for our normal jstv views, but custom data-displays may + * want to do something special here + */ + getRowProperties(aRow) { + return this._rowMap[aRow].getProperties(); + }, + + /** + * If an item in our list has the same level and parent as us, it's a sibling + */ + hasNextSibling(aIndex, aNextIndex) { + let targetLevel = this._rowMap[aIndex].level; + for (let i = aNextIndex + 1; i < this._rowMap.length; i++) { + if (this._rowMap[i].level == targetLevel) { + return true; + } + if (this._rowMap[i].level < targetLevel) { + return false; + } + } + return false; + }, + + /** + * If we have a child-list with at least one element, we are a container. + */ + isContainer(aIndex) { + return this._rowMap[aIndex].children.length > 0; + }, + + isContainerEmpty(aIndex) { + // If the container has no children, the container is empty. + return !this._rowMap[aIndex].children.length; + }, + + /** + * Just look at the jstv item here + */ + isContainerOpen(aIndex) { + return this._rowMap[aIndex].open; + }, + + isEditable(aRow, aCol) { + // We don't support editing rows in the tree yet. + return false; + }, + + isSeparator(aIndex) { + // There are no separators in our trees + return false; + }, + + isSorted() { + // We do our own customized sorting + return false; + }, + + setTree(aTree) { + this._tree = aTree; + }, + + recursivelyAddToMap(aChild, aNewIndex) { + // When we add sub-children, we're going to need to increase our index + // for the next add item at our own level. + let currentCount = this._rowMap.length; + if (aChild.children.length && aChild.open) { + for (let [i, child] of this._rowMap[aNewIndex].children.entries()) { + let index = aNewIndex + i + 1; + this._rowMap.splice(index, 0, child); + aNewIndex += this.recursivelyAddToMap(child, index); + } + } + return this._rowMap.length - currentCount; + }, + + /** + * Opens or closes a container with children. The logic here is a bit hairy, so + * be very careful about changing anything. + */ + toggleOpenState(aIndex) { + // Ok, this is a bit tricky. + this._rowMap[aIndex]._open = !this._rowMap[aIndex].open; + + if (!this._rowMap[aIndex].open) { + // We're closing the current container. Remove the children + + // Note that we can't simply splice out children.length, because some of + // them might have children too. Find out how many items we're actually + // going to splice + let level = this._rowMap[aIndex].level; + let row = aIndex + 1; + while (row < this._rowMap.length && this._rowMap[row].level > level) { + row++; + } + let count = row - aIndex - 1; + this._rowMap.splice(aIndex + 1, count); + + // Remove us from the persist map + let index = this._persistOpenMap.indexOf(this._rowMap[aIndex].id); + if (index != -1) { + this._persistOpenMap.splice(index, 1); + } + + // Notify the tree of changes + if (this._tree) { + this._tree.rowCountChanged(aIndex + 1, -count); + } + } else { + // We're opening the container. Add the children to our map + + // Note that these children may have been open when we were last closed, + // and if they are, we also have to add those grandchildren to the map + let oldCount = this._rowMap.length; + this.recursivelyAddToMap(this._rowMap[aIndex], aIndex); + + // Add this container to the persist map + let id = this._rowMap[aIndex].id; + if (!this._persistOpenMap.includes(id)) { + this._persistOpenMap.push(id); + } + + // Notify the tree of changes + if (this._tree) { + this._tree.rowCountChanged(aIndex + 1, this._rowMap.length - oldCount); + } + } + + // Invalidate the toggled row, so that the open/closed marker changes + if (this._tree) { + this._tree.invalidateRow(aIndex); + } + }, + + // We don't implement any of these at the moment + canDrop(aIndex, aOrientation) {}, + drop(aRow, aOrientation) {}, + selectionChanged() {}, + setCellText(aRow, aCol, aValue) {}, + setCellValue(aRow, aCol, aValue) {}, + getColumnProperties(aCol) { + return ""; + }, + getImageSrc(aRow, aCol) {}, + getProgressMode(aRow, aCol) {}, + cycleCell(aRow, aCol) {}, + cycleHeader(aCol) {}, + + _tree: null, + + /** + * An array of jstv items, where each item corresponds to a row in the tree + */ + _rowMap: null, + + /** + * This is a javascript map of which containers we had open, so that we can + * persist their state over-time. It is designed to be used as a JSON object. + */ + _persistOpenMap: null, + + _restoreOpenStates() { + // Note that as we iterate through here, .length may grow + for (let i = 0; i < this._rowMap.length; i++) { + if (this._persistOpenMap.includes(this._rowMap[i].id)) { + this.toggleOpenState(i); + } + } + }, + + QueryInterface: ChromeUtils.generateQI(["nsITreeView"]), +}; diff --git a/comm/mailnews/base/content/junkCommands.js b/comm/mailnews/base/content/junkCommands.js new file mode 100644 index 0000000000..1554d54256 --- /dev/null +++ b/comm/mailnews/base/content/junkCommands.js @@ -0,0 +1,449 @@ +/* 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/. */ + +/** + * Functions use for junk processing commands + */ + +/* + * TODO: These functions make the false assumption that a view only contains + * a single folder. This is not true for XF saved searches. + * + * globals prerequisites used: + * + * top.window.MsgStatusFeedback + */ + +/* globals gDBView, gViewWrapper */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +ChromeUtils.defineModuleGetter( + this, + "MailUtils", + "resource:///modules/MailUtils.jsm" +); + +/** + * Determines the actions that should be carried out on the messages + * that are being marked as junk + * + * @param {nsIMsgFolder} aFolder - The folder with messages being marked as junk. + * @returns {object} result an object with two properties. + * @returns {boolean} result.markRead - Whether the messages should be marked + * as read. + * @returns {?nsIMsgFolder} result.junkTargetFolder - Where the messages should + * be moved, or null if they should not be moved. + */ +function determineActionsForJunkMsgs(aFolder) { + var actions = { markRead: false, junkTargetFolder: null }; + var spamSettings = aFolder.server.spamSettings; + + // note we will do moves/marking as read even if the spam + // feature is disabled, since the user has asked to use it + // despite the disabling + + actions.markRead = spamSettings.markAsReadOnSpam; + actions.junkTargetFolder = null; + + // move only when the corresponding setting is activated + // and the currently viewed folder is not the junk folder. + if (spamSettings.moveOnSpam && !aFolder.getFlag(Ci.nsMsgFolderFlags.Junk)) { + var spamFolderURI = spamSettings.spamFolderURI; + if (!spamFolderURI) { + // XXX TODO + // we should use nsIPromptService to inform the user of the problem, + // e.g. when the junk folder was accidentally deleted. + dump("determineActionsForJunkMsgs: no spam folder found, not moving."); + } else { + actions.junkTargetFolder = MailUtils.getOrCreateFolder(spamFolderURI); + } + } + + return actions; +} + +/** + * Performs required operations on a list of newly-classified junk messages. + * + * @param {nsIMsgFolder} aFolder - The folder with messages being marked as + * junk. + * @param {nsIMsgDBHdr[]} aJunkMsgHdrs - New junk messages. + * @param {nsIMsgDBHdr[]} aGoodMsgHdrs - New good messages. + */ +async function performActionsOnJunkMsgs(aFolder, aJunkMsgHdrs, aGoodMsgHdrs) { + return new Promise((resolve, reject) => { + if (aFolder instanceof Ci.nsIMsgImapMailFolder) { + // need to update IMAP custom flags + if (aJunkMsgHdrs.length) { + let junkMsgKeys = aJunkMsgHdrs.map(hdr => hdr.messageKey); + aFolder.storeCustomKeywords(null, "Junk", "NonJunk", junkMsgKeys); + } + + if (aGoodMsgHdrs.length) { + let goodMsgKeys = aGoodMsgHdrs.map(hdr => hdr.messageKey); + aFolder.storeCustomKeywords(null, "NonJunk", "Junk", goodMsgKeys); + } + } + if (!aJunkMsgHdrs.length) { + resolve(); + return; + } + + let actionParams = determineActionsForJunkMsgs(aFolder); + if (actionParams.markRead) { + aFolder.markMessagesRead(aJunkMsgHdrs, true); + } + + if (!actionParams.junkTargetFolder) { + resolve(); + return; + } + + // @implements {nsIMsgCopyServiceListener} + let listener = { + QueryInterface: ChromeUtils.generateQI(["nsIMsgCopyServiceListener"]), + OnStartCopy() {}, + OnProgress(progress, progressMax) {}, + SetMessageKey(key) {}, + GetMessageId() {}, + OnStopCopy(status) { + if (Components.isSuccessCode(status)) { + resolve(); + return; + } + let uri = actionParams.junkTargetFolder.URI; + reject(new Error(`Moving junk to ${uri} failed.`)); + }, + }; + MailServices.copy.copyMessages( + aFolder, + aJunkMsgHdrs, + actionParams.junkTargetFolder, + true /* isMove */, + listener, + top.msgWindow, + true /* allow undo */ + ); + }); +} + +/** + * Helper object storing the list of pending messages to process, + * and implementing junk processing callback. + * + * @param {nsIMsgFolder} aFolder - The folder with messages to be analyzed for junk. + * @param {integer} aTotalMessages - Number of messages to process, used for + * progress report only. + */ + +function MessageClassifier(aFolder, aTotalMessages) { + this.mFolder = aFolder; + this.mJunkMsgHdrs = []; + this.mGoodMsgHdrs = []; + this.mMessages = {}; + this.mMessageQueue = []; + this.mTotalMessages = aTotalMessages; + this.mProcessedMessages = 0; + this.firstMessage = true; + this.lastStatusTime = Date.now(); +} + +/** + * @implements {nsIJunkMailClassificationListener} + */ +MessageClassifier.prototype = { + /** + * Starts the message classification process for a message. If the message + * sender's address is whitelisted, the message is skipped. + * + * @param {nsIMsgDBHdr} aMsgHdr - The header of the message to classify. + * @param {nsISpamSettings} aSpamSettings - The object with information about + * whitelists + */ + analyzeMessage(aMsgHdr, aSpamSettings) { + var junkscoreorigin = aMsgHdr.getStringProperty("junkscoreorigin"); + if (junkscoreorigin == "user") { + // don't override user-set junk status + return; + } + + // check whitelisting + if (aSpamSettings.checkWhiteList(aMsgHdr)) { + // message is ham from whitelist + var db = aMsgHdr.folder.msgDatabase; + db.setStringProperty( + aMsgHdr.messageKey, + "junkscore", + Ci.nsIJunkMailPlugin.IS_HAM_SCORE + ); + db.setStringProperty(aMsgHdr.messageKey, "junkscoreorigin", "whitelist"); + this.mGoodMsgHdrs.push(aMsgHdr); + return; + } + + let messageURI = aMsgHdr.folder.generateMessageURI(aMsgHdr.messageKey); + this.mMessages[messageURI] = aMsgHdr; + if (this.firstMessage) { + this.firstMessage = false; + MailServices.junk.classifyMessage(messageURI, top.msgWindow, this); + } else { + this.mMessageQueue.push(messageURI); + } + }, + + /** + * Callback function from nsIJunkMailPlugin with classification results. + * + * @param {string} aClassifiedMsgURI - URI of classified message. + * @param {integer} aClassification - Junk classification (0: UNCLASSIFIED, 1: GOOD, 2: JUNK) + * @param {integer} aJunkPercent - 0 - 100 indicator of junk likelihood, + * with 100 meaning probably junk. + * @see {nsIJunkMailClassificationListener} + */ + async onMessageClassified(aClassifiedMsgURI, aClassification, aJunkPercent) { + if (!aClassifiedMsgURI) { + // Ignore end of batch. + return; + } + var score = + aClassification == Ci.nsIJunkMailPlugin.JUNK + ? Ci.nsIJunkMailPlugin.IS_SPAM_SCORE + : Ci.nsIJunkMailPlugin.IS_HAM_SCORE; + const statusDisplayInterval = 1000; // milliseconds between status updates + + // set these props via the db (instead of the message header + // directly) so that the nsMsgDBView knows to update the UI + // + var msgHdr = this.mMessages[aClassifiedMsgURI]; + var db = msgHdr.folder.msgDatabase; + db.setStringProperty(msgHdr.messageKey, "junkscore", score); + db.setStringProperty(msgHdr.messageKey, "junkscoreorigin", "plugin"); + db.setStringProperty(msgHdr.messageKey, "junkpercent", aJunkPercent); + + if (aClassification == Ci.nsIJunkMailPlugin.JUNK) { + this.mJunkMsgHdrs.push(msgHdr); + } else if (aClassification == Ci.nsIJunkMailPlugin.GOOD) { + this.mGoodMsgHdrs.push(msgHdr); + } + + var nextMsgURI = this.mMessageQueue.shift(); + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + + if (nextMsgURI) { + ++this.mProcessedMessages; + if (Date.now() > this.lastStatusTime + statusDisplayInterval) { + this.lastStatusTime = Date.now(); + var percentDone = 0; + if (this.mTotalMessages) { + percentDone = Math.round( + (this.mProcessedMessages * 100) / this.mTotalMessages + ); + } + top.window.MsgStatusFeedback.showStatusString( + bundle.formatStringFromName("junkAnalysisPercentComplete", [ + percentDone + "%", + ]) + ); + } + MailServices.junk.classifyMessage(nextMsgURI, top.msgWindow, this); + } else { + top.window.MsgStatusFeedback.showStatusString( + bundle.GetStringFromName("processingJunkMessages") + ); + await performActionsOnJunkMsgs( + this.mFolder, + this.mJunkMsgHdrs, + this.mGoodMsgHdrs + ); + setTimeout(() => { + top.window.MsgStatusFeedback.showStatusString(""); + }, 500); + } + }, +}; + +/** + * Filter all messages in the current folder for junk + */ +async function filterFolderForJunk() { + await processFolderForJunk(true); +} + +/** + * Filter selected messages in the current folder for junk + */ +async function analyzeMessagesForJunk() { + await processFolderForJunk(false); +} + +/** + * Filter messages in the current folder for junk + * + * @param {boolean} aAll - true to filter all messages, else filter selection. + */ +async function processFolderForJunk(aAll) { + let indices; + if (aAll) { + // need to expand all threads, so we analyze everything + gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll); + var treeView = gDBView.QueryInterface(Ci.nsITreeView); + var count = treeView.rowCount; + if (!count) { + return; + } + } else { + indices = + AppConstants.MOZ_APP_NAME == "seamonkey" + ? window.GetSelectedIndices(gDBView) + : window.threadTree?.selectedIndices; + if (!indices || !indices.length) { + return; + } + } + let totalMessages = aAll ? count : indices.length; + + // retrieve server and its spam settings via the header of an arbitrary message + let tmpMsgURI; + for (let i = 0; i < totalMessages; i++) { + let index = aAll ? i : indices[i]; + try { + tmpMsgURI = gDBView.getURIForViewIndex(index); + break; + } catch (e) { + // dummy headers will fail, so look for another + continue; + } + } + if (!tmpMsgURI) { + return; + } + + let tmpMsgHdr = + MailServices.messageServiceFromURI(tmpMsgURI).messageURIToMsgHdr(tmpMsgURI); + let spamSettings = tmpMsgHdr.folder.server.spamSettings; + + // create a classifier instance to classify messages in the folder. + let msgClassifier = new MessageClassifier(tmpMsgHdr.folder, totalMessages); + + for (let i = 0; i < totalMessages; i++) { + let index = aAll ? i : indices[i]; + try { + let msgURI = gDBView.getURIForViewIndex(index); + let msgHdr = + MailServices.messageServiceFromURI(msgURI).messageURIToMsgHdr(msgURI); + msgClassifier.analyzeMessage(msgHdr, spamSettings); + } catch (ex) { + // blow off errors here - dummy headers will fail + } + } + if (msgClassifier.firstMessage) { + // the async plugin was not used, maybe all whitelisted? + await performActionsOnJunkMsgs( + msgClassifier.mFolder, + msgClassifier.mJunkMsgHdrs, + msgClassifier.mGoodMsgHdrs + ); + } +} + +/** + * Delete junk messages in the current folder. This provides the guarantee that + * the method will be synchronous if no messages are deleted. + * + * @returns {integer} The number of messages deleted. + */ +function deleteJunkInFolder() { + // use direct folder commands if possible so we don't mess with the selection + let selectedFolder = gViewWrapper.displayedFolder; + if (!selectedFolder.getFlag(Ci.nsMsgFolderFlags.Virtual)) { + let junkMsgHdrs = []; + for (let msgHdr of gDBView.msgFolder.messages) { + let junkScore = msgHdr.getStringProperty("junkscore"); + if (junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE) { + junkMsgHdrs.push(msgHdr); + } + } + + if (junkMsgHdrs.length) { + gDBView.msgFolder.deleteMessages( + junkMsgHdrs, + top.msgWindow, + false, + false, + null, + true + ); + } + return junkMsgHdrs.length; + } + + // Folder is virtual, let the view do the work (but we lose selection) + + // need to expand all threads, so we find everything + gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll); + + var treeView = gDBView.QueryInterface(Ci.nsITreeView); + var count = treeView.rowCount; + if (!count) { + return 0; + } + + var treeSelection = treeView.selection; + + var clearedSelection = false; + + // select the junk messages + var messageUri; + let numMessagesDeleted = 0; + for (let i = 0; i < count; ++i) { + try { + messageUri = gDBView.getURIForViewIndex(i); + } catch (ex) { + continue; // blow off errors for dummy rows + } + let msgHdr = + MailServices.messageServiceFromURI(messageUri).messageURIToMsgHdr( + messageUri + ); + let junkScore = msgHdr.getStringProperty("junkscore"); + var isJunk = junkScore == Ci.nsIJunkMailPlugin.IS_SPAM_SCORE; + // if the message is junk, select it. + if (isJunk) { + // only do this once + if (!clearedSelection) { + // clear the current selection + // since we will be deleting all selected messages + treeSelection.clearSelection(); + clearedSelection = true; + treeSelection.selectEventsSuppressed = true; + } + treeSelection.rangedSelect(i, i, true /* augment */); + numMessagesDeleted++; + } + } + + // if we didn't clear the selection + // there was no junk, so bail. + if (!clearedSelection) { + return 0; + } + + treeSelection.selectEventsSuppressed = false; + // delete the selected messages + // + // We'll leave no selection after the delete + if ("gNextMessageViewIndexAfterDelete" in window) { + window.gNextMessageViewIndexAfterDelete = 0xffffffff; // nsMsgViewIndex_None + } + gDBView.doCommand(Ci.nsMsgViewCommandType.deleteMsg); + treeSelection.clearSelection(); + return numMessagesDeleted; +} diff --git a/comm/mailnews/base/content/junkLog.js b/comm/mailnews/base/content/junkLog.js new file mode 100644 index 0000000000..7f2b73edf7 --- /dev/null +++ b/comm/mailnews/base/content/junkLog.js @@ -0,0 +1,48 @@ +/* -*- 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 { MailE10SUtils } = ChromeUtils.import( + "resource:///modules/MailE10SUtils.jsm" +); + +var gLogView; +var gLogFile; + +window.addEventListener("DOMContentLoaded", onLoad); + +function onLoad() { + gLogView = document.getElementById("logView"); + gLogView.browsingContext.allowJavascript = false; // for security, disable JS + + gLogView.addEventListener("load", () => { + addStyling(); + }); + + gLogFile = Services.dirsvc.get("ProfD", Ci.nsIFile); + gLogFile.append("junklog.html"); + if (gLogFile.exists()) { + MailE10SUtils.loadURI(gLogView, Services.io.newFileURI(gLogFile).spec); + } else { + addStyling(); // set style for initial about:blank + } +} + +function clearLog() { + if (gLogFile.exists()) { + gLogFile.remove(false); + gLogView.setAttribute("src", "about:blank"); // we don't have a log file to show + } +} + +function addStyling() { + let style = gLogView.contentDocument.createElement("style"); + gLogView.contentDocument.head.appendChild(style); + style.sheet.insertRule( + `@media (prefers-color-scheme: dark) { + :root { scrollbar-color: rgba(249, 249, 250, .4) rgba(20, 20, 25, .3);} + body { color: #f9f9fa; } + }` + ); +} diff --git a/comm/mailnews/base/content/junkLog.xhtml b/comm/mailnews/base/content/junkLog.xhtml new file mode 100644 index 0000000000..28c7071c4e --- /dev/null +++ b/comm/mailnews/base/content/junkLog.xhtml @@ -0,0 +1,57 @@ + + + + + + + + + + &adaptiveJunkLog.title; + + + + + + + + + diff --git a/comm/mailnews/base/content/markByDate.js b/comm/mailnews/base/content/markByDate.js new file mode 100644 index 0000000000..672897f1e1 --- /dev/null +++ b/comm/mailnews/base/content/markByDate.js @@ -0,0 +1,120 @@ +/* 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 dateFormat.js */ + +var MILLISECONDS_PER_HOUR = 60 * 60 * 1000; +var MICROSECONDS_PER_DAY = 1000 * MILLISECONDS_PER_HOUR * 24; + +window.addEventListener("load", onLoad); + +document.addEventListener("dialogaccept", onAccept); + +function onLoad() { + var upperDateBox = document.getElementById("upperDate"); + // focus the upper bound control - this is where we expect most users to enter + // a date + upperDateBox.focus(); + + // and give it an initial date - "yesterday" + var initialDate = new Date(); + initialDate.setHours(0); + initialDate.setTime(initialDate.getTime() - MILLISECONDS_PER_HOUR); + // note that this is sufficient - though it is at the end of the previous day, + // we convert it to a date string, and then the time part is truncated + upperDateBox.value = convertDateToString(initialDate); + upperDateBox.select(); // allows to start overwriting immediately +} + +function onAccept() { + // get the times as entered by the user + var lowerDateString = document.getElementById("lowerDate").value; + // the fallback for the lower bound, if not entered, is the "beginning of + // time" (1970-01-01), which actually is simply 0 :) + var prLower = lowerDateString ? convertStringToPRTime(lowerDateString) : 0; + + var upperDateString = document.getElementById("upperDate").value; + var prUpper; + if (upperDateString == "") { + // for the upper bound, the fallback is "today". + var dateThisMorning = new Date(); + dateThisMorning.setMilliseconds(0); + dateThisMorning.setSeconds(0); + dateThisMorning.setMinutes(0); + dateThisMorning.setHours(0); + // Javascript time is in milliseconds, PRTime is in microseconds + prUpper = dateThisMorning.getTime() * 1000; + } else { + prUpper = convertStringToPRTime(upperDateString); + } + + // for the upper date, we have to do a correction: + // if the user enters a date, then she means (hopefully) that all messages sent + // at this day should be marked, too, but the PRTime calculated from this would + // point to the beginning of the day. So we need to increment it by + // [number of micro seconds per day]. This will denote the first microsecond of + // the next day then, which is later used as exclusive boundary + prUpper += MICROSECONDS_PER_DAY; + + markInDatabase(prLower, prUpper); +} + +/** + * M arks all headers in the database, whose time is between the two given + * times, as read. + * + * @param {integer} lower - PRTime for the lower bound (inclusive). + * @param {integer} upper - PRTime for the upper bound (exclusive). + */ +function markInDatabase(lower, upper) { + let messageFolder; + let messageDatabase; + // extract the database + if (window.arguments && window.arguments[0]) { + messageFolder = window.arguments[0]; + messageDatabase = messageFolder.msgDatabase; + } + + if (!messageDatabase) { + dump("markByDate::markInDatabase: there /is/ no database to operate on!\n"); + return; + } + + let searchSession = Cc[ + "@mozilla.org/messenger/searchSession;1" + ].createInstance(Ci.nsIMsgSearchSession); + let searchTerms = []; + searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, messageFolder); + + const nsMsgSearchAttrib = Ci.nsMsgSearchAttrib; + const nsMsgSearchOp = Ci.nsMsgSearchOp; + + let searchTerm = searchSession.createTerm(); + searchTerm.attrib = nsMsgSearchAttrib.Date; + searchTerm.op = nsMsgSearchOp.IsBefore; + let value = searchTerm.value; + value.attrib = nsMsgSearchAttrib.Date; + value.date = upper; + searchTerm.value = value; + searchTerms.push(searchTerm); + + if (lower) { + searchTerm = searchSession.createTerm(); + searchTerm.booleanAnd = true; + searchTerm.attrib = nsMsgSearchAttrib.Date; + searchTerm.op = nsMsgSearchOp.IsAfter; + value = searchTerm.value; + value.attrib = nsMsgSearchAttrib.Date; + value.date = lower; + searchTerm.value = value; + searchTerms.push(searchTerm); + } + + let msgEnumerator = messageDatabase.getFilterEnumerator(searchTerms); + let headers = [...msgEnumerator]; + + if (headers.length) { + messageFolder.markMessagesRead(headers, true); + } +} diff --git a/comm/mailnews/base/content/markByDate.xhtml b/comm/mailnews/base/content/markByDate.xhtml new file mode 100644 index 0000000000..7928bf2b59 --- /dev/null +++ b/comm/mailnews/base/content/markByDate.xhtml @@ -0,0 +1,79 @@ + + + + + + + + + + + + &messageMarkByDate.label; + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/menulist-charsetpicker.js b/comm/mailnews/base/content/menulist-charsetpicker.js new file mode 100644 index 0000000000..eb354621a7 --- /dev/null +++ b/comm/mailnews/base/content/menulist-charsetpicker.js @@ -0,0 +1,86 @@ +/* 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 https://mozilla.org/MPL/2.0/. */ + +// The menulist CE is defined lazily. Create one now to get menulist defined, +// allowing us to inherit from it. +if (!customElements.get("menulist")) { + delete document.createXULElement("menulist"); +} + +// Wrap in a block to prevent leaking to window scope. +{ + /** + * MozMenulistCharsetpicker is a menulist widget that is automatically + * populated with charset selections. + * + * @augments {MozMenuList} + */ + class MozMenulistCharsetpickerViewing extends customElements.get("menulist") { + /** + * Get the charset values to show in the list. + * + * @abstract + * @returns {string[]} an array of character encoding names + */ + get charsetValues() { + return [ + "UTF-8", + "Big5", + "EUC-KR", + "gbk", + "KOI8-R", + "ISO-2022-JP", + "ISO-8859-1", + "ISO-8859-2", + "ISO-8859-7", + "windows-874", + "windows-1250", + "windows-1251", + "windows-1252", + "windows-1255", + "windows-1256", + "windows-1257", + "windows-1258", + ]; + } + + connectedCallback() { + super.connectedCallback(); + if (this.delayConnectedCallback()) { + return; + } + + if (this.menupopup) { + return; + } + + let charsetBundle = Services.strings.createBundle( + "chrome://messenger/locale/charsetTitles.properties" + ); + this.charsetValues + .map(item => { + let strCharset = charsetBundle.GetStringFromName( + item.toLowerCase() + ".title" + ); + return { label: strCharset, value: item }; + }) + .sort((a, b) => { + if (a.value == "UTF-8" || a.label < b.label) { + return -1; + } else if (b.value == "UTF-8" || a.label > b.label) { + return 1; + } + return 0; + }) + .forEach(item => { + this.appendItem(item.label, item.value); + }); + } + } + customElements.define( + "menulist-charsetpicker-viewing", + MozMenulistCharsetpickerViewing, + { extends: "menulist" } + ); +} diff --git a/comm/mailnews/base/content/msgAccountCentral.js b/comm/mailnews/base/content/msgAccountCentral.js new file mode 100644 index 0000000000..7574c19da3 --- /dev/null +++ b/comm/mailnews/base/content/msgAccountCentral.js @@ -0,0 +1,238 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * 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 { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm"); +var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm"); + +var gSelectedServer = null; +var gSelectedFolder = null; + +window.addEventListener("DOMContentLoaded", OnInit); + +/** + * Set up the whole page depending on the selected folder/account. + * The folder is passed in via the document URL. + */ +function OnInit() { + let el = document.getElementById("setupTitle"); + + document.l10n.setAttributes(el, "setup-title", { + accounts: MailServices.accounts.accounts.length, + }); + + // Selected folder URI is passed as folderURI argument in the query string. + let folderURI = decodeURIComponent( + document.location.search.replace("?folderURI=", "") + ); + gSelectedFolder = folderURI ? MailUtils.getExistingFolder(folderURI) : null; + gSelectedServer = gSelectedFolder ? gSelectedFolder.server : null; + + if (gSelectedServer) { + // Display and collapse items presented to the user based on account type + updateAccountCentralUI(); + } else { + // If there is no gSelectedServer, we are in a brand new profile. + document.getElementById("headerFirstRun").hidden = false; + document.getElementById("headerExistingAccounts").hidden = true; + document.getElementById("version").textContent = Services.appinfo.version; + + // Update the style of the account setup buttons and area. + let accountSection = document.getElementById("accountSetupSection"); + for (let btn of accountSection.querySelectorAll(".btn-hub")) { + btn.classList.remove("btn-inline"); + } + accountSection.classList.remove("zebra"); + + document.getElementById("accountFeaturesSection").hidden = true; + } + + UIDensity.registerWindow(window); + UIFontSize.registerWindow(window); +} + +/** + * Show items in the AccountCentral page depending on the capabilities + * of the given server. + */ +function updateAccountCentralUI() { + // Set the account name. + document.getElementById("accountName").textContent = + gSelectedServer.prettyName; + + // Update the account logo. + document + .getElementById("accountLogo") + .setAttribute("type", gSelectedServer.type); + + let exceptions = []; + let protocolInfo = null; + try { + protocolInfo = gSelectedServer.protocolInfo; + } catch (e) { + exceptions.push(e); + } + + // Is this a RSS account? + let isRssAccount = gSelectedServer?.type == "rss"; + + // Is this an NNTP account? + let isNNTPAccount = gSelectedServer?.type == "nntp"; + + // Is this a Local Folders account? + const isLocalFoldersAccount = gSelectedServer?.type == "none"; + + document + .getElementById("readButton") + .toggleAttribute("hidden", !getReadMessagesFolder()); + + // It can compose messages. + let showComposeMsgLink = false; + try { + showComposeMsgLink = protocolInfo && protocolInfo.showComposeMsgLink; + document + .getElementById("composeButton") + .toggleAttribute("hidden", !showComposeMsgLink); + } catch (e) { + exceptions.push(e); + } + + // It can subscribe to a newsgroup. + document + .getElementById("nntpSubscriptionButton") + .toggleAttribute("hidden", !isNNTPAccount); + + // It can subscribe to an RSS feed. + document + .getElementById("rssSubscriptionButton") + .toggleAttribute("hidden", !isRssAccount); + + // It can search messages. + let canSearchMessages = false; + try { + canSearchMessages = gSelectedServer && gSelectedServer.canSearchMessages; + document + .getElementById("searchButton") + .toggleAttribute("hidden", !canSearchMessages); + } catch (e) { + exceptions.push(e); + } + + // It can create filters. + let canHaveFilters = false; + try { + canHaveFilters = gSelectedServer && gSelectedServer.canHaveFilters; + document + .getElementById("filterButton") + .toggleAttribute("hidden", !canHaveFilters); + } catch (e) { + exceptions.push(e); + } + + // It can have End-to-end Encryption. + document + .getElementById("e2eButton") + .toggleAttribute( + "hidden", + isNNTPAccount || isRssAccount || isLocalFoldersAccount + ); + + // Check if we collected any exception. + while (exceptions.length) { + console.error( + "Error in setting AccountCentral Items: " + exceptions.pop() + "\n" + ); + } +} + +/** + * For the selected server, check for new messges and display first + * suitable folder (genrally Inbox) for reading. + */ +function readMessages() { + const folder = getReadMessagesFolder(); + top.MsgGetMessage([folder]); + parent.displayFolder(folder); +} + +/** + * Find the folder Read Messages should use. + * + * @returns {?nsIMsgFolder} folder to use, if we have a suitable one. + */ +function getReadMessagesFolder() { + const folder = MailUtils.getInboxFolder(gSelectedServer); + if (folder) { + return folder; + } + // For feeds and nntp, show the first non-trash folder. Don't use Outbox. + return gSelectedServer.rootFolder.descendants.find( + f => + !(f.flags & Ci.nsMsgFolderFlags.Trash) && + !(f.flags & Ci.nsMsgFolderFlags.Queue) + ); +} + +/** + * Open the AccountManager to view the settings for a given account. + * + * @param {string} selectPage - The xhtml file name for the viewing page, + * null for the account main page, other pages are 'am-server.xhtml', + * 'am-copies.xhtml', 'am-offline.xhtml', 'am-addressing.xhtml', + * 'am-smtp.xhtml' + */ +function viewSettings(selectPage) { + window.browsingContext.topChromeWindow.MsgAccountManager( + selectPage, + gSelectedServer + ); +} + +/** + * Bring up the search interface for selected account. + */ +function searchMessages() { + top.document + .getElementById("tabmail") + .currentAbout3Pane.commandController.doCommand("cmd_searchMessages"); +} + +/** + * Open the filters window. + */ +function createMsgFilters() { + window.browsingContext.topChromeWindow.MsgFilters(null, gSelectedFolder); +} + +/** + * Open the subscribe dialog. + */ +function subscribe() { + if (!gSelectedServer) { + return; + } + if (gSelectedServer.type == "rss") { + window.browsingContext.topChromeWindow.openSubscriptionsDialog( + gSelectedServer.rootFolder + ); + } else { + window.browsingContext.topChromeWindow.MsgSubscribe(gSelectedFolder); + } +} + +/** + * Open the target's url on an external browser. + * + * @param {Event} event - The keypress or click event. + */ +function openLink(event) { + event.preventDefault(); + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .loadURI(Services.io.newURI(event.target.href)); +} diff --git a/comm/mailnews/base/content/msgAccountCentral.xhtml b/comm/mailnews/base/content/msgAccountCentral.xhtml new file mode 100644 index 0000000000..843840c088 --- /dev/null +++ b/comm/mailnews/base/content/msgAccountCentral.xhtml @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +
+ + diff --git a/comm/mailnews/base/content/msgSelectOfflineFolders.js b/comm/mailnews/base/content/msgSelectOfflineFolders.js new file mode 100644 index 0000000000..18f304bae7 --- /dev/null +++ b/comm/mailnews/base/content/msgSelectOfflineFolders.js @@ -0,0 +1,189 @@ +/* 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 PROTO_TREE_VIEW */ + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + FolderUtils: "resource:///modules/FolderUtils.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", +}); + +var gFolderTreeView = new PROTO_TREE_VIEW(); + +var gSelectOffline = { + _treeElement: null, + _rollbackMap: new Map(), + + load() { + for (let account of FolderUtils.allAccountsSorted(true)) { + let server = account.incomingServer; + if ( + server instanceof Ci.nsIPop3IncomingServer && + server.deferredToAccount + ) { + continue; + } + if (!server.rootFolder.supportsOffline) { + continue; + } + + gFolderTreeView._rowMap.push(new FolderRow(server.rootFolder)); + } + + this._treeElement = document.getElementById("synchronizeTree"); + // TODO: Expand relevant rows. + this._treeElement.view = gFolderTreeView; + }, + + onKeyPress(aEvent) { + // For now, only do something on space key. + if (aEvent.charCode != aEvent.DOM_VK_SPACE) { + return; + } + + let selection = this._treeElement.view.selection; + let start = {}; + let end = {}; + let numRanges = selection.getRangeCount(); + + for (let range = 0; range < numRanges; range++) { + selection.getRangeAt(range, start, end); + for (let i = start.value; i <= end.value; i++) { + this._toggle(i); + } + } + }, + + onClick(aEvent) { + // We only care about button 0 (left click) events. + if (aEvent.button != 0) { + return; + } + + // We don't want to toggle when clicking on header or tree (scrollbar) or + // on treecol. + if (aEvent.target.nodeName != "treechildren") { + return; + } + + let treeCellInfo = this._treeElement.getCellAt( + aEvent.clientX, + aEvent.clientY + ); + + if (treeCellInfo.row == -1 || treeCellInfo.col.id != "syncCol") { + return; + } + + this._toggle(treeCellInfo.row); + }, + + _toggle(aRow) { + let folder = gFolderTreeView._rowMap[aRow]._folder; + + if (folder.isServer) { + return; + } + + // Save our current state for rollback, if necessary. + if (!this._rollbackMap.has(folder)) { + this._rollbackMap.set( + folder, + folder.getFlag(Ci.nsMsgFolderFlags.Offline) + ); + } + + folder.toggleFlag(Ci.nsMsgFolderFlags.Offline); + gFolderTreeView._tree.invalidateRow(aRow); + }, + + onCancel() { + for (let [folder, value] of this._rollbackMap) { + if (value != folder.getFlag(Ci.nsMsgFolderFlags.Offline)) { + folder.toggleFlag(Ci.nsMsgFolderFlags.Offline); + } + } + }, +}; + +window.addEventListener("load", () => gSelectOffline.load()); +document.addEventListener("dialogcancel", () => gSelectOffline.onCancel()); + +/** + * A tree row representing a single folder. + */ +class FolderRow { + constructor(folder, parent = null) { + this._folder = folder; + this._open = false; + this._level = parent ? parent.level + 1 : 0; + this._parent = parent; + this._children = null; + } + + get id() { + return this._folder.URI; + } + + get text() { + return this.getText("folderNameCol"); + } + + getText(aColName) { + switch (aColName) { + case "folderNameCol": + return this._folder.abbreviatedName; + default: + return ""; + } + } + + get open() { + return this._open; + } + + get level() { + return this._level; + } + + getProperties(column) { + let properties = ""; + switch (column?.id) { + case "folderNameCol": + // From folderUtils.jsm. + properties = FolderUtils.getFolderProperties(this._folder, this.open); + break; + case "syncCol": + if (this._folder.isServer) { + return "isServer-true"; + } + properties = "syncCol"; + if (this._folder.getFlag(Ci.nsMsgFolderFlags.Offline)) { + properties += " synchronize-true"; + } + break; + } + return properties; + } + + get children() { + if (this._children === null) { + this._children = []; + for (let subFolder of this._folder.subFolders) { + if (subFolder.supportsOffline) { + this._children.push(new FolderRow(subFolder, this)); + } + } + this._children.sort((a, b) => a._folder.compareSortKeys(b._folder)); + } + return this._children; + } +} diff --git a/comm/mailnews/base/content/msgSelectOfflineFolders.xhtml b/comm/mailnews/base/content/msgSelectOfflineFolders.xhtml new file mode 100644 index 0000000000..88e0ecbd39 --- /dev/null +++ b/comm/mailnews/base/content/msgSelectOfflineFolders.xhtml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + &MsgSelect.label; + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/msgSynchronize.js b/comm/mailnews/base/content/msgSynchronize.js new file mode 100644 index 0000000000..4393a2d301 --- /dev/null +++ b/comm/mailnews/base/content/msgSynchronize.js @@ -0,0 +1,192 @@ +/* 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 { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); + +var gSynchronizeTree = null; +var gParentMsgWindow; +var gMsgWindow; + +var gInitialFolderStates = {}; + +window.addEventListener("DOMContentLoaded", onLoad); + +document.addEventListener("dialogaccept", syncOkButton); + +function onLoad() { + gParentMsgWindow = window.arguments?.[0]?.msgWindow; + + document.getElementById("syncMail").checked = Services.prefs.getBoolPref( + "mailnews.offline_sync_mail" + ); + document.getElementById("syncNews").checked = Services.prefs.getBoolPref( + "mailnews.offline_sync_news" + ); + document.getElementById("sendMessage").checked = Services.prefs.getBoolPref( + "mailnews.offline_sync_send_unsent" + ); + document.getElementById("workOffline").checked = Services.prefs.getBoolPref( + "mailnews.offline_sync_work_offline" + ); +} + +function syncOkButton() { + var syncMail = document.getElementById("syncMail").checked; + var syncNews = document.getElementById("syncNews").checked; + var sendMessage = document.getElementById("sendMessage").checked; + var workOffline = document.getElementById("workOffline").checked; + + Services.prefs.setBoolPref("mailnews.offline_sync_mail", syncMail); + Services.prefs.setBoolPref("mailnews.offline_sync_news", syncNews); + Services.prefs.setBoolPref("mailnews.offline_sync_send_unsent", sendMessage); + Services.prefs.setBoolPref("mailnews.offline_sync_work_offline", workOffline); + + if (syncMail || syncNews || sendMessage || workOffline) { + var offlineManager = Cc[ + "@mozilla.org/messenger/offline-manager;1" + ].getService(Ci.nsIMsgOfflineManager); + if (offlineManager) { + offlineManager.synchronizeForOffline( + syncNews, + syncMail, + sendMessage, + workOffline, + gParentMsgWindow + ); + } + } +} + +function OnSelect() { + top.window.openDialog( + "chrome://messenger/content/msgSelectOfflineFolders.xhtml", + "", + "centerscreen,chrome,modal,titlebar,resizable=yes" + ); + return true; +} + +// All the code below is only used by Seamonkey. + +function selectOkButton() { + return true; +} + +function selectCancelButton() { + for (var resourceValue in gInitialFolderStates) { + let folder = MailUtils.getExistingFolder(resourceValue); + if (gInitialFolderStates[resourceValue]) { + folder.setFlag(Ci.nsMsgFolderFlags.Offline); + } else { + folder.clearFlag(Ci.nsMsgFolderFlags.Offline); + } + } + return true; +} + +function SortSynchronizePane(column, sortKey) { + var node = FindInWindow(window, column); + if (!node) { + dump("Couldn't find sort column\n"); + return; + } + + node.setAttribute("sort", sortKey); + node.setAttribute("sortDirection", "natural"); + var col = gSynchronizeTree.columns[column]; + gSynchronizeTree.view.cycleHeader(col); +} + +function FindInWindow(currentWindow, id) { + var item = currentWindow.document.getElementById(id); + if (item) { + return item; + } + + for (var i = 0; i < currentWindow.frames.length; i++) { + var frameItem = FindInWindow(currentWindow.frames[i], id); + if (frameItem) { + return frameItem; + } + } + + return null; +} + +function onSynchronizeClick(event) { + // we only care about button 0 (left click) events + if (event.button != 0) { + return; + } + + let treeCellInfo = gSynchronizeTree.getCellAt(event.clientX, event.clientY); + if (treeCellInfo.row == -1) { + return; + } + + if (treeCellInfo.childElt == "twisty") { + var folderResource = GetFolderResource(gSynchronizeTree, treeCellInfo.row); + var folder = folderResource.QueryInterface(Ci.nsIMsgFolder); + + if (!gSynchronizeTree.view.isContainerOpen(treeCellInfo.row)) { + var serverType = folder.server.type; + // imap is the only server type that does folder discovery + if (serverType != "imap") { + return; + } + + if (folder.isServer) { + var server = folder.server; + server.performExpand(gMsgWindow); + } else { + var imapFolder = folderResource.QueryInterface(Ci.nsIMsgImapMailFolder); + if (imapFolder) { + imapFolder.performExpand(gMsgWindow); + } + } + } + } else if (treeCellInfo.col.id == "syncCol") { + UpdateNode( + GetFolderResource(gSynchronizeTree, treeCellInfo.row), + treeCellInfo.row + ); + } +} + +function onSynchronizeTreeKeyPress(event) { + // for now, only do something on space key + if (event.charCode != KeyEvent.DOM_VK_SPACE) { + return; + } + + var treeSelection = gSynchronizeTree.view.selection; + for (let i = 0; i < treeSelection.getRangeCount(); i++) { + var start = {}, + end = {}; + treeSelection.getRangeAt(i, start, end); + for (let k = start.value; k <= end.value; k++) { + UpdateNode(GetFolderResource(gSynchronizeTree, k), k); + } + } +} + +function UpdateNode(resource, row) { + var folder = resource.QueryInterface(Ci.nsIMsgFolder); + + if (folder.isServer) { + return; + } + + if (!(resource.Value in gInitialFolderStates)) { + gInitialFolderStates[resource.Value] = folder.getFlag( + Ci.nsMsgFolderFlags.Offline + ); + } + + folder.toggleFlag(Ci.nsMsgFolderFlags.Offline); +} + +function GetFolderResource(aTree, aIndex) { + return aTree.view.getResourceAtIndex(aIndex); +} diff --git a/comm/mailnews/base/content/msgSynchronize.xhtml b/comm/mailnews/base/content/msgSynchronize.xhtml new file mode 100644 index 0000000000..27df730cac --- /dev/null +++ b/comm/mailnews/base/content/msgSynchronize.xhtml @@ -0,0 +1,76 @@ + + + + + + + + + + + &MsgSynchronize.label; + + + + + + &MsgSyncDesc.label; + &MsgSyncDirections.label; + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/newFolderDialog.js b/comm/mailnews/base/content/newFolderDialog.js new file mode 100644 index 0000000000..2476f198b0 --- /dev/null +++ b/comm/mailnews/base/content/newFolderDialog.js @@ -0,0 +1,82 @@ +/* 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 FOLDERS = 1; +var MESSAGES = 2; +var dialog; + +window.addEventListener("DOMContentLoaded", onLoad); +document.addEventListener("dialogaccept", onOK); + +function onLoad() { + var windowArgs = window.arguments[0]; + + dialog = {}; + + dialog.nameField = document.getElementById("name"); + dialog.nameField.focus(); + + // call this when OK is pressed + dialog.okCallback = windowArgs.okCallback; + + // pre select the folderPicker, based on what they selected in the folder pane + dialog.folder = windowArgs.folder; + try { + document + .getElementById("MsgNewFolderPopup") + .selectFolder(windowArgs.folder); + } catch (ex) { + // selected a child folder + document + .getElementById("msgNewFolderPicker") + .setAttribute("label", windowArgs.folder.prettyName); + } + + // can folders contain both folders and messages? + if (windowArgs.dualUseFolders) { + dialog.folderType = FOLDERS | MESSAGES; + + // hide the section when folder contain both folders and messages. + var newFolderTypeBox = document.getElementById("newFolderTypeBox"); + newFolderTypeBox.setAttribute("hidden", "true"); + } else { + // set our folder type by calling the default selected type's oncommand + document.getElementById("folderGroup").selectedItem.doCommand(); + } + + doEnabling(); +} + +function onFolderSelect(event) { + dialog.folder = event.target._folder; + document + .getElementById("msgNewFolderPicker") + .setAttribute("label", dialog.folder.prettyName); +} + +function onOK() { + var name = dialog.nameField.value; + + // do name validity check? + + // make sure name ends in "/" if folder to create can only contain folders + if (dialog.folderType == FOLDERS && !name.endsWith("/")) { + dialog.okCallback(name + "/", dialog.folder); + } else { + dialog.okCallback(name, dialog.folder); + } +} + +function onFoldersOnly() { + dialog.folderType = FOLDERS; +} + +function onMessagesOnly() { + dialog.folderType = MESSAGES; +} + +function doEnabling() { + document.querySelector("dialog").getButton("accept").disabled = + !dialog.nameField.value; +} diff --git a/comm/mailnews/base/content/newFolderDialog.xhtml b/comm/mailnews/base/content/newFolderDialog.xhtml new file mode 100644 index 0000000000..bc141b95f3 --- /dev/null +++ b/comm/mailnews/base/content/newFolderDialog.xhtml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + +%newFolderDTD; ]> + + + + &newFolderDialog.title; + + + + + + + + + + diff --git a/comm/mailnews/base/content/newmailalert.js b/comm/mailnews/base/content/newmailalert.js new file mode 100644 index 0000000000..898c8d6724 --- /dev/null +++ b/comm/mailnews/base/content/newmailalert.js @@ -0,0 +1,109 @@ +/* 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 { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); + +var gAlertListener = null; + +// NOTE: We must wait until "load" instead of "DOMContentLoaded" because +// otherwise the window height and width is not set in time for +// window.moveTo. +window.addEventListener("load", onAlertLoad); + +function prefillAlertInfo() { + // unwrap all the args.... + // arguments[0] --> The real nsIMsgFolder with new mail. + // arguments[1] --> The keys of new messages. + // arguments[2] --> The nsIObserver to receive window closed event. + let [folder, newMsgKeys, listener] = window.arguments; + newMsgKeys = newMsgKeys.wrappedJSObject; + gAlertListener = listener.QueryInterface(Ci.nsIObserver); + + // Generate an account label string based on the root folder. + var label = document.getElementById("alertTitle"); + var totalNumNewMessages = newMsgKeys.length; + let message = document + .getElementById("bundle_messenger") + .getString("newMailAlert_message"); + label.value = PluralForm.get(totalNumNewMessages, message) + .replace("#1", folder.server.rootFolder.prettyName) + .replace("#2", totalNumNewMessages); + + // handles rendering of new messages. + var folderSummaryInfoEl = document.getElementById("folderSummaryInfo"); + folderSummaryInfoEl.maxMsgHdrsInPopup = 6; + folderSummaryInfoEl.render(folder, newMsgKeys); +} + +function onAlertLoad() { + let dragSession = Cc["@mozilla.org/widget/dragservice;1"] + .getService(Ci.nsIDragService) + .getCurrentSession(); + if (dragSession && dragSession.sourceNode) { + // If a drag session is active, adjusting this window's dimensions causes + // the drag session to be abruptly terminated. To avoid interrupting the + // user, wait until the drag is finished and then set up and show the alert. + dragSession.sourceNode.addEventListener("dragend", () => doOnAlertLoad()); + } else { + doOnAlertLoad(); + } +} + +function doOnAlertLoad() { + prefillAlertInfo(); + + if (!document.getElementById("folderSummaryInfo").hasMessages()) { + closeAlert(); // no mail, so don't bother showing the alert... + return; + } + + // resize the alert based on our current content + let alertTextBox = document.getElementById("alertTextBox"); + let alertImageBox = document.getElementById("alertImageBox"); + alertImageBox.style.minHeight = alertTextBox.scrollHeight + "px"; + + // Show in bottom right, offset by 10px. + // We wait one cycle until the window has resized. + setTimeout(() => { + let x = screen.availLeft + screen.availWidth - window.outerWidth - 10; + let y = screen.availTop + screen.availHeight - window.outerHeight - 10; + window.moveTo(x, y); + }); + + let openTime = Services.prefs.getIntPref("alerts.totalOpenTime"); + var alertContainer = document.getElementById("alertContainer"); + // Don't fade in if the prefers-reduced-motion is true. + if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + alertContainer.setAttribute("noanimation", true); + setTimeout(closeAlert, openTime); + return; + } + + alertContainer.addEventListener("animationend", function hideAlert(event) { + if (event.animationName == "fade-in") { + alertContainer.removeEventListener("animationend", hideAlert); + setTimeout(fadeOutAlert, openTime); + } + }); + + alertContainer.setAttribute("fade-in", true); +} + +function fadeOutAlert() { + var alertContainer = document.getElementById("alertContainer"); + alertContainer.addEventListener("animationend", function fadeOut(event) { + if (event.animationName == "fade-out") { + alertContainer.removeEventListener("animationend", fadeOut); + closeAlert(); + } + }); + alertContainer.setAttribute("fade-out", true); +} + +function closeAlert() { + window.close(); + gAlertListener.observe(null, "newmailalert-closed", ""); +} diff --git a/comm/mailnews/base/content/newmailalert.xhtml b/comm/mailnews/base/content/newmailalert.xhtml new file mode 100644 index 0000000000..1346be9c9a --- /dev/null +++ b/comm/mailnews/base/content/newmailalert.xhtml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/newsError.js b/comm/mailnews/base/content/newsError.js new file mode 100644 index 0000000000..693083a166 --- /dev/null +++ b/comm/mailnews/base/content/newsError.js @@ -0,0 +1,48 @@ +/* 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/. */ + +// Error url must be formatted like this: +// about:newserror?r=response&m=messageid&k=messagekey&f=folderuri +// "r" is required; "m" and "f" are optional, but "k" always comes with "m". + +var folderUri; + +function initPage() { + let uri = document.documentURI; + let query = uri.slice(uri.indexOf("?") + 1); + let params = {}; + for (let piece of query.split("&")) { + let [key, value] = piece.split("="); + params[key] = decodeURIComponent(value); + } + + document.getElementById("ngResp").textContent = params.r; + + if ("m" in params) { + document.getElementById("msgId").textContent = params.m; + document.getElementById("msgKey").textContent = params.k; + } else { + document.getElementById("messageIdDesc").hidden = true; + } + + if ("f" in params) { + folderUri = params.f; + } else { + document.getElementById("errorTryAgain").hidden = true; + } +} + +function removeExpired() { + document.location.href = folderUri + "?list-ids"; +} + +let errorTryAgain = document.getElementById("errorTryAgain"); +errorTryAgain.addEventListener("click", function () { + removeExpired(); +}); + +// This must be called in this way, +// see mozilla-central/docshell/resources/content/netError.js after which +// this is modelled. +initPage(); diff --git a/comm/mailnews/base/content/newsError.xhtml b/comm/mailnews/base/content/newsError.xhtml new file mode 100644 index 0000000000..519d7c3897 --- /dev/null +++ b/comm/mailnews/base/content/newsError.xhtml @@ -0,0 +1,57 @@ + + + + +%htmlDTD; + +%netErrorDTD; ]> + + + + &newsError.title; + + + + + +
+
+

&articleNotFound.title;

+
+
+
+

&articleNotFound.desc;

+
+
+
    +
  • &serverResponded.title;
  • +
  • &articleExpired.title;
  • +
  • + &trySearching.title; <> () +
  • +
+
+
+ + +
+ + + + + + + + &rename.label; + + + + diff --git a/comm/mailnews/base/content/retention.js b/comm/mailnews/base/content/retention.js new file mode 100644 index 0000000000..ac40f173b3 --- /dev/null +++ b/comm/mailnews/base/content/retention.js @@ -0,0 +1,52 @@ +/* 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 gLockedPref */ // From either folderProps.js or am-offline.js. + +function initCommonRetentionSettings(retentionSettings) { + document.getElementById("retention.keepMsg").value = + retentionSettings.retainByPreference; + document.getElementById("retention.keepOldMsgMin").value = + retentionSettings.daysToKeepHdrs > 0 + ? retentionSettings.daysToKeepHdrs + : 30; + document.getElementById("retention.keepNewMsgMin").value = + retentionSettings.numHeadersToKeep > 0 + ? retentionSettings.numHeadersToKeep + : 2000; + + document.getElementById("retention.applyToFlagged").checked = + !retentionSettings.applyToFlaggedMessages; +} + +function saveCommonRetentionSettings(aRetentionSettings) { + aRetentionSettings.retainByPreference = + document.getElementById("retention.keepMsg").value; + + aRetentionSettings.daysToKeepHdrs = document.getElementById( + "retention.keepOldMsgMin" + ).value; + aRetentionSettings.numHeadersToKeep = document.getElementById( + "retention.keepNewMsgMin" + ).value; + + aRetentionSettings.applyToFlaggedMessages = !document.getElementById( + "retention.applyToFlagged" + ).checked; + + return aRetentionSettings; +} + +function onCheckKeepMsg() { + if (gLockedPref && gLockedPref["retention.keepMsg"]) { + // if the pref associated with the radiobutton is locked, as indicated + // by the gLockedPref, skip this function. All elements in this + // radiogroup have been locked by the function onLockPreference. + return; + } + + var keepMsg = document.getElementById("retention.keepMsg").value; + document.getElementById("retention.keepOldMsgMin").disabled = keepMsg != 2; + document.getElementById("retention.keepNewMsgMin").disabled = keepMsg != 3; +} diff --git a/comm/mailnews/base/content/shutdownWindow.js b/comm/mailnews/base/content/shutdownWindow.js new file mode 100644 index 0000000000..68c7f6b6f0 --- /dev/null +++ b/comm/mailnews/base/content/shutdownWindow.js @@ -0,0 +1,97 @@ +/* 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 curTaskIndex = 0; +var numTasks = 0; +var stringBundle; + +var msgShutdownService = Cc[ + "@mozilla.org/messenger/msgshutdownservice;1" +].getService(Ci.nsIMsgShutdownService); + +window.addEventListener("DOMContentLoaded", onLoad); +document.addEventListener("dialogcancel", onCancel); + +function onLoad() { + numTasks = msgShutdownService.getNumTasks(); + + stringBundle = document.getElementById("bundle_shutdown"); + document.title = stringBundle.getString("shutdownDialogTitle"); + + updateTaskProgressLabel(1); + updateProgressMeter(0); + + msgShutdownService.startShutdownTasks(); +} + +function updateProgressLabel(inTaskName) { + var curTaskLabel = document.getElementById("shutdownStatus_label"); + curTaskLabel.value = inTaskName; +} + +function updateTaskProgressLabel(inCurTaskNum) { + var taskProgressLabel = document.getElementById("shutdownTask_label"); + taskProgressLabel.value = stringBundle.getFormattedString("taskProgress", [ + inCurTaskNum, + numTasks, + ]); +} + +function updateProgressMeter(inPercent) { + var taskProgressmeter = document.getElementById("shutdown_progressmeter"); + taskProgressmeter.value = inPercent; +} + +function onCancel() { + msgShutdownService.cancelShutdownTasks(); +} + +function nsMsgShutdownTaskListener() { + msgShutdownService.setShutdownListener(this); +} + +nsMsgShutdownTaskListener.prototype = { + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) { + window.close(); + } + }, + + onProgressChange( + aWebProgress, + aRequest, + aCurSelfProgress, + aMaxSelfProgress, + aCurTotalProgress, + aMaxTotalProgress + ) { + updateProgressMeter((aCurTotalProgress / aMaxTotalProgress) * 100); + updateTaskProgressLabel(aCurTotalProgress + 1); + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) { + // we can ignore this notification + }, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) { + if (aMessage) { + updateProgressLabel(aMessage); + } + }, + + onSecurityChange(aWebProgress, aRequest, state) { + // we can ignore this notification + }, + + onContentBlockingEvent(aWebProgress, aRequest, aEvent) { + // we can ignore this notification + }, +}; + +var MsgShutdownTaskListener = new nsMsgShutdownTaskListener(); diff --git a/comm/mailnews/base/content/shutdownWindow.xhtml b/comm/mailnews/base/content/shutdownWindow.xhtml new file mode 100644 index 0000000000..32237dc44f --- /dev/null +++ b/comm/mailnews/base/content/shutdownWindow.xhtml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/subscribe.js b/comm/mailnews/base/content/subscribe.js new file mode 100644 index 0000000000..46eb51ce52 --- /dev/null +++ b/comm/mailnews/base/content/subscribe.js @@ -0,0 +1,496 @@ +/* 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 msgWindow, nsMsgStatusFeedback */ // From mailWindow.js + +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); + +var gSubscribeTree = null; +var gSubscribeBody = null; +var okCallback = null; +var gChangeTable = {}; +var gServerURI = null; +var gSubscribableServer = null; +var gNameField = null; +var gNameFieldLabel = null; +var gStatusFeedback; +var gSearchView = null; +var gSearchTree = null; +var gSubscribeBundle; + +window.addEventListener("DOMContentLoaded", SubscribeOnLoad); +window.addEventListener("unload", SubscribeOnUnload); + +document.addEventListener("dialogaccept", subscribeOK); +document.addEventListener("dialogcancel", subscribeCancel); + +function Stop() { + if (gSubscribableServer) { + gSubscribableServer.stopPopulating(msgWindow); + } +} + +function SetServerTypeSpecificTextValues() { + if (!gServerURI) { + return; + } + + let serverType = MailUtils.getExistingFolder(gServerURI).server.type; + + // Set the server specific ui elements. + let subscribeLabelString = gSubscribeBundle.getString( + "subscribeLabel-" + serverType + ); + let currentListTab = "currentListTab-" + serverType; + let currentListTabLabel = gSubscribeBundle.getString( + currentListTab + ".label" + ); + let currentListTabAccesskey = gSubscribeBundle.getString( + currentListTab + ".accesskey" + ); + + document + .getElementById("currentListTab") + .setAttribute("label", currentListTabLabel); + document + .getElementById("currentListTab") + .setAttribute("accesskey", currentListTabAccesskey); + document.getElementById("newGroupsTab").collapsed = serverType != "nntp"; // show newGroupsTab only for nntp servers + document + .getElementById("subscribeLabel") + .setAttribute("value", subscribeLabelString); +} + +function onServerClick(aFolder) { + gServerURI = aFolder.server.serverURI; + let serverMenu = document.getElementById("serverMenu"); + serverMenu.menupopup.selectFolder(aFolder); + + SetServerTypeSpecificTextValues(); + ShowCurrentList(); +} + +var MySubscribeListener = { + OnDonePopulating() { + gStatusFeedback._stopMeteors(); + document.getElementById("stopButton").disabled = true; + document.getElementById("refreshButton").disabled = false; + document.getElementById("currentListTab").disabled = false; + document.getElementById("newGroupsTab").disabled = false; + }, +}; + +function SetUpTree(forceToServer, getOnlyNew) { + if (!gServerURI) { + return; + } + + var server = MailUtils.getExistingFolder(gServerURI).server; + try { + CleanUpSearchView(); + gSubscribableServer = server.QueryInterface(Ci.nsISubscribableServer); + + // Enable (or disable) the search related UI. + EnableSearchUI(); + + // Clear out the text field when switching server. + gNameField.value = ""; + + // Since there is no text, switch to the Subscription view. + toggleSubscriptionView(false); + + gSubscribeTree.view = gSubscribableServer.folderView; + gSubscribableServer.subscribeListener = MySubscribeListener; + + document.getElementById("currentListTab").disabled = true; + document.getElementById("newGroupsTab").disabled = true; + document.getElementById("refreshButton").disabled = true; + + gStatusFeedback._startMeteors(); + gStatusFeedback.setStatusString(""); + gStatusFeedback.showStatusString( + gSubscribeBundle.getString("pleaseWaitString") + ); + document.getElementById("stopButton").disabled = false; + + gSubscribableServer.startPopulating(msgWindow, forceToServer, getOnlyNew); + } catch (e) { + if (e.result == 0x80550014) { + // NS_MSG_ERROR_OFFLINE + gStatusFeedback.setStatusString( + gSubscribeBundle.getString("offlineState") + ); + } else { + console.error("Failed to populate subscribe tree: " + e); + gStatusFeedback.setStatusString( + gSubscribeBundle.getString("errorPopulating") + ); + } + Stop(); + } +} + +function SubscribeOnUnload() { + try { + CleanUpSearchView(); + } catch (ex) { + dump("Failed to remove the subscribe tree: " + ex + "\n"); + } + + msgWindow.closeWindow(); +} + +function EnableSearchUI() { + if (gSubscribableServer.supportsSubscribeSearch) { + gNameField.removeAttribute("disabled"); + gNameFieldLabel.removeAttribute("disabled"); + } else { + gNameField.setAttribute("disabled", true); + gNameFieldLabel.setAttribute("disabled", true); + } +} + +function SubscribeOnLoad() { + gSubscribeBundle = document.getElementById("bundle_subscribe"); + + gSubscribeTree = document.getElementById("subscribeTree"); + gSubscribeBody = document.getElementById("subscribeTreeBody"); + gSearchTree = document.getElementById("searchTree"); + gSearchTree = document.getElementById("searchTree"); + gNameField = document.getElementById("namefield"); + gNameFieldLabel = document.getElementById("namefieldlabel"); + + // eslint-disable-next-line no-global-assign + msgWindow = Cc["@mozilla.org/messenger/msgwindow;1"].createInstance( + Ci.nsIMsgWindow + ); + msgWindow.domWindow = window; + gStatusFeedback = new nsMsgStatusFeedback(); + msgWindow.statusFeedback = gStatusFeedback; + msgWindow.rootDocShell.allowAuth = true; + msgWindow.rootDocShell.appType = Ci.nsIDocShell.APP_TYPE_MAIL; + + // look in arguments[0] for parameters + if (window.arguments && window.arguments[0]) { + if (window.arguments[0].okCallback) { + top.okCallback = window.arguments[0].okCallback; + } + } + + var serverMenu = document.getElementById("serverMenu"); + + gServerURI = null; + let folder = + "folder" in window.arguments[0] ? window.arguments[0].folder : null; + if (folder && folder.server instanceof Ci.nsISubscribableServer) { + serverMenu.menupopup.selectFolder(folder.server.rootMsgFolder); + try { + CleanUpSearchView(); + gSubscribableServer = folder.server.QueryInterface( + Ci.nsISubscribableServer + ); + // Enable (or disable) the search related UI. + EnableSearchUI(); + gServerURI = folder.server.serverURI; + } catch (ex) { + // dump("not a subscribable server\n"); + CleanUpSearchView(); + gSubscribableServer = null; + gServerURI = null; + } + } + + if (!gServerURI) { + // dump("subscribe: no uri\n"); + // dump("xxx todo: use the default news server. right now, I'm just using the first server\n"); + + serverMenu.selectedIndex = 0; + + if (serverMenu.selectedItem) { + gServerURI = serverMenu.selectedItem.getAttribute("id"); + } else { + // dump("xxx todo none of your servers are subscribable\n"); + // dump("xxx todo fix this by disabling subscribe if no subscribable server or, add a CREATE SERVER button, like in 4.x\n"); + return; + } + } + + SetServerTypeSpecificTextValues(); + + ShowCurrentList(); + + gNameField.focus(); +} + +function subscribeOK() { + if (top.okCallback) { + top.okCallback(top.gChangeTable); + } + Stop(); + if (gSubscribableServer) { + gSubscribableServer.subscribeCleanup(); + } +} + +function subscribeCancel() { + Stop(); + if (gSubscribableServer) { + gSubscribableServer.subscribeCleanup(); + } +} + +function SetState(name, state) { + var changed = gSubscribableServer.setState(name, state); + if (changed) { + StateChanged(name, state); + } +} + +function StateChanged(name, state) { + if (gServerURI in gChangeTable) { + if (name in gChangeTable[gServerURI]) { + var oldValue = gChangeTable[gServerURI][name]; + if (oldValue != state) { + delete gChangeTable[gServerURI][name]; + } + } else { + gChangeTable[gServerURI][name] = state; + } + } else { + gChangeTable[gServerURI] = {}; + gChangeTable[gServerURI][name] = state; + } +} + +function InSearchMode() { + return !document.getElementById("searchView").hidden; +} + +function SearchOnClick(event) { + // We only care about button 0 (left click) events. + if (event.button != 0 || event.target.localName != "treechildren") { + return; + } + + let treeCellInfo = gSearchTree.getCellAt(event.clientX, event.clientY); + if (treeCellInfo.row == -1 || treeCellInfo.row > gSearchView.rowCount - 1) { + return; + } + + if (treeCellInfo.col.id == "subscribedColumn2") { + if (event.detail != 2) { + // Single clicked on the check box + // (in the "subscribedColumn2" column) reverse state. + // If double click, do nothing. + ReverseStateFromRow(treeCellInfo.row); + } + } else if (event.detail == 2) { + // Double clicked on a row, reverse state. + ReverseStateFromRow(treeCellInfo.row); + } + + // Invalidate the row. + InvalidateSearchTreeRow(treeCellInfo.row); +} + +function ReverseStateFromRow(aRow) { + // To determine if the row is subscribed or not, + // we get the properties for the "subscribedColumn2" cell in the row + // and look for the "subscribed" property. + // If the "subscribed" string is in the list of properties + // we are subscribed. + let col = gSearchTree.columns.nameColumn2; + let name = gSearchView.getCellValue(aRow, col); + let isSubscribed = gSubscribableServer.isSubscribed(name); + SetStateFromRow(aRow, !isSubscribed); +} + +function SetStateFromRow(row, state) { + var col = gSearchTree.columns.nameColumn2; + var name = gSearchView.getCellValue(row, col); + SetState(name, state); +} + +function SetSubscribeState(state) { + try { + // We need to iterate over the tree selection, and set the state for + // all rows in the selection. + var inSearchMode = InSearchMode(); + var view = inSearchMode ? gSearchView : gSubscribeTree.view; + var colId = inSearchMode ? "nameColumn2" : "nameColumn"; + + var sel = view.selection; + for (var i = 0; i < sel.getRangeCount(); ++i) { + var start = {}, + end = {}; + sel.getRangeAt(i, start, end); + for (var k = start.value; k <= end.value; ++k) { + if (inSearchMode) { + SetStateFromRow(k, state); + } else { + let name = view.getCellValue(k, gSubscribeTree.columns[colId]); + SetState(name, state, k); + } + } + } + + if (inSearchMode) { + // Force a repaint. + InvalidateSearchTree(); + } else { + gSubscribeTree.invalidate(); + } + } catch (ex) { + dump("SetSubscribedState failed: " + ex + "\n"); + } +} + +function ReverseStateFromNode(row) { + let name = gSubscribeTree.view.getCellValue( + row, + gSubscribeTree.columns.nameColumn + ); + SetState(name, !gSubscribableServer.isSubscribed(name), row); +} + +function SubscribeOnClick(event) { + // We only care about button 0 (left click) events. + if (event.button != 0 || event.target.localName != "treechildren") { + return; + } + + let treeCellInfo = gSubscribeTree.getCellAt(event.clientX, event.clientY); + if ( + treeCellInfo.row == -1 || + treeCellInfo.row > gSubscribeTree.view.rowCount - 1 + ) { + return; + } + + if (event.detail == 2) { + // Only toggle subscribed state when double clicking something + // that isn't a container. + if (!gSubscribeTree.view.isContainer(treeCellInfo.row)) { + ReverseStateFromNode(treeCellInfo.row); + } + } else if (event.detail == 1) { + // If the user single clicks on the subscribe check box, we handle it here. + if (treeCellInfo.col.id == "subscribedColumn") { + ReverseStateFromNode(treeCellInfo.row); + } + } +} + +function Refresh() { + // Clear out the textfield's entry. + gNameField.value = ""; + + var newGroupsTab = document.getElementById("newGroupsTab"); + SetUpTree(true, newGroupsTab.selected); +} + +function ShowCurrentList() { + // Clear out the textfield's entry on call of Refresh(). + gNameField.value = ""; + + // Make sure the current list tab is selected. + document.getElementById("subscribeTabs").selectedIndex = 0; + + // Try loading the hostinfo before talk to server. + SetUpTree(false, false); +} + +function ShowNewGroupsList() { + // Clear out the textfield's entry. + gNameField.value = ""; + + // Make sure the new groups tab is selected. + document.getElementById("subscribeTabs").selectedIndex = 1; + + // Force it to talk to the server and get new groups. + SetUpTree(true, true); +} + +function InvalidateSearchTreeRow(row) { + gSearchTree.invalidateRow(row); +} + +function InvalidateSearchTree() { + gSearchTree.invalidate(); +} + +/** + * Toggle the tree panel in the dialog between search view and subscribe view. + * + * @param {boolean} toggle - If true, show the search view else show the + * subscribe view. + */ +function toggleSubscriptionView(toggle) { + document.getElementById("subscribeView").hidden = toggle; + document.getElementById("searchView").hidden = !toggle; +} + +function Search() { + let searchValue = gNameField.value; + if ( + searchValue.length && + gSubscribableServer && + gSubscribableServer.supportsSubscribeSearch + ) { + toggleSubscriptionView(true); + gSubscribableServer.setSearchValue(searchValue); + + if (!gSearchView && gSubscribableServer) { + gSearchView = gSubscribableServer.QueryInterface(Ci.nsITreeView); + gSearchView.selection = null; + gSearchTree.view = gSearchView; + } + return; + } + toggleSubscriptionView(false); +} + +function CleanUpSearchView() { + if (gSearchView) { + gSearchView.selection = null; + gSearchView = null; + } +} + +function onSearchTreeKeyPress(event) { + // For now, only do something on space key. + if (event.charCode != KeyEvent.DOM_VK_SPACE) { + return; + } + + var treeSelection = gSearchView.selection; + for (let i = 0; i < treeSelection.getRangeCount(); i++) { + var start = {}, + end = {}; + treeSelection.getRangeAt(i, start, end); + for (let k = start.value; k <= end.value; k++) { + ReverseStateFromRow(k); + } + + // Force a repaint. + InvalidateSearchTree(); + } +} + +function onSubscribeTreeKeyPress(event) { + // For now, only do something on space key. + if (event.charCode != KeyEvent.DOM_VK_SPACE) { + return; + } + + var treeSelection = gSubscribeTree.view.selection; + for (let i = 0; i < treeSelection.getRangeCount(); i++) { + var start = {}, + end = {}; + treeSelection.getRangeAt(i, start, end); + for (let k = start.value; k <= end.value; k++) { + ReverseStateFromNode(k); + } + } +} diff --git a/comm/mailnews/base/content/subscribe.xhtml b/comm/mailnews/base/content/subscribe.xhtml new file mode 100644 index 0000000000..f2838b95e2 --- /dev/null +++ b/comm/mailnews/base/content/subscribe.xhtml @@ -0,0 +1,235 @@ + + + + + + + + + + + + + + + + &subscribeDialog.title; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/virtualFolderListEdit.js b/comm/mailnews/base/content/virtualFolderListEdit.js new file mode 100644 index 0000000000..775d3ba1c7 --- /dev/null +++ b/comm/mailnews/base/content/virtualFolderListEdit.js @@ -0,0 +1,206 @@ +/* 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 PROTO_TREE_VIEW */ + +var { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + FolderUtils: "resource:///modules/FolderUtils.jsm", + MailUtils: "resource:///modules/MailUtils.jsm", +}); + +window.addEventListener("DOMContentLoaded", () => { + gSelectVirtual.load(); +}); + +var gFolderTreeView = new PROTO_TREE_VIEW(); + +var gSelectVirtual = { + _treeElement: null, + _selectedList: new Set(), + + load() { + if (window.arguments[0].searchFolderURIs) { + let srchFolderUriArray = window.arguments[0].searchFolderURIs.split("|"); + for (let uri of srchFolderUriArray) { + this._selectedList.add(MailUtils.getOrCreateFolder(uri)); + } + } + + // Add the top level of the folder tree. + for (let account of FolderUtils.allAccountsSorted(true)) { + let server = account.incomingServer; + if ( + server instanceof Ci.nsIPop3IncomingServer && + server.deferredToAccount + ) { + continue; + } + + gFolderTreeView._rowMap.push(new FolderRow(server.rootFolder)); + } + + // Recursively expand the tree to show all selected folders. + function expandToSelected(row, i) { + hiddenFolders.delete(row._folder); + for (let folder of hiddenFolders) { + if (row._folder.isAncestorOf(folder)) { + gFolderTreeView.toggleOpenState(i); + for (let j = row.children.length - 1; j >= 0; j--) { + expandToSelected(row.children[j], i + j + 1); + } + break; + } + } + } + + let hiddenFolders = new Set(gSelectVirtual._selectedList); + for (let i = gFolderTreeView.rowCount - 1; i >= 0; i--) { + expandToSelected(gFolderTreeView._rowMap[i], i); + } + + this._treeElement = document.getElementById("folderPickerTree"); + this._treeElement.view = gFolderTreeView; + }, + + onKeyPress(aEvent) { + // For now, only do something on space key. + if (aEvent.charCode != aEvent.DOM_VK_SPACE) { + return; + } + + let selection = this._treeElement.view.selection; + let start = {}; + let end = {}; + let numRanges = selection.getRangeCount(); + + for (let range = 0; range < numRanges; range++) { + selection.getRangeAt(range, start, end); + for (let i = start.value; i <= end.value; i++) { + this._toggle(i); + } + } + }, + + onClick(aEvent) { + // We only care about button 0 (left click) events. + if (aEvent.button != 0) { + return; + } + + // We don't want to toggle when clicking on header or tree (scrollbar) or + // on treecol. + if (aEvent.target.nodeName != "treechildren") { + return; + } + + let treeCellInfo = this._treeElement.getCellAt( + aEvent.clientX, + aEvent.clientY + ); + if (treeCellInfo.row == -1 || treeCellInfo.col.id != "selectedCol") { + return; + } + + this._toggle(treeCellInfo.row); + }, + + _toggle(aRow) { + let folder = gFolderTreeView._rowMap[aRow]._folder; + if (this._selectedList.has(folder)) { + this._selectedList.delete(folder); + } else { + this._selectedList.add(folder); + } + + gFolderTreeView._tree.invalidateRow(aRow); + }, + + onAccept() { + // XXX We should just pass the folder objects around... + let uris = [...this._selectedList.values()] + .map(folder => folder.URI) + .join("|"); + + if (window.arguments[0].okCallback) { + window.arguments[0].okCallback(uris); + } + }, +}; + +document.addEventListener("dialogaccept", () => gSelectVirtual.onAccept()); + +/** + * A tree row representing a single folder. + */ +class FolderRow { + constructor(folder, parent = null) { + this._folder = folder; + this._open = false; + this._level = parent ? parent.level + 1 : 0; + this._parent = parent; + this._children = null; + } + + get id() { + return this._folder.URI; + } + + get text() { + return this.getText("folderNameCol"); + } + + getText(aColName) { + switch (aColName) { + case "folderNameCol": + return this._folder.abbreviatedName; + default: + return ""; + } + } + + get open() { + return this._open; + } + + get level() { + return this._level; + } + + getProperties(column) { + let properties = ""; + switch (column?.id) { + case "folderNameCol": + // From folderUtils.jsm. + properties = FolderUtils.getFolderProperties(this._folder, this.open); + break; + case "selectedCol": + properties = "selectedColumn"; + if (gSelectVirtual._selectedList.has(this._folder)) { + properties += " selected-true"; + } + break; + } + return properties; + } + + get children() { + if (this._children === null) { + this._children = []; + for (let subFolder of this._folder.subFolders) { + if (!subFolder.getFlag(Ci.nsMsgFolderFlags.Virtual)) { + this._children.push(new FolderRow(subFolder, this)); + } + } + this._children.sort((a, b) => a._folder.compareSortKeys(b._folder)); + } + return this._children; + } +} diff --git a/comm/mailnews/base/content/virtualFolderListEdit.xhtml b/comm/mailnews/base/content/virtualFolderListEdit.xhtml new file mode 100644 index 0000000000..326892734a --- /dev/null +++ b/comm/mailnews/base/content/virtualFolderListEdit.xhtml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + &virtualFolderListTitle.title; + + + + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/content/virtualFolderProperties.js b/comm/mailnews/base/content/virtualFolderProperties.js new file mode 100644 index 0000000000..10429ec3d6 --- /dev/null +++ b/comm/mailnews/base/content/virtualFolderProperties.js @@ -0,0 +1,383 @@ +/* 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 ../../search/content/searchTerm.js */ + +var gPickedFolder; +var gMailView = null; +var msgWindow; // important, don't change the name of this variable. it's really a global used by commandglue.js +var gSearchTermSession; // really an in memory temporary filter we use to read in and write out the search terms +var gSearchFolderURIs = ""; +var gMessengerBundle = null; +var gFolderBundle = null; +var gDefaultColor = ""; +var gMsgFolder; + +var { FolderTreeProperties } = ChromeUtils.import( + "resource:///modules/FolderTreeProperties.jsm" +); +var { FolderUtils } = ChromeUtils.import("resource:///modules/FolderUtils.jsm"); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); +var { VirtualFolderHelper } = ChromeUtils.import( + "resource:///modules/VirtualFolderWrapper.jsm" +); + +window.addEventListener("DOMContentLoaded", onLoad); + +document.addEventListener("dialogaccept", onOK); +document.addEventListener("dialogcancel", onCancel); + +function onLoad() { + var windowArgs = window.arguments[0]; + var acceptButton = document.querySelector("dialog").getButton("accept"); + + gMessengerBundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + + gFolderBundle = Services.strings.createBundle( + "chrome://messenger/locale/folderWidgets.properties" + ); + + // call this when OK is pressed + msgWindow = windowArgs.msgWindow; // eslint-disable-line no-global-assign + + initializeSearchWidgets(); + + setSearchScope(Ci.nsMsgSearchScope.offlineMail); + if (windowArgs.editExistingFolder) { + acceptButton.label = document + .querySelector("dialog") + .getAttribute("editFolderAcceptButtonLabel"); + acceptButton.accesskey = document + .querySelector("dialog") + .getAttribute("editFolderAcceptButtonAccessKey"); + InitDialogWithVirtualFolder(windowArgs.folder); + } else { + // we are creating a new virtual folder + acceptButton.label = document + .querySelector("dialog") + .getAttribute("newFolderAcceptButtonLabel"); + acceptButton.accesskey = document + .querySelector("dialog") + .getAttribute("newFolderAcceptButtonAccessKey"); + // it is possible that we were given arguments to pre-fill the dialog with... + gSearchTermSession = Cc[ + "@mozilla.org/messenger/searchSession;1" + ].createInstance(Ci.nsIMsgSearchSession); + + if (windowArgs.searchTerms) { + // then add them to our search session + for (let searchTerm of windowArgs.searchTerms) { + gSearchTermSession.appendTerm(searchTerm); + } + } + if (windowArgs.folder) { + // pre select the folderPicker, based on what they selected in the folder pane + gPickedFolder = windowArgs.folder; + try { + document + .getElementById("msgNewFolderPopup") + .selectFolder(windowArgs.folder); + } catch (ex) { + document + .getElementById("msgNewFolderPicker") + .setAttribute("label", windowArgs.folder.prettyName); + } + + // if the passed in URI is not a server then pre-select it as the folder to search + if (!windowArgs.folder.isServer) { + gSearchFolderURIs = windowArgs.folder.URI; + } + } + + let folderNameField = document.getElementById("name"); + folderNameField.removeAttribute("hidden"); + folderNameField.focus(); + if (windowArgs.newFolderName) { + folderNameField.value = windowArgs.newFolderName; + } + if (windowArgs.searchFolderURIs) { + gSearchFolderURIs = windowArgs.searchFolderURIs; + } + + setupSearchRows(gSearchTermSession.searchTerms); + doEnabling(); // we only need to disable/enable the OK button for new virtual folders + } + + if (typeof windowArgs.searchOnline != "undefined") { + document.getElementById("searchOnline").checked = windowArgs.searchOnline; + } + updateOnlineSearchState(); + updateFoldersCount(); +} + +function setupSearchRows(aSearchTerms) { + if (aSearchTerms && aSearchTerms.length > 0) { + // Load the search terms for the folder. + initializeSearchRows(Ci.nsMsgSearchScope.offlineMail, aSearchTerms); + } else { + onMore(null); + } +} + +function updateOnlineSearchState() { + var enableCheckbox = false; + var checkbox = document.getElementById("searchOnline"); + // only enable the checkbox for selection, for online servers + var srchFolderUriArray = gSearchFolderURIs.split("|"); + if (srchFolderUriArray[0]) { + var realFolder = MailUtils.getOrCreateFolder(srchFolderUriArray[0]); + enableCheckbox = realFolder.server.offlineSupportLevel; // anything greater than 0 is an online server like IMAP or news + } + + if (enableCheckbox) { + checkbox.removeAttribute("disabled"); + } else { + checkbox.setAttribute("disabled", true); + checkbox.checked = false; + } +} + +function InitDialogWithVirtualFolder(aVirtualFolder) { + let virtualFolderWrapper = VirtualFolderHelper.wrapVirtualFolder( + window.arguments[0].folder + ); + gMsgFolder = window.arguments[0].folder; + + let styles = getComputedStyle(document.body); + let folderColors = { + Inbox: styles.getPropertyValue("--folder-color-inbox"), + Sent: styles.getPropertyValue("--folder-color-sent"), + Outbox: styles.getPropertyValue("--folder-color-outbox"), + Drafts: styles.getPropertyValue("--folder-color-draft"), + Trash: styles.getPropertyValue("--folder-color-trash"), + Archive: styles.getPropertyValue("--folder-color-archive"), + Templates: styles.getPropertyValue("--folder-color-template"), + Junk: styles.getPropertyValue("--folder-color-spam"), + Virtual: styles.getPropertyValue("--folder-color-folder-filter"), + RSS: styles.getPropertyValue("--folder-color-rss"), + Newsgroup: styles.getPropertyValue("--folder-color-newsletter"), + }; + gDefaultColor = styles.getPropertyValue("--folder-color-folder"); + + // when editing an existing folder, hide the folder picker that stores the parent location of the folder + document.getElementById("msgNewFolderPicker").collapsed = true; + let items = document.getElementsByClassName("chooseFolderLocation"); + for (let item of items) { + item.setAttribute("hidden", true); + } + let folderNameField = document.getElementById("existingName"); + folderNameField.removeAttribute("hidden"); + + // Show the icon color options. + document.getElementById("iconColorContainer").collapsed = false; + + let folderType = FolderUtils.getSpecialFolderString(gMsgFolder); + if (folderType in folderColors) { + gDefaultColor = folderColors[folderType]; + } + + let colorInput = document.getElementById("color"); + colorInput.value = + FolderTreeProperties.getColor(aVirtualFolder.URI) || gDefaultColor; + colorInput.addEventListener("input", event => { + // Preview the chosen color. + Services.obs.notifyObservers( + gMsgFolder, + "folder-color-preview", + colorInput.value + ); + }); + let resetColorButton = document.getElementById("resetColor"); + resetColorButton.addEventListener("click", function () { + colorInput.value = gDefaultColor; + // Preview the default color. + Services.obs.notifyObservers( + gMsgFolder, + "folder-color-preview", + gDefaultColor + ); + }); + + gSearchFolderURIs = virtualFolderWrapper.searchFolderURIs; + updateFoldersCount(); + document.getElementById("searchOnline").checked = + virtualFolderWrapper.onlineSearch; + gSearchTermSession = virtualFolderWrapper.searchTermsSession; + + setupSearchRows(gSearchTermSession.searchTerms); + + // set the name of the folder + let name = gFolderBundle.formatStringFromName("verboseFolderFormat", [ + aVirtualFolder.prettyName, + aVirtualFolder.server.prettyName, + ]); + folderNameField.setAttribute("value", name); + // update the window title based on the name of the saved search + document.title = gMessengerBundle.formatStringFromName( + "editVirtualFolderPropertiesTitle", + [aVirtualFolder.prettyName] + ); +} + +function onFolderPick(aEvent) { + gPickedFolder = aEvent.target._folder; + document.getElementById("msgNewFolderPopup").selectFolder(gPickedFolder); +} + +function onOK(event) { + var name = document.getElementById("name").value; + var searchOnline = document.getElementById("searchOnline").checked; + + if (!gSearchFolderURIs) { + Services.prompt.alert( + window, + null, + gMessengerBundle.GetStringFromName("alertNoSearchFoldersSelected") + ); + event.preventDefault(); + return; + } + + if (window.arguments[0].editExistingFolder) { + // update the search terms + gSearchTermSession.searchTerms = saveSearchTerms( + gSearchTermSession.searchTerms, + gSearchTermSession + ); + // save the settings + let virtualFolderWrapper = VirtualFolderHelper.wrapVirtualFolder( + window.arguments[0].folder + ); + virtualFolderWrapper.searchTerms = gSearchTermSession.searchTerms; + virtualFolderWrapper.searchFolders = gSearchFolderURIs; + virtualFolderWrapper.onlineSearch = searchOnline; + virtualFolderWrapper.cleanUpMessageDatabase(); + + MailServices.accounts.saveVirtualFolders(); + + let color = document.getElementById("color").value; + if (color == gDefaultColor) { + color = undefined; + } + FolderTreeProperties.setColor(gMsgFolder.URI, color); + // Tell 3-pane tabs to update the folder's color. + Services.obs.notifyObservers(gMsgFolder, "folder-color-changed", color); + + if (window.arguments[0].onOKCallback) { + window.arguments[0].onOKCallback(); + } + + return; + } + + var uri = gPickedFolder.URI; + if (name && uri) { + // create a new virtual folder + // check to see if we already have a folder with the same name and alert the user if so... + var parentFolder = MailUtils.getOrCreateFolder(uri); + + // sanity check the name based on the logic used by nsMsgBaseUtils.cpp. It can't start with a '.', it can't end with a '.', '~' or ' '. + // it can't contain a ';' or '#'. + if (/^\.|[\.\~ ]$|[\;\#]/.test(name)) { + Services.prompt.alert( + window, + null, + gMessengerBundle.GetStringFromName("folderCreationFailed") + ); + event.preventDefault(); + return; + } else if (parentFolder.containsChildNamed(name)) { + Services.prompt.alert( + window, + null, + gMessengerBundle.GetStringFromName("folderExists") + ); + event.preventDefault(); + return; + } + + gSearchTermSession.searchTerms = saveSearchTerms( + gSearchTermSession.searchTerms, + gSearchTermSession + ); + VirtualFolderHelper.createNewVirtualFolder( + name, + parentFolder, + gSearchFolderURIs, + gSearchTermSession.searchTerms, + searchOnline + ); + } +} + +function onCancel(event) { + if (gMsgFolder) { + // Clear any previewed color. + Services.obs.notifyObservers(gMsgFolder, "folder-color-preview"); + } +} + +function doEnabling() { + var acceptButton = document.querySelector("dialog").getButton("accept"); + acceptButton.disabled = !document.getElementById("name").value; +} + +function chooseFoldersToSearch() { + // if we have some search folders already, then root the folder picker dialog off the account + // for those folders. Otherwise fall back to the preselectedfolderURI which is the parent folder + // for this new virtual folder. + window.openDialog( + "chrome://messenger/content/virtualFolderListEdit.xhtml", + "", + "chrome,titlebar,modal,centerscreen,resizable", + { + searchFolderURIs: gSearchFolderURIs, + okCallback: onFolderListDialogCallback, + } + ); +} + +// callback routine from chooseFoldersToSearch +function onFolderListDialogCallback(searchFolderURIs) { + gSearchFolderURIs = searchFolderURIs; + updateFoldersCount(); + updateOnlineSearchState(); // we may have changed the server type we are searching... +} + +function updateFoldersCount() { + let srchFolderUriArray = gSearchFolderURIs.split("|"); + let folderCount = gSearchFolderURIs ? srchFolderUriArray.length : 0; + let foldersList = document.getElementById("chosenFoldersCount"); + foldersList.textContent = PluralForm.get( + folderCount, + gMessengerBundle.GetStringFromName("virtualFolderSourcesChosen") + ).replace("#1", folderCount); + if (folderCount > 0) { + let folderNames = []; + for (let folderURI of srchFolderUriArray) { + let folder = MailUtils.getOrCreateFolder(folderURI); + let name = this.gMessengerBundle.formatStringFromName( + "verboseFolderFormat", + [folder.prettyName, folder.server.prettyName] + ); + folderNames.push(name); + } + foldersList.setAttribute("tooltiptext", folderNames.join("\n")); + } else { + foldersList.removeAttribute("tooltiptext"); + } +} + +function onEnterInSearchTerm() { + // stub function called by the core search widget code... + // nothing for us to do here +} diff --git a/comm/mailnews/base/content/virtualFolderProperties.xhtml b/comm/mailnews/base/content/virtualFolderProperties.xhtml new file mode 100644 index 0000000000..07b8283f84 --- /dev/null +++ b/comm/mailnews/base/content/virtualFolderProperties.xhtml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + %folderDTD; + + %folderPropsDTD; + + %searchTermDTD; +]> + + + &virtualFolderProperties.title; + + + + + + + + + + + + + + + + + diff --git a/comm/mailnews/base/moz.build b/comm/mailnews/base/moz.build new file mode 100644 index 0000000000..a49689ab64 --- /dev/null +++ b/comm/mailnews/base/moz.build @@ -0,0 +1,11 @@ +# 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/. + +DIRS += [ + "public", + "src", +] + +TEST_DIRS += ["test"] diff --git a/comm/mailnews/base/prefs/.eslintrc.js b/comm/mailnews/base/prefs/.eslintrc.js new file mode 100644 index 0000000000..5816519fbb --- /dev/null +++ b/comm/mailnews/base/prefs/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/valid-jsdoc"], +}; diff --git a/comm/mailnews/base/prefs/content/AccountManager.js b/comm/mailnews/base/prefs/content/AccountManager.js new file mode 100644 index 0000000000..8b319d51d4 --- /dev/null +++ b/comm/mailnews/base/prefs/content/AccountManager.js @@ -0,0 +1,1949 @@ +/* -*- Mode: JavaScript; tab-width: 4; 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/. */ + +/** + * Here's how this dialog works: + * The main dialog contains a tree on the left (id="accounttree") and an + * iframe which loads a particular preference document (such as am-main.xhtml) + * on the right. + * + * When the user clicks on items in the tree on the left, two things have + * to be determined before the UI can be updated: + * - the relevant account + * - the relevant page + * + * When both of these are known, this is what happens: + * - every form element of the previous page is saved in the account value + * hashtable for the previous account + * - the relevant page is loaded into the iframe + * - each form element in the page is filled in with an appropriate value + * from the current account's hashtable + * - in the iframe inside the page, if there is an onInit() method, + * it is called. The onInit method can further update this page based + * on values set in the previous step. + */ + +/* import-globals-from accountUtils.js */ +/* import-globals-from am-prefs.js */ +/* import-globals-from amUtils.js */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm"); +var { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm"); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { UIDensity } = ChromeUtils.import("resource:///modules/UIDensity.jsm"); +var { UIFontSize } = ChromeUtils.import("resource:///modules/UIFontSize.jsm"); + +ChromeUtils.defineModuleGetter( + this, + "FolderUtils", + "resource:///modules/FolderUtils.jsm" +); +var { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import( + "resource:///modules/hostnameUtils.jsm" +); +var { ChatIcons } = ChromeUtils.importESModule( + "resource:///modules/chatIcons.sys.mjs" +); + +XPCOMUtils.defineLazyGetter(this, "gSubDialog", function () { + const { SubDialogManager } = ChromeUtils.importESModule( + "resource://gre/modules/SubDialog.sys.mjs" + ); + return new SubDialogManager({ + dialogStack: document.getElementById("dialogStack"), + dialogTemplate: document.getElementById("dialogTemplate"), + dialogOptions: { + styleSheets: [ + "chrome://messenger/skin/preferences/dialog.css", + "chrome://messenger/skin/preferences/preferences.css", + ], + resizeCallback: ({ title, frame }) => { + UIFontSize.registerWindow(frame.contentWindow); + + // Resize the dialog to fit the content with edited font size. + requestAnimationFrame(() => { + let dialogs = frame.ownerGlobal.gSubDialog._dialogs; + let dialog = dialogs.find( + d => d._frame.contentDocument == frame.contentDocument + ); + if (dialog) { + UIFontSize.resizeSubDialog(dialog); + } + }); + }, + }, + }); +}); + +// If Local directory has changed the app needs to restart. Once this is set +// a restart will be attempted at each attempt to close the Account manager with OK. +var gRestartNeeded = false; + +// This is a hash-map for every account we've touched in the pane. Each entry +// has additional maps of attribute-value pairs that we're going to want to save +// when the user hits OK. +var accountArray; +var gGenericAttributeTypes; + +var currentAccount; +var currentPageId; + +var pendingAccount; +var pendingPageId; + +/** + * This array contains filesystem folders that are deemed inappropriate + * for use as the local directory pref for message storage. + * It is global to allow extensions to add to/remove from it if needed. + * Extensions adding new server types should first consider setting + * nsIMsgProtocolInfo(of the server type).defaultLocalPath properly + * so that the test will allow that directory automatically. + * See the checkLocalDirectoryIsSafe function for description of the members. + */ +var gDangerousLocalStorageDirs = [ + // profile folder + { dirsvc: "ProfD", OS: null }, + // GRE install folder + { dirsvc: "GreD", OS: null }, + // Application install folder + { dirsvc: "CurProcD", OS: null }, + // system temporary folder + { dirsvc: "TmpD", OS: null }, + // Windows system folder + { dirsvc: "SysD", OS: "WINNT" }, + // Windows folder + { dirsvc: "WinD", OS: "WINNT" }, + // Program Files folder + { dirsvc: "ProgF", OS: "WINNT" }, + // trash folder + { dirsvc: "Trsh", OS: "Darwin" }, + // Mac OS system folder + { dir: "/System", OS: "Darwin" }, + // devices folder + { dir: "/dev", OS: "Darwin,Linux" }, + // process info folder + { dir: "/proc", OS: "Linux" }, + // system state folder + { dir: "/sys", OS: "Linux" }, +]; + +// This sets an attribute in a xul element so that we can later +// know what value to substitute in a prefstring. Different +// preference types set different attributes. We get the value +// in the same way as the function getAccountValue() determines it. +function updateElementWithKeys(account, element, type) { + switch (type) { + case "identity": + element.identitykey = account.defaultIdentity.key; + break; + case "pop3": + case "imap": + case "nntp": + case "server": + element.serverkey = account.incomingServer.key; + break; + case "smtp": + if (MailServices.smtp.defaultServer) { + element.serverkey = MailServices.smtp.defaultServer.key; + } + break; + default: + // dump("unknown element type! "+type+"\n"); + } +} + +// called when the whole document loads +// perform initialization here +function onLoad() { + let selectedServer = document.documentElement.server; + let selectPage = document.documentElement.selectPage || null; + + // Arguments can have two properties: (1) "server," the nsIMsgIncomingServer + // to select initially and (2) "selectPage," the page for that server to that + // should be selected. + + accountArray = {}; + gGenericAttributeTypes = {}; + + gAccountTree.load(); + + setTimeout(selectServer, 0, selectedServer, selectPage); + + let contentFrame = document.getElementById("contentFrame"); + contentFrame.addEventListener("load", event => { + let inputElements = contentFrame.contentDocument.querySelectorAll( + "checkbox, input, menulist, textarea, radiogroup, richlistbox" + ); + contentFrame.contentDocument.addEventListener("prefchange", event => { + onAccept(true); + }); + for (let input of inputElements) { + if (input.localName == "input" || input.localName == "textarea") { + input.addEventListener("change", event => { + onAccept(true); + }); + } else { + input.addEventListener("command", event => { + onAccept(true); + }); + } + } + UIFontSize.registerWindow(contentFrame.contentWindow); + }); + + UIDensity.registerWindow(window); + UIFontSize.registerWindow(window); +} + +function onUnload() { + gAccountTree.unload(); +} + +function selectServer(server, selectPageId) { + let accountTree = document.getElementById("accounttree"); + + // Default to showing the first account. + let accountRow = accountTree.rows[0]; + + // Find the tree-node for the account we want to select. + if (server) { + for (let row of accountTree.children) { + let account = row._account; + if (account && server == account.incomingServer) { + accountRow = row; + // Make sure all the panes of the account to be selected are shown. + accountTree.expandRow(accountRow); + break; + } + } + } + + let pageToSelect = accountRow; + + if (selectPageId) { + // Find the page that also corresponds to this server. + // It either is the accountRow itself... + let pageId = accountRow.getAttribute("PageTag"); + if (pageId != selectPageId) { + // ... or one of its children. + pageToSelect = accountRow.querySelector( + '[PageTag="' + selectPageId + '"]' + ); + } + } + + accountTree.selectedIndex = accountTree.rows.indexOf(pageToSelect); +} + +function replaceWithDefaultSmtpServer(deletedSmtpServerKey) { + // First we replace the smtpserverkey in every identity. + for (let identity of MailServices.accounts.allIdentities) { + if (identity.smtpServerKey == deletedSmtpServerKey) { + identity.smtpServerKey = ""; + } + } + + // When accounts have already been loaded in the panel then the first + // replacement will be overwritten when the accountvalues are written out + // from the pagedata. We get the loaded accounts and check to make sure + // that the account exists for the accountid and that it has a default + // identity associated with it (to exclude smtpservers and local folders) + // Then we check only for the identity[type] and smtpServerKey[slot] and + // replace that with the default smtpserverkey if necessary. + + for (var accountid in accountArray) { + var account = accountArray[accountid]._account; + if (account && account.defaultIdentity) { + var accountValues = accountArray[accountid]; + var smtpServerKey = getAccountValue( + account, + accountValues, + "identity", + "smtpServerKey", + null, + false + ); + if (smtpServerKey == deletedSmtpServerKey) { + setAccountValue(accountValues, "identity", "smtpServerKey", ""); + } + } + } +} + +/** + * Called when OK is clicked on the dialog. + * + * @param {boolean} aDoChecks - If true, execute checks on data, otherwise hope + * they were already done elsewhere and proceed directly to saving the data. + */ +function onAccept(aDoChecks) { + if (aDoChecks) { + // Check if user/host have been modified correctly. + if (!checkUserServerChanges(true)) { + return false; + } + + if (!checkAccountNameIsValid()) { + return false; + } + } + + // Run checks as if the page was being left. + if ("onLeave" in top.frames.contentFrame) { + if (!top.frames.contentFrame.onLeave()) { + // Prevent closing Account manager if user declined the changes. + return false; + } + } + + if (!onSave()) { + return false; + } + + // hack hack - save the prefs file NOW in case we crash + Services.prefs.savePrefFile(null); + + if (gRestartNeeded) { + MailUtils.restartApplication(); + // Prevent closing Account manager in case restart failed. If restart did not fail, + // return value does not matter, as we are restarting. + return false; + } + + return true; +} + +/** + * See if the given path to a directory is usable on the current OS. + * + * aLocalPath the nsIFile of a directory to check. + */ +function checkDirectoryIsValid(aLocalPath) { + // Any directory selected in the file picker already exists. + // Any directory specified in prefs.js will be created at start if it does + // not exist yet. + // If at the time of entering Account Manager the directory does not exist, + // it must be invalid in the current OS or not creatable due to permissions. + // Even then, the backend sometimes tries to create a new one + // under the current profile. + if (!aLocalPath.exists() || !aLocalPath.isDirectory()) { + return false; + } + + if (Services.appinfo.OS == "WINNT") { + // Do not allow some special filenames on Windows. + // Taken from mozilla/widget/windows/nsDataObj.cpp::MangleTextToValidFilename() + let dirLeafName = aLocalPath.leafName; + const kForbiddenNames = [ + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + "CON", + "PRN", + "AUX", + "NUL", + "CLOCK$", + ]; + if (kForbiddenNames.includes(dirLeafName)) { + return false; + } + } + + // The directory must be readable and writable to work as a mail store. + if (!(aLocalPath.isReadable() && aLocalPath.isWritable())) { + return false; + } + + return true; +} + +/** + * Even if the local path is usable, there are some special folders we do not + * want to allow for message storage as they cause problems (see e.g. bug 750781). + * + * aLocalPath The nsIFile of a directory to check. + */ +function checkDirectoryIsAllowed(aLocalPath) { + /** + * Check if the local path (aLocalPath) is 'safe' i.e. NOT a parent + * or subdirectory of the given special system/app directory (aDirToCheck). + * + * @param {object} aDirToCheck - An object describing the special directory. + * @param {string} aDirToCheck.dirsvc - A path keyword to retrieve from the + * Directory service. + * @param {string} aDirToCheck.dir - An absolute filesystem path. + * @param {string} aDirToCheck.OS - A string of comma separated values defining on which. + * Operating systems the folder is unusable: + * - null = all + * - WINNT = Windows + * - Darwin = OS X + * - Linux = Linux + * @param {string} aDirToCheck.safeSubdirs - An array of directory names that + * are allowed to be used under the tested directory. + * @param {nsIFile} aLocalPath - An nsIFile of the directory to check, + * intended for message storage. + */ + function checkLocalDirectoryIsSafe(aDirToCheck, aLocalPath) { + if (aDirToCheck.OS) { + if (!aDirToCheck.OS.split(",").includes(Services.appinfo.OS)) { + return true; + } + } + + let testDir = null; + if ("dirsvc" in aDirToCheck) { + try { + testDir = Services.dirsvc.get(aDirToCheck.dirsvc, Ci.nsIFile); + } catch (e) { + console.error( + "The special folder " + + aDirToCheck.dirsvc + + " cannot be retrieved on this platform: " + + e + ); + } + + if (!testDir) { + return true; + } + } else if ("dir" in aDirToCheck) { + testDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + testDir.initWithPath(aDirToCheck.dir); + if (!testDir.exists()) { + return true; + } + } else { + console.error("No directory to check?"); + return true; + } + + testDir.normalize(); + + if (testDir.equals(aLocalPath) || aLocalPath.contains(testDir)) { + return false; + } + + if (testDir.contains(aLocalPath)) { + if (!("safeSubdirs" in aDirToCheck)) { + return false; + } + + // While the tested directory may not be safe, + // a subdirectory of some safe subdirectories may be fine. + let isInSubdir = false; + for (let subDir of aDirToCheck.safeSubdirs) { + let checkDir = testDir.clone(); + checkDir.append(subDir); + if (checkDir.contains(aLocalPath)) { + isInSubdir = true; + break; + } + } + return isInSubdir; + } + + return true; + } // end of checkDirectoryIsNotSpecial + + // If the server type has a nsIMsgProtocolInfo.defaultLocalPath set, + // allow that directory. + if (currentAccount.incomingServer) { + try { + let defaultPath = + currentAccount.incomingServer.protocolInfo.defaultLocalPath; + if (defaultPath) { + defaultPath.normalize(); + if (defaultPath.contains(aLocalPath)) { + return true; + } + } + } catch (e) { + /* No problem if this fails. */ + } + } + + for (let tryDir of gDangerousLocalStorageDirs) { + if (!checkLocalDirectoryIsSafe(tryDir, aLocalPath)) { + return false; + } + } + + return true; +} + +/** + * Check if the specified directory does meet all the requirements + * for safe mail storage. + * + * aLocalPath the nsIFile of a directory to check. + */ +function checkDirectoryIsUsable(aLocalPath) { + const kAlertTitle = document + .getElementById("bundle_prefs") + .getString("prefPanel-server"); + const originalPath = aLocalPath; + + let invalidPath = false; + try { + aLocalPath.normalize(); + } catch (e) { + invalidPath = true; + } + + if (invalidPath || !checkDirectoryIsValid(aLocalPath)) { + let alertString = document + .getElementById("bundle_prefs") + .getFormattedString("localDirectoryInvalid", [originalPath.path]); + Services.prompt.alert(window, kAlertTitle, alertString); + return false; + } + + if (!checkDirectoryIsAllowed(aLocalPath)) { + let alertNotAllowed = document + .getElementById("bundle_prefs") + .getFormattedString("localDirectoryNotAllowed", [originalPath.path]); + Services.prompt.alert(window, kAlertTitle, alertNotAllowed); + return false; + } + + // Check that no other account has this same or dependent local directory. + for (let server of MailServices.accounts.allServers) { + if (server.key == currentAccount.incomingServer.key) { + continue; + } + + let serverPath = server.localPath; + try { + serverPath.normalize(); + let alertStringID = null; + if (serverPath.equals(aLocalPath)) { + alertStringID = "directoryAlreadyUsedByOtherAccount"; + } else if (serverPath.contains(aLocalPath)) { + alertStringID = "directoryParentUsedByOtherAccount"; + } else if (aLocalPath.contains(serverPath)) { + alertStringID = "directoryChildUsedByOtherAccount"; + } + + if (alertStringID) { + let alertString = document + .getElementById("bundle_prefs") + .getFormattedString(alertStringID, [server.prettyName]); + + Services.prompt.alert(window, kAlertTitle, alertString); + return false; + } + } catch (e) { + // The other account's path is seriously broken, so we can't compare it. + console.error( + "The Local Directory path of the account " + + server.prettyName + + " seems invalid." + ); + } + } + + return true; +} + +/** + * Check if the user and/or host names have been changed and if so check + * if the new names already exists for an account or are empty. + * Also check if the Local Directory path was changed. + * + * @param {boolean} showAlert - Show and alert if a problem with the host / user + * name is found. + */ +function checkUserServerChanges(showAlert) { + const prefBundle = document.getElementById("bundle_prefs"); + const alertTitle = prefBundle.getString("prefPanel-server"); + var alertText = null; + + var accountValues = getValueArrayFor(currentAccount); + if (!accountValues) { + return true; + } + + let currentServer = currentAccount ? currentAccount.incomingServer : null; + + // If this type doesn't exist (just removed) then return. + if (!("server" in accountValues) || !accountValues.server) { + return true; + } + + // Get the new username, hostname and type from the page. + var typeElem = getPageFormElement("server.type"); + var hostElem = getPageFormElement("server.hostName"); + var userElem = getPageFormElement("server.username"); + if (typeElem && userElem && hostElem) { + var newType = getFormElementValue(typeElem); + var oldHost = getAccountValue( + currentAccount, + accountValues, + "server", + "hostName", + null, + false + ); + var newHost = getFormElementValue(hostElem); + var oldUser = getAccountValue( + currentAccount, + accountValues, + "server", + "username", + null, + false + ); + + var newUser = getFormElementValue(userElem); + var checkUser = true; + // There is no username needed for e.g. news so reset it. + if (currentServer && !currentServer.protocolInfo.requiresUsername) { + oldUser = newUser = ""; + checkUser = false; + } + alertText = null; + // If something is changed then check if the new user/host already exists. + if (oldUser != newUser || oldHost != newHost) { + newUser = newUser.trim(); + newHost = cleanUpHostName(newHost); + if (checkUser && newUser == "") { + alertText = prefBundle.getString("userNameEmpty"); + } else if (!isLegalHostNameOrIP(newHost)) { + alertText = prefBundle.getString("enterValidServerName"); + } else { + let sameServer = MailServices.accounts.findServer( + newUser, + newHost, + newType + ); + if (sameServer && sameServer != currentServer) { + alertText = prefBundle.getString("modifiedAccountExists"); + } else { + // New hostname passed all checks. We may have cleaned it up so set + // the new value back into the input element. + setFormElementValue(hostElem, newHost); + } + } + + if (alertText) { + if (showAlert) { + Services.prompt.alert(window, alertTitle, alertText); + } + // Restore the old values before return + if (checkUser) { + setFormElementValue(userElem, oldUser); + } + setFormElementValue(hostElem, oldHost); + // If no message is shown to the user, silently revert the values + // and consider the check a success. + return !showAlert; + } + + // If username is changed remind users to change Your Name and Email Address. + // If server name is changed and has defined filters then remind users + // to edit rules. + if (showAlert) { + let filterList; + if (currentServer && checkUser) { + filterList = currentServer.getEditableFilterList(null); + } + let changeText = ""; + if ( + oldHost != newHost && + filterList != undefined && + filterList.filterCount + ) { + changeText = prefBundle.getString("serverNameChanged"); + } + // In the event that oldHost == newHost or oldUser == newUser, + // the \n\n will be trimmed off before the message is shown. + if (oldUser != newUser) { + changeText = + changeText + "\n\n" + prefBundle.getString("userNameChanged"); + } + + if (changeText != "") { + Services.prompt.alert(window, alertTitle, changeText.trim()); + } + } + + let l10n = new Localization(["messenger/accountManager.ftl"], true); + let cancel = Services.prompt.confirmEx( + window, + alertTitle, + l10n.formatValueSync("server-change-restart-required"), + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL, + prefBundle.getString("localDirectoryRestart"), + null, + null, + null, + {} + ); + if (cancel) { + setFormElementValue(hostElem, oldHost); + setFormElementValue(userElem, oldUser); + return false; + } + gRestartNeeded = true; + } + } + + // Check the new value of the server.localPath field for validity. + var pathElem = getPageFormElement("server.localPath"); + if (!pathElem) { + return true; + } + let dir = getFormElementValue(pathElem); + if (!checkDirectoryIsUsable(dir)) { + // return false; // Temporarily disable this. Just show warning but do not block. See bug 921371. + console.error( + `Local directory ${dir.path} of account ${currentAccount.key} is not safe to use. Consider changing it.` + ); + } + + // Warn if the Local directory path was changed. + // This can be removed once bug 2654 is fixed. + let oldLocalDir = getAccountValue( + currentAccount, + accountValues, + "server", + "localPath", + null, + false + ); // both return nsIFile + let newLocalDir = getFormElementValue(pathElem); + if (oldLocalDir && newLocalDir && oldLocalDir.path != newLocalDir.path) { + let brandName = document + .getElementById("bundle_brand") + .getString("brandShortName"); + alertText = prefBundle.getFormattedString("localDirectoryChanged", [ + brandName, + ]); + + let cancel = Services.prompt.confirmEx( + window, + alertTitle, + alertText, + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL, + prefBundle.getString("localDirectoryRestart"), + null, + null, + null, + {} + ); + if (cancel) { + setFormElementValue(pathElem, oldLocalDir); + return false; + } + gRestartNeeded = true; + } + + return true; +} + +/** + * If account name is not valid, alert the user. + */ +function checkAccountNameIsValid() { + if (!currentAccount) { + return true; + } + + const prefBundle = document.getElementById("bundle_prefs"); + let alertText = null; + + let serverNameElem = getPageFormElement("server.prettyName"); + if (serverNameElem) { + let accountName = getFormElementValue(serverNameElem); + + if (!accountName) { + alertText = prefBundle.getString("accountNameEmpty"); + } else if (accountNameExists(accountName, currentAccount.key)) { + alertText = prefBundle.getString("accountNameExists"); + // Change the account name to prevent UI freeze. + let counter = 2; + while ( + accountNameExists(`${accountName}_${counter}`, currentAccount.key) + ) { + counter++; + } + serverNameElem.value = `${accountName}_${counter}`; + } + + if (alertText) { + const alertTitle = prefBundle.getString("accountWizard"); + Services.prompt.alert(window, alertTitle, alertText); + return false; + } + } + + return true; +} + +function onSave() { + if (pendingPageId) { + dump("ERROR: " + pendingPageId + " hasn't loaded yet! Not saving.\n"); + return false; + } + + // make sure the current visible page is saved + savePage(currentAccount); + + for (var accountid in accountArray) { + var accountValues = accountArray[accountid]; + var account = accountArray[accountid]._account; + if (!saveAccount(accountValues, account)) { + return false; + } + } + + return true; +} + +/** + * Highlight the default account row in the account tree, + * optionally un-highlight the previous one. + * + * @param {?nsIMsgAccount} newDefault - The account that has become the new + * default. Can be given as null if there is none. + * @param {?nsIMsgAccount} oldDefault - The account that has stopped being the + * default. Can be given as null if there was none. + */ +function markDefaultServer(newDefault, oldDefault) { + if (oldDefault == newDefault) { + return; + } + + let accountTree = document.getElementById("accounttree"); + for (let accountRow of accountTree.children) { + if (newDefault && newDefault == accountRow._account) { + accountRow.classList.add("isDefaultServer"); + } + if (oldDefault && oldDefault == accountRow._account) { + accountRow.classList.remove("isDefaultServer"); + } + } +} + +/** + * Notify the UI to rebuild the account tree. + */ +function rebuildAccountTree() { + // TODO: Reimplement or replace. +} + +/** + * Make currentAccount (currently selected in the account tree) the default one. + */ +function onSetDefault(event) { + // Make sure this function was not called while the control item is disabled + if (event.target.getAttribute("disabled") == "true") { + return; + } + + let previousDefault = MailServices.accounts.defaultAccount; + MailServices.accounts.defaultAccount = currentAccount; + markDefaultServer(currentAccount, previousDefault); + + // Update gloda's myContact with the new default account's default identity. + Gloda._initMyIdentities(); + + gAccountTree.load(); +} + +function onRemoveAccount(event) { + if (event.target.getAttribute("disabled") == "true" || !currentAccount) { + return; + } + + let server = currentAccount.incomingServer; + + let canDelete = server.protocolInfo.canDelete || server.canDelete; + if (!canDelete) { + return; + } + + let serverList = []; + let accountTree = document.getElementById("accounttree"); + // build the list of servers in the account tree (order is important) + for (let row of accountTree.children) { + if ("_account" in row) { + let curServer = row._account.incomingServer; + if (!serverList.includes(curServer)) { + serverList.push(curServer); + } + } + } + + // get position of the current server in the server list + let serverIndex = serverList.indexOf(server); + + // After the current server is deleted, choose the next server/account, + // or the previous one if the last one was deleted. + if (serverIndex == serverList.length - 1) { + serverIndex--; + } else { + serverIndex++; + } + + // Need to save these before the account and its server is removed. + let serverId = server.serverURI; + + // Confirm account deletion. + let removeArgs = { + server, + account: currentAccount, + result: false, + }; + + let onCloseDialog = function () { + // If result is true, the account was removed. + if (!removeArgs.result) { + return; + } + + // clear cached data out of the account array + currentAccount = currentPageId = null; + if (serverId in accountArray) { + delete accountArray[serverId]; + } + + if (serverIndex >= 0 && serverIndex < serverList.length) { + selectServer(serverList[serverIndex], null); + } + + // Either the default account was deleted so there is a new one + // or the default account was not changed. Either way, there is + // no need to unmark the old one. + markDefaultServer(MailServices.accounts.defaultAccount, null); + }; + + gSubDialog.open( + "chrome://messenger/content/removeAccount.xhtml", + { + features: "resizable=no", + closingCallback: onCloseDialog, + }, + removeArgs + ); +} + +function saveAccount(accountValues, account) { + var identity = null; + var server = null; + + if (account) { + identity = account.defaultIdentity; + server = account.incomingServer; + } + + for (var type in accountValues) { + var dest; + try { + if (type == "identity") { + dest = identity; + } else if (type == "server") { + dest = server; + } else if (type == "pop3") { + dest = server.QueryInterface(Ci.nsIPop3IncomingServer); + } else if (type == "imap") { + dest = server.QueryInterface(Ci.nsIImapIncomingServer); + } else if (type == "none") { + dest = server.QueryInterface(Ci.nsINoIncomingServer); + } else if (type == "nntp") { + dest = server.QueryInterface(Ci.nsINntpIncomingServer); + } else if (type == "smtp") { + dest = MailServices.smtp.defaultServer; + } + } catch (ex) { + // don't do anything, just means we don't support that + } + if (dest == undefined) { + continue; + } + var typeArray = accountValues[type]; + + for (var slot in typeArray) { + if ( + type in gGenericAttributeTypes && + slot in gGenericAttributeTypes[type] + ) { + var methodName = "get"; + switch (gGenericAttributeTypes[type][slot]) { + case "int": + methodName += "Int"; + break; + case "wstring": + methodName += "Unichar"; + break; + case "string": + methodName += "Char"; + break; + case "bool": + // in some cases + // like for radiogroups of type boolean + // the value will be "false" instead of false + // we need to convert it. + if (typeArray[slot] == "false") { + typeArray[slot] = false; + } else if (typeArray[slot] == "true") { + typeArray[slot] = true; + } + + methodName += "Bool"; + break; + default: + dump( + "unexpected preftype: " + + gGenericAttributeTypes[type][slot] + + "\n" + ); + break; + } + methodName += methodName + "Value" in dest ? "Value" : "Attribute"; + if (dest[methodName](slot) != typeArray[slot]) { + methodName = methodName.replace("get", "set"); + dest[methodName](slot, typeArray[slot]); + } + } else if ( + slot in dest && + typeArray[slot] != undefined && + dest[slot] != typeArray[slot] + ) { + try { + dest[slot] = typeArray[slot]; + } catch (ex) { + // hrm... need to handle special types here + } + } + } + } + + // if we made account changes to the spam settings, we'll need to re-initialize + // our settings object + if (server && server.spamSettings) { + try { + server.spamSettings.initialize(server); + } catch (e) { + let accountName = getAccountValue( + account, + getValueArrayFor(account), + "server", + "prettyName", + null, + false + ); + let alertText = document + .getElementById("bundle_prefs") + .getFormattedString("junkSettingsBroken", [accountName]); + let review = Services.prompt.confirmEx( + window, + null, + alertText, + Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_YES + + Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_NO, + null, + null, + null, + null, + {} + ); + if (!review) { + onAccountTreeSelect("am-junk.xhtml", account); + return false; + } + } + } + + return true; +} + +/** + * Set enabled/disabled state for the actions in the Account Actions menu. + * Called only by Thunderbird. + */ +function initAccountActionsButtons(menupopup) { + if (!Services.prefs.getBoolPref("mail.chat.enabled")) { + document.getElementById("accountActionsAddIMAccount").hidden = true; + } + + updateItems( + document.getElementById("accounttree"), + getCurrentAccount(), + document.getElementById("accountActionsAddMailAccount"), + document.getElementById("accountActionsDropdownSetDefault"), + document.getElementById("accountActionsDropdownRemove") + ); + + updateBlockedItems(menupopup.children, true); +} + +/** + * Determine enabled/disabled state for the passed in elements + * representing account actions. + */ +function updateItems( + tree, + account, + addAccountItem, + setDefaultItem, + removeItem +) { + // Start with items disabled and then find out what can be enabled. + let canSetDefault = false; + let canDelete = false; + + if (account && tree.selectedIndex >= 0) { + // Only try to check properties if there was anything selected in the tree + // and it belongs to an account. + // Otherwise we have either selected a SMTP server, or there is some + // problem. Either way, we don't want the user to act on it. + let server = account.incomingServer; + + if ( + account != MailServices.accounts.defaultAccount && + server.canBeDefaultServer && + account.identities.length > 0 + ) { + canSetDefault = true; + } + + canDelete = server.protocolInfo.canDelete || server.canDelete; + } + + setEnabled(addAccountItem, true); + setEnabled(setDefaultItem, canSetDefault); + setEnabled(removeItem, canDelete); +} + +/** + * Disable buttons/menu items if their control preference is locked. + * + * @param {Node[]|NodeList} aItems - Elements to be checked. + * @param {boolean} aMustBeTrue - If true then the pref must be boolean and set + * to true to trigger the disabling. + */ +function updateBlockedItems(aItems, aMustBeTrue) { + for (let item of aItems) { + let prefstring = item.getAttribute("prefstring"); + if (!prefstring) { + continue; + } + + if ( + Services.prefs.prefIsLocked(prefstring) && + (!aMustBeTrue || Services.prefs.getBoolPref(prefstring)) + ) { + item.setAttribute("disabled", true); + } + } +} + +/** + * Set enabled/disabled state for the control. + */ +function setEnabled(control, enabled) { + if (!control) { + return; + } + + if (enabled) { + control.removeAttribute("disabled"); + } else { + control.setAttribute("disabled", true); + } +} + +// Called when someone clicks on an account. Figure out context by what they +// clicked on. This is also called when an account is removed. In this case, +// nothing is selected. +function onAccountTreeSelect(pageId, account) { + let tree = document.getElementById("accounttree"); + + let changeView = pageId && account; + if (!changeView) { + if (tree.selectedIndex < 0) { + return false; + } + + let node = tree.rows[tree.selectedIndex]; + account = "_account" in node ? node._account : null; + + pageId = node.getAttribute("PageTag"); + } + + if (pageId == currentPageId && account == currentAccount) { + return true; + } + + if ( + document + .getElementById("contentFrame") + .contentDocument.getElementById("server.localPath") + ) { + // Check if user/host names have been changed or the Local Directory is invalid. + if (!checkUserServerChanges(false)) { + changeView = true; + account = currentAccount; + pageId = currentPageId; + } + + if (gRestartNeeded) { + onAccept(false); + } + } + + if ( + document + .getElementById("contentFrame") + .contentDocument.getElementById("server.prettyName") + ) { + // Check if account name is valid. + if (!checkAccountNameIsValid()) { + changeView = true; + account = currentAccount; + pageId = currentPageId; + } + } + + if (currentPageId) { + // Change focus to the account tree first so that any 'onchange' handlers + // on elements in the current page have a chance to run before the page + // is saved and replaced by the new one. + tree.focus(); + } + + // Provide opportunity to do cleanups or checks when the current page is being left. + if ("onLeave" in top.frames.contentFrame) { + top.frames.contentFrame.onLeave(); + } + + // save the previous page + savePage(currentAccount); + + let changeAccount = account != currentAccount; + + if (changeView) { + selectServer(account.incomingServer, pageId); + } + + if (pageId != currentPageId) { + // loading a complete different page + + // prevent overwriting with bad stuff + currentAccount = currentPageId = null; + + pendingAccount = account; + pendingPageId = pageId; + loadPage(pageId); + } else if (changeAccount) { + // same page, different server + restorePage(pageId, account); + } + + return true; +} + +// page has loaded +function onPanelLoaded(pageId) { + if (pageId != pendingPageId) { + // if we're reloading the current page, we'll assume the + // page has asked itself to be completely reloaded from + // the prefs. to do this, clear out the the old entry in + // the account data, and then restore theh page + if (pageId == currentPageId) { + var serverId = currentAccount + ? currentAccount.incomingServer.serverURI + : "global"; + delete accountArray[serverId]; + restorePage(currentPageId, currentAccount); + } + } else { + restorePage(pendingPageId, pendingAccount); + } + + // probably unnecessary, but useful for debugging + pendingAccount = null; + pendingPageId = null; +} + +function pageURL(pageId) { + // If we have a special non account manager pane (e.g. about:blank), + // do not translate it into ChromePackageName URL. + if (!pageId.startsWith("am-")) { + return pageId; + } + + let chromePackageName; + try { + // we could compare against "main","server","copies","offline","addressing", + // "smtp" and "advanced" first to save the work, but don't, + // as some of these might be turned into extensions (for thunderbird) + let packageName = pageId.split("am-")[1].split(".xhtml")[0]; + chromePackageName = MailServices.accounts.getChromePackageName(packageName); + } catch (ex) { + chromePackageName = "messenger"; + } + return "chrome://" + chromePackageName + "/content/" + pageId; +} + +function loadPage(pageId) { + const loadURIOptions = { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }; + document + .getElementById("contentFrame") + .webNavigation.fixupAndLoadURIString(pageURL(pageId), loadURIOptions); +} + +// save the values of the widgets to the given server +function savePage(account) { + if (!account) { + return; + } + + // tell the page that it's about to save + if ("onSave" in top.frames.contentFrame) { + top.frames.contentFrame.onSave(); + } + + var accountValues = getValueArrayFor(account); + if (!accountValues) { + return; + } + // Reset accountArray so that only the current page will be saved. This is + // needed to prevent resetting prefs unintentionally. An example is when + // changing username/hostname, MsgIncomingServer.jsm will modify identities, + // without this, identities changes may be reverted to old values in + // accountArray. + accountArray = {}; + accountValues = {}; + let serverId = account.incomingServer.serverURI; + accountArray[serverId] = accountValues; + accountArray[serverId]._account = account; + + var pageElements = getPageFormElements(); + if (!pageElements) { + return; + } + + // store the value in the account + for (let i = 0; i < pageElements.length; i++) { + if (pageElements[i].id) { + let vals = pageElements[i].id.split("."); + if (vals.length >= 2) { + let type = vals[0]; + let slot = pageElements[i].id.slice(type.length + 1); + + setAccountValue( + accountValues, + type, + slot, + getFormElementValue(pageElements[i]) + ); + } + } + } +} + +function setAccountValue(accountValues, type, slot, value) { + if (!(type in accountValues)) { + accountValues[type] = {}; + } + + accountValues[type][slot] = value; +} + +function getAccountValue( + account, + accountValues, + type, + slot, + preftype, + isGeneric +) { + if (!(type in accountValues)) { + accountValues[type] = {}; + } + + // fill in the slot from the account if necessary + if ( + !(slot in accountValues[type]) || + accountValues[type][slot] == undefined + ) { + var server; + if (account) { + server = account.incomingServer; + } + var source = null; + try { + if (type == "identity") { + source = account.defaultIdentity; + } else if (type == "server") { + source = account.incomingServer; + } else if (type == "pop3") { + source = server.QueryInterface(Ci.nsIPop3IncomingServer); + } else if (type == "imap") { + source = server.QueryInterface(Ci.nsIImapIncomingServer); + } else if (type == "none") { + source = server.QueryInterface(Ci.nsINoIncomingServer); + } else if (type == "nntp") { + source = server.QueryInterface(Ci.nsINntpIncomingServer); + } else if (type == "smtp") { + source = MailServices.smtp.defaultServer; + } + } catch (ex) {} + + if (source) { + if (isGeneric) { + if (!(type in gGenericAttributeTypes)) { + gGenericAttributeTypes[type] = {}; + } + + // we need the preftype later, for setting when we save. + gGenericAttributeTypes[type][slot] = preftype; + var methodName = "get"; + switch (preftype) { + case "int": + methodName += "Int"; + break; + case "wstring": + methodName += "Unichar"; + break; + case "string": + methodName += "Char"; + break; + case "bool": + methodName += "Bool"; + break; + default: + dump("unexpected preftype: " + preftype + "\n"); + break; + } + methodName += methodName + "Value" in source ? "Value" : "Attribute"; + accountValues[type][slot] = source[methodName](slot); + } else if (slot in source) { + accountValues[type][slot] = source[slot]; + } else { + accountValues[type][slot] = null; + } + } else { + accountValues[type][slot] = null; + } + } + return accountValues[type][slot]; +} + +// restore the values of the widgets from the given server +function restorePage(pageId, account) { + if (!account) { + return; + } + + var accountValues = getValueArrayFor(account); + if (!accountValues) { + return; + } + + if ("onPreInit" in top.frames.contentFrame) { + top.frames.contentFrame.onPreInit(account, accountValues); + } + + var pageElements = getPageFormElements(); + if (!pageElements) { + return; + } + + // restore the value from the account + for (let i = 0; i < pageElements.length; i++) { + if (pageElements[i].id) { + let vals = pageElements[i].id.split("."); + if (vals.length >= 2) { + let type = vals[0]; + let slot = pageElements[i].id.slice(type.length + 1); + + // buttons are lockable, but don't have any data so we skip that part. + // elements that do have data, we get the values at poke them in. + if (pageElements[i].localName != "button") { + var value = getAccountValue( + account, + accountValues, + type, + slot, + pageElements[i].getAttribute("preftype"), + pageElements[i].getAttribute("genericattr") == "true" + ); + setFormElementValue(pageElements[i], value); + } + var element = pageElements[i]; + switch (type) { + case "identity": + element.identitykey = account.defaultIdentity.key; + break; + case "pop3": + case "imap": + case "nntp": + case "server": + element.serverkey = account.incomingServer.key; + break; + case "smtp": + if (MailServices.smtp.defaultServer) { + element.serverkey = MailServices.smtp.defaultServer.key; + } + break; + } + var isLocked = getAccountValueIsLocked(pageElements[i]); + setEnabled(pageElements[i], !isLocked); + } + } + } + + // tell the page that new values have been loaded + if ("onInit" in top.frames.contentFrame) { + top.frames.contentFrame.onInit(pageId, account.incomingServer.serverURI); + } + + // everything has succeeded, vervied by setting currentPageId + currentPageId = pageId; + currentAccount = account; +} + +/** + * Gets the value of a widget in current the account settings page, + * automatically setting the right property of it depending on element type. + * + * @param {HTMLInputElement} formElement - An input element. + */ +function getFormElementValue(formElement) { + try { + var type = formElement.localName; + if (type == "checkbox") { + if (formElement.getAttribute("reversed")) { + return !formElement.checked; + } + return formElement.checked; + } + if (type == "input" && formElement.getAttribute("datatype") == "nsIFile") { + if (formElement.value) { + let localfile = Cc["@mozilla.org/file/local;1"].createInstance( + Ci.nsIFile + ); + + localfile.initWithPath(formElement.value); + return localfile; + } + return null; + } + if (type == "input" || "value" in formElement) { + return formElement.value.trim(); + } + return null; + } catch (ex) { + console.error("getFormElementValue failed, ex=" + ex + "\n"); + } + return null; +} + +/** + * Sets the value of a widget in current the account settings page, + * automatically setting the right property of it depending on element type. + * + * @param {HTMLInputElement} formElement - An input element. + * @param {string|nsIFile} value - The value to store in the element. + */ +function setFormElementValue(formElement, value) { + var type = formElement.localName; + if (type == "checkbox") { + if (value == null) { + formElement.checked = false; + } else if (formElement.getAttribute("reversed")) { + formElement.checked = !value; + } else { + formElement.checked = value; + } + } else if (type == "radiogroup" || type == "menulist") { + if (value == null) { + formElement.selectedIndex = 0; + } else { + formElement.value = value; + } + } else if ( + type == "input" && + formElement.getAttribute("datatype") == "nsIFile" + ) { + // handle nsIFile + if (value) { + let localfile = value.QueryInterface(Ci.nsIFile); + try { + formElement.value = localfile.path; + } catch (ex) { + dump("Still need to fix uninitialized nsIFile problem!\n"); + } + } else { + formElement.value = ""; + } + } else if (type == "input") { + if (value == null) { + formElement.value = null; + } else { + formElement.value = value; + } + } else if (type == "label") { + formElement.value = value || ""; + } else if (value == null) { + // let the form figure out what to do with it + formElement.value = null; + } else { + formElement.value = value; + } +} + +// +// conversion routines - get data associated +// with a given pageId, serverId, etc +// + +// helper routine for account manager panels to get the current account for the selected server +function getCurrentAccount() { + return currentAccount; +} + +/** + * Get the array of persisted form elements for the given page. + */ +function getPageFormElements() { + // Uses getElementsByAttribute() which returns a live NodeList which is usually + // faster than e.g. querySelector(). + if ("getElementsByAttribute" in top.frames.contentFrame.document) { + return top.frames.contentFrame.document.getElementsByAttribute( + "wsm_persist", + "true" + ); + } + + return null; +} + +/** + * Get a single persisted form element in the current page. + * + * @param {srtring} aId - ID of the element requested. + */ +function getPageFormElement(aId) { + let elem = top.frames.contentFrame.document.getElementById(aId); + if (elem && elem.getAttribute("wsm_persist") == "true") { + return elem; + } + + return null; +} + +// get the value array for the given account +function getValueArrayFor(account) { + var serverId = account ? account.incomingServer.serverURI : "global"; + + if (!(serverId in accountArray)) { + accountArray[serverId] = {}; + accountArray[serverId]._account = account; + } + + return accountArray[serverId]; +} + +/** + * Sets the name of the account rowitem in the tree pane. + * + * @param {string} aAccountKey - The key of the account to change. + * @param {string} aLabel - The value of the label to set. + */ +function setAccountLabel(aAccountKey, aLabel) { + let row = document.getElementById(aAccountKey); + if (row) { + row.setAttribute("aria-label", aLabel); + row.title = aLabel; + row.querySelector(".name").textContent = aLabel; + } + rebuildAccountTree(false); +} + +var gAccountTree = { + load() { + this._build(); + + let mainTree = document.getElementById("accounttree"); + mainTree.__defineGetter__("_orderableChildren", function () { + let rows = [...this.children]; + rows.pop(); + return rows; + }); + mainTree.addEventListener("ordering", event => { + if (!event.detail || event.detail.id == "smtp") { + event.preventDefault(); + } + }); + mainTree.addEventListener("ordered", event => { + let accountKeyList = Array.from(mainTree.children, row => row.id); + accountKeyList.pop(); // Remove SMTP. + MailServices.accounts.reorderAccounts(accountKeyList); + rebuildAccountTree(); + }); + mainTree.addEventListener("expanded", event => { + this._dataStore.setValue( + document.documentURI, + event.target.id, + "open", + "true" + ); + }); + mainTree.addEventListener("collapsed", event => { + this._dataStore.setValue( + document.documentURI, + event.target.id, + "open", + "false" + ); + }); + + MailServices.accounts.addIncomingServerListener(this); + }, + unload() { + MailServices.accounts.removeIncomingServerListener(this); + }, + onServerLoaded(server) { + // We assume the newly appeared server was created by the user so we select + // it in the tree. + this._build(server); + }, + onServerUnloaded(aServer) { + this._build(); + }, + onServerChanged(aServer) {}, + + _dataStore: Services.xulStore, + + /** + * Retrieve from XULStore.json whether the account should be expanded (open) + * in the account tree. + * + * @param {string} aAccountKey - Key of the account to check. + */ + _getAccountOpenState(aAccountKey) { + if (!this._dataStore.hasValue(document.documentURI, aAccountKey, "open")) { + // If there was no value stored, use opened state. + return "true"; + } + // Retrieve the persisted value from XULStore.json. + // It is stored under the URI of the current document and ID of the XUL element. + return this._dataStore.getValue(document.documentURI, aAccountKey, "open"); + }, + + _build(newServer) { + var bundle = document.getElementById("bundle_prefs"); + function getString(aString) { + return bundle.getString(aString); + } + var panels = [ + { string: getString("prefPanel-server"), src: "am-server.xhtml" }, + { string: getString("prefPanel-copies"), src: "am-copies.xhtml" }, + { + string: getString("prefPanel-synchronization"), + src: "am-offline.xhtml", + }, + { string: getString("prefPanel-diskspace"), src: "am-offline.xhtml" }, + { string: getString("prefPanel-addressing"), src: "am-addressing.xhtml" }, + { string: getString("prefPanel-junk"), src: "am-junk.xhtml" }, + ]; + + let accounts = FolderUtils.allAccountsSorted(false); + + let mainTree = document.getElementById("accounttree"); + // Clear off all children... + while (mainTree.hasChildNodes()) { + mainTree.lastChild.remove(); + } + + for (let account of accounts) { + let accountName = null; + let accountKey = account.key; + let amChrome = "about:blank"; + let panelsToKeep = []; + let server = null; + + // This "try {} catch {}" block is intentionally very long to catch + // unknown exceptions and confine them to this single account. + // This may happen from broken accounts. See e.g. bug 813929. + // Other accounts can still be shown properly if they are valid. + try { + server = account.incomingServer; + + if ( + server.type == "im" && + !Services.prefs.getBoolPref("mail.chat.enabled") + ) { + continue; + } + + accountName = server.prettyName; + + // Now add our panels. + let idents = MailServices.accounts.getIdentitiesForServer(server); + if (idents.length) { + panelsToKeep.push(panels[0]); // The server panel is valid + panelsToKeep.push(panels[1]); // also the copies panel + panelsToKeep.push(panels[4]); // and addressing + } + + // Everyone except News, RSS and IM has a junk panel + // XXX: unextensible! + // The existence of server.spamSettings can't currently be used for this. + if ( + server.type != "nntp" && + server.type != "rss" && + server.type != "im" + ) { + panelsToKeep.push(panels[5]); + } + + // Check offline/diskspace support level. + let diskspace = server.supportsDiskSpace; + if (server.offlineSupportLevel >= 10 && diskspace) { + panelsToKeep.push(panels[2]); + } else if (diskspace) { + panelsToKeep.push(panels[3]); + } + + // extensions + const CATEGORY = "mailnews-accountmanager-extensions"; + for (let { data } of Services.catMan.enumerateCategory(CATEGORY)) { + try { + let svc = Cc[ + Services.catMan.getCategoryEntry(CATEGORY, data) + ].getService(Ci.nsIMsgAccountManagerExtension); + if (svc.showPanel(server)) { + let bundleName = + "chrome://" + + svc.chromePackageName + + "/locale/am-" + + svc.name + + ".properties"; + let bundle = Services.strings.createBundle(bundleName); + let title = bundle.GetStringFromName("prefPanel-" + svc.name); + panelsToKeep.push({ + string: title, + src: "am-" + svc.name + ".xhtml", + }); + } + } catch (e) { + // Fetching of this extension panel failed so do not show it, + // just log error. + let extName = data || "(unknown)"; + console.error( + "Error accessing panel from extension '" + extName + "': " + e + ); + } + } + amChrome = server.accountManagerChrome; + } catch (e) { + // Show only a placeholder in the account list saying this account + // is broken, with no child panels. + let accountID = accountName || accountKey; + console.error("Error accessing account " + accountID + ": " + e); + accountName = "Invalid account " + accountID; + panelsToKeep.length = 0; + } + + // Create the top level tree-item. + let treeitem = document + .getElementById("accountTreeItem") + .content.firstElementChild.cloneNode(true); + mainTree.appendChild(treeitem); + treeitem.setAttribute("aria-label", accountName); + treeitem.title = accountName; + treeitem.querySelector(".name").textContent = accountName; + treeitem.setAttribute("PageTag", amChrome); + // Add icons based on account type. + if (server) { + treeitem.classList.add("serverType-" + server.type); + if (server.isSecure) { + treeitem.classList.add("isSecure"); + } + // For IM accounts, we can try to fetch a protocol specific icon. + if (server.type == "im") { + treeitem.querySelector(".icon").style.backgroundImage = + "url(" + + ChatIcons.getProtocolIconURI( + server.wrappedJSObject.imAccount.protocol + ) + + ")"; + treeitem.id = accountKey; + } + } + + if (panelsToKeep.length > 0) { + let treekids = treeitem.querySelector("ul"); + for (let panel of panelsToKeep) { + let kidtreeitem = document.createElement("li"); + kidtreeitem.title = panel.string; + treekids.appendChild(kidtreeitem); + let kidtreerow = document.createElement("div"); + kidtreeitem.appendChild(kidtreerow); + let kidtreecell = document.createElement("span"); + kidtreecell.classList.add("name"); + kidtreecell.tabIndex = -1; + kidtreerow.appendChild(kidtreecell); + kidtreecell.textContent = panel.string; + kidtreeitem.setAttribute("PageTag", panel.src); + kidtreeitem._account = account; + kidtreeitem.id = `${accountKey}/${panel.src}`; + } + treeitem.id = accountKey; + // Load the 'open' state of the account from XULStore.json. + if (this._getAccountOpenState(accountKey) != "true") { + treeitem.classList.add("collapsed"); + } + } + treeitem._account = account; + } + + markDefaultServer(MailServices.accounts.defaultAccount, null); + + // Now add the outgoing server node. + let treeitem = document + .getElementById("accountTreeItem") + .content.firstElementChild.cloneNode(true); + mainTree.appendChild(treeitem); + treeitem.id = "smtp"; + treeitem.querySelector(".name").textContent = getString("prefPanel-smtp"); + treeitem.setAttribute("PageTag", "am-smtp.xhtml"); + treeitem.classList.add("serverType-smtp"); + + // If a new server was created, select the server after rebuild of the tree. + if (newServer) { + setTimeout(selectServer, 0, newServer); + } + }, +}; diff --git a/comm/mailnews/base/prefs/content/AccountManager.xhtml b/comm/mailnews/base/prefs/content/AccountManager.xhtml new file mode 100644 index 0000000000..7769ff730b --- /dev/null +++ b/comm/mailnews/base/prefs/content/AccountManager.xhtml @@ -0,0 +1,171 @@ + + + + + %accountManagerDTD; + + %utilityDTD; +]> + + + + &accountManagerTitle.label; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +