summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/base
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/base')
-rw-r--r--comm/mailnews/base/.eslintrc.js5
-rw-r--r--comm/mailnews/base/content/.eslintrc.js5
-rw-r--r--comm/mailnews/base/content/dateFormat.js227
-rw-r--r--comm/mailnews/base/content/folder-menupopup.js1238
-rw-r--r--comm/mailnews/base/content/folderProps.js480
-rw-r--r--comm/mailnews/base/content/folderProps.xhtml338
-rw-r--r--comm/mailnews/base/content/jsTreeView.js239
-rw-r--r--comm/mailnews/base/content/junkCommands.js449
-rw-r--r--comm/mailnews/base/content/junkLog.js48
-rw-r--r--comm/mailnews/base/content/junkLog.xhtml57
-rw-r--r--comm/mailnews/base/content/markByDate.js120
-rw-r--r--comm/mailnews/base/content/markByDate.xhtml79
-rw-r--r--comm/mailnews/base/content/menulist-charsetpicker.js86
-rw-r--r--comm/mailnews/base/content/msgAccountCentral.js238
-rw-r--r--comm/mailnews/base/content/msgAccountCentral.xhtml309
-rw-r--r--comm/mailnews/base/content/msgSelectOfflineFolders.js189
-rw-r--r--comm/mailnews/base/content/msgSelectOfflineFolders.xhtml89
-rw-r--r--comm/mailnews/base/content/msgSynchronize.js192
-rw-r--r--comm/mailnews/base/content/msgSynchronize.xhtml76
-rw-r--r--comm/mailnews/base/content/newFolderDialog.js82
-rw-r--r--comm/mailnews/base/content/newFolderDialog.xhtml107
-rw-r--r--comm/mailnews/base/content/newmailalert.js109
-rw-r--r--comm/mailnews/base/content/newmailalert.xhtml55
-rw-r--r--comm/mailnews/base/content/newsError.js48
-rw-r--r--comm/mailnews/base/content/newsError.xhtml57
-rw-r--r--comm/mailnews/base/content/renameFolderDialog.js43
-rw-r--r--comm/mailnews/base/content/renameFolderDialog.xhtml63
-rw-r--r--comm/mailnews/base/content/retention.js52
-rw-r--r--comm/mailnews/base/content/shutdownWindow.js97
-rw-r--r--comm/mailnews/base/content/shutdownWindow.xhtml53
-rw-r--r--comm/mailnews/base/content/subscribe.js496
-rw-r--r--comm/mailnews/base/content/subscribe.xhtml235
-rw-r--r--comm/mailnews/base/content/virtualFolderListEdit.js206
-rw-r--r--comm/mailnews/base/content/virtualFolderListEdit.xhtml84
-rw-r--r--comm/mailnews/base/content/virtualFolderProperties.js383
-rw-r--r--comm/mailnews/base/content/virtualFolderProperties.xhtml110
-rw-r--r--comm/mailnews/base/moz.build11
-rw-r--r--comm/mailnews/base/prefs/.eslintrc.js5
-rw-r--r--comm/mailnews/base/prefs/content/AccountManager.js1949
-rw-r--r--comm/mailnews/base/prefs/content/AccountManager.xhtml171
-rw-r--r--comm/mailnews/base/prefs/content/AccountWizard.js605
-rw-r--r--comm/mailnews/base/prefs/content/AccountWizard.xhtml161
-rw-r--r--comm/mailnews/base/prefs/content/SmtpServerEdit.js241
-rw-r--r--comm/mailnews/base/prefs/content/SmtpServerEdit.xhtml208
-rw-r--r--comm/mailnews/base/prefs/content/accountUtils.js369
-rw-r--r--comm/mailnews/base/prefs/content/am-addressing.inc.xhtml125
-rw-r--r--comm/mailnews/base/prefs/content/am-addressing.js68
-rw-r--r--comm/mailnews/base/prefs/content/am-addressing.xhtml32
-rw-r--r--comm/mailnews/base/prefs/content/am-archiveoptions.js74
-rw-r--r--comm/mailnews/base/prefs/content/am-archiveoptions.xhtml136
-rw-r--r--comm/mailnews/base/prefs/content/am-copies.inc.xhtml308
-rw-r--r--comm/mailnews/base/prefs/content/am-copies.js555
-rw-r--r--comm/mailnews/base/prefs/content/am-copies.xhtml35
-rw-r--r--comm/mailnews/base/prefs/content/am-identities-list.js206
-rw-r--r--comm/mailnews/base/prefs/content/am-identities-list.xhtml101
-rw-r--r--comm/mailnews/base/prefs/content/am-identity-edit.js577
-rw-r--r--comm/mailnews/base/prefs/content/am-identity-edit.xhtml239
-rw-r--r--comm/mailnews/base/prefs/content/am-junk.js335
-rw-r--r--comm/mailnews/base/prefs/content/am-junk.xhtml293
-rw-r--r--comm/mailnews/base/prefs/content/am-main.js110
-rw-r--r--comm/mailnews/base/prefs/content/am-main.xhtml344
-rw-r--r--comm/mailnews/base/prefs/content/am-offline.js435
-rw-r--r--comm/mailnews/base/prefs/content/am-offline.xhtml350
-rw-r--r--comm/mailnews/base/prefs/content/am-prefs.js142
-rw-r--r--comm/mailnews/base/prefs/content/am-server-advanced.js151
-rw-r--r--comm/mailnews/base/prefs/content/am-server-advanced.xhtml222
-rw-r--r--comm/mailnews/base/prefs/content/am-server.js615
-rw-r--r--comm/mailnews/base/prefs/content/am-server.xhtml674
-rw-r--r--comm/mailnews/base/prefs/content/am-serverwithnoidentities.js94
-rw-r--r--comm/mailnews/base/prefs/content/am-serverwithnoidentities.xhtml152
-rw-r--r--comm/mailnews/base/prefs/content/am-smtp.js277
-rw-r--r--comm/mailnews/base/prefs/content/am-smtp.xhtml121
-rw-r--r--comm/mailnews/base/prefs/content/amUtils.js275
-rw-r--r--comm/mailnews/base/prefs/content/aw-accname.js28
-rw-r--r--comm/mailnews/base/prefs/content/aw-done.js39
-rw-r--r--comm/mailnews/base/prefs/content/aw-identity.js77
-rw-r--r--comm/mailnews/base/prefs/content/aw-incoming.js42
-rw-r--r--comm/mailnews/base/prefs/content/converterDialog.js385
-rw-r--r--comm/mailnews/base/prefs/content/converterDialog.xhtml53
-rw-r--r--comm/mailnews/base/prefs/content/removeAccount.js168
-rw-r--r--comm/mailnews/base/prefs/content/removeAccount.xhtml84
-rw-r--r--comm/mailnews/base/public/MailNewsTypes.h40
-rw-r--r--comm/mailnews/base/public/MailNewsTypes2.idl93
-rw-r--r--comm/mailnews/base/public/moz.build80
-rw-r--r--comm/mailnews/base/public/mozINewMailListener.idl22
-rw-r--r--comm/mailnews/base/public/mozINewMailNotificationService.idl58
-rw-r--r--comm/mailnews/base/public/msgCore.h210
-rw-r--r--comm/mailnews/base/public/msgIOAuth2Module.idl56
-rw-r--r--comm/mailnews/base/public/nsICopyMessageListener.idl24
-rw-r--r--comm/mailnews/base/public/nsICopyMessageStreamListener.idl20
-rw-r--r--comm/mailnews/base/public/nsIFolderListener.idl70
-rw-r--r--comm/mailnews/base/public/nsIFolderLookupService.idl52
-rw-r--r--comm/mailnews/base/public/nsIIncomingServerListener.idl32
-rw-r--r--comm/mailnews/base/public/nsIMailAuthModule.idl40
-rw-r--r--comm/mailnews/base/public/nsIMailChannel.idl109
-rw-r--r--comm/mailnews/base/public/nsIMapiRegistry.idl50
-rw-r--r--comm/mailnews/base/public/nsIMessenger.idl127
-rw-r--r--comm/mailnews/base/public/nsIMessengerMigrator.idl14
-rw-r--r--comm/mailnews/base/public/nsIMessengerOSIntegration.idl26
-rw-r--r--comm/mailnews/base/public/nsIMessengerWindowService.idl17
-rw-r--r--comm/mailnews/base/public/nsIMessengerWindowsIntegration.idl16
-rw-r--r--comm/mailnews/base/public/nsIMsgAccount.idl86
-rw-r--r--comm/mailnews/base/public/nsIMsgAccountManager.idl256
-rw-r--r--comm/mailnews/base/public/nsIMsgAsyncPrompter.idl79
-rw-r--r--comm/mailnews/base/public/nsIMsgBiffManager.idl18
-rw-r--r--comm/mailnews/base/public/nsIMsgContentPolicy.idl35
-rw-r--r--comm/mailnews/base/public/nsIMsgCopyService.idl116
-rw-r--r--comm/mailnews/base/public/nsIMsgCopyServiceListener.idl55
-rw-r--r--comm/mailnews/base/public/nsIMsgCustomColumnHandler.idl40
-rw-r--r--comm/mailnews/base/public/nsIMsgDBView.idl558
-rw-r--r--comm/mailnews/base/public/nsIMsgEnumerator.idl94
-rw-r--r--comm/mailnews/base/public/nsIMsgFolder.idl877
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderCache.idl48
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderCacheElement.idl35
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderCompactor.idl34
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderListener.idl227
-rw-r--r--comm/mailnews/base/public/nsIMsgFolderNotificationService.idl119
-rw-r--r--comm/mailnews/base/public/nsIMsgHdr.idl109
-rw-r--r--comm/mailnews/base/public/nsIMsgIdentity.idl311
-rw-r--r--comm/mailnews/base/public/nsIMsgIncomingServer.idl596
-rw-r--r--comm/mailnews/base/public/nsIMsgMailNewsUrl.idl211
-rw-r--r--comm/mailnews/base/public/nsIMsgMailSession.idl78
-rw-r--r--comm/mailnews/base/public/nsIMsgMdnGenerator.idl70
-rw-r--r--comm/mailnews/base/public/nsIMsgMessageService.idl226
-rw-r--r--comm/mailnews/base/public/nsIMsgOfflineManager.idl22
-rw-r--r--comm/mailnews/base/public/nsIMsgPluggableStore.idl335
-rw-r--r--comm/mailnews/base/public/nsIMsgProgress.idl38
-rw-r--r--comm/mailnews/base/public/nsIMsgProtocolHandler.idl13
-rw-r--r--comm/mailnews/base/public/nsIMsgProtocolInfo.idl97
-rw-r--r--comm/mailnews/base/public/nsIMsgPurgeService.idl13
-rw-r--r--comm/mailnews/base/public/nsIMsgShutdown.idl67
-rw-r--r--comm/mailnews/base/public/nsIMsgStatusFeedback.idl18
-rw-r--r--comm/mailnews/base/public/nsIMsgTagService.idl67
-rw-r--r--comm/mailnews/base/public/nsIMsgThread.idl35
-rw-r--r--comm/mailnews/base/public/nsIMsgUserFeedbackListener.idl28
-rw-r--r--comm/mailnews/base/public/nsIMsgWindow.idl64
-rw-r--r--comm/mailnews/base/public/nsISpamSettings.idl97
-rw-r--r--comm/mailnews/base/public/nsIStatusBarBiffManager.idl13
-rw-r--r--comm/mailnews/base/public/nsIStopwatch.idl44
-rw-r--r--comm/mailnews/base/public/nsISubscribableServer.idl74
-rw-r--r--comm/mailnews/base/public/nsIUrlListener.idl32
-rw-r--r--comm/mailnews/base/public/nsIUserInfo.idl32
-rw-r--r--comm/mailnews/base/public/nsMsgFolderFlags.idl117
-rw-r--r--comm/mailnews/base/public/nsMsgGroupnameFlags.h48
-rw-r--r--comm/mailnews/base/public/nsMsgHeaderMasks.h53
-rw-r--r--comm/mailnews/base/public/nsMsgLocalFolderHdrs.h47
-rw-r--r--comm/mailnews/base/public/nsMsgMessageFlags.idl183
-rw-r--r--comm/mailnews/base/src/ABQueryUtils.jsm159
-rw-r--r--comm/mailnews/base/src/FolderLookupService.jsm163
-rw-r--r--comm/mailnews/base/src/FolderUtils.jsm364
-rw-r--r--comm/mailnews/base/src/HeaderReader.h305
-rw-r--r--comm/mailnews/base/src/JXON.jsm159
-rw-r--r--comm/mailnews/base/src/LineReader.h188
-rw-r--r--comm/mailnews/base/src/LineReader.jsm68
-rw-r--r--comm/mailnews/base/src/MailAuthenticator.jsm468
-rw-r--r--comm/mailnews/base/src/MailChannel.sys.mjs71
-rw-r--r--comm/mailnews/base/src/MailCryptoUtils.jsm76
-rw-r--r--comm/mailnews/base/src/MailNewsDLF.cpp84
-rw-r--r--comm/mailnews/base/src/MailNewsDLF.h37
-rw-r--r--comm/mailnews/base/src/MailNotificationManager.jsm478
-rw-r--r--comm/mailnews/base/src/MailNotificationService.jsm375
-rw-r--r--comm/mailnews/base/src/MailServices.jsm169
-rw-r--r--comm/mailnews/base/src/MailStringUtils.jsm102
-rw-r--r--comm/mailnews/base/src/MailnewsLoadContextInfo.cpp51
-rw-r--r--comm/mailnews/base/src/MailnewsLoadContextInfo.h32
-rw-r--r--comm/mailnews/base/src/MailnewsMigrator.jsm352
-rw-r--r--comm/mailnews/base/src/MsgAsyncPrompter.jsm621
-rw-r--r--comm/mailnews/base/src/MsgDBCacheManager.jsm185
-rw-r--r--comm/mailnews/base/src/MsgIncomingServer.jsm1268
-rw-r--r--comm/mailnews/base/src/MsgKeySet.jsm132
-rw-r--r--comm/mailnews/base/src/MsgProtocolInfo.sys.mjs53
-rw-r--r--comm/mailnews/base/src/OAuth2.jsm364
-rw-r--r--comm/mailnews/base/src/OAuth2Module.jsm203
-rw-r--r--comm/mailnews/base/src/OAuth2Providers.jsm259
-rw-r--r--comm/mailnews/base/src/TemplateUtils.jsm90
-rw-r--r--comm/mailnews/base/src/UrlListener.cpp22
-rw-r--r--comm/mailnews/base/src/UrlListener.h72
-rw-r--r--comm/mailnews/base/src/VirtualFolderWrapper.jsm257
-rw-r--r--comm/mailnews/base/src/WinUnreadBadge.jsm246
-rw-r--r--comm/mailnews/base/src/components.conf359
-rw-r--r--comm/mailnews/base/src/converterWorker.js533
-rw-r--r--comm/mailnews/base/src/hostnameUtils.jsm366
-rw-r--r--comm/mailnews/base/src/mailstoreConverter.jsm339
-rw-r--r--comm/mailnews/base/src/moz.build154
-rw-r--r--comm/mailnews/base/src/nsCidProtocolHandler.cpp49
-rw-r--r--comm/mailnews/base/src/nsCidProtocolHandler.h25
-rw-r--r--comm/mailnews/base/src/nsCopyMessageStreamListener.cpp106
-rw-r--r--comm/mailnews/base/src/nsCopyMessageStreamListener.h29
-rw-r--r--comm/mailnews/base/src/nsImapMoveCoalescer.cpp198
-rw-r--r--comm/mailnews/base/src/nsImapMoveCoalescer.h71
-rw-r--r--comm/mailnews/base/src/nsMailAuthModule.cpp85
-rw-r--r--comm/mailnews/base/src/nsMailAuthModule.h27
-rw-r--r--comm/mailnews/base/src/nsMailChannel.cpp139
-rw-r--r--comm/mailnews/base/src/nsMailChannel.h30
-rw-r--r--comm/mailnews/base/src/nsMailDirProvider.cpp160
-rw-r--r--comm/mailnews/base/src/nsMailDirProvider.h42
-rw-r--r--comm/mailnews/base/src/nsMailDirServiceDefs.h31
-rw-r--r--comm/mailnews/base/src/nsMessenger.cpp2446
-rw-r--r--comm/mailnews/base/src/nsMessenger.h118
-rw-r--r--comm/mailnews/base/src/nsMessengerBootstrap.cpp84
-rw-r--r--comm/mailnews/base/src/nsMessengerBootstrap.h30
-rw-r--r--comm/mailnews/base/src/nsMessengerOSXIntegration.h24
-rw-r--r--comm/mailnews/base/src/nsMessengerOSXIntegration.mm63
-rw-r--r--comm/mailnews/base/src/nsMessengerUnixIntegration.cpp24
-rw-r--r--comm/mailnews/base/src/nsMessengerUnixIntegration.h22
-rw-r--r--comm/mailnews/base/src/nsMessengerWinIntegration.cpp379
-rw-r--r--comm/mailnews/base/src/nsMessengerWinIntegration.h39
-rw-r--r--comm/mailnews/base/src/nsMsgAccount.cpp413
-rw-r--r--comm/mailnews/base/src/nsMsgAccount.h34
-rw-r--r--comm/mailnews/base/src/nsMsgAccountManager.cpp3546
-rw-r--r--comm/mailnews/base/src/nsMsgAccountManager.h211
-rw-r--r--comm/mailnews/base/src/nsMsgBiffManager.cpp341
-rw-r--r--comm/mailnews/base/src/nsMsgBiffManager.h52
-rw-r--r--comm/mailnews/base/src/nsMsgCompressIStream.cpp203
-rw-r--r--comm/mailnews/base/src/nsMsgCompressIStream.h33
-rw-r--r--comm/mailnews/base/src/nsMsgCompressOStream.cpp128
-rw-r--r--comm/mailnews/base/src/nsMsgCompressOStream.h26
-rw-r--r--comm/mailnews/base/src/nsMsgContentPolicy.cpp928
-rw-r--r--comm/mailnews/base/src/nsMsgContentPolicy.h95
-rw-r--r--comm/mailnews/base/src/nsMsgCopyService.cpp587
-rw-r--r--comm/mailnews/base/src/nsMsgCopyService.h91
-rw-r--r--comm/mailnews/base/src/nsMsgDBFolder.cpp5573
-rw-r--r--comm/mailnews/base/src/nsMsgDBFolder.h366
-rw-r--r--comm/mailnews/base/src/nsMsgDBView.cpp7411
-rw-r--r--comm/mailnews/base/src/nsMsgDBView.h558
-rw-r--r--comm/mailnews/base/src/nsMsgEnumerator.cpp138
-rw-r--r--comm/mailnews/base/src/nsMsgEnumerator.h45
-rw-r--r--comm/mailnews/base/src/nsMsgFileStream.cpp190
-rw-r--r--comm/mailnews/base/src/nsMsgFileStream.h35
-rw-r--r--comm/mailnews/base/src/nsMsgFolderCache.cpp570
-rw-r--r--comm/mailnews/base/src/nsMsgFolderCache.h60
-rw-r--r--comm/mailnews/base/src/nsMsgFolderCompactor.cpp1391
-rw-r--r--comm/mailnews/base/src/nsMsgFolderCompactor.h48
-rw-r--r--comm/mailnews/base/src/nsMsgFolderNotificationService.cpp174
-rw-r--r--comm/mailnews/base/src/nsMsgFolderNotificationService.h46
-rw-r--r--comm/mailnews/base/src/nsMsgGroupThread.cpp731
-rw-r--r--comm/mailnews/base/src/nsMsgGroupThread.h88
-rw-r--r--comm/mailnews/base/src/nsMsgGroupView.cpp941
-rw-r--r--comm/mailnews/base/src/nsMsgGroupView.h78
-rw-r--r--comm/mailnews/base/src/nsMsgI18N.cpp403
-rw-r--r--comm/mailnews/base/src/nsMsgI18N.h138
-rw-r--r--comm/mailnews/base/src/nsMsgIdentity.cpp645
-rw-r--r--comm/mailnews/base/src/nsMsgIdentity.h87
-rw-r--r--comm/mailnews/base/src/nsMsgIncomingServer.cpp2142
-rw-r--r--comm/mailnews/base/src/nsMsgIncomingServer.h103
-rw-r--r--comm/mailnews/base/src/nsMsgKeySet.cpp1412
-rw-r--r--comm/mailnews/base/src/nsMsgKeySet.h108
-rw-r--r--comm/mailnews/base/src/nsMsgLineBuffer.cpp351
-rw-r--r--comm/mailnews/base/src/nsMsgLineBuffer.h123
-rw-r--r--comm/mailnews/base/src/nsMsgMailNewsUrl.cpp1070
-rw-r--r--comm/mailnews/base/src/nsMsgMailNewsUrl.h142
-rw-r--r--comm/mailnews/base/src/nsMsgMailSession.cpp671
-rw-r--r--comm/mailnews/base/src/nsMsgMailSession.h110
-rw-r--r--comm/mailnews/base/src/nsMsgOfflineManager.cpp352
-rw-r--r--comm/mailnews/base/src/nsMsgOfflineManager.h79
-rw-r--r--comm/mailnews/base/src/nsMsgProgress.cpp250
-rw-r--r--comm/mailnews/base/src/nsMsgProgress.h45
-rw-r--r--comm/mailnews/base/src/nsMsgProtocol.cpp1512
-rw-r--r--comm/mailnews/base/src/nsMsgProtocol.h263
-rw-r--r--comm/mailnews/base/src/nsMsgPurgeService.cpp496
-rw-r--r--comm/mailnews/base/src/nsMsgPurgeService.h51
-rw-r--r--comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp806
-rw-r--r--comm/mailnews/base/src/nsMsgQuickSearchDBView.h99
-rw-r--r--comm/mailnews/base/src/nsMsgReadStateTxn.cpp43
-rw-r--r--comm/mailnews/base/src/nsMsgReadStateTxn.h44
-rw-r--r--comm/mailnews/base/src/nsMsgSearchDBView.cpp1344
-rw-r--r--comm/mailnews/base/src/nsMsgSearchDBView.h182
-rw-r--r--comm/mailnews/base/src/nsMsgSpecialViews.cpp163
-rw-r--r--comm/mailnews/base/src/nsMsgSpecialViews.h82
-rw-r--r--comm/mailnews/base/src/nsMsgStatusFeedback.cpp261
-rw-r--r--comm/mailnews/base/src/nsMsgStatusFeedback.h48
-rw-r--r--comm/mailnews/base/src/nsMsgTagService.cpp458
-rw-r--r--comm/mailnews/base/src/nsMsgTagService.h50
-rw-r--r--comm/mailnews/base/src/nsMsgThreadedDBView.cpp899
-rw-r--r--comm/mailnews/base/src/nsMsgThreadedDBView.h62
-rw-r--r--comm/mailnews/base/src/nsMsgTxn.cpp247
-rw-r--r--comm/mailnews/base/src/nsMsgTxn.h77
-rw-r--r--comm/mailnews/base/src/nsMsgUtils.cpp1926
-rw-r--r--comm/mailnews/base/src/nsMsgUtils.h462
-rw-r--r--comm/mailnews/base/src/nsMsgWindow.cpp327
-rw-r--r--comm/mailnews/base/src/nsMsgWindow.h52
-rw-r--r--comm/mailnews/base/src/nsMsgXFViewThread.cpp444
-rw-r--r--comm/mailnews/base/src/nsMsgXFViewThread.h51
-rw-r--r--comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp514
-rw-r--r--comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h70
-rw-r--r--comm/mailnews/base/src/nsNewMailnewsURI.cpp155
-rw-r--r--comm/mailnews/base/src/nsNewMailnewsURI.h15
-rw-r--r--comm/mailnews/base/src/nsQuarantinedOutputStream.cpp234
-rw-r--r--comm/mailnews/base/src/nsQuarantinedOutputStream.h72
-rw-r--r--comm/mailnews/base/src/nsSpamSettings.cpp806
-rw-r--r--comm/mailnews/base/src/nsSpamSettings.h68
-rw-r--r--comm/mailnews/base/src/nsStatusBarBiffManager.cpp254
-rw-r--r--comm/mailnews/base/src/nsStatusBarBiffManager.h37
-rw-r--r--comm/mailnews/base/src/nsStopwatch.cpp164
-rw-r--r--comm/mailnews/base/src/nsStopwatch.h51
-rw-r--r--comm/mailnews/base/src/nsSubscribableServer.cpp867
-rw-r--r--comm/mailnews/base/src/nsSubscribableServer.h59
-rw-r--r--comm/mailnews/base/src/nsUserInfo.h23
-rw-r--r--comm/mailnews/base/src/nsUserInfoMac.mm70
-rw-r--r--comm/mailnews/base/src/nsUserInfoUnix.cpp124
-rw-r--r--comm/mailnews/base/src/nsUserInfoWin.cpp99
-rw-r--r--comm/mailnews/base/test/.eslintrc.js5
-rw-r--r--comm/mailnews/base/test/TestMsgStripRE.cpp91
-rw-r--r--comm/mailnews/base/test/gtest/TestHeaderReader.cpp205
-rw-r--r--comm/mailnews/base/test/gtest/TestLineReader.cpp200
-rw-r--r--comm/mailnews/base/test/gtest/moz.build15
-rw-r--r--comm/mailnews/base/test/moz.build19
-rw-r--r--comm/mailnews/base/test/unit/data/folderCache.json206
-rw-r--r--comm/mailnews/base/test/unit/data/panacea.dat70
-rw-r--r--comm/mailnews/base/test/unit/data/panacea_empty.dat1
-rw-r--r--comm/mailnews/base/test/unit/data/remoteContent.sql41
-rw-r--r--comm/mailnews/base/test/unit/head_mailbase.js23
-rw-r--r--comm/mailnews/base/test/unit/nodelist_test.xml7
-rw-r--r--comm/mailnews/base/test/unit/test_MsgIncomingServer.js224
-rw-r--r--comm/mailnews/base/test/unit/test_MsgKeySet.js85
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgr.js102
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgr2.js199
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgrCustomTypes.js94
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgrMovedLocalFolders.js43
-rw-r--r--comm/mailnews/base/test/unit/test_accountMgrRemoveDefault.js55
-rw-r--r--comm/mailnews/base/test/unit/test_accountMigration.js184
-rw-r--r--comm/mailnews/base/test/unit/test_acctRepair.js56
-rw-r--r--comm/mailnews/base/test/unit/test_bccInDatabase.js54
-rw-r--r--comm/mailnews/base/test/unit/test_bug428427.js249
-rw-r--r--comm/mailnews/base/test/unit/test_bug434810.js27
-rw-r--r--comm/mailnews/base/test/unit/test_bug471682.js118
-rw-r--r--comm/mailnews/base/test/unit/test_bug514945.js40
-rw-r--r--comm/mailnews/base/test/unit/test_closedDB.js104
-rw-r--r--comm/mailnews/base/test/unit/test_compactFailure.js134
-rw-r--r--comm/mailnews/base/test/unit/test_converterDeferredAccount.js206
-rw-r--r--comm/mailnews/base/test/unit/test_copyChaining.js109
-rw-r--r--comm/mailnews/base/test/unit/test_copyToInvalidDB.js106
-rw-r--r--comm/mailnews/base/test/unit/test_detachToFile.js159
-rw-r--r--comm/mailnews/base/test/unit/test_emptyTrash.js174
-rw-r--r--comm/mailnews/base/test/unit/test_fix_deferred_accounts.js59
-rw-r--r--comm/mailnews/base/test/unit/test_folderCompact.js335
-rw-r--r--comm/mailnews/base/test/unit/test_folderCompact2.js300
-rw-r--r--comm/mailnews/base/test/unit/test_folderLookupService.js83
-rw-r--r--comm/mailnews/base/test/unit/test_folderStringProperties.js41
-rw-r--r--comm/mailnews/base/test/unit/test_formatFileSize.js144
-rw-r--r--comm/mailnews/base/test/unit/test_getMsgTextFromStream.js88
-rw-r--r--comm/mailnews/base/test/unit/test_headerFoldingInDatabase.js58
-rw-r--r--comm/mailnews/base/test/unit/test_hostnameUtils.js276
-rw-r--r--comm/mailnews/base/test/unit/test_identity.js69
-rw-r--r--comm/mailnews/base/test/unit/test_imapPump.js81
-rw-r--r--comm/mailnews/base/test/unit/test_incomingServer.js99
-rw-r--r--comm/mailnews/base/test/unit/test_inheritedFolderProperties.js183
-rw-r--r--comm/mailnews/base/test/unit/test_junkingWhenDisabled.js176
-rw-r--r--comm/mailnews/base/test/unit/test_loadVirtualFolders.js64
-rw-r--r--comm/mailnews/base/test/unit/test_mailServices.js67
-rw-r--r--comm/mailnews/base/test/unit/test_mailstoreConverter.js376
-rw-r--r--comm/mailnews/base/test/unit/test_mimemaltdetach.js160
-rw-r--r--comm/mailnews/base/test/unit/test_newMailNotification.js203
-rw-r--r--comm/mailnews/base/test/unit/test_nsIFolderListener.js45
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgContentPolicy.js68
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgFolder.js102
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgFolderCache.js228
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgFolderListener.js214
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgFolderListenerLocal.js444
-rw-r--r--comm/mailnews/base/test/unit/test_nsIMsgTagService.js113
-rw-r--r--comm/mailnews/base/test/unit/test_nsMailDirProvider.js23
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgDBView.js1212
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgDBView_headerValues.js110
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgMailSession_Alerts.js217
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgMailSession_Listeners.js161
-rw-r--r--comm/mailnews/base/test/unit/test_nsMsgTraitService.js130
-rw-r--r--comm/mailnews/base/test/unit/test_postPluginFilters.js223
-rw-r--r--comm/mailnews/base/test/unit/test_retention.js66
-rw-r--r--comm/mailnews/base/test/unit/test_saveAs.js172
-rw-r--r--comm/mailnews/base/test/unit/test_testsuite_base64.js22
-rw-r--r--comm/mailnews/base/test/unit/test_testsuite_fakeserverAuth.js58
-rw-r--r--comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_gmail.js92
-rw-r--r--comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_list-extended.js150
-rw-r--r--comm/mailnews/base/test/unit/test_viewSortByAddresses.js144
-rw-r--r--comm/mailnews/base/test/unit/test_virtualFolders1.js205
-rw-r--r--comm/mailnews/base/test/unit/test_virtualFolders2.js90
-rw-r--r--comm/mailnews/base/test/unit/test_virtualFolders3.js229
-rw-r--r--comm/mailnews/base/test/unit/xpcshell-imap.ini8
-rw-r--r--comm/mailnews/base/test/unit/xpcshell.ini77
379 files changed, 97165 insertions, 0 deletions
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 "<last>".
+ */
+ _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 <menulist>, 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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at https://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderProps.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderColors.css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/folderProps.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&folderProps.windowtitle.label;</title>
+ <link rel="localization" href="messenger/folderprops.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/retention.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/folderProps.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog id="folderPropertiesDialog">
+ <tabbox id="folderPropTabBox">
+ <tabs id="folderPropTabs">
+ <tab id="GeneralTab" label="&generalInfo.label;" />
+ <tab id="Retention" label="&retention.label;" />
+ <tab
+ id="SynchronizationTab"
+ hidefor="pop3,rss,none"
+ label="&folderSynchronizationTab.label;"
+ />
+ <tab
+ id="SharingTab"
+ hidefor="pop3,rss,none,nntp"
+ label="&folderSharingTab.label;"
+ />
+ <tab
+ id="QuotaTab"
+ hidefor="pop3,rss,none,nntp"
+ label="&folderQuotaTab.label;"
+ />
+ </tabs>
+ <tabpanels id="folderPropTabPanels">
+ <vbox id="GeneralPanel">
+ <hbox id="nameBox" align="center" class="input-container">
+ <label
+ id="nameLabel"
+ value="&folderProps.name.label;"
+ control="name"
+ accesskey="&folderProps.name.accesskey;"
+ />
+ <hbox align="center" class="input-container">
+ <html:input
+ id="name"
+ type="text"
+ readonly="readonly"
+ oninput="doEnabling();"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ />
+ <label
+ id="colorLabel"
+ value="&folderProps.color.label;"
+ control="color"
+ accesskey="&folderProps.color.accesskey;"
+ />
+ <html:input
+ id="color"
+ type="color"
+ value=""
+ class="input-inline-color"
+ aria-labelledby="colorLabel"
+ />
+ <button
+ id="resetColor"
+ tooltiptext="&folderProps.reset.tooltip;"
+ class="toolbarbutton-1 btn-flat btn-reset"
+ />
+ </hbox>
+ <label
+ id="locationLabel"
+ value="&folderProps.location.label;"
+ control="location"
+ accesskey="&folderProps.location.accesskey;"
+ />
+ <html:input
+ id="location"
+ type="text"
+ readonly="readonly"
+ class="uri-element input-inline"
+ aria-labelledby="locationLabel"
+ />
+ </hbox>
+ <vbox>
+ <spacer height="2" />
+ <hbox align="center">
+ <label value="&numberOfMessages.label;" />
+ <label id="numberOfMessages" value="&numberUnknown.label;" />
+ <spacer flex="1" />
+ <label value="&sizeOnDisk.label;" />
+ <label id="sizeOnDisk" value="&sizeUnknown.label;" />
+ </hbox>
+ <spacer height="2" />
+ </vbox>
+ <checkbox
+ id="folderIncludeInGlobalSearch"
+ hidefor="nntp"
+ label="&folderIncludeInGlobalSearch.label;"
+ accesskey="&folderIncludeInGlobalSearch.accesskey;"
+ />
+ <checkbox
+ hidefor="pop3,none,nntp"
+ id="folderCheckForNewMessages"
+ label="&folderCheckForNewMessages2.label;"
+ accesskey="&folderCheckForNewMessages2.accesskey;"
+ />
+ <separator class="thin" />
+ <hbox>
+ <description id="folderRebuildSummaryExplanation" flex="1">
+ &folderRebuildSummaryFile.explanation;
+ </description>
+ <vbox>
+ <button
+ id="folderRebuildSummaryButton"
+ label="&folderRebuildSummaryFile2.label;"
+ oncommand="RebuildSummaryInformation();"
+ accesskey="&folderRebuildSummaryFile2.accesskey;"
+ tooltiptext="&folderRebuildSummaryFileTip2.label;"
+ align="center"
+ />
+ </vbox>
+ </hbox>
+ </vbox>
+
+ <vbox id="RetentionPanel" align="start">
+ <description hidefor="imap,pop3" class="desc"
+ >&retentionCleanup.label;</description
+ >
+ <description hidefor="pop3,rss,none,nntp" class="desc"
+ >&retentionCleanupImap.label;</description
+ >
+ <description hidefor="imap,rss,none,nntp" class="desc"
+ >&retentionCleanupPop.label;</description
+ >
+
+ <hbox align="center" class="indent">
+ <checkbox
+ wsm_persist="true"
+ id="retention.useDefault"
+ accesskey="&retentionUseAccount.accesskey;"
+ label="&retentionUseAccount.label;"
+ checked="true"
+ oncommand="onUseDefaultRetentionSettings()"
+ />
+ </hbox>
+ <vbox class="indent">
+ <hbox class="indent">
+ <radiogroup
+ wsm_persist="true"
+ id="retention.keepMsg"
+ aria-labelledby="retention.useDefault"
+ >
+ <radio
+ wsm_persist="true"
+ value="1"
+ accesskey="&retentionKeepAll.accesskey;"
+ label="&retentionKeepAll.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <hbox flex="1" align="center">
+ <radio
+ wsm_persist="true"
+ id="keepNewMsg"
+ accesskey="&retentionKeepRecent.accesskey;"
+ value="3"
+ label="&retentionKeepRecent.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepNewMsgMin"
+ type="number"
+ class="size4"
+ min="1"
+ value="2000"
+ wsm_persist="true"
+ aria-labelledby="keepNewMsg retention.keepNewMsgMin retention.keepNewMsgMinLabel"
+ />
+ <label
+ value="&message.label;"
+ control="retention.keepNewMsgMin"
+ id="retention.keepNewMsgMinLabel"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ wsm_persist="true"
+ id="keepMsg"
+ accesskey="&retentionDeleteMsg.accesskey;"
+ value="2"
+ label="&retentionDeleteMsg.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepOldMsgMin"
+ type="number"
+ class="size4"
+ min="1"
+ value="30"
+ wsm_persist="true"
+ aria-labelledby="keepMsg retention.keepOldMsgMin retention.keepOldMsgMinLabel"
+ />
+ <label
+ value="&daysOld.label;"
+ control="retention.keepOldMsgMin"
+ id="retention.keepOldMsgMinLabel"
+ />
+ </hbox>
+ </radiogroup>
+ </hbox>
+ <hbox class="indent">
+ <checkbox
+ id="retention.applyToFlagged"
+ wsm_persist="true"
+ label="&retentionApplyToFlagged.label;"
+ accesskey="&retentionApplyToFlagged.accesskey;"
+ observes="retention.keepMsg"
+ checked="true"
+ />
+ </hbox>
+ </vbox>
+ </vbox>
+
+ <vbox id="SyncPanel" align="start">
+ <vbox>
+ <checkbox
+ hidefor="nntp"
+ wsm_persist="true"
+ id="offline.selectForOfflineFolder"
+ label="&offlineFolder.check.label;"
+ accesskey="&offlineFolder.check.accesskey;"
+ />
+ <checkbox
+ hidefor="imap"
+ wsm_persist="true"
+ id="offline.selectForOfflineNewsgroup"
+ label="&selectofflineNewsgroup.check.label;"
+ accesskey="&selectofflineNewsgroup.check.accesskey;"
+ />
+ </vbox>
+ <button
+ hidefor="nntp"
+ label="&offlineFolder.button.label;"
+ oncommand="onOfflineFolderDownload();"
+ accesskey="&offlineFolder.button.accesskey;"
+ id="offline.offlineFolderDownloadButton"
+ orient="right"
+ />
+ <button
+ hidefor="imap"
+ label="&offlineNewsgroup.button.label;"
+ oncommand="onOfflineFolderDownload();"
+ accesskey="&offlineNewsgroup.button.accesskey;"
+ id="offline.offlineNewsgroupDownloadButton"
+ orient="right"
+ />
+ </vbox>
+
+ <vbox id="SharingPanel">
+ <hbox align="start">
+ <label value="&folderType.label;" id="folderTypeLabel" />
+ <label value="" id="folderType.text" />
+ </hbox>
+ <vbox align="start">
+ <label value="" id="folderDescription.text" />
+ <label value=" " />
+ <label
+ value="&permissionsDesc.label;"
+ id="permissionsDescLabel"
+ />
+
+ <description id="folderPermissions.text"></description>
+ </vbox>
+ <vbox id="folderOtherUsers" align="start" hidden="true">
+ <label value=" " />
+ <label value="&folderOtherUsers.label;" />
+ <description id="folderOtherUsersText"></description>
+ </vbox>
+ <spacer flex="1" />
+ <vbox align="start">
+ <button
+ hidefor="pop3,none,rss,nntp"
+ label="&privileges.button.label;"
+ oncommand="onFolderPrivileges();"
+ accesskey="&privileges.button.accesskey;"
+ id="imap.FolderPrivileges"
+ orient="right"
+ />
+ </vbox>
+ </vbox>
+
+ <vbox id="quotaPanel">
+ <label id="folderQuotaStatus" flex="1" />
+ <hbox id="folderQuotaData" hidden="true">
+ <html:ul id="quotaDetails"></html:ul>
+ </hbox>
+ </vbox>
+ </tabpanels>
+ </tabbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/junkLog.dtd">
+
+<html
+ id="viewLogWindow"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="screenX screenY width height"
+ width="600"
+ height="375"
+ scrolling="false"
+>
+ <head>
+ <title>&adaptiveJunkLog.title;</title>
+ <script defer="defer" src="chrome://messenger/content/junkLog.js"></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept"
+ buttonlabelaccept="&closeLog.label;"
+ buttonaccesskeyaccept="&closeLog.accesskey;"
+ >
+ <vbox flex="1">
+ <hbox>
+ <label value="&adaptiveJunkLogInfo.label;" />
+ <spacer flex="1" />
+ <button
+ label="&clearLog.label;"
+ accesskey="&clearLog.accesskey;"
+ oncommand="clearLog();"
+ />
+ </hbox>
+ <separator class="thin" />
+ <hbox flex="1">
+ <browser
+ id="logView"
+ type="content"
+ disablehistory="true"
+ disablesecurity="true"
+ src="about:blank"
+ autofind="false"
+ flex="1"
+ />
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/markByDate.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&messageMarkByDate.label;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dateFormat.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/markByDate.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="accept,cancel">
+ <hbox align="center" pack="end">
+ <label
+ id="lowerDateLabel"
+ control="lowerDate"
+ value="&markByDateLower.label;"
+ accesskey="&markByDateLower.accesskey;"
+ />
+ <html:input
+ id="lowerDate"
+ type="text"
+ class="input-inline"
+ aria-labelledby="lowerDateLabel"
+ size="11"
+ />
+ </hbox>
+ <hbox align="center" pack="end">
+ <label
+ id="upperDateLabel"
+ control="upperDate"
+ value="&markByDateUpper.label;"
+ accesskey="&markByDateUpper.accesskey;"
+ />
+ <html:input
+ id="upperDate"
+ type="text"
+ class="input-inline"
+ aria-labelledby="upperDateLabel"
+ size="11"
+ />
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountCentral.css" type="text/css"?>
+
+<!DOCTYPE html>
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ windowtype="mail:accountcentral"
+ lightweightthemes="true"
+>
+ <head>
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/accountCentral.ftl" />
+
+ <script
+ defer="defer"
+ src="chrome://communicator/content/utilityOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/msgAccountCentral.js"
+ ></script>
+ </head>
+
+ <body>
+ <main id="accountCentral">
+ <header
+ id="headerFirstRun"
+ class="account-central-header"
+ hidden="hidden"
+ >
+ <img
+ id="brandLogo"
+ src="chrome://branding/content/about-logo.svg"
+ alt=""
+ />
+ <aside>
+ <h1
+ class="account-central-title"
+ data-l10n-id="account-central-title"
+ ></h1>
+ <div class="account-central-version">
+ <span id="version"></span>
+ <a
+ id="releasenotes"
+ data-l10n-id="release-notes"
+ onclick="openAboutDialog();"
+ onkeypress="if (event.key == 'Enter') { openAboutDialog(); }"
+ >
+ <img
+ src="chrome://messenger/skin/icons/new/compact/info.svg"
+ alt=""
+ />
+ </a>
+ </div>
+ </aside>
+ </header>
+
+ <header
+ id="headerExistingAccounts"
+ class="account-central-header summary-header"
+ >
+ <span id="accountLogo"></span>
+ <aside>
+ <h1 id="accountName"></h1>
+ </aside>
+ <aside class="settings-btn-container">
+ <button
+ id="settingsButton"
+ type="button"
+ data-l10n-id="account-settings"
+ class="btn-link"
+ onclick="viewSettings(null);"
+ ></button>
+ </aside>
+ </header>
+
+ <section id="accountFeaturesSection" class="account-central-section">
+ <aside class="row-container account-options">
+ <button
+ id="readButton"
+ type="button"
+ data-l10n-id="read"
+ class="btn-link btn-inline"
+ onclick="readMessages();"
+ ></button>
+ <button
+ id="nntpSubscriptionButton"
+ type="button"
+ data-l10n-id="nntp-subscription"
+ class="btn-link btn-inline"
+ onclick="subscribe();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="rssSubscriptionButton"
+ type="button"
+ data-l10n-id="rss-subscription"
+ class="btn-link btn-inline"
+ onclick="subscribe();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="composeButton"
+ type="button"
+ data-l10n-id="compose"
+ class="btn-link btn-inline"
+ onclick="window.browsingContext.topChromeWindow.MsgNewMessage(event);"
+ hidden="hidden"
+ ></button>
+ <button
+ id="searchButton"
+ type="button"
+ data-l10n-id="search"
+ class="btn-link btn-inline"
+ onclick="searchMessages();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="filterButton"
+ type="button"
+ data-l10n-id="filter"
+ class="btn-link btn-inline"
+ onclick="createMsgFilters();"
+ hidden="hidden"
+ ></button>
+ <button
+ id="e2eButton"
+ type="button"
+ data-l10n-id="e2e"
+ class="btn-link btn-inline"
+ onclick="viewSettings('am-e2e.xhtml');"
+ hidden="hidden"
+ ></button>
+ </aside>
+ </section>
+
+ <section
+ id="accountSetupSection"
+ class="account-central-section setup-section zebra"
+ >
+ <h2 id="setupTitle" class="section-title"></h2>
+
+ <aside class="row-container">
+ <button
+ id="setupEmail"
+ type="button"
+ data-l10n-id="email-label"
+ class="btn-hub btn-inline"
+ onclick="top.openAccountSetupTab();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="email-description"></p>
+ </div>
+
+ <button
+ id="setupAddressBook"
+ type="button"
+ data-l10n-id="addressbook-label"
+ class="btn-hub btn-inline"
+ onclick="top.addNewAddressBook('CARDDAV');"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="addressbook-description"></p>
+ </div>
+
+ <button
+ id="setupCalendar"
+ type="button"
+ data-l10n-id="calendar-label"
+ class="btn-hub btn-inline"
+ onclick="window.browsingContext.topChromeWindow.cal.window.openCalendarWizard(window);"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="calendar-description"></p>
+ </div>
+
+ <button
+ id="setupChat"
+ type="button"
+ data-l10n-id="chat-label"
+ class="btn-hub btn-inline"
+ onclick="top.AddIMAccount();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="chat-description"></p>
+ </div>
+
+ <button
+ id="setupFilelink"
+ type="button"
+ data-l10n-id="filelink-label"
+ class="btn-hub btn-inline"
+ onclick="top.openOptionsDialog('paneCompose', 'compositionAttachmentsCategory');"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="filelink-description"></p>
+ </div>
+
+ <button
+ id="setupFeeds"
+ type="button"
+ data-l10n-id="feeds-label"
+ class="btn-hub btn-inline"
+ onclick="top.AddFeedAccount();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="feeds-description"></p>
+ </div>
+
+ <button
+ id="setupNewsgroups"
+ type="button"
+ data-l10n-id="newsgroups-label"
+ class="btn-hub btn-inline"
+ onclick="top.openNewsgroupAccountWizard();"
+ ></button>
+ <div class="account-description">
+ <p data-l10n-id="newsgroups-description"></p>
+ </div>
+ </aside>
+ </section>
+
+ <section class="account-central-section">
+ <h2 class="section-title" data-l10n-id="import-title"></h2>
+
+ <aside class="row-container">
+ <p data-l10n-id="import-paragraph2"></p>
+ </aside>
+ <aside class="row-container">
+ <button
+ id="importButton"
+ type="button"
+ data-l10n-id="import-label"
+ class="btn-hub btn-inline"
+ onclick="top.toImport();"
+ ></button>
+ </aside>
+ </section>
+
+ <section class="account-central-section">
+ <h2 class="section-title" data-l10n-id="about-title"></h2>
+
+ <aside class="row-container">
+ <p data-l10n-id="about-paragraph"></p>
+ <p
+ id="donationParagraph"
+ data-l10n-id="about-paragraph-consider-donation"
+ >
+ <a
+ href="https://give.thunderbird.net/en-US/?utm_source=account_hub_tb_release&amp;utm_medium=referral&amp;utm_content=paragraph_text"
+ class="donation-link"
+ data-l10n-name="donation-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ </p>
+ </aside>
+ </section>
+
+ <section class="account-central-section">
+ <h2 class="section-title" data-l10n-id="resources-title"></h2>
+
+ <aside class="row-container">
+ <a
+ id="featuresLink"
+ href="https://www.thunderbird.net/en-US/features/"
+ class="resource-link"
+ data-l10n-id="explore-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ <a
+ id="supportLink"
+ href="https://support.mozilla.org/products/thunderbird"
+ class="resource-link"
+ data-l10n-id="support-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ <a
+ id="involvedLink"
+ href="https://www.thunderbird.net/en-US/get-involved/"
+ class="resource-link"
+ data-l10n-id="involved-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ <a
+ id="developerLink"
+ href="https://developer.thunderbird.net/"
+ class="resource-link"
+ data-l10n-id="developer-link"
+ onclick="openLink(event);"
+ onkeypress="if (event.key == 'Enter') { openLink(event); }"
+ tabindex="0"
+ ></a>
+ </aside>
+ </section>
+ </main>
+ </body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/msgSelectOffline.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/msgSynchronize.dtd">
+
+<html
+ id="selectOffline"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:selectOffline"
+ lightweightthemes="true"
+ width="450"
+ height="400"
+ persist="width height screenX screenY"
+ scrolling="false"
+>
+ <head>
+ <title>&MsgSelect.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/jsTreeView.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/msgSelectOfflineFolders.js"
+ ></script>
+ <style id="folderColorsStyle"></style>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog>
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+ <stringbundle
+ id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"
+ />
+
+ <label class="desc" control="synchronizeTree"
+ >&MsgSelectDesc.label;</label
+ >
+
+ <tree
+ id="synchronizeTree"
+ flex="1"
+ hidecolumnpicker="true"
+ seltype="multiple"
+ disableKeyNavigation="true"
+ simplelist="true"
+ mode="offline"
+ onkeypress="gSelectOffline.onKeyPress(event);"
+ onclick="gSelectOffline.onClick(event);"
+ >
+ <treecols>
+ <treecol
+ id="folderNameCol"
+ persist="hidden width"
+ label="&MsgSelectItems.label;"
+ primary="true"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="syncCol"
+ style="flex: 1 auto"
+ persist="hidden width"
+ label="&MsgSelectInd.label;"
+ cycler="true"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at https://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/msgSynchronize.dtd">
+<html
+ id="msgSynchronize"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="mailnews:synchronizeOffline"
+ lightweightthemes="true"
+ style="height: 22em"
+ scrolling="false"
+>
+ <head>
+ <title>&MsgSynchronize.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/msgSynchronize.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog id="msg-synchronizer" style="width: 63ch">
+ <html:p>&MsgSyncDesc.label;</html:p>
+ <html:p>&MsgSyncDirections.label;</html:p>
+
+ <vbox class="indent" align="start">
+ <checkbox
+ id="syncMail"
+ hidable="true"
+ hidefor="pop3"
+ label="&syncTypeMail.label;"
+ accesskey="&syncTypeMail.accesskey;"
+ />
+ <checkbox
+ id="syncNews"
+ label="&syncTypeNews.label;"
+ accesskey="&syncTypeNews.accesskey;"
+ />
+ </vbox>
+ <vbox align="start">
+ <checkbox
+ id="sendMessage"
+ label="&sendMessage.label;"
+ accesskey="&sendMessage.accesskey;"
+ />
+ <checkbox
+ id="workOffline"
+ label="&workOffline.label;"
+ accesskey="&workOffline.accesskey;"
+ />
+ </vbox>
+ <separator class="thin" />
+ <hbox pack="end">
+ <button
+ id="select"
+ label="&selectButton.label;"
+ accesskey="&selectButton.accesskey;"
+ oncommand="OnSelect();"
+ />
+ </hbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % newFolderDTD SYSTEM "chrome://messenger/locale/newFolderDialog.dtd">
+%newFolderDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&newFolderDialog.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/newFolderDialog.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttonlabelaccept="&accept.label;"
+ buttonaccesskeyaccept="&accept.accesskey;"
+ >
+ <label
+ id="nameLabel"
+ value="&name.label;"
+ accesskey="&name.accesskey;"
+ control="name"
+ />
+ <html:input
+ id="name"
+ type="text"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ oninput="doEnabling();"
+ />
+
+ <separator />
+
+ <label
+ value="&description.label;"
+ accesskey="&description.accesskey;"
+ control="msgNewFolderPicker"
+ />
+
+ <menulist
+ id="msgNewFolderPicker"
+ class="folderMenuItem"
+ displayformat="verbose"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="MsgNewFolderPopup"
+ showFileHereLabel="true"
+ class="menulist-menupopup"
+ mode="newFolder"
+ oncommand="onFolderSelect(event)"
+ />
+ </menulist>
+
+ <vbox id="newFolderTypeBox">
+ <separator class="thin" />
+
+ <label value="&folderRestriction1.label;" />
+ <label value="&folderRestriction2.label;" />
+
+ <separator class="thin" />
+
+ <radiogroup id="folderGroup" orient="horizontal" class="indent">
+ <radio oncommand="onFoldersOnly();" label="&foldersOnly.label;" />
+ <radio
+ oncommand="onMessagesOnly();"
+ label="&messagesOnly.label;"
+ selected="true"
+ />
+ </radiogroup>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
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);
+
+ // <folder-summary> 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 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/newmailalert.css" type="text/css"?>
+
+<html
+ id="newMailAlertNotification"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ windowtype="alert:alert"
+ scrolling="false"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/newmailalert.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ role="alert"
+ >
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <hbox id="alertContainer" align="start">
+ <hbox id="alertImageBox" align="center" pack="center">
+ <html:img
+ id="alertImage"
+ alt=""
+ src="chrome://branding/content/icon64.png"
+ srcset="chrome://branding/content/icon128.png 2x"
+ />
+ </hbox>
+
+ <vbox id="alertTextBox">
+ <label id="alertTitle" />
+ <separator id="alertSeparator" />
+ <folder-summary id="folderSummaryInfo" />
+ </vbox>
+
+ <toolbarbutton
+ id="closeButton"
+ class="close-icon"
+ onclick="closeAlert();"
+ />
+ </hbox>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html [ <!ENTITY % htmlDTD PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "DTD/xhtml1-strict.dtd">
+%htmlDTD;
+<!ENTITY % netErrorDTD
+ SYSTEM "chrome://messenger/locale/newsError.dtd">
+%netErrorDTD; ]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:" />
+ <title>&newsError.title;</title>
+ <link
+ rel="stylesheet"
+ href="chrome://global/skin/aboutNetError.css"
+ type="text/css"
+ media="all"
+ />
+ <!-- If the location of the favicon is changed here, the FAVICON_ERRORPAGE_URL symbol in
+ toolkit/components/places/src/nsFaviconService.h should be updated. -->
+ <link
+ rel="icon"
+ type="image/png"
+ id="favicon"
+ href="chrome://global/skin/icons/warning.svg"
+ />
+ </head>
+ <body>
+ <div id="errorPageContainer">
+ <div id="errorTitle">
+ <h1 id="errorTitleText">&articleNotFound.title;</h1>
+ </div>
+ <div id="errorLongContent">
+ <div id="errorShortDesc">
+ <p id="errorShortDescText"><b>&articleNotFound.desc;</b></p>
+ </div>
+ <div id="errorLongDesc">
+ <ul>
+ <li>&serverResponded.title; <span id="ngResp" /></li>
+ <li>&articleExpired.title;</li>
+ <li id="messageIdDesc">
+ &trySearching.title; &lt;<span id="msgId" />&gt; (<span
+ id="msgKey"
+ />)
+ </li>
+ </ul>
+ </div>
+ </div>
+ <!-- This button really means "remove all expired articles", but we use
+ the "errorTryAgain" id to piggyback on toolkit's CSS. -->
+ <button id="errorTryAgain">&removeExpiredArticles.title;</button>
+ </div>
+ <script src="chrome://messenger/content/newsError.js" />
+ </body>
+</html>
diff --git a/comm/mailnews/base/content/renameFolderDialog.js b/comm/mailnews/base/content/renameFolderDialog.js
new file mode 100644
index 0000000000..8525e0ba93
--- /dev/null
+++ b/comm/mailnews/base/content/renameFolderDialog.js
@@ -0,0 +1,43 @@
+/* -*- 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/. */
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onOK);
+
+var dialog;
+function onLoad() {
+ var windowArgs = window.arguments[0];
+
+ dialog = {};
+
+ dialog.OKButton = document.querySelector("dialog").getButton("accept");
+
+ dialog.nameField = document.getElementById("name");
+ dialog.nameField.value = windowArgs.name;
+ dialog.nameField.select();
+ 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.preselectedFolderURI = windowArgs.preselectedURI;
+
+ doEnabling();
+}
+
+function onOK() {
+ dialog.okCallback(dialog.nameField.value, dialog.preselectedFolderURI);
+}
+
+function doEnabling() {
+ if (dialog.nameField.value) {
+ if (dialog.OKButton.disabled) {
+ dialog.OKButton.disabled = false;
+ }
+ } else if (!dialog.OKButton.disabled) {
+ dialog.OKButton.disabled = true;
+ }
+}
diff --git a/comm/mailnews/base/content/renameFolderDialog.xhtml b/comm/mailnews/base/content/renameFolderDialog.xhtml
new file mode 100644
index 0000000000..f81ba88db0
--- /dev/null
+++ b/comm/mailnews/base/content/renameFolderDialog.xhtml
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<!-- -*- Mode: xml; 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/renameFolderDialog.dtd">
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ scrolling="false"
+>
+ <head>
+ <title>&renameFolderDialog.title;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/renameFolderDialog.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttonlabelaccept="&accept.label;"
+ buttonaccesskeyaccept="&accept.accesskey;"
+ >
+ <html:label
+ id="nameLabel"
+ for="name"
+ style="display: inline-block"
+ accesskey="&rename.accesskey;"
+ >&rename.label;</html:label
+ >
+ <html:input
+ id="name"
+ type="text"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ oninput="doEnabling();"
+ />
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!--
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ width="600"
+ height="150"
+ scrolling="false"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/shutdownWindow.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="cancel">
+ <stringbundle
+ id="bundle_shutdown"
+ src="chrome://messenger/locale/shutdownWindow.properties"
+ />
+
+ <vbox align="center">
+ <label id="shutdownStatus_label" value="" />
+ <separator class="thin" />
+ </vbox>
+
+ <html:progress id="shutdown_progressmeter" max="100" />
+
+ <vbox align="center">
+ <label id="shutdownTask_label" value="" />
+ <separator class="thick" />
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/subscribe.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/searchBox.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/subscribe.dtd">
+
+<html
+ id="subscribeWindow"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ persist="width height screenX screenY"
+ lightweightthemes="true"
+ windowtype="mailnews:subscribe"
+ scrolling="false"
+>
+ <head>
+ <title>&subscribeDialog.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/mailWindow.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/subscribe.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog>
+ <stringbundle
+ id="bundle_subscribe"
+ src="chrome://messenger/locale/subscribe.properties"
+ />
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <vbox flex="1">
+ <hbox>
+ <vbox>
+ <hbox pack="end" align="center" flex="1">
+ <label
+ value="&server.label;"
+ accesskey="&server.accesskey;"
+ control="serverMenu"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="namefieldlabel"
+ accesskey="&namefield.accesskey;"
+ value="&namefield.label;"
+ control="namefield"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="serverMenu" flex="1" class="folderMenuItem">
+ <menupopup
+ is="folder-menupopup"
+ mode="subscribe"
+ expandFolders="false"
+ oncommand="onServerClick(event.target._folder);"
+ />
+ </menulist>
+ <search-textbox
+ id="namefield"
+ class="themeableSearchBox"
+ flex="1"
+ timeout="300"
+ aria-controls="subscribeTree"
+ oncommand="Search();"
+ />
+ </vbox>
+ </hbox>
+ <spacer />
+ <separator class="thin" />
+ <spacer />
+ <vbox flex="1">
+ <tabbox flex="1" handleCtrlTab="false">
+ <tabs id="subscribeTabs">
+ <tab
+ id="currentListTab"
+ selected="true"
+ onclick="if (!event.target.disabled) ShowCurrentList()"
+ oncommand="ShowCurrentList()"
+ />
+ <tab
+ id="newGroupsTab"
+ label="&newGroupsTab.label;"
+ accesskey="&newGroupsTab.accesskey;"
+ onclick="if (!event.target.disabled) ShowNewGroupsList()"
+ oncommand="ShowNewGroupsList()"
+ />
+ </tabs>
+ <tabpanels flex="1">
+ <tabpanel id="treepanel" flex="1" orient="vertical">
+ <label id="subscribeLabel" />
+ <hbox flex="1">
+ <hbox id="subscribeView" flex="1">
+ <tree
+ id="subscribeTree"
+ treelines="true"
+ flex="1"
+ hidecolumnpicker="true"
+ seltype="multiple"
+ disableKeyNavigation="true"
+ onkeypress="onSubscribeTreeKeyPress(event);"
+ onclick="SubscribeOnClick(event);"
+ >
+ <treecols id="theSubscribeColumns">
+ <treecol
+ id="nameColumn"
+ primary="true"
+ hideheader="true"
+ crop="center"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="subscribedColumn"
+ type="checkbox"
+ style="flex: 1 auto"
+ hideheader="true"
+ />
+ </treecols>
+ <treechildren id="subscribeTreeBody" />
+ </tree>
+ </hbox>
+ <hbox id="searchView" flex="1" hidden="true">
+ <tree
+ id="searchTree"
+ flex="1"
+ disableKeyNavigation="true"
+ hidecolumnpicker="true"
+ onkeypress="onSearchTreeKeyPress(event);"
+ onclick="SearchOnClick(event);"
+ >
+ <treecols>
+ <treecol
+ id="nameColumn2"
+ primary="true"
+ hideheader="true"
+ sortDirection="ascending"
+ />
+ <splitter class="tree-splitter" />
+ <treecol
+ id="subscribedColumn2"
+ style="flex: 1 auto"
+ hideheader="true"
+ />
+ </treecols>
+ <treechildren id="searchTreeBody" />
+ </tree>
+ </hbox>
+ <vbox>
+ <button
+ id="subscribe"
+ label="&subscribeButton.label;"
+ accesskey="&subscribeButton.accesskey;"
+ oncommand="SetSubscribeState(true)"
+ />
+ <button
+ id="unsubscribe"
+ label="&unsubscribeButton.label;"
+ accesskey="&unsubscribeButton.accesskey;"
+ oncommand="SetSubscribeState(false)"
+ />
+ <button
+ id="refreshButton"
+ label="&refreshButton.label;"
+ accesskey="&refreshButton.accesskey;"
+ oncommand="Refresh()"
+ />
+ <button
+ id="stopButton"
+ label="&stopButton.label;"
+ accesskey="&stopButton.accesskey;"
+ oncommand="Stop()"
+ disabled="true"
+ />
+ <spacer flex="1" />
+ </vbox>
+ </hbox>
+ </tabpanel>
+ </tabpanels>
+ </tabbox>
+ </vbox>
+ <hbox>
+ <hbox id="statusContainerBox" flex="1">
+ <label id="statusText" class="statusbarpanel" crop="end" flex="1" />
+ <hbox
+ id="statusbar-progresspanel"
+ class="statusbarpanel statusbarpanel-progress"
+ collapsed="true"
+ pack="end"
+ flex="1"
+ >
+ <html:progress
+ class="progressmeter-statusbar"
+ id="statusbar-icon"
+ value="0"
+ max="100"
+ />
+ </hbox>
+ </hbox>
+ <hbox />
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/mailWindow1.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/virtualFolderListDialog.dtd">
+
+<html
+ id="virtualFolderList"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ persist="width height screenX screenY"
+ width="400"
+ height="370"
+ windowtype="mailnews:virtualFolderList"
+ scrolling="false"
+>
+ <head>
+ <title>&virtualFolderListTitle.title;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/dialogShadowDom.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/jsTreeView.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/virtualFolderListEdit.js"
+ ></script>
+ <style id="folderColorsStyle"></style>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog>
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <label control="folderPickerTree">&virtualFolderDesc.label;</label>
+ <tree
+ id="folderPickerTree"
+ flex="1"
+ style="height: 290px"
+ hidecolumnpicker="true"
+ seltype="multiple"
+ disableKeyNavigation="true"
+ simplelist="true"
+ mode="virtual"
+ onkeypress="gSelectVirtual.onKeyPress(event);"
+ onclick="gSelectVirtual.onClick(event);"
+ >
+ <treecols>
+ <treecol
+ id="folderNameCol"
+ label="&folderName.label;"
+ style="flex: 10 10 auto"
+ primary="true"
+ crop="center"
+ />
+ <treecol
+ id="selectedCol"
+ label="&folderSearch.label;"
+ style="flex: 1 auto"
+ cycler="true"
+ />
+ </treecols>
+ <treechildren />
+ </tree>
+ </dialog>
+ </html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ -->
+
+<?xml-stylesheet href="chrome://messenger/skin/searchDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/contextMenu.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderColors.css" type="text/css"?>
+
+<!DOCTYPE html [
+ <!ENTITY % folderDTD SYSTEM "chrome://messenger/locale/virtualFolderProperties.dtd">
+ %folderDTD;
+ <!ENTITY % folderPropsDTD SYSTEM "chrome://messenger/locale/folderProps.dtd">
+ %folderPropsDTD;
+ <!ENTITY % searchTermDTD SYSTEM "chrome://messenger/locale/searchTermOverlay.dtd">
+ %searchTermDTD;
+]>
+<html id="virtualFolderProperties" xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ lightweightthemes="true"
+ windowtype="mailnews:virtualFolderProperties"
+ style="min-width: 60em; min-height: 30em;"
+ scrolling="false">
+<head>
+ <title>&virtualFolderProperties.title;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchWidgets.js"></script>
+ <script defer="defer" src="chrome://messenger/content/mailCommands.js"></script>
+ <script defer="defer" src="chrome://messenger/content/searchTerm.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dateFormat.js"></script>
+ <script defer="defer" src="chrome://messenger/content/dialogShadowDom.js"></script>
+ <script defer="defer" src="chrome://messenger/content/virtualFolderProperties.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog id="virtualFolderPropertiesDialog"
+ buttons="accept,cancel"
+ newFolderAcceptButtonLabel="&newFolderButton.label;"
+ newFolderAcceptButtonAccessKey="&newFolderButton.accesskey;"
+ editFolderAcceptButtonLabel="&editFolderButton.label;"
+ editFolderAcceptButtonAccessKey="&editFolderButton.accesskey;"
+ style="min-width: 60em; min-height: 30em;">
+
+ <html:div class="grid-two-column-fr grid-items-center">
+ <label id="nameLabel" value="&name.label;" accesskey="&name.accesskey;"
+ control="name"/>
+ <hbox class="input-container">
+ <html:input id="name"
+ hidden="hidden"
+ class="input-inline"
+ aria-labelledby="nameLabel"
+ oninput="doEnabling();"/>
+ <html:input id="existingName"
+ readonly="readonly"
+ hidden="hidden"
+ class="input-inline"
+ tabindex="0"/>
+ <hbox id="iconColorContainer" align="center" collapsed="true">
+ <label id="colorLabel" value="&folderProps.color.label;" control="color"
+ accesskey="&folderProps.color.accesskey;"/>
+ <html:input id="color"
+ type="color"
+ value=""
+ class="input-inline-color"
+ aria-labelledby="colorLabel"/>
+ <button id="resetColor"
+ tooltiptext="&folderProps.reset.tooltip;"
+ class="toolbarbutton-1 btn-flat btn-reset"/>
+ </hbox>
+ </hbox>
+ <label class="chooseFolderLocation" value="&description.label;"
+ accesskey="&description.accesskey;" control="msgNewFolderPicker"/>
+ <menulist id="msgNewFolderPicker" class="folderMenuItem chooseFolderLocation"
+ align="center" displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgNewFolderPopup" class="menulist-menupopup"
+ mode="newFolder" showFileHereLabel="true" oncommand="onFolderPick(event);"/>
+ </menulist>
+ <label value="&folderSelectionCaption.label;"/>
+ <hbox align="center">
+ <label id="chosenFoldersCount"/>
+ <spacer flex="1"/>
+ <button id="folderListPicker" label="&chooseFoldersButton.label;"
+ accesskey="&chooseFoldersButton.accesskey;"
+ oncommand="chooseFoldersToSearch();"/>
+ </hbox>
+ </html:div>
+
+ <checkbox id="searchOnline" label="&searchOnline.label;"
+ accesskey="&searchOnline.accesskey;"/>
+
+ <separator class="thin"/>
+
+ <vbox id="virtualFolderSearchTerms">
+ <label value="&searchTermCaption.label;"/>
+ <hbox id="virtualFolderSearchTermListBoxWrapper">
+ <vbox id="virtualFolderSearchTermListBox">
+#include ../../search/content/searchTerm.inc.xhtml
+ </hbox>
+ </vbox>
+</dialog>
+</html:body>
+</html>
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 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!DOCTYPE html [
+ <!ENTITY % accountManagerDTD SYSTEM "chrome://messenger/locale/AccountManager.dtd">
+ %accountManagerDTD;
+ <!ENTITY % utilityDTD SYSTEM "chrome://communicator/locale/utilityOverlay.dtd">
+ %utilityDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false">
+<head>
+ <title>&accountManagerTitle.label;</title>
+
+ <meta http-equiv="Content-Security-Policy" content="default-src chrome:; script-src chrome: 'unsafe-inline'; img-src chrome: moz-icon: https: data:; style-src chrome: data: 'unsafe-inline'; object-src 'none'" />
+ <meta name="color-scheme" content="light dark" />
+
+ <link rel="icon" href="chrome://messenger/skin/icons/new/compact/account-settings.svg" />
+
+ <link rel="stylesheet" href="chrome://messenger/skin/inContentDialog.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/icons.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/folderPane.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/shared/tree-listbox.css" />
+ <link rel="stylesheet" href="chrome://messenger/skin/shared/accountManager.css" />
+
+ <link rel="localization" href="branding/brand.ftl" />
+ <link rel="localization" href="messenger/accountManager.ftl" />
+ <link rel="localization" href="messenger/addressbook/aboutAddressBook.ftl" />
+
+ <script defer="defer" src="chrome://messenger/content/mailCore.js"></script>
+ <script defer="defer" src="chrome://communicator/content/utilityOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/accountUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/AccountManager.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/tree-listbox.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => { onLoad(event); });
+ window.addEventListener("unload", event => { onUnload(); });
+ </script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <stringbundle id="bundle_brand" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/>
+ <html:aside id="accountTreeBox">
+ <html:ol is="orderable-tree-listbox" id="accounttree" class="no-overscroll"
+ role="tree"
+ flex="1"
+ onselect="onAccountTreeSelect(null, null);">
+ </html:ol>
+ <template id="accountTreeItem" xmlns="http://www.w3.org/1999/xhtml">
+ <li>
+ <div draggable="true">
+ <div class="twisty">
+ <img class="twisty-icon" alt=""
+ src="chrome://global/skin/icons/arrow-down-12.svg" />
+ </div>
+ <div class="icon"></div>
+ <span class="name" tabindex="-1"></span>
+ </div>
+ <ul></ul>
+ </li>
+ </template>
+
+ <button id="accountActionsButton" type="menu"
+ label="&accountActionsButton.label;"
+ accesskey="&accountActionsButton.accesskey;">
+ <menupopup id="accountActionsDropdown"
+ onpopupshowing="initAccountActionsButtons(this);">
+ <menuitem id="accountActionsAddMailAccount"
+ label="&addMailAccountButton.label;"
+ accesskey="&addMailAccountButton.accesskey;"
+ prefstring="mail.disable_new_account_addition"
+ oncommand="openAccountSetupTab();"/>
+ <menuitem id="accountActionsAddIMAccount"
+ label="&addIMAccountButton.label;"
+ accesskey="&addIMAccountButton.accesskey;"
+ prefstring="mail.disable_new_account_addition"
+ oncommand="AddIMAccount(event); event.stopPropagation();"/>
+ <menuitem id="accountActionsAddFeedAccount"
+ label="&addFeedAccountButton.label;"
+ accesskey="&addFeedAccountButton.accesskey;"
+ prefstring="mail.disable_new_account_addition"
+ oncommand="AddFeedAccount(event); event.stopPropagation();"/>
+ <menuitem id="accountActionsAddOtherAccount"
+ data-l10n-id="account-action-add-newsgroup-account"
+ prefstring="mail.disable_new_account_addition"
+ oncommand="openNewsgroupAccountWizard(); event.stopPropagation();"/>
+ <menuseparator id="accountActionsDropdownSep1"/>
+ <menuitem id="accountActionsDropdownSetDefault"
+ label="&setDefaultButton.label;"
+ accesskey="&setDefaultButton.accesskey;"
+ prefstring="mail.disable_button.set_default_account"
+ oncommand="onSetDefault(event); event.stopPropagation();"/>
+ <menuitem id="accountActionsDropdownRemove"
+ label="&removeButton.label;"
+ accesskey="&removeButton.accesskey;"
+ prefstring="mail.disable_button.delete_account"
+ oncommand="onRemoveAccount(event); event.stopPropagation();"/>
+ </menupopup>
+ </button>
+
+ <vbox class="sidebar-footer-list">
+ <html:a id="prefsButton" class="sidebar-footer-link"
+ onclick="openOptionsDialog();">
+ <html:img class="sidebar-footer-icon"
+ src="chrome://messenger/skin/icons/new/compact/settings.svg" alt="" />
+ <label class="sidebar-footer-label"
+ data-l10n-id="open-preferences-sidebar-button2"
+ flex="1"/>
+ </html:a>
+
+ <html:a id="addonsButton" class="sidebar-footer-link"
+ onclick="window.browsingContext.topChromeWindow.openAddonsMgr();">
+ <html:img class="sidebar-footer-icon"
+ src="chrome://messenger/skin/icons/new/compact/extension.svg"
+ alt="" />
+ <label class="sidebar-footer-label"
+ data-l10n-id="open-addons-sidebar-button"
+ flex="1"/>
+ </html:a>
+ </vbox>
+ </html:aside>
+ <html:aside>
+ <iframe id="contentFrame" name="contentFrame" />
+ </html:aside>
+
+ <stack id="dialogStack" hidden="true"/>
+ <vbox id="dialogTemplate"
+ class="dialogOverlay"
+ align="center"
+ pack="center"
+ topmost="true"
+ hidden="true">
+ <vbox class="dialogBox"
+ pack="end"
+ role="dialog"
+ aria-labelledby="dialogTitle">
+ <hbox class="dialogTitleBar" align="center">
+ <label class="dialogTitle" flex="1"/>
+ <button class="dialogClose close-icon"
+ aria-label="&accountManagerCloseButton.label;"/>
+ </hbox>
+ <browser class="dialogFrame"
+ autoscroll="false"
+ disablehistory="true"/>
+ </vbox>
+ </vbox>
+
+ <dialog xmlns="http://www.w3.org/1999/xhtml" id="editVCardDialog">
+ <form autocomplete="off">
+#include ../../../../mail/components/addrbook/content/vcard-edit/vCardTemplates.inc.xhtml
+ <vcard-edit class="edit"/>
+ <menu class="dialog-menu-container">
+ <button type="reset" class="cancel"
+ data-l10n-id="edit-vcard-dialog-cancel-button"
+ data-l10n-attrs="accesskey"></button>
+ <button type="submit" class="accept primary"
+ data-l10n-id="edit-vcard-dialog-accept-button"
+ data-l10n-attrs="accesskey"></button>
+ </menu>
+ </form>
+ </dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/AccountWizard.js b/comm/mailnews/base/prefs/content/AccountWizard.js
new file mode 100644
index 0000000000..69047c3f9e
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/AccountWizard.js
@@ -0,0 +1,605 @@
+/* -*- Mode: JavaScript; 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/. */
+
+/* import-globals-from accountUtils.js */
+/* import-globals-from amUtils.js */
+/* import-globals-from aw-identity.js */
+/* import-globals-from aw-incoming.js */
+/* import-globals-from aw-accname.js */
+/* import-globals-from aw-done.js */
+
+/* NOTE: This Account Wizard is *only* for Newsgroup accounts.
+ * Historically, it was a generic Account Wizard, hence the generic naming.
+ */
+
+/*
+ data flow into the account wizard like this:
+
+ For new accounts:
+ * pageData -> accountData -> createAccount -> finishAccount
+
+ for "unfinished accounts"
+ * account -> accountData -> pageData -> accountData -> finishAccount
+*/
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { NntpUtils } = ChromeUtils.import("resource:///modules/NntpUtils.jsm");
+
+var contentWindow;
+
+var gPageData;
+
+var nsIMsgIdentity = Ci.nsIMsgIdentity;
+var nsIMsgIncomingServer = Ci.nsIMsgIncomingServer;
+var gPrefsBundle, gMessengerBundle;
+
+// the current nsIMsgAccount
+var gCurrentAccount;
+
+// The default account before we create a new account.
+// We need to store this as just asking for the default account may switch
+// it to the newly created one if there was none before.
+var gDefaultAccount;
+
+// the current associative array that
+// will eventually be dumped into the account
+var gCurrentAccountData = null;
+
+// default picker mode for copies and folders
+var gDefaultSpecialFolderPickerMode = "0";
+
+// event handlers
+function onAccountWizardLoad() {
+ document.querySelector("wizard").addEventListener("wizardcancel", onCancel);
+ document
+ .querySelector("wizard")
+ .addEventListener("wizardfinish", FinishAccount);
+ let identityPage = document.getElementById("identitypage");
+ identityPage.addEventListener("pageshow", identityPageInit);
+ identityPage.addEventListener("pageadvanced", identityPageUnload);
+ identityPage.next = "newsserver";
+ let newsserverPage = document.getElementById("newsserver");
+ newsserverPage.addEventListener("pageshow", incomingPageInit);
+ newsserverPage.addEventListener("pageadvanced", incomingPageUnload);
+ newsserverPage.next = "accnamepage";
+ let accnamePage = document.getElementById("accnamepage");
+ accnamePage.addEventListener("pageshow", acctNamePageInit);
+ accnamePage.addEventListener("pageadvanced", acctNamePageUnload);
+ accnamePage.next = "done";
+ let donePage = document.getElementById("done");
+ donePage.addEventListener("pageshow", donePageInit);
+
+ gPrefsBundle = document.getElementById("bundle_prefs");
+ gMessengerBundle = document.getElementById("bundle_messenger");
+
+ checkForInvalidAccounts();
+
+ // It is fine if there is no default account, this is expected the first
+ // time you launch mail on a new profile.
+ gDefaultAccount = MailServices.accounts.defaultAccount;
+
+ identityPageInit();
+}
+
+function onCancel() {
+ if ("ActivationOnCancel" in this && this.ActivationOnCancel()) {
+ return false;
+ }
+ var firstInvalidAccount = getInvalidAccounts(
+ MailServices.accounts.accounts
+ ).find(account => account.incomingServer.type == "nntp");
+ var closeWizard = true;
+
+ // if the user cancels the the wizard when it pops up because of
+ // an invalid account (example, a webmail account that activation started)
+ // we just force create it by setting some values and calling the FinishAccount()
+ // see bug #47521 for the full discussion
+ if (firstInvalidAccount) {
+ var pageData = GetPageData();
+ // set the fullName if it doesn't exist
+ if (!pageData.fullName) {
+ pageData.fullName = "";
+ }
+
+ // set the email if it doesn't exist
+ if (!pageData.email) {
+ pageData.email = "user@domain.invalid";
+ }
+
+ // call FinishAccount() and not onFinish(), since the "finish"
+ // button may be disabled
+ FinishAccount();
+ } else if (!MailServices.accounts.accounts.length) {
+ // since this is not an invalid account
+ // really cancel if the user hits the "cancel" button
+ // if the length of the account list is less than 1, there are no accounts
+ let confirmMsg = gPrefsBundle.getString("cancelWizard");
+ let confirmTitle = gPrefsBundle.getString("accountWizard");
+ let result = Services.prompt.confirmEx(
+ window,
+ confirmTitle,
+ confirmMsg,
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1,
+ gPrefsBundle.getString("WizardExit"),
+ gPrefsBundle.getString("WizardContinue"),
+ null,
+ null,
+ { value: 0 }
+ );
+
+ if (result == 1) {
+ closeWizard = false;
+ }
+ }
+
+ return closeWizard;
+}
+
+function FinishAccount() {
+ try {
+ var pageData = GetPageData();
+
+ var accountData = gCurrentAccountData;
+
+ if (!accountData) {
+ accountData = {};
+ }
+
+ // we may need local folders before account is "Finished"
+ // if it's a pop3 account which defers to Local Folders.
+ verifyLocalFoldersAccount();
+
+ PageDataToAccountData(pageData, accountData);
+
+ // we might be simply finishing another account
+ if (!gCurrentAccount) {
+ gCurrentAccount = createAccount(accountData);
+ }
+
+ // transfer all attributes from the accountdata
+ finishAccount(gCurrentAccount, accountData);
+
+ setupCopiesAndFoldersServer(gCurrentAccount, accountData);
+
+ if (gCurrentAccount.incomingServer.canBeDefaultServer) {
+ EnableCheckMailAtStartUpIfNeeded(gCurrentAccount);
+ }
+
+ // in case we crash, force us a save of the prefs file NOW
+ try {
+ MailServices.accounts.saveAccountInfo();
+ } catch (ex) {
+ dump("Error saving account info: " + ex + "\n");
+ }
+ let openerWindow = window.opener.top;
+ // The following block is the same as in feedAccountWizard.js.
+ if ("selectServer" in openerWindow) {
+ // Opened from Account Settings.
+ openerWindow.selectServer(gCurrentAccount.incomingServer);
+ }
+
+ // Post a message to the main window on successful account setup.
+ openerWindow.postMessage("account-created", "*");
+
+ window.close();
+ } catch (ex) {
+ dump("FinishAccount failed, " + ex + "\n");
+ }
+}
+
+// prepopulate pageData with stuff from accountData
+// use: to prepopulate the wizard with account information
+function AccountDataToPageData(accountData, pageData) {
+ var server = accountData.incomingServer;
+ pageData.hostname = server.hostName;
+ pageData.prettyName = server.prettyName || "";
+
+ var identity;
+
+ if (accountData.identity) {
+ dump("This is an accountdata\n");
+ identity = accountData.identity;
+ } else if (accountData.identities) {
+ identity = accountData.identities[0];
+ dump("this is an account, id= " + identity + "\n");
+ }
+
+ pageData.email = identity.email || "";
+ pageData.fullName = identity.fullName || "";
+}
+
+// take data from each page of pageData and dump it into accountData
+// use: to put results of wizard into a account-oriented object
+function PageDataToAccountData(pageData, accountData) {
+ if (!accountData.identity) {
+ accountData.identity = {};
+ }
+ if (!accountData.incomingServer) {
+ accountData.incomingServer = {};
+ }
+
+ var identity = accountData.identity;
+ var server = accountData.incomingServer;
+
+ if (pageData.email) {
+ identity.email = pageData.email;
+ }
+ if (pageData.fullName) {
+ identity.fullName = pageData.fullName;
+ }
+
+ server.hostName = pageData.hostname;
+ if (pageData.prettyName) {
+ server.prettyName = pageData.prettyName;
+ }
+}
+
+// given an accountData structure, create an account
+// (but don't fill in any fields, that's for finishAccount()
+function createAccount(accountData) {
+ let hostName = accountData.incomingServer.hostName;
+ // If we're here, the server must not be associated with any account, so reuse
+ // it.
+ let server = NntpUtils.findServer(hostName);
+
+ if (!server) {
+ dump(`MailServices.accounts.createIncomingServer(${hostName})\n`);
+ // Create a (actual) server.
+ server = MailServices.accounts.createIncomingServer(null, hostName, "nntp");
+ }
+
+ dump("MailServices.accounts.createAccount()\n");
+ // Create an account.
+ let account = MailServices.accounts.createAccount();
+
+ // only create an identity for this account if we really have one
+ // (use the email address as a check)
+ if (accountData.identity && accountData.identity.email) {
+ dump("MailServices.accounts.createIdentity()\n");
+ // Create an identity.
+ let identity = MailServices.accounts.createIdentity();
+
+ // New nntp identities should use plain text by default;
+ // we want that GNKSA (The Good Net-Keeping Seal of Approval).
+ identity.composeHtml = false;
+
+ account.addIdentity(identity);
+ }
+
+ // we mark the server as invalid so that the account manager won't
+ // tell RDF about the new server - it's not quite finished getting
+ // set up yet, in particular, the deferred storage pref hasn't been set.
+ server.valid = false;
+ // Set the new account to use the new server.
+ account.incomingServer = server;
+ server.valid = true;
+ return account;
+}
+
+// given an accountData structure, copy the data into the
+// given account, incoming server, and so forth
+function finishAccount(account, accountData) {
+ if (accountData.incomingServer) {
+ var destServer = account.incomingServer;
+ var srcServer = accountData.incomingServer;
+ copyObjectToInterface(destServer, srcServer, true);
+
+ // See if there are any protocol-specific attributes.
+ // If so, we use the type to get the IID, QueryInterface
+ // as appropriate, then copy the data over.
+ const typeProperty = "ServerType-" + srcServer.type;
+ let serverAttrs =
+ typeProperty in srcServer ? srcServer[typeProperty] : null;
+ dump(`srcServer.${typeProperty} = ${serverAttrs}\n`);
+ if (serverAttrs) {
+ // handle server-specific stuff
+ var IID;
+ try {
+ IID = destServer.protocolInfo.serverIID;
+ } catch (ex) {
+ console.error("Could not get IID for " + srcServer.type + ": " + ex);
+ }
+
+ if (IID) {
+ let destProtocolServer = destServer.QueryInterface(IID);
+ let srcProtocolServer = srcServer["ServerType-" + srcServer.type];
+
+ dump("Copying over " + srcServer.type + "-specific data\n");
+ copyObjectToInterface(destProtocolServer, srcProtocolServer, false);
+ }
+ }
+
+ account.incomingServer.valid = true;
+ // hack to cause an account loaded notification now the server is valid
+ account.incomingServer = account.incomingServer; // eslint-disable-line no-self-assign
+ }
+
+ // copy identity info
+ var destIdentity = account.identities.length ? account.identities[0] : null;
+
+ if (destIdentity) {
+ // does this account have an identity?
+ if (accountData.identity && accountData.identity.email) {
+ // fixup the email address if we have a default domain
+ let emailArray = accountData.identity.email.split("@");
+ if (emailArray.length < 2 && accountData.domain) {
+ accountData.identity.email += "@" + accountData.domain;
+ }
+
+ copyObjectToInterface(destIdentity, accountData.identity, true);
+ destIdentity.valid = true;
+ }
+
+ /**
+ * If signature file need to be set, get the path to the signature file.
+ * Signature files, if exist, are placed under default location. Get
+ * default files location for messenger using directory service. Signature
+ * file name should be extracted from the account data to build the complete
+ * path for signature file. Once the path is built, set the identity's signature pref.
+ */
+ if (destIdentity.attachSignature) {
+ var sigFileName = accountData.signatureFileName;
+ let sigFile = MailServices.mailSession.getDataFilesDir("messenger");
+ sigFile.append(sigFileName);
+ destIdentity.signature = sigFile;
+ }
+ } // if the account has an identity...
+
+ if (this.FinishAccountHook != undefined) {
+ this.FinishAccountHook(accountData.domain);
+ }
+}
+
+// Helper method used by copyObjectToInterface which attempts to set dest[attribute] as a generic
+// attribute on the xpconnect object, src.
+// This routine skips any attribute that begins with ServerType-
+function setGenericAttribute(dest, src, attribute) {
+ if (!attribute.toLowerCase().startsWith("servertype-") && src[attribute]) {
+ switch (typeof src[attribute]) {
+ case "string":
+ dest.setUnicharAttribute(attribute, src[attribute]);
+ break;
+ case "boolean":
+ dest.setBoolAttribute(attribute, src[attribute]);
+ break;
+ case "number":
+ dest.setIntAttribute(attribute, src[attribute]);
+ break;
+ default:
+ dump(
+ "Error: No Generic attribute " +
+ attribute +
+ " found for: " +
+ dest +
+ "\n"
+ );
+ break;
+ }
+ }
+}
+
+// copy over all attributes from dest into src that already exist in src
+// the assumption is that src is an XPConnect interface full of attributes
+// @param useGenericFallback if we can't set an attribute directly on src, then fall back
+// and try setting it generically. This assumes that src supports setIntAttribute, setUnicharAttribute
+// and setBoolAttribute.
+function copyObjectToInterface(dest, src, useGenericFallback) {
+ if (!dest) {
+ return;
+ }
+ if (!src) {
+ return;
+ }
+
+ var attribute;
+ for (attribute in src) {
+ if (dest.__lookupSetter__(attribute)) {
+ if (dest[attribute] != src[attribute]) {
+ dest[attribute] = src[attribute];
+ }
+ } else if (useGenericFallback) {
+ // fall back to setting the attribute generically
+ setGenericAttribute(dest, src, attribute);
+ }
+ } // for each attribute in src we want to copy
+}
+
+// check if there already is a "Local Folders"
+// if not, create it.
+function verifyLocalFoldersAccount() {
+ var localMailServer = null;
+ try {
+ localMailServer = MailServices.accounts.localFoldersServer;
+ } catch (ex) {
+ // dump("exception in findserver: " + ex + "\n");
+ localMailServer = null;
+ }
+
+ try {
+ if (!localMailServer) {
+ // dump("Creating local mail account\n");
+ // creates a copy of the identity you pass in
+ MailServices.accounts.createLocalMailAccount();
+ try {
+ localMailServer = MailServices.accounts.localFoldersServer;
+ } catch (ex) {
+ dump(
+ "error! we should have found the local mail server after we created it.\n"
+ );
+ localMailServer = null;
+ }
+ }
+ } catch (ex) {
+ dump("Error in verifyLocalFoldersAccount" + ex + "\n");
+ }
+}
+
+function setupCopiesAndFoldersServer(account, accountData) {
+ try {
+ var server = account.incomingServer;
+
+ if (!account.identities.length) {
+ return false;
+ }
+
+ let identity = account.identities[0];
+ // For this server, do we default the folder prefs to this server, or to the "Local Folders" server
+ // If it's deferred, we use the local folders account.
+ var defaultCopiesAndFoldersPrefsToServer =
+ server.defaultCopiesAndFoldersPrefsToServer;
+
+ var copiesAndFoldersServer = null;
+ if (defaultCopiesAndFoldersPrefsToServer) {
+ copiesAndFoldersServer = server;
+ } else {
+ if (!MailServices.accounts.localFoldersServer) {
+ dump("error! we should have a local mail server at this point\n");
+ return false;
+ }
+ copiesAndFoldersServer = MailServices.accounts.localFoldersServer;
+ }
+
+ setDefaultCopiesAndFoldersPrefs(
+ identity,
+ copiesAndFoldersServer,
+ accountData
+ );
+ } catch (ex) {
+ // return false (meaning we did not setupCopiesAndFoldersServer)
+ // on any error
+ dump("Error in setupCopiesAndFoldersServer: " + ex + "\n");
+ return false;
+ }
+ return true;
+}
+
+function setDefaultCopiesAndFoldersPrefs(identity, server, accountData) {
+ var rootFolder = server.rootFolder;
+
+ // we need to do this or it is possible that the server's draft,
+ // stationery fcc folder will not be in rdf
+ //
+ // this can happen in a couple cases
+ // 1) the first account we create, creates the local mail. since
+ // local mail was just created, it obviously hasn't been opened,
+ // or in rdf..
+ // 2) the account we created is of a type where
+ // defaultCopiesAndFoldersPrefsToServer is true
+ // this since we are creating the server, it obviously hasn't been
+ // opened, or in rdf.
+ //
+ // this makes the assumption that the server's draft, stationery fcc folder
+ // are at the top level (ie subfolders of the root folder.) this works
+ // because we happen to be doing things that way, and if the user changes
+ // that, it will work because to change the folder, it must be in rdf,
+ // coming from the folder cache, in the worst case.
+ var msgFolder = rootFolder.QueryInterface(Ci.nsIMsgFolder);
+
+ /**
+ * When a new account is created, folders 'Sent', 'Drafts'
+ * and 'Templates' are not created then, but created on demand at runtime.
+ * But we do need to present them as possible choices in the Copies and Folders
+ * UI. To do that, folder URIs have to be created and stored in the prefs file.
+ * So, if there is a need to build special folders, append the special folder
+ * names and create right URIs.
+ */
+ var folderDelim = "/";
+
+ /* we use internal names known to everyone like Sent, Templates and Drafts */
+ /* if folder names were already given in isp rdf, we use them,
+ otherwise we use internal names known to everyone like Sent, Templates and Drafts */
+
+ // Note the capital F, D and S!
+ var draftFolder =
+ accountData.identity && accountData.identity.DraftFolder
+ ? accountData.identity.DraftFolder
+ : "Drafts";
+ var stationeryFolder =
+ accountData.identity && accountData.identity.StationeryFolder
+ ? accountData.identity.StationeryFolder
+ : "Templates";
+ var fccFolder =
+ accountData.identity && accountData.identity.FccFolder
+ ? accountData.identity.FccFolder
+ : "Sent";
+
+ identity.draftFolder = msgFolder.server.serverURI + folderDelim + draftFolder;
+ identity.stationeryFolder =
+ msgFolder.server.serverURI + folderDelim + stationeryFolder;
+ identity.fccFolder = msgFolder.server.serverURI + folderDelim + fccFolder;
+
+ // Note the capital F, D and S!
+ identity.fccFolderPickerMode =
+ accountData.identity && accountData.identity.FccFolder
+ ? 1
+ : gDefaultSpecialFolderPickerMode;
+ identity.draftsFolderPickerMode =
+ accountData.identity && accountData.identity.DraftFolder
+ ? 1
+ : gDefaultSpecialFolderPickerMode;
+ identity.tmplFolderPickerMode =
+ accountData.identity && accountData.identity.StationeryFolder
+ ? 1
+ : gDefaultSpecialFolderPickerMode;
+}
+
+function checkForInvalidAccounts() {
+ var firstInvalidAccount = getInvalidAccounts(
+ MailServices.accounts.accounts
+ ).find(account => account.incomingServer.type == "nntp");
+
+ if (firstInvalidAccount) {
+ var pageData = GetPageData();
+ dump(
+ "We have an invalid account, " +
+ firstInvalidAccount +
+ ", let's use that!\n"
+ );
+ gCurrentAccount = firstInvalidAccount;
+
+ var accountData = {};
+ accountData.incomingServer = firstInvalidAccount.incomingServer;
+ accountData.identity = firstInvalidAccount.identities[0];
+ AccountDataToPageData(accountData, pageData);
+
+ gCurrentAccountData = accountData;
+ }
+}
+
+function getUsernameFromEmail(email) {
+ return email && email.substr(0, email.indexOf("@"));
+}
+
+function GetPageData() {
+ if (!gPageData) {
+ gPageData = {};
+ }
+
+ return gPageData;
+}
+
+// flush the XUL cache - just for debugging purposes - not called
+function onFlush() {
+ Services.prefs.setBoolPref("nglayout.debug.disable_xul_cache", true);
+ Services.prefs.setBoolPref("nglayout.debug.disable_xul_cache", false);
+}
+
+/** If there are no default accounts..
+ * this is will be the new default, so enable
+ * check for mail at startup
+ */
+function EnableCheckMailAtStartUpIfNeeded(newAccount) {
+ // Check if default account existed.
+ // If no such account, make this one the default account
+ // and turn on the new mail check at startup for the current account
+ if (!gDefaultAccount) {
+ MailServices.accounts.defaultAccount = newAccount;
+ newAccount.incomingServer.loginAtStartUp = true;
+ newAccount.incomingServer.downloadOnBiff = true;
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/AccountWizard.xhtml b/comm/mailnews/base/prefs/content/AccountWizard.xhtml
new file mode 100644
index 0000000000..b58db60f60
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/AccountWizard.xhtml
@@ -0,0 +1,161 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<!-- NOTE: This Account Wizard is *only* for Newsgroup accounts. Historically,
+ - it was a generic Account Wizard, hence the generic naming. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountWizard.css" type="text/css"?>
+<?xml-stylesheet type="text/css" href="chrome://messenger/skin/input-fields.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % AccountWizardDTD SYSTEM "chrome://messenger/locale/AccountWizard.dtd">
+%AccountWizardDTD;
+<!ENTITY % newsblogDTD SYSTEM "chrome://messenger-newsblog/locale/am-newsblog.dtd">
+%newsblogDTD;
+]>
+
+<window id="AccountWizard" title="&windowTitle.label;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ onload="onAccountWizardLoad();"
+ style="width: 40em; height: 38em;"
+ lightweightthemes="true">
+ <stringbundle id="bundle_prefs" src="chrome://messenger/locale/prefs.properties"/>
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+ <linkset>
+ <html:link rel="localization" href="toolkit/global/wizard.ftl"/>
+ </linkset>
+
+ <script src="chrome://messenger/content/globalOverlay.js"/>
+ <script src="chrome://global/content/editMenuOverlay.js"/>
+ <script src="chrome://messenger/content/accountUtils.js"/>
+ <script src="chrome://messenger/content/amUtils.js"/>
+ <script src="chrome://messenger/content/AccountWizard.js"/>
+ <script src="chrome://messenger/content/aw-identity.js"/>
+ <script src="chrome://messenger/content/aw-incoming.js"/>
+ <script src="chrome://messenger/content/aw-accname.js"/>
+ <script src="chrome://messenger/content/aw-done.js"/>
+
+ <wizard>
+ <!-- Identity page : Collects user's full name and email address -->
+ <wizardpage id="identitypage" pageid="identitypage"
+ label="&identityTitle.label;">
+ <vbox>
+ <description flex="1">&identityDesc.label;</description>
+ <separator/>
+ <description flex="1">&fullnameDesc.label; &fullnameExample.label;</description>
+ <separator class="thin"/>
+ <hbox align="center" class="input-container">
+ <label id="fullnameLabel" class="awIdentityLabel" value="&fullnameLabel.label;"
+ accesskey="&fullnameLabel.accesskey;" control="fullName"/>
+ <html:input id="fullName"
+ type="text"
+ required="required"
+ wsm_persist="true"
+ name="fullName"
+ class="input-inline"
+ aria-labelledby="fullnameLabel"
+ oninput="identityPageValidate();"/>
+ </hbox>
+ <separator/>
+ <vbox>
+ <hbox>
+ <description id="emailDescText" flex="1"/>
+ </hbox>
+ <hbox align="center">
+ <label id="emailFieldLabel"
+ class="awIdentityLabel"
+ value="&emailLabel.label;"
+ accesskey="&emailLabel.accesskey;"
+ control="email"/>
+ <hbox class="uri-element input-container" align="center" flex="1">
+ <html:input id="email"
+ type="email"
+ required="required"
+ wsm_persist="true"
+ name="email"
+ aria-labelledby="emailFieldLabel"
+ oninput="identityPageValidate();"
+ class="uri-element input-inline"/>
+ </hbox>
+ </hbox>
+ </vbox>
+ </vbox>
+ </wizardpage>
+
+ <!-- News Server page : Collects the News groups server name -->
+ <wizardpage id="newsserver" pageid="newsserver"
+ label="&incomingTitle.label;">
+ <vbox flex="1">
+ <description>&newsServerNameDesc.label;</description>
+ <separator class="thin"/>
+ <hbox align="center" class="input-container">
+ <label id="newsServerLabel" control="newsServer"
+ value="&newsServerLabel.label;"
+ accesskey="&newsServerLabel.accesskey;"
+ style="width: 8em;"/>
+ <html:input id="newsServer"
+ type="text"
+ wsm_persist="true"
+ class="uri-element input-inline"
+ aria-labelledby="newsServerLabel"
+ oninput="incomingPageValidate();"/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <!-- Account name page : User gets a choice to enter a pretty name for the account -->
+ <!-- Defaults : Mail account -> Email address, Newsgroup account -> Newsgroup server name -->
+ <wizardpage id="accnamepage" pageid="accnamepage"
+ label="&accnameTitle.label;">
+ <vbox flex="1">
+ <description>&accnameDesc.label;</description>
+ <separator class="thin"/>
+ <hbox align="center" class="input-container">
+ <label id="prettyNameLabel" class="label"
+ value="&accnameLabel.label;"
+ style="width: 8em;"
+ accesskey="&accnameLabel.accesskey;"
+ control="prettyName"/>
+ <html:input id="prettyName"
+ type="text"
+ size="40"
+ wsm_persist="true"
+ class="input-inline"
+ aria-labelledby="prettyNameLabel"
+ oninput="acctNamePageValidate();"/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <!-- Done page : this page summarizes information collected to create a mail/news account -->
+ <wizardpage id="done" pageid="done"
+ label="&completionTitle.label;">
+ <vbox flex="1">
+ <description>&completionText.label;</description>
+ <separator class="thin"/>
+ <html:div class="grid-two-column-fr grid-items-baseline">
+ <label id="account.name.label" flex="1" class="label" value="&accnameLabel.label;"/>
+ <label id="account.name.text" flex="1" class="label"/>
+ <label id="identity.email.label" flex="1" class="label" value="&emailLabel.label;"/>
+ <label id="identity.email.text" flex="1" class="label"/>
+ <label id="server.username.label" flex="1" class="label" value="&incomingUsername.label;"/>
+ <label id="server.username.text" flex="1" class="label"/>
+ <label id="newsServer.name.label" flex="1" class="label" value="&newsServerNamePrefix.label;"/>
+ <label id="newsServer.name.text" flex="1" class="label"/>
+ </html:div>
+ <separator/>
+ <spacer flex="1"/>
+#ifndef XP_MACOSX
+ <description>&clickFinish.label;</description>
+#else
+ <description>&clickFinish.labelMac;</description>
+#endif
+ </vbox>
+ </wizardpage>
+ </wizard>
+</window>
diff --git a/comm/mailnews/base/prefs/content/SmtpServerEdit.js b/comm/mailnews/base/prefs/content/SmtpServerEdit.js
new file mode 100644
index 0000000000..56d6076c5f
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/SmtpServerEdit.js
@@ -0,0 +1,241 @@
+/* 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 { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import(
+ "resource:///modules/hostnameUtils.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { OAuth2Providers } = ChromeUtils.import(
+ "resource:///modules/OAuth2Providers.jsm"
+);
+
+var gSmtpServer;
+var gSmtpUsername;
+var gSmtpDescription;
+var gSmtpUsernameLabel;
+var gSmtpHostname;
+var gSmtpPort;
+var gSmtpAuthMethod;
+var gSmtpSocketType;
+var gPort;
+var gDefaultPort;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onAccept);
+
+function onLoad() {
+ gSmtpServer = window.arguments[0].server;
+ initSmtpSettings(gSmtpServer);
+}
+
+function onAccept(event) {
+ if (!isLegalHostNameOrIP(cleanUpHostName(gSmtpHostname.value))) {
+ let prefsBundle = document.getElementById("bundle_prefs");
+ let brandBundle = document.getElementById("bundle_brand");
+ let alertTitle = brandBundle.getString("brandShortName");
+ let alertMsg = prefsBundle.getString("enterValidServerName");
+ Services.prompt.alert(window, alertTitle, alertMsg);
+
+ window.arguments[0].result = false;
+ event.preventDefault();
+ return;
+ }
+
+ // If we didn't have an SMTP server to initialize with,
+ // we must be creating one.
+ try {
+ if (!gSmtpServer) {
+ gSmtpServer = MailServices.smtp.createServer();
+ window.arguments[0].addSmtpServer = gSmtpServer.key;
+ }
+
+ saveSmtpSettings(gSmtpServer);
+ } catch (ex) {
+ console.error("Error saving smtp server: " + ex);
+ }
+
+ window.arguments[0].result = true;
+}
+
+function initSmtpSettings(server) {
+ gSmtpUsername = document.getElementById("smtpUsername");
+ gSmtpDescription = document.getElementById("smtp.description");
+ gSmtpUsernameLabel = document.getElementById("smtpUsernameLabel");
+ gSmtpHostname = document.getElementById("smtp.hostname");
+ gSmtpPort = document.getElementById("smtp.port");
+ gSmtpAuthMethod = document.getElementById("smtp.authMethod");
+ gSmtpSocketType = document.getElementById("smtp.socketType");
+ gDefaultPort = document.getElementById("smtp.defaultPort");
+ gPort = document.getElementById("smtp.port");
+
+ if (server) {
+ gSmtpHostname.value = server.hostname;
+ gSmtpDescription.value = server.description;
+ gSmtpPort.value = server.port;
+ gSmtpUsername.value = server.username;
+ gSmtpAuthMethod.value = server.authMethod;
+ gSmtpSocketType.value = server.socketType < 4 ? server.socketType : 1;
+ } else {
+ // New server, load default values.
+ gSmtpAuthMethod.value = Services.prefs.getIntPref(
+ "mail.smtpserver.default.authMethod"
+ );
+ gSmtpSocketType.value = Services.prefs.getIntPref(
+ "mail.smtpserver.default.try_ssl"
+ );
+ }
+
+ // Although sslChanged will set a label for cleartext password,
+ // we need to use the long label so that we can size the dialog.
+ setLabelFromStringBundle("authMethod-no", "authNo");
+ setLabelFromStringBundle(
+ "authMethod-password-encrypted",
+ "authPasswordEncrypted"
+ );
+ setLabelFromStringBundle(
+ "authMethod-password-cleartext",
+ "authPasswordCleartextInsecurely"
+ );
+ setLabelFromStringBundle("authMethod-kerberos", "authKerberos");
+ setLabelFromStringBundle("authMethod-ntlm", "authNTLM");
+ setLabelFromStringBundle("authMethod-oauth2", "authOAuth2");
+ setLabelFromStringBundle("authMethod-anysecure", "authAnySecure");
+ setLabelFromStringBundle("authMethod-any", "authAny");
+
+ window.sizeToContent();
+
+ sslChanged(false);
+ authMethodChanged(false);
+
+ if (MailServices.smtp.defaultServer) {
+ onLockPreference();
+ }
+
+ // Hide OAuth2 option if we can't use it.
+ let details = server
+ ? OAuth2Providers.getHostnameDetails(server.hostname)
+ : null;
+ document.getElementById("authMethod-oauth2").hidden = !details;
+
+ // Hide deprecated/hidden auth options, unless selected
+ hideUnlessSelected(document.getElementById("authMethod-anysecure"));
+ hideUnlessSelected(document.getElementById("authMethod-any"));
+
+ // "STARTTLS, if available" is vulnerable to MITM attacks so we shouldn't
+ // allow users to choose it anymore. Hide the option unless the user already
+ // has it set.
+ hideUnlessSelected(document.getElementById("connectionSecurityType-1"));
+}
+
+function hideUnlessSelected(element) {
+ element.hidden = !element.selected;
+}
+
+function setLabelFromStringBundle(elementID, stringName) {
+ document.getElementById(elementID).label = document
+ .getElementById("bundle_messenger")
+ .getString(stringName);
+}
+
+// Disables xul elements that have associated preferences locked.
+function onLockPreference() {
+ try {
+ let allPrefElements = {
+ hostname: gSmtpHostname,
+ description: gSmtpDescription,
+ port: gSmtpPort,
+ authMethod: gSmtpAuthMethod,
+ try_ssl: gSmtpSocketType,
+ };
+ disableIfLocked(allPrefElements);
+ } catch (e) {
+ // non-fatal
+ console.error("Error while getting locked prefs: " + e);
+ }
+}
+
+/**
+ * Does the work of disabling an element given the array which contains
+ * id/prefstring pairs.
+ *
+ * @param {Element[]} prefstrArray - Elements to check.
+ *
+ * TODO: try to merge this with disableIfLocked function in am-offline.js (bug 755885)
+ */
+function disableIfLocked(prefstrArray) {
+ let smtpPrefBranch = Services.prefs.getBranch(
+ "mail.smtpserver." + MailServices.smtp.defaultServer.key + "."
+ );
+
+ for (let prefstring in prefstrArray) {
+ if (smtpPrefBranch.prefIsLocked(prefstring)) {
+ prefstrArray[prefstring].disabled = true;
+ }
+ }
+}
+
+function saveSmtpSettings(server) {
+ if (server) {
+ server.hostname = cleanUpHostName(gSmtpHostname.value);
+ server.description = gSmtpDescription.value;
+ server.port = gSmtpPort.value;
+ server.authMethod = gSmtpAuthMethod.value;
+ server.username = gSmtpUsername.value;
+ server.socketType = gSmtpSocketType.value;
+ }
+}
+
+function authMethodChanged(userAction) {
+ var noUsername = gSmtpAuthMethod.value == Ci.nsMsgAuthMethod.none;
+ gSmtpUsername.disabled = noUsername;
+ gSmtpUsernameLabel.disabled = noUsername;
+}
+
+/**
+ * Resets the default port to SMTP or SMTPS, dependending on
+ * the |gSmtpSocketType| value, and sets the port to use to this default,
+ * if that's appropriate.
+ *
+ * @param {boolean} userAction - false for dialog initialization,
+ * true for user action.
+ */
+function sslChanged(userAction) {
+ const DEFAULT_SMTP_PORT = "587";
+ const DEFAULT_SMTPS_PORT = "465";
+ var socketType = gSmtpSocketType.value;
+ var otherDefaultPort;
+ var prevDefaultPort = gDefaultPort.value;
+
+ if (socketType == Ci.nsMsgSocketType.SSL) {
+ gDefaultPort.value = DEFAULT_SMTPS_PORT;
+ otherDefaultPort = DEFAULT_SMTP_PORT;
+ } else {
+ gDefaultPort.value = DEFAULT_SMTP_PORT;
+ otherDefaultPort = DEFAULT_SMTPS_PORT;
+ }
+
+ // If the port is not set,
+ // or the user is causing the default port to change,
+ // and the port is set to the default for the other protocol,
+ // then set the port to the default for the new protocol.
+ if (
+ gPort.value == 0 ||
+ (userAction &&
+ gDefaultPort.value != prevDefaultPort &&
+ gPort.value == otherDefaultPort)
+ ) {
+ gPort.value = gDefaultPort.value;
+ }
+
+ // switch "insecure password" label
+ setLabelFromStringBundle(
+ "authMethod-password-cleartext",
+ socketType == Ci.nsMsgSocketType.SSL ||
+ socketType == Ci.nsMsgSocketType.alwaysSTARTTLS
+ ? "authPasswordCleartextViaSSL"
+ : "authPasswordCleartextInsecurely"
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/SmtpServerEdit.xhtml b/comm/mailnews/base/prefs/content/SmtpServerEdit.xhtml
new file mode 100644
index 0000000000..d0bbda97c6
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/SmtpServerEdit.xhtml
@@ -0,0 +1,208 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/input-fields.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/smtpEditOverlay.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+>
+ <head>
+ <title>&smtpEditTitle.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/SmtpServerEdit.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog style="width: 100vw; height: 100vh">
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+ <stringbundle
+ id="bundle_brand"
+ src="chrome://branding/locale/brand.properties"
+ />
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <vbox id="smtpServerEditor">
+ <html:div>
+ <html:fieldset>
+ <html:legend>&settings.caption;</html:legend>
+ <hbox flex="1">
+ <vbox>
+ <hbox flex="1" align="center">
+ <label
+ id="smtp.description.label"
+ value="&serverDescription.label;"
+ accesskey="&serverDescription.accesskey;"
+ control="smtp.description"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="smtp.hostname.label"
+ value="&serverName.label;"
+ accesskey="&serverName.accesskey;"
+ control="smtp.hostname"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ value="&serverPort.label;"
+ accesskey="&serverPort.accesskey;"
+ control="smtp.port"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <html:input
+ id="smtp.description"
+ type="text"
+ preftype="string"
+ class="input-inline"
+ prefstring="mail.smtpserver.%serverkey%.description"
+ aria-labelledby="smtp.description.label"
+ />
+ <html:input
+ id="smtp.hostname"
+ type="text"
+ preftype="string"
+ class="uri-element input-inline"
+ prefstring="mail.smtpserver.%serverkey%.hostname"
+ aria-labelledby="smtp.hostname.label"
+ />
+ <hbox align="center">
+ <html:input
+ id="smtp.port"
+ type="number"
+ class="size5 input-inline"
+ min="0"
+ max="65535"
+ preftype="int"
+ prefstring="mail.smtpserver.%serverkey%.port"
+ />
+ <label value="&serverPortDefault.label;" />
+ <label id="smtp.defaultPort" />
+ </hbox>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&security.caption;</html:legend>
+
+ <hbox flex="1">
+ <vbox>
+ <hbox flex="1" align="center">
+ <label
+ value="&connectionSecurity.label;"
+ accesskey="&connectionSecurity.accesskey;"
+ control="smtp.socketType"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ value="&authMethod.label;"
+ accesskey="&authMethod.accesskey;"
+ control="server.authMethod"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="smtpUsernameLabel"
+ value="&userName.label;"
+ accesskey="&userName.accesskey;"
+ control="smtpUsername"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <menulist
+ id="smtp.socketType"
+ oncommand="sslChanged(true);"
+ prefstring="mail.smtpserver.%serverkey%.try_ssl"
+ >
+ <menupopup id="smtp.socketTypePopup">
+ <menuitem
+ value="0"
+ label="&connectionSecurityType-0.label;"
+ />
+ <menuitem
+ id="connectionSecurityType-1"
+ value="1"
+ label="&connectionSecurityType-1.label;"
+ disabled="true"
+ hidden="true"
+ />
+ <menuitem
+ value="2"
+ label="&connectionSecurityType-2.label;"
+ />
+ <menuitem
+ value="3"
+ label="&connectionSecurityType-3.label;"
+ />
+ </menupopup>
+ </menulist>
+ <menulist
+ id="smtp.authMethod"
+ oncommand="authMethodChanged(true);"
+ wsm_persist="true"
+ preftype="int"
+ prefstring="mail.smtpserver.%serverkey%.authMethod"
+ >
+ <menupopup id="smtp.authMethodPopup">
+ <menuitem id="authMethod-no" value="1" />
+ <menuitem id="authMethod-password-cleartext" value="3" />
+ <menuitem id="authMethod-password-encrypted" value="4" />
+ <menuitem id="authMethod-kerberos" value="5" />
+ <menuitem id="authMethod-ntlm" value="6" />
+ <menuitem id="authMethod-oauth2" value="10" />
+ <menuitem id="authMethod-anysecure" value="8" />
+ <menuitem id="authMethod-any" value="9" />
+ </menupopup>
+ </menulist>
+ <hbox class="input-container">
+ <html:input
+ id="smtpUsername"
+ type="text"
+ class="input-inline"
+ preftype="string"
+ prefstring="mail.smtpserver.%serverkey%.username"
+ aria-labelledby="smtpUsernameLabel"
+ />
+ </hbox>
+ </vbox>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/accountUtils.js b/comm/mailnews/base/prefs/content/accountUtils.js
new file mode 100644
index 0000000000..8af2e72cf3
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/accountUtils.js
@@ -0,0 +1,369 @@
+/* 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 AccountManager.js */
+/* globals openTab */ // From utilityOverlay.js
+/* globals SelectFolder */ // From messageWindow.js or messenger.js.
+/* globals MsgGetMessage */ // From mailWindowOverlay.js.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gAnyValidIdentity = false; // If there are no valid identities for any account
+// returns the first account with an invalid server or identity
+
+var gNewAccountToLoad = null; // used to load new messages if we come from the mail3pane
+
+function getInvalidAccounts(accounts) {
+ let invalidAccounts = [];
+ for (let account of accounts) {
+ try {
+ if (!account.incomingServer.valid) {
+ invalidAccounts.push(account);
+ // skip to the next account
+ continue;
+ }
+ } catch (ex) {
+ // this account is busted, just keep going
+ continue;
+ }
+
+ for (let identity of account.identities) {
+ if (identity.valid) {
+ gAnyValidIdentity = true;
+ } else {
+ invalidAccounts.push(account);
+ }
+ }
+ }
+ return invalidAccounts;
+}
+
+function showMailIntegrationDialog() {
+ const nsIShellService = Ci.nsIShellService;
+
+ try {
+ var shellService =
+ Cc["@mozilla.org/suite/shell-service;1"].getService(nsIShellService);
+ var appTypesCheck =
+ shellService.shouldBeDefaultClientFor &
+ (nsIShellService.MAIL | nsIShellService.NEWS);
+
+ // show the default client dialog only if we have at least one account,
+ // if we should check for the default client, and we want to check if we are
+ // the default for mail/news and are not the default client for mail/news
+ if (
+ appTypesCheck &&
+ shellService.shouldCheckDefaultClient &&
+ !shellService.isDefaultClient(true, appTypesCheck)
+ ) {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://communicator/content/defaultClientDialog.xhtml",
+ "DefaultClient",
+ "modal,centerscreen,chrome,resizable=no"
+ );
+ }
+ } catch (ex) {}
+}
+
+/**
+ * Check that an account exists which requires Local Folders.
+ *
+ * @returns {boolean} - true if at least 1 account exists that requires
+ * Local Folders, else false.
+ */
+function requireLocalFoldersAccount() {
+ return MailServices.accounts.accounts.some(account =>
+ ["imap", "pop3", "nntp"].includes(account.incomingServer?.type)
+ );
+}
+
+/**
+ * Open the Nntp Account Wizard, or focus it if it's already open.
+ */
+function openNewsgroupAccountWizard() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://messenger/content/AccountWizard.xhtml",
+ "AccountWizard",
+ "chrome,modal,titlebar,centerscreen"
+ );
+}
+
+function AddIMAccount() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://messenger/content/chat/imAccountWizard.xhtml",
+ "",
+ "chrome,modal,titlebar,centerscreen"
+ );
+}
+
+function AddFeedAccount() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://messenger-newsblog/content/feedAccountWizard.xhtml",
+ "",
+ "chrome,modal,titlebar,centerscreen"
+ );
+}
+
+/**
+ * Opens Address Book tab and triggers the address book creation dialog based on
+ * the passed type.
+ *
+ * @param {string} type - The address book type needing creation. Accepted types
+ * are "JS", "LDAP", and "CARDDAV".
+ */
+function addNewAddressBook(type) {
+ window.browsingContext.topChromeWindow.toAddressBook({
+ action: `create_ab_${type}`,
+ });
+}
+
+function showCalendarWizard() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://calendar/content/calendar-creation.xhtml",
+ "caEditServer",
+ "chrome,titlebar,resizable,centerscreen",
+ {}
+ );
+}
+
+/**
+ * Opens the account settings window on the specified account
+ * and page of settings. If the window is already open it is only focused.
+ *
+ * @param {?string} selectPage - The file name for the viewing page, or null for
+ * the account main page. Other pages are 'am-server.xhtml',
+ * 'am-copies.xhtml', 'am-offline.xhtml', 'am-addressing.xhtml',
+ * 'am-smtp.xhtml'
+ * @param {nsIMsgIncomingServer} [server] - The server of the account to select.
+ */
+async function MsgAccountManager(selectPage, server) {
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ if (!win) {
+ // No window available, so force open a new one.
+ openTab(
+ "contentTab",
+ {
+ url: "about:accountsettings",
+ onLoad(event, browser) {
+ browser.contentDocument.documentElement.server = server;
+ browser.contentDocument.documentElement.selectPage = selectPage;
+ browser.contentDocument.getElementById("accounttree").focus();
+ },
+ },
+ "window"
+ );
+ return;
+ }
+
+ let tabmail = win.document.getElementById("tabmail");
+ // If the server wasn't specified, and we have the window open, try
+ // and use the currently selected folder to work out the server to select.
+ if (!server) {
+ server = tabmail.currentAbout3Pane?.gFolder ?? null;
+ }
+ // If the server is still not found, account settings will default to
+ // the first account.
+
+ // If Account settings tab is already open, change the server
+ // and the selected page, reload the tab and switch to the tab.
+ for (let tabInfo of tabmail.tabInfo) {
+ let tab = tabmail.getTabForBrowser(tabInfo.browser);
+ if (tab?.urlbar?.value == "about:accountsettings") {
+ tab.browser.contentDocument.documentElement.server = server;
+ tab.browser.contentDocument.documentElement.selectPage = selectPage;
+ tab.browser.contentWindow.onLoad();
+ tabmail.switchToTab(tabInfo);
+ return;
+ }
+ }
+
+ // Else fallback to opening a new tab in the window.
+ tabmail.openTab("contentTab", {
+ url: "about:accountsettings",
+ onLoad(event, browser) {
+ browser.contentDocument.documentElement.server = server;
+ browser.contentDocument.documentElement.selectPage = selectPage;
+ browser.contentDocument.getElementById("accounttree").focus();
+ },
+ });
+}
+
+function loadInboxForNewAccount() {
+ // gNewAccountToLoad is set in the final screen of the Account Wizard if a POP account
+ // was created, the download messages box is checked, and the wizard was opened from the 3pane
+ if (gNewAccountToLoad) {
+ var rootMsgFolder = gNewAccountToLoad.incomingServer.rootMsgFolder;
+ const kInboxFlag = Ci.nsMsgFolderFlags.Inbox;
+ var inboxFolder = rootMsgFolder.getFolderWithFlags(kInboxFlag);
+ SelectFolder(inboxFolder.URI);
+ window.focus();
+ setTimeout(MsgGetMessage, 0);
+ gNewAccountToLoad = null;
+ }
+}
+
+// returns true if we migrated - it knows this because 4.x did not have the
+// pref mailnews.quotingPrefs.version, so if it's not set, we're either
+// migrating from 4.x, or a much older version of Mozilla.
+function migrateGlobalQuotingPrefs(allIdentities) {
+ // if reply_on_top and auto_quote exist then, if non-default
+ // migrate and delete, if default just delete.
+ var reply_on_top = 0;
+ var auto_quote = true;
+ var quotingPrefs = Services.prefs.getIntPref(
+ "mailnews.quotingPrefs.version",
+ 0
+ );
+ var migrated = false;
+
+ // If the quotingPrefs version is 0 then we need to migrate our preferences
+ if (quotingPrefs == 0) {
+ migrated = true;
+ try {
+ reply_on_top = Services.prefs.getIntPref("mailnews.reply_on_top");
+ auto_quote = Services.prefs.getBoolPref("mail.auto_quote");
+ } catch (ex) {}
+
+ if (!auto_quote || reply_on_top) {
+ for (let identity of allIdentities) {
+ if (identity.valid) {
+ identity.autoQuote = auto_quote;
+ identity.replyOnTop = reply_on_top;
+ }
+ }
+ }
+ Services.prefs.setIntPref("mailnews.quotingPrefs.version", 1);
+ }
+ return migrated;
+}
+
+/**
+ * Open the Account Setup Tab or focus it if it's already open.
+ */
+function openAccountSetupTab() {
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+
+ // Switch to the account setup tab if it's already open.
+ for (let tabInfo of tabmail.tabInfo) {
+ let tab = tabmail.getTabForBrowser(tabInfo.browser);
+ if (tab?.urlbar?.value == "about:accountsetup") {
+ let accountSetup = tabInfo.browser.contentWindow.gAccountSetup;
+ // Reset the entire UI only if the previously opened setup was completed.
+ if (accountSetup._currentModename == "success") {
+ accountSetup.resetSetup();
+ }
+ tabmail.switchToTab(tabInfo);
+ return;
+ }
+ }
+
+ tabmail.openTab("contentTab", { url: "about:accountsetup" });
+}
+
+/**
+ * Open the account setup tab and switch to the success view to show the newly
+ * created account, or show an error if the account wasn't created.
+ *
+ * @param {object} account - A newly created account.
+ * @param {string} name - The account name defined in the provider's website.
+ * @param {string} email - The newly created email address.
+ */
+function openAccountSetupTabWithAccount(account, name, email) {
+ // Define which actions we need to take after the account setup tab has been
+ // loaded and we have access to its objects.
+ let onTabLoaded = function (event, browser, account) {
+ let accountSetup = browser.contentWindow.gAccountSetup;
+
+ if (account) {
+ // Update the account setup variables before kicking off the success view
+ // which will start fetching linked services with these values.
+ accountSetup._realname = name;
+ accountSetup._email = email;
+ accountSetup._password = account.incomingServer.password;
+ accountSetup.showSuccessView(account);
+ return;
+ }
+
+ accountSetup.showErrorNotification("account-setup-provisioner-error");
+ };
+
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+
+ // Switch to the account setup tab if it's already open.
+ for (let tabInfo of tabmail.tabInfo) {
+ let tab = tabmail.getTabForBrowser(tabInfo.browser);
+ if (tab?.urlbar?.value == "about:accountsetup") {
+ let accountSetup = tabInfo.browser.contentWindow.gAccountSetup;
+ // Reset the entire UI only if the previously opened setup was completed.
+ if (accountSetup._currentModename == "success") {
+ accountSetup.resetSetup();
+ }
+ tabmail.switchToTab(tabInfo);
+ onTabLoaded(null, tabInfo.browser, account);
+ return;
+ }
+ }
+
+ // Open the account setup tab.
+ tabmail.openTab("contentTab", {
+ url: "about:accountsetup",
+ onLoad(event, browser) {
+ onTabLoaded(event, browser, account);
+ },
+ });
+}
+
+/**
+ * Open the Account Provisioner Tab or focus it if it's already open.
+ */
+function openAccountProvisionerTab() {
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+
+ // Switch to the account setup tab if it's already open.
+ for (let tabInfo of tabmail.tabInfo) {
+ let tab = tabmail.getTabForBrowser(tabInfo.browser);
+ if (tab?.urlbar?.value == "about:accountprovisioner") {
+ tabmail.switchToTab(tabInfo);
+ return;
+ }
+ }
+
+ tabmail.openTab("contentTab", { url: "about:accountprovisioner" });
+}
+
+/**
+ * Reveal the Folder Pane after an account creation callback.
+ */
+function updateMailPaneUI() {
+ // Nothing to update since no account has been created.
+ if (MailServices.accounts.accounts.length == 0) {
+ return;
+ }
+
+ let mail3Pane = Services.wm.getMostRecentWindow("mail:3pane");
+ // Set the folderPaneVisible to true in the tabmail to prevent collapsing
+ // on tab switch.
+ let tabmail = mail3Pane.document.getElementById("tabmail");
+ tabmail.tabInfo[0].folderPaneVisible = true;
+}
+
+/**
+ * Open the OpenPGP Key Manager from outside the Account Settings.
+ */
+function openKeyManager() {
+ window.browsingContext.topChromeWindow.openDialog(
+ "chrome://openpgp/content/ui/enigmailKeyManager.xhtml",
+ "enigmail:KeyManager",
+ "dialog,centerscreen,resizable",
+ {
+ cancelCallback: null,
+ okCallback: null,
+ }
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-addressing.inc.xhtml b/comm/mailnews/base/prefs/content/am-addressing.inc.xhtml
new file mode 100644
index 0000000000..44d393f4ed
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-addressing.inc.xhtml
@@ -0,0 +1,125 @@
+# 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/.
+
+ <vbox flex="1" id="compositionAndAddressing">
+ <stringbundle id="bundle_addressBook" src="chrome://messenger/locale/addressbook/addressBook.properties"/>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&compositionGroupTitle.label;</html:legend>
+ <hbox align="center">
+ <checkbox wsm_persist="true" id="identity.composeHtml" label="&useHtml.label;"
+ accesskey="&useHtml.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.compose_html"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <checkbox wsm_persist="true" id="identity.autoQuote" label="&autoQuote.label;"
+ accesskey="&autoQuote.accesskey;"
+ pref="true" preftype="bool" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.auto_quote"/>
+ </hbox>
+ <hbox align="center">
+ <label value="&quoting.label;" accesskey="&quoting.accesskey;" control="identity.replyOnTop"/>
+ <menulist wsm_persist="true" id="identity.replyOnTop" oncommand="quoteEnabling();"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.reply_on_top">
+ <menupopup>
+ <menuitem value="1" label="&aboveQuote.label;"/>
+ <menuitem value="0" label="&belowQuote.label;"/>
+ <menuitem value="2" label="&selectAndQuote.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+ <hbox class="indent" align="center" id="placeBox">
+ <label value="&place.label;" accesskey="&place.accesskey;" control="identity.sig_bottom"/>
+ <menulist wsm_persist="true" id="identity.sig_bottom" genericattr="true"
+ pref="true" preftype="bool" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.sig_bottom">
+ <menupopup>
+ <menuitem value="true" label="&belowText.label;"/>
+ <menuitem value="false" label="&aboveText.label;"/>
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <checkbox id="identity.sig_on_reply" wsm_persist="true"
+ label="&includeSigOnReply.label;"
+ accesskey="&includeSigOnReply.accesskey;"
+ preftype="bool" genericattr="true"
+ prefstring="mail.identity.%identitykey%.sig_on_reply"/>
+
+ <checkbox id="identity.sig_on_fwd" wsm_persist="true"
+ label="&includeSigOnForward.label;"
+ accesskey="&includeSigOnForward.accesskey;"
+ preftype="bool" genericattr="true"
+ prefstring="mail.identity.%identitykey%.sig_on_fwd"/>
+
+ <separator class="thin"/>
+
+ <hbox pack="end">
+ <button id="globalComposingPrefsLink"
+ label="&globalComposingPrefs.label;"
+ accesskey="&globalComposingPrefs.accesskey;"
+ oncommand="showGlobalComposingPrefs();"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin"/>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&addressingGroupTitle.label;</html:legend>
+#ifndef MOZ_THUNDERBIRD
+ <hbox align="center">
+ <checkbox wsm_persist="true" id="identity.autocompleteToMyDomain"
+ label="&autocompleteToMyDomain.label;"
+ accesskey="&autocompleteToMyDomain.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.autocompleteToMyDomain"/>
+ </hbox>
+
+ <separator class="thin"/>
+#endif
+
+ <label control="identity.overrideGlobal_Pref">&addressingText.label;</label>
+ <radiogroup id="identity.overrideGlobal_Pref" class="indent"
+ oncommand="LDAPenabling();" wsm_persist="true"
+ genericattr="true" preftype="bool"
+ prefstring="mail.identity.%identitykey%.overrideGlobal_Pref">
+ <radio value="false" label="&useGlobal.label;"
+ accesskey="&useGlobal.accesskey;"/>
+ <radio value="true" id="directories" label="&directories.label;"
+ accesskey="&directories.accesskey;"/>
+ <hbox class="indent">
+ <menulist is="menulist-addrbooks" id="identity.directoryServer"
+ none="&directoriesNone.label;"
+ remoteonly="true"
+ wsm_persist="true"
+ preftype="string"
+ prefstring="mail.identity.%identitykey%.directoryServer"
+ style="min-width: 16em;"
+ aria-labelledby="directories"
+ flex="1"/>
+ <button id="editButton" label="&editDirectories.label;"
+ accesskey="&editDirectories.accesskey;"
+ oncommand="onEditDirectories();"/>
+ </hbox>
+ </radiogroup>
+
+ <separator class="thin"/>
+
+ <hbox pack="end">
+ <button id="globalAddressingPrefsLink"
+ label="&globalAddressingPrefs.label;"
+ accesskey="&globalAddressingPrefs.accesskey;"
+ oncommand="showGlobalAddressingPrefs();"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
diff --git a/comm/mailnews/base/prefs/content/am-addressing.js b/comm/mailnews/base/prefs/content/am-addressing.js
new file mode 100644
index 0000000000..24a3bf0004
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-addressing.js
@@ -0,0 +1,68 @@
+/* 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 am-prefs.js */
+/* import-globals-from amUtils.js */
+
+function onLoad() {
+ parent.onPanelLoaded("am-addressing.xhtml");
+}
+
+function onInit(aPageId, aServerId) {
+ onInitCompositionAndAddressing();
+}
+
+function onInitCompositionAndAddressing() {
+ LDAPenabling();
+ quoteEnabling();
+}
+
+function onEditDirectories() {
+ parent.gSubDialog.open(
+ "chrome://messenger/content/addressbook/pref-editdirectories.xhtml"
+ );
+}
+
+function onPreInit(account, accountValues) {}
+
+function LDAPenabling() {
+ onCheckItem("identity.directoryServer", ["directories"]);
+ onCheckItem("editButton", ["directories"]);
+}
+
+function quoteEnabling() {
+ var placebox = document.getElementById("placeBox");
+
+ if (document.getElementById("identity.replyOnTop").value == "1") {
+ placebox.firstElementChild.removeAttribute("disabled");
+ placebox.lastElementChild.removeAttribute("disabled");
+ } else {
+ placebox.firstElementChild.setAttribute("disabled", "true");
+ placebox.lastElementChild.setAttribute("disabled", "true");
+ }
+}
+
+/**
+ * Open the Preferences dialog on the tab with Addressing options.
+ */
+function showGlobalAddressingPrefs() {
+ openPrefsFromAccountManager(
+ "paneCompose",
+ "compositionAddressingCategory",
+ null,
+ "addressing_pane"
+ );
+}
+
+/**
+ * Open the Preferences dialog on the tab with Composing options.
+ */
+function showGlobalComposingPrefs() {
+ openPrefsFromAccountManager(
+ "paneCompose",
+ null,
+ null,
+ "composing_messages_pane"
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-addressing.xhtml b/comm/mailnews/base/prefs/content/am-addressing.xhtml
new file mode 100644
index 0000000000..554ee398c1
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-addressing.xhtml
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-addressing.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&addressing.label;</title>
+ <script defer="defer" src="chrome://messenger/content/am-addressing.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => { onLoad(); });
+ </script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <vbox id="containerBox" flex="1">
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&addressing.label;"/>
+ </hbox>
+#include am-addressing.inc.xhtml
+ </vbox>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-archiveoptions.js b/comm/mailnews/base/prefs/content/am-archiveoptions.js
new file mode 100644
index 0000000000..44c0817149
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-archiveoptions.js
@@ -0,0 +1,74 @@
+/* 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 gIdentity = null;
+
+window.addEventListener("load", onLoadArchiveOptions);
+document.addEventListener("dialogaccept", onAcceptArchiveOptions);
+
+/**
+ * Load the archive options dialog, set the radio/checkbox items to the
+ * appropriate values, and update the archive hierarchy example.
+ */
+function onLoadArchiveOptions() {
+ // extract the account
+ gIdentity = window.arguments[0].identity;
+
+ let granularity = document.getElementById("archiveGranularity");
+ granularity.selectedIndex = gIdentity.archiveGranularity;
+ granularity.addEventListener("command", updateArchiveExample);
+
+ let kfs = document.getElementById("archiveKeepFolderStructure");
+ kfs.checked = gIdentity.archiveKeepFolderStructure;
+ kfs.addEventListener("command", updateArchiveExample);
+
+ updateArchiveExample();
+}
+
+/**
+ * Save the archive settings to the current identity.
+ */
+function onAcceptArchiveOptions() {
+ gIdentity.archiveGranularity =
+ document.getElementById("archiveGranularity").selectedIndex;
+ gIdentity.archiveKeepFolderStructure = document.getElementById(
+ "archiveKeepFolderStructure"
+ ).checked;
+}
+
+/**
+ * Update the example tree to show what the current options would look like.
+ */
+function updateArchiveExample() {
+ let granularity = document.getElementById("archiveGranularity").selectedIndex;
+ let kfs = document.getElementById("archiveKeepFolderStructure").checked;
+ let hierarchy = [
+ document.getElementsByClassName("root"),
+ document.getElementsByClassName("year"),
+ document.getElementsByClassName("month"),
+ ];
+
+ // First, show/hide the appropriate levels in the hierarchy and turn the
+ // necessary items into containers.
+ for (let i = 0; i < hierarchy.length; i++) {
+ for (let j = 0; j < hierarchy[i].length; j++) {
+ hierarchy[i][j].setAttribute("container", granularity > i);
+ hierarchy[i][j].setAttribute("open", granularity > i);
+ hierarchy[i][j].hidden = granularity < i;
+ }
+ }
+
+ // Next, handle the "keep folder structures" case by moving a tree item around
+ // and making sure its parent is a container.
+ let folders = document.getElementById("folders");
+ folders.hidden = !kfs;
+ if (kfs) {
+ let parent = hierarchy[granularity][0];
+ parent.setAttribute("container", true);
+ parent.setAttribute("open", true);
+
+ let treechildren = parent.children[1];
+ treechildren.appendChild(folders);
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/am-archiveoptions.xhtml b/comm/mailnews/base/prefs/content/am-archiveoptions.xhtml
new file mode 100644
index 0000000000..e34de7a420
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-archiveoptions.xhtml
@@ -0,0 +1,136 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderPane.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-archiveoptions.dtd">
+<html
+ id="archiveOptions"
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="width: 500px"
+ scrolling="false"
+>
+ <head>
+ <title>&dialogTitle.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-archiveoptions.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog style="width: 100vw; height: 100vh">
+ <vbox flex="1">
+ <label>&archiveGranularityPrefix.label;</label>
+ <radiogroup id="archiveGranularity">
+ <radio
+ label="&archiveFlat.label;"
+ accesskey="&archiveFlat.accesskey;"
+ class="indent"
+ />
+ <radio
+ label="&archiveYearly.label;"
+ accesskey="&archiveYearly.accesskey;"
+ class="indent"
+ />
+ <radio
+ label="&archiveMonthly.label;"
+ accesskey="&archiveMonthly.accesskey;"
+ class="indent"
+ />
+ </radiogroup>
+ <checkbox
+ id="archiveKeepFolderStructure"
+ label="&keepFolderStructure.label;"
+ accesskey="&keepFolderStructure.accesskey;"
+ />
+
+ <html:div>
+ <html:fieldset flex="1" style="margin-block-end: 5px">
+ <html:legend>&archiveExample.label;</html:legend>
+ <hbox flex="1">
+ <tree
+ id="archiveTree"
+ hidecolumnpicker="true"
+ disabled="true"
+ flex="1"
+ style="min-height: 8em"
+ >
+ <treecols>
+ <treecol
+ id="folderNameCol"
+ primary="true"
+ hideheader="true"
+ style="flex: 1 auto"
+ />
+ </treecols>
+ <treechildren>
+ <treeitem class="root">
+ <treerow>
+ <treecell
+ properties="specialFolder-Archive"
+ label="&archiveFolderName.label;"
+ />
+ </treerow>
+ <treechildren>
+ <treeitem id="folders">
+ <treerow>
+ <treecell label="&inboxFolderName.label;" />
+ </treerow>
+ </treeitem>
+ <treeitem class="year">
+ <treerow>
+ <treecell label="2020" />
+ </treerow>
+ <treechildren>
+ <treeitem class="month">
+ <treerow>
+ <treecell label="2020-11" />
+ </treerow>
+ <treechildren />
+ </treeitem>
+ <treeitem class="month">
+ <treerow>
+ <treecell label="2020-12" />
+ </treerow>
+ <treechildren />
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ <treeitem class="year">
+ <treerow>
+ <treecell label="2021" />
+ </treerow>
+ <treechildren>
+ <treeitem class="month">
+ <treerow>
+ <treecell label="2021-01" />
+ </treerow>
+ <treechildren />
+ </treeitem>
+ <treeitem class="month">
+ <treerow>
+ <treecell label="2021-02" />
+ </treerow>
+ <treechildren />
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+ </treeitem>
+ </treechildren>
+ </tree>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-copies.inc.xhtml b/comm/mailnews/base/prefs/content/am-copies.inc.xhtml
new file mode 100644
index 0000000000..75834fd60a
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-copies.inc.xhtml
@@ -0,0 +1,308 @@
+# 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/.
+
+ <vbox id="copiesAndFolders" flex="1">
+
+ <linkset>
+ <html:link rel="localization" href="messenger/preferences/am-copies.ftl"/>
+ </linkset>
+
+ <stringbundle id="bundle_messenger" src="chrome://messenger/locale/messenger.properties"/>
+
+ <label id="identity.fccFolder" hidden="true" wsm_persist="true"
+ pref="true" preftype="wstring" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.fcc_folder"/>
+ <label id="identity.draftFolder" hidden="true" wsm_persist="true"
+ pref="true" preftype="wstring" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.draft_folder"/>
+ <label id="identity.archiveFolder" hidden="true" wsm_persist="true"
+ pref="true" preftype="wstring" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.archive_folder"/>
+ <label id="identity.stationeryFolder" hidden="true" wsm_persist="true"
+ pref="true" preftype="wstring" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.stationary_folder"/>
+ <label id="identity.email" hidden="true" wsm_persist="true"/>
+ <label id="identity.fccFolderPickerMode" hidden="true" wsm_persist="true"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.fcc_folder_picker_mode"/>
+ <label id="identity.draftsFolderPickerMode" hidden="true" wsm_persist="true"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.drafts_folder_picker_mode"/>
+ <label id="identity.archivesFolderPickerMode" hidden="true" wsm_persist="true"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.archives_folder_picker_mode"/>
+ <label id="identity.tmplFolderPickerMode" hidden="true" wsm_persist="true"
+ pref="true" preftype="int" prefattribute="value"
+ prefstring="mail.identity.%identitykey%.tmpl_folder_picker_mode"/>
+ <html:div>
+ <html:fieldset id="copiesGroup">
+ <html:legend>&sendingPrefix.label;</html:legend>
+
+ <hbox align="center">
+ <checkbox id="identity.doFcc" wsm_persist="true" label="&fccMailFolder.label;"
+ accesskey="&fccMailFolder.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.fcc"
+ oncommand="setupFccItems();"/>
+ </hbox>
+ <radiogroup id="doFcc" aria-labelledby="identity.doFcc">
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio id="fcc_selectAccount"
+ class="depends-on-do-fcc"
+ value="0" label="&sentFolderOn.label;"
+ accesskey="&sentFolderOn.accesskey;"
+ oncommand="setPickersState('msgFccAccountPicker', 'msgFccFolderPicker', event)"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio id="fcc_selectFolder"
+ class="depends-on-do-fcc"
+ value="1" label="&sentInOtherFolder.label;"
+ accesskey="&sentInOtherFolder.accesskey;"
+ oncommand="setPickersState('msgFccFolderPicker', 'msgFccAccountPicker', event)"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="msgFccAccountPicker"
+ class="folderMenuItem depends-on-do-fcc"
+ aria-labelledby="fcc_selectAccount">
+ <menupopup is="folder-menupopup" id="msgFccAccountPopup"
+ mode="filing"
+ expandFolders="false"
+ oncommand="noteSelectionChange('fcc', 'Account', event)"/>
+ </menulist>
+ <menulist id="msgFccFolderPicker"
+ class="folderMenuItem depends-on-do-fcc"
+ aria-labelledby="fcc_selectFolder"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgFccFolderPopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showFileHereLabel="true"
+ oncommand="noteSelectionChange('fcc', 'Folder', event)"/>
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+
+ <hbox align="center" class="fccReplyFollowsParent" hidable="true" hidefor="nntp,rss">
+ <checkbox id="identity.fccReplyFollowsParent" wsm_persist="true"
+ class="depends-on-do-fcc"
+ label="&fccReplyFollowsParent.label;"
+ accesskey="&fccReplyFollowsParent.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.fcc_reply_follows_parent"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox>
+ <vbox>
+ <hbox flex="1" align="center">
+ <checkbox id="identity.doCc" wsm_persist="true" label="&ccAddress.label;"
+ accesskey="&ccAddress.accesskey;"
+ control="identity.doCcList"
+ oncommand="identityDoCcBccOnCommand(event);"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.doCc"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <checkbox id="identity.doBcc" wsm_persist="true" label="&bccAddress.label;"
+ accesskey="&bccAddress.accesskey;"
+ control="identity.doBccList"
+ oncommand="identityDoCcBccOnCommand(event);"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.doBcc"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <hbox flex="1" class="input-container" align="center">
+ <html:input id="identity.doCcList" wsm_persist="true"
+ type="text"
+ aria-labelledby="identity.doCc"
+ prefstring="mail.identity.%identitykey%.doCcList"
+ class="uri-element input-inline"
+ placeholder="&ccAddressList.placeholder;"
+ onblur="identityDoCcBccOnBlur(event);"/>
+ </hbox>
+ <hbox flex="1" class="input-container" align="center">
+ <html:input id="identity.doBccList" wsm_persist="true"
+ type="text"
+ aria-labelledby="identity.doBcc"
+ prefstring="mail.identity.%identitykey%.doBccList"
+ class="uri-element input-inline"
+ placeholder="&bccAddressList.placeholder;"
+ onblur="identityDoCcBccOnBlur(event);"/>
+ </hbox>
+ </vbox>
+ </hbox>
+ <description class="indent tip-caption"
+ data-l10n-id="account-prefs-show-address-row-description"/>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset id="archivesGroup">
+ <html:legend>&archivesTitle.label;</html:legend>
+
+ <hbox pack="start">
+ <checkbox id="identity.archiveEnabled" wsm_persist="true"
+ label="&keepArchives.label;"
+ accesskey="&keepArchives.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.archive_enabled"
+ oncommand="setupArchiveItems();"/>
+ </hbox>
+
+ <radiogroup id="messageArchives">
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio id="archive_selectAccount"
+ class="depends-on-archive"
+ value="0" label="&archivesFolderOn.label;"
+ accesskey="&archivesFolderOn.accesskey;"
+ oncommand="setPickersState('msgArchivesAccountPicker', 'msgArchivesFolderPicker', event)"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio id="archive_selectFolder"
+ class="depends-on-archive"
+ value="1" label="&archiveInOtherFolder.label;"
+ accesskey="&archiveInOtherFolder.accesskey;"
+ oncommand="setPickersState('msgArchivesFolderPicker', 'msgArchivesAccountPicker', event)"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="msgArchivesAccountPicker"
+ class="folderMenuItem depends-on-archive"
+ aria-labelledby="archive_selectAccount">
+ <menupopup is="folder-menupopup" id="msgArchivesAccountPopup"
+ mode="filing"
+ expandFolders="false"
+ oncommand="noteSelectionChange('archive', 'Account', event)"/>
+ </menulist>
+ <menulist id="msgArchivesFolderPicker"
+ class="folderMenuItem depends-on-archive"
+ aria-labelledby="archive_selectFolder"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgArchivesFolderPopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showFileHereLabel="true"
+ oncommand="noteSelectionChange('archive', 'Folder', event)"/>
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+ <hbox pack="end">
+ <button id="archiveHierarchyButton"
+ class="depends-on-archive"
+ label="&archiveHierarchyButton.label;"
+ accesskey="&archiveHierarchyButton.accesskey;"
+ oncommand="ChangeArchiveHierarchy();"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset id="foldersGroup">
+ <html:legend>&specialFolders.label;</html:legend>
+
+ <hbox align="center">
+ <label value="&keepDrafts2.label;" control="messageDrafts"/>
+ </hbox>
+
+ <radiogroup id="messageDrafts">
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio id="draft_selectAccount"
+ oncommand="setPickersState('msgDraftsAccountPicker', 'msgDraftsFolderPicker', event)"
+ value="0" label="&draftsFolderOn.label;"
+ accesskey="&draftsFolderOn.accesskey;"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio id="draft_selectFolder"
+ oncommand="setPickersState('msgDraftsFolderPicker', 'msgDraftsAccountPicker', event)"
+ value="1" label="&draftInOtherFolder.label;"
+ accesskey="&draftInOtherFolder.accesskey;"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="msgDraftsAccountPicker"
+ class="folderMenuItem"
+ aria-labelledby="draft_selectAccount">
+ <menupopup is="folder-menupopup" id="msgDraftAccountPopup"
+ mode="filing"
+ expandFolders="false"
+ oncommand="noteSelectionChange('draft', 'Account', event)"/>
+ </menulist>
+ <menulist id="msgDraftsFolderPicker"
+ class="folderMenuItem"
+ aria-labelledby="draft_selectFolder"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgDraftFolderPopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showFileHereLabel="true"
+ oncommand="noteSelectionChange('draft', 'Folder', event)"/>
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+
+ <separator class="thin"/>
+
+ <hbox align="center">
+ <label value="&keepTemplates.label;" control="messageTemplates"/>
+ </hbox>
+
+ <radiogroup id="messageTemplates">
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio id="tmpl_selectAccount"
+ oncommand="setPickersState('msgStationeryAccountPicker', 'msgStationeryFolderPicker', event)"
+ value="0" label="&templatesFolderOn.label;"
+ accesskey="&templatesFolderOn.accesskey;"/>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio id="tmpl_selectFolder"
+ oncommand="setPickersState('msgStationeryFolderPicker', 'msgStationeryAccountPicker', event)"
+ value="1" label="&templateInOtherFolder.label;"
+ accesskey="&templateInOtherFolder.accesskey;"/>
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist id="msgStationeryAccountPicker"
+ class="folderMenuItem"
+ aria-labelledby="tmpl_selectAccount">
+ <menupopup is="folder-menupopup" id="msgFccAccountPopup"
+ mode="filing"
+ expandFolders="false"
+ oncommand="noteSelectionChange('tmpl', 'Account', event)"/>
+ </menulist>
+ <menulist id="msgStationeryFolderPicker"
+ class="folderMenuItem"
+ aria-labelledby="tmpl_selectFolder"
+ displayformat="verbose">
+ <menupopup is="folder-menupopup" id="msgTemplFolderPopup"
+ mode="filing"
+ class="menulist-menupopup"
+ showFileHereLabel="true"
+ oncommand="noteSelectionChange('tmpl', 'Folder', event)"/>
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+ <hbox align="center">
+ <checkbox id="identity.showSaveMsgDlg" wsm_persist="true" label="&saveMessageDlg.label;"
+ accesskey="&saveMessageDlg.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.showSaveMsgDlg"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
diff --git a/comm/mailnews/base/prefs/content/am-copies.js b/comm/mailnews/base/prefs/content/am-copies.js
new file mode 100644
index 0000000000..f848bab0c0
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-copies.js
@@ -0,0 +1,555 @@
+/* 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 am-prefs.js */
+/* import-globals-from amUtils.js */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gFccRadioElemChoice,
+ gDraftsRadioElemChoice,
+ gArchivesRadioElemChoice,
+ gTmplRadioElemChoice;
+var gFccRadioElemChoiceLocked,
+ gDraftsRadioElemChoiceLocked,
+ gArchivesRadioElemChoiceLocked,
+ gTmplRadioElemChoiceLocked;
+var gDefaultPickerMode = "1";
+
+var gFccFolderWithDelim,
+ gDraftsFolderWithDelim,
+ gArchivesFolderWithDelim,
+ gTemplatesFolderWithDelim;
+var gAccount;
+var gCurrentServerId;
+
+function onPreInit(account, accountValues) {
+ gAccount = account;
+ var type = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ hideShowControls(type);
+}
+
+/*
+ * Set the global radio element choices and initialize folder/account pickers.
+ * Also, initialize other UI elements (cc, bcc, fcc picker controller checkboxes).
+ */
+function onInit(aPageId, aServerId) {
+ gCurrentServerId = aServerId;
+ onInitCopiesAndFolders();
+}
+
+function onInitCopiesAndFolders() {
+ SetGlobalRadioElemChoices();
+
+ SetFolderDisplay(
+ gFccRadioElemChoice,
+ gFccRadioElemChoiceLocked,
+ "fcc",
+ "msgFccAccountPicker",
+ "identity.fccFolder",
+ "msgFccFolderPicker"
+ );
+
+ SetFolderDisplay(
+ gArchivesRadioElemChoice,
+ gArchivesRadioElemChoiceLocked,
+ "archive",
+ "msgArchivesAccountPicker",
+ "identity.archiveFolder",
+ "msgArchivesFolderPicker"
+ );
+
+ SetFolderDisplay(
+ gDraftsRadioElemChoice,
+ gDraftsRadioElemChoiceLocked,
+ "draft",
+ "msgDraftsAccountPicker",
+ "identity.draftFolder",
+ "msgDraftsFolderPicker"
+ );
+
+ SetFolderDisplay(
+ gTmplRadioElemChoice,
+ gTmplRadioElemChoiceLocked,
+ "tmpl",
+ "msgStationeryAccountPicker",
+ "identity.stationeryFolder",
+ "msgStationeryFolderPicker"
+ );
+
+ setupDoCcBccItems("identity.doCc", "identity.doCcList");
+ setupDoCcBccItems("identity.doBcc", "identity.doBccList");
+ setupFccItems();
+ setupArchiveItems();
+
+ SetSpecialFolderNamesWithDelims();
+}
+
+// Initialize the picker mode choices (account/folder picker) into global vars
+function SetGlobalRadioElemChoices() {
+ var pickerModeElement = document.getElementById(
+ "identity.fccFolderPickerMode"
+ );
+ gFccRadioElemChoice = pickerModeElement.getAttribute("value");
+ gFccRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled");
+ if (!gFccRadioElemChoice) {
+ gFccRadioElemChoice = gDefaultPickerMode;
+ }
+
+ pickerModeElement = document.getElementById(
+ "identity.archivesFolderPickerMode"
+ );
+ gArchivesRadioElemChoice = pickerModeElement.getAttribute("value");
+ gArchivesRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled");
+ if (!gArchivesRadioElemChoice) {
+ gArchivesRadioElemChoice = gDefaultPickerMode;
+ }
+
+ pickerModeElement = document.getElementById(
+ "identity.draftsFolderPickerMode"
+ );
+ gDraftsRadioElemChoice = pickerModeElement.getAttribute("value");
+ gDraftsRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled");
+ if (!gDraftsRadioElemChoice) {
+ gDraftsRadioElemChoice = gDefaultPickerMode;
+ }
+
+ pickerModeElement = document.getElementById("identity.tmplFolderPickerMode");
+ gTmplRadioElemChoice = pickerModeElement.getAttribute("value");
+ gTmplRadioElemChoiceLocked = pickerModeElement.getAttribute("disabled");
+ if (!gTmplRadioElemChoice) {
+ gTmplRadioElemChoice = gDefaultPickerMode;
+ }
+}
+
+/*
+ * Set Account and Folder elements based on the values read from
+ * preferences file. Default picker mode, if none specified at this stage, is
+ * set to 1 i.e., Other picker displaying the folder value read from the
+ * preferences file.
+ */
+function SetFolderDisplay(
+ pickerMode,
+ disableMode,
+ radioElemPrefix,
+ accountPickerId,
+ folderPickedField,
+ folderPickerId
+) {
+ if (!pickerMode) {
+ pickerMode = gDefaultPickerMode;
+ }
+
+ var selectAccountRadioId = radioElemPrefix + "_selectAccount";
+ var selectAccountRadioElem = document.getElementById(selectAccountRadioId);
+ var selectFolderRadioId = radioElemPrefix + "_selectFolder";
+ var selectFolderRadioElem = document.getElementById(selectFolderRadioId);
+ var accountPicker = document.getElementById(accountPickerId);
+ var folderPicker = document.getElementById(folderPickerId);
+ var rg = selectAccountRadioElem.radioGroup;
+ var folderPickedElement = document.getElementById(folderPickedField);
+ var uri = folderPickedElement.getAttribute("value");
+ // Get message folder from the given uri.
+ // There is no need to check for the existence of special folders as
+ // these folders are created on demand at runtime in case of imap accounts.
+ // For POP3 accounts, special folders are created at the account creation time.
+ var msgFolder = MailUtils.getOrCreateFolder(uri);
+ InitFolderDisplay(msgFolder.server.rootFolder, accountPicker);
+ InitFolderDisplay(msgFolder, folderPicker);
+
+ switch (pickerMode) {
+ case "0":
+ rg.selectedItem = selectAccountRadioElem;
+ SetPickerEnabling(accountPickerId, folderPickerId);
+ break;
+
+ case "1":
+ rg.selectedItem = selectFolderRadioElem;
+ SetPickerEnabling(folderPickerId, accountPickerId);
+ break;
+
+ default:
+ dump("Error in setting initial folder display on pickers\n");
+ break;
+ }
+
+ // Check to see if we need to lock page elements. Disable radio buttons
+ // and account/folder pickers when locked.
+ if (disableMode) {
+ selectAccountRadioElem.setAttribute("disabled", "true");
+ selectFolderRadioElem.setAttribute("disabled", "true");
+ accountPicker.setAttribute("disabled", "true");
+ folderPicker.setAttribute("disabled", "true");
+ }
+}
+
+// Initialize the folder display based on prefs values
+function InitFolderDisplay(folder, folderPicker) {
+ folderPicker.menupopup.selectFolder(folder);
+ folderPicker.folder = folder;
+}
+
+/**
+ * Capture any menulist changes and update the folder property.
+ *
+ * @param {string} aGroup - The prefix for the menulist we're handling
+ * (e.g. "drafts")
+ * @param {"Account"|"Folder"} aType - "Account" for the account picker or
+ * "Folder" for the folder picker.
+ * @param {Event} aEvent - The event that we're responding to.
+ */
+function noteSelectionChange(aGroup, aType, aEvent) {
+ var checkedElem = document.getElementById(aGroup + "_select" + aType);
+ var folder = aEvent.target._folder;
+ var modeValue = checkedElem.value;
+ var radioGroup = checkedElem.radioGroup.getAttribute("id");
+ var picker;
+
+ switch (radioGroup) {
+ case "doFcc":
+ gFccRadioElemChoice = modeValue;
+ picker = document.getElementById("msgFcc" + aType + "Picker");
+ break;
+
+ case "messageArchives":
+ gArchivesRadioElemChoice = modeValue;
+ picker = document.getElementById("msgArchives" + aType + "Picker");
+ updateArchiveHierarchyButton(folder);
+ break;
+
+ case "messageDrafts":
+ gDraftsRadioElemChoice = modeValue;
+ picker = document.getElementById("msgDrafts" + aType + "Picker");
+ break;
+
+ case "messageTemplates":
+ gTmplRadioElemChoice = modeValue;
+ picker = document.getElementById("msgStationery" + aType + "Picker");
+ break;
+ }
+
+ picker.folder = folder;
+ picker.menupopup.selectFolder(folder);
+}
+
+// Need to append special folders when account picker is selected.
+// Create a set of global special folder vars to be suffixed to the
+// server URI of the selected account.
+function SetSpecialFolderNamesWithDelims() {
+ var folderDelim = "/";
+ /* we use internal names known to everyone like "Sent", "Templates" and "Drafts" */
+
+ gFccFolderWithDelim = folderDelim + "Sent";
+ gArchivesFolderWithDelim = folderDelim + "Archives";
+ gDraftsFolderWithDelim = folderDelim + "Drafts";
+ gTemplatesFolderWithDelim = folderDelim + "Templates";
+}
+
+// Save all changes on this page
+function onSave() {
+ onSaveCopiesAndFolders();
+}
+
+function onSaveCopiesAndFolders() {
+ SaveFolderSettings(
+ gFccRadioElemChoice,
+ "doFcc",
+ gFccFolderWithDelim,
+ "msgFccAccountPicker",
+ "msgFccFolderPicker",
+ "identity.fccFolder",
+ "identity.fccFolderPickerMode"
+ );
+
+ SaveFolderSettings(
+ gArchivesRadioElemChoice,
+ "messageArchives",
+ gArchivesFolderWithDelim,
+ "msgArchivesAccountPicker",
+ "msgArchivesFolderPicker",
+ "identity.archiveFolder",
+ "identity.archivesFolderPickerMode"
+ );
+
+ SaveFolderSettings(
+ gDraftsRadioElemChoice,
+ "messageDrafts",
+ gDraftsFolderWithDelim,
+ "msgDraftsAccountPicker",
+ "msgDraftsFolderPicker",
+ "identity.draftFolder",
+ "identity.draftsFolderPickerMode"
+ );
+
+ SaveFolderSettings(
+ gTmplRadioElemChoice,
+ "messageTemplates",
+ gTemplatesFolderWithDelim,
+ "msgStationeryAccountPicker",
+ "msgStationeryFolderPicker",
+ "identity.stationeryFolder",
+ "identity.tmplFolderPickerMode"
+ );
+}
+
+// Save folder settings and radio element choices
+function SaveFolderSettings(
+ radioElemChoice,
+ radioGroupId,
+ folderSuffix,
+ accountPickerId,
+ folderPickerId,
+ folderElementId,
+ folderPickerModeId
+) {
+ var formElement = document.getElementById(folderElementId);
+ var uri;
+
+ if (
+ radioElemChoice == "0" ||
+ !document.getElementById(folderPickerId).value
+ ) {
+ // Default or revert to default if no folder chosen.
+ radioElemChoice = "0";
+ uri = document.getElementById(accountPickerId).folder.URI;
+ if (uri) {
+ // Create Folder URI.
+ uri = uri + folderSuffix;
+ }
+ } else if (radioElemChoice == "1") {
+ uri = document.getElementById(folderPickerId).folder.URI;
+ } else {
+ dump("Error saving folder preferences.\n");
+ return;
+ }
+
+ formElement.setAttribute("value", uri);
+
+ formElement = document.getElementById(folderPickerModeId);
+ formElement.setAttribute("value", radioElemChoice);
+}
+
+// Check the Fcc Self item and setup associated picker state
+function setupFccItems() {
+ let checked = document.getElementById("identity.doFcc").checked;
+ document.querySelectorAll(".depends-on-do-fcc").forEach(e => {
+ if (checked) {
+ e.removeAttribute("disabled");
+ } else {
+ e.setAttribute("disabled", "true");
+ }
+ });
+ if (!checked) {
+ return;
+ }
+
+ switch (gFccRadioElemChoice) {
+ case "0":
+ if (!gFccRadioElemChoiceLocked) {
+ SetPickerEnabling("msgFccAccountPicker", "msgFccFolderPicker");
+ }
+ SetRadioButtons("fcc_selectAccount", "fcc_selectFolder");
+ break;
+
+ case "1":
+ if (!gFccRadioElemChoiceLocked) {
+ SetPickerEnabling("msgFccFolderPicker", "msgFccAccountPicker");
+ }
+ SetRadioButtons("fcc_selectFolder", "fcc_selectAccount");
+ break;
+
+ default:
+ dump("Error in setting Fcc elements.\n");
+ break;
+ }
+}
+
+/**
+ * Handle the initial status and value of the Auto-Cc/Bcc text input fields.
+ *
+ * @param {string} checkboxId - The ID of an Auto-Cc/Bcc checkbox.
+ * @param {string} inputId - The ID of an Auto-Cc/Bcc text input element.
+ */
+function setupDoCcBccItems(checkboxId, inputId) {
+ // Enable address input according to the status of the checkbox.
+ let input = document.getElementById(inputId);
+ input.disabled = !document.getElementById(checkboxId).checked;
+ // Safeguard against space-padded address list to ensure list visibility.
+ input.value = input.value.trim();
+}
+
+/**
+ * Handle the command event of the Auto-Cc/Bcc checkboxes.
+ * Disable the respective text input element if the checkbox is not checked, and
+ * handle the default value of the input when the checkbox is toggled.
+ *
+ * @param {Event} event - The command event of the checkbox.
+ */
+function identityDoCcBccOnCommand(event) {
+ let checkbox = event.target;
+ let checked = checkbox.checked;
+ // For checkboxes #identity.doCc and #identity.doBcc, get the corresponding
+ // inputs: #identity.doCcList and #identity.doBccList.
+ let input = document.getElementById(`${checkbox.id}List`);
+ input.disabled = !checked;
+
+ // User toggled checkbox.
+ let identityEmailAddress = document.getElementById("identity.email").value;
+ if (checked) {
+ // If user checks the checkbox and there's no address, default to identity's
+ // email address.
+ if (!input.value) {
+ input.value = identityEmailAddress;
+ }
+ input.select();
+ return;
+ }
+
+ if (input.value == identityEmailAddress) {
+ // If user unchecks checkbox and the input has default address, clear input.
+ input.value = "";
+ }
+}
+
+/**
+ * Handle the blur event of the Auto-Cc/Bcc checkboxes.
+ *
+ * @param {Event} event - The blur event of the checkbox.
+ */
+function identityDoCcBccOnBlur(event) {
+ let input = event.target;
+ // Safeguard against space-padded address list to ensure list visibility.
+ input.value = input.value.trim();
+}
+
+// Enable and disable pickers based on the radio element clicked
+function SetPickerEnabling(enablePickerId, disablePickerId) {
+ var activePicker = document.getElementById(enablePickerId);
+ activePicker.removeAttribute("disabled");
+
+ var inactivePicker = document.getElementById(disablePickerId);
+ inactivePicker.setAttribute("disabled", "true");
+}
+
+// Set radio element choices and picker states
+function setPickersState(enablePickerId, disablePickerId, event) {
+ SetPickerEnabling(enablePickerId, disablePickerId);
+
+ var radioElemValue = event.target.value;
+
+ switch (event.target.id) {
+ case "fcc_selectAccount":
+ case "fcc_selectFolder":
+ gFccRadioElemChoice = radioElemValue;
+ break;
+ case "archive_selectAccount":
+ case "archive_selectFolder":
+ gArchivesRadioElemChoice = radioElemValue;
+ updateArchiveHierarchyButton(
+ document.getElementById(enablePickerId).folder
+ );
+ break;
+ case "draft_selectAccount":
+ case "draft_selectFolder":
+ gDraftsRadioElemChoice = radioElemValue;
+ break;
+ case "tmpl_selectAccount":
+ case "tmpl_selectFolder":
+ gTmplRadioElemChoice = radioElemValue;
+ break;
+ default:
+ dump("Error in setting picker state.\n");
+ }
+}
+
+// This routine is to restore the correct radio element
+// state when the fcc self checkbox broadcasts the change
+function SetRadioButtons(selectPickerId, unselectPickerId) {
+ var activeRadioElem = document.getElementById(selectPickerId);
+ activeRadioElem.radioGroup.selectedItem = activeRadioElem;
+}
+
+/**
+ * Enable/disable the archive hierarchy button depending on what folder is
+ * currently selected (Gmail IMAP folders should have the button disabled, since
+ * changing the archive hierarchy does nothing there.
+ *
+ * @param {nsIMsgFolder} archiveFolder - The currently-selected folder to store
+ * archives in
+ */
+function updateArchiveHierarchyButton(archiveFolder) {
+ let isGmailImap =
+ archiveFolder.server.type == "imap" &&
+ archiveFolder.server.QueryInterface(Ci.nsIImapIncomingServer).isGMailServer;
+ document.getElementById("archiveHierarchyButton").disabled = isGmailImap;
+}
+
+/**
+ * Enable or disable (as appropriate) the controls for setting archive options
+ */
+function setupArchiveItems() {
+ let checked = document.getElementById("identity.archiveEnabled").checked;
+ document.querySelectorAll(".depends-on-archive").forEach(e => {
+ if (checked) {
+ e.removeAttribute("disabled");
+ } else {
+ e.setAttribute("disabled", "true");
+ }
+ });
+ if (!checked) {
+ return;
+ }
+
+ switch (gArchivesRadioElemChoice) {
+ case "0":
+ if (!gArchivesRadioElemChoiceLocked) {
+ SetPickerEnabling(
+ "msgArchivesAccountPicker",
+ "msgArchivesFolderPicker"
+ );
+ }
+ SetRadioButtons("archive_selectAccount", "archive_selectFolder");
+ updateArchiveHierarchyButton(
+ document.getElementById("msgArchivesAccountPicker").folder
+ );
+ break;
+
+ case "1":
+ if (!gArchivesRadioElemChoiceLocked) {
+ SetPickerEnabling(
+ "msgArchivesFolderPicker",
+ "msgArchivesAccountPicker"
+ );
+ }
+ SetRadioButtons("archive_selectFolder", "archive_selectAccount");
+ updateArchiveHierarchyButton(
+ document.getElementById("msgArchivesFolderPicker").folder
+ );
+ break;
+
+ default:
+ dump("Error in setting Archive elements.\n");
+ }
+}
+
+/**
+ * Open a dialog to edit the folder hierarchy used when archiving messages.
+ */
+function ChangeArchiveHierarchy() {
+ let identity = parent.gIdentity || parent.getCurrentAccount().defaultIdentity;
+ let arg = { identity };
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/am-archiveoptions.xhtml",
+ undefined,
+ arg
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-copies.xhtml b/comm/mailnews/base/prefs/content/am-copies.xhtml
new file mode 100644
index 0000000000..c142cb8eb5
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-copies.xhtml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-copies.dtd">
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml">
+<head>
+ <title>&copyAndFolderTitle.label;</title>
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-copies.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => { parent.onPanelLoaded('am-copies.xhtml'); });
+ </script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <vbox id="containerBox" flex="1">
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&copyAndFolderTitle.label;"/>
+ </hbox>
+#include am-copies.inc.xhtml
+ </vbox>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-identities-list.js b/comm/mailnews/base/prefs/content/am-identities-list.js
new file mode 100644
index 0000000000..02d20e1434
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-identities-list.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/. */
+
+var { Gloda } = ChromeUtils.import("resource:///modules/gloda/Gloda.jsm");
+
+var gIdentityListBox; // the root <richlistbox> node
+var gAddButton;
+var gEditButton;
+var gSetDefaultButton;
+var gDeleteButton;
+
+var gAccount = null; // the account we are showing the identities for
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+document.addEventListener("dialogaccept", onOk);
+document.addEventListener("dialogcancel", onOk);
+
+function onLoad() {
+ gIdentityListBox = document.getElementById("identitiesList");
+ gAddButton = document.getElementById("cmd_add");
+ gEditButton = document.getElementById("cmd_edit");
+ gSetDefaultButton = document.getElementById("cmd_default");
+ gDeleteButton = document.getElementById("cmd_delete");
+
+ // extract the account
+ gAccount = window.arguments[0].account;
+
+ var accountName = window.arguments[0].accountName;
+ document.title = document
+ .getElementById("bundle_prefs")
+ .getFormattedString("identity-list-title", [accountName]);
+
+ refreshIdentityList(0);
+}
+
+/**
+ * Rebuilds the listbox holding the list of identities.
+ *
+ * @param {number} aSelectIndex - Attempt to select the identity with this index.
+ */
+function refreshIdentityList(aSelectIndex) {
+ // Remove all children.
+ while (gIdentityListBox.hasChildNodes()) {
+ gIdentityListBox.lastChild.remove();
+ }
+
+ // Build the list from the identities array.
+ for (let identity of gAccount.identities) {
+ if (identity.valid) {
+ let label = document.createXULElement("label");
+ label.setAttribute("value", identity.identityName);
+
+ let listitem = document.createXULElement("richlistitem");
+ listitem.appendChild(label);
+ listitem.setAttribute("key", identity.key);
+ gIdentityListBox.appendChild(listitem);
+ }
+ }
+
+ // Ensure one identity is always selected.
+ if (!aSelectIndex || aSelectIndex < 0) {
+ aSelectIndex = 0;
+ } else if (aSelectIndex >= gIdentityListBox.itemCount) {
+ aSelectIndex = gIdentityListBox.itemCount - 1;
+ }
+
+ // This also fires the onselect event, which in turn calls updateButtons().
+ gIdentityListBox.selectedIndex = aSelectIndex;
+}
+
+/**
+ * Opens the identity editor dialog.
+ *
+ * @param {nsIMsgIdentity} identity - The identity (if any) to load in the dialog.
+ */
+function openIdentityEditor(identity) {
+ let args = { identity, account: gAccount, result: false };
+
+ let indexToSelect = identity
+ ? gIdentityListBox.selectedIndex
+ : gIdentityListBox.itemCount;
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/am-identity-edit.xhtml",
+ { closingCallback: onCloseIdentity },
+ args
+ );
+
+ function onCloseIdentity() {
+ if (args.result) {
+ refreshIdentityList(indexToSelect);
+ }
+ }
+}
+
+function getSelectedIdentity() {
+ if (gIdentityListBox.selectedItems.length != 1) {
+ return null;
+ }
+
+ let identityKey = gIdentityListBox.selectedItems[0].getAttribute("key");
+ return (
+ gAccount.identities.find(id => id.valid && id.key == identityKey) || null
+ );
+}
+
+function onEdit(event) {
+ var id = getSelectedIdentity();
+ openIdentityEditor(id);
+}
+
+/**
+ * Enable/disable buttons depending on number of identities and current selection.
+ */
+function updateButtons() {
+ // In this listbox there should always be one item selected.
+ if (
+ gIdentityListBox.selectedItems.length != 1 ||
+ gIdentityListBox.itemCount == 0
+ ) {
+ // But in case this is not met (e.g. there is no identity for some reason,
+ // or the list is being rebuilt), disable all buttons.
+ gEditButton.setAttribute("disabled", "true");
+ gDeleteButton.setAttribute("disabled", "true");
+ gSetDefaultButton.setAttribute("disabled", "true");
+ return;
+ }
+
+ gEditButton.setAttribute("disabled", "false");
+ gDeleteButton.setAttribute(
+ "disabled",
+ gIdentityListBox.itemCount <= 1 ? "true" : "false"
+ );
+ gSetDefaultButton.setAttribute(
+ "disabled",
+ gIdentityListBox.selectedIndex == 0 ? "true" : "false"
+ );
+ // The Add command is always enabled.
+}
+
+function onSetDefault(event) {
+ let identity = getSelectedIdentity();
+ if (!identity) {
+ return;
+ }
+
+ // If the first identity is selected, there is nothing to do.
+ if (gIdentityListBox.selectedIndex == 0) {
+ return;
+ }
+
+ gAccount.defaultIdentity = identity;
+ // Rebuilt the identity list and select the moved identity again.
+ refreshIdentityList(0);
+ // Update gloda's myContact with the new default identity.
+ Gloda._initMyIdentities();
+}
+
+function onDelete(event) {
+ if (gIdentityListBox.itemCount <= 1) {
+ // don't support deleting the last identity
+ return;
+ }
+
+ // get delete confirmation
+ let selectedIdentity = getSelectedIdentity();
+
+ let prefsBundle = document.getElementById("bundle_prefs");
+ let confirmTitle = prefsBundle.getFormattedString(
+ "identity-delete-confirm-title",
+ [window.arguments[0].accountName]
+ );
+ let confirmText = prefsBundle.getFormattedString("identity-delete-confirm", [
+ selectedIdentity.identityName,
+ ]);
+ let confirmButton = prefsBundle.getString("identity-delete-confirm-button");
+
+ if (
+ Services.prompt.confirmEx(
+ window,
+ confirmTitle,
+ confirmText,
+ Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_CANCEL,
+ confirmButton,
+ null,
+ null,
+ null,
+ {}
+ )
+ ) {
+ return;
+ }
+
+ let selectedItemIndex = gIdentityListBox.selectedIndex;
+
+ gAccount.removeIdentity(selectedIdentity);
+
+ refreshIdentityList(selectedItemIndex);
+}
+
+function onOk() {
+ window.arguments[0].result = true;
+}
diff --git a/comm/mailnews/base/prefs/content/am-identities-list.xhtml b/comm/mailnews/base/prefs/content/am-identities-list.xhtml
new file mode 100644
index 0000000000..243630bd75
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-identities-list.xhtml
@@ -0,0 +1,101 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/shared/grid-layout.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-identities-list.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="min-width: 600px"
+ scrolling="false"
+>
+ <head>
+ <title><!-- identity-list-title --></title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-identities-list.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog
+ buttons="accept"
+ buttonlabelaccept="&identitiesListClose.label;"
+ buttonaccesskeyaccept="&identitiesListClose.accesskey;"
+ style="height: 100vh"
+ >
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+
+ <commandset>
+ <command id="cmd_add" oncommand="openIdentityEditor(null);" />
+ <command id="cmd_edit" oncommand="onEdit(event);" disabled="true" />
+ <command
+ id="cmd_default"
+ oncommand="onSetDefault(event);"
+ disabled="true"
+ />
+ <command id="cmd_delete" oncommand="onDelete(event);" disabled="true" />
+ </commandset>
+
+ <keyset>
+ <key keycode="VK_INSERT" command="cmd_add" />
+ <key keycode="VK_DELETE" command="cmd_delete" />
+ </keyset>
+
+ <label control="identitiesList">&identitiesListManageDesc.label;</label>
+
+ <separator class="thin" />
+
+ <html:div class="grid-two-column-auto-min" style="flex: 1">
+ <richlistbox
+ id="identitiesList"
+ ondblclick="onEdit(event);"
+ onselect="updateButtons();"
+ seltype="single"
+ flex="1"
+ style="min-height: 10em; max-height: 20em"
+ />
+
+ <html:div class="flex-content-column">
+ <button
+ id="addButton"
+ command="cmd_add"
+ label="&identitiesListAdd.label;"
+ accesskey="&identitiesListAdd.accesskey;"
+ />
+ <button
+ id="editButton"
+ disabled="true"
+ command="cmd_edit"
+ label="&identitiesListEdit.label;"
+ accesskey="&identitiesListEdit.accesskey;"
+ />
+ <button
+ id="setDefaultButton"
+ disabled="true"
+ command="cmd_default"
+ label="&identitiesListDefault.label;"
+ accesskey="&identitiesListDefault.accesskey;"
+ />
+ <button
+ id="deleteButton"
+ disabled="true"
+ command="cmd_delete"
+ label="&identitiesListDelete.label;"
+ accesskey="&identitiesListDelete.accesskey;"
+ />
+ </html:div>
+ </html:div>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-identity-edit.js b/comm/mailnews/base/prefs/content/am-identity-edit.js
new file mode 100644
index 0000000000..216edcaa53
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-identity-edit.js
@@ -0,0 +1,577 @@
+/* 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 am-addressing.js */
+/* import-globals-from am-copies.js */
+/* import-globals-from ../../../../mail/extensions/am-e2e/am-e2e.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gIdentity = null; // the identity we are editing (may be null for a new identity)
+var gAccount = null; // the account the identity is (or will be) associated with
+
+document.addEventListener("dialogaccept", onOk);
+
+function onLoadIdentityProperties() {
+ // extract the account
+ gIdentity = window.arguments[0].identity;
+ gAccount = window.arguments[0].account;
+ let prefBundle = document.getElementById("bundle_prefs");
+
+ if (gIdentity) {
+ let listName = gIdentity.identityName;
+ document.title = prefBundle.getFormattedString("identityDialogTitleEdit", [
+ listName,
+ ]);
+ } else {
+ document.title = prefBundle.getString("identityDialogTitleAdd");
+ }
+
+ loadSMTPServerList();
+
+ initIdentityValues(gIdentity);
+ initCopiesAndFolder(gIdentity);
+ initCompositionAndAddressing(gIdentity);
+ initE2EEncryption(gIdentity);
+
+ // E2E needs an email... hide until we have one.
+ document.getElementById("identityE2ETab").hidden = !gIdentity?.email;
+}
+
+// based on the values of gIdentity, initialize the identity fields we expose to the user
+function initIdentityValues(identity) {
+ function initSmtpServer(aServerKey) {
+ // Select a server in the SMTP server menulist by its key.
+ // The value of the identity.smtpServerKey is null when the
+ // "use default server" option is used so, if we get that passed in, select
+ // the useDefaultItem representing this option by using the value of "".
+ document.getElementById("identity.smtpServerKey").value = aServerKey || "";
+ }
+
+ if (identity) {
+ document.getElementById("identity.fullName").value = identity.fullName;
+ document.getElementById("identity.email").value = identity.email;
+ document.getElementById("identity.replyTo").value = identity.replyTo;
+ document.getElementById("identity.organization").value =
+ identity.organization;
+ document.getElementById("identity.attachSignature").checked =
+ identity.attachSignature;
+ document.getElementById("identity.htmlSigText").value =
+ identity.htmlSigText;
+ document.getElementById("identity.htmlSigFormat").checked =
+ identity.htmlSigFormat;
+
+ if (identity.signature) {
+ document.getElementById("identity.signature").value =
+ identity.signature.path;
+ }
+
+ document.getElementById("identity.attachVCard").checked =
+ identity.attachVCard;
+ document.getElementById("identity.escapedVCard").value =
+ identity.escapedVCard || "";
+
+ document.getElementById("identity.catchAll").checked = identity.catchAll;
+ document.getElementById("identity.catchAllHint").value =
+ identity.catchAllHint;
+
+ initSmtpServer(identity.smtpServerKey);
+
+ // In am-main.xhtml this field has no ID, because it's hidden by other means.
+ let catchAllBox = document.getElementById("identityCatchAllBox");
+ if (catchAllBox) {
+ let servers = MailServices.accounts.getServersForIdentity(identity);
+ catchAllBox.hidden = servers.length > 0 && servers[0].type == "nntp";
+ }
+
+ // This field does not exist for the default identity shown in the am-main.xhtml pane.
+ let idLabel = document.getElementById("identity.label");
+ if (idLabel) {
+ idLabel.value = identity.label;
+ }
+ } else {
+ // We're adding an identity, use the best default we have.
+ initSmtpServer(gAccount.defaultIdentity.smtpServerKey);
+
+ // Hide catchAll until we know what this identitity is associated with.
+ document.getElementById("identityCatchAllBox").hidden = true;
+ }
+
+ setupSignatureItems();
+}
+
+function initCopiesAndFolder(identity) {
+ // if we are editing an existing identity, use it...otherwise copy our values from the default identity
+ var copiesAndFoldersIdentity = identity ? identity : gAccount.defaultIdentity;
+
+ document.getElementById("identity.fccFolder").value =
+ copiesAndFoldersIdentity.fccFolder;
+ document.getElementById("identity.draftFolder").value =
+ copiesAndFoldersIdentity.draftFolder;
+ document.getElementById("identity.archiveFolder").value =
+ copiesAndFoldersIdentity.archiveFolder;
+ document.getElementById("identity.stationeryFolder").value =
+ copiesAndFoldersIdentity.stationeryFolder;
+
+ document.getElementById("identity.fccFolderPickerMode").value =
+ copiesAndFoldersIdentity.fccFolderPickerMode
+ ? copiesAndFoldersIdentity.fccFolderPickerMode
+ : 0;
+ document.getElementById("identity.draftsFolderPickerMode").value =
+ copiesAndFoldersIdentity.draftsFolderPickerMode
+ ? copiesAndFoldersIdentity.draftsFolderPickerMode
+ : 0;
+ document.getElementById("identity.archivesFolderPickerMode").value =
+ copiesAndFoldersIdentity.archivesFolderPickerMode
+ ? copiesAndFoldersIdentity.archivesFolderPickerMode
+ : 0;
+ document.getElementById("identity.tmplFolderPickerMode").value =
+ copiesAndFoldersIdentity.tmplFolderPickerMode
+ ? copiesAndFoldersIdentity.tmplFolderPickerMode
+ : 0;
+
+ document.getElementById("identity.doCc").checked =
+ copiesAndFoldersIdentity.doCc;
+ document.getElementById("identity.doCcList").value =
+ copiesAndFoldersIdentity.doCcList;
+ document.getElementById("identity.doBcc").checked =
+ copiesAndFoldersIdentity.doBcc;
+ document.getElementById("identity.doBccList").value =
+ copiesAndFoldersIdentity.doBccList;
+ document.getElementById("identity.doFcc").checked =
+ copiesAndFoldersIdentity.doFcc;
+ document.getElementById("identity.fccReplyFollowsParent").checked =
+ copiesAndFoldersIdentity.fccReplyFollowsParent;
+ document.getElementById("identity.showSaveMsgDlg").checked =
+ copiesAndFoldersIdentity.showSaveMsgDlg;
+ document.getElementById("identity.archiveEnabled").checked =
+ copiesAndFoldersIdentity.archiveEnabled;
+
+ onInitCopiesAndFolders(); // am-copies.js method
+}
+
+function initCompositionAndAddressing(identity) {
+ // if we are editing an existing identity, use it...otherwise copy our values from the default identity
+ var addressingIdentity = identity ? identity : gAccount.defaultIdentity;
+
+ document.getElementById("identity.directoryServer").value =
+ addressingIdentity.directoryServer;
+ document.getElementById("identity.overrideGlobal_Pref").value =
+ addressingIdentity.overrideGlobalPref;
+ let autoCompleteElement = document.getElementById(
+ "identity.autocompleteToMyDomain"
+ );
+ if (autoCompleteElement) {
+ // Thunderbird does not have this element.
+ autoCompleteElement.checked = addressingIdentity.autocompleteToMyDomain;
+ }
+
+ document.getElementById("identity.composeHtml").checked =
+ addressingIdentity.composeHtml;
+ document.getElementById("identity.autoQuote").checked =
+ addressingIdentity.autoQuote;
+ document.getElementById("identity.replyOnTop").value =
+ addressingIdentity.replyOnTop;
+ document.getElementById("identity.sig_bottom").value =
+ addressingIdentity.sigBottom;
+ document.getElementById("identity.sig_on_reply").checked =
+ addressingIdentity.sigOnReply;
+ document.getElementById("identity.sig_on_fwd").checked =
+ addressingIdentity.sigOnForward;
+
+ onInitCompositionAndAddressing(); // am-addressing.js method
+}
+
+function onOk(event) {
+ if (!validEmailAddress()) {
+ event.preventDefault();
+ return;
+ }
+
+ // if we are adding a new identity, create an identity, set the fields and add it to the
+ // account.
+ if (!gIdentity) {
+ // ask the account manager to create a new identity for us
+ gIdentity = MailServices.accounts.createIdentity();
+
+ // copy in the default identity settings so we inherit lots of stuff like the default drafts folder, etc.
+ gIdentity.copy(gAccount.defaultIdentity);
+
+ // assume the identity is valid by default?
+ gIdentity.valid = true;
+
+ // add the identity to the account
+ gAccount.addIdentity(gIdentity);
+
+ // now fall through to saveFields which will save our new values
+ }
+
+ // if we are modifying an existing identity, save the fields
+ saveIdentitySettings(gIdentity);
+ saveCopiesAndFolderSettings(gIdentity);
+ saveAddressingAndCompositionSettings(gIdentity);
+ saveE2EEncryptionSettings(gIdentity);
+
+ window.arguments[0].result = true;
+}
+
+// returns false and prompts the user if
+// the identity does not have an email address
+function validEmailAddress() {
+ var emailAddress = document.getElementById("identity.email").value;
+
+ // quickly test for an @ sign to test for an email address. We don't have
+ // to be anymore precise than that.
+ if (!emailAddress.includes("@")) {
+ // alert user about an invalid email address
+
+ var prefBundle = document.getElementById("bundle_prefs");
+
+ Services.prompt.alert(
+ window,
+ prefBundle.getString("identity-edit-req-title"),
+ prefBundle.getString("identity-edit-req")
+ );
+ return false;
+ }
+
+ return true;
+}
+
+function saveIdentitySettings(identity) {
+ if (identity) {
+ let idLabel = document.getElementById("identity.label");
+ if (idLabel) {
+ identity.label = idLabel.value;
+ }
+ identity.fullName = document.getElementById("identity.fullName").value;
+ identity.email = document.getElementById("identity.email").value;
+ identity.replyTo = document.getElementById("identity.replyTo").value;
+ identity.organization = document.getElementById(
+ "identity.organization"
+ ).value;
+ identity.attachSignature = document.getElementById(
+ "identity.attachSignature"
+ ).checked;
+ identity.htmlSigText = document.getElementById(
+ "identity.htmlSigText"
+ ).value;
+ identity.htmlSigFormat = document.getElementById(
+ "identity.htmlSigFormat"
+ ).checked;
+
+ identity.attachVCard = document.getElementById(
+ "identity.attachVCard"
+ ).checked;
+ identity.escapedVCard = document.getElementById(
+ "identity.escapedVCard"
+ ).value;
+ identity.catchAll = document.getElementById("identity.catchAll").checked;
+ identity.catchAllHint = document.getElementById(
+ "identity.catchAllHint"
+ ).value;
+ identity.smtpServerKey = document.getElementById(
+ "identity.smtpServerKey"
+ ).value;
+
+ let attachSignaturePath =
+ document.getElementById("identity.signature").value;
+ identity.signature = null; // this is important so we don't accidentally inherit the default
+
+ if (attachSignaturePath) {
+ // convert signature path back into a nsIFile
+ var sfile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ sfile.initWithPath(attachSignaturePath);
+ if (sfile.exists()) {
+ identity.signature = sfile;
+ }
+ }
+ }
+}
+
+function saveCopiesAndFolderSettings(identity) {
+ onSaveCopiesAndFolders(); // am-copies.js routine
+
+ identity.fccFolder = document.getElementById("identity.fccFolder").value;
+ identity.draftFolder = document.getElementById("identity.draftFolder").value;
+ identity.archiveFolder = document.getElementById(
+ "identity.archiveFolder"
+ ).value;
+ identity.stationeryFolder = document.getElementById(
+ "identity.stationeryFolder"
+ ).value;
+ identity.fccFolderPickerMode = document.getElementById(
+ "identity.fccFolderPickerMode"
+ ).value;
+ identity.draftsFolderPickerMode = document.getElementById(
+ "identity.draftsFolderPickerMode"
+ ).value;
+ identity.archivesFolderPickerMode = document.getElementById(
+ "identity.archivesFolderPickerMode"
+ ).value;
+ identity.tmplFolderPickerMode = document.getElementById(
+ "identity.tmplFolderPickerMode"
+ ).value;
+ identity.doCc = document.getElementById("identity.doCc").checked;
+ identity.doCcList = document.getElementById("identity.doCcList").value;
+ identity.doBcc = document.getElementById("identity.doBcc").checked;
+ identity.doBccList = document.getElementById("identity.doBccList").value;
+ identity.doFcc = document.getElementById("identity.doFcc").checked;
+ identity.fccReplyFollowsParent = document.getElementById(
+ "identity.fccReplyFollowsParent"
+ ).checked;
+ identity.showSaveMsgDlg = document.getElementById(
+ "identity.showSaveMsgDlg"
+ ).checked;
+ identity.archiveEnabled = document.getElementById(
+ "identity.archiveEnabled"
+ ).checked;
+}
+
+function saveAddressingAndCompositionSettings(identity) {
+ identity.directoryServer = document.getElementById(
+ "identity.directoryServer"
+ ).value;
+ identity.overrideGlobalPref =
+ document.getElementById("identity.overrideGlobal_Pref").value == "true";
+ let autoCompleteElement = document.getElementById(
+ "identity.autocompleteToMyDomain"
+ );
+ if (autoCompleteElement) {
+ // Thunderbird does not have this element.
+ identity.autocompleteToMyDomain = autoCompleteElement.checked;
+ }
+ identity.composeHtml = document.getElementById(
+ "identity.composeHtml"
+ ).checked;
+ identity.autoQuote = document.getElementById("identity.autoQuote").checked;
+ identity.replyOnTop = document.getElementById("identity.replyOnTop").value;
+ identity.sigBottom =
+ document.getElementById("identity.sig_bottom").value == "true";
+ identity.sigOnReply = document.getElementById(
+ "identity.sig_on_reply"
+ ).checked;
+ identity.sigOnForward = document.getElementById(
+ "identity.sig_on_fwd"
+ ).checked;
+}
+
+function selectFile() {
+ const nsIFilePicker = Ci.nsIFilePicker;
+
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+
+ var prefBundle = document.getElementById("bundle_prefs");
+ var title = prefBundle.getString("choosefile");
+ fp.init(window, title, nsIFilePicker.modeOpen);
+ fp.appendFilters(nsIFilePicker.filterAll);
+
+ // Get current signature folder, if there is one.
+ // We can set that to be the initial folder so that users
+ // can maintain their signatures better.
+ var sigFolder = GetSigFolder();
+ if (sigFolder) {
+ fp.displayDirectory = sigFolder;
+ }
+
+ fp.open(rv => {
+ if (rv != nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+ document.getElementById("identity.signature").value = fp.file.path;
+ document
+ .getElementById("identity.signature")
+ .dispatchEvent(new CustomEvent("change"));
+ });
+}
+
+/**
+ * Adjust the catch-all hint so that is removes stars from the allowed pattern.
+ * We only allow to use stars for matching full domains *@example.com,
+ * not *foo@example.com.
+ *
+ * @param {Event} event - the oninput event of the catchAllHint input field.
+ */
+function handleInputCatchAllHint(event) {
+ let value = event.target.value;
+ event.target.value = value
+ .replace(/(\*[^@]+)/g, "*")
+ .replace(/(^|\s)@/g, "$1*@")
+ .replace(/\s*[;,]/g, ",")
+ .replace(/\s+/g, " ");
+}
+
+function GetSigFolder() {
+ var sigFolder = null;
+ try {
+ var account = parent.getCurrentAccount();
+ var identity = account.defaultIdentity;
+ var signatureFile = identity.signature;
+
+ if (signatureFile) {
+ signatureFile = signatureFile.QueryInterface(Ci.nsIFile);
+ sigFolder = signatureFile.parent;
+
+ if (!sigFolder.exists()) {
+ sigFolder = null;
+ }
+ }
+ } catch (ex) {
+ dump("failed to get signature folder..\n");
+ }
+ return sigFolder;
+}
+
+// Signature textbox is active unless option to select from file is checked.
+// If a signature is need to be attached, the associated items which
+// displays the absolute path to the signature (in a textbox) and the way
+// to select a new signature file (a button) are enabled. Otherwise, they
+// are disabled. Check to see if the attachSignature is locked to block
+// broadcasting events.
+function setupSignatureItems() {
+ var signature = document.getElementById("identity.signature");
+ var browse = document.getElementById("identity.sigbrowsebutton");
+ var htmlSigText = document.getElementById("identity.htmlSigText");
+ var htmlSigFormat = document.getElementById("identity.htmlSigFormat");
+ var attachSignature = document.getElementById("identity.attachSignature");
+ var checked = attachSignature.checked;
+
+ if (checked) {
+ htmlSigText.setAttribute("disabled", "disabled");
+ htmlSigFormat.setAttribute("disabled", "true");
+ } else {
+ htmlSigText.removeAttribute("disabled");
+ htmlSigFormat.removeAttribute("disabled");
+ }
+
+ if (checked && !getAccountValueIsLocked(signature)) {
+ signature.removeAttribute("disabled");
+ } else {
+ signature.setAttribute("disabled", "disabled");
+ }
+
+ if (checked && !getAccountValueIsLocked(browse)) {
+ browse.removeAttribute("disabled");
+ } else {
+ browse.setAttribute("disabled", "true");
+ }
+}
+
+function editVCard() {
+ // Read vCard hidden value from UI.
+ let escapedVCard = document.getElementById("identity.escapedVCard");
+ let dialog = top.document.getElementById("editVCardDialog");
+ let form = dialog.querySelector("form");
+ let vCardEdit = dialog.querySelector("vcard-edit");
+
+ vCardEdit.vCardString = decodeURIComponent(escapedVCard.value);
+
+ top.addEventListener("keydown", editVCardKeyDown, { capture: true });
+ form.addEventListener("submit", editVCardSubmit);
+ form.addEventListener("reset", editVCardReset);
+
+ top.gSubDialog._topDialog?._overlay.removeAttribute("topmost");
+ dialog.showModal();
+}
+
+function editVCardKeyDown(event) {
+ let dialog = top.document.getElementById("editVCardDialog");
+ if (event.keyCode == KeyboardEvent.DOM_VK_ESCAPE && dialog.open) {
+ // This is a bit of a hack to prevent other dialogs (particularly
+ // SubDialogs) from closing when the vCard dialog is open.
+ event.preventDefault();
+ editVCardReset();
+ }
+}
+
+function editVCardSubmit(event) {
+ let escapedVCard = document.getElementById("identity.escapedVCard");
+ let dialog = top.document.getElementById("editVCardDialog");
+ let form = dialog.querySelector("form");
+ let vCardEdit = dialog.querySelector("vcard-edit");
+
+ vCardEdit.saveVCard();
+ escapedVCard.value = encodeURIComponent(vCardEdit.vCardString);
+ // Trigger a change event so for the am-main view the event listener
+ // set up in AccountManager.js onLoad() can make sure to save the change.
+ escapedVCard.dispatchEvent(new CustomEvent("change"));
+
+ top.gSubDialog._topDialog?._overlay.setAttribute("topmost", "true");
+ dialog.close();
+
+ event.preventDefault();
+ form.removeEventListener("submit", editVCardSubmit);
+ form.removeEventListener("reset", editVCardReset);
+}
+
+function editVCardReset() {
+ let dialog = top.document.getElementById("editVCardDialog");
+ let form = dialog.querySelector("form");
+
+ top.gSubDialog._topDialog?._overlay.setAttribute("topmost", "true");
+ dialog.close();
+
+ form.removeEventListener("submit", editVCardSubmit);
+ form.removeEventListener("reset", editVCardReset);
+}
+
+function getAccountForFolderPickerState() {
+ return gAccount;
+}
+
+/**
+ * Build the SMTP server list for display.
+ */
+function loadSMTPServerList() {
+ var smtpServerList = document.getElementById("identity.smtpServerKey");
+ let defaultServer = MailServices.smtp.defaultServer;
+ let currentValue = smtpServerList.value;
+
+ var smtpPopup = smtpServerList.menupopup;
+ while (smtpPopup.lastChild.nodeName != "menuseparator") {
+ smtpPopup.lastChild.remove();
+ }
+
+ for (let server of MailServices.smtp.servers) {
+ let serverName = "";
+ if (server.description) {
+ serverName = server.description + " - ";
+ } else if (server.username) {
+ serverName = server.username + " - ";
+ }
+ serverName += server.hostname;
+
+ if (defaultServer.key == server.key) {
+ serverName +=
+ " " +
+ document
+ .getElementById("bundle_messenger")
+ .getString("defaultServerTag");
+ }
+
+ smtpServerList.appendItem(serverName, server.key);
+ }
+
+ smtpServerList.value = currentValue;
+}
+
+/**
+ * Open dialog for editing properties of currently selected SMTP server.
+ */
+function editCurrentSMTP() {
+ let smtpKey = document.getElementById("identity.smtpServerKey").value;
+ let server =
+ smtpKey === ""
+ ? MailServices.smtp.defaultServer
+ : MailServices.smtp.getServerByKey(smtpKey);
+ let args = { server, result: false, addSmtpServer: "" };
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/SmtpServerEdit.xhtml",
+ { closingCallback: loadSMTPServerList },
+ args
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-identity-edit.xhtml b/comm/mailnews/base/prefs/content/am-identity-edit.xhtml
new file mode 100644
index 0000000000..d967b18202
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-identity-edit.xhtml
@@ -0,0 +1,239 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/openpgp/inlineNotification.css" type="text/css"?>
+
+<!DOCTYPE html [
+<!ENTITY % identityEditDTD SYSTEM "chrome://messenger/locale/am-identity-edit.dtd" >
+%identityEditDTD;
+<!ENTITY % identityDTD SYSTEM "chrome://messenger/locale/am-main.dtd" >
+%identityDTD;
+<!ENTITY % copiesDTD SYSTEM "chrome://messenger/locale/am-copies.dtd">
+%copiesDTD;
+<!ENTITY % addressingDTD SYSTEM "chrome://messenger/locale/am-addressing.dtd" >
+%addressingDTD;
+<!ENTITY % e2eDTD SYSTEM "chrome://messenger/locale/am-smime.dtd" >
+%e2eDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+ style="height: 900px; min-width: 800px;">
+<head>
+ <title><!-- identityDialogTitleEdit --></title>
+ <script defer="defer" src="chrome://messenger/content/globalOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/editMenuOverlay.js"></script>
+ <script defer="defer" src="chrome://global/content/preferencesBindings.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-copies.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-addressing.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-e2e.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-identity-edit.js"></script>
+ <script>
+ // NOTE: am-identity-edit.js is not only used for this page.
+ window.addEventListener("DOMContentLoaded", onLoadIdentityProperties);
+ </script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog id="identityDialog">
+ <stringbundle id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"/>
+ <stringbundle id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"/>
+
+ <tabbox flex="1" style="overflow: hidden;">
+ <tabs id="identitySettings">
+ <tab id="identitySettingsTab" label="&settingsTab.label;"/>
+ <tab id="identityCopiesFoldersTab" label="&copiesFoldersTab.label;"/>
+ <tab id="identityAddressingTab" label="&addressingTab.label;"/>
+ <tab id="identityE2ETab" label="&e2eTitle.label;"/>
+ </tabs>
+
+ <tabpanels id="identityTabsPanels" flex="1">
+ <!-- Identity Settings Tab -->
+ <vbox flex="1" name="settings">
+ <html:div>
+ <html:fieldset>
+ <html:legend>&publicData.label;</html:legend>
+ <html:table class="identity-table">
+ <html:tr>
+ <html:th>
+ <label id="identity.fullName.label"
+ value="&name.label;"
+ control="identity.fullName"
+ accesskey="&name.accesskey;"/>
+ </html:th>
+ <html:td>
+ <html:input id="identity.fullName"
+ type="text"
+ class="input-inline"
+ aria-labelledby="identity.fullName.label"
+ size="30"/>
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label id="identity.email.label"
+ value="&email.label;"
+ control="identity.email"
+ accesskey="&email.accesskey;"/>
+ </html:th>
+ <html:td>
+ <html:input id="identity.email"
+ type="email"
+ class="uri-element input-inline"
+ aria-labelledby="identity.email.label"/>
+
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label id="identity.replyTo.label"
+ value="&replyTo.label;"
+ accesskey="&replyTo.accesskey;"
+ control="identity.replyTo"/>
+ </html:th>
+ <html:td>
+ <html:input id="identity.replyTo"
+ type="text"
+ class="uri-element input-inline"
+ placeholder="&replyTo.placeholder;"
+ aria-labelledby="identity.replyTo.label"/>
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label id="identity.organization.label"
+ value="&organization.label;"
+ control="identity.organization"
+ accesskey="&organization.accesskey;"/>
+ </html:th>
+ <html:td>
+ <html:input id="identity.organization"
+ type="text"
+ class="input-inline"
+ aria-labelledby="identity.organization.label"/>
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label value="&signatureText.label;" control="identity.htmlSigText"
+ accesskey="&signatureText.accesskey;"/>
+ </html:th>
+ <html:td style="width:100%;">
+ <checkbox id="identity.htmlSigFormat" label="&signatureHtml.label;"
+ accesskey="&signatureHtml.accesskey;" style="width:100%;"/>
+ </html:td>
+ </html:tr>
+ </html:table>
+
+ <separator class="thin"/>
+
+ <hbox class="indent input-container" flex="1">
+ <html:textarea id="identity.htmlSigText" style="flex-grow: 1;" rows="4" class="signatureBox"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="identity.attachSignature" label="&signatureFile.label;"
+ flex="1" accesskey="&signatureFile.accesskey;"
+ oncommand="setupSignatureItems();"/>
+ </hbox>
+
+ <hbox align="center" class="indent input-container">
+ <html:input id="identity.signature"
+ type="text"
+ datatype="nsIFile"
+ name="identity.signature"
+ aria-labelledby="identity.attachSignature"
+ class="uri-element input-inline"/>
+ <button id="identity.sigbrowsebutton" class="push"
+ label="&choose.label;" accesskey="&choose.accesskey;"
+ oncommand="selectFile()"/>
+ </hbox>
+
+ <hbox align="center">
+ <checkbox id="identity.attachVCard" label="&attachVCard.label;"
+ flex="1" accesskey="&attachVCard.accesskey;"/>
+ <button class="push" label="&editVCard.label;"
+ accesskey="&editVCard.accesskey;" oncommand="editVCard()"/>
+ <label id="identity.escapedVCard" hidden="true"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&privateData.label;</html:legend>
+
+ <hbox id="identityCatchAllBox" align="center" class="input-container">
+ <checkbox id="identity.catchAll"
+ label="&catchAll.label;"
+ accesskey="&catchAll.accesskey;"
+ style="margin-block:auto;"/>
+ <html:input id="identity.catchAllHint"
+ type="text"
+ oninput="handleInputCatchAllHint(event);"
+ placeholder="list@example.com, *@example.com"
+ class="input-inline"
+ aria-labelledby="identity.catchAll"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <label value="&smtpName.label;"
+ control="identity.smtpServerKey"
+ accesskey="&smtpName.accesskey;"/>
+ <hbox align="center">
+ <menulist id="identity.smtpServerKey" flex="1">
+ <menupopup id="smtpPopup">
+ <menuitem id="useDefaultItem" value=""
+ label="&smtpDefaultServer.label;"/>
+ <menuseparator/>
+ <!-- list will be inserted here -->
+ </menupopup>
+ </menulist>
+
+ <button id="editSmtp"
+ label="&smtpServerEdit.label;"
+ accesskey="&smtpServerEdit.accesskey;"
+ oncommand="editCurrentSMTP();"/>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center" class="input-container">
+ <label id="identity.input.label" value="&identityAlias.label;"
+ accesskey="&identityAlias.accesskey;"
+ style="margin-block: auto;"
+ control="identity.label"/>
+ <html:input id="identity.label"
+ type="text"
+ class="input-inline"
+ aria-labelledby="identity.input.label"/>
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ </vbox>
+
+ <!-- Copies & Folders Tab -->
+#include am-copies.inc.xhtml
+
+ <!-- Composition & Addressing Tab -->
+#include am-addressing.inc.xhtml
+
+ <!-- Security Tab -->
+#include ../../../../mail/extensions/am-e2e/am-e2e.inc.xhtml
+
+ </tabpanels>
+ </tabbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-junk.js b/comm/mailnews/base/prefs/content/am-junk.js
new file mode 100644
index 0000000000..a102efdfae
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-junk.js
@@ -0,0 +1,335 @@
+/* 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 am-prefs.js */
+/* import-globals-from amUtils.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gDeferredToAccount = "";
+
+function onInit(aPageId, aServerId) {
+ // manually adjust several pref UI elements
+ document.getElementById("server.spamLevel.visible").checked =
+ document.getElementById("server.spamLevel").value > 0;
+
+ let deferredToURI = null;
+ if (gDeferredToAccount) {
+ deferredToURI =
+ MailServices.accounts.getAccount(gDeferredToAccount).incomingServer
+ .serverURI;
+ }
+
+ let spamActionTargetAccountElement = document.getElementById(
+ "server.spamActionTargetAccount"
+ );
+ let spamActionTargetFolderElement = document.getElementById(
+ "server.spamActionTargetFolder"
+ );
+
+ let spamActionTargetAccount = spamActionTargetAccountElement.value;
+ let spamActionTargetFolder = spamActionTargetFolderElement.value;
+
+ let moveOnSpamCheckbox = document.getElementById("server.moveOnSpam");
+ let moveOnSpamValue = moveOnSpamCheckbox.checked;
+
+ // Check if there are any invalid junk targets and fix them.
+ [spamActionTargetAccount, spamActionTargetFolder, moveOnSpamValue] =
+ sanitizeJunkTargets(
+ spamActionTargetAccount,
+ spamActionTargetFolder,
+ deferredToURI || aServerId,
+ document.getElementById("server.moveTargetMode").value,
+ MailUtils.getOrCreateFolder(aServerId).server.spamSettings,
+ moveOnSpamValue
+ );
+
+ spamActionTargetAccountElement.value = spamActionTargetAccount;
+ spamActionTargetFolderElement.value = spamActionTargetFolder;
+ moveOnSpamCheckbox.checked = moveOnSpamValue;
+
+ let server = MailUtils.getOrCreateFolder(spamActionTargetAccount);
+ document.getElementById("actionAccountPopup").selectFolder(server);
+
+ let folder = MailUtils.getExistingFolder(spamActionTargetFolder);
+ document.getElementById("actionFolderPopup").selectFolder(folder);
+
+ var currentArray = [];
+ if (document.getElementById("server.useWhiteList").checked) {
+ currentArray = document
+ .getElementById("server.whiteListAbURI")
+ .value.split(" ");
+ }
+
+ // set up the whitelist UI
+ var wList = document.getElementById("whiteListAbURI");
+ // Ensure the whitelist is empty
+ while (wList.hasChildNodes()) {
+ wList.lastChild.remove();
+ }
+
+ // Populate the listbox with address books
+ let abItems = [];
+ for (let ab of MailServices.ab.directories) {
+ // We skip mailing lists and remote address books.
+ if (ab.isMailList || ab.isRemote) {
+ continue;
+ }
+
+ abItems.push({ label: ab.dirName, URI: ab.URI });
+ }
+
+ // Sort the list
+ function sortFunc(a, b) {
+ return a.label.localeCompare(b.label);
+ }
+ abItems.sort(sortFunc);
+
+ // And then append each item to the listbox
+ for (let abItem of abItems) {
+ let checkbox = document.createXULElement("checkbox");
+ checkbox.setAttribute("label", abItem.label);
+ checkbox.checked = currentArray.includes(abItem.URI);
+
+ let item = document.createXULElement("richlistitem");
+ item.appendChild(checkbox);
+ item.setAttribute("value", abItem.URI);
+ wList.appendChild(item);
+ }
+
+ wList.addEventListener("keypress", event => {
+ if ([" ", "Enter"].includes(event.key)) {
+ let checkbox = wList.currentItem.firstElementChild;
+ checkbox.checked = !checkbox.checked;
+ wList.dispatchEvent(new CustomEvent("command"));
+ }
+ });
+
+ // enable or disable the whitelist
+ onAdaptiveJunkToggle();
+
+ // set up trusted IP headers
+ var serverFilterList = document.getElementById("useServerFilterList");
+ serverFilterList.value = document.getElementById(
+ "server.serverFilterName"
+ ).value;
+ if (!serverFilterList.selectedItem) {
+ serverFilterList.selectedIndex = 0;
+ }
+
+ // enable or disable the useServerFilter checkbox
+ onCheckItem("useServerFilterList", ["server.useServerFilter"]);
+
+ updateJunkTargetsAndRetention();
+}
+
+function onPreInit(account, accountValues) {
+ if (
+ top.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ ) == "pop3"
+ ) {
+ gDeferredToAccount = top.getAccountValue(
+ account,
+ accountValues,
+ "pop3",
+ "deferredToAccount",
+ null,
+ false
+ );
+ }
+
+ buildServerFilterMenuList();
+}
+
+/**
+ * Called when someone checks or unchecks the adaptive junk mail checkbox.
+ * set the value of the hidden element accordingly
+ *
+ * @param {boolean} aValue - The boolean value of the checkbox.
+ */
+function updateSpamLevel(aValue) {
+ document.getElementById("server.spamLevel").value = aValue ? 100 : 0;
+ onAdaptiveJunkToggle();
+}
+
+/**
+ * Propagate changes to the server filter menu list back to
+ * our hidden wsm element.
+ */
+function onServerFilterListChange() {
+ document.getElementById("server.serverFilterName").value =
+ document.getElementById("useServerFilterList").value;
+}
+
+/**
+ * Called when someone checks or unchecks the adaptive junk mail checkbox.
+ * We need to enable or disable the whitelist accordingly.
+ */
+function onAdaptiveJunkToggle() {
+ onCheckItem("whiteListAbURI", ["server.spamLevel.visible"]);
+ onCheckItem("whiteListLabel", ["server.spamLevel.visible"]);
+
+ // Enable/disable individual listbox rows.
+ // Setting enable/disable on the parent listbox does not seem to work.
+ let wList = document.getElementById("whiteListAbURI");
+ let wListDisabled = wList.disabled;
+
+ for (let i = 0; i < wList.getRowCount(); i++) {
+ let item = wList.getItemAtIndex(i);
+ item.setAttribute("disabled", wListDisabled);
+ item.firstElementChild.setAttribute("disabled", wListDisabled);
+ }
+}
+
+/**
+ * Called when someone checks or unchecks the "move new junk messages to"
+ * Enable/disable the radio group accordingly.
+ */
+function updateJunkTargetsAndRetention() {
+ onCheckItem("server.moveTargetMode", ["server.moveOnSpam"]);
+ updateJunkTargets();
+ onCheckItem("server.purgeSpam", ["server.moveOnSpam"]);
+ document.getElementById("purgeLabel").disabled =
+ document.getElementById("server.purgeSpam").disabled;
+ updateJunkRetention();
+}
+
+/**
+ * Enable/disable the folder pickers depending on which radio item is selected.
+ */
+function updateJunkTargets() {
+ onCheckItem("actionTargetAccount", ["server.moveOnSpam", "moveTargetMode0"]);
+ onCheckItem("actionTargetFolder", ["server.moveOnSpam", "moveTargetMode1"]);
+}
+
+/**
+ * Enable/disable the junk deletion interval depending on the state
+ * of the controlling checkbox.
+ */
+function updateJunkRetention() {
+ onCheckItem("server.purgeSpamInterval", [
+ "server.purgeSpam",
+ "server.moveOnSpam",
+ ]);
+}
+
+function onSave() {
+ onSaveWhiteList();
+}
+
+/**
+ * Propagate changes to the whitelist menu list back to
+ * our hidden wsm element.
+ */
+function onSaveWhiteList() {
+ var wList = document.getElementById("whiteListAbURI");
+ var wlArray = [];
+
+ for (let i = 0; i < wList.getRowCount(); i++) {
+ // Due to bug 448582, do not trust any properties of the listitems
+ // as they may not return the right value or may even not exist.
+ // Always get the attributes only.
+ var wlNode = wList.getItemAtIndex(i);
+ if (wlNode.firstElementChild.getAttribute("checked") == "true") {
+ let abURI = wlNode.getAttribute("value");
+ wlArray.push(abURI);
+ }
+ }
+ var wlValue = wlArray.join(" ");
+ document
+ .getElementById("server.whiteListAbURI")
+ .setAttribute("value", wlValue);
+ document.getElementById("server.useWhiteList").checked = wlValue != "";
+}
+
+/**
+ * Called when a new value is chosen in one of the junk target folder pickers.
+ * Sets the menu label according to the folder name.
+ */
+function onActionTargetChange(aEvent, aWSMElementId) {
+ let folder = aEvent.target._folder;
+ document.getElementById(aWSMElementId).value = folder.URI;
+ document.getElementById("actionFolderPopup").selectFolder(folder);
+}
+
+/**
+ * Enumerates over the "ISPDL" directories, calling buildServerFilterListFromDir
+ * for each one.
+ */
+function buildServerFilterMenuList() {
+ const KEY_ISP_DIRECTORY_LIST = "ISPDL";
+ let ispHeaderList = document.getElementById("useServerFilterList");
+
+ // Ensure the menulist is empty.
+ ispHeaderList.removeAllItems();
+
+ // Now walk through the isp directories looking for sfd files.
+ let ispDirectories = Services.dirsvc.get(
+ KEY_ISP_DIRECTORY_LIST,
+ Ci.nsISimpleEnumerator
+ );
+
+ let menuEntries = [];
+ while (ispDirectories.hasMoreElements()) {
+ let ispDirectory = ispDirectories.getNext().QueryInterface(Ci.nsIFile);
+ if (ispDirectory) {
+ menuEntries.push.apply(
+ menuEntries,
+ buildServerFilterListFromDir(ispDirectory, menuEntries)
+ );
+ }
+ }
+
+ menuEntries.sort((a, b) => a.localeCompare(b));
+ for (let entry of menuEntries) {
+ ispHeaderList.appendItem(entry, entry);
+ }
+}
+
+/**
+ * Helper function called by buildServerFilterMenuList. Enumerates over the
+ * passed in directory looking for .sfd files. For each entry found, it gets
+ * appended to the menu list.
+ *
+ * @param {nsIFile} aDir - Directory to look for .sfd files
+ * @param {string[]} aExistingEntries - Filter names already found.
+ */
+function buildServerFilterListFromDir(aDir, aExistingEntries) {
+ let newEntries = [];
+ // Now iterate over each file in the directory looking for .sfd files.
+ const kSuffix = ".sfd";
+
+ for (let entry of aDir.directoryEntries) {
+ // we only care about files that end in .sfd
+ if (entry.isFile() && entry.leafName.endsWith(kSuffix)) {
+ let fileName = entry.leafName.slice(0, -kSuffix.length);
+ // If we've already added an item with this name, then don't add it again.
+ if (!aExistingEntries.includes(fileName)) {
+ newEntries.push(fileName);
+ }
+ }
+ }
+ return newEntries;
+}
+
+/**
+ * Open the Preferences dialog on the Junk settings tab.
+ */
+function showGlobalJunkPrefs() {
+ openPrefsFromAccountManager(
+ "panePrivacy",
+ "privacyJunkCategory",
+ null,
+ "junk_pane"
+ );
+}
diff --git a/comm/mailnews/base/prefs/content/am-junk.xhtml b/comm/mailnews/base/prefs/content/am-junk.xhtml
new file mode 100644
index 0000000000..216d3c0431
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-junk.xhtml
@@ -0,0 +1,293 @@
+<?xml version="1.0"?>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % junkMailDTD SYSTEM "chrome://messenger/locale/am-junk.dtd">
+%junkMailDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&junkSettings.label;</title>
+ <script defer="defer" src="chrome://messenger/content/am-junk.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-junk.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&junkSettings.label;" />
+ </hbox>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&junkClassification.label;</html:legend>
+
+ <label
+ hidden="true"
+ id="server.spamLevel"
+ wsm_persist="true"
+ pref="true"
+ preftype="int"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.spamLevel"
+ />
+ <label
+ hidden="true"
+ id="server.spamActionTargetAccount"
+ wsm_persist="true"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.spamActionTargetAccount"
+ />
+ <label
+ hidden="true"
+ id="server.spamActionTargetFolder"
+ wsm_persist="true"
+ pref="true"
+ preftype="wstring"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.spamActionTargetFolder"
+ />
+ <label
+ hidden="true"
+ id="server.whiteListAbURI"
+ wsm_persist="true"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.whiteListAbURI"
+ />
+ <label
+ hidden="true"
+ id="server.serverFilterName"
+ wsm_persist="true"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.serverFilterName"
+ />
+
+ <checkbox
+ id="server.spamLevel.visible"
+ oncommand="updateSpamLevel(this.checked);"
+ accesskey="&level.accesskey;"
+ prefstring="mail.server.%serverkey%.spamLevel"
+ label="&level.label;"
+ />
+
+ <separator class="thin" />
+
+ <description width="1">&trainingDescription.label;</description>
+
+ <separator class="thin" />
+ <spacer height="3" />
+
+ <vbox class="indent">
+ <checkbox
+ hidden="true"
+ id="server.useWhiteList"
+ genericattr="true"
+ pref="true"
+ wsm_persist="true"
+ preftype="bool"
+ prefstring="mail.server.%serverkey%.useWhiteList"
+ />
+ <label
+ id="whiteListLabel"
+ accesskey="&whitelistHeader.accesskey;"
+ control="whiteListAbURI"
+ >&whitelistHeader.label;</label
+ >
+ <richlistbox id="whiteListAbURI" height="200px" />
+ </vbox>
+
+ <separator />
+
+ <vbox>
+ <hbox>
+ <checkbox
+ id="server.useServerFilter"
+ label="&ispHeaders.label;"
+ accesskey="&ispHeaders.accesskey;"
+ genericattr="true"
+ pref="true"
+ wsm_persist="true"
+ preftype="bool"
+ oncommand="onCheckItem('useServerFilterList', [this.id]);"
+ prefstring="mail.server.%serverkey%.useServerFilter"
+ />
+ <menulist
+ id="useServerFilterList"
+ oncommand="onServerFilterListChange();"
+ aria-labelledby="server.useServerFilter"
+ />
+ </hbox>
+ </vbox>
+
+ <separator class="thin" />
+
+ <description class="indent tip-caption" width="1"
+ >&ispHeadersWarning.label;</description
+ >
+ </html:fieldset>
+ </html:div>
+
+ <separator />
+ <separator />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&junkActions.label;</html:legend>
+
+ <checkbox
+ id="server.moveOnSpam"
+ label="&move.label;"
+ accesskey="&move.accesskey;"
+ oncommand="updateJunkTargetsAndRetention();"
+ wsm_persist="true"
+ pref="true"
+ preftype="bool"
+ genericattr="true"
+ prefstring="mail.server.%serverkey%.moveOnSpam"
+ />
+
+ <radiogroup
+ id="server.moveTargetMode"
+ aria-labelledby="server.moveOnSpam"
+ prefstring="mail.server.%serverkey%.moveTargetMode"
+ wsm_persist="true"
+ pref="true"
+ preftype="int"
+ genericattr="true"
+ oncommand="updateJunkTargets();"
+ prefvalue="value"
+ >
+ <hbox class="specialFolderPickerGrid indent">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="moveTargetMode0"
+ value="0"
+ label="&junkFolderOn.label;"
+ accesskey="&junkFolderOn.accesskey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="moveTargetMode1"
+ value="1"
+ label="&otherFolder.label;"
+ accesskey="&otherFolder.accesskey;"
+ />
+ </hbox>
+ </vbox>
+ <vbox flex="1">
+ <menulist
+ id="actionTargetAccount"
+ class="folderMenuItem"
+ aria-labelledby="moveTargetMode0"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="actionAccountPopup"
+ class="menulist-menupopup"
+ expandFolders="false"
+ mode="filing"
+ oncommand="onActionTargetChange(event, 'server.spamActionTargetAccount');"
+ />
+ </menulist>
+ <menulist
+ id="actionTargetFolder"
+ class="folderMenuItem"
+ aria-labelledby="moveTargetMode1"
+ displayformat="verbose"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="actionFolderPopup"
+ mode="junk"
+ showFileHereLabel="true"
+ oncommand="onActionTargetChange(event, 'server.spamActionTargetFolder');"
+ />
+ </menulist>
+ </vbox>
+ </hbox>
+ </radiogroup>
+
+ <hbox align="center" class="indent">
+ <checkbox
+ id="server.purgeSpam"
+ genericattr="true"
+ pref="true"
+ wsm_persist="true"
+ preftype="bool"
+ prefstring="mail.server.%serverkey%.purgeSpam"
+ accesskey="&purge1.accesskey;"
+ oncommand="updateJunkRetention();"
+ label="&purge1.label;"
+ />
+ <html:input
+ id="server.purgeSpamInterval"
+ type="number"
+ class="size3 input-inline"
+ min="1"
+ genericattr="true"
+ pref="true"
+ wsm_persist="true"
+ preftype="int"
+ aria-labelledby="server.purgeSpam server.purgeSpamInterval purgeLabel"
+ prefstring="mail.server.%serverkey%.purgeSpamInterval"
+ />
+ <label
+ id="purgeLabel"
+ value="&purge2.label;"
+ control="server.purgeSpamInterval"
+ />
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <hbox pack="end">
+ <button
+ id="globalJunkPrefsLink"
+ label="&globalJunkPrefs.label;"
+ accesskey="&globalJunkPrefs.accesskey;"
+ oncommand="showGlobalJunkPrefs();"
+ />
+ </hbox>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-main.js b/comm/mailnews/base/prefs/content/am-main.js
new file mode 100644
index 0000000000..b71ad49fb8
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-main.js
@@ -0,0 +1,110 @@
+/* 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 am-identity-edit.js */
+
+var gAccount;
+
+/**
+ * Initialize am-main account settings page when it gets shown.
+ * Update an account's main settings title and set up signature items.
+ */
+function onInit() {
+ setAccountTitle();
+ setupSignatureItems();
+ Services.obs.addObserver(
+ onDefaultIdentityChange,
+ "account-default-identity-changed"
+ );
+}
+
+window.addEventListener("unload", function () {
+ Services.obs.removeObserver(
+ onDefaultIdentityChange,
+ "account-default-identity-changed"
+ );
+});
+
+/**
+ * If the default identity for the current account changes, loads the values
+ * from the new default identity.
+ */
+function onDefaultIdentityChange(subject, topic, data) {
+ if (data == gAccount.key) {
+ initIdentityValues(subject.QueryInterface(Ci.nsIMsgIdentity));
+ }
+}
+
+/**
+ * Handle the blur event of the #server.prettyName pref input.
+ * Update account name in account manager tree and account settings' main title.
+ *
+ * @param {Event} event - Blur event from the pretty name input.
+ */
+function serverPrettyNameOnBlur(event) {
+ parent.setAccountLabel(gAccount.key, event.target.value);
+ setAccountTitle();
+}
+
+/**
+ * Update an account's main settings title with the account name if applicable.
+ */
+function setAccountTitle() {
+ let accountName = document.getElementById("server.prettyName");
+ let title = document.querySelector("#am-main-title .dialogheader-title");
+ let titleValue = title.getAttribute("defaultTitle");
+ if (accountName.value) {
+ titleValue += " - " + accountName.value;
+ }
+
+ title.setAttribute("value", titleValue);
+ document.title = titleValue;
+}
+
+function onPreInit(account, accountValues) {
+ gAccount = account;
+ loadSMTPServerList();
+ let type = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ hideShowControls(type);
+}
+
+function manageIdentities() {
+ // We want to save the current identity information before bringing up the multiple identities
+ // UI. This ensures that the changes are reflected in the identity list dialog
+ // onSave();
+
+ if (!gAccount) {
+ return;
+ }
+
+ var accountName = document.getElementById("server.prettyName").value;
+
+ var args = { account: gAccount, accountName, result: false };
+
+ // save the current identity settings so they show up correctly
+ // if the user just changed them in the manage identities dialog
+ var identity = gAccount.defaultIdentity;
+ saveIdentitySettings(identity);
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/am-identities-list.xhtml",
+ { closingCallback: onCloseIdentities },
+ args
+ );
+
+ function onCloseIdentities() {
+ if (args.result) {
+ // Refresh the SMTP list in case the user changed server properties
+ // from the identity dialog.
+ loadSMTPServerList();
+ }
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/am-main.xhtml b/comm/mailnews/base/prefs/content/am-main.xhtml
new file mode 100644
index 0000000000..09f5453fb8
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-main.xhtml
@@ -0,0 +1,344 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-main.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-identity-edit.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/am-main.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-main.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <hbox id="am-main-title" class="dialogheader">
+ <label class="dialogheader-title" defaultTitle="&accountTitle.label;" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <hbox class="input-container">
+ <label
+ id="server.prettyName.label"
+ value="&accountName.label;"
+ control="server.prettyName"
+ accesskey="&accountName.accesskey;"
+ />
+ <html:input
+ id="server.prettyName"
+ type="text"
+ wsm_persist="true"
+ class="input-inline"
+ onblur="serverPrettyNameOnBlur(event);"
+ prefstring="mail.server.%serverkey%.name"
+ aria-labelledby="server.prettyName.label"
+ />
+ </hbox>
+
+ <separator />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&identityTitle.label;</html:legend>
+ <description>&identityDesc.label;</description>
+ <separator class="thin" />
+ <html:table class="identity-table">
+ <html:tr>
+ <html:th>
+ <label
+ id="identity.fullName.label.label"
+ value="&name.label;"
+ control="identity.fullName"
+ accesskey="&name.accesskey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="identity.fullName"
+ type="text"
+ class="input-inline"
+ aria-labelledby="identity.fullName.label"
+ wsm_persist="true"
+ size="30"
+ prefstring="mail.identity.%identitykey%.fullName"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="identity.email.label"
+ value="&email.label;"
+ control="identity.email"
+ accesskey="&email.accesskey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="identity.email"
+ type="email"
+ wsm_persist="true"
+ prefstring="mail.identity.%identitykey%.useremail"
+ class="uri-element input-inline"
+ aria-labelledby="identity.email.label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="identity.replyTo.label"
+ value="&replyTo.label;"
+ control="identity.replyTo"
+ accesskey="&replyTo.accesskey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="identity.replyTo"
+ type="text"
+ wsm_persist="true"
+ prefstring="mail.identity.%identitykey%.reply_to"
+ class="uri-element input-inline"
+ placeholder="&replyTo.placeholder;"
+ aria-labelledby="identity.replyTo.label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ id="identity.organization.label"
+ value="&organization.label;"
+ control="identity.organization"
+ accesskey="&organization.accesskey;"
+ />
+ </html:th>
+ <html:td>
+ <html:input
+ id="identity.organization"
+ type="text"
+ wsm_persist="true"
+ prefstring="mail.identity.%identitykey%.organization"
+ class="input-inline"
+ aria-labelledby="identity.organization.label"
+ />
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:th>
+ <label
+ value="&signatureText.label;"
+ control="identity.htmlSigText"
+ accesskey="&signatureText.accesskey;"
+ />
+ </html:th>
+ <html:td style="width: 100%">
+ <hbox align="center">
+ <checkbox
+ id="identity.htmlSigFormat"
+ wsm_persist="true"
+ label="&signatureHtml.label;"
+ prefattribute="value"
+ accesskey="&signatureHtml.accesskey;"
+ style="width: 100%"
+ prefstring="mail.identity.%identitykey%.htmlSigFormat"
+ />
+ </hbox>
+ </html:td>
+ </html:tr>
+ </html:table>
+
+ <hbox
+ class="indent"
+ flex="1"
+ style="min-height: 100px; display: flex"
+ >
+ <html:textarea
+ id="identity.htmlSigText"
+ wsm_persist="true"
+ rows="4"
+ style="flex-grow: 1"
+ prefstring="mail.identity.%identitykey%.htmlSigText"
+ class="signatureBox"
+ />
+ </hbox>
+
+ <hbox align="center">
+ <checkbox
+ id="identity.attachSignature"
+ wsm_persist="true"
+ label="&signatureFile.label;"
+ flex="1"
+ accesskey="&signatureFile.accesskey;"
+ oncommand="setupSignatureItems();"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.attach_signature"
+ />
+ </hbox>
+
+ <hbox align="center" class="indent input-container">
+ <html:input
+ id="identity.signature"
+ type="text"
+ datatype="nsIFile"
+ wsm_persist="true"
+ name="identity.signature"
+ aria-labelledby="identity.attachSignature"
+ prefstring="mail.identity.%identitykey%.sig_file"
+ class="uri-element input-inline"
+ />
+ <button
+ class="push"
+ name="browse"
+ label="&choose.label;"
+ accesskey="&choose.accesskey;"
+ oncommand="selectFile()"
+ wsm_persist="true"
+ id="identity.sigbrowsebutton"
+ prefstring="mail.identity.%identitykey%.sigbrowse.disable"
+ />
+ </hbox>
+
+ <hbox align="center">
+ <checkbox
+ wsm_persist="true"
+ id="identity.attachVCard"
+ label="&attachVCard.label;"
+ flex="1"
+ accesskey="&attachVCard.accesskey;"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.attach_vcard"
+ />
+ <button
+ class="push"
+ name="editVCard"
+ label="&editVCard.label;"
+ accesskey="&editVCard.accesskey;"
+ oncommand="editVCard()"
+ />
+ <html:input
+ id="identity.escapedVCard"
+ type="hidden"
+ value=""
+ wsm_persist="true"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.escapedVCard"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <hbox align="center" class="input-container" hidefor="nntp">
+ <checkbox
+ id="identity.catchAll"
+ wsm_persist="true"
+ prefattribute="value"
+ label="&catchAll.label;"
+ accesskey="&catchAll.accesskey;"
+ style="margin-block: auto"
+ prefstring="mail.identity.%identitykey%.catchAll"
+ />
+ <html:input
+ id="identity.catchAllHint"
+ type="text"
+ wsm_persist="true"
+ prefstring="mail.identity.%identitykey%.catchAllHint"
+ class="input-inline"
+ oninput="handleInputCatchAllHint(event);"
+ placeholder="list@example.com, *@example.com"
+ aria-labelledby="identity.catchAll"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <hbox align="center">
+ <label
+ value="&smtpName.label;"
+ control="identity.smtpServerKey"
+ accesskey="&smtpName.accesskey;"
+ />
+ <menulist
+ wsm_persist="true"
+ id="identity.smtpServerKey"
+ flex="1"
+ pref="true"
+ preftype="string"
+ prefattribute="value"
+ prefstring="mail.identity.%identitykey%.smtpServer"
+ >
+ <menupopup id="smtpPopup">
+ <menuitem
+ value=""
+ label="&smtpDefaultServer.label;"
+ id="useDefaultItem"
+ />
+ <menuseparator />
+ <!-- list will be inserted here -->
+ </menupopup>
+ </menulist>
+ <button
+ id="editSmtp"
+ label="&smtpServerEdit.label;"
+ accesskey="&smtpServerEdit.accesskey;"
+ oncommand="editCurrentSMTP();"
+ />
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <separator class="thin" />
+
+ <hbox align="center">
+ <spacer flex="1" />
+ <button
+ label="&manageIdentities.label;"
+ oncommand="manageIdentities(event);"
+ accesskey="&manageIdentities.accesskey;"
+ wsm_persist="true"
+ id="identity.manageIdentitiesbutton"
+ />
+ </hbox>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-offline.js b/comm/mailnews/base/prefs/content/am-offline.js
new file mode 100644
index 0000000000..72158edace
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-offline.js
@@ -0,0 +1,435 @@
+/* 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 am-prefs.js */
+/* import-globals-from ../../content/retention.js */
+
+var gIncomingServer;
+var gServerType;
+var gImapIncomingServer;
+var gPref = null;
+var gLockedPref = {};
+var gOfflineMap = null; // map of folder URLs to offline flags
+var gOfflineFolders; // initial state of allFoldersOffline checkbox
+var gToggleOccurred = false;
+
+function onInit(aPageId, aServerId) {
+ onLockPreference();
+
+ // init values here
+ initServerSettings();
+ initRetentionSettings();
+ initDownloadSettings();
+ initOfflineSettings();
+
+ onCheckItem1("offline.notDownloadMin", "offline.notDownload");
+ onCheckItem1("nntp.downloadMsgMin", "nntp.downloadMsg");
+ onCheckItem1("nntp.removeBodyMin", "nntp.removeBody");
+ onCheckKeepMsg();
+}
+
+/**
+ * Store initial offline flag for each folder and the allFoldersOffline
+ * checkbox. Use to restore the flags and checkbox if edits are canceled.
+ */
+function initOfflineSettings() {
+ gOfflineMap = collectOfflineFolders();
+ gOfflineFolders = document.getElementById("offline.folders").checked;
+ gToggleOccurred = false;
+}
+
+function initServerSettings() {
+ document.getElementById("offline.notDownload").checked =
+ gIncomingServer.limitOfflineMessageSize;
+ document.getElementById("autosyncNotDownload").checked =
+ gIncomingServer.limitOfflineMessageSize;
+ if (gIncomingServer.maxMessageSize > 0) {
+ document.getElementById("offline.notDownloadMin").value =
+ gIncomingServer.maxMessageSize;
+ } else {
+ document.getElementById("offline.notDownloadMin").value = "50";
+ }
+
+ if (gServerType == "imap") {
+ gImapIncomingServer = gIncomingServer.QueryInterface(
+ Ci.nsIImapIncomingServer
+ );
+ document.getElementById("offline.folders").checked =
+ gImapIncomingServer.offlineDownload;
+ }
+}
+
+function initRetentionSettings() {
+ let retentionSettings = gIncomingServer.retentionSettings;
+ initCommonRetentionSettings(retentionSettings);
+
+ document.getElementById("nntp.removeBody").checked =
+ retentionSettings.cleanupBodiesByDays;
+ document.getElementById("nntp.removeBodyMin").value =
+ retentionSettings.daysToKeepBodies > 0
+ ? retentionSettings.daysToKeepBodies
+ : 30;
+}
+
+function initDownloadSettings() {
+ let downloadSettings = gIncomingServer.downloadSettings;
+ document.getElementById("nntp.downloadMsg").checked =
+ downloadSettings.downloadByDate;
+ document.getElementById("nntp.notDownloadRead").checked =
+ downloadSettings.downloadUnreadOnly;
+ document.getElementById("nntp.downloadMsgMin").value =
+ downloadSettings.ageLimitOfMsgsToDownload > 0
+ ? downloadSettings.ageLimitOfMsgsToDownload
+ : 30;
+
+ // Figure out what the most natural division of the autosync pref into
+ // a value and an interval is.
+ let autosyncSelect = document.getElementById("autosyncSelect");
+ let autosyncInterval = document.getElementById("autosyncInterval");
+ let autosyncValue = document.getElementById("autosyncValue");
+ let autosyncPref = document.getElementById("imap.autoSyncMaxAgeDays");
+ let autosyncPrefValue =
+ autosyncPref.value == "" ? -1 : parseInt(autosyncPref.value, 10);
+
+ // Clear the preference until we're done initializing.
+ autosyncPref.value = "";
+
+ if (autosyncPrefValue <= 0) {
+ // Special-case values <= 0 to have an interval of "All" and disabled
+ // controls for value and interval.
+ autosyncSelect.value = 0;
+ autosyncInterval.value = 1;
+ autosyncInterval.disabled = true;
+ autosyncValue.value = 30;
+ autosyncValue.disabled = true;
+ } else {
+ // Otherwise, get the list of possible intervals, in order from
+ // largest to smallest.
+ let valuesToTest = [];
+ for (let i = autosyncInterval.itemCount - 1; i >= 0; i--) {
+ valuesToTest.push(autosyncInterval.getItemAtIndex(i).value);
+ }
+
+ // and find the first one that divides the preference evenly.
+ for (let i in valuesToTest) {
+ if (!(autosyncPrefValue % valuesToTest[i])) {
+ autosyncSelect.value = 1;
+ autosyncInterval.value = valuesToTest[i];
+ autosyncValue.value = autosyncPrefValue / autosyncInterval.value;
+ break;
+ }
+ }
+ autosyncInterval.disabled = false;
+ autosyncValue.disabled = false;
+ }
+ autosyncPref.value = autosyncPrefValue;
+}
+
+function onPreInit(account, accountValues) {
+ gServerType = top.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ hideShowControls(gServerType);
+ gIncomingServer = account.incomingServer;
+ gIncomingServer.type = gServerType;
+
+ // 10 is OFFLINE_SUPPORT_LEVEL_REGULAR, see nsIMsgIncomingServer.idl
+ // currently, there is no offline without diskspace
+ var titleStringID =
+ gIncomingServer.offlineSupportLevel >= 10
+ ? "prefPanel-synchronization"
+ : "prefPanel-diskspace";
+
+ var prefBundle = document.getElementById("bundle_prefs");
+ document
+ .querySelector("#headertitle > .dialogheader-title")
+ .setAttribute("value", prefBundle.getString(titleStringID));
+ document.title = prefBundle.getString(titleStringID);
+
+ if (gServerType == "pop3") {
+ var pop3Server = gIncomingServer.QueryInterface(Ci.nsIPop3IncomingServer);
+ // hide retention settings for deferred accounts
+ if (pop3Server.deferredToAccount.length) {
+ var retentionRadio = document.getElementById("retention.keepMsg");
+ retentionRadio.setAttribute("hidden", "true");
+ var retentionLabel = document.getElementById("retentionDescriptionPop");
+ retentionLabel.setAttribute("hidden", "true");
+ var applyToFlaggedCheckbox = document.getElementById(
+ "retention.applyToFlagged"
+ );
+ applyToFlaggedCheckbox.setAttribute("hidden", "true");
+ }
+ }
+}
+
+function onClickSelect() {
+ parent.gSubDialog.open(
+ "chrome://messenger/content/msgSelectOfflineFolders.xhtml"
+ );
+}
+
+/**
+ * Handle updates to the Autosync
+ */
+function onAutosyncChange() {
+ let autosyncSelect = document.getElementById("autosyncSelect");
+ let autosyncInterval = document.getElementById("autosyncInterval");
+ let autosyncValue = document.getElementById("autosyncValue");
+ let autosyncPref = document.getElementById("imap.autoSyncMaxAgeDays");
+
+ // If we're not done initializing, don't do anything.
+ // (See initDownloadSettings() for more details.)
+ if (autosyncPref.value == "") {
+ return;
+ }
+
+ // If the user selected the All option, disable the autosync and the
+ // textbox.
+ if (autosyncSelect.value == 0) {
+ autosyncPref.value = -1;
+ autosyncInterval.disabled = true;
+ autosyncValue.disabled = true;
+ return;
+ }
+
+ let max = 0x7fffffff / (60 * 60 * 24 * autosyncInterval.value);
+ autosyncValue.setAttribute("max", max);
+ if (autosyncValue.value > max) {
+ autosyncValue.value = Math.floor(max);
+ }
+
+ autosyncInterval.disabled = false;
+ autosyncValue.disabled = false;
+ autosyncPref.value = autosyncValue.value * autosyncInterval.value;
+}
+
+function onAutosyncNotDownload() {
+ // This function is called when the autosync version of offline.notDownload
+ // is changed it simply copies the new checkbox value over to the element
+ // driving the preference.
+ document.getElementById("offline.notDownload").checked =
+ document.getElementById("autosyncNotDownload").checked;
+ onCheckItem1("offline.notDownloadMin", "offline.notDownload");
+}
+
+function onCancel() {
+ // restore the offline flags for all folders
+ restoreOfflineFolders(gOfflineMap);
+ document.getElementById("offline.folders").checked = gOfflineFolders;
+}
+
+/**
+ * Prompt to avoid unexpected folder sync changes.
+ */
+function onLeave() {
+ let changed = false;
+ if (gToggleOccurred) {
+ for (let folder of gIncomingServer.rootFolder.descendants) {
+ if (
+ gOfflineMap[folder.folderURL] !=
+ folder.getFlag(Ci.nsMsgFolderFlags.Offline)
+ ) {
+ // A change to the Offline flag to a folder was made.
+ changed = true;
+ break;
+ }
+ }
+ gToggleOccurred = false;
+ }
+
+ if (changed) {
+ // The user changed the "Keep messages in all folders..." checkbox and
+ // caused changes in online/offline status for all folders in this
+ // account. Prompt whether to restore the original status.
+ let prefBundle = document.getElementById("bundle_prefs");
+ let title = prefBundle.getString("confirmSyncChangesTitle");
+ let question = prefBundle.getString("confirmSyncChanges");
+ let discard = prefBundle.getString("confirmSyncChangesDiscard");
+ let result = Services.prompt.confirmEx(
+ window,
+ title,
+ question,
+ Services.prompt.BUTTON_TITLE_SAVE * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1,
+ null,
+ discard,
+ null,
+ null,
+ { value: 0 }
+ );
+ if (result == 1) {
+ // User clicked Discard button, so restore the online/offline changes for
+ // the current account. Changes made through the "Advanced..." dialog to
+ // other accounts will not be restored.
+ onCancel();
+ return false;
+ }
+ }
+ return true;
+}
+
+function onSave() {
+ var downloadSettings = Cc[
+ "@mozilla.org/msgDatabase/downloadSettings;1"
+ ].createInstance(Ci.nsIMsgDownloadSettings);
+
+ gIncomingServer.limitOfflineMessageSize = document.getElementById(
+ "offline.notDownload"
+ ).checked;
+ gIncomingServer.maxMessageSize = document.getElementById(
+ "offline.notDownloadMin"
+ ).value;
+
+ var retentionSettings = saveCommonRetentionSettings(
+ gIncomingServer.retentionSettings
+ );
+
+ retentionSettings.daysToKeepBodies =
+ document.getElementById("nntp.removeBodyMin").value;
+ retentionSettings.cleanupBodiesByDays =
+ document.getElementById("nntp.removeBody").checked;
+
+ downloadSettings.downloadByDate =
+ document.getElementById("nntp.downloadMsg").checked;
+ downloadSettings.downloadUnreadOnly = document.getElementById(
+ "nntp.notDownloadRead"
+ ).checked;
+ downloadSettings.ageLimitOfMsgsToDownload = document.getElementById(
+ "nntp.downloadMsgMin"
+ ).value;
+
+ gIncomingServer.retentionSettings = retentionSettings;
+ gIncomingServer.downloadSettings = downloadSettings;
+
+ if (gImapIncomingServer) {
+ // Set the pref on the incomingserver, and set the flag on all folders.
+ gImapIncomingServer.offlineDownload =
+ document.getElementById("offline.folders").checked;
+ }
+}
+
+// Does the work of disabling an element given the array which contains xul id/prefstring pairs.
+// Also saves the id/locked state in an array so that other areas of the code can avoid
+// stomping on the disabled state indiscriminately.
+function disableIfLocked(prefstrArray) {
+ for (let i = 0; i < prefstrArray.length; i++) {
+ var id = prefstrArray[i].id;
+ var element = document.getElementById(id);
+ if (gPref.prefIsLocked(prefstrArray[i].prefstring)) {
+ element.disabled = true;
+ gLockedPref[id] = true;
+ } else {
+ element.removeAttribute("disabled");
+ gLockedPref[id] = false;
+ }
+ }
+}
+
+// Disables xul elements that have associated preferences locked.
+function onLockPreference() {
+ var initPrefString = "mail.server";
+ var finalPrefString;
+
+ // This panel does not use the code in AccountManager.js to handle
+ // the load/unload/disable. keep in mind new prefstrings and changes
+ // to code in AccountManager, and update these as well.
+ var allPrefElements = [
+ { prefstring: "limit_offline_message_size", id: "offline.notDownload" },
+ { prefstring: "limit_offline_message_size", id: "autosyncNotDownload" },
+ { prefstring: "max_size", id: "offline.notDownloadMin" },
+ { prefstring: "downloadUnreadOnly", id: "nntp.notDownloadRead" },
+ { prefstring: "downloadByDate", id: "nntp.downloadMsg" },
+ { prefstring: "ageLimit", id: "nntp.downloadMsgMin" },
+ { prefstring: "retainBy", id: "retention.keepMsg" },
+ { prefstring: "daysToKeepHdrs", id: "retention.keepOldMsgMin" },
+ { prefstring: "numHdrsToKeep", id: "retention.keepNewMsgMin" },
+ { prefstring: "daysToKeepBodies", id: "nntp.removeBodyMin" },
+ { prefstring: "cleanupBodies", id: "nntp.removeBody" },
+ { prefstring: "applyToFlagged", id: "retention.applyToFlagged" },
+ { prefstring: "disable_button.selectFolder", id: "selectNewsgroupsButton" },
+ {
+ prefstring: "disable_button.selectFolder",
+ id: "selectImapFoldersButton",
+ },
+ ];
+
+ finalPrefString = initPrefString + "." + gIncomingServer.key + ".";
+ gPref = Services.prefs.getBranch(finalPrefString);
+
+ disableIfLocked(allPrefElements);
+}
+
+// XXX TODO: Function should be merged with onCheckItem in bug 755885.
+function onCheckItem1(changeElementId, checkElementId) {
+ var element = document.getElementById(changeElementId);
+ var checked = document.getElementById(checkElementId).checked;
+ if (checked && !gLockedPref[checkElementId]) {
+ element.removeAttribute("disabled");
+ } else {
+ element.setAttribute("disabled", "true");
+ }
+}
+
+function toggleOffline() {
+ let offline = document.getElementById("offline.folders").checked;
+ for (let folder of gIncomingServer.rootFolder.descendants) {
+ if (offline) {
+ folder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ } else {
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ }
+ }
+ gToggleOccurred = true;
+}
+
+function collectOfflineFolders() {
+ let offlineFolderMap = {};
+ for (let folder of gIncomingServer.rootFolder.descendants) {
+ offlineFolderMap[folder.folderURL] = folder.getFlag(
+ Ci.nsMsgFolderFlags.Offline
+ );
+ }
+
+ return offlineFolderMap;
+}
+
+function restoreOfflineFolders(offlineFolderMap) {
+ for (let folder of gIncomingServer.rootFolder.descendants) {
+ if (offlineFolderMap[folder.folderURL]) {
+ folder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ } else {
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ }
+ }
+}
+
+/**
+ * Checks if the user selected a permanent removal of messages from a server
+ * listed in the confirmfor attribute and warns about it.
+ *
+ * @param {Element} aRadio - The radiogroup element containing the retention options.
+ */
+function warnServerRemove(aRadio) {
+ let confirmFor = aRadio.getAttribute("confirmfor");
+
+ if (
+ confirmFor &&
+ confirmFor.split(",").includes(gServerType) &&
+ aRadio.value != 1
+ ) {
+ let prefBundle = document.getElementById("bundle_prefs");
+ let title = prefBundle.getString("removeFromServerTitle");
+ let question = prefBundle.getString("removeFromServer");
+ if (!Services.prompt.confirm(window, title, question)) {
+ // If the user doesn't agree, fall back to not deleting anything.
+ aRadio.value = 1;
+ onCheckKeepMsg();
+ }
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/am-offline.xhtml b/comm/mailnews/base/prefs/content/am-offline.xhtml
new file mode 100644
index 0000000000..0d3a12e396
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-offline.xhtml
@@ -0,0 +1,350 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-offline.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/retention.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-offline.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-offline.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+
+ <label hidden="true" wsm_persist="true" id="server.type" />
+ <label
+ id="imap.autoSyncMaxAgeDays"
+ hidden="true"
+ wsm_persist="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.autosync_max_age_days"
+ />
+
+ <hbox id="headertitle" class="dialogheader">
+ <label class="dialogheader-title" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset id="offline.titlebox" hidefor="pop3,none,rss">
+ <html:legend>&syncGroupTitle.label;</html:legend>
+
+ <vbox>
+ <checkbox
+ hidefor="pop3,nntp,none"
+ id="offline.folders"
+ label="&allFoldersOffline2.label;"
+ oncommand="toggleOffline()"
+ accesskey="&allFoldersOffline2.accesskey;"
+ />
+
+ <description hidefor="pop3,nntp,none,rss"
+ >&allFoldersOfflineNote.label;</description
+ >
+
+ <separator class="thin" hidefor="pop3,nntp,none" />
+
+ <hbox hidefor="pop3,nntp,none" pack="end">
+ <button
+ label="&offlineImapAdvancedOffline.label;"
+ accesskey="&offlineImapAdvancedOffline.accesskey;"
+ oncommand="onClickSelect()"
+ id="selectImapFoldersButton"
+ class="selectForOfflineUseButton"
+ />
+ </hbox>
+
+ <hbox hidefor="pop3,imap,none" pack="end">
+ <button
+ label="&offlineSelectNntp.label;"
+ accesskey="&offlineSelectNntp.accesskey;"
+ oncommand="onClickSelect()"
+ id="selectNewsgroupsButton"
+ class="selectForOfflineUseButton"
+ />
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset id="diskspace.titlebox">
+ <html:legend hidefor="pop3,none,rss"
+ >&diskspaceGroupTitle.label;</html:legend
+ >
+
+ <description hidefor="pop3,nntp,none,rss"
+ >&doNotDownloadImap.label;</description
+ >
+ <description hidefor="pop3,imap,none,rss"
+ >&doNotDownloadNntp.label;</description
+ >
+ <description hidefor="imap,nntp,none,rss"
+ >&doNotDownloadPop3Movemail.label;</description
+ >
+
+ <!-- IMAP Autosync Preference -->
+ <radiogroup
+ hidefor="pop3,nntp,none,rss"
+ id="autosyncSelect"
+ class="indent"
+ >
+ <radio
+ id="useAutosync.AllMsg"
+ value="0"
+ accesskey="&allAutosync.accesskey;"
+ label="&allAutosync.label;"
+ oncommand="onAutosyncChange();"
+ />
+ <hbox flex="1" align="center">
+ <radio
+ id="useAutosync.ByAge"
+ accesskey="&ageAutosync.accesskey;"
+ value="1"
+ label="&ageAutosyncBefore.label;"
+ oncommand="onAutosyncChange();"
+ />
+ <html:input
+ id="autosyncValue"
+ type="number"
+ class="size4 input-inline autosync"
+ min="1"
+ onchange="onAutosyncChange();"
+ aria-labelledby="ageAutosyncBefore autosyncValue ageAutosyncMiddle autosyncInterval ageAutosyncAfter"
+ />
+ <label
+ id="ageAutosyncMiddle"
+ control="autosyncValue"
+ value="&ageAutosyncMiddle.label;"
+ />
+ <menulist id="autosyncInterval" onselect="onAutosyncChange();">
+ <menupopup>
+ <menuitem label="&dayAgeInterval.label;" value="1" />
+ <menuitem label="&weekAgeInterval.label;" value="7" />
+ <menuitem label="&monthAgeInterval.label;" value="31" />
+ <menuitem label="&yearAgeInterval.label;" value="365" />
+ </menupopup>
+ </menulist>
+ <label
+ id="ageAutosyncAfter"
+ control="autosyncInterval"
+ value="&ageAutosyncAfter.label;"
+ />
+ </hbox>
+ </radiogroup>
+
+ <hbox align="center" class="indent" hidefor="rss">
+ <checkbox
+ hidefor="pop3,imap,none"
+ id="nntp.notDownloadRead"
+ wsm_persist="true"
+ label="&nntpNotDownloadRead.label;"
+ accesskey="&nntpNotDownloadRead.accesskey;"
+ />
+ </hbox>
+
+ <hbox align="center" class="indent" hidefor="none,rss">
+ <checkbox
+ wsm_persist="true"
+ id="offline.notDownload"
+ hidefor="imap"
+ label="&offlineNotDownload.label;"
+ accesskey="&offlineNotDownload.accesskey;"
+ oncommand="onCheckItem('offline.notDownloadMin', [this.id]);"
+ />
+ <checkbox
+ wsm_persist="true"
+ id="autosyncNotDownload"
+ hidefor="pop3,nntp"
+ label="&autosyncNotDownload.label;"
+ accesskey="&autosyncNotDownload.accesskey;"
+ oncommand="onAutosyncNotDownload();"
+ />
+ <html:input
+ id="offline.notDownloadMin"
+ type="number"
+ class="size4 input-inline"
+ min="1"
+ value="50"
+ wsm_persist="true"
+ aria-labelledby="offline.notDownload offline.notDownloadMin kbLabel"
+ />
+ <label
+ value="&kb.label;"
+ control="offline.notDownloadMin"
+ id="kbLabel"
+ />
+ </hbox>
+
+ <hbox align="center" class="indent" hidefor="pop3,imap,none,rss">
+ <checkbox
+ wsm_persist="true"
+ id="nntp.downloadMsg"
+ label="&nntpDownloadMsg.label;"
+ accesskey="&nntpDownloadMsg.accesskey;"
+ oncommand="onCheckItem('nntp.downloadMsgMin', [this.id]);"
+ />
+ <html:input
+ id="nntp.downloadMsgMin"
+ type="number"
+ class="size2 input-inline"
+ min="1"
+ value="30"
+ wsm_persist="true"
+ aria-labelledby="nntp.downloadMsg nntp.downloadMsgMin daysOldLabel"
+ />
+ <label
+ value="&daysOld.label;"
+ control="nntp.downloadMsgMin"
+ id="daysOldLabel"
+ />
+ </hbox>
+
+ <vbox align="start">
+ <separator hidefor="none,rss" />
+ <label
+ id="retentionDescription"
+ hidefor="imap,pop3"
+ class="desc"
+ control="retention.keepMsg"
+ >&retentionCleanup.label;</label
+ >
+ <label
+ id="retentionDescriptionImap"
+ hidefor="pop3,nntp,none,rss"
+ class="desc"
+ control="retention.keepMsg"
+ >&retentionCleanupImap.label;</label
+ >
+ <label
+ id="retentionDescriptionPop"
+ hidefor="imap,nntp,none,rss"
+ class="desc"
+ control="retention.keepMsg"
+ >&retentionCleanupPop.label;</label
+ >
+
+ <radiogroup
+ hidefor=""
+ confirmfor="imap,pop3"
+ id="retention.keepMsg"
+ class="indent"
+ oncommand="warnServerRemove(this);"
+ >
+ <radio
+ id="retention.keepAllMsg"
+ value="1"
+ accesskey="&retentionKeepAll.accesskey;"
+ label="&retentionKeepAll.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <hbox flex="1" align="center">
+ <radio
+ id="retention.keepNewMsg"
+ accesskey="&retentionKeepRecent.accesskey;"
+ value="3"
+ label="&retentionKeepRecent.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepNewMsgMin"
+ type="number"
+ class="size4 input-inline"
+ min="1"
+ value="2000"
+ aria-labelledby="retention.keepNewMsg retention.keepNewMsgMin newMsgLabel"
+ />
+ <label
+ value="&message.label;"
+ control="retention.keepNewMsgMin"
+ id="newMsgLabel"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="retention.keepOldMsg"
+ accesskey="&retentionKeepMsg.accesskey;"
+ value="2"
+ label="&retentionKeepMsg.label;"
+ oncommand="onCheckKeepMsg();"
+ />
+ <html:input
+ id="retention.keepOldMsgMin"
+ type="number"
+ class="size4 input-inline"
+ min="1"
+ value="30"
+ aria-labelledby="retention.keepOldMsg retention.keepOldMsgMin oldMsgLabel"
+ />
+ <label
+ value="&daysOld.label;"
+ control="retention.keepOldMsgMin"
+ id="oldMsgLabel"
+ />
+ </hbox>
+ </radiogroup>
+
+ <hbox align="center" class="indent">
+ <checkbox
+ id="retention.applyToFlagged"
+ label="&retentionApplyToFlagged.label;"
+ hidefor=""
+ accesskey="&retentionApplyToFlagged.accesskey;"
+ checked="true"
+ />
+ </hbox>
+ <hbox align="center" class="indent" hidefor="pop3,imap,none,rss">
+ <checkbox
+ id="nntp.removeBody"
+ accesskey="&nntpRemoveMsgBody.accesskey;"
+ label="&nntpRemoveMsgBody.label;"
+ oncommand="onCheckItem('nntp.removeBodyMin', [this.id]);"
+ />
+ <html:input
+ id="nntp.removeBodyMin"
+ type="number"
+ class="size2 input-inline"
+ min="1"
+ value="30"
+ aria-labelledby="nntp.removeBody nntp.removeBodyMin daysOldMsg"
+ />
+ <label
+ value="&daysOld.label;"
+ control="nntp.removeBodyMin"
+ id="daysOldMsg"
+ />
+ </hbox>
+ </vbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-prefs.js b/comm/mailnews/base/prefs/content/am-prefs.js
new file mode 100644
index 0000000000..cff09a5d93
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-prefs.js
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; 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/. */
+
+/* functions for disabling front end elements when the appropriate
+ back-end preference is locked. */
+
+/**
+ * Prefs in MailNews require dynamic portions to indicate
+ * which of multiple servers or identities. This function
+ * takes a string and a xul element.
+ *
+ * @param {string} aStr - The string is a prefstring with a token %tokenname%.
+ * @param {Element} aElement - The element has an attribute of name |tokenname|
+ * whose value is substituted into the string and returned by the function.
+ * Any tokens which do not have associated attribute value are not
+ * substituted, and left in the string as-is.
+ */
+function substPrefTokens(aStr, aElement) {
+ let tokenpat = /%(\w+)%/;
+ let token;
+ let newprefstr = "";
+
+ let prefPartsArray = aStr.split(".");
+ /* here's a little loop that goes through
+ each part of the string separated by a dot, and
+ if any parts are of the form %string%, it will replace
+ them with the value of the attribute of that name from
+ the xul object */
+ for (let i = 0; i < prefPartsArray.length; i++) {
+ token = prefPartsArray[i].match(tokenpat);
+ if (token) {
+ // We've got a %% match.
+ if (token[1]) {
+ if (aElement[token[1]]) {
+ newprefstr += aElement[token[1]] + "."; // here's where we get the info
+ } else {
+ // All we got was this stinkin %.
+ newprefstr += prefPartsArray[i] + ".";
+ }
+ }
+ } else {
+ // token is falsy.
+ newprefstr += prefPartsArray[i] + ".";
+ }
+ }
+ newprefstr = newprefstr.slice(0, -1); // remove the last char, a dot
+ if (newprefstr.length <= 0) {
+ newprefstr = null;
+ }
+
+ return newprefstr;
+}
+
+/**
+ * A simple function to check if a pref in an element is locked.
+ *
+ * @param {Element} aElement - An element with the pref related attributes
+ * (pref, preftype, prefstring)
+ * @returns {boolean} whether the prefstring specified in that element is
+ * locked (true/false). If it does not have a valid prefstring, a false is
+ * returned.
+ */
+function getAccountValueIsLocked(aElement) {
+ let prefstring = aElement.getAttribute("prefstring");
+ if (prefstring) {
+ let prefstr = substPrefTokens(prefstring, aElement);
+ // see if the prefstring is locked
+ if (prefstr) {
+ return Services.prefs.prefIsLocked(prefstr);
+ }
+ }
+ return false;
+}
+
+/**
+ * Enables/disables element (slave) according to the checked state
+ * of another elements (masters).
+ *
+ * @param {string} aChangeElementId - Slave element which should be enabled
+ * if all the checkElementIDs are checked. Otherwise it gets disabled.
+ * @param {string[]} aCheckElementIds - An array of IDs of the master elements.
+ *
+ * @see bug 728681 for the pattern on how this is used.
+ */
+function onCheckItem(aChangeElementId, aCheckElementIds) {
+ let elementToControl = document.getElementById(aChangeElementId);
+ let disabled = false;
+
+ for (let notifyId of aCheckElementIds) {
+ let notifyElement = document.getElementById(notifyId);
+ let notifyElementState = null;
+ if ("checked" in notifyElement) {
+ notifyElementState = notifyElement.checked;
+ } else if ("selected" in notifyElement) {
+ notifyElementState = notifyElement.selected;
+ } else {
+ console.error("Unknown type of control element: " + notifyElement.id);
+ }
+
+ if (!notifyElementState) {
+ disabled = true;
+ break;
+ }
+ }
+
+ if (!disabled && getAccountValueIsLocked(elementToControl)) {
+ disabled = true;
+ }
+
+ elementToControl.disabled = disabled;
+}
+
+/**
+ * Hides and shows elements relevant for the given server type.
+ *
+ * @param {string} serverType - Name of the server type for which to show/hide elements.
+ */
+function hideShowControls(serverType) {
+ let controls = document.querySelectorAll("[hidefor]");
+ for (let controlNo = 0; controlNo < controls.length; controlNo++) {
+ let control = controls[controlNo];
+ let hideFor = control.getAttribute("hidefor");
+
+ // Hide unsupported server types using hideFor="servertype1,servertype2".
+ let hide = false;
+ let hideForTokens = hideFor.split(",");
+ for (let tokenNo = 0; tokenNo < hideForTokens.length; tokenNo++) {
+ if (hideForTokens[tokenNo] == serverType) {
+ hide = true;
+ break;
+ }
+ }
+
+ if (hide) {
+ control.setAttribute("hidden", "true");
+ } else {
+ control.removeAttribute("hidden");
+ }
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/am-server-advanced.js b/comm/mailnews/base/prefs/content/am-server-advanced.js
new file mode 100644
index 0000000000..a37a8eced0
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-server-advanced.js
@@ -0,0 +1,151 @@
+/* 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"
+);
+
+window.addEventListener("DOMContentLoaded", onLoad);
+document.addEventListener("dialogaccept", onOk);
+
+// pull stuff out of window.arguments
+var gServerSettings = window.arguments[0];
+
+var gFirstDeferredAccount;
+// initialize the controls with the "gServerSettings" argument
+
+var gControls;
+function getControls() {
+ if (!gControls) {
+ gControls = document.getElementsByAttribute("amsa_persist", "true");
+ }
+ return gControls;
+}
+
+function getLocalFoldersAccount() {
+ return MailServices.accounts.FindAccountForServer(
+ MailServices.accounts.localFoldersServer
+ );
+}
+
+function onLoad() {
+ var prettyName = gServerSettings.serverPrettyName;
+
+ if (prettyName) {
+ document.getElementById("serverPrettyName").value = document
+ .getElementById("bundle_prefs")
+ .getFormattedString("forAccount", [prettyName]);
+ }
+
+ if (gServerSettings.serverType == "imap") {
+ document.getElementById("pop3Panel").hidden = true;
+ } else if (gServerSettings.serverType == "pop3") {
+ document.getElementById("imapPanel").hidden = true;
+ let radioGroup = document.getElementById("folderStorage");
+
+ gFirstDeferredAccount = gServerSettings.deferredToAccount;
+ let folderPopup = document.getElementById("deferredServerPopup");
+
+ // The current account should not be shown in the folder picker
+ // of the "other account" option.
+ folderPopup._teardown();
+ folderPopup.setAttribute(
+ "excludeServers",
+ gServerSettings.account.incomingServer.key
+ );
+ folderPopup._ensureInitialized();
+
+ if (gFirstDeferredAccount.length) {
+ // The current account is deferred.
+ let account = MailServices.accounts.getAccount(gFirstDeferredAccount);
+ radioGroup.value = "otherAccount";
+ folderPopup.selectFolder(account.incomingServer.rootFolder);
+ } else {
+ // Current account is not deferred.
+ radioGroup.value = "currentAccount";
+ // If there are no suitable accounts to defer to, then the menulist is
+ // disabled by the picker with an appropriate message.
+ folderPopup.selectFolder();
+ if (gServerSettings.account.incomingServer.isDeferredTo) {
+ // Some other account already defers to this account
+ // therefore this one can't be deferred further.
+ radioGroup.disabled = true;
+ }
+ }
+
+ let picker = document.getElementById("deferredServerFolderPicker");
+ picker.disabled = radioGroup.selectedIndex != 1;
+ }
+
+ var controls = getControls();
+
+ for (let i = 0; i < controls.length; i++) {
+ var slot = controls[i].id;
+ if (slot in gServerSettings) {
+ if (controls[i].localName == "checkbox") {
+ controls[i].checked = gServerSettings[slot];
+ } else {
+ controls[i].value = gServerSettings[slot];
+ }
+ }
+ }
+}
+
+function onOk(event) {
+ // Handle account deferral settings for POP3 accounts.
+ if (gServerSettings.serverType == "pop3") {
+ var radioGroup = document.getElementById("folderStorage");
+ var gPrefsBundle = document.getElementById("bundle_prefs");
+ let picker = document.getElementById("deferredServerFolderPicker");
+
+ // This account wasn't previously deferred, but is now deferred.
+ if (radioGroup.value != "currentAccount" && !gFirstDeferredAccount.length) {
+ // If the user hasn't selected a folder, keep the default.
+ if (!picker.selectedItem) {
+ return;
+ }
+
+ var confirmDeferAccount = gPrefsBundle.getString(
+ "confirmDeferAccountWarning"
+ );
+
+ var confirmTitle = gPrefsBundle.getString("confirmDeferAccountTitle");
+
+ if (!Services.prompt.confirm(window, confirmTitle, confirmDeferAccount)) {
+ event.preventDefault();
+ return;
+ }
+ }
+ switch (radioGroup.value) {
+ case "currentAccount":
+ gServerSettings.deferredToAccount = "";
+ break;
+ case "otherAccount":
+ let server = picker.selectedItem._folder.server;
+ let account = MailServices.accounts.FindAccountForServer(server);
+ gServerSettings.deferredToAccount = account.key;
+ break;
+ }
+ }
+
+ // Save the controls back to the "gServerSettings" array.
+ var controls = getControls();
+ for (let i = 0; i < controls.length; i++) {
+ var slot = controls[i].id;
+ if (slot in gServerSettings) {
+ if (controls[i].localName == "checkbox") {
+ gServerSettings[slot] = controls[i].checked;
+ } else {
+ gServerSettings[slot] = controls[i].value;
+ }
+ }
+ }
+}
+
+// Set radio element choices and picker states
+function updateInboxAccount(enablePicker) {
+ document.getElementById("deferredServerFolderPicker").disabled =
+ !enablePicker;
+ document.getElementById("deferGetNewMail").disabled = !enablePicker;
+}
diff --git a/comm/mailnews/base/prefs/content/am-server-advanced.xhtml b/comm/mailnews/base/prefs/content/am-server-advanced.xhtml
new file mode 100644
index 0000000000..b8349af38f
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-server-advanced.xhtml
@@ -0,0 +1,222 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-server-advanced.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ scrolling="false"
+>
+ <head>
+ <title>&serverAdvanced.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-server-advanced.js"
+ ></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <dialog buttons="accept,cancel" style="width: 100vw; height: 100vh">
+ <stringbundle
+ id="bundle_prefs"
+ src="chrome://messenger/locale/prefs.properties"
+ />
+
+ <label id="serverPrettyName" />
+
+ <separator class="thin" />
+
+ <!-- IMAP Panel -->
+ <vbox id="imapPanel">
+ <hbox align="center">
+ <label
+ id="serverDirectoryLabel"
+ value="&serverDirectory.label;"
+ accesskey="&serverDirectory.accesskey;"
+ control="serverDirectory"
+ />
+ <hbox class="input-container">
+ <html:input
+ id="serverDirectory"
+ type="text"
+ class="input-inline"
+ aria-labelledby="serverDirectoryLabel"
+ amsa_persist="true"
+ />
+ </hbox>
+ </hbox>
+
+ <checkbox
+ id="usingSubscription"
+ amsa_persist="true"
+ label="&usingSubscription.label;"
+ accesskey="&usingSubscription.accesskey;"
+ />
+
+ <checkbox
+ id="dualUseFolders"
+ amsa_persist="true"
+ label="&dualUseFolders.label;"
+ accesskey="&dualUseFolders.accesskey;"
+ />
+
+ <separator class="groove" />
+ <hbox align="center">
+ <label
+ id="maximumConnectionsNumberLabel"
+ control="maximumConnectionsNumber"
+ value="&maximumConnectionsNumber.label;"
+ accesskey="&maximumConnectionsNumber.accesskey;"
+ />
+ <html:input
+ id="maximumConnectionsNumber"
+ type="number"
+ class="size3 input-inline"
+ min="1"
+ max="1000"
+ amsa_persist="true"
+ aria-labelledby="maximumConnectionsNumberLabel"
+ />
+ </hbox>
+
+ <separator class="groove" />
+ <description>&namespaceDesc.label;</description>
+ <hbox class="indent">
+ <vbox>
+ <hbox flex="1" align="center">
+ <label
+ id="personalNamespaceLabel"
+ control="personalNamespace"
+ value="&personalNamespace.label;"
+ accesskey="&personalNamespace.accesskey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="publicNamespaceLabel"
+ control="publicNamespace"
+ value="&publicNamespace.label;"
+ accesskey="&publicNamespace.accesskey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <label
+ id="otherUsersNamespaceLabel"
+ control="otherUsersNamespace"
+ value="&otherUsersNamespace.label;"
+ accesskey="&otherUsersNamespace.accesskey;"
+ />
+ </hbox>
+ </vbox>
+ <vbox>
+ <html:input
+ id="personalNamespace"
+ type="text"
+ class="input-inline"
+ aria-labelledby="personalNamespaceLabel"
+ amsa_persist="true"
+ />
+ <html:input
+ id="publicNamespace"
+ type="text"
+ class="input-inline"
+ aria-labelledby="publicNamespaceLabel"
+ amsa_persist="true"
+ />
+ <html:input
+ id="otherUsersNamespace"
+ type="text"
+ class="input-inline"
+ aria-labelledby="otherUsersNamespaceLabel"
+ amsa_persist="true"
+ />
+ </vbox>
+ </hbox>
+ <hbox class="indent">
+ <checkbox
+ id="overrideNamespaces"
+ amsa_persist="true"
+ label="&overrideNamespaces.label;"
+ accesskey="&overrideNamespaces.accesskey;"
+ />
+ </hbox>
+ </vbox>
+
+ <!-- POP3 Panel -->
+ <vbox id="pop3Panel">
+ <label flex="1" control="folderStorage"
+ >&pop3DeferringDesc.label;</label
+ >
+ <hbox align="center">
+ <radiogroup
+ id="folderStorage"
+ flex="1"
+ orient="horizontal"
+ amsa_persist="true"
+ onselect="updateInboxAccount(this.selectedIndex == 1);"
+ >
+ <vbox flex="1">
+ <hbox>
+ <radio
+ id="deferToCurrentAccount"
+ value="currentAccount"
+ label="&accountInbox.label;"
+ accesskey="&accountInbox.accesskey;"
+ />
+ </hbox>
+ <vbox flex="1">
+ <hbox>
+ <radio
+ id="deferToOtherAccount"
+ value="otherAccount"
+ label="&deferToServer.label;"
+ accesskey="&deferToServer.accesskey;"
+ >
+ </radio>
+ <menulist
+ id="deferredServerFolderPicker"
+ class="folderMenuItem"
+ flex="1"
+ aria-labelledby="deferToServer"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="deferredServerPopup"
+ expandFolders="false"
+ mode="deferred"
+ oncommand="this.selectFolder(event.target._folder);"
+ />
+ </menulist>
+ </hbox>
+ <checkbox
+ amsa_persist="true"
+ id="deferGetNewMail"
+ label="&deferGetNewMail.label;"
+ class="indent"
+ accesskey="&deferGetNewMail.accesskey;"
+ />
+ </vbox>
+ </vbox>
+ </radiogroup>
+ </hbox>
+ </vbox>
+ </dialog>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-server.js b/comm/mailnews/base/prefs/content/am-server.js
new file mode 100644
index 0000000000..f73535b35c
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-server.js
@@ -0,0 +1,615 @@
+/* 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 am-prefs.js */
+/* import-globals-from amUtils.js */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+var gServer;
+var gOriginalStoreType;
+
+/**
+ * Called when the store type menu is clicked.
+ *
+ * @param {object} aStoreTypeElement - store type menu list element.
+ */
+function clickStoreTypeMenu(aStoreTypeElement) {
+ if (aStoreTypeElement.value == gOriginalStoreType) {
+ return;
+ }
+
+ // Response from migration dialog modal. If the conversion is complete
+ // 'response.newRootFolder' will hold the path to the new account root folder,
+ // otherwise 'response.newRootFolder' will be null.
+ let response = { newRootFolder: null };
+ // Send 'response' as an argument to converterDialog.xhtml.
+ window.browsingContext.topChromeWindow.openDialog(
+ "converterDialog.xhtml",
+ "mailnews:mailstoreconverter",
+ "modal,centerscreen,resizable=no,width=700,height=130",
+ gServer,
+ aStoreTypeElement.value,
+ response
+ );
+ changeStoreType(response);
+}
+
+/**
+ * Revert store type to the original store type if converter modal closes
+ * before migration is complete, otherwise change original store type to
+ * currently selected store type.
+ *
+ * @param {object} aResponse - response from migration dialog modal.
+ */
+function changeStoreType(aResponse) {
+ if (aResponse.newRootFolder) {
+ // The conversion is complete.
+ // Set local path to the new account root folder which is present
+ // in 'aResponse.newRootFolder'.
+ if (gServer.type == "nntp") {
+ let newRootFolder = aResponse.newRootFolder;
+ let lastSlash = newRootFolder.lastIndexOf("/");
+ let newsrc =
+ newRootFolder.slice(0, lastSlash) +
+ "/newsrc-" +
+ newRootFolder.slice(lastSlash + 1);
+ document.getElementById("nntp.newsrcFilePath").value = newsrc;
+ }
+
+ document.getElementById("server.localPath").value = aResponse.newRootFolder;
+ gOriginalStoreType = document.getElementById(
+ "server.storeTypeMenulist"
+ ).value;
+ MailUtils.restartApplication();
+ } else {
+ // The conversion failed or was cancelled.
+ // Restore selected item to what was selected before conversion.
+ document.getElementById("server.storeTypeMenulist").value =
+ gOriginalStoreType;
+ }
+}
+
+function onSave() {
+ let storeContractID = document.getElementById("server.storeTypeMenulist")
+ .selectedItem.value;
+ document
+ .getElementById("server.storeContractID")
+ .setAttribute("value", storeContractID);
+}
+
+function onInit(aPageId, aServerId) {
+ initServerType();
+
+ onCheckItem("server.biffMinutes", ["server.doBiff"]);
+ onCheckItem("nntp.maxArticles", ["nntp.notifyOn"]);
+ setupMailOnServerUI();
+ setupFixedUI();
+ let serverType = document.getElementById("server.type").getAttribute("value");
+ if (serverType == "imap") {
+ setupImapDeleteUI(aServerId);
+ }
+
+ // OAuth2 are only supported on IMAP and POP.
+ document.getElementById("authMethod-oauth2").hidden =
+ serverType != "imap" && serverType != "pop3";
+ // TLS Cert (External) only supported on IMAP.
+ document.getElementById("authMethod-external").hidden = serverType != "imap";
+
+ // "STARTTLS, if available" is vulnerable to MITM attacks so we shouldn't
+ // allow users to choose it anymore. Hide the option unless the user already
+ // has it set.
+ hideUnlessSelected(document.getElementById("connectionSecurityType-1"));
+
+ // UI for account store type.
+ let storeTypeElement = document.getElementById("server.storeTypeMenulist");
+ // set the menuitem to match the account
+ let currentStoreID = document
+ .getElementById("server.storeContractID")
+ .getAttribute("value");
+ let targetItem = storeTypeElement.getElementsByAttribute(
+ "value",
+ currentStoreID
+ );
+ storeTypeElement.selectedItem = targetItem[0];
+ // Disable store type change if store has not been used yet.
+ storeTypeElement.setAttribute(
+ "disabled",
+ gServer.getBoolValue("canChangeStoreType")
+ ? "false"
+ : !Services.prefs.getBoolPref("mail.store_conversion_enabled")
+ );
+ // Initialise 'gOriginalStoreType' to the item that was originally selected.
+ gOriginalStoreType = storeTypeElement.value;
+}
+
+function onPreInit(account, accountValues) {
+ var type = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ hideShowControls(type);
+
+ gServer = account.incomingServer;
+}
+
+function initServerType() {
+ var serverType = document.getElementById("server.type").getAttribute("value");
+ var propertyName = "serverType-" + serverType;
+
+ var messengerBundle = document.getElementById("bundle_messenger");
+ var verboseName;
+ try {
+ verboseName = messengerBundle.getString(propertyName);
+ } catch (e) {
+ // Addon-provided server types do not have a description string,
+ // then display the raw server type.
+ verboseName = serverType;
+ }
+ setDivText("servertypeVerbose", verboseName);
+
+ secureSelect(true);
+
+ setLabelFromStringBundle("authMethod-no", "authNo");
+ setLabelFromStringBundle("authMethod-old", "authOld");
+ setLabelFromStringBundle("authMethod-kerberos", "authKerberos");
+ setLabelFromStringBundle("authMethod-external", "authExternal");
+ setLabelFromStringBundle("authMethod-ntlm", "authNTLM");
+ setLabelFromStringBundle("authMethod-oauth2", "authOAuth2");
+ setLabelFromStringBundle("authMethod-anysecure", "authAnySecure");
+ setLabelFromStringBundle("authMethod-any", "authAny");
+ setLabelFromStringBundle(
+ "authMethod-password-encrypted",
+ "authPasswordEncrypted"
+ );
+ // authMethod-password-cleartext already set in secureSelect()
+
+ // Hide deprecated/hidden auth options, unless selected
+ hideUnlessSelected(document.getElementById("authMethod-no"));
+ hideUnlessSelected(document.getElementById("authMethod-old"));
+ hideUnlessSelected(document.getElementById("authMethod-anysecure"));
+ hideUnlessSelected(document.getElementById("authMethod-any"));
+}
+
+function hideUnlessSelected(element) {
+ element.hidden = !element.selected;
+}
+
+function setLabelFromStringBundle(elementID, stringName) {
+ document.getElementById(elementID).label = document
+ .getElementById("bundle_messenger")
+ .getString(stringName);
+}
+
+function setDivText(divname, value) {
+ var div = document.getElementById(divname);
+ if (!div) {
+ return;
+ }
+ div.setAttribute("value", value);
+}
+
+function onAdvanced() {
+ // Store the server type and, if an IMAP or POP3 server,
+ // the settings needed for the IMAP/POP3 tab into the array
+ var serverSettings = {};
+ var serverType = document.getElementById("server.type").getAttribute("value");
+ serverSettings.serverType = serverType;
+
+ serverSettings.serverPrettyName = gServer.prettyName;
+ serverSettings.account = top.getCurrentAccount();
+
+ if (serverType == "imap") {
+ serverSettings.dualUseFolders = document.getElementById(
+ "imap.dualUseFolders"
+ ).checked;
+ serverSettings.usingSubscription = document.getElementById(
+ "imap.usingSubscription"
+ ).checked;
+ serverSettings.maximumConnectionsNumber = document
+ .getElementById("imap.maximumConnectionsNumber")
+ .getAttribute("value");
+ serverSettings.personalNamespace = document
+ .getElementById("imap.personalNamespace")
+ .getAttribute("value");
+ serverSettings.publicNamespace = document
+ .getElementById("imap.publicNamespace")
+ .getAttribute("value");
+ serverSettings.serverDirectory = document
+ .getElementById("imap.serverDirectory")
+ .getAttribute("value");
+ serverSettings.otherUsersNamespace = document
+ .getElementById("imap.otherUsersNamespace")
+ .getAttribute("value");
+ serverSettings.overrideNamespaces = document.getElementById(
+ "imap.overrideNamespaces"
+ ).checked;
+ } else if (serverType == "pop3") {
+ serverSettings.deferGetNewMail = document.getElementById(
+ "pop3.deferGetNewMail"
+ ).checked;
+ serverSettings.deferredToAccount = document
+ .getElementById("pop3.deferredToAccount")
+ .getAttribute("value");
+ }
+
+ let onCloseAdvanced = function () {
+ if (serverType == "imap") {
+ document.getElementById("imap.dualUseFolders").checked =
+ serverSettings.dualUseFolders;
+ document.getElementById("imap.usingSubscription").checked =
+ serverSettings.usingSubscription;
+ document
+ .getElementById("imap.maximumConnectionsNumber")
+ .setAttribute("value", serverSettings.maximumConnectionsNumber);
+ document
+ .getElementById("imap.personalNamespace")
+ .setAttribute("value", serverSettings.personalNamespace);
+ document
+ .getElementById("imap.publicNamespace")
+ .setAttribute("value", serverSettings.publicNamespace);
+ document
+ .getElementById("imap.serverDirectory")
+ .setAttribute("value", serverSettings.serverDirectory);
+ document
+ .getElementById("imap.otherUsersNamespace")
+ .setAttribute("value", serverSettings.otherUsersNamespace);
+ document.getElementById("imap.overrideNamespaces").checked =
+ serverSettings.overrideNamespaces;
+ } else if (serverType == "pop3") {
+ document.getElementById("pop3.deferGetNewMail").checked =
+ serverSettings.deferGetNewMail;
+ document
+ .getElementById("pop3.deferredToAccount")
+ .setAttribute("value", serverSettings.deferredToAccount);
+ let pop3Server = gServer.QueryInterface(Ci.nsIPop3IncomingServer);
+ // we're explicitly setting this so we'll go through the SetDeferredToAccount method
+ pop3Server.deferredToAccount = serverSettings.deferredToAccount;
+ // Setting the server to be deferred causes a rebuild of the account tree,
+ // losing the current selection. Reselect the current server again as it
+ // didn't really disappear.
+ parent.selectServer(
+ parent.getCurrentAccount().incomingServer,
+ parent.currentPageId
+ );
+
+ // Iterate over all accounts to see if any of their junk targets are now
+ // invalid (pointed to the account that is now deferred).
+ // If any such target is found it is reset to a new safe folder
+ // (the deferred to account or Local Folders). If junk was really moved
+ // to that folder (moveOnSpam = true) then moving junk is disabled
+ // (so that the user notices it and checks the settings).
+ // This is the same sanitization as in am-junk.js, just applied to all POP accounts.
+ let deferredURI =
+ serverSettings.deferredToAccount &&
+ MailServices.accounts.getAccount(serverSettings.deferredToAccount)
+ .incomingServer.serverURI;
+
+ for (let account of MailServices.accounts.accounts) {
+ let accountValues = parent.getValueArrayFor(account);
+ let type = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "type",
+ null,
+ false
+ );
+ // Try to keep this list of account types not having Junk settings
+ // synchronized with the list in AccountManager.js.
+ if (type != "nntp" && type != "rss" && type != "im") {
+ let spamActionTargetAccount = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "spamActionTargetAccount",
+ "string",
+ true
+ );
+ let spamActionTargetFolder = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "spamActionTargetFolder",
+ "wstring",
+ true
+ );
+ let moveOnSpam = parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "moveOnSpam",
+ "bool",
+ true
+ );
+
+ // Check if there are any invalid junk targets and fix them.
+ [spamActionTargetAccount, spamActionTargetFolder, moveOnSpam] =
+ sanitizeJunkTargets(
+ spamActionTargetAccount,
+ spamActionTargetFolder,
+ deferredURI || account.incomingServer.serverURI,
+ parent.getAccountValue(
+ account,
+ accountValues,
+ "server",
+ "moveTargetMode",
+ "int",
+ true
+ ),
+ account.incomingServer.spamSettings,
+ moveOnSpam
+ );
+
+ parent.setAccountValue(
+ accountValues,
+ "server",
+ "moveOnSpam",
+ moveOnSpam
+ );
+ parent.setAccountValue(
+ accountValues,
+ "server",
+ "spamActionTargetAccount",
+ spamActionTargetAccount
+ );
+ parent.setAccountValue(
+ accountValues,
+ "server",
+ "spamActionTargetFolder",
+ spamActionTargetFolder
+ );
+ }
+ }
+ }
+ document.dispatchEvent(new CustomEvent("prefchange"));
+ };
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/am-server-advanced.xhtml",
+ { closingCallback: onCloseAdvanced },
+ serverSettings
+ );
+}
+
+function secureSelect(aLoading) {
+ var socketType = document.getElementById("server.socketType").value;
+ var defaultPort = gServer.protocolInfo.getDefaultServerPort(false);
+ var defaultPortSecure = gServer.protocolInfo.getDefaultServerPort(true);
+ var port = document.getElementById("server.port");
+ var portDefault = document.getElementById("defaultPort");
+ var prevDefaultPort = portDefault.value;
+
+ if (socketType == Ci.nsMsgSocketType.SSL) {
+ portDefault.value = defaultPortSecure;
+ if (
+ port.value == "" ||
+ (!aLoading &&
+ port.value == defaultPort &&
+ prevDefaultPort != portDefault.value)
+ ) {
+ port.value = defaultPortSecure;
+ }
+ } else {
+ portDefault.value = defaultPort;
+ if (
+ port.value == "" ||
+ (!aLoading &&
+ port.value == defaultPortSecure &&
+ prevDefaultPort != portDefault.value)
+ ) {
+ port.value = defaultPort;
+ }
+ }
+
+ // switch "insecure password" label
+ setLabelFromStringBundle(
+ "authMethod-password-cleartext",
+ socketType == Ci.nsMsgSocketType.SSL ||
+ socketType == Ci.nsMsgSocketType.alwaysSTARTTLS
+ ? "authPasswordCleartextViaSSL"
+ : "authPasswordCleartextInsecurely"
+ );
+}
+
+function setupMailOnServerUI() {
+ onCheckItem("pop3.deleteMailLeftOnServer", ["pop3.leaveMessagesOnServer"]);
+ setupAgeMsgOnServerUI();
+}
+
+function setupAgeMsgOnServerUI() {
+ const kLeaveMsgsId = "pop3.leaveMessagesOnServer";
+ const kDeleteByAgeId = "pop3.deleteByAgeFromServer";
+ onCheckItem(kDeleteByAgeId, [kLeaveMsgsId]);
+ onCheckItem("daysEnd", [kLeaveMsgsId]);
+ onCheckItem("pop3.numDaysToLeaveOnServer", [kLeaveMsgsId, kDeleteByAgeId]);
+}
+
+function setupFixedUI() {
+ var controls = [
+ document.getElementById("fixedServerName"),
+ document.getElementById("fixedUserName"),
+ document.getElementById("fixedServerPort"),
+ ];
+
+ var len = controls.length;
+ for (let i = 0; i < len; i++) {
+ var fixedElement = controls[i];
+ var otherElement = document.getElementById(
+ fixedElement.getAttribute("use")
+ );
+
+ fixedElement.setAttribute("collapsed", "true");
+ otherElement.removeAttribute("collapsed");
+ }
+}
+
+function BrowseForNewsrc() {
+ const nsIFilePicker = Ci.nsIFilePicker;
+ const nsIFile = Ci.nsIFile;
+
+ var newsrcTextBox = document.getElementById("nntp.newsrcFilePath");
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ fp.init(
+ window,
+ document.getElementById("browseForNewsrc").getAttribute("filepickertitle"),
+ nsIFilePicker.modeSave
+ );
+
+ var currentNewsrcFile;
+ try {
+ currentNewsrcFile = Cc["@mozilla.org/file/local;1"].createInstance(nsIFile);
+ currentNewsrcFile.initWithPath(newsrcTextBox.value);
+ } catch (e) {
+ dump("Failed to create nsIFile instance for the current newsrc file.\n");
+ }
+
+ if (currentNewsrcFile) {
+ fp.displayDirectory = currentNewsrcFile.parent;
+ fp.defaultString = currentNewsrcFile.leafName;
+ }
+
+ fp.appendFilters(nsIFilePicker.filterAll);
+
+ fp.open(rv => {
+ if (rv != nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+ newsrcTextBox.value = fp.file.path;
+ newsrcTextBox.dispatchEvent(new CustomEvent("change"));
+ });
+}
+
+function setupImapDeleteUI(aServerId) {
+ // read delete_model preference
+ let deleteModel = document
+ .getElementById("imap.deleteModel")
+ .getAttribute("value");
+ selectImapDeleteModel(deleteModel);
+
+ // read trash folder path preference
+ let trashFolderName = getTrashFolderName();
+
+ // set folderPicker menulist
+ let trashPopup = document.getElementById("msgTrashFolderPopup");
+ trashPopup._teardown();
+ trashPopup._parentFolder = MailUtils.getOrCreateFolder(aServerId);
+ trashPopup._ensureInitialized();
+
+ // Escape backslash and double-quote with another backslash before encoding.
+ let trashEscaped = trashFolderName.replace(/([\\"])/g, "\\$1");
+
+ // Convert the folder path from JS Unicode to MUTF-7 if necessary.
+ let imapServer = trashPopup._parentFolder.server.QueryInterface(
+ Ci.nsIImapIncomingServer
+ );
+
+ let trashFolder;
+ if (imapServer.utf8AcceptEnabled) {
+ // Trash folder with UTF8=ACCEPT capability in effect.
+ trashFolder = MailUtils.getOrCreateFolder(aServerId + "/" + trashEscaped);
+ } else {
+ // Traditional MUTF-7.
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ trashFolder = MailUtils.getOrCreateFolder(
+ aServerId + "/" + manager.unicodeToMutf7(trashEscaped)
+ );
+ }
+ trashPopup.selectFolder(trashFolder);
+ trashPopup.parentNode.folder = trashFolder;
+}
+
+function selectImapDeleteModel(choice) {
+ // set deleteModel to selected mode
+ document.getElementById("imap.deleteModel").setAttribute("value", choice);
+
+ switch (choice) {
+ case "0": // markDeleted
+ // disable folderPicker
+ document
+ .getElementById("msgTrashFolderPicker")
+ .setAttribute("disabled", "true");
+ break;
+ case "1": // moveToTrashFolder
+ // enable folderPicker
+ document
+ .getElementById("msgTrashFolderPicker")
+ .removeAttribute("disabled");
+ break;
+ case "2": // deleteImmediately
+ // disable folderPicker
+ document
+ .getElementById("msgTrashFolderPicker")
+ .setAttribute("disabled", "true");
+ break;
+ default:
+ dump("Error in enabling/disabling server.TrashFolderPicker\n");
+ break;
+ }
+}
+
+// Capture any menulist changes from folderPicker
+function folderPickerChange(aEvent) {
+ let folder = aEvent.target._folder;
+ // Since we need to deal with localised folder names, we simply use
+ // the path of the URI like we do in nsImapIncomingServer::DiscoveryDone().
+ // Note that the path is returned with a leading slash which we need to remove.
+ let folderPath = Services.io.newURI(folder.URI).pathQueryRef.substring(1);
+ let folderPathUnescaped = Services.io.unescapeString(
+ folderPath,
+ Ci.nsINetUtil.ESCAPE_URL_PATH
+ );
+
+ // Convert the folder path from MUTF-7 or UTF-8 to Unicode.
+ let imapServer = folder.server.QueryInterface(Ci.nsIImapIncomingServer);
+
+ let trashUnicode;
+ if (imapServer.utf8AcceptEnabled) {
+ // UTF8=ACCEPT capability in effect. Unescaping has brought back
+ // raw UTF-8 bytes, so convert them to JS Unicode.
+ let typedarray = new Uint8Array(folderPathUnescaped.length);
+ for (let i = 0; i < folderPathUnescaped.length; i++) {
+ typedarray[i] = folderPathUnescaped.charCodeAt(i);
+ }
+ let utf8Decoder = new TextDecoder("utf-8");
+ trashUnicode = utf8Decoder.decode(typedarray);
+ } else {
+ // We need to convert that from MUTF-7 to Unicode.
+ let manager = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ trashUnicode = manager.mutf7ToUnicode(folderPathUnescaped);
+ }
+
+ // Set the value to be persisted.
+ document
+ .getElementById("imap.trashFolderName")
+ .setAttribute("value", trashUnicode);
+
+ // Update the widget to show/do correct things even for subfolders.
+ let trashFolderPicker = document.getElementById("msgTrashFolderPicker");
+ trashFolderPicker.menupopup.selectFolder(folder);
+}
+
+// Get trash_folder_name from prefs. Despite its name this returns
+// a folder path, for example INBOX/Trash.
+function getTrashFolderName() {
+ let trashFolderName = document
+ .getElementById("imap.trashFolderName")
+ .getAttribute("value");
+ // if the preference hasn't been set, set it to a sane default
+ if (!trashFolderName) {
+ trashFolderName = "Trash"; // XXX Is this a useful default?
+ document
+ .getElementById("imap.trashFolderName")
+ .setAttribute("value", trashFolderName);
+ }
+ return trashFolderName;
+}
diff --git a/comm/mailnews/base/prefs/content/am-server.xhtml b/comm/mailnews/base/prefs/content/am-server.xhtml
new file mode 100644
index 0000000000..b41e93e290
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-server.xhtml
@@ -0,0 +1,674 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at https://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/icons.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/folderMenus.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % trashDTD SYSTEM "chrome://messenger/locale/am-server-top.dtd">
+%trashDTD; ]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&serverSettings.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-server.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/am-prefs.js"></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/menulist-charsetpicker.js"
+ ></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-server.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <label hidden="true" wsm_persist="true" id="server.type" />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ preftype="string"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.storeContractID"
+ genericattr="true"
+ id="server.storeContractID"
+ />
+
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&serverSettings.label;" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <div xmlns="http://www.w3.org/1999/xhtml" id="amServerSetting">
+ <div>
+ <xul:label value="&serverType.label;" />
+ </div>
+ <div>
+ <xul:label id="servertypeVerbose" />
+ </div>
+ <div style="grid-row: 2">
+ <xul:label
+ value="&serverName.label;"
+ accesskey="&serverName.accesskey;"
+ control="server.hostName"
+ />
+ </div>
+ <div class="input-container" style="grid-row: 2">
+ <xul:label
+ id="fixedServerName"
+ collapsed="true"
+ use="server.hostName"
+ />
+ <html:input
+ id="server.hostName"
+ type="text"
+ wsm_persist="true"
+ size="20"
+ prefstring="mail.server.%serverkey%.hostname"
+ class="uri-element input-flex input-inline"
+ aria-labelledby="fixedServerName"
+ />
+ </div>
+ <div class="input-container" style="grid-row: 2">
+ <xul:label
+ value="&port.label;"
+ accesskey="&port.accesskey;"
+ control="server.port"
+ />
+ <xul:label id="fixedServerPort" collapsed="true" use="server.port" />
+ <html:input
+ id="server.port"
+ type="number"
+ class="size3"
+ min="1"
+ max="65535"
+ wsm_persist="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.port"
+ />
+ <xul:label value="&serverPortDefault.label;" />
+ <xul:label id="defaultPort" />
+ </div>
+ <div hidefor="nntp" style="grid-row: 3">
+ <xul:label
+ value="&userName.label;"
+ accesskey="&userName.accesskey;"
+ control="server.username"
+ />
+ </div>
+ <div class="input-container" hidefor="nntp" style="grid-row: 3">
+ <xul:label
+ id="fixedUserName"
+ collapsed="true"
+ use="server.username"
+ />
+ <html:input
+ id="server.username"
+ type="text"
+ wsm_persist="true"
+ size="20"
+ class="input-flex input-inline"
+ prefstring="mail.server.%serverkey%.username"
+ aria-labelledby="fixedUserName"
+ />
+ </div>
+ </div>
+
+ <separator />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&securitySettings.label;</html:legend>
+ <html:table>
+ <html:tr>
+ <html:td>
+ <label
+ value="&connectionSecurity.label;"
+ accesskey="&connectionSecurity.accesskey;"
+ control="server.socketType"
+ />
+ </html:td>
+ <html:td>
+ <menulist
+ wsm_persist="true"
+ id="server.socketType"
+ oncommand="secureSelect();"
+ style="width: 100%"
+ >
+ <menupopup id="server.socketTypePopup">
+ <menuitem
+ value="0"
+ label="&connectionSecurityType-0.label;"
+ />
+ <menuitem
+ id="connectionSecurityType-1"
+ value="1"
+ label="&connectionSecurityType-1.label;"
+ disabled="true"
+ />
+ <menuitem
+ value="2"
+ label="&connectionSecurityType-2.label;"
+ hidefor="nntp"
+ />
+ <menuitem
+ value="3"
+ label="&connectionSecurityType-3.label;"
+ />
+ </menupopup>
+ </menulist>
+ </html:td>
+ </html:tr>
+ <html:tr>
+ <html:td>
+ <label
+ value="&authMethod.label;"
+ accesskey="&authMethod.accesskey;"
+ control="server.authMethod"
+ hidefor="nntp"
+ />
+ </html:td>
+ <html:td>
+ <menulist
+ id="server.authMethod"
+ wsm_persist="true"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.authMethod"
+ hidefor="nntp"
+ style="width: 100%"
+ >
+ <menupopup id="server.authMethodPopup">
+ <menuitem id="authMethod-no" value="1" />
+ <menuitem id="authMethod-old" value="2" />
+ <menuitem id="authMethod-password-cleartext" value="3" />
+ <menuitem id="authMethod-password-encrypted" value="4" />
+ <menuitem id="authMethod-kerberos" value="5" />
+ <menuitem id="authMethod-ntlm" value="6" />
+ <menuitem id="authMethod-external" value="7" />
+ <menuitem id="authMethod-oauth2" value="10" />
+ <menuitem id="authMethod-anysecure" value="8" />
+ <menuitem id="authMethod-any" value="9" />
+ </menupopup>
+ </menulist>
+ </html:td>
+ </html:tr>
+ </html:table>
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&serverSettings.label;</html:legend>
+ <vbox align="start">
+ <checkbox
+ wsm_persist="true"
+ id="server.loginAtStartUp"
+ label="&loginAtStartup.label;"
+ accesskey="&loginAtStartup.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.login_at_startup"
+ />
+ </vbox>
+ <hbox align="center">
+ <checkbox
+ wsm_persist="true"
+ id="server.doBiff"
+ label="&biffStart.label;"
+ accesskey="&biffStart.accesskey;"
+ oncommand="onCheckItem('server.biffMinutes', [this.id]);"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.check_new_mail"
+ />
+ <html:input
+ id="server.biffMinutes"
+ type="number"
+ class="size3"
+ min="1"
+ wsm_persist="true"
+ aria-labelledby="server.doBiff server.biffMinutes biffEnd"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.check_time"
+ />
+ <label
+ id="biffEnd"
+ control="server.biffMinutes"
+ value="&biffEnd.label;"
+ />
+ </hbox>
+ <vbox align="start" hidefor="pop3,nntp">
+ <checkbox
+ wsm_persist="true"
+ id="imap.useIdle"
+ label="&useIdleNotifications.label;"
+ accesskey="&useIdleNotifications.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.use_idle"
+ />
+ </vbox>
+
+ <!-- Necessary for POP3 (Bug 480945) -->
+ <!-- https://bugzilla.mozilla.org/show_bug.cgi?id=480945 -->
+ <vbox align="start" hidefor="imap,nntp">
+ <checkbox
+ wsm_persist="true"
+ id="server.downloadOnBiff"
+ label="&downloadOnBiff.label;"
+ prefattribute="value"
+ accesskey="&downloadOnBiff.accesskey;"
+ prefstring="mail.server.%serverkey%.download_on_biff"
+ />
+ </vbox>
+ <!-- POP3 -->
+ <vbox align="start" hidefor="imap,nntp">
+ <checkbox
+ wsm_persist="true"
+ id="pop3.headersOnly"
+ label="&headersOnly.label;"
+ accesskey="&headersOnly.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.headers_only"
+ />
+
+ <checkbox
+ wsm_persist="true"
+ id="pop3.leaveMessagesOnServer"
+ label="&leaveOnServer.label;"
+ oncommand="setupMailOnServerUI();"
+ accesskey="&leaveOnServer.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.leave_on_server"
+ />
+
+ <hbox align="center">
+ <checkbox
+ wsm_persist="true"
+ id="pop3.deleteByAgeFromServer"
+ class="indent"
+ label="&deleteByAgeFromServer.label;"
+ oncommand="setupAgeMsgOnServerUI();"
+ accesskey="&deleteByAgeFromServer.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.delete_by_age_from_server"
+ />
+ <html:input
+ id="pop3.numDaysToLeaveOnServer"
+ type="number"
+ class="size3"
+ min="1"
+ wsm_persist="true"
+ aria-labelledby="pop3.deleteByAgeFromServer pop3.numDaysToLeaveOnServer daysEnd"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.num_days_to_leave_on_server"
+ />
+ <label
+ id="daysEnd"
+ control="pop3.numDaysToLeaveOnServer"
+ value="&daysEnd.label;"
+ />
+ </hbox>
+
+ <checkbox
+ wsm_persist="true"
+ id="pop3.deleteMailLeftOnServer"
+ class="indent"
+ label="&deleteOnServer2.label;"
+ accesskey="&deleteOnServer2.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.delete_mail_left_on_server"
+ />
+
+ <!-- hidden elements for data transfer to and from advanced... dialog -->
+ <hbox flex="1" hidefor="imap,nntp" hidden="true">
+ <checkbox
+ hidden="true"
+ wsm_persist="true"
+ id="pop3.deferGetNewMail"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.deferGetNewMail"
+ />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="pop3.deferredToAccount"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.deferredToAccount"
+ />
+ </hbox>
+ </vbox>
+ <!-- IMAP -->
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="imap.trashFolderName"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.trash_folder_name"
+ />
+
+ <separator class="thin" />
+
+ <hbox align="center" hidefor="pop3,nntp">
+ <label
+ value="&deleteMessagePrefix.label;"
+ align="start"
+ control="imap.deleteModel"
+ />
+ </hbox>
+ <vbox>
+ <hbox
+ align="center"
+ id="imap.deleteModel.box"
+ hidefor="pop3,nntp"
+ flex="1"
+ >
+ <radiogroup
+ id="imap.deleteModel"
+ wsm_persist="true"
+ oncommand="selectImapDeleteModel(this.value);"
+ prefstring="mail.server.%serverkey%.delete_model"
+ >
+ <hbox class="specialFolderPickerGrid">
+ <vbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="modelMoveToTrash"
+ value="1"
+ label="&modelMoveToTrash.label;"
+ accesskey="&modelMoveToTrash.accesskey;"
+ />
+ <menulist
+ id="msgTrashFolderPicker"
+ class="folderMenuItem"
+ style="max-width: 300px"
+ flex="1"
+ crop="center"
+ aria-labelledby="modelMoveToTrash"
+ displayformat="verbose"
+ >
+ <menupopup
+ is="folder-menupopup"
+ id="msgTrashFolderPopup"
+ mode="filing"
+ showFileHereLabel="true"
+ oncommand="folderPickerChange(event);"
+ />
+ </menulist>
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="modelMarkDeleted"
+ value="0"
+ label="&modelMarkDeleted.label;"
+ accesskey="&modelMarkDeleted.accesskey;"
+ />
+ </hbox>
+ <hbox flex="1" align="center">
+ <radio
+ id="modelDeleteImmediately"
+ value="2"
+ label="&modelDeleteImmediately.label;"
+ accesskey="&modelDeleteImmediately.accesskey;"
+ />
+ </hbox>
+ </vbox>
+ </hbox>
+ </radiogroup>
+ </hbox>
+ <hbox pack="end">
+ <!-- This button should have identical attributes to the
+ server.popAdvancedButton except the hidefor attribute. -->
+ <button
+ label="&advancedButton.label;"
+ accesskey="&advancedButton.accesskey;"
+ oncommand="onAdvanced();"
+ wsm_persist="true"
+ id="server.imapAdvancedButton"
+ prefstring="mail.server.%serverkey%.advanced.disable"
+ hidefor="pop3,nntp"
+ />
+ </hbox>
+ </vbox>
+
+ <!-- Advanced IMAP settings -->
+ <hbox flex="1" hidefor="pop3,nntp" hidden="true">
+ <checkbox
+ hidden="true"
+ wsm_persist="true"
+ id="imap.dualUseFolders"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.dual_use_folders"
+ />
+ <checkbox
+ hidden="true"
+ wsm_persist="true"
+ id="imap.usingSubscription"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.using_subscription"
+ />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="imap.maximumConnectionsNumber"
+ />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="imap.personalNamespace"
+ />
+ <label hidden="true" wsm_persist="true" id="imap.publicNamespace" />
+ <label
+ hidden="true"
+ wsm_persist="true"
+ id="imap.otherUsersNamespace"
+ />
+ <label hidden="true" wsm_persist="true" id="imap.serverDirectory" />
+ <checkbox
+ hidden="true"
+ wsm_persist="true"
+ id="imap.overrideNamespaces"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.override_namespaces"
+ />
+ </hbox>
+
+ <!-- NNTP -->
+ <hbox hidefor="pop3,imap" align="center">
+ <checkbox
+ id="nntp.notifyOn"
+ wsm_persist="true"
+ label="&maxMessagesStart.label;"
+ accesskey="&maxMessagesStart.accesskey;"
+ oncommand="onCheckItem('nntp.maxArticles', [this.id]);"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.notify.on"
+ />
+ <html:input
+ id="nntp.maxArticles"
+ type="number"
+ class="size4"
+ min="1"
+ wsm_persist="true"
+ aria-labelledby="nntp.notifyOn nntp.maxArticles maxMessagesEnd"
+ preftype="int"
+ prefstring="mail.server.%serverkey%.max_articles"
+ />
+ <label
+ control="nntp.maxArticles"
+ value="&maxMessagesEnd.label;"
+ id="maxMessagesEnd"
+ />
+ </hbox>
+ <checkbox
+ hidefor="pop3,imap"
+ wsm_persist="true"
+ id="nntp.pushAuth"
+ label="&alwaysAuthenticate.label;"
+ accesskey="&alwaysAuthenticate.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.always_authenticate"
+ />
+ </html:fieldset>
+ </html:div>
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&messageStorage.label;</html:legend>
+
+ <hbox align="end">
+ <vbox align="start" flex="1" id="exitHandlingBox=">
+ <checkbox
+ hidefor="pop3,nntp"
+ wsm_persist="true"
+ id="imap.cleanupInboxOnExit"
+ label="&expungeOnExit.label;"
+ accesskey="&expungeOnExit.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.cleanup_inbox_on_exit"
+ />
+ <checkbox
+ hidefor="nntp"
+ wsm_persist="true"
+ id="server.emptyTrashOnExit"
+ label="&emptyTrashOnExit.label;"
+ accesskey="&emptyTrashOnExit.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.empty_trash_on_exit"
+ />
+ </vbox>
+ <button
+ label="&advancedButton.label;"
+ accesskey="&advancedButton.accesskey;"
+ oncommand="onAdvanced();"
+ wsm_persist="true"
+ id="server.popAdvancedButton"
+ prefstring="mail.server.%serverkey%.advanced.disable"
+ hidefor="imap,nntp"
+ />
+ </hbox>
+ <hbox align="center">
+ <label
+ value="&storeType.label;"
+ accesskey="&storeType.accesskey;"
+ control="server.storeTypeMenulist"
+ />
+ <menulist
+ id="server.storeTypeMenulist"
+ oncommand="clickStoreTypeMenu(this);"
+ >
+ <menupopup id="server.storeTypeMenupopup">
+ <menuitem
+ id="server.mboxStore"
+ value="@mozilla.org/msgstore/berkeleystore;1"
+ label="&mboxStore2.label;"
+ />
+ <menuitem
+ id="server.maildirStore"
+ value="@mozilla.org/msgstore/maildirstore;1"
+ label="&maildirStore.label;"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+
+ <hbox align="center" hidefor="imap,pop3">
+ <label
+ id="nntp.newsrcFilePath.label"
+ value="&newsrcFilePath1.label;"
+ control="nntp.newsrcFilePath"
+ />
+ <hbox class="input-container" flex="1">
+ <html:input
+ id="nntp.newsrcFilePath"
+ type="text"
+ readonly="readonly"
+ wsm_persist="true"
+ datatype="nsIFile"
+ prefstring="mail.server.%serverkey%.newsrc.file"
+ class="uri-element input-inline"
+ aria-labelledby="nntp.newsrcFilePath.label"
+ />
+ </hbox>
+ <button
+ id="browseForNewsrc"
+ label="&browseNewsrc.label;"
+ filepickertitle="&newsrcPicker1.label;"
+ accesskey="&browseNewsrc.accesskey;"
+ oncommand="BrowseForNewsrc();"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <hbox align="center">
+ <label
+ id="server.localPath.label"
+ value="&localPath1.label;"
+ control="server.localPath"
+ />
+ <hbox class="input-container" flex="1">
+ <html:input
+ id="server.localPath"
+ type="text"
+ readonly="readonly"
+ wsm_persist="true"
+ datatype="nsIFile"
+ prefstring="mail.server.%serverkey%.directory"
+ class="uri-element input-inline"
+ aria-labelledby="server.localPath.label"
+ />
+ </hbox>
+ <button
+ id="browseForLocalFolder"
+ label="&browseFolder.label;"
+ filepickertitle="&localFolderPicker.label;"
+ accesskey="&browseFolder.accesskey;"
+ oncommand="BrowseForLocalFolders();"
+ />
+ </hbox>
+ </html:fieldset>
+ </html:div>
+
+ <hbox hidefor="imap,pop3" align="center" iscontrolcontainer="true">
+ <separator class="thin" />
+ <label value="&serverDefaultCharset2.label;" control="nntp.charset" />
+ <menulist
+ is="menulist-charsetpicker-viewing"
+ id="nntp.charset"
+ hidable="true"
+ hidefor="imap,pop3"
+ wsm_persist="true"
+ preftype="string"
+ prefstring="mail.server.%serverkey%.charset"
+ />
+ </hbox>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-serverwithnoidentities.js b/comm/mailnews/base/prefs/content/am-serverwithnoidentities.js
new file mode 100644
index 0000000000..12c1e7b837
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-serverwithnoidentities.js
@@ -0,0 +1,94 @@
+/* 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 gAccount;
+var gOriginalStoreType;
+
+/**
+ * Called when the store type menu is clicked.
+ *
+ * @param {object} aStoreTypeElement - store type menu list element.
+ */
+function clickStoreTypeMenu(aStoreTypeElement) {
+ if (aStoreTypeElement.value == gOriginalStoreType) {
+ return;
+ }
+
+ // Response from migration dialog modal. If the conversion is complete
+ // 'response.newRootFolder' will hold the path to the new account root folder,
+ // otherwise 'response.newRootFolder' will be null.
+ let response = { newRootFolder: null };
+ // Send 'response' as an argument to converterDialog.xhtml.
+ window.browsingContext.topChromeWindow.openDialog(
+ "converterDialog.xhtml",
+ "mailnews:mailstoreconverter",
+ "modal,centerscreen,resizable=no,width=700,height=130",
+ gAccount.incomingServer,
+ aStoreTypeElement.value,
+ response
+ );
+ changeStoreType(response);
+}
+
+/**
+ * Revert store type to the original store type if converter modal closes
+ * before migration is complete, otherwise change original store type to
+ * currently selected store type.
+ *
+ * @param {object} aResponse - response from migration dialog modal.
+ */
+function changeStoreType(aResponse) {
+ if (aResponse.newRootFolder) {
+ // The conversion is complete.
+ // Set local path to the new account root folder which is present
+ // in 'aResponse.newRootFolder'.
+ document.getElementById("server.localPath").value = aResponse.newRootFolder;
+ gOriginalStoreType = document.getElementById(
+ "server.storeTypeMenulist"
+ ).value;
+ MailUtils.restartApplication();
+ } else {
+ // The conversion failed or was cancelled.
+ // Restore selected item to what was selected before conversion.
+ document.getElementById("server.storeTypeMenulist").value =
+ gOriginalStoreType;
+ }
+}
+
+function onInit(aPageId, aServerId) {
+ // UI for account store type
+ let storeTypeElement = document.getElementById("server.storeTypeMenulist");
+ // set the menuitem to match the account
+ let currentStoreID = document
+ .getElementById("server.storeContractID")
+ .getAttribute("value");
+ let targetItem = storeTypeElement.getElementsByAttribute(
+ "value",
+ currentStoreID
+ );
+ storeTypeElement.selectedItem = targetItem[0];
+ // Disable store type change if store has not been used yet.
+ storeTypeElement.setAttribute(
+ "disabled",
+ gAccount.incomingServer.getBoolValue("canChangeStoreType")
+ ? "false"
+ : !Services.prefs.getBoolPref("mail.store_conversion_enabled")
+ );
+ // Initialise 'gOriginalStoreType' to the item that was originally selected.
+ gOriginalStoreType = storeTypeElement.value;
+}
+
+function onPreInit(account, accountValues) {
+ gAccount = account;
+}
+
+function onSave() {
+ let storeContractID = document.getElementById("server.storeTypeMenulist")
+ .selectedItem.value;
+ document
+ .getElementById("server.storeContractID")
+ .setAttribute("value", storeContractID);
+}
diff --git a/comm/mailnews/base/prefs/content/am-serverwithnoidentities.xhtml b/comm/mailnews/base/prefs/content/am-serverwithnoidentities.xhtml
new file mode 100644
index 0000000000..1cdc36550f
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-serverwithnoidentities.xhtml
@@ -0,0 +1,152 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html [ <!ENTITY % accountNoIdentDTD SYSTEM "chrome://messenger/locale/am-serverwithnoidentities.dtd">
+%accountNoIdentDTD;
+<!ENTITY % accountServerTopDTD SYSTEM "chrome://messenger/locale/am-server-top.dtd">%accountServerTopDTD;
+]>
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&accountTitle.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/am-serverwithnoidentities.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script>
+ // FIXME: move to script file.
+ window.addEventListener("load", event => {
+ parent.onPanelLoaded("am-serverwithnoidentities.xhtml");
+ });
+ </script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <vbox id="containerBox" flex="1">
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&accountTitle.label;" />
+ </hbox>
+
+ <separator class="thin" />
+
+ <label
+ hidden="true"
+ wsm_persist="true"
+ preftype="string"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.storeContractID"
+ genericattr="true"
+ id="server.storeContractID"
+ />
+
+ <description class="secDesc">&accountSettingsDesc.label;</description>
+ <hbox class="input-container">
+ <label
+ id="server.prettyName.label"
+ value="&accountName.label;"
+ control="server.prettyName"
+ accesskey="&accountName.accesskey;"
+ />
+ <html:input
+ id="server.prettyName"
+ type="text"
+ wsm_persist="true"
+ class="input-inline"
+ onblur="parent.setAccountLabel(gAccount.key, this.value);"
+ prefstring="mail.server.%serverkey%.name"
+ aria-labelledby="server.prettyName.label"
+ />
+ </hbox>
+
+ <separator class="thin" />
+
+ <html:div>
+ <html:fieldset>
+ <html:legend>&messageStorage.label;</html:legend>
+
+ <vbox align="start">
+ <checkbox
+ wsm_persist="true"
+ id="server.emptyTrashOnExit"
+ label="&emptyTrashOnExit.label;"
+ accesskey="&emptyTrashOnExit.accesskey;"
+ prefattribute="value"
+ prefstring="mail.server.%serverkey%.empty_trash_on_exit"
+ />
+ <hbox align="center">
+ <label
+ value="&storeType.label;"
+ accesskey="&storeType.accesskey;"
+ control="server.storeTypeMenulist"
+ />
+ <menulist
+ id="server.storeTypeMenulist"
+ oncommand="clickStoreTypeMenu(this);"
+ >
+ <menupopup id="server.storeTypeMenupopup">
+ <menuitem
+ id="server.mboxStore"
+ value="@mozilla.org/msgstore/berkeleystore;1"
+ label="&mboxStore2.label;"
+ />
+ <menuitem
+ id="server.maildirStore"
+ value="@mozilla.org/msgstore/maildirstore;1"
+ label="&maildirStore.label;"
+ />
+ </menupopup>
+ </menulist>
+ </hbox>
+ </vbox>
+
+ <separator class="thin" />
+
+ <hbox align="center">
+ <label
+ id="server.localPath.label"
+ value="&localPath1.label;"
+ control="server.localPath"
+ />
+ <hbox class="input-container" flex="1">
+ <html:input
+ id="server.localPath"
+ type="text"
+ readonly="readonly"
+ wsm_persist="true"
+ datatype="nsIFile"
+ prefstring="mail.server.%serverkey%.directory"
+ class="uri-element input-inline"
+ aria-labelledby="server.localPath.label"
+ />
+ </hbox>
+ <button
+ id="browseForLocalFolder"
+ label="&browseFolder.label;"
+ filepickertitle="&localFolderPicker.label;"
+ accesskey="&browseFolder.accesskey;"
+ oncommand="BrowseForLocalFolders()"
+ />
+ </hbox>
+ </html:fieldset>
+ </html:div>
+ </vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/am-smtp.js b/comm/mailnews/base/prefs/content/am-smtp.js
new file mode 100644
index 0000000000..4dd48fa795
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-smtp.js
@@ -0,0 +1,277 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from amUtils.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+window.addEventListener("DOMContentLoaded", event => {
+ gSmtpServerListWindow.onLoad();
+});
+
+var gSmtpServerListWindow = {
+ mBundle: null,
+ mServerList: null,
+ mAddButton: null,
+ mEditButton: null,
+ mDeleteButton: null,
+ mSetDefaultServerButton: null,
+
+ onLoad() {
+ parent.onPanelLoaded("am-smtp.xhtml");
+
+ this.mBundle = document.getElementById("bundle_messenger");
+ this.mServerList = document.getElementById("smtpList");
+ this.mAddButton = document.getElementById("addButton");
+ this.mEditButton = document.getElementById("editButton");
+ this.mDeleteButton = document.getElementById("deleteButton");
+ this.mSetDefaultServerButton = document.getElementById("setDefaultButton");
+
+ this.refreshServerList("", false);
+
+ this.updateButtons();
+ },
+
+ onSelectionChanged(aEvent) {
+ var server = this.getSelectedServer();
+ if (!server) {
+ return;
+ }
+
+ this.updateButtons();
+ this.updateServerInfoBox(server);
+ },
+
+ onDeleteServer(aEvent) {
+ var server = this.getSelectedServer();
+ if (!server) {
+ return;
+ }
+
+ // confirm deletion
+ let cancel = Services.prompt.confirmEx(
+ window,
+ this.mBundle.getString("smtpServers-confirmServerDeletionTitle"),
+ this.mBundle.getFormattedString(
+ "smtpServers-confirmServerDeletion",
+ [server.hostname],
+ 1
+ ),
+ Services.prompt.STD_YES_NO_BUTTONS,
+ null,
+ null,
+ null,
+ null,
+ {}
+ );
+
+ if (!cancel) {
+ // Remove password information first.
+ try {
+ server.forgetPassword();
+ } catch (e) {
+ /* It is OK if this fails. */
+ }
+ // Remove the server.
+ MailServices.smtp.deleteServer(server);
+ parent.replaceWithDefaultSmtpServer(server.key);
+ this.refreshServerList("", true);
+ }
+ },
+
+ onAddServer(aEvent) {
+ this.openServerEditor(null);
+ },
+
+ onEditServer(aEvent) {
+ let server = this.getSelectedServer();
+ if (!server) {
+ return;
+ }
+
+ this.openServerEditor(server);
+ },
+
+ onSetDefaultServer(aEvent) {
+ let server = this.getSelectedServer();
+ if (!server) {
+ return;
+ }
+
+ MailServices.smtp.defaultServer = server;
+ this.refreshServerList(MailServices.smtp.defaultServer.key, true);
+ },
+
+ updateButtons() {
+ let server = this.getSelectedServer();
+
+ // can't delete default server
+ if (server && MailServices.smtp.defaultServer == server) {
+ this.mSetDefaultServerButton.setAttribute("disabled", "true");
+ this.mDeleteButton.setAttribute("disabled", "true");
+ } else {
+ this.mSetDefaultServerButton.removeAttribute("disabled");
+ this.mDeleteButton.removeAttribute("disabled");
+ }
+
+ if (!server) {
+ this.mEditButton.setAttribute("disabled", "true");
+ } else {
+ this.mEditButton.removeAttribute("disabled");
+ }
+ },
+
+ updateServerInfoBox(aServer) {
+ var noneSelected = this.mBundle.getString("smtpServerList-NotSpecified");
+
+ document.getElementById("nameValue").textContent = aServer.hostname;
+ document.getElementById("descriptionValue").textContent =
+ aServer.description || noneSelected;
+ document.getElementById("portValue").textContent =
+ aServer.port || noneSelected;
+ document.getElementById("userNameValue").textContent =
+ aServer.username || noneSelected;
+ document.getElementById("useSecureConnectionValue").textContent =
+ this.mBundle.getString(
+ "smtpServer-ConnectionSecurityType-" + aServer.socketType
+ );
+
+ const AuthMethod = Ci.nsMsgAuthMethod;
+ const SocketType = Ci.nsMsgSocketType;
+ var authStr = "";
+ switch (aServer.authMethod) {
+ case AuthMethod.none:
+ authStr = "authNo";
+ break;
+ case AuthMethod.passwordEncrypted:
+ authStr = "authPasswordEncrypted";
+ break;
+ case AuthMethod.GSSAPI:
+ authStr = "authKerberos";
+ break;
+ case AuthMethod.NTLM:
+ authStr = "authNTLM";
+ break;
+ case AuthMethod.secure:
+ authStr = "authAnySecure";
+ break;
+ case AuthMethod.passwordCleartext:
+ authStr =
+ aServer.socketType == SocketType.SSL ||
+ aServer.socketType == SocketType.alwaysSTARTTLS
+ ? "authPasswordCleartextViaSSL"
+ : "authPasswordCleartextInsecurely";
+ break;
+ case AuthMethod.OAuth2:
+ authStr = "authOAuth2";
+ break;
+ default:
+ // leave empty
+ console.error(
+ "Warning: unknown value for smtpserver... authMethod: " +
+ aServer.authMethod
+ );
+ }
+ document.getElementById("authMethodValue").textContent = authStr
+ ? this.mBundle.getString(authStr)
+ : noneSelected;
+ },
+
+ refreshServerList(aServerKeyToSelect, aFocusList) {
+ while (this.mServerList.hasChildNodes()) {
+ this.mServerList.lastChild.remove();
+ }
+ for (let server of MailServices.smtp.servers) {
+ let listitem = this.createSmtpListItem(
+ server,
+ MailServices.smtp.defaultServer.key == server.key
+ );
+ this.mServerList.appendChild(listitem);
+ }
+
+ if (aServerKeyToSelect) {
+ this.setSelectedServer(
+ this.mServerList.querySelector('[key="' + aServerKeyToSelect + '"]')
+ );
+ } else {
+ // Select the default server.
+ this.setSelectedServer(
+ this.mServerList.querySelector('[default="true"]')
+ );
+ }
+
+ if (aFocusList) {
+ this.mServerList.focus();
+ }
+ },
+
+ createSmtpListItem(aServer, aIsDefault) {
+ var listitem = document.createXULElement("richlistitem");
+ var serverName = "";
+
+ if (aServer.description) {
+ serverName = aServer.description + " - ";
+ } else if (aServer.username) {
+ serverName = aServer.username + " - ";
+ }
+
+ serverName += aServer.hostname;
+
+ if (aIsDefault) {
+ serverName += " " + this.mBundle.getString("defaultServerTag");
+ listitem.setAttribute("default", "true");
+ }
+
+ let label = document.createXULElement("label");
+ label.setAttribute("value", serverName);
+ listitem.appendChild(label);
+ listitem.setAttribute("key", aServer.key);
+ listitem.setAttribute("class", "smtpServerListItem");
+
+ // give it some unique id
+ listitem.id = "smtpServer." + aServer.key;
+ return listitem;
+ },
+
+ openServerEditor(aServer) {
+ let args = editSMTPServer(aServer);
+
+ // now re-select the server which was just added
+ if (args.result) {
+ this.refreshServerList(aServer ? aServer.key : args.addSmtpServer, true);
+ }
+
+ return args.result;
+ },
+
+ setSelectedServer(aServer) {
+ if (!aServer) {
+ return;
+ }
+
+ setTimeout(
+ function (aServerList) {
+ aServerList.ensureElementIsVisible(aServer);
+ aServerList.selectItem(aServer);
+ },
+ 0,
+ this.mServerList
+ );
+ },
+
+ getSelectedServer() {
+ // The list of servers is a single selection listbox
+ // therefore 1 item is always selected.
+ // But if there are no SMTP servers defined yet, nothing will be selected.
+ let selection = this.mServerList.selectedItem;
+ if (!selection) {
+ return null;
+ }
+
+ let serverKey = selection.getAttribute("key");
+ return MailServices.smtp.getServerByKey(serverKey);
+ },
+};
diff --git a/comm/mailnews/base/prefs/content/am-smtp.xhtml b/comm/mailnews/base/prefs/content/am-smtp.xhtml
new file mode 100644
index 0000000000..851fb8da3e
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/am-smtp.xhtml
@@ -0,0 +1,121 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/accountManage.css" type="text/css"?>
+
+<!DOCTYPE html SYSTEM "chrome://messenger/locale/am-advanced.dtd">
+
+<html
+ xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+>
+ <head>
+ <title>&smtpServer.label;</title>
+ <script
+ defer="defer"
+ src="chrome://messenger/content/globalOverlay.js"
+ ></script>
+ <script
+ defer="defer"
+ src="chrome://global/content/editMenuOverlay.js"
+ ></script>
+ <script defer="defer" src="chrome://messenger/content/amUtils.js"></script>
+ <script defer="defer" src="chrome://messenger/content/am-smtp.js"></script>
+ </head>
+ <html:body
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ >
+ <stringbundle
+ id="bundle_messenger"
+ src="chrome://messenger/locale/messenger.properties"
+ />
+
+ <vbox flex="1" style="overflow: auto"
+ ><vbox id="containerBox" flex="1">
+ <hbox class="dialogheader">
+ <label class="dialogheader-title" value="&smtpServer.label;" />
+ </hbox>
+
+ <separator class="thin" />
+ <label control="smtpList">&smtpDescription.label;</label>
+ <separator class="thin" />
+
+ <hbox flex="1">
+ <richlistbox
+ id="smtpList"
+ onselect="gSmtpServerListWindow.onSelectionChanged(event);"
+ ondblclick="gSmtpServerListWindow.onEditServer(event);"
+ seltype="single"
+ flex="1"
+ style="height: 400px"
+ />
+
+ <vbox>
+ <button
+ id="addButton"
+ oncommand="gSmtpServerListWindow.onAddServer(event);"
+ label="&smtpListAdd.label;"
+ accesskey="&smtpListAdd.accesskey;"
+ />
+ <button
+ id="editButton"
+ oncommand="gSmtpServerListWindow.onEditServer(event);"
+ label="&smtpListEdit.label;"
+ accesskey="&smtpListEdit.accesskey;"
+ />
+ <separator />
+ <button
+ id="deleteButton"
+ disabled="true"
+ oncommand="gSmtpServerListWindow.onDeleteServer(event);"
+ label="&smtpListDelete.label;"
+ accesskey="&smtpListDelete.accesskey;"
+ />
+ <button
+ id="setDefaultButton"
+ disabled="true"
+ oncommand="gSmtpServerListWindow.onSetDefaultServer(event);"
+ label="&smtpListSetDefault.label;"
+ accesskey="&smtpListSetDefault.accesskey;"
+ />
+ </vbox>
+ </hbox>
+
+ <separator />
+
+ <label class="header">&serverDetails.label;</label>
+ <html:table id="smtpServerInfoBox">
+ <html:tr>
+ <html:th id="descriptionLabel">&serverDescription.label;</html:th>
+ <html:td id="descriptionValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="nameLabel">&serverName.label;</html:th>
+ <html:td id="nameValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="portLabel">&serverPort.label;</html:th>
+ <html:td id="portValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="userNameLabel">&userName.label;</html:th>
+ <html:td id="userNameValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="authMethodLabel">&authMethod.label;</html:th>
+ <html:td id="authMethodValue"></html:td>
+ </html:tr>
+ <html:tr>
+ <html:th id="connectionSecurityLabel"
+ >&connectionSecurity.label;</html:th
+ >
+ <html:td id="useSecureConnectionValue"></html:td>
+ </html:tr>
+ </html:table>
+ <separator flex="1" /> </vbox
+ ></vbox>
+ </html:body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/amUtils.js b/comm/mailnews/base/prefs/content/amUtils.js
new file mode 100644
index 0000000000..b63f27b6c4
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/amUtils.js
@@ -0,0 +1,275 @@
+/* -*- Mode: C++; 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/. */
+
+/* import-globals-from am-smtp.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+
+function BrowseForLocalFolders() {
+ const nsIFilePicker = Ci.nsIFilePicker;
+ const nsIFile = Ci.nsIFile;
+
+ var currentFolderTextBox = document.getElementById("server.localPath");
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+
+ fp.init(
+ window,
+ document
+ .getElementById("browseForLocalFolder")
+ .getAttribute("filepickertitle"),
+ nsIFilePicker.modeGetFolder
+ );
+
+ var currentFolder = Cc["@mozilla.org/file/local;1"].createInstance(nsIFile);
+ try {
+ currentFolder.initWithPath(currentFolderTextBox.value);
+ fp.displayDirectory = currentFolder;
+ } catch (e) {
+ console.error(
+ `Failed to set folder path from value=${currentFolderTextBox.value}\n`
+ );
+ }
+
+ fp.open(rv => {
+ if (rv != nsIFilePicker.returnOK || !fp.file) {
+ return;
+ }
+ // Retrieve the selected folder.
+ let selectedFolder = fp.file;
+
+ // Check if the folder can be used for mail storage.
+ if (!top.checkDirectoryIsUsable(selectedFolder)) {
+ return;
+ }
+
+ currentFolderTextBox.value = selectedFolder.path;
+ currentFolderTextBox.dispatchEvent(new CustomEvent("change"));
+ });
+}
+
+/**
+ * Return server/folder name formatted with server name if needed.
+ *
+ * @param {nsIMsgFolder} aTargetFolder - nsIMsgFolder to format name for
+ @returns {string} THe formatted name.
+ * If target.isServer then only its name is returned.
+ * Otherwise return the name as "<foldername> on <servername>".
+ */
+function prettyFolderName(aTargetFolder) {
+ if (aTargetFolder.isServer) {
+ return aTargetFolder.prettyName;
+ }
+
+ return document
+ .getElementById("bundle_messenger")
+ .getFormattedString("verboseFolderFormat", [
+ aTargetFolder.prettyName,
+ aTargetFolder.server.prettyName,
+ ]);
+}
+
+/**
+ * Checks validity of junk target server name and folder.
+ *
+ * @param {string} aTargetURI - The URI specification to check.
+ * @param {boolean} aIsServer - true if the URI specifies only a server
+ * (without folder)
+ *
+ * @returns {string} the value of aTargetURI if it is valid (usable), otherwise null
+ */
+function checkJunkTargetFolder(aTargetURI, aIsServer) {
+ try {
+ // Does the target account exist?
+ let targetServer;
+ if (aIsServer) {
+ targetServer = MailUtils.getOrCreateFolder(aTargetURI + "/Junk").server;
+ } else {
+ targetServer = MailUtils.getExistingFolder(aTargetURI).server;
+ }
+
+ // If the target server has deferred storage, Junk can't be stored into it.
+ if (targetServer.rootFolder != targetServer.rootMsgFolder) {
+ return null;
+ }
+ } catch (e) {
+ return null;
+ }
+
+ return aTargetURI;
+}
+
+/**
+ * Finds a usable target for storing Junk mail.
+ * If the passed in server URI is not usable, choose Local Folders.
+ *
+ * @param {string} aTargetURI - The URI of a server or folder to try first
+ * @param {boolean} aIsServer - true if the URI specifies only a server (without folder)
+ *
+ * @returns {string} the server/folder URI of a usable target for storing Junk
+ */
+function chooseJunkTargetFolder(aTargetURI, aIsServer) {
+ let server = null;
+
+ if (aTargetURI) {
+ server = MailUtils.getOrCreateFolder(aTargetURI).server;
+ if (
+ !server.canCreateFoldersOnServer ||
+ !server.canSearchMessages ||
+ server.rootFolder != server.rootMsgFolder
+ ) {
+ server = null;
+ }
+ }
+ if (!server) {
+ server = MailServices.accounts.localFoldersServer;
+ }
+
+ return server.serverURI + (!aIsServer ? "/Junk" : "");
+}
+
+/**
+ * Fixes junk target folders if they point to an invalid/unusable (e.g. deferred)
+ * folder/account. Only returns the new safe values. It is up to the caller
+ * to push them to the proper elements/prefs.
+ *
+ * @param {string} aSpamActionTargetAccount - The value of the
+ * server.*.spamActionTargetAccount pref value (URI).
+ * @param {string} aSpamActionTargetFolder - The value of the
+ * server.*.spamActionTargetFolder pref value (URI).
+ * @param {string} aProposedTarget - The URI of a new target to try.
+ * @param {integer} aMoveTargetModeValue - The value of the
+ * server.*.moveTargetMode pref value (0/1).
+ * @param {nsISpamSettings} aServerSpamSettings - The nsISpamSettings object
+ * of any server (used just for the MOVE_TARGET_MODE_* constants).
+ * @param {boolean} aMoveOnSpam - The server.*.moveOnSpam pref value).
+ *
+ * @returns {object[]} an array containing:
+ * newTargetAccount new safe junk target account
+ * newTargetAccount new safe junk target folder
+ * newMoveOnSpam new moveOnSpam value
+ */
+function sanitizeJunkTargets(
+ aSpamActionTargetAccount,
+ aSpamActionTargetFolder,
+ aProposedTarget,
+ aMoveTargetModeValue,
+ aServerSpamSettings,
+ aMoveOnSpam
+) {
+ // Check if folder targets are valid.
+ aSpamActionTargetAccount = checkJunkTargetFolder(
+ aSpamActionTargetAccount,
+ true
+ );
+ if (!aSpamActionTargetAccount) {
+ // If aSpamActionTargetAccount is not valid,
+ // reset to default behavior to NOT move junk messages...
+ if (aMoveTargetModeValue == aServerSpamSettings.MOVE_TARGET_MODE_ACCOUNT) {
+ aMoveOnSpam = false;
+ }
+
+ // ... and find a good default target.
+ aSpamActionTargetAccount = chooseJunkTargetFolder(aProposedTarget, true);
+ }
+
+ aSpamActionTargetFolder = checkJunkTargetFolder(
+ aSpamActionTargetFolder,
+ false
+ );
+ if (!aSpamActionTargetFolder) {
+ // If aSpamActionTargetFolder is not valid,
+ // reset to default behavior to NOT move junk messages...
+ if (aMoveTargetModeValue == aServerSpamSettings.MOVE_TARGET_MODE_FOLDER) {
+ aMoveOnSpam = false;
+ }
+
+ // ... and find a good default target.
+ aSpamActionTargetFolder = chooseJunkTargetFolder(aProposedTarget, false);
+ }
+
+ return [aSpamActionTargetAccount, aSpamActionTargetFolder, aMoveOnSpam];
+}
+
+/**
+ * Opens Preferences (Options) dialog on the Advanced pane, General tab
+ * so that the user sees where the global receipts settings can be found.
+ *
+ * @param {string} aTBPaneId - Thunderbird pref paneID to open.
+ * @param {string} aTBScrollPaneTo - Thunderbird ID of the element to scroll into view.
+ * @param {any} aTBOtherArgs - Other arguments to send to the pref tab.
+ * @param {string} aSMPaneId - Seamonkey pref pane to open.
+ */
+function openPrefsFromAccountManager(
+ aTBPaneId,
+ aTBScrollPaneTo,
+ aTBOtherArgs,
+ aSMPaneId
+) {
+ let win =
+ Services.wm.getMostRecentWindow("mail:3pane") ||
+ Services.wm.getMostRecentWindow("mail:messageWindow") ||
+ Services.wm.getMostRecentWindow("msgcompose");
+ if (!win) {
+ return;
+ }
+
+ // If openOptionsDialog() exists, we are in Thunderbird.
+ if (typeof win.openOptionsDialog == "function") {
+ win.openOptionsDialog(aTBPaneId, aTBScrollPaneTo, aTBOtherArgs);
+ }
+ // If goPreferences() exists, we are in Seamonkey.
+ if (typeof win.goPreferences == "function") {
+ win.goPreferences(aSMPaneId);
+ }
+}
+
+/**
+ * Check if the given account name already exists in any account.
+ *
+ * @param {string} aAccountName - The account name string to look for.
+ * @param {string} [aAccountKey] - The key of an account that is skipped when
+ * searching the name. If unset, do not skip any account.
+ */
+function accountNameExists(aAccountName, aAccountKey) {
+ for (let account of MailServices.accounts.accounts) {
+ if (
+ account.key != aAccountKey &&
+ account.incomingServer &&
+ aAccountName == account.incomingServer.prettyName
+ ) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Open a dialog to edit properties of an SMTP server.
+ *
+ * @param {nsISmtpServer} aServer - The server to edit.
+ * @returns {object} Object with result member to indicate whether 'OK'
+ * was clicked and addSmtpServer with key of newly created server.
+ */
+function editSMTPServer(aServer) {
+ let args = { server: aServer, result: false, addSmtpServer: "" };
+
+ let onCloseSMTPDialog = function () {
+ if (args.result) {
+ gSmtpServerListWindow.refreshServerList(aServer, true);
+ }
+ };
+
+ parent.gSubDialog.open(
+ "chrome://messenger/content/SmtpServerEdit.xhtml",
+ { closingCallback: onCloseSMTPDialog },
+ args
+ );
+
+ return args;
+}
diff --git a/comm/mailnews/base/prefs/content/aw-accname.js b/comm/mailnews/base/prefs/content/aw-accname.js
new file mode 100644
index 0000000000..97dce27406
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/aw-accname.js
@@ -0,0 +1,28 @@
+/* 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 AccountWizard.js */
+
+function acctNamePageValidate() {
+ var accountname = document.getElementById("prettyName").value;
+ // Check if this accountname already exists. If so, return false so that
+ // user can enter a different unique account name.
+ document.querySelector("wizard").canAdvance =
+ !!accountname && !accountNameExists(accountname);
+}
+
+function acctNamePageUnload() {
+ var pageData = parent.GetPageData();
+ var accountname = document.getElementById("prettyName").value;
+ pageData.prettyName = accountname;
+ return true;
+}
+
+function acctNamePageInit() {
+ var accountNameInput = document.getElementById("prettyName");
+ if (accountNameInput.value == "") {
+ accountNameInput.value = parent.GetPageData().hostname;
+ }
+ acctNamePageValidate();
+}
diff --git a/comm/mailnews/base/prefs/content/aw-done.js b/comm/mailnews/base/prefs/content/aw-done.js
new file mode 100644
index 0000000000..93ac3994bc
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/aw-done.js
@@ -0,0 +1,39 @@
+/* -*- 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/. */
+
+/* import-globals-from AccountWizard.js */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function donePageInit() {
+ var pageData = parent.GetPageData();
+
+ var email = pageData.email;
+ setDivTextFromForm("identity.email", email);
+ setDivTextFromForm("server.username", getUsernameFromEmail(email));
+ setDivTextFromForm("account.name", pageData.prettyName);
+ setDivTextFromForm("newsServer.name", pageData.hostname);
+}
+
+function setDivTextFromForm(divid, value) {
+ // collapse the row if the div has no value
+ let label = document.getElementById(`${divid}.label`);
+ let text = document.getElementById(`${divid}.text`);
+
+ if (!value) {
+ text.style.display = "none";
+ label.style.display = "none";
+ return;
+ }
+
+ // otherwise fill in the .text element
+ text.style.display = null;
+ label.style.display = null;
+
+ // set the value
+ text.setAttribute("value", value);
+}
diff --git a/comm/mailnews/base/prefs/content/aw-identity.js b/comm/mailnews/base/prefs/content/aw-identity.js
new file mode 100644
index 0000000000..66d63d6e1e
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/aw-identity.js
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; 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/. */
+
+/* import-globals-from AccountWizard.js */
+
+var gPrefsBundle;
+
+function identityPageValidate() {
+ document.querySelector("wizard").canAdvance =
+ document.getElementById("fullName").validity.valid &&
+ document.getElementById("email").validity.valid;
+}
+
+function identityPageUnload() {
+ var pageData = parent.GetPageData();
+ var name = document.getElementById("fullName").value;
+ let email = document.getElementById("email").value.trim();
+ pageData.fullName = name;
+ pageData.email = email;
+
+ return true;
+}
+
+function identityPageInit() {
+ gPrefsBundle = document.getElementById("bundle_prefs");
+ setEmailDescriptionText();
+ checkForFullName();
+ checkForEmail();
+ identityPageValidate();
+}
+
+function setEmailDescriptionText() {
+ var emailDescText = document.getElementById("emailDescText");
+ var emailFieldLabel = document.getElementById("emailFieldLabel");
+
+ // Set the default field label
+ emailFieldLabel.setAttribute(
+ "value",
+ gPrefsBundle.getString("emailFieldText")
+ );
+
+ // Check for obtained values and set with default values if needed
+ var username = gPrefsBundle.getString("exampleEmailUserName");
+ var domain = gPrefsBundle.getString("exampleEmailDomain");
+
+ let displayText = gPrefsBundle.getFormattedString("defaultEmailText", [
+ username,
+ domain,
+ ]);
+
+ // Display the dynamically generated text for email description
+ emailDescText.textContent = displayText;
+}
+
+function checkForFullName() {
+ var name = document.getElementById("fullName");
+ if (name.value == "" && "@mozilla.org/userinfo;1" in Cc) {
+ name.value = Cc["@mozilla.org/userinfo;1"].getService(
+ Ci.nsIUserInfo
+ ).fullname;
+ }
+}
+
+function checkForEmail() {
+ var email = document.getElementById("email");
+ var pageData = parent.GetPageData();
+ if (pageData.email) {
+ email.value = pageData.email;
+ }
+ if (email.value == "" && "@mozilla.org/userinfo;1" in Cc) {
+ email.value = Cc["@mozilla.org/userinfo;1"].getService(
+ Ci.nsIUserInfo
+ ).emailAddress;
+ }
+}
diff --git a/comm/mailnews/base/prefs/content/aw-incoming.js b/comm/mailnews/base/prefs/content/aw-incoming.js
new file mode 100644
index 0000000000..a4f746d71d
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/aw-incoming.js
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* import-globals-from AccountWizard.js */
+
+var { cleanUpHostName, isLegalHostNameOrIP } = ChromeUtils.import(
+ "resource:///modules/hostnameUtils.jsm"
+);
+var { NntpUtils } = ChromeUtils.import("resource:///modules/NntpUtils.jsm");
+
+function incomingPageValidate() {
+ let hostName = cleanUpHostName(document.getElementById("newsServer").value);
+
+ let hasAccount = false;
+ let server = NntpUtils.findServer(hostName);
+ if (server) {
+ // It's OK if a server exists, as long as it's not used by any account.
+ hasAccount = MailServices.accounts.FindAccountForServer(server);
+ }
+ // Can advance if it's a legal host name and we do not already have a server
+ // in use with the same host name.
+ document.querySelector("wizard").canAdvance =
+ !!isLegalHostNameOrIP(hostName) && !hasAccount;
+}
+
+function incomingPageUnload() {
+ parent.GetPageData().hostname = cleanUpHostName(
+ document.getElementById("newsServer").value
+ );
+
+ return true;
+}
+
+function incomingPageInit() {
+ var pageData = parent.GetPageData();
+ if (pageData.hostname) {
+ document.getElementById("newsServer").value = pageData.hostname;
+ }
+ incomingPageValidate();
+}
diff --git a/comm/mailnews/base/prefs/content/converterDialog.js b/comm/mailnews/base/prefs/content/converterDialog.js
new file mode 100644
index 0000000000..e0ac74fd5b
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/converterDialog.js
@@ -0,0 +1,385 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file contains functionality for the front-end part of the mail store
+ * type conversion.
+ */
+
+var { MailUtils } = ChromeUtils.import("resource:///modules/MailUtils.jsm");
+var { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+ChromeUtils.defineModuleGetter(
+ this,
+ "FolderUtils",
+ "resource:///modules/FolderUtils.jsm"
+);
+var MailstoreConverter = ChromeUtils.import(
+ "resource:///modules/mailstoreConverter.jsm"
+);
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+window.addEventListener("DOMContentLoaded", () => {
+ for (let img of document.querySelectorAll(".infoIcon")) {
+ img.setAttribute(
+ "src",
+ "chrome://messenger/skin/icons/new/activity/warning.svg"
+ );
+ }
+});
+
+var log = console.createInstance({
+ prefix: "mail.mailstoreconverter",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.mailstoreconverter.loglevel",
+});
+// {nsIMsgIncomingServer} server for the account to be migrated.
+var gServer;
+// {nsIMsgFolder} account root folder.
+var gFolder;
+// 'gResponse.newRootFolder' is path to the new account root folder if migration
+// is complete, else null.
+// 'gResponse' is set to the modified response parameter received from
+// am-server.js.
+var gResponse;
+// Array to hold deferred accounts.
+var gDeferredAccounts = [];
+// Value of Services.io.offline before migration.
+var gOriginalOffline;
+/**
+ * Place account name in migration dialog modal.
+ *
+ * @param {nsIMsgIncomingServer} aServer - account server.
+ */
+function placeAccountName(aServer) {
+ gOriginalOffline = Services.io.offline;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/converterDialog.properties"
+ );
+
+ let brandShortName = Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName");
+
+ // 'deferredToRootFolder' holds path of rootMsgFolder of account to which
+ // other accounts have been deferred.
+ let deferredToRootFolder = aServer.rootMsgFolder.filePath.path;
+ // String to hold names of deferred accounts separated by commas.
+ let deferredAccountsString = "";
+ // Account to which other accounts have been deferred.
+ let deferredToAccount;
+ // Array of all accounts.
+ let accounts = FolderUtils.allAccountsSorted(true);
+
+ for (let account of accounts) {
+ if (
+ account.incomingServer.rootFolder.filePath.path == deferredToRootFolder
+ ) {
+ // Other accounts may be deferred to this account.
+ deferredToAccount = account;
+ } else if (
+ account.incomingServer.rootMsgFolder.filePath.path == deferredToRootFolder
+ ) {
+ // This is a deferred account.
+ gDeferredAccounts.push(account);
+ }
+ }
+
+ // String to hold the names of accounts to be converted separated by commas.
+ let accountsToConvert = "";
+
+ if (gDeferredAccounts.length >= 1) {
+ // Add account names to 'accountsToConvert' and 'deferredAccountsString'.
+ for (let i = 0; i < gDeferredAccounts.length; i++) {
+ if (i < gDeferredAccounts.length - 1) {
+ accountsToConvert +=
+ gDeferredAccounts[i].incomingServer.username + ", ";
+ deferredAccountsString +=
+ gDeferredAccounts[i].incomingServer.username + ", ";
+ } else {
+ accountsToConvert += gDeferredAccounts[i].incomingServer.username;
+ deferredAccountsString += gDeferredAccounts[i].incomingServer.username;
+ }
+ }
+
+ // Username of Local Folders is "nobody". So it's better to use
+ // its hostname which is "Local Folders".
+ // TODO: maybe test against .key == MailServices.accounts.localFoldersServer.key ?
+ if (deferredToAccount.incomingServer.hostName == "Local Folders") {
+ accountsToConvert += ", " + deferredToAccount.incomingServer.prettyName;
+ } else {
+ accountsToConvert += ", " + deferredToAccount.incomingServer.prettyName;
+ }
+ log.info(accountsToConvert + " will be converted");
+ let storeContractId = Services.prefs.getCharPref(
+ "mail.server." + deferredToAccount.incomingServer.key + ".storeContractID"
+ );
+
+ if (storeContractId == "@mozilla.org/msgstore/berkeleystore;1") {
+ storeContractId = "maildir";
+ } else {
+ storeContractId = "mbox";
+ }
+
+ // Username of Local Folders is "nobody". So it's better to use
+ // its hostname which is "Local Folders".
+ // TODO: maybe test against .key != MailServices.accounts.localFoldersServer.key ?
+ let deferredToAccountName = deferredToAccount.incomingServer.hostName;
+ if (deferredToAccountName != "Local Folders") {
+ deferredToAccountName = deferredToAccount.incomingServer.username;
+ }
+
+ if (aServer.rootFolder.filePath.path != deferredToRootFolder) {
+ document.getElementById("warningSpan").textContent =
+ bundle.formatStringFromName(
+ "converterDialog.warningForDeferredAccount",
+ [
+ aServer.username,
+ deferredToAccountName,
+ deferredToAccountName,
+ deferredAccountsString,
+ accountsToConvert,
+ storeContractId,
+ brandShortName,
+ ]
+ );
+ } else {
+ document.getElementById("warningSpan").textContent =
+ bundle.formatStringFromName(
+ "converterDialog.warningForDeferredToAccount",
+ [
+ deferredToAccountName,
+ deferredAccountsString,
+ accountsToConvert,
+ storeContractId,
+ brandShortName,
+ ]
+ );
+ }
+
+ document.getElementById("messageSpan").textContent =
+ bundle.formatStringFromName("converterDialog.messageForDeferredAccount", [
+ accountsToConvert,
+ storeContractId,
+ ]);
+ gServer = deferredToAccount.incomingServer;
+ } else {
+ // No account is deferred.
+ let storeContractId = Services.prefs.getCharPref(
+ "mail.server." + aServer.key + ".storeContractID"
+ );
+ if (storeContractId == "@mozilla.org/msgstore/berkeleystore;1") {
+ storeContractId = "maildir";
+ } else {
+ storeContractId = "mbox";
+ }
+
+ let tempName = aServer.username;
+ if (tempName == "nobody") {
+ tempName = "Local Folders";
+ } else if (!tempName) {
+ tempName = aServer.hostName;
+ }
+
+ document.getElementById("warningSpan").textContent =
+ bundle.formatStringFromName("converterDialog.warning", [
+ tempName,
+ storeContractId,
+ brandShortName,
+ ]);
+ document.getElementById("messageSpan").textContent =
+ bundle.formatStringFromName("converterDialog.message", [
+ tempName,
+ storeContractId,
+ ]);
+ gServer = aServer;
+ }
+
+ // Forces the resize of the dialog to the actual content
+ window.sizeToContent();
+}
+
+/**
+ * Start the conversion process.
+ *
+ * @param {string} aSelectedStoreType - mailstore type selected by user.
+ * @param {object} aResponse - response from the migration dialog modal.
+ */
+function startContinue(aSelectedStoreType, aResponse) {
+ gResponse = aResponse;
+ gFolder = gServer.rootFolder.filePath;
+
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/converterDialog.properties"
+ );
+
+ document
+ .getElementById("progress")
+ .addEventListener("progress", function (e) {
+ document.getElementById("progress").value = e.detail;
+ document.getElementById("progressPercent").textContent =
+ bundle.formatStringFromName("converterDialog.percentDone", [e.detail]);
+ });
+
+ document.getElementById("warningArea").hidden = true;
+ document.getElementById("progressArea").hidden = false;
+
+ // Storing original prefs and root folder path
+ // to revert changes in case of error.
+ let p1 = "mail.server." + gServer.key + ".directory";
+ let p2 = "mail.server." + gServer.key + ".directory-rel";
+ let p3 = "mail.server." + gServer.key + ".newsrc.file";
+ let p4 = "mail.server." + gServer.key + ".newsrc.file-rel";
+ let p5 = "mail.server." + gServer.key + ".storeContractID";
+
+ let originalDirectoryPref = Services.prefs.getCharPref(p1);
+ let originalDirectoryRelPref = Services.prefs.getCharPref(p2);
+ let originalNewsrcFilePref;
+ let originalNewsrcFileRelPref;
+ if (gServer.type == "nntp") {
+ originalNewsrcFilePref = Services.prefs.getCharPref(p3);
+ originalNewsrcFileRelPref = Services.prefs.getCharPref(p4);
+ }
+ let originalStoreContractID = Services.prefs.getCharPref(p5);
+ let originalRootFolderPath = gServer.rootFolder.filePath.path;
+
+ /**
+ * Called when promise returned by convertMailStoreTo() is rejected.
+ *
+ * @param {string} aReason - error because of which the promise was rejected.
+ */
+ function promiseRejected(aReason) {
+ log.error("Conversion to '" + aSelectedStoreType + "' failed: " + aReason);
+ document.getElementById("messageSpan").hidden = true;
+
+ document.getElementById("errorSpan").hidden = false;
+ gResponse.newRootFolder = null;
+
+ // Revert prefs.
+ Services.prefs.setCharPref(p1, originalDirectoryPref);
+ Services.prefs.setCharPref(p2, originalDirectoryRelPref);
+ if (gServer.type == "nntp") {
+ Services.prefs.setCharPref(p3, originalNewsrcFilePref);
+ Services.prefs.setCharPref(p4, originalNewsrcFileRelPref);
+ }
+ Services.prefs.setCharPref(p5, originalStoreContractID);
+ Services.prefs.savePrefFile(null);
+ if (gServer.rootFolder.filePath.path != originalRootFolderPath) {
+ gServer.rootFolder.filePath = new FileUtils.File(originalRootFolderPath);
+ }
+ Services.io.offline = gOriginalOffline;
+ }
+
+ /**
+ * Called when promise returned by convertMailStoreTo() is resolved.
+ *
+ * @param {string} aVal - path of the new account root folder with which the
+ * promise returned by convertMailStoreTo() is resolved.
+ */
+ function promiseResolved(aVal) {
+ log.info("Converted to '" + aSelectedStoreType + "' - " + aVal);
+
+ gResponse.newRootFolder = aVal;
+ for (let deferredAccount of gDeferredAccounts) {
+ let defServer = deferredAccount.incomingServer;
+ defServer.rootMsgFolder.filePath = new FileUtils.File(aVal);
+ Services.prefs.setCharPref(
+ "mail.server." + defServer.key + ".storeContractID",
+ aSelectedStoreType
+ );
+ }
+
+ Services.io.offline = gOriginalOffline;
+ document.getElementById("cancel").hidden = true;
+ document.getElementById("finish").hidden = false;
+ document.getElementById("messageSpan").hidden = true;
+ document.getElementById("completeSpan").hidden = false;
+ }
+
+ /**
+ * Check whether an mbox folder can be compacted or not.
+ *
+ * @param {nsIMsgFolder} aFolder - mbox folder that is to be checked.
+ */
+ function canCompact(aFolder) {
+ if (aFolder.expungedBytes != 0) {
+ return true;
+ }
+ if (aFolder.hasSubFolders) {
+ for (let subFolder of aFolder.subFolders) {
+ if (canCompact(subFolder)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ // Compaction (compactAll()) works only for mbox folders which satisfy one of
+ // the following 2 conditions -
+ // 1. Messages are moved out of the folder.
+ // 2. Messages are moved out of some descendant folder of the folder.
+ // If the account root folder can be compacted, start the conversion after
+ // compacting it.
+ if (
+ originalStoreContractID == "@mozilla.org/msgstore/berkeleystore;1" &&
+ canCompact(gServer.rootFolder)
+ ) {
+ let urlListener = {
+ OnStartRunningUrl(aUrl) {},
+ OnStopRunningUrl(aUrl, aExitCode) {
+ let pConvert = MailstoreConverter.convertMailStoreTo(
+ originalStoreContractID,
+ gServer,
+ document.getElementById("progress")
+ );
+ pConvert
+ .then(function (val) {
+ promiseResolved(val);
+ })
+ .catch(function (reason) {
+ promiseRejected(reason);
+ });
+ },
+ };
+ gServer.rootFolder.compactAll(urlListener, null);
+ } else {
+ let pConvert = MailstoreConverter.convertMailStoreTo(
+ originalStoreContractID,
+ gServer,
+ document.getElementById("progress")
+ );
+ pConvert
+ .then(function (val) {
+ promiseResolved(val);
+ })
+ .catch(function (reason) {
+ promiseRejected(reason);
+ });
+ }
+}
+
+/**
+ * Cancel the conversion.
+ *
+ * @param {object} aResponse - response param from the migration dialog modal.
+ */
+function cancelConversion(aResponse) {
+ gResponse = aResponse;
+ gResponse.newRootFolder = null;
+ MailstoreConverter.terminateWorkers();
+ Services.io.offline = gOriginalOffline;
+ window.close();
+}
+
+/**
+ * Called when "finish" button is clicked.
+ */
+function finishConversion() {
+ window.close();
+}
diff --git a/comm/mailnews/base/prefs/content/converterDialog.xhtml b/comm/mailnews/base/prefs/content/converterDialog.xhtml
new file mode 100644
index 0000000000..40944fd556
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/converterDialog.xhtml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+<!DOCTYPE html [ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % converterDTD SYSTEM "chrome://messenger/locale/converterDialog.dtd">
+%converterDTD; ]>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta charset="UTF-8" />
+ <title>&converterDialog.title;</title>
+ <link
+ rel="stylesheet"
+ href="chrome://messenger/skin/shared/converterDialog.css"
+ />
+ <script src="chrome://messenger/content/converterDialog.js"></script>
+ </head>
+
+ <body onload="placeAccountName(window.arguments[0]);">
+ <div id="warningArea" class="convert-area">
+ <img class="infoIcon" alt="" />
+ <p id="warningSpan"></p>
+ <div class="controls">
+ <button onclick="cancelConversion(window.arguments[2]);">
+ &converterDialog.cancelButton;
+ </button>
+ <button
+ onclick="startContinue(window.arguments[1], window.arguments[2]);"
+ >
+ &converterDialog.continueButton;
+ </button>
+ </div>
+ </div>
+ <div id="progressArea" class="convert-area" hidden="hidden">
+ <img class="infoIcon" alt="" />
+ <p id="messageSpan"></p>
+ <p id="completeSpan" hidden="hidden">&converterDialog.complete;</p>
+ <p id="errorSpan" hidden="hidden">&converterDialog.error;</p>
+ <progress id="progress" value="0" max="100"></progress>
+ <span id="progressPercent"></span>
+ <div class="controls">
+ <button id="cancel" onclick="cancelConversion(window.arguments[1]);">
+ &converterDialog.cancelButton;
+ </button>
+ <button id="finish" onclick="finishConversion();" hidden="hidden">
+ &converterDialog.finishButton;
+ </button>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/comm/mailnews/base/prefs/content/removeAccount.js b/comm/mailnews/base/prefs/content/removeAccount.js
new file mode 100644
index 0000000000..724e45e139
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/removeAccount.js
@@ -0,0 +1,168 @@
+/* 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 gServer;
+var gDialog;
+
+window.addEventListener("DOMContentLoaded", onLoad);
+
+document.addEventListener("dialogdisclosure", showInfo);
+document.addEventListener("dialogaccept", onAccept);
+document.subDialogSetDefaultFocus = isInitialFocus => {
+ gDialog.getButton("cancel").focus();
+ delete document.subDialogSetDefaultFocus;
+};
+
+function onLoad(event) {
+ gServer = window.arguments[0].account.incomingServer;
+ gDialog = document.querySelector("dialog");
+
+ let bundle = document.getElementById("bundle_removeAccount");
+ let removeQuestion = bundle.getFormattedString("removeQuestion", [
+ gServer.prettyName,
+ ]);
+ document.getElementById("accountName").textContent = removeQuestion;
+
+ // Allow to remove account data if it has a local storage.
+ let localDirectory = gServer.localPath;
+ if (localDirectory && localDirectory.exists()) {
+ localDirectory.normalize();
+
+ // Do not allow removal if localPath is outside of profile folder.
+ let profilePath = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ profilePath.normalize();
+
+ // TODO: bug 77652, decide what to do for deferred accounts.
+ // And inform the user if the account localPath is outside the profile.
+ if (
+ gServer.isDeferredTo ||
+ (gServer instanceof Ci.nsIPop3IncomingServer &&
+ gServer.deferredToAccount) ||
+ !profilePath.contains(localDirectory)
+ ) {
+ document.getElementById("removeData").disabled = true;
+ }
+ } else {
+ document.getElementById("removeDataPossibility").collapsed = true;
+ }
+
+ if (gServer.type == "im") {
+ let dataCheckbox = document.getElementById("removeData");
+ dataCheckbox.label = dataCheckbox.getAttribute("labelChat");
+ dataCheckbox.accessKey = dataCheckbox.getAttribute("accesskeyChat");
+ }
+
+ enableRemove();
+}
+
+function enableRemove() {
+ gDialog.getButton("accept").disabled =
+ !document.getElementById("removeAccount").checked &&
+ !document.getElementById("removeData").checked;
+}
+
+/**
+ * Show the local directory.
+ */
+function openLocalDirectory() {
+ let nsLocalFile = Components.Constructor(
+ "@mozilla.org/file/local;1",
+ "nsIFile",
+ "initWithPath"
+ );
+ let localDir = gServer.localPath.path;
+ try {
+ new nsLocalFile(localDir).reveal();
+ } catch (e) {
+ // Reveal may fail e.g. on Linux, then just show the path as a string.
+ document.getElementById("localDirectory").value = localDir;
+ document.getElementById("localDirectory").collapsed = false;
+ }
+}
+
+function showInfo() {
+ let descs = document.querySelectorAll("vbox.indent");
+ for (let desc of descs) {
+ desc.collapsed = false;
+ }
+
+ // TODO: bug 1238271, this should use showFor attributes if possible.
+ if (gServer.type == "imap" || gServer.type == "nntp") {
+ document.getElementById("serverAccount").collapsed = false;
+ } else if (gServer.type == "im") {
+ document.getElementById("chatAccount").collapsed = false;
+ } else {
+ document.getElementById("localAccount").collapsed = false;
+ }
+
+ parent.gSubDialog._topDialog.resizeDialog();
+ gDialog.getButton("disclosure").disabled = true;
+ gDialog.getButton("disclosure").blur();
+}
+
+function removeAccount() {
+ let removeAccount = document.getElementById("removeAccount").checked;
+ let removeData = document.getElementById("removeData").checked;
+ let account = window.arguments[0].account;
+ try {
+ // Remove the requested account data.
+ if (removeAccount) {
+ try {
+ // Remove password information first.
+ account.incomingServer.forgetPassword();
+ } catch (e) {
+ /* It is OK if this fails. */
+ }
+ // Remove account
+ MailServices.accounts.removeAccount(account, removeData);
+ account = null;
+ delete window.arguments[0].account;
+ gServer = null;
+ window.arguments[0].result = true;
+ } else if (removeData) {
+ // Remove files only.
+ // TODO: bug 1302193
+ window.arguments[0].result = false;
+ }
+
+ document.getElementById("success").hidden = false;
+ } catch (ex) {
+ document.getElementById("failure").hidden = false;
+ console.error("Failure to remove account: " + ex);
+ window.arguments[0].result = false;
+ }
+ document.getElementById("progress").hidden = true;
+}
+
+function onAccept(event) {
+ // If Cancel is disabled, we already tried to remove the account
+ // and can only close the dialog.
+ if (gDialog.getButton("cancel").disabled) {
+ return;
+ }
+
+ gDialog.getButton("accept").disabled = true;
+ gDialog.getButton("cancel").disabled = true;
+ gDialog.getButton("disclosure").disabled = true;
+
+ // Change the "Remove" to an "OK" button by clearing the custom label.
+ gDialog.removeAttribute("buttonlabelaccept");
+ gDialog.removeAttribute("buttonaccesskeyaccept");
+ gDialog.getButton("accept").removeAttribute("label");
+ gDialog.getButton("accept").removeAttribute("accesskey");
+ gDialog.buttons = "accept";
+
+ document.getElementById("removeAccountSection").hidden = true;
+ document.getElementById("confirmationSection").hidden = false;
+ window.sizeToContent();
+
+ removeAccount();
+
+ gDialog.getButton("accept").disabled = false;
+ event.preventDefault();
+}
diff --git a/comm/mailnews/base/prefs/content/removeAccount.xhtml b/comm/mailnews/base/prefs/content/removeAccount.xhtml
new file mode 100644
index 0000000000..510b3fef84
--- /dev/null
+++ b/comm/mailnews/base/prefs/content/removeAccount.xhtml
@@ -0,0 +1,84 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<?xml-stylesheet href="chrome://global/skin/checkbox.css" type="text/css"?>
+
+<!DOCTYPE html [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % removalDTD SYSTEM "chrome://messenger/locale/removeAccount.dtd">
+%removalDTD;
+]>
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ width="600"
+ scrolling="false">
+<head>
+ <title>&dialogTitle;</title>
+ <link rel="localization" href="branding/brand.ftl" />
+ <script defer="defer" src="chrome://messenger/content/removeAccount.js"></script>
+</head>
+<html:body xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+<dialog id="removeAccountDialog"
+ buttons="accept,disclosure,cancel"
+ buttonlabelaccept="&removeButton.label;"
+ buttonaccesskeyaccept="&removeButton.accesskey;"
+ defaultButton="cancel"
+ style="width:100vw; height:100vh;">
+ <stringbundle id="bundle_removeAccount"
+ src="chrome://messenger/locale/removeAccount.properties"/>
+ <vbox id="removeAccountSection">
+ <label id="accountName"></label>
+ <separator class="thin"/>
+ <checkbox id="removeAccount"
+ label="&removeAccount.label;"
+ checked="true"
+ disabled="true"
+ accesskey="&removeAccount.accesskey;"
+ oncommand="enableRemove();"/>
+ <vbox class="indent" collapsed="true">
+ <description>
+ &removeAccount.desc;
+ </description>
+ </vbox>
+ <vbox id="removeDataPossibility" collapsed="false">
+ <checkbox id="removeData"
+ label="&removeData.label;"
+ labelChat="&removeDataChat.label;"
+ accesskey="&removeData.accesskey;"
+ accesskeyChat="&removeDataChat.accesskey;"
+ oncommand="enableRemove();"/>
+ <vbox id="removeAccountBox" class="indent" collapsed="true">
+ <description id="localAccount" collapsed="true">
+ &removeDataLocalAccount.desc;
+ </description>
+ <description id="serverAccount" collapsed="true">
+ &removeDataServerAccount.desc;
+ </description>
+ <description id="chatAccount" collapsed="true">
+ &removeDataChatAccount.desc;
+ </description>
+ <hbox align="center">
+ <button id="showLocalDirectory"
+ label="&showData.label;"
+ accesskey="&showData.accesskey;"
+ oncommand="openLocalDirectory();"/>
+ <label id="localDirectory" collapsed="true"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </vbox>
+ <vbox id="confirmationSection" align="center" pack="center" flex="1" hidden="true">
+ <vbox id="progress" align="center">
+ <label>&progressPending;</label>
+ <html:progress max="100"/>
+ </vbox>
+ <label id="success" hidden="true">&progressSuccess;</label>
+ <label id="failure" hidden="true">&progressFailure;</label>
+ </vbox>
+</dialog>
+</html:body>
+</html>
diff --git a/comm/mailnews/base/public/MailNewsTypes.h b/comm/mailnews/base/public/MailNewsTypes.h
new file mode 100644
index 0000000000..e6f58cb56b
--- /dev/null
+++ b/comm/mailnews/base/public/MailNewsTypes.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef MailNewsTypes_h__
+#define MailNewsTypes_h__
+
+#include "msgCore.h"
+#include "MailNewsTypes2.h"
+
+/* nsMsgKey is a unique ID for a particular message in a folder. If you want
+ a handle to a message that will remain valid even after resorting the folder
+ or otherwise changing their indices, you want one of these rather than a
+ nsMsgViewIndex. nsMsgKeys don't survive local mail folder compression,
+ however.
+ */
+const nsMsgKey nsMsgKey_None = 0xffffffff;
+
+/* nsMsgViewIndex
+ *
+ * A generic index type from which other index types are derived. All
+ * nsMsgViewIndex derived types are zero based.
+ *
+ * The following index types are currently supported:
+ * - nsMsgViewIndex - an index into the list of messages or folders or groups,
+ * where zero is the first one to show, one is the second, etc...
+ * - AB_SelectionIndex
+ * - AB_NameCompletionIndex
+ */
+
+const nsMsgViewIndex nsMsgViewIndex_None = 0xFFFFFFFF;
+
+/* kSizeUnknown is a special value of folder size that indicates the size
+ * is unknown yet. Usually this causes the folder to determine the real size
+ * immediately as it is queried by a consumer.
+ */
+const int64_t kSizeUnknown = -1;
+
+#endif
diff --git a/comm/mailnews/base/public/MailNewsTypes2.idl b/comm/mailnews/base/public/MailNewsTypes2.idl
new file mode 100644
index 0000000000..aeb8df2ec6
--- /dev/null
+++ b/comm/mailnews/base/public/MailNewsTypes2.idl
@@ -0,0 +1,93 @@
+/* -*- 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/. */
+
+#include "nsISupports.idl"
+
+typedef unsigned long nsMsgKey;
+typedef unsigned long nsMsgViewIndex;
+
+typedef long nsMsgSearchScopeValue;
+
+typedef long nsMsgPriorityValue;
+typedef long nsMsgSocketTypeValue;
+typedef long nsMsgAuthMethodValue;
+
+typedef unsigned long nsMsgJunkStatus;
+
+typedef unsigned long nsMsgJunkScore;
+
+[scriptable, uuid(94C0D8D8-2045-11d3-8A8F-0060B0FC04D2)]
+interface nsMsgPriority : nsISupports {
+ const nsMsgPriorityValue notSet = 0;
+ const nsMsgPriorityValue none = 1;
+ const nsMsgPriorityValue lowest = 2;
+ const nsMsgPriorityValue low = 3;
+ const nsMsgPriorityValue normal = 4;
+ const nsMsgPriorityValue high = 5;
+ const nsMsgPriorityValue highest = 6;
+ //the default priority (if none) is set in the message
+ const nsMsgPriorityValue Default = 4;
+};
+
+/**
+ * Defines whether to use SSL or STARTTLS or not.
+ * Used by @see nsIMsgIncomingServer.socketType
+ * and @see nsISmtpServer.socketType
+ */
+[scriptable, uuid(bc78bc74-1b34-48e8-ac2b-968e8dff1aeb)]
+interface nsMsgSocketType : nsISupports {
+ /// No SSL or STARTTLS
+ const nsMsgSocketTypeValue plain = 0;
+ /// Use TLS via STARTTLS, but only if server offers it.
+ /// @deprecated This is vulnerable to MITM attacks
+ const nsMsgSocketTypeValue trySTARTTLS = 1;
+ /// Insist on TLS via STARTTLS.
+ /// Uses normal port.
+ const nsMsgSocketTypeValue alwaysSTARTTLS = 2;
+ /// Connect via SSL.
+ /// Needs special SSL port.
+ const nsMsgSocketTypeValue SSL = 3;
+};
+
+/**
+ * Defines which authentication schemes we should try.
+ * Used by @see nsIMsgIncomingServer.authMethod
+ * and @see nsISmtpServer.authMethod
+ */
+[scriptable, uuid(4a10e647-d179-4a53-b7ef-df575ff5f405)]
+interface nsMsgAuthMethod : nsISupports {
+ // 0 is intentionally undefined and invalid
+ /// No login needed. E.g. IP-address-based.
+ const nsMsgAuthMethodValue none = 1;
+ /// Do not use AUTH commands (e.g. AUTH=PLAIN),
+ /// but the original login commands that the protocol specified
+ /// (POP: "USER"/"PASS", IMAP: "login", not valid for SMTP)
+ const nsMsgAuthMethodValue old = 2;
+ /// password in the clear. AUTH=PLAIN/LOGIN or old-style login.
+ const nsMsgAuthMethodValue passwordCleartext = 3;
+ /// hashed password. CRAM-MD5, DIGEST-MD5
+ const nsMsgAuthMethodValue passwordEncrypted = 4;
+ /// Kerberos / GSSAPI (Unix single-signon)
+ const nsMsgAuthMethodValue GSSAPI = 5;
+ /// NTLM is a Windows single-singon scheme.
+ /// Includes MSN / Passport.net, which is the same with a different name.
+ const nsMsgAuthMethodValue NTLM = 6;
+ /// Auth External is cert-based authentication
+ const nsMsgAuthMethodValue External = 7;
+ /// Encrypted password or Kerberos / GSSAPI or NTLM.
+ /// @deprecated - for migration only.
+ const nsMsgAuthMethodValue secure = 8;
+ /// Let us pick any of the auth types supported by the server.
+ /// Discouraged, because vulnerable to MITM attacks, even if server offers secure auth.
+ const nsMsgAuthMethodValue anything = 9;
+
+ /// Use OAuth2 to authenticate.
+ const nsMsgAuthMethodValue OAuth2 = 10;
+};
+
+typedef long nsMsgViewSortOrderValue;
+typedef long nsMsgViewSortTypeValue;
+typedef long nsMsgViewTypeValue;
+typedef long nsMsgViewFlagsTypeValue;
diff --git a/comm/mailnews/base/public/moz.build b/comm/mailnews/base/public/moz.build
new file mode 100644
index 0000000000..be6deb7da9
--- /dev/null
+++ b/comm/mailnews/base/public/moz.build
@@ -0,0 +1,80 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "MailNewsTypes2.idl",
+ "mozINewMailListener.idl",
+ "mozINewMailNotificationService.idl",
+ "msgIOAuth2Module.idl",
+ "nsICopyMessageListener.idl",
+ "nsICopyMessageStreamListener.idl",
+ "nsIFolderListener.idl",
+ "nsIFolderLookupService.idl",
+ "nsIIncomingServerListener.idl",
+ "nsIMailAuthModule.idl",
+ "nsIMailChannel.idl",
+ "nsIMapiRegistry.idl",
+ "nsIMessenger.idl",
+ "nsIMessengerMigrator.idl",
+ "nsIMessengerOSIntegration.idl",
+ "nsIMessengerWindowService.idl",
+ "nsIMsgAccount.idl",
+ "nsIMsgAccountManager.idl",
+ "nsIMsgAsyncPrompter.idl",
+ "nsIMsgBiffManager.idl",
+ "nsIMsgContentPolicy.idl",
+ "nsIMsgCopyService.idl",
+ "nsIMsgCopyServiceListener.idl",
+ "nsIMsgCustomColumnHandler.idl",
+ "nsIMsgDBView.idl",
+ "nsIMsgEnumerator.idl",
+ "nsIMsgFolder.idl",
+ "nsIMsgFolderCache.idl",
+ "nsIMsgFolderCacheElement.idl",
+ "nsIMsgFolderCompactor.idl",
+ "nsIMsgFolderListener.idl",
+ "nsIMsgFolderNotificationService.idl",
+ "nsIMsgHdr.idl",
+ "nsIMsgIdentity.idl",
+ "nsIMsgIncomingServer.idl",
+ "nsIMsgMailNewsUrl.idl",
+ "nsIMsgMailSession.idl",
+ "nsIMsgMdnGenerator.idl",
+ "nsIMsgMessageService.idl",
+ "nsIMsgOfflineManager.idl",
+ "nsIMsgPluggableStore.idl",
+ "nsIMsgProgress.idl",
+ "nsIMsgProtocolHandler.idl",
+ "nsIMsgProtocolInfo.idl",
+ "nsIMsgPurgeService.idl",
+ "nsIMsgShutdown.idl",
+ "nsIMsgStatusFeedback.idl",
+ "nsIMsgTagService.idl",
+ "nsIMsgThread.idl",
+ "nsIMsgUserFeedbackListener.idl",
+ "nsIMsgWindow.idl",
+ "nsISpamSettings.idl",
+ "nsIStatusBarBiffManager.idl",
+ "nsIStopwatch.idl",
+ "nsISubscribableServer.idl",
+ "nsIUrlListener.idl",
+ "nsIUserInfo.idl",
+ "nsMsgFolderFlags.idl",
+ "nsMsgMessageFlags.idl",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ XPIDL_SOURCES += [
+ "nsIMessengerWindowsIntegration.idl",
+ ]
+
+XPIDL_MODULE = "msgbase"
+
+EXPORTS += [
+ "MailNewsTypes.h",
+ "msgCore.h",
+ "nsMsgHeaderMasks.h",
+ "nsMsgLocalFolderHdrs.h",
+]
diff --git a/comm/mailnews/base/public/mozINewMailListener.idl b/comm/mailnews/base/public/mozINewMailListener.idl
new file mode 100644
index 0000000000..467acb671c
--- /dev/null
+++ b/comm/mailnews/base/public/mozINewMailListener.idl
@@ -0,0 +1,22 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(e15f104f-a16d-4e51-a362-4b4c5efe05b9)]
+/**
+ * Callback interface for objects interested in receiving new mail notifications
+ * from mozINewMailNotificationService
+ * NOTE: THIS INTERFACE IS UNDER ACTIVE DEVELOPMENT AND SUBJECT TO CHANGE,
+ * see https://bugzilla.mozilla.org/show_bug.cgi?id=715799
+ */
+interface mozINewMailListener : nsISupports {
+ /** The new mail notification service will call this when the number of interesting
+ * messages has changed
+ *
+ * @param unreadCount The number of unread messages the user cares to be notified about
+ */
+ void onCountChanged(in unsigned long count);
+};
diff --git a/comm/mailnews/base/public/mozINewMailNotificationService.idl b/comm/mailnews/base/public/mozINewMailNotificationService.idl
new file mode 100644
index 0000000000..cdd049bcca
--- /dev/null
+++ b/comm/mailnews/base/public/mozINewMailNotificationService.idl
@@ -0,0 +1,58 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface mozINewMailListener;
+
+typedef long newMailListenerFlag;
+
+[scriptable, uuid(7fef9018-c9f1-4cbd-b57c-d6555cf3a668)]
+/** New mail notification service. This service watches all the relevant
+ * folder and message change events, preferences etc. and keeps track of
+ * the specific messages the user wants notifications for.
+ * NOTE: THIS INTERFACE IS UNDER ACTIVE DEVELOPMENT AND SUBJECT TO CHANGE,
+ * see https://bugzilla.mozilla.org/show_bug.cgi?id=715799
+ * Registered mozINewMailListeners are called when the message count or
+ * specific list of notified messages changes.
+ * ** Should also document the observer service callback that allows
+ * plugins to override notifications by folder
+ */
+interface mozINewMailNotificationService : nsISupports {
+ /**
+ * @name Notification flags
+ * These flags determine which notifications will be sent.
+ * @{
+ */
+ /// mozINewMailListener::count notification
+ const newMailListenerFlag count = 0x1;
+
+ /// mozINewMailListener::messages notification
+ const newMailListenerFlag messages = 0x2;
+
+ /** @} */
+
+ /**
+ * addListener - Register a mozINewMailListener to receive callbacks
+ * when the count or list of notification-worthy messages
+ * changes.
+ * @param aListener mozINewMailListener to call back
+ * @param flags Bitmask of newMailListenerFlag values indicating
+ * the particular callbacks this listener wants.
+ * If the listener is already registered with the
+ * notification service, the existing set of flags is
+ * replaced by the values passed in this parameter.
+ */
+ void addListener(in mozINewMailListener aListener,
+ in newMailListenerFlag flags);
+ /**
+ * removeListener - remove a listener from the service
+ * @param aListener The listener to remove
+ */
+ void removeListener(in mozINewMailListener aListener);
+
+ /// The current count of notification-worth unread messages
+ readonly attribute long messageCount;
+};
diff --git a/comm/mailnews/base/public/msgCore.h b/comm/mailnews/base/public/msgCore.h
new file mode 100644
index 0000000000..679e510887
--- /dev/null
+++ b/comm/mailnews/base/public/msgCore.h
@@ -0,0 +1,210 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Include files we are going to want available to all files....these files
+ include NSPR, memory, and string header files among others */
+
+#ifndef msgCore_h__
+#define msgCore_h__
+
+#include "nscore.h"
+#include "nspr.h"
+#include "plstr.h"
+#include "nsCRTGlue.h"
+
+class nsIMsgDBHdr;
+class nsIMsgFolder;
+
+// include common interfaces such as the service manager and the repository....
+#include "nsIServiceManager.h"
+#include "nsIComponentManager.h"
+
+/*
+ * The suffix we use for the mail summary file.
+ */
+#define SUMMARY_SUFFIX u".msf"
+#define SUMMARY_SUFFIX8 ".msf"
+#define SUMMARY_SUFFIX_LENGTH 4
+
+/*
+ * The suffix we use for folder subdirectories.
+ */
+#define FOLDER_SUFFIX u".sbd"
+#define FOLDER_SUFFIX8 ".sbd"
+#define FOLDER_SUFFIX_LENGTH 4
+
+/*
+ * These are folder property strings, which are used in several places.
+
+ */
+// Most recently used (opened, moved to, got new messages)
+#define MRU_TIME_PROPERTY "MRUTime"
+// Most recently moved to, for recent folders list in move menu
+#define MRM_TIME_PROPERTY "MRMTime"
+
+/* NS_ERROR_MODULE_MAILNEWS is defined in mozilla/xpcom/public/nsError.h */
+
+/*
+ * NS_ERROR macros - use these macros to generate error constants
+ * to be used by XPCOM interfaces and possibly other useful things
+ * do not use these macros in your code - declare error macros for
+ * each specific error you need.
+ *
+ * for example:
+ * #define NS_MSG_ERROR_NO_SUCH_FOLDER NS_MSG_GENERATE_FAILURE(4)
+ *
+ */
+
+/* use these routines to generate error values */
+#define NS_MSG_GENERATE_RESULT(severity, value) \
+ NS_ERROR_GENERATE(severity, NS_ERROR_MODULE_MAILNEWS, value)
+
+#define NS_MSG_GENERATE_SUCCESS(value) \
+ NS_ERROR_GENERATE_SUCCESS(NS_ERROR_MODULE_MAILNEWS, value)
+
+#define NS_MSG_GENERATE_FAILURE(value) \
+ NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_MAILNEWS, value)
+
+/* these are shortcuts to generate simple errors with a zero value */
+#define NS_MSG_SUCCESS NS_MSG_GENERATE_SUCCESS(0)
+#define NS_MSG_FAILURE NS_MSG_GENERATE_FAILURE(0)
+
+#define IS_SPACE(VAL) \
+ (((((PRIntn)(VAL)) & 0x7f) == ((PRIntn)(VAL))) && isspace((PRIntn)(VAL)))
+
+#define IS_DIGIT(i) ((((unsigned int)(i)) > 0x7f) ? (int)0 : isdigit(i))
+#if defined(XP_WIN)
+# define IS_ALPHA(VAL) (isascii((int)(VAL)) && isalpha((int)(VAL)))
+#else
+# define IS_ALPHA(VAL) \
+ ((((unsigned int)(VAL)) > 0x7f) ? (int)0 : isalpha((int)(VAL)))
+#endif
+
+/* for retrieving information out of messenger nsresults */
+
+#define NS_IS_MSG_ERROR(err) \
+ (NS_ERROR_GET_MODULE(err) == NS_ERROR_MODULE_MAILNEWS)
+
+#define NS_MSG_SUCCEEDED(err) (NS_IS_MSG_ERROR(err) && NS_SUCCEEDED(err))
+
+#define NS_MSG_FAILED(err) (NS_IS_MSG_ERROR(err) && NS_FAILED(err))
+
+#define NS_MSG_PASSWORD_PROMPT_CANCELLED NS_MSG_GENERATE_SUCCESS(1)
+
+/**
+ * Indicates that a search is done/terminated because it was interrupted.
+ * Interrupting a search originally notified listeners with
+ * OnSearchDone(NS_OK), so we define a success value to continue doing this,
+ * and because the search was fine except for an explicit call to interrupt it.
+ */
+#define NS_MSG_SEARCH_INTERRUPTED NS_MSG_GENERATE_SUCCESS(2)
+
+/* This is where we define our errors. There has to be a central
+ place so we don't use the same error codes for different errors.
+*/
+#define NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE NS_MSG_GENERATE_FAILURE(5)
+#define NS_MSG_ERROR_FOLDER_SUMMARY_MISSING NS_MSG_GENERATE_FAILURE(6)
+#define NS_MSG_ERROR_FOLDER_MISSING NS_MSG_GENERATE_FAILURE(7)
+
+#define NS_MSG_MESSAGE_NOT_FOUND NS_MSG_GENERATE_FAILURE(8)
+#define NS_MSG_NOT_A_MAIL_FOLDER NS_MSG_GENERATE_FAILURE(9)
+
+#define NS_MSG_FOLDER_BUSY NS_MSG_GENERATE_FAILURE(10)
+
+#define NS_MSG_COULD_NOT_CREATE_DIRECTORY NS_MSG_GENERATE_FAILURE(11)
+#define NS_MSG_CANT_CREATE_FOLDER NS_MSG_GENERATE_FAILURE(12)
+
+#define NS_MSG_FILTER_PARSE_ERROR NS_MSG_GENERATE_FAILURE(13)
+
+#define NS_MSG_FOLDER_UNREADABLE NS_MSG_GENERATE_FAILURE(14)
+
+#define NS_MSG_ERROR_WRITING_MAIL_FOLDER NS_MSG_GENERATE_FAILURE(15)
+
+#define NS_MSG_ERROR_NO_SEARCH_VALUES NS_MSG_GENERATE_FAILURE(16)
+
+#define NS_MSG_ERROR_INVALID_SEARCH_SCOPE NS_MSG_GENERATE_FAILURE(17)
+
+#define NS_MSG_ERROR_INVALID_SEARCH_TERM NS_MSG_GENERATE_FAILURE(18)
+
+#define NS_MSG_FOLDER_EXISTS NS_MSG_GENERATE_FAILURE(19)
+
+#define NS_MSG_ERROR_OFFLINE NS_MSG_GENERATE_FAILURE(20)
+
+#define NS_MSG_POP_FILTER_TARGET_ERROR NS_MSG_GENERATE_FAILURE(21)
+
+#define NS_MSG_INVALID_OR_MISSING_SERVER NS_MSG_GENERATE_FAILURE(22)
+
+#define NS_MSG_SERVER_USERNAME_MISSING NS_MSG_GENERATE_FAILURE(23)
+
+#define NS_MSG_INVALID_DBVIEW_INDEX NS_MSG_GENERATE_FAILURE(24)
+
+#define NS_MSG_NEWS_ARTICLE_NOT_FOUND NS_MSG_GENERATE_FAILURE(25)
+
+#define NS_MSG_ERROR_COPY_FOLDER_ABORTED NS_MSG_GENERATE_FAILURE(26)
+// this error means a url was queued but never run because one of the urls
+// it was queued after failed. We send an OnStopRunningUrl with this error code
+// so the listeners can know that we didn't run the url.
+#define NS_MSG_ERROR_URL_ABORTED NS_MSG_GENERATE_FAILURE(27)
+
+// when num of custom headers exceeds 50
+#define NS_MSG_CUSTOM_HEADERS_OVERFLOW NS_MSG_GENERATE_FAILURE(28)
+
+// when custom header has invalid characters (as per rfc 2822)
+#define NS_MSG_INVALID_CUSTOM_HEADER NS_MSG_GENERATE_FAILURE(29)
+
+// when local caches are password protect and user isn't auth
+#define NS_MSG_USER_NOT_AUTHENTICATED NS_MSG_GENERATE_FAILURE(30)
+
+#define NS_MSG_ERROR_COPYING_FROM_TMP_DOWNLOAD \
+ NS_MSG_GENERATE_FAILURE(31) // pop3 downloaded to tmp file, and failed.
+
+// The code tried to stream a message using the aLocalOnly argument, but
+// the message was not cached locally.
+#define NS_MSG_ERROR_MSG_NOT_OFFLINE NS_MSG_GENERATE_FAILURE(32)
+
+// The imap server returned NO or BAD for an IMAP command
+#define NS_MSG_ERROR_IMAP_COMMAND_FAILED NS_MSG_GENERATE_FAILURE(33)
+
+#define NS_MSG_ERROR_INVALID_FOLDER_NAME NS_MSG_GENERATE_FAILURE(34)
+
+/* Error codes for message compose are defined in
+ compose\src\nsMsgComposeStringBundle.h. Message compose use the same error
+ code space as other mailnews modules. To avoid any conflict, values between
+ 12500 and 12999 are reserved.
+*/
+#define NS_MSGCOMP_ERROR_BEGIN 12500
+/* NS_ERROR_NNTP_NO_CROSS_POSTING lives here, and not in
+ * nsMsgComposeStringBundle.h, because it is used in news and compose. */
+#define NS_ERROR_NNTP_NO_CROSS_POSTING NS_MSG_GENERATE_FAILURE(12554)
+#define NS_MSGCOMP_ERROR_END 12999
+
+#if defined(XP_WIN)
+# define MSG_LINEBREAK "\015\012"
+# define MSG_LINEBREAK_LEN 2
+#else
+# define MSG_LINEBREAK "\012"
+# define MSG_LINEBREAK_LEN 1
+#endif
+
+/*
+ * On Windows, we use \r\n as the line terminator in mbox files. On
+ * other platforms, we use \n. However, we need to be able to
+ * recognize line terminators produced on any platform, because we
+ * allow profiles (including the mbox files they contain) to be shared
+ * between platforms.
+ *
+ * Returns 0 (i.e., false) if the line is not blank, or otherwise the
+ * length of the line terminator, i.e., 1 for \n or 2 for \r\n.
+ */
+#define IS_MSG_LINEBREAK(line) \
+ (line[0] == '\012' ? 1 : ((line[0] == '\015' && line[1] == '\012') ? 2 : 0))
+
+#define NS_MSG_BASE
+#define NS_MSG_BASE_STATIC_MEMBER_(type) type
+
+/// The number of microseconds in a day. This comes up a lot.
+#define PR_USEC_PER_DAY (PRTime(PR_USEC_PER_SEC) * 60 * 60 * 24)
+
+#endif // msgCore_h__
diff --git a/comm/mailnews/base/public/msgIOAuth2Module.idl b/comm/mailnews/base/public/msgIOAuth2Module.idl
new file mode 100644
index 0000000000..e3d1f8aa15
--- /dev/null
+++ b/comm/mailnews/base/public/msgIOAuth2Module.idl
@@ -0,0 +1,56 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgIncomingServer;
+interface nsISmtpServer;
+
+/**
+ * A listener callback for OAuth2 SASL authentication. This would be represented
+ * as a promise, but this needs to be consumed by C++ code.
+ */
+[scriptable, uuid(9a088b49-bc13-4f99-9478-053a6a43e370)]
+interface msgIOAuth2ModuleListener : nsISupports {
+ /**
+ * Called on successful OAuth2 authentication with the base64-encoded
+ * string to send as the client initial response for SASL XOAUTH2.
+ */
+ void onSuccess(in ACString aBearerToken);
+
+ /// Called on failed OAuth2 authentication.
+ void onFailure(in nsresult aError);
+};
+
+/**
+ * An interface for managing the responsibilities of using OAuth2 to produce a
+ * bearer token, for use in SASL steps.
+ */
+[scriptable, uuid(68c275f8-cfa7-4622-b279-af290616cae6)]
+interface msgIOAuth2Module : nsISupports {
+ /**
+ * Initialize the OAuth2 parameters from an SMTP server, and return whether or
+ * not we can authenticate with OAuth2.
+ */
+ bool initFromSmtp(in nsISmtpServer aSmtpServer);
+
+ /**
+ * Initialize the OAuth2 parameters from an incoming server, and return
+ * whether or not we can authenticate with OAuth2.
+ */
+ bool initFromMail(in nsIMsgIncomingServer aServer);
+
+ /**
+ * Connect to the OAuth2 server to get an access token.
+ * @param aWithUI If false, do not allow a dialog to be popped up to query
+ * for a password.
+ * @param aCallback Listener that handles the async response.
+ */
+ void connect(in boolean aWithUI, in msgIOAuth2ModuleListener aCallback);
+};
+
+%{C++
+#define MSGIOAUTH2MODULE_CONTRACTID "@mozilla.org/mail/oauth2-module;1"
+%}
diff --git a/comm/mailnews/base/public/nsICopyMessageListener.idl b/comm/mailnews/base/public/nsICopyMessageListener.idl
new file mode 100644
index 0000000000..4c13dab280
--- /dev/null
+++ b/comm/mailnews/base/public/nsICopyMessageListener.idl
@@ -0,0 +1,24 @@
+/* -*- 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgDBHdr;
+interface nsIInputStream;
+
+[scriptable, uuid(53CA78FE-E231-11d2-8A4D-0060B0FC04D2)]
+
+/* Use this for any object that wants to handle copying/moving messages to it */
+
+interface nsICopyMessageListener : nsISupports
+{
+ void beginCopy();
+ void startMessage();
+ void copyData(in nsIInputStream aIStream, in long aLength);
+ void endMessage(in nsMsgKey key);
+ void endCopy(in boolean copySucceeded);
+ void endMove(in boolean moveSucceeded);
+};
diff --git a/comm/mailnews/base/public/nsICopyMessageStreamListener.idl b/comm/mailnews/base/public/nsICopyMessageStreamListener.idl
new file mode 100644
index 0000000000..772a69080b
--- /dev/null
+++ b/comm/mailnews/base/public/nsICopyMessageStreamListener.idl
@@ -0,0 +1,20 @@
+/* -*- 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsICopyMessageListener;
+interface nsIURI;
+
+[scriptable, uuid(7741DAEC-2125-11d3-8A90-0060B0FC04D2)]
+
+interface nsICopyMessageStreamListener: nsISupports
+{
+ void init(in nsICopyMessageListener destination);
+ void startMessage();
+ void endMessage(in nsMsgKey key);
+ void endCopy(in nsIURI uri, in nsresult status);
+};
diff --git a/comm/mailnews/base/public/nsIFolderListener.idl b/comm/mailnews/base/public/nsIFolderListener.idl
new file mode 100644
index 0000000000..3906ef2f46
--- /dev/null
+++ b/comm/mailnews/base/public/nsIFolderListener.idl
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgDBHdr;
+
+typedef unsigned long folderListenerNotifyFlagValue;
+
+/**
+ * nsIFolderListener defines callbacks to handle various notifications
+ * about changes in folders.
+ * These listeners can be attached to individual folders, or they
+ * can be registered globally, with nsIMsgMailSession.
+ * These notifications originate from nsIMsgFolder implementations.
+ * (nsIMsgFolder has corresponding methods for generating these
+ * notifications).
+ */
+[scriptable, uuid(f60ee1a2-6d81-422c-958f-d408b1b2daa7)]
+interface nsIFolderListener : nsISupports {
+ // "added" flag covers adding both messages and child folders.
+ const folderListenerNotifyFlagValue added = 0x1;
+ void onFolderAdded(in nsIMsgFolder parent, in nsIMsgFolder child);
+ void onMessageAdded(in nsIMsgFolder parent, in nsIMsgDBHdr msg);
+
+ // "removed" flag covers removing both messages and child folders.
+ const folderListenerNotifyFlagValue removed = 0x2;
+ void onFolderRemoved(in nsIMsgFolder parent, in nsIMsgFolder child);
+ void onMessageRemoved(in nsIMsgFolder parent, in nsIMsgDBHdr msg);
+
+ const folderListenerNotifyFlagValue propertyChanged = 0x4;
+ void onFolderPropertyChanged(in nsIMsgFolder folder,
+ in ACString property,
+ in AUTF8String oldValue,
+ in AUTF8String newValue);
+
+ const folderListenerNotifyFlagValue intPropertyChanged = 0x8;
+ // While this property handles long long (64bit wide) values,
+ // the Javascript engine will only pass values up to 2^53 to the consumers.
+ void onFolderIntPropertyChanged(in nsIMsgFolder folder,
+ in ACString property,
+ in long long oldValue,
+ in long long newValue);
+
+ const folderListenerNotifyFlagValue boolPropertyChanged = 0x10;
+ void onFolderBoolPropertyChanged(in nsIMsgFolder folder,
+ in ACString property,
+ in boolean oldValue,
+ in boolean newValue);
+
+ const folderListenerNotifyFlagValue unicharPropertyChanged = 0x20;
+ void onFolderUnicharPropertyChanged(in nsIMsgFolder folder,
+ in ACString property,
+ in AString oldValue,
+ in AString newValue);
+
+ const folderListenerNotifyFlagValue propertyFlagChanged = 0x40;
+ void onFolderPropertyFlagChanged(in nsIMsgDBHdr msg,
+ in ACString property,
+ in unsigned long oldFlag,
+ in unsigned long newFlag);
+
+ const folderListenerNotifyFlagValue event = 0x80;
+ void onFolderEvent(in nsIMsgFolder folder, in ACString event);
+
+ const folderListenerNotifyFlagValue all = 0xFFFFFFFF;
+};
diff --git a/comm/mailnews/base/public/nsIFolderLookupService.idl b/comm/mailnews/base/public/nsIFolderLookupService.idl
new file mode 100644
index 0000000000..fece4102ad
--- /dev/null
+++ b/comm/mailnews/base/public/nsIFolderLookupService.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+
+/**
+ * This service provides a way to lookup any nsIMsgFolder.
+ *
+ * When looking up folders by URL, note that the URL must be encoded to be a
+ * valid folder URL. Of particular note are the following requirements:
+ * - invalid characters in paths must be percent-encoded
+ * - the URL MUST NOT have a trailing slash (excepting root folders)
+ * - the case must match the expected value exactly
+ * An example of a valid URL is thus:
+ * imap://someuser%40google.com@imap.google.com/INBOX
+ *
+ * The contractid for this service is "@mozilla.org/mail/folder-lookup;1".
+ */
+[scriptable,uuid(f5ed5997-3945-48fc-a59d-d2191a94bb60)]
+interface nsIFolderLookupService : nsISupports
+{
+ /**
+ * Returns a folder with the given URL or null if no such folder exists.
+ *
+ * @param aUrl The folder URL
+ */
+ nsIMsgFolder getFolderForURL(in AUTF8String aUrl);
+
+ /**
+ * Returns a folder with the given URL.
+ * Will happily create and return an invalid (unparented) folder.
+ * Will return null if aUrl is not a folder url.
+ * NOTE: don't use this for new code! It's here purely to help
+ * transition away from RDF-based folder creation.
+ *
+ * @param aUrl The folder URL
+ */
+ nsIMsgFolder getOrCreateFolderForURL(in AUTF8String aUrl);
+
+ /**
+ * Set pretty name again from original name on all folders,
+ * typically used when locale changes.
+ */
+ void setPrettyNameFromOriginalAllFolders();
+};
+
+%{C++
+#define NSIFLS_CONTRACTID "@mozilla.org/mail/folder-lookup;1"
+%}
diff --git a/comm/mailnews/base/public/nsIIncomingServerListener.idl b/comm/mailnews/base/public/nsIIncomingServerListener.idl
new file mode 100644
index 0000000000..f2dd7faca5
--- /dev/null
+++ b/comm/mailnews/base/public/nsIIncomingServerListener.idl
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgIncomingServer.idl"
+
+[scriptable, uuid(E6B64B86-90CB-11d3-8B02-0060B0FC04D2)]
+interface nsIIncomingServerListener : nsISupports {
+ /**
+ * Notification sent when a server is first loaded into the account manager.
+ *
+ * @param server Loaded server.
+ */
+ void onServerLoaded(in nsIMsgIncomingServer server);
+
+ /**
+ * Notification sent when a server is unloaded from the account manager.
+ *
+ * @param server Unloaded server.
+ */
+ void onServerUnloaded(in nsIMsgIncomingServer server);
+
+ /**
+ * Notification sent when a server hostname or username changes.
+ *
+ * @param server Server that was changed.
+ */
+ void onServerChanged(in nsIMsgIncomingServer server);
+};
diff --git a/comm/mailnews/base/public/nsIMailAuthModule.idl b/comm/mailnews/base/public/nsIMailAuthModule.idl
new file mode 100644
index 0000000000..83c0bdca0d
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMailAuthModule.idl
@@ -0,0 +1,40 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * nsIAuthModule provides GSSAPI, NTLM authentications, but it is not
+ * scriptable. nsIMailAuthModule wraps nsIAuthModule and makes it easy to use in
+ * JavaScript.
+ *
+ * The contract id is: "@mozilla.org/mail/auth-module;1".
+ *
+ * @see nsIAuthModule
+ */
+[scriptable, uuid(9895e904-642e-11eb-bb69-6b87832ac976)]
+interface nsIMailAuthModule : nsISupports {
+
+ /**
+ * Initialize an auth module. This is a combination of
+ * nsIAuthModule::CreateInstance and nsIAuthModule::Init. The aType argument
+ * is passed to CreateInstance, other arguments are passed to Init.
+ */
+ void init(in string aType,
+ in ACString aServiceName,
+ in unsigned long aServiceFlags,
+ in AString aDomain,
+ in AString aUsername,
+ in AString aPassword);
+
+ /**
+ * Get the next token in a sequence of authentication steps.
+ * @param aInToken
+ * A base64 encoded string, usually a server challenge.
+ * @returns
+ * A base64 encoded string, usually a response to a server challenge.
+ */
+ ACString getNextToken(in ACString aInToken);
+};
diff --git a/comm/mailnews/base/public/nsIMailChannel.idl b/comm/mailnews/base/public/nsIMailChannel.idl
new file mode 100644
index 0000000000..f5785e6e65
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMailChannel.idl
@@ -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/. */
+
+#include "nsISupports.idl"
+#include "calIItipItem.idl"
+#include "nsIPropertyBag2.idl"
+#include "nsIMsgSMIMEHeaderSink.idl"
+
+interface nsIMailProgressListener;
+interface nsIWebProgress;
+interface nsIRequest;
+
+/**
+ * An interface that email-streaming channels can use to provide access to
+ * parsed message headers, message attachment info, and other metadata.
+ * The intended use is by QIing nsIChannel to nsIMailChannel.
+ */
+[scriptable, uuid(e4abdb58-54fa-4deb-8c43-714a69519b3a)]
+interface nsIMailChannel : nsISupports {
+ /**
+ * Called by MIME emitters to add a header to this mail channel.
+ * Do not call otherwise.
+ */
+ void addHeaderFromMIME(in AUTF8String name, in AUTF8String value);
+
+ /**
+ * Header names for this request, available at onStopRequest.
+ * The number of header names is the same as the number of header values,
+ * and they are in the same order.
+ */
+ readonly attribute Array<AUTF8String> headerNames;
+
+ /**
+ * Header values for this request, available at onStopRequest.
+ */
+ readonly attribute Array<AUTF8String> headerValues;
+
+ /**
+ * Called by MIME emitters to add attachment info to this mail channel.
+ * Do not call otherwise.
+ */
+ void handleAttachmentFromMIME(in AUTF8String contentType,
+ in AUTF8String url,
+ in AUTF8String displayName,
+ in AUTF8String uri,
+ in boolean aNotDownloaded);
+
+ /**
+ * Called by MIME emitters to add attachment info to this mail channel.
+ * Do not call otherwise.
+ */
+ void addAttachmentFieldFromMIME(in AUTF8String field, in AUTF8String value);
+
+ /**
+ * Attachments for this request, available at onStopRequest.
+ */
+ readonly attribute Array<nsIPropertyBag2> attachments;
+
+ /**
+ * The character set of the message, according to the MIME parser. Not the
+ * character set of the channel, which should always be UTF-8.
+ */
+ attribute AUTF8String mailCharacterSet;
+
+ /**
+ * The method property of iMIP attachments, as determined by the MIME parser.
+ * Not to be set after onStopRequest.
+ */
+ attribute AUTF8String imipMethod;
+
+ /**
+ * The actual iMIP invitation, as created by CalMIMEConverter.
+ * Not to be set after onStopRequest.
+ */
+ attribute calIItipItem imipItem;
+
+ /**
+ * Set this in onStartRequest to receive security status notifications.
+ */
+ attribute nsIMsgSMIMEHeaderSink smimeHeaderSink;
+
+ /**
+ * A listener for progress events. This object must also implement
+ * nsISupportsWeakReference.
+ */
+ attribute nsIMailProgressListener listener;
+};
+
+[scriptable, uuid(1286f969-1c20-422e-8247-233fe0d26ba5)]
+interface nsIMailProgressListener : nsISupports {
+ /**
+ * Receive a notification from the parser that it has finished outputting
+ * the headers to the channel.
+ */
+ void onHeadersComplete(in nsIMailChannel mailChannel);
+
+ /**
+ * Receive a notification from the parser that it has finished outputting
+ * the message body to the channel.
+ */
+ void onBodyComplete(in nsIMailChannel mailChannel);
+
+ /**
+ * Receive a notification from the parser that it has finished outputting
+ * the attachment information to the channel.
+ */
+ void onAttachmentsComplete(in nsIMailChannel mailChannel);
+};
diff --git a/comm/mailnews/base/public/nsIMapiRegistry.idl b/comm/mailnews/base/public/nsIMapiRegistry.idl
new file mode 100644
index 0000000000..c4a2c6d545
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMapiRegistry.idl
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface mozIDOMWindowProxy;
+
+/**
+ * This interface provides support for registering Mozilla as the default
+ * Mail Client. This interface can also be used to get/set the user preference
+ * for the default Mail Client.
+ *
+ */
+
+[scriptable, uuid(47D707C3-4369-46A6-A053-5118E12579D6)]
+interface nsIMapiRegistry: nsISupports {
+
+ /** This is set to TRUE if Mozilla is the default mail application
+ */
+ attribute boolean isDefaultMailClient;
+
+ /* Set to TRUE if Mozilla is the default news application */
+ attribute boolean isDefaultNewsClient;
+
+ /* Set to TRUE if we are the default feed/rss application */
+ attribute boolean isDefaultFeedClient;
+
+ /** This is set TRUE only once per session.
+ */
+ readonly attribute boolean showDialog;
+
+ /** This will bring the dialog asking the user if he/she wants to set
+ * Mozilla as default Mail Client.
+ * Call this only if Mozilla is not the default Mail client
+ */
+ void showMailIntegrationDialog(in mozIDOMWindowProxy parentWindow);
+
+ /* After being installed, when we first launch, make sure we add the correct
+ OS registry entries to make us show up as registered mail and news client
+ in the OS
+ */
+
+ void registerMailAndNewsClient();
+};
+
+%{C++
+#define NS_IMAPIREGISTRY_CONTRACTID "@mozilla.org/mapiregistry;1"
+#define NS_IMAPIREGISTRY_CLASSNAME "Mozilla MAPI Registry"
+%}
diff --git a/comm/mailnews/base/public/nsIMessenger.idl b/comm/mailnews/base/public/nsIMessenger.idl
new file mode 100644
index 0000000000..437879782a
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessenger.idl
@@ -0,0 +1,127 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+#include "nsrootidl.idl"
+#include "nsIMsgWindow.idl"
+#include "nsIMsgIdentity.idl"
+
+interface nsIMsgDBHdr;
+interface mozIDOMWindowProxy;
+interface nsITransactionManager;
+interface nsIMsgMessageService;
+interface nsIFile;
+interface nsIUrlListener;
+
+[scriptable, uuid(01b967c8-b289-4e32-ad46-6eb7c89d4106)]
+interface nsIMessenger : nsISupports {
+
+ const long eUnknown = 0;
+ const long eDeleteMsg = 1;
+ const long eMoveMsg = 2;
+ const long eCopyMsg = 3;
+ const long eMarkAllMsg = 4;
+
+ readonly attribute nsITransactionManager transactionManager;
+
+ void setWindow(in mozIDOMWindowProxy ptr, in nsIMsgWindow msgWindow);
+
+ boolean canUndo();
+ boolean canRedo();
+ unsigned long getUndoTransactionType();
+ unsigned long getRedoTransactionType();
+ void undo(in nsIMsgWindow msgWindow);
+ void redo(in nsIMsgWindow msgWindow);
+
+ /**
+ * Saves a given message to a file or template.
+ *
+ * @param aURI The URI of the message to save
+ * @param aAsFile If true, save as file, otherwise save as a template
+ * @param aIdentity When saving as a template, this is used to determine
+ * the location to save the template to.
+ * @param aMsgFilename When saving as a file, the filename to save the
+ * message as, or the default filename for the file
+ * picker.
+ * @param aBypassFilePicker
+ * If not specified or false, this function will show
+ * a file picker when saving as a file. If true, no
+ * file picker will be shown.
+ */
+ void saveAs(in AUTF8String aURI, in boolean aAsFile,
+ in nsIMsgIdentity aIdentity, in AString aMsgFilename,
+ [optional] in boolean aBypassFilePicker);
+
+ /**
+ * Save the given messages as files in a folder - the user will be prompted
+ * for which folder to use.
+ * @param count message count
+ * @param filenameArray the filenames to use
+ * @param messageUriArray uris of the messages to save
+ */
+ void saveMessages(in Array<AString> filenameArray,
+ in Array<AUTF8String> messageUriArray);
+
+ void saveAttachment(in AUTF8String contentType,
+ in AUTF8String url,
+ in AUTF8String displayName,
+ in AUTF8String messageUri,
+ in boolean isExternalAttachment);
+ void saveAllAttachments(in Array<AUTF8String> contentTypeArray,
+ in Array<AUTF8String> urlArray,
+ in Array<AUTF8String> displayNameArray,
+ in Array<AUTF8String> messageUriArray);
+
+ void saveAttachmentToFile(in nsIFile aFile,
+ in AUTF8String aUrl,
+ in AUTF8String aMessageUri,
+ in AUTF8String aContentType,
+ in nsIUrlListener aListener);
+
+ /**
+ * For a single message and attachments, save these attachments to a file, and
+ * remove from the message. No warning windows will appear, so this is
+ * suitable for use in test and filtering.
+ *
+ * @param aDestFolder Folder to save files in
+ * @param aCount Number of attachments to save
+ * @param aContentTypeArray Content types of the attachments
+ * @param aUrlArray Urls for the attachments
+ * @param aDisplayNameArray Files names to save attachments to. Unique
+ * names will be created if needed.
+ * @param aMessageUriArray Uri for the source message
+ * @param aListener Listener to inform of start and stop of detach
+ */
+ void detachAttachmentsWOPrompts(in nsIFile aDestFolder,
+ in Array<AUTF8String> aContentTypeArray,
+ in Array<AUTF8String> aUrlArray,
+ in Array<AUTF8String> aDisplayNameArray,
+ in Array<AUTF8String> aMessageUriArray,
+ in nsIUrlListener aListener);
+
+ void detachAttachment(in AUTF8String contentType,
+ in AUTF8String url,
+ in AUTF8String displayName,
+ in AUTF8String messageUri,
+ in boolean saveFirst,
+ [optional] in boolean withoutWarning);
+ void detachAllAttachments(in Array<AUTF8String> contentTypeArray,
+ in Array<AUTF8String> urlArray,
+ in Array<AUTF8String> displayNameArray,
+ in Array<AUTF8String> messageUriArray,
+ in boolean saveFirst,
+ [optional] in boolean withoutWarning);
+ // saveAttachmentToFolder is used by the drag and drop code to drop an attachment to a destination folder
+ // We need to return the actual file path (including the filename).
+ nsIFile saveAttachmentToFolder(in AUTF8String contentType,
+ in AUTF8String url,
+ in AUTF8String displayName,
+ in AUTF8String messageUri,
+ in nsIFile aDestFolder);
+
+ nsIMsgDBHdr msgHdrFromURI(in AUTF8String aUri);
+
+ AString formatFileSize(in unsigned long long aPos, [optional] in boolean aUseKB);
+};
diff --git a/comm/mailnews/base/public/nsIMessengerMigrator.idl b/comm/mailnews/base/public/nsIMessengerMigrator.idl
new file mode 100644
index 0000000000..e5a74123d3
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessengerMigrator.idl
@@ -0,0 +1,14 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(54818d98-1dd2-11b2-82aa-a9197f997503)]
+interface nsIMessengerMigrator: nsISupports {
+ /* migrate old mailnews prefs to the 5.x world */
+ void UpgradePrefs();
+
+ void createLocalMailAccount(in boolean migrating);
+};
diff --git a/comm/mailnews/base/public/nsIMessengerOSIntegration.idl b/comm/mailnews/base/public/nsIMessengerOSIntegration.idl
new file mode 100644
index 0000000000..99c5127701
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessengerOSIntegration.idl
@@ -0,0 +1,26 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Common interfaces to integrate with different platforms, how they are
+ * implemented depends on the specific platform.
+ */
+[scriptable, uuid(d9e45fee-1dd1-11b2-938c-9147855ed837)]
+interface nsIMessengerOSIntegration : nsISupports {
+ /**
+ * Update the unread count.
+ * @param unreadCount - The number of unread messages.
+ * @param unreadTooltip - The tooltip for the unread count.
+ */
+ void updateUnreadCount(in unsigned long unreadCount,
+ in AString unreadTooltip);
+
+ /**
+ * Use this to do necessary clean up on exit, e.g. reset the badge/tray icon.
+ */
+ void onExit();
+};
diff --git a/comm/mailnews/base/public/nsIMessengerWindowService.idl b/comm/mailnews/base/public/nsIMessengerWindowService.idl
new file mode 100644
index 0000000000..9056a734a8
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessengerWindowService.idl
@@ -0,0 +1,17 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+[scriptable, uuid(113a1a5a-1dd2-11b2-b1b7-a85ccc06c8ce)]
+interface nsIMessengerWindowService : nsISupports {
+ /**
+ * @param aWindowType the type of window you want to create. i.e. "mail:3pane"
+ * @param aFolderURI the folder resource you want pre-selected (if any)
+ * @param aMsgKey a particular message you may want selected in that folder (if any)
+ */
+ void openMessengerWindowWithUri(in string aWindowType, in AUTF8String aFolderURI, in nsMsgKey aMsgKey);
+};
diff --git a/comm/mailnews/base/public/nsIMessengerWindowsIntegration.idl b/comm/mailnews/base/public/nsIMessengerWindowsIntegration.idl
new file mode 100644
index 0000000000..86f33dd651
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMessengerWindowsIntegration.idl
@@ -0,0 +1,16 @@
+/* -*- Mode: IDL; 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/. */
+
+interface mozIDOMWindowProxy;
+
+#include "nsIBaseWindow.idl"
+#include "nsIMessengerOSIntegration.idl"
+
+[scriptable, uuid(e14eb9fe-e05e-4b78-bd31-5b7e1497f91b)]
+interface nsIMessengerWindowsIntegration : nsIMessengerOSIntegration {
+ void hideWindow(in nsIBaseWindow aWindow);
+
+ void showWindow(in mozIDOMWindowProxy aWindow);
+};
diff --git a/comm/mailnews/base/public/nsIMsgAccount.idl b/comm/mailnews/base/public/nsIMsgAccount.idl
new file mode 100644
index 0000000000..aa59222824
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgAccount.idl
@@ -0,0 +1,86 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgIncomingServer.idl"
+#include "nsIMsgIdentity.idl"
+
+/**
+ * An account consists of an incoming server and one or more
+ * outgoing identities. An account is identified by a key,
+ * which is the <account> string in the account preferences,
+ * such as in mail.account.<account>.identities.
+ */
+
+[scriptable, uuid(84181351-4ec8-4ca8-8439-5c68cc591177)]
+interface nsIMsgAccount : nsISupports {
+
+ /// Internal key identifying itself
+ attribute ACString key;
+
+ /// Incoming server stuff
+ attribute nsIMsgIncomingServer incomingServer;
+
+ /// Outgoing identity list (array of nsIMsgIdentity's)
+ readonly attribute Array<nsIMsgIdentity> identities;
+
+ /// The default identity for this account.
+ attribute nsIMsgIdentity defaultIdentity;
+
+ /// Add a new identity to this account
+ void addIdentity(in nsIMsgIdentity identity);
+
+ /// Remove an identity from this account
+ void removeIdentity(in nsIMsgIdentity identity);
+
+ /// Clear all user preferences associated with an account.
+ void clearAllValues();
+
+ /// Name in javascript
+ AString toString();
+
+ /**
+ * Create the server, with error returns.
+ *
+ * Normally each valid account also has a valid server.
+ *
+ * If an extension that creates a server type failed to load, then we
+ * may have an existing account without a valid server. We don't want
+ * to simply delete that account, as that would make the user re-enter
+ * all of the account information after what may be a temporary
+ * update glitch. But we also don't want to leave junk lying around
+ * forever. So what we do is note the time when we first noticed
+ * that the server was unavailable. After a period of time set
+ * in a preference, if the server is still unavailable then delete
+ * the associated account.
+ *
+ * Accounts with invalid server are not shown in any way by the account
+ * manager. But if the server becomes available (for example if an extension
+ * is loaded), then the account will reappear when accounts are loaded.
+ *
+ * Preference definitions:
+ *
+ * mail.server.serverN.secondsToLeaveUnavailable
+ * mail.server.serverN.timeFoundUnavailable
+ *
+ * secondsToLeaveUnavailable: is set by the extension to indicate the
+ * delay, in seconds, between first detection that a server is
+ * unavailable, and the time it can be deleted. This should be set
+ * by the extension when the server is created. If missing, treat as 0.
+ * A typical value would be 2592000 (30 days)(24 hr/day)(3600 seconds/hr)
+ *
+ * timeFoundUnavailable: is set by core code the first time that a
+ * server is detected as unavailable, using now() converted to seconds.
+ * If that time + secondsToLeaveUnavailable is exceeded, core code may
+ * delete the server and its associated account (though for now we default
+ * to the previous behavior, which is to just delete the account).
+ *
+ * @throws NS_ERROR_NOT_AVAILABLE if the server component type could not
+ * be created
+ * NS_ERROR_ALREADY_INITIALIZED if the server is already created
+ * (Other errors may also be possible)
+ */
+ void createServer();
+};
diff --git a/comm/mailnews/base/public/nsIMsgAccountManager.idl b/comm/mailnews/base/public/nsIMsgAccountManager.idl
new file mode 100644
index 0000000000..39f4aecd79
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgAccountManager.idl
@@ -0,0 +1,256 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgAccount.idl"
+#include "nsIMsgIdentity.idl"
+#include "nsIMsgIncomingServer.idl"
+#include "nsIIncomingServerListener.idl"
+#include "nsIMsgFolder.idl"
+
+interface nsIMsgFolderCache;
+interface nsIFolderListener;
+
+[scriptable, uuid(d5ab0eea-49c5-42f2-b2e6-8ad306606d8b)]
+interface nsIMsgAccountManager : nsISupports {
+
+ ACString getUniqueAccountKey();
+
+ nsIMsgAccount createAccount();
+ /*
+ * Return the account with the provided key, or null if none found.
+ */
+ nsIMsgAccount getAccount(in ACString key);
+
+ /**
+ * Removes the account from the list of accounts.
+ *
+ * @param aAccount the account to remove
+ * @param aRemoveFiles remove data directory (local directory) of this account
+ */
+ void removeAccount(in nsIMsgAccount aAccount, [optional] in boolean aRemoveFiles);
+
+ /*
+ * creates a new identity and assigns it a new, unique "key"
+ */
+ nsIMsgIdentity createIdentity();
+
+ /**
+ * Scan the preferences to find a unique server key.
+ */
+ ACString getUniqueServerKey();
+
+ /*
+ * creates a new server and assigns it a new, unique "key"
+ * the given type will be used to construct a ContractID
+ *
+ * @param type "imap", "pop3", "nntp", "none", "rss", "generic"
+ * (suffix of contract ID @mozilla.org/messenger/server;1?type= )
+ */
+ nsIMsgIncomingServer createIncomingServer(in ACString username,
+ in ACString hostname,
+ in ACString type);
+
+ /**
+ * Removes the server from the list of servers
+ *
+ * @param aServer server to remove
+ * @param aRemoveFiles remove directory from profile
+ *
+ * @throws NS_ERROR_FAILURE if server not found
+ */
+ void removeIncomingServer(in nsIMsgIncomingServer aServer,
+ in boolean aRemoveFiles);
+ /*
+ * get the identity with the given key
+ * if the identity does not exist, it will be created
+ */
+ nsIMsgIdentity getIdentity(in ACString key);
+
+ /*
+ * Gets the existing incoming server with the given key
+ * if the server's type does not exist in the preference,
+ * an error is returned/thrown
+ */
+ nsIMsgIncomingServer getIncomingServer(in ACString key);
+
+ /* account list stuff */
+
+ /**
+ * Returns the account that was marked as the default one.
+ * Only some server types can serve as default account.
+ * If there is no such account, null is returned.
+ * You can only set the defaultAccount to an
+ * account already in the account manager.
+ */
+ attribute nsIMsgAccount defaultAccount;
+
+ /**
+ * Ordered list of all accounts, by the order they are in the prefs.
+ * Accounts with hidden servers are not returned.
+ * array of nsIMsgAccount
+ */
+ readonly attribute Array<nsIMsgAccount> accounts;
+
+ /* list of all identities in all accounts
+ * array of nsIMsgIdentity
+ */
+ readonly attribute Array<nsIMsgIdentity> allIdentities;
+
+ /* list of all servers in all accounts, except for hidden and IM servers
+ * array of nsIMsgIncomingServer
+ */
+ readonly attribute Array<nsIMsgIncomingServer> allServers;
+
+ /* summary of summary files folder cache */
+ readonly attribute nsIMsgFolderCache folderCache;
+
+ /* are we shutting down */
+ readonly attribute boolean shutdownInProgress;
+
+ /**
+ * for preventing unauthenticated users from seeing header information
+ */
+ attribute boolean userNeedsToAuthenticate;
+ /*
+ * search for the server with the given username, hostname, and type
+ * the type is the same as is specified in the preferences,
+ * i.e. "imap", "pop3", "none", or "nntp"
+ */
+ nsIMsgIncomingServer findServer(in ACString userName,
+ in ACString hostname,
+ in ACString type,
+ [optional] in long port);
+
+ /*
+ * search for the server with the given uri
+ * an analog to FindServer()
+ */
+ nsIMsgIncomingServer findServerByURI(in nsIURI aURI);
+
+ /**
+ * find the index of this server in the (ordered) list of accounts
+ */
+ long FindServerIndex(in nsIMsgIncomingServer server);
+
+ /**
+ * Finds an account for the given incoming server.
+ *
+ * @param server An incoming server to find the account for.
+ * @return If found, the nsIMsgAccount representing the account found.
+ * Otherwise returns null.
+ */
+ nsIMsgAccount FindAccountForServer(in nsIMsgIncomingServer server);
+
+ /* given a server, return all identities in accounts that have this server
+ * returns an array of nsIMsgIdentity
+ */
+ Array<nsIMsgIdentity> getIdentitiesForServer(in nsIMsgIncomingServer server);
+
+ /**
+ * given a server, return the first identity in accounts that have this server
+ */
+ nsIMsgIdentity getFirstIdentityForServer(in nsIMsgIncomingServer server);
+
+ /* given an identity, return all servers in accounts that have
+ * this identity
+ * returns an array of nsIMsgIncomingServer
+ */
+ Array<nsIMsgIncomingServer> getServersForIdentity(in nsIMsgIdentity identity);
+
+ /* there is a special server "Local Folders" that is guaranteed to exist.
+ * this will allow you to get */
+ attribute nsIMsgIncomingServer localFoldersServer;
+
+ // Create the account for that special server.
+ void createLocalMailAccount();
+
+ /**
+ * Kicks off the creation of all accounts. You do not need to call this and
+ * all accounts should be loaded lazily if you use any of the above.
+ */
+ void loadAccounts();
+
+ /** Frees all the account manager data structures. */
+ void unloadAccounts();
+
+ /**
+ * When the server for an account could not be loaded, typically because the
+ * extension providing it could not be loaded, it is deactivated for a period
+ * of time as documented in nsIMsgAccount.idl. The server is normally only
+ * rechecked at startup but this function can be used to recheck all servers
+ * at any time to avoid having to restart to reactivate an account.
+ */
+ void reactivateAccounts();
+
+ void setSpecialFolders();
+
+ void loadVirtualFolders();
+
+ void WriteToFolderCache(in nsIMsgFolderCache folderCache);
+ void saveVirtualFolders();
+ void closeCachedConnections();
+ void shutdownServers();
+
+ void CleanupOnExit();
+ void SetFolderDoingEmptyTrash(in nsIMsgFolder folder);
+ boolean GetEmptyTrashInProgress();
+
+ void SetFolderDoingCleanupInbox(in nsIMsgFolder folder);
+ boolean GetCleanupInboxInProgress();
+
+ void addRootFolderListener(in nsIFolderListener listener);
+ void removeRootFolderListener(in nsIFolderListener listener);
+
+ // these are going away in favor of add/removeRootFolderListener
+ void addIncomingServerListener(in nsIIncomingServerListener serverListener);
+ void removeIncomingServerListener(in nsIIncomingServerListener serverListener);
+
+ // these are going away in favor of nsIMsgFolder::NotifyEvent(in ACString event);
+ // XXX what does this mean? There is no such function yet.
+ void notifyServerLoaded(in nsIMsgIncomingServer server);
+ void notifyServerUnloaded(in nsIMsgIncomingServer server);
+ void notifyServerChanged(in nsIMsgIncomingServer server);
+
+ // force account info out to prefs file
+ void saveAccountInfo();
+
+ ACString getChromePackageName(in ACString aExtensionName);
+
+ /// Enumerate all incoming servers and their folders and return in an array.
+ readonly attribute Array<nsIMsgFolder> allFolders;
+
+ /**
+ * Iterates over all folders looking for one with the passed in path,
+ * and returns the uri for the matching folder. In the future,
+ * the folder lookup service will provide this functionality.
+ *
+ * @param aLocalPath path of the folder whose uri we want.
+ * @return the URI of the folder that corresponds to aLocalPath
+ */
+ AUTF8String folderUriForPath(in nsIFile aLocalPath);
+
+ // Used to sort servers (accounts) for e.g. the folder pane
+ long getSortOrder(in nsIMsgIncomingServer server);
+
+ /**
+ * Sets new order of accounts.
+ *
+ * @param accountKeys - Account keys in the new preferred order.
+ */
+ void reorderAccounts(in Array<ACString> accountKeys);
+};
+
+%{C++
+#define MAILNEWS_ACCOUNTMANAGER_EXTENSIONS "mailnews-accountmanager-extensions"
+%}
+
+[scriptable, uuid(70032DE0-CD59-41ba-839D-FC1B65367EE7)]
+interface nsIMsgAccountManagerExtension : nsISupports
+{
+ readonly attribute ACString name; // examples: mdn
+ boolean showPanel(in nsIMsgIncomingServer server);
+ readonly attribute ACString chromePackageName; // example: messenger, chrome://messenger/content/am-mdn.xhtml and chrome://messenger/locale/am-mdn.properties
+};
diff --git a/comm/mailnews/base/public/nsIMsgAsyncPrompter.idl b/comm/mailnews/base/public/nsIMsgAsyncPrompter.idl
new file mode 100644
index 0000000000..0e1a68846b
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgAsyncPrompter.idl
@@ -0,0 +1,79 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgAsyncPromptListener;
+
+/**
+ * The nsIMsgAsyncPrompter is intended to provide a way to make asynchronous
+ * message prompts into synchronous ones - so that the user is only prompted
+ * with one at a time.
+ */
+[scriptable, uuid(15f67d0f-947a-4a1e-8f72-6ab7162b4b9c)]
+interface nsIMsgAsyncPrompter : nsISupports {
+ /**
+ * Queues an async prompt request. If there are none queued then this will be
+ * actioned straight away, otherwise the prompt will be queued for action
+ * once previous prompt(s) have been cleared.
+ *
+ * Queued prompts using the same aKey may be amalgamated into one prompt to
+ * save repeated prompts to the user.
+ *
+ * @param aKey A key to determine whether or not the queued prompts can
+ * be combined.
+ * @param aPromptImmediately If the user is retrying a failed password, we
+ * need to prompt right away, even if there is a
+ * prompt up, or prompts queued up. Note that
+ * immediately may not be synchronously, on OS/X.
+ * @param aCaller An nsIMsgAsyncPromptListener to call back to when the prompt
+ * is ready to be made.
+ */
+ void queueAsyncAuthPrompt(in ACString aKey, in boolean aPromptImmediately,
+ in nsIMsgAsyncPromptListener aCaller);
+};
+
+[scriptable, function, uuid(acca94c9-378e-46e3-9a91-6655bf9c91a3)]
+interface nsIMsgAsyncPromptCallback : nsISupports {
+ /**
+ * Called when an auth result is available. Can be passed as a function.
+ *
+ * @param aResult True if there is auth information available following the
+ * prompt, false otherwise.
+ */
+ void onAuthResult(in boolean aResult);
+};
+
+/**
+ * This is used in combination with nsIMsgAsyncPrompter.
+ */
+[scriptable, uuid(fb5307a3-39d0-462e-92c8-c5c288a2612f)]
+interface nsIMsgAsyncPromptListener : nsISupports {
+ /**
+ * This method has been deprecated, please use onPromptStartAsync instead.
+ */
+ boolean onPromptStart();
+
+ /**
+ * Called when the listener should do its prompt. This can happen
+ * synchronously or asynchronously, but in any case when done the callback
+ * method should be called.
+ *
+ * @param aCallback The callback to execute when auth prompt has completed.
+ */
+ void onPromptStartAsync(in nsIMsgAsyncPromptCallback aCallback);
+
+ /**
+ * Called in the case that the queued prompt was combined with another and
+ * there is now authentication information available.
+ */
+ void onPromptAuthAvailable();
+
+ /**
+ * Called in the case that the queued prompt was combined with another but
+ * the prompt was canceled.
+ */
+ void onPromptCanceled();
+};
diff --git a/comm/mailnews/base/public/nsIMsgBiffManager.idl b/comm/mailnews/base/public/nsIMsgBiffManager.idl
new file mode 100644
index 0000000000..168ce1ace1
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgBiffManager.idl
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgIncomingServer.idl"
+
+[scriptable, uuid(17275D52-1622-11d3-8A84-0060B0FC04D2)]
+interface nsIMsgBiffManager : nsISupports {
+
+ void init();
+ void addServerBiff(in nsIMsgIncomingServer server);
+ void removeServerBiff(in nsIMsgIncomingServer server);
+ void forceBiff(in nsIMsgIncomingServer server);
+ void forceBiffAll();
+ void shutdown();
+};
diff --git a/comm/mailnews/base/public/nsIMsgContentPolicy.idl b/comm/mailnews/base/public/nsIMsgContentPolicy.idl
new file mode 100644
index 0000000000..882a39b49b
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgContentPolicy.idl
@@ -0,0 +1,35 @@
+/* -*- mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(c29b2fd3-64d0-4083-a096-c20a9b847a99)]
+
+/**
+ * This interface provide functions which help extension developers
+ * add their customized schema to the exposed protocls of nsMsgContentPolicy.
+ * By default, a list of existing protocols (such as imap and nntp)
+ * are allowed to process urls locally, while non-matching urls are required
+ * to be processed as external.
+ * This interface allows additional protocols to be added to
+ * the list of protocols that are processed locally.
+ * Typically this would be used in cases where a new messaging protocol
+ * is being added by an extension.
+ */
+interface nsIMsgContentPolicy : nsISupports {
+ /**
+ * Add the specific aScheme to nsMsgContentPolicy's exposed protocols.
+ *
+ * @param aScheme scheme who will be added to nsMsgContentPolicy's exposed protocols
+ */
+ void addExposedProtocol(in ACString aScheme);
+
+ /**
+ * Remove the specific aScheme from nsMsgContentPolicy's exposed protocols.
+ *
+ * @param aScheme scheme who will be removed from nsMsgContentPolicy's exposed protocols
+ */
+ void removeExposedProtocol(in ACString aScheme);
+};
diff --git a/comm/mailnews/base/public/nsIMsgCopyService.idl b/comm/mailnews/base/public/nsIMsgCopyService.idl
new file mode 100644
index 0000000000..b28ee9170c
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgCopyService.idl
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; 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/. */
+
+#include "nsrootidl.idl"
+#include "nsISupports.idl"
+#include "nsIMsgFolder.idl"
+#include "nsIMsgCopyServiceListener.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgWindow;
+interface nsIFile;
+
+/**
+ * nsIMsgCopyService is a central point for kicking off message and folder
+ * copy/move operations.
+ * Each operation is queued up and executed in sequence. The actual work is
+ * handled by folder code in an asynchronous fashion. The folder indicates
+ * completion by calling notifyCompletion().
+ *
+ * If the operation was initiated with a non-null nsIMsgCopyServiceListener,
+ * its OnStartCopy() and OnStopCopy() methods will be called when the
+ * operation begins/ends. Any errors are communicated via the result code
+ * parameter passed to OnStopCopy().
+ */
+[scriptable, uuid(f21e428b-73c5-4607-993b-d37325b33722)]
+interface nsIMsgCopyService : nsISupports {
+
+ /**
+ * Copies or moves existing messages from source folder to destination folder.
+ *
+ * @param srcFolder Source folder of an operation.
+ * @param messages The array of nsIMsgHdrs in source folder which will be moved/copied.
+ * @param dstFolder Destination folder of operation.
+ * @param isMove false for copy operation, true for move operation.
+ * @param listener Listener which receive operation notifications
+ * @param msgWindow Window for notification callbacks, can be null.
+ * @param allowUndo Specifies if this operation will be done as an transaction
+ * that can be undone.
+ */
+ void copyMessages(in nsIMsgFolder srcFolder,
+ in Array<nsIMsgDBHdr> messages,
+ in nsIMsgFolder dstFolder,
+ in boolean isMove,
+ in nsIMsgCopyServiceListener listener,
+ in nsIMsgWindow msgWindow,
+ in boolean allowUndo);
+
+ /**
+ * Copies or moves a folder into an existing destination folder.
+ *
+ * @param srcFolder The nsIMsgFolder which will be moved/copied.
+ * @param dstFolder The destination folder of operation.
+ * @param isMove false for copy operation, true for move operation.
+ * @param listener Listener which receive operation notifications.
+ * @param msgWindow Window for notification callbacks, can be null.
+ */
+ void copyFolder(in nsIMsgFolder srcFolder,
+ in nsIMsgFolder dstFolder,
+ in boolean isMove,
+ in nsIMsgCopyServiceListener listener,
+ in nsIMsgWindow msgWindow);
+
+ /**
+ * Copies message in rfc format from file to folder.
+ *
+ * @param aFile A file which contains message in rfc format which
+ * will copied to destFolder.
+ * @param dstFolder Destination folder where a message will be copied.
+ * @param msgToReplace Header which identifies a message to use as a source
+ * of message properties, or null. For example, when
+ * deleting an attachment, the processed message is
+ * stored in a file, but the metadata should be copied
+ * from the original message. This method will NOT delete
+ * the original message.
+ * @param isDraftOrTemplate Specifies whether a message is a stored in draft
+ * folder or not. If is true listener should
+ * implement GetMessageId and return unique id for
+ * message in destination folder. This is important
+ * for IMAP servers which doesn't support uidplus.
+ * If destination folder contains message with the
+ * same message-id then it is possible that listener
+ * get wrong message key in callback
+ * nsIMsgCopyServiceListener::SetMessageKey.
+ * @param aMsgFlags Message flags which will be set after message is
+ * copied
+ * @param aMsgKeywords Keywords which will be set for newly copied
+ * message.
+ * @param listener Listener which receive copy notifications.
+ * @param msgWindow Window for notification callbacks, can be null.
+ */
+ void copyFileMessage(in nsIFile aFile,
+ in nsIMsgFolder dstFolder,
+ in nsIMsgDBHdr msgToReplace,
+ in boolean isDraftOrTemplate,
+ in unsigned long aMsgFlags,
+ in ACString aMsgKeywords,
+ in nsIMsgCopyServiceListener listener,
+ in nsIMsgWindow msgWindow);
+
+ /**
+ * Notify the message copy service that the destination folder has finished
+ * it's messages copying operation so that the copy service can continue
+ * copying the rest of the messages if there are more to copy with.
+ * aSupport and dstFolder uniquely identify a copy service request.
+ *
+ * @param aSupport The originator of CopyMessages or copyFileMessage; it can
+ * be either a nsIMsgFolder or a nsIFile
+ * @param dstFolder The destination folder which performs the copy operation
+ * @param result The result of the copy operation
+ */
+ void notifyCompletion(in nsISupports aSupport,
+ in nsIMsgFolder dstFolder,
+ in nsresult result);
+};
diff --git a/comm/mailnews/base/public/nsIMsgCopyServiceListener.idl b/comm/mailnews/base/public/nsIMsgCopyServiceListener.idl
new file mode 100644
index 0000000000..0c915c8d38
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgCopyServiceListener.idl
@@ -0,0 +1,55 @@
+/* -*- 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/. */
+
+#include "nsrootidl.idl"
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+[scriptable, uuid(da6b9843-5464-4630-b121-c5970aa3d6ed)]
+interface nsIMsgCopyServiceListener : nsISupports {
+
+ /**
+ * Notify the observer that the message has started to be copied. This
+ * method is called only once, at the beginning of a message
+ * copyoperation.
+ */
+ void OnStartCopy();
+
+ /**
+ * Notify the observer that progress as occurred for the message copy
+ * aProgress -
+ * aProgressMax -
+ */
+ void OnProgress(in uint32_t aProgress,
+ in uint32_t aProgressMax);
+
+ /**
+ * Setting newly created message key. This method is tailored specifically
+ * for nsIMsgCopyService::copyFileMessage() when saving Drafts/Templates.
+ * We need to have a way to inform the client what's the key of the newly
+ * created message.
+ * aKey -
+ */
+ void SetMessageKey(in nsMsgKey aKey);
+
+ /**
+ * Getting the file message message ID. This method is tailored
+ * specifically for nsIMsgCopyService::copyFileMessage() when saving
+ * Drafts/Templates. In order to work with imap server which doesn't
+ * support uidplus we have to use search command to retrieve the key of
+ * newly created message. Message ID generated by the compose guarantee its
+ * uniqueness.
+ * aMessageId -
+ */
+ void GetMessageId(out ACString aMessageId);
+
+ /**
+ * Notify the observer that the message copied operation has completed.
+ * This method is called regardless of whether the the operation was
+ * successful.
+ * aStatus - indicate whether the operation was succeeded
+ */
+ void OnStopCopy(in nsresult aStatus);
+};
diff --git a/comm/mailnews/base/public/nsIMsgCustomColumnHandler.idl b/comm/mailnews/base/public/nsIMsgCustomColumnHandler.idl
new file mode 100644
index 0000000000..1b5287c56e
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgCustomColumnHandler.idl
@@ -0,0 +1,40 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsITreeView.idl"
+
+interface nsIMsgDBHdr;
+
+ /* //TODO JavaDoc
+ When implementing a js custom column handler (of type nsITreeView) you must implement the following
+ functions:
+ 1. isEditable
+ 2. GetCellProperties
+ 3. GetImageSrc
+ 4. GetCellText
+ 5. CycleCell
+ 6. GetSortStringForRow
+ 7. GetSortLongForRow
+ 8. isString
+
+ You can, at your option, implement
+ 9. GetRowProperties.
+
+ With Bug 1192696, Grouped By Sort was implemented for custom columns.
+ Implementers should consider that the value returned by GetSortStringForRow
+ will be displayed in the grouped header row, as well as be used as the
+ sort string.
+
+ If implementing a c++ custom column handler, you must define all
+ nsITreeView and nsIMsgCustomColumnHandler methods.
+ */
+
+[scriptable, uuid(00f75b13-3ac4-4a17-a8b9-c6e4dd1b3f32)]
+interface nsIMsgCustomColumnHandler : nsITreeView
+{
+ AString getSortStringForRow(in nsIMsgDBHdr aHdr);
+ unsigned long getSortLongForRow(in nsIMsgDBHdr aHdr);
+ boolean isString();
+};
diff --git a/comm/mailnews/base/public/nsIMsgDBView.idl b/comm/mailnews/base/public/nsIMsgDBView.idl
new file mode 100644
index 0000000000..1c824c58f9
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgDBView.idl
@@ -0,0 +1,558 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgWindow;
+interface nsIMessenger;
+interface nsIMsgDBHdr;
+interface nsIMsgThread;
+interface nsIMsgDBViewCommandUpdater;
+interface nsIMsgJSTree;
+interface nsIMsgDatabase;
+interface nsIMsgSearchSession;
+interface nsIMsgEnumerator;
+interface nsIMsgCustomColumnHandler;
+
+typedef long nsMsgViewNotificationCodeValue;
+typedef long nsMsgViewCommandCheckStateValue;
+typedef long nsMsgViewCommandTypeValue;
+typedef long nsMsgNavigationTypeValue;
+
+[scriptable, uuid(682a18be-fd18-11d4-a5be-0060b0fc04b7)]
+interface nsMsgViewSortOrder : nsISupports
+{
+ const nsMsgViewSortOrderValue none = 0;
+ const nsMsgViewSortOrderValue ascending = 1;
+ const nsMsgViewSortOrderValue descending = 2;
+};
+
+[scriptable, uuid(f28a1cdf-06c3-4e98-8f66-f49991670071)]
+interface nsMsgViewType : nsISupports {
+ const nsMsgViewTypeValue eShowAllThreads = 0;
+ const nsMsgViewTypeValue eShowThreadsWithUnread = 2;
+ const nsMsgViewTypeValue eShowWatchedThreadsWithUnread = 3;
+ const nsMsgViewTypeValue eShowQuickSearchResults = 4;
+ const nsMsgViewTypeValue eShowVirtualFolderResults = 5;
+ const nsMsgViewTypeValue eShowSearch = 6;
+};
+
+[scriptable, uuid(64852276-1dd2-11b2-8103-afe12002c053)]
+interface nsMsgViewFlagsType : nsISupports
+{
+ /**
+ * flags for GetViewFlags
+ */
+ const nsMsgViewFlagsTypeValue kNone = 0x0;
+ const nsMsgViewFlagsTypeValue kThreadedDisplay = 0x1;
+ const nsMsgViewFlagsTypeValue kShowIgnored = 0x8;
+ const nsMsgViewFlagsTypeValue kUnreadOnly = 0x10;
+ const nsMsgViewFlagsTypeValue kExpandAll = 0x20;
+ const nsMsgViewFlagsTypeValue kGroupBySort = 0x40;
+};
+
+[scriptable, uuid(b94fc200-3008-420a-85c7-67842f133ef8)]
+interface nsMsgViewSortType : nsISupports
+{
+ const nsMsgViewSortTypeValue byNone = 0x11; /* not sorted */
+ const nsMsgViewSortTypeValue byDate = 0x12;
+ const nsMsgViewSortTypeValue bySubject = 0x13;
+ const nsMsgViewSortTypeValue byAuthor = 0x14;
+ const nsMsgViewSortTypeValue byId = 0x15;
+ const nsMsgViewSortTypeValue byThread = 0x16;
+ const nsMsgViewSortTypeValue byPriority = 0x17;
+ const nsMsgViewSortTypeValue byStatus = 0x18;
+ const nsMsgViewSortTypeValue bySize = 0x19;
+ const nsMsgViewSortTypeValue byFlagged = 0x1a;
+ const nsMsgViewSortTypeValue byUnread = 0x1b;
+ const nsMsgViewSortTypeValue byRecipient = 0x1c;
+ const nsMsgViewSortTypeValue byLocation = 0x1d;
+ const nsMsgViewSortTypeValue byTags = 0x1e;
+ const nsMsgViewSortTypeValue byJunkStatus = 0x1f;
+ const nsMsgViewSortTypeValue byAttachments = 0x20;
+ const nsMsgViewSortTypeValue byAccount = 0x21;
+ const nsMsgViewSortTypeValue byCustom = 0x22;
+ const nsMsgViewSortTypeValue byReceived = 0x23;
+ const nsMsgViewSortTypeValue byCorrespondent = 0x24;
+};
+
+[scriptable, uuid(255d1c1e-fde7-11d4-a5be-0060b0fc04b7)]
+interface nsMsgViewNotificationCode : nsISupports
+{
+ const nsMsgViewNotificationCodeValue none = 0;
+ /* No change; this call is just being used to potentially nest other sets of calls
+ inside it. The "where" and "num" parameters are unused.
+ */
+ const nsMsgViewNotificationCodeValue insertOrDelete = 1;
+ /* Some lines have been inserted or deleted.
+ The "where" parameter will indicate the first line that has been added or
+ removed; the "num" parameter will indicate how many lines, and will be positive on
+ an insertion and negative on a deletion.
+ */
+ const nsMsgViewNotificationCodeValue changed = 2;
+ /* Some lines have had their contents changed (e.g., messages have been marked read
+ or something.) "where" indicates the first line with a change; "num" indicates
+ how many changed.
+ */
+ const nsMsgViewNotificationCodeValue all = 4;
+ /* Everything changed. We're now not displaying anything like what we were; we
+ probably opened a new folder or something. The FE needs to forget anything it ever knew
+ about what was being displayed, and start over. The "where" and "num" parameters are
+ unused.
+ */
+};
+
+[scriptable, uuid(4ec9248e-0108-11d5-a5be-0060b0fc04b7)]
+interface nsMsgViewCommandCheckState : nsISupports
+{
+ const nsMsgViewCommandCheckStateValue notUsed = 0;
+ const nsMsgViewCommandCheckStateValue checked = 1;
+ const nsMsgViewCommandCheckStateValue unchecked = 2;
+};
+
+[scriptable, uuid(ad36e6cc-0109-11d5-a5be-0060b0fc04b7)]
+interface nsMsgViewCommandType : nsISupports
+{
+ const nsMsgViewCommandTypeValue markMessagesRead = 0;
+ const nsMsgViewCommandTypeValue markMessagesUnread = 1;
+ const nsMsgViewCommandTypeValue toggleMessageRead = 2;
+
+ const nsMsgViewCommandTypeValue flagMessages = 3;
+ const nsMsgViewCommandTypeValue unflagMessages = 4;
+
+ const nsMsgViewCommandTypeValue toggleThreadWatched = 6;
+
+ const nsMsgViewCommandTypeValue deleteMsg = 7;
+ const nsMsgViewCommandTypeValue deleteNoTrash = 8;
+ const nsMsgViewCommandTypeValue markThreadRead = 9;
+ const nsMsgViewCommandTypeValue markAllRead = 10;
+ const nsMsgViewCommandTypeValue expandAll = 11;
+ const nsMsgViewCommandTypeValue collapseAll = 12;
+
+ const nsMsgViewCommandTypeValue copyMessages = 13;
+ const nsMsgViewCommandTypeValue moveMessages = 14;
+
+ const nsMsgViewCommandTypeValue selectAll = 15;
+ const nsMsgViewCommandTypeValue downloadSelectedForOffline = 16;
+ const nsMsgViewCommandTypeValue downloadFlaggedForOffline = 17;
+
+ const nsMsgViewCommandTypeValue selectThread = 18;
+ const nsMsgViewCommandTypeValue selectFlagged = 19;
+ const nsMsgViewCommandTypeValue cmdRequiringMsgBody = 20;
+ const nsMsgViewCommandTypeValue label0 = 21;
+ const nsMsgViewCommandTypeValue label1 = 22;
+ const nsMsgViewCommandTypeValue label2 = 23;
+ const nsMsgViewCommandTypeValue label3 = 24;
+ const nsMsgViewCommandTypeValue label4 = 25;
+ const nsMsgViewCommandTypeValue label5 = 26;
+ const nsMsgViewCommandTypeValue lastLabel = 26;
+
+ const nsMsgViewCommandTypeValue junk = 27;
+ const nsMsgViewCommandTypeValue unjunk = 28;
+ const nsMsgViewCommandTypeValue undeleteMsg = 29;
+
+ const nsMsgViewCommandTypeValue applyFilters = 30;
+ const nsMsgViewCommandTypeValue runJunkControls = 31;
+ const nsMsgViewCommandTypeValue deleteJunk = 32;
+};
+
+[scriptable, uuid(65903eb2-1dd2-11b2-ac45-c5b69c1618d7)]
+interface nsMsgNavigationType : nsISupports
+{
+ const nsMsgNavigationTypeValue firstMessage = 1;
+ const nsMsgNavigationTypeValue nextMessage = 2;
+ const nsMsgNavigationTypeValue previousMessage = 3;
+ const nsMsgNavigationTypeValue lastMessage = 4;
+ /**
+ * must match nsMsgViewCommandTypeValue toggleThreadKilled
+ */
+ const nsMsgNavigationTypeValue toggleThreadKilled = 5;
+ const nsMsgNavigationTypeValue firstUnreadMessage = 6;
+ const nsMsgNavigationTypeValue nextUnreadMessage = 7;
+ const nsMsgNavigationTypeValue previousUnreadMessage = 8;
+ const nsMsgNavigationTypeValue lastUnreadMessage = 9;
+ const nsMsgNavigationTypeValue nextUnreadThread = 10;
+ const nsMsgNavigationTypeValue nextUnreadFolder = 11;
+ const nsMsgNavigationTypeValue nextFolder = 12;
+ const nsMsgNavigationTypeValue readMore = 13;
+ /**
+ * Go back to the previous visited message
+ */
+ const nsMsgNavigationTypeValue back = 15;
+ /**
+ * Go forward to the previous visited message
+ */
+ const nsMsgNavigationTypeValue forward = 16;
+ const nsMsgNavigationTypeValue firstFlagged = 17;
+ const nsMsgNavigationTypeValue nextFlagged = 18;
+ const nsMsgNavigationTypeValue previousFlagged = 19;
+ const nsMsgNavigationTypeValue firstNew = 20;
+ const nsMsgNavigationTypeValue editUndo = 21;
+ const nsMsgNavigationTypeValue editRedo = 22;
+ const nsMsgNavigationTypeValue toggleSubthreadKilled = 23;
+};
+
+/*
+ * The contract ID for this component is @mozilla.org/msgDBView/msgDBViewService;1.
+ */
+[scriptable, uuid(bcf6afbe-7d4f-11ec-9092-eb4fed0a5aaa)]
+interface nsIMsgDBViewService : nsISupports
+{
+ /**
+ * JS-callable service to initialize static variables in nsMsgDBView.cpp
+ * upon initialization or when locale changes.
+ */
+ void initializeDBViewStrings();
+};
+
+[scriptable, uuid(fe8a2326-4dd0-11e5-8b8a-206a8aa7a25c)]
+interface nsIMsgDBView : nsISupports
+{
+ /** A shim of XULTreeElement, with only the methods called by nsMsgDBView. */
+ void setJSTree(in nsIMsgJSTree tree);
+
+ void open(in nsIMsgFolder folder, in nsMsgViewSortTypeValue sortType, in nsMsgViewSortOrderValue sortOrder, in nsMsgViewFlagsTypeValue viewFlags, out long count);
+ void openWithHdrs(in nsIMsgEnumerator aHeaders, in nsMsgViewSortTypeValue aSortType,
+ in nsMsgViewSortOrderValue aSortOrder,
+ in nsMsgViewFlagsTypeValue aViewFlags, out long aCount);
+ void close();
+
+ void init(in nsIMessenger aMessengerInstance, in nsIMsgWindow aMsgWindow, in nsIMsgDBViewCommandUpdater aCommandUpdater);
+
+ void sort(in nsMsgViewSortTypeValue sortType, in nsMsgViewSortOrderValue sortOrder);
+
+ void doCommand(in nsMsgViewCommandTypeValue command);
+ void doCommandWithFolder(in nsMsgViewCommandTypeValue command, in nsIMsgFolder destFolder);
+ void getCommandStatus(in nsMsgViewCommandTypeValue command, out boolean selectable_p,
+ out nsMsgViewCommandCheckStateValue selected_p);
+ void applyCommandToIndices(in nsMsgViewCommandTypeValue command,
+ in Array<nsMsgViewIndex> selection);
+
+ readonly attribute nsMsgViewTypeValue viewType;
+ attribute nsMsgViewFlagsTypeValue viewFlags;
+ /** Assigning to this value does not induce a sort; use the sort() method! */
+ attribute nsMsgViewSortTypeValue sortType;
+ readonly attribute nsMsgViewSortOrderValue sortOrder;
+ /**
+ * Reflects the current secondary sort when a secondary sort is in effect.
+ * If the primary sort is by date or id, the value of this attribute is moot.
+ * Assigning to this value does not induce a sort; use the sort() method once
+ * to set your secondary sort, then use it again to set your primary sort.
+ * The only conceivable reason to write to this value is if you have a
+ * grouped view where you want to affect the sort order of the (secondary)
+ * date sort. (Secondary sort is always by date for grouped views.)
+ */
+ attribute nsMsgViewSortTypeValue secondarySortType;
+ /**
+ * Reflects the current secondary sort order.
+ * Assigning to this value does not induce a sort; use the sort() method for
+ * all primary and secondary sort needs. The only reason to assign to this
+ * value is to affect the secondary sort of a grouped view.
+ */
+ attribute nsMsgViewSortOrderValue secondarySortOrder;
+ readonly attribute nsMsgKey keyForFirstSelectedMessage;
+ readonly attribute nsMsgViewIndex viewIndexForFirstSelectedMsg;
+ /**
+ * this method will automatically expand the destination thread,
+ * if needs be.
+ */
+ void viewNavigate(in nsMsgNavigationTypeValue motion, out nsMsgKey resultId, out nsMsgViewIndex resultIndex, out nsMsgViewIndex threadIndex, in boolean wrap);
+
+ readonly attribute nsIMsgFolder msgFolder;
+ attribute nsIMsgFolder viewFolder; // in the case of virtual folders, the VF db.
+
+ nsMsgKey getKeyAt(in nsMsgViewIndex index);
+
+ /**
+ * Get the view flags at the passed in index.
+ *
+ * @param aIndex - index to get the view flags for
+ *
+ * @ return - 32 bit view flags (e.g., elided)
+ */
+ unsigned long getFlagsAt(in nsMsgViewIndex aIndex);
+
+ /**
+ * Get the msg hdr at the passed in index
+ *
+ * @param aIndex - index to get the msg hdr at.
+ *
+ * @return - msg hdr at the passed in index
+ * @exception - NS_MSG_INVALID_DBVIEW_INDEX
+ */
+ nsIMsgDBHdr getMsgHdrAt(in nsMsgViewIndex aIndex);
+
+ nsIMsgFolder getFolderForViewIndex(in nsMsgViewIndex index); // mainly for search
+ AUTF8String getURIForViewIndex(in nsMsgViewIndex index);
+ nsIMsgDBView cloneDBView(in nsIMessenger aMessengerInstance, in nsIMsgWindow aMsgWindow, in nsIMsgDBViewCommandUpdater aCommandUpdater);
+
+ /**
+ * Provides a list of the message headers for the currently selected messages.
+ * If the "mail.operate_on_msgs_in_collapsed_threads" preference is enabled,
+ * then any collapsed thread roots that are selected will also (conceptually)
+ * have all of the messages in that thread selected and they will be included
+ * in the returned list. The one exception to this is if the front end fails
+ * to summarize the selection, and we fall back to just displaying a single
+ * message. In that case, we won't include the children of the collapsed
+ * thread. However, the numSelected attribute will count those children,
+ * because the summarizeSelection code uses that to know that it should
+ * try to summarize the selection.
+ *
+ * If the user has right-clicked on a message, this will return that message
+ * (and any collapsed children if so enabled) and not the selection prior to
+ * the right-click.
+ *
+ * @return an array containing the selected message headers. You are free to
+ * mutate the array; it will not affect the underlying selection.
+ */
+ Array<nsIMsgDBHdr> getSelectedMsgHdrs();
+
+ Array<AUTF8String> getURIsForSelection();
+ Array<nsMsgViewIndex> getIndicesForSelection();
+
+ readonly attribute AUTF8String URIForFirstSelectedMessage;
+ readonly attribute nsIMsgDBHdr hdrForFirstSelectedMessage;
+
+ /**
+ * The number of selected messages. If the
+ * "mail.operate_on_msgs_in_collapsed_threads" preference is enabled, then
+ * any collapsed thread roots that are selected will also conceptually have
+ * all of the messages in that thread selected.
+ */
+ readonly attribute unsigned long numSelected;
+ readonly attribute nsMsgViewIndex msgToSelectAfterDelete;
+ readonly attribute nsMsgViewIndex currentlyDisplayedMessage;
+
+ /**
+ * Number of messages in view, including messages in collapsed threads.
+ * Not currently implemented for threads with unread or watched threads
+ * with unread.
+ */
+ readonly attribute long numMsgsInView;
+ // used by "go to folder" feature
+ // and "remember last selected message" feature
+ // if key is not found, we don't select.
+ void selectMsgByKey(in nsMsgKey key);
+
+ void selectFolderMsgByKey(in nsIMsgFolder aFolder, in nsMsgKey aKey);
+ // we'll suppress displaying messages if the message pane is collapsed
+ attribute boolean suppressMsgDisplay;
+
+ // we'll suppress command updating during folder loading
+ attribute boolean suppressCommandUpdating;
+
+ /**
+ * Suppress change notifications. This is faster than Begin/EndUpdateBatch
+ * on the tree, but less safe in that you're responsible for row invalidation
+ * and row count changes.
+ */
+ attribute boolean suppressChangeNotifications;
+
+ //to notify tree that rows are going away
+ void onDeleteCompleted(in boolean succeeded);
+
+ readonly attribute nsIMsgDatabase db;
+
+ readonly attribute boolean supportsThreading;
+
+ attribute nsIMsgSearchSession searchSession;
+ readonly attribute boolean removeRowOnMoveOrDelete;
+
+ /**
+ * Finds the view index of the passed in msgKey. Note this should not
+ * be called on cross-folder views since the msgKey may not be unique.
+ *
+ * @param aMsgKey - key to find.
+ * @param aExpand - whether to expand a collapsed thread to find the key.
+ *
+ * @return - view index of msg hdr, -1 if hdr not found.
+ */
+ nsMsgViewIndex findIndexFromKey(in nsMsgKey aMsgKey, in boolean aExpand);
+ /**
+ * Finds the view index of the passed in msgHdr.
+ *
+ * @param aMsgHdr - hdr to find.
+ * @param aExpand - whether to expand a collapsed thread to find the hdr.
+ *
+ * @return - view index of msg hdr, -1 if hdr not found.
+ */
+ nsMsgViewIndex findIndexOfMsgHdr(in nsIMsgDBHdr aMsgHdr, in boolean aExpand);
+
+ /**
+ * Expands a thread and selects all it's member messages.
+ *
+ * @param aIndex - View index of a message in the thread to expand (can be
+ any message which is a member of the thread).
+ * @param aAugment - If true, Augment the existing selection.
+ * If false, replace it.
+ */
+ void ExpandAndSelectThreadByIndex(in nsMsgViewIndex aIndex, in boolean aAugment);
+
+ /**
+ * This method returns the nsIMsgThread object containing the header displayed
+ * at the desired row. For grouped views and cross folder saved searches,
+ * this will be the view thread, not the db thread.
+ *
+ * @param aIndex view index we want corresponding thread object of.
+ *
+ * @return the thread object at the requested view index
+ */
+ nsIMsgThread getThreadContainingIndex(in nsMsgViewIndex aIndex);
+
+ /**
+ * Insert rows into the view. The caller should use NoteChange() below to
+ * update the view.
+ *
+ * @param aIndex view index for insertion start.
+ * @param aNumRows number of rows to insert.
+ * @param aKey msgKey.
+ * @param aFlags msgFlags.
+ * @param aLevel treeview indent level.
+ * @param aFolder nsIMsgFolder, required for search/xfvf views.
+ */
+ void insertTreeRows(in nsMsgViewIndex aIndex, in unsigned long aNumRows,
+ in nsMsgKey aKey, in nsMsgViewFlagsTypeValue aFlags,
+ in unsigned long aLevel, in nsIMsgFolder aFolder);
+
+ /**
+ * Remove rows from the view. The caller should use NoteChange() below to
+ * update the view.
+ *
+ * @param aIndex view index for removal start.
+ * @param aNumRows number of rows to remove.
+ */
+ void removeTreeRows(in nsMsgViewIndex aIndex, in unsigned long aNumRows);
+
+ /**
+ * Notify tree that rows have changed.
+ *
+ * @param aFirstLineChanged first view index for changed rows.
+ * @param aNumRows number of rows changed; < 0 means removed.
+ * @param aChangeType changeType.
+ */
+ void NoteChange(in nsMsgViewIndex aFirstLineChanged, in long aNumRows,
+ in nsMsgViewNotificationCodeValue aChangeType);
+
+ /**
+ * Return the view thread corresponding to aMsgHdr. If we're a cross-folder
+ * view, then it would be the cross folder view thread, otherwise, the
+ * db thread object.
+ *
+ * @param aMsgHdr message header we want the view thread object of.
+ *
+ * @return view thread object for msg hdr.
+ */
+ nsIMsgThread getThreadContainingMsgHdr(in nsIMsgDBHdr aMsgHdr);
+
+ // use lines or kB for size?
+ readonly attribute boolean usingLines;
+
+ // Custom Column Implementation note: see nsIMsgCustomColumnHandler
+
+ // attaches a custom column handler to a specific column (can be a new column or a built in)
+ void addColumnHandler(in AString aColumn, in nsIMsgCustomColumnHandler aHandler);
+
+ // removes a custom column handler leaving the column to be handled by the system
+ void removeColumnHandler(in AString aColumn);
+
+ // returns the custom column handler attached to a specific column - if any
+ nsIMsgCustomColumnHandler getColumnHandler(in AString aColumn);
+
+ /**
+ * The custom column to use for sorting purposes (when sort type is
+ * nsMsgViewSortType.byCustom.)
+ */
+ attribute AString curCustomColumn;
+
+ /**
+ * The custom column used for a secondary sort, blank if secondarySort is
+ * not byCustom. The secondary sort design is such that the desired secondary
+ * is sorted first, followed by sort by desired primary. The secondary is
+ * read only, as it is set internally according to this design.
+ */
+ readonly attribute AString secondaryCustomColumn;
+ /**
+ * Scriptable accessor for the cell text for a column
+ *
+ * @param aRow - row we want cell text for
+ * @param aColumnName - name of column we want cell text for
+ *
+ * @returns The cell text for the given row and column, if any.
+ * @notes This does not work for custom columns yet.
+ */
+ AString cellTextForColumn(in long aRow, in AString aColumnName);
+
+ /**
+ * Get all of the data needed to display a row. Effectively a combination of
+ * CellTextForColumn, GetRowProperties and GetLevel, for performance reasons.
+ *
+ * @param aRow - Index of the row we want data for.
+ * @param aColumnNames - The column names we want cell text for.
+ * @param aProperties - The properties of the row.
+ * @param aThreadLevel - The thread level of the row.
+ *
+ * @returns The cell text for the columns in `aColumnNames`.
+ */
+ Array<AString> cellDataForColumns(in long aRow,
+ in Array<AString> aColumnNames,
+ out AString aProperties,
+ out long aThreadLevel);
+};
+
+/* this interface is rapidly morphing from a command updater interface into a more generic
+ FE updater interface to handle changes in the view
+*/
+
+[scriptable, uuid(ce8f52ee-e742-4b31-8bdd-2b3a8168a117)]
+interface nsIMsgDBViewCommandUpdater : nsISupports
+{
+ /* Eventually we'll flush this out into some kind of rich interface
+ which may take specific selection changed type notifications like
+ no selections, single selection, multi-selection, etc. For starters,
+ we are going to keep it generic. The back end will only push an update
+ command status when the # of selected items changes.
+ */
+
+ void updateCommandStatus();
+
+ /* displayed message has changed */
+ void displayMessageChanged(in nsIMsgFolder aFolder, in AString aSubject, in ACString aKeywords);
+
+ /**
+ * allows the backend to tell the front end to re-determine
+ * which message we should selet after a delete or move
+ */
+ void updateNextMessageAfterDelete();
+
+ /**
+ * tell the front end that the selection has changed, and may need to be
+ * resummarized.
+ *
+ * @return true if we did summarize, false otherwise.
+ */
+ boolean summarizeSelection();
+
+ /**
+ * Tell the front end that the selected message was removed and it should update.
+ */
+ void selectedMessageRemoved();
+};
+
+/**
+ * A shim of XULTreeElement, with only the methods called by nsMsgDBView.
+ */
+[scriptable, uuid(c5f6b1a2-f56a-49cb-b863-badf158206d5)]
+interface nsIMsgJSTree : nsISupports
+{
+ void beginUpdateBatch();
+ void endUpdateBatch();
+ void ensureRowIsVisible(in long index);
+ void invalidate();
+ void invalidateRange(in long startIndex, in long endIndex);
+ void rowCountChanged(in long index, in long count);
+ attribute long currentIndex;
+};
diff --git a/comm/mailnews/base/public/nsIMsgEnumerator.idl b/comm/mailnews/base/public/nsIMsgEnumerator.idl
new file mode 100644
index 0000000000..9ce2523a88
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgEnumerator.idl
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgThread;
+
+/**
+ * nsIJSIterator implements the JavaScript iterator protocol.
+ * For details on the JS side, see:
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol
+ */
+[scriptable, uuid(4406175e-1aa4-414c-ad7c-86bd76e102f8)]
+interface nsIJSIterator : nsISupports {
+ /**
+ * @returns {{value: Object, done: Boolean}} the iteration state.
+ * 'value' holds the next item, and 'done' flags the end of the iteration.
+ * (notionally, both 'value' and 'done' are always present, but a missing
+ * 'done' is considered to be false, and 'value' should be ignored if
+ * 'done' is true, according to the protocol).
+ */
+ [implicit_jscontext]
+ jsval next();
+};
+
+
+/**
+ * nsIMsgEnumerator is an object for iterating forward over an ordered set of
+ * messages (nsIMsgHdrs). There is no provision to reset the iteration.
+ */
+[scriptable, uuid(5760cb6d-1a71-485f-ad85-5a98a9da2104)]
+interface nsIMsgEnumerator : nsISupports {
+ /**
+ * Implements the JavaScript iterable protocol, which allows
+ * for...of constructs and the like.
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol
+ * NOTE: the nsIMsgEnumerator is "used up" by iterator(), just as it is by
+ * getNext()/hasMoreElements(). So you can only loop over the enumerator once.
+ */
+ [symbol]
+ nsIJSIterator iterator();
+
+ /**
+ * Called to determine whether or not there are any messages remaining
+ * to be retrieved from the enumerator.
+ *
+ * @returns true if getNext() may be called to fetch a message, or
+ * false if there are no more messages.
+ */
+ boolean hasMoreElements();
+
+ /**
+ * Called to retrieve the next message in the set.
+ *
+ * @returns the next element in the enumeration.
+ */
+ nsIMsgDBHdr getNext();
+};
+
+/**
+ * nsIMsgThreadEnumerator is an object for iterating forward over an ordered set of
+ * message threads (nsIMsgThread). There is no provision to reset the iteration.
+ */
+[scriptable, uuid(e39ae1a8-2b94-4f2b-abb4-250171047501)]
+interface nsIMsgThreadEnumerator : nsISupports {
+ /**
+ * Implements the JavaScript iterable protocol, which allows
+ * for...of constructs and the like.
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol
+ * NOTE: the nsIMsgThreadEnumerator is "used up" by iterator(), just as it is by
+ * getNext()/hasMoreElements(). So you can only loop over the enumerator once.
+ */
+ [symbol]
+ nsIJSIterator iterator();
+
+ /**
+ * Called to determine whether or not there are any messages remaining
+ * to be retrieved from the enumerator.
+ *
+ * @returns true if getNext() may be called to fetch a message, or
+ * false if there are no more messages.
+ */
+ boolean hasMoreElements();
+
+ /**
+ * Called to retrieve the next thread in the set.
+ *
+ * @returns the next element in the enumeration.
+ */
+ nsIMsgThread getNext();
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolder.idl b/comm/mailnews/base/public/nsIMsgFolder.idl
new file mode 100644
index 0000000000..4f0edb8818
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolder.idl
@@ -0,0 +1,877 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+#include "nsIFolderListener.idl"
+#include "nsIMsgIncomingServer.idl"
+#include "nsIMsgCopyServiceListener.idl"
+#include "nsIUrlListener.idl"
+#include "nsIMsgEnumerator.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgWindow;
+interface nsIMsgDatabase;
+interface nsIDBFolderInfo;
+interface nsIMsgFilterList;
+interface nsIFile;
+interface nsIOutputStream;
+interface nsIInputStream;
+interface nsIFile;
+interface nsIMsgIdentity;
+interface nsIMsgThread;
+interface nsIMsgPluggableStore;
+
+typedef long nsMsgBiffState;
+
+// enumerated type for determining if a message has been replied to, forwarded, etc.
+typedef long nsMsgDispositionState;
+
+/*
+ * The contract ID for this component is @mozilla.org/msgFolder/msgFolderService;1.
+ */
+[scriptable, uuid(5639c204-48ac-4115-897f-3b16821fe118)]
+interface nsIMsgFolderService : nsISupports
+{
+ /**
+ * JS-callable service to initialize static variables in nsMsgDBFolder.cpp
+ * upon initialization or when locale changes.
+ */
+ void initializeFolderStrings();
+};
+
+[scriptable, uuid(5d253ba2-42aa-43a7-b584-0059855ababf)]
+interface nsIMsgFolder : nsISupports {
+
+ const nsMsgBiffState nsMsgBiffState_NewMail = 0; // User has new mail waiting.
+ const nsMsgBiffState nsMsgBiffState_NoMail = 1; // No new mail is waiting.
+ const nsMsgBiffState nsMsgBiffState_Unknown = 2; // We dunno whether there is new mail.
+
+ /* folder name properties */
+ readonly attribute AUTF8String URI;
+ attribute AString name;
+ attribute AString prettyName;
+ readonly attribute AString abbreviatedName;
+
+ /**
+ * Set pretty name again from original name,
+ * typically used when locale changes.
+ */
+ void setPrettyNameFromOriginal();
+
+ attribute nsIMsgFolder parent;
+
+ /// Returns an enumerator containing the messages within the current database.
+ readonly attribute nsIMsgEnumerator messages;
+
+ /**
+ * This method is called by the folder-lookup-service after constructing
+ * a folder to initialize its URI. You would not normally
+ * call this method directly.
+ *
+ * @param uri - The URI of the folder.
+ */
+ void Init(in AUTF8String uri);
+
+ void startFolderLoading();
+ void endFolderLoading();
+
+ void folderNamesReady(out boolean aReady);
+
+ /* get new headers for db */
+ void updateFolder(in nsIMsgWindow aWindow);
+
+ /**
+ * URL for this folder
+ */
+ readonly attribute AUTF8String folderURL;
+
+ /**
+ * should probably move to the server
+ */
+ readonly attribute boolean showDeletedMessages;
+
+ /**
+ * this folder's parent server
+ */
+ readonly attribute nsIMsgIncomingServer server;
+
+ /**
+ * is this folder the "phantom" server folder?
+ */
+ readonly attribute boolean isServer;
+ readonly attribute boolean canSubscribe;
+ readonly attribute boolean canFileMessages;
+ readonly attribute boolean noSelect; // this is an imap no select folder
+ readonly attribute boolean imapShared; // this is an imap shared folder
+ readonly attribute boolean canDeleteMessages; // can't delete from imap read-only
+
+ /**
+ * does this folder allow subfolders?
+ * for example, newsgroups cannot have subfolders, and the INBOX
+ * on some IMAP servers cannot have subfolders
+ */
+ readonly attribute boolean canCreateSubfolders;
+
+ /**
+ * can you change the name of this folder?
+ * for example, newsgroups
+ * and some special folders can't be renamed
+ */
+ readonly attribute boolean canRename;
+
+ readonly attribute boolean canCompact;
+
+ /**
+ * the phantom server folder
+ */
+ readonly attribute nsIMsgFolder rootFolder;
+
+ /**
+ * Get the server's list of filters. (Or in the case of news, the
+ * filter list for this newsgroup)
+ * This list SHOULD be used for all incoming messages.
+ *
+ * Since the returned nsIMsgFilterList is mutable, it is not necessary to call
+ * setFilterList after the filters have been changed.
+ *
+ * @param aMsgWindow @ref msgwindow "The standard message window"
+ * @return The list of filters
+ */
+ nsIMsgFilterList getFilterList(in nsIMsgWindow msgWindow);
+
+ /**
+ * Set the server's list of filters.
+ *
+ * Note that this does not persist the filter list. To change the contents
+ * of the existing filters, use getFilterList and mutate the values as
+ * appropriate.
+ *
+ * @param aFilterList The new list of filters.
+ */
+ void setFilterList(in nsIMsgFilterList filterList);
+
+ /**
+ * Get user editable filter list. This does not have to be the same as
+ * the filterlist above, typically depending on the users preferences.
+ * The filters in this list are not processed, but only to be edited by
+ * the user.
+ * @see getFilterList
+ *
+ * @param aMsgWindow @ref msgwindow "The standard message window"
+ * @return The list of filters
+ */
+ nsIMsgFilterList getEditableFilterList(in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Set user editable filter list.
+ * This does not persist the filterlist, @see setFilterList
+ * @see getEditableFilterList
+ * @see setFilterList
+ *
+ * @param aFilterList The new list of filters.
+ */
+ void setEditableFilterList(in nsIMsgFilterList aFilterList);
+
+ /**
+ * Force close the mail database associated with this folder.
+ */
+ void ForceDBClosed();
+ /**
+ * Close and backup a folder database prior to reparsing
+ *
+ * @param newName New name of the corresponding message folder.
+ * Used in rename to set the file name to match the renamed
+ * folder. Set to empty to use the existing folder name.
+ */
+ void closeAndBackupFolderDB(in ACString newName);
+
+ /**
+ * Delete the backing store of the folder, but not the folder itself.
+ */
+ [noscript]
+ void deleteStorage();
+
+ /**
+ * Delete this folder and its children, if any.
+ * Note: this may mean moving it to trash and/or requesting confirmation
+ * from the user, depending on implementation.
+ * So the deletion may not take place immediately (or at all!)
+ *
+ * @param msgWindow msgWindow to display status feedback in
+ */
+ void deleteSelf(in nsIMsgWindow msgWindow);
+
+ /**
+ * Delete the given subfolder of this folder.
+ * It does not need to be a direct child.
+ *
+ * @param folder a child subfolder to delete
+ * @param deleteStorage whether to also delete the folder storage on disk
+ */
+ void propagateDelete(in nsIMsgFolder folder, in boolean deleteStorage);
+
+ /**
+ * Delete the folder and all of its subfolders.
+ *
+ * @param deleteStorage whether to also delete the folder storage on disk
+ */
+ [noscript]
+ void recursiveDelete(in boolean deleteStorage);
+
+ /**
+ * Create a subfolder of the current folder with the passed in name.
+ * For IMAP, this will be an async operation and the folder won't exist
+ * until it is created on the server.
+ *
+ * @param folderName name of the folder to create.
+ * @param msgWindow msgWindow to display status feedback in.
+ *
+ * @exception NS_MSG_FOLDER_EXISTS
+ */
+ void createSubfolder(in AString folderName, in nsIMsgWindow msgWindow);
+
+ /**
+ * Adds the subfolder with the passed name to the folder hierarchy.
+ * This is used internally during folder discovery; It shouldn't be
+ * used to create folders since it won't create storage for the folder,
+ * especially for imap. Unless you know exactly what you're doing, you
+ * should be using createSubfolder + getChildNamed or createLocalSubfolder.
+ *
+ * @param aFolderName Name of the folder to add.
+ * @returns The folder added.
+ */
+ nsIMsgFolder addSubfolder(in AString aFolderName);
+
+ /* this method ensures the storage for the folder exists.
+ For local folders, it creates the berkeley mailbox if missing.
+ For imap folders, it subscribes to the folder if it exists,
+ or creates it if it doesn't exist
+ */
+ void createStorageIfMissing(in nsIUrlListener urlListener);
+
+ /**
+ * Compact this folder (Expunge _and_ compact, for IMAP folders).
+ *
+ * @param aListener Notified upon completion, can be null.
+ * OnStartRunningUrl() will not be called.
+ * OnStopRunningUrl() will be called upon completion,
+ * with a null URL.
+ * @param aMsgWindow For progress/status, can be null.
+ */
+ void compact(in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow);
+ /**
+ * Compact all folders in the account corresponding to this folder.
+ *
+ * @param aListener Notified upon completion, can be null.
+ * OnStartRunningUrl() will not be called.
+ * OnStopRunningUrl() will be called upon completion,
+ * with a null URL.
+ * @param aMsgWindow For progress/status, can be null.
+ */
+ void compactAll(in nsIUrlListener aListener, in nsIMsgWindow aMsgWindow);
+
+ void emptyTrash(in nsIUrlListener aListener);
+
+ /**
+ * change the name of the folder
+ *
+ * @param name the new name of the folder
+ */
+ void rename(in AString name, in nsIMsgWindow msgWindow);
+ void renameSubFolders( in nsIMsgWindow msgWindow, in nsIMsgFolder oldFolder);
+
+ AString generateUniqueSubfolderName(in AString prefix,
+ in nsIMsgFolder otherFolder);
+
+ void updateSummaryTotals(in boolean force);
+ void summaryChanged();
+ /**
+ * get the total number of unread messages in this folder,
+ * or in all subfolders
+ *
+ * @param deep if true, descends into all subfolders and gets a grand total
+ */
+ long getNumUnread(in boolean deep);
+
+ /**
+ * get the total number of messages in this folder,
+ * or in all subfolders
+ *
+ * @param deep if true, descends into all subfolders and gets a grand total
+ */
+ long getTotalMessages(in boolean deep);
+
+ /**
+ * These functions are used for tricking the front end into thinking that we
+ * have more messages than are really in the DB. This is usually after an
+ * IMAP message copy where we don't want to do an expensive select until the
+ * user actually opens that folder. These functions are called when
+ * MSG_Master::GetFolderLineById is populating a MSG_FolderLine struct used
+ * by the FE.
+ */
+ readonly attribute long numPendingUnread;
+ readonly attribute long numPendingTotalMessages;
+ void changeNumPendingUnread(in long delta);
+ void changeNumPendingTotalMessages(in long delta);
+
+ /**
+ * does this folder have new messages
+ *
+ */
+ attribute boolean hasNewMessages;
+
+ /**
+ * Indicates whether this folder or any of its subfolders have new messages.
+ */
+ readonly attribute boolean hasFolderOrSubfolderNewMessages;
+
+ /**
+ * return the first new message in the folder
+ *
+ */
+ readonly attribute nsIMsgDBHdr firstNewMessage;
+
+ /**
+ * clear new status flag of all of the new messages
+ */
+ void clearNewMessages();
+
+ readonly attribute long long expungedBytes;
+
+ /**
+ * Can this folder be deleted?
+ * For example, special folders and isServer folders cannot be deleted.
+ */
+ readonly attribute boolean deletable;
+
+ /**
+ * should we be displaying recipients instead of the sender?
+ * for example, in the Sent folder, recipients are more relevant
+ * than the sender
+ */
+ readonly attribute boolean displayRecipients;
+
+ /**
+ * used to determine if it will take a long time to download all
+ * the headers in this folder - so that we can do folder notifications
+ * synchronously instead of asynchronously
+ */
+ readonly attribute boolean manyHeadersToDownload;
+
+ readonly attribute ACString relativePathName;
+
+ /**
+ * size of this folder on disk (not including .msf file)
+ * for imap, it's the sum of the size of the messages
+ */
+ attribute long long sizeOnDisk;
+
+ readonly attribute ACString username;
+ readonly attribute ACString hostname;
+
+ /**
+ * Sets a flag on the folder. The known flags are defined in
+ * nsMsgFolderFlags.h.
+ *
+ * @param flag The flag to set on the folder.
+ */
+ void setFlag(in unsigned long flag);
+
+ /**
+ * Clears a flag on the folder. The known flags are defined in
+ * nsMsgFolderFlags.h.
+ *
+ * @param flag The flag to clear on the folder.
+ */
+ void clearFlag(in unsigned long flag);
+
+ /**
+ * Determines if a flag is set on the folder or not. The known flags are
+ * defined in nsMsgFolderFlags.h.
+ *
+ * @param flag The flag to check on the folder.
+ * @return True if the flag exists.
+ */
+ boolean getFlag(in unsigned long flag);
+
+ /**
+ * Toggles a flag on the folder. The known flags are defined in
+ * nsMsgFolderFlags.h.
+ *
+ * @param flag The flag to toggle
+ */
+ void toggleFlag(in unsigned long flag);
+
+ /**
+ * Called to notify the database and/or listeners of a change of flag. The
+ * known flags are defined in nsMsgFolderFlags.h
+ *
+ * @note This doesn't need to be called for normal flag changes via
+ * the *Flag functions on this interface.
+ *
+ * @param flag The flag that was changed.
+ */
+ void onFlagChange(in unsigned long flag);
+
+ /**
+ * Direct access to the set/get all the flags at once.
+ */
+ attribute unsigned long flags;
+
+ /**
+ * Gets the first folder that has the specified flags set.
+ *
+ * @param flags The flag(s) to check for.
+ * @return The folder or the first available child folder that has
+ * the specified flags set, or null if there are none.
+ */
+ nsIMsgFolder getFolderWithFlags(in unsigned long flags);
+
+ /**
+ * Gets the folders that have the specified flag set.
+ *
+ * @param flags The flag(s) to check for.
+ * @return An array of folders that have the specified flags set.
+ * The array may have zero elements.
+ */
+ Array<nsIMsgFolder> getFoldersWithFlags(in unsigned long flags);
+
+ /**
+ * Check if this folder (or one of its ancestors) is special.
+ *
+ * @param flags The "special" flags to check.
+ * @param checkAncestors Should ancestors be checked too.
+ */
+ boolean isSpecialFolder(in unsigned long flags,
+ [optional] in boolean checkAncestors);
+
+ AUTF8String getUriForMsg(in nsIMsgDBHdr msgHdr);
+
+ /**
+ * Deletes the messages from the folder.
+ *
+ * @param messages The array of nsIMsgDBHdr objects to be deleted.
+ * @param msgWindow The standard message window object, for alerts et al.
+ * @param deleteStorage Whether or not the message should be truly deleted, as
+ opposed to moving to trash.
+ * @param isMove Whether or not this is a deletion for moving messages.
+ * @param allowUndo Whether this action should be undoable.
+ */
+ void deleteMessages(in Array<nsIMsgDBHdr> messages,
+ in nsIMsgWindow msgWindow,
+ in boolean deleteStorage, in boolean isMove,
+ in nsIMsgCopyServiceListener listener, in boolean allowUndo);
+
+ void copyMessages(in nsIMsgFolder srcFolder, in Array<nsIMsgDBHdr> messages,
+ in boolean isMove, in nsIMsgWindow msgWindow,
+ in nsIMsgCopyServiceListener listener, in boolean isFolder,
+ in boolean allowUndo);
+
+ void copyFolder(in nsIMsgFolder srcFolder, in boolean isMoveFolder,
+ in nsIMsgWindow msgWindow, in nsIMsgCopyServiceListener listener);
+
+ void copyFileMessage(in nsIFile file, in nsIMsgDBHdr msgToReplace,
+ in boolean isDraft, in unsigned long newMsgFlags,
+ in ACString aKeywords,
+ in nsIMsgWindow msgWindow,
+ in nsIMsgCopyServiceListener listener);
+
+ void acquireSemaphore(in nsISupports semHolder);
+ void releaseSemaphore(in nsISupports semHolder);
+ boolean testSemaphore(in nsISupports semHolder);
+ readonly attribute boolean locked;
+
+ void getNewMessages(in nsIMsgWindow aWindow, in nsIUrlListener aListener);
+
+ /**
+ * Write out summary data for this folder to the given folder cache.
+ */
+ void writeToFolderCache(in nsIMsgFolderCache folderCache, in boolean deep);
+
+ attribute unsigned long biffState;
+
+ /**
+ * The number of new messages since this folder's last biff.
+ *
+ * @param deep if true, descends into all subfolders and gets a grand total
+ */
+
+ long getNumNewMessages(in boolean deep);
+
+ void setNumNewMessages(in long numNewMessages);
+
+ /**
+ * are we running a url as a result of the user clicking get msg?
+ */
+ attribute boolean gettingNewMessages;
+
+ /**
+ * local path of this folder
+ */
+ attribute nsIFile filePath;
+
+ /// an nsIFile corresponding to the .msf file.
+ readonly attribute nsIFile summaryFile;
+
+ readonly attribute AUTF8String baseMessageURI;
+ AUTF8String generateMessageURI(in nsMsgKey msgKey);
+
+ const nsMsgDispositionState nsMsgDispositionState_None = -1;
+ const nsMsgDispositionState nsMsgDispositionState_Replied = 0;
+ const nsMsgDispositionState nsMsgDispositionState_Forwarded = 1;
+ const nsMsgDispositionState nsMsgDispositionState_Redirected = 2;
+
+ void addMessageDispositionState(in nsIMsgDBHdr aMessage,
+ in nsMsgDispositionState aDispositionFlag);
+
+ void markMessagesRead(in Array<nsIMsgDBHdr> messages, in boolean markRead);
+ void markAllMessagesRead(in nsIMsgWindow aMsgWindow);
+ void markMessagesFlagged(in Array<nsIMsgDBHdr> messages, in boolean markFlagged);
+ void markThreadRead(in nsIMsgThread thread);
+
+ /**
+ * Gets the message database for the folder.
+ *
+ * Note that if the database is out of date, the implementation MAY choose to
+ * throw an error. For a handle to the database which MAY NOT throw an error,
+ * one can use getDBFolderInfoAndDB.
+ *
+ * The attribute can also be set to another database or to null to force the
+ * folder to reopen the same database when it is needed again.
+ *
+ * @exception NS_MSG_ERROR_FOLDER_SUMMARY_MISSING If the database does not
+ * exist.
+ * @exception NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE If the database contains
+ * out of date information.
+ * @see nsIMsgFolder::getDBFolderInfoAndDB.
+ */
+ attribute nsIMsgDatabase msgDatabase;
+
+ /// Close the database if not open in folder.
+ void closeDBIfFolderNotOpen(in boolean forceClosed);
+
+ /// Does the folder have a local reference to the msgDatabase?
+ readonly attribute boolean databaseOpen;
+
+ /**
+ * Get the backup message database, used in reparsing. This database must
+ * be created first using closeAndBackupFolderDB()
+ *
+ * @return backup message database
+ */
+ nsIMsgDatabase getBackupMsgDatabase();
+ /**
+ * Remove the backup message database file
+ */
+ void removeBackupMsgDatabase();
+ /**
+ * Open the backup message database file
+ */
+ void openBackupMsgDatabase();
+ nsIMsgDatabase getDBFolderInfoAndDB(out nsIDBFolderInfo folderInfo);
+ nsIMsgDBHdr GetMessageHeader(in nsMsgKey msgKey);
+
+ readonly attribute boolean supportsOffline;
+ boolean shouldStoreMsgOffline(in nsMsgKey msgKey);
+ boolean hasMsgOffline(in nsMsgKey msgKey);
+
+ /**
+ * Get an offline store output stream for the passed message header.
+ *
+ * @param aHdr hdr of message to get outputstream for
+ * @returns An output stream to write to.
+ */
+ nsIOutputStream getOfflineStoreOutputStream(in nsIMsgDBHdr aHdr);
+
+ /**
+ * !!! DEPRECATED (Bug 1733849) !!!
+ * Use getLocalMsgStream() instead.
+ *
+ * Get an input stream for the passed message header. The stream will
+ * be positioned at the start of the message.
+ *
+ * @param aHdr hdr of message to get the input stream for.
+ * @returns an input stream to read the message from
+ */
+ nsIInputStream getMsgInputStream(in nsIMsgDBHdr aHdr);
+
+ /**
+ * Returns an input stream for reading a locally-stored message.
+ * (That means any message in a local folder, or a message marked as
+ * Offline on a non-local folder).
+ * The stream contains the single message.
+ * The returned stream should _not_ be considered seekable.
+ *
+ * @param hdr The message to get the input stream for.
+ * @returns Input stream to read the message from.
+ */
+ nsIInputStream getLocalMsgStream(in nsIMsgDBHdr aHdr);
+
+ void downloadMessagesForOffline(in Array<nsIMsgDBHdr> messages,
+ in nsIMsgWindow window);
+ nsIMsgFolder getChildWithURI(in AUTF8String uri, in boolean deep,
+ in boolean caseInsensitive);
+ void downloadAllForOffline(in nsIUrlListener listener, in nsIMsgWindow window);
+ /**
+ * Turn notifications on/off for various notification types. Currently only
+ * supporting allMessageCountNotifications which refers to both total and
+ * unread message counts.
+ */
+ const unsigned long allMessageCountNotifications = 0;
+ void enableNotifications(in long notificationType, in boolean enable);
+ boolean isCommandEnabled(in ACString command);
+ boolean matchOrChangeFilterDestination(in nsIMsgFolder folder,
+ in boolean caseInsensitive);
+ boolean confirmFolderDeletionForFilter(in nsIMsgWindow msgWindow);
+ void alertFilterChanged(in nsIMsgWindow msgWindow);
+ void throwAlertMsg(in string msgName, in nsIMsgWindow msgWindow);
+ AString getStringWithFolderNameFromBundle(in string msgName);
+ void notifyCompactCompleted();
+ /**
+ * Calculate ordering of this folder against another.
+ */
+ long compareSortKeys(in nsIMsgFolder msgFolder);
+
+ attribute nsIMsgRetentionSettings retentionSettings;
+ attribute nsIMsgDownloadSettings downloadSettings;
+ boolean callFilterPlugins(in nsIMsgWindow aMsgWindow);
+ /**
+ * used for order in the folder pane, folder pickers, etc.
+ */
+ attribute long sortOrder;
+
+ attribute nsIDBFolderInfo dBTransferInfo;
+
+ /**
+ * Set a string property on the folder.
+ */
+ void setStringProperty(in string propertyName, in AUTF8String propertyValue);
+
+ /**
+ * Returns the value of a string property.
+ * Throws an error if property does not exist.
+ */
+ AUTF8String getStringProperty(in string propertyName);
+
+ /* does not persist across sessions */
+ attribute nsMsgKey lastMessageLoaded;
+
+ /**
+ * Returns an array containing nsIMsgFolder items that are
+ * subfolders of the instance this is called on.
+ */
+ readonly attribute Array<nsIMsgFolder> subFolders;
+
+ /**
+ * Returns true if this folder has sub folders.
+ */
+ readonly attribute boolean hasSubFolders;
+
+ /**
+ * Returns the number of sub folders that this folder has.
+ */
+ readonly attribute unsigned long numSubFolders;
+
+ /**
+ * Determines if this folder is an ancestor of the supplied folder.
+ *
+ * @param folder The folder that may or may not be a descendent of this
+ * folder.
+ */
+ boolean isAncestorOf(in nsIMsgFolder folder);
+
+ /**
+ * Looks in immediate children of this folder for the given name.
+ *
+ * @param name the name of the target subfolder
+ */
+ boolean containsChildNamed(in AString name);
+
+ /**
+ * Return the child folder which the specified name.
+ *
+ * @param aName The name of the child folder to find
+ * @return The child folder
+ * @exception NS_ERROR_FAILURE Thrown if the folder with aName does not exist
+ */
+ nsIMsgFolder getChildNamed(in AString aName);
+
+ /**
+ * Finds the sub folder with the specified name.
+ *
+ * @param escapedSubFolderName The name of the sub folder to find.
+ * @note Even if the folder doesn't currently exist,
+ * a nsIMsgFolder may be returned.
+ */
+ nsIMsgFolder findSubFolder(in ACString escapedSubFolderName);
+
+ void AddFolderListener(in nsIFolderListener listener);
+ void RemoveFolderListener(in nsIFolderListener listener);
+
+ // These notification functions invoke the appropriate nsIFolderListener
+ // method on all the listeners attached to this folder, and _also_
+ // all the nsIFolderListeners registered in the MailSession.
+ // See nsIMsgMailSession AddFolderListener()/RemoveFolderListener().
+ void NotifyPropertyChanged(in ACString property,
+ in ACString oldValue,
+ in ACString newValue);
+ void NotifyIntPropertyChanged(in ACString property,
+ in long long oldValue,
+ in long long newValue);
+ void NotifyBoolPropertyChanged(in ACString property,
+ in boolean oldValue,
+ in boolean newValue);
+ void NotifyPropertyFlagChanged(in nsIMsgDBHdr item,
+ in ACString property,
+ in unsigned long oldValue,
+ in unsigned long newValue);
+ void NotifyUnicharPropertyChanged(in ACString property,
+ in AString oldValue,
+ in AString newValue);
+
+ void notifyMessageAdded(in nsIMsgDBHdr msg);
+ void notifyMessageRemoved(in nsIMsgDBHdr msg);
+ void notifyFolderAdded(in nsIMsgFolder child);
+ void notifyFolderRemoved(in nsIMsgFolder child);
+
+ void NotifyFolderEvent(in ACString event);
+
+ // Gets all descendants, not just first level children.
+ readonly attribute Array<nsIMsgFolder> descendants;
+ void Shutdown(in boolean shutdownChildren);
+
+ void copyDataToOutputStreamForAppend(in nsIInputStream aIStream,
+ in long aLength, in nsIOutputStream outputStream);
+ void copyDataDone();
+ void setJunkScoreForMessages(in Array<nsIMsgDBHdr> aMessages, in ACString aJunkScore);
+ void applyRetentionSettings();
+
+ /**
+ * Get the beginning of the message bodies for the passed in keys and store
+ * them in the msg hdr property "preview". This is intended for
+ * new mail alerts, title tips on folders with new messages, and perhaps
+ * titletips/message preview in the thread pane.
+ *
+ * @param aKeysToFetch keys of msgs to fetch
+ * @param aUrlListener url listener to notify if we run url to fetch msgs
+ *
+ * @result aAsyncResults if true, we ran a url to fetch one or more of msg bodies
+ *
+ */
+ boolean fetchMsgPreviewText(in Array<nsMsgKey> aKeysToFetch,
+ in nsIUrlListener aUrlListener);
+
+ // used to set/clear tags - we could have a single method to setKeywords which
+ // would figure out the diffs, but these methods might be more convenient.
+ // keywords are space delimited, in the case of multiple keywords
+ void addKeywordsToMessages(in Array<nsIMsgDBHdr> aMessages, in ACString aKeywords);
+ void removeKeywordsFromMessages(in Array<nsIMsgDBHdr> aMessages, in ACString aKeywords);
+ /**
+ * Extract the message text from aStream.
+ *
+ * @param aStream stream to read from
+ * @param aCharset character set to use to interpret the body. If an empty string, then the
+ * charset is retrieved from the headers. msgHdr.charset is recommended in case you have it.
+ * @param aBytesToRead number of bytes to read from the stream. The function will read till the end
+ * of the line, and there will also be some read ahead due to NS_ReadLine
+ * @param aMaxOutputLen desired length of the converted message text. Used to control how many characters
+ * of msg text we want to store.
+ * @param aCompressQuotes Replace quotes and citations with " ... " in the preview text
+ * @param aStripHTMLTags strip HTML tags from the output, if present
+ * @param[out] aContentType the content type of the MIME part that was used to generate the text --
+ * for an HTML part, this will be "text/html" even though aStripHTMLTags might be true
+ */
+ AUTF8String getMsgTextFromStream(in nsIInputStream aStream, in ACString aCharset,
+ in unsigned long aBytesToRead, in unsigned long aMaxOutputLen,
+ in boolean aCompressQuotes, in boolean aStripHTMLTags,
+ out ACString aContentType);
+
+ AString convertMsgSnippetToPlainText(in AString aMessageText);
+
+ // this allows a folder to have a special identity. E.g., you might want to
+ // associate an identity with a particular newsgroup, or for IMAP shared folders in
+ // the other users namespace, you might want to create a delegated identity
+ readonly attribute nsIMsgIdentity customIdentity;
+
+ /**
+ * @{
+ * Processing flags, used to manage message processing.
+ *
+ * @param msgKey message key
+ * @return processing flags
+ */
+ unsigned long getProcessingFlags(in nsMsgKey msgKey);
+
+ /**
+ * @param msgKey message key
+ * @param mask mask to OR into the flags
+ */
+ void orProcessingFlags(in nsMsgKey msgKey, in unsigned long mask);
+
+ /**
+ * @param msgKey message key
+ * @param mask mask to AND into the flags
+ */
+ void andProcessingFlags(in nsMsgKey msgKey, in unsigned long mask);
+ /** @} */
+
+ /**
+ * Gets an inherited string property from the folder.
+ *
+ * If the forcePropertyEmpty boolean is set (see below), return an
+ * empty string.
+ *
+ * If the specified folder has a non-empty value for the property,
+ * return that value. Otherwise, return getInheritedStringProperty
+ * for the folder's parent.
+ *
+ * If a folder is the root folder for a server, then instead of
+ * checking the folder property, check the property of the same name
+ * for the server using nsIMsgIncomingServer.getCharValue(...)
+ *
+ * Note nsIMsgIncomingServer.getCharValue for a server inherits from
+ * the preference mail.server.default.(propertyName) as a global value
+ *
+ * (ex: if propertyName = "IAmAGlobal" and no folder nor server properties
+ * are set, then the inherited property will return the preference value
+ * mail.server.default.IAmAGlobal)
+ *
+ * If the propertyName is undefined, returns an empty, void string.
+ *
+ * @param propertyName The name of the property for the value to retrieve.
+ */
+ ACString getInheritedStringProperty(in string propertyName);
+
+ /**
+ * Set a boolean to force an inherited propertyName to return empty instead
+ * of inheriting from a parent folder, server, or the global
+ *
+ * @param propertyName The name of the property
+ * @param aForcePropertyEmpty true if an empty inherited property should be returned
+ */
+ void setForcePropertyEmpty(in string propertyName, in boolean aForcePropertyEmpty);
+
+ /**
+ * Get a boolean to force an inherited propertyName to return empty instead
+ * of inheriting from a parent folder, server, or the global
+ *
+ * @param propertyName The name of the property
+ *
+ * @return true if an empty inherited property should be returned
+ */
+ boolean getForcePropertyEmpty(in string propertyName);
+
+ /**
+ * Pluggable store for this folder. Currently, this will always be the same
+ * as the pluggable store for the server.
+ */
+ readonly attribute nsIMsgPluggableStore msgStore;
+
+ /**
+ * Protocol type, i.e. "pop3", "imap", "nntp", "none", etc
+ * used to construct URLs for this account type.
+ */
+ readonly attribute ACString incomingServerType;
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderCache.idl b/comm/mailnews/base/public/nsIMsgFolderCache.idl
new file mode 100644
index 0000000000..b5ba40afdc
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderCache.idl
@@ -0,0 +1,48 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIMsgFolderCacheElement;
+
+/**
+ * nsIMsgFolderCache is a store of values which might be slow for the folder
+ * to calculate. For example: the number of unread messages.
+ * The account manager holds the cache, and each folder manipulates its cached
+ * properties via nsIMsgFolderCacheElement.
+ */
+[scriptable, uuid(78C2B6A2-E29F-44de-9543-10DBB51E245C)]
+interface nsIMsgFolderCache : nsISupports
+{
+ /**
+ * Set up the cache, loading/saving to cacheFile.
+ * If a new-style cacheFile isn't found, it looks for an old panacea.dat,
+ * specified by legacyFile and migrates it to the new format.
+ * Neither file has to exist - it'll just start up with an empty cache.
+ * nsMsgFolderCache (the only implementation) will autosave to cacheFile
+ * when changes are made.
+ *
+ * @param cacheFile File to persist the cache data (folderCache.json).
+ * @param legacyFile Old panacea.dat file to check for and migrate, if
+ * cacheFile doesn't exist.
+ */
+ void init(in nsIFile cacheFile, in nsIFile legacyFile);
+
+ /**
+ * Return an nsIMsgFolderCacheElement for a given folder.
+ * Unless createIfMissing is set, a missing entry will cause failure.
+ */
+ nsIMsgFolderCacheElement getCacheElement(in ACString key, in boolean createIfMissing);
+ void removeElement(in ACString key);
+
+ /**
+ * Write immediately to cacheFile if any data has been changed.
+ * Write immediately to cacheFile if any data has been changed.
+ * This happens in the cache dtor anyway, but we use it during shutdown and
+ * in unit testing (so tests don't have to wait for JS garbage collection).
+ */
+ void flush();
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderCacheElement.idl b/comm/mailnews/base/public/nsIMsgFolderCacheElement.idl
new file mode 100644
index 0000000000..dd4b54c4f0
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderCacheElement.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Interface for a folder to get/set its values in the foldercache.
+ */
+[scriptable, uuid(c7392b12-f68a-46b2-af5e-d47350bb17c3)]
+interface nsIMsgFolderCacheElement : nsISupports
+{
+ readonly attribute ACString key;
+ // Notes on the getCached...() functions:
+ // - They will fail if the property doesn't exist. That is, they'll
+ // throw an exception in JS, or return an NS_ERROR_* code in C++.
+ // - null values are returned as empty string (for getCachedString()),
+ // or zero (for the numeric accessors). NOTE: there should be no
+ // way to actually set properties to null, but the
+ // panacea.dat->folderCache.json might have introduced some, so we
+ // need to handle them.
+ // - On the C++ side there is legacy code which calls these functions
+ // without checking the error code and relies on the return value
+ // remaining unchanged if the function fails.
+ AUTF8String getCachedString(in string propertyName);
+ long getCachedInt32(in string propertyName);
+ unsigned long getCachedUInt32(in string propertyName);
+ long long getCachedInt64(in string propertyName);
+
+ void setCachedString(in string propertyName, in AUTF8String propertyValue);
+ void setCachedInt32(in string propertyName, in long propertyValue);
+ void setCachedUInt32(in string propertyName, in unsigned long propertyValue);
+ void setCachedInt64(in string propertyName, in long long propertyValue);
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderCompactor.idl b/comm/mailnews/base/public/nsIMsgFolderCompactor.idl
new file mode 100644
index 0000000000..96df873f32
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderCompactor.idl
@@ -0,0 +1,34 @@
+/* -*- 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgWindow;
+interface nsIUrlListener;
+
+[scriptable, uuid(38c7e876-3083-4aea-8dcd-0ea0ec1753a3)]
+
+/**
+ * Use this for any object that wants to handle compacting folders.
+ * Currently, the folders themselves create this object.
+ */
+
+interface nsIMsgFolderCompactor : nsISupports
+{
+ /**
+ * Compact the passed in array of folders.
+ *
+ * @param folders The folders to compact.
+ * @param listener Notified of completion, can be null.
+ * OnStartRunningUrl() will not be called.
+ * OnStopRunningUrl() will be called upon
+ * completion, with a null URL.
+ * @param window Used for progress/status, can be null.
+ */
+ void compactFolders(in Array<nsIMsgFolder> folders,
+ in nsIUrlListener listener,
+ in nsIMsgWindow window);
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderListener.idl b/comm/mailnews/base/public/nsIMsgFolderListener.idl
new file mode 100644
index 0000000000..34932a3361
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderListener.idl
@@ -0,0 +1,227 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgFolder;
+
+/**
+ * nsIMsgFolderListener defines the callbacks which are invoked by
+ * nsIMsgFolderNotificationService.
+ *
+ * This is similar to nsIFolderListener, but with slightly different semantics,
+ * especially w.r.t. moving messages and folders. Some listeners want to know
+ * about moves, instead of getting an itemAdded and itemRemoved notification.
+ * Folder listeners also only tend to get called if a view is open on the folder,
+ * which is not always the case. I don't want to change nsIFolderListener at this
+ * point since there are lots of extensions that rely on it. Eventually,
+ * these two interfaces should be combined somehow.
+ */
+
+[scriptable, uuid(2f87be72-0565-4e64-a824-0eb9c258f884)]
+interface nsIMsgFolderListener : nsISupports {
+ /**
+ * Notified immediately after a message is added to a folder. This could be a
+ * new incoming message to a local folder, or a new message in an IMAP folder
+ * when it is opened.
+ *
+ * You may want to consider using the msgsClassified notification instead of
+ * this notification if any of the following are true:
+ *
+ * - You only want to be notified about messages after junk classification
+ * has occurred (if it is going to occur for a message). This also goes for
+ * trait classification which is a generic use of the bayesian engine at
+ * the heart of the spam logic.
+ *
+ * - You only want to be notified about messages after all filters have been
+ * run. Although some filters may be run before the msgAdded notification
+ * is generated, filters dependent on junk/trait classification wait until
+ * classification completes.
+ *
+ * @param aMsg The message header that was just added
+ */
+ void msgAdded(in nsIMsgDBHdr aMsg);
+
+ /**
+ * Notification that (new to the client) messages have been through junk and
+ * trait classification. This event will occur for all messages at some point
+ * after their existence is revealed by msgAdded.
+ *
+ * Because junk classification does not run if no messages have ever been
+ * marked as junk by the user, it is possible to receive this message without
+ * any classification having actually been performed. We still generate the
+ * notification in this case so that code is reliably notified about the
+ * existence of the new message headers.
+ *
+ * @param aMsgs The message headers that have been classified or were
+ * intentionally not classified.
+ * @param aJunkProcessed Were the messages processed for junk classification?
+ * @param aTraitProcessed Were the messages processed for trait
+ * classification?
+ */
+ void msgsClassified(in Array<nsIMsgDBHdr> aMsgs, in boolean aJunkProcessed,
+ in boolean aTraitProcessed);
+
+ /**
+ * msgsJunkStatusChanged indicates that some messages that had already been
+ * reported by msgsClassified have had their junk status changed. This
+ * event will not fire for the initial automatic classification of
+ * messages; msgsClassified will tell you about those messages.
+ * This is not guaranteed to be a comprehensive source of junk
+ * notification events; right now any time an nsMsgDBView marks things as
+ * junk/non-junk a notification is produced.
+ *
+ * @param {nsIMsgDBHdr[]} messages - The affected messages.
+ *
+ */
+ void msgsJunkStatusChanged(in Array<nsIMsgDBHdr> messages);
+
+ /**
+ * Notified after a command to delete a group of messages has been given, but before the
+ * messages have actually been deleted.
+ *
+ * @param aMsgs An array of the message headers about to be deleted
+ *
+ * @note
+ * This notification will not take place if the messages are being deleted from the folder
+ * as the result of a move to another folder. Instead, the msgsMoveCopyCompleted() notification
+ * takes place.
+ *
+ * @note
+ * "Deleting" to a trash folder is actually a move, and is covered by msgsMoveCopyCompleted()
+ *
+ * @note
+ * If the user has selected the IMAP delete model (marking messages as deleted, then purging them
+ * later) for an IMAP account, this notification will not take place on the delete. This will only
+ * take place on the purge.
+ */
+ void msgsDeleted(in Array<nsIMsgDBHdr> aMsgs);
+
+ /**
+ * Notified after a command to move or copy a group of messages completes. In
+ * case of a move, this is before the messages have been deleted from the
+ * source folder.
+ *
+ * @param aMove true if a move, false if a copy
+ * @param aSrcMsgs An array of the message headers in the source folder
+ * @param aDestFolder The folder these messages were moved to.
+ * @param aDestMsgs This provides the list of target message headers.
+ For imap messages, these will be "pseudo" headers, with
+ a made up UID. When we download the "real" header, we
+ will send a msgKeyChanged notification. Currently, if
+ the imap move/copy happens strictly online (essentially,
+ not user-initiated), then aDestMsgs will be null.
+ *
+ * @note
+ * If messages are moved from a server which uses the IMAP delete model,
+ * you'll get aMove = false. That's because the messages are not deleted from
+ * the source database, but instead simply marked deleted.
+ */
+ void msgsMoveCopyCompleted(in boolean aMove,
+ in Array<nsIMsgDBHdr> aSrcMsgs,
+ in nsIMsgFolder aDestFolder,
+ in Array<nsIMsgDBHdr> aDestMsgs);
+
+ /**
+ * Notification sent when the msg key for a header may have changed.
+ * This is used when we create a header for an offline imap move result,
+ * without knowing what the ultimate UID will be. When we download the
+ * headers for the new message, we replace the old "pseudo" header with
+ * a new header that has the correct UID/message key. The uid of the new hdr
+ * may turn out to be the same as aOldKey if we've guessed correctly but
+ * the listener can use this notification to know that it can ignore the
+ * msgAdded notification that's coming for aNewHdr. We do NOT send a
+ * msgsDeleted notification for the pseudo header.
+ *
+ * @param aOldKey The fake UID. The header with this key has been removed
+ * by the time this is called.
+ * @param aNewHdr The header that replaces the header with aOldKey.
+ */
+ void msgKeyChanged(in nsMsgKey aOldKey, in nsIMsgDBHdr aNewHdr);
+
+ /**
+ * msgUnincorporatedMoved: A message received via POP was moved by a
+ * "before junk" rule.
+ *
+ * @param {nsIMsgFolder} srcFolder - Folder the message was moved from.
+ * @param {nsIMsgDBHdr} msg - The message.
+ */
+ void msgUnincorporatedMoved(in nsIMsgFolder srcFolder, in nsIMsgDBHdr msg);
+
+ /**
+ * Notified after a folder has been added.
+ *
+ * @param aFolder The folder that has just been added
+ */
+ void folderAdded(in nsIMsgFolder aFolder);
+
+ /**
+ * Notified after a folder has been deleted and its corresponding file(s) deleted from disk.
+ *
+ * @param aFolder The folder that has just been deleted
+ *
+ * @note
+ * "Deleting" to a trash folder is actually a move, and is covered by folderMoveCopyCompleted()
+ */
+ void folderDeleted(in nsIMsgFolder aFolder);
+
+ /**
+ * Notified after a command to move or copy a folder completes. In case of a move, at this point,
+ * the original folder and its files have already been moved to the new location.
+ *
+ * @param aMove true if a move, false if a copy
+ * @param aSrcFolder The original folder that was moved
+ * @param aDestFolder The parent folder this folder was moved to
+ */
+ void folderMoveCopyCompleted(in boolean aMove,
+ in nsIMsgFolder aSrcFolder,
+ in nsIMsgFolder aDestFolder);
+
+ /**
+ * Notified after a folder is renamed.
+ *
+ * @param aOrigFolder The folder with the old name
+ * @param aNewFolder The folder with the new name
+ */
+ void folderRenamed(in nsIMsgFolder aOrigFolder, in nsIMsgFolder aNewFolder);
+
+
+ /**
+ * Called to indicate nsIMsgFolderCompactor is beginning compaction of the
+ * folder. If the summary file was missing or out-of-date and a parse
+ * is required, this notification will come after the completion of the
+ * parse. The compactor will be holding the folder's semaphore when
+ * this notification is generated. This only happens for local folders
+ * currently.
+ *
+ * @param {nsIMsgFolder} folder - Target folder of the compaction.
+ */
+ void folderCompactStart(in nsIMsgFolder folder);
+
+
+ /**
+ * Called when nsIMsgFolderCompactor has completed compaction of the folder.
+ * At this point, the folder semaphore has been released and the database
+ * has been committed.
+ *
+ * @param {nsIMsgFolder} folder - Target folder of the compaction.
+ */
+ void folderCompactFinish(in nsIMsgFolder folder);
+
+ /**
+ * The user has opted to rebuild the mork msf index for a folder.
+ * Following this notification, the database will be closed, backed up
+ * (so that header properties can be propagated), and then rebuilt from the
+ * source. The rebuild is triggered by a call to updateFolder, so an
+ * nsIFolderListener OnFolderEvent(folder, FolderLoaded atom) notification
+ * will be received if you want to know when this is all completed.
+ * Note: this event is only generated for Thunderbird because the event
+ * currently comes from Thunderbird-specific code.
+ *
+ * @param {nsIMsgFolder} folder - The folder being reindexed.
+ */
+ void folderReindexTriggered(in nsIMsgFolder folder);
+};
diff --git a/comm/mailnews/base/public/nsIMsgFolderNotificationService.idl b/comm/mailnews/base/public/nsIMsgFolderNotificationService.idl
new file mode 100644
index 0000000000..79cf0fb7d2
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgFolderNotificationService.idl
@@ -0,0 +1,119 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgDBHdr;
+interface nsIMsgFolder;
+interface nsIMsgFolderListener;
+
+typedef unsigned long msgFolderListenerFlag;
+
+
+/**
+ * nsIMsgFolderNotificationService provides a central point for sending out
+ * notifications related to folders.
+ * nsIMsgFolderListeners are registered with the service along with flags to
+ * indicate which kinds of notifications are of interest.
+ */
+[scriptable, uuid(e54a592c-2f23-4771-9670-bdb9d4f5dbbd)]
+interface nsIMsgFolderNotificationService : nsISupports {
+ /**
+ * @name Notification flags
+ * These flags determine which notifications will be sent.
+ * @{
+ */
+ /// nsIMsgFolderListener::msgAdded notification
+ const msgFolderListenerFlag msgAdded = 0x1;
+
+ /// nsIMsgFolderListener::msgsDeleted notification
+ const msgFolderListenerFlag msgsDeleted = 0x2;
+
+ /// nsIMsgFolderListener::msgsMoveCopyCompleted notification
+ const msgFolderListenerFlag msgsMoveCopyCompleted = 0x4;
+
+ /// nsIMsgFolderListener::msgsClassified notification
+ const msgFolderListenerFlag msgsClassified = 0x8;
+
+ /// nsIMsgFolderListener::msgsJunkStatusChanged notification
+ const msgFolderListenerFlag msgsJunkStatusChanged = 0x10;
+
+ /// nsIMsgFolderListener::msgUnincorporatedMoved notification
+ const msgFolderListenerFlag msgUnincorporatedMoved = 0x20;
+
+ /// nsIMsgFolderListener::folderAdded notification
+ const msgFolderListenerFlag folderAdded = 0x8000;
+
+ /// nsIMsgFolderListener::folderDeleted notification
+ const msgFolderListenerFlag folderDeleted = 0x1000;
+
+ /// nsIMsgFolderListener::folderMoveCopyCompleted notification
+ const msgFolderListenerFlag folderMoveCopyCompleted = 0x2000;
+
+ /// nsIMsgFolderListener::folderRenamed notification
+ const msgFolderListenerFlag folderRenamed = 0x4000;
+
+ /// nsIMsgFolderListener::folderCompactStart notification
+ const msgFolderListenerFlag folderCompactStart = 0x10000;
+
+ /// nsIMsgFolderListener::folderCompactFinish notification
+ const msgFolderListenerFlag folderCompactFinish = 0x20000;
+
+ /// nsIMsgFolderListener::folderReindexTriggered notification
+ const msgFolderListenerFlag folderReindexTriggered = 0x40000;
+
+ /// nsIMsgFolderListener::msgKeyChanged notification
+ const msgFolderListenerFlag msgKeyChanged = 0x2000000;
+
+ /** @} */
+
+ readonly attribute boolean hasListeners;
+ void addListener(in nsIMsgFolderListener aListener,
+ in msgFolderListenerFlag flags);
+ void removeListener(in nsIMsgFolderListener aListener);
+
+ // message-specific functions
+ // single message for added, array for delete/move/copy
+ void notifyMsgAdded(in nsIMsgDBHdr aMsg);
+ void notifyMsgsClassified(in Array<nsIMsgDBHdr> aMsgs,
+ in boolean aJunkProcessed,
+ in boolean aTraitProcessed);
+ void notifyMsgsJunkStatusChanged(in Array<nsIMsgDBHdr> messages);
+ void notifyMsgsDeleted(in Array<nsIMsgDBHdr> aMsgs);
+ void notifyMsgsMoveCopyCompleted(in boolean aMove,
+ in Array<nsIMsgDBHdr> aSrcMsgs,
+ in nsIMsgFolder aDestFolder,
+ in Array<nsIMsgDBHdr> aDestMsgs);
+
+ /**
+ * Notify listeners that the msg key for a header has changed. Currently,
+ * this is used when we create a header for an offline imap move result,
+ * without knowing what the ultimate UID will be. When we download the
+ * headers for the new message, we replace the old "pseudo" header with
+ * a new header that has the correct UID/message key, by cloning the pseudo
+ * header, which maintains all the existing header attributes.
+ *
+ * @param aOldKey The fake UID. The header with this key has been removed
+ * by the time this is called.
+ * @param aNewHdr The header that replaces the header with aOldKey.
+ */
+ void notifyMsgKeyChanged(in nsMsgKey aOldKey, in nsIMsgDBHdr aNewHdr);
+
+ void notifyMsgUnincorporatedMoved(in nsIMsgFolder srcFolder, in nsIMsgDBHdr msg);
+
+ // folder specific functions
+ // single folders, all the time
+ void notifyFolderAdded(in nsIMsgFolder aFolder);
+ void notifyFolderDeleted(in nsIMsgFolder aFolder);
+ void notifyFolderMoveCopyCompleted(in boolean aMove,
+ in nsIMsgFolder aSrcFolder,
+ in nsIMsgFolder aDestFolder);
+ void notifyFolderRenamed(in nsIMsgFolder aOrigFolder,
+ in nsIMsgFolder aNewFolder);
+
+ void notifyFolderCompactStart(in nsIMsgFolder folder);
+ void notifyFolderCompactFinish(in nsIMsgFolder folder);
+ void notifyFolderReindexTriggered(in nsIMsgFolder folder);
+};
diff --git a/comm/mailnews/base/public/nsIMsgHdr.idl b/comm/mailnews/base/public/nsIMsgHdr.idl
new file mode 100644
index 0000000000..14dc6cddf1
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgHdr.idl
@@ -0,0 +1,109 @@
+/* -*- 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/. */
+
+#include "nsISupports.idl"
+
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgFolder;
+interface nsIUTF8StringEnumerator;
+
+[scriptable, uuid(3c11ddbe-c805-40c5-b9c9-d065fad5d0be)]
+interface nsIMsgDBHdr : nsISupports
+{
+ void setStringProperty(in string propertyName, in AUTF8String propertyValue);
+ AUTF8String getStringProperty(in string propertyName);
+ unsigned long getUint32Property(in string propertyName);
+ void setUint32Property(in string propertyName,
+ in unsigned long propertyVal);
+
+ // accessors, to make our JS cleaner
+ readonly attribute boolean isRead;
+ readonly attribute boolean isFlagged;
+
+ // Special accessor that checks if a message is part of an ignored subthread
+ readonly attribute boolean isKilled;
+
+ // Mark message routines
+ void markRead(in boolean read);
+ void markFlagged(in boolean flagged);
+ void markHasAttachments(in boolean hasAttachments);
+
+ attribute nsMsgPriorityValue priority;
+
+ /* flag handling routines */
+ attribute unsigned long flags;
+ unsigned long orFlags(in unsigned long flags);
+ unsigned long andFlags(in unsigned long flags);
+
+ /* various threading stuff */
+ attribute nsMsgKey threadId;
+ attribute nsMsgKey messageKey;
+ attribute nsMsgKey threadParent;
+
+ /* meta information about the message, learned from reading the message */
+
+ /**
+ * For "Offline" supporting folders (IMAP, NNTP), .messageSize is
+ * the size of the original message on the server.
+ * For Local folders, this is the exact size of the message as written to
+ * the msgStore.
+ * See also Bug 1764857.
+ */
+ attribute unsigned long messageSize;
+ attribute unsigned long lineCount;
+ /**
+ * The offset into the local folder/offline store of the message. This
+ * will be pluggable store-dependent, e.g., for mail dir it should
+ * always be 0.
+ */
+ attribute unsigned long long messageOffset;
+ /**
+ * For "Offline" supporting folders (IMAP, NNTP): .offlineMessageSize is
+ * the exact size of the local copy of the message in the msgStore.
+ * If the message is not flagged Offline, this will be zero or unset.
+ * For Local folders, this is unset or zero.
+ * See also Bug 1764857.
+ */
+ attribute unsigned long offlineMessageSize;
+ /* common headers */
+ attribute PRTime date;
+ readonly attribute unsigned long dateInSeconds;
+ attribute string messageId;
+ attribute string ccList;
+ attribute string bccList;
+ attribute string author;
+ attribute AUTF8String subject;
+ attribute string recipients;
+
+ /* anything below here still has to be fixed */
+ void setReferences(in AUTF8String references);
+ readonly attribute unsigned short numReferences;
+ AUTF8String getStringReference(in long refNum);
+
+ readonly attribute AString mime2DecodedAuthor;
+ readonly attribute AString mime2DecodedSubject;
+ readonly attribute AString mime2DecodedRecipients;
+
+ Array<octet> getAuthorCollationKey();
+ Array<octet> getSubjectCollationKey();
+ Array<octet> getRecipientsCollationKey();
+
+ attribute string charset;
+
+ /**
+ * Returns the effective character set for the message (@ref charset).
+ * For NNTP, if there is no specific set defined for the message,
+ * the character set of the server instead.
+ */
+ readonly attribute ACString effectiveCharset;
+
+ attribute string accountKey;
+ readonly attribute nsIMsgFolder folder;
+
+ /// Array of names of all database properties in the header.
+ readonly attribute Array<AUTF8String> properties;
+};
+/* *******************************************************************************/
diff --git a/comm/mailnews/base/public/nsIMsgIdentity.idl b/comm/mailnews/base/public/nsIMsgIdentity.idl
new file mode 100644
index 0000000000..f02a240694
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgIdentity.idl
@@ -0,0 +1,311 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+#include "nsIFile.idl"
+
+/**
+ * This interface contains all the personal outgoing mail information
+ * for a given person.
+ * Each identity is identified by a key, which is the <id> string in
+ * the identity preferences, such as in mail.identity.<id>.replyTo.
+ */
+[scriptable, uuid(9dede9a0-f6fc-4afc-8fc9-a6af52414b3d)]
+interface nsIMsgIdentity : nsISupports {
+ /**
+ * Internal preferences ID.
+ */
+ attribute ACString key;
+
+ /**
+ * A unique identifier for this identity that can be used for the same
+ * identity synced across multiple profiles. Auto-generated on first use.
+ */
+ attribute AUTF8String UID;
+
+ /**
+ * Label describing this identity. May be empty.
+ */
+ attribute AString label;
+
+ /**
+ * Pretty display name to identify this specific identity. Will return a
+ * composed string like "fullname <email> (label)".
+ */
+ readonly attribute AString identityName;
+
+ /**
+ * User's full name, i.e. John Doe.
+ */
+ attribute AString fullName;
+
+ /**
+ * User's e-mail address, i.e. john@doe.com.
+ */
+ attribute ACString email;
+
+ /**
+ * Do we use multiple e-mail addresses (like Catch-All) with this identity?
+ */
+ attribute boolean catchAll;
+
+ /**
+ * Hint for when to use this identity as catch all. It is a comma separated
+ * list of things to look for delivery in headers when replying to a message
+ * that was not directly addressed to a matching identity.
+ */
+ attribute AUTF8String catchAllHint;
+
+ /**
+ * Formats fullName and email into the proper string to use as sender:
+ * name <email>
+ */
+ readonly attribute AString fullAddress;
+
+ /**
+ * Optional replyTo address, i.e. johnNOSPAM@doe.com.
+ */
+ attribute AUTF8String replyTo;
+
+ /**
+ * Optional organization.
+ */
+ attribute AString organization;
+
+ /**
+ * Should we compose with HTML by default?
+ */
+ attribute boolean composeHtml;
+
+ /**
+ * Should we attach a signature from file?
+ */
+ attribute boolean attachSignature;
+
+ /**
+ * Should we attach a vcard by default?
+ */
+ attribute boolean attachVCard;
+
+ /**
+ * Should we automatically quote the original message?
+ */
+ attribute boolean autoQuote;
+
+ /**
+ * What should our quoting preference be?
+ */
+ attribute long replyOnTop;
+
+ /**
+ * Should our signature be at the end of the quoted text when replying
+ * above it?
+ */
+ attribute boolean sigBottom;
+
+ /**
+ * Include a signature when forwarding a message?
+ */
+ attribute boolean sigOnForward;
+
+ /**
+ * Include a signature when replying to a message?
+ */
+ attribute boolean sigOnReply;
+
+ /**
+ * The current signature file.
+ */
+ attribute nsIFile signature;
+
+ /**
+ * Modification time of the signature file.
+ */
+ attribute long signatureDate;
+
+ /**
+ * Signature text if not read from file; format depends on htmlSigFormat.
+ */
+ attribute AString htmlSigText;
+
+ /**
+ * Does htmlSigText contain HTML? Use plain text if false.
+ */
+ attribute boolean htmlSigFormat;
+
+ /**
+ * Suppress the double-dash signature separator
+ */
+ attribute boolean suppressSigSep;
+
+ /**
+ * The encoded string representing the vcard.
+ */
+ attribute ACString escapedVCard;
+
+ attribute boolean doFcc;
+ /// URI for the fcc (Sent) folder
+ attribute AUTF8String fccFolder;
+ attribute boolean fccReplyFollowsParent;
+
+ /**
+ * @{
+ * these attributes control whether the special folder pickers for
+ * fcc, drafts,archives, and templates are set to pick between servers
+ * (e.g., Sent on accountName) or to pick any folder on any account.
+ * "0" means choose between servers; "1" means use the full folder picker.
+ */
+ attribute ACString fccFolderPickerMode;
+ attribute ACString draftsFolderPickerMode;
+ attribute ACString archivesFolderPickerMode;
+ attribute ACString tmplFolderPickerMode;
+ /** @} */
+
+ // Don't call bccSelf, bccOthers, and bccList directly, they are
+ // only used for migration and backward compatibility. Use doBcc
+ // and doBccList instead.
+ attribute boolean bccSelf;
+ attribute boolean bccOthers;
+ attribute ACString bccList;
+
+ attribute boolean doCc;
+ attribute AUTF8String doCcList;
+
+ attribute boolean doBcc;
+ attribute AUTF8String doBccList;
+ /**
+ * @{
+ * URIs for the special folders (drafts, templates, archive)
+ */
+ attribute AUTF8String draftFolder;
+ attribute AUTF8String archiveFolder;
+ attribute AUTF8String stationeryFolder;
+ /** @} */
+
+ attribute boolean archiveEnabled;
+ /**
+ * @{
+ * This attribute and constants control the granularity of sub-folders of the
+ * Archives folder - either messages go in the single archive folder, or a
+ * yearly archive folder, or in a monthly archive folder with a yearly
+ * parent folder. If the server doesn't support folders that both contain
+ * messages and have sub-folders, we will ignore this setting.
+ */
+ attribute long archiveGranularity;
+ const long singleArchiveFolder = 0;
+ const long perYearArchiveFolders = 1;
+ const long perMonthArchiveFolders = 2;
+ /// Maintain the source folder name when creating Archive subfolders
+ attribute boolean archiveKeepFolderStructure;
+ /** @} */
+
+ attribute boolean showSaveMsgDlg;
+ attribute ACString directoryServer;
+ attribute boolean overrideGlobalPref;
+ /**
+ * If this is false, don't append the user's domain
+ * to an autocomplete address with no matches
+ */
+ attribute boolean autocompleteToMyDomain;
+ /**
+ * valid determines if the UI should use this identity
+ * and the wizard uses this to determine whether or not
+ * to ask the user to complete all the fields
+ */
+ attribute boolean valid;
+
+ /**
+ * this is really dangerous. this destroys all pref values
+ * do not call this unless you know what you're doing!
+ */
+ void clearAllValues();
+
+ /**
+ * the preferred smtp server for this identity.
+ * if this is set, this the smtp server that should be used
+ * for the message send
+ */
+ attribute ACString smtpServerKey;
+
+ /**
+ * default request for return receipt option for this identity
+ * if this is set, the Return Receipt menu item on the compose
+ * window will be checked
+ */
+ readonly attribute boolean requestReturnReceipt;
+ readonly attribute long receiptHeaderType;
+
+ /**
+ * default request for DSN option for this identity
+ * if this is set, the DSN menu item on the compose
+ * window will be checked
+ */
+ readonly attribute boolean requestDSN;
+
+ /**
+ * If true, include supported Autocrypt headers whenever sending a
+ * plain text or OpenPGP message. (Ignored when sending S/MIME.)
+ */
+ attribute boolean sendAutocryptHeaders;
+
+ /**
+ * If true, automatically attach the user's own public OpenPGP key
+ * whenever adding an OpenPGP digital signature.
+ */
+ attribute boolean attachPgpKey;
+
+ /**
+ * If true, encrypt draft messages that are saved to disk or on the
+ * mail server. This requires that a personal OpenPGP key or S/MIME
+ * certificate is configured and valid, matching the selected
+ * encryption technology for the current message.
+ */
+ attribute boolean autoEncryptDrafts;
+
+ /*
+ * If true, when sending an OpenPGP encrypted message, encrypt the
+ * email's subject, too.
+ */
+ attribute boolean protectSubject;
+
+ /*
+ * The default encryption setting for new emails that aren't already
+ * in an encryption context. (When forwarding or replying to an
+ * encrypted message we always automatically turn on encryption.)
+ * 0: disable encryption
+ * 1: optional encryption (not implemented)
+ * 2: require encryption
+ */
+ attribute long encryptionPolicy;
+
+ /*
+ * If true, add a digital signature for messages that aren't using
+ * encryption. (A message that uses encryption will automatically
+ * have signing enabled.)
+ */
+ attribute boolean signMail;
+
+ /* copy the attributes of the identity we pass in */
+ void copy(in nsIMsgIdentity identity);
+
+ /**
+ * these generic getter / setters, useful for extending mailnews
+ * note, these attributes persist across sessions
+ */
+ AString getUnicharAttribute(in string name);
+ void setUnicharAttribute(in string name, in AString value);
+
+ ACString getCharAttribute(in string name);
+ void setCharAttribute(in string name, in ACString value);
+
+ boolean getBoolAttribute(in string name);
+ void setBoolAttribute(in string name, in boolean value);
+
+ long getIntAttribute(in string name);
+ void setIntAttribute(in string name, in long value);
+
+ /* useful for debugging */
+ AString toString();
+};
diff --git a/comm/mailnews/base/public/nsIMsgIncomingServer.idl b/comm/mailnews/base/public/nsIMsgIncomingServer.idl
new file mode 100644
index 0000000000..c1b4c7e16d
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgIncomingServer.idl
@@ -0,0 +1,596 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgFolderCache;
+interface nsIMsgWindow;
+interface nsIMsgProtocolInfo;
+interface nsIMsgFilterList;
+interface nsIMsgRetentionSettings;
+interface nsIMsgDownloadSettings;
+interface nsISpamSettings;
+interface nsIMsgFilterPlugin;
+interface nsIUrlListener;
+interface nsIMsgDBHdr;
+interface nsIFile;
+interface nsIURI;
+interface nsIMsgPluggableStore;
+
+/*
+ * Interface for incoming mail/news host
+ * this is the base interface for all mail server types (imap, pop, nntp, etc)
+ * often you will want to add extra interfaces that give you server-specific
+ * attributes and methods.
+ */
+[scriptable, uuid(aa9a3389-9dac-41f1-9ec5-18287cfaa47c)]
+interface nsIMsgIncomingServer : nsISupports {
+
+ /**
+ * internal pref key - guaranteed to be unique across all servers
+ */
+ attribute ACString key;
+
+ /**
+ * A unique identifier for this server that can be used for the same
+ * server synced across multiple profiles. Auto-generated on first use.
+ */
+ attribute AUTF8String UID;
+
+ /**
+ * pretty name - should be "userid on hostname"
+ * if the pref is not set
+ */
+ attribute AString prettyName;
+
+ /**
+ * helper function to construct the pretty name in a server type
+ * specific way - e.g., mail for foo@test.com, news on news.mozilla.org
+ */
+ readonly attribute AString constructedPrettyName;
+
+ /**
+ * hostname of the server
+ */
+ attribute AUTF8String hostName;
+
+ /* port of the server */
+ attribute long port;
+
+ /**
+ * userid to log into the server
+ */
+ attribute AUTF8String username;
+
+ /**
+ * protocol type, i.e. "pop3", "imap", "nntp", "none", etc
+ * used to construct URLs
+ */
+ attribute ACString type;
+
+ /**
+ * The CLIENTID to use for this server.
+ * @see https://tools.ietf.org/html/draft-yu-imap-client-id-01
+ */
+ attribute ACString clientid;
+
+ /**
+ * Whether the CLIENTID feature above is enabled.
+ */
+ attribute boolean clientidEnabled;
+
+ /**
+ * The proper instance of nsIMsgProtocolInfo corresponding to this server type.
+ */
+ readonly attribute nsIMsgProtocolInfo protocolInfo;
+
+ readonly attribute AString accountManagerChrome;
+
+ /**
+ * The schema for the local mail store, such as "mailbox", "imap", or "news"
+ * used to construct URIs. The contractID for the nsIMsgMessageService
+ * implementation that will manage access to messages associated with this
+ * server is constructed using this type.
+ */
+ readonly attribute ACString localStoreType;
+
+ /**
+ * The schema for the nsIMsgDatabase implementation, such as "mailbox" or
+ * "imap", that will be used to construct the database instance used by
+ * message folders associated with this server.
+ */
+ readonly attribute ACString localDatabaseType;
+
+ // Perform specific tasks (reset flags, remove files, etc) for account user/server name changes.
+ void onUserOrHostNameChanged(in AUTF8String oldName, in AUTF8String newName,
+ in bool hostnameChanged);
+
+ /// cleartext utf16 version of the password
+ attribute AString password;
+
+ /**
+ * Attempts to get the password first from the password manager, if that
+ * fails it will attempt to get it from the user if aMsgWindow is supplied.
+ *
+ * @param aPromptString The text of the prompt if the user is prompted for
+ * password.
+ * @param aPromptTitle The title of the prompt if the user is prompted.
+ * @return The obtained password. Could be an empty password.
+ *
+ * @exception NS_ERROR_FAILURE The password could not be obtained.
+ *
+ * @note NS_MSG_PASSWORD_PROMPT_CANCELLED is a success code that is returned
+ * if the prompt was presented to the user but the user cancelled the
+ * prompt.
+ */
+ AString getPasswordWithUI(in AString aPromptString, in AString aPromptTitle);
+
+ /* forget the password in memory and in single signon database */
+ void forgetPassword();
+
+ /**
+ * Forget the password in memory which is cached for the session.
+ *
+ * @param modifyLogin Only relevant for nsImapIncomingServer override. When
+ * true and authentication method is oauth2, the password
+ * and user authenticated flag are not cleared.
+ */
+ void forgetSessionPassword(in boolean modifyLogin);
+
+ /* should we download whole messages when biff goes off? */
+ attribute boolean downloadOnBiff;
+
+ /* should we biff the server? */
+ attribute boolean doBiff;
+
+ /* how often to biff */
+ attribute long biffMinutes;
+
+ /* current biff state */
+ attribute unsigned long biffState;
+
+ /* are we running a url as a result of biff going off? (different from user clicking get msg) */
+ attribute boolean performingBiff;
+
+ /* the on-disk path to message storage for this server */
+ attribute nsIFile localPath;
+
+ /// message store to use for the folders under this server.
+ readonly attribute nsIMsgPluggableStore msgStore;
+
+ /* the RDF URI for the root mail folder */
+ readonly attribute AUTF8String serverURI;
+
+ /* the root folder for this server, even if server is deferred */
+ attribute nsIMsgFolder rootFolder;
+
+ /* root folder for this account
+ - if account is deferred, root folder of deferred-to account */
+ readonly attribute nsIMsgFolder rootMsgFolder;
+
+ /* are we already getting new Messages on the current server..
+ This is used to help us prevent multiple get new msg commands from
+ going off at the same time. */
+ attribute boolean serverBusy;
+
+ /**
+ * Is the server using a secure channel (SSL or STARTTLS).
+ */
+ readonly attribute boolean isSecure;
+
+ /**
+ * Authentication mechanism.
+ *
+ * @see nsMsgAuthMethod (in MailNewsTypes2.idl)
+ * Same as "mail.server...authMethod" pref
+ */
+ attribute nsMsgAuthMethodValue authMethod;
+
+ /**
+ * Whether to SSL or STARTTLS or not
+ *
+ * @see nsMsgSocketType (in MailNewsTypes2.idl)
+ * Same as "mail.server...socketType" pref
+ */
+ attribute nsMsgSocketTypeValue socketType;
+
+ /* empty trash on exit */
+ attribute boolean emptyTrashOnExit;
+
+ /**
+ * Get the server's list of filters.
+ *
+ * This SHOULD be the same filter list as the root folder's, if the server
+ * supports per-folder filters. Furthermore, this list SHOULD be used for all
+ * incoming messages.
+ *
+ * Since the returned nsIMsgFilterList is mutable, it is not necessary to call
+ * setFilterList after the filters have been changed.
+ *
+ * @param aMsgWindow @ref msgwindow "The standard message window"
+ * @return The list of filters.
+ */
+ nsIMsgFilterList getFilterList(in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Set the server's list of filters.
+ *
+ * Note that this does not persist the filter list. To change the contents
+ * of the existing filters, use getFilterList and mutate the values as
+ * appropriate.
+ *
+ * @param aFilterList The new list of filters.
+ */
+ void setFilterList(in nsIMsgFilterList aFilterList);
+
+ /**
+ * Get user editable filter list. This does not have to be the same as
+ * the filterlist above, typically depending on the users preferences.
+ * The filters in this list are not processed, but only to be edited by
+ * the user.
+ * @see getFilterList
+ *
+ * @param aMsgWindow @ref msgwindow "The standard message window"
+ * @return The list of filters.
+ */
+ nsIMsgFilterList getEditableFilterList(in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Set user editable filter list.
+ * This does not persist the filterlist, @see setFilterList
+ * @see getEditableFilterList
+ * @see setFilterList
+ *
+ * @param aFilterList The new list of filters.
+ */
+ void setEditableFilterList(in nsIMsgFilterList aFilterList);
+
+ /* we use this to set the default local path. we use this when migrating prefs */
+ void setDefaultLocalPath(in nsIFile aDefaultLocalPath);
+
+ /**
+ * Verify that we can logon
+ *
+ * @param aUrlListener - gets called back with success or failure.
+ * @param aMsgWindow nsIMsgWindow to use for notification callbacks.
+ * @return - the url that we run.
+ */
+ nsIURI verifyLogon(in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ /* do a biff */
+ void performBiff(in nsIMsgWindow aMsgWindow);
+
+ /* get new messages */
+ void getNewMessages(in nsIMsgFolder aFolder, in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener);
+ /* this checks if a server needs a password to do biff */
+ readonly attribute boolean serverRequiresPasswordForBiff;
+
+ /* this gets called when the server is expanded in the folder pane */
+ void performExpand(in nsIMsgWindow aMsgWindow);
+
+ /* Write out all known folder data to folderCache */
+ void writeToFolderCache(in nsIMsgFolderCache folderCache);
+
+ /* close any server connections */
+ void closeCachedConnections();
+
+ /* ... */
+ void shutdown();
+
+ /**
+ * Get or set the value as determined by the preference tree.
+ *
+ * These methods MUST NOT fail if the preference is not set, and therefore
+ * they MUST have a default value. This default value is provided in practice
+ * by use of a default preference tree. The standard format for the pref
+ * branches are <tt>mail.server.<i>key</i>.</tt> for per-server preferences,
+ * such that the preference is <tt>mail.server.<i>key</i>.<i>attr</i></tt>.
+ *
+ * The attributes are passed in as strings for ease of access by the C++
+ * consumers of this method.
+ *
+ * @param attr The value for which the preference should be accessed.
+ * @param value The value of the preference to set.
+ * @return The value of the preference.
+ * @{
+ */
+ boolean getBoolValue(in string attr);
+ void setBoolValue(in string attr, in boolean value);
+
+ ACString getCharValue(in string attr);
+ void setCharValue(in string attr, in ACString value);
+
+ AString getUnicharValue(in string attr);
+ void setUnicharValue(in string attr, in AString value);
+
+ long getIntValue(in string attr);
+ void setIntValue(in string attr, in long value);
+ /** @} */
+
+ /**
+ * Get or set the value as determined by the preference tree.
+ *
+ * These methods MUST NOT fail if the preference is not set, and therefore
+ * they MUST have a default value. This default value is provided in practice
+ * by use of a default preference tree. The standard format for the pref
+ * branches are <tt>mail.server.<i>key</i>.</tt> for per-server preferences,
+ * such that the preference is <tt>mail.server.<i>key</i>.<i>attr</i></tt>.
+ *
+ * The attributes are passed in as strings for ease of access by the C++
+ * consumers of this method.
+ *
+ * There are two preference names on here for legacy reasons, where the first
+ * is the name which will be using a (preferred) relative preference and the
+ * second a deprecated absolute preference. Implementations that do not have
+ * to worry about supporting legacy preferences can safely ignore this second
+ * parameter. Callers must still provide a valid value, though.
+ *
+ * @param relpref The name of the relative file preference.
+ * @param absref The name of the absolute file preference.
+ * @param aValue The value of the preference to set.
+ * @return The value of the preference.
+ * @{
+ */
+ nsIFile getFileValue(in string relpref, in string abspref);
+ void setFileValue(in string relpref, in string abspref, in nsIFile aValue);
+ /** @} */
+
+ /**
+ * this is really dangerous. this destroys all pref values
+ * do not call this unless you know what you're doing!
+ */
+ void clearAllValues();
+
+ /**
+ * This is also very dangerous. This will low-level remove the files
+ * associated with this server on disk. It does not notify any listeners.
+ */
+ void removeFiles();
+
+ attribute boolean valid;
+
+ AString toString();
+
+ /* used for comparing nsIMsgIncomingServers */
+ boolean equals(in nsIMsgIncomingServer server);
+
+ /* Get Messages at startup */
+ readonly attribute boolean downloadMessagesAtStartup;
+
+ /* check to this if the server supports filters */
+ attribute boolean canHaveFilters;
+
+ /**
+ * can this server be removed from the account manager? for
+ * instance, local mail is not removable, but an imported folder is
+ */
+ attribute boolean canDelete;
+
+ attribute boolean loginAtStartUp;
+
+ attribute boolean limitOfflineMessageSize;
+ attribute long maxMessageSize;
+
+ attribute nsIMsgRetentionSettings retentionSettings;
+
+ /* check if this server can be a default server */
+ readonly attribute boolean canBeDefaultServer;
+
+ /* check if this server allows search operations */
+ readonly attribute boolean canSearchMessages;
+
+ /* display startup page once per account per session */
+ attribute boolean displayStartupPage;
+ attribute nsIMsgDownloadSettings downloadSettings;
+
+ /*
+ * Offline support level. Support level can vary based on abilities
+ * and features each server can offer wrt to offline service.
+ * Here is the legend to determine the each support level details
+ *
+ * supportLevel == 0 --> no offline support (default)
+ * supportLevel == 10 --> regular offline feature support
+ * supportLevel == 20 --> extended offline feature support
+ *
+ * Each server can initialize itself to the support level if needed
+ * to override the default choice i.e., no offline support.
+ *
+ * POP3, None will default to 0.
+ * IMAP level 10 and NEWS with level 20.
+ *
+ */
+ attribute long offlineSupportLevel;
+
+ /* create pretty name for migrated accounts */
+ AString generatePrettyNameForMigration();
+
+ /* does this server have disk space settings? */
+ readonly attribute boolean supportsDiskSpace;
+
+ /**
+ * Hide this server/account from the UI - used for smart mailboxes.
+ * The server can be retrieved from the account manager by name using the
+ * various Find methods, but nsIMsgAccountManager's GetAccounts and
+ * GetAllServers methods won't return the server/account.
+ */
+ attribute boolean hidden;
+
+ /**
+ * If the server supports Fcc/Sent/etc, default prefs can point to
+ * the server. Otherwise, copies and folders prefs should point to
+ * Local Folders.
+ *
+ * By default this value is set to true via global pref 'allows_specialfolders_usage'
+ * (mailnews.js). For Nntp, the value is overridden to be false.
+ * If ISPs want to modify this value, they should do that in their rdf file
+ * by using this attribute. Please look at mozilla/mailnews/base/ispdata/aol.rdf for
+ * usage example.
+ */
+ attribute boolean defaultCopiesAndFoldersPrefsToServer;
+
+ /* can this server allows sub folder creation */
+ attribute boolean canCreateFoldersOnServer;
+
+ /* can this server allows message filing ? */
+ attribute boolean canFileMessagesOnServer;
+
+ /* can this server allow compacting folders ? */
+ readonly attribute boolean canCompactFoldersOnServer;
+
+ /* can this server allow undo delete ? */
+ readonly attribute boolean canUndoDeleteOnServer;
+
+ /* used for setting up the filter UI */
+ readonly attribute nsMsgSearchScopeValue filterScope;
+
+ /* used for setting up the search UI */
+ readonly attribute nsMsgSearchScopeValue searchScope;
+
+ /**
+ * If the password for the server is available either via authentication
+ * in the current session or from password manager stored entries, return
+ * false. Otherwise, return true. If password is obtained from password
+ * manager, set the password member variable.
+ */
+ readonly attribute boolean passwordPromptRequired;
+
+ /**
+ * for mail, this configures both the MDN filter, and the server-side
+ * spam filter filters, if needed.
+ *
+ * If we have set up to filter return receipts into
+ * our Sent folder, this utility method creates
+ * a filter to do that, and adds it to our filterList
+ * if it doesn't exist. If it does, it will enable it.
+ *
+ * this is not used by news filters (yet).
+ */
+ void configureTemporaryFilters(in nsIMsgFilterList filterList);
+
+ /**
+ * If Sent folder pref is changed we need to clear the temporary
+ * return receipt filter so that the new return receipt filter can
+ * be recreated (by ConfigureTemporaryReturnReceiptsFilter()).
+ */
+ void clearTemporaryReturnReceiptsFilter();
+
+ /**
+ * spam settings
+ */
+ readonly attribute nsISpamSettings spamSettings;
+ readonly attribute nsIMsgFilterPlugin spamFilterPlugin;
+
+ nsIMsgFolder getMsgFolderFromURI(in nsIMsgFolder aFolderResource, in AUTF8String aURI);
+
+ /// Indicates if any other server has deferred storage to this account.
+ readonly attribute boolean isDeferredTo;
+
+ const long keepDups = 0;
+ const long deleteDups = 1;
+ const long moveDupsToTrash = 2;
+ const long markDupsRead = 3;
+
+ attribute long incomingDuplicateAction;
+
+ // check if new hdr is a duplicate of a recently arrived header
+ boolean isNewHdrDuplicate(in nsIMsgDBHdr aNewHdr);
+
+ /**
+ * Set a boolean to force an inherited propertyName to return empty instead
+ * of inheriting from a parent folder, server, or the global
+ *
+ * @param propertyName The name of the property
+ * @param aForcePropertyEmpty true if an empty inherited property should be returned
+ */
+ void setForcePropertyEmpty(in string propertyName, in boolean aForcePropertyEmpty);
+
+ /**
+ * Get a boolean to force an inherited propertyName to return empty instead
+ * of inheriting from a parent folder, server, or the global
+ *
+ * @param propertyName The name of the property
+ *
+ * @return true if an empty inherited property should be returned
+ */
+ boolean getForcePropertyEmpty(in string propertyName);
+
+ /**
+ * Return the order in which this server type should appear in the folder pane.
+ * This sort order is a number between 100000000 and 900000000 so that RDF can
+ * use it as a string.
+ * The current return values are these:
+ * 0 = default account, 100000000 = mail accounts (POP3/IMAP4),
+ * 200000000 = Local Folders, 300000000 = IM accounts,
+ * 400000000 = RSS, 500000000 = News
+ * If a new server type is created a TB UI reviewer must decide its sort order.
+ */
+ readonly attribute long sortOrder;
+};
+
+%{C++
+/*
+ * Following values for offline support have been used by
+ * various files. If you are modifying any of the values
+ * below, please do take care of the following files.
+ * - mozilla/mailnews/base/src/nsMsgAccountManagerDS.cpp
+ * - mozilla/mailnews/base/util/nsMsgIncomingServer.cpp
+ * - mozilla/mailnews/imap/src/nsImapIncomingServer.cpp
+ * - mozilla/mailnews/local/src/nsPop3IncomingServer.cpp
+ * - mozilla/mailnews/news/src/nsNntpIncomingServer.cpp
+ * - mozilla/mailnews/base/content/msgAccountCentral.js
+ * - mozilla/modules/libpref/src/init/mailnews.js
+ * - ns/modules/libpref/src/init/mailnews-ns.js
+ * - ns/mailnews/base/ispdata/aol.rdf
+ * - ns/mailnews/base/ispdata/nswebmail.rdf
+ */
+#define OFFLINE_SUPPORT_LEVEL_NONE 0
+#define OFFLINE_SUPPORT_LEVEL_REGULAR 10
+#define OFFLINE_SUPPORT_LEVEL_EXTENDED 20
+#define OFFLINE_SUPPORT_LEVEL_UNDEFINED -1
+
+// Value when no port setting is found
+#define PORT_NOT_SET -1
+
+/* some useful macros to implement nsIMsgIncomingServer accessors */
+#define NS_IMPL_SERVERPREF_STR(_class, _postfix, _prefname) \
+NS_IMETHODIMP \
+_class::Get##_postfix(nsACString& retval) \
+{ \
+ return GetCharValue(_prefname, retval); \
+} \
+NS_IMETHODIMP \
+_class::Set##_postfix(const nsACString& chvalue) \
+{ \
+ return SetCharValue(_prefname, chvalue); \
+}
+
+#define NS_IMPL_SERVERPREF_BOOL(_class, _postfix, _prefname)\
+NS_IMETHODIMP \
+_class::Get##_postfix(bool *retval) \
+{ \
+ return GetBoolValue(_prefname, retval); \
+} \
+NS_IMETHODIMP \
+_class::Set##_postfix(bool bvalue) \
+{ \
+ return SetBoolValue(_prefname, bvalue); \
+}
+
+#define NS_IMPL_SERVERPREF_INT(_class, _postfix, _prefname)\
+NS_IMETHODIMP \
+_class::Get##_postfix(int32_t *retval) \
+{ \
+ return GetIntValue(_prefname, retval); \
+} \
+NS_IMETHODIMP \
+_class::Set##_postfix(int32_t ivalue) \
+{ \
+ return SetIntValue(_prefname, ivalue); \
+}
+
+%}
diff --git a/comm/mailnews/base/public/nsIMsgMailNewsUrl.idl b/comm/mailnews/base/public/nsIMsgMailNewsUrl.idl
new file mode 100644
index 0000000000..290760c6f0
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgMailNewsUrl.idl
@@ -0,0 +1,211 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+#include "nsIURL.idl"
+
+interface nsIFile;
+interface nsIUrlListener;
+interface nsIMsgStatusFeedback;
+interface nsIMsgIncomingServer;
+interface nsIMsgWindow;
+interface nsILoadGroup;
+interface nsIMsgSearchSession;
+interface nsICacheEntry;
+interface nsIMimeHeaders;
+interface nsIStreamListener;
+interface nsIMsgFolder;
+interface nsIMsgDBHdr;
+interface nsIDocShell;
+interface nsITransportSecurityInfo;
+
+[scriptable, builtinclass, uuid(995455ba-5bb4-4643-8d70-2b877a2e1320)]
+interface nsIMsgMailNewsUrl : nsIURL {
+ [noscript,notxpcom,nostdcall]
+ nsresult setFileNameInternal(in ACString aFileName);
+
+ [noscript,notxpcom,nostdcall]
+ nsresult setSpecInternal(in ACString aSpec);
+
+ [noscript,notxpcom,nostdcall]
+ nsresult setPortInternal(in long aPort);
+
+ [noscript,notxpcom,nostdcall]
+ nsresult setQueryInternal(in ACString aQuery);
+
+ [noscript,notxpcom,nostdcall]
+ nsresult setUsernameInternal(in ACString aUsername);
+
+ ///////////////////////////////////////////////////////////////////////////////
+ // Eventually we'd like to push this type of functionality up into nsIURI.
+ // The idea is to allow the "application" (the part of the code which wants to
+ // run a url in order to perform some action) to register itself as a listener
+ // on url. As a url listener, the app will be informed when the url begins to run
+ // and when the url is finished.
+ ////////////////////////////////////////////////////////////////////////////////
+ void RegisterListener(in nsIUrlListener aUrlListener);
+ void UnRegisterListener(in nsIUrlListener aUrlListener);
+
+ readonly attribute nsIURI baseURI;
+
+ // if you really want to know what the current state of the url is (running or not
+ // running) you should look into becoming a urlListener...
+ void SetUrlState(in boolean runningUrl, in nsresult aStatusCode);
+ void GetUrlState(out boolean runningUrl);
+
+ readonly attribute nsIMsgIncomingServer server;
+
+ /**
+ * Transport-level security information (if any), in the case of a security
+ * error having occurred.
+ * This value should be considered undefined if an NSS error has not
+ * occurred. Read it as: "the secInfo that was being used when a failure
+ * occurred", not: "the secInfo that failed".
+ * Seems a bit ugly adding more state here, but the idea is that a
+ * nsIUrlListener.OnStopRunningUrl() needs to be able to access a bad
+ * certificate, so as to have the option of adding an exemption (See
+ * Bug 1590473).
+ */
+ attribute nsITransportSecurityInfo failedSecInfo;
+
+ /**
+ * The folder associated with this url.
+ *
+ * @exception NS_ERROR_FAILURE May be thrown if the url does not
+ * relate to a folder, e.g. standalone
+ * .eml messages.
+ */
+ attribute nsIMsgFolder folder;
+
+ attribute nsIMsgStatusFeedback statusFeedback;
+
+ /**
+ * The maximum progress for this URL. This might be a count, or it might
+ * be a number of bytes. A value of -1 indicates that this is unknown.
+ */
+ attribute long long maxProgress;
+
+ attribute nsIMsgWindow msgWindow;
+
+ // current mime headers if reading message
+ attribute nsIMimeHeaders mimeHeaders;
+
+ // the load group is computed from the msgWindow
+ readonly attribute nsILoadGroup loadGroup;
+
+ // search session, if we're running a search.
+ attribute nsIMsgSearchSession searchSession;
+ attribute boolean updatingFolder;
+ attribute boolean msgIsInLocalCache;
+ attribute boolean suppressErrorMsgs; // used to avoid displaying biff error messages
+
+ /**
+ * Set after an error occurred.
+ * It is not translated and contains no parameters.
+ * It is unique to each different kind of error, i.e. the same
+ * error has the same code, but a different error has a different code.
+ * This allows to recover from specific errors programmatically,
+ * or to keep error statistics.
+ * If the error comes from a server, the implementor should make
+ * efforts to pass on comparable server error identifiers and include
+ * them here, e.g. as suffixes. Example: "imap-sasl-S474", where-as "S474"
+ * comes from the server annd "imap-sasl-" is the prefix for where the
+ * server reports appears.
+ */
+ attribute ACString errorCode;
+ /**
+ * Set after an error occurred.
+ * An error message that can be displayed directly
+ * to the end user without further processing.
+ * It must have been translated.
+ * It may contain contain values as part of the message.
+ */
+ attribute AString errorMessage;
+
+ /**
+ * To be used in error situations, e.g. to give the URI to an error page
+ * that describes the problem.
+ */
+ attribute AUTF8String seeOtherURI;
+
+ attribute nsICacheEntry memCacheEntry;
+
+ const unsigned long eCopy = 0;
+ const unsigned long eMove = 1;
+ const unsigned long eDisplay = 2;
+ boolean IsUrlType(in unsigned long type);
+ nsIStreamListener getSaveAsListener(in boolean addDummyEnvelope, in nsIFile aFile);
+
+ /// Returns true if the URI is for a message (e.g., imap-message://)
+ readonly attribute boolean isMessageUri;
+
+ /**
+ * Loads the URI in a docshell. This will give priority to loading the
+ * URI in the passed-in docshell. If it can't be loaded there
+ * however, the URL dispatcher will go through its normal process of content
+ * loading.
+ *
+ * @param docshell The docshell that will consume the load.
+ *
+ * @param aLoadFlags Flags to modify load behaviour. Flags are defined in
+ * nsIWebNavigation. Normally only LOAD_FLAGS_NONE or
+ * LOAD_FLAGS_IS_LINK is needed, but there are eleven
+ * other allowed sets of flags. See nsDocShellLoadTypes.h
+ */
+ void loadURI(in nsIDocShell docshell,
+ in unsigned long aLoadFlags);
+
+};
+
+//////////////////////////////////////////////////////////////////////////////////
+// This is a very small interface which I'm grouping with the mailnewsUrl interface.
+// Several url types (mailbox, imap, nntp) have similar properties because they can
+// represent mail messages. For instance, these urls can be have URI
+// equivalents which represent a message.
+// We want to provide the app the ability to get the URI for the
+// url. This URI to URL mapping doesn't exist for all mailnews urls...hence I'm
+// grouping it into a separate interface...
+//////////////////////////////////////////////////////////////////////////////////
+
+[scriptable, uuid(388a37ec-2e1a-4a4f-9d8b-189bedf1bda2)]
+interface nsIMsgMessageUrl : nsISupports {
+ // get and set the RDF URI associated with the url. Note, not all urls have
+ // had uri's set on them so be prepared to handle cases where this string is empty.
+ attribute AUTF8String uri;
+ // used by imap, pop and nntp in order to implement save message to disk
+ attribute nsIFile messageFile;
+ attribute boolean AddDummyEnvelope;
+ attribute boolean canonicalLineEnding;
+ attribute AUTF8String originalSpec;
+
+ // This is used when creating a principal for the URL with a "normalized" spec
+ // that doesn't contain all the bits in the query part that mailnews URLs have.
+ // We need this to implement nsIURIWithSpecialOrigin, since mailnews URLs
+ // have ORIGIN_IS_FULL_SPEC.
+ readonly attribute AUTF8String normalizedSpec;
+
+ /**
+ * A message db header for that message.
+ *
+ * @note This attribute is not guaranteed to be set, so callers that
+ * actually require an nsIMsgDBHdr will need to use the uri attribute
+ * on this interface to get the appropriate nsIMsgMessageService and
+ * then get the header from there.
+ */
+ readonly attribute nsIMsgDBHdr messageHeader;
+};
+
+//////////////////////////////////////////////////////////////////////////////////
+// This is a very small interface which I'm grouping with the mailnewsUrl interface.
+// I want to isolate out all the I18N specific information that may be associated with
+// any given mailnews url. This gives I18N their own "sandbox" of routines they can add
+// and tweak as they see fit. For now it contains mostly charset information.
+//////////////////////////////////////////////////////////////////////////////////
+
+[scriptable, uuid(D71E0785-2862-11d4-98C1-001083010E9B)]
+interface nsIMsgI18NUrl : nsISupports {
+ // when true the user wants us to auto-detect the character set.
+ attribute boolean autodetectCharset;
+};
diff --git a/comm/mailnews/base/public/nsIMsgMailSession.idl b/comm/mailnews/base/public/nsIMsgMailSession.idl
new file mode 100644
index 0000000000..d11929edab
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgMailSession.idl
@@ -0,0 +1,78 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/*
+ * The mail session is a replacement for the old 4.x MSG_Master object. It
+ * contains mail session generic information such as the account manager, etc
+ * I'm starting this off as an empty interface and as people feel they need to
+ * add more information to it, they can. I think this is a better approach
+ * than trying to port over the old MSG_Master in its entirety as that had a
+ * lot of cruft in it....
+ */
+
+#include "nsIFolderListener.idl"
+
+interface nsIFile;
+interface nsIMsgWindow;
+interface nsIMsgUserFeedbackListener;
+interface nsIMsgMailNewsUrl;
+
+[scriptable, uuid(577ead34-553e-4cd6-b484-76ff6662082d)]
+interface nsIMsgMailSession : nsISupports {
+ void Shutdown();
+
+ /**
+ * Adds a listener to be notified when folders update.
+ *
+ * @param aListener The listener to add.
+ * @param aNotifyFlags A combination of flags detailing on which operations
+ * to notify the listener. See nsIFolderListener.idl for
+ * details.
+ */
+ void AddFolderListener(in nsIFolderListener aListener,
+ in folderListenerNotifyFlagValue aNotifyFlags);
+ /**
+ * Removes a listener from the folder notification list.
+ *
+ * @param aListener The listener to remove.
+ */
+ void RemoveFolderListener(in nsIFolderListener aListener);
+
+ /**
+ * Adds a listener to be notified of alert or prompt style feedback that
+ * should go to the user.
+ *
+ * @param aListener The listener to add.
+ */
+ void addUserFeedbackListener(in nsIMsgUserFeedbackListener aListener);
+
+ /**
+ * Removes a user feedback listener.
+ *
+ * @param aListener The listener to remove.
+ */
+ void removeUserFeedbackListener(in nsIMsgUserFeedbackListener aListener);
+
+ /**
+ * Call to alert the listeners of the message. If there are no listeners,
+ * or the listeners do not handle the alert, then this function will present
+ * the user with a modal dialog if aMsgWindow isn't null.
+ *
+ * @param aMessage The localized message string to alert.
+ * @param aUrl Optional mailnews url which is relevant to the operation
+ * which caused the alert to be generated.
+ */
+ void alertUser(in AString aMessage, [optional] in nsIMsgMailNewsUrl aUrl);
+
+ readonly attribute nsIMsgWindow topmostMsgWindow;
+ void AddMsgWindow(in nsIMsgWindow msgWindow);
+ void RemoveMsgWindow(in nsIMsgWindow msgWindow);
+ boolean IsFolderOpenInWindow(in nsIMsgFolder folder);
+
+ AUTF8String ConvertMsgURIToMsgURL(in AUTF8String aURI, in nsIMsgWindow aMsgWindow);
+ nsIFile getDataFilesDir(in string dirName);
+};
diff --git a/comm/mailnews/base/public/nsIMsgMdnGenerator.idl b/comm/mailnews/base/public/nsIMsgMdnGenerator.idl
new file mode 100644
index 0000000000..0da7475f8c
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgMdnGenerator.idl
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgWindow;
+interface nsIMsgFolder;
+interface nsIMimeHeaders;
+
+typedef long EDisposeType;
+typedef long ReceiptHdrType;
+typedef long MDNIncorporateType;
+
+[scriptable, uuid(440EA3DE-DACA-4886-9875-84E6CD7D7927)]
+interface nsIMsgMdnGenerator : nsISupports
+{
+ const EDisposeType eDisplayed = 0;
+ const EDisposeType eDispatched = 1;
+ const EDisposeType eProcessed = 2;
+ const EDisposeType eDeleted = 3;
+ const EDisposeType eDenied = 4;
+ const EDisposeType eFailed = 5;
+
+ const ReceiptHdrType eDntType = 0;
+ const ReceiptHdrType eRrtType = 1;
+ const ReceiptHdrType eDntRrtType = 2;
+
+ const MDNIncorporateType eIncorporateInbox = 0;
+ const MDNIncorporateType eIncorporateSent = 1;
+
+ /**
+ * Prepare the sending of a mdn reply, and checks the prefs whether a
+ * reply should be send. Might send the message automatically if the
+ * prefs say it should.
+ * @param eType One of EDisposeType above, indicating the action that led
+ * to sending the mdn reply
+ * @param aWindow The window the message was displayed in, acting as parent
+ * for any (error) dialogs
+ * @param folder The folder the message is in
+ * @param key the message key
+ * @param headers the message headers
+ * @param autoAction true if the request action led to sending the mdn
+ * reply was an automatic action, false if it was user initiated
+ * @returns true if the user needs to be asked for permission
+ * false in other cases (whether the message was sent or denied)
+ */
+ boolean process(in EDisposeType eType, in nsIMsgWindow aWindow,
+ in nsIMsgFolder folder, in nsMsgKey key,
+ in nsIMimeHeaders headers, in boolean autoAction);
+
+ /**
+ * Must be called when the user was asked for permission and agreed to
+ * sending the mdn reply.
+ * May only be called when |process| returned |true|. Behaviour is
+ * unspecified in other cases
+ */
+ void userAgreed();
+
+ /**
+ * Must be called when the user was asked for permission and declined to
+ * send the mdn reply.
+ * Will mark the message so that the user won't be asked next time.
+ * May only be called when |process| returned |true|. Behaviour is
+ * unspecified in other cases.
+ */
+ void userDeclined();
+};
diff --git a/comm/mailnews/base/public/nsIMsgMessageService.idl b/comm/mailnews/base/public/nsIMsgMessageService.idl
new file mode 100644
index 0000000000..93d9902e63
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgMessageService.idl
@@ -0,0 +1,226 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+interface nsIURI;
+interface nsIUrlListener;
+interface nsIStreamListener;
+interface nsIMsgWindow;
+interface nsIFile;
+interface nsIMsgFolder;
+interface nsIMsgSearchSession;
+interface nsIMsgDBHdr;
+interface nsIStreamConverter;
+interface nsICacheEntry;
+
+%{C++
+#include "MailNewsTypes.h"
+%}
+
+/**
+ * nsIMsgMessageService provides higher-level, UI-oriented calls for
+ * dealing with messages in a protocol-agnostic way.
+ * Things the user would recognise as actions they initiated.
+ * This covers things like displaying messages, copying them, saving them
+ * to disk, saving attachments...
+ */
+[scriptable, uuid(3aa7080a-73ac-4394-9636-fc00e182319b)]
+interface nsIMsgMessageService : nsISupports {
+
+ /**
+ * If you want a handle on the running task, pass in a valid nsIURI
+ * ptr. You can later interrupt this action by asking the netlib
+ * service manager to interrupt the url you are given back.
+ * Remember to release aURL when you are done with it. Pass nullptr
+ * in for aURL if you don't care about the returned URL.
+ */
+
+ /**
+ * Pass in the URI for the message you want to have copied.
+ *
+ * @param aSrcURI
+ * @param aCopyListener already knows about the destination folder.
+ * @param aMoveMessage TRUE if you want the message to be moved.
+ * FALSE leaves it as just a copy.
+ * @param aUrlListener
+ * @param aMsgWindow
+ */
+ void copyMessage(in AUTF8String aSrcURI, in nsIStreamListener aCopyListener, in boolean aMoveMessage,
+ in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Copy multiple messages at a time
+ *
+ * @param keys
+ * @param srcFolder
+ * @param aCopyListener
+ * @param aMoveMessage
+ * @param aUrlListener
+ * @param aMsgWindow
+ * @returns URI that's run to perform the copy
+ */
+ nsIURI CopyMessages(in Array<nsMsgKey> aKeys,
+ in nsIMsgFolder srcFolder,
+ in nsIStreamListener aCopyListener,
+ in boolean aMoveMessage,
+ in nsIUrlListener aUrlListener,
+ in nsIMsgWindow aMsgWindow);
+
+ /**
+ * When you want a message displayed.... this loads it into the consumer.
+ *
+ * @param aMessageURI Is a uri representing the message to display.
+ * @param aDisplayConsumer Is (for now) an nsIDocShell which we'll use to load
+ * the message into.
+ * XXXbz Should it be an nsIWebNavigation or something?
+ * @param aMsgWindow
+ * @param aUrlListener
+ * @param aAutodetectCharset (optional) if the characterset should be auto-detected.
+ */
+ void loadMessage(in AUTF8String aMessageURI,
+ in nsISupports aDisplayConsumer,
+ in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener,
+ in boolean aAutodetectCharset);
+
+ /**
+ * When you want to spool a message out to a file on disk.
+ * This is an asynch operation of course. You must pass in a
+ * url listener in order to figure out when the operation is done.
+ *
+ * @param aMessageURI The uri representing the message to spool out to disk.
+ * @param aFile The file you want the message saved to
+ * @param aGenerateDummyEnvelope Usually FALSE. Set to TRUE if you want the msg
+ * appended at the end of the file.
+ * @param aUrlListener
+ * @param aURL
+ * @param canonicalLineEnding
+ * @param aMsgWindow
+ */
+ void SaveMessageToDisk(in AUTF8String aMessageURI, in nsIFile aFile,
+ in boolean aGenerateDummyEnvelope,
+ in nsIUrlListener aUrlListener, out nsIURI aURL,
+ in boolean canonicalLineEnding, in nsIMsgWindow aMsgWindow);
+
+ /**
+ * When you have a uri and you would like to convert that
+ * to a url which can be run through necko, you can use this method.
+ * the Uri MUST refer to a message and not a folder!
+ *
+ * @param aMessageURI A message uri to convert.
+ * @param aMsgWindow
+ *
+ * @return a URL which can be run through necko
+ */
+ nsIURI getUrlForUri(in AUTF8String aMessageURI, [optional] in nsIMsgWindow aMsgWindow);
+
+ /**
+ *
+ *
+ * @param aSearchSession
+ * @param aMsgWindow
+ * @param aMsgFolder
+ * @param aSearchUri
+ */
+ void Search(in nsIMsgSearchSession aSearchSession, in nsIMsgWindow aMsgWindow, in nsIMsgFolder aMsgFolder, in AUTF8String aSearchUri);
+
+ /**
+ * This method streams a message to the passed in consumer. If aConvertData is true, it
+ * will create a stream converter from message rfc822 to star/star. It will also tack
+ * aAdditionalHeader onto the url (e.g., "header=filter").
+ *
+ * @param aMessageURI uri of message to stream
+ * @param aConsumer generally, a stream listener listening to the message
+ * @param aMsgWindow msgWindow for give progress and status feedback
+ * @param aUrlListener gets notified when url starts and stops
+ * @param aConvertData should we create a stream converter?
+ * @param aAdditionalHeader added to URI, e.g., "header=filter"
+ * @param aLocalOnly whether data should be retrieved only from local caches
+ * If streaming over the network is required and this is true, then
+ * an exception is thrown. This defaults to false.
+ *
+ * @note If we're offline, then even if aLocalOnly is false, we won't stream over the
+ * network
+ *
+ * @return the URL that gets run
+ */
+ nsIURI streamMessage(in AUTF8String aMessageURI, in nsISupports aConsumer,
+ in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener,
+ in boolean aConvertData,
+ in ACString aAdditionalHeader,
+ [optional] in boolean aLocalOnly);
+
+ /**
+ * This method streams a message's headers to the passed in consumer.
+ * This is for consumers who want a particular header but don't
+ * want to stream the whole message.
+ *
+ * @param aMessageURI uri of message whose headers we are to stream
+ * @param aConsumer a stream listener listening to the message
+ headers.
+ * @param aUrlListener gets notified when url starts and stops, if we run a url.
+ * @param aLocalOnly whether data should be retrieved only from local caches
+ * If streaming over the network is required and this is true, then
+ * an exception is thrown. This defaults to false.
+ *
+ * @note If we're offline, then even if aLocalOnly is false, we won't stream over the
+ * network
+ *
+ * @return the URL that gets run, if any.
+ */
+ nsIURI streamHeaders(in AUTF8String aMessageURI, in nsIStreamListener aConsumer,
+ in nsIUrlListener aUrlListener,
+ [optional] in boolean aLocalOnly);
+
+ /**
+ * Determines whether a message is in the memory cache. Local folders
+ * don't implement this.
+ * The URL needs to address a message, not a message part, all query
+ * qualifiers will be stripped before looking up the entry in the cache.
+ *
+ * @param aUrl The URL of the message, possibly with an appropriate command in it
+ * @param aFolder The folder this message is in
+ *
+ * @return TRUE if the message is in mem cache; FALSE if it is not.
+ */
+ boolean isMsgInMemCache(in nsIURI aUrl,
+ in nsIMsgFolder aFolder);
+
+ /**
+ * now the the message datasource is going away
+ * we need away to go from message uri to go nsIMsgDBHdr
+ *
+ * @param uri A message uri to get nsIMsgDBHdr for.
+ *
+ * @return nsIMsgDBHdr for specified uri or null if failed.
+ */
+ nsIMsgDBHdr messageURIToMsgHdr(in AUTF8String uri);
+};
+
+/**
+ * Some mail protocols (like imap) allow you to fetch individual mime parts. We use this interface
+ * to represent message services whose protocols support this. To use this interface, you should get
+ * the message service then QI for this interface. If it's present, then can fetch a mime part.
+ */
+[scriptable, uuid(3728C255-480C-11d4-98D0-001083010E9B)]
+interface nsIMsgMessageFetchPartService : nsISupports
+{
+ /**
+ * Used to fetch an individual mime part
+ *
+ * @param aURI url representing the message
+ * @param aMessageURI RDF URI including the part to fetch
+ * @param aDisplayConsumer
+ * @param aMsgWindow
+ * @param aUrlListener
+ *
+ * @return
+ */
+ nsIURI fetchMimePart(in nsIURI aURI, in AUTF8String aMessageUri, in nsISupports aDisplayConsumer,
+ in nsIMsgWindow aMsgWindow,
+ in nsIUrlListener aUrlListener);
+};
diff --git a/comm/mailnews/base/public/nsIMsgOfflineManager.idl b/comm/mailnews/base/public/nsIMsgOfflineManager.idl
new file mode 100644
index 0000000000..282af2d2b5
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgOfflineManager.idl
@@ -0,0 +1,22 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+// this is a service -there's only one Offline Manager, because you can only do one operation at a time
+// (go online or offline).
+
+interface nsIMsgWindow;
+
+[scriptable, uuid(5e885fec-09b0-11d5-a5bf-0060b0fc04b7)]
+interface nsIMsgOfflineManager : nsISupports
+{
+ attribute nsIMsgWindow window; // should be a progress window.
+ attribute boolean inProgress; // an online->offine or online->offline operation in progress.
+ // the offline menu should be disabled.
+ void goOnline(in boolean sendUnsentMessages, in boolean playbackOfflineImapOperations, in nsIMsgWindow aMsgWindow);
+ void synchronizeForOffline(in boolean downloadNews, in boolean downloadMail, in boolean sendUnsentMessages,
+ in boolean goOfflineWhenDone, in nsIMsgWindow aMsgWindow);
+};
diff --git a/comm/mailnews/base/public/nsIMsgPluggableStore.idl b/comm/mailnews/base/public/nsIMsgPluggableStore.idl
new file mode 100644
index 0000000000..5273d6f0a6
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgPluggableStore.idl
@@ -0,0 +1,335 @@
+/* -*- 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgFolder;
+interface nsIMsgCopyServiceListener;
+interface nsIMsgDBHdr;
+interface nsIMsgWindow;
+interface nsIOutputStream;
+interface nsIInputStream;
+interface nsIUrlListener;
+interface nsIMsgDatabase;
+interface nsITransaction;
+
+[scriptable, uuid(F732CE58-E540-4dc4-B803-9456056EBEFC)]
+
+/**
+ * Pluggable message store interface. Each incoming server can have a different
+ * message store.
+ * All methods are synchronous unless otherwise specified.
+ */
+interface nsIMsgPluggableStore : nsISupports {
+ /**
+ * Examines the store and adds subfolders for the existing folders in the
+ * profile directory. aParentFolder->AddSubfolder is the normal way
+ * to register the subfolders. This method is expected to be synchronous.
+ * This shouldn't be confused with server folder discovery, which is allowed
+ * to be asynchronous.
+ *
+ * @param aParentFolder folder whose existing children we want to discover.
+ * This will be the root folder for the server object.
+ * @param aDeep true if we should discover all descendents. Would we ever
+ * not want to do this?
+ */
+
+ void discoverSubFolders(in nsIMsgFolder aParentFolder, in boolean aDeep);
+ /**
+ * Creates storage for a new, empty folder.
+ *
+ * @param aParent parent folder
+ * @param aFolderName leaf name of folder.
+ * @return newly created folder.
+ * @exception NS_MSG_FOLDER_EXISTS If the child exists.
+ * @exception NS_MSG_CANT_CREATE_FOLDER for other errors.
+ */
+ nsIMsgFolder createFolder(in nsIMsgFolder aParent, in AString aFolderName);
+
+ /**
+ * Delete storage for a folder and its subfolders, if any.
+ * This is a real delete, not a move to the trash folder.
+ *
+ * @param aFolder folder to delete
+ */
+ void deleteFolder(in nsIMsgFolder aFolder);
+
+ /**
+ * Rename storage for an existing folder.
+ *
+ * @param aFolder folder to rename
+ * @param aNewName name to give new folder
+ * @return the renamed folder object
+ */
+ nsIMsgFolder renameFolder(in nsIMsgFolder aFolder, in AString aNewName);
+
+ /**
+ * Tells if the store has the requested amount of space available in the
+ * specified folder.
+ *
+ * @param aFolder folder we want to add messages to.
+ * @param aSpaceRequested How many bytes we're trying to add to the store.
+ *
+ * The function returns an exception if there is not enough space to
+ * indicate the reason of the shortage:
+ * NS_ERROR_FILE_TOO_BIG = the store cannot grow further due to internal limits
+ * NS_ERROR_FILE_NO_DEVICE_SPACE = there is not enough space on the disk
+ */
+ boolean hasSpaceAvailable(in nsIMsgFolder aFolder,
+ in long long aSpaceRequested);
+
+ /**
+ * Move/Copy a folder to a new parent folder. This method is asynchronous.
+ * The store needs to use the aListener to notify the core code of the
+ * completion of the operation. And it must send the appropriate
+ * nsIMsgFolderNotificationService notifications.
+ *
+ * @param aSrcFolder folder to move/copy
+ * @param aDstFolder parent dest folder
+ * @param aIsMoveFolder true if move, false if copy. If move, source folder
+ * is deleted when copy completes.
+ * @param aMsgWindow used to display progress, may be null
+ * @param aListener - used to get notification when copy is done.
+ * @param aNewName Optional new name for the target folder.
+ * If rename is not needed, set this to empty string.
+ */
+ void copyFolder(in nsIMsgFolder aSrcFolder, in nsIMsgFolder aDstFolder,
+ in boolean aIsMoveFolder, in nsIMsgWindow aMsgWindow,
+ in nsIMsgCopyServiceListener aListener,
+ in AString aNewName);
+
+ /**
+ * Get an output stream for a message in a folder.
+ *
+ * @param aFolder folder to create a message output stream for.
+ * @param aNewHdr If aNewHdr is set on input, then this is probably for
+ * offline storage of an existing message. If null, the
+ * this is a newly downloaded message and the store needs
+ * to create a new header for the new message. If the db
+ * is invalid, this can be null. But if the db is valid,
+ * the store should create a message header with the right
+ * message key, or whatever other property it needs to set to
+ * be able to retrieve the message contents later. If the store
+ * needs to base any of this on the contents of the message,
+ * it will need remember the message header and hook into
+ * the output stream somehow to alter the message header.
+ *
+ * @return The output stream to write to. The output stream will be positioned
+ * for writing (e.g., for berkeley mailbox, it will be at the end).
+ */
+ nsIOutputStream getNewMsgOutputStream(in nsIMsgFolder aFolder,
+ inout nsIMsgDBHdr aNewHdr);
+
+
+ /**
+ * Called when the current message is discarded, e.g., it is moved
+ * to an other folder as a filter action, or is deleted because it's
+ * a duplicate. This gives the berkeley mailbox store a chance to simply
+ * truncate the Inbox w/o leaving a deleted message in the store.
+ *
+ * discardNewMessage closes aOutputStream always unless the passed stream
+ * is nullptr due to error processing..
+ * (Clarification/Rationale in Bug 1121842, 1122698, 1242030)
+ *
+ * @param aOutputStream stream we were writing the message to be discarded to
+ * @param aNewHdr header of message to discard
+ */
+ void discardNewMessage(in nsIOutputStream aOutputStream,
+ in nsIMsgDBHdr aNewHdr);
+
+ /**
+ * Must be called by code that calls getNewMsgOutputStream to finish
+ * the process of storing a new message, if the new msg has not been
+ * discarded. Could/should this be combined with discardNewMessage?
+ *
+ * finishNewMessage closes aOutputStream always unless the passed stream
+ * is nullptr due to error processing.
+ * (Clarification/Rationale in Bug 1121842, 1122698, 1242030)
+ *
+ * @param aOutputStream stream we were writing the message to.
+ * @param aNewHdr header of message finished.
+ */
+ void finishNewMessage(in nsIOutputStream aOutputStream,
+ in nsIMsgDBHdr aNewHdr);
+
+ /**
+ * Called by pop3 message filters when a newly downloaded message is being
+ * moved by an incoming filter. This is called before finishNewMessage, and
+ * it allows the store to optimize that case.
+ *
+ * @param aNewHdr msg hdr of message being moved.
+ * @param aDestFolder folder to move message to, in the same store.
+ *
+ * @return true if successful, false if the store doesn't want to optimize
+ * this.
+ * @exception If the moved failed. values TBD
+ */
+ boolean moveNewlyDownloadedMessage(in nsIMsgDBHdr aNewHdr,
+ in nsIMsgFolder aDestFolder);
+
+ /**
+ * Get an input stream that we can read the contents of a message from.
+ *
+ * @param aMsgFolder Folder containing the message
+ * @param aMsgToken token that identifies message. This is store-dependent,
+ * and must be set as a string property "storeToken" on the
+ * message hdr by the store when the message is added
+ * to the store.
+ */
+ nsIInputStream getMsgInputStream(in nsIMsgFolder aFolder,
+ in ACString aMsgToken);
+
+ /**
+ * This is a hack to expose to allow JsAccount folders to implement a
+ * working getLocalMsgStream().
+ * It just provides a way to construct a SlicedInputStream from JS.
+ * It'll be removed once Bug 1733849 is complete.
+ *
+ * @param inStream The stream providing the data to be sliced.
+ * Should not be read after calling this function.
+ * @param start Where slice begins, from current position of inStream.
+ * @param length The size of the slice.
+ *
+ * @return A new input stream which produces the data slice when read from.
+ */
+ nsIInputStream sliceStream(in nsIInputStream inStream,
+ in unsigned long long start,
+ in unsigned long length);
+
+ /**
+ * Delete the passed in messages. These message should all be in the
+ * same folder.
+ * @param aHdrArray array of nsIMsgDBHdr's.
+ */
+ void deleteMessages(in Array<nsIMsgDBHdr> aHdrArray);
+
+ /**
+ * This allows the store to handle a msg move/copy if it wants. This lets
+ * it optimize move/copies within the same store. E.g., for maildir, a
+ * msg move mostly entails moving the file containing the message, and
+ * updating the db.
+ * If the store does the copy, it must return the appropriate undo action,
+ * which can be store dependent. And it must send the appropriate
+ * nsIMsgFolderNotificationService notifications.
+ * If the store does not perform the copy, it returns false and the caller
+ * has to handle the copy itself (by streaming messages).
+ * This function is synchronous.
+ *
+ * @param isMove true if this is a move, false if it is a copy.
+ * @param aHdrArray array of nsIMsgDBHdr's, all in the same folder
+ * @param aDstFolder folder to move/copy the messages to.
+ * @param aDstHdrs array of nsIMsgDBHdr's in the destination folder.
+ * @param[out,optional] aUndoAction transaction to provide undo, if
+ * the store does the copy itself.
+ * @return true if messages were copied, false if the core code should
+ * do the copy.
+ */
+ boolean copyMessages(in boolean isMove,
+ in Array<nsIMsgDBHdr> aHdrArray,
+ in nsIMsgFolder aDstFolder,
+ out Array<nsIMsgDBHdr> aDstHdrs,
+ out nsITransaction aUndoAction);
+
+ /**
+ * Does this store require compaction? For example, maildir doesn't require
+ * compaction at all. Berkeley mailbox does. A sqlite store probably doesn't.
+ * This is a static property of the store. It doesn't mean that any particular
+ * folder has space that can be reclaimed via compaction. Right now, the core
+ * code keeps track of the size of messages deleted, which it can use in
+ * conjunction with this store attribute.
+ */
+ readonly attribute boolean supportsCompaction;
+
+ /**
+ * Remove deleted messages from the store, reclaiming space. Some stores
+ * won't need to do anything here (e.g., maildir), and those stores
+ * should return false for needsCompaction. This operation is asynchronous,
+ * and the passed url listener should be called when the operation is done.
+ *
+ * @param aFolder folder whose storage is to be compacted
+ * @param aListener listener notified when compaction is done.
+ * @param aMsgWindow window to display progress/status in.
+ */
+ void compactFolder(in nsIMsgFolder aFolder, in nsIUrlListener aListener,
+ in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Is the summary file for the passed folder valid? For Berkeley Mailboxes,
+ * for local mail folders, this checks the timestamp and size of the local
+ * mail folder against values stored in the db. For other stores, this may
+ * be a noop, though other stores could certainly become invalid. For
+ * Berkeley Mailboxes, this is to deal with the case of other apps altering
+ * mailboxes from outside mailnews code, and this is certainly possible
+ * with other stores.
+ *
+ * @param aFolder Folder to check if summary is valid for.
+ * @param aDB DB to check validity of.
+ *
+ * @return return true if the summary file is valid, false otherwise.
+ */
+ boolean isSummaryFileValid(in nsIMsgFolder aFolder, in nsIMsgDatabase aDB);
+
+ /**
+ * Marks the summary file for aFolder as valid or invalid. This method
+ * may not be required, since it's really used by Berkeley Mailbox code
+ * to fix the timestamp and size for a folder.
+ *
+ * @param aFolder folder whose summary file should be marked (in)valid.
+ * @param aDB db to mark valid (may not be the folder's db in odd cases
+ * like folder compaction.
+ * @param aValid whether to mark it valid or invalid.
+ */
+ void setSummaryFileValid(in nsIMsgFolder aFolder, in nsIMsgDatabase aDB,
+ in boolean aValid);
+
+ /**
+ * Rebuild the index from information in the store. This involves creating
+ * a new nsIMsgDatabase for the folder, adding the information for all the
+ * messages in the store, and then copying the new msg database over the
+ * existing database. For Berkeley mailbox, we try to maintain meta data
+ * stored in the existing database when possible, and other stores should do
+ * the same. Ideally, I would figure out a way of making that easy. That
+ * might entail reworking the rebuild index process into one where the store
+ * would iterate over the messages, and stream each message through the
+ * message parser, and the common code would handle maintaining the
+ * meta data. But the berkeley mailbox code needs to do some parsing because
+ * it doesn't know how big the message is (i.e., the stream can't simply be
+ * a file stream).
+ * This operation is asynchronous,
+ * and the passed url listener should be called when the operation is done.
+ *
+ * @param aFolder folder whose storage is to be compacted
+ * @param aMsgDB db to put parsed headers in.
+ * @param aMsgWindow msgWindow to use for progress updates.
+ * @param aListener listener notified when the index is rebuilt.
+ */
+ void rebuildIndex(in nsIMsgFolder aFolder, in nsIMsgDatabase aMsgDB,
+ in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener);
+
+ /**
+ * Sets/Clears the passed flags on the passed messages.
+ * @param aHdrArray array of nsIMsgDBHdr's
+ * @param aFlags flags to set/clear
+ * @param aSet true to set the flag(s), false to clear.
+ */
+ void changeFlags(in Array<nsIMsgDBHdr> aHdrArray, in unsigned long aFlags,
+ in boolean aSet);
+ /**
+ *Sets/Clears the passed keywords on the passed messages.
+ * @param aHdrArray array of nsIMsgDBHdr's
+ * @param aKeywords keywords to set/clear
+ * @param aAdd true to add the keyword(s), false to remove.
+ */
+ void changeKeywords(in Array<nsIMsgDBHdr> aHdrArray, in ACString aKeywords,
+ in boolean aAdd);
+
+ /**
+ * Identifies a specific type of store. Please use this only for legacy
+ * bug fixes, and not as a method to change behavior!
+ *
+ * Typical values: "mbox", "maildir"
+ */
+ readonly attribute ACString storeType;
+};
diff --git a/comm/mailnews/base/public/nsIMsgProgress.idl b/comm/mailnews/base/public/nsIMsgProgress.idl
new file mode 100644
index 0000000000..4e370d090c
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgProgress.idl
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; 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/. */
+#include "nsISupports.idl"
+#include "domstubs.idl"
+#include "nsIPrompt.idl"
+#include "nsIWebProgressListener.idl"
+
+interface mozIDOMWindowProxy;
+interface nsIMsgWindow;
+
+[scriptable, uuid(6d6fe91d-7f9a-4552-9737-9f74b0e75538)]
+interface nsIMsgProgress: nsIWebProgressListener {
+
+ /**
+ * Open the progress dialog, you can specify parameters through an xpcom object
+ */
+ void openProgressDialog(in mozIDOMWindowProxy parent,
+ in nsIMsgWindow aMsgWindow,
+ in string dialogURL,
+ in boolean inDisplayModal,
+ in nsISupports parameters);
+
+ /* Close the progress dialog */
+ void closeProgressDialog(in boolean forceClose);
+
+ /* Register a Web Progress Listener */
+ void registerListener(in nsIWebProgressListener listener);
+
+ /* Unregister a Web Progress Listener */
+ void unregisterListener(in nsIWebProgressListener listener);
+
+ /* Indicated if the user asked to cancel the current process */
+ attribute boolean processCanceledByUser;
+
+ attribute nsIMsgWindow msgWindow;
+};
diff --git a/comm/mailnews/base/public/nsIMsgProtocolHandler.idl b/comm/mailnews/base/public/nsIMsgProtocolHandler.idl
new file mode 100644
index 0000000000..a526104676
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgProtocolHandler.idl
@@ -0,0 +1,13 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+[scriptable, uuid(4e9e4a43-343a-4309-a88b-08c5f37f5965)]
+interface nsIMsgProtocolHandler : nsISupports {
+ nsIURI newURI(in AUTF8String aSpec, in string aOriginCharset, in nsIURI aBaseURI);
+};
diff --git a/comm/mailnews/base/public/nsIMsgProtocolInfo.idl b/comm/mailnews/base/public/nsIMsgProtocolInfo.idl
new file mode 100644
index 0000000000..7a505673e0
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgProtocolInfo.idl
@@ -0,0 +1,97 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+%{C++
+#define NS_MSGPROTOCOLINFO_CONTRACTID_PREFIX \
+ "@mozilla.org/messenger/protocol/info;1?type="
+%}
+
+[scriptable, uuid(9428b5f5-8b12-493c-aae2-18296c2877b1)]
+interface nsIMsgProtocolInfo : nsISupports
+{
+ /**
+ * the default path to store local data for this type of
+ * server. Each server is usually in a subdirectory below this
+ */
+ attribute nsIFile defaultLocalPath;
+
+ /**
+ * the IID of the protocol-specific interface for this server
+ * usually used from JS to dynamically get server-specific attributes
+ */
+ readonly attribute nsIIDPtr serverIID;
+
+ /**
+ * does this server type require a username?
+ * for instance, news does not but IMAP/POP do
+ */
+ readonly attribute boolean requiresUsername;
+
+ /**
+ * if the pretty name of the server should
+ * just be the e-mail address. Otherwise it usually
+ * ends up being something like "news on hostname"
+ */
+ readonly attribute boolean preflightPrettyNameWithEmailAddress;
+
+ /**
+ * can this type of server be removed from the account manager?
+ * for instance, local mail is not removable
+ */
+ readonly attribute boolean canDelete;
+
+ /**
+ * can this type of server log in at startup?
+ */
+ readonly attribute boolean canLoginAtStartUp;
+
+ /**
+ * can you duplicate this server?
+ * for instance, local mail is unique and should not be duplicated.
+ */
+ readonly attribute boolean canDuplicate;
+
+ /* the default port
+ This is similar to nsIProtocolHanderl.defaultPort,
+ but for architectural reasons, there is a mail-specific interface to this.
+ When the input param isSecure is set to true, for all supported protocols,
+ the secure port value is returned. If isSecure is set to false the default
+ port value is returned */
+ long getDefaultServerPort(in boolean isSecure);
+
+ /**
+ * An attribute that tell us whether on not we can
+ * get messages for the given server type
+ * this is poorly named right now.
+ * it's really is there an inbox for this type?
+ * XXX todo, rename this.
+ */
+ readonly attribute boolean canGetMessages;
+
+ /**
+ * do messages arrive for this server
+ * if they do, we can use our junk controls on it.
+ */
+ readonly attribute boolean canGetIncomingMessages;
+
+ /**
+ * do biff by default?
+ */
+ readonly attribute boolean defaultDoBiff;
+
+ /**
+ * do we need to show compose message link in the AccountCentral page ?
+ */
+ readonly attribute boolean showComposeMsgLink;
+
+ /**
+ * Will new folders be created asynchronously?
+ */
+ readonly attribute boolean foldersCreatedAsync;
+};
diff --git a/comm/mailnews/base/public/nsIMsgPurgeService.idl b/comm/mailnews/base/public/nsIMsgPurgeService.idl
new file mode 100644
index 0000000000..a914d41525
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgPurgeService.idl
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(c73294b2-b619-4915-b0e8-314d4215e08d)]
+interface nsIMsgPurgeService : nsISupports {
+
+ void init();
+ void shutdown();
+};
diff --git a/comm/mailnews/base/public/nsIMsgShutdown.idl b/comm/mailnews/base/public/nsIMsgShutdown.idl
new file mode 100644
index 0000000000..419e5219a5
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgShutdown.idl
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+interface nsIUrlListener;
+interface nsIMsgWindow;
+interface nsIWebProgressListener;
+
+[scriptable, uuid(D1B43428-B631-4629-B691-AB0E01A2DB4B)]
+interface nsIMsgShutdownTask : nsISupports
+{
+ /**
+ * Inform the caller whether or not the task needs to be run. This method
+ * gives the task the flexibility to cancel running a task on shutdown
+ * if nothing needs to be run.
+ */
+ readonly attribute boolean needsToRunTask;
+
+ /**
+ * At shutdown-time, this function will be called to all registered implementors.
+ * Shutdown will be temporarily postponed until |OnStopRequest()| has been called
+ * on the passed in url-listener.
+ * @param inUrlListener The URL listener to report events to.
+ * @param inMsgWindow The current message window to allow for posing dialogs.
+ * @return If the shutdown URL was run or not. If the URL is running, the task
+ * will be responsible for notifying |inUrlListener| when the task is completed.
+ */
+ boolean doShutdownTask(in nsIUrlListener inUrlListener, in nsIMsgWindow inMsgWindow);
+
+ /**
+ * Get the displayable name of the current task. This textual information will be
+ * shown to the user so they know what shutdown task is being performed.
+ * @return The name of the current task being performed.
+ */
+ AString getCurrentTaskName();
+};
+
+[scriptable, uuid(483C8ABB-ECF9-48A3-A394-2C604B603BD5)]
+interface nsIMsgShutdownService : nsISupports
+{
+ /**
+ * Get the number of tasks that will need to be processed at shutdown time.
+ * @return The number of shutdown tasks to do.
+ */
+ long getNumTasks();
+
+ /**
+ * Start the shutdown tasks.
+ */
+ void startShutdownTasks();
+
+ /**
+ * Tell the service to stop running tasks and go ahead and shutdown the application.
+ */
+ void cancelShutdownTasks();
+
+ /**
+ * Set the shutdown listener.
+ */
+ void setShutdownListener(in nsIWebProgressListener inListener);
+
+ /**
+ * Set the status text of the shutdown progress dialog.
+ */
+ void setStatusText(in AString inStatusString);
+};
diff --git a/comm/mailnews/base/public/nsIMsgStatusFeedback.idl b/comm/mailnews/base/public/nsIMsgStatusFeedback.idl
new file mode 100644
index 0000000000..2aedd4713b
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgStatusFeedback.idl
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(AACBFA34-8D29-4A08-9283-A8E5B3AB067F)]
+interface nsIMsgStatusFeedback : nsISupports {
+ void showStatusString(in AString aStatus);
+ void startMeteors();
+ void stopMeteors();
+ void showProgress(in long aPercent);
+ void setStatusString(in AString aStatus); // will be displayed until next user action
+
+ /* aStatusFeedback: a wrapped JS status feedback object */
+ void setWrappedStatusFeedback(in nsIMsgStatusFeedback aStatusFeedback);
+};
diff --git a/comm/mailnews/base/public/nsIMsgTagService.idl b/comm/mailnews/base/public/nsIMsgTagService.idl
new file mode 100644
index 0000000000..0317b64960
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgTagService.idl
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/*
+ * Keys are the internal representation of tags, and use a limited range of
+ * characters, basically the characters allowed in imap keywords, which are
+ * alphanumeric characters, but don't include spaces. Keys are stored on
+ * the imap server, in local mail messages, and in summary files.
+ *
+ * Tags are the user visible representation of keys, and are full unicode
+ * strings. Tags should allow any unicode character.
+ *
+ * This service will do the mapping between keys and tags. When a tag
+ * is added, we'll need to "compute" the corresponding key to use. This
+ * will probably entail replacing illegal ascii characters (' ', '/', etc)
+ * with '_' and then converting to imap mod utf7. We'll then need to make
+ * sure that no other keyword has the same value since that algorithm
+ * doesn't guarantee a unique mapping.
+ *
+ * Tags are sorted internally by 'importance' by their ordinal strings (which by
+ * default are equal to a tag's key and thus only stored if different).
+ * The alphanumerically 'smallest' string is called the 'most important' one and
+ * comes first in any sorted array. The remainder follows in ascending order.
+ */
+
+[scriptable, uuid(84d593a3-5d8a-45e6-96e2-9189acd422e1)]
+interface nsIMsgTag : nsISupports {
+ readonly attribute ACString key; // distinct tag identifier
+ readonly attribute AString tag; // human readable tag name
+ readonly attribute ACString color; // tag color
+ readonly attribute ACString ordinal; // custom sort string (usually empty)
+};
+
+[scriptable, uuid(97360ce3-0fba-4f1c-8214-af7bdc6f8587)]
+interface nsIMsgTagService : nsISupports {
+ // create new tag by deriving the key from the tag
+ void addTag(in AString tag, in ACString color, in ACString ordinal);
+ // create/update tag with known key
+ void addTagForKey(in ACString key, in AString tag, in ACString color, in ACString ordinal);
+ // get the key representation of a given tag
+ ACString getKeyForTag(in AString tag);
+ // get the first key by ordinal order
+ ACString getTopKey(in ACString keyList);
+ // support functions for single tag aspects
+ AString getTagForKey(in ACString key); // look up the tag for a key.
+ void setTagForKey(in ACString key, in AString tag); // this can be used to "rename" a tag
+ ACString getColorForKey(in ACString key);
+ AString getSelectorForKey(in ACString key); // return wide string to avoid conversion
+ void setColorForKey(in ACString key, in ACString color);
+ ACString getOrdinalForKey(in ACString key);
+ void setOrdinalForKey(in ACString key, in ACString ordinal);
+ // delete a tag from the list of known tags (but not from any messages)
+ void deleteKey(in ACString key);
+ // get all known tags
+ Array<nsIMsgTag> getAllTags();
+ /*
+ * Determines if the token in aKey corresponds to a current valid tag
+ *
+ * @param aKey The string to test
+ * @return True if aKey is a current token
+ */
+ boolean isValidKey(in ACString aKey);
+};
diff --git a/comm/mailnews/base/public/nsIMsgThread.idl b/comm/mailnews/base/public/nsIMsgThread.idl
new file mode 100644
index 0000000000..91cb995c84
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgThread.idl
@@ -0,0 +1,35 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIMsgEnumerator;
+interface nsIMsgDBHdr;
+interface nsIDBChangeAnnouncer;
+
+[scriptable, uuid(84052876-90e9-4e21-ad38-13e2bb751d8f)]
+interface nsIMsgThread : nsISupports {
+ attribute nsMsgKey threadKey;
+ attribute unsigned long flags;
+ attribute ACString subject;
+ attribute unsigned long newestMsgDate;
+ readonly attribute unsigned long numChildren;
+ readonly attribute unsigned long numUnreadChildren;
+
+ void addChild(in nsIMsgDBHdr child, in nsIMsgDBHdr inReplyTo, in boolean threadInThread, in nsIDBChangeAnnouncer announcer);
+ nsMsgKey getChildKeyAt(in unsigned long index);
+ nsIMsgDBHdr getChild(in nsMsgKey msgKey);
+ nsIMsgDBHdr getChildHdrAt(in unsigned long index);
+ nsIMsgDBHdr getRootHdr();
+ void removeChildAt(in unsigned long index);
+ void removeChildHdr(in nsIMsgDBHdr child, in nsIDBChangeAnnouncer announcer);
+
+ void markChildRead(in boolean bRead);
+
+ nsIMsgDBHdr getFirstUnreadChild();
+
+ nsIMsgEnumerator enumerateMessages(in nsMsgKey parent);
+};
diff --git a/comm/mailnews/base/public/nsIMsgUserFeedbackListener.idl b/comm/mailnews/base/public/nsIMsgUserFeedbackListener.idl
new file mode 100644
index 0000000000..f798c4b86c
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgUserFeedbackListener.idl
@@ -0,0 +1,28 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgMailNewsUrl;
+
+/**
+ * Implement this interface to subscribe to errors and warnings passed out via
+ * nsIMsgMailSession.
+ */
+[scriptable, uuid(5e909ffa-77fe-4ce3-bf3c-06c54596d03d)]
+interface nsIMsgUserFeedbackListener : nsISupports {
+ /**
+ * Called when an alert from a protocol level implementation is generated.
+ *
+ * @param aMessage The localized message string to alert.
+ * @param aUrl Optional mailnews url which is relevant to the operation
+ * which caused the alert to be generated.
+ * @return True if you serviced the alert and it does not need
+ * to be prompted to the user separately.
+ * Note: The caller won't prompt if msgWindow in aUrl is
+ * null, regardless of the value returned.
+ */
+ boolean onAlert(in AString aMessage, [optional] in nsIMsgMailNewsUrl aUrl);
+};
diff --git a/comm/mailnews/base/public/nsIMsgWindow.idl b/comm/mailnews/base/public/nsIMsgWindow.idl
new file mode 100644
index 0000000000..2cbae8e23f
--- /dev/null
+++ b/comm/mailnews/base/public/nsIMsgWindow.idl
@@ -0,0 +1,64 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgStatusFeedback;
+interface nsIMsgFolder;
+interface nsITransactionManager;
+interface nsIDocShell;
+interface mozIDOMWindowProxy;
+interface nsIPrompt;
+interface nsIInterfaceRequestor;
+interface nsIAuthPrompt;
+interface nsIPrincipal;
+
+[scriptable, uuid(a846fe48-4022-4296-a1c4-1dcd7eaecfe5)]
+interface nsIMsgWindow : nsISupports {
+ attribute nsIMsgStatusFeedback statusFeedback;
+ attribute nsITransactionManager transactionManager;
+ attribute nsIMsgFolder openFolder;
+
+ /**
+ * @note Setting this attribute has various side effects, including
+ * wiring up this object as the parent nsIURIContentListener for the
+ * passed-in docshell as well as setting the message content policy service
+ * to listen for OnLocationChange notifications.
+ */
+ attribute nsIDocShell rootDocShell;
+
+ /**
+ * @note Small helper function used to optimize our use of a weak reference
+ * on the message window docshell. Under no circumstances should you be
+ * holding on to the docshell returned here outside the scope of your routine.
+ */
+ readonly attribute nsIDocShell messageWindowDocShell;
+
+ /**
+ * These are currently used to set notification callbacks on
+ * protocol channels to handle things like bad cert exceptions.
+ */
+ attribute nsIInterfaceRequestor notificationCallbacks;
+
+ /**
+ Has a running url been stopped? If you care about checking
+ this flag, you need to clear it before you start your operation since
+ there's no convenient place to clear it.
+ */
+ attribute boolean stopped;
+
+ attribute mozIDOMWindowProxy domWindow;
+
+ void StopUrls();
+
+ /**
+ when the msg window is being unloaded from the content window,
+ we can use this notification to force a flush on anything the
+ msg window hangs on too. For some reason xpconnect is still hanging
+ onto the msg window even though all of our objects have let go of it
+ this forces a release...
+ */
+ void closeWindow();
+};
diff --git a/comm/mailnews/base/public/nsISpamSettings.idl b/comm/mailnews/base/public/nsISpamSettings.idl
new file mode 100644
index 0000000000..96c81c6c08
--- /dev/null
+++ b/comm/mailnews/base/public/nsISpamSettings.idl
@@ -0,0 +1,97 @@
+/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIOutputStream;
+interface nsIMsgIncomingServer;
+interface nsIMsgDBHdr;
+interface nsIFile;
+
+[scriptable, uuid(1772BE95-FDA9-4dfd-A663-8AF92C1E3024)]
+interface nsISpamSettings: nsISupports {
+ /**
+ * 0 for nothing, 100 for highest
+ */
+ attribute long level;
+
+ attribute boolean moveOnSpam;
+ readonly attribute boolean markAsReadOnSpam;
+
+ /**
+ * Most consumers will just use spamFolderURI rather than accessing any of
+ * target attributes directly.
+ */
+ attribute long moveTargetMode;
+ const long MOVE_TARGET_MODE_ACCOUNT = 0;
+ const long MOVE_TARGET_MODE_FOLDER = 1;
+ // Despite their name the following are URIs.
+ attribute AUTF8String actionTargetAccount;
+ attribute AUTF8String actionTargetFolder;
+
+ /**
+ * built from moveTargetMode, actionTargetAccount, actionTargetFolder
+ */
+ readonly attribute AUTF8String spamFolderURI;
+
+ attribute boolean purge;
+ /**
+ * interval, in days
+ */
+ attribute long purgeInterval;
+
+ attribute boolean useWhiteList;
+ attribute AUTF8String whiteListAbURI;
+
+ /**
+ * Should we do something when the user manually marks a message as junk?
+ */
+ readonly attribute boolean manualMark;
+
+ /**
+ * With manualMark true, which action (move to the Junk folder, or delete)
+ * should we take when the user marks a message as junk.
+ */
+ readonly attribute long manualMarkMode;
+ const long MANUAL_MARK_MODE_MOVE = 0;
+ const long MANUAL_MARK_MODE_DELETE = 1;
+
+ /**
+ * integrate with server-side spam detection programs
+ */
+ attribute boolean useServerFilter;
+ attribute ACString serverFilterName;
+ readonly attribute nsIFile serverFilterFile;
+ const long TRUST_POSITIVES = 1;
+ const long TRUST_NEGATIVES = 2;
+ attribute long serverFilterTrustFlags;
+
+ // for logging
+ readonly attribute boolean loggingEnabled;
+ attribute nsIOutputStream logStream;
+ void logJunkHit(in nsIMsgDBHdr aMsgHdr, in boolean aMoveMessage);
+ void logJunkString(in string aLogText);
+ void clone(in nsISpamSettings aSpamSettings);
+
+ // aServer -> spam settings are associated with a particular server
+ void initialize(in nsIMsgIncomingServer aServer);
+
+ /**
+ * check if junk processing for a message should be bypassed
+ *
+ * Typically this is determined by comparing message to: address
+ * to a whitelist of known good addresses or domains.
+ *
+ * @param aMsgHdr database header representing the message.
+ *
+ * @return true if this message is whitelisted, and junk
+ * processing should be bypassed
+ *
+ * false otherwise (including in case of error)
+ */
+ boolean checkWhiteList(in nsIMsgDBHdr aMsgHdr);
+
+};
diff --git a/comm/mailnews/base/public/nsIStatusBarBiffManager.idl b/comm/mailnews/base/public/nsIStatusBarBiffManager.idl
new file mode 100644
index 0000000000..88dc4d7b40
--- /dev/null
+++ b/comm/mailnews/base/public/nsIStatusBarBiffManager.idl
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIMsgFolder.idl"
+#include "nsIFolderListener.idl"
+
+[scriptable, uuid(20b81f2b-ea81-4baa-b378-c5e6d3dc94e5)]
+interface nsIStatusBarBiffManager : nsIFolderListener {
+ // see nsIMsgFolder for definition and constants
+ readonly attribute nsMsgBiffState biffState;
+};
diff --git a/comm/mailnews/base/public/nsIStopwatch.idl b/comm/mailnews/base/public/nsIStopwatch.idl
new file mode 100644
index 0000000000..6e40ace070
--- /dev/null
+++ b/comm/mailnews/base/public/nsIStopwatch.idl
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "nsISupports.idl"
+
+/**
+ * Simple stopwatch mechanism for determining the amount of wall-clock time and
+ * CPU time (user + system) that has elapsed. It is not fancy. It is either
+ * running or it is not. If you want coherent cpu and real time values, then
+ * you had better stop it first. It does not keep counting when stopped,
+ * although one could add a resumeRetroactive or something to accomplish that.
+ */
+[scriptable, uuid(7a671d6e-d48f-4a4f-b87e-644815a5e381)]
+interface nsIStopwatch : nsISupports {
+ /**
+ * Start the stopwatch; all counters are reset to zero. If you want to
+ * keep the already accumulated values, use resume instead.
+ */
+ void start();
+
+ /**
+ * Stop the stopwatch.
+ */
+ void stop();
+
+ /**
+ * Resume the stopwatch without clearing the existing counters. Any time
+ * already accumulated on cpuTime/realTime will be kept.
+ */
+ void resume();
+
+ /**
+ * The total CPU time (user + system) in seconds accumulated between calls to
+ * start/resume and stop. You have to stop the stopwatch to cause this value
+ * to update.
+ */
+ readonly attribute double cpuTimeSeconds;
+ /**
+ * The total wall clock time in seconds accumulated between calls to
+ * start/resume and stop. You have to stop the stopwatch to cause this value
+ * to update.
+ */
+ readonly attribute double realTimeSeconds;
+};
diff --git a/comm/mailnews/base/public/nsISubscribableServer.idl b/comm/mailnews/base/public/nsISubscribableServer.idl
new file mode 100644
index 0000000000..70b0d980db
--- /dev/null
+++ b/comm/mailnews/base/public/nsISubscribableServer.idl
@@ -0,0 +1,74 @@
+/* -*- Mode: IDL; 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIMsgWindow;
+interface nsIMsgIncomingServer;
+interface nsITreeView;
+interface nsIUTF8StringEnumerator;
+
+/**
+ * A listener to receive notification of the subscribable folders of a server.
+ */
+[scriptable, uuid(f337b84a-1dd1-11b2-97c7-fb8b2e3f2280)]
+interface nsISubscribeListener : nsISupports {
+ /**
+ * The server has finished finding all folders to subscribe to.
+ */
+ void OnDonePopulating();
+};
+
+[scriptable, uuid(14b8597a-755b-4e93-b364-e0903801e6ea)]
+interface nsISubscribableServer : nsISupports {
+ attribute nsISubscribeListener subscribeListener;
+ attribute char delimiter;
+
+ void startPopulating(in nsIMsgWindow aMsgWindow, in boolean forceToServer, in boolean getOnlyNew);
+ void startPopulatingWithUri(in nsIMsgWindow aMsgWindow, in boolean forceToServer, in AUTF8String uri);
+ void stopPopulating(in nsIMsgWindow aMsgWindow);
+
+ // return true if state changed, false otherwise
+ boolean setState(in AUTF8String path, in boolean state);
+
+ void subscribeCleanup();
+
+ void subscribe(in wstring name);
+ void unsubscribe(in wstring name);
+
+ void commitSubscribeChanges();
+
+ // other stuff
+ void setIncomingServer(in nsIMsgIncomingServer server);
+ void addTo(in AUTF8String aName, in boolean addAsSubscribed,
+ in boolean aSubscribable, in boolean aChangeIfExists);
+ void setAsSubscribed(in AUTF8String path);
+ void updateSubscribed();
+ void setShowFullName(in boolean showFullName);
+
+ // if path is null, use the root
+ boolean hasChildren(in AUTF8String path);
+ // if path is null, use the root
+ boolean isSubscribed(in AUTF8String path);
+ // if path is null, use the root
+ boolean isSubscribable(in AUTF8String path);
+ // if path is null, use the root
+ AString getLeafName(in AUTF8String path);
+
+ /**
+ * Returns the children uris underneath the specified uri (path).
+ *
+ * @param aPath The server's uri; If this is null or empty, then the
+ * root server uri will be used.
+ */
+ Array<AUTF8String> getChildURIs(in AUTF8String aPath);
+ // if path is null, use the root
+ AUTF8String getFirstChildURI(in AUTF8String path);
+
+ // for searching
+ void setSearchValue(in AString searchValue);
+ readonly attribute boolean supportsSubscribeSearch;
+ readonly attribute nsITreeView folderView;
+};
diff --git a/comm/mailnews/base/public/nsIUrlListener.idl b/comm/mailnews/base/public/nsIUrlListener.idl
new file mode 100644
index 0000000000..93df30b6cd
--- /dev/null
+++ b/comm/mailnews/base/public/nsIUrlListener.idl
@@ -0,0 +1,32 @@
+/* -*- 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/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+%{C++
+#include "nsIURL.h"
+%}
+
+/// General interface that signify URL processing.
+[scriptable, uuid(47618220-D008-11d2-8069-006008128C4E)]
+interface nsIUrlListener : nsISupports {
+ /**
+ * Called to signify the beginning of an URL processing.
+ *
+ * @param url URL being processed.
+ */
+ void OnStartRunningUrl(in nsIURI url);
+
+ /**
+ * Called to signify the end of an URL processing.
+ * This call is always preceded by a call to OnStartRunningUrl.
+ *
+ * @param url URL being processed.
+ * @param aExitCode A result code of URL processing.
+ */
+ void OnStopRunningUrl(in nsIURI url, in nsresult aExitCode);
+};
diff --git a/comm/mailnews/base/public/nsIUserInfo.idl b/comm/mailnews/base/public/nsIUserInfo.idl
new file mode 100644
index 0000000000..c8e6fef78d
--- /dev/null
+++ b/comm/mailnews/base/public/nsIUserInfo.idl
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * These are things the system may know about the current user.
+ */
+[scriptable, uuid(6c1034f0-1dd2-11b2-aa14-e6657ed7bb0b)]
+interface nsIUserInfo : nsISupports
+{
+ readonly attribute AString fullname;
+
+ readonly attribute AString emailAddress;
+
+ readonly attribute AString username;
+
+ readonly attribute AString domain;
+};
+
+%{C++
+
+// 14c13684-1dd2-11b2-9463-bb10ba742554
+#define NS_USERINFO_CID \
+{ 0x14c13684, 0x1dd2, 0x11b2, \
+ {0x94, 0x63, 0xbb, 0x10, 0xba, 0x74, 0x25, 0x54}}
+
+#define NS_USERINFO_CONTRACTID "@mozilla.org/userinfo;1"
+
+%}
diff --git a/comm/mailnews/base/public/nsMsgFolderFlags.idl b/comm/mailnews/base/public/nsMsgFolderFlags.idl
new file mode 100644
index 0000000000..a36fe002e4
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgFolderFlags.idl
@@ -0,0 +1,117 @@
+/*-*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+// This must be limited to unsigned long (uint32_t, no uint64_t)
+// as long as nsIMsgFolder exposes the 'flags' property which contains
+// all the flags values. The callers are used to do
+// (folder.flags & nsMsgFolderFlags.<flagname>) in Javascript
+// which cuts the value to 32bit only. See bug 813459.
+typedef unsigned long nsMsgFolderFlagType;
+
+/// Flags about a folder or a newsgroup.
+[scriptable,uuid(440cd0fc-b4b3-4a0f-a492-92fbe7920588)]
+interface nsMsgFolderFlags : nsISupports {
+ /**
+ * @name Folder Type Flags
+ * These flags define the type of folder. Exactly one will be set.
+ * @{
+ */
+ /// This folder is a newsgroup folder.
+ const nsMsgFolderFlagType Newsgroup = 0x00000001;
+ /// Used to be for a folder that is a news server (NewsHost).
+ const nsMsgFolderFlagType Unused3 = 0x00000002;
+ /// This folder is a mail folder.
+ const nsMsgFolderFlagType Mail = 0x00000004;
+ /** @} */
+
+ /** Whether this is a directory: NewsHosts are always directories;
+ * NewsGroups can be directories if we are in ``show all groups'' mode;
+ * Mail folders will have this bit if they are really directories, not files.
+ * (Note that directories may have zero children.)
+ */
+ const nsMsgFolderFlagType Directory = 0x00000008;
+ /** Whether the children of this folder are currently hidden in the listing.
+ * This will only be present if the nsMsgFolderFlags::Directory bit is on.
+ */
+ const nsMsgFolderFlagType Elided = 0x00000010;
+ /// Whether this is a virtual search folder
+ const nsMsgFolderFlagType Virtual = 0x00000020;
+
+ /** @name News Folder Flags
+ * These flags only occur in folders which have
+ * the nsMsgFolderFlags::Newsgroup bit set, and do
+ * not have the nsMsgFolderFlags::Directory or
+ * nsMsgFolderFlags::Elided bits set.
+ * @{
+ */
+ /// Used to be for folders representing a subscribed newsgroup (Subscribed).
+ const nsMsgFolderFlagType Unused5 = 0x00000040;
+ /// Used to be for new newsgroups added by the `Check New Groups' command.
+ const nsMsgFolderFlagType Unused2 = 0x00000080;
+ /** @} */
+
+ /** @name Mail Folder Flags
+ * These flags only occur in folders which have
+ * the nsMsgFolderFlags::Mail bit set, and do
+ * not have the nsMsgFolderFlags::Directory or
+ * nsMsgFolderFlags::Elided bits set.
+ * @{
+ */
+ /// Whether this is the trash folder.
+ const nsMsgFolderFlagType Trash = 0x00000100;
+ /// Whether this is a folder that sent mail gets delivered to.
+ const nsMsgFolderFlagType SentMail = 0x00000200;
+ /// Whether this is the folder in which unfinished, unsent messages are saved for later editing.
+ const nsMsgFolderFlagType Drafts = 0x00000400;
+ /// Whether this is the folder in which messages are queued for later delivery.
+ const nsMsgFolderFlagType Queue = 0x00000800;
+ /// Whether this is the primary inbox folder.
+ const nsMsgFolderFlagType Inbox = 0x00001000;
+ /// Whether this folder on online IMAP
+ const nsMsgFolderFlagType ImapBox = 0x00002000;
+ /// Whether this is an archive folder
+ const nsMsgFolderFlagType Archive = 0x00004000;
+ /// This used to be used for virtual newsgroups
+ const nsMsgFolderFlagType Unused1 = 0x00008000;
+ /// Used to be for categories
+ const nsMsgFolderFlagType Unused4 = 0x00010000;
+ /// Used to be for new msgs in a folder
+ const nsMsgFolderFlagType Unused7 = 0x00020000;
+ /// Used to be for a folder that is an IMAP server (ImapServer)
+ const nsMsgFolderFlagType Unused6 = 0x00040000;
+ /// This folder is an IMAP personal folder
+ const nsMsgFolderFlagType ImapPersonal = 0x00080000;
+ /// This folder is an IMAP public folder
+ const nsMsgFolderFlagType ImapPublic = 0x00100000;
+ /// This folder is another user's IMAP folder. Think of it like a folder that someone would share.
+ const nsMsgFolderFlagType ImapOtherUser = 0x00200000;
+ /// Whether this is the template folder
+ const nsMsgFolderFlagType Templates = 0x00400000;
+ /// This folder is one of your personal folders that is shared with other users
+ const nsMsgFolderFlagType PersonalShared = 0x00800000;
+ /// This folder is an IMAP \\Noselect folder
+ const nsMsgFolderFlagType ImapNoselect = 0x01000000;
+ /// This folder created offline (this is never set in current code,
+ /// but it is still checked for and obeyed if found on a folder.
+ const nsMsgFolderFlagType CreatedOffline = 0x02000000;
+ /// This imap folder cannot have children :-(
+ const nsMsgFolderFlagType ImapNoinferiors = 0x04000000;
+ /// This folder configured for offline use
+ const nsMsgFolderFlagType Offline = 0x08000000;
+ /// This folder has offline events to play back
+ const nsMsgFolderFlagType OfflineEvents = 0x10000000;
+ /// This folder is checked for new messages
+ const nsMsgFolderFlagType CheckNew = 0x20000000;
+ /// This folder is for spam messages
+ const nsMsgFolderFlagType Junk = 0x40000000;
+ /// This folder is in favorites view
+ const nsMsgFolderFlagType Favorite = 0x80000000;
+ /// Special-use folders
+ const nsMsgFolderFlagType SpecialUse = Inbox|Drafts|Trash|SentMail|
+ Templates|Junk|Archive|Queue;
+ /** @} */
+};
diff --git a/comm/mailnews/base/public/nsMsgGroupnameFlags.h b/comm/mailnews/base/public/nsMsgGroupnameFlags.h
new file mode 100644
index 0000000000..11056807ab
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgGroupnameFlags.h
@@ -0,0 +1,48 @@
+/* -*- 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/. */
+
+#ifndef _msgGroupnameFlags_h_
+#define _msgGroupnameFlags_h_
+
+/* Flags in the subscribe pane (used inside of MSG_GroupNameLine). Where
+ the flags overlap with the nsMsgFolderFlags flags, it has the same value,
+ to reduce the chance of someone using the wrong constant. */
+
+/* Whether the children of this group are currently hidden in the listing.
+ This will only be present if it has any children. */
+#define MSG_GROUPNAME_FLAG_ELIDED 0x0010
+
+/* Whether this folder represents a moderated newsgroup. */
+#define MSG_GROUPNAME_FLAG_MODERATED 0x0020
+
+/* Whether this folder represents a subscribed newsgroup. */
+#define MSG_GROUPNAME_FLAG_SUBSCRIBED 0x0040
+
+/* A newsgroup which has just been added by the `Check New Groups` command. */
+#define MSG_GROUPNAME_FLAG_NEW_GROUP 0x0080
+
+/* Whether there are children of this group. Whether those children are visible
+ in this list is determined by the above "ELIDED" flag. Setting this to the
+ same value as an nsMsgFolderFlags IMAP server, since an IMAP _server_ will
+ never appear in the subscribe pane. */
+#define MSG_GROUPNAME_FLAG_HASCHILDREN 0x40000
+
+/* folder is an IMAP personal folder */
+#define MSG_GROUPNAME_FLAG_IMAP_PERSONAL 0x80000
+
+/* folder is an IMAP public folder */
+#define MSG_GROUPNAME_FLAG_IMAP_PUBLIC 0x100000
+
+/* folder is another user's IMAP folder */
+#define MSG_GROUPNAME_FLAG_IMAP_OTHER_USER 0x200000
+
+/* A \NoSelect IMAP folder */
+#define MSG_GROUPNAME_FLAG_IMAP_NOSELECT 0x400000
+
+/* whether or not this folder is one of your personal folders that is shared
+ with other users */
+#define MSG_GROUPNAME_FLAG_PERSONAL_SHARED 0x800000
+
+#endif
diff --git a/comm/mailnews/base/public/nsMsgHeaderMasks.h b/comm/mailnews/base/public/nsMsgHeaderMasks.h
new file mode 100644
index 0000000000..6de0194096
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgHeaderMasks.h
@@ -0,0 +1,53 @@
+/* -*- 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/. */
+
+#ifndef _msgHeaderMasks_h_
+#define _msgHeaderMasks_h_
+// clang-format off
+
+DO NOT USE ANYMORE!!!
+/* This set enumerates the header fields which may be displayed in the
+ message composition window.
+ */
+#define MSG_FROM_HEADER_MASK 0x00000001
+#define MSG_REPLY_TO_HEADER_MASK 0x00000002
+#define MSG_TO_HEADER_MASK 0x00000004
+#define MSG_CC_HEADER_MASK 0x00000008
+#define MSG_BCC_HEADER_MASK 0x00000010
+#define MSG_FCC_HEADER_MASK 0x00000020
+#define MSG_NEWSGROUPS_HEADER_MASK 0x00000040
+#define MSG_FOLLOWUP_TO_HEADER_MASK 0x00000080
+#define MSG_SUBJECT_HEADER_MASK 0x00000100
+#define MSG_ATTACHMENTS_HEADER_MASK 0x00000200
+
+/* These next four are typically not ever displayed in the UI, but are still
+ stored and used internally. */
+#define MSG_ORGANIZATION_HEADER_MASK 0x00000400
+#define MSG_REFERENCES_HEADER_MASK 0x00000800
+#define MSG_OTHERRANDOMHEADERS_HEADER_MASK 0x00001000
+#define MSG_NEWSPOSTURL_HEADER_MASK 0x00002000
+
+#define MSG_PRIORITY_HEADER_MASK 0x00004000
+//#define MSG_NEWS_FCC_HEADER_MASK 0x00008000
+//#define MSG_MESSAGE_ENCODING_HEADER_MASK 0x00010000
+#define MSG_CHARACTER_SET_HEADER_MASK 0x00008000
+#define MSG_MESSAGE_ID_HEADER_MASK 0x00010000
+//#define MSG_NEWS_BCC_HEADER_MASK 0x00080000
+
+/* This is also not exposed to the UI; it's used internally to help remember
+ whether the original message had an HTML portion that we can quote. */
+//#define MSG_HTML_PART_HEADER_MASK 0x00100000
+
+/* The "body=" pseudo-header (as in "mailto:me?body=hi+there") */
+//#define MSG_DEFAULTBODY_HEADER_MASK 0x00200000
+
+#define MSG_X_TEMPLATE_HEADER_MASK 0x00020000
+
+#define MSG_FCC2_HEADER_MASK 0x00400000
+
+/* IMAP folders for posting */
+//#define MSG_IMAP_FOLDER_HEADER_MASK 0x02000000
+// clang-format on
+#endif
diff --git a/comm/mailnews/base/public/nsMsgLocalFolderHdrs.h b/comm/mailnews/base/public/nsMsgLocalFolderHdrs.h
new file mode 100644
index 0000000000..bc794567f8
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgLocalFolderHdrs.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef _nsMsgLocalFolderHdrs_H
+#define _nsMsgLocalFolderHdrs_H
+// clang-format off
+
+/* The Netscape-specific header fields that we use for storing our
+ various bits of state in mail folders.
+ */
+#define X_MOZILLA_STATUS "X-Mozilla-Status"
+#define X_MOZILLA_STATUS_FORMAT X_MOZILLA_STATUS ": %4.4x"
+#define X_MOZILLA_STATUS_LEN /*1234567890123456*/ 16
+
+#define X_MOZILLA_STATUS2 "X-Mozilla-Status2"
+#define X_MOZILLA_STATUS2_FORMAT X_MOZILLA_STATUS2 ": %8.8x"
+#define X_MOZILLA_STATUS2_LEN /*12345678901234567*/ 17
+
+#define X_MOZILLA_DRAFT_INFO "X-Mozilla-Draft-Info"
+#define X_MOZILLA_DRAFT_INFO_LEN /*12345678901234567890*/ 20
+
+#define X_MOZILLA_NEWSHOST "X-Mozilla-News-Host"
+#define X_MOZILLA_NEWSHOST_LEN /*1234567890123456789*/ 19
+
+#define X_UIDL "X-UIDL"
+#define X_UIDL_LEN /*123456*/ 6
+
+#define CONTENT_LENGTH "Content-Length"
+#define CONTENT_LENGTH_LEN /*12345678901234*/ 14
+
+/* Provide a common means of detecting empty lines in a message. i.e. to detect the end of headers among other things...*/
+#define EMPTY_MESSAGE_LINE(buf) (buf[0] == '\r' || buf[0] == '\n' || buf[0] == '\0')
+
+
+// The default data for the X-Mozilla-Keys header. 80 spaces, room to set
+// a bunch of keywords before we have to rewrite the rest of the message.
+#define X_MOZILLA_KEYWORDS_BLANK " "
+#define X_MOZILLA_KEYWORDS_BLANK_LEN 80
+
+/* blank filled header to store keyword/tags in the mailbox */
+#define X_MOZILLA_KEYWORDS "X-Mozilla-Keys: " X_MOZILLA_KEYWORDS_BLANK MSG_LINEBREAK
+#define X_MOZILLA_KEYWORDS_LEN (sizeof(X_MOZILLA_KEYWORDS) - 1)
+
+// clang-format on
+#endif
diff --git a/comm/mailnews/base/public/nsMsgMessageFlags.idl b/comm/mailnews/base/public/nsMsgMessageFlags.idl
new file mode 100644
index 0000000000..2dac966b12
--- /dev/null
+++ b/comm/mailnews/base/public/nsMsgMessageFlags.idl
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+typedef unsigned long nsMsgMessageFlagType;
+
+/// Flags about a single message.
+[scriptable,uuid(1ea3acdb-7b9f-4e35-9513-76e0a0cc6baa)]
+interface nsMsgMessageFlags : nsISupports
+{
+ /// This message has been read
+ const nsMsgMessageFlagType Read = 0x00000001;
+
+ /// A reply to this message has been successfully sent
+ const nsMsgMessageFlagType Replied = 0x00000002;
+
+ /// This message has been flagged
+ const nsMsgMessageFlagType Marked = 0x00000004;
+
+ /**
+ * This message has already gone, but the folder hasn't been compacted yet.
+ * Since actually removing a message from a folder is a semi-expensive
+ * operation, we tend to delay it; messages with this bit set will be removed
+ * the next time folder compaction is done. Once this bit is set, it never
+ * gets un-set.
+ */
+ const nsMsgMessageFlagType Expunged = 0x00000008;
+
+ /**
+ * The subject of this message has "Re:" on the front. The folder summary
+ * uniquifies all of the strings in it, and to help this, any string which
+ * begins with "Re:" has that stripped first. This bit is then set, so that
+ * when presenting the message, we know to put it back (since the "Re:" is
+ * not itself stored in the file.)
+ */
+ const nsMsgMessageFlagType HasRe = 0x00000010;
+
+ /// The children of this sub-thread are folded in the display
+ const nsMsgMessageFlagType Elided = 0x00000020;
+
+ /// The message is a feed, originally downloaded in a server.type=rss account
+ const nsMsgMessageFlagType FeedMsg = 0x00000040;
+
+ /// This news article or IMAP message is present in the disk cache
+ const nsMsgMessageFlagType Offline = 0x00000080;
+
+ /// This thread is being watched
+ const nsMsgMessageFlagType Watched = 0x00000100;
+
+ /// This message's sender has been authenticated when sending this message
+ const nsMsgMessageFlagType SenderAuthed = 0x00000200;
+
+ /**
+ * This message's body is only the first ten or so of the message, and we
+ * need to add a link to let the user download the rest of it from the POP
+ * server.
+ */
+ const nsMsgMessageFlagType Partial = 0x00000400;
+
+ /**
+ * This message is queued for delivery. This only ever gets set on messages
+ * in the queue folder, but is used to protect against the case of other
+ * messages having made their way in there somehow -- if some other program
+ * put a message in the queue, we don't want to later deliver it!
+ */
+ const nsMsgMessageFlagType Queued = 0x00000800;
+
+ /// This message has been forwarded
+ const nsMsgMessageFlagType Forwarded = 0x00001000;
+
+ /// This message has been redirected
+ const nsMsgMessageFlagType Redirected = 0x00002000;
+
+ /**
+ * These are used to remember the message priority in the mozilla status
+ * flags, so we can regenerate a priority after a rule (or user) has changed
+ * it. They are not returned in MSG_MessageLine.flags, just in mozilla-status,
+ * so if you need more non-persistent flags, you could share these bits. But
+ * it would be wrong.
+ */
+ const nsMsgMessageFlagType Priorities = 0x0000E000;
+
+ /// This message is new since the last time the folder was closed
+ const nsMsgMessageFlagType New = 0x00010000;
+
+ /// This thread has been ignored
+ const nsMsgMessageFlagType Ignored = 0x00040000;
+
+ /// This IMAP message has been marked deleted on the server
+ const nsMsgMessageFlagType IMAPDeleted = 0x00200000;
+
+ /**
+ * This message has requested to send a message delivery notification to its
+ * sender
+ */
+ const nsMsgMessageFlagType MDNReportNeeded = 0x00400000;
+
+ /**
+ * A message delivery notification has been sent for this message. No more
+ * reports should be sent.
+ */
+ const nsMsgMessageFlagType MDNReportSent = 0x00800000;
+
+ /// This message is a template
+ const nsMsgMessageFlagType Template = 0x01000000;
+
+ // 0x8000000 is MSG_VIEW_FLAG_ISTHREAD, do not use.
+
+ /// This message has files attached to it
+ const nsMsgMessageFlagType Attachment = 0x10000000;
+
+ // 0x20000000 is MSG_VIEW_FLAG_DUMMY, do not use.
+ // 0x40000000 is MSG_VIEW_FLAG_HASCHILDREN, do not use.
+
+ /**
+ * These are used to remember the message labels in the mozilla status2
+ * flags. so we can regenerate a priority after a rule (or user) has changed
+ * it. They are not returned in nsMsgHdr.flags, just in mozilla-status2, so
+ * if you need more non-persistent flags, you could share these bits. But it
+ * would be wrong.
+ */
+ const nsMsgMessageFlagType Labels = 0x0E000000;
+
+ // We're trying to reserve the high byte of the flags for view flags, so,
+ // don't add flags to the high byte if possible.
+
+ /// The list of all message flags to not write to disk
+ const nsMsgMessageFlagType RuntimeOnly = Elided;
+};
+
+typedef unsigned long nsMsgProcessingFlagType;
+
+/**
+ * Definitions of processing flags. These flags are not saved to the database.
+ * They are used to define states for message processing. Any changes
+ * to these flags need to be supported in the key sets in nsMsgDBFolder
+ */
+[scriptable,uuid(1f7d642b-de2a-45f0-a27f-9c9ce0b741d8)]
+interface nsMsgProcessingFlags : nsISupports
+{
+ /// This message needs junk classification
+ const nsMsgProcessingFlagType ClassifyJunk = 0x00000001;
+
+ /// This message needs traits classification
+ const nsMsgProcessingFlagType ClassifyTraits = 0x00000002;
+
+ /// This message has completed any needed traits classification
+ const nsMsgProcessingFlagType TraitsDone = 0x00000004;
+
+ /// This message has completed any needed postPlugin filtering
+ const nsMsgProcessingFlagType FiltersDone = 0x00000008;
+
+ /// This message has a move scheduled by filters
+ const nsMsgProcessingFlagType FilterToMove = 0x00000010;
+
+ /**
+ * This message is new to the folder and has yet to be reported via the
+ * msgsClassified notification. This flag is required because the previously
+ * used mechanism relied on the database's list of new messages and its
+ * concept of 'new' is overloaded and has user-visible ramifications. This
+ * led to messages potentially being considered multiple times.
+ *
+ * Unfortunately none of the Done processing flags above are suitable for our
+ * needs because they are not consistently applied and basically constitute
+ * memory leaks (which makes the not consistently applied thing a good
+ * thing.)
+ *
+ * I suspect we cannot reliably convert the Done flags above to our use case
+ * either because of the situation where the user quits the program after the
+ * messages are added but before the messages are processed. Since the
+ * processing flags are suppression flags, assuming the 'new' status is
+ * persisted to the next time we are run, then this would represent a
+ * change in behaviour. I would need to exactly understand the new semantics
+ * to know for sure though.
+ */
+ const nsMsgProcessingFlagType NotReportedClassified = 0x00000020;
+
+ /// Number of processing flags
+ const nsMsgProcessingFlagType NumberOfFlags = 6;
+};
diff --git a/comm/mailnews/base/src/ABQueryUtils.jsm b/comm/mailnews/base/src/ABQueryUtils.jsm
new file mode 100644
index 0000000000..4944971ddf
--- /dev/null
+++ b/comm/mailnews/base/src/ABQueryUtils.jsm
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file contains helper methods for dealing with addressbook search URIs.
+ */
+
+const EXPORTED_SYMBOLS = [
+ "getSearchTokens",
+ "getModelQuery",
+ "modelQueryHasUserValue",
+ "generateQueryURI",
+ "encodeABTermValue",
+];
+
+/**
+ * Parse the multiword search string to extract individual search terms
+ * (separated on the basis of spaces) or quoted exact phrases to search
+ * against multiple fields of the addressbook cards.
+ *
+ * @param {string} aSearchString - The full search string entered by the user.
+ *
+ * @returns {Array} Array of separated search terms from the full search string.
+ */
+function getSearchTokens(aSearchString) {
+ // Trim leading and trailing whitespace and comma(s) to prevent empty search
+ // words when splitting unquoted parts of search string below.
+ let searchString = aSearchString
+ .replace(/^[,\s]+/, "")
+ .replace(/[,\s]+$/, "");
+ if (searchString == "") {
+ return [];
+ }
+
+ let quotedTerms = [];
+
+ // Split up multiple search words to create a *foo* and *bar* search against
+ // search fields, using the OR-search template from modelQuery for each word.
+ // If the search query has quoted terms like "foo bar", extract them as is.
+ let startIndex;
+ while ((startIndex = searchString.indexOf('"')) != -1) {
+ let endIndex = searchString.indexOf('"', startIndex + 1);
+ if (endIndex == -1) {
+ endIndex = searchString.length;
+ }
+
+ quotedTerms.push(searchString.substring(startIndex + 1, endIndex));
+ let query = searchString.substring(0, startIndex);
+ if (endIndex < searchString.length) {
+ query += searchString.substr(endIndex + 1);
+ }
+
+ searchString = query.trim();
+ }
+
+ let searchWords = [];
+ if (searchString.length != 0) {
+ // Split non-quoted search terms on whitespace and comma(s): Allow flexible
+ // incremental searches, and prevent false negatives for |Last, First| with
+ // |View > Show Name As > Last, First|, where comma is not found in data.
+ searchWords = quotedTerms.concat(searchString.split(/[,\s]+/));
+ } else {
+ searchWords = quotedTerms;
+ }
+
+ return searchWords;
+}
+
+/**
+ * For AB quicksearch or recipient autocomplete, get the normal or phonetic model
+ * query URL part from prefs, allowing users to customize these searches.
+ *
+ * @param {string} aBasePrefName - The full pref name of default, non-phonetic
+ * model query, e.g. mail.addr_book.quicksearchquery.format. If phonetic
+ * search is used, corresponding pref must exist:
+ * e.g. mail.addr_book.quicksearchquery.format.phonetic
+ * @returns {boolean} depending on mail.addr_book.show_phonetic_fields pref,
+ * the value of aBasePrefName or aBasePrefName + ".phonetic"
+ */
+function getModelQuery(aBasePrefName) {
+ let modelQuery = "";
+ if (
+ Services.prefs.getComplexValue(
+ "mail.addr_book.show_phonetic_fields",
+ Ci.nsIPrefLocalizedString
+ ).data == "true"
+ ) {
+ modelQuery = Services.prefs.getCharPref(aBasePrefName + ".phonetic");
+ } else {
+ modelQuery = Services.prefs.getCharPref(aBasePrefName);
+ }
+ // remove leading "?" to migrate existing customized values for mail.addr_book.quicksearchquery.format
+ // todo: could this be done in a once-off migration at install time to avoid repetitive calls?
+ if (modelQuery.startsWith("?")) {
+ modelQuery = modelQuery.slice(1);
+ }
+ return modelQuery;
+}
+
+/**
+ * Check if the currently used pref with the model query was customized by user.
+ *
+ * @param {string} aBasePrefName - The full pref name of default, non-phonetic
+ * model query, e.g. mail.addr_book.quicksearchquery.format
+ * If phonetic search is used, corresponding pref must exist:
+ * e.g. mail.addr_book.quicksearchquery.format.phonetic
+ * @returns {boolean} true or false
+ */
+function modelQueryHasUserValue(aBasePrefName) {
+ if (
+ Services.prefs.getComplexValue(
+ "mail.addr_book.show_phonetic_fields",
+ Ci.nsIPrefLocalizedString
+ ).data == "true"
+ ) {
+ return Services.prefs.prefHasUserValue(aBasePrefName + ".phonetic");
+ }
+ return Services.prefs.prefHasUserValue(aBasePrefName);
+}
+
+/*
+ * Given a database model query and a list of search tokens,
+ * return query URI.
+ *
+ * @param aModelQuery database model query
+ * @param aSearchWords an array of search tokens.
+ *
+ * @return query URI.
+ */
+function generateQueryURI(aModelQuery, aSearchWords) {
+ // If there are no search tokens, we simply return an empty string.
+ if (!aSearchWords || aSearchWords.length == 0) {
+ return "";
+ }
+
+ let queryURI = "";
+ aSearchWords.forEach(
+ searchWord =>
+ (queryURI += aModelQuery.replace(/@V/g, encodeABTermValue(searchWord)))
+ );
+
+ // queryURI has all the (or(...)) searches, link them up with (and(...)).
+ queryURI = "?(and" + queryURI + ")";
+
+ return queryURI;
+}
+
+/**
+ * Encode the string passed as value into an addressbook search term.
+ * The '(' and ')' characters are special for the addressbook
+ * search query language, but are not escaped in encodeURIComponent()
+ * so must be done manually on top of it.
+ */
+function encodeABTermValue(aString) {
+ return encodeURIComponent(aString)
+ .replace(/\(/g, "%28")
+ .replace(/\)/g, "%29");
+}
diff --git a/comm/mailnews/base/src/FolderLookupService.jsm b/comm/mailnews/base/src/FolderLookupService.jsm
new file mode 100644
index 0000000000..9530dc6e6e
--- /dev/null
+++ b/comm/mailnews/base/src/FolderLookupService.jsm
@@ -0,0 +1,163 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This module implements the folder lookup service (nsIFolderLookupService).
+ */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["FolderLookupService"];
+
+// This ensures that the service is only created once.
+var gCreated = false;
+
+/**
+ * FolderLookupService maintains an index of folders and provides
+ * lookup by folder URI.
+ *
+ * @class
+ */
+function FolderLookupService() {
+ if (gCreated) {
+ throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED);
+ }
+ this._map = new Map();
+ gCreated = true;
+}
+
+FolderLookupService.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderLookupService"]),
+
+ /**
+ * Fetch the folder corresponding to the given URI.
+ * Will only return folders which already exist and have a parent. If this
+ * not the case then null is returned.
+ *
+ * @param {string} uri - URI of folder to get.
+ * @returns {nsIMsgFolder|null}
+ */
+ getFolderForURL(uri) {
+ let folder = this._getExisting(uri);
+
+ if (folder && !this._isValidFolder(folder)) {
+ folder = null; // no dangling folders!
+ }
+ return folder;
+ },
+
+ /**
+ * Fetch the folder corresponding to the given URI, creating it if it does
+ * not exist. If the folder is created, it will be a "dangling" folder,
+ * without a parent and not part of a normal folder hierarchy.
+ * A lot of code relies on this behaviour, but for new code this
+ * call should probably be avoided.
+ *
+ * @param {string} uri - URI of folder to get.
+ * @returns {nsIMsgFolder}
+ */
+ getOrCreateFolderForURL(uri) {
+ let folder = this._getExisting(uri);
+ if (folder) {
+ return folder;
+ }
+
+ // Create new folder.
+
+ // Check that uri has an active scheme, in case this folder is from
+ // an extension that is currently disabled or hasn't started up yet.
+ let schemeMatch = uri.match(/^([-+.\w]+):/);
+ if (!schemeMatch) {
+ return null;
+ }
+ let scheme = schemeMatch[1];
+ let contractID = "@mozilla.org/mail/folder-factory;1?name=" + scheme;
+ if (!(contractID in Cc)) {
+ console.error(
+ "getOrCreateFolderForURL: factory not registered for " + uri
+ );
+ return null;
+ }
+
+ let factory = Components.manager.getClassObject(
+ Cc[contractID],
+ Ci.nsIFactory
+ );
+ if (!factory) {
+ console.error(
+ "getOrCreateFolderForURL: failed to get factory for " + uri
+ );
+ return null;
+ }
+
+ folder = factory.createInstance(Ci.nsIMsgFolder);
+ if (folder) {
+ folder.Init(uri);
+ // Add the new folder to our map. Store a weak reference instead, so that
+ // the folder can be closed when necessary.
+ let weakRef = folder
+ .QueryInterface(Ci.nsISupportsWeakReference)
+ .GetWeakReference();
+ this._map.set(uri, weakRef);
+ }
+
+ return folder;
+ },
+
+ /**
+ * Set pretty name again from original name on all folders,
+ * typically used when locale changes.
+ */
+ setPrettyNameFromOriginalAllFolders() {
+ for (const val of this._map.values()) {
+ try {
+ let folder = val.QueryReferent(Ci.nsIMsgFolder);
+ folder.setPrettyNameFromOriginal();
+ } catch (e) {}
+ }
+ },
+
+ // "private" stuff starts here.
+
+ /**
+ * Internal helper to find a folder (which may or may not be dangling).
+ *
+ * @param {string} uri - URI of folder to look up.
+ *
+ * @returns {nsIMsgFolder|null} - The folder, if in the index, else null.
+ */
+ _getExisting(uri) {
+ let folder = null;
+ // already created?
+ if (this._map.has(uri)) {
+ try {
+ folder = this._map.get(uri).QueryReferent(Ci.nsIMsgFolder);
+ } catch (e) {
+ // The object was deleted, so we can drop it.
+ this._map.delete(uri);
+ }
+ }
+ return folder;
+ },
+
+ /**
+ * Internal helper function to test if a folder is dangling or parented.
+ * Because we can return folders that don't exist, and we may be working
+ * with a deleted folder but we're still holding on to the reference. For
+ * valid folders, one of two scenarios is true: either the folder has a parent
+ * (the deletion code clears the parent to indicate its nonvalidity), or the
+ * folder is a root folder of some server. Getting the root folder may throw
+ * an exception if we attempted to create a server that doesn't exist, so we
+ * need to guard for that error.
+ *
+ * @returns {boolean} - true if folder valid (and parented).
+ */
+ _isValidFolder(folder) {
+ try {
+ return folder.parent != null || folder.rootFolder == folder;
+ } catch (e) {
+ return false;
+ }
+ },
+};
diff --git a/comm/mailnews/base/src/FolderUtils.jsm b/comm/mailnews/base/src/FolderUtils.jsm
new file mode 100644
index 0000000000..a438aa7480
--- /dev/null
+++ b/comm/mailnews/base/src/FolderUtils.jsm
@@ -0,0 +1,364 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This file contains helper methods for dealing with nsIMsgFolders.
+ */
+
+const EXPORTED_SYMBOLS = ["FolderUtils"];
+
+var FolderUtils = {
+ allAccountsSorted,
+ compareAccounts,
+ folderNameCompare,
+ getFolderIcon,
+ getFolderProperties,
+ getMostRecentFolders,
+ getSpecialFolderString,
+ canRenameDeleteJunkMail,
+ isSmartTagsFolder,
+ isSmartVirtualFolder,
+};
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Returns a string representation of a folder's "special" type.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder whose special type to return.
+ * @returns {string} the special type of the folder.
+ */
+function getSpecialFolderString(aFolder) {
+ let flags = aFolder.flags;
+ if (flags & Ci.nsMsgFolderFlags.Inbox) {
+ return "Inbox";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Trash) {
+ return "Trash";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Queue) {
+ return "Outbox";
+ }
+ if (flags & Ci.nsMsgFolderFlags.SentMail) {
+ return "Sent";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Drafts) {
+ return "Drafts";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Templates) {
+ return "Templates";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Junk) {
+ return "Junk";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Archive) {
+ return "Archive";
+ }
+ if (flags & Ci.nsMsgFolderFlags.Virtual) {
+ return "Virtual";
+ }
+ return "none";
+}
+
+/**
+ * This function is meant to be used with trees. It returns the property list
+ * for all of the common properties that css styling is based off of.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder whose properties should be
+ * returned as a string.
+ * @param {boolean} aOpen - Whether the folder is open (not expanded).
+ *
+ * @returns {string} A string of the property names, delimited by space.
+ */
+function getFolderProperties(aFolder, aOpen) {
+ const nsIMsgFolder = Ci.nsIMsgFolder;
+ let properties = [];
+
+ properties.push("folderNameCol");
+
+ properties.push("serverType-" + aFolder.server.type);
+
+ // set the SpecialFolder attribute
+ properties.push("specialFolder-" + getSpecialFolderString(aFolder));
+
+ // Now set the biffState
+ switch (aFolder.biffState) {
+ case nsIMsgFolder.nsMsgBiffState_NewMail:
+ properties.push("biffState-NewMail");
+ break;
+ case nsIMsgFolder.nsMsgBiffState_NoMail:
+ properties.push("biffState-NoMail");
+ break;
+ default:
+ properties.push("biffState-UnknownMail");
+ }
+
+ properties.push("isSecure-" + aFolder.server.isSecure);
+
+ // A folder has new messages, or a closed folder or any subfolder has new messages.
+ if (
+ aFolder.hasNewMessages ||
+ (!aOpen && aFolder.hasSubFolders && aFolder.hasFolderOrSubfolderNewMessages)
+ ) {
+ properties.push("newMessages-true");
+ }
+
+ if (aFolder.isServer) {
+ properties.push("isServer-true");
+ } else {
+ // We only set this if we're not a server
+ let shallowUnread = aFolder.getNumUnread(false);
+ if (shallowUnread > 0) {
+ properties.push("hasUnreadMessages-true");
+ } else {
+ // Make sure that shallowUnread isn't negative
+ shallowUnread = 0;
+ }
+ let deepUnread = aFolder.getNumUnread(true);
+ if (deepUnread - shallowUnread > 0) {
+ properties.push("subfoldersHaveUnreadMessages-true");
+ }
+ }
+
+ properties.push("noSelect-" + aFolder.noSelect);
+ properties.push("imapShared-" + aFolder.imapShared);
+
+ return properties.join(" ");
+}
+
+/**
+ * Returns the sort order value based on the server type to be used for sorting.
+ * The servers (accounts) go in the following order:
+ * (0) default account, (1) other mail accounts, (2) Local Folders,
+ * (3) IM accounts, (4) RSS, (5) News, (9) others (no server)
+ * This ordering is encoded in the .sortOrder property of each server type.
+ *
+ * @param {nsIMsgIncomingServer} aServer -The server to get sort order for.
+ */
+function getServerSortOrder(aServer) {
+ // If there is no server sort this object to the end.
+ if (!aServer) {
+ return 999999999;
+ }
+
+ // Otherwise get the server sort order from the Account manager.
+ return MailServices.accounts.getSortOrder(aServer);
+}
+
+/**
+ * Compares the passed in accounts according to their precedence.
+ */
+function compareAccounts(aAccount1, aAccount2) {
+ return (
+ getServerSortOrder(aAccount1.incomingServer) -
+ getServerSortOrder(aAccount2.incomingServer)
+ );
+}
+
+/**
+ * Returns a list of accounts sorted by server type.
+ *
+ * @param {boolean} aExcludeIMAccounts - Remove IM accounts from the list?
+ */
+function allAccountsSorted(aExcludeIMAccounts) {
+ // This is a HACK to work around bug 41133. If we have one of the
+ // dummy "news" accounts there, that account won't have an
+ // incomingServer attached to it, and everything will blow up.
+ let accountList = MailServices.accounts.accounts.filter(
+ a => a.incomingServer
+ );
+
+ // Remove IM servers.
+ if (aExcludeIMAccounts) {
+ accountList = accountList.filter(a => a.incomingServer.type != "im");
+ }
+
+ return accountList;
+}
+
+/**
+ * Returns the most recently used/modified folders from the passed in list.
+ *
+ * @param {nsIMsgFolder[]} aFolderList - The array of folders to search
+ * for recent folders.
+ * @param {integer} aMaxHits - How many folders to return.
+ * @param {"MRMTime"|"MRUTime"} aTimeProperty - Which folder time property to
+ * use. Use "MRMTime" for most recently modified time.
+ * Use "MRUTime" for most recently used time.
+ */
+function getMostRecentFolders(aFolderList, aMaxHits, aTimeProperty) {
+ let recentFolders = [];
+ const monthOld = Math.floor((Date.now() - 31 * 24 * 60 * 60 * 1000) / 1000);
+
+ /**
+ * This sub-function will add a folder to the recentFolders array if it
+ * is among the aMaxHits most recent. If we exceed aMaxHits folders,
+ * it will pop the oldest folder, ensuring that we end up with the
+ * right number.
+ *
+ * @param {nsIMsgFolders} aFolder - The folder to check for recency.
+ */
+ let oldestTime = 0;
+ function addIfRecent(aFolder) {
+ let time = 0;
+ try {
+ time = Number(aFolder.getStringProperty(aTimeProperty)) || 0;
+ } catch (e) {}
+ if (time <= oldestTime || time < monthOld) {
+ return;
+ }
+
+ if (recentFolders.length == aMaxHits) {
+ recentFolders.sort((a, b) => a.time < b.time);
+ recentFolders.pop();
+ oldestTime = recentFolders[recentFolders.length - 1].time;
+ }
+ recentFolders.push({ folder: aFolder, time });
+ }
+
+ for (let folder of aFolderList) {
+ addIfRecent(folder);
+ }
+
+ return recentFolders.map(f => f.folder);
+}
+
+/**
+ * A locale dependent comparison function to produce a case-insensitive sort order
+ * used to sort folder names.
+ *
+ * @param {string} aString1 - First string to compare.
+ * @param {string} aString2 - Second string to compare.
+ * @returns {interger} A positive number if aString1 > aString2,
+ * negative number if aString1 > aString2, otherwise 0.
+ */
+function folderNameCompare(aString1, aString2) {
+ // TODO: improve this as described in bug 992651.
+ return aString1
+ .toLocaleLowerCase()
+ .localeCompare(aString2.toLocaleLowerCase());
+}
+
+/**
+ * Get the icon to use for this folder.
+ *
+ * @param {nsIMsgFolder} folder - The folder to get icon for.
+ * @returns {string} URL of suitable icon.
+ */
+function getFolderIcon(folder) {
+ let iconName;
+ if (folder.isServer) {
+ switch (folder.server.type) {
+ case "nntp":
+ iconName = folder.server.isSecure ? "globe-secure.svg" : "globe.svg";
+ break;
+ case "imap":
+ case "pop":
+ iconName = folder.server.isSecure ? "mail-secure.svg" : "mail.svg";
+ break;
+ case "none":
+ iconName = "folder.svg";
+ break;
+ case "rss":
+ iconName = "rss.svg";
+ break;
+ default:
+ iconName = "mail.svg";
+ break;
+ }
+ } else if (folder.server.type == "nntp") {
+ iconName = "newsletter.svg";
+ } else {
+ switch (getSpecialFolderString(folder)) {
+ case "Virtual":
+ if (isSmartTagsFolder(folder)) {
+ iconName = "tag.svg";
+ } else {
+ iconName = "folder-filter.svg";
+ }
+ break;
+ case "Junk":
+ iconName = "spam.svg";
+ break;
+ case "Templates":
+ iconName = "template.svg";
+ break;
+ case "Archive":
+ iconName = "archive.svg";
+ break;
+ case "Trash":
+ iconName = "trash.svg";
+ break;
+ case "Drafts":
+ iconName = "draft.svg";
+ break;
+ case "Outbox":
+ iconName = "outbox.svg";
+ break;
+ case "Sent":
+ iconName = "sent.svg";
+ break;
+ case "Inbox":
+ iconName = "inbox.svg";
+ break;
+ default:
+ iconName = "folder.svg";
+ break;
+ }
+ }
+
+ return `chrome://messenger/skin/icons/new/compact/${iconName}`;
+}
+
+/**
+ * Checks if `folder` is a virtual folder for the Unified Folders pane mode.
+ *
+ * @param {nsIMsgFolder} folder
+ * @returns {boolean}
+ */
+function isSmartVirtualFolder(folder) {
+ return (
+ folder.isSpecialFolder(Ci.nsMsgFolderFlags.Virtual) &&
+ folder.server.hostName == "smart mailboxes" &&
+ folder.parent?.isServer
+ );
+}
+
+/**
+ * Checks if `folder` is a virtual folder for the Tags folder pane mode.
+ *
+ * @param {nsIMsgFolder} folder
+ * @returns {boolean}
+ */
+function isSmartTagsFolder(folder) {
+ return (
+ folder.isSpecialFolder(Ci.nsMsgFolderFlags.Virtual) &&
+ folder.server.hostName == "smart mailboxes" &&
+ folder.parent?.name == "tags"
+ );
+}
+
+/**
+ * Checks if the configured junk mail can be renamed or deleted.
+ *
+ * @param {string} aFolderUri
+ */
+function canRenameDeleteJunkMail(aFolderUri) {
+ // Go through junk mail settings for all servers and see if the folder is set/used by anyone.
+ for (let server of MailServices.accounts.allServers) {
+ let settings = server.spamSettings;
+ // If junk mail control or move junk mail to folder option is disabled then
+ // allow the folder to be removed/renamed since the folder is not used in this case.
+ if (!settings.level || !settings.moveOnSpam) {
+ continue;
+ }
+ if (settings.spamFolderURI == aFolderUri) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/comm/mailnews/base/src/HeaderReader.h b/comm/mailnews/base/src/HeaderReader.h
new file mode 100644
index 0000000000..da8defbbe0
--- /dev/null
+++ b/comm/mailnews/base/src/HeaderReader.h
@@ -0,0 +1,305 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef HeaderReader_h__
+#define HeaderReader_h__
+
+#include <algorithm>
+#include "LineReader.h"
+#include "nsMsgUtils.h"
+#include "nsString.h"
+#include "mozilla/Span.h"
+
+/**
+ * HeaderReader parses mail headers from a buffer.
+ * The input is fed in via Parse(), and a callback function is invoked for
+ * each header encountered.
+ *
+ * General goals:
+ *
+ * - Incremental. Parse() can be called multiple times as a buffer grows.
+ * - Works in-place. Headers are returned as byte ranges within the data.
+ * - Works with a partial header block (e.g. sniffing the first N bytes
+ * of a message file). It won't mistakenly emit an incomplete header.
+ * - Track exact byte offsets for values, to support rewriting headers in
+ * place. This is needed to support X-Mozilla-Status et al.
+ * - Avoids copying data where possible.
+ * - Callback is inlined.
+ * - Callback can halt processing (by returning false).
+ * - Tolerant of real-world oddness in input data (for now, we just skip
+ * lines which don't make sense).
+ *
+ * Example usage:
+ * nsCString raw = "To: Alice\r\nFrom: Bob\r\n\r\n...Message body..."_ns;
+ * auto cb = [&](HeaderReader::Header const& hdr) {
+ * printf("-> '%s':'%s'\n", hdr.Name(raw), hdr.Value(raw));
+ * return true;
+ * };
+ *
+ * HeaderReader rdr;
+ * rdr.Parse(raw, cb);
+ * // -> 'To':'Alice'
+ * // -> 'From':'Bob'
+ *
+ * See TestHeaderReader.cpp for more examples.
+ */
+class HeaderReader {
+ public:
+ /**
+ * Parse() scans an input buffer and invokes a callback for each complete
+ * header found.
+ *
+ * It can be called any number of times - it'll pick up where it left off.
+ * The idea is that the caller can accumulate data in multiple chunks and
+ * call Parse() to extract headers incrementally as they come in.
+ * It does rely on data being a single contiguous allocation, but it
+ * doesn't require the data being located in the same memory location
+ * each time. So can it can be safely used on a growable buffer.
+ *
+ * Signature of callback is:
+ * bool hdrCallback(HeaderReader::Hdr const& hdr);
+ *
+ * The callback should return true to continue parsing, or false to halt.
+ * This allows, for example, an early-out if you're scanning for one
+ * specific header and don't care about the rest.
+ *
+ * Parse() stops when one of these conditions is true:
+ * 1. The end of the header block is reached (the final blank line marker
+ * is consumed). Subsequent calls to IsComplete() will return true.
+ * 2. The callback returns false. If Parse() is called again, it will
+ * safely pick up where it left off.
+ * 3. No more headers can be read. There may be some unconsumed data
+ * returned (eg a partial line). Parse() can be safely called again
+ * when more data becomes available. It will resume from the point it
+ * reached previously.
+ *
+ * It is safe to call Parse() on a truncated header block. It will only
+ * invoke the callback for headers which are unambiguously complete.
+ *
+ * @param data - bytes containing the header block to parse.
+ * @param hdrCallback - callback to invoke for each header found
+ *
+ * @returns a span containing the unconsumed (leftover) data.
+ */
+ template <typename HeaderFn>
+ mozilla::Span<const char> Parse(mozilla::Span<const char> data,
+ HeaderFn hdrCallback);
+
+ /**
+ * Complete() returns true if the header block has been fully parsed.
+ * Further calls to Parse() will consume no more data.
+ * The blank line which separates the header block from the body is consumed.
+ */
+ bool IsComplete() const { return mFinished; }
+
+ /**
+ * Hdr holds offsets to a name/value pair within a header block.
+ * The name starts at pos.
+ * The value starts at pos+rawValOffset.
+ */
+ struct Hdr {
+ uint32_t pos{0}; // Start position of header within the block.
+ uint32_t len{0}; // Length of entire header, including final EOL.
+ uint32_t nameLen{0}; // Length of name.
+ uint32_t rawValOffset{0}; // Where the value starts, relative to pos.
+ uint32_t rawValLen{0}; // Excludes final EOL.
+ bool IsEmpty() const { return len == 0; }
+
+ /**
+ * Access the header name as a string.
+ *
+ * @param data - the data originally passed into Parse().
+ * @returns the name within data, wrapped for string access (so it is
+ * valid only as long as data is valid).
+ */
+ nsDependentCSubstring Name(mozilla::Span<const char> data) const {
+ return nsDependentCSubstring(data.Elements() + pos, nameLen);
+ }
+ /**
+ * Access the raw value as a string.
+ *
+ * @param data - the data originally passed into Parse().
+ * @returns the raw data, EOLs and all, wrapped for string access (so it
+ * is valid only as long as data is valid).
+ */
+ nsDependentCSubstring RawValue(mozilla::Span<const char> data) const {
+ return nsDependentCSubstring(data.Elements() + pos + rawValOffset,
+ rawValLen);
+ }
+ /**
+ * Decode the 'cooked' value into a string.
+ * NOTE: handles unfolding multi-line values. No attempt (yet) at dealing
+ * with comments or quoted strings...
+ *
+ * @param data - the data originally passed into Parse().
+ * @returns a new string containing the value.
+ */
+ nsCString Value(mozilla::Span<const char> data) const {
+ nsCString val(RawValue(data));
+ val.ReplaceSubstring("\r\n"_ns, ""_ns);
+ val.ReplaceSubstring("\n"_ns, ""_ns);
+ return val;
+ }
+
+ /**
+ * EOL() returns a string containing the eol characters at the end of the
+ * header. It will be "\n" or "\r\n".
+ * Calling this on an empty hdr struct is unsupported.
+ */
+ nsDependentCSubstring EOL(mozilla::Span<const char> data) const {
+ MOZ_ASSERT(len >= 2); // Empty or malformed?
+
+ uint32_t i = pos + len;
+ int n = 0;
+ if (data[i - 1] == '\n') {
+ ++n;
+ if (data[i - 2] == '\r') {
+ ++n;
+ }
+ }
+ return nsDependentCSubstring(data.Elements() + pos + len - n, n);
+ }
+ };
+
+ private:
+ // How far Parse() has gone so far.
+ uint32_t mPos{0};
+
+ // The current header we're accumulating.
+ Hdr mHdr;
+
+ // Number of EOL chars at the end of previous line (so we can strip it if the
+ // next line is folded).
+ int mEOLSize{0};
+
+ // Set when end of header block detected.
+ bool mFinished{false};
+
+ template <typename HeaderFn>
+ bool HandleLine(mozilla::Span<const char> line, HeaderFn hdrCallback);
+};
+
+// Parse() implementation.
+template <typename HeaderFn>
+mozilla::Span<const char> HeaderReader::Parse(mozilla::Span<const char> data,
+ HeaderFn hdrCallback) {
+ // If were're resuming, skip what we've already scanned.
+ auto remaining = mozilla::Span<const char>(data.cbegin() + mPos, data.cend());
+ if (mFinished) {
+ return remaining;
+ }
+ // Iterate over all the lines of our input.
+ remaining = SplitLines(remaining,
+ [this, hdrCallback](mozilla::Span<const char> line) {
+ return HandleLine(line, hdrCallback);
+ });
+
+ if (!mFinished) {
+ // We didn't get to the end of the header block, but we may still be
+ // able to finalise a previously-started header...
+ if (!mHdr.IsEmpty()) {
+ if (remaining.Length() > 0 && remaining[0] != ' ' &&
+ remaining[0] != '\t') {
+ // Next line isn't folded, so we know the header is complete.
+ mHdr.rawValLen -= mEOLSize;
+ hdrCallback(mHdr);
+ } else {
+ // Can't tell if header is complete. Rewind and try again next time.
+ mPos = mHdr.pos;
+ remaining =
+ mozilla::Span<const char>(data.cbegin() + mPos, data.cend());
+ }
+ mHdr = Hdr();
+ }
+ }
+ return remaining;
+}
+
+// Helper function - we call this on each complete line we encounter.
+template <typename HeaderFn>
+bool HeaderReader::HandleLine(mozilla::Span<const char> line,
+ HeaderFn hdrCallback) {
+ // Should never be here if we've finished.
+ MOZ_ASSERT(!mFinished);
+ // we should _never_ see empty strings.
+ MOZ_ASSERT(!line.IsEmpty());
+
+ // Find the EOL sequence (CRLF or LF).
+ auto eol = line.cend();
+ auto p = eol;
+ if (p > line.cbegin() && *(p - 1) == '\n') {
+ --eol;
+ if ((p - 1) > line.cbegin() && *(p - 2) == '\r') {
+ --eol;
+ }
+ }
+ // We should never have been called with a non-terminated line.
+ MOZ_ASSERT(eol != line.cend());
+
+ // Blank line indicates end of header block.
+ if (eol == line.cbegin()) {
+ if (!mHdr.IsEmpty()) {
+ // Emit the completed header.
+ mHdr.rawValLen -= mEOLSize;
+ hdrCallback(mHdr);
+ mHdr = Hdr();
+ }
+ mFinished = true;
+ mPos += line.Length();
+ return false; // Stop.
+ }
+
+ // A folded line?
+ // Leading space or tab indicates continuation of previous value.
+ if (line[0] == ' ' || line[0] == '\t') {
+ if (!mHdr.IsEmpty()) {
+ // Grow the existing header.
+ mHdr.len += line.Length();
+ mHdr.rawValLen += line.Length();
+ mEOLSize = line.cend() - eol;
+ } else {
+ // UHOH - a folded value but we haven't started a header...
+ // Not much we can do, so we'll just ignore the line.
+ NS_WARNING("Malformed header (bare continuation)");
+ }
+ mPos += line.Length();
+ return true; // Next line, please.
+ }
+
+ bool keepGoing = true;
+ // By now, we're expecting a "name: value" line, to start a fresh header.
+ if (!mHdr.IsEmpty()) {
+ // Flush previous header now we know it's complete.
+ mHdr.rawValLen -= mEOLSize;
+ keepGoing = hdrCallback(mHdr);
+ mHdr = Hdr();
+ }
+
+ auto colon = std::find(line.cbegin(), line.cend(), ':');
+ if (colon == line.cend()) {
+ // UHOH. We were expecting a "name: value" line, but didn't find one.
+ // Just ignore this line.
+ NS_WARNING("Malformed header (expected 'name: value')");
+ mPos += line.Length();
+ return keepGoing;
+ }
+ auto val = colon + 1;
+ if (*val == ' ' || *val == '\t') {
+ // Skip single leading whitespace.
+ ++val;
+ }
+
+ // Start filling out the new header (it may grow if folded lines come next).
+ mHdr.pos = mPos;
+ mHdr.len = line.Length();
+ mHdr.nameLen = colon - line.cbegin();
+
+ mHdr.rawValOffset = val - line.cbegin();
+ mHdr.rawValLen = line.cend() - val;
+ mEOLSize = line.cend() - eol;
+ mPos += line.Length();
+ return keepGoing;
+}
+
+#endif
diff --git a/comm/mailnews/base/src/JXON.jsm b/comm/mailnews/base/src/JXON.jsm
new file mode 100644
index 0000000000..00c1f2bb1f
--- /dev/null
+++ b/comm/mailnews/base/src/JXON.jsm
@@ -0,0 +1,159 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is a modification of the JXON parsers found on the page
+// <https://developer.mozilla.org/en-US/docs/JXON>
+
+var EXPORTED_SYMBOLS = ["JXON"];
+
+var JXON = new (function () {
+ const sValueProp = "value"; /* you can customize these values */
+ const sAttributesProp = "attr";
+ const sAttrPref = "@";
+ const sElementListPrefix = "$";
+ const sConflictSuffix = "_"; // used when there's a name conflict with special JXON properties
+ const aCache = [];
+ const rIsBool = /^(?:true|false)$/i;
+
+ function parseText(sValue) {
+ if (rIsBool.test(sValue)) {
+ return sValue.toLowerCase() === "true";
+ }
+ if (isFinite(sValue)) {
+ return parseFloat(sValue);
+ }
+ if (isFinite(Date.parse(sValue))) {
+ return new Date(sValue);
+ }
+ return sValue;
+ }
+
+ function EmptyTree() {}
+ EmptyTree.prototype = {
+ toString() {
+ return "null";
+ },
+ valueOf() {
+ return null;
+ },
+ };
+
+ function objectify(vValue) {
+ if (vValue === null) {
+ return new EmptyTree();
+ } else if (vValue instanceof Object) {
+ return vValue;
+ }
+ return new vValue.constructor(vValue); // What does this? copy?
+ }
+
+ function createObjTree(oParentNode, nVerb, bFreeze, bNesteAttr) {
+ const nLevelStart = aCache.length;
+ const bChildren = oParentNode.hasChildNodes();
+ const bAttributes = oParentNode.attributes && oParentNode.attributes.length;
+ const bHighVerb = Boolean(nVerb & 2);
+
+ var sProp = 0;
+ var vContent = 0;
+ var nLength = 0;
+ var sCollectedTxt = "";
+ var vResult = bHighVerb
+ ? {}
+ : /* put here the default value for empty nodes: */ true;
+
+ if (bChildren) {
+ for (
+ var oNode, nItem = 0;
+ nItem < oParentNode.childNodes.length;
+ nItem++
+ ) {
+ oNode = oParentNode.childNodes.item(nItem);
+ if (oNode.nodeType === 4) {
+ // CDATASection
+ sCollectedTxt += oNode.nodeValue;
+ } else if (oNode.nodeType === 3) {
+ // Text
+ sCollectedTxt += oNode.nodeValue;
+ } else if (oNode.nodeType === 1) {
+ // Element
+ aCache.push(oNode);
+ }
+ }
+ }
+
+ const nLevelEnd = aCache.length;
+ const vBuiltVal = parseText(sCollectedTxt);
+
+ if (!bHighVerb && (bChildren || bAttributes)) {
+ vResult = nVerb === 0 ? objectify(vBuiltVal) : {};
+ }
+
+ for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) {
+ sProp = aCache[nElId].nodeName;
+ if (sProp == sValueProp || sProp == sAttributesProp) {
+ sProp = sProp + sConflictSuffix;
+ }
+ vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr);
+ if (!vResult.hasOwnProperty(sProp)) {
+ vResult[sProp] = vContent;
+ vResult[sElementListPrefix + sProp] = [];
+ }
+ vResult[sElementListPrefix + sProp].push(vContent);
+ nLength++;
+ }
+
+ if (bAttributes) {
+ const nAttrLen = oParentNode.attributes.length;
+ const sAPrefix = bNesteAttr ? "" : sAttrPref;
+ const oAttrParent = bNesteAttr ? {} : vResult;
+
+ for (var oAttrib, nAttrib = 0; nAttrib < nAttrLen; nLength++, nAttrib++) {
+ oAttrib = oParentNode.attributes.item(nAttrib);
+ oAttrParent[sAPrefix + oAttrib.name] = parseText(oAttrib.value);
+ }
+
+ if (bNesteAttr) {
+ if (bFreeze) {
+ Object.freeze(oAttrParent);
+ }
+ vResult[sAttributesProp] = oAttrParent;
+ nLength -= nAttrLen - 1;
+ }
+ }
+
+ if (
+ nVerb === 3 ||
+ ((nVerb === 2 || (nVerb === 1 && nLength > 0)) && sCollectedTxt)
+ ) {
+ vResult[sValueProp] = vBuiltVal;
+ } else if (!bHighVerb && nLength === 0 && sCollectedTxt) {
+ vResult = vBuiltVal;
+ }
+
+ if (bFreeze && (bHighVerb || nLength > 0)) {
+ Object.freeze(vResult);
+ }
+
+ aCache.length = nLevelStart;
+
+ return vResult;
+ }
+
+ this.build = function (
+ oXMLParent,
+ nVerbosity /* optional */,
+ bFreeze /* optional */,
+ bNesteAttributes /* optional */
+ ) {
+ const _nVerb =
+ typeof nVerbosity === "number"
+ ? nVerbosity & 3
+ : /* put here the default verbosity level: */ 1;
+ return createObjTree(
+ oXMLParent,
+ _nVerb,
+ bFreeze || false,
+ bNesteAttributes !== undefined ? bNesteAttributes : _nVerb === 3
+ );
+ };
+})();
diff --git a/comm/mailnews/base/src/LineReader.h b/comm/mailnews/base/src/LineReader.h
new file mode 100644
index 0000000000..292c7ced7c
--- /dev/null
+++ b/comm/mailnews/base/src/LineReader.h
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef LineReader_h__
+#define LineReader_h__
+
+#include <algorithm>
+#include "mozilla/Span.h"
+#include "mozilla/Vector.h"
+
+/**
+ * FirstLine() returns the first line of a span.
+ * The EOL sequence (CRLF or LF) is included in the returned line.
+ * If no lines are found an empty span is returned.
+ */
+inline mozilla::Span<const char> FirstLine(
+ mozilla::Span<const char> const& data) {
+ auto eol = std::find(data.cbegin(), data.cend(), '\n');
+ if (eol == data.cend()) {
+ // no line ending found - return empty span.
+ return data.First(0);
+ }
+ ++eol;
+ return mozilla::Span<const char>(data.cbegin(), eol);
+}
+
+/**
+ * LineReader breaks up continuous character streams into lines.
+ * Data is fed in by calling Feed() as often as required, and a
+ * callback function is invoked to handle each resulting line.
+ *
+ * The resulting lines include the end-of-line char(s), except for any
+ * non-terminated final line.
+ * LF ('\n') is used as the line terminator. CRLF-terminated lines will
+ * be handled correctly - the resultant lines will include the line
+ * terminators exactly as they appear in the input data.
+ *
+ * Goals for LineReader:
+ * - Byte exact. The bytes fed in will appear _exactly_ in the callback fn.
+ * - Callback can be inlined (due to templating).
+ * - Avoid copying data if possible. The internal buffer is only used when
+ * lines are split across incoming chunks of data.
+ * - Tries to avoid heap allocation. If the internal buffer is used, it'll
+ * only allocate memory for long lines (>80 chars).
+ *
+ * Example usage:
+ *
+ * auto callback = [](mozilla::Span<const char> line) {
+ * printf("%s\n", nsCString(line).get());
+ * return true;
+ * };
+ *
+ * LineReader c;
+ * c.Feed("Line 1\r\nLine 2\r\nLine 3", callback);
+ * // -> "Line 1\r\n"
+ * // -> "Line 2\r\n"
+ * c.Feed("\r\nLeftovers.", callback);
+ * // -> "Line 3\r\n"
+ * c.Flush(callback);
+ * // -> "Leftovers."
+ *
+ * See TestLineReader.cpp for more examples.
+ */
+class LineReader {
+ public:
+ /*
+ * Feed() takes in a chunk of data to be split up into lines. You can call
+ * this as often as required to feed in all your data. Don't forget to call
+ * Flush() after the last Feed(), in case the last line has no line endings!
+ *
+ * The callback will be invoked once for each full line extracted.
+ * It should have the form:
+ * The callback is of the form:
+ * bool callback(mozilla::Span<const char> line);
+ *
+ * The data in `line` should be considered valid only until the callback
+ * returns. So if the callback wants to retain data it needs to copy it.
+ * `line` will include any EOL character(s).
+ * The callback should return true to continue processing.
+ * If the callback returns false, processing will stop, even if there is
+ * more data available.
+ */
+ template <typename LineFn>
+ void Feed(mozilla::Span<const char> data, LineFn callback) {
+ bool keepGoing = true;
+ while (!data.IsEmpty() && keepGoing) {
+ auto eol = std::find(data.cbegin(), data.cend(), '\n');
+ if (eol == data.cend()) {
+ // No LF. Just collect and wait for more.
+ // TODO: limit maximum mBuf size, to stop maliciously-crafted input
+ // OOMing us?
+ if (!mBuf.append(data.data(), data.size())) {
+ NS_ERROR("OOM!");
+ }
+ return;
+ }
+
+ // Consume everything up to and including the LF.
+ ++eol;
+ mozilla::Span<const char> line(data.cbegin(), eol);
+ data = mozilla::Span<const char>(eol, data.cend());
+
+ if (mBuf.empty()) {
+ // Pass the data through directly, no copying.
+ keepGoing = callback(line);
+ } else {
+ // Complete the line we previously started.
+ if (!mBuf.append(line.data(), line.size())) {
+ NS_ERROR("OOM!");
+ }
+ keepGoing = callback(mBuf);
+ mBuf.clear();
+ }
+ }
+ }
+
+ /*
+ * Flush() will invoke the callback with any leftover data, after the last
+ * Feed() call has completed.
+ * The line passed to the callback will be a partial line, without a final
+ * LF. If the input data has a final LF, there will be nothing to flush,
+ * and the callback will not be invoked.
+ */
+ template <typename LineFn>
+ void Flush(LineFn callback) {
+ if (!mBuf.empty()) {
+ callback(mBuf);
+ mBuf.clear();
+ }
+ }
+
+ private:
+ // Growable buffer, to collect lines which come in as multiple parts.
+ // Can handle lines up to 80 chars before needing to reallocate.
+ mozilla::Vector<char, 80> mBuf;
+};
+
+/**
+ * SplitLines() invokes a callback for every complete line it finds in the
+ * input data.
+ *
+ * The callback is of the form:
+ * bool callback(mozilla::Span<const char> line);
+ * where line is a span pointing to the range of bytes in the input data
+ * which comprises the line.
+ *
+ * If the callback returns false, processing is halted.
+ *
+ * The lines passed to the callback include end-of-line (EOL) character(s).
+ *
+ * Lines are considered terminated by '\n' (LF) but this means CRLF-delimited
+ * data is also handled correctly.
+ *
+ * This function is byte-exact: if you concatenate all the line spans, along
+ * with the unconsumed data returned at the end, you'll end up with the exact
+ * same byte sequence as the original input data.
+ *
+ * @param data - The input bytes.
+ * @param callback - The callback to invoke for each line.
+ *
+ * @returns the unconsumed data. Usually this will be empty, or an incomplete
+ * line at the end (with no EOL). However if the callback returned
+ * false, all the unused data will be returned.
+ */
+template <typename LineFn>
+mozilla::Span<const char> SplitLines(mozilla::Span<const char> data,
+ LineFn callback) {
+ while (!data.IsEmpty()) {
+ auto eol = std::find(data.cbegin(), data.cend(), '\n');
+ if (eol == data.cend()) {
+ // No LF - we're done. May or may not be some leftover data.
+ break;
+ }
+
+ // Consume everything up to and including the LF.
+ ++eol;
+ mozilla::Span<const char> line(data.cbegin(), eol);
+ data = mozilla::Span<const char>(eol, data.cend());
+
+ if (callback(line) == false) {
+ break;
+ }
+ }
+ return data;
+}
+
+#endif
diff --git a/comm/mailnews/base/src/LineReader.jsm b/comm/mailnews/base/src/LineReader.jsm
new file mode 100644
index 0000000000..2417457e3c
--- /dev/null
+++ b/comm/mailnews/base/src/LineReader.jsm
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["LineReader"];
+
+/**
+ * For a single request, mail servers may return several multi-line responses. A
+ * definition of multi-line responses can be found at rfc3977#section-3.1.1.
+ *
+ * This class helps dealing with multi-line responses by:
+ * - Break up a response to lines
+ * - Join incomplete line from a previous response with the current response
+ * - Remove stuffed dot (.. at the beginning of a line)
+ * - Detect the end of the response (\r\n.\r\n)
+ */
+class LineReader {
+ processingMultiLineResponse = false;
+ _data = "";
+
+ /**
+ * Read a multi-line response, emit each line through a callback.
+ *
+ * @param {string} data - A multi-line response received from the server.
+ * @param {Function} lineCallback - A line will be passed to the callback each
+ * time.
+ * @param {Function} doneCallback - A function to be called when data is ended.
+ */
+ read(data, lineCallback, doneCallback) {
+ this._data += data;
+ if (this._data == ".\r\n" || this._data.endsWith("\r\n.\r\n")) {
+ this.processingMultiLineResponse = false;
+ this._data = this._data.slice(0, -3);
+ } else {
+ this.processingMultiLineResponse = true;
+ }
+ if (this._running) {
+ // This function can be called multiple times, but this._data should only
+ // be consumed once.
+ return;
+ }
+
+ let i = 0;
+ this._running = true;
+ while (this._data) {
+ let index = this._data.indexOf("\r\n");
+ if (index == -1) {
+ // Not enough data, save it for the next round.
+ break;
+ }
+ let line = this._data.slice(0, index + 2);
+ if (line.startsWith("..")) {
+ // Remove stuffed dot.
+ line = line.slice(1);
+ }
+ lineCallback(line);
+ this._data = this._data.slice(index + 2);
+ if (++i % 100 == 0) {
+ // Prevent blocking main process for too long.
+ Services.tm.spinEventLoopUntilEmpty();
+ }
+ }
+ this._running = false;
+ if (!this.processingMultiLineResponse && !this._data) {
+ doneCallback();
+ }
+ }
+}
diff --git a/comm/mailnews/base/src/MailAuthenticator.jsm b/comm/mailnews/base/src/MailAuthenticator.jsm
new file mode 100644
index 0000000000..cf52a88f17
--- /dev/null
+++ b/comm/mailnews/base/src/MailAuthenticator.jsm
@@ -0,0 +1,468 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = [
+ "SmtpAuthenticator",
+ "NntpAuthenticator",
+ "Pop3Authenticator",
+ "ImapAuthenticator",
+];
+
+var { MailCryptoUtils } = ChromeUtils.import(
+ "resource:///modules/MailCryptoUtils.jsm"
+);
+var { MailStringUtils } = ChromeUtils.import(
+ "resource:///modules/MailStringUtils.jsm"
+);
+
+/**
+ * A base class for interfaces when authenticating a mail connection.
+ */
+class MailAuthenticator {
+ /**
+ * Get the hostname for a connection.
+ *
+ * @returns {string}
+ */
+ get hostname() {
+ throw Components.Exception(
+ "hostname getter not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Get the username for a connection.
+ *
+ * @returns {string}
+ */
+ get username() {
+ throw Components.Exception(
+ "username getter not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Forget cached password.
+ */
+ forgetPassword() {
+ throw Components.Exception(
+ "forgetPassword not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Get the password for a connection.
+ *
+ * @returns {string}
+ */
+ getPassword() {
+ throw Components.Exception(
+ "getPassword not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Get the CRAM-MD5 auth token for a connection.
+ *
+ * @param {string} password - The password, used as HMAC-MD5 secret.
+ * @param {string} challenge - The base64 encoded server challenge.
+ * @returns {string}
+ */
+ getCramMd5Token(password, challenge) {
+ // Hash the challenge.
+ let signature = MailCryptoUtils.hmacMd5(
+ new TextEncoder().encode(password),
+ new TextEncoder().encode(atob(challenge))
+ );
+ // Get the hex form of the signature.
+ let hex = [...signature].map(x => x.toString(16).padStart(2, "0")).join("");
+ return btoa(`${this.username} ${hex}`);
+ }
+
+ /**
+ * Get the OAuth token for a connection.
+ *
+ * @returns {string}
+ */
+ async getOAuthToken() {
+ throw Components.Exception(
+ "getOAuthToken not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Init a nsIMailAuthModule instance for GSSAPI auth.
+ *
+ * @param {('smtp'|'imap')} protocol - The protocol name.
+ */
+ initGssapiAuth(protocol) {
+ this._authModule = Cc["@mozilla.org/mail/auth-module;1"].createInstance(
+ Ci.nsIMailAuthModule
+ );
+ this._authModule.init(
+ "sasl-gssapi", // Auth module type
+ `${protocol}@${this.hostname}`,
+ 0, // nsIAuthModule::REQ_DEFAULT
+ null, // domain
+ this.username,
+ null // password
+ );
+ }
+
+ /**
+ * Get the next token in a sequence of GSSAPI auth steps.
+ *
+ * @param {string} inToken - A base64 encoded string, usually server challenge.
+ * @returns {string}
+ */
+ getNextGssapiToken(inToken) {
+ return this._authModule.getNextToken(inToken);
+ }
+
+ /**
+ * Init a nsIMailAuthModule instance for NTLM auth.
+ */
+ initNtlmAuth() {
+ this._authModule = Cc["@mozilla.org/mail/auth-module;1"].createInstance(
+ Ci.nsIMailAuthModule
+ );
+ this._authModule.init(
+ "ntlm", // Auth module type
+ null, // Service name
+ 0, // nsIAuthModule::REQ_DEFAULT
+ null, // domain
+ this.username,
+ this.getPassword()
+ );
+ }
+
+ /**
+ * Get the next token in a sequence of NTLM auth steps.
+ *
+ * @param {string} inToken - A base64 encoded string, usually server challenge.
+ * @returns {string}
+ */
+ getNextNtlmToken(inToken) {
+ return this._authModule.getNextToken(inToken);
+ }
+
+ /**
+ * Show a dialog for authentication failure.
+ *
+ * @returns {number} - 0: Retry; 1: Cancel; 2: New password.
+ */
+ promptAuthFailed() {
+ throw Components.Exception(
+ "promptAuthFailed not implemented",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ /**
+ * Show a dialog for authentication failure.
+ *
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {string} accountname - A user defined account name or the server hostname.
+ * @returns {number} 0: Retry; 1: Cancel; 2: New password.
+ */
+ _promptAuthFailed(msgWindow, accountname) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ let message = bundle.formatStringFromName("mailServerLoginFailed2", [
+ this.hostname,
+ this.username,
+ ]);
+
+ let title = bundle.formatStringFromName(
+ "mailServerLoginFailedTitleWithAccount",
+ [accountname]
+ );
+
+ let retryButtonLabel = bundle.GetStringFromName(
+ "mailServerLoginFailedRetryButton"
+ );
+ let newPasswordButtonLabel = bundle.GetStringFromName(
+ "mailServerLoginFailedEnterNewPasswordButton"
+ );
+ let buttonFlags =
+ Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING +
+ Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_CANCEL +
+ Ci.nsIPrompt.BUTTON_POS_2 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING;
+ let dummyValue = { value: false };
+
+ return Services.prompt.confirmEx(
+ msgWindow?.domWindow,
+ title,
+ message,
+ buttonFlags,
+ retryButtonLabel,
+ null,
+ newPasswordButtonLabel,
+ null,
+ dummyValue
+ );
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating an SMTP connection.
+ *
+ * @augments {MailAuthenticator}
+ */
+class SmtpAuthenticator extends MailAuthenticator {
+ /**
+ * @param {nsISmtpServer} server - The associated server instance.
+ */
+ constructor(server) {
+ super();
+ this._server = server;
+ }
+
+ get hostname() {
+ return this._server.hostname;
+ }
+
+ get username() {
+ return this._server.username;
+ }
+
+ forgetPassword() {
+ this._server.forgetPassword();
+ }
+
+ getPassword() {
+ if (this._server.password) {
+ return this._server.password;
+ }
+ let composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messengercompose/composeMsgs.properties"
+ );
+ let username = this._server.username;
+ let promptString;
+ if (username) {
+ promptString = composeBundle.formatStringFromName(
+ "smtpEnterPasswordPromptWithUsername",
+ [this._server.hostname, username]
+ );
+ } else {
+ promptString = composeBundle.formatStringFromName(
+ "smtpEnterPasswordPrompt",
+ [this._server.hostname]
+ );
+ }
+ let promptTitle = composeBundle.formatStringFromName(
+ "smtpEnterPasswordPromptTitleWithHostname",
+ [this._server.hostname]
+ );
+ return this._server.getPasswordWithUI(promptString, promptTitle);
+ }
+
+ /**
+ * Get the ByteString form of the current password.
+ *
+ * @returns {string}
+ */
+ getByteStringPassword() {
+ return MailStringUtils.stringToByteString(this.getPassword());
+ }
+
+ /**
+ * Get the PLAIN auth token for a connection.
+ *
+ * @returns {string}
+ */
+ getPlainToken() {
+ // According to rfc4616#section-2, password should be UTF-8 BinaryString
+ // before base64 encoded.
+ return btoa("\0" + this.username + "\0" + this.getByteStringPassword());
+ }
+
+ async getOAuthToken() {
+ let oauth2Module = Cc["@mozilla.org/mail/oauth2-module;1"].createInstance(
+ Ci.msgIOAuth2Module
+ );
+ if (!oauth2Module.initFromSmtp(this._server)) {
+ return Promise.reject(`initFromSmtp failed, hostname: ${this.hostname}`);
+ }
+ return new Promise((resolve, reject) => {
+ oauth2Module.connect(true, {
+ onSuccess: token => {
+ resolve(token);
+ },
+ onFailure: e => {
+ reject(e);
+ },
+ });
+ });
+ }
+
+ promptAuthFailed() {
+ return this._promptAuthFailed(
+ null,
+ this._server.description || this.hostname
+ );
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating an incoming server.
+ *
+ * @augments {MailAuthenticator}
+ */
+class IncomingServerAuthenticator extends MailAuthenticator {
+ /**
+ * @param {nsIMsgIncomingServer} server - The associated server instance.
+ */
+ constructor(server) {
+ super();
+ this._server = server;
+ }
+
+ get hostname() {
+ return this._server.hostName;
+ }
+
+ get username() {
+ return this._server.username;
+ }
+
+ forgetPassword() {
+ this._server.forgetPassword();
+ }
+
+ /**
+ * Get the ByteString form of the current password.
+ *
+ * @returns {string}
+ */
+ async getByteStringPassword() {
+ return MailStringUtils.stringToByteString(await this.getPassword());
+ }
+
+ /**
+ * Get the PLAIN auth token for a connection.
+ *
+ * @returns {string}
+ */
+ async getPlainToken() {
+ // According to rfc4616#section-2, password should be UTF-8 BinaryString
+ // before base64 encoded.
+ return btoa(
+ "\0" + this.username + "\0" + (await this.getByteStringPassword())
+ );
+ }
+
+ async getOAuthToken() {
+ let oauth2Module = Cc["@mozilla.org/mail/oauth2-module;1"].createInstance(
+ Ci.msgIOAuth2Module
+ );
+ if (!oauth2Module.initFromMail(this._server)) {
+ return Promise.reject(`initFromMail failed, hostname: ${this.hostname}`);
+ }
+ return new Promise((resolve, reject) => {
+ oauth2Module.connect(true, {
+ onSuccess: token => {
+ resolve(token);
+ },
+ onFailure: e => {
+ reject(e);
+ },
+ });
+ });
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating a NNTP connection.
+ *
+ * @augments {IncomingServerAuthenticator}
+ */
+class NntpAuthenticator extends IncomingServerAuthenticator {
+ /**
+ * @returns {string} - NNTP server has no userName pref, need to pass it in.
+ */
+ get username() {
+ return this._username;
+ }
+
+ set username(value) {
+ this._username = value;
+ }
+
+ promptAuthFailed() {
+ return this._promptAuthFailed(null, this._server.prettyName);
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating a POP connection.
+ *
+ * @augments {IncomingServerAuthenticator}
+ */
+class Pop3Authenticator extends IncomingServerAuthenticator {
+ async getPassword() {
+ if (this._server.password) {
+ return this._server.password;
+ }
+ let composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/localMsgs.properties"
+ );
+ let params = [this._server.username, this._server.hostName];
+ let promptString = composeBundle.formatStringFromName(
+ "pop3EnterPasswordPrompt",
+ params
+ );
+ let promptTitle = composeBundle.formatStringFromName(
+ "pop3EnterPasswordPromptTitleWithUsername",
+ [this._server.hostName]
+ );
+ return this._server.wrappedJSObject.getPasswordWithUIAsync(
+ promptString,
+ promptTitle
+ );
+ }
+
+ promptAuthFailed() {
+ return this._promptAuthFailed(null, this._server.prettyName);
+ }
+}
+
+/**
+ * Collection of helper functions for authenticating an IMAP connection.
+ *
+ * @augments {IncomingServerAuthenticator}
+ */
+class ImapAuthenticator extends IncomingServerAuthenticator {
+ async getPassword() {
+ if (this._server.password) {
+ return this._server.password;
+ }
+ let composeBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/imapMsgs.properties"
+ );
+ let params = [this._server.username, this._server.hostName];
+ let promptString = composeBundle.formatStringFromName(
+ "imapEnterServerPasswordPrompt",
+ params
+ );
+ let promptTitle = composeBundle.formatStringFromName(
+ "imapEnterPasswordPromptTitleWithUsername",
+ [this._server.hostName]
+ );
+ return this._server.wrappedJSObject.getPasswordWithUIAsync(
+ promptString,
+ promptTitle
+ );
+ }
+
+ promptAuthFailed() {
+ return this._promptAuthFailed(null, this._server.prettyName);
+ }
+}
diff --git a/comm/mailnews/base/src/MailChannel.sys.mjs b/comm/mailnews/base/src/MailChannel.sys.mjs
new file mode 100644
index 0000000000..a5fbf9ee75
--- /dev/null
+++ b/comm/mailnews/base/src/MailChannel.sys.mjs
@@ -0,0 +1,71 @@
+/* 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/. */
+
+/**
+ * @see {nsIMailChannel}
+ */
+export class MailChannel {
+ _headerNames = [];
+ _headerValues = [];
+ _attachments = [];
+ _mailCharacterSet = null;
+ _progressListener = null;
+
+ addHeaderFromMIME(name, value) {
+ this._headerNames.push(name);
+ this._headerValues.push(value);
+ }
+
+ get headerNames() {
+ return this._headerNames;
+ }
+
+ get headerValues() {
+ return this._headerValues;
+ }
+
+ handleAttachmentFromMIME(contentType, url, displayName, uri, notDownloaded) {
+ let attachment = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag2
+ );
+ attachment.setPropertyAsAUTF8String("contentType", contentType);
+ attachment.setPropertyAsAUTF8String("url", url);
+ attachment.setPropertyAsAUTF8String("displayName", displayName);
+ attachment.setPropertyAsAUTF8String("uri", uri);
+ attachment.setPropertyAsBool("notDownloaded", notDownloaded);
+ this._attachments.push(attachment);
+ }
+
+ addAttachmentFieldFromMIME(field, value) {
+ let attachment = this._attachments[this._attachments.length - 1];
+ attachment.setPropertyAsAUTF8String(field, value);
+ }
+
+ get attachments() {
+ return this._attachments.slice();
+ }
+
+ get mailCharacterSet() {
+ return this._mailCharacterSet;
+ }
+
+ set mailCharacterSet(value) {
+ let ccm = Cc["@mozilla.org/charset-converter-manager;1"].getService(
+ Ci.nsICharsetConverterManager
+ );
+ this._mailCharacterSet = ccm.getCharsetAlias(value);
+ }
+
+ imipMethod = null;
+ imipItem = null;
+ smimeHeaderSink = null;
+
+ get listener() {
+ return this._progressListener?.get();
+ }
+
+ set listener(listener) {
+ this._progressListener = Cu.getWeakReference(listener);
+ }
+}
diff --git a/comm/mailnews/base/src/MailCryptoUtils.jsm b/comm/mailnews/base/src/MailCryptoUtils.jsm
new file mode 100644
index 0000000000..6c378e6703
--- /dev/null
+++ b/comm/mailnews/base/src/MailCryptoUtils.jsm
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["MailCryptoUtils"];
+
+var MailCryptoUtils = {
+ /**
+ * Converts a binary string into a Uint8Array.
+ *
+ * @param {BinaryString} str - The string to convert.
+ * @returns {Uint8Array}.
+ */
+ binaryStringToTypedArray(str) {
+ let arr = new Uint8Array(str.length);
+ for (let i = 0; i < str.length; i++) {
+ arr[i] = str.charCodeAt(i);
+ }
+ return arr;
+ },
+
+ /**
+ * The HMAC-MD5 transform works like:
+ *
+ * MD5(K XOR opad, MD5(K XOR ipad, m))
+ *
+ * where
+ * K is an n byte key
+ * ipad is the byte 0x36 repeated 64 times
+ * opad is the byte 0x5c repeated 64 times
+ * m is the message being processed
+
+ * @param {Uint8Array} key
+ * @param {Uint8Array} data
+ * @returns {Uint8Array}
+ */
+ hmacMd5(key, data) {
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ let digest;
+
+ // If key is longer than 64 bytes, reset it to MD5(key).
+ if (key.length > 64) {
+ hasher.init(Ci.nsICryptoHash.MD5);
+ hasher.update(key, key.length);
+ digest = hasher.finish(false);
+ key = this.binaryStringToTypedArray(digest);
+ }
+
+ // Generate innerPad and outerPad.
+ let innerPad = new Uint8Array(64);
+ let outerPad = new Uint8Array(64);
+ for (let i = 0; i < 64; i++) {
+ let base = key[i] || 0;
+ innerPad[i] = base ^ 0x36;
+ outerPad[i] = base ^ 0x5c;
+ }
+
+ // Perform inner MD5.
+ hasher.init(Ci.nsICryptoHash.MD5);
+ hasher.update(innerPad, 64);
+ hasher.update(data, data.length);
+ digest = hasher.finish(false);
+
+ let result = this.binaryStringToTypedArray(digest);
+
+ // Perform outer MD5.
+ hasher.init(Ci.nsICryptoHash.MD5);
+ hasher.update(outerPad, 64);
+ hasher.update(result, result.length);
+ digest = hasher.finish(false);
+
+ return this.binaryStringToTypedArray(digest);
+ },
+};
diff --git a/comm/mailnews/base/src/MailNewsDLF.cpp b/comm/mailnews/base/src/MailNewsDLF.cpp
new file mode 100644
index 0000000000..8969992414
--- /dev/null
+++ b/comm/mailnews/base/src/MailNewsDLF.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsCOMPtr.h"
+#include "MailNewsDLF.h"
+#include "nsIChannel.h"
+#include "plstr.h"
+#include "nsString.h"
+#include "nsICategoryManager.h"
+#include "nsIServiceManager.h"
+#include "nsIStreamConverterService.h"
+#include "nsIStreamListener.h"
+#include "nsNetCID.h"
+#include "nsMsgUtils.h"
+
+namespace mozilla {
+namespace mailnews {
+NS_IMPL_ISUPPORTS(MailNewsDLF, nsIDocumentLoaderFactory)
+
+MailNewsDLF::MailNewsDLF() {}
+
+MailNewsDLF::~MailNewsDLF() {}
+
+NS_IMETHODIMP
+MailNewsDLF::CreateInstance(const char* aCommand, nsIChannel* aChannel,
+ nsILoadGroup* aLoadGroup,
+ const nsACString& aContentType,
+ nsIDocShell* aContainer, nsISupports* aExtraInfo,
+ nsIStreamListener** aDocListener,
+ nsIContentViewer** aDocViewer) {
+ nsresult rv;
+
+ bool viewSource =
+ (PL_strstr(PromiseFlatCString(aContentType).get(), "view-source") != 0);
+
+ aChannel->SetContentType(nsLiteralCString(TEXT_HTML));
+
+ // Get the HTML category
+ nsCOMPtr<nsICategoryManager> catMan(
+ do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString contractID;
+ rv = catMan->GetCategoryEntry("Gecko-Content-Viewers", TEXT_HTML, contractID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDocumentLoaderFactory> factory(
+ do_GetService(contractID.get(), &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStreamListener> listener;
+
+ if (viewSource) {
+ rv = factory->CreateInstance(
+ "view-source", aChannel, aLoadGroup,
+ nsLiteralCString(TEXT_HTML "; x-view-type=view-source"), aContainer,
+ aExtraInfo, getter_AddRefs(listener), aDocViewer);
+ } else {
+ rv = factory->CreateInstance(
+ "view", aChannel, aLoadGroup, nsLiteralCString(TEXT_HTML), aContainer,
+ aExtraInfo, getter_AddRefs(listener), aDocViewer);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStreamConverterService> scs(
+ do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return scs->AsyncConvertData(MESSAGE_RFC822, TEXT_HTML, listener, aChannel,
+ aDocListener);
+}
+
+NS_IMETHODIMP
+MailNewsDLF::CreateInstanceForDocument(nsISupports* aContainer,
+ mozilla::dom::Document* aDocument,
+ const char* aCommand,
+ nsIContentViewer** aDocViewer) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+} // namespace mailnews
+} // namespace mozilla
diff --git a/comm/mailnews/base/src/MailNewsDLF.h b/comm/mailnews/base/src/MailNewsDLF.h
new file mode 100644
index 0000000000..95455f4b56
--- /dev/null
+++ b/comm/mailnews/base/src/MailNewsDLF.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MailNewsDLF_h__
+#define MailNewsDLF_h__
+
+#include "nsIDocumentLoaderFactory.h"
+#include "nsMimeTypes.h"
+
+namespace mozilla {
+namespace mailnews {
+
+/*
+ * This factory is a thin wrapper around the text/html loader factory. All it
+ * does is convert message/rfc822 to text/html and delegate the rest of the
+ * work to the text/html factory.
+ */
+class MailNewsDLF : public nsIDocumentLoaderFactory {
+ public:
+ MailNewsDLF();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOCUMENTLOADERFACTORY
+
+ private:
+ virtual ~MailNewsDLF();
+};
+} // namespace mailnews
+} // namespace mozilla
+
+#define MAILNEWSDLF_CATEGORIES \
+ {"Gecko-Content-Viewers", MESSAGE_RFC822, \
+ "@mozilla.org/mailnews/document-loader-factory;1"},
+
+#endif
diff --git a/comm/mailnews/base/src/MailNotificationManager.jsm b/comm/mailnews/base/src/MailNotificationManager.jsm
new file mode 100644
index 0000000000..c16e37eb2f
--- /dev/null
+++ b/comm/mailnews/base/src/MailNotificationManager.jsm
@@ -0,0 +1,478 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["MailNotificationManager"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ WinUnreadBadge: "resource:///modules/WinUnreadBadge.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "l10n",
+ () => new Localization(["messenger/messenger.ftl"])
+);
+
+/**
+ * A module that listens to folder change events, and show notifications for new
+ * mails if necessary.
+ */
+class MailNotificationManager {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIFolderListener",
+ "mozINewMailListener",
+ ]);
+
+ constructor() {
+ this._systemAlertAvailable = true;
+ this._unreadChatCount = 0;
+ this._unreadMailCount = 0;
+ // @type {Map<nsIMsgFolder, number>} - A map of folder and its last biff time.
+ this._folderBiffTime = new Map();
+ // @type {Set<nsIMsgFolder>} - A set of folders to show alert for.
+ this._pendingFolders = new Set();
+
+ this._logger = console.createInstance({
+ prefix: "mail.notification",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.notification.loglevel",
+ });
+ this._bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ MailServices.mailSession.AddFolderListener(
+ this,
+ Ci.nsIFolderListener.intPropertyChanged
+ );
+
+ // Ensure that OS integration is defined before we attempt to initialize the
+ // system tray icon.
+ XPCOMUtils.defineLazyGetter(this, "_osIntegration", () => {
+ try {
+ return Cc["@mozilla.org/messenger/osintegration;1"].getService(
+ Ci.nsIMessengerOSIntegration
+ );
+ } catch (e) {
+ // We don't have OS integration on all platforms.
+ return null;
+ }
+ });
+
+ if (["macosx", "win"].includes(AppConstants.platform)) {
+ // We don't have indicator for unread count on Linux yet.
+ Cc["@mozilla.org/newMailNotificationService;1"]
+ .getService(Ci.mozINewMailNotificationService)
+ .addListener(this, Ci.mozINewMailNotificationService.count);
+
+ Services.obs.addObserver(this, "unread-im-count-changed");
+ Services.obs.addObserver(this, "profile-before-change");
+ }
+
+ if (AppConstants.platform == "macosx") {
+ Services.obs.addObserver(this, "new-directed-incoming-message");
+ }
+
+ if (AppConstants.platform == "win") {
+ Services.obs.addObserver(this, "windows-refresh-badge-tray");
+ Services.prefs.addObserver("mail.biff.show_badge", this);
+ Services.prefs.addObserver("mail.biff.show_tray_icon_always", this);
+ }
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "alertclickcallback":
+ // Display the associated message when an alert is clicked.
+ let msgHdr = Cc["@mozilla.org/messenger;1"]
+ .getService(Ci.nsIMessenger)
+ .msgHdrFromURI(data);
+ lazy.MailUtils.displayMessageInFolderTab(msgHdr, true);
+ return;
+ case "unread-im-count-changed":
+ this._logger.log(
+ `Unread chat count changed to ${this._unreadChatCount}`
+ );
+ this._unreadChatCount = parseInt(data, 10) || 0;
+ this._updateUnreadCount();
+ return;
+ case "new-directed-incoming-messenger":
+ this._animateDockIcon();
+ return;
+ case "windows-refresh-badge-tray":
+ this._updateUnreadCount();
+ return;
+ case "profile-before-change":
+ this._osIntegration?.onExit();
+ return;
+ case "newmailalert-closed":
+ // newmailalert.xhtml is closed, try to show the next queued folder.
+ this._customizedAlertShown = false;
+ this._showCustomizedAlert();
+ return;
+ case "nsPref:changed":
+ if (
+ data == "mail.biff.show_badge" ||
+ data == "mail.biff.show_tray_icon_always"
+ ) {
+ this._updateUnreadCount();
+ }
+ }
+ }
+
+ /**
+ * Following are nsIFolderListener interfaces. Do nothing about them.
+ */
+ onFolderAdded() {}
+ onMessageAdded() {}
+ onFolderRemoved() {}
+ onMessageRemoved() {}
+ onFolderPropertyChanged() {}
+ /**
+ * The only nsIFolderListener interface we care about.
+ *
+ * @see nsIFolderListener
+ */
+ onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
+ if (!Services.prefs.getBoolPref("mail.biff.show_alert")) {
+ return;
+ }
+
+ this._logger.debug(
+ `onFolderIntPropertyChanged; property=${property}: ${oldValue} => ${newValue}, folder.URI=${folder.URI}`
+ );
+
+ switch (property) {
+ case "BiffState":
+ if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) {
+ // The folder argument is a root folder.
+ this._fillAlertInfo(folder);
+ }
+ break;
+ case "NewMailReceived":
+ // The folder argument is a real folder.
+ this._fillAlertInfo(folder);
+ break;
+ }
+ }
+ onFolderBoolPropertyChanged() {}
+ onFolderUnicharPropertyChanged() {}
+ onFolderPropertyFlagChanged() {}
+ onFolderEvent() {}
+
+ /**
+ * @see mozINewMailNotificationService
+ */
+ onCountChanged(count) {
+ this._logger.log(`Unread mail count changed to ${count}`);
+ this._unreadMailCount = count;
+ this._updateUnreadCount();
+ }
+
+ /**
+ * Show an alert according to the changed folder.
+ *
+ * @param {nsIMsgFolder} changedFolder - The folder that emitted the change
+ * event, can be a root folder or a real folder.
+ */
+ async _fillAlertInfo(changedFolder) {
+ let folder = this._getFirstRealFolderWithNewMail(changedFolder);
+ if (!folder) {
+ return;
+ }
+
+ let newMsgKeys = this._getNewMsgKeysNotNotified(folder);
+ let numNewMessages = newMsgKeys.length;
+ if (!numNewMessages) {
+ return;
+ }
+
+ this._logger.debug(
+ `Filling alert info; folder.URI=${folder.URI}, numNewMessages=${numNewMessages}`
+ );
+ let firstNewMsgHdr = folder.msgDatabase.getMsgHdrForKey(newMsgKeys[0]);
+
+ let title = this._getAlertTitle(folder, numNewMessages);
+ let body;
+ try {
+ body = await this._getAlertBody(folder, firstNewMsgHdr);
+ } catch (e) {
+ this._logger.error(e);
+ }
+ if (!title || !body) {
+ return;
+ }
+ this._showAlert(firstNewMsgHdr, title, body);
+ this._animateDockIcon();
+ }
+
+ /**
+ * Iterate the subfolders of changedFolder, return the first real folder with
+ * new mail.
+ *
+ * @param {nsIMsgFolder} changedFolder - The folder that emitted the change event.
+ * @returns {nsIMsgFolder} The first real folder.
+ */
+ _getFirstRealFolderWithNewMail(changedFolder) {
+ let folders = changedFolder.descendants;
+ folders.unshift(changedFolder);
+
+ for (let folder of folders) {
+ let flags = folder.flags;
+ if (
+ !(flags & Ci.nsMsgFolderFlags.Inbox) &&
+ flags & (Ci.nsMsgFolderFlags.SpecialUse | Ci.nsMsgFolderFlags.Virtual)
+ ) {
+ // Do not notify if the folder is not Inbox but one of
+ // Drafts|Trash|SentMail|Templates|Junk|Archive|Queue or Virtual.
+ continue;
+ }
+
+ if (folder.getNumNewMessages(false) > 0) {
+ return folder;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get the title for the alert.
+ *
+ * @param {nsIMsgFolder} folder - The changed folder.
+ * @param {number} numNewMessages - The count of new messages.
+ * @returns {string} The alert title.
+ */
+ _getAlertTitle(folder, numNewMessages) {
+ return this._bundle.formatStringFromName(
+ numNewMessages == 1
+ ? "newMailNotification_message"
+ : "newMailNotification_messages",
+ [folder.server.prettyName, numNewMessages.toString()]
+ );
+ }
+
+ /**
+ * Get the body for the alert.
+ *
+ * @param {nsIMsgFolder} folder - The changed folder.
+ * @param {nsIMsgHdr} msgHdr - The nsIMsgHdr of the first new messages.
+ * @returns {string} The alert body.
+ */
+ async _getAlertBody(folder, msgHdr) {
+ await new Promise((resolve, reject) => {
+ let isAsync = folder.fetchMsgPreviewText([msgHdr.messageKey], {
+ OnStartRunningUrl() {},
+ // @see nsIUrlListener
+ OnStopRunningUrl(url, exitCode) {
+ Components.isSuccessCode(exitCode) ? resolve() : reject();
+ },
+ });
+ if (!isAsync) {
+ resolve();
+ }
+ });
+
+ let alertBody = "";
+
+ let subject = Services.prefs.getBoolPref("mail.biff.alert.show_subject")
+ ? msgHdr.mime2DecodedSubject
+ : "";
+ let author = "";
+ if (Services.prefs.getBoolPref("mail.biff.alert.show_sender")) {
+ let addressObjects = MailServices.headerParser.makeFromDisplayAddress(
+ msgHdr.mime2DecodedAuthor
+ );
+ let { name, email } = addressObjects[0] || {};
+ author = name || email;
+ }
+ if (subject && author) {
+ alertBody += this._bundle.formatStringFromName(
+ "newMailNotification_messagetitle",
+ [subject, author]
+ );
+ } else if (subject) {
+ alertBody += subject;
+ } else if (author) {
+ alertBody += author;
+ }
+ let showPreview = Services.prefs.getBoolPref(
+ "mail.biff.alert.show_preview"
+ );
+ if (showPreview) {
+ let previewLength = Services.prefs.getIntPref(
+ "mail.biff.alert.preview_length",
+ 40
+ );
+ let preview = msgHdr.getStringProperty("preview").slice(0, previewLength);
+ if (preview) {
+ alertBody += (alertBody ? "\n" : "") + preview;
+ }
+ }
+ return alertBody;
+ }
+
+ /**
+ * Show the alert.
+ *
+ * @param {nsIMsgHdr} msgHdr - The nsIMsgHdr of the first new messages.
+ * @param {string} title - The alert title.
+ * @param {string} body - The alert body.
+ */
+ _showAlert(msgHdr, title, body) {
+ let folder = msgHdr.folder;
+
+ // Try to use system alert first.
+ if (
+ Services.prefs.getBoolPref("mail.biff.use_system_alert", true) &&
+ this._systemAlertAvailable
+ ) {
+ let alertsService = Cc["@mozilla.org/system-alerts-service;1"].getService(
+ Ci.nsIAlertsService
+ );
+ let cookie = folder.generateMessageURI(msgHdr.messageKey);
+ try {
+ let alert = Cc["@mozilla.org/alert-notification;1"].createInstance(
+ Ci.nsIAlertNotification
+ );
+ alert.init(
+ cookie, // name
+ "chrome://messenger/skin/icons/new-mail-alert.png",
+ title,
+ body,
+ true, // clickable
+ cookie
+ );
+ alertsService.showAlert(alert, this);
+ return;
+ } catch (e) {
+ this._logger.error(e);
+ this._systemAlertAvailable = false;
+ }
+ }
+
+ // The use_system_alert pref is false or showAlert somehow failed, use the
+ // customized alert window.
+ this._showCustomizedAlert(folder);
+ }
+
+ /**
+ * Show a customized alert window (newmailalert.xhtml), if there is already
+ * one showing, do not show another one, because the newer one will block the
+ * older one. Instead, save the folder and newMsgKeys to this._pendingFolders.
+ *
+ * @param {nsIMsgFolder} [folder] - The folder containing new messages.
+ */
+ _showCustomizedAlert(folder) {
+ if (this._customizedAlertShown) {
+ // Queue the folder.
+ this._pendingFolders.add(folder);
+ return;
+ }
+ if (!folder) {
+ // Get the next folder from the queue.
+ folder = this._pendingFolders.keys().next().value;
+ if (!folder) {
+ return;
+ }
+ this._pendingFolders.delete(folder);
+ }
+
+ let newMsgKeys = this._getNewMsgKeysNotNotified(folder);
+ if (!newMsgKeys.length) {
+ // No NEW message in the current folder, try the next queued folder.
+ this._showCustomizedAlert();
+ return;
+ }
+
+ let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ args.appendElement(folder);
+ args.appendElement({
+ wrappedJSObject: newMsgKeys,
+ });
+ args.appendElement(this);
+ Services.ww.openWindow(
+ null,
+ "chrome://messenger/content/newmailalert.xhtml",
+ "_blank",
+ "chrome,dialog=yes,titlebar=no,popup=yes",
+ args
+ );
+ this._customizedAlertShown = true;
+ this._folderBiffTime.set(folder, Date.now());
+ }
+
+ /**
+ * Get all NEW messages from a folder that we received after last biff time.
+ *
+ * @param {nsIMsgFolder} folder - The message folder to check.
+ * @returns {number[]} An array of message keys.
+ */
+ _getNewMsgKeysNotNotified(folder) {
+ let msgDb = folder.msgDatabase;
+ let lastBiffTime = this._folderBiffTime.get(folder) || 0;
+ return msgDb
+ .getNewList()
+ .slice(-folder.getNumNewMessages(false))
+ .filter(key => {
+ let msgHdr = msgDb.getMsgHdrForKey(key);
+ return msgHdr.dateInSeconds * 1000 > lastBiffTime;
+ });
+ }
+
+ async _updateUnreadCount() {
+ if (this._updatingUnreadCount) {
+ // _updateUnreadCount can be triggered faster than we finish rendering the
+ // badge. When that happens, set a flag and return.
+ this._pendingUpdate = true;
+ return;
+ }
+ this._updatingUnreadCount = true;
+
+ this._logger.debug(
+ `Update unreadMailCount=${this._unreadMailCount}, unreadChatCount=${this._unreadChatCount}`
+ );
+ let count = this._unreadMailCount + this._unreadChatCount;
+ let tooltip = "";
+ if (AppConstants.platform == "win") {
+ if (!Services.prefs.getBoolPref("mail.biff.show_badge", true)) {
+ count = 0;
+ }
+ if (count > 0) {
+ tooltip = await lazy.l10n.formatValue("unread-messages-os-tooltip", {
+ count,
+ });
+ }
+ await lazy.WinUnreadBadge.updateUnreadCount(count, tooltip);
+ }
+ this._osIntegration?.updateUnreadCount(count, tooltip);
+
+ this._updatingUnreadCount = false;
+ if (this._pendingUpdate) {
+ // There was at least one _updateUnreadCount call while we were rendering
+ // the badge. Render one more time will ensure the badge reflects the
+ // current state.
+ this._pendingUpdate = false;
+ this._updateUnreadCount();
+ }
+ }
+
+ _animateDockIcon() {
+ if (Services.prefs.getBoolPref("mail.biff.animate_dock_icon", false)) {
+ Services.wm.getMostRecentWindow("mail:3pane")?.getAttention();
+ }
+ }
+}
diff --git a/comm/mailnews/base/src/MailNotificationService.jsm b/comm/mailnews/base/src/MailNotificationService.jsm
new file mode 100644
index 0000000000..8f2c57aad6
--- /dev/null
+++ b/comm/mailnews/base/src/MailNotificationService.jsm
@@ -0,0 +1,375 @@
+/* -*- 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/. */
+
+/**
+ * Platform-independent code to count new and unread messages and pass the
+ * information to platform-specific notification modules.
+ */
+
+var EXPORTED_SYMBOLS = ["NewMailNotificationService"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * NewMailNotificationService.
+ *
+ * @implements {mozINewMailNotificationService}
+ * @implements {nsIFolderListener}
+ * @implements {nsIObserver}
+ */
+class NewMailNotificationService {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsIFolderListener",
+ "mozINewMailNotificationService",
+ ]);
+
+ #unreadCount = 0;
+ #newCount = 0;
+ #listeners = [];
+ #log = null;
+
+ constructor() {
+ this.#log = console.createInstance({
+ prefix: "mail.notification",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.notification.loglevel",
+ });
+
+ Services.obs.addObserver(this, "profile-before-change");
+ MailServices.mailSession.AddFolderListener(
+ this,
+ Ci.nsIFolderListener.intPropertyChanged |
+ Ci.nsIFolderListener.added |
+ Ci.nsIFolderListener.removed |
+ Ci.nsIFolderListener.propertyFlagChanged
+ );
+ if (!this.useNewCountInBadge) {
+ let total = 0;
+ for (let server of MailServices.accounts.allServers) {
+ // Don't bother counting RSS or NNTP servers
+ let type = server.type;
+ if (type == "rss" || type == "nntp") {
+ continue;
+ }
+
+ let rootFolder = server.rootFolder;
+ if (rootFolder) {
+ total += this.countUnread(rootFolder);
+ }
+ }
+ this.#unreadCount = total;
+ }
+ }
+
+ get useNewCountInBadge() {
+ return Services.prefs.getBoolPref(
+ "mail.biff.use_new_count_in_badge",
+ false
+ );
+ }
+
+ /** Setter. Used for unit tests. */
+ set unreadCount(count) {
+ this.#unreadCount = count;
+ }
+
+ observe(subject, topic, data) {
+ if (topic == "profile-before-change") {
+ try {
+ MailServices.mailSession.RemoveFolderListener(this);
+ Services.obs.removeObserver(this, "profile-before-change");
+ } catch (e) {
+ this.#log.error("Unable to deregister listeners at shutdown: " + e);
+ }
+ }
+ }
+
+ // Count all the unread messages below the given folder
+ countUnread(folder) {
+ this.#log.debug(`countUnread for ${folder.URI}`);
+ let unreadCount = 0;
+
+ let allFolders = [folder, ...folder.descendants];
+ for (let folder of allFolders) {
+ if (this.confirmShouldCount(folder)) {
+ let count = folder.getNumUnread(false);
+ this.#log.debug(`${folder.URI} has ${count} unread`);
+ if (count > 0) {
+ unreadCount += count;
+ }
+ }
+ }
+ return unreadCount;
+ }
+
+ /**
+ * Filter out special folders and then ask for observers to see if
+ * we should monitor unread messages in this folder.
+ *
+ * @param {nsIMsgFolder} aFolder - The folder we're asking about.
+ */
+ confirmShouldCount(aFolder) {
+ let shouldCount = Cc["@mozilla.org/supports-PRBool;1"].createInstance(
+ Ci.nsISupportsPRBool
+ );
+ shouldCount.data = true;
+
+ // If it's not a mail folder we don't count it by default
+ if (!(aFolder.flags & Ci.nsMsgFolderFlags.Mail)) {
+ shouldCount.data = false;
+ } else if (aFolder.server?.type == "rss") {
+ // For whatever reason, RSS folders have the 'Mail' flag.
+ shouldCount.data = false;
+ } else if (
+ aFolder.flags & Ci.nsMsgFolderFlags.SpecialUse &&
+ !(aFolder.flags & Ci.nsMsgFolderFlags.Inbox)
+ ) {
+ // It's a special folder *other than the inbox*, don't count it by default.
+ shouldCount.data = false;
+ } else if (aFolder.flags & Ci.nsMsgFolderFlags.Virtual) {
+ shouldCount.data = false;
+ } else {
+ // If we're only counting inboxes and it's not an inbox...
+ let onlyCountInboxes = Services.prefs.getBoolPref(
+ "mail.notification.count.inbox_only",
+ true
+ );
+ if (onlyCountInboxes && !(aFolder.flags & Ci.nsMsgFolderFlags.Inbox)) {
+ shouldCount.data = false;
+ }
+ }
+
+ this.#log.debug(`${aFolder.URI}: shouldCount=${shouldCount.data}`);
+ Services.obs.notifyObservers(
+ shouldCount,
+ "before-count-unread-for-folder",
+ aFolder.URI
+ );
+ return shouldCount.data;
+ }
+
+ onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
+ try {
+ if (property == "FolderSize") {
+ return;
+ }
+ this.#log.trace(
+ `Changed int ${property} of ${folder.folderURL}: ${oldValue} -> ${newValue}`
+ );
+ if (property == "BiffState") {
+ this.#biffStateChanged(folder, oldValue, newValue);
+ } else if (property == "TotalUnreadMessages") {
+ this.#totalUnreadMessagesChanged(folder, oldValue, newValue);
+ } else if (property == "NewMailReceived") {
+ this.#newMailReceived(folder, oldValue, newValue);
+ }
+ } catch (error) {
+ this.#log.error("onFolderIntPropertyChanged: " + error);
+ }
+ }
+
+ #biffStateChanged(folder, oldValue, newValue) {
+ if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) {
+ if (folder.server && !folder.server.performingBiff) {
+ this.#log.debug(
+ `${folder.URI} notified, but server not performing biff`
+ );
+ return;
+ }
+
+ // Biff notifications come in for the top level of the server, we need to
+ // look for the folder that actually contains the new mail.
+
+ let allFolders = [folder, ...folder.descendants];
+
+ this.#log.debug(`${folder.URI} notified; will check subfolders`);
+ let newCount = 0;
+
+ for (let folder of allFolders) {
+ if (this.confirmShouldCount(folder)) {
+ let folderNew = folder.getNumNewMessages(false);
+ this.#log.debug(`${folder.URI}: ${folderNew} new`);
+ if (folderNew > 0) {
+ newCount += folderNew;
+ }
+ }
+ }
+ if (newCount > 0) {
+ this.#newCount += newCount;
+ this.#log.debug(`${folder.URI}: new mail count ${this.#newCount}`);
+ if (this.useNewCountInBadge) {
+ this._notifyListeners(
+ Ci.mozINewMailNotificationService.count,
+ "onCountChanged",
+ this.#newCount
+ );
+ }
+ }
+ } else if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NoMail) {
+ // Dodgy - when any folder tells us it has no mail, clear all unread mail
+ this.#newCount = 0;
+ this.#log.debug(`${folder.URI}: no new mail`);
+ if (this.useNewCountInBadge) {
+ this._notifyListeners(
+ Ci.mozINewMailNotificationService.count,
+ "onCountChanged",
+ this.#newCount
+ );
+ }
+ }
+ }
+
+ #newMailReceived(folder, oldValue, newValue) {
+ if (!this.confirmShouldCount(folder)) {
+ return;
+ }
+
+ if (!oldValue || oldValue < 0) {
+ oldValue = 0;
+ }
+ this.#newCount += newValue - oldValue;
+ this.#log.debug(`#newMailReceived ${folder.URI} - ${this.#newCount} new`);
+ if (this.useNewCountInBadge) {
+ this._notifyListeners(
+ Ci.mozINewMailNotificationService.count,
+ "onCountChanged",
+ this.#newCount
+ );
+ }
+ }
+
+ #totalUnreadMessagesChanged(folder, oldValue, newValue) {
+ if (!this.confirmShouldCount(folder)) {
+ return;
+ }
+
+ // treat "count unknown" as zero
+ if (oldValue < 0) {
+ oldValue = 0;
+ }
+ if (newValue < 0) {
+ newValue = 0;
+ }
+
+ this.#unreadCount += newValue - oldValue;
+ if (!this.useNewCountInBadge) {
+ this._notifyListeners(
+ Ci.mozINewMailNotificationService.count,
+ "onCountChanged",
+ this.#unreadCount
+ );
+ }
+ }
+
+ onFolderAdded(parentFolder, child) {
+ if (child.rootFolder == child) {
+ this.#log.trace(`Added root folder ${child.folderURL}`);
+ } else {
+ this.#log.trace(
+ `Added child folder ${child.folderURL} to ${parentFolder.folderURL}`
+ );
+ }
+ }
+
+ onMessageAdded(parentFolder, msg) {
+ if (this.confirmShouldCount(msg.folder)) {
+ this.#log.trace(`Added <${msg.messageId}> to ${msg.folder.folderURL}`);
+ }
+ }
+
+ onFolderPropertyFlagChanged(msg, property, oldFlag, newFlag) {
+ if (
+ oldFlag & Ci.nsMsgMessageFlags.New &&
+ !(newFlag & Ci.nsMsgMessageFlags.New)
+ ) {
+ this.#log.trace(
+ `<${msg.messageId}> marked read in ${msg.folder.folderURL}`
+ );
+ } else if (newFlag & Ci.nsMsgMessageFlags.New) {
+ this.#log.trace(
+ `<${msg.messageId}> marked unread in ${msg.folder.folderURL}`
+ );
+ }
+ }
+
+ onFolderRemoved(parentFolder, child) {
+ if (child.rootFolder == child) {
+ this.#log.trace(`Removed root folder ${child.folderURL}`);
+ } else {
+ this.#log.trace(
+ `Removed child folder ${child.folderURL} from ${parentFolder?.folderURL}`
+ );
+ }
+ }
+
+ onMessageRemoved(parentFolder, msg) {
+ if (!msg.isRead) {
+ this.#log.trace(
+ `Removed unread <${msg.messageId}> from ${msg.folder.folderURL}`
+ );
+ }
+ }
+
+ // Implement mozINewMailNotificationService
+
+ get messageCount() {
+ if (this.useNewCountInBadge) {
+ return this.#newCount;
+ }
+ return this.#unreadCount;
+ }
+
+ addListener(aListener, flags) {
+ for (let i = 0; i < this.#listeners.length; i++) {
+ let l = this.#listeners[i];
+ if (l.obj === aListener) {
+ l.flags = flags;
+ return;
+ }
+ }
+
+ // Ensure that first-time listeners get an accurate mail count.
+ if (flags & Ci.mozINewMailNotificationService.count) {
+ const count = this.useNewCountInBadge
+ ? this.#newCount
+ : this.#unreadCount;
+ aListener.onCountChanged(count);
+ }
+
+ // If we get here, the listener wasn't already in the list
+ this.#listeners.push({ obj: aListener, flags });
+ }
+
+ removeListener(aListener) {
+ for (let i = 0; i < this.#listeners.length; i++) {
+ let l = this.#listeners[i];
+ if (l.obj === aListener) {
+ this.#listeners.splice(i, 1);
+ return;
+ }
+ }
+ }
+
+ listenersForFlag(flag) {
+ let list = [];
+ for (let i = 0; i < this.#listeners.length; i++) {
+ let l = this.#listeners[i];
+ if (l.flags & flag) {
+ list.push(l.obj);
+ }
+ }
+ return list;
+ }
+
+ _notifyListeners(flag, func, value) {
+ let list = this.listenersForFlag(flag);
+ for (let i = 0; i < list.length; i++) {
+ list[i][func](value);
+ }
+ }
+}
diff --git a/comm/mailnews/base/src/MailServices.jsm b/comm/mailnews/base/src/MailServices.jsm
new file mode 100644
index 0000000000..f316b16bde
--- /dev/null
+++ b/comm/mailnews/base/src/MailServices.jsm
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["MailServices"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+var MailServices = {
+ /**
+ * Gets the `nsIMsgMessageService` for a given message URI. This should have
+ * the same behaviour as `GetMessageServiceFromURI` (nsMsgUtils.cpp).
+ *
+ * @param {string} uri - The URI of a folder or message.
+ * @returns {nsIMsgMessageService}
+ */
+ messageServiceFromURI(uri) {
+ let index = uri.indexOf(":");
+ if (index == -1) {
+ throw new Components.Exception(
+ `Bad message URI: ${uri}`,
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+
+ let protocol = uri.substring(0, index);
+ if (protocol == "file") {
+ protocol = "mailbox";
+ }
+ return Cc[
+ `@mozilla.org/messenger/messageservice;1?type=${protocol}`
+ ].getService(Ci.nsIMsgMessageService);
+ },
+};
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "mailSession",
+ "@mozilla.org/messenger/services/session;1",
+ "nsIMsgMailSession"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "accounts",
+ "@mozilla.org/messenger/account-manager;1",
+ "nsIMsgAccountManager"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "pop3",
+ "@mozilla.org/messenger/popservice;1",
+ "nsIPop3Service"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "imap",
+ "@mozilla.org/messenger/imapservice;1",
+ "nsIImapService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "nntp",
+ "@mozilla.org/messenger/nntpservice;1",
+ "nsINntpService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "smtp",
+ "@mozilla.org/messengercompose/smtp;1",
+ "nsISmtpService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "compose",
+ "@mozilla.org/messengercompose;1",
+ "nsIMsgComposeService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "ab",
+ "@mozilla.org/abmanager;1",
+ "nsIAbManager"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "copy",
+ "@mozilla.org/messenger/messagecopyservice;1",
+ "nsIMsgCopyService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "mfn",
+ "@mozilla.org/messenger/msgnotificationservice;1",
+ "nsIMsgFolderNotificationService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "headerParser",
+ "@mozilla.org/messenger/headerparser;1",
+ "nsIMsgHeaderParser"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "mimeConverter",
+ "@mozilla.org/messenger/mimeconverter;1",
+ "nsIMimeConverter"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "tags",
+ "@mozilla.org/messenger/tagservice;1",
+ "nsIMsgTagService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "filters",
+ "@mozilla.org/messenger/services/filters;1",
+ "nsIMsgFilterService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "junk",
+ "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter",
+ "nsIJunkMailPlugin"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "newMailNotification",
+ "@mozilla.org/newMailNotificationService;1",
+ "mozINewMailNotificationService"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ MailServices,
+ "folderLookup",
+ "@mozilla.org/mail/folder-lookup;1",
+ "nsIFolderLookupService"
+);
+
+// Clean up all of these references at shutdown, so that they don't appear as
+// a memory leak in test logs.
+Services.obs.addObserver(
+ {
+ observe() {
+ for (let key of Object.keys(MailServices)) {
+ delete MailServices[key];
+ }
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ },
+ },
+ "xpcom-shutdown"
+);
diff --git a/comm/mailnews/base/src/MailStringUtils.jsm b/comm/mailnews/base/src/MailStringUtils.jsm
new file mode 100644
index 0000000000..c892c328e4
--- /dev/null
+++ b/comm/mailnews/base/src/MailStringUtils.jsm
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["MailStringUtils"];
+
+var MailStringUtils = {
+ /**
+ * Convert a ByteString to a Uint8Array.
+ *
+ * @param {ByteString} str - The input string.
+ * @returns {Uint8Array} The output Uint8Array.
+ */
+ byteStringToUint8Array(str) {
+ let arr = new Uint8Array(str.length);
+ for (let i = 0; i < str.length; i++) {
+ arr[i] = str.charCodeAt(i);
+ }
+ return arr;
+ },
+
+ /**
+ * Convert a Uint8Array to a ByteString.
+ *
+ * @param {Uint8Array} arr - The input Uint8Array.
+ * @returns {ByteString} The output string.
+ */
+ uint8ArrayToByteString(arr) {
+ let str = "";
+ for (let i = 0; i < arr.length; i += 65536) {
+ str += String.fromCharCode.apply(null, arr.subarray(i, i + 65536));
+ }
+ return str;
+ },
+
+ /**
+ * Convert a ByteString to a string.
+ *
+ * @param {ByteString} str - The ByteString to convert.
+ * @returns {string} The converted string.
+ */
+ byteStringToString(str) {
+ return new TextDecoder().decode(this.byteStringToUint8Array(str));
+ },
+
+ /**
+ * Convert a string to a ByteString.
+ *
+ * @param {string} str - The string to convert.
+ * @returns {ByteString} The converted ByteString.
+ */
+ stringToByteString(str) {
+ return this.uint8ArrayToByteString(new TextEncoder().encode(str));
+ },
+
+ /**
+ * Detect the text encoding of a ByteString.
+ *
+ * @param {ByteString} str - The input string.
+ * @returns {string} The output charset name.
+ */
+ detectCharset(str) {
+ // Check the BOM.
+ let charset = "";
+ if (str.length >= 2) {
+ let byte0 = str.charCodeAt(0);
+ let byte1 = str.charCodeAt(1);
+ let byte2 = str.charCodeAt(2);
+ if (byte0 == 0xfe && byte1 == 0xff) {
+ charset = "UTF-16BE";
+ } else if (byte0 == 0xff && byte1 == 0xfe) {
+ charset = "UTF-16LE";
+ } else if (byte0 == 0xef && byte1 == 0xbb && byte2 == 0xbf) {
+ charset = "UTF-8";
+ }
+ }
+ if (charset) {
+ return charset;
+ }
+
+ // Use mozilla::EncodingDetector.
+ let compUtils = Cc[
+ "@mozilla.org/messengercompose/computils;1"
+ ].createInstance(Ci.nsIMsgCompUtils);
+ return compUtils.detectCharset(str);
+ },
+
+ /**
+ * Read and detect the charset of a file, then convert the file content to
+ * DOMString. If you're absolutely sure it's a UTF-8 encoded file, use
+ * IOUtils.readUTF8 instead.
+ *
+ * @param {string} path - An absolute file path.
+ * @returns {DOMString} The file content.
+ */
+ async readEncoded(path) {
+ let arr = await IOUtils.read(path);
+ let str = this.uint8ArrayToByteString(arr);
+ let charset = this.detectCharset(str);
+ return new TextDecoder(charset).decode(arr);
+ },
+};
diff --git a/comm/mailnews/base/src/MailnewsLoadContextInfo.cpp b/comm/mailnews/base/src/MailnewsLoadContextInfo.cpp
new file mode 100644
index 0000000000..2e4efc72f5
--- /dev/null
+++ b/comm/mailnews/base/src/MailnewsLoadContextInfo.cpp
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This was copied from netwerk/base/LoadContextInfo.cpp
+
+#include "MailnewsLoadContextInfo.h"
+
+#include "mozilla/dom/ToJSValue.h"
+#include "nsIChannel.h"
+#include "nsILoadContext.h"
+#include "nsIWebNavigation.h"
+#include "nsNetUtil.h"
+
+// MailnewsLoadContextInfo
+
+NS_IMPL_ISUPPORTS(MailnewsLoadContextInfo, nsILoadContextInfo)
+
+MailnewsLoadContextInfo::MailnewsLoadContextInfo(
+ bool aIsPrivate, bool aIsAnonymous,
+ mozilla::OriginAttributes aOriginAttributes)
+ : mIsPrivate(aIsPrivate),
+ mIsAnonymous(aIsAnonymous),
+ mOriginAttributes(aOriginAttributes) {
+ mOriginAttributes.SyncAttributesWithPrivateBrowsing(mIsPrivate);
+}
+
+MailnewsLoadContextInfo::~MailnewsLoadContextInfo() {}
+
+NS_IMETHODIMP MailnewsLoadContextInfo::GetIsPrivate(bool* aIsPrivate) {
+ *aIsPrivate = mIsPrivate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP MailnewsLoadContextInfo::GetIsAnonymous(bool* aIsAnonymous) {
+ *aIsAnonymous = mIsAnonymous;
+ return NS_OK;
+}
+
+mozilla::OriginAttributes const*
+MailnewsLoadContextInfo::OriginAttributesPtr() {
+ return &mOriginAttributes;
+}
+
+NS_IMETHODIMP MailnewsLoadContextInfo::GetOriginAttributes(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aVal) {
+ if (NS_WARN_IF(!ToJSValue(aCx, mOriginAttributes, aVal))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/MailnewsLoadContextInfo.h b/comm/mailnews/base/src/MailnewsLoadContextInfo.h
new file mode 100644
index 0000000000..b44f7ae43b
--- /dev/null
+++ b/comm/mailnews/base/src/MailnewsLoadContextInfo.h
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This was copied from netwerk/base/LoadContextInfo.h
+
+#ifndef MailnewsLoadContextInfo_h__
+#define MailnewsLoadContextInfo_h__
+
+#include "nsILoadContextInfo.h"
+
+class nsIChannel;
+class nsILoadContext;
+
+class MailnewsLoadContextInfo : public nsILoadContextInfo {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSILOADCONTEXTINFO
+
+ MailnewsLoadContextInfo(bool aIsPrivate, bool aIsAnonymous,
+ mozilla::OriginAttributes aOriginAttributes);
+
+ private:
+ virtual ~MailnewsLoadContextInfo();
+
+ protected:
+ bool mIsPrivate : 1;
+ bool mIsAnonymous : 1;
+ mozilla::OriginAttributes mOriginAttributes;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/MailnewsMigrator.jsm b/comm/mailnews/base/src/MailnewsMigrator.jsm
new file mode 100644
index 0000000000..bc6ad9c3ef
--- /dev/null
+++ b/comm/mailnews/base/src/MailnewsMigrator.jsm
@@ -0,0 +1,352 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Migrate profile (prefs and other files) from older versions of Mailnews to
+ * current.
+ * This should be run at startup. It migrates as needed: each migration
+ * function should be written to be a no-op when the value is already migrated
+ * or was never used in the old version.
+ */
+
+const EXPORTED_SYMBOLS = ["migrateMailnews"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const lazy = {};
+ChromeUtils.defineModuleGetter(
+ lazy,
+ "migrateServerUris",
+ "resource:///modules/MsgIncomingServer.jsm"
+);
+
+var kServerPrefVersion = 1;
+var kSmtpPrefVersion = 1;
+var kABRemoteContentPrefVersion = 1;
+
+function migrateMailnews() {
+ let migrations = [
+ migrateProfileClientid,
+ migrateServerAuthPref,
+ migrateServerAndUserName,
+ migrateABRemoteContentSettings,
+ ];
+
+ for (let fn of migrations) {
+ try {
+ fn();
+ } catch (e) {
+ console.error(e);
+ }
+ }
+}
+
+/**
+ * Creates the server specific 'CLIENTID' prefs and tries to pair up any imap
+ * services with smtp services which are using the same username and hostname.
+ */
+function migrateProfileClientid() {
+ // Comma-separated list of all account ids.
+ let accounts = Services.prefs.getCharPref("mail.accountmanager.accounts", "");
+ // Comma-separated list of all smtp servers.
+ let smtpServers = Services.prefs.getCharPref("mail.smtpservers", "");
+ // If both accounts and smtpservers are empty then there is nothing to do.
+ if (accounts.length == 0 && smtpServers.length == 0) {
+ return;
+ }
+ // A cache to allow CLIENTIDs to be stored and shared across services that
+ // share a username and hostname.
+ let clientidCache = new Map();
+ // There may be accounts but no smtpservers so check the length before
+ // trying to split the smtp servers and iterate in the loop below.
+ if (smtpServers.length > 0) {
+ // Now walk all smtp servers and generate any missing CLIENTIDS, caching
+ // all CLIENTIDS along the way to be reused for matching imap servers
+ // if possible.
+
+ // Since the length of the smtpServers string is non-zero then we can split
+ // the string by comma and iterate each entry in the comma-separated list.
+ for (let key of smtpServers.split(",")) {
+ let server = "mail.smtpserver." + key + ".";
+ if (
+ !Services.prefs.prefHasUserValue(server + "clientid") ||
+ !Services.prefs.getCharPref(server + "clientid", "")
+ ) {
+ // Always give outgoing servers a new unique CLIENTID.
+ let newClientid = Services.uuid
+ .generateUUID()
+ .toString()
+ .replace(/[{}]/g, "");
+ Services.prefs.setCharPref(server + "clientid", newClientid);
+ }
+ let username = Services.prefs.getCharPref(server + "username", "");
+ if (!username) {
+ // Not all SMTP servers require a username.
+ continue;
+ }
+
+ // Cache all CLIENTIDs from all outgoing servers to reuse them for any
+ // incoming servers which have a matching username and hostname.
+ let hostname = Services.prefs.getCharPref(server + "hostname");
+ let combinedKey;
+ try {
+ combinedKey =
+ username + "@" + Services.eTLD.getBaseDomainFromHost(hostname);
+ } catch (e) {
+ combinedKey = username + "@" + hostname;
+ }
+ clientidCache.set(
+ combinedKey,
+ Services.prefs.getCharPref(server + "clientid")
+ );
+ }
+ }
+
+ // Now walk all imap accounts and generate any missing CLIENTIDS, reusing
+ // cached CLIENTIDS if possible.
+ for (let key of accounts.split(",")) {
+ let serverKey = Services.prefs.getCharPref(
+ "mail.account." + key + ".server"
+ );
+ let server = "mail.server." + serverKey + ".";
+ // Check if this imap server needs the CLIENTID preference to be populated.
+ if (
+ !Services.prefs.prefHasUserValue(server + "clientid") ||
+ !Services.prefs.getCharPref(server + "clientid", "")
+ ) {
+ // Clientid should only be provisioned for imap accounts.
+ if (Services.prefs.getCharPref(server + "type", "") != "imap") {
+ continue;
+ }
+ // Grab username + hostname to check if a CLIENTID is cached.
+ let username = Services.prefs.getCharPref(server + "userName", "");
+ if (!username) {
+ continue;
+ }
+ let hostname = Services.prefs.getCharPref(server + "hostname");
+ let combinedKey;
+ try {
+ combinedKey =
+ username + "@" + Services.eTLD.getBaseDomainFromHost(hostname);
+ } catch (e) {
+ combinedKey = username + "@" + hostname;
+ }
+ if (!clientidCache.has(combinedKey)) {
+ // Generate a new CLIENTID if no matches were found from smtp servers.
+ let newClientid = Services.uuid
+ .generateUUID()
+ .toString()
+ .replace(/[{}]/g, "");
+ Services.prefs.setCharPref(server + "clientid", newClientid);
+ } else {
+ // Otherwise if a cached CLIENTID was found for this username + hostname
+ // then we can just use the outgoing CLIENTID which was matching.
+ Services.prefs.setCharPref(
+ server + "clientid",
+ clientidCache.get(combinedKey)
+ );
+ }
+ }
+ }
+}
+
+/**
+ * Migrates from pref useSecAuth to pref authMethod
+ */
+function migrateServerAuthPref() {
+ // comma-separated list of all accounts.
+ var accounts = Services.prefs
+ .getCharPref("mail.accountmanager.accounts")
+ .split(",");
+ for (let i = 0; i < accounts.length; i++) {
+ let accountKey = accounts[i]; // e.g. "account1"
+ if (!accountKey) {
+ continue;
+ }
+ let serverKey = Services.prefs.getCharPref(
+ "mail.account." + accountKey + ".server"
+ );
+ let server = "mail.server." + serverKey + ".";
+ if (Services.prefs.prefHasUserValue(server + "authMethod")) {
+ continue;
+ }
+ if (
+ !Services.prefs.prefHasUserValue(server + "useSecAuth") &&
+ !Services.prefs.prefHasUserValue(server + "auth_login")
+ ) {
+ continue;
+ }
+ if (Services.prefs.prefHasUserValue(server + "migrated")) {
+ continue;
+ }
+ // auth_login = false => old-style auth
+ // else: useSecAuth = true => "secure auth"
+ // else: cleartext pw
+ let auth_login = Services.prefs.getBoolPref(server + "auth_login", true);
+ // old default, default pref now removed
+ let useSecAuth = Services.prefs.getBoolPref(server + "useSecAuth", false);
+
+ if (auth_login) {
+ if (useSecAuth) {
+ Services.prefs.setIntPref(
+ server + "authMethod",
+ Ci.nsMsgAuthMethod.secure
+ );
+ } else {
+ Services.prefs.setIntPref(
+ server + "authMethod",
+ Ci.nsMsgAuthMethod.passwordCleartext
+ );
+ }
+ } else {
+ Services.prefs.setIntPref(server + "authMethod", Ci.nsMsgAuthMethod.old);
+ }
+ Services.prefs.setIntPref(server + "migrated", kServerPrefVersion);
+ }
+
+ // same again for SMTP servers
+ var smtpservers = Services.prefs.getCharPref("mail.smtpservers").split(",");
+ for (let i = 0; i < smtpservers.length; i++) {
+ if (!smtpservers[i]) {
+ continue;
+ }
+ let server = "mail.smtpserver." + smtpservers[i] + ".";
+ if (Services.prefs.prefHasUserValue(server + "authMethod")) {
+ continue;
+ }
+ if (
+ !Services.prefs.prefHasUserValue(server + "useSecAuth") &&
+ !Services.prefs.prefHasUserValue(server + "auth_method")
+ ) {
+ continue;
+ }
+ if (Services.prefs.prefHasUserValue(server + "migrated")) {
+ continue;
+ }
+ // auth_method = 0 => no auth
+ // else: useSecAuth = true => "secure auth"
+ // else: cleartext pw
+ let auth_method = Services.prefs.getIntPref(server + "auth_method", 1);
+ let useSecAuth = Services.prefs.getBoolPref(server + "useSecAuth", false);
+
+ if (auth_method) {
+ if (useSecAuth) {
+ Services.prefs.setIntPref(
+ server + "authMethod",
+ Ci.nsMsgAuthMethod.secure
+ );
+ } else {
+ Services.prefs.setIntPref(
+ server + "authMethod",
+ Ci.nsMsgAuthMethod.passwordCleartext
+ );
+ }
+ } else {
+ Services.prefs.setIntPref(server + "authMethod", Ci.nsMsgAuthMethod.none);
+ }
+ Services.prefs.setIntPref(server + "migrated", kSmtpPrefVersion);
+ }
+}
+
+/**
+ * For each mail.server.key. branch,
+ * - migrate realhostname to hostname
+ * - migrate realuserName to userName
+ */
+function migrateServerAndUserName() {
+ let branch = Services.prefs.getBranch("mail.server.");
+
+ // Collect all the server keys.
+ let keySet = new Set();
+ for (let name of branch.getChildList("")) {
+ keySet.add(name.split(".")[0]);
+ }
+ keySet.delete("default");
+
+ for (let key of keySet) {
+ let type = branch.getCharPref(`${key}.type`, "");
+ let hostname = branch.getCharPref(`${key}.hostname`, "");
+ let username = branch.getCharPref(`${key}.userName`, "");
+ let realHostname = branch.getCharPref(`${key}.realhostname`, "");
+ if (realHostname) {
+ branch.setCharPref(`${key}.hostname`, realHostname);
+ branch.clearUserPref(`${key}.realhostname`);
+ }
+ let realUsername = branch.getCharPref(`${key}.realuserName`, "");
+ if (realUsername) {
+ branch.setCharPref(`${key}.userName`, realUsername);
+ branch.clearUserPref(`${key}.realuserName`);
+ }
+ // Previously, when hostname/username changed, LoginManager and many prefs
+ // still contain the old hostname/username, try to migrate them to use the
+ // new hostname/username.
+ if (
+ ["imap", "pop3", "nntp"].includes(type) &&
+ (realHostname || realUsername)
+ ) {
+ let localStoreType = { imap: "imap", pop3: "mailbox", nntp: "news" }[
+ type
+ ];
+ lazy.migrateServerUris(
+ localStoreType,
+ hostname,
+ username,
+ realHostname || hostname,
+ realUsername || username
+ );
+ }
+ }
+}
+
+/**
+ * The address book used to contain information about whether to allow remote
+ * content for a given contact. Now we use the permission manager for that.
+ * Do a one-time migration for it.
+ */
+function migrateABRemoteContentSettings() {
+ if (Services.prefs.prefHasUserValue("mail.ab_remote_content.migrated")) {
+ return;
+ }
+
+ // Search through all of our local address books looking for a match.
+ for (let addrbook of MailServices.ab.directories) {
+ let migrateAddress = function (aEmail) {
+ let uri = Services.io.newURI(
+ "chrome://messenger/content/email=" + aEmail
+ );
+ Services.perms.addFromPrincipal(
+ Services.scriptSecurityManager.createContentPrincipal(uri, {}),
+ "image",
+ Services.perms.ALLOW_ACTION
+ );
+ };
+
+ try {
+ // If it's a read-only book, don't try to find a card as we we could never
+ // have set the AllowRemoteContent property.
+ if (addrbook.readOnly) {
+ continue;
+ }
+
+ for (let card of addrbook.childCards) {
+ if (card.getProperty("AllowRemoteContent", "0") == "0") {
+ // Not allowed for this contact.
+ continue;
+ }
+
+ for (let emailAddress of card.emailAddresses) {
+ migrateAddress(emailAddress);
+ }
+ }
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ Services.prefs.setIntPref(
+ "mail.ab_remote_content.migrated",
+ kABRemoteContentPrefVersion
+ );
+}
diff --git a/comm/mailnews/base/src/MsgAsyncPrompter.jsm b/comm/mailnews/base/src/MsgAsyncPrompter.jsm
new file mode 100644
index 0000000000..e04e9a9418
--- /dev/null
+++ b/comm/mailnews/base/src/MsgAsyncPrompter.jsm
@@ -0,0 +1,621 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["MsgAsyncPrompter", "MsgAuthPrompt"];
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const LoginInfo = Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1",
+ "nsILoginInfo",
+ "init"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Deprecated: "resource://gre/modules/Deprecated.sys.mjs",
+ PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "dialogsBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://global/locale/commonDialogs.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "passwordsBundle", function () {
+ return Services.strings.createBundle(
+ "chrome://passwordmgr/locale/passwordmgr.properties"
+ );
+});
+
+XPCOMUtils.defineLazyGetter(lazy, "brandFullName", function () {
+ return Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandFullName");
+});
+
+function runnablePrompter(asyncPrompter, hashKey) {
+ this._asyncPrompter = asyncPrompter;
+ this._hashKey = hashKey;
+}
+
+runnablePrompter.prototype = {
+ _asyncPrompter: null,
+ _hashKey: null,
+
+ _promiseAuthPrompt(listener) {
+ return new Promise((resolve, reject) => {
+ try {
+ listener.onPromptStartAsync({ onAuthResult: resolve });
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED) {
+ // Fall back to onPromptStart, for add-ons compat
+ lazy.Deprecated.warning(
+ "onPromptStart has been replaced by onPromptStartAsync",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=1176399"
+ );
+ let ok = listener.onPromptStart();
+ resolve(ok);
+ } else {
+ reject(e);
+ }
+ }
+ });
+ },
+
+ async run() {
+ await Services.logins.initializationPromise;
+ this._asyncPrompter._log.debug("Running prompt for " + this._hashKey);
+ let prompter = this._asyncPrompter._pendingPrompts[this._hashKey];
+ let ok = false;
+ try {
+ ok = await this._promiseAuthPrompt(prompter.first);
+ } catch (ex) {
+ console.error("runnablePrompter:run: " + ex + "\n");
+ prompter.first.onPromptCanceled();
+ }
+
+ delete this._asyncPrompter._pendingPrompts[this._hashKey];
+
+ for (var consumer of prompter.consumers) {
+ try {
+ if (ok) {
+ consumer.onPromptAuthAvailable();
+ } else {
+ consumer.onPromptCanceled();
+ }
+ } catch (ex) {
+ // Log the error for extension devs and others to pick up.
+ console.error(
+ "runnablePrompter:run: consumer.onPrompt* reported an exception: " +
+ ex +
+ "\n"
+ );
+ }
+ }
+ this._asyncPrompter._asyncPromptInProgress--;
+
+ this._asyncPrompter._log.debug(
+ "Finished running prompter for " + this._hashKey
+ );
+ this._asyncPrompter._doAsyncAuthPrompt();
+ },
+};
+
+function MsgAsyncPrompter() {
+ this._pendingPrompts = {};
+ // By default, only log warnings to the error console
+ // You can use the preference:
+ // msgAsyncPrompter.loglevel
+ // To change this up. Values should be one of:
+ // Fatal/Error/Warn/Info/Config/Debug/Trace/All
+ this._log = console.createInstance({
+ prefix: "mail.asyncprompter",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.asyncprompter.loglevel",
+ });
+}
+
+MsgAsyncPrompter.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgAsyncPrompter"]),
+
+ _pendingPrompts: null,
+ _asyncPromptInProgress: 0,
+ _log: null,
+
+ queueAsyncAuthPrompt(aKey, aJumpQueue, aCaller) {
+ if (aKey in this._pendingPrompts) {
+ this._log.debug(
+ "Prompt bound to an existing one in the queue, key: " + aKey
+ );
+ this._pendingPrompts[aKey].consumers.push(aCaller);
+ return;
+ }
+
+ this._log.debug("Adding new prompt to the queue, key: " + aKey);
+ let asyncPrompt = {
+ first: aCaller,
+ consumers: [],
+ };
+
+ this._pendingPrompts[aKey] = asyncPrompt;
+ if (aJumpQueue) {
+ this._asyncPromptInProgress++;
+
+ this._log.debug("Forcing runnablePrompter for " + aKey);
+
+ let runnable = new runnablePrompter(this, aKey);
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ } else {
+ this._doAsyncAuthPrompt();
+ }
+ },
+
+ _doAsyncAuthPrompt() {
+ if (this._asyncPromptInProgress > 0) {
+ this._log.debug(
+ "_doAsyncAuthPrompt bypassed - prompt already in progress"
+ );
+ return;
+ }
+
+ // Find the first prompt key we have in the queue.
+ let hashKey = null;
+ for (hashKey in this._pendingPrompts) {
+ break;
+ }
+
+ if (!hashKey) {
+ return;
+ }
+
+ this._asyncPromptInProgress++;
+
+ this._log.debug("Dispatching runnablePrompter for " + hashKey);
+
+ let runnable = new runnablePrompter(this, hashKey);
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+};
+
+/**
+ * An implementation of nsIAuthPrompt which is roughly the same as
+ * LoginManagerAuthPrompter was before the check box option was removed from
+ * nsIPromptService.
+ *
+ * Calls our own version of promptUsernameAndPassword/promptPassword, which
+ * directly open the prompt.
+ *
+ * @implements {nsIAuthPrompt}
+ */
+class MsgAuthPrompt {
+ QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt"]);
+
+ _getFormattedOrigin(aURI) {
+ let uri;
+ if (aURI instanceof Ci.nsIURI) {
+ uri = aURI;
+ } else {
+ uri = Services.io.newURI(aURI);
+ }
+
+ return uri.scheme + "://" + uri.displayHostPort;
+ }
+
+ _getRealmInfo(aRealmString) {
+ let httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString)) {
+ return [null, null, null];
+ }
+
+ let uri = Services.io.newURI(aRealmString);
+ let pathname = "";
+
+ if (uri.pathQueryRef != "/") {
+ pathname = uri.pathQueryRef;
+ }
+
+ let formattedOrigin = this._getFormattedOrigin(uri);
+
+ return [formattedOrigin, formattedOrigin + pathname, uri.username];
+ }
+
+ _getLocalizedString(key, formatArgs) {
+ if (formatArgs) {
+ return lazy.passwordsBundle.formatStringFromName(key, formatArgs);
+ }
+ return lazy.passwordsBundle.GetStringFromName(key);
+ }
+
+ /**
+ * Wrapper around the prompt service prompt. Saving random fields here
+ * doesn't really make sense and therefore isn't implemented.
+ */
+ prompt(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aDefaultText,
+ aResult
+ ) {
+ if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) {
+ throw new Components.Exception(
+ "prompt only supports SAVE_PASSWORD_NEVER",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ if (aDefaultText) {
+ aResult.value = aDefaultText;
+ }
+
+ return Services.prompt.prompt(
+ this._chromeWindow,
+ aDialogTitle,
+ aText,
+ aResult,
+ null,
+ {}
+ );
+ }
+
+ /**
+ * Looks up a username and password in the database. Will prompt the user
+ * with a dialog, even if a username and password are found.
+ */
+ promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aUsername,
+ aPassword
+ ) {
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
+ throw new Components.Exception(
+ "promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ let checkBox = { value: false };
+ let checkBoxLabel = null;
+ let [origin, realm] = this._getRealmInfo(aPasswordRealm);
+
+ // If origin is null, we can't save this login.
+ if (origin) {
+ let canRememberLogin =
+ aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
+ Services.logins.getLoginSavingEnabled(origin);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin) {
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+ }
+
+ for (let login of Services.logins.findLogins(origin, null, realm)) {
+ if (login.username == aUsername.value) {
+ checkBox.value = true;
+ aUsername.value = login.username;
+ // If the caller provided a password, prefer it.
+ if (!aPassword.value) {
+ aPassword.value = login.password;
+ }
+ }
+ }
+ }
+
+ let ok = nsIPrompt_promptUsernameAndPassword(
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (!ok || !checkBox.value || !origin) {
+ return ok;
+ }
+
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ aUsername.value,
+ aPassword.value
+ );
+ Services.logins.addLogin(newLogin);
+
+ return ok;
+ }
+
+ /**
+ * If a password is found in the database for the password realm, it is
+ * returned straight away without displaying a dialog.
+ *
+ * If a password is not found in the database, the user will be prompted
+ * with a dialog with a text field and ok/cancel buttons. If the user
+ * allows it, then the password will be saved in the database.
+ */
+ promptPassword(
+ aDialogTitle,
+ aText,
+ aPasswordRealm,
+ aSavePassword,
+ aPassword
+ ) {
+ if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) {
+ throw new Components.Exception(
+ "promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION",
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ let checkBox = { value: false };
+ let checkBoxLabel = null;
+ let [origin, realm, username] = this._getRealmInfo(aPasswordRealm);
+
+ username = decodeURIComponent(username);
+
+ // If origin is null, we can't save this login.
+ if (origin) {
+ let canRememberLogin =
+ aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY &&
+ Services.logins.getLoginSavingEnabled(origin);
+
+ // if checkBoxLabel is null, the checkbox won't be shown at all.
+ if (canRememberLogin) {
+ checkBoxLabel = this._getLocalizedString("rememberPassword");
+ }
+
+ if (!aPassword.value) {
+ // Look for existing logins.
+ for (let login of Services.logins.findLogins(origin, null, realm)) {
+ if (login.username == username) {
+ aPassword.value = login.password;
+ return true;
+ }
+ }
+ }
+ }
+
+ let ok = nsIPrompt_promptPassword(
+ aDialogTitle,
+ aText,
+ aPassword,
+ checkBoxLabel,
+ checkBox
+ );
+
+ if (ok && checkBox.value && origin && aPassword.value) {
+ let newLogin = new LoginInfo(
+ origin,
+ null,
+ realm,
+ username,
+ aPassword.value
+ );
+
+ Services.logins.addLogin(newLogin);
+ }
+
+ return ok;
+ }
+
+ /**
+ * Implements nsIPrompt.promptPassword as it was before the check box option
+ * was removed.
+ *
+ * Puts up a dialog with a password field and an optional, labelled checkbox.
+ *
+ * @param {string} dialogTitle - Text to appear in the title of the dialog.
+ * @param {string} text - Text to appear in the body of the dialog.
+ * @param {?object} password - Contains the default value for the password
+ * field when this method is called (null value is ok).
+ * Upon return, if the user pressed OK, then this parameter contains a
+ * newly allocated string value.
+ * Otherwise, the parameter's value is unmodified.
+ * @param {?string} checkMsg - Text to appear with the checkbox. If null,
+ * check box will not be shown.
+ * @param {?object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state after
+ * this method returns.
+ *
+ * @returns {boolean} true for OK, false for Cancel.
+ */
+ promptPassword2(dialogTitle, text, password, checkMsg, checkValue) {
+ return nsIPrompt_promptPassword(
+ dialogTitle,
+ text,
+ password,
+ checkMsg,
+ checkValue
+ );
+ }
+
+ /**
+ * Requests a username and a password. Implementations will commonly show a
+ * dialog with a username and password field, depending on flags also a
+ * domain field.
+ *
+ * @param {nsIChannel} channel - The channel that requires authentication.
+ * @param {number} level - One of the level constants from nsIAuthPrompt2.
+ * See there for descriptions of the levels.
+ * @param {nsIAuthInformation} authInfo - Authentication information object.
+ * The implementation should fill in this object with the information
+ * entered by the user before returning.
+ * @param {string} checkboxLabel
+ * Text to appear with the checkbox. If null, check box will not be shown.
+ * @param {object} checkValue
+ * Contains the initial checked state of the checkbox when this method
+ * is called and the final checked state after this method returns.
+ * @returns {boolean} true for OK, false for Cancel.
+ */
+ promptAuth(channel, level, authInfo, checkboxLabel, checkValue) {
+ let title = lazy.dialogsBundle.formatStringFromName(
+ "PromptUsernameAndPassword3",
+ [lazy.brandFullName]
+ );
+ let text = lazy.dialogsBundle.formatStringFromName(
+ "EnterUserPasswordFor2",
+ [`${channel.URI.scheme}://${channel.URI.host}`]
+ );
+
+ let username = { value: authInfo.username || "" };
+ let password = { value: authInfo.password || "" };
+
+ let ok = nsIPrompt_promptUsernameAndPassword(
+ title,
+ text,
+ username,
+ password,
+ checkboxLabel,
+ checkValue
+ );
+
+ if (ok) {
+ authInfo.username = username.value;
+ authInfo.password = password.value;
+ }
+
+ return ok;
+ }
+}
+
+/**
+ * @param {string} dialogTitle - Text to appear in the title of the dialog.
+ * @param {string} text - Text to appear in the body of the dialog.
+ * @param {?object} username
+ * Contains the default value for the username field when this method
+ * is called (null value is ok). Upon return, if the user pressed OK,
+ * then this parameter contains a newly allocated string value.
+ * @param {?object} password - Contains the default value for the password
+ * field when this method is called (null value is ok).
+ * Upon return, if the user pressed OK, then this parameter contains a
+ * newly allocated string value.
+ * Otherwise, the parameter's value is unmodified.
+ * @param {?string} checkMsg - Text to appear with the checkbox. If null,
+ * check box will not be shown.
+ * @param {?object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state after
+ * this method returns.
+ * @returns {boolean} true for OK, false for Cancel.
+ */
+function nsIPrompt_promptUsernameAndPassword(
+ dialogTitle,
+ text,
+ username,
+ password,
+ checkMsg,
+ checkValue
+) {
+ if (!dialogTitle) {
+ dialogTitle = lazy.dialogsBundle.formatStringFromName(
+ "PromptUsernameAndPassword3",
+ [lazy.brandFullName]
+ );
+ }
+
+ let args = {
+ promptType: "promptUserAndPass",
+ title: dialogTitle,
+ text,
+ user: username.value,
+ pass: password.value,
+ checkLabel: checkMsg,
+ checked: checkValue.value,
+ ok: false,
+ };
+
+ let propBag = lazy.PromptUtils.objectToPropBag(args);
+ Services.ww.openWindow(
+ Services.ww.activeWindow,
+ "chrome://global/content/commonDialog.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ propBag
+ );
+ lazy.PromptUtils.propBagToObject(propBag, args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ username.value = args.user;
+ password.value = args.pass;
+ }
+
+ return ok;
+}
+
+/**
+ * Implements nsIPrompt.promptPassword as it was before the check box option
+ * was removed.
+ *
+ * Puts up a dialog with a password field and an optional, labelled checkbox.
+ *
+ * @param {string} dialogTitle - Text to appear in the title of the dialog.
+ * @param {string} text - Text to appear in the body of the dialog.
+ * @param {?object} password - Contains the default value for the password
+ * field when this method is called (null value is ok).
+ * Upon return, if the user pressed OK, then this parameter contains a
+ * newly allocated string value.
+ * Otherwise, the parameter's value is unmodified.
+ * @param {?string} checkMsg - Text to appear with the checkbox. If null,
+ * check box will not be shown.
+ * @param {?object} checkValue - Contains the initial checked state of the
+ * checkbox when this method is called and the final checked state after
+ * this method returns.
+ *
+ * @returns {boolean} true for OK, false for Cancel.
+ */
+function nsIPrompt_promptPassword(
+ dialogTitle,
+ text,
+ password,
+ checkMsg,
+ checkValue
+) {
+ if (!dialogTitle) {
+ dialogTitle = lazy.dialogsBundle.formatStringFromName(
+ "PromptUsernameAndPassword3",
+ [lazy.brandFullName]
+ );
+ }
+
+ let args = {
+ promptType: "promptPassword",
+ title: dialogTitle,
+ text,
+ pass: password.value,
+ checkLabel: checkMsg,
+ checked: checkValue.value,
+ ok: false,
+ };
+
+ let propBag = lazy.PromptUtils.objectToPropBag(args);
+ Services.ww.openWindow(
+ Services.ww.activeWindow,
+ "chrome://global/content/commonDialog.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ propBag
+ );
+ lazy.PromptUtils.propBagToObject(propBag, args);
+
+ // Did user click Ok or Cancel?
+ let ok = args.ok;
+ if (ok) {
+ checkValue.value = args.checked;
+ password.value = args.pass;
+ }
+
+ return ok;
+}
diff --git a/comm/mailnews/base/src/MsgDBCacheManager.jsm b/comm/mailnews/base/src/MsgDBCacheManager.jsm
new file mode 100644
index 0000000000..9506f08e5e
--- /dev/null
+++ b/comm/mailnews/base/src/MsgDBCacheManager.jsm
@@ -0,0 +1,185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Message DB Cache manager
+ */
+
+/* :::::::: Constants and Helpers ::::::::::::::: */
+
+const EXPORTED_SYMBOLS = ["msgDBCacheManager"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var log = console.createInstance({
+ prefix: "mailnews.database.dbcache",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.database.dbcache.loglevel",
+});
+
+/**
+ */
+var DBCACHE_INTERVAL_DEFAULT_MS = 60000; // 1 minute
+
+/* :::::::: The Module ::::::::::::::: */
+
+var msgDBCacheManager = {
+ _initialized: false,
+
+ _msgDBCacheTimer: null,
+
+ _msgDBCacheTimerIntervalMS: DBCACHE_INTERVAL_DEFAULT_MS,
+
+ _dbService: null,
+
+ /**
+ * This is called on startup
+ */
+ init() {
+ if (this._initialized) {
+ return;
+ }
+
+ this._dbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+ );
+
+ // we listen for "quit-application-granted" instead of
+ // "quit-application-requested" because other observers of the
+ // latter can cancel the shutdown.
+ Services.obs.addObserver(this, "quit-application-granted");
+
+ this.startPeriodicCheck();
+
+ this._initialized = true;
+ },
+
+ /* ........ Timer Callback ................*/
+
+ _dbCacheCheckTimerCallback() {
+ msgDBCacheManager.checkCachedDBs();
+ },
+
+ /* ........ Observer Notification Handler ................*/
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ // This is observed before any windows start unloading if something other
+ // than the last 3pane window closing requested the application be
+ // shutdown. For example, when the user quits via the file menu.
+ case "quit-application-granted":
+ Services.obs.removeObserver(this, "quit-application-granted");
+ this.stopPeriodicCheck();
+ break;
+ }
+ },
+
+ /* ........ Public API ................*/
+
+ /**
+ * Stops db cache check
+ */
+ stopPeriodicCheck() {
+ if (this._dbCacheCheckTimer) {
+ this._dbCacheCheckTimer.cancel();
+
+ delete this._dbCacheCheckTimer;
+ this._dbCacheCheckTimer = null;
+ }
+ },
+
+ /**
+ * Starts periodic db cache check
+ */
+ startPeriodicCheck() {
+ if (!this._dbCacheCheckTimer) {
+ this._dbCacheCheckTimer = Cc["@mozilla.org/timer;1"].createInstance(
+ Ci.nsITimer
+ );
+
+ this._dbCacheCheckTimer.initWithCallback(
+ this._dbCacheCheckTimerCallback,
+ this._msgDBCacheTimerIntervalMS,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+ }
+ },
+
+ /**
+ * Checks if any DBs need to be closed due to inactivity or too many of them open.
+ */
+ checkCachedDBs() {
+ let idleLimit = Services.prefs.getIntPref("mail.db.idle_limit");
+ let maxOpenDBs = Services.prefs.getIntPref("mail.db.max_open");
+
+ // db.lastUseTime below is in microseconds while Date.now and idleLimit pref
+ // is in milliseconds.
+ let closeThreshold = (Date.now() - idleLimit) * 1000;
+ let cachedDBs = this._dbService.openDBs;
+ log.info(
+ "Periodic check of cached folder databases (DBs), count=" +
+ cachedDBs.length
+ );
+ // Count databases that are already closed or get closed now due to inactivity.
+ let numClosing = 0;
+ // Count databases whose folder is open in a window.
+ let numOpenInWindow = 0;
+ let dbs = [];
+ for (let db of cachedDBs) {
+ if (!db.folder?.databaseOpen) {
+ // The DB isn't really open anymore.
+ log.debug("Skipping, DB not open for folder: " + db.folder?.name);
+ numClosing++;
+ continue;
+ }
+
+ if (MailServices.mailSession.IsFolderOpenInWindow(db.folder)) {
+ // The folder is open in a window so this DB must not be closed.
+ log.debug("Skipping, DB open in window for folder: " + db.folder.name);
+ numOpenInWindow++;
+ continue;
+ }
+
+ if (db.lastUseTime < closeThreshold) {
+ // DB open too log without activity.
+ log.debug("Closing expired DB for folder: " + db.folder.name);
+ db.folder.msgDatabase = null;
+ numClosing++;
+ continue;
+ }
+
+ // Database eligible for closing.
+ dbs.push(db);
+ }
+ log.info(
+ "DBs open in a window: " +
+ numOpenInWindow +
+ ", DBs open: " +
+ dbs.length +
+ ", DBs already closing: " +
+ numClosing
+ );
+ let dbsToClose = Math.max(
+ dbs.length - Math.max(maxOpenDBs - numOpenInWindow, 0),
+ 0
+ );
+ if (dbsToClose > 0) {
+ // Close some DBs so that we do not have more than maxOpenDBs.
+ // However, we skipped DBs for folders that are open in a window
+ // so if there are so many windows open, it may be possible for
+ // more than maxOpenDBs folders to stay open after this loop.
+ log.info("Need to close " + dbsToClose + " more DBs");
+ // Order databases by lowest lastUseTime (oldest) at the end.
+ dbs.sort((a, b) => b.lastUseTime - a.lastUseTime);
+ while (dbsToClose > 0) {
+ let db = dbs.pop();
+ log.debug("Closing DB for folder: " + db.folder.name);
+ db.folder.msgDatabase = null;
+ dbsToClose--;
+ }
+ }
+ },
+};
diff --git a/comm/mailnews/base/src/MsgIncomingServer.jsm b/comm/mailnews/base/src/MsgIncomingServer.jsm
new file mode 100644
index 0000000000..768fe9340e
--- /dev/null
+++ b/comm/mailnews/base/src/MsgIncomingServer.jsm
@@ -0,0 +1,1268 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["migrateServerUris", "MsgIncomingServer"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * When hostname/username changes, update the corresponding entry in
+ * nsILoginManager.
+ *
+ * @param {string} localStoreType - The store type of the current server.
+ * @param {string} oldHostname - The hostname before the change.
+ * @param {string} oldUsername - The username before the change.
+ * @param {string} newHostname - The hostname after the change.
+ * @param {string} newUsername - The username after the change.
+ */
+function migratePassword(
+ localStoreType,
+ oldHostname,
+ oldUsername,
+ newHostname,
+ newUsername
+) {
+ // When constructing nsIURI, need to wrap IPv6 address in [].
+ oldHostname = oldHostname.includes(":") ? `[${oldHostname}]` : oldHostname;
+ let oldServerUri = `${localStoreType}://${encodeURIComponent(oldHostname)}`;
+ newHostname = newHostname.includes(":") ? `[${newHostname}]` : newHostname;
+ let newServerUri = `${localStoreType}://${encodeURIComponent(newHostname)}`;
+
+ let logins = Services.logins.findLogins(oldServerUri, "", oldServerUri);
+ for (let login of logins) {
+ if (login.username == oldUsername) {
+ // If a nsILoginInfo exists for the old hostname/username, update it to
+ // use the new hostname/username.
+ let newLogin = Cc[
+ "@mozilla.org/login-manager/loginInfo;1"
+ ].createInstance(Ci.nsILoginInfo);
+ newLogin.init(
+ newServerUri,
+ null,
+ newServerUri,
+ newUsername,
+ login.password,
+ "",
+ ""
+ );
+ Services.logins.modifyLogin(login, newLogin);
+ }
+ }
+}
+
+/**
+ * When hostname/username changes, update the folder attributes in related
+ * identities.
+ *
+ * @param {string} oldServerUri - The server uri before the change.
+ * @param {string} newServerUri - The server uri after the change.
+ */
+function migrateIdentities(oldServerUri, newServerUri) {
+ for (let identity of MailServices.accounts.allIdentities) {
+ let attributes = [
+ "fcc_folder",
+ "draft_folder",
+ "archive_folder",
+ "stationery_folder",
+ ];
+ for (let attr of attributes) {
+ let folderUri = identity.getUnicharAttribute(attr);
+ if (folderUri.startsWith(oldServerUri)) {
+ identity.setUnicharAttribute(
+ attr,
+ folderUri.replace(oldServerUri, newServerUri)
+ );
+ }
+ }
+ }
+}
+
+/**
+ * When hostname/username changes, update .spamActionTargetAccount and
+ * .spamActionTargetFolder prefs.
+ *
+ * @param {string} oldServerUri - The server uri before the change.
+ * @param {string} newServerUri - The server uri after the change.
+ */
+function migrateSpamActions(oldServerUri, newServerUri) {
+ for (let server of MailServices.accounts.allServers) {
+ let targetAccount = server.getCharValue("spamActionTargetAccount");
+ let targetFolder = server.getUnicharValue("spamActionTargetFolder");
+ if (targetAccount.startsWith(oldServerUri)) {
+ server.setCharValue(
+ "spamActionTargetAccount",
+ targetAccount.replace(oldServerUri, newServerUri)
+ );
+ }
+ if (targetFolder.startsWith(oldServerUri)) {
+ server.setUnicharValue(
+ "spamActionTargetFolder",
+ targetFolder.replace(oldServerUri, newServerUri)
+ );
+ }
+ }
+}
+
+/**
+ * When hostname/username changes, update targetFolderUri in related filters
+ * to the new folder uri.
+ *
+ * @param {string} oldServerUri - The server uri before the change.
+ * @param {string} newServerUri - The server uri after the change.
+ */
+function migrateFilters(oldServerUri, newServerUri) {
+ for (let server of MailServices.accounts.allServers) {
+ let filterList;
+ try {
+ filterList = server.getFilterList(null);
+ if (!server.canHaveFilters || !filterList) {
+ continue;
+ }
+ } catch (e) {
+ continue;
+ }
+ let changed = false;
+ for (let i = 0; i < filterList.filterCount; i++) {
+ let filter = filterList.getFilterAt(i);
+ for (let action of filter.sortedActionList) {
+ let targetFolderUri;
+ try {
+ targetFolderUri = action.targetFolderUri;
+ } catch (e) {
+ continue;
+ }
+ if (targetFolderUri.startsWith(oldServerUri)) {
+ action.targetFolderUri = targetFolderUri.replace(
+ oldServerUri,
+ newServerUri
+ );
+ changed = true;
+ }
+ }
+ }
+ if (changed) {
+ filterList.saveToDefaultFile();
+ }
+ }
+}
+
+/**
+ * Migrate server uris in LoginManager and various account/folder prefs.
+ *
+ * @param {string} localStoreType - The store type of the current server.
+ * @param {string} oldHostname - The hostname before the change.
+ * @param {string} oldUsername - The username before the change.
+ * @param {string} newHostname - The hostname after the change.
+ * @param {string} newUsername - The username after the change.
+ */
+function migrateServerUris(
+ localStoreType,
+ oldHostname,
+ oldUsername,
+ newHostname,
+ newUsername
+) {
+ try {
+ migratePassword(
+ localStoreType,
+ oldHostname,
+ oldUsername,
+ newHostname,
+ newUsername
+ );
+ } catch (e) {
+ console.error(e);
+ }
+
+ let oldAuth = oldUsername ? `${encodeURIComponent(oldUsername)}@` : "";
+ let newAuth = newUsername ? `${encodeURIComponent(newUsername)}@` : "";
+ // When constructing nsIURI, need to wrap IPv6 address in [].
+ oldHostname = oldHostname.includes(":") ? `[${oldHostname}]` : oldHostname;
+ let oldServerUri = `${localStoreType}://${oldAuth}${encodeURIComponent(
+ oldHostname
+ )}`;
+ newHostname = newHostname.includes(":") ? `[${newHostname}]` : newHostname;
+ let newServerUri = `${localStoreType}://${newAuth}${encodeURIComponent(
+ newHostname
+ )}`;
+
+ try {
+ migrateIdentities(oldServerUri, newServerUri);
+ } catch (e) {
+ console.error(e);
+ }
+ try {
+ migrateSpamActions(oldServerUri, newServerUri);
+ } catch (e) {
+ console.error(e);
+ }
+ try {
+ migrateFilters(oldServerUri, newServerUri);
+ } catch (e) {
+ console.error(e);
+ }
+}
+
+/**
+ * A base class for incoming server, should not be used directly.
+ *
+ * @implements {nsIMsgIncomingServer}
+ * @implements {nsISupportsWeakReference}
+ * @implements {nsIObserver}
+ * @abstract
+ */
+class MsgIncomingServer {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMsgIncomingServer",
+ "nsISupportsWeakReference",
+ "nsIObserver",
+ ]);
+
+ constructor() {
+ // nsIMsgIncomingServer attributes that map directly to pref values.
+ this._mapAttrsToPrefs([
+ ["Char", "type"],
+ ["Char", "clientid"],
+ ["Int", "authMethod"],
+ ["Int", "biffMinutes", "check_time"],
+ ["Int", "maxMessageSize", "max_size"],
+ ["Int", "incomingDuplicateAction", "dup_action"],
+ ["Bool", "clientidEnabled"],
+ ["Bool", "downloadOnBiff", "download_on_biff"],
+ ["Bool", "valid"],
+ ["Bool", "emptyTrashOnExit", "empty_trash_on_exit"],
+ ["Bool", "canDelete"],
+ ["Bool", "loginAtStartUp", "login_at_startup"],
+ [
+ "Bool",
+ "defaultCopiesAndFoldersPrefsToServer",
+ "allows_specialfolders_usage",
+ ],
+ ["Bool", "canCreateFoldersOnServer", "canCreateFolders"],
+ ["Bool", "canFileMessagesOnServer", "canFileMessages"],
+ ["Bool", "limitOfflineMessageSize", "limit_offline_message_size"],
+ ["Bool", "hidden"],
+ ]);
+
+ // nsIMsgIncomingServer attributes.
+ this.performingBiff = false;
+ this.accountManagerChrome = "am-main.xhtml";
+ this.biffState = Ci.nsIMsgFolder.nsMsgBiffState_Unknown;
+ this.downloadMessagesAtStartup = false;
+ this.canHaveFilters = true;
+ this.canBeDefaultServer = false;
+ this.displayStartupPage = true;
+ this.supportsDiskSpace = true;
+ this.canCompactFoldersOnServer = true;
+ this.canUndoDeleteOnServer = true;
+ this.sortOrder = 100000000;
+
+ // @type {Map<string, number>} - The key is MsgId+Subject, the value is
+ // this._hdrIndex.
+ this._knownHdrMap = new Map();
+ this._hdrIndex = 0;
+
+ Services.obs.addObserver(this, "passwordmgr-storage-changed");
+ }
+
+ /**
+ * Observe() receives notifications for all accounts, not just this server's
+ * account. So we ignore all notifications not intended for this server.
+ * When the state of the password manager changes we need to clear the
+ * this server's password from the cache in case the user just changed or
+ * removed the password or username.
+ * OAuth2 servers often automatically change the password manager's stored
+ * password (the token).
+ */
+ observe(subject, topic, data) {
+ if (topic == "passwordmgr-storage-changed") {
+ // Check that the notification is for this server and user.
+ let otherFullName = "";
+ let otherUsername = "";
+ if (subject instanceof Ci.nsILoginInfo) {
+ // The login info for a server has been removed with data being
+ // "removeLogin" or "removeAllLogins".
+ otherFullName = subject.origin;
+ otherUsername = subject.username;
+ } else if (subject instanceof Ci.nsIArray) {
+ // Probably a 2 element array containing old and new login info due to
+ // data being "modifyLogin". E.g., a user has modified the password or
+ // username in the password manager or an OAuth2 token string has
+ // automatically changed. Only need to look at names in first array
+ // element (login info before any modification) since the user might
+ // have changed the username as found in the 2nd elements. (The
+ // hostname can't be modified in the password manager.
+ otherFullName = subject.queryElementAt(0, Ci.nsISupports).origin;
+ otherUsername = subject.queryElementAt(0, Ci.nsISupports).username;
+ }
+ if (otherFullName) {
+ if (
+ otherFullName != "mailbox://" + this.hostName ||
+ otherUsername != this.username
+ ) {
+ // Not for this server; keep this server's cached password.
+ return;
+ }
+ } else if (data != "hostSavingDisabled") {
+ // "hostSavingDisabled" only occurs during test_smtpServer.js and
+ // expects the password to be removed from memory cache. Otherwise, we
+ // don't have enough information to decide to remove the cached
+ // password, so keep it.
+ return;
+ }
+ // Remove the password for this server cached in memory.
+ this.password = "";
+ }
+ }
+
+ /**
+ * Set up getters/setters for attributes that map directly to pref values.
+ *
+ * @param {string[]} attributes - An array of attributes. Each attribute is
+ * defined by its type, name and corresponding prefName.
+ */
+ _mapAttrsToPrefs(attributes) {
+ for (let [type, attrName, prefName] of attributes) {
+ prefName = prefName || attrName;
+ Object.defineProperty(this, attrName, {
+ configurable: true,
+ get: () => this[`get${type}Value`](prefName),
+ set: value => {
+ this[`set${type}Value`](prefName, value);
+ },
+ });
+ }
+ }
+
+ get key() {
+ return this._key;
+ }
+
+ set key(key) {
+ this._key = key;
+ this._prefs = Services.prefs.getBranch(`mail.server.${key}.`);
+ this._defaultPrefs = Services.prefs.getBranch("mail.server.default.");
+ }
+
+ get UID() {
+ let uid = this._prefs.getStringPref("uid", "");
+ if (uid) {
+ return uid;
+ }
+ return (this.UID = Services.uuid
+ .generateUUID()
+ .toString()
+ .substring(1, 37));
+ }
+
+ set UID(uid) {
+ if (this._prefs.prefHasUserValue("uid")) {
+ throw new Components.Exception("uid is already set", Cr.NS_ERROR_ABORT);
+ }
+ this._prefs.setStringPref("uid", uid);
+ }
+
+ get hostName() {
+ let hostname = this.getUnicharValue("hostname");
+ if (hostname.includes(":")) {
+ // Reformat the hostname if it contains a port number.
+ this.hostName = hostname;
+ return this.hostName;
+ }
+ return hostname;
+ }
+
+ set hostName(value) {
+ let oldName = this.hostName;
+ this._setHostName("hostname", value);
+
+ if (oldName && oldName != value) {
+ this.onUserOrHostNameChanged(oldName, value, true);
+ }
+ }
+
+ _setHostName(prefName, value) {
+ let [host, port] = value.split(":");
+ if (port) {
+ this.port = Number(port);
+ }
+ this.setUnicharValue(prefName, host);
+ }
+
+ get username() {
+ return this.getUnicharValue("userName");
+ }
+
+ set username(value) {
+ let oldName = this.username;
+ if (oldName && oldName != value) {
+ this.setUnicharValue("userName", value);
+ this.onUserOrHostNameChanged(oldName, value, false);
+ } else {
+ this.setUnicharValue("userName", value);
+ }
+ }
+
+ get port() {
+ let port = this.getIntValue("port");
+ if (port > 1) {
+ return port;
+ }
+
+ // If the port isn't set, use the default port based on the protocol.
+ return this.protocolInfo.getDefaultServerPort(
+ this.socketType == Ci.nsMsgSocketType.SSL
+ );
+ }
+
+ set port(value) {
+ this.setIntValue("port", value);
+ }
+
+ get protocolInfo() {
+ return Cc[
+ `@mozilla.org/messenger/protocol/info;1?type=${this.type}`
+ ].getService(Ci.nsIMsgProtocolInfo);
+ }
+
+ get socketType() {
+ try {
+ return this._prefs.getIntPref("socketType");
+ } catch (e) {
+ // socketType is set to default value. Look at isSecure setting.
+ if (this._prefs.getBoolPref("isSecure", false)) {
+ return Ci.nsMsgSocketType.SSL;
+ }
+ return this._defaultPrefs.getIntPref(
+ "socketType",
+ Ci.nsMsgSocketType.plain
+ );
+ }
+ }
+
+ set socketType(value) {
+ let wasSecure = this.isSecure;
+ this._prefs.setIntPref("socketType", value);
+ let isSecure = this.isSecure;
+ if (wasSecure != isSecure) {
+ this.rootFolder.NotifyBoolPropertyChanged(
+ "isSecure",
+ wasSecure,
+ isSecure
+ );
+ }
+ }
+
+ get isSecure() {
+ return [Ci.nsMsgSocketType.alwaysSTARTTLS, Ci.nsMsgSocketType.SSL].includes(
+ this.socketType
+ );
+ }
+
+ get serverURI() {
+ return this._getServerURI(true);
+ }
+
+ /**
+ * Get server URI in the form of localStoreType://[user@]hostname.
+ *
+ * @param {boolean} includeUsername - Whether to include the username.
+ * @returns {string}
+ */
+ _getServerURI(includeUsername) {
+ let auth =
+ includeUsername && this.username
+ ? `${encodeURIComponent(this.username)}@`
+ : "";
+ // When constructing nsIURI, need to wrap IPv6 address in [].
+ let hostname = this.hostName.includes(":")
+ ? `[${this.hostName}]`
+ : this.hostName;
+ return `${this.localStoreType}://${auth}${encodeURIComponent(hostname)}`;
+ }
+
+ get prettyName() {
+ return this.getUnicharValue("name") || this.constructedPrettyName;
+ }
+
+ set prettyName(value) {
+ this.setUnicharValue("name", value);
+ this.rootFolder.prettyName = value;
+ }
+
+ /**
+ * Construct a pretty name from username and hostname.
+ *
+ * @param {string} username - The user name.
+ * @param {string} hostname - The host name.
+ * @returns {string}
+ */
+ _constructPrettyName(username, hostname) {
+ let prefix = username ? `${username} on ` : "";
+ return `${prefix}${hostname}`;
+ }
+
+ get constructedPrettyName() {
+ return this._constructPrettyName(this.username, this.hostName);
+ }
+
+ get localPath() {
+ let localPath = this.getFileValue("directory-rel", "directory");
+ if (localPath) {
+ // If the local path has already been set, use it.
+ return localPath;
+ }
+
+ // Create the path using protocol info and hostname.
+ localPath = this.protocolInfo.defaultLocalPath;
+ if (!localPath.exists()) {
+ localPath.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+
+ localPath.append(this.hostName);
+ localPath.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ this.localPath = localPath;
+ return localPath;
+ }
+
+ set localPath(localPath) {
+ this.setFileValue("directory-rel", "directory", localPath);
+ }
+
+ get rootFolder() {
+ if (!this._rootFolder) {
+ this._rootFolder = MailServices.folderLookup.getOrCreateFolderForURL(
+ this.serverURI
+ );
+ }
+ return this._rootFolder;
+ }
+
+ get rootMsgFolder() {
+ return this.rootFolder;
+ }
+
+ get msgStore() {
+ if (!this._msgStore) {
+ let contractId = this.getCharValue("storeContractID");
+ if (!contractId) {
+ contractId = "@mozilla.org/msgstore/berkeleystore;1";
+ this.setCharValue("storeContractID", contractId);
+ }
+
+ // After someone starts using the pluggable store, we can no longer
+ // change the value.
+ this.setBoolValue("canChangeStoreType", false);
+
+ this._msgStore = Cc[contractId].createInstance(Ci.nsIMsgPluggableStore);
+ }
+ return this._msgStore;
+ }
+
+ get doBiff() {
+ try {
+ return this._prefs.getBoolPref("check_new_mail");
+ } catch (e) {
+ return this.protocolInfo.defaultDoBiff;
+ }
+ }
+
+ set doBiff(value) {
+ let biffManager = Cc["@mozilla.org/messenger/biffManager;1"].getService(
+ Ci.nsIMsgBiffManager
+ );
+ if (value) {
+ biffManager.addServerBiff(this);
+ } else {
+ biffManager.removeServerBiff(this);
+ }
+ this._prefs.setBoolPref("check_new_mail", value);
+ }
+
+ /**
+ * type, attribute name, pref name
+ */
+ _retentionSettingsPrefs = [
+ ["Int", "retainByPreference", "retainBy"],
+ ["Int", "numHeadersToKeep", "numHdrsToKeep"],
+ ["Int", "daysToKeepHdrs"],
+ ["Int", "daysToKeepBodies"],
+ ["Bool", "cleanupBodiesByDays", "cleanupBodies"],
+ ["Bool", "applyToFlaggedMessages"],
+ ];
+
+ get retentionSettings() {
+ let settings = Cc[
+ "@mozilla.org/msgDatabase/retentionSettings;1"
+ ].createInstance(Ci.nsIMsgRetentionSettings);
+ for (let [type, attrName, prefName] of this._retentionSettingsPrefs) {
+ prefName = prefName || attrName;
+ settings[attrName] = this[`get${type}Value`](prefName);
+ }
+ return settings;
+ }
+
+ set retentionSettings(settings) {
+ for (let [type, attrName, prefName] of this._retentionSettingsPrefs) {
+ prefName = prefName || attrName;
+ this[`set${type}Value`](prefName, settings[attrName]);
+ }
+ }
+
+ get spamSettings() {
+ if (!this.getCharValue("spamActionTargetAccount")) {
+ this.setCharValue("spamActionTargetAccount", this.serverURI);
+ }
+ if (!this._spamSettings) {
+ this._spamSettings = Cc[
+ "@mozilla.org/messenger/spamsettings;1"
+ ].createInstance(Ci.nsISpamSettings);
+ try {
+ this._spamSettings.initialize(this);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ return this._spamSettings;
+ }
+
+ get spamFilterPlugin() {
+ if (!this._spamFilterPlugin) {
+ this._spamFilterPlugin = Cc[
+ "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter"
+ ].getService(Ci.nsIMsgFilterPlugin);
+ }
+ return this._spamFilterPlugin;
+ }
+
+ get isDeferredTo() {
+ let account = MailServices.accounts.FindAccountForServer(this);
+ if (!account) {
+ return false;
+ }
+ return MailServices.accounts.allServers.some(
+ server => server.getCharValue("deferred_to_account") == account.key
+ );
+ }
+
+ get serverRequiresPasswordForBiff() {
+ return true;
+ }
+
+ /**
+ * type, attribute name, pref name
+ */
+ _downloadSettingsPrefs = [
+ ["Int", "ageLimitOfMsgsToDownload", "ageLimit"],
+ ["Bool", "downloadUnreadOnly"],
+ ["Bool", "downloadByDate"],
+ ];
+
+ get downloadSettings() {
+ if (!this._downloadSettings) {
+ this._downloadSettings = Cc[
+ "@mozilla.org/msgDatabase/downloadSettings;1"
+ ].createInstance(Ci.nsIMsgDownloadSettings);
+ for (let [type, attrName, prefName] of this._downloadSettingsPrefs) {
+ prefName = prefName || attrName;
+ this._downloadSettings[attrName] = this[`get${type}Value`](prefName);
+ }
+ }
+ return this._downloadSettings;
+ }
+
+ set downloadSettings(settings) {
+ this._downloadSettings = settings;
+ for (let [type, attrName, prefName] of this._downloadSettingsPrefs) {
+ prefName = prefName || attrName;
+ this[`set${type}Value`](prefName, settings[attrName]);
+ }
+ }
+
+ get offlineSupportLevel() {
+ const OFFLINE_SUPPORT_LEVEL_NONE = 0;
+ const OFFLINE_SUPPORT_LEVEL_UNDEFINED = -1;
+ let level = this.getIntValue("offline_support_level");
+ return level == OFFLINE_SUPPORT_LEVEL_UNDEFINED
+ ? OFFLINE_SUPPORT_LEVEL_NONE
+ : level;
+ }
+
+ set offlineSupportLevel(value) {
+ this.setIntValue("offline_support_level", value);
+ }
+
+ get filterScope() {
+ return Ci.nsMsgSearchScope.offlineMailFilter;
+ }
+
+ get searchScope() {
+ return Ci.nsMsgSearchScope.offlineMail;
+ }
+
+ get passwordPromptRequired() {
+ if (!this.serverRequiresPasswordForBiff) {
+ // If the password is not even required for biff we don't need to check
+ // any further.
+ return false;
+ }
+ if (!this.password) {
+ // If the password is empty, check to see if it is stored.
+ this.password = this._getPasswordWithoutUI();
+ }
+ if (this.password) {
+ return false;
+ }
+ return this.authMethod != Ci.nsMsgAuthMethod.OAuth2;
+ }
+
+ getCharValue(prefName) {
+ try {
+ return this._prefs.getCharPref(prefName);
+ } catch (e) {
+ return this._defaultPrefs.getCharPref(prefName, "");
+ }
+ }
+
+ setCharValue(prefName, value) {
+ let defaultValue = this._defaultPrefs.getCharPref(prefName, "");
+ if (!value || value == defaultValue) {
+ this._prefs.clearUserPref(prefName);
+ } else {
+ this._prefs.setCharPref(prefName, value);
+ }
+ }
+
+ getUnicharValue(prefName) {
+ try {
+ return this._prefs.getStringPref(prefName);
+ } catch (e) {
+ return this._defaultPrefs.getStringPref(prefName, "");
+ }
+ }
+
+ setUnicharValue(prefName, value) {
+ let defaultValue = this._defaultPrefs.getStringPref(prefName, "");
+ if (!value || value == defaultValue) {
+ this._prefs.clearUserPref(prefName);
+ } else {
+ this._prefs.setStringPref(prefName, value);
+ }
+ }
+
+ getIntValue(prefName) {
+ try {
+ return this._prefs.getIntPref(prefName);
+ } catch (e) {
+ return this._defaultPrefs.getIntPref(prefName, 0);
+ }
+ }
+
+ setIntValue(prefName, value) {
+ let defaultValue = this._defaultPrefs.getIntPref(prefName, value - 1);
+ if (defaultValue == value) {
+ this._prefs.clearUserPref(prefName);
+ } else {
+ this._prefs.setIntPref(prefName, value);
+ }
+ }
+
+ getBoolValue(prefName) {
+ try {
+ return this._prefs.getBoolPref(prefName);
+ } catch (e) {
+ return this._defaultPrefs.getBoolPref(prefName, false);
+ }
+ }
+
+ setBoolValue(prefName, value) {
+ let defaultValue = this._defaultPrefs.getBoolPref(prefName, !value);
+ if (defaultValue == value) {
+ this._prefs.clearUserPref(prefName);
+ } else {
+ this._prefs.setBoolPref(prefName, value);
+ }
+ }
+
+ getFileValue(relPrefName, absPrefName) {
+ try {
+ let file = this._prefs.getComplexValue(
+ relPrefName,
+ Ci.nsIRelativeFilePref
+ ).file;
+ file.normalize();
+ return file;
+ } catch (e) {
+ try {
+ let file = this._prefs.getComplexValue(absPrefName, Ci.nsIFile);
+ this._prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, {
+ QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]),
+ file,
+ relativeToKey: "ProfD",
+ });
+ return file;
+ } catch (e) {
+ return null;
+ }
+ }
+ }
+
+ setFileValue(relPrefName, absPrefName, file) {
+ this._prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, {
+ QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]),
+ file,
+ relativeToKey: "ProfD",
+ });
+ this._prefs.setComplexValue(absPrefName, Ci.nsIFile, file);
+ }
+
+ onUserOrHostNameChanged(oldValue, newValue, hostnameChanged) {
+ migrateServerUris(
+ this.localStoreType,
+ hostnameChanged ? oldValue : this.hostName,
+ hostnameChanged ? this.username : oldValue,
+ this.hostName,
+ this.username
+ );
+ this._spamSettings = null;
+
+ // Clear the clientid because the user or host have changed.
+ this.clientid = "";
+
+ let atIndex = newValue.indexOf("@");
+ if (!this.prettyName || (!hostnameChanged && atIndex != -1)) {
+ // If new username contains @ then better not update the pretty name.
+ return;
+ }
+
+ atIndex = this.prettyName.indexOf("@");
+ if (
+ !hostnameChanged &&
+ atIndex != -1 &&
+ oldValue == this.prettyName.slice(0, atIndex)
+ ) {
+ // If username changed and the pretty name has the old username before @,
+ // update to the new username.
+ this.prettyName = newValue + this.prettyName.slice(atIndex);
+ } else if (
+ hostnameChanged &&
+ oldValue == this.prettyName.slice(atIndex + 1)
+ ) {
+ // If hostname changed and the pretty name has the old hostname after @,
+ // update to the new hostname.
+ this.prettyName = this.prettyName.slice(0, atIndex + 1) + newValue;
+ } else {
+ // Set the `name` pref anyway, to make tests happy.
+ // eslint-disable-next-line no-self-assign
+ this.prettyName = this.prettyName;
+ }
+ }
+
+ /**
+ * Try to get the password from nsILoginManager.
+ *
+ * @returns {string}
+ */
+ _getPasswordWithoutUI() {
+ let serverURI = this._getServerURI();
+ let logins = Services.logins.findLogins(serverURI, "", serverURI);
+ for (let login of logins) {
+ if (login.username == this.username) {
+ return login.password;
+ }
+ }
+ return null;
+ }
+
+ getPasswordWithUI(promptMessage, promptTitle) {
+ let password = this._getPasswordWithoutUI();
+ if (password) {
+ this.password = password;
+ return this.password;
+ }
+ let outUsername = {};
+ let outPassword = {};
+ let ok;
+ let authPrompt;
+ try {
+ // This prompt has a checkbox for saving password.
+ authPrompt = Cc["@mozilla.org/messenger/msgAuthPrompt;1"].getService(
+ Ci.nsIAuthPrompt
+ );
+ } catch (e) {
+ // Often happens in tests. This prompt has no checkbox for saving password.
+ authPrompt = Services.ww.getNewAuthPrompter(null);
+ }
+ if (this.username) {
+ ok = authPrompt.promptPassword(
+ promptTitle,
+ promptMessage,
+ this.serverURI,
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
+ outPassword
+ );
+ } else {
+ ok = authPrompt.promptUsernameAndPassword(
+ promptTitle,
+ promptMessage,
+ this.serverURI,
+ Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY,
+ outUsername,
+ outPassword
+ );
+ }
+ if (ok) {
+ if (outUsername.value) {
+ this.username = outUsername.value;
+ }
+ this.password = outPassword.value;
+ } else {
+ throw Components.Exception("Password dialog canceled", Cr.NS_ERROR_ABORT);
+ }
+ return this.password;
+ }
+
+ forgetPassword() {
+ let serverURI = this._getServerURI();
+ let logins = Services.logins.findLogins(serverURI, "", serverURI);
+ for (let login of logins) {
+ if (login.username == this.username) {
+ Services.logins.removeLogin(login);
+ }
+ }
+ this.password = "";
+ }
+
+ forgetSessionPassword() {
+ this.password = "";
+ }
+
+ closeCachedConnections() {}
+
+ shutdown() {
+ this.closeCachedConnections();
+
+ if (this._filterList) {
+ this._filterList.logStream = null;
+ this._filterList = null;
+ }
+ if (this._spamSettings) {
+ this._spamSettings.logStream = null;
+ this._spamSettings = null;
+ }
+ }
+
+ getFilterList(msgWindow) {
+ if (!this._filterList) {
+ if (!this.rootFolder.filePath.path) {
+ // Happens in tests.
+ return null;
+ }
+ let filterFile = this.rootFolder.filePath.clone();
+ filterFile.append("msgFilterRules.dat");
+ try {
+ this._filterList = MailServices.filters.OpenFilterList(
+ filterFile,
+ this.rootFolder,
+ msgWindow
+ );
+ } catch (e) {
+ console.error(e);
+ const NS_ERROR_FILE_FS_CORRUPTED = 0x80520016;
+ if (e.result == NS_ERROR_FILE_FS_CORRUPTED && filterFile.exists()) {
+ // OpenFilterList will create a new one next time.
+ filterFile.renameTo(filterFile.parent, "msgFilterRules.dat.orig");
+ }
+ }
+ }
+ return this._filterList;
+ }
+
+ setFilterList(value) {
+ this._filterList = value;
+ }
+
+ getEditableFilterList(msgWindow) {
+ if (!this._editableFilterList) {
+ return this.getFilterList(msgWindow);
+ }
+ return this._editableFilterList;
+ }
+
+ setEditableFilterList(value) {
+ this._editableFilterList = value;
+ }
+
+ setDefaultLocalPath(value) {
+ this.protocolInfo.setDefaultLocalPath(value);
+ }
+
+ getNewMessages(folder, msgWindow, urlListener) {
+ folder.getNewMessages(msgWindow, urlListener);
+ }
+
+ writeToFolderCache(folderCache) {
+ this.rootFolder.writeToFolderCache(folderCache, true);
+ }
+
+ clearAllValues() {
+ for (let prefName of this._prefs.getChildList("")) {
+ this._prefs.clearUserPref(prefName);
+ }
+ }
+
+ removeFiles() {
+ if (this.getCharValue("deferred_to_account") || this.isDeferredTo) {
+ throw Components.Exception(
+ "Should not remove files for a deferred account",
+ Cr.NS_ERROR_FAILURE
+ );
+ }
+ this.localPath.remove(true);
+ }
+
+ getMsgFolderFromURI(folder, uri) {
+ try {
+ return this.rootMsgFolder.getChildWithURI(uri, true, true) || folder;
+ } catch (e) {
+ return folder;
+ }
+ }
+
+ isNewHdrDuplicate(newHdr) {
+ // If the message has been partially downloaded, the message should not
+ // be considered a duplicated message. See bug 714090.
+ if (newHdr.flags & Ci.nsMsgMessageFlags.Partial) {
+ return false;
+ }
+
+ if (!newHdr.subject || !newHdr.messageId) {
+ return false;
+ }
+
+ let key = `${newHdr.messageId}${newHdr.subject}`;
+ if (this._knownHdrMap.get(key)) {
+ return true;
+ }
+
+ this._knownHdrMap.set(key, ++this._hdrIndex);
+
+ const MAX_SIZE = 500;
+ if (this._knownHdrMap.size > MAX_SIZE) {
+ // Release the oldest half of downloaded hdrs.
+ for (let [k, v] of this._knownHdrMap) {
+ if (v < this._hdrIndex - MAX_SIZE / 2) {
+ this._knownHdrMap.delete(k);
+ } else if (this._knownHdrMap.size <= MAX_SIZE / 2) {
+ break;
+ }
+ }
+ }
+ return false;
+ }
+
+ equals(server) {
+ return this.key == server.key;
+ }
+
+ _configureTemporaryReturnReceiptsFilter(filterList) {
+ let identity = MailServices.accounts.getFirstIdentityForServer(this);
+ if (!identity) {
+ return;
+ }
+ let incorp = Ci.nsIMsgMdnGenerator.eIncorporateInbox;
+ if (identity.getBoolAttribute("use_custom_prefs")) {
+ incorp = this.getIntValue("incorporate_return_receipt");
+ } else {
+ incorp = Services.prefs.getIntPref("mail.incorporate.return_receipt");
+ }
+
+ let enable = incorp == Ci.nsIMsgMdnGenerator.eIncorporateSent;
+
+ const FILTER_NAME = "mozilla-temporary-internal-MDN-receipt-filter";
+ let filter = filterList.getFilterNamed(FILTER_NAME);
+
+ if (filter) {
+ filter.enabled = enable;
+ return;
+ } else if (!enable || !identity.fccFolder) {
+ return;
+ }
+
+ filter = filterList.createFilter(FILTER_NAME);
+ if (!filter) {
+ return;
+ }
+
+ filter.enabled = true;
+ filter.temporary = true;
+
+ let term = filter.createTerm();
+ let value = term.value;
+ value.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+ value.str = "multipart/report";
+ term.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+ term.op = Ci.nsMsgSearchOp.Contains;
+ term.booleanAnd = true;
+ term.arbitraryHeader = "Content-Type";
+ term.value = value;
+ filter.appendTerm(term);
+
+ term = filter.createTerm();
+ value = term.value;
+ value.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+ value.str = "disposition-notification";
+ term.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1;
+ term.op = Ci.nsMsgSearchOp.Contains;
+ term.booleanAnd = true;
+ term.arbitraryHeader = "Content-Type";
+ term.value = value;
+ filter.appendTerm(term);
+
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MoveToFolder;
+ action.targetFolderUri = identity.fccFolder;
+ filter.appendAction(action);
+ filterList.insertFilterAt(0, filter);
+ }
+
+ _configureTemporaryServerSpamFilters(filterList) {
+ let spamSettings = this.spamSettings;
+ if (!spamSettings.useServerFilter) {
+ return;
+ }
+ let serverFilterName = spamSettings.serverFilterName;
+ let serverFilterTrustFlags = spamSettings.serverFilterTrustFlags;
+ if (!serverFilterName || !serverFilterName) {
+ return;
+ }
+
+ // Check if filters have been setup already.
+ let yesFilterName = `${serverFilterName}Yes`;
+ let noFilterName = `${serverFilterName}No`;
+ let filter = filterList.getFilterNamed(yesFilterName);
+ if (!filter) {
+ filter = filterList.getFilterNamed(noFilterName);
+ }
+ if (filter) {
+ return;
+ }
+
+ let serverFilterList = MailServices.filters.OpenFilterList(
+ spamSettings.serverFilterFile,
+ null,
+ null
+ );
+ filter = serverFilterList.getFilterNamed(yesFilterName);
+ if (filter && serverFilterTrustFlags & Ci.nsISpamSettings.TRUST_POSITIVES) {
+ filter.temporary = true;
+ // Check if we're supposed to move junk mail to junk folder; if so, add
+ // filter action to do so.
+ let searchTerms = filter.searchTerms;
+ if (searchTerms.length) {
+ searchTerms[0].beginsGrouping = true;
+ searchTerms.at(-1).endsGrouping = true;
+ }
+
+ // Create a new term, checking if the user set junk status. The term will
+ // search for junkscoreorigin != "user".
+ let term = filter.createTerm();
+ term.attrib = Ci.nsMsgSearchAttrib.JunkScoreOrigin;
+ term.op = Ci.nsMsgSearchOp.Isnt;
+ term.booleanAnd = true;
+ let value = term.value;
+ value.attrib = Ci.nsMsgSearchAttrib.JunkScoreOrigin;
+ value.str = "user";
+ term.value = value;
+ filter.appendTerm(term);
+
+ if (spamSettings.moveOnSpam) {
+ let spamFolderURI = spamSettings.spamFolderURI;
+ if (spamFolderURI) {
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MoveToFolder;
+ action.targetFolderUri = spamFolderURI;
+ filter.appendAction(action);
+ }
+ }
+
+ if (spamSettings.markAsReadOnSpam) {
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MarkRead;
+ filter.appendAction(action);
+ }
+ filterList.insertFilterAt(0, filter);
+ }
+
+ filter = serverFilterList.getFilterNamed(noFilterName);
+ if (filter && serverFilterTrustFlags & Ci.nsISpamSettings.TRUST_NEGATIVES) {
+ filter.temporary = true;
+ filterList.insertFilterAt(0, filter);
+ }
+ }
+
+ configureTemporaryFilters(filterList) {
+ this._configureTemporaryReturnReceiptsFilter(filterList);
+ this._configureTemporaryServerSpamFilters(filterList);
+ }
+
+ clearTemporaryReturnReceiptsFilter() {
+ if (!this._filterList) {
+ return;
+ }
+ let filter = this._filterList.getFilterNamed(
+ "mozilla-temporary-internal-MDN-receipt-filter"
+ );
+ if (filter) {
+ this._filterList.removeFilter(filter);
+ }
+ }
+
+ getForcePropertyEmpty(name) {
+ return this.getCharValue(`${name}.empty`) == "true";
+ }
+
+ setForcePropertyEmpty(name, value) {
+ return this.setCharValue(`${name}.empty`, value ? "true" : "");
+ }
+
+ performExpand(msgWindow) {}
+
+ get wrappedJSObject() {
+ return this;
+ }
+
+ _passwordPromise = null;
+
+ /**
+ * Show a password prompt. If a prompt is currently shown, just wait for it.
+ *
+ * @param {string} message - The text inside the prompt.
+ * @param {string} title - The title of the prompt.
+ */
+ async getPasswordWithUIAsync(message, title) {
+ if (this._passwordPromise) {
+ await this._passwordPromise;
+ return this.password;
+ }
+ let deferred = {};
+ this._passwordPromise = new Promise((resolve, reject) => {
+ deferred.resolve = resolve;
+ deferred.reject = reject;
+ });
+ try {
+ this.getPasswordWithUI(message, title);
+ } catch (e) {
+ deferred.reject(e);
+ throw e;
+ } finally {
+ this._passwordPromise = null;
+ }
+ deferred.resolve();
+ return this.password;
+ }
+}
diff --git a/comm/mailnews/base/src/MsgKeySet.jsm b/comm/mailnews/base/src/MsgKeySet.jsm
new file mode 100644
index 0000000000..bbbd580ba9
--- /dev/null
+++ b/comm/mailnews/base/src/MsgKeySet.jsm
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["MsgKeySet"];
+
+/**
+ * A structure to represent a set of articles. This is usually for lines from
+ * the newsrc, which have article lists like
+ *
+ * 1-29627,29635,29658,32861-32863
+ *
+ * so the data has these properties:
+ *
+ * - strictly increasing
+ * - large subsequences of monotonically increasing ranges
+ * - gaps in the set are usually small, but not always
+ * - consecutive ranges tend to be large
+ */
+class MsgKeySet {
+ /**
+ * @param {string} [str] - The raw string to represent a set of articles.
+ */
+ constructor(str) {
+ // An array of tuples, each tuple contains the start and end value of a sub
+ // range.
+ // @type {Array<[number, number]>}
+ this._ranges = str
+ ? str.split(",").map(part => {
+ let [start, end] = part.split("-");
+ return [+start, +end || +start];
+ })
+ : [];
+ }
+
+ /**
+ * Add a value to the set.
+ *
+ * @param {number} value - The value to add.
+ */
+ add(value) {
+ this.addRange(value, value);
+ }
+
+ /**
+ * Add a range to the set.
+ *
+ * @param {number} low - The smallest value of the range.
+ * @param {number} high - The largest value of the range.
+ */
+ addRange(low, high) {
+ let index = 0;
+ for (let [start] of this._ranges) {
+ if (start > low) {
+ break;
+ }
+ index++;
+ }
+ this._ranges.splice(index, 0, [low, high]);
+ this._rebuild();
+ }
+
+ /**
+ * Check if a value is in the set.
+ *
+ * @param {number} value - The value to check.
+ * @returns {boolean}
+ */
+ has(value) {
+ return this._ranges.some(([start, end]) =>
+ end ? start <= value && value <= end : start == value
+ );
+ }
+
+ /**
+ * Get the last range that is in the input range, but not in the key set.
+ *
+ * @param {number} low - The smallest value of the input range.
+ * @param {number} high - The largest value of the input range.
+ * @returns {number[]} - Array of lenght two with [low, high].
+ */
+ getLastMissingRange(low, high) {
+ let length = this._ranges.length;
+ for (let i = length - 1; i >= 0; i--) {
+ let [start, end] = this._ranges[i];
+ if (end < high) {
+ return [Math.max(low, end + 1), high];
+ } else if (low < start && high > start) {
+ high = start - 1;
+ } else {
+ return [];
+ }
+ }
+ return [low, high];
+ }
+
+ /**
+ * Get the string representation of the key set.
+ *
+ * @returns {string}
+ */
+ toString() {
+ return this._ranges
+ .map(([start, end]) => (start == end ? start : `${start}-${end}`))
+ .join(",");
+ }
+
+ /**
+ * Sub ranges may become overlapped after some operations. This method merges
+ * them if needed.
+ */
+ _rebuild() {
+ if (this._ranges.length < 2) {
+ return;
+ }
+ let newRanges = [];
+ let [cursorStart, cursorEnd] = this._ranges[0];
+ for (let [start, end] of this._ranges.slice(1)) {
+ if (cursorEnd < start - 1) {
+ // No overlap between the two ranges.
+ newRanges.push([cursorStart, cursorEnd]);
+ cursorStart = start;
+ cursorEnd = end;
+ } else {
+ // Overlapped, merge them.
+ cursorEnd = end;
+ }
+ }
+ newRanges.push([cursorStart, cursorEnd]);
+ this._ranges = newRanges;
+ }
+}
diff --git a/comm/mailnews/base/src/MsgProtocolInfo.sys.mjs b/comm/mailnews/base/src/MsgProtocolInfo.sys.mjs
new file mode 100644
index 0000000000..7c9088e12e
--- /dev/null
+++ b/comm/mailnews/base/src/MsgProtocolInfo.sys.mjs
@@ -0,0 +1,53 @@
+/* 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/. */
+
+/**
+ * @see {nsIMsgProtocolInfo}
+ */
+export class MsgProtocolInfo {
+ get defaultLocalPath() {
+ let file = this._getFileValue(this.RELATIVE_PREF, this.ABSOLUTE_PREF);
+ if (!file) {
+ file = Services.dirsvc.get(this.DIR_SERVICE_PROP, Ci.nsIFile);
+ this._setFileValue(this.RELATIVE_PREF, this.ABSOLUTE_PREF, file);
+ }
+ if (!file.exists()) {
+ file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o775);
+ }
+ file.normalize();
+ return file;
+ }
+
+ set defaultLocalPath(value) {
+ this._setFileValue(this.RELATIVE_PREF, this.ABSOLUTE_PREF, value);
+ }
+
+ _getFileValue(relPrefName, absPrefName) {
+ try {
+ return Services.prefs.getComplexValue(relPrefName, Ci.nsIRelativeFilePref)
+ .file;
+ } catch (e) {
+ try {
+ let file = Services.prefs.getComplexValue(absPrefName, Ci.nsIFile);
+ Services.prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, {
+ QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]),
+ file,
+ relativeToKey: "ProfD",
+ });
+ return file;
+ } catch (e) {
+ return null;
+ }
+ }
+ }
+
+ _setFileValue(relPrefName, absPrefName, file) {
+ Services.prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, {
+ QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]),
+ file,
+ relativeToKey: "ProfD",
+ });
+ Services.prefs.setComplexValue(absPrefName, Ci.nsIFile, file);
+ }
+}
diff --git a/comm/mailnews/base/src/OAuth2.jsm b/comm/mailnews/base/src/OAuth2.jsm
new file mode 100644
index 0000000000..c5148d41a7
--- /dev/null
+++ b/comm/mailnews/base/src/OAuth2.jsm
@@ -0,0 +1,364 @@
+/* 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/. */
+
+/**
+ * Provides OAuth 2.0 authentication.
+ *
+ * @see RFC 6749
+ */
+var EXPORTED_SYMBOLS = ["OAuth2"];
+
+var { CryptoUtils } = ChromeUtils.importESModule(
+ "resource://services-crypto/utils.sys.mjs"
+);
+
+// Only allow one connecting window per endpoint.
+var gConnecting = {};
+
+/**
+ * Constructor for the OAuth2 object.
+ *
+ * @class
+ * @param {?string} scope - The scope as specified by RFC 6749 Section 3.3.
+ * Will not be included in the requests if falsy.
+ * @param {object} issuerDetails
+ * @param {string} issuerDetails.authorizationEndpoint - The authorization
+ * endpoint as defined by RFC 6749 Section 3.1.
+ * @param {string} issuerDetails.clientId - The client_id as specified by RFC
+ * 6749 Section 2.3.1.
+ * @param {string} issuerDetails.clientSecret - The client_secret as specified
+ * in RFC 6749 section 2.3.1. Will not be included in the requests if null.
+ * @param {boolean} issuerDetails.usePKCE - Whether to use PKCE as specified
+ * in RFC 7636 during the oauth registration process
+ * @param {string} issuerDetails.redirectionEndpoint - The redirect_uri as
+ * specified by RFC 6749 section 3.1.2.
+ * @param {string} issuerDetails.tokenEndpoint - The token endpoint as defined
+ * by RFC 6749 Section 3.2.
+ */
+function OAuth2(scope, issuerDetails) {
+ this.scope = scope;
+ this.authorizationEndpoint = issuerDetails.authorizationEndpoint;
+ this.clientId = issuerDetails.clientId;
+ this.consumerSecret = issuerDetails.clientSecret || null;
+ this.usePKCE = issuerDetails.usePKCE;
+ this.redirectionEndpoint =
+ issuerDetails.redirectionEndpoint || "http://localhost";
+ this.tokenEndpoint = issuerDetails.tokenEndpoint;
+
+ this.extraAuthParams = [];
+
+ this.log = console.createInstance({
+ prefix: "mailnews.oauth",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.oauth.loglevel",
+ });
+}
+
+OAuth2.prototype = {
+ clientId: null,
+ consumerSecret: null,
+ requestWindowURI: "chrome://messenger/content/browserRequest.xhtml",
+ requestWindowFeatures: "chrome,centerscreen,width=980,height=750",
+ requestWindowTitle: "",
+ scope: null,
+ usePKCE: false,
+ codeChallenge: null,
+
+ accessToken: null,
+ refreshToken: null,
+ tokenExpires: 0,
+
+ connect(aSuccess, aFailure, aWithUI, aRefresh) {
+ this.connectSuccessCallback = aSuccess;
+ this.connectFailureCallback = aFailure;
+
+ if (this.accessToken && !this.tokenExpired && !aRefresh) {
+ aSuccess();
+ } else if (this.refreshToken) {
+ this.requestAccessToken(this.refreshToken, true);
+ } else {
+ if (!aWithUI) {
+ aFailure('{ "error": "auth_noui" }');
+ return;
+ }
+ if (gConnecting[this.authorizationEndpoint]) {
+ aFailure("Window already open");
+ return;
+ }
+ this.requestAuthorization();
+ }
+ },
+
+ /**
+ * True if the token has expired, or will expire within the grace time.
+ */
+ get tokenExpired() {
+ // 30 seconds to allow for network inefficiency, clock drift, etc.
+ const OAUTH_GRACE_TIME_MS = 30 * 1000;
+ return this.tokenExpires - OAUTH_GRACE_TIME_MS < Date.now();
+ },
+
+ requestAuthorization() {
+ let params = new URLSearchParams({
+ response_type: "code",
+ client_id: this.clientId,
+ redirect_uri: this.redirectionEndpoint,
+ });
+
+ // The scope is optional.
+ if (this.scope) {
+ params.append("scope", this.scope);
+ }
+
+ // See rfc7636
+ if (this.usePKCE) {
+ // Convert base64 to base64url (rfc4648#section-5)
+ const to_b64url = b =>
+ b.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
+
+ params.append("code_challenge_method", "S256");
+
+ // rfc7636#section-4.1
+ // code_verifier = high-entropy cryptographic random STRING ... with a minimum
+ // length of 43 characters and a maximum length of 128 characters.
+ const code_verifier = to_b64url(
+ btoa(CryptoUtils.generateRandomBytesLegacy(64))
+ );
+ this.codeVerifier = code_verifier;
+
+ // rfc7636#section-4.2
+ // code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))
+ const code_challenge = to_b64url(CryptoUtils.sha256Base64(code_verifier));
+ params.append("code_challenge", code_challenge);
+ }
+
+ for (let [name, value] of this.extraAuthParams) {
+ params.append(name, value);
+ }
+
+ let authEndpointURI = this.authorizationEndpoint + "?" + params.toString();
+ this.log.info(
+ "Interacting with the resource owner to obtain an authorization grant " +
+ "from the authorization endpoint: " +
+ authEndpointURI
+ );
+
+ this._browserRequest = {
+ account: this,
+ url: authEndpointURI,
+ _active: true,
+ iconURI: "",
+ cancelled() {
+ if (!this._active) {
+ return;
+ }
+
+ this.account.finishAuthorizationRequest();
+ this.account.onAuthorizationFailed(
+ Cr.NS_ERROR_ABORT,
+ '{ "error": "cancelled"}'
+ );
+ },
+
+ loaded(aWindow, aWebProgress) {
+ if (!this._active) {
+ return;
+ }
+
+ this._listener = {
+ window: aWindow,
+ webProgress: aWebProgress,
+ _parent: this.account,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIWebProgressListener",
+ "nsISupportsWeakReference",
+ ]),
+
+ _cleanUp() {
+ this.webProgress.removeProgressListener(this);
+ this.window.close();
+ delete this.window;
+ },
+
+ _checkForRedirect(url) {
+ if (!url.startsWith(this._parent.redirectionEndpoint)) {
+ return;
+ }
+
+ this._parent.finishAuthorizationRequest();
+ this._parent.onAuthorizationReceived(url);
+ },
+
+ onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ const wpl = Ci.nsIWebProgressListener;
+ if (aStateFlags & (wpl.STATE_START | wpl.STATE_IS_NETWORK)) {
+ let channel = aRequest.QueryInterface(Ci.nsIChannel);
+ this._checkForRedirect(channel.URI.spec);
+ }
+ },
+ onLocationChange(aWebProgress, aRequest, aLocation) {
+ this._checkForRedirect(aLocation.spec);
+ },
+ onProgressChange() {},
+ onStatusChange() {},
+ onSecurityChange() {},
+ };
+ aWebProgress.addProgressListener(
+ this._listener,
+ Ci.nsIWebProgress.NOTIFY_ALL
+ );
+ aWindow.document.title = this.account.requestWindowTitle;
+ },
+ };
+
+ const windowPrivacy = Services.prefs.getBoolPref(
+ "mailnews.oauth.usePrivateBrowser",
+ false
+ )
+ ? "private"
+ : "non-private";
+ const windowFeatures = `${this.requestWindowFeatures},${windowPrivacy}`;
+
+ this.wrappedJSObject = this._browserRequest;
+ gConnecting[this.authorizationEndpoint] = true;
+ Services.ww.openWindow(
+ null,
+ this.requestWindowURI,
+ null,
+ windowFeatures,
+ this
+ );
+ },
+ finishAuthorizationRequest() {
+ gConnecting[this.authorizationEndpoint] = false;
+ if (!("_browserRequest" in this)) {
+ return;
+ }
+
+ this._browserRequest._active = false;
+ if ("_listener" in this._browserRequest) {
+ this._browserRequest._listener._cleanUp();
+ }
+ delete this._browserRequest;
+ },
+
+ /**
+ * @param {string} aURL - Redirection URI with additional parameters.
+ */
+ onAuthorizationReceived(aURL) {
+ this.log.info("OAuth2 authorization response received: url=" + aURL);
+ const url = new URL(aURL);
+ if (url.searchParams.has("code")) {
+ // @see RFC 6749 section 4.1.2: Authorization Response
+ this.requestAccessToken(url.searchParams.get("code"), false);
+ } else {
+ // @see RFC 6749 section 4.1.2.1: Error Response
+ if (url.searchParams.has("error")) {
+ let error = url.searchParams.get("error");
+ let errorDescription = url.searchParams.get("error_description") || "";
+ if (error == "invalid_scope") {
+ errorDescription += ` Invalid scope: ${this.scope}.`;
+ }
+ if (url.searchParams.has("error_uri")) {
+ errorDescription += ` See ${url.searchParams.get("error_uri")}.`;
+ }
+ this.log.error(`Authorization error [${error}]: ${errorDescription}`);
+ }
+ this.onAuthorizationFailed(null, aURL);
+ }
+ },
+
+ onAuthorizationFailed(aError, aData) {
+ this.connectFailureCallback(aData);
+ },
+
+ /**
+ * Request a new access token, or refresh an existing one.
+ *
+ * @param {string} aCode - The token issued to the client.
+ * @param {boolean} aRefresh - Whether it's a refresh of a token or not.
+ */
+ requestAccessToken(aCode, aRefresh) {
+ // @see RFC 6749 section 4.1.3. Access Token Request
+ // @see RFC 6749 section 6. Refreshing an Access Token
+
+ let data = new URLSearchParams();
+ data.append("client_id", this.clientId);
+ if (this.consumerSecret !== null) {
+ // Section 2.3.1. of RFC 6749 states that empty secrets MAY be omitted
+ // by the client. This OAuth implementation delegates this decision to
+ // the caller: If the secret is null, it will be omitted.
+ data.append("client_secret", this.consumerSecret);
+ }
+
+ if (aRefresh) {
+ this.log.info(
+ `Making a refresh request to the token endpoint: ${this.tokenEndpoint}`
+ );
+ data.append("grant_type", "refresh_token");
+ data.append("refresh_token", aCode);
+ } else {
+ this.log.info(
+ `Making access token request to the token endpoint: ${this.tokenEndpoint}`
+ );
+ data.append("grant_type", "authorization_code");
+ data.append("code", aCode);
+ data.append("redirect_uri", this.redirectionEndpoint);
+ if (this.usePKCE) {
+ data.append("code_verifier", this.codeVerifier);
+ }
+ }
+
+ fetch(this.tokenEndpoint, {
+ method: "POST",
+ cache: "no-cache",
+ body: data,
+ })
+ .then(response => response.json())
+ .then(result => {
+ let resultStr = JSON.stringify(result, null, 2);
+ if ("error" in result) {
+ // RFC 6749 section 5.2. Error Response
+ let err = result.error;
+ if ("error_description" in result) {
+ err += "; " + result.error_description;
+ }
+ if ("error_uri" in result) {
+ err += "; " + result.error_uri;
+ }
+ this.log.warn(`Error response from the authorization server: ${err}`);
+ this.log.info(`Error response details: ${resultStr}`);
+
+ // Typically in production this would be {"error": "invalid_grant"}.
+ // That is, the token expired or was revoked (user changed password?).
+ // Reset the tokens we have and call success so that the auth flow
+ // will be re-triggered.
+ this.accessToken = null;
+ this.refreshToken = null;
+ this.connectSuccessCallback();
+ return;
+ }
+
+ // RFC 6749 section 5.1. Successful Response
+ this.log.info(
+ `Successful response from the authorization server: ${resultStr}`
+ );
+ this.accessToken = result.access_token;
+ if ("refresh_token" in result) {
+ this.refreshToken = result.refresh_token;
+ }
+ if ("expires_in" in result) {
+ this.tokenExpires = new Date().getTime() + result.expires_in * 1000;
+ } else {
+ this.tokenExpires = Number.MAX_VALUE;
+ }
+ this.connectSuccessCallback();
+ })
+ .catch(err => {
+ this.log.info(`Connection to authorization server failed: ${err}`);
+ this.connectFailureCallback(err);
+ });
+ },
+};
diff --git a/comm/mailnews/base/src/OAuth2Module.jsm b/comm/mailnews/base/src/OAuth2Module.jsm
new file mode 100644
index 0000000000..79826779c4
--- /dev/null
+++ b/comm/mailnews/base/src/OAuth2Module.jsm
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["OAuth2Module"];
+
+var { OAuth2 } = ChromeUtils.import("resource:///modules/OAuth2.jsm");
+var { OAuth2Providers } = ChromeUtils.import(
+ "resource:///modules/OAuth2Providers.jsm"
+);
+
+/**
+ * OAuth2Module is the glue layer that gives XPCOM access to an OAuth2
+ * bearer token it can use to authenticate in SASL steps.
+ * It also takes care of persising the refreshToken for later usage.
+ *
+ * @implements {msgIOAuth2Module}
+ */
+function OAuth2Module() {}
+OAuth2Module.prototype = {
+ QueryInterface: ChromeUtils.generateQI(["msgIOAuth2Module"]),
+
+ initFromSmtp(aServer) {
+ return this._initPrefs(
+ "mail.smtpserver." + aServer.key + ".",
+ aServer.username,
+ aServer.hostname
+ );
+ },
+ initFromMail(aServer) {
+ return this._initPrefs(
+ "mail.server." + aServer.key + ".",
+ aServer.username,
+ aServer.hostName
+ );
+ },
+ initFromABDirectory(aDirectory, aHostname) {
+ this._initPrefs(
+ aDirectory.dirPrefId + ".",
+ aDirectory.getStringValue("carddav.username", "") || aDirectory.UID,
+ aHostname
+ );
+ },
+ _initPrefs(root, aUsername, aHostname) {
+ let issuer = Services.prefs.getStringPref(root + "oauth2.issuer", null);
+ let scope = Services.prefs.getStringPref(root + "oauth2.scope", null);
+
+ let details = OAuth2Providers.getHostnameDetails(aHostname);
+ if (
+ details &&
+ (details[0] != issuer ||
+ !scope?.split(" ").every(s => details[1].split(" ").includes(s)))
+ ) {
+ // Found in the list of hardcoded providers. Use the hardcoded values.
+ // But only if what we had wasn't a narrower scope of current
+ // defaults. Updating scope would cause re-authorization.
+ [issuer, scope] = details;
+ // Store them for the future, can be useful once we support
+ // dynamic registration.
+ Services.prefs.setStringPref(root + "oauth2.issuer", issuer);
+ Services.prefs.setStringPref(root + "oauth2.scope", scope);
+ }
+ if (!issuer || !scope) {
+ // We need these properties for OAuth2 support.
+ return false;
+ }
+
+ // Find the app key we need for the OAuth2 string. Eventually, this should
+ // be using dynamic client registration, but there are no current
+ // implementations that we can test this with.
+ const issuerDetails = OAuth2Providers.getIssuerDetails(issuer);
+ if (!issuerDetails.clientId) {
+ return false;
+ }
+
+ // Username is needed to generate the XOAUTH2 string.
+ this._username = aUsername;
+ // loginOrigin is needed to save the refresh token in the password manager.
+ this._loginOrigin = "oauth://" + issuer;
+ // We use the scope to indicate realm when storing in the password manager.
+ this._scope = scope;
+
+ // Define the OAuth property and store it.
+ this._oauth = new OAuth2(scope, issuerDetails);
+
+ // Try hinting the username...
+ this._oauth.extraAuthParams = [["login_hint", aUsername]];
+
+ // Set the window title to something more useful than "Unnamed"
+ this._oauth.requestWindowTitle = Services.strings
+ .createBundle("chrome://messenger/locale/messenger.properties")
+ .formatStringFromName("oauth2WindowTitle", [aUsername, aHostname]);
+
+ // This stores the refresh token in the login manager.
+ Object.defineProperty(this._oauth, "refreshToken", {
+ get: () => this.refreshToken,
+ set: token => {
+ this.refreshToken = token;
+ },
+ });
+
+ return true;
+ },
+
+ get refreshToken() {
+ for (let login of Services.logins.findLogins(this._loginOrigin, null, "")) {
+ if (
+ login.username == this._username &&
+ (login.httpRealm == this._scope ||
+ login.httpRealm.split(" ").includes(this._scope))
+ ) {
+ return login.password;
+ }
+ }
+ return "";
+ },
+ set refreshToken(token) {
+ // Check if we already have a login with this username, and modify the
+ // password on that, if we do.
+ let logins = Services.logins.findLogins(
+ this._loginOrigin,
+ null,
+ this._scope
+ );
+ for (let login of logins) {
+ if (login.username == this._username) {
+ if (token) {
+ if (token != login.password) {
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(
+ Ci.nsIWritablePropertyBag
+ );
+ propBag.setProperty("password", token);
+ Services.logins.modifyLogin(login, propBag);
+ }
+ } else {
+ Services.logins.removeLogin(login);
+ }
+ return;
+ }
+ }
+
+ // Unless the token is null, we need to create and fill in a new login
+ if (token) {
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ login.init(
+ this._loginOrigin,
+ null,
+ this._scope,
+ this._username,
+ token,
+ "",
+ ""
+ );
+ Services.logins.addLogin(login);
+ }
+ },
+
+ connect(aWithUI, aListener) {
+ let oauth = this._oauth;
+ let promptlistener = {
+ onPromptStartAsync(callback) {
+ this.onPromptAuthAvailable(callback);
+ },
+
+ onPromptAuthAvailable: callback => {
+ oauth.connect(
+ () => {
+ aListener.onSuccess(
+ btoa(
+ `user=${this._username}\x01auth=Bearer ${oauth.accessToken}\x01\x01`
+ )
+ );
+ if (callback) {
+ callback.onAuthResult(true);
+ }
+ },
+ () => {
+ aListener.onFailure(Cr.NS_ERROR_ABORT);
+ if (callback) {
+ callback.onAuthResult(false);
+ }
+ },
+ aWithUI,
+ false
+ );
+ },
+ onPromptCanceled() {
+ aListener.onFailure(Cr.NS_ERROR_ABORT);
+ },
+ onPromptStart() {},
+ };
+
+ let asyncprompter = Cc[
+ "@mozilla.org/messenger/msgAsyncPrompter;1"
+ ].getService(Ci.nsIMsgAsyncPrompter);
+ let promptkey = this._loginOrigin + "/" + this._username;
+ asyncprompter.queueAsyncAuthPrompt(promptkey, false, promptlistener);
+ },
+};
diff --git a/comm/mailnews/base/src/OAuth2Providers.jsm b/comm/mailnews/base/src/OAuth2Providers.jsm
new file mode 100644
index 0000000000..86300b2ead
--- /dev/null
+++ b/comm/mailnews/base/src/OAuth2Providers.jsm
@@ -0,0 +1,259 @@
+/* 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/. */
+
+/**
+ * Details of supported OAuth2 Providers.
+ */
+var EXPORTED_SYMBOLS = ["OAuth2Providers"];
+
+// When we add a Google mail account, ask for address book and calendar scopes
+// as well. Then we can add an address book or calendar without asking again.
+//
+// Don't ask for all the scopes when adding an address book or calendar
+// independently of the mail set-up process. If a mail account already exists,
+// we already have a token, and if it doesn't the user is likely to be setting
+// up an address book/calendar without wanting mail.
+const GOOGLE_SCOPES =
+ "https://mail.google.com/ https://www.googleapis.com/auth/carddav https://www.googleapis.com/auth/calendar";
+const FASTMAIL_SCOPES =
+ "https://www.fastmail.com/dev/protocol-imap https://www.fastmail.com/dev/protocol-pop https://www.fastmail.com/dev/protocol-smtp https://www.fastmail.com/dev/protocol-carddav https://www.fastmail.com/dev/protocol-caldav";
+const COMCAST_SCOPES = "https://email.comcast.net/ profile openid";
+
+/**
+ * Map of hostnames to [issuer, scope].
+ */
+var kHostnames = new Map([
+ ["imap.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["smtp.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["pop.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["imap.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["smtp.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ ["pop.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]],
+ [
+ "www.googleapis.com",
+ ["accounts.google.com", "https://www.googleapis.com/auth/carddav"],
+ ],
+
+ ["imap.mail.ru", ["o2.mail.ru", "mail.imap"]],
+ ["smtp.mail.ru", ["o2.mail.ru", "mail.imap"]],
+
+ ["imap.yandex.com", ["oauth.yandex.com", "mail:imap_full"]],
+ ["smtp.yandex.com", ["oauth.yandex.com", "mail:smtp"]],
+
+ ["imap.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
+ ["pop.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
+ ["smtp.mail.yahoo.com", ["login.yahoo.com", "mail-w"]],
+
+ ["imap.aol.com", ["login.aol.com", "mail-w"]],
+ ["pop.aol.com", ["login.aol.com", "mail-w"]],
+ ["smtp.aol.com", ["login.aol.com", "mail-w"]],
+
+ [
+ "outlook.office365.com",
+ [
+ "login.microsoftonline.com",
+ "https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access",
+ ],
+ ],
+ [
+ "smtp.office365.com",
+ [
+ "login.microsoftonline.com",
+ "https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access",
+ ],
+ ],
+
+ ["imap.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
+ ["pop.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
+ ["smtp.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]],
+ [
+ "carddav.fastmail.com",
+ ["www.fastmail.com", "https://www.fastmail.com/dev/protocol-carddav"],
+ ],
+
+ ["imap.comcast.net", ["comcast.net", COMCAST_SCOPES]],
+ ["pop.comcast.net", ["comcast.net", COMCAST_SCOPES]],
+ ["smtp.comcast.net", ["comcast.net", COMCAST_SCOPES]],
+
+ // For testing purposes.
+ ["mochi.test", ["mochi.test", "test_scope"]],
+]);
+
+/**
+ * Map of issuers to clientId, clientSecret, authorizationEndpoint, tokenEndpoint,
+ * and usePKCE (RFC7636).
+ * Issuer is a unique string for the organization that a Thunderbird account
+ * was registered at.
+ *
+ * For the moment these details are hard-coded, since dynamic client
+ * registration is not yet supported. Don't copy these values for your
+ * own application - register one for yourself! This code (and possibly even the
+ * registration itself) will disappear when this is switched to dynamic
+ * client registration.
+ */
+var kIssuers = new Map([
+ [
+ "accounts.google.com",
+ {
+ clientId:
+ "406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com",
+ clientSecret: "kSmqreRr0qwBWJgbf5Y-PjSU",
+ authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth",
+ tokenEndpoint: "https://www.googleapis.com/oauth2/v3/token",
+ },
+ ],
+ [
+ "o2.mail.ru",
+ {
+ clientId: "thunderbird",
+ clientSecret: "I0dCAXrcaNFujaaY",
+ authorizationEndpoint: "https://o2.mail.ru/login",
+ tokenEndpoint: "https://o2.mail.ru/token",
+ },
+ ],
+ [
+ "oauth.yandex.com",
+ {
+ clientId: "2a00bba7374047a6ab79666485ffce31",
+ clientSecret: "3ded85b4ec574c2187a55dc49d361280",
+ authorizationEndpoint: "https://oauth.yandex.com/authorize",
+ tokenEndpoint: "https://oauth.yandex.com/token",
+ },
+ ],
+ [
+ "login.yahoo.com",
+ {
+ clientId:
+ "dj0yJmk9NUtCTWFMNVpTaVJmJmQ9WVdrOVJ6UjVTa2xJTXpRbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD0yYw--",
+ clientSecret: "f2de6a30ae123cdbc258c15e0812799010d589cc",
+ authorizationEndpoint: "https://api.login.yahoo.com/oauth2/request_auth",
+ tokenEndpoint: "https://api.login.yahoo.com/oauth2/get_token",
+ },
+ ],
+ [
+ "login.aol.com",
+ {
+ clientId:
+ "dj0yJmk9OXRHc1FqZHRQYzVvJmQ9WVdrOU1UQnJOR0pvTjJrbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD02NQ--",
+ clientSecret: "79c1c11991d148ddd02a919000d69879942fc278",
+ authorizationEndpoint: "https://api.login.aol.com/oauth2/request_auth",
+ tokenEndpoint: "https://api.login.aol.com/oauth2/get_token",
+ },
+ ],
+
+ [
+ "login.microsoftonline.com",
+ {
+ clientId: "9e5f94bc-e8a4-4e73-b8be-63364c29d753", // Application (client) ID
+ // https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
+ authorizationEndpoint:
+ "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
+ tokenEndpoint:
+ "https://login.microsoftonline.com/common/oauth2/v2.0/token",
+ redirectionEndpoint: "https://localhost",
+ },
+ ],
+
+ [
+ "www.fastmail.com",
+ {
+ clientId: "35f141ae",
+ authorizationEndpoint: "https://api.fastmail.com/oauth/authorize",
+ tokenEndpoint: "https://api.fastmail.com/oauth/refresh",
+ usePKCE: true,
+ },
+ ],
+
+ [
+ "comcast.net",
+ {
+ clientId: "thunderbird-oauth",
+ clientSecret: "fc5d0a314549bb3d059e0cec751fa4bd40a9cc7b",
+ authorizationEndpoint: "https://oauth.xfinity.com/oauth/authorize",
+ tokenEndpoint: "https://oauth.xfinity.com/oauth/token",
+ usePKCE: true,
+ },
+ ],
+
+ // For testing purposes.
+ [
+ "mochi.test",
+ {
+ clientId: "test_client_id",
+ clientSecret: "test_secret",
+ authorizationEndpoint:
+ "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/redirect_auto.sjs",
+ tokenEndpoint:
+ "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/token.sjs",
+ // I don't know why, but tests refuse to work with a plain HTTP endpoint
+ // (the request is redirected to HTTPS, which we're not listening to).
+ // Just use an HTTPS endpoint.
+ redirectionEndpoint: "https://localhost",
+ },
+ ],
+]);
+
+/**
+ * OAuth2Providers: Methods to lookup OAuth2 parameters for supported OAuth2
+ * providers.
+ */
+var OAuth2Providers = {
+ /**
+ * Map a hostname to the relevant issuer and scope.
+ *
+ * @param {string} hostname - The hostname of the server. For example
+ * "imap.googlemail.com".
+ *
+ * @returns {Array} An array containing [issuer, scope] for the hostname, or
+ * undefined if not found.
+ * - issuer is a string representing the organization
+ * - scope is an OAuth2 parameter describing the required access level
+ */
+ getHostnameDetails(hostname) {
+ // During CardDAV SRV autodiscovery, rfc6764#section-6 says:
+ //
+ // * The client will need to make authenticated HTTP requests to
+ // the service. Typically, a "user identifier" is required for
+ // some form of user/password authentication. When a user
+ // identifier is required, clients MUST first use the "mailbox"
+ //
+ // However macOS Contacts does not do this and just uses the "localpart"
+ // instead. To work around this bug, during SRV autodiscovery Fastmail
+ // returns SRV records of the form '0 1 443 d[0-9]+.carddav.fastmail.com.'
+ // which encodes the internal domainid of the queried SRV domain in the
+ // sub-domain of the Target (rfc2782) of the SRV result. This can
+ // then be extracted from the Host header on each DAV request, the
+ // original domain looked up and attached to the "localpart" to create
+ // a full "mailbox", allowing autodiscovery to just work for usernames
+ // in any domain including self hosted domains.
+ //
+ // So for this hostname -> issuer/scope lookup to work, we need to
+ // look not just at the hostname, but also any sub-domains of this
+ // hostname.
+ while (hostname.includes(".")) {
+ let foundHost = kHostnames.get(hostname);
+ if (foundHost) {
+ return foundHost;
+ }
+ hostname = hostname.replace(/^[^.]*[.]/, "");
+ }
+ return undefined;
+ },
+
+ /**
+ * Map an issuer to OAuth2 account details.
+ *
+ * @param {string} issuer - The organization issuing OAuth2 parameters, e.g.
+ * "accounts.google.com".
+ *
+ * @returns {Array} An array containing [clientId, clientSecret, authorizationEndpoint, tokenEndpoint].
+ * clientId and clientSecret are strings representing the account registered
+ * for Thunderbird with the organization.
+ * authorizationEndpoint and tokenEndpoint are url strings representing
+ * endpoints to access OAuth2 authentication.
+ */
+ getIssuerDetails(issuer) {
+ return kIssuers.get(issuer);
+ },
+};
diff --git a/comm/mailnews/base/src/TemplateUtils.jsm b/comm/mailnews/base/src/TemplateUtils.jsm
new file mode 100644
index 0000000000..fca56fca67
--- /dev/null
+++ b/comm/mailnews/base/src/TemplateUtils.jsm
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["PluralStringFormatter", "makeFriendlyDateAgo"];
+
+var { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+function PluralStringFormatter(aBundleURI) {
+ this._bundle = Services.strings.createBundle(aBundleURI);
+}
+
+PluralStringFormatter.prototype = {
+ get(aStringName, aReplacements, aPluralCount) {
+ let str = this._bundle.GetStringFromName(aStringName);
+ if (aPluralCount !== undefined) {
+ str = PluralForm.get(aPluralCount, str);
+ }
+ if (aReplacements !== undefined) {
+ for (let i = 0; i < aReplacements.length; i++) {
+ str = str.replace("#" + (i + 1), aReplacements[i]);
+ }
+ }
+ return str;
+ },
+};
+
+var gTemplateUtilsStrings = new PluralStringFormatter(
+ "chrome://messenger/locale/templateUtils.properties"
+);
+
+const _dateFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "short",
+});
+const _dayMonthFormatter = new Services.intl.DateTimeFormat(undefined, {
+ month: "long",
+ day: "numeric",
+});
+const _timeFormatter = new Services.intl.DateTimeFormat(undefined, {
+ timeStyle: "short",
+});
+const _weekdayFormatter = new Services.intl.DateTimeFormat(undefined, {
+ weekday: "long",
+});
+
+/**
+ * Helper function to generate a localized "friendly" representation of
+ * time relative to the present. If the time input is "today", it returns
+ * a string corresponding to just the time. If it's yesterday, it returns
+ * "yesterday" (localized). If it's in the last week, it returns the day
+ * of the week. If it's before that, it returns the date.
+ *
+ * @param {Date} time - The time (better be in the past!)
+ * @returns {string} A "human-friendly" representation of that time
+ * relative to now.
+ */
+function makeFriendlyDateAgo(time) {
+ // TODO: use Intl.RelativeTimeFormat instead.
+ // Figure out when today begins
+ let now = new Date();
+ let today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+
+ // Get the end time to display
+ let end = time;
+
+ // Figure out if the end time is from today, yesterday,
+ // this week, etc.
+ let dateTime;
+ let kDayInMsecs = 24 * 60 * 60 * 1000;
+ let k6DaysInMsecs = 6 * kDayInMsecs;
+ if (end >= today) {
+ // activity finished after today started, show the time
+ dateTime = _timeFormatter.format(end);
+ } else if (today - end < kDayInMsecs) {
+ // activity finished after yesterday started, show yesterday
+ dateTime = gTemplateUtilsStrings.get("yesterday");
+ } else if (today - end < k6DaysInMsecs) {
+ // activity finished after last week started, show day of week
+ dateTime = _weekdayFormatter.format(end);
+ } else if (now.getFullYear() == end.getFullYear()) {
+ // activity must have been from some time ago.. show month/day
+ dateTime = _dayMonthFormatter.format(end);
+ } else {
+ // not this year, so show full date format
+ dateTime = _dateFormatter.format(end);
+ }
+ return dateTime;
+}
diff --git a/comm/mailnews/base/src/UrlListener.cpp b/comm/mailnews/base/src/UrlListener.cpp
new file mode 100644
index 0000000000..ce2ef4904a
--- /dev/null
+++ b/comm/mailnews/base/src/UrlListener.cpp
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "UrlListener.h"
+
+NS_IMPL_ISUPPORTS(UrlListener, nsIUrlListener)
+
+NS_IMETHODIMP UrlListener::OnStartRunningUrl(nsIURI* url) {
+ if (!mStartFn) {
+ return NS_OK;
+ }
+ return mStartFn(url);
+}
+
+NS_IMETHODIMP UrlListener::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
+ if (!mStopFn) {
+ return NS_OK;
+ }
+ return mStopFn(url, exitCode);
+}
diff --git a/comm/mailnews/base/src/UrlListener.h b/comm/mailnews/base/src/UrlListener.h
new file mode 100644
index 0000000000..22a0597854
--- /dev/null
+++ b/comm/mailnews/base/src/UrlListener.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef UrlListener_h__
+#define UrlListener_h__
+
+#include <functional> // For std::function.
+#include "nsIUrlListener.h"
+class nsIURI;
+
+/**
+ * UrlListener is a small nsIUrlListener implementation which allows
+ * callable objects (including lambdas) to be plugged in instead of deriving
+ * your own nsIUrlListener.
+ *
+ * The aim is to encourage more readable code by allowing the start/stop
+ * notifications of a long-running operation to be handled near to where the
+ * operation was initiated.
+ *
+ * A contrived example:
+ *
+ * void Kick() {
+ * UrlListener* listener = new UrlListener;
+ * listener->mStopFn = [](nsIURI* url, nsresult status) -> nsresult {
+ * // Note that we may get here waaaaaaay after Kick() has returned...
+ * printf("LongRunningOperation is finished.\n");
+ * return NS_OK;
+ * };
+ * thingService.startLongRunningOperation(listener);
+ * //...continue doing other stuff while operation is ongoing...
+ * }
+ *
+ * Traditionally, c-c code has tended to use multiple inheritance to add
+ * listener callbacks to the class of the object initiating the operation.
+ * This has a couple of undesirable side effects:
+ *
+ * 1) It separates out the onStopRunningUrl handling into some other
+ * part of the code, which makes the order of things much harder to follow.
+ * 2) Often the same onStopRunningUrl handler will be used for many different
+ * kinds of operations (see nsImapMailFolder::OnStopRunningUrl(), for
+ * example).
+ * 3) It exposes implementation details as part of the public interface
+ * e.g see all the listener types nsMsgDBFolder derives from to implement
+ * it's internals. That's all just confusing noise that shouldn't be seen
+ * from outside the class.
+ *
+ * Just as PromiseTestUtils.jsm brings the Javascript side up from callback
+ * hell to async lovelyness, this can be used to raise the C++ side from
+ * callback-somewhere-else-maybe-in-this-class-but-who-can-really-tell hell
+ * up to normal callback hell :-)
+ *
+ */
+class UrlListener : public nsIUrlListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+
+ UrlListener() {}
+ /**
+ * mStartFn and mStopFn are the OnStartRunning() and OnStopRunningUrl()
+ * handlers. It's fine for them to be null (often you'll only need mStopFn).
+ */
+ std::function<nsresult(nsIURI*)> mStartFn;
+ std::function<nsresult(nsIURI*, nsresult)> mStopFn;
+
+ protected:
+ virtual ~UrlListener() {}
+};
+
+#endif // UrlListener_h__
diff --git a/comm/mailnews/base/src/VirtualFolderWrapper.jsm b/comm/mailnews/base/src/VirtualFolderWrapper.jsm
new file mode 100644
index 0000000000..6fc86c8d1e
--- /dev/null
+++ b/comm/mailnews/base/src/VirtualFolderWrapper.jsm
@@ -0,0 +1,257 @@
+/* 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/. */
+
+/*
+ * Wrap everything about virtual folders.
+ */
+
+const EXPORTED_SYMBOLS = ["VirtualFolderHelper"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var VirtualFolderHelper = {
+ /**
+ * Create a new virtual folder (an actual nsIMsgFolder that did not previously
+ * exist), wrapping it in a VirtualFolderWrapper, and returning that wrapper.
+ *
+ * If the call to addSubfolder fails (and therefore throws), we will NOT catch
+ * it.
+ *
+ * @param {string} aFolderName - The name of the new folder to create.
+ * @param {nsIMsgFolder} aParentFolder - The folder in which to create the
+ * search folder.
+ * @param {nsIMsgFolder[]} aSearchFolders A list of nsIMsgFolders that you
+ * want to use as the sources for the virtual folder OR a string that is
+ * the already '|' delimited list of folder URIs to use.
+ * @param {nsIMsgSearchTerms[]} aSearchTerms - The search terms to
+ * use for the virtual folder.
+ * @param {boolean} aOnlineSearch Should the search attempt to use the
+ * server's search capabilities when possible and appropriate?
+ * @returns {VirtualFolderWrapper} The VirtualFolderWrapper wrapping the
+ * newly created folder. You would probably only want this for its
+ * virtualFolder attribute which has the nsIMsgFolder we created.
+ * Be careful about accessing any of the other attributes, as they will
+ * bring its message database back to life.
+ */
+ createNewVirtualFolder(
+ aFolderName,
+ aParentFolder,
+ aSearchFolders,
+ aSearchTerms,
+ aOnlineSearch
+ ) {
+ let msgFolder = aParentFolder.addSubfolder(aFolderName);
+ msgFolder.prettyName = aFolderName;
+ msgFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);
+
+ let wrappedVirt = new VirtualFolderWrapper(msgFolder);
+ wrappedVirt.searchTerms = aSearchTerms;
+ wrappedVirt.searchFolders = aSearchFolders;
+ wrappedVirt.onlineSearch = aOnlineSearch;
+
+ let msgDatabase = msgFolder.msgDatabase;
+ msgDatabase.summaryValid = true;
+ msgDatabase.close(true);
+
+ aParentFolder.notifyFolderAdded(msgFolder);
+ MailServices.accounts.saveVirtualFolders();
+
+ return wrappedVirt;
+ },
+
+ /**
+ * Given an existing nsIMsgFolder that is a virtual folder, wrap it into a
+ * VirtualFolderWrapper.
+ *
+ * @param {nsIMsgFolder} aMsgFolder - The folder to use.
+ */
+ wrapVirtualFolder(aMsgFolder) {
+ return new VirtualFolderWrapper(aMsgFolder);
+ },
+};
+
+/**
+ * Abstracts dealing with the properties of a virtual folder that differentiate
+ * it from a non-virtual folder. A virtual folder is an odd duck. When
+ * holding an nsIMsgFolder that is a virtual folder, it is distinguished by
+ * the virtual flag and a number of properties that tell us the string
+ * representation of its search, the folders it searches over, and whether we
+ * use online searching or not.
+ * Virtual folders and their defining attributes are loaded from
+ * virtualFolders.dat (in the profile directory) by the account manager at
+ * startup, (re-)creating them if need be. It also saves them back to the
+ * file at shutdown. The most important thing the account manager does is to
+ * create VirtualFolderChangeListener instances that are registered with the
+ * message database service. This means that if one of the databases for the
+ * folders that the virtual folder includes is opened for some reason (for
+ * example, new messages are added to the folder because of a filter or they
+ * are delivered there), the virtual folder gets a chance to know about this
+ * and update the virtual folder's "cache" of information, such as the message
+ * counts or the presence of the message in the folder.
+ * The odd part is that a lot of the virtual folder logic also happens as a
+ * result of the nsMsgDBView subclasses being told the search query and the
+ * underlying folders. This makes for an odd collaboration of UI and backend
+ * logic.
+ *
+ * Justification for this class: Virtual folders aren't all that complex, but
+ * they are complex enough that we don't want to have the same code duplicated
+ * all over the place. We also don't want to have a loose assembly of global
+ * functions for working with them. So here we are.
+ *
+ * Important! Accessing any of our attributes results in the message database
+ * being loaded so that we can access the dBFolderInfo associated with the
+ * database. The message database is not automatically forgotten by the
+ * folder, which can lead to an (effective) memory leak. Please make sure
+ * that you are playing your part in not leaking memory by only using the
+ * wrapper when you have a serious need to access the database, and by
+ * forcing the folder to forget about the database when you are done by
+ * setting the database to null (unless you know with confidence someone else
+ * definitely wants the database around and will clean it up.)
+ *
+ * @param {nsIMsgFolder} aVirtualFolder - Folder to wrap.
+ */
+function VirtualFolderWrapper(aVirtualFolder) {
+ this.virtualFolder = aVirtualFolder;
+}
+VirtualFolderWrapper.prototype = {
+ /**
+ * @returns {nsIMsgFolders[]} The list of nsIMsgFolders that this virtual
+ * folder is a search over.
+ */
+ get searchFolders() {
+ return this.dbFolderInfo
+ .getCharProperty("searchFolderUri")
+ .split("|")
+ .sort() // Put folders in URI order so a parent is always before a child.
+ .map(uri => MailServices.folderLookup.getOrCreateFolderForURL(uri))
+ .filter(Boolean);
+ },
+ /**
+ * Set the search folders that back this virtual folder.
+ *
+ * @param {string|nsIMsgFolder[]} aFolders - Either a "|"-delimited string of
+ * folder URIs or a list of folders.
+ */
+ set searchFolders(aFolders) {
+ if (typeof aFolders == "string") {
+ this.dbFolderInfo.setCharProperty("searchFolderUri", aFolders);
+ } else {
+ let uris = aFolders.map(folder => folder.URI);
+ this.dbFolderInfo.setCharProperty("searchFolderUri", uris.join("|"));
+ }
+ Services.obs.notifyObservers(this.virtualFolder, "search-folders-changed");
+ },
+
+ /**
+ * @returns {string} a "|"-delimited string containing the URIs of the folders
+ * that back this virtual folder.
+ */
+ get searchFolderURIs() {
+ return this.dbFolderInfo.getCharProperty("searchFolderUri");
+ },
+
+ /**
+ * @returns {nsIMsgSearchTerm[]} The list of search terms that define this
+ * virtual folder.
+ */
+ get searchTerms() {
+ return this.searchTermsSession.searchTerms;
+ },
+ /**
+ * @returns {nsIMsgFilterList} A newly created filter with the search terms
+ * loaded into it that define this virtual folder. The filter is apparently
+ * useful as an nsIMsgSearchSession stand-in to some code.
+ */
+ get searchTermsSession() {
+ // Temporary means it doesn't get exposed to the UI and doesn't get saved to
+ // disk. Which is good, because this is just a trick to parse the string
+ // into search terms.
+ let filterList = MailServices.filters.getTempFilterList(this.virtualFolder);
+ let tempFilter = filterList.createFilter("temp");
+ filterList.parseCondition(tempFilter, this.searchString);
+ return tempFilter;
+ },
+
+ /**
+ * Set the search string for this virtual folder to the stringified version of
+ * the provided list of nsIMsgSearchTerm search terms. If you already have
+ * a strinigified version of the search constraint, just set |searchString|
+ * directly.
+ *
+ * @param {string[]} aTerms - a list of search terms
+ */
+ set searchTerms(aTerms) {
+ let condition = "";
+ for (let term of aTerms) {
+ if (condition) {
+ condition += " ";
+ }
+ if (term.matchAll) {
+ condition = "ALL";
+ break;
+ }
+ condition += term.booleanAnd ? "AND (" : "OR (";
+ condition += term.termAsString + ")";
+ }
+ this.searchString = condition;
+ },
+
+ /**
+ * @returns {string} the set of search terms that define this virtual folder
+ * as a string. You may prefer to use |searchTerms| which converts them
+ * into a list of nsIMsgSearchTerms instead.
+ */
+ get searchString() {
+ return this.dbFolderInfo.getCharProperty("searchStr");
+ },
+ /**
+ * Set the search that defines this virtual folder from a string. If you have
+ * a list of nsIMsgSearchTerms, you should use |searchTerms| instead.
+ *
+ * @param {string} aSearchString
+ */
+ set searchString(aSearchString) {
+ this.dbFolderInfo.setCharProperty("searchStr", aSearchString);
+ },
+
+ /**
+ * @returns {boolean} whether the virtual folder is configured for online search.
+ */
+ get onlineSearch() {
+ return this.dbFolderInfo.getBooleanProperty("searchOnline", false);
+ },
+ /**
+ * Set whether the virtual folder is configured for online search.
+ *
+ * @param {boolean} aOnlineSearch
+ */
+ set onlineSearch(aOnlineSearch) {
+ this.dbFolderInfo.setBooleanProperty("searchOnline", aOnlineSearch);
+ },
+
+ /**
+ * @returns {?nsIDBFolderInfo} The dBFolderInfo associated with the virtual
+ * folder directly. Maybe null. Will cause the message database to be
+ * opened, which may have memory bloat/leak ramifications, so make sure
+ * the folder's database was already going to be opened anyways or that you
+ * call |cleanUpMessageDatabase|.
+ */
+ get dbFolderInfo() {
+ let msgDatabase = this.virtualFolder.msgDatabase;
+ return msgDatabase && msgDatabase.dBFolderInfo;
+ },
+
+ /**
+ * Avoid memory bloat by making the virtual folder forget about its database.
+ * If the database is actually in use (read: someone is keeping it alive by
+ * having references to it from places other than the nsIMsgFolder), the
+ * folder will be able to re-establish the reference for minimal cost.
+ */
+ cleanUpMessageDatabase() {
+ this.virtualFolder.msgDatabase.close(true);
+ this.virtualFolder.msgDatabase = null;
+ },
+};
diff --git a/comm/mailnews/base/src/WinUnreadBadge.jsm b/comm/mailnews/base/src/WinUnreadBadge.jsm
new file mode 100644
index 0000000000..819d8c5719
--- /dev/null
+++ b/comm/mailnews/base/src/WinUnreadBadge.jsm
@@ -0,0 +1,246 @@
+/* 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/.
+ *
+ * Based on https://github.com/bstreiff/unread-badge.
+ *
+ * Copyright (c) 2013-2020 Brandon Streiff
+ */
+
+const EXPORTED_SYMBOLS = ["WinUnreadBadge"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ imgTools: ["@mozilla.org/image/tools;1", "imgITools"],
+ taskbar: ["@mozilla.org/windows-taskbar;1", "nsIWinTaskbar"],
+});
+
+/**
+ * Get an imgIContainer instance from a canvas element.
+ *
+ * @param {HTMLCanvasElement} canvas - The canvas element.
+ * @param {number} width - The width of the canvas to use.
+ * @param {number} height - The height of the canvas to use.
+ * @returns {imgIContainer}
+ */
+function getCanvasAsImgContainer(canvas, width, height) {
+ let imageData = canvas.getContext("2d").getImageData(0, 0, width, height);
+
+ // Create an imgIEncoder so we can turn the image data into a PNG stream.
+ let imgEncoder = Cc["@mozilla.org/image/encoder;2?type=image/png"].getService(
+ Ci.imgIEncoder
+ );
+ imgEncoder.initFromData(
+ imageData.data,
+ imageData.data.length,
+ imageData.width,
+ imageData.height,
+ imageData.width * 4,
+ imgEncoder.INPUT_FORMAT_RGBA,
+ ""
+ );
+
+ // Now turn the PNG stream into an imgIContainer.
+ let imgBuffer = lazy.NetUtil.readInputStreamToString(
+ imgEncoder,
+ imgEncoder.available()
+ );
+ let iconImage = lazy.imgTools.decodeImageFromBuffer(
+ imgBuffer,
+ imgBuffer.length,
+ "image/png"
+ );
+
+ // Close the PNG stream.
+ imgEncoder.close();
+ return iconImage;
+}
+
+/**
+ * Draw text centered in the middle of a CanvasRenderingContext2D.
+ *
+ * @param {CanvasRenderingContext2D} cxt - The canvas context to operate on.
+ * @param {string} text - The text to draw.
+ */
+function drawUnreadCountText(cxt, text) {
+ cxt.save();
+
+ let imageSize = cxt.canvas.width;
+
+ // Use smaller fonts for longer text to try and squeeze it in.
+ let fontSize = imageSize * (0.95 - 0.15 * text.length);
+
+ cxt.font = "500 " + fontSize + "px Calibri";
+ cxt.fillStyle = "#ffffff";
+ cxt.textAlign = "center";
+
+ // TODO: There isn't a textBaseline for accurate vertical centering ('middle' is the
+ // middle of the 'em block', and digits extend higher than 'm'), and the Mozilla core
+ // does not currently support computation of ascenders and descenters in measureText().
+ // So, we just assume that the font is 70% of the 'px' height we requested, then
+ // compute where the baseline ought to be located.
+ let approximateHeight = fontSize * 0.7;
+
+ cxt.textBaseline = "alphabetic";
+ cxt.fillText(
+ text,
+ imageSize / 2,
+ imageSize - (imageSize - approximateHeight) / 2
+ );
+
+ cxt.restore();
+}
+
+/**
+ * Create a flat badge, as is the Windows 8/10 style.
+ *
+ * @param {HTMLCanvasElement} canvas - The canvas element to draw the badge.
+ * @param {string} text - The text to draw in the badge.
+ */
+function createModernBadgeStyle(canvas, text) {
+ let cxt = canvas.getContext("2d");
+ let iconSize = canvas.width;
+
+ // Draw the background.
+ cxt.save();
+ // Solid color first.
+ cxt.fillStyle = "#ff0039";
+ cxt.shadowOffsetX = 0;
+ cxt.shadowOffsetY = 0;
+ cxt.shadowColor = "rgba(0,0,0,0.7)";
+ cxt.shadowBlur = iconSize / 10;
+ cxt.beginPath();
+ cxt.arc(iconSize / 2, iconSize / 2, iconSize / 2.25, 0, Math.PI * 2, true);
+ cxt.fill();
+ cxt.clip();
+ cxt.closePath();
+ cxt.restore();
+
+ drawUnreadCountText(cxt, text);
+}
+
+/**
+ * Downsample by 4X with simple averaging.
+ *
+ * Drawing at 4X and then downscaling like this gives us better results than
+ * using either CanvasRenderingContext2D.drawImage() to resize or letting
+ * the Windows taskbar service handle the resize, both of which seem to just
+ * give us a simple point resize.
+ *
+ * @param {Window} window - The DOM window.
+ * @param {HTMLCanvasElement} canvas - The input canvas element to resize.
+ * @returns {HTMLCanvasElement} The resized canvas element.
+ */
+function downsampleBy4X(window, canvas) {
+ let resizedCanvas = window.document.createElement("canvas");
+ resizedCanvas.width = resizedCanvas.height = canvas.width / 4;
+ resizedCanvas.style.width = resizedCanvas.style.height =
+ resizedCanvas.width + "px";
+
+ let source = canvas
+ .getContext("2d")
+ .getImageData(0, 0, canvas.width, canvas.height);
+ let downsampled = resizedCanvas
+ .getContext("2d")
+ .createImageData(resizedCanvas.width, resizedCanvas.height);
+
+ for (let y = 0; y < resizedCanvas.height; ++y) {
+ for (let x = 0; x < resizedCanvas.width; ++x) {
+ let r = 0,
+ g = 0,
+ b = 0,
+ a = 0;
+ let index;
+
+ for (let i = 0; i < 4; ++i) {
+ for (let j = 0; j < 4; ++j) {
+ index = ((y * 4 + i) * source.width + (x * 4 + j)) * 4;
+ r += source.data[index];
+ g += source.data[index + 1];
+ b += source.data[index + 2];
+ a += source.data[index + 3];
+ }
+ }
+
+ index = (y * downsampled.width + x) * 4;
+ downsampled.data[index] = Math.round(r / 16);
+ downsampled.data[index + 1] = Math.round(g / 16);
+ downsampled.data[index + 2] = Math.round(b / 16);
+ downsampled.data[index + 3] = Math.round(a / 16);
+ }
+ }
+
+ resizedCanvas.getContext("2d").putImageData(downsampled, 0, 0);
+
+ return resizedCanvas;
+}
+
+/**
+ * A module to manage the unread badge icon on Windows.
+ */
+var WinUnreadBadge = {
+ /**
+ * Keeping an instance of nsITaskbarOverlayIconController alive
+ * to show a taskbar icon after the updateUnreadCount method exits.
+ */
+ _controller: null,
+
+ /**
+ * Update the unread badge.
+ *
+ * @param {number} unreadCount - Unread message count.
+ * @param {number} unreadTooltip - Unread message count tooltip.
+ */
+ async updateUnreadCount(unreadCount, unreadTooltip) {
+ let window = Services.wm.getMostRecentBrowserWindow();
+ if (!window) {
+ return;
+ }
+ if (!this._controller) {
+ this._controller = lazy.taskbar.getOverlayIconController(window.docShell);
+ }
+ if (unreadCount == 0) {
+ // Remove the badge if no unread.
+ this._controller.setOverlayIcon(null, "");
+ return;
+ }
+
+ // Draw the badge in a canvas.
+ let smallIconSize = Cc["@mozilla.org/windows-ui-utils;1"].getService(
+ Ci.nsIWindowsUIUtils
+ ).systemSmallIconSize;
+ let iconSize = Math.floor(
+ (window.windowUtils.displayDPI / 96) * smallIconSize
+ );
+ let iconSize4X = iconSize * 4;
+ let badge = window.document.createElement("canvas");
+ badge.width = badge.height = iconSize4X;
+ badge.style.width = badge.style.height = badge.width + "px";
+
+ createModernBadgeStyle(
+ badge,
+ unreadCount < 100 ? unreadCount.toString() : "99+"
+ );
+
+ badge = downsampleBy4X(window, badge);
+ let icon = getCanvasAsImgContainer(badge, iconSize, iconSize);
+ // Purge image from cache to force encodeImage() to not be lazy
+ icon.requestDiscard();
+ // Side effect of encodeImage() is that it decodes original image
+ lazy.imgTools.encodeImage(icon, "image/png");
+ // Somehow this is needed to prevent NS_ERROR_NOT_AVAILABLE error in
+ // setOverlayIcon.
+ await new Promise(resolve => window.setTimeout(resolve));
+
+ this._controller.setOverlayIcon(icon, unreadTooltip);
+ },
+};
diff --git a/comm/mailnews/base/src/components.conf b/comm/mailnews/base/src/components.conf
new file mode 100644
index 0000000000..776148e132
--- /dev/null
+++ b/comm/mailnews/base/src/components.conf
@@ -0,0 +1,359 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ "cid": "{a30be08c-afc8-4fed-9af7-79778a23db23}",
+ "contract_ids": ["@mozilla.org/mail/folder-lookup;1"],
+ "jsm": "resource:///modules/FolderLookupService.jsm",
+ "constructor": "FolderLookupService",
+ },
+ {
+ "cid": "{49b04761-23dd-45d7-903d-619418a4d319}",
+ "contract_ids": ["@mozilla.org/messenger/msgAsyncPrompter;1"],
+ "jsm": "resource:///modules/MsgAsyncPrompter.jsm",
+ "constructor": "MsgAsyncPrompter",
+ },
+ {
+ "cid": "{26ebf3a7-3e52-4b7a-a89b-fa7c0b7506f9}",
+ "contract_ids": ["@mozilla.org/messenger/msgAuthPrompt;1"],
+ "jsm": "resource:///modules/MsgAsyncPrompter.jsm",
+ "constructor": "MsgAuthPrompt",
+ },
+ {
+ "cid": "{b63d8e4c-bf60-439b-be0e-7c9f67291042}",
+ "contract_ids": ["@mozilla.org/mail/oauth2-module;1"],
+ "jsm": "resource:///modules/OAuth2Module.jsm",
+ "constructor": "OAuth2Module",
+ },
+ {
+ "cid": "{740880e6-e299-4165-b82f-df1dcab3ae22}",
+ "contract_ids": ["@mozilla.org/newMailNotificationService;1"],
+ "jsm": "resource:///modules/MailNotificationService.jsm",
+ "constructor": "NewMailNotificationService",
+ "name": "MailNotification",
+ "interfaces": ["mozINewMailNotificationService"],
+ },
+ {
+ "cid": "{37246055-3596-4bfa-911f-3d2977e8d284}",
+ "contract_ids": ["@mozilla.org/mail/auth-module;1"],
+ "type": "nsMailAuthModule",
+ "headers": ["/comm/mailnews/base/src/nsMailAuthModule.h"],
+ },
+ {
+ "cid": "{e9aef539-29db-4936-9fdc-40ba11c70cb3}",
+ "contract_ids": ["@mozilla.org/mail/notification-manager;1"],
+ "jsm": "resource:///modules/MailNotificationManager.jsm",
+ "constructor": "MailNotificationManager",
+ },
+ {
+ "cid": "{4a85a5d0-cddd-11d2-b7f6-00805f05ffa5}",
+ "contract_ids": [
+ "@mozilla.org/appshell/component/messenger;1",
+ "@mozilla.org/messenger/windowservice;1",
+ ],
+ "type": "nsMessengerBootstrap",
+ "headers": ["/comm/mailnews/base/src/nsMessengerBootstrap.h"],
+ },
+ {
+ "cid": "{d5124441-d59e-11d2-806a-006080128c4e}",
+ "contract_ids": ["@mozilla.org/messenger/services/session;1"],
+ "type": "nsMsgMailSession",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgMailSession.h"],
+ "name": "MailSession",
+ "interfaces": ["nsIMsgMailSession"],
+ },
+ {
+ "cid": "{f436a174-e2c0-4955-9afe-e3feb68aee56}",
+ "contract_ids": ["@mozilla.org/messenger;1"],
+ "type": "nsMessenger",
+ "headers": ["/comm/mailnews/base/src/nsMessenger.h"],
+ },
+ {
+ "cid": "{d2876e50-e62c-11d2-b7fc-00805f05ffa5}",
+ "contract_ids": ["@mozilla.org/messenger/account-manager;1"],
+ "type": "nsMsgAccountManager",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgAccountManager.h"],
+ "name": "AccountManager",
+ "interfaces": ["nsIMsgAccountManager"],
+ },
+ {
+ "cid": "{68b25510-e641-11d2-b7fc-00805f05ffa5}",
+ "contract_ids": ["@mozilla.org/messenger/account;1"],
+ "type": "nsMsgAccount",
+ "headers": ["/comm/mailnews/base/src/nsMsgAccount.h"],
+ },
+ {
+ "cid": "{8fbf6ac0-ebcc-11d2-b7fc-00805f05ffa5}",
+ "contract_ids": ["@mozilla.org/messenger/identity;1"],
+ "type": "nsMsgIdentity",
+ "headers": ["/comm/mailnews/base/src/nsMsgIdentity.h"],
+ },
+ {
+ "cid": "{4a374e7e-190f-11d3-8a88-0060b0fc04d2}",
+ "contract_ids": ["@mozilla.org/messenger/biffManager;1"],
+ "type": "nsMsgBiffManager",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgBiffManager.h"],
+ },
+ {
+ "cid": "{a687b474-afd8-418f-8ad9-f362202ae9a9}",
+ "contract_ids": ["@mozilla.org/messenger/purgeService;1"],
+ "type": "nsMsgPurgeService",
+ "headers": ["/comm/mailnews/base/src/nsMsgPurgeService.h"],
+ },
+ {
+ "cid": "{7f9a9fb0-4161-11d4-9876-00c04fa0d2a6}",
+ "contract_ids": ["@mozilla.org/messenger/statusBarBiffManager;1"],
+ "type": "nsStatusBarBiffManager",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsStatusBarBiffManager.h"],
+ },
+ {
+ "cid": "{7741daed-2125-11d3-8a90-0060b0fc04d2}",
+ "contract_ids": ["@mozilla.org/messenger/copymessagestreamlistener;1"],
+ "type": "nsCopyMessageStreamListener",
+ "headers": ["/comm/mailnews/base/src/nsCopyMessageStreamListener.h"],
+ },
+ {
+ "cid": "{c766e666-29bd-11d3-afb3-001083002da8}",
+ "contract_ids": ["@mozilla.org/messenger/messagecopyservice;1"],
+ "type": "nsMsgCopyService",
+ "headers": ["/comm/mailnews/base/src/nsMsgCopyService.h"],
+ "name": "Copy",
+ "interfaces": ["nsIMsgCopyService"],
+ },
+ {
+ "cid": "{bcdca970-3b22-11d3-8d76-0080f58a6617}",
+ "contract_ids": ["@mozilla.org/messenger/msgFolderCache;1"],
+ "type": "nsMsgFolderCache",
+ "headers": ["/comm/mailnews/base/src/nsMsgFolderCache.h"],
+ },
+ {
+ "cid": "{bd85a417-5433-11d3-8ac5-0060b0fc04d2}",
+ "contract_ids": ["@mozilla.org/messenger/statusfeedback;1"],
+ "type": "nsMsgStatusFeedback",
+ "headers": ["/comm/mailnews/base/src/nsMsgStatusFeedback.h"],
+ },
+ {
+ "cid": "{bb460dff-8bf0-11d3-8afe-0060b0fc04d2}",
+ "contract_ids": ["@mozilla.org/messenger/msgwindow;1"],
+ "type": "nsMsgWindow",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgWindow.h"],
+ },
+ {
+ "cid": "{8510876a-1dd2-11b2-8253-91f71b348a25}",
+ "contract_ids": ["@mozilla.org/messenger/subscribableserver;1"],
+ "type": "nsSubscribableServer",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsSubscribableServer.h"],
+ },
+ {
+ "cid": "{56c4c2ac-fe4a-4528-aa78-f8fb579b029c}",
+ "contract_ids": ["@mozilla.org/messenger/foldercompactor;1"],
+ "type": "nsMsgFolderCompactor",
+ "headers": ["/comm/mailnews/base/src/nsMsgFolderCompactor.h"],
+ },
+ {
+ "cid": "{52f860e0-1dd2-11b2-aa72-bb751981bd00}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=threaded"],
+ "type": "nsMsgThreadedDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgThreadedDBView.h"],
+ },
+ {
+ "cid": "{ca79a00e-010d-11d5-a5be-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=threadswithunread"],
+ "type": "nsMsgThreadsWithUnreadDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgSpecialViews.h"],
+ },
+ {
+ "cid": "{597e1ffe-0123-11d5-a5be-0060b0fc04b7}",
+ "contract_ids": [
+ "@mozilla.org/messenger/msgdbview;1?type=watchedthreadswithunread"
+ ],
+ "type": "nsMsgWatchedThreadsWithUnreadDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgSpecialViews.h"],
+ },
+ {
+ "cid": "{aeac118c-0823-11d5-a5bf-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=search"],
+ "type": "nsMsgSearchDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgSearchDBView.h"],
+ },
+ {
+ "cid": "{2dd9d0fe-b609-11d6-bacc-00108335748d}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=quicksearch"],
+ "type": "nsMsgQuickSearchDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgQuickSearchDBView.h"],
+ },
+ {
+ "cid": "{2af6e050-04f6-495a-8387-86b0aeb1863c}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=xfvf"],
+ "type": "nsMsgXFVirtualFolderDBView",
+ "headers": ["/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h"],
+ },
+ {
+ "cid": "{e4603d6c-0a74-47c5-b69e-2f8876990304}",
+ "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=group"],
+ "type": "nsMsgGroupView",
+ "headers": ["/comm/mailnews/base/src/nsMsgGroupView.h"],
+ },
+ {
+ "cid": "{bcf6afbe-7d4f-11ec-9092-eb4fed0a5aaa}",
+ "contract_ids": ["@mozilla.org/msgDBView/msgDBViewService;1"],
+ "type": "nsMsgDBViewService",
+ "headers": ["/comm/mailnews/base/src/nsMsgDBView.h"],
+ "name": "DBView",
+ "interfaces": ["nsIMsgDBViewService"],
+ },
+ {
+ "cid": "{ac6c518a-09b2-11d5-a5bf-0060b0fc04b7}",
+ "contract_ids": ["@mozilla.org/messenger/offline-manager;1"],
+ "type": "nsMsgOfflineManager",
+ "headers": ["/comm/mailnews/base/src/nsMsgOfflineManager.h"],
+ },
+ {
+ "cid": "{9f4dd201-3b1f-11d5-9daa-c345c9453d3c}",
+ "contract_ids": ["@mozilla.org/messenger/progress;1"],
+ "type": "nsMsgProgress",
+ "headers": ["/comm/mailnews/base/src/nsMsgProgress.h"],
+ },
+ {
+ "cid": "{ce6038ae-e5e0-4372-9cff-2a6633333b2b}",
+ "contract_ids": ["@mozilla.org/messenger/spamsettings;1"],
+ "type": "nsSpamSettings",
+ "headers": ["/comm/mailnews/base/src/nsSpamSettings.h"],
+ },
+ {
+ "cid": "{b3db9392-1b15-48ba-a136-0cc3db13d87b}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=cid"],
+ "type": "nsCidProtocolHandler",
+ "headers": ["/comm/mailnews/base/src/nsCidProtocolHandler.h"],
+ "protocol_config": {
+ "scheme": "cid",
+ "flags": [
+ "URI_DANGEROUS_TO_LOAD"
+ ],
+ },
+ },
+ {
+ "cid": "{b897da55-8256-4cf5-892b-32e77bc7c50b}",
+ "contract_ids": ["@mozilla.org/messenger/tagservice;1"],
+ "type": "nsMsgTagService",
+ "headers": ["/comm/mailnews/base/src/nsMsgTagService.h"],
+ "name": "Tag",
+ "interfaces": ["nsIMsgTagService"],
+ },
+ {
+ "cid": "{0c8ec907-49c7-49bc-8bdf-b16e29bd6c47}",
+ "contract_ids": ["@mozilla.org/msgFolder/msgFolderService;1"],
+ "type": "nsMsgFolderService",
+ "headers": ["/comm/mailnews/base/src/nsMsgDBFolder.h"],
+ "name": "Folder",
+ "interfaces": ["nsIMsgFolderService"],
+ },
+ {
+ "cid": "{f1f7cbcd-d5e3-45a0-aa2d-cecf1a95ab03}",
+ "contract_ids": ["@mozilla.org/messenger/msgnotificationservice;1"],
+ "type": "nsMsgFolderNotificationService",
+ "headers": ["/comm/mailnews/base/src/nsMsgFolderNotificationService.h"],
+ "name": "FolderNotification",
+ "interfaces": ["nsIMsgFolderNotificationService"],
+ },
+ {
+ "cid": "{dbfcfdf0-4489-4faa-8122-190fd1efa16c}",
+ "contract_ids": ["@mozilla.org/messenger/content-policy;1"],
+ "type": "nsMsgContentPolicy",
+ "init_method": "Init",
+ "headers": ["/comm/mailnews/base/src/nsMsgContentPolicy.h"],
+ "categories": {"content-policy": "@mozilla.org/messenger/content-policy;1"},
+ },
+ {
+ "cid": "{483c8abb-ecf9-48a3-a394-2c604b603bd5}",
+ "contract_ids": ["@mozilla.org/messenger/msgshutdownservice;1"],
+ "type": "nsMsgShutdownService",
+ "headers": ["/comm/mailnews/base/src/nsMsgMailSession.h"],
+ },
+ {
+ "cid": "{03f9bb53-a680-4349-8de9-d26864d9ffd9}",
+ "contract_ids": ["@mozilla.org/mail/dir-provider;1"],
+ "type": "nsMailDirProvider",
+ "headers": ["/comm/mailnews/base/src/nsMailDirProvider.h"],
+ "categories": {"xpcom-directory-providers": "mail-directory-provider"},
+ },
+ {
+ "cid": "{6ef7eafd-72d0-4c56-9409-67e16d0f255b}",
+ "contract_ids": ["@mozilla.org/stopwatch;1"],
+ "type": "nsStopwatch",
+ "headers": ["/comm/mailnews/base/src/nsStopwatch.h"],
+ },
+ {
+ "cid": "{de0f34a9-a87f-4f4c-b978-6187db187b90}",
+ "contract_ids": ["@mozilla.org/mailnews/document-loader-factory;1"],
+ "type": "mailnews::MailNewsDLF",
+ "headers": ["/comm/mailnews/base/src/MailNewsDLF.h"],
+ "categories": {"Gecko-Content-Viewers": "message/rfc822"},
+ },
+ {
+ "cid": "{bf88b48c-fd8e-40b4-ba36-c7c3ad6d8ac9}",
+ "contract_ids": ["@mozilla.org/embedcomp/base-command-controller;1"],
+ "type": "nsBaseCommandController",
+ "headers": ["/dom/commandhandler/nsBaseCommandController.h"],
+ },
+ {
+ "cid": "{9c8f9601-801a-11d2-98ba-00805f297d89}",
+ "contract_ids": ["@mozilla.org/transactionmanager;1"],
+ "type": "TransactionManager",
+ "headers": ["/editor/txmgr/TransactionManager.h"],
+ },
+ {
+ "cid": "{14c13684-1dd2-11b2-9463-bb10ba742554}",
+ "contract_ids": ["@mozilla.org/userinfo;1"],
+ "type": "nsUserInfo",
+ "headers": ["/comm/mailnews/base/src/nsUserInfo.h"],
+ },
+ # The XPCOM registration for nsSyncStreamListener was moved to comm-central
+ # in bug 1501718. In bug 1800606 all uses were removed from comm-central.
+ # The registration is maintained for some MailExtension Experiments.
+ # Use of this service is discouraged due to its synchronous processing and
+ # probable deprecation in the future. Instead use asynchronous code like in
+ # `get_msg_source()` in ComposeHelpers.jsm.
+ {
+ "cid": "{439400d3-6f23-43db-8b06-8aafe1869bd8}",
+ "contract_ids": ["@mozilla.org/network/sync-stream-listener;1"],
+ "constructor": "SyncStreamListenerCreate",
+ "headers": ["/comm/mailnews/base/src/nsMsgUtils.h"],
+ },
+]
+
+if buildconfig.substs["OS_ARCH"] == "Darwin":
+ Classes += [
+ {
+ "cid": "{746b28a5-d239-4719-b1a2-cf8093332ae3}",
+ "contract_ids": ["@mozilla.org/messenger/osintegration;1"],
+ "type": "nsMessengerOSXIntegration",
+ "headers": ["/comm/mailnews/base/src/nsMessengerOSXIntegration.h"],
+ },
+ ]
+
+ Categories = {
+ "app-startup": {
+ "OS Integration": "@mozilla.org/messenger/osintegration;1",
+ }
+ }
+
+if buildconfig.substs["OS_ARCH"] == "WINNT":
+ Classes += [
+ {
+ "cid": "{a74dd1d6-2ec4-4985-98f3-f69e18d20811}",
+ "contract_ids": ["@mozilla.org/messenger/osintegration;1"],
+ "type": "nsMessengerWinIntegration",
+ "headers": ["/comm/mailnews/base/src/nsMessengerWinIntegration.h"],
+ },
+ ]
diff --git a/comm/mailnews/base/src/converterWorker.js b/comm/mailnews/base/src/converterWorker.js
new file mode 100644
index 0000000000..188476c1e1
--- /dev/null
+++ b/comm/mailnews/base/src/converterWorker.js
@@ -0,0 +1,533 @@
+/* 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/. */
+
+/* eslint-env mozilla/chrome-worker, node */
+
+/**
+ * This worker will perform mbox<->maildir conversions on a tree of
+ * directories. It operates purely at the filesystem level.
+ *
+ * The initial message data should pass in these params to control
+ * the conversion:
+ *
+ * srcType - source mailstore type ('mbox' or 'maildir')
+ * destType - destination mailstore type ('maildir' or 'mbox')
+ * srcRoot - root path of source (eg ".../ImapMail/imap.example.com")
+ * destRoot - root path of destination (eg "/tmp/imap.example.com-maildir")
+ *
+ * The conversion is non-destructive - srcRoot will be left untouched.
+ *
+ * The worker will post progress messages back to the main thread of
+ * the form:
+ *
+ * {"msg": "progress", "val": val, "total": total}
+ *
+ * Where `val` is the current progress, out of `total`.
+ * The units used for val and total are undefined.
+ *
+ * When the conversion is complete, before exiting, the worker sends a
+ * message of the form:
+ *
+ * {"msg": "success"}
+ *
+ * Errors are posted back to the main thread via the standard
+ * "error" event.
+ *
+ */
+
+/**
+ * Merge all the messages in a maildir into a single mbox file.
+ *
+ * @param {string} maildir - Path to the source maildir.
+ * @param {string} mboxFilename - Path of the mbox file to create.
+ * @param {Function(number)} progressFn - Function to be invoked regularly with
+ * progress updates. Param is number of
+ * "units" processed since last update.
+ */
+async function maildirToMBox(maildir, mboxFilename, progressFn) {
+ // Helper to format dates
+ // eg "Thu Jan 18 12:34:56 2018"
+ let fmtUTC = function (d) {
+ const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
+ const monthNames = [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ];
+ return (
+ dayNames[d.getUTCDay()] +
+ " " +
+ monthNames[d.getUTCMonth()] +
+ " " +
+ d.getUTCDate().toString().padStart(2) +
+ " " +
+ d.getUTCHours().toString().padStart(2, "0") +
+ ":" +
+ d.getUTCMinutes().toString().padStart(2, "0") +
+ ":" +
+ d.getUTCSeconds().toString().padStart(2, "0") +
+ " " +
+ d.getUTCFullYear()
+ );
+ };
+
+ // Initialize mbox file
+ await IOUtils.write(mboxFilename, new Uint8Array(), {
+ mode: "create",
+ });
+
+ // Iterate over all the message files in "cur".
+ let curPath = PathUtils.join(maildir, "cur");
+ let paths = await IOUtils.getChildren(curPath);
+ let files = await Promise.all(
+ paths.map(async path => {
+ let stat = await IOUtils.stat(path);
+ return {
+ path,
+ creationDate: stat.creationTime,
+ };
+ })
+ );
+ // We write out the mbox messages ordered by creation time.
+ // Not ideal, but best we can do without parsing message.
+ files.sort(function (a, b) {
+ return a.creationDate - b.creationDate;
+ });
+
+ for (let ent of files) {
+ let raw = await IOUtils.read(ent.path);
+ // Old converter had a bug where maildir messages included the
+ // leading "From " marker, so we need to cope with any
+ // cases of this left in the wild.
+ if (String.fromCharCode.apply(null, raw.slice(0, 5)) != "From ") {
+ // Write the separator line.
+ // Technically, timestamp should be the reception time of the
+ // message, but we don't really want to have to parse the
+ // message here and nothing is likely to rely on it.
+ let sepLine = "From - " + fmtUTC(new Date()) + "\n";
+ await IOUtils.writeUTF8(mboxFilename, sepLine, {
+ mode: "append",
+ });
+ }
+
+ await IOUtils.write(mboxFilename, raw, {
+ mode: "append",
+ });
+ // Maildir progress is one per message.
+ progressFn(1);
+ }
+}
+
+/**
+ * Split an mbox file up into a maildir.
+ *
+ * @param {string} mboxPath - Path of the mbox file to split.
+ * @param {string} maildirPath - Path of the maildir to create.
+ * @param {Function(number)} progressFn - Function to be invoked regularly with
+ * progress updates. One parameter is
+ * passed - the number of "cost units"
+ * since the previous update.
+ */
+async function mboxToMaildir(mboxPath, maildirPath, progressFn) {
+ // Create the maildir structure.
+ await IOUtils.makeDirectory(maildirPath);
+ let curDirPath = PathUtils.join(maildirPath, "cur");
+ let tmpDirPath = PathUtils.join(maildirPath, "tmp");
+ await IOUtils.makeDirectory(curDirPath);
+ await IOUtils.makeDirectory(tmpDirPath);
+
+ const CHUNK_SIZE = 1000000;
+ // SAFE_MARGIN is how much to keep back between chunks in order to
+ // cope with separator lines which might span chunks.
+ const SAFE_MARGIN = 100;
+
+ // A regexp to match mbox separator lines. Separator lines in the wild can
+ // have all sorts of forms, for example:
+ //
+ // "From "
+ // "From MAILER-DAEMON Fri Jul 8 12:08:34 2011"
+ // "From - Mon Jul 11 12:08:34 2011"
+ // "From bob@example.com Fri Jul 8 12:08:34 2011"
+ //
+ // So we accept any line beginning with "From " and ignore the rest of it.
+ //
+ // We also require a message header on the next line, in order
+ // to better cope with unescaped "From " lines in the message body.
+ // note: the first subexpression matches the separator line, so
+ // that it can be removed from the input.
+ let sepRE = /^(From (?:.*?)\r?\n)[\x21-\x7E]+:/gm;
+
+ // Use timestamp as starting name for output messages, incrementing
+ // by one for each.
+ let ident = Date.now();
+
+ /**
+ * Helper. Convert a string into a Uint8Array, using no encoding. The low
+ * byte of each 16 bit character will be used, the high byte discarded.
+ *
+ * @param {string} str - Input string with chars in 0-255 range.
+ * @returns {Uint8Array} The output bytes.
+ */
+ let stringToBytes = function (str) {
+ var bytes = new Uint8Array(str.length);
+ for (let i = 0; i < str.length; i++) {
+ bytes[i] = str.charCodeAt(i);
+ }
+ return bytes;
+ };
+
+ /**
+ * Helper. Convert a Uint8Array directly into a string, using each byte
+ * directly as a character code. So all characters in the resulting string
+ * will range from 0 to 255, even though they are 16 bit values.
+ *
+ * @param {Uint8Array} bytes - The bytes to convert.
+ * @returns {string} The byte values in string form.
+ */
+ let bytesToString = function (bytes) {
+ return bytes.reduce(function (str, b) {
+ return str + String.fromCharCode(b);
+ }, "");
+ };
+
+ let outPath;
+
+ /**
+ * Helper. Write out a block of bytes to the current message file, starting
+ * a new file if required.
+ *
+ * @param {string} str - The bytes to append (as chars in range 0-255).
+ */
+ let writeToMsg = async function (str) {
+ let mode = "append";
+ if (!outPath) {
+ outPath = PathUtils.join(curDirPath, ident.toString() + ".eml");
+ ident += 1;
+ mode = "create";
+ }
+ // We know that str is really raw 8-bit data, not UTF-16. So we can
+ // discard the upper byte and just keep the low byte of each char.
+ let raw = stringToBytes(str);
+ await IOUtils.write(outPath, raw, { mode });
+ // For mbox->maildir conversion, progress is measured in bytes.
+ progressFn(raw.byteLength);
+ };
+
+ let buf = "";
+ let eof = false;
+ let offset = 0;
+ while (!eof) {
+ let rawBytes = await IOUtils.read(mboxPath, {
+ offset,
+ maxBytes: CHUNK_SIZE,
+ });
+ // We're using JavaScript strings (which hold 16bit characters) to store
+ // 8 bit data. This sucks, but is faster than trying to operate directly
+ // upon Uint8Arrays. A lot of work goes into optimising JavaScript strings.
+ buf += bytesToString(rawBytes);
+ offset += rawBytes.byteLength;
+ eof = rawBytes.byteLength < CHUNK_SIZE;
+
+ let pos = 0;
+ sepRE.lastIndex = 0; // start at beginning of buf
+ let m = null;
+ while ((m = sepRE.exec(buf)) !== null) {
+ // Output everything up to the line separator.
+ if (m.index > pos) {
+ await writeToMsg(buf.substring(pos, m.index));
+ }
+ pos = m.index;
+ pos += m[1].length; // skip the "From " line
+ // Reset the current message file path if any.
+ if (outPath) {
+ outPath = null;
+ }
+ }
+
+ // Deal with whatever is left in the buffer.
+ let endPos = buf.length;
+ if (!eof) {
+ // Keep back enough to cope with separator lines crossing
+ // chunk boundaries.
+ endPos -= SAFE_MARGIN;
+ if (endPos < pos) {
+ endPos = pos;
+ }
+ }
+
+ if (endPos > pos) {
+ await writeToMsg(buf.substring(pos, endPos));
+ }
+ buf = buf.substring(endPos);
+ }
+}
+
+/**
+ * Check if directory is a subfolder directory.
+ *
+ * @param {string} name - Name of directory to check.
+ * @returns {boolean} - true if subfolder.
+ */
+function isSBD(name) {
+ return name.substr(-4) == ".sbd";
+}
+
+/**
+ * Check if file is a type which should be copied verbatim as part of a
+ * conversion.
+ * See also: nsMsgLocalStoreUtils::nsShouldIgnoreFile().
+ *
+ * @param {string} name - Name of file to check.
+ * @returns {boolean} - true if file should be copied verbatim.
+ */
+function isFileToCopy(name) {
+ let ext4 = name.substr(-4);
+ // Database and config files.
+ if (ext4 == ".msf" || ext4 == ".dat") {
+ return true;
+ }
+ // Summary files.
+ if (ext4 == ".snm" || ext4 == ".toc") {
+ return true;
+ }
+ // A few files we know might be lurking there.
+ const SPECIAL_FILES = [
+ "filterlog.html",
+ "junklog.html",
+ "feeds.json",
+ "feeds.json.tmp",
+ "feeds.json.backup",
+ "feeds.json.corrupt",
+ "feeditems.json",
+ "feeditems.json.tmp",
+ "feeditems.json.backup",
+ "feeditems.json.corrupt",
+ "mailfilt.log",
+ "filters.js",
+ ];
+ if (SPECIAL_FILES.includes(name)) {
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Check if file is an mbox.
+ * (actually we can't really tell if it's an mbox or not just from the name.
+ * we just assume it is, if it's not .msf or .dat).
+ *
+ * @param {string} name - Name of file to check.
+ * @returns {boolean} - true if file is an mbox
+ */
+function isMBoxName(name) {
+ // If it's not a "special" file, assume it's mbox.
+ return !isFileToCopy(name);
+}
+
+/**
+ * Check if directory is a maildir (by looking for a "cur" subdir).
+ *
+ * @param {string} dir - Path of directory to check.
+ * @returns {Promise<boolean>} - true if directory is a maildir.
+ */
+async function isMaildir(dir) {
+ try {
+ let cur = PathUtils.join(dir, "cur");
+ let fi = await IOUtils.stat(cur);
+ return fi.type === "directory";
+ } catch (ex) {
+ if (ex instanceof DOMException && ex.name === "NotFoundError") {
+ // "cur" does not exist - not a maildir.
+ return false;
+ }
+ throw ex; // Other error.
+ }
+}
+
+/**
+ * Count the number of messages in the "cur" dir of maildir.
+ *
+ * @param {string} maildir - Path of maildir.
+ * @returns {Promise<number>} - number of messages found.
+ */
+async function countMaildirMsgs(maildir) {
+ let cur = PathUtils.join(maildir, "cur");
+ let paths = await IOUtils.getChildren(cur);
+ return paths.length;
+}
+
+/**
+ * Recursively calculate the 'cost' of a hierarchy of maildir folders.
+ * This is the figure used for progress updates.
+ * For maildir, cost is 1 per message.
+ *
+ * @param {string} srcPath - Path of root dir containing maildirs.
+ * @returns {Promise<number>} - calculated conversion cost.
+ */
+async function calcMaildirCost(srcPath) {
+ let cost = 0;
+ for (let path of await IOUtils.getChildren(srcPath)) {
+ let stat = await IOUtils.stat(path);
+ if (stat.type === "directory") {
+ let name = PathUtils.filename(path);
+ if (isSBD(name)) {
+ // Recurse into subfolder.
+ cost += await calcMaildirCost(path);
+ } else if (await isMaildir(path)) {
+ // Looks like a maildir. Cost is number of messages.
+ cost += await countMaildirMsgs(path);
+ }
+ }
+ }
+ return cost;
+}
+
+/**
+ * Recursively calculate the 'cost' of a hierarchy of mbox folders.
+ * This is the figure used for progress updates.
+ * For mbox, cost is the total byte size of data. This avoids the need to
+ * parse the mbox files to count the number of messages.
+ * Note that this byte count cost is not 100% accurate because it includes
+ * the "From " lines which are not written into the maildir files. But it's
+ * definitely close enough to give good user feedback.
+ *
+ * @param {string} srcPath - Path of root dir containing maildirs.
+ * @returns {Promise<number>} - calculated conversion cost.
+ */
+async function calcMBoxCost(srcPath) {
+ let cost = 0;
+ for (const path of await IOUtils.getChildren(srcPath)) {
+ let stat = await IOUtils.stat(path);
+ let name = PathUtils.filename(path);
+ if (stat.type === "directory") {
+ if (isSBD(name)) {
+ // Recurse into .sbd subfolder.
+ cost += await calcMBoxCost(path);
+ }
+ } else if (isMBoxName(name)) {
+ cost += stat.size;
+ }
+ }
+ return cost;
+}
+
+/**
+ * Recursively convert a tree of mbox-based folders to maildirs.
+ *
+ * @param {string} srcPath - Root path containing mboxes.
+ * @param {string} destPath - Where to create destination root.
+ * @param {Function(number)} progressFn - Function to be invoked regularly with
+ * progress updates (called with number of
+ * cost "units" since last update)
+ */
+async function convertTreeMBoxToMaildir(srcPath, destPath, progressFn) {
+ await IOUtils.makeDirectory(destPath);
+
+ for (const path of await IOUtils.getChildren(srcPath)) {
+ let name = PathUtils.filename(path);
+ let dest = PathUtils.join(destPath, name);
+ let stat = await IOUtils.stat(path);
+ if (stat.type === "directory") {
+ if (isSBD(name)) {
+ // Recurse into .sbd subfolder.
+ await convertTreeMBoxToMaildir(path, dest, progressFn);
+ }
+ } else if (isFileToCopy(name)) {
+ await IOUtils.copy(path, dest);
+ } else if (isMBoxName(name)) {
+ // It's an mbox. Convert it.
+ await mboxToMaildir(path, dest, progressFn);
+ }
+ }
+}
+
+/**
+ * Recursively convert a tree of maildir-based folders to mbox.
+ *
+ * @param {string} srcPath - Root path containing maildirs.
+ * @param {string} destPath - Where to create destination root.
+ * @param {Function(number)} progressFn - Function to be invoked regularly with
+ * progress updates (called with number of
+ * cost "units" since last update)
+ */
+async function convertTreeMaildirToMBox(srcPath, destPath, progressFn) {
+ await IOUtils.makeDirectory(destPath);
+
+ for (let path of await IOUtils.getChildren(srcPath)) {
+ let name = PathUtils.filename(path);
+ let dest = PathUtils.join(destPath, name);
+ let stat = await IOUtils.stat(path);
+ if (stat.type === "directory") {
+ if (isSBD(name)) {
+ // Recurse into .sbd subfolder.
+ await convertTreeMaildirToMBox(path, dest, progressFn);
+ } else if (await isMaildir(path)) {
+ // It's a maildir - convert it.
+ await maildirToMBox(path, dest, progressFn);
+ }
+ } else if (isFileToCopy(name)) {
+ await IOUtils.copy(path, dest);
+ }
+ }
+}
+
+// propagate unhandled rejections to the error handler on the main thread
+self.addEventListener("unhandledrejection", function (error) {
+ throw error.reason;
+});
+
+self.addEventListener("message", function (e) {
+ // Unpack the request params from the main thread.
+ let srcType = e.data.srcType;
+ let destType = e.data.destType;
+ let srcRoot = e.data.srcRoot;
+ let destRoot = e.data.destRoot;
+ // destRoot will be a temporary dir, so if it all goes pear-shaped
+ // we can just bail out without cleaning up.
+
+ // Configure the conversion.
+ let costFn = null;
+ let convertFn = null;
+ if (srcType == "maildir" && destType == "mbox") {
+ costFn = calcMaildirCost;
+ convertFn = convertTreeMaildirToMBox;
+ } else if (srcType == "mbox" && destType == "maildir") {
+ costFn = calcMBoxCost;
+ convertFn = convertTreeMBoxToMaildir;
+ } else {
+ throw new Error(`Unsupported conversion: ${srcType} => ${destType}`);
+ }
+
+ // Go!
+ costFn(srcRoot).then(totalCost => {
+ let v = 0;
+ let progressFn = function (n) {
+ v += n;
+ self.postMessage({ msg: "progress", val: v, total: totalCost });
+ };
+ convertFn(srcRoot, destRoot, progressFn).then(() => {
+ // We fake a final progress update, with exactly 100% completed.
+ // Our byte-counting on mbox->maildir conversion will fall slightly short:
+ // The total is estimated from the mbox filesize, but progress is tracked
+ // by counting bytes as they are written out - and the mbox "From " lines
+ // are _not_ written out to the maildir files.
+ // This is still accurate enough to provide progress to the user, but we
+ // don't want the GUI left showing "progress 97% - conversion complete!"
+ // or anything silly like that.
+ self.postMessage({ msg: "progress", val: totalCost, total: totalCost });
+
+ // Let the main thread know we succeeded.
+ self.postMessage({ msg: "success" });
+ });
+ });
+});
diff --git a/comm/mailnews/base/src/hostnameUtils.jsm b/comm/mailnews/base/src/hostnameUtils.jsm
new file mode 100644
index 0000000000..e80c210b2e
--- /dev/null
+++ b/comm/mailnews/base/src/hostnameUtils.jsm
@@ -0,0 +1,366 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Generic shared utility code for checking of IP and hostname validity.
+ */
+
+const EXPORTED_SYMBOLS = [
+ "isLegalHostNameOrIP",
+ "isLegalHostName",
+ "isLegalIPv4Address",
+ "isLegalIPv6Address",
+ "isLegalIPAddress",
+ "isLegalLocalIPAddress",
+ "cleanUpHostName",
+ "kMinPort",
+ "kMaxPort",
+];
+
+var kMinPort = 1;
+var kMaxPort = 65535;
+
+/**
+ * Check if aHostName is an IP address or a valid hostname.
+ *
+ * @param {string} aHostName - The string to check for validity.
+ * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in addition to decimal.
+ * @returns {?string} Unobscured host name if aHostName is valid.
+ * Returns null if it's not.
+ */
+function isLegalHostNameOrIP(aHostName, aAllowExtendedIPFormats) {
+ /*
+ RFC 1123:
+ Whenever a user inputs the identity of an Internet host, it SHOULD
+ be possible to enter either (1) a host domain name or (2) an IP
+ address in dotted-decimal ("#.#.#.#") form. The host SHOULD check
+ the string syntactically for a dotted-decimal number before
+ looking it up in the Domain Name System.
+ */
+
+ return (
+ isLegalIPAddress(aHostName, aAllowExtendedIPFormats) ||
+ isLegalHostName(aHostName)
+ );
+}
+
+/**
+ * Check if aHostName is a valid hostname.
+ *
+ * @returns {?string} The host name if it is valid. Returns null if it's not.
+ */
+function isLegalHostName(aHostName) {
+ /*
+ RFC 952:
+ A "name" (Net, Host, Gateway, or Domain name) is a text string up
+ to 24 characters drawn from the alphabet (A-Z), digits (0-9), minus
+ sign (-), and period (.). Note that periods are only allowed when
+ they serve to delimit components of "domain style names". (See
+ RFC-921, "Domain Name System Implementation Schedule", for
+ background). No blank or space characters are permitted as part of a
+ name. No distinction is made between upper and lower case. The first
+ character must be an alpha character. The last character must not be
+ a minus sign or period.
+
+ RFC 1123:
+ The syntax of a legal Internet host name was specified in RFC-952
+ [DNS:4]. One aspect of host name syntax is hereby changed: the
+ restriction on the first character is relaxed to allow either a
+ letter or a digit. Host software MUST support this more liberal
+ syntax.
+
+ Host software MUST handle host names of up to 63 characters and
+ SHOULD handle host names of up to 255 characters.
+
+ RFC 1034:
+ Relative names are either taken relative to a well known origin, or to a
+ list of domains used as a search list. Relative names appear mostly at
+ the user interface, where their interpretation varies from
+ implementation to implementation, and in master files, where they are
+ relative to a single origin domain name. The most common interpretation
+ uses the root "." as either the single origin or as one of the members
+ of the search list, so a multi-label relative name is often one where
+ the trailing dot has been omitted to save typing.
+
+ Since a complete domain name ends with the root label, this leads to
+ a printed form which ends in a dot.
+ */
+
+ const hostPattern =
+ /^(([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.?$/i;
+ return aHostName.length <= 255 && hostPattern.test(aHostName)
+ ? aHostName
+ : null;
+}
+
+/**
+ * Check if aHostName is a valid IPv4 address.
+ *
+ * @param {string} aHostName - The string to check for validity.
+ * @param {boolean} aAllowExtendedIPFormats - If false, only IPv4 addresses
+ * in the common decimal format (4 components, each up to 255)
+ * will be accepted, no hex/octal formats.
+ * @returns {string} Unobscured canonicalized address if aHostName is an
+ * IPv4 address. Returns null if it's not.
+ */
+function isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) {
+ // Scammers frequently obscure the IP address by encoding each component as
+ // decimal, octal, hex or in some cases a mix match of each. There can even
+ // be less than 4 components where the last number covers the missing components.
+ // See the test at mailnews/base/test/unit/test_hostnameUtils.js for possible
+ // combinations.
+
+ if (!aHostName) {
+ return null;
+ }
+
+ // Break the IP address down into individual components.
+ let ipComponents = aHostName.split(".");
+ let componentCount = ipComponents.length;
+ if (componentCount > 4 || (componentCount < 4 && !aAllowExtendedIPFormats)) {
+ return null;
+ }
+
+ /**
+ * Checks validity of an IP address component.
+ *
+ * @param {string} aValue - The component string.
+ * @param {integer} aWidth - How many components does this string cover.
+ * @returns {integer|null} The value of the component in decimal if it is valid.
+ * Returns null if it's not.
+ */
+ const kPowersOf256 = [1, 256, 65536, 16777216, 4294967296];
+ function isLegalIPv4Component(aValue, aWidth) {
+ let component;
+ // Is the component decimal?
+ if (/^(0|([1-9][0-9]{0,9}))$/.test(aValue)) {
+ component = parseInt(aValue, 10);
+ } else if (aAllowExtendedIPFormats) {
+ // Is the component octal?
+ if (/^(0[0-7]{1,12})$/.test(aValue)) {
+ component = parseInt(aValue, 8);
+ } else if (/^(0x[0-9a-f]{1,8})$/i.test(aValue)) {
+ // The component is hex.
+ component = parseInt(aValue, 16);
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+
+ // Make sure the component in not larger than the expected maximum.
+ if (component >= kPowersOf256[aWidth]) {
+ return null;
+ }
+
+ return component;
+ }
+
+ for (let i = 0; i < componentCount; i++) {
+ // If we are on the last supplied component but we do not have 4,
+ // the last one covers the remaining ones.
+ let componentWidth = i == componentCount - 1 ? 4 - i : 1;
+ let componentValue = isLegalIPv4Component(ipComponents[i], componentWidth);
+ if (componentValue == null) {
+ return null;
+ }
+
+ // If we have a component spanning multiple ones, split it.
+ for (let j = 0; j < componentWidth; j++) {
+ ipComponents[i + j] =
+ (componentValue >> ((componentWidth - 1 - j) * 8)) & 255;
+ }
+ }
+
+ // First component of zero is not valid.
+ if (ipComponents[0] == 0) {
+ return null;
+ }
+
+ return ipComponents.join(".");
+}
+
+/**
+ * Check if aHostName is a valid IPv6 address.
+ *
+ * @param {string} aHostName - The string to check for validity.
+ * @returns {string} Unobscured canonicalized address if aHostName is an
+ * IPv6 address. Returns null if it's not.
+ */
+function isLegalIPv6Address(aHostName) {
+ if (!aHostName) {
+ return null;
+ }
+
+ // Break the IP address down into individual components.
+ let ipComponents = aHostName.toLowerCase().split(":");
+
+ // Make sure there are at least 3 components.
+ if (ipComponents.length < 3) {
+ return null;
+ }
+
+ let ipLength = ipComponents.length - 1;
+
+ // Take care if the last part is written in decimal using dots as separators.
+ let lastPart = isLegalIPv4Address(ipComponents[ipLength], false);
+ if (lastPart) {
+ let lastPartComponents = lastPart.split(".");
+ // Convert it into standard IPv6 components.
+ ipComponents[ipLength] = (
+ (lastPartComponents[0] << 8) |
+ lastPartComponents[1]
+ ).toString(16);
+ ipComponents[ipLength + 1] = (
+ (lastPartComponents[2] << 8) |
+ lastPartComponents[3]
+ ).toString(16);
+ }
+
+ // Make sure that there is only one empty component.
+ let emptyIndex;
+ for (let i = 1; i < ipComponents.length - 1; i++) {
+ if (ipComponents[i] == "") {
+ // If we already found an empty component return null.
+ if (emptyIndex) {
+ return null;
+ }
+
+ emptyIndex = i;
+ }
+ }
+
+ // If we found an empty component, extend it.
+ if (emptyIndex) {
+ ipComponents[emptyIndex] = 0;
+
+ // Add components so we have a total of 8.
+ for (let count = ipComponents.length; count < 8; count++) {
+ ipComponents.splice(emptyIndex, 0, 0);
+ }
+ }
+
+ // Make sure there are 8 components.
+ if (ipComponents.length != 8) {
+ return null;
+ }
+
+ // Format all components to 4 character hex value.
+ for (let i = 0; i < ipComponents.length; i++) {
+ if (ipComponents[i] == "") {
+ ipComponents[i] = 0;
+ }
+
+ // Make sure the component is a number and it isn't larger than 0xffff.
+ if (/^[0-9a-f]{1,4}$/.test(ipComponents[i])) {
+ ipComponents[i] = parseInt(ipComponents[i], 16);
+ if (isNaN(ipComponents[i]) || ipComponents[i] > 0xffff) {
+ return null;
+ }
+ } else {
+ return null;
+ }
+
+ // Pad the component with 0:s.
+ ipComponents[i] = ("0000" + ipComponents[i].toString(16)).substr(-4);
+ }
+
+ // TODO: support Zone indices in Link-local addresses? Currently they are rejected.
+ // http://en.wikipedia.org/wiki/IPv6_address#Link-local_addresses_and_zone_indices
+
+ let hostName = ipComponents.join(":");
+ // Treat 0000:0000:0000:0000:0000:0000:0000:0000 as an invalid IPv6 address.
+ return hostName != "0000:0000:0000:0000:0000:0000:0000:0000"
+ ? hostName
+ : null;
+}
+
+/**
+ * Check if aHostName is a valid IP address (IPv4 or IPv6).
+ *
+ * @param {string} aHostName - The string to check for validity.
+ * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in
+ * addition to decimal.
+ * @returns {?string} Unobscured canonicalized IPv4 or IPv6 address if it is
+ * valid, otherwise null.
+ */
+function isLegalIPAddress(aHostName, aAllowExtendedIPFormats) {
+ return (
+ isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) ||
+ isLegalIPv6Address(aHostName)
+ );
+}
+
+/**
+ * Check if aIPAddress is a local or private IP address.
+ * Note: if the passed in address is not in canonical (unobscured form),
+ * the result may be wrong.
+ *
+ * @param {string} aIPAddress - A valid IP address literal in canonical
+ * (unobscured) form.
+ * @returns {boolean} frue if it is a local/private IPv4 or IPv6 address.
+ */
+function isLegalLocalIPAddress(aIPAddress) {
+ // IPv4 address?
+ let ipComponents = aIPAddress.split(".");
+ if (ipComponents.length == 4) {
+ // Check if it's a local or private IPv4 address.
+ return (
+ ipComponents[0] == 10 ||
+ ipComponents[0] == 127 || // loopback address
+ (ipComponents[0] == 192 && ipComponents[1] == 168) ||
+ (ipComponents[0] == 169 && ipComponents[1] == 254) ||
+ (ipComponents[0] == 172 && ipComponents[1] >= 16 && ipComponents[1] < 32)
+ );
+ }
+
+ // IPv6 address?
+ ipComponents = aIPAddress.split(":");
+ if (ipComponents.length == 8) {
+ // ::1/128 - localhost
+ if (
+ ipComponents[0] == "0000" &&
+ ipComponents[1] == "0000" &&
+ ipComponents[2] == "0000" &&
+ ipComponents[3] == "0000" &&
+ ipComponents[4] == "0000" &&
+ ipComponents[5] == "0000" &&
+ ipComponents[6] == "0000" &&
+ ipComponents[7] == "0001"
+ ) {
+ return true;
+ }
+
+ // fe80::/10 - link local addresses
+ if (ipComponents[0] == "fe80") {
+ return true;
+ }
+
+ // fc00::/7 - unique local addresses
+ if (
+ ipComponents[0].startsWith("fc") || // usage has not been defined yet
+ ipComponents[0].startsWith("fd")
+ ) {
+ return true;
+ }
+
+ return false;
+ }
+
+ return false;
+}
+
+/**
+ * Clean up the hostname or IP. Usually used to sanitize a value input by the user.
+ * It is usually applied before we know if the hostname is even valid.
+ *
+ * @param {string} aHostName - The hostname or IP string to clean up.
+ */
+function cleanUpHostName(aHostName) {
+ // TODO: Bug 235312: if UTF8 string was input, convert to punycode using convertUTF8toACE()
+ // but bug 563172 needs resolving first.
+ return aHostName.trim();
+}
diff --git a/comm/mailnews/base/src/mailstoreConverter.jsm b/comm/mailnews/base/src/mailstoreConverter.jsm
new file mode 100644
index 0000000000..6e5be5ebe1
--- /dev/null
+++ b/comm/mailnews/base/src/mailstoreConverter.jsm
@@ -0,0 +1,339 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["convertMailStoreTo", "terminateWorkers"];
+
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+let log = console.createInstance({
+ prefix: "mail.mailstoreconverter",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.mailstoreconverter.loglevel",
+});
+
+let gConverterWorker = null;
+
+/**
+ * Sets a server to use a different type of mailstore, converting
+ * all the existing data.
+ *
+ * @param {string} aMailstoreContractId - XPCOM id of new mailstore type.
+ * @param {nsIMsgServer} aServer - server to migrate.
+ * @param {?Element} aEventTarget - If set, element to send progress events.
+ *
+ * @returns {Promise<string>} - Resolves with a string containing the new root
+ * directory for the migrated server.
+ * Rejects with an error message.
+ */
+function convertMailStoreTo(aMailstoreContractId, aServer, aEventTarget) {
+ let accountRootFolder = aServer.rootFolder.filePath;
+
+ let srcType = null;
+ let destType = null;
+ if (aMailstoreContractId == "@mozilla.org/msgstore/maildirstore;1") {
+ srcType = "maildir";
+ destType = "mbox";
+ } else {
+ srcType = "mbox";
+ destType = "maildir";
+ }
+
+ // Go offline before conversion, so there aren't messages coming in during
+ // the process.
+ Services.io.offline = true;
+ let destDir = createTmpConverterFolder(
+ accountRootFolder,
+ aMailstoreContractId
+ );
+
+ // Return a promise that will complete once the worker is done.
+ return new Promise(function (resolve, reject) {
+ let worker = new ChromeWorker("resource:///modules/converterWorker.js");
+ gConverterWorker = worker;
+
+ // Helper to log error, clean up and reject with error message.
+ let bailout = function (errmsg) {
+ log.error("bailing out (" + errmsg + ")");
+ // Cleanup.
+ log.info("Trying to remove converter folder: " + destDir.path);
+ destDir.remove(true);
+ reject(errmsg);
+ };
+
+ // Handle exceptions thrown by the worker thread.
+ worker.addEventListener("error", function (e) {
+ // (e is type ErrorEvent)
+
+ // if we're lucky, the error will contain location info
+ if (e.filename && e.lineno) {
+ bailout(e.filename + ":" + e.lineno + ": " + e.message);
+ } else {
+ bailout(e.message);
+ }
+ });
+
+ // Handle updates from the worker thread.
+ worker.addEventListener("message", function (e) {
+ let response = e.data;
+ // log.debug("WORKER SAYS: " + JSON.stringify(response) + "\n");
+ if (response.msg == "progress") {
+ let val = response.val;
+ let total = response.total;
+
+ // Send the percentage completion to the GUI.
+ // XXX TODO: should probably check elapsed time, and throttle
+ // the events to avoid spending all our time drawing!
+ let ev = new Event("progress");
+ ev.detail = parseInt((val / total) * 100);
+ if (aEventTarget) {
+ aEventTarget.dispatchEvent(ev);
+ }
+ }
+ if (response.msg == "success") {
+ // If we receive this, the worker has completed, without errors.
+ let storeTypeIDs = {
+ mbox: "@mozilla.org/msgstore/berkeleystore;1",
+ maildir: "@mozilla.org/msgstore/maildirstore;1",
+ };
+ let newStoreTypeID = storeTypeIDs[destType];
+
+ try {
+ let finalRoot = installNewRoot(aServer, destDir, newStoreTypeID);
+ log.info(
+ "Conversion complete. Converted dir installed as: " + finalRoot
+ );
+ resolve(finalRoot);
+ } catch (e) {
+ bailout("installNewRoot() failed");
+ }
+ }
+ });
+
+ // Kick off the worker.
+ worker.postMessage({
+ srcType,
+ destType,
+ srcRoot: accountRootFolder.path,
+ destRoot: destDir.path,
+ });
+ });
+}
+
+/**
+ * Checks if Converter folder exists in tmp dir, removes it and creates a new
+ * "Converter" folder.
+ *
+ * @param {nsIFile} aFolder - account root folder.
+ * @param {string} aMailstoreContractId - XPCOM id of dest mailstore type
+ *
+ * @returns {nsIFile} - the new tmp directory to use as converter dest.
+ */
+function createTmpConverterFolder(aFolder, aMailstoreContractId) {
+ let tmpDir = FileUtils.getDir("TmpD", [], false);
+ let tmpFolder;
+ switch (aMailstoreContractId) {
+ case "@mozilla.org/msgstore/maildirstore;1": {
+ if (aFolder.leafName.substr(-8) == "-maildir") {
+ tmpFolder = new FileUtils.File(
+ PathUtils.join(
+ tmpDir.path,
+ aFolder.leafName.substr(0, aFolder.leafName.length - 8) + "-mbox"
+ )
+ );
+ } else {
+ tmpFolder = new FileUtils.File(
+ PathUtils.join(tmpDir.path, aFolder.leafName + "-mbox")
+ );
+ }
+
+ if (tmpFolder.exists()) {
+ log.info(
+ "Temporary Converter folder " +
+ tmpFolder.path +
+ " exists in tmp dir. Removing it"
+ );
+ tmpFolder.remove(true);
+ }
+ return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
+ }
+
+ case "@mozilla.org/msgstore/berkeleystore;1": {
+ if (aFolder.leafName.substr(-5) == "-mbox") {
+ tmpFolder = new FileUtils.File(
+ PathUtils.join(
+ tmpDir.path,
+ aFolder.leafName.substr(0, aFolder.leafName.length - 5) + "-maildir"
+ )
+ );
+ } else {
+ tmpFolder = new FileUtils.File(
+ PathUtils.join(tmpDir.path, aFolder.leafName + "-maildir")
+ );
+ }
+
+ if (tmpFolder.exists()) {
+ log.info(
+ "Temporary Converter folder " +
+ tmpFolder.path +
+ "exists in tmp dir. Removing it"
+ );
+ tmpFolder.remove(true);
+ }
+ return FileUtils.getDir("TmpD", [tmpFolder.leafName], true);
+ }
+
+ default: {
+ throw new Error(
+ "Unexpected mailstoreContractId: " + aMailstoreContractId
+ );
+ }
+ }
+}
+
+/**
+ * Switch server over to use the newly-converted directory tree.
+ * Moves the converted directory into an appropriate place for the server.
+ *
+ * @param {nsIMsgServer} server - server to migrate.
+ * @param {string} dir - dir of converted mailstore to install
+ * (will be moved by this function).
+ * @param {string} newStoreTypeID - XPCOM id of new mailstore type.
+ * @returns {string} new location of dir.
+ */
+function installNewRoot(server, dir, newStoreTypeID) {
+ let accountRootFolder = server.rootFolder.filePath;
+
+ // Migration is complete, get path of parent of account root
+ // folder into "parentPath" check if Converter folder already
+ // exists in "parentPath". If yes, remove it.
+ let lastSlash = accountRootFolder.path.lastIndexOf("/");
+ let parentPath = accountRootFolder.parent.path;
+ log.info("Path to parent folder of account root folder: " + parentPath);
+
+ let parent = new FileUtils.File(parentPath);
+ log.info("Path to parent folder of account root folder: " + parent.path);
+
+ let converterFolder = new FileUtils.File(
+ PathUtils.join(parent.path, dir.leafName)
+ );
+ if (converterFolder.exists()) {
+ log.info(
+ "Converter folder exists in " +
+ parentPath +
+ ". Removing already existing folder"
+ );
+ converterFolder.remove(true);
+ }
+
+ // Move Converter folder into the parent of account root folder.
+ try {
+ dir.moveTo(parent, dir.leafName);
+ // {nsIFile} new account root folder.
+ log.info("Path to new account root folder: " + converterFolder.path);
+ } catch (e) {
+ // Cleanup.
+ log.error(e);
+ log.error("Trying to remove converter folder: " + converterFolder.path);
+ converterFolder.remove(true);
+ throw e;
+ }
+
+ // If the account is imap then copy the msf file for the original
+ // root folder and rename the copy with the name of the new root
+ // folder.
+ if (server.type != "pop3" && server.type != "none") {
+ let converterFolderMsf = new FileUtils.File(
+ PathUtils.join(parent.path, dir.leafName + ".msf")
+ );
+ if (converterFolderMsf.exists()) {
+ converterFolderMsf.remove(true);
+ }
+
+ let oldRootFolderMsf = new FileUtils.File(
+ PathUtils.join(parent.path, accountRootFolder.leafName + ".msf")
+ );
+ if (oldRootFolderMsf.exists()) {
+ oldRootFolderMsf.copyTo(parent, converterFolderMsf.leafName);
+ }
+ }
+
+ if (server.type == "nntp") {
+ let converterFolderNewsrc = new FileUtils.File(
+ PathUtils.join(parent.path, "newsrc-" + dir.leafName)
+ );
+ if (converterFolderNewsrc.exists()) {
+ converterFolderNewsrc.remove(true);
+ }
+ let oldNewsrc = new FileUtils.File(
+ PathUtils.join(parent.path, "newsrc-" + accountRootFolder.leafName)
+ );
+ if (oldNewsrc.exists()) {
+ oldNewsrc.copyTo(parent, converterFolderNewsrc.leafName);
+ }
+ }
+
+ server.rootFolder.filePath = converterFolder;
+ server.localPath = converterFolder;
+ log.info("Path to account root folder: " + server.rootFolder.filePath.path);
+
+ // Set various preferences.
+ let p1 = "mail.server." + server.key + ".directory";
+ let p2 = "mail.server." + server.key + ".directory-rel";
+ let p3 = "mail.server." + server.key + ".newsrc.file";
+ let p4 = "mail.server." + server.key + ".newsrc.file-rel";
+ let p5 = "mail.server." + server.key + ".storeContractID";
+
+ Services.prefs.setCharPref(p1, converterFolder.path);
+ log.info(p1 + ": " + converterFolder.path);
+
+ // The directory-rel pref is of the form "[ProfD]Mail/pop.gmail.com
+ // " (pop accounts) or "[ProfD]ImapMail/imap.gmail.com" (imap
+ // accounts) ie the last slash "/" is followed by the root folder
+ // name. So, replace the old root folder name that follows the last
+ // slash with the new root folder name to set the correct value of
+ // directory-rel pref.
+ let directoryRel = Services.prefs.getCharPref(p2);
+ lastSlash = directoryRel.lastIndexOf("/");
+ directoryRel =
+ directoryRel.slice(0, lastSlash) + "/" + converterFolder.leafName;
+ Services.prefs.setCharPref(p2, directoryRel);
+ log.info(p2 + ": " + directoryRel);
+
+ if (server.type == "nntp") {
+ let newNewsrc = FileUtils.File(
+ PathUtils.join(parent.path, "newsrc-" + converterFolder.leafName)
+ );
+ Services.prefs.setCharPref(p3, newNewsrc.path);
+
+ // The newsrc.file-rel pref is of the form "[ProfD]News/newsrc-
+ // news.mozilla.org" ie the last slash "/" is followed by the
+ // newsrc file name. So, replace the old newsrc file name that
+ // follows the last slash with the new newsrc file name to set
+ // the correct value of newsrc.file-rel pref.
+ let newsrcRel = Services.prefs.getCharPref(p4);
+ lastSlash = newsrcRel.lastIndexOf("/");
+ newsrcRel = newsrcRel.slice(0, lastSlash) + "/" + newNewsrc.leafName;
+ Services.prefs.setCharPref(p4, newsrcRel);
+ log.info(p4 + ": " + newsrcRel);
+ }
+
+ Services.prefs.setCharPref(p5, newStoreTypeID);
+
+ Services.prefs.savePrefFile(null);
+
+ return converterFolder.path;
+}
+
+/**
+ * Terminate any workers involved in the conversion process.
+ */
+function terminateWorkers() {
+ // We're only using a single worker right now.
+ if (gConverterWorker !== null) {
+ gConverterWorker.terminate();
+ gConverterWorker = null;
+ }
+}
diff --git a/comm/mailnews/base/src/moz.build b/comm/mailnews/base/src/moz.build
new file mode 100644
index 0000000000..8255f50c67
--- /dev/null
+++ b/comm/mailnews/base/src/moz.build
@@ -0,0 +1,154 @@
+# 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/.
+
+EXPORTS += [
+ "HeaderReader.h",
+ "LineReader.h",
+ "nsImapMoveCoalescer.h",
+ "nsMailAuthModule.h",
+ "nsMailChannel.h",
+ "nsMailDirServiceDefs.h",
+ "nsMsgCompressIStream.h",
+ "nsMsgCompressOStream.h",
+ "nsMsgDBFolder.h",
+ "nsMsgEnumerator.h",
+ "nsMsgI18N.h",
+ "nsMsgIdentity.h",
+ "nsMsgIncomingServer.h",
+ "nsMsgKeySet.h",
+ "nsMsgLineBuffer.h",
+ "nsMsgMailNewsUrl.h",
+ "nsMsgProtocol.h",
+ "nsMsgReadStateTxn.h",
+ "nsMsgTxn.h",
+ "nsMsgUtils.h",
+ "nsNewMailnewsURI.h",
+ "nsQuarantinedOutputStream.h",
+ "UrlListener.h",
+]
+
+SOURCES += [
+ "MailNewsDLF.cpp",
+ "MailnewsLoadContextInfo.cpp",
+ "nsCidProtocolHandler.cpp",
+ "nsCopyMessageStreamListener.cpp",
+ "nsImapMoveCoalescer.cpp",
+ "nsMailAuthModule.cpp",
+ "nsMailChannel.cpp",
+ "nsMailDirProvider.cpp",
+ "nsMessenger.cpp",
+ "nsMessengerBootstrap.cpp",
+ "nsMsgAccount.cpp",
+ "nsMsgAccountManager.cpp",
+ "nsMsgBiffManager.cpp",
+ "nsMsgCompressIStream.cpp",
+ "nsMsgCompressOStream.cpp",
+ "nsMsgContentPolicy.cpp",
+ "nsMsgCopyService.cpp",
+ "nsMsgDBFolder.cpp",
+ "nsMsgDBView.cpp",
+ "nsMsgEnumerator.cpp",
+ "nsMsgFileStream.cpp",
+ "nsMsgFolderCache.cpp",
+ "nsMsgFolderCompactor.cpp",
+ "nsMsgFolderNotificationService.cpp",
+ "nsMsgGroupThread.cpp",
+ "nsMsgGroupView.cpp",
+ "nsMsgI18N.cpp",
+ "nsMsgIdentity.cpp",
+ "nsMsgIncomingServer.cpp",
+ "nsMsgKeySet.cpp",
+ "nsMsgLineBuffer.cpp",
+ "nsMsgMailNewsUrl.cpp",
+ "nsMsgMailSession.cpp",
+ "nsMsgOfflineManager.cpp",
+ "nsMsgProgress.cpp",
+ "nsMsgProtocol.cpp",
+ "nsMsgPurgeService.cpp",
+ "nsMsgQuickSearchDBView.cpp",
+ "nsMsgReadStateTxn.cpp",
+ "nsMsgSearchDBView.cpp",
+ "nsMsgSpecialViews.cpp",
+ "nsMsgStatusFeedback.cpp",
+ "nsMsgTagService.cpp",
+ "nsMsgThreadedDBView.cpp",
+ "nsMsgTxn.cpp",
+ "nsMsgUtils.cpp",
+ "nsMsgWindow.cpp",
+ "nsMsgXFViewThread.cpp",
+ "nsMsgXFVirtualFolderDBView.cpp",
+ "nsNewMailnewsURI.cpp",
+ "nsQuarantinedOutputStream.cpp",
+ "nsSpamSettings.cpp",
+ "nsStatusBarBiffManager.cpp",
+ "nsStopwatch.cpp",
+ "nsSubscribableServer.cpp",
+ "UrlListener.cpp",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ SOURCES += [
+ "nsMessengerWinIntegration.cpp",
+ # This file cannot be built in unified mode because of name clashes with Windows headers.
+ "nsUserInfoWin.cpp",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ SOURCES += [
+ "nsMessengerUnixIntegration.cpp",
+ "nsUserInfoUnix.cpp",
+ ]
+elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ SOURCES += [
+ "nsMessengerOSXIntegration.mm",
+ "nsUserInfoMac.mm",
+ ]
+
+EXTRA_JS_MODULES += [
+ "ABQueryUtils.jsm",
+ "converterWorker.js",
+ "FolderLookupService.jsm",
+ "FolderUtils.jsm",
+ "hostnameUtils.jsm",
+ "JXON.jsm",
+ "LineReader.jsm",
+ "MailAuthenticator.jsm",
+ "MailChannel.sys.mjs",
+ "MailCryptoUtils.jsm",
+ "MailnewsMigrator.jsm",
+ "MailNotificationManager.jsm",
+ "MailNotificationService.jsm",
+ "MailServices.jsm",
+ "mailstoreConverter.jsm",
+ "MailStringUtils.jsm",
+ "MsgAsyncPrompter.jsm",
+ "MsgDBCacheManager.jsm",
+ "MsgIncomingServer.jsm",
+ "MsgKeySet.jsm",
+ "MsgProtocolInfo.sys.mjs",
+ "OAuth2.jsm",
+ "OAuth2Module.jsm",
+ "OAuth2Providers.jsm",
+ "TemplateUtils.jsm",
+ "VirtualFolderWrapper.jsm",
+ "WinUnreadBadge.jsm",
+]
+
+USE_LIBS += [
+ "jsoncpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/netwerk/base",
+ "/toolkit/components/jsoncpp/include",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "mail"
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mailnews/base/src/nsCidProtocolHandler.cpp b/comm/mailnews/base/src/nsCidProtocolHandler.cpp
new file mode 100644
index 0000000000..50f14bd782
--- /dev/null
+++ b/comm/mailnews/base/src/nsCidProtocolHandler.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsCidProtocolHandler.h"
+#include "nsString.h"
+#include "nsIURI.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsComponentManagerUtils.h"
+
+nsCidProtocolHandler::nsCidProtocolHandler() {}
+
+nsCidProtocolHandler::~nsCidProtocolHandler() {}
+
+NS_IMPL_ISUPPORTS(nsCidProtocolHandler, nsIProtocolHandler)
+
+NS_IMETHODIMP nsCidProtocolHandler::GetScheme(nsACString& aScheme) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsCidProtocolHandler::NewURI(const nsACString& aSpec,
+ const char* aOriginCharset,
+ nsIURI* aBaseURI, nsIURI** _retval) {
+ // the right fix is to use the baseSpec (or aBaseUri)
+ // and specify the cid, and then fix mime
+ // to handle that, like it does with "...&part=1.2"
+ // for now, do about blank to prevent spam
+ // from popping up annoying alerts about not implementing the cid
+ // protocol
+ nsCOMPtr<nsIURI> url;
+ nsresult rv = NS_NewURI(getter_AddRefs(url), "about:blank");
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ url.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCidProtocolHandler::NewChannel(nsIURI* aURI,
+ nsILoadInfo* aLoadInfo,
+ nsIChannel** _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsCidProtocolHandler::AllowPort(int32_t port, const char* scheme,
+ bool* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/comm/mailnews/base/src/nsCidProtocolHandler.h b/comm/mailnews/base/src/nsCidProtocolHandler.h
new file mode 100644
index 0000000000..9d84812ee7
--- /dev/null
+++ b/comm/mailnews/base/src/nsCidProtocolHandler.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsCidProtocolHandler_h__
+#define nsCidProtocolHandler_h__
+
+#include "nsCOMPtr.h"
+#include "nsIProtocolHandler.h"
+
+class nsCidProtocolHandler : public nsIProtocolHandler {
+ public:
+ nsCidProtocolHandler();
+ static nsresult NewURI(const nsACString& aSpec, const char* aOriginCharset,
+ nsIURI* aBaseURI, nsIURI** _retval);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROTOCOLHANDLER
+
+ private:
+ virtual ~nsCidProtocolHandler();
+};
+
+#endif /* nsCidProtocolHandler_h__ */
diff --git a/comm/mailnews/base/src/nsCopyMessageStreamListener.cpp b/comm/mailnews/base/src/nsCopyMessageStreamListener.cpp
new file mode 100644
index 0000000000..343c27ac54
--- /dev/null
+++ b/comm/mailnews/base/src/nsCopyMessageStreamListener.cpp
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsCopyMessageStreamListener.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsIChannel.h"
+#include "nsIURI.h"
+
+NS_IMPL_ISUPPORTS(nsCopyMessageStreamListener, nsIStreamListener,
+ nsIRequestObserver, nsICopyMessageStreamListener)
+
+nsCopyMessageStreamListener::nsCopyMessageStreamListener() {}
+
+nsCopyMessageStreamListener::~nsCopyMessageStreamListener() {
+ // All member variables are nsCOMPtr's.
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::Init(
+ nsICopyMessageListener* destination) {
+ mDestination = destination;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::StartMessage() {
+ if (mDestination) {
+ return mDestination->StartMessage();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::EndMessage(nsMsgKey key) {
+ if (mDestination) {
+ return mDestination->EndMessage(key);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::OnDataAvailable(
+ nsIRequest* /* request */, nsIInputStream* aIStream, uint64_t sourceOffset,
+ uint32_t aLength) {
+ return mDestination->CopyData(aIStream, aLength);
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::OnStartRequest(nsIRequest* request) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIURI> uri;
+
+ // We know the request is an nsIChannel we can get a URI from, but this is
+ // probably bad form. See Bug 1528662.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error QI nsIRequest to nsIChannel failed");
+ if (NS_SUCCEEDED(rv)) rv = channel->GetURI(getter_AddRefs(uri));
+ if (NS_SUCCEEDED(rv)) rv = mDestination->BeginCopy();
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::EndCopy(nsIURI* uri,
+ nsresult status) {
+ nsresult rv;
+ bool copySucceeded = (status == NS_BINDING_SUCCEEDED);
+ rv = mDestination->EndCopy(copySucceeded);
+ // If this is a move and we finished the copy, delete the old message.
+ bool moveMessage = false;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailURL(do_QueryInterface(uri));
+ if (mailURL) rv = mailURL->IsUrlType(nsIMsgMailNewsUrl::eMove, &moveMessage);
+
+ if (NS_FAILED(rv)) moveMessage = false;
+
+ // OK, this is wrong if we're moving to an imap folder, for example. This
+ // really says that we were able to pull the message from the source, NOT that
+ // we were able to put it in the destination!
+ if (moveMessage) {
+ // don't do this if we're moving to an imap folder - that's handled
+ // elsewhere.
+ nsCOMPtr<nsIMsgImapMailFolder> destImap = do_QueryInterface(mDestination);
+ // if the destination is a local folder, it will handle the delete from the
+ // source in EndMove
+ if (!destImap) rv = mDestination->EndMove(copySucceeded);
+ }
+ // Even if the above actions failed we probably still want to return NS_OK.
+ // There should probably be some error dialog if either the copy or delete
+ // failed.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsCopyMessageStreamListener::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ nsresult rv;
+ // We know the request is an nsIChannel we can get a URI from, but this is
+ // probably bad form. See Bug 1528662.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error QI nsIRequest to nsIChannel failed");
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> uri;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return EndCopy(uri, aStatus);
+}
diff --git a/comm/mailnews/base/src/nsCopyMessageStreamListener.h b/comm/mailnews/base/src/nsCopyMessageStreamListener.h
new file mode 100644
index 0000000000..509dcef019
--- /dev/null
+++ b/comm/mailnews/base/src/nsCopyMessageStreamListener.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef NSCOPYMESSAGESTREAMLISTENER_H
+#define NSCOPYMESSAGESTREAMLISTENER_H
+
+#include "nsICopyMessageStreamListener.h"
+#include "nsIStreamListener.h"
+#include "nsICopyMessageListener.h"
+#include "nsCOMPtr.h"
+
+class nsCopyMessageStreamListener : public nsIStreamListener,
+ public nsICopyMessageStreamListener {
+ public:
+ nsCopyMessageStreamListener();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSICOPYMESSAGESTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ protected:
+ virtual ~nsCopyMessageStreamListener();
+ nsCOMPtr<nsICopyMessageListener> mDestination;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsImapMoveCoalescer.cpp b/comm/mailnews/base/src/nsImapMoveCoalescer.cpp
new file mode 100644
index 0000000000..160f204a61
--- /dev/null
+++ b/comm/mailnews/base/src/nsImapMoveCoalescer.cpp
@@ -0,0 +1,198 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsImapMoveCoalescer.h"
+#include "nsIImapService.h"
+#include "nsIMsgCopyService.h"
+#include "nsIMsgFolder.h" // TO include biffState enum. Change to bool later...
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsThreadUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/ArrayUtils.h"
+
+NS_IMPL_ISUPPORTS(nsImapMoveCoalescer, nsIUrlListener)
+
+nsImapMoveCoalescer::nsImapMoveCoalescer(nsIMsgFolder* sourceFolder,
+ nsIMsgWindow* msgWindow) {
+ m_sourceFolder = sourceFolder;
+ m_msgWindow = msgWindow;
+ m_hasPendingMoves = false;
+}
+
+nsImapMoveCoalescer::~nsImapMoveCoalescer() {}
+
+nsresult nsImapMoveCoalescer::AddMove(nsIMsgFolder* folder, nsMsgKey key) {
+ m_hasPendingMoves = true;
+ int32_t folderIndex = m_destFolders.IndexOf(folder);
+ nsTArray<nsMsgKey>* keysToAdd = nullptr;
+
+ if (folderIndex >= 0)
+ keysToAdd = &(m_sourceKeyArrays[folderIndex]);
+ else {
+ m_destFolders.AppendObject(folder);
+ keysToAdd = m_sourceKeyArrays.AppendElement();
+ if (!keysToAdd) return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (!keysToAdd->Contains(key)) keysToAdd->AppendElement(key);
+
+ return NS_OK;
+}
+
+nsresult nsImapMoveCoalescer::PlaybackMoves(
+ bool doNewMailNotification /* = false */) {
+ int32_t numFolders = m_destFolders.Count();
+ // Nothing to do, so don't change the member variables.
+ if (numFolders == 0) return NS_OK;
+
+ nsresult rv = NS_OK;
+ m_hasPendingMoves = false;
+ m_doNewMailNotification = doNewMailNotification;
+ m_outstandingMoves = 0;
+
+ for (int32_t i = 0; i < numFolders; ++i) {
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // is this the right place to make sure dest folder exists
+ // (and has proper flags?), before we start copying?
+ nsCOMPtr<nsIMsgFolder> destFolder(m_destFolders[i]);
+ nsTArray<nsMsgKey>& keysToAdd = m_sourceKeyArrays[i];
+ int32_t numNewMessages = 0;
+ int32_t numKeysToAdd = keysToAdd.Length();
+ if (numKeysToAdd == 0) continue;
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages(keysToAdd.Length());
+ for (uint32_t keyIndex = 0; keyIndex < keysToAdd.Length(); keyIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> mailHdr = nullptr;
+ rv = m_sourceFolder->GetMessageHeader(keysToAdd.ElementAt(keyIndex),
+ getter_AddRefs(mailHdr));
+ if (NS_SUCCEEDED(rv) && mailHdr) {
+ messages.AppendElement(mailHdr);
+ bool isRead = false;
+ mailHdr->GetIsRead(&isRead);
+ if (!isRead) numNewMessages++;
+ }
+ }
+ uint32_t destFlags;
+ destFolder->GetFlags(&destFlags);
+ if (!(destFlags &
+ nsMsgFolderFlags::Junk)) // don't set has new on junk folder
+ {
+ destFolder->SetNumNewMessages(numNewMessages);
+ if (numNewMessages > 0) destFolder->SetHasNewMessages(true);
+ }
+ // adjust the new message count on the source folder
+ int32_t oldNewMessageCount = 0;
+ m_sourceFolder->GetNumNewMessages(false, &oldNewMessageCount);
+ if (oldNewMessageCount >= numKeysToAdd)
+ oldNewMessageCount -= numKeysToAdd;
+ else
+ oldNewMessageCount = 0;
+
+ m_sourceFolder->SetNumNewMessages(oldNewMessageCount);
+
+ keysToAdd.Clear();
+ nsCOMPtr<nsIMsgCopyService> copySvc =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1");
+ if (copySvc) {
+ nsCOMPtr<nsIMsgCopyServiceListener> listener;
+ if (m_doNewMailNotification) {
+ nsMoveCoalescerCopyListener* copyListener =
+ new nsMoveCoalescerCopyListener(this, destFolder);
+ if (copyListener) listener = copyListener;
+ }
+ rv = copySvc->CopyMessages(m_sourceFolder, messages, destFolder, true,
+ listener, m_msgWindow, false /*allowUndo*/);
+ if (NS_SUCCEEDED(rv)) m_outstandingMoves++;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsImapMoveCoalescer::OnStartRunningUrl(nsIURI* aUrl) {
+ NS_ASSERTION(aUrl, "just a sanity check");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsImapMoveCoalescer::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ m_outstandingMoves--;
+ if (m_doNewMailNotification && !m_outstandingMoves) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder =
+ do_QueryInterface(m_sourceFolder);
+ if (imapFolder) imapFolder->NotifyIfNewMail();
+ }
+ return NS_OK;
+}
+
+nsTArray<nsMsgKey>* nsImapMoveCoalescer::GetKeyBucket(uint32_t keyArrayIndex) {
+ NS_ASSERTION(keyArrayIndex < MOZ_ARRAY_LENGTH(m_keyBuckets), "invalid index");
+
+ return keyArrayIndex < mozilla::ArrayLength(m_keyBuckets)
+ ? &(m_keyBuckets[keyArrayIndex])
+ : nullptr;
+}
+
+NS_IMPL_ISUPPORTS(nsMoveCoalescerCopyListener, nsIMsgCopyServiceListener)
+
+nsMoveCoalescerCopyListener::nsMoveCoalescerCopyListener(
+ nsImapMoveCoalescer* coalescer, nsIMsgFolder* destFolder) {
+ m_destFolder = destFolder;
+ m_coalescer = coalescer;
+}
+
+nsMoveCoalescerCopyListener::~nsMoveCoalescerCopyListener() {}
+
+NS_IMETHODIMP nsMoveCoalescerCopyListener::OnStartCopy() {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */
+NS_IMETHODIMP nsMoveCoalescerCopyListener::OnProgress(uint32_t aProgress,
+ uint32_t aProgressMax) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void SetMessageKey (in uint32_t aKey); */
+NS_IMETHODIMP nsMoveCoalescerCopyListener::SetMessageKey(uint32_t aKey) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void GetMessageId (in nsACString aMessageId); */
+NS_IMETHODIMP nsMoveCoalescerCopyListener::GetMessageId(nsACString& messageId) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void OnStopCopy (in nsresult aStatus); */
+NS_IMETHODIMP nsMoveCoalescerCopyListener::OnStopCopy(nsresult aStatus) {
+ nsresult rv = NS_OK;
+ if (NS_SUCCEEDED(aStatus)) {
+ // if the dest folder is imap, update it.
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_destFolder);
+ if (imapFolder) {
+ uint32_t folderFlags;
+ m_destFolder->GetFlags(&folderFlags);
+ if (!(folderFlags & (nsMsgFolderFlags::Junk | nsMsgFolderFlags::Trash))) {
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> url;
+ rv = imapService->SelectFolder(m_destFolder, m_coalescer, nullptr,
+ getter_AddRefs(url));
+ }
+ } else // give junk filters a chance to run on new msgs in destination
+ // local folder
+ {
+ bool filtersRun;
+ m_destFolder->CallFilterPlugins(nullptr, &filtersRun);
+ }
+ }
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsImapMoveCoalescer.h b/comm/mailnews/base/src/nsImapMoveCoalescer.h
new file mode 100644
index 0000000000..5471c4bd88
--- /dev/null
+++ b/comm/mailnews/base/src/nsImapMoveCoalescer.h
@@ -0,0 +1,71 @@
+/* -*- 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/. */
+
+#ifndef _nsImapMoveCoalescer_H
+#define _nsImapMoveCoalescer_H
+
+#include "msgCore.h"
+#include "nsCOMArray.h"
+#include "nsIMsgWindow.h"
+#include "nsCOMPtr.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgCopyServiceListener.h"
+
+// imap move coalescer class - in order to keep nsImapMailFolder from growing
+// like Topsy Logically, we want to keep track of an nsTArray<nsMsgKey> per
+// nsIMsgFolder, and then be able to retrieve them one by one and play back the
+// moves. This utility class will be used by both the filter code and the
+// offline playback code, to avoid multiple moves to the same folder.
+
+class nsImapMoveCoalescer : public nsIUrlListener {
+ public:
+ friend class nsMoveCoalescerCopyListener;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+
+ nsImapMoveCoalescer(nsIMsgFolder* sourceFolder, nsIMsgWindow* msgWindow);
+
+ nsresult AddMove(nsIMsgFolder* folder, nsMsgKey key);
+ nsresult PlaybackMoves(bool doNewMailNotification = false);
+ // this lets the caller store keys in an arbitrary number of buckets. If the
+ // bucket for the passed in index doesn't exist, it will get created.
+ nsTArray<nsMsgKey>* GetKeyBucket(uint32_t keyArrayIndex);
+ nsIMsgWindow* GetMsgWindow() { return m_msgWindow; }
+ bool HasPendingMoves() { return m_hasPendingMoves; }
+
+ protected:
+ virtual ~nsImapMoveCoalescer();
+ // m_sourceKeyArrays and m_destFolders are parallel arrays.
+ nsTArray<nsTArray<nsMsgKey> > m_sourceKeyArrays;
+ nsCOMArray<nsIMsgFolder> m_destFolders;
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ nsCOMPtr<nsIMsgFolder> m_sourceFolder;
+ bool m_doNewMailNotification;
+ bool m_hasPendingMoves;
+ nsTArray<nsMsgKey> m_keyBuckets[2];
+ int32_t m_outstandingMoves;
+};
+
+class nsMoveCoalescerCopyListener final : public nsIMsgCopyServiceListener {
+ public:
+ nsMoveCoalescerCopyListener(nsImapMoveCoalescer* coalescer,
+ nsIMsgFolder* destFolder);
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ nsCOMPtr<nsIMsgFolder> m_destFolder;
+
+ nsImapMoveCoalescer* m_coalescer;
+ // when we get OnStopCopy, update the folder. When we've finished all the
+ // copies, send the biff notification.
+
+ private:
+ ~nsMoveCoalescerCopyListener();
+};
+
+#endif // _nsImapMoveCoalescer_H
diff --git a/comm/mailnews/base/src/nsMailAuthModule.cpp b/comm/mailnews/base/src/nsMailAuthModule.cpp
new file mode 100644
index 0000000000..69089737df
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailAuthModule.cpp
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIAuthModule.h"
+#include "nsIMailAuthModule.h"
+#include "nsMailAuthModule.h"
+#include "nsString.h"
+#include "plbase64.h"
+
+NS_IMPL_ISUPPORTS(nsMailAuthModule, nsIMailAuthModule)
+
+nsMailAuthModule::nsMailAuthModule() {}
+
+nsMailAuthModule::~nsMailAuthModule() {}
+
+/**
+ * A simple wrap of CreateInstance and Init of nsIAuthModule.
+ */
+NS_IMETHODIMP
+nsMailAuthModule::Init(const char* type, const nsACString& serviceName,
+ uint32_t serviceFlags, const nsAString& domain,
+ const nsAString& username, const nsAString& password) {
+ mAuthModule = nsIAuthModule::CreateInstance(type);
+ return mAuthModule->Init(serviceName, serviceFlags, domain, username,
+ password);
+}
+
+/**
+ * A wrap of nsIAuthModule::GetNextToken with two extra processings:
+ * 1. inToken is base64 decoded then passed to nsIAuthModule::GetNextToken.
+ * 2. The out value of nsIAuthModule::GetNextToken is base64 encoded then
+ * assigned to outToken.
+ */
+NS_IMETHODIMP
+nsMailAuthModule::GetNextToken(const nsACString& inToken,
+ nsACString& outToken) {
+ nsresult rv;
+ void *inBuf, *outBuf;
+ uint32_t inBufLen = 0, outBufLen = 0;
+ uint32_t len = inToken.Length();
+ if (len > 0) {
+ // Decode into the input buffer.
+ inBufLen = (len * 3) / 4; // sufficient size (see plbase64.h)
+ inBuf = moz_xmalloc(inBufLen);
+
+ // Strip off any padding (see bug 230351).
+ char* challenge = ToNewCString(inToken);
+ while (challenge[len - 1] == '=') len--;
+
+ // We need to know the exact length of the decoded string to give to
+ // the GSSAPI libraries. But NSPR's base64 routine doesn't seem capable
+ // of telling us that. So, we figure it out for ourselves.
+
+ // For every 4 characters, add 3 to the destination
+ // If there are 3 remaining, add 2
+ // If there are 2 remaining, add 1
+ // 1 remaining is an error
+ inBufLen =
+ (len / 4) * 3 + ((len % 4 == 3) ? 2 : 0) + ((len % 4 == 2) ? 1 : 0);
+ PL_Base64Decode(challenge, len, (char*)inBuf);
+ free(challenge);
+ } else {
+ inBufLen = 0;
+ inBuf = NULL;
+ }
+
+ rv = mAuthModule->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen);
+ free(inBuf);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // It's not an error if outBuf is empty, return an empty string as reply to
+ // the server.
+ if (outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str) {
+ outToken.Adopt(base64Str);
+ } else {
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ }
+ free(outBuf);
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMailAuthModule.h b/comm/mailnews/base/src/nsMailAuthModule.h
new file mode 100644
index 0000000000..e6bca30623
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailAuthModule.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMailAuthModule_h__
+#define nsMailAuthModule_h__
+
+#include "nsCOMPtr.h"
+#include "nsIAuthModule.h"
+#include "nsIMailAuthModule.h"
+
+class nsMailAuthModule : public nsIMailAuthModule {
+ public:
+ nsMailAuthModule();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMAILAUTHMODULE
+
+ protected:
+ virtual ~nsMailAuthModule();
+
+ private:
+ nsCOMPtr<nsIAuthModule> mAuthModule;
+};
+
+#endif /* nsMailAuthModule_h__ */
diff --git a/comm/mailnews/base/src/nsMailChannel.cpp b/comm/mailnews/base/src/nsMailChannel.cpp
new file mode 100644
index 0000000000..1e427718e0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailChannel.cpp
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMailChannel.h"
+#include "nsHashPropertyBag.h"
+#include "nsServiceManagerUtils.h"
+#include "nsICharsetConverterManager.h"
+
+NS_IMETHODIMP
+nsMailChannel::AddHeaderFromMIME(const nsACString& name,
+ const nsACString& value) {
+ mHeaderNames.AppendElement(name);
+ mHeaderValues.AppendElement(value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetHeaderNames(nsTArray<nsCString>& aHeaderNames) {
+ aHeaderNames = mHeaderNames.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetHeaderValues(nsTArray<nsCString>& aHeaderValues) {
+ aHeaderValues = mHeaderValues.Clone();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::HandleAttachmentFromMIME(const nsACString& contentType,
+ const nsACString& url,
+ const nsACString& displayName,
+ const nsACString& uri,
+ bool notDownloaded) {
+ RefPtr<nsIWritablePropertyBag2> attachment = new nsHashPropertyBag();
+ attachment->SetPropertyAsAUTF8String(u"contentType"_ns, contentType);
+ attachment->SetPropertyAsAUTF8String(u"url"_ns, url);
+ attachment->SetPropertyAsAUTF8String(u"displayName"_ns, displayName);
+ attachment->SetPropertyAsAUTF8String(u"uri"_ns, uri);
+ attachment->SetPropertyAsBool(u"notDownloaded"_ns, notDownloaded);
+ mAttachments.AppendElement(attachment);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::AddAttachmentFieldFromMIME(const nsACString& field,
+ const nsACString& value) {
+ nsIWritablePropertyBag2* attachment = mAttachments.LastElement();
+ attachment->SetPropertyAsAUTF8String(NS_ConvertUTF8toUTF16(nsCString(field)),
+ value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetAttachments(
+ nsTArray<RefPtr<nsIPropertyBag2> >& aAttachments) {
+ aAttachments.Clear();
+ for (nsIWritablePropertyBag2* attachment : mAttachments) {
+ aAttachments.AppendElement(static_cast<nsIPropertyBag2*>(attachment));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetMailCharacterSet(nsACString& aMailCharacterSet) {
+ aMailCharacterSet = mMailCharacterSet;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetMailCharacterSet(const nsACString& aMailCharacterSet) {
+ mMailCharacterSet = aMailCharacterSet;
+
+ // Convert to a canonical charset name instead of using the charset name from
+ // the message header as is. This is needed for charset menu item to have a
+ // check mark correctly.
+ nsresult rv;
+ nsCOMPtr<nsICharsetConverterManager> ccm =
+ do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return ccm->GetCharsetAlias(PromiseFlatCString(aMailCharacterSet).get(),
+ mMailCharacterSet);
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetImipMethod(nsACString& aImipMethod) {
+ aImipMethod = mImipMethod;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetImipMethod(const nsACString& aImipMethod) {
+ mImipMethod = aImipMethod;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetImipItem(calIItipItem** aImipItem) {
+ NS_IF_ADDREF(*aImipItem = mImipItem);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetImipItem(calIItipItem* aImipItem) {
+ mImipItem = aImipItem;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetSmimeHeaderSink(nsIMsgSMIMEHeaderSink** aSmimeHeaderSink) {
+ NS_IF_ADDREF(*aSmimeHeaderSink = mSmimeHeaderSink);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetSmimeHeaderSink(nsIMsgSMIMEHeaderSink* aSmimeHeaderSink) {
+ mSmimeHeaderSink = aSmimeHeaderSink;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::GetListener(nsIMailProgressListener** aListener) {
+ nsCOMPtr<nsIMailProgressListener> listener = do_QueryReferent(mListener);
+ if (listener) {
+ NS_IF_ADDREF(*aListener = listener);
+ } else {
+ *aListener = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailChannel::SetListener(nsIMailProgressListener* aListener) {
+ nsresult rv;
+ mListener = do_GetWeakReference(aListener, &rv);
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMailChannel.h b/comm/mailnews/base/src/nsMailChannel.h
new file mode 100644
index 0000000000..91cf59705a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailChannel.h
@@ -0,0 +1,30 @@
+/* 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/. */
+
+#ifndef nsMailChannel_h__
+#define nsMailChannel_h__
+
+#include "nsIMailChannel.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsTArray.h"
+#include "nsTString.h"
+#include "calIItipItem.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsMailChannel : public nsIMailChannel {
+ public:
+ NS_DECL_NSIMAILCHANNEL
+
+ protected:
+ nsTArray<nsCString> mHeaderNames;
+ nsTArray<nsCString> mHeaderValues;
+ nsTArray<RefPtr<nsIWritablePropertyBag2>> mAttachments;
+ nsCString mMailCharacterSet;
+ nsCString mImipMethod;
+ nsCOMPtr<calIItipItem> mImipItem;
+ nsCOMPtr<nsIMsgSMIMEHeaderSink> mSmimeHeaderSink;
+ nsWeakPtr mListener;
+};
+
+#endif /* nsMailChannel_h__ */
diff --git a/comm/mailnews/base/src/nsMailDirProvider.cpp b/comm/mailnews/base/src/nsMailDirProvider.cpp
new file mode 100644
index 0000000000..5d092203cf
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailDirProvider.cpp
@@ -0,0 +1,160 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMailDirProvider.h"
+#include "nsMailDirServiceDefs.h"
+#include "nsXULAppAPI.h"
+#include "nsCOMArray.h"
+#include "nsEnumeratorUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIChromeRegistry.h"
+#include "nsICategoryManager.h"
+#include "nsServiceManagerUtils.h"
+#include "nsDirectoryServiceUtils.h"
+
+#define MAIL_DIR_50_NAME "Mail"
+#define IMAP_MAIL_DIR_50_NAME "ImapMail"
+#define NEWS_DIR_50_NAME "News"
+
+nsresult nsMailDirProvider::EnsureDirectory(nsIFile* aDirectory) {
+ bool exists;
+ nsresult rv = aDirectory->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!exists) rv = aDirectory->Create(nsIFile::DIRECTORY_TYPE, 0700);
+
+ return rv;
+}
+
+NS_IMPL_ISUPPORTS(nsMailDirProvider, nsIDirectoryServiceProvider,
+ nsIDirectoryServiceProvider2)
+
+NS_IMETHODIMP
+nsMailDirProvider::GetFile(const char* aKey, bool* aPersist,
+ nsIFile** aResult) {
+ // NOTE: This function can be reentrant through the NS_GetSpecialDirectory
+ // call, so be careful not to cause infinite recursion.
+ // i.e. the check for supported files must come first.
+ const char* leafName = nullptr;
+ bool isDirectory = true;
+
+ if (!strcmp(aKey, NS_APP_MAIL_50_DIR)) {
+ leafName = MAIL_DIR_50_NAME;
+ } else if (!strcmp(aKey, NS_APP_IMAP_MAIL_50_DIR)) {
+ leafName = IMAP_MAIL_DIR_50_NAME;
+ } else if (!strcmp(aKey, NS_APP_NEWS_50_DIR)) {
+ leafName = NEWS_DIR_50_NAME;
+ } else if (!strcmp(aKey, NS_APP_MESSENGER_FOLDER_CACHE_50_FILE)) {
+ isDirectory = false;
+ leafName = "folderCache.json";
+ } else if (!strcmp(aKey, NS_APP_MESSENGER_LEGACY_FOLDER_CACHE_50_FILE)) {
+ isDirectory = false;
+ leafName = "panacea.dat";
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIFile> parentDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(parentDir));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIFile> file;
+ rv = parentDir->Clone(getter_AddRefs(file));
+ if (NS_FAILED(rv)) return rv;
+
+ nsDependentCString leafStr(leafName);
+ rv = file->AppendNative(leafStr);
+ if (NS_FAILED(rv)) return rv;
+
+ bool exists;
+ if (isDirectory && NS_SUCCEEDED(file->Exists(&exists)) && !exists)
+ rv = EnsureDirectory(file);
+
+ *aPersist = true;
+ file.forget(aResult);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMailDirProvider::GetFiles(const char* aKey, nsISimpleEnumerator** aResult) {
+ if (strcmp(aKey, ISP_DIRECTORY_LIST) != 0) return NS_ERROR_FAILURE;
+
+ // The list of isp directories includes the isp directory
+ // in the current process dir (i.e. <path to thunderbird.exe>\isp and
+ // <path to thunderbird.exe>\isp\locale
+ // and isp and isp\locale for each active extension
+
+ nsCOMPtr<nsIProperties> dirSvc =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
+ if (!dirSvc) return NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIFile> currentProcessDir;
+ nsresult rv = dirSvc->Get(NS_XPCOM_CURRENT_PROCESS_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(currentProcessDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISimpleEnumerator> directoryEnumerator;
+ rv = NS_NewSingletonEnumerator(getter_AddRefs(directoryEnumerator),
+ currentProcessDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ADDREF(*aResult = new AppendingEnumerator(directoryEnumerator));
+ return NS_SUCCESS_AGGREGATE_RESULT;
+}
+
+NS_IMETHODIMP
+nsMailDirProvider::AppendingEnumerator::HasMoreElements(bool* aResult) {
+ *aResult = mNext || mNextWithLocale ? true : false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMailDirProvider::AppendingEnumerator::GetNext(nsISupports** aResult) {
+ // Set the return value to the next directory we want to enumerate over
+ if (aResult) NS_ADDREF(*aResult = mNext);
+
+ if (mNextWithLocale) {
+ mNext = mNextWithLocale;
+ mNextWithLocale = nullptr;
+ return NS_OK;
+ }
+
+ mNext = nullptr;
+
+ // Ignore all errors
+
+ bool more;
+ while (NS_SUCCEEDED(mBase->HasMoreElements(&more)) && more) {
+ nsCOMPtr<nsISupports> nextbasesupp;
+ mBase->GetNext(getter_AddRefs(nextbasesupp));
+
+ nsCOMPtr<nsIFile> nextbase(do_QueryInterface(nextbasesupp));
+ if (!nextbase) continue;
+
+ nextbase->Clone(getter_AddRefs(mNext));
+ if (!mNext) continue;
+
+ mNext->AppendNative("isp"_ns);
+ bool exists;
+ nsresult rv = mNext->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists) {
+ break;
+ }
+
+ mNext = nullptr;
+ }
+
+ return NS_OK;
+}
+
+nsMailDirProvider::AppendingEnumerator::AppendingEnumerator(
+ nsISimpleEnumerator* aBase)
+ : mBase(aBase) {
+ // Initialize mNext to begin
+ GetNext(nullptr);
+}
diff --git a/comm/mailnews/base/src/nsMailDirProvider.h b/comm/mailnews/base/src/nsMailDirProvider.h
new file mode 100644
index 0000000000..e67c87a9e3
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailDirProvider.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMailDirProvider_h__
+#define nsMailDirProvider_h__
+
+#include "nsIDirectoryService.h"
+#include "nsSimpleEnumerator.h"
+#include "nsString.h"
+#include "nsCOMPtr.h"
+#include "nsIFile.h"
+
+class nsMailDirProvider final : public nsIDirectoryServiceProvider2 {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDIRECTORYSERVICEPROVIDER
+ NS_DECL_NSIDIRECTORYSERVICEPROVIDER2
+
+ private:
+ ~nsMailDirProvider() {}
+
+ nsresult EnsureDirectory(nsIFile* aDirectory);
+
+ class AppendingEnumerator final : public nsSimpleEnumerator {
+ public:
+ const nsID& DefaultInterface() override { return NS_GET_IID(nsIFile); }
+
+ NS_DECL_NSISIMPLEENUMERATOR
+
+ explicit AppendingEnumerator(nsISimpleEnumerator* aBase);
+
+ private:
+ ~AppendingEnumerator() override = default;
+ nsCOMPtr<nsISimpleEnumerator> mBase;
+ nsCOMPtr<nsIFile> mNext;
+ nsCOMPtr<nsIFile> mNextWithLocale;
+ };
+};
+
+#endif // nsMailDirProvider_h__
diff --git a/comm/mailnews/base/src/nsMailDirServiceDefs.h b/comm/mailnews/base/src/nsMailDirServiceDefs.h
new file mode 100644
index 0000000000..3753c96782
--- /dev/null
+++ b/comm/mailnews/base/src/nsMailDirServiceDefs.h
@@ -0,0 +1,31 @@
+/* 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/. */
+
+#ifndef nsMailDirectoryServiceDefs_h___
+#define nsMailDirectoryServiceDefs_h___
+
+//=============================================================================
+//
+// Defines property names for directories available from the mail-specific
+// nsMailDirProvider.
+//
+// System and XPCOM properties are defined in nsDirectoryServiceDefs.h.
+// General application properties are defined in nsAppDirectoryServiceDefs.h.
+//
+//=============================================================================
+
+// ----------------------------------------------------------------------------
+// Files and directories that exist on a per-profile basis.
+// ----------------------------------------------------------------------------
+
+#define NS_APP_MAIL_50_DIR "MailD"
+#define NS_APP_IMAP_MAIL_50_DIR "IMapMD"
+#define NS_APP_NEWS_50_DIR "NewsD"
+
+#define NS_APP_MESSENGER_LEGACY_FOLDER_CACHE_50_FILE "MLFCaF"
+#define NS_APP_MESSENGER_FOLDER_CACHE_50_FILE "MFCaF"
+
+#define ISP_DIRECTORY_LIST "ISPDL"
+
+#endif
diff --git a/comm/mailnews/base/src/nsMessenger.cpp b/comm/mailnews/base/src/nsMessenger.cpp
new file mode 100644
index 0000000000..7fc5c05934
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessenger.cpp
@@ -0,0 +1,2446 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "prsystem.h"
+
+#include "nsMessenger.h"
+
+// xpcom
+#include "nsIComponentManager.h"
+#include "nsIServiceManager.h"
+#include "nsIStringStream.h"
+#include "nsLocalFile.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsQuickSort.h"
+#include "nsNativeCharsetUtils.h"
+#include "mozilla/Path.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/LoadURIOptionsBinding.h"
+
+// necko
+#include "nsMimeTypes.h"
+#include "nsIURL.h"
+#include "nsIPrompt.h"
+#include "nsIStreamListener.h"
+#include "nsIStreamConverterService.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIMIMEInfo.h"
+
+// gecko
+#include "nsLayoutCID.h"
+#include "nsIContentViewer.h"
+
+/* for access to docshell */
+#include "nsPIDOMWindow.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIWebNavigation.h"
+#include "nsContentUtils.h"
+#include "nsDocShellLoadState.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/XULFrameElement.h"
+#include "nsFrameLoader.h"
+#include "mozilla/dom/Document.h"
+
+// mail
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgMailSession.h"
+#include "nsIMailboxUrl.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgIncomingServer.h"
+
+#include "nsIMsgMessageService.h"
+
+#include "nsIMsgHdr.h"
+// compose
+#include "nsNativeCharsetUtils.h"
+
+// draft/folders/sendlater/etc
+#include "nsIMsgCopyService.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIUrlListener.h"
+#include "UrlListener.h"
+
+// undo
+#include "nsITransaction.h"
+#include "nsMsgTxn.h"
+
+// charset conversions
+#include "nsIMimeConverter.h"
+
+// Save As
+#include "nsIStringBundle.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsCExternalHandlerService.h"
+#include "nsIExternalProtocolService.h"
+#include "nsIMIMEService.h"
+#include "nsITransfer.h"
+
+#define MESSENGER_SAVE_DIR_PREF_NAME "messenger.save.dir"
+#define MIMETYPE_DELETED "text/x-moz-deleted"
+#define ATTACHMENT_PERMISSION 00664
+
+//
+// Convert an nsString buffer to plain text...
+//
+#include "nsMsgUtils.h"
+#include "nsCharsetSource.h"
+#include "nsIChannel.h"
+#include "nsIOutputStream.h"
+#include "nsIPrincipal.h"
+
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/dom/RemoteType.h"
+#include "nsQueryObject.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+static void ConvertAndSanitizeFileName(const nsACString& displayName,
+ nsString& aResult) {
+ nsCString unescapedName;
+
+ /* we need to convert the UTF-8 fileName to platform specific character set.
+ The display name is in UTF-8 because it has been escaped from JS
+ */
+ MsgUnescapeString(displayName, 0, unescapedName);
+ CopyUTF8toUTF16(unescapedName, aResult);
+
+ // replace platform specific path separator and illegale characters to avoid
+ // any confusion
+ aResult.ReplaceChar(u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, u'-');
+}
+
+// ***************************************************
+// jefft - this is a rather obscured class serves for Save Message As File,
+// Save Message As Template, and Save Attachment to a file
+// It's used to save out a single item. If multiple items are to be saved,
+// a nsSaveAllAttachmentsState should be set, which holds a list of items.
+//
+class nsSaveAllAttachmentsState;
+
+class nsSaveMsgListener : public nsIUrlListener,
+ public nsIMsgCopyServiceListener,
+ public nsIStreamListener,
+ public nsICancelable {
+ using PathChar = mozilla::filesystem::Path::value_type;
+
+ public:
+ nsSaveMsgListener(nsIFile* file, nsMessenger* aMessenger,
+ nsIUrlListener* aListener);
+
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSICANCELABLE
+
+ nsCOMPtr<nsIFile> m_file;
+ nsCOMPtr<nsIOutputStream> m_outputStream;
+ char m_dataBuffer[FILE_IO_BUFFER_SIZE];
+ nsCOMPtr<nsIChannel> m_channel;
+ nsCString m_templateUri;
+ RefPtr<nsMessenger> m_messenger;
+ nsSaveAllAttachmentsState* m_saveAllAttachmentsState;
+
+ // rhp: For character set handling
+ bool m_doCharsetConversion;
+ nsString m_charset;
+ enum { eUnknown, ePlainText, eHTML } m_outputFormat;
+ nsCString m_msgBuffer;
+
+ nsCString m_contentType; // used only when saving attachment
+
+ nsCOMPtr<nsITransfer> mTransfer;
+ nsCOMPtr<nsIUrlListener> mListener;
+ nsCOMPtr<nsIURI> mListenerUri;
+ int64_t mProgress;
+ int64_t mMaxProgress;
+ bool mCanceled;
+ bool mInitialized;
+ bool mUrlHasStopped;
+ bool mRequestHasStopped;
+ nsresult InitializeDownload(nsIRequest* aRequest);
+
+ private:
+ virtual ~nsSaveMsgListener();
+};
+
+// This helper class holds a list of attachments to be saved and (optionally)
+// detached. It's used by nsSaveMsgListener (which only sticks around for a
+// single item, then passes the nsSaveAllAttachmentsState along to the next
+// SaveAttachment() call).
+class nsSaveAllAttachmentsState {
+ using PathChar = mozilla::filesystem::Path::value_type;
+
+ public:
+ nsSaveAllAttachmentsState(const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray,
+ const PathChar* directoryName,
+ bool detachingAttachments,
+ nsIUrlListener* overallListener);
+ virtual ~nsSaveAllAttachmentsState();
+
+ uint32_t m_count;
+ uint32_t m_curIndex;
+ PathChar* m_directoryName;
+ nsTArray<nsCString> m_contentTypeArray;
+ nsTArray<nsCString> m_urlArray;
+ nsTArray<nsCString> m_displayNameArray;
+ nsTArray<nsCString> m_messageUriArray;
+ bool m_detachingAttachments;
+ // The listener to invoke when all the items have been saved.
+ nsCOMPtr<nsIUrlListener> m_overallListener;
+ // if detaching, do without warning? Will create unique files instead of
+ // prompting if duplicate files exist.
+ bool m_withoutWarning;
+ // if detaching first, remember where we saved to.
+ nsTArray<nsCString> m_savedFiles;
+};
+
+//
+// nsMessenger
+//
+nsMessenger::nsMessenger() {}
+
+nsMessenger::~nsMessenger() {}
+
+NS_IMPL_ISUPPORTS(nsMessenger, nsIMessenger, nsISupportsWeakReference)
+
+NS_IMETHODIMP nsMessenger::SetWindow(mozIDOMWindowProxy* aWin,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aWin) {
+ aMsgWindow->GetTransactionManager(getter_AddRefs(mTxnMgr));
+ mMsgWindow = aMsgWindow;
+ mWindow = aWin;
+
+ NS_ENSURE_TRUE(aWin, NS_ERROR_FAILURE);
+ nsCOMPtr<nsPIDOMWindowOuter> win = nsPIDOMWindowOuter::From(aWin);
+ mDocShell = win->GetDocShell();
+ } else {
+ mWindow = nullptr;
+ mDocShell = nullptr;
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsMessenger::nsFilePickerShownCallback,
+ nsIFilePickerShownCallback)
+nsMessenger::nsFilePickerShownCallback::nsFilePickerShownCallback() {
+ mResult = nsIFilePicker::returnOK;
+ mPickerDone = false;
+}
+
+NS_IMETHODIMP
+nsMessenger::nsFilePickerShownCallback::Done(
+ nsIFilePicker::ResultCode aResult) {
+ mResult = aResult;
+ mPickerDone = true;
+ return NS_OK;
+}
+
+nsresult nsMessenger::ShowPicker(nsIFilePicker* aPicker,
+ nsIFilePicker::ResultCode* aResult) {
+ nsCOMPtr<nsIFilePickerShownCallback> callback =
+ new nsMessenger::nsFilePickerShownCallback();
+ nsFilePickerShownCallback* cb =
+ static_cast<nsFilePickerShownCallback*>(callback.get());
+
+ nsresult rv;
+ rv = aPicker->Open(callback);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Spin the event loop until the callback was called.
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+ while (!cb->mPickerDone) {
+ NS_ProcessNextEvent(thread, true);
+ }
+
+ *aResult = cb->mResult;
+ return NS_OK;
+}
+
+nsresult nsMessenger::PromptIfFileExists(nsIFile* file) {
+ nsresult rv = NS_ERROR_FAILURE;
+ bool exists;
+ file->Exists(&exists);
+ if (!exists) return NS_OK;
+
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(mDocShell));
+ if (!dialog) return rv;
+ nsAutoString path;
+ bool dialogResult = false;
+ nsString errorMessage;
+
+ file->GetPath(path);
+ AutoTArray<nsString, 1> pathFormatStrings = {path};
+
+ if (!mStringBundle) {
+ rv = InitStringBundle();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = mStringBundle->FormatStringFromName("fileExists", pathFormatStrings,
+ errorMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = dialog->Confirm(nullptr, errorMessage.get(), &dialogResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (dialogResult) return NS_OK; // user says okay to replace
+
+ // if we don't re-init the path for redisplay the picker will
+ // show the full path, not just the file name
+ nsCOMPtr<nsIFile> currentFile =
+ do_CreateInstance("@mozilla.org/file/local;1");
+ if (!currentFile) return NS_ERROR_FAILURE;
+
+ rv = currentFile->InitWithPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString leafName;
+ currentFile->GetLeafName(leafName);
+ if (!leafName.IsEmpty())
+ path.Assign(leafName); // path should be a copy of leafName
+
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString saveAttachmentStr;
+ GetString(u"SaveAttachment"_ns, saveAttachmentStr);
+ filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeSave);
+ filePicker->SetDefaultString(path);
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+
+ nsCOMPtr<nsIFile> lastSaveDir;
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir) {
+ filePicker->SetDisplayDirectory(lastSaveDir);
+ }
+
+ nsIFilePicker::ResultCode dialogReturn;
+ rv = ShowPicker(filePicker, &dialogReturn);
+ if (NS_FAILED(rv) || dialogReturn == nsIFilePicker::returnCancel) {
+ // XXX todo
+ // don't overload the return value like this
+ // change this function to have an out boolean
+ // that we check to see if the user cancelled
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIFile> localFile;
+
+ rv = filePicker->GetFile(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLastSaveDirectory(localFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // reset the file to point to the new path
+ return file->InitWithFile(localFile);
+}
+
+NS_IMETHODIMP nsMessenger::SaveAttachmentToFile(nsIFile* aFile,
+ const nsACString& aURL,
+ const nsACString& aMessageUri,
+ const nsACString& aContentType,
+ nsIUrlListener* aListener) {
+ return SaveAttachment(aFile, aURL, aMessageUri, aContentType, nullptr,
+ aListener);
+}
+
+NS_IMETHODIMP
+nsMessenger::DetachAttachmentsWOPrompts(
+ nsIFile* aDestFolder, const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray, nsIUrlListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aDestFolder);
+ MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() &&
+ aUrlArray.Length() == aDisplayNameArray.Length() &&
+ aDisplayNameArray.Length() == aMessageUriArray.Length());
+
+ if (!aContentTypeArray.Length()) return NS_OK;
+ nsCOMPtr<nsIFile> attachmentDestination;
+ nsresult rv = aDestFolder->Clone(getter_AddRefs(attachmentDestination));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PathString path = attachmentDestination->NativePath();
+
+ nsAutoString unescapedFileName;
+ ConvertAndSanitizeFileName(aDisplayNameArray[0], unescapedFileName);
+ rv = attachmentDestination->Append(unescapedFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = attachmentDestination->CreateUnique(nsIFile::NORMAL_FILE_TYPE,
+ ATTACHMENT_PERMISSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Set up to detach the attachments once they've been saved out.
+ // NOTE: nsSaveAllAttachmentsState has a detach option, but I'd like to
+ // phase it out, so we set up a listener to call DetachAttachments()
+ // instead.
+ UrlListener* listener = new UrlListener;
+ nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState(
+ aContentTypeArray, aUrlArray, aDisplayNameArray, aMessageUriArray,
+ path.get(),
+ false, // detach = false
+ listener);
+
+ // Note: saveState is kept in existence by SaveAttachment() until after
+ // the last item is saved.
+ listener->mStopFn = [saveState, self = RefPtr<nsMessenger>(this),
+ originalListener = nsCOMPtr<nsIUrlListener>(aListener)](
+ nsIURI* url, nsresult status) -> nsresult {
+ if (NS_SUCCEEDED(status)) {
+ status = self->DetachAttachments(
+ saveState->m_contentTypeArray, saveState->m_urlArray,
+ saveState->m_displayNameArray, saveState->m_messageUriArray,
+ &saveState->m_savedFiles, originalListener,
+ saveState->m_withoutWarning);
+ }
+ if (NS_FAILED(status) && originalListener) {
+ return originalListener->OnStopRunningUrl(nullptr, status);
+ }
+ return NS_OK;
+ };
+
+ // This method is used in filters, where we don't want to warn
+ saveState->m_withoutWarning = true;
+
+ rv = SaveAttachment(attachmentDestination, aUrlArray[0], aMessageUriArray[0],
+ aContentTypeArray[0], saveState, nullptr);
+ return rv;
+}
+
+// Internal helper for Saving attachments.
+// It handles a single attachment, but multiple attachments can be saved
+// by passing in an nsSaveAllAttachmentsState. In this case, SaveAttachment()
+// will be called for each attachment, and the saveState keeps track of which
+// one we're up to.
+//
+// aListener is invoked to cover this single attachment save.
+// If a saveState is used, it can also contain a nsIUrlListener which
+// will be invoked when _all_ the saves are complete.
+//
+// SaveAttachment() takes ownership of the saveState passed in.
+// If SaveAttachment() fails, then
+// saveState->m_overallListener->OnStopRunningUrl()
+// will be invoked and saveState itself will be deleted.
+//
+// Even though SaveAttachment() takes ownership of saveState,
+// nsSaveMsgListener is responsible for finally deleting it when the
+// last save operation successfully completes.
+//
+// Yes, this is convoluted. Bug 1788159 covers simplifying all this stuff.
+nsresult nsMessenger::SaveAttachment(nsIFile* aFile, const nsACString& aURL,
+ const nsACString& aMessageUri,
+ const nsACString& aContentType,
+ nsSaveAllAttachmentsState* saveState,
+ nsIUrlListener* aListener) {
+ nsCOMPtr<nsIMsgMessageService> messageService;
+ nsCOMPtr<nsIMsgMessageFetchPartService> fetchService;
+ nsAutoCString urlString;
+ nsAutoCString fullMessageUri(aMessageUri);
+
+ nsresult rv = NS_OK;
+
+ // This instance will be held onto by the listeners, and will be released once
+ // the transfer has been completed.
+ RefPtr<nsSaveMsgListener> saveListener(
+ new nsSaveMsgListener(aFile, this, aListener));
+
+ saveListener->m_contentType = aContentType;
+ if (saveState) {
+ if (saveState->m_overallListener && saveState->m_curIndex == 0) {
+ // This is the first item, so tell the caller we're starting.
+ saveState->m_overallListener->OnStartRunningUrl(nullptr);
+ }
+ saveListener->m_saveAllAttachmentsState = saveState;
+ // Record the resultant file:// URL for each saved attachment as we go
+ // along. It'll be used later if we want to also detach them from the email.
+ // Placeholder text will be inserted into the email to replace the
+ // removed attachment pointing at it's final resting place.
+ nsCOMPtr<nsIURI> outputURI;
+ rv = NS_NewFileURI(getter_AddRefs(outputURI), aFile);
+ if (NS_SUCCEEDED(rv)) {
+ nsAutoCString fileUriSpec;
+ rv = outputURI->GetSpec(fileUriSpec);
+ if NS_SUCCEEDED (rv) {
+ saveState->m_savedFiles.AppendElement(fileUriSpec);
+ }
+ }
+ }
+
+ nsCOMPtr<nsIURI> URL;
+ if (NS_SUCCEEDED(rv)) {
+ urlString = aURL;
+ // strip out ?type=application/x-message-display because it confuses libmime
+
+ int32_t typeIndex = urlString.Find("?type=application/x-message-display");
+ if (typeIndex != kNotFound) {
+ urlString.Cut(typeIndex,
+ sizeof("?type=application/x-message-display") - 1);
+ // we also need to replace the next '&' with '?'
+ int32_t firstPartIndex = urlString.FindChar('&');
+ if (firstPartIndex != kNotFound) urlString.SetCharAt('?', firstPartIndex);
+ }
+
+ urlString.ReplaceSubstring("/;section", "?section");
+ rv = NS_NewURI(getter_AddRefs(URL), urlString);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = GetMessageServiceFromURI(aMessageUri, getter_AddRefs(messageService));
+ if (NS_SUCCEEDED(rv)) {
+ fetchService = do_QueryInterface(messageService);
+ // if the message service has a fetch part service then we know we can
+ // fetch mime parts...
+ if (fetchService) {
+ int32_t partPos = urlString.FindChar('?');
+ if (partPos == kNotFound) return NS_ERROR_FAILURE;
+ fullMessageUri.Append(Substring(urlString, partPos));
+ }
+
+ nsCOMPtr<nsIStreamListener> convertedListener;
+ saveListener->QueryInterface(NS_GET_IID(nsIStreamListener),
+ getter_AddRefs(convertedListener));
+
+ nsCOMPtr<nsIURI> dummyNull;
+ if (fetchService)
+ rv = fetchService->FetchMimePart(URL, fullMessageUri, convertedListener,
+ mMsgWindow, saveListener,
+ getter_AddRefs(dummyNull));
+ else
+ rv = messageService->LoadMessage(fullMessageUri, convertedListener,
+ mMsgWindow, nullptr, false);
+ } // if we got a message service
+ } // if we created a url
+
+ if (NS_FAILED(rv)) {
+ if (saveState) {
+ // If we had a listener, make sure it sees the failure!
+ if (saveState->m_overallListener) {
+ saveState->m_overallListener->OnStopRunningUrl(nullptr, rv);
+ }
+ // Ugh. Ownership is all over the place here!
+ // Usually nsSaveMsgListener is responsible for cleaning up
+ // nsSaveAllAttachmentsState... but we're not getting
+ // that far, so have to clean it up here!
+ delete saveState;
+ saveListener->m_saveAllAttachmentsState = nullptr;
+ }
+ Alert("saveAttachmentFailed");
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveAttachmentToFolder(const nsACString& contentType,
+ const nsACString& url,
+ const nsACString& displayName,
+ const nsACString& messageUri,
+ nsIFile* aDestFolder, nsIFile** aOutFile) {
+ NS_ENSURE_ARG_POINTER(aDestFolder);
+ nsresult rv;
+
+ nsCOMPtr<nsIFile> attachmentDestination;
+ rv = aDestFolder->Clone(getter_AddRefs(attachmentDestination));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString unescapedFileName;
+ ConvertAndSanitizeFileName(displayName, unescapedFileName);
+ rv = attachmentDestination->Append(unescapedFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+#ifdef XP_MACOSX
+ rv = attachmentDestination->CreateUnique(nsIFile::NORMAL_FILE_TYPE,
+ ATTACHMENT_PERMISSION);
+ NS_ENSURE_SUCCESS(rv, rv);
+#endif
+
+ rv = SaveAttachment(attachmentDestination, url, messageUri, contentType,
+ nullptr, nullptr);
+ attachmentDestination.forget(aOutFile);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveAttachment(const nsACString& aContentType,
+ const nsACString& aURL,
+ const nsACString& aDisplayName,
+ const nsACString& aMessageUri,
+ bool aIsExternalAttachment) {
+ return SaveOneAttachment(aContentType, aURL, aDisplayName, aMessageUri,
+ false);
+}
+
+nsresult nsMessenger::SaveOneAttachment(const nsACString& aContentType,
+ const nsACString& aURL,
+ const nsACString& aDisplayName,
+ const nsACString& aMessageUri,
+ bool detaching) {
+ nsresult rv = NS_ERROR_OUT_OF_MEMORY;
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIFilePicker::ResultCode dialogResult;
+ nsCOMPtr<nsIFile> localFile;
+ nsCOMPtr<nsIFile> lastSaveDir;
+ nsCString filePath;
+ nsString saveAttachmentStr;
+ nsString defaultDisplayString;
+ ConvertAndSanitizeFileName(aDisplayName, defaultDisplayString);
+
+ if (detaching) {
+ GetString(u"DetachAttachment"_ns, saveAttachmentStr);
+ } else {
+ GetString(u"SaveAttachment"_ns, saveAttachmentStr);
+ }
+ filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeSave);
+ filePicker->SetDefaultString(defaultDisplayString);
+
+ // Check if the attachment file name has an extension (which must not
+ // contain spaces) and set it as the default extension for the attachment.
+ int32_t extensionIndex = defaultDisplayString.RFindChar('.');
+ if (extensionIndex > 0 &&
+ defaultDisplayString.FindChar(' ', extensionIndex) == kNotFound) {
+ nsString extension;
+ extension = Substring(defaultDisplayString, extensionIndex + 1);
+ filePicker->SetDefaultExtension(extension);
+ if (!mStringBundle) {
+ rv = InitStringBundle();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsString filterName;
+ AutoTArray<nsString, 1> extensionParam = {extension};
+ rv = mStringBundle->FormatStringFromName("saveAsType", extensionParam,
+ filterName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ extension.InsertLiteral(u"*.", 0);
+ filePicker->AppendFilter(filterName, extension);
+ }
+
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir)
+ filePicker->SetDisplayDirectory(lastSaveDir);
+
+ rv = ShowPicker(filePicker, &dialogResult);
+ if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) return rv;
+
+ rv = filePicker->GetFile(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SetLastSaveDirectory(localFile);
+
+ PathString dirName = localFile->NativePath();
+
+ AutoTArray<nsCString, 1> contentTypeArray = {
+ PromiseFlatCString(aContentType)};
+ AutoTArray<nsCString, 1> urlArray = {PromiseFlatCString(aURL)};
+ AutoTArray<nsCString, 1> displayNameArray = {
+ PromiseFlatCString(aDisplayName)};
+ AutoTArray<nsCString, 1> messageUriArray = {PromiseFlatCString(aMessageUri)};
+ nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState(
+ contentTypeArray, urlArray, displayNameArray, messageUriArray,
+ dirName.get(), detaching, nullptr);
+
+ // SaveAttachment takes ownership of saveState.
+ return SaveAttachment(localFile, aURL, aMessageUri, aContentType, saveState,
+ nullptr);
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveAllAttachments(const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray) {
+ uint32_t len = contentTypeArray.Length();
+ NS_ENSURE_TRUE(urlArray.Length() == len, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(displayNameArray.Length() == len, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(messageUriArray.Length() == len, NS_ERROR_INVALID_ARG);
+ if (len == 0) {
+ return NS_OK;
+ }
+ return SaveAllAttachments(contentTypeArray, urlArray, displayNameArray,
+ messageUriArray, false);
+}
+
+nsresult nsMessenger::SaveAllAttachments(
+ const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray, bool detaching) {
+ nsresult rv = NS_ERROR_OUT_OF_MEMORY;
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ nsCOMPtr<nsIFile> localFile;
+ nsCOMPtr<nsIFile> lastSaveDir;
+ nsIFilePicker::ResultCode dialogResult;
+ nsString saveAttachmentStr;
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (detaching) {
+ GetString(u"DetachAllAttachments"_ns, saveAttachmentStr);
+ } else {
+ GetString(u"SaveAllAttachments"_ns, saveAttachmentStr);
+ }
+ filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeGetFolder);
+
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir)
+ filePicker->SetDisplayDirectory(lastSaveDir);
+
+ rv = ShowPicker(filePicker, &dialogResult);
+ if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) return rv;
+
+ rv = filePicker->GetFile(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLastSaveDirectory(localFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PathString dirName = localFile->NativePath();
+
+ nsString unescapedName;
+ ConvertAndSanitizeFileName(displayNameArray[0], unescapedName);
+ rv = localFile->Append(unescapedName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = PromptIfFileExists(localFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState(
+ contentTypeArray, urlArray, displayNameArray, messageUriArray,
+ dirName.get(), detaching, nullptr);
+ // SaveAttachment takes ownership of saveState.
+ rv = SaveAttachment(localFile, urlArray[0], messageUriArray[0],
+ contentTypeArray[0], saveState, nullptr);
+ return rv;
+}
+
+enum MESSENGER_SAVEAS_FILE_TYPE {
+ EML_FILE_TYPE = 0,
+ HTML_FILE_TYPE = 1,
+ TEXT_FILE_TYPE = 2,
+ ANY_FILE_TYPE = 3
+};
+#define HTML_FILE_EXTENSION ".htm"
+#define HTML_FILE_EXTENSION2 ".html"
+#define TEXT_FILE_EXTENSION ".txt"
+
+/**
+ * Adjust the file name, removing characters from the middle of the name if
+ * the name would otherwise be too long - too long for what file systems
+ * usually support.
+ */
+nsresult nsMessenger::AdjustFileIfNameTooLong(nsIFile* aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ nsAutoString path;
+ nsresult rv = aFile->GetPath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Most common file systems have a max filename length of 255. On windows, the
+ // total path length is (at least for all practical purposees) limited to 255.
+ // Let's just don't allow paths longer than that elsewhere either for
+ // simplicity.
+ uint32_t MAX = 255;
+ if (path.Length() > MAX) {
+ nsAutoString leafName;
+ rv = aFile->GetLeafName(leafName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t pathLengthUpToLeaf = path.Length() - leafName.Length();
+ if (pathLengthUpToLeaf >= MAX - 8) { // want at least 8 chars for name
+ return NS_ERROR_FILE_NAME_TOO_LONG;
+ }
+ uint32_t x = MAX - pathLengthUpToLeaf; // x = max leaf size
+ nsAutoString truncatedLeaf;
+ truncatedLeaf.Append(Substring(leafName, 0, x / 2));
+ truncatedLeaf.AppendLiteral("...");
+ truncatedLeaf.Append(
+ Substring(leafName, leafName.Length() - x / 2 + 3, leafName.Length()));
+ rv = aFile->SetLeafName(truncatedLeaf);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveAs(const nsACString& aURI, bool aAsFile,
+ nsIMsgIdentity* aIdentity, const nsAString& aMsgFilename,
+ bool aBypassFilePicker) {
+ nsCOMPtr<nsIMsgMessageService> messageService;
+ nsCOMPtr<nsIUrlListener> urlListener;
+ RefPtr<nsSaveMsgListener> saveListener;
+ nsCOMPtr<nsIStreamListener> convertedListener;
+ int32_t saveAsFileType = EML_FILE_TYPE;
+
+ nsresult rv = GetMessageServiceFromURI(aURI, getter_AddRefs(messageService));
+ if (NS_FAILED(rv)) goto done;
+
+ if (aAsFile) {
+ nsCOMPtr<nsIFile> saveAsFile;
+ // show the file picker if BypassFilePicker is not specified (null) or false
+ if (!aBypassFilePicker) {
+ rv = GetSaveAsFile(aMsgFilename, &saveAsFileType,
+ getter_AddRefs(saveAsFile));
+ // A null saveAsFile means that the user canceled the save as
+ if (NS_FAILED(rv) || !saveAsFile) goto done;
+ } else {
+ saveAsFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ rv = saveAsFile->InitWithPath(aMsgFilename);
+ if (NS_FAILED(rv)) goto done;
+ if (StringEndsWith(aMsgFilename,
+ NS_LITERAL_STRING_FROM_CSTRING(TEXT_FILE_EXTENSION),
+ nsCaseInsensitiveStringComparator))
+ saveAsFileType = TEXT_FILE_TYPE;
+ else if ((StringEndsWith(
+ aMsgFilename,
+ NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION),
+ nsCaseInsensitiveStringComparator)) ||
+ (StringEndsWith(
+ aMsgFilename,
+ NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION2),
+ nsCaseInsensitiveStringComparator)))
+ saveAsFileType = HTML_FILE_TYPE;
+ else
+ saveAsFileType = EML_FILE_TYPE;
+ }
+
+ rv = AdjustFileIfNameTooLong(saveAsFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = PromptIfFileExists(saveAsFile);
+ if (NS_FAILED(rv)) {
+ goto done;
+ }
+
+ // After saveListener goes out of scope, the listener will be owned by
+ // whoever the listener is registered with, usually a URL.
+ saveListener = new nsSaveMsgListener(saveAsFile, this, nullptr);
+ rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener),
+ getter_AddRefs(urlListener));
+ if (NS_FAILED(rv)) goto done;
+
+ if (saveAsFileType == EML_FILE_TYPE) {
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = messageService->SaveMessageToDisk(
+ aURI, saveAsFile, false, urlListener, getter_AddRefs(dummyNull), true,
+ mMsgWindow);
+ } else {
+ nsAutoCString urlString(aURI);
+
+ // we can't go RFC822 to TXT until bug #1775 is fixed
+ // so until then, do the HTML to TXT conversion in
+ // nsSaveMsgListener::OnStopRequest(), see ConvertBufToPlainText()
+ //
+ // Setup the URL for a "Save As..." Operation...
+ // For now, if this is a save as TEXT operation, then do
+ // a "printing" operation
+ if (saveAsFileType == TEXT_FILE_TYPE) {
+ saveListener->m_outputFormat = nsSaveMsgListener::ePlainText;
+ saveListener->m_doCharsetConversion = true;
+ urlString.AppendLiteral("?header=print");
+ } else {
+ saveListener->m_outputFormat = nsSaveMsgListener::eHTML;
+ saveListener->m_doCharsetConversion = false;
+ urlString.AppendLiteral("?header=saveas");
+ }
+
+ nsCOMPtr<nsIURI> url;
+ rv = NS_NewURI(getter_AddRefs(url), urlString);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewURI failed");
+ if (NS_FAILED(rv)) goto done;
+
+ nsCOMPtr<nsIPrincipal> nullPrincipal =
+ NullPrincipal::CreateWithoutOriginAttributes();
+
+ saveListener->m_channel = nullptr;
+ rv = NS_NewInputStreamChannel(
+ getter_AddRefs(saveListener->m_channel), url, nullptr, nullPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewChannel failed");
+ if (NS_FAILED(rv)) goto done;
+
+ nsCOMPtr<nsIStreamConverterService> streamConverterService =
+ do_GetService("@mozilla.org/streamConverters;1");
+ nsCOMPtr<nsISupports> channelSupport =
+ do_QueryInterface(saveListener->m_channel);
+
+ // we can't go RFC822 to TXT until bug #1775 is fixed
+ // so until then, do the HTML to TXT conversion in
+ // nsSaveMsgListener::OnStopRequest(), see ConvertBufToPlainText()
+ rv = streamConverterService->AsyncConvertData(
+ MESSAGE_RFC822, TEXT_HTML, saveListener, channelSupport,
+ getter_AddRefs(convertedListener));
+ NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncConvertData failed");
+ if (NS_FAILED(rv)) goto done;
+
+ rv = messageService->LoadMessage(urlString, convertedListener, mMsgWindow,
+ nullptr, false);
+ }
+ } else {
+ // ** save as Template
+ nsCOMPtr<nsIFile> tmpFile;
+ nsresult rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "nsmail.tmp",
+ getter_AddRefs(tmpFile));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // For temp file, we should use restrictive 00600 instead of
+ // ATTACHMENT_PERMISSION
+ rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ if (NS_FAILED(rv)) goto done;
+
+ // The saveListener is owned by whoever we ultimately register the
+ // listener with, generally a URL.
+ saveListener = new nsSaveMsgListener(tmpFile, this, nullptr);
+
+ if (aIdentity)
+ rv = aIdentity->GetStationeryFolder(saveListener->m_templateUri);
+ if (NS_FAILED(rv)) goto done;
+
+ bool needDummyHeader =
+ StringBeginsWith(saveListener->m_templateUri, "mailbox://"_ns);
+ bool canonicalLineEnding =
+ StringBeginsWith(saveListener->m_templateUri, "imap://"_ns);
+
+ rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener),
+ getter_AddRefs(urlListener));
+ if (NS_FAILED(rv)) goto done;
+
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = messageService->SaveMessageToDisk(
+ aURI, tmpFile, needDummyHeader, urlListener, getter_AddRefs(dummyNull),
+ canonicalLineEnding, mMsgWindow);
+ }
+
+done:
+ if (NS_FAILED(rv)) {
+ Alert("saveMessageFailed");
+ }
+ return rv;
+}
+
+nsresult nsMessenger::GetSaveAsFile(const nsAString& aMsgFilename,
+ int32_t* aSaveAsFileType,
+ nsIFile** aSaveAsFile) {
+ nsresult rv;
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString saveMailAsStr;
+ GetString(u"SaveMailAs"_ns, saveMailAsStr);
+ filePicker->Init(mWindow, saveMailAsStr, nsIFilePicker::modeSave);
+
+ // if we have a non-null filename use it, otherwise use default save message
+ // one
+ if (aMsgFilename.IsEmpty()) {
+ nsString saveMsgStr;
+ GetString(u"defaultSaveMessageAsFileName"_ns, saveMsgStr);
+ filePicker->SetDefaultString(saveMsgStr);
+ } else {
+ filePicker->SetDefaultString(aMsgFilename);
+ }
+
+ // because we will be using GetFilterIndex()
+ // we must call AppendFilters() one at a time,
+ // in MESSENGER_SAVEAS_FILE_TYPE order
+ nsString emlFilesStr;
+ GetString(u"EMLFiles"_ns, emlFilesStr);
+ filePicker->AppendFilter(emlFilesStr, u"*.eml"_ns);
+ filePicker->AppendFilters(nsIFilePicker::filterHTML);
+ filePicker->AppendFilters(nsIFilePicker::filterText);
+ filePicker->AppendFilters(nsIFilePicker::filterAll);
+
+ // Save as the "All Files" file type by default. We want to save as .eml by
+ // default, but the filepickers on some platforms don't switch extensions
+ // based on the file type selected (bug 508597).
+ filePicker->SetFilterIndex(ANY_FILE_TYPE);
+ // Yes, this is fine even if we ultimately save as HTML or text. On Windows,
+ // this actually is a boolean telling the file picker to automatically add
+ // the correct extension depending on the filter. On Mac or Linux this is a
+ // no-op.
+ filePicker->SetDefaultExtension(u"eml"_ns);
+
+ nsIFilePicker::ResultCode dialogResult;
+
+ nsCOMPtr<nsIFile> lastSaveDir;
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir)
+ filePicker->SetDisplayDirectory(lastSaveDir);
+
+ nsCOMPtr<nsIFile> localFile;
+ rv = ShowPicker(filePicker, &dialogResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (dialogResult == nsIFilePicker::returnCancel) {
+ // We'll indicate this by setting the outparam to null.
+ *aSaveAsFile = nullptr;
+ return NS_OK;
+ }
+
+ rv = filePicker->GetFile(getter_AddRefs(localFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLastSaveDirectory(localFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t selectedSaveAsFileType;
+ rv = filePicker->GetFilterIndex(&selectedSaveAsFileType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If All Files was selected, look at the extension
+ if (selectedSaveAsFileType == ANY_FILE_TYPE) {
+ nsAutoString fileName;
+ rv = localFile->GetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (StringEndsWith(fileName,
+ NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION),
+ nsCaseInsensitiveStringComparator) ||
+ StringEndsWith(fileName,
+ NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION2),
+ nsCaseInsensitiveStringComparator))
+ *aSaveAsFileType = HTML_FILE_TYPE;
+ else if (StringEndsWith(fileName,
+ NS_LITERAL_STRING_FROM_CSTRING(TEXT_FILE_EXTENSION),
+ nsCaseInsensitiveStringComparator))
+ *aSaveAsFileType = TEXT_FILE_TYPE;
+ else
+ // The default is .eml
+ *aSaveAsFileType = EML_FILE_TYPE;
+ } else {
+ *aSaveAsFileType = selectedSaveAsFileType;
+ }
+
+ if (dialogResult == nsIFilePicker::returnReplace) {
+ // be extra safe and only delete when the file is really a file
+ bool isFile;
+ rv = localFile->IsFile(&isFile);
+ if (NS_SUCCEEDED(rv) && isFile) {
+ rv = localFile->Remove(false /* recursive delete */);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // We failed, or this isn't a file. We can't do anything about it.
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ *aSaveAsFile = nullptr;
+ localFile.forget(aSaveAsFile);
+ return NS_OK;
+}
+
+/**
+ * Show a Save All dialog allowing the user to pick which folder to save
+ * messages to.
+ * @param [out] aSaveDir directory to save to. Will be null on cancel.
+ */
+nsresult nsMessenger::GetSaveToDir(nsIFile** aSaveDir) {
+ nsresult rv;
+ nsCOMPtr<nsIFilePicker> filePicker =
+ do_CreateInstance("@mozilla.org/filepicker;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString chooseFolderStr;
+ GetString(u"ChooseFolder"_ns, chooseFolderStr);
+ filePicker->Init(mWindow, chooseFolderStr, nsIFilePicker::modeGetFolder);
+
+ nsCOMPtr<nsIFile> lastSaveDir;
+ rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir));
+ if (NS_SUCCEEDED(rv) && lastSaveDir)
+ filePicker->SetDisplayDirectory(lastSaveDir);
+
+ nsIFilePicker::ResultCode dialogResult;
+ rv = ShowPicker(filePicker, &dialogResult);
+ if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) {
+ // We'll indicate this by setting the outparam to null.
+ *aSaveDir = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> dir;
+ rv = filePicker->GetFile(getter_AddRefs(dir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLastSaveDirectory(dir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aSaveDir = nullptr;
+ dir.forget(aSaveDir);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessenger::SaveMessages(const nsTArray<nsString>& aFilenameArray,
+ const nsTArray<nsCString>& aMessageUriArray) {
+ MOZ_ASSERT(aFilenameArray.Length() == aMessageUriArray.Length());
+
+ nsresult rv;
+
+ nsCOMPtr<nsIFile> saveDir;
+ rv = GetSaveToDir(getter_AddRefs(saveDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!saveDir) // A null saveDir means that the user canceled the save.
+ return NS_OK;
+
+ for (uint32_t i = 0; i < aFilenameArray.Length(); i++) {
+ nsCOMPtr<nsIFile> saveToFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = saveToFile->InitWithFile(saveDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = saveToFile->Append(aFilenameArray[i]);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AdjustFileIfNameTooLong(saveToFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = PromptIfFileExists(saveToFile);
+ if (NS_FAILED(rv)) continue;
+
+ nsCOMPtr<nsIMsgMessageService> messageService;
+ nsCOMPtr<nsIUrlListener> urlListener;
+
+ rv = GetMessageServiceFromURI(aMessageUriArray[i],
+ getter_AddRefs(messageService));
+ if (NS_FAILED(rv)) {
+ Alert("saveMessageFailed");
+ return rv;
+ }
+
+ RefPtr<nsSaveMsgListener> saveListener =
+ new nsSaveMsgListener(saveToFile, this, nullptr);
+
+ rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener),
+ getter_AddRefs(urlListener));
+ if (NS_FAILED(rv)) {
+ Alert("saveMessageFailed");
+ return rv;
+ }
+
+ // Ok, now save the message.
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = messageService->SaveMessageToDisk(
+ aMessageUriArray[i], saveToFile, false, urlListener,
+ getter_AddRefs(dummyNull), true, mMsgWindow);
+ if (NS_FAILED(rv)) {
+ Alert("saveMessageFailed");
+ return rv;
+ }
+ }
+ return rv;
+}
+
+nsresult nsMessenger::Alert(const char* stringName) {
+ nsresult rv = NS_OK;
+
+ if (mDocShell) {
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(mDocShell));
+
+ if (dialog) {
+ nsString alertStr;
+ GetString(NS_ConvertASCIItoUTF16(stringName), alertStr);
+ rv = dialog->Alert(nullptr, alertStr.get());
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::MsgHdrFromURI(const nsACString& aUri, nsIMsgDBHdr** aMsgHdr) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ nsresult rv;
+
+ rv = GetMessageServiceFromURI(aUri, getter_AddRefs(msgService));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgService->MessageURIToMsgHdr(aUri, aMsgHdr);
+}
+
+NS_IMETHODIMP nsMessenger::GetUndoTransactionType(uint32_t* txnType) {
+ NS_ENSURE_TRUE(txnType && mTxnMgr, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ *txnType = nsMessenger::eUnknown;
+ nsCOMPtr<nsITransaction> txn;
+ rv = mTxnMgr->PeekUndoStack(getter_AddRefs(txn));
+ if (NS_SUCCEEDED(rv) && txn) {
+ nsCOMPtr<nsIPropertyBag2> propertyBag = do_QueryInterface(txn, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return propertyBag->GetPropertyAsUint32(u"type"_ns, txnType);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMessenger::CanUndo(bool* bValue) {
+ NS_ENSURE_TRUE(bValue && mTxnMgr, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ *bValue = false;
+ int32_t count = 0;
+ rv = mTxnMgr->GetNumberOfUndoItems(&count);
+ if (NS_SUCCEEDED(rv) && count > 0) *bValue = true;
+ return rv;
+}
+
+NS_IMETHODIMP nsMessenger::GetRedoTransactionType(uint32_t* txnType) {
+ NS_ENSURE_TRUE(txnType && mTxnMgr, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ *txnType = nsMessenger::eUnknown;
+ nsCOMPtr<nsITransaction> txn;
+ rv = mTxnMgr->PeekRedoStack(getter_AddRefs(txn));
+ if (NS_SUCCEEDED(rv) && txn) {
+ nsCOMPtr<nsIPropertyBag2> propertyBag = do_QueryInterface(txn, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return propertyBag->GetPropertyAsUint32(u"type"_ns, txnType);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMessenger::CanRedo(bool* bValue) {
+ NS_ENSURE_TRUE(bValue && mTxnMgr, NS_ERROR_NULL_POINTER);
+
+ nsresult rv;
+ *bValue = false;
+ int32_t count = 0;
+ rv = mTxnMgr->GetNumberOfRedoItems(&count);
+ if (NS_SUCCEEDED(rv) && count > 0) *bValue = true;
+ return rv;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+nsMessenger::Undo(nsIMsgWindow* msgWindow) {
+ nsresult rv = NS_OK;
+ if (mTxnMgr) {
+ int32_t numTxn = 0;
+ rv = mTxnMgr->GetNumberOfUndoItems(&numTxn);
+ if (NS_SUCCEEDED(rv) && numTxn > 0) {
+ nsCOMPtr<nsITransaction> txn;
+ rv = mTxnMgr->PeekUndoStack(getter_AddRefs(txn));
+ if (NS_SUCCEEDED(rv) && txn) {
+ static_cast<nsMsgTxn*>(static_cast<nsITransaction*>(txn.get()))
+ ->SetMsgWindow(msgWindow);
+ }
+ nsCOMPtr<nsITransactionManager> txnMgr = mTxnMgr;
+ txnMgr->UndoTransaction();
+ }
+ }
+ return rv;
+}
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
+nsMessenger::Redo(nsIMsgWindow* msgWindow) {
+ nsresult rv = NS_OK;
+ if (mTxnMgr) {
+ int32_t numTxn = 0;
+ rv = mTxnMgr->GetNumberOfRedoItems(&numTxn);
+ if (NS_SUCCEEDED(rv) && numTxn > 0) {
+ nsCOMPtr<nsITransaction> txn;
+ rv = mTxnMgr->PeekRedoStack(getter_AddRefs(txn));
+ if (NS_SUCCEEDED(rv) && txn) {
+ static_cast<nsMsgTxn*>(static_cast<nsITransaction*>(txn.get()))
+ ->SetMsgWindow(msgWindow);
+ }
+ nsCOMPtr<nsITransactionManager> txnMgr = mTxnMgr;
+ txnMgr->RedoTransaction();
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessenger::GetTransactionManager(nsITransactionManager** aTxnMgr) {
+ NS_ENSURE_TRUE(mTxnMgr && aTxnMgr, NS_ERROR_NULL_POINTER);
+ NS_ADDREF(*aTxnMgr = mTxnMgr);
+ return NS_OK;
+}
+
+nsSaveMsgListener::nsSaveMsgListener(nsIFile* aFile, nsMessenger* aMessenger,
+ nsIUrlListener* aListener) {
+ m_file = aFile;
+ m_messenger = aMessenger;
+ mListener = aListener;
+ mUrlHasStopped = false;
+ mRequestHasStopped = false;
+
+ // rhp: for charset handling
+ m_doCharsetConversion = false;
+ m_saveAllAttachmentsState = nullptr;
+ mProgress = 0;
+ mMaxProgress = -1;
+ mCanceled = false;
+ m_outputFormat = eUnknown;
+ mInitialized = false;
+}
+
+nsSaveMsgListener::~nsSaveMsgListener() {}
+
+//
+// nsISupports
+//
+NS_IMPL_ISUPPORTS(nsSaveMsgListener, nsIUrlListener, nsIMsgCopyServiceListener,
+ nsIStreamListener, nsIRequestObserver, nsICancelable)
+
+NS_IMETHODIMP
+nsSaveMsgListener::Cancel(nsresult status) {
+ mCanceled = true;
+ return NS_OK;
+}
+
+//
+// nsIUrlListener
+//
+NS_IMETHODIMP
+nsSaveMsgListener::OnStartRunningUrl(nsIURI* url) {
+ if (mListener) mListener->OnStartRunningUrl(url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
+ nsresult rv = exitCode;
+ mUrlHasStopped = true;
+
+ // ** save as template goes here
+ if (!m_templateUri.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> templateFolder;
+ rv = GetOrCreateFolder(m_templateUri, getter_AddRefs(templateFolder));
+ if (NS_FAILED(rv)) goto done;
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1");
+ if (copyService) {
+ nsCOMPtr<nsIFile> clone;
+ m_file->Clone(getter_AddRefs(clone));
+ rv = copyService->CopyFileMessage(clone, templateFolder, nullptr, true,
+ nsMsgMessageFlags::Read, EmptyCString(),
+ this, nullptr);
+ // Clear this so we don't end up in a loop if OnStopRunningUrl gets
+ // called again.
+ m_templateUri.Truncate();
+ }
+ } else if (m_outputStream && mRequestHasStopped) {
+ m_outputStream->Close();
+ m_outputStream = nullptr;
+ }
+
+done:
+ if (NS_FAILED(rv)) {
+ if (m_file) m_file->Remove(false);
+ if (m_messenger) m_messenger->Alert("saveMessageFailed");
+ }
+
+ if (mRequestHasStopped && mListener)
+ mListener->OnStopRunningUrl(url, exitCode);
+ else
+ mListenerUri = url;
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStartCopy(void) { return NS_OK; }
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnProgress(uint32_t aProgress, uint32_t aProgressMax) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::SetMessageKey(nsMsgKey aKey) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::GetMessageId(nsACString& aMessageId) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStopCopy(nsresult aStatus) {
+ if (m_file) m_file->Remove(false);
+ return aStatus;
+}
+
+// initializes the progress window if we are going to show one
+// and for OSX, sets creator flags on the output file
+nsresult nsSaveMsgListener::InitializeDownload(nsIRequest* aRequest) {
+ nsresult rv = NS_OK;
+
+ mInitialized = true;
+ nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest));
+
+ if (!channel) return rv;
+
+ // Get the max progress from the URL if we haven't already got it.
+ if (mMaxProgress == -1) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(uri));
+ if (mailnewsUrl) mailnewsUrl->GetMaxProgress(&mMaxProgress);
+ }
+
+ if (!m_contentType.IsEmpty()) {
+ nsCOMPtr<nsIMIMEService> mimeService(
+ do_GetService(NS_MIMESERVICE_CONTRACTID));
+ nsCOMPtr<nsIMIMEInfo> mimeinfo;
+
+ mimeService->GetFromTypeAndExtension(m_contentType, EmptyCString(),
+ getter_AddRefs(mimeinfo));
+
+ // create a download progress window
+
+ // Set saveToDisk explicitly to avoid launching the saved file.
+ // See
+ // https://hg.mozilla.org/mozilla-central/file/814a6f071472/toolkit/components/jsdownloads/src/DownloadLegacy.js#l164
+ mimeinfo->SetPreferredAction(nsIHandlerInfo::saveToDisk);
+
+ // When we don't allow warnings, also don't show progress, as this
+ // is an environment (typically filters) where we don't want
+ // interruption.
+ bool allowProgress = true;
+ if (m_saveAllAttachmentsState)
+ allowProgress = !m_saveAllAttachmentsState->m_withoutWarning;
+ if (allowProgress) {
+ nsCOMPtr<nsITransfer> tr = do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv);
+ if (tr && m_file) {
+ PRTime timeDownloadStarted = PR_Now();
+
+ nsCOMPtr<nsIURI> outputURI;
+ NS_NewFileURI(getter_AddRefs(outputURI), m_file);
+
+ nsCOMPtr<nsIURI> url;
+ channel->GetURI(getter_AddRefs(url));
+ rv = tr->Init(url, nullptr, outputURI, EmptyString(), mimeinfo,
+ timeDownloadStarted, nullptr, this, false,
+ nsITransfer::DOWNLOAD_ACCEPTABLE, nullptr, false);
+
+ // now store the web progresslistener
+ mTransfer = tr;
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStartRequest(nsIRequest* request) {
+ if (m_file)
+ MsgNewBufferedFileOutputStream(getter_AddRefs(m_outputStream), m_file, -1,
+ ATTACHMENT_PERMISSION);
+ if (!m_outputStream) {
+ mCanceled = true;
+ if (m_messenger) m_messenger->Alert("saveAttachmentFailed");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnStopRequest(nsIRequest* request, nsresult status) {
+ nsresult rv = NS_OK;
+ mRequestHasStopped = true;
+
+ // rhp: If we are doing the charset conversion magic, this is different
+ // processing, otherwise, its just business as usual.
+ // If we need text/plain, then we need to convert the HTML and then convert
+ // to the systems charset.
+ if (m_doCharsetConversion && m_outputStream) {
+ // For HTML, code is emitted immediately in OnDataAvailable.
+ MOZ_ASSERT(m_outputFormat == ePlainText,
+ "For HTML, m_doCharsetConversion shouldn't be set");
+ NS_ConvertUTF8toUTF16 utf16Buffer(m_msgBuffer);
+ ConvertBufToPlainText(utf16Buffer, false, false, false);
+
+ nsCString outCString;
+ // NS_CopyUnicodeToNative() doesn't return an error, so we have no choice
+ // but to always use UTF-8.
+ CopyUTF16toUTF8(utf16Buffer, outCString);
+ uint32_t writeCount;
+ rv = m_outputStream->Write(outCString.get(), outCString.Length(),
+ &writeCount);
+ if (outCString.Length() != writeCount) rv = NS_ERROR_FAILURE;
+ }
+
+ if (m_outputStream) {
+ m_outputStream->Close();
+ m_outputStream = nullptr;
+ }
+
+ // Are there more attachments to deal with?
+ nsSaveAllAttachmentsState* state = m_saveAllAttachmentsState;
+ if (state) {
+ state->m_curIndex++;
+ if (!mCanceled && state->m_curIndex < state->m_count) {
+ // Yes, start on the next attachment.
+ uint32_t i = state->m_curIndex;
+ nsString unescapedName;
+ RefPtr<nsLocalFile> localFile =
+ new nsLocalFile(nsTDependentString<PathChar>(state->m_directoryName));
+ if (localFile->NativePath().IsEmpty()) {
+ rv = NS_ERROR_FAILURE;
+ goto done;
+ }
+
+ ConvertAndSanitizeFileName(state->m_displayNameArray[i], unescapedName);
+ rv = localFile->Append(unescapedName);
+ if (NS_FAILED(rv)) goto done;
+
+ // When we are running with no warnings (typically filters and other
+ // automatic uses), then don't prompt for duplicates, but create a unique
+ // file instead.
+ if (!state->m_withoutWarning) {
+ rv = m_messenger->PromptIfFileExists(localFile);
+ if (NS_FAILED(rv)) goto done;
+ } else {
+ rv = localFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE,
+ ATTACHMENT_PERMISSION);
+ if (NS_FAILED(rv)) goto done;
+ }
+ // Start the next attachment saving.
+ // NOTE: null listener passed in on subsequent saves! The original
+ // listener will already have been invoked.
+ // See Bug 1789565
+ rv = m_messenger->SaveAttachment(
+ localFile, state->m_urlArray[i], state->m_messageUriArray[i],
+ state->m_contentTypeArray[i], state, nullptr);
+ if (NS_FAILED(rv)) {
+ // If SaveAttachment() fails, state will have been deleted, and
+ // m_overallListener->OnStopRunningUrl() will have been called.
+ state = nullptr;
+ m_saveAllAttachmentsState = nullptr;
+ }
+ done:
+ if (NS_FAILED(rv) && state) {
+ if (state->m_overallListener) {
+ state->m_overallListener->OnStopRunningUrl(nullptr, rv);
+ }
+ delete state;
+ m_saveAllAttachmentsState = nullptr;
+ }
+ } else {
+ // All attachments have been saved.
+ if (state->m_overallListener) {
+ state->m_overallListener->OnStopRunningUrl(
+ nullptr, mCanceled ? NS_ERROR_FAILURE : NS_OK);
+ }
+ // Check if we're supposed to be detaching attachments after saving them.
+ if (state->m_detachingAttachments && !mCanceled) {
+ m_messenger->DetachAttachments(
+ state->m_contentTypeArray, state->m_urlArray,
+ state->m_displayNameArray, state->m_messageUriArray,
+ &state->m_savedFiles, nullptr, state->m_withoutWarning);
+ }
+ delete m_saveAllAttachmentsState;
+ m_saveAllAttachmentsState = nullptr;
+ }
+ }
+
+ if (mTransfer) {
+ mTransfer->OnProgressChange64(nullptr, nullptr, mMaxProgress, mMaxProgress,
+ mMaxProgress, mMaxProgress);
+ mTransfer->OnStateChange(nullptr, nullptr,
+ nsIWebProgressListener::STATE_STOP |
+ nsIWebProgressListener::STATE_IS_NETWORK,
+ NS_OK);
+ mTransfer = nullptr; // break any circular dependencies between the
+ // progress dialog and use
+ }
+
+ if (mUrlHasStopped && mListener)
+ mListener->OnStopRunningUrl(mListenerUri, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSaveMsgListener::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStream, uint64_t srcOffset,
+ uint32_t count) {
+ nsresult rv = NS_ERROR_FAILURE;
+ // first, check to see if we've been canceled....
+ if (mCanceled) // then go cancel our underlying channel too
+ return request->Cancel(NS_BINDING_ABORTED);
+
+ if (!mInitialized) InitializeDownload(request);
+
+ if (m_outputStream) {
+ mProgress += count;
+ uint64_t available;
+ uint32_t readCount, maxReadCount = sizeof(m_dataBuffer);
+ uint32_t writeCount;
+ rv = inStream->Available(&available);
+ while (NS_SUCCEEDED(rv) && available) {
+ if (maxReadCount > available) maxReadCount = (uint32_t)available;
+ rv = inStream->Read(m_dataBuffer, maxReadCount, &readCount);
+
+ // rhp:
+ // Ok, now we do one of two things. If we are sending out HTML, then
+ // just write it to the HTML stream as it comes along...but if this is
+ // a save as TEXT operation, we need to buffer this up for conversion
+ // when we are done. When the stream converter for HTML-TEXT gets in
+ // place, this magic can go away.
+ //
+ if (NS_SUCCEEDED(rv)) {
+ if ((m_doCharsetConversion) && (m_outputFormat == ePlainText))
+ m_msgBuffer.Append(Substring(m_dataBuffer, m_dataBuffer + readCount));
+ else
+ rv = m_outputStream->Write(m_dataBuffer, readCount, &writeCount);
+
+ available -= readCount;
+ }
+ }
+
+ if (NS_SUCCEEDED(rv) && mTransfer) // Send progress notification.
+ mTransfer->OnProgressChange64(nullptr, request, mProgress, mMaxProgress,
+ mProgress, mMaxProgress);
+ }
+ return rv;
+}
+
+#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties"
+
+nsresult nsMessenger::InitStringBundle() {
+ if (mStringBundle) return NS_OK;
+
+ const char propertyURL[] = MESSENGER_STRING_URL;
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED);
+ return sBundleService->CreateBundle(propertyURL,
+ getter_AddRefs(mStringBundle));
+}
+
+void nsMessenger::GetString(const nsString& aStringName, nsString& aValue) {
+ nsresult rv;
+ aValue.Truncate();
+
+ if (!mStringBundle) InitStringBundle();
+
+ if (mStringBundle)
+ rv = mStringBundle->GetStringFromName(
+ NS_ConvertUTF16toUTF8(aStringName).get(), aValue);
+ else
+ rv = NS_ERROR_FAILURE;
+
+ if (NS_FAILED(rv) || aValue.IsEmpty()) aValue = aStringName;
+ return;
+}
+
+nsSaveAllAttachmentsState::nsSaveAllAttachmentsState(
+ const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray, const PathChar* dirName,
+ bool detachingAttachments, nsIUrlListener* overallListener)
+ : m_contentTypeArray(contentTypeArray.Clone()),
+ m_urlArray(urlArray.Clone()),
+ m_displayNameArray(displayNameArray.Clone()),
+ m_messageUriArray(messageUriArray.Clone()),
+ m_detachingAttachments(detachingAttachments),
+ m_overallListener(overallListener),
+ m_withoutWarning(false) {
+ m_count = contentTypeArray.Length();
+ m_curIndex = 0;
+ m_directoryName = NS_xstrdup(dirName);
+}
+
+nsSaveAllAttachmentsState::~nsSaveAllAttachmentsState() {
+ free(m_directoryName);
+}
+
+nsresult nsMessenger::GetLastSaveDirectory(nsIFile** aLastSaveDir) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this can fail, and it will, on the first time we call it, as there is no
+ // default for this pref.
+ nsCOMPtr<nsIFile> localFile;
+ rv = prefBranch->GetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME,
+ NS_GET_IID(nsIFile),
+ getter_AddRefs(localFile));
+ if (NS_SUCCEEDED(rv)) localFile.forget(aLastSaveDir);
+ return rv;
+}
+
+nsresult nsMessenger::SetLastSaveDirectory(nsIFile* aLocalFile) {
+ NS_ENSURE_ARG_POINTER(aLocalFile);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if the file is a directory, just use it for the last dir chosen
+ // otherwise, use the parent of the file as the last dir chosen.
+ // IsDirectory() will return error on saving a file, as the
+ // file doesn't exist yet.
+ bool isDirectory;
+ rv = aLocalFile->IsDirectory(&isDirectory);
+ if (NS_SUCCEEDED(rv) && isDirectory) {
+ rv = prefBranch->SetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME,
+ NS_GET_IID(nsIFile), aLocalFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ nsCOMPtr<nsIFile> parent;
+ rv = aLocalFile->GetParent(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = prefBranch->SetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME,
+ NS_GET_IID(nsIFile), parent);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessenger::FormatFileSize(uint64_t aSize, bool aUseKB,
+ nsAString& aFormattedSize) {
+ return ::FormatFileSize(aSize, aUseKB, aFormattedSize);
+}
+
+///////////////////////////////////////////////////////////////////////////////
+// Detach/Delete Attachments
+///////////////////////////////////////////////////////////////////////////////
+
+static const char* GetAttachmentPartId(const char* aAttachmentUrl) {
+ static const char partIdPrefix[] = "part=";
+ const char* partId = PL_strstr(aAttachmentUrl, partIdPrefix);
+ return partId ? (partId + sizeof(partIdPrefix) - 1) : nullptr;
+}
+
+static int CompareAttachmentPartId(const char* aAttachUrlLeft,
+ const char* aAttachUrlRight) {
+ // part ids are numbers separated by periods, like "1.2.3.4".
+ // we sort by doing a numerical comparison on each item in turn. e.g. "1.4" <
+ // "1.25" shorter entries come before longer entries. e.g. "1.4" < "1.4.1.2"
+ // return values:
+ // -2 left is a parent of right
+ // -1 left is less than right
+ // 0 left == right
+ // 1 right is greater than left
+ // 2 right is a parent of left
+
+ const char* partIdLeft = GetAttachmentPartId(aAttachUrlLeft);
+ const char* partIdRight = GetAttachmentPartId(aAttachUrlRight);
+
+ // for detached attachments the URL does not contain any "part=xx"
+ if (!partIdLeft) partIdLeft = "0";
+
+ if (!partIdRight) partIdRight = "0";
+
+ long idLeft, idRight;
+ do {
+ MOZ_ASSERT(partIdLeft && IS_DIGIT(*partIdLeft),
+ "Invalid character in part id string");
+ MOZ_ASSERT(partIdRight && IS_DIGIT(*partIdRight),
+ "Invalid character in part id string");
+
+ // if the part numbers are different then the numerically smaller one is
+ // first
+ char* fixConstLoss;
+ idLeft = strtol(partIdLeft, &fixConstLoss, 10);
+ partIdLeft = fixConstLoss;
+ idRight = strtol(partIdRight, &fixConstLoss, 10);
+ partIdRight = fixConstLoss;
+ if (idLeft != idRight) return idLeft < idRight ? -1 : 1;
+
+ // if one part id is complete but the other isn't, then the shortest one
+ // is first (parents before children)
+ if (*partIdLeft != *partIdRight) return *partIdRight ? -2 : 2;
+
+ // if both part ids are complete (*partIdLeft == *partIdRight now) then
+ // they are equal
+ if (!*partIdLeft) return 0;
+
+ MOZ_ASSERT(*partIdLeft == '.', "Invalid character in part id string");
+ MOZ_ASSERT(*partIdRight == '.', "Invalid character in part id string");
+
+ ++partIdLeft;
+ ++partIdRight;
+ } while (true);
+}
+
+// ------------------------------------
+
+// struct on purpose -> show that we don't ever want a vtable
+struct msgAttachment {
+ msgAttachment(const nsACString& aContentType, const nsACString& aUrl,
+ const nsACString& aDisplayName, const nsACString& aMessageUri)
+ : mContentType(aContentType),
+ mUrl(aUrl),
+ mDisplayName(aDisplayName),
+ mMessageUri(aMessageUri) {}
+
+ nsCString mContentType;
+ nsCString mUrl;
+ nsCString mDisplayName;
+ nsCString mMessageUri;
+};
+
+// ------------------------------------
+
+class nsAttachmentState {
+ public:
+ nsAttachmentState();
+ nsresult Init(const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray);
+ nsresult PrepareForAttachmentDelete();
+
+ private:
+ static int CompareAttachmentsByPartId(const void* aLeft, const void* aRight);
+
+ public:
+ uint32_t mCurIndex;
+ nsTArray<msgAttachment> mAttachmentArray;
+};
+
+nsAttachmentState::nsAttachmentState() : mCurIndex(0) {}
+
+nsresult nsAttachmentState::Init(const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray) {
+ MOZ_ASSERT(aContentTypeArray.Length() > 0);
+ MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() &&
+ aUrlArray.Length() == aDisplayNameArray.Length() &&
+ aDisplayNameArray.Length() == aMessageUriArray.Length());
+
+ uint32_t count = aContentTypeArray.Length();
+ mCurIndex = 0;
+ mAttachmentArray.Clear();
+ mAttachmentArray.SetCapacity(count);
+
+ for (uint32_t u = 0; u < count; ++u) {
+ mAttachmentArray.AppendElement(
+ msgAttachment(aContentTypeArray[u], aUrlArray[u], aDisplayNameArray[u],
+ aMessageUriArray[u]));
+ }
+
+ return NS_OK;
+}
+
+nsresult nsAttachmentState::PrepareForAttachmentDelete() {
+ // this must be called before any processing
+ if (mCurIndex != 0) return NS_ERROR_FAILURE;
+
+ // this prepares the attachment list for use in deletion. In order to prepare,
+ // we sort the attachments in numerical ascending order on their part id,
+ // remove all duplicates and remove any subparts which will be removed
+ // automatically by the removal of the parent.
+ //
+ // e.g. the attachment list processing (showing only part ids)
+ // before: 1.11, 1.3, 1.2, 1.2.1.3, 1.4.1.2
+ // sorted: 1.2, 1.2.1.3, 1.3, 1.4.1.2, 1.11
+ // after: 1.2, 1.3, 1.4.1.2, 1.11
+
+ // sort
+ qsort(mAttachmentArray.Elements(), mAttachmentArray.Length(),
+ sizeof(msgAttachment), CompareAttachmentsByPartId);
+
+ // remove duplicates and sub-items
+ int nCompare;
+ for (uint32_t u = 1; u < mAttachmentArray.Length();) {
+ nCompare = ::CompareAttachmentPartId(mAttachmentArray[u - 1].mUrl.get(),
+ mAttachmentArray[u].mUrl.get());
+ if (nCompare == 0 ||
+ nCompare == -2) // [u-1] is the same as or a parent of [u]
+ {
+ // shuffle the array down (and thus keeping the sorted order)
+ mAttachmentArray.RemoveElementAt(u);
+ } else {
+ ++u;
+ }
+ }
+
+ return NS_OK;
+}
+
+// Static compare callback for sorting.
+int nsAttachmentState::CompareAttachmentsByPartId(const void* aLeft,
+ const void* aRight) {
+ msgAttachment& attachLeft = *((msgAttachment*)aLeft);
+ msgAttachment& attachRight = *((msgAttachment*)aRight);
+ return ::CompareAttachmentPartId(attachLeft.mUrl.get(),
+ attachRight.mUrl.get());
+}
+
+// ------------------------------------
+// Helper class to coordinate deleting attachments from a message.
+//
+// Implementation notes:
+// The basic technique is to use nsIMsgMessageService.streamMessage() to
+// stream the message through a streamconverter which is set up to strip
+// out the attachments. The result is written out to a temporary file,
+// which is then copied over the old message using
+// nsIMsgCopyService.copyFileMessage() and the old message deleted with
+// nsIMsgFolder.deleteMessages(). Phew.
+//
+// The nsIStreamListener, nsIUrlListener and nsIMsgCopyServiceListener
+// inheritances here are just unfortunately-exposed implementation details.
+// And they are a bit of a mess. Some are used multiple times, for different
+// phases of the operation. So we use m_state to keep track.
+class AttachmentDeleter : public nsIStreamListener,
+ public nsIUrlListener,
+ public nsIMsgCopyServiceListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ public:
+ AttachmentDeleter();
+ nsresult StartProcessing(nsMessenger* aMessenger, nsIMsgWindow* aMsgWindow,
+ nsAttachmentState* aAttach, bool aSaveFirst);
+
+ public:
+ nsAttachmentState* mAttach; // list of attachments to process
+ bool mSaveFirst; // detach (true) or delete (false)
+ nsCOMPtr<nsIFile> mMsgFile; // temporary file (processed mail)
+ nsCOMPtr<nsIOutputStream> mMsgFileStream; // temporary file (processed mail)
+ nsCOMPtr<nsIMsgMessageService> mMessageService; // original message service
+ nsCOMPtr<nsIMsgDBHdr> mOriginalMessage; // original message header
+ nsCOMPtr<nsIMsgFolder> mMessageFolder; // original message folder
+ nsCOMPtr<nsIMessenger> mMessenger; // our messenger instance
+ nsCOMPtr<nsIMsgWindow> mMsgWindow; // our UI window
+ nsMsgKey mOriginalMessageKey; // old message key
+ nsMsgKey mNewMessageKey; // new message key
+ uint32_t mOrigMsgFlags;
+
+ enum {
+ eStarting,
+ eCopyingNewMsg,
+ eUpdatingFolder, // for IMAP
+ eDeletingOldMessage,
+ eSelectingNewMessage
+ } m_state;
+ // temp
+ nsTArray<nsCString> mDetachedFileUris;
+
+ // The listener to invoke when the full operation is complete.
+ nsCOMPtr<nsIUrlListener> mListener;
+
+ private:
+ nsresult InternalStartProcessing(nsMessenger* aMessenger,
+ nsIMsgWindow* aMsgWindow,
+ nsAttachmentState* aAttach, bool aSaveFirst);
+ nsresult DeleteOriginalMessage();
+ virtual ~AttachmentDeleter();
+};
+
+//
+// nsISupports
+//
+NS_IMPL_ISUPPORTS(AttachmentDeleter, nsIStreamListener, nsIRequestObserver,
+ nsIUrlListener, nsIMsgCopyServiceListener)
+
+//
+// nsIRequestObserver
+//
+NS_IMETHODIMP
+AttachmentDeleter::OnStartRequest(nsIRequest* aRequest) {
+ // called when we start processing the StreamMessage request.
+ // This is called after OnStartRunningUrl().
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
+ // called when we have completed processing the StreamMessage request.
+ // This is called before OnStopRunningUrl(). This means that we have now
+ // received all data of the message and we have completed processing.
+ // We now start to copy the processed message from the temporary file
+ // back into the message store, replacing the original message.
+
+ mMessageFolder->CopyDataDone();
+ if (NS_FAILED(aStatusCode)) return aStatusCode;
+
+ // copy the file back into the folder. Note: setting msgToReplace only copies
+ // metadata, so we do the delete ourselves
+ nsCOMPtr<nsIMsgCopyServiceListener> listenerCopyService;
+ nsresult rv = this->QueryInterface(NS_GET_IID(nsIMsgCopyServiceListener),
+ getter_AddRefs(listenerCopyService));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mMsgFileStream->Close();
+ mMsgFileStream = nullptr;
+ mNewMessageKey = nsMsgKey_None;
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1");
+ m_state = eCopyingNewMsg;
+ // clone file because nsIFile on Windows caches the wrong file size.
+ nsCOMPtr<nsIFile> clone;
+ mMsgFile->Clone(getter_AddRefs(clone));
+ if (copyService) {
+ nsCString originalKeys;
+ mOriginalMessage->GetStringProperty("keywords", originalKeys);
+ rv = copyService->CopyFileMessage(clone, mMessageFolder, mOriginalMessage,
+ false, mOrigMsgFlags, originalKeys,
+ listenerCopyService, mMsgWindow);
+ }
+ return rv;
+}
+
+//
+// nsIStreamListener
+//
+
+NS_IMETHODIMP
+AttachmentDeleter::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInStream,
+ uint64_t aSrcOffset, uint32_t aCount) {
+ if (!mMsgFileStream) return NS_ERROR_NULL_POINTER;
+ return mMessageFolder->CopyDataToOutputStreamForAppend(aInStream, aCount,
+ mMsgFileStream);
+}
+
+//
+// nsIUrlListener
+//
+
+NS_IMETHODIMP
+AttachmentDeleter::OnStartRunningUrl(nsIURI* aUrl) {
+ // called when we start processing the StreamMessage request. This is
+ // called before OnStartRequest().
+ return NS_OK;
+}
+
+nsresult AttachmentDeleter::DeleteOriginalMessage() {
+ nsCOMPtr<nsIMsgCopyServiceListener> listenerCopyService;
+ QueryInterface(NS_GET_IID(nsIMsgCopyServiceListener),
+ getter_AddRefs(listenerCopyService));
+
+ mOriginalMessage->SetUint32Property("attachmentDetached", 1);
+ RefPtr<nsIMsgDBHdr> doomed(mOriginalMessage);
+ mOriginalMessage = nullptr;
+ m_state = eDeletingOldMessage;
+ return mMessageFolder->DeleteMessages({doomed}, // messages
+ mMsgWindow, // msgWindow
+ true, // deleteStorage
+ false, // isMove
+ listenerCopyService, // listener
+ false); // allowUndo
+}
+
+// This is called (potentially) multiple times.
+// Firstly, as a result of StreamMessage() (when the message is being passed
+// through a streamconverter to strip the attachments).
+// Secondly, after the DeleteMessages() call. But maybe not for IMAP?
+// Maybe also after CopyFileMessage()? Gah.
+NS_IMETHODIMP
+AttachmentDeleter::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ nsresult rv = NS_OK;
+ if (mOriginalMessage && m_state == eUpdatingFolder)
+ rv = DeleteOriginalMessage();
+
+ return rv;
+}
+
+//
+// nsIMsgCopyServiceListener
+//
+
+NS_IMETHODIMP
+AttachmentDeleter::OnStartCopy(void) {
+ // never called?
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::OnProgress(uint32_t aProgress, uint32_t aProgressMax) {
+ // never called?
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::SetMessageKey(nsMsgKey aKey) {
+ // called during the copy of the modified message back into the message
+ // store to notify us of the message key of the newly created message.
+ mNewMessageKey = aKey;
+
+ nsCString folderURI;
+ nsresult rv = mMessageFolder->GetURI(folderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozilla::JSONStringWriteFunc<nsCString> jsonString;
+ mozilla::JSONWriter data(jsonString);
+ data.Start();
+ data.IntProperty("oldMessageKey", mOriginalMessageKey);
+ data.IntProperty("newMessageKey", aKey);
+ data.StringProperty("folderURI", folderURI);
+ data.End();
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "attachment-delete-msgkey-changed",
+ NS_ConvertUTF8toUTF16(jsonString.StringCRef()).get());
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::GetMessageId(nsACString& aMessageId) {
+ // never called?
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+AttachmentDeleter::OnStopCopy(nsresult aStatus) {
+ // This is called via `CopyFileMessage()` and `DeleteMessages()`.
+ // `m_state` tells us which callback it is.
+ if (m_state == eDeletingOldMessage) {
+ m_state = eSelectingNewMessage;
+
+ // OK... that's it. The entire operation is now done.
+ // (there may still be another call to OnStopRunningUrl(), but that'll be
+ // a no-op in this state).
+ if (mListener) {
+ mListener->OnStopRunningUrl(nullptr, aStatus);
+ }
+ return NS_OK;
+ }
+
+ // For non-IMAP messages, the original is deleted here, for IMAP messages
+ // that happens in `OnStopRunningUrl()` which isn't called for non-IMAP
+ // messages.
+ const nsACString& messageUri = mAttach->mAttachmentArray[0].mMessageUri;
+ if (mOriginalMessage &&
+ !Substring(messageUri, 0, 13).EqualsLiteral("imap-message:")) {
+ return DeleteOriginalMessage();
+ } else {
+ // Arrange for the message to be deleted in the next `OnStopRunningUrl()`
+ // call.
+ m_state = eUpdatingFolder;
+ }
+
+ return NS_OK;
+}
+
+//
+// local methods
+//
+
+AttachmentDeleter::AttachmentDeleter()
+ : mAttach(nullptr),
+ mSaveFirst(false),
+ mOriginalMessageKey(nsMsgKey_None),
+ mNewMessageKey(nsMsgKey_None),
+ mOrigMsgFlags(0),
+ m_state(eStarting) {}
+
+AttachmentDeleter::~AttachmentDeleter() {
+ if (mAttach) {
+ delete mAttach;
+ }
+ if (mMsgFileStream) {
+ mMsgFileStream->Close();
+ mMsgFileStream = nullptr;
+ }
+ if (mMsgFile) {
+ mMsgFile->Remove(false);
+ }
+}
+
+nsresult AttachmentDeleter::StartProcessing(nsMessenger* aMessenger,
+ nsIMsgWindow* aMsgWindow,
+ nsAttachmentState* aAttach,
+ bool detaching) {
+ if (mListener) {
+ mListener->OnStartRunningUrl(nullptr);
+ }
+
+ nsresult rv =
+ InternalStartProcessing(aMessenger, aMsgWindow, aAttach, detaching);
+ if (NS_FAILED(rv)) {
+ if (mListener) {
+ mListener->OnStopRunningUrl(nullptr, rv);
+ }
+ }
+ return rv;
+}
+
+nsresult AttachmentDeleter::InternalStartProcessing(nsMessenger* aMessenger,
+ nsIMsgWindow* aMsgWindow,
+ nsAttachmentState* aAttach,
+ bool detaching) {
+ aMessenger->QueryInterface(NS_GET_IID(nsIMessenger),
+ getter_AddRefs(mMessenger));
+ mMsgWindow = aMsgWindow;
+ mAttach = aAttach;
+
+ nsresult rv;
+
+ // all attachments refer to the same message
+ const nsCString& messageUri = mAttach->mAttachmentArray[0].mMessageUri;
+
+ // get the message service, original message and folder for this message
+ rv = GetMessageServiceFromURI(messageUri, getter_AddRefs(mMessageService));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mMessageService->MessageURIToMsgHdr(messageUri,
+ getter_AddRefs(mOriginalMessage));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mOriginalMessage->GetMessageKey(&mOriginalMessageKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mOriginalMessage->GetFolder(getter_AddRefs(mMessageFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ mOriginalMessage->GetFlags(&mOrigMsgFlags);
+
+ // ensure that we can store and delete messages in this folder, if we
+ // can't then we can't do attachment deleting
+ bool canDelete = false;
+ mMessageFolder->GetCanDeleteMessages(&canDelete);
+ bool canFile = false;
+ mMessageFolder->GetCanFileMessages(&canFile);
+ if (!canDelete || !canFile) return NS_ERROR_FAILURE;
+
+ // create an output stream on a temporary file. This stream will save the
+ // modified message data to a file which we will later use to replace the
+ // existing message. The file is removed in the destructor.
+ rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "nsmail.tmp",
+ getter_AddRefs(mMsgFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // For temp file, we should use restrictive 00600 instead of
+ // ATTACHMENT_PERMISSION
+ rv = mMsgFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mMsgFileStream), mMsgFile,
+ -1, ATTACHMENT_PERMISSION);
+
+ // Create the additional header for data conversion. This will tell the stream
+ // converter which MIME emitter we want to use, and it will tell the MIME
+ // emitter which attachments should be deleted.
+ // It also supplies the path of the already-saved attachments, so that
+ // path can be noted in the message, where those attachements are removed.
+ // The X-Mozilla-External-Attachment-URL header will be added, with the
+ // location of the saved attachment.
+ const char* partId;
+ const char* nextField;
+ nsAutoCString sHeader("attach&del=");
+ nsAutoCString detachToHeader("&detachTo=");
+ for (uint32_t u = 0; u < mAttach->mAttachmentArray.Length(); ++u) {
+ if (u > 0) {
+ sHeader.Append(',');
+ if (detaching) detachToHeader.Append(',');
+ }
+ partId = GetAttachmentPartId(mAttach->mAttachmentArray[u].mUrl.get());
+ if (partId) {
+ nextField = PL_strchr(partId, '&');
+ sHeader.Append(partId, nextField ? nextField - partId : -1);
+ }
+ if (detaching) {
+ // The URI can contain commas, so percent-encode those first.
+ nsAutoCString uri(mDetachedFileUris[u]);
+ int ind = uri.FindChar(',');
+ while (ind != kNotFound) {
+ uri.Replace(ind, 1, "%2C");
+ ind = uri.FindChar(',');
+ }
+ detachToHeader.Append(uri);
+ }
+ }
+
+ if (detaching) sHeader.Append(detachToHeader);
+ // stream this message to our listener converting it via the attachment mime
+ // converter. The listener will just write the converted message straight to
+ // disk.
+ nsCOMPtr<nsISupports> listenerSupports;
+ rv = this->QueryInterface(NS_GET_IID(nsISupports),
+ getter_AddRefs(listenerSupports));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIUrlListener> listenerUrlListener =
+ do_QueryInterface(listenerSupports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = mMessageService->StreamMessage(messageUri, listenerSupports, mMsgWindow,
+ listenerUrlListener, true, sHeader, false,
+ getter_AddRefs(dummyNull));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// ------------------------------------
+
+NS_IMETHODIMP
+nsMessenger::DetachAttachment(const nsACString& aContentType,
+ const nsACString& aURL,
+ const nsACString& aDisplayName,
+ const nsACString& aMessageUri, bool aSaveFirst,
+ bool withoutWarning = false) {
+ if (aSaveFirst)
+ return SaveOneAttachment(aContentType, aURL, aDisplayName, aMessageUri,
+ true);
+ AutoTArray<nsCString, 1> contentTypeArray = {
+ PromiseFlatCString(aContentType)};
+ AutoTArray<nsCString, 1> urlArray = {PromiseFlatCString(aURL)};
+ AutoTArray<nsCString, 1> displayNameArray = {
+ PromiseFlatCString(aDisplayName)};
+ AutoTArray<nsCString, 1> messageUriArray = {PromiseFlatCString(aMessageUri)};
+ return DetachAttachments(contentTypeArray, urlArray, displayNameArray,
+ messageUriArray, nullptr, nullptr, withoutWarning);
+}
+
+NS_IMETHODIMP
+nsMessenger::DetachAllAttachments(const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray,
+ bool aSaveFirst,
+ bool withoutWarning = false) {
+ NS_ENSURE_ARG_MIN(aContentTypeArray.Length(), 1);
+ MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() &&
+ aUrlArray.Length() == aDisplayNameArray.Length() &&
+ aDisplayNameArray.Length() == aMessageUriArray.Length());
+
+ if (aSaveFirst)
+ return SaveAllAttachments(aContentTypeArray, aUrlArray, aDisplayNameArray,
+ aMessageUriArray, true);
+ else
+ return DetachAttachments(aContentTypeArray, aUrlArray, aDisplayNameArray,
+ aMessageUriArray, nullptr, nullptr,
+ withoutWarning);
+}
+
+nsresult nsMessenger::DetachAttachments(
+ const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray,
+ nsTArray<nsCString>* saveFileUris, nsIUrlListener* aListener,
+ bool withoutWarning) {
+ // if withoutWarning no dialog for user
+ if (!withoutWarning && NS_FAILED(PromptIfDeleteAttachments(
+ saveFileUris != nullptr, aDisplayNameArray)))
+ return NS_OK;
+
+ nsresult rv = NS_OK;
+
+ // ensure that our arguments are valid
+ // char * partId;
+ for (uint32_t u = 0; u < aContentTypeArray.Length(); ++u) {
+ // ensure all of the message URI are the same, we cannot process
+ // attachments from different messages
+ if (u > 0 && aMessageUriArray[0] != aMessageUriArray[u]) {
+ rv = NS_ERROR_INVALID_ARG;
+ break;
+ }
+
+ // ensure that we don't have deleted messages in this list
+ if (aContentTypeArray[u].EqualsLiteral(MIMETYPE_DELETED)) {
+ rv = NS_ERROR_INVALID_ARG;
+ break;
+ }
+
+ // for the moment we prevent any attachments other than root level
+ // attachments being deleted (i.e. you can't delete attachments from a
+ // email forwarded as an attachment). We do this by ensuring that the
+ // part id only has a single period in it (e.g. "1.2").
+ // TODO: support non-root level attachment delete
+ // partId = ::GetAttachmentPartId(aUrlArray[u]);
+ // if (!partId || PL_strchr(partId, '.') != PL_strrchr(partId, '.'))
+ // {
+ // rv = NS_ERROR_INVALID_ARG;
+ // break;
+ // }
+ }
+ if (NS_FAILED(rv)) {
+ Alert("deleteAttachmentFailure");
+ return rv;
+ }
+
+ // TODO: ensure that nothing else is processing this message uri at the same
+ // time
+
+ // TODO: if any of the selected attachments are messages that contain other
+ // attachments we need to warn the user that all sub-attachments of those
+ // messages will also be deleted. Best to display a list of them.
+
+ RefPtr<AttachmentDeleter> deleter = new AttachmentDeleter;
+ deleter->mListener = aListener;
+ if (saveFileUris) {
+ deleter->mDetachedFileUris = saveFileUris->Clone();
+ }
+ // create the attachments for use by the deleter
+ nsAttachmentState* attach = new nsAttachmentState;
+ rv = attach->Init(aContentTypeArray, aUrlArray, aDisplayNameArray,
+ aMessageUriArray);
+ if (NS_SUCCEEDED(rv)) rv = attach->PrepareForAttachmentDelete();
+ if (NS_FAILED(rv)) {
+ delete attach;
+ return rv;
+ }
+
+ // initialize our deleter with the attachments and details. The deleter
+ // takes ownership of 'attach' immediately irrespective of the return value
+ // (error or not).
+ return deleter->StartProcessing(this, mMsgWindow, attach,
+ saveFileUris != nullptr);
+}
+
+nsresult nsMessenger::PromptIfDeleteAttachments(
+ bool aSaveFirst, const nsTArray<nsCString>& aDisplayNameArray) {
+ nsresult rv = NS_ERROR_FAILURE;
+
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(mDocShell));
+ if (!dialog) return rv;
+
+ if (!mStringBundle) {
+ rv = InitStringBundle();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // create the list of attachments we are removing
+ nsString displayString;
+ nsString attachmentList;
+ for (uint32_t u = 0; u < aDisplayNameArray.Length(); ++u) {
+ ConvertAndSanitizeFileName(aDisplayNameArray[u], displayString);
+ attachmentList.Append(displayString);
+ attachmentList.Append(char16_t('\n'));
+ }
+ AutoTArray<nsString, 1> formatStrings = {attachmentList};
+
+ // format the message and display
+ nsString promptMessage;
+ const char* propertyName =
+ aSaveFirst ? "detachAttachments" : "deleteAttachments";
+ rv = mStringBundle->FormatStringFromName(propertyName, formatStrings,
+ promptMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool dialogResult = false;
+ rv = dialog->Confirm(nullptr, promptMessage.get(), &dialogResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return dialogResult ? NS_OK : NS_ERROR_FAILURE;
+}
diff --git a/comm/mailnews/base/src/nsMessenger.h b/comm/mailnews/base/src/nsMessenger.h
new file mode 100644
index 0000000000..b6eab3f179
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessenger.h
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __nsMsgAppCore_h
+#define __nsMsgAppCore_h
+
+#include "nscore.h"
+#include "nsIMessenger.h"
+#include "nsCOMPtr.h"
+#include "nsITransactionManager.h"
+#include "nsIFile.h"
+#include "nsIDocShell.h"
+#include "nsString.h"
+#include "nsIStringBundle.h"
+#include "nsIFile.h"
+#include "nsIFilePicker.h"
+#include "nsWeakReference.h"
+#include "mozIDOMWindow.h"
+#include "nsTArray.h"
+#include "nsIMsgStatusFeedback.h"
+
+class nsSaveAllAttachmentsState;
+
+class nsMessenger : public nsIMessenger, public nsSupportsWeakReference {
+ using PathString = mozilla::PathString;
+
+ public:
+ nsMessenger();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMESSENGER
+
+ nsresult Alert(const char* stringName);
+
+ nsresult SaveAttachment(nsIFile* file, const nsACString& unescapedUrl,
+ const nsACString& messageUri,
+ const nsACString& contentType,
+ nsSaveAllAttachmentsState* saveState,
+ nsIUrlListener* aListener);
+ nsresult PromptIfFileExists(nsIFile* file);
+ nsresult DetachAttachments(const nsTArray<nsCString>& aContentTypeArray,
+ const nsTArray<nsCString>& aUrlArray,
+ const nsTArray<nsCString>& aDisplayNameArray,
+ const nsTArray<nsCString>& aMessageUriArray,
+ nsTArray<nsCString>* saveFileUris,
+ nsIUrlListener* aListener,
+ bool withoutWarning = false);
+ nsresult SaveAllAttachments(const nsTArray<nsCString>& contentTypeArray,
+ const nsTArray<nsCString>& urlArray,
+ const nsTArray<nsCString>& displayNameArray,
+ const nsTArray<nsCString>& messageUriArray,
+ bool detaching);
+ nsresult SaveOneAttachment(const nsACString& aContentType,
+ const nsACString& aURL,
+ const nsACString& aDisplayName,
+ const nsACString& aMessageUri, bool detaching);
+
+ protected:
+ virtual ~nsMessenger();
+
+ void GetString(const nsString& aStringName, nsString& stringValue);
+ nsresult InitStringBundle();
+ nsresult PromptIfDeleteAttachments(
+ bool saveFirst, const nsTArray<nsCString>& displayNameArray);
+
+ private:
+ nsresult GetLastSaveDirectory(nsIFile** aLastSaveAsDir);
+ // if aLocalFile is a dir, we use it. otherwise, we use the parent of
+ // aLocalFile.
+ nsresult SetLastSaveDirectory(nsIFile* aLocalFile);
+
+ nsresult AdjustFileIfNameTooLong(nsIFile* aFile);
+
+ nsresult GetSaveAsFile(const nsAString& aMsgFilename,
+ int32_t* aSaveAsFileType, nsIFile** aSaveAsFile);
+
+ nsresult GetSaveToDir(nsIFile** aSaveToDir);
+ nsresult ShowPicker(nsIFilePicker* aPicker,
+ nsIFilePicker::ResultCode* aResult);
+
+ class nsFilePickerShownCallback : public nsIFilePickerShownCallback {
+ virtual ~nsFilePickerShownCallback() {}
+
+ public:
+ nsFilePickerShownCallback();
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Done(nsIFilePicker::ResultCode aResult) override;
+
+ public:
+ bool mPickerDone;
+ nsIFilePicker::ResultCode mResult;
+ };
+
+ nsString mId;
+ nsCOMPtr<nsITransactionManager> mTxnMgr;
+
+ /* rhp - need this to drive message display */
+ nsCOMPtr<mozIDOMWindowProxy> mWindow;
+ nsCOMPtr<nsIMsgWindow> mMsgWindow;
+ nsCOMPtr<nsIDocShell> mDocShell;
+
+ // String bundles...
+ nsCOMPtr<nsIStringBundle> mStringBundle;
+
+ nsCOMPtr<nsISupports> mSearchContext;
+};
+
+#define NS_MESSENGER_CID \
+ { /* f436a174-e2c0-4955-9afe-e3feb68aee56 */ \
+ 0xf436a174, 0xe2c0, 0x4955, { \
+ 0x9a, 0xfe, 0xe3, 0xfe, 0xb6, 0x8a, 0xee, 0x56 \
+ } \
+ }
+
+#endif
diff --git a/comm/mailnews/base/src/nsMessengerBootstrap.cpp b/comm/mailnews/base/src/nsMessengerBootstrap.cpp
new file mode 100644
index 0000000000..9d01b5380d
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerBootstrap.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMessengerBootstrap.h"
+#include "nsCOMPtr.h"
+
+#include "nsIMutableArray.h"
+#include "nsIMsgFolder.h"
+#include "nsIWindowWatcher.h"
+#include "nsMsgUtils.h"
+#include "nsISupportsPrimitives.h"
+#include "mozIDOMWindow.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+
+NS_IMPL_ISUPPORTS(nsMessengerBootstrap, nsIMessengerWindowService)
+
+nsMessengerBootstrap::nsMessengerBootstrap() {}
+
+nsMessengerBootstrap::~nsMessengerBootstrap() {}
+
+NS_IMETHODIMP nsMessengerBootstrap::OpenMessengerWindowWithUri(
+ const char* windowType, const nsACString& aFolderURI,
+ nsMsgKey aMessageKey) {
+ bool standAloneMsgWindow = false;
+ nsAutoCString chromeUrl("chrome://messenger/content/");
+ if (windowType && !strcmp(windowType, "mail:messageWindow")) {
+ chromeUrl.AppendLiteral("messageWindow.xhtml");
+ standAloneMsgWindow = true;
+ } else {
+ chromeUrl.AppendLiteral("messenger.xhtml");
+ }
+ nsresult rv;
+ nsCOMPtr<nsIMutableArray> argsArray(
+ do_CreateInstance(NS_ARRAY_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // create scriptable versions of our strings that we can store in our
+ // nsIMutableArray....
+ if (!aFolderURI.IsEmpty()) {
+ if (standAloneMsgWindow) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetExistingFolder(aFolderURI, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString msgUri;
+ folder->GetBaseMessageURI(msgUri);
+
+ nsCOMPtr<nsISupportsCString> scriptableMsgURI(
+ do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableMsgURI, NS_ERROR_FAILURE);
+ msgUri.Append('#');
+ msgUri.AppendInt(aMessageKey, 10);
+ scriptableMsgURI->SetData(msgUri);
+ argsArray->AppendElement(scriptableMsgURI);
+ }
+ nsCOMPtr<nsISupportsCString> scriptableFolderURI(
+ do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableFolderURI, NS_ERROR_FAILURE);
+
+ scriptableFolderURI->SetData(aFolderURI);
+ argsArray->AppendElement(scriptableFolderURI);
+
+ if (!standAloneMsgWindow) {
+ nsCOMPtr<nsISupportsPRUint32> scriptableMessageKey(
+ do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID));
+ NS_ENSURE_TRUE(scriptableMessageKey, NS_ERROR_FAILURE);
+ scriptableMessageKey->SetData(aMessageKey);
+ argsArray->AppendElement(scriptableMessageKey);
+ }
+ }
+
+ nsCOMPtr<nsIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we need to use the "mailnews.reuse_thread_window2" pref
+ // to determine if we should open a new window, or use an existing one.
+ nsCOMPtr<mozIDOMWindowProxy> newWindow;
+ return wwatch->OpenWindow(0, chromeUrl, "_blank"_ns,
+ "chrome,all,dialog=no"_ns, argsArray,
+ getter_AddRefs(newWindow));
+}
diff --git a/comm/mailnews/base/src/nsMessengerBootstrap.h b/comm/mailnews/base/src/nsMessengerBootstrap.h
new file mode 100644
index 0000000000..a81e81eeb9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerBootstrap.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef __nsMessenger_h
+#define __nsMessenger_h
+
+#include "nscore.h"
+#include "nsIMessengerWindowService.h"
+
+#define NS_MESSENGERBOOTSTRAP_CID \
+ { /* 4a85a5d0-cddd-11d2-b7f6-00805f05ffa5 */ \
+ 0x4a85a5d0, 0xcddd, 0x11d2, { \
+ 0xb7, 0xf6, 0x00, 0x80, 0x5f, 0x05, 0xff, 0xa5 \
+ } \
+ }
+
+class nsMessengerBootstrap : public nsIMessengerWindowService {
+ public:
+ nsMessengerBootstrap();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMESSENGERWINDOWSERVICE
+
+ private:
+ virtual ~nsMessengerBootstrap();
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMessengerOSXIntegration.h b/comm/mailnews/base/src/nsMessengerOSXIntegration.h
new file mode 100644
index 0000000000..e4b503cb12
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerOSXIntegration.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef __nsMessengerOSXIntegration_h
+#define __nsMessengerOSXIntegration_h
+
+#include "nsIMessengerOSIntegration.h"
+
+class nsMessengerOSXIntegration : public nsIMessengerOSIntegration {
+ public:
+ nsMessengerOSXIntegration();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMESSENGEROSINTEGRATION
+
+ private:
+ virtual ~nsMessengerOSXIntegration();
+
+ nsresult RestoreDockIcon();
+};
+
+#endif // __nsMessengerOSXIntegration_h
diff --git a/comm/mailnews/base/src/nsMessengerOSXIntegration.mm b/comm/mailnews/base/src/nsMessengerOSXIntegration.mm
new file mode 100644
index 0000000000..858e1417df
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerOSXIntegration.mm
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMessengerOSXIntegration.h"
+#include "nsObjCExceptions.h"
+#include "nsString.h"
+#include "mozilla/ErrorResult.h"
+
+#include <Carbon/Carbon.h>
+#import <Cocoa/Cocoa.h>
+
+nsMessengerOSXIntegration::nsMessengerOSXIntegration() {}
+
+nsMessengerOSXIntegration::~nsMessengerOSXIntegration() {}
+
+NS_IMPL_ADDREF(nsMessengerOSXIntegration)
+NS_IMPL_RELEASE(nsMessengerOSXIntegration)
+
+NS_INTERFACE_MAP_BEGIN(nsMessengerOSXIntegration)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMessengerOSIntegration)
+ NS_INTERFACE_MAP_ENTRY(nsIMessengerOSIntegration)
+NS_INTERFACE_MAP_END
+
+nsresult nsMessengerOSXIntegration::RestoreDockIcon() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ id tile = [[NSApplication sharedApplication] dockTile];
+ [tile setBadgeLabel:nil];
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMessengerOSXIntegration::UpdateUnreadCount(uint32_t unreadCount, const nsAString& unreadTooltip) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ if (unreadCount == 0) {
+ RestoreDockIcon();
+ return NS_OK;
+ }
+
+ nsAutoString total;
+ if (unreadCount > 99) {
+ total.AppendLiteral("99+");
+ } else {
+ total.AppendInt(unreadCount);
+ }
+ id tile = [[NSApplication sharedApplication] dockTile];
+ [tile setBadgeLabel:[NSString stringWithFormat:@"%S", (const unichar*)total.get()]];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+nsMessengerOSXIntegration::OnExit() {
+ RestoreDockIcon();
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMessengerUnixIntegration.cpp b/comm/mailnews/base/src/nsMessengerUnixIntegration.cpp
new file mode 100644
index 0000000000..96ddb9c48a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerUnixIntegration.cpp
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMessengerUnixIntegration.h"
+#include "nsString.h"
+
+/**
+ * This is only a placeholder for now, register it in components.conf later if
+ * needed.
+ */
+nsMessengerUnixIntegration::nsMessengerUnixIntegration() {}
+
+NS_IMPL_ISUPPORTS(nsMessengerUnixIntegration, nsIMessengerOSIntegration)
+
+NS_IMETHODIMP
+nsMessengerUnixIntegration::UpdateUnreadCount(uint32_t unreadCount,
+ const nsAString& unreadTooltip) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessengerUnixIntegration::OnExit() { return NS_OK; }
diff --git a/comm/mailnews/base/src/nsMessengerUnixIntegration.h b/comm/mailnews/base/src/nsMessengerUnixIntegration.h
new file mode 100644
index 0000000000..180f268560
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerUnixIntegration.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __nsMessengerUnixIntegration_h
+#define __nsMessengerUnixIntegration_h
+
+#include "nsIMessengerOSIntegration.h"
+
+class nsMessengerUnixIntegration : public nsIMessengerOSIntegration {
+ public:
+ nsMessengerUnixIntegration();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMESSENGEROSINTEGRATION
+
+ private:
+ virtual ~nsMessengerUnixIntegration() {}
+};
+
+#endif // __nsMessengerUnixIntegration_h
diff --git a/comm/mailnews/base/src/nsMessengerWinIntegration.cpp b/comm/mailnews/base/src/nsMessengerWinIntegration.cpp
new file mode 100644
index 0000000000..7db74af5e0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerWinIntegration.cpp
@@ -0,0 +1,379 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <windows.h>
+#include <shellapi.h>
+#include <strsafe.h>
+
+#include "mozilla/Components.h"
+#include "mozilla/Services.h"
+#include "mozIDOMWindow.h"
+#include "nsIBaseWindow.h"
+#include "nsIDocShell.h"
+#include "nsIMsgWindow.h"
+#include "nsIObserverService.h"
+#include "nsIPrefService.h"
+#include "nsIWidget.h"
+#include "nsIWindowMediator.h"
+#include "nsMessengerWinIntegration.h"
+#include "nsMsgDBFolder.h"
+#include "nsPIDOMWindow.h"
+
+#define IDI_MAILBIFF 32576
+#define SHOW_TRAY_ICON_PREF "mail.biff.show_tray_icon"
+#define SHOW_TRAY_ICON_ALWAYS_PREF "mail.biff.show_tray_icon_always"
+
+// since we are including windows.h in this file, undefine get user name....
+#ifdef GetUserName
+# undef GetUserName
+#endif
+
+#ifndef NIIF_USER
+# define NIIF_USER 0x00000004
+#endif
+
+#ifndef NIIF_NOSOUND
+# define NIIF_NOSOUND 0x00000010
+#endif
+
+using namespace mozilla;
+
+nsMessengerWinIntegration::nsMessengerWinIntegration() {}
+
+nsMessengerWinIntegration::~nsMessengerWinIntegration() {}
+
+NS_IMPL_ADDREF(nsMessengerWinIntegration)
+NS_IMPL_RELEASE(nsMessengerWinIntegration)
+
+NS_INTERFACE_MAP_BEGIN(nsMessengerWinIntegration)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMessengerOSIntegration)
+ NS_INTERFACE_MAP_ENTRY(nsIMessengerWindowsIntegration)
+ NS_INTERFACE_MAP_ENTRY(nsIMessengerOSIntegration)
+NS_INTERFACE_MAP_END
+
+static HWND hwndForDOMWindow(mozIDOMWindowProxy* window) {
+ if (!window) {
+ return 0;
+ }
+ nsCOMPtr<nsPIDOMWindowOuter> pidomwindow = nsPIDOMWindowOuter::From(window);
+
+ nsCOMPtr<nsIBaseWindow> ppBaseWindow =
+ do_QueryInterface(pidomwindow->GetDocShell());
+ if (!ppBaseWindow) return 0;
+
+ nsCOMPtr<nsIWidget> ppWidget;
+ ppBaseWindow->GetMainWidget(getter_AddRefs(ppWidget));
+
+ return (HWND)(ppWidget->GetNativeData(NS_NATIVE_WIDGET));
+}
+
+static void activateWindow(mozIDOMWindowProxy* win) {
+ // Try to get native window handle.
+ HWND hwnd = hwndForDOMWindow(win);
+ if (hwnd) {
+ // Restore the window if it is minimized.
+ if (::IsIconic(hwnd)) ::ShowWindow(hwnd, SW_RESTORE);
+ // Use the OS call, if possible.
+ ::SetForegroundWindow(hwnd);
+ } else {
+ // Use internal method.
+ nsCOMPtr<nsPIDOMWindowOuter> privateWindow = nsPIDOMWindowOuter::From(win);
+ privateWindow->Focus(mozilla::dom::CallerType::System);
+ }
+}
+
+NOTIFYICONDATAW sMailIconData = {
+ /* cbSize */ (DWORD)NOTIFYICONDATAW_V2_SIZE,
+ /* hWnd */ 0,
+ /* uID */ 2,
+ /* uFlags */ NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO,
+ /* uCallbackMessage */ WM_USER,
+ /* hIcon */ 0,
+ /* szTip */ L"",
+ /* dwState */ 0,
+ /* dwStateMask */ 0,
+ /* szInfo */ L"",
+ /* uVersion */ {30000},
+ /* szInfoTitle */ L"",
+ /* dwInfoFlags */ NIIF_USER | NIIF_NOSOUND};
+
+static nsCOMArray<nsIBaseWindow> sHiddenWindows;
+static HWND sIconWindow;
+static uint32_t sUnreadCount;
+/* static */
+LRESULT CALLBACK nsMessengerWinIntegration::IconWindowProc(HWND msgWindow,
+ UINT msg, WPARAM wp,
+ LPARAM lp) {
+ nsresult rv;
+ static UINT sTaskbarRecreated;
+
+ switch (msg) {
+ case WM_USER:
+ if (msg == WM_USER && lp == WM_LBUTTONDOWN) {
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, FALSE);
+ bool showTrayIcon;
+ rv = prefBranch->GetBoolPref(SHOW_TRAY_ICON_PREF, &showTrayIcon);
+ NS_ENSURE_SUCCESS(rv, FALSE);
+ bool showTrayIconAlways;
+ if (NS_FAILED(prefBranch->GetBoolPref(SHOW_TRAY_ICON_ALWAYS_PREF,
+ &showTrayIconAlways))) {
+ showTrayIconAlways = false;
+ }
+ if ((!showTrayIcon || !sUnreadCount) && !showTrayIconAlways) {
+ ::Shell_NotifyIconW(NIM_DELETE, &sMailIconData);
+ if (auto instance = reinterpret_cast<nsMessengerWinIntegration*>(
+ ::GetWindowLongPtrW(msgWindow, GWLP_USERDATA))) {
+ instance->mTrayIconShown = false;
+ }
+ }
+
+ // No minimzed window, bring the most recent 3pane window to the front.
+ if (sHiddenWindows.Length() == 0) {
+ nsCOMPtr<nsIWindowMediator> windowMediator =
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, FALSE);
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ rv = windowMediator->GetMostRecentBrowserWindow(
+ getter_AddRefs(domWindow));
+ NS_ENSURE_SUCCESS(rv, FALSE);
+ if (domWindow) {
+ activateWindow(domWindow);
+ return TRUE;
+ }
+ }
+
+ // Bring the minimized windows to the front.
+ for (uint32_t i = 0; i < sHiddenWindows.Length(); i++) {
+ auto window = sHiddenWindows.SafeElementAt(i);
+ if (!window) {
+ continue;
+ }
+ window->SetVisibility(true);
+
+ nsCOMPtr<nsIWidget> widget;
+ window->GetMainWidget(getter_AddRefs(widget));
+ if (!widget) {
+ continue;
+ }
+
+ HWND hwnd = (HWND)(widget->GetNativeData(NS_NATIVE_WIDGET));
+ ::ShowWindow(hwnd, SW_RESTORE);
+ ::SetForegroundWindow(hwnd);
+
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(window, "windows-refresh-badge-tray", 0);
+ }
+
+ sHiddenWindows.Clear();
+ }
+ break;
+ case WM_CREATE:
+ sTaskbarRecreated = ::RegisterWindowMessageW(L"TaskbarCreated");
+ break;
+ default:
+ if (msg == sTaskbarRecreated) {
+ // When taskbar is recreated (e.g. by restarting Windows Explorer), all
+ // tray icons are removed. If there are windows minimized to tray icon,
+ // we have to recreate the tray icon, otherwise the windows can't be
+ // restored.
+ if (auto instance = reinterpret_cast<nsMessengerWinIntegration*>(
+ ::GetWindowLongPtrW(msgWindow, GWLP_USERDATA))) {
+ instance->mTrayIconShown = false;
+ }
+ for (uint32_t i = 0; i < sHiddenWindows.Length(); i++) {
+ auto window = sHiddenWindows.SafeElementAt(i);
+ if (!window) {
+ continue;
+ }
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(window, "windows-refresh-badge-tray", 0);
+ }
+ }
+ break;
+ }
+ return ::DefWindowProc(msgWindow, msg, wp, lp);
+}
+
+nsresult nsMessengerWinIntegration::HideWindow(nsIBaseWindow* aWindow) {
+ NS_ENSURE_ARG(aWindow);
+ aWindow->SetVisibility(false);
+ sHiddenWindows.AppendElement(aWindow);
+
+ nsresult rv;
+ rv = CreateIconWindow();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mTrayIconShown) {
+ auto idi = IDI_APPLICATION;
+ if (sUnreadCount > 0) {
+ idi = MAKEINTRESOURCE(IDI_MAILBIFF);
+ }
+ sMailIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), idi);
+ nsresult rv = SetTooltip();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ::Shell_NotifyIconW(NIM_ADD, &sMailIconData);
+ ::Shell_NotifyIconW(NIM_SETVERSION, &sMailIconData);
+ mTrayIconShown = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessengerWinIntegration::ShowWindow(mozIDOMWindowProxy* aWindow) {
+ activateWindow(aWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMessengerWinIntegration::UpdateUnreadCount(uint32_t unreadCount,
+ const nsAString& unreadTooltip) {
+ sUnreadCount = unreadCount;
+ mUnreadTooltip = unreadTooltip;
+ nsresult rv = UpdateTrayIcon();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMessengerWinIntegration::OnExit() {
+ if (mTrayIconShown) {
+ ::Shell_NotifyIconW(NIM_DELETE, &sMailIconData);
+ mTrayIconShown = false;
+ }
+ return NS_OK;
+}
+
+/**
+ * Set a tooltip to the tray icon. Including the brand short name, and unread
+ * message count.
+ */
+nsresult nsMessengerWinIntegration::SetTooltip() {
+ nsresult rv = NS_OK;
+ if (mBrandShortName.IsEmpty()) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://branding/locale/brand.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = bundle->GetStringFromName("brandShortName", mBrandShortName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsString tooltip = mBrandShortName;
+ if (!mUnreadTooltip.IsEmpty()) {
+ tooltip.AppendLiteral("\n");
+ tooltip.Append(mUnreadTooltip);
+ }
+ size_t destLength =
+ sizeof sMailIconData.szTip / (sizeof sMailIconData.szTip[0]);
+ ::StringCchCopyNW(sMailIconData.szTip, destLength, tooltip.get(),
+ tooltip.Length());
+ return rv;
+}
+
+/**
+ * Create a custom window for the taskbar icon if it's not created yet.
+ */
+nsresult nsMessengerWinIntegration::CreateIconWindow() {
+ if (sMailIconData.hWnd) {
+ return NS_OK;
+ }
+
+ const wchar_t kClassName[] = L"IconWindowClass";
+ WNDCLASS classStruct = {/* style */ 0,
+ /* lpfnWndProc */ &IconWindowProc,
+ /* cbClsExtra */ 0,
+ /* cbWndExtra */ 0,
+ /* hInstance */ 0,
+ /* hIcon */ 0,
+ /* hCursor */ 0,
+ /* hbrBackground */ 0,
+ /* lpszMenuName */ 0,
+ /* lpszClassName */ kClassName};
+
+ // Register the window class.
+ NS_ENSURE_TRUE(::RegisterClass(&classStruct), NS_ERROR_FAILURE);
+ // Create the window.
+ NS_ENSURE_TRUE(sIconWindow = ::CreateWindow(
+ /* className */ kClassName,
+ /* title */ 0,
+ /* style */ WS_CAPTION,
+ /* x, y, cx, cy */ 0, 0, 0, 0,
+ /* parent */ 0,
+ /* menu */ 0,
+ /* instance */ 0,
+ /* create struct */ 0),
+ NS_ERROR_FAILURE);
+ NS_ENSURE_TRUE(::SetWindowLongPtrW(sIconWindow, GWLP_USERDATA,
+ reinterpret_cast<LONG_PTR>(this)) == 0,
+ NS_ERROR_FAILURE);
+
+ sMailIconData.hWnd = sIconWindow;
+ return NS_OK;
+}
+
+/**
+ * Update the tray icon according to the current unread count and pref value.
+ */
+nsresult nsMessengerWinIntegration::UpdateTrayIcon() {
+ nsresult rv;
+
+ rv = CreateIconWindow();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mPrefBranch) {
+ mPrefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = SetTooltip();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool showTrayIconAlways;
+ if (NS_FAILED(mPrefBranch->GetBoolPref(SHOW_TRAY_ICON_ALWAYS_PREF,
+ &showTrayIconAlways))) {
+ showTrayIconAlways = false;
+ }
+ if (sUnreadCount > 0 || showTrayIconAlways) {
+ auto idi = IDI_APPLICATION;
+ if (sUnreadCount > 0) {
+ // Only showing the new mail marker when there are actual unread mail
+ idi = MAKEINTRESOURCE(IDI_MAILBIFF);
+ }
+ sMailIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), idi);
+ if (mTrayIconShown) {
+ // If the tray icon is already shown, just modify it.
+ ::Shell_NotifyIconW(NIM_MODIFY, &sMailIconData);
+ } else {
+ bool showTrayIcon;
+ rv = mPrefBranch->GetBoolPref(SHOW_TRAY_ICON_PREF, &showTrayIcon);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (showTrayIcon) {
+ // Show a tray icon only if the pref value is true.
+ ::Shell_NotifyIconW(NIM_ADD, &sMailIconData);
+ ::Shell_NotifyIconW(NIM_SETVERSION, &sMailIconData);
+ mTrayIconShown = true;
+ }
+ }
+ } else if (mTrayIconShown) {
+ if (sHiddenWindows.Length() > 0) {
+ // At least one window is minimized, modify the icon only.
+ sMailIconData.hIcon =
+ ::LoadIcon(::GetModuleHandle(NULL), IDI_APPLICATION);
+ ::Shell_NotifyIconW(NIM_MODIFY, &sMailIconData);
+ } else if (!showTrayIconAlways) {
+ // No unread, no need to show the tray icon.
+ ::Shell_NotifyIconW(NIM_DELETE, &sMailIconData);
+ mTrayIconShown = false;
+ }
+ }
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMessengerWinIntegration.h b/comm/mailnews/base/src/nsMessengerWinIntegration.h
new file mode 100644
index 0000000000..0fb9f1a718
--- /dev/null
+++ b/comm/mailnews/base/src/nsMessengerWinIntegration.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef __nsMessengerWinIntegration_h
+#define __nsMessengerWinIntegration_h
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsIMessengerWindowsIntegration.h"
+#include "nsIStringBundle.h"
+#include "nsIPrefBranch.h"
+
+class nsMessengerWinIntegration : public nsIMessengerWindowsIntegration {
+ public:
+ nsMessengerWinIntegration();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMESSENGERWINDOWSINTEGRATION
+ NS_DECL_NSIMESSENGEROSINTEGRATION
+
+ private:
+ static LRESULT CALLBACK IconWindowProc(HWND msgWindow, UINT msg, WPARAM wp,
+ LPARAM lp);
+
+ virtual ~nsMessengerWinIntegration();
+
+ nsresult CreateIconWindow();
+ nsresult SetTooltip();
+ nsresult UpdateTrayIcon();
+
+ nsCOMPtr<nsIPrefBranch> mPrefBranch;
+ bool mTrayIconShown = false;
+ nsString mBrandShortName;
+ nsString mUnreadTooltip;
+};
+
+#endif // __nsMessengerWinIntegration_h
diff --git a/comm/mailnews/base/src/nsMsgAccount.cpp b/comm/mailnews/base/src/nsMsgAccount.cpp
new file mode 100644
index 0000000000..8751e4cdde
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgAccount.cpp
@@ -0,0 +1,413 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "prprf.h"
+#include "plstr.h"
+#include "prmem.h"
+#include "nsIComponentManager.h"
+#include "nsIServiceManager.h"
+#include "nsCRTGlue.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsPrintfCString.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsMsgAccount.h"
+#include "nsIMsgAccount.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMemory.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+
+NS_IMPL_ISUPPORTS(nsMsgAccount, nsIMsgAccount)
+
+nsMsgAccount::nsMsgAccount()
+ : m_identitiesValid(false), mTriedToGetServer(false) {}
+
+nsMsgAccount::~nsMsgAccount() {}
+
+nsresult nsMsgAccount::getPrefService() {
+ if (m_prefs) return NS_OK;
+
+ nsresult rv;
+ NS_ENSURE_FALSE(m_accountKey.IsEmpty(), NS_ERROR_NOT_INITIALIZED);
+ nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString accountRoot("mail.account.");
+ accountRoot.Append(m_accountKey);
+ accountRoot.Append('.');
+ return prefs->GetBranch(accountRoot.get(), getter_AddRefs(m_prefs));
+}
+
+NS_IMETHODIMP
+nsMsgAccount::GetIncomingServer(nsIMsgIncomingServer** aIncomingServer) {
+ NS_ENSURE_ARG_POINTER(aIncomingServer);
+
+ // create the incoming server lazily
+ if (!mTriedToGetServer && !m_incomingServer) {
+ mTriedToGetServer = true;
+ // ignore the error (and return null), but it's still bad so warn
+ mozilla::DebugOnly<nsresult> rv = createIncomingServer();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "couldn't lazily create the server\n");
+ }
+
+ NS_IF_ADDREF(*aIncomingServer = m_incomingServer);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::CreateServer() {
+ if (m_incomingServer) return NS_ERROR_ALREADY_INITIALIZED;
+ return createIncomingServer();
+}
+
+nsresult nsMsgAccount::createIncomingServer() {
+ // from here, load mail.account.myaccount.server
+ // Load the incoming server
+ //
+ // ex) mail.account.myaccount.server = "myserver"
+
+ nsresult rv = getPrefService();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the "server" pref
+ nsCString serverKey;
+ rv = m_prefs->GetCharPref("server", serverKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the server from the account manager
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = accountManager->GetIncomingServer(serverKey, getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString hostname;
+ rv = server->GetHostName(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hostname.IsEmpty()) {
+ NS_WARNING(
+ nsPrintfCString("Server had no hostname; key=%s", serverKey.get())
+ .get());
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ // store the server in this structure
+ m_incomingServer = server;
+ accountManager->NotifyServerLoaded(server);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::SetIncomingServer(nsIMsgIncomingServer* aIncomingServer) {
+ NS_ENSURE_ARG_POINTER(aIncomingServer);
+
+ nsCString key;
+ nsresult rv = aIncomingServer->GetKey(key);
+
+ if (NS_SUCCEEDED(rv)) {
+ rv = getPrefService();
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_prefs->SetCharPref("server", key);
+ }
+
+ m_incomingServer = aIncomingServer;
+
+ bool serverValid;
+ (void)aIncomingServer->GetValid(&serverValid);
+ // only notify server loaded if server is valid so
+ // account manager only gets told about finished accounts.
+ if (serverValid) {
+ // this is the point at which we can notify listeners about the
+ // creation of the root folder, which implies creation of the new server.
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = aIncomingServer->GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFolderListener> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mailSession->OnFolderAdded(nullptr, rootFolder);
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyFolderAdded(rootFolder);
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv)) accountManager->NotifyServerLoaded(aIncomingServer);
+
+ // Force built-in folders to be created and discovered. Then, notify
+ // listeners about them.
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = rootFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* msgFolder : subFolders) {
+ mailSession->OnFolderAdded(rootFolder, msgFolder);
+ notifier->NotifyFolderAdded(msgFolder);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::GetIdentities(nsTArray<RefPtr<nsIMsgIdentity>>& identities) {
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE);
+ identities.Clear();
+ identities.AppendElements(m_identities);
+ return NS_OK;
+}
+
+/*
+ * set up the m_identities array
+ * do not call this more than once or we'll leak.
+ */
+nsresult nsMsgAccount::createIdentities() {
+ NS_ENSURE_FALSE(m_identitiesValid, NS_ERROR_FAILURE);
+
+ nsresult rv;
+ m_identities.Clear();
+
+ nsCString identityKey;
+ rv = getPrefService();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_prefs->GetCharPref("identities", identityKey);
+ if (identityKey.IsEmpty()) {
+ // not an error if no identities, but strtok will be unhappy.
+ m_identitiesValid = true;
+ return NS_OK;
+ }
+ // get the server from the account manager
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char* newStr = identityKey.BeginWriting();
+ char* token = NS_strtok(",", &newStr);
+
+ // temporaries used inside the loop
+ nsCOMPtr<nsIMsgIdentity> identity;
+ nsAutoCString key;
+
+ // iterate through id1,id2, etc
+ while (token) {
+ key = token;
+ key.StripWhitespace();
+
+ // create the account
+ rv = accountManager->GetIdentity(key, getter_AddRefs(identity));
+ if (NS_SUCCEEDED(rv)) {
+ m_identities.AppendElement(identity);
+ }
+
+ // advance to next key, if any
+ token = NS_strtok(",", &newStr);
+ }
+
+ m_identitiesValid = true;
+ return rv;
+}
+
+/* attribute nsIMsgIdentity defaultIdentity; */
+NS_IMETHODIMP
+nsMsgAccount::GetDefaultIdentity(nsIMsgIdentity** aDefaultIdentity) {
+ NS_ENSURE_ARG_POINTER(aDefaultIdentity);
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_NOT_INITIALIZED);
+
+ // Default identity is the first in the list.
+ if (m_identities.IsEmpty()) {
+ *aDefaultIdentity = nullptr;
+ } else {
+ NS_IF_ADDREF(*aDefaultIdentity = m_identities[0]);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::SetDefaultIdentity(nsIMsgIdentity* aDefaultIdentity) {
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE);
+
+ auto position = m_identities.IndexOf(aDefaultIdentity);
+ if (position == m_identities.NoIndex) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Move it to the front of the list.
+ m_identities.RemoveElementAt(position);
+ m_identities.InsertElementAt(0, aDefaultIdentity);
+
+ nsresult rv = saveIdentitiesPref();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aDefaultIdentity, "account-default-identity-changed",
+ NS_ConvertUTF8toUTF16(m_accountKey).get());
+ }
+
+ return NS_OK;
+}
+
+/* void addIdentity (in nsIMsgIdentity identity); */
+NS_IMETHODIMP
+nsMsgAccount::AddIdentity(nsIMsgIdentity* identity) {
+ NS_ENSURE_ARG_POINTER(identity);
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE);
+
+ // hack hack - need to add this to the list of identities.
+ // for now just treat this as a Setxxx accessor
+ // when this is actually implemented, don't refcount the default identity
+ nsCString key;
+ nsresult rv = identity->GetKey(key);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCString identityList;
+ m_prefs->GetCharPref("identities", identityList);
+
+ nsAutoCString newIdentityList(identityList);
+
+ nsAutoCString testKey; // temporary to strip whitespace
+ bool foundIdentity = false; // if the input identity is found
+
+ if (!identityList.IsEmpty()) {
+ char* newStr = identityList.BeginWriting();
+ char* token = NS_strtok(",", &newStr);
+
+ // look for the identity key that we're adding
+ while (token) {
+ testKey = token;
+ testKey.StripWhitespace();
+
+ if (testKey.Equals(key)) foundIdentity = true;
+
+ token = NS_strtok(",", &newStr);
+ }
+ }
+
+ // if it didn't already exist, append it
+ if (!foundIdentity) {
+ if (newIdentityList.IsEmpty())
+ newIdentityList = key;
+ else {
+ newIdentityList.Append(',');
+ newIdentityList.Append(key);
+ }
+ }
+
+ m_prefs->SetCharPref("identities", newIdentityList);
+
+ // now add it to the in-memory list
+ m_identities.AppendElement(identity);
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(identity, "account-identity-added",
+ NS_ConvertUTF8toUTF16(key).get());
+ }
+ }
+
+ return NS_OK;
+}
+
+/* void removeIdentity (in nsIMsgIdentity identity); */
+NS_IMETHODIMP
+nsMsgAccount::RemoveIdentity(nsIMsgIdentity* aIdentity) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+ NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE);
+
+ // At least one identity must stay after the delete.
+ NS_ENSURE_TRUE(m_identities.Length() > 1, NS_ERROR_FAILURE);
+
+ nsCString key;
+ nsresult rv = aIdentity->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_identities.RemoveElement(aIdentity)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Notify before clearing the pref values, so we do not get the superfluous
+ // update notifications.
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aIdentity, "account-identity-removed",
+ NS_ConvertUTF8toUTF16(key).get());
+ }
+
+ // Clear out the actual pref values associated with the identity.
+ aIdentity->ClearAllValues();
+ return saveIdentitiesPref();
+}
+
+nsresult nsMsgAccount::saveIdentitiesPref() {
+ nsAutoCString newIdentityList;
+
+ // Iterate over the existing identities and build the pref value,
+ // a string of identity keys: id1, id2, idX...
+ nsCString key;
+ bool first = true;
+ for (auto identity : m_identities) {
+ identity->GetKey(key);
+
+ if (first) {
+ newIdentityList = key;
+ first = false;
+ } else {
+ newIdentityList.Append(',');
+ newIdentityList.Append(key);
+ }
+ }
+
+ // Save the pref.
+ m_prefs->SetCharPref("identities", newIdentityList);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccount::GetKey(nsACString& accountKey) {
+ accountKey = m_accountKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::SetKey(const nsACString& accountKey) {
+ m_accountKey = accountKey;
+ m_prefs = nullptr;
+ m_identitiesValid = false;
+ m_identities.Clear();
+ return createIdentities();
+}
+
+NS_IMETHODIMP
+nsMsgAccount::ToString(nsAString& aResult) {
+ nsAutoString val;
+ aResult.AssignLiteral("[nsIMsgAccount: ");
+ aResult.Append(NS_ConvertASCIItoUTF16(m_accountKey));
+ aResult.Append(']');
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccount::ClearAllValues() {
+ nsTArray<nsCString> prefNames;
+ nsresult rv = m_prefs->GetChildList("", prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ m_prefs->ClearUserPref(prefName.get());
+ }
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgAccount.h b/comm/mailnews/base/src/nsMsgAccount.h
new file mode 100644
index 0000000000..60b8005390
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgAccount.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nscore.h"
+#include "nsIMsgAccount.h"
+#include "nsIPrefBranch.h"
+#include "nsString.h"
+
+class nsMsgAccount : public nsIMsgAccount {
+ public:
+ nsMsgAccount();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGACCOUNT
+
+ private:
+ virtual ~nsMsgAccount();
+ nsCString m_accountKey;
+ nsCOMPtr<nsIPrefBranch> m_prefs;
+ nsCOMPtr<nsIMsgIncomingServer> m_incomingServer;
+
+ bool m_identitiesValid;
+ nsTArray<nsCOMPtr<nsIMsgIdentity>> m_identities;
+
+ nsresult getPrefService();
+ nsresult createIncomingServer();
+ nsresult createIdentities();
+ nsresult saveIdentitiesPref();
+
+ // Have we tried to get the server yet?
+ bool mTriedToGetServer;
+};
diff --git a/comm/mailnews/base/src/nsMsgAccountManager.cpp b/comm/mailnews/base/src/nsMsgAccountManager.cpp
new file mode 100644
index 0000000000..5352486cb4
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgAccountManager.cpp
@@ -0,0 +1,3546 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * The account manager service - manages all accounts, servers, and identities
+ */
+
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsIThread.h"
+#include "nscore.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Likely.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/RefCountType.h"
+#include "mozilla/RefPtr.h"
+#include "nsIComponentManager.h"
+#include "nsIServiceManager.h"
+#include "nsMsgAccountManager.h"
+#include "prmem.h"
+#include "prcmon.h"
+#include "prthread.h"
+#include "plstr.h"
+#include "nsString.h"
+#include "nsMemory.h"
+#include "nsUnicharUtils.h"
+#include "nscore.h"
+#include "prprf.h"
+#include "nsIMsgFolderCache.h"
+#include "nsMsgFolderCache.h"
+#include "nsMsgUtils.h"
+#include "nsMsgDBFolder.h"
+#include "nsIFile.h"
+#include "nsIURL.h"
+#include "nsNetCID.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsISmtpService.h"
+#include "nsIMsgBiffManager.h"
+#include "nsIMsgPurgeService.h"
+#include "nsIObserverService.h"
+#include "nsINoIncomingServer.h"
+#include "nsIMsgMailSession.h"
+#include "nsIDirectoryService.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsMailDirServiceDefs.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsIImapIncomingServer.h"
+#include "nsIImapUrl.h"
+#include "nsICategoryManager.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIMsgFilterService.h"
+#include "nsIMsgFilter.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgHdr.h"
+#include "nsILineInputStream.h"
+#include "nsThreadUtils.h"
+#include "nsNetUtil.h"
+#include "nsIStringBundle.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgFilterList.h"
+#include "nsDirectoryServiceUtils.h"
+#include "mozilla/Components.h"
+#include "mozilla/Services.h"
+#include "nsIFileStreams.h"
+#include "nsIOutputStream.h"
+#include "nsISafeOutputStream.h"
+#include "nsXULAppAPI.h"
+#include "nsICacheStorageService.h"
+#include "UrlListener.h"
+#include "nsIIDNService.h"
+
+#define PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS "mail.accountmanager.accounts"
+#define PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT \
+ "mail.accountmanager.defaultaccount"
+#define PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER \
+ "mail.accountmanager.localfoldersserver"
+#define PREF_MAIL_SERVER_PREFIX "mail.server."
+#define ACCOUNT_PREFIX "account"
+#define SERVER_PREFIX "server"
+#define ID_PREFIX "id"
+#define ABOUT_TO_GO_OFFLINE_TOPIC "network:offline-about-to-go-offline"
+#define ACCOUNT_DELIMITER ','
+#define APPEND_ACCOUNTS_VERSION_PREF_NAME "append_preconfig_accounts.version"
+#define MAILNEWS_ROOT_PREF "mailnews."
+#define PREF_MAIL_ACCOUNTMANAGER_APPEND_ACCOUNTS \
+ "mail.accountmanager.appendaccounts"
+
+#define NS_MSGACCOUNT_CID \
+ { \
+ 0x68b25510, 0xe641, 0x11d2, { \
+ 0xb7, 0xfc, 0x0, 0x80, 0x5f, 0x5, 0xff, 0xa5 \
+ } \
+ }
+static NS_DEFINE_CID(kMsgAccountCID, NS_MSGACCOUNT_CID);
+
+#define SEARCH_FOLDER_FLAG "searchFolderFlag"
+#define SEARCH_FOLDER_FLAG_LEN (sizeof(SEARCH_FOLDER_FLAG) - 1)
+
+const char* kSearchFolderUriProp = "searchFolderUri";
+
+bool nsMsgAccountManager::m_haveShutdown = false;
+bool nsMsgAccountManager::m_shutdownInProgress = false;
+
+NS_IMPL_ISUPPORTS(nsMsgAccountManager, nsIMsgAccountManager, nsIObserver,
+ nsISupportsWeakReference, nsIFolderListener)
+
+nsMsgAccountManager::nsMsgAccountManager()
+ : m_accountsLoaded(false),
+ m_emptyTrashInProgress(false),
+ m_cleanupInboxInProgress(false),
+ m_userAuthenticated(false),
+ m_loadingVirtualFolders(false),
+ m_virtualFoldersLoaded(false),
+ m_lastFindServerPort(0) {}
+
+nsMsgAccountManager::~nsMsgAccountManager() {
+ if (!m_haveShutdown) {
+ Shutdown();
+ // Don't remove from Observer service in Shutdown because Shutdown also gets
+ // called from xpcom shutdown observer. And we don't want to remove from
+ // the service in that case.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, "search-folders-changed");
+ observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ observerService->RemoveObserver(this, "quit-application-granted");
+ observerService->RemoveObserver(this, ABOUT_TO_GO_OFFLINE_TOPIC);
+ observerService->RemoveObserver(this, "sleep_notification");
+ }
+ }
+}
+
+nsresult nsMsgAccountManager::Init() {
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv;
+
+ m_prefs = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "search-folders-changed", true);
+ observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
+ observerService->AddObserver(this, "quit-application-granted", true);
+ observerService->AddObserver(this, ABOUT_TO_GO_OFFLINE_TOPIC, true);
+ observerService->AddObserver(this, "profile-before-change", true);
+ observerService->AddObserver(this, "sleep_notification", true);
+ }
+
+ // Make sure PSM gets initialized before any accounts use certificates.
+ net_EnsurePSMInit();
+
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::Shutdown() {
+ if (m_haveShutdown) // do not shutdown twice
+ return NS_OK;
+
+ nsresult rv;
+
+ SaveVirtualFolders();
+
+ if (m_dbService) {
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter(
+ m_virtualFolderListeners);
+ RefPtr<VirtualFolderChangeListener> listener;
+
+ while (iter.HasMore()) {
+ listener = iter.GetNext();
+ m_dbService->UnregisterPendingListener(listener);
+ }
+
+ m_dbService = nullptr;
+ }
+ m_virtualFolders.Clear();
+ if (m_msgFolderCache) WriteToFolderCache(m_msgFolderCache);
+ (void)ShutdownServers();
+ (void)UnloadAccounts();
+
+ // shutdown removes nsIIncomingServer listener from biff manager, so do it
+ // after accounts have been unloaded
+ nsCOMPtr<nsIMsgBiffManager> biffService =
+ do_GetService("@mozilla.org/messenger/biffManager;1", &rv);
+ if (NS_SUCCEEDED(rv) && biffService) biffService->Shutdown();
+
+ // shutdown removes nsIIncomingServer listener from purge service, so do it
+ // after accounts have been unloaded
+ nsCOMPtr<nsIMsgPurgeService> purgeService =
+ do_GetService("@mozilla.org/messenger/purgeService;1", &rv);
+ if (NS_SUCCEEDED(rv) && purgeService) purgeService->Shutdown();
+
+ if (m_msgFolderCache) {
+ // The DTOR is meant to do the flushing, but observed behaviour is
+ // that it doesn't always get called. So flush explicitly.
+ m_msgFolderCache->Flush();
+ m_msgFolderCache = nullptr;
+ }
+
+ m_haveShutdown = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetShutdownInProgress(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = m_shutdownInProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetUserNeedsToAuthenticate(bool* aRetval) {
+ NS_ENSURE_ARG_POINTER(aRetval);
+ if (!m_userAuthenticated)
+ return m_prefs->GetBoolPref("mail.password_protect_local_cache", aRetval);
+ *aRetval = !m_userAuthenticated;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SetUserNeedsToAuthenticate(bool aUserNeedsToAuthenticate) {
+ m_userAuthenticated = !aUserNeedsToAuthenticate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* someData) {
+ if (!strcmp(aTopic, "search-folders-changed")) {
+ nsCOMPtr<nsIMsgFolder> virtualFolder = do_QueryInterface(aSubject);
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ nsCString srchFolderUris;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, srchFolderUris);
+ AddVFListenersForVF(virtualFolder, srchFolderUris);
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ Shutdown();
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, "quit-application-granted")) {
+ // CleanupOnExit will set m_shutdownInProgress to true.
+ CleanupOnExit();
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, ABOUT_TO_GO_OFFLINE_TOPIC)) {
+ nsAutoString dataString(u"offline"_ns);
+ if (someData) {
+ nsAutoString someDataString(someData);
+ if (dataString.Equals(someDataString)) CloseCachedConnections();
+ }
+ return NS_OK;
+ }
+ if (!strcmp(aTopic, "sleep_notification")) return CloseCachedConnections();
+
+ if (!strcmp(aTopic, "profile-before-change")) {
+ Shutdown();
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetUniqueAccountKey(nsACString& aResult) {
+ int32_t lastKey = 0;
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefservice(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ prefservice->GetBranch("", getter_AddRefs(prefBranch));
+
+ rv = prefBranch->GetIntPref("mail.account.lastKey", &lastKey);
+ if (NS_FAILED(rv) || lastKey == 0) {
+ // If lastKey pref does not contain a valid value, loop over existing
+ // pref names mail.account.* .
+ nsCOMPtr<nsIPrefBranch> prefBranchAccount;
+ rv = prefservice->GetBranch("mail.account.",
+ getter_AddRefs(prefBranchAccount));
+ if (NS_SUCCEEDED(rv)) {
+ nsTArray<nsCString> prefList;
+ rv = prefBranchAccount->GetChildList("", prefList);
+ if (NS_SUCCEEDED(rv)) {
+ // Pref names are of the format accountX.
+ // Find the maximum value of 'X' used so far.
+ for (auto& prefName : prefList) {
+ if (StringBeginsWith(prefName, nsLiteralCString(ACCOUNT_PREFIX))) {
+ int32_t dotPos = prefName.FindChar('.');
+ if (dotPos != kNotFound) {
+ nsCString keyString(Substring(prefName, strlen(ACCOUNT_PREFIX),
+ dotPos - strlen(ACCOUNT_PREFIX)));
+ int32_t thisKey = keyString.ToInteger(&rv);
+ if (NS_SUCCEEDED(rv)) lastKey = std::max(lastKey, thisKey);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Use next available key and store the value in the pref.
+ aResult.Assign(ACCOUNT_PREFIX);
+ aResult.AppendInt(++lastKey);
+ rv = prefBranch->SetIntPref("mail.account.lastKey", lastKey);
+ } else {
+ // If pref service is not working, try to find a free accountX key
+ // by checking which keys exist.
+ int32_t i = 1;
+ nsCOMPtr<nsIMsgAccount> account;
+
+ do {
+ aResult = ACCOUNT_PREFIX;
+ aResult.AppendInt(i++);
+ GetAccount(aResult, getter_AddRefs(account));
+ } while (account);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetUniqueServerKey(nsACString& aResult) {
+ nsAutoCString prefResult;
+ bool usePrefsScan = true;
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefService(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) usePrefsScan = false;
+
+ // Loop over existing pref names mail.server.server(lastKey).type
+ nsCOMPtr<nsIPrefBranch> prefBranchServer;
+ if (prefService) {
+ rv = prefService->GetBranch(PREF_MAIL_SERVER_PREFIX,
+ getter_AddRefs(prefBranchServer));
+ if (NS_FAILED(rv)) usePrefsScan = false;
+ }
+
+ if (usePrefsScan) {
+ nsAutoCString type;
+ nsAutoCString typeKey;
+ for (uint32_t lastKey = 1;; lastKey++) {
+ aResult.AssignLiteral(SERVER_PREFIX);
+ aResult.AppendInt(lastKey);
+ typeKey.Assign(aResult);
+ typeKey.AppendLiteral(".type");
+ prefBranchServer->GetCharPref(typeKey.get(), type);
+ if (type.IsEmpty()) // a server slot with no type is considered empty
+ return NS_OK;
+ }
+ } else {
+ // If pref service fails, try to find a free serverX key
+ // by checking which keys exist.
+ nsAutoCString internalResult;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ uint32_t i = 1;
+ do {
+ aResult.AssignLiteral(SERVER_PREFIX);
+ aResult.AppendInt(i++);
+ m_incomingServers.Get(aResult, getter_AddRefs(server));
+ } while (server);
+ return NS_OK;
+ }
+}
+
+nsresult nsMsgAccountManager::CreateIdentity(nsIMsgIdentity** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsresult rv;
+ nsAutoCString key;
+ nsCOMPtr<nsIMsgIdentity> identity;
+ int32_t i = 1;
+ do {
+ key.AssignLiteral(ID_PREFIX);
+ key.AppendInt(i++);
+ m_identities.Get(key, getter_AddRefs(identity));
+ } while (identity);
+
+ rv = createKeyedIdentity(key, _retval);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetIdentity(const nsACString& key,
+ nsIMsgIdentity** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsresult rv = NS_OK;
+ *_retval = nullptr;
+
+ if (!key.IsEmpty()) {
+ nsCOMPtr<nsIMsgIdentity> identity;
+ m_identities.Get(key, getter_AddRefs(identity));
+ if (identity)
+ identity.forget(_retval);
+ else // identity doesn't exist. create it.
+ rv = createKeyedIdentity(key, _retval);
+ }
+
+ return rv;
+}
+
+/*
+ * the shared identity-creation code
+ * create an identity and add it to the accountmanager's list.
+ */
+nsresult nsMsgAccountManager::createKeyedIdentity(const nsACString& key,
+ nsIMsgIdentity** aIdentity) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIdentity> identity =
+ do_CreateInstance("@mozilla.org/messenger/identity;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ identity->SetKey(key);
+ m_identities.InsertOrUpdate(key, identity);
+ identity.forget(aIdentity);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CreateIncomingServer(const nsACString& username,
+ const nsACString& hostname,
+ const nsACString& type,
+ nsIMsgIncomingServer** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString key;
+ GetUniqueServerKey(key);
+ rv = createKeyedServer(key, username, hostname, type, _retval);
+ if (*_retval) {
+ nsCString defaultStore;
+ m_prefs->GetCharPref("mail.serverDefaultStoreContractID", defaultStore);
+ (*_retval)->SetCharValue("storeContractID", defaultStore);
+
+ // From when we first create the account until we have created some folders,
+ // we can change the store type.
+ (*_retval)->SetBoolValue("canChangeStoreType", true);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetIncomingServer(const nsACString& key,
+ nsIMsgIncomingServer** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsresult rv;
+
+ if (m_incomingServers.Get(key, _retval)) return NS_OK;
+
+ // server doesn't exist, so create it
+ // this is really horrible because we are doing our own prefname munging
+ // instead of leaving it up to the incoming server.
+ // this should be fixed somehow so that we can create the incoming server
+ // and then read from the incoming server's attributes
+
+ // in order to create the right kind of server, we have to look
+ // at the pref for this server to get the username, hostname, and type
+ nsAutoCString serverPrefPrefix(PREF_MAIL_SERVER_PREFIX);
+ serverPrefPrefix.Append(key);
+
+ nsCString serverType;
+ nsAutoCString serverPref(serverPrefPrefix);
+ serverPref.AppendLiteral(".type");
+ rv = m_prefs->GetCharPref(serverPref.get(), serverType);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_INITIALIZED);
+
+ //
+ // .userName
+ serverPref = serverPrefPrefix;
+ serverPref.AppendLiteral(".userName");
+ nsCString username;
+ rv = m_prefs->GetCharPref(serverPref.get(), username);
+
+ // .hostname
+ serverPref = serverPrefPrefix;
+ serverPref.AppendLiteral(".hostname");
+ nsCString hostname;
+ rv = m_prefs->GetCharPref(serverPref.get(), hostname);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_INITIALIZED);
+
+ return createKeyedServer(key, username, hostname, serverType, _retval);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::RemoveIncomingServer(nsIMsgIncomingServer* aServer,
+ bool aRemoveFiles) {
+ NS_ENSURE_ARG_POINTER(aServer);
+
+ nsCString serverKey;
+ nsresult rv = aServer->GetKey(serverKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // close cached connections and forget session password
+ LogoutOfServer(aServer);
+
+ // invalidate the FindServer() cache if we are removing the cached server
+ if (m_lastFindServerResult == aServer)
+ SetLastServerFound(nullptr, EmptyCString(), EmptyCString(), 0,
+ EmptyCString());
+
+ m_incomingServers.Remove(serverKey);
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = aServer->GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rv = rootFolder->GetDescendants(allDescendants);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier =
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1");
+ nsCOMPtr<nsIFolderListener> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+
+ for (auto folder : allDescendants) {
+ folder->ForceDBClosed();
+ if (notifier) notifier->NotifyFolderDeleted(folder);
+ if (mailSession) {
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ folder->GetParent(getter_AddRefs(parentFolder));
+ mailSession->OnFolderRemoved(parentFolder, folder);
+ }
+ }
+ if (notifier) notifier->NotifyFolderDeleted(rootFolder);
+ if (mailSession) mailSession->OnFolderRemoved(nullptr, rootFolder);
+
+ removeListenersFromFolder(rootFolder);
+ NotifyServerUnloaded(aServer);
+ if (aRemoveFiles) {
+ rv = aServer->RemoveFiles();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aServer, "message-server-removed",
+ NS_ConvertUTF8toUTF16(serverKey).get());
+ }
+
+ // now clear out the server once and for all.
+ // watch out! could be scary
+ aServer->ClearAllValues();
+ rootFolder->Shutdown(true);
+ return rv;
+}
+
+/*
+ * create a server when you know the key and the type
+ */
+nsresult nsMsgAccountManager::createKeyedServer(
+ const nsACString& key, const nsACString& username,
+ const nsACString& hostname, const nsACString& type,
+ nsIMsgIncomingServer** aServer) {
+ nsresult rv;
+ *aServer = nullptr;
+
+ // construct the contractid
+ nsAutoCString serverContractID("@mozilla.org/messenger/server;1?type=");
+ serverContractID += type;
+
+ // finally, create the server
+ // (This will fail if type is from an extension that has been removed)
+ nsCOMPtr<nsIMsgIncomingServer> server =
+ do_CreateInstance(serverContractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_AVAILABLE);
+
+ int32_t port;
+ nsCOMPtr<nsIMsgIncomingServer> existingServer;
+ server->SetKey(key);
+ server->SetType(type);
+ server->SetUsername(username);
+ server->SetHostName(hostname);
+ server->GetPort(&port);
+ FindServer(username, hostname, type, port, getter_AddRefs(existingServer));
+ // don't allow duplicate servers.
+ if (existingServer) return NS_ERROR_FAILURE;
+
+ m_incomingServers.InsertOrUpdate(key, server);
+
+ // now add all listeners that are supposed to be
+ // waiting on root folders
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>>::ForwardIterator iter(
+ mFolderListeners);
+ while (iter.HasMore()) {
+ rootFolder->AddFolderListener(iter.GetNext());
+ }
+
+ server.forget(aServer);
+ return NS_OK;
+}
+
+void nsMsgAccountManager::removeListenersFromFolder(nsIMsgFolder* aFolder) {
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>>::ForwardIterator iter(
+ mFolderListeners);
+ while (iter.HasMore()) {
+ aFolder->RemoveFolderListener(iter.GetNext());
+ }
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::RemoveAccount(nsIMsgAccount* aAccount,
+ bool aRemoveFiles = false) {
+ NS_ENSURE_ARG_POINTER(aAccount);
+ // Hold account in scope while we tidy up potentially-shared identities.
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_accounts.RemoveElement(aAccount)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ rv = OutputAccountsPref();
+ // If we couldn't write out the pref, restore the account.
+ if (NS_FAILED(rv)) {
+ m_accounts.AppendElement(aAccount);
+ return rv;
+ }
+
+ // If it's the default, choose a new default account.
+ if (m_defaultAccount == aAccount) AutosetDefaultAccount();
+
+ // XXX - need to figure out if this is the last time this server is
+ // being used, and only send notification then.
+ // (and only remove from hashtable then too!)
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = aAccount->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) RemoveIncomingServer(server, aRemoveFiles);
+
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ rv = aAccount->GetIdentities(identities);
+ if (NS_SUCCEEDED(rv)) {
+ for (auto identity : identities) {
+ bool identityStillUsed = false;
+ // for each identity, see if any remaining account still uses it,
+ // and if not, clear it.
+ // Note that we are also searching here accounts with missing servers from
+ // unloaded extension types.
+ for (auto account : m_accounts) {
+ nsTArray<RefPtr<nsIMsgIdentity>> existingIdentities;
+ account->GetIdentities(existingIdentities);
+ auto pos = existingIdentities.IndexOf(identity);
+ if (pos != existingIdentities.NoIndex) {
+ identityStillUsed = true;
+ break;
+ }
+ }
+ // clear out all identity information if no other account uses it.
+ if (!identityStillUsed) identity->ClearAllValues();
+ }
+ }
+
+ nsCString accountKey;
+ aAccount->GetKey(accountKey);
+
+ // It is not a critical problem if this fails as the account was already
+ // removed from the list of accounts so should not ever be referenced.
+ // Just print it out for debugging.
+ rv = aAccount->ClearAllValues();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "removing of account prefs failed");
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(nullptr, "message-account-removed",
+ NS_ConvertUTF8toUTF16(accountKey).get());
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::OutputAccountsPref() {
+ nsCString accountKey;
+ mAccountKeyList.Truncate();
+
+ for (uint32_t index = 0; index < m_accounts.Length(); index++) {
+ m_accounts[index]->GetKey(accountKey);
+ if (index) mAccountKeyList.Append(ACCOUNT_DELIMITER);
+ mAccountKeyList.Append(accountKey);
+ }
+ return m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS,
+ mAccountKeyList);
+}
+
+/**
+ * Get the default account. If no default account, return null.
+ */
+NS_IMETHODIMP
+nsMsgAccountManager::GetDefaultAccount(nsIMsgAccount** aDefaultAccount) {
+ NS_ENSURE_ARG_POINTER(aDefaultAccount);
+
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_defaultAccount) {
+ nsCString defaultKey;
+ rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT,
+ defaultKey);
+ if (NS_SUCCEEDED(rv)) {
+ rv = GetAccount(defaultKey, getter_AddRefs(m_defaultAccount));
+ if (NS_SUCCEEDED(rv) && m_defaultAccount) {
+ bool canBeDefault = false;
+ rv = CheckDefaultAccount(m_defaultAccount, canBeDefault);
+ if (NS_FAILED(rv) || !canBeDefault) m_defaultAccount = nullptr;
+ }
+ }
+ }
+
+ NS_IF_ADDREF(*aDefaultAccount = m_defaultAccount);
+ return NS_OK;
+}
+
+/**
+ * Check if the given account can be default.
+ */
+nsresult nsMsgAccountManager::CheckDefaultAccount(nsIMsgAccount* aAccount,
+ bool& aCanBeDefault) {
+ aCanBeDefault = false;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // Server could be null if created by an unloaded extension.
+ nsresult rv = aAccount->GetIncomingServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (server) {
+ // Check if server can be default.
+ rv = server->GetCanBeDefaultServer(&aCanBeDefault);
+ }
+ return rv;
+}
+
+/**
+ * Pick the first account that can be default and make it the default.
+ */
+nsresult nsMsgAccountManager::AutosetDefaultAccount() {
+ for (nsIMsgAccount* account : m_accounts) {
+ bool canBeDefault = false;
+ nsresult rv = CheckDefaultAccount(account, canBeDefault);
+ if (NS_SUCCEEDED(rv) && canBeDefault) {
+ return SetDefaultAccount(account);
+ }
+ }
+
+ // No accounts can be the default. Clear it.
+ if (m_defaultAccount) {
+ nsCOMPtr<nsIMsgAccount> oldAccount = m_defaultAccount;
+ m_defaultAccount = nullptr;
+ (void)setDefaultAccountPref(nullptr);
+ (void)notifyDefaultServerChange(oldAccount, nullptr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SetDefaultAccount(nsIMsgAccount* aDefaultAccount) {
+ if (!aDefaultAccount) return NS_ERROR_INVALID_ARG;
+
+ if (m_defaultAccount != aDefaultAccount) {
+ bool canBeDefault = false;
+ nsresult rv = CheckDefaultAccount(aDefaultAccount, canBeDefault);
+ if (NS_FAILED(rv) || !canBeDefault) {
+ // Report failure if we were explicitly asked to use an unusable server.
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsCOMPtr<nsIMsgAccount> oldAccount = m_defaultAccount;
+ m_defaultAccount = aDefaultAccount;
+ (void)setDefaultAccountPref(aDefaultAccount);
+ (void)notifyDefaultServerChange(oldAccount, aDefaultAccount);
+ }
+ return NS_OK;
+}
+
+// fire notifications
+nsresult nsMsgAccountManager::notifyDefaultServerChange(
+ nsIMsgAccount* aOldAccount, nsIMsgAccount* aNewAccount) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+
+ // first tell old server it's no longer the default
+ if (aOldAccount) {
+ rv = aOldAccount->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) {
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder)
+ rootFolder->NotifyBoolPropertyChanged(kDefaultServer, true, false);
+ }
+ }
+
+ // now tell new server it is.
+ if (aNewAccount) {
+ rv = aNewAccount->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) {
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder)
+ rootFolder->NotifyBoolPropertyChanged(kDefaultServer, false, true);
+ }
+ }
+
+ // only notify if the user goes and changes default account
+ if (aOldAccount && aNewAccount) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ if (observerService)
+ observerService->NotifyObservers(nullptr, "mailDefaultAccountChanged",
+ nullptr);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::setDefaultAccountPref(
+ nsIMsgAccount* aDefaultAccount) {
+ nsresult rv;
+
+ if (aDefaultAccount) {
+ nsCString key;
+ rv = aDefaultAccount->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT, key);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else
+ m_prefs->ClearUserPref(PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT);
+
+ return NS_OK;
+}
+
+void nsMsgAccountManager::LogoutOfServer(nsIMsgIncomingServer* aServer) {
+ if (!aServer) return;
+ mozilla::DebugOnly<nsresult> rv = aServer->Shutdown();
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Shutdown of server failed");
+ rv = aServer->ForgetSessionPassword(false);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "failed to remove the password associated with server");
+}
+
+NS_IMETHODIMP nsMsgAccountManager::GetFolderCache(
+ nsIMsgFolderCache** aFolderCache) {
+ NS_ENSURE_ARG_POINTER(aFolderCache);
+
+ if (m_msgFolderCache) {
+ NS_IF_ADDREF(*aFolderCache = m_msgFolderCache);
+ return NS_OK;
+ }
+
+ // Create the foldercache.
+ nsCOMPtr<nsIFile> cacheFile;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_MESSENGER_FOLDER_CACHE_50_FILE,
+ getter_AddRefs(cacheFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFile> legacyFile;
+ rv = NS_GetSpecialDirectory(NS_APP_MESSENGER_LEGACY_FOLDER_CACHE_50_FILE,
+ getter_AddRefs(legacyFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_msgFolderCache = new nsMsgFolderCache();
+ m_msgFolderCache->Init(cacheFile, legacyFile);
+ NS_IF_ADDREF(*aFolderCache = m_msgFolderCache);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetAccounts(nsTArray<RefPtr<nsIMsgAccount>>& accounts) {
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ accounts.Clear();
+ accounts.SetCapacity(m_accounts.Length());
+ for (auto existingAccount : m_accounts) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ existingAccount->GetIncomingServer(getter_AddRefs(server));
+ if (!server) continue;
+ if (server) {
+ bool hidden = false;
+ server->GetHidden(&hidden);
+ if (hidden) continue;
+ }
+ accounts.AppendElement(existingAccount);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetAllIdentities(
+ nsTArray<RefPtr<nsIMsgIdentity>>& result) {
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ result.Clear();
+
+ for (auto account : m_accounts) {
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ rv = account->GetIdentities(identities);
+ if (NS_FAILED(rv)) continue;
+
+ for (auto identity : identities) {
+ // Have we already got this identity?
+ nsAutoCString key;
+ rv = identity->GetKey(key);
+ if (NS_FAILED(rv)) continue;
+
+ bool found = false;
+ for (auto thisIdentity : result) {
+ nsAutoCString thisKey;
+ rv = thisIdentity->GetKey(thisKey);
+ if (NS_FAILED(rv)) continue;
+
+ if (key == thisKey) {
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) result.AppendElement(identity);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetAllServers(
+ nsTArray<RefPtr<nsIMsgIncomingServer>>& servers) {
+ servers.Clear();
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (!server) continue;
+
+ bool hidden = false;
+ server->GetHidden(&hidden);
+ if (hidden) continue;
+
+ nsCString type;
+ if (NS_FAILED(server->GetType(type))) {
+ NS_WARNING("server->GetType() failed");
+ continue;
+ }
+
+ if (!type.EqualsLiteral("im")) {
+ servers.AppendElement(server);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::LoadAccounts() {
+ nsresult rv;
+
+ // for now safeguard multiple calls to this function
+ if (m_accountsLoaded) return NS_OK;
+
+ // If we have code trying to do things after we've unloaded accounts,
+ // ignore it.
+ if (m_shutdownInProgress || m_haveShutdown) return NS_ERROR_FAILURE;
+
+ // Make sure correct modules are loaded before creating any server.
+ nsCOMPtr<nsIObserver> moduleLoader;
+ moduleLoader =
+ do_GetService("@mozilla.org/messenger/imap-module-loader;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+
+ if (NS_SUCCEEDED(rv))
+ mailSession->AddFolderListener(
+ this, nsIFolderListener::added | nsIFolderListener::removed |
+ nsIFolderListener::intPropertyChanged);
+
+ // Ensure biff service has started
+ nsCOMPtr<nsIMsgBiffManager> biffService =
+ do_GetService("@mozilla.org/messenger/biffManager;1", &rv);
+
+ if (NS_SUCCEEDED(rv)) biffService->Init();
+
+ // Ensure purge service has started
+ nsCOMPtr<nsIMsgPurgeService> purgeService =
+ do_GetService("@mozilla.org/messenger/purgeService;1", &rv);
+
+ if (NS_SUCCEEDED(rv)) purgeService->Init();
+
+ nsCOMPtr<nsIPrefService> prefservice(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // mail.accountmanager.accounts is the main entry point for all accounts
+ nsCString accountList;
+ rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS, accountList);
+
+ /**
+ * Check to see if we need to add pre-configured accounts.
+ * Following prefs are important to note in understanding the procedure here.
+ *
+ * 1. pref("mailnews.append_preconfig_accounts.version", version number);
+ * This pref registers the current version in the user prefs file. A default
+ * value is stored in mailnews.js file. If a given vendor needs to add more
+ * preconfigured accounts, the default version number can be increased.
+ * Comparing version number from user's prefs file and the default one from
+ * mailnews.js, we can add new accounts and any other version level changes
+ * that need to be done.
+ *
+ * 2. pref("mail.accountmanager.appendaccounts", <comma sep. account list>);
+ * This pref contains the list of pre-configured accounts that ISP/Vendor
+ * wants to add to the existing accounts list.
+ */
+ nsCOMPtr<nsIPrefBranch> defaultsPrefBranch;
+ rv = prefservice->GetDefaultBranch(MAILNEWS_ROOT_PREF,
+ getter_AddRefs(defaultsPrefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ rv = prefservice->GetBranch(MAILNEWS_ROOT_PREF, getter_AddRefs(prefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t appendAccountsCurrentVersion = 0;
+ int32_t appendAccountsDefaultVersion = 0;
+ rv = prefBranch->GetIntPref(APPEND_ACCOUNTS_VERSION_PREF_NAME,
+ &appendAccountsCurrentVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = defaultsPrefBranch->GetIntPref(APPEND_ACCOUNTS_VERSION_PREF_NAME,
+ &appendAccountsDefaultVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Update the account list if needed
+ if ((appendAccountsCurrentVersion <= appendAccountsDefaultVersion)) {
+ // Get a list of pre-configured accounts
+ nsCString appendAccountList;
+ rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_APPEND_ACCOUNTS,
+ appendAccountList);
+ appendAccountList.StripWhitespace();
+
+ // If there are pre-configured accounts, we need to add them to the
+ // existing list.
+ if (!appendAccountList.IsEmpty()) {
+ if (!accountList.IsEmpty()) {
+ // Tokenize the data and add each account
+ // in the user's current mailnews account list
+ nsTArray<nsCString> accountsArray;
+ ParseString(accountList, ACCOUNT_DELIMITER, accountsArray);
+ uint32_t i = accountsArray.Length();
+
+ // Append each account in the pre-configured account list
+ ParseString(appendAccountList, ACCOUNT_DELIMITER, accountsArray);
+
+ // Now add each account that does not already appear in the list
+ for (; i < accountsArray.Length(); i++) {
+ if (accountsArray.IndexOf(accountsArray[i]) == i) {
+ accountList.Append(ACCOUNT_DELIMITER);
+ accountList.Append(accountsArray[i]);
+ }
+ }
+ } else {
+ accountList = appendAccountList;
+ }
+ // Increase the version number so that updates will happen as and when
+ // needed
+ rv = prefBranch->SetIntPref(APPEND_ACCOUNTS_VERSION_PREF_NAME,
+ appendAccountsCurrentVersion + 1);
+ }
+ }
+
+ // It is ok to return null accounts like when we create new profile.
+ m_accountsLoaded = true;
+ m_haveShutdown = false;
+
+ if (accountList.IsEmpty()) return NS_OK;
+
+ /* parse accountList and run loadAccount on each string, comma-separated */
+ nsCOMPtr<nsIMsgAccount> account;
+ // Tokenize the data and add each account
+ // in the user's current mailnews account list
+ nsTArray<nsCString> accountsArray;
+ ParseString(accountList, ACCOUNT_DELIMITER, accountsArray);
+
+ // These are the duplicate accounts we found. We keep track of these
+ // because if any other server defers to one of these accounts, we need
+ // to defer to the correct account.
+ nsCOMArray<nsIMsgAccount> dupAccounts;
+
+ // Now add each account that does not already appear in the list
+ for (uint32_t i = 0; i < accountsArray.Length(); i++) {
+ // if we've already seen this exact account, advance to the next account.
+ // After the loop, we'll notice that we don't have as many actual accounts
+ // as there were accounts in the pref, and rewrite the pref.
+ if (accountsArray.IndexOf(accountsArray[i]) != i) continue;
+
+ // get the "server" pref to see if we already have an account with this
+ // server. If we do, we ignore this account.
+ nsAutoCString serverKeyPref("mail.account.");
+ serverKeyPref += accountsArray[i];
+
+ nsCOMPtr<nsIPrefBranch> accountPrefBranch;
+ rv = prefservice->GetBranch(serverKeyPref.get(),
+ getter_AddRefs(accountPrefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ serverKeyPref += ".server";
+ nsCString serverKey;
+ rv = m_prefs->GetCharPref(serverKeyPref.get(), serverKey);
+ if (NS_FAILED(rv)) continue;
+
+ nsCOMPtr<nsIMsgAccount> serverAccount;
+ findAccountByServerKey(serverKey, getter_AddRefs(serverAccount));
+ // If we have an existing account with the same server, ignore this account
+ if (serverAccount) continue;
+
+ if (NS_FAILED(createKeyedAccount(accountsArray[i], true,
+ getter_AddRefs(account))) ||
+ !account) {
+ NS_WARNING("unexpected entry in account list; prefs corrupt?");
+ continue;
+ }
+
+ // See nsIMsgAccount.idl for a description of the secondsToLeaveUnavailable
+ // and timeFoundUnavailable preferences
+ nsAutoCString toLeavePref(PREF_MAIL_SERVER_PREFIX);
+ toLeavePref.Append(serverKey);
+ nsAutoCString unavailablePref(
+ toLeavePref); // this is the server-specific prefix
+ unavailablePref.AppendLiteral(".timeFoundUnavailable");
+ toLeavePref.AppendLiteral(".secondsToLeaveUnavailable");
+ int32_t secondsToLeave = 0;
+ int32_t timeUnavailable = 0;
+
+ m_prefs->GetIntPref(toLeavePref.get(), &secondsToLeave);
+
+ // force load of accounts (need to find a better way to do this)
+ nsTArray<RefPtr<nsIMsgIdentity>> unused;
+ account->GetIdentities(unused);
+
+ rv = account->CreateServer();
+ bool deleteAccount = NS_FAILED(rv);
+
+ if (secondsToLeave) { // we need to process timeUnavailable
+ if (NS_SUCCEEDED(rv)) // clear the time if server is available
+ {
+ m_prefs->ClearUserPref(unavailablePref.get());
+ }
+ // NS_ERROR_NOT_AVAILABLE signifies a server that could not be
+ // instantiated, presumably because of an invalid type.
+ else if (rv == NS_ERROR_NOT_AVAILABLE) {
+ m_prefs->GetIntPref(unavailablePref.get(), &timeUnavailable);
+ if (!timeUnavailable) { // we need to set it, this must be the first
+ // time unavailable
+ uint32_t nowSeconds;
+ PRTime2Seconds(PR_Now(), &nowSeconds);
+ m_prefs->SetIntPref(unavailablePref.get(), nowSeconds);
+ deleteAccount = false;
+ }
+ }
+ }
+
+ // Our server is still unavailable. Have we timed out yet?
+ if (rv == NS_ERROR_NOT_AVAILABLE && timeUnavailable != 0) {
+ uint32_t nowSeconds;
+ PRTime2Seconds(PR_Now(), &nowSeconds);
+ if ((int32_t)nowSeconds < timeUnavailable + secondsToLeave)
+ deleteAccount = false;
+ }
+
+ if (deleteAccount) {
+ dupAccounts.AppendObject(account);
+ m_accounts.RemoveElement(account);
+ }
+ }
+
+ // Check if we removed one or more of the accounts in the pref string.
+ // If so, rewrite the pref string.
+ if (accountsArray.Length() != m_accounts.Length()) OutputAccountsPref();
+
+ int32_t cnt = dupAccounts.Count();
+ nsCOMPtr<nsIMsgAccount> dupAccount;
+
+ // Go through the accounts seeing if any existing server is deferred to
+ // an account we removed. If so, fix the deferral. Then clean up the prefs
+ // for the removed account.
+ for (int32_t i = 0; i < cnt; i++) {
+ dupAccount = dupAccounts[i];
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ /*
+ * This loop gets run for every incoming server, and is passed a
+ * duplicate account. It checks that the server is not deferred to the
+ * duplicate account. If it is, then it looks up the information for the
+ * duplicate account's server (username, hostName, type), and finds an
+ * account with a server with the same username, hostname, and type, and
+ * if it finds one, defers to that account instead. Generally, this will
+ * be a Local Folders account, since 2.0 has a bug where duplicate Local
+ * Folders accounts are created.
+ */
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ nsCString type;
+ server->GetType(type);
+ if (type.EqualsLiteral("pop3")) {
+ nsCString deferredToAccount;
+ // Get the pref directly, because the GetDeferredToAccount accessor
+ // attempts to fix broken deferrals, but we know more about what the
+ // deferred to account was.
+ server->GetCharValue("deferred_to_account", deferredToAccount);
+ if (!deferredToAccount.IsEmpty()) {
+ nsCString dupAccountKey;
+ dupAccount->GetKey(dupAccountKey);
+ if (deferredToAccount.Equals(dupAccountKey)) {
+ nsresult rv;
+ nsCString accountPref("mail.account.");
+ nsCString dupAccountServerKey;
+ accountPref.Append(dupAccountKey);
+ accountPref.AppendLiteral(".server");
+ nsCOMPtr<nsIPrefService> prefservice(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ rv =
+ prefBranch->GetCharPref(accountPref.get(), dupAccountServerKey);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCOMPtr<nsIPrefBranch> serverPrefBranch;
+ nsCString serverKeyPref(PREF_MAIL_SERVER_PREFIX);
+ serverKeyPref.Append(dupAccountServerKey);
+ serverKeyPref.Append('.');
+ rv = prefservice->GetBranch(serverKeyPref.get(),
+ getter_AddRefs(serverPrefBranch));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCString userName;
+ nsCString hostName;
+ nsCString type;
+ serverPrefBranch->GetCharPref("userName", userName);
+ serverPrefBranch->GetCharPref("hostname", hostName);
+ serverPrefBranch->GetCharPref("type", type);
+ // Find a server with the same info.
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ accountManager->FindServer(userName, hostName, type, 0,
+ getter_AddRefs(server));
+ if (server) {
+ nsCOMPtr<nsIMsgAccount> replacement;
+ accountManager->FindAccountForServer(server,
+ getter_AddRefs(replacement));
+ if (replacement) {
+ nsCString accountKey;
+ replacement->GetKey(accountKey);
+ if (!accountKey.IsEmpty())
+ server->SetCharValue("deferred_to_account", accountKey);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ nsAutoCString accountKeyPref("mail.account.");
+ nsCString dupAccountKey;
+ dupAccount->GetKey(dupAccountKey);
+ if (dupAccountKey.IsEmpty()) continue;
+ accountKeyPref.Append(dupAccountKey);
+ accountKeyPref.Append('.');
+
+ nsCOMPtr<nsIPrefBranch> accountPrefBranch;
+ rv = prefservice->GetBranch(accountKeyPref.get(),
+ getter_AddRefs(accountPrefBranch));
+ if (accountPrefBranch) {
+ nsTArray<nsCString> prefNames;
+ nsresult rv = accountPrefBranch->GetChildList("", prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ accountPrefBranch->ClearUserPref(prefName.get());
+ }
+ }
+ }
+
+ // Make sure we have an account that points at the local folders server
+ nsCString localFoldersServerKey;
+ rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER,
+ localFoldersServerKey);
+
+ if (!localFoldersServerKey.IsEmpty()) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetIncomingServer(localFoldersServerKey, getter_AddRefs(server));
+ if (server) {
+ nsCOMPtr<nsIMsgAccount> localFoldersAccount;
+ findAccountByServerKey(localFoldersServerKey,
+ getter_AddRefs(localFoldersAccount));
+ // If we don't have an existing account pointing at the local folders
+ // server, we're going to add one.
+ if (!localFoldersAccount) {
+ nsCOMPtr<nsIMsgAccount> account;
+ (void)CreateAccount(getter_AddRefs(account));
+ if (account) account->SetIncomingServer(server);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::ReactivateAccounts() {
+ for (nsIMsgAccount* account : m_accounts) {
+ // This will error out if the account already has its server, or
+ // if this isn't the account that the extension is trying to reactivate.
+ if (NS_SUCCEEDED(account->CreateServer())) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ account->GetIncomingServer(getter_AddRefs(server));
+ // This triggers all of the notifications required by the UI.
+ account->SetIncomingServer(server);
+ }
+ }
+ return NS_OK;
+}
+
+// this routine goes through all the identities and makes sure
+// that the special folders for each identity have the
+// correct special folder flags set, e.g, the Sent folder has
+// the sent flag set.
+//
+// it also goes through all the spam settings for each account
+// and makes sure the folder flags are set there, too
+NS_IMETHODIMP
+nsMsgAccountManager::SetSpecialFolders() {
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ GetAllIdentities(identities);
+
+ for (auto identity : identities) {
+ nsresult rv;
+ nsCString folderUri;
+ nsCOMPtr<nsIMsgFolder> folder;
+
+ identity->GetFccFolder(folderUri);
+ if (!folderUri.IsEmpty() &&
+ NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = folder->GetParent(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ rv = folder->SetFlag(nsMsgFolderFlags::SentMail);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ identity->GetDraftFolder(folderUri);
+ if (!folderUri.IsEmpty() &&
+ NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = folder->GetParent(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ rv = folder->SetFlag(nsMsgFolderFlags::Drafts);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ identity->GetArchiveFolder(folderUri);
+ if (!folderUri.IsEmpty() &&
+ NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = folder->GetParent(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ bool archiveEnabled;
+ identity->GetArchiveEnabled(&archiveEnabled);
+ if (archiveEnabled)
+ rv = folder->SetFlag(nsMsgFolderFlags::Archive);
+ else
+ rv = folder->ClearFlag(nsMsgFolderFlags::Archive);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ identity->GetStationeryFolder(folderUri);
+ if (!folderUri.IsEmpty() &&
+ NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = folder->GetParent(getter_AddRefs(parent));
+ if (NS_SUCCEEDED(rv) && parent) {
+ folder->SetFlag(nsMsgFolderFlags::Templates);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ }
+
+ // XXX todo
+ // get all servers
+ // get all spam settings for each server
+ // set the JUNK folder flag on the spam folders, right?
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::UnloadAccounts() {
+ // release the default account
+ m_defaultAccount = nullptr;
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (!server) continue;
+ nsresult rv;
+ NotifyServerUnloaded(server);
+
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv)) {
+ removeListenersFromFolder(rootFolder);
+
+ rootFolder->Shutdown(true);
+ }
+ }
+
+ m_accounts.Clear(); // will release all elements
+ m_identities.Clear();
+ m_incomingServers.Clear();
+ mAccountKeyList.Truncate();
+ SetLastServerFound(nullptr, EmptyCString(), EmptyCString(), 0,
+ EmptyCString());
+
+ if (m_accountsLoaded) {
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ if (mailSession) mailSession->RemoveFolderListener(this);
+ m_accountsLoaded = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::ShutdownServers() {
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (server) server->Shutdown();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CloseCachedConnections() {
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (server) server->CloseCachedConnections();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CleanupOnExit() {
+ // This can get called multiple times, and potentially re-entrantly.
+ // So add some protection against that.
+ if (m_shutdownInProgress) return NS_OK;
+
+ m_shutdownInProgress = true;
+
+ nsresult rv;
+ // If enabled, clear cache on shutdown. This is common to all accounts.
+ bool clearCache = false;
+ m_prefs->GetBoolPref("privacy.clearOnShutdown.cache", &clearCache);
+ if (clearCache) {
+ nsCOMPtr<nsICacheStorageService> cacheStorageService =
+ do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv);
+ if (NS_SUCCEEDED(rv)) cacheStorageService->Clear();
+ }
+
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+
+ bool emptyTrashOnExit = false;
+ bool cleanupInboxOnExit = false;
+
+ if (WeAreOffline()) break;
+
+ if (!server) continue;
+
+ server->GetEmptyTrashOnExit(&emptyTrashOnExit);
+ nsCOMPtr<nsIImapIncomingServer> imapserver = do_QueryInterface(server);
+ if (imapserver) {
+ imapserver->GetCleanupInboxOnExit(&cleanupInboxOnExit);
+ imapserver->SetShuttingDown(true);
+ }
+ if (emptyTrashOnExit || cleanupInboxOnExit) {
+ nsCOMPtr<nsIMsgFolder> root;
+ server->GetRootFolder(getter_AddRefs(root));
+ nsCString type;
+ server->GetType(type);
+ if (root) {
+ nsString passwd;
+ int32_t authMethod = 0;
+ bool serverRequiresPasswordForAuthentication = true;
+ bool isImap = type.EqualsLiteral("imap");
+ if (isImap) {
+ server->GetServerRequiresPasswordForBiff(
+ &serverRequiresPasswordForAuthentication);
+ server->GetPassword(passwd);
+ server->GetAuthMethod(&authMethod);
+ }
+ if (!isImap || (isImap && (!serverRequiresPasswordForAuthentication ||
+ !passwd.IsEmpty() ||
+ authMethod == nsMsgAuthMethod::OAuth2))) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) continue;
+
+ if (isImap && cleanupInboxOnExit) {
+ // Find the inbox.
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ rv = root->GetSubFolders(subFolders);
+ if (NS_SUCCEEDED(rv)) {
+ for (nsIMsgFolder* folder : subFolders) {
+ uint32_t flags;
+ folder->GetFlags(&flags);
+ if (flags & nsMsgFolderFlags::Inbox) {
+ // This is inbox, so Compact() it. There's an implied
+ // Expunge too, because this is IMAP.
+ RefPtr<UrlListener> cleanupListener = new UrlListener();
+ RefPtr<nsMsgAccountManager> self = this;
+ // This runs when the compaction (+expunge) is complete.
+ cleanupListener->mStopFn =
+ [self](nsIURI* url, nsresult status) -> nsresult {
+ if (self->m_folderDoingCleanupInbox) {
+ PR_CEnterMonitor(self->m_folderDoingCleanupInbox);
+ PR_CNotifyAll(self->m_folderDoingCleanupInbox);
+ self->m_cleanupInboxInProgress = false;
+ PR_CExitMonitor(self->m_folderDoingCleanupInbox);
+ self->m_folderDoingCleanupInbox = nullptr;
+ }
+ return NS_OK;
+ };
+
+ rv = folder->Compact(cleanupListener, nullptr);
+ if (NS_SUCCEEDED(rv))
+ accountManager->SetFolderDoingCleanupInbox(folder);
+ break;
+ }
+ }
+ }
+ }
+
+ if (emptyTrashOnExit) {
+ RefPtr<UrlListener> emptyTrashListener = new UrlListener();
+ RefPtr<nsMsgAccountManager> self = this;
+ // This runs when the trash-emptying is complete.
+ // (It'll be a nsIImapUrl::nsImapDeleteAllMsgs url).
+ emptyTrashListener->mStopFn = [self](nsIURI* url,
+ nsresult status) -> nsresult {
+ if (self->m_folderDoingEmptyTrash) {
+ PR_CEnterMonitor(self->m_folderDoingEmptyTrash);
+ PR_CNotifyAll(self->m_folderDoingEmptyTrash);
+ self->m_emptyTrashInProgress = false;
+ PR_CExitMonitor(self->m_folderDoingEmptyTrash);
+ self->m_folderDoingEmptyTrash = nullptr;
+ }
+ return NS_OK;
+ };
+
+ rv = root->EmptyTrash(emptyTrashListener);
+ if (isImap && NS_SUCCEEDED(rv))
+ accountManager->SetFolderDoingEmptyTrash(root);
+ }
+
+ if (isImap) {
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+
+ // Pause until any possible inbox-compaction and trash-emptying
+ // are complete (or time out).
+ bool inProgress = false;
+ if (cleanupInboxOnExit) {
+ int32_t loopCount = 0; // used to break out after 5 seconds
+ accountManager->GetCleanupInboxInProgress(&inProgress);
+ while (inProgress && loopCount++ < 5000) {
+ accountManager->GetCleanupInboxInProgress(&inProgress);
+ PR_CEnterMonitor(root);
+ PR_CWait(root, PR_MicrosecondsToInterval(1000UL));
+ PR_CExitMonitor(root);
+ NS_ProcessPendingEvents(thread,
+ PR_MicrosecondsToInterval(1000UL));
+ }
+ }
+ if (emptyTrashOnExit) {
+ accountManager->GetEmptyTrashInProgress(&inProgress);
+ int32_t loopCount = 0;
+ while (inProgress && loopCount++ < 5000) {
+ accountManager->GetEmptyTrashInProgress(&inProgress);
+ PR_CEnterMonitor(root);
+ PR_CWait(root, PR_MicrosecondsToInterval(1000UL));
+ PR_CExitMonitor(root);
+ NS_ProcessPendingEvents(thread,
+ PR_MicrosecondsToInterval(1000UL));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Try to do this early on in the shutdown process before
+ // necko shuts itself down.
+ CloseCachedConnections();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::WriteToFolderCache(nsIMsgFolderCache* folderCache) {
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ iter.Data()->WriteToFolderCache(folderCache);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::createKeyedAccount(const nsCString& key,
+ bool forcePositionToEnd,
+ nsIMsgAccount** aAccount) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccount> account = do_CreateInstance(kMsgAccountCID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ account->SetKey(key);
+
+ nsCString localFoldersAccountKey;
+ nsCString lastFolderAccountKey;
+ if (!forcePositionToEnd) {
+ nsCOMPtr<nsIMsgIncomingServer> localFoldersServer;
+ rv = GetLocalFoldersServer(getter_AddRefs(localFoldersServer));
+ if (NS_SUCCEEDED(rv)) {
+ for (auto account : m_accounts) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = account->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server == localFoldersServer) {
+ account->GetKey(localFoldersAccountKey);
+ break;
+ }
+ }
+ }
+
+ // Extracting the account key of the last mail acoount.
+ for (int32_t index = m_accounts.Length() - 1; index >= 0; index--) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = m_accounts[index]->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) {
+ nsCString accountType;
+ rv = server->GetType(accountType);
+ if (NS_SUCCEEDED(rv) && !accountType.EqualsLiteral("im")) {
+ m_accounts[index]->GetKey(lastFolderAccountKey);
+ break;
+ }
+ }
+ }
+ }
+
+ if (!forcePositionToEnd && !localFoldersAccountKey.IsEmpty() &&
+ !lastFolderAccountKey.IsEmpty() &&
+ lastFolderAccountKey == localFoldersAccountKey) {
+ // Insert account before Local Folders if that is the last account.
+ m_accounts.InsertElementAt(m_accounts.Length() - 1, account);
+ } else {
+ m_accounts.AppendElement(account);
+ }
+
+ nsCString newAccountKeyList;
+ nsCString accountKey;
+ for (uint32_t index = 0; index < m_accounts.Length(); index++) {
+ m_accounts[index]->GetKey(accountKey);
+ if (index) newAccountKeyList.Append(ACCOUNT_DELIMITER);
+ newAccountKeyList.Append(accountKey);
+ }
+ mAccountKeyList = newAccountKeyList;
+
+ m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS, mAccountKeyList);
+ account.forget(aAccount);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CreateAccount(nsIMsgAccount** _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsAutoCString key;
+ GetUniqueAccountKey(key);
+
+ return createKeyedAccount(key, false, _retval);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetAccount(const nsACString& aKey,
+ nsIMsgAccount** aAccount) {
+ NS_ENSURE_ARG_POINTER(aAccount);
+ *aAccount = nullptr;
+
+ for (uint32_t i = 0; i < m_accounts.Length(); ++i) {
+ nsCOMPtr<nsIMsgAccount> account(m_accounts[i]);
+ nsCString key;
+ account->GetKey(key);
+ if (key.Equals(aKey)) {
+ account.forget(aAccount);
+ break;
+ }
+ }
+
+ // If not found, create on demand.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::FindServerIndex(nsIMsgIncomingServer* server,
+ int32_t* result) {
+ NS_ENSURE_ARG_POINTER(server);
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsCString key;
+ nsresult rv = server->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // do this by account because the account list is in order
+ uint32_t i;
+ for (i = 0; i < m_accounts.Length(); ++i) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = m_accounts[i]->GetIncomingServer(getter_AddRefs(server));
+ if (!server || NS_FAILED(rv)) continue;
+
+ nsCString serverKey;
+ rv = server->GetKey(serverKey);
+ if (NS_FAILED(rv)) continue;
+
+ // stop when found,
+ // index will be set to the current index
+ if (serverKey.Equals(key)) break;
+ }
+
+ // Even if the search failed, we can return index.
+ // This means that all servers not in the array return an index higher
+ // than all "registered" servers.
+ *result = i;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::AddIncomingServerListener(
+ nsIIncomingServerListener* serverListener) {
+ m_incomingServerListeners.AppendObject(serverListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::RemoveIncomingServerListener(
+ nsIIncomingServerListener* serverListener) {
+ m_incomingServerListeners.RemoveObject(serverListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::NotifyServerLoaded(
+ nsIMsgIncomingServer* server) {
+ int32_t count = m_incomingServerListeners.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsIIncomingServerListener* listener = m_incomingServerListeners[i];
+ listener->OnServerLoaded(server);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::NotifyServerUnloaded(
+ nsIMsgIncomingServer* server) {
+ NS_ENSURE_ARG_POINTER(server);
+
+ int32_t count = m_incomingServerListeners.Count();
+ // Clear this to cut shutdown leaks. We are always passing valid non-null
+ // server here.
+ server->SetFilterList(nullptr);
+
+ for (int32_t i = 0; i < count; i++) {
+ nsIIncomingServerListener* listener = m_incomingServerListeners[i];
+ listener->OnServerUnloaded(server);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::NotifyServerChanged(
+ nsIMsgIncomingServer* server) {
+ int32_t count = m_incomingServerListeners.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsIIncomingServerListener* listener = m_incomingServerListeners[i];
+ listener->OnServerChanged(server);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::FindServerByURI(nsIURI* aURI,
+ nsIMsgIncomingServer** aResult) {
+ NS_ENSURE_ARG_POINTER(aURI);
+
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get username and hostname and port so we can get the server
+ nsAutoCString username;
+ nsAutoCString escapedUsername;
+ rv = aURI->GetUserPass(escapedUsername);
+ if (NS_SUCCEEDED(rv) && !escapedUsername.IsEmpty())
+ MsgUnescapeString(escapedUsername, 0, username);
+
+ nsAutoCString hostname;
+ nsAutoCString escapedHostname;
+ rv = aURI->GetHost(escapedHostname);
+ if (NS_SUCCEEDED(rv) && !escapedHostname.IsEmpty()) {
+ MsgUnescapeString(escapedHostname, 0, hostname);
+ }
+
+ nsAutoCString type;
+ rv = aURI->GetScheme(type);
+ if (NS_SUCCEEDED(rv) && !type.IsEmpty()) {
+ // Remove "-message" from the scheme in case we get called with
+ // "imap-message", "mailbox-message", or friends.
+ if (StringEndsWith(type, "-message"_ns)) type.SetLength(type.Length() - 8);
+ // now modify type if pop or news
+ if (type.EqualsLiteral("pop")) type.AssignLiteral("pop3");
+ // we use "nntp" in the server list so translate it here.
+ else if (type.EqualsLiteral("news"))
+ type.AssignLiteral("nntp");
+ // we use "any" as the wildcard type.
+ else if (type.EqualsLiteral("any"))
+ type.Truncate();
+ }
+
+ int32_t port = 0;
+ // check the port of the scheme is not 'none' or blank
+ if (!(type.EqualsLiteral("none") || type.IsEmpty())) {
+ rv = aURI->GetPort(&port);
+ // Set the port to zero if we got a -1 (use default)
+ if (NS_SUCCEEDED(rv) && (port == -1)) port = 0;
+ }
+
+ return findServerInternal(username, hostname, type, port, aResult);
+}
+
+nsresult nsMsgAccountManager::findServerInternal(
+ const nsACString& username, const nsACString& serverHostname,
+ const nsACString& type, int32_t port, nsIMsgIncomingServer** aResult) {
+ if ((m_lastFindServerUserName.Equals(username)) &&
+ (m_lastFindServerHostName.Equals(serverHostname)) &&
+ (m_lastFindServerType.Equals(type)) && (m_lastFindServerPort == port) &&
+ m_lastFindServerResult) {
+ NS_ADDREF(*aResult = m_lastFindServerResult);
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCString hostname;
+ nsCOMPtr<nsIIDNService> idnService =
+ do_GetService("@mozilla.org/network/idn-service;1");
+ rv = idnService->Normalize(serverHostname, hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ // Find matching server by user+host+type+port.
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+
+ if (!server) continue;
+
+ nsCString thisHostname;
+ rv = server->GetHostName(thisHostname);
+ if (NS_FAILED(rv)) continue;
+
+ rv = idnService->Normalize(thisHostname, thisHostname);
+ if (NS_FAILED(rv)) continue;
+
+ // If the hostname was a IP with trailing dot, that dot gets removed
+ // during URI mutation. We may well be here in findServerInternal to
+ // find a server from a folder URI. Remove the trailing dot so we can
+ // find the server.
+ nsCString thisHostnameNoDot(thisHostname);
+ if (!thisHostname.IsEmpty() &&
+ thisHostname.CharAt(thisHostname.Length() - 1) == '.') {
+ thisHostnameNoDot.Cut(thisHostname.Length() - 1, 1);
+ }
+
+ nsCString thisUsername;
+ rv = server->GetUsername(thisUsername);
+ if (NS_FAILED(rv)) continue;
+
+ nsCString thisType;
+ rv = server->GetType(thisType);
+ if (NS_FAILED(rv)) continue;
+
+ int32_t thisPort = -1; // use the default port identifier
+ // Don't try and get a port for the 'none' scheme
+ if (!thisType.EqualsLiteral("none")) {
+ rv = server->GetPort(&thisPort);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ }
+
+ // treat "" as a wild card, so if the caller passed in "" for the desired
+ // attribute treat it as a match
+ if ((type.IsEmpty() || thisType.Equals(type)) &&
+ (hostname.IsEmpty() ||
+ thisHostname.Equals(hostname, nsCaseInsensitiveCStringComparator) ||
+ thisHostnameNoDot.Equals(hostname,
+ nsCaseInsensitiveCStringComparator)) &&
+ (!(port != 0) || (port == thisPort)) &&
+ (username.IsEmpty() || thisUsername.Equals(username))) {
+ // stop on first find; cache for next time
+ SetLastServerFound(server, hostname, username, port, type);
+
+ NS_ADDREF(*aResult = server); // Was populated from member variable.
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_UNEXPECTED;
+}
+
+// Always return NS_OK;
+NS_IMETHODIMP
+nsMsgAccountManager::FindServer(const nsACString& username,
+ const nsACString& hostname,
+ const nsACString& type, int32_t port,
+ nsIMsgIncomingServer** aResult) {
+ *aResult = nullptr;
+ findServerInternal(username, hostname, type, port, aResult);
+ return NS_OK;
+}
+
+void nsMsgAccountManager::findAccountByServerKey(const nsCString& aKey,
+ nsIMsgAccount** aResult) {
+ *aResult = nullptr;
+
+ for (uint32_t i = 0; i < m_accounts.Length(); ++i) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = m_accounts[i]->GetIncomingServer(getter_AddRefs(server));
+ if (!server || NS_FAILED(rv)) continue;
+
+ nsCString key;
+ rv = server->GetKey(key);
+ if (NS_FAILED(rv)) continue;
+
+ // if the keys are equal, the servers are equal
+ if (key.Equals(aKey)) {
+ NS_ADDREF(*aResult = m_accounts[i]);
+ break; // stop on first found account
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::FindAccountForServer(nsIMsgIncomingServer* server,
+ nsIMsgAccount** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (!server) {
+ (*aResult) = nullptr;
+ return NS_OK;
+ }
+
+ nsresult rv;
+
+ nsCString key;
+ rv = server->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ findAccountByServerKey(key, aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetFirstIdentityForServer(nsIMsgIncomingServer* aServer,
+ nsIMsgIdentity** aIdentity) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ NS_ENSURE_ARG_POINTER(aIdentity);
+
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ nsresult rv = GetIdentitiesForServer(aServer, identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // not all servers have identities
+ // for example, Local Folders
+ if (identities.IsEmpty()) {
+ *aIdentity = nullptr;
+ } else {
+ NS_IF_ADDREF(*aIdentity = identities[0]);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetIdentitiesForServer(
+ nsIMsgIncomingServer* server,
+ nsTArray<RefPtr<nsIMsgIdentity>>& identities) {
+ NS_ENSURE_ARG_POINTER(server);
+ nsresult rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ identities.Clear();
+
+ nsAutoCString serverKey;
+ rv = server->GetKey(serverKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto account : m_accounts) {
+ nsCOMPtr<nsIMsgIncomingServer> thisServer;
+ rv = account->GetIncomingServer(getter_AddRefs(thisServer));
+ if (NS_FAILED(rv) || !thisServer) continue;
+
+ nsAutoCString thisServerKey;
+ rv = thisServer->GetKey(thisServerKey);
+ if (serverKey.Equals(thisServerKey)) {
+ nsTArray<RefPtr<nsIMsgIdentity>> theseIdentities;
+ rv = account->GetIdentities(theseIdentities);
+ NS_ENSURE_SUCCESS(rv, rv);
+ identities.AppendElements(theseIdentities);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetServersForIdentity(
+ nsIMsgIdentity* aIdentity,
+ nsTArray<RefPtr<nsIMsgIncomingServer>>& servers) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+ servers.Clear();
+
+ nsresult rv;
+ rv = LoadAccounts();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto account : m_accounts) {
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ if (NS_FAILED(account->GetIdentities(identities))) continue;
+
+ nsCString identityKey;
+ aIdentity->GetKey(identityKey);
+ for (auto thisIdentity : identities) {
+ nsCString thisIdentityKey;
+ rv = thisIdentity->GetKey(thisIdentityKey);
+
+ if (NS_SUCCEEDED(rv) && identityKey.Equals(thisIdentityKey)) {
+ nsCOMPtr<nsIMsgIncomingServer> thisServer;
+ rv = account->GetIncomingServer(getter_AddRefs(thisServer));
+ if (thisServer && NS_SUCCEEDED(rv)) {
+ servers.AppendElement(thisServer);
+ break;
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::AddRootFolderListener(nsIFolderListener* aListener) {
+ NS_ENSURE_TRUE(aListener, NS_OK);
+ mFolderListeners.AppendElement(aListener);
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = iter.Data()->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ rv = rootFolder->AddFolderListener(aListener);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::RemoveRootFolderListener(nsIFolderListener* aListener) {
+ NS_ENSURE_TRUE(aListener, NS_OK);
+ mFolderListeners.RemoveElement(aListener);
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = iter.Data()->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ rv = rootFolder->RemoveFolderListener(aListener);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::SetLocalFoldersServer(
+ nsIMsgIncomingServer* aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ nsCString key;
+ nsresult rv = aServer->GetKey(key);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER, key);
+}
+
+NS_IMETHODIMP nsMsgAccountManager::GetLocalFoldersServer(
+ nsIMsgIncomingServer** aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+
+ nsCString serverKey;
+
+ nsresult rv = m_prefs->GetCharPref(
+ PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER, serverKey);
+
+ if (NS_SUCCEEDED(rv) && !serverKey.IsEmpty()) {
+ rv = GetIncomingServer(serverKey, aServer);
+ if (NS_SUCCEEDED(rv)) return rv;
+ // otherwise, we're going to fall through to looking for an existing local
+ // folders account, because now we fail creating one if one already exists.
+ }
+
+ // try ("nobody","Local Folders","none"), and work down to any "none" server.
+ rv = findServerInternal("nobody"_ns, "Local Folders"_ns, "none"_ns, 0,
+ aServer);
+ if (NS_FAILED(rv) || !*aServer) {
+ rv = findServerInternal("nobody"_ns, EmptyCString(), "none"_ns, 0, aServer);
+ if (NS_FAILED(rv) || !*aServer) {
+ rv = findServerInternal(EmptyCString(), "Local Folders"_ns, "none"_ns, 0,
+ aServer);
+ if (NS_FAILED(rv) || !*aServer)
+ rv = findServerInternal(EmptyCString(), EmptyCString(), "none"_ns, 0,
+ aServer);
+ }
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!*aServer) return NS_ERROR_FAILURE;
+
+ // we don't want the Smart Mailboxes server to be the local server.
+ bool hidden;
+ (*aServer)->GetHidden(&hidden);
+ if (hidden) return NS_ERROR_FAILURE;
+
+ rv = SetLocalFoldersServer(*aServer);
+ return rv;
+}
+
+nsresult nsMsgAccountManager::GetLocalFoldersPrettyName(
+ nsString& localFoldersName) {
+ // we don't want "nobody at Local Folders" to show up in the
+ // folder pane, so we set the pretty name to a localized "Local Folders"
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED);
+
+ rv = sBundleService->CreateBundle(
+ "chrome://messenger/locale/messenger.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return bundle->GetStringFromName("localFolders", localFoldersName);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::CreateLocalMailAccount() {
+ // create the server
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = CreateIncomingServer("nobody"_ns, "Local Folders"_ns, "none"_ns,
+ getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString localFoldersName;
+ rv = GetLocalFoldersPrettyName(localFoldersName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ server->SetPrettyName(localFoldersName);
+
+ nsCOMPtr<nsINoIncomingServer> noServer;
+ noServer = do_QueryInterface(server, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // create the directory structure for old 4.x "Local Mail"
+ // under <profile dir>/Mail/Local Folders or
+ // <"mail.directory" pref>/Local Folders
+ nsCOMPtr<nsIFile> mailDir;
+ bool dirExists;
+
+ // we want <profile>/Mail
+ rv = NS_GetSpecialDirectory(NS_APP_MAIL_50_DIR, getter_AddRefs(mailDir));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = mailDir->Exists(&dirExists);
+ if (NS_SUCCEEDED(rv) && !dirExists)
+ rv = mailDir->Create(nsIFile::DIRECTORY_TYPE, 0775);
+ if (NS_FAILED(rv)) return rv;
+
+ // set the default local path for "none"
+ rv = server->SetDefaultLocalPath(mailDir);
+ if (NS_FAILED(rv)) return rv;
+
+ // Create an account when valid server values are established.
+ // This will keep the status of accounts sane by avoiding the addition of
+ // incomplete accounts.
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = CreateAccount(getter_AddRefs(account));
+ if (NS_FAILED(rv)) return rv;
+
+ // notice, no identity for local mail
+ // hook the server to the account
+ // after we set the server's local path
+ // (see bug #66018)
+ account->SetIncomingServer(server);
+
+ // remember this as the local folders server
+ return SetLocalFoldersServer(server);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SetFolderDoingEmptyTrash(nsIMsgFolder* folder) {
+ m_folderDoingEmptyTrash = folder;
+ m_emptyTrashInProgress = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetEmptyTrashInProgress(bool* bVal) {
+ NS_ENSURE_ARG_POINTER(bVal);
+ *bVal = m_emptyTrashInProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SetFolderDoingCleanupInbox(nsIMsgFolder* folder) {
+ m_folderDoingCleanupInbox = folder;
+ m_cleanupInboxInProgress = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetCleanupInboxInProgress(bool* bVal) {
+ NS_ENSURE_ARG_POINTER(bVal);
+ *bVal = m_cleanupInboxInProgress;
+ return NS_OK;
+}
+
+void nsMsgAccountManager::SetLastServerFound(nsIMsgIncomingServer* server,
+ const nsACString& hostname,
+ const nsACString& username,
+ const int32_t port,
+ const nsACString& type) {
+ m_lastFindServerResult = server;
+ m_lastFindServerHostName = hostname;
+ m_lastFindServerUserName = username;
+ m_lastFindServerPort = port;
+ m_lastFindServerType = type;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::SaveAccountInfo() {
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> pref(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return pref->SavePrefFile(nullptr);
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetChromePackageName(const nsACString& aExtensionName,
+ nsACString& aChromePackageName) {
+ nsresult rv;
+ nsCOMPtr<nsICategoryManager> catman =
+ do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISimpleEnumerator> e;
+ rv = catman->EnumerateCategory(MAILNEWS_ACCOUNTMANAGER_EXTENSIONS,
+ getter_AddRefs(e));
+ if (NS_SUCCEEDED(rv) && e) {
+ while (true) {
+ nsCOMPtr<nsISupports> supports;
+ rv = e->GetNext(getter_AddRefs(supports));
+ nsCOMPtr<nsISupportsCString> catEntry = do_QueryInterface(supports);
+ if (NS_FAILED(rv) || !catEntry) break;
+
+ nsAutoCString entryString;
+ rv = catEntry->GetData(entryString);
+ if (NS_FAILED(rv)) break;
+
+ nsCString contractidString;
+ rv = catman->GetCategoryEntry(MAILNEWS_ACCOUNTMANAGER_EXTENSIONS,
+ entryString, contractidString);
+ if (NS_FAILED(rv)) break;
+
+ nsCOMPtr<nsIMsgAccountManagerExtension> extension =
+ do_GetService(contractidString.get(), &rv);
+ if (NS_FAILED(rv) || !extension) break;
+
+ nsCString name;
+ rv = extension->GetName(name);
+ if (NS_FAILED(rv)) break;
+
+ if (name.Equals(aExtensionName))
+ return extension->GetChromePackageName(aChromePackageName);
+ }
+ }
+ return NS_ERROR_UNEXPECTED;
+}
+
+class VFChangeListenerEvent : public mozilla::Runnable {
+ public:
+ VFChangeListenerEvent(VirtualFolderChangeListener* vfChangeListener,
+ nsIMsgFolder* virtFolder, nsIMsgDatabase* virtDB)
+ : mozilla::Runnable("VFChangeListenerEvent"),
+ mVFChangeListener(vfChangeListener),
+ mFolder(virtFolder),
+ mDB(virtDB) {}
+
+ NS_IMETHOD Run() {
+ if (mVFChangeListener) mVFChangeListener->ProcessUpdateEvent(mFolder, mDB);
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<VirtualFolderChangeListener> mVFChangeListener;
+ nsCOMPtr<nsIMsgFolder> mFolder;
+ nsCOMPtr<nsIMsgDatabase> mDB;
+};
+
+NS_IMPL_ISUPPORTS(VirtualFolderChangeListener, nsIDBChangeListener)
+
+VirtualFolderChangeListener::VirtualFolderChangeListener()
+ : m_searchOnMsgStatus(false), m_batchingEvents(false) {}
+
+nsresult VirtualFolderChangeListener::Init() {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ nsresult rv = m_virtualFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(msgDB));
+ if (NS_SUCCEEDED(rv) && msgDB) {
+ nsCString searchTermString;
+ dbFolderInfo->GetCharProperty("searchStr", searchTermString);
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ rv = filterService->GetTempFilterList(m_virtualFolder,
+ getter_AddRefs(filterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFilter> tempFilter;
+ filterList->CreateFilter(u"temp"_ns, getter_AddRefs(tempFilter));
+ NS_ENSURE_SUCCESS(rv, rv);
+ filterList->ParseCondition(tempFilter, searchTermString.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_searchSession =
+ do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ rv = tempFilter->GetSearchTerms(searchTerms);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we add the search scope right before we match the header,
+ // because we don't want the search scope caching the body input
+ // stream, because that holds onto the mailbox file, breaking
+ // compaction.
+
+ // add each search term to the search session
+ for (nsIMsgSearchTerm* searchTerm : searchTerms) {
+ nsMsgSearchAttribValue attrib;
+ searchTerm->GetAttrib(&attrib);
+ if (attrib == nsMsgSearchAttrib::MsgStatus) m_searchOnMsgStatus = true;
+ m_searchSession->AppendTerm(searchTerm);
+ }
+ }
+ return rv;
+}
+
+/**
+ * nsIDBChangeListener
+ */
+
+NS_IMETHODIMP
+VirtualFolderChangeListener::OnHdrPropertyChanged(
+ nsIMsgDBHdr* aHdrChanged, const nsACString& property, bool aPreChange,
+ uint32_t* aStatus, nsIDBChangeListener* aInstigator) {
+ const uint32_t kMatch = 0x1;
+ const uint32_t kRead = 0x2;
+ const uint32_t kNew = 0x4;
+ NS_ENSURE_ARG_POINTER(aHdrChanged);
+ NS_ENSURE_ARG_POINTER(aStatus);
+
+ uint32_t flags;
+ bool match;
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // we don't want any early returns from this function, until we've
+ // called ClearScopes on the search session.
+ m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail,
+ m_folderWatching);
+ rv = m_searchSession->MatchHdr(aHdrChanged, msgDB, &match);
+ m_searchSession->ClearScopes();
+ NS_ENSURE_SUCCESS(rv, rv);
+ aHdrChanged->GetFlags(&flags);
+
+ if (aPreChange) // We're looking at the old header, save status
+ {
+ *aStatus = 0;
+ if (match) *aStatus |= kMatch;
+ if (flags & nsMsgMessageFlags::Read) *aStatus |= kRead;
+ if (flags & nsMsgMessageFlags::New) *aStatus |= kNew;
+ return NS_OK;
+ }
+
+ // This is the post change section where changes are detected
+
+ bool wasMatch = *aStatus & kMatch;
+ if (!match && !wasMatch) // header not in virtual folder
+ return NS_OK;
+
+ int32_t totalDelta = 0, unreadDelta = 0, newDelta = 0;
+
+ if (match) {
+ totalDelta++;
+ if (!(flags & nsMsgMessageFlags::Read)) unreadDelta++;
+ if (flags & nsMsgMessageFlags::New) newDelta++;
+ }
+
+ if (wasMatch) {
+ totalDelta--;
+ if (!(*aStatus & kRead)) unreadDelta--;
+ if (*aStatus & kNew) newDelta--;
+ }
+
+ if (!(unreadDelta || totalDelta || newDelta)) return NS_OK;
+
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (unreadDelta) dbFolderInfo->ChangeNumUnreadMessages(unreadDelta);
+
+ if (newDelta) {
+ int32_t numNewMessages;
+ m_virtualFolder->GetNumNewMessages(false, &numNewMessages);
+ m_virtualFolder->SetNumNewMessages(numNewMessages + newDelta);
+ m_virtualFolder->SetHasNewMessages(numNewMessages + newDelta > 0);
+ }
+
+ if (totalDelta) {
+ dbFolderInfo->ChangeNumMessages(totalDelta);
+ nsCString searchUri;
+ m_virtualFolder->GetURI(searchUri);
+ msgDB->UpdateHdrInCache(searchUri, aHdrChanged, totalDelta == 1);
+ }
+
+ PostUpdateEvent(m_virtualFolder, virtDatabase);
+
+ return NS_OK;
+}
+
+void VirtualFolderChangeListener::DecrementNewMsgCount() {
+ int32_t numNewMessages;
+ m_virtualFolder->GetNumNewMessages(false, &numNewMessages);
+ if (numNewMessages > 0) numNewMessages--;
+ m_virtualFolder->SetNumNewMessages(numNewMessages);
+ if (!numNewMessages) m_virtualFolder->SetHasNewMessages(false);
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnHdrFlagsChanged(
+ nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+
+ nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB));
+ bool oldMatch = false, newMatch = false;
+ // we don't want any early returns from this function, until we've
+ // called ClearScopes 0n the search session.
+ m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail,
+ m_folderWatching);
+ rv = m_searchSession->MatchHdr(aHdrChanged, msgDB, &newMatch);
+ if (NS_SUCCEEDED(rv) && m_searchOnMsgStatus) {
+ // if status is a search criteria, check if the header matched before
+ // it changed, in order to determine if we need to bump the counts.
+ aHdrChanged->SetFlags(aOldFlags);
+ rv = m_searchSession->MatchHdr(aHdrChanged, msgDB, &oldMatch);
+ // restore new flags even on match failure.
+ aHdrChanged->SetFlags(aNewFlags);
+ } else
+ oldMatch = newMatch;
+ m_searchSession->ClearScopes();
+ NS_ENSURE_SUCCESS(rv, rv);
+ // we don't want to change the total counts if this virtual folder is open in
+ // a view, because we won't remove the header from view while it's open. On
+ // the other hand, it's hard to fix the count when the user clicks away to
+ // another folder, w/o re-running the search, or setting some sort of pending
+ // count change. Maybe this needs to be handled in the view code...the view
+ // could do the same calculation and also keep track of the counts changed.
+ // Then, when the view was closed, if it's a virtual folder, it could update
+ // the counts for the db.
+ if (oldMatch != newMatch ||
+ (oldMatch && (aOldFlags & nsMsgMessageFlags::Read) !=
+ (aNewFlags & nsMsgMessageFlags::Read))) {
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t totalDelta = 0, unreadDelta = 0;
+ if (oldMatch != newMatch) {
+ // bool isOpen = false;
+ // nsCOMPtr<nsIMsgMailSession> mailSession =
+ // do_GetService("@mozilla.org/messenger/services/session;1");
+ // if (mailSession && aFolder)
+ // mailSession->IsFolderOpenInWindow(m_virtualFolder, &isOpen);
+ // we can't remove headers that no longer match - but we might add headers
+ // that newly match, someday.
+ // if (!isOpen)
+ totalDelta = (oldMatch) ? -1 : 1;
+ }
+ bool msgHdrIsRead;
+ aHdrChanged->GetIsRead(&msgHdrIsRead);
+ if (oldMatch == newMatch) // read flag changed state
+ unreadDelta = (msgHdrIsRead) ? -1 : 1;
+ else if (oldMatch) // else header should removed
+ unreadDelta = (aOldFlags & nsMsgMessageFlags::Read) ? 0 : -1;
+ else // header should be added
+ unreadDelta = (aNewFlags & nsMsgMessageFlags::Read) ? 0 : 1;
+ if (unreadDelta) dbFolderInfo->ChangeNumUnreadMessages(unreadDelta);
+ if (totalDelta) dbFolderInfo->ChangeNumMessages(totalDelta);
+ if (unreadDelta == -1 && aOldFlags & nsMsgMessageFlags::New)
+ DecrementNewMsgCount();
+
+ if (totalDelta) {
+ nsCString searchUri;
+ m_virtualFolder->GetURI(searchUri);
+ msgDB->UpdateHdrInCache(searchUri, aHdrChanged, totalDelta == 1);
+ }
+
+ PostUpdateEvent(m_virtualFolder, virtDatabase);
+ } else if (oldMatch && (aOldFlags & nsMsgMessageFlags::New) &&
+ !(aNewFlags & nsMsgMessageFlags::New))
+ DecrementNewMsgCount();
+
+ return rv;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnHdrDeleted(
+ nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+
+ nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool match = false;
+ m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail,
+ m_folderWatching);
+ // Since the notifier went to the trouble of passing in the msg flags,
+ // we should use them when doing the match.
+ uint32_t msgFlags;
+ aHdrDeleted->GetFlags(&msgFlags);
+ aHdrDeleted->SetFlags(aFlags);
+ rv = m_searchSession->MatchHdr(aHdrDeleted, msgDB, &match);
+ aHdrDeleted->SetFlags(msgFlags);
+ m_searchSession->ClearScopes();
+ if (match) {
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool msgHdrIsRead;
+ aHdrDeleted->GetIsRead(&msgHdrIsRead);
+ if (!msgHdrIsRead) dbFolderInfo->ChangeNumUnreadMessages(-1);
+ dbFolderInfo->ChangeNumMessages(-1);
+ if (aFlags & nsMsgMessageFlags::New) {
+ int32_t numNewMessages;
+ m_virtualFolder->GetNumNewMessages(false, &numNewMessages);
+ m_virtualFolder->SetNumNewMessages(numNewMessages - 1);
+ if (numNewMessages == 1) m_virtualFolder->SetHasNewMessages(false);
+ }
+
+ nsCString searchUri;
+ m_virtualFolder->GetURI(searchUri);
+ msgDB->UpdateHdrInCache(searchUri, aHdrDeleted, false);
+
+ PostUpdateEvent(m_virtualFolder, virtDatabase);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnHdrAdded(
+ nsIMsgDBHdr* aNewHdr, nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+
+ nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool match = false;
+ if (!m_searchSession) return NS_ERROR_NULL_POINTER;
+
+ m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail,
+ m_folderWatching);
+ rv = m_searchSession->MatchHdr(aNewHdr, msgDB, &match);
+ m_searchSession->ClearScopes();
+ if (match) {
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool msgHdrIsRead;
+ uint32_t msgFlags;
+ aNewHdr->GetIsRead(&msgHdrIsRead);
+ aNewHdr->GetFlags(&msgFlags);
+ if (!msgHdrIsRead) dbFolderInfo->ChangeNumUnreadMessages(1);
+ if (msgFlags & nsMsgMessageFlags::New) {
+ int32_t numNewMessages;
+ m_virtualFolder->GetNumNewMessages(false, &numNewMessages);
+ m_virtualFolder->SetHasNewMessages(true);
+ m_virtualFolder->SetNumNewMessages(numNewMessages + 1);
+ }
+ nsCString searchUri;
+ m_virtualFolder->GetURI(searchUri);
+ msgDB->UpdateHdrInCache(searchUri, aNewHdr, true);
+ dbFolderInfo->ChangeNumMessages(1);
+ PostUpdateEvent(m_virtualFolder, virtDatabase);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnParentChanged(
+ nsMsgKey aKeyChanged, nsMsgKey oldParent, nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnAnnouncerGoingAway(
+ nsIDBChangeAnnouncer* instigator) {
+ nsCOMPtr<nsIMsgDatabase> msgDB = do_QueryInterface(instigator);
+ if (msgDB) msgDB->RemoveListener(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+VirtualFolderChangeListener::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnReadChanged(
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP VirtualFolderChangeListener::OnJunkScoreChanged(
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+nsresult VirtualFolderChangeListener::PostUpdateEvent(
+ nsIMsgFolder* virtualFolder, nsIMsgDatabase* virtDatabase) {
+ if (m_batchingEvents) return NS_OK;
+ m_batchingEvents = true;
+ nsCOMPtr<nsIRunnable> event =
+ new VFChangeListenerEvent(this, virtualFolder, virtDatabase);
+ return NS_DispatchToCurrentThread(event);
+}
+
+void VirtualFolderChangeListener::ProcessUpdateEvent(nsIMsgFolder* virtFolder,
+ nsIMsgDatabase* virtDB) {
+ m_batchingEvents = false;
+ virtFolder->UpdateSummaryTotals(true); // force update from db.
+ virtDB->Commit(nsMsgDBCommitType::kLargeCommit);
+}
+
+nsresult nsMsgAccountManager::GetVirtualFoldersFile(nsCOMPtr<nsIFile>& aFile) {
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = profileDir->AppendNative("virtualFolders.dat"_ns);
+ if (NS_SUCCEEDED(rv)) aFile = profileDir;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::LoadVirtualFolders() {
+ nsCOMPtr<nsIFile> file;
+ GetVirtualFoldersFile(file);
+ if (!file) return NS_ERROR_FAILURE;
+
+ if (m_virtualFoldersLoaded) return NS_OK;
+
+ m_loadingVirtualFolders = true;
+
+ // Before loading virtual folders, ensure that all real folders exist.
+ // Some may not have been created yet, which would break virtual folders
+ // that depend on them.
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ nsresult rv = GetAllServers(allServers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (auto server : allServers) {
+ if (server) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ rootFolder->GetSubFolders(dummy);
+ }
+ }
+ }
+
+ if (!m_dbService) {
+ m_dbService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIFileInputStream> fileStream =
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = fileStream->Init(file, PR_RDONLY, 0664, false);
+ nsCOMPtr<nsILineInputStream> lineInputStream(do_QueryInterface(fileStream));
+
+ bool isMore = true;
+ nsAutoCString buffer;
+ int32_t version = -1;
+ nsCOMPtr<nsIMsgFolder> virtualFolder;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) {
+ if (!buffer.IsEmpty()) {
+ if (version == -1) {
+ buffer.Cut(0, 8);
+ nsresult irv;
+ version = buffer.ToInteger(&irv);
+ continue;
+ }
+ if (StringBeginsWith(buffer, "uri="_ns)) {
+ buffer.Cut(0, 4);
+ dbFolderInfo = nullptr;
+
+ rv = GetOrCreateFolder(buffer, getter_AddRefs(virtualFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ virtualFolder->SetFlag(nsMsgFolderFlags::Virtual);
+
+ nsCOMPtr<nsIMsgFolder> grandParent;
+ nsCOMPtr<nsIMsgFolder> oldParent;
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ bool isServer;
+ // This loop handles creating virtual folders without an existing
+ // parent.
+ do {
+ // need to add the folder as a sub-folder of its parent.
+ int32_t lastSlash = buffer.RFindChar('/');
+ if (lastSlash == kNotFound) break;
+ nsDependentCSubstring parentUri(buffer, 0, lastSlash);
+ // hold a reference so it won't get deleted before it's parented.
+ oldParent = parentFolder;
+
+ rv = GetOrCreateFolder(parentUri, getter_AddRefs(parentFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString currentFolderNameStr;
+ nsAutoCString currentFolderNameCStr;
+ MsgUnescapeString(
+ nsCString(Substring(buffer, lastSlash + 1, buffer.Length())), 0,
+ currentFolderNameCStr);
+ CopyUTF8toUTF16(currentFolderNameCStr, currentFolderNameStr);
+ nsCOMPtr<nsIMsgFolder> childFolder;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // force db to get created.
+ // XXX TODO: is this SetParent() right? Won't it screw up if virtual
+ // folder is nested >2 deep? Leave for now, but revisit when getting
+ // rid of dangling folders (BenC).
+ virtualFolder->SetParent(parentFolder);
+ rv = virtualFolder->GetMsgDatabase(getter_AddRefs(db));
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING)
+ m_dbService->CreateNewDB(virtualFolder, getter_AddRefs(db));
+ if (db)
+ rv = db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ else
+ break;
+
+ parentFolder->AddSubfolder(currentFolderNameStr,
+ getter_AddRefs(childFolder));
+ if (childFolder) parentFolder->NotifyFolderAdded(childFolder);
+ // here we make sure if our parent is rooted - if not, we're
+ // going to loop and add our parent as a child of its grandparent
+ // and repeat until we get to the server, or a folder that
+ // has its parent set.
+ parentFolder->GetParent(getter_AddRefs(grandParent));
+ parentFolder->GetIsServer(&isServer);
+ buffer.SetLength(lastSlash);
+ } while (!grandParent && !isServer);
+ } else if (dbFolderInfo && StringBeginsWith(buffer, "scope="_ns)) {
+ buffer.Cut(0, 6);
+ // if this is a cross folder virtual folder, we have a list of folders
+ // uris, and we have to add a pending listener for each of them.
+ if (!buffer.IsEmpty()) {
+ ParseAndVerifyVirtualFolderScope(buffer);
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, buffer);
+ AddVFListenersForVF(virtualFolder, buffer);
+ }
+ } else if (dbFolderInfo && StringBeginsWith(buffer, "terms="_ns)) {
+ buffer.Cut(0, 6);
+ dbFolderInfo->SetCharProperty("searchStr", buffer);
+ } else if (dbFolderInfo && StringBeginsWith(buffer, "searchOnline="_ns)) {
+ buffer.Cut(0, 13);
+ dbFolderInfo->SetBooleanProperty("searchOnline",
+ buffer.EqualsLiteral("true"));
+ } else if (dbFolderInfo &&
+ Substring(buffer, 0, SEARCH_FOLDER_FLAG_LEN + 1)
+ .Equals(SEARCH_FOLDER_FLAG "=")) {
+ buffer.Cut(0, SEARCH_FOLDER_FLAG_LEN + 1);
+ dbFolderInfo->SetCharProperty(SEARCH_FOLDER_FLAG, buffer);
+ }
+ }
+ }
+
+ m_loadingVirtualFolders = false;
+ m_virtualFoldersLoaded = true;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::SaveVirtualFolders() {
+ if (!m_virtualFoldersLoaded) return NS_OK;
+
+ nsCOMPtr<nsIFile> file;
+ GetVirtualFoldersFile(file);
+
+ // Open a buffered, safe output stream
+ nsCOMPtr<nsIOutputStream> outStream;
+ nsresult rv = MsgNewSafeBufferedFileOutputStream(
+ getter_AddRefs(outStream), file, PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE,
+ 0664);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ WriteLineToOutputStream("version=", "1", outStream);
+ for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) {
+ nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data();
+ if (server) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ nsTArray<RefPtr<nsIMsgFolder>> virtualFolders;
+ nsresult rv = rootFolder->GetFoldersWithFlags(nsMsgFolderFlags::Virtual,
+ virtualFolders);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ for (auto msgFolder : virtualFolders) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = msgFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db)); // force db to get created.
+ if (dbFolderInfo) {
+ nsCString srchFolderUri;
+ nsCString searchTerms;
+ nsCString regexScope;
+ nsCString vfFolderFlag;
+ bool searchOnline = false;
+ dbFolderInfo->GetBooleanProperty("searchOnline", false,
+ &searchOnline);
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, srchFolderUri);
+ dbFolderInfo->GetCharProperty("searchStr", searchTerms);
+ // logically searchFolderFlag is an int, but since we want to
+ // write out a string, get it as a string.
+ dbFolderInfo->GetCharProperty(SEARCH_FOLDER_FLAG, vfFolderFlag);
+ nsCString uri;
+ msgFolder->GetURI(uri);
+ if (!srchFolderUri.IsEmpty() && !searchTerms.IsEmpty()) {
+ WriteLineToOutputStream("uri=", uri.get(), outStream);
+ if (!vfFolderFlag.IsEmpty())
+ WriteLineToOutputStream(SEARCH_FOLDER_FLAG "=",
+ vfFolderFlag.get(), outStream);
+ WriteLineToOutputStream("scope=", srchFolderUri.get(), outStream);
+ WriteLineToOutputStream("terms=", searchTerms.get(), outStream);
+ WriteLineToOutputStream(
+ "searchOnline=", searchOnline ? "true" : "false", outStream);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(outStream, &rv);
+ NS_ASSERTION(safeStream, "expected a safe output stream!");
+ if (safeStream) {
+ rv = safeStream->Finish();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("failed to save personal dictionary file! possible data loss");
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgAccountManager::WriteLineToOutputStream(
+ const char* prefix, const char* line, nsIOutputStream* outputStream) {
+ uint32_t writeCount;
+ outputStream->Write(prefix, strlen(prefix), &writeCount);
+ outputStream->Write(line, strlen(line), &writeCount);
+ outputStream->Write("\n", 1, &writeCount);
+ return NS_OK;
+}
+
+/**
+ * Parse the '|' separated folder uri string into individual folders, verify
+ * that the folders are real. If we were to add things like wildcards, we
+ * could implement the expansion into real folders here.
+ *
+ * @param buffer On input, list of folder uri's, on output, verified list.
+ */
+void nsMsgAccountManager::ParseAndVerifyVirtualFolderScope(nsCString& buffer) {
+ if (buffer.Equals("*")) {
+ // This is a special virtual folder that searches all folders in all
+ // accounts. Folders are chosen by the front end at search time.
+ return;
+ }
+ nsCString verifiedFolders;
+ nsTArray<nsCString> folderUris;
+ ParseString(buffer, '|', folderUris);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsCOMPtr<nsIMsgFolder> parent;
+
+ for (uint32_t i = 0; i < folderUris.Length(); i++) {
+ nsCOMPtr<nsIMsgFolder> realFolder;
+ nsresult rv = GetOrCreateFolder(folderUris[i], getter_AddRefs(realFolder));
+ if (!NS_SUCCEEDED(rv)) {
+ continue;
+ }
+ realFolder->GetParent(getter_AddRefs(parent));
+ if (!parent) continue;
+ realFolder->GetServer(getter_AddRefs(server));
+ if (!server) continue;
+ if (!verifiedFolders.IsEmpty()) verifiedFolders.Append('|');
+ verifiedFolders.Append(folderUris[i]);
+ }
+ buffer.Assign(verifiedFolders);
+}
+
+// This conveniently works to add a single folder as well.
+nsresult nsMsgAccountManager::AddVFListenersForVF(
+ nsIMsgFolder* virtualFolder, const nsCString& srchFolderUris) {
+ if (srchFolderUris.Equals("*")) {
+ return NS_OK;
+ }
+
+ nsresult rv;
+ if (!m_dbService) {
+ m_dbService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Avoid any possible duplicate listeners for this virtual folder.
+ RemoveVFListenerForVF(virtualFolder, nullptr);
+
+ nsTArray<nsCString> folderUris;
+ ParseString(srchFolderUris, '|', folderUris);
+
+ for (uint32_t i = 0; i < folderUris.Length(); i++) {
+ nsCOMPtr<nsIMsgFolder> realFolder;
+ rv = GetOrCreateFolder(folderUris[i], getter_AddRefs(realFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ RefPtr<VirtualFolderChangeListener> dbListener =
+ new VirtualFolderChangeListener();
+ NS_ENSURE_TRUE(dbListener, NS_ERROR_OUT_OF_MEMORY);
+ dbListener->m_virtualFolder = virtualFolder;
+ dbListener->m_folderWatching = realFolder;
+ if (NS_FAILED(dbListener->Init())) {
+ dbListener = nullptr;
+ continue;
+ }
+ m_virtualFolderListeners.AppendElement(dbListener);
+ m_dbService->RegisterPendingListener(realFolder, dbListener);
+ }
+ if (!m_virtualFolders.Contains(virtualFolder)) {
+ m_virtualFolders.AppendElement(virtualFolder);
+ }
+ return NS_OK;
+}
+
+// This is called if a folder that's part of the scope of a saved search
+// has gone away.
+nsresult nsMsgAccountManager::RemoveVFListenerForVF(nsIMsgFolder* virtualFolder,
+ nsIMsgFolder* folder) {
+ nsresult rv;
+ if (!m_dbService) {
+ m_dbService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter(
+ m_virtualFolderListeners);
+ RefPtr<VirtualFolderChangeListener> listener;
+
+ while (iter.HasMore()) {
+ listener = iter.GetNext();
+ if (listener->m_virtualFolder == virtualFolder &&
+ (!folder || listener->m_folderWatching == folder)) {
+ m_dbService->UnregisterPendingListener(listener);
+ m_virtualFolderListeners.RemoveElement(listener);
+ if (folder) {
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::GetAllFolders(
+ nsTArray<RefPtr<nsIMsgFolder>>& aAllFolders) {
+ aAllFolders.Clear();
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ nsresult rv = GetAllServers(allServers);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto server : allServers) {
+ if (server) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ nsTArray<RefPtr<nsIMsgFolder>> descendents;
+ rootFolder->GetDescendants(descendents);
+ aAllFolders.AppendElements(descendents);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderAdded(nsIMsgFolder* parent,
+ nsIMsgFolder* folder) {
+ if (!parent) {
+ // This method gets called for folders that aren't connected to anything,
+ // such as a junk folder that appears when an IMAP account is created. We
+ // don't want these added to the virtual folder.
+ return NS_OK;
+ }
+
+ uint32_t folderFlags;
+ folder->GetFlags(&folderFlags);
+
+ bool addToSmartFolders = false;
+ folder->IsSpecialFolder(nsMsgFolderFlags::Inbox |
+ nsMsgFolderFlags::Templates |
+ nsMsgFolderFlags::Trash |
+ nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Junk,
+ false, &addToSmartFolders);
+ // For Sent/Archives/Trash, we treat sub-folders of those folders as
+ // "special", and want to add them the smart folders search scope.
+ // So we check if this is a sub-folder of one of those special folders
+ // and set the corresponding folderFlag if so.
+ if (!addToSmartFolders) {
+ bool isSpecial = false;
+ folder->IsSpecialFolder(nsMsgFolderFlags::SentMail, true, &isSpecial);
+ if (isSpecial) {
+ addToSmartFolders = true;
+ folderFlags |= nsMsgFolderFlags::SentMail;
+ }
+ folder->IsSpecialFolder(nsMsgFolderFlags::Archive, true, &isSpecial);
+ if (isSpecial) {
+ addToSmartFolders = true;
+ folderFlags |= nsMsgFolderFlags::Archive;
+ }
+ folder->IsSpecialFolder(nsMsgFolderFlags::Trash, true, &isSpecial);
+ if (isSpecial) {
+ addToSmartFolders = true;
+ folderFlags |= nsMsgFolderFlags::Trash;
+ }
+ }
+ nsresult rv = NS_OK;
+ // if this is a special folder, check if we have a saved search over
+ // folders with this flag, and if so, add this folder to the scope.
+ if (addToSmartFolders) {
+ // quick way to enumerate the saved searches.
+ for (nsCOMPtr<nsIMsgFolder> virtualFolder : m_virtualFolders) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ if (dbFolderInfo) {
+ uint32_t vfFolderFlag;
+ dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag);
+ // found a saved search over folders w/ the same flag as the new folder.
+ if (vfFolderFlag & folderFlags) {
+ nsCString searchURI;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI);
+
+ // "normalize" searchURI so we can search for |folderURI|.
+ if (!searchURI.IsEmpty()) {
+ searchURI.Insert('|', 0);
+ searchURI.Append('|');
+ }
+ nsCString folderURI;
+ folder->GetURI(folderURI);
+ folderURI.Insert('|', 0);
+ folderURI.Append('|');
+
+ int32_t index = searchURI.Find(folderURI);
+ if (index == kNotFound) {
+ searchURI.Cut(0, 1);
+ folderURI.Cut(0, 1);
+ folderURI.SetLength(folderURI.Length() - 1);
+ searchURI.Append(folderURI);
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI);
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(virtualFolder, "search-folders-changed",
+ nullptr);
+ }
+
+ // Add sub-folders to smart folder.
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rv = folder->GetDescendants(allDescendants);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ for (auto subFolder : allDescendants) {
+ subFolder->GetParent(getter_AddRefs(parentFolder));
+ OnFolderAdded(parentFolder, subFolder);
+ }
+ }
+ }
+ }
+ }
+
+ // Find any virtual folders that search `parent`, and add `folder` to them.
+ if (!(folderFlags & nsMsgFolderFlags::Virtual)) {
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter(
+ m_virtualFolderListeners);
+ RefPtr<VirtualFolderChangeListener> listener;
+
+ while (iter.HasMore()) {
+ listener = iter.GetNext();
+ if (listener->m_folderWatching == parent) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ listener->m_virtualFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(db));
+
+ uint32_t vfFolderFlag;
+ dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag);
+ if (addToSmartFolders && vfFolderFlag &&
+ !(vfFolderFlag & nsMsgFolderFlags::Trash)) {
+ // Don't add folders of one type to the unified folder of another
+ // type, unless it's the Trash unified folder.
+ continue;
+ }
+ nsCString searchURI;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI);
+
+ // "normalize" searchURI so we can search for |folderURI|.
+ if (!searchURI.IsEmpty()) {
+ searchURI.Insert('|', 0);
+ searchURI.Append('|');
+ }
+ nsCString folderURI;
+ folder->GetURI(folderURI);
+
+ int32_t index = searchURI.Find(folderURI);
+ if (index == kNotFound) {
+ searchURI.Cut(0, 1);
+ searchURI.Append(folderURI);
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI);
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(listener->m_virtualFolder,
+ "search-folders-changed", nullptr);
+ }
+ }
+ }
+ }
+
+ // need to make sure this isn't happening during loading of virtualfolders.dat
+ if (folderFlags & nsMsgFolderFlags::Virtual && !m_loadingVirtualFolders) {
+ // When a new virtual folder is added, need to create a db Listener for it.
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = folder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString srchFolderUri;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, srchFolderUri);
+ AddVFListenersForVF(folder, srchFolderUri);
+ rv = SaveVirtualFolders();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnMessageAdded(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderRemoved(nsIMsgFolder* parentFolder,
+ nsIMsgFolder* folder) {
+ nsresult rv = NS_OK;
+ uint32_t folderFlags;
+ folder->GetFlags(&folderFlags);
+ // if we removed a VF, flush VF list to disk.
+ if (folderFlags & nsMsgFolderFlags::Virtual) {
+ RemoveVFListenerForVF(folder, nullptr);
+ m_virtualFolders.RemoveElement(folder);
+ rv = SaveVirtualFolders();
+ // clear flags on deleted folder if it's a virtual folder, so that creating
+ // a new folder with the same name doesn't cause confusion.
+ folder->SetFlags(0);
+ return rv;
+ }
+ // need to update the saved searches to check for a few things:
+ // 1. Folder removed was in the scope of a saved search - if so, remove the
+ // uri from the scope of the saved search.
+ // 2. If the scope is now empty, remove the saved search.
+
+ // build a "normalized" uri that we can do a find on.
+ nsCString removedFolderURI;
+ folder->GetURI(removedFolderURI);
+ removedFolderURI.Insert('|', 0);
+ removedFolderURI.Append('|');
+
+ // Enumerate the saved searches.
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter(
+ m_virtualFolderListeners);
+ RefPtr<VirtualFolderChangeListener> listener;
+
+ while (iter.HasMore()) {
+ listener = iter.GetNext();
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsCOMPtr<nsIMsgFolder> savedSearch = listener->m_virtualFolder;
+ savedSearch->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ if (dbFolderInfo) {
+ nsCString searchURI;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI);
+ // "normalize" searchURI so we can search for |folderURI|.
+ searchURI.Insert('|', 0);
+ searchURI.Append('|');
+ int32_t index = searchURI.Find(removedFolderURI);
+ if (index != kNotFound) {
+ RemoveVFListenerForVF(savedSearch, folder);
+
+ // remove |folderURI
+ searchURI.Cut(index, removedFolderURI.Length() - 1);
+ // remove last '|' we added
+ searchURI.SetLength(searchURI.Length() - 1);
+
+ uint32_t vfFolderFlag;
+ dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag);
+
+ // If saved search is empty now, delete it. But not if it's a smart
+ // folder.
+ if (searchURI.IsEmpty() && !vfFolderFlag) {
+ db = nullptr;
+ dbFolderInfo = nullptr;
+
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = savedSearch->GetParent(getter_AddRefs(parent));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!parent) continue;
+ parent->PropagateDelete(savedSearch, true);
+ } else {
+ if (!searchURI.IsEmpty()) {
+ // Remove leading '|' we added (or one after |folderURI, if first
+ // URI).
+ searchURI.Cut(0, 1);
+ }
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI);
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(savedSearch, "search-folders-changed", nullptr);
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnMessageRemoved(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderPropertyChanged(
+ nsIMsgFolder* folder, const nsACString& property,
+ const nsACString& oldValue, const nsACString& newValue) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::OnFolderIntPropertyChanged(nsIMsgFolder* aFolder,
+ const nsACString& aProperty,
+ int64_t oldValue,
+ int64_t newValue) {
+ if (aProperty.Equals(kFolderFlag)) {
+ if (newValue & nsMsgFolderFlags::Virtual) {
+ // This is a virtual folder, let's get out of here.
+ return NS_OK;
+ }
+ uint32_t smartFlagsChanged =
+ (oldValue ^ newValue) &
+ (nsMsgFolderFlags::SpecialUse & ~nsMsgFolderFlags::Queue);
+ if (smartFlagsChanged) {
+ if (smartFlagsChanged & newValue) {
+ // if the smart folder flag was set, calling OnFolderAdded will
+ // do the right thing.
+ nsCOMPtr<nsIMsgFolder> parent;
+ aFolder->GetParent(getter_AddRefs(parent));
+ nsresult rv = OnFolderAdded(parent, aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This folder has one of the smart folder flags.
+ // Remove it from any other smart folders it might've been included in
+ // because of the flags of its ancestors.
+ RemoveFolderFromSmartFolder(
+ aFolder, (nsMsgFolderFlags::SpecialUse & ~nsMsgFolderFlags::Queue) &
+ ~newValue);
+ return NS_OK;
+ }
+ RemoveFolderFromSmartFolder(aFolder, smartFlagsChanged);
+
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ nsresult rv = aFolder->GetDescendants(allDescendants);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (auto subFolder : allDescendants) {
+ RemoveFolderFromSmartFolder(subFolder, smartFlagsChanged);
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgAccountManager::RemoveFolderFromSmartFolder(
+ nsIMsgFolder* aFolder, uint32_t flagsChanged) {
+ nsCString removedFolderURI;
+ aFolder->GetURI(removedFolderURI);
+ removedFolderURI.Insert('|', 0);
+ removedFolderURI.Append('|');
+ uint32_t flags;
+ aFolder->GetFlags(&flags);
+ NS_ASSERTION(!(flags & flagsChanged), "smart folder flag should not be set");
+ // Flag was removed. Look for smart folder based on that flag,
+ // and remove this folder from its scope.
+ for (nsCOMPtr<nsIMsgFolder> virtualFolder : m_virtualFolders) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(db));
+ if (dbFolderInfo) {
+ uint32_t vfFolderFlag;
+ dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag);
+ // found a smart folder over the removed flag
+ if (vfFolderFlag & flagsChanged) {
+ nsCString searchURI;
+ dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI);
+ // "normalize" searchURI so we can search for |folderURI|.
+ searchURI.Insert('|', 0);
+ searchURI.Append('|');
+ int32_t index = searchURI.Find(removedFolderURI);
+ if (index != kNotFound) {
+ RemoveVFListenerForVF(virtualFolder, aFolder);
+
+ // remove |folderURI
+ searchURI.Cut(index, removedFolderURI.Length() - 1);
+ // remove last '|' we added
+ searchURI.SetLength(searchURI.Length() - 1);
+
+ // remove leading '|' we added (or one after |folderURI, if first URI)
+ searchURI.Cut(0, 1);
+ dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI);
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ obs->NotifyObservers(virtualFolder, "search-folders-changed",
+ nullptr);
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderBoolPropertyChanged(
+ nsIMsgFolder* folder, const nsACString& property, bool oldValue,
+ bool newValue) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderUnicharPropertyChanged(
+ nsIMsgFolder* folder, const nsACString& property, const nsAString& oldValue,
+ const nsAString& newValue) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderPropertyFlagChanged(
+ nsIMsgDBHdr* msg, const nsACString& property, uint32_t oldFlag,
+ uint32_t newFlag) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgAccountManager::OnFolderEvent(nsIMsgFolder* aFolder,
+ const nsACString& aEvent) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::FolderUriForPath(nsIFile* aLocalPath,
+ nsACString& aMailboxUri) {
+ NS_ENSURE_ARG_POINTER(aLocalPath);
+ bool equals;
+ if (m_lastPathLookedUp &&
+ NS_SUCCEEDED(aLocalPath->Equals(m_lastPathLookedUp, &equals)) && equals) {
+ aMailboxUri = m_lastFolderURIForPath;
+ return NS_OK;
+ }
+ nsTArray<RefPtr<nsIMsgFolder>> folders;
+ nsresult rv = GetAllFolders(folders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto folder : folders) {
+ nsCOMPtr<nsIFile> folderPath;
+ rv = folder->GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check if we're equal
+ rv = folderPath->Equals(aLocalPath, &equals);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (equals) {
+ rv = folder->GetURI(aMailboxUri);
+ m_lastFolderURIForPath = aMailboxUri;
+ aLocalPath->Clone(getter_AddRefs(m_lastPathLookedUp));
+ return rv;
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::GetSortOrder(nsIMsgIncomingServer* aServer,
+ int32_t* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+
+ // If the passed in server is the default, return its sort order as 0
+ // regardless of its server sort order.
+
+ nsCOMPtr<nsIMsgAccount> defaultAccount;
+ nsresult rv = GetDefaultAccount(getter_AddRefs(defaultAccount));
+ if (NS_SUCCEEDED(rv) && defaultAccount) {
+ nsCOMPtr<nsIMsgIncomingServer> defaultServer;
+ rv = m_defaultAccount->GetIncomingServer(getter_AddRefs(defaultServer));
+ if (NS_SUCCEEDED(rv) && (aServer == defaultServer)) {
+ *aSortOrder = 0;
+ return NS_OK;
+ }
+ // It is OK if there is no default account.
+ }
+
+ // This function returns the sort order by querying the server object for its
+ // sort order value and then incrementing it by the position of the server in
+ // the accounts list. This ensures that even when several accounts have the
+ // same sort order value, the returned value is not the same and keeps
+ // their relative order in the account list when and unstable sort is run
+ // on the returned sort order values.
+ int32_t sortOrder;
+ int32_t serverIndex;
+
+ rv = aServer->GetSortOrder(&sortOrder);
+ if (NS_SUCCEEDED(rv)) rv = FindServerIndex(aServer, &serverIndex);
+
+ if (NS_FAILED(rv)) {
+ *aSortOrder = 999999999;
+ } else {
+ *aSortOrder = sortOrder + serverIndex;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgAccountManager::ReorderAccounts(const nsTArray<nsCString>& newAccounts) {
+ nsTArray<nsCString> allNewAccounts = newAccounts.Clone();
+
+ // Add all hidden accounts to the list of new accounts.
+ nsresult rv;
+ for (auto account : m_accounts) {
+ nsCString key;
+ account->GetKey(key);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = account->GetIncomingServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) {
+ bool hidden = false;
+ rv = server->GetHidden(&hidden);
+ if (NS_SUCCEEDED(rv) && hidden && !allNewAccounts.Contains(key)) {
+ allNewAccounts.AppendElement(key);
+ }
+ }
+ }
+
+ // Check that the new account list contains all the existing accounts,
+ // just in a different order.
+ if (allNewAccounts.Length() != m_accounts.Length())
+ return NS_ERROR_INVALID_ARG;
+
+ for (uint32_t i = 0; i < m_accounts.Length(); i++) {
+ nsCString accountKey;
+ m_accounts[i]->GetKey(accountKey);
+ if (!allNewAccounts.Contains(accountKey)) return NS_ERROR_INVALID_ARG;
+ }
+
+ // In-place swap the elements in m_accounts to the order defined in
+ // newAccounts.
+ for (uint32_t i = 0; i < allNewAccounts.Length(); i++) {
+ nsCString newKey = allNewAccounts[i];
+ for (uint32_t j = i; j < m_accounts.Length(); j++) {
+ nsCString oldKey;
+ m_accounts[j]->GetKey(oldKey);
+ if (newKey.Equals(oldKey)) {
+ if (i != j) std::swap(m_accounts[i], m_accounts[j]);
+ break;
+ }
+ }
+ }
+
+ return OutputAccountsPref();
+}
diff --git a/comm/mailnews/base/src/nsMsgAccountManager.h b/comm/mailnews/base/src/nsMsgAccountManager.h
new file mode 100644
index 0000000000..9dc1e45da3
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgAccountManager.h
@@ -0,0 +1,211 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * This Original Code has been modified by IBM Corporation. Modifications made
+ * by IBM described herein are Copyright (c) International Business Machines
+ * Corporation, 2000. Modifications to Mozilla code or documentation identified
+ * per MPL Section 3.3
+ *
+ * Date Modified by Description of modification
+ * 04/20/2000 IBM Corp. OS/2 VisualAge build.
+ */
+
+#include "nscore.h"
+#include "nsIMsgAccountManager.h"
+#include "nsCOMPtr.h"
+#include "nsISmtpServer.h"
+#include "nsIPrefBranch.h"
+#include "nsIMsgFolderCache.h"
+#include "nsIMsgFolder.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsIUrlListener.h"
+#include "nsCOMArray.h"
+#include "nsIMsgSearchSession.h"
+#include "nsInterfaceHashtable.h"
+#include "nsIMsgDatabase.h"
+#include "nsIDBChangeListener.h"
+#include "nsTObserverArray.h"
+
+class VirtualFolderChangeListener final : public nsIDBChangeListener {
+ public:
+ VirtualFolderChangeListener();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDBCHANGELISTENER
+
+ nsresult Init();
+ /**
+ * Posts an event to update the summary totals and commit the db.
+ * We post the event to avoid committing each time we're called
+ * in a synchronous loop.
+ */
+ nsresult PostUpdateEvent(nsIMsgFolder* folder, nsIMsgDatabase* db);
+ /// Handles event posted to event queue to batch notifications.
+ void ProcessUpdateEvent(nsIMsgFolder* folder, nsIMsgDatabase* db);
+
+ void DecrementNewMsgCount();
+
+ // folder we're listening to db changes on behalf of.
+ nsCOMPtr<nsIMsgFolder> m_virtualFolder;
+ // folder whose db we're listening to.
+ nsCOMPtr<nsIMsgFolder> m_folderWatching;
+ nsCOMPtr<nsIMsgSearchSession> m_searchSession;
+ bool m_searchOnMsgStatus;
+ bool m_batchingEvents;
+
+ private:
+ ~VirtualFolderChangeListener() {}
+};
+
+class nsMsgAccountManager : public nsIMsgAccountManager,
+ public nsIObserver,
+ public nsSupportsWeakReference,
+ public nsIFolderListener {
+ public:
+ nsMsgAccountManager();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /* nsIMsgAccountManager methods */
+
+ NS_DECL_NSIMSGACCOUNTMANAGER
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIFOLDERLISTENER
+
+ nsresult Init();
+ nsresult Shutdown();
+ void LogoutOfServer(nsIMsgIncomingServer* aServer);
+
+ private:
+ virtual ~nsMsgAccountManager();
+
+ bool m_accountsLoaded;
+ nsCOMPtr<nsIMsgFolderCache> m_msgFolderCache;
+ nsTArray<nsCOMPtr<nsIMsgAccount>> m_accounts;
+ nsInterfaceHashtable<nsCStringHashKey, nsIMsgIdentity> m_identities;
+ nsInterfaceHashtable<nsCStringHashKey, nsIMsgIncomingServer>
+ m_incomingServers;
+ nsCOMPtr<nsIMsgAccount> m_defaultAccount;
+ nsCOMArray<nsIIncomingServerListener> m_incomingServerListeners;
+ nsTObserverArray<RefPtr<VirtualFolderChangeListener>>
+ m_virtualFolderListeners;
+ nsTArray<nsCOMPtr<nsIMsgFolder>> m_virtualFolders;
+ nsCOMPtr<nsIMsgFolder> m_folderDoingEmptyTrash;
+ nsCOMPtr<nsIMsgFolder> m_folderDoingCleanupInbox;
+ bool m_emptyTrashInProgress;
+ bool m_cleanupInboxInProgress;
+
+ nsCString mAccountKeyList;
+
+ // These are static because the account manager may go away during
+ // shutdown, and get recreated.
+ static bool m_haveShutdown;
+ static bool m_shutdownInProgress;
+
+ bool m_userAuthenticated;
+ bool m_loadingVirtualFolders;
+ bool m_virtualFoldersLoaded;
+
+ /* we call FindServer() a lot. so cache the last server found */
+ nsCOMPtr<nsIMsgIncomingServer> m_lastFindServerResult;
+ nsCString m_lastFindServerHostName;
+ nsCString m_lastFindServerUserName;
+ int32_t m_lastFindServerPort;
+ nsCString m_lastFindServerType;
+
+ void SetLastServerFound(nsIMsgIncomingServer* server,
+ const nsACString& hostname,
+ const nsACString& username, const int32_t port,
+ const nsACString& type);
+
+ // Cache the results of the last call to FolderUriFromDirInProfile
+ nsCOMPtr<nsIFile> m_lastPathLookedUp;
+ nsCString m_lastFolderURIForPath;
+
+ /* internal creation routines - updates m_identities and m_incomingServers */
+ nsresult createKeyedAccount(const nsCString& key, bool forcePositionToEnd,
+ nsIMsgAccount** _retval);
+ nsresult createKeyedServer(const nsACString& key, const nsACString& username,
+ const nsACString& password, const nsACString& type,
+ nsIMsgIncomingServer** _retval);
+
+ nsresult createKeyedIdentity(const nsACString& key, nsIMsgIdentity** _retval);
+
+ nsresult GetLocalFoldersPrettyName(nsString& localFoldersName);
+
+ /**
+ * Check if the given account can be the set as the default account.
+ */
+ nsresult CheckDefaultAccount(nsIMsgAccount* aAccount, bool& aCanBeDefault);
+
+ /**
+ * Find a new account that can serve as default.
+ */
+ nsresult AutosetDefaultAccount();
+
+ // sets the pref for the default server
+ nsresult setDefaultAccountPref(nsIMsgAccount* aDefaultAccount);
+
+ // Write out the accounts pref from the m_accounts list of accounts.
+ nsresult OutputAccountsPref();
+
+ // fires notifications to the appropriate root folders
+ nsresult notifyDefaultServerChange(nsIMsgAccount* aOldAccount,
+ nsIMsgAccount* aNewAccount);
+
+ //
+ // account enumerators
+ // ("element" is always an account)
+ //
+
+ // find the servers that correspond to the given identity
+ static bool findServersForIdentity(nsISupports* element, void* aData);
+
+ void findAccountByServerKey(const nsCString& aKey, nsIMsgAccount** aResult);
+
+ //
+ // server enumerators
+ // ("element" is always a server)
+ //
+
+ nsresult findServerInternal(const nsACString& username,
+ const nsACString& hostname,
+ const nsACString& type, int32_t port,
+ nsIMsgIncomingServer** aResult);
+
+ // handle virtual folders
+ static nsresult GetVirtualFoldersFile(nsCOMPtr<nsIFile>& file);
+ static nsresult WriteLineToOutputStream(const char* prefix, const char* line,
+ nsIOutputStream* outputStream);
+ void ParseAndVerifyVirtualFolderScope(nsCString& buffer);
+ nsresult AddVFListenersForVF(nsIMsgFolder* virtualFolder,
+ const nsCString& srchFolderUris);
+
+ nsresult RemoveVFListenerForVF(nsIMsgFolder* virtualFolder,
+ nsIMsgFolder* folder);
+
+ nsresult RemoveFolderFromSmartFolder(nsIMsgFolder* aFolder,
+ uint32_t flagsChanged);
+
+ nsresult SetSendLaterUriPref(nsIMsgIncomingServer* server);
+
+ nsCOMPtr<nsIPrefBranch> m_prefs;
+ nsCOMPtr<nsIMsgDBService> m_dbService;
+
+ //
+ // root folder listener stuff
+ //
+
+ // this array is for folder listeners that are supposed to be listening
+ // on the root folders.
+ // When a new server is created, all of the the folder listeners
+ // should be added to the new server
+ // When a new listener is added, it should be added to all root folders.
+ // similar for when servers are deleted or listeners removed
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>> mFolderListeners;
+
+ void removeListenersFromFolder(nsIMsgFolder* aFolder);
+};
diff --git a/comm/mailnews/base/src/nsMsgBiffManager.cpp b/comm/mailnews/base/src/nsMsgBiffManager.cpp
new file mode 100644
index 0000000000..9d990a30bd
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgBiffManager.cpp
@@ -0,0 +1,341 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgBiffManager.h"
+#include "nsIMsgAccountManager.h"
+#include "nsStatusBarBiffManager.h"
+#include "nsCOMArray.h"
+#include "mozilla/Logging.h"
+#include "nspr.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIObserverService.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "nsITimer.h"
+#include "mozilla/Services.h"
+
+#define PREF_BIFF_JITTER "mail.biff.add_interval_jitter"
+
+#define NS_STATUSBARBIFFMANAGER_CID \
+ { \
+ 0x7f9a9fb0, 0x4161, 0x11d4, { \
+ 0x98, 0x76, 0x00, 0xc0, 0x4f, 0xa0, 0xd2, 0xa6 \
+ } \
+ }
+static NS_DEFINE_CID(kStatusBarBiffManagerCID, NS_STATUSBARBIFFMANAGER_CID);
+
+static mozilla::LazyLogModule MsgBiffLogModule("MsgBiff");
+
+NS_IMPL_ISUPPORTS(nsMsgBiffManager, nsIMsgBiffManager,
+ nsIIncomingServerListener, nsIObserver,
+ nsISupportsWeakReference)
+
+void OnBiffTimer(nsITimer* timer, void* aBiffManager) {
+ nsMsgBiffManager* biffManager = (nsMsgBiffManager*)aBiffManager;
+ biffManager->PerformBiff();
+}
+
+nsMsgBiffManager::nsMsgBiffManager() {
+ mHaveShutdown = false;
+ mInited = false;
+}
+
+nsMsgBiffManager::~nsMsgBiffManager() {
+ if (mBiffTimer) mBiffTimer->Cancel();
+
+ if (!mHaveShutdown) Shutdown();
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, "wake_notification");
+ observerService->RemoveObserver(this, "sleep_notification");
+ }
+}
+
+NS_IMETHODIMP nsMsgBiffManager::Init() {
+ if (mInited) return NS_OK;
+
+ mInited = true;
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv)) accountManager->AddIncomingServerListener(this);
+
+ // in turbo mode on profile change we don't need to do anything below this
+ if (mHaveShutdown) {
+ mHaveShutdown = false;
+ return NS_OK;
+ }
+
+ // Ensure status bar biff service has started
+ nsCOMPtr<nsIFolderListener> statusBarBiffService =
+ do_GetService(kStatusBarBiffManagerCID, &rv);
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "sleep_notification", true);
+ observerService->AddObserver(this, "wake_notification", true);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::Shutdown() {
+ if (mBiffTimer) {
+ mBiffTimer->Cancel();
+ mBiffTimer = nullptr;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv)) accountManager->RemoveIncomingServerListener(this);
+
+ mHaveShutdown = true;
+ mInited = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* someData) {
+ if (!strcmp(aTopic, "sleep_notification") && mBiffTimer) {
+ mBiffTimer->Cancel();
+ mBiffTimer = nullptr;
+ } else if (!strcmp(aTopic, "wake_notification")) {
+ // wait 10 seconds after waking up to start biffing again.
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mBiffTimer), OnBiffTimer, (void*)this, 10000,
+ nsITimer::TYPE_ONE_SHOT, "nsMsgBiffManager::OnBiffTimer", nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not start mBiffTimer timer");
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::AddServerBiff(nsIMsgIncomingServer* server) {
+ NS_ENSURE_ARG_POINTER(server);
+
+ int32_t biffMinutes;
+
+ nsresult rv = server->GetBiffMinutes(&biffMinutes);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Don't add if biffMinutes isn't > 0
+ if (biffMinutes > 0) {
+ int32_t serverIndex = FindServer(server);
+ // Only add it if it hasn't been added already.
+ if (serverIndex == -1) {
+ nsBiffEntry biffEntry;
+ biffEntry.server = server;
+ rv = SetNextBiffTime(biffEntry, PR_Now());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AddBiffEntry(biffEntry);
+ SetupNextBiff();
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::RemoveServerBiff(nsIMsgIncomingServer* server) {
+ int32_t pos = FindServer(server);
+ if (pos != -1) mBiffArray.RemoveElementAt(pos);
+
+ // Should probably reset biff time if this was the server that gets biffed
+ // next.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::ForceBiff(nsIMsgIncomingServer* server) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::ForceBiffAll() { return NS_OK; }
+
+NS_IMETHODIMP nsMsgBiffManager::OnServerLoaded(nsIMsgIncomingServer* server) {
+ NS_ENSURE_ARG_POINTER(server);
+
+ bool doBiff = false;
+ nsresult rv = server->GetDoBiff(&doBiff);
+
+ if (NS_SUCCEEDED(rv) && doBiff) rv = AddServerBiff(server);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgBiffManager::OnServerUnloaded(nsIMsgIncomingServer* server) {
+ return RemoveServerBiff(server);
+}
+
+NS_IMETHODIMP nsMsgBiffManager::OnServerChanged(nsIMsgIncomingServer* server) {
+ // nothing required. If the hostname or username changed
+ // the next time biff fires, we'll ping the right server
+ return NS_OK;
+}
+
+int32_t nsMsgBiffManager::FindServer(nsIMsgIncomingServer* server) {
+ uint32_t count = mBiffArray.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ if (server == mBiffArray[i].server.get()) return i;
+ }
+ return -1;
+}
+
+nsresult nsMsgBiffManager::AddBiffEntry(nsBiffEntry& biffEntry) {
+ uint32_t i;
+ uint32_t count = mBiffArray.Length();
+ for (i = 0; i < count; i++) {
+ if (biffEntry.nextBiffTime < mBiffArray[i].nextBiffTime) break;
+ }
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info,
+ ("inserting biff entry at %d", i));
+ mBiffArray.InsertElementAt(i, biffEntry);
+ return NS_OK;
+}
+
+nsresult nsMsgBiffManager::SetNextBiffTime(nsBiffEntry& biffEntry,
+ PRTime currentTime) {
+ nsIMsgIncomingServer* server = biffEntry.server;
+ NS_ENSURE_TRUE(server, NS_ERROR_FAILURE);
+
+ int32_t biffInterval;
+ nsresult rv = server->GetBiffMinutes(&biffInterval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Add biffInterval, converted in microseconds, to current time.
+ // Force 64-bit multiplication.
+ PRTime chosenTimeInterval = biffInterval * 60000000LL;
+ biffEntry.nextBiffTime = currentTime + chosenTimeInterval;
+
+ // Check if we should jitter.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefs) {
+ bool shouldUseBiffJitter = false;
+ prefs->GetBoolPref(PREF_BIFF_JITTER, &shouldUseBiffJitter);
+ if (shouldUseBiffJitter) {
+ // Calculate a jitter of +/-5% on chosenTimeInterval
+ // - minimum 1 second (to avoid a modulo with 0)
+ // - maximum 30 seconds (to avoid problems when biffInterval is very
+ // large)
+ int64_t jitter = (int64_t)(0.05 * (int64_t)chosenTimeInterval);
+ jitter =
+ std::max<int64_t>(1000000LL, std::min<int64_t>(jitter, 30000000LL));
+ jitter = ((rand() % 2) ? 1 : -1) * (rand() % jitter);
+
+ biffEntry.nextBiffTime += jitter;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgBiffManager::SetupNextBiff() {
+ if (mBiffArray.Length() > 0) {
+ // Get the next biff entry
+ const nsBiffEntry& biffEntry = mBiffArray[0];
+ PRTime currentTime = PR_Now();
+ int64_t biffDelay;
+ int64_t ms(1000);
+
+ if (currentTime > biffEntry.nextBiffTime) {
+ // Let's wait 30 seconds before firing biff again
+ biffDelay = 30 * PR_USEC_PER_SEC;
+ } else
+ biffDelay = biffEntry.nextBiffTime - currentTime;
+
+ // Convert biffDelay into milliseconds
+ int64_t timeInMS = biffDelay / ms;
+ uint32_t timeInMSUint32 = (uint32_t)timeInMS;
+
+ // Can't currently reset a timer when it's in the process of
+ // calling Notify. So, just release the timer here and create a new one.
+ if (mBiffTimer) mBiffTimer->Cancel();
+
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info,
+ ("setting %d timer", timeInMSUint32));
+
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mBiffTimer), OnBiffTimer, (void*)this, timeInMSUint32,
+ nsITimer::TYPE_ONE_SHOT, "nsMsgBiffManager::OnBiffTimer", nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not start mBiffTimer timer");
+ }
+ }
+ return NS_OK;
+}
+
+// This is the function that does a biff on all of the servers whose time it is
+// to biff.
+nsresult nsMsgBiffManager::PerformBiff() {
+ PRTime currentTime = PR_Now();
+ nsCOMArray<nsIMsgFolder> targetFolders;
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info, ("performing biffs"));
+
+ uint32_t count = mBiffArray.Length();
+ for (uint32_t i = 0; i < count; i++) {
+ // Take a copy of the entry rather than the a reference so that we can
+ // remove and add if necessary, but keep the references and memory alive.
+ nsBiffEntry current = mBiffArray[i];
+ if (current.nextBiffTime < currentTime) {
+ bool serverBusy = false;
+ bool serverRequiresPassword = true;
+ bool passwordPromptRequired;
+
+ current.server->GetPasswordPromptRequired(&passwordPromptRequired);
+ current.server->GetServerBusy(&serverBusy);
+ current.server->GetServerRequiresPasswordForBiff(&serverRequiresPassword);
+ // find the dest folder we're actually downloading to...
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ current.server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ int32_t targetFolderIndex = targetFolders.IndexOfObject(rootMsgFolder);
+ if (targetFolderIndex == kNotFound)
+ targetFolders.AppendObject(rootMsgFolder);
+
+ // so if we need to be authenticated to biff, check that we are
+ // (since we don't want to prompt the user for password UI)
+ // and make sure the server isn't already in the middle of downloading
+ // new messages
+ if (!serverBusy && (!serverRequiresPassword || !passwordPromptRequired) &&
+ targetFolderIndex == kNotFound) {
+ nsCString serverKey;
+ current.server->GetKey(serverKey);
+ nsresult rv = current.server->PerformBiff(nullptr);
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info,
+ ("biffing server %s rv = %" PRIx32, serverKey.get(),
+ static_cast<uint32_t>(rv)));
+ } else {
+ MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info,
+ ("not biffing server serverBusy = %d requirespassword = %d "
+ "password prompt required = %d targetFolderIndex = %d",
+ serverBusy, serverRequiresPassword, passwordPromptRequired,
+ targetFolderIndex));
+ }
+ // if we didn't do this server because the destination server was already
+ // being biffed into, leave this server in the biff array so it will fire
+ // next.
+ if (targetFolderIndex == kNotFound) {
+ mBiffArray.RemoveElementAt(i);
+ i--; // Because we removed it we need to look at the one that just
+ // moved up.
+ SetNextBiffTime(current, currentTime);
+ AddBiffEntry(current);
+ }
+#ifdef DEBUG_David_Bienvenu
+ else
+ printf("dest account performing biff\n");
+#endif
+ } else
+ // since we're in biff order, there's no reason to keep checking
+ break;
+ }
+ SetupNextBiff();
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgBiffManager.h b/comm/mailnews/base/src/nsMsgBiffManager.h
new file mode 100644
index 0000000000..e334429d71
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgBiffManager.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef NSMSGBIFFMANAGER_H
+#define NSMSGBIFFMANAGER_H
+
+#include "msgCore.h"
+#include "nsIMsgBiffManager.h"
+#include "nsITimer.h"
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+#include "nsIIncomingServerListener.h"
+#include "nsWeakReference.h"
+#include "nsIObserver.h"
+
+typedef struct {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ PRTime nextBiffTime;
+} nsBiffEntry;
+
+class nsMsgBiffManager : public nsIMsgBiffManager,
+ public nsIIncomingServerListener,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ nsMsgBiffManager();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGBIFFMANAGER
+ NS_DECL_NSIINCOMINGSERVERLISTENER
+ NS_DECL_NSIOBSERVER
+
+ nsresult PerformBiff();
+
+ protected:
+ virtual ~nsMsgBiffManager();
+
+ int32_t FindServer(nsIMsgIncomingServer* server);
+ nsresult SetNextBiffTime(nsBiffEntry& biffEntry, PRTime currentTime);
+ nsresult SetupNextBiff();
+ nsresult AddBiffEntry(nsBiffEntry& biffEntry);
+
+ protected:
+ nsCOMPtr<nsITimer> mBiffTimer;
+ nsTArray<nsBiffEntry> mBiffArray;
+ bool mHaveShutdown;
+ bool mInited;
+};
+
+#endif // NSMSGBIFFMANAGER_H
diff --git a/comm/mailnews/base/src/nsMsgCompressIStream.cpp b/comm/mailnews/base/src/nsMsgCompressIStream.cpp
new file mode 100644
index 0000000000..a0dd6b7776
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCompressIStream.cpp
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgCompressIStream.h"
+#include "prio.h"
+#include "prmem.h"
+#include <algorithm>
+
+#define BUFFER_SIZE 16384
+
+nsMsgCompressIStream::nsMsgCompressIStream()
+ : m_dataptr(nullptr), m_dataleft(0), m_inflateAgain(false) {}
+
+nsMsgCompressIStream::~nsMsgCompressIStream() { Close(); }
+
+NS_IMPL_ISUPPORTS(nsMsgCompressIStream, nsIInputStream, nsIAsyncInputStream)
+
+nsresult nsMsgCompressIStream::InitInputStream(nsIInputStream* rawStream) {
+ // protect against repeat calls
+ if (m_iStream) return NS_ERROR_UNEXPECTED;
+
+ // allocate some memory for buffering
+ m_zbuf = mozilla::MakeUnique<char[]>(BUFFER_SIZE);
+ if (!m_zbuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // allocate some memory for buffering
+ m_databuf = mozilla::MakeUnique<char[]>(BUFFER_SIZE);
+ if (!m_databuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // set up zlib object
+ m_zstream.zalloc = Z_NULL;
+ m_zstream.zfree = Z_NULL;
+ m_zstream.opaque = Z_NULL;
+
+ // http://zlib.net/manual.html is rather silent on the topic, but
+ // perl's Compress::Raw::Zlib manual says:
+ // -WindowBits
+ // To compress an RFC 1951 data stream, set WindowBits to -MAX_WBITS.
+ if (inflateInit2(&m_zstream, -MAX_WBITS) != Z_OK) return NS_ERROR_FAILURE;
+
+ m_iStream = rawStream;
+
+ return NS_OK;
+}
+
+nsresult nsMsgCompressIStream::DoInflation() {
+ // if there's something in the input buffer of the zstream, process it.
+ m_zstream.next_out = (Bytef*)m_databuf.get();
+ m_zstream.avail_out = BUFFER_SIZE;
+ int zr = inflate(&m_zstream, Z_SYNC_FLUSH);
+
+ // inflate() should normally be called until it returns
+ // Z_STREAM_END or an error, and Z_BUF_ERROR just means
+ // unable to progress any further (possible if we filled
+ // an output buffer exactly)
+ if (zr == Z_BUF_ERROR || zr == Z_STREAM_END) zr = Z_OK;
+
+ // otherwise it's an error
+ if (zr != Z_OK) return NS_ERROR_FAILURE;
+
+ // http://www.zlib.net/manual.html says:
+ // If inflate returns Z_OK and with zero avail_out, it must be called
+ // again after making room in the output buffer because there might be
+ // more output pending.
+ m_inflateAgain = m_zstream.avail_out ? false : true;
+
+ // set the pointer to the start of the buffer, and the count to how
+ // based on how many bytes are left unconsumed.
+ m_dataptr = m_databuf.get();
+ m_dataleft = BUFFER_SIZE - m_zstream.avail_out;
+
+ return NS_OK;
+}
+
+/* void close (); */
+NS_IMETHODIMP nsMsgCompressIStream::Close() { return CloseWithStatus(NS_OK); }
+
+NS_IMETHODIMP nsMsgCompressIStream::CloseWithStatus(nsresult reason) {
+ nsresult rv = NS_OK;
+
+ if (m_iStream) {
+ // pass the status through to our wrapped stream
+ nsCOMPtr<nsIAsyncInputStream> asyncInputStream =
+ do_QueryInterface(m_iStream);
+ if (asyncInputStream) rv = asyncInputStream->CloseWithStatus(reason);
+
+ // tidy up
+ m_iStream = nullptr;
+ inflateEnd(&m_zstream);
+ }
+
+ // clean up all the buffers
+ m_zbuf = nullptr;
+ m_databuf = nullptr;
+ m_dataptr = nullptr;
+ m_dataleft = 0;
+
+ return rv;
+}
+
+/* unsigned long long available (); */
+NS_IMETHODIMP nsMsgCompressIStream::Available(uint64_t* aResult) {
+ if (!m_iStream) return NS_BASE_STREAM_CLOSED;
+
+ // check if there's anything still in flight
+ if (!m_dataleft && m_inflateAgain) {
+ nsresult rv = DoInflation();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // we'll be returning this many to the next read, guaranteed
+ if (m_dataleft) {
+ *aResult = m_dataleft;
+ return NS_OK;
+ }
+
+ // this value isn't accurate, but will give a good true/false
+ // indication for idle purposes, and next read will fill
+ // m_dataleft, so we'll have an accurate count for the next call.
+ return m_iStream->Available(aResult);
+}
+
+/* [noscript] unsigned long read (in charPtr aBuf, in unsigned long aCount); */
+NS_IMETHODIMP nsMsgCompressIStream::Read(char* aBuf, uint32_t aCount,
+ uint32_t* aResult) {
+ if (!m_iStream) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ // There are two stages of buffering:
+ // * m_zbuf contains the compressed data from the remote server
+ // * m_databuf contains the uncompressed raw bytes for consumption
+ // by the local client.
+ //
+ // Each buffer will only be filled when the following buffers
+ // have been entirely consumed.
+ //
+ // m_dataptr and m_dataleft are respectively a pointer to the
+ // unconsumed portion of m_databuf and the number of bytes
+ // of uncompressed data remaining in m_databuf.
+ //
+ // both buffers have a maximum size of BUFFER_SIZE, so it is
+ // possible that multiple inflate passes will be required to
+ // consume all of m_zbuf.
+ while (!m_dataleft) {
+ // get some more data if we don't already have any
+ if (!m_inflateAgain) {
+ uint32_t bytesRead;
+ nsresult rv =
+ m_iStream->Read(m_zbuf.get(), (uint32_t)BUFFER_SIZE, &bytesRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!bytesRead) return NS_BASE_STREAM_CLOSED;
+ m_zstream.next_in = (Bytef*)m_zbuf.get();
+ m_zstream.avail_in = bytesRead;
+ }
+
+ nsresult rv = DoInflation();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ *aResult = std::min(m_dataleft, aCount);
+
+ if (*aResult) {
+ memcpy(aBuf, m_dataptr, *aResult);
+ m_dataptr += *aResult;
+ m_dataleft -= *aResult;
+ }
+
+ return NS_OK;
+}
+
+/* [noscript] unsigned long readSegments (in nsWriteSegmentFun aWriter, in
+ * voidPtr aClosure, in unsigned long aCount); */
+NS_IMETHODIMP nsMsgCompressIStream::ReadSegments(nsWriteSegmentFun aWriter,
+ void* aClosure,
+ uint32_t aCount,
+ uint32_t* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgCompressIStream::AsyncWait(nsIInputStreamCallback* callback,
+ uint32_t flags, uint32_t amount,
+ nsIEventTarget* target) {
+ if (!m_iStream) return NS_BASE_STREAM_CLOSED;
+
+ nsCOMPtr<nsIAsyncInputStream> asyncInputStream = do_QueryInterface(m_iStream);
+ if (asyncInputStream)
+ return asyncInputStream->AsyncWait(callback, flags, amount, target);
+
+ return NS_OK;
+}
+
+/* boolean isNonBlocking (); */
+NS_IMETHODIMP nsMsgCompressIStream::IsNonBlocking(bool* aNonBlocking) {
+ *aNonBlocking = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompressIStream::StreamStatus() {
+ return m_iStream->StreamStatus();
+}
diff --git a/comm/mailnews/base/src/nsMsgCompressIStream.h b/comm/mailnews/base/src/nsMsgCompressIStream.h
new file mode 100644
index 0000000000..642d35f9fa
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCompressIStream.h
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIInputStream.h"
+#include "nsCOMPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "zlib.h"
+
+class nsMsgCompressIStream final : public nsIAsyncInputStream {
+ public:
+ nsMsgCompressIStream();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIINPUTSTREAM
+ NS_DECL_NSIASYNCINPUTSTREAM
+
+ nsresult InitInputStream(nsIInputStream* rawStream);
+
+ protected:
+ ~nsMsgCompressIStream();
+ nsresult DoInflation();
+ nsCOMPtr<nsIInputStream> m_iStream;
+ mozilla::UniquePtr<char[]> m_zbuf;
+ mozilla::UniquePtr<char[]> m_databuf;
+ char* m_dataptr;
+ uint32_t m_dataleft;
+ bool m_inflateAgain;
+ z_stream m_zstream;
+};
diff --git a/comm/mailnews/base/src/nsMsgCompressOStream.cpp b/comm/mailnews/base/src/nsMsgCompressOStream.cpp
new file mode 100644
index 0000000000..fd490274ca
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCompressOStream.cpp
@@ -0,0 +1,128 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgCompressOStream.h"
+#include "prio.h"
+#include "prmem.h"
+
+#define BUFFER_SIZE 16384
+
+nsMsgCompressOStream::nsMsgCompressOStream() : m_zbuf(nullptr) {}
+
+nsMsgCompressOStream::~nsMsgCompressOStream() { Close(); }
+
+NS_IMPL_ISUPPORTS(nsMsgCompressOStream, nsIOutputStream)
+
+nsresult nsMsgCompressOStream::InitOutputStream(nsIOutputStream* rawStream) {
+ // protect against repeat calls
+ if (m_oStream) return NS_ERROR_UNEXPECTED;
+
+ // allocate some memory for a buffer
+ m_zbuf = mozilla::MakeUnique<char[]>(BUFFER_SIZE);
+ if (!m_zbuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // set up the zlib object
+ m_zstream.zalloc = Z_NULL;
+ m_zstream.zfree = Z_NULL;
+ m_zstream.opaque = Z_NULL;
+
+ // http://zlib.net/manual.html is rather silent on the topic, but
+ // perl's Compress::Raw::Zlib manual says:
+ // -WindowBits [...]
+ // To compress an RFC 1951 data stream, set WindowBits to -MAX_WBITS.
+ if (deflateInit2(&m_zstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS,
+ MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY) != Z_OK)
+ return NS_ERROR_FAILURE;
+
+ m_oStream = rawStream;
+
+ return NS_OK;
+}
+
+/* void close (); */
+NS_IMETHODIMP nsMsgCompressOStream::Close() {
+ if (m_oStream) {
+ m_oStream = nullptr;
+ deflateEnd(&m_zstream);
+ }
+ m_zbuf = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompressOStream::Write(const char* buf, uint32_t count, uint32_t* result) {
+ if (!m_oStream) return NS_BASE_STREAM_CLOSED;
+
+ m_zstream.next_in = (Bytef*)buf;
+ m_zstream.avail_in = count;
+
+ // keep looping until the buffer doesn't get filled
+ do {
+ m_zstream.next_out = (Bytef*)m_zbuf.get();
+ m_zstream.avail_out = BUFFER_SIZE;
+ // Using "Z_SYNC_FLUSH" may cause excess flushes if the calling
+ // code does a lot of small writes. An option with the IMAP
+ // protocol is to check the buffer for "\n" at the end, but
+ // in the interests of keeping this generic, don't optimise
+ // yet. An alternative is to require ->Flush always, but that
+ // is likely to break callers.
+ int zr = deflate(&m_zstream, Z_SYNC_FLUSH);
+ if (zr == Z_STREAM_END || zr == Z_BUF_ERROR)
+ zr = Z_OK; // not an error for our purposes
+ if (zr != Z_OK) return NS_ERROR_FAILURE;
+
+ uint32_t out_size = BUFFER_SIZE - m_zstream.avail_out;
+ const char* out_buf = m_zbuf.get();
+
+ // push everything in the buffer before repeating
+ while (out_size) {
+ uint32_t out_result;
+ nsresult rv = m_oStream->Write(out_buf, out_size, &out_result);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!out_result) return NS_BASE_STREAM_CLOSED;
+ out_size -= out_result;
+ out_buf += out_result;
+ }
+
+ // http://www.zlib.net/manual.html says:
+ // If deflate returns with avail_out == 0, this function must be
+ // called again with the same value of the flush parameter and
+ // more output space (updated avail_out), until the flush is
+ // complete (deflate returns with non-zero avail_out).
+ } while (!m_zstream.avail_out);
+
+ *result = count;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgCompressOStream::Flush(void) {
+ if (!m_oStream) return NS_BASE_STREAM_CLOSED;
+
+ return m_oStream->Flush();
+}
+
+NS_IMETHODIMP
+nsMsgCompressOStream::WriteFrom(nsIInputStream* inStr, uint32_t count,
+ uint32_t* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgCompressOStream::WriteSegments(nsReadSegmentFun reader, void* closure,
+ uint32_t count, uint32_t* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* boolean isNonBlocking (); */
+NS_IMETHODIMP nsMsgCompressOStream::IsNonBlocking(bool* aNonBlocking) {
+ *aNonBlocking = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgCompressOStream::StreamStatus() {
+ return m_oStream->StreamStatus();
+}
diff --git a/comm/mailnews/base/src/nsMsgCompressOStream.h b/comm/mailnews/base/src/nsMsgCompressOStream.h
new file mode 100644
index 0000000000..96d38ad9c0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCompressOStream.h
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsIOutputStream.h"
+#include "nsCOMPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "zlib.h"
+
+class nsMsgCompressOStream final : public nsIOutputStream {
+ public:
+ nsMsgCompressOStream();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIOUTPUTSTREAM
+
+ nsresult InitOutputStream(nsIOutputStream* rawStream);
+
+ protected:
+ ~nsMsgCompressOStream();
+ nsCOMPtr<nsIOutputStream> m_oStream;
+ mozilla::UniquePtr<char[]> m_zbuf;
+ z_stream m_zstream;
+};
diff --git a/comm/mailnews/base/src/nsMsgContentPolicy.cpp b/comm/mailnews/base/src/nsMsgContentPolicy.cpp
new file mode 100644
index 0000000000..6494732cad
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgContentPolicy.cpp
@@ -0,0 +1,928 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgContentPolicy.h"
+#include "nsIMsgMailSession.h"
+#include "nsIPermissionManager.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIAbManager.h"
+#include "nsIAbDirectory.h"
+#include "nsIAbCard.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgHdr.h"
+#include "nsIEncryptedSMIMEURIsSrvc.h"
+#include "nsNetUtil.h"
+#include "nsIMsgComposeService.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIWebNavigation.h"
+#include "nsContentPolicyUtils.h"
+#include "nsFrameLoaderOwner.h"
+#include "nsFrameLoader.h"
+#include "nsMsgUtils.h"
+#include "nsThreadUtils.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "mozilla/dom/HTMLImageElement.h"
+#include "nsINntpUrl.h"
+#include "nsILoadInfo.h"
+#include "nsSandboxFlags.h"
+#include "nsQueryObject.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/SyncRunnable.h"
+#include "nsIObserverService.h"
+
+static const char kBlockRemoteImages[] =
+ "mailnews.message_display.disable_remote_image";
+static const char kTrustedDomains[] = "mail.trusteddomains";
+
+using namespace mozilla;
+using namespace mozilla::mailnews;
+
+// Per message headder flags to keep track of whether the user is allowing
+// remote content for a particular message. if you change or add more values to
+// these constants, be sure to modify the corresponding definitions in
+// mailWindowOverlay.js
+#define kNoRemoteContentPolicy 0
+#define kBlockRemoteContent 1
+#define kAllowRemoteContent 2
+
+NS_IMPL_ISUPPORTS(nsMsgContentPolicy, nsIContentPolicy, nsIMsgContentPolicy,
+ nsIObserver, nsISupportsWeakReference)
+
+nsMsgContentPolicy::nsMsgContentPolicy() { mBlockRemoteImages = true; }
+
+nsMsgContentPolicy::~nsMsgContentPolicy() {
+ // hey, we are going away...clean up after ourself....unregister our observer
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefInternal =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ prefInternal->RemoveObserver(kBlockRemoteImages, this);
+ }
+}
+
+nsresult nsMsgContentPolicy::Init() {
+ nsresult rv;
+
+ // register ourself as an observer on the mail preference to block remote
+ // images
+ nsCOMPtr<nsIPrefBranch> prefInternal =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ prefInternal->AddObserver(kBlockRemoteImages, this, true);
+
+ prefInternal->GetCharPref(kTrustedDomains, mTrustedMailDomains);
+ prefInternal->GetBoolPref(kBlockRemoteImages, &mBlockRemoteImages);
+
+ // Grab a handle on the PermissionManager service for managing allowed remote
+ // content senders.
+ mPermissionManager = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/**
+ * @returns true if the sender referenced by aMsgHdr is explicitly allowed to
+ * load remote images according to the PermissionManager
+ */
+bool nsMsgContentPolicy::ShouldAcceptRemoteContentForSender(
+ nsIMsgDBHdr* aMsgHdr) {
+ if (!aMsgHdr) return false;
+
+ // extract the e-mail address from the msg hdr
+ nsCString author;
+ nsresult rv = aMsgHdr->GetAuthor(getter_Copies(author));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCString emailAddress;
+ ExtractEmail(EncodedHeader(author), emailAddress);
+ if (emailAddress.IsEmpty()) return false;
+
+ nsCOMPtr<nsIIOService> ios =
+ do_GetService("@mozilla.org/network/io-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+ nsCOMPtr<nsIURI> mailURI;
+ emailAddress.InsertLiteral("chrome://messenger/content/email=", 0);
+ rv = ios->NewURI(emailAddress, nullptr, nullptr, getter_AddRefs(mailURI));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // check with permission manager
+ uint32_t permission = 0;
+ mozilla::OriginAttributes attrs;
+ RefPtr<mozilla::BasePrincipal> principal =
+ mozilla::BasePrincipal::CreateContentPrincipal(mailURI, attrs);
+ rv = mPermissionManager->TestPermissionFromPrincipal(principal, "image"_ns,
+ &permission);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Only return true if the permission manager has an explicit allow
+ return (permission == nsIPermissionManager::ALLOW_ACTION);
+}
+
+/**
+ * Extract the host name from aContentLocation, and look it up in our list
+ * of trusted domains.
+ */
+bool nsMsgContentPolicy::IsTrustedDomain(nsIURI* aContentLocation) {
+ bool trustedDomain = false;
+ // get the host name of the server hosting the remote image
+ nsAutoCString host;
+ nsresult rv = aContentLocation->GetHost(host);
+
+ if (NS_SUCCEEDED(rv) && !mTrustedMailDomains.IsEmpty())
+ trustedDomain = MsgHostDomainIsTrusted(host, mTrustedMailDomains);
+
+ return trustedDomain;
+}
+
+NS_IMETHODIMP
+nsMsgContentPolicy::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo,
+ const nsACString& aMimeGuess,
+ int16_t* aDecision) {
+ nsresult rv = NS_OK;
+ ExtContentPolicyType aContentType = aLoadInfo->GetExternalContentPolicyType();
+ nsCOMPtr<nsISupports> aRequestingContext;
+ if (aContentType == ExtContentPolicy::TYPE_DOCUMENT)
+ aRequestingContext = aLoadInfo->ContextForTopLevelLoad();
+ else
+ aRequestingContext = aLoadInfo->LoadingNode();
+ nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadInfo->GetLoadingPrincipal();
+ nsCOMPtr<nsIURI> aRequestingLocation;
+ if (loadingPrincipal) {
+ BasePrincipal::Cast(loadingPrincipal)
+ ->GetURI(getter_AddRefs(aRequestingLocation));
+ }
+
+ // The default decision at the start of the function is to accept the load.
+ // Once we have checked the content type and the requesting location, then
+ // we switch it to reject.
+ //
+ // Be very careful about returning error codes - if this method returns an
+ // NS_ERROR_*, any decision made here will be ignored, and the document could
+ // be accepted when we don't want it to be.
+ //
+ // In most cases if an error occurs, its something we didn't expect so we
+ // should be rejecting the document anyway.
+ *aDecision = nsIContentPolicy::ACCEPT;
+
+ NS_ENSURE_ARG_POINTER(aContentLocation);
+
+#ifdef DEBUG_MsgContentPolicy
+ fprintf(stderr, "aContentType: %d\naContentLocation = %s\n", aContentType,
+ aContentLocation->GetSpecOrDefault().get());
+ fprintf(stderr, "aRequestingContext is %s\n",
+ aRequestingContext ? "not null" : "null");
+#endif
+
+#ifndef MOZ_THUNDERBIRD
+ // Go find out if we are dealing with mailnews. Anything else
+ // isn't our concern and we accept content.
+ nsCOMPtr<nsIDocShell> rootDocShell;
+ rv = GetRootDocShellForContext(aRequestingContext,
+ getter_AddRefs(rootDocShell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We only want to deal with mailnews
+ if (rootDocShell->GetAppType() != nsIDocShell::APP_TYPE_MAIL) return NS_OK;
+#endif
+
+ switch (aContentType) {
+ // Plugins (nsIContentPolicy::TYPE_OBJECT) are blocked on document load.
+ case ExtContentPolicy::TYPE_DOCUMENT:
+ // At this point, we have no intention of supporting a different JS
+ // setting on a subdocument, so we don't worry about TYPE_SUBDOCUMENT
+ // here.
+
+ if (NS_IsMainThread()) {
+ rv = SetDisableItemsOnMailNewsUrlDocshells(aContentLocation, aLoadInfo);
+ } else {
+ auto SetDisabling = [&, location = nsCOMPtr(aContentLocation),
+ loadInfo = nsCOMPtr(aLoadInfo)]() -> auto {
+ rv = SetDisableItemsOnMailNewsUrlDocshells(location, loadInfo);
+ };
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction("SetDisabling", SetDisabling);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ }
+ // if something went wrong during the tweaking, reject this content
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to set disable items on docShells");
+ *aDecision = nsIContentPolicy::REJECT_TYPE;
+ return NS_OK;
+ }
+ break;
+
+ case ExtContentPolicy::TYPE_CSP_REPORT:
+ // We cannot block CSP reports.
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ break;
+
+ default:
+ break;
+ }
+
+ // NOTE: Not using NS_ENSURE_ARG_POINTER because this is a legitimate case
+ // that can happen. Also keep in mind that the default policy used for a
+ // failure code is ACCEPT.
+ if (!aRequestingLocation) return NS_ERROR_INVALID_POINTER;
+
+#ifdef DEBUG_MsgContentPolicy
+ fprintf(stderr, "aRequestingLocation = %s\n",
+ aRequestingLocation->GetSpecOrDefault().get());
+#endif
+
+ // If the requesting location is safe, accept the content location request.
+ if (IsSafeRequestingLocation(aRequestingLocation)) return rv;
+
+ // Now default to reject so early returns via NS_ENSURE_SUCCESS
+ // cause content to be rejected.
+ *aDecision = nsIContentPolicy::REJECT_REQUEST;
+
+ // We want to establish the following:
+ // \--------\ requester | | |
+ // content \------------\ | | |
+ // requested \| mail message | news message | http(s)/data etc.
+ // -------------------------+---------------+--------------+------------------
+ // mail message content | load if same | don't load | don't load
+ // mailbox, imap, JsAccount | message (1) | (2) | (3)
+ // -------------------------+---------------+--------------+------------------
+ // news message | don't load (4)| load (5) | load (6)
+ // -------------------------+---------------+--------------+------------------
+ // http(s)/data, etc. | (default) | (default) | (default)
+ // -------------------------+---------------+--------------+------------------
+ nsCOMPtr<nsIMsgMessageUrl> contentURL(do_QueryInterface(aContentLocation));
+ if (contentURL) {
+ nsCOMPtr<nsINntpUrl> contentNntpURL(do_QueryInterface(aContentLocation));
+ if (!contentNntpURL) {
+ // Mail message (mailbox, imap or JsAccount) content requested, for
+ // example a message part, like an image: To load mail message content the
+ // requester must have the same "normalized" principal. This is basically
+ // a "same origin" test, it protects against cross-loading of mail message
+ // content from other mail or news messages.
+ nsCOMPtr<nsIMsgMessageUrl> requestURL(
+ do_QueryInterface(aRequestingLocation));
+ // If the request URL is not also a message URL, then we don't accept.
+ if (requestURL) {
+ nsCString contentPrincipalSpec, requestPrincipalSpec;
+ nsresult rv1 = contentURL->GetNormalizedSpec(contentPrincipalSpec);
+ nsresult rv2 = requestURL->GetNormalizedSpec(requestPrincipalSpec);
+ if (NS_SUCCEEDED(rv1) && NS_SUCCEEDED(rv2) &&
+ contentPrincipalSpec.Equals(requestPrincipalSpec))
+ *aDecision = nsIContentPolicy::ACCEPT; // (1)
+ }
+ return NS_OK; // (2) and (3)
+ }
+
+ // News message content requested. Don't accept request coming
+ // from a mail message since it would access the news server.
+ nsCOMPtr<nsIMsgMessageUrl> requestURL(
+ do_QueryInterface(aRequestingLocation));
+ if (requestURL) {
+ nsCOMPtr<nsINntpUrl> requestNntpURL(
+ do_QueryInterface(aRequestingLocation));
+ if (!requestNntpURL) return NS_OK; // (4)
+ }
+ *aDecision = nsIContentPolicy::ACCEPT; // (5) and (6)
+ return NS_OK;
+ }
+
+ // If exposed protocol not covered by the test above or protocol that has been
+ // specifically exposed by an add-on, or is a chrome url, then allow the load.
+ if (IsExposedProtocol(aContentLocation)) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ // Never load unexposed protocols except for web protocols and file.
+ // Protocols like ftp are always blocked.
+ if (ShouldBlockUnexposedProtocol(aContentLocation)) return NS_OK;
+
+ // Mailnews URIs are not loaded in child processes, so I think that beyond
+ // here, if we're in a child process, the decision will always be accept.
+ //
+ // targetContext->Canonical does not work in a child process, so we can't
+ // really move on anyway.
+ if (!XRE_IsParentProcess()) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ // Find out the URI that originally initiated the set of requests for this
+ // context.
+ RefPtr<mozilla::dom::BrowsingContext> targetContext;
+ rv = aLoadInfo->GetTargetBrowsingContext(getter_AddRefs(targetContext));
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ if (!targetContext) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIURI> originatorLocation;
+ dom::CanonicalBrowsingContext* cbc = targetContext->Canonical();
+ if (cbc) {
+ dom::WindowGlobalParent* wgp = cbc->GetCurrentWindowGlobal();
+ if (wgp) {
+ originatorLocation = wgp->GetDocumentURI();
+ }
+ }
+ if (!originatorLocation) {
+ return NS_OK;
+ }
+
+#ifdef DEBUG_MsgContentPolicy
+ fprintf(stderr, "originatorLocation = %s\n",
+ originatorLocation->GetSpecOrDefault().get());
+#endif
+
+ // Don't load remote content for encrypted messages.
+ nsCOMPtr<nsIEncryptedSMIMEURIsService> encryptedURIService = do_GetService(
+ "@mozilla.org/messenger-smime/smime-encrypted-uris-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool isEncrypted;
+ rv = encryptedURIService->IsEncrypted(aRequestingLocation->GetSpecOrDefault(),
+ &isEncrypted);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isEncrypted) {
+ *aDecision = nsIContentPolicy::REJECT_REQUEST;
+ NotifyContentWasBlocked(targetContext->Id(), aContentLocation);
+ return NS_OK;
+ }
+
+ // If we are allowing all remote content...
+ if (!mBlockRemoteImages) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ uint32_t permission;
+ mozilla::OriginAttributes attrs;
+ RefPtr<mozilla::BasePrincipal> principal =
+ mozilla::BasePrincipal::CreateContentPrincipal(aContentLocation, attrs);
+ mPermissionManager->TestPermissionFromPrincipal(principal, "image"_ns,
+ &permission);
+ switch (permission) {
+ case nsIPermissionManager::UNKNOWN_ACTION: {
+ // No exception was found for this location.
+ break;
+ }
+ case nsIPermissionManager::ALLOW_ACTION: {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+ case nsIPermissionManager::DENY_ACTION: {
+ *aDecision = nsIContentPolicy::REJECT_REQUEST;
+ return NS_OK;
+ }
+ }
+
+ // Handle compose windows separately from mail. Work out if we're in a compose
+ // window or not.
+ nsCOMPtr<nsIMsgCompose> msgCompose =
+ GetMsgComposeForBrowsingContext(targetContext);
+ if (msgCompose) {
+ ComposeShouldLoad(msgCompose, aRequestingContext, originatorLocation,
+ aContentLocation, aDecision);
+ return NS_OK;
+ }
+
+ // Allow content when using a remote page.
+ bool isHttp;
+ bool isHttps;
+ rv = originatorLocation->SchemeIs("http", &isHttp);
+ nsresult rv2 = originatorLocation->SchemeIs("https", &isHttps);
+ if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv2) && (isHttp || isHttps)) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+ }
+
+ // The default decision is still to reject.
+ ShouldAcceptContentForPotentialMsg(targetContext->Id(), aRequestingLocation,
+ aContentLocation, aDecision);
+ return NS_OK;
+}
+
+/**
+ * Determines if the requesting location is a safe one, i.e. its under the
+ * app/user's control - so file, about, chrome etc.
+ */
+bool nsMsgContentPolicy::IsSafeRequestingLocation(nsIURI* aRequestingLocation) {
+ if (!aRequestingLocation) return false;
+
+ // If aRequestingLocation is one of chrome, resource, file or view-source,
+ // allow aContentLocation to load.
+ bool isChrome;
+ bool isRes;
+ bool isFile;
+ bool isViewSource;
+
+ nsresult rv = aRequestingLocation->SchemeIs("chrome", &isChrome);
+ NS_ENSURE_SUCCESS(rv, false);
+ rv = aRequestingLocation->SchemeIs("resource", &isRes);
+ NS_ENSURE_SUCCESS(rv, false);
+ rv = aRequestingLocation->SchemeIs("file", &isFile);
+ NS_ENSURE_SUCCESS(rv, false);
+ rv = aRequestingLocation->SchemeIs("view-source", &isViewSource);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (isChrome || isRes || isFile || isViewSource) return true;
+
+ // Only allow about: to load anything if the requesting location is not the
+ // special about:blank one.
+ bool isAbout;
+ rv = aRequestingLocation->SchemeIs("about", &isAbout);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (!isAbout) return false;
+
+ nsCString fullSpec;
+ rv = aRequestingLocation->GetSpec(fullSpec);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return !fullSpec.EqualsLiteral("about:blank");
+}
+
+/**
+ * Determines if the content location is a scheme that we're willing to expose
+ * for unlimited loading of content.
+ */
+bool nsMsgContentPolicy::IsExposedProtocol(nsIURI* aContentLocation) {
+ nsAutoCString contentScheme;
+ nsresult rv = aContentLocation->GetScheme(contentScheme);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Check some exposed protocols. Not all protocols in the list of
+ // network.protocol-handler.expose.* prefs in all-thunderbird.js are
+ // admitted purely based on their scheme.
+ // news, snews, nntp, imap and mailbox are checked before the call
+ // to this function by matching content location and requesting location.
+ if (contentScheme.LowerCaseEqualsLiteral("mailto")) return true;
+
+ if (contentScheme.LowerCaseEqualsLiteral("about")) {
+ // We want to allow about pages to load content freely. But not about:blank.
+ nsAutoCString fullSpec;
+ rv = aContentLocation->GetSpec(fullSpec);
+ NS_ENSURE_SUCCESS(rv, false);
+ if (fullSpec.EqualsLiteral("about:blank")) {
+ return false;
+ }
+ return true;
+ }
+
+ // check if customized exposed scheme
+ if (mCustomExposedProtocols.Contains(contentScheme)) return true;
+
+ bool isChrome;
+ rv = aContentLocation->SchemeIs("chrome", &isChrome);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool isRes;
+ rv = aContentLocation->SchemeIs("resource", &isRes);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool isData;
+ rv = aContentLocation->SchemeIs("data", &isData);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool isMozExtension;
+ rv = aContentLocation->SchemeIs("moz-extension", &isMozExtension);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ return isChrome || isRes || isData || isMozExtension;
+}
+
+/**
+ * We block most unexposed protocols that access remote data
+ * - apart from web protocols, and file.
+ */
+bool nsMsgContentPolicy::ShouldBlockUnexposedProtocol(
+ nsIURI* aContentLocation) {
+ // Error condition - we must return true so that we block.
+
+ // about:blank is "web", it should not be blocked.
+ nsAutoCString fullSpec;
+ nsresult rv = aContentLocation->GetSpec(fullSpec);
+ NS_ENSURE_SUCCESS(rv, true);
+ if (fullSpec.EqualsLiteral("about:blank")) {
+ return false;
+ }
+
+ bool isHttp;
+ rv = aContentLocation->SchemeIs("http", &isHttp);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isHttps;
+ rv = aContentLocation->SchemeIs("https", &isHttps);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isWs; // websocket
+ rv = aContentLocation->SchemeIs("ws", &isWs);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isWss; // secure websocket
+ rv = aContentLocation->SchemeIs("wss", &isWss);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isBlob;
+ rv = aContentLocation->SchemeIs("blob", &isBlob);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ bool isFile;
+ rv = aContentLocation->SchemeIs("file", &isFile);
+ NS_ENSURE_SUCCESS(rv, true);
+
+ return !isHttp && !isHttps && !isWs && !isWss && !isBlob && !isFile;
+}
+
+/**
+ * The default for this function will be to reject the content request.
+ * When determining if to allow the request for a given msg hdr, the function
+ * will go through the list of remote content blocking criteria:
+ *
+ * #1 Allow if there is a db header for a manual override.
+ * #2 Allow if the message is in an RSS folder.
+ * #3 Allow if the domain for the remote image in our white list.
+ * #4 Allow if the author has been specifically white listed.
+ */
+int16_t nsMsgContentPolicy::ShouldAcceptRemoteContentForMsgHdr(
+ nsIMsgDBHdr* aMsgHdr, nsIURI* aRequestingLocation,
+ nsIURI* aContentLocation) {
+ if (!aMsgHdr) return static_cast<int16_t>(nsIContentPolicy::REJECT_REQUEST);
+
+ // Case #1, check the db hdr for the remote content policy on this particular
+ // message.
+ uint32_t remoteContentPolicy = kNoRemoteContentPolicy;
+ aMsgHdr->GetUint32Property("remoteContentPolicy", &remoteContentPolicy);
+
+ // Case #2, check if the message is in an RSS folder
+ bool isRSS = false;
+ IsRSSArticle(aRequestingLocation, &isRSS);
+
+ // Case #3, the domain for the remote image is in our white list
+ bool trustedDomain = IsTrustedDomain(aContentLocation);
+
+ // Case 4 means looking up items in the permissions database. So if
+ // either of the two previous items means we load the data, just do it.
+ if (isRSS || remoteContentPolicy == kAllowRemoteContent || trustedDomain)
+ return nsIContentPolicy::ACCEPT;
+
+ // Case #4, author is in our white list..
+ bool allowForSender = ShouldAcceptRemoteContentForSender(aMsgHdr);
+
+ int16_t result = allowForSender
+ ? static_cast<int16_t>(nsIContentPolicy::ACCEPT)
+ : static_cast<int16_t>(nsIContentPolicy::REJECT_REQUEST);
+
+ // kNoRemoteContentPolicy means we have never set a value on the message
+ if (result == nsIContentPolicy::REJECT_REQUEST && !remoteContentPolicy)
+ aMsgHdr->SetUint32Property("remoteContentPolicy", kBlockRemoteContent);
+
+ return result;
+}
+
+class RemoteContentNotifierEvent : public mozilla::Runnable {
+ public:
+ RemoteContentNotifierEvent(uint64_t aBrowsingContextId, nsIURI* aContentURI)
+ : mozilla::Runnable("RemoteContentNotifierEvent"),
+ mBrowsingContextId(aBrowsingContextId),
+ mContentURI(aContentURI) {}
+
+ NS_IMETHOD Run() {
+ nsAutoString data;
+ data.AppendInt(mBrowsingContextId);
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ observerService->NotifyObservers(mContentURI, "remote-content-blocked",
+ data.get());
+ return NS_OK;
+ }
+
+ private:
+ uint64_t mBrowsingContextId;
+ nsCOMPtr<nsIURI> mContentURI;
+};
+
+/**
+ * This function is used to show a blocked remote content notification.
+ */
+void nsMsgContentPolicy::NotifyContentWasBlocked(uint64_t aBrowsingContextId,
+ nsIURI* aContentLocation) {
+ // Post this as an event because it can cause dom mutations, and we
+ // get called at a bad time to be causing dom mutations.
+ NS_DispatchToCurrentThread(
+ new RemoteContentNotifierEvent(aBrowsingContextId, aContentLocation));
+}
+
+/**
+ * This function is used to determine if we allow content for a remote message.
+ * If we reject loading remote content, then we'll inform the message window
+ * that this message has remote content (and hence we are not loading it).
+ *
+ * See ShouldAcceptRemoteContentForMsgHdr for the actual decisions that
+ * determine if we are going to allow remote content.
+ */
+void nsMsgContentPolicy::ShouldAcceptContentForPotentialMsg(
+ uint64_t aBrowsingContextId, nsIURI* aRequestingLocation,
+ nsIURI* aContentLocation, int16_t* aDecision) {
+ NS_ASSERTION(
+ *aDecision == nsIContentPolicy::REJECT_REQUEST,
+ "AllowContentForPotentialMessage expects default decision to be reject!");
+
+ // Is it a mailnews url?
+ nsresult rv;
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl(
+ do_QueryInterface(aRequestingLocation, &rv));
+ if (NS_FAILED(rv)) {
+ // It isn't a mailnews url - so we accept the load here, and let other
+ // content policies make the decision if we should be loading it or not.
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return;
+ }
+
+ nsCString resourceURI;
+ rv = msgUrl->GetUri(resourceURI);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(resourceURI, getter_AddRefs(msgHdr));
+
+ // Get a decision on whether or not to allow remote content for this message
+ // header.
+ *aDecision = ShouldAcceptRemoteContentForMsgHdr(msgHdr, aRequestingLocation,
+ aContentLocation);
+
+ // If we're not allowing the remote content, tell the nsIMsgWindow loading
+ // this url that this is the case, so that the UI knows to show the remote
+ // content header bar, so the user can override if they wish.
+ if (*aDecision == nsIContentPolicy::REJECT_REQUEST) {
+ NotifyContentWasBlocked(aBrowsingContextId, aContentLocation);
+ }
+}
+
+/**
+ * Content policy logic for compose windows
+ */
+void nsMsgContentPolicy::ComposeShouldLoad(nsIMsgCompose* aMsgCompose,
+ nsISupports* aRequestingContext,
+ nsIURI* aOriginatorLocation,
+ nsIURI* aContentLocation,
+ int16_t* aDecision) {
+ NS_ASSERTION(*aDecision == nsIContentPolicy::REJECT_REQUEST,
+ "ComposeShouldLoad expects default decision to be reject!");
+
+ nsCString originalMsgURI;
+ nsresult rv = aMsgCompose->GetOriginalMsgURI(originalMsgURI);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ if (!originalMsgURI.IsEmpty()) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(originalMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ *aDecision =
+ ShouldAcceptRemoteContentForMsgHdr(msgHdr, nullptr, aContentLocation);
+
+ if (!aOriginatorLocation->GetSpecOrDefault().EqualsLiteral(
+ "about:blank?compose")) {
+ return;
+ }
+ }
+
+ // We want to allow the user to add remote content, but do that only when
+ // the allowRemoteContent was set. This way quoted remoted content won't
+ // automatically load, but e.g. pasted content will load because the UI
+ // code toggles the flag.
+ nsCOMPtr<mozilla::dom::Element> element =
+ do_QueryInterface(aRequestingContext);
+ RefPtr<mozilla::dom::HTMLImageElement> image =
+ mozilla::dom::HTMLImageElement::FromNodeOrNull(element);
+ if (image) {
+ // Special case image elements.
+ bool allowRemoteContent = false;
+ aMsgCompose->GetAllowRemoteContent(&allowRemoteContent);
+ if (allowRemoteContent) {
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return;
+ }
+ }
+}
+
+already_AddRefed<nsIMsgCompose>
+nsMsgContentPolicy::GetMsgComposeForBrowsingContext(
+ mozilla::dom::BrowsingContext* aBrowsingContext) {
+ nsresult rv;
+
+ nsIDocShell* shell = aBrowsingContext->GetDocShell();
+ if (!shell) return nullptr;
+ nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem(shell);
+
+ nsCOMPtr<nsIDocShellTreeItem> rootItem;
+ rv = docShellTreeItem->GetInProcessSameTypeRootTreeItem(
+ getter_AddRefs(rootItem));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(rootItem, &rv));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIMsgComposeService> composeService(
+ do_GetService("@mozilla.org/messengercompose;1", &rv));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIMsgCompose> msgCompose;
+ // Don't bother checking rv, as GetMsgComposeForDocShell returns
+ // NS_ERROR_FAILURE for not found.
+ composeService->GetMsgComposeForDocShell(docShell,
+ getter_AddRefs(msgCompose));
+ return msgCompose.forget();
+}
+
+nsresult nsMsgContentPolicy::SetDisableItemsOnMailNewsUrlDocshells(
+ nsIURI* aContentLocation, nsILoadInfo* aLoadInfo) {
+ // XXX if this class changes so that this method can be called from
+ // ShouldProcess, and if it's possible for this to be null when called from
+ // ShouldLoad, but not in the corresponding ShouldProcess call,
+ // we need to re-think the assumptions underlying this code.
+
+ NS_ENSURE_ARG_POINTER(aContentLocation);
+ NS_ENSURE_ARG_POINTER(aLoadInfo);
+
+ RefPtr<mozilla::dom::BrowsingContext> browsingContext =
+ aLoadInfo->GetTargetBrowsingContext();
+ if (!browsingContext) {
+ return NS_OK;
+ }
+
+ // We're only worried about policy settings in content docshells.
+ if (!browsingContext->IsContent()) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIDocShell> docShell = browsingContext->GetDocShell();
+ if (!docShell) {
+ // If there's no docshell to get to, there's nowhere for the JavaScript to
+ // run, so we're already safe and don't need to disable anything.
+ return NS_OK;
+ }
+
+ // Ensure starting off unsandboxed. We sandbox later if needed.
+ MOZ_ALWAYS_SUCCEEDS(browsingContext->SetSandboxFlags(SANDBOXED_NONE));
+
+ nsresult rv;
+ bool isAllowedContent = !ShouldBlockUnexposedProtocol(aContentLocation);
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(aContentLocation);
+ if (!msgUrl && !isAllowedContent) {
+ // If it's not a mailnews url or allowed content url (http[s]|file) then
+ // bail; otherwise set whether JavaScript is allowed.
+ return NS_OK;
+ }
+
+ if (!isAllowedContent) {
+ // Disable JavaScript on message URLs.
+ rv = browsingContext->SetAllowJavascript(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = browsingContext->SetAllowContentRetargetingOnChildren(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // NOTE! Do not set single sandboxing flags only. Sandboxing - when used -
+ // starts off with all things sandboxed, and individual sandbox keywords
+ // will *allow* the specific feature.
+ // Disabling by setting single flags without starting off with all things
+ // sandboxed would the normal assumptions about sandboxing.
+ // The flags - contrary to the keywords - *prevent* a given feature.
+ uint32_t sandboxFlags = SANDBOX_ALL_FLAGS;
+
+ // Do not block links with target attribute from opening (at all).
+ // xref bug 421310 - we would like to prevent using target, but *handle*
+ // links like the target wasn't there.
+ sandboxFlags &= ~SANDBOXED_AUXILIARY_NAVIGATION;
+
+ // For some unexplicable reason, when SANDBOXED_ORIGIN is in affect, then
+ // images will not work with test --verify. So unset it.
+ sandboxFlags &= ~SANDBOXED_ORIGIN;
+
+ // Having both SANDBOXED_TOPLEVEL_NAVIGATION and
+ // SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION will generate a warning,
+ // see BothAllowTopNavigationAndUserActivationPresent. So unset it.
+ sandboxFlags &= ~SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION;
+
+ rv = browsingContext->SetSandboxFlags(sandboxFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // JavaScript is allowed on non-message URLs.
+ rv = browsingContext->SetAllowJavascript(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = browsingContext->SetAllowContentRetargetingOnChildren(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = docShell->SetAllowPlugins(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+/**
+ * Gets the root docshell from a requesting context.
+ */
+nsresult nsMsgContentPolicy::GetRootDocShellForContext(
+ nsISupports* aRequestingContext, nsIDocShell** aDocShell) {
+ NS_ENSURE_ARG_POINTER(aRequestingContext);
+ nsresult rv;
+
+ nsIDocShell* shell = NS_CP_GetDocShellFromContext(aRequestingContext);
+ NS_ENSURE_TRUE(shell, NS_ERROR_NULL_POINTER);
+ nsCOMPtr<nsIDocShellTreeItem> docshellTreeItem(shell);
+
+ nsCOMPtr<nsIDocShellTreeItem> rootItem;
+ rv = docshellTreeItem->GetInProcessRootTreeItem(getter_AddRefs(rootItem));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return CallQueryInterface(rootItem, aDocShell);
+}
+
+/**
+ * Gets the originating URI that started off a set of requests, accounting
+ * for multiple iframes.
+ *
+ * Navigates up the docshell tree from aRequestingContext and finds the
+ * highest parent with the same type docshell as aRequestingContext, then
+ * returns the URI associated with that docshell.
+ */
+nsresult nsMsgContentPolicy::GetOriginatingURIForContext(
+ nsISupports* aRequestingContext, nsIURI** aURI) {
+ NS_ENSURE_ARG_POINTER(aRequestingContext);
+ nsresult rv;
+
+ nsIDocShell* shell = NS_CP_GetDocShellFromContext(aRequestingContext);
+ if (!shell) {
+ *aURI = nullptr;
+ return NS_OK;
+ }
+ nsCOMPtr<nsIDocShellTreeItem> docshellTreeItem(shell);
+
+ nsCOMPtr<nsIDocShellTreeItem> rootItem;
+ rv = docshellTreeItem->GetInProcessSameTypeRootTreeItem(
+ getter_AddRefs(rootItem));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIWebNavigation> webNavigation(do_QueryInterface(rootItem, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return webNavigation->GetCurrentURI(aURI);
+}
+
+NS_IMETHODIMP
+nsMsgContentPolicy::ShouldProcess(nsIURI* aContentLocation,
+ nsILoadInfo* aLoadInfo,
+ const nsACString& aMimeGuess,
+ int16_t* aDecision) {
+ // XXX Returning ACCEPT is presumably only a reasonable thing to do if we
+ // think that ShouldLoad is going to catch all possible cases (i.e. that
+ // everything we use to make decisions is going to be available at
+ // ShouldLoad time, and not only become available in time for ShouldProcess).
+ // Do we think that's actually the case?
+ *aDecision = nsIContentPolicy::ACCEPT;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgContentPolicy::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, aTopic)) {
+ NS_LossyConvertUTF16toASCII pref(aData);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIPrefBranch> prefBranchInt = do_QueryInterface(aSubject, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (pref.Equals(kBlockRemoteImages))
+ prefBranchInt->GetBoolPref(kBlockRemoteImages, &mBlockRemoteImages);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Implementation of nsIMsgContentPolicy
+ *
+ */
+NS_IMETHODIMP
+nsMsgContentPolicy::AddExposedProtocol(const nsACString& aScheme) {
+ if (mCustomExposedProtocols.Contains(nsCString(aScheme))) return NS_OK;
+
+ mCustomExposedProtocols.AppendElement(aScheme);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgContentPolicy::RemoveExposedProtocol(const nsACString& aScheme) {
+ mCustomExposedProtocols.RemoveElement(nsCString(aScheme));
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgContentPolicy.h b/comm/mailnews/base/src/nsMsgContentPolicy.h
new file mode 100644
index 0000000000..e6d3d10e79
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgContentPolicy.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/******************************************************************************
+ * nsMsgContentPolicy enforces the specified content policy on images, js,
+ * plugins, etc. This is the class used to determine what elements in a message
+ * should be loaded.
+ *
+ * nsMsgCookiePolicy enforces our cookie policy for mail and RSS messages.
+ ******************************************************************************/
+
+#ifndef _nsMsgContentPolicy_H_
+#define _nsMsgContentPolicy_H_
+
+#include "nsIContentPolicy.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsString.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgCompose.h"
+#include "nsIDocShell.h"
+#include "nsIPermissionManager.h"
+#include "nsIMsgContentPolicy.h"
+#include "nsTArray.h"
+
+/* DBFCFDF0-4489-4faa-8122-190FD1EFA16C */
+#define NS_MSGCONTENTPOLICY_CID \
+ { \
+ 0xdbfcfdf0, 0x4489, 0x4faa, { \
+ 0x81, 0x22, 0x19, 0xf, 0xd1, 0xef, 0xa1, 0x6c \
+ } \
+ }
+
+#define NS_MSGCONTENTPOLICY_CONTRACTID "@mozilla.org/messenger/content-policy;1"
+
+class nsIMsgDBHdr;
+class nsIDocShell;
+
+class nsMsgContentPolicy : public nsIContentPolicy,
+ public nsIObserver,
+ public nsIMsgContentPolicy,
+ public nsSupportsWeakReference {
+ public:
+ nsMsgContentPolicy();
+
+ nsresult Init();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSICONTENTPOLICY
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIMSGCONTENTPOLICY
+
+ protected:
+ virtual ~nsMsgContentPolicy();
+
+ bool mBlockRemoteImages;
+ nsCString mTrustedMailDomains;
+ nsCOMPtr<nsIPermissionManager> mPermissionManager;
+
+ bool IsTrustedDomain(nsIURI* aContentLocation);
+ bool IsSafeRequestingLocation(nsIURI* aRequestingLocation);
+ bool IsExposedProtocol(nsIURI* aContentLocation);
+ bool IsExposedChromeProtocol(nsIURI* aContentLocation);
+ bool ShouldBlockUnexposedProtocol(nsIURI* aContentLocation);
+
+ bool ShouldAcceptRemoteContentForSender(nsIMsgDBHdr* aMsgHdr);
+ int16_t ShouldAcceptRemoteContentForMsgHdr(nsIMsgDBHdr* aMsgHdr,
+ nsIURI* aRequestingLocation,
+ nsIURI* aContentLocation);
+ void NotifyContentWasBlocked(uint64_t aBrowsingContextId,
+ nsIURI* aContentLocation);
+ void ShouldAcceptContentForPotentialMsg(uint64_t aBrowsingContextId,
+ nsIURI* aOriginatorLocation,
+ nsIURI* aContentLocation,
+ int16_t* aDecision);
+ void ComposeShouldLoad(nsIMsgCompose* aMsgCompose,
+ nsISupports* aRequestingContext,
+ nsIURI* aOriginatorLocation, nsIURI* aContentLocation,
+ int16_t* aDecision);
+ already_AddRefed<nsIMsgCompose> GetMsgComposeForBrowsingContext(
+ mozilla::dom::BrowsingContext* aRequestingContext);
+
+ nsresult GetRootDocShellForContext(nsISupports* aRequestingContext,
+ nsIDocShell** aDocShell);
+ nsresult GetOriginatingURIForContext(nsISupports* aRequestingContext,
+ nsIURI** aURI);
+ nsresult SetDisableItemsOnMailNewsUrlDocshells(nsIURI* aContentLocation,
+ nsILoadInfo* aLoadInfo);
+
+ nsTArray<nsCString> mCustomExposedProtocols;
+};
+
+#endif // _nsMsgContentPolicy_H_
diff --git a/comm/mailnews/base/src/nsMsgCopyService.cpp b/comm/mailnews/base/src/nsMsgCopyService.cpp
new file mode 100644
index 0000000000..22df6bb6e9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCopyService.cpp
@@ -0,0 +1,587 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgCopyService.h"
+#include "nsCOMArray.h"
+#include "nspr.h"
+#include "nsIFile.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Logging.h"
+
+static mozilla::LazyLogModule gCopyServiceLog("MsgCopyService");
+
+// ******************** nsCopySource ******************
+
+nsCopySource::nsCopySource() : m_processed(false) {
+ MOZ_COUNT_CTOR(nsCopySource);
+}
+
+nsCopySource::nsCopySource(nsIMsgFolder* srcFolder) : m_processed(false) {
+ MOZ_COUNT_CTOR(nsCopySource);
+ m_msgFolder = srcFolder;
+}
+
+nsCopySource::~nsCopySource() { MOZ_COUNT_DTOR(nsCopySource); }
+
+void nsCopySource::AddMessage(nsIMsgDBHdr* aMsg) {
+ m_messageArray.AppendElement(aMsg);
+}
+
+// ************ nsCopyRequest *****************
+//
+
+nsCopyRequest::nsCopyRequest()
+ : m_requestType(nsCopyMessagesType),
+ m_isMoveOrDraftOrTemplate(false),
+ m_processed(false),
+ m_newMsgFlags(0) {
+ MOZ_COUNT_CTOR(nsCopyRequest);
+}
+
+nsCopyRequest::~nsCopyRequest() {
+ MOZ_COUNT_DTOR(nsCopyRequest);
+
+ int32_t j = m_copySourceArray.Length();
+ while (j-- > 0) delete m_copySourceArray.ElementAt(j);
+}
+
+nsresult nsCopyRequest::Init(nsCopyRequestType type, nsISupports* aSupport,
+ nsIMsgFolder* dstFolder, bool bVal,
+ uint32_t newMsgFlags,
+ const nsACString& newMsgKeywords,
+ nsIMsgCopyServiceListener* listener,
+ nsIMsgWindow* msgWindow, bool allowUndo) {
+ nsresult rv = NS_OK;
+ m_requestType = type;
+ m_srcSupport = aSupport;
+ m_dstFolder = dstFolder;
+ m_isMoveOrDraftOrTemplate = bVal;
+ m_allowUndo = allowUndo;
+ m_newMsgFlags = newMsgFlags;
+ m_newMsgKeywords = newMsgKeywords;
+
+ if (listener) m_listener = listener;
+ if (msgWindow) {
+ m_msgWindow = msgWindow;
+ if (m_allowUndo) msgWindow->GetTransactionManager(getter_AddRefs(m_txnMgr));
+ }
+ if (type == nsCopyFoldersType) {
+ // To support multiple copy folder operations to the same destination, we
+ // need to save the leaf name of the src file spec so that FindRequest() is
+ // able to find the right request when copy finishes.
+ nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryInterface(aSupport, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString folderName;
+ rv = srcFolder->GetName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_dstFolderName = folderName;
+ }
+
+ return rv;
+}
+
+nsCopySource* nsCopyRequest::AddNewCopySource(nsIMsgFolder* srcFolder) {
+ nsCopySource* newSrc = new nsCopySource(srcFolder);
+ if (newSrc) {
+ m_copySourceArray.AppendElement(newSrc);
+ if (srcFolder == m_dstFolder) newSrc->m_processed = true;
+ }
+ return newSrc;
+}
+
+// ************* nsMsgCopyService ****************
+//
+
+nsMsgCopyService::nsMsgCopyService() {}
+
+nsMsgCopyService::~nsMsgCopyService() {
+ int32_t i = m_copyRequests.Length();
+
+ while (i-- > 0) ClearRequest(m_copyRequests.ElementAt(i), NS_ERROR_FAILURE);
+}
+
+void nsMsgCopyService::LogCopyCompletion(nsISupports* aSrc,
+ nsIMsgFolder* aDest) {
+ nsCString srcFolderUri, destFolderUri;
+ nsCOMPtr<nsIMsgFolder> srcFolder(do_QueryInterface(aSrc));
+ if (srcFolder) srcFolder->GetURI(srcFolderUri);
+ aDest->GetURI(destFolderUri);
+ MOZ_LOG(gCopyServiceLog, mozilla::LogLevel::Info,
+ ("NotifyCompletion - src %s dest %s\n", srcFolderUri.get(),
+ destFolderUri.get()));
+}
+
+void nsMsgCopyService::LogCopyRequest(const char* logMsg,
+ nsCopyRequest* aRequest) {
+ nsCString srcFolderUri, destFolderUri;
+ nsCOMPtr<nsIMsgFolder> srcFolder(do_QueryInterface(aRequest->m_srcSupport));
+ if (srcFolder) srcFolder->GetURI(srcFolderUri);
+ aRequest->m_dstFolder->GetURI(destFolderUri);
+ uint32_t numMsgs = 0;
+ if (aRequest->m_requestType == nsCopyMessagesType &&
+ aRequest->m_copySourceArray.Length() > 0) {
+ numMsgs = aRequest->m_copySourceArray[0]->m_messageArray.Length();
+ }
+ MOZ_LOG(gCopyServiceLog, mozilla::LogLevel::Info,
+ ("request %p %s - src %s dest %s numItems %d type=%d", aRequest,
+ logMsg, srcFolderUri.get(), destFolderUri.get(), numMsgs,
+ aRequest->m_requestType));
+}
+
+nsresult nsMsgCopyService::ClearRequest(nsCopyRequest* aRequest, nsresult rv) {
+ if (aRequest) {
+ if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info))
+ LogCopyRequest(
+ NS_SUCCEEDED(rv) ? "Clearing OK request" : "Clearing failed request",
+ aRequest);
+
+ if (NS_SUCCEEDED(rv) && aRequest->m_requestType == nsCopyFoldersType) {
+ // Send folder copy/move notifications to nsIMsgFolderListeners.
+ // BAD SMELL ALERT: Seems odd that this is the only place the folder
+ // notification is invoked from the copyService.
+ // For message copy/move operations, the folder code handles the
+ // notification (to take one example).
+ // This suggests lack of clarity of responsibility.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ for (nsCopySource* copySource : aRequest->m_copySourceArray) {
+ notifier->NotifyFolderMoveCopyCompleted(
+ aRequest->m_isMoveOrDraftOrTemplate, copySource->m_msgFolder,
+ aRequest->m_dstFolder);
+ }
+ }
+ }
+
+ // undo stuff
+ if (aRequest->m_allowUndo && aRequest->m_copySourceArray.Length() > 1 &&
+ aRequest->m_txnMgr)
+ aRequest->m_txnMgr->EndBatch(false);
+
+ m_copyRequests.RemoveElement(aRequest);
+ if (aRequest->m_listener) aRequest->m_listener->OnStopCopy(rv);
+ delete aRequest;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgCopyService::QueueRequest(nsCopyRequest* aRequest,
+ bool* aCopyImmediately) {
+ NS_ENSURE_ARG_POINTER(aRequest);
+ NS_ENSURE_ARG_POINTER(aCopyImmediately);
+ *aCopyImmediately = true;
+ nsCopyRequest* copyRequest;
+
+ // Check through previous requests to see if the copy can start immediately.
+ uint32_t cnt = m_copyRequests.Length();
+
+ for (uint32_t i = 0; i < cnt; i++) {
+ copyRequest = m_copyRequests.ElementAt(i);
+ if (aRequest->m_requestType == nsCopyFoldersType) {
+ // For copy folder, see if both destination folder (root)
+ // (ie, Local Folder) and folder name (ie, abc) are the same.
+ if (copyRequest->m_dstFolderName == aRequest->m_dstFolderName &&
+ SameCOMIdentity(copyRequest->m_dstFolder, aRequest->m_dstFolder)) {
+ *aCopyImmediately = false;
+ break;
+ }
+ } else if (SameCOMIdentity(copyRequest->m_dstFolder,
+ aRequest->m_dstFolder)) {
+ // If dst are same and we already have a request, we cannot copy
+ // immediately.
+ *aCopyImmediately = false;
+ break;
+ }
+ }
+
+ // Queue it.
+ m_copyRequests.AppendElement(aRequest);
+ return NS_OK;
+}
+
+nsresult nsMsgCopyService::DoCopy(nsCopyRequest* aRequest) {
+ NS_ENSURE_ARG(aRequest);
+ bool copyImmediately;
+ QueueRequest(aRequest, &copyImmediately);
+ if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info))
+ LogCopyRequest(copyImmediately ? "DoCopy" : "QueueRequest", aRequest);
+
+ // if no active request for this dest folder then we can copy immediately
+ if (copyImmediately) return DoNextCopy();
+
+ return NS_OK;
+}
+
+nsresult nsMsgCopyService::DoNextCopy() {
+ nsresult rv = NS_OK;
+ nsCopyRequest* copyRequest = nullptr;
+ nsCopySource* copySource = nullptr;
+ uint32_t i, j, scnt;
+
+ uint32_t cnt = m_copyRequests.Length();
+ if (cnt > 0) {
+ nsCOMArray<nsIMsgFolder> activeTargets;
+
+ // ** jt -- always FIFO
+ for (i = 0; i < cnt; i++) {
+ copyRequest = m_copyRequests.ElementAt(i);
+ copySource = nullptr;
+ scnt = copyRequest->m_copySourceArray.Length();
+ if (!copyRequest->m_processed) {
+ // if the target folder of this request already has an active
+ // copy request, skip this request for now.
+ if (activeTargets.ContainsObject(copyRequest->m_dstFolder)) {
+ copyRequest = nullptr;
+ continue;
+ }
+ if (scnt <= 0) goto found; // must be CopyFileMessage
+ for (j = 0; j < scnt; j++) {
+ copySource = copyRequest->m_copySourceArray.ElementAt(j);
+ if (!copySource->m_processed) goto found;
+ }
+ if (j >= scnt) // all processed set the value
+ copyRequest->m_processed = true;
+ }
+ if (copyRequest->m_processed) {
+ // Keep track of folders actively getting copied to.
+ activeTargets.AppendObject(copyRequest->m_dstFolder);
+ }
+ }
+ found:
+ if (copyRequest && !copyRequest->m_processed) {
+ if (copyRequest->m_listener) copyRequest->m_listener->OnStartCopy();
+ if (copyRequest->m_requestType == nsCopyMessagesType && copySource) {
+ copySource->m_processed = true;
+ rv = copyRequest->m_dstFolder->CopyMessages(
+ copySource->m_msgFolder, copySource->m_messageArray,
+ copyRequest->m_isMoveOrDraftOrTemplate, copyRequest->m_msgWindow,
+ copyRequest->m_listener, false,
+ copyRequest->m_allowUndo); // isFolder operation false
+
+ } else if (copyRequest->m_requestType == nsCopyFoldersType) {
+ NS_ENSURE_STATE(copySource);
+ copySource->m_processed = true;
+
+ nsCOMPtr<nsIMsgFolder> dstFolder = copyRequest->m_dstFolder;
+ nsCOMPtr<nsIMsgFolder> srcFolder = copySource->m_msgFolder;
+
+ // If folder transfer is not within the same server and if a folder
+ // move was requested, set the request move flag false to avoid
+ // removing the list of marked deleted messages in the source folder.
+ bool isMove = copyRequest->m_isMoveOrDraftOrTemplate;
+ if (copyRequest->m_isMoveOrDraftOrTemplate) {
+ bool sameServer;
+ IsOnSameServer(dstFolder, srcFolder, &sameServer);
+ if (!sameServer) copyRequest->m_isMoveOrDraftOrTemplate = false;
+ }
+
+ // NOTE: The folder invokes NotifyCompletion() when the operation is
+ // complete. Some folders (localfolder!) invoke it before CopyFolder()
+ // even returns. This will likely delete the request object, so
+ // you have to assume that copyRequest is invalid when CopyFolder()
+ // returns.
+ rv = dstFolder->CopyFolder(srcFolder, isMove, copyRequest->m_msgWindow,
+ copyRequest->m_listener);
+ // If CopyFolder() fails (e.g. destination folder already exists),
+ // it won't send a completion notification (NotifyCompletion()).
+ // So copyRequest will still exist, and we need to ditch it.
+ if (NS_FAILED(rv)) {
+ ClearRequest(copyRequest, rv);
+ }
+ } else if (copyRequest->m_requestType == nsCopyFileMessageType) {
+ nsCOMPtr<nsIFile> aFile(
+ do_QueryInterface(copyRequest->m_srcSupport, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ // ** in case of saving draft/template; the very first
+ // time we may not have the original message to replace
+ // with; if we do we shall have an instance of copySource
+ nsCOMPtr<nsIMsgDBHdr> aMessage;
+ if (copySource) {
+ aMessage = copySource->m_messageArray[0];
+ copySource->m_processed = true;
+ }
+ copyRequest->m_processed = true;
+ rv = copyRequest->m_dstFolder->CopyFileMessage(
+ aFile, aMessage, copyRequest->m_isMoveOrDraftOrTemplate,
+ copyRequest->m_newMsgFlags, copyRequest->m_newMsgKeywords,
+ copyRequest->m_msgWindow, copyRequest->m_listener);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+/**
+ * Find a request in m_copyRequests which matches the passed in source
+ * and destination folders.
+ *
+ * @param aSupport the iSupports of the source folder.
+ * @param dstFolder the destination folder of the copy request.
+ */
+nsCopyRequest* nsMsgCopyService::FindRequest(nsISupports* aSupport,
+ nsIMsgFolder* dstFolder) {
+ nsCopyRequest* copyRequest = nullptr;
+ uint32_t cnt = m_copyRequests.Length();
+ for (uint32_t i = 0; i < cnt; i++) {
+ copyRequest = m_copyRequests.ElementAt(i);
+ if (SameCOMIdentity(copyRequest->m_srcSupport, aSupport) &&
+ SameCOMIdentity(copyRequest->m_dstFolder.get(), dstFolder))
+ break;
+
+ // When copying folders the notification of the message copy serves as a
+ // proxy for the folder copy. Check for that here.
+ if (copyRequest->m_requestType == nsCopyFoldersType) {
+ // If the src is different then check next request.
+ if (!SameCOMIdentity(copyRequest->m_srcSupport, aSupport)) {
+ copyRequest = nullptr;
+ continue;
+ }
+
+ // See if the parent of the copied folder is the same as the one when the
+ // request was made. Note if the destination folder is already a server
+ // folder then no need to get parent.
+ nsCOMPtr<nsIMsgFolder> parentMsgFolder;
+ nsresult rv = NS_OK;
+ bool isServer = false;
+ dstFolder->GetIsServer(&isServer);
+ if (!isServer) rv = dstFolder->GetParent(getter_AddRefs(parentMsgFolder));
+ if ((NS_FAILED(rv)) || (!parentMsgFolder && !isServer) ||
+ (copyRequest->m_dstFolder.get() != parentMsgFolder)) {
+ copyRequest = nullptr;
+ continue;
+ }
+
+ // Now checks if the folder name is the same.
+ nsString folderName;
+ rv = dstFolder->GetName(folderName);
+ if (NS_FAILED(rv)) {
+ copyRequest = nullptr;
+ continue;
+ }
+
+ if (copyRequest->m_dstFolderName == folderName) break;
+ } else
+ copyRequest = nullptr;
+ }
+
+ return copyRequest;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgCopyService, nsIMsgCopyService)
+
+MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsMsgCopyService::CopyMessages(
+ nsIMsgFolder* srcFolder, /* UI src folder */
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgFolder* dstFolder,
+ bool isMove, nsIMsgCopyServiceListener* listener, nsIMsgWindow* window,
+ bool allowUndo) {
+ NS_ENSURE_ARG_POINTER(srcFolder);
+ NS_ENSURE_ARG_POINTER(dstFolder);
+
+ MOZ_LOG(gCopyServiceLog, mozilla::LogLevel::Debug, ("CopyMessages"));
+
+ if (srcFolder == dstFolder) {
+ NS_ERROR("src and dest folders for msg copy can't be the same");
+ return NS_ERROR_FAILURE;
+ }
+ nsCopyRequest* copyRequest;
+ nsCopySource* copySource = nullptr;
+ nsIMsgDBHdr* msg;
+ nsCOMPtr<nsIMsgFolder> curFolder;
+ nsCOMPtr<nsISupports> aSupport;
+ int cnt;
+ nsresult rv;
+
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // make sure dest folder exists
+ // and has proper flags, before we start copying?
+
+ // bail early if nothing to do
+ if (messages.IsEmpty()) {
+ if (listener) {
+ listener->OnStartCopy();
+ listener->OnStopCopy(NS_OK);
+ }
+ return NS_OK;
+ }
+
+ copyRequest = new nsCopyRequest();
+ if (!copyRequest) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsTArray<RefPtr<nsIMsgDBHdr>> unprocessed = messages.Clone();
+ aSupport = srcFolder;
+
+ rv = copyRequest->Init(nsCopyMessagesType, aSupport, dstFolder, isMove,
+ 0 /* new msg flags, not used */, EmptyCString(),
+ listener, window, allowUndo);
+ if (NS_FAILED(rv)) goto done;
+
+ if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info))
+ LogCopyRequest("CopyMessages request", copyRequest);
+
+ // Build up multiple nsCopySource objects. Each holds a single source folder
+ // and all the messages in the folder that are to be copied.
+ cnt = unprocessed.Length();
+ while (cnt-- > 0) {
+ msg = unprocessed[cnt];
+ rv = msg->GetFolder(getter_AddRefs(curFolder));
+
+ if (NS_FAILED(rv)) goto done;
+ if (!copySource) {
+ // Begin a folder grouping.
+ copySource = copyRequest->AddNewCopySource(curFolder);
+ if (!copySource) {
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ goto done;
+ }
+ }
+
+ // Stash message if in the current folder grouping.
+ if (curFolder == copySource->m_msgFolder) {
+ copySource->AddMessage(msg);
+ unprocessed.RemoveElementAt((size_t)cnt);
+ }
+
+ if (cnt == 0) {
+ // Finished a folder. Start a new pass to handle any remaining messages
+ // in other folders.
+ cnt = unprocessed.Length();
+ if (cnt > 0) {
+ // Force to create a new one and continue grouping the messages.
+ copySource = nullptr;
+ }
+ }
+ }
+
+ // undo stuff
+ if (NS_SUCCEEDED(rv) && copyRequest->m_allowUndo &&
+ copyRequest->m_copySourceArray.Length() > 1 && copyRequest->m_txnMgr) {
+ nsCOMPtr<nsITransactionManager> txnMgr = copyRequest->m_txnMgr;
+ txnMgr->BeginBatch(nullptr);
+ }
+
+done:
+
+ if (NS_FAILED(rv))
+ delete copyRequest;
+ else
+ rv = DoCopy(copyRequest);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgCopyService::CopyFolder(nsIMsgFolder* srcFolder, nsIMsgFolder* dstFolder,
+ bool isMove, nsIMsgCopyServiceListener* listener,
+ nsIMsgWindow* window) {
+ NS_ENSURE_ARG_POINTER(srcFolder);
+ NS_ENSURE_ARG_POINTER(dstFolder);
+ nsCopyRequest* copyRequest;
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> curFolder;
+
+ copyRequest = new nsCopyRequest();
+ rv = copyRequest->Init(nsCopyFoldersType, srcFolder, dstFolder, isMove,
+ 0 /* new msg flags, not used */, EmptyCString(),
+ listener, window, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ copyRequest->AddNewCopySource(srcFolder);
+ return DoCopy(copyRequest);
+}
+
+NS_IMETHODIMP
+nsMsgCopyService::CopyFileMessage(nsIFile* file, nsIMsgFolder* dstFolder,
+ nsIMsgDBHdr* msgToReplace, bool isDraft,
+ uint32_t aMsgFlags,
+ const nsACString& aNewMsgKeywords,
+ nsIMsgCopyServiceListener* listener,
+ nsIMsgWindow* window) {
+ nsresult rv = NS_ERROR_NULL_POINTER;
+ nsCopyRequest* copyRequest;
+ nsCopySource* copySource = nullptr;
+
+ NS_ENSURE_ARG_POINTER(file);
+ NS_ENSURE_ARG_POINTER(dstFolder);
+
+ copyRequest = new nsCopyRequest();
+ if (!copyRequest) return rv;
+
+ rv = copyRequest->Init(nsCopyFileMessageType, file, dstFolder, isDraft,
+ aMsgFlags, aNewMsgKeywords, listener, window, false);
+ if (NS_FAILED(rv)) goto done;
+
+ if (msgToReplace) {
+ // The actual source of the message is a file not a folder, but
+ // we still need an nsCopySource to reference the old message header
+ // which will be used to recover message metadata.
+ copySource = copyRequest->AddNewCopySource(nullptr);
+ if (!copySource) {
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ goto done;
+ }
+ copySource->AddMessage(msgToReplace);
+ }
+
+done:
+ if (NS_FAILED(rv)) {
+ delete copyRequest;
+ } else {
+ rv = DoCopy(copyRequest);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgCopyService::NotifyCompletion(nsISupports* aSupport,
+ nsIMsgFolder* dstFolder, nsresult result) {
+ if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info))
+ LogCopyCompletion(aSupport, dstFolder);
+ nsCopyRequest* copyRequest = nullptr;
+ uint32_t numOrigRequests = m_copyRequests.Length();
+ do {
+ // loop for copy requests, because if we do a cross server folder copy,
+ // we'll have a copy request for the folder copy, which will in turn
+ // generate a copy request for the messages in the folder, which
+ // will have the same src support.
+ copyRequest = FindRequest(aSupport, dstFolder);
+
+ if (copyRequest) {
+ // ClearRequest can cause a new request to get added to m_copyRequests
+ // with matching source and dest folders if the copy listener starts
+ // a new copy. We want to ignore any such request here, because it wasn't
+ // the one that was completed. So we keep track of how many original
+ // requests there were.
+ if (m_copyRequests.IndexOf(copyRequest) >= numOrigRequests) break;
+ // check if this copy request is done by making sure all the
+ // sources have been processed.
+ int32_t sourceIndex, sourceCount;
+ sourceCount = copyRequest->m_copySourceArray.Length();
+ for (sourceIndex = 0; sourceIndex < sourceCount;) {
+ if (!(copyRequest->m_copySourceArray.ElementAt(sourceIndex))
+ ->m_processed)
+ break;
+ sourceIndex++;
+ }
+ // if all sources processed, mark the request as processed
+ if (sourceIndex >= sourceCount) copyRequest->m_processed = true;
+ // if this request is done, or failed, clear it.
+ if (copyRequest->m_processed || NS_FAILED(result)) {
+ ClearRequest(copyRequest, result);
+ numOrigRequests--;
+ } else
+ break;
+ } else
+ break;
+ } while (copyRequest);
+
+ return DoNextCopy();
+}
diff --git a/comm/mailnews/base/src/nsMsgCopyService.h b/comm/mailnews/base/src/nsMsgCopyService.h
new file mode 100644
index 0000000000..ab9be438d0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgCopyService.h
@@ -0,0 +1,91 @@
+/* -*- 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/. */
+
+#ifndef nsMsgCopyService_h__
+#define nsMsgCopyService_h__
+
+#include "nscore.h"
+#include "nsIMsgCopyService.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgWindow.h"
+#include "nsITransactionManager.h"
+#include "nsTArray.h"
+
+typedef enum _nsCopyRequestType {
+ nsCopyMessagesType = 0x0,
+ nsCopyFileMessageType = 0x1,
+ nsCopyFoldersType = 0x2
+} nsCopyRequestType;
+
+class nsCopyRequest;
+
+// nsCopySource represents either:
+// 1. a bundle of messages to be copied, all from the same folder.
+// or
+// 2. a folder to be copied (and no messages).
+class nsCopySource {
+ public:
+ nsCopySource();
+ explicit nsCopySource(nsIMsgFolder* srcFolder);
+ ~nsCopySource();
+ void AddMessage(nsIMsgDBHdr* aMsg);
+
+ nsCOMPtr<nsIMsgFolder> m_msgFolder;
+ nsTArray<RefPtr<nsIMsgDBHdr>> m_messageArray;
+ bool m_processed;
+};
+
+class nsCopyRequest {
+ public:
+ nsCopyRequest();
+ ~nsCopyRequest();
+
+ nsresult Init(nsCopyRequestType type, nsISupports* aSupport,
+ nsIMsgFolder* dstFolder, bool bVal, uint32_t newMsgFlags,
+ const nsACString& newMsgKeywords,
+ nsIMsgCopyServiceListener* listener, nsIMsgWindow* msgWindow,
+ bool allowUndo);
+ nsCopySource* AddNewCopySource(nsIMsgFolder* srcFolder);
+
+ nsCOMPtr<nsISupports> m_srcSupport; // ui source folder or file spec
+ nsCOMPtr<nsIMsgFolder> m_dstFolder;
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ nsCOMPtr<nsIMsgCopyServiceListener> m_listener;
+ nsCOMPtr<nsITransactionManager> m_txnMgr;
+ nsCopyRequestType m_requestType;
+ bool m_isMoveOrDraftOrTemplate;
+ bool m_allowUndo;
+ bool m_processed;
+ uint32_t m_newMsgFlags;
+ nsCString m_newMsgKeywords;
+ nsString m_dstFolderName; // used for copy folder.
+ nsTArray<nsCopySource*> m_copySourceArray; // array of nsCopySource
+};
+
+class nsMsgCopyService : public nsIMsgCopyService {
+ public:
+ nsMsgCopyService();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_DECL_NSIMSGCOPYSERVICE
+
+ private:
+ virtual ~nsMsgCopyService();
+
+ nsresult ClearRequest(nsCopyRequest* aRequest, nsresult rv);
+ nsresult DoCopy(nsCopyRequest* aRequest);
+ nsresult DoNextCopy();
+ nsCopyRequest* FindRequest(nsISupports* aSupport, nsIMsgFolder* dstFolder);
+ nsresult QueueRequest(nsCopyRequest* aRequest, bool* aCopyImmediately);
+ void LogCopyCompletion(nsISupports* aSrc, nsIMsgFolder* aDest);
+ void LogCopyRequest(const char* logMsg, nsCopyRequest* aRequest);
+
+ nsTArray<nsCopyRequest*> m_copyRequests;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgDBFolder.cpp b/comm/mailnews/base/src/nsMsgDBFolder.cpp
new file mode 100644
index 0000000000..8e82ce1a5c
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgDBFolder.cpp
@@ -0,0 +1,5573 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsUnicharUtils.h"
+#include "nsMsgDBFolder.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsNetUtil.h"
+#include "nsIMsgFolderCache.h"
+#include "nsIMsgFolderCacheElement.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsMsgDatabase.h"
+#include "nsIMsgAccountManager.h"
+#include "nsISeekableStream.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsIChannel.h"
+#include "nsITransport.h"
+#include "nsIWindowWatcher.h"
+#include "nsIMsgFolderCompactor.h"
+#include "nsIDocShell.h"
+#include "nsIMsgWindow.h"
+#include "nsIPrompt.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIAbCard.h"
+#include "nsIAbDirectory.h"
+#include "nsISpamSettings.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsIMsgMailSession.h"
+#include "nsTextFormatter.h"
+#include "nsReadLine.h"
+#include "nsLayoutCID.h"
+#include "nsIParserUtils.h"
+#include "nsIDocumentEncoder.h"
+#include "nsMsgI18N.h"
+#include "nsIMIMEHeaderParam.h"
+#include "plbase64.h"
+#include <time.h>
+#include "nsIMsgFolderNotificationService.h"
+#include "nsIMimeHeaders.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIMsgTraitService.h"
+#include "nsIMessenger.h"
+#include "nsThreadUtils.h"
+#include "nsITransactionManager.h"
+#include "nsMsgReadStateTxn.h"
+#include "prmem.h"
+#include "nsIPK11TokenDB.h"
+#include "nsIPK11Token.h"
+#include "nsMsgLocalFolderHdrs.h"
+#define oneHour 3600000000U
+#include "nsMsgUtils.h"
+#include "nsIMsgFilterService.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsMimeTypes.h"
+#include "nsIMsgFilter.h"
+#include "nsIScriptError.h"
+#include "nsIURIMutator.h"
+#include "nsIXULAppInfo.h"
+#include "nsPrintfCString.h"
+#include "mozilla/Components.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/Logging.h"
+#include "mozilla/SlicedInputStream.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Utf8.h"
+#include "nsIPromptService.h"
+#include "nsEmbedCID.h"
+
+using namespace mozilla;
+
+extern LazyLogModule FILTERLOGMODULE;
+extern LazyLogModule DBLog;
+
+static PRTime gtimeOfLastPurgeCheck; // variable to know when to check for
+ // purge threshold
+
+#define PREF_MAIL_PROMPT_PURGE_THRESHOLD "mail.prompt_purge_threshhold"
+#define PREF_MAIL_PURGE_THRESHOLD "mail.purge_threshhold"
+#define PREF_MAIL_PURGE_THRESHOLD_MB "mail.purge_threshhold_mb"
+#define PREF_MAIL_PURGE_MIGRATED "mail.purge_threshold_migrated"
+#define PREF_MAIL_PURGE_ASK "mail.purge.ask"
+#define PREF_MAIL_WARN_FILTER_CHANGED "mail.warn_filter_changed"
+
+const char* kUseServerRetentionProp = "useServerRetention";
+
+/**
+ * mozilla::intl APIs require sizeable buffers. This class abstracts over
+ * the nsTArray.
+ */
+class nsTArrayU8Buffer {
+ public:
+ using CharType = uint8_t;
+
+ // Do not allow copy or move. Move could be added in the future if needed.
+ nsTArrayU8Buffer(const nsTArrayU8Buffer&) = delete;
+ nsTArrayU8Buffer& operator=(const nsTArrayU8Buffer&) = delete;
+
+ explicit nsTArrayU8Buffer(nsTArray<CharType>& aArray) : mArray(aArray) {}
+
+ /**
+ * Ensures the buffer has enough space to accommodate |size| elements.
+ */
+ [[nodiscard]] bool reserve(size_t size) {
+ mArray.SetCapacity(size);
+ // nsTArray::SetCapacity returns void, return true to keep the API the same
+ // as the other Buffer implementations.
+ return true;
+ }
+
+ /**
+ * Returns the raw data inside the buffer.
+ */
+ CharType* data() { return mArray.Elements(); }
+
+ /**
+ * Returns the count of elements written into the buffer.
+ */
+ size_t length() const { return mArray.Length(); }
+
+ /**
+ * Returns the buffer's overall capacity.
+ */
+ size_t capacity() const { return mArray.Capacity(); }
+
+ /**
+ * Resizes the buffer to the given amount of written elements.
+ */
+ void written(size_t amount) {
+ MOZ_ASSERT(amount <= mArray.Capacity());
+ // This sets |mArray|'s internal size so that it matches how much was
+ // written. This is necessary because the write happens across FFI
+ // boundaries.
+ mArray.SetLengthAndRetainStorage(amount);
+ }
+
+ private:
+ nsTArray<CharType>& mArray;
+};
+
+NS_IMPL_ISUPPORTS(nsMsgFolderService, nsIMsgFolderService)
+
+// This method serves the only purpose to re-initialize the
+// folder name strings when UI initialization is done.
+// XXX TODO: This can be removed when the localization system gets
+// initialized in M-C code before, for example, the permission manager
+// triggers folder creation during imap: URI creation.
+// In fact, the entire class together with nsMsgDBFolder::FolderNamesReady()
+// can be removed.
+NS_IMETHODIMP nsMsgFolderService::InitializeFolderStrings() {
+ nsMsgDBFolder::initializeStrings();
+ nsMsgDBFolder::gInitializeStringsDone = true;
+ nsMsgDBFolder::gIsEnglishApp = -1;
+ return NS_OK;
+}
+
+mozilla::UniquePtr<mozilla::intl::Collator>
+ nsMsgDBFolder::gCollationKeyGenerator = nullptr;
+
+nsString nsMsgDBFolder::kLocalizedInboxName;
+nsString nsMsgDBFolder::kLocalizedTrashName;
+nsString nsMsgDBFolder::kLocalizedSentName;
+nsString nsMsgDBFolder::kLocalizedDraftsName;
+nsString nsMsgDBFolder::kLocalizedTemplatesName;
+nsString nsMsgDBFolder::kLocalizedUnsentName;
+nsString nsMsgDBFolder::kLocalizedJunkName;
+nsString nsMsgDBFolder::kLocalizedArchivesName;
+
+nsString nsMsgDBFolder::kLocalizedBrandShortName;
+
+nsrefcnt nsMsgDBFolder::mInstanceCount = 0;
+bool nsMsgDBFolder::gInitializeStringsDone = false;
+// This is used in `nonEnglishApp()` to determine localised
+// folders strings.
+// -1: not retrieved yet, 1: English, 0: non-English.
+int nsMsgDBFolder::gIsEnglishApp;
+
+// We define strings for folder properties and events.
+// Properties:
+constexpr nsLiteralCString kBiffState = "BiffState"_ns;
+constexpr nsLiteralCString kCanFileMessages = "CanFileMessages"_ns;
+constexpr nsLiteralCString kDefaultServer = "DefaultServer"_ns;
+constexpr nsLiteralCString kFlagged = "Flagged"_ns;
+constexpr nsLiteralCString kFolderFlag = "FolderFlag"_ns;
+constexpr nsLiteralCString kFolderSize = "FolderSize"_ns;
+constexpr nsLiteralCString kIsDeferred = "isDeferred"_ns;
+constexpr nsLiteralCString kIsSecure = "isSecure"_ns;
+constexpr nsLiteralCString kJunkStatusChanged = "JunkStatusChanged"_ns;
+constexpr nsLiteralCString kKeywords = "Keywords"_ns;
+constexpr nsLiteralCString kMRMTimeChanged = "MRMTimeChanged"_ns;
+constexpr nsLiteralCString kMsgLoaded = "msgLoaded"_ns;
+constexpr nsLiteralCString kName = "Name"_ns;
+constexpr nsLiteralCString kNewMailReceived = "NewMailReceived"_ns;
+constexpr nsLiteralCString kNewMessages = "NewMessages"_ns;
+constexpr nsLiteralCString kOpen = "open"_ns;
+constexpr nsLiteralCString kSortOrder = "SortOrder"_ns;
+constexpr nsLiteralCString kStatus = "Status"_ns;
+constexpr nsLiteralCString kSynchronize = "Synchronize"_ns;
+constexpr nsLiteralCString kTotalMessages = "TotalMessages"_ns;
+constexpr nsLiteralCString kTotalUnreadMessages = "TotalUnreadMessages"_ns;
+
+// Events:
+constexpr nsLiteralCString kAboutToCompact = "AboutToCompact"_ns;
+constexpr nsLiteralCString kCompactCompleted = "CompactCompleted"_ns;
+constexpr nsLiteralCString kDeleteOrMoveMsgCompleted =
+ "DeleteOrMoveMsgCompleted"_ns;
+constexpr nsLiteralCString kDeleteOrMoveMsgFailed = "DeleteOrMoveMsgFailed"_ns;
+constexpr nsLiteralCString kFiltersApplied = "FiltersApplied"_ns;
+constexpr nsLiteralCString kFolderCreateCompleted = "FolderCreateCompleted"_ns;
+constexpr nsLiteralCString kFolderCreateFailed = "FolderCreateFailed"_ns;
+constexpr nsLiteralCString kFolderLoaded = "FolderLoaded"_ns;
+constexpr nsLiteralCString kNumNewBiffMessages = "NumNewBiffMessages"_ns;
+constexpr nsLiteralCString kRenameCompleted = "RenameCompleted"_ns;
+
+NS_IMPL_ISUPPORTS(nsMsgDBFolder, nsISupportsWeakReference, nsIMsgFolder,
+ nsIDBChangeListener, nsIUrlListener,
+ nsIJunkMailClassificationListener,
+ nsIMsgTraitClassificationListener)
+
+nsMsgDBFolder::nsMsgDBFolder(void)
+ : mAddListener(true),
+ mNewMessages(false),
+ mGettingNewMessages(false),
+ mLastMessageLoaded(nsMsgKey_None),
+ m_numOfflineMsgLines(0),
+ m_bytesAddedToLocalMsg(0),
+ m_tempMessageStreamBytesWritten(0),
+ mFlags(0),
+ mNumUnreadMessages(-1),
+ mNumTotalMessages(-1),
+ mNotifyCountChanges(true),
+ mExpungedBytes(0),
+ mInitializedFromCache(false),
+ mSemaphoreHolder(nullptr),
+ mNumPendingUnreadMessages(0),
+ mNumPendingTotalMessages(0),
+ mFolderSize(kSizeUnknown),
+ mNumNewBiffMessages(0),
+ mHaveParsedURI(false),
+ mIsServerIsValid(false),
+ mIsServer(false),
+ mBayesJunkClassifying(false),
+ mBayesTraitClassifying(false) {
+ if (mInstanceCount++ <= 0) {
+ initializeStrings();
+
+ do {
+ nsresult rv;
+ // We need to check whether we're running under xpcshell,
+ // in that case, we always assume that the strings are good.
+ // XXX TODO: This hack can be removed when the localization system gets
+ // initialized in M-C code before, for example, the permission manager
+ // triggers folder creation during imap: URI creation.
+ nsCOMPtr<nsIXULAppInfo> appinfo =
+ do_GetService("@mozilla.org/xre/app-info;1", &rv);
+ if (NS_FAILED(rv)) break;
+ nsAutoCString appName;
+ rv = appinfo->GetName(appName);
+ if (NS_FAILED(rv)) break;
+ if (appName.Equals("xpcshell")) gInitializeStringsDone = true;
+ } while (false);
+
+ createCollationKeyGenerator();
+ gtimeOfLastPurgeCheck = 0;
+ }
+
+ mProcessingFlag[0].bit = nsMsgProcessingFlags::ClassifyJunk;
+ mProcessingFlag[1].bit = nsMsgProcessingFlags::ClassifyTraits;
+ mProcessingFlag[2].bit = nsMsgProcessingFlags::TraitsDone;
+ mProcessingFlag[3].bit = nsMsgProcessingFlags::FiltersDone;
+ mProcessingFlag[4].bit = nsMsgProcessingFlags::FilterToMove;
+ mProcessingFlag[5].bit = nsMsgProcessingFlags::NotReportedClassified;
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ mProcessingFlag[i].keys = nsMsgKeySetU::Create();
+}
+
+nsMsgDBFolder::~nsMsgDBFolder(void) {
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ delete mProcessingFlag[i].keys;
+
+ if (--mInstanceCount == 0) {
+ nsMsgDBFolder::gCollationKeyGenerator = nullptr;
+ }
+ // shutdown but don't shutdown children.
+ Shutdown(false);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::FolderNamesReady(bool* aReady) {
+ *aReady = gInitializeStringsDone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::Shutdown(bool shutdownChildren) {
+ if (mDatabase) {
+ mDatabase->RemoveListener(this);
+ mDatabase->ForceClosed();
+ mDatabase = nullptr;
+ if (mBackupDatabase) {
+ mBackupDatabase->ForceClosed();
+ mBackupDatabase = nullptr;
+ }
+ }
+
+ if (shutdownChildren) {
+ int32_t count = mSubFolders.Count();
+
+ for (int32_t i = 0; i < count; i++) mSubFolders[i]->Shutdown(true);
+
+ // Reset incoming server pointer and pathname.
+ mServer = nullptr;
+ mPath = nullptr;
+ mHaveParsedURI = false;
+ mName.Truncate();
+ mSubFolders.Clear();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ForceDBClosed() {
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) mSubFolders[i]->ForceDBClosed();
+
+ if (mDatabase) {
+ mDatabase->ForceClosed();
+ mDatabase = nullptr;
+ } else {
+ nsCOMPtr<nsIMsgDBService> mailDBFactory(
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1"));
+ if (mailDBFactory) mailDBFactory->ForceFolderDBClosed(this);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CloseAndBackupFolderDB(const nsACString& newName) {
+ ForceDBClosed();
+
+ // We only support backup for mail at the moment
+ if (!(mFlags & nsMsgFolderFlags::Mail)) return NS_OK;
+
+ nsCOMPtr<nsIFile> folderPath;
+ nsresult rv = GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> dbFile;
+ rv = GetSummaryFileLocation(folderPath, getter_AddRefs(dbFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDir;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDBFile;
+ rv = GetBackupSummaryFile(getter_AddRefs(backupDBFile), newName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mBackupDatabase) {
+ mBackupDatabase->ForceClosed();
+ mBackupDatabase = nullptr;
+ }
+
+ backupDBFile->Remove(false);
+ bool backupExists;
+ backupDBFile->Exists(&backupExists);
+ NS_ASSERTION(!backupExists, "Couldn't delete database backup");
+ if (backupExists) return NS_ERROR_FAILURE;
+
+ if (!newName.IsEmpty()) {
+ nsAutoCString backupName;
+ rv = backupDBFile->GetNativeLeafName(backupName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return dbFile->CopyToNative(backupDir, backupName);
+ } else
+ return dbFile->CopyToNative(backupDir, EmptyCString());
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OpenBackupMsgDatabase() {
+ if (mBackupDatabase) return NS_OK;
+ nsCOMPtr<nsIFile> folderPath;
+ nsresult rv = GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString folderName;
+ rv = folderPath->GetLeafName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDir;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We use a dummy message folder file so we can use
+ // GetSummaryFileLocation to get the db file name
+ nsCOMPtr<nsIFile> backupDBDummyFolder;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDBDummyFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = backupDBDummyFolder->Append(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgDBService->OpenMailDBFromFile(backupDBDummyFolder, this, false, true,
+ getter_AddRefs(mBackupDatabase));
+ // we add a listener so that we can close the db during OnAnnouncerGoingAway.
+ // There should not be any other calls to the listener with the backup
+ // database
+ if (NS_SUCCEEDED(rv) && mBackupDatabase) mBackupDatabase->AddListener(this);
+
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE)
+ // this is normal in reparsing
+ rv = NS_OK;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RemoveBackupMsgDatabase() {
+ nsCOMPtr<nsIFile> folderPath;
+ nsresult rv = GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString folderName;
+ rv = folderPath->GetLeafName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDir;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We use a dummy message folder file so we can use
+ // GetSummaryFileLocation to get the db file name
+ nsCOMPtr<nsIFile> backupDBDummyFolder;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDBDummyFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = backupDBDummyFolder->Append(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDBFile;
+ rv =
+ GetSummaryFileLocation(backupDBDummyFolder, getter_AddRefs(backupDBFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mBackupDatabase) {
+ mBackupDatabase->ForceClosed();
+ mBackupDatabase = nullptr;
+ }
+
+ return backupDBFile->Remove(false);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::StartFolderLoading(void) {
+ if (mDatabase) mDatabase->RemoveListener(this);
+ mAddListener = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::EndFolderLoading(void) {
+ if (mDatabase) mDatabase->AddListener(this);
+ mAddListener = true;
+ UpdateSummaryTotals(true);
+
+ // GGGG check for new mail here and call SetNewMessages...?? -- ONE OF
+ // THE 2 PLACES
+ if (mDatabase) m_newMsgs.Clear();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetExpungedBytes(int64_t* count) {
+ NS_ENSURE_ARG_POINTER(count);
+
+ if (mDatabase) {
+ nsresult rv;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = mDatabase->GetDBFolderInfo(getter_AddRefs(folderInfo));
+ if (NS_FAILED(rv)) return rv;
+ rv = folderInfo->GetExpungedBytes(count);
+ if (NS_SUCCEEDED(rv)) mExpungedBytes = *count; // sync up with the database
+ return rv;
+ } else {
+ ReadDBFolderInfo(false);
+ *count = mExpungedBytes;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetHasNewMessages(bool* hasNewMessages) {
+ NS_ENSURE_ARG_POINTER(hasNewMessages);
+ *hasNewMessages = mNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetHasNewMessages(bool curNewMessages) {
+ if (curNewMessages != mNewMessages) {
+ // Only change mru time if we're going from doesn't have new to has new.
+ // technically, we should probably update mru time for every new message
+ // but we would pay a performance penalty for that. If the user
+ // opens the folder, the mrutime will get updated anyway.
+ if (curNewMessages) SetMRUTime();
+ bool oldNewMessages = mNewMessages;
+ mNewMessages = curNewMessages;
+ NotifyBoolPropertyChanged(kNewMessages, oldNewMessages, curNewMessages);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetHasFolderOrSubfolderNewMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ bool hasNewMessages = mNewMessages;
+
+ if (!hasNewMessages) {
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ bool hasNew = false;
+ mSubFolders[i]->GetHasFolderOrSubfolderNewMessages(&hasNew);
+ if (hasNew) {
+ hasNewMessages = true;
+ break;
+ }
+ }
+ }
+
+ *aResult = hasNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetGettingNewMessages(bool* gettingNewMessages) {
+ NS_ENSURE_ARG_POINTER(gettingNewMessages);
+ *gettingNewMessages = mGettingNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetGettingNewMessages(bool gettingNewMessages) {
+ mGettingNewMessages = gettingNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFirstNewMessage(nsIMsgDBHdr** firstNewMessage) {
+ // If there's not a db then there can't be new messages. Return failure since
+ // you should use HasNewMessages first.
+ if (!mDatabase) return NS_ERROR_FAILURE;
+
+ nsresult rv;
+ nsMsgKey key;
+ rv = mDatabase->GetFirstNew(&key);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(hdr));
+ if (NS_FAILED(rv)) return rv;
+
+ return mDatabase->GetMsgHdrForKey(key, firstNewMessage);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ClearNewMessages() {
+ nsresult rv = NS_OK;
+ bool dbWasCached = mDatabase != nullptr;
+ if (!dbWasCached) GetDatabase();
+
+ if (mDatabase) {
+ mDatabase->GetNewList(m_saveNewMsgs);
+ mDatabase->ClearNewList(true);
+ }
+ if (!dbWasCached) SetMsgDatabase(nullptr);
+
+ m_newMsgs.Clear();
+ mNumNewBiffMessages = 0;
+ return rv;
+}
+
+void nsMsgDBFolder::UpdateNewMessages() {
+ if (!(mFlags & nsMsgFolderFlags::Virtual)) {
+ bool hasNewMessages = false;
+ for (uint32_t keyIndex = 0; keyIndex < m_newMsgs.Length(); keyIndex++) {
+ bool containsKey = false;
+ mDatabase->ContainsKey(m_newMsgs[keyIndex], &containsKey);
+ if (!containsKey) continue;
+ bool isRead = false;
+ nsresult rv2 = mDatabase->IsRead(m_newMsgs[keyIndex], &isRead);
+ if (NS_SUCCEEDED(rv2) && !isRead) {
+ hasNewMessages = true;
+ mDatabase->AddToNewList(m_newMsgs[keyIndex]);
+ }
+ }
+ SetHasNewMessages(hasNewMessages);
+ }
+}
+
+// helper function that gets the cache element that corresponds to the passed in
+// file spec. This could be static, or could live in another class - it's not
+// specific to the current nsMsgDBFolder. If it lived at a higher level, we
+// could cache the account manager and folder cache.
+nsresult nsMsgDBFolder::GetFolderCacheElemFromFile(
+ nsIFile* file, nsIMsgFolderCacheElement** cacheElement) {
+ nsresult result;
+ NS_ENSURE_ARG_POINTER(file);
+ NS_ENSURE_ARG_POINTER(cacheElement);
+ nsCOMPtr<nsIMsgFolderCache> folderCache;
+#ifdef DEBUG_bienvenu1
+ bool exists;
+ NS_ASSERTION(NS_SUCCEEDED(fileSpec->Exists(&exists)) && exists,
+ "whoops, file doesn't exist, mac will break");
+#endif
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &result);
+ if (NS_SUCCEEDED(result)) {
+ result = accountMgr->GetFolderCache(getter_AddRefs(folderCache));
+ if (NS_SUCCEEDED(result) && folderCache) {
+ nsCString persistentPath;
+ result = file->GetPersistentDescriptor(persistentPath);
+ NS_ENSURE_SUCCESS(result, result);
+ result =
+ folderCache->GetCacheElement(persistentPath, false, cacheElement);
+ }
+ }
+ return result;
+}
+
+nsresult nsMsgDBFolder::ReadDBFolderInfo(bool force) {
+ // Since it turns out to be pretty expensive to open and close
+ // the DBs all the time, if we have to open it once, get everything
+ // we might need while we're here
+ nsresult result = NS_OK;
+
+ // If we reload the cache we might get stale info, so don't do it.
+ if (!mInitializedFromCache) {
+ // Path is used as a key into the foldercache.
+ nsCOMPtr<nsIFile> dbPath;
+ result = GetFolderCacheKey(getter_AddRefs(dbPath));
+ if (dbPath) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ result = GetFolderCacheElemFromFile(dbPath, getter_AddRefs(cacheElement));
+ if (NS_SUCCEEDED(result) && cacheElement) {
+ if (NS_SUCCEEDED(ReadFromFolderCacheElem(cacheElement))) {
+ mInitializedFromCache = true;
+ }
+ }
+ }
+ }
+
+ if (force || !mInitializedFromCache) {
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ bool weOpenedDB = !mDatabase;
+ result =
+ GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(result)) {
+ if (folderInfo) {
+ if (!mInitializedFromCache) {
+ folderInfo->GetFlags((int32_t*)&mFlags);
+#ifdef DEBUG_bienvenu1
+ nsString name;
+ GetName(name);
+ NS_ASSERTION(Compare(name, kLocalizedTrashName) ||
+ (mFlags & nsMsgFolderFlags::Trash),
+ "lost trash flag");
+#endif
+ mInitializedFromCache = true;
+ }
+
+ folderInfo->GetNumMessages(&mNumTotalMessages);
+ folderInfo->GetNumUnreadMessages(&mNumUnreadMessages);
+ folderInfo->GetExpungedBytes(&mExpungedBytes);
+
+ nsCString utf8Name;
+ folderInfo->GetFolderName(utf8Name);
+ if (!utf8Name.IsEmpty()) CopyUTF8toUTF16(utf8Name, mName);
+
+ // These should be put in IMAP folder only.
+ // folderInfo->GetImapTotalPendingMessages(&mNumPendingTotalMessages);
+ // folderInfo->GetImapUnreadPendingMessages(&mNumPendingUnreadMessages);
+
+ if (db) {
+ bool hasnew;
+ nsresult rv;
+ rv = db->HasNew(&hasnew);
+ if (NS_FAILED(rv)) return rv;
+ }
+ if (weOpenedDB) CloseDBIfFolderNotOpen(false);
+ }
+ } else {
+ // we tried to open DB but failed - don't keep trying.
+ // If a DB is created, we will call this method with force == TRUE,
+ // and read from the db that way.
+ mInitializedFromCache = true;
+ }
+ }
+ return result;
+}
+
+nsresult nsMsgDBFolder::SendFlagNotifications(nsIMsgDBHdr* item,
+ uint32_t oldFlags,
+ uint32_t newFlags) {
+ nsresult rv = NS_OK;
+ uint32_t changedFlags = oldFlags ^ newFlags;
+ if ((changedFlags & nsMsgMessageFlags::Read) &&
+ (changedFlags & nsMsgMessageFlags::New)) {
+ //..so..if the msg is read in the folder and the folder has new msgs clear
+ // the account level and status bar biffs.
+ rv = NotifyPropertyFlagChanged(item, kStatus, oldFlags, newFlags);
+ rv = SetBiffState(nsMsgBiffState_NoMail);
+ } else if (changedFlags &
+ (nsMsgMessageFlags::Read | nsMsgMessageFlags::Replied |
+ nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::IMAPDeleted |
+ nsMsgMessageFlags::New | nsMsgMessageFlags::Offline))
+ rv = NotifyPropertyFlagChanged(item, kStatus, oldFlags, newFlags);
+ else if ((changedFlags & nsMsgMessageFlags::Marked))
+ rv = NotifyPropertyFlagChanged(item, kFlagged, oldFlags, newFlags);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgWindow*) {
+ NS_ASSERTION(false, "imap and news need to override this");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) {
+ NS_ASSERTION(false, "imap and news need to override this");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetMsgStore(nsIMsgPluggableStore** aStore) {
+ NS_ENSURE_ARG_POINTER(aStore);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+ return server->GetMsgStore(aStore);
+}
+
+nsresult nsMsgDBFolder::GetOfflineFileStream(nsMsgKey msgKey, uint64_t* offset,
+ uint32_t* size,
+ nsIInputStream** aFileStream) {
+ NS_ENSURE_ARG(aFileStream);
+
+ *offset = 0;
+ *size = 0;
+
+ // Initialise to nullptr since this is checked by some callers for the success
+ // of the function.
+ *aFileStream = nullptr;
+
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ hdr->GetOfflineMessageSize(size);
+ NS_ENSURE_TRUE(*size != 0, NS_ERROR_UNEXPECTED);
+ rv = GetMsgInputStream(hdr, aFileStream);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(nsPrintfCString(
+ "(debug) nsMsgDBFolder::GetOfflineFileStream: "
+ "GetMsgInputStream(hdr, aFileStream); rv=0x%" PRIx32 "\n",
+ static_cast<uint32_t>(rv))
+ .get());
+ // Return early: If we could not find an offline stream, clear the offline
+ // flag which will fall back to reading the message from the server.
+ if (mDatabase) mDatabase->MarkOffline(msgKey, false, nullptr);
+
+ return rv;
+ }
+
+ // Check if the database has the correct offset into the offline store by
+ // reading up to 300 bytes. If it is incorrect, clear the offline flag on the
+ // message and return false. This will cause a fall back to reading the
+ // message from the server. We will also advance the offset past the envelope
+ // header ("From " or "FCC") and "X-Mozilla-Status*" lines so these line are
+ // not included when the message is read from the file.
+ // Note: This occurs for both mbox and maildir offline store and probably any
+ // future pluggable store that may be supported.
+ nsCOMPtr<nsISeekableStream> seekableStream = do_QueryInterface(*aFileStream);
+ if (seekableStream) {
+ int64_t o;
+ seekableStream->Tell(&o);
+ *offset = uint64_t(o);
+ char startOfMsg[301];
+ uint32_t bytesRead = 0;
+ uint32_t bytesToRead = sizeof(startOfMsg) - 1;
+ rv = (*aFileStream)->Read(startOfMsg, bytesToRead, &bytesRead);
+ startOfMsg[bytesRead] = '\0';
+ uint32_t msgOffset = 0;
+ uint32_t keepMsgOffset = 0;
+ char* headerLine = startOfMsg;
+ int32_t findPos;
+ // Check a few lines in startOfMsg[] to verify message record validity.
+ bool line1 = true;
+ bool foundError = false;
+ // If Read() above fails, don't check any lines and set record bad.
+ // Even if Read() succeeds, don't enter the loop below if bytesRead is 0.
+ bool foundNextLine = NS_SUCCEEDED(rv) && (bytesRead > 0) ? true : false;
+ while (foundNextLine) {
+ headerLine = startOfMsg + msgOffset;
+ // Ignore lines beginning X-Mozilla-Status or X-Mozilla-Status2
+ if (!strncmp(headerLine, X_MOZILLA_STATUS, X_MOZILLA_STATUS_LEN) ||
+ !strncmp(headerLine, X_MOZILLA_STATUS2, X_MOZILLA_STATUS2_LEN)) {
+ // If there is an invalid line ahead of X-Mozilla-Status lines,
+ // immediately flag this a bad record. Only the "From " or "FCC"
+ // delimiter line is expected and OK before this.
+ if (foundError) {
+ break;
+ }
+ foundNextLine =
+ MsgAdvanceToNextLine(startOfMsg, msgOffset, bytesRead - 1);
+ line1 = false;
+ continue;
+ }
+ if (line1) {
+ // Ignore "From " and, for Drafts, "FCC" when on first line.
+ if ((!strncmp(headerLine, "From ", 5) ||
+ ((mFlags & nsMsgFolderFlags::Drafts) &&
+ !strncmp(headerLine, "FCC", 3)))) {
+ foundNextLine =
+ MsgAdvanceToNextLine(startOfMsg, msgOffset, bytesRead - 1);
+ line1 = false;
+ continue;
+ }
+ }
+ bool validOrFrom = false;
+ // Check if line looks like a valid header (just check for a colon). Also
+ // a line beginning with "From " as is sometimes returned by broken IMAP
+ // servers is also acceptable. Also, size of message must be greater than
+ // the offset of the first header line into the message (msgOffset).
+ findPos = MsgFindCharInSet(nsDependentCString(headerLine), ":\n\r", 0);
+ if (((findPos != kNotFound && headerLine[findPos] == ':') ||
+ !strncmp(headerLine, "From ", 5)) &&
+ *size > msgOffset) {
+ validOrFrom = true;
+ }
+ if (!foundError) {
+ if (validOrFrom) {
+ // Record looks OK, accept it.
+ break;
+ } else {
+ foundError = true;
+ keepMsgOffset = msgOffset;
+ foundNextLine =
+ MsgAdvanceToNextLine(startOfMsg, msgOffset, bytesRead - 1);
+ if (MOZ_LOG_TEST(DBLog, LogLevel::Info)) {
+ char save;
+ if (foundNextLine) {
+ // Temporarily null terminate the bad header line for logging.
+ save = startOfMsg[msgOffset - 1]; // should be \r or \n
+ startOfMsg[msgOffset - 1] = 0;
+
+ // DEBUG: the error happened while checking network outage
+ // condition.
+ // XXX TODO We may need to check if dovecot and other imap
+ // servers are returning funny "From " line, etc.
+ NS_WARNING(
+ nsPrintfCString("Strange startOfMsg: |%s|\n", startOfMsg)
+ .get());
+ }
+ MOZ_LOG(DBLog, LogLevel::Info,
+ ("Invalid header line in offline store: %s",
+ startOfMsg + keepMsgOffset));
+ if (foundNextLine) startOfMsg[msgOffset - 1] = save;
+ }
+ line1 = false;
+ continue;
+ }
+ } else {
+ if (validOrFrom) {
+ // Previous was bad, this is good, accept the record at bad line.
+ foundError = false;
+ msgOffset = keepMsgOffset;
+ break;
+ }
+ // If reached, two consecutive lines bad, reject the record
+ break;
+ }
+ } // while (foundNextLine)
+
+ if (!foundNextLine) {
+ // Can't find a valid header line in the buffer or buffer read() failed.
+ foundError = true;
+ }
+
+ if (!foundError) {
+ *offset += msgOffset;
+ *size -= msgOffset;
+ seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, *offset);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ } else {
+ // Offline store message record looks bad. Cause message fetch from
+ // server and store in RAM cache.
+ MOZ_ASSERT(mDatabase); // Would have crashed above if mDatabase null!
+ mDatabase->MarkOffline(msgKey, false, nullptr);
+ MOZ_LOG(DBLog, LogLevel::Error,
+ ("Leading offline store file content appears invalid, will fetch "
+ "message from server."));
+ MOZ_LOG(
+ DBLog, LogLevel::Error,
+ ("First 300 bytes of offline store content are:\n%s", startOfMsg));
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) {
+ // Eventually this will be purely a matter of fetching the storeToken
+ // from the header and asking the msgStore for an inputstream.
+ // But for now, the InputStream returned by the mbox msgStore doesn't
+ // EOF at the end of the message, so we need to jump through hoops here.
+ // For now, we implement it in the derived classes, as the message size
+ // is stored in different msgHdr attributes depending on folder type.
+ // See Bug 1764857.
+ // Also, IMAP has a gmail hack to work with (multiple msgHdrs referrring
+ // to the same locally-stored message).
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetOfflineStoreOutputStream(nsIMsgDBHdr* aHdr,
+ nsIOutputStream** aOutputStream) {
+ NS_ENSURE_ARG_POINTER(aOutputStream);
+ NS_ENSURE_ARG_POINTER(aHdr);
+
+ nsCOMPtr<nsIMsgPluggableStore> offlineStore;
+ nsresult rv = GetMsgStore(getter_AddRefs(offlineStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = offlineStore->GetNewMsgOutputStream(this, &aHdr, aOutputStream);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetMsgInputStream(nsIMsgDBHdr* aMsgHdr,
+ nsIInputStream** aInputStream) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ NS_ENSURE_ARG_POINTER(aInputStream);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString storeToken;
+ rv = aMsgHdr->GetStringProperty("storeToken", storeToken);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Handle legacy DB which has mbox offset but no storeToken.
+ // If this is still needed (open question), it should be done as separate
+ // migration pass, probably at folder creation when store and DB are set
+ // up (but that's tricky at the moment, because the DB is created
+ // on-demand).
+ if (storeToken.IsEmpty()) {
+ nsAutoCString storeType;
+ msgStore->GetStoreType(storeType);
+ if (!storeType.EqualsLiteral("mbox")) {
+ return NS_ERROR_FAILURE; // DB is missing storeToken.
+ }
+ uint64_t offset;
+ aMsgHdr->GetMessageOffset(&offset);
+ storeToken = nsPrintfCString("%" PRIu64, offset);
+ rv = aMsgHdr->SetStringProperty("storeToken", storeToken);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = msgStore->GetMsgInputStream(this, storeToken, aInputStream);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(nsPrintfCString(
+ "(debug) nsMsgDBFolder::GetMsgInputStream: msgStore->"
+ "GetMsgInputStream(this, ...) returned error rv=0x%" PRIx32
+ "\n",
+ static_cast<uint32_t>(rv))
+ .get());
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// path coming in is the root path without the leaf name,
+// on the way out, it's the whole path.
+nsresult nsMsgDBFolder::CreateFileForDB(const nsAString& userLeafName,
+ nsIFile* path, nsIFile** dbFile) {
+ NS_ENSURE_ARG_POINTER(dbFile);
+
+ nsAutoString proposedDBName(userLeafName);
+ NS_MsgHashIfNecessary(proposedDBName);
+
+ // (note, the caller of this will be using the dbFile to call db->Open()
+ // will turn the path into summary file path, and append the ".msf" extension)
+ //
+ // we want db->Open() to create a new summary file
+ // so we have to jump through some hoops to make sure the .msf it will
+ // create is unique. now that we've got the "safe" proposedDBName,
+ // we append ".msf" to see if the file exists. if so, we make the name
+ // unique and then string off the ".msf" so that we pass the right thing
+ // into Open(). this isn't ideal, since this is not atomic
+ // but it will make do.
+ nsresult rv;
+ nsCOMPtr<nsIFile> dbPath = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ dbPath->InitWithFile(path);
+ proposedDBName.AppendLiteral(SUMMARY_SUFFIX);
+ dbPath->Append(proposedDBName);
+ bool exists;
+ dbPath->Exists(&exists);
+ if (exists) {
+ rv = dbPath->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ dbPath->GetLeafName(proposedDBName);
+ }
+ // now, take the ".msf" off
+ proposedDBName.SetLength(proposedDBName.Length() - SUMMARY_SUFFIX_LENGTH);
+ dbPath->SetLeafName(proposedDBName);
+
+ dbPath.forget(dbFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetMsgDatabase(nsIMsgDatabase** aMsgDatabase) {
+ NS_ENSURE_ARG_POINTER(aMsgDatabase);
+ GetDatabase();
+ if (!mDatabase) return NS_ERROR_FAILURE;
+ NS_ADDREF(*aMsgDatabase = mDatabase);
+ mDatabase->SetLastUseTime(PR_Now());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetMsgDatabase(nsIMsgDatabase* aMsgDatabase) {
+ if (mDatabase) {
+ // commit here - db might go away when all these refs are released.
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ mDatabase->RemoveListener(this);
+ mDatabase->ClearCachedHdrs();
+ if (!aMsgDatabase) {
+ mDatabase->GetNewList(m_newMsgs);
+ }
+ }
+ mDatabase = aMsgDatabase;
+
+ if (aMsgDatabase) aMsgDatabase->AddListener(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetDatabaseOpen(bool* aOpen) {
+ NS_ENSURE_ARG_POINTER(aOpen);
+
+ *aOpen = (mDatabase != nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetBackupMsgDatabase(nsIMsgDatabase** aMsgDatabase) {
+ NS_ENSURE_ARG_POINTER(aMsgDatabase);
+ nsresult rv = OpenBackupMsgDatabase();
+ if (NS_FAILED(rv)) {
+ NS_WARNING(nsPrintfCString(
+ "(debug) OpenBackupMsgDatabase(); returns error=0x%" PRIx32
+ "\n",
+ static_cast<uint32_t>(rv))
+ .get());
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mBackupDatabase) return NS_ERROR_FAILURE;
+
+ NS_ADDREF(*aMsgDatabase = mBackupDatabase);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** database) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnReadChanged(nsIDBChangeListener* aInstigator) {
+ /* do nothing. if you care about this, override it. see nsNewsFolder.cpp */
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnJunkScoreChanged(nsIDBChangeListener* aInstigator) {
+ NotifyFolderEvent(kJunkStatusChanged);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange,
+ const nsACString& property, bool aPreChange,
+ uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) {
+ /* do nothing. if you care about this, override it.*/
+ return NS_OK;
+}
+
+// 1. When the status of a message changes.
+NS_IMETHODIMP nsMsgDBFolder::OnHdrFlagsChanged(
+ nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ if (aHdrChanged) {
+ SendFlagNotifications(aHdrChanged, aOldFlags, aNewFlags);
+ UpdateSummaryTotals(true);
+ }
+
+ // The old state was new message state
+ // We check and see if this state has changed
+ if (aOldFlags & nsMsgMessageFlags::New) {
+ // state changing from new to something else
+ if (!(aNewFlags & nsMsgMessageFlags::New))
+ CheckWithNewMessagesStatus(false);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::CheckWithNewMessagesStatus(bool messageAdded) {
+ bool hasNewMessages;
+ if (messageAdded)
+ SetHasNewMessages(true);
+ else // message modified or deleted
+ {
+ if (mDatabase) {
+ nsresult rv = mDatabase->HasNew(&hasNewMessages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SetHasNewMessages(hasNewMessages);
+ }
+ }
+
+ return NS_OK;
+}
+
+// 3. When a message gets deleted, we need to see if it was new
+// When we lose a new message we need to check if there are still new
+// messages
+NS_IMETHODIMP nsMsgDBFolder::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged,
+ nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ // check to see if a new message is being deleted
+ // as in this case, if there is only one new message and it's being deleted
+ // the folder newness has to be cleared.
+ CheckWithNewMessagesStatus(false);
+ // Remove all processing flags. This is generally a good thing although
+ // undo-ing a message back into position will not re-gain the flags.
+ nsMsgKey msgKey;
+ aHdrChanged->GetMessageKey(&msgKey);
+ AndProcessingFlags(msgKey, 0);
+ return OnHdrAddedOrDeleted(aHdrChanged, false);
+}
+
+// 2. When a new messages gets added, we need to see if it's new.
+NS_IMETHODIMP nsMsgDBFolder::OnHdrAdded(nsIMsgDBHdr* aHdrChanged,
+ nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ if (aFlags & nsMsgMessageFlags::New) CheckWithNewMessagesStatus(true);
+ return OnHdrAddedOrDeleted(aHdrChanged, true);
+}
+
+nsresult nsMsgDBFolder::OnHdrAddedOrDeleted(nsIMsgDBHdr* aHdrChanged,
+ bool added) {
+ if (added)
+ NotifyMessageAdded(aHdrChanged);
+ else
+ NotifyMessageRemoved(aHdrChanged);
+ UpdateSummaryTotals(true);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OnParentChanged(nsMsgKey aKeyChanged,
+ nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDBHdr> hdrChanged;
+ mDatabase->GetMsgHdrForKey(aKeyChanged, getter_AddRefs(hdrChanged));
+ // In reality we probably want to just change the parent because otherwise we
+ // will lose things like selection.
+ if (hdrChanged) {
+ // First delete the child from the old threadParent
+ OnHdrAddedOrDeleted(hdrChanged, false);
+ // Then add it to the new threadParent
+ OnHdrAddedOrDeleted(hdrChanged, true);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OnAnnouncerGoingAway(
+ nsIDBChangeAnnouncer* instigator) {
+ if (mBackupDatabase && instigator == mBackupDatabase) {
+ mBackupDatabase->RemoveListener(this);
+ mBackupDatabase = nullptr;
+ } else if (mDatabase) {
+ mDatabase->RemoveListener(this);
+ mDatabase = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetManyHeadersToDownload(bool* retval) {
+ NS_ENSURE_ARG_POINTER(retval);
+ int32_t numTotalMessages;
+
+ // is there any reason to return false?
+ if (!mDatabase)
+ *retval = true;
+ else if (NS_SUCCEEDED(GetTotalMessages(false, &numTotalMessages)) &&
+ numTotalMessages <= 0)
+ *retval = true;
+ else
+ *retval = false;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::MsgFitsDownloadCriteria(nsMsgKey msgKey, bool* result) {
+ if (!mDatabase) return NS_ERROR_FAILURE;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr));
+ if (NS_FAILED(rv)) return rv;
+
+ if (hdr) {
+ uint32_t msgFlags = 0;
+ hdr->GetFlags(&msgFlags);
+ // check if we already have this message body offline
+ if (!(msgFlags & nsMsgMessageFlags::Offline)) {
+ *result = true;
+ // check against the server download size limit .
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = GetServer(getter_AddRefs(incomingServer));
+ if (NS_SUCCEEDED(rv) && incomingServer) {
+ bool limitDownloadSize = false;
+ rv = incomingServer->GetLimitOfflineMessageSize(&limitDownloadSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (limitDownloadSize) {
+ int32_t maxDownloadMsgSize = 0;
+ uint32_t msgSize;
+ hdr->GetMessageSize(&msgSize);
+ rv = incomingServer->GetMaxMessageSize(&maxDownloadMsgSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ maxDownloadMsgSize *= 1024;
+ if (msgSize > (uint32_t)maxDownloadMsgSize) *result = false;
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetSupportsOffline(bool* aSupportsOffline) {
+ NS_ENSURE_ARG_POINTER(aSupportsOffline);
+ if (mFlags & nsMsgFolderFlags::Virtual) {
+ *aSupportsOffline = false;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!server) return NS_ERROR_FAILURE;
+
+ int32_t offlineSupportLevel;
+ rv = server->GetOfflineSupportLevel(&offlineSupportLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aSupportsOffline = (offlineSupportLevel >= OFFLINE_SUPPORT_LEVEL_REGULAR);
+ return NS_OK;
+}
+
+// Note: this probably always returns false for local folders!
+// Looks like it's only ever used for IMAP folders.
+NS_IMETHODIMP nsMsgDBFolder::ShouldStoreMsgOffline(nsMsgKey msgKey,
+ bool* result) {
+ NS_ENSURE_ARG(result);
+ uint32_t flags = 0;
+ *result = false;
+ GetFlags(&flags);
+ return flags & nsMsgFolderFlags::Offline
+ ? MsgFitsDownloadCriteria(msgKey, result)
+ : NS_OK;
+}
+
+// Looks like this implementation is only ever used for IMAP folders.
+NS_IMETHODIMP nsMsgDBFolder::HasMsgOffline(nsMsgKey msgKey, bool* result) {
+ NS_ENSURE_ARG(result);
+ *result = false;
+ GetDatabase();
+ if (!mDatabase) return NS_ERROR_FAILURE;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr));
+ if (NS_FAILED(rv)) return rv;
+
+ if (hdr) {
+ uint32_t msgFlags = 0;
+ hdr->GetFlags(&msgFlags);
+ // check if we already have this message body offline
+ if ((msgFlags & nsMsgMessageFlags::Offline)) *result = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFlags(uint32_t* _retval) {
+ ReadDBFolderInfo(false);
+ *_retval = mFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ReadFromFolderCacheElem(
+ nsIMsgFolderCacheElement* element) {
+ nsresult rv = NS_OK;
+
+ element->GetCachedUInt32("flags", &mFlags);
+ element->GetCachedInt32("totalMsgs", &mNumTotalMessages);
+ element->GetCachedInt32("totalUnreadMsgs", &mNumUnreadMessages);
+ element->GetCachedInt32("pendingUnreadMsgs", &mNumPendingUnreadMessages);
+ element->GetCachedInt32("pendingMsgs", &mNumPendingTotalMessages);
+ element->GetCachedInt64("expungedBytes", &mExpungedBytes);
+ element->GetCachedInt64("folderSize", &mFolderSize);
+
+#ifdef DEBUG_bienvenu1
+ nsCString uri;
+ GetURI(uri);
+ printf("read total %ld for %s\n", mNumTotalMessages, uri.get());
+#endif
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetFolderCacheKey(nsIFile** aFile) {
+ nsresult rv;
+ bool isServer = false;
+ GetIsServer(&isServer);
+
+ // if it's a server, we don't need the .msf appended to the name
+ nsCOMPtr<nsIFile> dbPath;
+ if (isServer) {
+ rv = GetFilePath(getter_AddRefs(dbPath));
+ } else {
+ rv = GetSummaryFile(getter_AddRefs(dbPath));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+ dbPath.forget(aFile);
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::FlushToFolderCache() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv) && accountManager) {
+ nsCOMPtr<nsIMsgFolderCache> folderCache;
+ rv = accountManager->GetFolderCache(getter_AddRefs(folderCache));
+ if (NS_SUCCEEDED(rv) && folderCache)
+ rv = WriteToFolderCache(folderCache, false);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::WriteToFolderCache(nsIMsgFolderCache* folderCache,
+ bool deep) {
+ nsresult rv = NS_OK;
+
+ if (folderCache) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ nsCOMPtr<nsIFile> dbPath;
+ rv = GetFolderCacheKey(getter_AddRefs(dbPath));
+#ifdef DEBUG_bienvenu1
+ bool exists;
+ NS_ASSERTION(NS_SUCCEEDED(dbPath->Exists(&exists)) && exists,
+ "file spec we're adding to cache should exist");
+#endif
+ if (NS_SUCCEEDED(rv) && dbPath) {
+ nsCString persistentPath;
+ rv = dbPath->GetPersistentDescriptor(persistentPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folderCache->GetCacheElement(persistentPath, true,
+ getter_AddRefs(cacheElement));
+ if (NS_SUCCEEDED(rv) && cacheElement)
+ rv = WriteToFolderCacheElem(cacheElement);
+ }
+
+ if (deep) {
+ for (nsIMsgFolder* msgFolder : mSubFolders) {
+ rv = msgFolder->WriteToFolderCache(folderCache, true);
+ if (NS_FAILED(rv)) break;
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::WriteToFolderCacheElem(
+ nsIMsgFolderCacheElement* element) {
+ nsresult rv = NS_OK;
+
+ element->SetCachedUInt32("flags", mFlags);
+ element->SetCachedInt32("totalMsgs", mNumTotalMessages);
+ element->SetCachedInt32("totalUnreadMsgs", mNumUnreadMessages);
+ element->SetCachedInt32("pendingUnreadMsgs", mNumPendingUnreadMessages);
+ element->SetCachedInt32("pendingMsgs", mNumPendingTotalMessages);
+ element->SetCachedInt64("expungedBytes", mExpungedBytes);
+ element->SetCachedInt64("folderSize", mFolderSize);
+
+#ifdef DEBUG_bienvenu1
+ nsCString uri;
+ GetURI(uri);
+ printf("writing total %ld for %s\n", mNumTotalMessages, uri.get());
+#endif
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::AddMessageDispositionState(
+ nsIMsgDBHdr* aMessage, nsMsgDispositionState aDispositionFlag) {
+ NS_ENSURE_ARG_POINTER(aMessage);
+
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ nsMsgKey msgKey;
+ aMessage->GetMessageKey(&msgKey);
+
+ if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Replied)
+ mDatabase->MarkReplied(msgKey, true, nullptr);
+ else if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Forwarded)
+ mDatabase->MarkForwarded(msgKey, true, nullptr);
+ else if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Redirected)
+ mDatabase->MarkRedirected(msgKey, true, nullptr);
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::AddMarkAllReadUndoAction(nsIMsgWindow* msgWindow,
+ nsMsgKey* thoseMarked,
+ uint32_t numMarked) {
+ RefPtr<nsMsgReadStateTxn> readStateTxn = new nsMsgReadStateTxn();
+ if (!readStateTxn) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsresult rv = readStateTxn->Init(this, numMarked, thoseMarked);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = readStateTxn->SetTransactionType(nsIMessenger::eMarkAllMsg);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsITransactionManager> txnMgr;
+ rv = msgWindow->GetTransactionManager(getter_AddRefs(txnMgr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = txnMgr->DoTransaction(readStateTxn);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) {
+ nsresult rv = GetDatabase();
+ m_newMsgs.Clear();
+
+ if (NS_SUCCEEDED(rv)) {
+ EnableNotifications(allMessageCountNotifications, false);
+ nsTArray<nsMsgKey> thoseMarked;
+ rv = mDatabase->MarkAllRead(thoseMarked);
+ EnableNotifications(allMessageCountNotifications, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Setup a undo-state
+ if (aMsgWindow && thoseMarked.Length() > 0)
+ rv = AddMarkAllReadUndoAction(aMsgWindow, thoseMarked.Elements(),
+ thoseMarked.Length());
+ }
+
+ SetHasNewMessages(false);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::MarkThreadRead(nsIMsgThread* thread) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsTArray<nsMsgKey> keys;
+ return mDatabase->MarkThreadRead(thread, nullptr, keys);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnStartRunningUrl(nsIURI* aUrl) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ NS_ENSURE_ARG_POINTER(aUrl);
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl);
+ if (mailUrl) {
+ bool updatingFolder = false;
+ if (NS_SUCCEEDED(mailUrl->GetUpdatingFolder(&updatingFolder)) &&
+ updatingFolder)
+ NotifyFolderEvent(kFolderLoaded);
+
+ // be sure to remove ourselves as a url listener
+ mailUrl->UnRegisterListener(this);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetRetentionSettings(nsIMsgRetentionSettings** settings) {
+ NS_ENSURE_ARG_POINTER(settings);
+ *settings = nullptr;
+ nsresult rv = NS_OK;
+ bool useServerDefaults = false;
+ if (!m_retentionSettings) {
+ nsCString useServerRetention;
+ GetStringProperty(kUseServerRetentionProp, useServerRetention);
+ if (useServerRetention.EqualsLiteral("1")) {
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = GetServer(getter_AddRefs(incomingServer));
+ if (NS_SUCCEEDED(rv) && incomingServer) {
+ rv = incomingServer->GetRetentionSettings(settings);
+ useServerDefaults = true;
+ }
+ } else {
+ GetDatabase();
+ if (mDatabase) {
+ // get the settings from the db - if the settings from the db say the
+ // folder is not overriding the incoming server settings, get the
+ // settings from the server.
+ rv = mDatabase->GetMsgRetentionSettings(settings);
+ if (NS_SUCCEEDED(rv) && *settings) {
+ (*settings)->GetUseServerDefaults(&useServerDefaults);
+ if (useServerDefaults) {
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = GetServer(getter_AddRefs(incomingServer));
+ NS_IF_RELEASE(*settings);
+ if (NS_SUCCEEDED(rv) && incomingServer)
+ incomingServer->GetRetentionSettings(settings);
+ }
+ if (useServerRetention.EqualsLiteral("1") != useServerDefaults) {
+ if (useServerDefaults)
+ useServerRetention.Assign('1');
+ else
+ useServerRetention.Assign('0');
+ SetStringProperty(kUseServerRetentionProp, useServerRetention);
+ }
+ }
+ } else
+ return NS_ERROR_FAILURE;
+ }
+ // Only cache the retention settings if we've overridden the server
+ // settings (otherwise, we won't notice changes to the server settings).
+ if (!useServerDefaults) m_retentionSettings = *settings;
+ } else
+ NS_IF_ADDREF(*settings = m_retentionSettings);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetRetentionSettings(
+ nsIMsgRetentionSettings* settings) {
+ bool useServerDefaults;
+ nsCString useServerRetention;
+
+ settings->GetUseServerDefaults(&useServerDefaults);
+ if (useServerDefaults) {
+ useServerRetention.Assign('1');
+ m_retentionSettings = nullptr;
+ } else {
+ useServerRetention.Assign('0');
+ m_retentionSettings = settings;
+ }
+ SetStringProperty(kUseServerRetentionProp, useServerRetention);
+ GetDatabase();
+ if (mDatabase) mDatabase->SetMsgRetentionSettings(settings);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetDownloadSettings(
+ nsIMsgDownloadSettings** settings) {
+ NS_ENSURE_ARG_POINTER(settings);
+ nsresult rv = NS_OK;
+ if (!m_downloadSettings) {
+ GetDatabase();
+ if (mDatabase) {
+ // get the settings from the db - if the settings from the db say the
+ // folder is not overriding the incoming server settings, get the settings
+ // from the server.
+ rv =
+ mDatabase->GetMsgDownloadSettings(getter_AddRefs(m_downloadSettings));
+ if (NS_SUCCEEDED(rv) && m_downloadSettings) {
+ bool useServerDefaults;
+ m_downloadSettings->GetUseServerDefaults(&useServerDefaults);
+ if (useServerDefaults) {
+ nsCOMPtr<nsIMsgIncomingServer> incomingServer;
+ rv = GetServer(getter_AddRefs(incomingServer));
+ if (NS_SUCCEEDED(rv) && incomingServer)
+ incomingServer->GetDownloadSettings(
+ getter_AddRefs(m_downloadSettings));
+ }
+ }
+ }
+ }
+ NS_IF_ADDREF(*settings = m_downloadSettings);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetDownloadSettings(
+ nsIMsgDownloadSettings* settings) {
+ m_downloadSettings = settings;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::IsCommandEnabled(const nsACString& command,
+ bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = true;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::WriteStartOfNewLocalMessage() {
+ nsAutoCString result;
+ uint32_t writeCount;
+ time_t now = time((time_t*)0);
+ char* ct = ctime(&now);
+ ct[24] = 0;
+ result = "From - ";
+ result += ct;
+ result += MSG_LINEBREAK;
+ m_bytesAddedToLocalMsg = result.Length();
+
+ MOZ_ASSERT(m_tempMessageStream,
+ "Temporary message stream must not be nullptr");
+
+ nsresult rv =
+ m_tempMessageStream->Write(result.get(), result.Length(), &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += writeCount;
+
+ constexpr auto MozillaStatus = "X-Mozilla-Status: 0001"_ns MSG_LINEBREAK;
+ rv = m_tempMessageStream->Write(MozillaStatus.get(), MozillaStatus.Length(),
+ &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += writeCount;
+ m_bytesAddedToLocalMsg += writeCount;
+ constexpr auto MozillaStatus2 =
+ "X-Mozilla-Status2: 00000000"_ns MSG_LINEBREAK;
+ m_bytesAddedToLocalMsg += MozillaStatus2.Length();
+ rv = m_tempMessageStream->Write(MozillaStatus2.get(), MozillaStatus2.Length(),
+ &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += writeCount;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::StartNewOfflineMessage() {
+ bool isLocked;
+ GetLocked(&isLocked);
+ bool hasSemaphore = false;
+ if (isLocked) {
+ // it's OK if we, the folder, have the semaphore.
+ TestSemaphore(static_cast<nsIMsgFolder*>(this), &hasSemaphore);
+ if (!hasSemaphore) {
+ NS_WARNING("folder locked trying to download offline");
+ return NS_MSG_FOLDER_BUSY;
+ }
+ }
+ m_tempMessageStreamBytesWritten = 0;
+ nsresult rv = GetOfflineStoreOutputStream(
+ m_offlineHeader, getter_AddRefs(m_tempMessageStream));
+ if (NS_SUCCEEDED(rv) && !hasSemaphore)
+ AcquireSemaphore(static_cast<nsIMsgFolder*>(this));
+ if (NS_SUCCEEDED(rv)) WriteStartOfNewLocalMessage();
+ m_numOfflineMsgLines = 0;
+ return rv;
+}
+
+nsresult nsMsgDBFolder::EndNewOfflineMessage(nsresult status) {
+ int64_t curStorePos;
+ uint64_t messageOffset;
+ uint32_t messageSize;
+
+ nsMsgKey messageKey;
+
+ nsresult rv1, rv2;
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_offlineHeader->GetMessageKey(&messageKey);
+
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Are we being asked to abort and clean up?
+ if (NS_FAILED(status)) {
+ mDatabase->MarkOffline(messageKey, false, nullptr);
+ if (m_tempMessageStream) {
+ msgStore->DiscardNewMessage(m_tempMessageStream, m_offlineHeader);
+ }
+ m_tempMessageStream = nullptr;
+ m_offlineHeader = nullptr;
+ return NS_OK;
+ }
+
+ if (m_tempMessageStream) {
+ m_tempMessageStream->Flush();
+ }
+
+ // Some sanity checking.
+ // This will go away once nsIMsgPluggableStore stops serving up seekable
+ // output streams.
+ // If quarantining (mailnews.downloadToTempFile == true) is on we'll already
+ // have a non-seekable stream.
+ nsCOMPtr<nsISeekableStream> seekable;
+ if (m_tempMessageStream) seekable = do_QueryInterface(m_tempMessageStream);
+ if (seekable) {
+ int64_t tellPos;
+ seekable->Tell(&tellPos);
+ curStorePos = tellPos;
+
+ // N.B. This only works if we've set the offline flag for the message,
+ // so be careful about moving the call to MarkOffline above.
+ m_offlineHeader->GetMessageOffset(&messageOffset);
+ curStorePos -= messageOffset;
+ m_offlineHeader->GetMessageSize(&messageSize);
+ messageSize += m_bytesAddedToLocalMsg;
+ // unix/mac has a one byte line ending, but the imap server returns
+ // crlf terminated lines.
+ if (MSG_LINEBREAK_LEN == 1) messageSize -= m_numOfflineMsgLines;
+
+ // We clear the offline flag on the message if the size
+ // looks wrong. Check if we're off by more than one byte per line.
+ if (messageSize > (uint32_t)curStorePos &&
+ (messageSize - (uint32_t)curStorePos) >
+ (uint32_t)m_numOfflineMsgLines) {
+ mDatabase->MarkOffline(messageKey, false, nullptr);
+ // we should truncate the offline store at messageOffset
+ ReleaseSemaphore(static_cast<nsIMsgFolder*>(this));
+ rv1 = rv2 = NS_OK;
+ if (msgStore) {
+ // DiscardNewMessage now closes the stream all the time.
+ rv1 = msgStore->DiscardNewMessage(m_tempMessageStream, m_offlineHeader);
+ m_tempMessageStream = nullptr; // avoid accessing closed stream
+ } else {
+ rv2 = m_tempMessageStream->Close();
+ m_tempMessageStream = nullptr; // ditto
+ }
+ // XXX We should check for errors of rv1 and rv2.
+ if (NS_FAILED(rv1)) NS_WARNING("DiscardNewMessage returned error");
+ if (NS_FAILED(rv2))
+ NS_WARNING("m_tempMessageStream->Close() returned error");
+#ifdef _DEBUG
+ nsAutoCString message("Offline message too small: messageSize=");
+ message.AppendInt(messageSize);
+ message.AppendLiteral(" curStorePos=");
+ message.AppendInt(curStorePos);
+ message.AppendLiteral(" numOfflineMsgLines=");
+ message.AppendInt(m_numOfflineMsgLines);
+ message.AppendLiteral(" bytesAdded=");
+ message.AppendInt(m_bytesAddedToLocalMsg);
+ NS_ERROR(message.get());
+#endif
+ m_offlineHeader = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+ } // seekable
+
+ // Success! Finalise the message.
+ mDatabase->MarkOffline(messageKey, true, nullptr);
+ m_offlineHeader->SetOfflineMessageSize(m_tempMessageStreamBytesWritten);
+ m_offlineHeader->SetLineCount(m_numOfflineMsgLines);
+
+ // (But remember, stream might be buffered and closing/flushing could still
+ // fail!)
+
+ rv1 = rv2 = NS_OK;
+ if (msgStore) {
+ rv1 = msgStore->FinishNewMessage(m_tempMessageStream, m_offlineHeader);
+ m_tempMessageStream = nullptr;
+ }
+
+ // We can not let this happen: I think the code assumes this.
+ // That is the if-expression above is always true.
+ NS_ASSERTION(msgStore, "msgStore is nullptr");
+
+ // Notify users of the errors for now, just use NS_WARNING.
+ if (NS_FAILED(rv1)) NS_WARNING("FinishNewMessage returned error");
+ if (NS_FAILED(rv2)) NS_WARNING("m_tempMessageStream->Close() returned error");
+
+ m_tempMessageStream = nullptr;
+ m_offlineHeader = nullptr;
+
+ if (NS_FAILED(rv1)) return rv1;
+ if (NS_FAILED(rv2)) return rv2;
+
+ return rv;
+}
+
+class AutoCompactEvent : public mozilla::Runnable {
+ public:
+ AutoCompactEvent(nsIMsgWindow* aMsgWindow, nsMsgDBFolder* aFolder)
+ : mozilla::Runnable("AutoCompactEvent"),
+ mMsgWindow(aMsgWindow),
+ mFolder(aFolder) {}
+
+ NS_IMETHOD Run() {
+ if (mFolder) mFolder->HandleAutoCompactEvent(mMsgWindow);
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsIMsgWindow> mMsgWindow;
+ RefPtr<nsMsgDBFolder> mFolder;
+};
+
+nsresult nsMsgDBFolder::HandleAutoCompactEvent(nsIMsgWindow* aWindow) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ rv = accountMgr->GetAllServers(allServers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numServers = allServers.Length();
+ if (numServers > 0) {
+ nsTArray<RefPtr<nsIMsgFolder>> folderArray;
+ nsTArray<RefPtr<nsIMsgFolder>> offlineFolderArray;
+ int64_t totalExpungedBytes = 0;
+ int64_t offlineExpungedBytes = 0;
+ int64_t localExpungedBytes = 0;
+ uint32_t serverIndex = 0;
+ do {
+ nsCOMPtr<nsIMsgIncomingServer> server(allServers[serverIndex]);
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = server->GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!msgStore) continue;
+ bool supportsCompaction;
+ msgStore->GetSupportsCompaction(&supportsCompaction);
+ if (!supportsCompaction) continue;
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder) {
+ int32_t offlineSupportLevel;
+ rv = server->GetOfflineSupportLevel(&offlineSupportLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsTArray<RefPtr<nsIMsgFolder>> allDescendants;
+ rootFolder->GetDescendants(allDescendants);
+ int64_t expungedBytes = 0;
+ if (offlineSupportLevel > 0) {
+ uint32_t flags;
+ for (auto folder : allDescendants) {
+ expungedBytes = 0;
+ folder->GetFlags(&flags);
+ if (flags & nsMsgFolderFlags::Offline)
+ folder->GetExpungedBytes(&expungedBytes);
+ if (expungedBytes > 0) {
+ offlineFolderArray.AppendElement(folder);
+ offlineExpungedBytes += expungedBytes;
+ }
+ }
+ } else // pop or local
+ {
+ for (auto folder : allDescendants) {
+ expungedBytes = 0;
+ folder->GetExpungedBytes(&expungedBytes);
+ if (expungedBytes > 0) {
+ folderArray.AppendElement(folder);
+ localExpungedBytes += expungedBytes;
+ }
+ }
+ }
+ }
+ } while (++serverIndex < numServers);
+ totalExpungedBytes = localExpungedBytes + offlineExpungedBytes;
+ int32_t purgeThreshold;
+ rv = GetPurgeThreshold(&purgeThreshold);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (totalExpungedBytes > ((int64_t)purgeThreshold * 1024)) {
+ bool okToCompact = false;
+ nsCOMPtr<nsIPrefService> pref =
+ do_GetService(NS_PREFSERVICE_CONTRACTID);
+ nsCOMPtr<nsIPrefBranch> branch;
+ pref->GetBranch("", getter_AddRefs(branch));
+
+ bool askBeforePurge;
+ branch->GetBoolPref(PREF_MAIL_PURGE_ASK, &askBeforePurge);
+ if (askBeforePurge && aWindow) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoString compactSize;
+ FormatFileSize(totalExpungedBytes, true, compactSize);
+
+ bool neverAsk = false; // "Do not ask..." - unchecked by default.
+ int32_t buttonPressed = 0;
+
+ nsCOMPtr<nsIWindowWatcher> ww(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ nsCOMPtr<nsIWritablePropertyBag2> props(
+ do_CreateInstance("@mozilla.org/hash-property-bag;1"));
+ props->SetPropertyAsAString(u"compactSize"_ns, compactSize);
+ nsCOMPtr<mozIDOMWindowProxy> migrateWizard;
+ rv = ww->OpenWindow(
+ nullptr,
+ "chrome://messenger/content/compactFoldersDialog.xhtml"_ns,
+ "_blank"_ns, "chrome,dialog,modal,centerscreen"_ns, props,
+ getter_AddRefs(migrateWizard));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = props->GetPropertyAsBool(u"checked"_ns, &neverAsk);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv =
+ props->GetPropertyAsInt32(u"buttonNumClicked"_ns, &buttonPressed);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (buttonPressed == 0) {
+ okToCompact = true;
+ if (neverAsk) // [X] Remove deletions automatically and do not ask
+ branch->SetBoolPref(PREF_MAIL_PURGE_ASK, false);
+ }
+ } else
+ okToCompact = aWindow || !askBeforePurge;
+
+ if (okToCompact) {
+ NotifyFolderEvent(kAboutToCompact);
+
+ if (localExpungedBytes > 0 || offlineExpungedBytes > 0) {
+ nsCOMPtr<nsIMsgFolderCompactor> folderCompactor = do_CreateInstance(
+ "@mozilla.org/messenger/foldercompactor;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (nsIMsgFolder* f : offlineFolderArray) {
+ folderArray.AppendElement(f);
+ }
+ rv = folderCompactor->CompactFolders(folderArray, nullptr, aWindow);
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgDBFolder::AutoCompact(nsIMsgWindow* aWindow) {
+ // we don't check for null aWindow, because this routine can get called
+ // in unit tests where we have no window. Just assume not OK if no window.
+ bool prompt;
+ nsresult rv = GetPromptPurgeThreshold(&prompt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ PRTime timeNow = PR_Now(); // time in microseconds
+ PRTime timeAfterOneHourOfLastPurgeCheck = gtimeOfLastPurgeCheck + oneHour;
+ if (timeAfterOneHourOfLastPurgeCheck < timeNow && prompt) {
+ gtimeOfLastPurgeCheck = timeNow;
+ nsCOMPtr<nsIRunnable> event = new AutoCompactEvent(aWindow, this);
+ // Post this as an event because it can put up an alert, which
+ // might cause issues depending on the stack when we are called.
+ if (event) NS_DispatchToCurrentThread(event);
+ }
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetPromptPurgeThreshold(bool* aPrompt) {
+ NS_ENSURE_ARG(aPrompt);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv) && prefBranch) {
+ rv = prefBranch->GetBoolPref(PREF_MAIL_PROMPT_PURGE_THRESHOLD, aPrompt);
+ if (NS_FAILED(rv)) {
+ *aPrompt = false;
+ rv = NS_OK;
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetPurgeThreshold(int32_t* aThreshold) {
+ NS_ENSURE_ARG(aThreshold);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (NS_SUCCEEDED(rv) && prefBranch) {
+ int32_t thresholdMB = 200;
+ bool thresholdMigrated = false;
+ prefBranch->GetIntPref(PREF_MAIL_PURGE_THRESHOLD_MB, &thresholdMB);
+ prefBranch->GetBoolPref(PREF_MAIL_PURGE_MIGRATED, &thresholdMigrated);
+ if (!thresholdMigrated) {
+ *aThreshold = 20480;
+ (void)prefBranch->GetIntPref(PREF_MAIL_PURGE_THRESHOLD, aThreshold);
+ if (*aThreshold / 1024 != thresholdMB) {
+ thresholdMB = std::max(1, *aThreshold / 1024);
+ prefBranch->SetIntPref(PREF_MAIL_PURGE_THRESHOLD_MB, thresholdMB);
+ }
+ prefBranch->SetBoolPref(PREF_MAIL_PURGE_MIGRATED, true);
+ }
+ *aThreshold = thresholdMB * 1024;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP // called on the folder that is renamed or about to be deleted
+nsMsgDBFolder::MatchOrChangeFilterDestination(nsIMsgFolder* newFolder,
+ bool caseInsensitive,
+ bool* found) {
+ NS_ENSURE_ARG_POINTER(found);
+ *found = false;
+ nsCString oldUri;
+ nsresult rv = GetURI(oldUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString newUri;
+ if (newFolder) // for matching uri's this will be null
+ {
+ rv = newFolder->GetURI(newUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ rv = accountMgr->GetAllServers(allServers);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto server : allServers) {
+ if (server) {
+ bool canHaveFilters;
+ rv = server->GetCanHaveFilters(&canHaveFilters);
+ if (NS_SUCCEEDED(rv) && canHaveFilters) {
+ // update the filterlist to match the new folder name
+ rv = server->GetFilterList(nullptr, getter_AddRefs(filterList));
+ if (NS_SUCCEEDED(rv) && filterList) {
+ bool match;
+ rv = filterList->MatchOrChangeFilterTarget(oldUri, newUri,
+ caseInsensitive, &match);
+ if (NS_SUCCEEDED(rv) && match) {
+ *found = true;
+ if (newFolder && !newUri.IsEmpty())
+ rv = filterList->SaveToDefaultFile();
+ }
+ }
+ // update the editable filterlist to match the new folder name
+ rv = server->GetEditableFilterList(nullptr, getter_AddRefs(filterList));
+ if (NS_SUCCEEDED(rv) && filterList) {
+ bool match;
+ rv = filterList->MatchOrChangeFilterTarget(oldUri, newUri,
+ caseInsensitive, &match);
+ if (NS_SUCCEEDED(rv) && match) {
+ *found = true;
+ if (newFolder && !newUri.IsEmpty())
+ rv = filterList->SaveToDefaultFile();
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetDBTransferInfo(nsIDBFolderInfo** aTransferInfo) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), getter_AddRefs(db));
+ NS_ENSURE_STATE(dbFolderInfo);
+ return dbFolderInfo->GetTransferInfo(aTransferInfo);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetDBTransferInfo(nsIDBFolderInfo* aTransferInfo) {
+ NS_ENSURE_ARG(aTransferInfo);
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ GetMsgDatabase(getter_AddRefs(db));
+ if (db) {
+ db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) {
+ dbFolderInfo->InitFromTransferInfo(aTransferInfo);
+ dbFolderInfo->SetBooleanProperty("forceReparse", false);
+ }
+ db->SetSummaryValid(true);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetStringProperty(const char* propertyName,
+ nsACString& propertyValue) {
+ NS_ENSURE_ARG_POINTER(propertyName);
+ nsCOMPtr<nsIFile> dbPath;
+ nsresult rv = GetFolderCacheKey(getter_AddRefs(dbPath));
+ if (dbPath) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ rv = GetFolderCacheElemFromFile(dbPath, getter_AddRefs(cacheElement));
+ if (cacheElement) // try to get from cache
+ rv = cacheElement->GetCachedString(propertyName, propertyValue);
+ if (NS_FAILED(rv)) // if failed, then try to get from db, usually.
+ {
+ if (strcmp(propertyName, MRU_TIME_PROPERTY) == 0 ||
+ strcmp(propertyName, MRM_TIME_PROPERTY) == 0 ||
+ strcmp(propertyName, "LastPurgeTime") == 0) {
+ // Don't open DB for missing time properties.
+ // Missing time properties can happen if the folder was never
+ // accessed, for exaple after an import. They happen if
+ // folderCache.json is removed or becomes invalid after moving
+ // a profile (see bug 1726660).
+ propertyValue.Truncate();
+ return NS_OK;
+ }
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ bool exists;
+ rv = dbPath->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) return NS_MSG_ERROR_FOLDER_MISSING;
+ bool weOpenedDB = !mDatabase;
+ rv = GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv))
+ rv = folderInfo->GetCharProperty(propertyName, propertyValue);
+ if (weOpenedDB) CloseDBIfFolderNotOpen(false);
+ if (NS_SUCCEEDED(rv)) {
+ // Now that we have the value, store it in our cache.
+ if (cacheElement) {
+ cacheElement->SetCachedString(propertyName, propertyValue);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetStringProperty(const char* propertyName,
+ const nsACString& propertyValue) {
+ NS_ENSURE_ARG_POINTER(propertyName);
+ nsCOMPtr<nsIFile> dbPath;
+ GetFolderCacheKey(getter_AddRefs(dbPath));
+ if (dbPath) {
+ nsCOMPtr<nsIMsgFolderCacheElement> cacheElement;
+ GetFolderCacheElemFromFile(dbPath, getter_AddRefs(cacheElement));
+ if (cacheElement) // try to set in the cache
+ cacheElement->SetCachedString(propertyName, propertyValue);
+ }
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv =
+ GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv)) {
+ folderInfo->SetCharProperty(propertyName, propertyValue);
+ db->Commit(nsMsgDBCommitType::kLargeCommit); // committing the db also
+ // commits the cache
+ }
+ return NS_OK;
+}
+
+// Get/Set ForcePropertyEmpty is only used with inherited properties
+NS_IMETHODIMP
+nsMsgDBFolder::GetForcePropertyEmpty(const char* aPropertyName, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsAutoCString nameEmpty(aPropertyName);
+ nameEmpty.AppendLiteral(".empty");
+ nsCString value;
+ GetStringProperty(nameEmpty.get(), value);
+ *_retval = value.EqualsLiteral("true");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetForcePropertyEmpty(const char* aPropertyName, bool aValue) {
+ nsAutoCString nameEmpty(aPropertyName);
+ nameEmpty.AppendLiteral(".empty");
+ return SetStringProperty(nameEmpty.get(), aValue ? "true"_ns : ""_ns);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetInheritedStringProperty(const char* aPropertyName,
+ nsACString& aPropertyValue) {
+ NS_ENSURE_ARG_POINTER(aPropertyName);
+ nsCString value;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+
+ bool forceEmpty = false;
+
+ if (!mIsServer) {
+ GetForcePropertyEmpty(aPropertyName, &forceEmpty);
+ } else {
+ // root folders must get their values from the server
+ GetServer(getter_AddRefs(server));
+ if (server) server->GetForcePropertyEmpty(aPropertyName, &forceEmpty);
+ }
+
+ if (forceEmpty) {
+ aPropertyValue.Truncate();
+ return NS_OK;
+ }
+
+ // servers will automatically inherit from the preference
+ // mail.server.default.(propertyName)
+ if (server) return server->GetCharValue(aPropertyName, aPropertyValue);
+
+ GetStringProperty(aPropertyName, value);
+ if (value.IsEmpty()) {
+ // inherit from the parent
+ nsCOMPtr<nsIMsgFolder> parent;
+ GetParent(getter_AddRefs(parent));
+ if (parent)
+ return parent->GetInheritedStringProperty(aPropertyName, aPropertyValue);
+ }
+
+ aPropertyValue.Assign(value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnMessageClassified(const nsACString& aMsgURI,
+ nsMsgJunkStatus aClassification,
+ uint32_t aJunkPercent) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ if (aMsgURI.IsEmpty()) // This signifies end of batch.
+ {
+ // Apply filters if needed.
+ if (!mPostBayesMessagesToFilter.IsEmpty()) {
+ // Apply post-bayes filtering.
+ nsCOMPtr<nsIMsgFilterService> filterService(
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv));
+ if (NS_SUCCEEDED(rv))
+ // We use a null nsIMsgWindow because we don't want some sort of ui
+ // appearing in the middle of automatic filtering (plus I really don't
+ // want to propagate that value.)
+ rv = filterService->ApplyFilters(nsMsgFilterType::PostPlugin,
+ mPostBayesMessagesToFilter, this,
+ nullptr, nullptr);
+ mPostBayesMessagesToFilter.Clear();
+ }
+
+ // If we classified any messages, send out a notification.
+ nsTArray<RefPtr<nsIMsgDBHdr>> hdrs;
+ rv = MsgGetHeadersFromKeys(mDatabase, mClassifiedMsgKeys, hdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hdrs.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService(
+ "@mozilla.org/messenger/msgnotificationservice;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyMsgsClassified(hdrs, mBayesJunkClassifying,
+ mBayesTraitClassifying);
+ }
+ return rv;
+ }
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey msgKey;
+ rv = msgHdr->GetMessageKey(&msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if this message needs junk classification
+ uint32_t processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+
+ if (processingFlags & nsMsgProcessingFlags::ClassifyJunk) {
+ mClassifiedMsgKeys.AppendElement(msgKey);
+ AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::ClassifyJunk);
+
+ nsAutoCString msgJunkScore;
+ msgJunkScore.AppendInt(aClassification == nsIJunkMailPlugin::JUNK
+ ? nsIJunkMailPlugin::IS_SPAM_SCORE
+ : nsIJunkMailPlugin::IS_HAM_SCORE);
+ mDatabase->SetStringProperty(msgKey, "junkscore", msgJunkScore);
+ mDatabase->SetStringProperty(msgKey, "junkscoreorigin", "plugin"_ns);
+
+ nsAutoCString strPercent;
+ strPercent.AppendInt(aJunkPercent);
+ mDatabase->SetStringProperty(msgKey, "junkpercent", strPercent);
+
+ if (aClassification == nsIJunkMailPlugin::JUNK) {
+ // IMAP has its own way of marking read.
+ if (!(mFlags & nsMsgFolderFlags::ImapBox)) {
+ bool markAsReadOnSpam;
+ (void)spamSettings->GetMarkAsReadOnSpam(&markAsReadOnSpam);
+ if (markAsReadOnSpam) {
+ rv = mDatabase->MarkRead(msgKey, true, this);
+ if (!NS_SUCCEEDED(rv))
+ NS_WARNING("failed marking spam message as read");
+ }
+ }
+ // mail folders will log junk hits with move info. Perhaps we should
+ // add a log here for non-mail folders as well, that don't override
+ // onMessageClassified
+ // rv = spamSettings->LogJunkHit(msgHdr, false);
+ // NS_ENSURE_SUCCESS(rv,rv);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::OnMessageTraitsClassified(const nsACString& aMsgURI,
+ const nsTArray<uint32_t>& aTraits,
+ const nsTArray<uint32_t>& aPercents) {
+ if (aMsgURI.IsEmpty()) // This signifies end of batch
+ return NS_OK; // We are not handling batching
+
+ MOZ_ASSERT(aTraits.Length() == aPercents.Length());
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey msgKey;
+ rv = msgHdr->GetMessageKey(&msgKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+ if (!(processingFlags & nsMsgProcessingFlags::ClassifyTraits)) return NS_OK;
+
+ AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::ClassifyTraits);
+
+ nsCOMPtr<nsIMsgTraitService> traitService;
+ traitService = do_GetService("@mozilla.org/msg-trait-service;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (uint32_t i = 0; i < aTraits.Length(); i++) {
+ if (aTraits[i] == nsIJunkMailPlugin::JUNK_TRAIT)
+ continue; // junk is processed by the junk listener
+ nsAutoCString traitId;
+ rv = traitService->GetId(aTraits[i], traitId);
+ traitId.InsertLiteral("bayespercent/", 0);
+ nsAutoCString strPercent;
+ strPercent.AppendInt(aPercents[i]);
+ mDatabase->SetStringPropertyByHdr(msgHdr, traitId.get(), strPercent);
+ }
+ return NS_OK;
+}
+
+/**
+ * Call the filter plugins (XXX currently just one)
+ */
+NS_IMETHODIMP
+nsMsgDBFolder::CallFilterPlugins(nsIMsgWindow* aMsgWindow, bool* aFiltersRun) {
+ NS_ENSURE_ARG_POINTER(aFiltersRun);
+
+ nsString folderName;
+ GetPrettyName(folderName);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Running filter plugins on folder '%s'",
+ NS_ConvertUTF16toUTF8(folderName).get()));
+
+ *aFiltersRun = false;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ int32_t spamLevel = 0;
+
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverType;
+ server->GetType(serverType);
+
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ nsCOMPtr<nsIMsgFilterPlugin> filterPlugin;
+ server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin));
+ if (!filterPlugin) // it's not an error not to have the filter plugin.
+ return NS_OK;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIJunkMailPlugin> junkMailPlugin = do_QueryInterface(filterPlugin);
+ if (!junkMailPlugin) // we currently only support the junk mail plugin
+ return NS_OK;
+
+ // if it's a news folder, then we really don't support junk in the ui
+ // yet the legacy spamLevel seems to think we should analyze it.
+ // Maybe we should upgrade that, but for now let's not analyze. We'll
+ // let an extension set an inherited property if they really want us to
+ // analyze this. We need that anyway to allow extension-based overrides.
+ // When we finalize adding junk in news to core, we'll deal with the
+ // spamLevel issue
+
+ // if this is the junk folder, or the trash folder
+ // don't analyze for spam, because we don't care
+ //
+ // if it's the sent, unsent, templates, or drafts,
+ // don't analyze for spam, because the user
+ // created that message
+ //
+ // if it's a public imap folder, or another users
+ // imap folder, don't analyze for spam, because
+ // it's not ours to analyze
+ //
+
+ bool filterForJunk = true;
+ if (serverType.EqualsLiteral("rss") ||
+ (mFlags &
+ (nsMsgFolderFlags::SpecialUse | nsMsgFolderFlags::ImapPublic |
+ nsMsgFolderFlags::Newsgroup | nsMsgFolderFlags::ImapOtherUser) &&
+ !(mFlags & nsMsgFolderFlags::Inbox)))
+ filterForJunk = false;
+
+ spamSettings->GetLevel(&spamLevel);
+ if (!spamLevel) filterForJunk = false;
+
+ /*
+ * We'll use inherited folder properties for the junk trait to override the
+ * standard server-based activation of junk processing. This provides a
+ * hook for extensions to customize the application of junk filtering.
+ * Set inherited property "dobayes.mailnews@mozilla.org#junk" to "true"
+ * to force junk processing, and "false" to skip junk processing.
+ */
+
+ nsAutoCString junkEnableOverride;
+ GetInheritedStringProperty("dobayes.mailnews@mozilla.org#junk",
+ junkEnableOverride);
+ if (junkEnableOverride.EqualsLiteral("true"))
+ filterForJunk = true;
+ else if (junkEnableOverride.EqualsLiteral("false"))
+ filterForJunk = false;
+
+ bool userHasClassified = false;
+ // if the user has not classified any messages yet, then we shouldn't bother
+ // running the junk mail controls. This creates a better first use experience.
+ // See Bug #250084.
+ junkMailPlugin->GetUserHasClassified(&userHasClassified);
+ if (!userHasClassified) filterForJunk = false;
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Will run Spam filter: %s", filterForJunk ? "true" : "false"));
+
+ nsCOMPtr<nsIMsgDatabase> database(mDatabase);
+ rv = GetMsgDatabase(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check if trait processing needed
+
+ nsCOMPtr<nsIMsgTraitService> traitService(
+ do_GetService("@mozilla.org/msg-trait-service;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<uint32_t> proIndices;
+ rv = traitService->GetEnabledProIndices(proIndices);
+ bool filterForOther = false;
+ // We just skip this on failure, since it is rarely used.
+ if (NS_SUCCEEDED(rv)) {
+ for (uint32_t i = 0; i < proIndices.Length(); ++i) {
+ // The trait service determines which traits are globally enabled or
+ // disabled. If a trait is enabled, it can still be made inactive
+ // on a particular folder using an inherited property. To do that,
+ // set "dobayes." + trait proID as an inherited folder property with
+ // the string value "false"
+ //
+ // If any non-junk traits are active on the folder, then the bayes
+ // processing will calculate probabilities for all enabled traits.
+
+ if (proIndices[i] != nsIJunkMailPlugin::JUNK_TRAIT) {
+ filterForOther = true;
+ nsAutoCString traitId;
+ nsAutoCString property("dobayes.");
+ traitService->GetId(proIndices[i], traitId);
+ property.Append(traitId);
+ nsAutoCString isEnabledOnFolder;
+ GetInheritedStringProperty(property.get(), isEnabledOnFolder);
+ if (isEnabledOnFolder.EqualsLiteral("false")) filterForOther = false;
+ // We might have to allow a "true" override in the future, but
+ // for now there is no way for that to affect the processing
+ break;
+ }
+ }
+ }
+
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Will run Trait classification: %s", filterForOther ? "true" : "false"));
+ // clang-format on
+
+ // Do we need to apply message filters?
+ bool filterPostPlugin = false; // Do we have a post-analysis filter?
+ nsCOMPtr<nsIMsgFilterList> filterList;
+ GetFilterList(aMsgWindow, getter_AddRefs(filterList));
+ if (filterList) {
+ uint32_t filterCount = 0;
+ filterList->GetFilterCount(&filterCount);
+ for (uint32_t index = 0; index < filterCount && !filterPostPlugin;
+ ++index) {
+ nsCOMPtr<nsIMsgFilter> filter;
+ filterList->GetFilterAt(index, getter_AddRefs(filter));
+ if (!filter) continue;
+ nsMsgFilterTypeType filterType;
+ filter->GetFilterType(&filterType);
+ if (!(filterType & nsMsgFilterType::PostPlugin)) continue;
+ bool enabled = false;
+ filter->GetEnabled(&enabled);
+ if (!enabled) continue;
+ filterPostPlugin = true;
+ }
+ }
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Will run Post-classification filters: %s",
+ filterPostPlugin ? "true" : "false"));
+
+ // If there is nothing to do, leave now but let NotifyHdrsNotBeingClassified
+ // generate the msgsClassified notification for all newly added messages as
+ // tracked by the NotReportedClassified processing flag.
+ if (!filterForOther && !filterForJunk && !filterPostPlugin) {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("No filters need to be run"));
+ NotifyHdrsNotBeingClassified();
+ return NS_OK;
+ }
+
+ // get the list of new messages
+ //
+ nsTArray<nsMsgKey> newKeys;
+ rv = database->GetNewList(newKeys);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Running filters on %" PRIu32 " new messages",
+ (uint32_t)newKeys.Length()));
+
+ nsTArray<nsMsgKey> newMessageKeys;
+ // Start from m_saveNewMsgs (and clear its current state). m_saveNewMsgs is
+ // where we stash the list of new messages when we are told to clear the list
+ // of new messages by the UI (which purges the list from the nsMsgDatabase).
+ newMessageKeys.SwapElements(m_saveNewMsgs);
+ newMessageKeys.AppendElements(newKeys);
+
+ // build up list of keys to classify
+ nsTArray<nsMsgKey> classifyMsgKeys;
+ nsCString uri;
+
+ uint32_t numNewMessages = newMessageKeys.Length();
+ for (uint32_t i = 0; i < numNewMessages; ++i) {
+ nsMsgKey msgKey = newMessageKeys[i];
+ // clang-format off
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Running filters on message with key %" PRIu32, msgKeyToInt(msgKey)));
+ // clang-format on
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = database->GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr));
+ if (!NS_SUCCEEDED(rv)) continue;
+ // per-message junk tests.
+ bool filterMessageForJunk = false;
+ while (filterForJunk) // we'll break from this at the end
+ {
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("Spam filter"));
+ nsCString junkScore;
+ msgHdr->GetStringProperty("junkscore", junkScore);
+ if (!junkScore.IsEmpty()) {
+ // ignore already scored messages.
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Message already scored previously, skipping"));
+ break;
+ }
+
+ bool whiteListMessage = false;
+ spamSettings->CheckWhiteList(msgHdr, &whiteListMessage);
+ if (whiteListMessage) {
+ // mark this msg as non-junk, because we whitelisted it.
+ nsAutoCString msgJunkScore;
+ msgJunkScore.AppendInt(nsIJunkMailPlugin::IS_HAM_SCORE);
+ database->SetStringProperty(msgKey, "junkscore", msgJunkScore);
+ database->SetStringProperty(msgKey, "junkscoreorigin", "whitelist"_ns);
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Message whitelisted, skipping"));
+ break; // skip this msg since it's in the white list
+ }
+ filterMessageForJunk = true;
+
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("Message is to be classified"));
+ OrProcessingFlags(msgKey, nsMsgProcessingFlags::ClassifyJunk);
+ // Since we are junk processing, we want to defer the msgsClassified
+ // notification until the junk classification has occurred. The event
+ // is sufficiently reliable that we know this will be handled in
+ // OnMessageClassified at the end of the batch. We clear the
+ // NotReportedClassified flag since we know the message is in good hands.
+ AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::NotReportedClassified);
+ break;
+ }
+
+ uint32_t processingFlags;
+ GetProcessingFlags(msgKey, &processingFlags);
+
+ bool filterMessageForOther = false;
+ // trait processing
+ if (!(processingFlags & nsMsgProcessingFlags::TraitsDone)) {
+ // don't do trait processing on this message again
+ OrProcessingFlags(msgKey, nsMsgProcessingFlags::TraitsDone);
+ if (filterForOther) {
+ filterMessageForOther = true;
+ OrProcessingFlags(msgKey, nsMsgProcessingFlags::ClassifyTraits);
+ }
+ }
+
+ if (filterMessageForJunk || filterMessageForOther)
+ classifyMsgKeys.AppendElement(newMessageKeys[i]);
+
+ // Set messages to filter post-bayes.
+ // Have we already filtered this message?
+ if (!(processingFlags & nsMsgProcessingFlags::FiltersDone)) {
+ if (filterPostPlugin) {
+ // Don't do filters on this message again.
+ // (Only set this if we are actually filtering since this is
+ // tantamount to a memory leak.)
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Filters done on this message"));
+ OrProcessingFlags(msgKey, nsMsgProcessingFlags::FiltersDone);
+ mPostBayesMessagesToFilter.AppendElement(msgHdr);
+ }
+ }
+ }
+
+ NotifyHdrsNotBeingClassified();
+ // If there weren't any new messages, just return.
+ if (newMessageKeys.IsEmpty()) return NS_OK;
+
+ // If we do not need to do any work, leave.
+ // (We needed to get the list of new messages so we could get their headers so
+ // we can send notifications about them here.)
+
+ if (!classifyMsgKeys.IsEmpty()) {
+ // Remember what classifications are the source of this decision for when
+ // we perform the notification in OnMessageClassified at the conclusion of
+ // classification.
+ mBayesJunkClassifying = filterForJunk;
+ mBayesTraitClassifying = filterForOther;
+
+ uint32_t numMessagesToClassify = classifyMsgKeys.Length();
+ MOZ_LOG(FILTERLOGMODULE, LogLevel::Info,
+ ("Running Spam classification on %" PRIu32 " messages",
+ numMessagesToClassify));
+
+ nsTArray<nsCString> messageURIs(numMessagesToClassify);
+ for (uint32_t msgIndex = 0; msgIndex < numMessagesToClassify; ++msgIndex) {
+ nsCString tmpStr;
+ rv = GenerateMessageURI(classifyMsgKeys[msgIndex], tmpStr);
+ if (NS_SUCCEEDED(rv)) {
+ messageURIs.AppendElement(tmpStr);
+ } else {
+ NS_WARNING(
+ "nsMsgDBFolder::CallFilterPlugins(): could not"
+ " generate URI for message");
+ }
+ }
+ // filterMsgs
+ *aFiltersRun = true;
+
+ // Already got proIndices, but need antiIndices too.
+ nsTArray<uint32_t> antiIndices;
+ rv = traitService->GetEnabledAntiIndices(antiIndices);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = junkMailPlugin->ClassifyTraitsInMessages(
+ messageURIs, proIndices, antiIndices, this, aMsgWindow, this);
+ } else if (filterPostPlugin) {
+ // Nothing to classify, so need to end batch ourselves. We do this so that
+ // post analysis filters will run consistently on a folder, even if
+ // disabled junk processing, which could be dynamic through whitelisting,
+ // makes the bayes analysis unnecessary.
+ OnMessageClassified(EmptyCString(), nsIJunkMailPlugin::UNCLASSIFIED, 0);
+ }
+
+ return rv;
+}
+
+/**
+ * Adds the messages in the NotReportedClassified mProcessing set to the
+ * (possibly empty) array of msgHdrsNotBeingClassified, and send the
+ * nsIMsgFolderNotificationService notification.
+ */
+nsresult nsMsgDBFolder::NotifyHdrsNotBeingClassified() {
+ if (mProcessingFlag[5].keys) {
+ nsTArray<nsMsgKey> keys;
+ mProcessingFlag[5].keys->ToMsgKeyArray(keys);
+ if (keys.Length()) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsNotBeingClassified;
+ rv = MsgGetHeadersFromKeys(mDatabase, keys, msgHdrsNotBeingClassified);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Since we know we've handled all the NotReportedClassified messages,
+ // we clear the set by deleting and recreating it.
+ delete mProcessingFlag[5].keys;
+ mProcessingFlag[5].keys = nsMsgKeySetU::Create();
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier)
+ notifier->NotifyMsgsClassified(msgHdrsNotBeingClassified,
+ // no classification is being performed
+ false, false);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetLastMessageLoaded(nsMsgKey* aMsgKey) {
+ NS_ENSURE_ARG_POINTER(aMsgKey);
+ *aMsgKey = mLastMessageLoaded;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetLastMessageLoaded(nsMsgKey aMsgKey) {
+ mLastMessageLoaded = aMsgKey;
+ return NS_OK;
+}
+
+// Returns true if: a) there is no need to prompt or b) the user is already
+// logged in or c) the user logged in successfully.
+bool nsMsgDBFolder::PromptForMasterPasswordIfNecessary() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool userNeedsToAuthenticate = false;
+ // if we're PasswordProtectLocalCache, then we need to find out if the server
+ // is authenticated.
+ (void)accountManager->GetUserNeedsToAuthenticate(&userNeedsToAuthenticate);
+ if (!userNeedsToAuthenticate) return true;
+
+ // Do we have a master password?
+ nsCOMPtr<nsIPK11TokenDB> tokenDB =
+ do_GetService(NS_PK11TOKENDB_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsCOMPtr<nsIPK11Token> token;
+ rv = tokenDB->GetInternalKeyToken(getter_AddRefs(token));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool result;
+ rv = token->CheckPassword(EmptyCString(), &result);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (result) {
+ // We don't have a master password, so this function isn't supported,
+ // therefore just tell account manager we've authenticated and return true.
+ accountManager->SetUserNeedsToAuthenticate(false);
+ return true;
+ }
+
+ // We have a master password, so try and login to the slot.
+ rv = token->Login(false);
+ if (NS_FAILED(rv))
+ // Login failed, so we didn't get a password (e.g. prompt cancelled).
+ return false;
+
+ // Double-check that we are now logged in
+ rv = token->IsLoggedIn(&result);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ accountManager->SetUserNeedsToAuthenticate(!result);
+ return result;
+}
+
+// this gets called after the last junk mail classification has run.
+nsresult nsMsgDBFolder::PerformBiffNotifications(void) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t numBiffMsgs = 0;
+ nsCOMPtr<nsIMsgFolder> root;
+ rv = GetRootFolder(getter_AddRefs(root));
+ root->GetNumNewMessages(true, &numBiffMsgs);
+ if (numBiffMsgs > 0) {
+ server->SetPerformingBiff(true);
+ SetBiffState(nsIMsgFolder::nsMsgBiffState_NewMail);
+ server->SetPerformingBiff(false);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::initializeStrings() {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/messenger.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bundle->GetStringFromName("inboxFolderName", kLocalizedInboxName);
+ bundle->GetStringFromName("trashFolderName", kLocalizedTrashName);
+ bundle->GetStringFromName("sentFolderName", kLocalizedSentName);
+ bundle->GetStringFromName("draftsFolderName", kLocalizedDraftsName);
+ bundle->GetStringFromName("templatesFolderName", kLocalizedTemplatesName);
+ bundle->GetStringFromName("junkFolderName", kLocalizedJunkName);
+ bundle->GetStringFromName("outboxFolderName", kLocalizedUnsentName);
+ bundle->GetStringFromName("archivesFolderName", kLocalizedArchivesName);
+
+ nsCOMPtr<nsIStringBundle> brandBundle;
+ rv = bundleService->CreateBundle("chrome://branding/locale/brand.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bundle->GetStringFromName("brandShortName", kLocalizedBrandShortName);
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::createCollationKeyGenerator() {
+ if (!gCollationKeyGenerator) {
+ auto result = mozilla::intl::LocaleService::TryCreateComponent<Collator>();
+ if (result.isErr()) {
+ NS_WARNING("Could not create mozilla::intl::Collation.");
+ return NS_ERROR_FAILURE;
+ }
+
+ gCollationKeyGenerator = result.unwrap();
+
+ // Sort in a case-insensitive way, where "base" letters are considered
+ // equal, e.g: a = á, a = A, a ≠ b.
+ Collator::Options options{};
+ options.sensitivity = Collator::Sensitivity::Base;
+ auto optResult = gCollationKeyGenerator->SetOptions(options);
+
+ if (optResult.isErr()) {
+ NS_WARNING("Could not configure the mozilla::intl::Collation.");
+ gCollationKeyGenerator = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::Init(const nsACString& uri) {
+ mURI = uri;
+ return CreateBaseMessageURI(uri);
+}
+
+nsresult nsMsgDBFolder::CreateBaseMessageURI(const nsACString& aURI) {
+ // Each folder needs to implement this.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetURI(nsACString& name) {
+ name = mURI;
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+#if 0
+typedef bool
+(*nsArrayFilter)(nsISupports* element, void* data);
+#endif
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) {
+ folders.ClearAndRetainStorage();
+ folders.SetCapacity(mSubFolders.Length());
+ for (nsIMsgFolder* f : mSubFolders) {
+ folders.AppendElement(f);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::FindSubFolder(const nsACString& aEscapedSubFolderName,
+ nsIMsgFolder** aFolder) {
+ // XXX use necko here
+ nsAutoCString uri;
+ uri.Append(mURI);
+ uri.Append('/');
+ uri.Append(aEscapedSubFolderName);
+
+ return GetOrCreateFolder(uri, aFolder);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetHasSubFolders(bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = mSubFolders.Count() > 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetNumSubFolders(uint32_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = mSubFolders.Count();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AddFolderListener(nsIFolderListener* listener) {
+ NS_ENSURE_ARG_POINTER(listener);
+ mListeners.AppendElement(listener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RemoveFolderListener(nsIFolderListener* listener) {
+ NS_ENSURE_ARG_POINTER(listener);
+ mListeners.RemoveElement(listener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetParent(nsIMsgFolder* aParent) {
+ mParent = do_GetWeakReference(aParent);
+ if (aParent) {
+ nsresult rv;
+ // servers do not have parents, so we must not be a server
+ mIsServer = false;
+ mIsServerIsValid = true;
+
+ // also set the server itself while we're here.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = aParent->GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) mServer = do_GetWeakReference(server);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetParent(nsIMsgFolder** aParent) {
+ NS_ENSURE_ARG_POINTER(aParent);
+ nsCOMPtr<nsIMsgFolder> parent = do_QueryReferent(mParent);
+ parent.forget(aParent);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetMessages(nsIMsgEnumerator** result) {
+ NS_ENSURE_ARG_POINTER(result);
+ // Make sure mDatabase is set.
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mDatabase->EnumerateMessages(result);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::UpdateFolder(nsIMsgWindow*) { return NS_OK; }
+
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsMsgDBFolder::GetFolderURL(nsACString& url) {
+ url.Assign(EmptyCString());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetServer(nsIMsgIncomingServer** aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ nsresult rv;
+ // short circuit the server if we have it.
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(mServer, &rv);
+ if (NS_FAILED(rv)) {
+ // try again after parsing the URI
+ rv = parseURI(true);
+ server = do_QueryReferent(mServer);
+ }
+ server.forget(aServer);
+ return *aServer ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgDBFolder::parseURI(bool needServer) {
+ nsresult rv;
+ nsCOMPtr<nsIURL> url;
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(mURI)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // empty path tells us it's a server.
+ if (!mIsServerIsValid) {
+ nsAutoCString path;
+ rv = url->GetPathQueryRef(path);
+ if (NS_SUCCEEDED(rv)) mIsServer = path.EqualsLiteral("/");
+ mIsServerIsValid = true;
+ }
+
+ // grab the name off the leaf of the server
+ if (mName.IsEmpty()) {
+ // mName:
+ // the name is the trailing directory in the path
+ nsAutoCString fileName;
+ nsAutoCString escapedFileName;
+ url->GetFileName(escapedFileName);
+ if (!escapedFileName.IsEmpty()) {
+ // XXX conversion to unicode here? is fileName in UTF8?
+ // yes, let's say it is in utf8
+ MsgUnescapeString(escapedFileName, 0, fileName);
+ NS_ASSERTION(mozilla::IsUtf8(fileName), "fileName is not in UTF-8");
+ CopyUTF8toUTF16(fileName, mName);
+ }
+ }
+
+ // grab the server by parsing the URI and looking it up
+ // in the account manager...
+ // But avoid this extra work by first asking the parent, if any
+ nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(mServer, &rv);
+ if (NS_FAILED(rv)) {
+ // first try asking the parent instead of the URI
+ nsCOMPtr<nsIMsgFolder> parentMsgFolder;
+ GetParent(getter_AddRefs(parentMsgFolder));
+
+ if (parentMsgFolder)
+ rv = parentMsgFolder->GetServer(getter_AddRefs(server));
+
+ // no parent. do the extra work of asking
+ if (!server && needServer) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverType;
+ GetIncomingServerType(serverType);
+ if (serverType.IsEmpty()) {
+ NS_WARNING("can't determine folder's server type");
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = NS_MutateURI(url).SetScheme(serverType).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ mServer = do_GetWeakReference(server);
+ } /* !mServer */
+
+ // now try to find the local path for this folder
+ if (server) {
+ nsAutoCString newPath;
+ nsAutoCString escapedUrlPath;
+ nsAutoCString urlPath;
+ url->GetFilePath(escapedUrlPath);
+ if (!escapedUrlPath.IsEmpty()) {
+ MsgUnescapeString(escapedUrlPath, 0, urlPath);
+
+ // transform the filepath from the URI, such as
+ // "/folder1/folder2/foldern"
+ // to
+ // "folder1.sbd/folder2.sbd/foldern"
+ // (remove leading / and add .sbd to first n-1 folders)
+ // to be appended onto the server's path
+ bool isNewsFolder = false;
+ nsAutoCString scheme;
+ if (NS_SUCCEEDED(url->GetScheme(scheme))) {
+ isNewsFolder = scheme.EqualsLiteral("news") ||
+ scheme.EqualsLiteral("snews") ||
+ scheme.EqualsLiteral("nntp");
+ }
+ NS_MsgCreatePathStringFromFolderURI(urlPath.get(), newPath, scheme,
+ isNewsFolder);
+ }
+
+ // now append munged path onto server path
+ nsCOMPtr<nsIFile> serverPath;
+ rv = server->GetLocalPath(getter_AddRefs(serverPath));
+ if (NS_FAILED(rv)) return rv;
+
+ if (!mPath && serverPath) {
+ if (!newPath.IsEmpty()) {
+ // I hope this is temporary - Ultimately,
+ // NS_MsgCreatePathStringFromFolderURI will need to be fixed.
+#if defined(XP_WIN)
+ newPath.ReplaceChar('/', '\\');
+#endif
+ rv = serverPath->AppendRelativeNativePath(newPath);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to append to the serverPath");
+ if (NS_FAILED(rv)) {
+ mPath = nullptr;
+ return rv;
+ }
+ }
+ mPath = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mPath->InitWithFile(serverPath);
+ }
+ // URI is completely parsed when we've attempted to get the server
+ mHaveParsedURI = true;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetIsServer(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ // make sure we've parsed the URI
+ if (!mIsServerIsValid) {
+ nsresult rv = parseURI();
+ if (NS_FAILED(rv) || !mIsServerIsValid) return NS_ERROR_FAILURE;
+ }
+
+ *aResult = mIsServer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetNoSelect(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetImapShared(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ return GetFlag(nsMsgFolderFlags::PersonalShared, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanSubscribe(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ // by default, you can't subscribe.
+ // if otherwise, override it.
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanFileMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ // varada - checking folder flag to see if it is the "Unsent Messages"
+ // and if so return FALSE
+ if (mFlags & (nsMsgFolderFlags::Queue | nsMsgFolderFlags::Virtual)) {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ if (NS_FAILED(rv)) return rv;
+
+ // by default, you can't file messages into servers, only to folders
+ // if otherwise, override it.
+ *aResult = !isServer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanDeleteMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanCreateSubfolders(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ // Checking folder flag to see if it is the "Unsent Messages"
+ // or a virtual folder, and if so return FALSE
+ if (mFlags & (nsMsgFolderFlags::Queue | nsMsgFolderFlags::Virtual)) {
+ *aResult = false;
+ return NS_OK;
+ }
+
+ // by default, you can create subfolders on server and folders
+ // if otherwise, override it.
+ *aResult = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanRename(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ if (NS_FAILED(rv)) return rv;
+ // by default, you can't rename servers, only folders
+ // if otherwise, override it.
+ //
+ // check if the folder is a special folder
+ // (Trash, Drafts, Unsent Messages, Inbox, Sent, Templates, Junk, Archives)
+ // if it is, don't allow the user to rename it
+ // (which includes dnd moving it with in the same server)
+ //
+ // this errors on the side of caution. we'll return false a lot
+ // more often if we use flags,
+ // instead of checking if the folder really is being used as a
+ // special folder by looking at the "copies and folders" prefs on the
+ // identities.
+ *aResult = !(isServer || (mFlags & nsMsgFolderFlags::SpecialUse));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetCanCompact(bool* canCompact) {
+ NS_ENSURE_ARG_POINTER(canCompact);
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // servers (root folder) cannot be compacted
+ // virtual search folders cannot be compacted
+ *canCompact = !isServer && !(mFlags & nsMsgFolderFlags::Virtual);
+ // If *canCompact now true and folder is imap, keep *canCompact true and
+ // return; otherwise, when not imap, type of store controls it. E.g., mbox
+ // sets *canCompact true, maildir sets it false.
+ if (*canCompact && !(mFlags & nsMsgFolderFlags::ImapBox)) {
+ // Check if the storage type supports compaction
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ GetMsgStore(getter_AddRefs(msgStore));
+ if (msgStore) msgStore->GetSupportsCompaction(canCompact);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetPrettyName(nsAString& name) {
+ return GetName(name);
+}
+
+static bool nonEnglishApp() {
+ if (nsMsgDBFolder::gIsEnglishApp == -1) {
+ nsAutoCString locale;
+ mozilla::intl::LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale);
+ nsMsgDBFolder::gIsEnglishApp =
+ (locale.EqualsLiteral("en") || StringBeginsWith(locale, "en-"_ns)) ? 1
+ : 0;
+ }
+ return nsMsgDBFolder::gIsEnglishApp ? false : true;
+}
+
+static bool hasTrashName(const nsAString& name) {
+ // Microsoft calls the folder "Deleted". If the application is non-English,
+ // we want to use the localised name instead.
+ return name.LowerCaseEqualsLiteral("trash") ||
+ (name.LowerCaseEqualsLiteral("deleted") && nonEnglishApp());
+}
+
+static bool hasDraftsName(const nsAString& name) {
+ // Some IMAP providers call the folder "Draft". If the application is
+ // non-English, we want to use the localised name instead.
+ return name.LowerCaseEqualsLiteral("drafts") ||
+ (name.LowerCaseEqualsLiteral("draft") && nonEnglishApp());
+}
+
+static bool hasSentName(const nsAString& name) {
+ // Some IMAP providers call the folder for sent messages "Outbox". That IMAP
+ // folder is not related to Thunderbird's local folder for queued messages.
+ // If we find such a folder with the 'SentMail' flag, we can safely localize
+ // its name if the application is non-English.
+ return name.LowerCaseEqualsLiteral("sent") ||
+ (name.LowerCaseEqualsLiteral("outbox") && nonEnglishApp());
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetPrettyName(const nsAString& name) {
+ nsresult rv;
+ // Keep original name.
+ mOriginalName = name;
+
+ // Set pretty name only if special flag is set and if it the default folder
+ // name
+ if (mFlags & nsMsgFolderFlags::Inbox && name.LowerCaseEqualsLiteral("inbox"))
+ rv = SetName(kLocalizedInboxName);
+ else if (mFlags & nsMsgFolderFlags::SentMail && hasSentName(name))
+ rv = SetName(kLocalizedSentName);
+ else if (mFlags & nsMsgFolderFlags::Drafts && hasDraftsName(name))
+ rv = SetName(kLocalizedDraftsName);
+ else if (mFlags & nsMsgFolderFlags::Templates &&
+ name.LowerCaseEqualsLiteral("templates"))
+ rv = SetName(kLocalizedTemplatesName);
+ else if (mFlags & nsMsgFolderFlags::Trash && hasTrashName(name))
+ rv = SetName(kLocalizedTrashName);
+ else if (mFlags & nsMsgFolderFlags::Queue &&
+ name.LowerCaseEqualsLiteral("unsent messages"))
+ rv = SetName(kLocalizedUnsentName);
+ else if (mFlags & nsMsgFolderFlags::Junk &&
+ name.LowerCaseEqualsLiteral("junk"))
+ rv = SetName(kLocalizedJunkName);
+ else if (mFlags & nsMsgFolderFlags::Archive &&
+ name.LowerCaseEqualsLiteral("archives"))
+ rv = SetName(kLocalizedArchivesName);
+ else
+ rv = SetName(name);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetPrettyNameFromOriginal(void) {
+ if (mOriginalName.IsEmpty()) return NS_OK;
+ return SetPrettyName(mOriginalName);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetName(nsAString& name) {
+ nsresult rv;
+ if (!mHaveParsedURI && mName.IsEmpty()) {
+ rv = parseURI();
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ // if it's a server, just forward the call
+ if (mIsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) return server->GetPrettyName(name);
+ }
+
+ name = mName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetName(const nsAString& name) {
+ // override the URI-generated name
+ if (!mName.Equals(name)) {
+ mName = name;
+ // old/new value doesn't matter here
+ NotifyUnicharPropertyChanged(kName, name, name);
+ }
+ return NS_OK;
+}
+
+// For default, just return name
+NS_IMETHODIMP nsMsgDBFolder::GetAbbreviatedName(nsAString& aAbbreviatedName) {
+ return GetName(aAbbreviatedName);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetChildNamed(const nsAString& aName, nsIMsgFolder** aChild) {
+ NS_ENSURE_ARG_POINTER(aChild);
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ GetSubFolders(dummy); // initialize mSubFolders
+ *aChild = nullptr;
+
+ for (nsIMsgFolder* child : mSubFolders) {
+ nsString folderName;
+ nsresult rv = child->GetName(folderName);
+ // case-insensitive compare is probably LCD across OS filesystems
+ if (NS_SUCCEEDED(rv) &&
+ folderName.Equals(aName, nsCaseInsensitiveStringComparator)) {
+ NS_ADDREF(*aChild = child);
+ return NS_OK;
+ }
+ }
+ // don't return NS_OK if we didn't find the folder
+ // see http://bugzilla.mozilla.org/show_bug.cgi?id=210089#c15
+ // and http://bugzilla.mozilla.org/show_bug.cgi?id=210089#c17
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetChildWithURI(const nsACString& uri, bool deep,
+ bool caseInsensitive,
+ nsIMsgFolder** child) {
+ NS_ENSURE_ARG_POINTER(child);
+ // will return nullptr if we can't find it
+ *child = nullptr;
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ nsresult rv = GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* folder : subFolders) {
+ nsCString folderURI;
+ rv = folder->GetURI(folderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool equal =
+ (caseInsensitive
+ ? uri.Equals(folderURI, nsCaseInsensitiveCStringComparator)
+ : uri.Equals(folderURI));
+ if (equal) {
+ NS_ADDREF(*child = folder);
+ return NS_OK;
+ }
+ if (deep) {
+ rv = folder->GetChildWithURI(uri, deep, caseInsensitive, child);
+ if (NS_FAILED(rv)) return rv;
+
+ if (*child) return NS_OK;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetShowDeletedMessages(bool* showDeletedMessages) {
+ NS_ENSURE_ARG_POINTER(showDeletedMessages);
+ *showDeletedMessages = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::DeleteStorage() {
+ ForceDBClosed();
+
+ // Delete the .msf file.
+ // NOTE: this doesn't remove .msf files in subfolders, but
+ // both nsMsgBrkMBoxStore::DeleteFolder() and
+ // nsMsgMaildirStore::DeleteFolder() will remove those .msf files
+ // as a side-effect of deleting the .sbd directory.
+ nsCOMPtr<nsIFile> summaryFile;
+ nsresult rv = GetSummaryFile(getter_AddRefs(summaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool exists = false;
+ summaryFile->Exists(&exists);
+ if (exists) {
+ rv = summaryFile->Remove(false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Ask the msgStore to delete the actual storage (mbox, maildir or whatever
+ // else may be supported in future).
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->DeleteFolder(this);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::DeleteSelf(nsIMsgWindow* msgWindow) {
+ nsCOMPtr<nsIMsgFolder> parent;
+ GetParent(getter_AddRefs(parent));
+ if (!parent) {
+ return NS_ERROR_FAILURE;
+ }
+ return parent->PropagateDelete(this, true);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CreateStorageIfMissing(
+ nsIUrlListener* /* urlListener */) {
+ NS_ASSERTION(false, "needs to be overridden");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::PropagateDelete(nsIMsgFolder* folder,
+ bool deleteStorage) {
+ // first, find the folder we're looking to delete
+ nsresult rv = NS_OK;
+
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> child(mSubFolders[i]);
+ if (folder == child.get()) {
+ // Remove self as parent
+ child->SetParent(nullptr);
+ // maybe delete disk storage for it, and its subfolders
+ rv = child->RecursiveDelete(deleteStorage);
+ if (NS_SUCCEEDED(rv)) {
+ // Remove from list of subfolders.
+ mSubFolders.RemoveObjectAt(i);
+ NotifyFolderRemoved(child);
+ break;
+ } else // setting parent back if we failed
+ child->SetParent(this);
+ } else
+ rv = child->PropagateDelete(folder, deleteStorage);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RecursiveDelete(bool deleteStorage) {
+ // If deleteStorage is true, recursively deletes disk storage for this folder
+ // and all its subfolders.
+ // Regardless of deleteStorage, always unlinks them from the children lists
+ // and frees memory for the subfolders but NOT for _this_
+ // and does not remove _this_ from the parent's list of children.
+
+ nsCOMPtr<nsIFile> dbPath;
+ // first remove the deleted folder from the folder cache;
+ nsresult rv = GetFolderCacheKey(getter_AddRefs(dbPath));
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ nsCOMPtr<nsIMsgFolderCache> folderCache;
+ rv = accountMgr->GetFolderCache(getter_AddRefs(folderCache));
+ if (NS_SUCCEEDED(rv) && folderCache) {
+ nsCString persistentPath;
+ rv = dbPath->GetPersistentDescriptor(persistentPath);
+ if (NS_SUCCEEDED(rv)) folderCache->RemoveElement(persistentPath);
+ }
+ }
+
+ int32_t count = mSubFolders.Count();
+ while (count > 0) {
+ nsCOMPtr<nsIMsgFolder> child(mSubFolders[0]);
+ child->SetParent(nullptr);
+ rv = child->RecursiveDelete(deleteStorage);
+ if (NS_SUCCEEDED(rv)) {
+ // unlink it from this child's list
+ mSubFolders.RemoveObjectAt(0);
+ NotifyFolderRemoved(child);
+ } else {
+ // setting parent back if we failed for some reason
+ child->SetParent(this);
+ break;
+ }
+
+ count--;
+ }
+
+ // now delete the disk storage for _this_
+ if (deleteStorage && NS_SUCCEEDED(rv)) {
+ // All delete commands use deleteStorage = true, and local moves use false.
+ // IMAP moves use true, leaving this here in the hope that bug 439108
+ // works out.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderDeleted(this);
+ rv = DeleteStorage();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CreateSubfolder(const nsAString& folderName,
+ nsIMsgWindow* msgWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AddSubfolder(const nsAString& name,
+ nsIMsgFolder** child) {
+ NS_ENSURE_ARG_POINTER(child);
+
+ int32_t flags = 0;
+ nsresult rv;
+
+ nsAutoCString uri(mURI);
+ uri.Append('/');
+
+ // URI should use UTF-8
+ // (see RFC2396 Uniform Resource Identifiers (URI): Generic Syntax)
+ nsAutoCString escapedName;
+ rv = NS_MsgEscapeEncodeURLPath(name, escapedName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensure the containing (.sbd) dir exists.
+ nsCOMPtr<nsIFile> path;
+ rv = CreateDirectoryForFolder(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // fix for #192780
+ // if this is the root folder
+ // make sure the the special folders
+ // have the right uri.
+ // on disk, host\INBOX should be a folder with the uri
+ // mailbox://user@host/Inbox" as mailbox://user@host/Inbox !=
+ // mailbox://user@host/INBOX
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = GetRootFolder(getter_AddRefs(rootFolder));
+ if (NS_SUCCEEDED(rv) && rootFolder &&
+ (rootFolder.get() == (nsIMsgFolder*)this)) {
+ if (escapedName.LowerCaseEqualsLiteral("inbox"))
+ uri += "Inbox";
+ else if (escapedName.LowerCaseEqualsLiteral("unsent%20messages"))
+ uri += "Unsent%20Messages";
+ else if (escapedName.LowerCaseEqualsLiteral("drafts"))
+ uri += "Drafts";
+ else if (escapedName.LowerCaseEqualsLiteral("trash"))
+ uri += "Trash";
+ else if (escapedName.LowerCaseEqualsLiteral("sent"))
+ uri += "Sent";
+ else if (escapedName.LowerCaseEqualsLiteral("templates"))
+ uri += "Templates";
+ else if (escapedName.LowerCaseEqualsLiteral("archives"))
+ uri += "Archives";
+ else
+ uri += escapedName.get();
+ } else
+ uri += escapedName.get();
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = GetChildWithURI(uri, false /*deep*/, true /*case Insensitive*/,
+ getter_AddRefs(msgFolder));
+ if (NS_SUCCEEDED(rv) && msgFolder) return NS_MSG_FOLDER_EXISTS;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(uri, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ folder->GetFlags((uint32_t*)&flags);
+ flags |= nsMsgFolderFlags::Mail;
+ folder->SetParent(this);
+
+ bool isServer;
+ rv = GetIsServer(&isServer);
+
+ // Only set these if these are top level children.
+ if (NS_SUCCEEDED(rv) && isServer) {
+ if (name.LowerCaseEqualsLiteral("inbox")) {
+ flags |= nsMsgFolderFlags::Inbox;
+ SetBiffState(nsIMsgFolder::nsMsgBiffState_Unknown);
+ } else if (name.LowerCaseEqualsLiteral("trash"))
+ flags |= nsMsgFolderFlags::Trash;
+ else if (name.LowerCaseEqualsLiteral("unsent messages") ||
+ name.LowerCaseEqualsLiteral("outbox"))
+ flags |= nsMsgFolderFlags::Queue;
+ }
+
+ folder->SetFlags(flags);
+
+ if (folder) mSubFolders.AppendObject(folder);
+
+ folder.forget(child);
+ // at this point we must be ok and we don't want to return failure in case
+ // GetIsServer failed.
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CompactAll(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::EmptyTrash(nsIUrlListener* aListener) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsMsgDBFolder::CheckIfFolderExists(const nsAString& newFolderName,
+ nsIMsgFolder* parentFolder,
+ nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG_POINTER(parentFolder);
+ nsTArray<RefPtr<nsIMsgFolder>> subFolders;
+ nsresult rv = parentFolder->GetSubFolders(subFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (nsIMsgFolder* msgFolder : subFolders) {
+ nsString folderName;
+
+ msgFolder->GetName(folderName);
+ if (folderName.Equals(newFolderName, nsCaseInsensitiveStringComparator)) {
+ ThrowAlertMsg("folderExists", msgWindow);
+ return NS_MSG_FOLDER_EXISTS;
+ }
+ }
+ return NS_OK;
+}
+
+bool nsMsgDBFolder::ConfirmAutoFolderRename(nsIMsgWindow* msgWindow,
+ const nsString& aOldName,
+ const nsString& aNewName) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ nsString folderName;
+ GetName(folderName);
+ AutoTArray<nsString, 3> formatStrings = {aOldName, folderName, aNewName};
+
+ nsString confirmString;
+ rv = bundle->FormatStringFromName("confirmDuplicateFolderRename",
+ formatStrings, confirmString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ bool confirmed = false;
+ rv = ThrowConfirmationPrompt(msgWindow, confirmString, &confirmed);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ return confirmed;
+}
+
+nsresult nsMsgDBFolder::AddDirectorySeparator(nsIFile* path) {
+ nsAutoString leafName;
+ path->GetLeafName(leafName);
+ leafName.AppendLiteral(FOLDER_SUFFIX);
+ return path->SetLeafName(leafName);
+}
+
+/* Finds the subdirectory associated with this folder. That is if the path is
+ c:\Inbox, it will return c:\Inbox.sbd if it succeeds. If that path doesn't
+ currently exist then it will create it. Path is strictly an out parameter.
+ */
+nsresult nsMsgDBFolder::CreateDirectoryForFolder(nsIFile** resultFile) {
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isServer;
+ rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isServer) {
+ // Server dir doesn't have .sbd suffix.
+ // Ensure it exists and is a directory.
+ bool pathExists;
+ path->Exists(&pathExists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!pathExists) {
+ rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ bool isDir;
+ rv = path->IsDirectory(&isDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDir) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+ path.forget(resultFile);
+ return NS_OK;
+ }
+
+ // Append .sbd suffix.
+ rv = AddDirectorySeparator(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Already exists?
+ bool exists;
+ rv = path->Exists(&exists);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (exists) {
+ bool isDir;
+ rv = path->IsDirectory(&isDir);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!isDir) {
+ // Uhoh. Not the dir we were expecting!
+ return NS_MSG_COULD_NOT_CREATE_DIRECTORY;
+ }
+ // Already been created.
+ path.forget(resultFile);
+ return NS_OK;
+ }
+
+ // Need to create it.
+ rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ NS_ENSURE_SUCCESS(rv, rv);
+ path.forget(resultFile);
+ return NS_OK;
+}
+
+/* Finds the backup directory associated with this folder, stored on the temp
+ drive. If that path doesn't currently exist then it will create it. Path is
+ strictly an out parameter.
+ */
+nsresult nsMsgDBFolder::CreateBackupDirectory(nsIFile** resultFile) {
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = path->Append(u"MozillaMailnews"_ns);
+ bool pathIsDirectory;
+ path->IsDirectory(&pathIsDirectory);
+
+ // If that doesn't exist, then we have to create this directory
+ if (!pathIsDirectory) {
+ bool pathExists;
+ path->Exists(&pathExists);
+ // If for some reason there's a file with the directory separator
+ // then we are going to fail.
+ rv = pathExists ? NS_MSG_COULD_NOT_CREATE_DIRECTORY
+ : path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ }
+ if (NS_SUCCEEDED(rv)) path.forget(resultFile);
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetBackupSummaryFile(nsIFile** aBackupFile,
+ const nsACString& newName) {
+ nsCOMPtr<nsIFile> backupDir;
+ nsresult rv = CreateBackupDirectory(getter_AddRefs(backupDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We use a dummy message folder file so we can use
+ // GetSummaryFileLocation to get the db file name
+ nsCOMPtr<nsIFile> backupDBDummyFolder;
+ rv = CreateBackupDirectory(getter_AddRefs(backupDBDummyFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!newName.IsEmpty()) {
+ rv = backupDBDummyFolder->AppendNative(newName);
+ } else // if newName is null, use the folder name
+ {
+ nsCOMPtr<nsIFile> folderPath;
+ rv = GetFilePath(getter_AddRefs(folderPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString folderName;
+ rv = folderPath->GetNativeLeafName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = backupDBDummyFolder->AppendNative(folderName);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> backupDBFile;
+ rv =
+ GetSummaryFileLocation(backupDBDummyFolder, getter_AddRefs(backupDBFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ backupDBFile.forget(aBackupFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::Rename(const nsAString& aNewName,
+ nsIMsgWindow* msgWindow) {
+ nsCOMPtr<nsIFile> oldPathFile;
+ nsresult rv = GetFilePath(getter_AddRefs(oldPathFile));
+ if (NS_FAILED(rv)) return rv;
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ rv = GetParent(getter_AddRefs(parentFolder));
+ if (!parentFolder) return NS_ERROR_FAILURE;
+ nsCOMPtr<nsISupports> parentSupport = do_QueryInterface(parentFolder);
+ nsCOMPtr<nsIFile> oldSummaryFile;
+ rv = GetSummaryFileLocation(oldPathFile, getter_AddRefs(oldSummaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> dirFile;
+ int32_t count = mSubFolders.Count();
+
+ if (count > 0) rv = CreateDirectoryForFolder(getter_AddRefs(dirFile));
+
+ nsAutoString newDiskName(aNewName);
+ NS_MsgHashIfNecessary(newDiskName);
+
+ if (mName.Equals(aNewName, nsCaseInsensitiveStringComparator)) {
+ rv = ThrowAlertMsg("folderExists", msgWindow);
+ return NS_MSG_FOLDER_EXISTS;
+ } else {
+ nsCOMPtr<nsIFile> parentPathFile;
+ parentFolder->GetFilePath(getter_AddRefs(parentPathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool isDirectory = false;
+ parentPathFile->IsDirectory(&isDirectory);
+ if (!isDirectory) AddDirectorySeparator(parentPathFile);
+
+ rv = CheckIfFolderExists(aNewName, parentFolder, msgWindow);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ ForceDBClosed();
+
+ // Save of dir name before appending .msf
+ nsAutoString newNameDirStr(newDiskName);
+
+ if (!(mFlags & nsMsgFolderFlags::Virtual))
+ rv = oldPathFile->MoveTo(nullptr, newDiskName);
+ if (NS_SUCCEEDED(rv)) {
+ newDiskName.AppendLiteral(SUMMARY_SUFFIX);
+ oldSummaryFile->MoveTo(nullptr, newDiskName);
+ } else {
+ ThrowAlertMsg("folderRenameFailed", msgWindow);
+ return rv;
+ }
+
+ if (NS_SUCCEEDED(rv) && count > 0) {
+ // rename "*.sbd" directory
+ newNameDirStr.AppendLiteral(FOLDER_SUFFIX);
+ dirFile->MoveTo(nullptr, newNameDirStr);
+ }
+
+ nsCOMPtr<nsIMsgFolder> newFolder;
+ if (parentSupport) {
+ rv = parentFolder->AddSubfolder(aNewName, getter_AddRefs(newFolder));
+ if (newFolder) {
+ newFolder->SetPrettyName(EmptyString());
+ newFolder->SetPrettyName(aNewName);
+ newFolder->SetFlags(mFlags);
+ bool changed = false;
+ MatchOrChangeFilterDestination(newFolder, true /*case-insensitive*/,
+ &changed);
+ if (changed) AlertFilterChanged(msgWindow);
+
+ if (count > 0) newFolder->RenameSubFolders(msgWindow, this);
+
+ if (parentFolder) {
+ SetParent(nullptr);
+ parentFolder->PropagateDelete(this, false);
+ parentFolder->NotifyFolderAdded(newFolder);
+ }
+ newFolder->NotifyFolderEvent(kRenameCompleted);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RenameSubFolders(nsIMsgWindow* msgWindow,
+ nsIMsgFolder* oldFolder) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ContainsChildNamed(const nsAString& name,
+ bool* containsChild) {
+ NS_ENSURE_ARG_POINTER(containsChild);
+ nsCOMPtr<nsIMsgFolder> child;
+ GetChildNamed(name, getter_AddRefs(child));
+ *containsChild = child != nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::IsAncestorOf(nsIMsgFolder* child,
+ bool* isAncestor) {
+ NS_ENSURE_ARG_POINTER(isAncestor);
+ nsresult rv = NS_OK;
+
+ int32_t count = mSubFolders.Count();
+
+ for (int32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> folder(mSubFolders[i]);
+ if (folder.get() == child)
+ *isAncestor = true;
+ else
+ folder->IsAncestorOf(child, isAncestor);
+
+ if (*isAncestor) return NS_OK;
+ }
+ *isAncestor = false;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GenerateUniqueSubfolderName(
+ const nsAString& prefix, nsIMsgFolder* otherFolder, nsAString& name) {
+ /* only try 256 times */
+ for (int count = 0; count < 256; count++) {
+ nsAutoString uniqueName;
+ uniqueName.Assign(prefix);
+ if (count > 0) {
+ uniqueName.AppendInt(count);
+ }
+ bool containsChild;
+ bool otherContainsChild = false;
+ ContainsChildNamed(uniqueName, &containsChild);
+ if (otherFolder)
+ otherFolder->ContainsChildNamed(uniqueName, &otherContainsChild);
+
+ if (!containsChild && !otherContainsChild) {
+ name = uniqueName;
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::UpdateSummaryTotals(bool force) {
+ if (!mNotifyCountChanges) return NS_OK;
+
+ int32_t oldUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages;
+ int32_t oldTotalMessages = mNumTotalMessages + mNumPendingTotalMessages;
+ // We need to read this info from the database
+ nsresult rv = ReadDBFolderInfo(force);
+
+ if (NS_SUCCEEDED(rv)) {
+ int32_t newUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages;
+ int32_t newTotalMessages = mNumTotalMessages + mNumPendingTotalMessages;
+
+ // Need to notify listeners that total count changed.
+ if (oldTotalMessages != newTotalMessages)
+ NotifyIntPropertyChanged(kTotalMessages, oldTotalMessages,
+ newTotalMessages);
+
+ if (oldUnreadMessages != newUnreadMessages)
+ NotifyIntPropertyChanged(kTotalUnreadMessages, oldUnreadMessages,
+ newUnreadMessages);
+
+ FlushToFolderCache();
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SummaryChanged() {
+ UpdateSummaryTotals(false);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNumUnread(bool deep, int32_t* numUnread) {
+ NS_ENSURE_ARG_POINTER(numUnread);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t total = isServer ? 0 : mNumUnreadMessages + mNumPendingUnreadMessages;
+
+ if (deep) {
+ if (total < 0) // deep search never returns negative counts
+ total = 0;
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> folder(mSubFolders[i]);
+ int32_t num;
+ uint32_t folderFlags;
+ folder->GetFlags(&folderFlags);
+ if (!(folderFlags & nsMsgFolderFlags::Virtual)) {
+ folder->GetNumUnread(deep, &num);
+ total += num;
+ }
+ }
+ }
+ *numUnread = total;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetTotalMessages(bool deep,
+ int32_t* totalMessages) {
+ NS_ENSURE_ARG_POINTER(totalMessages);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t total = isServer ? 0 : mNumTotalMessages + mNumPendingTotalMessages;
+
+ if (deep) {
+ if (total < 0) // deep search never returns negative counts
+ total = 0;
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> folder(mSubFolders[i]);
+ int32_t num;
+ uint32_t folderFlags;
+ folder->GetFlags(&folderFlags);
+ if (!(folderFlags & nsMsgFolderFlags::Virtual)) {
+ folder->GetTotalMessages(deep, &num);
+ total += num;
+ }
+ }
+ }
+ *totalMessages = total;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNumPendingUnread(int32_t* aPendingUnread) {
+ *aPendingUnread = mNumPendingUnreadMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNumPendingTotalMessages(
+ int32_t* aPendingTotal) {
+ *aPendingTotal = mNumPendingTotalMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ChangeNumPendingUnread(int32_t delta) {
+ if (delta) {
+ int32_t oldUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages;
+ mNumPendingUnreadMessages += delta;
+ int32_t newUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages;
+ NS_ASSERTION(newUnreadMessages >= 0,
+ "shouldn't have negative unread message count");
+ if (newUnreadMessages >= 0) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsresult rv =
+ GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo)
+ folderInfo->SetImapUnreadPendingMessages(mNumPendingUnreadMessages);
+ NotifyIntPropertyChanged(kTotalUnreadMessages, oldUnreadMessages,
+ newUnreadMessages);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ChangeNumPendingTotalMessages(int32_t delta) {
+ if (delta) {
+ int32_t oldTotalMessages = mNumTotalMessages + mNumPendingTotalMessages;
+ mNumPendingTotalMessages += delta;
+ int32_t newTotalMessages = mNumTotalMessages + mNumPendingTotalMessages;
+
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsresult rv =
+ GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo)
+ folderInfo->SetImapTotalPendingMessages(mNumPendingTotalMessages);
+ NotifyIntPropertyChanged(kTotalMessages, oldTotalMessages,
+ newTotalMessages);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetFlag(uint32_t flag) {
+ // If calling this function causes us to open the db (i.e., it was not
+ // open before), we're going to close the db before returning.
+ bool dbWasOpen = mDatabase != nullptr;
+
+ ReadDBFolderInfo(false);
+ // OnFlagChange can be expensive, so don't call it if we don't need to
+ bool flagSet;
+ nsresult rv;
+
+ if (NS_FAILED(rv = GetFlag(flag, &flagSet))) return rv;
+
+ if (!flagSet) {
+ mFlags |= flag;
+ OnFlagChange(flag);
+ }
+ if (!dbWasOpen && mDatabase) SetMsgDatabase(nullptr);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ClearFlag(uint32_t flag) {
+ // OnFlagChange can be expensive, so don't call it if we don't need to
+ bool flagSet;
+ nsresult rv;
+
+ if (NS_FAILED(rv = GetFlag(flag, &flagSet))) return rv;
+
+ if (flagSet) {
+ mFlags &= ~flag;
+ OnFlagChange(flag);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFlag(uint32_t flag, bool* _retval) {
+ *_retval = ((mFlags & flag) != 0);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ToggleFlag(uint32_t flag) {
+ mFlags ^= flag;
+ OnFlagChange(flag);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OnFlagChange(uint32_t flag) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo) {
+#ifdef DEBUG_bienvenu1
+ nsString name;
+ rv = GetName(name);
+ NS_ASSERTION(Compare(name, kLocalizedTrashName) ||
+ (mFlags & nsMsgFolderFlags::Trash),
+ "lost trash flag");
+#endif
+ folderInfo->SetFlags((int32_t)mFlags);
+ if (db) db->Commit(nsMsgDBCommitType::kLargeCommit);
+
+ if (mFlags & flag)
+ NotifyIntPropertyChanged(kFolderFlag, mFlags & ~flag, mFlags);
+ else
+ NotifyIntPropertyChanged(kFolderFlag, mFlags | flag, mFlags);
+
+ if (flag & nsMsgFolderFlags::Offline) {
+ bool newValue = mFlags & nsMsgFolderFlags::Offline;
+ rv = NotifyBoolPropertyChanged(kSynchronize, !newValue, !!newValue);
+ } else if (flag & nsMsgFolderFlags::Elided) {
+ bool newValue = mFlags & nsMsgFolderFlags::Elided;
+ rv = NotifyBoolPropertyChanged(kOpen, !!newValue, !newValue);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetFlags(uint32_t aFlags) {
+ if (mFlags != aFlags) {
+ uint32_t changedFlags = aFlags ^ mFlags;
+ mFlags = aFlags;
+ OnFlagChange(changedFlags);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFolderWithFlags(uint32_t aFlags,
+ nsIMsgFolder** aResult) {
+ if ((mFlags & aFlags) == aFlags) {
+ NS_ADDREF(*aResult = this);
+ return NS_OK;
+ }
+
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ GetSubFolders(dummy); // initialize mSubFolders
+
+ int32_t count = mSubFolders.Count();
+ *aResult = nullptr;
+ for (int32_t i = 0; !*aResult && i < count; ++i)
+ mSubFolders[i]->GetFolderWithFlags(aFlags, aResult);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetFoldersWithFlags(
+ uint32_t aFlags, nsTArray<RefPtr<nsIMsgFolder>>& aResult) {
+ aResult.Clear();
+
+ // Ensure initialisation of mSubFolders.
+ nsTArray<RefPtr<nsIMsgFolder>> dummy;
+ GetSubFolders(dummy);
+
+ if ((mFlags & aFlags) == aFlags) {
+ aResult.AppendElement(this);
+ }
+
+ // Recurse down through children.
+ for (nsIMsgFolder* child : mSubFolders) {
+ nsTArray<RefPtr<nsIMsgFolder>> subMatches;
+ child->GetFoldersWithFlags(aFlags, subMatches);
+ aResult.AppendElements(subMatches);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::IsSpecialFolder(uint32_t aFlags,
+ bool aCheckAncestors,
+ bool* aIsSpecial) {
+ NS_ENSURE_ARG_POINTER(aIsSpecial);
+
+ if ((mFlags & aFlags) == 0) {
+ nsCOMPtr<nsIMsgFolder> parentMsgFolder;
+ GetParent(getter_AddRefs(parentMsgFolder));
+
+ if (parentMsgFolder && aCheckAncestors)
+ parentMsgFolder->IsSpecialFolder(aFlags, aCheckAncestors, aIsSpecial);
+ else
+ *aIsSpecial = false;
+ } else {
+ // The user can set their INBOX to be their SENT folder.
+ // in that case, we want this folder to act like an INBOX,
+ // and not a SENT folder
+ *aIsSpecial = !((aFlags & nsMsgFolderFlags::SentMail) &&
+ (mFlags & nsMsgFolderFlags::Inbox));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetDeletable(bool* deletable) {
+ NS_ENSURE_ARG_POINTER(deletable);
+ *deletable = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetDisplayRecipients(bool* displayRecipients) {
+ *displayRecipients = false;
+ if (mFlags & nsMsgFolderFlags::SentMail &&
+ !(mFlags & nsMsgFolderFlags::Inbox))
+ *displayRecipients = true;
+ else if (mFlags & nsMsgFolderFlags::Queue)
+ *displayRecipients = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AcquireSemaphore(nsISupports* semHolder) {
+ nsresult rv = NS_OK;
+ if (mSemaphoreHolder == NULL)
+ mSemaphoreHolder = semHolder; // Don't AddRef due to ownership issues.
+ else
+ rv = NS_MSG_FOLDER_BUSY;
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ReleaseSemaphore(nsISupports* semHolder) {
+ if (!mSemaphoreHolder || mSemaphoreHolder == semHolder)
+ mSemaphoreHolder = NULL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::TestSemaphore(nsISupports* semHolder,
+ bool* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = (mSemaphoreHolder == semHolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetLocked(bool* isLocked) {
+ *isLocked = mSemaphoreHolder != NULL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetRelativePathName(nsACString& pathName) {
+ pathName.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetSizeOnDisk(int64_t* size) {
+ NS_ENSURE_ARG_POINTER(size);
+ *size = kSizeUnknown;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetSizeOnDisk(int64_t aSizeOnDisk) {
+ NotifyIntPropertyChanged(kFolderSize, mFolderSize, aSizeOnDisk);
+ mFolderSize = aSizeOnDisk;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetUsername(nsACString& userName) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetUsername(userName);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetHostname(nsACString& hostName) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetHostName(hostName);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNewMessages(nsIMsgWindow*,
+ nsIUrlListener* /* aListener */) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetBiffState(uint32_t* aBiffState) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetBiffState(aBiffState);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetBiffState(uint32_t aBiffState) {
+ uint32_t oldBiffState = nsMsgBiffState_Unknown;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) server->GetBiffState(&oldBiffState);
+
+ if (oldBiffState != aBiffState) {
+ // Get the server and notify it and not inbox.
+ if (!mIsServer) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetRootFolder(getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv) && folder) return folder->SetBiffState(aBiffState);
+ }
+ if (server) server->SetBiffState(aBiffState);
+
+ NotifyIntPropertyChanged(kBiffState, oldBiffState, aBiffState);
+ } else if (aBiffState == oldBiffState &&
+ aBiffState == nsMsgBiffState_NewMail) {
+ // The folder has been updated, so update the MRUTime
+ SetMRUTime();
+ // biff is already set, but notify that there is additional new mail for the
+ // folder
+ NotifyIntPropertyChanged(kNewMailReceived, 0, mNumNewBiffMessages);
+ } else if (aBiffState == nsMsgBiffState_NoMail) {
+ // even if the old biff state equals the new biff state, it is still
+ // possible that we've never cleared the number of new messages for this
+ // particular folder. This happens when the new mail state got cleared by
+ // viewing a new message in folder that is different from this one. Biff
+ // state is stored per server
+ // the num. of new messages is per folder.
+ SetNumNewMessages(0);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetNumNewMessages(bool deep,
+ int32_t* aNumNewMessages) {
+ NS_ENSURE_ARG_POINTER(aNumNewMessages);
+
+ int32_t numNewMessages = (!deep || !(mFlags & nsMsgFolderFlags::Virtual))
+ ? mNumNewBiffMessages
+ : 0;
+ if (deep) {
+ int32_t count = mSubFolders.Count();
+ for (int32_t i = 0; i < count; i++) {
+ int32_t num;
+ mSubFolders[i]->GetNumNewMessages(deep, &num);
+ if (num > 0) // it's legal for counts to be negative if we don't know
+ numNewMessages += num;
+ }
+ }
+ *aNumNewMessages = numNewMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetNumNewMessages(int32_t aNumNewMessages) {
+ if (aNumNewMessages != mNumNewBiffMessages) {
+ int32_t oldNumMessages = mNumNewBiffMessages;
+ mNumNewBiffMessages = aNumNewMessages;
+
+ nsAutoCString oldNumMessagesStr;
+ oldNumMessagesStr.AppendInt(oldNumMessages);
+ nsAutoCString newNumMessagesStr;
+ newNumMessagesStr.AppendInt(aNumNewMessages);
+ NotifyPropertyChanged(kNumNewBiffMessages, oldNumMessagesStr,
+ newNumMessagesStr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetRootFolder(nsIMsgFolder** aRootFolder) {
+ NS_ENSURE_ARG_POINTER(aRootFolder);
+ nsresult rv;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetRootMsgFolder(aRootFolder);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetFilePath(nsIFile* aFile) {
+ mPath = aFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetFilePath(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ nsresult rv;
+ // make a new nsIFile object in case the caller
+ // alters the underlying file object.
+ nsCOMPtr<nsIFile> file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!mPath) {
+ rv = parseURI(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = file->InitWithFile(mPath);
+ file.forget(aFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetSummaryFile(nsIFile** aSummaryFile) {
+ NS_ENSURE_ARG_POINTER(aSummaryFile);
+
+ nsresult rv;
+ nsCOMPtr<nsIFile> newSummaryLocation =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> pathFile;
+ rv = GetFilePath(getter_AddRefs(pathFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newSummaryLocation->InitWithFile(pathFile);
+
+ nsString fileName;
+ rv = newSummaryLocation->GetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ fileName.AppendLiteral(SUMMARY_SUFFIX);
+ rv = newSummaryLocation->SetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newSummaryLocation.forget(aSummaryFile);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::MarkMessagesRead(const nsTArray<RefPtr<nsIMsgDBHdr>>& messages,
+ bool markRead) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto message : messages) {
+ rv = message->MarkRead(markRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::MarkMessagesFlagged(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, bool markFlagged) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto message : messages) {
+ rv = message->MarkFlagged(markFlagged);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetJunkScoreForMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& junkScore) {
+ GetDatabase();
+ if (mDatabase) {
+ for (auto message : aMessages) {
+ nsMsgKey msgKey;
+ (void)message->GetMessageKey(&msgKey);
+ mDatabase->SetStringProperty(msgKey, "junkscore", junkScore);
+ mDatabase->SetStringProperty(msgKey, "junkscoreorigin", "filter"_ns);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::ApplyRetentionSettings() { return ApplyRetentionSettings(true); }
+
+nsresult nsMsgDBFolder::ApplyRetentionSettings(bool deleteViaFolder) {
+ if (mFlags & nsMsgFolderFlags::Virtual) // ignore virtual folders.
+ return NS_OK;
+ bool weOpenedDB = !mDatabase;
+ nsCOMPtr<nsIMsgRetentionSettings> retentionSettings;
+ nsresult rv = GetRetentionSettings(getter_AddRefs(retentionSettings));
+ if (NS_SUCCEEDED(rv)) {
+ nsMsgRetainByPreference retainByPreference =
+ nsIMsgRetentionSettings::nsMsgRetainAll;
+
+ retentionSettings->GetRetainByPreference(&retainByPreference);
+ if (retainByPreference != nsIMsgRetentionSettings::nsMsgRetainAll) {
+ rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (mDatabase)
+ rv = mDatabase->ApplyRetentionSettings(retentionSettings,
+ deleteViaFolder);
+ }
+ }
+ // we don't want applying retention settings to keep the db open, because
+ // if we try to purge a bunch of folders, that will leave the dbs all open.
+ // So if we opened the db, close it.
+ if (weOpenedDB) CloseDBIfFolderNotOpen(false);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* msgWindow, bool deleteStorage,
+ bool isMove, nsIMsgCopyServiceListener* listener,
+ bool allowUndo) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::CopyMessages(nsIMsgFolder* srcFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ bool isMove, nsIMsgWindow* window,
+ nsIMsgCopyServiceListener* listener, bool isFolder,
+ bool allowUndo) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::CopyFolder(nsIMsgFolder* srcFolder, bool isMoveFolder,
+ nsIMsgWindow* window,
+ nsIMsgCopyServiceListener* listener) {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::CopyFileMessage(nsIFile* aFile, nsIMsgDBHdr* messageToReplace,
+ bool isDraftOrTemplate, uint32_t aNewMsgFlags,
+ const nsACString& aNewMsgKeywords,
+ nsIMsgWindow* window,
+ nsIMsgCopyServiceListener* listener) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CopyDataToOutputStreamForAppend(
+ nsIInputStream* aInStream, int32_t aLength,
+ nsIOutputStream* aOutputStream) {
+ if (!aInStream) return NS_OK;
+
+ uint32_t uiWritten;
+ return aOutputStream->WriteFrom(aInStream, aLength, &uiWritten);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CopyDataDone() { return NS_OK; }
+
+#define NOTIFY_LISTENERS(propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>>::ForwardIterator iter( \
+ mListeners); \
+ nsCOMPtr<nsIFolderListener> listener; \
+ while (iter.HasMore()) { \
+ listener = iter.GetNext(); \
+ listener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyPropertyChanged(const nsACString& aProperty,
+ const nsACString& aOldValue,
+ const nsACString& aNewValue) {
+ NOTIFY_LISTENERS(OnFolderPropertyChanged,
+ (this, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderPropertyChanged(this, aProperty,
+ aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyUnicharPropertyChanged(const nsACString& aProperty,
+ const nsAString& aOldValue,
+ const nsAString& aNewValue) {
+ NOTIFY_LISTENERS(OnFolderUnicharPropertyChanged,
+ (this, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderUnicharPropertyChanged(
+ this, aProperty, aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyIntPropertyChanged(const nsACString& aProperty,
+ int64_t aOldValue, int64_t aNewValue) {
+ // Don't send off count notifications if they are turned off.
+ if (!mNotifyCountChanges && (aProperty.Equals(kTotalMessages) ||
+ aProperty.Equals(kTotalUnreadMessages)))
+ return NS_OK;
+
+ NOTIFY_LISTENERS(OnFolderIntPropertyChanged,
+ (this, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderIntPropertyChanged(
+ this, aProperty, aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyBoolPropertyChanged(const nsACString& aProperty,
+ bool aOldValue, bool aNewValue) {
+ NOTIFY_LISTENERS(OnFolderBoolPropertyChanged,
+ (this, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderBoolPropertyChanged(
+ this, aProperty, aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::NotifyPropertyFlagChanged(nsIMsgDBHdr* aItem,
+ const nsACString& aProperty,
+ uint32_t aOldValue,
+ uint32_t aNewValue) {
+ NOTIFY_LISTENERS(OnFolderPropertyFlagChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderPropertyFlagChanged(
+ aItem, aProperty, aOldValue, aNewValue);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::NotifyMessageAdded(nsIMsgDBHdr* msg) {
+ // Notify our directly-registered listeners.
+ NOTIFY_LISTENERS(OnMessageAdded, (this, msg));
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folderListenerManager->OnMessageAdded(this, msg);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::NotifyMessageRemoved(nsIMsgDBHdr* msg) {
+ // Notify our directly-registered listeners.
+ NOTIFY_LISTENERS(OnMessageRemoved, (this, msg));
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folderListenerManager->OnMessageRemoved(this, msg);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::NotifyFolderAdded(nsIMsgFolder* child) {
+ NOTIFY_LISTENERS(OnFolderAdded, (this, child));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderAdded(this, child);
+}
+
+nsresult nsMsgDBFolder::NotifyFolderRemoved(nsIMsgFolder* child) {
+ NOTIFY_LISTENERS(OnFolderRemoved, (this, child));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderRemoved(this, child);
+}
+
+nsresult nsMsgDBFolder::NotifyFolderEvent(const nsACString& aEvent) {
+ NOTIFY_LISTENERS(OnFolderEvent, (this, aEvent));
+
+ // Notify listeners who listen to every folder
+ nsresult rv;
+ nsCOMPtr<nsIFolderListener> folderListenerManager =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderListenerManager->OnFolderEvent(this, aEvent);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetFilterList(aMsgWindow, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetFilterList(nsIMsgFilterList* aFilterList) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->SetFilterList(aFilterList);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetEditableFilterList(aMsgWindow, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::SetEditableFilterList(nsIMsgFilterList* aFilterList) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->SetEditableFilterList(aFilterList);
+}
+
+/* void enableNotifications (in long notificationType, in boolean enable); */
+NS_IMETHODIMP nsMsgDBFolder::EnableNotifications(int32_t notificationType,
+ bool enable) {
+ if (notificationType == nsIMsgFolder::allMessageCountNotifications) {
+ mNotifyCountChanges = enable;
+ if (enable) {
+ UpdateSummaryTotals(true);
+ }
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetMessageHeader(nsMsgKey msgKey,
+ nsIMsgDBHdr** aMsgHdr) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ nsCOMPtr<nsIMsgDatabase> database;
+ nsresult rv = GetMsgDatabase(getter_AddRefs(database));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return (database) ? database->GetMsgHdrForKey(msgKey, aMsgHdr)
+ : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetDescendants(
+ nsTArray<RefPtr<nsIMsgFolder>>& aDescendants) {
+ aDescendants.Clear();
+ for (nsIMsgFolder* child : mSubFolders) {
+ aDescendants.AppendElement(child);
+ nsTArray<RefPtr<nsIMsgFolder>> grandchildren;
+ child->GetDescendants(grandchildren);
+ aDescendants.AppendElements(grandchildren);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetBaseMessageURI(nsACString& baseMessageURI) {
+ if (mBaseMessageURI.IsEmpty()) return NS_ERROR_FAILURE;
+ baseMessageURI = mBaseMessageURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetUriForMsg(nsIMsgDBHdr* msgHdr,
+ nsACString& aURI) {
+ NS_ENSURE_ARG(msgHdr);
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ nsAutoCString uri;
+ uri.Assign(mBaseMessageURI);
+
+ // append a "#" followed by the message key.
+ uri.Append('#');
+ uri.AppendInt(msgKey);
+ aURI = uri;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GenerateMessageURI(nsMsgKey msgKey,
+ nsACString& aURI) {
+ nsCString uri;
+ nsresult rv = GetBaseMessageURI(uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // append a "#" followed by the message key.
+ uri.Append('#');
+ uri.AppendInt(msgKey);
+ aURI = uri;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::GetBaseStringBundle(nsIStringBundle** aBundle) {
+ NS_ENSURE_ARG_POINTER(aBundle);
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ bundleService->CreateBundle("chrome://messenger/locale/messenger.properties",
+ getter_AddRefs(bundle));
+ bundle.forget(aBundle);
+ return NS_OK;
+}
+
+// Do not use this routine if you have to call it very often because
+// it creates a new bundle each time
+nsresult nsMsgDBFolder::GetStringFromBundle(const char* msgName,
+ nsString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ if (NS_SUCCEEDED(rv) && bundle)
+ rv = bundle->GetStringFromName(msgName, aResult);
+ return rv;
+}
+
+nsresult nsMsgDBFolder::ThrowConfirmationPrompt(nsIMsgWindow* msgWindow,
+ const nsAString& confirmString,
+ bool* confirmed) {
+ if (msgWindow) {
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ if (docShell) {
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell));
+ if (dialog && !confirmString.IsEmpty())
+ dialog->Confirm(nullptr, nsString(confirmString).get(), confirmed);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBFolder::GetStringWithFolderNameFromBundle(const char* msgName,
+ nsAString& aResult) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ if (NS_SUCCEEDED(rv) && bundle) {
+ nsString folderName;
+ GetName(folderName);
+ AutoTArray<nsString, 2> formatStrings = {folderName,
+ kLocalizedBrandShortName};
+ nsString resultStr;
+ rv = bundle->FormatStringFromName(msgName, formatStrings, resultStr);
+ if (NS_SUCCEEDED(rv)) aResult.Assign(resultStr);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ConfirmFolderDeletionForFilter(
+ nsIMsgWindow* msgWindow, bool* confirmed) {
+ nsString confirmString;
+ nsresult rv = GetStringWithFolderNameFromBundle(
+ "confirmFolderDeletionForFilter", confirmString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ThrowConfirmationPrompt(msgWindow, confirmString, confirmed);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ThrowAlertMsg(const char* msgName,
+ nsIMsgWindow* msgWindow) {
+ if (!msgWindow) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Assemble a pretty folder identifier, e.g. "Trash on bob@example.com".
+ nsAutoString ident;
+ nsAutoString folderName;
+ GetName(folderName);
+ nsAutoString serverName;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (NS_SUCCEEDED(GetServer(getter_AddRefs(server)))) {
+ server->GetPrettyName(serverName);
+ bundle->FormatStringFromName("verboseFolderFormat",
+ {folderName, serverName}, ident);
+ }
+ if (ident.IsEmpty()) {
+ ident = folderName; // Fallback, just in case.
+ }
+
+ // Format the actual error message (NOTE: not all error messages use the
+ // params - extra values are just ignored).
+ nsAutoString alertString;
+ rv = bundle->FormatStringFromName(msgName, {ident, kLocalizedBrandShortName},
+ alertString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Include the folder identifier in the alert title for good measure,
+ // because not all the error messages include the folder.
+ nsAutoString title;
+ bundle->FormatStringFromName("folderErrorAlertTitle", {ident}, title);
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ rv = msgWindow->GetDomWindow(getter_AddRefs(domWindow));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return dlgService->Alert(domWindow, title.IsEmpty() ? nullptr : title.get(),
+ alertString.get());
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AlertFilterChanged(nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG(msgWindow);
+ nsresult rv = NS_OK;
+ bool checkBox = false;
+ GetWarnFilterChanged(&checkBox);
+ if (!checkBox) {
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ nsString alertString;
+ rv = GetStringFromBundle("alertFilterChanged", alertString);
+ nsString alertCheckbox;
+ rv = GetStringFromBundle("alertFilterCheckbox", alertCheckbox);
+ if (!alertString.IsEmpty() && !alertCheckbox.IsEmpty() && docShell) {
+ nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell));
+ if (dialog) {
+ dialog->AlertCheck(nullptr, alertString.get(), alertCheckbox.get(),
+ &checkBox);
+ SetWarnFilterChanged(checkBox);
+ }
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgDBFolder::GetWarnFilterChanged(bool* aVal) {
+ NS_ENSURE_ARG(aVal);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = prefBranch->GetBoolPref(PREF_MAIL_WARN_FILTER_CHANGED, aVal);
+ if (NS_FAILED(rv)) *aVal = false;
+ return NS_OK;
+}
+
+nsresult nsMsgDBFolder::SetWarnFilterChanged(bool aVal) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->SetBoolPref(PREF_MAIL_WARN_FILTER_CHANGED, aVal);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::NotifyCompactCompleted() {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CloseDBIfFolderNotOpen(bool aForceClosed) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool folderOpen;
+ session->IsFolderOpenInWindow(this, &folderOpen);
+ if (!folderOpen &&
+ !(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox))) {
+ if (aForceClosed && mDatabase) mDatabase->ForceClosed();
+ SetMsgDatabase(nullptr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::SetSortOrder(int32_t order) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetSortOrder(int32_t* order) {
+ NS_ENSURE_ARG_POINTER(order);
+
+ uint32_t flags;
+ nsresult rv = GetFlags(&flags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (flags & nsMsgFolderFlags::Inbox)
+ *order = 0;
+ else if (flags & nsMsgFolderFlags::Drafts)
+ *order = 1;
+ else if (flags & nsMsgFolderFlags::Templates)
+ *order = 2;
+ else if (flags & nsMsgFolderFlags::SentMail)
+ *order = 3;
+ else if (flags & nsMsgFolderFlags::Archive)
+ *order = 4;
+ else if (flags & nsMsgFolderFlags::Junk)
+ *order = 5;
+ else if (flags & nsMsgFolderFlags::Trash)
+ *order = 6;
+ else if (flags & nsMsgFolderFlags::Virtual)
+ *order = 7;
+ else if (flags & nsMsgFolderFlags::Queue)
+ *order = 8;
+ else
+ *order = 9;
+
+ return NS_OK;
+}
+
+// static Helper function for CompareSortKeys().
+// Builds a collation key for a given folder based on "{sortOrder}{name}"
+nsresult nsMsgDBFolder::BuildFolderSortKey(nsIMsgFolder* aFolder,
+ nsTArray<uint8_t>& aKey) {
+ aKey.Clear();
+ int32_t order;
+ nsresult rv = aFolder->GetSortOrder(&order);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoString orderString;
+ orderString.AppendInt(order);
+ nsString folderName;
+ rv = aFolder->GetName(folderName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ orderString.Append(folderName);
+ NS_ENSURE_TRUE(gCollationKeyGenerator, NS_ERROR_NULL_POINTER);
+
+ nsTArrayU8Buffer buffer(aKey);
+
+ auto result = gCollationKeyGenerator->GetSortKey(orderString, buffer);
+ NS_ENSURE_TRUE(result.isOk(), NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::CompareSortKeys(nsIMsgFolder* aFolder,
+ int32_t* sortOrder) {
+ nsTArray<uint8_t> sortKey1;
+ nsTArray<uint8_t> sortKey2;
+ nsresult rv = BuildFolderSortKey(this, sortKey1);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = BuildFolderSortKey(aFolder, sortKey2);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *sortOrder = gCollationKeyGenerator->CompareSortKeys(sortKey1, sortKey2);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::FetchMsgPreviewText(
+ nsTArray<nsMsgKey> const& aKeysToFetch, nsIUrlListener* aUrlListener,
+ bool* aAsyncResults) {
+ NS_ENSURE_ARG_POINTER(aAsyncResults);
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetMsgTextFromStream(
+ nsIInputStream* stream, const nsACString& aCharset, uint32_t bytesToRead,
+ uint32_t aMaxOutputLen, bool aCompressQuotes, bool aStripHTMLTags,
+ nsACString& aContentType, nsACString& aMsgText) {
+ /*
+ 1. non mime message - the message body starts after the blank line
+ following the headers.
+ 2. mime message, multipart/alternative - we could simply scan for the
+ boundary line, advance past its headers, and treat the next few lines
+ as the text.
+ 3. mime message, text/plain - body follows headers
+ 4. multipart/mixed - scan past boundary, treat next part as body.
+ */
+
+ UniquePtr<nsLineBuffer<char>> lineBuffer(new nsLineBuffer<char>);
+
+ nsAutoCString msgText;
+ nsAutoString contentType;
+ nsAutoString encoding;
+ nsAutoCString curLine;
+ nsAutoCString charset(aCharset);
+
+ // might want to use a state var instead of bools.
+ bool msgBodyIsHtml = false;
+ bool more = true;
+ bool reachedEndBody = false;
+ bool isBase64 = false;
+ bool inMsgBody = false;
+ bool justPassedEndBoundary = false;
+
+ uint32_t bytesRead = 0;
+
+ nsresult rv;
+
+ // Both are used to extract data from the headers
+ nsCOMPtr<nsIMimeHeaders> mimeHeaders(
+ do_CreateInstance(NS_IMIMEHEADERS_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMIMEHeaderParam> mimeHdrParam(
+ do_GetService(NS_MIMEHEADERPARAM_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Stack of boundaries, used to figure out where we are
+ nsTArray<nsCString> boundaryStack;
+
+ while (!inMsgBody && bytesRead <= bytesToRead) {
+ nsAutoCString msgHeaders;
+ // We want to NS_ReadLine until we get to a blank line (the end of the
+ // headers)
+ while (more) {
+ rv = NS_ReadLine(stream, lineBuffer.get(), curLine, &more);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (curLine.IsEmpty()) break;
+ msgHeaders.Append(curLine);
+ msgHeaders.AppendLiteral("\r\n");
+ bytesRead += curLine.Length();
+ if (bytesRead > bytesToRead) break;
+ }
+
+ // There's no point in processing if we can't get the body
+ if (bytesRead > bytesToRead) break;
+
+ // Process the headers, looking for things we need
+ rv = mimeHeaders->Initialize(msgHeaders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString contentTypeHdr;
+ mimeHeaders->ExtractHeader("Content-Type", false, contentTypeHdr);
+
+ // Get the content type
+ // If we don't have a content type, then we assign text/plain
+ // this is in violation of the RFC for multipart/digest, though
+ // Also, if we've just passed an end boundary, we're going to ignore this.
+ if (!justPassedEndBoundary && contentTypeHdr.IsEmpty())
+ contentType.AssignLiteral(u"text/plain");
+ else
+ mimeHdrParam->GetParameter(contentTypeHdr, nullptr, EmptyCString(), false,
+ nullptr, contentType);
+
+ justPassedEndBoundary = false;
+
+ // If we are multipart, then we need to get the boundary
+ if (StringBeginsWith(contentType, u"multipart/"_ns,
+ nsCaseInsensitiveStringComparator)) {
+ nsAutoString boundaryParam;
+ mimeHdrParam->GetParameter(contentTypeHdr, "boundary", EmptyCString(),
+ false, nullptr, boundaryParam);
+ if (!boundaryParam.IsEmpty()) {
+ nsAutoCString boundary("--"_ns);
+ boundary.Append(NS_ConvertUTF16toUTF8(boundaryParam));
+ boundaryStack.AppendElement(boundary);
+ }
+ }
+
+ // If we are message/rfc822, then there's another header block coming up
+ else if (contentType.LowerCaseEqualsLiteral("message/rfc822"))
+ continue;
+
+ // If we are a text part, then we want it
+ else if (StringBeginsWith(contentType, u"text/"_ns,
+ nsCaseInsensitiveStringComparator)) {
+ inMsgBody = true;
+
+ if (contentType.LowerCaseEqualsLiteral("text/html")) msgBodyIsHtml = true;
+
+ // Also get the charset if required
+ if (charset.IsEmpty()) {
+ nsAutoString charsetW;
+ mimeHdrParam->GetParameter(contentTypeHdr, "charset", EmptyCString(),
+ false, nullptr, charsetW);
+ charset.Assign(NS_ConvertUTF16toUTF8(charsetW));
+ }
+
+ // Finally, get the encoding
+ nsAutoCString encodingHdr;
+ mimeHeaders->ExtractHeader("Content-Transfer-Encoding", false,
+ encodingHdr);
+ if (!encodingHdr.IsEmpty())
+ mimeHdrParam->GetParameter(encodingHdr, nullptr, EmptyCString(), false,
+ nullptr, encoding);
+
+ if (encoding.LowerCaseEqualsLiteral(ENCODING_BASE64)) isBase64 = true;
+ }
+
+ // We need to consume the rest, until the next headers
+ uint32_t count = boundaryStack.Length();
+ nsAutoCString boundary;
+ nsAutoCString endBoundary;
+ if (count) {
+ boundary.Assign(boundaryStack.ElementAt(count - 1));
+ endBoundary.Assign(boundary);
+ endBoundary.AppendLiteral("--");
+ }
+ while (more) {
+ rv = NS_ReadLine(stream, lineBuffer.get(), curLine, &more);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (count) {
+ // If we've reached a MIME final delimiter, pop and break
+ if (StringBeginsWith(curLine, endBoundary)) {
+ if (inMsgBody) reachedEndBody = true;
+ boundaryStack.RemoveElementAt(count - 1);
+ justPassedEndBoundary = true;
+ break;
+ }
+ // If we've reached the end of this MIME part, we can break
+ if (StringBeginsWith(curLine, boundary)) {
+ if (inMsgBody) reachedEndBody = true;
+ break;
+ }
+ }
+
+ // Only append the text if we're actually in the message body
+ if (inMsgBody) {
+ msgText.Append(curLine);
+ if (!isBase64) msgText.AppendLiteral("\r\n");
+ }
+
+ bytesRead += curLine.Length();
+ if (bytesRead > bytesToRead) break;
+ }
+ }
+ lineBuffer.reset();
+
+ // if the snippet is encoded, decode it
+ if (!encoding.IsEmpty())
+ decodeMsgSnippet(NS_ConvertUTF16toUTF8(encoding), !reachedEndBody, msgText);
+
+ // In order to turn our snippet into unicode, we need to convert it from the
+ // charset we detected earlier.
+ nsString unicodeMsgBodyStr;
+ nsMsgI18NConvertToUnicode(charset, msgText, unicodeMsgBodyStr);
+
+ // now we've got a msg body. If it's html, convert it to plain text.
+ if (msgBodyIsHtml && aStripHTMLTags)
+ ConvertMsgSnippetToPlainText(unicodeMsgBodyStr, unicodeMsgBodyStr);
+
+ // We want to remove any whitespace from the beginning and end of the string
+ unicodeMsgBodyStr.Trim(" \t\r\n", true, true);
+
+ // step 3, optionally remove quoted text from the snippet
+ nsString compressedQuotesMsgStr;
+ if (aCompressQuotes)
+ compressQuotesInMsgSnippet(unicodeMsgBodyStr, compressedQuotesMsgStr);
+
+ // now convert back to utf-8 which is more convenient for storage
+ CopyUTF16toUTF8(aCompressQuotes ? compressedQuotesMsgStr : unicodeMsgBodyStr,
+ aMsgText);
+
+ // finally, truncate the string based on aMaxOutputLen
+ if (aMsgText.Length() > aMaxOutputLen) {
+ if (NS_IsAscii(aMsgText.BeginReading()))
+ aMsgText.SetLength(aMaxOutputLen);
+ else
+ nsMsgI18NShrinkUTF8Str(nsCString(aMsgText), aMaxOutputLen, aMsgText);
+ }
+
+ // Also assign the content type being returned
+ aContentType.Assign(NS_ConvertUTF16toUTF8(contentType));
+ return rv;
+}
+
+/**
+ * decodeMsgSnippet - helper function which applies the appropriate transfer
+ * decoding to the message snippet based on aEncodingType. Currently handles
+ * base64 and quoted-printable. If aEncodingType refers to an encoding we
+ * don't handle, the message data is passed back unmodified.
+ * @param aEncodingType the encoding type (base64, quoted-printable)
+ * @param aIsComplete the snippet is actually the entire message so the
+ * decoder doesn't have to worry about partial data
+ * @param aMsgSnippet in/out argument. The encoded msg snippet and then the
+ * decoded snippet
+ */
+void nsMsgDBFolder::decodeMsgSnippet(const nsACString& aEncodingType,
+ bool aIsComplete, nsCString& aMsgSnippet) {
+ if (aEncodingType.LowerCaseEqualsLiteral(ENCODING_BASE64)) {
+ int32_t base64Len = aMsgSnippet.Length();
+ if (aIsComplete) base64Len -= base64Len % 4;
+ char* decodedBody = PL_Base64Decode(aMsgSnippet.get(), base64Len, nullptr);
+ if (decodedBody) aMsgSnippet.Adopt(decodedBody);
+ } else if (aEncodingType.LowerCaseEqualsLiteral(ENCODING_QUOTED_PRINTABLE)) {
+ MsgStripQuotedPrintable(aMsgSnippet);
+ }
+}
+
+/**
+ * stripQuotesFromMsgSnippet - Reduces quoted reply text including the citation
+ * (Scott wrote:) from the message snippet to " ... ". Assumes the snippet has
+ * been decoded and converted to plain text.
+ * @param aMsgSnippet in/out argument. The string to strip quotes from.
+ */
+void nsMsgDBFolder::compressQuotesInMsgSnippet(const nsString& aMsgSnippet,
+ nsAString& aCompressedQuotes) {
+ int32_t msgBodyStrLen = aMsgSnippet.Length();
+ bool lastLineWasAQuote = false;
+ int32_t offset = 0;
+ int32_t lineFeedPos = 0;
+ while (offset < msgBodyStrLen) {
+ lineFeedPos = aMsgSnippet.FindChar('\n', offset);
+ if (lineFeedPos != -1) {
+ const nsAString& currentLine =
+ Substring(aMsgSnippet, offset, lineFeedPos - offset);
+ // this catches quoted text ("> "), nested quotes of any level (">> ",
+ // ">>> ", ...) it also catches empty line quoted text (">"). It might be
+ // over aggressive and require tweaking later. Try to strip the citation.
+ // If the current line ends with a ':' and the next line looks like a
+ // quoted reply (starts with a ">") skip the current line
+ if (StringBeginsWith(currentLine, u">"_ns) ||
+ (lineFeedPos + 1 < msgBodyStrLen && lineFeedPos &&
+ aMsgSnippet[lineFeedPos - 1] == char16_t(':') &&
+ aMsgSnippet[lineFeedPos + 1] == char16_t('>'))) {
+ lastLineWasAQuote = true;
+ } else if (!currentLine.IsEmpty()) {
+ if (lastLineWasAQuote) {
+ aCompressedQuotes += u" ... "_ns;
+ lastLineWasAQuote = false;
+ }
+
+ aCompressedQuotes += currentLine;
+ // Don't forget to substitute a space for the line feed.
+ aCompressedQuotes += char16_t(' ');
+ }
+
+ offset = lineFeedPos + 1;
+ } else {
+ aCompressedQuotes.Append(
+ Substring(aMsgSnippet, offset, msgBodyStrLen - offset));
+ break;
+ }
+ }
+}
+
+NS_IMETHODIMP nsMsgDBFolder::ConvertMsgSnippetToPlainText(
+ const nsAString& aMessageText, nsAString& aOutText) {
+ uint32_t flags = nsIDocumentEncoder::OutputLFLineBreak |
+ nsIDocumentEncoder::OutputNoScriptContent |
+ nsIDocumentEncoder::OutputNoFramesContent |
+ nsIDocumentEncoder::OutputBodyOnly;
+ nsCOMPtr<nsIParserUtils> utils = do_GetService(NS_PARSERUTILS_CONTRACTID);
+ return utils->ConvertToPlainText(aMessageText, flags, 80, aOutText);
+}
+
+nsresult nsMsgDBFolder::GetMsgPreviewTextFromStream(nsIMsgDBHdr* msgHdr,
+ nsIInputStream* stream) {
+ nsCString msgBody;
+ nsAutoCString charset;
+ msgHdr->GetCharset(getter_Copies(charset));
+ nsAutoCString contentType;
+ nsresult rv = GetMsgTextFromStream(stream, charset, 4096, 255, true, true,
+ contentType, msgBody);
+ // replaces all tabs and line returns with a space,
+ // then trims off leading and trailing white space
+ msgBody.CompressWhitespace();
+ msgHdr->SetStringProperty("preview", msgBody);
+ return rv;
+}
+
+void nsMsgDBFolder::UpdateTimestamps(bool allowUndo) {
+ if (!(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) {
+ SetMRUTime();
+ if (allowUndo) // This is a proxy for a user-initiated act.
+ {
+ bool isArchive;
+ IsSpecialFolder(nsMsgFolderFlags::Archive, true, &isArchive);
+ if (!isArchive) {
+ SetMRMTime();
+ NotifyFolderEvent(kMRMTimeChanged);
+ }
+ }
+ }
+}
+
+void nsMsgDBFolder::SetMRUTime() {
+ uint32_t seconds;
+ PRTime2Seconds(PR_Now(), &seconds);
+ nsAutoCString nowStr;
+ nowStr.AppendInt(seconds);
+ SetStringProperty(MRU_TIME_PROPERTY, nowStr);
+}
+
+void nsMsgDBFolder::SetMRMTime() {
+ uint32_t seconds;
+ PRTime2Seconds(PR_Now(), &seconds);
+ nsAutoCString nowStr;
+ nowStr.AppendInt(seconds);
+ SetStringProperty(MRM_TIME_PROPERTY, nowStr);
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AddKeywordsToMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) {
+ nsresult rv = NS_OK;
+ GetDatabase();
+ if (mDatabase) {
+ nsCString keywords;
+
+ for (auto message : aMessages) {
+ message->GetStringProperty("keywords", keywords);
+ nsTArray<nsCString> keywordArray;
+ ParseString(aKeywords, ' ', keywordArray);
+ uint32_t addCount = 0;
+ for (uint32_t j = 0; j < keywordArray.Length(); j++) {
+ int32_t start, length;
+ if (!MsgFindKeyword(keywordArray[j], keywords, &start, &length)) {
+ if (!keywords.IsEmpty()) keywords.Append(' ');
+ keywords.Append(keywordArray[j]);
+ addCount++;
+ }
+ }
+ // avoid using the message key to set the string property, because
+ // in the case of filters running on incoming pop3 mail with quarantining
+ // turned on, the message key is wrong.
+ mDatabase->SetStringPropertyByHdr(message, "keywords", keywords);
+
+ if (addCount) NotifyPropertyFlagChanged(message, kKeywords, 0, addCount);
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::RemoveKeywordsFromMessages(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages,
+ const nsACString& aKeywords) {
+ nsresult rv = NS_OK;
+ GetDatabase();
+ if (mDatabase) {
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsTArray<nsCString> keywordArray;
+ ParseString(aKeywords, ' ', keywordArray);
+ nsCString keywords;
+ // If the tag is also a label, we should remove the label too...
+
+ for (auto message : aMessages) {
+ rv = message->GetStringProperty("keywords", keywords);
+ uint32_t removeCount = 0;
+ for (uint32_t j = 0; j < keywordArray.Length(); j++) {
+ int32_t startOffset, length;
+ if (MsgFindKeyword(keywordArray[j], keywords, &startOffset, &length)) {
+ // delete any leading space delimiters
+ while (startOffset && keywords.CharAt(startOffset - 1) == ' ') {
+ startOffset--;
+ length++;
+ }
+ // but if the keyword is at the start then delete the following space
+ if (!startOffset &&
+ length < static_cast<int32_t>(keywords.Length()) &&
+ keywords.CharAt(length) == ' ')
+ length++;
+ keywords.Cut(startOffset, length);
+ removeCount++;
+ }
+ }
+
+ if (removeCount) {
+ mDatabase->SetStringPropertyByHdr(message, "keywords", keywords);
+ NotifyPropertyFlagChanged(message, kKeywords, removeCount, 0);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetCustomIdentity(nsIMsgIdentity** aIdentity) {
+ NS_ENSURE_ARG_POINTER(aIdentity);
+ *aIdentity = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::GetProcessingFlags(nsMsgKey aKey,
+ uint32_t* aFlags) {
+ NS_ENSURE_ARG_POINTER(aFlags);
+ *aFlags = 0;
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ if (mProcessingFlag[i].keys && mProcessingFlag[i].keys->IsMember(aKey))
+ *aFlags |= mProcessingFlag[i].bit;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::OrProcessingFlags(nsMsgKey aKey, uint32_t mask) {
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ if (mProcessingFlag[i].bit & mask && mProcessingFlag[i].keys)
+ mProcessingFlag[i].keys->Add(aKey);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgDBFolder::AndProcessingFlags(nsMsgKey aKey, uint32_t mask) {
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++)
+ if (!(mProcessingFlag[i].bit & mask) && mProcessingFlag[i].keys)
+ mProcessingFlag[i].keys->Remove(aKey);
+ return NS_OK;
+}
+
+// Each implementation must provide an override of this, connecting the folder
+// type to the corresponding incoming server type.
+NS_IMETHODIMP nsMsgDBFolder::GetIncomingServerType(
+ nsACString& aIncomingServerType) {
+ NS_ASSERTION(false, "subclasses need to override this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+void nsMsgDBFolder::ClearProcessingFlags() {
+ for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++) {
+ // There is no clear method so we need to delete and re-create.
+ delete mProcessingFlag[i].keys;
+ mProcessingFlag[i].keys = nsMsgKeySetU::Create();
+ }
+}
+
+nsresult nsMsgDBFolder::MessagesInKeyOrder(
+ nsTArray<nsMsgKey> const& aKeyArray, nsIMsgFolder* srcFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& messages) {
+ messages.Clear();
+ messages.SetCapacity(aKeyArray.Length());
+
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && db) {
+ for (auto key : aKeyArray) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (msgHdr) messages.AppendElement(msgHdr);
+ }
+ }
+ return rv;
+}
+
+/* static */ nsMsgKeySetU* nsMsgKeySetU::Create() {
+ nsMsgKeySetU* set = new nsMsgKeySetU;
+ if (set) {
+ set->loKeySet = nsMsgKeySet::Create();
+ set->hiKeySet = nsMsgKeySet::Create();
+ if (!(set->loKeySet && set->hiKeySet)) {
+ delete set;
+ set = nullptr;
+ }
+ }
+ return set;
+}
+
+nsMsgKeySetU::nsMsgKeySetU() : hiKeySet(nullptr) {}
+
+nsMsgKeySetU::~nsMsgKeySetU() {}
+
+const uint32_t kLowerBits = 0x7fffffff;
+
+int nsMsgKeySetU::Add(nsMsgKey aKey) {
+ int32_t intKey = static_cast<int32_t>(aKey);
+ if (intKey >= 0) return loKeySet->Add(intKey);
+ return hiKeySet->Add(intKey & kLowerBits);
+}
+
+int nsMsgKeySetU::Remove(nsMsgKey aKey) {
+ int32_t intKey = static_cast<int32_t>(aKey);
+ if (intKey >= 0) return loKeySet->Remove(intKey);
+ return hiKeySet->Remove(intKey & kLowerBits);
+}
+
+bool nsMsgKeySetU::IsMember(nsMsgKey aKey) {
+ int32_t intKey = static_cast<int32_t>(aKey);
+ if (intKey >= 0) return loKeySet->IsMember(intKey);
+ return hiKeySet->IsMember(intKey & kLowerBits);
+}
+
+nsresult nsMsgKeySetU::ToMsgKeyArray(nsTArray<nsMsgKey>& aArray) {
+ nsresult rv = loKeySet->ToMsgKeyArray(aArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return hiKeySet->ToMsgKeyArray(aArray);
+}
diff --git a/comm/mailnews/base/src/nsMsgDBFolder.h b/comm/mailnews/base/src/nsMsgDBFolder.h
new file mode 100644
index 0000000000..1cd01199f6
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgDBFolder.h
@@ -0,0 +1,366 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMsgDBFolder_h__
+#define nsMsgDBFolder_h__
+
+#include "mozilla/Attributes.h"
+#include "msgCore.h"
+#include "nsIMsgFolder.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsCOMPtr.h"
+#include "nsIDBChangeListener.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIURL.h"
+#include "nsIFile.h"
+#include "nsWeakReference.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIMsgFilterList.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgHdr.h"
+#include "nsIOutputStream.h"
+#include "nsITransport.h"
+#include "nsIStringBundle.h"
+#include "nsTObserverArray.h"
+#include "nsCOMArray.h"
+#include "nsMsgKeySet.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgFilterPlugin.h"
+#include "mozilla/intl/Collator.h"
+
+// We declare strings for folder properties and events.
+// Properties:
+extern const nsLiteralCString kBiffState;
+extern const nsLiteralCString kCanFileMessages;
+extern const nsLiteralCString kDefaultServer;
+extern const nsLiteralCString kFlagged;
+extern const nsLiteralCString kFolderFlag;
+extern const nsLiteralCString kFolderSize;
+extern const nsLiteralCString kIsDeferred;
+extern const nsLiteralCString kIsSecure;
+extern const nsLiteralCString kJunkStatusChanged;
+extern const nsLiteralCString kKeywords;
+extern const nsLiteralCString kMRMTimeChanged;
+extern const nsLiteralCString kMsgLoaded;
+extern const nsLiteralCString kName;
+extern const nsLiteralCString kNewMailReceived;
+extern const nsLiteralCString kNewMessages;
+extern const nsLiteralCString kOpen;
+extern const nsLiteralCString kSortOrder;
+extern const nsLiteralCString kStatus;
+extern const nsLiteralCString kSynchronize;
+extern const nsLiteralCString kTotalMessages;
+extern const nsLiteralCString kTotalUnreadMessages;
+
+// Events:
+extern const nsLiteralCString kAboutToCompact;
+extern const nsLiteralCString kCompactCompleted;
+extern const nsLiteralCString kDeleteOrMoveMsgCompleted;
+extern const nsLiteralCString kDeleteOrMoveMsgFailed;
+extern const nsLiteralCString kFiltersApplied;
+extern const nsLiteralCString kFolderCreateCompleted;
+extern const nsLiteralCString kFolderCreateFailed;
+extern const nsLiteralCString kFolderLoaded;
+extern const nsLiteralCString kNumNewBiffMessages;
+extern const nsLiteralCString kRenameCompleted;
+
+using mozilla::intl::Collator;
+
+class nsIMsgFolderCacheElement;
+class nsMsgKeySetU;
+
+class nsMsgFolderService final : public nsIMsgFolderService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFOLDERSERVICE
+
+ nsMsgFolderService(){};
+
+ protected:
+ ~nsMsgFolderService(){};
+};
+
+/*
+ * nsMsgDBFolder
+ * class derived from nsMsgFolder for those folders that use an nsIMsgDatabase
+ */
+class nsMsgDBFolder : public nsSupportsWeakReference,
+ public nsIMsgFolder,
+ public nsIDBChangeListener,
+ public nsIUrlListener,
+ public nsIJunkMailClassificationListener,
+ public nsIMsgTraitClassificationListener {
+ public:
+ friend class nsMsgFolderService;
+
+ nsMsgDBFolder(void);
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGFOLDER
+ NS_DECL_NSIDBCHANGELISTENER
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIJUNKMAILCLASSIFICATIONLISTENER
+ NS_DECL_NSIMSGTRAITCLASSIFICATIONLISTENER
+
+ NS_IMETHOD WriteToFolderCacheElem(nsIMsgFolderCacheElement* element);
+ NS_IMETHOD ReadFromFolderCacheElem(nsIMsgFolderCacheElement* element);
+
+ nsresult CreateDirectoryForFolder(nsIFile** result);
+ nsresult CreateBackupDirectory(nsIFile** result);
+ nsresult GetBackupSummaryFile(nsIFile** result, const nsACString& newName);
+ nsresult GetMsgPreviewTextFromStream(nsIMsgDBHdr* msgHdr,
+ nsIInputStream* stream);
+ nsresult HandleAutoCompactEvent(nsIMsgWindow* aMsgWindow);
+ static int gIsEnglishApp;
+
+ protected:
+ virtual ~nsMsgDBFolder();
+
+ virtual nsresult CreateBaseMessageURI(const nsACString& aURI);
+
+ void compressQuotesInMsgSnippet(const nsString& aMessageText,
+ nsAString& aCompressedQuotesStr);
+ void decodeMsgSnippet(const nsACString& aEncodingType, bool aIsComplete,
+ nsCString& aMsgSnippet);
+
+ // helper routine to parse the URI and update member variables
+ nsresult parseURI(bool needServer = false);
+ nsresult GetBaseStringBundle(nsIStringBundle** aBundle);
+ nsresult GetStringFromBundle(const char* msgName, nsString& aResult);
+ nsresult ThrowConfirmationPrompt(nsIMsgWindow* msgWindow,
+ const nsAString& confirmString,
+ bool* confirmed);
+ nsresult GetWarnFilterChanged(bool* aVal);
+ nsresult SetWarnFilterChanged(bool aVal);
+ nsresult CreateCollationKey(const nsString& aSource, uint8_t** aKey,
+ uint32_t* aLength);
+
+ // All children will override this to create the right class of object.
+ virtual nsresult CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) = 0;
+ virtual nsresult ReadDBFolderInfo(bool force);
+ virtual nsresult FlushToFolderCache();
+ virtual nsresult GetDatabase() = 0;
+ virtual nsresult SendFlagNotifications(nsIMsgDBHdr* item, uint32_t oldFlags,
+ uint32_t newFlags);
+
+ // Overriden by IMAP to handle gmail hack.
+ virtual nsresult GetOfflineFileStream(nsMsgKey msgKey, uint64_t* offset,
+ uint32_t* size,
+ nsIInputStream** aFileStream);
+
+ nsresult CheckWithNewMessagesStatus(bool messageAdded);
+ void UpdateNewMessages();
+ nsresult OnHdrAddedOrDeleted(nsIMsgDBHdr* hdrChanged, bool added);
+ nsresult CreateFileForDB(const nsAString& userLeafName, nsIFile* baseDir,
+ nsIFile** dbFile);
+
+ nsresult GetFolderCacheKey(nsIFile** aFile);
+ nsresult GetFolderCacheElemFromFile(nsIFile* file,
+ nsIMsgFolderCacheElement** cacheElement);
+ nsresult AddDirectorySeparator(nsIFile* path);
+ nsresult CheckIfFolderExists(const nsAString& newFolderName,
+ nsIMsgFolder* parentFolder,
+ nsIMsgWindow* msgWindow);
+ bool ConfirmAutoFolderRename(nsIMsgWindow* aMsgWindow,
+ const nsString& aOldName,
+ const nsString& aNewName);
+
+ // Returns true if: a) there is no need to prompt or b) the user is already
+ // logged in or c) the user logged in successfully.
+ static bool PromptForMasterPasswordIfNecessary();
+
+ // Offline support methods. Used by IMAP and News folders, but not local
+ // folders.
+ nsresult StartNewOfflineMessage();
+ nsresult WriteStartOfNewLocalMessage();
+ nsresult EndNewOfflineMessage(nsresult status);
+
+ nsresult AutoCompact(nsIMsgWindow* aWindow);
+ // this is a helper routine that ignores whether nsMsgMessageFlags::Offline is
+ // set for the folder
+ nsresult MsgFitsDownloadCriteria(nsMsgKey msgKey, bool* result);
+ nsresult GetPromptPurgeThreshold(bool* aPrompt);
+ nsresult GetPurgeThreshold(int32_t* aThreshold);
+ nsresult ApplyRetentionSettings(bool deleteViaFolder);
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult AddMarkAllReadUndoAction(
+ nsIMsgWindow* msgWindow, nsMsgKey* thoseMarked, uint32_t numMarked);
+
+ nsresult PerformBiffNotifications(
+ void); // if there are new, non spam messages, do biff
+
+ // Helper function for Move code to call to update the MRU and MRM time.
+ void UpdateTimestamps(bool allowUndo);
+ void SetMRUTime();
+ void SetMRMTime();
+ /**
+ * Clear all processing flags, presumably because message keys are no longer
+ * valid.
+ */
+ void ClearProcessingFlags();
+
+ nsresult NotifyHdrsNotBeingClassified();
+ static nsresult BuildFolderSortKey(nsIMsgFolder* aFolder,
+ nsTArray<uint8_t>& aKey);
+ /**
+ * Produce an array of messages ordered like the input keys.
+ */
+ nsresult MessagesInKeyOrder(nsTArray<nsMsgKey> const& aKeyArray,
+ nsIMsgFolder* srcFolder,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& messages);
+ nsCString mURI;
+
+ nsCOMPtr<nsIMsgDatabase> mDatabase;
+ nsCOMPtr<nsIMsgDatabase> mBackupDatabase;
+ bool mAddListener;
+ bool mNewMessages;
+ bool mGettingNewMessages;
+ nsMsgKey mLastMessageLoaded;
+
+ /*
+ * Start of offline-message-writing vars.
+ * These track offline message writing for IMAP and News folders.
+ * But *not* for local folders, which do their own thing.
+ * They are set up by StartNewOfflineMessage() and cleaned up
+ * by EndNewOfflineMessage().
+ * IMAP folder also uses these vars when saving messages to disk.
+ */
+
+ // The header of the message currently being written.
+ nsCOMPtr<nsIMsgDBHdr> m_offlineHeader;
+ int32_t m_numOfflineMsgLines;
+ // Number of bytes added due to add X-Mozilla-* headers.
+ int32_t m_bytesAddedToLocalMsg;
+ // This is currently used when we do a save as of an imap or news message..
+ // Also used by IMAP/News offline messsage writing.
+ nsCOMPtr<nsIOutputStream> m_tempMessageStream;
+ // The number of bytes written to m_tempMessageStream so far.
+ uint32_t m_tempMessageStreamBytesWritten;
+
+ /*
+ * End of offline message tracking vars
+ */
+
+ nsCOMPtr<nsIMsgRetentionSettings> m_retentionSettings;
+ nsCOMPtr<nsIMsgDownloadSettings> m_downloadSettings;
+ static nsrefcnt mInstanceCount;
+
+ uint32_t mFlags;
+ nsWeakPtr mParent; // This won't be refcounted for ownership reasons.
+ int32_t mNumUnreadMessages; /* count of unread messages (-1 means unknown; -2
+ means unknown but we already tried to find
+ out.) */
+ int32_t mNumTotalMessages; /* count of existing messages. */
+ bool mNotifyCountChanges;
+ int64_t mExpungedBytes;
+ nsCOMArray<nsIMsgFolder> mSubFolders;
+ nsTObserverArray<nsCOMPtr<nsIFolderListener>> mListeners;
+
+ bool mInitializedFromCache;
+ nsISupports* mSemaphoreHolder; // set when the folder is being written to
+ // Due to ownership issues, this won't be
+ // AddRef'd.
+
+ nsWeakPtr mServer;
+
+ // These values are used for tricking the front end into thinking that we have
+ // more messages than are really in the DB. This is usually after and IMAP
+ // message copy where we don't want to do an expensive select until the user
+ // actually opens that folder
+ int32_t mNumPendingUnreadMessages;
+ int32_t mNumPendingTotalMessages;
+ int64_t mFolderSize;
+
+ int32_t mNumNewBiffMessages;
+
+ // these are previous set of new msgs, which we might
+ // want to run junk controls on. This is in addition to "new" hdrs
+ // in the db, which might get cleared because the user clicked away
+ // from the folder.
+ nsTArray<nsMsgKey> m_saveNewMsgs;
+
+ // These are the set of new messages for a folder who has had
+ // its db closed, without the user reading the folder. This
+ // happens with pop3 mail filtered to a different local folder.
+ nsTArray<nsMsgKey> m_newMsgs;
+
+ //
+ // stuff from the uri
+ //
+ bool mHaveParsedURI; // is the URI completely parsed?
+ bool mIsServerIsValid;
+ bool mIsServer;
+ nsString mName;
+ nsString mOriginalName;
+ nsCOMPtr<nsIFile> mPath;
+ nsCString mBaseMessageURI; // The uri with the message scheme
+
+ // static stuff for cross-instance objects like atoms
+ static nsrefcnt gInstanceCount;
+
+ static nsresult initializeStrings();
+ static nsresult createCollationKeyGenerator();
+
+ static nsString kLocalizedInboxName;
+ static nsString kLocalizedTrashName;
+ static nsString kLocalizedSentName;
+ static nsString kLocalizedDraftsName;
+ static nsString kLocalizedTemplatesName;
+ static nsString kLocalizedUnsentName;
+ static nsString kLocalizedJunkName;
+ static nsString kLocalizedArchivesName;
+
+ static nsString kLocalizedBrandShortName;
+
+ static mozilla::UniquePtr<mozilla::intl::Collator> gCollationKeyGenerator;
+ static bool gInitializeStringsDone;
+
+ // store of keys that have a processing flag set
+ struct {
+ uint32_t bit;
+ nsMsgKeySetU* keys;
+ } mProcessingFlag[nsMsgProcessingFlags::NumberOfFlags];
+
+ // list of nsIMsgDBHdrs for messages to process post-bayes
+ nsTArray<RefPtr<nsIMsgDBHdr>> mPostBayesMessagesToFilter;
+
+ /**
+ * The list of message keys that have been classified for msgsClassified
+ * batch notification purposes. We add to this list in OnMessageClassified
+ * when we are told about a classified message (a URI is provided), and we
+ * notify for the list and clear it when we are told all the messages in
+ * the batch were classified (a URI is not provided).
+ */
+ nsTArray<nsMsgKey> mClassifiedMsgKeys;
+ // Is the current bayes filtering doing junk classification?
+ bool mBayesJunkClassifying;
+ // Is the current bayes filtering doing trait classification?
+ bool mBayesTraitClassifying;
+};
+
+// This class is a kludge to allow nsMsgKeySet to be used with uint32_t keys
+class nsMsgKeySetU {
+ public:
+ // Creates an empty set.
+ static nsMsgKeySetU* Create();
+ ~nsMsgKeySetU();
+ // IsMember() returns whether the given key is a member of this set.
+ bool IsMember(nsMsgKey key);
+ // Add() adds the given key to the set. (Returns 1 if a change was
+ // made, 0 if it was already there, and negative on error.)
+ int Add(nsMsgKey key);
+ // Remove() removes the given article from the set.
+ int Remove(nsMsgKey key);
+ // Add the keys in the set to aArray.
+ nsresult ToMsgKeyArray(nsTArray<nsMsgKey>& aArray);
+
+ protected:
+ nsMsgKeySetU();
+ RefPtr<nsMsgKeySet> loKeySet;
+ RefPtr<nsMsgKeySet> hiKeySet;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgDBView.cpp b/comm/mailnews/base/src/nsMsgDBView.cpp
new file mode 100644
index 0000000000..6532b52e0e
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgDBView.cpp
@@ -0,0 +1,7411 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <algorithm>
+#include "msgCore.h"
+#include "prmem.h"
+#include "nsArrayUtils.h"
+#include "nsIMsgCustomColumnHandler.h"
+#include "nsMsgDBView.h"
+#include "nsISupports.h"
+#include "nsIMsgFolder.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgFolder.h"
+#include "MailNewsTypes2.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsImapCore.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgCopyService.h"
+#include "nsISpamSettings.h"
+#include "nsIMsgAccountManager.h"
+#include "nsTreeColumns.h"
+#include "nsTextFormatter.h"
+#include "nsIMimeConverter.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIPrompt.h"
+#include "nsIWindowWatcher.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMemory.h"
+#include "nsIAbManager.h"
+#include "nsIAbDirectory.h"
+#include "nsIAbCard.h"
+#include "mozilla/Components.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/DataTransfer.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "nsTArray.h"
+#include "mozilla/intl/OSPreferences.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+
+using namespace mozilla::mailnews;
+
+nsString nsMsgDBView::kHighestPriorityString;
+nsString nsMsgDBView::kHighPriorityString;
+nsString nsMsgDBView::kLowestPriorityString;
+nsString nsMsgDBView::kLowPriorityString;
+nsString nsMsgDBView::kNormalPriorityString;
+
+nsString nsMsgDBView::kReadString;
+nsString nsMsgDBView::kRepliedString;
+nsString nsMsgDBView::kForwardedString;
+nsString nsMsgDBView::kRedirectedString;
+nsString nsMsgDBView::kNewString;
+
+nsString nsMsgDBView::kTodayString;
+nsString nsMsgDBView::kYesterdayString;
+nsString nsMsgDBView::kLastWeekString;
+nsString nsMsgDBView::kTwoWeeksAgoString;
+nsString nsMsgDBView::kOldMailString;
+nsString nsMsgDBView::kFutureDateString;
+
+bool nsMsgDBView::m_dateFormatsInitialized = false;
+nsDateFormatSelectorComm nsMsgDBView::m_dateFormatDefault = kDateFormatShort;
+nsDateFormatSelectorComm nsMsgDBView::m_dateFormatThisWeek = kDateFormatShort;
+nsDateFormatSelectorComm nsMsgDBView::m_dateFormatToday = kDateFormatNone;
+
+nsString nsMsgDBView::m_connectorPattern;
+nsCOMPtr<nsIStringBundle> nsMsgDBView::mMessengerStringBundle;
+
+static const uint32_t kMaxNumSortColumns = 2;
+
+static void GetCachedName(const nsCString& unparsedString,
+ int32_t displayVersion, nsACString& cachedName);
+
+static void UpdateCachedName(nsIMsgDBHdr* aHdr, const char* header_field,
+ const nsAString& newName);
+
+// viewSortInfo is context data passed into the sort comparison functions -
+// FnSortIdUint32 for comparing numeric fields, FnSortIdKey for everything
+// else. If a comparison function finds two elements with equal primary
+// ordering, it'll call SecondaryCompare() to break the deadlock.
+// SecondaryCompare() uses the same comparison functions again, but using the
+// secondary key and potentially with different criteria (eg secondary sort
+// order might be different to primary). The viewSortInfo::isSecondarySort
+// flag lets the comparison function know not to call SecondaryCompare()
+// again (and again and again)...
+class viewSortInfo {
+ public:
+ nsMsgDBView* view;
+ nsIMsgDatabase* db; // Which db to use for collation compares.
+ bool isSecondarySort;
+ bool ascendingSort;
+};
+
+NS_IMPL_ISUPPORTS(nsMsgDBViewService, nsIMsgDBViewService)
+NS_IMETHODIMP nsMsgDBViewService::InitializeDBViewStrings() {
+ nsMsgDBView::InitializeLiterals();
+ nsMsgDBView::m_connectorPattern.Truncate();
+ nsMsgDBView::mMessengerStringBundle = nullptr;
+ // Initialize date display format.
+ if (!nsMsgDBView::m_dateFormatsInitialized) {
+ nsMsgDBView::InitDisplayFormats();
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ADDREF(nsMsgDBView)
+NS_IMPL_RELEASE(nsMsgDBView)
+
+NS_INTERFACE_MAP_BEGIN(nsMsgDBView)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMsgDBView)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgDBView)
+ NS_INTERFACE_MAP_ENTRY(nsIDBChangeListener)
+ NS_INTERFACE_MAP_ENTRY(nsITreeView)
+ NS_INTERFACE_MAP_ENTRY(nsIJunkMailClassificationListener)
+NS_INTERFACE_MAP_END
+
+nsMsgDBView::nsMsgDBView() {
+ // Member initializers and constructor code.
+ m_sortValid = false;
+ m_checkedCustomColumns = false;
+ m_sortOrder = nsMsgViewSortOrder::none;
+ m_sortType = nsMsgViewSortType::byNone;
+ m_viewFlags = nsMsgViewFlagsType::kNone;
+ m_secondarySort = nsMsgViewSortType::byId;
+ m_secondarySortOrder = nsMsgViewSortOrder::ascending;
+ m_cachedMsgKey = nsMsgKey_None;
+ m_currentlyDisplayedMsgKey = nsMsgKey_None;
+ m_currentlyDisplayedViewIndex = nsMsgViewIndex_None;
+ mNumSelectedRows = 0;
+ mSuppressMsgDisplay = false;
+ mSuppressCommandUpdating = false;
+ mSuppressChangeNotification = false;
+ mSummarizeFailed = false;
+ mSelectionSummarized = false;
+
+ mIsNews = false;
+ mIsRss = false;
+ mIsXFVirtual = false;
+ mDeleteModel = nsMsgImapDeleteModels::MoveToTrash;
+ m_deletingRows = false;
+ mNumMessagesRemainingInBatch = 0;
+ mShowSizeInLines = false;
+ mSortThreadsByRoot = false;
+
+ // mCommandsNeedDisablingBecauseOfSelection - A boolean that tell us if we
+ // needed to disable commands because of what's selected. If we're offline
+ // w/o a downloaded msg selected, or a dummy message was selected.
+ mCommandsNeedDisablingBecauseOfSelection = false;
+ mRemovingRow = false;
+ m_saveRestoreSelectionDepth = 0;
+ mRecentlyDeletedArrayIndex = 0;
+}
+
+void nsMsgDBView::InitializeLiterals() {
+ // Priority strings.
+ GetString(u"priorityHighest", kHighestPriorityString);
+ GetString(u"priorityHigh", kHighPriorityString);
+ GetString(u"priorityLowest", kLowestPriorityString);
+ GetString(u"priorityLow", kLowPriorityString);
+ GetString(u"priorityNormal", kNormalPriorityString);
+
+ GetString(u"read", kReadString);
+ GetString(u"replied", kRepliedString);
+ GetString(u"forwarded", kForwardedString);
+ GetString(u"redirected", kRedirectedString);
+ GetString(u"new", kNewString);
+
+ GetString(u"today", kTodayString);
+ GetString(u"yesterday", kYesterdayString);
+ GetString(u"last7Days", kLastWeekString);
+ GetString(u"last14Days", kTwoWeeksAgoString);
+ GetString(u"older", kOldMailString);
+ GetString(u"futureDate", kFutureDateString);
+}
+
+nsMsgDBView::~nsMsgDBView() {
+ if (m_db) m_db->RemoveListener(this);
+}
+
+// Helper function used to fetch strings from the messenger string bundle
+void nsMsgDBView::GetString(const char16_t* aStringName, nsAString& aValue) {
+ nsresult res = NS_ERROR_UNEXPECTED;
+
+ if (!nsMsgDBView::mMessengerStringBundle) {
+ static const char propertyURL[] = MESSENGER_STRING_URL;
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+
+ if (sBundleService)
+ res = sBundleService->CreateBundle(
+ propertyURL, getter_AddRefs(nsMsgDBView::mMessengerStringBundle));
+ }
+
+ if (nsMsgDBView::mMessengerStringBundle)
+ res = mMessengerStringBundle->GetStringFromName(
+ NS_ConvertUTF16toUTF8(aStringName).get(), aValue);
+
+ if (NS_FAILED(res)) {
+ aValue = aStringName;
+ }
+}
+
+// Helper function used to fetch localized strings from the prefs
+nsresult nsMsgDBView::GetPrefLocalizedString(const char* aPrefName,
+ nsString& aResult) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIPrefBranch> prefBranch;
+ nsCOMPtr<nsIPrefLocalizedString> pls;
+ nsString ucsval;
+
+ prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = prefBranch->GetComplexValue(
+ aPrefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(pls));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pls->ToString(getter_Copies(ucsval));
+ aResult = ucsval.get();
+ return rv;
+}
+
+nsresult nsMsgDBView::AppendKeywordProperties(const nsACString& keywords,
+ nsAString& properties,
+ bool* tagAdded) {
+ *tagAdded = false;
+ // Get the top most keyword's CSS selector and append that as a property.
+ nsresult rv;
+ if (!mTagService) {
+ mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCString topKey;
+ rv = mTagService->GetTopKey(keywords, topKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (topKey.IsEmpty()) return NS_OK;
+
+ nsString selector;
+ rv = mTagService->GetSelectorForKey(topKey, selector);
+ if (NS_SUCCEEDED(rv)) {
+ *tagAdded = true;
+ properties.Append(' ');
+ properties.Append(selector);
+ }
+ return rv;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// nsITreeView Implementation Methods (and helper methods)
+///////////////////////////////////////////////////////////////////////////
+
+static nsresult GetDisplayNameInAddressBook(const nsACString& emailAddress,
+ nsAString& displayName) {
+ nsresult rv;
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIAbCard> cardForAddress;
+ rv = abManager->CardForEmailAddress(emailAddress,
+ getter_AddRefs(cardForAddress));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (cardForAddress) {
+ bool preferDisplayName = true;
+ rv = cardForAddress->GetPropertyAsBool("PreferDisplayName", true,
+ &preferDisplayName);
+
+ if (NS_FAILED(rv) || preferDisplayName)
+ rv = cardForAddress->GetDisplayName(displayName);
+ }
+
+ return rv;
+}
+
+/*
+ * The unparsedString has following format:
+ * "version|displayname"
+ */
+static void GetCachedName(const nsCString& unparsedString,
+ int32_t displayVersion, nsACString& cachedName) {
+ nsresult err;
+
+ // Get version #.
+ int32_t cachedVersion = unparsedString.ToInteger(&err);
+ if (cachedVersion != displayVersion) return;
+
+ // Get cached name.
+ int32_t pos = unparsedString.FindChar('|');
+ if (pos != kNotFound) cachedName = Substring(unparsedString, pos + 1);
+}
+
+static void UpdateCachedName(nsIMsgDBHdr* aHdr, const char* header_field,
+ const nsAString& newName) {
+ nsCString newCachedName;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ int32_t currentDisplayNameVersion = 0;
+
+ prefs->GetIntPref("mail.displayname.version", &currentDisplayNameVersion);
+
+ // Save version number.
+ newCachedName.AppendInt(currentDisplayNameVersion);
+ newCachedName.Append('|');
+
+ // Save name.
+ newCachedName.Append(NS_ConvertUTF16toUTF8(newName));
+
+ aHdr->SetStringProperty(header_field, newCachedName);
+}
+
+nsresult nsMsgDBView::FetchAuthor(nsIMsgDBHdr* aHdr, nsAString& aSenderString) {
+ nsCString unparsedAuthor;
+ bool showCondensedAddresses = false;
+ int32_t currentDisplayNameVersion = 0;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ prefs->GetIntPref("mail.displayname.version", &currentDisplayNameVersion);
+ prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses);
+
+ aHdr->GetStringProperty("sender_name", unparsedAuthor);
+
+ // If the author is already computed, use it.
+ if (!unparsedAuthor.IsEmpty()) {
+ nsCString cachedDisplayName;
+ GetCachedName(unparsedAuthor, currentDisplayNameVersion, cachedDisplayName);
+ if (!cachedDisplayName.IsEmpty()) {
+ CopyUTF8toUTF16(cachedDisplayName, aSenderString);
+ return NS_OK;
+ }
+ }
+
+ nsCString author;
+ (void)aHdr->GetAuthor(getter_Copies(author));
+
+ nsCString headerCharset;
+ aHdr->GetEffectiveCharset(headerCharset);
+
+ nsString name;
+ nsCString emailAddress;
+ nsCOMArray<msgIAddressObject> addresses =
+ EncodedHeader(author, headerCharset.get());
+ bool multipleAuthors = addresses.Length() > 1;
+
+ ExtractFirstAddress(addresses, name, emailAddress);
+
+ if (showCondensedAddresses)
+ GetDisplayNameInAddressBook(emailAddress, aSenderString);
+
+ if (aSenderString.IsEmpty()) {
+ // We can't use the display name in the card; use the name contained in
+ // the header or email address.
+ if (name.IsEmpty()) {
+ CopyUTF8toUTF16(emailAddress, aSenderString);
+ } else {
+ int32_t atPos;
+ if ((atPos = name.FindChar('@')) == kNotFound ||
+ name.FindChar('.', atPos) == kNotFound) {
+ aSenderString = name;
+ } else {
+ // Found @ followed by a dot, so this looks like a spoofing case.
+ aSenderString = name;
+ aSenderString.AppendLiteral(" <");
+ AppendUTF8toUTF16(emailAddress, aSenderString);
+ aSenderString.Append('>');
+ }
+ }
+ }
+
+ if (multipleAuthors) {
+ aSenderString.AppendLiteral(" ");
+ nsAutoString val;
+ GetString(u"andOthers", val);
+ aSenderString.Append(val);
+ }
+
+ UpdateCachedName(aHdr, "sender_name", aSenderString);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchAccount(nsIMsgDBHdr* aHdr, nsAString& aAccount) {
+ nsCString accountKey;
+ nsresult rv = aHdr->GetAccountKey(getter_Copies(accountKey));
+
+ // Cache the account manager?
+ nsCOMPtr<nsIMsgAccountManager> accountManager(
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgAccount> account;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ if (!accountKey.IsEmpty())
+ rv = accountManager->GetAccount(accountKey, getter_AddRefs(account));
+
+ if (account) {
+ account->GetIncomingServer(getter_AddRefs(server));
+ } else {
+ nsCOMPtr<nsIMsgFolder> folder;
+ aHdr->GetFolder(getter_AddRefs(folder));
+ if (folder) folder->GetServer(getter_AddRefs(server));
+ }
+
+ if (server)
+ server->GetPrettyName(aAccount);
+ else
+ CopyASCIItoUTF16(accountKey, aAccount);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchRecipients(nsIMsgDBHdr* aHdr,
+ nsAString& aRecipientsString) {
+ nsCString recipients;
+ int32_t currentDisplayNameVersion = 0;
+ bool showCondensedAddresses = false;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ prefs->GetIntPref("mail.displayname.version", &currentDisplayNameVersion);
+ prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses);
+
+ aHdr->GetStringProperty("recipient_names", recipients);
+
+ if (!recipients.IsEmpty()) {
+ nsCString cachedRecipients;
+ GetCachedName(recipients, currentDisplayNameVersion, cachedRecipients);
+
+ // Recipients have already been cached, check if the addressbook
+ // was changed after cache.
+ if (!cachedRecipients.IsEmpty()) {
+ CopyUTF8toUTF16(cachedRecipients, aRecipientsString);
+ return NS_OK;
+ }
+ }
+
+ nsCString unparsedRecipients;
+ nsresult rv = aHdr->GetRecipients(getter_Copies(unparsedRecipients));
+
+ nsCString headerCharset;
+ aHdr->GetEffectiveCharset(headerCharset);
+
+ nsTArray<nsString> names;
+ nsTArray<nsCString> emails;
+ ExtractAllAddresses(EncodedHeader(unparsedRecipients, headerCharset.get()),
+ names, UTF16ArrayAdapter<>(emails));
+
+ uint32_t numAddresses = names.Length();
+
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+
+ // Go through each email address in the recipients and compute its
+ // display name.
+ for (uint32_t i = 0; i < numAddresses; i++) {
+ nsString recipient;
+ nsCString& curAddress = emails[i];
+ nsString& curName = names[i];
+
+ if (showCondensedAddresses)
+ GetDisplayNameInAddressBook(curAddress, recipient);
+
+ if (recipient.IsEmpty()) {
+ // We can't use the display name in the card; use the name contained in
+ // the header or email address.
+ if (curName.IsEmpty()) {
+ CopyUTF8toUTF16(curAddress, recipient);
+ } else {
+ int32_t atPos;
+ if ((atPos = curName.FindChar('@')) == kNotFound ||
+ curName.FindChar('.', atPos) == kNotFound) {
+ recipient = curName;
+ } else {
+ // Found @ followed by a dot, so this looks like a spoofing case.
+ recipient = curName;
+ recipient.AppendLiteral(" <");
+ AppendUTF8toUTF16(curAddress, recipient);
+ recipient.Append('>');
+ }
+ }
+ }
+
+ // Add ', ' between each recipient.
+ if (i != 0) aRecipientsString.AppendLiteral(u", ");
+
+ aRecipientsString.Append(recipient);
+ }
+
+ if (numAddresses == 0 && unparsedRecipients.FindChar(':') != kNotFound) {
+ // No addresses and a colon, so an empty group like
+ // "undisclosed-recipients: ;".
+ // Add group name so at least something displays.
+ nsString group;
+ CopyUTF8toUTF16(unparsedRecipients, group);
+ aRecipientsString.Assign(group);
+ }
+
+ UpdateCachedName(aHdr, "recipient_names", aRecipientsString);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchSubject(nsIMsgDBHdr* aMsgHdr, uint32_t aFlags,
+ nsAString& aValue) {
+ if (aFlags & nsMsgMessageFlags::HasRe) {
+ nsString subject;
+ aMsgHdr->GetMime2DecodedSubject(subject);
+ aValue.AssignLiteral("Re: ");
+ aValue.Append(subject);
+ } else {
+ aMsgHdr->GetMime2DecodedSubject(aValue);
+ }
+
+ return NS_OK;
+}
+
+// In case we want to play around with the date string, I've broken it out into
+// a separate routine. Set rcvDate to true to get the Received: date instead
+// of the Date: date.
+nsresult nsMsgDBView::FetchDate(nsIMsgDBHdr* aHdr, nsAString& aDateString,
+ bool rcvDate) {
+ PRTime dateOfMsg;
+ PRTime dateOfMsgLocal;
+ uint32_t rcvDateSecs;
+ nsresult rv;
+
+ // Silently return Date: instead if Received: is unavailable.
+ if (rcvDate) {
+ rv = aHdr->GetUint32Property("dateReceived", &rcvDateSecs);
+ if (rcvDateSecs != 0) Seconds2PRTime(rcvDateSecs, &dateOfMsg);
+ }
+
+ if (!rcvDate || rcvDateSecs == 0) rv = aHdr->GetDate(&dateOfMsg);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PRTime currentTime = PR_Now();
+ PRExplodedTime explodedCurrentTime;
+ PR_ExplodeTime(currentTime, PR_LocalTimeParameters, &explodedCurrentTime);
+ PRExplodedTime explodedMsgTime;
+ PR_ExplodeTime(dateOfMsg, PR_LocalTimeParameters, &explodedMsgTime);
+
+ // If the message is from today, don't show the date, only the time (3:15 pm).
+ // If the message is from the last week, show the day of the week
+ // (Mon 3:15 pm). In all other cases, show the full date (03/19/01 3:15 pm).
+
+ nsDateFormatSelectorComm dateFormat = m_dateFormatDefault;
+ if (explodedCurrentTime.tm_year == explodedMsgTime.tm_year &&
+ explodedCurrentTime.tm_month == explodedMsgTime.tm_month &&
+ explodedCurrentTime.tm_mday == explodedMsgTime.tm_mday) {
+ // Same day.
+ dateFormat = m_dateFormatToday;
+ } else if (currentTime > dateOfMsg) {
+ // The following chunk of code allows us to show a day instead of a number
+ // if the message was received within the last 7 days. i.e. Mon 5:10pm
+ // (depending on the mail.ui.display.dateformat.thisweek pref).
+ // The concrete format used is dependent on a preference setting
+ // (see InitDisplayFormats).
+ // Convert the times from GMT to local time
+ int64_t GMTLocalTimeShift =
+ PR_USEC_PER_SEC * int64_t(explodedCurrentTime.tm_params.tp_gmt_offset +
+ explodedCurrentTime.tm_params.tp_dst_offset);
+ currentTime += GMTLocalTimeShift;
+ dateOfMsgLocal = dateOfMsg + GMTLocalTimeShift;
+
+ // Find the most recent midnight.
+ int64_t todaysMicroSeconds = currentTime % PR_USEC_PER_DAY;
+ int64_t mostRecentMidnight = currentTime - todaysMicroSeconds;
+
+ // Most recent midnight minus 6 days.
+ int64_t mostRecentWeek = mostRecentMidnight - (PR_USEC_PER_DAY * 6);
+
+ // Was the message sent during the last week?
+ if (dateOfMsgLocal >= mostRecentWeek) dateFormat = m_dateFormatThisWeek;
+ }
+
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ switch (dateFormat) {
+ case kDateFormatNone:
+ rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg,
+ aDateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case kDateFormatLong:
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg,
+ aDateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case kDateFormatShort:
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg,
+ aDateString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ case kDateFormatWeekday: {
+ // We want weekday + time.
+ nsAutoString timeString;
+ nsAutoString weekdayString;
+ rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg,
+ timeString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mozilla::intl::DateTimeFormat::ComponentsBag components{};
+ components.weekday =
+ mozilla::Some(mozilla::intl::DateTimeFormat::Text::Short);
+ rv = mozilla::intl::AppDateTimeFormat::Format(
+ components, &explodedMsgTime, weekdayString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (nsMsgDBView::m_connectorPattern.IsEmpty()) {
+ nsAutoCString locale;
+ AutoTArray<nsCString, 10> regionalPrefsLocales;
+ mozilla::intl::LocaleService::GetInstance()->GetRegionalPrefsLocales(
+ regionalPrefsLocales);
+ locale.Assign(regionalPrefsLocales[0]);
+ nsAutoCString str;
+ mozilla::intl::OSPreferences::GetInstance()
+ ->GetDateTimeConnectorPattern(locale, str);
+ nsMsgDBView::m_connectorPattern = NS_ConvertUTF8toUTF16(str);
+ }
+
+ nsAutoString pattern(nsMsgDBView::m_connectorPattern);
+ int32_t ind = pattern.Find(u"{1}"_ns);
+ if (ind != kNotFound) {
+ pattern.Replace(ind, 3, weekdayString);
+ }
+ ind = pattern.Find(u"{0}"_ns);
+ if (ind != kNotFound) {
+ pattern.Replace(ind, 3, timeString);
+ }
+ aDateString = pattern;
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::FetchStatus(uint32_t aFlags, nsAString& aStatusString) {
+ if (aFlags & nsMsgMessageFlags::Replied)
+ aStatusString = kRepliedString;
+ else if (aFlags & nsMsgMessageFlags::Forwarded)
+ aStatusString = kForwardedString;
+ else if (aFlags & nsMsgMessageFlags::Redirected)
+ aStatusString = kRedirectedString;
+ else if (aFlags & nsMsgMessageFlags::New)
+ aStatusString = kNewString;
+ else if (aFlags & nsMsgMessageFlags::Read)
+ aStatusString = kReadString;
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchSize(nsIMsgDBHdr* aHdr, nsAString& aSizeString) {
+ nsresult rv;
+ nsAutoString formattedSizeString;
+ uint32_t msgSize = 0;
+
+ // For news, show the line count, not the size if the user wants so.
+ if (mShowSizeInLines) {
+ aHdr->GetLineCount(&msgSize);
+ formattedSizeString.AppendInt(msgSize);
+ } else {
+ uint32_t flags = 0;
+
+ aHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial)
+ aHdr->GetUint32Property("onlineSize", &msgSize);
+
+ if (msgSize == 0) aHdr->GetMessageSize(&msgSize);
+
+ rv = FormatFileSize(msgSize, true, formattedSizeString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ aSizeString = formattedSizeString;
+ // The formattingString Length includes the null terminator byte!
+ if (!formattedSizeString.Last())
+ aSizeString.SetLength(formattedSizeString.Length() - 1);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchPriority(nsIMsgDBHdr* aHdr,
+ nsAString& aPriorityString) {
+ nsMsgPriorityValue priority = nsMsgPriority::notSet;
+ aHdr->GetPriority(&priority);
+
+ switch (priority) {
+ case nsMsgPriority::highest:
+ aPriorityString = kHighestPriorityString;
+ break;
+ case nsMsgPriority::high:
+ aPriorityString = kHighPriorityString;
+ break;
+ case nsMsgPriority::low:
+ aPriorityString = kLowPriorityString;
+ break;
+ case nsMsgPriority::lowest:
+ aPriorityString = kLowestPriorityString;
+ break;
+ case nsMsgPriority::normal:
+ aPriorityString = kNormalPriorityString;
+ break;
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FetchKeywords(nsIMsgDBHdr* aHdr,
+ nsACString& keywordString) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ nsresult rv = NS_OK;
+ if (!mTagService) {
+ mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsCString keywords;
+ aHdr->GetStringProperty("keywords", keywords);
+ keywordString = keywords;
+ return NS_OK;
+}
+
+// If the row is a collapsed thread, we optionally roll-up the keywords in all
+// the messages in the thread, otherwise, return just the keywords for the row.
+nsresult nsMsgDBView::FetchRowKeywords(nsMsgViewIndex aRow, nsIMsgDBHdr* aHdr,
+ nsACString& keywordString) {
+ nsresult rv = FetchKeywords(aHdr, keywordString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool cascadeKeywordsUp = true;
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ prefs->GetBoolPref("mailnews.display_reply_tag_colors_for_collapsed_threads",
+ &cascadeKeywordsUp);
+
+ if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ cascadeKeywordsUp) {
+ if ((m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) &&
+ (m_flags[aRow] & nsMsgMessageFlags::Elided)) {
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCString moreKeywords;
+ for (uint32_t index = 0; index < numChildren; index++) {
+ thread->GetChildHdrAt(index, getter_AddRefs(msgHdr));
+ rv = FetchKeywords(msgHdr, moreKeywords);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!keywordString.IsEmpty() && !moreKeywords.IsEmpty())
+ keywordString.Append(' ');
+
+ keywordString.Append(moreKeywords);
+ }
+ }
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::FetchTags(nsIMsgDBHdr* aHdr, nsAString& aTagString) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ nsresult rv = NS_OK;
+ if (!mTagService) {
+ mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsString tags;
+ nsCString keywords;
+ aHdr->GetStringProperty("keywords", keywords);
+
+ nsTArray<nsCString> keywordsArray;
+ ParseString(keywords, ' ', keywordsArray);
+ nsAutoString tag;
+
+ for (uint32_t i = 0; i < keywordsArray.Length(); i++) {
+ rv = mTagService->GetTagForKey(keywordsArray[i], tag);
+ if (NS_SUCCEEDED(rv) && !tag.IsEmpty()) {
+ if (!tags.IsEmpty()) tags.Append((char16_t)' ');
+
+ tags.Append(tag);
+ }
+ }
+
+ aTagString = tags;
+ return NS_OK;
+}
+
+/**
+ * Lowercase the email and remove a possible plus addressing part.
+ * E.g. John+test@example.com -> john@example.com.
+ */
+static void ToLowerCaseDropPlusAddessing(nsCString& aEmail) {
+ ToLowerCase(aEmail);
+ int32_t indPlus;
+ if ((indPlus = aEmail.FindChar('+')) == kNotFound) return;
+ int32_t indAt;
+ indAt = aEmail.FindChar('@', indPlus);
+ if (indAt == kNotFound) return;
+ aEmail.ReplaceLiteral(indPlus, indAt - indPlus, "");
+}
+
+bool nsMsgDBView::IsOutgoingMsg(nsIMsgDBHdr* aHdr) {
+ nsString author;
+ aHdr->GetMime2DecodedAuthor(author);
+
+ nsCString emailAddress;
+ nsString name;
+ ExtractFirstAddress(DecodedHeader(author), name, emailAddress);
+ ToLowerCaseDropPlusAddessing(emailAddress);
+ return mEmails.Contains(emailAddress);
+}
+
+// If you call SaveAndClearSelection make sure to call RestoreSelection(),
+// otherwise m_saveRestoreSelectionDepth will be incorrect and will lead to
+// selection msg problems.
+nsresult nsMsgDBView::SaveAndClearSelection(nsMsgKey* aCurrentMsgKey,
+ nsTArray<nsMsgKey>& aMsgKeyArray) {
+ // Always return a value in the first parameter.
+ if (aCurrentMsgKey) *aCurrentMsgKey = nsMsgKey_None;
+
+ // We don't do anything on nested Save / Restore calls.
+ m_saveRestoreSelectionDepth++;
+ if (m_saveRestoreSelectionDepth != 1) return NS_OK;
+
+ if (!mTreeSelection) return NS_OK;
+
+ // First, freeze selection.
+ mTreeSelection->SetSelectEventsSuppressed(true);
+
+ // Second, save the current index.
+ if (aCurrentMsgKey) {
+ int32_t currentIndex;
+ if (NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(&currentIndex)) &&
+ currentIndex >= 0 && uint32_t(currentIndex) < GetSize())
+ *aCurrentMsgKey = m_keys[currentIndex];
+ else
+ *aCurrentMsgKey = nsMsgKey_None;
+ }
+
+ // Third, get an array of view indices for the selection.
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ int32_t numIndices = selection.Length();
+ aMsgKeyArray.SetLength(numIndices);
+
+ // Now store the msg key for each selected item.
+ nsMsgKey msgKey;
+ for (int32_t index = 0; index < numIndices; index++) {
+ msgKey = m_keys[selection[index]];
+ aMsgKeyArray[index] = msgKey;
+ }
+
+ // Clear the selection, we'll manually restore it later.
+ if (mTreeSelection) mTreeSelection->ClearSelection();
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::RestoreSelection(nsMsgKey aCurrentMsgKey,
+ nsTArray<nsMsgKey>& aMsgKeyArray) {
+ // We don't do anything on nested Save / Restore calls.
+ m_saveRestoreSelectionDepth--;
+ if (m_saveRestoreSelectionDepth) return NS_OK;
+
+ // Don't assert.
+ if (!mTreeSelection) return NS_OK;
+
+ // Turn our message keys into corresponding view indices.
+ int32_t arraySize = aMsgKeyArray.Length();
+ nsMsgViewIndex currentViewPosition = nsMsgViewIndex_None;
+ nsMsgViewIndex newViewPosition = nsMsgViewIndex_None;
+
+ // If we are threaded, we need to do a little more work
+ // we need to find (and expand) all the threads that contain messages
+ // that we had selected before.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ for (int32_t index = 0; index < arraySize; index++)
+ FindKey(aMsgKeyArray[index], true /* expand */);
+ }
+
+ for (int32_t index = 0; index < arraySize; index++) {
+ newViewPosition = FindKey(aMsgKeyArray[index], false);
+ // Add the index back to the selection.
+ if (newViewPosition != nsMsgViewIndex_None)
+ mTreeSelection->ToggleSelect(newViewPosition);
+ }
+
+ // Make sure the currentView was preserved.
+ if (aCurrentMsgKey != nsMsgKey_None)
+ currentViewPosition = FindKey(aCurrentMsgKey, true);
+
+ if (mJSTree) mJSTree->SetCurrentIndex(currentViewPosition);
+
+ // Make sure the current message is once again visible in the thread pane
+ // so we don't have to go search for it in the thread pane
+ if (currentViewPosition != nsMsgViewIndex_None) {
+ if (mJSTree) {
+ mJSTree->EnsureRowIsVisible(currentViewPosition);
+ } else if (mTree) {
+ mTree->EnsureRowIsVisible(currentViewPosition);
+ }
+ }
+
+ // Unfreeze selection.
+ mTreeSelection->SetSelectEventsSuppressed(false);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GenerateURIForMsgKey(nsMsgKey aMsgKey,
+ nsIMsgFolder* folder,
+ nsACString& aURI) {
+ NS_ENSURE_ARG(folder);
+ return folder->GenerateMessageURI(aMsgKey, aURI);
+}
+
+nsresult nsMsgDBView::GetMessageEnumerator(nsIMsgEnumerator** enumerator) {
+ return m_db->EnumerateMessages(enumerator);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsEditable(int32_t row, nsTreeColumn* col, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(col);
+ NS_ENSURE_ARG_POINTER(_retval);
+ // Attempt to retrieve a custom column handler. If it exists call it and
+ // return.
+ const nsAString& colID = col->GetId();
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+
+ if (colHandler) {
+ colHandler->IsEditable(row, col, _retval);
+ return NS_OK;
+ }
+
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetCellValue(int32_t row, nsTreeColumn* col,
+ const nsAString& value) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetCellText(int32_t row, nsTreeColumn* col,
+ const nsAString& value) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetRowCount(int32_t* aRowCount) {
+ *aRowCount = GetSize();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSelection(nsITreeSelection** aSelection) {
+ NS_IF_ADDREF(*aSelection = mTreeSelection);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSelection(nsITreeSelection* aSelection) {
+ mTreeSelection = aSelection;
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::UpdateDisplayMessage(nsMsgViewIndex viewPosition) {
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ if (!commandUpdater) return NS_OK;
+
+ if (!IsValidIndex(viewPosition)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ // Get the subject and the folder for the message and inform the front
+ // end that we changed the message we are currently displaying.
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(viewPosition, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString subject;
+ if (viewPosition >= (nsMsgViewIndex)m_flags.Length())
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ FetchSubject(msgHdr, m_flags[viewPosition], subject);
+
+ nsCString keywords;
+ rv = msgHdr->GetStringProperty("keywords", keywords);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> folder = m_viewFolder ? m_viewFolder : m_folder;
+
+ commandUpdater->DisplayMessageChanged(folder, subject, keywords);
+
+ if (folder) {
+ if (viewPosition >= (nsMsgViewIndex)m_keys.Length())
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ rv = folder->SetLastMessageLoaded(m_keys[viewPosition]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SelectionChangedXPCOM() {
+ // If the currentSelection changed then we have a message to display -
+ // not if we are in the middle of deleting rows.
+ if (m_deletingRows) return NS_OK;
+
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+
+ bool commandsNeedDisablingBecauseOfSelection = false;
+
+ if (!selection.IsEmpty()) {
+ if (WeAreOffline())
+ commandsNeedDisablingBecauseOfSelection = !OfflineMsgSelected(selection);
+
+ if (!NonDummyMsgSelected(selection))
+ commandsNeedDisablingBecauseOfSelection = true;
+ }
+
+ bool selectionSummarized = false;
+ mSummarizeFailed = false;
+ // Let the front-end adjust the message pane appropriately with either
+ // the message body, or a summary of the selection.
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ if (commandUpdater) {
+ commandUpdater->SummarizeSelection(&selectionSummarized);
+ // Check if the selection was not summarized, but we expected it to be,
+ // and if so, remember it so GetHeadersFromSelection won't include
+ // the messages in collapsed threads.
+ if (!selectionSummarized &&
+ (selection.Length() > 1 ||
+ (selection.Length() == 1 &&
+ m_flags[selection[0]] & nsMsgMessageFlags::Elided &&
+ OperateOnMsgsInCollapsedThreads()))) {
+ mSummarizeFailed = true;
+ }
+ }
+
+ bool summaryStateChanged = selectionSummarized != mSelectionSummarized;
+ mSelectionSummarized = selectionSummarized;
+
+ if (!mTreeSelection || selection.Length() != 1 || selectionSummarized) {
+ // If we have zero or multiple items selected, we shouldn't be displaying
+ // any message.
+ m_currentlyDisplayedMsgKey = nsMsgKey_None;
+ m_currentlyDisplayedMsgUri.Truncate();
+ m_currentlyDisplayedViewIndex = nsMsgViewIndex_None;
+ }
+
+ // Determine if we need to push command update notifications out to the UI.
+ // We need to push a command update notification iff, one of the following
+ // conditions are met
+ // (1) the selection went from 0 to 1
+ // (2) it went from 1 to 0
+ // (3) it went from 1 to many
+ // (4) it went from many to 1 or 0
+ // (5) a different msg was selected - perhaps it was offline or not,
+ // matters only when we are offline
+ // (6) we did a forward/back, or went from having no history to having
+ // history - not sure how to tell this.
+ // (7) whether the selection was summarized or not changed.
+
+ // I think we're going to need to keep track of whether forward/back were
+ // enabled/should be enabled, and when this changes, force a command update.
+
+ if (!summaryStateChanged &&
+ (selection.Length() == mNumSelectedRows ||
+ (selection.Length() > 1 && mNumSelectedRows > 1)) &&
+ commandsNeedDisablingBecauseOfSelection ==
+ mCommandsNeedDisablingBecauseOfSelection) {
+ // Don't update commands if we're suppressing them, or if we're removing
+ // rows, unless it was the last row.
+ } else if (!mSuppressCommandUpdating && commandUpdater &&
+ (!mRemovingRow || GetSize() == 0)) {
+ commandUpdater->UpdateCommandStatus();
+ }
+
+ mCommandsNeedDisablingBecauseOfSelection =
+ commandsNeedDisablingBecauseOfSelection;
+ mNumSelectedRows = selection.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetRowProperties(int32_t index, nsAString& properties) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ // This is where we tell the tree to apply styles to a particular row.
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = NS_OK;
+
+ rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+
+ if (NS_FAILED(rv) || !msgHdr) {
+ ClearHdrCache();
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ if (IsOutgoingMsg(msgHdr)) properties.AppendLiteral(" outgoing");
+
+ nsCString keywordProperty;
+ FetchRowKeywords(index, msgHdr, keywordProperty);
+ bool tagAdded = false;
+ if (!keywordProperty.IsEmpty()) {
+ AppendKeywordProperties(keywordProperty, properties, &tagAdded);
+ }
+ if (tagAdded) {
+ properties.AppendLiteral(" tagged");
+ } else {
+ properties.AppendLiteral(" untagged");
+ }
+
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+
+ if (!(flags & nsMsgMessageFlags::Read))
+ properties.AppendLiteral(" unread");
+ else
+ properties.AppendLiteral(" read");
+
+ if (flags & nsMsgMessageFlags::Replied) properties.AppendLiteral(" replied");
+
+ if (flags & nsMsgMessageFlags::Forwarded)
+ properties.AppendLiteral(" forwarded");
+
+ if (flags & nsMsgMessageFlags::Redirected)
+ properties.AppendLiteral(" redirected");
+
+ if (flags & nsMsgMessageFlags::New) properties.AppendLiteral(" new");
+
+ if (m_flags[index] & nsMsgMessageFlags::Marked)
+ properties.AppendLiteral(" flagged");
+
+ // Give the custom column handlers a chance to style the row.
+ for (int i = 0; i < m_customColumnHandlers.Count(); i++) {
+ nsString extra;
+ m_customColumnHandlers[i]->GetRowProperties(index, extra);
+ if (!extra.IsEmpty()) {
+ properties.Append(' ');
+ properties.Append(extra);
+ }
+ }
+
+ // For threaded display add the ignoreSubthread property to the
+ // subthread top row (this row). For non-threaded add it to all rows.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ (flags & nsMsgMessageFlags::Ignored)) {
+ properties.AppendLiteral(" ignoreSubthread");
+ } else {
+ bool ignored;
+ msgHdr->GetIsKilled(&ignored);
+ if (ignored) properties.AppendLiteral(" ignoreSubthread");
+ }
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+
+ if ((flags & nsMsgMessageFlags::Offline) ||
+ (localFolder && !(flags & nsMsgMessageFlags::Partial)))
+ properties.AppendLiteral(" offline");
+
+ if (flags & nsMsgMessageFlags::Attachment)
+ properties.AppendLiteral(" attach");
+
+ if ((mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) &&
+ (flags & nsMsgMessageFlags::IMAPDeleted))
+ properties.AppendLiteral(" imapdeleted");
+
+ nsCString imageSize;
+ msgHdr->GetStringProperty("imageSize", imageSize);
+ if (!imageSize.IsEmpty()) properties.AppendLiteral(" hasimage");
+
+ nsCString junkScoreStr;
+ msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ if (!junkScoreStr.IsEmpty()) {
+ if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE)
+ properties.AppendLiteral(" junk");
+ else
+ properties.AppendLiteral(" notjunk");
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed.");
+ }
+
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingIndex(index, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ uint32_t numUnreadChildren;
+ thread->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) properties.AppendLiteral(" hasUnread");
+
+ // For threaded display add the ignore/watch properties to the
+ // thread top row. For non-threaded add it to all rows.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) ||
+ ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ (m_flags[index] & MSG_VIEW_FLAG_ISTHREAD))) {
+ thread->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Watched)
+ properties.AppendLiteral(" watch");
+ if (flags & nsMsgMessageFlags::Ignored)
+ properties.AppendLiteral(" ignore");
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetColumnProperties(nsTreeColumn* col, nsAString& properties) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCellProperties(int32_t aRow, nsTreeColumn* col,
+ nsAString& properties) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ // This is where we tell the tree to apply styles to a particular row
+ // i.e. if the row is an unread message...
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = NS_OK;
+
+ rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+
+ if (NS_FAILED(rv) || !msgHdr) {
+ ClearHdrCache();
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ const nsAString& colID = col->GetId();
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+ if (colHandler != nullptr) {
+ colHandler->GetCellProperties(aRow, col, properties);
+ } else if (colID[0] == 'c') {
+ // Correspondent.
+ if (IsOutgoingMsg(msgHdr))
+ properties.AssignLiteral("outgoing");
+ else
+ properties.AssignLiteral("incoming");
+ }
+
+ if (!properties.IsEmpty()) properties.Append(' ');
+
+ properties.Append(mMessageType);
+
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+
+ if (!(flags & nsMsgMessageFlags::Read))
+ properties.AppendLiteral(" unread");
+ else
+ properties.AppendLiteral(" read");
+
+ if (flags & nsMsgMessageFlags::Replied) properties.AppendLiteral(" replied");
+
+ if (flags & nsMsgMessageFlags::Forwarded)
+ properties.AppendLiteral(" forwarded");
+
+ if (flags & nsMsgMessageFlags::Redirected)
+ properties.AppendLiteral(" redirected");
+
+ if (flags & nsMsgMessageFlags::New) properties.AppendLiteral(" new");
+
+ if (m_flags[aRow] & nsMsgMessageFlags::Marked)
+ properties.AppendLiteral(" flagged");
+
+ // For threaded display add the ignoreSubthread property to the
+ // subthread top row (this row). For non-threaded add it to all rows.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ (flags & nsMsgMessageFlags::Ignored)) {
+ properties.AppendLiteral(" ignoreSubthread");
+ } else {
+ bool ignored;
+ msgHdr->GetIsKilled(&ignored);
+ if (ignored) properties.AppendLiteral(" ignoreSubthread");
+ }
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+
+ if ((flags & nsMsgMessageFlags::Offline) ||
+ (localFolder && !(flags & nsMsgMessageFlags::Partial)))
+ properties.AppendLiteral(" offline");
+
+ if (flags & nsMsgMessageFlags::Attachment)
+ properties.AppendLiteral(" attach");
+
+ if ((mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) &&
+ (flags & nsMsgMessageFlags::IMAPDeleted))
+ properties.AppendLiteral(" imapdeleted");
+
+ nsCString imageSize;
+ msgHdr->GetStringProperty("imageSize", imageSize);
+ if (!imageSize.IsEmpty()) properties.AppendLiteral(" hasimage");
+
+ nsCString junkScoreStr;
+ msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ if (!junkScoreStr.IsEmpty()) {
+ if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE)
+ properties.AppendLiteral(" junk");
+ else
+ properties.AppendLiteral(" notjunk");
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed.");
+ }
+
+ nsCString keywords;
+ FetchRowKeywords(aRow, msgHdr, keywords);
+ bool tagAdded = false;
+ if (!keywords.IsEmpty()) {
+ AppendKeywordProperties(keywords, properties, &tagAdded);
+ }
+ if (tagAdded) {
+ properties.AppendLiteral(" tagged");
+ } else {
+ properties.AppendLiteral(" untagged");
+ }
+
+ // This is a double fetch of the keywords property since we also fetch
+ // it for the tags - do we want to do this?
+ // I'm not sure anyone uses the kw- property, though it could be nice
+ // for people wanting to extend the thread pane.
+ nsCString keywordProperty;
+ msgHdr->GetStringProperty("keywords", keywordProperty);
+ if (!keywordProperty.IsEmpty()) {
+ NS_ConvertUTF8toUTF16 keywords(keywordProperty);
+ int32_t spaceIndex = 0;
+ do {
+ spaceIndex = keywords.FindChar(' ');
+ int32_t endOfKeyword =
+ (spaceIndex == -1) ? keywords.Length() : spaceIndex;
+ properties.AppendLiteral(" kw-");
+ properties.Append(StringHead(keywords, endOfKeyword));
+ if (spaceIndex > 0) keywords.Cut(0, endOfKeyword + 1);
+ } while (spaceIndex > 0);
+ }
+
+#ifdef SUPPORT_PRIORITY_COLORS
+ // Add special styles for priority.
+ nsMsgPriorityValue priority;
+ msgHdr->GetPriority(&priority);
+ switch (priority) {
+ case nsMsgPriority::highest:
+ properties.append(" priority-highest");
+ break;
+ case nsMsgPriority::high:
+ properties.append(" priority-high");
+ break;
+ case nsMsgPriority::low:
+ properties.append(" priority-low");
+ break;
+ case nsMsgPriority::lowest:
+ properties.append(" priority-lowest");
+ break;
+ default:
+ break;
+ }
+#endif
+
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ uint32_t numUnreadChildren;
+ thread->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) properties.AppendLiteral(" hasUnread");
+
+ // For threaded display add the ignore/watch properties to the
+ // thread top row. For non-threaded add it to all rows.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) ||
+ ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD))) {
+ thread->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Watched)
+ properties.AppendLiteral(" watch");
+ if (flags & nsMsgMessageFlags::Ignored)
+ properties.AppendLiteral(" ignore");
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsContainer(int32_t index, bool* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ uint32_t flags = m_flags[index];
+ *_retval = !!(flags & MSG_VIEW_FLAG_HASCHILDREN);
+ } else {
+ *_retval = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsContainerOpen(int32_t index, bool* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ uint32_t flags = m_flags[index];
+ *_retval = (flags & MSG_VIEW_FLAG_HASCHILDREN) &&
+ !(flags & nsMsgMessageFlags::Elided);
+ } else {
+ *_retval = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsContainerEmpty(int32_t index, bool* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ uint32_t flags = m_flags[index];
+ *_retval = !(flags & MSG_VIEW_FLAG_HASCHILDREN);
+ } else {
+ *_retval = false;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::IsSeparator(int32_t index, bool* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ *_retval = false;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetParentIndex(int32_t rowIndex, int32_t* _retval) {
+ *_retval = -1;
+
+ int32_t rowIndexLevel;
+ nsresult rv = GetLevel(rowIndex, &rowIndexLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t i;
+ for (i = rowIndex; i >= 0; i--) {
+ int32_t l;
+ GetLevel(i, &l);
+ if (l < rowIndexLevel) {
+ *_retval = i;
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::HasNextSibling(int32_t rowIndex, int32_t afterIndex,
+ bool* _retval) {
+ *_retval = false;
+
+ int32_t rowIndexLevel;
+ GetLevel(rowIndex, &rowIndexLevel);
+
+ int32_t i;
+ int32_t count;
+ GetRowCount(&count);
+ for (i = afterIndex + 1; i < count; i++) {
+ int32_t l;
+ GetLevel(i, &l);
+ if (l < rowIndexLevel) break;
+
+ if (l == rowIndexLevel) {
+ *_retval = true;
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetLevel(int32_t index, int32_t* _retval) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ *_retval = m_levels[index];
+ else
+ *_retval = 0;
+
+ return NS_OK;
+}
+
+// Search view will override this since headers can span db's.
+nsresult nsMsgDBView::GetMsgHdrForViewIndex(nsMsgViewIndex index,
+ nsIMsgDBHdr** msgHdr) {
+ nsresult rv = NS_OK;
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsMsgKey key = m_keys[index];
+ if (key == nsMsgKey_None || !m_db) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (key == m_cachedMsgKey) {
+ NS_IF_ADDREF(*msgHdr = m_cachedHdr);
+ } else {
+ rv = m_db->GetMsgHdrForKey(key, msgHdr);
+ if (NS_SUCCEEDED(rv)) {
+ m_cachedHdr = *msgHdr;
+ m_cachedMsgKey = key;
+ }
+ }
+
+ return rv;
+}
+
+void nsMsgDBView::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) {
+ if ((int32_t)index < 0 || index > m_keys.Length()) {
+ // Something's gone wrong in a caller, but we have no clue why.
+ // Return without adding the header to the view.
+ NS_ERROR("Index for message header insertion out of array range!");
+ return;
+ }
+
+ m_keys.InsertElementAt(index, msgKey);
+ m_flags.InsertElementAt(index, flags);
+ m_levels.InsertElementAt(index, level);
+}
+
+void nsMsgDBView::SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index,
+ nsMsgKey msgKey, uint32_t flags, uint32_t level) {
+ m_keys[index] = msgKey;
+ m_flags[index] = flags;
+ m_levels[index] = level;
+}
+
+nsresult nsMsgDBView::GetFolderForViewIndex(nsMsgViewIndex index,
+ nsIMsgFolder** aFolder) {
+ NS_IF_ADDREF(*aFolder = m_folder);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GetDBForViewIndex(nsMsgViewIndex index,
+ nsIMsgDatabase** db) {
+ NS_IF_ADDREF(*db = m_db);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
+ NS_ENSURE_ARG_POINTER(aCol);
+ // Attempt to retrieve a custom column handler. If it exists call it and
+ // return.
+ const nsAString& colID = aCol->GetId();
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+
+ if (colHandler) {
+ colHandler->GetImageSrc(aRow, aCol, aValue);
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCellValue(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+
+ if (NS_FAILED(rv) || !msgHdr) {
+ ClearHdrCache();
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ const nsAString& colID = aCol->GetId();
+
+ aValue.Truncate();
+ if (colID.IsEmpty()) return NS_OK;
+
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+
+ // Provide a string "value" for cells that do not normally have text.
+ // Use empty string for the normal states "Read", "Not Starred",
+ // "No Attachment" and "Not Junk".
+ switch (colID.First()) {
+ case 'a':
+ if (colID.EqualsLiteral("attachmentCol") &&
+ flags & nsMsgMessageFlags::Attachment) {
+ GetString(u"messageHasAttachment", aValue);
+ }
+ break;
+ case 'f':
+ if (colID.EqualsLiteral("flaggedCol") &&
+ flags & nsMsgMessageFlags::Marked) {
+ GetString(u"messageHasFlag", aValue);
+ }
+ break;
+ case 'j':
+ if (colID.EqualsLiteral("junkStatusCol") && JunkControlsEnabled(aRow)) {
+ nsCString junkScoreStr;
+ msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ // Only need to assign a real value for junk, it's empty already
+ // as it should be for non-junk.
+ if (!junkScoreStr.IsEmpty() &&
+ (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE))
+ aValue.AssignLiteral("messageJunk");
+
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Converting junkScore to integer failed.");
+ }
+ break;
+ case 't':
+ if (colID.EqualsLiteral("threadCol") &&
+ (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ // thread column
+ bool isContainer, isContainerEmpty, isContainerOpen;
+ IsContainer(aRow, &isContainer);
+ if (isContainer) {
+ IsContainerEmpty(aRow, &isContainerEmpty);
+ if (!isContainerEmpty) {
+ IsContainerOpen(aRow, &isContainerOpen);
+ GetString(
+ isContainerOpen ? u"messageExpanded" : u"messageCollapsed",
+ aValue);
+ }
+ }
+ }
+ break;
+ case 'u':
+ if (colID.EqualsLiteral("unreadButtonColHeader") &&
+ !(flags & nsMsgMessageFlags::Read)) {
+ GetString(u"messageUnread", aValue);
+ }
+ break;
+ default:
+ aValue.Assign(colID);
+ break;
+ }
+
+ return rv;
+}
+
+void nsMsgDBView::RememberDeletedMsgHdr(nsIMsgDBHdr* msgHdr) {
+ nsCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ if (mRecentlyDeletedArrayIndex >= mRecentlyDeletedMsgIds.Length())
+ mRecentlyDeletedMsgIds.AppendElement(messageId);
+ else
+ mRecentlyDeletedMsgIds[mRecentlyDeletedArrayIndex] = messageId;
+
+ // Only remember last 20 deleted msgs.
+ mRecentlyDeletedArrayIndex = (mRecentlyDeletedArrayIndex + 1) % 20;
+}
+
+bool nsMsgDBView::WasHdrRecentlyDeleted(nsIMsgDBHdr* msgHdr) {
+ nsCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ return mRecentlyDeletedMsgIds.Contains(messageId);
+}
+
+/**
+ * CUSTOM COLUMNS.
+ */
+
+// Add a custom column handler.
+NS_IMETHODIMP
+nsMsgDBView::AddColumnHandler(const nsAString& column,
+ nsIMsgCustomColumnHandler* handler) {
+ bool custColInSort = false;
+ size_t index = m_customColumnHandlerIDs.IndexOf(column);
+
+ nsAutoString strColID(column);
+
+ // Does not exist.
+ if (index == m_customColumnHandlerIDs.NoIndex) {
+ m_customColumnHandlerIDs.AppendElement(strColID);
+ m_customColumnHandlers.AppendObject(handler);
+ } else {
+ // Insert new handler into the appropriate place in the COMPtr array;
+ // no need to replace the column ID (it's the same).
+ m_customColumnHandlers.ReplaceObjectAt(handler, index);
+ }
+
+ // Check if the column name matches any of the columns in
+ // m_sortColumns, and if so, set m_sortColumns[i].mColHandler
+ for (uint32_t i = 0; i < m_sortColumns.Length(); i++) {
+ MsgViewSortColumnInfo& sortInfo = m_sortColumns[i];
+ if (sortInfo.mSortType == nsMsgViewSortType::byCustom &&
+ sortInfo.mCustomColumnName.Equals(column)) {
+ custColInSort = true;
+ sortInfo.mColHandler = handler;
+ }
+ }
+
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ // Grouped view has its own ways.
+ return NS_OK;
+
+ // This cust col is in sort columns, and all are now registered, so sort.
+ if (custColInSort && !CustomColumnsInSortAndNotRegistered())
+ Sort(m_sortType, m_sortOrder);
+
+ return NS_OK;
+}
+
+// Remove a custom column handler.
+NS_IMETHODIMP
+nsMsgDBView::RemoveColumnHandler(const nsAString& aColID) {
+ // Here we should check if the column name matches any of the columns in
+ // m_sortColumns, and if so, clear m_sortColumns[i].mColHandler.
+ size_t index = m_customColumnHandlerIDs.IndexOf(aColID);
+
+ if (index != m_customColumnHandlerIDs.NoIndex) {
+ m_customColumnHandlerIDs.RemoveElementAt(index);
+ m_customColumnHandlers.RemoveObjectAt(index);
+ // Check if the column name matches any of the columns in
+ // m_sortColumns, and if so, clear m_sortColumns[i].mColHandler.
+ for (uint32_t i = 0; i < m_sortColumns.Length(); i++) {
+ MsgViewSortColumnInfo& sortInfo = m_sortColumns[i];
+ if (sortInfo.mSortType == nsMsgViewSortType::byCustom &&
+ sortInfo.mCustomColumnName.Equals(aColID))
+ sortInfo.mColHandler = nullptr;
+ }
+
+ return NS_OK;
+ }
+
+ // Can't remove a column that isn't currently custom handled.
+ return NS_ERROR_FAILURE;
+}
+
+// TODO: NS_ENSURE_SUCCESS
+nsIMsgCustomColumnHandler* nsMsgDBView::GetCurColumnHandler() {
+ return GetColumnHandler(m_curCustomColumn);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetCurCustomColumn(const nsAString& aColID) {
+ m_curCustomColumn = aColID;
+ if (m_viewFolder) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ folderInfo->SetProperty("customSortCol", aColID);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCurCustomColumn(nsAString& result) {
+ result = m_curCustomColumn;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSecondaryCustomColumn(nsAString& result) {
+ result = m_secondaryCustomColumn;
+ return NS_OK;
+}
+
+nsIMsgCustomColumnHandler* nsMsgDBView::GetColumnHandler(
+ const nsAString& colID) {
+ size_t index = m_customColumnHandlerIDs.IndexOf(colID);
+ return (index != m_customColumnHandlerIDs.NoIndex)
+ ? m_customColumnHandlers[index]
+ : nullptr;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetColumnHandler(const nsAString& aColID,
+ nsIMsgCustomColumnHandler** aHandler) {
+ NS_ENSURE_ARG_POINTER(aHandler);
+ nsAutoString column(aColID);
+ NS_IF_ADDREF(*aHandler = GetColumnHandler(column));
+ return (*aHandler) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+// Check if any active sort columns are custom. If none are custom, return false
+// and go on as always. If any are custom, and all are not registered yet,
+// return true (so that the caller can postpone sort). When the custom column
+// observer is notified with MsgCreateDBView and registers the handler,
+// AddColumnHandler will sort once all required handlers are set.
+bool nsMsgDBView::CustomColumnsInSortAndNotRegistered() {
+ // The initial sort on view open has been started, subsequent user initiated
+ // sort callers can ignore verifying cust col registration.
+ m_checkedCustomColumns = true;
+
+ // DecodeColumnSort must have already created m_sortColumns, otherwise we
+ // can't know, but go on anyway.
+ if (!m_sortColumns.Length()) return false;
+
+ bool custColNotRegistered = false;
+ for (uint32_t i = 0; i < m_sortColumns.Length() && !custColNotRegistered;
+ i++) {
+ if (m_sortColumns[i].mSortType == nsMsgViewSortType::byCustom &&
+ m_sortColumns[i].mColHandler == nullptr)
+ custColNotRegistered = true;
+ }
+
+ return custColNotRegistered;
+}
+// END CUSTOM COLUMNS.
+
+NS_IMETHODIMP
+nsMsgDBView::GetCellText(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) {
+ const nsAString& colID = aCol->GetId();
+
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ aValue.Truncate();
+
+ // Attempt to retrieve a custom column handler. If it exists call it and
+ // return.
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+
+ if (colHandler) {
+ colHandler->GetCellText(aRow, aCol, aValue);
+ return NS_OK;
+ }
+
+ return CellTextForColumn(aRow, colID, aValue);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CellTextForColumn(int32_t aRow, const nsAString& aColumnName,
+ nsAString& aValue) {
+ if (aColumnName.IsEmpty()) {
+ aValue.Truncate();
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+
+ if (NS_FAILED(rv) || !msgHdr) {
+ ClearHdrCache();
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ nsCOMPtr<nsIMsgThread> thread;
+
+ switch (aColumnName.First()) {
+ case 's':
+ if (aColumnName.EqualsLiteral("subjectCol"))
+ rv = FetchSubject(msgHdr, m_flags[aRow], aValue);
+ else if (aColumnName.EqualsLiteral("senderCol"))
+ rv = FetchAuthor(msgHdr, aValue);
+ else if (aColumnName.EqualsLiteral("sizeCol"))
+ rv = FetchSize(msgHdr, aValue);
+ else if (aColumnName.EqualsLiteral("statusCol")) {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ rv = FetchStatus(flags, aValue);
+ }
+ break;
+ case 'r':
+ if (aColumnName.EqualsLiteral("recipientCol"))
+ rv = FetchRecipients(msgHdr, aValue);
+ else if (aColumnName.EqualsLiteral("receivedCol"))
+ rv = FetchDate(msgHdr, aValue, true);
+ break;
+ case 'd':
+ if (aColumnName.EqualsLiteral("dateCol")) rv = FetchDate(msgHdr, aValue);
+ break;
+ case 'c':
+ if (aColumnName.EqualsLiteral("correspondentCol")) {
+ if (IsOutgoingMsg(msgHdr))
+ rv = FetchRecipients(msgHdr, aValue);
+ else
+ rv = FetchAuthor(msgHdr, aValue);
+ }
+ break;
+ case 'p':
+ if (aColumnName.EqualsLiteral("priorityCol"))
+ rv = FetchPriority(msgHdr, aValue);
+ break;
+ case 'a':
+ if (aColumnName.EqualsLiteral("accountCol"))
+ rv = FetchAccount(msgHdr, aValue);
+ break;
+ case 't':
+ // total msgs in thread column
+ if (aColumnName.EqualsLiteral("totalCol") &&
+ m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ if (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) {
+ rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ nsAutoString formattedCountString;
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ formattedCountString.AppendInt(numChildren);
+ aValue.Assign(formattedCountString);
+ }
+ }
+ } else if (aColumnName.EqualsLiteral("tagsCol")) {
+ rv = FetchTags(msgHdr, aValue);
+ }
+ break;
+ case 'u':
+ // unread msgs in thread col
+ if (aColumnName.EqualsLiteral("unreadCol") &&
+ m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ if (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) {
+ rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv) && thread) {
+ nsAutoString formattedCountString;
+ uint32_t numUnreadChildren;
+ thread->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) {
+ formattedCountString.AppendInt(numUnreadChildren);
+ aValue.Assign(formattedCountString);
+ }
+ }
+ }
+ }
+ break;
+ case 'j': {
+ if (aColumnName.EqualsLiteral("junkStatusCol")) {
+ nsCString junkScoreStr;
+ msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ CopyASCIItoUTF16(junkScoreStr, aValue);
+ }
+ break;
+ }
+ case 'i': {
+ if (aColumnName.EqualsLiteral("idCol")) {
+ nsAutoString keyString;
+ nsMsgKey key;
+ msgHdr->GetMessageKey(&key);
+ keyString.AppendInt((int64_t)key);
+ aValue.Assign(keyString);
+ }
+ break;
+ }
+ case 'l': {
+ if (aColumnName.EqualsLiteral("locationCol")) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = GetFolderForViewIndex(aRow, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ folder->GetPrettyName(aValue);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CellDataForColumns(int32_t aRow,
+ const nsTArray<nsString>& aColumnNames,
+ nsAString& aProperties, int32_t* aThreadLevel,
+ nsTArray<nsString>& _retval) {
+ nsresult rv;
+ _retval.Clear();
+
+ uint32_t count = aColumnNames.Length();
+ _retval.SetCapacity(count);
+ for (nsString column : aColumnNames) {
+ nsString text;
+ rv = CellTextForColumn(aRow, column, text);
+ if (NS_FAILED(rv)) {
+ _retval.Clear();
+ return rv;
+ }
+ _retval.AppendElement(text);
+ }
+
+ rv = GetRowProperties(aRow, aProperties);
+ if (NS_FAILED(rv)) {
+ _retval.Clear();
+ return rv;
+ }
+
+ rv = GetLevel(aRow, aThreadLevel);
+ if (NS_FAILED(rv)) {
+ _retval.Clear();
+ aProperties.Truncate();
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetTree(mozilla::dom::XULTreeElement* tree) {
+ mTree = tree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetJSTree(nsIMsgJSTree* tree) {
+ mJSTree = tree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::ToggleOpenState(int32_t index) {
+ uint32_t numChanged;
+ nsresult rv = ToggleExpansion(index, &numChanged);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CycleHeader(nsTreeColumn* aCol) {
+ // Let HandleColumnClick() in threadPane.js handle it
+ // since it will set / clear the sort indicators.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CycleCell(int32_t row, nsTreeColumn* col) {
+ if (!IsValidIndex(row)) {
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ const nsAString& colID = col->GetId();
+
+ // Attempt to retrieve a custom column handler. If it exists call it and
+ // return.
+ nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID);
+
+ if (colHandler) {
+ colHandler->CycleCell(row, col);
+ return NS_OK;
+ }
+
+ // The cyclers below don't work for the grouped header dummy row, currently.
+ // A future implementation should consider both collapsed and expanded state.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort &&
+ m_flags[row] & MSG_VIEW_FLAG_DUMMY)
+ return NS_OK;
+
+ if (colID.IsEmpty()) return NS_OK;
+
+ switch (colID.First()) {
+ case 'u':
+ if (colID.EqualsLiteral("unreadButtonColHeader")) {
+ ApplyCommandToIndices(nsMsgViewCommandType::toggleMessageRead,
+ {(nsMsgViewIndex)row});
+ }
+ break;
+ case 't':
+ if (colID.EqualsLiteral("threadCol")) {
+ ExpandAndSelectThreadByIndex(row, false);
+ } else if (colID.EqualsLiteral("tagsCol")) {
+ // XXX Do we want to keep this behaviour but switch it to tags?
+ // We could enumerate over the tags and go to the next one - it looks
+ // to me like this wasn't working before tags landed, so maybe not
+ // worth bothering with.
+ }
+ break;
+ case 'f':
+ if (colID.EqualsLiteral("flaggedCol")) {
+ // toggle the flagged status of the element at row.
+ if (m_flags[row] & nsMsgMessageFlags::Marked) {
+ ApplyCommandToIndices(nsMsgViewCommandType::unflagMessages,
+ {(nsMsgViewIndex)row});
+ } else {
+ ApplyCommandToIndices(nsMsgViewCommandType::flagMessages,
+ {(nsMsgViewIndex)row});
+ }
+ }
+ break;
+ case 'j': {
+ if (!colID.EqualsLiteral("junkStatusCol") || !JunkControlsEnabled(row)) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(row, getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ nsCString junkScoreStr;
+ rv = msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ if (junkScoreStr.IsEmpty() ||
+ (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_HAM_SCORE)) {
+ ApplyCommandToIndices(nsMsgViewCommandType::junk,
+ {(nsMsgViewIndex)row});
+ } else {
+ ApplyCommandToIndices(nsMsgViewCommandType::unjunk,
+ {(nsMsgViewIndex)row});
+ }
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "Converting junkScore to integer failed.");
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// end nsITreeView Implementation Methods
+///////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsMsgDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) {
+ m_viewFlags = viewFlags;
+ m_sortOrder = sortOrder;
+ m_sortType = sortType;
+
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool userNeedsToAuthenticate = false;
+ // If we're PasswordProtectLocalCache, then we need to find out if the
+ // server is authenticated.
+ (void)accountManager->GetUserNeedsToAuthenticate(&userNeedsToAuthenticate);
+ if (userNeedsToAuthenticate) return NS_MSG_USER_NOT_AUTHENTICATED;
+
+ if (folder) {
+ // Search view will have a null folder.
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ rv = folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(m_db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgDBService->RegisterPendingListener(folder, this);
+ m_folder = folder;
+
+ if (!m_viewFolder) {
+ // There is never a viewFolder already set except for the single folder
+ // saved search case, where the backing folder m_folder is different from
+ // the m_viewFolder with its own dbFolderInfo state.
+ m_viewFolder = folder;
+ }
+
+ SetMRUTimeForFolder(m_viewFolder);
+
+ RestoreSortInfo();
+
+ // Determine if we are in a news folder or not. If yes, we'll show lines
+ // instead of size, and special icons in the thread pane.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString type;
+ rv = server->GetType(type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // I'm not sure this is correct, because XF virtual folders with mixed news
+ // and mail can have this set.
+ mIsNews = type.LowerCaseEqualsLiteral("nntp");
+
+ // Default to a virtual folder if folder not set, since synthetic search
+ // views may not have a folder.
+ uint32_t folderFlags = nsMsgFolderFlags::Virtual;
+ if (folder) folder->GetFlags(&folderFlags);
+
+ mIsXFVirtual = folderFlags & nsMsgFolderFlags::Virtual;
+ if (!mIsXFVirtual && type.LowerCaseEqualsLiteral("rss")) mIsRss = true;
+
+ // Special case nntp --> news since we'll break themes if we try to be
+ // consistent.
+ if (mIsNews)
+ mMessageType.AssignLiteral("news");
+ else
+ CopyUTF8toUTF16(type, mMessageType);
+
+ GetImapDeleteModel(nullptr);
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) {
+ prefs->GetBoolPref("mailnews.sort_threads_by_root", &mSortThreadsByRoot);
+ if (mIsNews)
+ prefs->GetBoolPref("news.show_size_in_lines", &mShowSizeInLines);
+ }
+ }
+
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ rv = accountManager->GetAllIdentities(identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto identity : identities) {
+ if (!identity) continue;
+
+ nsCString email;
+ identity->GetEmail(email);
+ if (!email.IsEmpty()) {
+ ToLowerCaseDropPlusAddessing(email);
+ mEmails.PutEntry(email);
+ }
+
+ identity->GetReplyTo(email);
+ if (!email.IsEmpty()) {
+ ToLowerCaseDropPlusAddessing(email);
+ mEmails.PutEntry(email);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::Close() {
+ int32_t oldSize = GetSize();
+ // This is important, because the tree will ask us for our row count, which
+ // gets determined from the number of keys.
+ m_keys.Clear();
+ // Be consistent.
+ m_flags.Clear();
+ m_levels.Clear();
+
+ // Clear these out since they no longer apply if we're switching a folder
+ mJunkHdrs.Clear();
+
+ // This needs to happen after we remove all the keys, since RowCountChanged()
+ // will call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -oldSize);
+
+ ClearHdrCache();
+ if (m_db) {
+ m_db->RemoveListener(this);
+ m_db = nullptr;
+ }
+ if (m_folder) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgDBService->UnregisterPendingListener(this);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags, int32_t* aCount) {
+ NS_ASSERTION(false, "not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::Init(nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ mMessengerWeak = do_GetWeakReference(aMessengerInstance);
+ mMsgWindowWeak = do_GetWeakReference(aMsgWindow);
+ mCommandUpdater = do_GetWeakReference(aCmdUpdater);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSuppressCommandUpdating(bool aSuppressCommandUpdating) {
+ mSuppressCommandUpdating = aSuppressCommandUpdating;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSuppressCommandUpdating(bool* aSuppressCommandUpdating) {
+ *aSuppressCommandUpdating = mSuppressCommandUpdating;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSuppressMsgDisplay(bool aSuppressDisplay) {
+ mSuppressMsgDisplay = aSuppressDisplay;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSuppressMsgDisplay(bool* aSuppressDisplay) {
+ *aSuppressDisplay = mSuppressMsgDisplay;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetUsingLines(bool* aUsingLines) {
+ *aUsingLines = mShowSizeInLines;
+ return NS_OK;
+}
+
+int CompareViewIndices(const void* v1, const void* v2, void*) {
+ nsMsgViewIndex i1 = *(nsMsgViewIndex*)v1;
+ nsMsgViewIndex i2 = *(nsMsgViewIndex*)v2;
+ return i1 - i2;
+}
+
+// Array<nsMsgViewIndex> getIndicesForSelection();
+NS_IMETHODIMP
+nsMsgDBView::GetIndicesForSelection(nsTArray<nsMsgViewIndex>& indices) {
+ indices.Clear();
+ if (mTreeSelection) {
+ int32_t viewSize = GetSize();
+ int32_t count;
+ mTreeSelection->GetCount(&count);
+ indices.SetCapacity(count);
+ int32_t selectionCount;
+ mTreeSelection->GetRangeCount(&selectionCount);
+ for (int32_t i = 0; i < selectionCount; i++) {
+ int32_t startRange = -1;
+ int32_t endRange = -1;
+ mTreeSelection->GetRangeAt(i, &startRange, &endRange);
+ if (startRange >= 0 && startRange < viewSize) {
+ for (int32_t rangeIndex = startRange;
+ rangeIndex <= endRange && rangeIndex < viewSize; rangeIndex++) {
+ indices.AppendElement(rangeIndex);
+ }
+ }
+ }
+
+ NS_ASSERTION(indices.Length() == uint32_t(count),
+ "selection count is wrong");
+ } else {
+ // If there is no tree selection object then we must be in stand alone
+ // message mode. In that case the selected indices are really just the
+ // current message key.
+ nsMsgViewIndex viewIndex = FindViewIndex(m_currentlyDisplayedMsgKey);
+ if (viewIndex != nsMsgViewIndex_None) indices.AppendElement(viewIndex);
+ }
+
+ return NS_OK;
+}
+
+// Array<nsIMsgDBHdr> getSelectedMsgHdrs();
+NS_IMETHODIMP
+nsMsgDBView::GetSelectedMsgHdrs(nsTArray<RefPtr<nsIMsgDBHdr>>& aResult) {
+ nsMsgViewIndexArray selection;
+ aResult.Clear();
+ nsresult rv = GetIndicesForSelection(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return GetHeadersFromSelection(selection, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetURIsForSelection(nsTArray<nsCString>& uris) {
+ uris.Clear();
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages;
+ nsresult rv = GetSelectedMsgHdrs(messages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uris.SetCapacity(messages.Length());
+ for (nsIMsgDBHdr* msgHdr : messages) {
+ nsCString tmpUri;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFolder(getter_AddRefs(folder));
+ rv = GenerateURIForMsgKey(msgKey, folder, tmpUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uris.AppendElement(tmpUri);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetURIForViewIndex(nsMsgViewIndex index, nsACString& result) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder = m_folder;
+ if (!folder) {
+ rv = GetFolderForViewIndex(index, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (index == nsMsgViewIndex_None || index >= m_flags.Length() ||
+ m_flags[index] & MSG_VIEW_FLAG_DUMMY) {
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ return GenerateURIForMsgKey(m_keys[index], folder, result);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::DoCommandWithFolder(nsMsgViewCommandTypeValue command,
+ nsIMsgFolder* destFolder) {
+ NS_ENSURE_ARG_POINTER(destFolder);
+
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+
+ nsresult rv = NS_OK;
+ switch (command) {
+ case nsMsgViewCommandType::copyMessages:
+ case nsMsgViewCommandType::moveMessages:
+ rv = ApplyCommandToIndicesWithFolder(command, selection, destFolder);
+ NoteChange(0, 0, nsMsgViewNotificationCode::none);
+ break;
+ default:
+ NS_ASSERTION(false, "invalid command type");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::DoCommand(nsMsgViewCommandTypeValue command) {
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+
+ nsresult rv = NS_OK;
+ switch (command) {
+ case nsMsgViewCommandType::downloadSelectedForOffline:
+ return DownloadForOffline(msgWindow, selection);
+ case nsMsgViewCommandType::downloadFlaggedForOffline:
+ return DownloadFlaggedForOffline(msgWindow);
+ case nsMsgViewCommandType::markMessagesRead:
+ case nsMsgViewCommandType::markMessagesUnread:
+ case nsMsgViewCommandType::toggleMessageRead:
+ case nsMsgViewCommandType::flagMessages:
+ case nsMsgViewCommandType::unflagMessages:
+ case nsMsgViewCommandType::deleteMsg:
+ case nsMsgViewCommandType::undeleteMsg:
+ case nsMsgViewCommandType::deleteNoTrash:
+ case nsMsgViewCommandType::markThreadRead:
+ case nsMsgViewCommandType::junk:
+ case nsMsgViewCommandType::unjunk:
+ rv = ApplyCommandToIndices(command, selection);
+ NoteChange(0, 0, nsMsgViewNotificationCode::none);
+ break;
+ case nsMsgViewCommandType::selectAll:
+ if (mTreeSelection) {
+ // If in threaded mode, we need to expand all before selecting.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ rv = ExpandAll();
+
+ mTreeSelection->SelectAll();
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+ }
+ break;
+ case nsMsgViewCommandType::selectThread:
+ rv = ExpandAndSelectThread();
+ break;
+ case nsMsgViewCommandType::selectFlagged:
+ if (!mTreeSelection) {
+ rv = NS_ERROR_UNEXPECTED;
+ } else {
+ mTreeSelection->SetSelectEventsSuppressed(true);
+ mTreeSelection->ClearSelection();
+ // XXX ExpandAll?
+ uint32_t numIndices = GetSize();
+ for (uint32_t curIndex = 0; curIndex < numIndices; curIndex++) {
+ if (m_flags[curIndex] & nsMsgMessageFlags::Marked)
+ mTreeSelection->ToggleSelect(curIndex);
+ }
+
+ mTreeSelection->SetSelectEventsSuppressed(false);
+ }
+ break;
+ case nsMsgViewCommandType::markAllRead:
+ if (m_folder) {
+ SetSuppressChangeNotifications(true);
+ rv = m_folder->MarkAllMessagesRead(msgWindow);
+ SetSuppressChangeNotifications(false);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+ }
+ break;
+ case nsMsgViewCommandType::toggleThreadWatched:
+ rv = ToggleWatched(selection);
+ break;
+ case nsMsgViewCommandType::expandAll:
+ rv = ExpandAll();
+ m_viewFlags |= nsMsgViewFlagsType::kExpandAll;
+ SetViewFlags(m_viewFlags);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ break;
+ case nsMsgViewCommandType::collapseAll:
+ rv = CollapseAll();
+ m_viewFlags &= ~nsMsgViewFlagsType::kExpandAll;
+ SetViewFlags(m_viewFlags);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ break;
+ default:
+ NS_ASSERTION(false, "invalid command type");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ return rv;
+}
+
+bool nsMsgDBView::ServerSupportsFilterAfterTheFact() {
+ // Cross folder virtual folders might not have a folder set.
+ if (!m_folder) return false;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = m_folder->GetServer(getter_AddRefs(server));
+ // Unexpected.
+ if (NS_FAILED(rv)) return false;
+
+ // Filter after the fact is implement using search so if you can't search,
+ // you can't filter after the fact.
+ bool canSearch;
+ rv = server->GetCanSearchMessages(&canSearch);
+ // Unexpected.
+ if (NS_FAILED(rv)) return false;
+
+ return canSearch;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCommandStatus(nsMsgViewCommandTypeValue command,
+ bool* selectable_p,
+ nsMsgViewCommandCheckStateValue* selected_p) {
+ nsresult rv = NS_OK;
+
+ bool haveSelection;
+ int32_t rangeCount;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ // If range count is non-zero, we have at least one item selected, so we
+ // have a selection.
+ if (mTreeSelection &&
+ NS_SUCCEEDED(mTreeSelection->GetRangeCount(&rangeCount)) &&
+ rangeCount > 0) {
+ haveSelection = NonDummyMsgSelected(selection);
+ } else {
+ // If we don't have a tree selection we must be in stand alone mode.
+ haveSelection = IsValidIndex(m_currentlyDisplayedViewIndex);
+ }
+
+ switch (command) {
+ case nsMsgViewCommandType::deleteMsg:
+ case nsMsgViewCommandType::deleteNoTrash: {
+ bool canDelete;
+ if (m_folder &&
+ NS_SUCCEEDED(m_folder->GetCanDeleteMessages(&canDelete)) &&
+ !canDelete) {
+ *selectable_p = false;
+ } else {
+ *selectable_p = haveSelection;
+ }
+ break;
+ }
+ case nsMsgViewCommandType::applyFilters:
+ // Disable if no messages.
+ // XXX todo, check that we have filters, and at least one is enabled.
+ *selectable_p = GetSize();
+ if (*selectable_p) *selectable_p = ServerSupportsFilterAfterTheFact();
+
+ break;
+ case nsMsgViewCommandType::runJunkControls:
+ // Disable if no messages.
+ // XXX todo, check that we have JMC enabled?
+ *selectable_p = GetSize() && JunkControlsEnabled(nsMsgViewIndex_None);
+ break;
+ case nsMsgViewCommandType::deleteJunk: {
+ // Disable if no messages, or if we can't delete (like news and
+ // certain imap folders).
+ bool canDelete;
+ *selectable_p =
+ GetSize() && m_folder &&
+ NS_SUCCEEDED(m_folder->GetCanDeleteMessages(&canDelete)) && canDelete;
+ break;
+ }
+ case nsMsgViewCommandType::markMessagesRead:
+ case nsMsgViewCommandType::markMessagesUnread:
+ case nsMsgViewCommandType::toggleMessageRead:
+ case nsMsgViewCommandType::flagMessages:
+ case nsMsgViewCommandType::unflagMessages:
+ case nsMsgViewCommandType::toggleThreadWatched:
+ case nsMsgViewCommandType::markThreadRead:
+ case nsMsgViewCommandType::downloadSelectedForOffline:
+ *selectable_p = haveSelection;
+ break;
+ case nsMsgViewCommandType::junk:
+ case nsMsgViewCommandType::unjunk:
+ *selectable_p = haveSelection && !selection.IsEmpty() &&
+ JunkControlsEnabled(selection[0]);
+ break;
+ case nsMsgViewCommandType::cmdRequiringMsgBody:
+ *selectable_p =
+ haveSelection && (!WeAreOffline() || OfflineMsgSelected(selection));
+ break;
+ case nsMsgViewCommandType::downloadFlaggedForOffline:
+ case nsMsgViewCommandType::markAllRead:
+ *selectable_p = true;
+ break;
+ default:
+ NS_ASSERTION(false, "invalid command type");
+ rv = NS_ERROR_FAILURE;
+ }
+
+ return rv;
+}
+
+// This method needs to be overridden by the various view classes
+// that have different kinds of threads. For example, in a
+// threaded quick search db view, we'd only want to include children
+// of the thread that fit the view (IMO). And when we have threaded
+// cross folder views, we would include all the children of the
+// cross-folder thread.
+nsresult nsMsgDBView::ListCollapsedChildren(
+ nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> thread;
+ GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr));
+ if (!msgHdr) {
+ NS_ASSERTION(false, "couldn't find message to expand");
+ return NS_MSG_MESSAGE_NOT_FOUND;
+ }
+
+ nsresult rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ for (uint32_t i = 1; i < numChildren && NS_SUCCEEDED(rv); i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = thread->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ messageArray.AppendElement(msgHdr);
+ }
+ }
+
+ return rv;
+}
+
+bool nsMsgDBView::OperateOnMsgsInCollapsedThreads() {
+ if (!mJSTree && mTreeSelection) {
+ RefPtr<mozilla::dom::XULTreeElement> selTree;
+ mTreeSelection->GetTree(getter_AddRefs(selTree));
+ // No tree means stand-alone message window.
+ if (!selTree) return false;
+ }
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ bool includeCollapsedMsgs = false;
+ prefBranch->GetBoolPref("mail.operate_on_msgs_in_collapsed_threads",
+ &includeCollapsedMsgs);
+ return includeCollapsedMsgs;
+}
+
+nsresult nsMsgDBView::GetHeadersFromSelection(
+ nsTArray<nsMsgViewIndex> const& selection,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& hdrs) {
+ hdrs.Clear();
+ hdrs.SetCapacity(selection.Length()); // Best guess.
+ nsresult rv = NS_OK;
+
+ // Don't include collapsed messages if the front end failed to summarize
+ // the selection.
+ bool includeCollapsedMsgs =
+ OperateOnMsgsInCollapsedThreads() && !mSummarizeFailed;
+
+ for (nsMsgViewIndex viewIndex : selection) {
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ if (viewIndex == nsMsgViewIndex_None) {
+ continue;
+ }
+
+ uint32_t viewIndexFlags = m_flags[viewIndex];
+ if (viewIndexFlags & MSG_VIEW_FLAG_DUMMY) {
+ // If collapsed dummy header selected, list its children.
+ if (includeCollapsedMsgs && viewIndexFlags & nsMsgMessageFlags::Elided &&
+ m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ rv = ListCollapsedChildren(viewIndex, hdrs);
+
+ continue;
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ hdrs.AppendElement(msgHdr);
+ if (includeCollapsedMsgs && viewIndexFlags & nsMsgMessageFlags::Elided &&
+ viewIndexFlags & MSG_VIEW_FLAG_HASCHILDREN &&
+ m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ rv = ListCollapsedChildren(viewIndex, hdrs);
+ }
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::CopyMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool isMove, nsIMsgFolder* destFolder) {
+ if (m_deletingRows) {
+ NS_ASSERTION(false, "Last move did not complete");
+ return NS_OK;
+ }
+
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(destFolder);
+
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> hdrs;
+ rv = GetHeadersFromSelection(selection, hdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_deletingRows = isMove && mDeleteModel != nsMsgImapDeleteModels::IMAPDelete;
+ if (m_deletingRows) {
+ mIndicesToNoteChange.AppendElements(selection);
+ }
+
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return copyService->CopyMessages(m_folder /* source folder */, hdrs,
+ destFolder, isMove, nullptr /* listener */,
+ window, true /* allow Undo */);
+}
+
+nsresult nsMsgDBView::ApplyCommandToIndicesWithFolder(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder) {
+ nsresult rv = NS_OK;
+ NS_ENSURE_ARG_POINTER(destFolder);
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ switch (command) {
+ case nsMsgViewCommandType::copyMessages:
+ NS_ASSERTION(!(m_folder == destFolder),
+ "The source folder and the destination folder are the same");
+ if (m_folder != destFolder)
+ rv = CopyMessages(msgWindow, selection, false /* isMove */, destFolder);
+
+ break;
+ case nsMsgViewCommandType::moveMessages:
+ NS_ASSERTION(!(m_folder == destFolder),
+ "The source folder and the destination folder are the same");
+ if (m_folder != destFolder)
+ rv = CopyMessages(msgWindow, selection, true /* isMove */, destFolder);
+
+ break;
+ default:
+ NS_ASSERTION(false, "unhandled command");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::ApplyCommandToIndices(nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection) {
+ if (selection.IsEmpty()) {
+ // Return quietly, just in case/
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = GetFolderForViewIndex(selection[0], getter_AddRefs(folder));
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ if (command == nsMsgViewCommandType::deleteMsg)
+ return DeleteMessages(msgWindow, selection, false);
+
+ if (command == nsMsgViewCommandType::deleteNoTrash)
+ return DeleteMessages(msgWindow, selection, true);
+
+ nsTArray<nsMsgKey> imapUids;
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder);
+ bool thisIsImapFolder = (imapFolder != nullptr);
+ nsCOMPtr<nsIJunkMailPlugin> junkPlugin;
+
+ // If this is a junk command, get the junk plugin.
+ if (command == nsMsgViewCommandType::junk ||
+ command == nsMsgViewCommandType::unjunk) {
+ // Get the folder from the first item; we assume that
+ // all messages in the view are from the same folder (no
+ // more junk status column in the 'search messages' dialog
+ // like in earlier versions...).
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFilterPlugin> filterPlugin;
+ rv = server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ junkPlugin = do_QueryInterface(filterPlugin, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications,
+ false);
+
+ // No sense going through the code that handles messages in collasped threads
+ // for mark thread read.
+ if (command == nsMsgViewCommandType::markThreadRead) {
+ for (nsMsgViewIndex viewIndex : selection) {
+ SetThreadOfMsgReadByIndex(viewIndex, imapUids, true);
+ }
+ } else {
+ // Turn the selection into an array of msg hdrs. This may include messages
+ // in collapsed threads
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages;
+ rv = GetHeadersFromSelection(selection, messages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t length = messages.Length();
+
+ if (thisIsImapFolder) {
+ imapUids.SetLength(length);
+ }
+
+ for (uint32_t i = 0; i < length; i++) {
+ nsMsgKey msgKey;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr(messages[i]);
+ msgHdr->GetMessageKey(&msgKey);
+ if (thisIsImapFolder) imapUids[i] = msgKey;
+
+ switch (command) {
+ case nsMsgViewCommandType::junk:
+ mNumMessagesRemainingInBatch++;
+ mJunkHdrs.AppendElement(msgHdr);
+ rv = SetMsgHdrJunkStatus(junkPlugin.get(), msgHdr,
+ nsIJunkMailPlugin::JUNK);
+ break;
+ case nsMsgViewCommandType::unjunk:
+ mNumMessagesRemainingInBatch++;
+ mJunkHdrs.AppendElement(msgHdr);
+ rv = SetMsgHdrJunkStatus(junkPlugin.get(), msgHdr,
+ nsIJunkMailPlugin::GOOD);
+ break;
+ case nsMsgViewCommandType::toggleMessageRead:
+ case nsMsgViewCommandType::undeleteMsg:
+ case nsMsgViewCommandType::markMessagesRead:
+ case nsMsgViewCommandType::markMessagesUnread:
+ case nsMsgViewCommandType::unflagMessages:
+ case nsMsgViewCommandType::flagMessages:
+ // This is completely handled in the code below.
+ break;
+ default:
+ NS_ERROR("unhandled command");
+ break;
+ }
+ }
+
+ switch (command) {
+ case nsMsgViewCommandType::toggleMessageRead: {
+ if (messages.IsEmpty()) break;
+
+ uint32_t msgFlags;
+ messages[0]->GetFlags(&msgFlags);
+ folder->MarkMessagesRead(messages,
+ !(msgFlags & nsMsgMessageFlags::Read));
+ break;
+ }
+ case nsMsgViewCommandType::markMessagesRead:
+ case nsMsgViewCommandType::markMessagesUnread:
+ folder->MarkMessagesRead(
+ messages, command == nsMsgViewCommandType::markMessagesRead);
+ break;
+ case nsMsgViewCommandType::unflagMessages:
+ case nsMsgViewCommandType::flagMessages:
+ folder->MarkMessagesFlagged(
+ messages, command == nsMsgViewCommandType::flagMessages);
+ break;
+ default:
+ break;
+ }
+
+ // Provide junk-related batch notifications.
+ if (command == nsMsgViewCommandType::junk ||
+ command == nsMsgViewCommandType::unjunk) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyMsgsJunkStatusChanged(messages);
+ }
+ }
+ }
+
+ folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, true);
+
+ if (thisIsImapFolder) {
+ imapMessageFlagsType flags = kNoImapMsgFlag;
+ bool addFlags = false;
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ switch (command) {
+ case nsMsgViewCommandType::markThreadRead:
+ flags |= kImapMsgSeenFlag;
+ addFlags = true;
+ break;
+ case nsMsgViewCommandType::undeleteMsg:
+ flags = kImapMsgDeletedFlag;
+ addFlags = false;
+ break;
+ case nsMsgViewCommandType::junk:
+ return imapFolder->StoreCustomKeywords(msgWindow, "Junk"_ns,
+ "NonJunk"_ns, imapUids, nullptr);
+ case nsMsgViewCommandType::unjunk: {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetHdrForFirstSelectedMessage(getter_AddRefs(msgHdr));
+ uint32_t msgFlags = 0;
+ if (msgHdr) msgHdr->GetFlags(&msgFlags);
+
+ if (msgFlags & nsMsgMessageFlags::IMAPDeleted)
+ imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, imapUids,
+ nullptr);
+
+ return imapFolder->StoreCustomKeywords(msgWindow, "NonJunk"_ns,
+ "Junk"_ns, imapUids, nullptr);
+ }
+ default:
+ break;
+ }
+
+ // Can't get here without thisIsImapThreadPane == TRUE.
+ if (flags != kNoImapMsgFlag) {
+ imapFolder->StoreImapFlags(flags, addFlags, imapUids, nullptr);
+ }
+ }
+
+ return rv;
+}
+
+/**
+ * View modifications methods by index.
+ */
+
+// This method just removes the specified line from the view. It does
+// NOT delete it from the database.
+nsresult nsMsgDBView::RemoveByIndex(nsMsgViewIndex index) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ m_keys.RemoveElementAt(index);
+ m_flags.RemoveElementAt(index);
+ m_levels.RemoveElementAt(index);
+
+ // The call to NoteChange() has to happen after we remove the key as
+ // NoteChange() will call RowCountChanged() which will call our GetRowCount().
+ // An example where view is not the listener - D&D messages.
+ if (!m_deletingRows)
+ NoteChange(index, -1, nsMsgViewNotificationCode::insertOrDelete);
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::DeleteMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) {
+ if (m_deletingRows) {
+ NS_WARNING("Last delete did not complete");
+ return NS_OK;
+ }
+
+ nsresult rv;
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> hdrs;
+ rv = GetHeadersFromSelection(selection, hdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const char* warnCollapsedPref = "mail.warn_on_collapsed_thread_operation";
+ const char* warnShiftDelPref = "mail.warn_on_shift_delete";
+ const char* warnNewsPref = "news.warn_on_delete";
+ const char* warnTrashDelPref = "mail.warn_on_delete_from_trash";
+ const char* activePref = nullptr;
+ nsString warningName;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool trashFolder = false;
+ rv = m_folder->GetFlag(nsMsgFolderFlags::Trash, &trashFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (trashFolder) {
+ bool pref = false;
+ prefBranch->GetBoolPref(warnTrashDelPref, &pref);
+ if (pref) {
+ activePref = warnTrashDelPref;
+ warningName.AssignLiteral("confirmMsgDelete.deleteFromTrash.desc");
+ }
+ }
+
+ if (!activePref && (selection.Length() != hdrs.Length())) {
+ bool pref = false;
+ prefBranch->GetBoolPref(warnCollapsedPref, &pref);
+ if (pref) {
+ activePref = warnCollapsedPref;
+ warningName.AssignLiteral("confirmMsgDelete.collapsed.desc");
+ }
+ }
+
+ if (!activePref && deleteStorage && !trashFolder) {
+ bool pref = false;
+ prefBranch->GetBoolPref(warnShiftDelPref, &pref);
+ if (pref) {
+ activePref = warnShiftDelPref;
+ warningName.AssignLiteral("confirmMsgDelete.deleteNoTrash.desc");
+ }
+ }
+
+ if (!activePref && mIsNews) {
+ bool pref = false;
+ prefBranch->GetBoolPref(warnNewsPref, &pref);
+ if (pref) {
+ activePref = warnNewsPref;
+ warningName.AssignLiteral("confirmMsgDelete.deleteNoTrash.desc");
+ }
+ }
+
+ if (activePref) {
+ nsCOMPtr<nsIPrompt> dialog;
+
+ nsCOMPtr<nsIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = wwatch->GetNewPrompter(0, getter_AddRefs(dialog));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // "Don't ask..." - unchecked by default.
+ bool dontAsk = false;
+ int32_t buttonPressed = 0;
+
+ nsString dialogTitle;
+ nsString confirmString;
+ nsString checkboxText;
+ nsString buttonApplyNowText;
+ GetString(u"confirmMsgDelete.title", dialogTitle);
+ GetString(u"confirmMsgDelete.dontAsk.label", checkboxText);
+ GetString(u"confirmMsgDelete.delete.label", buttonApplyNowText);
+
+ GetString(warningName.get(), confirmString);
+
+ const uint32_t buttonFlags =
+ (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) +
+ (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1);
+
+ rv = dialog->ConfirmEx(dialogTitle.get(), confirmString.get(), buttonFlags,
+ buttonApplyNowText.get(), nullptr, nullptr,
+ checkboxText.get(), &dontAsk, &buttonPressed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (buttonPressed) return NS_ERROR_FAILURE;
+
+ if (dontAsk) prefBranch->SetBoolPref(activePref, false);
+ }
+
+ if (!deleteStorage) {
+ rv = m_folder->MarkMessagesRead(hdrs, true);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete) {
+ m_deletingRows = true;
+ }
+
+ if (m_deletingRows) {
+ mIndicesToNoteChange.AppendElements(selection);
+ }
+
+ rv = m_folder->DeleteMessages(hdrs, window, deleteStorage, false, nullptr,
+ true /* allow Undo */);
+ if (NS_FAILED(rv)) {
+ m_deletingRows = false;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::DownloadForOffline(
+ nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection) {
+ nsresult rv = NS_OK;
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsMsgKey key = m_keys[viewIndex];
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = m_db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (msgHdr) {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ if (!(flags & nsMsgMessageFlags::Offline)) {
+ messages.AppendElement(msgHdr);
+ }
+ }
+ }
+
+ m_folder->DownloadMessagesForOffline(messages, window);
+ return rv;
+}
+
+nsresult nsMsgDBView::DownloadFlaggedForOffline(nsIMsgWindow* window) {
+ nsresult rv = NS_OK;
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages;
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ rv = GetMessageEnumerator(getter_AddRefs(enumerator));
+ if (NS_SUCCEEDED(rv) && enumerator) {
+ bool hasMore;
+ while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ rv = enumerator->GetNext(getter_AddRefs(header));
+ if (header && NS_SUCCEEDED(rv)) {
+ uint32_t flags;
+ header->GetFlags(&flags);
+ if ((flags & nsMsgMessageFlags::Marked) &&
+ !(flags & nsMsgMessageFlags::Offline)) {
+ messages.AppendElement(header);
+ }
+ }
+ }
+ }
+
+ m_folder->DownloadMessagesForOffline(messages, window);
+ return rv;
+}
+
+// Read/unread handling.
+nsresult nsMsgDBView::ToggleReadByIndex(nsMsgViewIndex index) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ return SetReadByIndex(index, !(m_flags[index] & nsMsgMessageFlags::Read));
+}
+
+nsresult nsMsgDBView::SetReadByIndex(nsMsgViewIndex index, bool read) {
+ nsresult rv;
+
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (read) {
+ OrExtraFlag(index, nsMsgMessageFlags::Read);
+ // MarkRead() will clear this flag in the db and then call OnKeyChange(),
+ // but because we are the instigator of the change we'll ignore the change.
+ // So we need to clear it in m_flags to keep the db and m_flags in sync.
+ AndExtraFlag(index, ~nsMsgMessageFlags::New);
+ } else {
+ AndExtraFlag(index, ~nsMsgMessageFlags::Read);
+ }
+
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ rv = GetDBForViewIndex(index, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = dbToUse->MarkRead(m_keys[index], read, this);
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsMsgViewIndex threadIndex = GetThreadIndex(index);
+ if (threadIndex != index)
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::SetThreadOfMsgReadByIndex(
+ nsMsgViewIndex index, nsTArray<nsMsgKey>& keysMarkedRead, bool /*read*/) {
+ nsresult rv;
+
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ rv = MarkThreadOfMsgRead(m_keys[index], index, keysMarkedRead, true);
+ return rv;
+}
+
+nsresult nsMsgDBView::SetFlaggedByIndex(nsMsgViewIndex index, bool mark) {
+ nsresult rv;
+
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ rv = GetDBForViewIndex(index, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mark)
+ OrExtraFlag(index, nsMsgMessageFlags::Marked);
+ else
+ AndExtraFlag(index, ~nsMsgMessageFlags::Marked);
+
+ rv = dbToUse->MarkMarked(m_keys[index], mark, this);
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ return rv;
+}
+
+nsresult nsMsgDBView::SetMsgHdrJunkStatus(nsIJunkMailPlugin* aJunkPlugin,
+ nsIMsgDBHdr* aMsgHdr,
+ nsMsgJunkStatus aNewClassification) {
+ // Get the old junk score.
+ nsCString junkScoreStr;
+ nsresult rv = aMsgHdr->GetStringProperty("junkscore", junkScoreStr);
+
+ // And the old origin.
+ nsCString oldOriginStr;
+ rv = aMsgHdr->GetStringProperty("junkscoreorigin", oldOriginStr);
+
+ // If this was not classified by the user, say so.
+ nsMsgJunkStatus oldUserClassification;
+ if (oldOriginStr.get()[0] != 'u') {
+ oldUserClassification = nsIJunkMailPlugin::UNCLASSIFIED;
+ } else {
+ // Otherwise, pass the actual user classification.
+ if (junkScoreStr.IsEmpty())
+ oldUserClassification = nsIJunkMailPlugin::UNCLASSIFIED;
+ else if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE)
+ oldUserClassification = nsIJunkMailPlugin::JUNK;
+ else
+ oldUserClassification = nsIJunkMailPlugin::GOOD;
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed.");
+ }
+
+ // Get the URI for this message so we can pass it to the plugin.
+ nsCString uri;
+ nsMsgKey msgKey;
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsCOMPtr<nsIMsgDatabase> db;
+ aMsgHdr->GetMessageKey(&msgKey);
+ rv = aMsgHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ GenerateURIForMsgKey(msgKey, folder, uri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folder->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Tell the plugin about this change, so that it can (potentially)
+ // adjust its database appropriately.
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ rv = aJunkPlugin->SetMessageClassification(
+ uri, oldUserClassification, aNewClassification, msgWindow, this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // This routine is only reached if the user someone touched the UI
+ // and told us the junk status of this message.
+ // Set origin first so that listeners on the junkscore will
+ // know the correct origin.
+ rv = db->SetStringProperty(msgKey, "junkscoreorigin", "user"_ns);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "SetStringPropertyByIndex failed");
+
+ // Set the junk score on the message itself.
+ nsAutoCString msgJunkScore;
+ msgJunkScore.AppendInt(aNewClassification == nsIJunkMailPlugin::JUNK
+ ? nsIJunkMailPlugin::IS_SPAM_SCORE
+ : nsIJunkMailPlugin::IS_HAM_SCORE);
+ db->SetStringProperty(msgKey, "junkscore", msgJunkScore);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
+
+nsresult nsMsgDBView::GetFolderFromMsgURI(const nsACString& aMsgURI,
+ nsIMsgFolder** aFolder) {
+ NS_IF_ADDREF(*aFolder = m_folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnMessageClassified(const nsACString& aMsgURI,
+ nsMsgJunkStatus aClassification,
+ uint32_t aJunkPercent)
+
+{
+ // Note: we know all messages in a batch have the same
+ // classification, since unlike OnMessageClassified
+ // methods in other classes (such as nsLocalMailFolder
+ // and nsImapMailFolder), this class, nsMsgDBView, currently
+ // only triggers message classifications due to a command to
+ // mark some of the messages in the view as junk, or as not
+ // junk - so the classification is dictated to the filter,
+ // not suggested by it.
+ //
+ // For this reason the only thing we (may) have to do is
+ // perform the action on all of the junk messages.
+
+ uint32_t numJunk = mJunkHdrs.Length();
+ NS_ASSERTION(aClassification == nsIJunkMailPlugin::GOOD || numJunk,
+ "the classification of a manually-marked junk message has "
+ "been classified as junk, yet there seem to be no such "
+ "outstanding messages");
+
+ // Is this the last message in the batch?
+ if (--mNumMessagesRemainingInBatch == 0 && numJunk > 0) {
+ PerformActionsOnJunkMsgs(aClassification == nsIJunkMailPlugin::JUNK);
+ mJunkHdrs.Clear();
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::PerformActionsOnJunkMsgs(bool msgsAreJunk) {
+ uint32_t numJunkHdrs = mJunkHdrs.Length();
+ if (!numJunkHdrs) {
+ NS_ERROR("no indices of marked-as-junk messages to act on");
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgFolder> srcFolder;
+ mJunkHdrs[0]->GetFolder(getter_AddRefs(srcFolder));
+
+ bool moveMessages, changeReadState;
+ nsCOMPtr<nsIMsgFolder> targetFolder;
+
+ nsresult rv = DetermineActionsForJunkChange(msgsAreJunk, srcFolder,
+ moveMessages, changeReadState,
+ getter_AddRefs(targetFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Nothing to do, bail out.
+ if (!(moveMessages || changeReadState)) return NS_OK;
+
+ if (changeReadState) {
+ // Notes on marking junk as read:
+ // 1. There are 2 occasions on which junk messages are marked as
+ // read: after a manual marking (here and in the front end) and after
+ // automatic classification by the bayesian filter (see code for local
+ // mail folders and for imap mail folders). The server-specific
+ // markAsReadOnSpam pref only applies to the latter, the former is
+ // controlled by "mailnews.ui.junk.manualMarkAsJunkMarksRead".
+ // 2. Even though move/delete on manual mark may be
+ // turned off, we might still need to mark as read.
+
+ rv = srcFolder->MarkMessagesRead(mJunkHdrs, msgsAreJunk);
+ NoteChange(0, 0, nsMsgViewNotificationCode::none);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "marking marked-as-junk messages as read failed");
+ }
+
+ if (moveMessages) {
+ // Check if one of the messages to be junked is actually selected.
+ // If more than one message being junked, one must be selected.
+ // If no tree selection at all, must be in stand-alone message window.
+ bool junkedMsgSelected = numJunkHdrs > 1 || !mTreeSelection;
+ for (nsMsgViewIndex junkIndex = 0;
+ !junkedMsgSelected && junkIndex < numJunkHdrs; junkIndex++) {
+ nsMsgViewIndex hdrIndex = FindHdr(mJunkHdrs[junkIndex]);
+ if (hdrIndex != nsMsgViewIndex_None)
+ mTreeSelection->IsSelected(hdrIndex, &junkedMsgSelected);
+ }
+
+ // If a junked msg is selected, tell the FE to call
+ // SetNextMessageAfterDelete() because a delete is coming.
+ if (junkedMsgSelected) {
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ if (commandUpdater) {
+ rv = commandUpdater->UpdateNextMessageAfterDelete();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ if (targetFolder) {
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = copyService->CopyMessages(srcFolder, mJunkHdrs, targetFolder, true,
+ nullptr, msgWindow, true);
+ } else if (msgsAreJunk) {
+ if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) {
+ // Unfortunately the DeleteMessages in this case is interpreted by
+ // IMAP as a delete toggle. So what we have to do is to assemble a
+ // new delete array, keeping only those that are not deleted.
+ nsTArray<RefPtr<nsIMsgDBHdr>> hdrsToDelete;
+ for (nsIMsgDBHdr* msgHdr : mJunkHdrs) {
+ if (msgHdr) {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ if (!(flags & nsMsgMessageFlags::IMAPDeleted)) {
+ hdrsToDelete.AppendElement(msgHdr);
+ }
+ }
+ }
+
+ if (!hdrsToDelete.IsEmpty())
+ rv = srcFolder->DeleteMessages(hdrsToDelete, msgWindow, false, false,
+ nullptr, true);
+ } else {
+ rv = srcFolder->DeleteMessages(mJunkHdrs, msgWindow, false, false,
+ nullptr, true);
+ }
+ } else if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) {
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(srcFolder));
+ nsTArray<nsMsgKey> imapUids(numJunkHdrs);
+ for (nsIMsgDBHdr* msgHdr : mJunkHdrs) {
+ nsMsgKey key;
+ msgHdr->GetMessageKey(&key);
+ imapUids.AppendElement(key);
+ }
+
+ imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, imapUids, nullptr);
+ }
+
+ NoteChange(0, 0, nsMsgViewNotificationCode::none);
+
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "move or deletion of message marked-as-junk/non junk failed");
+ }
+
+ return rv;
+}
+
+nsresult nsMsgDBView::DetermineActionsForJunkChange(
+ bool msgsAreJunk, nsIMsgFolder* srcFolder, bool& moveMessages,
+ bool& changeReadState, nsIMsgFolder** targetFolder) {
+ // There are two possible actions which may be performed
+ // on messages marked as spam: marking as read and moving
+ // somewhere. When a message is marked as non junk,
+ // it may be moved to the inbox, and marked unread.
+ moveMessages = false;
+ changeReadState = false;
+
+ // The 'somewhere', junkTargetFolder, can be a folder,
+ // but if it remains null we'll delete the messages.
+ *targetFolder = nullptr;
+
+ uint32_t folderFlags;
+ srcFolder->GetFlags(&folderFlags);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = srcFolder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Handle the easy case of marking a junk message as good first.
+ // Set the move target folder to the inbox, if any.
+ if (!msgsAreJunk) {
+ if (folderFlags & nsMsgFolderFlags::Junk) {
+ prefBranch->GetBoolPref("mail.spam.markAsNotJunkMarksUnRead",
+ &changeReadState);
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rootMsgFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, targetFolder);
+ moveMessages = *targetFolder != nullptr;
+ }
+
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // When the user explicitly marks a message as junk, we can mark it as read,
+ // too. This is independent of the "markAsReadOnSpam" pref, which applies
+ // only to automatically-classified messages.
+ // Note that this behaviour should match the one in the front end for marking
+ // as junk via toolbar/context menu.
+ prefBranch->GetBoolPref("mailnews.ui.junk.manualMarkAsJunkMarksRead",
+ &changeReadState);
+
+ // Now let's determine whether we'll be taking the second action,
+ // the move / deletion (and also determine which of these two).
+ bool manualMark;
+ (void)spamSettings->GetManualMark(&manualMark);
+ if (!manualMark) return NS_OK;
+
+ int32_t manualMarkMode;
+ (void)spamSettings->GetManualMarkMode(&manualMarkMode);
+ NS_ASSERTION(manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_MOVE ||
+ manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_DELETE,
+ "bad manual mark mode");
+
+ if (manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_MOVE) {
+ // If this is a junk folder (not only "the" junk folder for this account)
+ // don't do the move.
+ if (folderFlags & nsMsgFolderFlags::Junk) return NS_OK;
+
+ nsCString spamFolderURI;
+ rv = spamSettings->GetSpamFolderURI(spamFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(!spamFolderURI.IsEmpty(),
+ "spam folder URI is empty, can't move");
+ if (!spamFolderURI.IsEmpty()) {
+ rv = FindFolder(spamFolderURI, targetFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*targetFolder) {
+ moveMessages = true;
+ } else {
+ // XXX TODO: GetOrCreateJunkFolder will only create a folder with
+ // localized name "Junk" regardless of spamFolderURI. So if someone
+ // sets the junk folder to an existing folder of a different name,
+ // then deletes that folder, this will fail to create the correct
+ // folder.
+ rv = GetOrCreateJunkFolder(spamFolderURI, nullptr /* aListener */);
+ if (NS_SUCCEEDED(rv))
+ rv = GetExistingFolder(spamFolderURI, targetFolder);
+
+ NS_ASSERTION(NS_SUCCEEDED(rv), "GetOrCreateJunkFolder failed");
+ }
+ }
+
+ return NS_OK;
+ }
+
+ // At this point manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_DELETE).
+
+ // If this is in the trash, let's not delete.
+ if (folderFlags & nsMsgFolderFlags::Trash) return NS_OK;
+
+ return srcFolder->GetCanDeleteMessages(&moveMessages);
+}
+
+// Reversing threads involves reversing the threads but leaving the
+// expanded messages ordered relative to the thread, so we
+// make a copy of each array and copy them over.
+void nsMsgDBView::ReverseThreads() {
+ nsTArray<uint32_t> newFlagArray;
+ nsTArray<nsMsgKey> newKeyArray;
+ nsTArray<uint8_t> newLevelArray;
+
+ uint32_t viewSize = GetSize();
+ uint32_t startThread = viewSize;
+ uint32_t nextThread = viewSize;
+ uint32_t destIndex = 0;
+
+ newKeyArray.SetLength(m_keys.Length());
+ newFlagArray.SetLength(m_flags.Length());
+ newLevelArray.SetLength(m_levels.Length());
+
+ while (startThread) {
+ startThread--;
+
+ if (m_flags[startThread] & MSG_VIEW_FLAG_ISTHREAD) {
+ for (uint32_t sourceIndex = startThread; sourceIndex < nextThread;
+ sourceIndex++) {
+ newKeyArray[destIndex] = m_keys[sourceIndex];
+ newFlagArray[destIndex] = m_flags[sourceIndex];
+ newLevelArray[destIndex] = m_levels[sourceIndex];
+ destIndex++;
+ }
+ // Because we're copying in reverse order.
+ nextThread = startThread;
+ }
+ }
+
+ m_keys.SwapElements(newKeyArray);
+ m_flags.SwapElements(newFlagArray);
+ m_levels.SwapElements(newLevelArray);
+}
+
+void nsMsgDBView::ReverseSort() {
+ uint32_t topIndex = GetSize();
+
+ nsCOMArray<nsIMsgFolder>* folders = GetFolders();
+
+ // Go up half the array swapping values.
+ for (uint32_t bottomIndex = 0; bottomIndex < --topIndex; bottomIndex++) {
+ // Swap flags.
+ uint32_t tempFlags = m_flags[bottomIndex];
+ m_flags[bottomIndex] = m_flags[topIndex];
+ m_flags[topIndex] = tempFlags;
+
+ // Swap keys.
+ nsMsgKey tempKey = m_keys[bottomIndex];
+ m_keys[bottomIndex] = m_keys[topIndex];
+ m_keys[topIndex] = tempKey;
+
+ if (folders) {
+ // Swap folders -- needed when search is done across multiple folders.
+ nsIMsgFolder* bottomFolder = folders->ObjectAt(bottomIndex);
+ nsIMsgFolder* topFolder = folders->ObjectAt(topIndex);
+ folders->ReplaceObjectAt(topFolder, bottomIndex);
+ folders->ReplaceObjectAt(bottomFolder, topIndex);
+ }
+
+ // No need to swap elements in m_levels; since we only call
+ // ReverseSort in non-threaded mode, m_levels are all the same.
+ }
+}
+
+int nsMsgDBView::FnSortIdKey(const IdKey* pItem1, const IdKey* pItem2,
+ viewSortInfo* sortInfo) {
+ int32_t retVal = 0;
+
+ nsIMsgDatabase* db = sortInfo->db;
+
+ mozilla::DebugOnly<nsresult> rv =
+ db->CompareCollationKeys(pItem1->key, pItem2->key, &retVal);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "compare failed");
+
+ if (retVal) return sortInfo->ascendingSort ? retVal : -retVal;
+
+ return sortInfo->view->SecondaryCompare(pItem1->id, pItem1->folder,
+ pItem2->id, pItem2->folder, sortInfo);
+}
+
+int nsMsgDBView::FnSortIdUint32(const IdUint32* pItem1, const IdUint32* pItem2,
+ viewSortInfo* sortInfo) {
+ if (pItem1->dword > pItem2->dword) {
+ return (sortInfo->ascendingSort) ? 1 : -1;
+ }
+
+ if (pItem1->dword < pItem2->dword) {
+ return (sortInfo->ascendingSort) ? -1 : 1;
+ }
+
+ return sortInfo->view->SecondaryCompare(pItem1->id, pItem1->folder,
+ pItem2->id, pItem2->folder, sortInfo);
+}
+
+// XXX are these still correct?
+// To compensate for memory alignment required for systems such as HP-UX, these
+// values must be 4 bytes aligned. Don't break this when modifying the
+// constants.
+const int kMaxSubjectKey = 160;
+const int kMaxLocationKey = 160; // Also used for account.
+const int kMaxAuthorKey = 160;
+const int kMaxRecipientKey = 80;
+
+// There are cases when pFieldType is not set:
+// one case returns NS_ERROR_UNEXPECTED;
+// the other case now return NS_ERROR_NULL_POINTER (this is only when
+// colHandler below is null, but is very unlikely).
+// The latter case used to return NS_OK, which was incorrect.
+nsresult nsMsgDBView::GetFieldTypeAndLenForSort(
+ nsMsgViewSortTypeValue sortType, uint16_t* pMaxLen, eFieldType* pFieldType,
+ nsIMsgCustomColumnHandler* colHandler) {
+ NS_ENSURE_ARG_POINTER(pMaxLen);
+ NS_ENSURE_ARG_POINTER(pFieldType);
+
+ switch (sortType) {
+ case nsMsgViewSortType::bySubject:
+ *pFieldType = kCollationKey;
+ *pMaxLen = kMaxSubjectKey;
+ break;
+ case nsMsgViewSortType::byAccount:
+ case nsMsgViewSortType::byTags:
+ case nsMsgViewSortType::byLocation:
+ *pFieldType = kCollationKey;
+ *pMaxLen = kMaxLocationKey;
+ break;
+ case nsMsgViewSortType::byRecipient:
+ case nsMsgViewSortType::byCorrespondent:
+ *pFieldType = kCollationKey;
+ *pMaxLen = kMaxRecipientKey;
+ break;
+ case nsMsgViewSortType::byAuthor:
+ *pFieldType = kCollationKey;
+ *pMaxLen = kMaxAuthorKey;
+ break;
+ case nsMsgViewSortType::byDate:
+ case nsMsgViewSortType::byReceived:
+ case nsMsgViewSortType::byPriority:
+ case nsMsgViewSortType::byThread:
+ case nsMsgViewSortType::byId:
+ case nsMsgViewSortType::bySize:
+ case nsMsgViewSortType::byFlagged:
+ case nsMsgViewSortType::byUnread:
+ case nsMsgViewSortType::byStatus:
+ case nsMsgViewSortType::byJunkStatus:
+ case nsMsgViewSortType::byAttachments:
+ *pFieldType = kU32;
+ *pMaxLen = 0;
+ break;
+ case nsMsgViewSortType::byCustom: {
+ if (colHandler == nullptr) {
+ NS_WARNING("colHandler is null. *pFieldType is not set.");
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ bool isString;
+ colHandler->IsString(&isString);
+
+ if (isString) {
+ *pFieldType = kCollationKey;
+ // 80 - do we need a separate k?
+ *pMaxLen = kMaxRecipientKey;
+ } else {
+ *pFieldType = kU32;
+ *pMaxLen = 0;
+ }
+ break;
+ }
+ case nsMsgViewSortType::byNone:
+ // Bug 901948.
+ return NS_ERROR_INVALID_ARG;
+ default: {
+ nsAutoCString message("unexpected switch value: sortType=");
+ message.AppendInt(sortType);
+ NS_WARNING(message.get());
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+
+ return NS_OK;
+}
+
+#define MSG_STATUS_MASK \
+ (nsMsgMessageFlags::Replied | nsMsgMessageFlags::Forwarded)
+
+nsresult nsMsgDBView::GetStatusSortValue(nsIMsgDBHdr* msgHdr,
+ uint32_t* result) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(result);
+
+ uint32_t messageFlags;
+ nsresult rv = msgHdr->GetFlags(&messageFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (messageFlags & nsMsgMessageFlags::New) {
+ // Happily, new by definition stands alone.
+ *result = 0;
+ return NS_OK;
+ }
+
+ switch (messageFlags & MSG_STATUS_MASK) {
+ case nsMsgMessageFlags::Replied:
+ *result = 2;
+ break;
+ case nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied:
+ *result = 1;
+ break;
+ case nsMsgMessageFlags::Forwarded:
+ *result = 3;
+ break;
+ default:
+ *result = (messageFlags & nsMsgMessageFlags::Read) ? 4 : 5;
+ break;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GetLongField(nsIMsgDBHdr* msgHdr,
+ nsMsgViewSortTypeValue sortType,
+ uint32_t* result,
+ nsIMsgCustomColumnHandler* colHandler) {
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(result);
+
+ bool isRead;
+ uint32_t bits;
+
+ switch (sortType) {
+ case nsMsgViewSortType::bySize:
+ rv = (mShowSizeInLines) ? msgHdr->GetLineCount(result)
+ : msgHdr->GetMessageSize(result);
+ break;
+ case nsMsgViewSortType::byPriority:
+ nsMsgPriorityValue priority;
+ rv = msgHdr->GetPriority(&priority);
+ // Treat "none" as "normal" when sorting.
+ if (priority == nsMsgPriority::none) priority = nsMsgPriority::normal;
+
+ // We want highest priority to have lowest value
+ // so ascending sort will have highest priority first.
+ *result = nsMsgPriority::highest - priority;
+ break;
+ case nsMsgViewSortType::byStatus:
+ rv = GetStatusSortValue(msgHdr, result);
+ break;
+ case nsMsgViewSortType::byFlagged:
+ bits = 0;
+ rv = msgHdr->GetFlags(&bits);
+ // Make flagged come out on top.
+ *result = !(bits & nsMsgMessageFlags::Marked);
+ break;
+ case nsMsgViewSortType::byUnread:
+ rv = msgHdr->GetIsRead(&isRead);
+ if (NS_SUCCEEDED(rv)) *result = !isRead;
+
+ break;
+ case nsMsgViewSortType::byJunkStatus: {
+ nsCString junkScoreStr;
+ rv = msgHdr->GetStringProperty("junkscore", junkScoreStr);
+ // Unscored messages should come before messages that are scored
+ // junkScoreStr is "", and "0" - "100"; normalize to 0 - 101.
+ *result = junkScoreStr.IsEmpty() ? (0) : atoi(junkScoreStr.get()) + 1;
+ break;
+ }
+ case nsMsgViewSortType::byAttachments:
+ bits = 0;
+ rv = msgHdr->GetFlags(&bits);
+ *result = !(bits & nsMsgMessageFlags::Attachment);
+ break;
+ case nsMsgViewSortType::byDate:
+ // When sorting threads by date, we may want the date of the newest msg
+ // in the thread.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) &&
+ !mSortThreadsByRoot) {
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread));
+ if (NS_SUCCEEDED(rv)) {
+ thread->GetNewestMsgDate(result);
+ break;
+ }
+ }
+ rv = msgHdr->GetDateInSeconds(result);
+ break;
+ case nsMsgViewSortType::byReceived:
+ // When sorting threads by received date, we may want the received date
+ // of the newest msg in the thread.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) &&
+ !mSortThreadsByRoot) {
+ nsCOMPtr<nsIMsgThread> thread;
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ thread->GetNewestMsgDate(result);
+ } else {
+ // Already in seconds.
+ rv = msgHdr->GetUint32Property("dateReceived", result);
+ if (*result == 0)
+ // Use Date instead, we have no Received property
+ rv = msgHdr->GetDateInSeconds(result);
+ }
+ break;
+ case nsMsgViewSortType::byCustom:
+ if (colHandler != nullptr) {
+ colHandler->GetSortLongForRow(msgHdr, result);
+ rv = NS_OK;
+ } else {
+ NS_ASSERTION(false,
+ "should not be here (Sort Type: byCustom (Long), but no "
+ "custom handler)");
+ rv = NS_ERROR_UNEXPECTED;
+ }
+ break;
+ case nsMsgViewSortType::byNone:
+ // Bug 901948.
+ return NS_ERROR_INVALID_ARG;
+
+ case nsMsgViewSortType::byId:
+ // Handled by caller, since caller knows the key.
+ default:
+ NS_ERROR("should not be here");
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+MsgViewSortColumnInfo::MsgViewSortColumnInfo(
+ const MsgViewSortColumnInfo& other) {
+ mSortType = other.mSortType;
+ mSortOrder = other.mSortOrder;
+ mCustomColumnName = other.mCustomColumnName;
+ mColHandler = other.mColHandler;
+}
+
+bool MsgViewSortColumnInfo::operator==(
+ const MsgViewSortColumnInfo& other) const {
+ return (mSortType == nsMsgViewSortType::byCustom)
+ ? mCustomColumnName.Equals(other.mCustomColumnName)
+ : mSortType == other.mSortType;
+}
+
+nsresult nsMsgDBView::EncodeColumnSort(nsString& columnSortString) {
+ for (uint32_t i = 0; i < m_sortColumns.Length(); i++) {
+ MsgViewSortColumnInfo& sortInfo = m_sortColumns[i];
+ columnSortString.Append((char)sortInfo.mSortType);
+ columnSortString.Append((char)sortInfo.mSortOrder + '0');
+ if (sortInfo.mSortType == nsMsgViewSortType::byCustom) {
+ columnSortString.Append(sortInfo.mCustomColumnName);
+ columnSortString.Append((char16_t)'\r');
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::DecodeColumnSort(nsString& columnSortString) {
+ const char16_t* stringPtr = columnSortString.BeginReading();
+ while (*stringPtr) {
+ MsgViewSortColumnInfo sortColumnInfo;
+ sortColumnInfo.mSortType = (nsMsgViewSortTypeValue)*stringPtr++;
+ sortColumnInfo.mSortOrder = (nsMsgViewSortOrderValue)(*stringPtr++) - '0';
+ if (sortColumnInfo.mSortType == nsMsgViewSortType::byCustom) {
+ while (*stringPtr && *stringPtr != '\r')
+ sortColumnInfo.mCustomColumnName.Append(*stringPtr++);
+
+ sortColumnInfo.mColHandler =
+ GetColumnHandler(sortColumnInfo.mCustomColumnName);
+
+ // Advance past '\r'.
+ if (*stringPtr) stringPtr++;
+ }
+
+ m_sortColumns.AppendElement(sortColumnInfo);
+ }
+
+ return NS_OK;
+}
+
+// Secondary Sort Key: when you select a column to sort, that
+// becomes the new Primary sort key, and all previous sort keys
+// become secondary. For example, if you first click on Date,
+// the messages are sorted by Date; then click on From, and now the
+// messages are sorted by From, and for each value of From the
+// messages are in Date order.
+
+void nsMsgDBView::PushSort(const MsgViewSortColumnInfo& newSort) {
+ // Handle byNone (bug 901948) ala a mail/base/modules/DBViewerWrapper.jsm
+ // where we don't push the secondary sort type if it's ::byNone;
+ // (and secondary sort type is NOT the same as the first sort type
+ // there). This code should behave the same way.
+
+ // We don't expect to be passed sort type ::byNone,
+ // but if we are it's safe to ignore it.
+ if (newSort.mSortType == nsMsgViewSortType::byNone) return;
+
+ // byId is a unique key (misnamed as Order Received). If we are sorting byId,
+ // we don't need to keep any secondary sort keys.
+ if (newSort.mSortType == nsMsgViewSortType::byId) m_sortColumns.Clear();
+
+ m_sortColumns.RemoveElement(newSort);
+ m_sortColumns.InsertElementAt(0, newSort);
+ if (m_sortColumns.Length() > kMaxNumSortColumns)
+ m_sortColumns.RemoveElementAt(kMaxNumSortColumns);
+}
+
+nsresult nsMsgDBView::GetCollationKey(nsIMsgDBHdr* msgHdr,
+ nsMsgViewSortTypeValue sortType,
+ nsTArray<uint8_t>& result,
+ nsIMsgCustomColumnHandler* colHandler) {
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ NS_ENSURE_ARG_POINTER(msgHdr);
+
+ switch (sortType) {
+ case nsMsgViewSortType::bySubject:
+ rv = msgHdr->GetSubjectCollationKey(result);
+ break;
+ case nsMsgViewSortType::byLocation:
+ rv = GetLocationCollationKey(msgHdr, result);
+ break;
+ case nsMsgViewSortType::byRecipient: {
+ nsString recipients;
+ rv = FetchRecipients(msgHdr, recipients);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = dbToUse->CreateCollationKey(recipients, result);
+ }
+ break;
+ }
+ case nsMsgViewSortType::byAuthor: {
+ rv = msgHdr->GetAuthorCollationKey(result);
+ nsString author;
+ rv = FetchAuthor(msgHdr, author);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = dbToUse->CreateCollationKey(author, result);
+ }
+ break;
+ }
+ case nsMsgViewSortType::byAccount:
+ case nsMsgViewSortType::byTags: {
+ nsString str;
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+
+ if (!dbToUse)
+ // Probably a search view.
+ GetDBForViewIndex(0, getter_AddRefs(dbToUse));
+
+ rv = (sortType == nsMsgViewSortType::byAccount)
+ ? FetchAccount(msgHdr, str)
+ : FetchTags(msgHdr, str);
+ if (NS_SUCCEEDED(rv) && dbToUse)
+ rv = dbToUse->CreateCollationKey(str, result);
+
+ break;
+ }
+ case nsMsgViewSortType::byCustom:
+ if (colHandler != nullptr) {
+ nsAutoString strKey;
+ rv = colHandler->GetSortStringForRow(msgHdr, strKey);
+ NS_ASSERTION(NS_SUCCEEDED(rv),
+ "failed to get sort string for custom row");
+ nsAutoString strTemp(strKey);
+
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = dbToUse->CreateCollationKey(strKey, result);
+ } else {
+ NS_ERROR(
+ "should not be here (Sort Type: byCustom (String), but no custom "
+ "handler)");
+ rv = NS_ERROR_UNEXPECTED;
+ }
+ break;
+ case nsMsgViewSortType::byCorrespondent: {
+ nsString value;
+ if (IsOutgoingMsg(msgHdr))
+ rv = FetchRecipients(msgHdr, value);
+ else
+ rv = FetchAuthor(msgHdr, value);
+
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = dbToUse->CreateCollationKey(value, result);
+ }
+ break;
+ }
+ default:
+ rv = NS_ERROR_UNEXPECTED;
+ break;
+ }
+
+ // Bailing out with failure will stop the sort and leave us in
+ // a bad state. Try to continue on, instead.
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to get the collation key");
+ if (NS_FAILED(rv)) {
+ result.Clear();
+ }
+
+ return NS_OK;
+}
+
+// As the location collation key is created getting folder from the msgHdr,
+// it is defined in this file and not from the db.
+nsresult nsMsgDBView::GetLocationCollationKey(nsIMsgDBHdr* msgHdr,
+ nsTArray<uint8_t>& result) {
+ nsCOMPtr<nsIMsgFolder> folder;
+
+ nsresult rv = msgHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ rv = folder->GetMsgDatabase(getter_AddRefs(dbToUse));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString locationString;
+ rv = folder->GetPrettyName(locationString);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return dbToUse->CreateCollationKey(locationString, result);
+}
+
+nsresult nsMsgDBView::SaveSortInfo(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ if (m_viewFolder) {
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo) {
+ // Save off sort type and order, view type and flags.
+ folderInfo->SetSortType(sortType);
+ folderInfo->SetSortOrder(sortOrder);
+
+ nsString sortColumnsString;
+ rv = EncodeColumnSort(sortColumnsString);
+ NS_ENSURE_SUCCESS(rv, rv);
+ folderInfo->SetProperty("sortColumns", sortColumnsString);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::RestoreSortInfo() {
+ if (!m_viewFolder) return NS_OK;
+
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && folderInfo) {
+ // Restore m_sortColumns from db.
+ nsString sortColumnsString;
+ folderInfo->GetProperty("sortColumns", sortColumnsString);
+ DecodeColumnSort(sortColumnsString);
+ if (m_sortColumns.Length() > 1) {
+ m_secondarySort = m_sortColumns[1].mSortType;
+ m_secondarySortOrder = m_sortColumns[1].mSortOrder;
+ m_secondaryCustomColumn = m_sortColumns[1].mCustomColumnName;
+ }
+
+ // Restore curCustomColumn from db.
+ folderInfo->GetProperty("customSortCol", m_curCustomColumn);
+ }
+
+ return NS_OK;
+}
+
+// Called by msgDBView::Sort, at which point any persisted active custom
+// columns must be registered. If not, reset their m_sortColumns entries
+// to byDate; Sort will fill in values if necessary based on new user sort.
+void nsMsgDBView::EnsureCustomColumnsValid() {
+ if (!m_sortColumns.Length()) return;
+
+ for (uint32_t i = 0; i < m_sortColumns.Length(); i++) {
+ if (m_sortColumns[i].mSortType == nsMsgViewSortType::byCustom &&
+ m_sortColumns[i].mColHandler == nullptr) {
+ m_sortColumns[i].mSortType = nsMsgViewSortType::byDate;
+ m_sortColumns[i].mCustomColumnName.Truncate();
+ // There are only two...
+ if (i == 0 && m_sortType != nsMsgViewSortType::byCustom)
+ SetCurCustomColumn(EmptyString());
+ if (i == 1) m_secondaryCustomColumn.Truncate();
+ }
+ }
+}
+
+int32_t nsMsgDBView::SecondaryCompare(nsMsgKey key1, nsIMsgFolder* folder1,
+ nsMsgKey key2, nsIMsgFolder* folder2,
+ viewSortInfo* comparisonContext) {
+ nsMsgViewSortTypeValue sortType = comparisonContext->view->m_secondarySort;
+ bool isAscendingSort = comparisonContext->view->m_secondarySortOrder ==
+ nsMsgViewSortOrder::ascending;
+
+ // We need to make sure that in the case of the secondary sort field also
+ // matching, we don't recurse.
+ if (comparisonContext->isSecondarySort ||
+ sortType == nsMsgViewSortType::byId) {
+ if (key1 > key2) {
+ return isAscendingSort ? 1 : -1;
+ }
+
+ if (key1 < key2) {
+ return isAscendingSort ? -1 : 1;
+ }
+
+ return 0;
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> hdr1, hdr2;
+ nsresult rv = folder1->GetMessageHeader(key1, getter_AddRefs(hdr1));
+ NS_ENSURE_SUCCESS(rv, 0);
+ rv = folder2->GetMessageHeader(key2, getter_AddRefs(hdr2));
+ NS_ENSURE_SUCCESS(rv, 0);
+ IdKey EntryInfo1, EntryInfo2;
+
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the *secondary* sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = nullptr;
+ if (sortType == nsMsgViewSortType::byCustom &&
+ comparisonContext->view->m_sortColumns.Length() > 1) {
+ colHandler = comparisonContext->view->m_sortColumns[1].mColHandler;
+ }
+
+ // The following may leave fieldType undefined.
+ // In this case, we can return 0 right away since
+ // it is the value returned in the default case of
+ // switch (fieldType) statement below.
+ rv = GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler);
+ NS_ENSURE_SUCCESS(rv, 0);
+
+ hdr1->GetMessageKey(&EntryInfo1.id);
+ hdr2->GetMessageKey(&EntryInfo2.id);
+
+ // Set up new viewSortInfo data for our secondary comparison.
+ viewSortInfo ctx = {
+ .view = comparisonContext->view,
+ .db = comparisonContext->db,
+ .isSecondarySort = true, // To avoid recursing back here!
+ .ascendingSort = isAscendingSort,
+ };
+
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(hdr1, sortType, EntryInfo1.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ rv = GetCollationKey(hdr2, sortType, EntryInfo2.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+
+ return FnSortIdKey(&EntryInfo1, &EntryInfo2, &ctx);
+ case kU32:
+ if (sortType == nsMsgViewSortType::byId) {
+ EntryInfo1.dword = EntryInfo1.id;
+ EntryInfo2.dword = EntryInfo2.id;
+ } else {
+ GetLongField(hdr1, sortType, &EntryInfo1.dword, colHandler);
+ GetLongField(hdr2, sortType, &EntryInfo2.dword, colHandler);
+ }
+ return FnSortIdUint32(&EntryInfo1, &EntryInfo2, &ctx);
+ default:
+ return 0;
+ }
+}
+
+NS_IMETHODIMP nsMsgDBView::Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ EnsureCustomColumnsValid();
+
+ // If we're doing a stable sort, we can't just reverse the messages.
+ // Check also that the custom column we're sorting on hasn't changed.
+ // Otherwise, to be on the safe side, resort.
+ // Note: m_curCustomColumn is the desired (possibly new) custom column name,
+ // while m_sortColumns[0].mCustomColumnName is the name for the last completed
+ // sort, since these are persisted after each sort.
+ if (m_sortType == sortType && m_sortValid &&
+ (sortType != nsMsgViewSortType::byCustom ||
+ (sortType == nsMsgViewSortType::byCustom && m_sortColumns.Length() &&
+ m_sortColumns[0].mCustomColumnName.Equals(m_curCustomColumn))) &&
+ m_sortColumns.Length() < 2) {
+ // Same as it ever was. Do nothing.
+ if (m_sortOrder == sortOrder) return NS_OK;
+
+ // For secondary sort, remember the sort order on a per column basis.
+ if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = sortOrder;
+
+ SaveSortInfo(sortType, sortOrder);
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ ReverseThreads();
+ } else {
+ ReverseSort();
+ }
+
+ m_sortOrder = sortOrder;
+ // We just reversed the sort order, we still need to invalidate the view.
+ return NS_OK;
+ }
+
+ if (sortType == nsMsgViewSortType::byThread) return NS_OK;
+
+ // If a sortType has changed, or the sortType is byCustom and a column has
+ // changed, this is the new primary sortColumnInfo.
+ // Note: m_curCustomColumn is the desired (possibly new) custom column name,
+ // while m_sortColumns[0].mCustomColumnName is the name for the last completed
+ // sort, since these are persisted after each sort.
+ if (m_sortType != sortType ||
+ (sortType == nsMsgViewSortType::byCustom && m_sortColumns.Length() &&
+ !m_sortColumns[0].mCustomColumnName.Equals(m_curCustomColumn))) {
+ // For secondary sort, remember the sort order of the original primary sort!
+ if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = m_sortOrder;
+
+ MsgViewSortColumnInfo sortColumnInfo;
+ sortColumnInfo.mSortType = sortType;
+ sortColumnInfo.mSortOrder = sortOrder;
+ if (sortType == nsMsgViewSortType::byCustom) {
+ GetCurCustomColumn(sortColumnInfo.mCustomColumnName);
+ sortColumnInfo.mColHandler = GetCurColumnHandler();
+ }
+
+ PushSort(sortColumnInfo);
+ } else {
+ // For primary sort, remember the sort order on a per column basis.
+ if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = sortOrder;
+ }
+
+ if (m_sortColumns.Length() > 1) {
+ m_secondarySort = m_sortColumns[1].mSortType;
+ m_secondarySortOrder = m_sortColumns[1].mSortOrder;
+ m_secondaryCustomColumn = m_sortColumns[1].mCustomColumnName;
+ }
+
+ SaveSortInfo(sortType, sortOrder);
+ // Figure out how much memory we'll need, and then malloc it.
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // If we did not obtain proper fieldType, it needs to be checked
+ // because the subsequent code does not handle it very well.
+ nsresult rv =
+ GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler);
+
+ // Don't sort if the field type is not supported: Bug 901948.
+ if (NS_FAILED(rv)) return NS_OK;
+
+ nsTArray<void*> ptrs;
+ uint32_t arraySize = GetSize();
+
+ if (!arraySize) return NS_OK;
+
+ nsCOMArray<nsIMsgFolder>* folders = GetFolders();
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ // Probably a search view.
+ if (!dbToUse) {
+ GetDBForViewIndex(0, getter_AddRefs(dbToUse));
+ if (!dbToUse) return NS_ERROR_FAILURE;
+ }
+
+ viewSortInfo qsPrivateData{
+ .view = this,
+ .db = dbToUse,
+ .isSecondarySort = false,
+ .ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending),
+ };
+
+ switch (fieldType) {
+ case kCollationKey: {
+ // Sort on a non-numeric field. We'll be calculating a collation key for
+ // each message.
+ nsTArray<IdKey> entries;
+ entries.SetLength(arraySize);
+ nsTArray<IdKey*> pPtrBase;
+ pPtrBase.SetLength(arraySize);
+ for (uint32_t i = 0; i < arraySize; ++i) {
+ IdKey* info = &entries[i];
+ pPtrBase[i] = info;
+ info->id = m_keys[i];
+ info->bits = m_flags[i];
+ info->dword = 0;
+ info->folder = folders ? folders->ObjectAt(i) : m_folder.get();
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+ NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "header not found");
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetCollationKey(msgHdr, sortType, info->key, colHandler);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Perform the sort.
+ std::sort(pPtrBase.begin(), pPtrBase.end(),
+ [&qsPrivateData](const auto& lhs, const auto& rhs) {
+ return FnSortIdKey(lhs, rhs, &qsPrivateData) < 0;
+ });
+
+ // Now update the view state to reflect the new order.
+ for (uint32_t i = 0; i < arraySize; ++i) {
+ m_keys[i] = pPtrBase[i]->id;
+ m_flags[i] = pPtrBase[i]->bits;
+ if (folders) folders->ReplaceObjectAt(pPtrBase[i]->folder, i);
+ }
+ m_sortType = sortType;
+ m_sortOrder = sortOrder;
+ m_sortValid = true;
+ return NS_OK;
+ }
+ case kU32: {
+ // Sort on a numeric field.
+ nsTArray<IdUint32> entries;
+ entries.SetLength(arraySize);
+ nsTArray<IdUint32*> pPtrBase;
+ pPtrBase.SetLength(arraySize);
+ for (uint32_t i = 0; i < arraySize; ++i) {
+ IdUint32* info = &entries[i];
+ pPtrBase[i] = info;
+ info->id = m_keys[i];
+ info->bits = m_flags[i];
+ info->folder = folders ? folders->ObjectAt(i) : m_folder.get();
+ if (sortType == nsMsgViewSortType::byId) {
+ info->dword = info->id; // No msgHdr required.
+ } else {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+ NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "header not found");
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetLongField(msgHdr, sortType, &info->dword, colHandler);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ // Perform the sort.
+ std::sort(pPtrBase.begin(), pPtrBase.end(),
+ [&qsPrivateData](const auto& lhs, const auto& rhs) {
+ return FnSortIdUint32(lhs, rhs, &qsPrivateData) < 0;
+ });
+
+ // Now update the view state to reflect the new order.
+ for (uint32_t i = 0; i < arraySize; ++i) {
+ m_keys[i] = pPtrBase[i]->id;
+ m_flags[i] = pPtrBase[i]->bits;
+ if (folders) folders->ReplaceObjectAt(pPtrBase[i]->folder, i);
+ }
+ m_sortType = sortType;
+ m_sortOrder = sortOrder;
+ m_sortValid = true;
+ return NS_OK;
+ }
+ default:
+ // If we get this far, we've got a bad fieldType.
+ return NS_ERROR_UNEXPECTED;
+ }
+}
+
+nsMsgViewIndex nsMsgDBView::GetIndexOfFirstDisplayedKeyInThread(
+ nsIMsgThread* threadHdr, bool allowDummy) {
+ nsMsgViewIndex retIndex = nsMsgViewIndex_None;
+ uint32_t childIndex = 0;
+ // We could speed up the unreadOnly view by starting our search with the first
+ // unread message in the thread. Sometimes, that will be wrong, however, so
+ // let's skip it until we're sure it's necessary.
+ // (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly)
+ // ? threadHdr->GetFirstUnreadKey(m_db) : threadHdr->GetChildAt(0);
+ uint32_t numThreadChildren;
+ threadHdr->GetNumChildren(&numThreadChildren);
+ while (retIndex == nsMsgViewIndex_None && childIndex < numThreadChildren) {
+ nsCOMPtr<nsIMsgDBHdr> childHdr;
+ threadHdr->GetChildHdrAt(childIndex++, getter_AddRefs(childHdr));
+ if (childHdr) retIndex = FindHdr(childHdr, 0, allowDummy);
+ }
+
+ return retIndex;
+}
+
+nsresult nsMsgDBView::GetFirstMessageHdrToDisplayInThread(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr** result) {
+ nsresult rv;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly)
+ rv = threadHdr->GetFirstUnreadChild(result);
+ else
+ rv = threadHdr->GetChildHdrAt(0, result);
+
+ return rv;
+}
+
+// Find the view index of the thread containing the passed msgKey, if
+// the thread is in the view. MsgIndex is passed in as a shortcut if
+// it turns out the msgKey is the first message in the thread,
+// then we can avoid looking for the msgKey.
+nsMsgViewIndex nsMsgDBView::ThreadIndexOfMsg(
+ nsMsgKey msgKey, nsMsgViewIndex msgIndex /* = nsMsgViewIndex_None */,
+ int32_t* pThreadCount /* = NULL */, uint32_t* pFlags /* = NULL */) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ return nsMsgViewIndex_None;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = m_db->GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None);
+ return ThreadIndexOfMsgHdr(msgHdr, msgIndex, pThreadCount, pFlags);
+}
+
+nsMsgViewIndex nsMsgDBView::GetThreadIndex(nsMsgViewIndex msgIndex) {
+ if (!IsValidIndex(msgIndex)) return nsMsgViewIndex_None;
+
+ // Scan up looking for level 0 message.
+ while (m_levels[msgIndex] && msgIndex) --msgIndex;
+
+ return msgIndex;
+}
+
+nsMsgViewIndex nsMsgDBView::ThreadIndexOfMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex msgIndex,
+ int32_t* pThreadCount,
+ uint32_t* pFlags) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsresult rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None);
+
+ nsMsgViewIndex retIndex = nsMsgViewIndex_None;
+
+ if (threadHdr != nullptr) {
+ if (msgIndex == nsMsgViewIndex_None) msgIndex = FindHdr(msgHdr, 0, true);
+
+ // Hdr is not in view, need to find by thread.
+ if (msgIndex == nsMsgViewIndex_None) {
+ msgIndex = GetIndexOfFirstDisplayedKeyInThread(threadHdr, true);
+ // nsMsgKey threadKey = (msgIndex == nsMsgViewIndex_None) ? nsMsgKey_None
+ // :
+ // GetAt(msgIndex);
+ if (pFlags) threadHdr->GetFlags(pFlags);
+ }
+
+ nsMsgViewIndex startOfThread = msgIndex;
+ while ((int32_t)startOfThread >= 0 && m_levels[startOfThread] != 0)
+ startOfThread--;
+
+ retIndex = startOfThread;
+ if (pThreadCount) {
+ int32_t numChildren = 0;
+ nsMsgViewIndex threadIndex = startOfThread;
+ do {
+ threadIndex++;
+ numChildren++;
+ } while (threadIndex < m_levels.Length() && m_levels[threadIndex] != 0);
+
+ *pThreadCount = numChildren;
+ }
+ }
+
+ return retIndex;
+}
+
+nsMsgKey nsMsgDBView::GetKeyOfFirstMsgInThread(nsMsgKey key) {
+ // Just report no key for any failure. This can occur when a
+ // message is deleted from a threaded view.
+ nsCOMPtr<nsIMsgThread> pThread;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = m_db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ if (NS_FAILED(rv)) return nsMsgKey_None;
+
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread));
+ if (NS_FAILED(rv)) return nsMsgKey_None;
+
+ nsMsgKey firstKeyInThread = nsMsgKey_None;
+
+ if (!pThread) return firstKeyInThread;
+
+ // ### dmb UnreadOnly - this is wrong. But didn't seem to matter in 4.x
+ pThread->GetChildKeyAt(0, &firstKeyInThread);
+ return firstKeyInThread;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetKeyAt(nsMsgViewIndex index, nsMsgKey* result) {
+ NS_ENSURE_ARG(result);
+ *result = GetAt(index);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetFlagsAt(nsMsgViewIndex aIndex, uint32_t* aResult) {
+ NS_ENSURE_ARG(aResult);
+ if (!IsValidIndex(aIndex)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ *aResult = m_flags[aIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetMsgHdrAt(nsMsgViewIndex aIndex, nsIMsgDBHdr** aResult) {
+ NS_ENSURE_ARG(aResult);
+ if (!IsValidIndex(aIndex)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ return GetMsgHdrForViewIndex(aIndex, aResult);
+}
+
+nsMsgViewIndex nsMsgDBView::FindHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startIndex,
+ bool allowDummy) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ nsMsgViewIndex viewIndex = m_keys.IndexOf(msgKey, startIndex);
+ if (viewIndex == nsMsgViewIndex_None) return viewIndex;
+
+ // If we're supposed to allow dummies, and the previous index is a dummy that
+ // is not elided, then it must be the dummy corresponding to our node and
+ // we should return that instead.
+ if (allowDummy && viewIndex &&
+ (m_flags[viewIndex - 1] & MSG_VIEW_FLAG_DUMMY) &&
+ !(m_flags[viewIndex - 1] & nsMsgMessageFlags::Elided)) {
+ viewIndex--;
+ } else if (!allowDummy && m_flags[viewIndex] & MSG_VIEW_FLAG_DUMMY) {
+ // We're not allowing dummies, and we found a dummy, look again
+ // one past the dummy.
+ return m_keys.IndexOf(msgKey, viewIndex + 1);
+ }
+
+ // Check that the message we found matches the message we were looking for.
+ if (viewIndex != nsMsgViewIndex_None) {
+ nsCOMPtr<nsIMsgDBHdr> foundMsgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(foundMsgHdr));
+ if (NS_FAILED(rv) || foundMsgHdr != msgHdr) {
+ viewIndex = nsMsgViewIndex_None;
+ }
+ }
+
+ return viewIndex;
+}
+
+nsMsgViewIndex nsMsgDBView::FindKey(nsMsgKey key, bool expand) {
+ nsMsgViewIndex retIndex = nsMsgViewIndex_None;
+ retIndex = (nsMsgViewIndex)(m_keys.IndexOf(key));
+ // For dummy headers, try to expand if the caller says so. And if the thread
+ // is expanded, ignore the dummy header and return the real header index.
+ if (retIndex != nsMsgViewIndex_None &&
+ m_flags[retIndex] & MSG_VIEW_FLAG_DUMMY &&
+ !(m_flags[retIndex] & nsMsgMessageFlags::Elided)) {
+ return (nsMsgViewIndex)m_keys.IndexOf(key, retIndex + 1);
+ }
+
+ if (key != nsMsgKey_None &&
+ (retIndex == nsMsgViewIndex_None ||
+ m_flags[retIndex] & MSG_VIEW_FLAG_DUMMY) &&
+ expand && m_db) {
+ nsMsgKey threadKey = GetKeyOfFirstMsgInThread(key);
+ if (threadKey != nsMsgKey_None) {
+ nsMsgViewIndex threadIndex = FindKey(threadKey, false);
+ if (threadIndex != nsMsgViewIndex_None) {
+ uint32_t flags = m_flags[threadIndex];
+ if ((flags & nsMsgMessageFlags::Elided &&
+ NS_SUCCEEDED(ExpandByIndex(threadIndex, nullptr))) ||
+ flags & MSG_VIEW_FLAG_DUMMY) {
+ retIndex = (nsMsgViewIndex)m_keys.IndexOf(key, threadIndex + 1);
+ }
+ }
+ }
+ }
+
+ return retIndex;
+}
+
+nsresult nsMsgDBView::GetThreadCount(nsMsgViewIndex index,
+ uint32_t* pThreadCount) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgThread> pThread;
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread));
+ if (NS_SUCCEEDED(rv) && pThread != nullptr)
+ rv = pThread->GetNumChildren(pThreadCount);
+
+ return rv;
+}
+
+// This counts the number of messages in an expanded thread, given the
+// index of the first message in the thread.
+int32_t nsMsgDBView::CountExpandedThread(nsMsgViewIndex index) {
+ int32_t numInThread = 0;
+ nsMsgViewIndex startOfThread = index;
+ while ((int32_t)startOfThread >= 0 && m_levels[startOfThread] != 0)
+ startOfThread--;
+
+ nsMsgViewIndex threadIndex = startOfThread;
+ do {
+ threadIndex++;
+ numInThread++;
+ } while (threadIndex < m_levels.Length() && m_levels[threadIndex] != 0);
+
+ return numInThread;
+}
+
+// Returns the number of lines that would be added (> 0) or removed (< 0)
+// if we were to try to expand/collapse the passed index.
+nsresult nsMsgDBView::ExpansionDelta(nsMsgViewIndex index,
+ int32_t* expansionDelta) {
+ uint32_t numChildren;
+ nsresult rv;
+
+ *expansionDelta = 0;
+ if (index >= ((nsMsgViewIndex)m_keys.Length()))
+ return NS_MSG_MESSAGE_NOT_FOUND;
+
+ char flags = m_flags[index];
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) return NS_OK;
+
+ // The client can pass in the key of any message
+ // in a thread and get the expansion delta for the thread.
+
+ if (flags & nsMsgMessageFlags::Elided) {
+ rv = GetThreadCount(index, &numChildren);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *expansionDelta = numChildren - 1;
+ } else {
+ numChildren = CountExpandedThread(index);
+ *expansionDelta = -(int32_t)(numChildren - 1);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ToggleExpansion(nsMsgViewIndex index,
+ uint32_t* numChanged) {
+ nsresult rv;
+ NS_ENSURE_ARG(numChanged);
+ *numChanged = 0;
+ nsMsgViewIndex threadIndex = GetThreadIndex(index);
+ if (threadIndex == nsMsgViewIndex_None) {
+ NS_ASSERTION(false, "couldn't find thread");
+ return NS_MSG_MESSAGE_NOT_FOUND;
+ }
+
+ int32_t flags = m_flags[threadIndex];
+
+ // If not a thread, or doesn't have children, no expand/collapse.
+ // If we add sub-thread expand collapse, this will need to be relaxed.
+ if (!(flags & MSG_VIEW_FLAG_ISTHREAD) || !(flags & MSG_VIEW_FLAG_HASCHILDREN))
+ return NS_MSG_MESSAGE_NOT_FOUND;
+
+ if (flags & nsMsgMessageFlags::Elided)
+ rv = ExpandByIndex(threadIndex, numChanged);
+ else
+ rv = CollapseByIndex(threadIndex, numChanged);
+
+ // If we collaps/uncollapse a thread, this changes the selected URIs.
+ SelectionChangedXPCOM();
+ return rv;
+}
+
+nsresult nsMsgDBView::ExpandAndSelectThread() {
+ nsresult rv;
+
+ NS_ASSERTION(mTreeSelection, "no tree selection");
+ if (!mTreeSelection) return NS_ERROR_UNEXPECTED;
+
+ int32_t index;
+ rv = mTreeSelection->GetCurrentIndex(&index);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = ExpandAndSelectThreadByIndex(index, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ExpandAndSelectThreadByIndex(nsMsgViewIndex index,
+ bool augment) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsresult rv;
+
+ nsMsgViewIndex threadIndex;
+ bool inThreadedMode = (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay);
+
+ if (inThreadedMode) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ threadIndex = ThreadIndexOfMsgHdr(msgHdr, index);
+ if (threadIndex == nsMsgViewIndex_None) {
+ NS_ASSERTION(false, "couldn't find thread");
+ return NS_MSG_MESSAGE_NOT_FOUND;
+ }
+ } else {
+ threadIndex = index;
+ }
+
+ int32_t flags = m_flags[threadIndex];
+ int32_t count = 0;
+
+ if (inThreadedMode && flags & MSG_VIEW_FLAG_ISTHREAD &&
+ flags & MSG_VIEW_FLAG_HASCHILDREN) {
+ // If closed, expand this thread.
+ if (flags & nsMsgMessageFlags::Elided) {
+ uint32_t numExpanded;
+ rv = ExpandByIndex(threadIndex, &numExpanded);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Get the number of messages in the expanded thread so we know how many
+ // to select.
+ count = CountExpandedThread(threadIndex);
+ } else {
+ count = 1;
+ }
+
+ NS_ASSERTION(count > 0, "bad count");
+
+ // Update the selection.
+
+ NS_ASSERTION(mTreeSelection, "no tree selection");
+ if (!mTreeSelection) return NS_ERROR_UNEXPECTED;
+
+ // The count should be 1 or greater. If there was only one message in the
+ // thread, we just select it. If more, we select all of them.
+ mTreeSelection->RangedSelect(threadIndex + count - 1, threadIndex, augment);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ExpandAll() {
+ if (mTree) mTree->BeginUpdateBatch();
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+
+ for (int32_t i = GetSize() - 1; i >= 0; i--) {
+ uint32_t numExpanded;
+ uint32_t flags = m_flags[i];
+ if (flags & nsMsgMessageFlags::Elided) ExpandByIndex(i, &numExpanded);
+ }
+
+ if (mTree) mTree->EndUpdateBatch();
+ if (mJSTree) mJSTree->EndUpdateBatch();
+
+ SelectionChangedXPCOM();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(pThread);
+ if (!m_db) return NS_ERROR_FAILURE;
+ return m_db->GetThreadContainingMsgHdr(msgHdr, pThread);
+}
+
+nsresult nsMsgDBView::ExpandByIndex(nsMsgViewIndex index,
+ uint32_t* pNumExpanded) {
+ if ((uint32_t)index >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ uint32_t flags = m_flags[index];
+ uint32_t numExpanded = 0;
+
+ NS_ASSERTION(flags & nsMsgMessageFlags::Elided,
+ "can't expand an already expanded thread");
+ flags &= ~nsMsgMessageFlags::Elided;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> pThread;
+ nsresult rv = GetThreadContainingIndex(index, getter_AddRefs(pThread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) {
+ // Keep top level hdr in thread, even though read.
+ if (flags & nsMsgMessageFlags::Read) {
+ m_levels.AppendElement(0);
+ }
+
+ rv = ListUnreadIdsInThread(pThread, index, &numExpanded);
+ } else {
+ rv = ListIdsInThread(pThread, index, &numExpanded);
+ }
+
+ if (numExpanded > 0) {
+ m_flags[index] = flags;
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ NoteChange(index + 1, numExpanded, nsMsgViewNotificationCode::insertOrDelete);
+
+ if (pNumExpanded != nullptr) *pNumExpanded = numExpanded;
+
+ return rv;
+}
+
+nsresult nsMsgDBView::CollapseAll() {
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+ for (uint32_t i = 0; i < GetSize(); i++) {
+ uint32_t numExpanded;
+ uint32_t flags = m_flags[i];
+ if (!(flags & nsMsgMessageFlags::Elided) &&
+ (flags & MSG_VIEW_FLAG_HASCHILDREN))
+ CollapseByIndex(i, &numExpanded);
+ }
+
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ SelectionChangedXPCOM();
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::CollapseByIndex(nsMsgViewIndex index,
+ uint32_t* pNumCollapsed) {
+ nsresult rv;
+ int32_t flags = m_flags[index];
+ int32_t rowDelta = 0;
+
+ if (flags & nsMsgMessageFlags::Elided ||
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) ||
+ !(flags & MSG_VIEW_FLAG_HASCHILDREN)) {
+ return NS_OK;
+ }
+
+ if (index > m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ rv = ExpansionDelta(index, &rowDelta);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ flags |= nsMsgMessageFlags::Elided;
+
+ m_flags[index] = flags;
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+
+ // Don't count first header in thread.
+ int32_t numRemoved = -rowDelta;
+ if (index + 1 + numRemoved > m_keys.Length()) {
+ NS_ERROR("trying to remove too many rows");
+ numRemoved -= (index + 1 + numRemoved) - m_keys.Length();
+ if (numRemoved <= 0) return NS_MSG_MESSAGE_NOT_FOUND;
+ }
+
+ // Start at first id after thread.
+ RemoveRows(index + 1, numRemoved);
+ if (pNumCollapsed != nullptr) *pNumCollapsed = numRemoved;
+
+ NoteChange(index + 1, rowDelta, nsMsgViewNotificationCode::insertOrDelete);
+
+ return rv;
+}
+
+nsresult nsMsgDBView::OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool /*ensureListed*/) {
+ nsresult rv = NS_OK;
+ // Views can override this behaviour, which is to append to view.
+ // This is the mail behaviour, but threaded views will want
+ // to insert in order...
+ if (newHdr) rv = AddHdr(newHdr);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetThreadContainingIndex(nsMsgViewIndex index,
+ nsIMsgThread** resultThread) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return GetThreadContainingMsgHdr(msgHdr, resultThread);
+}
+
+nsMsgViewIndex nsMsgDBView::GetIndexForThread(nsIMsgDBHdr* msgHdr) {
+ // Take advantage of the fact that we're already sorted
+ // and find the insert index via a binary search, though expanded threads
+ // make that tricky.
+
+ nsMsgViewIndex highIndex = m_keys.Length();
+ nsMsgViewIndex lowIndex = 0;
+ IdKey EntryInfo1, EntryInfo2;
+
+ nsresult rv;
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // The following may leave fieldType undefined.
+ // In this case, we can return highIndex right away since
+ // it is the value returned in the default case of
+ // switch (fieldType) statement below.
+ rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler);
+ NS_ENSURE_SUCCESS(rv, highIndex);
+
+ int retStatus = 0;
+ msgHdr->GetMessageKey(&EntryInfo1.id);
+ msgHdr->GetFolder(&EntryInfo1.folder);
+ EntryInfo1.folder->Release();
+
+ viewSortInfo comparisonContext{
+ .view = this,
+ .isSecondarySort = false,
+ .ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending),
+ };
+
+ nsCOMPtr<nsIMsgDatabase> hdrDB;
+ EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB));
+ comparisonContext.db = hdrDB.get();
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(msgHdr, m_sortType, EntryInfo1.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ break;
+ case kU32:
+ if (m_sortType == nsMsgViewSortType::byId) {
+ EntryInfo1.dword = EntryInfo1.id;
+ } else {
+ GetLongField(msgHdr, m_sortType, &EntryInfo1.dword, colHandler);
+ }
+
+ break;
+ default:
+ return highIndex;
+ }
+
+ while (highIndex > lowIndex) {
+ nsMsgViewIndex tryIndex = (lowIndex + highIndex) / 2;
+ // Need to adjust tryIndex if it's not a thread.
+ while (m_levels[tryIndex] && tryIndex) tryIndex--;
+
+ if (tryIndex < lowIndex) {
+ NS_ERROR("try index shouldn't be less than low index");
+ break;
+ }
+
+ EntryInfo2.id = m_keys[tryIndex];
+ GetFolderForViewIndex(tryIndex, &EntryInfo2.folder);
+ EntryInfo2.folder->Release();
+
+ nsCOMPtr<nsIMsgDBHdr> tryHdr;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // ### this should get the db from the folder...
+ GetDBForViewIndex(tryIndex, getter_AddRefs(db));
+ if (db) db->GetMsgHdrForKey(EntryInfo2.id, getter_AddRefs(tryHdr));
+
+ if (!tryHdr) break;
+
+ if (tryHdr == msgHdr) {
+ NS_WARNING("didn't expect header to already be in view");
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (fieldType == kCollationKey) {
+ rv = GetCollationKey(tryHdr, m_sortType, EntryInfo2.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+
+ retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ } else if (fieldType == kU32) {
+ if (m_sortType == nsMsgViewSortType::byId) {
+ EntryInfo2.dword = EntryInfo2.id;
+ } else {
+ GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler);
+ }
+
+ retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ }
+
+ if (retStatus == 0) {
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (retStatus < 0) {
+ highIndex = tryIndex;
+ // We already made sure tryIndex was at a thread at the top of the loop.
+ } else {
+ lowIndex = tryIndex + 1;
+ while (lowIndex < GetSize() && m_levels[lowIndex]) lowIndex++;
+ }
+ }
+
+ return highIndex;
+}
+
+nsMsgViewIndex nsMsgDBView::GetInsertIndexHelper(
+ nsIMsgDBHdr* msgHdr, nsTArray<nsMsgKey>& keys,
+ nsCOMArray<nsIMsgFolder>* folders, nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewSortTypeValue sortType) {
+ nsMsgViewIndex highIndex = keys.Length();
+ nsMsgViewIndex lowIndex = 0;
+ IdKey EntryInfo1, EntryInfo2;
+
+ nsresult rv;
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // The following may leave fieldType undefined.
+ // In this case, we can return highIndex right away since
+ // it is the value returned in the default case of
+ // switch (fieldType) statement below.
+ rv = GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler);
+ NS_ENSURE_SUCCESS(rv, highIndex);
+
+ int retStatus = 0;
+ msgHdr->GetMessageKey(&EntryInfo1.id);
+ msgHdr->GetFolder(&EntryInfo1.folder);
+ EntryInfo1.folder->Release();
+
+ viewSortInfo comparisonContext{
+ .view = this,
+ .isSecondarySort = false,
+ .ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending),
+ };
+
+ rv = EntryInfo1.folder->GetMsgDatabase(&comparisonContext.db);
+ NS_ENSURE_SUCCESS(rv, highIndex);
+ comparisonContext.db->Release();
+
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(msgHdr, sortType, EntryInfo1.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ break;
+ case kU32:
+ if (sortType == nsMsgViewSortType::byId) {
+ EntryInfo1.dword = EntryInfo1.id;
+ } else {
+ GetLongField(msgHdr, sortType, &EntryInfo1.dword, colHandler);
+ }
+
+ break;
+ default:
+ return highIndex;
+ }
+
+ while (highIndex > lowIndex) {
+ nsMsgViewIndex tryIndex = (lowIndex + highIndex - 1) / 2;
+ EntryInfo2.id = keys[tryIndex];
+ EntryInfo2.folder = folders ? folders->ObjectAt(tryIndex) : m_folder.get();
+
+ nsCOMPtr<nsIMsgDBHdr> tryHdr;
+ EntryInfo2.folder->GetMessageHeader(EntryInfo2.id, getter_AddRefs(tryHdr));
+ if (!tryHdr) break;
+
+ if (fieldType == kCollationKey) {
+ rv = GetCollationKey(tryHdr, sortType, EntryInfo2.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+
+ retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ } else if (fieldType == kU32) {
+ if (sortType == nsMsgViewSortType::byId) {
+ EntryInfo2.dword = EntryInfo2.id;
+ } else {
+ GetLongField(tryHdr, sortType, &EntryInfo2.dword, colHandler);
+ }
+
+ retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ }
+
+ if (retStatus == 0) {
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (retStatus < 0) {
+ highIndex = tryIndex;
+ } else {
+ lowIndex = tryIndex + 1;
+ }
+ }
+
+ return highIndex;
+}
+
+nsMsgViewIndex nsMsgDBView::GetInsertIndex(nsIMsgDBHdr* msgHdr) {
+ if (!GetSize()) return 0;
+
+ if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) != 0 &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) &&
+ m_sortOrder != nsMsgViewSortType::byId) {
+ return GetIndexForThread(msgHdr);
+ }
+
+ return GetInsertIndexHelper(msgHdr, m_keys, GetFolders(), m_sortOrder,
+ m_sortType);
+}
+
+nsresult nsMsgDBView::AddHdr(nsIMsgDBHdr* msgHdr, nsMsgViewIndex* resultIndex) {
+ uint32_t flags = 0;
+#ifdef DEBUG_bienvenu
+ NS_ASSERTION(m_keys.Length() == m_flags.Length() &&
+ (int)m_keys.Length() == m_levels.Length(),
+ "view arrays out of sync!");
+#endif
+
+ if (resultIndex) *resultIndex = nsMsgViewIndex_None;
+
+ if (!GetShowingIgnored()) {
+ nsCOMPtr<nsIMsgThread> thread;
+ GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread));
+ if (thread) {
+ thread->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Ignored) return NS_OK;
+ }
+
+ bool ignored;
+ msgHdr->GetIsKilled(&ignored);
+ if (ignored) return NS_OK;
+ }
+
+ nsMsgKey msgKey, threadId;
+ nsMsgKey threadParent;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetThreadId(&threadId);
+ msgHdr->GetThreadParent(&threadParent);
+
+ msgHdr->GetFlags(&flags);
+ // XXX this isn't quite right, is it?
+ // Should be checking that our thread parent key is none?
+ if (threadParent == nsMsgKey_None) flags |= MSG_VIEW_FLAG_ISTHREAD;
+
+ nsMsgViewIndex insertIndex = GetInsertIndex(msgHdr);
+ if (insertIndex == nsMsgViewIndex_None) {
+ // If unreadonly, level is 0 because we must be the only msg in the thread.
+ int32_t levelToAdd = 0;
+
+ if (m_sortOrder == nsMsgViewSortOrder::ascending) {
+ InsertMsgHdrAt(GetSize(), msgHdr, msgKey, flags, levelToAdd);
+ if (resultIndex) *resultIndex = GetSize() - 1;
+
+ // The call to NoteChange() has to happen after we add the key as
+ // NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ NoteChange(GetSize() - 1, 1, nsMsgViewNotificationCode::insertOrDelete);
+ } else {
+ InsertMsgHdrAt(0, msgHdr, msgKey, flags, levelToAdd);
+ if (resultIndex) *resultIndex = 0;
+
+ // The call to NoteChange() has to happen after we insert the key as
+ // NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ NoteChange(0, 1, nsMsgViewNotificationCode::insertOrDelete);
+ }
+
+ m_sortValid = false;
+ } else {
+ InsertMsgHdrAt(insertIndex, msgHdr, msgKey, flags, 0);
+ if (resultIndex) *resultIndex = insertIndex;
+
+ // The call to NoteChange() has to happen after we add the key as
+ // NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ }
+
+ OnHeaderAddedOrDeleted();
+ return NS_OK;
+}
+
+bool nsMsgDBView::WantsThisThread(nsIMsgThread* /*threadHdr*/) {
+ // Default is to want all threads.
+ return true;
+}
+
+nsMsgViewIndex nsMsgDBView::FindParentInThread(
+ nsMsgKey parentKey, nsMsgViewIndex startOfThreadViewIndex) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ while (parentKey != nsMsgKey_None) {
+ nsMsgViewIndex parentIndex =
+ m_keys.IndexOf(parentKey, startOfThreadViewIndex);
+ if (parentIndex != nsMsgViewIndex_None) return parentIndex;
+
+ if (NS_FAILED(m_db->GetMsgHdrForKey(parentKey, getter_AddRefs(msgHdr))))
+ break;
+
+ msgHdr->GetThreadParent(&parentKey);
+ }
+
+ return startOfThreadViewIndex;
+}
+
+nsresult nsMsgDBView::ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey, uint32_t level,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed) {
+ nsCOMPtr<nsIMsgEnumerator> msgEnumerator;
+ nsresult rv =
+ threadHdr->EnumerateMessages(parentKey, getter_AddRefs(msgEnumerator));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numChildren;
+ (void)threadHdr->GetNumChildren(&numChildren);
+ NS_ASSERTION(numChildren, "Empty thread in view/db");
+ // Bogus, but harmless.
+ if (!numChildren) return NS_OK;
+
+ // Account for the existing thread root.
+ numChildren--;
+
+ // Skip the first one.
+ bool hasMore;
+ while (NS_SUCCEEDED(msgEnumerator->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgEnumerator->GetNext(getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (*pNumListed == numChildren) {
+ MOZ_ASSERT_UNREACHABLE("thread corrupt in db");
+ // If we've listed more messages than are in the thread, then the db
+ // is corrupt, and we should invalidate it.
+ // We'll use this rv to indicate there's something wrong with the db
+ // though for now it probably won't get paid attention to.
+ m_db->SetSummaryValid(false);
+ return NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) {
+ bool ignored;
+ msgHdr->GetIsKilled(&ignored);
+ // We are not going to process subthreads, horribly invalidating the
+ // numChildren characteristic.
+ if (ignored) continue;
+ }
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags, newFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ AdjustReadFlag(msgHdr, &msgFlags);
+ SetMsgHdrAt(msgHdr, *viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, level);
+ // Turn off thread or elided bit if they got turned on (maybe from new
+ // only view?)
+ msgHdr->AndFlags(~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided),
+ &newFlags);
+ (*pNumListed)++;
+ (*viewIndex)++;
+ rv = ListIdsInThreadOrder(threadHdr, msgKey, level + 1, viewIndex,
+ pNumListed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+void nsMsgDBView::InsertEmptyRows(nsMsgViewIndex viewIndex, int32_t numRows) {
+ m_keys.InsertElementsAt(viewIndex, numRows, 0);
+ m_flags.InsertElementsAt(viewIndex, numRows, 0);
+ m_levels.InsertElementsAt(viewIndex, numRows, 1);
+}
+
+void nsMsgDBView::RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) {
+ m_keys.RemoveElementsAt(viewIndex, numRows);
+ m_flags.RemoveElementsAt(viewIndex, numRows);
+ m_levels.RemoveElementsAt(viewIndex, numRows);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::InsertTreeRows(nsMsgViewIndex aIndex, uint32_t aNumRows,
+ nsMsgKey aKey, nsMsgViewFlagsTypeValue aFlags,
+ uint32_t aLevel, nsIMsgFolder* aFolder) {
+ if (GetSize() < aIndex) return NS_ERROR_UNEXPECTED;
+
+ nsCOMArray<nsIMsgFolder>* folders = GetFolders();
+ if (folders) {
+ // In a search/xfvf view only, a folder is required.
+ NS_ENSURE_ARG_POINTER(aFolder);
+ for (size_t i = 0; i < aNumRows; i++)
+ // Insert into m_folders.
+ if (!folders->InsertObjectAt(aFolder, aIndex + i))
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ m_keys.InsertElementsAt(aIndex, aNumRows, aKey);
+ m_flags.InsertElementsAt(aIndex, aNumRows, aFlags);
+ m_levels.InsertElementsAt(aIndex, aNumRows, aLevel);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::RemoveTreeRows(nsMsgViewIndex aIndex, uint32_t aNumRows) {
+ // Prevent a crash if attempting to remove rows which don't exist.
+ if (GetSize() < aIndex + aNumRows) return NS_ERROR_UNEXPECTED;
+
+ nsMsgDBView::RemoveRows(aIndex, aNumRows);
+
+ nsCOMArray<nsIMsgFolder>* folders = GetFolders();
+ if (folders)
+ // In a search/xfvf view only, remove from m_folders.
+ if (!folders->RemoveObjectsAt(aIndex, aNumRows)) return NS_ERROR_UNEXPECTED;
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ListIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) {
+ NS_ENSURE_ARG(threadHdr);
+ // These children ids should be in thread order.
+ nsresult rv = NS_OK;
+ uint32_t i;
+ nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1;
+ *pNumListed = 0;
+
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ NS_ASSERTION(numChildren, "Empty thread in view/db");
+ if (!numChildren) return NS_OK;
+
+ // Account for the existing thread root.
+ numChildren--;
+ InsertEmptyRows(viewIndex, numChildren);
+
+ // ### need to rework this when we implemented threading in group views.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) {
+ nsMsgKey parentKey = m_keys[startOfThreadViewIndex];
+ // If the thread is bigger than the hdr cache, expanding the thread
+ // can be slow. Increasing the hdr cache size will help a fair amount.
+ uint32_t hdrCacheSize;
+ m_db->GetMsgHdrCacheSize(&hdrCacheSize);
+ if (numChildren > hdrCacheSize) m_db->SetMsgHdrCacheSize(numChildren);
+
+ // If this fails, *pNumListed will be 0, and we'll fall back to just
+ // enumerating the messages in the thread below.
+ rv = ListIdsInThreadOrder(threadHdr, parentKey, 1, &viewIndex, pNumListed);
+ if (numChildren > hdrCacheSize) m_db->SetMsgHdrCacheSize(hdrCacheSize);
+ }
+
+ if (!*pNumListed) {
+ uint32_t ignoredHeaders = 0;
+ // If we're not threaded, just list em out in db order.
+ for (i = 1; i <= numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+
+ if (msgHdr != nullptr) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) {
+ bool killed;
+ msgHdr->GetIsKilled(&killed);
+ if (killed) {
+ ignoredHeaders++;
+ continue;
+ }
+ }
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags, newFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ AdjustReadFlag(msgHdr, &msgFlags);
+ SetMsgHdrAt(msgHdr, viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, 1);
+ // Here, we're either flat, or we're grouped - in either case,
+ // level is 1. Turn off thread or elided bit if they got turned on
+ // (maybe from new only view?).
+ if (i > 0)
+ msgHdr->AndFlags(
+ ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided), &newFlags);
+
+ (*pNumListed)++;
+ viewIndex++;
+ }
+ }
+
+ if (ignoredHeaders + *pNumListed < numChildren) {
+ MOZ_ASSERT_UNREACHABLE("thread corrupt in db");
+ // If we've listed fewer messages than are in the thread, then the db
+ // is corrupt, and we should invalidate it.
+ // We'll use this rv to indicate there's something wrong with the db
+ // though for now it probably won't get paid attention to.
+ m_db->SetSummaryValid(false);
+ rv = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+ }
+
+ // We may have added too many elements (i.e., subthreads were cut).
+ // XXX Fix for cross folder view case.
+ if (*pNumListed < numChildren)
+ RemoveRows(viewIndex, numChildren - *pNumListed);
+
+ return rv;
+}
+
+int32_t nsMsgDBView::FindLevelInThread(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startOfThread,
+ nsMsgViewIndex viewIndex) {
+ nsCOMPtr<nsIMsgDBHdr> curMsgHdr = msgHdr;
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+
+ // Look through the ancestors of the passed in msgHdr in turn, looking for
+ // them in the view, up to the start of the thread. If we find an ancestor,
+ // then our level is one greater than the level of the ancestor.
+ while (curMsgHdr) {
+ nsMsgKey parentKey;
+ curMsgHdr->GetThreadParent(&parentKey);
+ if (parentKey == nsMsgKey_None) break;
+
+ // Scan up to find view index of ancestor, if any.
+ for (nsMsgViewIndex indexToTry = viewIndex;
+ indexToTry && indexToTry-- >= startOfThread;) {
+ if (m_keys[indexToTry] == parentKey) return m_levels[indexToTry] + 1;
+ }
+
+ // If msgHdr's key is its parentKey, we'll loop forever, so protect
+ // against that corruption.
+ if (msgKey == parentKey || NS_FAILED(m_db->GetMsgHdrForKey(
+ parentKey, getter_AddRefs(curMsgHdr)))) {
+ NS_ERROR(
+ "msgKey == parentKey, or GetMsgHdrForKey failed, this used to be an "
+ "infinite loop condition");
+ curMsgHdr = nullptr;
+ } else {
+ // Need to update msgKey so the check for a msgHdr with matching
+ // key+parentKey will work after first time through loop.
+ curMsgHdr->GetMessageKey(&msgKey);
+ }
+ }
+
+ return 1;
+}
+
+// XXX Can this be combined with GetIndexForThread??
+nsMsgViewIndex nsMsgDBView::GetThreadRootIndex(nsIMsgDBHdr* msgHdr) {
+ if (!msgHdr) {
+ NS_WARNING("null msgHdr parameter");
+ return nsMsgViewIndex_None;
+ }
+
+ // Take advantage of the fact that we're already sorted
+ // and find the thread root via a binary search.
+
+ nsMsgViewIndex highIndex = m_keys.Length();
+ nsMsgViewIndex lowIndex = 0;
+ IdKey EntryInfo1, EntryInfo2;
+
+ nsresult rv;
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // The following may leave fieldType undefined.
+ // In this case, we can return highIndex right away since
+ // it is the value returned in the default case of
+ // switch (fieldType) statement below.
+ rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler);
+ NS_ENSURE_SUCCESS(rv, highIndex);
+
+ int retStatus = 0;
+ msgHdr->GetMessageKey(&EntryInfo1.id);
+ msgHdr->GetFolder(&EntryInfo1.folder);
+ EntryInfo1.folder->Release();
+
+ viewSortInfo comparisonContext{
+ .view = this,
+ .isSecondarySort = false,
+ .ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending),
+ };
+
+ nsCOMPtr<nsIMsgDatabase> hdrDB;
+ EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB));
+ comparisonContext.db = hdrDB.get();
+
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(msgHdr, m_sortType, EntryInfo1.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ break;
+ case kU32:
+ if (m_sortType == nsMsgViewSortType::byId) {
+ EntryInfo1.dword = EntryInfo1.id;
+ } else {
+ GetLongField(msgHdr, m_sortType, &EntryInfo1.dword, colHandler);
+ }
+
+ break;
+ default:
+ return highIndex;
+ }
+
+ while (highIndex > lowIndex) {
+ nsMsgViewIndex tryIndex = (lowIndex + highIndex) / 2;
+ // Need to adjust tryIndex if it's not a thread.
+ while (m_levels[tryIndex] && tryIndex) tryIndex--;
+
+ if (tryIndex < lowIndex) {
+ NS_ERROR("try index shouldn't be less than low index");
+ break;
+ }
+
+ EntryInfo2.id = m_keys[tryIndex];
+ GetFolderForViewIndex(tryIndex, &EntryInfo2.folder);
+ EntryInfo2.folder->Release();
+
+ nsCOMPtr<nsIMsgDBHdr> tryHdr;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // ### this should get the db from the folder...
+ GetDBForViewIndex(tryIndex, getter_AddRefs(db));
+ if (db) db->GetMsgHdrForKey(EntryInfo2.id, getter_AddRefs(tryHdr));
+
+ if (!tryHdr) break;
+
+ if (tryHdr == msgHdr) {
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (fieldType == kCollationKey) {
+ rv = GetCollationKey(tryHdr, m_sortType, EntryInfo2.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ } else if (fieldType == kU32) {
+ if (m_sortType == nsMsgViewSortType::byId) {
+ EntryInfo2.dword = EntryInfo2.id;
+ } else {
+ GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler);
+ }
+
+ retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext);
+ }
+
+ if (retStatus == 0) {
+ highIndex = tryIndex;
+ break;
+ }
+
+ if (retStatus < 0) {
+ highIndex = tryIndex;
+ // We already made sure tryIndex was at a thread at the top of the loop.
+ } else {
+ lowIndex = tryIndex + 1;
+ while (lowIndex < GetSize() && m_levels[lowIndex]) lowIndex++;
+ }
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> resultHdr;
+ GetMsgHdrForViewIndex(highIndex, getter_AddRefs(resultHdr));
+
+ if (resultHdr != msgHdr) {
+ NS_WARNING("didn't find hdr");
+ highIndex = FindHdr(msgHdr);
+#ifdef DEBUG_David_Bienvenu
+ if (highIndex != nsMsgViewIndex_None) {
+ NS_WARNING("but find hdr did");
+ printf("level of found hdr = %d\n", m_levels[highIndex]);
+ ValidateSort();
+ }
+#endif
+ return highIndex;
+ }
+
+ return msgHdr == resultHdr ? highIndex : nsMsgViewIndex_None;
+}
+
+#ifdef DEBUG_David_Bienvenu
+
+void nsMsgDBView::InitEntryInfoForIndex(nsMsgViewIndex i, IdKey& EntryInfo) {
+ nsresult rv;
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // The following may leave fieldType undefined.
+ rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to obtain fieldType");
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+
+ msgHdr->GetMessageKey(&EntryInfo.id);
+ msgHdr->GetFolder(&EntryInfo.folder);
+ EntryInfo.folder->Release();
+
+ nsCOMPtr<nsIMsgDatabase> hdrDB;
+ EntryInfo.folder->GetMsgDatabase(getter_AddRefs(hdrDB));
+ switch (fieldType) {
+ case kCollationKey:
+ rv = GetCollationKey(msgHdr, m_sortType, EntryInfo.key, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key");
+ break;
+ case kU32:
+ if (m_sortType == nsMsgViewSortType::byId)
+ EntryInfo.dword = EntryInfo.id;
+ else
+ GetLongField(msgHdr, m_sortType, &EntryInfo.dword, colHandler);
+
+ break;
+ default:
+ NS_ERROR("invalid field type");
+ }
+}
+
+void nsMsgDBView::ValidateSort() {
+ IdKey EntryInfo1, EntryInfo2;
+ nsCOMPtr<nsIMsgDBHdr> hdr1, hdr2;
+
+ uint16_t maxLen;
+ eFieldType fieldType;
+
+ // Get the custom column handler for the primary sort and pass it first
+ // to GetFieldTypeAndLenForSort to get the fieldType and then either
+ // GetCollationKey or GetLongField.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+
+ // It is not entirely clear what we should do since,
+ // if fieldType is not available, there is no way to know
+ // how to compare the field to check for sorting.
+ // So we bomb out here. It is OK since this is debug code
+ // inside #ifdef DEBUG_David_Bienvenu
+ nsresult rv =
+ GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "failed to obtain fieldType");
+
+ viewSortInfo comparisonContext;
+ comparisonContext.view = this;
+ comparisonContext.isSecondarySort = false;
+ comparisonContext.ascendingSort =
+ (m_sortOrder == nsMsgViewSortOrder::ascending);
+ nsCOMPtr<nsIMsgDatabase> db;
+ GetDBForViewIndex(0, getter_AddRefs(db));
+ // This is only for comparing collation keys - it could be any db.
+ comparisonContext.db = db.get();
+
+ for (nsMsgViewIndex i = 0; i < m_keys.Length();) {
+ // Ignore non threads.
+ if (m_levels[i]) {
+ i++;
+ continue;
+ }
+
+ // Find next header.
+ nsMsgViewIndex j = i + 1;
+ while (j < m_keys.Length() && m_levels[j]) j++;
+
+ if (j == m_keys.Length()) break;
+
+ InitEntryInfoForIndex(i, EntryInfo1);
+ InitEntryInfoForIndex(j, EntryInfo2);
+ const void *pValue1 = &EntryInfo1, *pValue2 = &EntryInfo2;
+ int retStatus = 0;
+ if (fieldType == kCollationKey)
+ retStatus = FnSortIdKey(&pValue1, &pValue2, &comparisonContext);
+ else if (fieldType == kU32)
+ retStatus = FnSortIdUint32(&pValue1, &pValue2, &comparisonContext);
+
+ if (retStatus &&
+ (retStatus < 0) == (m_sortOrder == nsMsgViewSortOrder::ascending)) {
+ NS_ERROR("view not sorted correctly");
+ break;
+ }
+ // j is the new i.
+ i = j;
+ }
+}
+
+#endif
+
+nsresult nsMsgDBView::ListUnreadIdsInThread(
+ nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) {
+ NS_ENSURE_ARG(threadHdr);
+ // These children ids should be in thread order.
+ nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1;
+ *pNumListed = 0;
+ nsMsgKey topLevelMsgKey = m_keys[startOfThreadViewIndex];
+
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ uint32_t i;
+ for (i = 0; i < numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr != nullptr) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) {
+ bool killed;
+ msgHdr->GetIsKilled(&killed);
+ if (killed) continue;
+ }
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ bool isRead = AdjustReadFlag(msgHdr, &msgFlags);
+ if (!isRead) {
+ // Just make sure flag is right in db.
+ m_db->MarkHdrRead(msgHdr, false, nullptr);
+ if (msgKey != topLevelMsgKey) {
+ InsertMsgHdrAt(
+ viewIndex, msgHdr, msgKey, msgFlags,
+ FindLevelInThread(msgHdr, startOfThreadViewIndex, viewIndex));
+ viewIndex++;
+ (*pNumListed)++;
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ // If we're not the instigator, update flags if this key is in our view.
+ if (aInstigator != this) {
+ NS_ENSURE_ARG_POINTER(aHdrChanged);
+ nsMsgKey msgKey;
+ aHdrChanged->GetMessageKey(&msgKey);
+ nsMsgViewIndex index = FindHdr(aHdrChanged);
+ if (index != nsMsgViewIndex_None) {
+ uint32_t viewOnlyFlags =
+ m_flags[index] & (MSG_VIEW_FLAGS | nsMsgMessageFlags::Elided);
+
+ // XXX what about saving the old view only flags, like IsThread and
+ // HasChildren?
+ // I think we'll want to save those away.
+ m_flags[index] = aNewFlags | viewOnlyFlags;
+ // Tell the view the extra flag changed, so it can
+ // update the previous view, if any.
+ OnExtraFlagChanged(index, aNewFlags);
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ uint32_t deltaFlags = (aOldFlags ^ aNewFlags);
+ if (deltaFlags & (nsMsgMessageFlags::Read | nsMsgMessageFlags::New)) {
+ nsMsgViewIndex threadIndex =
+ ThreadIndexOfMsgHdr(aHdrChanged, index, nullptr, nullptr);
+
+ // May need to fix thread counts.
+ if (threadIndex != nsMsgViewIndex_None && threadIndex != index)
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+
+ // Don't need to propagate notifications, right?
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey,
+ int32_t aFlags, nsIDBChangeListener* aInstigator) {
+ nsMsgViewIndex deletedIndex = FindHdr(aHdrChanged);
+ if (IsValidIndex(deletedIndex)) {
+ // Check if this message is currently selected. If it is, tell the frontend
+ // to be prepared for a delete.
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ bool isMsgSelected = false;
+ if (mTreeSelection && commandUpdater) {
+ mTreeSelection->IsSelected(deletedIndex, &isMsgSelected);
+ if (isMsgSelected) commandUpdater->UpdateNextMessageAfterDelete();
+ }
+
+ RemoveByIndex(deletedIndex);
+
+ if (isMsgSelected) {
+ // Now tell the front end that the delete happened.
+ commandUpdater->SelectedMessageRemoved();
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnHdrAdded(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey,
+ int32_t aFlags, nsIDBChangeListener* aInstigator) {
+ return OnNewHeader(aHdrChanged, aParentKey, false);
+ // Probably also want to pass that parent key in, since we went to the
+ // trouble of figuring out what it is.
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange,
+ const nsACString& property, bool aPreChange,
+ uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) {
+ if (aPreChange) return NS_OK;
+
+ if (aHdrToChange) {
+ nsMsgViewIndex index = FindHdr(aHdrToChange);
+ if (index != nsMsgViewIndex_None)
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) {
+ if (m_db) {
+ m_db->RemoveListener(this);
+ m_db = nullptr;
+ }
+
+ int32_t saveSize = GetSize();
+ ClearHdrCache();
+
+ // This is important, because the tree will ask us for our
+ // row count, which get determine from the number of keys.
+ m_keys.Clear();
+ // Be consistent.
+ m_flags.Clear();
+ m_levels.Clear();
+
+ // Tell the tree all the rows have gone away.
+ if (mTree) mTree->RowCountChanged(0, -saveSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -saveSize);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) {
+ if (!strcmp(aEvent, "DBOpened")) m_db = aDB;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnReadChanged(nsIDBChangeListener* aInstigator) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgDBView::OnJunkScoreChanged(nsIDBChangeListener* aInstigator) {
+ return NS_OK;
+}
+
+void nsMsgDBView::ClearHdrCache() {
+ m_cachedHdr = nullptr;
+ m_cachedMsgKey = nsMsgKey_None;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSuppressChangeNotifications(bool aSuppressChangeNotifications) {
+ mSuppressChangeNotification = aSuppressChangeNotifications;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSuppressChangeNotifications(
+ bool* aSuppressChangeNotifications) {
+ NS_ENSURE_ARG_POINTER(aSuppressChangeNotifications);
+ *aSuppressChangeNotifications = mSuppressChangeNotification;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::NoteChange(nsMsgViewIndex firstLineChanged, int32_t numChanged,
+ nsMsgViewNotificationCodeValue changeType) {
+ if ((mTree || mJSTree) && !mSuppressChangeNotification) {
+ switch (changeType) {
+ case nsMsgViewNotificationCode::changed:
+ if (mTree)
+ mTree->InvalidateRange(firstLineChanged,
+ firstLineChanged + numChanged - 1);
+ if (mJSTree)
+ mJSTree->InvalidateRange(firstLineChanged,
+ firstLineChanged + numChanged - 1);
+ break;
+ case nsMsgViewNotificationCode::insertOrDelete:
+ if (numChanged < 0) mRemovingRow = true;
+
+ // The caller needs to have adjusted m_keys before getting here, since
+ // RowCountChanged() will call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(firstLineChanged, numChanged);
+ if (mJSTree) mJSTree->RowCountChanged(firstLineChanged, numChanged);
+ mRemovingRow = false;
+ [[fallthrough]];
+ case nsMsgViewNotificationCode::all:
+ ClearHdrCache();
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSortOrder(nsMsgViewSortOrderValue* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+ *aSortOrder = m_sortOrder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSortType(nsMsgViewSortTypeValue* aSortType) {
+ NS_ENSURE_ARG_POINTER(aSortType);
+ *aSortType = m_sortType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSortType(nsMsgViewSortTypeValue aSortType) {
+ m_sortType = aSortType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ERROR("you should be overriding this");
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSecondarySortOrder(nsMsgViewSortOrderValue* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+ *aSortOrder = m_secondarySortOrder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSecondarySortOrder(nsMsgViewSortOrderValue aSortOrder) {
+ m_secondarySortOrder = aSortOrder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSecondarySortType(nsMsgViewSortTypeValue* aSortType) {
+ NS_ENSURE_ARG_POINTER(aSortType);
+ *aSortType = m_secondarySort;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSecondarySortType(nsMsgViewSortTypeValue aSortType) {
+ m_secondarySort = aSortType;
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::PersistFolderInfo(nsIDBFolderInfo** dbFolderInfo) {
+ nsresult rv = m_db->GetDBFolderInfo(dbFolderInfo);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Save off sort type and order, view type and flags.
+ (*dbFolderInfo)->SetSortType(m_sortType);
+ (*dbFolderInfo)->SetSortOrder(m_sortOrder);
+ (*dbFolderInfo)->SetViewFlags(m_viewFlags);
+ nsMsgViewTypeValue viewType;
+ GetViewType(&viewType);
+ (*dbFolderInfo)->SetViewType(viewType);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetViewFlags(nsMsgViewFlagsTypeValue* aViewFlags) {
+ NS_ENSURE_ARG_POINTER(aViewFlags);
+ *aViewFlags = m_viewFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) {
+ // If we're turning off threaded display, we need to expand all so that all
+ // messages will be displayed.
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(aViewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ ExpandAll();
+ // Invalidate the sort so sorting will do something.
+ m_sortValid = false;
+ }
+
+ m_viewFlags = aViewFlags;
+
+ if (m_viewFolder) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folderInfo->SetViewFlags(aViewFlags);
+ } else
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::MarkThreadOfMsgRead(nsMsgKey msgId,
+ nsMsgViewIndex msgIndex,
+ nsTArray<nsMsgKey>& idsMarkedRead,
+ bool bRead) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsresult rv = GetThreadContainingIndex(msgIndex, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgViewIndex threadIndex;
+
+ NS_ASSERTION(threadHdr, "threadHdr is null");
+ if (!threadHdr) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ nsCOMPtr<nsIMsgDBHdr> firstHdr;
+ rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(firstHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgKey firstHdrId;
+ firstHdr->GetMessageKey(&firstHdrId);
+ if (msgId != firstHdrId)
+ threadIndex = GetIndexOfFirstDisplayedKeyInThread(threadHdr);
+ else
+ threadIndex = msgIndex;
+
+ return MarkThreadRead(threadHdr, threadIndex, idsMarkedRead, bRead);
+}
+
+nsresult nsMsgDBView::MarkThreadRead(nsIMsgThread* threadHdr,
+ nsMsgViewIndex threadIndex,
+ nsTArray<nsMsgKey>& idsMarkedRead,
+ bool bRead) {
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ idsMarkedRead.SetCapacity(numChildren);
+ for (int32_t childIndex = 0; childIndex < (int32_t)numChildren;
+ childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(childIndex, getter_AddRefs(msgHdr));
+ NS_ASSERTION(msgHdr, "msgHdr is null");
+ if (!msgHdr) continue;
+
+ bool isRead;
+ nsMsgKey hdrMsgId;
+ msgHdr->GetMessageKey(&hdrMsgId);
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = GetDBForHeader(msgHdr, getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ db->IsRead(hdrMsgId, &isRead);
+
+ if (isRead != bRead) {
+ // MarkHdrRead will change the unread count on the thread.
+ db->MarkHdrRead(msgHdr, bRead, nullptr);
+ // Insert at the front. Should we insert at the end?
+ idsMarkedRead.InsertElementAt(0, hdrMsgId);
+ }
+ }
+
+ return NS_OK;
+}
+
+bool nsMsgDBView::AdjustReadFlag(nsIMsgDBHdr* msgHdr, uint32_t* msgFlags) {
+ // If we're a cross-folder view, just bail on this.
+ if (GetFolders()) return *msgFlags & nsMsgMessageFlags::Read;
+
+ bool isRead = false;
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ m_db->IsRead(msgKey, &isRead);
+ // Just make sure flag is right in db.
+#ifdef DEBUG_David_Bienvenu
+ NS_ASSERTION(isRead == ((*msgFlags & nsMsgMessageFlags::Read) != 0),
+ "msgFlags out of sync");
+#endif
+ if (isRead)
+ *msgFlags |= nsMsgMessageFlags::Read;
+ else
+ *msgFlags &= ~nsMsgMessageFlags::Read;
+
+ m_db->MarkHdrRead(msgHdr, isRead, nullptr);
+ return isRead;
+}
+
+// Starting from startIndex, performs the passed in navigation, including
+// any marking read needed, and returns the resultId and resultIndex of the
+// destination of the navigation. If no message is found in the view,
+// it returns a resultId of nsMsgKey_None and an resultIndex of
+// nsMsgViewIndex_None.
+NS_IMETHODIMP
+nsMsgDBView::ViewNavigate(nsMsgNavigationTypeValue motion, nsMsgKey* pResultKey,
+ nsMsgViewIndex* pResultIndex,
+ nsMsgViewIndex* pThreadIndex, bool wrap) {
+ NS_ENSURE_ARG_POINTER(pResultKey);
+ NS_ENSURE_ARG_POINTER(pResultIndex);
+ NS_ENSURE_ARG_POINTER(pThreadIndex);
+
+ int32_t currentIndex;
+ nsMsgViewIndex startIndex;
+
+ if (!mTreeSelection) {
+ // We must be in stand alone message mode.
+ currentIndex = FindViewIndex(m_currentlyDisplayedMsgKey);
+ } else {
+ nsresult rv = mTreeSelection->GetCurrentIndex(&currentIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ startIndex = currentIndex;
+ return nsMsgDBView::NavigateFromPos(motion, startIndex, pResultKey,
+ pResultIndex, pThreadIndex, wrap);
+}
+
+nsresult nsMsgDBView::NavigateFromPos(nsMsgNavigationTypeValue motion,
+ nsMsgViewIndex startIndex,
+ nsMsgKey* pResultKey,
+ nsMsgViewIndex* pResultIndex,
+ nsMsgViewIndex* pThreadIndex, bool wrap) {
+ nsresult rv = NS_OK;
+ nsMsgKey resultThreadKey;
+ nsMsgViewIndex curIndex;
+ nsMsgViewIndex lastIndex =
+ (GetSize() > 0) ? (nsMsgViewIndex)GetSize() - 1 : nsMsgViewIndex_None;
+ nsMsgViewIndex threadIndex = nsMsgViewIndex_None;
+
+ // If there aren't any messages in the view, bail out.
+ if (GetSize() <= 0) {
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ return NS_OK;
+ }
+ *pResultKey = nsMsgKey_None;
+
+ switch (motion) {
+ case nsMsgNavigationType::firstMessage:
+ *pResultIndex = 0;
+ *pResultKey = m_keys[0];
+ break;
+ case nsMsgNavigationType::nextMessage:
+ // Return same index and id on next on last message.
+ *pResultIndex = std::min(startIndex + 1, lastIndex);
+ *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::previousMessage:
+ if (startIndex != nsMsgViewIndex_None && startIndex > 0) {
+ *pResultIndex = startIndex - 1;
+ }
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::lastMessage:
+ *pResultIndex = lastIndex;
+ *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::firstFlagged:
+ rv = FindFirstFlagged(pResultIndex);
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::nextFlagged:
+ rv = FindNextFlagged(startIndex + 1, pResultIndex);
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::previousFlagged:
+ rv = FindPrevFlagged(startIndex, pResultIndex);
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::firstNew:
+ rv = FindFirstNew(pResultIndex);
+ if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex];
+ break;
+ case nsMsgNavigationType::firstUnreadMessage:
+ startIndex = nsMsgViewIndex_None;
+ // Note fall through - is this motion ever used?
+ [[fallthrough]];
+ case nsMsgNavigationType::nextUnreadMessage:
+ for (curIndex = (startIndex == nsMsgViewIndex_None) ? 0 : startIndex;
+ curIndex <= lastIndex && lastIndex != nsMsgViewIndex_None;
+ curIndex++) {
+ uint32_t flags = m_flags[curIndex];
+ // Don't return start index since navigate should move.
+ if (!(flags & (nsMsgMessageFlags::Read | MSG_VIEW_FLAG_DUMMY)) &&
+ (curIndex != startIndex)) {
+ *pResultIndex = curIndex;
+ *pResultKey = m_keys[*pResultIndex];
+ break;
+ }
+
+ // Check for collapsed thread with new children.
+ if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) &&
+ flags & MSG_VIEW_FLAG_ISTHREAD &&
+ flags & nsMsgMessageFlags::Elided) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ GetThreadContainingIndex(curIndex, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(threadHdr, "threadHdr is null");
+ if (!threadHdr) continue;
+
+ uint32_t numUnreadChildren;
+ threadHdr->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) {
+ uint32_t numExpanded;
+ ExpandByIndex(curIndex, &numExpanded);
+ lastIndex += numExpanded;
+ if (pThreadIndex) *pThreadIndex = curIndex;
+ }
+ }
+ }
+
+ if (curIndex > lastIndex) {
+ // Wrap around by starting at index 0.
+ if (wrap) {
+ nsMsgKey startKey = GetAt(startIndex);
+ rv = NavigateFromPos(nsMsgNavigationType::nextUnreadMessage,
+ nsMsgViewIndex_None, pResultKey, pResultIndex,
+ pThreadIndex, false);
+
+ if (*pResultKey == startKey) {
+ // wrapped around and found start message!
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ }
+ } else {
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ }
+ }
+ break;
+ case nsMsgNavigationType::previousUnreadMessage:
+ if (!IsValidIndex(startIndex)) break;
+
+ rv = FindPrevUnread(m_keys[startIndex], pResultKey, &resultThreadKey);
+ if (NS_SUCCEEDED(rv)) {
+ *pResultIndex = FindViewIndex(*pResultKey);
+ if (*pResultKey != resultThreadKey &&
+ (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ threadIndex = GetThreadIndex(*pResultIndex);
+ if (*pResultIndex == nsMsgViewIndex_None) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = m_db->GetMsgHdrForKey(*pResultKey, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(threadHdr, "threadHdr is null");
+ if (threadHdr) break;
+
+ uint32_t numUnreadChildren;
+ threadHdr->GetNumUnreadChildren(&numUnreadChildren);
+ if (numUnreadChildren > 0) {
+ uint32_t numExpanded;
+ ExpandByIndex(threadIndex, &numExpanded);
+ }
+
+ *pResultIndex = FindViewIndex(*pResultKey);
+ }
+ }
+
+ if (pThreadIndex) *pThreadIndex = threadIndex;
+ }
+ break;
+ case nsMsgNavigationType::lastUnreadMessage:
+ break;
+ case nsMsgNavigationType::nextUnreadThread:
+ if (startIndex != nsMsgViewIndex_None) {
+ ApplyCommandToIndices(nsMsgViewCommandType::markThreadRead,
+ {startIndex});
+ }
+
+ return NavigateFromPos(nsMsgNavigationType::nextUnreadMessage, startIndex,
+ pResultKey, pResultIndex, pThreadIndex, true);
+ case nsMsgNavigationType::toggleThreadKilled: {
+ bool resultKilled;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ ToggleIgnored(selection, &threadIndex, &resultKilled);
+ if (resultKilled) {
+ return NavigateFromPos(nsMsgNavigationType::nextUnreadThread,
+ threadIndex, pResultKey, pResultIndex,
+ pThreadIndex, true);
+ } else {
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ return NS_OK;
+ }
+ }
+ case nsMsgNavigationType::toggleSubthreadKilled: {
+ bool resultKilled;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ ToggleMessageKilled(selection, &threadIndex, &resultKilled);
+ if (resultKilled) {
+ return NavigateFromPos(nsMsgNavigationType::nextUnreadMessage,
+ threadIndex, pResultKey, pResultIndex,
+ pThreadIndex, true);
+ } else {
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ return NS_OK;
+ }
+ }
+ // Check where navigate says this will take us. If we have the message
+ // in the view, return it. Otherwise, return an error.
+ case nsMsgNavigationType::back:
+ case nsMsgNavigationType::forward:
+ // Handled purely in JS.
+ *pResultIndex = nsMsgViewIndex_None;
+ *pResultKey = nsMsgKey_None;
+ break;
+ default:
+ NS_ERROR("unsupported motion");
+ break;
+ }
+
+ return NS_OK;
+}
+
+// Note that these routines do NOT expand collapsed threads! This mimics the
+// old behaviour, but it's also because we don't remember whether a thread
+// contains a flagged message the same way we remember if a thread contains
+// new messages. It would be painful to dive down into each collapsed thread
+// to update navigate status. We could cache this info, but it would still be
+// expensive the first time this status needs to get updated.
+nsresult nsMsgDBView::FindNextFlagged(nsMsgViewIndex startIndex,
+ nsMsgViewIndex* pResultIndex) {
+ nsMsgViewIndex lastIndex = (nsMsgViewIndex)GetSize() - 1;
+ nsMsgViewIndex curIndex;
+
+ *pResultIndex = nsMsgViewIndex_None;
+
+ if (GetSize() > 0) {
+ for (curIndex = startIndex; curIndex <= lastIndex; curIndex++) {
+ uint32_t flags = m_flags[curIndex];
+ if (flags & nsMsgMessageFlags::Marked) {
+ *pResultIndex = curIndex;
+ break;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FindFirstNew(nsMsgViewIndex* pResultIndex) {
+ if (m_db) {
+ nsMsgKey firstNewKey = nsMsgKey_None;
+ m_db->GetFirstNew(&firstNewKey);
+ *pResultIndex = (firstNewKey != nsMsgKey_None) ? FindKey(firstNewKey, true)
+ : nsMsgViewIndex_None;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::FindPrevUnread(nsMsgKey startKey, nsMsgKey* pResultKey,
+ nsMsgKey* resultThreadId) {
+ nsMsgViewIndex startIndex = FindViewIndex(startKey);
+ nsMsgViewIndex curIndex = startIndex;
+ nsresult rv = NS_MSG_MESSAGE_NOT_FOUND;
+
+ if (startIndex == nsMsgViewIndex_None) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ *pResultKey = nsMsgKey_None;
+ if (resultThreadId) *resultThreadId = nsMsgKey_None;
+
+ for (; (int)curIndex >= 0 && (*pResultKey == nsMsgKey_None); curIndex--) {
+ uint32_t flags = m_flags[curIndex];
+ if (curIndex != startIndex && flags & MSG_VIEW_FLAG_ISTHREAD &&
+ flags & nsMsgMessageFlags::Elided) {
+ NS_ERROR("fix this");
+ // nsMsgKey threadId = m_keys[curIndex];
+ // rv = m_db->GetUnreadKeyInThread(threadId, pResultKey, resultThreadId);
+ if (NS_SUCCEEDED(rv) && (*pResultKey != nsMsgKey_None)) break;
+ }
+
+ if (!(flags & (nsMsgMessageFlags::Read | MSG_VIEW_FLAG_DUMMY)) &&
+ (curIndex != startIndex)) {
+ *pResultKey = m_keys[curIndex];
+ rv = NS_OK;
+ break;
+ }
+ }
+
+ // Found unread message but we don't know the thread.
+ NS_ASSERTION(!(*pResultKey != nsMsgKey_None && resultThreadId &&
+ *resultThreadId == nsMsgKey_None),
+ "fix this");
+ return rv;
+}
+
+nsresult nsMsgDBView::FindFirstFlagged(nsMsgViewIndex* pResultIndex) {
+ return FindNextFlagged(0, pResultIndex);
+}
+
+nsresult nsMsgDBView::FindPrevFlagged(nsMsgViewIndex startIndex,
+ nsMsgViewIndex* pResultIndex) {
+ nsMsgViewIndex curIndex;
+
+ *pResultIndex = nsMsgViewIndex_None;
+
+ if (GetSize() > 0 && IsValidIndex(startIndex)) {
+ curIndex = startIndex;
+ do {
+ if (curIndex != 0) curIndex--;
+
+ uint32_t flags = m_flags[curIndex];
+ if (flags & nsMsgMessageFlags::Marked) {
+ *pResultIndex = curIndex;
+ break;
+ }
+ } while (curIndex != 0);
+ }
+
+ return NS_OK;
+}
+
+bool nsMsgDBView::IsValidIndex(nsMsgViewIndex index) {
+ return index != nsMsgViewIndex_None &&
+ (index < (nsMsgViewIndex)m_keys.Length());
+}
+
+nsresult nsMsgDBView::OrExtraFlag(nsMsgViewIndex index, uint32_t orflag) {
+ uint32_t flag;
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ flag = m_flags[index];
+ flag |= orflag;
+ m_flags[index] = flag;
+ OnExtraFlagChanged(index, flag);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::AndExtraFlag(nsMsgViewIndex index, uint32_t andflag) {
+ uint32_t flag;
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ flag = m_flags[index];
+ flag &= andflag;
+ m_flags[index] = flag;
+ OnExtraFlagChanged(index, flag);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::SetExtraFlag(nsMsgViewIndex index, uint32_t extraflag) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ m_flags[index] = extraflag;
+ OnExtraFlagChanged(index, extraflag);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ToggleIgnored(nsTArray<nsMsgViewIndex> const& selection,
+ nsMsgViewIndex* resultIndex,
+ bool* resultToggleState) {
+ nsCOMPtr<nsIMsgThread> thread;
+
+ // Ignored state is toggled based on the first selected thread.
+ nsMsgViewIndex threadIndex =
+ GetThreadFromMsgIndex(selection[0], getter_AddRefs(thread));
+ NS_ENSURE_STATE(thread);
+ uint32_t threadFlags;
+ thread->GetFlags(&threadFlags);
+ uint32_t ignored = threadFlags & nsMsgMessageFlags::Ignored;
+
+ // Process threads in reverse order.
+ // Otherwise collapsing the threads will invalidate the indices.
+ threadIndex = nsMsgViewIndex_None;
+ uint32_t numIndices = selection.Length();
+ while (numIndices) {
+ numIndices--;
+ if (selection[numIndices] < threadIndex) {
+ threadIndex =
+ GetThreadFromMsgIndex(selection[numIndices], getter_AddRefs(thread));
+ thread->GetFlags(&threadFlags);
+ if ((threadFlags & nsMsgMessageFlags::Ignored) == ignored)
+ SetThreadIgnored(thread, threadIndex, !ignored);
+ }
+ }
+
+ if (resultIndex) *resultIndex = threadIndex;
+
+ if (resultToggleState) *resultToggleState = !ignored;
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::ToggleMessageKilled(
+ nsTArray<nsMsgViewIndex> const& selection, nsMsgViewIndex* resultIndex,
+ bool* resultToggleState) {
+ NS_ENSURE_ARG_POINTER(resultToggleState);
+
+ nsCOMPtr<nsIMsgDBHdr> header;
+ // Ignored state is toggled based on the first selected message.
+ nsresult rv = GetMsgHdrForViewIndex(selection[0], getter_AddRefs(header));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t msgFlags;
+ header->GetFlags(&msgFlags);
+ uint32_t ignored = msgFlags & nsMsgMessageFlags::Ignored;
+
+ // Process messages in reverse order.
+ // Otherwise the indices may be invalidated.
+ nsMsgViewIndex msgIndex = nsMsgViewIndex_None;
+ uint32_t numIndices = selection.Length();
+ while (numIndices) {
+ numIndices--;
+ if (selection[numIndices] < msgIndex) {
+ msgIndex = selection[numIndices];
+ rv = GetMsgHdrForViewIndex(msgIndex, getter_AddRefs(header));
+ NS_ENSURE_SUCCESS(rv, rv);
+ header->GetFlags(&msgFlags);
+ if ((msgFlags & nsMsgMessageFlags::Ignored) == ignored)
+ SetSubthreadKilled(header, msgIndex, !ignored);
+ }
+ }
+
+ if (resultIndex) *resultIndex = msgIndex;
+
+ if (resultToggleState) *resultToggleState = !ignored;
+
+ return NS_OK;
+}
+
+nsMsgViewIndex nsMsgDBView::GetThreadFromMsgIndex(nsMsgViewIndex index,
+ nsIMsgThread** threadHdr) {
+ if (threadHdr == nullptr) return nsMsgViewIndex_None;
+ nsMsgKey msgKey = GetAt(index);
+
+ nsresult rv = GetThreadContainingIndex(index, threadHdr);
+ NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None);
+
+ if (*threadHdr == nullptr) return nsMsgViewIndex_None;
+
+ nsMsgKey threadKey;
+ (*threadHdr)->GetThreadKey(&threadKey);
+ nsMsgViewIndex threadIndex;
+ if (msgKey != threadKey)
+ threadIndex = GetIndexOfFirstDisplayedKeyInThread(*threadHdr);
+ else
+ threadIndex = index;
+ return threadIndex;
+}
+
+nsresult nsMsgDBView::ToggleWatched(nsTArray<nsMsgViewIndex> const& selection) {
+ MOZ_ASSERT(!selection.IsEmpty());
+ nsCOMPtr<nsIMsgThread> thread;
+
+ // Watched state is toggled based on the first selected thread.
+ nsMsgViewIndex threadIndex =
+ GetThreadFromMsgIndex(selection[0], getter_AddRefs(thread));
+ NS_ENSURE_STATE(thread);
+ uint32_t threadFlags;
+ thread->GetFlags(&threadFlags);
+ uint32_t watched = threadFlags & nsMsgMessageFlags::Watched;
+
+ // Process threads in reverse order for consistency with ToggleIgnored.
+ threadIndex = nsMsgViewIndex_None;
+ uint32_t numIndices = selection.Length();
+ while (numIndices) {
+ numIndices--;
+ if (selection[numIndices] < threadIndex) {
+ threadIndex =
+ GetThreadFromMsgIndex(selection[numIndices], getter_AddRefs(thread));
+ thread->GetFlags(&threadFlags);
+ if ((threadFlags & nsMsgMessageFlags::Watched) == watched)
+ SetThreadWatched(thread, threadIndex, !watched);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::SetThreadIgnored(nsIMsgThread* thread,
+ nsMsgViewIndex threadIndex,
+ bool ignored) {
+ if (!IsValidIndex(threadIndex)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ if (ignored) {
+ nsTArray<nsMsgKey> idsMarkedRead;
+ MarkThreadRead(thread, threadIndex, idsMarkedRead, true);
+ CollapseByIndex(threadIndex, nullptr);
+ }
+
+ if (!m_db) return NS_ERROR_FAILURE;
+
+ return m_db->MarkThreadIgnored(thread, m_keys[threadIndex], ignored, this);
+}
+
+nsresult nsMsgDBView::SetSubthreadKilled(nsIMsgDBHdr* header,
+ nsMsgViewIndex msgIndex,
+ bool ignored) {
+ if (!IsValidIndex(msgIndex)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ NoteChange(msgIndex, 1, nsMsgViewNotificationCode::changed);
+
+ if (!m_db) return NS_ERROR_FAILURE;
+
+ nsresult rv = m_db->MarkHeaderKilled(header, ignored, this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (ignored) {
+ nsCOMPtr<nsIMsgThread> thread;
+ nsresult rv;
+ rv = GetThreadContainingMsgHdr(header, getter_AddRefs(thread));
+ // So we didn't mark threads read.
+ if (NS_FAILED(rv)) return NS_OK;
+
+ uint32_t children, current;
+ thread->GetNumChildren(&children);
+
+ nsMsgKey headKey;
+ header->GetMessageKey(&headKey);
+
+ for (current = 0; current < children; current++) {
+ nsMsgKey newKey;
+ thread->GetChildKeyAt(current, &newKey);
+ if (newKey == headKey) break;
+ }
+
+ // Process all messages, starting with this message.
+ for (; current < children; current++) {
+ nsCOMPtr<nsIMsgDBHdr> nextHdr;
+ bool isKilled;
+
+ thread->GetChildHdrAt(current, getter_AddRefs(nextHdr));
+ nextHdr->GetIsKilled(&isKilled);
+
+ // Ideally, the messages should stop processing here.
+ // However, the children are ordered not by thread...
+ if (isKilled) nextHdr->MarkRead(true);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::SetThreadWatched(nsIMsgThread* thread,
+ nsMsgViewIndex index, bool watched) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ nsresult rv = m_db->MarkThreadWatched(thread, m_keys[index], watched, this);
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetMsgFolder(nsIMsgFolder** aMsgFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ NS_IF_ADDREF(*aMsgFolder = m_folder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetViewFolder(nsIMsgFolder* aMsgFolder) {
+ m_viewFolder = aMsgFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetViewFolder(nsIMsgFolder** aMsgFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ NS_IF_ADDREF(*aMsgFolder = m_viewFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetNumSelected(uint32_t* aNumSelected) {
+ NS_ENSURE_ARG_POINTER(aNumSelected);
+
+ if (!mTreeSelection) {
+ // No tree selection can mean we're in the stand alone mode.
+ *aNumSelected = (m_currentlyDisplayedMsgKey != nsMsgKey_None) ? 1 : 0;
+ return NS_OK;
+ }
+
+ bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads();
+
+ // We call this a lot from the front end JS, so make it fast.
+ nsresult rv = mTreeSelection->GetCount((int32_t*)aNumSelected);
+ if (!*aNumSelected || !includeCollapsedMsgs ||
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ return rv;
+
+ int32_t numSelectedIncludingCollapsed = *aNumSelected;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+ int32_t numIndices = selection.Length();
+ // Iterate over the selection, counting up the messages in collapsed
+ // threads.
+ for (int32_t i = 0; i < numIndices; i++) {
+ if (m_flags[selection[i]] & nsMsgMessageFlags::Elided) {
+ int32_t collapsedCount;
+ ExpansionDelta(selection[i], &collapsedCount);
+ numSelectedIncludingCollapsed += collapsedCount;
+ }
+ }
+
+ *aNumSelected = numSelectedIncludingCollapsed;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetNumMsgsInView(int32_t* aNumMsgs) {
+ NS_ENSURE_ARG_POINTER(aNumMsgs);
+ return (m_folder) ? m_folder->GetTotalMessages(false, aNumMsgs)
+ : NS_ERROR_FAILURE;
+}
+
+/**
+ * @note For the IMAP delete model, this applies to both deleting and
+ * undeleting a message.
+ */
+NS_IMETHODIMP
+nsMsgDBView::GetMsgToSelectAfterDelete(nsMsgViewIndex* msgToSelectAfterDelete) {
+ NS_ENSURE_ARG_POINTER(msgToSelectAfterDelete);
+ *msgToSelectAfterDelete = nsMsgViewIndex_None;
+
+ bool isMultiSelect = false;
+ int32_t startFirstRange = nsMsgViewIndex_None;
+ int32_t endFirstRange = nsMsgViewIndex_None;
+ if (!mTreeSelection) {
+ // If we don't have a tree selection then we must be in stand alone mode.
+ // return the index of the current message key as the first selected index.
+ *msgToSelectAfterDelete = FindViewIndex(m_currentlyDisplayedMsgKey);
+ } else {
+ int32_t selectionCount;
+ int32_t startRange;
+ int32_t endRange;
+ nsresult rv = mTreeSelection->GetRangeCount(&selectionCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (int32_t i = 0; i < selectionCount; i++) {
+ rv = mTreeSelection->GetRangeAt(i, &startRange, &endRange);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Save off the first range in case we need it later.
+ if (i == 0) {
+ startFirstRange = startRange;
+ endFirstRange = endRange;
+ } else {
+ // If the tree selection is goofy (eg adjacent or overlapping ranges),
+ // complain about it, but don't try and cope. Just live with the fact
+ // that one of the deleted messages is going to end up selected.
+ NS_WARNING_ASSERTION(
+ endFirstRange != startRange,
+ "goofy tree selection state: two ranges are adjacent!");
+ }
+
+ *msgToSelectAfterDelete =
+ std::min(*msgToSelectAfterDelete, (nsMsgViewIndex)startRange);
+ }
+
+ // Multiple selection either using Ctrl, Shift, or one of the affordances
+ // to select an entire thread.
+ isMultiSelect = (selectionCount > 1 || (endRange - startRange) > 0);
+ }
+
+ if (*msgToSelectAfterDelete == nsMsgViewIndex_None) return NS_OK;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ GetMsgFolder(getter_AddRefs(folder));
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder);
+ bool thisIsImapFolder = (imapFolder != nullptr);
+ // Need to update the imap-delete model, can change more than once in a
+ // session.
+ if (thisIsImapFolder) GetImapDeleteModel(nullptr);
+
+ // If mail.delete_matches_sort_order is true,
+ // for views sorted in descending order (newest at the top), make
+ // msgToSelectAfterDelete advance in the same direction as the sort order.
+ bool deleteMatchesSort = false;
+ if (m_sortOrder == nsMsgViewSortOrder::descending &&
+ *msgToSelectAfterDelete) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ prefBranch->GetBoolPref("mail.delete_matches_sort_order",
+ &deleteMatchesSort);
+ }
+
+ if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) {
+ if (isMultiSelect) {
+ if (deleteMatchesSort)
+ *msgToSelectAfterDelete = startFirstRange - 1;
+ else
+ *msgToSelectAfterDelete = endFirstRange + 1;
+ } else {
+ if (deleteMatchesSort)
+ *msgToSelectAfterDelete -= 1;
+ else
+ *msgToSelectAfterDelete += 1;
+ }
+ } else if (deleteMatchesSort) {
+ *msgToSelectAfterDelete -= 1;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetRemoveRowOnMoveOrDelete(bool* aRemoveRowOnMoveOrDelete) {
+ NS_ENSURE_ARG_POINTER(aRemoveRowOnMoveOrDelete);
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_folder);
+ if (!imapFolder) {
+ *aRemoveRowOnMoveOrDelete = true;
+ return NS_OK;
+ }
+
+ // Need to update the imap-delete model, can change more than once in a
+ // session.
+ GetImapDeleteModel(nullptr);
+
+ // Unlike the other imap delete models, "mark as deleted" does not remove
+ // rows on delete (or move).
+ *aRemoveRowOnMoveOrDelete =
+ (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetCurrentlyDisplayedMessage(
+ nsMsgViewIndex* currentlyDisplayedMessage) {
+ NS_ENSURE_ARG_POINTER(currentlyDisplayedMessage);
+ *currentlyDisplayedMessage = FindViewIndex(m_currentlyDisplayedMsgKey);
+ return NS_OK;
+}
+
+// If nothing selected, return an NS_ERROR.
+NS_IMETHODIMP
+nsMsgDBView::GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) {
+ NS_ENSURE_ARG_POINTER(hdr);
+
+ nsresult rv;
+ nsMsgKey key;
+ rv = GetKeyForFirstSelectedMessage(&key);
+ // Don't assert, it is legal for nothing to be selected.
+ if (NS_FAILED(rv)) return rv;
+
+ if (key == nsMsgKey_None) {
+ *hdr = nullptr;
+ return NS_OK;
+ }
+
+ if (!m_db) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ rv = m_db->GetMsgHdrForKey(key, hdr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+// If nothing selected, return an NS_ERROR.
+NS_IMETHODIMP
+nsMsgDBView::GetURIForFirstSelectedMessage(nsACString& uri) {
+ nsresult rv;
+ nsMsgViewIndex viewIndex;
+ rv = GetViewIndexForFirstSelectedMsg(&viewIndex);
+ // Don't assert, it is legal for nothing to be selected.
+ if (NS_FAILED(rv)) return rv;
+
+ return GetURIForViewIndex(viewIndex, uri);
+}
+
+NS_IMETHODIMP
+nsMsgDBView::OnDeleteCompleted(bool aSucceeded) {
+ if (m_deletingRows && aSucceeded) {
+ uint32_t numIndices = mIndicesToNoteChange.Length();
+ if (numIndices && (mTree || mJSTree)) {
+ if (numIndices > 1) mIndicesToNoteChange.Sort();
+
+ // The call to NoteChange() has to happen after we are done removing the
+ // keys as NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ if (numIndices > 1) {
+ if (mTree) mTree->BeginUpdateBatch();
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+ }
+
+ for (uint32_t i = 0; i < numIndices; i++)
+ NoteChange(mIndicesToNoteChange[i], -1,
+ nsMsgViewNotificationCode::insertOrDelete);
+
+ if (numIndices > 1) {
+ if (mTree) mTree->EndUpdateBatch();
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ }
+ }
+
+ mIndicesToNoteChange.Clear();
+ }
+
+ m_deletingRows = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetDb(nsIMsgDatabase** aDB) {
+ NS_ENSURE_ARG_POINTER(aDB);
+ NS_IF_ADDREF(*aDB = m_db);
+ return NS_OK;
+}
+
+bool nsMsgDBView::OfflineMsgSelected(
+ nsTArray<nsMsgViewIndex> const& selection) {
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder);
+ if (localFolder) {
+ return true;
+ }
+
+ for (nsMsgViewIndex viewIndex : selection) {
+ // For cross-folder saved searches, we need to check if any message
+ // is in a local folder.
+ if (!m_folder) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ GetFolderForViewIndex(viewIndex, getter_AddRefs(folder));
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(folder);
+ if (localFolder) {
+ return true;
+ }
+ }
+
+ uint32_t flags = m_flags[viewIndex];
+ if ((flags & nsMsgMessageFlags::Offline)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool nsMsgDBView::NonDummyMsgSelected(
+ nsTArray<nsMsgViewIndex> const& selection) {
+ bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads();
+
+ for (nsMsgViewIndex viewIndex : selection) {
+ uint32_t flags = m_flags[viewIndex];
+ // We now treat having a collapsed dummy message selected as if
+ // the whole group was selected so we can apply commands to the group.
+ if (!(flags & MSG_VIEW_FLAG_DUMMY) ||
+ (flags & nsMsgMessageFlags::Elided && includeCollapsedMsgs)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetViewIndexForFirstSelectedMsg(nsMsgViewIndex* aViewIndex) {
+ NS_ENSURE_ARG_POINTER(aViewIndex);
+ // If we don't have a tree selection we must be in stand alone mode...
+ if (!mTreeSelection) {
+ *aViewIndex = m_currentlyDisplayedViewIndex;
+ return NS_OK;
+ }
+
+ int32_t startRange;
+ int32_t endRange;
+ nsresult rv = mTreeSelection->GetRangeAt(0, &startRange, &endRange);
+ // Don't assert, it is legal for nothing to be selected.
+ if (NS_FAILED(rv)) return rv;
+
+ // Check that the first index is valid, it may not be if nothing is selected.
+ if (startRange < 0 || uint32_t(startRange) >= GetSize())
+ return NS_ERROR_UNEXPECTED;
+
+ *aViewIndex = startRange;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetKeyForFirstSelectedMessage(nsMsgKey* key) {
+ NS_ENSURE_ARG_POINTER(key);
+ // If we don't have a tree selection we must be in stand alone mode...
+ if (!mTreeSelection) {
+ *key = m_currentlyDisplayedMsgKey;
+ return NS_OK;
+ }
+
+ int32_t selectionCount;
+ mTreeSelection->GetRangeCount(&selectionCount);
+ if (selectionCount == 0) {
+ *key = nsMsgKey_None;
+ return NS_OK;
+ }
+
+ int32_t startRange;
+ int32_t endRange;
+ nsresult rv = mTreeSelection->GetRangeAt(0, &startRange, &endRange);
+ // Don't assert, it is legal for nothing to be selected.
+ if (NS_FAILED(rv)) return rv;
+
+ // Check that the first index is valid, it may not be if nothing is selected.
+ if (startRange < 0 || uint32_t(startRange) >= GetSize())
+ return NS_ERROR_UNEXPECTED;
+
+ if (m_flags[startRange] & MSG_VIEW_FLAG_DUMMY)
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ *key = m_keys[startRange];
+ return NS_OK;
+}
+
+nsCOMArray<nsIMsgFolder>* nsMsgDBView::GetFolders() { return nullptr; }
+
+nsresult nsMsgDBView::AdjustRowCount(int32_t rowCountBeforeSort,
+ int32_t rowCountAfterSort) {
+ int32_t rowChange = rowCountAfterSort - rowCountBeforeSort;
+
+ if (rowChange) {
+ // This is not safe to use when you have a selection.
+ // RowCountChanged() will call AdjustSelection().
+ uint32_t numSelected = 0;
+ GetNumSelected(&numSelected);
+ NS_ASSERTION(
+ numSelected == 0,
+ "it is not save to call AdjustRowCount() when you have a selection");
+
+ if (mTree) mTree->RowCountChanged(0, rowChange);
+ if (mJSTree) mJSTree->RowCountChanged(0, rowChange);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GetImapDeleteModel(nsIMsgFolder* folder) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // For the search view.
+ if (folder)
+ folder->GetServer(getter_AddRefs(server));
+ else if (m_folder)
+ m_folder->GetServer(getter_AddRefs(server));
+
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server, &rv);
+ if (NS_SUCCEEDED(rv) && imapServer) imapServer->GetDeleteModel(&mDeleteModel);
+
+ return rv;
+}
+
+//
+// CanDrop
+//
+// Can't drop on the thread pane.
+//
+NS_IMETHODIMP
+nsMsgDBView::CanDrop(int32_t index, int32_t orient,
+ mozilla::dom::DataTransfer* dataTransfer, bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ *_retval = false;
+
+ return NS_OK;
+}
+
+//
+// Drop
+//
+// Can't drop on the thread pane.
+//
+NS_IMETHODIMP
+nsMsgDBView::Drop(int32_t row, int32_t orient,
+ mozilla::dom::DataTransfer* dataTransfer) {
+ return NS_OK;
+}
+
+//
+// IsSorted
+//
+// ...
+//
+NS_IMETHODIMP
+nsMsgDBView::IsSorted(bool* _retval) {
+ *_retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SelectFolderMsgByKey(nsIMsgFolder* aFolder, nsMsgKey aKey) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ if (aKey == nsMsgKey_None) return NS_ERROR_FAILURE;
+
+ // This is OK for non search views.
+
+ nsMsgViewIndex viewIndex = FindKey(aKey, true /* expand */);
+
+ if (mTree) mTreeSelection->SetCurrentIndex(viewIndex);
+
+ // Make sure the current message is once again visible in the thread pane
+ // so we don't have to go search for it in the thread pane.
+ if (mTree && viewIndex != nsMsgViewIndex_None) {
+ mTreeSelection->Select(viewIndex);
+ mTree->EnsureRowIsVisible(viewIndex);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SelectMsgByKey(nsMsgKey aKey) {
+ NS_ASSERTION(aKey != nsMsgKey_None, "bad key");
+ if (aKey == nsMsgKey_None) return NS_OK;
+
+ // Use SaveAndClearSelection()
+ // and RestoreSelection() so that we'll clear the current selection
+ // but pass in a different key array so that we'll
+ // select (and load) the desired message.
+
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ nsresult rv = SaveAndClearSelection(nullptr, preservedSelection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now, restore our desired selection.
+ AutoTArray<nsMsgKey, 1> keyArray;
+ keyArray.AppendElement(aKey);
+
+ // If the key was not found
+ // (this can happen with "remember last selected message")
+ // nothing will be selected.
+ rv = RestoreSelection(aKey, keyArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgDBView* newMsgDBView = new nsMsgDBView();
+
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ NS_ENSURE_ARG_POINTER(aNewMsgDBView);
+ if (aMsgWindow) {
+ aNewMsgDBView->mMsgWindowWeak = do_GetWeakReference(aMsgWindow);
+ aMsgWindow->SetOpenFolder(m_viewFolder ? m_viewFolder : m_folder);
+ }
+
+ aNewMsgDBView->mMessengerWeak = do_GetWeakReference(aMessengerInstance);
+ aNewMsgDBView->mCommandUpdater = do_GetWeakReference(aCmdUpdater);
+ aNewMsgDBView->m_folder = m_folder;
+ aNewMsgDBView->m_viewFlags = m_viewFlags;
+ aNewMsgDBView->m_sortOrder = m_sortOrder;
+ aNewMsgDBView->m_sortType = m_sortType;
+ aNewMsgDBView->m_curCustomColumn = m_curCustomColumn;
+ aNewMsgDBView->m_secondarySort = m_secondarySort;
+ aNewMsgDBView->m_secondarySortOrder = m_secondarySortOrder;
+ aNewMsgDBView->m_secondaryCustomColumn = m_secondaryCustomColumn;
+ aNewMsgDBView->m_db = m_db;
+ if (m_db) aNewMsgDBView->m_db->AddListener(aNewMsgDBView);
+
+ aNewMsgDBView->mIsNews = mIsNews;
+ aNewMsgDBView->mIsRss = mIsRss;
+ aNewMsgDBView->mIsXFVirtual = mIsXFVirtual;
+ aNewMsgDBView->mShowSizeInLines = mShowSizeInLines;
+ aNewMsgDBView->mDeleteModel = mDeleteModel;
+ aNewMsgDBView->m_flags = m_flags.Clone();
+ aNewMsgDBView->m_levels = m_levels.Clone();
+ aNewMsgDBView->m_keys = m_keys.Clone();
+
+ aNewMsgDBView->m_customColumnHandlerIDs = m_customColumnHandlerIDs.Clone();
+ aNewMsgDBView->m_customColumnHandlers.AppendObjects(m_customColumnHandlers);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSearchSession(nsIMsgSearchSession** aSession) {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::SetSearchSession(nsIMsgSearchSession* aSession) {
+ NS_ASSERTION(false, "should be overridden by child class");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::GetSupportsThreading(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::FindIndexFromKey(nsMsgKey aMsgKey, bool aExpand,
+ nsMsgViewIndex* aIndex) {
+ NS_ENSURE_ARG_POINTER(aIndex);
+ *aIndex = FindKey(aMsgKey, aExpand);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::FindIndexOfMsgHdr(nsIMsgDBHdr* aMsgHdr, bool aExpand,
+ nsMsgViewIndex* aIndex) {
+ NS_ENSURE_ARG(aMsgHdr);
+ NS_ENSURE_ARG_POINTER(aIndex);
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsMsgViewIndex threadIndex = ThreadIndexOfMsgHdr(aMsgHdr);
+ if (threadIndex != nsMsgViewIndex_None) {
+ if (aExpand && (m_flags[threadIndex] & nsMsgMessageFlags::Elided))
+ ExpandByIndex(threadIndex, nullptr);
+
+ *aIndex = FindHdr(aMsgHdr, threadIndex);
+ } else {
+ *aIndex = nsMsgViewIndex_None;
+ }
+ } else {
+ *aIndex = FindHdr(aMsgHdr);
+ }
+
+ return NS_OK;
+}
+
+static void getDateFormatPref(nsIPrefBranch* _prefBranch,
+ const char* _prefLocalName,
+ nsDateFormatSelectorComm& _format) {
+ // Read.
+ int32_t nFormatSetting(0);
+ nsresult result = _prefBranch->GetIntPref(_prefLocalName, &nFormatSetting);
+ if (NS_SUCCEEDED(result)) {
+ // Translate.
+ nsDateFormatSelectorComm res;
+ res = static_cast<nsDateFormatSelectorComm>(nFormatSetting);
+ // Transfer if valid.
+ if (res >= kDateFormatNone && res <= kDateFormatShort)
+ _format = res;
+ else if (res == kDateFormatWeekday)
+ _format = res;
+ }
+}
+
+nsresult nsMsgDBView::InitDisplayFormats() {
+ m_dateFormatsInitialized = true;
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIPrefService> prefs =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIPrefBranch> dateFormatPrefs;
+ rv = prefs->GetBranch("mail.ui.display.dateformat.",
+ getter_AddRefs(dateFormatPrefs));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ getDateFormatPref(dateFormatPrefs, "default", m_dateFormatDefault);
+ getDateFormatPref(dateFormatPrefs, "thisweek", m_dateFormatThisWeek);
+ getDateFormatPref(dateFormatPrefs, "today", m_dateFormatToday);
+ return rv;
+}
+
+void nsMsgDBView::SetMRUTimeForFolder(nsIMsgFolder* folder) {
+ uint32_t seconds;
+ PRTime2Seconds(PR_Now(), &seconds);
+ nsAutoCString nowStr;
+ nowStr.AppendInt(seconds);
+ folder->SetStringProperty(MRU_TIME_PROPERTY, nowStr);
+}
+
+nsMsgDBView::nsMsgViewHdrEnumerator::nsMsgViewHdrEnumerator(nsMsgDBView* view) {
+ // We need to clone the view because the caller may clear the
+ // current view immediately. It also makes it easier to expand all
+ // if we're working on a copy.
+ nsCOMPtr<nsIMsgDBView> clonedView;
+ view->CloneDBView(nullptr, nullptr, nullptr, getter_AddRefs(clonedView));
+ m_view = static_cast<nsMsgDBView*>(clonedView.get());
+ // Make sure we enumerate over collapsed threads by expanding all.
+ m_view->ExpandAll();
+ m_curHdrIndex = 0;
+}
+
+nsMsgDBView::nsMsgViewHdrEnumerator::~nsMsgViewHdrEnumerator() {
+ if (m_view) m_view->Close();
+}
+
+NS_IMETHODIMP
+nsMsgDBView::nsMsgViewHdrEnumerator::GetNext(nsIMsgDBHdr** aItem) {
+ NS_ENSURE_ARG_POINTER(aItem);
+
+ if (m_curHdrIndex >= m_view->GetSize()) return NS_ERROR_FAILURE;
+
+ // Ignore dummy header. We won't have empty groups, so
+ // we know the view index is good.
+ if (m_view->m_flags[m_curHdrIndex] & MSG_VIEW_FLAG_DUMMY) ++m_curHdrIndex;
+
+ nsCOMPtr<nsIMsgDBHdr> nextHdr;
+
+ nsresult rv =
+ m_view->GetMsgHdrForViewIndex(m_curHdrIndex++, getter_AddRefs(nextHdr));
+ nextHdr.forget(aItem);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgDBView::nsMsgViewHdrEnumerator::HasMoreElements(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_curHdrIndex < m_view->GetSize();
+ return NS_OK;
+}
+
+nsresult nsMsgDBView::GetViewEnumerator(nsIMsgEnumerator** enumerator) {
+ NS_IF_ADDREF(*enumerator = new nsMsgViewHdrEnumerator(this));
+ return (*enumerator) ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
+}
+
+nsresult nsMsgDBView::GetDBForHeader(nsIMsgDBHdr* msgHdr, nsIMsgDatabase** db) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = msgHdr->GetFolder(getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folder->GetMsgDatabase(db);
+}
+
+/**
+ * Determine whether junk commands should be enabled on this view.
+ * Junk commands are always enabled for mail. For nntp and rss, they
+ * may be selectively enabled using an inherited folder property.
+ *
+ * @param aViewIndex view index of the message to check
+ * @return true if junk controls should be enabled
+ */
+bool nsMsgDBView::JunkControlsEnabled(nsMsgViewIndex aViewIndex) {
+ // For normal mail, junk commands are always enabled.
+ if (!(mIsNews || mIsRss || mIsXFVirtual)) return true;
+
+ // We need to check per message or folder.
+ nsCOMPtr<nsIMsgFolder> folder = m_folder;
+ if (!folder && IsValidIndex(aViewIndex))
+ GetFolderForViewIndex(aViewIndex, getter_AddRefs(folder));
+
+ if (folder) {
+ // Check if this is a mail message in search folders.
+ if (mIsXFVirtual) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ folder->GetServer(getter_AddRefs(server));
+ nsAutoCString type;
+ if (server) server->GetType(type);
+
+ if (!(type.LowerCaseEqualsLiteral("nntp") ||
+ type.LowerCaseEqualsLiteral("rss")))
+ return true;
+ }
+
+ // For rss and news, check the inherited folder property.
+ nsAutoCString junkEnableOverride;
+ folder->GetInheritedStringProperty("dobayes.mailnews@mozilla.org#junk",
+ junkEnableOverride);
+ if (junkEnableOverride.EqualsLiteral("true")) return true;
+ }
+
+ return false;
+}
diff --git a/comm/mailnews/base/src/nsMsgDBView.h b/comm/mailnews/base/src/nsMsgDBView.h
new file mode 100644
index 0000000000..6033a490bc
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgDBView.h
@@ -0,0 +1,558 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsMsgDBView_H_
+#define _nsMsgDBView_H_
+
+#include "nsIMsgDBView.h"
+#include "nsIMsgWindow.h"
+#include "nsIMessenger.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "MailNewsTypes.h"
+#include "nsIDBChangeListener.h"
+#include "nsITreeView.h"
+#include "mozilla/dom/XULTreeElement.h"
+#include "nsITreeSelection.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgThread.h"
+#include "nsMsgUtils.h"
+#include "nsIImapIncomingServer.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsIStringBundle.h"
+#include "nsMsgTagService.h"
+#include "nsCOMArray.h"
+#include "nsTArray.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+#include "nsIMsgCustomColumnHandler.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsMsgEnumerator.h"
+
+#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties"
+
+typedef AutoTArray<nsMsgViewIndex, 1> nsMsgViewIndexArray;
+static_assert(nsMsgViewIndex(nsMsgViewIndexArray::NoIndex) ==
+ nsMsgViewIndex_None,
+ "These need to be the same value.");
+
+enum eFieldType { kCollationKey, kU32 };
+
+// This is used in an nsTArray<> to keep track of a multi-column sort.
+class MsgViewSortColumnInfo {
+ public:
+ MsgViewSortColumnInfo(const MsgViewSortColumnInfo& other);
+ MsgViewSortColumnInfo()
+ : mSortType(nsMsgViewSortType::byNone),
+ mSortOrder(nsMsgViewSortOrder::none) {}
+ bool operator==(const MsgViewSortColumnInfo& other) const;
+ nsMsgViewSortTypeValue mSortType;
+ nsMsgViewSortOrderValue mSortOrder;
+ // If mSortType == byCustom, info about the custom column sort.
+ nsString mCustomColumnName;
+ nsCOMPtr<nsIMsgCustomColumnHandler> mColHandler;
+};
+
+// Reserve some bits in the msg flags for the view-only flags.
+// NOTE: this bit space is shared by nsMsgMessageFlags (and labels).
+#define MSG_VIEW_FLAG_ISTHREAD 0x8000000
+#define MSG_VIEW_FLAG_DUMMY 0x20000000
+#define MSG_VIEW_FLAG_HASCHILDREN 0x40000000
+#define MSG_VIEW_FLAGS \
+ (MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_DUMMY | MSG_VIEW_FLAG_ISTHREAD)
+
+// Helper struct for sorting by numeric fields.
+// Associates a message with a key for ordering it in the view.
+struct IdUint32 {
+ nsMsgKey id;
+ uint32_t bits;
+ uint32_t dword; // The numeric key.
+ nsIMsgFolder* folder;
+};
+
+// Extends IdUint32 for sorting by a collation key field (eg subject).
+// (Also used as IdUint32 a couple of places to simplify the code, where
+// the overhead of an unused nsTArray isn't a big deal).
+struct IdKey : public IdUint32 {
+ nsTArray<uint8_t> key;
+};
+
+class nsMsgDBViewService final : public nsIMsgDBViewService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGDBVIEWSERVICE
+
+ nsMsgDBViewService(){};
+
+ protected:
+ ~nsMsgDBViewService(){};
+};
+
+// This is an abstract implementation class.
+// The actual view objects will be instances of sub-classes of this class.
+class nsMsgDBView : public nsIMsgDBView,
+ public nsIDBChangeListener,
+ public nsITreeView,
+ public nsIJunkMailClassificationListener {
+ public:
+ friend class nsMsgDBViewService;
+ nsMsgDBView();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGDBVIEW
+ NS_DECL_NSIDBCHANGELISTENER
+ NS_DECL_NSITREEVIEW
+ NS_DECL_NSIJUNKMAILCLASSIFICATIONLISTENER
+
+ nsMsgViewIndex GetInsertIndexHelper(nsIMsgDBHdr* msgHdr,
+ nsTArray<nsMsgKey>& keys,
+ nsCOMArray<nsIMsgFolder>* folders,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewSortTypeValue sortType);
+ int32_t SecondaryCompare(nsMsgKey key1, nsIMsgFolder* folder1, nsMsgKey key2,
+ nsIMsgFolder* folder2,
+ class viewSortInfo* comparisonContext);
+
+ protected:
+ virtual ~nsMsgDBView();
+
+ static nsString kHighestPriorityString;
+ static nsString kHighPriorityString;
+ static nsString kLowestPriorityString;
+ static nsString kLowPriorityString;
+ static nsString kNormalPriorityString;
+
+ static nsString kReadString;
+ static nsString kRepliedString;
+ static nsString kForwardedString;
+ static nsString kRedirectedString;
+ static nsString kNewString;
+
+ // Used for group views.
+ static nsString kTodayString;
+ static nsString kYesterdayString;
+ static nsString kLastWeekString;
+ static nsString kTwoWeeksAgoString;
+ static nsString kOldMailString;
+ static nsString kFutureDateString;
+
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+ nsCOMPtr<nsIMsgJSTree> mJSTree;
+ nsCOMPtr<nsITreeSelection> mTreeSelection;
+ // We cache this to determine when to push command status notifications.
+ uint32_t mNumSelectedRows;
+ // Set when the message pane is collapsed.
+ bool mSuppressMsgDisplay;
+ bool mSuppressCommandUpdating;
+ // Set when we're telling the outline a row is being removed. Used to
+ // suppress msg loading during delete/move operations.
+ bool mRemovingRow;
+ bool mCommandsNeedDisablingBecauseOfSelection;
+ bool mSuppressChangeNotification;
+
+ virtual const char* GetViewName(void) { return "MsgDBView"; }
+ nsresult FetchAuthor(nsIMsgDBHdr* aHdr, nsAString& aAuthorString);
+ nsresult FetchRecipients(nsIMsgDBHdr* aHdr, nsAString& aRecipientsString);
+ nsresult FetchSubject(nsIMsgDBHdr* aMsgHdr, uint32_t aFlags,
+ nsAString& aValue);
+ nsresult FetchDate(nsIMsgDBHdr* aHdr, nsAString& aDateString,
+ bool rcvDate = false);
+ nsresult FetchStatus(uint32_t aFlags, nsAString& aStatusString);
+ nsresult FetchSize(nsIMsgDBHdr* aHdr, nsAString& aSizeString);
+ nsresult FetchPriority(nsIMsgDBHdr* aHdr, nsAString& aPriorityString);
+ nsresult FetchLabel(nsIMsgDBHdr* aHdr, nsAString& aLabelString);
+ nsresult FetchTags(nsIMsgDBHdr* aHdr, nsAString& aTagString);
+ nsresult FetchKeywords(nsIMsgDBHdr* aHdr, nsACString& keywordString);
+ nsresult FetchRowKeywords(nsMsgViewIndex aRow, nsIMsgDBHdr* aHdr,
+ nsACString& keywordString);
+ nsresult FetchAccount(nsIMsgDBHdr* aHdr, nsAString& aAccount);
+ bool IsOutgoingMsg(nsIMsgDBHdr* aHdr);
+
+ // The default enumerator is over the db, but things like
+ // quick search views will enumerate just the displayed messages.
+ virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator);
+ // this is a message enumerator that enumerates based on the view contents
+ virtual nsresult GetViewEnumerator(nsIMsgEnumerator** enumerator);
+
+ // Save and Restore Selection are a pair of routines you should
+ // use when performing an operation which is going to change the view
+ // and you want to remember the selection. (i.e. for sorting).
+ // Call SaveAndClearSelection and we'll give you an array of msg keys for
+ // the current selection. We also freeze and clear the selection.
+ // When you are done changing the view,
+ // call RestoreSelection passing in the same array
+ // and we'll restore the selection AND unfreeze selection in the UI.
+ nsresult SaveAndClearSelection(nsMsgKey* aCurrentMsgKey,
+ nsTArray<nsMsgKey>& aMsgKeyArray);
+ nsresult RestoreSelection(nsMsgKey aCurrentmsgKey,
+ nsTArray<nsMsgKey>& aMsgKeyArray);
+
+ // This is not safe to use when you have a selection.
+ // RowCountChanged() will call AdjustSelection().
+ // It should be called after SaveAndClearSelection() and before
+ // RestoreSelection().
+ nsresult AdjustRowCount(int32_t rowCountBeforeSort,
+ int32_t rowCountAfterSort);
+
+ nsresult GenerateURIForMsgKey(nsMsgKey aMsgKey, nsIMsgFolder* folder,
+ nsACString& aURI);
+
+ // Routines used in building up view.
+ virtual bool WantsThisThread(nsIMsgThread* thread);
+ virtual nsresult AddHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex* resultIndex = nullptr);
+ bool GetShowingIgnored() {
+ return (m_viewFlags & nsMsgViewFlagsType::kShowIgnored) != 0;
+ }
+ bool OperateOnMsgsInCollapsedThreads();
+
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* aNewHdr, nsMsgKey parentKey,
+ bool ensureListed);
+ virtual nsMsgViewIndex GetInsertIndex(nsIMsgDBHdr* msgHdr);
+ nsMsgViewIndex GetIndexForThread(nsIMsgDBHdr* hdr);
+ nsMsgViewIndex GetThreadRootIndex(nsIMsgDBHdr* msgHdr);
+ virtual nsresult GetMsgHdrForViewIndex(nsMsgViewIndex index,
+ nsIMsgDBHdr** msgHdr);
+ // Given a view index, return the index of the top-level msg in the thread.
+ nsMsgViewIndex GetThreadIndex(nsMsgViewIndex msgIndex);
+
+ virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr,
+ nsMsgKey msgKey, uint32_t flags, uint32_t level);
+ virtual void SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index,
+ nsMsgKey msgKey, uint32_t flags, uint32_t level);
+ virtual void InsertEmptyRows(nsMsgViewIndex viewIndex, int32_t numRows);
+ virtual void RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows);
+ nsresult ToggleExpansion(nsMsgViewIndex index, uint32_t* numChanged);
+ nsresult ExpandByIndex(nsMsgViewIndex index, uint32_t* pNumExpanded);
+ nsresult CollapseByIndex(nsMsgViewIndex index, uint32_t* pNumCollapsed);
+ nsresult ExpandAll();
+ nsresult CollapseAll();
+ nsresult ExpandAndSelectThread();
+
+ // Helper routines for thread expanding and collapsing.
+ nsresult GetThreadCount(nsMsgViewIndex viewIndex, uint32_t* pThreadCount);
+ /**
+ * Retrieve the view index of the first displayed message in the given thread.
+ * @param threadHdr The thread you care about.
+ * @param allowDummy Should dummy headers be returned when the non-dummy
+ * header is available? If the root node of the thread is a dummy header
+ * and you pass false, then we will return the first child of the thread
+ * unless the thread is elided, in which case we will return the root.
+ * If you pass true, we will always return the root.
+ * @return the view index of the first message in the thread, if any.
+ */
+ nsMsgViewIndex GetIndexOfFirstDisplayedKeyInThread(nsIMsgThread* threadHdr,
+ bool allowDummy = false);
+ virtual nsresult GetFirstMessageHdrToDisplayInThread(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr** result);
+ virtual nsMsgViewIndex ThreadIndexOfMsg(
+ nsMsgKey msgKey, nsMsgViewIndex msgIndex = nsMsgViewIndex_None,
+ int32_t* pThreadCount = nullptr, uint32_t* pFlags = nullptr);
+ nsMsgViewIndex ThreadIndexOfMsgHdr(
+ nsIMsgDBHdr* msgHdr, nsMsgViewIndex msgIndex = nsMsgViewIndex_None,
+ int32_t* pThreadCount = nullptr, uint32_t* pFlags = nullptr);
+ nsMsgKey GetKeyOfFirstMsgInThread(nsMsgKey key);
+ int32_t CountExpandedThread(nsMsgViewIndex index);
+ virtual nsresult ExpansionDelta(nsMsgViewIndex index,
+ int32_t* expansionDelta);
+ void ReverseSort();
+ void ReverseThreads();
+ nsresult SaveSortInfo(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder);
+ nsresult RestoreSortInfo();
+ nsresult PersistFolderInfo(nsIDBFolderInfo** dbFolderInfo);
+ void SetMRUTimeForFolder(nsIMsgFolder* folder);
+
+ nsMsgKey GetAt(nsMsgViewIndex index) {
+ return m_keys.SafeElementAt(index, nsMsgKey_None);
+ }
+
+ nsMsgViewIndex FindViewIndex(nsMsgKey key) { return FindKey(key, false); }
+ /**
+ * Find the message header if it is visible in this view. (Messages in
+ * threads/groups that are elided will not be
+ * @param msgHdr Message header to look for.
+ * @param startIndex The index to start looking from.
+ * @param allowDummy Are dummy headers acceptable? If yes, then for a group
+ * with a dummy header, we return the root of the thread (the dummy
+ * header), otherwise we return the actual "content" header for the
+ * message.
+ * @return The view index of the header found, if any.
+ */
+ virtual nsMsgViewIndex FindHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startIndex = 0,
+ bool allowDummy = false);
+ virtual nsMsgViewIndex FindKey(nsMsgKey key, bool expand);
+ virtual nsresult GetDBForViewIndex(nsMsgViewIndex index, nsIMsgDatabase** db);
+ virtual nsCOMArray<nsIMsgFolder>* GetFolders();
+ virtual nsresult GetFolderFromMsgURI(const nsACString& aMsgURI,
+ nsIMsgFolder** aFolder);
+
+ virtual nsresult ListIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex viewIndex,
+ uint32_t* pNumListed);
+ nsresult ListUnreadIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed);
+ nsMsgViewIndex FindParentInThread(nsMsgKey parentKey,
+ nsMsgViewIndex startOfThreadViewIndex);
+ virtual nsresult ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey, uint32_t level,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed);
+ uint32_t GetSize(void) { return (m_keys.Length()); }
+
+ // For commands.
+ virtual nsresult ApplyCommandToIndicesWithFolder(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder);
+ virtual nsresult CopyMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool isMove, nsIMsgFolder* destFolder);
+ virtual nsresult DeleteMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage);
+ nsresult GetHeadersFromSelection(nsTArray<nsMsgViewIndex> const& selection,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& hdrs);
+ // ListCollapsedChildren() adds to messageArray (rather than replacing it).
+ virtual nsresult ListCollapsedChildren(
+ nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray);
+
+ nsresult SetMsgHdrJunkStatus(nsIJunkMailPlugin* aJunkPlugin,
+ nsIMsgDBHdr* aMsgHdr,
+ nsMsgJunkStatus aNewClassification);
+ nsresult ToggleReadByIndex(nsMsgViewIndex index);
+ nsresult SetReadByIndex(nsMsgViewIndex index, bool read);
+ nsresult SetThreadOfMsgReadByIndex(nsMsgViewIndex index,
+ nsTArray<nsMsgKey>& keysMarkedRead,
+ bool read);
+ nsresult SetFlaggedByIndex(nsMsgViewIndex index, bool mark);
+ nsresult OrExtraFlag(nsMsgViewIndex index, uint32_t orflag);
+ nsresult AndExtraFlag(nsMsgViewIndex index, uint32_t andflag);
+ nsresult SetExtraFlag(nsMsgViewIndex index, uint32_t extraflag);
+ virtual nsresult RemoveByIndex(nsMsgViewIndex index);
+ virtual void OnExtraFlagChanged(nsMsgViewIndex /*index*/,
+ uint32_t /*extraFlag*/) {}
+ virtual void OnHeaderAddedOrDeleted() {}
+ nsresult ToggleWatched(nsTArray<nsMsgViewIndex> const& selection);
+ nsresult SetThreadWatched(nsIMsgThread* thread, nsMsgViewIndex index,
+ bool watched);
+ nsresult SetThreadIgnored(nsIMsgThread* thread, nsMsgViewIndex threadIndex,
+ bool ignored);
+ nsresult SetSubthreadKilled(nsIMsgDBHdr* header, nsMsgViewIndex msgIndex,
+ bool ignored);
+ nsresult DownloadForOffline(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection);
+ nsresult DownloadFlaggedForOffline(nsIMsgWindow* window);
+ nsMsgViewIndex GetThreadFromMsgIndex(nsMsgViewIndex index,
+ nsIMsgThread** threadHdr);
+ /// Should junk commands be enabled for the current message in the view?
+ bool JunkControlsEnabled(nsMsgViewIndex aViewIndex);
+
+ // For sorting.
+ nsresult GetFieldTypeAndLenForSort(
+ nsMsgViewSortTypeValue sortType, uint16_t* pMaxLen,
+ eFieldType* pFieldType, nsIMsgCustomColumnHandler* colHandler = nullptr);
+ nsresult GetCollationKey(nsIMsgDBHdr* msgHdr, nsMsgViewSortTypeValue sortType,
+ nsTArray<uint8_t>& result,
+ nsIMsgCustomColumnHandler* colHandler = nullptr);
+ nsresult GetLongField(nsIMsgDBHdr* msgHdr, nsMsgViewSortTypeValue sortType,
+ uint32_t* result,
+ nsIMsgCustomColumnHandler* colHandler = nullptr);
+
+ static int FnSortIdKey(const IdKey* pItem1, const IdKey* pItem2,
+ viewSortInfo* sortInfo);
+ static int FnSortIdUint32(const IdUint32* pItem1, const IdUint32* pItem2,
+ viewSortInfo* sortInfo);
+
+ nsresult GetStatusSortValue(nsIMsgDBHdr* msgHdr, uint32_t* result);
+ nsresult GetLocationCollationKey(nsIMsgDBHdr* msgHdr,
+ nsTArray<uint8_t>& result);
+ void PushSort(const MsgViewSortColumnInfo& newSort);
+ nsresult EncodeColumnSort(nsString& columnSortString);
+ nsresult DecodeColumnSort(nsString& columnSortString);
+ // For view navigation.
+ nsresult NavigateFromPos(nsMsgNavigationTypeValue motion,
+ nsMsgViewIndex startIndex, nsMsgKey* pResultKey,
+ nsMsgViewIndex* pResultIndex,
+ nsMsgViewIndex* pThreadIndex, bool wrap);
+ nsresult FindNextFlagged(nsMsgViewIndex startIndex,
+ nsMsgViewIndex* pResultIndex);
+ nsresult FindFirstNew(nsMsgViewIndex* pResultIndex);
+ nsresult FindPrevUnread(nsMsgKey startKey, nsMsgKey* pResultKey,
+ nsMsgKey* resultThreadId);
+ nsresult FindFirstFlagged(nsMsgViewIndex* pResultIndex);
+ nsresult FindPrevFlagged(nsMsgViewIndex startIndex,
+ nsMsgViewIndex* pResultIndex);
+ nsresult MarkThreadOfMsgRead(nsMsgKey msgId, nsMsgViewIndex msgIndex,
+ nsTArray<nsMsgKey>& idsMarkedRead, bool bRead);
+ nsresult MarkThreadRead(nsIMsgThread* threadHdr, nsMsgViewIndex threadIndex,
+ nsTArray<nsMsgKey>& idsMarkedRead, bool bRead);
+ bool IsValidIndex(nsMsgViewIndex index);
+ nsresult ToggleIgnored(nsTArray<nsMsgViewIndex> const& selection,
+ nsMsgViewIndex* resultIndex, bool* resultToggleState);
+ nsresult ToggleMessageKilled(nsTArray<nsMsgViewIndex> const& selection,
+ nsMsgViewIndex* resultIndex,
+ bool* resultToggleState);
+ bool OfflineMsgSelected(nsTArray<nsMsgViewIndex> const& selection);
+ bool NonDummyMsgSelected(nsTArray<nsMsgViewIndex> const& selection);
+ static void GetString(const char16_t* aStringName, nsAString& aValue);
+ static nsresult GetPrefLocalizedString(const char* aPrefName,
+ nsString& aResult);
+ nsresult AppendKeywordProperties(const nsACString& keywords,
+ nsAString& properties, bool* tagAdded);
+ static nsresult InitLabelStrings(void);
+ nsresult CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater);
+ static void InitializeLiterals();
+ virtual int32_t FindLevelInThread(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startOfThread,
+ nsMsgViewIndex viewIndex);
+ nsresult GetImapDeleteModel(nsIMsgFolder* folder);
+ nsresult UpdateDisplayMessage(nsMsgViewIndex viewPosition);
+ nsresult GetDBForHeader(nsIMsgDBHdr* msgHdr, nsIMsgDatabase** db);
+
+ bool AdjustReadFlag(nsIMsgDBHdr* msgHdr, uint32_t* msgFlags);
+ void FreeAll(nsTArray<void*>* ptrs);
+ void ClearHdrCache();
+
+ // The message held in each row.
+ nsTArray<nsMsgKey> m_keys;
+ // Flags for each row, combining nsMsgMessageFlags and MSG_VIEW_FLAGS.
+ nsTArray<uint32_t> m_flags;
+ // Threading level of each row (1=top)
+ nsTArray<uint8_t> m_levels;
+
+ nsMsgImapDeleteModel mDeleteModel;
+
+ // Cache the most recently asked for header and corresponding msgKey.
+ nsCOMPtr<nsIMsgDBHdr> m_cachedHdr;
+ nsMsgKey m_cachedMsgKey;
+
+ // We need to store the message key for the message we are currently
+ // displaying to ensure we don't try to redisplay the same message just
+ // because the selection changed (i.e. after a sort).
+ nsMsgKey m_currentlyDisplayedMsgKey;
+ nsCString m_currentlyDisplayedMsgUri;
+ nsMsgViewIndex m_currentlyDisplayedViewIndex;
+ // If we're deleting messages, we want to hold off loading messages on
+ // selection changed until the delete is done and we want to batch
+ // notifications.
+ bool m_deletingRows;
+ // For certain special folders and descendants of those folders
+ // (like the "Sent" folder, "Sent/Old Sent").
+ // The Sender column really shows recipients.
+
+ // Server types for this view's folder
+ bool mIsNews; // We have special icons for news.
+ bool mIsRss; // RSS affects enabling of junk commands.
+ bool mIsXFVirtual; // A virtual folder with multiple folders.
+
+ bool mShowSizeInLines; // For news we show lines instead of size when true.
+ bool mSortThreadsByRoot; // As opposed to by the newest message.
+ bool m_sortValid;
+ bool m_checkedCustomColumns;
+ bool mSelectionSummarized;
+ // We asked the front end to summarize the selection and it did not.
+ bool mSummarizeFailed;
+ uint8_t m_saveRestoreSelectionDepth;
+
+ nsCOMPtr<nsIMsgDatabase> m_db;
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ // For virtual folders, the VF db.
+ nsCOMPtr<nsIMsgFolder> m_viewFolder;
+ nsString mMessageType;
+ nsTArray<MsgViewSortColumnInfo> m_sortColumns;
+ nsMsgViewSortTypeValue m_sortType;
+ nsMsgViewSortOrderValue m_sortOrder;
+ nsString m_curCustomColumn;
+ nsMsgViewSortTypeValue m_secondarySort;
+ nsMsgViewSortOrderValue m_secondarySortOrder;
+ nsString m_secondaryCustomColumn;
+ nsMsgViewFlagsTypeValue m_viewFlags;
+
+ // I18N date formatter service which we'll want to cache locally.
+ nsCOMPtr<nsIMsgTagService> mTagService;
+ nsWeakPtr mMessengerWeak;
+ nsWeakPtr mMsgWindowWeak;
+ // We push command update notifications to the UI from this.
+ nsWeakPtr mCommandUpdater;
+ static nsCOMPtr<nsIStringBundle> mMessengerStringBundle;
+
+ // Used to determine when to start and end junk plugin batches.
+ uint32_t mNumMessagesRemainingInBatch;
+
+ // These are the headers of the messages in the current
+ // batch/series of batches of messages manually marked
+ // as junk.
+ nsTArray<RefPtr<nsIMsgDBHdr>> mJunkHdrs;
+
+ nsTArray<uint32_t> mIndicesToNoteChange;
+
+ nsTHashtable<nsCStringHashKey> mEmails;
+
+ // The saved search views keep track of the XX most recently deleted msg ids,
+ // so that if the delete is undone, we can add the msg back to the search
+ // results, even if it no longer matches the search criteria (e.g., a saved
+ // search over unread messages). We use mRecentlyDeletedArrayIndex to treat
+ // the array as a list of the XX most recently deleted msgs.
+ nsTArray<nsCString> mRecentlyDeletedMsgIds;
+ uint32_t mRecentlyDeletedArrayIndex;
+ void RememberDeletedMsgHdr(nsIMsgDBHdr* msgHdr);
+ bool WasHdrRecentlyDeleted(nsIMsgDBHdr* msgHdr);
+
+ // These hold pointers (and IDs) for the nsIMsgCustomColumnHandler object
+ // that constitutes the custom column handler.
+ nsCOMArray<nsIMsgCustomColumnHandler> m_customColumnHandlers;
+ nsTArray<nsString> m_customColumnHandlerIDs;
+
+ nsIMsgCustomColumnHandler* GetColumnHandler(const nsAString& colID);
+ nsIMsgCustomColumnHandler* GetCurColumnHandler();
+ bool CustomColumnsInSortAndNotRegistered();
+ void EnsureCustomColumnsValid();
+
+#ifdef DEBUG_David_Bienvenu
+ void InitEntryInfoForIndex(nsMsgViewIndex i, IdKey& EntryInfo);
+ void ValidateSort();
+#endif
+
+ protected:
+ static nsresult InitDisplayFormats();
+
+ private:
+ static bool m_dateFormatsInitialized;
+ static nsDateFormatSelectorComm m_dateFormatDefault;
+ static nsDateFormatSelectorComm m_dateFormatThisWeek;
+ static nsDateFormatSelectorComm m_dateFormatToday;
+ static nsString m_connectorPattern;
+
+ bool ServerSupportsFilterAfterTheFact();
+
+ nsresult PerformActionsOnJunkMsgs(bool msgsAreJunk);
+ nsresult DetermineActionsForJunkChange(bool msgsAreJunk,
+ nsIMsgFolder* srcFolder,
+ bool& moveMessages,
+ bool& changeReadState,
+ nsIMsgFolder** targetFolder);
+
+ class nsMsgViewHdrEnumerator final : public nsBaseMsgEnumerator {
+ public:
+ explicit nsMsgViewHdrEnumerator(nsMsgDBView* view);
+
+ // nsIMsgEnumerator support.
+ NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override;
+ NS_IMETHOD HasMoreElements(bool* aResult) override;
+
+ RefPtr<nsMsgDBView> m_view;
+ nsMsgViewIndex m_curHdrIndex;
+
+ private:
+ virtual ~nsMsgViewHdrEnumerator() override;
+ };
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgEnumerator.cpp b/comm/mailnews/base/src/nsMsgEnumerator.cpp
new file mode 100644
index 0000000000..5763a8d4dc
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgEnumerator.cpp
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgEnumerator.h"
+
+#include "mozilla/dom/IteratorResultBinding.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/ResultExtensions.h"
+#include "nsContentUtils.h"
+
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+/**
+ * Internal class to support iteration over nsBaseMsgEnumerator in javascript.
+ */
+class JSMsgIterator final : public nsIJSIterator {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIJSITERATOR
+
+ explicit JSMsgIterator(nsBaseMsgEnumerator* aEnumerator)
+ : mEnumerator(aEnumerator) {}
+
+ private:
+ ~JSMsgIterator() = default;
+ RefPtr<nsBaseMsgEnumerator> mEnumerator;
+};
+
+NS_IMETHODIMP JSMsgIterator::Next(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ // result is object of the form: {value: ..., done: ...}
+ RootedDictionary<IteratorResult> result(aCx);
+
+ // We're really using the enumerator itself as the iterator.
+ nsCOMPtr<nsIMsgDBHdr> msg;
+ if (NS_FAILED(mEnumerator->GetNext(getter_AddRefs(msg)))) {
+ result.mDone = true;
+ // Leave value unset.
+ } else {
+ result.mDone = false;
+
+ JS::Rooted<JS::Value> value(aCx);
+ MOZ_TRY(
+ nsContentUtils::WrapNative(aCx, msg, &NS_GET_IID(nsIMsgDBHdr), &value));
+ result.mValue = value;
+ }
+
+ if (!ToJSValue(aCx, result, aResult)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(JSMsgIterator, nsIJSIterator)
+
+// nsBaseMsgEnumerator implementation.
+
+NS_IMETHODIMP nsBaseMsgEnumerator::Iterator(nsIJSIterator** aResult) {
+ auto result = MakeRefPtr<JSMsgIterator>(this);
+ result.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseMsgEnumerator::GetNext(nsIMsgDBHdr** aItem) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsBaseMsgEnumerator::HasMoreElements(bool* aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMPL_ISUPPORTS(nsBaseMsgEnumerator, nsIMsgEnumerator)
+
+/**
+ * Internal class to support iteration over nsBaseMsgThreadEnumerator in
+ * javascript.
+ */
+class JSThreadIterator final : public nsIJSIterator {
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIJSITERATOR
+
+ explicit JSThreadIterator(nsBaseMsgThreadEnumerator* aEnumerator)
+ : mEnumerator(aEnumerator) {}
+
+ private:
+ ~JSThreadIterator() = default;
+ RefPtr<nsBaseMsgThreadEnumerator> mEnumerator;
+};
+
+NS_IMETHODIMP JSThreadIterator::Next(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ // result is object of the form: {value: ..., done: ...}
+ RootedDictionary<IteratorResult> result(aCx);
+
+ // We're really using the enumerator itself as the iterator.
+ nsCOMPtr<nsIMsgThread> msg;
+ if (NS_FAILED(mEnumerator->GetNext(getter_AddRefs(msg)))) {
+ result.mDone = true;
+ // Leave value unset.
+ } else {
+ result.mDone = false;
+
+ JS::Rooted<JS::Value> value(aCx);
+ MOZ_TRY(nsContentUtils::WrapNative(aCx, msg, &NS_GET_IID(nsIMsgThread),
+ &value));
+ result.mValue = value;
+ }
+
+ if (!ToJSValue(aCx, result, aResult)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(JSThreadIterator, nsIJSIterator)
+
+// nsBaseMsgThreadEnumerator implementation.
+
+NS_IMETHODIMP nsBaseMsgThreadEnumerator::Iterator(nsIJSIterator** aResult) {
+ auto result = MakeRefPtr<JSThreadIterator>(this);
+ result.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsBaseMsgThreadEnumerator::GetNext(nsIMsgThread** aItem) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsBaseMsgThreadEnumerator::HasMoreElements(bool* aResult) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMPL_ISUPPORTS(nsBaseMsgThreadEnumerator, nsIMsgThreadEnumerator)
diff --git a/comm/mailnews/base/src/nsMsgEnumerator.h b/comm/mailnews/base/src/nsMsgEnumerator.h
new file mode 100644
index 0000000000..306c3e3d72
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgEnumerator.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef _nsMsgEnumerator_H_
+#define _nsMsgEnumerator_H_
+
+#include "nsIMsgEnumerator.h"
+
+/**
+ * A base implementation nsIMsgEnumerator for stepping over an ordered set
+ * of nsIMsgDBHdr objects.
+ * This provides the javascript iterable protocol (to support for...of
+ * constructs), but getNext() and hasMoreElements() must be implemented by
+ * derived classes.
+ */
+class nsBaseMsgEnumerator : public nsIMsgEnumerator {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGENUMERATOR
+ nsBaseMsgEnumerator(){};
+
+ protected:
+ virtual ~nsBaseMsgEnumerator(){};
+};
+
+/**
+ * A base implementation nsIMsgThreadEnumerator for stepping over an ordered
+ * set of nsIMsgThread objects.
+ * This provides the javascript iterable protocol (to support for...of
+ * constructs), but getNext() and hasMoreElements() must be implemented by
+ * derived classes.
+ */
+class nsBaseMsgThreadEnumerator : public nsIMsgThreadEnumerator {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGTHREADENUMERATOR
+ nsBaseMsgThreadEnumerator(){};
+
+ protected:
+ virtual ~nsBaseMsgThreadEnumerator(){};
+};
+
+#endif /* _nsMsgEnumerator_H_ */
diff --git a/comm/mailnews/base/src/nsMsgFileStream.cpp b/comm/mailnews/base/src/nsMsgFileStream.cpp
new file mode 100644
index 0000000000..9e18ae70f8
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFileStream.cpp
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIFile.h"
+#include "nsMsgFileStream.h"
+#include "prerr.h"
+#include "prerror.h"
+
+/* From nsDebugImpl.cpp: */
+static nsresult ErrorAccordingToNSPR() {
+ PRErrorCode err = PR_GetError();
+ switch (err) {
+ case PR_OUT_OF_MEMORY_ERROR:
+ return NS_ERROR_OUT_OF_MEMORY;
+ case PR_WOULD_BLOCK_ERROR:
+ return NS_BASE_STREAM_WOULD_BLOCK;
+ case PR_FILE_NOT_FOUND_ERROR:
+ return NS_ERROR_FILE_NOT_FOUND;
+ case PR_READ_ONLY_FILESYSTEM_ERROR:
+ return NS_ERROR_FILE_READ_ONLY;
+ case PR_NOT_DIRECTORY_ERROR:
+ return NS_ERROR_FILE_NOT_DIRECTORY;
+ case PR_IS_DIRECTORY_ERROR:
+ return NS_ERROR_FILE_IS_DIRECTORY;
+ case PR_LOOP_ERROR:
+ return NS_ERROR_FILE_UNRESOLVABLE_SYMLINK;
+ case PR_FILE_EXISTS_ERROR:
+ return NS_ERROR_FILE_ALREADY_EXISTS;
+ case PR_FILE_IS_LOCKED_ERROR:
+ return NS_ERROR_FILE_IS_LOCKED;
+ case PR_FILE_TOO_BIG_ERROR:
+ return NS_ERROR_FILE_TOO_BIG;
+ case PR_NO_DEVICE_SPACE_ERROR:
+ return NS_ERROR_FILE_NO_DEVICE_SPACE;
+ case PR_NAME_TOO_LONG_ERROR:
+ return NS_ERROR_FILE_NAME_TOO_LONG;
+ case PR_DIRECTORY_NOT_EMPTY_ERROR:
+ return NS_ERROR_FILE_DIR_NOT_EMPTY;
+ case PR_NO_ACCESS_RIGHTS_ERROR:
+ return NS_ERROR_FILE_ACCESS_DENIED;
+ default:
+ return NS_ERROR_FAILURE;
+ }
+}
+
+nsMsgFileStream::nsMsgFileStream() {
+ mFileDesc = nullptr;
+ mSeekedToEnd = false;
+}
+
+nsMsgFileStream::~nsMsgFileStream() {
+ if (mFileDesc) PR_Close(mFileDesc);
+}
+
+NS_IMPL_ISUPPORTS(nsMsgFileStream, nsIInputStream, nsIOutputStream,
+ nsITellableStream, nsISeekableStream)
+
+nsresult nsMsgFileStream::InitWithFile(nsIFile* file) {
+ return file->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 0664, &mFileDesc);
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::Seek(int32_t whence, int64_t offset) {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+
+ bool seekingToEnd = whence == PR_SEEK_END && offset == 0;
+ if (seekingToEnd && mSeekedToEnd) return NS_OK;
+
+ int64_t cnt = PR_Seek64(mFileDesc, offset, (PRSeekWhence)whence);
+ if (cnt == int64_t(-1)) {
+ return ErrorAccordingToNSPR();
+ }
+
+ mSeekedToEnd = seekingToEnd;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::Tell(int64_t* result) {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+
+ int64_t cnt = PR_Seek64(mFileDesc, 0, PR_SEEK_CUR);
+ if (cnt == int64_t(-1)) {
+ return ErrorAccordingToNSPR();
+ }
+ *result = cnt;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::SetEOF() {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* void close (); */
+NS_IMETHODIMP nsMsgFileStream::Close() {
+ nsresult rv = NS_OK;
+ if (mFileDesc && (PR_Close(mFileDesc) == PR_FAILURE))
+ rv = NS_BASE_STREAM_OSERROR;
+ mFileDesc = nullptr;
+ return rv;
+}
+
+/* unsigned long long available (); */
+NS_IMETHODIMP nsMsgFileStream::Available(uint64_t* aResult) {
+ if (!mFileDesc) return NS_BASE_STREAM_CLOSED;
+
+ int64_t avail = PR_Available64(mFileDesc);
+ if (avail == -1) return ErrorAccordingToNSPR();
+
+ *aResult = avail;
+ return NS_OK;
+}
+
+/* [noscript] unsigned long read (in charPtr aBuf, in unsigned long aCount); */
+NS_IMETHODIMP nsMsgFileStream::Read(char* aBuf, uint32_t aCount,
+ uint32_t* aResult) {
+ if (!mFileDesc) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ int32_t bytesRead = PR_Read(mFileDesc, aBuf, aCount);
+ if (bytesRead == -1) return ErrorAccordingToNSPR();
+
+ *aResult = bytesRead;
+ return NS_OK;
+}
+
+/* [noscript] unsigned long readSegments (in nsWriteSegmentFun aWriter, in
+ * voidPtr aClosure, in unsigned long aCount); */
+NS_IMETHODIMP nsMsgFileStream::ReadSegments(nsWriteSegmentFun aWriter,
+ void* aClosure, uint32_t aCount,
+ uint32_t* _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+/* boolean isNonBlocking (); */
+NS_IMETHODIMP nsMsgFileStream::IsNonBlocking(bool* aNonBlocking) {
+ *aNonBlocking = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::Write(const char* buf, uint32_t count, uint32_t* result) {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+
+ int32_t cnt = PR_Write(mFileDesc, buf, count);
+ if (cnt == -1) {
+ return ErrorAccordingToNSPR();
+ }
+ *result = cnt;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::Flush(void) {
+ if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED;
+
+ int32_t cnt = PR_Sync(mFileDesc);
+ if (cnt == -1) return ErrorAccordingToNSPR();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::WriteFrom(nsIInputStream* inStr, uint32_t count,
+ uint32_t* _retval) {
+ MOZ_ASSERT_UNREACHABLE("WriteFrom (see source comment)");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ // File streams intentionally do not support this method.
+ // If you need something like this, then you should wrap
+ // the file stream using nsIBufferedOutputStream
+}
+
+NS_IMETHODIMP
+nsMsgFileStream::WriteSegments(nsReadSegmentFun reader, void* closure,
+ uint32_t count, uint32_t* _retval) {
+ MOZ_ASSERT_UNREACHABLE("WriteSegments (see source comment)");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ // File streams intentionally do not support this method.
+ // If you need something like this, then you should wrap
+ // the file stream using nsIBufferedOutputStream
+}
+
+NS_IMETHODIMP nsMsgFileStream::StreamStatus() {
+ return mFileDesc ? NS_OK : NS_BASE_STREAM_CLOSED;
+}
diff --git a/comm/mailnews/base/src/nsMsgFileStream.h b/comm/mailnews/base/src/nsMsgFileStream.h
new file mode 100644
index 0000000000..11b5c24e91
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFileStream.h
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Attributes.h"
+#include "msgCore.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsISeekableStream.h"
+#include "prio.h"
+
+class nsMsgFileStream final : public nsIInputStream,
+ public nsIOutputStream,
+ public nsISeekableStream {
+ public:
+ nsMsgFileStream();
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Available(uint64_t* _retval) override;
+ NS_IMETHOD Read(char* aBuf, uint32_t aCount, uint32_t* _retval) override;
+ NS_IMETHOD ReadSegments(nsWriteSegmentFun aWriter, void* aClosure,
+ uint32_t aCount, uint32_t* _retval) override;
+ NS_DECL_NSIOUTPUTSTREAM
+ NS_DECL_NSISEEKABLESTREAM
+ NS_DECL_NSITELLABLESTREAM
+
+ nsresult InitWithFile(nsIFile* localFile);
+
+ protected:
+ ~nsMsgFileStream();
+
+ PRFileDesc* mFileDesc;
+ bool mSeekedToEnd;
+};
diff --git a/comm/mailnews/base/src/nsMsgFolderCache.cpp b/comm/mailnews/base/src/nsMsgFolderCache.cpp
new file mode 100644
index 0000000000..c843730f24
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderCache.cpp
@@ -0,0 +1,570 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgFolderCache.h"
+#include "nsIMsgFolderCacheElement.h"
+#include "nsNetUtil.h"
+#include "nsStreamUtils.h"
+#include "nsIOutputStream.h"
+#include "nsIInputStream.h"
+#include "nsISafeOutputStream.h"
+#include "prprf.h"
+#include "mozilla/Logging.h"
+// Mork-related includes.
+#include "nsIMdbFactoryFactory.h"
+#include "mdb.h"
+// Includes for jsoncpp.
+#include "json/json.h"
+#include <string>
+
+using namespace mozilla;
+
+static LazyLogModule sFolderCacheLog("MsgFolderCache");
+
+// Helper functions for migration of legacy pancea.dat files.
+static nsresult importFromMork(PathString const& dbName, Json::Value& root);
+static nsresult convertTable(nsIMdbEnv* env, nsIMdbStore* store,
+ nsIMdbTable* table, Json::Value& root);
+static void applyEntry(nsCString const& name, nsCString const& val,
+ Json::Value& obj);
+
+/*
+ * nsMsgFolderCacheElement
+ * Folders are given this to let them manipulate their cache data.
+ */
+class nsMsgFolderCacheElement : public nsIMsgFolderCacheElement {
+ public:
+ nsMsgFolderCacheElement(nsMsgFolderCache* owner, nsACString const& key)
+ : mOwner(owner), mKey(key) {}
+ nsMsgFolderCacheElement() = delete;
+
+ NS_DECL_ISUPPORTS
+ NS_IMETHOD GetKey(nsACString& key) override {
+ key = mKey;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetCachedString(const char* name, nsACString& _retval) override {
+ if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE;
+ Json::Value& o = Obj()[name];
+ if (o.isConvertibleTo(Json::stringValue)) {
+ _retval = o.asString().c_str();
+ return NS_OK;
+ }
+ // Leave _retval unchanged if an error occurs.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NS_IMETHOD GetCachedInt32(const char* name, int32_t* _retval) override {
+ if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE;
+ Json::Value& o = Obj()[name];
+ if (o.isConvertibleTo(Json::intValue)) {
+ *_retval = o.asInt();
+ return NS_OK;
+ }
+ // Leave _retval unchanged if an error occurs.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NS_IMETHOD GetCachedUInt32(const char* name, uint32_t* _retval) override {
+ if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE;
+ Json::Value& o = Obj()[name];
+ if (o.isConvertibleTo(Json::uintValue)) {
+ *_retval = o.asUInt();
+ return NS_OK;
+ }
+ // Leave _retval unchanged if an error occurs.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NS_IMETHOD GetCachedInt64(const char* name, int64_t* _retval) override {
+ if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE;
+ Json::Value& o = Obj()[name];
+ // isConvertibleTo() doesn't seem to support Int64. Hence multiple checks.
+ if (o.isNumeric() || o.isNull() || o.isBool()) {
+ *_retval = o.asInt64();
+ return NS_OK;
+ }
+ // Leave _retval unchanged if an error occurs.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ NS_IMETHOD SetCachedString(const char* name,
+ const nsACString& value) override {
+ if (Obj()[name] != PromiseFlatCString(value).get()) {
+ Obj()[name] = PromiseFlatCString(value).get();
+ mOwner->SetModified();
+ }
+ return NS_OK;
+ }
+
+ NS_IMETHOD SetCachedInt32(const char* name, int32_t value) override {
+ if (Obj()[name] != value) {
+ Obj()[name] = value;
+ mOwner->SetModified();
+ }
+ return NS_OK;
+ }
+
+ NS_IMETHOD SetCachedUInt32(const char* name, uint32_t value) override {
+ if (Obj()[name] != value) {
+ Obj()[name] = value;
+ mOwner->SetModified();
+ }
+ return NS_OK;
+ }
+ NS_IMETHOD SetCachedInt64(const char* name, int64_t value) override {
+ if (Obj()[name] != value) {
+ Obj()[name] = value;
+ mOwner->SetModified();
+ }
+ return NS_OK;
+ }
+
+ protected:
+ virtual ~nsMsgFolderCacheElement() {}
+ RefPtr<nsMsgFolderCache> mOwner;
+ nsAutoCString mKey;
+
+ // Helper to get the Json object for this nsFolderCacheElement,
+ // creating it if it doesn't already exist.
+ Json::Value& Obj() {
+ Json::Value& root = *mOwner->mRoot;
+ // This will create an empty object if it doesn't already exist.
+ Json::Value& v = root[mKey.get()];
+ if (v.isObject()) {
+ return v;
+ }
+ // uhoh... either the folder entry doesn't exist (expected) or
+ // the json file wasn't the structure we were expecting.
+ // We _really_ don't want jsoncpp to be throwing exceptions, so in either
+ // case we'll create a fresh new empty object there.
+ root[mKey.get()] = Json::Value(Json::objectValue);
+ return root[mKey.get()];
+ }
+};
+
+NS_IMPL_ISUPPORTS(nsMsgFolderCacheElement, nsIMsgFolderCacheElement)
+
+/*
+ * nsMsgFolderCache implementation
+ */
+
+NS_IMPL_ISUPPORTS(nsMsgFolderCache, nsIMsgFolderCache)
+
+// mRoot dynamically allocated here to avoid exposing Json in header file.
+nsMsgFolderCache::nsMsgFolderCache()
+ : mRoot(new Json::Value(Json::objectValue)),
+ mSavePending(false),
+ mSaveTimer(NS_NewTimer()) {}
+
+NS_IMETHODIMP nsMsgFolderCache::Init(nsIFile* cacheFile, nsIFile* legacyFile) {
+ mCacheFile = cacheFile;
+ // Is there a JSON file to load?
+ bool exists;
+ nsresult rv = cacheFile->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists) {
+ rv = LoadFolderCache(cacheFile);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(
+ sFolderCacheLog, LogLevel::Error,
+ ("Failed to load %s (code 0x%x)",
+ cacheFile->HumanReadablePath().get(), static_cast<uint32_t>(rv)));
+ }
+ // Ignore error. If load fails, we'll just start off with empty cache.
+ return NS_OK;
+ }
+
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug, ("No cache file found."));
+
+ // No sign of new-style JSON file. Maybe there's an old panacea.dat we can
+ // migrate?
+ rv = legacyFile->Exists(&exists);
+ if (NS_SUCCEEDED(rv) && exists) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Found %s. Attempting migration.",
+ legacyFile->HumanReadablePath().get()));
+ Json::Value root(Json::objectValue);
+ rv = importFromMork(legacyFile->NativePath(), root);
+ if (NS_SUCCEEDED(rv)) {
+ *mRoot = root;
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Migration: Legacy cache imported"));
+ // Migrate it to JSON.
+ rv = SaveFolderCache(cacheFile);
+ if (NS_SUCCEEDED(rv)) {
+ // We're done with the legacy panacea.dat - remove it.
+ legacyFile->Remove(false);
+ } else {
+ MOZ_LOG(
+ sFolderCacheLog, LogLevel::Error,
+ ("Migration: save failed (code 0x%x)", static_cast<uint32_t>(rv)));
+ }
+ } else {
+ MOZ_LOG(
+ sFolderCacheLog, LogLevel::Error,
+ ("Migration: import failed (code 0x%x)", static_cast<uint32_t>(rv)));
+ }
+ }
+ // Never fails.
+ return NS_OK;
+}
+
+nsMsgFolderCache::~nsMsgFolderCache() {
+ Flush();
+ delete mRoot;
+}
+
+NS_IMETHODIMP nsMsgFolderCache::Flush() {
+ if (mSavePending) {
+ mSaveTimer->Cancel();
+ mSavePending = false;
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug, ("Forced save."));
+ nsresult rv = SaveFolderCache(mCacheFile);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(
+ sFolderCacheLog, LogLevel::Error,
+ ("Failed to write to %s (code 0x%x)",
+ mCacheFile->HumanReadablePath().get(), static_cast<uint32_t>(rv)));
+ }
+ }
+ return NS_OK;
+}
+
+// Read the cache data from inFile.
+// It's atomic - if a failure occurs, the cache data will be left unchanged.
+nsresult nsMsgFolderCache::LoadFolderCache(nsIFile* inFile) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Loading %s", inFile->HumanReadablePath().get()));
+
+ nsCOMPtr<nsIInputStream> inStream;
+ nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream), inFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString data;
+ rv = NS_ConsumeStream(inStream, UINT32_MAX, data);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Error, ("Read failed."));
+ return rv;
+ }
+
+ Json::Value root;
+ Json::CharReaderBuilder builder;
+ std::unique_ptr<Json::CharReader> const reader(builder.newCharReader());
+ if (!reader->parse(data.BeginReading(), data.EndReading(), &root, nullptr)) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Error, ("Error parsing JSON"));
+ return NS_ERROR_FAILURE; // parsing failed.
+ }
+ if (!root.isObject()) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Error, ("JSON root is not an object"));
+ return NS_ERROR_FAILURE; // bad format.
+ }
+ *mRoot = root;
+ return NS_OK;
+}
+
+// Write the cache data to outFile.
+nsresult nsMsgFolderCache::SaveFolderCache(nsIFile* outFile) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Save to %s", outFile->HumanReadablePath().get()));
+
+ // Serialise the data.
+ Json::StreamWriterBuilder b;
+ // b["indentation"] = "";
+ std::string out = Json::writeString(b, *mRoot);
+
+ // Safe stream, writes to a tempfile first then moves into proper place when
+ // Finish() is called. Could use NS_NewAtomicFileOutputStream, but seems hard
+ // to justify a full filesystem flush).
+ nsCOMPtr<nsIOutputStream> outputStream;
+ nsresult rv =
+ NS_NewSafeLocalFileOutputStream(getter_AddRefs(outputStream), outFile,
+ PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ const char* ptr = out.data();
+ uint32_t remaining = out.length();
+ while (remaining > 0) {
+ uint32_t written = 0;
+ rv = outputStream->Write(ptr, remaining, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ remaining -= written;
+ ptr += written;
+ }
+ nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(outputStream);
+ MOZ_ASSERT(safeStream);
+ rv = safeStream->Finish();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderCache::GetCacheElement(
+ const nsACString& pathKey, bool createIfMissing,
+ nsIMsgFolderCacheElement** result) {
+ nsAutoCString key(pathKey);
+ if (mRoot->isMember(key.get()) || createIfMissing) {
+ nsCOMPtr<nsIMsgFolderCacheElement> element =
+ new nsMsgFolderCacheElement(this, pathKey);
+ element.forget(result);
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsMsgFolderCache::RemoveElement(const nsACString& key) {
+ mRoot->removeMember(PromiseFlatCString(key).get());
+ return NS_OK;
+}
+
+void nsMsgFolderCache::SetModified() {
+ if (mSavePending) {
+ return;
+ }
+ nsresult rv = mSaveTimer->InitWithNamedFuncCallback(
+ doSave, (void*)this, kSaveDelayMs, nsITimer::TYPE_ONE_SHOT,
+ "msgFolderCache::doSave");
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("AutoSave in %ds", kSaveDelayMs / 1000));
+ mSavePending = true;
+ }
+}
+
+// static
+void nsMsgFolderCache::doSave(nsITimer*, void* closure) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug, ("AutoSave"));
+ nsMsgFolderCache* that = static_cast<nsMsgFolderCache*>(closure);
+ nsresult rv = that->SaveFolderCache(that->mCacheFile);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(sFolderCacheLog, LogLevel::Error,
+ ("Failed writing %s (code 0x%x)",
+ that->mCacheFile->HumanReadablePath().get(),
+ static_cast<uint32_t>(rv)));
+ }
+ that->mSavePending = false;
+}
+
+// Helper to apply a legacy property to the new JSON format.
+static void applyEntry(nsCString const& name, nsCString const& val,
+ Json::Value& obj) {
+ // The old mork version stored all numbers as hex, so we need to convert
+ // them into proper Json numeric types. But there's no type info in the
+ // database so we just have to know which values to convert.
+ // We can find a list of all the numeric values by grepping the codebase
+ // for GetCacheInt32/GetCachedInt64. We treat everything else as a string.
+ // It's much harder to get a definitive list of possible keys for strings,
+ // because nsIMsgFolderCache is also used to cache nsDBFolderInfo data -
+ // see nsMsgDBFolder::GetStringProperty().
+
+ // One of the Int32 properties?
+ if (name.EqualsLiteral("hierDelim") ||
+ name.EqualsLiteral("lastSyncTimeInSec") ||
+ name.EqualsLiteral("nextUID") || name.EqualsLiteral("pendingMsgs") ||
+ name.EqualsLiteral("pendingUnreadMsgs") ||
+ name.EqualsLiteral("serverRecent") || name.EqualsLiteral("serverTotal") ||
+ name.EqualsLiteral("serverUnseen") || name.EqualsLiteral("totalMsgs") ||
+ name.EqualsLiteral("totalUnreadMsgs")) {
+ if (val.IsEmpty()) {
+ return;
+ }
+ int32_t i32;
+ if (PR_sscanf(val.get(), "%x", &i32) != 1) {
+ return;
+ }
+ obj[name.get()] = i32;
+ return;
+ }
+
+ // Flags were int32. But the upper bit can be set, meaning we'll get
+ // annoying negative values, which isn't what we want. Not so much of an
+ // issue with legacy pancea.dat as it was all hex strings anyway. But let's
+ // fix it up as we go to JSON.
+ if (name.EqualsLiteral("aclFlags") || name.EqualsLiteral("boxFlags") ||
+ name.EqualsLiteral("flags")) {
+ uint32_t u32;
+ if (PR_sscanf(val.get(), "%x", &u32) != 1) {
+ return;
+ }
+ obj[name.get()] = u32;
+ return;
+ }
+
+ // One of the Int64 properties?
+ if (name.EqualsLiteral("expungedBytes") || name.EqualsLiteral("folderSize")) {
+ if (val.IsEmpty()) {
+ return;
+ }
+ int64_t i64;
+ if (PR_sscanf(val.get(), "%llx", &i64) != 1) {
+ return;
+ }
+ obj[name.get()] = i64;
+ return;
+ }
+
+ // Assume anything else is a string.
+ obj[name.get()] = val.get();
+}
+
+// Import an old panacea.dat mork file, converting it into our JSON form.
+// The flow of this is taken from the old implementation. There are a couple
+// of steps that may not be strictly required, but have been left in anyway
+// on the grounds that it works.
+static nsresult importFromMork(PathString const& dbName, Json::Value& root) {
+ nsresult rv;
+ nsCOMPtr<nsIMdbFactoryService> factoryService =
+ do_GetService("@mozilla.org/db/mork;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMdbFactory> factory;
+ rv = factoryService->GetMdbFactory(getter_AddRefs(factory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMdbEnv> env;
+ rv = factory->MakeEnv(nullptr, getter_AddRefs(env));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ env->SetAutoClear(true);
+ nsCOMPtr<nsIMdbFile> dbFile;
+ rv = factory->OpenOldFile(env,
+ nullptr, // Use default heap alloc fns.
+ dbName.get(),
+ mdbBool_kTrue, // Frozen (read only).
+ getter_AddRefs(dbFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Unsure if we actually need this...
+ mdb_bool canOpen;
+ mdbYarn outFormatVersion;
+ rv = factory->CanOpenFilePort(env, dbFile, &canOpen, &outFormatVersion);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!canOpen) {
+ return NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE;
+ }
+
+ mdbOpenPolicy inOpenPolicy;
+ inOpenPolicy.mOpenPolicy_ScopePlan.mScopeStringSet_Count = 0;
+ inOpenPolicy.mOpenPolicy_MinMemory = 0;
+ inOpenPolicy.mOpenPolicy_MaxLazy = 0;
+
+ nsCOMPtr<nsIMdbThumb> thumb;
+ rv = factory->OpenFileStore(env,
+ nullptr, // Use default heap alloc fns.
+ dbFile, &inOpenPolicy, getter_AddRefs(thumb));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Unsure what this is doing. Applying appended-but-unapplied writes?
+ {
+ mdb_count outTotal; // total somethings to do in operation
+ mdb_count outCurrent; // subportion of total completed so far
+ mdb_bool outDone; // is operation finished?
+ mdb_bool outBroken; // is operation irreparably dead and broken?
+ do {
+ rv = thumb->DoMore(env, &outTotal, &outCurrent, &outDone, &outBroken);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } while (!outBroken && !outDone);
+ }
+
+ // Finally, open the store.
+ nsCOMPtr<nsIMdbStore> store;
+ rv = factory->ThumbToOpenStore(env, thumb, getter_AddRefs(store));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Resolve some tokens we'll need.
+ const char* kFoldersScope = "ns:msg:db:row:scope:folders:all";
+ mdb_token folderRowScopeToken;
+ rv = store->StringToToken(env, kFoldersScope, &folderRowScopeToken);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Find the table. Only one, and we assume id=1. Eek! But original code
+ // did this too...
+ mdbOid allFoldersTableOID{folderRowScopeToken, 1};
+ nsCOMPtr<nsIMdbTable> allFoldersTable;
+ rv = store->GetTable(env, &allFoldersTableOID,
+ getter_AddRefs(allFoldersTable));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // GetTable() can return null even without an error.
+ NS_ENSURE_STATE(allFoldersTable);
+
+ rv = convertTable(env, store, allFoldersTable, root);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// The legacy panacea.dat mork db has a single table, with a row per
+// folder. This function reads it in and writes it into our Json::Value
+// object.
+static nsresult convertTable(nsIMdbEnv* env, nsIMdbStore* store,
+ nsIMdbTable* table, Json::Value& root) {
+ MOZ_ASSERT(root.isObject());
+ MOZ_ASSERT(table);
+
+ nsresult rv;
+ nsCOMPtr<nsIMdbTableRowCursor> rowCursor;
+ rv = table->GetTableRowCursor(env, -1, getter_AddRefs(rowCursor));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // For each row in the table...
+ while (true) {
+ nsCOMPtr<nsIMdbRow> row;
+ mdb_pos pos;
+ rv = rowCursor->NextRow(env, getter_AddRefs(row), &pos);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!row) {
+ break; // That's all the rows done.
+ }
+
+ nsCOMPtr<nsIMdbRowCellCursor> cellCursor;
+ rv = row->GetRowCellCursor(env, -1, getter_AddRefs(cellCursor));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ Json::Value obj(Json::objectValue);
+ // For each cell in the row...
+ nsAutoCString rowKey;
+ while (true) {
+ nsCOMPtr<nsIMdbCell> cell;
+ mdb_column column;
+ rv = cellCursor->NextCell(env, getter_AddRefs(cell), &column, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!cell) {
+ break; // No more cells.
+ }
+
+ // Get the column name
+ nsAutoCString colName;
+ {
+ char buf[100];
+ mdbYarn colYarn{buf, 0, sizeof(buf), 0, 0, nullptr};
+ // Get the column of the cell
+ nsresult rv = store->TokenToString(env, column, &colYarn);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ colName.Assign((const char*)colYarn.mYarn_Buf, colYarn.mYarn_Fill);
+ }
+ // Get the value
+ nsAutoCString colValue;
+ {
+ mdbYarn yarn;
+ cell->AliasYarn(env, &yarn);
+ colValue.Assign((const char*)yarn.mYarn_Buf, yarn.mYarn_Fill);
+ }
+ if (colName.EqualsLiteral("key")) {
+ rowKey = colValue;
+ } else {
+ applyEntry(colName, colValue, obj);
+ }
+ }
+ if (rowKey.IsEmpty()) {
+ continue;
+ }
+
+ MOZ_LOG(sFolderCacheLog, LogLevel::Debug,
+ ("Migration: migrated key '%s' (%d properties)", rowKey.get(),
+ (int)obj.size()));
+ root[rowKey.get()] = obj;
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgFolderCache.h b/comm/mailnews/base/src/nsMsgFolderCache.h
new file mode 100644
index 0000000000..480814ad10
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderCache.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef nsMsgFolderCache_H
+#define nsMsgFolderCache_H
+
+#include "nsIMsgFolderCache.h"
+#include "nsIFile.h"
+#include "nsITimer.h"
+
+namespace Json {
+class Value;
+};
+
+/**
+ * nsMsgFolderCache implements the folder cache, which stores values which
+ * might be slow for the folder to calculate.
+ * It persists the cache data by dumping it out to a .json file when changes
+ * are made. To avoid huge numbers of writes, this autosaving is deferred -
+ * when a cached value is changed, it'll wait a minute or so before
+ * writing, to collect any other changes that occur during that time.
+ * If any changes are outstanding at destruction time, it'll perform an
+ * immediate save then.
+ */
+class nsMsgFolderCache : public nsIMsgFolderCache {
+ public:
+ friend class nsMsgFolderCacheElement;
+
+ nsMsgFolderCache();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFOLDERCACHE
+
+ protected:
+ virtual ~nsMsgFolderCache();
+
+ nsresult LoadFolderCache(nsIFile* jsonFile);
+ nsresult SaveFolderCache(nsIFile* jsonFile);
+ // Flag that a save is required. It'll be deferred by kAutoSaveDelayMs.
+ void SetModified();
+ static constexpr uint32_t kSaveDelayMs = 1000 * 60 * 1; // 1 minute.
+ static void doSave(nsITimer*, void* closure);
+
+ // Path to the JSON file backing the cache.
+ nsCOMPtr<nsIFile> mCacheFile;
+
+ // This is our data store. Kept as a Json::Value for ease of saving, but
+ // it's actually not a bad format for access (it's basically a std::map).
+ // Using a pointer to allow forward declaration. The json headers aren't
+ // in the include path for other modules, so we don't want to expose them
+ // here.
+ Json::Value* mRoot;
+
+ bool mSavePending;
+ nsCOMPtr<nsITimer> mSaveTimer;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgFolderCompactor.cpp b/comm/mailnews/base/src/nsMsgFolderCompactor.cpp
new file mode 100644
index 0000000000..c9740bedb8
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderCompactor.cpp
@@ -0,0 +1,1391 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h" // precompiled header...
+#include "nsCOMPtr.h"
+#include "nsIMsgFolder.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "nsIMsgHdr.h"
+#include "nsIChannel.h"
+#include "nsIStreamListener.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgUtils.h"
+#include "nsISeekableStream.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIPrompt.h"
+#include "nsIMsgLocalMailFolder.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsMailHeaders.h"
+#include "nsMsgLocalFolderHdrs.h"
+#include "nsIMsgDatabase.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsIMsgFolderNotificationService.h"
+#include "nsMsgFolderCompactor.h"
+#include "nsIOutputStream.h"
+#include "nsIInputStream.h"
+#include "nsPrintfCString.h"
+#include "nsIStringBundle.h"
+#include "nsICopyMessageStreamListener.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgPluggableStore.h"
+#include "mozilla/Buffer.h"
+#include "HeaderReader.h"
+#include "LineReader.h"
+#include "mozilla/Components.h"
+
+static nsresult GetBaseStringBundle(nsIStringBundle** aBundle) {
+ NS_ENSURE_ARG_POINTER(aBundle);
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIStringBundle> bundle;
+ return bundleService->CreateBundle(
+ "chrome://messenger/locale/messenger.properties", aBundle);
+}
+
+#define COMPACTOR_READ_BUFF_SIZE 16384
+
+/**
+ * nsFolderCompactState is a helper class for nsFolderCompactor, which
+ * handles compacting the mbox for a single local folder.
+ *
+ * This class also patches X-Mozilla-* headers where required. Usually
+ * these headers are edited in-place without changing the overall size,
+ * but sometimes there's not enough room. So as compaction involves
+ * rewriting the whole file anyway, we take the opportunity to make some
+ * more space and correct those headers.
+ *
+ * NOTE (for future cleanups):
+ *
+ * This base class calls nsIMsgMessageService.copyMessages() to iterate
+ * through messages, passing itself in as a listener. Callbacks from
+ * both nsICopyMessageStreamListener and nsIStreamListener are invoked.
+ *
+ * nsOfflineStoreCompactState uses a different mechanism - see separate
+ * notes below.
+ *
+ * The way the service invokes the listener callbacks is pretty quirky
+ * and probably needs a good sorting out, but for now I'll just document what
+ * I've observed here:
+ *
+ * - The service calls OnStartRequest() at the start of the first message.
+ * - StartMessage() is called at the start of subsequent messages.
+ * - EndCopy() is called at the end of every message except the last one,
+ * where OnStopRequest() is invoked instead.
+ * - OnDataAvailable() is called to pass the message body of each message
+ * (in multiple calls if the message is big enough).
+ * - EndCopy() doesn't ever seem to be passed a failing error code from
+ * what I can see, and its own return code is ignored by upstream code.
+ */
+class nsFolderCompactState : public nsIStreamListener,
+ public nsICopyMessageStreamListener,
+ public nsIUrlListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSICOPYMESSAGESTREAMLISTENER
+ NS_DECL_NSIURLLISTENER
+
+ nsFolderCompactState(void);
+
+ nsresult Compact(nsIMsgFolder* folder,
+ std::function<void(nsresult, uint64_t)> completionFn,
+ nsIMsgWindow* msgWindow);
+
+ protected:
+ virtual ~nsFolderCompactState(void);
+
+ virtual nsresult InitDB(nsIMsgDatabase* db);
+ virtual nsresult StartCompacting();
+ virtual nsresult FinishCompact();
+ void CloseOutputStream();
+ void CleanupTempFilesAfterError();
+ nsresult FlushBuffer();
+
+ nsresult Init(nsIMsgFolder* aFolder, const char* aBaseMsgUri,
+ nsIMsgDatabase* aDb, nsIFile* aPath, nsIMsgWindow* aMsgWindow);
+ nsresult BuildMessageURI(const char* baseURI, nsMsgKey key, nsCString& uri);
+ nsresult ShowStatusMsg(const nsString& aMsg);
+ nsresult ReleaseFolderLock();
+ void ShowCompactingStatusMsg();
+
+ nsCString m_baseMessageUri; // base message uri
+ nsCString m_messageUri; // current message uri being copy
+ nsCOMPtr<nsIMsgFolder> m_folder; // current folder being compact
+ nsCOMPtr<nsIMsgDatabase> m_db; // new database for the compact folder
+ nsCOMPtr<nsIFile> m_file; // new mailbox for the compact folder
+ nsCOMPtr<nsIOutputStream> m_fileStream; // output file stream for writing
+ // All message keys that need to be copied over.
+ nsTArray<nsMsgKey> m_keys;
+
+ // Sum of the sizes of the messages, accumulated as we visit each msg.
+ uint64_t m_totalMsgSize{0};
+ // Number of bytes that can be expunged while compacting.
+ uint64_t m_totalExpungedBytes{0};
+ // Index of the current copied message key in key array.
+ uint32_t m_curIndex{0};
+ // Offset in mailbox of new message.
+ uint64_t m_startOfNewMsg{0};
+ mozilla::Buffer<char> m_buffer{COMPACTOR_READ_BUFF_SIZE};
+ uint32_t m_bufferCount{0};
+
+ // We'll use this if we need to output any EOLs - we try to preserve the
+ // convention found in the input data.
+ nsCString m_eolSeq{MSG_LINEBREAK};
+
+ // The status of the copying operation.
+ nsresult m_status{NS_OK};
+ nsCOMPtr<nsIMsgMessageService> m_messageService; // message service for
+ // copying
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsCOMPtr<nsIMsgDBHdr> m_curSrcHdr;
+ // Flag set when we're waiting for local folder to complete parsing.
+ bool m_parsingFolder;
+ // Flag to indicate we're starting a new message, and that no data has
+ // been written for it yet.
+ bool m_startOfMsg;
+ // Function which will be run when the folder compaction completes.
+ // Takes a result code and the number of bytes which were expunged.
+ std::function<void(nsresult, uint64_t)> m_completionFn;
+ bool m_alreadyWarnedDiskSpace{false};
+};
+
+NS_IMPL_ISUPPORTS(nsFolderCompactState, nsIRequestObserver, nsIStreamListener,
+ nsICopyMessageStreamListener, nsIUrlListener)
+
+nsFolderCompactState::nsFolderCompactState() {
+ m_parsingFolder = false;
+ m_startOfMsg = true;
+}
+
+nsFolderCompactState::~nsFolderCompactState() {
+ CloseOutputStream();
+ if (NS_FAILED(m_status)) {
+ CleanupTempFilesAfterError();
+ // if for some reason we failed remove the temp folder and database
+ }
+}
+
+void nsFolderCompactState::CloseOutputStream() {
+ if (m_fileStream) {
+ m_fileStream->Close();
+ m_fileStream = nullptr;
+ }
+}
+
+void nsFolderCompactState::CleanupTempFilesAfterError() {
+ CloseOutputStream();
+ if (m_db) m_db->ForceClosed();
+ nsCOMPtr<nsIFile> summaryFile;
+ GetSummaryFileLocation(m_file, getter_AddRefs(summaryFile));
+ m_file->Remove(false);
+ summaryFile->Remove(false);
+}
+
+nsresult nsFolderCompactState::BuildMessageURI(const char* baseURI,
+ nsMsgKey key, nsCString& uri) {
+ uri.Append(baseURI);
+ uri.Append('#');
+ uri.AppendInt(key);
+
+ return NS_OK;
+}
+
+nsresult nsFolderCompactState::InitDB(nsIMsgDatabase* db) {
+ nsCOMPtr<nsIMsgDatabase> mailDBFactory;
+ nsresult rv = db->ListAllKeys(m_keys);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgDBService->OpenMailDBFromFile(m_file, m_folder, true, false,
+ getter_AddRefs(m_db));
+
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE ||
+ rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING)
+ // if it's out of date then reopen with upgrade.
+ return msgDBService->OpenMailDBFromFile(m_file, m_folder, true, true,
+ getter_AddRefs(m_db));
+ return rv;
+}
+
+nsresult nsFolderCompactState::Compact(
+ nsIMsgFolder* folder, std::function<void(nsresult, uint64_t)> completionFn,
+ nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG_POINTER(folder);
+ m_completionFn = completionFn;
+ m_window = msgWindow;
+ nsresult rv;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIFile> path;
+ nsCString baseMessageURI;
+
+ nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(folder, &rv);
+ if (NS_SUCCEEDED(rv) && localFolder) {
+ rv = localFolder->GetDatabaseWOReparse(getter_AddRefs(db));
+ if (NS_FAILED(rv) || !db) {
+ if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING ||
+ rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) {
+ m_folder = folder; // will be used to compact
+ m_parsingFolder = true;
+ rv = localFolder->ParseFolder(m_window, this);
+ }
+ return rv;
+ } else {
+ bool valid;
+ rv = db->GetSummaryValid(&valid);
+ if (!valid) // we are probably parsing the folder because we selected it.
+ {
+ folder->NotifyCompactCompleted();
+ if (m_completionFn) {
+ m_completionFn(NS_OK, m_totalExpungedBytes);
+ }
+ return NS_OK;
+ }
+ }
+ } else {
+ rv = folder->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = folder->GetFilePath(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ do {
+ bool exists = false;
+ rv = path->Exists(&exists);
+ if (!exists) {
+ // No need to compact if the local file does not exist.
+ // Can happen e.g. on IMAP when the folder is not marked for offline use.
+ break;
+ }
+
+ int64_t expunged = 0;
+ folder->GetExpungedBytes(&expunged);
+ if (expunged == 0) {
+ // No need to compact if nothing would be expunged.
+ break;
+ }
+
+ int64_t diskSize;
+ rv = folder->GetSizeOnDisk(&diskSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t diskFree;
+ rv = path->GetDiskSpaceAvailable(&diskFree);
+ if (NS_FAILED(rv)) {
+ // If GetDiskSpaceAvailable() failed, better bail out fast.
+ if (rv != NS_ERROR_NOT_IMPLEMENTED) return rv;
+ // Some platforms do not have GetDiskSpaceAvailable implemented.
+ // In that case skip the preventive free space analysis and let it
+ // fail in compact later if space actually wasn't available.
+ } else {
+ // Let's try to not even start compact if there is really low free space.
+ // It may still fail later as we do not know how big exactly the folder DB
+ // will end up being. The DB already doesn't contain references to
+ // messages that are already deleted. So theoretically it shouldn't shrink
+ // with compact. But in practice, the automatic shrinking of the DB may
+ // still have not yet happened. So we cap the final size at 1KB per
+ // message.
+ db->Commit(nsMsgDBCommitType::kCompressCommit);
+
+ int64_t dbSize;
+ rv = db->GetDatabaseSize(&dbSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t totalMsgs;
+ rv = folder->GetTotalMessages(false, &totalMsgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int64_t expectedDBSize =
+ std::min<int64_t>(dbSize, ((int64_t)totalMsgs) * 1024);
+ if (diskFree < diskSize - expunged + expectedDBSize) {
+ if (!m_alreadyWarnedDiskSpace) {
+ folder->ThrowAlertMsg("compactFolderInsufficientSpace", m_window);
+ m_alreadyWarnedDiskSpace = true;
+ }
+ break;
+ }
+ }
+
+ rv = folder->GetBaseMessageURI(baseMessageURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = Init(folder, baseMessageURI.get(), db, path, m_window);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isLocked = true;
+ m_folder->GetLocked(&isLocked);
+ if (isLocked) {
+ CleanupTempFilesAfterError();
+ m_folder->ThrowAlertMsg("compactFolderDeniedLock", m_window);
+ break;
+ }
+
+ // If we got here start the real compacting.
+ nsCOMPtr<nsISupports> supports;
+ QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(supports));
+ m_folder->AcquireSemaphore(supports);
+ m_totalExpungedBytes += expunged;
+ return StartCompacting();
+
+ } while (false); // block for easy skipping the compaction using 'break'
+
+ // Skipped folder, for whatever reason.
+ folder->NotifyCompactCompleted();
+ if (m_completionFn) {
+ m_completionFn(NS_OK, m_totalExpungedBytes);
+ }
+ return NS_OK;
+}
+
+nsresult nsFolderCompactState::ShowStatusMsg(const nsString& aMsg) {
+ if (!m_window || aMsg.IsEmpty()) return NS_OK;
+
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ nsresult rv = m_window->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (NS_FAILED(rv) || !statusFeedback) return NS_OK;
+
+ // Try to prepend account name to the message.
+ nsString statusMessage;
+ do {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = m_folder->GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) break;
+ nsAutoString accountName;
+ rv = server->GetPrettyName(accountName);
+ if (NS_FAILED(rv)) break;
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ if (NS_FAILED(rv)) break;
+ AutoTArray<nsString, 2> params = {accountName, aMsg};
+ rv = bundle->FormatStringFromName("statusMessage", params, statusMessage);
+ } while (false);
+
+ // If fetching any of the needed info failed, just show the original message.
+ if (NS_FAILED(rv)) statusMessage.Assign(aMsg);
+ return statusFeedback->SetStatusString(statusMessage);
+}
+
+nsresult nsFolderCompactState::Init(nsIMsgFolder* folder,
+ const char* baseMsgUri, nsIMsgDatabase* db,
+ nsIFile* path, nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+
+ m_folder = folder;
+ m_baseMessageUri = baseMsgUri;
+ m_file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_file->InitWithFile(path);
+
+ m_file->SetNativeLeafName("nstmp"_ns);
+ // Make sure we are not crunching existing nstmp file.
+ rv = m_file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_window = aMsgWindow;
+ m_totalMsgSize = 0;
+ rv = InitDB(db);
+ if (NS_FAILED(rv)) {
+ CleanupTempFilesAfterError();
+ return rv;
+ }
+
+ m_curIndex = 0;
+
+ rv = MsgNewBufferedFileOutputStream(getter_AddRefs(m_fileStream), m_file, -1,
+ 00600);
+ if (NS_FAILED(rv))
+ m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window);
+ else
+ rv = GetMessageServiceFromURI(nsDependentCString(baseMsgUri),
+ getter_AddRefs(m_messageService));
+ if (NS_FAILED(rv)) {
+ m_status = rv;
+ }
+ return rv;
+}
+
+void nsFolderCompactState::ShowCompactingStatusMsg() {
+ nsString statusString;
+ nsresult rv = m_folder->GetStringWithFolderNameFromBundle("compactingFolder",
+ statusString);
+ if (!statusString.IsEmpty() && NS_SUCCEEDED(rv)) ShowStatusMsg(statusString);
+}
+
+NS_IMETHODIMP nsFolderCompactState::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+// If we had to kick off a folder parse, this will be called when it
+// completes.
+NS_IMETHODIMP nsFolderCompactState::OnStopRunningUrl(nsIURI* url,
+ nsresult status) {
+ if (m_parsingFolder) {
+ m_parsingFolder = false;
+ if (NS_SUCCEEDED(status)) {
+ // Folder reparse succeeded. Start compacting it.
+ status = Compact(m_folder, m_completionFn, m_window);
+ if (NS_SUCCEEDED(status)) {
+ return NS_OK;
+ }
+ }
+ }
+
+ // This is from bug 249754. The aim is to close the DB file to avoid
+ // running out of filehandles when large numbers of folders are compacted.
+ // But it seems like filehandle management would be better off being
+ // handled by the DB class itself (it might be already, but it's hard to
+ // tell)...
+ m_folder->SetMsgDatabase(nullptr);
+
+ if (m_completionFn) {
+ m_completionFn(status, m_totalExpungedBytes);
+ }
+ return NS_OK;
+}
+
+nsresult nsFolderCompactState::StartCompacting() {
+ nsresult rv = NS_OK;
+ // Notify that compaction is beginning. We do this even if there are no
+ // messages to be copied because the summary database still gets blown away
+ // which is still pretty interesting. (And we like consistency.)
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyFolderCompactStart(m_folder);
+ }
+
+ // TODO: test whether sorting the messages (m_keys) by messageOffset
+ // would improve performance on large files (less seeks).
+ // The m_keys array is in the order as stored in DB and on IMAP or News
+ // the messages stored on the mbox file are not necessarily in the same order.
+ if (m_keys.Length() > 0) {
+ nsCOMPtr<nsIURI> notUsed;
+ ShowCompactingStatusMsg();
+ NS_ADDREF_THIS();
+ rv = m_messageService->CopyMessages(m_keys, m_folder, this, false, nullptr,
+ m_window, getter_AddRefs(notUsed));
+ } else { // no messages to copy with
+ FinishCompact();
+ }
+ return rv;
+}
+
+nsresult nsFolderCompactState::FinishCompact() {
+ NS_ENSURE_TRUE(m_folder, NS_ERROR_NOT_INITIALIZED);
+ NS_ENSURE_TRUE(m_file, NS_ERROR_NOT_INITIALIZED);
+
+ // All okay time to finish up the compact process
+ nsCOMPtr<nsIFile> path;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+
+ // get leaf name and database name of the folder
+ nsresult rv = m_folder->GetFilePath(getter_AddRefs(path));
+ nsCOMPtr<nsIFile> folderPath =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = folderPath->InitWithFile(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> oldSummaryFile;
+ rv = GetSummaryFileLocation(folderPath, getter_AddRefs(oldSummaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsAutoCString dbName;
+ oldSummaryFile->GetNativeLeafName(dbName);
+ nsAutoCString folderName;
+ path->GetNativeLeafName(folderName);
+
+ // close down the temp file stream; preparing for deleting the old folder
+ // and its database; then rename the temp folder and database
+ if (m_fileStream) {
+ m_fileStream->Flush();
+ m_fileStream->Close();
+ m_fileStream = nullptr;
+ }
+
+ // make sure the new database is valid.
+ // Close it so we can rename the .msf file.
+ if (m_db) {
+ m_db->ForceClosed();
+ m_db = nullptr;
+ }
+
+ nsCOMPtr<nsIFile> newSummaryFile;
+ rv = GetSummaryFileLocation(m_file, getter_AddRefs(newSummaryFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDBFolderInfo> transferInfo;
+ m_folder->GetDBTransferInfo(getter_AddRefs(transferInfo));
+
+ // close down database of the original folder
+ m_folder->ForceDBClosed();
+
+ nsCOMPtr<nsIFile> cloneFile;
+ int64_t fileSize = 0;
+ rv = m_file->Clone(getter_AddRefs(cloneFile));
+ if (NS_SUCCEEDED(rv)) rv = cloneFile->GetFileSize(&fileSize);
+ bool tempFileRightSize = ((uint64_t)fileSize == m_totalMsgSize);
+ NS_WARNING_ASSERTION(tempFileRightSize,
+ "temp file not of expected size in compact");
+
+ bool folderRenameSucceeded = false;
+ bool msfRenameSucceeded = false;
+ if (NS_SUCCEEDED(rv) && tempFileRightSize) {
+ // First we're going to try and move the old summary file out the way.
+ // We don't delete it yet, as we want to keep the files in sync.
+ nsCOMPtr<nsIFile> tempSummaryFile;
+ rv = oldSummaryFile->Clone(getter_AddRefs(tempSummaryFile));
+ if (NS_SUCCEEDED(rv))
+ rv = tempSummaryFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+
+ nsAutoCString tempSummaryFileName;
+ if (NS_SUCCEEDED(rv))
+ rv = tempSummaryFile->GetNativeLeafName(tempSummaryFileName);
+
+ if (NS_SUCCEEDED(rv))
+ rv = oldSummaryFile->MoveToNative((nsIFile*)nullptr, tempSummaryFileName);
+
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error moving compacted folder's db out of the way");
+ if (NS_SUCCEEDED(rv)) {
+ // Now we've successfully moved the summary file out the way, try moving
+ // the newly compacted message file over the old one.
+ rv = m_file->MoveToNative((nsIFile*)nullptr, folderName);
+ folderRenameSucceeded = NS_SUCCEEDED(rv);
+ NS_WARNING_ASSERTION(folderRenameSucceeded,
+ "error renaming compacted folder");
+ if (folderRenameSucceeded) {
+ // That worked, so land the new summary file in the right place.
+ nsCOMPtr<nsIFile> renamedCompactedSummaryFile;
+ newSummaryFile->Clone(getter_AddRefs(renamedCompactedSummaryFile));
+ if (renamedCompactedSummaryFile) {
+ rv = renamedCompactedSummaryFile->MoveToNative((nsIFile*)nullptr,
+ dbName);
+ msfRenameSucceeded = NS_SUCCEEDED(rv);
+ }
+ NS_WARNING_ASSERTION(msfRenameSucceeded,
+ "error renaming compacted folder's db");
+ }
+
+ if (!msfRenameSucceeded) {
+ // Do our best to put the summary file back to where it was
+ rv = tempSummaryFile->MoveToNative((nsIFile*)nullptr, dbName);
+ if (NS_SUCCEEDED(rv)) {
+ // Flagging that a renamed db no longer exists.
+ tempSummaryFile = nullptr;
+ } else {
+ NS_WARNING("error restoring uncompacted folder's db");
+ }
+ }
+ }
+ // We don't want any temporarily renamed summary file to lie around
+ if (tempSummaryFile) tempSummaryFile->Remove(false);
+ }
+
+ NS_WARNING_ASSERTION(msfRenameSucceeded, "compact failed");
+ nsresult rvReleaseFolderLock = ReleaseFolderLock();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rvReleaseFolderLock),
+ "folder lock not released successfully");
+ rv = NS_FAILED(rv) ? rv : rvReleaseFolderLock;
+
+ // Cleanup of nstmp-named compacted files if failure
+ if (!folderRenameSucceeded) {
+ // remove the abandoned compacted version with the wrong name
+ m_file->Remove(false);
+ }
+ if (!msfRenameSucceeded) {
+ // remove the abandoned compacted summary file
+ newSummaryFile->Remove(false);
+ }
+
+ if (msfRenameSucceeded) {
+ // Transfer local db information from transferInfo
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = msgDBService->OpenFolderDB(m_folder, true, getter_AddRefs(m_db));
+ NS_ENSURE_TRUE(m_db, NS_FAILED(rv) ? rv : NS_ERROR_FAILURE);
+ // These errors are expected.
+ rv = (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING ||
+ rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE)
+ ? NS_OK
+ : rv;
+ m_db->SetSummaryValid(true);
+ if (transferInfo) m_folder->SetDBTransferInfo(transferInfo);
+
+ // since we're transferring info from the old db, we need to reset the
+ // expunged bytes
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) dbFolderInfo->SetExpungedBytes(0);
+ }
+ if (m_db) m_db->Close(true);
+ m_db = nullptr;
+
+ // Notify that compaction of the folder is completed.
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ notifier->NotifyFolderCompactFinish(m_folder);
+ }
+
+ m_folder->NotifyCompactCompleted();
+ if (m_completionFn) {
+ m_completionFn(rv, m_totalExpungedBytes);
+ }
+
+ return NS_OK;
+}
+
+nsresult nsFolderCompactState::ReleaseFolderLock() {
+ nsresult result = NS_OK;
+ if (!m_folder) return result;
+ bool haveSemaphore;
+ nsCOMPtr<nsISupports> supports;
+ QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(supports));
+ result = m_folder->TestSemaphore(supports, &haveSemaphore);
+ if (NS_SUCCEEDED(result) && haveSemaphore)
+ result = m_folder->ReleaseSemaphore(supports);
+ return result;
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::OnStartRequest(nsIRequest* request) {
+ return StartMessage();
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::OnStopRequest(nsIRequest* request, nsresult status) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ if (NS_FAILED(status)) {
+ // Set m_status to status so the destructor can remove the temp folder and
+ // database.
+ m_status = status;
+ CleanupTempFilesAfterError();
+ m_folder->NotifyCompactCompleted();
+ ReleaseFolderLock();
+ m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window);
+ } else {
+ // XXX TODO: Error checking and handling missing here.
+ EndCopy(nullptr, status);
+ if (m_curIndex >= m_keys.Length()) {
+ msgHdr = nullptr;
+ // no more to copy finish it up
+ FinishCompact();
+ } else {
+ // in case we're not getting an error, we still need to pretend we did get
+ // an error, because the compact did not successfully complete.
+ m_folder->NotifyCompactCompleted();
+ CleanupTempFilesAfterError();
+ ReleaseFolderLock();
+ }
+ }
+ NS_RELEASE_THIS(); // kill self
+ return status;
+}
+
+// Handle the message data.
+// (NOTE: nsOfflineStoreCompactState overrides this)
+NS_IMETHODIMP
+nsFolderCompactState::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset, uint32_t count) {
+ MOZ_ASSERT(m_fileStream);
+ MOZ_ASSERT(inStr);
+
+ nsresult rv = NS_OK;
+
+ // TODO: This block should be moved in to the callback that indicates the
+ // start of a new message, but it's complicated because of the derived
+ // nsOfflineStoreCompactState and also the odd message copy listener
+ // orderings. Leaving it here for now, but it's ripe for tidying up in
+ // future.
+ if (m_startOfMsg) {
+ m_messageUri.Truncate(); // clear the previous message uri
+ if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex],
+ m_messageUri))) {
+ rv = m_messageService->MessageURIToMsgHdr(m_messageUri,
+ getter_AddRefs(m_curSrcHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ while (count > 0) {
+ uint32_t maxReadCount =
+ std::min((uint32_t)m_buffer.Length() - m_bufferCount, count);
+ uint32_t readCount;
+ rv = inStr->Read(m_buffer.Elements() + m_bufferCount, maxReadCount,
+ &readCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ count -= readCount;
+ m_bufferCount += readCount;
+ if (m_bufferCount == m_buffer.Length()) {
+ rv = FlushBuffer();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ if (m_bufferCount > 0) {
+ rv = FlushBuffer();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+// Helper to write data to an outputstream, until complete or error.
+static nsresult WriteSpan(nsIOutputStream* writeable,
+ mozilla::Span<const char> data) {
+ while (!data.IsEmpty()) {
+ uint32_t n;
+ nsresult rv = writeable->Write(data.Elements(), data.Length(), &n);
+ NS_ENSURE_SUCCESS(rv, rv);
+ data = data.Last(data.Length() - n);
+ }
+ return NS_OK;
+}
+
+// Flush contents of m_buffer to the output file.
+// (NOTE: not used by nsOfflineStoreCompactState)
+// More complicated than it should be because we need to fiddle with
+// some of the X-Mozilla-* headers on the fly.
+nsresult nsFolderCompactState::FlushBuffer() {
+ MOZ_ASSERT(m_fileStream);
+ nsresult rv;
+ auto buf = m_buffer.AsSpan().First(m_bufferCount);
+ if (!m_startOfMsg) {
+ // We only do header twiddling for the first chunk. So from now on we
+ // just copy data verbatim.
+ rv = WriteSpan(m_fileStream, buf);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_bufferCount = 0;
+ return NS_OK;
+ }
+
+ // This is the first chunk of a new message. We'll update the
+ // X-Mozilla-(Status|Status2|Keys) headers as we go.
+ m_startOfMsg = false;
+
+ // Sniff the data to see if we can spot any CRs.
+ // If so, we'll use CRLFs instead of platform-native EOLs.
+ auto sniffChunk = buf.First(std::min<size_t>(buf.Length(), 512));
+ auto cr = std::find(sniffChunk.cbegin(), sniffChunk.cend(), '\r');
+ if (cr != sniffChunk.cend()) {
+ m_eolSeq.Assign("\r\n"_ns);
+ }
+
+ // Add a "From " line if missing.
+ // NOTE: Ultimately we should never see "From " lines in this data - it's an
+ // mbox detail the message streaming should filter out. But for now we'll
+ // handle it optionally.
+ nsAutoCString fromLine;
+ auto l = FirstLine(buf);
+ if (l.Length() > 5 &&
+ nsDependentCSubstring(l.Elements(), 5).EqualsLiteral("From ")) {
+ fromLine = nsDependentCSubstring(l);
+ buf = buf.From(l.Length());
+ } else {
+ fromLine = "From "_ns + m_eolSeq;
+ }
+ rv = WriteSpan(m_fileStream, fromLine);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Read as many headers as we can. We might not have the complete header
+ // block our in buffer, but that's OK - the X-Mozilla-* ones should be
+ // right at the start).
+ nsTArray<HeaderReader::Hdr> headers;
+ HeaderReader rdr;
+ auto leftover = rdr.Parse(buf, [&](auto const& hdr) -> bool {
+ auto const& name = hdr.Name(buf);
+ if (!name.EqualsLiteral(HEADER_X_MOZILLA_STATUS) &&
+ !name.EqualsLiteral(HEADER_X_MOZILLA_STATUS2) &&
+ !name.EqualsLiteral(HEADER_X_MOZILLA_KEYWORDS)) {
+ headers.AppendElement(hdr);
+ }
+ return true;
+ });
+
+ // Write out X-Mozilla-* headers first - we'll create these from scratch.
+ uint32_t msgFlags = 0;
+ nsAutoCString keywords;
+ if (m_curSrcHdr) {
+ m_curSrcHdr->GetFlags(&msgFlags);
+ m_curSrcHdr->GetStringProperty("keywords", keywords);
+ // growKeywords is set if msgStore didn't have enough room to edit
+ // X-Mozilla-* headers in situ. We'll rewrite all those headers
+ // regardless but we still want to clear it.
+ uint32_t grow;
+ m_curSrcHdr->GetUint32Property("growKeywords", &grow);
+ if (grow) {
+ m_curSrcHdr->SetUint32Property("growKeywords", 0);
+ }
+ }
+
+ auto out =
+ nsPrintfCString(HEADER_X_MOZILLA_STATUS ": %4.4x", msgFlags & 0xFFFF);
+ out.Append(m_eolSeq);
+ rv = WriteSpan(m_fileStream, out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ out = nsPrintfCString(HEADER_X_MOZILLA_STATUS2 ": %8.8x",
+ msgFlags & 0xFFFF0000);
+ out.Append(m_eolSeq);
+ rv = WriteSpan(m_fileStream, out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Try to leave room for future in-place keyword edits.
+ while (keywords.Length() < X_MOZILLA_KEYWORDS_BLANK_LEN) {
+ keywords.Append(' ');
+ }
+ out = nsPrintfCString(HEADER_X_MOZILLA_KEYWORDS ": %s", keywords.get());
+ out.Append(m_eolSeq);
+ rv = WriteSpan(m_fileStream, out);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Write out the rest of the headers.
+ for (auto const& hdr : headers) {
+ rv = WriteSpan(m_fileStream, buf.Subspan(hdr.pos, hdr.len));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // The header parser consumes the blank line, If we've completed parsing
+ // we need to output it now.
+ // If we haven't parsed all the headers yet then the blank line will be
+ // safely copied verbatim as part of the remaining data.
+ if (rdr.IsComplete()) {
+ rv = WriteSpan(m_fileStream, m_eolSeq);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Write out everything else in the buffer verbatim.
+ if (leftover.Length() > 0) {
+ rv = WriteSpan(m_fileStream, leftover);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ m_bufferCount = 0;
+ return NS_OK;
+}
+
+/**
+ * nsOfflineStoreCompactState is a helper class for nsFolderCompactor which
+ * handles compacting the mbox for a single offline IMAP folder.
+ *
+ * nsOfflineStoreCompactState does *not* do any special X-Mozilla-* header
+ * handling, unlike the base class.
+ *
+ * NOTE (for future cleanups):
+ * This class uses a different mechanism to iterate through messages. It uses
+ * nsIMsgMessageService.streamMessage() to stream each message in turn,
+ * passing itself in as an nsIStreamListener. The nsICopyMessageStreamListener
+ * callbacks implemented in the base class are _not_ used here.
+ * For each message, the standard OnStartRequest(), OnDataAvailable()...,
+ * OnStopRequest() sequence is seen.
+ * Nothing too fancy, but it's not always clear where code from the base class
+ * is being used and when it is not, so it can be complicated to pick through.
+ *
+ */
+class nsOfflineStoreCompactState : public nsFolderCompactState {
+ public:
+ nsOfflineStoreCompactState(void);
+ virtual ~nsOfflineStoreCompactState(void);
+ NS_IMETHOD OnStopRequest(nsIRequest* request, nsresult status) override;
+ NS_IMETHODIMP OnDataAvailable(nsIRequest* request, nsIInputStream* inStr,
+ uint64_t sourceOffset, uint32_t count) override;
+
+ protected:
+ nsresult CopyNextMessage(bool& done);
+ virtual nsresult InitDB(nsIMsgDatabase* db) override;
+ virtual nsresult StartCompacting() override;
+ virtual nsresult FinishCompact() override;
+
+ char m_dataBuffer[COMPACTOR_READ_BUFF_SIZE + 1]; // temp data buffer for
+ // copying message
+ uint32_t m_offlineMsgSize;
+};
+
+nsOfflineStoreCompactState::nsOfflineStoreCompactState()
+ : m_offlineMsgSize(0) {}
+
+nsOfflineStoreCompactState::~nsOfflineStoreCompactState() {}
+
+nsresult nsOfflineStoreCompactState::InitDB(nsIMsgDatabase* db) {
+ // Start with the list of messages we have offline as the possible
+ // message to keep when compacting the offline store.
+ db->ListAllOfflineMsgs(m_keys);
+ m_db = db;
+ return NS_OK;
+}
+
+/**
+ * This will copy one message to the offline store, but if it fails to
+ * copy the next message, it will keep trying messages until it finds one
+ * it can copy, or it runs out of messages.
+ */
+nsresult nsOfflineStoreCompactState::CopyNextMessage(bool& done) {
+ while (m_curIndex < m_keys.Length()) {
+ // Filter out msgs that have the "pendingRemoval" attribute set.
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ nsCString pendingRemoval;
+ nsresult rv =
+ m_db->GetMsgHdrForKey(m_keys[m_curIndex], getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ hdr->GetStringProperty("pendingRemoval", pendingRemoval);
+ if (!pendingRemoval.IsEmpty()) {
+ m_curIndex++;
+ // Turn off offline flag for message, since after the compact is
+ // completed; we won't have the message in the offline store.
+ uint32_t resultFlags;
+ hdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags);
+ // We need to clear this in case the user changes the offline retention
+ // settings.
+ hdr->SetStringProperty("pendingRemoval", ""_ns);
+ continue;
+ }
+ m_messageUri.Truncate(); // clear the previous message uri
+ rv = BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex],
+ m_messageUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_startOfMsg = true;
+ nsCOMPtr<nsISupports> thisSupports;
+ QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(thisSupports));
+ nsCOMPtr<nsIURI> dummyNull;
+ rv = m_messageService->StreamMessage(m_messageUri, thisSupports, m_window,
+ nullptr, false, EmptyCString(), true,
+ getter_AddRefs(dummyNull));
+ // if copy fails, we clear the offline flag on the source message.
+ if (NS_FAILED(rv)) {
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ m_messageService->MessageURIToMsgHdr(m_messageUri, getter_AddRefs(hdr));
+ if (hdr) {
+ uint32_t resultFlags;
+ hdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags);
+ }
+ m_curIndex++;
+ continue;
+ } else
+ break;
+ }
+ done = m_curIndex >= m_keys.Length();
+ // In theory, we might be able to stream the next message, so
+ // return NS_OK, not rv.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsOfflineStoreCompactState::OnStopRequest(nsIRequest* request,
+ nsresult status) {
+ nsresult rv = status;
+ nsCOMPtr<nsIURI> uri;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ nsCOMPtr<nsIChannel> channel;
+ bool done = false;
+
+ // The NS_MSG_ERROR_MSG_NOT_OFFLINE error should allow us to continue, so we
+ // check for it specifically and don't terminate the compaction.
+ if (NS_FAILED(rv) && rv != NS_MSG_ERROR_MSG_NOT_OFFLINE) goto done;
+
+ // We know the request is an nsIChannel we can get a URI from, but this is
+ // probably bad form. See Bug 1528662.
+ channel = do_QueryInterface(request, &rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error QI nsIRequest to nsIChannel failed");
+ if (NS_FAILED(rv)) goto done;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ if (NS_FAILED(rv)) goto done;
+ rv = m_messageService->MessageURIToMsgHdr(m_messageUri,
+ getter_AddRefs(msgHdr));
+ if (NS_FAILED(rv)) goto done;
+
+ // This is however an unexpected condition, so let's print a warning.
+ if (rv == NS_MSG_ERROR_MSG_NOT_OFFLINE) {
+ nsAutoCString spec;
+ uri->GetSpec(spec);
+ nsPrintfCString msg("Message expectedly not available offline: %s",
+ spec.get());
+ NS_WARNING(msg.get());
+ }
+
+ if (msgHdr) {
+ if (NS_SUCCEEDED(status)) {
+ msgHdr->SetMessageOffset(m_startOfNewMsg);
+ nsCString storeToken = nsPrintfCString("%" PRIu64, m_startOfNewMsg);
+ msgHdr->SetStringProperty("storeToken", storeToken);
+ msgHdr->SetOfflineMessageSize(m_offlineMsgSize);
+ } else {
+ uint32_t resultFlags;
+ msgHdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags);
+ }
+ }
+
+ if (m_window) {
+ m_window->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (statusFeedback)
+ statusFeedback->ShowProgress(100 * m_curIndex / m_keys.Length());
+ }
+ // advance to next message
+ m_curIndex++;
+ rv = CopyNextMessage(done);
+ if (done) {
+ m_db->Commit(nsMsgDBCommitType::kCompressCommit);
+ msgHdr = nullptr;
+ // no more to copy finish it up
+ ReleaseFolderLock();
+ FinishCompact();
+ NS_RELEASE_THIS(); // kill self
+ }
+
+done:
+ if (NS_FAILED(rv)) {
+ m_status = rv; // set the status to rv so the destructor can remove the
+ // temp folder and database
+ ReleaseFolderLock();
+ NS_RELEASE_THIS(); // kill self
+
+ if (m_completionFn) {
+ m_completionFn(m_status, m_totalExpungedBytes);
+ }
+ return rv;
+ }
+ return rv;
+}
+
+nsresult nsOfflineStoreCompactState::FinishCompact() {
+ // All okay time to finish up the compact process
+ nsCOMPtr<nsIFile> path;
+ uint32_t flags;
+
+ // get leaf name and database name of the folder
+ m_folder->GetFlags(&flags);
+ nsresult rv = m_folder->GetFilePath(getter_AddRefs(path));
+
+ nsCString leafName;
+ path->GetNativeLeafName(leafName);
+
+ if (m_fileStream) {
+ // close down the temp file stream; preparing for deleting the old folder
+ // and its database; then rename the temp folder and database
+ m_fileStream->Flush();
+ m_fileStream->Close();
+ m_fileStream = nullptr;
+ }
+
+ // make sure the new database is valid
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) dbFolderInfo->SetExpungedBytes(0);
+ // this forces the m_folder to update mExpungedBytes from the db folder info.
+ int64_t expungedBytes;
+ m_folder->GetExpungedBytes(&expungedBytes);
+ m_folder->UpdateSummaryTotals(true);
+ m_db->SetSummaryValid(true);
+
+ // remove the old folder
+ path->Remove(false);
+
+ // rename the copied folder to be the original folder
+ m_file->MoveToNative((nsIFile*)nullptr, leafName);
+
+ ShowStatusMsg(EmptyString());
+ m_folder->NotifyCompactCompleted();
+ if (m_completionFn) {
+ m_completionFn(NS_OK, m_totalExpungedBytes);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::Init(nsICopyMessageListener* destination) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::StartMessage() {
+ nsresult rv = NS_ERROR_FAILURE;
+ NS_ASSERTION(m_fileStream, "Fatal, null m_fileStream...");
+ if (m_fileStream) {
+ nsCOMPtr<nsISeekableStream> seekableStream =
+ do_QueryInterface(m_fileStream, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // this will force an internal flush, but not a sync. Tell should really do
+ // an internal flush, but it doesn't, and I'm afraid to change that
+ // nsIFileStream.cpp code anymore.
+ seekableStream->Seek(nsISeekableStream::NS_SEEK_CUR, 0);
+ // record the new message key for the message
+ int64_t curStreamPos;
+ seekableStream->Tell(&curStreamPos);
+ m_startOfNewMsg = curStreamPos;
+ rv = NS_OK;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsFolderCompactState::EndMessage(nsMsgKey key) { return NS_OK; }
+
+// XXX TODO: This function is sadly lacking all status checking, it always
+// returns NS_OK and moves onto the next message.
+NS_IMETHODIMP
+nsFolderCompactState::EndCopy(nsIURI* uri, nsresult status) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgDBHdr> newMsgHdr;
+
+ if (m_curIndex >= m_keys.Length()) {
+ NS_ASSERTION(false, "m_curIndex out of bounds");
+ return NS_OK;
+ }
+
+ // Take note of the end offset of the message (without the trailing blank
+ // line).
+ nsCOMPtr<nsITellableStream> tellable(do_QueryInterface(m_fileStream));
+ MOZ_ASSERT(tellable);
+ int64_t endOfMsg;
+ nsresult rv = tellable->Tell(&endOfMsg);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /* Messages need to have trailing blank lines */
+ rv = WriteSpan(m_fileStream, m_eolSeq);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ /*
+ * Done with the current message; copying the existing message header
+ * to the new database.
+ */
+ if (m_curSrcHdr) {
+ nsMsgKey key;
+ m_curSrcHdr->GetMessageKey(&key);
+ m_db->CopyHdrFromExistingHdr(key, m_curSrcHdr, true,
+ getter_AddRefs(newMsgHdr));
+ }
+ m_curSrcHdr = nullptr;
+ if (newMsgHdr) {
+ nsCString storeToken = nsPrintfCString("%" PRIu64, m_startOfNewMsg);
+ newMsgHdr->SetStringProperty("storeToken", storeToken);
+ newMsgHdr->SetMessageOffset(m_startOfNewMsg);
+ uint64_t msgSize = endOfMsg - m_startOfNewMsg;
+ newMsgHdr->SetMessageSize(msgSize);
+
+ m_totalMsgSize += msgSize + m_eolSeq.Length();
+ }
+
+ // m_db->Commit(nsMsgDBCommitType::kLargeCommit); // no sense committing
+ // until the end
+ // advance to next message
+ m_curIndex++;
+ m_startOfMsg = true;
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ if (m_window) {
+ m_window->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (statusFeedback)
+ statusFeedback->ShowProgress(100 * m_curIndex / m_keys.Length());
+ }
+ return NS_OK;
+}
+
+nsresult nsOfflineStoreCompactState::StartCompacting() {
+ nsresult rv = NS_OK;
+ if (m_keys.Length() > 0 && m_curIndex == 0) {
+ NS_ADDREF_THIS(); // we own ourselves, until we're done, anyway.
+ ShowCompactingStatusMsg();
+ bool done = false;
+ rv = CopyNextMessage(done);
+ if (!done) return rv;
+ }
+ ReleaseFolderLock();
+ FinishCompact();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsOfflineStoreCompactState::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset,
+ uint32_t count) {
+ if (!m_fileStream || !inStr) return NS_ERROR_FAILURE;
+
+ nsresult rv = NS_OK;
+
+ if (m_startOfMsg) {
+ m_offlineMsgSize = 0;
+ m_messageUri.Truncate(); // clear the previous message uri
+ if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex],
+ m_messageUri))) {
+ rv = m_messageService->MessageURIToMsgHdr(m_messageUri,
+ getter_AddRefs(m_curSrcHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ uint32_t maxReadCount, readCount, writeCount;
+ uint32_t bytesWritten;
+
+ while (NS_SUCCEEDED(rv) && (int32_t)count > 0) {
+ maxReadCount =
+ count > sizeof(m_dataBuffer) - 1 ? sizeof(m_dataBuffer) - 1 : count;
+ writeCount = 0;
+ rv = inStr->Read(m_dataBuffer, maxReadCount, &readCount);
+
+ if (NS_SUCCEEDED(rv)) {
+ if (m_startOfMsg) {
+ m_startOfMsg = false;
+ // check if there's an envelope header; if not, write one.
+ if (strncmp(m_dataBuffer, "From ", 5)) {
+ m_fileStream->Write("From " CRLF, 7, &bytesWritten);
+ m_offlineMsgSize += bytesWritten;
+ }
+ }
+ m_fileStream->Write(m_dataBuffer, readCount, &bytesWritten);
+ m_offlineMsgSize += bytesWritten;
+ writeCount += bytesWritten;
+ count -= readCount;
+ if (writeCount != readCount) {
+ m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window);
+ return NS_MSG_ERROR_WRITING_MAIL_FOLDER;
+ }
+ }
+ }
+ return rv;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// nsMsgFolderCompactor implementation
+//////////////////////////////////////////////////////////////////////////////
+
+NS_IMPL_ISUPPORTS(nsMsgFolderCompactor, nsIMsgFolderCompactor)
+
+nsMsgFolderCompactor::nsMsgFolderCompactor() {}
+
+nsMsgFolderCompactor::~nsMsgFolderCompactor() {}
+
+NS_IMETHODIMP nsMsgFolderCompactor::CompactFolders(
+ const nsTArray<RefPtr<nsIMsgFolder>>& folders, nsIUrlListener* listener,
+ nsIMsgWindow* window) {
+ MOZ_ASSERT(mQueue.IsEmpty());
+ mWindow = window;
+ mListener = listener;
+ mTotalBytesGained = 0;
+ mQueue = folders.Clone();
+ mQueue.Reverse();
+
+ // Can't guarantee that anyone will keep us in scope until we're done, so...
+ MOZ_ASSERT(!mKungFuDeathGrip);
+ mKungFuDeathGrip = this;
+
+ // nsIMsgFolderCompactor idl states this isn't called...
+ // but maybe it should be?
+ // if (mListener) {
+ // mListener->OnStartRunningUrl(nullptr);
+ // }
+
+ NextFolder();
+
+ return NS_OK;
+}
+
+void nsMsgFolderCompactor::NextFolder() {
+ while (!mQueue.IsEmpty()) {
+ // Should only ever have one compactor running.
+ MOZ_ASSERT(mCompactor == nullptr);
+
+ nsCOMPtr<nsIMsgFolder> folder = mQueue.PopLastElement();
+
+ // Sanity check - should we be compacting this folder?
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ nsresult rv = folder->GetMsgStore(getter_AddRefs(msgStore));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Skipping folder with no msgStore");
+ continue;
+ }
+ bool storeSupportsCompaction;
+ msgStore->GetSupportsCompaction(&storeSupportsCompaction);
+ if (!storeSupportsCompaction) {
+ NS_WARNING("Trying to compact a non-mbox folder");
+ continue; // just skip it.
+ }
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(folder));
+ if (imapFolder) {
+ uint32_t flags;
+ folder->GetFlags(&flags);
+ if (flags & nsMsgFolderFlags::Offline) {
+ mCompactor = new nsOfflineStoreCompactState();
+ }
+ } else {
+ mCompactor = new nsFolderCompactState();
+ }
+ if (!mCompactor) {
+ NS_WARNING("skipping compact of non-offline folder");
+ continue;
+ }
+ nsCString uri;
+ folder->GetURI(uri);
+
+ // Callback for when a folder compaction completes.
+ auto completionFn = [self = RefPtr<nsMsgFolderCompactor>(this),
+ compactState = mCompactor](nsresult status,
+ uint64_t expungedBytes) {
+ if (NS_SUCCEEDED(status)) {
+ self->mTotalBytesGained += expungedBytes;
+ } else {
+ // Failed. We want to keep going with the next folder, but make sure
+ // we return a failing code upon overall completion.
+ self->mOverallStatus = status;
+ NS_WARNING("folder compact failed.");
+ }
+
+ // Release our lock on the compactor - it's done.
+ self->mCompactor = nullptr;
+ self->NextFolder();
+ };
+
+ rv = mCompactor->Compact(folder, completionFn, mWindow);
+ if (NS_SUCCEEDED(rv)) {
+ // Now wait for the compactor to let us know it's finished,
+ // via the completion callback fn.
+ return;
+ }
+ mOverallStatus = rv;
+ mCompactor = nullptr;
+ NS_WARNING("folder compact failed - skipping folder");
+ }
+
+ // Done. No more folders to compact.
+
+ if (mListener) {
+ // If there were multiple failures, this will communicate only the
+ // last one, but that's OK. Main thing is to indicate that _something_
+ // went wrong.
+ mListener->OnStopRunningUrl(nullptr, mOverallStatus);
+ }
+ ShowDoneStatus();
+
+ // We're not needed any more.
+ mKungFuDeathGrip = nullptr;
+ return;
+}
+
+void nsMsgFolderCompactor::ShowDoneStatus() {
+ if (!mWindow) {
+ return;
+ }
+ nsCOMPtr<nsIStringBundle> bundle;
+ nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS_VOID(rv);
+ nsAutoString expungedAmount;
+ FormatFileSize(mTotalBytesGained, true, expungedAmount);
+ AutoTArray<nsString, 1> params = {expungedAmount};
+ nsString msg;
+ rv = bundle->FormatStringFromName("compactingDone", params, msg);
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ mWindow->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (statusFeedback) {
+ statusFeedback->SetStatusString(msg);
+ }
+}
diff --git a/comm/mailnews/base/src/nsMsgFolderCompactor.h b/comm/mailnews/base/src/nsMsgFolderCompactor.h
new file mode 100644
index 0000000000..b9de16be12
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderCompactor.h
@@ -0,0 +1,48 @@
+/* -*- 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/. */
+
+#ifndef _nsMsgFolderCompactor_h
+#define _nsMsgFolderCompactor_h
+
+#include "nsIMsgFolderCompactor.h"
+
+class nsIMsgFolder;
+class nsIMsgWindow;
+class nsFolderCompactState;
+
+/**
+ * nsMsgFolderCompactor implements nsIMsgFolderCompactor, which allows the
+ * caller to kick off a batch of folder compactions (via compactFolders()).
+ */
+class nsMsgFolderCompactor : public nsIMsgFolderCompactor {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFOLDERCOMPACTOR
+
+ nsMsgFolderCompactor();
+
+ protected:
+ virtual ~nsMsgFolderCompactor();
+ // The folders waiting to be compacted.
+ nsTArray<RefPtr<nsIMsgFolder>> mQueue;
+
+ // If any individual folders fail to compact, we stash the latest fail code
+ // here (to return via listener upon overall completion).
+ nsresult mOverallStatus{NS_OK};
+
+ // If set, OnStopRunningUrl() will be called when all folders done.
+ nsCOMPtr<nsIUrlListener> mListener;
+ // If set, progress status updates will be sent here.
+ nsCOMPtr<nsIMsgWindow> mWindow;
+ RefPtr<nsMsgFolderCompactor> mKungFuDeathGrip;
+ uint64_t mTotalBytesGained{0};
+
+ // The currently-running compactor.
+ RefPtr<nsFolderCompactState> mCompactor;
+
+ void NextFolder();
+ void ShowDoneStatus();
+};
+#endif
diff --git a/comm/mailnews/base/src/nsMsgFolderNotificationService.cpp b/comm/mailnews/base/src/nsMsgFolderNotificationService.cpp
new file mode 100644
index 0000000000..7983a8ec56
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderNotificationService.cpp
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgFolderNotificationService.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsIImapIncomingServer.h"
+
+//
+// nsMsgFolderNotificationService
+//
+NS_IMPL_ISUPPORTS(nsMsgFolderNotificationService,
+ nsIMsgFolderNotificationService)
+
+nsMsgFolderNotificationService::nsMsgFolderNotificationService() {}
+
+nsMsgFolderNotificationService::~nsMsgFolderNotificationService() {
+ /* destructor code */
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::GetHasListeners(
+ bool* aHasListeners) {
+ NS_ENSURE_ARG_POINTER(aHasListeners);
+ *aHasListeners = mListeners.Length() > 0;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::AddListener(
+ nsIMsgFolderListener* aListener, msgFolderListenerFlag aFlags) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ MsgFolderListener listener(aListener, aFlags);
+ mListeners.AppendElementUnlessExists(listener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::RemoveListener(
+ nsIMsgFolderListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ mListeners.RemoveElement(aListener);
+ return NS_OK;
+}
+
+#define NOTIFY_MSGFOLDER_LISTENERS(propertyflag_, propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<MsgFolderListener>::ForwardIterator iter(mListeners); \
+ while (iter.HasMore()) { \
+ const MsgFolderListener& listener = iter.GetNext(); \
+ if (listener.mFlags & propertyflag_) \
+ listener.mListener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgAdded(
+ nsIMsgDBHdr* aMsg) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgAdded, MsgAdded, (aMsg));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsClassified(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgs, bool aJunkProcessed,
+ bool aTraitProcessed) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgsClassified, MsgsClassified,
+ (aMsgs, aJunkProcessed, aTraitProcessed));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsJunkStatusChanged(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& messages) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgsJunkStatusChanged, MsgsJunkStatusChanged,
+ (messages));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsDeleted(
+ const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgs) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgsDeleted, MsgsDeleted, (aMsgs));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsMoveCopyCompleted(
+ bool aMove, const nsTArray<RefPtr<nsIMsgDBHdr>>& aSrcMsgs,
+ nsIMsgFolder* aDestFolder, const nsTArray<RefPtr<nsIMsgDBHdr>>& aDestMsgs) {
+ // IMAP delete model means that a "move" isn't really a move, it is a copy,
+ // followed by storing the IMAP deleted flag on the message.
+ bool isReallyMove = aMove;
+ if (aMove && !mListeners.IsEmpty() && !aSrcMsgs.IsEmpty()) {
+ nsresult rv;
+ // Assume that all the source messages are from the same server.
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = aSrcMsgs[0]->GetFolder(getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(msgFolder));
+ if (imapFolder) {
+ nsCOMPtr<nsIImapIncomingServer> imapServer;
+ imapFolder->GetImapIncomingServer(getter_AddRefs(imapServer));
+ if (imapServer) {
+ nsMsgImapDeleteModel deleteModel;
+ imapServer->GetDeleteModel(&deleteModel);
+ if (deleteModel == nsMsgImapDeleteModels::IMAPDelete)
+ isReallyMove = false;
+ }
+ }
+ }
+
+ NOTIFY_MSGFOLDER_LISTENERS(msgsMoveCopyCompleted, MsgsMoveCopyCompleted,
+ (isReallyMove, aSrcMsgs, aDestFolder, aDestMsgs));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFolderNotificationService::NotifyMsgKeyChanged(nsMsgKey aOldKey,
+ nsIMsgDBHdr* aNewHdr) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgKeyChanged, MsgKeyChanged, (aOldKey, aNewHdr));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgFolderNotificationService::NotifyMsgUnincorporatedMoved(
+ nsIMsgFolder* srcFolder, nsIMsgDBHdr* msg) {
+ NOTIFY_MSGFOLDER_LISTENERS(msgUnincorporatedMoved, MsgUnincorporatedMoved,
+ (srcFolder, msg));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderAdded(
+ nsIMsgFolder* aFolder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderAdded, FolderAdded, (aFolder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderDeleted(
+ nsIMsgFolder* aFolder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderDeleted, FolderDeleted, (aFolder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderMoveCopyCompleted(
+ bool aMove, nsIMsgFolder* aSrcFolder, nsIMsgFolder* aDestFolder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderMoveCopyCompleted, FolderMoveCopyCompleted,
+ (aMove, aSrcFolder, aDestFolder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderRenamed(
+ nsIMsgFolder* aOrigFolder, nsIMsgFolder* aNewFolder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderRenamed, FolderRenamed,
+ (aOrigFolder, aNewFolder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderCompactStart(
+ nsIMsgFolder* folder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderCompactStart, FolderCompactStart, (folder));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderCompactFinish(
+ nsIMsgFolder* folder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderCompactFinish, FolderCompactFinish,
+ (folder));
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderReindexTriggered(
+ nsIMsgFolder* folder) {
+ NOTIFY_MSGFOLDER_LISTENERS(folderReindexTriggered, FolderReindexTriggered,
+ (folder));
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgFolderNotificationService.h b/comm/mailnews/base/src/nsMsgFolderNotificationService.h
new file mode 100644
index 0000000000..fecf3b3064
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgFolderNotificationService.h
@@ -0,0 +1,46 @@
+/* -*- 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/. */
+
+#ifndef nsMsgFolderNotificationService_h__
+#define nsMsgFolderNotificationService_h__
+
+#include "nsIMsgFolderNotificationService.h"
+#include "nsIMsgFolderListener.h"
+#include "nsTObserverArray.h"
+#include "nsCOMPtr.h"
+
+class nsMsgFolderNotificationService final
+ : public nsIMsgFolderNotificationService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGFOLDERNOTIFICATIONSERVICE
+
+ nsMsgFolderNotificationService();
+
+ private:
+ ~nsMsgFolderNotificationService();
+ struct MsgFolderListener {
+ nsCOMPtr<nsIMsgFolderListener> mListener;
+ msgFolderListenerFlag mFlags;
+
+ MsgFolderListener(nsIMsgFolderListener* aListener,
+ msgFolderListenerFlag aFlags)
+ : mListener(aListener), mFlags(aFlags) {}
+ MsgFolderListener(const MsgFolderListener& aListener)
+ : mListener(aListener.mListener), mFlags(aListener.mFlags) {}
+ ~MsgFolderListener() {}
+
+ int operator==(nsIMsgFolderListener* aListener) const {
+ return mListener == aListener;
+ }
+ int operator==(const MsgFolderListener& aListener) const {
+ return mListener == aListener.mListener;
+ }
+ };
+
+ nsTObserverArray<MsgFolderListener> mListeners;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgGroupThread.cpp b/comm/mailnews/base/src/nsMsgGroupThread.cpp
new file mode 100644
index 0000000000..32a132d27a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgGroupThread.cpp
@@ -0,0 +1,731 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgGroupThread.h"
+#include "nsMsgDBView.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgUtils.h"
+#include "nsSimpleEnumerator.h"
+
+NS_IMPL_ISUPPORTS(nsMsgGroupThread, nsIMsgThread)
+
+nsMsgGroupThread::nsMsgGroupThread() { Init(); }
+
+nsMsgGroupThread::nsMsgGroupThread(nsIMsgDatabase* db) {
+ m_db = db;
+ Init();
+}
+
+void nsMsgGroupThread::Init() {
+ m_threadKey = nsMsgKey_None;
+ m_threadRootKey = nsMsgKey_None;
+ m_numUnreadChildren = 0;
+ m_flags = 0;
+ m_newestMsgDate = 0;
+ m_dummy = false;
+}
+
+nsMsgGroupThread::~nsMsgGroupThread() {}
+
+NS_IMETHODIMP nsMsgGroupThread::SetThreadKey(nsMsgKey threadKey) {
+ m_threadKey = threadKey;
+ // by definition, the initial thread key is also the thread root key.
+ m_threadRootKey = threadKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetThreadKey(nsMsgKey* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_threadKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetFlags(uint32_t* aFlags) {
+ NS_ENSURE_ARG_POINTER(aFlags);
+ *aFlags = m_flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::SetFlags(uint32_t aFlags) {
+ m_flags = aFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::SetSubject(const nsACString& aSubject) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetSubject(nsACString& result) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetNumChildren(uint32_t* aNumChildren) {
+ NS_ENSURE_ARG_POINTER(aNumChildren);
+ *aNumChildren = m_keys.Length(); // - ((m_dummy) ? 1 : 0);
+ return NS_OK;
+}
+
+uint32_t nsMsgGroupThread::NumRealChildren() {
+ return m_keys.Length() - ((m_dummy) ? 1 : 0);
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetNumUnreadChildren(
+ uint32_t* aNumUnreadChildren) {
+ NS_ENSURE_ARG_POINTER(aNumUnreadChildren);
+ *aNumUnreadChildren = m_numUnreadChildren;
+ return NS_OK;
+}
+
+void nsMsgGroupThread::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ m_keys.InsertElementAt(index, msgKey);
+}
+
+void nsMsgGroupThread::SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ m_keys[index] = msgKey;
+}
+
+nsMsgViewIndex nsMsgGroupThread::FindMsgHdr(nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ return (nsMsgViewIndex)m_keys.IndexOf(msgKey);
+}
+
+NS_IMETHODIMP nsMsgGroupThread::AddChild(nsIMsgDBHdr* child,
+ nsIMsgDBHdr* inReplyTo,
+ bool threadInThread,
+ nsIDBChangeAnnouncer* announcer) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsMsgViewIndex nsMsgGroupThread::AddMsgHdrInDateOrder(nsIMsgDBHdr* child,
+ nsMsgDBView* view) {
+ nsMsgKey newHdrKey;
+ child->GetMessageKey(&newHdrKey);
+ uint32_t insertIndex = 0;
+ // since we're sorted by date, we could do a binary search for the
+ // insert point. Or, we could start at the end...
+ if (m_keys.Length() > 0) {
+ nsMsgViewSortTypeValue sortType;
+ nsMsgViewSortOrderValue sortOrder;
+ (void)view->GetSortType(&sortType);
+ (void)view->GetSortOrder(&sortOrder);
+ // historical behavior is ascending date order unless our primary sort is
+ // on date
+ nsMsgViewSortOrderValue threadSortOrder =
+ (sortType == nsMsgViewSortType::byDate &&
+ sortOrder == nsMsgViewSortOrder::descending)
+ ? nsMsgViewSortOrder::descending
+ : nsMsgViewSortOrder::ascending;
+ // new behavior is tricky and uses the secondary sort order if the secondary
+ // sort is on the date
+ nsMsgViewSortTypeValue secondarySortType;
+ nsMsgViewSortOrderValue secondarySortOrder;
+ (void)view->GetSecondarySortType(&secondarySortType);
+ (void)view->GetSecondarySortOrder(&secondarySortOrder);
+ if (secondarySortType == nsMsgViewSortType::byDate)
+ threadSortOrder = secondarySortOrder;
+ // sort by date within group.
+ insertIndex = GetInsertIndexFromView(view, child, threadSortOrder);
+ }
+ m_keys.InsertElementAt(insertIndex, newHdrKey);
+ if (!insertIndex) m_threadRootKey = newHdrKey;
+ return insertIndex;
+}
+
+nsMsgViewIndex nsMsgGroupThread::GetInsertIndexFromView(
+ nsMsgDBView* view, nsIMsgDBHdr* child,
+ nsMsgViewSortOrderValue threadSortOrder) {
+ return view->GetInsertIndexHelper(child, m_keys, nullptr, threadSortOrder,
+ nsMsgViewSortType::byDate);
+}
+
+nsMsgViewIndex nsMsgGroupThread::AddChildFromGroupView(nsIMsgDBHdr* child,
+ nsMsgDBView* view) {
+ uint32_t newHdrFlags = 0;
+ uint32_t msgDate;
+ nsMsgKey newHdrKey = 0;
+
+ child->GetFlags(&newHdrFlags);
+ child->GetMessageKey(&newHdrKey);
+ child->GetDateInSeconds(&msgDate);
+ if (msgDate > m_newestMsgDate) SetNewestMsgDate(msgDate);
+
+ child->AndFlags(~(nsMsgMessageFlags::Watched), &newHdrFlags);
+ uint32_t numChildren;
+
+ // get the num children before we add the new header.
+ GetNumChildren(&numChildren);
+
+ // if this is an empty thread, set the root key to this header's key
+ if (numChildren == 0) m_threadRootKey = newHdrKey;
+
+ if (!(newHdrFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(1);
+
+ return AddMsgHdrInDateOrder(child, view);
+}
+
+nsresult nsMsgGroupThread::ReparentNonReferenceChildrenOf(
+ nsIMsgDBHdr* topLevelHdr, nsMsgKey newParentKey,
+ nsIDBChangeAnnouncer* announcer) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetChildKeyAt(uint32_t aIndex,
+ nsMsgKey* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (aIndex >= m_keys.Length()) return NS_ERROR_INVALID_ARG;
+ *aResult = m_keys[aIndex];
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetChildHdrAt(uint32_t aIndex,
+ nsIMsgDBHdr** aResult) {
+ if (aIndex >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+ return m_db->GetMsgHdrForKey(m_keys[aIndex], aResult);
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetChild(nsMsgKey msgKey,
+ nsIMsgDBHdr** aResult) {
+ return GetChildHdrAt(m_keys.IndexOf(msgKey), aResult);
+}
+
+NS_IMETHODIMP nsMsgGroupThread::RemoveChildAt(uint32_t aIndex) {
+ NS_ENSURE_TRUE(aIndex < m_keys.Length(), NS_MSG_MESSAGE_NOT_FOUND);
+
+ m_keys.RemoveElementAt(aIndex);
+ return NS_OK;
+}
+
+nsresult nsMsgGroupThread::RemoveChild(nsMsgKey msgKey) {
+ m_keys.RemoveElement(msgKey);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::RemoveChildHdr(
+ nsIMsgDBHdr* child, nsIDBChangeAnnouncer* announcer) {
+ NS_ENSURE_ARG_POINTER(child);
+
+ uint32_t flags;
+ nsMsgKey key;
+
+ child->GetFlags(&flags);
+ child->GetMessageKey(&key);
+
+ // if this was the newest msg, clear the newest msg date so we'll recalc.
+ uint32_t date;
+ child->GetDateInSeconds(&date);
+ if (date == m_newestMsgDate) SetNewestMsgDate(0);
+
+ if (!(flags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(-1);
+ nsMsgViewIndex threadIndex = FindMsgHdr(child);
+ bool wasFirstChild = threadIndex == 0;
+ nsresult rv = RemoveChildAt(threadIndex);
+ // if we're deleting the root of a dummy thread, need to update the threadKey
+ // and the dummy header at position 0
+ if (m_dummy && wasFirstChild && m_keys.Length() > 1) {
+ nsIMsgDBHdr* newRootChild;
+ rv = GetChildHdrAt(1, &newRootChild);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SetMsgHdrAt(0, newRootChild);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgGroupThread::ReparentChildrenOf(nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeAnnouncer* announcer) {
+ nsresult rv = NS_OK;
+
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ if (numChildren > 0) {
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(curHdr));
+ if (NS_SUCCEEDED(rv) && curHdr) {
+ nsMsgKey threadParent;
+
+ curHdr->GetThreadParent(&threadParent);
+ if (threadParent == oldParent) {
+ nsMsgKey curKey;
+
+ curHdr->SetThreadParent(newParent);
+ curHdr->GetMessageKey(&curKey);
+ if (announcer)
+ announcer->NotifyParentChangedAll(curKey, oldParent, newParent,
+ nullptr);
+ // if the old parent was the root of the thread, then only the first
+ // child gets promoted to root, and other children become children of
+ // the new root.
+ if (newParent == nsMsgKey_None) {
+ m_threadRootKey = curKey;
+ newParent = curKey;
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::MarkChildRead(bool bRead) {
+ ChangeUnreadChildCount(bRead ? -1 : 1);
+ return NS_OK;
+}
+
+// this could be moved into utils, because I think it's the same as the db impl.
+class nsMsgGroupThreadEnumerator : public nsBaseMsgEnumerator {
+ public:
+ // nsIMsgEnumerator support.
+ NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override;
+ NS_IMETHOD HasMoreElements(bool* aResult) override;
+
+ // nsMsgGroupThreadEnumerator methods:
+ typedef nsresult (*nsMsgGroupThreadEnumeratorFilter)(nsIMsgDBHdr* hdr,
+ void* closure);
+
+ nsMsgGroupThreadEnumerator(nsMsgGroupThread* thread, nsMsgKey startKey,
+ nsMsgGroupThreadEnumeratorFilter filter,
+ void* closure);
+ int32_t MsgKeyFirstChildIndex(nsMsgKey inMsgKey);
+
+ protected:
+ virtual ~nsMsgGroupThreadEnumerator();
+
+ nsresult Prefetch();
+
+ nsCOMPtr<nsIMsgDBHdr> mResultHdr;
+ RefPtr<nsMsgGroupThread> mThread;
+ nsMsgKey mThreadParentKey;
+ nsMsgKey mFirstMsgKey;
+ int32_t mChildIndex;
+ bool mDone;
+ bool mNeedToPrefetch;
+ nsMsgGroupThreadEnumeratorFilter mFilter;
+ void* mClosure;
+ bool mFoundChildren;
+};
+
+nsMsgGroupThreadEnumerator::nsMsgGroupThreadEnumerator(
+ nsMsgGroupThread* thread, nsMsgKey startKey,
+ nsMsgGroupThreadEnumeratorFilter filter, void* closure)
+ : mDone(false), mFilter(filter), mClosure(closure), mFoundChildren(false) {
+ mThreadParentKey = startKey;
+ mChildIndex = 0;
+ mThread = thread;
+ mNeedToPrefetch = true;
+ mFirstMsgKey = nsMsgKey_None;
+
+ nsresult rv = mThread->GetRootHdr(getter_AddRefs(mResultHdr));
+ if (NS_SUCCEEDED(rv) && mResultHdr) mResultHdr->GetMessageKey(&mFirstMsgKey);
+
+ uint32_t numChildren;
+ mThread->GetNumChildren(&numChildren);
+
+ if (mThreadParentKey != nsMsgKey_None) {
+ nsMsgKey msgKey = nsMsgKey_None;
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = mThread->GetChildHdrAt(childIndex, getter_AddRefs(mResultHdr));
+ if (NS_SUCCEEDED(rv) && mResultHdr) {
+ mResultHdr->GetMessageKey(&msgKey);
+ if (msgKey == startKey) {
+ mChildIndex = MsgKeyFirstChildIndex(msgKey);
+ mDone = (mChildIndex < 0);
+ break;
+ }
+
+ if (mDone) break;
+ } else
+ NS_ASSERTION(false, "couldn't get child from thread");
+ }
+ }
+
+#ifdef DEBUG_bienvenu1
+ nsCOMPtr<nsIMsgDBHdr> child;
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ rv = mThread->GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey threadParent;
+ nsMsgKey msgKey;
+ // we're only doing one level of threading, so check if caller is
+ // asking for children of the first message in the thread or not.
+ // if not, we will tell him there are no children.
+ child->GetMessageKey(&msgKey);
+ child->GetThreadParent(&threadParent);
+
+ printf("index = %ld key = %ld parent = %lx\n", childIndex, msgKey,
+ threadParent);
+ }
+ }
+#endif
+}
+
+nsMsgGroupThreadEnumerator::~nsMsgGroupThreadEnumerator() {}
+
+int32_t nsMsgGroupThreadEnumerator::MsgKeyFirstChildIndex(nsMsgKey inMsgKey) {
+ // look through rest of thread looking for a child of this message.
+ // If the inMsgKey is the first message in the thread, then all children
+ // without parents are considered to be children of inMsgKey.
+ // Otherwise, only true children qualify.
+ uint32_t numChildren;
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ int32_t firstChildIndex = -1;
+
+ mThread->GetNumChildren(&numChildren);
+
+ for (uint32_t curChildIndex = 0; curChildIndex < numChildren;
+ curChildIndex++) {
+ nsresult rv = mThread->GetChildHdrAt(curChildIndex, getter_AddRefs(curHdr));
+ if (NS_SUCCEEDED(rv) && curHdr) {
+ nsMsgKey parentKey;
+
+ curHdr->GetThreadParent(&parentKey);
+ if (parentKey == inMsgKey) {
+ firstChildIndex = curChildIndex;
+ break;
+ }
+ }
+ }
+#ifdef DEBUG_bienvenu1
+ printf("first child index of %ld = %ld\n", inMsgKey, firstChildIndex);
+#endif
+ return firstChildIndex;
+}
+
+NS_IMETHODIMP nsMsgGroupThreadEnumerator::GetNext(nsIMsgDBHdr** aItem) {
+ NS_ENSURE_ARG_POINTER(aItem);
+ nsresult rv = NS_OK;
+
+ if (mNeedToPrefetch) rv = Prefetch();
+
+ if (NS_SUCCEEDED(rv) && mResultHdr) {
+ NS_ADDREF(*aItem = mResultHdr);
+ mNeedToPrefetch = true;
+ }
+ return rv;
+}
+
+nsresult nsMsgGroupThreadEnumerator::Prefetch() {
+ nsresult rv = NS_OK; // XXX or should this default to an error?
+ mResultHdr = nullptr;
+ if (mThreadParentKey == nsMsgKey_None) {
+ rv = mThread->GetRootHdr(getter_AddRefs(mResultHdr));
+ NS_ASSERTION(NS_SUCCEEDED(rv) && mResultHdr,
+ "better be able to get root hdr");
+ mChildIndex = 0; // since root can be anywhere, set mChildIndex to 0.
+ } else if (!mDone) {
+ uint32_t numChildren;
+ mThread->GetNumChildren(&numChildren);
+
+ while ((uint32_t)mChildIndex < numChildren) {
+ rv = mThread->GetChildHdrAt(mChildIndex++, getter_AddRefs(mResultHdr));
+ if (NS_SUCCEEDED(rv) && mResultHdr) {
+ nsMsgKey parentKey;
+ nsMsgKey curKey;
+
+ if (mFilter && NS_FAILED(mFilter(mResultHdr, mClosure))) {
+ mResultHdr = nullptr;
+ continue;
+ }
+
+ mResultHdr->GetThreadParent(&parentKey);
+ mResultHdr->GetMessageKey(&curKey);
+ // if the parent is the same as the msg we're enumerating over,
+ // or the parentKey isn't set, and we're iterating over the top
+ // level message in the thread, then leave mResultHdr set to cur msg.
+ if (parentKey == mThreadParentKey ||
+ (parentKey == nsMsgKey_None && mThreadParentKey == mFirstMsgKey &&
+ curKey != mThreadParentKey))
+ break;
+ mResultHdr = nullptr;
+ } else
+ NS_ASSERTION(false, "better be able to get child");
+ }
+ // if (!mResultHdr && mThreadParentKey == mFirstMsgKey && !mFoundChildren &&
+ // numChildren > 1) {
+ // mThread->ReparentMsgsWithInvalidParent(numChildren, mThreadParentKey);
+ // }
+ }
+ if (!mResultHdr) {
+ mDone = true;
+ return NS_ERROR_FAILURE;
+ }
+ if (NS_FAILED(rv)) {
+ mDone = true;
+ return rv;
+ } else
+ mNeedToPrefetch = false;
+ mFoundChildren = true;
+
+#ifdef DEBUG_bienvenu1
+ nsMsgKey debugMsgKey;
+ mResultHdr->GetMessageKey(&debugMsgKey);
+ printf("next for %ld = %ld\n", mThreadParentKey, debugMsgKey);
+#endif
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgGroupThreadEnumerator::HasMoreElements(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (mNeedToPrefetch) Prefetch();
+ *aResult = !mDone;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::EnumerateMessages(nsMsgKey parentKey,
+ nsIMsgEnumerator** result) {
+ NS_ADDREF(*result = new nsMsgGroupThreadEnumerator(this, parentKey, nullptr,
+ nullptr));
+ return NS_OK;
+}
+
+#if 0
+nsresult nsMsgGroupThread::ReparentMsgsWithInvalidParent(uint32_t numChildren, nsMsgKey threadParentKey)
+{
+ nsresult ret = NS_OK;
+ // run through looking for messages that don't have a correct parent,
+ // i.e., a parent that's in the thread!
+ for (int32_t childIndex = 0; childIndex < (int32_t) numChildren; childIndex++)
+ {
+ nsCOMPtr<nsIMsgDBHdr> curChild;
+ ret = GetChildHdrAt(childIndex, getter_AddRefs(curChild));
+ if (NS_SUCCEEDED(ret) && curChild)
+ {
+ nsMsgKey parentKey;
+ nsCOMPtr<nsIMsgDBHdr> parent;
+
+ curChild->GetThreadParent(&parentKey);
+
+ if (parentKey != nsMsgKey_None)
+ {
+ GetChild(parentKey, getter_AddRefs(parent));
+ if (!parent)
+ curChild->SetThreadParent(threadParentKey);
+ }
+ }
+ }
+ return ret;
+}
+#endif
+
+NS_IMETHODIMP nsMsgGroupThread::GetRootHdr(nsIMsgDBHdr** result) {
+ NS_ENSURE_ARG_POINTER(result);
+
+ *result = nullptr;
+ int32_t resultIndex = -1;
+
+ if (m_threadRootKey != nsMsgKey_None) {
+ nsresult ret = GetChildHdrForKey(m_threadRootKey, result, &resultIndex);
+ if (NS_SUCCEEDED(ret) && *result)
+ return ret;
+ else {
+ printf("need to reset thread root key\n");
+ uint32_t numChildren;
+ nsMsgKey threadParentKey = nsMsgKey_None;
+ GetNumChildren(&numChildren);
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> curChild;
+ ret = GetChildHdrAt(childIndex, getter_AddRefs(curChild));
+ if (NS_SUCCEEDED(ret) && curChild) {
+ nsMsgKey parentKey;
+
+ curChild->GetThreadParent(&parentKey);
+ if (parentKey == nsMsgKey_None) {
+ NS_ASSERTION(!(*result), "two top level msgs, not good");
+ curChild->GetMessageKey(&threadParentKey);
+ m_threadRootKey = threadParentKey;
+ curChild.forget(result);
+ }
+ }
+ }
+ if (*result) {
+ return NS_OK;
+ }
+ }
+ // if we can't get the thread root key, we'll just get the first hdr.
+ // there's a bug where sometimes we weren't resetting the thread root key
+ // when removing the thread root key.
+ }
+ return GetChildHdrAt(0, result);
+}
+
+nsresult nsMsgGroupThread::ChangeUnreadChildCount(int32_t delta) {
+ m_numUnreadChildren += delta;
+ return NS_OK;
+}
+
+nsresult nsMsgGroupThread::GetChildHdrForKey(nsMsgKey desiredKey,
+ nsIMsgDBHdr** result,
+ int32_t* resultIndex) {
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsresult rv = NS_OK; // XXX or should this default to an error?
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ uint32_t childIndex;
+ for (childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey msgKey;
+ // we're only doing one level of threading, so check if caller is
+ // asking for children of the first message in the thread or not.
+ // if not, we will tell him there are no children.
+ child->GetMessageKey(&msgKey);
+
+ if (msgKey == desiredKey) {
+ child.forget(result);
+ break;
+ }
+ }
+ }
+ if (resultIndex) *resultIndex = (int32_t)childIndex;
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetFirstUnreadChild(nsIMsgDBHdr** result) {
+ NS_ENSURE_ARG_POINTER(result);
+
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ nsresult rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey msgKey;
+ child->GetMessageKey(&msgKey);
+
+ bool isRead;
+ rv = m_db->IsRead(msgKey, &isRead);
+ if (NS_SUCCEEDED(rv) && !isRead) {
+ child.forget(result);
+ break;
+ }
+ }
+ }
+
+ return (*result) ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::GetNewestMsgDate(uint32_t* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ // if this hasn't been set, figure it out by enumerating the msgs in the
+ // thread.
+ if (!m_newestMsgDate) {
+ nsresult rv = NS_OK;
+
+ uint32_t numChildren = 0;
+ GetNumChildren(&numChildren);
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ uint32_t msgDate;
+ child->GetDateInSeconds(&msgDate);
+ if (msgDate > m_newestMsgDate) m_newestMsgDate = msgDate;
+ }
+ }
+ }
+ *aResult = m_newestMsgDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgGroupThread::SetNewestMsgDate(uint32_t aNewestMsgDate) {
+ m_newestMsgDate = aNewestMsgDate;
+ return NS_OK;
+}
+
+nsMsgXFGroupThread::nsMsgXFGroupThread() {}
+
+nsMsgXFGroupThread::~nsMsgXFGroupThread() {}
+
+NS_IMETHODIMP nsMsgXFGroupThread::GetNumChildren(uint32_t* aNumChildren) {
+ NS_ENSURE_ARG_POINTER(aNumChildren);
+ *aNumChildren = m_folders.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgXFGroupThread::GetChildHdrAt(uint32_t aIndex,
+ nsIMsgDBHdr** aResult) {
+ if (aIndex >= m_folders.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+ return m_folders.ObjectAt(aIndex)->GetMessageHeader(m_keys[aIndex], aResult);
+}
+
+NS_IMETHODIMP nsMsgXFGroupThread::GetChildKeyAt(uint32_t aIndex,
+ nsMsgKey* aResult) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgXFGroupThread::RemoveChildAt(uint32_t aIndex) {
+ NS_ENSURE_TRUE(aIndex < m_folders.Length(), NS_MSG_MESSAGE_NOT_FOUND);
+
+ nsresult rv = nsMsgGroupThread::RemoveChildAt(aIndex);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_folders.RemoveElementAt(aIndex);
+ return NS_OK;
+}
+
+void nsMsgXFGroupThread::InsertMsgHdrAt(nsMsgViewIndex index,
+ nsIMsgDBHdr* hdr) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ m_folders.InsertObjectAt(folder, index);
+ nsMsgGroupThread::InsertMsgHdrAt(index, hdr);
+}
+
+void nsMsgXFGroupThread::SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ m_folders.ReplaceObjectAt(folder, index);
+ nsMsgGroupThread::SetMsgHdrAt(index, hdr);
+}
+
+nsMsgViewIndex nsMsgXFGroupThread::FindMsgHdr(nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ size_t index = 0;
+ while (true) {
+ index = m_keys.IndexOf(msgKey, index);
+ if (index == m_keys.NoIndex || m_folders[index] == folder) break;
+ index++;
+ }
+ return (nsMsgViewIndex)index;
+}
+
+nsMsgViewIndex nsMsgXFGroupThread::AddMsgHdrInDateOrder(nsIMsgDBHdr* child,
+ nsMsgDBView* view) {
+ nsMsgViewIndex insertIndex =
+ nsMsgGroupThread::AddMsgHdrInDateOrder(child, view);
+ nsCOMPtr<nsIMsgFolder> folder;
+ child->GetFolder(getter_AddRefs(folder));
+ m_folders.InsertObjectAt(folder, insertIndex);
+ return insertIndex;
+}
+nsMsgViewIndex nsMsgXFGroupThread::GetInsertIndexFromView(
+ nsMsgDBView* view, nsIMsgDBHdr* child,
+ nsMsgViewSortOrderValue threadSortOrder) {
+ return view->GetInsertIndexHelper(child, m_keys, &m_folders, threadSortOrder,
+ nsMsgViewSortType::byDate);
+}
diff --git a/comm/mailnews/base/src/nsMsgGroupThread.h b/comm/mailnews/base/src/nsMsgGroupThread.h
new file mode 100644
index 0000000000..735b9580e3
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgGroupThread.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Attributes.h"
+#include "msgCore.h"
+#include "nsCOMArray.h"
+#include "nsIMsgThread.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgDBView.h"
+
+class nsMsgGroupView;
+
+class nsMsgGroupThread : public nsIMsgThread {
+ public:
+ friend class nsMsgGroupView;
+
+ nsMsgGroupThread();
+ explicit nsMsgGroupThread(nsIMsgDatabase* db);
+
+ NS_DECL_NSIMSGTHREAD
+ NS_DECL_ISUPPORTS
+
+ protected:
+ virtual ~nsMsgGroupThread();
+
+ void Init();
+ nsMsgViewIndex AddChildFromGroupView(nsIMsgDBHdr* child, nsMsgDBView* view);
+ nsresult RemoveChild(nsMsgKey msgKey);
+ nsresult RerootThread(nsIMsgDBHdr* newParentOfOldRoot, nsIMsgDBHdr* oldRoot,
+ nsIDBChangeAnnouncer* announcer);
+
+ virtual nsMsgViewIndex AddMsgHdrInDateOrder(nsIMsgDBHdr* child,
+ nsMsgDBView* view);
+ virtual nsMsgViewIndex GetInsertIndexFromView(
+ nsMsgDBView* view, nsIMsgDBHdr* child,
+ nsMsgViewSortOrderValue threadSortOrder);
+ nsresult ReparentNonReferenceChildrenOf(nsIMsgDBHdr* topLevelHdr,
+ nsMsgKey newParentKey,
+ nsIDBChangeAnnouncer* announcer);
+
+ nsresult ReparentChildrenOf(nsMsgKey oldParent, nsMsgKey newParent,
+ nsIDBChangeAnnouncer* announcer);
+ nsresult ChangeUnreadChildCount(int32_t delta);
+ nsresult GetChildHdrForKey(nsMsgKey desiredKey, nsIMsgDBHdr** result,
+ int32_t* resultIndex);
+ uint32_t NumRealChildren();
+ virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr);
+ virtual void SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr);
+ virtual nsMsgViewIndex FindMsgHdr(nsIMsgDBHdr* hdr);
+
+ nsMsgKey m_threadKey;
+ uint32_t m_numUnreadChildren;
+ uint32_t m_flags;
+ nsMsgKey m_threadRootKey;
+ uint32_t m_newestMsgDate;
+ nsTArray<nsMsgKey> m_keys;
+ bool m_dummy; // top level msg is a dummy, e.g., grouped by age.
+ nsCOMPtr<nsIMsgDatabase> m_db; // should we make a weak ref or just a ptr?
+};
+
+class nsMsgXFGroupThread : public nsMsgGroupThread {
+ public:
+ nsMsgXFGroupThread();
+
+ NS_IMETHOD GetNumChildren(uint32_t* aNumChildren) override;
+ NS_IMETHOD GetChildKeyAt(uint32_t aIndex, nsMsgKey* aResult) override;
+ NS_IMETHOD GetChildHdrAt(uint32_t aIndex, nsIMsgDBHdr** aResult) override;
+ NS_IMETHOD RemoveChildAt(uint32_t aIndex) override;
+
+ protected:
+ virtual ~nsMsgXFGroupThread();
+
+ virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) override;
+ virtual void SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) override;
+ virtual nsMsgViewIndex FindMsgHdr(nsIMsgDBHdr* hdr) override;
+ virtual nsMsgViewIndex AddMsgHdrInDateOrder(nsIMsgDBHdr* child,
+ nsMsgDBView* view) override;
+ virtual nsMsgViewIndex GetInsertIndexFromView(
+ nsMsgDBView* view, nsIMsgDBHdr* child,
+ nsMsgViewSortOrderValue threadSortOrder) override;
+
+ nsCOMArray<nsIMsgFolder> m_folders;
+};
diff --git a/comm/mailnews/base/src/nsMsgGroupView.cpp b/comm/mailnews/base/src/nsMsgGroupView.cpp
new file mode 100644
index 0000000000..ca42d01a5a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgGroupView.cpp
@@ -0,0 +1,941 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgUtils.h"
+#include "nsMsgGroupView.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgSearchSession.h"
+#include "nsMsgGroupThread.h"
+#include "nsTreeColumns.h"
+#include "nsMsgMessageFlags.h"
+#include <plhash.h>
+#include "mozilla/Attributes.h"
+
+// Allocate this more to avoid reallocation on new mail.
+#define MSGHDR_CACHE_LOOK_AHEAD_SIZE 25
+// Max msghdr cache entries.
+#define MSGHDR_CACHE_MAX_SIZE 8192
+#define MSGHDR_CACHE_DEFAULT_SIZE 100
+
+nsMsgGroupView::nsMsgGroupView() { m_dayChanged = false; }
+
+nsMsgGroupView::~nsMsgGroupView() {}
+
+NS_IMETHODIMP
+nsMsgGroupView::Open(nsIMsgFolder* aFolder, nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags, int32_t* aCount) {
+ nsresult rv =
+ nsMsgDBView::Open(aFolder, aSortType, aSortOrder, aViewFlags, aCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ PersistFolderInfo(getter_AddRefs(dbFolderInfo));
+
+ nsCOMPtr<nsIMsgEnumerator> headers;
+ rv = m_db->EnumerateMessages(getter_AddRefs(headers));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return OpenWithHdrs(headers, aSortType, aSortOrder, aViewFlags, aCount);
+}
+
+void nsMsgGroupView::InternalClose() {
+ m_groupsTable.Clear();
+ // Nothing to do if we're not grouped.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) return;
+
+ bool rcvDate = false;
+
+ if (m_sortType == nsMsgViewSortType::byReceived) rcvDate = true;
+
+ if (m_db && ((m_sortType == nsMsgViewSortType::byDate) ||
+ (m_sortType == nsMsgViewSortType::byReceived))) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ if (dbFolderInfo) {
+ uint32_t expandFlags = 0;
+ uint32_t num = GetSize();
+
+ for (uint32_t i = 0; i < num; i++) {
+ if (m_flags[i] & MSG_VIEW_FLAG_ISTHREAD &&
+ !(m_flags[i] & nsMsgMessageFlags::Elided)) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ uint32_t ageBucket;
+ nsresult rv = GetAgeBucketValue(msgHdr, &ageBucket, rcvDate);
+ if (NS_SUCCEEDED(rv)) expandFlags |= 1 << ageBucket;
+ }
+ }
+ }
+ dbFolderInfo->SetUint32Property("dateGroupFlags", expandFlags);
+ }
+ }
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::Close() {
+ InternalClose();
+ return nsMsgDBView::Close();
+}
+
+// Set rcvDate to true to get the Received: date instead of the Date: date.
+nsresult nsMsgGroupView::GetAgeBucketValue(nsIMsgDBHdr* aMsgHdr,
+ uint32_t* aAgeBucket, bool rcvDate) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ NS_ENSURE_ARG_POINTER(aAgeBucket);
+
+ PRTime dateOfMsg;
+ nsresult rv;
+ if (!rcvDate)
+ rv = aMsgHdr->GetDate(&dateOfMsg);
+ else {
+ uint32_t rcvDateSecs;
+ rv = aMsgHdr->GetUint32Property("dateReceived", &rcvDateSecs);
+ Seconds2PRTime(rcvDateSecs, &dateOfMsg);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PRTime currentTime = PR_Now();
+ PRExplodedTime currentExplodedTime;
+ PR_ExplodeTime(currentTime, PR_LocalTimeParameters, &currentExplodedTime);
+ PRExplodedTime explodedMsgTime;
+ PR_ExplodeTime(dateOfMsg, PR_LocalTimeParameters, &explodedMsgTime);
+
+ if (m_lastCurExplodedTime.tm_mday &&
+ m_lastCurExplodedTime.tm_mday != currentExplodedTime.tm_mday)
+ // This will cause us to rebuild the view.
+ m_dayChanged = true;
+
+ m_lastCurExplodedTime = currentExplodedTime;
+ if (currentExplodedTime.tm_year == explodedMsgTime.tm_year &&
+ currentExplodedTime.tm_month == explodedMsgTime.tm_month &&
+ currentExplodedTime.tm_mday == explodedMsgTime.tm_mday) {
+ // Same day.
+ *aAgeBucket = 1;
+ }
+ // Figure out how many days ago this msg arrived.
+ else if (currentTime > dateOfMsg) {
+ // Setting the time variables to local time.
+ int64_t GMTLocalTimeShift = currentExplodedTime.tm_params.tp_gmt_offset +
+ currentExplodedTime.tm_params.tp_dst_offset;
+ GMTLocalTimeShift *= PR_USEC_PER_SEC;
+ currentTime += GMTLocalTimeShift;
+ dateOfMsg += GMTLocalTimeShift;
+
+ // The most recent midnight, counting from current time.
+ int64_t mostRecentMidnight = currentTime - currentTime % PR_USEC_PER_DAY;
+ int64_t yesterday = mostRecentMidnight - PR_USEC_PER_DAY;
+ // Most recent midnight minus 6 days.
+ int64_t mostRecentWeek = mostRecentMidnight - (PR_USEC_PER_DAY * 6);
+
+ // Was the message sent yesterday?
+ if (dateOfMsg >= yesterday)
+ *aAgeBucket = 2;
+ else if (dateOfMsg >= mostRecentWeek)
+ *aAgeBucket = 3;
+ else {
+ int64_t lastTwoWeeks = mostRecentMidnight - PR_USEC_PER_DAY * 13;
+ *aAgeBucket = (dateOfMsg >= lastTwoWeeks) ? 4 : 5;
+ }
+ } else {
+ // All that remains is a future date.
+ *aAgeBucket = 6;
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgGroupView::HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey) {
+ nsCString cStringKey;
+ aHashKey.Truncate();
+ nsresult rv = NS_OK;
+ bool rcvDate = false;
+
+ switch (m_sortType) {
+ case nsMsgViewSortType::bySubject:
+ (void)msgHdr->GetSubject(cStringKey);
+ CopyASCIItoUTF16(cStringKey, aHashKey);
+ break;
+ case nsMsgViewSortType::byAuthor:
+ rv = nsMsgDBView::FetchAuthor(msgHdr, aHashKey);
+ break;
+ case nsMsgViewSortType::byRecipient:
+ (void)msgHdr->GetRecipients(getter_Copies(cStringKey));
+ CopyASCIItoUTF16(cStringKey, aHashKey);
+ break;
+ case nsMsgViewSortType::byAccount:
+ case nsMsgViewSortType::byTags: {
+ nsCOMPtr<nsIMsgDatabase> dbToUse = m_db;
+ if (!dbToUse)
+ // Probably a search view.
+ GetDBForViewIndex(0, getter_AddRefs(dbToUse));
+
+ rv = (m_sortType == nsMsgViewSortType::byAccount)
+ ? FetchAccount(msgHdr, aHashKey)
+ : FetchTags(msgHdr, aHashKey);
+ } break;
+ case nsMsgViewSortType::byAttachments: {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ aHashKey.Assign(flags & nsMsgMessageFlags::Attachment ? '1' : '0');
+ break;
+ }
+ case nsMsgViewSortType::byFlagged: {
+ uint32_t flags;
+ msgHdr->GetFlags(&flags);
+ aHashKey.Assign(flags & nsMsgMessageFlags::Marked ? '1' : '0');
+ break;
+ }
+ case nsMsgViewSortType::byPriority: {
+ nsMsgPriorityValue priority;
+ msgHdr->GetPriority(&priority);
+ aHashKey.AppendInt(priority);
+ } break;
+ case nsMsgViewSortType::byStatus: {
+ uint32_t status = 0;
+ GetStatusSortValue(msgHdr, &status);
+ aHashKey.AppendInt(status);
+ } break;
+ case nsMsgViewSortType::byReceived:
+ rcvDate = true;
+ [[fallthrough]];
+ case nsMsgViewSortType::byDate: {
+ uint32_t ageBucket;
+ rv = GetAgeBucketValue(msgHdr, &ageBucket, rcvDate);
+ if (NS_SUCCEEDED(rv)) aHashKey.AppendInt(ageBucket);
+
+ break;
+ }
+ case nsMsgViewSortType::byCustom: {
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+ if (colHandler) {
+ bool isString;
+ colHandler->IsString(&isString);
+ if (isString) {
+ rv = colHandler->GetSortStringForRow(msgHdr, aHashKey);
+ } else {
+ uint32_t intKey;
+ rv = colHandler->GetSortLongForRow(msgHdr, &intKey);
+ aHashKey.AppendInt(intKey);
+ }
+ }
+ break;
+ }
+ case nsMsgViewSortType::byCorrespondent:
+ if (IsOutgoingMsg(msgHdr))
+ rv = FetchRecipients(msgHdr, aHashKey);
+ else
+ rv = FetchAuthor(msgHdr, aHashKey);
+
+ break;
+ default:
+ NS_ASSERTION(false, "no hash key for this type");
+ rv = NS_ERROR_FAILURE;
+ }
+ return rv;
+}
+
+nsMsgGroupThread* nsMsgGroupView::CreateGroupThread(nsIMsgDatabase* db) {
+ return new nsMsgGroupThread(db);
+}
+
+nsMsgGroupThread* nsMsgGroupView::AddHdrToThread(nsIMsgDBHdr* msgHdr,
+ bool* pNewThread) {
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ nsString hashKey;
+ nsresult rv = HashHdr(msgHdr, hashKey);
+ if (NS_FAILED(rv)) return nullptr;
+
+ // if (m_sortType == nsMsgViewSortType::byDate)
+ // msgKey = ((nsPRUint32Key *)hashKey)->GetValue();
+ nsCOMPtr<nsIMsgThread> msgThread;
+ m_groupsTable.Get(hashKey, getter_AddRefs(msgThread));
+ bool newThread = !msgThread;
+ *pNewThread = newThread;
+ // Index of first message in thread in view.
+ nsMsgViewIndex viewIndexOfThread;
+ // Index of newly added header in thread.
+ nsMsgViewIndex threadInsertIndex;
+
+ nsMsgGroupThread* foundThread =
+ static_cast<nsMsgGroupThread*>(msgThread.get());
+ if (foundThread) {
+ // Find the view index of the root node of the thread in the view.
+ viewIndexOfThread = GetIndexOfFirstDisplayedKeyInThread(foundThread, true);
+ if (viewIndexOfThread == nsMsgViewIndex_None) {
+ // Something is wrong with the group table. Remove the old group and
+ // insert a new one.
+ m_groupsTable.Remove(hashKey);
+ foundThread = nullptr;
+ *pNewThread = newThread = true;
+ }
+ }
+
+ // If the thread does not already exist, create one
+ if (!foundThread) {
+ foundThread = CreateGroupThread(m_db);
+ msgThread = foundThread;
+ m_groupsTable.InsertOrUpdate(hashKey, msgThread);
+ if (GroupViewUsesDummyRow()) {
+ foundThread->m_dummy = true;
+ msgFlags |= MSG_VIEW_FLAG_DUMMY | MSG_VIEW_FLAG_HASCHILDREN;
+ }
+
+ viewIndexOfThread = GetInsertIndex(msgHdr);
+ if (viewIndexOfThread == nsMsgViewIndex_None)
+ viewIndexOfThread = m_keys.Length();
+
+ // Add the thread root node to the view.
+ InsertMsgHdrAt(
+ viewIndexOfThread, msgHdr, msgKey,
+ msgFlags | MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided, 0);
+
+ // For dummy rows, Have the header serve as the dummy node (it will be
+ // added again for its actual content later).
+ if (GroupViewUsesDummyRow()) foundThread->InsertMsgHdrAt(0, msgHdr);
+
+ // Calculate the (integer thread key); this really only needs to be done for
+ // the byDate case where the expanded state of the groups can be easily
+ // persisted and restored because of the bounded, consecutive value space
+ // occupied. We calculate an integer value in all cases mainly because
+ // it's the sanest choice available...
+ // (The thread key needs to be an integer, so parse hash keys that are
+ // stringified integers to real integers, and hash actual strings into
+ // integers.)
+ if ((m_sortType == nsMsgViewSortType::byAttachments) ||
+ (m_sortType == nsMsgViewSortType::byFlagged) ||
+ (m_sortType == nsMsgViewSortType::byPriority) ||
+ (m_sortType == nsMsgViewSortType::byStatus) ||
+ (m_sortType == nsMsgViewSortType::byReceived) ||
+ (m_sortType == nsMsgViewSortType::byDate))
+ foundThread->m_threadKey =
+ atoi(NS_LossyConvertUTF16toASCII(hashKey).get());
+ else
+ foundThread->m_threadKey =
+ (nsMsgKey)PL_HashString(NS_LossyConvertUTF16toASCII(hashKey).get());
+ }
+
+ // Add the message to the thread as an actual content-bearing header.
+ // (If we use dummy rows, it was already added to the thread during creation.)
+ threadInsertIndex = foundThread->AddChildFromGroupView(msgHdr, this);
+ // Check if new hdr became thread root.
+ if (!newThread && threadInsertIndex == 0) {
+ // Update the root node's header (in the view) to be the same as the root
+ // node in the thread.
+ SetMsgHdrAt(msgHdr, viewIndexOfThread, msgKey,
+ (msgFlags & ~(nsMsgMessageFlags::Elided)) |
+ // Maintain elided flag and dummy flag.
+ (m_flags[viewIndexOfThread] &
+ (nsMsgMessageFlags::Elided | MSG_VIEW_FLAG_DUMMY)) |
+ // Ensure thread and has-children flags are set.
+ MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN,
+ 0);
+ // Update the content-bearing copy in the thread to match. (the root and
+ // first nodes in the thread should always be the same header.)
+ // Note: the guy who used to be the root will still exist. If our list of
+ // nodes was [A A], a new node B is introduced which sorts to be the first
+ // node, giving us [B A A], our copy makes that [B B A], and things are
+ // right in the world (since we want the first two headers to be the same
+ // since one is our dummy and one is real.)
+ if (GroupViewUsesDummyRow()) {
+ // Replace the old duplicate dummy header.
+ // We do not update the content-bearing copy in the view to match; we
+ // leave that up to OnNewHeader, which is the piece of code who gets to
+ // care about whether the thread's children are shown or not (elided).
+ foundThread->SetMsgHdrAt(1, msgHdr);
+ }
+ }
+
+ return foundThread;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) {
+ nsresult rv = NS_OK;
+
+ m_groupsTable.Clear();
+ if (aSortType == nsMsgViewSortType::byThread ||
+ aSortType == nsMsgViewSortType::byId ||
+ aSortType == nsMsgViewSortType::byNone ||
+ aSortType == nsMsgViewSortType::bySize)
+ return NS_ERROR_INVALID_ARG;
+
+ m_sortType = aSortType;
+ m_sortOrder = aSortOrder;
+ m_viewFlags = aViewFlags | nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kGroupBySort;
+ SaveSortInfo(m_sortType, m_sortOrder);
+
+ if (m_sortType == nsMsgViewSortType::byCustom) {
+ // If the desired sort is a custom column and there is no handler found,
+ // it hasn't been registered yet; after the custom column observer is
+ // notified with MsgCreateDBView and registers the handler, it will come
+ // back and build the view.
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+ if (!colHandler) return rv;
+ }
+
+ bool hasMore;
+ nsCOMPtr<nsISupports> supports;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ while (NS_SUCCEEDED(rv) &&
+ NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) {
+ rv = aHeaders->GetNext(getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ bool notUsed;
+ AddHdrToThread(msgHdr, &notUsed);
+ }
+ }
+ uint32_t expandFlags = 0;
+ bool expandAll = m_viewFlags & nsMsgViewFlagsType::kExpandAll;
+ uint32_t viewFlag =
+ (m_sortType == nsMsgViewSortType::byDate) ? MSG_VIEW_FLAG_DUMMY : 0;
+ if (viewFlag && m_db) {
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsresult rv = m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (dbFolderInfo)
+ dbFolderInfo->GetUint32Property("dateGroupFlags", 0, &expandFlags);
+ }
+ // Go through the view updating the flags for threads with more than one
+ // message, and if grouped by date, expanding threads that were expanded
+ // before.
+ for (uint32_t viewIndex = 0; viewIndex < m_keys.Length(); viewIndex++) {
+ nsCOMPtr<nsIMsgThread> thread;
+ GetThreadContainingIndex(viewIndex, getter_AddRefs(thread));
+ if (thread) {
+ uint32_t numChildren;
+ thread->GetNumChildren(&numChildren);
+ if (numChildren > 1 || viewFlag)
+ OrExtraFlag(viewIndex, viewFlag | MSG_VIEW_FLAG_HASCHILDREN);
+ if (expandAll || expandFlags) {
+ nsMsgGroupThread* groupThread =
+ static_cast<nsMsgGroupThread*>((nsIMsgThread*)thread);
+ if (expandAll || expandFlags & (1 << groupThread->m_threadKey)) {
+ uint32_t numExpanded;
+ ExpandByIndex(viewIndex, &numExpanded);
+ viewIndex += numExpanded;
+ }
+ }
+ }
+ }
+ *aCount = m_keys.Length();
+ return rv;
+}
+
+// We wouldn't need this if we never instantiated this directly,
+// but instead used nsMsgThreadedDBView with the grouping flag set.
+// Or, we could get rid of the nsMsgThreadedDBView impl of this method.
+NS_IMETHODIMP
+nsMsgGroupView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowAllThreads;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ nsMsgDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow,
+ aCmdUpdater);
+ nsMsgGroupView* newMsgDBView = (nsMsgGroupView*)aNewMsgDBView;
+
+ // If grouped, we need to clone the group thread hash table.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) {
+ for (auto iter = m_groupsTable.Iter(); !iter.Done(); iter.Next()) {
+ newMsgDBView->m_groupsTable.InsertOrUpdate(iter.Key(), iter.UserData());
+ }
+ }
+ return NS_OK;
+}
+
+// E.g., if the day has changed, we need to close and re-open the view.
+// Or, if we're switching between grouping and threading in a cross-folder
+// saved search. In that case, we needed to build an enumerator based on the
+// old view type, and internally close the view based on its old type, but
+// rebuild the new view based on the new view type. So we pass the new
+// view flags to OpenWithHdrs.
+nsresult nsMsgGroupView::RebuildView(nsMsgViewFlagsTypeValue newFlags) {
+ nsCOMPtr<nsIMsgEnumerator> headers;
+ if (NS_SUCCEEDED(GetMessageEnumerator(getter_AddRefs(headers)))) {
+ int32_t count;
+ m_dayChanged = false;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ nsMsgKey curSelectedKey;
+ SaveAndClearSelection(&curSelectedKey, preservedSelection);
+ InternalClose();
+ int32_t oldSize = GetSize();
+ // This is important, because the tree will ask us for our row count,
+ // which gets determined from the number of keys.
+ m_keys.Clear();
+ // Be consistent.
+ m_flags.Clear();
+ m_levels.Clear();
+
+ // This needs to happen after we remove all the keys, since
+ // RowCountChanged() will call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -oldSize);
+
+ SetSuppressChangeNotifications(true);
+ nsresult rv =
+ OpenWithHdrs(headers, m_sortType, m_sortOrder, newFlags, &count);
+ SetSuppressChangeNotifications(false);
+ if (mTree) mTree->RowCountChanged(0, GetSize());
+ if (mJSTree) mJSTree->RowCountChanged(0, GetSize());
+
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Now, restore our desired selection.
+ AutoTArray<nsMsgKey, 1> keyArray;
+ keyArray.AppendElement(curSelectedKey);
+
+ return RestoreSelection(curSelectedKey, keyArray);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgGroupView::OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool ensureListed) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::OnNewHeader(newHdr, aParentKey, ensureListed);
+
+ // Check if we're adding a header, and the current day has changed.
+ // If it has, we're just going to close and re-open the view so things
+ // will be correctly categorized.
+ if (m_dayChanged) return RebuildView(m_viewFlags);
+
+ bool newThread;
+ nsMsgGroupThread* thread = AddHdrToThread(newHdr, &newThread);
+ if (thread) {
+ // Find the view index of (the root node of) the thread.
+ nsMsgViewIndex threadIndex = ThreadIndexOfMsgHdr(newHdr);
+ // May need to fix thread counts.
+ if (threadIndex != nsMsgViewIndex_None) {
+ if (newThread) {
+ // AddHdrToThread creates the header elided, so we need to un-elide it
+ // if we want it expanded.
+ if (m_viewFlags & nsMsgViewFlagsType::kExpandAll)
+ m_flags[threadIndex] &= ~nsMsgMessageFlags::Elided;
+ } else {
+ m_flags[threadIndex] |=
+ MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD;
+ }
+
+ int32_t numRowsToInvalidate = 1;
+ // If the thread is expanded (not elided), we should add the header to
+ // the view.
+ if (!(m_flags[threadIndex] & nsMsgMessageFlags::Elided)) {
+ uint32_t msgIndexInThread = thread->FindMsgHdr(newHdr);
+ bool insertedAtThreadRoot = !msgIndexInThread;
+ // Add any new display node and potentially fix-up changes in the root.
+ // (If this is a new thread and we are not using a dummy row, the only
+ // node to display is the root node which has already been added by
+ // AddHdrToThread. And since there is just the one, no change in root
+ // could have occurred, so we have nothing to do.)
+ if (!newThread || GroupViewUsesDummyRow()) {
+ // We never want to insert/update the root node, because
+ // AddHdrToThread has already done that for us (in all cases).
+ if (insertedAtThreadRoot) msgIndexInThread++;
+ // If this header is the new parent of the thread... AND
+ // If we are not using a dummy row, this means we need to append our
+ // old node as the first child of the new root.
+ // (If we are using a dummy row, the old node's "content" node already
+ // exists (at position threadIndex + 1) and we need to insert the
+ // "content" copy of the new root node there, pushing our old
+ // "content" node down.)
+ // Example mini-diagrams, wrapping the to-add thing with ()
+ // No dummy row; we had: [A], now we have [B], we want [B (A)].
+ // Dummy row; we had: [A A], now we have [B A], we want [B (B) A].
+ // (Coming into this we're adding 'B')
+ if (!newThread && insertedAtThreadRoot && !GroupViewUsesDummyRow()) {
+ // Grab a copy of the old root node ('A') from the thread so we can
+ // insert it. (offset msgIndexInThread=1 is the right thing; we are
+ // non-dummy.)
+ thread->GetChildHdrAt(msgIndexInThread, &newHdr);
+ }
+ // Nothing to do for dummy case, we're already inserting 'B'.
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ newHdr->GetMessageKey(&msgKey);
+ newHdr->GetFlags(&msgFlags);
+ InsertMsgHdrAt(threadIndex + msgIndexInThread, newHdr, msgKey,
+ msgFlags, 1);
+ }
+ // The call to NoteChange() has to happen after we add the key
+ // as NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ // (msgIndexInThread states - new thread: 0, old thread at root: 1).
+ if (newThread && GroupViewUsesDummyRow())
+ NoteChange(threadIndex, 2, nsMsgViewNotificationCode::insertOrDelete);
+ else
+ NoteChange(threadIndex + msgIndexInThread, 1,
+ nsMsgViewNotificationCode::insertOrDelete);
+
+ numRowsToInvalidate = msgIndexInThread;
+ } else if (newThread) {
+ // We still need the addition notification for new threads when elided.
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ }
+
+ NoteChange(threadIndex, numRowsToInvalidate,
+ nsMsgViewNotificationCode::changed);
+ }
+ }
+
+ // If thread is expanded, we need to add hdr to view...
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags,
+ aInstigator);
+
+ nsCOMPtr<nsIMsgThread> thread;
+
+ // Check if we're adding a header, and the current day has changed.
+ // If it has, we're just going to close and re-open the view so things
+ // will be correctly categorized.
+ if (m_dayChanged) return RebuildView(m_viewFlags);
+
+ nsresult rv = GetThreadContainingMsgHdr(aHdrChanged, getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t deltaFlags = (aOldFlags ^ aNewFlags);
+ if (deltaFlags & nsMsgMessageFlags::Read)
+ thread->MarkChildRead(aNewFlags & nsMsgMessageFlags::Read);
+
+ return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags,
+ aInstigator);
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags, nsIDBChangeListener* aInstigator) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags,
+ aInstigator);
+
+ // Check if we're adding a header, and the current day has changed.
+ // If it has, we're just going to close and re-open the view so things
+ // will be correctly categorized.
+ if (m_dayChanged) return RebuildView(m_viewFlags);
+
+ nsCOMPtr<nsIMsgThread> thread;
+ nsMsgKey keyDeleted;
+ aHdrDeleted->GetMessageKey(&keyDeleted);
+
+ nsresult rv = GetThreadContainingMsgHdr(aHdrDeleted, getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgViewIndex viewIndexOfThread =
+ GetIndexOfFirstDisplayedKeyInThread(thread, true); // Yes to dummy node.
+
+ thread->RemoveChildHdr(aHdrDeleted, nullptr);
+
+ nsMsgGroupThread* groupThread =
+ static_cast<nsMsgGroupThread*>((nsIMsgThread*)thread);
+
+ bool rootDeleted = viewIndexOfThread != nsMsgKey_None &&
+ m_keys[viewIndexOfThread] == keyDeleted;
+ rv = nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags, aInstigator);
+ if (groupThread->m_dummy) {
+ if (!groupThread->NumRealChildren()) {
+ // Get rid of dummy.
+ thread->RemoveChildAt(0);
+ if (viewIndexOfThread != nsMsgKey_None) {
+ RemoveByIndex(viewIndexOfThread);
+ if (m_deletingRows)
+ mIndicesToNoteChange.AppendElement(viewIndexOfThread);
+ }
+ } else if (rootDeleted) {
+ // Reflect new thread root into view.dummy row.
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ thread->GetChildHdrAt(0, getter_AddRefs(hdr));
+ if (hdr) {
+ nsMsgKey msgKey;
+ hdr->GetMessageKey(&msgKey);
+ SetMsgHdrAt(hdr, viewIndexOfThread, msgKey, m_flags[viewIndexOfThread],
+ 0);
+ }
+ }
+ }
+ if (!groupThread->m_keys.Length()) {
+ nsString hashKey;
+ rv = HashHdr(aHdrDeleted, hashKey);
+ if (NS_SUCCEEDED(rv)) m_groupsTable.Remove(hashKey);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::GetRowProperties(int32_t aRow, nsAString& aProperties) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) {
+ aProperties.AssignLiteral("dummy");
+ return NS_OK;
+ }
+
+ return nsMsgDBView::GetRowProperties(aRow, aProperties);
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::GetCellProperties(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aProperties) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) {
+ aProperties.AssignLiteral("dummy read");
+
+ if (!(m_flags[aRow] & nsMsgMessageFlags::Elided)) return NS_OK;
+
+ // Set unread property if a collapsed group thread has unread.
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString hashKey;
+ rv = HashHdr(msgHdr, hashKey);
+ if (NS_FAILED(rv)) return NS_OK;
+
+ nsCOMPtr<nsIMsgThread> msgThread;
+ m_groupsTable.Get(hashKey, getter_AddRefs(msgThread));
+ nsMsgGroupThread* groupThread =
+ static_cast<nsMsgGroupThread*>(msgThread.get());
+ if (!groupThread) return NS_OK;
+
+ uint32_t numUnrMsg = 0;
+ groupThread->GetNumUnreadChildren(&numUnrMsg);
+ if (numUnrMsg > 0) aProperties.AppendLiteral(" hasUnread");
+
+ return NS_OK;
+ }
+
+ return nsMsgDBView::GetCellProperties(aRow, aCol, aProperties);
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::CellTextForColumn(int32_t aRow, const nsAString& aColumnName,
+ nsAString& aValue) {
+ if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (!(m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) ||
+ aColumnName.EqualsLiteral("unreadCol"))
+ return nsMsgDBView::CellTextForColumn(aRow, aColumnName, aValue);
+
+ // We only treat "subject" and "total" here.
+ bool isSubject;
+ if (!(isSubject = aColumnName.EqualsLiteral("subjectCol")) &&
+ !aColumnName.EqualsLiteral("totalCol"))
+ return NS_OK;
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsString hashKey;
+ rv = HashHdr(msgHdr, hashKey);
+ if (NS_FAILED(rv)) return NS_OK;
+ nsCOMPtr<nsIMsgThread> msgThread;
+ m_groupsTable.Get(hashKey, getter_AddRefs(msgThread));
+ nsMsgGroupThread* groupThread =
+ static_cast<nsMsgGroupThread*>(msgThread.get());
+ if (isSubject) {
+ uint32_t flags;
+ bool rcvDate = false;
+ msgHdr->GetFlags(&flags);
+ aValue.Truncate();
+ switch (m_sortType) {
+ case nsMsgViewSortType::byReceived:
+ rcvDate = true;
+ [[fallthrough]];
+ case nsMsgViewSortType::byDate: {
+ uint32_t ageBucket = 0;
+ GetAgeBucketValue(msgHdr, &ageBucket, rcvDate);
+ switch (ageBucket) {
+ case 1:
+ aValue.Assign(nsMsgDBView::kTodayString);
+ break;
+ case 2:
+ aValue.Assign(nsMsgDBView::kYesterdayString);
+ break;
+ case 3:
+ aValue.Assign(nsMsgDBView::kLastWeekString);
+ break;
+ case 4:
+ aValue.Assign(nsMsgDBView::kTwoWeeksAgoString);
+ break;
+ case 5:
+ aValue.Assign(nsMsgDBView::kOldMailString);
+ break;
+ default:
+ // Future date, error/spoofed.
+ aValue.Assign(nsMsgDBView::kFutureDateString);
+ break;
+ }
+ break;
+ }
+ case nsMsgViewSortType::bySubject:
+ FetchSubject(msgHdr, m_flags[aRow], aValue);
+ break;
+ case nsMsgViewSortType::byAuthor:
+ FetchAuthor(msgHdr, aValue);
+ break;
+ case nsMsgViewSortType::byStatus:
+ rv = FetchStatus(m_flags[aRow], aValue);
+ if (aValue.IsEmpty()) {
+ GetString(u"messagesWithNoStatus", aValue);
+ }
+ break;
+ case nsMsgViewSortType::byTags:
+ rv = FetchTags(msgHdr, aValue);
+ if (aValue.IsEmpty()) {
+ GetString(u"untaggedMessages", aValue);
+ }
+ break;
+ case nsMsgViewSortType::byPriority:
+ FetchPriority(msgHdr, aValue);
+ if (aValue.IsEmpty()) {
+ GetString(u"noPriority", aValue);
+ }
+ break;
+ case nsMsgViewSortType::byAccount:
+ FetchAccount(msgHdr, aValue);
+ break;
+ case nsMsgViewSortType::byRecipient:
+ FetchRecipients(msgHdr, aValue);
+ break;
+ case nsMsgViewSortType::byAttachments:
+ GetString(flags & nsMsgMessageFlags::Attachment ? u"attachments"
+ : u"noAttachments",
+ aValue);
+ break;
+ case nsMsgViewSortType::byFlagged:
+ GetString(
+ flags & nsMsgMessageFlags::Marked ? u"groupFlagged" : u"notFlagged",
+ aValue);
+ break;
+ // byLocation is a special case; we don't want to have duplicate
+ // all this logic in nsMsgSearchDBView, and its hash key is what we
+ // want anyways, so just copy it across.
+ case nsMsgViewSortType::byLocation:
+ case nsMsgViewSortType::byCorrespondent:
+ aValue = hashKey;
+ break;
+ case nsMsgViewSortType::byCustom: {
+ nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler();
+ if (colHandler) {
+ bool isString;
+ colHandler->IsString(&isString);
+ if (isString) {
+ rv = colHandler->GetSortStringForRow(msgHdr.get(), aValue);
+ } else {
+ uint32_t intKey;
+ rv = colHandler->GetSortLongForRow(msgHdr.get(), &intKey);
+ aValue.AppendInt(intKey);
+ }
+ }
+ if (aValue.IsEmpty()) aValue.Assign('*');
+ break;
+ }
+
+ default:
+ NS_ASSERTION(false, "we don't sort by group for this type");
+ break;
+ }
+
+ if (groupThread) {
+ // Get number of messages in group.
+ nsAutoString formattedCountMsg;
+ uint32_t numMsg = groupThread->NumRealChildren();
+ formattedCountMsg.AppendInt(numMsg);
+
+ // Get number of unread messages.
+ nsAutoString formattedCountUnrMsg;
+ uint32_t numUnrMsg = 0;
+ groupThread->GetNumUnreadChildren(&numUnrMsg);
+ formattedCountUnrMsg.AppendInt(numUnrMsg);
+
+ // Add text to header.
+ aValue.AppendLiteral(u" (");
+ if (numUnrMsg) {
+ aValue.Append(formattedCountUnrMsg);
+ aValue.Append(u'/');
+ }
+
+ aValue.Append(formattedCountMsg);
+ aValue.Append(u')');
+ }
+ } else {
+ nsAutoString formattedCountString;
+ uint32_t numChildren = (groupThread) ? groupThread->NumRealChildren() : 0;
+ formattedCountString.AppendInt(numChildren);
+ aValue.Assign(formattedCountString);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::GetThreadContainingMsgHdr(msgHdr, pThread);
+
+ nsString hashKey;
+ nsresult rv = HashHdr(msgHdr, hashKey);
+ *pThread = nullptr;
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgThread> thread;
+ m_groupsTable.Get(hashKey, getter_AddRefs(thread));
+ thread.forget(pThread);
+ }
+
+ return (*pThread) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+int32_t nsMsgGroupView::FindLevelInThread(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startOfThread,
+ nsMsgViewIndex viewIndex) {
+ if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort))
+ return nsMsgDBView::FindLevelInThread(msgHdr, startOfThread, viewIndex);
+
+ return (startOfThread == viewIndex) ? 0 : 1;
+}
+
+bool nsMsgGroupView::GroupViewUsesDummyRow() {
+ // Return true to always use a header row as root grouped parent row.
+ return true;
+}
+
+NS_IMETHODIMP
+nsMsgGroupView::AddColumnHandler(const nsAString& column,
+ nsIMsgCustomColumnHandler* handler) {
+ nsMsgDBView::AddColumnHandler(column, handler);
+
+ // If the sortType is byCustom and the desired custom column is the one just
+ // registered, build the view.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort &&
+ m_sortType == nsMsgViewSortType::byCustom) {
+ nsAutoString curCustomColumn;
+ GetCurCustomColumn(curCustomColumn);
+ if (curCustomColumn == column) RebuildView(m_viewFlags);
+ }
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgGroupView.h b/comm/mailnews/base/src/nsMsgGroupView.h
new file mode 100644
index 0000000000..3b646f159b
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgGroupView.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsMsgGroupView_H_
+#define _nsMsgGroupView_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgDBView.h"
+#include "nsInterfaceHashtable.h"
+
+class nsIMsgThread;
+class nsMsgGroupThread;
+
+// Please note that if you override a method of nsMsgDBView,
+// you will most likely want to check the m_viewFlags to see if
+// we're grouping, and if not, call the base class implementation.
+class nsMsgGroupView : public nsMsgDBView {
+ public:
+ nsMsgGroupView();
+ virtual ~nsMsgGroupView();
+
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater);
+ NS_IMETHOD Close() override;
+ NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) override;
+
+ NS_IMETHOD GetCellProperties(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aProperties) override;
+ NS_IMETHOD GetRowProperties(int32_t aRow, nsAString& aProperties) override;
+ NS_IMETHOD CellTextForColumn(int32_t aRow, const nsAString& aColumnName,
+ nsAString& aValue) override;
+ NS_IMETHOD GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) override;
+ NS_IMETHOD AddColumnHandler(const nsAString& column,
+ nsIMsgCustomColumnHandler* handler) override;
+
+ protected:
+ virtual void InternalClose();
+ nsMsgGroupThread* AddHdrToThread(nsIMsgDBHdr* msgHdr, bool* pNewThread);
+ virtual nsresult HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey);
+ // Helper function to get age bucket for a hdr, useful when grouped by date.
+ nsresult GetAgeBucketValue(nsIMsgDBHdr* aMsgHdr, uint32_t* aAgeBucket,
+ bool rcvDate = false);
+ nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool /*ensureListed*/) override;
+ virtual int32_t FindLevelInThread(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startOfThread,
+ nsMsgViewIndex viewIndex) override;
+
+ // Returns true if we are grouped by a sort attribute that uses a dummy row.
+ bool GroupViewUsesDummyRow();
+ nsresult RebuildView(nsMsgViewFlagsTypeValue viewFlags);
+ virtual nsMsgGroupThread* CreateGroupThread(nsIMsgDatabase* db);
+
+ nsInterfaceHashtable<nsStringHashKey, nsIMsgThread> m_groupsTable;
+ PRExplodedTime m_lastCurExplodedTime{0};
+ bool m_dayChanged;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgI18N.cpp b/comm/mailnews/base/src/nsMsgI18N.cpp
new file mode 100644
index 0000000000..1c81456403
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgI18N.cpp
@@ -0,0 +1,403 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsICharsetConverterManager.h"
+#include "mozilla/Utf8.h"
+#include "nsIServiceManager.h"
+
+#include "nsISupports.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIMimeConverter.h"
+#include "nsMsgUtils.h"
+#include "nsMsgI18N.h"
+#include "nsILineInputStream.h"
+#include "nsMimeTypes.h"
+#include "nsString.h"
+#include "prmem.h"
+#include "plstr.h"
+#include "nsUTF8Utils.h"
+#include "nsNetUtil.h"
+#include "nsCRTGlue.h"
+#include "nsComponentManagerUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsIFileStreams.h"
+#include "../../intl/nsUTF7ToUnicode.h"
+#include "../../intl/nsMUTF7ToUnicode.h"
+#include "../../intl/nsUnicodeToMUTF7.h"
+
+#include <stdlib.h>
+#include <tuple>
+
+//
+// International functions necessary for composition
+//
+
+nsresult nsMsgI18NConvertFromUnicode(const nsACString& aCharset,
+ const nsAString& inString,
+ nsACString& outString,
+ bool aReportUencNoMapping) {
+ if (inString.IsEmpty()) {
+ outString.Truncate();
+ return NS_OK;
+ }
+
+ auto encoding = mozilla::Encoding::ForLabelNoReplacement(aCharset);
+ if (!encoding) {
+ return NS_ERROR_UCONV_NOCONV;
+ } else if (encoding == UTF_16LE_ENCODING || encoding == UTF_16BE_ENCODING) {
+ // We shouldn't ever ship anything in these encodings.
+ return NS_ERROR_UCONV_NOCONV;
+ }
+
+ nsresult rv;
+ std::tie(rv, std::ignore) = encoding->Encode(inString, outString);
+
+ if (rv == NS_OK_HAD_REPLACEMENTS) {
+ rv = aReportUencNoMapping ? NS_ERROR_UENC_NOMAPPING : NS_OK;
+ }
+
+ return rv;
+}
+
+nsresult nsMsgI18NConvertToUnicode(const nsACString& aCharset,
+ const nsACString& inString,
+ nsAString& outString) {
+ if (inString.IsEmpty()) {
+ outString.Truncate();
+ return NS_OK;
+ }
+ if (aCharset.IsEmpty()) {
+ // Despite its name, it also works for Latin-1.
+ CopyASCIItoUTF16(inString, outString);
+ return NS_OK;
+ }
+
+ if (aCharset.Equals("UTF-8", nsCaseInsensitiveCStringComparator)) {
+ return UTF_8_ENCODING->DecodeWithBOMRemoval(inString, outString);
+ }
+
+ // Look up Thunderbird's special aliases from charsetalias.properties.
+ nsresult rv;
+ nsCOMPtr<nsICharsetConverterManager> ccm =
+ do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString newCharset;
+ rv = ccm->GetCharsetAlias(PromiseFlatCString(aCharset).get(), newCharset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (newCharset.Equals("UTF-7", nsCaseInsensitiveCStringComparator)) {
+ // Special treatment for decoding UTF-7 since it's not handled by
+ // encoding_rs.
+ return CopyUTF7toUTF16(inString, outString);
+ }
+
+ auto encoding = mozilla::Encoding::ForLabelNoReplacement(newCharset);
+ if (!encoding) return NS_ERROR_UCONV_NOCONV;
+ return encoding->DecodeWithoutBOMHandling(inString, outString);
+}
+
+// This is used to decode UTF-7. No support for encoding in UTF-7.
+nsresult CopyUTF7toUTF16(const nsACString& aSrc, nsAString& aDest) {
+ // UTF-7 encoding size cannot be larger than the size in UTF-16.
+ nsUTF7ToUnicode converter;
+ int32_t inLen = aSrc.Length();
+ int32_t outLen = inLen;
+ aDest.SetLength(outLen);
+ converter.ConvertNoBuff(aSrc.BeginReading(), &inLen, aDest.BeginWriting(),
+ &outLen);
+ MOZ_ASSERT(inLen == (int32_t)aSrc.Length(),
+ "UTF-7 should not produce a longer output");
+ aDest.SetLength(outLen);
+ return NS_OK;
+}
+
+nsresult CopyUTF16toMUTF7(const nsAString& aSrc, nsACString& aDest) {
+#define IMAP_UTF7_BUF_LENGTH 100
+ nsUnicodeToMUTF7 converter;
+ static char buffer[IMAP_UTF7_BUF_LENGTH];
+ const char16_t* in = aSrc.BeginReading();
+ int32_t inLen = aSrc.Length();
+ int32_t outLen;
+ aDest.Truncate();
+ while (inLen > 0) {
+ outLen = IMAP_UTF7_BUF_LENGTH;
+ int32_t remaining = inLen;
+ converter.ConvertNoBuffNoErr(in, &remaining, buffer, &outLen);
+ aDest.Append(buffer, outLen);
+ in += remaining;
+ inLen -= remaining;
+ }
+ outLen = IMAP_UTF7_BUF_LENGTH;
+ converter.FinishNoBuff(buffer, &outLen);
+ if (outLen > 0) aDest.Append(buffer, outLen);
+ return NS_OK;
+}
+
+// Hacky function to use for IMAP folders where the name can be in
+// MUTF-7 or UTF-8.
+nsresult CopyFolderNameToUTF16(const nsACString& aSrc, nsAString& aDest) {
+ if (NS_IsAscii(aSrc.BeginReading(), aSrc.Length())) {
+ // An ASCII string may not be valid MUTF-7. For example, it may contain an
+ // ampersand not immediately followed by a dash which is invalid MUTF-7.
+ // Check for validity by converting to UTF-16 and then back to MUTF-7 and
+ // the result should be unchanged. If the MUTF-7 is invalid, treat it as
+ // UTF-8.
+ if (NS_SUCCEEDED(CopyMUTF7toUTF16(aSrc, aDest))) {
+ nsAutoCString tmp;
+ CopyUTF16toMUTF7(aDest, tmp);
+ if (aSrc.Equals(tmp)) return NS_OK;
+ }
+ }
+ // Do if aSrc non-ASCII or if ASCII but invalid MUTF-7.
+ CopyUTF8toUTF16(aSrc, aDest);
+ return NS_OK;
+}
+
+nsresult CopyMUTF7toUTF16(const nsACString& aSrc, nsAString& aDest) {
+ // MUTF-7 encoding size cannot be larger than the size in UTF-16.
+ nsMUTF7ToUnicode converter;
+ int32_t inLen = aSrc.Length();
+ int32_t outLen = inLen;
+ aDest.SetLength(outLen);
+ converter.ConvertNoBuff(aSrc.BeginReading(), &inLen, aDest.BeginWriting(),
+ &outLen);
+ MOZ_ASSERT(inLen == (int32_t)aSrc.Length(),
+ "MUTF-7 should not produce a longer output");
+ aDest.SetLength(outLen);
+ return NS_OK;
+}
+
+// MIME encoder, output string should be freed by PR_FREE
+// XXX : fix callers later to avoid allocation and copy
+char* nsMsgI18NEncodeMimePartIIStr(const char* header, bool structured,
+ const char* charset, int32_t fieldnamelen,
+ bool usemime) {
+ // No MIME, convert to the outgoing mail charset.
+ if (!usemime) {
+ nsAutoCString convertedStr;
+ if (NS_SUCCEEDED(nsMsgI18NConvertFromUnicode(
+ charset ? nsDependentCString(charset) : EmptyCString(),
+ NS_ConvertUTF8toUTF16(header), convertedStr)))
+ return PL_strdup(convertedStr.get());
+ else
+ return PL_strdup(header);
+ }
+
+ nsAutoCString encodedString;
+ nsresult res;
+ nsCOMPtr<nsIMimeConverter> converter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &res);
+ if (NS_SUCCEEDED(res) && nullptr != converter) {
+ res = converter->EncodeMimePartIIStr_UTF8(
+ nsDependentCString(header), structured, fieldnamelen,
+ nsIMimeConverter::MIME_ENCODED_WORD_SIZE, encodedString);
+ }
+
+ return NS_SUCCEEDED(res) ? PL_strdup(encodedString.get()) : nullptr;
+}
+
+// Return True if a charset is stateful (e.g. JIS).
+bool nsMsgI18Nstateful_charset(const char* charset) {
+ // TODO: use charset manager's service
+ return (PL_strcasecmp(charset, "ISO-2022-JP") == 0);
+}
+
+bool nsMsgI18Nmultibyte_charset(const char* charset) {
+ nsresult res;
+ nsCOMPtr<nsICharsetConverterManager> ccm =
+ do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &res);
+ bool result = false;
+
+ if (NS_SUCCEEDED(res)) {
+ nsAutoString charsetData;
+ res = ccm->GetCharsetData(charset, u".isMultibyte", charsetData);
+ if (NS_SUCCEEDED(res)) {
+ result = charsetData.LowerCaseEqualsLiteral("true");
+ }
+ }
+
+ return result;
+}
+
+bool nsMsgI18Ncheck_data_in_charset_range(const char* charset,
+ const char16_t* inString) {
+ if (!charset || !*charset || !inString || !*inString) return true;
+
+ bool res = true;
+
+ auto encoding =
+ mozilla::Encoding::ForLabelNoReplacement(nsDependentCString(charset));
+ if (!encoding) return false;
+ auto encoder = encoding->NewEncoder();
+
+ uint8_t buffer[512];
+ auto src = mozilla::MakeStringSpan(inString);
+ auto dst = mozilla::Span(buffer);
+ while (true) {
+ uint32_t result;
+ size_t read;
+ size_t written;
+ std::tie(result, read, written) =
+ encoder->EncodeFromUTF16WithoutReplacement(src, dst, false);
+ if (result == mozilla::kInputEmpty) {
+ // All converted successfully.
+ break;
+ } else if (result != mozilla::kOutputFull) {
+ // Didn't use all the input but the output isn't full, hence
+ // there was an unencodable character.
+ res = false;
+ break;
+ }
+ src = src.From(read);
+ // dst = dst.From(written); // Just overwrite output since we don't need it.
+ }
+
+ return res;
+}
+
+// Simple parser to parse META charset.
+// It only supports the case when the description is within one line.
+const char* nsMsgI18NParseMetaCharset(nsIFile* file) {
+ static char charset[nsIMimeConverter::MAX_CHARSET_NAME_LENGTH + 1];
+
+ *charset = '\0';
+
+ bool isDirectory = false;
+ file->IsDirectory(&isDirectory);
+ if (isDirectory) {
+ NS_ERROR("file is a directory");
+ return charset;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIFileInputStream> fileStream =
+ do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, charset);
+
+ rv = fileStream->Init(file, PR_RDONLY, 0664, false);
+ nsCOMPtr<nsILineInputStream> lineStream = do_QueryInterface(fileStream, &rv);
+
+ nsCString curLine;
+ bool more = true;
+ while (NS_SUCCEEDED(rv) && more) {
+ rv = lineStream->ReadLine(curLine, &more);
+ if (curLine.IsEmpty()) continue;
+
+ ToUpperCase(curLine);
+
+ if (curLine.Find("/HEAD") != -1) break;
+
+ if (curLine.Find("META") != -1 && curLine.Find("HTTP-EQUIV") != -1 &&
+ curLine.Find("CONTENT-TYPE") != -1 && curLine.Find("CHARSET") != -1) {
+ char* cp = (char*)PL_strchr(PL_strstr(curLine.get(), "CHARSET"), '=');
+ char* token = nullptr;
+ if (cp) {
+ char* newStr = cp + 1;
+ token = NS_strtok(" \"\'", &newStr);
+ }
+ if (token) {
+ PL_strncpy(charset, token, sizeof(charset));
+ charset[sizeof(charset) - 1] = '\0';
+
+ // this function cannot parse a file if it is really
+ // encoded by one of the following charsets
+ // so we can say that the charset label must be incorrect for
+ // the .html if we actually see those charsets parsed
+ // and we should ignore them
+ if (!PL_strncasecmp("UTF-16", charset, sizeof("UTF-16") - 1) ||
+ !PL_strncasecmp("UTF-32", charset, sizeof("UTF-32") - 1))
+ charset[0] = '\0';
+
+ break;
+ }
+ }
+ }
+
+ return charset;
+}
+
+nsresult nsMsgI18NShrinkUTF8Str(const nsCString& inString, uint32_t aMaxLength,
+ nsACString& outString) {
+ if (inString.IsEmpty()) {
+ outString.Truncate();
+ return NS_OK;
+ }
+ if (inString.Length() < aMaxLength) {
+ outString.Assign(inString);
+ return NS_OK;
+ }
+ NS_ASSERTION(mozilla::IsUtf8(inString), "Invalid UTF-8 string is inputted");
+ const char* start = inString.get();
+ const char* end = start + inString.Length();
+ const char* last = start + aMaxLength;
+ const char* cur = start;
+ const char* prev = nullptr;
+ bool err = false;
+ while (cur < last) {
+ prev = cur;
+ if (!UTF8CharEnumerator::NextChar(&cur, end, &err) || err) break;
+ }
+ if (!prev || err) {
+ outString.Truncate();
+ return NS_OK;
+ }
+ uint32_t len = prev - start;
+ outString.Assign(Substring(inString, 0, len));
+ return NS_OK;
+}
+
+void nsMsgI18NConvertRawBytesToUTF16(const nsCString& inString,
+ const nsACString& charset,
+ nsAString& outString) {
+ if (mozilla::IsUtf8(inString)) {
+ CopyUTF8toUTF16(inString, outString);
+ return;
+ }
+
+ nsresult rv = nsMsgI18NConvertToUnicode(charset, inString, outString);
+ if (NS_SUCCEEDED(rv)) return;
+
+ const char* cur = inString.BeginReading();
+ const char* end = inString.EndReading();
+ outString.Truncate();
+ while (cur < end) {
+ char c = *cur++;
+ if (c & char(0x80))
+ outString.Append(UCS2_REPLACEMENT_CHAR);
+ else
+ outString.Append(c);
+ }
+}
+
+void nsMsgI18NConvertRawBytesToUTF8(const nsCString& inString,
+ const nsACString& charset,
+ nsACString& outString) {
+ if (mozilla::IsUtf8(inString)) {
+ outString.Assign(inString);
+ return;
+ }
+
+ nsAutoString utf16Text;
+ nsresult rv = nsMsgI18NConvertToUnicode(charset, inString, utf16Text);
+ if (NS_SUCCEEDED(rv)) {
+ CopyUTF16toUTF8(utf16Text, outString);
+ return;
+ }
+
+ // EF BF BD (UTF-8 encoding of U+FFFD)
+ constexpr auto utf8ReplacementChar = "\357\277\275"_ns;
+ const char* cur = inString.BeginReading();
+ const char* end = inString.EndReading();
+ outString.Truncate();
+ while (cur < end) {
+ char c = *cur++;
+ if (c & char(0x80))
+ outString.Append(utf8ReplacementChar);
+ else
+ outString.Append(c);
+ }
+}
diff --git a/comm/mailnews/base/src/nsMsgI18N.h b/comm/mailnews/base/src/nsMsgI18N.h
new file mode 100644
index 0000000000..2268b64e26
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgI18N.h
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsMsgI18N_H_
+#define _nsMsgI18N_H_
+
+#include "nscore.h"
+#include "msgCore.h"
+#include "nsString.h"
+class nsIFile;
+
+/**
+ * Encode an input string into RFC 2047 form.
+ *
+ * @param header [IN] A header to encode.
+ * @param structured [IN] Specify the header is structured or non-structured
+ * field (See RFC-822).
+ * @param charset [IN] Charset name to convert.
+ * @param fieldnamelen [IN] Header field name length. (e.g. "From: " -> 6)
+ * @param usemime [IN] If false then apply charset conversion only no MIME
+ * encoding.
+ * @return Encoded buffer (in C string) or NULL in case of error.
+ */
+NS_MSG_BASE char* nsMsgI18NEncodeMimePartIIStr(const char* header,
+ bool structured,
+ const char* charset,
+ int32_t fieldnamelen,
+ bool usemime);
+
+/**
+ * Check if given charset is stateful (e.g. ISO-2022-JP).
+ *
+ * @param charset [IN] Charset name.
+ * @return True if stateful
+ */
+NS_MSG_BASE bool nsMsgI18Nstateful_charset(const char* charset);
+
+/**
+ * Check if given charset is multibyte (e.g. Shift_JIS, Big5).
+ *
+ * @param charset [IN] Charset name.
+ * @return True if multibyte
+ */
+NS_MSG_BASE bool nsMsgI18Nmultibyte_charset(const char* charset);
+
+/**
+ * Check the input (unicode) string is in a range of the given charset after the
+ * conversion. Note, do not use this for large string (e.g. message body) since
+ * this actually applies the conversion to the buffer.
+ *
+ * @param charset [IN] Charset to be converted.
+ * @param inString [IN] Input unicode string to be examined.
+ * @return True if the string can be converted within the charset range.
+ * False if one or more characters cannot be converted to the
+ * target charset.
+ */
+NS_MSG_BASE bool nsMsgI18Ncheck_data_in_charset_range(const char* charset,
+ const char16_t* inString);
+/**
+ * Convert from unicode to target charset.
+ *
+ * @param charset [IN] Charset name.
+ * @param inString [IN] Unicode string to convert.
+ * @param outString [OUT] Converted output string.
+ * @param aReportUencNoMapping [IN] Set encoder to report (instead of using
+ * replacement char on errors). Set to true
+ * to receive NS_ERROR_UENC_NOMAPPING when
+ * that happens. Note that
+ * NS_ERROR_UENC_NOMAPPING is a success code!
+ * @return nsresult.
+ */
+NS_MSG_BASE nsresult nsMsgI18NConvertFromUnicode(
+ const nsACString& aCharset, const nsAString& inString,
+ nsACString& outString, bool reportUencNoMapping = false);
+/**
+ * Convert from charset to unicode.
+ *
+ * @param charset [IN] Charset name.
+ * @param inString [IN] Input string to convert.
+ * @param outString [OUT] Output unicode string.
+ * @return nsresult.
+ */
+NS_MSG_BASE nsresult nsMsgI18NConvertToUnicode(const nsACString& aCharset,
+ const nsACString& inString,
+ nsAString& outString);
+/**
+ * Parse for META charset.
+ *
+ * @param file [IN] A nsIFile.
+ * @return A charset name or empty string if not found.
+ */
+NS_MSG_BASE const char* nsMsgI18NParseMetaCharset(nsIFile* file);
+
+/**
+ * Shrink the aStr to aMaxLength bytes. Note that this doesn't check whether
+ * the aUTF8Str is valid UTF-8 string.
+ *
+ * @param inString [IN] Input UTF-8 string (it must be valid UTF-8 string)
+ * @param aMaxLength [IN] Shrink to this length (it means bytes)
+ * @param outString [OUT] Shrunken UTF-8 string
+ * @return nsresult
+ */
+NS_MSG_BASE nsresult nsMsgI18NShrinkUTF8Str(const nsCString& inString,
+ uint32_t aMaxLength,
+ nsACString& outString);
+
+/*
+ * Convert raw bytes in header to UTF-16
+ *
+ * @param inString [IN] Input raw octets
+ * @param outString [OUT] Output UTF-16 string
+ */
+NS_MSG_BASE void nsMsgI18NConvertRawBytesToUTF16(const nsCString& inString,
+ const nsACString& charset,
+ nsAString& outString);
+
+/*
+ * Convert raw bytes in header to UTF-8
+ *
+ * @param inString [IN] Input raw octets
+ * @param outString [OUT] Output UTF-8 string
+ */
+NS_MSG_BASE void nsMsgI18NConvertRawBytesToUTF8(const nsCString& inString,
+ const nsACString& charset,
+ nsACString& outString);
+
+// Decode UTF-7 to UTF-16. No encoding supported.
+NS_MSG_BASE nsresult CopyUTF7toUTF16(const nsACString& aSrc, nsAString& aDest);
+
+// Convert between UTF-16 and modified UTF-7 used for IMAP.
+NS_MSG_BASE nsresult CopyFolderNameToUTF16(const nsACString& aSrc,
+ nsAString& aDest);
+NS_MSG_BASE nsresult CopyUTF16toMUTF7(const nsAString& aSrc, nsACString& aDest);
+NS_MSG_BASE nsresult CopyMUTF7toUTF16(const nsACString& aSrc, nsAString& aDest);
+
+#endif /* _nsMsgI18N_H_ */
diff --git a/comm/mailnews/base/src/nsMsgIdentity.cpp b/comm/mailnews/base/src/nsMsgIdentity.cpp
new file mode 100644
index 0000000000..a36107203d
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgIdentity.cpp
@@ -0,0 +1,645 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h" // for pre-compiled headers
+#include "nsMsgIdentity.h"
+#include "nsIPrefService.h"
+#include "nsString.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIMsgAccountManager.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "nsIMsgHeaderParser.h"
+#include "prprf.h"
+#include "nsISupportsPrimitives.h"
+#include "nsMsgUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIUUIDGenerator.h"
+#include "mozilla/Components.h"
+
+#define REL_FILE_PREF_SUFFIX "-rel"
+
+NS_IMPL_ISUPPORTS(nsMsgIdentity, nsIMsgIdentity)
+
+/*
+ * accessors for pulling values directly out of preferences
+ * instead of member variables, etc
+ */
+
+NS_IMETHODIMP
+nsMsgIdentity::GetKey(nsACString& aKey) {
+ aKey = mKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetKey(const nsACString& identityKey) {
+ mKey = identityKey;
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString branchName;
+ branchName.AssignLiteral("mail.identity.");
+ branchName += mKey;
+ branchName.Append('.');
+ rv = prefs->GetBranch(branchName.get(), getter_AddRefs(mPrefBranch));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = prefs->GetBranch("mail.identity.default.",
+ getter_AddRefs(mDefPrefBranch));
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetUID(nsACString& uid) {
+ bool hasValue;
+ nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasValue) {
+ return mPrefBranch->GetCharPref("uid", uid);
+ }
+
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ mozilla::components::UUIDGenerator::Service();
+ NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+
+ nsID id;
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char idString[NSID_LENGTH];
+ id.ToProvidedString(idString);
+
+ uid.AppendASCII(idString + 1, NSID_LENGTH - 3);
+ return SetUID(uid);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetUID(const nsACString& uid) {
+ bool hasValue;
+ nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasValue) {
+ return NS_ERROR_ABORT;
+ }
+ return SetCharAttribute("uid", uid);
+}
+
+nsresult nsMsgIdentity::GetIdentityName(nsAString& idName) {
+ idName.AssignLiteral("");
+ // Try to use "fullname <email>" as the name.
+ nsresult rv = GetFullAddress(idName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If a non-empty label exists, append it.
+ nsString label;
+ rv = GetLabel(label);
+ if (NS_SUCCEEDED(rv) &&
+ !label.IsEmpty()) { // TODO: this should be localizable
+ idName.AppendLiteral(" (");
+ idName.Append(label);
+ idName.Append(')');
+ }
+
+ if (!idName.IsEmpty()) return NS_OK;
+
+ // If we still found nothing to use, use our key.
+ return ToString(idName);
+}
+
+nsresult nsMsgIdentity::GetFullAddress(nsAString& fullAddress) {
+ nsAutoString fullName;
+ nsresult rv = GetFullName(fullName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString email;
+ rv = GetEmail(email);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (fullName.IsEmpty() && email.IsEmpty()) {
+ fullAddress.Truncate();
+ } else {
+ nsCOMPtr<msgIAddressObject> mailbox;
+ nsCOMPtr<nsIMsgHeaderParser> headerParser(
+ mozilla::components::HeaderParser::Service());
+ NS_ENSURE_TRUE(headerParser, NS_ERROR_UNEXPECTED);
+ headerParser->MakeMailboxObject(fullName, NS_ConvertUTF8toUTF16(email),
+ getter_AddRefs(mailbox));
+ mailbox->ToString(fullAddress);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::ToString(nsAString& aResult) {
+ aResult.AssignLiteral("[nsIMsgIdentity: ");
+ aResult.Append(NS_ConvertASCIItoUTF16(mKey));
+ aResult.Append(']');
+ return NS_OK;
+}
+
+/* Identity attribute accessors */
+
+NS_IMETHODIMP
+nsMsgIdentity::GetSignature(nsIFile** sig) {
+ bool gotRelPref;
+ nsresult rv =
+ NS_GetPersistentFile("sig_file" REL_FILE_PREF_SUFFIX, "sig_file", nullptr,
+ gotRelPref, sig, mPrefBranch);
+ if (NS_SUCCEEDED(rv) && !gotRelPref) {
+ rv = NS_SetPersistentFile("sig_file" REL_FILE_PREF_SUFFIX, "sig_file", *sig,
+ mPrefBranch);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to write signature file pref.");
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetSignature(nsIFile* sig) {
+ nsresult rv = NS_OK;
+ if (sig)
+ rv = NS_SetPersistentFile("sig_file" REL_FILE_PREF_SUFFIX, "sig_file", sig,
+ mPrefBranch);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::ClearAllValues() {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsTArray<nsCString> prefNames;
+ nsresult rv = mPrefBranch->GetChildList("", prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ mPrefBranch->ClearUserPref(prefName.get());
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_IDPREF_STR(EscapedVCard, "escapedVCard")
+NS_IMPL_IDPREF_STR(SmtpServerKey, "smtpServer")
+NS_IMPL_IDPREF_WSTR(FullName, "fullName")
+NS_IMPL_IDPREF_STR(Email, "useremail")
+NS_IMPL_IDPREF_BOOL(CatchAll, "catchAll")
+NS_IMPL_IDPREF_STR(CatchAllHint, "catchAllHint")
+NS_IMPL_IDPREF_WSTR(Label, "label")
+NS_IMPL_IDPREF_STR(ReplyTo, "reply_to")
+NS_IMPL_IDPREF_WSTR(Organization, "organization")
+NS_IMPL_IDPREF_BOOL(ComposeHtml, "compose_html")
+NS_IMPL_IDPREF_BOOL(AttachVCard, "attach_vcard")
+NS_IMPL_IDPREF_BOOL(AttachSignature, "attach_signature")
+NS_IMPL_IDPREF_WSTR(HtmlSigText, "htmlSigText")
+NS_IMPL_IDPREF_BOOL(HtmlSigFormat, "htmlSigFormat")
+
+NS_IMPL_IDPREF_BOOL(AutoQuote, "auto_quote")
+NS_IMPL_IDPREF_INT(ReplyOnTop, "reply_on_top")
+NS_IMPL_IDPREF_BOOL(SigBottom, "sig_bottom")
+NS_IMPL_IDPREF_BOOL(SigOnForward, "sig_on_fwd")
+NS_IMPL_IDPREF_BOOL(SigOnReply, "sig_on_reply")
+
+NS_IMPL_IDPREF_INT(SignatureDate, "sig_date")
+
+NS_IMPL_IDPREF_BOOL(DoFcc, "fcc")
+
+NS_IMPL_FOLDERPREF_STR(FccFolder, "fcc_folder", "Sent"_ns,
+ nsMsgFolderFlags::SentMail)
+NS_IMPL_IDPREF_STR(FccFolderPickerMode, "fcc_folder_picker_mode")
+NS_IMPL_IDPREF_BOOL(FccReplyFollowsParent, "fcc_reply_follows_parent")
+NS_IMPL_IDPREF_STR(DraftsFolderPickerMode, "drafts_folder_picker_mode")
+NS_IMPL_IDPREF_STR(ArchivesFolderPickerMode, "archives_folder_picker_mode")
+NS_IMPL_IDPREF_STR(TmplFolderPickerMode, "tmpl_folder_picker_mode")
+
+NS_IMPL_IDPREF_BOOL(BccSelf, "bcc_self")
+NS_IMPL_IDPREF_BOOL(BccOthers, "bcc_other")
+NS_IMPL_IDPREF_STR(BccList, "bcc_other_list")
+
+NS_IMPL_IDPREF_BOOL(SuppressSigSep, "suppress_signature_separator")
+
+NS_IMPL_IDPREF_BOOL(DoCc, "doCc")
+NS_IMPL_IDPREF_STR(DoCcList, "doCcList")
+
+NS_IMPL_IDPREF_BOOL(AttachPgpKey, "attachPgpKey")
+NS_IMPL_IDPREF_BOOL(SendAutocryptHeaders, "sendAutocryptHeaders")
+NS_IMPL_IDPREF_BOOL(AutoEncryptDrafts, "autoEncryptDrafts")
+NS_IMPL_IDPREF_BOOL(ProtectSubject, "protectSubject")
+NS_IMPL_IDPREF_INT(EncryptionPolicy, "encryptionpolicy")
+NS_IMPL_IDPREF_BOOL(SignMail, "sign_mail")
+
+NS_IMETHODIMP
+nsMsgIdentity::GetDoBcc(bool* aValue) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv = mPrefBranch->GetBoolPref("doBcc", aValue);
+ if (NS_SUCCEEDED(rv)) return rv;
+
+ bool bccSelf = false;
+ GetBccSelf(&bccSelf);
+
+ bool bccOthers = false;
+ GetBccOthers(&bccOthers);
+
+ nsCString others;
+ GetBccList(others);
+
+ *aValue = bccSelf || (bccOthers && !others.IsEmpty());
+
+ return SetDoBcc(*aValue);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetDoBcc(bool aValue) {
+ return SetBoolAttribute("doBcc", aValue);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetDoBccList(nsACString& aValue) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString val;
+ nsresult rv = mPrefBranch->GetCharPref("doBccList", val);
+ aValue = val;
+ if (NS_SUCCEEDED(rv)) return rv;
+
+ bool bccSelf = false;
+ rv = GetBccSelf(&bccSelf);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (bccSelf) GetEmail(aValue);
+
+ bool bccOthers = false;
+ rv = GetBccOthers(&bccOthers);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString others;
+ rv = GetBccList(others);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (bccOthers && !others.IsEmpty()) {
+ if (bccSelf) aValue.Append(',');
+ aValue.Append(others);
+ }
+
+ return SetDoBccList(aValue);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::SetDoBccList(const nsACString& aValue) {
+ return SetCharAttribute("doBccList", aValue);
+}
+
+NS_IMPL_FOLDERPREF_STR(DraftFolder, "draft_folder", "Drafts"_ns,
+ nsMsgFolderFlags::Drafts)
+NS_IMPL_FOLDERPREF_STR(ArchiveFolder, "archive_folder", "Archives"_ns,
+ nsMsgFolderFlags::Archive)
+NS_IMPL_FOLDERPREF_STR(StationeryFolder, "stationery_folder", "Templates"_ns,
+ nsMsgFolderFlags::Templates)
+
+NS_IMPL_IDPREF_BOOL(ArchiveEnabled, "archive_enabled")
+NS_IMPL_IDPREF_INT(ArchiveGranularity, "archive_granularity")
+NS_IMPL_IDPREF_BOOL(ArchiveKeepFolderStructure, "archive_keep_folder_structure")
+
+NS_IMPL_IDPREF_BOOL(ShowSaveMsgDlg, "showSaveMsgDlg")
+NS_IMPL_IDPREF_STR(DirectoryServer, "directoryServer")
+NS_IMPL_IDPREF_BOOL(OverrideGlobalPref, "overrideGlobal_Pref")
+NS_IMPL_IDPREF_BOOL(AutocompleteToMyDomain, "autocompleteToMyDomain")
+
+NS_IMPL_IDPREF_BOOL(Valid, "valid")
+
+nsresult nsMsgIdentity::getFolderPref(const char* prefname, nsACString& retval,
+ const nsACString& folderName,
+ uint32_t folderflag) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv = mPrefBranch->GetStringPref(prefname, EmptyCString(), 0, retval);
+ if (NS_SUCCEEDED(rv) && !retval.IsEmpty()) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(retval, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // Make sure that folder hierarchy is built so that legitimate parent-child
+ // relationship is established.
+ folder->GetServer(getter_AddRefs(server));
+ if (server) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsCOMPtr<nsIMsgFolder> deferredToRootFolder;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ server->GetRootMsgFolder(getter_AddRefs(deferredToRootFolder));
+ // check if we're using a deferred account - if not, use the uri;
+ // otherwise, fall through to code that will fix this pref.
+ if (rootFolder == deferredToRootFolder) {
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = server->GetMsgFolderFromURI(folder, retval,
+ getter_AddRefs(msgFolder));
+ return NS_SUCCEEDED(rv) ? msgFolder->GetURI(retval) : rv;
+ }
+ }
+ }
+
+ // if the server doesn't exist, fall back to the default pref.
+ rv = mDefPrefBranch->GetStringPref(prefname, EmptyCString(), 0, retval);
+ if (NS_SUCCEEDED(rv) && !retval.IsEmpty())
+ return setFolderPref(prefname, retval, folderflag);
+
+ // here I think we need to create a uri for the folder on the
+ // default server for this identity.
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgIncomingServer>> servers;
+ rv = accountManager->GetServersForIdentity(this, servers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (servers.IsEmpty()) {
+ // if there are no servers for this identity, return generic failure.
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIMsgIncomingServer> server(servers[0]);
+ bool defaultToServer;
+ server->GetDefaultCopiesAndFoldersPrefsToServer(&defaultToServer);
+ // if we should default to special folders on the server,
+ // use the local folders server
+ if (!defaultToServer) {
+ rv = accountManager->GetLocalFoldersServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ // this will get the deferred to server's root folder, if "server"
+ // is deferred, e.g., using the pop3 global inbox.
+ rv = server->GetRootMsgFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (rootFolder) {
+ rv = rootFolder->GetURI(retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ retval.Append('/');
+ retval.Append(folderName);
+ return setFolderPref(prefname, retval, folderflag);
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsMsgIdentity::setFolderPref(const char* prefname,
+ const nsACString& value,
+ uint32_t folderflag) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString oldpref;
+ nsresult rv;
+ nsCOMPtr<nsIMsgFolder> folder;
+
+ if (folderflag == nsMsgFolderFlags::SentMail) {
+ // Clear the temporary return receipt filter so that the new filter
+ // rule can be recreated (by ConfigureTemporaryFilters()).
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgIncomingServer>> servers;
+ rv = accountManager->GetServersForIdentity(this, servers);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!servers.IsEmpty()) {
+ servers[0]->ClearTemporaryReturnReceiptsFilter();
+ // okay to fail; no need to check for return code
+ }
+ }
+
+ // get the old folder, and clear the special folder flag on it
+ rv = mPrefBranch->GetStringPref(prefname, EmptyCString(), 0, oldpref);
+ if (NS_SUCCEEDED(rv) && !oldpref.IsEmpty()) {
+ rv = GetOrCreateFolder(oldpref, getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv)) {
+ rv = folder->ClearFlag(folderflag);
+ }
+ }
+
+ // set the new folder, and set the special folder flags on it
+ rv = SetUnicharAttribute(prefname, NS_ConvertUTF8toUTF16(value));
+ if (NS_SUCCEEDED(rv) && !value.IsEmpty()) {
+ rv = GetOrCreateFolder(value, getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv)) rv = folder->SetFlag(folderflag);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIdentity::SetUnicharAttribute(const char* aName,
+ const nsAString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ if (!val.IsEmpty())
+ return mPrefBranch->SetStringPref(aName, NS_ConvertUTF16toUTF8(val));
+
+ mPrefBranch->ClearUserPref(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::GetUnicharAttribute(const char* aName,
+ nsAString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString valueUtf8;
+ if (NS_FAILED(
+ mPrefBranch->GetStringPref(aName, EmptyCString(), 0, valueUtf8)))
+ mDefPrefBranch->GetStringPref(aName, EmptyCString(), 0, valueUtf8);
+ CopyUTF8toUTF16(valueUtf8, val);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::SetCharAttribute(const char* aName,
+ const nsACString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ if (!val.IsEmpty()) return mPrefBranch->SetCharPref(aName, val);
+
+ mPrefBranch->ClearUserPref(aName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::GetCharAttribute(const char* aName,
+ nsACString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString tmpVal;
+ if (NS_FAILED(mPrefBranch->GetCharPref(aName, tmpVal)))
+ mDefPrefBranch->GetCharPref(aName, tmpVal);
+ val = tmpVal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::SetBoolAttribute(const char* aName, bool val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ return mPrefBranch->SetBoolPref(aName, val);
+}
+
+NS_IMETHODIMP nsMsgIdentity::GetBoolAttribute(const char* aName, bool* val) {
+ NS_ENSURE_ARG_POINTER(val);
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ *val = false;
+
+ if (NS_FAILED(mPrefBranch->GetBoolPref(aName, val)))
+ mDefPrefBranch->GetBoolPref(aName, val);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIdentity::SetIntAttribute(const char* aName, int32_t val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ return mPrefBranch->SetIntPref(aName, val);
+}
+
+NS_IMETHODIMP nsMsgIdentity::GetIntAttribute(const char* aName, int32_t* val) {
+ NS_ENSURE_ARG_POINTER(val);
+
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ *val = 0;
+
+ if (NS_FAILED(mPrefBranch->GetIntPref(aName, val)))
+ mDefPrefBranch->GetIntPref(aName, val);
+
+ return NS_OK;
+}
+
+#define COPY_IDENTITY_FILE_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsresult macro_rv; \
+ nsCOMPtr<nsIFile> macro_spec; \
+ macro_rv = SRC_ID->MACRO_GETTER(getter_AddRefs(macro_spec)); \
+ if (NS_SUCCEEDED(macro_rv)) this->MACRO_SETTER(macro_spec); \
+ }
+
+#define COPY_IDENTITY_INT_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsresult macro_rv; \
+ int32_t macro_oldInt; \
+ macro_rv = SRC_ID->MACRO_GETTER(&macro_oldInt); \
+ if (NS_SUCCEEDED(macro_rv)) this->MACRO_SETTER(macro_oldInt); \
+ }
+
+#define COPY_IDENTITY_BOOL_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsresult macro_rv; \
+ bool macro_oldBool; \
+ macro_rv = SRC_ID->MACRO_GETTER(&macro_oldBool); \
+ if (NS_SUCCEEDED(macro_rv)) this->MACRO_SETTER(macro_oldBool); \
+ }
+
+#define COPY_IDENTITY_STR_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsCString macro_oldStr; \
+ nsresult macro_rv; \
+ macro_rv = SRC_ID->MACRO_GETTER(macro_oldStr); \
+ if (NS_SUCCEEDED(macro_rv)) { \
+ this->MACRO_SETTER(macro_oldStr); \
+ } \
+ }
+
+#define COPY_IDENTITY_WSTR_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \
+ { \
+ nsString macro_oldStr; \
+ nsresult macro_rv; \
+ macro_rv = SRC_ID->MACRO_GETTER(macro_oldStr); \
+ if (NS_SUCCEEDED(macro_rv)) { \
+ this->MACRO_SETTER(macro_oldStr); \
+ } \
+ }
+
+NS_IMETHODIMP
+nsMsgIdentity::Copy(nsIMsgIdentity* identity) {
+ NS_ENSURE_ARG_POINTER(identity);
+
+ COPY_IDENTITY_BOOL_VALUE(identity, GetComposeHtml, SetComposeHtml)
+ COPY_IDENTITY_STR_VALUE(identity, GetEmail, SetEmail)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetCatchAll, SetCatchAll)
+ COPY_IDENTITY_WSTR_VALUE(identity, GetLabel, SetLabel)
+ COPY_IDENTITY_STR_VALUE(identity, GetReplyTo, SetReplyTo)
+ COPY_IDENTITY_WSTR_VALUE(identity, GetFullName, SetFullName)
+ COPY_IDENTITY_WSTR_VALUE(identity, GetOrganization, SetOrganization)
+ COPY_IDENTITY_STR_VALUE(identity, GetDraftFolder, SetDraftFolder)
+ COPY_IDENTITY_STR_VALUE(identity, GetArchiveFolder, SetArchiveFolder)
+ COPY_IDENTITY_STR_VALUE(identity, GetFccFolder, SetFccFolder)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetFccReplyFollowsParent,
+ SetFccReplyFollowsParent)
+ COPY_IDENTITY_STR_VALUE(identity, GetStationeryFolder, SetStationeryFolder)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetArchiveEnabled, SetArchiveEnabled)
+ COPY_IDENTITY_INT_VALUE(identity, GetArchiveGranularity,
+ SetArchiveGranularity)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetArchiveKeepFolderStructure,
+ SetArchiveKeepFolderStructure)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAttachSignature, SetAttachSignature)
+ COPY_IDENTITY_FILE_VALUE(identity, GetSignature, SetSignature)
+ COPY_IDENTITY_WSTR_VALUE(identity, GetHtmlSigText, SetHtmlSigText)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetHtmlSigFormat, SetHtmlSigFormat)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAutoQuote, SetAutoQuote)
+ COPY_IDENTITY_INT_VALUE(identity, GetReplyOnTop, SetReplyOnTop)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSigBottom, SetSigBottom)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSigOnForward, SetSigOnForward)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSigOnReply, SetSigOnReply)
+ COPY_IDENTITY_INT_VALUE(identity, GetSignatureDate, SetSignatureDate)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAttachVCard, SetAttachVCard)
+ COPY_IDENTITY_STR_VALUE(identity, GetEscapedVCard, SetEscapedVCard)
+ COPY_IDENTITY_STR_VALUE(identity, GetSmtpServerKey, SetSmtpServerKey)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSuppressSigSep, SetSuppressSigSep)
+
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAttachPgpKey, SetAttachPgpKey)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSendAutocryptHeaders,
+ SetSendAutocryptHeaders)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetAutoEncryptDrafts, SetAutoEncryptDrafts)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetProtectSubject, SetProtectSubject)
+ COPY_IDENTITY_INT_VALUE(identity, GetEncryptionPolicy, SetEncryptionPolicy)
+ COPY_IDENTITY_BOOL_VALUE(identity, GetSignMail, SetSignMail)
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetRequestReturnReceipt(bool* aVal) {
+ NS_ENSURE_ARG_POINTER(aVal);
+
+ bool useCustomPrefs = false;
+ nsresult rv = GetBoolAttribute("use_custom_prefs", &useCustomPrefs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (useCustomPrefs)
+ return GetBoolAttribute("request_return_receipt_on", aVal);
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefs->GetBoolPref("mail.receipt.request_return_receipt_on", aVal);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetReceiptHeaderType(int32_t* aType) {
+ NS_ENSURE_ARG_POINTER(aType);
+
+ bool useCustomPrefs = false;
+ nsresult rv = GetBoolAttribute("use_custom_prefs", &useCustomPrefs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (useCustomPrefs)
+ return GetIntAttribute("request_receipt_header_type", aType);
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefs->GetIntPref("mail.receipt.request_header_type", aType);
+}
+
+NS_IMETHODIMP
+nsMsgIdentity::GetRequestDSN(bool* aVal) {
+ NS_ENSURE_ARG_POINTER(aVal);
+
+ bool useCustomPrefs = false;
+ nsresult rv = GetBoolAttribute("dsn_use_custom_prefs", &useCustomPrefs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (useCustomPrefs) return GetBoolAttribute("dsn_always_request_on", aVal);
+
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefs->GetBoolPref("mail.dsn.always_request_on", aVal);
+}
diff --git a/comm/mailnews/base/src/nsMsgIdentity.h b/comm/mailnews/base/src/nsMsgIdentity.h
new file mode 100644
index 0000000000..1b9e0f1635
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgIdentity.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef nsMsgIdentity_h___
+#define nsMsgIdentity_h___
+
+#include "nsIMsgIdentity.h"
+#include "nsIPrefBranch.h"
+#include "msgCore.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+class nsMsgIdentity final : public nsIMsgIdentity {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGIDENTITY
+
+ private:
+ ~nsMsgIdentity() {}
+ nsCString mKey;
+ nsCOMPtr<nsIPrefBranch> mPrefBranch;
+ nsCOMPtr<nsIPrefBranch> mDefPrefBranch;
+
+ protected:
+ nsresult getFolderPref(const char* pref, nsACString& retval,
+ const nsACString& folderName, uint32_t folderFlag);
+ nsresult setFolderPref(const char* pref, const nsACString& retval,
+ uint32_t folderFlag);
+};
+
+#define NS_IMPL_IDPREF_STR(_postfix, _prefname) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(nsACString& retval) { \
+ return GetCharAttribute(_prefname, retval); \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(const nsACString& value) { \
+ return SetCharAttribute(_prefname, value); \
+ }
+
+#define NS_IMPL_IDPREF_WSTR(_postfix, _prefname) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(nsAString& retval) { \
+ return GetUnicharAttribute(_prefname, retval); \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(const nsAString& value) { \
+ return SetUnicharAttribute(_prefname, value); \
+ }
+
+#define NS_IMPL_IDPREF_BOOL(_postfix, _prefname) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(bool* retval) { \
+ return GetBoolAttribute(_prefname, retval); \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(bool value) { \
+ return mPrefBranch->SetBoolPref(_prefname, value); \
+ }
+
+#define NS_IMPL_IDPREF_INT(_postfix, _prefname) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(int32_t* retval) { \
+ return GetIntAttribute(_prefname, retval); \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(int32_t value) { \
+ return mPrefBranch->SetIntPref(_prefname, value); \
+ }
+
+#define NS_IMPL_FOLDERPREF_STR(_postfix, _prefname, _foldername, _flag) \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Get##_postfix(nsACString& retval) { \
+ nsresult rv; \
+ nsCString folderPref; \
+ rv = getFolderPref(_prefname, folderPref, _foldername, _flag); \
+ retval = folderPref; \
+ return rv; \
+ } \
+ NS_IMETHODIMP \
+ nsMsgIdentity::Set##_postfix(const nsACString& value) { \
+ return setFolderPref(_prefname, value, _flag); \
+ }
+
+#endif /* nsMsgIdentity_h___ */
diff --git a/comm/mailnews/base/src/nsMsgIncomingServer.cpp b/comm/mailnews/base/src/nsMsgIncomingServer.cpp
new file mode 100644
index 0000000000..0d914ffbd9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgIncomingServer.cpp
@@ -0,0 +1,2142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgIncomingServer.h"
+#include "nscore.h"
+#include "plstr.h"
+#include "prmem.h"
+#include "prprf.h"
+
+#include "nsIServiceManager.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsMemory.h"
+#include "nsISupportsPrimitives.h"
+
+#include "nsIMsgBiffManager.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgDBFolder.h"
+#include "nsIMsgFolderCache.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIMsgFolderCacheElement.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgFilterService.h"
+#include "nsIMsgProtocolInfo.h"
+#include "nsIPrefService.h"
+#include "nsIRelativeFilePref.h"
+#include "mozilla/nsRelativeFilePref.h"
+#include "nsIDocShell.h"
+#include "nsIAuthPrompt.h"
+#include "nsNetUtil.h"
+#include "nsIWindowWatcher.h"
+#include "nsIStringBundle.h"
+#include "nsIMsgHdr.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsILoginInfo.h"
+#include "nsILoginManager.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIMsgMdnGenerator.h"
+#include "nsMsgUtils.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "mozilla/Components.h"
+#include "mozilla/Services.h"
+#include "nsIMsgFilter.h"
+#include "nsIObserverService.h"
+#include "mozilla/Unused.h"
+#include "nsIUUIDGenerator.h"
+#include "nsArrayUtils.h"
+
+#define PORT_NOT_SET -1
+
+nsMsgIncomingServer::nsMsgIncomingServer()
+ : m_rootFolder(nullptr),
+ m_downloadedHdrs(50),
+ m_numMsgsDownloaded(0),
+ m_biffState(nsIMsgFolder::nsMsgBiffState_Unknown),
+ m_serverBusy(false),
+ m_canHaveFilters(true),
+ m_displayStartupPage(true),
+ mPerformingBiff(false) {}
+
+nsresult nsMsgIncomingServer::Init() {
+ // We need to know when the password manager changes.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED);
+
+ observerService->AddObserver(this, "passwordmgr-storage-changed", false);
+ observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ return NS_OK;
+}
+
+nsMsgIncomingServer::~nsMsgIncomingServer() {}
+
+NS_IMPL_ISUPPORTS(nsMsgIncomingServer, nsIMsgIncomingServer,
+ nsISupportsWeakReference, nsIObserver)
+
+/**
+ * Observe() receives notifications for all accounts, not just this server's
+ * account. So we ignore all notifications not intended for this server.
+ * When the state of the password manager changes we need to clear the
+ * this server's password from the cache in case the user just changed or
+ * removed the password or username.
+ * Oauth2 servers often automatically change the password manager's stored
+ * password (the token).
+ */
+NS_IMETHODIMP
+nsMsgIncomingServer::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ nsresult rv;
+ if (strcmp(aTopic, "passwordmgr-storage-changed") == 0) {
+ nsAutoString otherFullName;
+ nsAutoString otherUserName;
+ // Check that the notification is for this server.
+ nsCOMPtr<nsILoginInfo> loginInfo = do_QueryInterface(aSubject);
+ if (loginInfo) {
+ // The login info for this server has been removed with aData being
+ // "removeLogin" or "removeAllLogins".
+ loginInfo->GetOrigin(otherFullName);
+ loginInfo->GetUsername(otherUserName);
+ } else {
+ // Probably a 2 element array containing old and new login info due to
+ // aData being "modifyLogin". E.g., a user has modified password or
+ // username in the password manager or an OAuth2 token string has
+ // automatically changed.
+ nsCOMPtr<nsIArray> logins = do_QueryInterface(aSubject);
+ if (logins) {
+ // Only need to look at names in first array element (login info before
+ // any modification) since the user might have changed the username as
+ // found in the 2nd elements. (The hostname can't be modified in the
+ // password manager.)
+ nsCOMPtr<nsILoginInfo> login;
+ logins->QueryElementAt(0, NS_GET_IID(nsILoginInfo),
+ getter_AddRefs(login));
+ if (login) {
+ login->GetOrigin(otherFullName);
+ login->GetUsername(otherUserName);
+ }
+ }
+ }
+ if (!otherFullName.IsEmpty()) {
+ nsAutoCString thisHostname;
+ nsAutoCString thisUsername;
+ GetHostName(thisHostname);
+ GetUsername(thisUsername);
+ nsAutoCString thisFullName;
+ GetType(thisFullName);
+ if (thisFullName.EqualsLiteral("pop3")) {
+ // Note: POP3 now handled by MsgIncomingServer.jsm so does not occur.
+ MOZ_ASSERT_UNREACHABLE("pop3 should not use nsMsgIncomingServer");
+ thisFullName = "mailbox://"_ns + thisHostname;
+ } else {
+ thisFullName += "://"_ns + thisHostname;
+ }
+ if (!thisFullName.Equals(NS_ConvertUTF16toUTF8(otherFullName)) ||
+ !thisUsername.Equals(NS_ConvertUTF16toUTF8(otherUserName))) {
+ // Not for this server; keep this server's cached password.
+ return NS_OK;
+ }
+ } else if (NS_strcmp(aData, u"hostSavingDisabled") != 0) {
+ // "hostSavingDisabled" only occurs during test_smtpServer.js and
+ // expects the password to be removed from memory cache. Otherwise, we
+ // don't have enough information to decide to remove the cached
+ // password, so keep it.
+ return NS_OK;
+ }
+ // When nsMsgImapIncomingServer::ForgetSessionPassword called with
+ // parameter modifyLogin true and if the server uses OAuth2, it causes the
+ // password to not be cleared from cache. This is needed by autosync. When
+ // the aData paremater of Observe() is not "modifyLogin" but is
+ // e.g., "removeLogin" or "removeAllLogins", ForgetSessionPassword(false)
+ // will still clear the cached password regardless of authentication method.
+ rv = ForgetSessionPassword(NS_strcmp(aData, u"modifyLogin") == 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
+ // Now remove ourselves from the observer service as well.
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED);
+
+ observerService->RemoveObserver(this, "passwordmgr-storage-changed");
+ observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetServerBusy(bool aServerBusy) {
+ m_serverBusy = aServerBusy;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetServerBusy(bool* aServerBusy) {
+ NS_ENSURE_ARG_POINTER(aServerBusy);
+ *aServerBusy = m_serverBusy;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetKey(nsACString& serverKey) {
+ serverKey = m_serverKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetKey(const nsACString& serverKey) {
+ m_serverKey.Assign(serverKey);
+
+ // in order to actually make use of the key, we need the prefs
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString branchName;
+ branchName.AssignLiteral("mail.server.");
+ branchName.Append(m_serverKey);
+ branchName.Append('.');
+ rv = prefs->GetBranch(branchName.get(), getter_AddRefs(mPrefBranch));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return prefs->GetBranch("mail.server.default.",
+ getter_AddRefs(mDefPrefBranch));
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetUID(nsACString& uid) {
+ bool hasValue;
+ nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasValue) {
+ return GetCharValue("uid", uid);
+ }
+
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ mozilla::components::UUIDGenerator::Service();
+ NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+
+ nsID id;
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char idString[NSID_LENGTH];
+ id.ToProvidedString(idString);
+
+ uid.AppendASCII(idString + 1, NSID_LENGTH - 3);
+ return SetUID(uid);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetUID(const nsACString& uid) {
+ bool hasValue;
+ nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasValue) {
+ return NS_ERROR_ABORT;
+ }
+ return SetCharValue("uid", uid);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetRootFolder(nsIMsgFolder* aRootFolder) {
+ m_rootFolder = aRootFolder;
+ return NS_OK;
+}
+
+// this will return the root folder of this account,
+// even if this server is deferred.
+NS_IMETHODIMP
+nsMsgIncomingServer::GetRootFolder(nsIMsgFolder** aRootFolder) {
+ NS_ENSURE_ARG_POINTER(aRootFolder);
+ if (!m_rootFolder) {
+ nsresult rv = CreateRootFolder();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aRootFolder = m_rootFolder);
+ return NS_OK;
+}
+
+// this will return the root folder of the deferred to account,
+// if this server is deferred.
+NS_IMETHODIMP
+nsMsgIncomingServer::GetRootMsgFolder(nsIMsgFolder** aRootMsgFolder) {
+ return GetRootFolder(aRootMsgFolder);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::PerformExpand(nsIMsgWindow* aMsgWindow) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgIncomingServer::VerifyLogon(nsIUrlListener* aUrlListener,
+ nsIMsgWindow* aMsgWindow, nsIURI** aURL) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::PerformBiff(nsIMsgWindow* aMsgWindow) {
+ // This has to be implemented in the derived class, but in case someone
+ // doesn't implement it just return not implemented.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetNewMessages(nsIMsgFolder* aFolder,
+ nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aUrlListener) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+ return aFolder->GetNewMessages(aMsgWindow, aUrlListener);
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetPerformingBiff(bool* aPerformingBiff) {
+ NS_ENSURE_ARG_POINTER(aPerformingBiff);
+ *aPerformingBiff = mPerformingBiff;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetPerformingBiff(bool aPerformingBiff) {
+ mPerformingBiff = aPerformingBiff;
+ return NS_OK;
+}
+
+NS_IMPL_GETSET(nsMsgIncomingServer, BiffState, uint32_t, m_biffState)
+
+NS_IMETHODIMP nsMsgIncomingServer::WriteToFolderCache(
+ nsIMsgFolderCache* folderCache) {
+ nsresult rv = NS_OK;
+ if (m_rootFolder) {
+ rv = m_rootFolder->WriteToFolderCache(folderCache, true /* deep */);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::Shutdown() {
+ nsresult rv = CloseCachedConnections();
+ mFilterPlugin = nullptr;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mFilterList) {
+ // close the filter log stream
+ rv = mFilterList->SetLogStream(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mFilterList = nullptr;
+ }
+
+ if (mSpamSettings) {
+ // close the spam log stream
+ rv = mSpamSettings->SetLogStream(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSpamSettings = nullptr;
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::CloseCachedConnections() {
+ // derived class should override if they cache connections.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetDownloadMessagesAtStartup(bool* getMessagesAtStartup) {
+ // derived class should override if they need to do this.
+ *getMessagesAtStartup = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanHaveFilters(bool* canHaveFilters) {
+ NS_ENSURE_ARG_POINTER(canHaveFilters);
+ *canHaveFilters = m_canHaveFilters;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetCanHaveFilters(bool aCanHaveFilters) {
+ m_canHaveFilters = aCanHaveFilters;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanBeDefaultServer(bool* canBeDefaultServer) {
+ // derived class should override if they need to do this.
+ *canBeDefaultServer = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanSearchMessages(bool* canSearchMessages) {
+ // derived class should override if they need to do this.
+ NS_ENSURE_ARG_POINTER(canSearchMessages);
+ *canSearchMessages = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanCompactFoldersOnServer(
+ bool* canCompactFoldersOnServer) {
+ // derived class should override if they need to do this.
+ NS_ENSURE_ARG_POINTER(canCompactFoldersOnServer);
+ *canCompactFoldersOnServer = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCanUndoDeleteOnServer(bool* canUndoDeleteOnServer) {
+ // derived class should override if they need to do this.
+ NS_ENSURE_ARG_POINTER(canUndoDeleteOnServer);
+ *canUndoDeleteOnServer = true;
+ return NS_OK;
+}
+
+// construct <localStoreType>://[<username>@]<hostname
+NS_IMETHODIMP
+nsMsgIncomingServer::GetServerURI(nsACString& aResult) {
+ nsresult rv;
+ rv = GetLocalStoreType(aResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aResult.AppendLiteral("://");
+
+ nsCString username;
+ rv = GetUsername(username);
+ if (NS_SUCCEEDED(rv) && !username.IsEmpty()) {
+ nsCString escapedUsername;
+ MsgEscapeString(username, nsINetUtil::ESCAPE_XALPHAS, escapedUsername);
+ // not all servers have a username
+ aResult.Append(escapedUsername);
+ aResult.Append('@');
+ }
+
+ nsCString hostname;
+ rv = GetHostName(hostname);
+ if (NS_SUCCEEDED(rv) && !hostname.IsEmpty()) {
+ nsCString escapedHostname;
+ MsgEscapeString(hostname, nsINetUtil::ESCAPE_URL_PATH, escapedHostname);
+ // not all servers have a hostname
+ aResult.Append(escapedHostname);
+ }
+ return NS_OK;
+}
+
+// helper routine to create local folder on disk, if it doesn't exist.
+nsresult nsMsgIncomingServer::CreateLocalFolder(const nsAString& folderName) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ nsresult rv = GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgFolder> child;
+ rv = rootFolder->GetChildNamed(folderName, getter_AddRefs(child));
+ if (child) return NS_OK;
+ nsCOMPtr<nsIMsgPluggableStore> msgStore;
+ rv = GetMsgStore(getter_AddRefs(msgStore));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgStore->CreateFolder(rootFolder, folderName, getter_AddRefs(child));
+}
+
+nsresult nsMsgIncomingServer::CreateRootFolder() {
+ nsresult rv;
+ // get the URI from the incoming server
+ nsCString serverUri;
+ rv = GetServerURI(serverUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetOrCreateFolder(serverUri, getter_AddRefs(m_rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetBoolValue(const char* prefname, bool* val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ NS_ENSURE_ARG_POINTER(val);
+ *val = false;
+
+ if (NS_FAILED(mPrefBranch->GetBoolPref(prefname, val)))
+ mDefPrefBranch->GetBoolPref(prefname, val);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetBoolValue(const char* prefname, bool val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ bool defaultValue;
+ nsresult rv = mDefPrefBranch->GetBoolPref(prefname, &defaultValue);
+
+ if (NS_SUCCEEDED(rv) && val == defaultValue)
+ mPrefBranch->ClearUserPref(prefname);
+ else
+ rv = mPrefBranch->SetBoolPref(prefname, val);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetIntValue(const char* prefname, int32_t* val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ NS_ENSURE_ARG_POINTER(val);
+ *val = 0;
+
+ if (NS_FAILED(mPrefBranch->GetIntPref(prefname, val)))
+ mDefPrefBranch->GetIntPref(prefname, val);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetFileValue(const char* aRelPrefName,
+ const char* aAbsPrefName,
+ nsIFile** aLocalFile) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ // Get the relative first
+ nsCOMPtr<nsIRelativeFilePref> relFilePref;
+ nsresult rv = mPrefBranch->GetComplexValue(aRelPrefName,
+ NS_GET_IID(nsIRelativeFilePref),
+ getter_AddRefs(relFilePref));
+ if (relFilePref) {
+ rv = relFilePref->GetFile(aLocalFile);
+ NS_ASSERTION(*aLocalFile, "An nsIRelativeFilePref has no file.");
+ if (NS_SUCCEEDED(rv)) (*aLocalFile)->Normalize();
+ } else {
+ rv = mPrefBranch->GetComplexValue(aAbsPrefName, NS_GET_IID(nsIFile),
+ reinterpret_cast<void**>(aLocalFile));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIRelativeFilePref> relFilePref =
+ new mozilla::nsRelativeFilePref();
+ mozilla::Unused << relFilePref->SetFile(*aLocalFile);
+ mozilla::Unused << relFilePref->SetRelativeToKey(
+ nsLiteralCString(NS_APP_USER_PROFILE_50_DIR));
+
+ rv = mPrefBranch->SetComplexValue(
+ aRelPrefName, NS_GET_IID(nsIRelativeFilePref), relFilePref);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetFileValue(const char* aRelPrefName,
+ const char* aAbsPrefName,
+ nsIFile* aLocalFile) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ // Write the relative path.
+ nsCOMPtr<nsIRelativeFilePref> relFilePref = new mozilla::nsRelativeFilePref();
+ mozilla::Unused << relFilePref->SetFile(aLocalFile);
+ mozilla::Unused << relFilePref->SetRelativeToKey(
+ nsLiteralCString(NS_APP_USER_PROFILE_50_DIR));
+
+ nsresult rv = mPrefBranch->SetComplexValue(
+ aRelPrefName, NS_GET_IID(nsIRelativeFilePref), relFilePref);
+ if (NS_FAILED(rv)) return rv;
+
+ return mPrefBranch->SetComplexValue(aAbsPrefName, NS_GET_IID(nsIFile),
+ aLocalFile);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetIntValue(const char* prefname, int32_t val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ int32_t defaultVal;
+ nsresult rv = mDefPrefBranch->GetIntPref(prefname, &defaultVal);
+
+ if (NS_SUCCEEDED(rv) && defaultVal == val)
+ mPrefBranch->ClearUserPref(prefname);
+ else
+ rv = mPrefBranch->SetIntPref(prefname, val);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetCharValue(const char* prefname, nsACString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString tmpVal;
+ if (NS_FAILED(mPrefBranch->GetCharPref(prefname, tmpVal)))
+ mDefPrefBranch->GetCharPref(prefname, tmpVal);
+ val = tmpVal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetUnicharValue(const char* prefname, nsAString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsCString valueUtf8;
+ if (NS_FAILED(
+ mPrefBranch->GetStringPref(prefname, EmptyCString(), 0, valueUtf8)))
+ mDefPrefBranch->GetStringPref(prefname, EmptyCString(), 0, valueUtf8);
+ CopyUTF8toUTF16(valueUtf8, val);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetCharValue(const char* prefname, const nsACString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ if (val.IsEmpty()) {
+ mPrefBranch->ClearUserPref(prefname);
+ return NS_OK;
+ }
+
+ nsCString defaultVal;
+ nsresult rv = mDefPrefBranch->GetCharPref(prefname, defaultVal);
+
+ if (NS_SUCCEEDED(rv) && defaultVal.Equals(val))
+ mPrefBranch->ClearUserPref(prefname);
+ else
+ rv = mPrefBranch->SetCharPref(prefname, val);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetUnicharValue(const char* prefname,
+ const nsAString& val) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ if (val.IsEmpty()) {
+ mPrefBranch->ClearUserPref(prefname);
+ return NS_OK;
+ }
+
+ nsCString defaultVal;
+ nsresult rv =
+ mDefPrefBranch->GetStringPref(prefname, EmptyCString(), 0, defaultVal);
+
+ if (NS_SUCCEEDED(rv) && defaultVal.Equals(NS_ConvertUTF16toUTF8(val)))
+ mPrefBranch->ClearUserPref(prefname);
+ else
+ rv = mPrefBranch->SetStringPref(prefname, NS_ConvertUTF16toUTF8(val));
+
+ return rv;
+}
+
+// pretty name is the display name to show to the user
+NS_IMETHODIMP
+nsMsgIncomingServer::GetPrettyName(nsAString& retval) {
+ nsresult rv = GetUnicharValue("name", retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if there's no name, then just return the hostname
+ return retval.IsEmpty() ? GetConstructedPrettyName(retval) : rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetPrettyName(const nsAString& value) {
+ SetUnicharValue("name", value);
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) rootFolder->SetPrettyName(value);
+ return NS_OK;
+}
+
+// construct the pretty name to show to the user if they haven't
+// specified one. This should be overridden for news and mail.
+NS_IMETHODIMP
+nsMsgIncomingServer::GetConstructedPrettyName(nsAString& retval) {
+ nsCString username;
+ nsresult rv = GetUsername(username);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!username.IsEmpty()) {
+ CopyASCIItoUTF16(username, retval);
+ retval.AppendLiteral(" on ");
+ }
+
+ nsCString hostname;
+ rv = GetHostName(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ retval.Append(NS_ConvertASCIItoUTF16(hostname));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ToString(nsAString& aResult) {
+ aResult.AssignLiteral("[nsIMsgIncomingServer: ");
+ aResult.Append(NS_ConvertASCIItoUTF16(m_serverKey));
+ aResult.Append(']');
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetPassword(const nsAString& aPassword) {
+ m_password = aPassword;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetPassword(nsAString& aPassword) {
+ aPassword = m_password;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetServerRequiresPasswordForBiff(
+ bool* aServerRequiresPasswordForBiff) {
+ NS_ENSURE_ARG_POINTER(aServerRequiresPasswordForBiff);
+ *aServerRequiresPasswordForBiff = true;
+ return NS_OK;
+}
+
+// This sets m_password if we find a password in the pw mgr.
+nsresult nsMsgIncomingServer::GetPasswordWithoutUI() {
+ nsresult rv;
+ nsCOMPtr<nsILoginManager> loginMgr(
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the current server URI
+ nsCString currServerUri;
+ rv = GetLocalStoreType(currServerUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ currServerUri.AppendLiteral("://");
+
+ nsCString temp;
+ rv = GetHostName(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ currServerUri.Append(temp);
+
+ NS_ConvertUTF8toUTF16 currServer(currServerUri);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(currServer, EmptyString(), currServer, logins);
+
+ // Login manager can produce valid fails, e.g. NS_ERROR_ABORT when a user
+ // cancels the master password dialog. Therefore handle that here, but don't
+ // warn about it.
+ if (NS_FAILED(rv)) return rv;
+ uint32_t numLogins = logins.Length();
+
+ // Don't abort here, if we didn't find any or failed, then we'll just have
+ // to prompt.
+ if (numLogins > 0) {
+ nsCString serverCUsername;
+ rv = GetUsername(serverCUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ConvertUTF8toUTF16 serverUsername(serverCUsername);
+
+ nsString username;
+ for (uint32_t i = 0; i < numLogins; ++i) {
+ rv = logins[i]->GetUsername(username);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (username.Equals(serverUsername)) {
+ nsString password;
+ rv = logins[i]->GetPassword(password);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ m_password = password;
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetPasswordWithUI(const nsAString& aPromptMessage,
+ const nsAString& aPromptTitle,
+ nsAString& aPassword) {
+ nsresult rv = NS_OK;
+
+ if (m_password.IsEmpty()) {
+ // let's see if we have the password in the password manager and
+ // can avoid this prompting thing. This makes it easier to get embedders
+ // to get up and running w/o a password prompting UI.
+ rv = GetPasswordWithoutUI();
+ // If GetPasswordWithoutUI returns NS_ERROR_ABORT, the most likely case
+ // is the user canceled getting the master password, so just return
+ // straight away, as they won't want to get prompted again.
+ if (rv == NS_ERROR_ABORT) return NS_MSG_PASSWORD_PROMPT_CANCELLED;
+ }
+ if (m_password.IsEmpty()) {
+ nsCOMPtr<nsIAuthPrompt> authPrompt =
+ do_GetService("@mozilla.org/messenger/msgAuthPrompt;1");
+ if (authPrompt) {
+ // prompt the user for the password
+ nsCString serverUri;
+ rv = GetLocalStoreType(serverUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ serverUri.AppendLiteral("://");
+ nsCString temp;
+ rv = GetUsername(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!temp.IsEmpty()) {
+ nsCString escapedUsername;
+ MsgEscapeString(temp, nsINetUtil::ESCAPE_XALPHAS, escapedUsername);
+ serverUri.Append(escapedUsername);
+ serverUri.Append('@');
+ }
+
+ rv = GetHostName(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ serverUri.Append(temp);
+
+ // we pass in the previously used password, if any, into PromptPassword
+ // so that it will appear as ******. This means we can't use an nsString
+ // and getter_Copies.
+ char16_t* uniPassword = nullptr;
+ if (!aPassword.IsEmpty()) uniPassword = ToNewUnicode(aPassword);
+
+ bool okayValue = true;
+ rv = authPrompt->PromptPassword(PromiseFlatString(aPromptTitle).get(),
+ PromiseFlatString(aPromptMessage).get(),
+ NS_ConvertASCIItoUTF16(serverUri).get(),
+ nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY,
+ &uniPassword, &okayValue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!okayValue) // if the user pressed cancel, just return an empty
+ // string;
+ {
+ aPassword.Truncate();
+ return NS_MSG_PASSWORD_PROMPT_CANCELLED;
+ }
+
+ // we got a password back...so remember it
+ rv = SetPassword(nsDependentString(uniPassword));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ PR_FREEIF(uniPassword);
+ } // if we got a prompt dialog
+ else
+ return NS_ERROR_FAILURE;
+ } // if the password is empty
+ return GetPassword(aPassword);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ForgetPassword() {
+ nsresult rv;
+ nsCOMPtr<nsILoginManager> loginMgr =
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the current server URI
+ nsCString currServerUri;
+ rv = GetLocalStoreType(currServerUri);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ currServerUri.AppendLiteral("://");
+
+ nsCString temp;
+ rv = GetHostName(temp);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ currServerUri.Append(temp);
+
+ NS_ConvertUTF8toUTF16 currServer(currServerUri);
+
+ nsCString serverCUsername;
+ rv = GetUsername(serverCUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ConvertUTF8toUTF16 serverUsername(serverCUsername);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(currServer, EmptyString(), currServer, logins);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // There should only be one-login stored for this url, however just in case
+ // there isn't.
+ nsString username;
+ for (uint32_t i = 0; i < logins.Length(); ++i) {
+ rv = logins[i]->GetUsername(username);
+ int32_t atPos = serverUsername.FindChar('@');
+ if (NS_SUCCEEDED(rv) &&
+ (username.Equals(serverUsername) ||
+ StringHead(serverUsername, atPos).Equals(username))) {
+ // If this fails, just continue, we'll still want to remove the password
+ // from our local cache.
+ loginMgr->RemoveLogin(logins[i]);
+ }
+ }
+
+ return SetPassword(EmptyString());
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ForgetSessionPassword(bool modifyLogin) {
+ m_password.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetDefaultLocalPath(nsIFile* aDefaultLocalPath) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return protocolInfo->SetDefaultLocalPath(aDefaultLocalPath);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetLocalPath(nsIFile** aLocalPath) {
+ nsresult rv;
+
+ // if the local path has already been set, use it
+ rv = GetFileValue("directory-rel", "directory", aLocalPath);
+ if (NS_SUCCEEDED(rv) && *aLocalPath) return rv;
+
+ // otherwise, create the path using the protocol info.
+ // note we are using the
+ // hostname, unless that directory exists.
+ // this should prevent all collisions.
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> localPath;
+ rv = protocolInfo->GetDefaultLocalPath(getter_AddRefs(localPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = localPath->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ if (rv == NS_ERROR_FILE_ALREADY_EXISTS) rv = NS_OK;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString hostname;
+ rv = GetHostName(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // set the leaf name to "dummy", and then call MakeUnique with a suggested
+ // leaf name
+ rv = localPath->AppendNative(hostname);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = localPath->CreateUnique(nsIFile::DIRECTORY_TYPE, 0755);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetLocalPath(localPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ localPath.forget(aLocalPath);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetMsgStore(nsIMsgPluggableStore** aMsgStore) {
+ NS_ENSURE_ARG_POINTER(aMsgStore);
+ if (!m_msgStore) {
+ nsCString storeContractID;
+ nsresult rv;
+ // We don't want there to be a default pref, I think, since
+ // we can't change the default. We may want no pref to mean
+ // berkeley store, and then set the store pref off of some sort
+ // of default when creating a server. But we need to make sure
+ // that we do always write a store pref.
+ GetCharValue("storeContractID", storeContractID);
+ if (storeContractID.IsEmpty()) {
+ storeContractID.AssignLiteral("@mozilla.org/msgstore/berkeleystore;1");
+ SetCharValue("storeContractID", storeContractID);
+ }
+
+ // After someone starts using the pluggable store, we can no longer
+ // change the value.
+ SetBoolValue("canChangeStoreType", false);
+
+ // Right now, we just have one pluggable store per server. If we want
+ // to support multiple, this pref could be a list of pluggable store
+ // contract id's.
+ m_msgStore = do_CreateInstance(storeContractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ NS_IF_ADDREF(*aMsgStore = m_msgStore);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetLocalPath(nsIFile* aLocalPath) {
+ NS_ENSURE_ARG_POINTER(aLocalPath);
+ nsresult rv = aLocalPath->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ if (rv == NS_ERROR_FILE_ALREADY_EXISTS) rv = NS_OK;
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetFileValue("directory-rel", "directory", aLocalPath);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetLocalStoreType(nsACString& aResult) {
+ MOZ_ASSERT_UNREACHABLE(
+ "nsMsgIncomingServer superclass not implementing GetLocalStoreType!");
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetLocalDatabaseType(nsACString& aResult) {
+ MOZ_ASSERT_UNREACHABLE(
+ "nsMsgIncomingServer superclass not implementing GetLocalDatabaseType!");
+ return NS_ERROR_UNEXPECTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetAccountManagerChrome(nsAString& aResult) {
+ aResult.AssignLiteral("am-main.xhtml");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::Equals(nsIMsgIncomingServer* server, bool* _retval) {
+ nsresult rv;
+
+ NS_ENSURE_ARG_POINTER(server);
+ NS_ENSURE_ARG_POINTER(_retval);
+
+ nsCString key1;
+ nsCString key2;
+
+ rv = GetKey(key1);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = server->GetKey(key2);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // compare the server keys
+ *_retval = key1.Equals(key2, nsCaseInsensitiveCStringComparator);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ClearAllValues() {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsTArray<nsCString> prefNames;
+ nsresult rv = mPrefBranch->GetChildList("", prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ mPrefBranch->ClearUserPref(prefName.get());
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::RemoveFiles() {
+ // IMPORTANT, see bug #77652
+ // TODO: Decide what to do for deferred accounts.
+ nsCString deferredToAccount;
+ GetCharValue("deferred_to_account", deferredToAccount);
+ bool isDeferredTo = true;
+ GetIsDeferredTo(&isDeferredTo);
+ if (!deferredToAccount.IsEmpty() || isDeferredTo) {
+ NS_ASSERTION(false, "shouldn't remove files for a deferred account");
+ return NS_ERROR_FAILURE;
+ }
+ nsCOMPtr<nsIFile> localPath;
+ nsresult rv = GetLocalPath(getter_AddRefs(localPath));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return localPath->Remove(true);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetFilterList(nsIMsgFilterList* aFilterList) {
+ mFilterList = aFilterList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (!mFilterList) {
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ // use GetRootFolder so for deferred pop3 accounts, we'll get the filters
+ // file from the deferred account, not the deferred to account,
+ // so that filters will still be per-server.
+ nsresult rv = GetRootFolder(getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString filterType;
+ rv = GetCharValue("filter.type", filterType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!filterType.IsEmpty() && !filterType.EqualsLiteral("default")) {
+ nsAutoCString contractID("@mozilla.org/filterlist;1?type=");
+ contractID += filterType;
+ ToLowerCase(contractID);
+ mFilterList = do_CreateInstance(contractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mFilterList->SetFolder(msgFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ADDREF(*aResult = mFilterList);
+ return NS_OK;
+ }
+
+ // The default case, a local folder, is a bit special. It requires
+ // more initialization.
+
+ nsCOMPtr<nsIFile> thisFolder;
+ rv = msgFolder->GetFilePath(getter_AddRefs(thisFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mFilterFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = mFilterFile->InitWithFile(thisFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mFilterFile->AppendNative("msgFilterRules.dat"_ns);
+
+ bool fileExists;
+ mFilterFile->Exists(&fileExists);
+ if (!fileExists) {
+ nsCOMPtr<nsIFile> oldFilterFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = oldFilterFile->InitWithFile(thisFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ oldFilterFile->AppendNative("rules.dat"_ns);
+
+ oldFilterFile->Exists(&fileExists);
+ if (fileExists) // copy rules.dat --> msgFilterRules.dat
+ {
+ rv = oldFilterFile->CopyToNative(thisFolder, "msgFilterRules.dat"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = filterService->OpenFilterList(mFilterFile, msgFolder, aMsgWindow,
+ getter_AddRefs(mFilterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aResult = mFilterList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetEditableFilterList(
+ nsIMsgFilterList* aEditableFilterList) {
+ mEditableFilterList = aEditableFilterList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ if (!mEditableFilterList) {
+ bool editSeparate;
+ nsresult rv = GetBoolValue("filter.editable.separate", &editSeparate);
+ if (NS_FAILED(rv) || !editSeparate)
+ return GetFilterList(aMsgWindow, aResult);
+
+ nsCString filterType;
+ rv = GetCharValue("filter.editable.type", filterType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString contractID("@mozilla.org/filterlist;1?type=");
+ contractID += filterType;
+ ToLowerCase(contractID);
+ mEditableFilterList = do_CreateInstance(contractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ // use GetRootFolder so for deferred pop3 accounts, we'll get the filters
+ // file from the deferred account, not the deferred to account,
+ // so that filters will still be per-server.
+ rv = GetRootFolder(getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mEditableFilterList->SetFolder(msgFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ADDREF(*aResult = mEditableFilterList);
+ return NS_OK;
+ }
+
+ NS_IF_ADDREF(*aResult = mEditableFilterList);
+ return NS_OK;
+}
+
+// If the hostname contains ':' (like hostname:1431)
+// then parse and set the port number.
+nsresult nsMsgIncomingServer::InternalSetHostName(const nsACString& aHostname,
+ const char* prefName) {
+ nsCString hostname;
+ hostname = aHostname;
+ if (hostname.CountChar(':') == 1) {
+ int32_t colonPos = hostname.FindChar(':');
+ nsAutoCString portString(Substring(hostname, colonPos));
+ hostname.SetLength(colonPos);
+ nsresult err;
+ int32_t port = portString.ToInteger(&err);
+ if (NS_SUCCEEDED(err)) SetPort(port);
+ }
+ return SetCharValue(prefName, hostname);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::OnUserOrHostNameChanged(const nsACString& oldName,
+ const nsACString& newName,
+ bool hostnameChanged) {
+ nsresult rv;
+
+ // 1. Reset password so that users are prompted for new password for the new
+ // user/host.
+ int32_t atPos = newName.FindChar('@');
+ if (hostnameChanged) {
+ ForgetPassword();
+ }
+
+ // 2. Replace all occurrences of old name in the acct name with the new one.
+ nsString acctName;
+ rv = GetPrettyName(acctName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // 3. Clear the clientid because the user or host have changed.
+ SetClientid(EmptyCString());
+
+ // Will be generated again when used.
+ mPrefBranch->ClearUserPref("spamActionTargetAccount");
+
+ // If new username contains @ then better do not update the account name.
+ if (acctName.IsEmpty() || (!hostnameChanged && (atPos != kNotFound)))
+ return NS_OK;
+
+ atPos = acctName.FindChar('@');
+
+ // get previous username and hostname
+ nsCString userName, hostName;
+ if (hostnameChanged) {
+ rv = GetUsername(userName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ hostName.Assign(oldName);
+ } else {
+ userName.Assign(oldName);
+ rv = GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // switch corresponding part of the account name to the new name...
+ if (!hostnameChanged && (atPos != kNotFound)) {
+ // ...if username changed and the previous username was equal to the part
+ // of the account name before @
+ if (StringHead(acctName, atPos).Equals(NS_ConvertASCIItoUTF16(userName)))
+ acctName.Replace(0, userName.Length(), NS_ConvertASCIItoUTF16(newName));
+ }
+ if (hostnameChanged) {
+ // ...if hostname changed and the previous hostname was equal to the part
+ // of the account name after @, or to the whole account name
+ if (atPos == kNotFound)
+ atPos = 0;
+ else
+ atPos += 1;
+ if (Substring(acctName, atPos).Equals(NS_ConvertASCIItoUTF16(hostName))) {
+ acctName.Replace(atPos, acctName.Length() - atPos,
+ NS_ConvertASCIItoUTF16(newName));
+ }
+ }
+
+ return SetPrettyName(acctName);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetHostName(const nsACString& aHostname) {
+ nsCString oldName;
+ nsresult rv = GetHostName(oldName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = InternalSetHostName(aHostname, "hostname");
+
+ if (!oldName.IsEmpty() &&
+ !aHostname.Equals(oldName, nsCaseInsensitiveCStringComparator))
+ rv = OnUserOrHostNameChanged(oldName, aHostname, true);
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetHostName(nsACString& aResult) {
+ nsresult rv = GetCharValue("hostname", aResult);
+ if (aResult.CountChar(':') == 1) {
+ // gack, we need to reformat the hostname - SetHostName will do that
+ SetHostName(aResult);
+ rv = GetCharValue("hostname", aResult);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetUsername(const nsACString& aUsername) {
+ nsCString oldName;
+ nsresult rv = GetUsername(oldName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!oldName.IsEmpty() && !oldName.Equals(aUsername)) {
+ // If only username changed and the new name just added a domain we can keep
+ // the password.
+ int32_t atPos = aUsername.FindChar('@');
+ if ((atPos == kNotFound) ||
+ !StringHead(NS_ConvertASCIItoUTF16(aUsername), atPos)
+ .Equals(NS_ConvertASCIItoUTF16(oldName))) {
+ ForgetPassword();
+ }
+ rv = SetCharValue("userName", aUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = OnUserOrHostNameChanged(oldName, aUsername, false);
+ } else {
+ rv = SetCharValue("userName", aUsername);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetUsername(nsACString& aResult) {
+ return GetCharValue("userName", aResult);
+}
+
+#define BIFF_PREF_NAME "check_new_mail"
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetDoBiff(bool* aDoBiff) {
+ NS_ENSURE_ARG_POINTER(aDoBiff);
+
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv;
+
+ rv = mPrefBranch->GetBoolPref(BIFF_PREF_NAME, aDoBiff);
+ if (NS_SUCCEEDED(rv)) return rv;
+
+ // if the pref isn't set, use the default
+ // value based on the protocol
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = protocolInfo->GetDefaultDoBiff(aDoBiff);
+ // note, don't call SetDoBiff()
+ // since we keep changing our minds on
+ // if biff should be on or off, let's keep the ability
+ // to change the default in future builds.
+ // if we call SetDoBiff() here, it will be in the users prefs.
+ // and we can't do anything after that.
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetDoBiff(bool aDoBiff) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ // Update biffManager immediately, no restart required. Adding/removing
+ // existing/non-existing server is handled without error checking.
+ nsresult rv;
+ nsCOMPtr<nsIMsgBiffManager> biffService =
+ do_GetService("@mozilla.org/messenger/biffManager;1", &rv);
+ if (NS_SUCCEEDED(rv) && biffService) {
+ if (aDoBiff)
+ (void)biffService->AddServerBiff(this);
+ else
+ (void)biffService->RemoveServerBiff(this);
+ }
+
+ return mPrefBranch->SetBoolPref(BIFF_PREF_NAME, aDoBiff);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetPort(int32_t* aPort) {
+ NS_ENSURE_ARG_POINTER(aPort);
+
+ nsresult rv;
+ rv = GetIntValue("port", aPort);
+ // We can't use a port of 0, because the URI parsing code fails.
+ if (*aPort != PORT_NOT_SET && *aPort) return rv;
+
+ // if the port isn't set, use the default
+ // port based on the protocol
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ rv = GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool useSSLPort = (socketType == nsMsgSocketType::SSL);
+ return protocolInfo->GetDefaultServerPort(useSSLPort, aPort);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetPort(int32_t aPort) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ rv = GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool useSSLPort = (socketType == nsMsgSocketType::SSL);
+
+ int32_t defaultPort;
+ protocolInfo->GetDefaultServerPort(useSSLPort, &defaultPort);
+ return SetIntValue("port", aPort == defaultPort ? PORT_NOT_SET : aPort);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetProtocolInfo(nsIMsgProtocolInfo** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ nsCString type;
+ nsresult rv = GetType(type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString contractid(NS_MSGPROTOCOLINFO_CONTRACTID_PREFIX);
+ contractid.Append(type);
+
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo =
+ do_GetService(contractid.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ protocolInfo.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetRetentionSettings(
+ nsIMsgRetentionSettings** settings) {
+ NS_ENSURE_ARG_POINTER(settings);
+ nsMsgRetainByPreference retainByPreference;
+ int32_t daysToKeepHdrs = 0;
+ int32_t numHeadersToKeep = 0;
+ int32_t daysToKeepBodies = 0;
+ bool cleanupBodiesByDays = false;
+ bool applyToFlaggedMessages = false;
+ nsresult rv = NS_OK;
+ // Create an empty retention settings object,
+ // get the settings from the server prefs, and init the object from the prefs.
+ nsCOMPtr<nsIMsgRetentionSettings> retentionSettings =
+ do_CreateInstance("@mozilla.org/msgDatabase/retentionSettings;1");
+ if (retentionSettings) {
+ rv = GetIntValue("retainBy", (int32_t*)&retainByPreference);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetIntValue("numHdrsToKeep", &numHeadersToKeep);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetIntValue("daysToKeepHdrs", &daysToKeepHdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetIntValue("daysToKeepBodies", &daysToKeepBodies);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetBoolValue("cleanupBodies", &cleanupBodiesByDays);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = GetBoolValue("applyToFlaggedMessages", &applyToFlaggedMessages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ retentionSettings->SetRetainByPreference(retainByPreference);
+ retentionSettings->SetNumHeadersToKeep((uint32_t)numHeadersToKeep);
+ retentionSettings->SetDaysToKeepBodies(daysToKeepBodies);
+ retentionSettings->SetDaysToKeepHdrs(daysToKeepHdrs);
+ retentionSettings->SetCleanupBodiesByDays(cleanupBodiesByDays);
+ retentionSettings->SetApplyToFlaggedMessages(applyToFlaggedMessages);
+ } else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ NS_IF_ADDREF(*settings = retentionSettings);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetRetentionSettings(
+ nsIMsgRetentionSettings* settings) {
+ nsMsgRetainByPreference retainByPreference;
+ uint32_t daysToKeepHdrs = 0;
+ uint32_t numHeadersToKeep = 0;
+ uint32_t daysToKeepBodies = 0;
+ bool cleanupBodiesByDays = false;
+ bool applyToFlaggedMessages = false;
+ settings->GetRetainByPreference(&retainByPreference);
+ settings->GetNumHeadersToKeep(&numHeadersToKeep);
+ settings->GetDaysToKeepBodies(&daysToKeepBodies);
+ settings->GetDaysToKeepHdrs(&daysToKeepHdrs);
+ settings->GetCleanupBodiesByDays(&cleanupBodiesByDays);
+ settings->GetApplyToFlaggedMessages(&applyToFlaggedMessages);
+ nsresult rv = SetIntValue("retainBy", retainByPreference);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetIntValue("numHdrsToKeep", numHeadersToKeep);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetIntValue("daysToKeepHdrs", daysToKeepHdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetIntValue("daysToKeepBodies", daysToKeepBodies);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetBoolValue("cleanupBodies", cleanupBodiesByDays);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetBoolValue("applyToFlaggedMessages", applyToFlaggedMessages);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetDisplayStartupPage(bool* displayStartupPage) {
+ NS_ENSURE_ARG_POINTER(displayStartupPage);
+ *displayStartupPage = m_displayStartupPage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetDisplayStartupPage(bool displayStartupPage) {
+ m_displayStartupPage = displayStartupPage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetDownloadSettings(
+ nsIMsgDownloadSettings** settings) {
+ NS_ENSURE_ARG_POINTER(settings);
+ bool downloadUnreadOnly = false;
+ bool downloadByDate = false;
+ uint32_t ageLimitOfMsgsToDownload = 0;
+ nsresult rv = NS_OK;
+ if (!m_downloadSettings) {
+ m_downloadSettings =
+ do_CreateInstance("@mozilla.org/msgDatabase/downloadSettings;1");
+ if (m_downloadSettings) {
+ rv = GetBoolValue("downloadUnreadOnly", &downloadUnreadOnly);
+ rv = GetBoolValue("downloadByDate", &downloadByDate);
+ rv = GetIntValue("ageLimit", (int32_t*)&ageLimitOfMsgsToDownload);
+ m_downloadSettings->SetDownloadUnreadOnly(downloadUnreadOnly);
+ m_downloadSettings->SetDownloadByDate(downloadByDate);
+ m_downloadSettings->SetAgeLimitOfMsgsToDownload(ageLimitOfMsgsToDownload);
+ } else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ // Create an empty download settings object,
+ // get the settings from the server prefs, and init the object from the
+ // prefs.
+ }
+ NS_IF_ADDREF(*settings = m_downloadSettings);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetDownloadSettings(
+ nsIMsgDownloadSettings* settings) {
+ m_downloadSettings = settings;
+ bool downloadUnreadOnly = false;
+ bool downloadByDate = false;
+ uint32_t ageLimitOfMsgsToDownload = 0;
+ m_downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly);
+ m_downloadSettings->GetDownloadByDate(&downloadByDate);
+ m_downloadSettings->GetAgeLimitOfMsgsToDownload(&ageLimitOfMsgsToDownload);
+ nsresult rv = SetBoolValue("downloadUnreadOnly", downloadUnreadOnly);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SetBoolValue("downloadByDate", downloadByDate);
+ return SetIntValue("ageLimit", ageLimitOfMsgsToDownload);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSupportsDiskSpace(bool* aSupportsDiskSpace) {
+ NS_ENSURE_ARG_POINTER(aSupportsDiskSpace);
+ *aSupportsDiskSpace = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetOfflineSupportLevel(int32_t* aSupportLevel) {
+ NS_ENSURE_ARG_POINTER(aSupportLevel);
+
+ nsresult rv = GetIntValue("offline_support_level", aSupportLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (*aSupportLevel == OFFLINE_SUPPORT_LEVEL_UNDEFINED)
+ *aSupportLevel = OFFLINE_SUPPORT_LEVEL_NONE;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetOfflineSupportLevel(int32_t aSupportLevel) {
+ SetIntValue("offline_support_level", aSupportLevel);
+ return NS_OK;
+}
+
+// Called only during the migration process. A unique name is generated for the
+// migrated account.
+NS_IMETHODIMP
+nsMsgIncomingServer::GeneratePrettyNameForMigration(nsAString& aPrettyName) {
+ /**
+ * 4.x had provisions for multiple imap servers to be maintained under
+ * single identity. So, when migrated each of those server accounts need
+ * to be represented by unique account name. nsImapIncomingServer will
+ * override the implementation for this to do the right thing.
+ */
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetFilterScope(nsMsgSearchScopeValue* filterScope) {
+ NS_ENSURE_ARG_POINTER(filterScope);
+ *filterScope = nsMsgSearchScope::offlineMailFilter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSearchScope(nsMsgSearchScopeValue* searchScope) {
+ NS_ENSURE_ARG_POINTER(searchScope);
+ *searchScope = nsMsgSearchScope::offlineMail;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetIsSecure(bool* aIsSecure) {
+ NS_ENSURE_ARG_POINTER(aIsSecure);
+ int32_t socketType;
+ nsresult rv = GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aIsSecure = (socketType == nsMsgSocketType::alwaysSTARTTLS ||
+ socketType == nsMsgSocketType::SSL);
+ return NS_OK;
+}
+
+// use the convenience macros to implement the accessors
+NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, AuthMethod, "authMethod")
+NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, BiffMinutes, "check_time")
+NS_IMPL_SERVERPREF_STR(nsMsgIncomingServer, Type, "type")
+NS_IMPL_SERVERPREF_STR(nsMsgIncomingServer, Clientid, "clientid")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, ClientidEnabled, "clientidEnabled")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, DownloadOnBiff, "download_on_biff")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, Valid, "valid")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, EmptyTrashOnExit,
+ "empty_trash_on_exit")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanDelete, "canDelete")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, LoginAtStartUp, "login_at_startup")
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer,
+ DefaultCopiesAndFoldersPrefsToServer,
+ "allows_specialfolders_usage")
+
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanCreateFoldersOnServer,
+ "canCreateFolders")
+
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanFileMessagesOnServer,
+ "canFileMessages")
+
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, LimitOfflineMessageSize,
+ "limit_offline_message_size")
+
+NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, MaxMessageSize, "max_size")
+
+NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, IncomingDuplicateAction,
+ "dup_action")
+
+NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, Hidden, "hidden")
+
+NS_IMETHODIMP nsMsgIncomingServer::GetSocketType(int32_t* aSocketType) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ nsresult rv = mPrefBranch->GetIntPref("socketType", aSocketType);
+
+ // socketType is set to default value. Look at isSecure setting
+ if (NS_FAILED(rv)) {
+ bool isSecure;
+ rv = mPrefBranch->GetBoolPref("isSecure", &isSecure);
+ if (NS_SUCCEEDED(rv) && isSecure) {
+ *aSocketType = nsMsgSocketType::SSL;
+ // don't call virtual method in case overrides call GetSocketType
+ nsMsgIncomingServer::SetSocketType(*aSocketType);
+ } else {
+ if (!mDefPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+ rv = mDefPrefBranch->GetIntPref("socketType", aSocketType);
+ if (NS_FAILED(rv)) *aSocketType = nsMsgSocketType::plain;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::SetSocketType(int32_t aSocketType) {
+ if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED;
+
+ int32_t socketType = nsMsgSocketType::plain;
+ mPrefBranch->GetIntPref("socketType", &socketType);
+
+ nsresult rv = mPrefBranch->SetIntPref("socketType", aSocketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isSecureOld = (socketType == nsMsgSocketType::alwaysSTARTTLS ||
+ socketType == nsMsgSocketType::SSL);
+ bool isSecureNew = (aSocketType == nsMsgSocketType::alwaysSTARTTLS ||
+ aSocketType == nsMsgSocketType::SSL);
+ if ((isSecureOld != isSecureNew) && m_rootFolder) {
+ m_rootFolder->NotifyBoolPropertyChanged(kIsSecure, isSecureOld,
+ isSecureNew);
+ }
+ return NS_OK;
+}
+
+// Check if the password is available and return a boolean indicating whether
+// it is being authenticated or not.
+NS_IMETHODIMP
+nsMsgIncomingServer::GetPasswordPromptRequired(bool* aPasswordIsRequired) {
+ NS_ENSURE_ARG_POINTER(aPasswordIsRequired);
+ *aPasswordIsRequired = true;
+
+ // If the password is not even required for biff we don't need to check any
+ // further
+ nsresult rv = GetServerRequiresPasswordForBiff(aPasswordIsRequired);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!*aPasswordIsRequired) return NS_OK;
+
+ // If the password is empty, check to see if it is stored and to be retrieved
+ if (m_password.IsEmpty()) (void)GetPasswordWithoutUI();
+
+ *aPasswordIsRequired = m_password.IsEmpty();
+ if (*aPasswordIsRequired) {
+ // Set *aPasswordIsRequired false if authMethod is oauth2.
+ int32_t authMethod = 0;
+ rv = GetAuthMethod(&authMethod);
+ if (NS_SUCCEEDED(rv) && authMethod == nsMsgAuthMethod::OAuth2) {
+ *aPasswordIsRequired = false;
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::ConfigureTemporaryFilters(
+ nsIMsgFilterList* aFilterList) {
+ nsresult rv = ConfigureTemporaryReturnReceiptsFilter(aFilterList);
+ if (NS_FAILED(rv)) // shut up warnings...
+ return rv;
+ return ConfigureTemporaryServerSpamFilters(aFilterList);
+}
+
+nsresult nsMsgIncomingServer::ConfigureTemporaryServerSpamFilters(
+ nsIMsgFilterList* filterList) {
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ nsresult rv = GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool useServerFilter;
+ rv = spamSettings->GetUseServerFilter(&useServerFilter);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if we aren't configured to use server filters, then return early.
+ if (!useServerFilter) return NS_OK;
+
+ // For performance reasons, we'll handle clearing of filters if the user turns
+ // off the server-side filters from the junk mail controls, in the junk mail
+ // controls.
+ nsAutoCString serverFilterName;
+ spamSettings->GetServerFilterName(serverFilterName);
+ if (serverFilterName.IsEmpty()) return NS_OK;
+ int32_t serverFilterTrustFlags = 0;
+ (void)spamSettings->GetServerFilterTrustFlags(&serverFilterTrustFlags);
+ if (!serverFilterTrustFlags) return NS_OK;
+ // check if filters have been setup already.
+ nsAutoString yesFilterName, noFilterName;
+ CopyASCIItoUTF16(serverFilterName, yesFilterName);
+ yesFilterName.AppendLiteral("Yes");
+
+ CopyASCIItoUTF16(serverFilterName, noFilterName);
+ noFilterName.AppendLiteral("No");
+
+ nsCOMPtr<nsIMsgFilter> newFilter;
+ (void)filterList->GetFilterNamed(yesFilterName, getter_AddRefs(newFilter));
+
+ if (!newFilter)
+ (void)filterList->GetFilterNamed(noFilterName, getter_AddRefs(newFilter));
+ if (newFilter) return NS_OK;
+
+ nsCOMPtr<nsIFile> file;
+ spamSettings->GetServerFilterFile(getter_AddRefs(file));
+
+ // it's possible that we can no longer find the sfd file (i.e. the user
+ // disabled an extnsion that was supplying the .sfd file.
+ if (!file) return NS_OK;
+
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ nsCOMPtr<nsIMsgFilterList> serverFilterList;
+
+ rv = filterService->OpenFilterList(file, NULL, NULL,
+ getter_AddRefs(serverFilterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = serverFilterList->GetFilterNamed(yesFilterName,
+ getter_AddRefs(newFilter));
+ if (newFilter && serverFilterTrustFlags & nsISpamSettings::TRUST_POSITIVES) {
+ newFilter->SetTemporary(true);
+ // check if we're supposed to move junk mail to junk folder; if so,
+ // add filter action to do so.
+
+ /*
+ * We don't want this filter to activate on messages that have
+ * been marked by the user as not spam. This occurs when messages that
+ * were marked as good are moved back into the inbox. But to
+ * do this with a filter, we have to add a boolean term. That requires
+ * that we rewrite the existing filter search terms to group them.
+ */
+
+ // get the list of search terms from the filter
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ rv = newFilter->GetSearchTerms(searchTerms);
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t count = searchTerms.Length();
+ if (count > 1) // don't need to group a single term
+ {
+ // beginGrouping the first term, and endGrouping the last term
+ searchTerms[0]->SetBeginsGrouping(true);
+ searchTerms[count - 1]->SetEndsGrouping(true);
+ }
+
+ // Create a new term, checking if the user set junk status. The term will
+ // search for junkscoreorigin != "user"
+ nsCOMPtr<nsIMsgSearchTerm> searchTerm;
+ rv = newFilter->CreateTerm(getter_AddRefs(searchTerm));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ searchTerm->SetAttrib(nsMsgSearchAttrib::JunkScoreOrigin);
+ searchTerm->SetOp(nsMsgSearchOp::Isnt);
+ searchTerm->SetBooleanAnd(true);
+
+ nsCOMPtr<nsIMsgSearchValue> searchValue;
+ searchTerm->GetValue(getter_AddRefs(searchValue));
+ NS_ENSURE_SUCCESS(rv, rv);
+ searchValue->SetAttrib(nsMsgSearchAttrib::JunkScoreOrigin);
+ searchValue->SetStr(u"user"_ns);
+ searchTerm->SetValue(searchValue);
+
+ newFilter->AppendTerm(searchTerm);
+
+ bool moveOnSpam, markAsReadOnSpam;
+ spamSettings->GetMoveOnSpam(&moveOnSpam);
+ if (moveOnSpam) {
+ nsCString spamFolderURI;
+ rv = spamSettings->GetSpamFolderURI(spamFolderURI);
+ if (NS_SUCCEEDED(rv) && (!spamFolderURI.IsEmpty())) {
+ nsCOMPtr<nsIMsgRuleAction> moveAction;
+ rv = newFilter->CreateAction(getter_AddRefs(moveAction));
+ if (NS_SUCCEEDED(rv)) {
+ moveAction->SetType(nsMsgFilterAction::MoveToFolder);
+ moveAction->SetTargetFolderUri(spamFolderURI);
+ newFilter->AppendAction(moveAction);
+ }
+ }
+ }
+ spamSettings->GetMarkAsReadOnSpam(&markAsReadOnSpam);
+ if (markAsReadOnSpam) {
+ nsCOMPtr<nsIMsgRuleAction> markAsReadAction;
+ rv = newFilter->CreateAction(getter_AddRefs(markAsReadAction));
+ if (NS_SUCCEEDED(rv)) {
+ markAsReadAction->SetType(nsMsgFilterAction::MarkRead);
+ newFilter->AppendAction(markAsReadAction);
+ }
+ }
+ filterList->InsertFilterAt(0, newFilter);
+ }
+
+ rv =
+ serverFilterList->GetFilterNamed(noFilterName, getter_AddRefs(newFilter));
+ if (newFilter && serverFilterTrustFlags & nsISpamSettings::TRUST_NEGATIVES) {
+ newFilter->SetTemporary(true);
+ filterList->InsertFilterAt(0, newFilter);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgIncomingServer::ConfigureTemporaryReturnReceiptsFilter(
+ nsIMsgFilterList* filterList) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountMgr =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIdentity> identity;
+ rv = accountMgr->GetFirstIdentityForServer(this, getter_AddRefs(identity));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // this can return success and a null identity...
+
+ bool useCustomPrefs = false;
+ int32_t incorp = nsIMsgMdnGenerator::eIncorporateInbox;
+ NS_ENSURE_TRUE(identity, NS_ERROR_NULL_POINTER);
+
+ identity->GetBoolAttribute("use_custom_prefs", &useCustomPrefs);
+ if (useCustomPrefs)
+ rv = GetIntValue("incorporate_return_receipt", &incorp);
+ else {
+ nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefs) prefs->GetIntPref("mail.incorporate.return_receipt", &incorp);
+ }
+
+ bool enable = (incorp == nsIMsgMdnGenerator::eIncorporateSent);
+
+ // this is a temporary, internal mozilla filter
+ // it will not show up in the UI, it will not be written to disk
+ constexpr auto internalReturnReceiptFilterName =
+ u"mozilla-temporary-internal-MDN-receipt-filter"_ns;
+
+ nsCOMPtr<nsIMsgFilter> newFilter;
+ rv = filterList->GetFilterNamed(internalReturnReceiptFilterName,
+ getter_AddRefs(newFilter));
+ if (newFilter)
+ newFilter->SetEnabled(enable);
+ else if (enable) {
+ nsCString actionTargetFolderUri;
+ rv = identity->GetFccFolder(actionTargetFolderUri);
+ if (!actionTargetFolderUri.IsEmpty()) {
+ filterList->CreateFilter(internalReturnReceiptFilterName,
+ getter_AddRefs(newFilter));
+ if (newFilter) {
+ newFilter->SetEnabled(true);
+ // this internal filter is temporary
+ // and should not show up in the UI or be written to disk
+ newFilter->SetTemporary(true);
+
+ nsCOMPtr<nsIMsgSearchTerm> term;
+ nsCOMPtr<nsIMsgSearchValue> value;
+
+ rv = newFilter->CreateTerm(getter_AddRefs(term));
+ if (NS_SUCCEEDED(rv)) {
+ rv = term->GetValue(getter_AddRefs(value));
+ if (NS_SUCCEEDED(rv)) {
+ // we need to use OtherHeader + 1 so nsMsgFilter::GetTerm will
+ // return our custom header.
+ value->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1);
+ value->SetStr(u"multipart/report"_ns);
+ term->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1);
+ term->SetOp(nsMsgSearchOp::Contains);
+ term->SetBooleanAnd(true);
+ term->SetArbitraryHeader("Content-Type"_ns);
+ term->SetValue(value);
+ newFilter->AppendTerm(term);
+ }
+ }
+ rv = newFilter->CreateTerm(getter_AddRefs(term));
+ if (NS_SUCCEEDED(rv)) {
+ rv = term->GetValue(getter_AddRefs(value));
+ if (NS_SUCCEEDED(rv)) {
+ // XXX todo
+ // determine if ::OtherHeader is the best way to do this.
+ // see nsMsgSearchOfflineMail::MatchTerms()
+ value->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1);
+ value->SetStr(u"disposition-notification"_ns);
+ term->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1);
+ term->SetOp(nsMsgSearchOp::Contains);
+ term->SetBooleanAnd(true);
+ term->SetArbitraryHeader("Content-Type"_ns);
+ term->SetValue(value);
+ newFilter->AppendTerm(term);
+ }
+ }
+ nsCOMPtr<nsIMsgRuleAction> filterAction;
+ rv = newFilter->CreateAction(getter_AddRefs(filterAction));
+ if (NS_SUCCEEDED(rv)) {
+ filterAction->SetType(nsMsgFilterAction::MoveToFolder);
+ filterAction->SetTargetFolderUri(actionTargetFolderUri);
+ newFilter->AppendAction(filterAction);
+ filterList->InsertFilterAt(0, newFilter);
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::ClearTemporaryReturnReceiptsFilter() {
+ if (mFilterList) {
+ nsCOMPtr<nsIMsgFilter> mdnFilter;
+ nsresult rv = mFilterList->GetFilterNamed(
+ u"mozilla-temporary-internal-MDN-receipt-filter"_ns,
+ getter_AddRefs(mdnFilter));
+ if (NS_SUCCEEDED(rv) && mdnFilter)
+ return mFilterList->RemoveFilter(mdnFilter);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetMsgFolderFromURI(nsIMsgFolder* aFolderResource,
+ const nsACString& aURI,
+ nsIMsgFolder** aFolder) {
+ nsCOMPtr<nsIMsgFolder> rootMsgFolder;
+ nsresult rv = GetRootMsgFolder(getter_AddRefs(rootMsgFolder));
+ NS_ENSURE_TRUE(rootMsgFolder, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = rootMsgFolder->GetChildWithURI(aURI, true, true /*caseInsensitive*/,
+ getter_AddRefs(msgFolder));
+ if (NS_FAILED(rv) || !msgFolder) msgFolder = aFolderResource;
+ NS_IF_ADDREF(*aFolder = msgFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSpamSettings(nsISpamSettings** aSpamSettings) {
+ NS_ENSURE_ARG_POINTER(aSpamSettings);
+
+ nsAutoCString spamActionTargetAccount;
+ GetCharValue("spamActionTargetAccount", spamActionTargetAccount);
+ if (spamActionTargetAccount.IsEmpty()) {
+ GetServerURI(spamActionTargetAccount);
+ SetCharValue("spamActionTargetAccount", spamActionTargetAccount);
+ }
+
+ if (!mSpamSettings) {
+ nsresult rv;
+ mSpamSettings =
+ do_CreateInstance("@mozilla.org/messenger/spamsettings;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSpamSettings->Initialize(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_ADDREF(*aSpamSettings = mSpamSettings);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSpamFilterPlugin(nsIMsgFilterPlugin** aFilterPlugin) {
+ NS_ENSURE_ARG_POINTER(aFilterPlugin);
+ if (!mFilterPlugin) {
+ nsresult rv;
+ mFilterPlugin = do_GetService(
+ "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aFilterPlugin = mFilterPlugin);
+ return NS_OK;
+}
+
+// get all the servers that defer to the account for the passed in server. Note
+// that destServer may not be "this"
+nsresult nsMsgIncomingServer::GetDeferredServers(
+ nsIMsgIncomingServer* destServer,
+ nsTArray<RefPtr<nsIPop3IncomingServer>>& aServers) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> thisAccount;
+ accountManager->FindAccountForServer(destServer, getter_AddRefs(thisAccount));
+ if (thisAccount) {
+ nsCString accountKey;
+ thisAccount->GetKey(accountKey);
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ accountManager->GetAllServers(allServers);
+ for (auto server : allServers) {
+ nsCOMPtr<nsIPop3IncomingServer> popServer(do_QueryInterface(server));
+ if (popServer) {
+ nsCString deferredToAccount;
+ popServer->GetDeferredToAccount(deferredToAccount);
+ if (deferredToAccount.Equals(accountKey))
+ aServers.AppendElement(popServer);
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgIncomingServer::GetIsDeferredTo(bool* aIsDeferredTo) {
+ NS_ENSURE_ARG_POINTER(aIsDeferredTo);
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1");
+ if (accountManager) {
+ nsCOMPtr<nsIMsgAccount> thisAccount;
+ accountManager->FindAccountForServer(this, getter_AddRefs(thisAccount));
+ if (thisAccount) {
+ nsCString accountKey;
+ thisAccount->GetKey(accountKey);
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ accountManager->GetAllServers(allServers);
+ for (auto server : allServers) {
+ if (server) {
+ nsCString deferredToAccount;
+ server->GetCharValue("deferred_to_account", deferredToAccount);
+ if (deferredToAccount.Equals(accountKey)) {
+ *aIsDeferredTo = true;
+ return NS_OK;
+ }
+ }
+ }
+ }
+ }
+ *aIsDeferredTo = false;
+ return NS_OK;
+}
+
+const long kMaxDownloadTableSize = 500;
+
+// hash the concatenation of the message-id and subject as the hash table key,
+// and store the arrival index as the value. To limit the size of the hash
+// table, we just throw out ones with a lower ordinal value than the cut-off
+// point.
+NS_IMETHODIMP nsMsgIncomingServer::IsNewHdrDuplicate(nsIMsgDBHdr* aNewHdr,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ NS_ENSURE_ARG_POINTER(aNewHdr);
+ *aResult = false;
+
+ // If the message has been partially downloaded, the message should not
+ // be considered a duplicated message. See bug 714090.
+ uint32_t flags;
+ aNewHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::Partial) return NS_OK;
+
+ nsAutoCString strHashKey;
+ nsCString messageId, subject;
+ aNewHdr->GetMessageId(getter_Copies(messageId));
+ strHashKey.Append(messageId);
+ aNewHdr->GetSubject(subject);
+ // err on the side of caution and ignore messages w/o subject or messageid.
+ if (subject.IsEmpty() || messageId.IsEmpty()) return NS_OK;
+ strHashKey.Append(subject);
+ int32_t hashValue = m_downloadedHdrs.Get(strHashKey);
+ if (hashValue)
+ *aResult = true;
+ else {
+ // we store the current size of the hash table as the hash
+ // value - this allows us to delete older entries.
+ m_downloadedHdrs.InsertOrUpdate(strHashKey, ++m_numMsgsDownloaded);
+ // Check if hash table is larger than some reasonable size
+ // and if is it, iterate over hash table deleting messages
+ // with an arrival index < number of msgs downloaded - half the reasonable
+ // size.
+ if (m_downloadedHdrs.Count() >= kMaxDownloadTableSize) {
+ for (auto iter = m_downloadedHdrs.Iter(); !iter.Done(); iter.Next()) {
+ if (iter.Data() < m_numMsgsDownloaded - kMaxDownloadTableSize / 2) {
+ iter.Remove();
+ } else if (m_downloadedHdrs.Count() <= kMaxDownloadTableSize / 2) {
+ break;
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetForcePropertyEmpty(const char* aPropertyName,
+ bool* _retval) {
+ NS_ENSURE_ARG_POINTER(_retval);
+ nsAutoCString nameEmpty(aPropertyName);
+ nameEmpty.AppendLiteral(".empty");
+ nsCString value;
+ GetCharValue(nameEmpty.get(), value);
+ *_retval = value.EqualsLiteral("true");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::SetForcePropertyEmpty(const char* aPropertyName,
+ bool aValue) {
+ nsAutoCString nameEmpty(aPropertyName);
+ nameEmpty.AppendLiteral(".empty");
+ return SetCharValue(nameEmpty.get(), aValue ? "true"_ns : ""_ns);
+}
+
+NS_IMETHODIMP
+nsMsgIncomingServer::GetSortOrder(int32_t* aSortOrder) {
+ NS_ENSURE_ARG_POINTER(aSortOrder);
+ *aSortOrder = 100000000;
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgIncomingServer.h b/comm/mailnews/base/src/nsMsgIncomingServer.h
new file mode 100644
index 0000000000..c75c1f07c9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgIncomingServer.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef nsMsgIncomingServer_h__
+#define nsMsgIncomingServer_h__
+
+#include "nsIMsgIncomingServer.h"
+#include "nsIPrefBranch.h"
+#include "nsIMsgFilterList.h"
+#include "msgCore.h"
+#include "nsIMsgFolder.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsIPop3IncomingServer.h"
+#include "nsWeakReference.h"
+#include "nsIMsgDatabase.h"
+#include "nsISpamSettings.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsTHashMap.h"
+#include "nsIMsgPluggableStore.h"
+#include "nsIObserver.h"
+
+class nsIMsgFolderCache;
+class nsIMsgProtocolInfo;
+
+/*
+ * base class for nsIMsgIncomingServer - derive your class from here
+ * if you want to get some free implementation
+ *
+ * this particular implementation is not meant to be used directly.
+ */
+
+class nsMsgIncomingServer : public nsIMsgIncomingServer,
+ public nsSupportsWeakReference,
+ public nsIObserver {
+ public:
+ nsMsgIncomingServer();
+ nsresult Init();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGINCOMINGSERVER
+ NS_DECL_NSIOBSERVER
+
+ protected:
+ virtual ~nsMsgIncomingServer();
+ nsCString m_serverKey;
+
+ // Sets m_password, if password found. Can return NS_ERROR_ABORT if the
+ // user cancels the master password dialog.
+ nsresult GetPasswordWithoutUI();
+
+ nsresult ConfigureTemporaryReturnReceiptsFilter(nsIMsgFilterList* filterList);
+ nsresult ConfigureTemporaryServerSpamFilters(nsIMsgFilterList* filterList);
+
+ nsCOMPtr<nsIMsgFolder> m_rootFolder;
+ nsCOMPtr<nsIMsgDownloadSettings> m_downloadSettings;
+
+ // For local servers, where we put messages. For imap/pop3, where we store
+ // offline messages.
+ nsCOMPtr<nsIMsgPluggableStore> m_msgStore;
+
+ /// Helper routine to create local folder on disk if it doesn't exist
+ /// under the account's rootFolder.
+ nsresult CreateLocalFolder(const nsAString& folderName);
+
+ static nsresult GetDeferredServers(
+ nsIMsgIncomingServer* destServer,
+ nsTArray<RefPtr<nsIPop3IncomingServer>>& aServers);
+
+ nsresult CreateRootFolder();
+ virtual nsresult CreateRootFolderFromUri(const nsACString& serverUri,
+ nsIMsgFolder** rootFolder) = 0;
+
+ nsresult InternalSetHostName(const nsACString& aHostname,
+ const char* prefName);
+
+ nsCOMPtr<nsIFile> mFilterFile;
+ nsCOMPtr<nsIMsgFilterList> mFilterList;
+ nsCOMPtr<nsIMsgFilterList> mEditableFilterList;
+ nsCOMPtr<nsIPrefBranch> mPrefBranch;
+ nsCOMPtr<nsIPrefBranch> mDefPrefBranch;
+
+ // these allow us to handle duplicate incoming messages, e.g. delete them.
+ nsTHashMap<nsCStringHashKey, int32_t> m_downloadedHdrs;
+ int32_t m_numMsgsDownloaded;
+
+ private:
+ uint32_t m_biffState;
+ bool m_serverBusy;
+ nsCOMPtr<nsISpamSettings> mSpamSettings;
+ nsCOMPtr<nsIMsgFilterPlugin> mFilterPlugin; // XXX should be a list
+
+ protected:
+ nsString m_password;
+ bool m_canHaveFilters;
+ bool m_displayStartupPage;
+ bool mPerformingBiff;
+};
+
+#endif // nsMsgIncomingServer_h__
diff --git a/comm/mailnews/base/src/nsMsgKeySet.cpp b/comm/mailnews/base/src/nsMsgKeySet.cpp
new file mode 100644
index 0000000000..7423e2560a
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgKeySet.cpp
@@ -0,0 +1,1412 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h" // precompiled header...
+#include "prlog.h"
+
+#include "MailNewsTypes.h"
+#include "nsMsgKeySet.h"
+#include "prprf.h"
+#include "prmem.h"
+#include "nsTArray.h"
+#include "nsMemory.h"
+#include <ctype.h>
+
+#if defined(DEBUG_seth_) || defined(DEBUG_sspitzer_)
+# define DEBUG_MSGKEYSET 1
+#endif
+
+/* A compressed encoding for sets of article. This is usually for lines from
+ the newsrc, which have article lists like
+
+ 1-29627,29635,29658,32861-32863
+
+ so the data has these properties:
+
+ - strictly increasing
+ - large subsequences of monotonically increasing ranges
+ - gaps in the set are usually small, but not always
+ - consecutive ranges tend to be large
+
+ The biggest win is to run-length encode the data, storing ranges as two
+ numbers (start+length or start,end). We could also store each number as a
+ delta from the previous number for further compression, but that gets kind
+ of tricky, since there are no guarantees about the sizes of the gaps, and
+ we'd have to store variable-length words.
+
+ Current data format:
+
+ DATA := SIZE [ CHUNK ]*
+ CHUNK := [ RANGE | VALUE ]
+ RANGE := -LENGTH START
+ START := VALUE
+ LENGTH := int32_t
+ VALUE := a literal positive integer, for now
+ it could also be an offset from the previous value.
+ LENGTH could also perhaps be a less-than-32-bit quantity,
+ at least most of the time.
+
+ Lengths of CHUNKs are stored negative to distinguish the beginning of
+ a chunk from a literal: negative means two-word sequence, positive
+ means one-word sequence.
+
+ 0 represents a literal 0, but should not occur, and should never occur
+ except in the first position.
+
+ A length of -1 won't occur either, except temporarily - a sequence of
+ two elements is represented as two literals, since they take up the same
+ space.
+
+ Another optimization we make is to notice that we typically ask the
+ question ``is N a member of the set'' for increasing values of N. So the
+ set holds a cache of the last value asked for, and can simply resume the
+ search from there. */
+
+nsMsgKeySet::nsMsgKeySet(/* MSG_NewsHost* host*/) {
+ m_cached_value = -1;
+ m_cached_value_index = 0;
+ m_length = 0;
+ m_data_size = 10;
+ m_data = (int32_t*)PR_Malloc(sizeof(int32_t) * m_data_size);
+#ifdef NEWSRC_DOES_HOST_STUFF
+ m_host = host;
+#endif
+}
+
+nsMsgKeySet::~nsMsgKeySet() { PR_FREEIF(m_data); }
+
+bool nsMsgKeySet::Grow() {
+ int32_t new_size;
+ int32_t* new_data;
+ new_size = m_data_size * 2;
+ new_data = (int32_t*)PR_REALLOC(m_data, sizeof(int32_t) * new_size);
+ if (!new_data) return false;
+ m_data_size = new_size;
+ m_data = new_data;
+ return true;
+}
+
+nsMsgKeySet::nsMsgKeySet(const char* numbers /* , MSG_NewsHost* host */) {
+ int32_t *head, *tail, *end;
+
+#ifdef NEWSRC_DOES_HOST_STUFF
+ m_host = host;
+#endif
+ m_cached_value = -1;
+ m_cached_value_index = 0;
+ m_length = 0;
+ m_data_size = 10;
+ m_data = (int32_t*)PR_Malloc(sizeof(int32_t) * m_data_size);
+ if (!m_data) return;
+
+ head = m_data;
+ tail = head;
+ end = head + m_data_size;
+
+ if (!numbers) {
+ return;
+ }
+
+ while (isspace(*numbers)) numbers++;
+ while (*numbers) {
+ int32_t from = 0;
+ int32_t to;
+
+ if (tail >= end - 4) {
+ /* out of room! */
+ int32_t tailo = tail - head;
+ if (!Grow()) {
+ PR_FREEIF(m_data);
+ return;
+ }
+ /* data may have been relocated */
+ head = m_data;
+ tail = head + tailo;
+ end = head + m_data_size;
+ }
+
+ while (isspace(*numbers)) numbers++;
+ if (*numbers && !isdigit(*numbers)) {
+ break; /* illegal character */
+ }
+ while (isdigit(*numbers)) {
+ from = (from * 10) + (*numbers++ - '0');
+ }
+ while (isspace(*numbers)) numbers++;
+ if (*numbers != '-') {
+ to = from;
+ } else {
+ to = 0;
+ numbers++;
+ while (*numbers >= '0' && *numbers <= '9')
+ to = (to * 10) + (*numbers++ - '0');
+ while (isspace(*numbers)) numbers++;
+ }
+
+ if (to < from) to = from; /* illegal */
+
+ /* This is a hack - if the newsrc file specifies a range 1-x as
+ being read, we internally pretend that article 0 is read as well.
+ (But if only 2-x are read, then 0 is not read.) This is needed
+ because some servers think that article 0 is an article (I think)
+ but some news readers (including Netscape 1.1) choke if the .newsrc
+ file has lines beginning with 0... ### */
+ if (from == 1) from = 0;
+
+ if (to == from) {
+ /* Write it as a literal */
+ *tail = from;
+ tail++;
+ } else /* Write it as a range. */ {
+ *tail = -(to - from);
+ tail++;
+ *tail = from;
+ tail++;
+ }
+
+ while (*numbers == ',' || isspace(*numbers)) {
+ numbers++;
+ }
+ }
+
+ m_length = tail - head; /* size of data */
+}
+
+nsMsgKeySet* nsMsgKeySet::Create(/*MSG_NewsHost* host*/) {
+ nsMsgKeySet* set = new nsMsgKeySet(/* host */);
+ if (set && set->m_data == NULL) {
+ delete set;
+ set = NULL;
+ }
+ return set;
+}
+
+nsMsgKeySet* nsMsgKeySet::Create(const char* value /* , MSG_NewsHost* host */) {
+#ifdef DEBUG_MSGKEYSET
+ printf("create from %s\n", value);
+#endif
+
+ nsMsgKeySet* set = new nsMsgKeySet(value /* , host */);
+ if (set && set->m_data == NULL) {
+ delete set;
+ set = NULL;
+ }
+ return set;
+}
+
+/* Returns the lowest non-member of the set greater than 0.
+ */
+int32_t nsMsgKeySet::FirstNonMember() {
+ if (m_length <= 0) {
+ return 1;
+ } else if (m_data[0] < 0 && m_data[1] != 1 && m_data[1] != 0) {
+ /* first range not equal to 0 or 1, always return 1 */
+ return 1;
+ } else if (m_data[0] < 0) {
+ /* it's a range */
+ /* If there is a range [N-M] we can presume that M+1 is not in the
+ set. */
+ return (m_data[1] - m_data[0] + 1);
+ } else {
+ /* it's a literal */
+ if (m_data[0] == 1) {
+ /* handle "1,..." */
+ if (m_length > 1 && m_data[1] == 2) {
+ /* This is "1,2,M-N,..." or "1,2,M,..." where M >= 4. Note
+ that M will never be 3, because in that case we would have
+ started with a range: "1-3,..." */
+ return 3;
+ } else {
+ return 2; /* handle "1,M-N,.." or "1,M,..."
+ where M >= 3; */
+ }
+ } else if (m_data[0] == 0) {
+ /* handle "0,..." */
+ if (m_length > 1 && m_data[1] == 1) {
+ /* this is 0,1, (see above) */
+ return 2;
+ } else {
+ return 1;
+ }
+
+ } else {
+ /* handle "M,..." where M >= 2. */
+ return 1;
+ }
+ }
+}
+
+nsresult nsMsgKeySet::Output(char** outputStr) {
+ NS_ENSURE_ARG(outputStr);
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+ int32_t s_size;
+ char* s_head;
+ char *s, *s_end;
+ int32_t last_art = -1;
+
+ *outputStr = nullptr;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ s_size = (size * 12) +
+ 10; // dmb - try to make this allocation get used at least once.
+ s_head = (char*)moz_xmalloc(s_size);
+ if (!s_head) return NS_ERROR_OUT_OF_MEMORY;
+
+ s_head[0] = '\0'; // otherwise, s_head will contain garbage.
+ s = s_head;
+ s_end = s + s_size;
+
+ while (tail < end) {
+ int32_t from;
+ int32_t to;
+
+ if (s > (s_end - (12 * 2 + 10))) { /* 12 bytes for each number (enough
+ for "2147483647" aka 2^31-1),
+ plus 10 bytes of slop. */
+ int32_t so = s - s_head;
+ s_size += 200;
+ char* tmp = (char*)moz_xmalloc(s_size);
+ if (tmp) PL_strcpy(tmp, s_head);
+ free(s_head);
+ s_head = tmp;
+ if (!s_head) return NS_ERROR_OUT_OF_MEMORY;
+ s = s_head + so;
+ s_end = s_head + s_size;
+ }
+
+ if (*tail < 0) {
+ /* it's a range */
+ from = tail[1];
+ to = from + (-(tail[0]));
+ tail += 2;
+ } else /* it's a literal */
+ {
+ from = *tail;
+ to = from;
+ tail++;
+ }
+ if (from == 0) {
+ from = 1; /* See 'hack' comment above ### */
+ }
+ if (from <= last_art) from = last_art + 1;
+ if (from <= to) {
+ if (from < to) {
+ PR_snprintf(s, s_end - s, "%lu-%lu,", from, to);
+ } else {
+ PR_snprintf(s, s_end - s, "%lu,", from);
+ }
+ s += PL_strlen(s);
+ last_art = to;
+ }
+ }
+ if (last_art >= 0) {
+ s--; /* Strip off the last ',' */
+ }
+
+ *s = 0;
+
+ *outputStr = s_head;
+ return NS_OK;
+}
+
+int32_t nsMsgKeySet::GetLastMember() {
+ if (m_length > 1) {
+ int32_t nextToLast = m_data[m_length - 2];
+ if (nextToLast < 0) // is range at end?
+ {
+ int32_t last = m_data[m_length - 1];
+ return (-nextToLast + last - 1);
+ } else // no, so last number must be last member
+ {
+ return m_data[m_length - 1];
+ }
+ } else if (m_length == 1)
+ return m_data[0]; // must be only 1 read.
+ else
+ return 0;
+}
+
+void nsMsgKeySet::SetLastMember(int32_t newHighWaterMark) {
+ if (newHighWaterMark < GetLastMember()) {
+ while (true) {
+ if (m_length > 1) {
+ int32_t nextToLast = m_data[m_length - 2];
+ int32_t curHighWater;
+ if (nextToLast < 0) // is range at end?
+ {
+ int32_t rangeStart = m_data[m_length - 1];
+ int32_t rangeLength = -nextToLast;
+ curHighWater = (rangeLength + rangeStart - 1);
+ if (curHighWater > newHighWaterMark) {
+ if (rangeStart > newHighWaterMark) {
+ m_length -= 2; // throw away whole range
+ } else if (rangeStart == newHighWaterMark) {
+ // turn range into single element.
+ m_data[m_length - 2] = newHighWaterMark;
+ m_length--;
+ break;
+ } else // just shorten range
+ {
+ m_data[m_length - 2] = -(newHighWaterMark - rangeStart);
+ break;
+ }
+ } else {
+ // prevent the infinite loop
+ // see bug #13062
+ break;
+ }
+ } else if (m_data[m_length - 1] >
+ newHighWaterMark) // no, so last number must be last member
+ {
+ m_length--;
+ } else
+ break;
+ } else
+ break;
+ }
+ // well, the whole range is probably invalid, because the server probably
+ // re-ordered ids, but what can you do?
+#ifdef NEWSRC_DOES_HOST_STUFF
+ if (m_host) m_host->MarkDirty();
+#endif
+ }
+}
+
+int32_t nsMsgKeySet::GetFirstMember() {
+ if (m_length > 1) {
+ int32_t first = m_data[0];
+ if (first < 0) // is range at start?
+ {
+ int32_t second = m_data[1];
+ return (second);
+ } else // no, so first number must be first member
+ {
+ return m_data[0];
+ }
+ } else if (m_length == 1)
+ return m_data[0]; // must be only 1 read.
+ else
+ return 0;
+}
+
+/* Re-compresses a `nsMsgKeySet' object.
+
+ The assumption is made that the `nsMsgKeySet' is syntactically correct
+ (all ranges have a length of at least 1, and all values are non-
+ decreasing) but will optimize the compression, for example, merging
+ consecutive literals or ranges into one range.
+
+ Returns true if successful, false if there wasn't enough memory to
+ allocate scratch space.
+
+ #### This should be changed to modify the buffer in place.
+
+ Also note that we never call Optimize() unless we actually changed
+ something, so it's a great place to tell the MSG_NewsHost* that something
+ changed.
+ */
+bool nsMsgKeySet::Optimize() {
+ int32_t input_size;
+ int32_t output_size;
+ int32_t* input_tail;
+ int32_t* output_data;
+ int32_t* output_tail;
+ int32_t* input_end;
+ int32_t* output_end;
+
+ input_size = m_length;
+ output_size = input_size + 1;
+ input_tail = m_data;
+ output_data = (int32_t*)PR_Malloc(sizeof(int32_t) * output_size);
+ if (!output_data) return false;
+
+ output_tail = output_data;
+ input_end = input_tail + input_size;
+ output_end = output_data + output_size;
+
+ /* We're going to modify the set, so invalidate the cache. */
+ m_cached_value = -1;
+
+ while (input_tail < input_end) {
+ int32_t from, to;
+ bool range_p = (*input_tail < 0);
+
+ if (range_p) {
+ /* it's a range */
+ from = input_tail[1];
+ to = from + (-(input_tail[0]));
+
+ /* Copy it over */
+ *output_tail++ = *input_tail++;
+ *output_tail++ = *input_tail++;
+ } else {
+ /* it's a literal */
+ from = *input_tail;
+ to = from;
+
+ /* Copy it over */
+ *output_tail++ = *input_tail++;
+ }
+ NS_ASSERTION(output_tail < output_end, "invalid end of output string");
+ if (output_tail >= output_end) {
+ PR_Free(output_data);
+ return false;
+ }
+
+ /* As long as this chunk is followed by consecutive chunks,
+ keep extending it. */
+ while (input_tail < input_end &&
+ ((*input_tail > 0 && /* literal... */
+ *input_tail == to + 1) || /* ...and consecutive, or */
+ (*input_tail <= 0 && /* range... */
+ input_tail[1] == to + 1)) /* ...and consecutive. */
+ ) {
+ if (!range_p) {
+ /* convert the literal to a range. */
+ output_tail++;
+ output_tail[-2] = 0;
+ output_tail[-1] = from;
+ range_p = true;
+ }
+
+ if (*input_tail > 0) { /* literal */
+ output_tail[-2]--; /* increase length by 1 */
+ to++;
+ input_tail++;
+ } else {
+ int32_t L2 = (-*input_tail) + 1;
+ output_tail[-2] -= L2; /* increase length by N */
+ to += L2;
+ input_tail += 2;
+ }
+ }
+ }
+
+ PR_Free(m_data);
+ m_data = output_data;
+ m_data_size = output_size;
+ m_length = output_tail - output_data;
+
+ /* One last pass to turn [N - N+1] into [N, N+1]. */
+ output_tail = output_data;
+ output_end = output_tail + m_length;
+ while (output_tail < output_end) {
+ if (*output_tail < 0) {
+ /* it's a range */
+ if (output_tail[0] == -1) {
+ output_tail[0] = output_tail[1];
+ output_tail[1]++;
+ }
+ output_tail += 2;
+ } else {
+ /* it's a literal */
+ output_tail++;
+ }
+ }
+
+#ifdef NEWSRC_DOES_HOST_STUFF
+ if (m_host) m_host->MarkDirty();
+#endif
+ return true;
+}
+
+bool nsMsgKeySet::IsMember(int32_t number) {
+ bool value = false;
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ /* If there is a value cached, and that value is smaller than the
+ value we're looking for, skip forward that far. */
+ if (m_cached_value > 0 && m_cached_value < number) {
+ tail += m_cached_value_index;
+ }
+
+ while (tail < end) {
+ if (*tail < 0) {
+ /* it's a range */
+ int32_t from = tail[1];
+ int32_t to = from + (-(tail[0]));
+ if (from > number) {
+ /* This range begins after the number - we've passed it. */
+ value = false;
+ goto DONE;
+ } else if (to >= number) {
+ /* In range. */
+ value = true;
+ goto DONE;
+ } else {
+ tail += 2;
+ }
+ } else {
+ /* it's a literal */
+ if (*tail == number) {
+ /* bang */
+ value = true;
+ goto DONE;
+ } else if (*tail > number) {
+ /* This literal is after the number - we've passed it. */
+ value = false;
+ goto DONE;
+ } else {
+ tail++;
+ }
+ }
+ }
+
+DONE:
+ /* Store the position of this chunk for next time. */
+ m_cached_value = number;
+ m_cached_value_index = tail - head;
+
+ return value;
+}
+
+int nsMsgKeySet::Add(int32_t number) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+
+#ifdef DEBUG_MSGKEYSET
+ printf("add %d\n", number);
+#endif
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ NS_ASSERTION(number >= 0, "can't have negative items");
+ if (number < 0) return 0;
+
+ /* We're going to modify the set, so invalidate the cache. */
+ m_cached_value = -1;
+
+ while (tail < end) {
+ if (*tail < 0) {
+ /* it's a range */
+ int32_t from = tail[1];
+ int32_t to = from + (-(tail[0]));
+
+ if (from <= number && to >= number) {
+ /* This number is already present - we don't need to do
+ anything. */
+ return 0;
+ }
+
+ if (to > number) {
+ /* We have found the point before which the new number
+ should be inserted. */
+ break;
+ }
+
+ tail += 2;
+ } else {
+ /* it's a literal */
+ if (*tail == number) {
+ /* This number is already present - we don't need to do
+ anything. */
+ return 0;
+ }
+
+ if (*tail > number) {
+ /* We have found the point before which the new number
+ should be inserted. */
+ break;
+ }
+
+ tail++;
+ }
+ }
+
+ /* At this point, `tail' points to a position in the set which represents
+ a value greater than `new'; or it is at `end'. In the interest of
+ avoiding massive duplication of code, simply insert a literal here and
+ then run the optimizer.
+ */
+ int mid = (tail - head);
+
+ if (m_data_size <= m_length + 1) {
+ int endo = end - head;
+ if (!Grow()) {
+ // out of memory
+ return -1;
+ }
+ head = m_data;
+ end = head + endo;
+ }
+
+ if (tail == end) {
+ /* at the end */
+ /* Add a literal to the end. */
+ m_data[m_length++] = number;
+ } else {
+ /* need to insert (or edit) in the middle */
+ int32_t i;
+ for (i = size; i > mid; i--) {
+ m_data[i] = m_data[i - 1];
+ }
+ m_data[i] = number;
+ m_length++;
+ }
+
+ Optimize();
+ return 1;
+}
+
+int nsMsgKeySet::Remove(int32_t number) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+#ifdef DEBUG_MSGKEYSET
+ printf("remove %d\n", number);
+#endif
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ // **** I am not sure this is a right thing to comment the following
+ // statements out. The reason for this is due to the implementation of
+ // offline save draft and template. We use faked UIDs (negative ids) for
+ // offline draft and template in order to distinguish them from real
+ // UID. David I need your help here. **** jt
+
+ // PR_ASSERT(number >= 0);
+ // if (number < 0) {
+ // return -1;
+ /// }
+
+ /* We're going to modify the set, so invalidate the cache. */
+ m_cached_value = -1;
+
+ while (tail < end) {
+ int32_t mid = (tail - m_data);
+
+ if (*tail < 0) {
+ /* it's a range */
+ int32_t from = tail[1];
+ int32_t to = from + (-(tail[0]));
+
+ if (number < from || number > to) {
+ /* Not this range */
+ tail += 2;
+ continue;
+ }
+
+ if (to == from + 1) {
+ /* If this is a range [N - N+1] and we are removing M
+ (which must be either N or N+1) replace it with a
+ literal. This reduces the length by 1. */
+ m_data[mid] = (number == from ? to : from);
+ while (++mid < m_length) {
+ m_data[mid] = m_data[mid + 1];
+ }
+ m_length--;
+ Optimize();
+ return 1;
+ } else if (to == from + 2) {
+ /* If this is a range [N - N+2] and we are removing M,
+ replace it with the literals L,M (that is, either
+ (N, N+1), (N, N+2), or (N+1, N+2). The overall
+ length remains the same. */
+ m_data[mid] = from;
+ m_data[mid + 1] = to;
+ if (from == number) {
+ m_data[mid] = from + 1;
+ } else if (to == number) {
+ m_data[mid + 1] = to - 1;
+ }
+ Optimize();
+ return 1;
+ } else if (from == number) {
+ /* This number is at the beginning of a long range (meaning a
+ range which will still be long enough to remain a range.)
+ Increase start and reduce length of the range. */
+ m_data[mid]++;
+ m_data[mid + 1]++;
+ Optimize();
+ return 1;
+ } else if (to == number) {
+ /* This number is at the end of a long range (meaning a range
+ which will still be long enough to remain a range.)
+ Just decrease the length of the range. */
+ m_data[mid]++;
+ Optimize();
+ return 1;
+ } else {
+ /* The number being deleted is in the middle of a range which
+ must be split. This increases overall length by 2.
+ */
+ int32_t i;
+ int endo = end - head;
+ if (m_data_size - m_length <= 2) {
+ if (!Grow())
+ // out of memory
+ return -1;
+ }
+ head = m_data;
+ end = head + endo;
+
+ for (i = m_length + 2; i > mid + 2; i--) {
+ m_data[i] = m_data[i - 2];
+ }
+
+ m_data[mid] = (-(number - from - 1));
+ m_data[mid + 1] = from;
+ m_data[mid + 2] = (-(to - number - 1));
+ m_data[mid + 3] = number + 1;
+ m_length += 2;
+
+ /* Oops, if we've ended up with a range with a 0 length,
+ which is illegal, convert it to a literal, which reduces
+ the overall length by 1. */
+ if (m_data[mid] == 0) {
+ /* first range */
+ m_data[mid] = m_data[mid + 1];
+ for (i = mid + 1; i < m_length; i++) {
+ m_data[i] = m_data[i + 1];
+ }
+ m_length--;
+ }
+ if (m_data[mid + 2] == 0) {
+ /* second range */
+ m_data[mid + 2] = m_data[mid + 3];
+ for (i = mid + 3; i < m_length; i++) {
+ m_data[i] = m_data[i + 1];
+ }
+ m_length--;
+ }
+ Optimize();
+ return 1;
+ }
+ } else {
+ /* it's a literal */
+ if (*tail != number) {
+ /* Not this literal */
+ tail++;
+ continue;
+ }
+
+ /* Excise this literal. */
+ m_length--;
+ while (mid < m_length) {
+ m_data[mid] = m_data[mid + 1];
+ mid++;
+ }
+ Optimize();
+ return 1;
+ }
+ }
+
+ /* It wasn't here at all. */
+ return 0;
+}
+
+static int32_t* msg_emit_range(int32_t* tmp, int32_t a, int32_t b) {
+ if (a == b) {
+ *tmp++ = a;
+ } else {
+ NS_ASSERTION(a < b && a >= 0, "range is out of order");
+ *tmp++ = -(b - a);
+ *tmp++ = a;
+ }
+ return tmp;
+}
+
+int nsMsgKeySet::AddRange(int32_t start, int32_t end) {
+ int32_t tmplength;
+ int32_t* tmp;
+ int32_t* in;
+ int32_t* out;
+ int32_t* tail;
+ int32_t a;
+ int32_t b;
+ bool didit = false;
+
+ /* We're going to modify the set, so invalidate the cache. */
+ m_cached_value = -1;
+
+ NS_ASSERTION(start <= end, "invalid range");
+ if (start > end) return -1;
+
+ if (start == end) {
+ return Add(start);
+ }
+
+ tmplength = m_length + 2;
+ tmp = (int32_t*)PR_Malloc(sizeof(int32_t) * tmplength);
+
+ if (!tmp)
+ // out of memory
+ return -1;
+
+ in = m_data;
+ out = tmp;
+ tail = in + m_length;
+
+#define EMIT(x, y) out = msg_emit_range(out, x, y)
+
+ while (in < tail) {
+ // Set [a,b] to be this range.
+ if (*in < 0) {
+ b = -*in++;
+ a = *in++;
+ b += a;
+ } else {
+ a = b = *in++;
+ }
+
+ if (a <= start && b >= end) {
+ // We already have the entire range marked.
+ PR_Free(tmp);
+ return 0;
+ }
+ if (start > b + 1) {
+ // No overlap yet.
+ EMIT(a, b);
+ } else if (end < a - 1) {
+ // No overlap, and we passed it.
+ EMIT(start, end);
+ EMIT(a, b);
+ didit = true;
+ break;
+ } else {
+ // The ranges overlap. Suck this range into our new range, and
+ // keep looking for other ranges that might overlap.
+ start = start < a ? start : a;
+ end = end > b ? end : b;
+ }
+ }
+ if (!didit) EMIT(start, end);
+ while (in < tail) {
+ *out++ = *in++;
+ }
+
+#undef EMIT
+
+ PR_Free(m_data);
+ m_data = tmp;
+ m_length = out - tmp;
+ m_data_size = tmplength;
+#ifdef NEWSRC_DOES_HOST_STUFF
+ if (m_host) m_host->MarkDirty();
+#endif
+ return 1;
+}
+
+int32_t nsMsgKeySet::CountMissingInRange(int32_t range_start,
+ int32_t range_end) {
+ int32_t count;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+
+ NS_ASSERTION(range_start >= 0 && range_end >= 0 && range_end >= range_start,
+ "invalid range");
+ if (range_start < 0 || range_end < 0 || range_end < range_start) return -1;
+
+ head = m_data;
+ tail = head;
+ end = head + m_length;
+
+ count = range_end - range_start + 1;
+
+ while (tail < end) {
+ if (*tail < 0) {
+ /* it's a range */
+ int32_t from = tail[1];
+ int32_t to = from + (-(tail[0]));
+ if (from < range_start) from = range_start;
+ if (to > range_end) to = range_end;
+
+ if (to >= from) count -= (to - from + 1);
+
+ tail += 2;
+ } else {
+ /* it's a literal */
+ if (*tail >= range_start && *tail <= range_end) count--;
+ tail++;
+ }
+ NS_ASSERTION(count >= 0, "invalid count");
+ }
+ return count;
+}
+
+int nsMsgKeySet::FirstMissingRange(int32_t min, int32_t max, int32_t* first,
+ int32_t* last) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+ int32_t from = 0;
+ int32_t to = 0;
+ int32_t a;
+ int32_t b;
+
+ NS_ASSERTION(first && last, "invalid parameter");
+ if (!first || !last) return -1;
+
+ *first = *last = 0;
+
+ NS_ASSERTION(min <= max && min > 0, "invalid min or max param");
+ if (min > max || min <= 0) return -1;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ while (tail < end) {
+ a = to + 1;
+ if (*tail < 0) { /* We got a range. */
+ from = tail[1];
+ to = from + (-(tail[0]));
+ tail += 2;
+ } else {
+ from = to = tail[0];
+ tail++;
+ }
+ b = from - 1;
+ /* At this point, [a,b] is the range of unread articles just before
+ the current range of read articles [from,to]. See if this range
+ intersects the [min,max] range we were given. */
+ if (a > max) return 0; /* It's hopeless; there are none. */
+ if (a <= b && b >= min) {
+ /* Ah-hah! We found an intersection. */
+ *first = a > min ? a : min;
+ *last = b < max ? b : max;
+ return 0;
+ }
+ }
+ /* We found no holes in the newsrc that overlaps the range, nor did we hit
+ something read beyond the end of the range. So, the great infinite
+ range of unread articles at the end of any newsrc line intersects the
+ range we want, and we just need to return that. */
+ a = to + 1;
+ *first = a > min ? a : min;
+ *last = max;
+ return 0;
+}
+
+// I'm guessing we didn't include this because we didn't think we're going
+// to need it. I'm not so sure. I'm putting it in for now.
+int nsMsgKeySet::LastMissingRange(int32_t min, int32_t max, int32_t* first,
+ int32_t* last) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+ int32_t from = 0;
+ int32_t to = 0;
+ int32_t a;
+ int32_t b;
+
+ NS_ASSERTION(first && last, "invalid null param");
+ if (!first || !last) return -1;
+
+ *first = *last = 0;
+
+ NS_ASSERTION(min <= max && min > 0, "invalid min or max");
+ if (min > max || min <= 0) return -1;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ while (tail < end) {
+ a = to + 1;
+ if (*tail < 0) { /* We got a range. */
+ from = tail[1];
+ to = from + (-(tail[0]));
+ tail += 2;
+ } else {
+ from = to = tail[0];
+ tail++;
+ }
+ b = from - 1;
+ /* At this point, [a,b] is the range of unread articles just before
+ the current range of read articles [from,to]. See if this range
+ intersects the [min,max] range we were given. */
+ if (a > max)
+ return 0; /* We're done. If we found something, it's already
+ sitting in [*first,*last]. */
+ if (a <= b && b >= min) {
+ /* Ah-hah! We found an intersection. */
+ *first = a > min ? a : min;
+ *last = b < max ? b : max;
+ /* Continue on, looking for a later range. */
+ }
+ }
+ if (to < max) {
+ /* The great infinite range of unread articles at the end of any newsrc
+ line intersects the range we want, and we just need to return that. */
+ a = to + 1;
+ *first = a > min ? a : min;
+ *last = max;
+ }
+ return 0;
+}
+
+/**
+ * Fill the passed in aArray with the keys in the message key set.
+ */
+nsresult nsMsgKeySet::ToMsgKeyArray(nsTArray<nsMsgKey>& aArray) {
+ int32_t size;
+ int32_t* head;
+ int32_t* tail;
+ int32_t* end;
+ int32_t last_art = -1;
+
+ size = m_length;
+ head = m_data;
+ tail = head;
+ end = head + size;
+
+ while (tail < end) {
+ int32_t from;
+ int32_t to;
+
+ if (*tail < 0) {
+ /* it's a range */
+ from = tail[1];
+ to = from + (-(tail[0]));
+ tail += 2;
+ } else /* it's a literal */
+ {
+ from = *tail;
+ to = from;
+ tail++;
+ }
+ // The horrible news-hack used to adjust from to 1 if it was zero right
+ // here, but there is no longer a consumer of this method with that
+ // broken use-case.
+ if (from <= last_art) from = last_art + 1;
+ if (from <= to) {
+ if (from < to) {
+ for (int32_t i = from; i <= to; ++i) {
+ aArray.AppendElement(i);
+ }
+ } else {
+ aArray.AppendElement(from);
+ }
+ last_art = to;
+ }
+ }
+
+ return NS_OK;
+}
+
+#ifdef DEBUG /* A lot of test cases for the above */
+
+# define countof(x) (sizeof(x) / sizeof(*(x)))
+
+void nsMsgKeySet::test_decoder(const char* string) {
+ nsMsgKeySet set(string /* , NULL */);
+ char* tmp;
+ set.Output(&tmp);
+ printf("\t\"%s\"\t--> \"%s\"\n", string, tmp);
+ free(tmp);
+}
+
+# define START(STRING) \
+ string = STRING; \
+ if (!(set = nsMsgKeySet::Create(string))) abort()
+
+# define FROB(N, PUSHP) \
+ i = N; \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %-58s %c %3lu =\n", (unsigned long)set->m_length, s, \
+ (PUSHP ? '+' : '-'), (unsigned long)i); \
+ free(s); \
+ if (PUSHP ? set->Add(i) < 0 : set->Remove(i) < 0) abort(); \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %-58s optimized =\n", (unsigned long)set->m_length, s); \
+ free(s);
+
+# define END() \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %s\n\n", (unsigned long)set->m_length, s); \
+ free(s); \
+ delete set;
+
+void nsMsgKeySet::test_adder(void) {
+ const char* string;
+ nsMsgKeySet* set;
+ char* s;
+ int32_t i;
+
+ START("0-70,72-99,105,107,110-111,117-200");
+
+ FROB(205, true);
+ FROB(206, true);
+ FROB(207, true);
+ FROB(208, true);
+ FROB(208, true);
+ FROB(109, true);
+ FROB(72, true);
+
+ FROB(205, false);
+ FROB(206, false);
+ FROB(207, false);
+ FROB(208, false);
+ FROB(208, false);
+ FROB(109, false);
+ FROB(72, false);
+
+ FROB(72, true);
+ FROB(109, true);
+ FROB(208, true);
+ FROB(208, true);
+ FROB(207, true);
+ FROB(206, true);
+ FROB(205, true);
+
+ FROB(205, false);
+ FROB(206, false);
+ FROB(207, false);
+ FROB(208, false);
+ FROB(208, false);
+ FROB(109, false);
+ FROB(72, false);
+
+ FROB(100, true);
+ FROB(101, true);
+ FROB(102, true);
+ FROB(103, true);
+ FROB(106, true);
+ FROB(104, true);
+ FROB(109, true);
+ FROB(108, true);
+ END();
+
+ // clang-format off
+ START("1-6"); FROB(7, false); END();
+ START("1-6"); FROB(6, false); END();
+ START("1-6"); FROB(5, false); END();
+ START("1-6"); FROB(4, false); END();
+ START("1-6"); FROB(3, false); END();
+ START("1-6"); FROB(2, false); END();
+ START("1-6"); FROB(1, false); END();
+ START("1-6"); FROB(0, false); END();
+
+ START("1-3"); FROB(1, false); END();
+ START("1-3"); FROB(2, false); END();
+ START("1-3"); FROB(3, false); END();
+
+ START("1,3,5-7,9,10"); FROB(5, false); END();
+ START("1,3,5-7,9,10"); FROB(6, false); END();
+ START("1,3,5-7,9,10"); FROB(7, false); FROB(7, true); FROB(8, true);
+ FROB (4, true); FROB (2, false); FROB (2, true);
+
+ FROB (4, false); FROB (5, false); FROB (6, false); FROB (7, false);
+ FROB (8, false); FROB (9, false); FROB (10, false); FROB (3, false);
+ FROB (2, false); FROB (1, false); FROB (1, false); FROB (0, false);
+ END();
+ // clang-format on
+}
+
+# undef START
+# undef FROB
+# undef END
+
+# define START(STRING) \
+ string = STRING; \
+ if (!(set = nsMsgKeySet::Create(string))) abort()
+
+# define FROB(N, M) \
+ i = N; \
+ j = M; \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %-58s + %3lu-%3lu =\n", (unsigned long)set->m_length, s, \
+ (unsigned long)i, (unsigned long)j); \
+ free(s); \
+ switch (set->AddRange(i, j)) { \
+ case 0: \
+ printf("(no-op)\n"); \
+ break; \
+ case 1: \
+ break; \
+ default: \
+ abort(); \
+ } \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %-58s\n", (unsigned long)set->m_length, s); \
+ free(s);
+
+# define END() \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf("%3lu: %s\n\n", (unsigned long)set->m_length, s); \
+ free(s); \
+ delete set;
+
+void nsMsgKeySet::test_ranges(void) {
+ const char* string;
+ nsMsgKeySet* set;
+ char* s;
+ int32_t i;
+ int32_t j;
+
+ START("20-40,72-99,105,107,110-111,117-200");
+
+ FROB(205, 208);
+ FROB(50, 70);
+ FROB(0, 10);
+ FROB(112, 113);
+ FROB(101, 101);
+ FROB(5, 75);
+ FROB(103, 109);
+ FROB(2, 20);
+ FROB(1, 9999);
+
+ END();
+
+# undef START
+# undef FROB
+# undef END
+}
+
+# define TEST(N) \
+ if (!with_cache) set->m_cached_value = -1; \
+ if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \
+ printf(" %3d = %s\n", N, (set->IsMember(N) ? "true" : "false")); \
+ free(s);
+
+void nsMsgKeySet::test_member(bool with_cache) {
+ nsMsgKeySet* set;
+ char* s;
+
+ const char* st1 = "1-70,72-99,105,107,110-111,117-200";
+ printf("\n\nTesting %s (with%s cache)\n", st1, with_cache ? "" : "out");
+ if (!(set = Create(st1))) {
+ abort();
+ }
+
+ TEST(-1);
+ TEST(0);
+ TEST(1);
+ TEST(20);
+
+ delete set;
+ const char* st2 = "0-70,72-99,105,107,110-111,117-200";
+ printf("\n\nTesting %s (with%s cache)\n", st2, with_cache ? "" : "out");
+ if (!(set = Create(st2))) {
+ abort();
+ }
+
+ TEST(-1);
+ TEST(0);
+ TEST(1);
+ TEST(20);
+ TEST(69);
+ TEST(70);
+ TEST(71);
+ TEST(72);
+ TEST(73);
+ TEST(74);
+ TEST(104);
+ TEST(105);
+ TEST(106);
+ TEST(107);
+ TEST(108);
+ TEST(109);
+ TEST(110);
+ TEST(111);
+ TEST(112);
+ TEST(116);
+ TEST(117);
+ TEST(118);
+ TEST(119);
+ TEST(200);
+ TEST(201);
+ TEST(65535);
+
+ delete set;
+}
+
+# undef TEST
+
+// static void
+// test_newsrc (char *file)
+// {
+// FILE *fp = fopen (file, "r");
+// char buf [1024];
+// if (! fp) abort ();
+// while (fgets (buf, sizeof (buf), fp))
+// {
+// if (!strncmp (buf, "options ", 8))
+// fwrite (buf, 1, strlen (buf), stdout);
+// else
+// {
+// char *sep = buf;
+// while (*sep != 0 && *sep != ':' && *sep != '!')
+// sep++;
+// if (*sep) sep++;
+// while (isspace (*sep)) sep++;
+// fwrite (buf, 1, sep - buf, stdout);
+// if (*sep)
+// {
+// char *s;
+// msg_NewsRCSet *set = msg_parse_newsrc_set (sep, &allocinfo);
+// if (! set)
+// abort ();
+// if (! msg_OptimizeNewsRCSet (set))
+// abort ();
+// if (! ((s = msg_format_newsrc_set (set))))
+// abort ();
+// msg_free_newsrc_set (set, &allocinfo);
+// fwrite (s, 1, strlen (s), stdout);
+// free (s);
+// fwrite ("\n", 1, 1, stdout);
+// }
+// }
+// }
+// fclose (fp);
+// }
+
+void nsMsgKeySet::RunTests() {
+ test_decoder("");
+ test_decoder(" ");
+ test_decoder("0");
+ test_decoder("1");
+ test_decoder("123");
+ test_decoder(" 123 ");
+ test_decoder(" 123 4");
+ test_decoder(" 1,2, 3, 4");
+ test_decoder("0-70,72-99,100,101");
+ test_decoder(" 0-70 , 72 - 99 ,100,101 ");
+ test_decoder("0 - 268435455");
+ /* This one overflows - we can't help it.
+ test_decoder ("0 - 4294967295"); */
+
+ test_adder();
+
+ test_ranges();
+
+ test_member(false);
+ test_member(true);
+
+ // test_newsrc ("/u/montulli/.newsrc");
+ /* test_newsrc ("/u/jwz/.newsrc");*/
+}
+
+#endif /* DEBUG */
diff --git a/comm/mailnews/base/src/nsMsgKeySet.h b/comm/mailnews/base/src/nsMsgKeySet.h
new file mode 100644
index 0000000000..7390645870
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgKeySet.h
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef _nsMsgKeySet_H_
+#define _nsMsgKeySet_H_
+
+#include "msgCore.h"
+#include "nsTArray.h"
+
+// nsMsgKeySet represents a set of articles. Typically, it is the set of
+// read articles from a .newsrc file, but it can be used for other purposes
+// too.
+
+#if 0
+// If a MSG_NewsHost* is supplied to the creation routine, then that
+// MSG_NewsHost will be notified whenever a change is made to set.
+class MSG_NewsHost;
+#endif
+
+class nsMsgKeySet {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(nsMsgKeySet);
+ // Creates an empty set.
+ static nsMsgKeySet* Create(/* MSG_NewsHost* host = NULL*/);
+
+ // Creates a set from the list of numbers, as might be found in a
+ // newsrc file.
+ static nsMsgKeySet* Create(const char* str /* , MSG_NewsHost* host = NULL*/);
+
+ // FirstNonMember() returns the lowest non-member of the set that is
+ // greater than 0.
+ int32_t FirstNonMember();
+
+ // Output() converts to a string representation suitable for writing to a
+ // .newsrc file.
+ nsresult Output(char** outputStr);
+
+ // IsMember() returns whether the given article is a member of this set.
+ bool IsMember(int32_t art);
+
+ // Add() adds the given article to the set. (Returns 1 if a change was
+ // made, 0 if it was already there, and negative on error.)
+ int Add(int32_t art);
+
+ // Remove() removes the given article from the set.
+ int Remove(int32_t art);
+
+ // AddRange() adds the (inclusive) given range of articles to the set.
+ int AddRange(int32_t first, int32_t last);
+
+ // CountMissingInRange() takes an inclusive range of articles and returns
+ // the number of articles in that range which are not in the set.
+ int32_t CountMissingInRange(int32_t start, int32_t end);
+
+ // FirstMissingRange() takes an inclusive range and finds the first range
+ // of articles that are not in the set. If none, return zeros.
+ int FirstMissingRange(int32_t min, int32_t max, int32_t* first,
+ int32_t* last);
+
+ // LastMissingRange() takes an inclusive range and finds the last range
+ // of articles that are not in the set. If none, return zeros.
+ int LastMissingRange(int32_t min, int32_t max, int32_t* first, int32_t* last);
+
+ int32_t GetLastMember();
+ int32_t GetFirstMember();
+ void SetLastMember(int32_t highWaterMark);
+ // For debugging only...
+ int32_t getLength() { return m_length; }
+
+ /**
+ * Fill the passed in aArray with the keys in the message key set.
+ */
+ nsresult ToMsgKeyArray(nsTArray<nsMsgKey>& aArray);
+
+#ifdef DEBUG
+ static void RunTests();
+#endif
+
+ protected:
+ nsMsgKeySet(/* MSG_NewsHost* host */);
+ explicit nsMsgKeySet(const char* /* , MSG_NewsHost* host */);
+ bool Grow();
+ bool Optimize();
+
+#ifdef DEBUG
+ static void test_decoder(const char*);
+ static void test_adder();
+ static void test_ranges();
+ static void test_member(bool with_cache);
+#endif
+
+ int32_t* m_data; /* the numbers composing the `chunks' */
+ int32_t m_data_size; /* size of that malloc'ed block */
+ int32_t m_length; /* active area */
+
+ int32_t m_cached_value; /* a potential set member, or -1 if unset*/
+ int32_t m_cached_value_index; /* the index into `data' at which a search
+ to determine whether `cached_value' was
+ a member of the set ended. */
+#ifdef NEWSRC_DOES_HOST_STUFF
+ MSG_NewsHost* m_host;
+#endif
+ ~nsMsgKeySet();
+};
+
+#endif /* _nsMsgKeySet_H_ */
diff --git a/comm/mailnews/base/src/nsMsgLineBuffer.cpp b/comm/mailnews/base/src/nsMsgLineBuffer.cpp
new file mode 100644
index 0000000000..60d38b5b8f
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgLineBuffer.cpp
@@ -0,0 +1,351 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "prlog.h"
+#include "prmem.h"
+#include "nsMsgLineBuffer.h"
+#include "nsMsgUtils.h"
+#include "nsIInputStream.h" // used by nsMsgLineStreamBuffer
+
+nsByteArray::nsByteArray() {
+ MOZ_COUNT_CTOR(nsByteArray);
+ m_buffer = NULL;
+ m_bufferSize = 0;
+ m_bufferPos = 0;
+}
+
+nsByteArray::~nsByteArray() {
+ MOZ_COUNT_DTOR(nsByteArray);
+ PR_FREEIF(m_buffer);
+}
+
+nsresult nsByteArray::GrowBuffer(uint64_t desired_size, uint32_t quantum) {
+ if (desired_size > PR_UINT32_MAX) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ if (m_bufferSize < desired_size) {
+ char* new_buf;
+ uint32_t increment = desired_size - m_bufferSize;
+ if (increment < quantum) /* always grow by a minimum of N bytes */
+ increment = quantum;
+
+ new_buf =
+ (m_buffer ? (char*)PR_REALLOC(m_buffer, (m_bufferSize + increment))
+ : (char*)PR_MALLOC(m_bufferSize + increment));
+ if (!new_buf) return NS_ERROR_OUT_OF_MEMORY;
+ m_buffer = new_buf;
+ m_bufferSize += increment;
+ }
+ return NS_OK;
+}
+
+nsresult nsByteArray::AppendString(const char* string) {
+ uint32_t strLength = (string) ? PL_strlen(string) : 0;
+ return AppendBuffer(string, strLength);
+}
+
+nsresult nsByteArray::AppendBuffer(const char* buffer, uint32_t length) {
+ nsresult ret = NS_OK;
+ if (m_bufferPos + length > m_bufferSize)
+ ret = GrowBuffer(m_bufferPos + length, 1024);
+ if (NS_SUCCEEDED(ret)) {
+ memcpy(m_buffer + m_bufferPos, buffer, length);
+ m_bufferPos += length;
+ }
+ return ret;
+}
+
+nsMsgLineBuffer::nsMsgLineBuffer() { MOZ_COUNT_CTOR(nsMsgLineBuffer); }
+
+nsMsgLineBuffer::~nsMsgLineBuffer() { MOZ_COUNT_DTOR(nsMsgLineBuffer); }
+
+nsresult nsMsgLineBuffer::BufferInput(const char* net_buffer,
+ int32_t net_buffer_size) {
+ if (net_buffer_size < 0) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ nsresult status = NS_OK;
+ if (m_bufferPos > 0 && m_buffer && m_buffer[m_bufferPos - 1] == '\r' &&
+ net_buffer_size > 0 && net_buffer[0] != '\n') {
+ /* The last buffer ended with a CR. The new buffer does not start
+ with a LF. This old buffer should be shipped out and discarded. */
+ PR_ASSERT(m_bufferSize > m_bufferPos);
+ if (m_bufferSize <= m_bufferPos) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ if (NS_FAILED(HandleLine(m_buffer, m_bufferPos))) {
+ return NS_ERROR_FAILURE;
+ }
+ m_bufferPos = 0;
+ }
+ while (net_buffer_size > 0) {
+ const char* net_buffer_end = net_buffer + net_buffer_size;
+ const char* newline = 0;
+ const char* s;
+
+ for (s = net_buffer; s < net_buffer_end; s++) {
+ /* Move forward in the buffer until the first newline.
+ Stop when we see CRLF, CR, or LF, or the end of the buffer.
+ *But*, if we see a lone CR at the *very end* of the buffer,
+ treat this as if we had reached the end of the buffer without
+ seeing a line terminator. This is to catch the case of the
+ buffers splitting a CRLF pair, as in "FOO\r\nBAR\r" "\nBAZ\r\n".
+ */
+ if (*s == '\r' || *s == '\n') {
+ newline = s;
+ if (newline[0] == '\r') {
+ if (s == net_buffer_end - 1) {
+ /* CR at end - wait for the next character. */
+ newline = 0;
+ break;
+ } else if (newline[1] == '\n') {
+ /* CRLF seen; swallow both. */
+ newline++;
+ }
+ }
+ newline++;
+ break;
+ }
+ }
+
+ /* Ensure room in the net_buffer and append some or all of the current
+ chunk of data to it. */
+ {
+ const char* end = (newline ? newline : net_buffer_end);
+ uint64_t desired_size = (end - net_buffer) + (uint64_t)m_bufferPos + 1;
+ if (desired_size >= PR_INT32_MAX) {
+ // We're not willing to buffer more than 2GB data without seeing
+ // a newline, something is wrong with the input.
+ // Using this limit prevents us from overflowing.
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (desired_size >= m_bufferSize) {
+ status = GrowBuffer(desired_size, 1024);
+ if (NS_FAILED(status)) return status;
+ }
+ memcpy(m_buffer + m_bufferPos, net_buffer, (end - net_buffer));
+ m_bufferPos += (end - net_buffer);
+ }
+
+ /* Now m_buffer contains either a complete line, or as complete
+ a line as we have read so far.
+
+ If we have a line, process it, and then remove it from `m_buffer'.
+ Then go around the loop again, until we drain the incoming data.
+ */
+ if (!newline) return NS_OK;
+
+ if (NS_FAILED(HandleLine(m_buffer, m_bufferPos))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ net_buffer_size -= (newline - net_buffer);
+ net_buffer = newline;
+ m_bufferPos = 0;
+ }
+ return NS_OK;
+}
+
+// If there's still some data (non CRLF terminated) flush it out
+nsresult nsMsgLineBuffer::Flush() {
+ nsresult rv = NS_OK;
+ if (m_bufferPos > 0) {
+ rv = HandleLine(m_buffer, m_bufferPos);
+ m_bufferPos = 0;
+ }
+ return rv;
+}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// This is a utility class used to efficiently extract lines from an input
+// stream by buffering read but unprocessed stream data in a buffer.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+nsMsgLineStreamBuffer::nsMsgLineStreamBuffer(uint32_t aBufferSize,
+ bool aAllocateNewLines,
+ bool aEatCRLFs, char aLineToken)
+ : m_eatCRLFs(aEatCRLFs),
+ m_allocateNewLines(aAllocateNewLines),
+ m_lineToken(aLineToken) {
+ NS_ASSERTION(aBufferSize > 0, "invalid buffer size!!!");
+ m_dataBuffer = nullptr;
+ m_startPos = 0;
+ m_numBytesInBuffer = 0;
+
+ // used to buffer incoming data by ReadNextLineFromInput
+ if (aBufferSize > 0) {
+ m_dataBuffer = (char*)PR_CALLOC(sizeof(char) * aBufferSize);
+ }
+
+ m_dataBufferSize = aBufferSize;
+}
+
+nsMsgLineStreamBuffer::~nsMsgLineStreamBuffer() {
+ PR_FREEIF(m_dataBuffer); // release our buffer...
+}
+
+nsresult nsMsgLineStreamBuffer::GrowBuffer(uint32_t desiredSize) {
+ char* newBuffer = (char*)PR_REALLOC(m_dataBuffer, desiredSize);
+ NS_ENSURE_TRUE(newBuffer, NS_ERROR_OUT_OF_MEMORY);
+ m_dataBuffer = newBuffer;
+ m_dataBufferSize = desiredSize;
+ return NS_OK;
+}
+
+void nsMsgLineStreamBuffer::ClearBuffer() {
+ m_startPos = 0;
+ m_numBytesInBuffer = 0;
+}
+
+// aInputStream - the input stream we want to read a line from
+// aPauseForMoreData is returned as true if the stream does not yet contain a
+// line and we must wait for more data to come into the stream. Note to people
+// wishing to modify this function: Be *VERY CAREFUL* this is a critical
+// function used by all of our mail protocols including imap, nntp, and pop. If
+// you screw it up, you could break a lot of stuff.....
+
+char* nsMsgLineStreamBuffer::ReadNextLine(nsIInputStream* aInputStream,
+ uint32_t& aNumBytesInLine,
+ bool& aPauseForMoreData,
+ nsresult* prv,
+ bool addLineTerminator) {
+ // try to extract a line from m_inputBuffer. If we don't have an entire line,
+ // then read more bytes out from the stream. If the stream is empty then wait
+ // on the monitor for more data to come in.
+
+ NS_ASSERTION(m_dataBuffer && m_dataBufferSize > 0,
+ "invalid input arguments for read next line from input");
+
+ if (prv) *prv = NS_OK;
+ // initialize out values
+ aPauseForMoreData = false;
+ aNumBytesInLine = 0;
+ char* endOfLine = nullptr;
+ char* startOfLine = m_dataBuffer + m_startPos;
+
+ if (m_numBytesInBuffer > 0) // any data in our internal buffer?
+ endOfLine = PL_strchr(startOfLine, m_lineToken); // see if we already
+ // have a line ending...
+
+ // it's possible that we got here before the first time we receive data from
+ // the server so aInputStream will be nullptr...
+ if (!endOfLine && aInputStream) // get some more data from the server
+ {
+ nsresult rv;
+ uint64_t numBytesInStream = 0;
+ uint32_t numBytesCopied = 0;
+ bool nonBlockingStream;
+ aInputStream->IsNonBlocking(&nonBlockingStream);
+ rv = aInputStream->Available(&numBytesInStream);
+ if (NS_FAILED(rv)) {
+ if (prv) *prv = rv;
+ aNumBytesInLine = 0;
+ return nullptr;
+ }
+ if (!nonBlockingStream && numBytesInStream == 0) // if no data available,
+ numBytesInStream = m_dataBufferSize / 2; // ask for half the data
+ // buffer size.
+
+ // if the number of bytes we want to read from the stream, is greater than
+ // the number of bytes left in our buffer, then we need to shift the start
+ // pos and its contents down to the beginning of m_dataBuffer...
+ uint32_t numFreeBytesInBuffer =
+ m_dataBufferSize - m_startPos - m_numBytesInBuffer;
+ if (numBytesInStream >= numFreeBytesInBuffer) {
+ if (m_startPos) {
+ memmove(m_dataBuffer, startOfLine, m_numBytesInBuffer);
+ // make sure the end of the buffer is terminated
+ m_dataBuffer[m_numBytesInBuffer] = '\0';
+ m_startPos = 0;
+ startOfLine = m_dataBuffer;
+ numFreeBytesInBuffer = m_dataBufferSize - m_numBytesInBuffer;
+ }
+ // If we didn't make enough space (or any), grow the buffer
+ if (numBytesInStream >= numFreeBytesInBuffer) {
+ int64_t growBy = (numBytesInStream - numFreeBytesInBuffer) * 2 + 1;
+ // GrowBuffer cannot handle over 4GB size.
+ if (m_dataBufferSize + growBy > PR_UINT32_MAX) return nullptr;
+ // try growing buffer by twice as much as we need.
+ nsresult rv = GrowBuffer(m_dataBufferSize + growBy);
+ // if we can't grow the buffer, we have to bail.
+ if (NS_FAILED(rv)) return nullptr;
+ startOfLine = m_dataBuffer;
+ numFreeBytesInBuffer += growBy;
+ }
+ NS_ASSERTION(m_startPos == 0, "m_startPos should be 0 .....");
+ }
+
+ uint32_t numBytesToCopy = /* leave one for a null terminator */
+ std::min(uint64_t(numFreeBytesInBuffer - 1), numBytesInStream);
+ if (numBytesToCopy > 0) {
+ // read the data into the end of our data buffer
+ char* startOfNewData = startOfLine + m_numBytesInBuffer;
+ rv = aInputStream->Read(startOfNewData, numBytesToCopy, &numBytesCopied);
+ if (prv) *prv = rv;
+ uint32_t i;
+ for (i = 0; i < numBytesCopied; i++) // replace nulls with spaces
+ {
+ if (!startOfNewData[i]) startOfNewData[i] = ' ';
+ }
+ m_numBytesInBuffer += numBytesCopied;
+ m_dataBuffer[m_startPos + m_numBytesInBuffer] = '\0';
+
+ // okay, now that we've tried to read in more data from the stream,
+ // look for another end of line character in the new data
+ endOfLine = PL_strchr(startOfNewData, m_lineToken);
+ }
+ }
+
+ // okay, now check again for endOfLine.
+ if (endOfLine) {
+ if (!m_eatCRLFs) endOfLine += 1; // count for LF or CR
+
+ aNumBytesInLine = endOfLine - startOfLine;
+
+ if (m_eatCRLFs && aNumBytesInLine > 0 &&
+ startOfLine[aNumBytesInLine - 1] == '\r')
+ aNumBytesInLine--; // Remove the CR in a CRLF sequence.
+
+ // PR_CALLOC zeros out the allocated line
+ char* newLine = (char*)PR_CALLOC(
+ aNumBytesInLine + (addLineTerminator ? MSG_LINEBREAK_LEN : 0) + 1);
+ if (!newLine) {
+ aNumBytesInLine = 0;
+ aPauseForMoreData = true;
+ return nullptr;
+ }
+
+ memcpy(newLine, startOfLine,
+ aNumBytesInLine); // copy the string into the new line buffer
+ if (addLineTerminator) {
+ memcpy(newLine + aNumBytesInLine, MSG_LINEBREAK, MSG_LINEBREAK_LEN);
+ aNumBytesInLine += MSG_LINEBREAK_LEN;
+ }
+
+ if (m_eatCRLFs)
+ endOfLine += 1; // advance past LF or CR if we haven't already done so...
+
+ // now we need to update the data buffer to go past the line we just read
+ // out.
+ m_numBytesInBuffer -= (endOfLine - startOfLine);
+ if (m_numBytesInBuffer)
+ m_startPos = endOfLine - m_dataBuffer;
+ else
+ m_startPos = 0;
+
+ return newLine;
+ }
+
+ aPauseForMoreData = true;
+ return nullptr; // if we somehow got here. we don't have another line in the
+ // buffer yet...need to wait for more data...
+}
+
+bool nsMsgLineStreamBuffer::NextLineAvailable() {
+ return (m_numBytesInBuffer > 0 &&
+ PL_strchr(m_dataBuffer + m_startPos, m_lineToken));
+}
diff --git a/comm/mailnews/base/src/nsMsgLineBuffer.h b/comm/mailnews/base/src/nsMsgLineBuffer.h
new file mode 100644
index 0000000000..2ac579be3b
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgLineBuffer.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; 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/. */
+#ifndef _nsMsgLineBuffer_H
+#define _nsMsgLineBuffer_H
+
+#include "msgCore.h" // precompiled header...
+
+// I can't believe I have to have this stupid class, but I can't find
+// anything suitable (nsStrImpl might be, when it's done). nsIByteBuffer
+// would do, if I had a stream for input, which I don't.
+
+class nsByteArray {
+ public:
+ nsByteArray();
+ virtual ~nsByteArray();
+ uint32_t GetSize() { return m_bufferSize; }
+ uint32_t GetBufferPos() { return m_bufferPos; }
+ nsresult GrowBuffer(uint64_t desired_size, uint32_t quantum = 1024);
+ nsresult AppendString(const char* string);
+ nsresult AppendBuffer(const char* buffer, uint32_t length);
+ void ResetWritePos() { m_bufferPos = 0; }
+ char* GetBuffer() { return m_buffer; }
+
+ protected:
+ char* m_buffer;
+ uint32_t m_bufferSize;
+ uint32_t
+ m_bufferPos; // write Pos in m_buffer - where the next byte should go.
+};
+
+/**
+ * nsMsgLineBuffer breaks up incoming data into lines.
+ * It accepts CRLF, CR or LF line endings.
+ *
+ * Data is fed in via BufferInput(). The virtual HandleLine() will be
+ * invoked for each line. The data passed to HandleLine() is verbatim,
+ * and will include whatever line endings were in the source data.
+ *
+ * Flush() should be called when the data is exhausted, to handle any
+ * leftover bytes in the buffer (e.g. if the data doesn't end with an EOL).
+ */
+class nsMsgLineBuffer : private nsByteArray {
+ public:
+ nsMsgLineBuffer();
+ virtual ~nsMsgLineBuffer();
+ nsresult BufferInput(const char* net_buffer, int32_t net_buffer_size);
+
+ /**
+ * HandleLine should be implemented by derived classes, to handle a line.
+ * The line will have whatever end-of-line characters were present in the
+ * source data (potentially none, if the data ends mid-line).
+ */
+ virtual nsresult HandleLine(const char* line, uint32_t line_length) = 0;
+
+ /**
+ * Flush processes any unprocessed data currently in the buffer. Should
+ * be called when the source data is exhausted.
+ */
+ nsresult Flush();
+};
+
+// I'm adding this utility class here for lack of a better place. This utility
+// class is similar to nsMsgLineBuffer except it works from an input stream. It
+// is geared towards efficiently parsing new lines out of a stream by storing
+// read but unprocessed bytes in a buffer. I envision the primary use of this to
+// be our mail protocols such as imap, news and pop which need to process line
+// by line data being returned in the form of a proxied stream from the server.
+
+class nsIInputStream;
+
+class nsMsgLineStreamBuffer {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(nsMsgLineStreamBuffer)
+
+ // aBufferSize -- size of the buffer you want us to use for buffering stream
+ // data
+ // aEndOfLinetoken -- The delimiter string to be used for determining the end
+ // of line. This allows us to parse platform specific end of
+ // line endings by making it a parameter.
+ // aAllocateNewLines -- true if you want calls to ReadNextLine to allocate new
+ // memory for the line.
+ // if false, the char * returned is just a ptr into the buffer.
+ // Subsequent calls to ReadNextLine will alter the data so your
+ // ptr only has a life time of a per call.
+ // aEatCRLFs -- true if you don't want to see the CRLFs on the lines
+ // returned by ReadNextLine.
+ // false if you do want to see them.
+ // aLineToken -- Specify the line token to look for, by default is LF ('\n')
+ // which cover as well CRLF. If lines are terminated with a CR
+ // only, you need to set aLineToken to CR ('\r')
+ nsMsgLineStreamBuffer(
+ uint32_t aBufferSize, bool aAllocateNewLines, bool aEatCRLFs = true,
+ char aLineToken = '\n'); // specify the size of the buffer you want the
+ // class to use....
+
+ // Caller must free the line returned using PR_Free
+ // aEndOfLinetoken -- delimiter used to denote the end of a line.
+ // aNumBytesInLine -- The number of bytes in the line returned
+ // aPauseForMoreData -- There is not enough data in the stream to make a line
+ // at this time...
+ char* ReadNextLine(nsIInputStream* aInputStream, uint32_t& aNumBytesInLine,
+ bool& aPauseForMoreData, nsresult* rv = nullptr,
+ bool addLineTerminator = false);
+ nsresult GrowBuffer(uint32_t desiredSize);
+ void ClearBuffer();
+ bool NextLineAvailable();
+
+ private:
+ virtual ~nsMsgLineStreamBuffer();
+
+ protected:
+ bool m_eatCRLFs;
+ bool m_allocateNewLines;
+ char* m_dataBuffer;
+ uint32_t m_dataBufferSize;
+ uint32_t m_startPos;
+ uint32_t m_numBytesInBuffer;
+ char m_lineToken;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgMailNewsUrl.cpp b/comm/mailnews/base/src/nsMsgMailNewsUrl.cpp
new file mode 100644
index 0000000000..3796aeaef2
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgMailNewsUrl.cpp
@@ -0,0 +1,1070 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsIMsgAccountManager.h"
+#include "nsString.h"
+#include "nsILoadGroup.h"
+#include "nsIDocShell.h"
+#include "nsIWebProgress.h"
+#include "nsIWebProgressListener.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIIOService.h"
+#include "nsNetCID.h"
+#include "nsIStreamListener.h"
+#include "nsIOutputStream.h"
+#include "nsIInputStream.h"
+#include "nsNetUtil.h"
+#include "nsIFile.h"
+#include "prmem.h"
+#include <time.h>
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+#include "nsProxyRelease.h"
+#include "mozilla/Encoding.h"
+#include "nsDocShellLoadState.h"
+#include "nsContentUtils.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIChannel.h"
+
+nsMsgMailNewsUrl::nsMsgMailNewsUrl() {
+ // nsIURI specific state
+ m_runningUrl = false;
+ m_updatingFolder = false;
+ m_msgIsInLocalCache = false;
+ m_suppressErrorMsgs = false;
+ m_hasNormalizedOrigin = false; // SetSpecInternal() will set this correctly.
+ mMaxProgress = -1;
+}
+
+#define NOTIFY_URL_LISTENERS(propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<nsCOMPtr<nsIUrlListener>>::ForwardIterator iter( \
+ mUrlListeners); \
+ while (iter.HasMore()) { \
+ nsCOMPtr<nsIUrlListener> listener = iter.GetNext(); \
+ listener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+nsMsgMailNewsUrl::~nsMsgMailNewsUrl() {
+ // In IMAP this URL is created and destroyed on the imap thread,
+ // so we must ensure that releases of XPCOM objects (which might be
+ // implemented by non-threadsafe JS components) are released on the
+ // main thread.
+ NS_ReleaseOnMainThread("nsMsgMailNewsUrl::m_baseURL", m_baseURL.forget());
+ NS_ReleaseOnMainThread("nsMsgMailNewsUrl::mMimeHeaders",
+ mMimeHeaders.forget());
+ NS_ReleaseOnMainThread("nsMsgMailNewsUrl::m_searchSession",
+ m_searchSession.forget());
+
+ nsTObserverArray<nsCOMPtr<nsIUrlListener>>::ForwardIterator iter(
+ mUrlListeners);
+ while (iter.HasMore()) {
+ nsCOMPtr<nsIUrlListener> listener = iter.GetNext();
+ if (listener)
+ NS_ReleaseOnMainThread("nsMsgMailNewsUrl::mUrlListeners",
+ listener.forget());
+ }
+}
+
+NS_IMPL_ADDREF(nsMsgMailNewsUrl)
+NS_IMPL_RELEASE(nsMsgMailNewsUrl)
+
+// We want part URLs to QI to nsIURIWithSpecialOrigin so we can give
+// them a "normalized" origin. URLs that already have a "normalized"
+// origin should not QI to nsIURIWithSpecialOrigin.
+NS_INTERFACE_MAP_BEGIN(nsMsgMailNewsUrl)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMsgMailNewsUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgMailNewsUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIURL)
+ NS_INTERFACE_MAP_ENTRY(nsIURI)
+ NS_INTERFACE_MAP_ENTRY(nsISerializable)
+ NS_INTERFACE_MAP_ENTRY(nsIClassInfo)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIURIWithSpecialOrigin,
+ m_hasNormalizedOrigin)
+NS_INTERFACE_MAP_END
+
+//--------------------------
+// Support for serialization
+//--------------------------
+// nsMsgMailNewsUrl is only partly serialized by serializing the "base URL"
+// which is an nsStandardURL, or by only serializing the Spec. This may
+// cause problems in the future. See bug 1512356 and bug 1515337 for details,
+// follow-up in bug 1512698.
+
+NS_IMETHODIMP_(void)
+nsMsgMailNewsUrl::Serialize(mozilla::ipc::URIParams& aParams) {
+ m_baseURL->Serialize(aParams);
+}
+
+//----------------------------
+// Support for nsISerializable
+//----------------------------
+NS_IMETHODIMP nsMsgMailNewsUrl::Read(nsIObjectInputStream* stream) {
+ nsAutoCString urlstr;
+ nsresult rv = NS_ReadOptionalCString(stream, urlstr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIURI> url;
+ rv = ioService->NewURI(urlstr, nullptr, nullptr, getter_AddRefs(url));
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_baseURL = do_QueryInterface(url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::Write(nsIObjectOutputStream* stream) {
+ nsAutoCString urlstr;
+ nsresult rv = m_baseURL->GetSpec(urlstr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_WriteOptionalStringZ(stream, urlstr.get());
+}
+
+//-------------------------
+// Support for nsIClassInfo
+//-------------------------
+NS_IMETHODIMP nsMsgMailNewsUrl::GetInterfaces(nsTArray<nsIID>& array) {
+ array.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetScriptableHelper(
+ nsIXPCScriptable** _retval) {
+ *_retval = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetContractID(nsACString& aContractID) {
+ aContractID.SetIsVoid(true);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetClassDescription(
+ nsACString& aClassDescription) {
+ aClassDescription.SetIsVoid(true);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetClassID(nsCID** aClassID) {
+ *aClassID = (nsCID*)moz_xmalloc(sizeof(nsCID));
+ return GetClassIDNoAlloc(*aClassID);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFlags(uint32_t* aFlags) {
+ *aFlags = 0;
+ return NS_OK;
+}
+
+#define NS_MSGMAILNEWSURL_CID \
+ { \
+ 0x3fdae3ab, 0x4ac1, 0x4ad4, { \
+ 0xb2, 0x8a, 0x28, 0xd0, 0xfa, 0x36, 0x39, 0x29 \
+ } \
+ }
+static NS_DEFINE_CID(kNS_MSGMAILNEWSURL_CID, NS_MSGMAILNEWSURL_CID);
+NS_IMETHODIMP nsMsgMailNewsUrl::GetClassIDNoAlloc(nsCID* aClassIDNoAlloc) {
+ *aClassIDNoAlloc = kNS_MSGMAILNEWSURL_CID;
+ return NS_OK;
+}
+
+//------------------------------------
+// Support for nsIURIWithSpecialOrigin
+//------------------------------------
+NS_IMETHODIMP nsMsgMailNewsUrl::GetOrigin(nsIURI** aOrigin) {
+ MOZ_ASSERT(m_hasNormalizedOrigin,
+ "nsMsgMailNewsUrl::GetOrigin() can only be called for URLs with "
+ "normalized spec");
+
+ if (!m_normalizedOrigin) {
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl;
+ QueryInterface(NS_GET_IID(nsIMsgMessageUrl), getter_AddRefs(msgUrl));
+
+ nsAutoCString spec;
+ if (!msgUrl || NS_FAILED(msgUrl->GetNormalizedSpec(spec))) {
+ MOZ_ASSERT(false, "Can't get normalized spec");
+ // just use the normal spec.
+ GetSpec(spec);
+ }
+
+ nsresult rv = NS_NewURI(getter_AddRefs(m_normalizedOrigin), spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aOrigin = m_normalizedOrigin);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// Begin nsIMsgMailNewsUrl specific support
+////////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsMsgMailNewsUrl::GetUrlState(bool* aRunningUrl) {
+ if (aRunningUrl) *aRunningUrl = m_runningUrl;
+
+ return NS_OK;
+}
+
+nsresult nsMsgMailNewsUrl::SetUrlState(bool aRunningUrl, nsresult aExitCode) {
+ // if we already knew this running state, return, unless the url was aborted
+ if (m_runningUrl == aRunningUrl && aExitCode != NS_MSG_ERROR_URL_ABORTED) {
+ return NS_OK;
+ }
+ m_runningUrl = aRunningUrl;
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+
+ // put this back - we need it for urls that don't run through the doc loader
+ if (NS_SUCCEEDED(GetStatusFeedback(getter_AddRefs(statusFeedback))) &&
+ statusFeedback) {
+ if (m_runningUrl)
+ statusFeedback->StartMeteors();
+ else {
+ statusFeedback->ShowProgress(0);
+ statusFeedback->StopMeteors();
+ }
+ }
+
+ if (m_runningUrl) {
+ NOTIFY_URL_LISTENERS(OnStartRunningUrl, (this));
+ } else {
+ NOTIFY_URL_LISTENERS(OnStopRunningUrl, (this, aExitCode));
+ mUrlListeners.Clear();
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::RegisterListener(nsIUrlListener* aUrlListener) {
+ NS_ENSURE_ARG_POINTER(aUrlListener);
+ mUrlListeners.AppendElement(aUrlListener);
+ return NS_OK;
+}
+
+nsresult nsMsgMailNewsUrl::UnRegisterListener(nsIUrlListener* aUrlListener) {
+ NS_ENSURE_ARG_POINTER(aUrlListener);
+
+ // Due to the way mailnews is structured, some listeners attempt to remove
+ // themselves twice. This may in fact be an error in the coding, however
+ // if they didn't do it as they do currently, then they could fail to remove
+ // their listeners.
+ mUrlListeners.RemoveElement(aUrlListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetServer(
+ nsIMsgIncomingServer** aIncomingServer) {
+ // mscott --> we could cache a copy of the server here....but if we did, we
+ // run the risk of leaking the server if any single url gets leaked....of
+ // course that shouldn't happen...but it could. so i'm going to look it up
+ // every time and we can look at caching it later.
+
+ nsresult rv;
+
+ nsAutoCString urlstr;
+ rv = m_baseURL->GetSpec(urlstr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURL> url;
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(urlstr)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString scheme;
+ rv = GetScheme(scheme);
+ if (NS_SUCCEEDED(rv)) {
+ if (scheme.EqualsLiteral("pop")) scheme.AssignLiteral("pop3");
+ // we use "nntp" in the server list so translate it here.
+ if (scheme.EqualsLiteral("news")) scheme.AssignLiteral("nntp");
+ rv = NS_MutateURI(url).SetScheme(scheme).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = accountManager->FindServerByURI(url, aIncomingServer);
+ if (!*aIncomingServer && scheme.EqualsLiteral("imap")) {
+ // look for any imap server with this host name so clicking on
+ // other users folder urls will work. We could override this method
+ // for imap urls, or we could make caching of servers work and
+ // just set the server in the imap code for this case.
+ rv = NS_MutateURI(url).SetUserPass(EmptyCString()).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = accountManager->FindServerByURI(url, aIncomingServer);
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMsgWindow(nsIMsgWindow** aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+ *aMsgWindow = nullptr;
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ msgWindow.forget(aMsgWindow);
+ return *aMsgWindow ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMsgWindow(nsIMsgWindow* aMsgWindow) {
+#ifdef DEBUG_David_Bienvenu
+ NS_ASSERTION(aMsgWindow || !m_msgWindowWeak,
+ "someone crunching non-null msg window");
+#endif
+ m_msgWindowWeak = do_GetWeakReference(aMsgWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetStatusFeedback(
+ nsIMsgStatusFeedback** aMsgFeedback) {
+ // note: it is okay to return a null status feedback and not return an error
+ // it's possible the url really doesn't have status feedback
+ *aMsgFeedback = nullptr;
+ if (!m_statusFeedbackWeak) {
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ if (msgWindow) msgWindow->GetStatusFeedback(aMsgFeedback);
+ } else {
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback(
+ do_QueryReferent(m_statusFeedbackWeak));
+ statusFeedback.forget(aMsgFeedback);
+ }
+ return *aMsgFeedback ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetStatusFeedback(
+ nsIMsgStatusFeedback* aMsgFeedback) {
+ if (aMsgFeedback) m_statusFeedbackWeak = do_GetWeakReference(aMsgFeedback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMaxProgress(int64_t* aMaxProgress) {
+ *aMaxProgress = mMaxProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMaxProgress(int64_t aMaxProgress) {
+ mMaxProgress = aMaxProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ *aLoadGroup = nullptr;
+ // note: it is okay to return a null load group and not return an error
+ // it's possible the url really doesn't have load group
+ nsCOMPtr<nsILoadGroup> loadGroup(do_QueryReferent(m_loadGroupWeak));
+ if (!loadGroup) {
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ if (msgWindow) {
+ // XXXbz This is really weird... why are we getting some
+ // random loadgroup we're not really a part of?
+ nsCOMPtr<nsIDocShell> docShell;
+ msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ loadGroup = do_GetInterface(docShell);
+ m_loadGroupWeak = do_GetWeakReference(loadGroup);
+ }
+ }
+ loadGroup.forget(aLoadGroup);
+ return *aLoadGroup ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetUpdatingFolder(bool* aResult) {
+ NS_ENSURE_ARG(aResult);
+ *aResult = m_updatingFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetUpdatingFolder(bool updatingFolder) {
+ m_updatingFolder = updatingFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMsgIsInLocalCache(bool* aMsgIsInLocalCache) {
+ NS_ENSURE_ARG(aMsgIsInLocalCache);
+ *aMsgIsInLocalCache = m_msgIsInLocalCache;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMsgIsInLocalCache(bool aMsgIsInLocalCache) {
+ m_msgIsInLocalCache = aMsgIsInLocalCache;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSuppressErrorMsgs(bool* aSuppressErrorMsgs) {
+ NS_ENSURE_ARG(aSuppressErrorMsgs);
+ *aSuppressErrorMsgs = m_suppressErrorMsgs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetSuppressErrorMsgs(bool aSuppressErrorMsgs) {
+ m_suppressErrorMsgs = aSuppressErrorMsgs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetErrorCode(nsACString& aErrorCode) {
+ aErrorCode = m_errorCode;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetErrorCode(const nsACString& aErrorCode) {
+ m_errorCode.Assign(aErrorCode);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetErrorMessage(nsAString& aErrorMessage) {
+ aErrorMessage = m_errorMessage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetErrorMessage(
+ const nsAString& aErrorMessage) {
+ m_errorMessage.Assign(aErrorMessage);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetSeeOtherURI(const nsACString& aSeeOtherURI) {
+ m_seeOtherURI.Assign(aSeeOtherURI);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSeeOtherURI(nsACString& aSeeOtherURI) {
+ aSeeOtherURI = m_seeOtherURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::IsUrlType(uint32_t type, bool* isType) {
+ // base class doesn't know about any specific types
+ NS_ENSURE_ARG(isType);
+ *isType = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetSearchSession(
+ nsIMsgSearchSession* aSearchSession) {
+ if (aSearchSession) m_searchSession = aSearchSession;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSearchSession(
+ nsIMsgSearchSession** aSearchSession) {
+ NS_ENSURE_ARG(aSearchSession);
+ NS_IF_ADDREF(*aSearchSession = m_searchSession);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////////
+// End nsIMsgMailNewsUrl specific support
+////////////////////////////////////////////////////////////////////////////////////
+
+////////////////////////////////////////////////////////////////////////////////////
+// Begin nsIURI support
+////////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSpec(nsACString& aSpec) {
+ return m_baseURL->GetSpec(aSpec);
+}
+
+nsresult nsMsgMailNewsUrl::CreateURL(const nsACString& aSpec, nsIURL** aURL) {
+ nsCOMPtr<nsIURL> url;
+ nsresult rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(aSpec)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ url.forget(aURL);
+ return NS_OK;
+}
+
+#define FILENAME_PART_LEN 10
+
+nsresult nsMsgMailNewsUrl::SetSpecInternal(const nsACString& aSpec) {
+ nsAutoCString spec(aSpec);
+ // Parse out "filename" attribute if present.
+ char *start, *end;
+ start = PL_strcasestr(spec.BeginWriting(), "?filename=");
+ if (!start) start = PL_strcasestr(spec.BeginWriting(), "&filename=");
+ if (start) { // Make sure we only get our own value.
+ end = PL_strcasestr((char*)(start + FILENAME_PART_LEN), "&");
+ if (end) {
+ *end = 0;
+ mAttachmentFileName = start + FILENAME_PART_LEN;
+ *end = '&';
+ } else
+ mAttachmentFileName = start + FILENAME_PART_LEN;
+ }
+
+ // Now, set the rest.
+ nsresult rv = CreateURL(aSpec, getter_AddRefs(m_baseURL));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check whether the URL is in normalized form.
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl;
+ QueryInterface(NS_GET_IID(nsIMsgMessageUrl), getter_AddRefs(msgUrl));
+
+ nsAutoCString normalizedSpec;
+ if (!msgUrl || NS_FAILED(msgUrl->GetNormalizedSpec(normalizedSpec))) {
+ // If we can't get the normalized spec, never QI this to
+ // nsIURIWithSpecialOrigin.
+ m_hasNormalizedOrigin = false;
+ } else {
+ m_hasNormalizedOrigin = !spec.Equals(normalizedSpec);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetPrePath(nsACString& aPrePath) {
+ return m_baseURL->GetPrePath(aPrePath);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetScheme(nsACString& aScheme) {
+ return m_baseURL->GetScheme(aScheme);
+}
+
+nsresult nsMsgMailNewsUrl::SetScheme(const nsACString& aScheme) {
+ return NS_MutateURI(m_baseURL).SetScheme(aScheme).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetUserPass(nsACString& aUserPass) {
+ return m_baseURL->GetUserPass(aUserPass);
+}
+
+nsresult nsMsgMailNewsUrl::SetUserPass(const nsACString& aUserPass) {
+ return NS_MutateURI(m_baseURL).SetUserPass(aUserPass).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetUsername(nsACString& aUsername) {
+ /* note: this will return an escaped string */
+ return m_baseURL->GetUsername(aUsername);
+}
+
+nsresult nsMsgMailNewsUrl::SetUsername(const nsACString& aUsername) {
+ return NS_MutateURI(m_baseURL).SetUsername(aUsername).Finalize(m_baseURL);
+}
+
+nsresult nsMsgMailNewsUrl::SetUsernameInternal(const nsACString& aUsername) {
+ return NS_MutateURI(m_baseURL).SetUsername(aUsername).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetPassword(nsACString& aPassword) {
+ return m_baseURL->GetPassword(aPassword);
+}
+
+nsresult nsMsgMailNewsUrl::SetPassword(const nsACString& aPassword) {
+ return NS_MutateURI(m_baseURL).SetPassword(aPassword).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetHostPort(nsACString& aHostPort) {
+ return m_baseURL->GetHostPort(aHostPort);
+}
+
+nsresult nsMsgMailNewsUrl::SetHostPort(const nsACString& aHostPort) {
+ return NS_MutateURI(m_baseURL).SetHostPort(aHostPort).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetHost(nsACString& aHost) {
+ return m_baseURL->GetHost(aHost);
+}
+
+nsresult nsMsgMailNewsUrl::SetHost(const nsACString& aHost) {
+ return NS_MutateURI(m_baseURL).SetHost(aHost).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetPort(int32_t* aPort) {
+ return m_baseURL->GetPort(aPort);
+}
+
+nsresult nsMsgMailNewsUrl::SetPort(int32_t aPort) {
+ return NS_MutateURI(m_baseURL).SetPort(aPort).Finalize(m_baseURL);
+}
+
+nsresult nsMsgMailNewsUrl::SetPortInternal(int32_t aPort) {
+ return NS_MutateURI(m_baseURL).SetPort(aPort).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetPathQueryRef(nsACString& aPath) {
+ return m_baseURL->GetPathQueryRef(aPath);
+}
+
+nsresult nsMsgMailNewsUrl::SetPathQueryRef(const nsACString& aPath) {
+ return NS_MutateURI(m_baseURL).SetPathQueryRef(aPath).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetAsciiHost(nsACString& aHostA) {
+ return m_baseURL->GetAsciiHost(aHostA);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetAsciiHostPort(nsACString& aHostPortA) {
+ return m_baseURL->GetAsciiHostPort(aHostPortA);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetAsciiSpec(nsACString& aSpecA) {
+ return m_baseURL->GetAsciiSpec(aSpecA);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetBaseURI(nsIURI** aBaseURI) {
+ NS_ENSURE_ARG_POINTER(aBaseURI);
+ return m_baseURL->QueryInterface(NS_GET_IID(nsIURI), (void**)aBaseURI);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::Equals(nsIURI* other, bool* _retval) {
+ // The passed-in URI might be a mail news url. Pass our inner URL to its
+ // Equals method. The other mail news url will then pass its inner URL to
+ // to the Equals method of our inner URL. Other URIs will return false.
+ if (other) return other->Equals(m_baseURL, _retval);
+
+ return m_baseURL->Equals(other, _retval);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::EqualsExceptRef(nsIURI* other, bool* result) {
+ // The passed-in URI might be a mail news url. Pass our inner URL to its
+ // Equals method. The other mail news url will then pass its inner URL to
+ // to the Equals method of our inner URL. Other URIs will return false.
+ if (other) return other->EqualsExceptRef(m_baseURL, result);
+
+ return m_baseURL->EqualsExceptRef(other, result);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetSpecIgnoringRef(nsACString& result) {
+ return m_baseURL->GetSpecIgnoringRef(result);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetDisplaySpec(nsACString& aUnicodeSpec) {
+ return m_baseURL->GetDisplaySpec(aUnicodeSpec);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetDisplayHostPort(nsACString& aHostPort) {
+ return m_baseURL->GetDisplayHostPort(aHostPort);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetDisplayHost(nsACString& aHost) {
+ return m_baseURL->GetDisplayHost(aHost);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetDisplayPrePath(nsACString& aPrePath) {
+ return m_baseURL->GetDisplayPrePath(aPrePath);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::GetHasRef(bool* result) {
+ return m_baseURL->GetHasRef(result);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SchemeIs(const char* aScheme, bool* _retval) {
+ return m_baseURL->SchemeIs(aScheme, _retval);
+}
+
+nsresult nsMsgMailNewsUrl::Clone(nsIURI** _retval) {
+ nsresult rv;
+ nsAutoCString urlSpec;
+ nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED);
+ rv = GetSpec(urlSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> newUri;
+ rv = ioService->NewURI(urlSpec, nullptr, nullptr, getter_AddRefs(newUri));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // add the msg window to the cloned url
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak));
+ if (msgWindow) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgMailNewsUrl = do_QueryInterface(newUri, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgMailNewsUrl->SetMsgWindow(msgWindow);
+ }
+
+ newUri.forget(_retval);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::Resolve(const nsACString& relativePath,
+ nsACString& result) {
+ // only resolve anchor urls....i.e. urls which start with '#' against the
+ // mailnews url... everything else shouldn't be resolved against mailnews
+ // urls.
+ nsresult rv = NS_OK;
+
+ if (relativePath.IsEmpty()) {
+ // Return base URL.
+ rv = GetSpec(result);
+ } else if (!relativePath.IsEmpty() &&
+ relativePath.First() == '#') // an anchor
+ {
+ rv = m_baseURL->Resolve(relativePath, result);
+ } else {
+ // if relativePath is a complete url with it's own scheme then allow it...
+ nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED);
+ nsAutoCString scheme;
+
+ rv = ioService->ExtractScheme(relativePath, scheme);
+ // if we have a fully qualified scheme then pass the relative path back as
+ // the result
+ if (NS_SUCCEEDED(rv) && !scheme.IsEmpty()) {
+ result = relativePath;
+ rv = NS_OK;
+ } else {
+ result.Truncate();
+ rv = NS_ERROR_FAILURE;
+ }
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetDirectory(nsACString& aDirectory) {
+ return m_baseURL->GetDirectory(aDirectory);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFileName(nsACString& aFileName) {
+ if (!mAttachmentFileName.IsEmpty()) {
+ aFileName = mAttachmentFileName;
+ return NS_OK;
+ }
+ return m_baseURL->GetFileName(aFileName);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFileBaseName(nsACString& aFileBaseName) {
+ return m_baseURL->GetFileBaseName(aFileBaseName);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFileExtension(nsACString& aFileExtension) {
+ if (!mAttachmentFileName.IsEmpty()) {
+ int32_t pos = mAttachmentFileName.RFindChar(char16_t('.'));
+ if (pos > 0)
+ aFileExtension =
+ Substring(mAttachmentFileName, pos + 1 /* skip the '.' */);
+ return NS_OK;
+ }
+ return m_baseURL->GetFileExtension(aFileExtension);
+}
+
+nsresult nsMsgMailNewsUrl::SetFileNameInternal(const nsACString& aFileName) {
+ mAttachmentFileName = aFileName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetQuery(nsACString& aQuery) {
+ return m_baseURL->GetQuery(aQuery);
+}
+
+nsresult nsMsgMailNewsUrl::SetQuery(const nsACString& aQuery) {
+ return NS_MutateURI(m_baseURL).SetQuery(aQuery).Finalize(m_baseURL);
+}
+
+nsresult nsMsgMailNewsUrl::SetQueryInternal(const nsACString& aQuery) {
+ return NS_MutateURI(m_baseURL).SetQuery(aQuery).Finalize(m_baseURL);
+}
+
+nsresult nsMsgMailNewsUrl::SetQueryWithEncoding(
+ const nsACString& aQuery, const mozilla::Encoding* aEncoding) {
+ return NS_MutateURI(m_baseURL)
+ .SetQueryWithEncoding(aQuery, aEncoding)
+ .Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetRef(nsACString& aRef) {
+ return m_baseURL->GetRef(aRef);
+}
+
+nsresult nsMsgMailNewsUrl::SetRef(const nsACString& aRef) {
+ return NS_MutateURI(m_baseURL).SetRef(aRef).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFilePath(nsACString& o_DirFile) {
+ return m_baseURL->GetFilePath(o_DirFile);
+}
+
+nsresult nsMsgMailNewsUrl::SetFilePath(const nsACString& i_DirFile) {
+ return NS_MutateURI(m_baseURL).SetFilePath(i_DirFile).Finalize(m_baseURL);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetCommonBaseSpec(nsIURI* uri2,
+ nsACString& result) {
+ return m_baseURL->GetCommonBaseSpec(uri2, result);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetRelativeSpec(nsIURI* uri2,
+ nsACString& result) {
+ return m_baseURL->GetRelativeSpec(uri2, result);
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMemCacheEntry(nsICacheEntry* memCacheEntry) {
+ m_memCacheEntry = memCacheEntry;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMemCacheEntry(
+ nsICacheEntry** memCacheEntry) {
+ NS_ENSURE_ARG(memCacheEntry);
+ nsresult rv = NS_OK;
+
+ if (m_memCacheEntry) {
+ NS_ADDREF(*memCacheEntry = m_memCacheEntry);
+ } else {
+ *memCacheEntry = nullptr;
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetMimeHeaders(nsIMimeHeaders** mimeHeaders) {
+ NS_ENSURE_ARG_POINTER(mimeHeaders);
+ NS_IF_ADDREF(*mimeHeaders = mMimeHeaders);
+ return (mMimeHeaders) ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetMimeHeaders(nsIMimeHeaders* mimeHeaders) {
+ mMimeHeaders = mimeHeaders;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::LoadURI(nsIDocShell* docShell,
+ uint32_t aLoadFlags) {
+ NS_ENSURE_ARG_POINTER(docShell);
+ RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(this);
+ loadState->SetLoadFlags(aLoadFlags);
+ loadState->SetLoadType(MAKE_LOAD_TYPE(LOAD_NORMAL, aLoadFlags));
+ loadState->SetFirstParty(false);
+ loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal());
+ return docShell->LoadURI(loadState, false);
+}
+
+#define SAVE_BUF_SIZE FILE_IO_BUFFER_SIZE
+class nsMsgSaveAsListener : public nsIStreamListener {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ nsMsgSaveAsListener(nsIFile* aFile, bool addDummyEnvelope);
+ nsresult SetupMsgWriteStream(nsIFile* aFile, bool addDummyEnvelope);
+
+ protected:
+ virtual ~nsMsgSaveAsListener();
+ nsCOMPtr<nsIOutputStream> m_outputStream;
+ nsCOMPtr<nsIFile> m_outputFile;
+ bool m_addDummyEnvelope;
+ bool m_writtenData;
+ uint32_t m_leftOver;
+ char m_dataBuffer[SAVE_BUF_SIZE +
+ 1]; // temporary buffer for this save operation
+};
+
+NS_IMPL_ISUPPORTS(nsMsgSaveAsListener, nsIStreamListener, nsIRequestObserver)
+
+nsMsgSaveAsListener::nsMsgSaveAsListener(nsIFile* aFile,
+ bool addDummyEnvelope) {
+ m_outputFile = aFile;
+ m_writtenData = false;
+ m_addDummyEnvelope = addDummyEnvelope;
+ m_leftOver = 0;
+}
+
+nsMsgSaveAsListener::~nsMsgSaveAsListener() {}
+
+NS_IMETHODIMP nsMsgSaveAsListener::OnStartRequest(nsIRequest* request) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSaveAsListener::OnStopRequest(nsIRequest* request, nsresult aStatus) {
+ if (m_outputStream) {
+ m_outputStream->Flush();
+ m_outputStream->Close();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSaveAsListener::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStream,
+ uint64_t srcOffset,
+ uint32_t count) {
+ nsresult rv;
+ uint64_t available;
+ rv = inStream->Available(&available);
+ if (!m_writtenData) {
+ m_writtenData = true;
+ rv = SetupMsgWriteStream(m_outputFile, m_addDummyEnvelope);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ bool useCanonicalEnding = false;
+ // We know the request is an nsIChannel we can get a URI from, but this is
+ // probably bad form. See Bug 1528662.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "error QI nsIRequest to nsIChannel failed");
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIURI> uri;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(uri);
+ if (msgUrl) msgUrl->GetCanonicalLineEnding(&useCanonicalEnding);
+
+ const char* lineEnding = (useCanonicalEnding) ? CRLF : MSG_LINEBREAK;
+ uint32_t lineEndingLength = (useCanonicalEnding) ? 2 : MSG_LINEBREAK_LEN;
+
+ uint32_t readCount, maxReadCount = SAVE_BUF_SIZE - m_leftOver;
+ uint32_t writeCount;
+ char *start, *end, lastCharInPrevBuf = '\0';
+ uint32_t linebreak_len = 0;
+
+ while (count > 0) {
+ if (count < maxReadCount) maxReadCount = count;
+ rv = inStream->Read(m_dataBuffer + m_leftOver, maxReadCount, &readCount);
+ if (NS_FAILED(rv)) return rv;
+
+ m_leftOver += readCount;
+ m_dataBuffer[m_leftOver] = '\0';
+
+ start = m_dataBuffer;
+ // make sure we don't insert another LF, accidentally, by ignoring
+ // second half of CRLF spanning blocks.
+ if (lastCharInPrevBuf == '\r' && *start == '\n') start++;
+
+ end = PL_strpbrk(start, "\r\n");
+ if (end) linebreak_len = (end[0] == '\r' && end[1] == '\n') ? 2 : 1;
+
+ count -= readCount;
+ maxReadCount = SAVE_BUF_SIZE - m_leftOver;
+
+ if (!end && count > maxReadCount)
+ // must be a very very long line; sorry cannot handle it
+ return NS_ERROR_FAILURE;
+
+ while (start && end) {
+ if (m_outputStream && PL_strncasecmp(start, "X-Mozilla-Status:", 17) &&
+ PL_strncasecmp(start, "X-Mozilla-Status2:", 18) &&
+ PL_strncmp(start, "From - ", 7)) {
+ rv = m_outputStream->Write(start, end - start, &writeCount);
+ nsresult tmp =
+ m_outputStream->Write(lineEnding, lineEndingLength, &writeCount);
+ if (NS_FAILED(tmp)) {
+ rv = tmp;
+ }
+ }
+ start = end + linebreak_len;
+ if (start >= m_dataBuffer + m_leftOver) {
+ maxReadCount = SAVE_BUF_SIZE;
+ m_leftOver = 0;
+ break;
+ }
+ end = PL_strpbrk(start, "\r\n");
+ if (end) linebreak_len = (end[0] == '\r' && end[1] == '\n') ? 2 : 1;
+ if (start && !end) {
+ m_leftOver -= (start - m_dataBuffer);
+ memcpy(m_dataBuffer, start,
+ m_leftOver + 1); // including null
+ maxReadCount = SAVE_BUF_SIZE - m_leftOver;
+ }
+ }
+ if (NS_FAILED(rv)) return rv;
+ if (end) lastCharInPrevBuf = *end;
+ }
+ return rv;
+
+ // rv = m_outputStream->WriteFrom(inStream, std::min(available, count),
+ // &bytesWritten);
+}
+
+nsresult nsMsgSaveAsListener::SetupMsgWriteStream(nsIFile* aFile,
+ bool addDummyEnvelope) {
+ // If the file already exists, delete it, but do this before
+ // getting the outputstream.
+ // Due to bug 328027, the nsSaveMsgListener created in
+ // nsMessenger::SaveAs now opens the stream on the nsIFile
+ // object, thus creating an empty file. Actual save operations for
+ // IMAP and NNTP use this nsMsgSaveAsListener here, though, so we
+ // have to close the stream before deleting the file, else data
+ // would still be written happily into a now non-existing file.
+ // (Windows doesn't care, btw, just unixoids do...)
+ aFile->Remove(false);
+
+ nsresult rv = MsgNewBufferedFileOutputStream(getter_AddRefs(m_outputStream),
+ aFile, -1, 0666);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (m_outputStream && addDummyEnvelope) {
+ nsAutoCString result;
+ uint32_t writeCount;
+
+ time_t now = time((time_t*)0);
+ char* ct = ctime(&now);
+ // Remove the ending new-line character.
+ ct[24] = '\0';
+ result = "From - ";
+ result += ct;
+ result += MSG_LINEBREAK;
+ m_outputStream->Write(result.get(), result.Length(), &writeCount);
+
+ result = "X-Mozilla-Status: 0001";
+ result += MSG_LINEBREAK;
+ result += "X-Mozilla-Status2: 00000000";
+ result += MSG_LINEBREAK;
+ m_outputStream->Write(result.get(), result.Length(), &writeCount);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetSaveAsListener(
+ bool addDummyEnvelope, nsIFile* aFile, nsIStreamListener** aSaveListener) {
+ NS_ENSURE_ARG_POINTER(aSaveListener);
+ nsMsgSaveAsListener* saveAsListener =
+ new nsMsgSaveAsListener(aFile, addDummyEnvelope);
+ return saveAsListener->QueryInterface(NS_GET_IID(nsIStreamListener),
+ (void**)aSaveListener);
+}
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::SetFailedSecInfo(nsITransportSecurityInfo* secInfo) {
+ mFailedSecInfo = secInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFailedSecInfo(
+ nsITransportSecurityInfo** secInfo) {
+ NS_ENSURE_ARG_POINTER(secInfo);
+ NS_IF_ADDREF(*secInfo = mFailedSecInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::SetFolder(nsIMsgFolder* /* aFolder */) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetFolder(nsIMsgFolder** /* aFolder */) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgMailNewsUrl::GetIsMessageUri(bool* aIsMessageUri) {
+ NS_ENSURE_ARG(aIsMessageUri);
+ nsAutoCString scheme;
+ m_baseURL->GetScheme(scheme);
+ *aIsMessageUri = StringEndsWith(scheme, "-message"_ns);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsMsgMailNewsUrl::Mutator, nsIURISetters, nsIURIMutator)
+
+NS_IMETHODIMP
+nsMsgMailNewsUrl::Mutate(nsIURIMutator** aMutator) {
+ RefPtr<nsMsgMailNewsUrl::Mutator> mutator = new nsMsgMailNewsUrl::Mutator();
+ nsresult rv = mutator->InitFromURI(this);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ mutator.forget(aMutator);
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgMailNewsUrl.h b/comm/mailnews/base/src/nsMsgMailNewsUrl.h
new file mode 100644
index 0000000000..21d56e8e65
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgMailNewsUrl.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef nsMsgMailNewsUrl_h___
+#define nsMsgMailNewsUrl_h___
+
+#include "msgCore.h"
+#include "nscore.h"
+#include "nsISupports.h"
+#include "nsIUrlListener.h"
+#include "nsTObserverArray.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsIMimeHeaders.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIURL.h"
+#include "nsIURIWithSpecialOrigin.h"
+#include "nsILoadGroup.h"
+#include "nsIMsgSearchSession.h"
+#include "nsICacheEntry.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsString.h"
+#include "nsIURIMutator.h"
+#include "nsISerializable.h"
+#include "nsIClassInfo.h"
+#include "nsITransportSecurityInfo.h"
+
+///////////////////////////////////////////////////////////////////////////////////
+// Okay, I found that all of the mail and news url interfaces needed to support
+// several common interfaces (in addition to those provided through nsIURI).
+// So I decided to group them all in this implementation so we don't have to
+// duplicate the code.
+//
+//////////////////////////////////////////////////////////////////////////////////
+
+class NS_MSG_BASE nsMsgMailNewsUrl : public nsIMsgMailNewsUrl,
+ public nsIURIWithSpecialOrigin,
+ public nsISerializable,
+ public nsIClassInfo {
+ public:
+ nsMsgMailNewsUrl();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGMAILNEWSURL
+ NS_DECL_NSIURI
+ NS_DECL_NSIURL
+ NS_DECL_NSIURIWITHSPECIALORIGIN
+ NS_DECL_NSISERIALIZABLE
+ NS_DECL_NSICLASSINFO
+
+ protected:
+ virtual nsresult Clone(nsIURI** _retval);
+ virtual nsresult SetScheme(const nsACString& aScheme);
+ virtual nsresult SetUserPass(const nsACString& aUserPass);
+ virtual nsresult SetUsername(const nsACString& aUsername);
+ virtual nsresult SetPassword(const nsACString& aPassword);
+ virtual nsresult SetHostPort(const nsACString& aHostPort);
+ virtual nsresult SetHost(const nsACString& aHost);
+ virtual nsresult SetPort(int32_t aPort);
+ virtual nsresult SetPathQueryRef(const nsACString& aPath);
+ virtual nsresult SetRef(const nsACString& aRef);
+ virtual nsresult SetFilePath(const nsACString& aFilePath);
+ virtual nsresult SetQuery(const nsACString& aQuery);
+ virtual nsresult SetQueryWithEncoding(const nsACString& aQuery,
+ const mozilla::Encoding* aEncoding);
+ virtual nsresult CreateURL(const nsACString& aSpec,
+ nsIURL** aURL); // nsMailboxUrl overrides this.
+
+ public:
+ class Mutator : public nsIURIMutator,
+ public BaseURIMutator<nsMsgMailNewsUrl> {
+ NS_DECL_ISUPPORTS
+ NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI)
+
+ NS_IMETHOD Deserialize(const mozilla::ipc::URIParams& aParams) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ NS_IMETHOD Finalize(nsIURI** aURI) override {
+ mURI.forget(aURI);
+ return NS_OK;
+ }
+
+ NS_IMETHOD SetSpec(const nsACString& aSpec,
+ nsIURIMutator** aMutator) override {
+ if (aMutator) NS_ADDREF(*aMutator = this);
+ return InitFromSpec(aSpec);
+ }
+
+ explicit Mutator() {}
+
+ private:
+ virtual ~Mutator() {}
+
+ friend class nsMsgMailNewsUrl;
+ };
+ friend BaseURIMutator<nsMsgMailNewsUrl>;
+
+ protected:
+ virtual ~nsMsgMailNewsUrl();
+
+ nsCOMPtr<nsIURL> m_baseURL;
+ nsCOMPtr<nsIURI> m_normalizedOrigin;
+ nsWeakPtr m_statusFeedbackWeak;
+ nsWeakPtr m_msgWindowWeak;
+ nsWeakPtr m_loadGroupWeak;
+ nsCOMPtr<nsIMimeHeaders> mMimeHeaders;
+ nsCOMPtr<nsIMsgSearchSession> m_searchSession;
+ nsCOMPtr<nsICacheEntry> m_memCacheEntry;
+ nsCString m_errorCode;
+ nsCString m_seeOtherURI;
+ nsString m_errorMessage;
+ nsString m_errorParameters;
+ int64_t mMaxProgress;
+ bool m_runningUrl;
+ bool m_updatingFolder;
+ bool m_msgIsInLocalCache;
+ bool m_suppressErrorMsgs;
+ bool m_hasNormalizedOrigin;
+
+ // the following field is really a bit of a hack to make
+ // open attachments work. The external applications code sometimes tries to
+ // figure out the right handler to use by looking at the file extension of the
+ // url we are trying to load. Unfortunately, the attachment file name really
+ // isn't part of the url string....so we'll store it here...and if the url we
+ // are running is an attachment url, we'll set it here. Then when the helper
+ // apps code asks us for it, we'll return the right value.
+ nsCString mAttachmentFileName;
+
+ nsTObserverArray<nsCOMPtr<nsIUrlListener> > mUrlListeners;
+
+ // Security info from the socket transport (if any), after a failed operation.
+ // Here so that urlListeners can access and handle bad certificates in
+ // their OnStopRunningUrl() callback.
+ nsCOMPtr<nsITransportSecurityInfo> mFailedSecInfo;
+};
+
+#endif /* nsMsgMailNewsUrl_h___ */
diff --git a/comm/mailnews/base/src/nsMsgMailSession.cpp b/comm/mailnews/base/src/nsMsgMailSession.cpp
new file mode 100644
index 0000000000..ac34d5d487
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgMailSession.cpp
@@ -0,0 +1,671 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h" // for pre-compiled headers
+#include "nsMsgMailSession.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIChromeRegistry.h"
+#include "nsIDirectoryService.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsPIDOMWindow.h"
+#include "nsIDocShell.h"
+#include "mozilla/dom/Document.h"
+#include "nsIObserverService.h"
+#include "nsIAppStartup.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIAppShellService.h"
+#include "nsAppShellCID.h"
+#include "nsIWindowMediator.h"
+#include "nsIWindowWatcher.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "prcmon.h"
+#include "nsThreadUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIProperties.h"
+#include "mozilla/Services.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/Components.h"
+#include "nsFocusManager.h"
+#include "nsIPromptService.h"
+#include "nsEmbedCID.h"
+
+NS_IMPL_ISUPPORTS(nsMsgMailSession, nsIMsgMailSession, nsIFolderListener)
+
+nsMsgMailSession::nsMsgMailSession() {}
+
+nsMsgMailSession::~nsMsgMailSession() { Shutdown(); }
+
+nsresult nsMsgMailSession::Init() {
+ // Ensures the shutdown service is initialised
+ nsresult rv;
+ nsCOMPtr<nsIMsgShutdownService> shutdownService =
+ do_GetService("@mozilla.org/messenger/msgshutdownservice;1", &rv);
+ return rv;
+}
+
+nsresult nsMsgMailSession::Shutdown() { return NS_OK; }
+
+NS_IMETHODIMP nsMsgMailSession::AddFolderListener(nsIFolderListener* aListener,
+ uint32_t aNotifyFlags) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ // we don't care about the notification flags for equivalence purposes
+ size_t index = mListeners.IndexOf(aListener);
+ NS_ASSERTION(index == size_t(-1), "tried to add duplicate listener");
+ if (index == size_t(-1)) {
+ folderListener newListener(aListener, aNotifyFlags);
+ mListeners.AppendElement(newListener);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::RemoveFolderListener(
+ nsIFolderListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ mListeners.RemoveElement(aListener);
+ return NS_OK;
+}
+
+#define NOTIFY_FOLDER_LISTENERS(propertyflag_, propertyfunc_, params_) \
+ PR_BEGIN_MACRO \
+ nsTObserverArray<folderListener>::ForwardIterator iter(mListeners); \
+ while (iter.HasMore()) { \
+ const folderListener& fL = iter.GetNext(); \
+ if (fL.mNotifyFlags & nsIFolderListener::propertyflag_) \
+ fL.mListener->propertyfunc_ params_; \
+ } \
+ PR_END_MACRO
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderPropertyChanged(nsIMsgFolder* aItem,
+ const nsACString& aProperty,
+ const nsACString& aOldValue,
+ const nsACString& aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(propertyChanged, OnFolderPropertyChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderUnicharPropertyChanged(nsIMsgFolder* aItem,
+ const nsACString& aProperty,
+ const nsAString& aOldValue,
+ const nsAString& aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(unicharPropertyChanged,
+ OnFolderUnicharPropertyChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderIntPropertyChanged(nsIMsgFolder* aItem,
+ const nsACString& aProperty,
+ int64_t aOldValue,
+ int64_t aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(intPropertyChanged, OnFolderIntPropertyChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderBoolPropertyChanged(nsIMsgFolder* aItem,
+ const nsACString& aProperty,
+ bool aOldValue, bool aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(boolPropertyChanged, OnFolderBoolPropertyChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::OnFolderPropertyFlagChanged(nsIMsgDBHdr* aItem,
+ const nsACString& aProperty,
+ uint32_t aOldValue,
+ uint32_t aNewValue) {
+ NOTIFY_FOLDER_LISTENERS(propertyFlagChanged, OnFolderPropertyFlagChanged,
+ (aItem, aProperty, aOldValue, aNewValue));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::OnFolderAdded(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ NOTIFY_FOLDER_LISTENERS(added, OnFolderAdded, (parent, child));
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgMailSession::OnMessageAdded(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ NOTIFY_FOLDER_LISTENERS(added, OnMessageAdded, (parent, msg));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::OnFolderRemoved(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ NOTIFY_FOLDER_LISTENERS(removed, OnFolderRemoved, (parent, child));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::OnMessageRemoved(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ NOTIFY_FOLDER_LISTENERS(removed, OnMessageRemoved, (parent, msg));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::OnFolderEvent(nsIMsgFolder* aFolder,
+ const nsACString& aEvent) {
+ NOTIFY_FOLDER_LISTENERS(event, OnFolderEvent, (aFolder, aEvent));
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::AddUserFeedbackListener(
+ nsIMsgUserFeedbackListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ size_t index = mFeedbackListeners.IndexOf(aListener);
+ NS_ASSERTION(index == size_t(-1), "tried to add duplicate listener");
+ if (index == size_t(-1)) mFeedbackListeners.AppendElement(aListener);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::RemoveUserFeedbackListener(
+ nsIMsgUserFeedbackListener* aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+
+ mFeedbackListeners.RemoveElement(aListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::AlertUser(const nsAString& aMessage,
+ nsIMsgMailNewsUrl* aUrl) {
+ bool listenersNotified = false;
+ nsTObserverArray<nsCOMPtr<nsIMsgUserFeedbackListener>>::ForwardIterator iter(
+ mFeedbackListeners);
+ nsCOMPtr<nsIMsgUserFeedbackListener> listener;
+
+ while (iter.HasMore()) {
+ bool notified = false;
+ listener = iter.GetNext();
+ listener->OnAlert(aMessage, aUrl, &notified);
+ listenersNotified = listenersNotified || notified;
+ }
+
+ // If the listeners notified the user, then we don't need to. Also exit if
+ // aUrl is null because we won't have a nsIMsgWindow in that case.
+ if (listenersNotified || !aUrl) return NS_OK;
+
+ // If the url hasn't got a message window, then the error was a generated as a
+ // result of background activity (e.g. autosync, biff, etc), and hence we
+ // shouldn't prompt either.
+ nsCOMPtr<nsIMsgWindow> msgWindow;
+ aUrl->GetMsgWindow(getter_AddRefs(msgWindow));
+
+ if (!msgWindow) return NS_OK;
+
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ msgWindow->GetDomWindow(getter_AddRefs(domWindow));
+
+ nsresult rv;
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ dlgService->Alert(domWindow, nullptr, PromiseFlatString(aMessage).get());
+
+ return NS_OK;
+}
+
+nsresult nsMsgMailSession::GetTopmostMsgWindow(nsIMsgWindow** aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ *aMsgWindow = nullptr;
+
+ uint32_t count = mWindows.Count();
+
+ if (count == 1) {
+ NS_ADDREF(*aMsgWindow = mWindows[0]);
+ return (*aMsgWindow) ? NS_OK : NS_ERROR_FAILURE;
+ } else if (count > 1) {
+ // If multiple message windows then we have lots more work.
+ nsresult rv;
+
+ // The msgWindows array does not hold z-order info. Use mediator to get
+ // the top most window then match that with the msgWindows array.
+ nsCOMPtr<nsIWindowMediator> windowMediator =
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISimpleEnumerator> windowEnum;
+
+ rv = windowMediator->GetEnumerator(nullptr, getter_AddRefs(windowEnum));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupports> windowSupports;
+ nsCOMPtr<nsPIDOMWindowOuter> topMostWindow;
+ nsAutoString windowType;
+ bool more;
+
+ // loop to get the top most with attribute "mail:3pane" or
+ // "mail:messageWindow"
+ windowEnum->HasMoreElements(&more);
+ while (more) {
+ rv = windowEnum->GetNext(getter_AddRefs(windowSupports));
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(windowSupports, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsPIDOMWindowOuter> window =
+ do_QueryInterface(windowSupports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
+
+ mozilla::dom::Document* domDocument = window->GetDoc();
+ NS_ENSURE_TRUE(domDocument, NS_ERROR_FAILURE);
+
+ mozilla::dom::Element* domElement = domDocument->GetDocumentElement();
+ NS_ENSURE_TRUE(domElement, NS_ERROR_FAILURE);
+
+ domElement->GetAttribute(u"windowtype"_ns, windowType);
+ if (windowType.EqualsLiteral("mail:3pane") ||
+ windowType.EqualsLiteral("mail:messageWindow")) {
+ // topMostWindow is the last 3pane/messageWindow found, not necessarily
+ // the top most.
+ topMostWindow = window;
+ RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager();
+ nsCOMPtr<mozIDOMWindowProxy> currentWindow =
+ do_QueryInterface(windowSupports, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<mozIDOMWindowProxy> activeWindow;
+ rv = fm->GetActiveWindow(getter_AddRefs(activeWindow));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (currentWindow == activeWindow) {
+ // We are sure topMostWindow is really the top most now.
+ break;
+ }
+ }
+
+ windowEnum->HasMoreElements(&more);
+ }
+
+ // identified the top most window
+ if (topMostWindow) {
+ // use this for the match
+ nsIDocShell* topDocShell = topMostWindow->GetDocShell();
+
+ // loop for the msgWindow array to find the match
+ nsCOMPtr<nsIDocShell> docShell;
+
+ while (count) {
+ nsIMsgWindow* msgWindow = mWindows[--count];
+
+ rv = msgWindow->GetRootDocShell(getter_AddRefs(docShell));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (topDocShell == docShell) {
+ NS_IF_ADDREF(*aMsgWindow = msgWindow);
+ break;
+ }
+ }
+ }
+ }
+
+ return (*aMsgWindow) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgMailSession::AddMsgWindow(nsIMsgWindow* msgWindow) {
+ NS_ENSURE_ARG_POINTER(msgWindow);
+
+ mWindows.AppendObject(msgWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::RemoveMsgWindow(nsIMsgWindow* msgWindow) {
+ mWindows.RemoveObject(msgWindow);
+ // Mac keeps a hidden window open so the app doesn't shut down when
+ // the last window is closed. So don't shutdown the account manager in that
+ // case. Similarly, for suite, we don't want to disable mailnews when the
+ // last mail window is closed.
+#if !defined(XP_MACOSX) && !defined(MOZ_SUITE)
+ if (!mWindows.Count()) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ if (NS_FAILED(rv)) return rv;
+ accountManager->CleanupOnExit();
+ }
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgMailSession::IsFolderOpenInWindow(nsIMsgFolder* folder,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ *aResult = false;
+
+ uint32_t count = mWindows.Count();
+
+ for (uint32_t i = 0; i < count; i++) {
+ nsCOMPtr<nsIMsgFolder> openFolder;
+ mWindows[i]->GetOpenFolder(getter_AddRefs(openFolder));
+ if (folder == openFolder.get()) {
+ *aResult = true;
+ break;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgMailSession::ConvertMsgURIToMsgURL(const nsACString& aURI,
+ nsIMsgWindow* aMsgWindow,
+ nsACString& aURL) {
+ // convert the rdf msg uri into a url that represents the message...
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ nsresult rv = GetMessageServiceFromURI(aURI, getter_AddRefs(msgService));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NULL_POINTER);
+
+ nsCOMPtr<nsIURI> tURI;
+ rv = msgService->GetUrlForUri(aURI, aMsgWindow, getter_AddRefs(tURI));
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_NULL_POINTER);
+
+ rv = tURI->GetSpec(aURL);
+ return rv;
+}
+
+//-------------------------------------------------------------------------
+// GetSelectedLocaleDataDir - If a locale is selected, appends the selected
+// locale to the defaults data dir and returns
+// that new defaults data dir
+//-------------------------------------------------------------------------
+nsresult nsMsgMailSession::GetSelectedLocaleDataDir(nsIFile* defaultsDir) {
+ NS_ENSURE_ARG_POINTER(defaultsDir);
+
+ return NS_OK;
+}
+
+//-----------------------------------------------------------------------------
+// GetDataFilesDir - Gets the application's default folder and then appends the
+// subdirectory named passed in as param dirName. If there is
+// a selected locale, will append that to the dir path before
+// returning the value
+//-----------------------------------------------------------------------------
+NS_IMETHODIMP
+nsMsgMailSession::GetDataFilesDir(const char* dirName, nsIFile** dataFilesDir) {
+ NS_ENSURE_ARG_POINTER(dirName);
+ NS_ENSURE_ARG_POINTER(dataFilesDir);
+
+ nsresult rv;
+ nsCOMPtr<nsIProperties> directoryService =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> defaultsDir;
+ rv = directoryService->Get(NS_APP_DEFAULTS_50_DIR, NS_GET_IID(nsIFile),
+ getter_AddRefs(defaultsDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = defaultsDir->AppendNative(nsDependentCString(dirName));
+ if (NS_SUCCEEDED(rv)) rv = GetSelectedLocaleDataDir(defaultsDir);
+
+ defaultsDir.forget(dataFilesDir);
+
+ return rv;
+}
+
+/********************************************************************************/
+
+NS_IMPL_ISUPPORTS(nsMsgShutdownService, nsIMsgShutdownService, nsIUrlListener,
+ nsIObserver)
+
+nsMsgShutdownService::nsMsgShutdownService()
+ : mTaskIndex(0),
+ mQuitMode(nsIAppStartup::eAttemptQuit),
+ mProcessedShutdown(false),
+ mQuitForced(false),
+ mReadyToQuit(false) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "quit-application-requested", false);
+ observerService->AddObserver(this, "quit-application-granted", false);
+ observerService->AddObserver(this, "quit-application", false);
+ }
+}
+
+nsMsgShutdownService::~nsMsgShutdownService() {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, "quit-application-requested");
+ observerService->RemoveObserver(this, "quit-application-granted");
+ observerService->RemoveObserver(this, "quit-application");
+ }
+}
+
+nsresult nsMsgShutdownService::ProcessNextTask() {
+ bool shutdownTasksDone = true;
+
+ uint32_t count = mShutdownTasks.Length();
+ if (mTaskIndex < count) {
+ shutdownTasksDone = false;
+
+ nsCOMPtr<nsIMsgShutdownTask> curTask = mShutdownTasks[mTaskIndex];
+ nsString taskName;
+ curTask->GetCurrentTaskName(taskName);
+ SetStatusText(taskName);
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ NS_ENSURE_TRUE(mailSession, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIMsgWindow> topMsgWindow;
+ mailSession->GetTopmostMsgWindow(getter_AddRefs(topMsgWindow));
+
+ bool taskIsRunning = true;
+ nsresult rv = curTask->DoShutdownTask(this, topMsgWindow, &taskIsRunning);
+ if (NS_FAILED(rv) || !taskIsRunning) {
+ // We have failed, let's go on to the next task.
+ mTaskIndex++;
+ mMsgProgress->OnProgressChange(nullptr, nullptr, 0, 0,
+ (int32_t)mTaskIndex, count);
+ ProcessNextTask();
+ }
+ }
+
+ if (shutdownTasksDone) {
+ if (mMsgProgress)
+ mMsgProgress->OnStateChange(nullptr, nullptr,
+ nsIWebProgressListener::STATE_STOP, NS_OK);
+ AttemptShutdown();
+ }
+
+ return NS_OK;
+}
+
+void nsMsgShutdownService::AttemptShutdown() {
+ if (mQuitForced) {
+ PR_CEnterMonitor(this);
+ mReadyToQuit = true;
+ PR_CNotifyAll(this);
+ PR_CExitMonitor(this);
+ } else {
+ nsCOMPtr<nsIAppStartup> appStartup =
+ mozilla::components::AppStartup::Service();
+ NS_ENSURE_TRUE_VOID(appStartup);
+ bool userAllowedQuit = true;
+ NS_ENSURE_SUCCESS_VOID(appStartup->Quit(mQuitMode, 0, &userAllowedQuit));
+ }
+}
+
+NS_IMETHODIMP nsMsgShutdownService::SetShutdownListener(
+ nsIWebProgressListener* inListener) {
+ NS_ENSURE_TRUE(mMsgProgress, NS_ERROR_FAILURE);
+ mMsgProgress->RegisterListener(inListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ // Due to bug 459376 we don't always get quit-application-requested and
+ // quit-application-granted. quit-application-requested is preferred, but if
+ // we don't then we have to hook onto quit-application, but we don't want
+ // to do the checking twice so we set some flags to prevent that.
+ if (!strcmp(aTopic, "quit-application-granted")) {
+ // Quit application has been requested and granted, therefore we will shut
+ // down.
+ mProcessedShutdown = true;
+ return NS_OK;
+ }
+
+ // If we've already processed a shutdown notification, no need to do it again.
+ if (!strcmp(aTopic, "quit-application")) {
+ if (mProcessedShutdown)
+ return NS_OK;
+ else
+ mQuitForced = true;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ NS_ENSURE_STATE(observerService);
+
+ nsCOMPtr<nsISimpleEnumerator> listenerEnum;
+ nsresult rv = observerService->EnumerateObservers(
+ "msg-shutdown", getter_AddRefs(listenerEnum));
+ if (NS_SUCCEEDED(rv) && listenerEnum) {
+ bool hasMore;
+ listenerEnum->HasMoreElements(&hasMore);
+ if (!hasMore) return NS_OK;
+
+ while (hasMore) {
+ nsCOMPtr<nsISupports> curObject;
+ listenerEnum->GetNext(getter_AddRefs(curObject));
+
+ nsCOMPtr<nsIMsgShutdownTask> curTask = do_QueryInterface(curObject);
+ if (curTask) {
+ bool shouldRunTask;
+ curTask->GetNeedsToRunTask(&shouldRunTask);
+ if (shouldRunTask) mShutdownTasks.AppendObject(curTask);
+ }
+
+ listenerEnum->HasMoreElements(&hasMore);
+ }
+
+ if (mShutdownTasks.Count() < 1) return NS_ERROR_FAILURE;
+
+ mTaskIndex = 0;
+
+ mMsgProgress = do_CreateInstance("@mozilla.org/messenger/progress;1");
+ NS_ENSURE_TRUE(mMsgProgress, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ NS_ENSURE_TRUE(mailSession, NS_ERROR_FAILURE);
+
+ nsCOMPtr<nsIMsgWindow> topMsgWindow;
+ mailSession->GetTopmostMsgWindow(getter_AddRefs(topMsgWindow));
+
+ nsCOMPtr<mozIDOMWindowProxy> internalDomWin;
+ if (topMsgWindow)
+ topMsgWindow->GetDomWindow(getter_AddRefs(internalDomWin));
+
+ if (!internalDomWin) {
+ // First see if there is a window open.
+ nsCOMPtr<nsIWindowMediator> winMed =
+ do_GetService(NS_WINDOWMEDIATOR_CONTRACTID);
+ winMed->GetMostRecentWindow(nullptr, getter_AddRefs(internalDomWin));
+
+ // If not use the hidden window.
+ if (!internalDomWin) {
+ nsCOMPtr<nsIAppShellService> appShell(
+ do_GetService(NS_APPSHELLSERVICE_CONTRACTID));
+ appShell->GetHiddenDOMWindow(getter_AddRefs(internalDomWin));
+ NS_ENSURE_TRUE(internalDomWin,
+ NS_ERROR_FAILURE); // bail if we don't get a window.
+ }
+ }
+
+ if (!mQuitForced) {
+ nsCOMPtr<nsISupportsPRBool> stopShutdown = do_QueryInterface(aSubject);
+ stopShutdown->SetData(true);
+
+ // If the attempted quit was a restart, be sure to restart the app once
+ // the tasks have been run. This is usually the case when addons or
+ // updates are going to be installed.
+ if (aData && nsDependentString(aData).EqualsLiteral("restart"))
+ mQuitMode |= nsIAppStartup::eRestart;
+ }
+
+ mMsgProgress->OpenProgressDialog(
+ internalDomWin, topMsgWindow,
+ "chrome://messenger/content/shutdownWindow.xhtml", false, nullptr);
+
+ if (mQuitForced) {
+ nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
+
+ mReadyToQuit = false;
+ while (!mReadyToQuit) {
+ PR_CEnterMonitor(this);
+ // Waiting for 50 milliseconds
+ PR_CWait(this, PR_MicrosecondsToInterval(50000UL));
+ PR_CExitMonitor(this);
+ NS_ProcessPendingEvents(thread);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+// nsIUrlListener
+NS_IMETHODIMP nsMsgShutdownService::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::OnStopRunningUrl(nsIURI* url,
+ nsresult aExitCode) {
+ mTaskIndex++;
+
+ if (mMsgProgress) {
+ int32_t numTasks = mShutdownTasks.Count();
+ mMsgProgress->OnProgressChange(nullptr, nullptr, 0, 0, (int32_t)mTaskIndex,
+ numTasks);
+ }
+
+ ProcessNextTask();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::GetNumTasks(int32_t* inNumTasks) {
+ *inNumTasks = mShutdownTasks.Count();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::StartShutdownTasks() {
+ ProcessNextTask();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::CancelShutdownTasks() {
+ AttemptShutdown();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgShutdownService::SetStatusText(
+ const nsAString& inStatusString) {
+ nsString statusString(inStatusString);
+ if (mMsgProgress)
+ mMsgProgress->OnStatusChange(nullptr, nullptr, NS_OK,
+ nsString(statusString).get());
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgMailSession.h b/comm/mailnews/base/src/nsMsgMailSession.h
new file mode 100644
index 0000000000..09cea632e3
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgMailSession.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMsgMailSession_h___
+#define nsMsgMailSession_h___
+
+#include "nsIMsgMailSession.h"
+#include "nsISupports.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsIMsgWindow.h"
+#include "nsCOMArray.h"
+#include "nsIMsgShutdown.h"
+#include "nsIObserver.h"
+#include "nsIMsgProgress.h"
+#include "nsTObserverArray.h"
+#include "nsIMsgUserFeedbackListener.h"
+#include "nsIUrlListener.h"
+
+///////////////////////////////////////////////////////////////////////////////////
+// The mail session is a replacement for the old 4.x MSG_Master object. It
+// contains mail session generic information such as the user's current mail
+// identity, .... I'm starting this off as an empty interface and as people feel
+// they need to add more information to it, they can. I think this is a better
+// approach than trying to port over the old MSG_Master in its entirety as that
+// had a lot of cruft in it....
+//////////////////////////////////////////////////////////////////////////////////
+
+// nsMsgMailSession also implements nsIFolderListener, in order to relay
+// notifications to its registered listeners.
+// Calling a method on the MailSession causes that method to be invoked upon
+// all the registered listeners (but not listeners directly attached to
+// folders!)
+// In normal operation, most notifications will originate from the
+// nsIMsgFolder.Notify*() functions, which invoke both folder-local
+// listeners, and the global MailSession-registered ones).
+class nsMsgMailSession : public nsIMsgMailSession, public nsIFolderListener {
+ public:
+ nsMsgMailSession();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGMAILSESSION
+ NS_DECL_NSIFOLDERLISTENER
+
+ nsresult Init();
+ nsresult GetSelectedLocaleDataDir(nsIFile* defaultsDir);
+
+ protected:
+ virtual ~nsMsgMailSession();
+
+ struct folderListener {
+ nsCOMPtr<nsIFolderListener> mListener;
+ uint32_t mNotifyFlags;
+
+ folderListener(nsIFolderListener* aListener, uint32_t aNotifyFlags)
+ : mListener(aListener), mNotifyFlags(aNotifyFlags) {}
+ folderListener(const folderListener& aListener)
+ : mListener(aListener.mListener),
+ mNotifyFlags(aListener.mNotifyFlags) {}
+ ~folderListener() {}
+
+ int operator==(nsIFolderListener* aListener) const {
+ return mListener == aListener;
+ }
+ int operator==(const folderListener& aListener) const {
+ return mListener == aListener.mListener &&
+ mNotifyFlags == aListener.mNotifyFlags;
+ }
+ };
+
+ nsTObserverArray<folderListener> mListeners;
+ nsTObserverArray<nsCOMPtr<nsIMsgUserFeedbackListener> > mFeedbackListeners;
+
+ nsCOMArray<nsIMsgWindow> mWindows;
+ // stick this here temporarily
+ nsCOMPtr<nsIMsgWindow> m_temporaryMsgWindow;
+};
+
+/********************************************************************************/
+
+class nsMsgShutdownService : public nsIMsgShutdownService,
+ public nsIUrlListener,
+ public nsIObserver {
+ public:
+ nsMsgShutdownService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGSHUTDOWNSERVICE
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIOBSERVER
+
+ protected:
+ nsresult ProcessNextTask();
+ void AttemptShutdown();
+
+ private:
+ virtual ~nsMsgShutdownService();
+
+ nsCOMArray<nsIMsgShutdownTask> mShutdownTasks;
+ nsCOMPtr<nsIMsgProgress> mMsgProgress;
+ uint32_t mTaskIndex;
+ uint32_t mQuitMode;
+ bool mProcessedShutdown;
+ bool mQuitForced;
+ bool mReadyToQuit;
+};
+
+#endif /* nsMsgMailSession_h__ */
diff --git a/comm/mailnews/base/src/nsMsgOfflineManager.cpp b/comm/mailnews/base/src/nsMsgOfflineManager.cpp
new file mode 100644
index 0000000000..7d6935fc89
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgOfflineManager.cpp
@@ -0,0 +1,352 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * The offline manager service - manages going online and offline, and
+ * synchronization
+ */
+#include "msgCore.h"
+#include "netCore.h"
+#include "nsMsgOfflineManager.h"
+#include "nsIServiceManager.h"
+#include "nsIImapService.h"
+#include "nsIMsgSendLater.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIIOService.h"
+#include "nsNetCID.h"
+#include "nsINntpService.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Components.h"
+
+#define NS_MSGSENDLATER_CID \
+ { /* E15C83F1-1CF4-11d3-8EF0-00A024A7D144 */ \
+ 0xe15c83f1, 0x1cf4, 0x11d3, { \
+ 0x8e, 0xf0, 0x0, 0xa0, 0x24, 0xa7, 0xd1, 0x44 \
+ } \
+ }
+static NS_DEFINE_CID(kMsgSendLaterCID, NS_MSGSENDLATER_CID);
+
+NS_IMPL_ISUPPORTS(nsMsgOfflineManager, nsIMsgOfflineManager,
+ nsIMsgSendLaterListener, nsIObserver,
+ nsISupportsWeakReference, nsIUrlListener)
+
+nsMsgOfflineManager::nsMsgOfflineManager()
+ : m_inProgress(false),
+ m_sendUnsentMessages(false),
+ m_downloadNews(false),
+ m_downloadMail(false),
+ m_playbackOfflineImapOps(false),
+ m_goOfflineWhenDone(false),
+ m_curState(eNoState),
+ m_curOperation(eNoOp) {}
+
+nsMsgOfflineManager::~nsMsgOfflineManager() {}
+
+/* attribute nsIMsgWindow window; */
+NS_IMETHODIMP nsMsgOfflineManager::GetWindow(nsIMsgWindow** aWindow) {
+ NS_ENSURE_ARG(aWindow);
+ NS_IF_ADDREF(*aWindow = m_window);
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgOfflineManager::SetWindow(nsIMsgWindow* aWindow) {
+ m_window = aWindow;
+ if (m_window)
+ m_window->GetStatusFeedback(getter_AddRefs(m_statusFeedback));
+ else
+ m_statusFeedback = nullptr;
+ return NS_OK;
+}
+
+/* attribute boolean inProgress; */
+NS_IMETHODIMP nsMsgOfflineManager::GetInProgress(bool* aInProgress) {
+ NS_ENSURE_ARG(aInProgress);
+ *aInProgress = m_inProgress;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineManager::SetInProgress(bool aInProgress) {
+ m_inProgress = aInProgress;
+ return NS_OK;
+}
+
+nsresult nsMsgOfflineManager::StopRunning(nsresult exitStatus) {
+ m_inProgress = false;
+ return exitStatus;
+}
+
+nsresult nsMsgOfflineManager::AdvanceToNextState(nsresult exitStatus) {
+ // NS_BINDING_ABORTED is used for the user pressing stop, which
+ // should cause us to abort the offline process. Other errors
+ // should allow us to continue.
+ if (exitStatus == NS_BINDING_ABORTED) {
+ return StopRunning(exitStatus);
+ }
+ if (m_curOperation == eGoingOnline) {
+ switch (m_curState) {
+ case eNoState:
+
+ m_curState = eSendingUnsent;
+ if (m_sendUnsentMessages) {
+ SendUnsentMessages();
+ } else
+ AdvanceToNextState(NS_OK);
+ break;
+ case eSendingUnsent:
+
+ m_curState = eSynchronizingOfflineImapChanges;
+ if (m_playbackOfflineImapOps)
+ return SynchronizeOfflineImapChanges();
+ else
+ AdvanceToNextState(NS_OK); // recurse to next state.
+ break;
+ case eSynchronizingOfflineImapChanges:
+ m_curState = eDone;
+ return StopRunning(exitStatus);
+ default:
+ NS_ASSERTION(false, "unhandled current state when going online");
+ }
+ } else if (m_curOperation == eDownloadingForOffline) {
+ switch (m_curState) {
+ case eNoState:
+ m_curState = eDownloadingNews;
+ if (m_downloadNews)
+ DownloadOfflineNewsgroups();
+ else
+ AdvanceToNextState(NS_OK);
+ break;
+ case eSendingUnsent:
+ if (m_goOfflineWhenDone) {
+ SetOnlineState(false);
+ }
+ break;
+ case eDownloadingNews:
+ m_curState = eDownloadingMail;
+ if (m_downloadMail)
+ DownloadMail();
+ else
+ AdvanceToNextState(NS_OK);
+ break;
+ case eDownloadingMail:
+ m_curState = eSendingUnsent;
+ if (m_sendUnsentMessages)
+ SendUnsentMessages();
+ else
+ AdvanceToNextState(NS_OK);
+ break;
+ default:
+ NS_ASSERTION(false,
+ "unhandled current state when downloading for offline");
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgOfflineManager::SynchronizeOfflineImapChanges() {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->PlaybackAllOfflineOperations(
+ m_window, this, getter_AddRefs(mOfflineImapSync));
+}
+
+nsresult nsMsgOfflineManager::SendUnsentMessages() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgSendLater> pMsgSendLater(do_GetService(kMsgSendLaterCID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // now we have to iterate over the identities, finding the *unique* unsent
+ // messages folder for each one, determine if they have unsent messages, and
+ // if so, add them to the list of identities to send unsent messages from.
+ // However, I think there's only ever one unsent messages folder at the
+ // moment, so I think we'll go with that for now.
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+
+ if (NS_SUCCEEDED(rv) && accountManager) {
+ rv = accountManager->GetAllIdentities(identities);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ nsCOMPtr<nsIMsgIdentity> identityToUse;
+ for (auto thisIdentity : identities) {
+ if (thisIdentity) {
+ nsCOMPtr<nsIMsgFolder> outboxFolder;
+ pMsgSendLater->GetUnsentMessagesFolder(thisIdentity,
+ getter_AddRefs(outboxFolder));
+ if (outboxFolder) {
+ int32_t numMessages;
+ outboxFolder->GetTotalMessages(false, &numMessages);
+ if (numMessages > 0) {
+ identityToUse = thisIdentity;
+ break;
+ }
+ }
+ }
+ }
+ if (identityToUse) {
+#ifdef MOZ_SUITE
+ if (m_statusFeedback) pMsgSendLater->SetStatusFeedback(m_statusFeedback);
+#endif
+
+ pMsgSendLater->AddListener(this);
+ rv = pMsgSendLater->SendUnsentMessages(identityToUse);
+ ShowStatus("sendingUnsent");
+ // if we succeeded, return - we'll run the next operation when the
+ // send finishes. Otherwise, advance to the next state.
+ if (NS_SUCCEEDED(rv)) return rv;
+ }
+ return AdvanceToNextState(rv);
+}
+
+#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties"
+
+nsresult nsMsgOfflineManager::ShowStatus(const char* statusMsgName) {
+ if (!mStringBundle) {
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED);
+ sBundleService->CreateBundle(MESSENGER_STRING_URL,
+ getter_AddRefs(mStringBundle));
+ return NS_OK;
+ }
+
+ nsString statusString;
+ nsresult res = mStringBundle->GetStringFromName(statusMsgName, statusString);
+
+ if (NS_SUCCEEDED(res) && m_statusFeedback)
+ m_statusFeedback->ShowStatusString(statusString);
+
+ return res;
+}
+
+nsresult nsMsgOfflineManager::DownloadOfflineNewsgroups() {
+ nsresult rv;
+ ShowStatus("downloadingNewsgroups");
+ nsCOMPtr<nsINntpService> nntpService(
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv));
+ if (NS_SUCCEEDED(rv) && nntpService)
+ rv = nntpService->DownloadNewsgroupsForOffline(m_window, this);
+
+ if (NS_FAILED(rv)) return AdvanceToNextState(rv);
+ return rv;
+}
+
+nsresult nsMsgOfflineManager::DownloadMail() {
+ nsresult rv = NS_OK;
+ ShowStatus("downloadingMail");
+ nsCOMPtr<nsIImapService> imapService =
+ do_GetService("@mozilla.org/messenger/imapservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return imapService->DownloadAllOffineImapFolders(m_window, this);
+ // ### we should do get new mail on pop servers, and download imap messages
+ // for offline use.
+}
+
+NS_IMETHODIMP nsMsgOfflineManager::GoOnline(bool sendUnsentMessages,
+ bool playbackOfflineImapOperations,
+ nsIMsgWindow* aMsgWindow) {
+ m_sendUnsentMessages = sendUnsentMessages;
+ m_playbackOfflineImapOps = playbackOfflineImapOperations;
+ m_curOperation = eGoingOnline;
+ m_curState = eNoState;
+ SetWindow(aMsgWindow);
+ SetOnlineState(true);
+ if (!m_sendUnsentMessages && !playbackOfflineImapOperations)
+ return NS_OK;
+ else
+ AdvanceToNextState(NS_OK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineManager::SynchronizeForOffline(
+ bool downloadNews, bool downloadMail, bool sendUnsentMessages,
+ bool goOfflineWhenDone, nsIMsgWindow* aMsgWindow) {
+ m_curOperation = eDownloadingForOffline;
+ m_downloadNews = downloadNews;
+ m_downloadMail = downloadMail;
+ m_sendUnsentMessages = sendUnsentMessages;
+ SetWindow(aMsgWindow);
+ m_goOfflineWhenDone = goOfflineWhenDone;
+ m_curState = eNoState;
+ if (!downloadNews && !downloadMail && !sendUnsentMessages) {
+ if (goOfflineWhenDone) return SetOnlineState(false);
+ } else
+ return AdvanceToNextState(NS_OK);
+ return NS_OK;
+}
+
+nsresult nsMsgOfflineManager::SetOnlineState(bool online) {
+ nsCOMPtr<nsIIOService> netService = mozilla::components::IO::Service();
+ NS_ENSURE_TRUE(netService, NS_ERROR_UNEXPECTED);
+ return netService->SetOffline(!online);
+}
+
+// nsIUrlListener methods
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnStartRunningUrl(nsIURI* aUrl) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) {
+ mOfflineImapSync = nullptr;
+
+ AdvanceToNextState(aExitCode);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgOfflineManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* someData) {
+ return NS_OK;
+}
+
+// nsIMsgSendLaterListener implementation
+NS_IMETHODIMP
+nsMsgOfflineManager::OnStartSending(uint32_t aTotalMessageCount) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnMessageStartSending(uint32_t aCurrentMessage,
+ uint32_t aTotalMessageCount,
+ nsIMsgDBHdr* aMessageHeader,
+ nsIMsgIdentity* aIdentity) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnMessageSendProgress(uint32_t aCurrentMessage,
+ uint32_t aTotalMessageCount,
+ uint32_t aMessageSendPercent,
+ uint32_t aMessageCopyPercent) {
+ if (m_statusFeedback && aTotalMessageCount)
+ return m_statusFeedback->ShowProgress((100 * aCurrentMessage) /
+ aTotalMessageCount);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnMessageSendError(uint32_t aCurrentMessage,
+ nsIMsgDBHdr* aMessageHeader,
+ nsresult aStatus,
+ const char16_t* aMsg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgOfflineManager::OnStopSending(nsresult aStatus, const char16_t* aMsg,
+ uint32_t aTotalTried, uint32_t aSuccessful) {
+#ifdef NS_DEBUG
+ if (NS_SUCCEEDED(aStatus))
+ printf(
+ "SendLaterListener::OnStopSending: Tried to send %d messages. %d "
+ "successful.\n",
+ aTotalTried, aSuccessful);
+#endif
+ return AdvanceToNextState(aStatus);
+}
diff --git a/comm/mailnews/base/src/nsMsgOfflineManager.h b/comm/mailnews/base/src/nsMsgOfflineManager.h
new file mode 100644
index 0000000000..f340ee0fd1
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgOfflineManager.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMsgOfflineManager_h__
+#define nsMsgOfflineManager_h__
+
+#include "nscore.h"
+#include "nsIMsgOfflineManager.h"
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+#include "nsWeakReference.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgSendLaterListener.h"
+#include "nsIStringBundle.h"
+
+class nsMsgOfflineManager : public nsIMsgOfflineManager,
+ public nsIObserver,
+ public nsSupportsWeakReference,
+ public nsIMsgSendLaterListener,
+ public nsIUrlListener {
+ public:
+ nsMsgOfflineManager();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /* nsIMsgOfflineManager methods */
+
+ NS_DECL_NSIMSGOFFLINEMANAGER
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGSENDLATERLISTENER
+
+ typedef enum {
+ eStarting = 0,
+ eSynchronizingOfflineImapChanges = 1,
+ eDownloadingNews = 2,
+ eDownloadingMail = 3,
+ eSendingUnsent = 4,
+ eDone = 5,
+ eNoState = 6 // we're not doing anything
+ } offlineManagerState;
+
+ typedef enum {
+ eGoingOnline = 0,
+ eDownloadingForOffline = 1,
+ eNoOp = 2 // no operation in progress
+ } offlineManagerOperation;
+
+ private:
+ virtual ~nsMsgOfflineManager();
+
+ nsresult AdvanceToNextState(nsresult exitStatus);
+ nsresult SynchronizeOfflineImapChanges();
+ nsresult StopRunning(nsresult exitStatus);
+ nsresult SendUnsentMessages();
+ nsresult DownloadOfflineNewsgroups();
+ nsresult DownloadMail();
+
+ nsresult SetOnlineState(bool online);
+ nsresult ShowStatus(const char* statusMsgName);
+
+ bool m_inProgress;
+ bool m_sendUnsentMessages;
+ bool m_downloadNews;
+ bool m_downloadMail;
+ bool m_playbackOfflineImapOps;
+ bool m_goOfflineWhenDone;
+ offlineManagerState m_curState;
+ offlineManagerOperation m_curOperation;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsCOMPtr<nsIMsgStatusFeedback> m_statusFeedback;
+ nsCOMPtr<nsIStringBundle> mStringBundle;
+ nsCOMPtr<nsISupports> mOfflineImapSync;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgProgress.cpp b/comm/mailnews/base/src/nsMsgProgress.cpp
new file mode 100644
index 0000000000..a6349b5add
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgProgress.cpp
@@ -0,0 +1,250 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgProgress.h"
+
+#include "nsIBaseWindow.h"
+#include "nsXPCOM.h"
+#include "nsIMutableArray.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIComponentManager.h"
+#include "nsError.h"
+#include "nsIWindowWatcher.h"
+#include "nsPIDOMWindow.h"
+#include "mozIDOMWindow.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+#include "mozilla/dom/BrowsingContext.h"
+
+NS_IMPL_ISUPPORTS(nsMsgProgress, nsIMsgStatusFeedback, nsIMsgProgress,
+ nsIWebProgressListener, nsIProgressEventSink,
+ nsISupportsWeakReference)
+
+nsMsgProgress::nsMsgProgress() {
+ m_closeProgress = false;
+ m_processCanceled = false;
+ m_pendingStateFlags = -1;
+ m_pendingStateValue = NS_OK;
+}
+
+nsMsgProgress::~nsMsgProgress() { (void)ReleaseListeners(); }
+
+NS_IMETHODIMP nsMsgProgress::OpenProgressDialog(
+ mozIDOMWindowProxy* parentDOMWindow, nsIMsgWindow* aMsgWindow,
+ const char* dialogURL, bool inDisplayModal, nsISupports* parameters) {
+ nsresult rv;
+
+ if (aMsgWindow) {
+ SetMsgWindow(aMsgWindow);
+ aMsgWindow->SetStatusFeedback(this);
+ }
+
+ NS_ENSURE_ARG_POINTER(dialogURL);
+ NS_ENSURE_ARG_POINTER(parentDOMWindow);
+ nsCOMPtr<nsPIDOMWindowOuter> parent =
+ nsPIDOMWindowOuter::From(parentDOMWindow);
+
+ // Set up window.arguments[0]...
+ nsCOMPtr<nsIMutableArray> array(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsISupportsInterfacePointer> ifptr =
+ do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ ifptr->SetData(static_cast<nsIMsgProgress*>(this));
+ ifptr->SetDataIID(&NS_GET_IID(nsIMsgProgress));
+
+ array->AppendElement(ifptr);
+ array->AppendElement(parameters);
+
+ // Open the dialog.
+ RefPtr<mozilla::dom::BrowsingContext> newWindow;
+
+ nsString chromeOptions(u"chrome,dependent,centerscreen"_ns);
+ if (inDisplayModal) chromeOptions.AppendLiteral(",modal");
+
+ return parent->OpenDialog(NS_ConvertASCIItoUTF16(dialogURL), u"_blank"_ns,
+ chromeOptions, array, getter_AddRefs(newWindow));
+}
+
+NS_IMETHODIMP nsMsgProgress::CloseProgressDialog(bool forceClose) {
+ m_closeProgress = true;
+ return OnStateChange(nullptr, nullptr, nsIWebProgressListener::STATE_STOP,
+ forceClose ? NS_ERROR_FAILURE : NS_OK);
+}
+
+NS_IMETHODIMP nsMsgProgress::GetProcessCanceledByUser(
+ bool* aProcessCanceledByUser) {
+ NS_ENSURE_ARG_POINTER(aProcessCanceledByUser);
+ *aProcessCanceledByUser = m_processCanceled;
+ return NS_OK;
+}
+NS_IMETHODIMP nsMsgProgress::SetProcessCanceledByUser(
+ bool aProcessCanceledByUser) {
+ m_processCanceled = aProcessCanceledByUser;
+ OnStateChange(nullptr, nullptr, nsIWebProgressListener::STATE_STOP,
+ NS_BINDING_ABORTED);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::RegisterListener(
+ nsIWebProgressListener* listener) {
+ if (!listener) // Nothing to do with a null listener!
+ return NS_OK;
+
+ NS_ENSURE_ARG(this != listener); // Check for self-reference (see bug 271700)
+
+ m_listenerList.AppendObject(listener);
+ if (m_closeProgress || m_processCanceled)
+ listener->OnStateChange(nullptr, nullptr,
+ nsIWebProgressListener::STATE_STOP, NS_OK);
+ else {
+ listener->OnStatusChange(nullptr, nullptr, NS_OK, m_pendingStatus.get());
+ if (m_pendingStateFlags != -1)
+ listener->OnStateChange(nullptr, nullptr, m_pendingStateFlags,
+ m_pendingStateValue);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::UnregisterListener(
+ nsIWebProgressListener* listener) {
+ if (listener) m_listenerList.RemoveObject(listener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aStateFlags,
+ nsresult aStatus) {
+ m_pendingStateFlags = aStateFlags;
+ m_pendingStateValue = aStatus;
+
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindow));
+ if (aStateFlags == nsIWebProgressListener::STATE_STOP && msgWindow &&
+ NS_FAILED(aStatus)) {
+ msgWindow->StopUrls();
+ msgWindow->SetStatusFeedback(nullptr);
+ }
+
+ for (int32_t i = m_listenerList.Count() - 1; i >= 0; i--)
+ m_listenerList[i]->OnStateChange(aWebProgress, aRequest, aStateFlags,
+ aStatus);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ for (int32_t i = m_listenerList.Count() - 1; i >= 0; i--)
+ m_listenerList[i]->OnProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress, aMaxSelfProgress,
+ aCurTotalProgress, aMaxTotalProgress);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnLocationChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsIURI* location,
+ uint32_t aFlags) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ nsresult aStatus,
+ const char16_t* aMessage) {
+ if (aMessage && *aMessage) m_pendingStatus = aMessage;
+ for (int32_t i = m_listenerList.Count() - 1; i >= 0; i--)
+ m_listenerList[i]->OnStatusChange(aWebProgress, aRequest, aStatus,
+ aMessage);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t state) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProgress::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t aEvent) {
+ return NS_OK;
+}
+
+nsresult nsMsgProgress::ReleaseListeners() {
+ m_listenerList.Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::ShowStatusString(const nsAString& aStatus) {
+ return OnStatusChange(nullptr, nullptr, NS_OK,
+ PromiseFlatString(aStatus).get());
+}
+
+NS_IMETHODIMP nsMsgProgress::SetStatusString(const nsAString& aStatus) {
+ return OnStatusChange(nullptr, nullptr, NS_OK,
+ PromiseFlatString(aStatus).get());
+}
+
+NS_IMETHODIMP nsMsgProgress::StartMeteors() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP nsMsgProgress::StopMeteors() { return NS_ERROR_NOT_IMPLEMENTED; }
+
+NS_IMETHODIMP nsMsgProgress::ShowProgress(int32_t percent) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgProgress::SetWrappedStatusFeedback(
+ nsIMsgStatusFeedback* aJSStatusFeedback) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgProgress::SetMsgWindow(nsIMsgWindow* aMsgWindow) {
+ m_msgWindow = do_GetWeakReference(aMsgWindow);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::GetMsgWindow(nsIMsgWindow** aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ if (m_msgWindow)
+ CallQueryReferent(m_msgWindow.get(), aMsgWindow);
+ else
+ *aMsgWindow = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProgress::OnProgress(nsIRequest* request, int64_t aProgress,
+ int64_t aProgressMax) {
+ // XXX: What should the nsIWebProgress be?
+ // XXX: This truncates 64-bit to 32-bit
+ return OnProgressChange(nullptr, request, int32_t(aProgress),
+ int32_t(aProgressMax),
+ int32_t(aProgress) /* current total progress */,
+ int32_t(aProgressMax) /* max total progress */);
+}
+
+NS_IMETHODIMP nsMsgProgress::OnStatus(nsIRequest* request, nsresult aStatus,
+ const char16_t* aStatusArg) {
+ nsresult rv;
+ nsCOMPtr<nsIStringBundleService> sbs =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sbs, NS_ERROR_UNEXPECTED);
+ nsString str;
+ rv = sbs->FormatStatusMessage(aStatus, aStatusArg, str);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return ShowStatusString(str);
+}
diff --git a/comm/mailnews/base/src/nsMsgProgress.h b/comm/mailnews/base/src/nsMsgProgress.h
new file mode 100644
index 0000000000..57fae38aa9
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgProgress.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMsgProgress_h_
+#define nsMsgProgress_h_
+
+#include "nsIMsgProgress.h"
+#include "nsCOMPtr.h"
+#include "nsCOMArray.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsString.h"
+#include "nsIMsgWindow.h"
+#include "nsIProgressEventSink.h"
+#include "nsIStringBundle.h"
+#include "nsWeakReference.h"
+
+class nsMsgProgress : public nsIMsgProgress,
+ public nsIMsgStatusFeedback,
+ public nsIProgressEventSink,
+ public nsSupportsWeakReference {
+ public:
+ nsMsgProgress();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGPROGRESS
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIMSGSTATUSFEEDBACK
+ NS_DECL_NSIPROGRESSEVENTSINK
+
+ private:
+ virtual ~nsMsgProgress();
+ nsresult ReleaseListeners(void);
+
+ bool m_closeProgress;
+ bool m_processCanceled;
+ nsString m_pendingStatus;
+ int32_t m_pendingStateFlags;
+ nsresult m_pendingStateValue;
+ nsWeakPtr m_msgWindow;
+ nsCOMArray<nsIWebProgressListener> m_listenerList;
+};
+
+#endif // nsMsgProgress_h_
diff --git a/comm/mailnews/base/src/nsMsgProtocol.cpp b/comm/mailnews/base/src/nsMsgProtocol.cpp
new file mode 100644
index 0000000000..7fc832758c
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgProtocol.cpp
@@ -0,0 +1,1512 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsString.h"
+#include "nsMemory.h"
+#include "nsMsgProtocol.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgMailSession.h"
+#include "nsIStreamTransportService.h"
+#include "nsISocketTransportService.h"
+#include "nsISocketTransport.h"
+#include "nsITLSSocketControl.h"
+#include "nsITransportSecurityInfo.h"
+#include "nsILoadGroup.h"
+#include "nsILoadInfo.h"
+#include "nsIIOService.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsIWebProgressListener.h"
+#include "nsIPipe.h"
+#include "nsIPrompt.h"
+#include "prprf.h"
+#include "plbase64.h"
+#include "nsIStringBundle.h"
+#include "nsIProxyInfo.h"
+#include "nsThreadUtils.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsMsgUtils.h"
+#include "nsILineInputStream.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIInputStreamPump.h"
+#include "nsICancelable.h"
+#include "nsMimeTypes.h"
+#include "mozilla/Components.h"
+#include "mozilla/SlicedInputStream.h"
+#include "nsContentSecurityManager.h"
+#include "nsPrintfCString.h"
+
+#undef PostMessage // avoid to collision with WinUser.h
+
+using namespace mozilla;
+
+NS_IMPL_ISUPPORTS_INHERITED(nsMsgProtocol, nsHashPropertyBag, nsIMailChannel,
+ nsIChannel, nsIStreamListener, nsIRequestObserver,
+ nsIRequest, nsITransportEventSink)
+
+static char16_t* FormatStringWithHostNameByName(const char16_t* stringName,
+ nsIMsgMailNewsUrl* msgUri);
+
+nsMsgProtocol::nsMsgProtocol(nsIURI* aURL) {
+ m_flags = 0;
+ m_readCount = 0;
+ mLoadFlags = 0;
+ m_socketIsOpen = false;
+ mContentLength = -1;
+ m_isChannel = false;
+ mContentDisposition = nsIChannel::DISPOSITION_INLINE;
+
+ GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "tempMessage.eml",
+ getter_AddRefs(m_tempMsgFile));
+
+ mSuppressListenerNotifications = false;
+ InitFromURI(aURL);
+}
+
+nsresult nsMsgProtocol::InitFromURI(nsIURI* aUrl) {
+ m_url = aUrl;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl);
+ if (mailUrl) {
+ mailUrl->GetLoadGroup(getter_AddRefs(m_loadGroup));
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ mailUrl->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ mProgressEventSink = do_QueryInterface(statusFeedback);
+ }
+
+ // Reset channel data in case the object is reused and initialised again.
+ mCharset.Truncate();
+
+ return NS_OK;
+}
+
+nsMsgProtocol::~nsMsgProtocol() {}
+
+static bool gGotTimeoutPref;
+static int32_t gSocketTimeout = 60;
+
+nsresult nsMsgProtocol::GetQoSBits(uint8_t* aQoSBits) {
+ NS_ENSURE_ARG_POINTER(aQoSBits);
+ const char* protocol = GetType();
+
+ if (!protocol) return NS_ERROR_NOT_IMPLEMENTED;
+
+ nsAutoCString prefName("mail.");
+ prefName.Append(protocol);
+ prefName.AppendLiteral(".qos");
+
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t val;
+ rv = prefBranch->GetIntPref(prefName.get(), &val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aQoSBits = (uint8_t)clamped(val, 0, 0xff);
+ return NS_OK;
+}
+
+nsresult nsMsgProtocol::OpenNetworkSocketWithInfo(
+ const char* aHostName, int32_t aGetPort, const char* connectionType,
+ nsIProxyInfo* aProxyInfo, nsIInterfaceRequestor* callbacks) {
+ NS_ENSURE_ARG(aHostName);
+
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsISocketTransportService> socketService(
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID));
+ NS_ENSURE_TRUE(socketService, NS_ERROR_FAILURE);
+
+ // with socket connections we want to read as much data as arrives
+ m_readCount = -1;
+
+ nsCOMPtr<nsISocketTransport> strans;
+ AutoTArray<nsCString, 1> connectionTypeArray;
+ if (connectionType) connectionTypeArray.AppendElement(connectionType);
+ rv = socketService->CreateTransport(
+ connectionTypeArray, nsDependentCString(aHostName), aGetPort, aProxyInfo,
+ nullptr, getter_AddRefs(strans));
+ if (NS_FAILED(rv)) return rv;
+
+ strans->SetSecurityCallbacks(callbacks);
+
+ // creates cyclic reference!
+ nsCOMPtr<nsIThread> currentThread(do_GetCurrentThread());
+ strans->SetEventSink(this, currentThread);
+
+ m_socketIsOpen = false;
+ m_transport = strans;
+
+ if (!gGotTimeoutPref) {
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ if (prefBranch) {
+ prefBranch->GetIntPref("mailnews.tcptimeout", &gSocketTimeout);
+ gGotTimeoutPref = true;
+ }
+ }
+ strans->SetTimeout(nsISocketTransport::TIMEOUT_CONNECT, gSocketTimeout + 60);
+ strans->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, gSocketTimeout);
+
+ uint8_t qos;
+ rv = GetQoSBits(&qos);
+ if (NS_SUCCEEDED(rv)) strans->SetQoSBits(qos);
+
+ return SetupTransportState();
+}
+
+nsresult nsMsgProtocol::GetFileFromURL(nsIURI* aURL, nsIFile** aResult) {
+ NS_ENSURE_ARG_POINTER(aURL);
+ NS_ENSURE_ARG_POINTER(aResult);
+ // extract the file path from the uri...
+ nsAutoCString urlSpec;
+ aURL->GetPathQueryRef(urlSpec);
+ urlSpec.InsertLiteral("file://", 0);
+ nsresult rv;
+
+ // dougt - there should be an easier way!
+ nsCOMPtr<nsIURI> uri;
+ if (NS_FAILED(rv = NS_NewURI(getter_AddRefs(uri), urlSpec.get()))) return rv;
+
+ nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(uri);
+ if (!fileURL) return NS_ERROR_FAILURE;
+
+ return fileURL->GetFile(aResult);
+ // dougt
+}
+
+nsresult nsMsgProtocol::OpenFileSocket(nsIURI* aURL, uint64_t aStartPosition,
+ int64_t aReadCount) {
+ // mscott - file needs to be encoded directly into aURL. I should be able to
+ // get rid of this method completely.
+
+ nsresult rv = NS_OK;
+ m_readCount = aReadCount;
+ nsCOMPtr<nsIFile> file;
+
+ rv = GetFileFromURL(aURL, getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIInputStream> stream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file);
+ if (NS_FAILED(rv)) return rv;
+
+ // create input stream transport
+ nsCOMPtr<nsIStreamTransportService> sts =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // This can be called with aReadCount == -1 which means "read as much as we
+ // can". We pass this on as UINT64_MAX, which is in fact uint64_t(-1).
+ RefPtr<SlicedInputStream> slicedStream = new SlicedInputStream(
+ stream.forget(), aStartPosition,
+ aReadCount == -1 ? UINT64_MAX : uint64_t(aReadCount));
+ rv = sts->CreateInputTransport(slicedStream, true,
+ getter_AddRefs(m_transport));
+
+ m_socketIsOpen = false;
+ return rv;
+}
+
+nsresult nsMsgProtocol::GetTopmostMsgWindow(nsIMsgWindow** aWindow) {
+ nsresult rv;
+ nsCOMPtr<nsIMsgMailSession> mailSession(
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return mailSession->GetTopmostMsgWindow(aWindow);
+}
+
+nsresult nsMsgProtocol::SetupTransportState() {
+ if (!m_socketIsOpen && m_transport) {
+ nsresult rv;
+
+ // open buffered, blocking output stream
+ rv = m_transport->OpenOutputStream(nsITransport::OPEN_BLOCKING, 0, 0,
+ getter_AddRefs(m_outputStream));
+ if (NS_FAILED(rv)) return rv;
+ // we want to open the stream
+ } // if m_transport
+
+ return NS_OK;
+}
+
+nsresult nsMsgProtocol::CloseSocket() {
+ nsresult rv = NS_OK;
+ // release all of our socket state
+ m_socketIsOpen = false;
+ m_outputStream = nullptr;
+ if (m_transport) {
+ nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(m_transport);
+ if (strans) {
+ strans->SetEventSink(nullptr, nullptr); // break cyclic reference!
+ }
+ }
+ // we need to call Cancel so that we remove the socket transport from the
+ // mActiveTransportList. see bug #30648
+ if (m_request) {
+ rv = m_request->Cancel(NS_BINDING_ABORTED);
+ }
+ m_request = nullptr;
+ if (m_transport) {
+ m_transport->Close(NS_BINDING_ABORTED);
+ m_transport = nullptr;
+ }
+
+ return rv;
+}
+
+/*
+ * Writes the data contained in dataBuffer into the current output stream. It
+ * also informs the transport layer that this data is now available for
+ * transmission. Returns a positive number for success, 0 for failure (not all
+ * the bytes were written to the stream, etc). We need to make another pass
+ * through this file to install an error system (mscott)
+ *
+ * No logging is done in the base implementation, so aSuppressLogging is
+ * ignored.
+ */
+
+nsresult nsMsgProtocol::SendData(const char* dataBuffer,
+ bool aSuppressLogging) {
+ uint32_t writeCount = 0;
+
+ if (dataBuffer && m_outputStream)
+ return m_outputStream->Write(dataBuffer, PL_strlen(dataBuffer),
+ &writeCount);
+ // TODO make sure all the bytes in PL_strlen(dataBuffer) were written
+ else
+ return NS_ERROR_INVALID_ARG;
+}
+
+// Whenever data arrives from the connection, core netlib notifices the protocol
+// by calling OnDataAvailable. We then read and process the incoming data from
+// the input stream.
+NS_IMETHODIMP nsMsgProtocol::OnDataAvailable(nsIRequest* request,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset,
+ uint32_t count) {
+ // right now, this really just means turn around and churn through the state
+ // machine
+ nsCOMPtr<nsIURI> uri;
+ GetURI(getter_AddRefs(uri));
+
+ return ProcessProtocolState(uri, inStr, sourceOffset, count);
+}
+
+NS_IMETHODIMP nsMsgProtocol::OnStartRequest(nsIRequest* request) {
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIURI> uri;
+ GetURI(getter_AddRefs(uri));
+
+ if (uri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> aMsgUrl = do_QueryInterface(uri);
+ rv = aMsgUrl->SetUrlState(true, NS_OK);
+ if (m_loadGroup)
+ m_loadGroup->AddRequest(static_cast<nsIRequest*>(this),
+ nullptr /* context isupports */);
+ }
+
+ // if we are set up as a channel, we should notify our channel listener that
+ // we are starting... so pass in ourself as the channel and not the underlying
+ // socket or file channel the protocol happens to be using
+ if (!mSuppressListenerNotifications && m_channelListener) {
+ m_isChannel = true;
+ rv = m_channelListener->OnStartRequest(this);
+ }
+
+ nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(m_transport);
+
+ if (strans)
+ strans->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, gSocketTimeout);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+void nsMsgProtocol::ShowAlertMessage(nsIMsgMailNewsUrl* aMsgUrl,
+ nsresult aStatus) {
+ const char16_t* errorString = nullptr;
+ switch (aStatus) {
+ case NS_ERROR_UNKNOWN_HOST:
+ case NS_ERROR_UNKNOWN_PROXY_HOST:
+ errorString = u"unknownHostError";
+ break;
+ case NS_ERROR_CONNECTION_REFUSED:
+ case NS_ERROR_PROXY_CONNECTION_REFUSED:
+ errorString = u"connectionRefusedError";
+ break;
+ case NS_ERROR_NET_TIMEOUT:
+ errorString = u"netTimeoutError";
+ break;
+ case NS_ERROR_NET_RESET:
+ errorString = u"netResetError";
+ break;
+ case NS_ERROR_NET_INTERRUPT:
+ errorString = u"netInterruptError";
+ break;
+ case NS_ERROR_OFFLINE:
+ // Don't alert when offline as that is already displayed in the UI.
+ return;
+ default:
+ nsPrintfCString msg(
+ "Unexpected status passed to ShowAlertMessage: %" PRIx32,
+ static_cast<uint32_t>(aStatus));
+ NS_WARNING(msg.get());
+ return;
+ }
+
+ nsString errorMsg;
+ errorMsg.Adopt(FormatStringWithHostNameByName(errorString, aMsgUrl));
+ if (errorMsg.IsEmpty()) {
+ errorMsg.AssignLiteral(u"[StringID ");
+ errorMsg.Append(errorString);
+ errorMsg.AppendLiteral(u"?]");
+ }
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1");
+ if (mailSession) mailSession->AlertUser(errorMsg, aMsgUrl);
+}
+
+// stop binding is a "notification" informing us that the stream associated with
+// aURL is going away.
+NS_IMETHODIMP nsMsgProtocol::OnStopRequest(nsIRequest* request,
+ nsresult aStatus) {
+ nsresult rv = NS_OK;
+
+ // if we are set up as a channel, we should notify our channel listener that
+ // we are starting... so pass in ourself as the channel and not the underlying
+ // socket or file channel the protocol happens to be using
+ if (!mSuppressListenerNotifications && m_channelListener)
+ rv = m_channelListener->OnStopRequest(this, aStatus);
+
+ nsCOMPtr<nsIURI> uri;
+ GetURI(getter_AddRefs(uri));
+
+ if (uri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl = do_QueryInterface(uri);
+ rv = msgUrl->SetUrlState(false, aStatus); // Always returns NS_OK.
+ if (m_loadGroup)
+ m_loadGroup->RemoveRequest(static_cast<nsIRequest*>(this), nullptr,
+ aStatus);
+
+ // !m_isChannel because if we're set up as a channel, then the remove
+ // request above will handle alerting the user, so we don't need to.
+ //
+ // !NS_BINDING_ABORTED because we don't want to see an alert if the user
+ // cancelled the operation. also, we'll get here because we call Cancel()
+ // to force removal of the nsSocketTransport. see CloseSocket()
+ // bugs #30775 and #30648 relate to this
+ if (!m_isChannel && NS_FAILED(aStatus) && (aStatus != NS_BINDING_ABORTED))
+ ShowAlertMessage(msgUrl, aStatus);
+ } // if we have a mailnews url.
+
+ // Drop notification callbacks to prevent cycles.
+ mCallbacks = nullptr;
+ mProgressEventSink = nullptr;
+ // Call CloseSocket(), in case we got here because the server dropped the
+ // connection while reading, and we never get a chance to get back into
+ // the protocol state machine via OnDataAvailable.
+ if (m_socketIsOpen) CloseSocket();
+
+ return rv;
+}
+
+nsresult nsMsgProtocol::LoadUrl(nsIURI* aURL, nsISupports* aConsumer) {
+ // nsMsgProtocol implements nsIChannel, and all channels are required to
+ // have non-null loadInfo. So if it's still unset, we've not been correctly
+ // initialised.
+ MOZ_ASSERT(m_loadInfo);
+
+ // okay now kick us off to the next state...
+ // our first state is a process state so drive the state machine...
+ nsresult rv = NS_OK;
+ nsCOMPtr<nsIMsgMailNewsUrl> aMsgUrl = do_QueryInterface(aURL, &rv);
+
+ if (NS_SUCCEEDED(rv) && aMsgUrl) {
+ bool msgIsInLocalCache;
+ aMsgUrl->GetMsgIsInLocalCache(&msgIsInLocalCache);
+
+ // Set the url as a url currently being run...
+ rv = aMsgUrl->SetUrlState(true, NS_OK);
+
+ // if the url is given a stream consumer then we should use it to forward
+ // calls to...
+ if (!m_channelListener &&
+ aConsumer) // if we don't have a registered listener already
+ {
+ m_channelListener = do_QueryInterface(aConsumer);
+ m_isChannel = true;
+ }
+
+ if (!m_socketIsOpen) {
+ if (m_transport) {
+ // open buffered, asynchronous input stream
+ nsCOMPtr<nsIInputStream> stream;
+ rv = m_transport->OpenInputStream(0, 0, 0, getter_AddRefs(stream));
+ if (NS_FAILED(rv)) return rv;
+
+ // m_readCount can be -1 which means "read as much as we can".
+ // We pass this on as UINT64_MAX, which is in fact uint64_t(-1).
+ // We don't clone m_inputStream here, we simply give up ownership
+ // since otherwise the original would never be closed.
+ RefPtr<SlicedInputStream> slicedStream = new SlicedInputStream(
+ stream.forget(), 0,
+ m_readCount == -1 ? UINT64_MAX : uint64_t(m_readCount));
+ nsCOMPtr<nsIInputStreamPump> pump;
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump), slicedStream.forget());
+ if (NS_FAILED(rv)) return rv;
+
+ m_request = pump; // keep a reference to the pump so we can cancel it
+
+ // Put us in a state where we are always notified of incoming data.
+ // OnDataAvailable() will be called when that happens, which will
+ // pass that data into ProcessProtocolState().
+ rv = pump->AsyncRead(this);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncRead failed");
+ m_socketIsOpen = true; // mark the channel as open
+ }
+ } else if (!msgIsInLocalCache) {
+ // The connection is already open so we should begin processing our url.
+ rv = ProcessProtocolState(aURL, nullptr, 0, 0);
+ }
+ }
+
+ return rv;
+}
+
+///////////////////////////////////////////////////////////////////////
+// The rest of this file is mostly nsIChannel mumbo jumbo stuff
+///////////////////////////////////////////////////////////////////////
+
+nsresult nsMsgProtocol::SetUrl(nsIURI* aURL) {
+ m_url = aURL;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetLoadGroup(nsILoadGroup* aLoadGroup) {
+ m_loadGroup = aLoadGroup;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetTRRMode(nsIRequest::TRRMode* aTRRMode) {
+ return GetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetTRRMode(nsIRequest::TRRMode aTRRMode) {
+ return SetTRRModeImpl(aTRRMode);
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetOriginalURI(nsIURI** aURI) {
+ NS_IF_ADDREF(*aURI = m_originalUrl ? m_originalUrl : m_url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetOriginalURI(nsIURI* aURI) {
+ m_originalUrl = aURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetURI(nsIURI** aURI) {
+ NS_IF_ADDREF(*aURI = m_url);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::Open(nsIInputStream** _retval) {
+ nsCOMPtr<nsIStreamListener> listener;
+ nsresult rv =
+ nsContentSecurityManager::doContentSecurityCheck(this, listener);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_ImplementChannelOpen(this, _retval);
+}
+
+NS_IMETHODIMP nsMsgProtocol::AsyncOpen(nsIStreamListener* aListener) {
+ nsCOMPtr<nsIStreamListener> listener = aListener;
+ nsresult rv =
+ nsContentSecurityManager::doContentSecurityCheck(this, listener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t port;
+ rv = m_url->GetPort(&port);
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString scheme;
+ rv = m_url->GetScheme(scheme);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = NS_CheckPortSafety(port, scheme.get());
+ if (NS_FAILED(rv)) return rv;
+
+ // set the stream listener and then load the url
+ m_isChannel = true;
+
+ m_channelListener = listener;
+ return LoadUrl(m_url, nullptr);
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetLoadFlags(nsLoadFlags* aLoadFlags) {
+ *aLoadFlags = mLoadFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetLoadFlags(nsLoadFlags aLoadFlags) {
+ mLoadFlags = aLoadFlags;
+ return NS_OK; // don't fail when trying to set this
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetContentType(nsACString& aContentType) {
+ // as url dispatching matures, we'll be intelligent and actually start
+ // opening the url before specifying the content type. This will allow
+ // us to optimize the case where the message url actual refers to
+ // a part in the message that has a content type that is not message/rfc822
+
+ if (mContentType.IsEmpty())
+ aContentType.AssignLiteral("message/rfc822");
+ else
+ aContentType = mContentType;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetContentType(const nsACString& aContentType) {
+ nsAutoCString charset;
+ nsresult rv =
+ NS_ParseResponseContentType(aContentType, mContentType, charset);
+ if (NS_FAILED(rv) || mContentType.IsEmpty())
+ mContentType.AssignLiteral(UNKNOWN_CONTENT_TYPE);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetContentCharset(nsACString& aContentCharset) {
+ aContentCharset.Assign(mCharset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetContentCharset(
+ const nsACString& aContentCharset) {
+ mCharset.Assign(aContentCharset);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetContentDisposition(uint32_t* aContentDisposition) {
+ *aContentDisposition = mContentDisposition;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::SetContentDisposition(uint32_t aContentDisposition) {
+ mContentDisposition = aContentDisposition;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetContentDispositionFilename(
+ nsAString& aContentDispositionFilename) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::SetContentDispositionFilename(
+ const nsAString& aContentDispositionFilename) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetContentDispositionHeader(
+ nsACString& aContentDispositionHeader) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetContentLength(int64_t* aContentLength) {
+ *aContentLength = mContentLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetContentLength(int64_t aContentLength) {
+ mContentLength = aContentLength;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetSecurityInfo(
+ nsITransportSecurityInfo** secInfo) {
+ *secInfo = nullptr;
+ if (m_transport) {
+ nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(m_transport);
+ if (strans) {
+ nsCOMPtr<nsITLSSocketControl> tlsSocketControl;
+ if (NS_SUCCEEDED(
+ strans->GetTlsSocketControl(getter_AddRefs(tlsSocketControl)))) {
+ nsCOMPtr<nsITransportSecurityInfo> transportSecInfo;
+ if (NS_SUCCEEDED(tlsSocketControl->GetSecurityInfo(
+ getter_AddRefs(transportSecInfo)))) {
+ transportSecInfo.forget(secInfo);
+ }
+ }
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetName(nsACString& result) {
+ if (m_url) return m_url->GetSpec(result);
+ result.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetOwner(nsISupports** aPrincipal) {
+ NS_IF_ADDREF(*aPrincipal = mOwner);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetOwner(nsISupports* aPrincipal) {
+ mOwner = aPrincipal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetLoadGroup(nsILoadGroup** aLoadGroup) {
+ NS_IF_ADDREF(*aLoadGroup = m_loadGroup);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetLoadInfo(nsILoadInfo** aLoadInfo) {
+ NS_IF_ADDREF(*aLoadInfo = m_loadInfo);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetLoadInfo(nsILoadInfo* aLoadInfo) {
+ m_loadInfo = aLoadInfo;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetNotificationCallbacks(
+ nsIInterfaceRequestor** aNotificationCallbacks) {
+ NS_IF_ADDREF(*aNotificationCallbacks = mCallbacks.get());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::SetNotificationCallbacks(
+ nsIInterfaceRequestor* aNotificationCallbacks) {
+ mCallbacks = aNotificationCallbacks;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::OnTransportStatus(nsITransport* transport, nsresult status,
+ int64_t progress, int64_t progressMax) {
+ if ((mLoadFlags & LOAD_BACKGROUND) || !m_url) return NS_OK;
+
+ // these transport events should not generate any status messages
+ if (status == NS_NET_STATUS_RECEIVING_FROM ||
+ status == NS_NET_STATUS_SENDING_TO)
+ return NS_OK;
+
+ if (!mProgressEventSink) {
+ NS_QueryNotificationCallbacks(mCallbacks, m_loadGroup, mProgressEventSink);
+ if (!mProgressEventSink) return NS_OK;
+ }
+
+ nsAutoCString host;
+ m_url->GetHost(host);
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url);
+ if (mailnewsUrl) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ mailnewsUrl->GetServer(getter_AddRefs(server));
+ if (server) server->GetHostName(host);
+ }
+ mProgressEventSink->OnStatus(this, status, NS_ConvertUTF8toUTF16(host).get());
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgProtocol::GetIsDocument(bool* aIsDocument) {
+ return NS_GetIsDocumentChannel(this, aIsDocument);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// From nsIRequest
+////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP nsMsgProtocol::IsPending(bool* result) {
+ *result = m_channelListener != nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetStatus(nsresult* status) {
+ if (m_request) return m_request->GetStatus(status);
+
+ *status = NS_OK;
+ return *status;
+}
+
+NS_IMETHODIMP nsMsgProtocol::SetCanceledReason(const nsACString& aReason) {
+ return SetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetCanceledReason(nsACString& aReason) {
+ return GetCanceledReasonImpl(aReason);
+}
+
+NS_IMETHODIMP nsMsgProtocol::CancelWithReason(nsresult aStatus,
+ const nsACString& aReason) {
+ return CancelWithReasonImpl(aStatus, aReason);
+}
+
+NS_IMETHODIMP nsMsgProtocol::Cancel(nsresult status) {
+ if (m_proxyRequest) m_proxyRequest->Cancel(status);
+
+ if (m_request) return m_request->Cancel(status);
+
+ NS_WARNING("no request to cancel");
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsMsgProtocol::GetCanceled(bool* aCanceled) {
+ nsresult status = NS_ERROR_FAILURE;
+ GetStatus(&status);
+ *aCanceled = NS_FAILED(status);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgProtocol::Suspend() {
+ if (m_request) return m_request->Suspend();
+
+ NS_WARNING("no request to suspend");
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+NS_IMETHODIMP nsMsgProtocol::Resume() {
+ if (m_request) return m_request->Resume();
+
+ NS_WARNING("no request to resume");
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult nsMsgProtocol::PostMessage(nsIURI* url, nsIFile* postFile) {
+ if (!url || !postFile) return NS_ERROR_NULL_POINTER;
+
+#define POST_DATA_BUFFER_SIZE 2048
+
+ // mscott -- this function should be re-written to use the file url code
+ // so it can be asynch
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv =
+ NS_NewLocalFileInputStream(getter_AddRefs(inputStream), postFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsILineInputStream> lineInputStream(
+ do_QueryInterface(inputStream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more = true;
+ nsCString line;
+ nsCString outputBuffer;
+
+ do {
+ lineInputStream->ReadLine(line, &more);
+
+ /* escape starting periods
+ */
+ if (line.CharAt(0) == '.') line.Insert('.', 0);
+ line.AppendLiteral(CRLF);
+ outputBuffer.Append(line);
+ // test hack by mscott. If our buffer is almost full, then
+ // send it off & reset ourselves
+ // to make more room.
+ if (outputBuffer.Length() > POST_DATA_BUFFER_SIZE || !more) {
+ rv = SendData(outputBuffer.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ // does this keep the buffer around? That would be best.
+ // Maybe SetLength(0) instead?
+ outputBuffer.Truncate();
+ }
+ } while (more);
+
+ return NS_OK;
+}
+
+nsresult nsMsgProtocol::DoGSSAPIStep1(const nsACString& service,
+ const char* username,
+ nsCString& response) {
+ nsresult rv;
+#ifdef DEBUG_BenB
+ printf("GSSAPI step 1 for service %s, username %s\n", service, username);
+#endif
+
+ // if this fails, then it means that we cannot do GSSAPI SASL.
+ m_authModule = nsIAuthModule::CreateInstance("sasl-gssapi");
+
+ m_authModule->Init(service, nsIAuthModule::REQ_DEFAULT, u""_ns,
+ NS_ConvertUTF8toUTF16(username), u""_ns);
+
+ void* outBuf;
+ uint32_t outBufLen;
+ rv = m_authModule->GetNextToken((void*)nullptr, 0, &outBuf, &outBufLen);
+ if (NS_SUCCEEDED(rv) && outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str)
+ response.Adopt(base64Str);
+ else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ free(outBuf);
+ }
+
+#ifdef DEBUG_BenB
+ printf("GSSAPI step 1 succeeded\n");
+#endif
+ return rv;
+}
+
+nsresult nsMsgProtocol::DoGSSAPIStep2(nsCString& commandResponse,
+ nsCString& response) {
+#ifdef DEBUG_BenB
+ printf("GSSAPI step 2\n");
+#endif
+ nsresult rv;
+ void *inBuf, *outBuf;
+ uint32_t inBufLen, outBufLen;
+ uint32_t len = commandResponse.Length();
+
+ // Cyrus SASL may send us zero length tokens (grrrr)
+ if (len > 0) {
+ // decode into the input secbuffer
+ inBufLen = (len * 3) / 4; // sufficient size (see plbase64.h)
+ inBuf = moz_xmalloc(inBufLen);
+ if (!inBuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // strip off any padding (see bug 230351)
+ const char* challenge = commandResponse.get();
+ while (challenge[len - 1] == '=') len--;
+
+ // We need to know the exact length of the decoded string to give to
+ // the GSSAPI libraries. But NSPR's base64 routine doesn't seem capable
+ // of telling us that. So, we figure it out for ourselves.
+
+ // For every 4 characters, add 3 to the destination
+ // If there are 3 remaining, add 2
+ // If there are 2 remaining, add 1
+ // 1 remaining is an error
+ inBufLen =
+ (len / 4) * 3 + ((len % 4 == 3) ? 2 : 0) + ((len % 4 == 2) ? 1 : 0);
+
+ rv = (PL_Base64Decode(challenge, len, (char*)inBuf))
+ ? m_authModule->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen)
+ : NS_ERROR_FAILURE;
+
+ free(inBuf);
+ } else {
+ rv = m_authModule->GetNextToken(NULL, 0, &outBuf, &outBufLen);
+ }
+ if (NS_SUCCEEDED(rv)) {
+ // And in return, we may need to send Cyrus zero length tokens back
+ if (outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str)
+ response.Adopt(base64Str);
+ else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ } else
+ response.Adopt((char*)moz_xmemdup("", 1));
+ }
+
+#ifdef DEBUG_BenB
+ printf(NS_SUCCEEDED(rv) ? "GSSAPI step 2 succeeded\n"
+ : "GSSAPI step 2 failed\n");
+#endif
+ return rv;
+}
+
+nsresult nsMsgProtocol::DoNtlmStep1(const nsACString& username,
+ const nsAString& password,
+ nsCString& response) {
+ nsresult rv;
+
+ m_authModule = nsIAuthModule::CreateInstance("ntlm");
+
+ m_authModule->Init(""_ns, 0, u""_ns, NS_ConvertUTF8toUTF16(username),
+ PromiseFlatString(password));
+
+ void* outBuf;
+ uint32_t outBufLen;
+ rv = m_authModule->GetNextToken((void*)nullptr, 0, &outBuf, &outBufLen);
+ if (NS_SUCCEEDED(rv) && outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str)
+ response.Adopt(base64Str);
+ else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ free(outBuf);
+ }
+
+ return rv;
+}
+
+nsresult nsMsgProtocol::DoNtlmStep2(nsCString& commandResponse,
+ nsCString& response) {
+ nsresult rv;
+ void *inBuf, *outBuf;
+ uint32_t inBufLen, outBufLen;
+ uint32_t len = commandResponse.Length();
+
+ // decode into the input secbuffer
+ inBufLen = (len * 3) / 4; // sufficient size (see plbase64.h)
+ inBuf = moz_xmalloc(inBufLen);
+ if (!inBuf) return NS_ERROR_OUT_OF_MEMORY;
+
+ // strip off any padding (see bug 230351)
+ const char* challenge = commandResponse.get();
+ while (challenge[len - 1] == '=') len--;
+
+ rv = (PL_Base64Decode(challenge, len, (char*)inBuf))
+ ? m_authModule->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen)
+ : NS_ERROR_FAILURE;
+
+ free(inBuf);
+ if (NS_SUCCEEDED(rv) && outBuf) {
+ char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr);
+ if (base64Str)
+ response.Adopt(base64Str);
+ else
+ rv = NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (NS_FAILED(rv)) response = "*";
+
+ return rv;
+}
+
+/////////////////////////////////////////////////////////////////////
+// nsMsgAsyncWriteProtocol subclass and related helper classes
+/////////////////////////////////////////////////////////////////////
+
+class nsMsgProtocolStreamProvider : public nsIOutputStreamCallback {
+ public:
+ // XXX this probably doesn't need to be threadsafe
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ nsMsgProtocolStreamProvider() {}
+
+ void Init(nsMsgAsyncWriteProtocol* aProtInstance,
+ nsIInputStream* aInputStream) {
+ mMsgProtocol =
+ do_GetWeakReference(static_cast<nsIStreamListener*>(aProtInstance));
+ mInStream = aInputStream;
+ }
+
+ //
+ // nsIOutputStreamCallback implementation ...
+ //
+ NS_IMETHODIMP OnOutputStreamReady(nsIAsyncOutputStream* aOutStream) override {
+ NS_ASSERTION(mInStream, "not initialized");
+
+ nsresult rv;
+ uint64_t avail;
+
+ // Write whatever is available in the pipe. If the pipe is empty, then
+ // return NS_BASE_STREAM_WOULD_BLOCK; we will resume the write when there
+ // is more data.
+
+ rv = mInStream->Available(&avail);
+ if (NS_FAILED(rv)) return rv;
+
+ nsMsgAsyncWriteProtocol* protInst = nullptr;
+ nsCOMPtr<nsIStreamListener> callback = do_QueryReferent(mMsgProtocol);
+ if (!callback) return NS_ERROR_FAILURE;
+ protInst = static_cast<nsMsgAsyncWriteProtocol*>(callback.get());
+
+ if (avail == 0 && !protInst->mAsyncBuffer.Length()) {
+ // ok, stop writing...
+ protInst->mSuspendedWrite = true;
+ return NS_OK;
+ }
+ protInst->mSuspendedWrite = false;
+
+ uint32_t bytesWritten;
+
+ if (avail) {
+ rv = aOutStream->WriteFrom(mInStream,
+ std::min(avail, uint64_t(FILE_IO_BUFFER_SIZE)),
+ &bytesWritten);
+ // if were full at the time, the input stream may be backed up and we need
+ // to read any remains from the last ODA call before we'll get more ODA
+ // calls
+ if (protInst->mSuspendedRead) protInst->UnblockPostReader();
+ } else {
+ rv = aOutStream->Write(protInst->mAsyncBuffer.get(),
+ protInst->mAsyncBuffer.Length(), &bytesWritten);
+ protInst->mAsyncBuffer.Cut(0, bytesWritten);
+ }
+
+ protInst->UpdateProgress(bytesWritten);
+
+ // try to write again...
+ if (NS_SUCCEEDED(rv))
+ rv = aOutStream->AsyncWait(this, 0, 0, protInst->mProviderThread);
+
+ NS_ASSERTION(NS_SUCCEEDED(rv) || rv == NS_BINDING_ABORTED,
+ "unexpected error writing stream");
+ return NS_OK;
+ }
+
+ protected:
+ virtual ~nsMsgProtocolStreamProvider() {}
+
+ nsWeakPtr mMsgProtocol;
+ nsCOMPtr<nsIInputStream> mInStream;
+};
+
+NS_IMPL_ISUPPORTS(nsMsgProtocolStreamProvider, nsIOutputStreamCallback)
+
+class nsMsgFilePostHelper : public nsIStreamListener {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ nsMsgFilePostHelper() { mSuspendedPostFileRead = false; }
+ nsresult Init(nsIOutputStream* aOutStream,
+ nsMsgAsyncWriteProtocol* aProtInstance, nsIFile* aFileToPost);
+ nsCOMPtr<nsIRequest> mPostFileRequest;
+ bool mSuspendedPostFileRead;
+ void CloseSocket() { mProtInstance = nullptr; }
+
+ protected:
+ virtual ~nsMsgFilePostHelper() {}
+ nsCOMPtr<nsIOutputStream> mOutStream;
+ nsWeakPtr mProtInstance;
+};
+
+NS_IMPL_ISUPPORTS(nsMsgFilePostHelper, nsIStreamListener, nsIRequestObserver)
+
+nsresult nsMsgFilePostHelper::Init(nsIOutputStream* aOutStream,
+ nsMsgAsyncWriteProtocol* aProtInstance,
+ nsIFile* aFileToPost) {
+ nsresult rv = NS_OK;
+ mOutStream = aOutStream;
+ mProtInstance =
+ do_GetWeakReference(static_cast<nsIStreamListener*>(aProtInstance));
+
+ nsCOMPtr<nsIInputStream> stream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), aFileToPost);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIInputStreamPump> pump;
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget());
+ if (NS_FAILED(rv)) return rv;
+
+ rv = pump->AsyncRead(this);
+ if (NS_FAILED(rv)) return rv;
+
+ mPostFileRequest = pump;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilePostHelper::OnStartRequest(nsIRequest* aChannel) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilePostHelper::OnStopRequest(nsIRequest* aChannel,
+ nsresult aStatus) {
+ nsMsgAsyncWriteProtocol* protInst = nullptr;
+ nsCOMPtr<nsIStreamListener> callback = do_QueryReferent(mProtInstance);
+ if (!callback) return NS_OK;
+ protInst = static_cast<nsMsgAsyncWriteProtocol*>(callback.get());
+
+ if (!mSuspendedPostFileRead) protInst->PostDataFinished();
+
+ mSuspendedPostFileRead = false;
+ protInst->mFilePostHelper = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgFilePostHelper::OnDataAvailable(nsIRequest* /* aChannel */,
+ nsIInputStream* inStr,
+ uint64_t sourceOffset,
+ uint32_t count) {
+ nsMsgAsyncWriteProtocol* protInst = nullptr;
+ nsCOMPtr<nsIStreamListener> callback = do_QueryReferent(mProtInstance);
+ if (!callback) return NS_OK;
+
+ protInst = static_cast<nsMsgAsyncWriteProtocol*>(callback.get());
+
+ if (mSuspendedPostFileRead) {
+ protInst->UpdateSuspendedReadBytes(count, protInst->mInsertPeriodRequired);
+ return NS_OK;
+ }
+
+ protInst->ProcessIncomingPostData(inStr, count);
+
+ if (protInst->mSuspendedWrite) {
+ // if we got here then we had suspended the write 'cause we didn't have
+ // anymore data to write (i.e. the pipe went empty). So resume the channel
+ // to kick things off again.
+ protInst->mSuspendedWrite = false;
+ protInst->mAsyncOutStream->AsyncWait(protInst->mProvider, 0, 0,
+ protInst->mProviderThread);
+ }
+
+ return NS_OK;
+}
+
+NS_IMPL_ADDREF_INHERITED(nsMsgAsyncWriteProtocol, nsMsgProtocol)
+NS_IMPL_RELEASE_INHERITED(nsMsgAsyncWriteProtocol, nsMsgProtocol)
+
+NS_INTERFACE_MAP_BEGIN(nsMsgAsyncWriteProtocol)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END_INHERITING(nsMsgProtocol)
+
+nsMsgAsyncWriteProtocol::nsMsgAsyncWriteProtocol(nsIURI* aURL)
+ : nsMsgProtocol(aURL) {
+ mSuspendedWrite = false;
+ mSuspendedReadBytes = 0;
+ mSuspendedRead = false;
+ mInsertPeriodRequired = false;
+ mGenerateProgressNotifications = false;
+ mSuspendedReadBytesPostPeriod = 0;
+ mFilePostHelper = nullptr;
+ mNumBytesPosted = 0;
+ mFilePostSize = 0;
+}
+
+nsMsgAsyncWriteProtocol::~nsMsgAsyncWriteProtocol() {}
+
+NS_IMETHODIMP nsMsgAsyncWriteProtocol::Cancel(nsresult status) {
+ mGenerateProgressNotifications = false;
+
+ if (m_proxyRequest) {
+ m_proxyRequest->Cancel(status);
+ }
+
+ if (m_request) m_request->Cancel(status);
+
+ if (mAsyncOutStream) mAsyncOutStream->CloseWithStatus(status);
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::PostMessage(nsIURI* url, nsIFile* file) {
+ nsCOMPtr<nsIStreamListener> listener = new nsMsgFilePostHelper();
+
+ if (!listener) return NS_ERROR_OUT_OF_MEMORY;
+
+ // be sure to initialize some state before posting
+ mSuspendedReadBytes = 0;
+ mNumBytesPosted = 0;
+ file->GetFileSize(&mFilePostSize);
+ mSuspendedRead = false;
+ mInsertPeriodRequired = false;
+ mSuspendedReadBytesPostPeriod = 0;
+ mGenerateProgressNotifications = true;
+
+ mFilePostHelper = static_cast<nsMsgFilePostHelper*>(
+ static_cast<nsIStreamListener*>(listener));
+
+ static_cast<nsMsgFilePostHelper*>(static_cast<nsIStreamListener*>(listener))
+ ->Init(m_outputStream, this, file);
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::SuspendPostFileRead() {
+ if (mFilePostHelper && !mFilePostHelper->mSuspendedPostFileRead) {
+ // uhoh we need to pause reading in the file until we get unblocked...
+ mFilePostHelper->mPostFileRequest->Suspend();
+ mFilePostHelper->mSuspendedPostFileRead = true;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::ResumePostFileRead() {
+ if (mFilePostHelper) {
+ if (mFilePostHelper->mSuspendedPostFileRead) {
+ mFilePostHelper->mPostFileRequest->Resume();
+ mFilePostHelper->mSuspendedPostFileRead = false;
+ }
+ } else // we must be done with the download so send the '.'
+ {
+ PostDataFinished();
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::UpdateSuspendedReadBytes(
+ uint32_t aNewBytes, bool aAddToPostPeriodByteCount) {
+ // depending on our current state, we'll either add aNewBytes to
+ // mSuspendedReadBytes or mSuspendedReadBytesAfterPeriod.
+
+ mSuspendedRead = true;
+ if (aAddToPostPeriodByteCount)
+ mSuspendedReadBytesPostPeriod += aNewBytes;
+ else
+ mSuspendedReadBytes += aNewBytes;
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::PostDataFinished() {
+ nsresult rv = SendData("." CRLF);
+ if (NS_FAILED(rv)) return rv;
+ mGenerateProgressNotifications = false;
+ mPostDataStream = nullptr;
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::ProcessIncomingPostData(nsIInputStream* inStr,
+ uint32_t count) {
+ if (!m_socketIsOpen) return NS_OK; // kick out if the socket was canceled
+
+ // We need to quote any '.' that occur at the beginning of a line.
+ // but I don't want to waste time reading out the data into a buffer and
+ // searching let's try to leverage nsIBufferedInputStream and see if we can
+ // "peek" into the current contents for this particular case.
+
+ nsCOMPtr<nsISearchableInputStream> bufferInputStr = do_QueryInterface(inStr);
+ NS_ASSERTION(
+ bufferInputStr,
+ "i made a wrong assumption about the type of stream we are getting");
+ NS_ASSERTION(mSuspendedReadBytes == 0, "oops, I missed something");
+
+ if (!mPostDataStream) mPostDataStream = inStr;
+
+ if (bufferInputStr) {
+ uint32_t amountWritten;
+
+ while (count > 0) {
+ bool found = false;
+ uint32_t offset = 0;
+ bufferInputStr->Search("\012.", true, &found, &offset); // LF.
+
+ if (!found || offset > count) {
+ // push this data into the output stream
+ m_outputStream->WriteFrom(inStr, count, &amountWritten);
+ // store any remains which need read out at a later date
+ if (count > amountWritten) // stream will block
+ {
+ UpdateSuspendedReadBytes(count - amountWritten, false);
+ SuspendPostFileRead();
+ }
+ break;
+ } else {
+ // count points to the LF in a LF followed by a '.'
+ // go ahead and write up to offset..
+ m_outputStream->WriteFrom(inStr, offset + 1, &amountWritten);
+ count -= amountWritten;
+ if (offset + 1 > amountWritten) {
+ UpdateSuspendedReadBytes(offset + 1 - amountWritten, false);
+ mInsertPeriodRequired = true;
+ UpdateSuspendedReadBytes(count, mInsertPeriodRequired);
+ SuspendPostFileRead();
+ break;
+ }
+
+ // write out the extra '.'
+ m_outputStream->Write(".", 1, &amountWritten);
+ if (amountWritten != 1) {
+ mInsertPeriodRequired = true;
+ // once we do write out the '.', if we are now blocked we need to
+ // remember the remaining count that comes after the '.' so we can
+ // perform processing on that once we become unblocked.
+ UpdateSuspendedReadBytes(count, mInsertPeriodRequired);
+ SuspendPostFileRead();
+ break;
+ }
+ }
+ } // while count > 0
+ }
+
+ return NS_OK;
+}
+nsresult nsMsgAsyncWriteProtocol::UnblockPostReader() {
+ uint32_t amountWritten = 0;
+
+ if (!m_socketIsOpen) return NS_OK; // kick out if the socket was canceled
+
+ if (mSuspendedRead) {
+ // (1) attempt to write out any remaining read bytes we need in order to
+ // unblock the reader
+ if (mSuspendedReadBytes > 0 && mPostDataStream) {
+ uint64_t avail = 0;
+ mPostDataStream->Available(&avail);
+
+ m_outputStream->WriteFrom(mPostDataStream,
+ std::min(avail, uint64_t(mSuspendedReadBytes)),
+ &amountWritten);
+ // hmm sometimes my mSuspendedReadBytes is getting out of whack...so for
+ // now, reset it if necessary.
+ if (mSuspendedReadBytes > avail) mSuspendedReadBytes = avail;
+
+ if (mSuspendedReadBytes > amountWritten)
+ mSuspendedReadBytes -= amountWritten;
+ else
+ mSuspendedReadBytes = 0;
+ }
+
+ // (2) if we are now unblocked, and we need to insert a '.' then do so
+ // now...
+ if (mInsertPeriodRequired && mSuspendedReadBytes == 0) {
+ amountWritten = 0;
+ m_outputStream->Write(".", 1, &amountWritten);
+ if (amountWritten == 1) // if we succeeded then clear pending '.' flag
+ mInsertPeriodRequired = false;
+ }
+
+ // (3) if we inserted a '.' and we still have bytes after the '.' which need
+ // processed before the stream is unblocked then fake an ODA call to handle
+ // this now...
+ if (!mInsertPeriodRequired && mSuspendedReadBytesPostPeriod > 0) {
+ // these bytes actually need processed for extra '.''s.....
+ uint32_t postbytes = mSuspendedReadBytesPostPeriod;
+ mSuspendedReadBytesPostPeriod = 0;
+ ProcessIncomingPostData(mPostDataStream, postbytes);
+ }
+
+ // (4) determine if we are out of the suspended read state...
+ if (mSuspendedReadBytes == 0 && !mInsertPeriodRequired &&
+ mSuspendedReadBytesPostPeriod == 0) {
+ mSuspendedRead = false;
+ ResumePostFileRead();
+ }
+
+ } // if we are in the suspended read state
+
+ return NS_OK;
+}
+
+nsresult nsMsgAsyncWriteProtocol::SetupTransportState() {
+ nsresult rv = NS_OK;
+
+ if (!m_outputStream && m_transport) {
+ // first create a pipe which we'll use to write the data we want to send
+ // into.
+ nsCOMPtr<nsIPipe> pipe = do_CreateInstance("@mozilla.org/pipe;1");
+ rv = pipe->Init(true, true, 1024, 8);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsIAsyncInputStream* inputStream = nullptr;
+ // This always succeeds because the pipe is initialized above.
+ MOZ_ALWAYS_SUCCEEDS(pipe->GetInputStream(&inputStream));
+ mInStream = dont_AddRef(static_cast<nsIInputStream*>(inputStream));
+
+ nsIAsyncOutputStream* outputStream = nullptr;
+ // This always succeeds because the pipe is initialized above.
+ MOZ_ALWAYS_SUCCEEDS(pipe->GetOutputStream(&outputStream));
+ m_outputStream = dont_AddRef(static_cast<nsIOutputStream*>(outputStream));
+
+ mProviderThread = do_GetCurrentThread();
+
+ nsMsgProtocolStreamProvider* provider = new nsMsgProtocolStreamProvider();
+
+ if (!provider) return NS_ERROR_OUT_OF_MEMORY;
+
+ provider->Init(this, mInStream);
+ mProvider = provider; // ADDREF
+
+ nsCOMPtr<nsIOutputStream> stream;
+ rv = m_transport->OpenOutputStream(0, 0, 0, getter_AddRefs(stream));
+ if (NS_FAILED(rv)) return rv;
+
+ mAsyncOutStream = do_QueryInterface(stream, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ // wait for the output stream to become writable
+ rv = mAsyncOutStream->AsyncWait(mProvider, 0, 0, mProviderThread);
+ } // if m_transport
+
+ return rv;
+}
+
+nsresult nsMsgAsyncWriteProtocol::CloseSocket() {
+ nsresult rv = NS_OK;
+ if (mAsyncOutStream) mAsyncOutStream->CloseWithStatus(NS_BINDING_ABORTED);
+
+ nsMsgProtocol::CloseSocket();
+
+ if (mFilePostHelper) {
+ mFilePostHelper->CloseSocket();
+ mFilePostHelper = nullptr;
+ }
+
+ mAsyncOutStream = nullptr;
+ mProvider = nullptr;
+ mProviderThread = nullptr;
+ mAsyncBuffer.Truncate();
+ return rv;
+}
+
+void nsMsgAsyncWriteProtocol::UpdateProgress(uint32_t aNewBytes) {
+ if (!mGenerateProgressNotifications) return;
+
+ mNumBytesPosted += aNewBytes;
+ if (mFilePostSize > 0) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(m_url);
+ if (!mailUrl) return;
+
+ nsCOMPtr<nsIMsgStatusFeedback> statusFeedback;
+ mailUrl->GetStatusFeedback(getter_AddRefs(statusFeedback));
+ if (!statusFeedback) return;
+
+ nsCOMPtr<nsIWebProgressListener> webProgressListener(
+ do_QueryInterface(statusFeedback));
+ if (!webProgressListener) return;
+
+ // XXX not sure if m_request is correct here
+ webProgressListener->OnProgressChange(nullptr, m_request, mNumBytesPosted,
+ static_cast<uint32_t>(mFilePostSize),
+ mNumBytesPosted, mFilePostSize);
+ }
+
+ return;
+}
+
+nsresult nsMsgAsyncWriteProtocol::SendData(const char* dataBuffer,
+ bool aSuppressLogging) {
+ this->mAsyncBuffer.Append(dataBuffer);
+ if (!mAsyncOutStream) return NS_ERROR_FAILURE;
+ return mAsyncOutStream->AsyncWait(mProvider, 0, 0, mProviderThread);
+}
+
+char16_t* FormatStringWithHostNameByName(const char16_t* stringName,
+ nsIMsgMailNewsUrl* msgUri) {
+ if (!msgUri) return nullptr;
+
+ nsresult rv;
+
+ nsCOMPtr<nsIStringBundleService> sBundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sBundleService, nullptr);
+
+ nsCOMPtr<nsIStringBundle> sBundle;
+ rv = sBundleService->CreateBundle(MSGS_URL, getter_AddRefs(sBundle));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = msgUri->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsCString hostName;
+ rv = server->GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ AutoTArray<nsString, 1> params;
+ CopyASCIItoUTF16(hostName, *params.AppendElement());
+ nsAutoString str;
+ rv = sBundle->FormatStringFromName(NS_ConvertUTF16toUTF8(stringName).get(),
+ params, str);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ return ToNewUnicode(str);
+}
+
+// vim: ts=2 sw=2
diff --git a/comm/mailnews/base/src/nsMsgProtocol.h b/comm/mailnews/base/src/nsMsgProtocol.h
new file mode 100644
index 0000000000..c4aa707300
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgProtocol.h
@@ -0,0 +1,263 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMsgProtocol_h__
+#define nsMsgProtocol_h__
+
+#include "mozilla/Attributes.h"
+#include "nsIStreamListener.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsIChannel.h"
+#include "nsIURL.h"
+#include "nsIThread.h"
+#include "nsILoadGroup.h"
+#include "nsIFile.h"
+#include "nsCOMPtr.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIProgressEventSink.h"
+#include "nsITransport.h"
+#include "nsIAsyncOutputStream.h"
+#include "nsIAuthModule.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+#include "nsHashPropertyBag.h"
+#include "nsMailChannel.h"
+
+class nsIMsgWindow;
+class nsIPrompt;
+class nsIMsgMailNewsUrl;
+class nsMsgFilePostHelper;
+class nsIProxyInfo;
+class nsICancelable;
+
+// This is a helper class used to encapsulate code shared between all of the
+// mailnews protocol objects (imap, news, pop, smtp, etc.) In particular,
+// it unifies the core networking code for the protocols. My hope is that
+// this will make unification with Necko easier as we'll only have to change
+// this class and not all of the mailnews protocols.
+class nsMsgProtocol : public nsIStreamListener,
+ public nsIChannel,
+ public nsITransportEventSink,
+ public nsMailChannel,
+ public nsHashPropertyBag {
+ public:
+ nsMsgProtocol(nsIURI* aURL);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ // nsIChannel support
+ NS_DECL_NSICHANNEL
+ NS_DECL_NSIREQUEST
+
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSITRANSPORTEVENTSINK
+
+ // LoadUrl -- A protocol typically overrides this function, sets up any local
+ // state for the url and then calls the base class which opens the socket if
+ // it needs opened. If the socket is already opened then we just call
+ // ProcessProtocolState to start the churning process. aConsumer is the
+ // consumer for the url. It can be null if this argument is not appropriate
+ virtual nsresult LoadUrl(nsIURI* aURL, nsISupports* aConsumer = nullptr);
+
+ virtual nsresult SetUrl(
+ nsIURI* aURL); // sometimes we want to set the url before we load it
+ void ShowAlertMessage(nsIMsgMailNewsUrl* aMsgUrl, nsresult aStatus);
+
+ // Flag manipulators
+ virtual bool TestFlag(uint32_t flag) { return flag & m_flags; }
+ virtual void SetFlag(uint32_t flag) { m_flags |= flag; }
+ virtual void ClearFlag(uint32_t flag) { m_flags &= ~flag; }
+
+ protected:
+ virtual ~nsMsgProtocol();
+
+ // methods for opening and closing a socket with core netlib....
+ // mscott -okay this is lame. I should break this up into a file protocol and
+ // a socket based protocool class instead of cheating and putting both methods
+ // here...
+
+ // open a connection with a specific host and port
+ // aHostName must be UTF-8 encoded.
+ virtual nsresult OpenNetworkSocketWithInfo(const char* aHostName,
+ int32_t aGetPort,
+ const char* connectionType,
+ nsIProxyInfo* aProxyInfo,
+ nsIInterfaceRequestor* callbacks);
+ // helper routine
+ nsresult GetFileFromURL(nsIURI* aURL, nsIFile** aResult);
+ virtual nsresult OpenFileSocket(
+ nsIURI* aURL, uint64_t aStartPosition,
+ int64_t aReadCount); // used to open a file socket connection
+
+ nsresult GetTopmostMsgWindow(nsIMsgWindow** aWindow);
+
+ virtual const char* GetType() { return nullptr; }
+ nsresult GetQoSBits(uint8_t* aQoSBits);
+
+ // a Protocol typically overrides this method. They free any of their own
+ // connection state and then they call up into the base class to free the
+ // generic connection objects
+ virtual nsresult CloseSocket();
+
+ virtual nsresult
+ SetupTransportState(); // private method used by OpenNetworkSocket and
+ // OpenFileSocket
+
+ // ProcessProtocolState - This is the function that gets churned by calls to
+ // OnDataAvailable. As data arrives on the socket, OnDataAvailable calls
+ // ProcessProtocolState.
+
+ virtual nsresult ProcessProtocolState(nsIURI* url,
+ nsIInputStream* inputStream,
+ uint64_t sourceOffset,
+ uint32_t length) = 0;
+
+ // SendData -- Writes the data contained in dataBuffer into the current output
+ // stream. It also informs the transport layer that this data is now available
+ // for transmission. Returns a positive number for success, 0 for failure (not
+ // all the bytes were written to the stream, etc). aSuppressLogging is a hint
+ // that sensitive data is being sent and should not be logged
+ virtual nsresult SendData(const char* dataBuffer,
+ bool aSuppressLogging = false);
+
+ virtual nsresult PostMessage(nsIURI* url, nsIFile* aPostFile);
+
+ virtual nsresult InitFromURI(nsIURI* aUrl);
+
+ nsresult DoNtlmStep1(const nsACString& username, const nsAString& password,
+ nsCString& response);
+ nsresult DoNtlmStep2(nsCString& commandResponse, nsCString& response);
+
+ nsresult DoGSSAPIStep1(const nsACString& service, const char* username,
+ nsCString& response);
+ nsresult DoGSSAPIStep2(nsCString& commandResponse, nsCString& response);
+ // Output stream for writing commands to the socket
+ nsCOMPtr<nsIOutputStream>
+ m_outputStream; // this will be obtained from the transport interface
+
+ // Output stream for writing commands to the socket
+ nsCOMPtr<nsITransport> m_transport;
+ nsCOMPtr<nsIRequest> m_request;
+ nsCOMPtr<nsICancelable> m_proxyRequest;
+
+ bool m_socketIsOpen; // mscott: we should look into keeping this state in the
+ // nsSocketTransport... I'm using it to make sure I open
+ // the socket the first time a URL is loaded into the
+ // connection
+ uint32_t m_flags; // used to store flag information
+ // uint32_t m_startPosition;
+ int64_t m_readCount;
+
+ nsCOMPtr<nsIFile>
+ m_tempMsgFile; // we currently have a hack where displaying a msg
+ // involves writing it to a temp file first
+
+ // auth module for access to NTLM functions
+ nsCOMPtr<nsIAuthModule> m_authModule;
+
+ // the following is a catch all for nsIChannel related data
+ nsCOMPtr<nsIURI> m_originalUrl; // the original url
+ nsCOMPtr<nsIURI> m_url; // the running url
+ nsCOMPtr<nsISupports> m_consumer;
+ nsCOMPtr<nsIStreamListener> m_channelListener;
+ bool m_isChannel;
+ nsCOMPtr<nsILoadGroup> m_loadGroup;
+ nsLoadFlags mLoadFlags;
+ nsCOMPtr<nsIProgressEventSink> mProgressEventSink;
+ nsCOMPtr<nsIInterfaceRequestor> mCallbacks;
+ nsCOMPtr<nsISupports> mOwner;
+ nsCString mContentType;
+ nsCString mCharset;
+ int64_t mContentLength;
+ nsCOMPtr<nsILoadInfo> m_loadInfo;
+
+ nsString m_lastPasswordSent; // used to prefill the password prompt
+
+ // if a url isn't going to result in any content then we want to suppress
+ // calls to OnStartRequest, OnDataAvailable and OnStopRequest
+ bool mSuppressListenerNotifications;
+
+ uint32_t mContentDisposition;
+};
+
+// This is is a subclass of nsMsgProtocol extends the parent class with
+// AsyncWrite support. Protocols like smtp and news want to leverage async
+// write. We don't want everyone who inherits from nsMsgProtocol to have to pick
+// up the extra overhead.
+class nsMsgAsyncWriteProtocol : public nsMsgProtocol,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+
+ NS_IMETHOD Cancel(nsresult status) override;
+
+ nsMsgAsyncWriteProtocol(nsIURI* aURL);
+
+ // temporary over ride...
+ virtual nsresult PostMessage(nsIURI* url, nsIFile* postFile) override;
+
+ // over ride the following methods from the base class
+ virtual nsresult SetupTransportState() override;
+ virtual nsresult SendData(const char* dataBuffer,
+ bool aSuppressLogging = false) override;
+ nsCString mAsyncBuffer;
+
+ // if we suspended the asynch write while waiting for more data to write then
+ // this will be TRUE
+ bool mSuspendedWrite;
+ nsCOMPtr<nsIRequest> m_WriteRequest;
+ nsCOMPtr<nsIAsyncOutputStream> mAsyncOutStream;
+ nsCOMPtr<nsIOutputStreamCallback> mProvider;
+ nsCOMPtr<nsIThread> mProviderThread;
+
+ // because we are reading the post data in asynchronously, it's possible that
+ // we aren't sending it out fast enough and the reading gets blocked. The
+ // following set of state variables are used to track this.
+ bool mSuspendedRead;
+ bool mInsertPeriodRequired; // do we need to insert a '.' as part of the
+ // unblocking process
+
+ nsresult ProcessIncomingPostData(nsIInputStream* inStr, uint32_t count);
+ nsresult UnblockPostReader();
+ nsresult UpdateSuspendedReadBytes(uint32_t aNewBytes,
+ bool aAddToPostPeriodByteCount);
+ nsresult PostDataFinished(); // this is so we'll send out a closing '.' and
+ // release any state related to the post
+
+ // these two routines are used to pause and resume our loading of the file
+ // containing the contents we are trying to post. We call these routines when
+ // we aren't sending the bits out fast enough to keep up with the file read.
+ nsresult SuspendPostFileRead();
+ nsresult ResumePostFileRead();
+ nsresult UpdateSuspendedReadBytes(uint32_t aNewBytes);
+ void UpdateProgress(uint32_t aNewBytes);
+ nsMsgFilePostHelper* mFilePostHelper; // needs to be a weak reference
+ protected:
+ virtual ~nsMsgAsyncWriteProtocol();
+
+ // the streams for the pipe used to queue up data for the async write calls to
+ // the server. we actually re-use the same mOutStream variable in our parent
+ // class for the output stream to the socket channel. So no need for a new
+ // variable here.
+ nsCOMPtr<nsIInputStream> mInStream;
+ nsCOMPtr<nsIInputStream> mPostDataStream;
+ uint32_t mSuspendedReadBytes; // remaining # of bytes we need to read before
+ // the input stream becomes unblocked
+ uint32_t mSuspendedReadBytesPostPeriod; // # of bytes which need processed
+ // after we insert a '.' before the
+ // input stream becomes unblocked.
+ int64_t
+ mFilePostSize; // used for file size, we post a single message in a file
+ uint32_t mNumBytesPosted; // used for determining progress on posting files
+ bool
+ mGenerateProgressNotifications; // set during a post operation after
+ // we've started sending the post data...
+
+ virtual nsresult CloseSocket() override;
+};
+
+#endif /* nsMsgProtocol_h__ */
diff --git a/comm/mailnews/base/src/nsMsgPurgeService.cpp b/comm/mailnews/base/src/nsMsgPurgeService.cpp
new file mode 100644
index 0000000000..0f72416c7b
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgPurgeService.cpp
@@ -0,0 +1,496 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgPurgeService.h"
+#include "nsIMsgAccountManager.h"
+#include "nsMsgUtils.h"
+#include "nsMsgSearchCore.h"
+#include "msgCore.h"
+#include "nsISpamSettings.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgProtocolInfo.h"
+#include "nsIMsgFilterPlugin.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "mozilla/Logging.h"
+#include "nsMsgFolderFlags.h"
+#include "nsITimer.h"
+#include <stdlib.h>
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+
+static mozilla::LazyLogModule MsgPurgeLogModule("MsgPurge");
+
+NS_IMPL_ISUPPORTS(nsMsgPurgeService, nsIMsgPurgeService, nsIMsgSearchNotify)
+
+void OnPurgeTimer(nsITimer* timer, void* aPurgeService) {
+ nsMsgPurgeService* purgeService = (nsMsgPurgeService*)aPurgeService;
+ purgeService->PerformPurge();
+}
+
+nsMsgPurgeService::nsMsgPurgeService() {
+ mHaveShutdown = false;
+ // never purge a folder more than once every 8 hours (60 min/hour * 8 hours.
+ mMinDelayBetweenPurges = 480;
+ // fire the purge timer every 5 minutes, starting 5 minutes after the service
+ // is created (when we load accounts).
+ mPurgeTimerInterval = 5;
+}
+
+nsMsgPurgeService::~nsMsgPurgeService() {
+ if (mPurgeTimer) mPurgeTimer->Cancel();
+
+ if (!mHaveShutdown) Shutdown();
+}
+
+NS_IMETHODIMP nsMsgPurgeService::Init() {
+ nsresult rv;
+
+ // these prefs are here to help QA test this feature
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (NS_SUCCEEDED(rv)) {
+ int32_t min_delay;
+ rv = prefBranch->GetIntPref("mail.purge.min_delay", &min_delay);
+ if (NS_SUCCEEDED(rv) && min_delay) mMinDelayBetweenPurges = min_delay;
+
+ int32_t purge_timer_interval;
+ rv = prefBranch->GetIntPref("mail.purge.timer_interval",
+ &purge_timer_interval);
+ if (NS_SUCCEEDED(rv) && purge_timer_interval)
+ mPurgeTimerInterval = purge_timer_interval;
+ }
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("mail.purge.min_delay=%d minutes", mMinDelayBetweenPurges));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("mail.purge.timer_interval=%d minutes", mPurgeTimerInterval));
+
+ // don't start purging right away.
+ // because the accounts aren't loaded and because the user might be trying to
+ // sign in or startup, etc.
+ SetupNextPurge();
+
+ mHaveShutdown = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgPurgeService::Shutdown() {
+ if (mPurgeTimer) {
+ mPurgeTimer->Cancel();
+ mPurgeTimer = nullptr;
+ }
+
+ mHaveShutdown = true;
+ return NS_OK;
+}
+
+nsresult nsMsgPurgeService::SetupNextPurge() {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("setting to check again in %d minutes", mPurgeTimerInterval));
+
+ // Convert mPurgeTimerInterval into milliseconds
+ uint32_t timeInMSUint32 = mPurgeTimerInterval * 60000;
+
+ // Can't currently reset a timer when it's in the process of
+ // calling Notify. So, just release the timer here and create a new one.
+ if (mPurgeTimer) mPurgeTimer->Cancel();
+
+ nsresult rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(mPurgeTimer), OnPurgeTimer, (void*)this, timeInMSUint32,
+ nsITimer::TYPE_ONE_SHOT, "nsMsgPurgeService::OnPurgeTimer", nullptr);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Could not start mPurgeTimer timer");
+ }
+
+ return NS_OK;
+}
+
+// This is the function that looks for the first folder to purge. It also
+// applies retention settings to any folder that hasn't had retention settings
+// applied in mMinDelayBetweenPurges minutes (default, 8 hours).
+// However, if we've spent more than .5 seconds in this loop, don't
+// apply any more retention settings because it might lock up the UI.
+// This might starve folders later on in the hierarchy, since we always
+// start at the top, but since we also apply retention settings when you
+// open a folder, or when you compact all folders, I think this will do
+// for now, until we have a cleanup on shutdown architecture.
+nsresult nsMsgPurgeService::PerformPurge() {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("performing purge"));
+
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool keepApplyingRetentionSettings = true;
+
+ nsTArray<RefPtr<nsIMsgIncomingServer>> allServers;
+ rv = accountManager->GetAllServers(allServers);
+ if (NS_SUCCEEDED(rv)) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("%d servers", (int)allServers.Length()));
+ nsCOMPtr<nsIMsgFolder> folderToPurge;
+ PRIntervalTime startTime = PR_IntervalNow();
+ int32_t purgeIntervalToUse = 0;
+ PRTime oldestPurgeTime =
+ 0; // we're going to pick the least-recently purged folder
+
+ // apply retention settings to folders that haven't had retention settings
+ // applied in mMinDelayBetweenPurges minutes (default 8 hours)
+ // Because we get last purge time from the folder cache,
+ // this code won't open db's for folders until it decides it needs
+ // to apply retention settings, and since
+ // nsIMsgFolder::ApplyRetentionSettings will close any db's it opens, this
+ // code won't leave db's open.
+ for (uint32_t serverIndex = 0; serverIndex < allServers.Length();
+ serverIndex++) {
+ nsCOMPtr<nsIMsgIncomingServer> server(allServers[serverIndex]);
+ if (server) {
+ if (keepApplyingRetentionSettings) {
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+ rv = server->GetRootFolder(getter_AddRefs(rootFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsIMsgFolder>> childFolders;
+ rv = rootFolder->GetDescendants(childFolders);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto childFolder : childFolders) {
+ uint32_t folderFlags;
+ (void)childFolder->GetFlags(&folderFlags);
+ if (folderFlags & nsMsgFolderFlags::Virtual) continue;
+ PRTime curFolderLastPurgeTime = 0;
+ nsCString curFolderLastPurgeTimeString, curFolderUri;
+ rv = childFolder->GetStringProperty("LastPurgeTime",
+ curFolderLastPurgeTimeString);
+ if (NS_FAILED(rv))
+ continue; // it is ok to fail, go on to next folder
+
+ if (!curFolderLastPurgeTimeString.IsEmpty()) {
+ PRTime theTime;
+ PR_ParseTimeString(curFolderLastPurgeTimeString.get(), false,
+ &theTime);
+ curFolderLastPurgeTime = theTime;
+ }
+
+ childFolder->GetURI(curFolderUri);
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("%s curFolderLastPurgeTime=%s (if blank, then never)",
+ curFolderUri.get(), curFolderLastPurgeTimeString.get()));
+
+ // check if this folder is due to purge
+ // has to have been purged at least mMinDelayBetweenPurges minutes
+ // ago we don't want to purge the folders all the time - once a
+ // day is good enough
+ int64_t minDelayBetweenPurges(mMinDelayBetweenPurges);
+ int64_t microSecondsPerMinute(60000000);
+ PRTime nextPurgeTime =
+ curFolderLastPurgeTime +
+ (minDelayBetweenPurges * microSecondsPerMinute);
+ if (nextPurgeTime < PR_Now()) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("purging %s", curFolderUri.get()));
+ childFolder->ApplyRetentionSettings();
+ }
+ PRIntervalTime elapsedTime = PR_IntervalNow() - startTime;
+ // check if more than 500 milliseconds have elapsed in this purge
+ // process
+ if (PR_IntervalToMilliseconds(elapsedTime) > 500) {
+ keepApplyingRetentionSettings = false;
+ break;
+ }
+ }
+ }
+ nsCString type;
+ nsresult rv = server->GetType(type);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString hostName;
+ server->GetHostName(hostName);
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] %s (%s)", serverIndex, hostName.get(), type.get()));
+
+ nsCOMPtr<nsISpamSettings> spamSettings;
+ rv = server->GetSpamSettings(getter_AddRefs(spamSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t spamLevel;
+ spamSettings->GetLevel(&spamLevel);
+ // clang-format off
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] spamLevel=%d (if 0, don't purge)", serverIndex, spamLevel));
+ // clang-format on
+ if (!spamLevel) continue;
+
+ // check if we are set up to purge for this server
+ // if not, skip it.
+ bool purgeSpam;
+ spamSettings->GetPurge(&purgeSpam);
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] purgeSpam=%s (if false, don't purge)", serverIndex,
+ purgeSpam ? "true" : "false"));
+ if (!purgeSpam) continue;
+
+ // check if the spam folder uri is set for this server
+ // if not skip it.
+ nsCString junkFolderURI;
+ rv = spamSettings->GetSpamFolderURI(junkFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] junkFolderURI=%s (if empty, don't purge)", serverIndex,
+ junkFolderURI.get()));
+ if (junkFolderURI.IsEmpty()) continue;
+
+ // if the junk folder doesn't exist
+ // because the folder pane isn't built yet, for example
+ // skip this account
+ nsCOMPtr<nsIMsgFolder> junkFolder;
+ rv = FindFolder(junkFolderURI, getter_AddRefs(junkFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // clang-format off
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] %s exists? %s (if doesn't exist, don't purge)", serverIndex,
+ junkFolderURI.get(), junkFolder ? "true" : "false"));
+ // clang-format on
+ if (!junkFolder) continue;
+
+ PRTime curJunkFolderLastPurgeTime = 0;
+ nsCString curJunkFolderLastPurgeTimeString;
+ rv = junkFolder->GetStringProperty("curJunkFolderLastPurgeTime",
+ curJunkFolderLastPurgeTimeString);
+ if (NS_FAILED(rv))
+ continue; // it is ok to fail, junk folder may not exist
+
+ if (!curJunkFolderLastPurgeTimeString.IsEmpty()) {
+ PRTime theTime;
+ PR_ParseTimeString(curJunkFolderLastPurgeTimeString.get(), false,
+ &theTime);
+ curJunkFolderLastPurgeTime = theTime;
+ }
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] %s curJunkFolderLastPurgeTime=%s (if blank, then never)",
+ serverIndex, junkFolderURI.get(),
+ curJunkFolderLastPurgeTimeString.get()));
+
+ // check if this account is due to purge
+ // has to have been purged at least mMinDelayBetweenPurges minutes ago
+ // we don't want to purge the folders all the time
+ PRTime nextPurgeTime =
+ curJunkFolderLastPurgeTime +
+ mMinDelayBetweenPurges * 60000000 // convert to microseconds.
+ ;
+ if (nextPurgeTime < PR_Now()) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] last purge greater than min delay", serverIndex));
+
+ nsCOMPtr<nsIMsgIncomingServer> junkFolderServer;
+ rv = junkFolder->GetServer(getter_AddRefs(junkFolderServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool serverBusy = false;
+ bool serverRequiresPassword = true;
+ bool passwordPromptRequired;
+ bool canSearchMessages = false;
+ junkFolderServer->GetPasswordPromptRequired(&passwordPromptRequired);
+ junkFolderServer->GetServerBusy(&serverBusy);
+ junkFolderServer->GetServerRequiresPasswordForBiff(
+ &serverRequiresPassword);
+ junkFolderServer->GetCanSearchMessages(&canSearchMessages);
+ // Make sure we're logged on before doing the search (assuming we need
+ // to be) and make sure the server isn't already in the middle of
+ // downloading new messages and make sure a search isn't already going
+ // on
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] (search in progress? %s)", serverIndex,
+ mSearchSession ? "true" : "false"));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] (server busy? %s)", serverIndex,
+ serverBusy ? "true" : "false"));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] (serverRequiresPassword? %s)", serverIndex,
+ serverRequiresPassword ? "true" : "false"));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] (passwordPromptRequired? %s)", serverIndex,
+ passwordPromptRequired ? "true" : "false"));
+ if (canSearchMessages && !mSearchSession && !serverBusy &&
+ (!serverRequiresPassword || !passwordPromptRequired)) {
+ int32_t purgeInterval;
+ spamSettings->GetPurgeInterval(&purgeInterval);
+
+ if ((oldestPurgeTime == 0) ||
+ (curJunkFolderLastPurgeTime < oldestPurgeTime)) {
+ // clang-format off
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] purging! searching for messages older than %d days",
+ serverIndex, purgeInterval));
+ // clang-format on
+ oldestPurgeTime = curJunkFolderLastPurgeTime;
+ purgeIntervalToUse = purgeInterval;
+ folderToPurge = junkFolder;
+ // if we've never purged this folder, do it...
+ if (curJunkFolderLastPurgeTime == 0) break;
+ }
+ } else {
+ NS_ASSERTION(canSearchMessages,
+ "unexpected, you should be able to search");
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] not a good time for this server, try again later",
+ serverIndex));
+ }
+ } else {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("[%d] last purge too recent", serverIndex));
+ }
+ }
+ }
+ if (folderToPurge && purgeIntervalToUse != 0)
+ rv = SearchFolderToPurge(folderToPurge, purgeIntervalToUse);
+ }
+
+ // set up timer to check accounts again
+ SetupNextPurge();
+ return rv;
+}
+
+nsresult nsMsgPurgeService::SearchFolderToPurge(nsIMsgFolder* folder,
+ int32_t purgeInterval) {
+ nsresult rv;
+ mSearchSession =
+ do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mSearchSession->RegisterListener(this, nsIMsgSearchSession::allNotifications);
+
+ // update the time we attempted to purge this folder
+ char dateBuf[100];
+ dateBuf[0] = '\0';
+ PRExplodedTime exploded;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
+ PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y",
+ &exploded);
+ folder->SetStringProperty("curJunkFolderLastPurgeTime",
+ nsDependentCString(dateBuf));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("curJunkFolderLastPurgeTime is now %s", dateBuf));
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // We need to get the folder's server scope because imap can have
+ // local junk folder.
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgSearchScopeValue searchScope;
+ server->GetSearchScope(&searchScope);
+
+ mSearchSession->AddScopeTerm(searchScope, folder);
+
+ // look for messages older than the cutoff
+ // you can't also search by junk status, see
+ // nsMsgPurgeService::OnSearchHit()
+ nsCOMPtr<nsIMsgSearchTerm> searchTerm;
+ mSearchSession->CreateTerm(getter_AddRefs(searchTerm));
+ if (searchTerm) {
+ searchTerm->SetAttrib(nsMsgSearchAttrib::AgeInDays);
+ searchTerm->SetOp(nsMsgSearchOp::IsGreaterThan);
+ nsCOMPtr<nsIMsgSearchValue> searchValue;
+ searchTerm->GetValue(getter_AddRefs(searchValue));
+ if (searchValue) {
+ searchValue->SetAttrib(nsMsgSearchAttrib::AgeInDays);
+ searchValue->SetAge((uint32_t)purgeInterval);
+ searchTerm->SetValue(searchValue);
+ }
+ searchTerm->SetBooleanAnd(false);
+ mSearchSession->AppendTerm(searchTerm);
+ }
+
+ // we are about to search
+ // create mHdrsToDelete array (if not previously created)
+ NS_ASSERTION(mHdrsToDelete.IsEmpty(), "mHdrsToDelete is not empty");
+
+ mSearchFolder = folder;
+ return mSearchSession->Search(nullptr);
+}
+
+NS_IMETHODIMP nsMsgPurgeService::OnNewSearch() {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("on new search"));
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgPurgeService::OnSearchHit(nsIMsgDBHdr* aMsgHdr,
+ nsIMsgFolder* aFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+
+ nsCString messageId;
+ nsCString author;
+ nsCString subject;
+
+ aMsgHdr->GetMessageId(getter_Copies(messageId));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("messageId=%s", messageId.get()));
+ aMsgHdr->GetSubject(subject);
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("subject=%s", subject.get()));
+ aMsgHdr->GetAuthor(getter_Copies(author));
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("author=%s", author.get()));
+
+ // double check that the message is junk before adding to
+ // the list of messages to delete
+ //
+ // note, we can't just search for messages that are junk
+ // because not all imap server support keywords
+ // (which we use for the junk score)
+ // so the junk status would be in the message db.
+ //
+ // see bug #194090
+ nsCString junkScoreStr;
+ nsresult rv = aMsgHdr->GetStringProperty("junkscore", junkScoreStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("junkScore=%s (if empty or != nsIJunkMailPlugin::IS_SPAM_SCORE, "
+ "don't add to list delete)",
+ junkScoreStr.get()));
+
+ // if "junkscore" is not set, don't delete the message
+ if (junkScoreStr.IsEmpty()) return NS_OK;
+
+ if (atoi(junkScoreStr.get()) == nsIJunkMailPlugin::IS_SPAM_SCORE) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("added message to delete"));
+ mHdrsToDelete.AppendElement(aMsgHdr);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgPurgeService::OnSearchDone(nsresult status) {
+ if (NS_SUCCEEDED(status)) {
+ uint32_t count = mHdrsToDelete.Length();
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info,
+ ("%d messages to delete", count));
+
+ if (count > 0) {
+ MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("delete messages"));
+ if (mSearchFolder)
+ mSearchFolder->DeleteMessages(
+ mHdrsToDelete, nullptr, false /*delete storage*/, false /*isMove*/,
+ nullptr, false /*allowUndo*/);
+ }
+ }
+ mHdrsToDelete.Clear();
+ if (mSearchSession) mSearchSession->UnregisterListener(this);
+ // don't cache the session
+ // just create another search session next time we search, rather than
+ // clearing scopes, terms etc. we also use mSearchSession to determine if the
+ // purge service is "busy"
+ mSearchSession = nullptr;
+ mSearchFolder = nullptr;
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgPurgeService.h b/comm/mailnews/base/src/nsMsgPurgeService.h
new file mode 100644
index 0000000000..30643624d7
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgPurgeService.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef NSMSGPURGESERVICE_H
+#define NSMSGPURGESERVICE_H
+
+#include "msgCore.h"
+#include "nsIMsgPurgeService.h"
+#include "nsIMsgSearchSession.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgFolderCache.h"
+#include "nsIMsgFolderCacheElement.h"
+
+class nsMsgPurgeService : public nsIMsgPurgeService, public nsIMsgSearchNotify {
+ public:
+ nsMsgPurgeService();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGPURGESERVICE
+ NS_DECL_NSIMSGSEARCHNOTIFY
+
+ nsresult PerformPurge();
+
+ protected:
+ virtual ~nsMsgPurgeService();
+ int32_t FindServer(nsIMsgIncomingServer* server);
+ nsresult SetupNextPurge();
+ nsresult PurgeSurver(nsIMsgIncomingServer* server);
+ nsresult SearchFolderToPurge(nsIMsgFolder* folder, int32_t purgeInterval);
+
+ protected:
+ nsCOMPtr<nsITimer> mPurgeTimer;
+ nsCOMPtr<nsIMsgSearchSession> mSearchSession;
+ nsCOMPtr<nsIMsgFolder> mSearchFolder;
+ nsTArray<RefPtr<nsIMsgDBHdr>> mHdrsToDelete;
+ bool mHaveShutdown;
+
+ private:
+ // in minutes, how long must pass between two consecutive purges on the
+ // same junk folder?
+ int32_t mMinDelayBetweenPurges;
+ // in minutes, how often to check if we need to purge one of the junk folders?
+ int32_t mPurgeTimerInterval;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp b/comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp
new file mode 100644
index 0000000000..97d412acc4
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp
@@ -0,0 +1,806 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgQuickSearchDBView.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgImapMailFolder.h"
+#include "nsImapCore.h"
+#include "nsIMsgHdr.h"
+#include "nsIDBFolderInfo.h"
+#include "nsMsgMessageFlags.h"
+#include "nsMsgUtils.h"
+
+nsMsgQuickSearchDBView::nsMsgQuickSearchDBView() {
+ m_usingCachedHits = false;
+ m_cacheEmpty = true;
+}
+
+nsMsgQuickSearchDBView::~nsMsgQuickSearchDBView() {}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsMsgQuickSearchDBView, nsMsgDBView, nsIMsgDBView,
+ nsIMsgSearchNotify)
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::Open(nsIMsgFolder* folder,
+ nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags,
+ int32_t* pCount) {
+ nsresult rv =
+ nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_db) return NS_ERROR_NULL_POINTER;
+ m_viewFolder = nullptr;
+
+ int32_t count;
+ rv = InitThreadedView(count);
+ if (pCount) *pCount = count;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgQuickSearchDBView* newMsgDBView = new nsMsgQuickSearchDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ nsMsgThreadedDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow,
+ aCmdUpdater);
+ nsMsgQuickSearchDBView* newMsgDBView = (nsMsgQuickSearchDBView*)aNewMsgDBView;
+
+ // now copy all of our private member data
+ newMsgDBView->m_origKeys = m_origKeys.Clone();
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::DeleteMessages(
+ nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) {
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ (void)GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ RememberDeletedMsgHdr(msgHdr);
+ }
+ }
+ return nsMsgDBView::DeleteMessages(window, selection, deleteStorage);
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::DoCommand(
+ nsMsgViewCommandTypeValue aCommand) {
+ if (aCommand == nsMsgViewCommandType::markAllRead) {
+ nsresult rv = NS_OK;
+ m_folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications,
+ false);
+
+ for (uint32_t i = 0; NS_SUCCEEDED(rv) && i < GetSize(); i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ m_db->GetMsgHdrForKey(m_keys[i], getter_AddRefs(msgHdr));
+ rv = m_db->MarkHdrRead(msgHdr, true, nullptr);
+ }
+
+ m_folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications,
+ true);
+
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_folder);
+ if (NS_SUCCEEDED(rv) && imapFolder)
+ rv = imapFolder->StoreImapFlags(kImapMsgSeenFlag, true, m_keys, nullptr);
+
+ m_db->SetSummaryValid(true);
+ return rv;
+ } else
+ return nsMsgDBView::DoCommand(aCommand);
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::GetViewType(
+ nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowQuickSearchResults;
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::AddHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex* resultIndex) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ // protect against duplication.
+ if (m_origKeys.BinaryIndexOf(msgKey) == m_origKeys.NoIndex) {
+ nsMsgViewIndex insertIndex = GetInsertIndexHelper(
+ msgHdr, m_origKeys, nullptr, nsMsgViewSortOrder::ascending,
+ nsMsgViewSortType::byId);
+ m_origKeys.InsertElementAt(insertIndex, msgKey);
+ }
+ if (m_viewFlags & (nsMsgViewFlagsType::kGroupBySort |
+ nsMsgViewFlagsType::kThreadedDisplay)) {
+ nsMsgKey parentKey;
+ msgHdr->GetThreadParent(&parentKey);
+ return nsMsgThreadedDBView::OnNewHeader(msgHdr, parentKey, true);
+ } else
+ return nsMsgDBView::AddHdr(msgHdr, resultIndex);
+}
+
+nsresult nsMsgQuickSearchDBView::OnNewHeader(nsIMsgDBHdr* newHdr,
+ nsMsgKey aParentKey,
+ bool ensureListed) {
+ if (newHdr) {
+ bool match = false;
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession);
+ if (searchSession) searchSession->MatchHdr(newHdr, m_db, &match);
+ if (match) {
+ // put the new header in m_origKeys, so that expanding a thread will
+ // show the newly added header.
+ nsMsgKey newKey;
+ (void)newHdr->GetMessageKey(&newKey);
+ nsMsgViewIndex insertIndex = GetInsertIndexHelper(
+ newHdr, m_origKeys, nullptr, nsMsgViewSortOrder::ascending,
+ nsMsgViewSortType::byId);
+ m_origKeys.InsertElementAt(insertIndex, newKey);
+ nsMsgThreadedDBView::OnNewHeader(
+ newHdr, aParentKey,
+ ensureListed); // do not add a new message if there isn't a match.
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::OnHdrFlagsChanged(
+ nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ nsresult rv = nsMsgGroupView::OnHdrFlagsChanged(aHdrChanged, aOldFlags,
+ aNewFlags, aInstigator);
+
+ if (m_viewFolder && (m_viewFolder != m_folder) &&
+ (aOldFlags & nsMsgMessageFlags::Read) !=
+ (aNewFlags & nsMsgMessageFlags::Read)) {
+ // if we're displaying a single folder virtual folder for an imap folder,
+ // the search criteria might be on message body, and we might not have the
+ // message body offline, in which case we can't tell if the message
+ // matched or not. But if the unread flag changed, we need to update the
+ // unread counts. Normally, VirtualFolderChangeListener::OnHdrFlagsChanged
+ // will handle this, but it won't work for body criteria when we don't have
+ // the body offline.
+ nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_viewFolder);
+ if (imapFolder) {
+ nsMsgViewIndex hdrIndex = FindHdr(aHdrChanged);
+ if (hdrIndex != nsMsgViewIndex_None) {
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession);
+ if (searchSession) {
+ bool oldMatch, newMatch;
+ rv = searchSession->MatchHdr(aHdrChanged, m_db, &newMatch);
+ aHdrChanged->SetFlags(aOldFlags);
+ rv = searchSession->MatchHdr(aHdrChanged, m_db, &oldMatch);
+ aHdrChanged->SetFlags(aNewFlags);
+ // if it doesn't match the criteria,
+ // VirtualFolderChangeListener::OnHdrFlagsChanged won't tweak the
+ // read/unread counts. So do it here:
+ if (!oldMatch && !newMatch) {
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+
+ rv = m_viewFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ dbFolderInfo->ChangeNumUnreadMessages(
+ (aOldFlags & nsMsgMessageFlags::Read) ? 1 : -1);
+ m_viewFolder->UpdateSummaryTotals(true); // force update from db.
+ virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ }
+ }
+ }
+ }
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrChanged,
+ const nsACString& property,
+ bool aPreChange, uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) {
+ // If the junk mail plugin just activated on a message, then
+ // we'll allow filters to remove from view.
+ // Otherwise, just update the view line.
+ //
+ // Note this will not add newly matched headers to the view. This is
+ // probably a bug that needs fixing.
+
+ NS_ENSURE_ARG_POINTER(aStatus);
+ NS_ENSURE_ARG_POINTER(aHdrChanged);
+
+ nsMsgViewIndex index = FindHdr(aHdrChanged);
+ if (index == nsMsgViewIndex_None) // message does not appear in view
+ return NS_OK;
+
+ nsCString originStr;
+ (void)aHdrChanged->GetStringProperty("junkscoreorigin", originStr);
+ // check for "plugin" with only first character for performance
+ bool plugin = (originStr.get()[0] == 'p');
+
+ if (aPreChange) {
+ // first call, done prior to the change
+ *aStatus = plugin;
+ return NS_OK;
+ }
+
+ // second call, done after the change
+ bool wasPlugin = *aStatus;
+
+ bool match = true;
+ nsCOMPtr<nsIMsgSearchSession> searchSession(
+ do_QueryReferent(m_searchSession));
+ if (searchSession) searchSession->MatchHdr(aHdrChanged, m_db, &match);
+
+ if (!match && plugin && !wasPlugin)
+ RemoveByIndex(index); // remove hdr from view
+ else
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::GetSearchSession(nsIMsgSearchSession** aSession) {
+ NS_ASSERTION(false, "GetSearchSession method is not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::SetSearchSession(nsIMsgSearchSession* aSession) {
+ m_searchSession = do_GetWeakReference(aSession);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnSearchHit(nsIMsgDBHdr* aMsgHdr,
+ nsIMsgFolder* folder) {
+ NS_ENSURE_ARG(aMsgHdr);
+ if (!m_db) return NS_ERROR_NULL_POINTER;
+ // remember search hit and when search is done, reconcile cache
+ // with new hits;
+ m_hdrHits.AppendObject(aMsgHdr);
+ nsMsgKey key;
+ aMsgHdr->GetMessageKey(&key);
+ // Is FindKey going to be expensive here? A lot of hits could make
+ // it a little bit slow to search through the view for every hit.
+ if (m_cacheEmpty || FindKey(key, false) == nsMsgViewIndex_None)
+ return AddHdr(aMsgHdr);
+ else
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnSearchDone(nsresult status) {
+ // This batch began in OnNewSearch.
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ // We're a single-folder virtual folder if viewFolder != folder, and that is
+ // the only case in which we want to be messing about with a results cache
+ // or unread counts.
+ if (m_db && m_viewFolder && m_viewFolder != m_folder) {
+ nsTArray<nsMsgKey> keyArray;
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ uint32_t count = m_hdrHits.Count();
+ // Build up message keys. The cache expects them to be sorted.
+ for (uint32_t i = 0; i < count; i++) {
+ nsMsgKey key;
+ m_hdrHits[i]->GetMessageKey(&key);
+ keyArray.AppendElement(key);
+ }
+ keyArray.Sort();
+ nsTArray<nsMsgKey> staleHits;
+ nsresult rv = m_db->RefreshCache(searchUri, keyArray, staleHits);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (nsMsgKey staleKey : staleHits) {
+ nsCOMPtr<nsIMsgDBHdr> hdrDeleted;
+ m_db->GetMsgHdrForKey(staleKey, getter_AddRefs(hdrDeleted));
+ if (hdrDeleted) OnHdrDeleted(hdrDeleted, nsMsgKey_None, 0, this);
+ }
+
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo),
+ getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numUnread = 0;
+ size_t numTotal = m_origKeys.Length();
+
+ for (size_t i = 0; i < m_origKeys.Length(); i++) {
+ bool isRead;
+ m_db->IsRead(m_origKeys[i], &isRead);
+ if (!isRead) numUnread++;
+ }
+ dbFolderInfo->SetNumUnreadMessages(numUnread);
+ dbFolderInfo->SetNumMessages(numTotal);
+ m_viewFolder->UpdateSummaryTotals(true); // force update from db.
+ virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ }
+ if (m_sortType !=
+ nsMsgViewSortType::byThread) // we do not find levels for the results.
+ {
+ m_sortValid = false; // sort the results
+ Sort(m_sortType, m_sortOrder);
+ }
+ if (m_viewFolder && (m_viewFolder != m_folder))
+ SetMRUTimeForFolder(m_viewFolder);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnNewSearch() {
+ int32_t oldSize = GetSize();
+
+ m_keys.Clear();
+ m_levels.Clear();
+ m_flags.Clear();
+ m_hdrHits.Clear();
+ // this needs to happen after we remove all the keys, since RowCountChanged()
+ // will call our GetRowCount()
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ uint32_t folderFlags = 0;
+ if (m_viewFolder) m_viewFolder->GetFlags(&folderFlags);
+ // check if it's a virtual folder - if so, we should get the cached hits
+ // from the db, and set a flag saying that we're using cached values.
+ if (folderFlags & nsMsgFolderFlags::Virtual) {
+ nsCOMPtr<nsIMsgEnumerator> cachedHits;
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ m_db->GetCachedHits(searchUri, getter_AddRefs(cachedHits));
+ if (cachedHits) {
+ bool hasMore;
+
+ m_usingCachedHits = true;
+ cachedHits->HasMoreElements(&hasMore);
+ m_cacheEmpty = !hasMore;
+ if (mTree) mTree->BeginUpdateBatch();
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+ while (hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ nsresult rv = cachedHits->GetNext(getter_AddRefs(header));
+ if (header && NS_SUCCEEDED(rv))
+ AddHdr(header);
+ else
+ break;
+ cachedHits->HasMoreElements(&hasMore);
+ }
+ if (mTree) mTree->EndUpdateBatch();
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ }
+ }
+
+ // Prevent updates for every message found. This batch ends in OnSearchDone.
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::GetFirstMessageHdrToDisplayInThread(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr** result) {
+ uint32_t numChildren;
+ nsresult rv = NS_OK;
+ uint8_t minLevel = 0xff;
+ threadHdr->GetNumChildren(&numChildren);
+ nsMsgKey threadRootKey;
+ nsCOMPtr<nsIMsgDBHdr> rootParent;
+ threadHdr->GetRootHdr(getter_AddRefs(rootParent));
+ if (rootParent)
+ rootParent->GetMessageKey(&threadRootKey);
+ else
+ threadHdr->GetThreadKey(&threadRootKey);
+
+ nsCOMPtr<nsIMsgDBHdr> retHdr;
+
+ // iterate over thread, finding mgsHdr in view with the lowest level.
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ rv = threadHdr->GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey msgKey;
+ child->GetMessageKey(&msgKey);
+
+ // this works because we've already sorted m_keys by id.
+ nsMsgViewIndex keyIndex = m_origKeys.BinaryIndexOf(msgKey);
+ if (keyIndex != nsMsgViewIndex_None) {
+ // this is the root, so it's the best we're going to do.
+ if (msgKey == threadRootKey) {
+ retHdr = child;
+ break;
+ }
+ uint8_t level = 0;
+ nsMsgKey parentId;
+ child->GetThreadParent(&parentId);
+ nsCOMPtr<nsIMsgDBHdr> parent;
+ // count number of ancestors - that's our level
+ while (parentId != nsMsgKey_None) {
+ m_db->GetMsgHdrForKey(parentId, getter_AddRefs(parent));
+ if (parent) {
+ nsMsgKey saveParentId = parentId;
+ parent->GetThreadParent(&parentId);
+ // message is it's own parent - bad, let's break out of here.
+ // Or we've got some circular ancestry.
+ if (parentId == saveParentId || level > numChildren) break;
+ level++;
+ } else // if we can't find the parent, don't loop forever.
+ break;
+ }
+ if (level < minLevel) {
+ minLevel = level;
+ retHdr = child;
+ }
+ }
+ }
+ }
+ retHdr.forget(result);
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::SortThreads(
+ nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder) {
+ // don't need to sort by threads for group view.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) return NS_OK;
+ // iterate over the messages in the view, getting the thread id's
+ // sort m_keys so we can quickly find if a key is in the view.
+ m_keys.Sort();
+ // array of the threads' root hdr keys.
+ nsTArray<nsMsgKey> threadRootIds;
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ for (uint32_t i = 0; i < m_keys.Length(); i++) {
+ GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr));
+ m_db->GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr));
+ if (threadHdr) {
+ nsMsgKey rootKey;
+ threadHdr->GetChildKeyAt(0, &rootKey);
+ nsMsgViewIndex threadRootIndex = threadRootIds.BinaryIndexOf(rootKey);
+ // if we already have that id in top level threads, ignore this msg.
+ if (threadRootIndex != nsMsgViewIndex_None) continue;
+ // it would be nice if GetInsertIndexHelper always found the hdr, but it
+ // doesn't.
+ threadHdr->GetChildHdrAt(0, getter_AddRefs(rootHdr));
+ if (!rootHdr) continue;
+ threadRootIndex = GetInsertIndexHelper(rootHdr, threadRootIds, nullptr,
+ nsMsgViewSortOrder::ascending,
+ nsMsgViewSortType::byId);
+ threadRootIds.InsertElementAt(threadRootIndex, rootKey);
+ }
+ }
+
+ m_sortType = nsMsgViewSortType::byNone; // sort from scratch
+ // need to sort the top level threads now by sort order, if it's not by id
+ // and ascending (which is the order per above).
+ if (!(sortType == nsMsgViewSortType::byId &&
+ sortOrder == nsMsgViewSortOrder::ascending)) {
+ m_keys.SwapElements(threadRootIds);
+ nsMsgDBView::Sort(sortType, sortOrder);
+ threadRootIds.SwapElements(m_keys);
+ }
+ m_keys.Clear();
+ m_levels.Clear();
+ m_flags.Clear();
+ // now we've build up the list of thread ids - need to build the view
+ // from that. So for each thread id, we need to list the messages in the
+ // thread.
+ uint32_t numThreads = threadRootIds.Length();
+ for (uint32_t threadIndex = 0; threadIndex < numThreads; threadIndex++) {
+ m_db->GetMsgHdrForKey(threadRootIds[threadIndex], getter_AddRefs(rootHdr));
+ if (rootHdr) {
+ nsCOMPtr<nsIMsgDBHdr> displayRootHdr;
+ m_db->GetThreadContainingMsgHdr(rootHdr, getter_AddRefs(threadHdr));
+ if (threadHdr) {
+ nsMsgKey rootKey;
+ uint32_t rootFlags;
+ GetFirstMessageHdrToDisplayInThread(threadHdr,
+ getter_AddRefs(displayRootHdr));
+ if (!displayRootHdr) continue;
+ displayRootHdr->GetMessageKey(&rootKey);
+ displayRootHdr->GetFlags(&rootFlags);
+ rootFlags |= MSG_VIEW_FLAG_ISTHREAD;
+ m_keys.AppendElement(rootKey);
+ m_flags.AppendElement(rootFlags);
+ m_levels.AppendElement(0);
+
+ nsMsgViewIndex startOfThreadViewIndex = m_keys.Length();
+ nsMsgViewIndex rootIndex = startOfThreadViewIndex - 1;
+ uint32_t numListed = 0;
+ ListIdsInThreadOrder(threadHdr, rootKey, 1, &startOfThreadViewIndex,
+ &numListed);
+ if (numListed > 0)
+ m_flags[rootIndex] = rootFlags | MSG_VIEW_FLAG_HASCHILDREN;
+ }
+ }
+ }
+
+ // The thread state is left expanded (despite viewFlags) so at least reflect
+ // the correct state.
+ m_viewFlags |= nsMsgViewFlagsType::kExpandAll;
+
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::ListCollapsedChildren(
+ nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsresult rv = GetThreadContainingIndex(viewIndex, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsMsgKey rootKey;
+ GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(rootHdr));
+ rootHdr->GetMessageKey(&rootKey);
+ // group threads can have the root key twice, one for the dummy row.
+ bool rootKeySkipped = false;
+ for (uint32_t i = 0; i < numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ if (msgKey != rootKey || (GroupViewUsesDummyRow() && rootKeySkipped)) {
+ // if this hdr is in the original view, add it to new view.
+ if (m_origKeys.BinaryIndexOf(msgKey) != m_origKeys.NoIndex)
+ messageArray.AppendElement(msgHdr);
+ } else {
+ rootKeySkipped = true;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::ListIdsInThread(
+ nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) {
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) {
+ nsMsgKey parentKey = m_keys[startOfThreadViewIndex++];
+ return ListIdsInThreadOrder(threadHdr, parentKey, 1,
+ &startOfThreadViewIndex, pNumListed);
+ }
+
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ uint32_t i;
+ uint32_t viewIndex = startOfThreadViewIndex + 1;
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsMsgKey rootKey;
+ uint32_t rootFlags = m_flags[startOfThreadViewIndex];
+ *pNumListed = 0;
+ GetMsgHdrForViewIndex(startOfThreadViewIndex, getter_AddRefs(rootHdr));
+ rootHdr->GetMessageKey(&rootKey);
+ // group threads can have the root key twice, one for the dummy row.
+ bool rootKeySkipped = false;
+ for (i = 0; i < numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr != nullptr) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ if (msgKey != rootKey || (GroupViewUsesDummyRow() && rootKeySkipped)) {
+ nsMsgViewIndex threadRootIndex = m_origKeys.BinaryIndexOf(msgKey);
+ // if this hdr is in the original view, add it to new view.
+ if (threadRootIndex != nsMsgViewIndex_None) {
+ uint32_t childFlags;
+ msgHdr->GetFlags(&childFlags);
+ InsertMsgHdrAt(
+ viewIndex, msgHdr, msgKey, childFlags,
+ FindLevelInThread(msgHdr, startOfThreadViewIndex, viewIndex));
+ if (!(rootFlags & MSG_VIEW_FLAG_HASCHILDREN))
+ m_flags[startOfThreadViewIndex] =
+ rootFlags | MSG_VIEW_FLAG_HASCHILDREN;
+
+ viewIndex++;
+ (*pNumListed)++;
+ }
+ } else {
+ rootKeySkipped = true;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgQuickSearchDBView::ListIdsInThreadOrder(
+ nsIMsgThread* threadHdr, nsMsgKey parentKey, uint32_t level,
+ uint32_t callLevel, nsMsgKey keyToSkip, nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed) {
+ nsCOMPtr<nsIMsgEnumerator> msgEnumerator;
+ nsresult rv =
+ threadHdr->EnumerateMessages(parentKey, getter_AddRefs(msgEnumerator));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // We use the numChildren as a sanity check on the thread structure.
+ uint32_t numChildren;
+ (void)threadHdr->GetNumChildren(&numChildren);
+ bool hasMore;
+ while (NS_SUCCEEDED(rv = msgEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgEnumerator->GetNext(getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ if (msgKey == keyToSkip) continue;
+
+ // If we discover depths of more than numChildren, it means we have
+ // some sort of circular thread relationship and we bail out of the
+ // while loop before overflowing the stack with recursive calls.
+ // Technically, this is an error, but forcing a database rebuild
+ // is too destructive so we just return.
+ if (*pNumListed > numChildren || callLevel > numChildren) {
+ NS_ERROR("loop in message threading while listing children");
+ return NS_OK;
+ }
+
+ int32_t childLevel = level;
+ if (m_origKeys.BinaryIndexOf(msgKey) != m_origKeys.NoIndex) {
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ InsertMsgHdrAt(*viewIndex, msgHdr, msgKey, msgFlags & ~MSG_VIEW_FLAGS,
+ level);
+ (*pNumListed)++;
+ (*viewIndex)++;
+ childLevel++;
+ }
+ rv = ListIdsInThreadOrder(threadHdr, msgKey, childLevel, callLevel + 1,
+ keyToSkip, viewIndex, pNumListed);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return rv;
+}
+
+nsresult nsMsgQuickSearchDBView::ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey,
+ uint32_t level,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed) {
+ nsresult rv = ListIdsInThreadOrder(threadHdr, parentKey, level, level,
+ nsMsgKey_None, viewIndex, pNumListed);
+ // Because a quick search view might not have the actual thread root
+ // as its root, and thus might have a message that potentially has siblings
+ // as its root, and the enumerator will miss the siblings, we might need to
+ // make a pass looking for the siblings of the non-root root. We'll put
+ // those after the potential children of the root. So we will list the
+ // children of the faux root's parent, ignoring the faux root.
+ if (level == 1) {
+ nsCOMPtr<nsIMsgDBHdr> root;
+ nsCOMPtr<nsIMsgDBHdr> rootParent;
+ nsMsgKey rootKey;
+ threadHdr->GetRootHdr(getter_AddRefs(rootParent));
+ if (rootParent) {
+ rootParent->GetMessageKey(&rootKey);
+ if (rootKey != parentKey)
+ rv = ListIdsInThreadOrder(threadHdr, rootKey, level, level, parentKey,
+ viewIndex, pNumListed);
+ }
+ }
+ return rv;
+}
+
+nsresult nsMsgQuickSearchDBView::ExpansionDelta(nsMsgViewIndex index,
+ int32_t* expansionDelta) {
+ *expansionDelta = 0;
+ if (index >= ((nsMsgViewIndex)m_keys.Length()))
+ return NS_MSG_MESSAGE_NOT_FOUND;
+
+ char flags = m_flags[index];
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) return NS_OK;
+
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ nsresult rv = GetThreadContainingIndex(index, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsMsgKey rootKey;
+ GetMsgHdrForViewIndex(index, getter_AddRefs(rootHdr));
+ rootHdr->GetMessageKey(&rootKey);
+ // group threads can have the root key twice, one for the dummy row.
+ bool rootKeySkipped = false;
+ for (uint32_t i = 0; i < numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ if (msgKey != rootKey || (GroupViewUsesDummyRow() && rootKeySkipped)) {
+ // if this hdr is in the original view, add it to new view.
+ if (m_origKeys.BinaryIndexOf(msgKey) != m_origKeys.NoIndex)
+ (*expansionDelta)++;
+ } else {
+ rootKeySkipped = true;
+ }
+ }
+ }
+ if (!(flags & nsMsgMessageFlags::Elided))
+ *expansionDelta = -(*expansionDelta);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) {
+ if (aViewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OpenWithHdrs(aHeaders, aSortType, aSortOrder,
+ aViewFlags, aCount);
+
+ m_sortType = aSortType;
+ m_sortOrder = aSortOrder;
+ m_viewFlags = aViewFlags;
+
+ bool hasMore;
+ nsCOMPtr<nsISupports> supports;
+ nsresult rv = NS_OK;
+ while (NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = aHeaders->GetNext(getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ AddHdr(msgHdr);
+ } else {
+ break;
+ }
+ }
+ *aCount = m_keys.Length();
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::SetViewFlags(
+ nsMsgViewFlagsTypeValue aViewFlags) {
+ nsresult rv = NS_OK;
+ // if the grouping has changed, rebuild the view
+ if ((m_viewFlags & nsMsgViewFlagsType::kGroupBySort) ^
+ (aViewFlags & nsMsgViewFlagsType::kGroupBySort))
+ rv = RebuildView(aViewFlags);
+ nsMsgDBView::SetViewFlags(aViewFlags);
+
+ return rv;
+}
+
+nsresult nsMsgQuickSearchDBView::GetMessageEnumerator(
+ nsIMsgEnumerator** enumerator) {
+ return GetViewEnumerator(enumerator);
+}
+
+NS_IMETHODIMP
+nsMsgQuickSearchDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted,
+ nsMsgKey aParentKey, int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ NS_ENSURE_ARG_POINTER(aHdrDeleted);
+ nsMsgKey msgKey;
+ aHdrDeleted->GetMessageKey(&msgKey);
+ size_t keyIndex = m_origKeys.BinaryIndexOf(msgKey);
+ if (keyIndex != m_origKeys.NoIndex) m_origKeys.RemoveElementAt(keyIndex);
+ return nsMsgThreadedDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags,
+ aInstigator);
+}
+
+NS_IMETHODIMP nsMsgQuickSearchDBView::GetNumMsgsInView(int32_t* aNumMsgs) {
+ NS_ENSURE_ARG_POINTER(aNumMsgs);
+ *aNumMsgs = m_origKeys.Length();
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgQuickSearchDBView.h b/comm/mailnews/base/src/nsMsgQuickSearchDBView.h
new file mode 100644
index 0000000000..da10a3d9e1
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgQuickSearchDBView.h
@@ -0,0 +1,99 @@
+/* -*- 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/. */
+
+#ifndef _nsMsgQuickSearchDBView_H_
+#define _nsMsgQuickSearchDBView_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgThreadedDBView.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsIMsgSearchSession.h"
+#include "nsCOMArray.h"
+#include "nsIMsgHdr.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsMsgQuickSearchDBView : public nsMsgThreadedDBView,
+ public nsIMsgSearchNotify {
+ public:
+ nsMsgQuickSearchDBView();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMSGSEARCHNOTIFY
+
+ virtual const char* GetViewName(void) override { return "QuickSearchView"; }
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCommandUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) override;
+ NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue aCommand) override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) override;
+ NS_IMETHOD SetSearchSession(nsIMsgSearchSession* aSearchSession) override;
+ NS_IMETHOD GetSearchSession(nsIMsgSearchSession** aSearchSession) override;
+ NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange,
+ const nsACString& property, bool aPreChange,
+ uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override;
+
+ protected:
+ virtual ~nsMsgQuickSearchDBView();
+ nsWeakPtr m_searchSession;
+ nsTArray<nsMsgKey> m_origKeys;
+ bool m_usingCachedHits;
+ bool m_cacheEmpty;
+ nsCOMArray<nsIMsgDBHdr> m_hdrHits;
+ virtual nsresult AddHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex* resultIndex = nullptr) override;
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool ensureListed) override;
+ virtual nsresult DeleteMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) override;
+ virtual nsresult SortThreads(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) override;
+ virtual nsresult GetFirstMessageHdrToDisplayInThread(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr** result) override;
+ virtual nsresult ExpansionDelta(nsMsgViewIndex index,
+ int32_t* expansionDelta) override;
+ virtual nsresult ListCollapsedChildren(
+ nsMsgViewIndex viewIndex,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) override;
+ virtual nsresult ListIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) override;
+ virtual nsresult ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey, uint32_t level,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed) override;
+ virtual nsresult ListIdsInThreadOrder(nsIMsgThread* threadHdr,
+ nsMsgKey parentKey, uint32_t level,
+ uint32_t callLevel, nsMsgKey keyToSkip,
+ nsMsgViewIndex* viewIndex,
+ uint32_t* pNumListed);
+ virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator) override;
+ void SavePreSearchInfo();
+ void ClearPreSearchInfo();
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgReadStateTxn.cpp b/comm/mailnews/base/src/nsMsgReadStateTxn.cpp
new file mode 100644
index 0000000000..80cd0d0bd1
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgReadStateTxn.cpp
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgReadStateTxn.h"
+
+#include "nsIMsgHdr.h"
+#include "nsComponentManagerUtils.h"
+
+nsMsgReadStateTxn::nsMsgReadStateTxn() {}
+
+nsMsgReadStateTxn::~nsMsgReadStateTxn() {}
+
+nsresult nsMsgReadStateTxn::Init(nsIMsgFolder* aParentFolder, uint32_t aNumKeys,
+ nsMsgKey* aMsgKeyArray) {
+ NS_ENSURE_ARG_POINTER(aParentFolder);
+
+ mParentFolder = aParentFolder;
+ mMarkedMessages.AppendElements(aMsgKeyArray, aNumKeys);
+
+ return nsMsgTxn::Init();
+}
+
+NS_IMETHODIMP
+nsMsgReadStateTxn::UndoTransaction() { return MarkMessages(false); }
+
+NS_IMETHODIMP
+nsMsgReadStateTxn::RedoTransaction() { return MarkMessages(true); }
+
+NS_IMETHODIMP
+nsMsgReadStateTxn::MarkMessages(bool aAsRead) {
+ nsTArray<RefPtr<nsIMsgDBHdr>> messages(mMarkedMessages.Length());
+ for (auto msgKey : mMarkedMessages) {
+ nsCOMPtr<nsIMsgDBHdr> curMsgHdr;
+ nsresult rv =
+ mParentFolder->GetMessageHeader(msgKey, getter_AddRefs(curMsgHdr));
+ if (NS_SUCCEEDED(rv) && curMsgHdr) {
+ messages.AppendElement(curMsgHdr);
+ }
+ }
+ return mParentFolder->MarkMessagesRead(messages, aAsRead);
+}
diff --git a/comm/mailnews/base/src/nsMsgReadStateTxn.h b/comm/mailnews/base/src/nsMsgReadStateTxn.h
new file mode 100644
index 0000000000..07bd4c8670
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgReadStateTxn.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMsgBaseUndoTxn_h_
+#define nsMsgBaseUndoTxn_h_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgTxn.h"
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+#include "MailNewsTypes.h"
+#include "nsIMsgFolder.h"
+
+#define NS_MSGREADSTATETXN_IID \
+ { /* 121FCE4A-3EA1-455C-8161-839E1557D0CF */ \
+ 0x121FCE4A, 0x3EA1, 0x455C, { \
+ 0x81, 0x61, 0x83, 0x9E, 0x15, 0x57, 0xD0, 0xCF \
+ } \
+ }
+
+//------------------------------------------------------------------------------
+// A mark-all transaction handler. Helper for redo/undo of message read states.
+//------------------------------------------------------------------------------
+class nsMsgReadStateTxn : public nsMsgTxn {
+ public:
+ nsMsgReadStateTxn();
+ virtual ~nsMsgReadStateTxn();
+
+ nsresult Init(nsIMsgFolder* aParentFolder, uint32_t aNumKeys,
+ nsMsgKey* aMsgKeyArray);
+ NS_IMETHOD UndoTransaction() override;
+ NS_IMETHOD RedoTransaction() override;
+
+ protected:
+ NS_IMETHOD MarkMessages(bool aAsRead);
+
+ private:
+ nsCOMPtr<nsIMsgFolder> mParentFolder;
+ nsTArray<nsMsgKey> mMarkedMessages;
+};
+
+#endif // nsMsgBaseUndoTxn_h_
diff --git a/comm/mailnews/base/src/nsMsgSearchDBView.cpp b/comm/mailnews/base/src/nsMsgSearchDBView.cpp
new file mode 100644
index 0000000000..f036d0d304
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgSearchDBView.cpp
@@ -0,0 +1,1344 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgSearchDBView.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+#include "nsQuickSort.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgCopyService.h"
+#include "nsMsgUtils.h"
+#include "nsTreeColumns.h"
+#include "nsIMsgMessageService.h"
+#include "nsMsgGroupThread.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgSearchSession.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+
+static bool gReferenceOnlyThreading;
+
+nsMsgSearchDBView::nsMsgSearchDBView() {
+ // Don't try to display messages for the search pane.
+ mSuppressMsgDisplay = true;
+ m_totalMessagesInView = 0;
+ m_nextThreadId = 1;
+ mCurIndex = 0;
+ mTotalIndices = 0;
+ mCommand = -1;
+}
+
+nsMsgSearchDBView::~nsMsgSearchDBView() {}
+
+NS_IMPL_ISUPPORTS_INHERITED(nsMsgSearchDBView, nsMsgDBView, nsIMsgDBView,
+ nsIMsgCopyServiceListener, nsIMsgSearchNotify)
+
+NS_IMETHODIMP
+nsMsgSearchDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) {
+ // DBViewWrapper.jsm likes to create search views with a sort order
+ // of byNone, in order to have the order be the order the search results
+ // are returned. But this doesn't work with threaded view, so make the
+ // sort order be byDate if we're threaded.
+
+ if (viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ sortType == nsMsgViewSortType::byNone)
+ sortType = nsMsgViewSortType::byDate;
+
+ nsresult rv =
+ nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ prefBranch->GetBoolPref("mail.strict_threading", &gReferenceOnlyThreading);
+
+ // Our sort is automatically valid because we have no contents at this point!
+ m_sortValid = true;
+
+ if (pCount) *pCount = 0;
+
+ m_folder = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgSearchDBView* newMsgDBView = new nsMsgSearchDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ nsMsgGroupView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow,
+ aCmdUpdater);
+ nsMsgSearchDBView* newMsgDBView = (nsMsgSearchDBView*)aNewMsgDBView;
+
+ // Now copy all of our private member data.
+ newMsgDBView->mDestFolder = mDestFolder;
+ newMsgDBView->mCommand = mCommand;
+ newMsgDBView->mTotalIndices = mTotalIndices;
+ newMsgDBView->mCurIndex = mCurIndex;
+ newMsgDBView->m_folders.InsertObjectsAt(m_folders, 0);
+ newMsgDBView->m_curCustomColumn = m_curCustomColumn;
+ for (auto const& hdrs : m_hdrsForEachFolder) {
+ newMsgDBView->m_hdrsForEachFolder.AppendElement(hdrs.Clone());
+ }
+ newMsgDBView->m_uniqueFoldersSelected.InsertObjectsAt(m_uniqueFoldersSelected,
+ 0);
+
+ int32_t count = m_dbToUseList.Count();
+ for (int32_t i = 0; i < count; i++) {
+ newMsgDBView->m_dbToUseList.AppendObject(m_dbToUseList[i]);
+ // Register the new view with the database so it gets notifications.
+ m_dbToUseList[i]->AddListener(newMsgDBView);
+ }
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ // We need to clone the thread and msg hdr hash tables.
+ for (auto iter = m_threadsTable.Iter(); !iter.Done(); iter.Next()) {
+ newMsgDBView->m_threadsTable.InsertOrUpdate(iter.Key(), iter.UserData());
+ }
+ for (auto iter = m_hdrsTable.Iter(); !iter.Done(); iter.Next()) {
+ newMsgDBView->m_hdrsTable.InsertOrUpdate(iter.Key(), iter.UserData());
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::Close() {
+ int32_t count = m_dbToUseList.Count();
+
+ for (int32_t i = 0; i < count; i++) m_dbToUseList[i]->RemoveListener(this);
+
+ m_dbToUseList.Clear();
+
+ return nsMsgGroupView::Close();
+}
+
+void nsMsgSearchDBView::InternalClose() {
+ m_threadsTable.Clear();
+ m_hdrsTable.Clear();
+ nsMsgGroupView::InternalClose();
+ m_folders.Clear();
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetCellText(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aValue) {
+ NS_ENSURE_TRUE(IsValidIndex(aRow), NS_MSG_INVALID_DBVIEW_INDEX);
+ NS_ENSURE_ARG_POINTER(aCol);
+
+ const nsAString& colID = aCol->GetId();
+ // The only thing we contribute is location; dummy rows have no location, so
+ // bail in that case. Otherwise, check if we are dealing with 'location'.
+ // 'location', need to check for "lo" not just "l" to avoid "label" column.
+ if (!(m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) && colID.Length() >= 2 &&
+ colID.First() == 'l' && colID.CharAt(1) == 'o')
+ return FetchLocation(aRow, aValue);
+ else
+ return nsMsgGroupView::GetCellText(aRow, aCol, aValue);
+}
+
+nsresult nsMsgSearchDBView::HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey) {
+ if (m_sortType == nsMsgViewSortType::byLocation) {
+ aHashKey.Truncate();
+ nsCOMPtr<nsIMsgFolder> folder;
+ msgHdr->GetFolder(getter_AddRefs(folder));
+ return folder->GetPrettyName(aHashKey);
+ }
+
+ return nsMsgGroupView::HashHdr(msgHdr, aHashKey);
+}
+
+nsresult nsMsgSearchDBView::FetchLocation(int32_t aRow,
+ nsAString& aLocationString) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ nsresult rv = GetFolderForViewIndex(aRow, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return folder->GetPrettyName(aLocationString);
+}
+
+nsresult nsMsgSearchDBView::OnNewHeader(nsIMsgDBHdr* newHdr,
+ nsMsgKey aParentKey,
+ bool /*ensureListed*/) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags,
+ aInstigator);
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsMsgViewIndex deletedIndex = FindHdr(aHdrDeleted);
+ uint32_t savedFlags = 0;
+ if (deletedIndex != nsMsgViewIndex_None) {
+ // Check if this message is currently selected. If it is, tell the front
+ // end to be prepared for a delete.
+ nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater(
+ do_QueryReferent(mCommandUpdater));
+ bool isMsgSelected = false;
+ if (mTreeSelection && commandUpdater) {
+ mTreeSelection->IsSelected(deletedIndex, &isMsgSelected);
+ if (isMsgSelected) commandUpdater->UpdateNextMessageAfterDelete();
+ }
+
+ savedFlags = m_flags[deletedIndex];
+ RemoveByIndex(deletedIndex);
+
+ if (isMsgSelected) {
+ // Now tell the front end that the delete happened.
+ commandUpdater->SelectedMessageRemoved();
+ }
+ }
+
+ nsCOMPtr<nsIMsgThread> thread;
+ GetXFThreadFromMsgHdr(aHdrDeleted, getter_AddRefs(thread));
+ if (thread) {
+ nsMsgXFViewThread* viewThread =
+ static_cast<nsMsgXFViewThread*>(thread.get());
+ viewThread->RemoveChildHdr(aHdrDeleted, nullptr);
+ if (deletedIndex == nsMsgViewIndex_None && viewThread->MsgCount() == 1) {
+ // Remove the last child of a collapsed thread. Need to find the root,
+ // and remove the thread flags on it.
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ thread->GetRootHdr(getter_AddRefs(rootHdr));
+ if (rootHdr) {
+ nsMsgViewIndex threadIndex = GetThreadRootIndex(rootHdr);
+ if (IsValidIndex(threadIndex))
+ AndExtraFlag(threadIndex,
+ ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN));
+ }
+ } else if (savedFlags & MSG_VIEW_FLAG_HASCHILDREN) {
+ if (savedFlags & nsMsgMessageFlags::Elided) {
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ nsresult rv = thread->GetRootHdr(getter_AddRefs(rootHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ rootHdr->GetMessageKey(&msgKey);
+ rootHdr->GetFlags(&msgFlags);
+ // Promote the new thread root.
+ if (viewThread->MsgCount() > 1)
+ msgFlags |= MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN;
+ InsertMsgHdrAt(deletedIndex, rootHdr, msgKey, msgFlags, 0);
+ if (!m_deletingRows)
+ NoteChange(deletedIndex, 1,
+ nsMsgViewNotificationCode::insertOrDelete);
+ } else if (viewThread->MsgCount() > 1) {
+ OrExtraFlag(deletedIndex,
+ MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN);
+ }
+ }
+ }
+ } else {
+ return nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags,
+ aInstigator);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged,
+ uint32_t aOldFlags, uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) {
+ // Defer to base class if we're grouped or not threaded at all.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort ||
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ return nsMsgGroupView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags,
+ aInstigator);
+ }
+
+ nsCOMPtr<nsIMsgThread> thread;
+ bool foundMessageId;
+ // Check if the hdr that changed is in a xf thread, and if the read flag
+ // changed, update the thread unread count. GetXFThreadFromMsgHdr returns
+ // the thread the header does or would belong to, so we need to also
+ // check that the header is actually in the thread.
+ GetXFThreadFromMsgHdr(aHdrChanged, getter_AddRefs(thread), &foundMessageId);
+ if (foundMessageId) {
+ nsMsgXFViewThread* viewThread =
+ static_cast<nsMsgXFViewThread*>(thread.get());
+ if (viewThread->HdrIndex(aHdrChanged) != -1) {
+ uint32_t deltaFlags = (aOldFlags ^ aNewFlags);
+ if (deltaFlags & nsMsgMessageFlags::Read)
+ thread->MarkChildRead(aNewFlags & nsMsgMessageFlags::Read);
+ }
+ }
+
+ return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags,
+ aInstigator);
+}
+
+void nsMsgSearchDBView::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) {
+ if ((int32_t)index < 0) {
+ NS_ERROR("invalid insert index");
+ index = 0;
+ level = 0;
+ } else if (index > m_keys.Length()) {
+ NS_ERROR("inserting past end of array");
+ index = m_keys.Length();
+ }
+
+ m_keys.InsertElementAt(index, msgKey);
+ m_flags.InsertElementAt(index, flags);
+ m_levels.InsertElementAt(index, level);
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ m_folders.InsertObjectAt(folder, index);
+}
+
+void nsMsgSearchDBView::SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) {
+ m_keys[index] = msgKey;
+ m_flags[index] = flags;
+ m_levels[index] = level;
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetFolder(getter_AddRefs(folder));
+ m_folders.ReplaceObjectAt(folder, index);
+}
+
+void nsMsgSearchDBView::InsertEmptyRows(nsMsgViewIndex viewIndex,
+ int32_t numRows) {
+ for (int32_t i = 0; i < numRows; i++) {
+ m_folders.InsertObjectAt(nullptr, viewIndex + i);
+ }
+
+ return nsMsgDBView::InsertEmptyRows(viewIndex, numRows);
+}
+
+void nsMsgSearchDBView::RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) {
+ nsMsgDBView::RemoveRows(viewIndex, numRows);
+ for (int32_t i = 0; i < numRows; i++) m_folders.RemoveObjectAt(viewIndex);
+}
+
+nsresult nsMsgSearchDBView::GetMsgHdrForViewIndex(nsMsgViewIndex index,
+ nsIMsgDBHdr** msgHdr) {
+ if (index == nsMsgViewIndex_None || index >= (uint32_t)m_folders.Count()) {
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+ }
+
+ nsIMsgFolder* folder = m_folders[index];
+ if (folder) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = folder->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (db) {
+ return db->GetMsgHdrForKey(m_keys[index], msgHdr);
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetFolderForViewIndex(nsMsgViewIndex index,
+ nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ if (index == nsMsgViewIndex_None || index >= (uint32_t)m_folders.Count())
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ NS_IF_ADDREF(*aFolder = m_folders[index]);
+ return *aFolder ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+nsresult nsMsgSearchDBView::GetDBForViewIndex(nsMsgViewIndex index,
+ nsIMsgDatabase** db) {
+ nsCOMPtr<nsIMsgFolder> aFolder;
+ nsresult rv = GetFolderForViewIndex(index, getter_AddRefs(aFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return aFolder->GetMsgDatabase(db);
+}
+
+nsresult nsMsgSearchDBView::AddHdrFromFolder(nsIMsgDBHdr* msgHdr,
+ nsIMsgFolder* folder) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OnNewHeader(msgHdr, nsMsgKey_None, true);
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsCOMPtr<nsIMsgThread> thread;
+ nsCOMPtr<nsIMsgDBHdr> threadRoot;
+ // If we find an xf thread in the hash table corresponding to the new msg's
+ // message id, a previous header must be a reference child of the new
+ // message, which means we need to reparent later.
+ bool msgIsReferredTo;
+ GetXFThreadFromMsgHdr(msgHdr, getter_AddRefs(thread), &msgIsReferredTo);
+ bool newThread = !thread;
+ nsMsgXFViewThread* viewThread;
+ if (!thread) {
+ viewThread = new nsMsgXFViewThread(this, m_nextThreadId++);
+ if (!viewThread) return NS_ERROR_OUT_OF_MEMORY;
+
+ thread = viewThread;
+ } else {
+ viewThread = static_cast<nsMsgXFViewThread*>(thread.get());
+ thread->GetChildHdrAt(0, getter_AddRefs(threadRoot));
+ }
+
+ AddMsgToHashTables(msgHdr, thread);
+ nsCOMPtr<nsIMsgDBHdr> parent;
+ uint32_t posInThread;
+ // We need to move threads in order to keep ourselves sorted
+ // correctly. We want the index of the original thread...we can do this by
+ // getting the root header before we add the new header, and finding that.
+ if (newThread || !viewThread->MsgCount()) {
+ viewThread->AddHdr(msgHdr, false, posInThread, getter_AddRefs(parent));
+ nsMsgViewIndex insertIndex = GetIndexForThread(msgHdr);
+ NS_ASSERTION(insertIndex == m_levels.Length() ||
+ (IsValidIndex(insertIndex) && !m_levels[insertIndex]),
+ "inserting into middle of thread");
+ if (insertIndex == nsMsgViewIndex_None)
+ return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kExpandAll))
+ msgFlags |= nsMsgMessageFlags::Elided;
+
+ InsertMsgHdrAt(insertIndex, msgHdr, msgKey, msgFlags, 0);
+ NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ } else {
+ // Get the thread root index before we add the header, because adding
+ // the header can change the sort position.
+ nsMsgViewIndex threadIndex = GetThreadRootIndex(threadRoot);
+ viewThread->AddHdr(msgHdr, msgIsReferredTo, posInThread,
+ getter_AddRefs(parent));
+ if (!IsValidIndex(threadIndex)) {
+ NS_ERROR("couldn't find thread index for newly inserted header");
+ // Not really OK, but not failure exactly.
+ return NS_OK;
+ }
+
+ NS_ASSERTION(!m_levels[threadIndex],
+ "threadRoot incorrect, or level incorrect");
+
+ bool moveThread = false;
+ if (m_sortType == nsMsgViewSortType::byDate) {
+ uint32_t newestMsgInThread = 0, msgDate = 0;
+ viewThread->GetNewestMsgDate(&newestMsgInThread);
+ msgHdr->GetDateInSeconds(&msgDate);
+ moveThread = (msgDate == newestMsgInThread);
+ }
+
+ OrExtraFlag(threadIndex,
+ MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD);
+ if (!(m_flags[threadIndex] & nsMsgMessageFlags::Elided)) {
+ if (parent) {
+ // Since we know posInThread, we just want to insert the new hdr
+ // at threadIndex + posInThread, and then rebuild the view until we
+ // get to a sibling of the new hdr.
+ uint8_t newMsgLevel = viewThread->ChildLevelAt(posInThread);
+ InsertMsgHdrAt(threadIndex + posInThread, msgHdr, msgKey, msgFlags,
+ newMsgLevel);
+
+ NoteChange(threadIndex + posInThread, 1,
+ nsMsgViewNotificationCode::insertOrDelete);
+ for (nsMsgViewIndex viewIndex = threadIndex + ++posInThread;
+ posInThread < viewThread->MsgCount() &&
+ viewThread->ChildLevelAt(posInThread) > newMsgLevel;
+ viewIndex++) {
+ m_levels[viewIndex] = viewThread->ChildLevelAt(posInThread++);
+ }
+
+ } else {
+ // The new header is the root, so we need to adjust all the children.
+ InsertMsgHdrAt(threadIndex, msgHdr, msgKey, msgFlags, 0);
+
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ nsMsgViewIndex i;
+ for (i = threadIndex + 1;
+ i < m_keys.Length() && (i == threadIndex + 1 || m_levels[i]);
+ i++)
+ m_levels[i] = m_levels[i] + 1;
+ // Turn off thread flags on old root.
+ AndExtraFlag(threadIndex + 1,
+ ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN));
+
+ NoteChange(threadIndex + 1, i - threadIndex + 1,
+ nsMsgViewNotificationCode::changed);
+ }
+ } else if (!parent) {
+ // New parent came into collapsed thread.
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ msgHdr->GetFolder(getter_AddRefs(msgFolder));
+ m_keys[threadIndex] = msgKey;
+ m_folders.ReplaceObjectAt(msgFolder, threadIndex);
+ m_flags[threadIndex] = msgFlags | MSG_VIEW_FLAG_ISTHREAD |
+ nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN;
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+
+ if (moveThread) MoveThreadAt(threadIndex);
+ }
+ } else {
+ m_folders.AppendObject(folder);
+ // nsMsgKey_None means it's not a valid hdr.
+ if (msgKey != nsMsgKey_None) {
+ msgHdr->GetFlags(&msgFlags);
+ m_keys.AppendElement(msgKey);
+ m_levels.AppendElement(0);
+ m_flags.AppendElement(msgFlags);
+ NoteChange(GetSize() - 1, 1, nsMsgViewNotificationCode::insertOrDelete);
+ }
+ }
+
+ return NS_OK;
+}
+
+// This method removes the thread at threadIndex from the view
+// and puts it back in its new position, determined by the sort order.
+// And, if the selection is affected, save and restore the selection.
+void nsMsgSearchDBView::MoveThreadAt(nsMsgViewIndex threadIndex) {
+ bool updatesSuppressed = mSuppressChangeNotification;
+ // Turn off tree notifications so that we don't reload the current message.
+ if (!updatesSuppressed) SetSuppressChangeNotifications(true);
+
+ nsCOMPtr<nsIMsgDBHdr> threadHdr;
+ GetMsgHdrForViewIndex(threadIndex, getter_AddRefs(threadHdr));
+
+ uint32_t saveFlags = m_flags[threadIndex];
+ bool threadIsExpanded = !(saveFlags & nsMsgMessageFlags::Elided);
+ int32_t childCount = 0;
+ nsMsgKey preservedKey;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ int32_t selectionCount;
+ int32_t currentIndex;
+ bool hasSelection =
+ mTreeSelection &&
+ ((NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(&currentIndex)) &&
+ currentIndex >= 0 && (uint32_t)currentIndex < GetSize()) ||
+ (NS_SUCCEEDED(mTreeSelection->GetRangeCount(&selectionCount)) &&
+ selectionCount > 0));
+ if (hasSelection) SaveAndClearSelection(&preservedKey, preservedSelection);
+
+ if (threadIsExpanded) {
+ ExpansionDelta(threadIndex, &childCount);
+ childCount = -childCount;
+ }
+
+ nsTArray<nsMsgKey> threadKeys;
+ nsTArray<uint32_t> threadFlags;
+ nsTArray<uint8_t> threadLevels;
+ nsCOMArray<nsIMsgFolder> threadFolders;
+
+ if (threadIsExpanded) {
+ threadKeys.SetCapacity(childCount);
+ threadFlags.SetCapacity(childCount);
+ threadLevels.SetCapacity(childCount);
+ threadFolders.SetCapacity(childCount);
+ for (nsMsgViewIndex index = threadIndex + 1;
+ index < (nsMsgViewIndex)GetSize() && m_levels[index]; index++) {
+ threadKeys.AppendElement(m_keys[index]);
+ threadFlags.AppendElement(m_flags[index]);
+ threadLevels.AppendElement(m_levels[index]);
+ threadFolders.AppendObject(m_folders[index]);
+ }
+
+ uint32_t collapseCount;
+ CollapseByIndex(threadIndex, &collapseCount);
+ }
+
+ nsMsgDBView::RemoveByIndex(threadIndex);
+ m_folders.RemoveObjectAt(threadIndex);
+ nsMsgViewIndex newIndex = GetIndexForThread(threadHdr);
+ NS_ASSERTION(newIndex == m_levels.Length() ||
+ (IsValidIndex(newIndex) && !m_levels[newIndex]),
+ "inserting into middle of thread");
+ if (newIndex == nsMsgViewIndex_None) newIndex = 0;
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ threadHdr->GetMessageKey(&msgKey);
+ threadHdr->GetFlags(&msgFlags);
+ InsertMsgHdrAt(newIndex, threadHdr, msgKey, msgFlags, 0);
+
+ if (threadIsExpanded) {
+ m_keys.InsertElementsAt(newIndex + 1, threadKeys);
+ m_flags.InsertElementsAt(newIndex + 1, threadFlags);
+ m_levels.InsertElementsAt(newIndex + 1, threadLevels);
+ m_folders.InsertObjectsAt(threadFolders, newIndex + 1);
+ }
+
+ m_flags[newIndex] = saveFlags;
+ // Unfreeze selection.
+ if (hasSelection) RestoreSelection(preservedKey, preservedSelection);
+
+ if (!updatesSuppressed) SetSuppressChangeNotifications(false);
+
+ nsMsgViewIndex lowIndex = threadIndex < newIndex ? threadIndex : newIndex;
+ nsMsgViewIndex highIndex = lowIndex == threadIndex ? newIndex : threadIndex;
+ NoteChange(lowIndex, highIndex - lowIndex + childCount + 1,
+ nsMsgViewNotificationCode::changed);
+}
+
+nsresult nsMsgSearchDBView::GetMessageEnumerator(
+ nsIMsgEnumerator** enumerator) {
+ // We do not have an m_db, so the default behavior (in nsMsgDBView) is not
+ // what we want (it will crash). We just want someone to enumerate the
+ // headers that we already have. Conveniently, nsMsgDBView already knows
+ // how to do this with its view enumerator, so we just use that.
+ return nsMsgDBView::GetViewEnumerator(enumerator);
+}
+
+nsresult nsMsgSearchDBView::InsertHdrFromFolder(nsIMsgDBHdr* msgHdr,
+ nsIMsgFolder* folder) {
+ nsMsgViewIndex insertIndex = nsMsgViewIndex_None;
+ // Threaded view always needs to go through AddHdrFromFolder since
+ // it handles the xf view thread object creation.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ insertIndex = GetInsertIndex(msgHdr);
+
+ if (insertIndex == nsMsgViewIndex_None)
+ return AddHdrFromFolder(msgHdr, folder);
+
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ InsertMsgHdrAt(insertIndex, msgHdr, msgKey, msgFlags, 0);
+
+ // The call to NoteChange() has to happen after we add the key as
+ // NoteChange() will call RowCountChanged() which will call our GetRowCount().
+ NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnSearchHit(nsIMsgDBHdr* aMsgHdr, nsIMsgFolder* folder) {
+ NS_ENSURE_ARG(aMsgHdr);
+ NS_ENSURE_ARG(folder);
+
+ if (m_folders.IndexOf(folder) < 0)
+ // Do this just for new folder.
+ {
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(dbToUse));
+ if (dbToUse) {
+ dbToUse->AddListener(this);
+ m_dbToUseList.AppendObject(dbToUse);
+ }
+ }
+
+ m_totalMessagesInView++;
+ if (m_sortValid)
+ return InsertHdrFromFolder(aMsgHdr, folder);
+ else
+ return AddHdrFromFolder(aMsgHdr, folder);
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnSearchDone(nsresult status) {
+ // We want to set imap delete model once the search is over because setting
+ // next message after deletion will happen before deleting the message and
+ // search scope can change with every search.
+
+ // Set to default in case it is non-imap folder.
+ mDeleteModel = nsMsgImapDeleteModels::MoveToTrash;
+ nsIMsgFolder* curFolder = m_folders.SafeObjectAt(0);
+ if (curFolder) GetImapDeleteModel(curFolder);
+
+ return NS_OK;
+}
+
+// For now also acts as a way of resetting the search datasource.
+NS_IMETHODIMP
+nsMsgSearchDBView::OnNewSearch() {
+ int32_t oldSize = GetSize();
+
+ int32_t count = m_dbToUseList.Count();
+ for (int32_t j = 0; j < count; j++) m_dbToUseList[j]->RemoveListener(this);
+
+ m_dbToUseList.Clear();
+ m_folders.Clear();
+ m_keys.Clear();
+ m_levels.Clear();
+ m_flags.Clear();
+ m_totalMessagesInView = 0;
+
+ // Needs to happen after we remove the keys, since RowCountChanged() will
+ // call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -oldSize);
+
+ // mSearchResults->Clear();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowSearch;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::SetSearchSession(nsIMsgSearchSession* aSession) {
+ m_searchSession = do_GetWeakReference(aSession);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::OnAnnouncerGoingAway(
+ nsIDBChangeAnnouncer* instigator) {
+ nsIMsgDatabase* db = static_cast<nsIMsgDatabase*>(instigator);
+ if (db) {
+ db->RemoveListener(this);
+ m_dbToUseList.RemoveObject(db);
+ }
+
+ return NS_OK;
+}
+
+nsCOMArray<nsIMsgFolder>* nsMsgSearchDBView::GetFolders() { return &m_folders; }
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetCommandStatus(
+ nsMsgViewCommandTypeValue command, bool* selectable_p,
+ nsMsgViewCommandCheckStateValue* selected_p) {
+ if (command != nsMsgViewCommandType::runJunkControls)
+ return nsMsgDBView::GetCommandStatus(command, selectable_p, selected_p);
+
+ *selectable_p = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::DoCommandWithFolder(nsMsgViewCommandTypeValue command,
+ nsIMsgFolder* destFolder) {
+ mCommand = command;
+ mDestFolder = destFolder;
+ return nsMsgDBView::DoCommandWithFolder(command, destFolder);
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::DoCommand(nsMsgViewCommandTypeValue command) {
+ mCommand = command;
+ if (command == nsMsgViewCommandType::deleteMsg ||
+ command == nsMsgViewCommandType::deleteNoTrash ||
+ command == nsMsgViewCommandType::selectAll ||
+ command == nsMsgViewCommandType::selectThread ||
+ command == nsMsgViewCommandType::selectFlagged ||
+ command == nsMsgViewCommandType::expandAll ||
+ command == nsMsgViewCommandType::collapseAll)
+ return nsMsgDBView::DoCommand(command);
+
+ nsresult rv = NS_OK;
+ nsMsgViewIndexArray selection;
+ GetIndicesForSelection(selection);
+
+ // We need to break apart the selection by folders, and then call
+ // ApplyCommandToIndices with the command and the indices in the
+ // selection that are from that folder.
+
+ mozilla::UniquePtr<nsTArray<nsMsgViewIndex>[]> indexArrays;
+ int32_t numArrays;
+ rv = PartitionSelectionByFolder(selection, indexArrays, &numArrays);
+ NS_ENSURE_SUCCESS(rv, rv);
+ for (int32_t folderIndex = 0; folderIndex < numArrays; folderIndex++) {
+ rv = ApplyCommandToIndices(command, (indexArrays.get())[folderIndex]);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return rv;
+}
+
+// This method removes the specified line from the view, and adjusts the
+// various flags and levels of affected messages.
+nsresult nsMsgSearchDBView::RemoveByIndex(nsMsgViewIndex index) {
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> thread;
+ nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ GetXFThreadFromMsgHdr(msgHdr, getter_AddRefs(thread));
+ if (thread) {
+ nsMsgXFViewThread* viewThread =
+ static_cast<nsMsgXFViewThread*>(thread.get());
+ if (viewThread->MsgCount() == 2) {
+ // If we removed the next to last message in the thread,
+ // we need to adjust the flags on the first message in the thread.
+ nsMsgViewIndex threadIndex = m_levels[index] ? index - 1 : index;
+ if (threadIndex != nsMsgViewIndex_None) {
+ AndExtraFlag(threadIndex,
+ ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN));
+ m_levels[threadIndex] = 0;
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+
+ // Bump up the level of all the descendents of the message
+ // that was removed, if the thread was expanded.
+ uint8_t removedLevel = m_levels[index];
+ nsMsgViewIndex i = index + 1;
+ if (i < m_levels.Length() && m_levels[i] > removedLevel) {
+ // Promote the child of the removed message.
+ uint8_t promotedLevel = m_levels[i];
+ m_levels[i] = promotedLevel - 1;
+ i++;
+ // Now promote all the children of the promoted message.
+ for (; i < m_levels.Length() && m_levels[i] > promotedLevel; i++)
+ m_levels[i] = m_levels[i] - 1;
+ }
+ }
+ }
+
+ m_folders.RemoveObjectAt(index);
+ return nsMsgDBView::RemoveByIndex(index);
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::ApplyCommandToIndices(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection) {
+ mCommand = command;
+ return nsMsgDBView::ApplyCommandToIndices(command, selection);
+}
+
+nsresult nsMsgSearchDBView::DeleteMessages(
+ nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) {
+ nsresult rv = GetFoldersAndHdrsForSelection(selection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (mDeleteModel != nsMsgImapDeleteModels::MoveToTrash) deleteStorage = true;
+
+ if (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete) m_deletingRows = true;
+
+ // Remember the deleted messages in case the user undoes the delete,
+ // and we want to restore the hdr to the view, even if it no
+ // longer matches the search criteria.
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ (void)GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ RememberDeletedMsgHdr(msgHdr);
+ }
+
+ // If we are deleting rows, save off the view indices.
+ if (m_deletingRows) {
+ mIndicesToNoteChange.AppendElement(viewIndex);
+ }
+ }
+ rv = deleteStorage ? ProcessRequestsInAllFolders(window)
+ : ProcessRequestsInOneFolder(window);
+ if (NS_FAILED(rv)) m_deletingRows = false;
+
+ return rv;
+}
+
+nsresult nsMsgSearchDBView::CopyMessages(
+ nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection,
+ bool isMove, nsIMsgFolder* destFolder) {
+ GetFoldersAndHdrsForSelection(selection);
+ return ProcessRequestsInOneFolder(window);
+}
+
+nsresult nsMsgSearchDBView::PartitionSelectionByFolder(
+ nsTArray<nsMsgViewIndex> const& selection,
+ mozilla::UniquePtr<nsTArray<nsMsgViewIndex>[]>& indexArrays,
+ int32_t* numArrays) {
+ nsCOMArray<nsIMsgFolder> uniqueFoldersSelected;
+ nsTArray<uint32_t> numIndicesSelected;
+ mCurIndex = 0;
+
+ // Build unique folder list based on headers selected by the user.
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsIMsgFolder* curFolder = m_folders[viewIndex];
+ int32_t folderIndex = uniqueFoldersSelected.IndexOf(curFolder);
+ if (folderIndex < 0) {
+ uniqueFoldersSelected.AppendObject(curFolder);
+ numIndicesSelected.AppendElement(1);
+ } else {
+ numIndicesSelected[folderIndex]++;
+ }
+ }
+
+ int32_t numFolders = uniqueFoldersSelected.Count();
+ indexArrays = mozilla::MakeUnique<nsTArray<nsMsgViewIndex>[]>(numFolders);
+ *numArrays = numFolders;
+ NS_ENSURE_TRUE(indexArrays, NS_ERROR_OUT_OF_MEMORY);
+ for (int32_t folderIndex = 0; folderIndex < numFolders; folderIndex++) {
+ (indexArrays.get())[folderIndex].SetCapacity(
+ numIndicesSelected[folderIndex]);
+ }
+ for (nsMsgViewIndex viewIndex : selection) {
+ nsIMsgFolder* curFolder = m_folders[viewIndex];
+ int32_t folderIndex = uniqueFoldersSelected.IndexOf(curFolder);
+ (indexArrays.get())[folderIndex].AppendElement(viewIndex);
+ }
+ return NS_OK;
+}
+
+nsresult nsMsgSearchDBView::GetFoldersAndHdrsForSelection(
+ nsTArray<nsMsgViewIndex> const& selection) {
+ nsresult rv = NS_OK;
+ mCurIndex = 0;
+ m_uniqueFoldersSelected.Clear();
+ m_hdrsForEachFolder.Clear();
+
+ AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages;
+ rv = GetHeadersFromSelection(selection, messages);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Build unique folder list based on headers selected by the user.
+ for (nsIMsgDBHdr* hdr : messages) {
+ nsCOMPtr<nsIMsgFolder> curFolder;
+ hdr->GetFolder(getter_AddRefs(curFolder));
+ if (m_uniqueFoldersSelected.IndexOf(curFolder) < 0) {
+ m_uniqueFoldersSelected.AppendObject(curFolder);
+ }
+ }
+
+ // Group the headers selected by each folder.
+ uint32_t numFolders = m_uniqueFoldersSelected.Count();
+ for (uint32_t folderIndex = 0; folderIndex < numFolders; folderIndex++) {
+ nsIMsgFolder* curFolder = m_uniqueFoldersSelected[folderIndex];
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsForOneFolder;
+ for (nsIMsgDBHdr* hdr : messages) {
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ hdr->GetFolder(getter_AddRefs(msgFolder));
+ if (NS_SUCCEEDED(rv) && msgFolder && msgFolder == curFolder) {
+ msgHdrsForOneFolder.AppendElement(hdr);
+ }
+ }
+
+ m_hdrsForEachFolder.AppendElement(msgHdrsForOneFolder.Clone());
+ }
+
+ return rv;
+}
+
+nsresult nsMsgSearchDBView::ApplyCommandToIndicesWithFolder(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder) {
+ mCommand = command;
+ mDestFolder = destFolder;
+ return nsMsgDBView::ApplyCommandToIndicesWithFolder(command, selection,
+ destFolder);
+}
+
+// nsIMsgCopyServiceListener methods
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnStartCopy() { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnProgress(uint32_t aProgress, uint32_t aProgressMax) {
+ return NS_OK;
+}
+
+// Believe it or not, these next two are msgcopyservice listener methods!
+NS_IMETHODIMP
+nsMsgSearchDBView::SetMessageKey(nsMsgKey aMessageKey) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetMessageId(nsACString& messageId) { return NS_OK; }
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OnStopCopy(nsresult aStatus) {
+ if (NS_SUCCEEDED(aStatus)) {
+ mCurIndex++;
+ if ((int32_t)mCurIndex < m_uniqueFoldersSelected.Count()) {
+ nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak));
+ ProcessRequestsInOneFolder(msgWindow);
+ }
+ }
+
+ return NS_OK;
+}
+
+// End nsIMsgCopyServiceListener methods.
+
+nsresult nsMsgSearchDBView::ProcessRequestsInOneFolder(nsIMsgWindow* window) {
+ nsresult rv = NS_OK;
+
+ // Folder operations like copy/move are not implemented for .eml files.
+ if (m_uniqueFoldersSelected.Count() == 0) return NS_ERROR_NOT_IMPLEMENTED;
+
+ nsIMsgFolder* curFolder = m_uniqueFoldersSelected[mCurIndex];
+ NS_ASSERTION(curFolder, "curFolder is null");
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& msgs = m_hdrsForEachFolder[mCurIndex];
+
+ // called for delete with trash, copy and move
+ if (mCommand == nsMsgViewCommandType::deleteMsg)
+ curFolder->DeleteMessages(msgs, window, false /* delete storage */,
+ false /* is move*/, this, true /*allowUndo*/);
+ else {
+ NS_ASSERTION(!(curFolder == mDestFolder),
+ "The source folder and the destination folder are the same");
+ if (NS_SUCCEEDED(rv) && curFolder != mDestFolder) {
+ nsCOMPtr<nsIMsgCopyService> copyService =
+ do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ if (mCommand == nsMsgViewCommandType::moveMessages)
+ copyService->CopyMessages(curFolder, msgs, mDestFolder,
+ true /* isMove */, this, window,
+ true /*allowUndo*/);
+ else if (mCommand == nsMsgViewCommandType::copyMessages)
+ copyService->CopyMessages(curFolder, msgs, mDestFolder,
+ false /* isMove */, this, window,
+ true /*allowUndo*/);
+ }
+ }
+ }
+
+ return rv;
+}
+
+nsresult nsMsgSearchDBView::ProcessRequestsInAllFolders(nsIMsgWindow* window) {
+ uint32_t numFolders = m_uniqueFoldersSelected.Count();
+ for (uint32_t folderIndex = 0; folderIndex < numFolders; folderIndex++) {
+ nsIMsgFolder* curFolder = m_uniqueFoldersSelected[folderIndex];
+ NS_ASSERTION(curFolder, "curFolder is null");
+ curFolder->DeleteMessages(
+ m_hdrsForEachFolder[folderIndex], window, true /* delete storage */,
+ false /* is move*/, nullptr /*copyServListener*/, false /*allowUndo*/);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgSearchDBView::Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ if (!m_checkedCustomColumns && CustomColumnsInSortAndNotRegistered())
+ return NS_OK;
+
+ int32_t rowCountBeforeSort = GetSize();
+
+ if (!rowCountBeforeSort) return NS_OK;
+
+ if (m_viewFlags & (nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kGroupBySort)) {
+ // ### This forgets which threads were expanded, and is sub-optimal
+ // since it rebuilds the thread objects.
+ m_sortType = sortType;
+ m_sortOrder = sortOrder;
+ return RebuildView(m_viewFlags);
+ }
+
+ nsMsgKey preservedKey;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ SaveAndClearSelection(&preservedKey, preservedSelection);
+
+ nsresult rv = nsMsgDBView::Sort(sortType, sortOrder);
+ // The sort may have changed the number of rows before we restore the
+ // selection, tell the tree do this before we call restore selection.
+ // This is safe when there is no selection.
+ rv = AdjustRowCount(rowCountBeforeSort, GetSize());
+
+ RestoreSelection(preservedKey, preservedSelection);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+// If nothing selected, return an NS_ERROR.
+NS_IMETHODIMP
+nsMsgSearchDBView::GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) {
+ NS_ENSURE_ARG_POINTER(hdr);
+ nsMsgViewIndex index;
+ nsresult rv = GetViewIndexForFirstSelectedMsg(&index);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return GetMsgHdrForViewIndex(index, hdr);
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) {
+ if (aViewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OpenWithHdrs(aHeaders, aSortType, aSortOrder,
+ aViewFlags, aCount);
+
+ m_sortType = aSortType;
+ m_sortOrder = aSortOrder;
+ m_viewFlags = aViewFlags;
+ SaveSortInfo(m_sortType, m_sortOrder);
+
+ bool hasMore;
+ nsresult rv = NS_OK;
+ while (NS_SUCCEEDED(rv) &&
+ NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = aHeaders->GetNext(getter_AddRefs(msgHdr));
+ if (NS_SUCCEEDED(rv) && msgHdr) {
+ msgHdr->GetFolder(getter_AddRefs(folder));
+ AddHdrFromFolder(msgHdr, folder);
+ }
+ }
+
+ *aCount = m_keys.Length();
+ return rv;
+}
+
+nsresult nsMsgSearchDBView::GetFolderFromMsgURI(const nsACString& aMsgURI,
+ nsIMsgFolder** aFolder) {
+ nsCOMPtr<nsIMsgMessageService> msgMessageService;
+ nsresult rv =
+ GetMessageServiceFromURI(aMsgURI, getter_AddRefs(msgMessageService));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgMessageService->MessageURIToMsgHdr(aMsgURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return msgHdr->GetFolder(aFolder);
+}
+
+nsMsgViewIndex nsMsgSearchDBView::FindHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startIndex,
+ bool allowDummy) {
+ nsCOMPtr<nsIMsgDBHdr> curHdr;
+ uint32_t index;
+ // It would be nice to take advantage of sorted views when possible.
+ for (index = startIndex; index < GetSize(); index++) {
+ GetMsgHdrForViewIndex(index, getter_AddRefs(curHdr));
+ if (curHdr == msgHdr &&
+ (allowDummy || !(m_flags[index] & MSG_VIEW_FLAG_DUMMY) ||
+ (m_flags[index] & nsMsgMessageFlags::Elided)))
+ break;
+ }
+
+ return index < GetSize() ? index : nsMsgViewIndex_None;
+}
+
+// This method looks for the XF thread that corresponds to this message hdr,
+// first by looking up the message id, then references, and finally, if subject
+// threading is turned on, the subject.
+nsresult nsMsgSearchDBView::GetXFThreadFromMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread,
+ bool* foundByMessageId) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(pThread);
+
+ nsAutoCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ *pThread = nullptr;
+ m_threadsTable.Get(messageId, pThread);
+ // The caller may want to know if we found the thread by the msgHdr's
+ // messageId.
+ if (foundByMessageId) *foundByMessageId = *pThread != nullptr;
+
+ if (!*pThread) {
+ uint16_t numReferences = 0;
+ msgHdr->GetNumReferences(&numReferences);
+ for (int32_t i = numReferences - 1; i >= 0 && !*pThread; i--) {
+ nsAutoCString reference;
+ msgHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ m_threadsTable.Get(reference, pThread);
+ }
+ }
+
+ // If we're threading by subject, and we couldn't find the thread by ref,
+ // just treat subject as an other ref.
+ if (!*pThread && !gReferenceOnlyThreading) {
+ nsCString subject;
+ msgHdr->GetSubject(subject);
+ // This is the raw rfc822 subject header, so this is OK.
+ m_threadsTable.Get(subject, pThread);
+ }
+
+ return (*pThread) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+bool nsMsgSearchDBView::GetMsgHdrFromHash(nsCString& reference,
+ nsIMsgDBHdr** hdr) {
+ return m_hdrsTable.Get(reference, hdr);
+}
+
+bool nsMsgSearchDBView::GetThreadFromHash(nsCString& reference,
+ nsIMsgThread** thread) {
+ return m_threadsTable.Get(reference, thread);
+}
+
+nsresult nsMsgSearchDBView::AddRefToHash(nsCString& reference,
+ nsIMsgThread* thread) {
+ // Check if this reference is already is associated with a thread;
+ // If so, don't overwrite that association.
+ nsCOMPtr<nsIMsgThread> oldThread;
+ m_threadsTable.Get(reference, getter_AddRefs(oldThread));
+ if (oldThread) return NS_OK;
+
+ m_threadsTable.InsertOrUpdate(reference, thread);
+ return NS_OK;
+}
+
+nsresult nsMsgSearchDBView::AddMsgToHashTables(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread* thread) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+
+ uint16_t numReferences = 0;
+ nsresult rv;
+
+ msgHdr->GetNumReferences(&numReferences);
+ for (int32_t i = 0; i < numReferences; i++) {
+ nsAutoCString reference;
+ msgHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ rv = AddRefToHash(reference, thread);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ nsCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ m_hdrsTable.InsertOrUpdate(messageId, msgHdr);
+ if (!gReferenceOnlyThreading) {
+ nsCString subject;
+ msgHdr->GetSubject(subject);
+ // if we're threading by subject, just treat subject as an other ref.
+ AddRefToHash(subject, thread);
+ }
+
+ return AddRefToHash(messageId, thread);
+}
+
+nsresult nsMsgSearchDBView::RemoveRefFromHash(nsCString& reference) {
+ m_threadsTable.Remove(reference);
+ return NS_OK;
+}
+
+nsresult nsMsgSearchDBView::RemoveMsgFromHashTables(nsIMsgDBHdr* msgHdr) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+
+ uint16_t numReferences = 0;
+ nsresult rv = NS_OK;
+
+ msgHdr->GetNumReferences(&numReferences);
+
+ for (int32_t i = 0; i < numReferences; i++) {
+ nsAutoCString reference;
+ msgHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ rv = RemoveRefFromHash(reference);
+ if (NS_FAILED(rv)) break;
+ }
+
+ nsCString messageId;
+ msgHdr->GetMessageId(getter_Copies(messageId));
+ m_hdrsTable.Remove(messageId);
+ RemoveRefFromHash(messageId);
+ if (!gReferenceOnlyThreading) {
+ nsCString subject;
+ msgHdr->GetSubject(subject);
+ // If we're threading by subject, just treat subject as an other ref.
+ RemoveRefFromHash(subject);
+ }
+
+ return rv;
+}
+
+nsMsgGroupThread* nsMsgSearchDBView::CreateGroupThread(
+ nsIMsgDatabase* /* db */) {
+ return new nsMsgXFGroupThread();
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::GetThreadContainingMsgHdr(msgHdr, pThread);
+ else if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ return GetXFThreadFromMsgHdr(msgHdr, pThread);
+
+ // If not threaded, use the real thread.
+ nsCOMPtr<nsIMsgDatabase> msgDB;
+ nsresult rv = GetDBForHeader(msgHdr, getter_AddRefs(msgDB));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return msgDB->GetThreadContainingMsgHdr(msgHdr, pThread);
+}
+
+nsresult nsMsgSearchDBView::ListIdsInThread(
+ nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) {
+ NS_ENSURE_ARG_POINTER(threadHdr);
+ NS_ENSURE_ARG_POINTER(pNumListed);
+
+ // These children ids should be in thread order.
+ uint32_t i;
+ nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1;
+ *pNumListed = 0;
+
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ NS_ASSERTION(numChildren, "Empty thread in view/db");
+ if (!numChildren) return NS_OK;
+
+ // Account for the existing thread root.
+ numChildren--;
+ InsertEmptyRows(viewIndex, numChildren);
+
+ bool threadedView = m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay &&
+ !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort);
+ nsMsgXFViewThread* viewThread;
+ if (threadedView) viewThread = static_cast<nsMsgXFViewThread*>(threadHdr);
+
+ for (i = 1; i <= numChildren; i++) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr));
+
+ if (msgHdr) {
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ msgHdr->GetMessageKey(&msgKey);
+ msgHdr->GetFlags(&msgFlags);
+ uint8_t level = (threadedView) ? viewThread->ChildLevelAt(i) : 1;
+ SetMsgHdrAt(msgHdr, viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, level);
+ (*pNumListed)++;
+ viewIndex++;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgSearchDBView::GetNumMsgsInView(int32_t* aNumMsgs) {
+ NS_ENSURE_ARG_POINTER(aNumMsgs);
+ *aNumMsgs = m_totalMessagesInView;
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgSearchDBView.h b/comm/mailnews/base/src/nsMsgSearchDBView.h
new file mode 100644
index 0000000000..3dd363b58f
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgSearchDBView.h
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsMsgSearchDBViews_H_
+#define _nsMsgSearchDBViews_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgGroupView.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsMsgXFViewThread.h"
+#include "nsCOMArray.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsMsgSearchDBView : public nsMsgGroupView,
+ public nsIMsgCopyServiceListener,
+ public nsIMsgSearchNotify {
+ public:
+ nsMsgSearchDBView();
+
+ // these are tied together pretty intimately
+ friend class nsMsgXFViewThread;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMSGSEARCHNOTIFY
+ NS_DECL_NSIMSGCOPYSERVICELISTENER
+
+ NS_IMETHOD SetSearchSession(nsIMsgSearchSession* aSearchSession) override;
+
+ virtual const char* GetViewName(void) override { return "SearchView"; }
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) override;
+ NS_IMETHOD Close() override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) override;
+ NS_IMETHOD GetCommandStatus(
+ nsMsgViewCommandTypeValue command, bool* selectable_p,
+ nsMsgViewCommandCheckStateValue* selected_p) override;
+ NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue command) override;
+ NS_IMETHOD DoCommandWithFolder(nsMsgViewCommandTypeValue command,
+ nsIMsgFolder* destFolder) override;
+ NS_IMETHOD GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) override;
+ NS_IMETHOD OpenWithHdrs(nsIMsgEnumerator* aHeaders,
+ nsMsgViewSortTypeValue aSortType,
+ nsMsgViewSortOrderValue aSortOrder,
+ nsMsgViewFlagsTypeValue aViewFlags,
+ int32_t* aCount) override;
+ NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey,
+ int32_t aFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags,
+ uint32_t aNewFlags,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override;
+ // override to get location
+ NS_IMETHOD GetCellText(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aValue) override;
+ virtual nsresult GetMsgHdrForViewIndex(nsMsgViewIndex index,
+ nsIMsgDBHdr** msgHdr) override;
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey parentKey,
+ bool ensureListed) override;
+ NS_IMETHOD GetFolderForViewIndex(nsMsgViewIndex index,
+ nsIMsgFolder** folder) override;
+
+ NS_IMETHOD OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) override;
+
+ virtual nsCOMArray<nsIMsgFolder>* GetFolders() override;
+ virtual nsresult GetFolderFromMsgURI(const nsACString& aMsgURI,
+ nsIMsgFolder** aFolder) override;
+
+ NS_IMETHOD GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr,
+ nsIMsgThread** pThread) override;
+
+ NS_IMETHOD ApplyCommandToIndices(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection) override;
+
+ protected:
+ virtual ~nsMsgSearchDBView();
+ virtual void InternalClose() override;
+ virtual nsresult HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey) override;
+ virtual nsresult ListIdsInThread(nsIMsgThread* threadHdr,
+ nsMsgViewIndex startOfThreadViewIndex,
+ uint32_t* pNumListed) override;
+ nsresult FetchLocation(int32_t aRow, nsAString& aLocationString);
+ virtual nsresult AddHdrFromFolder(nsIMsgDBHdr* msgHdr, nsIMsgFolder* folder);
+ virtual nsresult GetDBForViewIndex(nsMsgViewIndex index,
+ nsIMsgDatabase** db) override;
+ virtual nsresult RemoveByIndex(nsMsgViewIndex index) override;
+ virtual nsresult CopyMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool isMove, nsIMsgFolder* destFolder) override;
+ virtual nsresult DeleteMessages(nsIMsgWindow* window,
+ nsTArray<nsMsgViewIndex> const& selection,
+ bool deleteStorage) override;
+ virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) override;
+ virtual void SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index,
+ nsMsgKey msgKey, uint32_t flags,
+ uint32_t level) override;
+ virtual void InsertEmptyRows(nsMsgViewIndex viewIndex,
+ int32_t numRows) override;
+ virtual void RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) override;
+ virtual nsMsgViewIndex FindHdr(nsIMsgDBHdr* msgHdr,
+ nsMsgViewIndex startIndex = 0,
+ bool allowDummy = false) override;
+ nsresult GetFoldersAndHdrsForSelection(
+ nsTArray<nsMsgViewIndex> const& selection);
+ nsresult GroupSearchResultsByFolder();
+ nsresult PartitionSelectionByFolder(
+ nsTArray<nsMsgViewIndex> const& selection,
+ mozilla::UniquePtr<nsTArray<uint32_t>[]>& indexArrays,
+ int32_t* numArrays);
+
+ virtual nsresult ApplyCommandToIndicesWithFolder(
+ nsMsgViewCommandTypeValue command,
+ nsTArray<nsMsgViewIndex> const& selection,
+ nsIMsgFolder* destFolder) override;
+ void MoveThreadAt(nsMsgViewIndex threadIndex);
+
+ virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator) override;
+ virtual nsresult InsertHdrFromFolder(nsIMsgDBHdr* msgHdr,
+ nsIMsgFolder* folder);
+
+ // Holds the original folder of each message in this view.
+ // Augments the existing arrays in nsMsgDBView (m_keys, m_flags and m_levels),
+ // and is kept in sync with them.
+ nsCOMArray<nsIMsgFolder> m_folders;
+
+ nsTArray<nsTArray<RefPtr<nsIMsgDBHdr>>> m_hdrsForEachFolder;
+ nsCOMArray<nsIMsgFolder> m_uniqueFoldersSelected;
+ uint32_t mCurIndex;
+
+ int32_t mTotalIndices;
+ nsCOMArray<nsIMsgDatabase> m_dbToUseList;
+ nsMsgViewCommandTypeValue mCommand;
+ nsCOMPtr<nsIMsgFolder> mDestFolder;
+ nsWeakPtr m_searchSession;
+
+ nsresult ProcessRequestsInOneFolder(nsIMsgWindow* window);
+ nsresult ProcessRequestsInAllFolders(nsIMsgWindow* window);
+ // these are for doing threading of the search hits
+
+ // used for assigning thread id's to xfview threads.
+ nsMsgKey m_nextThreadId;
+ // this maps message-ids and reference message ids to
+ // the corresponding nsMsgXFViewThread object. If we're
+ // doing subject threading, we would throw subjects
+ // into the same table.
+ nsInterfaceHashtable<nsCStringHashKey, nsIMsgThread> m_threadsTable;
+
+ // map message-ids to msg hdrs in the view, used for threading.
+ nsInterfaceHashtable<nsCStringHashKey, nsIMsgDBHdr> m_hdrsTable;
+ uint32_t m_totalMessagesInView;
+
+ virtual nsMsgGroupThread* CreateGroupThread(nsIMsgDatabase* db) override;
+ nsresult GetXFThreadFromMsgHdr(nsIMsgDBHdr* msgHdr, nsIMsgThread** pThread,
+ bool* foundByMessageId = nullptr);
+ bool GetThreadFromHash(nsCString& reference, nsIMsgThread** thread);
+ bool GetMsgHdrFromHash(nsCString& reference, nsIMsgDBHdr** hdr);
+ nsresult AddRefToHash(nsCString& reference, nsIMsgThread* thread);
+ nsresult AddMsgToHashTables(nsIMsgDBHdr* msgHdr, nsIMsgThread* thread);
+ nsresult RemoveRefFromHash(nsCString& reference);
+ nsresult RemoveMsgFromHashTables(nsIMsgDBHdr* msgHdr);
+ nsresult InitRefHash();
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgSpecialViews.cpp b/comm/mailnews/base/src/nsMsgSpecialViews.cpp
new file mode 100644
index 0000000000..6bd42176e0
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgSpecialViews.cpp
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgSpecialViews.h"
+#include "nsIMsgThread.h"
+#include "nsMsgMessageFlags.h"
+
+nsMsgThreadsWithUnreadDBView::nsMsgThreadsWithUnreadDBView()
+ : m_totalUnwantedMessagesInView(0) {}
+
+nsMsgThreadsWithUnreadDBView::~nsMsgThreadsWithUnreadDBView() {}
+
+NS_IMETHODIMP nsMsgThreadsWithUnreadDBView::GetViewType(
+ nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowThreadsWithUnread;
+ return NS_OK;
+}
+
+bool nsMsgThreadsWithUnreadDBView::WantsThisThread(nsIMsgThread* threadHdr) {
+ if (threadHdr) {
+ uint32_t numNewChildren;
+
+ threadHdr->GetNumUnreadChildren(&numNewChildren);
+ if (numNewChildren > 0) return true;
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ m_totalUnwantedMessagesInView += numChildren;
+ }
+ return false;
+}
+
+nsresult nsMsgThreadsWithUnreadDBView::AddMsgToThreadNotInView(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr* msgHdr, bool ensureListed) {
+ nsresult rv = NS_OK;
+
+ nsCOMPtr<nsIMsgDBHdr> parentHdr;
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ GetFirstMessageHdrToDisplayInThread(threadHdr, getter_AddRefs(parentHdr));
+ if (parentHdr && (ensureListed || !(msgFlags & nsMsgMessageFlags::Read))) {
+ nsMsgKey key;
+ uint32_t numMsgsInThread;
+ rv = AddHdr(parentHdr);
+ threadHdr->GetNumChildren(&numMsgsInThread);
+ if (numMsgsInThread > 1) {
+ parentHdr->GetMessageKey(&key);
+ nsMsgViewIndex viewIndex = FindViewIndex(key);
+ if (viewIndex != nsMsgViewIndex_None)
+ OrExtraFlag(viewIndex,
+ nsMsgMessageFlags::Elided | MSG_VIEW_FLAG_HASCHILDREN);
+ }
+ m_totalUnwantedMessagesInView -= numMsgsInThread;
+ } else
+ m_totalUnwantedMessagesInView++;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgThreadsWithUnreadDBView::CloneDBView(
+ nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater, nsIMsgDBView** _retval) {
+ nsMsgThreadsWithUnreadDBView* newMsgDBView =
+ new nsMsgThreadsWithUnreadDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgThreadsWithUnreadDBView::GetNumMsgsInView(
+ int32_t* aNumMsgs) {
+ nsresult rv = nsMsgDBView::GetNumMsgsInView(aNumMsgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aNumMsgs = *aNumMsgs - m_totalUnwantedMessagesInView;
+ return rv;
+}
+
+nsMsgWatchedThreadsWithUnreadDBView::nsMsgWatchedThreadsWithUnreadDBView()
+ : m_totalUnwantedMessagesInView(0) {}
+
+NS_IMETHODIMP nsMsgWatchedThreadsWithUnreadDBView::GetViewType(
+ nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowWatchedThreadsWithUnread;
+ return NS_OK;
+}
+
+bool nsMsgWatchedThreadsWithUnreadDBView::WantsThisThread(
+ nsIMsgThread* threadHdr) {
+ if (threadHdr) {
+ uint32_t numNewChildren;
+ uint32_t threadFlags;
+
+ threadHdr->GetNumUnreadChildren(&numNewChildren);
+ threadHdr->GetFlags(&threadFlags);
+ if (numNewChildren > 0 && (threadFlags & nsMsgMessageFlags::Watched) != 0)
+ return true;
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ m_totalUnwantedMessagesInView += numChildren;
+ }
+ return false;
+}
+
+nsresult nsMsgWatchedThreadsWithUnreadDBView::AddMsgToThreadNotInView(
+ nsIMsgThread* threadHdr, nsIMsgDBHdr* msgHdr, bool ensureListed) {
+ nsresult rv = NS_OK;
+ uint32_t threadFlags;
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ threadHdr->GetFlags(&threadFlags);
+ if (threadFlags & nsMsgMessageFlags::Watched) {
+ nsCOMPtr<nsIMsgDBHdr> parentHdr;
+ GetFirstMessageHdrToDisplayInThread(threadHdr, getter_AddRefs(parentHdr));
+ if (parentHdr && (ensureListed || !(msgFlags & nsMsgMessageFlags::Read))) {
+ uint32_t numChildren;
+ threadHdr->GetNumChildren(&numChildren);
+ rv = AddHdr(parentHdr);
+ if (numChildren > 1) {
+ nsMsgKey key;
+ parentHdr->GetMessageKey(&key);
+ nsMsgViewIndex viewIndex = FindViewIndex(key);
+ if (viewIndex != nsMsgViewIndex_None)
+ OrExtraFlag(viewIndex, nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_ISTHREAD |
+ MSG_VIEW_FLAG_HASCHILDREN |
+ nsMsgMessageFlags::Watched);
+ }
+ m_totalUnwantedMessagesInView -= numChildren;
+ return rv;
+ }
+ }
+ m_totalUnwantedMessagesInView++;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgWatchedThreadsWithUnreadDBView::CloneDBView(
+ nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater, nsIMsgDBView** _retval) {
+ nsMsgWatchedThreadsWithUnreadDBView* newMsgDBView =
+ new nsMsgWatchedThreadsWithUnreadDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgWatchedThreadsWithUnreadDBView::GetNumMsgsInView(int32_t* aNumMsgs) {
+ nsresult rv = nsMsgDBView::GetNumMsgsInView(aNumMsgs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ *aNumMsgs = *aNumMsgs - m_totalUnwantedMessagesInView;
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMsgSpecialViews.h b/comm/mailnews/base/src/nsMsgSpecialViews.h
new file mode 100644
index 0000000000..1503eec123
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgSpecialViews.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsMsgSpecialViews_H_
+#define _nsMsgSpecialViews_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgThreadedDBView.h"
+
+class nsMsgThreadsWithUnreadDBView : public nsMsgThreadedDBView {
+ public:
+ nsMsgThreadsWithUnreadDBView();
+ virtual ~nsMsgThreadsWithUnreadDBView();
+ virtual const char* GetViewName(void) override {
+ return "ThreadsWithUnreadView";
+ }
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCommandUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override;
+ virtual bool WantsThisThread(nsIMsgThread* threadHdr) override;
+
+ protected:
+ virtual nsresult AddMsgToThreadNotInView(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr* msgHdr,
+ bool ensureListed) override;
+ uint32_t m_totalUnwantedMessagesInView;
+};
+
+class nsMsgWatchedThreadsWithUnreadDBView : public nsMsgThreadedDBView {
+ public:
+ nsMsgWatchedThreadsWithUnreadDBView();
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCommandUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override;
+ virtual const char* GetViewName(void) override {
+ return "WatchedThreadsWithUnreadView";
+ }
+ virtual bool WantsThisThread(nsIMsgThread* threadHdr) override;
+
+ protected:
+ virtual nsresult AddMsgToThreadNotInView(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr* msgHdr,
+ bool ensureListed) override;
+ uint32_t m_totalUnwantedMessagesInView;
+};
+#ifdef DOING_CACHELESS_VIEW
+// This view will initially be used for cacheless IMAP.
+class nsMsgCachelessView : public nsMsgDBView {
+ public:
+ nsMsgCachelessView();
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType);
+ virtual ~nsMsgCachelessView();
+ virtual const char* GetViewName(void) { return "nsMsgCachelessView"; }
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue viewType,
+ int32_t* count);
+ nsresult SetViewSize(int32_t setSize); // Override
+ virtual nsresult AddNewMessages();
+ virtual nsresult AddHdr(nsIMsgDBHdr* msgHdr);
+ // for news, xover line, potentially, for IMAP, imap line...
+ virtual nsresult AddHdrFromServerLine(char* line, nsMsgKey* msgId);
+ virtual void SetInitialSortState(void);
+ virtual nsresult Init(uint32_t* pCount);
+
+ protected:
+ void ClearPendingIds();
+
+ nsIMsgFolder* m_folder;
+ nsMsgViewIndex m_curStartSeq;
+ nsMsgViewIndex m_curEndSeq;
+ bool m_sizeInitialized;
+};
+
+#endif /* DOING_CACHELESS_VIEW */
+#endif
diff --git a/comm/mailnews/base/src/nsMsgStatusFeedback.cpp b/comm/mailnews/base/src/nsMsgStatusFeedback.cpp
new file mode 100644
index 0000000000..3afa14de87
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgStatusFeedback.cpp
@@ -0,0 +1,261 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+
+#include "nsIWebProgress.h"
+#include "nsIXULBrowserWindow.h"
+#include "nsMsgStatusFeedback.h"
+#include "mozilla/dom/Document.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIChannel.h"
+#include "prinrval.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIMsgWindow.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgDBFolder.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/Components.h"
+#include "nsMsgUtils.h"
+
+#define MSGFEEDBACK_TIMER_INTERVAL 500
+
+nsMsgStatusFeedback::nsMsgStatusFeedback()
+ : m_meteorsSpinning(false), m_lastPercent(0), m_lastProgressTime(0) {
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+
+ if (bundleService)
+ bundleService->CreateBundle(
+ "chrome://messenger/locale/messenger.properties",
+ getter_AddRefs(mBundle));
+}
+
+nsMsgStatusFeedback::~nsMsgStatusFeedback() { mBundle = nullptr; }
+
+NS_IMPL_ISUPPORTS(nsMsgStatusFeedback, nsIMsgStatusFeedback,
+ nsIProgressEventSink, nsIWebProgressListener,
+ nsISupportsWeakReference)
+
+//////////////////////////////////////////////////////////////////////////////////
+// nsMsgStatusFeedback::nsIWebProgressListener
+//////////////////////////////////////////////////////////////////////////////////
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnProgressChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ int32_t aCurSelfProgress,
+ int32_t aMaxSelfProgress,
+ int32_t aCurTotalProgress,
+ int32_t aMaxTotalProgress) {
+ int32_t percentage = 0;
+ if (aMaxTotalProgress > 0) {
+ percentage = (aCurTotalProgress * 100) / aMaxTotalProgress;
+ if (percentage) ShowProgress(percentage);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnStateChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aProgressStateFlags,
+ nsresult aStatus) {
+ nsresult rv;
+
+ NS_ENSURE_TRUE(mBundle, NS_ERROR_NULL_POINTER);
+ if (aProgressStateFlags & STATE_IS_NETWORK) {
+ if (aProgressStateFlags & STATE_START) {
+ m_lastPercent = 0;
+ StartMeteors();
+ nsString loadingDocument;
+ rv = mBundle->GetStringFromName("documentLoading", loadingDocument);
+ if (NS_SUCCEEDED(rv)) ShowStatusString(loadingDocument);
+ } else if (aProgressStateFlags & STATE_STOP) {
+ // if we are loading message for display purposes, this STATE_STOP
+ // notification is the only notification we get when layout is actually
+ // done rendering the message. We need to fire the appropriate msgHdrSink
+ // notification in this particular case.
+ nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest);
+ if (channel) {
+ nsCOMPtr<nsIURI> uri;
+ channel->GetURI(getter_AddRefs(uri));
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(uri));
+ if (mailnewsUrl) {
+ // get the url type
+ bool messageDisplayUrl;
+ mailnewsUrl->IsUrlType(nsIMsgMailNewsUrl::eDisplay,
+ &messageDisplayUrl);
+
+ if (messageDisplayUrl) {
+ // get the folder and notify that the msg has been loaded. We're
+ // using NotifyPropertyFlagChanged. To be completely consistent,
+ // we'd send a similar notification that the old message was
+ // unloaded.
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ mailnewsUrl->GetFolder(getter_AddRefs(msgFolder));
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(mailnewsUrl);
+ if (msgUrl) {
+ // not sending this notification is not a fatal error...
+ (void)msgUrl->GetMessageHeader(getter_AddRefs(msgHdr));
+ if (msgFolder && msgHdr)
+ msgFolder->NotifyPropertyFlagChanged(msgHdr, kMsgLoaded, 0, 1);
+ }
+ }
+ }
+ }
+ StopMeteors();
+ nsString documentDone;
+ rv = mBundle->GetStringFromName("documentDone", documentDone);
+ if (NS_SUCCEEDED(rv)) ShowStatusString(documentDone);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgStatusFeedback::OnLocationChange(
+ nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI* aLocation,
+ uint32_t aFlags) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnStatusChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, nsresult aStatus,
+ const char16_t* aMessage) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnSecurityChange(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest, uint32_t state) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::OnContentBlockingEvent(nsIWebProgress* aWebProgress,
+ nsIRequest* aRequest,
+ uint32_t aEvent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::ShowStatusString(const nsAString& aStatus) {
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->ShowStatusString(aStatus);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::SetStatusString(const nsAString& aStatus) {
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->SetStatusString(aStatus);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::ShowProgress(int32_t aPercentage) {
+ // if the percentage hasn't changed...OR if we are going from 0 to 100% in one
+ // step then don't bother....just fall out....
+ if (aPercentage == m_lastPercent ||
+ (m_lastPercent == 0 && aPercentage >= 100))
+ return NS_OK;
+
+ m_lastPercent = aPercentage;
+
+ int64_t nowMS = 0;
+ if (aPercentage < 100) // always need to do 100%
+ {
+ nowMS = PR_IntervalToMilliseconds(PR_IntervalNow());
+ if (nowMS < m_lastProgressTime + 250) return NS_OK;
+ }
+
+ m_lastProgressTime = nowMS;
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->ShowProgress(aPercentage);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::StartMeteors() {
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->StartMeteors();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgStatusFeedback::StopMeteors() {
+ nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback(
+ do_QueryReferent(mJSStatusFeedbackWeak));
+ if (jsStatusFeedback) jsStatusFeedback->StopMeteors();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgStatusFeedback::SetWrappedStatusFeedback(
+ nsIMsgStatusFeedback* aJSStatusFeedback) {
+ NS_ENSURE_ARG_POINTER(aJSStatusFeedback);
+ mJSStatusFeedbackWeak = do_GetWeakReference(aJSStatusFeedback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgStatusFeedback::OnProgress(nsIRequest* request,
+ int64_t aProgress,
+ int64_t aProgressMax) {
+ // XXX: What should the nsIWebProgress be?
+ // XXX: this truncates 64-bit to 32-bit
+ return OnProgressChange(nullptr, request, int32_t(aProgress),
+ int32_t(aProgressMax),
+ int32_t(aProgress) /* current total progress */,
+ int32_t(aProgressMax) /* max total progress */);
+}
+
+NS_IMETHODIMP nsMsgStatusFeedback::OnStatus(nsIRequest* request,
+ nsresult aStatus,
+ const char16_t* aStatusArg) {
+ nsresult rv;
+ nsCOMPtr<nsIURI> uri;
+ nsString accountName;
+ // fetching account name from nsIRequest
+ nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
+ rv = aChannel->GetURI(getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIMsgMailNewsUrl> url(do_QueryInterface(uri));
+ if (url) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ url->GetServer(getter_AddRefs(server));
+ if (server) server->GetPrettyName(accountName);
+ }
+
+ // forming the status message
+ nsCOMPtr<nsIStringBundleService> sbs =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(sbs, NS_ERROR_UNEXPECTED);
+ nsString str;
+ rv = sbs->FormatStatusMessage(aStatus, aStatusArg, str);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // prefixing the account name to the status message if status message isn't
+ // blank and doesn't already contain the account name.
+ nsString statusMessage;
+ if (!str.IsEmpty() && str.Find(accountName) == kNotFound) {
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = sbs->CreateBundle(MSGS_URL, getter_AddRefs(bundle));
+ AutoTArray<nsString, 2> params = {accountName, str};
+ rv = bundle->FormatStringFromName("statusMessage", params, statusMessage);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ statusMessage.Assign(str);
+ }
+ return ShowStatusString(statusMessage);
+}
diff --git a/comm/mailnews/base/src/nsMsgStatusFeedback.h b/comm/mailnews/base/src/nsMsgStatusFeedback.h
new file mode 100644
index 0000000000..6713eaea4c
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgStatusFeedback.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsMsgStatusFeedback_h
+#define _nsMsgStatusFeedback_h
+
+#include "nsIWebProgressListener.h"
+#include "nsIObserver.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsIProgressEventSink.h"
+#include "nsIStringBundle.h"
+#include "nsWeakReference.h"
+#include "nsIWeakReferenceUtils.h"
+
+class nsMsgStatusFeedback : public nsIMsgStatusFeedback,
+ public nsIProgressEventSink,
+ public nsIWebProgressListener,
+ public nsSupportsWeakReference {
+ public:
+ nsMsgStatusFeedback();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMSGSTATUSFEEDBACK
+ NS_DECL_NSIWEBPROGRESSLISTENER
+ NS_DECL_NSIPROGRESSEVENTSINK
+
+ protected:
+ virtual ~nsMsgStatusFeedback();
+
+ bool m_meteorsSpinning;
+ int32_t m_lastPercent;
+ int64_t m_lastProgressTime;
+
+ void BeginObserving();
+ void EndObserving();
+
+ // the JS status feedback implementation object...eventually this object
+ // will replace this very C++ class you are looking at.
+ nsWeakPtr mJSStatusFeedbackWeak;
+
+ nsCOMPtr<nsIStringBundle> mBundle;
+};
+
+#endif // _nsMsgStatusFeedback_h
diff --git a/comm/mailnews/base/src/nsMsgTagService.cpp b/comm/mailnews/base/src/nsMsgTagService.cpp
new file mode 100644
index 0000000000..83ced5fdeb
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgTagService.cpp
@@ -0,0 +1,458 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgTagService.h"
+#include "nsIPrefService.h"
+#include "nsISupportsPrimitives.h"
+#include "nsMsgI18N.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsMsgDBView.h" // for labels migration
+#include "nsQuickSort.h"
+#include "nsMsgUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsMemory.h"
+
+#define STRLEN(s) (sizeof(s) - 1)
+
+#define TAG_PREF_VERSION "version"
+#define TAG_PREF_SUFFIX_TAG ".tag"
+#define TAG_PREF_SUFFIX_COLOR ".color"
+#define TAG_PREF_SUFFIX_ORDINAL ".ordinal"
+
+static bool gMigratingKeys = false;
+
+// Comparator to set sort order in GetAllTags().
+struct CompareMsgTags {
+ private:
+ int cmp(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const {
+ // Sort nsMsgTag objects by ascending order, using their ordinal or key.
+ // The "smallest" value will be first in the sorted array,
+ // thus being the most important element.
+
+ // Only use the key if the ordinal is not defined or empty.
+ nsAutoCString value1, value2;
+ element1->GetOrdinal(value1);
+ if (value1.IsEmpty()) element1->GetKey(value1);
+ element2->GetOrdinal(value2);
+ if (value2.IsEmpty()) element2->GetKey(value2);
+
+ return strcmp(value1.get(), value2.get());
+ }
+
+ public:
+ bool Equals(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const {
+ return cmp(element1, element2) == 0;
+ }
+ bool LessThan(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const {
+ return cmp(element1, element2) < 0;
+ }
+};
+
+//
+// nsMsgTag
+//
+NS_IMPL_ISUPPORTS(nsMsgTag, nsIMsgTag)
+
+nsMsgTag::nsMsgTag(const nsACString& aKey, const nsAString& aTag,
+ const nsACString& aColor, const nsACString& aOrdinal)
+ : mTag(aTag), mKey(aKey), mColor(aColor), mOrdinal(aOrdinal) {}
+
+nsMsgTag::~nsMsgTag() {}
+
+/* readonly attribute ACString key; */
+NS_IMETHODIMP nsMsgTag::GetKey(nsACString& aKey) {
+ aKey = mKey;
+ return NS_OK;
+}
+
+/* readonly attribute AString tag; */
+NS_IMETHODIMP nsMsgTag::GetTag(nsAString& aTag) {
+ aTag = mTag;
+ return NS_OK;
+}
+
+/* readonly attribute ACString color; */
+NS_IMETHODIMP nsMsgTag::GetColor(nsACString& aColor) {
+ aColor = mColor;
+ return NS_OK;
+}
+
+/* readonly attribute ACString ordinal; */
+NS_IMETHODIMP nsMsgTag::GetOrdinal(nsACString& aOrdinal) {
+ aOrdinal = mOrdinal;
+ return NS_OK;
+}
+
+//
+// nsMsgTagService
+//
+NS_IMPL_ISUPPORTS(nsMsgTagService, nsIMsgTagService)
+
+nsMsgTagService::nsMsgTagService() {
+ m_tagPrefBranch = nullptr;
+ nsCOMPtr<nsIPrefService> prefService(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (prefService)
+ prefService->GetBranch("mailnews.tags.", getter_AddRefs(m_tagPrefBranch));
+ SetupLabelTags();
+ RefreshKeyCache();
+}
+
+nsMsgTagService::~nsMsgTagService() {} /* destructor code */
+
+/* wstring getTagForKey (in string key); */
+NS_IMETHODIMP nsMsgTagService::GetTagForKey(const nsACString& key,
+ nsAString& _retval) {
+ nsAutoCString prefName(key);
+ if (!gMigratingKeys) ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG);
+ return GetUnicharPref(prefName.get(), _retval);
+}
+
+/* void setTagForKey (in string key); */
+NS_IMETHODIMP nsMsgTagService::SetTagForKey(const nsACString& key,
+ const nsAString& tag) {
+ nsAutoCString prefName(key);
+ ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG);
+ return SetUnicharPref(prefName.get(), tag);
+}
+
+/* void getKeyForTag (in wstring tag); */
+NS_IMETHODIMP nsMsgTagService::GetKeyForTag(const nsAString& aTag,
+ nsACString& aKey) {
+ nsTArray<nsCString> prefList;
+ nsresult rv = m_tagPrefBranch->GetChildList("", prefList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // traverse the list, and look for a pref with the desired tag value.
+ // XXXbz is there a good reason to reverse the list here, or did the
+ // old code do it just to be clever and save some characters in the
+ // for loop header?
+ for (auto& prefName : mozilla::Reversed(prefList)) {
+ // We are returned the tag prefs in the form "<key>.<tag_data_type>", but
+ // since we only want the tags, just check that the string ends with "tag".
+ if (StringEndsWith(prefName, nsLiteralCString(TAG_PREF_SUFFIX_TAG))) {
+ nsAutoString curTag;
+ GetUnicharPref(prefName.get(), curTag);
+ if (aTag.Equals(curTag)) {
+ aKey = Substring(prefName, 0,
+ prefName.Length() - STRLEN(TAG_PREF_SUFFIX_TAG));
+ break;
+ }
+ }
+ }
+ ToLowerCase(aKey);
+ return NS_OK;
+}
+
+/* ACString getTopKey (in ACString keylist); */
+NS_IMETHODIMP nsMsgTagService::GetTopKey(const nsACString& keyList,
+ nsACString& _retval) {
+ _retval.Truncate();
+ // find the most important key
+ nsTArray<nsCString> keyArray;
+ ParseString(keyList, ' ', keyArray);
+ uint32_t keyCount = keyArray.Length();
+ nsCString *topKey = nullptr, *key, topOrdinal, ordinal;
+ for (uint32_t i = 0; i < keyCount; ++i) {
+ key = &keyArray[i];
+ if (key->IsEmpty()) continue;
+
+ // ignore unknown keywords
+ nsAutoString tagValue;
+ nsresult rv = GetTagForKey(*key, tagValue);
+ if (NS_FAILED(rv) || tagValue.IsEmpty()) continue;
+
+ // new top key, judged by ordinal order?
+ rv = GetOrdinalForKey(*key, ordinal);
+ if (NS_FAILED(rv) || ordinal.IsEmpty()) ordinal = *key;
+ if ((ordinal < topOrdinal) || topOrdinal.IsEmpty()) {
+ topOrdinal = ordinal;
+ topKey = key; // copy actual result key only once - later
+ }
+ }
+ // return the most important key - if any
+ if (topKey) _retval = *topKey;
+ return NS_OK;
+}
+
+/* void addTagForKey (in string key, in wstring tag, in string color, in string
+ * ordinal); */
+NS_IMETHODIMP nsMsgTagService::AddTagForKey(const nsACString& key,
+ const nsAString& tag,
+ const nsACString& color,
+ const nsACString& ordinal) {
+ nsAutoCString prefName(key);
+ ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG);
+ nsresult rv = SetUnicharPref(prefName.get(), tag);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetColorForKey(key, color);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = RefreshKeyCache();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return SetOrdinalForKey(key, ordinal);
+}
+
+/* void addTag (in wstring tag, in long color); */
+NS_IMETHODIMP nsMsgTagService::AddTag(const nsAString& tag,
+ const nsACString& color,
+ const nsACString& ordinal) {
+ // figure out key from tag. Apply transformation stripping out
+ // illegal characters like <SP> and then convert to imap mod utf7.
+ // Then, check if we have a tag with that key yet, and if so,
+ // make it unique by appending A, AA, etc.
+ // Should we use an iterator?
+ nsAutoString transformedTag(tag);
+ transformedTag.ReplaceChar(u" ()/{%*<>\\\"", u'_');
+ nsAutoCString key;
+ CopyUTF16toMUTF7(transformedTag, key);
+ // We have an imap server that converts keys to upper case so we're going
+ // to normalize all keys to lower case (upper case looks ugly in prefs.js)
+ ToLowerCase(key);
+ nsAutoCString prefName(key);
+ while (true) {
+ nsAutoString tagValue;
+ nsresult rv = GetTagForKey(prefName, tagValue);
+ if (NS_FAILED(rv) || tagValue.IsEmpty() || tagValue.Equals(tag))
+ return AddTagForKey(prefName, tag, color, ordinal);
+ prefName.Append('A');
+ }
+ NS_ASSERTION(false, "can't get here");
+ return NS_ERROR_FAILURE;
+}
+
+/* long getColorForKey (in string key); */
+NS_IMETHODIMP nsMsgTagService::GetColorForKey(const nsACString& key,
+ nsACString& _retval) {
+ nsAutoCString prefName(key);
+ if (!gMigratingKeys) ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_COLOR);
+ nsCString color;
+ nsresult rv = m_tagPrefBranch->GetCharPref(prefName.get(), color);
+ if (NS_SUCCEEDED(rv)) _retval = color;
+ return NS_OK;
+}
+
+/* long getSelectorForKey (in ACString key, out AString selector); */
+NS_IMETHODIMP nsMsgTagService::GetSelectorForKey(const nsACString& key,
+ nsAString& _retval) {
+ // Our keys are the result of MUTF-7 encoding. For CSS selectors we need
+ // to reduce this to 0-9A-Za-z_ with a leading alpha character.
+ // We encode non-alphanumeric characters using _ as an escape character
+ // and start with a leading T in all cases. This way users defining tags
+ // "selected" or "focus" don't collide with inbuilt "selected" or "focus".
+
+ // Calculate length of selector string.
+ const char* in = key.BeginReading();
+ size_t outLen = 1;
+ while (*in) {
+ if (('0' <= *in && *in <= '9') || ('A' <= *in && *in <= 'Z') ||
+ ('a' <= *in && *in <= 'z')) {
+ outLen++;
+ } else {
+ outLen += 3;
+ }
+ in++;
+ }
+
+ // Now fill selector string.
+ _retval.SetCapacity(outLen);
+ _retval.Assign('T');
+ in = key.BeginReading();
+ while (*in) {
+ if (('0' <= *in && *in <= '9') || ('A' <= *in && *in <= 'Z') ||
+ ('a' <= *in && *in <= 'z')) {
+ _retval.Append(*in);
+ } else {
+ _retval.AppendPrintf("_%02x", *in);
+ }
+ in++;
+ }
+
+ return NS_OK;
+}
+
+/* void setColorForKey (in ACString key, in ACString color); */
+NS_IMETHODIMP nsMsgTagService::SetColorForKey(const nsACString& key,
+ const nsACString& color) {
+ nsAutoCString prefName(key);
+ ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_COLOR);
+ if (color.IsEmpty()) {
+ m_tagPrefBranch->ClearUserPref(prefName.get());
+ return NS_OK;
+ }
+ return m_tagPrefBranch->SetCharPref(prefName.get(), color);
+}
+
+/* ACString getOrdinalForKey (in ACString key); */
+NS_IMETHODIMP nsMsgTagService::GetOrdinalForKey(const nsACString& key,
+ nsACString& _retval) {
+ nsAutoCString prefName(key);
+ if (!gMigratingKeys) ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_ORDINAL);
+ nsCString ordinal;
+ nsresult rv = m_tagPrefBranch->GetCharPref(prefName.get(), ordinal);
+ _retval = ordinal;
+ return rv;
+}
+
+/* void setOrdinalForKey (in ACString key, in ACString ordinal); */
+NS_IMETHODIMP nsMsgTagService::SetOrdinalForKey(const nsACString& key,
+ const nsACString& ordinal) {
+ nsAutoCString prefName(key);
+ ToLowerCase(prefName);
+ prefName.AppendLiteral(TAG_PREF_SUFFIX_ORDINAL);
+ if (ordinal.IsEmpty()) {
+ m_tagPrefBranch->ClearUserPref(prefName.get());
+ return NS_OK;
+ }
+ return m_tagPrefBranch->SetCharPref(prefName.get(), ordinal);
+}
+
+/* void deleteTag (in wstring tag); */
+NS_IMETHODIMP nsMsgTagService::DeleteKey(const nsACString& key) {
+ // clear the associated prefs
+ nsAutoCString prefName(key);
+ if (!gMigratingKeys) ToLowerCase(prefName);
+ prefName.Append('.');
+
+ nsTArray<nsCString> prefNames;
+ nsresult rv = m_tagPrefBranch->GetChildList(prefName.get(), prefNames);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (auto& prefName : prefNames) {
+ m_tagPrefBranch->ClearUserPref(prefName.get());
+ }
+
+ return RefreshKeyCache();
+}
+
+/* Array<nsIMsgTag> getAllTags(); */
+NS_IMETHODIMP nsMsgTagService::GetAllTags(
+ nsTArray<RefPtr<nsIMsgTag>>& aTagArray) {
+ aTagArray.Clear();
+
+ // get the actual tag definitions
+ nsresult rv;
+ nsTArray<nsCString> prefList;
+ rv = m_tagPrefBranch->GetChildList("", prefList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // sort them by key for ease of processing
+ prefList.Sort();
+
+ nsString tag;
+ nsCString lastKey, color, ordinal;
+ for (auto& pref : mozilla::Reversed(prefList)) {
+ // extract just the key from <key>.<info=tag|color|ordinal>
+ int32_t dotLoc = pref.RFindChar('.');
+ if (dotLoc != kNotFound) {
+ auto& key = Substring(pref, 0, dotLoc);
+ if (key != lastKey) {
+ if (!key.IsEmpty()) {
+ // .tag MUST exist (but may be empty)
+ rv = GetTagForKey(key, tag);
+ if (NS_SUCCEEDED(rv)) {
+ // .color MAY exist
+ color.Truncate();
+ GetColorForKey(key, color);
+ // .ordinal MAY exist
+ rv = GetOrdinalForKey(key, ordinal);
+ if (NS_FAILED(rv)) ordinal.Truncate();
+ // store the tag info in our array
+ aTagArray.AppendElement(new nsMsgTag(key, tag, color, ordinal));
+ }
+ }
+ lastKey = key;
+ }
+ }
+ }
+
+ // sort the non-null entries by ordinal
+ aTagArray.Sort(CompareMsgTags());
+ return NS_OK;
+}
+
+nsresult nsMsgTagService::SetUnicharPref(const char* prefName,
+ const nsAString& val) {
+ nsresult rv = NS_OK;
+ if (!val.IsEmpty()) {
+ rv = m_tagPrefBranch->SetStringPref(prefName, NS_ConvertUTF16toUTF8(val));
+ } else {
+ m_tagPrefBranch->ClearUserPref(prefName);
+ }
+ return rv;
+}
+
+nsresult nsMsgTagService::GetUnicharPref(const char* prefName,
+ nsAString& prefValue) {
+ nsCString valueUtf8;
+ nsresult rv =
+ m_tagPrefBranch->GetStringPref(prefName, EmptyCString(), 0, valueUtf8);
+ CopyUTF8toUTF16(valueUtf8, prefValue);
+ return rv;
+}
+
+nsresult nsMsgTagService::SetupLabelTags() {
+ nsCString prefString;
+
+ int32_t prefVersion = 0;
+ nsresult rv = m_tagPrefBranch->GetIntPref(TAG_PREF_VERSION, &prefVersion);
+ if (NS_SUCCEEDED(rv) && prefVersion > 1) {
+ return rv;
+ }
+ nsCOMPtr<nsIPrefBranch> prefRoot(do_GetService(NS_PREFSERVICE_CONTRACTID));
+ nsCOMPtr<nsIPrefLocalizedString> pls;
+ nsString ucsval;
+ nsAutoCString labelKey("$label1");
+ for (int32_t i = 0; i < 5;) {
+ prefString.AssignLiteral("mailnews.labels.description.");
+ prefString.AppendInt(i + 1);
+ rv = prefRoot->GetComplexValue(prefString.get(),
+ NS_GET_IID(nsIPrefLocalizedString),
+ getter_AddRefs(pls));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pls->ToString(getter_Copies(ucsval));
+
+ prefString.AssignLiteral("mailnews.labels.color.");
+ prefString.AppendInt(i + 1);
+ nsCString csval;
+ rv = prefRoot->GetCharPref(prefString.get(), csval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = AddTagForKey(labelKey, ucsval, csval, EmptyCString());
+ NS_ENSURE_SUCCESS(rv, rv);
+ labelKey.SetCharAt(++i + '1', 6);
+ }
+ m_tagPrefBranch->SetIntPref(TAG_PREF_VERSION, 2);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgTagService::IsValidKey(const nsACString& aKey,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_keys.Contains(aKey);
+ return NS_OK;
+}
+
+// refresh the local tag key array m_keys from preferences
+nsresult nsMsgTagService::RefreshKeyCache() {
+ nsTArray<RefPtr<nsIMsgTag>> tagArray;
+ nsresult rv = GetAllTags(tagArray);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_keys.Clear();
+
+ uint32_t numTags = tagArray.Length();
+ m_keys.SetCapacity(numTags);
+ for (uint32_t tagIndex = 0; tagIndex < numTags; tagIndex++) {
+ nsAutoCString key;
+ tagArray[tagIndex]->GetKey(key);
+ m_keys.InsertElementAt(tagIndex, key);
+ }
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMsgTagService.h b/comm/mailnews/base/src/nsMsgTagService.h
new file mode 100644
index 0000000000..7eee648271
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgTagService.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsMsgTagService_h__
+#define nsMsgTagService_h__
+
+#include "nsIMsgTagService.h"
+#include "nsIPrefBranch.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsMsgTag final : public nsIMsgTag {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGTAG
+
+ nsMsgTag(const nsACString& aKey, const nsAString& aTag,
+ const nsACString& aColor, const nsACString& aOrdinal);
+
+ protected:
+ ~nsMsgTag();
+
+ nsString mTag;
+ nsCString mKey, mColor, mOrdinal;
+};
+
+class nsMsgTagService final : public nsIMsgTagService {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIMSGTAGSERVICE
+
+ nsMsgTagService();
+
+ private:
+ ~nsMsgTagService();
+
+ protected:
+ nsresult SetUnicharPref(const char* prefName, const nsAString& prefValue);
+ nsresult GetUnicharPref(const char* prefName, nsAString& prefValue);
+ nsresult SetupLabelTags();
+ nsresult RefreshKeyCache();
+
+ nsCOMPtr<nsIPrefBranch> m_tagPrefBranch;
+ nsTArray<nsCString> m_keys;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgThreadedDBView.cpp b/comm/mailnews/base/src/nsMsgThreadedDBView.cpp
new file mode 100644
index 0000000000..84105bc4fc
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgThreadedDBView.cpp
@@ -0,0 +1,899 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgThreadedDBView.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgSearchSession.h"
+#include "nsMsgMessageFlags.h"
+
+// Allocate this more to avoid reallocation on new mail.
+#define MSGHDR_CACHE_LOOK_AHEAD_SIZE 25
+// Max msghdr cache entries.
+#define MSGHDR_CACHE_MAX_SIZE 8192
+#define MSGHDR_CACHE_DEFAULT_SIZE 100
+
+nsMsgThreadedDBView::nsMsgThreadedDBView() {
+ /* member initializers and constructor code */
+ m_havePrevView = false;
+}
+
+nsMsgThreadedDBView::~nsMsgThreadedDBView() {} /* destructor code */
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) {
+ nsresult rv =
+ nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!m_db) return NS_ERROR_NULL_POINTER;
+
+ // Preset msg hdr cache size for performance reason.
+ int32_t totalMessages, unreadMessages;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ PersistFolderInfo(getter_AddRefs(dbFolderInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Save off sort type and order, view type and flags.
+ dbFolderInfo->GetNumUnreadMessages(&unreadMessages);
+ dbFolderInfo->GetNumMessages(&totalMessages);
+ if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) {
+ // Set unread msg size + extra entries to avoid reallocation on new mail.
+ totalMessages = (uint32_t)unreadMessages + MSGHDR_CACHE_LOOK_AHEAD_SIZE;
+ } else {
+ if (totalMessages > MSGHDR_CACHE_MAX_SIZE)
+ // Use max default.
+ totalMessages = MSGHDR_CACHE_MAX_SIZE;
+ else if (totalMessages > 0)
+ // Allocate extra entries to avoid reallocation on new mail.
+ totalMessages += MSGHDR_CACHE_LOOK_AHEAD_SIZE;
+ }
+
+ // If total messages is 0, then we probably don't have any idea how many
+ // headers are in the db so we have no business setting the cache size.
+ if (totalMessages > 0) m_db->SetMsgHdrCacheSize((uint32_t)totalMessages);
+
+ int32_t count;
+ rv = InitThreadedView(count);
+ if (pCount) *pCount = count;
+
+ // This is a hack, but we're trying to find a way to correct
+ // incorrect total and unread msg counts w/o paying a big
+ // performance penalty. So, if we're not threaded, just add
+ // up the total and unread messages in the view and see if that
+ // matches what the db totals say. Except ignored threads are
+ // going to throw us off...hmm. Unless we just look at the
+ // unread counts which is what mostly tweaks people anyway...
+ int32_t unreadMsgsInView = 0;
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ for (uint32_t i = m_flags.Length(); i > 0;) {
+ if (!(m_flags[--i] & nsMsgMessageFlags::Read)) ++unreadMsgsInView;
+ }
+
+ if (unreadMessages != unreadMsgsInView) m_db->SyncCounts();
+ }
+
+ m_db->SetMsgHdrCacheSize(MSGHDR_CACHE_DEFAULT_SIZE);
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::Close() { return nsMsgDBView::Close(); }
+
+// Populate the view with the ids of the first message in each thread.
+nsresult nsMsgThreadedDBView::InitThreadedView(int32_t& count) {
+ count = 0;
+ m_keys.Clear();
+ m_flags.Clear();
+ m_levels.Clear();
+ m_prevKeys.Clear();
+ m_prevFlags.Clear();
+ m_prevLevels.Clear();
+ m_havePrevView = false;
+
+ bool unreadOnly = (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly);
+
+ nsCOMPtr<nsIMsgThreadEnumerator> threads;
+ nsresult rv = m_db->EnumerateThreads(getter_AddRefs(threads));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore = false;
+ while (NS_SUCCEEDED(rv = threads->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ rv = threads->GetNext(getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t numChildren;
+ if (unreadOnly)
+ threadHdr->GetNumUnreadChildren(&numChildren);
+ else
+ threadHdr->GetNumChildren(&numChildren);
+
+ if (numChildren == 0) {
+ continue; // An empty thread.
+ }
+
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ if (unreadOnly) {
+ rv = threadHdr->GetFirstUnreadChild(getter_AddRefs(msgHdr));
+ } else {
+ rv = threadHdr->GetRootHdr(getter_AddRefs(msgHdr));
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Hook to allow derived classes to filter out unwanted threads.
+ if (!WantsThisThread(threadHdr)) {
+ continue;
+ }
+
+ uint32_t msgFlags;
+ msgHdr->GetFlags(&msgFlags);
+ // Turn off high byte of msg flags - used for view flags.
+ msgFlags &= ~MSG_VIEW_FLAGS;
+ // Turn off these flags on msg hdr - they belong in thread.
+ uint32_t newMsgFlagsUnused;
+ msgHdr->AndFlags(~(nsMsgMessageFlags::Watched), &newMsgFlagsUnused);
+ AdjustReadFlag(msgHdr, &msgFlags);
+ // Try adding in MSG_VIEW_FLAG_ISTHREAD flag for unreadonly view.
+ uint32_t threadFlags;
+ threadHdr->GetFlags(&threadFlags);
+ msgFlags |= MSG_VIEW_FLAG_ISTHREAD | threadFlags;
+ if (numChildren > 1) {
+ msgFlags |= MSG_VIEW_FLAG_HASCHILDREN;
+ }
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) {
+ // Skip ignored threads.
+ if (msgFlags & nsMsgMessageFlags::Ignored) {
+ continue;
+ }
+ // Skip ignored subthreads
+ bool killed;
+ msgHdr->GetIsKilled(&killed);
+ if (killed) {
+ continue;
+ }
+ }
+
+ // By default, make threads collapsed unless we're only viewing new msgs.
+ if (msgFlags & MSG_VIEW_FLAG_HASCHILDREN) {
+ msgFlags |= nsMsgMessageFlags::Elided;
+ }
+
+ // OK, now add it to the view!
+ nsMsgKey msgKey;
+ msgHdr->GetMessageKey(&msgKey);
+ m_keys.AppendElement(msgKey);
+ m_flags.AppendElement(msgFlags);
+ m_levels.AppendElement(0);
+
+ // We expand as we build the view, which allows us to insert at the end
+ // of the key array, instead of the middle, and is much faster.
+ if ((!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) ||
+ m_viewFlags & nsMsgViewFlagsType::kExpandAll) &&
+ msgFlags & nsMsgMessageFlags::Elided) {
+ ExpandByIndex(m_keys.Length() - 1, nullptr);
+ }
+
+ count++;
+ }
+
+ rv = InitSort(m_sortType, m_sortOrder);
+ SaveSortInfo(m_sortType, m_sortOrder);
+ return rv;
+}
+
+nsresult nsMsgThreadedDBView::SortThreads(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ NS_ASSERTION(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay,
+ "trying to sort unthreaded threads");
+
+ uint32_t numThreads = 0;
+ // The idea here is that copy the current view, then build up an m_keys and
+ // m_flags array of just the top level messages in the view, and then call
+ // nsMsgDBView::Sort(sortType, sortOrder).
+ // Then, we expand the threads in the result array that were expanded in the
+ // original view (perhaps by copying from the original view, but more likely
+ // just be calling expand).
+ for (uint32_t i = 0; i < m_keys.Length(); i++) {
+ if (m_flags[i] & MSG_VIEW_FLAG_ISTHREAD) {
+ if (numThreads < i) {
+ m_keys[numThreads] = m_keys[i];
+ m_flags[numThreads] = m_flags[i];
+ }
+
+ m_levels[numThreads] = 0;
+ numThreads++;
+ }
+ }
+
+ m_keys.SetLength(numThreads);
+ m_flags.SetLength(numThreads);
+ m_levels.SetLength(numThreads);
+ // m_viewFlags &= ~nsMsgViewFlagsType::kThreadedDisplay;
+ m_sortType = nsMsgViewSortType::byNone; // sort from scratch
+ nsMsgDBView::Sort(sortType, sortOrder);
+ m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay;
+ SetSuppressChangeNotifications(true);
+
+ // Loop through the original array, for each thread that's expanded,
+ // find it in the new array and expand the thread. We have to update
+ // MSG_VIEW_FLAG_HAS_CHILDREN because we may be going from a flat sort,
+ // which doesn't maintain that flag, to a threaded sort, which requires
+ // that flag.
+ for (uint32_t j = 0; j < m_keys.Length(); j++) {
+ uint32_t flags = m_flags[j];
+ if ((flags & (MSG_VIEW_FLAG_HASCHILDREN | nsMsgMessageFlags::Elided)) ==
+ MSG_VIEW_FLAG_HASCHILDREN) {
+ uint32_t numExpanded;
+ m_flags[j] = flags | nsMsgMessageFlags::Elided;
+ ExpandByIndex(j, &numExpanded);
+ j += numExpanded;
+ if (numExpanded > 0)
+ m_flags[j - numExpanded] = flags | MSG_VIEW_FLAG_HASCHILDREN;
+ } else if (flags & MSG_VIEW_FLAG_ISTHREAD &&
+ !(flags & MSG_VIEW_FLAG_HASCHILDREN)) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ nsCOMPtr<nsIMsgThread> pThread;
+ m_db->GetMsgHdrForKey(m_keys[j], getter_AddRefs(msgHdr));
+ if (msgHdr) {
+ m_db->GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread));
+ if (pThread) {
+ uint32_t numChildren;
+ pThread->GetNumChildren(&numChildren);
+ if (numChildren > 1)
+ m_flags[j] =
+ flags | MSG_VIEW_FLAG_HASCHILDREN | nsMsgMessageFlags::Elided;
+ }
+ }
+ }
+ }
+
+ SetSuppressChangeNotifications(false);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ nsresult rv;
+
+ int32_t rowCountBeforeSort = GetSize();
+
+ if (!rowCountBeforeSort) {
+ // Still need to setup our flags even when no articles - bug 98183.
+ m_sortType = sortType;
+ m_sortOrder = sortOrder;
+ if (sortType == nsMsgViewSortType::byThread &&
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ SetViewFlags(m_viewFlags | nsMsgViewFlagsType::kThreadedDisplay);
+ }
+
+ SaveSortInfo(sortType, sortOrder);
+ return NS_OK;
+ }
+
+ if (!m_checkedCustomColumns && CustomColumnsInSortAndNotRegistered())
+ return NS_OK;
+
+ // Sort threads by sort order.
+ bool sortThreads = m_viewFlags & (nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kGroupBySort);
+
+ // If sort type is by thread, and we're already threaded, change sort type
+ // to byId.
+ if (sortType == nsMsgViewSortType::byThread &&
+ (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) != 0) {
+ sortType = nsMsgViewSortType::byId;
+ }
+
+ nsMsgKey preservedKey;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ SaveAndClearSelection(&preservedKey, preservedSelection);
+ // If the client wants us to forget our cached id arrays, they
+ // should build a new view. If this isn't good enough, we
+ // need a method to do that.
+ if (sortType != m_sortType || !m_sortValid || sortThreads) {
+ SaveSortInfo(sortType, sortOrder);
+ if (sortType == nsMsgViewSortType::byThread) {
+ m_sortType = sortType;
+ m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay;
+ m_viewFlags &= ~nsMsgViewFlagsType::kGroupBySort;
+ if (m_havePrevView) {
+ // Restore saved id array and flags array.
+ m_keys = m_prevKeys.Clone();
+ m_flags = m_prevFlags.Clone();
+ m_levels = m_prevLevels.Clone();
+ m_sortValid = true;
+
+ // The sort may have changed the number of rows
+ // before we restore the selection, tell the tree
+ // do this before we call restore selection
+ // this is safe when there is no selection.
+ rv = AdjustRowCount(rowCountBeforeSort, GetSize());
+
+ RestoreSelection(preservedKey, preservedSelection);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ return NS_OK;
+ } else {
+ // Set sort info in anticipation of what Init will do.
+ // Build up thread list.
+ int32_t unused; // count.
+ InitThreadedView(unused);
+ if (sortOrder != nsMsgViewSortOrder::ascending)
+ Sort(sortType, sortOrder);
+
+ // The sort may have changed the number of rows
+ // before we update the selection, tell the tree
+ // do this before we call restore selection
+ // this is safe when there is no selection.
+ rv = AdjustRowCount(rowCountBeforeSort, GetSize());
+
+ RestoreSelection(preservedKey, preservedSelection);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ return NS_OK;
+ }
+ } else if (sortType != nsMsgViewSortType::byThread &&
+ (m_sortType == nsMsgViewSortType::byThread || sortThreads)
+ /* && !m_havePrevView*/) {
+ if (sortThreads) {
+ SortThreads(sortType, sortOrder);
+ // Hack so base class won't do anything.
+ sortType = nsMsgViewSortType::byThread;
+ } else {
+ // Going from SortByThread to non-thread sort - must build new key,
+ // level, and flags arrays.
+ m_prevKeys = m_keys.Clone();
+ m_prevFlags = m_flags.Clone();
+ m_prevLevels = m_levels.Clone();
+ // Do this before we sort, so that we'll use the cheap method
+ // of expanding.
+ m_viewFlags &= ~(nsMsgViewFlagsType::kThreadedDisplay |
+ nsMsgViewFlagsType::kGroupBySort);
+ ExpandAll();
+ // m_idArray.RemoveAll();
+ // m_flags.Clear();
+ m_havePrevView = true;
+ }
+ }
+ } else if (m_sortOrder != sortOrder) {
+ // Check for toggling the sort.
+ nsMsgDBView::Sort(sortType, sortOrder);
+ }
+
+ if (!sortThreads) {
+ // Call the base class in case we're not sorting by thread.
+ rv = nsMsgDBView::Sort(sortType, sortOrder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SaveSortInfo(sortType, sortOrder);
+ }
+
+ // The sort may have changed the number of rows
+ // before we restore the selection, tell the tree
+ // do this before we call restore selection
+ // this is safe when there is no selection.
+ rv = AdjustRowCount(rowCountBeforeSort, GetSize());
+
+ RestoreSelection(preservedKey, preservedSelection);
+ if (mTree) mTree->Invalidate();
+ if (mJSTree) mJSTree->Invalidate();
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+void nsMsgThreadedDBView::OnExtraFlagChanged(nsMsgViewIndex index,
+ uint32_t extraFlag) {
+ if (IsValidIndex(index)) {
+ if (m_havePrevView) {
+ nsMsgKey keyChanged = m_keys[index];
+ nsMsgViewIndex prevViewIndex = m_prevKeys.IndexOf(keyChanged);
+ if (prevViewIndex != nsMsgViewIndex_None) {
+ uint32_t prevFlag = m_prevFlags[prevViewIndex];
+ // Don't want to change the elided bit, or has children or is thread.
+ if (prevFlag & nsMsgMessageFlags::Elided)
+ extraFlag |= nsMsgMessageFlags::Elided;
+ else
+ extraFlag &= ~nsMsgMessageFlags::Elided;
+
+ if (prevFlag & MSG_VIEW_FLAG_ISTHREAD)
+ extraFlag |= MSG_VIEW_FLAG_ISTHREAD;
+ else
+ extraFlag &= ~MSG_VIEW_FLAG_ISTHREAD;
+
+ if (prevFlag & MSG_VIEW_FLAG_HASCHILDREN)
+ extraFlag |= MSG_VIEW_FLAG_HASCHILDREN;
+ else
+ extraFlag &= ~MSG_VIEW_FLAG_HASCHILDREN;
+
+ // Will this be right?
+ m_prevFlags[prevViewIndex] = extraFlag;
+ }
+ }
+ }
+
+ // We don't really know what's changed, but to be on the safe side, set the
+ // sort invalid so that reverse sort will pick it up.
+ if (m_sortType == nsMsgViewSortType::byStatus ||
+ m_sortType == nsMsgViewSortType::byFlagged ||
+ m_sortType == nsMsgViewSortType::byUnread ||
+ m_sortType == nsMsgViewSortType::byPriority) {
+ m_sortValid = false;
+ }
+}
+
+void nsMsgThreadedDBView::OnHeaderAddedOrDeleted() { ClearPrevIdArray(); }
+
+void nsMsgThreadedDBView::ClearPrevIdArray() {
+ m_prevKeys.Clear();
+ m_prevLevels.Clear();
+ m_prevFlags.Clear();
+ m_havePrevView = false;
+}
+
+nsresult nsMsgThreadedDBView::InitSort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) {
+ // Nothing to do.
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) return NS_OK;
+
+ if (sortType == nsMsgViewSortType::byThread) {
+ // Sort top level threads by id.
+ nsMsgDBView::Sort(nsMsgViewSortType::byId, sortOrder);
+ m_sortType = nsMsgViewSortType::byThread;
+ m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay;
+ m_viewFlags &= ~nsMsgViewFlagsType::kGroupBySort;
+ // Persist the view flags.
+ SetViewFlags(m_viewFlags);
+ // m_db->SetSortInfo(m_sortType, sortOrder);
+ }
+ // else
+ // m_viewFlags &= ~nsMsgViewFlagsType::kThreadedDisplay;
+
+ // By default, the unread only view should have all threads expanded.
+ if ((m_viewFlags &
+ (nsMsgViewFlagsType::kUnreadOnly | nsMsgViewFlagsType::kExpandAll)) &&
+ (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ ExpandAll();
+ }
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ // For now, expand all and do a flat sort.
+ ExpandAll();
+ }
+
+ Sort(sortType, sortOrder);
+ if (sortType != nsMsgViewSortType::byThread) {
+ // Forget prev view, since it has everything expanded.
+ ClearPrevIdArray();
+ }
+
+ return NS_OK;
+}
+
+nsresult nsMsgThreadedDBView::OnNewHeader(nsIMsgDBHdr* newHdr,
+ nsMsgKey aParentKey,
+ bool ensureListed) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ return nsMsgGroupView::OnNewHeader(newHdr, aParentKey, ensureListed);
+
+ NS_ENSURE_TRUE(newHdr, NS_MSG_MESSAGE_NOT_FOUND);
+
+ nsMsgKey newKey;
+ newHdr->GetMessageKey(&newKey);
+
+ // Views can override this behaviour, which is to append to view.
+ // This is the mail behaviour, but threaded views want
+ // to insert in order...
+ uint32_t msgFlags;
+ newHdr->GetFlags(&msgFlags);
+ if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly && !ensureListed &&
+ msgFlags & nsMsgMessageFlags::Read) {
+ return NS_OK;
+ }
+
+ // Currently, we only add the header in a threaded view if it's a thread.
+ // We used to check if this was the first header in the thread, but that's
+ // a bit harder in the unreadOnly view. But we'll catch it below.
+
+ // If not threaded display just add it to the view.
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ return AddHdr(newHdr);
+
+ // Need to find the thread we added this to so we can change the hasnew flag
+ // added message to existing thread, but not to view.
+ // Fix flags on thread header.
+ int32_t threadCount;
+ uint32_t threadFlags;
+ bool moveThread = false;
+ nsMsgViewIndex threadIndex =
+ ThreadIndexOfMsg(newKey, nsMsgViewIndex_None, &threadCount, &threadFlags);
+ bool threadRootIsDisplayed = false;
+
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ m_db->GetThreadContainingMsgHdr(newHdr, getter_AddRefs(threadHdr));
+ if (threadHdr && m_sortType == nsMsgViewSortType::byDate) {
+ uint32_t newestMsgInThread = 0, msgDate = 0;
+ threadHdr->GetNewestMsgDate(&newestMsgInThread);
+ newHdr->GetDateInSeconds(&msgDate);
+ moveThread = (msgDate == newestMsgInThread);
+ }
+
+ if (threadIndex != nsMsgViewIndex_None) {
+ threadRootIsDisplayed = (m_currentlyDisplayedViewIndex == threadIndex);
+ uint32_t flags = m_flags[threadIndex];
+ if (!(flags & MSG_VIEW_FLAG_HASCHILDREN)) {
+ flags |= MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD;
+ if (!(m_viewFlags & nsMsgViewFlagsType::kUnreadOnly))
+ flags |= nsMsgMessageFlags::Elided;
+
+ m_flags[threadIndex] = flags;
+ }
+
+ if (!(flags & nsMsgMessageFlags::Elided)) {
+ // Thread is expanded.
+ // Insert child into thread.
+ // Levels of other hdrs may have changed!
+ uint32_t newFlags = msgFlags;
+ int32_t level = 0;
+ nsMsgViewIndex insertIndex = threadIndex;
+ if (aParentKey == nsMsgKey_None) {
+ newFlags |= MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN;
+ } else {
+ nsMsgViewIndex parentIndex =
+ FindParentInThread(aParentKey, threadIndex);
+ level = m_levels[parentIndex] + 1;
+ insertIndex = GetInsertInfoForNewHdr(newHdr, parentIndex, level);
+ }
+
+ InsertMsgHdrAt(insertIndex, newHdr, newKey, newFlags, level);
+ // The call to NoteChange() has to happen after we add the key as
+ // NoteChange() will call RowCountChanged() which will call our
+ // GetRowCount().
+ NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete);
+
+ if (aParentKey == nsMsgKey_None) {
+ // this header is the new king! try collapsing the existing thread,
+ // removing it, installing this header as king, and expanding it.
+ CollapseByIndex(threadIndex, nullptr);
+ // call base class, so child won't get promoted.
+ // nsMsgDBView::RemoveByIndex(threadIndex);
+ ExpandByIndex(threadIndex, nullptr);
+ }
+ } else if (aParentKey == nsMsgKey_None) {
+ // if we have a collapsed thread which just got a new
+ // top of thread, change the keys array.
+ m_keys[threadIndex] = newKey;
+ }
+
+ // If this message is new, the thread is collapsed, it is the
+ // root and it was displayed, expand it so that the user does
+ // not find that their message has magically turned into a summary.
+ if (msgFlags & nsMsgMessageFlags::New &&
+ m_flags[threadIndex] & nsMsgMessageFlags::Elided &&
+ threadRootIsDisplayed)
+ ExpandByIndex(threadIndex, nullptr);
+
+ if (moveThread)
+ MoveThreadAt(threadIndex);
+ else
+ // note change, to update the parent thread's unread and total counts
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ } else if (threadHdr) {
+ // Adding msg to thread that's not in view.
+ AddMsgToThreadNotInView(threadHdr, newHdr, ensureListed);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) {
+ // We need to adjust the level of the hdr whose parent changed, and
+ // invalidate that row, iff we're in threaded mode.
+#if 0
+ // This code never runs due to the if (false) and Clang complains about it
+ // so it is ifdefed out for now.
+ if (false && m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)
+ {
+ nsMsgViewIndex childIndex = FindViewIndex(aKeyChanged);
+ if (childIndex != nsMsgViewIndex_None)
+ {
+ nsMsgViewIndex parentIndex = FindViewIndex(newParent);
+ int32_t newParentLevel =
+ (parentIndex == nsMsgViewIndex_None) ? -1 : m_levels[parentIndex];
+
+ nsMsgViewIndex oldParentIndex = FindViewIndex(oldParent);
+
+ int32_t oldParentLevel =
+ (oldParentIndex != nsMsgViewIndex_None ||
+ newParent == nsMsgKey_None) ? m_levels[oldParentIndex] : -1 ;
+
+ int32_t levelChanged = m_levels[childIndex];
+ int32_t parentDelta = oldParentLevel - newParentLevel;
+ m_levels[childIndex] = (newParent == nsMsgKey_None) ? 0 : newParentLevel + 1;
+ if (parentDelta > 0)
+ {
+ for (nsMsgViewIndex viewIndex = childIndex + 1;
+ viewIndex < GetSize() && m_levels[viewIndex] > levelChanged;
+ viewIndex++)
+ {
+ m_levels[viewIndex] = m_levels[viewIndex] - parentDelta;
+ NoteChange(viewIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+
+ NoteChange(childIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+#endif
+ return NS_OK;
+}
+
+nsMsgViewIndex nsMsgThreadedDBView::GetInsertInfoForNewHdr(
+ nsIMsgDBHdr* newHdr, nsMsgViewIndex parentIndex, int32_t targetLevel) {
+ uint32_t viewSize = GetSize();
+ while (++parentIndex < viewSize) {
+ // Loop until we find a message at a level less than or equal to the
+ // parent level
+ if (m_levels[parentIndex] < targetLevel) break;
+ }
+
+ return parentIndex;
+}
+
+// This method removes the thread at threadIndex from the view
+// and puts it back in its new position, determined by the sort order.
+// And, if the selection is affected, save and restore the selection.
+void nsMsgThreadedDBView::MoveThreadAt(nsMsgViewIndex threadIndex) {
+ // We need to check if the thread is collapsed or not...
+ // We want to turn off tree notifications so that we don't
+ // reload the current message.
+ // We also need to invalidate the range between where the thread was
+ // and where it ended up.
+ bool changesDisabled = mSuppressChangeNotification;
+ if (!changesDisabled) SetSuppressChangeNotifications(true);
+
+ nsCOMPtr<nsIMsgDBHdr> threadHdr;
+
+ GetMsgHdrForViewIndex(threadIndex, getter_AddRefs(threadHdr));
+ int32_t childCount = 0;
+
+ nsMsgKey preservedKey;
+ AutoTArray<nsMsgKey, 1> preservedSelection;
+ int32_t selectionCount;
+ int32_t currentIndex;
+ bool hasSelection =
+ mTreeSelection &&
+ ((NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(&currentIndex)) &&
+ currentIndex >= 0 && (uint32_t)currentIndex < GetSize()) ||
+ (NS_SUCCEEDED(mTreeSelection->GetRangeCount(&selectionCount)) &&
+ selectionCount > 0));
+ if (hasSelection) SaveAndClearSelection(&preservedKey, preservedSelection);
+
+ uint32_t saveFlags = m_flags[threadIndex];
+ bool threadIsExpanded = !(saveFlags & nsMsgMessageFlags::Elided);
+
+ if (threadIsExpanded) {
+ ExpansionDelta(threadIndex, &childCount);
+ childCount = -childCount;
+ }
+
+ nsTArray<nsMsgKey> threadKeys;
+ nsTArray<uint32_t> threadFlags;
+ nsTArray<uint8_t> threadLevels;
+
+ if (threadIsExpanded) {
+ threadKeys.SetCapacity(childCount);
+ threadFlags.SetCapacity(childCount);
+ threadLevels.SetCapacity(childCount);
+ for (nsMsgViewIndex index = threadIndex + 1;
+ index < GetSize() && m_levels[index]; index++) {
+ threadKeys.AppendElement(m_keys[index]);
+ threadFlags.AppendElement(m_flags[index]);
+ threadLevels.AppendElement(m_levels[index]);
+ }
+
+ uint32_t collapseCount;
+ CollapseByIndex(threadIndex, &collapseCount);
+ }
+
+ nsMsgDBView::RemoveByIndex(threadIndex);
+ nsMsgViewIndex newIndex = nsMsgViewIndex_None;
+ AddHdr(threadHdr, &newIndex);
+
+ // AddHdr doesn't always set newIndex, and getting it to do so
+ // is going to require some refactoring.
+ if (newIndex == nsMsgViewIndex_None) newIndex = FindHdr(threadHdr);
+
+ if (threadIsExpanded) {
+ m_keys.InsertElementsAt(newIndex + 1, threadKeys);
+ m_flags.InsertElementsAt(newIndex + 1, threadFlags);
+ m_levels.InsertElementsAt(newIndex + 1, threadLevels);
+ }
+
+ if (newIndex == nsMsgViewIndex_None) {
+ NS_WARNING("newIndex=-1 in MoveThreadAt");
+ newIndex = 0;
+ }
+
+ m_flags[newIndex] = saveFlags;
+ // Unfreeze selection.
+ if (hasSelection) RestoreSelection(preservedKey, preservedSelection);
+
+ if (!changesDisabled) SetSuppressChangeNotifications(false);
+
+ nsMsgViewIndex lowIndex = threadIndex < newIndex ? threadIndex : newIndex;
+ nsMsgViewIndex highIndex = lowIndex == threadIndex ? newIndex : threadIndex;
+
+ NoteChange(lowIndex, highIndex - lowIndex + childCount + 1,
+ nsMsgViewNotificationCode::changed);
+}
+
+nsresult nsMsgThreadedDBView::AddMsgToThreadNotInView(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr* msgHdr,
+ bool ensureListed) {
+ nsresult rv = NS_OK;
+ uint32_t threadFlags;
+ threadHdr->GetFlags(&threadFlags);
+ if (!(threadFlags & nsMsgMessageFlags::Ignored)) {
+ bool msgKilled;
+ msgHdr->GetIsKilled(&msgKilled);
+ if (!msgKilled) rv = nsMsgDBView::AddHdr(msgHdr);
+ }
+
+ return rv;
+}
+
+// This method just removes the specified line from the view. It does
+// NOT delete it from the database.
+nsresult nsMsgThreadedDBView::RemoveByIndex(nsMsgViewIndex index) {
+ nsresult rv = NS_OK;
+ int32_t flags;
+
+ if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX;
+
+ OnHeaderAddedOrDeleted();
+
+ flags = m_flags[index];
+
+ if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay))
+ return nsMsgDBView::RemoveByIndex(index);
+
+ nsCOMPtr<nsIMsgThread> threadHdr;
+ GetThreadContainingIndex(index, getter_AddRefs(threadHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ uint32_t numThreadChildren = 0;
+ // If we can't get a thread, it's already deleted and thus has 0 children.
+ if (threadHdr) threadHdr->GetNumChildren(&numThreadChildren);
+
+ // Check if we're the top level msg in the thread, and we're not collapsed.
+ if ((flags & MSG_VIEW_FLAG_ISTHREAD) &&
+ !(flags & nsMsgMessageFlags::Elided) &&
+ (flags & MSG_VIEW_FLAG_HASCHILDREN)) {
+ // Fix flags on thread header - newly promoted message should have
+ // flags set correctly.
+ if (threadHdr) {
+ nsMsgDBView::RemoveByIndex(index);
+ nsCOMPtr<nsIMsgThread> nextThreadHdr;
+ // Above RemoveByIndex may now make index out of bounds.
+ if (IsValidIndex(index) && numThreadChildren > 0) {
+ // unreadOnly
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(msgHdr));
+ if (msgHdr != nullptr) {
+ uint32_t flag = 0;
+ msgHdr->GetFlags(&flag);
+ if (numThreadChildren > 1)
+ flag |= MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN;
+
+ m_flags[index] = flag;
+ m_levels[index] = 0;
+ }
+ }
+ }
+
+ return rv;
+ } else if (!(flags & MSG_VIEW_FLAG_ISTHREAD)) {
+ // We're not deleting the top level msg, but top level msg might be the
+ // only msg in thread now.
+ if (threadHdr && numThreadChildren == 1) {
+ nsMsgKey msgKey;
+ rv = threadHdr->GetChildKeyAt(0, &msgKey);
+ if (NS_SUCCEEDED(rv)) {
+ nsMsgViewIndex threadIndex = FindViewIndex(msgKey);
+ if (IsValidIndex(threadIndex)) {
+ uint32_t flags = m_flags[threadIndex];
+ flags &= ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided |
+ MSG_VIEW_FLAG_HASCHILDREN);
+ m_flags[threadIndex] = flags;
+ NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed);
+ }
+ }
+ }
+
+ return nsMsgDBView::RemoveByIndex(index);
+ }
+
+ // Deleting collapsed thread header is special case. Child will be promoted,
+ // so just tell FE that line changed, not that it was deleted.
+ // Header has already been deleted from thread.
+ if (threadHdr && numThreadChildren > 0) {
+ // Change the id array and flags array to reflect the child header.
+ // If we're not deleting the header, we want the second header,
+ // Otherwise, the first one (which just got promoted).
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(msgHdr));
+ if (msgHdr != nullptr) {
+ msgHdr->GetMessageKey(&m_keys[index]);
+ uint32_t flag = 0;
+ msgHdr->GetFlags(&flag);
+ flag |= MSG_VIEW_FLAG_ISTHREAD;
+
+ // If only hdr in thread (with one about to be deleted).
+ if (numThreadChildren == 1) {
+ // Adjust flags.
+ flag &= ~MSG_VIEW_FLAG_HASCHILDREN;
+ flag &= ~nsMsgMessageFlags::Elided;
+ // Tell FE that thread header needs to be repainted.
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ } else {
+ flag |= MSG_VIEW_FLAG_HASCHILDREN;
+ flag |= nsMsgMessageFlags::Elided;
+ }
+
+ m_flags[index] = flag;
+ mIndicesToNoteChange.RemoveElement(index);
+ } else {
+ NS_ASSERTION(false, "couldn't find thread child");
+ }
+
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+ } else {
+ // We may have deleted a whole, collapsed thread - if so,
+ // ensure that the current index will be noted as changed.
+ if (!mIndicesToNoteChange.Contains(index))
+ mIndicesToNoteChange.AppendElement(index);
+
+ rv = nsMsgDBView::RemoveByIndex(index);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowAllThreads;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgThreadedDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgThreadedDBView* newMsgDBView = new nsMsgThreadedDBView();
+
+ if (!newMsgDBView) return NS_ERROR_OUT_OF_MEMORY;
+
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsMsgThreadedDBView.h b/comm/mailnews/base/src/nsMsgThreadedDBView.h
new file mode 100644
index 0000000000..a33c98a3bf
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgThreadedDBView.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsMsgThreadedDBView_H_
+#define _nsMsgThreadedDBView_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgGroupView.h"
+
+class nsMsgThreadedDBView : public nsMsgGroupView {
+ public:
+ nsMsgThreadedDBView();
+ virtual ~nsMsgThreadedDBView();
+
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCommandUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD Close() override;
+ NS_IMETHOD Sort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder) override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent,
+ nsMsgKey newParent,
+ nsIDBChangeListener* aInstigator) override;
+
+ protected:
+ virtual const char* GetViewName(void) override { return "ThreadedDBView"; }
+ nsresult InitThreadedView(int32_t& count);
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey,
+ bool ensureListed) override;
+ virtual nsresult AddMsgToThreadNotInView(nsIMsgThread* threadHdr,
+ nsIMsgDBHdr* msgHdr,
+ bool ensureListed);
+ nsresult InitSort(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder);
+ virtual nsresult SortThreads(nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder);
+ virtual void OnExtraFlagChanged(nsMsgViewIndex index,
+ uint32_t extraFlag) override;
+ virtual void OnHeaderAddedOrDeleted() override;
+ void ClearPrevIdArray();
+ virtual nsresult RemoveByIndex(nsMsgViewIndex index) override;
+ nsMsgViewIndex GetInsertInfoForNewHdr(nsIMsgDBHdr* newHdr,
+ nsMsgViewIndex threadIndex,
+ int32_t targetLevel);
+ void MoveThreadAt(nsMsgViewIndex threadIndex);
+
+ // these are used to save off the previous view so that bopping back and forth
+ // between two views is quick (e.g., threaded and flat sorted by date).
+ bool m_havePrevView;
+ nsTArray<nsMsgKey> m_prevKeys; // this is used for caching non-threaded view.
+ nsTArray<uint32_t> m_prevFlags;
+ nsTArray<uint8_t> m_prevLevels;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgTxn.cpp b/comm/mailnews/base/src/nsMsgTxn.cpp
new file mode 100644
index 0000000000..0074fc1960
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgTxn.cpp
@@ -0,0 +1,247 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgTxn.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgDatabase.h"
+#include "nsCOMArray.h"
+#include "nsArrayEnumerator.h"
+#include "nsComponentManagerUtils.h"
+#include "nsVariant.h"
+#include "nsIProperty.h"
+#include "nsMsgMessageFlags.h"
+#include "nsIMsgFolder.h"
+
+NS_IMPL_ADDREF(nsMsgTxn)
+NS_IMPL_RELEASE(nsMsgTxn)
+NS_INTERFACE_MAP_BEGIN(nsMsgTxn)
+ NS_INTERFACE_MAP_ENTRY(nsIWritablePropertyBag)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIPropertyBag, nsIWritablePropertyBag)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWritablePropertyBag)
+ NS_INTERFACE_MAP_ENTRY(nsITransaction)
+ NS_INTERFACE_MAP_ENTRY(nsIPropertyBag2)
+ NS_INTERFACE_MAP_ENTRY(nsIWritablePropertyBag2)
+NS_INTERFACE_MAP_END
+
+nsMsgTxn::nsMsgTxn() { m_txnType = 0; }
+
+nsMsgTxn::~nsMsgTxn() {}
+
+nsresult nsMsgTxn::Init() { return NS_OK; }
+
+NS_IMETHODIMP nsMsgTxn::HasKey(const nsAString& name, bool* aResult) {
+ *aResult = mPropertyHash.Get(name, nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgTxn::Get(const nsAString& name, nsIVariant** _retval) {
+ mPropertyHash.Get(name, _retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgTxn::GetProperty(const nsAString& name,
+ nsIVariant** _retval) {
+ return mPropertyHash.Get(name, _retval) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP nsMsgTxn::SetProperty(const nsAString& name, nsIVariant* value) {
+ NS_ENSURE_ARG_POINTER(value);
+ mPropertyHash.InsertOrUpdate(name, value);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgTxn::DeleteProperty(const nsAString& name) {
+ if (!mPropertyHash.Get(name, nullptr)) return NS_ERROR_FAILURE;
+
+ mPropertyHash.Remove(name);
+ return mPropertyHash.Get(name, nullptr) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+//
+// nsMailSimpleProperty class and impl; used for GetEnumerator
+// This is same as nsSimpleProperty but for external API use.
+//
+
+class nsMailSimpleProperty final : public nsIProperty {
+ public:
+ nsMailSimpleProperty(const nsAString& aName, nsIVariant* aValue)
+ : mName(aName), mValue(aValue) {}
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIPROPERTY
+ protected:
+ ~nsMailSimpleProperty() {}
+
+ nsString mName;
+ nsCOMPtr<nsIVariant> mValue;
+};
+
+NS_IMPL_ISUPPORTS(nsMailSimpleProperty, nsIProperty)
+
+NS_IMETHODIMP nsMailSimpleProperty::GetName(nsAString& aName) {
+ aName.Assign(mName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMailSimpleProperty::GetValue(nsIVariant** aValue) {
+ NS_IF_ADDREF(*aValue = mValue);
+ return NS_OK;
+}
+
+// end nsMailSimpleProperty
+
+NS_IMETHODIMP nsMsgTxn::GetEnumerator(nsISimpleEnumerator** _retval) {
+ nsCOMArray<nsIProperty> propertyArray;
+ for (auto iter = mPropertyHash.Iter(); !iter.Done(); iter.Next()) {
+ nsMailSimpleProperty* sprop =
+ new nsMailSimpleProperty(iter.Key(), iter.Data());
+ propertyArray.AppendObject(sprop);
+ }
+ return NS_NewArrayEnumerator(_retval, propertyArray, NS_GET_IID(nsIProperty));
+}
+
+#define IMPL_GETSETPROPERTY_AS(Name, Type) \
+ NS_IMETHODIMP \
+ nsMsgTxn::GetPropertyAs##Name(const nsAString& prop, Type* _retval) { \
+ nsIVariant* v = mPropertyHash.GetWeak(prop); \
+ if (!v) return NS_ERROR_NOT_AVAILABLE; \
+ return v->GetAs##Name(_retval); \
+ } \
+ \
+ NS_IMETHODIMP \
+ nsMsgTxn::SetPropertyAs##Name(const nsAString& prop, Type value) { \
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant(); \
+ var->SetAs##Name(value); \
+ return SetProperty(prop, var); \
+ }
+
+IMPL_GETSETPROPERTY_AS(Int32, int32_t)
+IMPL_GETSETPROPERTY_AS(Uint32, uint32_t)
+IMPL_GETSETPROPERTY_AS(Int64, int64_t)
+IMPL_GETSETPROPERTY_AS(Uint64, uint64_t)
+IMPL_GETSETPROPERTY_AS(Double, double)
+IMPL_GETSETPROPERTY_AS(Bool, bool)
+
+NS_IMETHODIMP nsMsgTxn::GetPropertyAsAString(const nsAString& prop,
+ nsAString& _retval) {
+ nsIVariant* v = mPropertyHash.GetWeak(prop);
+ if (!v) return NS_ERROR_NOT_AVAILABLE;
+ return v->GetAsAString(_retval);
+}
+
+NS_IMETHODIMP nsMsgTxn::GetPropertyAsACString(const nsAString& prop,
+ nsACString& _retval) {
+ nsIVariant* v = mPropertyHash.GetWeak(prop);
+ if (!v) return NS_ERROR_NOT_AVAILABLE;
+ return v->GetAsACString(_retval);
+}
+
+NS_IMETHODIMP nsMsgTxn::GetPropertyAsAUTF8String(const nsAString& prop,
+ nsACString& _retval) {
+ nsIVariant* v = mPropertyHash.GetWeak(prop);
+ if (!v) return NS_ERROR_NOT_AVAILABLE;
+ return v->GetAsAUTF8String(_retval);
+}
+
+NS_IMETHODIMP nsMsgTxn::GetPropertyAsInterface(const nsAString& prop,
+ const nsIID& aIID,
+ void** _retval) {
+ nsIVariant* v = mPropertyHash.GetWeak(prop);
+ if (!v) return NS_ERROR_NOT_AVAILABLE;
+ nsCOMPtr<nsISupports> val;
+ nsresult rv = v->GetAsISupports(getter_AddRefs(val));
+ if (NS_FAILED(rv)) return rv;
+ if (!val) {
+ // We have a value, but it's null
+ *_retval = nullptr;
+ return NS_OK;
+ }
+ return val->QueryInterface(aIID, _retval);
+}
+
+NS_IMETHODIMP nsMsgTxn::SetPropertyAsAString(const nsAString& prop,
+ const nsAString& value) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ var->SetAsAString(value);
+ return SetProperty(prop, var);
+}
+
+NS_IMETHODIMP nsMsgTxn::SetPropertyAsACString(const nsAString& prop,
+ const nsACString& value) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ var->SetAsACString(value);
+ return SetProperty(prop, var);
+}
+
+NS_IMETHODIMP nsMsgTxn::SetPropertyAsAUTF8String(const nsAString& prop,
+ const nsACString& value) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ var->SetAsAUTF8String(value);
+ return SetProperty(prop, var);
+}
+
+NS_IMETHODIMP nsMsgTxn::SetPropertyAsInterface(const nsAString& prop,
+ nsISupports* value) {
+ nsCOMPtr<nsIWritableVariant> var = new nsVariant();
+ var->SetAsISupports(value);
+ return SetProperty(prop, var);
+}
+
+/////////////////////// Transaction Stuff //////////////////
+NS_IMETHODIMP nsMsgTxn::DoTransaction(void) { return NS_OK; }
+
+NS_IMETHODIMP nsMsgTxn::GetIsTransient(bool* aIsTransient) {
+ if (nullptr != aIsTransient)
+ *aIsTransient = false;
+ else
+ return NS_ERROR_NULL_POINTER;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgTxn::Merge(nsITransaction* aTransaction, bool* aDidMerge) {
+ *aDidMerge = false;
+ return NS_OK;
+}
+
+nsresult nsMsgTxn::GetMsgWindow(nsIMsgWindow** msgWindow) {
+ if (!msgWindow || !m_msgWindow) return NS_ERROR_NULL_POINTER;
+ NS_ADDREF(*msgWindow = m_msgWindow);
+ return NS_OK;
+}
+
+nsresult nsMsgTxn::SetMsgWindow(nsIMsgWindow* msgWindow) {
+ m_msgWindow = msgWindow;
+ return NS_OK;
+}
+
+nsresult nsMsgTxn::SetTransactionType(uint32_t txnType) {
+ return SetPropertyAsUint32(u"type"_ns, txnType);
+}
+
+/*none of the callers pass null aFolder,
+ we always initialize aResult (before we pass in) for the case where the key is
+ not in the db*/
+nsresult nsMsgTxn::CheckForToggleDelete(nsIMsgFolder* aFolder,
+ const nsMsgKey& aMsgKey,
+ bool* aResult) {
+ NS_ENSURE_ARG(aResult);
+ nsCOMPtr<nsIMsgDBHdr> message;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = aFolder->GetMsgDatabase(getter_AddRefs(db));
+ if (db) {
+ bool containsKey;
+ rv = db->ContainsKey(aMsgKey, &containsKey);
+ if (NS_FAILED(rv) || !containsKey) // the message has been deleted from db,
+ // so we cannot do toggle here
+ return NS_OK;
+ rv = db->GetMsgHdrForKey(aMsgKey, getter_AddRefs(message));
+ uint32_t flags;
+ if (NS_SUCCEEDED(rv) && message) {
+ message->GetFlags(&flags);
+ *aResult = (flags & nsMsgMessageFlags::IMAPDeleted) != 0;
+ }
+ }
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsMsgTxn.h b/comm/mailnews/base/src/nsMsgTxn.h
new file mode 100644
index 0000000000..824b26993b
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgTxn.h
@@ -0,0 +1,77 @@
+/* -*- 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/. */
+
+#ifndef nsMsgTxn_h__
+#define nsMsgTxn_h__
+
+#include "mozilla/Attributes.h"
+#include "nsITransaction.h"
+#include "msgCore.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgWindow.h"
+#include "nsInterfaceHashtable.h"
+#include "MailNewsTypes2.h"
+#include "nsIVariant.h"
+#include "nsIWritablePropertyBag.h"
+#include "nsIWritablePropertyBag2.h"
+
+#include "mozilla/EditTransactionBase.h"
+
+using mozilla::EditTransactionBase;
+
+#define NS_MESSAGETRANSACTION_IID \
+ { /* da621b30-1efc-11d3-abe4-00805f8ac968 */ \
+ 0xda621b30, 0x1efc, 0x11d3, { \
+ 0xab, 0xe4, 0x00, 0x80, 0x5f, 0x8a, 0xc9, 0x68 \
+ } \
+ }
+/**
+ * base class for all message undo/redo transactions.
+ */
+
+class nsMsgTxn : public nsITransaction,
+ public nsIWritablePropertyBag,
+ public nsIWritablePropertyBag2 {
+ public:
+ nsMsgTxn();
+
+ nsresult Init();
+
+ NS_IMETHOD DoTransaction(void) override;
+
+ NS_IMETHOD UndoTransaction(void) override = 0;
+
+ NS_IMETHOD RedoTransaction(void) override = 0;
+
+ NS_IMETHOD GetIsTransient(bool* aIsTransient) override;
+
+ NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override;
+
+ nsresult GetMsgWindow(nsIMsgWindow** msgWindow);
+ nsresult SetMsgWindow(nsIMsgWindow* msgWindow);
+ nsresult SetTransactionType(uint32_t txnType);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIPROPERTYBAG
+ NS_DECL_NSIPROPERTYBAG2
+ NS_DECL_NSIWRITABLEPROPERTYBAG
+ NS_DECL_NSIWRITABLEPROPERTYBAG2
+
+ NS_IMETHOD GetAsEditTransactionBase(EditTransactionBase**) final {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ protected:
+ virtual ~nsMsgTxn();
+
+ // a hash table of string -> nsIVariant
+ nsInterfaceHashtable<nsStringHashKey, nsIVariant> mPropertyHash;
+ nsCOMPtr<nsIMsgWindow> m_msgWindow;
+ uint32_t m_txnType;
+ nsresult CheckForToggleDelete(nsIMsgFolder* aFolder, const nsMsgKey& aMsgKey,
+ bool* aResult);
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgUtils.cpp b/comm/mailnews/base/src/nsMsgUtils.cpp
new file mode 100644
index 0000000000..661b4fb219
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgUtils.cpp
@@ -0,0 +1,1926 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgUtils.h"
+#include "nsMsgFolderFlags.h"
+#include "nsMsgMessageFlags.h"
+#include "nsString.h"
+#include "nsIServiceManager.h"
+#include "nsCOMPtr.h"
+#include "nsIFolderLookupService.h"
+#include "nsIImapUrl.h"
+#include "nsIMailboxUrl.h"
+#include "nsINntpUrl.h"
+#include "nsMsgI18N.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsCharTraits.h"
+#include "prprf.h"
+#include "prmem.h"
+#include "nsNetCID.h"
+#include "nsIIOService.h"
+#include "nsIMimeConverter.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIPrefLocalizedString.h"
+#include "nsIRelativeFilePref.h"
+#include "mozilla/nsRelativeFilePref.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsISpamSettings.h"
+#include "nsICryptoHash.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIRssIncomingServer.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgProtocolInfo.h"
+#include "nsIMsgMessageService.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIOutputStream.h"
+#include "nsMsgFileStream.h"
+#include "nsIFileURL.h"
+#include "nsNetUtil.h"
+#include "nsProtocolProxyService.h"
+#include "nsIProtocolProxyCallback.h"
+#include "nsICancelable.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIStringBundle.h"
+#include "nsIMsgWindow.h"
+#include "nsIWindowWatcher.h"
+#include "nsIPrompt.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsTextFormatter.h"
+#include "nsIStreamListener.h"
+#include "nsReadLine.h"
+#include "nsILineInputStream.h"
+#include "nsIParserUtils.h"
+#include "nsICharsetConverterManager.h"
+#include "nsIDocumentEncoder.h"
+#include "mozilla/Components.h"
+#include "locale.h"
+#include "nsStringStream.h"
+#include "nsIInputStreamPump.h"
+#include "nsIInputStream.h"
+#include "nsIChannel.h"
+#include "nsIURIMutator.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/EncodingDetector.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Utf8.h"
+#include "mozilla/Buffer.h"
+#include "nsIPromptService.h"
+#include "nsEmbedCID.h"
+
+/* for logging to Error Console */
+#include "nsIScriptError.h"
+#include "nsIConsoleService.h"
+
+// Log an error string to the error console
+// (adapted from nsContentUtils::LogSimpleConsoleError).
+// Flag can indicate error, warning or info.
+NS_MSG_BASE void MsgLogToConsole4(const nsAString& aErrorText,
+ const nsAString& aFilename,
+ uint32_t aLinenumber, uint32_t aFlag) {
+ nsCOMPtr<nsIScriptError> scriptError =
+ do_CreateInstance(NS_SCRIPTERROR_CONTRACTID);
+ if (NS_WARN_IF(!scriptError)) return;
+ nsCOMPtr<nsIConsoleService> console =
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+ if (NS_WARN_IF(!console)) return;
+ if (NS_FAILED(scriptError->Init(aErrorText, aFilename, EmptyString(),
+ aLinenumber, 0, aFlag, "mailnews"_ns, false,
+ false)))
+ return;
+ console->LogMessage(scriptError);
+ return;
+}
+
+using namespace mozilla;
+using namespace mozilla::net;
+
+#define ILLEGAL_FOLDER_CHARS ";#"
+#define ILLEGAL_FOLDER_CHARS_AS_FIRST_LETTER "."
+#define ILLEGAL_FOLDER_CHARS_AS_LAST_LETTER ".~ "
+
+nsresult GetMessageServiceContractIDForURI(const char* uri,
+ nsCString& contractID) {
+ nsresult rv = NS_OK;
+ // Find protocol
+ nsAutoCString uriStr(uri);
+ int32_t pos = uriStr.FindChar(':');
+ if (pos == -1) return NS_ERROR_FAILURE;
+
+ nsAutoCString protocol(StringHead(uriStr, pos));
+
+ if (protocol.EqualsLiteral("file")) {
+ protocol.AssignLiteral("mailbox");
+ }
+ // Build message service contractid
+ contractID = "@mozilla.org/messenger/messageservice;1?type=";
+ contractID += protocol.get();
+
+ return rv;
+}
+
+// Note: This function is also implemented in JS, see MailServices.jsm.
+nsresult GetMessageServiceFromURI(const nsACString& uri,
+ nsIMsgMessageService** aMessageService) {
+ nsresult rv;
+
+ nsAutoCString contractID;
+ rv = GetMessageServiceContractIDForURI(PromiseFlatCString(uri).get(),
+ contractID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMessageService> msgService =
+ do_GetService(contractID.get(), &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ msgService.forget(aMessageService);
+ return rv;
+}
+
+nsresult GetMsgDBHdrFromURI(const nsACString& uri, nsIMsgDBHdr** msgHdr) {
+ nsCOMPtr<nsIMsgMessageService> msgMessageService;
+ nsresult rv =
+ GetMessageServiceFromURI(uri, getter_AddRefs(msgMessageService));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!msgMessageService) return NS_ERROR_FAILURE;
+
+ return msgMessageService->MessageURIToMsgHdr(uri, msgHdr);
+}
+
+// Where should this live? It's a utility used to convert a string priority,
+// e.g., "High, Low, Normal" to an enum.
+// Perhaps we should have an interface that groups together all these
+// utilities...
+nsresult NS_MsgGetPriorityFromString(const char* const priority,
+ nsMsgPriorityValue& outPriority) {
+ if (!priority) return NS_ERROR_NULL_POINTER;
+
+ // Note: Checking the values separately and _before_ the names,
+ // hoping for a much faster match;
+ // Only _drawback_, as "priority" handling is not truly specified:
+ // some software may have the number meanings reversed (1=Lowest) !?
+ if (PL_strchr(priority, '1'))
+ outPriority = nsMsgPriority::highest;
+ else if (PL_strchr(priority, '2'))
+ outPriority = nsMsgPriority::high;
+ else if (PL_strchr(priority, '3'))
+ outPriority = nsMsgPriority::normal;
+ else if (PL_strchr(priority, '4'))
+ outPriority = nsMsgPriority::low;
+ else if (PL_strchr(priority, '5'))
+ outPriority = nsMsgPriority::lowest;
+ else if (PL_strcasestr(priority, "Highest"))
+ outPriority = nsMsgPriority::highest;
+ // Important: "High" must be tested after "Highest" !
+ else if (PL_strcasestr(priority, "High") || PL_strcasestr(priority, "Urgent"))
+ outPriority = nsMsgPriority::high;
+ else if (PL_strcasestr(priority, "Normal"))
+ outPriority = nsMsgPriority::normal;
+ else if (PL_strcasestr(priority, "Lowest"))
+ outPriority = nsMsgPriority::lowest;
+ // Important: "Low" must be tested after "Lowest" !
+ else if (PL_strcasestr(priority, "Low") ||
+ PL_strcasestr(priority, "Non-urgent"))
+ outPriority = nsMsgPriority::low;
+ else
+ // "Default" case gets default value.
+ outPriority = nsMsgPriority::Default;
+
+ return NS_OK;
+}
+
+nsresult NS_MsgGetPriorityValueString(const nsMsgPriorityValue p,
+ nsACString& outValueString) {
+ switch (p) {
+ case nsMsgPriority::highest:
+ outValueString.Assign('1');
+ break;
+ case nsMsgPriority::high:
+ outValueString.Assign('2');
+ break;
+ case nsMsgPriority::normal:
+ outValueString.Assign('3');
+ break;
+ case nsMsgPriority::low:
+ outValueString.Assign('4');
+ break;
+ case nsMsgPriority::lowest:
+ outValueString.Assign('5');
+ break;
+ case nsMsgPriority::none:
+ case nsMsgPriority::notSet:
+ // Note: '0' is a "fake" value; we expect to never be in this case.
+ outValueString.Assign('0');
+ break;
+ default:
+ NS_ASSERTION(false, "invalid priority value");
+ }
+
+ return NS_OK;
+}
+
+nsresult NS_MsgGetUntranslatedPriorityName(const nsMsgPriorityValue p,
+ nsACString& outName) {
+ switch (p) {
+ case nsMsgPriority::highest:
+ outName.AssignLiteral("Highest");
+ break;
+ case nsMsgPriority::high:
+ outName.AssignLiteral("High");
+ break;
+ case nsMsgPriority::normal:
+ outName.AssignLiteral("Normal");
+ break;
+ case nsMsgPriority::low:
+ outName.AssignLiteral("Low");
+ break;
+ case nsMsgPriority::lowest:
+ outName.AssignLiteral("Lowest");
+ break;
+ case nsMsgPriority::none:
+ case nsMsgPriority::notSet:
+ // Note: 'None' is a "fake" value; we expect to never be in this case.
+ outName.AssignLiteral("None");
+ break;
+ default:
+ NS_ASSERTION(false, "invalid priority value");
+ }
+
+ return NS_OK;
+}
+
+/* this used to be XP_StringHash2 from xp_hash.c */
+/* phong's linear congruential hash */
+static uint32_t StringHash(const char* ubuf, int32_t len = -1) {
+ unsigned char* buf = (unsigned char*)ubuf;
+ uint32_t h = 1;
+ unsigned char* end = buf + (len == -1 ? strlen(ubuf) : len);
+ while (buf < end) {
+ h = 0x63c63cd9 * h + 0x9c39c33d + (int32_t)*buf;
+ buf++;
+ }
+ return h;
+}
+
+inline uint32_t StringHash(const nsAutoString& str) {
+ const char16_t* strbuf = str.get();
+ return StringHash(reinterpret_cast<const char*>(strbuf), str.Length() * 2);
+}
+
+/* Utility functions used in a few places in mailnews */
+int32_t MsgFindCharInSet(const nsCString& aString, const char* aChars,
+ uint32_t aOffset) {
+ return aString.FindCharInSet(aChars, aOffset);
+}
+
+int32_t MsgFindCharInSet(const nsString& aString, const char16_t* aChars,
+ uint32_t aOffset) {
+ return aString.FindCharInSet(aChars, aOffset);
+}
+
+static bool ConvertibleToNative(const nsAutoString& str) {
+ nsAutoCString native;
+ nsAutoString roundTripped;
+ NS_CopyUnicodeToNative(str, native);
+ NS_CopyNativeToUnicode(native, roundTripped);
+ return str.Equals(roundTripped);
+}
+
+#if defined(XP_UNIX)
+const static uint32_t MAX_LEN = 55;
+#elif defined(XP_WIN)
+const static uint32_t MAX_LEN = 55;
+#else
+# error need_to_define_your_max_filename_length
+#endif
+
+nsresult NS_MsgHashIfNecessary(nsAutoCString& name) {
+ if (name.IsEmpty()) return NS_OK; // Nothing to do.
+ nsAutoCString str(name);
+
+ // Given a filename, make it safe for filesystem
+ // certain filenames require hashing because they
+ // are too long or contain illegal characters
+ int32_t illegalCharacterIndex = MsgFindCharInSet(
+ str, FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS ILLEGAL_FOLDER_CHARS, 0);
+
+ // Need to check the first ('.') and last ('.', '~' and ' ') char
+ if (illegalCharacterIndex == -1) {
+ int32_t lastIndex = str.Length() - 1;
+ if (nsLiteralCString(ILLEGAL_FOLDER_CHARS_AS_FIRST_LETTER)
+ .FindChar(str[0]) != -1)
+ illegalCharacterIndex = 0;
+ else if (nsLiteralCString(ILLEGAL_FOLDER_CHARS_AS_LAST_LETTER)
+ .FindChar(str[lastIndex]) != -1)
+ illegalCharacterIndex = lastIndex;
+ else
+ illegalCharacterIndex = -1;
+ }
+
+ char hashedname[MAX_LEN + 1];
+ if (illegalCharacterIndex == -1) {
+ // no illegal chars, it's just too long
+ // keep the initial part of the string, but hash to make it fit
+ if (str.Length() > MAX_LEN) {
+ PL_strncpy(hashedname, str.get(), MAX_LEN + 1);
+ PR_snprintf(hashedname + MAX_LEN - 8, 9, "%08lx",
+ (unsigned long)StringHash(str.get()));
+ name = hashedname;
+ }
+ } else {
+ // found illegal chars, hash the whole thing
+ // if we do substitution, then hash, two strings
+ // could hash to the same value.
+ // for example, on mac: "foo__bar", "foo:_bar", "foo::bar"
+ // would map to "foo_bar". this way, all three will map to
+ // different values
+ PR_snprintf(hashedname, 9, "%08lx", (unsigned long)StringHash(str.get()));
+ name = hashedname;
+ }
+
+ return NS_OK;
+}
+
+// XXX : The number of UTF-16 2byte code units are half the number of
+// bytes in legacy encodings for CJK strings and non-Latin1 in UTF-8.
+// The ratio can be 1/3 for CJK strings in UTF-8. However, we can
+// get away with using the same MAX_LEN for nsCString and nsString
+// because MAX_LEN is defined rather conservatively in the first place.
+nsresult NS_MsgHashIfNecessary(nsAutoString& name) {
+ if (name.IsEmpty()) return NS_OK; // Nothing to do.
+ int32_t illegalCharacterIndex = MsgFindCharInSet(
+ name,
+ u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS ILLEGAL_FOLDER_CHARS, 0);
+
+ // Need to check the first ('.') and last ('.', '~' and ' ') char
+ if (illegalCharacterIndex == -1) {
+ int32_t lastIndex = name.Length() - 1;
+ if (NS_LITERAL_STRING_FROM_CSTRING(ILLEGAL_FOLDER_CHARS_AS_FIRST_LETTER)
+ .FindChar(name[0]) != -1)
+ illegalCharacterIndex = 0;
+ else if (NS_LITERAL_STRING_FROM_CSTRING(ILLEGAL_FOLDER_CHARS_AS_LAST_LETTER)
+ .FindChar(name[lastIndex]) != -1)
+ illegalCharacterIndex = lastIndex;
+ else
+ illegalCharacterIndex = -1;
+ }
+
+ char hashedname[9];
+ int32_t keptLength = -1;
+ if (illegalCharacterIndex != -1)
+ keptLength = illegalCharacterIndex;
+ else if (!ConvertibleToNative(name))
+ keptLength = 0;
+ else if (name.Length() > MAX_LEN) {
+ keptLength = MAX_LEN - 8;
+ // To avoid keeping only the high surrogate of a surrogate pair
+ if (NS_IS_HIGH_SURROGATE(name.CharAt(keptLength - 1))) --keptLength;
+ }
+
+ if (keptLength >= 0) {
+ PR_snprintf(hashedname, 9, "%08lx", (unsigned long)StringHash(name));
+ name.SetLength(keptLength);
+ name.Append(NS_ConvertASCIItoUTF16(hashedname));
+ }
+
+ return NS_OK;
+}
+
+nsresult FormatFileSize(int64_t size, bool useKB, nsAString& formattedSize) {
+ const char* sizeAbbrNames[] = {
+ "byteAbbreviation2", "kiloByteAbbreviation2", "megaByteAbbreviation2",
+ "gigaByteAbbreviation2", "teraByteAbbreviation2", "petaByteAbbreviation2",
+ };
+
+ nsresult rv;
+
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleSvc, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleSvc->CreateBundle("chrome://messenger/locale/messenger.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ double unitSize = size < 0 ? 0.0 : size;
+ uint32_t unitIndex = 0;
+
+ if (useKB) {
+ // Start by formatting in kilobytes
+ unitSize /= 1024;
+ if (unitSize < 0.1 && unitSize != 0) unitSize = 0.1;
+ unitIndex++;
+ }
+
+ // Convert to next unit if it needs 4 digits (after rounding), but only if
+ // we know the name of the next unit
+ while ((unitSize >= 999.5) && (unitIndex < ArrayLength(sizeAbbrNames) - 1)) {
+ unitSize /= 1024;
+ unitIndex++;
+ }
+
+ // Grab the string for the appropriate unit
+ nsString sizeAbbr;
+ rv = bundle->GetStringFromName(sizeAbbrNames[unitIndex], sizeAbbr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get rid of insignificant bits by truncating to 1 or 0 decimal points
+ // 0.1 -> 0.1; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
+ nsTextFormatter::ssprintf(
+ formattedSize, sizeAbbr.get(),
+ (unitIndex != 0) && (unitSize < 99.95 && unitSize != 0) ? 1 : 0,
+ unitSize);
+
+ int32_t separatorPos = formattedSize.FindChar('.');
+ if (separatorPos != kNotFound) {
+ // The ssprintf returned a decimal number using a dot (.) as the decimal
+ // separator. Now we try to localize the separator.
+ // Try to get the decimal separator from the system's locale.
+ char* decimalPoint;
+#ifdef HAVE_LOCALECONV
+ struct lconv* locale = localeconv();
+ decimalPoint = locale->decimal_point;
+#else
+ decimalPoint = getenv("LOCALE_DECIMAL_POINT");
+#endif
+ NS_ConvertUTF8toUTF16 decimalSeparator(decimalPoint);
+ if (decimalSeparator.IsEmpty()) decimalSeparator.Assign('.');
+
+ formattedSize.Replace(separatorPos, 1, decimalSeparator);
+ }
+
+ return NS_OK;
+}
+
+nsresult NS_MsgCreatePathStringFromFolderURI(const char* aFolderURI,
+ nsCString& aPathCString,
+ const nsCString& aScheme,
+ bool aIsNewsFolder) {
+ // A file name has to be in native charset. Here we convert
+ // to UTF-16 and check for 'unsafe' characters before converting
+ // to native charset.
+ NS_ENSURE_TRUE(mozilla::IsUtf8(nsDependentCString(aFolderURI)),
+ NS_ERROR_UNEXPECTED);
+ NS_ConvertUTF8toUTF16 oldPath(aFolderURI);
+
+ nsAutoString pathPiece, path;
+
+ int32_t startSlashPos = oldPath.FindChar('/');
+ int32_t endSlashPos = (startSlashPos >= 0)
+ ? oldPath.FindChar('/', startSlashPos + 1) - 1
+ : oldPath.Length() - 1;
+ if (endSlashPos < 0) endSlashPos = oldPath.Length();
+#if defined(XP_UNIX) || defined(XP_MACOSX)
+ bool isLocalUri = aScheme.EqualsLiteral("none") ||
+ aScheme.EqualsLiteral("pop3") ||
+ aScheme.EqualsLiteral("rss");
+#endif
+ // trick to make sure we only add the path to the first n-1 folders
+ bool haveFirst = false;
+ while (startSlashPos != -1) {
+ pathPiece.Assign(
+ Substring(oldPath, startSlashPos + 1, endSlashPos - startSlashPos));
+ // skip leading '/' (and other // style things)
+ if (!pathPiece.IsEmpty()) {
+ // add .sbd onto the previous path
+ if (haveFirst) {
+ path.AppendLiteral(FOLDER_SUFFIX "/");
+ }
+
+ if (aIsNewsFolder) {
+ nsAutoCString tmp;
+ CopyUTF16toMUTF7(pathPiece, tmp);
+ CopyASCIItoUTF16(tmp, pathPiece);
+ }
+#if defined(XP_UNIX) || defined(XP_MACOSX)
+ // Don't hash path pieces because local mail folder uri's have already
+ // been hashed. We're only doing this on the mac to limit potential
+ // regressions.
+ if (!isLocalUri)
+#endif
+ NS_MsgHashIfNecessary(pathPiece);
+ path += pathPiece;
+ haveFirst = true;
+ }
+ // look for the next slash
+ startSlashPos = endSlashPos + 1;
+
+ endSlashPos = (startSlashPos >= 0)
+ ? oldPath.FindChar('/', startSlashPos + 1) - 1
+ : oldPath.Length() - 1;
+ if (endSlashPos < 0) endSlashPos = oldPath.Length();
+
+ if (startSlashPos >= endSlashPos) break;
+ }
+ return NS_CopyUnicodeToNative(path, aPathCString);
+}
+
+bool NS_MsgStripRE(const nsCString& subject, nsCString& modifiedSubject) {
+ bool result = false;
+
+ // Get localizedRe pref.
+ nsresult rv;
+ nsString utf16LocalizedRe;
+ NS_GetLocalizedUnicharPreferenceWithDefault(nullptr, "mailnews.localizedRe",
+ EmptyString(), utf16LocalizedRe);
+ NS_ConvertUTF16toUTF8 localizedRe(utf16LocalizedRe);
+
+ // Hardcoded "Re" so that no one can configure Mozilla standards incompatible.
+ nsAutoCString checkString("Re,RE,re,rE");
+ if (!localizedRe.IsEmpty()) {
+ checkString.Append(',');
+ checkString.Append(localizedRe);
+ }
+
+ // Decode the string.
+ nsCString decodedString;
+ nsCOMPtr<nsIMimeConverter> mimeConverter;
+ // We cannot strip "Re:" for RFC2047-encoded subject without modifying the
+ // original.
+ if (subject.Find("=?") != kNotFound) {
+ mimeConverter =
+ do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv);
+ if (NS_SUCCEEDED(rv))
+ rv = mimeConverter->DecodeMimeHeaderToUTF8(subject, nullptr, false, true,
+ decodedString);
+ }
+
+ const char *s, *s_end;
+ if (decodedString.IsEmpty()) {
+ s = subject.BeginReading();
+ s_end = s + subject.Length();
+ } else {
+ s = decodedString.BeginReading();
+ s_end = s + decodedString.Length();
+ }
+
+AGAIN:
+ while (s < s_end && IS_SPACE(*s)) s++;
+
+ const char* tokPtr = checkString.get();
+ while (*tokPtr) {
+ // Tokenize the comma separated list.
+ size_t tokenLength = 0;
+ while (*tokPtr && *tokPtr != ',') {
+ tokenLength++;
+ tokPtr++;
+ }
+ // Check if the beginning of s is the actual token.
+ if (tokenLength && !strncmp(s, tokPtr - tokenLength, tokenLength)) {
+ if (s[tokenLength] == ':') {
+ s = s + tokenLength + 1; /* Skip over "Re:" */
+ result = true; /* Yes, we stripped it. */
+ goto AGAIN; /* Skip whitespace and try again. */
+ } else if (s[tokenLength] == '[' || s[tokenLength] == '(') {
+ const char* s2 = s + tokenLength + 1; /* Skip over "Re[" */
+
+ // Skip forward over digits after the "[".
+ while (s2 < (s_end - 2) && isdigit((unsigned char)*s2)) s2++;
+
+ // Now ensure that the following thing is "]:".
+ // Only if it is do we alter `s`.
+ if ((s2[0] == ']' || s2[0] == ')') && s2[1] == ':') {
+ s = s2 + 2; /* Skip over "]:" */
+ result = true; /* Yes, we stripped it. */
+ goto AGAIN; /* Skip whitespace and try again. */
+ }
+ }
+ }
+ if (*tokPtr) tokPtr++;
+ }
+
+ // If we didn't strip anything, we can return here.
+ if (!result) return false;
+
+ if (decodedString.IsEmpty()) {
+ // We didn't decode anything, so just return a new string.
+ modifiedSubject.Assign(s);
+ return true;
+ }
+
+ // We decoded the string, so we need to encode it again. We always encode in
+ // UTF-8.
+ mimeConverter->EncodeMimePartIIStr_UTF8(
+ nsDependentCString(s), false, sizeof("Subject:"),
+ nsIMimeConverter::MIME_ENCODED_WORD_SIZE, modifiedSubject);
+ return true;
+}
+
+/* Very similar to strdup except it free's too
+ */
+char* NS_MsgSACopy(char** destination, const char* source) {
+ if (*destination) {
+ PR_Free(*destination);
+ *destination = 0;
+ }
+ if (!source)
+ *destination = nullptr;
+ else {
+ *destination = (char*)PR_Malloc(PL_strlen(source) + 1);
+ if (*destination == nullptr) return (nullptr);
+
+ PL_strcpy(*destination, source);
+ }
+ return *destination;
+}
+
+/* Again like strdup but it concatenates and free's and uses Realloc.
+ */
+char* NS_MsgSACat(char** destination, const char* source) {
+ if (source && *source) {
+ int destLength = *destination ? PL_strlen(*destination) : 0;
+ char* newDestination =
+ (char*)PR_Realloc(*destination, destLength + PL_strlen(source) + 1);
+ if (newDestination == nullptr) return nullptr;
+
+ *destination = newDestination;
+ PL_strcpy(*destination + destLength, source);
+ }
+ return *destination;
+}
+
+nsresult NS_MsgEscapeEncodeURLPath(const nsAString& aStr, nsCString& aResult) {
+ return MsgEscapeString(NS_ConvertUTF16toUTF8(aStr),
+ nsINetUtil::ESCAPE_URL_PATH, aResult);
+}
+
+nsresult NS_MsgDecodeUnescapeURLPath(const nsACString& aPath,
+ nsAString& aResult) {
+ nsAutoCString unescapedName;
+ MsgUnescapeString(
+ aPath,
+ nsINetUtil::ESCAPE_URL_FILE_BASENAME | nsINetUtil::ESCAPE_URL_FORCED,
+ unescapedName);
+ CopyUTF8toUTF16(unescapedName, aResult);
+ return NS_OK;
+}
+
+bool WeAreOffline() {
+ bool offline = false;
+
+ nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service();
+ if (ioService) ioService->GetOffline(&offline);
+
+ return offline;
+}
+
+// Find a folder by URL. If it doesn't exist, null will be returned
+// via aFolder.
+nsresult FindFolder(const nsACString& aFolderURI, nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ *aFolder = nullptr;
+
+ nsresult rv;
+ nsCOMPtr<nsIFolderLookupService> fls(do_GetService(NSIFLS_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // GetFolderForURL returns NS_OK and null for non-existent folders
+ rv = fls->GetFolderForURL(aFolderURI, aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
+// Fetch an existing folder by URL
+// The returned aFolder will be non-null if and only if result is NS_OK.
+// NS_OK - folder was found
+// NS_MSG_FOLDER_MISSING - if aFolderURI not found
+nsresult GetExistingFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder) {
+ nsresult rv = FindFolder(aFolderURI, aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return *aFolder ? NS_OK : NS_MSG_ERROR_FOLDER_MISSING;
+}
+
+nsresult GetOrCreateFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder) {
+ NS_ENSURE_ARG_POINTER(aFolder);
+
+ *aFolder = nullptr;
+
+ nsresult rv;
+ nsCOMPtr<nsIFolderLookupService> fls(do_GetService(NSIFLS_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = fls->GetOrCreateFolderForURL(aFolderURI, aFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return *aFolder ? NS_OK : NS_ERROR_FAILURE;
+}
+
+bool IsAFromSpaceLine(char* start, const char* end) {
+ bool rv = false;
+ while ((start < end) && (*start == '>')) start++;
+ // If the leading '>'s are followed by an 'F' then we have a possible case
+ // here.
+ if ((*start == 'F') && (end - start > 4) && !strncmp(start, "From ", 5))
+ rv = true;
+ return rv;
+}
+
+//
+// This function finds all lines starting with "From " or "From " preceding
+// with one or more '>' (ie, ">From", ">>From", etc) in the input buffer
+// (between 'start' and 'end') and prefix them with a ">" .
+//
+nsresult EscapeFromSpaceLine(nsIOutputStream* outputStream, char* start,
+ const char* end) {
+ nsresult rv;
+ char* pChar;
+ uint32_t written;
+
+ pChar = start;
+ while (start < end) {
+ while ((pChar < end) && (*pChar != '\r') && ((pChar + 1) < end) &&
+ (*(pChar + 1) != '\n'))
+ pChar++;
+ if ((pChar + 1) == end) pChar++;
+
+ if (pChar < end) {
+ // Found a line so check if it's a qualified "From " line.
+ if (IsAFromSpaceLine(start, pChar)) {
+ rv = outputStream->Write(">", 1, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ int32_t lineTerminatorCount = (*(pChar + 1) == '\n') ? 2 : 1;
+ rv = outputStream->Write(start, pChar - start + lineTerminatorCount,
+ &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ pChar += lineTerminatorCount;
+ start = pChar;
+ } else if (start < end) {
+ // Check and flush out the remaining data and we're done.
+ if (IsAFromSpaceLine(start, end)) {
+ rv = outputStream->Write(">", 1, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ rv = outputStream->Write(start, end - start, &written);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult IsRFC822HeaderFieldName(const char* aHdr, bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aHdr);
+ NS_ENSURE_ARG_POINTER(aResult);
+ uint32_t length = strlen(aHdr);
+ for (uint32_t i = 0; i < length; i++) {
+ char c = aHdr[i];
+ if (c < '!' || c == ':' || c > '~') {
+ *aResult = false;
+ return NS_OK;
+ }
+ }
+ *aResult = true;
+ return NS_OK;
+}
+
+// Warning, currently this routine only works for the Junk Folder
+nsresult GetOrCreateJunkFolder(const nsACString& aURI,
+ nsIUrlListener* aListener) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(aURI, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // don't check validity of folder - caller will handle creating it
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ // make sure that folder hierarchy is built so that legitimate parent-child
+ // relationship is established
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!server) return NS_ERROR_UNEXPECTED;
+
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ rv = server->GetMsgFolderFromURI(folder, aURI, getter_AddRefs(msgFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFolder> parent;
+ rv = msgFolder->GetParent(getter_AddRefs(parent));
+ if (NS_FAILED(rv) || !parent) {
+ nsCOMPtr<nsIFile> folderPath;
+ // for local folders, path is to the berkeley mailbox.
+ // for imap folders, path needs to have .msf appended to the name
+ msgFolder->GetFilePath(getter_AddRefs(folderPath));
+
+ nsCOMPtr<nsIMsgProtocolInfo> protocolInfo;
+ rv = server->GetProtocolInfo(getter_AddRefs(protocolInfo));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool isAsyncFolder;
+ rv = protocolInfo->GetFoldersCreatedAsync(&isAsyncFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // if we can't get the path from the folder, then try to create the storage.
+ // for imap, it doesn't matter if the .msf file exists - it still might not
+ // exist on the server, so we should try to create it
+ bool exists = false;
+ if (!isAsyncFolder && folderPath) folderPath->Exists(&exists);
+ if (!exists) {
+ // Hack to work around a localization bug with the Junk Folder.
+ // Please see Bug #270261 for more information...
+ nsString localizedJunkName;
+ msgFolder->GetName(localizedJunkName);
+
+ // force the junk folder name to be Junk so it gets created on disk
+ // correctly...
+ msgFolder->SetName(u"Junk"_ns);
+ msgFolder->SetFlag(nsMsgFolderFlags::Junk);
+ rv = msgFolder->CreateStorageIfMissing(aListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // now restore the localized folder name...
+ msgFolder->SetName(localizedJunkName);
+
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // ugh, I hate this hack
+ // we have to do this (for now)
+ // because imap and local are different (one creates folder asynch, the
+ // other synch) one will notify the listener, one will not. I blame
+ // nsMsgCopy. we should look into making it so no matter what the folder
+ // type we always call the listener this code should move into local
+ // folder's version of CreateStorageIfMissing()
+ if (!isAsyncFolder && aListener) {
+ rv = aListener->OnStartRunningUrl(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aListener->OnStopRunningUrl(nullptr, NS_OK);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+ } else {
+ // if the folder exists, we should set the junk flag on it
+ // which is what the listener will do
+ if (aListener) {
+ rv = aListener->OnStartRunningUrl(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aListener->OnStopRunningUrl(nullptr, NS_OK);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult IsRSSArticle(nsIURI* aMsgURI, bool* aIsRSSArticle) {
+ nsresult rv;
+ *aIsRSSArticle = false;
+
+ nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(aMsgURI, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ nsCString resourceURI;
+ msgUrl->GetUri(resourceURI);
+
+ // get the msg service for this URI
+ nsCOMPtr<nsIMsgMessageService> msgService;
+ rv = GetMessageServiceFromURI(resourceURI, getter_AddRefs(msgService));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check if the message is a feed message, regardless of folder.
+ uint32_t flags;
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = msgService->MessageURIToMsgHdr(resourceURI, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ msgHdr->GetFlags(&flags);
+ if (flags & nsMsgMessageFlags::FeedMsg) {
+ *aIsRSSArticle = true;
+ return rv;
+ }
+
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aMsgURI, &rv);
+ mozilla::Unused << mailnewsUrl;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // get the folder and the server from the msghdr
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = msgHdr->GetFolder(getter_AddRefs(folder));
+ if (NS_SUCCEEDED(rv) && folder) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ folder->GetServer(getter_AddRefs(server));
+ nsCOMPtr<nsIRssIncomingServer> rssServer = do_QueryInterface(server);
+
+ if (rssServer) *aIsRSSArticle = true;
+ }
+
+ return rv;
+}
+
+// digest needs to be a pointer to a DIGEST_LENGTH (16) byte buffer
+nsresult MSGCramMD5(const char* text, int32_t text_len, const char* key,
+ int32_t key_len, unsigned char* digest) {
+ nsresult rv;
+
+ nsAutoCString hash;
+ nsCOMPtr<nsICryptoHash> hasher =
+ do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this code adapted from
+ // http://www.cis.ohio-state.edu/cgi-bin/rfc/rfc2104.html
+
+ char innerPad[65]; /* inner padding - key XORd with innerPad */
+ char outerPad[65]; /* outer padding - key XORd with outerPad */
+ int i;
+ /* if key is longer than 64 bytes reset it to key=MD5(key) */
+ if (key_len > 64) {
+ rv = hasher->Init(nsICryptoHash::MD5);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Update((const uint8_t*)key, key_len);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Finish(false, hash);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ key = hash.get();
+ key_len = DIGEST_LENGTH;
+ }
+
+ /*
+ * the HMAC_MD5 transform looks like:
+ *
+ * MD5(K XOR outerPad, MD5(K XOR innerPad, text))
+ *
+ * where K is an n byte key
+ * innerPad is the byte 0x36 repeated 64 times
+ * outerPad is the byte 0x5c repeated 64 times
+ * and text is the data being protected
+ */
+
+ /* start out by storing key in pads */
+ memset(innerPad, 0, sizeof innerPad);
+ memset(outerPad, 0, sizeof outerPad);
+ memcpy(innerPad, key, key_len);
+ memcpy(outerPad, key, key_len);
+
+ /* XOR key with innerPad and outerPad values */
+ for (i = 0; i < 64; i++) {
+ innerPad[i] ^= 0x36;
+ outerPad[i] ^= 0x5c;
+ }
+ /*
+ * perform inner MD5
+ */
+ nsAutoCString result;
+ rv = hasher->Init(nsICryptoHash::MD5); /* init context for 1st pass */
+ rv = hasher->Update((const uint8_t*)innerPad, 64); /* start with inner pad */
+ rv = hasher->Update((const uint8_t*)text,
+ text_len); /* then text of datagram */
+ rv = hasher->Finish(false, result); /* finish up 1st pass */
+
+ /*
+ * perform outer MD5
+ */
+ hasher->Init(nsICryptoHash::MD5); /* init context for 2nd pass */
+ rv = hasher->Update((const uint8_t*)outerPad, 64); /* start with outer pad */
+ rv = hasher->Update((const uint8_t*)result.get(),
+ 16); /* then results of 1st hash */
+ rv = hasher->Finish(false, result); /* finish up 2nd pass */
+
+ if (result.Length() != DIGEST_LENGTH) return NS_ERROR_UNEXPECTED;
+
+ memcpy(digest, result.get(), DIGEST_LENGTH);
+
+ return rv;
+}
+
+// digest needs to be a pointer to a DIGEST_LENGTH (16) byte buffer
+nsresult MSGApopMD5(const char* text, int32_t text_len, const char* password,
+ int32_t password_len, unsigned char* digest) {
+ nsresult rv;
+ nsAutoCString result;
+
+ nsCOMPtr<nsICryptoHash> hasher =
+ do_CreateInstance("@mozilla.org/security/hash;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Init(nsICryptoHash::MD5);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Update((const uint8_t*)text, text_len);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Update((const uint8_t*)password, password_len);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = hasher->Finish(false, result);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (result.Length() != DIGEST_LENGTH) return NS_ERROR_UNEXPECTED;
+
+ memcpy(digest, result.get(), DIGEST_LENGTH);
+ return rv;
+}
+
+NS_MSG_BASE nsresult NS_GetPersistentFile(const char* relPrefName,
+ const char* absPrefName,
+ const char* dirServiceProp,
+ bool& gotRelPref, nsIFile** aFile,
+ nsIPrefBranch* prefBranch) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ *aFile = nullptr;
+ NS_ENSURE_ARG(relPrefName);
+ NS_ENSURE_ARG(absPrefName);
+ gotRelPref = false;
+
+ nsCOMPtr<nsIPrefBranch> mainBranch;
+ if (!prefBranch) {
+ nsCOMPtr<nsIPrefService> prefService(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (!prefService) return NS_ERROR_FAILURE;
+ prefService->GetBranch(nullptr, getter_AddRefs(mainBranch));
+ if (!mainBranch) return NS_ERROR_FAILURE;
+ prefBranch = mainBranch;
+ }
+
+ nsCOMPtr<nsIFile> localFile;
+
+ // Get the relative first
+ nsCOMPtr<nsIRelativeFilePref> relFilePref;
+ prefBranch->GetComplexValue(relPrefName, NS_GET_IID(nsIRelativeFilePref),
+ getter_AddRefs(relFilePref));
+ if (relFilePref) {
+ relFilePref->GetFile(getter_AddRefs(localFile));
+ NS_ASSERTION(localFile, "An nsIRelativeFilePref has no file.");
+ if (localFile) gotRelPref = true;
+ }
+
+ // If not, get the old absolute
+ if (!localFile) {
+ prefBranch->GetComplexValue(absPrefName, NS_GET_IID(nsIFile),
+ getter_AddRefs(localFile));
+
+ // If not, and given a dirServiceProp, use directory service.
+ if (!localFile && dirServiceProp) {
+ nsCOMPtr<nsIProperties> dirService(
+ do_GetService("@mozilla.org/file/directory_service;1"));
+ if (!dirService) return NS_ERROR_FAILURE;
+ dirService->Get(dirServiceProp, NS_GET_IID(nsIFile),
+ getter_AddRefs(localFile));
+ if (!localFile) return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (localFile) {
+ localFile->Normalize();
+ localFile.forget(aFile);
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_MSG_BASE nsresult NS_SetPersistentFile(const char* relPrefName,
+ const char* absPrefName,
+ nsIFile* aFile,
+ nsIPrefBranch* prefBranch) {
+ NS_ENSURE_ARG(relPrefName);
+ NS_ENSURE_ARG(absPrefName);
+ NS_ENSURE_ARG(aFile);
+
+ nsCOMPtr<nsIPrefBranch> mainBranch;
+ if (!prefBranch) {
+ nsCOMPtr<nsIPrefService> prefService(
+ do_GetService(NS_PREFSERVICE_CONTRACTID));
+ if (!prefService) return NS_ERROR_FAILURE;
+ prefService->GetBranch(nullptr, getter_AddRefs(mainBranch));
+ if (!mainBranch) return NS_ERROR_FAILURE;
+ prefBranch = mainBranch;
+ }
+
+ // Write the absolute for backwards compatibilty's sake.
+ // Or, if aPath is on a different drive than the profile dir.
+ nsresult rv =
+ prefBranch->SetComplexValue(absPrefName, NS_GET_IID(nsIFile), aFile);
+
+ // Write the relative path.
+ nsCOMPtr<nsIRelativeFilePref> relFilePref = new nsRelativeFilePref();
+ mozilla::Unused << relFilePref->SetFile(aFile);
+ mozilla::Unused << relFilePref->SetRelativeToKey(
+ nsLiteralCString(NS_APP_USER_PROFILE_50_DIR));
+
+ nsresult rv2 = prefBranch->SetComplexValue(
+ relPrefName, NS_GET_IID(nsIRelativeFilePref), relFilePref);
+ if (NS_FAILED(rv2) && NS_SUCCEEDED(rv))
+ prefBranch->ClearUserPref(relPrefName);
+
+ return rv;
+}
+
+NS_MSG_BASE nsresult NS_GetUnicharPreferenceWithDefault(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, const nsAString& defValue, nsAString& prefValue) {
+ NS_ENSURE_ARG(prefName);
+
+ nsCOMPtr<nsIPrefBranch> pbr;
+ if (!prefBranch) {
+ pbr = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefBranch = pbr;
+ }
+
+ nsCString valueUtf8;
+ nsresult rv =
+ prefBranch->GetStringPref(prefName, EmptyCString(), 0, valueUtf8);
+ if (NS_SUCCEEDED(rv))
+ CopyUTF8toUTF16(valueUtf8, prefValue);
+ else
+ prefValue = defValue;
+ return NS_OK;
+}
+
+NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreferenceWithDefault(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, const nsAString& defValue, nsAString& prefValue) {
+ NS_ENSURE_ARG(prefName);
+
+ nsCOMPtr<nsIPrefBranch> pbr;
+ if (!prefBranch) {
+ pbr = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefBranch = pbr;
+ }
+
+ nsCOMPtr<nsIPrefLocalizedString> str;
+ nsresult rv = prefBranch->GetComplexValue(
+ prefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(str));
+ if (NS_SUCCEEDED(rv)) {
+ nsString tmpValue;
+ str->ToString(getter_Copies(tmpValue));
+ prefValue.Assign(tmpValue);
+ } else
+ prefValue = defValue;
+ return NS_OK;
+}
+
+NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreference(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, nsAString& prefValue) {
+ NS_ENSURE_ARG_POINTER(prefName);
+
+ nsCOMPtr<nsIPrefBranch> pbr;
+ if (!prefBranch) {
+ pbr = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ prefBranch = pbr;
+ }
+
+ nsCOMPtr<nsIPrefLocalizedString> str;
+ nsresult rv = prefBranch->GetComplexValue(
+ prefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(str));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString tmpValue;
+ str->ToString(getter_Copies(tmpValue));
+ prefValue.Assign(tmpValue);
+ return NS_OK;
+}
+
+void PRTime2Seconds(PRTime prTime, uint32_t* seconds) {
+ *seconds = (uint32_t)(prTime / PR_USEC_PER_SEC);
+}
+
+void PRTime2Seconds(PRTime prTime, int32_t* seconds) {
+ *seconds = (int32_t)(prTime / PR_USEC_PER_SEC);
+}
+
+void Seconds2PRTime(uint32_t seconds, PRTime* prTime) {
+ *prTime = (PRTime)seconds * PR_USEC_PER_SEC;
+}
+
+nsresult GetSummaryFileLocation(nsIFile* fileLocation,
+ nsIFile** summaryLocation) {
+ nsresult rv;
+ nsCOMPtr<nsIFile> newSummaryLocation =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newSummaryLocation->InitWithFile(fileLocation);
+ nsString fileName;
+
+ rv = newSummaryLocation->GetLeafName(fileName);
+ if (NS_FAILED(rv)) return rv;
+
+ fileName.AppendLiteral(SUMMARY_SUFFIX);
+ rv = newSummaryLocation->SetLeafName(fileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ newSummaryLocation.forget(summaryLocation);
+ return NS_OK;
+}
+
+void MsgGenerateNowStr(nsACString& nowStr) {
+ char dateBuf[100];
+ dateBuf[0] = '\0';
+ PRExplodedTime exploded;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
+ PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y",
+ &exploded);
+ nowStr.Assign(dateBuf);
+}
+
+// Gets a special directory and appends the supplied file name onto it.
+nsresult GetSpecialDirectoryWithFileName(const char* specialDirName,
+ const char* fileName,
+ nsIFile** result) {
+ nsresult rv = NS_GetSpecialDirectory(specialDirName, result);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return (*result)->AppendNative(nsDependentCString(fileName));
+}
+
+// Cleans up temp files with matching names
+nsresult MsgCleanupTempFiles(const char* fileName, const char* extension) {
+ nsCOMPtr<nsIFile> tmpFile;
+ nsCString rootName(fileName);
+ rootName.Append('.');
+ rootName.Append(extension);
+ nsresult rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, rootName.get(),
+ getter_AddRefs(tmpFile));
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ int index = 1;
+ bool exists;
+ do {
+ tmpFile->Exists(&exists);
+ if (exists) {
+ tmpFile->Remove(false);
+ nsCString leafName(fileName);
+ leafName.Append('-');
+ leafName.AppendInt(index);
+ leafName.Append('.');
+ leafName.Append(extension);
+ // start with "Picture-1.jpg" after "Picture.jpg" exists
+ tmpFile->SetNativeLeafName(leafName);
+ }
+ } while (exists && index++ < 10000);
+ return NS_OK;
+}
+
+nsresult MsgGetFileStream(nsIFile* file, nsIOutputStream** fileStream) {
+ RefPtr<nsMsgFileStream> newFileStream = new nsMsgFileStream;
+ nsresult rv = newFileStream->InitWithFile(file);
+ NS_ENSURE_SUCCESS(rv, rv);
+ newFileStream.forget(fileStream);
+ return NS_OK;
+}
+
+nsresult MsgNewBufferedFileOutputStream(nsIOutputStream** aResult,
+ nsIFile* aFile, int32_t aIOFlags,
+ int32_t aPerm) {
+ nsCOMPtr<nsIOutputStream> stream;
+ nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), aFile,
+ aIOFlags, aPerm);
+ if (NS_SUCCEEDED(rv))
+ rv = NS_NewBufferedOutputStream(aResult, stream.forget(),
+ FILE_IO_BUFFER_SIZE);
+ return rv;
+}
+
+nsresult MsgNewSafeBufferedFileOutputStream(nsIOutputStream** aResult,
+ nsIFile* aFile, int32_t aIOFlags,
+ int32_t aPerm) {
+ nsCOMPtr<nsIOutputStream> stream;
+ nsresult rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), aFile,
+ aIOFlags, aPerm);
+ if (NS_SUCCEEDED(rv))
+ rv = NS_NewBufferedOutputStream(aResult, stream.forget(),
+ FILE_IO_BUFFER_SIZE);
+ return rv;
+}
+
+bool MsgFindKeyword(const nsCString& keyword, nsCString& keywords,
+ int32_t* aStartOfKeyword, int32_t* aLength) {
+// nsTString_CharT::Find(const nsCString& aString,
+// bool aIgnoreCase=false,
+// int32_t aOffset=0,
+// int32_t aCount=-1 ) const;
+#define FIND_KEYWORD(keywords, keyword, offset) \
+ ((keywords).Find((keyword), (offset)))
+ // 'keyword' is the single keyword we're looking for
+ // 'keywords' is a space delimited list of keywords to be searched,
+ // which may be just a single keyword or even be empty
+ const int32_t kKeywordLen = keyword.Length();
+ const char* start = keywords.BeginReading();
+ const char* end = keywords.EndReading();
+ *aStartOfKeyword = FIND_KEYWORD(keywords, keyword, 0);
+ while (*aStartOfKeyword >= 0) {
+ const char* matchStart = start + *aStartOfKeyword;
+ const char* matchEnd = matchStart + kKeywordLen;
+ // For a real match, matchStart must be the start of keywords or preceded
+ // by a space and matchEnd must be the end of keywords or point to a space.
+ if ((matchStart == start || *(matchStart - 1) == ' ') &&
+ (matchEnd == end || *matchEnd == ' ')) {
+ *aLength = kKeywordLen;
+ return true;
+ }
+ *aStartOfKeyword =
+ FIND_KEYWORD(keywords, keyword, *aStartOfKeyword + kKeywordLen);
+ }
+
+ *aLength = 0;
+ return false;
+#undef FIND_KEYWORD
+}
+
+bool MsgHostDomainIsTrusted(nsCString& host, nsCString& trustedMailDomains) {
+ const char* end;
+ uint32_t hostLen, domainLen;
+ bool domainIsTrusted = false;
+
+ const char* domain = trustedMailDomains.BeginReading();
+ const char* domainEnd = trustedMailDomains.EndReading();
+ const char* hostStart = host.BeginReading();
+ hostLen = host.Length();
+
+ do {
+ // skip any whitespace
+ while (*domain == ' ' || *domain == '\t') ++domain;
+
+ // find end of this domain in the string
+ end = strchr(domain, ',');
+ if (!end) end = domainEnd;
+
+ // to see if the hostname is in the domain, check if the domain
+ // matches the end of the hostname.
+ domainLen = end - domain;
+ if (domainLen && hostLen >= domainLen) {
+ const char* hostTail = hostStart + hostLen - domainLen;
+ if (PL_strncasecmp(domain, hostTail, domainLen) == 0) {
+ // now, make sure either that the hostname is a direct match or
+ // that the hostname begins with a dot.
+ if (hostLen == domainLen || *hostTail == '.' ||
+ *(hostTail - 1) == '.') {
+ domainIsTrusted = true;
+ break;
+ }
+ }
+ }
+
+ domain = end + 1;
+ } while (*end);
+ return domainIsTrusted;
+}
+
+nsresult MsgGetLocalFileFromURI(const nsACString& aUTF8Path, nsIFile** aFile) {
+ nsresult rv;
+ nsCOMPtr<nsIURI> argURI;
+ rv = NS_NewURI(getter_AddRefs(argURI), aUTF8Path);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFileURL> argFileURL(do_QueryInterface(argURI, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> argFile;
+ rv = argFileURL->GetFile(getter_AddRefs(argFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ argFile.forget(aFile);
+ return NS_OK;
+}
+
+NS_MSG_BASE void MsgStripQuotedPrintable(nsCString& aSrc) {
+ // decode quoted printable text in place
+
+ if (aSrc.IsEmpty()) return;
+
+ char* src = aSrc.BeginWriting();
+ char* dest = src;
+ int srcIdx = 0, destIdx = 0;
+
+ while (src[srcIdx] != 0) {
+ // Decode sequence of '=XY' into a character with code XY.
+ if (src[srcIdx] == '=') {
+ if (MsgIsHex((const char*)src + srcIdx + 1, 2)) {
+ // If we got here, we successfully decoded a quoted printable sequence,
+ // so bump each pointer past it and move on to the next char.
+ dest[destIdx++] = MsgUnhex((const char*)src + srcIdx + 1, 2);
+ srcIdx += 3;
+ } else {
+ // If first char after '=' isn't hex check if it's a normal char
+ // or a soft line break. If it's a soft line break, eat the
+ // CR/LF/CRLF.
+ if (src[srcIdx + 1] == '\r' || src[srcIdx + 1] == '\n') {
+ srcIdx++; // soft line break, ignore the '=';
+ if (src[srcIdx] == '\r' || src[srcIdx] == '\n') {
+ srcIdx++;
+ if (src[srcIdx] == '\n') srcIdx++;
+ }
+ } else // The first or second char after '=' isn't hex, just copy the
+ // '='.
+ {
+ dest[destIdx++] = src[srcIdx++];
+ }
+ continue;
+ }
+ } else
+ dest[destIdx++] = src[srcIdx++];
+ }
+
+ dest[destIdx] = src[srcIdx]; // null terminate
+ aSrc.SetLength(destIdx);
+}
+
+NS_MSG_BASE nsresult MsgEscapeString(const nsACString& aStr, uint32_t aType,
+ nsACString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsINetUtil> nu = do_GetService(NS_NETUTIL_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nu->EscapeString(aStr, aType, aResult);
+}
+
+NS_MSG_BASE nsresult MsgUnescapeString(const nsACString& aStr, uint32_t aFlags,
+ nsACString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsINetUtil> nu = do_GetService(NS_NETUTIL_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nu->UnescapeString(aStr, aFlags, aResult);
+}
+
+NS_MSG_BASE nsresult MsgEscapeURL(const nsACString& aStr, uint32_t aFlags,
+ nsACString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsINetUtil> nu = do_GetService(NS_NETUTIL_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return nu->EscapeURL(aStr, aFlags, aResult);
+}
+
+NS_MSG_BASE nsresult
+MsgGetHeadersFromKeys(nsIMsgDatabase* aDB, const nsTArray<nsMsgKey>& aMsgKeys,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& aHeaders) {
+ NS_ENSURE_ARG_POINTER(aDB);
+ aHeaders.Clear();
+ aHeaders.SetCapacity(aMsgKeys.Length());
+
+ for (auto key : aMsgKeys) {
+ // This function silently skips when the key is not found. This is an
+ // expected case.
+ bool hasKey;
+ nsresult rv = aDB->ContainsKey(key, &hasKey);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (hasKey) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = aDB->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ aHeaders.AppendElement(msgHdr);
+ }
+ }
+ return NS_OK;
+}
+
+bool MsgAdvanceToNextLine(const char* buffer, uint32_t& bufferOffset,
+ uint32_t maxBufferOffset) {
+ bool result = false;
+ for (; bufferOffset < maxBufferOffset; bufferOffset++) {
+ if (buffer[bufferOffset] == '\r' || buffer[bufferOffset] == '\n') {
+ bufferOffset++;
+ if (buffer[bufferOffset - 1] == '\r' && buffer[bufferOffset] == '\n')
+ bufferOffset++;
+ result = true;
+ break;
+ }
+ }
+ return result;
+}
+
+NS_MSG_BASE nsresult MsgExamineForProxyAsync(nsIChannel* channel,
+ nsIProtocolProxyCallback* listener,
+ nsICancelable** result) {
+ nsresult rv;
+
+#ifdef DEBUG
+ nsCOMPtr<nsIURI> uri;
+ rv = channel->GetURI(getter_AddRefs(uri));
+ NS_ASSERTION(NS_SUCCEEDED(rv) && uri,
+ "The URI needs to be set before calling the proxy service");
+#endif
+
+ nsCOMPtr<nsIProtocolProxyService> pps =
+ do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return pps->AsyncResolve(channel, 0, listener, nullptr, result);
+}
+
+NS_MSG_BASE nsresult MsgPromptLoginFailed(nsIMsgWindow* aMsgWindow,
+ const nsACString& aHostname,
+ const nsACString& aUsername,
+ const nsAString& aAccountname,
+ int32_t* aResult) {
+ nsCOMPtr<mozIDOMWindowProxy> domWindow;
+ if (aMsgWindow) {
+ aMsgWindow->GetDomWindow(getter_AddRefs(domWindow));
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIPromptService> dlgService(
+ do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIStringBundleService> bundleSvc =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleSvc, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleSvc->CreateBundle("chrome://messenger/locale/messenger.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString message;
+ AutoTArray<nsString, 2> formatStrings;
+ CopyUTF8toUTF16(aHostname, *formatStrings.AppendElement());
+ CopyUTF8toUTF16(aUsername, *formatStrings.AppendElement());
+
+ rv = bundle->FormatStringFromName("mailServerLoginFailed2", formatStrings,
+ message);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString title;
+ if (aAccountname.IsEmpty()) {
+ // Account name may be empty e.g. on a SMTP server.
+ rv = bundle->GetStringFromName("mailServerLoginFailedTitle", title);
+ } else {
+ AutoTArray<nsString, 1> formatStrings = {nsString(aAccountname)};
+ rv = bundle->FormatStringFromName("mailServerLoginFailedTitleWithAccount",
+ formatStrings, title);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString button0;
+ rv = bundle->GetStringFromName("mailServerLoginFailedRetryButton", button0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString button2;
+ rv = bundle->GetStringFromName("mailServerLoginFailedEnterNewPasswordButton",
+ button2);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool dummyValue = false;
+ return dlgService->ConfirmEx(
+ domWindow, title.get(), message.get(),
+ (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) +
+ (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1) +
+ (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_2),
+ button0.get(), nullptr, button2.get(), nullptr, &dummyValue, aResult);
+}
+
+NS_MSG_BASE PRTime MsgConvertAgeInDaysToCutoffDate(int32_t ageInDays) {
+ PRTime now = PR_Now();
+
+ return now - PR_USEC_PER_DAY * ageInDays;
+}
+
+NS_MSG_BASE nsresult
+MsgTermListToString(nsTArray<RefPtr<nsIMsgSearchTerm>> const& aTermList,
+ nsCString& aOutString) {
+ nsresult rv = NS_OK;
+ for (nsIMsgSearchTerm* term : aTermList) {
+ nsAutoCString stream;
+
+ if (aOutString.Length() > 1) aOutString += ' ';
+
+ bool booleanAnd;
+ bool matchAll;
+ term->GetBooleanAnd(&booleanAnd);
+ term->GetMatchAll(&matchAll);
+ if (matchAll) {
+ aOutString += "ALL";
+ continue;
+ } else if (booleanAnd)
+ aOutString += "AND (";
+ else
+ aOutString += "OR (";
+
+ rv = term->GetTermAsString(stream);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ aOutString += stream;
+ aOutString += ')';
+ }
+ return rv;
+}
+
+NS_MSG_BASE uint64_t ParseUint64Str(const char* str) {
+#ifdef XP_WIN
+ {
+ char* endPtr;
+ return _strtoui64(str, &endPtr, 10);
+ }
+#else
+ return strtoull(str, nullptr, 10);
+#endif
+}
+
+NS_MSG_BASE uint64_t MsgUnhex(const char* aHexString, size_t aNumChars) {
+ // Large numbers will not fit into uint64_t.
+ NS_ASSERTION(aNumChars <= 16, "Hex literal too long to convert!");
+
+ uint64_t result = 0;
+ for (size_t i = 0; i < aNumChars; i++) {
+ unsigned char c = aHexString[i];
+ uint8_t digit;
+ if ((c >= '0') && (c <= '9'))
+ digit = (c - '0');
+ else if ((c >= 'a') && (c <= 'f'))
+ digit = ((c - 'a') + 10);
+ else if ((c >= 'A') && (c <= 'F'))
+ digit = ((c - 'A') + 10);
+ else
+ break;
+
+ result = (result << 4) | digit;
+ }
+
+ return result;
+}
+
+NS_MSG_BASE bool MsgIsHex(const char* aHexString, size_t aNumChars) {
+ for (size_t i = 0; i < aNumChars; i++) {
+ if (!isxdigit(aHexString[i])) return false;
+ }
+ return true;
+}
+
+NS_MSG_BASE nsresult MsgStreamMsgHeaders(nsIInputStream* aInputStream,
+ nsIStreamListener* aConsumer) {
+ mozilla::UniquePtr<nsLineBuffer<char>> lineBuffer(new nsLineBuffer<char>);
+
+ nsresult rv;
+
+ nsAutoCString msgHeaders;
+ nsAutoCString curLine;
+
+ bool more = true;
+
+ // We want to NS_ReadLine until we get to a blank line (the end of the
+ // headers)
+ while (more) {
+ rv = NS_ReadLine(aInputStream, lineBuffer.get(), curLine, &more);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (curLine.IsEmpty()) break;
+ msgHeaders.Append(curLine);
+ msgHeaders.AppendLiteral("\r\n");
+ }
+ lineBuffer.reset();
+ nsCOMPtr<nsIStringInputStream> hdrsStream =
+ do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ hdrsStream->SetData(msgHeaders.get(), msgHeaders.Length());
+
+ nsCOMPtr<nsIInputStreamPump> pump;
+ rv = NS_NewInputStreamPump(getter_AddRefs(pump), hdrsStream.forget());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return pump->AsyncRead(aConsumer);
+}
+
+NS_MSG_BASE nsresult MsgDetectCharsetFromFile(nsIFile* aFile,
+ nsACString& aCharset) {
+ // We do the detection in this order:
+ // Check BOM.
+ // If no BOM, run localized detection (Russian, Ukrainian or Japanese).
+ // We need to run this first, since ISO-2022-JP is 7bit ASCII and would be
+ // detected as UTF-8. If ISO-2022-JP not detected, check for UTF-8. If no
+ // UTF-8, but detector detected something, use that, otherwise return an
+ // error.
+ aCharset.Truncate();
+
+ nsresult rv;
+ nsCOMPtr<nsIInputStream> inputStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Check the BOM.
+ char sniffBuf[3];
+ uint32_t numRead;
+ rv = inputStream->Read(sniffBuf, sizeof(sniffBuf), &numRead);
+
+ if (numRead >= 2 && sniffBuf[0] == (char)0xfe && sniffBuf[1] == (char)0xff) {
+ aCharset = "UTF-16BE";
+ } else if (numRead >= 2 && sniffBuf[0] == (char)0xff &&
+ sniffBuf[1] == (char)0xfe) {
+ aCharset = "UTF-16LE";
+ } else if (numRead >= 3 && sniffBuf[0] == (char)0xef &&
+ sniffBuf[1] == (char)0xbb && sniffBuf[2] == (char)0xbf) {
+ aCharset = "UTF-8";
+ }
+ if (!aCharset.IsEmpty()) return NS_OK;
+
+ // Position back to the beginning.
+ nsCOMPtr<nsISeekableStream> seekStream = do_QueryInterface(inputStream);
+ if (seekStream) seekStream->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+
+ // Use detector.
+ mozilla::UniquePtr<mozilla::EncodingDetector> detector =
+ mozilla::EncodingDetector::Create();
+ char buffer[1024];
+ numRead = 0;
+ while (NS_SUCCEEDED(inputStream->Read(buffer, sizeof(buffer), &numRead))) {
+ mozilla::Span<const uint8_t> src =
+ mozilla::AsBytes(mozilla::Span(buffer, numRead));
+ Unused << detector->Feed(src, false);
+ if (numRead == 0) {
+ break;
+ }
+ }
+ Unused << detector->Feed(nullptr, true);
+ auto encoding = detector->Guess(nullptr, true);
+ encoding->Name(aCharset);
+ return NS_OK;
+}
+
+/*
+ * Converts a buffer to plain text. Some conversions may
+ * or may not work with certain end charsets which is why we
+ * need that as an argument to the function. If charset is
+ * unknown or deemed of no importance NULL could be passed.
+ */
+NS_MSG_BASE nsresult ConvertBufToPlainText(nsString& aConBuf, bool formatFlowed,
+ bool formatOutput,
+ bool disallowBreaks) {
+ if (aConBuf.IsEmpty()) return NS_OK;
+
+ int32_t wrapWidth = 72;
+ nsCOMPtr<nsIPrefBranch> pPrefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID));
+
+ if (pPrefBranch) {
+ pPrefBranch->GetIntPref("mailnews.wraplength", &wrapWidth);
+ // Let sanity reign!
+ if (wrapWidth == 0 || wrapWidth > 990)
+ wrapWidth = 990;
+ else if (wrapWidth < 10)
+ wrapWidth = 10;
+ }
+
+ uint32_t converterFlags = nsIDocumentEncoder::OutputPersistNBSP;
+ if (formatFlowed) converterFlags |= nsIDocumentEncoder::OutputFormatFlowed;
+ if (formatOutput) converterFlags |= nsIDocumentEncoder::OutputFormatted;
+ if (disallowBreaks)
+ converterFlags |= nsIDocumentEncoder::OutputDisallowLineBreaking;
+
+ nsCOMPtr<nsIParserUtils> utils = do_GetService(NS_PARSERUTILS_CONTRACTID);
+ return utils->ConvertToPlainText(aConBuf, converterFlags, wrapWidth, aConBuf);
+}
+
+NS_MSG_BASE nsMsgKey msgKeyFromInt(uint32_t aValue) { return aValue; }
+
+NS_MSG_BASE nsMsgKey msgKeyFromInt(uint64_t aValue) {
+ NS_ASSERTION(aValue <= PR_UINT32_MAX, "Msg key value too big!");
+ return aValue;
+}
+
+NS_MSG_BASE uint32_t msgKeyToInt(nsMsgKey aMsgKey) { return (uint32_t)aMsgKey; }
+
+// Helper function to extract a query qualifier.
+nsCString MsgExtractQueryPart(const nsACString& spec,
+ const char* queryToExtract) {
+ nsCString queryPart;
+ int32_t queryIndex = PromiseFlatCString(spec).Find(queryToExtract);
+ if (queryIndex == kNotFound) return queryPart;
+
+ int32_t queryEnd = spec.FindChar('&', queryIndex + 1);
+ if (queryEnd == kNotFound) queryEnd = spec.FindChar('?', queryIndex + 1);
+ if (queryEnd == kNotFound) {
+ // Nothing follows, so return from where the query qualifier started.
+ queryPart.Assign(Substring(spec, queryIndex));
+ } else {
+ // Return the substring that represents the query qualifier.
+ queryPart.Assign(Substring(spec, queryIndex, queryEnd - queryIndex));
+ }
+ return queryPart;
+}
+
+// Helper function to remove query part from URL spec or path.
+void MsgRemoveQueryPart(nsCString& aSpec) {
+ // Sadly the query part can have different forms, these were seen
+ // "in the wild", even with two ?:
+ // /;section=2?part=1.2&filename=A01.JPG
+ // ?section=2?part=1.2&filename=A01.JPG&type=image/jpeg&filename=A01.JPG
+ // ?header=quotebody/;section=2.2?part=1.2.2&filename=lijbmghmkilicioj.png
+ // ?part=1.2&type=image/jpeg&filename=IMG_C0030.jpg
+ // ?header=quotebody&part=1.2&filename=lijbmghmkilicioj.png
+
+ // Truncate path at the first of /; or ?
+ int32_t ind = aSpec.FindChar('?');
+ if (ind != kNotFound) aSpec.SetLength(ind);
+ ind = aSpec.Find("/;");
+ if (ind != kNotFound) aSpec.SetLength(ind);
+}
+
+// Perform C-style string escaping.
+// e.g. "foo\r\n" => "foo\\r\\n"
+// (See also CEscape(), in protobuf, for similar function).
+nsCString CEscapeString(nsACString const& s) {
+ nsCString out;
+ for (size_t i = 0; i < s.Length(); ++i) {
+ char c = s[i];
+ if (c & 0x80) {
+ out.AppendPrintf("\\x%02x", (uint8_t)c);
+ continue;
+ }
+ switch (c) {
+ case '\a':
+ out += "\\a";
+ break;
+ case '\b':
+ out += "\\b";
+ break;
+ case '\f':
+ out += "\\f";
+ break;
+ case '\n':
+ out += "\\n";
+ break;
+ case '\r':
+ out += "\\r";
+ break;
+ case '\t':
+ out += "\\t";
+ break;
+ case '\v':
+ out += "\\v";
+ break;
+ default:
+ if (c < ' ') {
+ out.AppendPrintf("\\x%02x", (uint8_t)c);
+ } else {
+ out += c;
+ }
+ break;
+ }
+ }
+ return out;
+}
+
+nsresult SyncCopyStream(nsIInputStream* src, nsIOutputStream* dest,
+ uint64_t& bytesCopied, size_t bufSize) {
+ mozilla::Buffer<char> buf(bufSize);
+ nsresult rv;
+
+ bytesCopied = 0;
+ while (1) {
+ uint32_t numRead;
+ rv = src->Read(buf.Elements(), buf.Length(), &numRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (numRead == 0) {
+ break; // EOF.
+ }
+ uint32_t pos = 0;
+ while (pos < numRead) {
+ uint32_t n;
+ rv = dest->Write(&buf[pos], numRead - pos, &n);
+ NS_ENSURE_SUCCESS(rv, rv);
+ pos += n;
+ bytesCopied += n;
+ }
+ }
+ return NS_OK;
+}
+
+// Used for "@mozilla.org/network/sync-stream-listener;1".
+already_AddRefed<nsIStreamListener> SyncStreamListenerCreate() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIStreamListener> listener;
+ nsCOMPtr<nsIInputStream> stream;
+ nsresult rv = NS_NewSyncStreamListener(getter_AddRefs(listener),
+ getter_AddRefs(stream));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ return listener.forget();
+}
+
+// Determine if folder1 and folder2 reside on the same server
+nsresult IsOnSameServer(nsIMsgFolder* folder1, nsIMsgFolder* folder2,
+ bool* sameServer) {
+ NS_ENSURE_ARG_POINTER(folder1);
+ NS_ENSURE_ARG_POINTER(folder2);
+ NS_ENSURE_ARG_POINTER(sameServer);
+
+ nsCOMPtr<nsIMsgIncomingServer> server1;
+ nsresult rv = folder1->GetServer(getter_AddRefs(server1));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ nsCOMPtr<nsIMsgIncomingServer> server2;
+ rv = folder2->GetServer(getter_AddRefs(server2));
+ NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER);
+
+ NS_ENSURE_TRUE(server2, NS_ERROR_NULL_POINTER);
+ return server2->Equals(server1, sameServer);
+}
diff --git a/comm/mailnews/base/src/nsMsgUtils.h b/comm/mailnews/base/src/nsMsgUtils.h
new file mode 100644
index 0000000000..6f289d4b37
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgUtils.h
@@ -0,0 +1,462 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _NSMSGUTILS_H
+#define _NSMSGUTILS_H
+
+#include "nsIURL.h"
+#include "nsString.h"
+#include "msgCore.h"
+#include "nsCOMPtr.h"
+#include "MailNewsTypes2.h"
+#include "nsTArray.h"
+#include "nsInterfaceRequestorAgg.h"
+#include "nsILoadGroup.h"
+#include "nsINetUtil.h"
+#include "nsIRequest.h"
+#include "nsILoadInfo.h"
+#include "nsServiceManagerUtils.h"
+#include "nsUnicharUtils.h"
+#include "nsIFile.h"
+
+class nsIChannel;
+class nsIFile;
+class nsIPrefBranch;
+class nsIMsgFolder;
+class nsIMsgMessageService;
+class nsIUrlListener;
+class nsIOutputStream;
+class nsIInputStream;
+class nsIMsgDatabase;
+class nsIProxyInfo;
+class nsIMsgWindow;
+class nsIStreamListener;
+class nsICancelable;
+class nsIProtocolProxyCallback;
+class nsIMsgSearchTerm;
+
+#define FILE_IO_BUFFER_SIZE (16 * 1024)
+#define MSGS_URL "chrome://messenger/locale/messenger.properties"
+
+enum nsDateFormatSelectorComm : long {
+ kDateFormatNone = 0,
+ kDateFormatLong = 1,
+ kDateFormatShort = 2,
+ kDateFormatUnused = 3,
+ kDateFormatWeekday = 4
+};
+
+// These are utility functions that can used throughout the mailnews code
+
+NS_MSG_BASE nsresult GetMessageServiceContractIDForURI(const char* uri,
+ nsCString& contractID);
+
+NS_MSG_BASE nsresult GetMessageServiceFromURI(
+ const nsACString& uri, nsIMsgMessageService** aMessageService);
+
+NS_MSG_BASE nsresult GetMsgDBHdrFromURI(const nsACString& uri,
+ nsIMsgDBHdr** msgHdr);
+
+NS_MSG_BASE nsresult NS_MsgGetPriorityFromString(
+ const char* const priority, nsMsgPriorityValue& outPriority);
+
+NS_MSG_BASE nsresult NS_MsgGetPriorityValueString(const nsMsgPriorityValue p,
+ nsACString& outValueString);
+
+NS_MSG_BASE nsresult NS_MsgGetUntranslatedPriorityName(
+ const nsMsgPriorityValue p, nsACString& outName);
+
+NS_MSG_BASE nsresult NS_MsgHashIfNecessary(nsAutoString& name);
+NS_MSG_BASE nsresult NS_MsgHashIfNecessary(nsAutoCString& name);
+
+NS_MSG_BASE nsresult FormatFileSize(int64_t size, bool useKB,
+ nsAString& formattedSize);
+
+/**
+ * given a folder uri, return the path to folder in the user profile directory.
+ *
+ * @param aFolderURI uri of folder we want the path to, without the scheme
+ * @param[out] aPathString result path string
+ * @param aScheme scheme of the uri
+ * @param[optional] aIsNewsFolder is this a news folder?
+ */
+NS_MSG_BASE nsresult NS_MsgCreatePathStringFromFolderURI(
+ const char* aFolderURI, nsCString& aPathString, const nsCString& aScheme,
+ bool aIsNewsFolder = false);
+
+/**
+ * Given a string and a length, removes any "Re:" strings from the front.
+ * It also deals with that dumbass "Re[2]:" thing that some losing mailers do.
+ *
+ * If mailnews.localizedRe is set, it will also remove localized "Re:" strings.
+ *
+ * @return true if it made a change (in which case the caller should look to
+ * modifiedSubject for the result) and false otherwise (in which
+ * case the caller should look at subject for the result)
+ */
+NS_MSG_BASE bool NS_MsgStripRE(const nsCString& subject,
+ nsCString& modifiedSubject);
+
+NS_MSG_BASE char* NS_MsgSACopy(char** destination, const char* source);
+
+NS_MSG_BASE char* NS_MsgSACat(char** destination, const char* source);
+
+NS_MSG_BASE nsresult NS_MsgEscapeEncodeURLPath(const nsAString& aStr,
+ nsCString& aResult);
+
+NS_MSG_BASE nsresult NS_MsgDecodeUnescapeURLPath(const nsACString& aPath,
+ nsAString& aResult);
+
+NS_MSG_BASE bool WeAreOffline();
+
+// Get a folder by Uri, returning null if it doesn't exist (or if some
+// error occurs). A missing folder is not considered an error.
+NS_MSG_BASE nsresult FindFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder);
+
+// Get a folder by Uri.
+// A missing folder is considered to be an error.
+// Returns a non-null folder if and only if result is NS_OK.
+NS_MSG_BASE nsresult GetExistingFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder);
+
+// Get a folder by Uri, creating it if it doesn't already exist.
+// An error is returned if a folder cannot be found or created.
+// Created folders will be 'dangling' folders (ie not connected to a
+// parent).
+NS_MSG_BASE nsresult GetOrCreateFolder(const nsACString& aFolderURI,
+ nsIMsgFolder** aFolder);
+
+// Escape lines starting with "From ", ">From ", etc. in a buffer.
+NS_MSG_BASE nsresult EscapeFromSpaceLine(nsIOutputStream* ouputStream,
+ char* start, const char* end);
+NS_MSG_BASE bool IsAFromSpaceLine(char* start, const char* end);
+
+NS_MSG_BASE nsresult NS_GetPersistentFile(
+ const char* relPrefName, const char* absPrefName,
+ const char* dirServiceProp, // Can be NULL
+ bool& gotRelPref, nsIFile** aFile, nsIPrefBranch* prefBranch = nullptr);
+
+NS_MSG_BASE nsresult NS_SetPersistentFile(const char* relPrefName,
+ const char* absPrefName,
+ nsIFile* aFile,
+ nsIPrefBranch* prefBranch = nullptr);
+
+NS_MSG_BASE nsresult IsRFC822HeaderFieldName(const char* aHdr, bool* aResult);
+
+NS_MSG_BASE nsresult NS_GetUnicharPreferenceWithDefault(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, const nsAString& defValue, nsAString& prefValue);
+
+NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreferenceWithDefault(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, const nsAString& defValue, nsAString& prefValue);
+
+NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreference(
+ nsIPrefBranch* prefBranch, // can be null, if so uses the root branch
+ const char* prefName, nsAString& prefValue);
+
+/**
+ * this needs a listener, because we might have to create the folder
+ * on the server, and that is asynchronous
+ */
+NS_MSG_BASE nsresult GetOrCreateJunkFolder(const nsACString& aURI,
+ nsIUrlListener* aListener);
+
+// Returns true if the nsIURI is a message under an RSS account
+NS_MSG_BASE nsresult IsRSSArticle(nsIURI* aMsgURI, bool* aIsRSSArticle);
+
+// digest needs to be a pointer to a 16 byte buffer
+#define DIGEST_LENGTH 16
+
+NS_MSG_BASE nsresult MSGCramMD5(const char* text, int32_t text_len,
+ const char* key, int32_t key_len,
+ unsigned char* digest);
+NS_MSG_BASE nsresult MSGApopMD5(const char* text, int32_t text_len,
+ const char* password, int32_t password_len,
+ unsigned char* digest);
+
+// helper functions to convert a 64bits PRTime into a 32bits value (compatible
+// time_t) and vice versa.
+NS_MSG_BASE void PRTime2Seconds(PRTime prTime, uint32_t* seconds);
+NS_MSG_BASE void PRTime2Seconds(PRTime prTime, int32_t* seconds);
+NS_MSG_BASE void Seconds2PRTime(uint32_t seconds, PRTime* prTime);
+// helper function to generate current date+time as a string
+NS_MSG_BASE void MsgGenerateNowStr(nsACString& nowStr);
+
+// Appends the correct summary file extension onto the supplied fileLocation
+// and returns it in summaryLocation.
+NS_MSG_BASE nsresult GetSummaryFileLocation(nsIFile* fileLocation,
+ nsIFile** summaryLocation);
+
+// Gets a special directory and appends the supplied file name onto it.
+NS_MSG_BASE nsresult GetSpecialDirectoryWithFileName(const char* specialDirName,
+ const char* fileName,
+ nsIFile** result);
+
+// cleanup temp files with the given filename and extension, including
+// the consecutive -NNNN ones that we can find. If there are holes, e.g.,
+// <filename>-1-10,12.<extension> exist, but <filename>-11.<extension> does not
+// we'll clean up 1-10. If the leaks are common, I think the gaps will tend to
+// be filled.
+NS_MSG_BASE nsresult MsgCleanupTempFiles(const char* fileName,
+ const char* extension);
+
+NS_MSG_BASE nsresult MsgGetFileStream(nsIFile* file,
+ nsIOutputStream** fileStream);
+
+// Automatically creates an output stream with a suitable buffer
+NS_MSG_BASE nsresult MsgNewBufferedFileOutputStream(nsIOutputStream** aResult,
+ nsIFile* aFile,
+ int32_t aIOFlags = -1,
+ int32_t aPerm = -1);
+
+// Automatically creates an output stream with a suitable buffer, but write to a
+// temporary file first, then rename to aFile
+NS_MSG_BASE nsresult
+MsgNewSafeBufferedFileOutputStream(nsIOutputStream** aResult, nsIFile* aFile,
+ int32_t aIOFlags = -1, int32_t aPerm = -1);
+
+// fills in the position of the passed in keyword in the passed in keyword list
+// and returns false if the keyword isn't present
+NS_MSG_BASE bool MsgFindKeyword(const nsCString& keyword, nsCString& keywords,
+ int32_t* aStartOfKeyword, int32_t* aLength);
+
+NS_MSG_BASE bool MsgHostDomainIsTrusted(nsCString& host,
+ nsCString& trustedMailDomains);
+
+// gets an nsIFile from a UTF-8 file:// path
+NS_MSG_BASE nsresult MsgGetLocalFileFromURI(const nsACString& aUTF8Path,
+ nsIFile** aFile);
+
+NS_MSG_BASE void MsgStripQuotedPrintable(nsCString& aSrc);
+
+/*
+ * Utility functions that call functions from nsINetUtil
+ */
+
+NS_MSG_BASE nsresult MsgEscapeString(const nsACString& aStr, uint32_t aType,
+ nsACString& aResult);
+
+NS_MSG_BASE nsresult MsgUnescapeString(const nsACString& aStr, uint32_t aFlags,
+ nsACString& aResult);
+
+NS_MSG_BASE nsresult MsgEscapeURL(const nsACString& aStr, uint32_t aFlags,
+ nsACString& aResult);
+
+// Given a message db and a set of keys, fetch the corresponding message
+// headers.
+NS_MSG_BASE nsresult
+MsgGetHeadersFromKeys(nsIMsgDatabase* aDB, const nsTArray<nsMsgKey>& aKeys,
+ nsTArray<RefPtr<nsIMsgDBHdr>>& aHeaders);
+
+NS_MSG_BASE nsresult MsgExamineForProxyAsync(nsIChannel* channel,
+ nsIProtocolProxyCallback* listener,
+ nsICancelable** result);
+
+NS_MSG_BASE int32_t MsgFindCharInSet(const nsCString& aString,
+ const char* aChars, uint32_t aOffset = 0);
+NS_MSG_BASE int32_t MsgFindCharInSet(const nsString& aString,
+ const char16_t* aChars,
+ uint32_t aOffset = 0);
+
+// advances bufferOffset to the beginning of the next line, if we don't
+// get to maxBufferOffset first. Returns false if we didn't get to the
+// next line.
+NS_MSG_BASE bool MsgAdvanceToNextLine(const char* buffer,
+ uint32_t& bufferOffset,
+ uint32_t maxBufferOffset);
+
+/**
+ * Alerts the user that the login to the server failed. Asks whether the
+ * connection should: retry, cancel, or request a new password.
+ *
+ * @param aMsgWindow The message window associated with this action (cannot
+ * be null).
+ * @param aHostname The hostname of the server for which the login failed.
+ * @param aResult The button pressed. 0 for retry, 1 for cancel,
+ * 2 for enter a new password.
+ * @return NS_OK for success, NS_ERROR_* if there was a failure in
+ * creating the dialog.
+ */
+NS_MSG_BASE nsresult MsgPromptLoginFailed(nsIMsgWindow* aMsgWindow,
+ const nsACString& aHostname,
+ const nsACString& aUsername,
+ const nsAString& aAccountname,
+ int32_t* aResult);
+
+/**
+ * Calculate a PRTime value used to determine if a date is XX
+ * days ago. This is used by various retention setting algorithms.
+ */
+NS_MSG_BASE PRTime MsgConvertAgeInDaysToCutoffDate(int32_t ageInDays);
+
+/**
+ * Converts the passed in term list to its string representation.
+ *
+ * @param aTermList Array of nsIMsgSearchTerms
+ * @param[out] aOutString result representation of search terms.
+ *
+ */
+NS_MSG_BASE nsresult MsgTermListToString(
+ nsTArray<RefPtr<nsIMsgSearchTerm>> const& aTermList, nsCString& aOutString);
+
+NS_MSG_BASE nsresult MsgStreamMsgHeaders(nsIInputStream* aInputStream,
+ nsIStreamListener* aConsumer);
+
+/**
+ * convert string to uint64_t
+ *
+ * @param str converted string
+ * @returns uint64_t value for success, 0 for parse failure
+ */
+NS_MSG_BASE uint64_t ParseUint64Str(const char* str);
+
+/**
+ * Detect charset of file
+ *
+ * @param aFile The target of nsIFile
+ * @param[out] aCharset The charset string
+ */
+NS_MSG_BASE nsresult MsgDetectCharsetFromFile(nsIFile* aFile,
+ nsACString& aCharset);
+
+/*
+ * Converts a buffer to plain text. Some conversions may
+ * or may not work with certain end charsets which is why we
+ * need that as an argument to the function. If charset is
+ * unknown or deemed of no importance NULL could be passed.
+ * @param[in/out] aConBuf Variable with the text to convert
+ * @param formatFlowed Use format flowed?
+ * @param formatOutput Reformat the output?
+ & @param disallowBreaks Disallow breaks when formatting
+ */
+NS_MSG_BASE nsresult ConvertBufToPlainText(nsString& aConBuf, bool formatFlowed,
+ bool formatOutput,
+ bool disallowBreaks);
+
+#include "nsEscape.h"
+
+/**
+ * Converts a hex string into an integer.
+ * Processes up to aNumChars characters or the first non-hex char.
+ * It is not an error if less than aNumChars valid hex digits are found.
+ */
+NS_MSG_BASE uint64_t MsgUnhex(const char* aHexString, size_t aNumChars);
+
+/**
+ * Checks if a string is a valid hex literal containing at least aNumChars
+ * digits.
+ */
+NS_MSG_BASE bool MsgIsHex(const char* aHexString, size_t aNumChars);
+
+/**
+ * Convert an uint32_t to a nsMsgKey.
+ * Currently they are mostly the same but we need to preserve the notion that
+ * nsMsgKey is an opaque value that can't be treated as a generic integer
+ * (except when storing it into the database). It enables type safety checks and
+ * may prevent coding errors.
+ */
+NS_MSG_BASE nsMsgKey msgKeyFromInt(uint32_t aValue);
+
+NS_MSG_BASE nsMsgKey msgKeyFromInt(uint64_t aValue);
+
+NS_MSG_BASE uint32_t msgKeyToInt(nsMsgKey aMsgKey);
+
+/**
+ * Helper function to extract query part from URL spec.
+ */
+nsCString MsgExtractQueryPart(const nsACString& spec,
+ const char* queryToExtract);
+/**
+ * Helper function to remove query part from URL spec or path.
+ */
+void MsgRemoveQueryPart(nsCString& aSpec);
+
+/**
+ * Helper macro for defining getter/setters. Ported from nsISupportsObsolete.h
+ */
+#define NS_IMPL_GETSET(clazz, attr, type, member) \
+ NS_IMETHODIMP clazz::Get##attr(type* result) { \
+ NS_ENSURE_ARG_POINTER(result); \
+ *result = member; \
+ return NS_OK; \
+ } \
+ NS_IMETHODIMP clazz::Set##attr(type aValue) { \
+ member = aValue; \
+ return NS_OK; \
+ }
+
+/**
+ * Macro and helper function for reporting an error, warning or
+ * informational message to the Error Console
+ *
+ * This will require the inclusion of the following files in the source file
+ * #include "nsIScriptError.h"
+ * #include "nsIConsoleService.h"
+ *
+ */
+
+NS_MSG_BASE
+void MsgLogToConsole4(const nsAString& aErrorText, const nsAString& aFilename,
+ uint32_t aLine, uint32_t flags);
+
+// Macro with filename and line number
+#define MSG_LOG_TO_CONSOLE(_text, _flag) \
+ MsgLogToConsole4(NS_LITERAL_STRING_FROM_CSTRING(_text), \
+ NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, _flag)
+#define MSG_LOG_ERR_TO_CONSOLE(_text) \
+ MSG_LOG_TO_CONSOLE(_text, nsIScriptError::errorFlag)
+#define MSG_LOG_WARN_TO_CONSOLE(_text) \
+ MSG_LOG_TO_CONSOLE(_text, nsIScriptError::warningFlag)
+#define MSG_LOG_INFO_TO_CONSOLE(_text) \
+ MSG_LOG_TO_CONSOLE(_text, nsIScriptError::infoFlag)
+
+// Helper macros to cope with shoddy I/O error reporting (or lack thereof)
+#define MSG_NS_ERROR(_txt) \
+ do { \
+ NS_ERROR(_txt); \
+ MSG_LOG_ERR_TO_CONSOLE(_txt); \
+ } while (0)
+#define MSG_NS_WARNING(_txt) \
+ do { \
+ NS_WARNING(_txt); \
+ MSG_LOG_WARN_TO_CONSOLE(_txt); \
+ } while (0)
+#define MSG_NS_WARN_IF_FALSE(_val, _txt) \
+ do { \
+ if (!(_val)) { \
+ NS_WARNING(_txt); \
+ MSG_LOG_WARN_TO_CONSOLE(_txt); \
+ } \
+ } while (0)
+#define MSG_NS_INFO(_txt) \
+ do { \
+ MSG_LOCAL_INFO_TO_CONSOLE(_txt); \
+ fprintf(stderr, "(info) %s (%s:%d)\n", _txt, __FILE__, __LINE__); \
+ } while (0)
+
+/**
+ * Perform C-style string escaping. E.g. "foo\r\n" => "foo\\r\\n"
+ * This is primarily intended for debuggin purposes.
+ */
+nsCString CEscapeString(nsACString const& s);
+
+/**
+ * Synchronously copy the contents of src to dest, until EOF is encountered
+ * or an error occurs.
+ * The total number of bytes copied is returned in bytesCopied.
+ */
+nsresult SyncCopyStream(nsIInputStream* src, nsIOutputStream* dest,
+ uint64_t& bytesCopied,
+ size_t bufSize = FILE_IO_BUFFER_SIZE);
+
+// Used for "@mozilla.org/network/sync-stream-listener;1".
+already_AddRefed<nsIStreamListener> SyncStreamListenerCreate();
+
+nsresult IsOnSameServer(nsIMsgFolder* folder1, nsIMsgFolder* folder2,
+ bool* sameServer);
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgWindow.cpp b/comm/mailnews/base/src/nsMsgWindow.cpp
new file mode 100644
index 0000000000..1b0129d8f8
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgWindow.cpp
@@ -0,0 +1,327 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsMsgWindow.h"
+#include "nsIURILoader.h"
+#include "nsCURILoader.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "mozIDOMWindow.h"
+#include "nsTransactionManagerCID.h"
+#include "nsIComponentManager.h"
+#include "nsILoadGroup.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIWebProgress.h"
+#include "nsIWebProgressListener.h"
+#include "nsPIDOMWindow.h"
+#include "nsIPrompt.h"
+#include "nsICharsetConverterManager.h"
+#include "nsIChannel.h"
+#include "nsIRequestObserver.h"
+#include "netCore.h"
+#include "prmem.h"
+#include "plbase64.h"
+#include "nsMsgI18N.h"
+#include "nsIWebNavigation.h"
+#include "nsContentUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIAuthPrompt.h"
+#include "nsMsgUtils.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/TransactionManager.h"
+#include "mozilla/dom/LoadURIOptionsBinding.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/XULFrameElement.h"
+#include "nsFrameLoader.h"
+
+NS_IMPL_ISUPPORTS(nsMsgWindow, nsIMsgWindow, nsIURIContentListener,
+ nsISupportsWeakReference)
+
+nsMsgWindow::nsMsgWindow() {
+ mCharsetOverride = false;
+ m_stopped = false;
+}
+
+nsMsgWindow::~nsMsgWindow() { CloseWindow(); }
+
+nsresult nsMsgWindow::Init() {
+ // create Undo/Redo Transaction Manager
+ mTransactionManager = new mozilla::TransactionManager();
+ return mTransactionManager->SetMaxTransactionCount(-1);
+}
+
+NS_IMETHODIMP nsMsgWindow::GetMessageWindowDocShell(nsIDocShell** aDocShell) {
+ *aDocShell = nullptr;
+
+ nsCOMPtr<nsIDocShell> docShell(do_QueryReferent(mMessageWindowDocShellWeak));
+ nsCOMPtr<nsIDocShell> rootShell(do_QueryReferent(mRootDocShellWeak));
+ if (rootShell) {
+ // There seem to be some issues with shutdown (see Bug 1610406).
+ // This workaround should prevent the GetElementById() call dying horribly
+ // but really, we shouldn't even get here in such cases.
+ bool doomed;
+ rootShell->IsBeingDestroyed(&doomed);
+ if (doomed) {
+ return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ }
+
+ RefPtr<mozilla::dom::Element> el =
+ rootShell->GetDocument()->GetElementById(u"messagepane"_ns);
+ RefPtr<mozilla::dom::XULFrameElement> frame =
+ mozilla::dom::XULFrameElement::FromNodeOrNull(el);
+ NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE);
+ RefPtr<mozilla::dom::Document> doc = frame->GetContentDocument();
+ NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+ docShell = doc->GetDocShell();
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+
+ // we don't own mMessageWindowDocShell so don't try to keep a reference to
+ // it!
+ mMessageWindowDocShellWeak = do_GetWeakReference(docShell);
+ }
+
+ NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
+ docShell.forget(aDocShell);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::CloseWindow() {
+ mStatusFeedback = nullptr;
+
+ StopUrls();
+
+ nsCOMPtr<nsIDocShell> messagePaneDocShell(
+ do_QueryReferent(mMessageWindowDocShellWeak));
+ if (messagePaneDocShell) {
+ nsCOMPtr<nsIURIContentListener> listener(
+ do_GetInterface(messagePaneDocShell));
+ if (listener) listener->SetParentContentListener(nullptr);
+ SetRootDocShell(nullptr);
+ mMessageWindowDocShellWeak = nullptr;
+ }
+
+ // in case nsMsgWindow leaks, make sure other stuff doesn't leak.
+ mTransactionManager = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetStatusFeedback(
+ nsIMsgStatusFeedback** aStatusFeedback) {
+ NS_ENSURE_ARG_POINTER(aStatusFeedback);
+ NS_IF_ADDREF(*aStatusFeedback = mStatusFeedback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetStatusFeedback(
+ nsIMsgStatusFeedback* aStatusFeedback) {
+ mStatusFeedback = aStatusFeedback;
+ nsCOMPtr<nsIDocShell> messageWindowDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell));
+
+ // register our status feedback object as a web progress listener
+ nsCOMPtr<nsIWebProgress> webProgress(do_GetInterface(messageWindowDocShell));
+ if (webProgress && mStatusFeedback && messageWindowDocShell) {
+ nsCOMPtr<nsIWebProgressListener> webProgressListener =
+ do_QueryInterface(mStatusFeedback);
+ webProgress->AddProgressListener(webProgressListener,
+ nsIWebProgress::NOTIFY_ALL);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetTransactionManager(
+ nsITransactionManager** aTransactionManager) {
+ NS_ENSURE_ARG_POINTER(aTransactionManager);
+ NS_IF_ADDREF(*aTransactionManager = mTransactionManager);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetTransactionManager(
+ nsITransactionManager* aTransactionManager) {
+ mTransactionManager = aTransactionManager;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetOpenFolder(nsIMsgFolder** aOpenFolder) {
+ NS_ENSURE_ARG_POINTER(aOpenFolder);
+ NS_IF_ADDREF(*aOpenFolder = mOpenFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetOpenFolder(nsIMsgFolder* aOpenFolder) {
+ mOpenFolder = aOpenFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetRootDocShell(nsIDocShell** aDocShell) {
+ if (mRootDocShellWeak)
+ CallQueryReferent(mRootDocShellWeak.get(), aDocShell);
+ else
+ *aDocShell = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetRootDocShell(nsIDocShell* aDocShell) {
+ // Query for the doc shell and release it
+ mRootDocShellWeak = nullptr;
+ if (aDocShell) {
+ mRootDocShellWeak = do_GetWeakReference(aDocShell);
+
+ nsCOMPtr<nsIDocShell> messagePaneDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messagePaneDocShell));
+ nsCOMPtr<nsIURIContentListener> listener(
+ do_GetInterface(messagePaneDocShell));
+ if (listener) listener->SetParentContentListener(this);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetDomWindow(mozIDOMWindowProxy** aWindow) {
+ NS_ENSURE_ARG_POINTER(aWindow);
+ if (mDomWindow)
+ CallQueryReferent(mDomWindow.get(), aWindow);
+ else
+ *aWindow = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetDomWindow(mozIDOMWindowProxy* aWindow) {
+ NS_ENSURE_ARG_POINTER(aWindow);
+ mDomWindow = do_GetWeakReference(aWindow);
+
+ nsCOMPtr<nsPIDOMWindowOuter> win = nsPIDOMWindowOuter::From(aWindow);
+ nsIDocShell* docShell = nullptr;
+ if (win) docShell = win->GetDocShell();
+
+ nsCOMPtr<nsIDocShellTreeItem> docShellAsItem(docShell);
+
+ if (docShellAsItem) {
+ nsCOMPtr<nsIDocShellTreeItem> rootAsItem;
+ docShellAsItem->GetInProcessSameTypeRootTreeItem(
+ getter_AddRefs(rootAsItem));
+
+ nsCOMPtr<nsIDocShell> rootAsShell(do_QueryInterface(rootAsItem));
+ SetRootDocShell(rootAsShell);
+
+ // force ourselves to figure out the message pane
+ nsCOMPtr<nsIDocShell> messageWindowDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetNotificationCallbacks(
+ nsIInterfaceRequestor* aNotificationCallbacks) {
+ mNotificationCallbacks = aNotificationCallbacks;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetNotificationCallbacks(
+ nsIInterfaceRequestor** aNotificationCallbacks) {
+ NS_ENSURE_ARG_POINTER(aNotificationCallbacks);
+ NS_IF_ADDREF(*aNotificationCallbacks = mNotificationCallbacks);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::StopUrls() {
+ m_stopped = true;
+ nsCOMPtr<nsIWebNavigation> webnav(do_QueryReferent(mRootDocShellWeak));
+ return webnav ? webnav->Stop(nsIWebNavigation::STOP_NETWORK)
+ : NS_ERROR_FAILURE;
+}
+
+// nsIURIContentListener support
+
+NS_IMETHODIMP nsMsgWindow::DoContent(const nsACString& aContentType,
+ bool aIsContentPreferred,
+ nsIRequest* request,
+ nsIStreamListener** aContentHandler,
+ bool* aAbortProcess) {
+ if (!aContentType.IsEmpty()) {
+ // forward the DoContent call to our docshell
+ nsCOMPtr<nsIDocShell> messageWindowDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell));
+ nsCOMPtr<nsIURIContentListener> ctnListener =
+ do_QueryInterface(messageWindowDocShell);
+ if (ctnListener) {
+ nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request);
+ if (!aChannel) return NS_ERROR_FAILURE;
+
+ // get the url for the channel...let's hope it is a mailnews url so we can
+ // set our msg hdr sink on it.. right now, this is the only way I can
+ // think of to force the msg hdr sink into the mime converter so it can
+ // get too it later...
+ nsCOMPtr<nsIURI> uri;
+ aChannel->GetURI(getter_AddRefs(uri));
+ if (uri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(uri));
+ if (mailnewsUrl) mailnewsUrl->SetMsgWindow(this);
+ }
+ return ctnListener->DoContent(aContentType, aIsContentPreferred, request,
+ aContentHandler, aAbortProcess);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgWindow::IsPreferred(const char* aContentType, char** aDesiredContentType,
+ bool* aCanHandleContent) {
+ // We don't want to handle opening any attachments inside the
+ // message pane, but want to let nsIExternalHelperAppService take care.
+ *aCanHandleContent = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::CanHandleContent(const char* aContentType,
+ bool aIsContentPreferred,
+ char** aDesiredContentType,
+ bool* aCanHandleContent)
+
+{
+ // the mail window knows nothing about the default content types
+ // its docshell can handle...ask the content area if it can handle
+ // the content type...
+
+ nsCOMPtr<nsIDocShell> messageWindowDocShell;
+ GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell));
+ nsCOMPtr<nsIURIContentListener> ctnListener(
+ do_GetInterface(messageWindowDocShell));
+ if (ctnListener)
+ return ctnListener->CanHandleContent(aContentType, aIsContentPreferred,
+ aDesiredContentType,
+ aCanHandleContent);
+ else
+ *aCanHandleContent = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetParentContentListener(
+ nsIURIContentListener** aParent) {
+ NS_ENSURE_ARG_POINTER(aParent);
+ *aParent = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetParentContentListener(
+ nsIURIContentListener* aParent) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::GetLoadCookie(nsISupports** aLoadCookie) {
+ NS_ENSURE_ARG_POINTER(aLoadCookie);
+ *aLoadCookie = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgWindow::SetLoadCookie(nsISupports* aLoadCookie) {
+ return NS_OK;
+}
+
+NS_IMPL_GETSET(nsMsgWindow, Stopped, bool, m_stopped)
diff --git a/comm/mailnews/base/src/nsMsgWindow.h b/comm/mailnews/base/src/nsMsgWindow.h
new file mode 100644
index 0000000000..1ca2370552
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgWindow.h
@@ -0,0 +1,52 @@
+/* -*- 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/. */
+
+#ifndef _nsMsgWindow_h
+#define _nsMsgWindow_h
+
+#include "nsIMsgWindow.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsITransactionManager.h"
+#include "nsIMsgFolder.h"
+#include "nsCOMPtr.h"
+#include "nsIDocShell.h"
+#include "nsIURIContentListener.h"
+#include "nsWeakReference.h"
+#include "nsIWeakReferenceUtils.h"
+#include "nsIInterfaceRequestor.h"
+
+class nsMsgWindow : public nsIMsgWindow,
+ public nsIURIContentListener,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ nsMsgWindow();
+ nsresult Init();
+ NS_DECL_NSIMSGWINDOW
+ NS_DECL_NSIURICONTENTLISTENER
+
+ protected:
+ virtual ~nsMsgWindow();
+ nsCOMPtr<nsIMsgStatusFeedback> mStatusFeedback;
+ nsCOMPtr<nsITransactionManager> mTransactionManager;
+ nsCOMPtr<nsIMsgFolder> mOpenFolder;
+ // These are used by the backend protocol code to attach
+ // notification callbacks to channels, e.g., nsIBadCertListner2.
+ nsCOMPtr<nsIInterfaceRequestor> mNotificationCallbacks;
+ // authorization prompt used during testing only
+ nsCOMPtr<nsIAuthPrompt> mAuthPrompt;
+
+ // let's not make this a strong ref - we don't own it.
+ nsWeakPtr mRootDocShellWeak;
+ nsWeakPtr mMessageWindowDocShellWeak;
+ nsWeakPtr mDomWindow;
+
+ nsCString mMailCharacterSet;
+ bool mCharsetOverride;
+ bool m_stopped;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgXFViewThread.cpp b/comm/mailnews/base/src/nsMsgXFViewThread.cpp
new file mode 100644
index 0000000000..0180a7c910
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgXFViewThread.cpp
@@ -0,0 +1,444 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgXFViewThread.h"
+#include "nsMsgSearchDBView.h"
+#include "nsMsgMessageFlags.h"
+
+NS_IMPL_ISUPPORTS(nsMsgXFViewThread, nsIMsgThread)
+
+nsMsgXFViewThread::nsMsgXFViewThread(nsMsgSearchDBView* view,
+ nsMsgKey threadId) {
+ m_numUnreadChildren = 0;
+ m_numChildren = 0;
+ m_flags = 0;
+ m_newestMsgDate = 0;
+ m_view = view;
+ m_threadId = threadId;
+}
+
+nsMsgXFViewThread::~nsMsgXFViewThread() {}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::SetThreadKey(nsMsgKey threadKey) {
+ m_threadId = threadKey;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetThreadKey(nsMsgKey* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = m_threadId;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetFlags(uint32_t* aFlags) {
+ NS_ENSURE_ARG_POINTER(aFlags);
+ *aFlags = m_flags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::SetFlags(uint32_t aFlags) {
+ m_flags = aFlags;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::SetSubject(const nsACString& aSubject) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetSubject(nsACString& result) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetNumChildren(uint32_t* aNumChildren) {
+ NS_ENSURE_ARG_POINTER(aNumChildren);
+ *aNumChildren = m_keys.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetNumUnreadChildren(uint32_t* aNumUnreadChildren) {
+ NS_ENSURE_ARG_POINTER(aNumUnreadChildren);
+ *aNumUnreadChildren = m_numUnreadChildren;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::AddChild(nsIMsgDBHdr* aNewHdr, nsIMsgDBHdr* aInReplyTo,
+ bool aThreadInThread,
+ nsIDBChangeAnnouncer* aAnnouncer) {
+ uint32_t whereInserted;
+ return AddHdr(aNewHdr, false, whereInserted, nullptr);
+}
+
+// Returns the parent of the newly added header. If reparentChildren
+// is true, we believe that the new header is a parent of an existing
+// header, and we should find it, and reparent it.
+nsresult nsMsgXFViewThread::AddHdr(nsIMsgDBHdr* newHdr, bool reparentChildren,
+ uint32_t& whereInserted,
+ nsIMsgDBHdr** outParent) {
+ nsCOMPtr<nsIMsgFolder> newHdrFolder;
+ newHdr->GetFolder(getter_AddRefs(newHdrFolder));
+
+ uint32_t newHdrFlags = 0;
+ uint32_t msgDate;
+ nsMsgKey newHdrKey = 0;
+
+ newHdr->GetMessageKey(&newHdrKey);
+ newHdr->GetDateInSeconds(&msgDate);
+ newHdr->GetFlags(&newHdrFlags);
+ if (msgDate > m_newestMsgDate) SetNewestMsgDate(msgDate);
+
+ if (newHdrFlags & nsMsgMessageFlags::Watched)
+ SetFlags(m_flags | nsMsgMessageFlags::Watched);
+
+ ChangeChildCount(1);
+ if (!(newHdrFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(1);
+
+ if (m_numChildren == 1) {
+ m_keys.InsertElementAt(0, newHdrKey);
+ m_levels.InsertElementAt(0, 0);
+ m_folders.InsertObjectAt(newHdrFolder, 0);
+ if (outParent) *outParent = nullptr;
+
+ whereInserted = 0;
+ return NS_OK;
+ }
+
+ // Find our parent, if any, in the thread. Starting at the newest
+ // reference, and working our way back, see if we've mapped that reference
+ // to this thread.
+ uint16_t numReferences;
+ newHdr->GetNumReferences(&numReferences);
+ nsCOMPtr<nsIMsgDBHdr> parent;
+ int32_t parentIndex = -1;
+
+ for (int32_t i = numReferences - 1; i >= 0; i--) {
+ nsAutoCString reference;
+ newHdr->GetStringReference(i, reference);
+ if (reference.IsEmpty()) break;
+
+ // I could look for the thread from the reference, but getting
+ // the header directly should be fine. If it's not, that means
+ // that the parent isn't in this thread, though it should be.
+ m_view->GetMsgHdrFromHash(reference, getter_AddRefs(parent));
+ if (parent) {
+ parentIndex = HdrIndex(parent);
+ if (parentIndex == -1) {
+ NS_ERROR("how did we get in the wrong thread?");
+ parent = nullptr;
+ }
+
+ break;
+ }
+ }
+
+ if (parent) {
+ uint32_t parentLevel = m_levels[parentIndex];
+ nsMsgKey parentKey;
+ parent->GetMessageKey(&parentKey);
+ nsCOMPtr<nsIMsgFolder> parentFolder;
+ parent->GetFolder(getter_AddRefs(parentFolder));
+
+ if (outParent) parent.forget(outParent);
+
+ // Iterate over our parents' children until we find one we're older than,
+ // and insert ourselves before it, or as the last child. In other words,
+ // insert, sorted by date.
+ uint32_t msgDate, childDate;
+ newHdr->GetDateInSeconds(&msgDate);
+ nsCOMPtr<nsIMsgDBHdr> child;
+ nsMsgViewIndex i;
+ nsMsgViewIndex insertIndex = m_keys.Length();
+ uint32_t insertLevel = parentLevel + 1;
+ for (i = parentIndex;
+ i < m_keys.Length() &&
+ (i == (nsMsgViewIndex)parentIndex || m_levels[i] >= parentLevel);
+ i++) {
+ GetChildHdrAt(i, getter_AddRefs(child));
+ if (child) {
+ if (reparentChildren && IsHdrParentOf(newHdr, child)) {
+ insertIndex = i;
+ // Bump all the children of the current child, and the child.
+ nsMsgViewIndex j = insertIndex;
+ uint8_t childLevel = m_levels[insertIndex];
+ do {
+ m_levels[j] = m_levels[j] + 1;
+ j++;
+ } while (j < m_keys.Length() && m_levels[j] > childLevel);
+ break;
+ } else if (m_levels[i] == parentLevel + 1) {
+ // Possible sibling.
+ child->GetDateInSeconds(&childDate);
+ if (msgDate < childDate) {
+ // If we think we need to reparent, remember this insert index,
+ // but keep looking for children.
+ insertIndex = i;
+ insertLevel = m_levels[i];
+ // If the sibling we're inserting after has children, we need
+ // to go after the children.
+ while (insertIndex + 1 < m_keys.Length() &&
+ m_levels[insertIndex + 1] > insertLevel) {
+ insertIndex++;
+ }
+
+ if (!reparentChildren) break;
+ }
+ }
+ }
+ }
+
+ m_keys.InsertElementAt(insertIndex, newHdrKey);
+ m_levels.InsertElementAt(insertIndex, insertLevel);
+ m_folders.InsertObjectAt(newHdrFolder, insertIndex);
+ whereInserted = insertIndex;
+ } else {
+ if (outParent) *outParent = nullptr;
+
+ nsCOMPtr<nsIMsgDBHdr> rootHdr;
+ GetChildHdrAt(0, getter_AddRefs(rootHdr));
+ // If the new header is a parent of the root then it should be promoted.
+ if (rootHdr && IsHdrParentOf(newHdr, rootHdr)) {
+ m_keys.InsertElementAt(0, newHdrKey);
+ m_levels.InsertElementAt(0, 0);
+ m_folders.InsertObjectAt(newHdrFolder, 0);
+ whereInserted = 0;
+ // Adjust level of old root hdr and its children
+ for (nsMsgViewIndex i = 1; i < m_keys.Length(); i++)
+ m_levels[i] = m_levels[1] + 1;
+ } else {
+ m_keys.AppendElement(newHdrKey);
+ m_levels.AppendElement(1);
+ m_folders.AppendObject(newHdrFolder);
+ if (outParent) rootHdr.forget(outParent);
+
+ whereInserted = m_keys.Length() - 1;
+ }
+ }
+
+ // ### TODO handle the case where the root header starts
+ // with Re, and the new one doesn't, and is earlier. In that
+ // case, we want to promote the new header to root.
+ // PRTime newHdrDate;
+ // newHdr->GetDate(&newHdrDate);
+ // if (numChildren > 0 && !(newHdrFlags & nsMsgMessageFlags::HasRe)) {
+ // PRTime topLevelHdrDate;
+ // nsCOMPtr<nsIMsgDBHdr> topLevelHdr;
+ // rv = GetRootHdr(getter_AddRefs(topLevelHdr));
+ // if (NS_SUCCEEDED(rv) && topLevelHdr) {
+ // topLevelHdr->GetDate(&topLevelHdrDate);
+ // if (newHdrDate < topLevelHdrDate) ?? and now ??
+ // }
+ // }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetChildHdrAt(uint32_t aIndex, nsIMsgDBHdr** aResult) {
+ if (aIndex >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND;
+
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = m_folders[aIndex]->GetMsgDatabase(getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return db->GetMsgHdrForKey(m_keys[aIndex], aResult);
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::RemoveChildAt(uint32_t aIndex) {
+ m_keys.RemoveElementAt(aIndex);
+ m_levels.RemoveElementAt(aIndex);
+ m_folders.RemoveObjectAt(aIndex);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::RemoveChildHdr(nsIMsgDBHdr* child,
+ nsIDBChangeAnnouncer* announcer) {
+ NS_ENSURE_ARG_POINTER(child);
+ nsMsgKey msgKey;
+ uint32_t msgFlags;
+ child->GetMessageKey(&msgKey);
+ child->GetFlags(&msgFlags);
+ nsCOMPtr<nsIMsgFolder> msgFolder;
+ child->GetFolder(getter_AddRefs(msgFolder));
+ // If this was the newest msg, clear the newest msg date so we'll recalc.
+ uint32_t date;
+ child->GetDateInSeconds(&date);
+ if (date == m_newestMsgDate) SetNewestMsgDate(0);
+
+ for (uint32_t childIndex = 0; childIndex < m_keys.Length(); childIndex++) {
+ if (m_keys[childIndex] == msgKey && m_folders[childIndex] == msgFolder) {
+ uint8_t levelRemoved = m_keys[childIndex];
+ // Adjust the levels of all the children of this header.
+ nsMsgViewIndex i;
+ for (i = childIndex + 1;
+ i < m_keys.Length() && m_levels[i] > levelRemoved; i++) {
+ m_levels[i] = m_levels[i] - 1;
+ }
+
+ m_view->NoteChange(childIndex + 1, i - childIndex + 1,
+ nsMsgViewNotificationCode::changed);
+ m_keys.RemoveElementAt(childIndex);
+ m_levels.RemoveElementAt(childIndex);
+ m_folders.RemoveObjectAt(childIndex);
+ if (!(msgFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(-1);
+
+ ChangeChildCount(-1);
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetRootHdr(nsIMsgDBHdr** aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ return GetChildHdrAt(0, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetChildKeyAt(uint32_t aIndex, nsMsgKey* aResult) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetChild(nsMsgKey msgKey, nsIMsgDBHdr** aResult) {
+ NS_ASSERTION(false, "shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+int32_t nsMsgXFViewThread::HdrIndex(nsIMsgDBHdr* hdr) {
+ nsMsgKey msgKey;
+ nsCOMPtr<nsIMsgFolder> folder;
+ hdr->GetMessageKey(&msgKey);
+ hdr->GetFolder(getter_AddRefs(folder));
+ for (uint32_t i = 0; i < m_keys.Length(); i++) {
+ if (m_keys[i] == msgKey && m_folders[i] == folder) return i;
+ }
+
+ return -1;
+}
+
+void nsMsgXFViewThread::ChangeUnreadChildCount(int32_t delta) {
+ m_numUnreadChildren += delta;
+}
+
+void nsMsgXFViewThread::ChangeChildCount(int32_t delta) {
+ m_numChildren += delta;
+}
+
+bool nsMsgXFViewThread::IsHdrParentOf(nsIMsgDBHdr* possibleParent,
+ nsIMsgDBHdr* possibleChild) {
+ uint16_t referenceToCheck = 0;
+ possibleChild->GetNumReferences(&referenceToCheck);
+ nsAutoCString reference;
+
+ nsCString messageId;
+ possibleParent->GetMessageId(getter_Copies(messageId));
+
+ while (referenceToCheck > 0) {
+ possibleChild->GetStringReference(referenceToCheck - 1, reference);
+
+ if (reference.Equals(messageId)) return true;
+
+ // If reference didn't match, check if this ref is for a non-existent
+ // header. If it is, continue looking at ancestors.
+ nsCOMPtr<nsIMsgDBHdr> refHdr;
+ m_view->GetMsgHdrFromHash(reference, getter_AddRefs(refHdr));
+ if (refHdr) break;
+
+ referenceToCheck--;
+ }
+
+ return false;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetNewestMsgDate(uint32_t* aResult) {
+ // If this hasn't been set, figure it out by enumerating the msgs in the
+ // thread.
+ if (!m_newestMsgDate) {
+ uint32_t numChildren;
+ nsresult rv = NS_OK;
+
+ GetNumChildren(&numChildren);
+
+ if ((int32_t)numChildren < 0) numChildren = 0;
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ uint32_t msgDate;
+ child->GetDateInSeconds(&msgDate);
+ if (msgDate > m_newestMsgDate) m_newestMsgDate = msgDate;
+ }
+ }
+ }
+
+ *aResult = m_newestMsgDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::SetNewestMsgDate(uint32_t aNewestMsgDate) {
+ m_newestMsgDate = aNewestMsgDate;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::MarkChildRead(bool aRead) {
+ ChangeUnreadChildCount(aRead ? -1 : 1);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::GetFirstUnreadChild(nsIMsgDBHdr** aResult) {
+ NS_ENSURE_ARG(aResult);
+ uint32_t numChildren;
+ GetNumChildren(&numChildren);
+
+ if ((int32_t)numChildren < 0) numChildren = 0;
+
+ for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) {
+ nsCOMPtr<nsIMsgDBHdr> child;
+ nsresult rv = GetChildHdrAt(childIndex, getter_AddRefs(child));
+ if (NS_SUCCEEDED(rv) && child) {
+ nsMsgKey msgKey;
+ child->GetMessageKey(&msgKey);
+
+ bool isRead;
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = m_folders[childIndex]->GetMsgDatabase(getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv)) rv = db->IsRead(msgKey, &isRead);
+
+ if (NS_SUCCEEDED(rv) && !isRead) {
+ child.forget(aResult);
+ break;
+ }
+ }
+ }
+
+ return (*aResult) ? NS_OK : NS_ERROR_NULL_POINTER;
+}
+
+NS_IMETHODIMP
+nsMsgXFViewThread::EnumerateMessages(nsMsgKey aParentKey,
+ nsIMsgEnumerator** aResult) {
+ NS_ERROR("shouldn't call this");
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
diff --git a/comm/mailnews/base/src/nsMsgXFViewThread.h b/comm/mailnews/base/src/nsMsgXFViewThread.h
new file mode 100644
index 0000000000..ff3276bf3f
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgXFViewThread.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef nsMsgXFViewThread_h__
+#define nsMsgXFViewThread_h__
+
+#include "msgCore.h"
+#include "nsCOMArray.h"
+#include "nsIMsgThread.h"
+#include "MailNewsTypes.h"
+#include "nsTArray.h"
+#include "nsIMsgDatabase.h"
+#include "nsIMsgHdr.h"
+#include "nsMsgDBView.h"
+
+class nsMsgSearchDBView;
+
+class nsMsgXFViewThread : public nsIMsgThread {
+ public:
+ nsMsgXFViewThread(nsMsgSearchDBView* view, nsMsgKey threadId);
+
+ NS_DECL_NSIMSGTHREAD
+ NS_DECL_ISUPPORTS
+
+ bool IsHdrParentOf(nsIMsgDBHdr* possibleParent, nsIMsgDBHdr* possibleChild);
+
+ void ChangeUnreadChildCount(int32_t delta);
+ void ChangeChildCount(int32_t delta);
+
+ nsresult AddHdr(nsIMsgDBHdr* newHdr, bool reparentChildren,
+ uint32_t& whereInserted, nsIMsgDBHdr** outParent);
+ int32_t HdrIndex(nsIMsgDBHdr* hdr);
+ uint32_t ChildLevelAt(uint32_t msgIndex) { return m_levels[msgIndex]; }
+ uint32_t MsgCount() { return m_numChildren; };
+
+ protected:
+ virtual ~nsMsgXFViewThread();
+
+ nsMsgSearchDBView* m_view;
+ uint32_t m_numUnreadChildren;
+ uint32_t m_numChildren;
+ uint32_t m_flags;
+ uint32_t m_newestMsgDate;
+ nsMsgKey m_threadId;
+ nsTArray<nsMsgKey> m_keys;
+ nsCOMArray<nsIMsgFolder> m_folders;
+ nsTArray<uint8_t> m_levels;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp
new file mode 100644
index 0000000000..fc2a1886eb
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp
@@ -0,0 +1,514 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nsMsgXFVirtualFolderDBView.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgThread.h"
+#include "nsQuickSort.h"
+#include "nsIDBFolderInfo.h"
+#include "nsIMsgCopyService.h"
+#include "nsMsgUtils.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsMsgMessageFlags.h"
+#include "nsServiceManagerUtils.h"
+
+nsMsgXFVirtualFolderDBView::nsMsgXFVirtualFolderDBView() {
+ mSuppressMsgDisplay = false;
+ m_doingSearch = false;
+ m_doingQuickSearch = false;
+ m_totalMessagesInView = 0;
+ m_curFolderHasCachedHits = false;
+}
+
+nsMsgXFVirtualFolderDBView::~nsMsgXFVirtualFolderDBView() {}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::Open(nsIMsgFolder* folder,
+ nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags,
+ int32_t* pCount) {
+ m_viewFolder = folder;
+ return nsMsgSearchDBView::Open(folder, sortType, sortOrder, viewFlags,
+ pCount);
+}
+
+void nsMsgXFVirtualFolderDBView::RemovePendingDBListeners() {
+ nsresult rv;
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+
+ // UnregisterPendingListener will return an error when there are no more
+ // instances of this object registered as pending listeners.
+ while (NS_SUCCEEDED(rv)) rv = msgDBService->UnregisterPendingListener(this);
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::Close() {
+ RemovePendingDBListeners();
+ return nsMsgSearchDBView::Close();
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) {
+ nsMsgXFVirtualFolderDBView* newMsgDBView = new nsMsgXFVirtualFolderDBView();
+ nsresult rv =
+ CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_IF_ADDREF(*_retval = newMsgDBView);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::CopyDBView(
+ nsMsgDBView* aNewMsgDBView, nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow, nsIMsgDBViewCommandUpdater* aCmdUpdater) {
+ nsMsgSearchDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow,
+ aCmdUpdater);
+
+ nsMsgXFVirtualFolderDBView* newMsgDBView =
+ (nsMsgXFVirtualFolderDBView*)aNewMsgDBView;
+
+ newMsgDBView->m_viewFolder = m_viewFolder;
+ newMsgDBView->m_searchSession = m_searchSession;
+
+ int32_t scopeCount;
+ nsresult rv;
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession, &rv);
+ // It's OK not to have a search session.
+ NS_ENSURE_SUCCESS(rv, NS_OK);
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ searchSession->CountSearchScopes(&scopeCount);
+ for (int32_t i = 0; i < scopeCount; i++) {
+ nsMsgSearchScopeValue scopeId;
+ nsCOMPtr<nsIMsgFolder> searchFolder;
+ searchSession->GetNthSearchScope(i, &scopeId, getter_AddRefs(searchFolder));
+ if (searchFolder)
+ msgDBService->RegisterPendingListener(searchFolder, newMsgDBView);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::GetViewType(nsMsgViewTypeValue* aViewType) {
+ NS_ENSURE_ARG_POINTER(aViewType);
+ *aViewType = nsMsgViewType::eShowVirtualFolderResults;
+ return NS_OK;
+}
+
+nsresult nsMsgXFVirtualFolderDBView::OnNewHeader(nsIMsgDBHdr* newHdr,
+ nsMsgKey aParentKey,
+ bool /*ensureListed*/) {
+ if (newHdr) {
+ bool match = false;
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession);
+
+ if (searchSession) searchSession->MatchHdr(newHdr, m_db, &match);
+
+ if (!match) match = WasHdrRecentlyDeleted(newHdr);
+
+ if (match) {
+ nsCOMPtr<nsIMsgFolder> folder;
+ newHdr->GetFolder(getter_AddRefs(folder));
+ bool saveDoingSearch = m_doingSearch;
+ m_doingSearch = false;
+ OnSearchHit(newHdr, folder);
+ m_doingSearch = saveDoingSearch;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::OnHdrPropertyChanged(
+ nsIMsgDBHdr* aHdrChanged, const nsACString& property, bool aPreChange,
+ uint32_t* aStatus, nsIDBChangeListener* aInstigator) {
+ // If the junk mail plugin just activated on a message, then
+ // we'll allow filters to remove from view.
+ // Otherwise, just update the view line.
+ //
+ // Note this will not add newly matched headers to the view. This is
+ // probably a bug that needs fixing.
+
+ NS_ENSURE_ARG_POINTER(aStatus);
+ NS_ENSURE_ARG_POINTER(aHdrChanged);
+
+ nsMsgViewIndex index = FindHdr(aHdrChanged);
+ // Message does not appear in view.
+ if (index == nsMsgViewIndex_None) return NS_OK;
+
+ nsCString originStr;
+ (void)aHdrChanged->GetStringProperty("junkscoreorigin", originStr);
+ // Check for "plugin" with only first character for performance.
+ bool plugin = (originStr.get()[0] == 'p');
+
+ if (aPreChange) {
+ // First call, done prior to the change.
+ *aStatus = plugin;
+ return NS_OK;
+ }
+
+ // Second call, done after the change.
+ bool wasPlugin = *aStatus;
+
+ bool match = true;
+ nsCOMPtr<nsIMsgSearchSession> searchSession(
+ do_QueryReferent(m_searchSession));
+ if (searchSession) searchSession->MatchHdr(aHdrChanged, m_db, &match);
+
+ if (!match && plugin && !wasPlugin)
+ // Remove hdr from view.
+ RemoveByIndex(index);
+ else
+ NoteChange(index, 1, nsMsgViewNotificationCode::changed);
+
+ return NS_OK;
+}
+
+void nsMsgXFVirtualFolderDBView::UpdateCacheAndViewForFolder(
+ nsIMsgFolder* folder, nsTArray<nsMsgKey> const& newHits) {
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsresult rv = folder->GetMsgDatabase(getter_AddRefs(db));
+ if (NS_SUCCEEDED(rv) && db) {
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ nsTArray<nsMsgKey> badHits;
+ rv = db->RefreshCache(searchUri, newHits, badHits);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIMsgDBHdr> badHdr;
+ for (nsMsgKey badKey : badHits) {
+ // ### of course, this isn't quite right, since we should be
+ // using FindHdr, and we shouldn't be expanding the threads.
+ db->GetMsgHdrForKey(badKey, getter_AddRefs(badHdr));
+ // Let nsMsgSearchDBView decide what to do about this header
+ // getting removed.
+ if (badHdr) OnHdrDeleted(badHdr, nsMsgKey_None, 0, this);
+ }
+ }
+ }
+}
+
+void nsMsgXFVirtualFolderDBView::UpdateCacheAndViewForPrevSearchedFolders(
+ nsIMsgFolder* curSearchFolder) {
+ // Handle the most recent folder with hits, if any.
+ if (m_curFolderGettingHits) {
+ uint32_t count = m_hdrHits.Count();
+ nsTArray<nsMsgKey> newHits;
+ newHits.SetLength(count);
+ for (uint32_t i = 0; i < count; i++)
+ m_hdrHits[i]->GetMessageKey(&newHits[i]);
+
+ newHits.Sort();
+ UpdateCacheAndViewForFolder(m_curFolderGettingHits, newHits);
+ m_foldersSearchingOver.RemoveObject(m_curFolderGettingHits);
+ }
+
+ while (m_foldersSearchingOver.Count() > 0) {
+ // This new folder has cached hits.
+ if (m_foldersSearchingOver[0] == curSearchFolder) {
+ m_curFolderHasCachedHits = true;
+ m_foldersSearchingOver.RemoveObjectAt(0);
+ break;
+ } else {
+ // This must be a folder that had no hits with the current search.
+ // So all cached hits, if any, need to be removed.
+ nsTArray<nsMsgKey> noHits;
+ UpdateCacheAndViewForFolder(m_foldersSearchingOver[0], noHits);
+ m_foldersSearchingOver.RemoveObjectAt(0);
+ }
+ }
+}
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::OnSearchHit(nsIMsgDBHdr* aMsgHdr,
+ nsIMsgFolder* aFolder) {
+ NS_ENSURE_ARG(aMsgHdr);
+ NS_ENSURE_ARG(aFolder);
+
+ if (m_curFolderGettingHits != aFolder && m_doingSearch &&
+ !m_doingQuickSearch) {
+ m_curFolderHasCachedHits = false;
+ // Since we've gotten a hit for a new folder, the searches for
+ // any previous folders are done, so deal with stale cached hits
+ // for those folders now.
+ UpdateCacheAndViewForPrevSearchedFolders(aFolder);
+ m_curFolderGettingHits = aFolder;
+ m_hdrHits.Clear();
+ m_curFolderStartKeyIndex = m_keys.Length();
+ }
+
+ bool hdrInCache = false;
+ if (!m_doingQuickSearch) {
+ nsCOMPtr<nsIMsgDatabase> dbToUse;
+ nsCOMPtr<nsIDBFolderInfo> dummyInfo;
+ nsresult rv = aFolder->GetDBFolderInfoAndDB(getter_AddRefs(dummyInfo),
+ getter_AddRefs(dbToUse));
+ if (NS_SUCCEEDED(rv)) {
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ dbToUse->HdrIsInCache(searchUri, aMsgHdr, &hdrInCache);
+ }
+ }
+
+ if (!m_doingSearch || !m_curFolderHasCachedHits || !hdrInCache) {
+ if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort)
+ nsMsgGroupView::OnNewHeader(aMsgHdr, nsMsgKey_None, true);
+ else if (m_sortValid)
+ InsertHdrFromFolder(aMsgHdr, aFolder);
+ else
+ AddHdrFromFolder(aMsgHdr, aFolder);
+ }
+
+ m_hdrHits.AppendObject(aMsgHdr);
+ m_totalMessagesInView++;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::OnSearchDone(nsresult status) {
+ // This batch began in OnNewSearch.
+ if (mJSTree) mJSTree->EndUpdateBatch();
+
+ NS_ENSURE_TRUE(m_viewFolder, NS_ERROR_NOT_INITIALIZED);
+
+ // Handle any non verified hits we haven't handled yet.
+ if (NS_SUCCEEDED(status) && !m_doingQuickSearch &&
+ status != NS_MSG_SEARCH_INTERRUPTED)
+ UpdateCacheAndViewForPrevSearchedFolders(nullptr);
+
+ m_doingSearch = false;
+ // We want to set imap delete model once the search is over because setting
+ // next message after deletion will happen before deleting the message and
+ // search scope can change with every search.
+
+ // Set to default in case it is non-imap folder.
+ mDeleteModel = nsMsgImapDeleteModels::MoveToTrash;
+ nsIMsgFolder* curFolder = m_folders.SafeObjectAt(0);
+ if (curFolder) GetImapDeleteModel(curFolder);
+
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+ // Count up the number of unread and total messages from the view, and set
+ // those in the folder - easier than trying to keep the count up to date in
+ // the face of search hits coming in while the user is reading/deleting
+ // messages.
+ uint32_t numUnread = 0;
+ for (uint32_t i = 0; i < m_flags.Length(); i++) {
+ if (m_flags[i] & nsMsgMessageFlags::Elided) {
+ nsCOMPtr<nsIMsgThread> thread;
+ GetThreadContainingIndex(i, getter_AddRefs(thread));
+ if (thread) {
+ uint32_t unreadInThread;
+ thread->GetNumUnreadChildren(&unreadInThread);
+ numUnread += unreadInThread;
+ }
+ } else {
+ if (!(m_flags[i] & nsMsgMessageFlags::Read)) numUnread++;
+ }
+ }
+
+ dbFolderInfo->SetNumUnreadMessages(numUnread);
+ dbFolderInfo->SetNumMessages(m_totalMessagesInView);
+ // Force update from db.
+ m_viewFolder->UpdateSummaryTotals(true);
+ virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ if (!m_sortValid && m_sortType != nsMsgViewSortType::byThread &&
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ // Sort the results.
+ m_sortValid = false;
+ Sort(m_sortType, m_sortOrder);
+ }
+
+ m_foldersSearchingOver.Clear();
+ m_curFolderGettingHits = nullptr;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::OnNewSearch() {
+ int32_t oldSize = GetSize();
+
+ RemovePendingDBListeners();
+ m_doingSearch = true;
+ m_totalMessagesInView = 0;
+ m_folders.Clear();
+ m_keys.Clear();
+ m_levels.Clear();
+ m_flags.Clear();
+
+ // Needs to happen after we remove the keys, since RowCountChanged() will
+ // call our GetRowCount().
+ if (mTree) mTree->RowCountChanged(0, -oldSize);
+ if (mJSTree) mJSTree->RowCountChanged(0, -oldSize);
+
+ // To use the search results cache, we'll need to iterate over the scopes
+ // in the search session, calling getNthSearchScope
+ // for i = 0; i < searchSession.countSearchScopes; i++
+ // and for each folder, then open the db and pull out the cached hits,
+ // add them to the view. For each hit in a new folder, we'll then clean up
+ // the stale hits from the previous folder(s).
+
+ int32_t scopeCount;
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_QueryReferent(m_searchSession);
+ // Just ignore.
+ NS_ENSURE_TRUE(searchSession, NS_OK);
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1");
+ searchSession->CountSearchScopes(&scopeCount);
+
+ // Figure out how many search terms the virtual folder has.
+ nsCOMPtr<nsIMsgDatabase> virtDatabase;
+ nsCOMPtr<nsIDBFolderInfo> dbFolderInfo;
+ nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(
+ getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString terms;
+ dbFolderInfo->GetCharProperty("searchStr", terms);
+ nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms;
+ rv = searchSession->GetSearchTerms(searchTerms);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString curSearchAsString;
+
+ rv = MsgTermListToString(searchTerms, curSearchAsString);
+ // Trim off the initial AND/OR, which is irrelevant and inconsistent between
+ // what SearchSpec.jsm generates, and what's in virtualFolders.dat.
+ curSearchAsString.Cut(0,
+ StringBeginsWith(curSearchAsString, "AND"_ns) ? 3 : 2);
+ terms.Cut(0, StringBeginsWith(terms, "AND"_ns) ? 3 : 2);
+
+ NS_ENSURE_SUCCESS(rv, rv);
+ // If the search session search string doesn't match the vf search str,
+ // then we're doing quick search, which means we don't want to invalidate
+ // cached results, or used cached results.
+ m_doingQuickSearch = !curSearchAsString.Equals(terms);
+
+ if (!m_doingQuickSearch) {
+ if (mTree) mTree->BeginUpdateBatch();
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+ }
+
+ for (int32_t i = 0; i < scopeCount; i++) {
+ nsMsgSearchScopeValue scopeId;
+ nsCOMPtr<nsIMsgFolder> searchFolder;
+ searchSession->GetNthSearchScope(i, &scopeId, getter_AddRefs(searchFolder));
+ if (searchFolder) {
+ nsCOMPtr<nsIMsgDatabase> searchDB;
+ nsCString searchUri;
+ m_viewFolder->GetURI(searchUri);
+ nsresult rv = searchFolder->GetMsgDatabase(getter_AddRefs(searchDB));
+ if (NS_SUCCEEDED(rv) && searchDB) {
+ if (msgDBService)
+ msgDBService->RegisterPendingListener(searchFolder, this);
+
+ m_foldersSearchingOver.AppendObject(searchFolder);
+ // Ignore cached hits in quick search case.
+ if (m_doingQuickSearch) continue;
+
+ nsCOMPtr<nsIMsgEnumerator> cachedHits;
+ searchDB->GetCachedHits(searchUri, getter_AddRefs(cachedHits));
+ bool hasMore;
+ if (cachedHits) {
+ cachedHits->HasMoreElements(&hasMore);
+ if (hasMore) {
+ mozilla::DebugOnly<nsMsgKey> prevKey = nsMsgKey_None;
+ while (hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ nsresult rv = cachedHits->GetNext(getter_AddRefs(header));
+ if (header && NS_SUCCEEDED(rv)) {
+ nsMsgKey msgKey;
+ header->GetMessageKey(&msgKey);
+ NS_ASSERTION(prevKey == nsMsgKey_None || msgKey > prevKey,
+ "cached Hits not sorted");
+#ifdef DEBUG
+ prevKey = msgKey;
+#endif
+ AddHdrFromFolder(header, searchFolder);
+ } else {
+ break;
+ }
+
+ cachedHits->HasMoreElements(&hasMore);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (!m_doingQuickSearch) {
+ if (mTree) mTree->EndUpdateBatch();
+ if (mJSTree) mJSTree->EndUpdateBatch();
+ }
+
+ m_curFolderStartKeyIndex = 0;
+ m_curFolderGettingHits = nullptr;
+ m_curFolderHasCachedHits = false;
+
+ // If we have cached hits, sort them.
+ if (GetSize() > 0) {
+ // Currently, we keep threaded views sorted while we build them.
+ if (m_sortType != nsMsgViewSortType::byThread &&
+ !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) {
+ // Sort the results.
+ m_sortValid = false;
+ Sort(m_sortType, m_sortOrder);
+ } else if (mJSTree) {
+ mJSTree->Invalidate();
+ }
+ }
+
+ // Prevent updates for every message found. This batch ends in OnSearchDone.
+ if (mJSTree) mJSTree->BeginUpdateBatch();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::DoCommand(nsMsgViewCommandTypeValue command) {
+ return nsMsgSearchDBView::DoCommand(command);
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::GetMsgFolder(nsIMsgFolder** aMsgFolder) {
+ NS_ENSURE_ARG_POINTER(aMsgFolder);
+ NS_IF_ADDREF(*aMsgFolder = m_viewFolder);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgXFVirtualFolderDBView::SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) {
+ nsresult rv = NS_OK;
+ // If the grouping/threading has changed, rebuild the view.
+ if ((m_viewFlags & (nsMsgViewFlagsType::kGroupBySort |
+ nsMsgViewFlagsType::kThreadedDisplay)) !=
+ (aViewFlags & (nsMsgViewFlagsType::kGroupBySort |
+ nsMsgViewFlagsType::kThreadedDisplay))) {
+ rv = RebuildView(aViewFlags);
+ }
+
+ nsMsgDBView::SetViewFlags(aViewFlags);
+ return rv;
+}
+
+nsresult nsMsgXFVirtualFolderDBView::GetMessageEnumerator(
+ nsIMsgEnumerator** enumerator) {
+ return GetViewEnumerator(enumerator);
+}
diff --git a/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h
new file mode 100644
index 0000000000..5b8b627347
--- /dev/null
+++ b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsMsgXFVirtualFolderDBView_H_
+#define _nsMsgXFVirtualFolderDBView_H_
+
+#include "mozilla/Attributes.h"
+#include "nsMsgSearchDBView.h"
+#include "nsIMsgCopyServiceListener.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsCOMArray.h"
+
+class nsMsgGroupThread;
+
+class nsMsgXFVirtualFolderDBView : public nsMsgSearchDBView {
+ public:
+ nsMsgXFVirtualFolderDBView();
+ virtual ~nsMsgXFVirtualFolderDBView();
+
+ // we override all the methods, currently. Might change...
+ NS_DECL_NSIMSGSEARCHNOTIFY
+
+ virtual const char* GetViewName(void) override {
+ return "XFVirtualFolderView";
+ }
+ NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType,
+ nsMsgViewSortOrderValue sortOrder,
+ nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override;
+ NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater,
+ nsIMsgDBView** _retval) override;
+ NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView,
+ nsIMessenger* aMessengerInstance,
+ nsIMsgWindow* aMsgWindow,
+ nsIMsgDBViewCommandUpdater* aCmdUpdater) override;
+ NS_IMETHOD Close() override;
+ NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override;
+ NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue command) override;
+ NS_IMETHOD SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) override;
+ NS_IMETHOD OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange,
+ const nsACString& property, bool aPreChange,
+ uint32_t* aStatus,
+ nsIDBChangeListener* aInstigator) override;
+ NS_IMETHOD GetMsgFolder(nsIMsgFolder** aMsgFolder) override;
+
+ virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey parentKey,
+ bool ensureListed) override;
+ void UpdateCacheAndViewForPrevSearchedFolders(nsIMsgFolder* curSearchFolder);
+ void UpdateCacheAndViewForFolder(nsIMsgFolder* folder,
+ nsTArray<nsMsgKey> const& newHits);
+ void RemovePendingDBListeners();
+
+ protected:
+ virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator) override;
+
+ nsCOMArray<nsIMsgFolder> m_foldersSearchingOver;
+ nsCOMArray<nsIMsgDBHdr> m_hdrHits;
+ nsCOMPtr<nsIMsgFolder> m_curFolderGettingHits;
+ // keeps track of the index of the first hit from the cur folder
+ uint32_t m_curFolderStartKeyIndex;
+ bool m_curFolderHasCachedHits;
+ bool m_doingSearch;
+ // Are we doing a quick search on top of the virtual folder search?
+ bool m_doingQuickSearch;
+};
+
+#endif
diff --git a/comm/mailnews/base/src/nsNewMailnewsURI.cpp b/comm/mailnews/base/src/nsNewMailnewsURI.cpp
new file mode 100644
index 0000000000..305e9273a4
--- /dev/null
+++ b/comm/mailnews/base/src/nsNewMailnewsURI.cpp
@@ -0,0 +1,155 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsNewMailnewsURI.h"
+#include "nsURLHelper.h"
+#include "nsSimpleURI.h"
+#include "nsStandardURL.h"
+#include "nsThreadUtils.h"
+#include "MainThreadUtils.h"
+#include "mozilla/SyncRunnable.h"
+#include "nsIMsgProtocolHandler.h"
+#include "nsIComponentRegistrar.h"
+#include "nsXULAppAPI.h"
+
+#include "../../local/src/nsPop3URL.h"
+#include "../../local/src/nsMailboxService.h"
+#include "../../compose/src/nsSmtpUrl.h"
+#include "../../addrbook/src/nsLDAPURL.h"
+#include "../../imap/src/nsImapService.h"
+#include "../../news/src/nsNntpUrl.h"
+#include "../src/nsCidProtocolHandler.h"
+
+nsresult NS_NewMailnewsURI(nsIURI** aURI, const nsACString& aSpec,
+ const char* aCharset /* = nullptr */,
+ nsIURI* aBaseURI /* = nullptr */) {
+ // Mailnews URIs aren't allowed in child processes.
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_UNKNOWN_PROTOCOL;
+ }
+
+ nsAutoCString scheme;
+ nsresult rv = net_ExtractURLScheme(aSpec, scheme);
+ if (NS_FAILED(rv)) {
+ // then aSpec is relative
+ if (!aBaseURI) {
+ return NS_ERROR_MALFORMED_URI;
+ }
+
+ rv = aBaseURI->GetScheme(scheme);
+ if (NS_FAILED(rv)) return rv;
+ }
+
+ // Creating IMAP/mailbox URIs off the main thread can lead to crashes.
+ // Seems to happen when viewing PDFs.
+ if (scheme.EqualsLiteral("mailbox") ||
+ scheme.EqualsLiteral("mailbox-message")) {
+ if (NS_IsMainThread()) {
+ return nsMailboxService::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ }
+ auto NewURI = [&aSpec, &aCharset, &aBaseURI, aURI, &rv ]() -> auto{
+ rv = nsMailboxService::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ };
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ return rv;
+ }
+ if (scheme.EqualsLiteral("imap") || scheme.EqualsLiteral("imap-message")) {
+ if (NS_IsMainThread()) {
+ return nsImapService::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ }
+ auto NewURI = [&aSpec, &aCharset, &aBaseURI, aURI, &rv ]() -> auto{
+ rv = nsImapService::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ };
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ return rv;
+ }
+ if (scheme.EqualsLiteral("smtp") || scheme.EqualsLiteral("smtps")) {
+ return nsSmtpUrl::NewSmtpURI(aSpec, aBaseURI, aURI);
+ }
+ if (scheme.EqualsLiteral("mailto")) {
+ if (NS_IsMainThread()) {
+ return nsMailtoUrl::NewMailtoURI(aSpec, aBaseURI, aURI);
+ }
+ // If we're for some reason not on the main thread, dispatch to main
+ // or else we'll crash.
+ auto NewURI = [&aSpec, &aBaseURI, aURI, &rv ]() -> auto{
+ rv = nsMailtoUrl::NewMailtoURI(aSpec, aBaseURI, aURI);
+ };
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ return rv;
+ }
+ if (scheme.EqualsLiteral("pop") || scheme.EqualsLiteral("pop3")) {
+ return nsPop3URL::NewURI(aSpec, aBaseURI, aURI);
+ }
+ if (scheme.EqualsLiteral("news") || scheme.EqualsLiteral("snews") ||
+ scheme.EqualsLiteral("news-message") || scheme.EqualsLiteral("nntp")) {
+ return nsNntpUrl::NewURI(aSpec, aBaseURI, aURI);
+ }
+ if (scheme.EqualsLiteral("cid")) {
+ return nsCidProtocolHandler::NewURI(aSpec, aCharset, aBaseURI, aURI);
+ }
+ if (scheme.EqualsLiteral("ldap") || scheme.EqualsLiteral("ldaps")) {
+ nsCOMPtr<nsILDAPURL> url = do_CreateInstance(NS_LDAPURL_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = url->Init(nsIStandardURL::URLTYPE_STANDARD,
+ scheme.EqualsLiteral("ldap") ? 389 : 636, aSpec, aCharset,
+ aBaseURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ url.forget(aURI);
+ return NS_OK;
+ }
+ if (scheme.EqualsLiteral("smile")) {
+ return NS_MutateURI(new mozilla::net::nsSimpleURI::Mutator())
+ .SetSpec(aSpec)
+ .Finalize(aURI);
+ }
+ if (scheme.EqualsLiteral("moz-cal-handle-itip")) {
+ return NS_MutateURI(new mozilla::net::nsStandardURL::Mutator())
+ .SetSpec(aSpec)
+ .Finalize(aURI);
+ }
+ if (scheme.EqualsLiteral("webcal") || scheme.EqualsLiteral("webcals")) {
+ return NS_MutateURI(new mozilla::net::nsStandardURL::Mutator())
+ .SetSpec(aSpec)
+ .Finalize(aURI);
+ }
+
+ rv = NS_ERROR_UNKNOWN_PROTOCOL; // Let M-C handle it by default.
+
+ nsCOMPtr<nsIComponentRegistrar> compMgr;
+ NS_GetComponentRegistrar(getter_AddRefs(compMgr));
+ if (compMgr) {
+ nsAutoCString contractID(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX);
+ contractID += scheme;
+ bool isRegistered = false;
+ compMgr->IsContractIDRegistered(contractID.get(), &isRegistered);
+ if (isRegistered) {
+ auto NewURI =
+ [&aSpec, &aCharset, &aBaseURI, aURI, &contractID, &rv ]() -> auto{
+ nsCOMPtr<nsIMsgProtocolHandler> handler(
+ do_GetService(contractID.get()));
+ if (handler) {
+ // We recognise this URI. Use the protocol handler's result.
+ rv = handler->NewURI(aSpec, aCharset, aBaseURI, aURI);
+ }
+ };
+ if (NS_IsMainThread()) {
+ NewURI();
+ } else {
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI);
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), task);
+ }
+ }
+ }
+
+ return rv;
+}
diff --git a/comm/mailnews/base/src/nsNewMailnewsURI.h b/comm/mailnews/base/src/nsNewMailnewsURI.h
new file mode 100644
index 0000000000..a5775d1978
--- /dev/null
+++ b/comm/mailnews/base/src/nsNewMailnewsURI.h
@@ -0,0 +1,15 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef nsNewMailnewsURI_h__
+#define nsNewMailnewsURI_h__
+
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+
+nsresult NS_NewMailnewsURI(nsIURI** aURI, const nsACString& aSpec,
+ const char* aCharset /* = nullptr */,
+ nsIURI* aBaseURI /* = nullptr */);
+#endif
diff --git a/comm/mailnews/base/src/nsQuarantinedOutputStream.cpp b/comm/mailnews/base/src/nsQuarantinedOutputStream.cpp
new file mode 100644
index 0000000000..1f325edadf
--- /dev/null
+++ b/comm/mailnews/base/src/nsQuarantinedOutputStream.cpp
@@ -0,0 +1,234 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsQuarantinedOutputStream.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIInputStream.h"
+#include "nsISeekableStream.h"
+#include "nsIFile.h"
+#include "nsNetUtil.h"
+#include "mozilla/UniquePtr.h"
+
+NS_IMPL_ISUPPORTS(nsQuarantinedOutputStream, nsIOutputStream,
+ nsISafeOutputStream)
+
+nsQuarantinedOutputStream::~nsQuarantinedOutputStream() { Close(); }
+
+// Initialise mTempFile and open it for writing (mTempStream).
+nsresult nsQuarantinedOutputStream::InitTemp() {
+ MOZ_ASSERT(mState == eUninitialized);
+ MOZ_ASSERT(!mTempFile);
+ MOZ_ASSERT(!mTempStream);
+ // Create a unique temp file.
+ {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = file->Append(u"newmsg"_ns);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mTempFile = std::move(file);
+ }
+
+ // Open the temp file for writing.
+ {
+ nsCOMPtr<nsIOutputStream> stream;
+ nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), mTempFile,
+ -1, 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mTempStream = std::move(stream);
+ }
+
+ return NS_OK;
+}
+
+// Put us into the error state and clean up (by deleting the temp file
+// if it exists).
+void nsQuarantinedOutputStream::EnterErrorState(nsresult status) {
+ mState = eError;
+ mError = status;
+ mTarget = nullptr;
+
+ if (mTempStream) {
+ mTempStream = nullptr;
+ }
+ if (mTempFile) {
+ mTempFile->Remove(false);
+ mTempFile = nullptr;
+ }
+}
+
+// copyStream copies all the data in the input stream to the output stream.
+// It keeps going until it sees an EOF on the input.
+static nsresult copyStream(nsIInputStream* in, nsIOutputStream* out) {
+ constexpr uint32_t BUFSIZE = 8192;
+ auto buf = mozilla::MakeUnique<char[]>(BUFSIZE);
+ while (true) {
+ // Read input stream into buf.
+ uint32_t bufCnt;
+ nsresult rv = in->Read(buf.get(), BUFSIZE, &bufCnt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (bufCnt == 0) {
+ break; // EOF. We're all done!
+ }
+ // Write buf to output stream.
+ uint32_t pos = 0;
+ while (pos < bufCnt) {
+ uint32_t writeCnt;
+ rv = out->Write(buf.get() + pos, bufCnt - pos, &writeCnt);
+ NS_ENSURE_SUCCESS(rv, rv);
+ pos += writeCnt;
+ }
+ }
+ return NS_OK;
+}
+
+// copyStreamSafely() wraps copyStream(). If the output stream is seekable,
+// it will try to roll it back if an error occurs during the copy.
+static nsresult copyStreamSafely(nsIInputStream* in, nsIOutputStream* out) {
+ nsCOMPtr<nsISeekableStream> outSeekable = do_QueryInterface(out);
+ if (!outSeekable) {
+ // It's not seekable, so we jump out without a parachute.
+ return copyStream(in, out);
+ }
+ int64_t initialOffset;
+ nsresult rv = outSeekable->Tell(&initialOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = copyStream(in, out);
+ if (NS_FAILED(rv)) {
+ // Uhoh... the copy failed! Try to remove the partially-written data.
+ rv = outSeekable->Seek(nsISeekableStream::NS_SEEK_SET, initialOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = outSeekable->SetEOF();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::Close() {
+ if (mState != eOpen) {
+ // Already failed or closed or no data written. That's OK.
+ return NS_OK;
+ }
+ nsresult rv = NS_OK;
+ if (mTempStream) {
+ rv = mTempStream->Close();
+ mTempStream = nullptr;
+ }
+ if (mTempFile) {
+ mTempFile->Remove(false);
+ mTempFile = nullptr;
+ }
+ mTarget->Close();
+ mTarget = nullptr;
+ mState = eClosed;
+ return rv;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::Finish() {
+ // Fail here if there was a previous error.
+ if (mState == eError) {
+ return mError;
+ }
+ if (mState != eOpen) {
+ // Already closed or no data written. That's OK.
+ return NS_OK;
+ }
+
+ // Flush and close the temp file. Hopefully any virus checker will now act
+ // and prevent us reopening any suspicious-looking file.
+ MOZ_ASSERT(mTempStream);
+ MOZ_ASSERT(mTempFile);
+ mTempStream->Flush();
+ nsresult rv = mTempStream->Close();
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ mTempStream = nullptr;
+
+ // Write the tempfile out to the target stream
+ {
+ nsCOMPtr<nsIInputStream> ins;
+ // If a virus checker smells something bad, it should show up here as a
+ // failure to (re)open the temp file.
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(ins), mTempFile);
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ rv = copyStreamSafely(ins, mTarget);
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ }
+
+ // All done!
+ mTarget->Close();
+ mTempFile->Remove(false);
+ mTempFile = nullptr;
+ mState = eClosed;
+ mTarget = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::Flush() {
+ if (mState != eOpen) {
+ return NS_OK; // Don't rock the boat.
+ }
+ nsresult rv = mTempStream->Flush();
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::Write(const char* buf, uint32_t count,
+ uint32_t* result) {
+ if (mState == eUninitialized) {
+ // Lazy open.
+ nsresult rv = InitTemp();
+ if NS_FAILED (rv) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ mState = eOpen;
+ }
+
+ if (mState != eOpen) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsresult rv = mTempStream->Write(buf, count, result);
+ if (NS_FAILED(rv)) {
+ EnterErrorState(rv);
+ return rv;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::WriteFrom(nsIInputStream* fromStream,
+ uint32_t count,
+ uint32_t* retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::WriteSegments(nsReadSegmentFun reader,
+ void* closure,
+ uint32_t count,
+ uint32_t* retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::IsNonBlocking(bool* nonBlocking) {
+ *nonBlocking = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsQuarantinedOutputStream::StreamStatus() {
+ return mState == eOpen ? NS_OK : NS_BASE_STREAM_CLOSED;
+}
diff --git a/comm/mailnews/base/src/nsQuarantinedOutputStream.h b/comm/mailnews/base/src/nsQuarantinedOutputStream.h
new file mode 100644
index 0000000000..515440494b
--- /dev/null
+++ b/comm/mailnews/base/src/nsQuarantinedOutputStream.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsQuarantinedOutputStream_h__
+#define nsQuarantinedOutputStream_h__
+
+// #include "nsISupports.h"
+#include "nsIOutputStream.h"
+#include "nsISafeOutputStream.h"
+#include "nsCOMPtr.h"
+class nsIFile;
+
+/**
+ * nsQuarantinedOutputStream layers on top of an existing target output stream.
+ * The idea is to let an OS virus checker quarantine individual messages
+ * _before_ they hit the mbox. You don't want entire mboxes embargoed if
+ * you can avoid it.
+ *
+ * It works by buffering all writes to a temporary file.
+ * When finish() is called the temporary file is closed, reopened,
+ * then copied into a pre-existing target stream. There's no special OS
+ * virus-checker integration - the assumption is that the checker will hook
+ * into the filesystem and prevent us from opening a file it has flagged as
+ * dodgy. Hence the temp file close/reopen before the final write.
+ *
+ * If the nsQuarantinedOutputStream is closed (or released) without calling
+ * finish(), the write is discarded (as per nsISafeOutputStream requirements).
+ *
+ * Upon close() or finish(), the underlying target file is also closed.
+ */
+class nsQuarantinedOutputStream : public nsIOutputStream, nsISafeOutputStream {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOUTPUTSTREAM
+ NS_DECL_NSISAFEOUTPUTSTREAM
+
+ /**
+ * Pass the target output stream in during construction. Upon Close(),
+ * the written data will be copied here.
+ */
+ explicit nsQuarantinedOutputStream(nsIOutputStream* target)
+ : mTarget(target) {}
+ nsQuarantinedOutputStream() = delete;
+
+ protected:
+ virtual ~nsQuarantinedOutputStream();
+
+ // Set up mTempFile and mTempStream (called at
+ // (lazily set up, upon first write).
+ nsresult InitTemp();
+ nsresult PerformAppend();
+ void EnterErrorState(nsresult status);
+
+ // The temporary file and stream we're writing to.
+ nsCOMPtr<nsIFile> mTempFile;
+ nsCOMPtr<nsIOutputStream> mTempStream;
+
+ // The stream we'll be appending to if it all succeeds.
+ nsCOMPtr<nsIOutputStream> mTarget;
+
+ enum {
+ eUninitialized, // No temp file yet.
+ eOpen, // We're up and running.
+ eClosed, // The file has been closed.
+ eError // An error has occurred (stored in mError).
+ } mState{eUninitialized};
+ nsresult mError{NS_OK};
+};
+
+#endif // nsQuarantinedOutputStream_h__
diff --git a/comm/mailnews/base/src/nsSpamSettings.cpp b/comm/mailnews/base/src/nsSpamSettings.cpp
new file mode 100644
index 0000000000..61dc1e8f43
--- /dev/null
+++ b/comm/mailnews/base/src/nsSpamSettings.cpp
@@ -0,0 +1,806 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSpamSettings.h"
+#include "nsIFile.h"
+#include "plstr.h"
+#include "prmem.h"
+#include "nsIMsgHdr.h"
+#include "nsNetUtil.h"
+#include "nsIMsgFolder.h"
+#include "nsMsgUtils.h"
+#include "nsMsgFolderFlags.h"
+#include "nsImapCore.h"
+#include "nsIImapIncomingServer.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIStringBundle.h"
+#include "mozilla/Components.h"
+#include "mozilla/mailnews/MimeHeaderParser.h"
+#include "nsMailDirServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsISimpleEnumerator.h"
+#include "nsIAbCard.h"
+#include "nsIAbManager.h"
+#include "nsIMsgAccountManager.h"
+#include "mozilla/intl/AppDateTimeFormat.h"
+
+using namespace mozilla::mailnews;
+
+nsSpamSettings::nsSpamSettings() {
+ mLevel = 0;
+ mMoveOnSpam = false;
+ mMoveTargetMode = nsISpamSettings::MOVE_TARGET_MODE_ACCOUNT;
+ mPurge = false;
+ mPurgeInterval = 14; // 14 days
+
+ mServerFilterTrustFlags = 0;
+ mInhibitWhiteListingIdentityUser = false;
+ mInhibitWhiteListingIdentityDomain = false;
+ mUseWhiteList = false;
+ mUseServerFilter = false;
+
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mLogFile));
+ if (NS_SUCCEEDED(rv)) mLogFile->Append(u"junklog.html"_ns);
+}
+
+nsSpamSettings::~nsSpamSettings() {}
+
+NS_IMPL_ISUPPORTS(nsSpamSettings, nsISpamSettings, nsIUrlListener)
+
+NS_IMETHODIMP
+nsSpamSettings::GetLevel(int32_t* aLevel) {
+ NS_ENSURE_ARG_POINTER(aLevel);
+ *aLevel = mLevel;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetLevel(int32_t aLevel) {
+ NS_ASSERTION((aLevel >= 0 && aLevel <= 100), "bad level");
+ mLevel = aLevel;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpamSettings::GetMoveTargetMode(int32_t* aMoveTargetMode) {
+ NS_ENSURE_ARG_POINTER(aMoveTargetMode);
+ *aMoveTargetMode = mMoveTargetMode;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetMoveTargetMode(int32_t aMoveTargetMode) {
+ NS_ASSERTION((aMoveTargetMode == nsISpamSettings::MOVE_TARGET_MODE_FOLDER ||
+ aMoveTargetMode == nsISpamSettings::MOVE_TARGET_MODE_ACCOUNT),
+ "bad move target mode");
+ mMoveTargetMode = aMoveTargetMode;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetManualMark(bool* aManualMark) {
+ NS_ENSURE_ARG_POINTER(aManualMark);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->GetBoolPref("mail.spam.manualMark", aManualMark);
+}
+
+NS_IMETHODIMP nsSpamSettings::GetManualMarkMode(int32_t* aManualMarkMode) {
+ NS_ENSURE_ARG_POINTER(aManualMarkMode);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->GetIntPref("mail.spam.manualMarkMode", aManualMarkMode);
+}
+
+NS_IMETHODIMP nsSpamSettings::GetLoggingEnabled(bool* aLoggingEnabled) {
+ NS_ENSURE_ARG_POINTER(aLoggingEnabled);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->GetBoolPref("mail.spam.logging.enabled", aLoggingEnabled);
+}
+
+NS_IMETHODIMP nsSpamSettings::GetMarkAsReadOnSpam(bool* aMarkAsReadOnSpam) {
+ NS_ENSURE_ARG_POINTER(aMarkAsReadOnSpam);
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return prefBranch->GetBoolPref("mail.spam.markAsReadOnSpam",
+ aMarkAsReadOnSpam);
+}
+
+NS_IMPL_GETSET(nsSpamSettings, MoveOnSpam, bool, mMoveOnSpam)
+NS_IMPL_GETSET(nsSpamSettings, Purge, bool, mPurge)
+NS_IMPL_GETSET(nsSpamSettings, UseWhiteList, bool, mUseWhiteList)
+NS_IMPL_GETSET(nsSpamSettings, UseServerFilter, bool, mUseServerFilter)
+
+NS_IMETHODIMP nsSpamSettings::GetWhiteListAbURI(nsACString& aWhiteListAbURI) {
+ aWhiteListAbURI = mWhiteListAbURI;
+ return NS_OK;
+}
+NS_IMETHODIMP nsSpamSettings::SetWhiteListAbURI(
+ const nsACString& aWhiteListAbURI) {
+ mWhiteListAbURI = aWhiteListAbURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetActionTargetAccount(
+ nsACString& aActionTargetAccount) {
+ aActionTargetAccount = mActionTargetAccount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetActionTargetAccount(
+ const nsACString& aActionTargetAccount) {
+ mActionTargetAccount = aActionTargetAccount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetActionTargetFolder(
+ nsACString& aActionTargetFolder) {
+ aActionTargetFolder = mActionTargetFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetActionTargetFolder(
+ const nsACString& aActionTargetFolder) {
+ mActionTargetFolder = aActionTargetFolder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetPurgeInterval(int32_t* aPurgeInterval) {
+ NS_ENSURE_ARG_POINTER(aPurgeInterval);
+ *aPurgeInterval = mPurgeInterval;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetPurgeInterval(int32_t aPurgeInterval) {
+ NS_ASSERTION(aPurgeInterval >= 0, "bad purge interval");
+ mPurgeInterval = aPurgeInterval;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpamSettings::SetLogStream(nsIOutputStream* aLogStream) {
+ // if there is a log stream already, close it
+ if (mLogStream) {
+ // will flush
+ nsresult rv = mLogStream->Close();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ mLogStream = aLogStream;
+ return NS_OK;
+}
+
+#define LOG_HEADER \
+ "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style " \
+ "type=\"text/css\">body{font-family:Consolas,\"Lucida " \
+ "Console\",Monaco,\"Courier " \
+ "New\",Courier,monospace;font-size:small}</style>\n</head>\n<body>\n"
+#define LOG_HEADER_LEN (strlen(LOG_HEADER))
+
+NS_IMETHODIMP
+nsSpamSettings::GetLogStream(nsIOutputStream** aLogStream) {
+ NS_ENSURE_ARG_POINTER(aLogStream);
+
+ nsresult rv;
+
+ if (!mLogStream) {
+ // append to the end of the log file
+ rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mLogStream), mLogFile,
+ PR_CREATE_FILE | PR_WRONLY | PR_APPEND,
+ 0600);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int64_t fileSize;
+ rv = mLogFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // write the header at the start
+ if (fileSize == 0) {
+ uint32_t writeCount;
+
+ rv = mLogStream->Write(LOG_HEADER, LOG_HEADER_LEN, &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == LOG_HEADER_LEN,
+ "failed to write out log header");
+ }
+ }
+
+ NS_ADDREF(*aLogStream = mLogStream);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::Initialize(nsIMsgIncomingServer* aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+ nsresult rv;
+ int32_t spamLevel;
+ rv = aServer->GetIntValue("spamLevel", &spamLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetLevel(spamLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool moveOnSpam;
+ rv = aServer->GetBoolValue("moveOnSpam", &moveOnSpam);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetMoveOnSpam(moveOnSpam);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t moveTargetMode;
+ rv = aServer->GetIntValue("moveTargetMode", &moveTargetMode);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetMoveTargetMode(moveTargetMode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString spamActionTargetAccount;
+ rv =
+ aServer->GetCharValue("spamActionTargetAccount", spamActionTargetAccount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetActionTargetAccount(spamActionTargetAccount);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString spamActionTargetFolder;
+ rv = aServer->GetUnicharValue("spamActionTargetFolder",
+ spamActionTargetFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetActionTargetFolder(NS_ConvertUTF16toUTF8(spamActionTargetFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool useWhiteList;
+ rv = aServer->GetBoolValue("useWhiteList", &useWhiteList);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetUseWhiteList(useWhiteList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString whiteListAbURI;
+ rv = aServer->GetCharValue("whiteListAbURI", whiteListAbURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetWhiteListAbURI(whiteListAbURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool purgeSpam;
+ rv = aServer->GetBoolValue("purgeSpam", &purgeSpam);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetPurge(purgeSpam);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t purgeSpamInterval;
+ rv = aServer->GetIntValue("purgeSpamInterval", &purgeSpamInterval);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetPurgeInterval(purgeSpamInterval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool useServerFilter;
+ rv = aServer->GetBoolValue("useServerFilter", &useServerFilter);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetUseServerFilter(useServerFilter);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverFilterName;
+ rv = aServer->GetCharValue("serverFilterName", serverFilterName);
+ if (NS_SUCCEEDED(rv)) SetServerFilterName(serverFilterName);
+ int32_t serverFilterTrustFlags = 0;
+ rv = aServer->GetIntValue("serverFilterTrustFlags", &serverFilterTrustFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = SetServerFilterTrustFlags(serverFilterTrustFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ if (prefBranch)
+ prefBranch->GetCharPref("mail.trusteddomains", mTrustedMailDomains);
+
+ mWhiteListDirArray.Clear();
+ if (!mWhiteListAbURI.IsEmpty()) {
+ nsCOMPtr<nsIAbManager> abManager(
+ do_GetService("@mozilla.org/abmanager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<nsCString> whiteListArray;
+ ParseString(mWhiteListAbURI, ' ', whiteListArray);
+
+ for (uint32_t index = 0; index < whiteListArray.Length(); index++) {
+ nsCOMPtr<nsIAbDirectory> directory;
+ rv = abManager->GetDirectory(whiteListArray[index],
+ getter_AddRefs(directory));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (directory) mWhiteListDirArray.AppendObject(directory);
+ }
+ }
+
+ // the next two preferences affect whether we try to whitelist our own
+ // address or domain. Spammers send emails with spoofed from address matching
+ // either the email address of the recipient, or the recipient's domain,
+ // hoping to get whitelisted.
+ //
+ // The terms to describe this get wrapped up in chains of negatives. A full
+ // definition of the boolean inhibitWhiteListingIdentityUser is "Suppress
+ // address book whitelisting if the sender matches an identity's email
+ // address"
+
+ rv = aServer->GetBoolValue("inhibitWhiteListingIdentityUser",
+ &mInhibitWhiteListingIdentityUser);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = aServer->GetBoolValue("inhibitWhiteListingIdentityDomain",
+ &mInhibitWhiteListingIdentityDomain);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // collect lists of identity users if needed
+ if (mInhibitWhiteListingIdentityDomain || mInhibitWhiteListingIdentityUser) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager(
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgAccount> account;
+ rv = accountManager->FindAccountForServer(aServer, getter_AddRefs(account));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString accountKey;
+ if (account) account->GetKey(accountKey);
+
+ // Loop through all accounts, adding emails from this account, as well as
+ // from any accounts that defer to this account.
+ mEmails.Clear();
+ nsTArray<RefPtr<nsIMsgAccount>> accounts;
+ // No sense scanning accounts if we've nothing to match.
+ if (account) {
+ rv = accountManager->GetAccounts(accounts);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ for (auto loopAccount : accounts) {
+ if (!loopAccount) continue;
+ nsAutoCString loopAccountKey;
+ loopAccount->GetKey(loopAccountKey);
+ nsCOMPtr<nsIMsgIncomingServer> loopServer;
+ loopAccount->GetIncomingServer(getter_AddRefs(loopServer));
+ nsAutoCString deferredToAccountKey;
+ if (loopServer)
+ loopServer->GetCharValue("deferred_to_account", deferredToAccountKey);
+
+ // Add the emails for any account that defers to this one, or for the
+ // account itself.
+ if (accountKey.Equals(deferredToAccountKey) ||
+ accountKey.Equals(loopAccountKey)) {
+ nsTArray<RefPtr<nsIMsgIdentity>> identities;
+ loopAccount->GetIdentities(identities);
+ for (auto identity : identities) {
+ nsAutoCString email;
+ identity->GetEmail(email);
+ if (!email.IsEmpty()) mEmails.AppendElement(email);
+ }
+ }
+ }
+ }
+
+ return UpdateJunkFolderState();
+}
+
+nsresult nsSpamSettings::UpdateJunkFolderState() {
+ nsresult rv;
+
+ // if the spam folder uri changed on us, we need to unset the junk flag
+ // on the old spam folder
+ nsCString newJunkFolderURI;
+ rv = GetSpamFolderURI(newJunkFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!mCurrentJunkFolderURI.IsEmpty() &&
+ !mCurrentJunkFolderURI.Equals(newJunkFolderURI)) {
+ nsCOMPtr<nsIMsgFolder> oldJunkFolder;
+ rv = FindFolder(mCurrentJunkFolderURI, getter_AddRefs(oldJunkFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (oldJunkFolder) {
+ // remove the nsMsgFolderFlags::Junk on the old junk folder
+ // XXX TODO
+ // JUNK MAIL RELATED
+ // (in ClearFlag?) we need to make sure that this folder
+ // is not the junk folder for another account
+ // the same goes for set flag. have fun with all that.
+ oldJunkFolder->ClearFlag(nsMsgFolderFlags::Junk);
+ }
+ }
+
+ mCurrentJunkFolderURI = newJunkFolderURI;
+
+ // only try to create the junk folder if we are moving junk
+ // and we have a non-empty uri
+ if (mMoveOnSpam && !mCurrentJunkFolderURI.IsEmpty()) {
+ // as the url listener, the spam settings will set the
+ // nsMsgFolderFlags::Junk folder flag on the junk mail folder, after it is
+ // created
+ rv = GetOrCreateJunkFolder(mCurrentJunkFolderURI, this);
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsSpamSettings::Clone(nsISpamSettings* aSpamSettings) {
+ NS_ENSURE_ARG_POINTER(aSpamSettings);
+
+ nsresult rv = aSpamSettings->GetUseWhiteList(&mUseWhiteList);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (void)aSpamSettings->GetMoveOnSpam(&mMoveOnSpam);
+ (void)aSpamSettings->GetPurge(&mPurge);
+ (void)aSpamSettings->GetUseServerFilter(&mUseServerFilter);
+
+ rv = aSpamSettings->GetPurgeInterval(&mPurgeInterval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aSpamSettings->GetLevel(&mLevel);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = aSpamSettings->GetMoveTargetMode(&mMoveTargetMode);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString actionTargetAccount;
+ rv = aSpamSettings->GetActionTargetAccount(actionTargetAccount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mActionTargetAccount = actionTargetAccount;
+
+ nsCString actionTargetFolder;
+ rv = aSpamSettings->GetActionTargetFolder(actionTargetFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mActionTargetFolder = actionTargetFolder;
+
+ nsCString whiteListAbURI;
+ rv = aSpamSettings->GetWhiteListAbURI(whiteListAbURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mWhiteListAbURI = whiteListAbURI;
+
+ aSpamSettings->GetServerFilterName(mServerFilterName);
+ aSpamSettings->GetServerFilterTrustFlags(&mServerFilterTrustFlags);
+
+ return rv;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetSpamFolderURI(nsACString& aSpamFolderURI) {
+ if (mMoveTargetMode == nsISpamSettings::MOVE_TARGET_MODE_FOLDER)
+ return GetActionTargetFolder(aSpamFolderURI);
+
+ // if the mode is nsISpamSettings::MOVE_TARGET_MODE_ACCOUNT
+ // the spam folder URI = account uri + "/Junk"
+ nsCString folderURI;
+ nsresult rv = GetActionTargetAccount(folderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we might be trying to get the old spam folder uri
+ // in order to clear the flag
+ // if we didn't have one, bail out.
+ if (folderURI.IsEmpty()) return NS_OK;
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(folderURI, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = folder->GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // see nsMsgFolder::SetPrettyName() for where the pretty name is set.
+
+ // Check for an existing junk folder - this will do a case-insensitive
+ // search by URI - if we find a junk folder, use its URI.
+ nsCOMPtr<nsIMsgFolder> junkFolder;
+ folderURI.AppendLiteral("/Junk");
+ if (NS_SUCCEEDED(server->GetMsgFolderFromURI(nullptr, folderURI,
+ getter_AddRefs(junkFolder))) &&
+ junkFolder)
+ junkFolder->GetURI(folderURI);
+
+ // XXX todo
+ // better not to make base depend in imap
+ // but doing it here, like in nsMsgCopy.cpp
+ // one day, we'll fix this (and nsMsgCopy.cpp) to use GetMsgFolderFromURI()
+ nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server);
+ if (imapServer) {
+ // Make sure an specific IMAP folder has correct personal namespace
+ // see bug #197043
+ nsCString folderUriWithNamespace;
+ (void)imapServer->GetUriWithNamespacePrefixIfNecessary(
+ kPersonalNamespace, folderURI, folderUriWithNamespace);
+ if (!folderUriWithNamespace.IsEmpty()) folderURI = folderUriWithNamespace;
+ }
+
+ aSpamFolderURI = folderURI;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetServerFilterName(nsACString& aFilterName) {
+ aFilterName = mServerFilterName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::SetServerFilterName(
+ const nsACString& aFilterName) {
+ mServerFilterName = aFilterName;
+ mServerFilterFile = nullptr; // clear out our stored location value
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::GetServerFilterFile(nsIFile** aFile) {
+ NS_ENSURE_ARG_POINTER(aFile);
+ if (!mServerFilterFile) {
+ nsresult rv;
+ nsAutoCString serverFilterFileName;
+ GetServerFilterName(serverFilterFileName);
+ serverFilterFileName.AppendLiteral(".sfd");
+
+ nsCOMPtr<nsIProperties> dirSvc =
+ do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Walk through the list of isp directories
+ nsCOMPtr<nsISimpleEnumerator> ispDirectories;
+ rv = dirSvc->Get(ISP_DIRECTORY_LIST, NS_GET_IID(nsISimpleEnumerator),
+ getter_AddRefs(ispDirectories));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasMore;
+ nsCOMPtr<nsIFile> file;
+ while (NS_SUCCEEDED(ispDirectories->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> elem;
+ ispDirectories->GetNext(getter_AddRefs(elem));
+ file = do_QueryInterface(elem);
+
+ if (file) {
+ // append our desired leaf name then test to see if the file exists. If
+ // it does, we've found mServerFilterFile.
+ file->AppendNative(serverFilterFileName);
+ bool exists;
+ if (NS_SUCCEEDED(file->Exists(&exists)) && exists) {
+ file.swap(mServerFilterFile);
+ break;
+ }
+ } // if file
+ } // until we find the location of mServerFilterName
+ } // if we haven't already stored mServerFilterFile
+
+ NS_IF_ADDREF(*aFile = mServerFilterFile);
+ return NS_OK;
+}
+
+NS_IMPL_GETSET(nsSpamSettings, ServerFilterTrustFlags, int32_t,
+ mServerFilterTrustFlags)
+
+#define LOG_ENTRY_START_TAG "<p>\n"
+#define LOG_ENTRY_START_TAG_LEN (strlen(LOG_ENTRY_START_TAG))
+#define LOG_ENTRY_END_TAG "</p>\n"
+#define LOG_ENTRY_END_TAG_LEN (strlen(LOG_ENTRY_END_TAG))
+// Does this need to be localizable?
+#define LOG_ENTRY_TIMESTAMP "[$S] "
+
+NS_IMETHODIMP nsSpamSettings::LogJunkHit(nsIMsgDBHdr* aMsgHdr,
+ bool aMoveMessage) {
+ bool loggingEnabled;
+ nsresult rv = GetLoggingEnabled(&loggingEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!loggingEnabled) return NS_OK;
+
+ PRTime date;
+
+ nsString authorValue;
+ nsString subjectValue;
+ nsString dateValue;
+
+ (void)aMsgHdr->GetDate(&date);
+ PRExplodedTime exploded;
+ PR_ExplodeTime(date, PR_LocalTimeParameters, &exploded);
+
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue);
+
+ (void)aMsgHdr->GetMime2DecodedAuthor(authorValue);
+ (void)aMsgHdr->GetMime2DecodedSubject(subjectValue);
+
+ nsCString buffer;
+ // this is big enough to hold a log entry.
+ // do this so we avoid growing and copying as we append to the log.
+ buffer.SetCapacity(512);
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(
+ "chrome://messenger/locale/filter.properties", getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<nsString, 3> junkLogDetectFormatStrings = {
+ authorValue, subjectValue, dateValue};
+ nsString junkLogDetectStr;
+ rv = bundle->FormatStringFromName(
+ "junkLogDetectStr", junkLogDetectFormatStrings, junkLogDetectStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ buffer += NS_ConvertUTF16toUTF8(junkLogDetectStr);
+ buffer += "\n";
+
+ if (aMoveMessage) {
+ nsCString msgId;
+ aMsgHdr->GetMessageId(getter_Copies(msgId));
+
+ nsCString junkFolderURI;
+ rv = GetSpamFolderURI(junkFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ AutoTArray<nsString, 2> logMoveFormatStrings;
+ CopyASCIItoUTF16(msgId, *logMoveFormatStrings.AppendElement());
+ CopyASCIItoUTF16(junkFolderURI, *logMoveFormatStrings.AppendElement());
+ nsString logMoveStr;
+ rv = bundle->FormatStringFromName("logMoveStr", logMoveFormatStrings,
+ logMoveStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ buffer += NS_ConvertUTF16toUTF8(logMoveStr);
+ buffer += "\n";
+ }
+
+ return LogJunkString(buffer.get());
+}
+
+NS_IMETHODIMP nsSpamSettings::LogJunkString(const char* string) {
+ bool loggingEnabled;
+ nsresult rv = GetLoggingEnabled(&loggingEnabled);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!loggingEnabled) return NS_OK;
+
+ nsString dateValue;
+ PRExplodedTime exploded;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded);
+
+ mozilla::intl::DateTimeFormat::StyleBag style;
+ style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short);
+ style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long);
+ mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue);
+
+ nsCString timestampString(LOG_ENTRY_TIMESTAMP);
+ timestampString.ReplaceSubstring("$S",
+ NS_ConvertUTF16toUTF8(dateValue).get());
+
+ nsCOMPtr<nsIOutputStream> logStream;
+ rv = GetLogStream(getter_AddRefs(logStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ uint32_t writeCount;
+
+ rv = logStream->Write(LOG_ENTRY_START_TAG, LOG_ENTRY_START_TAG_LEN,
+ &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == LOG_ENTRY_START_TAG_LEN,
+ "failed to write out start log tag");
+
+ rv = logStream->Write(timestampString.get(), timestampString.Length(),
+ &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == timestampString.Length(),
+ "failed to write out timestamp");
+
+ // HTML-escape the log for security reasons.
+ // We don't want someone to send us a message with a subject with
+ // HTML tags, especially <script>.
+ nsCString escapedBuffer;
+ nsAppendEscapedHTML(nsDependentCString(string), escapedBuffer);
+
+ uint32_t escapedBufferLen = escapedBuffer.Length();
+ rv = logStream->Write(escapedBuffer.get(), escapedBufferLen, &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == escapedBufferLen, "failed to write out log hit");
+
+ rv = logStream->Write(LOG_ENTRY_END_TAG, LOG_ENTRY_END_TAG_LEN, &writeCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(writeCount == LOG_ENTRY_END_TAG_LEN,
+ "failed to write out end log tag");
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::OnStartRunningUrl(nsIURI* aURL) {
+ // do nothing
+ // all the action happens in OnStopRunningUrl()
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsSpamSettings::OnStopRunningUrl(nsIURI* aURL,
+ nsresult exitCode) {
+ nsCString junkFolderURI;
+ nsresult rv = GetSpamFolderURI(junkFolderURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (junkFolderURI.IsEmpty()) return NS_ERROR_UNEXPECTED;
+
+ // when we get here, the folder should exist.
+ nsCOMPtr<nsIMsgFolder> junkFolder;
+ rv = GetExistingFolder(junkFolderURI, getter_AddRefs(junkFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = junkFolder->SetFlag(nsMsgFolderFlags::Junk);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+NS_IMETHODIMP nsSpamSettings::CheckWhiteList(nsIMsgDBHdr* aMsgHdr,
+ bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aMsgHdr);
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false; // default in case of error or no whitelisting
+
+ if (!mUseWhiteList ||
+ (!mWhiteListDirArray.Count() && mTrustedMailDomains.IsEmpty()))
+ return NS_OK;
+
+ // do per-message processing
+
+ nsCString author;
+ aMsgHdr->GetAuthor(getter_Copies(author));
+
+ nsAutoCString authorEmailAddress;
+ ExtractEmail(EncodedHeader(author), authorEmailAddress);
+
+ if (authorEmailAddress.IsEmpty()) return NS_OK;
+
+ // should we skip whitelisting for the identity email?
+ if (mInhibitWhiteListingIdentityUser) {
+ for (uint32_t i = 0; i < mEmails.Length(); ++i) {
+ if (mEmails[i].Equals(authorEmailAddress,
+ nsCaseInsensitiveCStringComparator))
+ return NS_OK;
+ }
+ }
+
+ if (!mTrustedMailDomains.IsEmpty() || mInhibitWhiteListingIdentityDomain) {
+ nsAutoCString domain;
+ int32_t atPos = authorEmailAddress.FindChar('@');
+ if (atPos >= 0) domain = Substring(authorEmailAddress, atPos + 1);
+ if (!domain.IsEmpty()) {
+ if (!mTrustedMailDomains.IsEmpty() &&
+ MsgHostDomainIsTrusted(domain, mTrustedMailDomains)) {
+ *aResult = true;
+ return NS_OK;
+ }
+
+ if (mInhibitWhiteListingIdentityDomain) {
+ for (uint32_t i = 0; i < mEmails.Length(); ++i) {
+ nsAutoCString identityDomain;
+ int32_t atPos = mEmails[i].FindChar('@');
+ if (atPos >= 0) {
+ identityDomain = Substring(mEmails[i], atPos + 1);
+ if (identityDomain.Equals(domain,
+ nsCaseInsensitiveCStringComparator))
+ return NS_OK; // don't whitelist
+ }
+ }
+ }
+ }
+ }
+
+ if (mWhiteListDirArray.Count()) {
+ nsCOMPtr<nsIAbCard> cardForAddress;
+ for (int32_t index = 0;
+ index < mWhiteListDirArray.Count() && !cardForAddress; index++) {
+ mWhiteListDirArray[index]->CardForEmailAddress(
+ authorEmailAddress, getter_AddRefs(cardForAddress));
+ }
+ if (cardForAddress) {
+ *aResult = true;
+ return NS_OK;
+ }
+ }
+ return NS_OK; // default return is false
+}
diff --git a/comm/mailnews/base/src/nsSpamSettings.h b/comm/mailnews/base/src/nsSpamSettings.h
new file mode 100644
index 0000000000..56725f7fe4
--- /dev/null
+++ b/comm/mailnews/base/src/nsSpamSettings.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsSpamSettings_h__
+#define nsSpamSettings_h__
+
+#include "nsCOMPtr.h"
+#include "nsISpamSettings.h"
+#include "nsString.h"
+#include "nsIOutputStream.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIUrlListener.h"
+#include "nsCOMArray.h"
+#include "nsIAbDirectory.h"
+#include "nsTArray.h"
+
+class nsSpamSettings : public nsISpamSettings, public nsIUrlListener {
+ public:
+ nsSpamSettings();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISPAMSETTINGS
+ NS_DECL_NSIURLLISTENER
+
+ private:
+ virtual ~nsSpamSettings();
+
+ nsCOMPtr<nsIOutputStream> mLogStream;
+ nsCOMPtr<nsIFile> mLogFile;
+
+ int32_t mLevel;
+ int32_t mPurgeInterval;
+ int32_t mMoveTargetMode;
+
+ bool mPurge;
+ bool mUseWhiteList;
+ bool mMoveOnSpam;
+ bool mUseServerFilter;
+
+ nsCString mActionTargetAccount;
+ nsCString mActionTargetFolder;
+ nsCString mWhiteListAbURI;
+ // used to detect changes to the spam folder in ::initialize
+ nsCString mCurrentJunkFolderURI;
+
+ nsCString mServerFilterName;
+ nsCOMPtr<nsIFile> mServerFilterFile;
+ int32_t mServerFilterTrustFlags;
+
+ // array of address directories to use in junk whitelisting
+ nsCOMArray<nsIAbDirectory> mWhiteListDirArray;
+ // mail domains to use in junk whitelisting
+ nsCString mTrustedMailDomains;
+ // should we inhibit whitelisting address of identity?
+ bool mInhibitWhiteListingIdentityUser;
+ // should we inhibit whitelisting domain of identity?
+ bool mInhibitWhiteListingIdentityDomain;
+ // email addresses associated with this server
+ nsTArray<nsCString> mEmails;
+
+ // helper routine used by Initialize which unsets the junk flag on the
+ // previous junk folder for this account, and sets it on the new junk folder.
+ nsresult UpdateJunkFolderState();
+};
+
+#endif /* nsSpamSettings_h__ */
diff --git a/comm/mailnews/base/src/nsStatusBarBiffManager.cpp b/comm/mailnews/base/src/nsStatusBarBiffManager.cpp
new file mode 100644
index 0000000000..df85a7bcee
--- /dev/null
+++ b/comm/mailnews/base/src/nsStatusBarBiffManager.cpp
@@ -0,0 +1,254 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsStatusBarBiffManager.h"
+#include "nsMsgBiffManager.h"
+#include "nsIMsgMailSession.h"
+#include "nsIMsgAccountManager.h"
+#include "nsIObserverService.h"
+#include "nsIWindowMediator.h"
+#include "nsIMsgMailSession.h"
+#include "MailNewsTypes.h"
+#include "nsIMsgFolder.h" // TO include biffState enum. Change to bool later...
+#include "nsMsgDBFolder.h"
+#include "nsIFileChannel.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsIURL.h"
+#include "nsNetUtil.h"
+#include "nsIFileURL.h"
+#include "nsIFile.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Services.h"
+
+// QueryInterface, AddRef, and Release
+//
+NS_IMPL_ISUPPORTS(nsStatusBarBiffManager, nsIStatusBarBiffManager,
+ nsIFolderListener, nsIObserver)
+
+nsStatusBarBiffManager::nsStatusBarBiffManager()
+ : mInitialized(false),
+ mCurrentBiffState(nsIMsgFolder::nsMsgBiffState_Unknown) {}
+
+nsStatusBarBiffManager::~nsStatusBarBiffManager() {}
+
+#define NEW_MAIL_PREF_BRANCH "mail.biff."
+#define CHAT_PREF_BRANCH "mail.chat."
+#define FEED_PREF_BRANCH "mail.feed."
+#define PREF_PLAY_SOUND "play_sound"
+#define PREF_SOUND_URL "play_sound.url"
+#define PREF_SOUND_TYPE "play_sound.type"
+#define SYSTEM_SOUND_TYPE 0
+#define CUSTOM_SOUND_TYPE 1
+#define PREF_CHAT_ENABLED "mail.chat.enabled"
+#define PLAY_CHAT_NOTIFICATION_SOUND "play-chat-notification-sound"
+
+nsresult nsStatusBarBiffManager::Init() {
+ if (mInitialized) return NS_ERROR_ALREADY_INITIALIZED;
+
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMailSession> mailSession =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ if (NS_SUCCEEDED(rv))
+ mailSession->AddFolderListener(this, nsIFolderListener::intPropertyChanged);
+
+ nsCOMPtr<nsIPrefBranch> pref(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool chatEnabled = false;
+ if (NS_SUCCEEDED(rv)) rv = pref->GetBoolPref(PREF_CHAT_ENABLED, &chatEnabled);
+ if (NS_SUCCEEDED(rv) && chatEnabled) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService)
+ observerService->AddObserver(this, PLAY_CHAT_NOTIFICATION_SOUND, false);
+ }
+
+ mInitialized = true;
+ return NS_OK;
+}
+
+nsresult nsStatusBarBiffManager::PlayBiffSound(const char* aPrefBranch) {
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefSvc =
+ (do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIPrefBranch> pref;
+ rv = prefSvc->GetBranch(aPrefBranch, getter_AddRefs(pref));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool playSound;
+ if (mServerType.EqualsLiteral("rss")) {
+ nsCOMPtr<nsIPrefBranch> prefFeed;
+ rv = prefSvc->GetBranch(FEED_PREF_BRANCH, getter_AddRefs(prefFeed));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = prefFeed->GetBoolPref(PREF_PLAY_SOUND, &playSound);
+ } else {
+ rv = pref->GetBoolPref(PREF_PLAY_SOUND, &playSound);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!playSound) return NS_OK;
+
+ // lazily create the sound instance
+ if (!mSound) mSound = do_CreateInstance("@mozilla.org/sound;1");
+
+ int32_t soundType = SYSTEM_SOUND_TYPE;
+ rv = pref->GetIntPref(PREF_SOUND_TYPE, &soundType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifndef XP_MACOSX
+ bool customSoundPlayed = false;
+#endif
+
+ if (soundType == CUSTOM_SOUND_TYPE) {
+ nsCString soundURLSpec;
+ rv = pref->GetCharPref(PREF_SOUND_URL, soundURLSpec);
+
+ if (NS_SUCCEEDED(rv) && !soundURLSpec.IsEmpty()) {
+ if (!strncmp(soundURLSpec.get(), "file://", 7)) {
+ nsCOMPtr<nsIURI> fileURI;
+ rv = NS_NewURI(getter_AddRefs(fileURI), soundURLSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsIFileURL> soundURL = do_QueryInterface(fileURI, &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIFile> soundFile;
+ rv = soundURL->GetFile(getter_AddRefs(soundFile));
+ if (NS_SUCCEEDED(rv)) {
+ bool soundFileExists = false;
+ rv = soundFile->Exists(&soundFileExists);
+ if (NS_SUCCEEDED(rv) && soundFileExists) {
+ rv = mSound->Play(soundURL);
+#ifndef XP_MACOSX
+ if (NS_SUCCEEDED(rv)) customSoundPlayed = true;
+#endif
+ }
+ }
+ }
+ }
+ // XXX TODO: See if we can create a nsIFile using the string as a native
+ // path.
+ }
+ }
+#ifndef XP_MACOSX
+ // if nothing played, play the default system sound
+ if (!customSoundPlayed) {
+ rv = mSound->PlayEventSound(nsISound::EVENT_NEW_MAIL_RECEIVED);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+#endif
+ return rv;
+}
+
+// nsIFolderListener methods....
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderAdded(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnMessageAdded(nsIMsgFolder* parent, nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderRemoved(nsIMsgFolder* parent,
+ nsIMsgFolder* child) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnMessageRemoved(nsIMsgFolder* parent,
+ nsIMsgDBHdr* msg) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderPropertyChanged(nsIMsgFolder* folder,
+ const nsACString& property,
+ const nsACString& oldValue,
+ const nsACString& newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderIntPropertyChanged(nsIMsgFolder* folder,
+ const nsACString& property,
+ int64_t oldValue,
+ int64_t newValue) {
+ if (property.Equals(kBiffState) && mCurrentBiffState != newValue) {
+ // if we got new mail, attempt to play a sound.
+ // if we fail along the way, don't return.
+ // we still need to update the UI.
+ if (newValue == nsIMsgFolder::nsMsgBiffState_NewMail) {
+ // Get the folder's server type.
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = folder->GetServer(getter_AddRefs(server));
+ if (NS_SUCCEEDED(rv) && server) server->GetType(mServerType);
+
+ // if we fail to play the biff sound, keep going.
+ (void)PlayBiffSound(NEW_MAIL_PREF_BRANCH);
+ }
+ mCurrentBiffState = newValue;
+
+ // don't care if notification fails
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ if (observerService)
+ observerService->NotifyObservers(
+ static_cast<nsIStatusBarBiffManager*>(this),
+ "mail:biff-state-changed", nullptr);
+ } else if (property.Equals(kNewMailReceived)) {
+ (void)PlayBiffSound(NEW_MAIL_PREF_BRANCH);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderBoolPropertyChanged(nsIMsgFolder* folder,
+ const nsACString& property,
+ bool oldValue,
+ bool newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderUnicharPropertyChanged(
+ nsIMsgFolder* folder, const nsACString& property, const nsAString& oldValue,
+ const nsAString& newValue) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderPropertyFlagChanged(nsIMsgDBHdr* msg,
+ const nsACString& property,
+ uint32_t oldFlag,
+ uint32_t newFlag) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsStatusBarBiffManager::OnFolderEvent(nsIMsgFolder* folder,
+ const nsACString& event) {
+ return NS_OK;
+}
+
+// nsIObserver implementation
+NS_IMETHODIMP
+nsStatusBarBiffManager::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ return PlayBiffSound(CHAT_PREF_BRANCH);
+}
+
+// nsIStatusBarBiffManager method....
+NS_IMETHODIMP
+nsStatusBarBiffManager::GetBiffState(int32_t* aBiffState) {
+ NS_ENSURE_ARG_POINTER(aBiffState);
+ *aBiffState = mCurrentBiffState;
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsStatusBarBiffManager.h b/comm/mailnews/base/src/nsStatusBarBiffManager.h
new file mode 100644
index 0000000000..bd4d5d5bed
--- /dev/null
+++ b/comm/mailnews/base/src/nsStatusBarBiffManager.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef nsStatusBarBiffManager_h__
+#define nsStatusBarBiffManager_h__
+
+#include "nsIStatusBarBiffManager.h"
+
+#include "msgCore.h"
+#include "nsCOMPtr.h"
+#include "nsISound.h"
+#include "nsIObserver.h"
+
+class nsStatusBarBiffManager : public nsIStatusBarBiffManager,
+ public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIFOLDERLISTENER
+ NS_DECL_NSISTATUSBARBIFFMANAGER
+ NS_DECL_NSIOBSERVER
+
+ nsStatusBarBiffManager();
+ nsresult Init();
+
+ private:
+ virtual ~nsStatusBarBiffManager();
+
+ bool mInitialized;
+ int32_t mCurrentBiffState;
+ nsCString mServerType;
+ nsCOMPtr<nsISound> mSound;
+ nsresult PlayBiffSound(const char* aPrefBranch);
+};
+
+#endif // nsStatusBarBiffManager_h__
diff --git a/comm/mailnews/base/src/nsStopwatch.cpp b/comm/mailnews/base/src/nsStopwatch.cpp
new file mode 100644
index 0000000000..585450d113
--- /dev/null
+++ b/comm/mailnews/base/src/nsStopwatch.cpp
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsStopwatch.h"
+
+#include <stdio.h>
+#include <time.h>
+#if defined(XP_UNIX)
+# include <unistd.h>
+# include <sys/times.h>
+# include <sys/time.h>
+# include <errno.h>
+#elif defined(XP_WIN)
+# include "windows.h"
+#endif // elif defined(XP_WIN)
+
+#include "nsMemory.h"
+/*
+ * This basis for the logic in this file comes from (will used to come from):
+ * (mozilla/)modules/libutil/public/stopwatch.cpp.
+ *
+ * It was no longer used in the mozilla tree, and is being migrated to
+ * comm-central where we actually have a need for it. ("Being" in the sense
+ * that it will not be removed immediately from mozilla-central.)
+ *
+ * Simplification and general clean-up has been performed and the fix for
+ * bug 96669 has been integrated.
+ */
+
+NS_IMPL_ISUPPORTS(nsStopwatch, nsIStopwatch)
+
+#if defined(XP_UNIX)
+/** the number of ticks per second */
+static double gTicks = 0;
+# define MICRO_SECONDS_TO_SECONDS_MULT static_cast<double>(1.0e-6)
+#elif defined(WIN32)
+# ifdef DEBUG
+# include "nsPrintfCString.h"
+# endif
+// 1 tick per 100ns = 10 per us = 10 * 1,000 per ms = 10 * 1,000 * 1,000 per
+// sec.
+# define WIN32_TICK_RESOLUTION static_cast<double>(1.0e-7)
+// subtract off to get to the unix epoch
+# define UNIX_EPOCH_IN_FILE_TIME 116444736000000000L
+#endif // elif defined(WIN32)
+
+nsStopwatch::nsStopwatch()
+ : fTotalRealTimeSecs(0.0), fTotalCpuTimeSecs(0.0), fRunning(false) {
+#if defined(XP_UNIX)
+ // idempotent in the event of a race under all coherency models
+ if (!gTicks) {
+ // we need to clear errno because sysconf's spec says it leaves it the same
+ // on success and only sets it on failure.
+ errno = 0;
+ gTicks = (clock_t)sysconf(_SC_CLK_TCK);
+ // in event of failure, pick an arbitrary value so we don't divide by zero.
+ if (errno) gTicks = 1000000L;
+ }
+#endif
+}
+
+nsStopwatch::~nsStopwatch() {}
+
+NS_IMETHODIMP nsStopwatch::Start() {
+ fTotalRealTimeSecs = 0.0;
+ fTotalCpuTimeSecs = 0.0;
+ return Resume();
+}
+
+NS_IMETHODIMP nsStopwatch::Stop() {
+ fStopRealTimeSecs = GetRealTime();
+ fStopCpuTimeSecs = GetCPUTime();
+ if (fRunning) {
+ fTotalCpuTimeSecs += fStopCpuTimeSecs - fStartCpuTimeSecs;
+ fTotalRealTimeSecs += fStopRealTimeSecs - fStartRealTimeSecs;
+ }
+ fRunning = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsStopwatch::Resume() {
+ if (!fRunning) {
+ fStartRealTimeSecs = GetRealTime();
+ fStartCpuTimeSecs = GetCPUTime();
+ }
+ fRunning = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsStopwatch::GetCpuTimeSeconds(double* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = fTotalCpuTimeSecs;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsStopwatch::GetRealTimeSeconds(double* result) {
+ NS_ENSURE_ARG_POINTER(result);
+ *result = fTotalRealTimeSecs;
+ return NS_OK;
+}
+
+double nsStopwatch::GetRealTime() {
+#if defined(XP_UNIX)
+ struct timeval t;
+ gettimeofday(&t, NULL);
+ return t.tv_sec + t.tv_usec * MICRO_SECONDS_TO_SECONDS_MULT;
+#elif defined(WIN32)
+ union {
+ FILETIME ftFileTime;
+ __int64 ftInt64;
+ } ftRealTime; // time the process has spent in kernel mode
+ SYSTEMTIME st;
+ GetSystemTime(&st);
+ SystemTimeToFileTime(&st, &ftRealTime.ftFileTime);
+ return (ftRealTime.ftInt64 - UNIX_EPOCH_IN_FILE_TIME) * WIN32_TICK_RESOLUTION;
+#else
+# error "nsStopwatch not supported on this platform."
+#endif
+}
+
+double nsStopwatch::GetCPUTime() {
+#if defined(XP_UNIX)
+ struct tms cpt;
+ times(&cpt);
+ return (double)(cpt.tms_utime + cpt.tms_stime) / gTicks;
+#elif defined(WIN32)
+ FILETIME ftCreate, // when the process was created
+ ftExit; // when the process exited
+
+ union {
+ FILETIME ftFileTime;
+ __int64 ftInt64;
+ } ftKernel; // time the process has spent in kernel mode
+
+ union {
+ FILETIME ftFileTime;
+ __int64 ftInt64;
+ } ftUser; // time the process has spent in user mode
+
+ HANDLE hProcess = GetCurrentProcess();
+# ifdef DEBUG
+ BOOL ret =
+# endif
+ GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel.ftFileTime,
+ &ftUser.ftFileTime);
+# ifdef DEBUG
+ if (!ret)
+ NS_ERROR(nsPrintfCString("GetProcessTimes() failed, error=0x%lx.",
+ GetLastError())
+ .get());
+# endif
+
+ /*
+ * Process times are returned in a 64-bit structure, as the number of
+ * 100 nanosecond ticks since 1 January 1601. User mode and kernel mode
+ * times for this process are in separate 64-bit structures.
+ * Add them and convert the result to seconds.
+ */
+ return (ftKernel.ftInt64 + ftUser.ftInt64) * WIN32_TICK_RESOLUTION;
+#else
+# error "nsStopwatch not supported on this platform."
+#endif
+}
diff --git a/comm/mailnews/base/src/nsStopwatch.h b/comm/mailnews/base/src/nsStopwatch.h
new file mode 100644
index 0000000000..9873519809
--- /dev/null
+++ b/comm/mailnews/base/src/nsStopwatch.h
@@ -0,0 +1,51 @@
+/* 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/. */
+
+#ifndef _nsStopwatch_h_
+#define _nsStopwatch_h_
+
+#include "nsIStopwatch.h"
+
+#include "msgCore.h"
+
+#define NS_STOPWATCH_CID \
+ { \
+ 0x6ef7eafd, 0x72d0, 0x4c56, { \
+ 0x94, 0x09, 0x67, 0xe1, 0x6d, 0x0f, 0x25, 0x5b \
+ } \
+ }
+
+#define NS_STOPWATCH_CONTRACTID "@mozilla.org/stopwatch;1"
+
+class nsStopwatch : public nsIStopwatch {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISTOPWATCH
+
+ nsStopwatch();
+
+ private:
+ virtual ~nsStopwatch();
+
+ /// Wall-clock start time in seconds since unix epoch.
+ double fStartRealTimeSecs;
+ /// Wall-clock stop time in seconds since unix epoch.
+ double fStopRealTimeSecs;
+ /// CPU-clock start time in seconds (of CPU time used since app start)
+ double fStartCpuTimeSecs;
+ /// CPU-clock stop time in seconds (of CPU time used since app start)
+ double fStopCpuTimeSecs;
+ /// Total wall-clock time elapsed in seconds.
+ double fTotalRealTimeSecs;
+ /// Total CPU time elapsed in seconds.
+ double fTotalCpuTimeSecs;
+
+ /// Is the timer running?
+ bool fRunning;
+
+ static double GetRealTime();
+ static double GetCPUTime();
+};
+
+#endif // _nsStopwatch_h_
diff --git a/comm/mailnews/base/src/nsSubscribableServer.cpp b/comm/mailnews/base/src/nsSubscribableServer.cpp
new file mode 100644
index 0000000000..043ef2e4d3
--- /dev/null
+++ b/comm/mailnews/base/src/nsSubscribableServer.cpp
@@ -0,0 +1,867 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsSubscribableServer.h"
+#include "nsIMsgIncomingServer.h"
+#include "nsIServiceManager.h"
+#include "nsMsgI18N.h"
+#include "nsMsgUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTreeColumns.h"
+#include "mozilla/dom/DataTransfer.h"
+
+/**
+ * The basic structure for the tree of the implementation.
+ *
+ * These elements are stored in reverse alphabetical order.
+ * Each node owns its children and subsequent siblings.
+ */
+struct SubscribeTreeNode {
+ nsCString name;
+ nsCString path;
+ bool isSubscribed;
+ SubscribeTreeNode* prevSibling;
+ SubscribeTreeNode* nextSibling;
+ SubscribeTreeNode* firstChild;
+ SubscribeTreeNode* lastChild;
+ SubscribeTreeNode* parent;
+ // Stores the child considered most likely to be next searched for - usually
+ // the most recently-added child. If names match the search can early-out.
+ SubscribeTreeNode* cachedChild;
+ bool isSubscribable;
+ bool isOpen;
+};
+
+nsSubscribableServer::nsSubscribableServer(void) {
+ mDelimiter = '.';
+ mShowFullName = true;
+ mTreeRoot = nullptr;
+ mStopped = false;
+}
+
+nsresult nsSubscribableServer::Init() { return NS_OK; }
+
+nsSubscribableServer::~nsSubscribableServer(void) {
+ FreeRows();
+ if (mTreeRoot) {
+ FreeSubtree(mTreeRoot);
+ }
+}
+
+NS_IMPL_ISUPPORTS(nsSubscribableServer, nsISubscribableServer, nsITreeView)
+
+NS_IMETHODIMP
+nsSubscribableServer::SetIncomingServer(nsIMsgIncomingServer* aServer) {
+ if (!aServer) {
+ mIncomingServerUri.AssignLiteral("");
+ mServerType.Truncate();
+ return NS_OK;
+ }
+ aServer->GetType(mServerType);
+
+ // We intentionally do not store a pointer to the aServer here
+ // as it would create reference loops, because nsIImapIncomingServer
+ // and nsINntpIncomingServer keep a reference to an internal
+ // nsISubscribableServer object.
+ // We only need the URI of the server anyway.
+ return aServer->GetServerURI(mIncomingServerUri);
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetDelimiter(char* aDelimiter) {
+ NS_ENSURE_ARG_POINTER(aDelimiter);
+ *aDelimiter = mDelimiter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetDelimiter(char aDelimiter) {
+ mDelimiter = aDelimiter;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetAsSubscribed(const nsACString& path) {
+ nsresult rv = NS_OK;
+
+ SubscribeTreeNode* node = nullptr;
+ rv = FindAndCreateNode(path, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+ node->isSubscribable = true;
+ node->isSubscribed = true;
+
+ return rv;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::AddTo(const nsACString& aName, bool aAddAsSubscribed,
+ bool aSubscribable, bool aChangeIfExists) {
+ nsresult rv = NS_OK;
+
+ if (mStopped) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SubscribeTreeNode* node = nullptr;
+
+ // todo, shouldn't we pass in aAddAsSubscribed, for the
+ // default value if we create it?
+ rv = FindAndCreateNode(aName, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ if (aChangeIfExists) {
+ node->isSubscribed = aAddAsSubscribed;
+ }
+
+ node->isSubscribable = aSubscribable;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetState(const nsACString& aPath, bool aState,
+ bool* aStateChanged) {
+ nsresult rv = NS_OK;
+ NS_ASSERTION(!aPath.IsEmpty() && aStateChanged, "no path or stateChanged");
+ if (aPath.IsEmpty() || !aStateChanged) return NS_ERROR_NULL_POINTER;
+
+ NS_ASSERTION(mozilla::IsUtf8(aPath), "aPath is not in UTF-8");
+
+ *aStateChanged = false;
+
+ SubscribeTreeNode* node = nullptr;
+ rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ NS_ASSERTION(node->isSubscribable, "fix this");
+ if (!node->isSubscribable) {
+ return NS_OK;
+ }
+
+ if (node->isSubscribed == aState) {
+ return NS_OK;
+ } else {
+ node->isSubscribed = aState;
+ *aStateChanged = true;
+
+ // Repaint the tree row to show/clear the check mark.
+ if (mTree) {
+ bool dummy;
+ int32_t index = GetRow(node, &dummy);
+ if (index >= 0) mTree->InvalidateRow(index);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetSubscribeListener(nsISubscribeListener* aListener) {
+ mSubscribeListener = aListener;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetSubscribeListener(nsISubscribeListener** aListener) {
+ NS_ENSURE_ARG_POINTER(aListener);
+ NS_IF_ADDREF(*aListener = mSubscribeListener);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SubscribeCleanup() {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::StartPopulatingWithUri(nsIMsgWindow* aMsgWindow,
+ bool aForceToServer,
+ const nsACString& uri) {
+ mStopped = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::StartPopulating(nsIMsgWindow* aMsgWindow,
+ bool aForceToServer,
+ bool aGetOnlyNew /*ignored*/) {
+ mStopped = false;
+
+ FreeRows();
+ if (mTreeRoot) {
+ FreeSubtree(mTreeRoot);
+ mTreeRoot = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::StopPopulating(nsIMsgWindow* aMsgWindow) {
+ mStopped = true;
+
+ FreeRows();
+
+ if (mTreeRoot) {
+ SubscribeTreeNode* node = mTreeRoot->lastChild;
+ // Add top level items as closed.
+ while (node) {
+ node->isOpen = false;
+ mRowMap.AppendElement(node);
+ node = node->prevSibling;
+ }
+
+ // Invalidate the whole thing.
+ if (mTree) mTree->RowCountChanged(0, mRowMap.Length());
+
+ // Open all the top level items if they are containers.
+ uint32_t topRows = mRowMap.Length();
+ for (int32_t i = topRows - 1; i >= 0; i--) {
+ bool isContainer = false;
+ IsContainer(i, &isContainer);
+ if (isContainer) ToggleOpenState(i);
+ }
+ }
+
+ if (mSubscribeListener) mSubscribeListener->OnDonePopulating();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::UpdateSubscribed() {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::Subscribe(const char16_t* aName) {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::Unsubscribe(const char16_t* aName) {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetShowFullName(bool showFullName) {
+ mShowFullName = showFullName;
+ return NS_OK;
+}
+
+void nsSubscribableServer::FreeSubtree(SubscribeTreeNode* node) {
+ // NOTE: this doesn't cleanly detach each node, but that's not an issue
+ // here. This is a nuking, not a surgical removal.
+
+ // recursively free the children
+ if (node->firstChild) {
+ // will free node->firstChild
+ FreeSubtree(node->firstChild);
+ node->firstChild = nullptr;
+ }
+
+ // recursively free the siblings
+ if (node->nextSibling) {
+ FreeSubtree(node->nextSibling);
+ node->nextSibling = nullptr;
+ }
+
+ delete node;
+}
+
+void nsSubscribableServer::FreeRows() {
+ int32_t rowCount = mRowMap.Length();
+ mRowMap.Clear();
+ if (mTree) mTree->RowCountChanged(0, -rowCount);
+}
+
+SubscribeTreeNode* nsSubscribableServer::CreateNode(SubscribeTreeNode* parent,
+ nsACString const& name,
+ nsACString const& path) {
+ SubscribeTreeNode* node = new SubscribeTreeNode();
+
+ node->name.Assign(name);
+ node->path.Assign(path);
+ node->parent = parent;
+ node->prevSibling = nullptr;
+ node->nextSibling = nullptr;
+ node->firstChild = nullptr;
+ node->lastChild = nullptr;
+ node->isSubscribed = false;
+ node->isSubscribable = false;
+ node->isOpen = true;
+ node->cachedChild = nullptr;
+ if (parent) {
+ parent->cachedChild = node;
+ }
+ return node;
+}
+
+nsresult nsSubscribableServer::AddChildNode(SubscribeTreeNode* parent,
+ nsACString const& name,
+ const nsACString& path,
+ SubscribeTreeNode** child) {
+ nsresult rv = NS_OK;
+ NS_ASSERTION(parent && child && !name.IsEmpty() && !path.IsEmpty(),
+ "parent, child or name is null");
+ if (!parent || !child || name.IsEmpty() || path.IsEmpty())
+ return NS_ERROR_NULL_POINTER;
+
+ // Adding to a node with no children?
+ if (!parent->firstChild) {
+ // CreateNode will set the parent->cachedChild
+ *child = CreateNode(parent, name, path);
+ parent->firstChild = *child;
+ parent->lastChild = *child;
+ return NS_OK;
+ }
+
+ // Did we just add a child of this name?
+ if (parent->cachedChild) {
+ if (parent->cachedChild->name.Equals(name)) {
+ *child = parent->cachedChild;
+ return NS_OK;
+ }
+ }
+
+ SubscribeTreeNode* current = parent->firstChild;
+
+ /*
+ * Insert in reverse alphabetical order.
+ * This will reduce the # of strcmps since this is faster assuming:
+ * 1) the hostinfo.dat feeds us the groups in alphabetical order
+ * since we control the hostinfo.dat file, we can guarantee this.
+ * 2) the server gives us the groups in alphabetical order
+ * we can't guarantee this, but it seems to be a common thing.
+ *
+ * Because we have firstChild, lastChild, nextSibling, prevSibling,
+ * we can efficiently reverse the order when dumping to hostinfo.dat
+ * or to GetTargets().
+ */
+ int32_t compare = Compare(current->name, name);
+
+ while (current && (compare != 0)) {
+ if (compare < 0) {
+ // CreateNode will set the parent->cachedChild
+ *child = CreateNode(parent, name, path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (*child)->nextSibling = current;
+ (*child)->prevSibling = current->prevSibling;
+ current->prevSibling = (*child);
+ if (!(*child)->prevSibling) {
+ parent->firstChild = (*child);
+ } else {
+ (*child)->prevSibling->nextSibling = (*child);
+ }
+
+ return NS_OK;
+ }
+ current = current->nextSibling;
+ if (current) {
+ NS_ASSERTION(!current->name.IsEmpty(), "no name!");
+ compare = Compare(current->name, name);
+ } else {
+ compare = -1; // anything but 0, since that would be a match
+ }
+ }
+
+ if (compare == 0) {
+ // already exists;
+ *child = current;
+ parent->cachedChild = *child;
+ return NS_OK;
+ }
+
+ // CreateNode will set the parent->cachedChild
+ *child = CreateNode(parent, name, path);
+
+ (*child)->prevSibling = parent->lastChild;
+ (*child)->nextSibling = nullptr;
+ parent->lastChild->nextSibling = *child;
+ parent->lastChild = *child;
+
+ return NS_OK;
+}
+
+nsresult nsSubscribableServer::FindAndCreateNode(const nsACString& aPath,
+ SubscribeTreeNode** aResult) {
+ nsresult rv = NS_OK;
+ NS_ASSERTION(aResult, "no result");
+ NS_ENSURE_ARG_POINTER(aResult);
+
+ if (!mTreeRoot) {
+ // the root has no parent, and its name is server uri
+ mTreeRoot = CreateNode(nullptr, mIncomingServerUri, EmptyCString());
+ }
+
+ if (aPath.IsEmpty()) {
+ *aResult = mTreeRoot;
+ return NS_OK;
+ }
+
+ *aResult = nullptr;
+
+ SubscribeTreeNode* parent = mTreeRoot;
+ SubscribeTreeNode* child = nullptr;
+
+ uint32_t tokenStart = 0;
+ // Special case paths that start with the hierarchy delimiter.
+ // We want to include that delimiter in the first token name.
+ // So start from position 1.
+ int32_t tokenEnd = aPath.FindChar(mDelimiter, tokenStart + 1);
+ while (true) {
+ if (tokenEnd == kNotFound) {
+ if (tokenStart >= aPath.Length()) break;
+ tokenEnd = aPath.Length();
+ }
+ nsCString token(Substring(aPath, tokenStart, tokenEnd - tokenStart));
+ rv = AddChildNode(parent, token, Substring(aPath, 0, tokenEnd), &child);
+ if (NS_FAILED(rv)) return rv;
+ tokenStart = tokenEnd + 1;
+ tokenEnd = aPath.FindChar(mDelimiter, tokenStart);
+ parent = child;
+ }
+
+ // the last child we add is the result
+ *aResult = child;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::HasChildren(const nsACString& aPath, bool* aHasChildren) {
+ NS_ASSERTION(aHasChildren, "no hasChildren");
+ NS_ENSURE_ARG_POINTER(aHasChildren);
+
+ *aHasChildren = false;
+
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ *aHasChildren = (node->firstChild != nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsSubscribed(const nsACString& aPath,
+ bool* aIsSubscribed) {
+ NS_ENSURE_ARG_POINTER(aIsSubscribed);
+
+ *aIsSubscribed = false;
+
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ *aIsSubscribed = node->isSubscribed;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsSubscribable(const nsACString& aPath,
+ bool* aIsSubscribable) {
+ NS_ENSURE_ARG_POINTER(aIsSubscribable);
+
+ *aIsSubscribable = false;
+
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ *aIsSubscribable = node->isSubscribable;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetLeafName(const nsACString& aPath,
+ nsAString& aLeafName) {
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ // XXX TODO FIXME
+ // I'm assuming that mShowFullName is true for NNTP, false for IMAP.
+ // For imap, the node name is in MUTF-7; for news, the path is escaped UTF-8.
+ // When we switch to using the tree, this hack will go away.
+ if (mShowFullName) {
+ return NS_MsgDecodeUnescapeURLPath(aPath, aLeafName);
+ }
+
+ return CopyFolderNameToUTF16(node->name, aLeafName);
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetFirstChildURI(const nsACString& aPath,
+ nsACString& aResult) {
+ aResult.Truncate();
+
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ // no children
+ if (!node->firstChild) return NS_ERROR_FAILURE;
+
+ aResult.Assign(node->firstChild->path);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetChildURIs(const nsACString& aPath,
+ nsTArray<nsCString>& aResult) {
+ aResult.Clear();
+ SubscribeTreeNode* node = nullptr;
+ nsresult rv = FindAndCreateNode(aPath, &node);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ASSERTION(node, "didn't find the node");
+ if (!node) return NS_ERROR_FAILURE;
+
+ NS_ASSERTION(mTreeRoot, "no tree root!");
+ if (!mTreeRoot) return NS_ERROR_UNEXPECTED;
+
+ // We inserted them in reverse alphabetical order.
+ // So pull them out in reverse to get the right order
+ // in the subscribe dialog.
+ SubscribeTreeNode* current = node->lastChild;
+ // return failure if there are no children.
+ if (!current) return NS_ERROR_FAILURE;
+
+ while (current) {
+ NS_ASSERTION(!current->name.IsEmpty(), "no name");
+ if (current->name.IsEmpty()) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.AppendElement(current->path);
+ current = current->prevSibling;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::CommitSubscribeChanges() {
+ NS_ASSERTION(false, "override this.");
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetSearchValue(const nsAString& aSearchValue) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetSupportsSubscribeSearch(bool* retVal) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetFolderView(nsITreeView** aView) {
+ NS_ENSURE_ARG_POINTER(aView);
+ return this->QueryInterface(NS_GET_IID(nsITreeView), (void**)aView);
+}
+
+int32_t nsSubscribableServer::GetRow(SubscribeTreeNode* node, bool* open) {
+ int32_t parentRow = -1;
+ if (node->parent) parentRow = GetRow(node->parent, open);
+
+ // If the parent wasn't opened, we're not in the row map
+ if (open && *open == false) return -1;
+
+ if (open) *open = node->isOpen;
+
+ for (uint32_t row = parentRow + 1; row < mRowMap.Length(); row++) {
+ if (mRowMap[row] == node) return row;
+ }
+
+ // Apparently, we're not in the map
+ return -1;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetSelection(nsITreeSelection** selection) {
+ NS_IF_ADDREF(*selection = mSelection);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetSelection(nsITreeSelection* selection) {
+ mSelection = selection;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetRowCount(int32_t* rowCount) {
+ *rowCount = mRowMap.Length();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetTree(mozilla::dom::XULTreeElement* aTree) {
+ mTree = aTree;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsContainer(int32_t aIndex, bool* retval) {
+ *retval = !!mRowMap[aIndex]->firstChild;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsContainerEmpty(int32_t aIndex, bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsContainerOpen(int32_t aIndex, bool* retval) {
+ *retval = mRowMap[aIndex]->isOpen;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetParentIndex(int32_t aIndex, int32_t* retval) {
+ SubscribeTreeNode* parent = mRowMap[aIndex]->parent;
+ if (!parent) {
+ *retval = -1;
+ return NS_OK;
+ }
+
+ int32_t index;
+ for (index = aIndex - 1; index >= 0; index--) {
+ if (mRowMap[index] == parent) {
+ *retval = index;
+ return NS_OK;
+ }
+ }
+ *retval = -1;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex,
+ bool* retval) {
+ // This looks odd, but is correct. Using ->nextSibling gives a bad tree.
+ *retval = !!mRowMap[aRowIndex]->prevSibling;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetLevel(int32_t aIndex, int32_t* retval) {
+ // When starting with -2, we increase twice and return 0 for a top level node.
+ int32_t level = -2;
+ SubscribeTreeNode* node = mRowMap[aIndex];
+ while (node) {
+ node = node->parent;
+ level++;
+ }
+
+ *retval = level;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::ToggleOpenState(int32_t aIndex) {
+ SubscribeTreeNode* node = mRowMap[aIndex];
+ if (node->isOpen) {
+ node->isOpen = false;
+
+ // Remove subtree by deleting elements from array up to next sibling.
+ int32_t count = 0;
+ do {
+ // Find next sibling or the beginning of the next subtree.
+ if (node->prevSibling) {
+ count = mRowMap.IndexOf(node->prevSibling, aIndex) - aIndex - 1;
+ } else {
+ node = node->parent;
+ // When node reaches the root, delete the rest of the array.
+ if (!node->parent) {
+ count = mRowMap.Length() - aIndex - 1;
+ }
+ }
+ } while (!count && node->parent);
+ mRowMap.RemoveElementsAt(aIndex + 1, count);
+ if (mTree) {
+ mTree->RowCountChanged(aIndex + 1, -count);
+ mTree->InvalidateRow(aIndex);
+ }
+ } else {
+ // Recursively add the children nodes (i.e., remember open)
+ node->isOpen = true;
+ int32_t total = 0;
+ node = node->lastChild;
+ while (node) {
+ total += AddSubtree(node, aIndex + 1 + total);
+ node = node->prevSibling;
+ }
+ if (mTree) {
+ mTree->RowCountChanged(aIndex + 1, total);
+ mTree->InvalidateRow(aIndex);
+ }
+ }
+ return NS_OK;
+}
+
+int32_t nsSubscribableServer::AddSubtree(SubscribeTreeNode* node,
+ int32_t index) {
+ mRowMap.InsertElementAt(index, node);
+ int32_t total = 1;
+ if (node->isOpen) {
+ node = node->lastChild;
+ while (node) {
+ total += AddSubtree(node, index + total);
+ node = node->prevSibling;
+ }
+ }
+ return total;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetCellText(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& retval) {
+ nsString colId;
+ aCol->GetId(colId);
+ if (colId.EqualsLiteral("nameColumn")) {
+ nsCString path(mRowMap[aRow]->path);
+ GetLeafName(path, retval);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetCellValue(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& retval) {
+ nsString colId;
+ aCol->GetId(colId);
+ if (colId.EqualsLiteral("nameColumn"))
+ retval = NS_ConvertUTF8toUTF16(mRowMap[aRow]->path);
+ if (colId.EqualsLiteral("subscribedColumn")) {
+ retval = mRowMap[aRow]->isSubscribed ? u"true"_ns : u"false"_ns;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetCellText(int32_t aRow, nsTreeColumn* aCol,
+ const nsAString& aText) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::SetCellValue(int32_t aRow, nsTreeColumn* aCol,
+ const nsAString& aText) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetCellProperties(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& aProps) {
+ SubscribeTreeNode* node = mRowMap[aRow];
+ if (node->isSubscribable)
+ aProps.AssignLiteral("subscribable-true");
+ else
+ aProps.AssignLiteral("subscribable-false");
+
+ nsString colId;
+ aCol->GetId(colId);
+ if (colId.EqualsLiteral("subscribedColumn")) {
+ if (node->isSubscribed)
+ aProps.AppendLiteral(" subscribed-true");
+ else
+ aProps.AppendLiteral(" subscribed-false");
+ } else if (colId.EqualsLiteral("nameColumn")) {
+ aProps.AppendLiteral(" serverType-");
+ aProps.Append(NS_ConvertUTF8toUTF16(mServerType));
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetRowProperties(int32_t aRow, nsAString& aProps) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetColumnProperties(nsTreeColumn* aCol,
+ nsAString& aProps) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsEditable(int32_t aRow, nsTreeColumn* aCol,
+ bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsSeparator(int32_t aRowIndex, bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::IsSorted(bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::CanDrop(int32_t aIndex, int32_t aOrientation,
+ mozilla::dom::DataTransfer* aData, bool* retval) {
+ *retval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::Drop(int32_t aRow, int32_t aOrientation,
+ mozilla::dom::DataTransfer* aData) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::GetImageSrc(int32_t aRow, nsTreeColumn* aCol,
+ nsAString& retval) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSubscribableServer::CycleHeader(nsTreeColumn* aCol) { return NS_OK; }
+
+NS_IMETHODIMP
+nsSubscribableServer::SelectionChangedXPCOM() { return NS_OK; }
+
+NS_IMETHODIMP
+nsSubscribableServer::CycleCell(int32_t aRow, nsTreeColumn* aCol) {
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsSubscribableServer.h b/comm/mailnews/base/src/nsSubscribableServer.h
new file mode 100644
index 0000000000..7fbafd0838
--- /dev/null
+++ b/comm/mailnews/base/src/nsSubscribableServer.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; 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/. */
+
+#ifndef nsSubscribableServer_h__
+#define nsSubscribableServer_h__
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "mozilla/dom/XULTreeElement.h"
+#include "nsITreeSelection.h"
+#include "nsITreeView.h"
+#include "nsISubscribableServer.h"
+#include "nsTArray.h"
+
+struct SubscribeTreeNode;
+
+class nsSubscribableServer : public nsISubscribableServer, public nsITreeView {
+ public:
+ nsSubscribableServer();
+
+ nsresult Init();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISUBSCRIBABLESERVER
+ NS_DECL_NSITREEVIEW
+
+ private:
+ virtual ~nsSubscribableServer();
+
+ nsresult ConvertNameToUnichar(const char* inStr, char16_t** outStr);
+ nsCOMPtr<nsISubscribeListener> mSubscribeListener;
+ nsCString mIncomingServerUri;
+ char mDelimiter;
+ bool mShowFullName;
+ bool mStopped;
+ nsCString mServerType;
+
+ // root of the folder tree while items are discovered on the server
+ SubscribeTreeNode* mTreeRoot;
+ // array of nodes representing the rows for the tree element
+ nsTArray<SubscribeTreeNode*> mRowMap;
+ nsCOMPtr<nsITreeSelection> mSelection;
+ RefPtr<mozilla::dom::XULTreeElement> mTree;
+ void FreeSubtree(SubscribeTreeNode* node);
+ void FreeRows();
+ SubscribeTreeNode* CreateNode(SubscribeTreeNode* parent,
+ nsACString const& name, nsACString const& path);
+ nsresult AddChildNode(SubscribeTreeNode* parent, nsACString const& name,
+ const nsACString& aPath, SubscribeTreeNode** child);
+ nsresult FindAndCreateNode(const nsACString& aPath,
+ SubscribeTreeNode** aResult);
+
+ int32_t GetRow(SubscribeTreeNode* node, bool* open);
+ int32_t AddSubtree(SubscribeTreeNode* node, int32_t index);
+};
+
+#endif // nsSubscribableServer_h__
diff --git a/comm/mailnews/base/src/nsUserInfo.h b/comm/mailnews/base/src/nsUserInfo.h
new file mode 100644
index 0000000000..df3f81adef
--- /dev/null
+++ b/comm/mailnews/base/src/nsUserInfo.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef __nsUserInfo_h
+#define __nsUserInfo_h
+
+#include "nsIUserInfo.h"
+
+class nsUserInfo : public nsIUserInfo
+
+{
+ public:
+ nsUserInfo(void);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIUSERINFO
+
+ protected:
+ virtual ~nsUserInfo();
+};
+
+#endif /* __nsUserInfo_h */
diff --git a/comm/mailnews/base/src/nsUserInfoMac.mm b/comm/mailnews/base/src/nsUserInfoMac.mm
new file mode 100644
index 0000000000..a98d1f72ea
--- /dev/null
+++ b/comm/mailnews/base/src/nsUserInfoMac.mm
@@ -0,0 +1,70 @@
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsUserInfo.h"
+#include "nsObjCExceptions.h"
+#include "nsString.h"
+#include "nsCocoaUtils.h"
+
+#import <Cocoa/Cocoa.h>
+#import <AddressBook/AddressBook.h>
+
+NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo)
+
+nsUserInfo::nsUserInfo() {}
+
+nsUserInfo::~nsUserInfo() {}
+
+NS_IMETHODIMP
+nsUserInfo::GetFullname(nsAString& aFullname) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ nsCocoaUtils::GetStringForNSString(NSFullUserName(), aFullname);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetUsername(nsAString& aUsername) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ nsCocoaUtils::GetStringForNSString(NSUserName(), aUsername);
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetEmailAddress(nsAString& aEmailAddress) {
+ NS_OBJC_BEGIN_TRY_IGNORE_BLOCK
+
+ aEmailAddress.Truncate();
+ // Try to get this user's primary email from the system addressbook's "me card"
+ // (if they've filled it)
+ ABPerson* me = [[ABAddressBook sharedAddressBook] me];
+ ABMultiValue* emailAddresses = [me valueForProperty:kABEmailProperty];
+ if ([emailAddresses count] > 0) {
+ // get the index of the primary email, in case there are more than one
+ int primaryEmailIndex = [emailAddresses indexForIdentifier:[emailAddresses primaryIdentifier]];
+ nsCocoaUtils::GetStringForNSString([emailAddresses valueAtIndex:primaryEmailIndex],
+ aEmailAddress);
+ }
+
+ NS_OBJC_END_TRY_IGNORE_BLOCK
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetDomain(nsAString& aDomain) {
+ GetEmailAddress(aDomain);
+ int32_t index = aDomain.FindChar('@');
+ if (index != -1) {
+ // chop off everything before, and including the '@'
+ aDomain.Cut(0, index + 1);
+ }
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsUserInfoUnix.cpp b/comm/mailnews/base/src/nsUserInfoUnix.cpp
new file mode 100644
index 0000000000..559b68f062
--- /dev/null
+++ b/comm/mailnews/base/src/nsUserInfoUnix.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsUserInfo.h"
+#include "nsCRT.h"
+
+#include <pwd.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/utsname.h>
+
+#include "nsString.h"
+#include "nsReadableUtils.h"
+#include "nsNativeCharsetUtils.h"
+
+nsUserInfo::nsUserInfo() {}
+
+nsUserInfo::~nsUserInfo() {}
+
+NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo)
+
+NS_IMETHODIMP
+nsUserInfo::GetFullname(nsAString& aFullname) {
+ aFullname.Truncate();
+ struct passwd* pw = nullptr;
+
+ pw = getpwuid(geteuid());
+
+ if (!pw || !pw->pw_gecos) return NS_OK;
+
+ nsAutoCString fullname(pw->pw_gecos);
+
+ // now try to parse the GECOS information, which will be in the form
+ // Full Name, <other stuff> - eliminate the ", <other stuff>
+ // also, sometimes GECOS uses "&" to mean "the user name" so do
+ // the appropriate substitution
+
+ // truncate at first comma (field delimiter)
+ int32_t index;
+ if ((index = fullname.Find(",")) != kNotFound) fullname.Truncate(index);
+
+ // replace ampersand with username
+ if (pw->pw_name) {
+ nsAutoCString username(pw->pw_name);
+ if (!username.IsEmpty())
+ username.SetCharAt(nsCRT::ToUpper(username.CharAt(0)), 0);
+
+ fullname.ReplaceSubstring("&", username.get());
+ }
+
+ CopyUTF8toUTF16(fullname, aFullname);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetUsername(nsAString& aUsername) {
+ aUsername.Truncate();
+ struct passwd* pw = nullptr;
+
+ // is this portable? those are POSIX compliant calls, but I need to check
+ pw = getpwuid(geteuid());
+
+ if (!pw || !pw->pw_name) return NS_OK;
+
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(pw->pw_name), aUsername);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetDomain(nsAString& aDomain) {
+ aDomain.Truncate();
+ struct utsname buf;
+ char* domainname = nullptr;
+
+ if (uname(&buf) < 0) {
+ return NS_OK;
+ }
+
+#if defined(__linux__)
+ domainname = buf.domainname;
+#endif
+
+ if (domainname && domainname[0]) {
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(domainname), aDomain);
+ } else {
+ // try to get the hostname from the nodename
+ // on machines that use DHCP, domainname may not be set
+ // but the nodename might.
+ if (buf.nodename[0]) {
+ // if the nodename is foo.bar.org, use bar.org as the domain
+ char* pos = strchr(buf.nodename, '.');
+ if (pos) {
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(pos + 1), aDomain);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetEmailAddress(nsAString& aEmailAddress) {
+ // use username + "@" + domain for the email address
+
+ nsString username;
+ nsString domain;
+
+ GetUsername(username);
+ GetDomain(domain);
+
+ if (!username.IsEmpty() && !domain.IsEmpty()) {
+ aEmailAddress = username;
+ aEmailAddress.Append('@');
+ aEmailAddress += domain;
+ } else {
+ aEmailAddress.Truncate();
+ }
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/src/nsUserInfoWin.cpp b/comm/mailnews/base/src/nsUserInfoWin.cpp
new file mode 100644
index 0000000000..a5b69cf9a2
--- /dev/null
+++ b/comm/mailnews/base/src/nsUserInfoWin.cpp
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsUserInfo.h"
+
+#include "mozilla/ArrayUtils.h" // ArrayLength
+#include "nsString.h"
+#include "windows.h"
+#include "nsCRT.h"
+
+#define SECURITY_WIN32
+#include "lm.h"
+#include "security.h"
+
+nsUserInfo::nsUserInfo() {}
+
+nsUserInfo::~nsUserInfo() {}
+
+NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo)
+
+NS_IMETHODIMP
+nsUserInfo::GetUsername(nsAString& aUsername) {
+ aUsername.Truncate();
+
+ // UNLEN is the max username length as defined in lmcons.h
+ wchar_t username[UNLEN + 1];
+ DWORD size = mozilla::ArrayLength(username);
+ if (!GetUserNameW(username, &size)) return NS_OK;
+
+ aUsername.Assign(username);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetFullname(nsAString& aFullname) {
+ aFullname.Truncate();
+
+ wchar_t fullName[512];
+ DWORD size = mozilla::ArrayLength(fullName);
+
+ if (GetUserNameExW(NameDisplay, fullName, &size)) {
+ aFullname.Assign(fullName);
+ } else {
+ // Try to use the net APIs regardless of the error because it may be
+ // able to obtain the information.
+ wchar_t username[UNLEN + 1];
+ size = mozilla::ArrayLength(username);
+ if (!GetUserNameW(username, &size)) {
+ return NS_OK;
+ }
+
+ const DWORD level = 2;
+ LPBYTE info;
+ // If the NetUserGetInfo function has no full name info it will return
+ // success with an empty string.
+ NET_API_STATUS status = NetUserGetInfo(nullptr, username, level, &info);
+ if (status != NERR_Success) {
+ return NS_OK;
+ }
+
+ aFullname.Assign(reinterpret_cast<USER_INFO_2*>(info)->usri2_full_name);
+ NetApiBufferFree(info);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetDomain(nsAString& aDomain) {
+ aDomain.Truncate();
+
+ const DWORD level = 100;
+ LPBYTE info;
+ NET_API_STATUS status = NetWkstaGetInfo(nullptr, level, &info);
+ if (status == NERR_Success) {
+ aDomain.Assign(reinterpret_cast<WKSTA_INFO_100*>(info)->wki100_langroup);
+ NetApiBufferFree(info);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsUserInfo::GetEmailAddress(nsAString& aEmailAddress) {
+ aEmailAddress.Truncate();
+
+ // RFC3696 says max length of an email address is 254
+ wchar_t emailAddress[255];
+ DWORD size = mozilla::ArrayLength(emailAddress);
+
+ if (!GetUserNameExW(NameUserPrincipal, emailAddress, &size)) {
+ return NS_OK;
+ }
+
+ aEmailAddress.Assign(emailAddress);
+ return NS_OK;
+}
diff --git a/comm/mailnews/base/test/.eslintrc.js b/comm/mailnews/base/test/.eslintrc.js
new file mode 100644
index 0000000000..5816519fbb
--- /dev/null
+++ b/comm/mailnews/base/test/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/valid-jsdoc"],
+};
diff --git a/comm/mailnews/base/test/TestMsgStripRE.cpp b/comm/mailnews/base/test/TestMsgStripRE.cpp
new file mode 100644
index 0000000000..eae217dcb2
--- /dev/null
+++ b/comm/mailnews/base/test/TestMsgStripRE.cpp
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsCOMPtr.h"
+#include "nsMsgUtils.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsISupportsPrimitives.h"
+#include "nsString.h"
+
+#define STRING_SIZE 255
+struct testInfo {
+ char encodedInput[STRING_SIZE];
+ char expectedOutput[STRING_SIZE];
+ bool expectedDidModify;
+};
+
+int testStripRe(const char* encodedInput, char* expectedOutput,
+ bool expectedDidModify) {
+ // call NS_StripRE with the appropriate args
+ nsCString modifiedSubject;
+ bool didModify;
+ didModify = NS_MsgStripRE(nsDependentCString(encodedInput), modifiedSubject);
+
+ // make sure we got the right results
+ if (didModify != expectedDidModify) return 2;
+
+ if (didModify) {
+ if (strcmp(expectedOutput, modifiedSubject.get())) {
+ return 3;
+ }
+ } else if (strcmp(expectedOutput, encodedInput)) {
+ return 4;
+ }
+
+ // test passed
+ return 0;
+}
+
+// int main(int argc, char** argv)
+TEST(TestMsgStripRE, TestMsgStripREMain)
+{
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch(
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv));
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ // set localizedRe pref, value "SV,ÆØÅ",
+ // \xC3\x86, \xC3\x98 and \xC3\x85 are the UTF-8 encodings of Æ, Ø and Å.
+ rv = prefBranch->SetStringPref("mailnews.localizedRe",
+ "SV,\xC3\x86\xC3\x98\xC3\x85"_ns);
+ EXPECT_TRUE(NS_SUCCEEDED(rv));
+
+ // run our tests
+ struct testInfo testInfoStructs[] = {
+ // Note that re-encoding always happens in UTF-8.
+ {"SV: =?ISO-8859-1?Q?=C6blegr=F8d?=", "=?UTF-8?B?w4ZibGVncsO4ZA==?=",
+ true},
+ {"=?ISO-8859-1?Q?SV=3A=C6blegr=F8d?=", "=?UTF-8?B?w4ZibGVncsO4ZA==?=",
+ true},
+
+ // Note that in the next two tests, the only ISO-8859-1 chars are in the
+ // localizedRe piece, so once they've been stripped, the re-encoding
+ // process simply writes out ASCII rather than an ISO-8859-1 encoded
+ // string with no actual ISO-8859-1 special characters, which seems
+ // reasonable.
+ {"=?ISO-8859-1?Q?=C6=D8=C5=3A_Foo_bar?=", "Foo bar", true},
+ {"=?ISO-8859-1?Q?=C6=D8=C5=3AFoo_bar?=", "Foo bar", true}};
+
+ bool allTestsPassed = true;
+ int result;
+ for (unsigned int i = 0; i < MOZ_ARRAY_LENGTH(testInfoStructs); i++) {
+ result = testStripRe(testInfoStructs[i].encodedInput,
+ testInfoStructs[i].expectedOutput,
+ testInfoStructs[i].expectedDidModify);
+ if (result) {
+ printf("Failed: %s, i=%d | result=%d\n", __FILE__, i, result);
+ allTestsPassed = false;
+ }
+ EXPECT_TRUE(result == 0);
+ }
+
+ if (allTestsPassed) {
+ printf("all tests passed\n");
+ }
+ EXPECT_TRUE(allTestsPassed);
+}
diff --git a/comm/mailnews/base/test/gtest/TestHeaderReader.cpp b/comm/mailnews/base/test/gtest/TestHeaderReader.cpp
new file mode 100644
index 0000000000..326279655a
--- /dev/null
+++ b/comm/mailnews/base/test/gtest/TestHeaderReader.cpp
@@ -0,0 +1,205 @@
+#include "gtest/gtest.h"
+#include "nsString.h"
+#include "HeaderReader.h"
+#include "nsTArray.h"
+#include "mozilla/ArrayUtils.h"
+
+// Invocation:
+// $ ./mach gtest "TestHeaderReader.*"
+
+TEST(TestHeaderReader, Basic)
+{
+ struct hdr {
+ nsCString name;
+ nsCString rawValue;
+ };
+
+ struct {
+ nsCString raw;
+ nsTArray<hdr> expected;
+ bool isComplete; // expect to find complete header block?
+ nsCString leftOver; // Data which should be left unprocessed.
+ } testMsgs[] = {
+ // Simple case works?
+ {
+ "Message-ID: one\r\n"
+ "To: alice@example.com\r\n"
+ "\r\n"
+ "Body here. HeaderReader should have stopped by now.\r\n"
+ "Note: this line looks like a header, but it isn't!\r\n"_ns,
+ {
+ {"Message-ID"_ns, "one"_ns},
+ {"To"_ns, "alice@example.com"_ns},
+ },
+ true,
+ "Body here. HeaderReader should have stopped by now.\r\n"
+ "Note: this line looks like a header, but it isn't!\r\n"_ns,
+ },
+ // Handle folded header values correctly?
+ {
+ "To: bob@example.com\r\n"
+ "Subject: Do\r\n"
+ " we\r\n"
+ " handle\r\n"
+ "\tfolded\r\n" // Can fold with tabs too.
+ " fields OK?\r\n"
+ "Message-ID: two\r\n"
+ "\r\n"
+ "...message body here...\r\n"_ns,
+ {
+ {"To"_ns, "bob@example.com"_ns},
+ {"Subject"_ns,
+ "Do\r\n we\r\n handle\r\n\tfolded\r\n fields OK?"_ns},
+ {"Message-ID"_ns, "two"_ns},
+ },
+ true,
+ "...message body here...\r\n"_ns,
+ },
+ // Handle no whitespace after colon?
+ {
+ "Foo:bar\r\n"
+ "\r\n"_ns,
+ {
+ {"Foo"_ns, "bar"_ns},
+ },
+ true,
+ ""_ns,
+ },
+ // Folding with no text on first line?
+ // (I _think_ this is legal...)
+ {
+ "Foo: \r\n"
+ " bar\r\n"
+ "\r\n"_ns,
+ {
+ {"Foo"_ns, "\r\n bar"_ns},
+ },
+ true,
+ ""_ns,
+ },
+ // Folded line with no end of header block.
+ // Input could be truncated. So we don't want this to output any headers
+ // (The missing next line could be folded or not - we just don't know).
+ {
+ "Foo:\r\n"
+ " bar\r\n"
+ " wibble\r\n"_ns,
+ {},
+ false,
+ "Foo:\r\n bar\r\n wibble\r\n"_ns,
+ },
+ // Ignore incomplete lines as expected?.
+ {
+ "Foo: bar\r\n"
+ "Wibble: this is a part"_ns, // ... "ial line".
+ {
+ {"Foo"_ns, "bar"_ns},
+ },
+ false,
+ "Wibble: this is a part"_ns,
+ },
+ // Ignore incomplete folded lines?
+ {
+ "Foo: bar\r\n"
+ "Wibble: this\r\n"
+ " value is not co"_ns, // ... "mplete".
+ {
+ {"Foo"_ns, "bar"_ns},
+ },
+ false,
+ "Wibble: this\r\n value is not co"_ns,
+ },
+ // Handle empty input without crashing?
+ {
+ ""_ns,
+ {},
+ false,
+ ""_ns,
+ }};
+
+ for (size_t i = 0; i < mozilla::ArrayLength(testMsgs); ++i) {
+ auto const& t = testMsgs[i];
+ // Collect all the headers.
+ nsTArray<HeaderReader::Hdr> gotHeaders;
+ HeaderReader rdr;
+ auto handler = [&](HeaderReader::Hdr const& hdr) {
+ gotHeaders.AppendElement(hdr);
+ return true; // Keep going.
+ };
+
+ // Simulate multiple passes over a growing buffer - we'll add a quarter
+ // of the data each pass.
+ for (uint32_t i = 1; i < 4; ++i) {
+ // Encourage each pass to use different memory.
+ nsCString fudge(t.raw.BeginReading(), i * t.raw.Length() / 4);
+ rdr.Parse(fudge, handler);
+ }
+
+ // Last pass - give the reader the entire input.
+ auto leftOver = rdr.Parse(t.raw, handler);
+
+ // Did we get all the headers we expected?
+ ASSERT_EQ(gotHeaders.Length(), t.expected.Length());
+ for (size_t i = 0; i < t.expected.Length(); ++i) {
+ auto const& expect = t.expected[i];
+ auto const& got = gotHeaders[i];
+ ASSERT_EQ(expect.name, nsCString(got.Name(t.raw)));
+ ASSERT_EQ(expect.rawValue, nsCString(got.RawValue(t.raw)));
+ }
+
+ // Correctly detected the end of the header block?
+ ASSERT_EQ(t.isComplete, rdr.IsComplete());
+
+ // Make sure processing stopped where expected.
+ ASSERT_EQ(t.leftOver, nsCString(leftOver));
+ }
+}
+
+// Check that callback can halt processing, and that it can be correctly
+// resumed.
+TEST(TestHeaderReader, Stop)
+{
+ // We'll stop each time we find a header name containing "STOP".
+ struct {
+ nsCString raw;
+ int expectedCount; // Number of headers we expect to find.
+ int expectedPasses; // Number of passes we expect to run.
+ } testMsgs[] = {
+ {"foo: bar\r\n\r\n"_ns, 1, 1},
+ {"Message-ID: one\r\n"
+ "STOP: pause here please\r\n"
+ "foo-One: nothing to see here...\r\n"
+ "foo-Two: still nothing...\r\n"
+ "\r\n"_ns,
+ 4, 2},
+ {"A: eh\r\n"
+ "B: bee\r\n"
+ "STOP_1: pause here.\r\n"
+ "C: sea\r\n"
+ "STOP_2: another pause.\r\n"
+ "E: eee\r\n"
+ "STOP_3: last one.\r\n"
+ "\r\n"_ns,
+ 7, 3},
+ };
+
+ for (size_t i = 0; i < mozilla::ArrayLength(testMsgs); ++i) {
+ auto const& t = testMsgs[i];
+ HeaderReader rdr;
+ int gotCount = 0;
+ auto handler = [&](HeaderReader::Hdr const& hdr) {
+ ++gotCount;
+ return nsCString(hdr.Name(t.raw)).Find("STOP") == kNotFound;
+ };
+
+ int passes = 0;
+ // All our examples have a complete header block.
+ while (!rdr.IsComplete()) {
+ rdr.Parse(t.raw, handler);
+ ++passes;
+ }
+ ASSERT_EQ(gotCount, t.expectedCount);
+ ASSERT_EQ(passes, t.expectedPasses);
+ ASSERT_TRUE(rdr.IsComplete());
+ }
+}
diff --git a/comm/mailnews/base/test/gtest/TestLineReader.cpp b/comm/mailnews/base/test/gtest/TestLineReader.cpp
new file mode 100644
index 0000000000..e84274fcb0
--- /dev/null
+++ b/comm/mailnews/base/test/gtest/TestLineReader.cpp
@@ -0,0 +1,200 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "nsString.h"
+#include "LineReader.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Span.h"
+
+// Invocation:
+// $ ./mach gtest "TestLineReader.*"
+
+TEST(TestLineReader, Basic)
+{
+ struct {
+ nsTArray<nsCString> chunks;
+ int expectedLines;
+ } testCases[] = {
+ // Split lines as expected?
+ {
+ {"Line one\r\nLine two\r\n"_ns},
+ 2,
+ },
+
+ // Plain LFs should be accepted.
+ {
+ {"Line one\nLine two\n"_ns},
+ 2,
+ },
+
+ // Empty lines appear as expected?
+ {
+ {"\r\n\r\n\r\n"_ns},
+ 3,
+ },
+
+ // Empty case handled?
+ {
+ {""_ns},
+ 0,
+ },
+
+ // Split in mid-CRLF.
+ {
+ {
+ "EOL split across\r"_ns,
+ "\nchunks.\r\n"_ns,
+ },
+ 2,
+ },
+
+ // Chunks join up correctly?
+ {
+ {"This single line is "_ns, "fed in as "_ns,
+ "multiple chunks\r\n"_ns},
+ 1,
+ },
+ // Handle an empty chunk OK?
+ {
+ {"foo"_ns, ""_ns,
+ "bar\n"_ns
+ "wibble\n"_ns},
+ 2,
+ },
+ // Handle lines without EOL?
+ {
+ {"This line has no EOL and relies on Flush()."_ns},
+ 1,
+ },
+ };
+
+ for (size_t i = 0; i < mozilla::ArrayLength(testCases); ++i) {
+ auto const& t = testCases[i];
+
+ // Join chunks into one string - we expect the output to be
+ // this, byte-for-byte.
+ nsCString expectedText;
+ for (auto chunk : t.chunks) {
+ expectedText.Append(chunk);
+ }
+
+ // Callback to collect the lines and count them.
+ int gotLineCount = 0;
+ nsCString gotText;
+ auto callback = [&](mozilla::Span<const char> line) {
+ ++gotLineCount;
+ gotText.Append(line);
+ return true;
+ };
+ // Parse the chunks.
+ LineReader chopper;
+ for (auto chunk : t.chunks) {
+ chopper.Feed(chunk, callback);
+ }
+ chopper.Flush(callback);
+
+ ASSERT_EQ(t.expectedLines, gotLineCount);
+ ASSERT_EQ(expectedText, gotText);
+ }
+}
+
+// Check that processing is aborted when callback returns false.
+TEST(TestLineReader, Stop)
+{
+ struct {
+ nsTArray<nsCString> chunks;
+ int expectedLines;
+ } testCases[] = {
+ // Stop at 2 lines.
+ {
+ {"Line one\nSTOP\nThis line is never seen.\n"_ns},
+ 2,
+ },
+
+ // Line split up over multiple chunks.
+ {
+ {"one\r\nST"_ns, "OP\r\nblah blah\r\n"_ns},
+ 2,
+ },
+
+ // Empty string -> no lines.
+ {
+ {""_ns},
+ 0,
+ },
+
+ // No EOL, relies on Flush().
+ {
+ {
+ "STOP"_ns,
+ },
+ 1,
+ },
+ };
+
+ for (size_t i = 0; i < mozilla::ArrayLength(testCases); ++i) {
+ auto const& t = testCases[i];
+
+ // Callback to collect the lines and count them, stopping when
+ // we find a line containing "STOP".
+ int gotLineCount = 0;
+ auto callback = [&](mozilla::Span<const char> line) -> bool {
+ ++gotLineCount;
+ return nsCString(line).Find("STOP"_ns) == kNotFound;
+ };
+ // Parse.
+ LineReader chopper;
+ for (auto chunk : t.chunks) {
+ chopper.Feed(chunk, callback);
+ }
+ chopper.Flush(callback);
+ ASSERT_EQ(t.expectedLines, gotLineCount);
+ }
+}
+
+// Test the SplitLines() fn.
+TEST(TestLineReader, SplitLines)
+{
+ struct {
+ nsCString input;
+ nsTArray<nsCString> expect; // The lines we expect to see.
+ nsCString expectLeftover; // The unconsumed data we expect at the end.
+ } testCases[] = {
+ // Empty string -> no lines.
+ {""_ns, {}, ""_ns},
+ // Incomplete line
+ {"foo"_ns, {}, "foo"_ns},
+ // Blank lines split as expected?
+ {"\r\n\r\n\r\n"_ns, {"\r\n"_ns, "\r\n"_ns, "\r\n"_ns}, ""_ns},
+ // A couple of normal-looking lines.
+ {"one\r\ntwo\r\n"_ns, {"one\r\n"_ns, "two\r\n"_ns}, ""_ns},
+ // Handles bare LFs?
+ {"one\ntwo\n"_ns, {"one\n"_ns, "two\n"_ns}, ""_ns},
+ // Ignores bare CRs?
+ {"one\rtwo\r"_ns, {}, "one\rtwo\r"_ns},
+
+ // Early-out works?
+ {"one\r\nSTOP\r\n3\r\n4\r\n"_ns,
+ {"one\r\n"_ns, "STOP\r\n"_ns},
+ "3\r\n4\r\n"_ns},
+ };
+
+ for (auto const& t : testCases) {
+ nsTArray<nsCString> got;
+ auto fn = [&](mozilla::Span<const char> line) -> bool {
+ got.AppendElement(line);
+ // Finish early if line contains "STOP".
+ return nsCString(line).Find("STOP"_ns) == kNotFound;
+ };
+ mozilla::Span<const char> leftover = SplitLines(t.input, fn);
+
+ ASSERT_EQ(t.expect.Length(), got.Length());
+ for (size_t i = 0; i < t.expect.Length(); ++i) {
+ ASSERT_EQ(t.expect[i], got[i]);
+ }
+ ASSERT_EQ(t.expectLeftover, nsCString(leftover));
+ }
+}
diff --git a/comm/mailnews/base/test/gtest/moz.build b/comm/mailnews/base/test/gtest/moz.build
new file mode 100644
index 0000000000..a0b6129efd
--- /dev/null
+++ b/comm/mailnews/base/test/gtest/moz.build
@@ -0,0 +1,15 @@
+# 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/.
+
+FINAL_LIBRARY = "xul-gtest"
+
+UNIFIED_SOURCES += [
+ "TestHeaderReader.cpp",
+ "TestLineReader.cpp",
+]
+
+# LOCAL_INCLUDES += [
+# "../../src",
+# ]
diff --git a/comm/mailnews/base/test/moz.build b/comm/mailnews/base/test/moz.build
new file mode 100644
index 0000000000..4bb676ffb3
--- /dev/null
+++ b/comm/mailnews/base/test/moz.build
@@ -0,0 +1,19 @@
+# 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/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell-imap.ini", "unit/xpcshell.ini"]
+
+FINAL_LIBRARY = "xul-gtest"
+
+UNIFIED_SOURCES += [
+ "TestMsgStripRE.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/netwerk/test",
+ "/xpcom/tests",
+]
+
+TEST_DIRS += ["gtest"]
diff --git a/comm/mailnews/base/test/unit/data/folderCache.json b/comm/mailnews/base/test/unit/data/folderCache.json
new file mode 100644
index 0000000000..e593e6cbb9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/data/folderCache.json
@@ -0,0 +1,206 @@
+{
+ "/foo/bar/profile-default/ImapMail/imap.localhost": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": -1,
+ "totalUnreadMsgs": -1
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Archives.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Archives",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Drafts.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Drafts",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/INBOX.msf": {
+ "MRMTime": "1628202592",
+ "MRUTime": "1628202593",
+ "aclFlags": 0,
+ "boxFlags": 2097216,
+ "expungedBytes": 11536,
+ "flags": 2282237972,
+ "folderSize": 22906,
+ "hierDelim": 46,
+ "lastSyncTimeInSec": 0,
+ "nextUID": 2,
+ "onlineName": "INBOX",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 2,
+ "serverUnseen": 0,
+ "totalMsgs": 2,
+ "totalUnreadMsgs": 1
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Junk.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Junk",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Sent.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Sent",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Templates.msf": {
+ "aclFlags": 0,
+ "boxFlags": 0,
+ "expungedBytes": 0,
+ "flags": 0,
+ "folderSize": -1,
+ "hierDelim": 94,
+ "onlineName": "Templates",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/Trash.msf": {
+ "aclFlags": 0,
+ "boxFlags": 262224,
+ "expungedBytes": 0,
+ "flags": 532756,
+ "folderSize": -1,
+ "hierDelim": 46,
+ "onlineName": "Trash",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 0,
+ "serverUnseen": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/blah.msf": {
+ "MRUTime": "1628202572",
+ "aclFlags": 0,
+ "boxFlags": 262208,
+ "expungedBytes": 0,
+ "flags": 134750228,
+ "folderSize": 11872,
+ "hierDelim": 46,
+ "lastSyncTimeInSec": 0,
+ "nextUID": 3,
+ "onlineName": "blah",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 2,
+ "serverUnseen": 0,
+ "totalMsgs": 2,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/ImapMail/imap.localhost/foo.msf": {
+ "MRUTime": "1628202573",
+ "aclFlags": 0,
+ "boxFlags": 262208,
+ "expungedBytes": 11536,
+ "flags": 134750228,
+ "folderSize": 10707667,
+ "hierDelim": 46,
+ "lastSyncTimeInSec": 0,
+ "nextUID": 5,
+ "onlineName": "foo",
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "serverRecent": 0,
+ "serverTotal": 4,
+ "serverUnseen": 0,
+ "totalMsgs": 3,
+ "totalUnreadMsgs": 0
+ },
+ "/foo/bar/profile-default/Mail/Local Folders": {
+ "expungedBytes": 0,
+ "flags": 28,
+ "folderName": "Local Folders",
+ "folderSize": -1,
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "totalMsgs": -1,
+ "totalUnreadMsgs": -1
+ },
+ "/foo/bar/profile-default/Mail/Local Folders/Trash.msf": {
+ "MRUTime": "1628202569",
+ "expungedBytes": 0,
+ "flags": 260,
+ "folderName": "Trash",
+ "folderSize": 0,
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0,
+ "useServerRetention": "1"
+ },
+ "/foo/bar/profile-default/Mail/Local Folders/Unsent Messages.msf": {
+ "MRUTime": "1628202570",
+ "expungedBytes": 0,
+ "flags": 2052,
+ "folderName": "Outbox",
+ "folderSize": 0,
+ "pendingMsgs": 0,
+ "pendingUnreadMsgs": 0,
+ "totalMsgs": 0,
+ "totalUnreadMsgs": 0,
+ "useServerRetention": "1"
+ }
+}
diff --git a/comm/mailnews/base/test/unit/data/panacea.dat b/comm/mailnews/base/test/unit/data/panacea.dat
new file mode 100644
index 0000000000..214176e31a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/data/panacea.dat
@@ -0,0 +1,70 @@
+// <!-- <mdb:mork:z v="1.4"/> -->
+< <(a=c)> // (f=iso-8859-1)
+ (8A=boxFlags)(8B=hierDelim)(8C=onlineName)(8D=aclFlags)(8E=serverTotal)
+ (8F=serverUnseen)(90=serverRecent)(91=indexingPriority)(92=folderName)
+ (93=applyIncomingFilters.empty)(94=applyIncomingFilters)(95=MRUTime)
+ (96=dobayes.mailnews@mozilla.org#junk.empty)
+ (97=dobayes.mailnews@mozilla.org#junk)(98=nextUID)
+ (99=lastSyncTimeInSec)(9A=useServerRetention)(9B=MRMTime)
+ (80=ns:msg:db:row:scope:folders:all)(81=ns:msg:db:table:kind:folders)
+ (82=key)(83=flags)(84=totalMsgs)(85=totalUnreadMsgs)
+ (86=pendingUnreadMsgs)(87=pendingMsgs)(88=expungedBytes)(89=folderSize)>
+
+<(80
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/INBOX.msf)(A6=88083014)(AA=2)(A8=1)(81=0)(B8=2d10)(BB=597a)
+ (A7=200040)(9D=2e)(84=INBOX)(BA=1628202593)(B5=1628202592)(86
+ =/foo/bar/profile-default/Mail/Loc\
+al Folders/Trash.msf)(87=104)(88=Trash)(AB=1628202569)(89
+ =/foo/bar/profile-default/Mail/Loc\
+al Folders/Unsent Messages.msf)(8A=804)(8C=Outbox)(AC=1628202570)(8D
+ =/foo/bar/profile-default/Mail/Loc\
+al Folders)(8E=1c)(8F=ffffffff)(82=ffffffffffffffff)(90=Local Folders)
+ (91
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Drafts.msf)(83=5e)(92=Drafts)(93
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Templates.msf)(94=Templates)(95
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Sent.msf)(96=Sent)(97
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Archives.msf)(98=Archives)(9A
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Trash.msf)(9B=82114)(9C=40050)(9E
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/blah.msf)(A0=8082014)(AF=2e60)(A1=40040)(9F=blah)(AE
+ =1628202572)(B0=3)(A2
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/foo.msf)(B9=a362d3)(A3=foo)(B2=4)(B1=1628202573)(B4=5)
+ (A4
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost/Junk.msf)(A5=Junk)(BC
+ =/foo/bar/profile-default/ImapMail\
+/imap.localhost)(BD=)>
+{1:^80 {(k^81:c)(s=9)}
+ [1(^82^80)(^83^A6)(^84=2)(^85=1)(^86=0)(^87=0)(^88^B8)(^89^BB)(^8A^A7)
+ (^8B=2e)(^8C^84)(^8D=0)(^8E=2)(^8F=0)(^90=0)(^95^BA)(^98=2)(^99=0)
+ (^9B^B5)]
+ [2(^82^86)(^83^87)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89=0)(^92^88)
+ (^95^AB)(^9A=1)]
+ [3(^82^89)(^83^8A)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89=0)(^92^8C)
+ (^95^AC)(^9A=1)]
+ [4(^82^8D)(^83=1c)(^84^8F)(^85^8F)(^86=0)(^87=0)(^88=0)(^89^82)(^92^90)]
+ [5(^82^91)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^92)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [6(^82^93)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^94)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [7(^82^95)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^96)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [8(^82^97)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^98)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [9(^82^9A)(^83^9B)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A^9C)
+ (^8B=2e)(^8C^88)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [A(^82^9E)(^83^A0)(^84=2)(^85=0)(^86=0)(^87=0)(^88=0)(^89^AF)(^8A^A1)
+ (^8B=2e)(^8C^9F)(^8D=0)(^8E=2)(^8F=0)(^90=0)(^95^AE)(^98=3)(^99=0)]
+ [B(^82^A2)(^83^A0)(^84=3)(^85=0)(^86=0)(^87=0)(^88^B8)(^89^B9)(^8A^A1)
+ (^8B=2e)(^8C^A3)(^8D=0)(^8E=4)(^8F=0)(^90=0)(^95^B1)(^98=5)(^99=0)]
+ [C(^82^A4)(^83=0)(^84=0)(^85=0)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C^A5)(^8D=0)(^8E=0)(^8F=0)(^90=0)]
+ [D(^82^BC)(^83=0)(^84^8F)(^85^8F)(^86=0)(^87=0)(^88=0)(^89^82)(^8A=0)
+ (^8B=5e)(^8C=)(^8D=0)(^8E=0)(^8F=0)(^90=0)]}
diff --git a/comm/mailnews/base/test/unit/data/panacea_empty.dat b/comm/mailnews/base/test/unit/data/panacea_empty.dat
new file mode 100644
index 0000000000..b169b34da9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/data/panacea_empty.dat
@@ -0,0 +1 @@
+// <!-- <mdb:mork:z v="1.4"/> -->
diff --git a/comm/mailnews/base/test/unit/data/remoteContent.sql b/comm/mailnews/base/test/unit/data/remoteContent.sql
new file mode 100644
index 0000000000..042cfefbd9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/data/remoteContent.sql
@@ -0,0 +1,41 @@
+-- Address book with remote content permissions for use in test_accountMigration.js.
+PRAGMA user_version = 1;
+
+CREATE TABLE cards (uid TEXT PRIMARY KEY, localId INTEGER);
+CREATE TABLE properties (card TEXT, name TEXT, value TEXT);
+CREATE TABLE lists (uid TEXT PRIMARY KEY, localId INTEGER, name TEXT, nickName TEXT, description TEXT);
+CREATE TABLE list_cards (list TEXT, card TEXT, PRIMARY KEY(list, card));
+
+INSERT INTO cards (uid, localId) VALUES
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 1),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 2),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 3);
+
+INSERT INTO properties (card, name, value) VALUES
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PrimaryEmail', 'no@test.invalid'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PhotoType', 'generic'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'LowercasePrimaryEmail', 'no@test.invalid'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PopularityIndex', '0'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PreferMailFormat', '0'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'LastModifiedDate', '0'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'PreferDisplayName', '1'),
+ ('f36c4b94-fbab-4cd6-b175-bf8242e6e757', 'AllowRemoteContent', '0'),
+
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PrimaryEmail', 'yes@test.invalid'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PhotoType', 'generic'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'LowercasePrimaryEmail', 'yes@test.invalid'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PopularityIndex', '0'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PreferMailFormat', '0'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'LastModifiedDate', '0'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'PreferDisplayName', '1'),
+ ('7ecf5197-87cd-4633-8fcb-bf4ed8f6239e', 'AllowRemoteContent', '0'),
+
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'LastModifiedDate', '1397383824'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PrimaryEmail', 'yes@test.invalid'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PhotoType', 'generic'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'SecondEmail', 'yes2@test.invalid'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'LowercasePrimaryEmail', 'yes@test.invalid'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PopularityIndex', '0'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PreferMailFormat', '0'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'PreferDisplayName', '1'),
+ ('90a1de3d-0f5d-4352-b927-60ee8205ac8f', 'AllowRemoteContent', '1');
diff --git a/comm/mailnews/base/test/unit/head_mailbase.js b/comm/mailnews/base/test/unit/head_mailbase.js
new file mode 100644
index 0000000000..9f37623291
--- /dev/null
+++ b/comm/mailnews/base/test/unit/head_mailbase.js
@@ -0,0 +1,23 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var CC = Components.Constructor;
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../";
+
+registerCleanupFunction(function () {
+ load(gDEPTH + "mailnews/resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/base/test/unit/nodelist_test.xml b/comm/mailnews/base/test/unit/nodelist_test.xml
new file mode 100644
index 0000000000..e1640b5b5b
--- /dev/null
+++ b/comm/mailnews/base/test/unit/nodelist_test.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" ?>
+
+<rootnode>
+ <node id="childnode1" />
+ <node id="childnode2" />
+ <node id="childnode3" />
+</rootnode>
diff --git a/comm/mailnews/base/test/unit/test_MsgIncomingServer.js b/comm/mailnews/base/test/unit/test_MsgIncomingServer.js
new file mode 100644
index 0000000000..184d2d4072
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_MsgIncomingServer.js
@@ -0,0 +1,224 @@
+/* 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/. */
+
+Services.prefs.setBoolPref("mailnews.imap.jsmodule", true);
+
+registerCleanupFunction(() => {
+ Services.logins.removeAllLogins();
+ Services.prefs.clearUserPref("mailnews.imap.jsmodule");
+});
+
+/**
+ * Test password is migrated when changing hostname/username.
+ */
+add_task(function testMigratePasswordOnChangeUsernameHostname() {
+ // Add two logins.
+ let loginItems = [
+ ["news://news.localhost", "user-nntp", "password-nntp"],
+ ["mailbox://pop3.localhost", "user-pop", "password-pop"],
+ ];
+ for (let [uri, username, password] of loginItems) {
+ let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ login.init(uri, null, uri, username, password, "", "");
+ Services.logins.addLogin(login);
+ }
+
+ // Create a nntp server, check the password can be found correctly.
+ let nntpIncomingServer = MailServices.accounts.createIncomingServer(
+ "user-nntp",
+ "news.localhost",
+ "nntp"
+ );
+ nntpIncomingServer.getPasswordWithUI("", "");
+ equal(nntpIncomingServer.password, "password-nntp");
+
+ // Change the username, check password can be found using the new username.
+ nntpIncomingServer.username = "nntp";
+ let password;
+ let serverUri = "news://news.localhost";
+ for (let login of Services.logins.findLogins(serverUri, "", serverUri)) {
+ if (login.username == "nntp") {
+ password = login.password;
+ }
+ }
+ equal(password, "password-nntp");
+
+ // Create a pop3 server, check the password can be found correctly.
+ let pop3IncomingServer = MailServices.accounts.createIncomingServer(
+ "user-pop",
+ "pop3.localhost",
+ "pop3"
+ );
+ pop3IncomingServer.getPasswordWithUI("", "");
+ equal(pop3IncomingServer.password, "password-pop");
+
+ // Change the hostname, check password can be found using the new hostname.
+ pop3IncomingServer.hostName = "localhost";
+ serverUri = "mailbox://localhost";
+ for (let login of Services.logins.findLogins(serverUri, "", serverUri)) {
+ if (login.username == "user-pop") {
+ password = login.password;
+ }
+ }
+ equal(password, "password-pop");
+});
+
+/**
+ * Test identity folders are migrated when changing hostname/username.
+ */
+add_task(function testMigrateIdentitiesOnChangeUsernameHostname() {
+ // Create an imap server.
+ let incomingServer1 = MailServices.accounts.createIncomingServer(
+ "user-imap",
+ "imap.localhost",
+ "imap"
+ );
+ // Create a pop server.
+ let incomingServer2 = MailServices.accounts.createIncomingServer(
+ "user-pop",
+ "pop3.localhost",
+ "pop3"
+ );
+
+ // Create an identity and point folders to incomingServer1.
+ let identity1 = MailServices.accounts.createIdentity();
+ identity1.fccFolder = incomingServer1.serverURI + "/Sent";
+ identity1.draftFolder = incomingServer1.serverURI + "/Drafts";
+ identity1.archiveFolder = incomingServer1.serverURI + "/Archives";
+ identity1.stationeryFolder = incomingServer1.serverURI + "/Templates";
+ let account1 = MailServices.accounts.createAccount();
+ account1.addIdentity(identity1);
+ // Create another identity and point folders to both servers.
+ let identity2 = MailServices.accounts.createIdentity();
+ identity2.fccFolder = incomingServer1.serverURI + "/Sent";
+ identity2.draftFolder = incomingServer2.serverURI + "/Drafts";
+ let account2 = MailServices.accounts.createAccount();
+ account2.addIdentity(identity2);
+
+ // Check folders were correctly set.
+ equal(identity1.fccFolder, "imap://user-imap@imap.localhost/Sent");
+ equal(identity1.draftFolder, "imap://user-imap@imap.localhost/Drafts");
+ equal(identity1.archiveFolder, "imap://user-imap@imap.localhost/Archives");
+ equal(
+ identity1.stationeryFolder,
+ "imap://user-imap@imap.localhost/Templates"
+ );
+ equal(identity2.fccFolder, "imap://user-imap@imap.localhost/Sent");
+ equal(identity2.draftFolder, "mailbox://user-pop@pop3.localhost/Drafts");
+
+ // Change the hostname.
+ incomingServer1.hostName = "localhost";
+
+ // Check folders were correctly updated.
+ identity1 = MailServices.accounts.getIdentity(identity1.key);
+ equal(identity1.fccFolder, "imap://user-imap@localhost/Sent");
+ equal(identity1.draftFolder, "imap://user-imap@localhost/Drafts");
+ equal(identity1.archiveFolder, "imap://user-imap@localhost/Archives");
+ equal(identity1.stationeryFolder, "imap://user-imap@localhost/Templates");
+ equal(identity2.fccFolder, "imap://user-imap@localhost/Sent");
+ equal(identity2.draftFolder, "mailbox://user-pop@pop3.localhost/Drafts");
+});
+
+/**
+ * Test spam action prefs are migrated when changing hostname/username.
+ */
+add_task(function testMigrateSpamActionsOnChangeUsernameHostname() {
+ // Create an imap server.
+ let incomingServer1 = MailServices.accounts.createIncomingServer(
+ "user-imap",
+ "imap.localhost",
+ "imap"
+ );
+ incomingServer1.setUnicharValue(
+ "spamActionTargetFolder",
+ incomingServer1.serverURI + "/Спам"
+ );
+
+ equal(
+ incomingServer1.spamSettings.actionTargetAccount,
+ "imap://user-imap@imap.localhost"
+ );
+ equal(
+ incomingServer1.spamSettings.actionTargetFolder,
+ "imap://user-imap@imap.localhost/Спам"
+ );
+
+ // Change the username.
+ incomingServer1.username = "user";
+
+ equal(
+ incomingServer1.spamSettings.actionTargetAccount,
+ "imap://user@imap.localhost"
+ );
+ equal(
+ incomingServer1.spamSettings.actionTargetFolder,
+ "imap://user@imap.localhost/Спам"
+ );
+});
+
+/**
+ * Test filters are migrated when changing hostname/username.
+ */
+add_task(function testMigrateFiltersOnChangeUsernameHostname() {
+ // Create a nntp server.
+ let nntpIncomingServer = MailServices.accounts.createIncomingServer(
+ "user-nntp",
+ "news.localhost",
+ "nntp"
+ );
+ let filterList = nntpIncomingServer.getFilterList(null);
+
+ // Insert a CopyToFolder filter.
+ let filter = filterList.createFilter("filter1");
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.CopyToFolder;
+ action.targetFolderUri = "news://user-nntp@news.localhost/dest1";
+ filter.appendAction(action);
+ filterList.insertFilterAt(filterList.filterCount, filter);
+
+ // Insert a MarkRead filter.
+ filter = filterList.createFilter("filter2");
+ action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MarkRead;
+ filter.appendAction(action);
+ filterList.insertFilterAt(filterList.filterCount, filter);
+
+ // Insert a MoveToFolder filter.
+ filter = filterList.createFilter("filter3");
+ action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.MoveToFolder;
+ action.targetFolderUri = "news://user-nntp@news.localhost/dest2";
+ filter.appendAction(action);
+ filterList.insertFilterAt(filterList.filterCount, filter);
+
+ // Change the hostname, test targetFolderUri of filters are changed accordingly.
+ nntpIncomingServer.hostName = "localhost";
+ filterList = nntpIncomingServer.getFilterList(null);
+ filter = filterList.getFilterAt(0);
+ equal(
+ filter.sortedActionList[0].targetFolderUri,
+ "news://user-nntp@localhost/dest1"
+ );
+ filter = filterList.getFilterAt(2);
+ equal(
+ filter.sortedActionList[0].targetFolderUri,
+ "news://user-nntp@localhost/dest2"
+ );
+
+ // Change the username, test targetFolderUri of filters are changed accordingly.
+ nntpIncomingServer.username = "nntp";
+ filterList = nntpIncomingServer.getFilterList(null);
+ filter = filterList.getFilterAt(0);
+ equal(
+ filter.sortedActionList[0].targetFolderUri,
+ "news://nntp@localhost/dest1"
+ );
+ filter = filterList.getFilterAt(2);
+ equal(
+ filter.sortedActionList[0].targetFolderUri,
+ "news://nntp@localhost/dest2"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_MsgKeySet.js b/comm/mailnews/base/test/unit/test_MsgKeySet.js
new file mode 100644
index 0000000000..7ae1eea938
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_MsgKeySet.js
@@ -0,0 +1,85 @@
+/* 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 { MsgKeySet } = ChromeUtils.import("resource:///modules/MsgKeySet.jsm");
+
+/**
+ * Test MsgKeySet.addRange works correctly.
+ */
+add_task(function testAddRange() {
+ // Init an empty set.
+ let keySet = new MsgKeySet();
+ ok(!keySet.has(1));
+
+ // Add two ranges.
+ keySet.addRange(90, 99);
+ keySet.addRange(2, 19);
+
+ // Test members.
+ ok(!keySet.has(1));
+ ok(keySet.has(2));
+ ok(keySet.has(16));
+ ok(!keySet.has(20));
+ ok(keySet.has(99));
+ equal(keySet.toString(), "2-19,90-99");
+
+ // Init a set from a string.
+ keySet = new MsgKeySet("102,199");
+ ok(!keySet.has(22));
+ ok(keySet.has(199));
+
+ // Add two ranges.
+ keySet.addRange(2, 19);
+ keySet.addRange(12, 29);
+
+ // Test members.
+ ok(keySet.has(2));
+ ok(keySet.has(22));
+ ok(keySet.has(199));
+ equal(keySet.toString(), "2-29,102,199");
+});
+
+/**
+ * Test MsgKeySet.add works correctly.
+ */
+add_task(function testAdd() {
+ // Init an empty set.
+ let keySet = new MsgKeySet();
+ ok(!keySet.has(1));
+
+ // Add three values.
+ keySet.add(1);
+ keySet.add(2);
+ keySet.add(4);
+
+ // Test members.
+ ok(keySet.has(1));
+ ok(keySet.has(2));
+ ok(!keySet.has(3));
+ ok(keySet.has(4));
+ equal(keySet.toString(), "1-2,4");
+});
+
+/**
+ * Test MsgKeySet.getLastMissingRange works correctly.
+ */
+add_task(function testGetLastMissingRange() {
+ // Init a set.
+ let keySet = new MsgKeySet("2-9,12-29");
+
+ // Test `start` should be a value not already in keySet.
+ let [start, end] = keySet.getLastMissingRange(2, 33);
+ equal(start, 30);
+ equal(end, 33);
+
+ // Test `start` should be the lowest input value.
+ [start, end] = keySet.getLastMissingRange(33, 33);
+ equal(start, 33);
+ equal(end, 33);
+
+ // Test get missing old messages range works.
+ [start, end] = keySet.getLastMissingRange(3, 23);
+ equal(start, 10);
+ equal(end, 11);
+});
diff --git a/comm/mailnews/base/test/unit/test_accountMgr.js b/comm/mailnews/base/test/unit/test_accountMgr.js
new file mode 100644
index 0000000000..6036dccfa9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgr.js
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that we cleanup the account prefs when the account manager is
+ * loaded. This entails removing duplicate accounts from
+ * mail.accountmanager.accounts list, and removing duplicate accounts with
+ * the same server.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create account prefs with both kinds of duplication.
+
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account4.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account4.server", "server4");
+ Services.prefs.setCharPref("mail.account.account5.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account5.server", "server5");
+ Services.prefs.setCharPref("mail.account.account6.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account6.server", "server5");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+ Services.prefs.setCharPref("mail.server.server2.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server2.type", "none");
+ Services.prefs.setCharPref("mail.server.server2.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server2.directory-rel",
+ "[ProfD]Mail/Local Folders-1"
+ );
+ Services.prefs.setCharPref("mail.server.server4.hostname", "mail.host4.org");
+ Services.prefs.setCharPref("mail.server.server4.type", "pop3");
+ Services.prefs.setCharPref("mail.server.server5.hostname", "pop3.host.org");
+ Services.prefs.setCharPref("mail.server.server5.type", "pop3");
+ Services.prefs.setCharPref(
+ "mail.server.server5.deferred_to_account",
+ "account2"
+ );
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account4,account5,account5,account6,account1,account2"
+ );
+ // Set the default account to one we're going to get rid of. The account
+ // manager should recover relatively gracefully.
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account6");
+
+ // This will force the load of the accounts setup above.
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+ // Here all the accounts are local but the first account will behave as
+ // an actual local account and will be kept last always.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account4,account5,account1"
+ );
+ let server5 = MailServices.accounts
+ .getIncomingServer("server5")
+ .QueryInterface(Ci.nsIPop3IncomingServer);
+ Assert.equal(server5.deferredToAccount, "account1");
+
+ // Just make sure this doesn't throw an exception, because we did remove the
+ // default account.
+ let defaultAccount = MailServices.accounts.defaultAccount;
+ Assert.equal(defaultAccount, null);
+
+ // Remove an account, and verify that the account list pref looks OK:
+ let server = MailServices.accounts.getIncomingServer("server4");
+
+ // We need to get the root folder to read from the folder cache
+ // before it gets removed or else we'll assert, because we're
+ // not completely initialized...
+ server.rootFolder.flags;
+
+ MailServices.accounts.removeAccount(
+ MailServices.accounts.FindAccountForServer(server)
+ );
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account5,account1"
+ );
+ // Make sure cleaning up duplicate accounts didn't hork accounts.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.account.account1.server"),
+ "server1"
+ );
+ Assert.equal(
+ Services.prefs.getCharPref("mail.account.account5.server"),
+ "server5"
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_accountMgr2.js b/comm/mailnews/base/test/unit/test_accountMgr2.js
new file mode 100644
index 0000000000..a48685da00
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgr2.js
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests various methods and attributes on nsIMsgAccountManager.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_task(async function () {
+ // Create a couple of test accounts.
+ let acc1 = MailServices.accounts.createAccount();
+ acc1.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_imap",
+ "imap.example.com",
+ "imap"
+ );
+ let id1 = MailServices.accounts.createIdentity();
+ id1.email = "bob_imap@example.com";
+ acc1.addIdentity(id1);
+
+ let acc2 = MailServices.accounts.createAccount();
+ acc2.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_pop3",
+ "pop3.EXAMPLE.com.",
+ "pop3"
+ );
+ let id2 = MailServices.accounts.createIdentity();
+ id2.email = "bob_pop3@example.com";
+ acc2.addIdentity(id2);
+
+ // Add an identity shared by both accounts.
+ let id3 = MailServices.accounts.createIdentity();
+ id3.email = "bob_common@example.com";
+ acc1.addIdentity(id3);
+ acc2.addIdentity(id3);
+
+ // The special "Local Folders" account and server (server type is "none").
+ MailServices.accounts.createLocalMailAccount();
+
+ // Setup done. Now check that things are as we expect.
+
+ // At this point we should have 3 accounts and servers (imap, pop, local).
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+ Assert.equal(MailServices.accounts.allServers.length, 3);
+
+ // The identities we explicitly created.
+ Assert.equal(MailServices.accounts.allIdentities.length, 3);
+
+ // Check we find the right number of identities associated with each server.
+ Assert.equal(
+ MailServices.accounts.getIdentitiesForServer(acc1.incomingServer).length,
+ 2
+ );
+ Assert.equal(
+ MailServices.accounts.getIdentitiesForServer(acc2.incomingServer).length,
+ 2
+ );
+ Assert.equal(
+ MailServices.accounts.getIdentitiesForServer(
+ MailServices.accounts.localFoldersServer
+ ).length,
+ 0
+ );
+
+ // id1 and id2 are on separate accounts (and servers).
+ Assert.equal(MailServices.accounts.getServersForIdentity(id1).length, 1);
+ Assert.equal(MailServices.accounts.getServersForIdentity(id2).length, 1);
+ // id3 is shared.
+ Assert.equal(MailServices.accounts.getServersForIdentity(id3).length, 2);
+
+ // Does allFolders return the default folders we'd expect?
+ // IMAP has Inbox only.
+ // POP3 and local accounts both have Inbox and Trash.
+ Assert.equal(MailServices.accounts.allFolders.length, 1 + 2 + 2);
+
+ // Let's ditch the IMAP account.
+ MailServices.accounts.removeAccount(acc1);
+
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+ Assert.equal(MailServices.accounts.allServers.length, 2);
+
+ // It should have taken the imap-specific identity with it.
+ Assert.equal(MailServices.accounts.allIdentities.length, 2);
+
+ // Test a special hostname.
+ const acc4 = MailServices.accounts.createAccount();
+ acc4.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_unavail",
+ "0.0.0.0.", // Note ending dot which would not do anything for an IP.
+ "pop3"
+ );
+ let id4 = MailServices.accounts.createIdentity();
+ id4.email = "bob_unavail@example.com";
+ acc4.addIdentity(id4);
+
+ Assert.equal(
+ MailServices.accounts.accounts.length,
+ 3,
+ "acc4 should be in accounts"
+ );
+
+ // Test that an account with empty server hostname doesn't even get listed.
+ const serverKey = acc4.incomingServer.key;
+ Services.prefs.setStringPref(`mail.server.${serverKey}.hostname`, "");
+ MailServices.accounts.unloadAccounts();
+ MailServices.accounts.loadAccounts();
+ Assert.equal(
+ MailServices.accounts.accounts.length,
+ 2,
+ "invalid acc4 should have been removed"
+ );
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account3",
+ "listed accounts should be correct after testing blank host"
+ );
+
+ // Test that an account that had punycode hostname entered is found.
+ const acc5 = MailServices.accounts.createAccount();
+ acc5.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_imap5",
+ "xn--thnderbird-beb.test",
+ "imap"
+ );
+ const id5 = MailServices.accounts.createIdentity();
+ id5.email = "bob_imap5@xn--thnderbird-beb.test";
+ acc5.addIdentity(id5);
+
+ MailServices.accounts.unloadAccounts();
+ MailServices.accounts.loadAccounts();
+
+ Assert.equal(
+ MailServices.accounts.accounts.length,
+ 3,
+ "added acc5 should still be listed"
+ );
+
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account5,account3",
+ "listed accounts should be correct after testing punycode host"
+ );
+ const punyServer = MailServices.accounts.findServerByURI(
+ Services.io.newURI("imap://xn--thnderbird-beb.test:143/INBOX")
+ );
+ Assert.ok(
+ punyServer?.hostName,
+ "should find server by uri for punycode hostname"
+ );
+
+ const punyServer2 = MailServices.accounts.findServerByURI(
+ Services.io.newURI("imap://thünderbird.test:143/INBOX")
+ );
+ Assert.ok(
+ punyServer2?.hostName,
+ "should find ACE server by normalized IDN hostname"
+ );
+
+ // Test that an account with IDN hostname entered is found.
+ const acc6 = MailServices.accounts.createAccount();
+ acc6.incomingServer = MailServices.accounts.createIncomingServer(
+ "bob_imap6",
+ "thünderbird.example",
+ "imap"
+ );
+ const id6 = MailServices.accounts.createIdentity();
+ id6.email = "bob_imap6@thünderbird.example";
+ acc6.addIdentity(id6);
+
+ MailServices.accounts.unloadAccounts();
+ MailServices.accounts.loadAccounts();
+
+ Assert.equal(
+ MailServices.accounts.accounts.length,
+ 4,
+ "added acc6 should still be listed"
+ );
+
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account5,account6,account3",
+ "listed accounts should be correct after testing IDN host"
+ );
+ const idnServer = MailServices.accounts.findServerByURI(
+ Services.io.newURI("imap://thünderbird.example:143/INBOX")
+ );
+ Assert.ok(idnServer?.hostName, "should find server by uri for IDN hostname");
+
+ const idnServer2 = MailServices.accounts.findServerByURI(
+ Services.io.newURI("imap://xn--thnderbird-beb.example:143/INBOX")
+ );
+ Assert.ok(
+ idnServer2?.hostName,
+ "should find idn server by by ACE encodeed uri"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_accountMgrCustomTypes.js b/comm/mailnews/base/test/unit/test_accountMgrCustomTypes.js
new file mode 100644
index 0000000000..f8b8f707c7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgrCustomTypes.js
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that accounts with invalid types, such as could be created
+ * from an extension, do not disappear immediately when the extension
+ * is unloaded.
+ *
+ * Adapted from test_AccountMgr.js by Kent James <kent@caspia.com>
+ */
+
+function run_test() {
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+
+ // Here we are simulating a server and account that is added by an
+ // extension, but that extension is currently unloaded. The extension
+ // added "secondsToLeaveUnavailable" (though a typical value would be
+ // one month, not 2 seconds!) to tell the core code to leave this alone
+ // for awhile if the extension is unloaded.
+ Services.prefs.setCharPref("mail.server.server2.hostname", "pop3.host.org");
+ Services.prefs.setCharPref("mail.server.server2.type", "invalid");
+ Services.prefs.setIntPref("mail.server.server2.secondsToLeaveUnavailable", 2);
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account2,account1"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account1");
+
+ // This will force the load of the accounts setup above.
+ // We don't see the invalid account.
+ Assert.equal(MailServices.accounts.accounts.length, 1);
+
+ // But it is really there.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account1"
+ );
+
+ // Add a new account (so that we can check if this clobbers the existing
+ // inactive account or its server).
+ let newAccount = MailServices.accounts.createAccount();
+ let newIdentity = MailServices.accounts.createIdentity();
+ newAccount.addIdentity(newIdentity);
+ newAccount.defaultIdentity = newIdentity;
+ newAccount.incomingServer = MailServices.accounts.createIncomingServer(
+ "somename",
+ "somehost.example.com",
+ "pop3"
+ );
+
+ // No collisions with the inactive account.
+ Assert.notEqual(newIdentity.key, "id2");
+ Assert.notEqual(newAccount.incomingServer.key, "server2");
+ Assert.notEqual(newAccount.key, "account2");
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+
+ MailServices.accounts.unloadAccounts();
+
+ // Set the unavailable account to a valid type, and watch it appear.
+ Services.prefs.setCharPref("mail.server.server2.type", "pop3");
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+
+ // Make it bad again, and reload it to restart the timeout before delete.
+ MailServices.accounts.unloadAccounts();
+ Services.prefs.setCharPref("mail.server.server2.type", "invalid");
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+ MailServices.accounts.unloadAccounts();
+
+ // Now let the bad type timeout, and watch it magically disappear!
+ do_test_pending();
+ do_timeout(3000, function () {
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+
+ // It is now gone.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ newAccount.key + ",account1"
+ );
+
+ do_test_finished();
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_accountMgrMovedLocalFolders.js b/comm/mailnews/base/test/unit/test_accountMgrMovedLocalFolders.js
new file mode 100644
index 0000000000..5972012e85
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgrMovedLocalFolders.js
@@ -0,0 +1,43 @@
+/* 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"
+);
+
+function run_test() {
+ // Create account prefs with Local Folders in the middle
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account4.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account4.server", "server4");
+ Services.prefs.setCharPref("mail.account.account5.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account5.server", "server5");
+ Services.prefs.setCharPref("mail.account.account6.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account6.server", "server5");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+ Services.prefs.setCharPref("mail.server.server4.hostname", "mail.host4.org");
+ Services.prefs.setCharPref("mail.server.server4.type", "pop3");
+ Services.prefs.setCharPref("mail.server.server5.hostname", "pop3.host.org");
+ Services.prefs.setCharPref("mail.server.server5.type", "pop3");
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account4,account1,account5"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account4");
+
+ // This will force the load of the accounts setup above.
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account4,account1,account5"
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_accountMgrRemoveDefault.js b/comm/mailnews/base/test/unit/test_accountMgrRemoveDefault.js
new file mode 100644
index 0000000000..640c314499
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMgrRemoveDefault.js
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that if the default account is removed, the default becomes
+ * another account or null. The removed account must not remain the default.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create account prefs.
+
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account3.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account3.server", "server3");
+
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server2.hostname", "host2.invalid");
+ Services.prefs.setCharPref("mail.server.server2.type", "pop3");
+ Services.prefs.setCharPref("mail.server.server3.hostname", "host3.invalid");
+ Services.prefs.setCharPref("mail.server.server3.type", "pop3");
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account2,account3,account1"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account3");
+
+ // Load of the accounts setup above.
+ Assert.equal(MailServices.accounts.accounts.length, 3);
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account2,account3,account1"
+ );
+ Assert.equal(MailServices.accounts.defaultAccount?.key, "account3");
+
+ // Remove the default account, account3. The default should be set to a
+ // sensible replacement, account2.
+
+ MailServices.accounts.removeAccount(MailServices.accounts.defaultAccount);
+ Assert.equal(MailServices.accounts.defaultAccount?.key, "account2");
+
+ // Remove the default account, account2. No remaining accounts can be the
+ // default, so it should become null.
+
+ MailServices.accounts.removeAccount(MailServices.accounts.defaultAccount);
+ Assert.equal(MailServices.accounts.defaultAccount, null);
+}
diff --git a/comm/mailnews/base/test/unit/test_accountMigration.js b/comm/mailnews/base/test/unit/test_accountMigration.js
new file mode 100644
index 0000000000..d2bb801c49
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_accountMigration.js
@@ -0,0 +1,184 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that we don't try to reset the mail.server.server<n>.authMethod
+ * preference every time we run the migration code, and other migration stuff
+ */
+
+var { migrateMailnews } = ChromeUtils.import(
+ "resource:///modules/MailnewsMigrator.jsm"
+);
+
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+function testPermission(aURI) {
+ let principal = Services.scriptSecurityManager.createContentPrincipal(
+ aURI,
+ {}
+ );
+ return Services.perms.testPermissionFromPrincipal(principal, "image");
+}
+
+function run_test() {
+ // Set up some basic accounts with limited prefs - enough to satisfy the
+ // migrator.
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account3.server", "server3");
+
+ // Server1 has nothing set.
+
+ // Server2 has useSecAuth set to true, auth_login unset
+ Services.prefs.setBoolPref("mail.server.server2.useSecAuth", true);
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account1,account2,account3"
+ );
+
+ // Set server1 and server2 username and hostname to test clientid population.
+ Services.prefs.setCharPref("mail.server.server1.userName", "testuser1");
+ Services.prefs.setCharPref("mail.server.server2.userName", "testuser2");
+ Services.prefs.setCharPref("mail.server.server1.type", "imap");
+ Services.prefs.setCharPref("mail.server.server2.type", "imap");
+ Services.prefs.setCharPref(
+ "mail.server.server1.hostname",
+ "mail.sampledomain1.com"
+ );
+ Services.prefs.setCharPref(
+ "mail.server.server2.hostname",
+ "mail.sampledomain2.com"
+ );
+
+ loadABFile("data/remoteContent", kPABData.fileName);
+
+ let uriAllowed = Services.io.newURI(
+ "chrome://messenger/content/email=yes@test.invalid"
+ );
+ let uriAllowed2 = Services.io.newURI(
+ "chrome://messenger/content/email=yes2@test.invalid"
+ );
+ let uriDisallowed = Services.io.newURI(
+ "chrome://messenger/content/email=no@test.invalid"
+ );
+
+ // Check that this email that according to the ab data has (had!)
+ // remote content premissions, has no premissions pre migration.
+ Assert.equal(testPermission(uriAllowed), Services.perms.UNKNOWN_ACTION);
+ Assert.equal(testPermission(uriAllowed2), Services.perms.UNKNOWN_ACTION);
+ Assert.equal(testPermission(uriDisallowed), Services.perms.UNKNOWN_ACTION);
+
+ // Now migrate the prefs.
+ migrateMailnews();
+
+ // Check that server1 and server2 have the same clientid.
+ Assert.ok(Services.prefs.prefHasUserValue("mail.server.server1.clientid"));
+ Assert.ok(Services.prefs.prefHasUserValue("mail.server.server2.clientid"));
+
+ // Check that server3 didn't get a clientid, since it has no userName.
+ // This is the case for nntp.
+ Assert.ok(!Services.prefs.prefHasUserValue("mail.server.server3.clientid"));
+
+ // Check what has been set.
+ Assert.ok(!Services.prefs.prefHasUserValue("mail.server.server1.authMethod"));
+ Assert.ok(Services.prefs.prefHasUserValue("mail.server.server2.authMethod"));
+ Assert.equal(
+ Services.prefs.getIntPref("mail.server.server2.authMethod"),
+ Ci.nsMsgAuthMethod.secure
+ );
+
+ // Now clear the authMethod for set for server2. This simulates the user
+ // setting the value back to "3", i.e. Ci.nsMsgAuthMethod.passwordCleartext.
+ Services.prefs.clearUserPref("mail.server.server2.authMethod");
+
+ // Now attempt migration again, e.g. a second load of TB
+ migrateMailnews();
+
+ // This time around, both of these should not be set.
+ Assert.ok(!Services.prefs.prefHasUserValue("mail.server.server1.authMethod"));
+ Assert.ok(!Services.prefs.prefHasUserValue("mail.server.server2.authMethod"));
+
+ //
+ // Now check SMTP
+ //
+
+ Services.prefs.setCharPref("mail.smtpservers", "smtp1,smtp2,smtp3");
+
+ // smtp1 has nothing set.
+
+ // smtp2 has useSecAuth set to true, auth_method unset
+ Services.prefs.setBoolPref("mail.smtpserver.smtp2.useSecAuth", true);
+
+ // Set server1 and server2 username and hostname to test clientid population.
+ Services.prefs.setCharPref("mail.smtpserver.smtp1.username", "testuser1");
+ Services.prefs.setCharPref("mail.smtpserver.smtp2.username", "testuser2");
+ Services.prefs.setCharPref(
+ "mail.smtpserver.smtp1.hostname",
+ "mail.sampledomain1.com"
+ );
+ Services.prefs.setCharPref(
+ "mail.smtpserver.smtp2.hostname",
+ "mail.sampledomain2.com"
+ );
+ Services.prefs.setCharPref(
+ "mail.smtpserver.smtp3.hostname",
+ "mail.nousername.example.com"
+ );
+
+ // Migration should now have added permissions for the address that had them
+ // and not for the one that didn't have them.
+ Assert.ok(Services.prefs.getIntPref("mail.ab_remote_content.migrated") > 0);
+ Assert.equal(testPermission(uriAllowed), Services.perms.ALLOW_ACTION);
+ Assert.equal(testPermission(uriAllowed2), Services.perms.ALLOW_ACTION);
+ Assert.equal(testPermission(uriDisallowed), Services.perms.UNKNOWN_ACTION);
+
+ // Now migrate the prefs
+ migrateMailnews();
+
+ // Check that smtpserver 1 and smtpserver 2 now have a clientid.
+ Assert.ok(Services.prefs.prefHasUserValue("mail.smtpserver.smtp1.clientid"));
+ Assert.ok(Services.prefs.prefHasUserValue("mail.smtpserver.smtp2.clientid"));
+
+ // Check that the smtp3server 2 got a clientid, even if it has no
+ // username. All SMTP servers don't require a username.
+ Assert.ok(Services.prefs.prefHasUserValue("mail.smtpserver.smtp3.clientid"));
+
+ Assert.ok(
+ !Services.prefs.prefHasUserValue("mail.smtpserver.smtp1.authMethod")
+ );
+ Assert.ok(
+ Services.prefs.prefHasUserValue("mail.smtpserver.smtp2.authMethod")
+ );
+ Assert.equal(
+ Services.prefs.getIntPref("mail.smtpserver.smtp2.authMethod"),
+ Ci.nsMsgAuthMethod.secure
+ );
+
+ // Now clear the authMethod for set for smtp2. This simulates the user
+ // setting the value back to "3", i.e. Ci.nsMsgAuthMethod.passwordCleartext.
+ Services.prefs.clearUserPref("mail.smtpserver.smtp2.authMethod");
+
+ // Now clear the mail.server.server1.clientid to test re-population.
+ Services.prefs.clearUserPref("mail.server.server2.clientid");
+
+ // Now attempt migration again, e.g. a second load of TB
+ migrateMailnews();
+
+ // This time around, both of these should not be set.
+ Assert.ok(
+ !Services.prefs.prefHasUserValue("mail.smtpserver.smtp1.authMethod")
+ );
+ Assert.ok(
+ !Services.prefs.prefHasUserValue("mail.smtpserver.smtp2.authMethod")
+ );
+
+ // The server2 clientid should be the same as the smtpserver2 now since
+ // they are for the same mail.sampledomain2.com domain.
+ Assert.equal(
+ Services.prefs.getCharPref("mail.smtpserver.smtp2.clientid"),
+ Services.prefs.getCharPref("mail.server.server2.clientid")
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_acctRepair.js b/comm/mailnews/base/test/unit/test_acctRepair.js
new file mode 100644
index 0000000000..b702dcfd61
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_acctRepair.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that we recover from having a local folders server
+ * without having an account that points at it.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create account prefs with both kinds of duplication.
+
+ Services.prefs.setCharPref("mail.account.account2.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account2.server", "server1");
+ Services.prefs.setCharPref("mail.account.account6.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account6.server", "server5");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref(
+ "mail.server.server1.directory-rel",
+ "[ProfD]Mail/Local Folders"
+ );
+ Services.prefs.setCharPref("mail.server.server5.hostname", "pop3.host.org");
+ Services.prefs.setCharPref("mail.server.server5.type", "pop3");
+ Services.prefs.setCharPref(
+ "mail.server.server5.deferred_to_account",
+ "account2"
+ );
+
+ Services.prefs.setCharPref("mail.accountmanager.accounts", "account6");
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account6");
+ Services.prefs.setCharPref(
+ "mail.accountmanager.localfoldersserver",
+ "server1"
+ );
+ // This will force the load of the accounts setup above.
+ // We should have created an account for the local folders.
+ Assert.equal(MailServices.accounts.accounts.length, 2);
+ Assert.equal(
+ Services.prefs.getCharPref("mail.accountmanager.accounts"),
+ "account6,account7"
+ );
+ Assert.equal(
+ Services.prefs.getCharPref("mail.account.account7.server"),
+ "server1"
+ );
+ let server5 = MailServices.accounts
+ .getIncomingServer("server5")
+ .QueryInterface(Ci.nsIPop3IncomingServer);
+ Assert.equal(server5.deferredToAccount, "account7");
+}
diff --git a/comm/mailnews/base/test/unit/test_bccInDatabase.js b/comm/mailnews/base/test/unit/test_bccInDatabase.js
new file mode 100644
index 0000000000..ad9ff039b7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bccInDatabase.js
@@ -0,0 +1,54 @@
+/* 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/. */
+
+/*
+ * Testing of bcc in message summary file added in bug 481667
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var hdr;
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ continueTest();
+ },
+ };
+
+ // Get a message into the local filestore.
+ var draft = do_get_file("../../../data/draft1");
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ draft,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+function continueTest() {
+ // dump("\nbccList >" + hdr.bccList);
+ // dump("\nccList >" + hdr.ccList);
+ // dump("\n");
+ Assert.ok(hdr.bccList.includes("Another Person"));
+ Assert.ok(hdr.bccList.includes("<u1@example.com>"));
+ Assert.ok(!hdr.bccList.includes("IDoNotExist"));
+ hdr = null;
+ do_test_finished();
+}
diff --git a/comm/mailnews/base/test/unit/test_bug428427.js b/comm/mailnews/base/test/unit/test_bug428427.js
new file mode 100644
index 0000000000..f00f2bd571
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bug428427.js
@@ -0,0 +1,249 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Test of message count changes in virtual folder views
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var bugmail1 = do_get_file("../../../data/bugmail1");
+// main test
+
+// the headers for the test messages. All messages are identical, but
+// have different properties set on them.
+var hdrs = [];
+
+// how many identical messages to load
+var messageCount = 5;
+
+// tag used with test messages
+var tag1 = "istag";
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // Get messageCount messages into the local filestore.
+ do_test_pending();
+
+ // function setupVirtualFolder() continues the testing after copyFileMessage.
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ return true;
+}
+
+// nsIMsgCopyServiceListener implementation
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ hdrs.push(localAccountUtils.inboxFolder.GetMessageHeader(aKey));
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ if (--messageCount) {
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ } else {
+ try {
+ setupVirtualFolder();
+ } catch (ex) {
+ dump(ex);
+ }
+ }
+ },
+};
+
+var virtualFolder;
+var numTotalMessages;
+var numUnreadMessages;
+
+// virtual folder setup
+function setupVirtualFolder() {
+ // add as valid tag tag1, though probably not really necessary
+ MailServices.tags.addTagForKey(tag1, tag1, null, null);
+
+ // add tag1 to 4 messages
+ let messages0to3 = [hdrs[0], hdrs[1], hdrs[2], hdrs[3]];
+ localAccountUtils.inboxFolder.addKeywordsToMessages(messages0to3, tag1);
+
+ // set 3 messages unread, 2 messages read
+ let messages0to2 = [hdrs[0], hdrs[1], hdrs[2]];
+ localAccountUtils.inboxFolder.markMessagesRead(messages0to2, false);
+
+ let messages3to4 = [hdrs[3], hdrs[4]];
+ localAccountUtils.inboxFolder.markMessagesRead(messages3to4, true);
+
+ // search will look for tag tag1 in the inbox folder
+ var searchTerm = makeSearchTerm(
+ localAccountUtils.inboxFolder,
+ tag1,
+ Ci.nsMsgSearchAttrib.Keywords,
+ Ci.nsMsgSearchOp.Contains
+ );
+
+ dump("creating virtual folder\n");
+ var rootFolder = localAccountUtils.incomingServer.rootMsgFolder;
+ virtualFolder = CreateVirtualFolder(
+ "VfTest",
+ rootFolder,
+ localAccountUtils.inboxFolder.URI,
+ searchTerm,
+ false
+ );
+
+ // Setup search session. Execution continues with testVirtualFolder()
+ // after search is done.
+
+ var searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(
+ Ci.nsMsgSearchScope.offlineMail,
+ localAccountUtils.inboxFolder
+ );
+ searchSession.appendTerm(searchTerm, false);
+ searchSession.registerListener(searchListener);
+ dump("starting search of vf\n");
+ searchSession.search(null);
+}
+
+// partially based on gSearchNotificationListener in searchBar.js
+// nsIMsgSearchNotify implementation
+var searchListener = {
+ onNewSearch() {
+ dump("in onnewsearch\n");
+ numTotalMessages = 0;
+ numUnreadMessages = 0;
+ },
+ onSearchHit(dbHdr, folder) {
+ print("Search hit, isRead is " + dbHdr.isRead);
+ numTotalMessages++;
+ if (!dbHdr.isRead) {
+ numUnreadMessages++;
+ }
+ },
+ onSearchDone(status) {
+ print("Finished search hitCount = " + numTotalMessages);
+ var db = virtualFolder.msgDatabase;
+ var dbFolderInfo = db.dBFolderInfo;
+ dbFolderInfo.numMessages = numTotalMessages;
+ dbFolderInfo.numUnreadMessages = numUnreadMessages;
+ virtualFolder.updateSummaryTotals(true);
+ print("virtual folder unread is " + virtualFolder.getNumUnread(false));
+ testVirtualFolder();
+ },
+};
+
+function testVirtualFolder() {
+ // basic functionality tests
+
+ // total messages matching search
+ Assert.equal(4, virtualFolder.getTotalMessages(false));
+
+ // total unread messages in search
+ Assert.equal(3, virtualFolder.getNumUnread(false));
+
+ // change unread of one item in search to decrease count
+ localAccountUtils.inboxFolder.markMessagesRead([hdrs[0]], true);
+ virtualFolder.updateSummaryTotals(true);
+
+ Assert.equal(2, virtualFolder.getNumUnread(false));
+
+ // failures fixed in this bug
+
+ // remove tag from one item to decrease count
+ var message1 = [hdrs[1]];
+ localAccountUtils.inboxFolder.removeKeywordsFromMessages(message1, tag1);
+ virtualFolder.updateSummaryTotals(true);
+ Assert.equal(3, virtualFolder.getTotalMessages(false));
+ Assert.equal(1, virtualFolder.getNumUnread(false));
+
+ // End of test, so release our header references
+ hdrs = null;
+
+ do_test_finished();
+ return true;
+}
+
+// helper functions
+
+// adapted from commandglue.js
+function CreateVirtualFolder(
+ newName,
+ parentFolder,
+ searchFolderURIs,
+ searchTerm,
+ searchOnline
+) {
+ var newFolder = parentFolder.addSubfolder(newName);
+ newFolder.setFlag(Ci.nsMsgFolderFlags.Virtual);
+ var vfdb = newFolder.msgDatabase;
+ var searchTermString = getSearchTermString(searchTerm);
+
+ var dbFolderInfo = vfdb.dBFolderInfo;
+ // set the view string as a property of the db folder info
+ // set the original folder name as well.
+ dbFolderInfo.setCharProperty("searchStr", searchTermString);
+ dbFolderInfo.setCharProperty("searchFolderUri", searchFolderURIs);
+ dbFolderInfo.setBooleanProperty("searchOnline", searchOnline);
+ // This fails because the folder doesn't exist - why were we doing it?
+ // vfdb.summaryValid = true;
+ vfdb.close(true);
+ // use acctMgr to setup the virtual folder listener
+ var acctMgr = MailServices.accounts.QueryInterface(Ci.nsIFolderListener);
+ // print(acctMgr);
+ acctMgr.onFolderAdded(parentFolder, newFolder);
+ return newFolder;
+}
+
+function getSearchTermString(term) {
+ var condition = "";
+
+ if (condition.length > 1) {
+ condition += " ";
+ }
+
+ if (term.matchAll) {
+ condition = "ALL";
+ }
+ condition += term.booleanAnd ? "AND (" : "OR (";
+ condition += term.termAsString + ")";
+ return condition;
+}
+
+// Create a search term for searching aFolder
+// using aAttrib, aOp, and string aStrValue
+function makeSearchTerm(aFolder, aStrValue, aAttrib, aOp) {
+ // use a temporary search session
+ var searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.offlineMail, aFolder);
+ var searchTerm = searchSession.createTerm();
+ var value = searchTerm.value;
+ value.str = aStrValue;
+ searchTerm.value = value;
+ searchTerm.attrib = aAttrib;
+ searchTerm.op = aOp;
+ searchTerm.booleanAnd = false;
+ searchSession = null;
+ return searchTerm;
+}
diff --git a/comm/mailnews/base/test/unit/test_bug434810.js b/comm/mailnews/base/test/unit/test_bug434810.js
new file mode 100644
index 0000000000..f41eea82d9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bug434810.js
@@ -0,0 +1,27 @@
+/* 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/. */
+
+// Test of setup of localMailFolders
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ var rootFolder = localAccountUtils.incomingServer.rootFolder;
+
+ var msgProps = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+
+ var expectedFolders = ["Inbox"]; // Inbox hard-coded in LocalAccountUtils.jsm
+
+ // These two MailNews adds by default
+ expectedFolders.push(msgProps.GetStringFromName("outboxFolderName"));
+ expectedFolders.push(msgProps.GetStringFromName("trashFolderName"));
+
+ Assert.equal(rootFolder.numSubFolders, expectedFolders.length);
+ for (var i = 0; i < expectedFolders.length; ++i) {
+ Assert.ok(rootFolder.containsChildNamed(expectedFolders[i]));
+ }
+ Assert.ok(rootFolder.isAncestorOf(localAccountUtils.inboxFolder));
+}
diff --git a/comm/mailnews/base/test/unit/test_bug471682.js b/comm/mailnews/base/test/unit/test_bug471682.js
new file mode 100644
index 0000000000..73e431d84f
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bug471682.js
@@ -0,0 +1,118 @@
+/* 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/. */
+
+/*
+ * Test of message database validity on local copy from bug 471682. What
+ * we want to do here is to copy a couple of message to a new folder, and
+ * then compare the date and filesize of the folder file with the
+ * stored result in dbfolderinfo. If they don't match, that's bad.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var bugmail1 = do_get_file("../../../data/bugmail1");
+var gHdr; // header of test message in local folder
+
+localAccountUtils.loadLocalMailAccount();
+// create a subfolder as a target for copies
+var gSubfolder =
+ localAccountUtils.inboxFolder.createLocalSubfolder("subfolder");
+
+function run_test() {
+ // make sure we're using berkeley mailbox format here since this test
+ // assumes berkeley mailbox format.
+ if (
+ Services.prefs.getCharPref("mail.serverDefaultStoreContractID") !=
+ "@mozilla.org/msgstore/berkeleystore;1"
+ ) {
+ return;
+ }
+
+ do_test_pending();
+ // step 1: copy a message into the local inbox
+ MailServices.copy.copyFileMessage(
+ bugmail1,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ step2,
+ null
+ );
+}
+
+// step 2: copy one message into a subfolder to establish an
+// mbox file time and size
+// nsIMsgCopyServiceListener implementation
+var step2 = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ dump("in set message key\n");
+ gHdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ Assert.notEqual(gHdr, null);
+ // copy the message into the subfolder
+ MailServices.copy.copyMessages(
+ localAccountUtils.inboxFolder,
+ [gHdr],
+ gSubfolder,
+ false,
+ step3,
+ null,
+ false
+ );
+ },
+};
+
+// step 3: after the copy, delay to allow copy to complete and allow possible
+// file error time
+// nsIMsgCopyServiceListener implementation
+var step3 = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ do_timeout(2000, step4);
+ },
+};
+
+// step 4: start a second copy
+function step4() {
+ MailServices.copy.copyMessages(
+ localAccountUtils.inboxFolder,
+ [gHdr],
+ gSubfolder,
+ false,
+ step5,
+ null,
+ false
+ );
+}
+
+// step 5: actual tests of file size and date
+// nsIMsgCopyServiceListener implementation
+var step5 = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ var dbSize = gSubfolder.msgDatabase.dBFolderInfo.folderSize;
+ var dbDate = gSubfolder.msgDatabase.dBFolderInfo.folderDate;
+ var filePath = gSubfolder.filePath;
+ var date = parseInt(filePath.lastModifiedTime / 1000);
+ var size = filePath.fileSize;
+ Assert.equal(size, dbSize);
+ Assert.equal(date, dbDate);
+ // End of test, so release our header reference
+ gHdr = null;
+ do_test_finished();
+ },
+};
diff --git a/comm/mailnews/base/test/unit/test_bug514945.js b/comm/mailnews/base/test/unit/test_bug514945.js
new file mode 100644
index 0000000000..90ec8084dd
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_bug514945.js
@@ -0,0 +1,40 @@
+/* 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/. */
+
+/*
+ * Testing of GetAvailable crashes in bug 514945
+ */
+
+function run_test() {
+ const kValidityManager = Cc[
+ "@mozilla.org/mail/search/validityManager;1"
+ ].getService(Ci.nsIMsgSearchValidityManager);
+
+ let validityTable = kValidityManager.getTable(
+ Ci.nsMsgSearchScope.offlineMail
+ );
+
+ // When we try to access a bad value of getAvailable, it should give an error,
+ // not crash.
+ let BAD_VALUE = 1000000; // some large value that is beyond the array bounds
+ let haveExpectedError = false;
+ try {
+ validityTable.getAvailable(Ci.nsMsgSearchAttrib.Subject, BAD_VALUE);
+ } catch (e) {
+ dump("Error but no crash, this is what we want:" + e + "\n");
+ haveExpectedError = true;
+ }
+
+ Assert.ok(haveExpectedError);
+
+ // One of the causes of this is that search term operators are not being
+ // initialized, resulting in random values of the operator. Make sure that is
+ // fixed.
+
+ const kSearchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ let searchTerm = kSearchSession.createTerm();
+ Assert.equal(searchTerm.op, Ci.nsMsgSearchOp.Contains);
+}
diff --git a/comm/mailnews/base/test/unit/test_closedDB.js b/comm/mailnews/base/test/unit/test_closedDB.js
new file mode 100644
index 0000000000..fd752bf81b
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_closedDB.js
@@ -0,0 +1,104 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that an nsMsgDBFolder's database connection and listeners are
+ * restored when called by certain functions.
+ */
+
+const { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+add_task(async function () {
+ // Create a folder and some messages.
+
+ let generator = new MessageGenerator();
+
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ account.addIdentity(MailServices.accounts.createIdentity());
+
+ let rootFolder = account.incomingServer.rootFolder;
+ rootFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ let testFolder = rootFolder.createLocalSubfolder("testFolder");
+ testFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ testFolder.addMessageBatch(
+ generator.makeMessages({ count: 5 }).map(message => message.toMboxString())
+ );
+ let testMessages = [...testFolder.messages];
+
+ // Listen for notifications.
+
+ let folderListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
+ notifications: [],
+ onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
+ if (property != "TotalUnreadMessages") {
+ return;
+ }
+
+ this.notifications.push({ folder, property, oldValue, newValue });
+ },
+ consumeNotification(expectedFolder, expectedOldValue, expectedNewValue) {
+ let { folder, oldValue, newValue } = this.notifications.shift();
+ Assert.equal(folder, expectedFolder, "notification folder");
+ Assert.equal(oldValue, expectedOldValue, "notification oldValue");
+ Assert.equal(newValue, expectedNewValue, "notification newValue");
+ },
+ };
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.intPropertyChanged
+ );
+
+ // Clear the database reference, then mark some messages as read. We should
+ // see the unread count change and get two notifications about it. We could
+ // check `testFolder.msgDatabase` is not null afterwards, but that would be
+ // pointless, because the getter restores the database.
+
+ testFolder.msgDatabase = null;
+ testFolder.markMessagesRead([testMessages[0], testMessages[4]], true);
+ Assert.equal(
+ testFolder.getNumUnread(false),
+ 3,
+ "unread message count should be updated"
+ );
+ Assert.equal(
+ folderListener.notifications.length,
+ 2,
+ "two folder notifications should have fired"
+ );
+ folderListener.consumeNotification(testFolder, 5, 4);
+ folderListener.consumeNotification(testFolder, 4, 3);
+
+ // Clear the database reference, then mark some messages as flagged. This
+ // doesn't prove much except that nothing exploded.
+
+ testFolder.msgDatabase = null;
+ testFolder.markMessagesFlagged([testMessages[1], testMessages[3]], true);
+
+ // Clear the database reference, then mark all messages as read. We should
+ // see the unread count change to zero and get a notification about it.
+
+ testFolder.msgDatabase = null;
+ testFolder.markAllMessagesRead(null);
+ Assert.equal(
+ testFolder.getNumUnread(false),
+ 0,
+ "unread message count should be updated"
+ );
+ Assert.equal(
+ folderListener.notifications.length,
+ 1,
+ "a folder notifications should have fired"
+ );
+ folderListener.consumeNotification(testFolder, 3, 0);
+
+ // Clean up.
+
+ MailServices.mailSession.RemoveFolderListener(folderListener);
+ MailServices.accounts.removeAccount(account, false);
+});
diff --git a/comm/mailnews/base/test/unit/test_compactFailure.js b/comm/mailnews/base/test/unit/test_compactFailure.js
new file mode 100644
index 0000000000..300c8abda7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_compactFailure.js
@@ -0,0 +1,134 @@
+/* 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 { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+
+/* import-globals-from ../../../test/resources/logHelper.js */
+load("../../../resources/logHelper.js");
+var { addMessagesToFolder, MessageGenerator, MessageScenarioFactory } =
+ ChromeUtils.import("resource://testing-common/mailnews/MessageGenerator.jsm");
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+var gTargetFolder;
+var gCid;
+
+// Allow certain xpcom errors.
+logHelperAllowedErrors.push("NS_ERROR_FILE_NOT_FOUND");
+
+var MsgDBServiceFailure = {
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgDBService"]),
+
+ openMailDBFromFile(file, folder, create, leaveInvalidDB) {
+ if (folder.name == "ShouldFail") {
+ throw Components.Exception("", Cr.NS_ERROR_FILE_NOT_FOUND);
+ }
+ return this._genuine.openMailDBFromFile(
+ file,
+ folder,
+ create,
+ leaveInvalidDB
+ );
+ },
+
+ openFolderDB(folder, leaveInvalidDB) {
+ return this._genuine.openFolderDB(folder, leaveInvalidDB);
+ },
+ createNewDB(folder) {
+ return this._genuine.createNewDB(folder);
+ },
+ registerPendingListener(folder, listener) {
+ this._genuine.registerPendingListener(folder, listener);
+ },
+ unregisterPendingListener(listener) {
+ this._genuine.unregisterPendingListener(listener);
+ },
+ cachedDBForFolder(folder) {
+ return this._genuine.cachedDBFolder(folder);
+ },
+ get openDBs() {
+ return this._genuine.openDBs;
+ },
+};
+
+function generate_messages() {
+ let messageGenerator = new MessageGenerator();
+ let scenarioFactory = new MessageScenarioFactory(messageGenerator);
+ let messages = [];
+ messages = messages.concat(scenarioFactory.directReply(10));
+ return messages;
+}
+
+async function compact_with_exception(expectedException) {
+ let compactor = Cc["@mozilla.org/messenger/foldercompactor;1"].createInstance(
+ Ci.nsIMsgFolderCompactor
+ );
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ compactor.compactFolders([gTargetFolder], listener, null);
+ try {
+ await listener.promise;
+ do_throw(
+ "nsIMsgFolderCompactor listener wasn't called with a failure code."
+ );
+ } catch (failureCode) {
+ Assert.equal(expectedException, failureCode);
+ }
+}
+
+function create_local_folders() {
+ let rootFolder = localAccountUtils.rootFolder;
+ let localTrashFolder = rootFolder.getChildNamed("Trash");
+ localTrashFolder.setFlag(Ci.nsMsgFolderFlags.Trash);
+}
+
+async function delete_all_messages() {
+ let promiseCopyListener = new PromiseTestUtils.PromiseCopyListener();
+ gTargetFolder.deleteMessages(
+ [...gTargetFolder.messages],
+ null,
+ false, // Do not delete storage.
+ true, // Is a move.
+ promiseCopyListener,
+ false // Do not allow undo, currently leaks.
+ );
+ await promiseCopyListener.promise;
+}
+
+add_setup(function () {
+ localAccountUtils.loadLocalMailAccount();
+ create_local_folders();
+});
+
+add_task(async function test_compact_without_failure() {
+ // Setup open failure folder.
+ gTargetFolder =
+ localAccountUtils.rootFolder.createLocalSubfolder("ShouldFail");
+ addMessagesToFolder(generate_messages(), gTargetFolder);
+
+ await new Promise(resolve => {
+ mailTestUtils.updateFolderAndNotify(gTargetFolder, resolve);
+ });
+ // Delete messages.
+ await delete_all_messages();
+ // Setup db service mock.
+ gCid = MockRegistrar.register(
+ "@mozilla.org/msgDatabase/msgDBService;1",
+ MsgDBServiceFailure
+ );
+ // Test compact without failure.
+ await compact_with_exception(Cr.NS_ERROR_FILE_NOT_FOUND);
+ // Teardown db service mock.
+ MockRegistrar.unregister(gCid);
+});
diff --git a/comm/mailnews/base/test/unit/test_converterDeferredAccount.js b/comm/mailnews/base/test/unit/test_converterDeferredAccount.js
new file mode 100644
index 0000000000..effc6d57e2
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_converterDeferredAccount.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/. */
+
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { convertMailStoreTo } = ChromeUtils.import(
+ "resource:///modules/mailstoreConverter.jsm"
+);
+const { FolderUtils } = ChromeUtils.import(
+ "resource:///modules/FolderUtils.jsm"
+);
+
+// XXX: merge into test_converter.js
+
+var log = console.createInstance({
+ prefix: "mail.mailstoreconverter",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.mailstoreconverter.loglevel",
+});
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// No. of messages/files and folders copied.
+var gMsgHdrs = [];
+// {nsIMsgLocalMailFolder} folder carrying messages for the pop server.
+var gInbox;
+// {nsIMsgIncomingServer} server for first deferred pop account.
+var gServer1;
+// {nsIMsgIncomingServer} server for second deferred pop account.
+var gServer2;
+// {nsIMsgIncomingServer} server to convert.
+var gServer;
+
+var copyListenerWrap = {
+ SetMessageKey(aKey) {
+ let hdr = gInbox.GetMessageHeader(aKey);
+ gMsgHdrs.push({ hdr, ID: hdr.messageId });
+ },
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ },
+};
+
+var EventTarget = function () {
+ this.dispatchEvent = function (event) {
+ if (event.type == "progress") {
+ log.trace("Progress: " + event.detail);
+ }
+ };
+};
+
+function copyFileMessage(file, destFolder, isDraftOrTemplate) {
+ let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ isDraftOrTemplate,
+ 0,
+ "",
+ listener,
+ null
+ );
+ return listener.promise;
+}
+
+/**
+ * Check that conversion worked for the given source.
+ *
+ * @param {nsIFile} source - mbox source directory.
+ * @param {nsIFile} target - maildir target directory.
+ */
+function checkConversion(source, target) {
+ for (let sourceContent of source.directoryEntries) {
+ let sourceContentName = sourceContent.leafName;
+ let ext = sourceContentName.substr(-4);
+ let targetFile = FileUtils.File(
+ PathUtils.join(target.path, sourceContentName)
+ );
+ log.debug("Checking path: " + targetFile.path);
+ if (ext == ".dat") {
+ Assert.ok(targetFile.exists());
+ } else if (sourceContent.isDirectory()) {
+ Assert.ok(targetFile.exists());
+ checkConversion(sourceContent, targetFile);
+ } else if (ext != ".msf") {
+ Assert.ok(targetFile.exists());
+ let cur = FileUtils.File(PathUtils.join(targetFile.path, "cur"));
+ Assert.ok(cur.exists());
+ let tmp = FileUtils.File(PathUtils.join(targetFile.path, "tmp"));
+ Assert.ok(tmp.exists());
+ if (targetFile.leafName == "Inbox") {
+ let curContents = cur.directoryEntries;
+ let curContentsCount = [...curContents].length;
+ Assert.equal(curContentsCount, 1000);
+ }
+ }
+ }
+}
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // Set up two deferred pop accounts.
+ gServer1 = MailServices.accounts.createIncomingServer(
+ "test1",
+ "localhost1",
+ "pop3"
+ );
+ gServer2 = MailServices.accounts.createIncomingServer(
+ "test2",
+ "localhost2",
+ "pop3"
+ );
+ var accountPop1 = MailServices.accounts.createAccount();
+ var accountPop2 = MailServices.accounts.createAccount();
+
+ // Set incoming servers.
+ accountPop1.incomingServer = gServer1;
+ gServer1.QueryInterface(Ci.nsIPop3IncomingServer);
+ gServer1.valid = true;
+ accountPop2.incomingServer = gServer2;
+ gServer2.QueryInterface(Ci.nsIPop3IncomingServer);
+ gServer2.valid = true;
+
+ // Defer accounts to Local Folders.
+ gServer1.deferredToAccount = localAccountUtils.msgAccount.key;
+ gServer2.deferredToAccount = localAccountUtils.msgAccount.key;
+
+ // 'gServer1' should be deferred. Get the path of the root folder to which
+ // other accounts are deferred.
+ ok(gServer1.rootFolder.filePath.path != gServer1.rootMsgFolder.filePath.path);
+ let deferredToRootFolder = gServer1.rootMsgFolder.filePath.path;
+
+ // Account to which other accounts have been deferred.
+ let deferredToAccount;
+ // String to hold names of accounts to convert.
+ let accountsToConvert = "";
+
+ let accounts = FolderUtils.allAccountsSorted(true);
+ for (let account of accounts) {
+ if (
+ account.incomingServer.rootFolder.filePath.path == deferredToRootFolder
+ ) {
+ // Other accounts may be deferred to this account.
+ deferredToAccount = account;
+ } else if (
+ account.incomingServer.rootMsgFolder.filePath.path == deferredToRootFolder
+ ) {
+ // This is a deferred account.
+ accountsToConvert += account.incomingServer.username + ", ";
+ }
+ }
+
+ accountsToConvert =
+ accountsToConvert + deferredToAccount.incomingServer.username;
+ log.info(accountsToConvert + " will be converted");
+
+ gInbox = localAccountUtils.inboxFolder;
+ gServer = deferredToAccount.incomingServer;
+
+ run_next_test();
+}
+
+add_setup(async function () {
+ let msgFile = do_get_file("../../../data/bugmail10");
+ // Add 1000 messages to the "Inbox" folder.
+ for (let i = 0; i < 1000; i++) {
+ await copyFileMessage(msgFile, gInbox, false);
+ }
+});
+
+add_task(function testMaildirConversion() {
+ let mailstoreContractId = Services.prefs.getCharPref(
+ "mail.server." + gServer.key + ".storeContractID"
+ );
+
+ do_test_pending();
+ let pConverted = convertMailStoreTo(
+ mailstoreContractId,
+ gServer,
+ new EventTarget()
+ );
+ let originalRootFolder = gServer.rootFolder.filePath;
+
+ pConverted
+ .then(function (val) {
+ log.debug("Conversion done: " + originalRootFolder.path + " => " + val);
+ let newRootFolder = gServer.rootFolder.filePath;
+ checkConversion(originalRootFolder, newRootFolder);
+ do_test_finished();
+ })
+ .catch(function (reason) {
+ log.error("Conversion failed: " + reason.error);
+ ok(false); // Fail the test!
+ });
+});
diff --git a/comm/mailnews/base/test/unit/test_copyChaining.js b/comm/mailnews/base/test/unit/test_copyChaining.js
new file mode 100644
index 0000000000..b014049ed3
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_copyChaining.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/. */
+
+// Test of chaining copies between the same folders
+
+/* import-globals-from ../../../test/resources/MessageGenerator.jsm */
+load("../../../resources/MessageGenerator.jsm");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gCopySource;
+var gCopyDest;
+var gMessages;
+var gCurTestNum = 1;
+
+// main test
+
+var gTestArray = [
+ function copyMsg1() {
+ gMessages = [...gCopySource.msgDatabase.enumerateMessages()];
+ CopyNextMessage();
+ },
+ function copyMsg2() {
+ CopyNextMessage();
+ },
+ function copyMsg3() {
+ CopyNextMessage();
+ },
+ function copyMsg4() {
+ CopyNextMessage();
+ },
+];
+
+function CopyNextMessage() {
+ if (gMessages.length > 0) {
+ let msgHdr = gMessages.shift();
+ MailServices.copy.copyMessages(
+ gCopySource,
+ [msgHdr],
+ gCopyDest,
+ true,
+ copyListener,
+ null,
+ false
+ );
+ } else {
+ do_throw("TEST FAILED - out of messages");
+ }
+}
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ let messageGenerator = new MessageGenerator();
+ let scenarioFactory = new MessageScenarioFactory(messageGenerator);
+
+ // "Master" do_test_pending(), paired with a do_test_finished() at the end of
+ // all the operations.
+ do_test_pending();
+
+ gCopyDest = localAccountUtils.inboxFolder.createLocalSubfolder("copyDest");
+ // build up a diverse list of messages
+ let messages = [];
+ messages = messages.concat(scenarioFactory.directReply(10));
+ gCopySource = localAccountUtils.rootFolder.createLocalSubfolder("copySource");
+ addMessagesToFolder(messages, gCopySource);
+
+ mailTestUtils.updateFolderAndNotify(gCopySource, doTest);
+ return true;
+}
+
+function doTest() {
+ var test = gCurTestNum;
+ if (test <= gTestArray.length) {
+ var testFn = gTestArray[test - 1];
+ dump("Doing test " + test + " " + testFn.name + "\n");
+
+ try {
+ testFn();
+ } catch (ex) {
+ do_throw("TEST FAILED " + ex);
+ }
+ } else {
+ endTest();
+ }
+}
+
+function endTest() {
+ // Cleanup, null out everything
+ dump(" Exiting mail tests\n");
+ gMessages = null;
+ do_test_finished(); // for the one in run_test()
+}
+
+// nsIMsgCopyServiceListener implementation
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {},
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ ++gCurTestNum;
+ doTest();
+ },
+};
diff --git a/comm/mailnews/base/test/unit/test_copyToInvalidDB.js b/comm/mailnews/base/test/unit/test_copyToInvalidDB.js
new file mode 100644
index 0000000000..2c0dd85d9f
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_copyToInvalidDB.js
@@ -0,0 +1,106 @@
+/*
+ * Simple tests for copying local messages to a folder whose db is missing
+ * or invalid
+ */
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+const { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+
+async function setup() {
+ let createSubfolder = async function (parentFolder, name) {
+ let promiseAdded = PromiseTestUtils.promiseFolderAdded(name);
+ parentFolder.createSubfolder(name, null);
+ await promiseAdded;
+ return parentFolder.getChildNamed(name);
+ };
+
+ // Create account.
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.FindAccountForServer(
+ MailServices.accounts.localFoldersServer
+ );
+ let root = account.incomingServer.rootFolder;
+
+ // Add a couple of folders containing some test messages.
+ let folder1 = await createSubfolder(root, "test1");
+ folder1.QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ let folder2 = await createSubfolder(root, "test2");
+ folder2.QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ let gen = new MessageGenerator();
+ let msg1 = gen.makeMessage();
+ let msg2 = gen.makeMessage({ inReplyTo: msg1 });
+ folder1.addMessageBatch([msg1, msg2].map(m => m.toMboxString()));
+
+ let msg3 = gen.makeMessage();
+ folder2.addMessage(msg3.toMboxString());
+
+ return [folder1, folder2];
+}
+
+add_task(async function test_copyToInvalidDB() {
+ let [folder1, folder2] = await setup();
+
+ // folder1 contains [msg1, msg2].
+ // folder2 contains [msg3].
+
+ // Take note of the message we're going to move (first msg in folder1).
+ let msgHdr = Array.from(folder1.msgDatabase.enumerateMessages())[0];
+ let expectedID = msgHdr.messageId;
+ let expectedMsg = mailTestUtils.loadMessageToString(folder1, msgHdr);
+
+ // Sabotage the destination folder2 database.
+ folder2.msgDatabase.summaryValid = false;
+ folder2.msgDatabase = null;
+ folder2.ForceDBClosed();
+ // In fact, delete the .msf file entirely.
+ folder2.summaryFile.remove(false);
+
+ // So folder2 has no trace of a DB.
+ Assert.equal(folder2.databaseOpen, false);
+ Assert.equal(folder2.summaryFile.exists(), false);
+
+ // Move the message from folder1 to folder2.
+ let copyListener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyMessages(
+ folder1,
+ [msgHdr],
+ folder2,
+ true, // isMove
+ copyListener,
+ null, // window
+ false // allowUndo
+ );
+ await copyListener.promise;
+
+ // Current behaviour:
+ // After the move, there's still no sign of a DB file.
+ // Yet the copy didn't fail (see Bug 1737203).
+ Assert.equal(folder2.databaseOpen, false);
+ Assert.equal(folder2.summaryFile.exists(), false);
+
+ // Rebuild the the database.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ try {
+ folder2.getDatabaseWithReparse(urlListener, null);
+ } catch (ex) {
+ // We expect this - it indicates the DB is not valid. But it will have
+ // kicked off an async reparse, so we need to wait for the listener.
+ Assert.equal(ex.result, Cr.NS_ERROR_NOT_INITIALIZED);
+ await urlListener.promise;
+ }
+
+ // Check that the message moved over intact.
+ let gotHdr = folder2.msgDatabase.getMsgHdrForMessageID(expectedID);
+ let gotMsg = mailTestUtils.loadMessageToString(folder2, gotHdr);
+ // NOTE: With maildir store, the message seems to gain an extra trailing
+ // "\n" during the copy. See Bug 1716651.
+ // For now, use .trim() as a workaround.
+ Assert.equal(gotMsg.trim(), expectedMsg.trim());
+});
diff --git a/comm/mailnews/base/test/unit/test_detachToFile.js b/comm/mailnews/base/test/unit/test_detachToFile.js
new file mode 100644
index 0000000000..3c1913a9a8
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_detachToFile.js
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests nsIMessenger's detachAttachmentsWOPrompts
+ */
+
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+
+function SaveAttachmentCallback() {
+ this.attachments = null;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+}
+
+SaveAttachmentCallback.prototype = {
+ callback: function saveAttachmentCallback_callback(aMsgHdr, aMimeMessage) {
+ this.attachments = aMimeMessage.allAttachments;
+ this._resolve();
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+var gCallbackObject = new SaveAttachmentCallback();
+
+add_setup(async function () {
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+});
+
+add_task(async function startCopy() {
+ // Get a message into the local filestore.
+ let mailFile = do_get_file("../../../data/external-attach-test");
+ let listener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ mailFile,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ listener,
+ null
+ );
+ await listener.promise;
+});
+
+// process the message through mime
+add_task(async function startMime() {
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ MsgHdrToMimeMessage(
+ msgHdr,
+ gCallbackObject,
+ gCallbackObject.callback,
+ true // allowDownload
+ );
+
+ await gCallbackObject.promise;
+});
+
+// detach any found attachments
+add_task(async function startDetach() {
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+ let msgURI = msgHdr.folder.generateMessageURI(msgHdr.messageKey);
+
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ let attachment = gCallbackObject.attachments[0];
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+
+ messenger.detachAttachmentsWOPrompts(
+ do_get_profile(),
+ [attachment.contentType],
+ [attachment.url],
+ [attachment.name],
+ [msgURI],
+ listener
+ );
+ await listener.promise;
+});
+
+/**
+ * Test that the detachment was successful.
+ */
+add_task(async function testDetach() {
+ // The message contained a file "check.pdf" which should
+ // now exist in the profile directory.
+ let checkFile = do_get_profile().clone();
+ checkFile.append("check.pdf");
+ Assert.ok(checkFile.exists());
+
+ // The message should now have a detached attachment. Read the message,
+ // and search for "AttachmentDetached" which is added on detachment.
+
+ // Get the message header
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ let messageContent = await getContentFromMessage(msgHdr);
+ Assert.ok(messageContent.includes("AttachmentDetached"));
+});
+
+/**
+ * Get the full message content.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - Message whose text body will be read.
+ * @returns {Promise<string>} full message contents.
+ */
+function getContentFromMessage(aMsgHdr) {
+ let msgFolder = aMsgHdr.folder;
+ let msgUri = msgFolder.getUriForMsg(aMsgHdr);
+
+ return new Promise((resolve, reject) => {
+ let streamListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ sis: Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ ),
+ content: "",
+ onDataAvailable(request, inputStream, offset, count) {
+ this.sis.init(inputStream);
+ this.content += this.sis.read(count);
+ },
+ onStartRequest(request) {},
+ onStopRequest(request, statusCode) {
+ this.sis.close();
+ if (Components.isSuccessCode(statusCode)) {
+ resolve(this.content);
+ } else {
+ reject(new Error(statusCode));
+ }
+ },
+ };
+ MailServices.messageServiceFromURI(msgUri).streamMessage(
+ msgUri,
+ streamListener,
+ null,
+ null,
+ false,
+ "",
+ false
+ );
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_emptyTrash.js b/comm/mailnews/base/test/unit/test_emptyTrash.js
new file mode 100644
index 0000000000..528691117e
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_emptyTrash.js
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test suite for empty trash
+ *
+ * Currently tested:
+ * - Empty local trash
+ * TODO
+ * - Empty imap trash
+ */
+
+// Globals
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var gMsgFile1;
+var gLocalTrashFolder;
+var gCurTestNum;
+var gMsgHdrs = [];
+var gRootFolder;
+
+var nsIMFNService = Ci.nsIMsgFolderNotificationService;
+
+// nsIMsgCopyServiceListener implementation
+var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ let hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ gMsgHdrs.push({ hdr, ID: hdr.messageId });
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ // Ugly hack: make sure we don't get stuck in a JS->C++->JS->C++... call stack
+ // This can happen with a bunch of synchronous functions grouped together, and
+ // can even cause tests to fail because they're still waiting for the listener
+ // to return
+ do_timeout(0, function () {
+ doTest(++gCurTestNum);
+ });
+ },
+};
+
+var urlListener = {
+ OnStartRunningUrl(aUrl) {},
+ OnStopRunningUrl(aUrl, aExitCode) {
+ // Check: message successfully copied.
+ Assert.equal(aExitCode, 0);
+ // Ugly hack: make sure we don't get stuck in a JS->C++->JS->C++... call stack
+ // This can happen with a bunch of synchronous functions grouped together, and
+ // can even cause tests to fail because they're still waiting for the listener
+ // to return
+ do_timeout(0, function () {
+ doTest(++gCurTestNum);
+ });
+ },
+};
+
+function copyFileMessage(file, destFolder, isDraftOrTemplate) {
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ isDraftOrTemplate,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+function deleteMessages(srcFolder, items) {
+ srcFolder.deleteMessages(items, null, false, true, copyListener, true);
+}
+
+/*
+ * TESTS
+ */
+
+// Beware before commenting out a test -- later tests might just depend on earlier ones
+var gTestArray = [
+ // Copying message from file
+ function testCopyFileMessage1() {
+ copyFileMessage(gMsgFile1, localAccountUtils.inboxFolder, false);
+ },
+
+ // Delete message
+ function testDeleteMessage() {
+ // delete to trash
+ // Let's take a moment to re-initialize stuff that got moved
+ let inboxDB = localAccountUtils.inboxFolder.msgDatabase;
+ gMsgHdrs[0].hdr = inboxDB.getMsgHdrForMessageID(gMsgHdrs[0].ID);
+
+ // Now delete the message
+ deleteMessages(localAccountUtils.inboxFolder, [gMsgHdrs[0].hdr]);
+ },
+ function emptyTrash() {
+ gRootFolder = localAccountUtils.incomingServer.rootMsgFolder;
+ gLocalTrashFolder = gRootFolder.getChildNamed("Trash");
+ // hold onto a db to make sure that empty trash deals with the case
+ // of someone holding onto the db, but the trash folder has a null db.
+ let gLocalTrashDB = gLocalTrashFolder.msgDatabase; // eslint-disable-line no-unused-vars
+ gLocalTrashFolder.msgDatabase = null;
+ // this is synchronous
+ gLocalTrashFolder.emptyTrash(null);
+ // check that the trash folder is 0 size, that the db has a 0 message count
+ // and has no messages.
+ Assert.equal(0, gLocalTrashFolder.filePath.fileSize);
+ Assert.equal(0, gLocalTrashFolder.msgDatabase.dBFolderInfo.numMessages);
+ let msgs = [...gLocalTrashFolder.msgDatabase.enumerateMessages()];
+ Assert.equal(0, msgs.length);
+ urlListener.OnStopRunningUrl(null, 0);
+ },
+];
+
+// Our listener, which captures events.
+function gMFListener() {}
+gMFListener.prototype = {
+ folderDeleted(aFolder) {
+ aFolder.msgDatabase = null;
+ },
+};
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ // Load up a message so that we can copy it in later.
+ gMsgFile1 = do_get_file("../../../data/bugmail10");
+ // our front end code clears the msg db when it gets told the folder for
+ // an open view has been deleted - so simulate that.
+ var folderDeletedListener = new gMFListener();
+ MailServices.mfn.addListener(
+ folderDeletedListener,
+ nsIMFNService.folderDeleted
+ );
+
+ // "Master" do_test_pending(), paired with a do_test_finished() at the end of all the operations.
+ do_test_pending();
+
+ // Do the test.
+ doTest(1);
+}
+
+function doTest(test) {
+ if (test <= gTestArray.length) {
+ gCurTestNum = test;
+
+ var testFn = gTestArray[test - 1];
+ // Set a limit of three seconds; if the notifications haven't arrived by then there's a problem.
+ do_timeout(10000, function () {
+ if (gCurTestNum == test) {
+ do_throw(
+ "Notifications not received in 10000 ms for operation " + testFn.name
+ );
+ }
+ });
+ try {
+ testFn();
+ } catch (ex) {
+ dump(ex);
+ }
+ } else {
+ gMsgHdrs = null;
+ do_test_finished(); // for the one in run_test()
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_fix_deferred_accounts.js b/comm/mailnews/base/test/unit/test_fix_deferred_accounts.js
new file mode 100644
index 0000000000..0fd09a543a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_fix_deferred_accounts.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that we cleanup the account prefs when a pop3 account has
+ * been deferred to a hidden account.
+ */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create account prefs with a pop3 account deferred to a hidden account.
+
+ Services.prefs.setCharPref("mail.account.account1.identities", "id1");
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account4.identities", "id2");
+ Services.prefs.setCharPref("mail.account.account4.server", "server4");
+ Services.prefs.setCharPref("mail.account.account5.identities", "id3");
+ Services.prefs.setCharPref("mail.account.account5.server", "server5");
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server2.hostname", "Smart Mailboxes");
+ Services.prefs.setCharPref("mail.server.server2.type", "none");
+ Services.prefs.setBoolPref("mail.server.server2.hidden", true);
+ Services.prefs.setCharPref("mail.server.server4.hostname", "mail.host4.org");
+ Services.prefs.setCharPref("mail.server.server4.type", "pop3");
+ Services.prefs.setCharPref(
+ "mail.server.server4.deferred_to_account",
+ "account2"
+ );
+ Services.prefs.setCharPref("mail.server.server5.hostname", "mail.host5.org");
+ Services.prefs.setCharPref("mail.server.server5.type", "pop3");
+ Services.prefs.setCharPref(
+ "mail.server.server5.deferred_to_account",
+ "account2"
+ );
+
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account1,account2,account4,account5"
+ );
+ // Set the default account to one we're going to get rid of. The account manager
+ // should recover relatively gracefully.
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account1");
+
+ // This will force the load of the accounts setup above.
+ Assert.equal(MailServices.accounts.accounts.length, 3); // hidden account not included
+ let server4 = MailServices.accounts
+ .getAccount("account4")
+ .incomingServer.QueryInterface(Ci.nsIPop3IncomingServer);
+ Assert.equal(server4.deferredToAccount, "account1");
+ let server5 = MailServices.accounts
+ .getAccount("account5")
+ .incomingServer.QueryInterface(Ci.nsIPop3IncomingServer);
+ Assert.equal(server5.deferredToAccount, "account1");
+}
diff --git a/comm/mailnews/base/test/unit/test_folderCompact.js b/comm/mailnews/base/test/unit/test_folderCompact.js
new file mode 100644
index 0000000000..876672c0e5
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_folderCompact.js
@@ -0,0 +1,335 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test suite for folder compaction
+ *
+ * Currently tested:
+ * - Compacting local folders
+ * TODO
+ * - Compacting imap offline stores.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// Globals
+var gMsgFile1, gMsgFile2, gMsgFile3;
+var gMsg2ID = "200804111417.m3BEHTk4030129@mrapp51.mozilla.org";
+var gMsg3ID = "4849BF7B.2030800@example.com";
+var gLocalFolder2;
+var gLocalFolder3;
+// After a compact (or other operation), this is what we expect the
+// folder size to be.
+var gExpectedFolderSize;
+var gMsgHdrs = [];
+var gExpectedInboxSize;
+var gExpectedFolder2Size;
+var gExpectedFolder3Size;
+// Use this to account for the size of the separating line after the message
+// body. The compactor tries to preserve the convention already in the mbox,
+// and we know that our test messages have CRLFs, so that's what we'll use.
+var gSeparatorLine = "\r\n";
+
+// Transfer message keys between function calls.
+var gMsgKeys = [];
+
+// nsIMsgCopyServiceListener implementation
+var copyListenerWrap = {
+ SetMessageKey(aKey) {
+ let hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ gMsgHdrs.push({ hdr, ID: hdr.messageId });
+ },
+ OnStopCopy(aStatus) {
+ // Check: message successfully copied.
+ Assert.equal(aStatus, 0);
+ },
+};
+
+var urlListenerWrap = {
+ OnStopRunningUrl(aUrl, aExitCode) {
+ // Check: message successfully copied.
+ Assert.equal(aExitCode, 0);
+
+ if (gMsgKeys.length > 0) {
+ // Bug 854798: Check if the new message keys are the same as before compaction.
+ let folderMsgs = [...gMsgKeys.folder.messages];
+ // First message was deleted so skip it in the old array.
+ let expectedKeys = [...gMsgKeys].slice(1);
+ Assert.equal(folderMsgs.length, expectedKeys.length);
+ for (let i = 1; i < expectedKeys.length; i++) {
+ Assert.equal(folderMsgs[i], expectedKeys[i]);
+ }
+ gMsgKeys.length = 0;
+ }
+ },
+};
+
+function copyFileMessage(file, destFolder, isDraftOrTemplate) {
+ let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ isDraftOrTemplate,
+ 0,
+ "",
+ listener,
+ null
+ );
+ return listener.promise;
+}
+
+function copyMessages(items, isMove, srcFolder, destFolder) {
+ let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
+ MailServices.copy.copyMessages(
+ srcFolder,
+ items,
+ destFolder,
+ isMove,
+ listener,
+ null,
+ true
+ );
+ return listener.promise;
+}
+
+function deleteMessages(srcFolder, items) {
+ let listener = new PromiseTestUtils.PromiseCopyListener(copyListenerWrap);
+ srcFolder.deleteMessages(items, null, false, true, listener, true);
+ return listener.promise;
+}
+
+function calculateFolderSize(folder) {
+ let msgDB = folder.msgDatabase;
+ let totalSize = 0;
+ for (let header of msgDB.enumerateMessages()) {
+ totalSize += header.messageSize + gSeparatorLine.length;
+ }
+ return totalSize;
+}
+
+function verifyMsgOffsets(folder) {
+ let msgDB = folder.msgDatabase;
+ let enumerator = msgDB.enumerateMessages();
+ if (enumerator) {
+ for (let header of enumerator) {
+ if (header instanceof Ci.nsIMsgDBHdr) {
+ let storeToken = header.getStringProperty("storeToken");
+ Assert.equal(storeToken, header.messageOffset);
+ }
+ }
+ }
+}
+
+/*
+ * TESTS
+ */
+
+// Beware before commenting out a test -- later tests might just depend on earlier ones
+var gTestArray = [
+ // Copying messages from files
+ async function testCopyFileMessage1() {
+ await copyFileMessage(gMsgFile1, localAccountUtils.inboxFolder, false);
+ },
+ async function testCopyFileMessage2() {
+ await copyFileMessage(gMsgFile2, localAccountUtils.inboxFolder, false);
+ },
+ async function testCopyFileMessage3() {
+ await copyFileMessage(gMsgFile3, localAccountUtils.inboxFolder, true);
+ showMessages(
+ localAccountUtils.inboxFolder,
+ "after initial 3 messages copy to inbox"
+ );
+ },
+
+ // Moving/copying messages
+ async function testCopyMessages1() {
+ await copyMessages(
+ [gMsgHdrs[0].hdr],
+ false,
+ localAccountUtils.inboxFolder,
+ gLocalFolder2
+ );
+ },
+ async function testCopyMessages2() {
+ await copyMessages(
+ [gMsgHdrs[1].hdr, gMsgHdrs[2].hdr],
+ false,
+ localAccountUtils.inboxFolder,
+ gLocalFolder2
+ );
+ showMessages(gLocalFolder2, "after copying 3 messages");
+ },
+ async function testMoveMessages1() {
+ await copyMessages(
+ [gMsgHdrs[0].hdr, gMsgHdrs[1].hdr],
+ true,
+ localAccountUtils.inboxFolder,
+ gLocalFolder3
+ );
+
+ showMessages(localAccountUtils.inboxFolder, "after moving 2 messages");
+ showMessages(gLocalFolder3, "after moving 2 messages");
+ },
+
+ // Deleting messages
+ async function testDeleteMessages1() {
+ // delete to trash
+ // Let's take a moment to re-initialize stuff that got moved
+ var folder3DB = gLocalFolder3.msgDatabase;
+ gMsgHdrs[0].hdr = folder3DB.getMsgHdrForMessageID(gMsgHdrs[0].ID);
+
+ // Store message keys before deletion and compaction.
+ gMsgKeys.folder = gLocalFolder3;
+ for (let header of gLocalFolder3.messages) {
+ gMsgKeys.push(header.messageKey);
+ }
+
+ // Now delete the message
+ await deleteMessages(gLocalFolder3, [gMsgHdrs[0].hdr]);
+
+ showMessages(gLocalFolder3, "after deleting 1 message to trash");
+ },
+ async function compactFolder() {
+ gExpectedFolderSize = calculateFolderSize(gLocalFolder3);
+ Assert.notEqual(gLocalFolder3.expungedBytes, 0);
+ let listener = new PromiseTestUtils.PromiseUrlListener(urlListenerWrap);
+ gLocalFolder3.compact(listener, null);
+ await listener.promise;
+
+ showMessages(gLocalFolder3, "after compact");
+ },
+ async function testDeleteMessages2() {
+ Assert.equal(gExpectedFolderSize, gLocalFolder3.filePath.fileSize);
+ verifyMsgOffsets(gLocalFolder3);
+ var folder2DB = gLocalFolder2.msgDatabase;
+ gMsgHdrs[0].hdr = folder2DB.getMsgHdrForMessageID(gMsgHdrs[0].ID);
+
+ // Store message keys before deletion and compaction.
+ gMsgKeys.folder = gLocalFolder2;
+ for (let header of gLocalFolder2.messages) {
+ gMsgKeys.push(header.messageKey);
+ }
+
+ // Now delete the message
+ await deleteMessages(gLocalFolder2, [gMsgHdrs[0].hdr]);
+
+ showMessages(gLocalFolder2, "after deleting 1 message");
+ },
+ async function compactAllFolders() {
+ gExpectedInboxSize = calculateFolderSize(localAccountUtils.inboxFolder);
+ gExpectedFolder2Size = calculateFolderSize(gLocalFolder2);
+ gExpectedFolder3Size = calculateFolderSize(gLocalFolder3);
+
+ // Save the first message key, which will change after compact with
+ // rebuild.
+ let f2m2Key =
+ gLocalFolder2.msgDatabase.getMsgHdrForMessageID(gMsg2ID).messageKey;
+
+ // force expunged bytes count to get cached.
+ gLocalFolder2.expungedBytes;
+ // mark localFolder2 as having an invalid db, and remove it
+ // for good measure.
+ gLocalFolder2.msgDatabase.summaryValid = false;
+ gLocalFolder2.msgDatabase = null;
+ gLocalFolder2.ForceDBClosed();
+ let dbPath = gLocalFolder2.filePath;
+ dbPath.leafName = dbPath.leafName + ".msf";
+ dbPath.remove(false);
+
+ showMessages(localAccountUtils.inboxFolder, "before compactAll");
+ // Save the key for the inbox message, we'll check after compact that it
+ // did not change.
+ let preInboxMsg3Key =
+ localAccountUtils.inboxFolder.msgDatabase.getMsgHdrForMessageID(
+ gMsg3ID
+ ).messageKey;
+
+ // We used to check here that the keys did not change during rebuild.
+ // But that is no true in general, it was only conicidental since the
+ // checked folder had never been compacted, so the key equaled the offset.
+ // We do not in guarantee that, indeed after rebuild we expect the keys
+ // to change.
+ let checkResult = {
+ OnStopRunningUrl(aUrl, aExitCode) {
+ // Check: message successfully compacted.
+ Assert.equal(aExitCode, 0);
+ },
+ };
+ let listener = new PromiseTestUtils.PromiseUrlListener(checkResult);
+ localAccountUtils.inboxFolder.compactAll(listener, null);
+ await listener.promise;
+
+ showMessages(localAccountUtils.inboxFolder, "after compactAll");
+ showMessages(gLocalFolder2, "after compactAll");
+
+ // For the inbox, which was compacted but not rebuild, key is unchanged.
+ let postInboxMsg3Key =
+ localAccountUtils.inboxFolder.msgDatabase.getMsgHdrForMessageID(
+ gMsg3ID
+ ).messageKey;
+ Assert.equal(preInboxMsg3Key, postInboxMsg3Key);
+
+ // For folder2, which was rebuilt, keys change but all messages should exist.
+ let message2 = gLocalFolder2.msgDatabase.getMsgHdrForMessageID(gMsg2ID);
+ Assert.ok(message2);
+ Assert.ok(gLocalFolder2.msgDatabase.getMsgHdrForMessageID(gMsg3ID));
+
+ // In folder2, gMsg2ID is the first message. After compact with database
+ // rebuild, that key has now changed.
+ Assert.notEqual(message2.messageKey, f2m2Key);
+ },
+ function lastTestCheck() {
+ Assert.equal(
+ gExpectedInboxSize,
+ localAccountUtils.inboxFolder.filePath.fileSize
+ );
+ Assert.equal(gExpectedFolder2Size, gLocalFolder2.filePath.fileSize);
+ Assert.equal(gExpectedFolder3Size, gLocalFolder3.filePath.fileSize);
+ verifyMsgOffsets(gLocalFolder2);
+ verifyMsgOffsets(gLocalFolder3);
+ verifyMsgOffsets(localAccountUtils.inboxFolder);
+ },
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ // Load up some messages so that we can copy them in later.
+ gMsgFile1 = do_get_file("../../../data/bugmail10");
+ gMsgFile2 = do_get_file("../../../data/bugmail11");
+ gMsgFile3 = do_get_file("../../../data/draft1");
+
+ // Create another folder to move and copy messages around, and force initialization.
+ gLocalFolder2 = localAccountUtils.rootFolder.createLocalSubfolder("folder2");
+
+ // Create a third folder for more testing.
+ gLocalFolder3 = localAccountUtils.rootFolder.createLocalSubfolder("folder3");
+
+ gTestArray.forEach(x => add_task(x));
+ run_next_test();
+}
+
+// debug utility to show the key/offset/ID relationship of messages in a folder
+function showMessages(folder, text) {
+ dump(`***** Show messages for folder <${folder.name}> "${text} *****\n`);
+ for (let hdr of folder.messages) {
+ dump(
+ ` key: ${hdr.messageKey} offset: ${hdr.messageOffset} size: ${hdr.messageSize} ID: ${hdr.messageId}\n`
+ );
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_folderCompact2.js b/comm/mailnews/base/test/unit/test_folderCompact2.js
new file mode 100644
index 0000000000..dc9d0b865a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_folderCompact2.js
@@ -0,0 +1,300 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/*
+ * A few more specific mbox compaction tests for local folders:
+ * - mbox file contains exactly what we expect after compaction?
+ * test_folderCompact.js tends to rely on checking values in the msgDB.
+ * - works with messages larger than compaction code internal buffer?
+ * - X-Mozilla-Status/Status2/Keys headers handled as expected?
+ *
+ * Note: all these tests perform a seemingly-arbitrary delete.
+ * This is to trigger the folder compactor to actually do work. Without that
+ * delete it tends to think that compaction isn't required and does nothing.
+ */
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// Escape a string for more useful debug output - show EOLs and spaces.
+function esc(s) {
+ s = s.replace(/\r/g, "\\r");
+ s = s.replace(/\n/g, "\\n\n");
+ s = s.replace(/ /g, "\u2420"); // U+2420 SYMBOL FOR SPACE
+ return s;
+}
+
+// Load raw messages into folder (and sanitycheck them).
+function loadMsgs(folder, inputMsgs) {
+ folder.addMessageBatch(inputMsgs);
+ // Make sure all the loaded messages are the expected size.
+ // If this fails, it probably means addMessageBatch() no longer assumes input
+ // data is mbox format, which is good! See Bug 1763263.
+ let hdrs = Array.from(folder.messages);
+ for (let i = 0; i < hdrs.length; ++i) {
+ Assert.equal(
+ inputMsgs[i].length,
+ hdrs[i].messageSize,
+ `Loaded message ${i} should match size in msgDB`
+ );
+ }
+}
+
+// Delete the specified messages.
+async function deleteMsgs(folder, indexesToDelete) {
+ let hdrs = Array.from(folder.messages);
+ let doomed = indexesToDelete.map(i => hdrs[i]);
+ let listener = new PromiseTestUtils.PromiseCopyListener();
+ folder.deleteMessages(doomed, null, false, true, listener, true);
+ await listener.promise;
+}
+
+// Check the raw mbox file against what we expect to see.
+async function checkMbox(folder, outputMsgs) {
+ let bytes = await IOUtils.read(folder.filePath.path);
+ let mbox = "";
+ for (let b of bytes) {
+ mbox += String.fromCharCode(b);
+ }
+
+ let expected = "";
+ for (let raw of outputMsgs) {
+ // mbox has blank line between messages
+ expected += raw + "\n";
+ }
+
+ // Force all EOLs to linefeeds for comparisons.
+ // EOLs are handled inconsistently - most code will just leave EOLs as they
+ // come in, but new EOLs (e.g. added between messages in mbox) will use
+ // platform native EOLs. So our cheap and cheerful hack here is to just
+ // ditch all CRs and use pure LFs.
+ mbox = mbox.replace(/\r/g, "");
+ expected = expected.replace(/\r/g, "");
+
+ if (mbox != expected) {
+ // Pretty-print before we assert. Makes life so much easier.
+ dump(`=======mbox=========\n${esc(mbox)}\n============\n`);
+ dump(`=======expected=====\n${esc(expected)}\n============\n`);
+ }
+ Assert.ok(mbox == expected, "mbox should contain expected data");
+}
+
+// Some chunks from which we'll construct test messages.
+
+// These are the default X-Mozilla- headers for local folders (they are
+// re-written in place when flags and keywords are modified).
+let xhdrs =
+ `X-Mozilla-Status: 0000\r\n` +
+ `X-Mozilla-Status2: 00010000\r\n` + // 'New' flag is set
+ `X-Mozilla-Keys: \r\n`;
+
+let hdrs1 =
+ "Date: Fri, 21 Nov 1997 09:26:06 -0600\r\n" +
+ "From: bob@invalid\r\n" +
+ "Subject: Test message 1\r\n" +
+ "Message-ID: <blah1@invalid>\r\n";
+
+let bod1 = "Body of message 1.\r\n";
+
+let hdrs2 =
+ "Date: Fri, 21 Nov 1997 10:55:32 -0600\r\n" +
+ "From: bob@invalid\r\n" +
+ "Subject: Test message 2\r\n" +
+ "Message-ID: <blah2@invalid>\r\n";
+
+let bod2 = "Body of message2.\r\n";
+
+let hdrs3 =
+ `Date: Fri, 21 Nov 1997 11:09:14 -0600\r\n` +
+ `From: bob@invalid\r\n` +
+ `Message-ID: <blah3@invalid>\r\n` +
+ `Subject: Test message 3\r\n`;
+
+let bod3 = `message\r\nthree\r\nis multiple\r\nlines.\r\n`;
+
+let from = "From \r\n";
+
+// Check compact works after a simple delete.
+add_task(async function testSimple() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ let inMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs2}\r\n${bod2}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+ let doomed = [1]; // Delete message msg2.
+ // Out expected output:
+ let outMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+
+ loadMsgs(inbox, inMsgs);
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ await checkMbox(inbox, outMsgs);
+});
+
+// Check that local folder compact adds missing X-Mozilla- headers.
+add_task(async function testMissingXMozillaHdrs() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ // No X-Mozilla-* headers on input.
+ let inMsgs = [
+ `${from}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${hdrs2}\r\n${bod2}\r\n`,
+ `${from}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+ let doomed = [1]; // Delete msg2.
+ // Out expected output.
+ // Compact should have added X-Mozilla-* headers.
+ let outMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+
+ loadMsgs(inbox, inMsgs);
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ await checkMbox(inbox, outMsgs);
+});
+
+// Check localfolder compact works on messages are bigger than internal read buffer.
+add_task(async function testBigMessages() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ // Compaction uses buffer of around 16KB, so we'll go way bigger.
+ let targSize = 256 * 1024;
+
+ let inMsgs = [];
+ let outMsgs = [];
+ let doomed = [0, 1]; // We'll delete the first 2 messages.
+ for (let i = 0; i < 5; ++i) {
+ let raw =
+ `From \r\n` +
+ xhdrs +
+ `Date: Fri, 21 Nov 1997 09:55:06 -0600\r\n` +
+ `From: bob${i}@invalid\r\n` +
+ `Message-ID: <blah${i}@invalid>\r\n` +
+ `\r\n`;
+ while (raw.length < targSize) {
+ raw +=
+ "BlahBlahBlahBlahBlahBlahBlahBlahBlah" +
+ "BlahBlahBlahBlahBlahBlahBlahBlahBlah\r\n";
+ }
+ raw += `\r\n`;
+ inMsgs.push(raw);
+ if (!doomed.includes(i)) {
+ outMsgs.push(raw);
+ }
+ }
+
+ loadMsgs(inbox, inMsgs);
+
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ // outMsgs is what we expect to see in the mbox.
+ await checkMbox(inbox, outMsgs);
+});
+
+// Check that local folder compact moves X-Mozilla-* headers to start of
+// header block.
+add_task(async function testMoveXMozillaHdrs() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ // These have X-Mozilla-* headers after all the other headers.
+ let inMsgs = [
+ `${from}${hdrs1}${xhdrs}\r\n${bod1}\r\n`,
+ `${from}${hdrs2}${xhdrs}\r\n${bod2}\r\n`,
+ `${from}${hdrs3}${xhdrs}\r\n${bod3}\r\n`,
+ ];
+ let doomed = [1]; // Delete msg2.
+ // The messages we expect to see in the final mbox.
+ // Compact should have moved the X-Mozilla-* headers to the front.
+ let outMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+
+ loadMsgs(inbox, inMsgs);
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ await checkMbox(inbox, outMsgs);
+});
+
+// Check that local folder compact handles large X-Mozilla-Keys value.
+add_task(async function testBigXMozillaKeys() {
+ localAccountUtils.clearAll();
+ localAccountUtils.loadLocalMailAccount();
+ let inbox = localAccountUtils.inboxFolder;
+
+ let bigKeyword =
+ "HugeGreatBigStupidlyLongKeywordNameWhichWillDefinitelyOverflowThe80" +
+ "CharactersUsuallyReservedInTheKeywordsHeaderForInPlaceEditing";
+
+ let inMsgs = [
+ `${from}${xhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs2}\r\n${bod2}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+ let doomed = [1]; // Delete msg2.
+
+ let bigxhdrs =
+ `X-Mozilla-Status: 0000\r\n` +
+ `X-Mozilla-Status2: 00010000\r\n` + // 'New' flag is set
+ `X-Mozilla-Keys: ${bigKeyword}\r\n`;
+
+ // The messages we expect to see in the final mbox:
+ let outMsgs = [
+ `${from}${bigxhdrs}${hdrs1}\r\n${bod1}\r\n`,
+ `${from}${xhdrs}${hdrs3}\r\n${bod3}\r\n`,
+ ];
+
+ loadMsgs(inbox, inMsgs);
+
+ let msgs = Array.from(inbox.messages);
+ inbox.addKeywordsToMessages([msgs[0]], bigKeyword);
+
+ await deleteMsgs(inbox, doomed);
+
+ let l = new PromiseTestUtils.PromiseUrlListener();
+ inbox.compact(l, null);
+ await l.promise;
+
+ await checkMbox(inbox, outMsgs);
+});
diff --git a/comm/mailnews/base/test/unit/test_folderLookupService.js b/comm/mailnews/base/test/unit/test_folderLookupService.js
new file mode 100644
index 0000000000..039fa2a383
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_folderLookupService.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests that nsIFolderLookupService works according to specification.
+ */
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var kRootURI = "mailbox://nobody@Local%20Folders";
+
+add_task(async function test_fls_basics() {
+ let fls = Cc["@mozilla.org/mail/folder-lookup;1"].getService(
+ Ci.nsIFolderLookupService
+ );
+
+ // Make sure that the local mail account exists.
+ localAccountUtils.loadLocalMailAccount();
+
+ // There should exist an inbox.
+ Assert.equal(
+ fls.getFolderForURL(kRootURI + "/Inbox"),
+ localAccountUtils.inboxFolder
+ );
+
+ // Can we get the root folder?
+ let root = localAccountUtils.rootFolder;
+ Assert.equal(fls.getFolderForURL(kRootURI), root);
+
+ // The child folder Child doesn't exist yet... make sure that fls doesn't
+ // return it yet
+ Assert.equal(fls.getFolderForURL(kRootURI + "/Child"), null);
+
+ // Create the Child folder, and test we can find it.
+ // (NOTE: createSubFolder() can be async for IMAP folders. But for now
+ // we know we're using local folders and it'll happen right away).
+ root.createSubfolder("Child", null);
+ Assert.equal(
+ fls.getFolderForURL(kRootURI + "/Child"),
+ root.getChildNamed("Child")
+ );
+
+ // Try it again... it should load from cache this time.
+ Assert.equal(
+ fls.getFolderForURL(kRootURI + "/Child"),
+ root.getChildNamed("Child")
+ );
+
+ // Now delete the folder; we should be unable to find it
+ root.propagateDelete(root.getChildNamed("Child"), true);
+ Assert.equal(fls.getFolderForURL(kRootURI + "/Child"), null);
+
+ Assert.equal(fls.getFolderForURL(kRootURI + "/"), null);
+ Assert.equal(
+ fls.getFolderForURL("mailbox://idonotexist@Local%20Folders"),
+ null
+ );
+});
+
+add_task(async function test_unicode_uris() {
+ let fls = Cc["@mozilla.org/mail/folder-lookup;1"].getService(
+ Ci.nsIFolderLookupService
+ );
+ localAccountUtils.loadLocalMailAccount();
+ let root = localAccountUtils.rootFolder;
+
+ // Create a folder with non-ASCII characters.
+ // Unicode abuse - dotless letter i and a metal umlaut over the n.
+ let tapName = "Sp\u0131n\u0308al Tap";
+ root.createSubfolder(tapName, null);
+
+ // Make sure we can find it.
+ // (URI is percent-escaped utf-8)
+ let tapNameEscaped = "Sp%C4%B1n%CC%88al%20Tap";
+ if (AppConstants.platform == "win") {
+ // For !ConvertibleToNative(), folder name is hashed on Windows.
+ tapNameEscaped = "a2d874f7";
+ }
+ let tapURI = kRootURI + "/" + tapNameEscaped;
+ let tap = fls.getFolderForURL(tapURI);
+ Assert.equal(tap.URI, tapURI);
+});
diff --git a/comm/mailnews/base/test/unit/test_folderStringProperties.js b/comm/mailnews/base/test/unit/test_folderStringProperties.js
new file mode 100644
index 0000000000..8f508625b7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_folderStringProperties.js
@@ -0,0 +1,41 @@
+/* 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/. */
+
+/*
+ * Test the nsIMsgFolder .(get|set)StringProperty methods.
+ */
+
+add_task(function test_string_properties() {
+ localAccountUtils.loadLocalMailAccount();
+ let root = localAccountUtils.incomingServer.rootMsgFolder;
+
+ // Ensure unset properties return an error.
+ Assert.throws(function () {
+ root.getStringProperty("this-property-doesnt-exist");
+ }, /NS_ERROR_.*/);
+
+ // Check basic set/get operation.
+ root.setStringProperty("test-property", "wibble");
+ Assert.equal(root.getStringProperty("test-property"), "wibble");
+
+ // Keys are case-sensitive.
+ Assert.throws(function () {
+ root.getStringProperty("TEST-PROPERTY");
+ }, /NS_ERROR_.*/);
+
+ // Values with non-latin chars?
+ root.setStringProperty("test-property", "日本語");
+ Assert.equal(root.getStringProperty("test-property"), "日本語");
+
+ // Check that things stay as strings, even if they are values that could
+ // be misinterpreted in JSON.
+ root.setStringProperty("test-property", "");
+ Assert.equal(root.getStringProperty("test-property"), "");
+
+ root.setStringProperty("test-property", "null");
+ Assert.equal(root.getStringProperty("test-property"), "null");
+
+ root.setStringProperty("test-property", "0");
+ Assert.equal(root.getStringProperty("test-property"), "0");
+});
diff --git a/comm/mailnews/base/test/unit/test_formatFileSize.js b/comm/mailnews/base/test/unit/test_formatFileSize.js
new file mode 100644
index 0000000000..1269fd33c3
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_formatFileSize.js
@@ -0,0 +1,144 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests for the formatFileSize method.
+ */
+
+var gStringBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+);
+
+var gMessenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+function isDigit(c) {
+ return "0123456789".includes(c);
+}
+
+function test_formatFileSize(aArgs) {
+ const strings = {
+ b: "byteAbbreviation2",
+ kb: "kiloByteAbbreviation2",
+ mb: "megaByteAbbreviation2",
+ gb: "gigaByteAbbreviation2",
+ tb: "teraByteAbbreviation2",
+ pb: "petaByteAbbreviation2",
+ };
+
+ let actual = gMessenger.formatFileSize(aArgs.bytes, aArgs.useKB);
+ let expected = gStringBundle
+ .GetStringFromName(strings[aArgs.units])
+ .replace("%.*f", aArgs.mantissa);
+
+ // If the actual string contains a non-numeric character at the position
+ // where we'd expect a decimal separator, assume it is a localized separator
+ // and just convert it to a dot for easy comparing.
+ let separatorPos = aArgs.mantissa.indexOf(".");
+ if (!isDigit(actual.charAt(separatorPos))) {
+ actual =
+ actual.substring(0, separatorPos) + "." + actual.substr(separatorPos + 1);
+ }
+
+ Assert.equal(actual, expected);
+}
+
+var test_data = [
+ { bytes: 0, useKB: false, mantissa: "0", units: "b" },
+ { bytes: 1, useKB: false, mantissa: "1", units: "b" },
+ { bytes: 10, useKB: false, mantissa: "10", units: "b" },
+ { bytes: 999, useKB: false, mantissa: "999", units: "b" },
+ { bytes: 1000, useKB: false, mantissa: "1.0", units: "kb" },
+ { bytes: 1024, useKB: false, mantissa: "1.0", units: "kb" },
+ { bytes: 10 * 1024, useKB: false, mantissa: "10.0", units: "kb" },
+ { bytes: 999 * 1024, useKB: false, mantissa: "999", units: "kb" },
+ { bytes: 1000 * 1024, useKB: false, mantissa: "1.0", units: "mb" },
+ { bytes: 1024 * 1024, useKB: false, mantissa: "1.0", units: "mb" },
+ { bytes: 10 * 1024 * 1024, useKB: false, mantissa: "10.0", units: "mb" },
+ { bytes: 999 * 1024 * 1024, useKB: false, mantissa: "999", units: "mb" },
+ { bytes: 1000 * 1024 * 1024, useKB: false, mantissa: "1.0", units: "gb" },
+ { bytes: 1024 * 1024 * 1024, useKB: false, mantissa: "1.0", units: "gb" },
+ {
+ bytes: 10 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "10.0",
+ units: "gb",
+ },
+ {
+ bytes: 999 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "999",
+ units: "gb",
+ },
+ {
+ bytes: 1000 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "tb",
+ },
+ {
+ bytes: 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "tb",
+ },
+ {
+ bytes: 10 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "10.0",
+ units: "tb",
+ },
+ {
+ bytes: 999 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "999",
+ units: "tb",
+ },
+ {
+ bytes: 1000 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "pb",
+ },
+ {
+ bytes: 1000 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "pb",
+ },
+ {
+ bytes: 1024 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1.0",
+ units: "pb",
+ },
+ {
+ bytes: 10 * 1024 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "10.0",
+ units: "pb",
+ },
+ {
+ bytes: 999 * 1024 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "999",
+ units: "pb",
+ },
+ {
+ bytes: 1000 * 1024 * 1024 * 1024 * 1024 * 1024,
+ useKB: false,
+ mantissa: "1000",
+ units: "pb",
+ },
+
+ { bytes: 0, useKB: true, mantissa: "0", units: "kb" },
+ { bytes: 1, useKB: true, mantissa: "0.1", units: "kb" },
+ { bytes: 500, useKB: true, mantissa: "0.5", units: "kb" },
+ { bytes: 999, useKB: true, mantissa: "1.0", units: "kb" },
+];
+
+add_task(function test_format_file_size() {
+ test_data.map(entry => {
+ test_formatFileSize(entry);
+ });
+});
diff --git a/comm/mailnews/base/test/unit/test_getMsgTextFromStream.js b/comm/mailnews/base/test/unit/test_getMsgTextFromStream.js
new file mode 100644
index 0000000000..b803619e21
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_getMsgTextFromStream.js
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test suite for GetMsgTextFromStream.
+ *
+ * Currently tests: text/plain, text/html -- with tags stripped and without,
+ * base64, multipart.
+ * Does not currently test: quoted-printable, stripping quotes, UTF-8, small values of
+ * bytesToRead.
+ */
+var kDataRoot = "../../../data/";
+
+function create(fileName, bytes, compressQuotes, stripHTML, outContentType) {
+ return {
+ name: fileName,
+ bytesToRead: bytes,
+ compressQuotes,
+ stripHTML,
+ contentType: outContentType,
+ };
+}
+
+var gTestFiles = [
+ create("basic1", 1024, false, false, "text/plain"), // Simple plain text
+ create("basic1", 1024, false, true, "text/plain"), // should be same as above
+ create("basic2", 1024, false, false, "text/html"), // Simple HTML
+ create("basic3", 1024, false, true, "text/html"), // HTML with tags stripped out
+ create("basic4", 1024, false, false, "text/plain"), // No content type, should be assumed to be text/plain
+ create("basic4", 1024, false, true, "text/plain"),
+ create("basic5", 1024, false, false, "text/plain"), // HTML content in text/plain
+ create("basic5", 1024, false, true, "text/plain"),
+ create("base64-1", 1024, false, false, "text/plain"), // base64 text/plain
+ create("base64-1", 1024, false, true, "text/plain"),
+ create("base64-2", 1024, false, false, "text/html"), // base64 text/html
+ create("base64-3", 1024, false, true, "text/html"), // strip out tags here
+ create("multipart1", 1024, false, false, "text/plain"), // basic multipart message
+ create("multipart1", 1024, false, true, "text/plain"),
+ create("multipart2", 1024, false, false, "text/html"), // multipart HTML
+ create("multipart3", 1024, false, true, "text/html"),
+ create("multipart4", 1024, false, false, "text/plain"), // text with no headers
+ create("multipart4", 1024, false, true, "text/plain"),
+ create("multipart-base64-1", 1024, false, false, "text/plain"), // base64 encoded text
+ create("multipart-base64-1", 1024, false, true, "text/plain"),
+ create("multipart-base64-2", 1024, false, false, "text/html"),
+ create("multipart-base64-3", 1024, false, true, "text/html"),
+ create("multipart-complex1", 1024, false, true, "text/html"), // Things get more complex here
+ create("multipart-complex2", 1024, false, false, "text/plain"),
+ create("multipart-complex2", 1024, false, true, "text/plain"),
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ var folder = localAccountUtils.incomingServer.rootMsgFolder;
+
+ gTestFiles.forEach(function (test) {
+ dump("Testing " + test.name + "\n");
+ var inFile = do_get_file(kDataRoot + test.name);
+ var inStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ inStream.init(inFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+
+ // Now get the message body using getMsgTextFromStream
+ var contentType = {};
+ var body = folder.getMsgTextFromStream(
+ inStream,
+ "",
+ test.bytesToRead,
+ 65536,
+ test.compressQuotes,
+ test.stripHTML,
+ contentType
+ );
+
+ // Now we need to compare the output
+ Assert.equal(test.contentType, contentType.value);
+
+ var resultFile = do_get_file(kDataRoot + test.name + ".out");
+ var actualBody = mailTestUtils.loadFileToString(resultFile, "UTF-8");
+ Assert.equal(body, actualBody);
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_headerFoldingInDatabase.js b/comm/mailnews/base/test/unit/test_headerFoldingInDatabase.js
new file mode 100644
index 0000000000..c3165ba720
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_headerFoldingInDatabase.js
@@ -0,0 +1,58 @@
+/* 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/. */
+
+/*
+ * Testing header folding in nsParseMailMessageState::ParseHeaders(),
+ * see bug 1454257 and bug 1456001.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var hdr;
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ var copyListener = {
+ OnStartCopy() {},
+ OnProgress(aProgress, aProgressMax) {},
+ SetMessageKey(aKey) {
+ hdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey);
+ },
+ SetMessageId(aMessageId) {},
+ OnStopCopy(aStatus) {
+ continueTest();
+ },
+ };
+
+ // Get a message into the local filestore.
+ var message = do_get_file("../../../data/badly-folded-headers.eml");
+ do_test_pending();
+ MailServices.copy.copyFileMessage(
+ message,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+}
+
+function continueTest() {
+ Assert.equal(hdr.author, "sender@example.com");
+ Assert.equal(
+ hdr.recipients,
+ '"Recipient with spaces" <recipient@example.com>'
+ );
+ Assert.equal(
+ hdr.subject,
+ "Badly folded headers, one line with space between To and From"
+ );
+ hdr = null;
+ do_test_finished();
+}
diff --git a/comm/mailnews/base/test/unit/test_hostnameUtils.js b/comm/mailnews/base/test/unit/test_hostnameUtils.js
new file mode 100644
index 0000000000..5f8839165d
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_hostnameUtils.js
@@ -0,0 +1,276 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests for hostnameUtils.jsm.
+ */
+
+var {
+ isLegalHostName,
+ isLegalHostNameOrIP,
+ isLegalIPAddress,
+ isLegalIPv4Address,
+ isLegalIPv6Address,
+ isLegalLocalIPAddress,
+} = ChromeUtils.import("resource:///modules/hostnameUtils.jsm");
+
+/**
+ * Checks if valid and invalid IPs are properly allowed or rejected.
+ */
+function test_IPaddresses() {
+ const kIPsToTest = [
+ // isValid, IP addr. isIPv6, isLocal, extend, result
+ // IPv4
+ [true, "1.2.3.4", false, false, false],
+ [true, "123.245.111.222", false, false, false],
+ [true, "255.255.255.255", false, false, false],
+ [true, "1.2.0.4", false, false, false],
+ [true, "1.2.3.4", false, false, false],
+ [true, "127.1.2.3", false, true, false],
+ [true, "10.1.2.3", false, true, false],
+ [true, "192.168.2.3", false, true, false],
+
+ [false, "1.2.3.4.5", false, false, false],
+ [false, "1.2.3", false, false, false],
+ [false, "1.2.3.", false, false, false],
+ [false, ".1.2.3", false, false, false],
+ [false, "1.2.3.256", false, false, false],
+ [false, "1.2.3.12345", false, false, false],
+ [false, "1.2..123", false, false, false],
+ [false, "1", false, false, false],
+ [false, "", false, false, false],
+ [false, "0.1.2.3", false, false, false],
+ [false, "0.0.2.3", false, false, false],
+ [false, "0.0.0.0", false, false, false],
+ [false, "1.2.3.d", false, false, false],
+ [false, "a.b.c.d", false, false, false],
+ [false, "a.b.c.d", false, false, true],
+ // IPv6
+ [
+ true,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ true,
+ false,
+ false,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ ],
+ [
+ true,
+ "2001:db8:85a3:0:0:8a2e:370:7334",
+ true,
+ false,
+ false,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ ],
+ [
+ true,
+ "2001:db8:85a3::8a2e:370:7334",
+ true,
+ false,
+ false,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:7334",
+ ],
+ [
+ true,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:",
+ true,
+ false,
+ false,
+ "2001:0db8:85a3:0000:0000:8a2e:0370:0000",
+ ],
+ [
+ true,
+ "::ffff:c000:0280",
+ true,
+ false,
+ false,
+ "0000:0000:0000:0000:0000:ffff:c000:0280",
+ ],
+ [
+ true,
+ "::ffff:192.0.2.128",
+ true,
+ false,
+ false,
+ "0000:0000:0000:0000:0000:ffff:c000:0280",
+ ],
+ [
+ true,
+ "2001:db8::1",
+ true,
+ false,
+ false,
+ "2001:0db8:0000:0000:0000:0000:0000:0001",
+ ],
+ [
+ true,
+ "2001:DB8::1",
+ true,
+ false,
+ false,
+ "2001:0db8:0000:0000:0000:0000:0000:0001",
+ ],
+ [
+ true,
+ "1:2:3:4:5:6:7:8",
+ true,
+ false,
+ false,
+ "0001:0002:0003:0004:0005:0006:0007:0008",
+ ],
+
+ [true, "::1", true, true, false, "0000:0000:0000:0000:0000:0000:0000:0001"],
+ [
+ true,
+ "::0000:0000:1",
+ true,
+ true,
+ false,
+ "0000:0000:0000:0000:0000:0000:0000:0001",
+ ],
+
+ [false, "::", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:8a2e:0370:73346", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:8a2e:0370:7334:1", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:8a2e:0370:7334x", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:8a2e:03707334", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000x8a2e:0370:7334", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:::1", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:0000:some:junk", true, false, false],
+ [false, "2001:0db8:85a3:0000:0000:0000::192.0.2.359", true, false, false],
+ [false, "some::junk", true, false, false],
+ [false, "some_junk", true, false, false],
+
+ // Extended formats of IPv4, hex, octal, decimal up to DWORD
+ [true, "0xff.0x12.0x45.0x78", false, false, true, "255.18.69.120"],
+ [true, "01.0123.056.077", false, false, true, "1.83.46.63"],
+ [true, "0xff.2.3.4", false, false, true, "255.2.3.4"],
+ [true, "0xff.2.3.077", false, false, true, "255.2.3.63"],
+ [true, "0x7f.2.3.077", false, true, true, "127.2.3.63"],
+
+ [false, "0xZZ.1.2.3", false, false, true],
+ [false, "0x00.0123.056.077", false, false, true],
+ [false, "0x11.0123.056.078", false, false, true],
+ [false, "0x11.0123.056.0789", false, false, true],
+
+ [true, "1234566945", false, false, true, "73.149.255.33"],
+ [false, "12345", false, false, true],
+ [false, "123456789123456", false, false, true],
+
+ [true, "127.1", false, true, true, "127.0.0.1"],
+ [true, "0x7f.100", false, true, true, "127.0.0.100"],
+ [true, "0x7f.100.1000", false, true, true, "127.100.3.232"],
+ [true, "0xff.100.1024", false, false, true, "255.100.4.0"],
+ [true, "0xC0.0xA8.0x2A48", false, true, true, "192.168.42.72"],
+ [true, "0xC0.0xA82A48", false, true, true, "192.168.42.72"],
+ [true, "0xC0A82A48", false, true, true, "192.168.42.72"],
+ [true, "0324.062477106", false, false, true, "212.202.126.70"],
+
+ [false, "0.0.1000", false, false, true],
+ [false, "0324.06247710677", false, false, true],
+ ];
+
+ for (let item of kIPsToTest) {
+ let result = null;
+ let [isValid, address, isIPv6, isLocal, isExtended, wantedResult] = item;
+ if (!wantedResult) {
+ wantedResult = isValid ? address : null;
+ }
+
+ if (isIPv6) {
+ result = isLegalIPv6Address(address);
+ Assert.equal(result, wantedResult);
+ if (isValid) {
+ // If this is valid IPv6, it can't be valid IPv4. The opposite is unknown.
+ result = isLegalIPv4Address(address);
+ Assert.equal(result, null);
+ }
+ } else {
+ result = isLegalIPv4Address(address, isExtended);
+ Assert.equal(result, wantedResult);
+ if (isValid) {
+ // If this is valid IPv4, it can't be valid IPv6. The opposite is unknown.
+ result = isLegalIPv6Address(address);
+ Assert.equal(result, null);
+ }
+ }
+
+ result = isLegalIPAddress(address, isExtended);
+ Assert.equal(result, wantedResult);
+
+ if (isValid) {
+ // isLegalLocalIPAddress operates on a normalized address,
+ // not the original one.
+ result = isLegalLocalIPAddress(result);
+ Assert.equal(result, isLocal);
+ }
+
+ // If something is a valid IP, it also passes isLegalHostNameOrIP.
+ // However, an invalid IP string may still be a valid hostname.
+ // So only check success if the IP is valid.
+ result = isLegalHostNameOrIP(address, isExtended);
+ if (isValid) {
+ Assert.equal(result, wantedResult);
+ }
+ }
+}
+/**
+ * Checks if valid and invalid host names are properly allowed or rejected.
+ */
+function test_hostnames() {
+ const kHostsToTest = [
+ // isValid, hostname
+ [true, "localhost"],
+ [true, "some-server"],
+ [true, "server.company.invalid"],
+ [true, "server.comp-any.invalid"],
+ [true, "server.123.invalid"],
+ [true, "1server.123.invalid"],
+ [true, "1.2.3.4.5"],
+ [true, "very.log.sub.domain.name.invalid"],
+ [true, "1234567890"],
+ [true, "1234567890."], // FQDN
+ [true, "server.company.invalid."], // FQDN
+
+ [false, ""],
+ [false, "server.badcompany!.invalid"],
+ [false, "server._badcompany.invalid"],
+ [false, "server.bad_company.invalid"],
+ [false, "server.badcompany-.invalid"],
+ [false, "server.bad company.invalid"],
+ [false, "server.b…dcompany.invalid"],
+ [false, ".server.badcompany.invalid"],
+ [
+ false,
+ "make-this-a-long-host-name-component-that-is-over-63-characters-long.invalid",
+ ],
+ [
+ false,
+ "append-strings-to-make-this-a-too-long-host-name.that-is-really-over-255-characters-long.invalid." +
+ "append-strings-to-make-this-a-too-long-host-name.that-is-really-over-255-characters-long.invalid." +
+ "append-strings-to-make-this-a-too-long-host-name.that-is-really-over-255-characters-long.invalid." +
+ "append-strings-to-make-this-a-too-long-host-name.that-is-really-over-255-characters-long.invalid",
+ ],
+ ];
+
+ for (let item of kHostsToTest) {
+ let result = null;
+ let [wantedResult, hostname] = item;
+ wantedResult = wantedResult ? hostname : null;
+
+ result = isLegalHostName(hostname);
+ Assert.equal(result, wantedResult);
+
+ result = isLegalHostNameOrIP(hostname, false);
+ Assert.equal(result, wantedResult);
+ }
+}
+
+var gTests = [test_IPaddresses, test_hostnames];
+
+function run_test() {
+ for (let test of gTests) {
+ test();
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_identity.js b/comm/mailnews/base/test/unit/test_identity.js
new file mode 100644
index 0000000000..c28615451a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_identity.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests the UID attribute of identities.
+ */
+add_task(async function testUID() {
+ const UUID_REGEXP =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+
+ // Create an identity and check it the UID is set when accessed.
+
+ let identityA = MailServices.accounts.createIdentity();
+ Assert.stringMatches(
+ identityA.UID,
+ UUID_REGEXP,
+ "identity A's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.identity.${identityA.key}.uid`),
+ identityA.UID,
+ "identity A's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (identityA.UID = "00001111-2222-3333-4444-555566667777"),
+ /NS_ERROR_ABORT/,
+ "identity A's UID should be unchangeable after it is set"
+ );
+
+ // Create a second identity and check the two UIDs don't match.
+
+ let identityB = MailServices.accounts.createIdentity();
+ Assert.stringMatches(
+ identityB.UID,
+ UUID_REGEXP,
+ "identity B's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.identity.${identityB.key}.uid`),
+ identityB.UID,
+ "identity B's UID should be saved to the preferences"
+ );
+ Assert.notEqual(
+ identityB.UID,
+ identityA.UID,
+ "identity B's UID should not be the same as identity A's"
+ );
+
+ // Create a third identity and set the UID before it is accessed.
+
+ let identityC = MailServices.accounts.createIdentity();
+ identityC.UID = "11112222-3333-4444-5555-666677778888";
+ Assert.equal(
+ identityC.UID,
+ "11112222-3333-4444-5555-666677778888",
+ "identity C's UID set correctly"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.identity.${identityC.key}.uid`),
+ "11112222-3333-4444-5555-666677778888",
+ "identity C's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (identityC.UID = "22223333-4444-5555-6666-777788889999"),
+ /NS_ERROR_ABORT/,
+ "identity C's UID should be unchangeable after it is set"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_imapPump.js b/comm/mailnews/base/test/unit/test_imapPump.js
new file mode 100644
index 0000000000..ccc34bae60
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_imapPump.js
@@ -0,0 +1,81 @@
+/* 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/. */
+
+/**
+ * Simple demonstration of the imap pump test method.
+ */
+
+// async support
+/* import-globals-from ../../../test/resources/logHelper.js */
+load("../../../resources/logHelper.js");
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// IMAP pump
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { ImapMessage } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Imapd.jsm"
+);
+
+var { fsDebugAll } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+
+// Globals
+
+// Messages to load must have CRLF line endings, that is Windows style
+var gMessage = "bugmail10"; // message file used as the test message
+
+// Definition of tests
+
+// load and update a message in the imap fake server
+
+var gTestArray = [
+ // initial setup of IMAP environment
+ setupIMAPPump,
+
+ // optionally set server parameters, here enabling debug messages
+ function serverParms() {
+ IMAPPump.server.setDebugLevel(fsDebugAll);
+ },
+
+ // the main test
+ async function loadImapMessage() {
+ IMAPPump.mailbox.addMessage(
+ new ImapMessage(specForFileName(gMessage), IMAPPump.mailbox.uidnext++, [])
+ );
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+
+ Assert.equal(1, IMAPPump.inbox.getTotalMessages(false));
+ let msgHdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ Assert.ok(msgHdr instanceof Ci.nsIMsgDBHdr);
+ },
+
+ // all done
+ teardownIMAPPump,
+];
+
+add_setup(() => {
+ Services.prefs.setBoolPref(
+ "mail.server.default.autosync_offline_stores",
+ false
+ );
+ gTestArray.forEach(x => add_task(x));
+});
+
+/*
+ * helper functions
+ */
+
+// given a test file, return the file uri spec
+function specForFileName(aFileName) {
+ let file = do_get_file(gDEPTH + "mailnews/data/" + aFileName);
+ let msgfileuri = Services.io.newFileURI(file).QueryInterface(Ci.nsIFileURL);
+ return msgfileuri.spec;
+}
diff --git a/comm/mailnews/base/test/unit/test_incomingServer.js b/comm/mailnews/base/test/unit/test_incomingServer.js
new file mode 100644
index 0000000000..9093ab4806
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_incomingServer.js
@@ -0,0 +1,99 @@
+/* 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/. */
+
+function subtestUID(type) {
+ const UUID_REGEXP =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+
+ // Create a server and check it the UID is set when accessed.
+
+ let serverA = MailServices.accounts.createIncomingServer(
+ "userA",
+ "hostA",
+ type
+ );
+ Assert.stringMatches(
+ serverA.UID,
+ UUID_REGEXP,
+ "server A's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.server.${serverA.key}.uid`),
+ serverA.UID,
+ "server A's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (serverA.UID = "00001111-2222-3333-4444-555566667777"),
+ /NS_ERROR_ABORT/,
+ "server A's UID should be unchangeable after it is set"
+ );
+
+ // Create a second server and check the two UIDs don't match.
+
+ let serverB = MailServices.accounts.createIncomingServer(
+ "userB",
+ "hostB",
+ type
+ );
+ Assert.stringMatches(
+ serverB.UID,
+ UUID_REGEXP,
+ "server B's UID should exist and be a UUID"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.server.${serverB.key}.uid`),
+ serverB.UID,
+ "server B's UID should be saved to the preferences"
+ );
+ Assert.notEqual(
+ serverB.UID,
+ serverA.UID,
+ "server B's UID should not be the same as server A's"
+ );
+
+ // Create a third server and set the UID before it is accessed.
+
+ let serverC = MailServices.accounts.createIncomingServer(
+ "userC",
+ "hostC",
+ type
+ );
+ serverC.UID = "11112222-3333-4444-5555-666677778888";
+ Assert.equal(
+ serverC.UID,
+ "11112222-3333-4444-5555-666677778888",
+ "server C's UID set correctly"
+ );
+ Assert.equal(
+ Services.prefs.getStringPref(`mail.server.${serverC.key}.uid`),
+ "11112222-3333-4444-5555-666677778888",
+ "server C's UID should be saved to the preferences"
+ );
+ Assert.throws(
+ () => (serverC.UID = "22223333-4444-5555-6666-777788889999"),
+ /NS_ERROR_ABORT/,
+ "server C's UID should be unchangeable after it is set"
+ );
+}
+
+/**
+ * Tests the UID attribute of IMAP servers.
+ */
+add_task(function testUID_IMAP() {
+ subtestUID("imap");
+});
+
+/**
+ * Tests the UID attribute of NNTP servers.
+ */
+add_task(function testUID_NNTP() {
+ subtestUID("nntp");
+});
+
+/**
+ * Tests the UID attribute of POP3 servers.
+ */
+add_task(function testUID_POP3() {
+ subtestUID("pop3");
+});
diff --git a/comm/mailnews/base/test/unit/test_inheritedFolderProperties.js b/comm/mailnews/base/test/unit/test_inheritedFolderProperties.js
new file mode 100644
index 0000000000..55fb1da6c8
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_inheritedFolderProperties.js
@@ -0,0 +1,183 @@
+/* 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/. */
+
+/*
+ * Testing of inherited folder properties
+ */
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+ var rootFolder = localAccountUtils.incomingServer.rootMsgFolder;
+
+ // add subfolders to the inbox
+ const subFolder11 = localAccountUtils.inboxFolder
+ .createLocalSubfolder("subfolder11")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+ const subFolder12 = localAccountUtils.inboxFolder
+ .createLocalSubfolder("subfolder12")
+ .QueryInterface(Ci.nsIMsgLocalMailFolder);
+ const subFolder21 = subFolder11.createLocalSubfolder("subfolder21");
+ const subFolder22 = subFolder12.createLocalSubfolder("subfolder22");
+
+ // add a global preference
+ const propertyName = "iexist";
+ const invalidName = "idontexist";
+ const globalPref = "mail.server.default." + propertyName;
+ const globalValue = "iAmGlobal";
+ const folderValue = "iAmFolder";
+ const folderValue2 = "iAmFolder2";
+ const rootValue = "iAmRoot";
+ Services.prefs.setCharPref(globalPref, globalValue);
+
+ // test that the global preference is honored
+ Assert.equal(
+ rootFolder.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder22.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(rootFolder.getInheritedStringProperty(invalidName), null);
+ Assert.equal(subFolder11.getInheritedStringProperty(invalidName), null);
+ Assert.equal(subFolder22.getInheritedStringProperty(invalidName), null);
+
+ // set a value on a subfolder and check
+ subFolder11.setStringProperty(propertyName, folderValue);
+ Assert.equal(
+ rootFolder.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(
+ subFolder12.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(
+ subFolder22.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+
+ // set a root folder value and check
+ localAccountUtils.incomingServer.setCharValue(propertyName, rootValue);
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), rootValue);
+
+ // force an empty string
+ subFolder12.setForcePropertyEmpty(propertyName, true);
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), "");
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), "");
+
+ // reset a folder to allow inheritance
+ subFolder12.setForcePropertyEmpty(propertyName, false);
+ subFolder12.setStringProperty(propertyName, "");
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), rootValue);
+
+ // force an empty string on the server
+ localAccountUtils.incomingServer.setForcePropertyEmpty(propertyName, true);
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), "");
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), "");
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), "");
+
+ // reset a server to allow inheritance from the global
+ localAccountUtils.incomingServer.setCharValue(propertyName, "");
+ localAccountUtils.incomingServer.setForcePropertyEmpty(propertyName, false);
+ Assert.equal(
+ rootFolder.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(
+ subFolder12.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(
+ subFolder22.getInheritedStringProperty(propertyName),
+ globalValue
+ );
+
+ // check with all levels populated
+ subFolder21.setStringProperty(propertyName, folderValue2);
+ localAccountUtils.incomingServer.setCharValue(propertyName, rootValue);
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), rootValue);
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue2
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), rootValue);
+
+ // clear the global value and the root value
+ Services.prefs.clearUserPref(globalPref);
+ localAccountUtils.incomingServer.setCharValue(propertyName, "");
+ Assert.equal(rootFolder.getInheritedStringProperty(propertyName), null);
+ Assert.equal(
+ subFolder11.getInheritedStringProperty(propertyName),
+ folderValue
+ );
+ Assert.equal(subFolder12.getInheritedStringProperty(propertyName), null);
+ Assert.equal(
+ subFolder21.getInheritedStringProperty(propertyName),
+ folderValue2
+ );
+ Assert.equal(subFolder22.getInheritedStringProperty(propertyName), null);
+}
diff --git a/comm/mailnews/base/test/unit/test_junkingWhenDisabled.js b/comm/mailnews/base/test/unit/test_junkingWhenDisabled.js
new file mode 100644
index 0000000000..b6a103b069
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_junkingWhenDisabled.js
@@ -0,0 +1,176 @@
+/* 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/. */
+
+/*
+ * Test that junk actions work even when the bayes filtering of incoming
+ * messages is disabled, as fixed in bug 487610. Test developed by Kent
+ * James using test_nsMsgDBView.js as a base.
+ */
+
+const { TreeSelection } = ChromeUtils.importESModule(
+ "chrome://messenger/content/tree-selection.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+
+var nsIMFNService = Ci.nsIMsgFolderNotificationService;
+
+// fake objects needed to get nsMsgDBView to operate on selected messages.
+// Warning: these are partial implementations. If someone adds additional
+// calls to these objects in nsMsgDBView and friends, it will also
+// be necessary to add fake versions of those calls here.
+
+var gFakeSelection = new TreeSelection(null);
+
+// Items used to add messages to the folder
+
+var gMessageGenerator = new MessageGenerator();
+
+var messageInjection = new MessageInjection(
+ { mode: "local" },
+ gMessageGenerator
+);
+
+var gLocalInboxFolder = messageInjection.getInboxFolder();
+var gListener;
+var gCommandUpdater;
+
+var gDBView;
+var gTreeView;
+
+var CommandUpdaterWithPromise = function () {
+ this.deferred = PromiseUtils.defer();
+};
+CommandUpdaterWithPromise.prototype = {
+ async promiseSelectionSummarized() {
+ await this.deferred.promise;
+ this.deferred = PromiseUtils.defer();
+ return this.deferred.promise;
+ },
+
+ updateCommandStatus() {
+ // the back end is smart and is only telling us to update command status
+ // when the # of items in the selection has actually changed.
+ },
+
+ displayMessageChanged(aFolder, aSubject, aKeywords) {},
+
+ updateNextMessageAfterDelete() {},
+ summarizeSelection() {
+ this.deferred.resolve();
+ },
+};
+
+// Our listener, which captures events and does the real tests.
+function gMFListener() {
+ this._promiseMsgsMoveCopyCompleted = new Promise(resolve => {
+ this._resolveMsgsMoveCopyCompleted = resolve;
+ });
+ this._promiseFolderAdded = new Promise(resolve => {
+ this._resolveFolderAdded = resolve;
+ });
+}
+gMFListener.prototype = {
+ msgsMoveCopyCompleted(aMove, aSrcMsgs, aDestFolder, aDestMsgs) {
+ Assert.ok(aDestFolder.getFlag(Ci.nsMsgFolderFlags.Junk));
+ // I tried to test this by counting messages in the folder, didn't work.
+ // Maybe all updates are not completed yet. Anyway I do it by just
+ // making sure there is something in the destination array.
+ Assert.ok(aDestMsgs.length > 0);
+ this._resolveMsgsMoveCopyCompleted();
+ },
+
+ folderAdded(aFolder) {
+ // this should be a junk folder
+ Assert.ok(aFolder.getFlag(Ci.nsMsgFolderFlags.Junk));
+ this._resolveFolderAdded();
+ },
+ get promiseMsgsMoveCopyCompleted() {
+ return this._promiseMsgsMoveCopyCompleted;
+ },
+ get promiseFolderAdded() {
+ return this._promiseFolderAdded;
+ },
+};
+
+add_setup(async function () {
+ // Set option so that when messages are marked as junk, they move to the junk folder
+ Services.prefs.setBoolPref("mail.spam.manualMark", true);
+
+ // 0 == "move to junk folder", 1 == "delete"
+ Services.prefs.setIntPref("mail.spam.manualMarkMode", 0);
+
+ // Disable bayes filtering on the local account. That's the whole point of this test,
+ // to make sure that the junk move happens anyway.
+ gLocalInboxFolder.server.spamSettings.level = 0;
+
+ // Add folder listeners that will capture async events.
+ let flags = nsIMFNService.msgsMoveCopyCompleted | nsIMFNService.folderAdded;
+ gListener = new gMFListener();
+ MailServices.mfn.addListener(gListener, flags);
+
+ // Build up a message.
+ await messageInjection.makeNewSetsInFolders([gLocalInboxFolder], [{}]);
+ let view_type = "threaded";
+ let view_flag = Ci.nsMsgViewFlagsType.kThreadedDisplay;
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=" + view_type;
+
+ // Always start out fully expanded.
+ view_flag |= Ci.nsMsgViewFlagsType.kExpandAll;
+
+ gCommandUpdater = new CommandUpdaterWithPromise();
+
+ gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ gDBView.init(null, null, null);
+ var outCount = {};
+ gDBView.open(
+ gLocalInboxFolder,
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ view_flag,
+ outCount
+ );
+
+ gTreeView = gDBView.QueryInterface(Ci.nsITreeView);
+ gTreeView.selection = gFakeSelection;
+ gFakeSelection.view = gTreeView;
+});
+
+add_task(async function test_first_junking_create_folder() {
+ // In the proposed fix for bug 487610, the first call to junk messages
+ // only creates the junk folder, it does not actually successfully move
+ // messages. So we junk messages twice so we can really see a move. But
+ // if that gets fixed and the messages actually move on the first call,
+ // I want this test to succeed as well. So I don't actually count how
+ // many messages get moved, just that some do on the second move.
+
+ // Select and junk all messages.
+ gDBView.doCommand(Ci.nsMsgViewCommandType.selectAll);
+ gDBView.doCommand(Ci.nsMsgViewCommandType.junk);
+ await gCommandUpdater.promiseSelectionSummarized;
+ await gListener.promiseFolderAdded;
+});
+
+add_task(async function test_add_further_message() {
+ // Add another message in case the first one moved.
+ await messageInjection.makeNewSetsInFolders([gLocalInboxFolder], [{}]);
+});
+
+add_task(async function test_second_junking_move_msgs() {
+ // Select and junk all messages.
+ gDBView.doCommand(Ci.nsMsgViewCommandType.selectAll);
+ gDBView.doCommand(Ci.nsMsgViewCommandType.junk);
+ await gCommandUpdater.promiseSelectionSummarized;
+ await gListener.promiseMsgsMoveCopyCompleted;
+});
diff --git a/comm/mailnews/base/test/unit/test_loadVirtualFolders.js b/comm/mailnews/base/test/unit/test_loadVirtualFolders.js
new file mode 100644
index 0000000000..13afd3f03b
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_loadVirtualFolders.js
@@ -0,0 +1,64 @@
+/* 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/. */
+
+// Test loading of virtualFolders.dat, including verification of the search
+// scopes, i.e., folder uri's.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// As currently written, this test will only work with Berkeley store.
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// main test
+
+function run_test() {
+ let vfdat = do_get_file("../../../data/test_virtualFolders.dat");
+
+ vfdat.copyTo(do_get_profile(), "virtualFolders.dat");
+ localAccountUtils.loadLocalMailAccount();
+ let localMailDir = do_get_profile().clone();
+ localMailDir.append("Mail");
+ localMailDir.append("Local Folders");
+ localMailDir.append("unread-local");
+ localMailDir.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ localMailDir.leafName = "invalidserver-local";
+ localMailDir.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ localMailDir.leafName = "$label1";
+ localMailDir.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ MailServices.accounts.loadVirtualFolders();
+ let unreadLocal =
+ localAccountUtils.incomingServer.rootMsgFolder.getChildNamed(
+ "unread-local"
+ );
+ let searchScope =
+ unreadLocal.msgDatabase.dBFolderInfo.getCharProperty("searchFolderUri");
+ Assert.equal(
+ searchScope,
+ "mailbox://nobody@Local%20Folders/Inbox|mailbox://nobody@Local%20Folders/Trash"
+ );
+ let invalidServer =
+ localAccountUtils.incomingServer.rootMsgFolder.getChildNamed(
+ "invalidserver-local"
+ );
+ searchScope =
+ invalidServer.msgDatabase.dBFolderInfo.getCharProperty("searchFolderUri");
+ Assert.equal(searchScope, "mailbox://nobody@Local%20Folders/Inbox");
+
+ let tagsFolder =
+ localAccountUtils.incomingServer.rootMsgFolder.getChildNamed("$label1");
+ Assert.equal(
+ tagsFolder.msgDatabase.dBFolderInfo.getCharProperty("searchFolderUri"),
+ "*"
+ );
+ Assert.equal(
+ tagsFolder.msgDatabase.dBFolderInfo.getCharProperty("searchStr"),
+ "AND (tag,contains,$label1)"
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_mailServices.js b/comm/mailnews/base/test/unit/test_mailServices.js
new file mode 100644
index 0000000000..1c8299b4f5
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_mailServices.js
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests for the MailServices module.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_task(function test_services() {
+ function check_service(service, serviceInterface) {
+ Assert.ok(
+ service in MailServices,
+ `${service} should be a member of MailServices`
+ );
+ Assert.ok(
+ MailServices[service] instanceof serviceInterface,
+ `MailServices.${service} should implement Ci.${serviceInterface.name}`
+ );
+ }
+
+ check_service("mailSession", Ci.nsIMsgMailSession);
+ check_service("accounts", Ci.nsIMsgAccountManager);
+ check_service("pop3", Ci.nsIPop3Service);
+ check_service("imap", Ci.nsIImapService);
+ check_service("nntp", Ci.nsINntpService);
+ check_service("smtp", Ci.nsISmtpService);
+ check_service("compose", Ci.nsIMsgComposeService);
+ check_service("ab", Ci.nsIAbManager);
+ check_service("copy", Ci.nsIMsgCopyService);
+ check_service("mfn", Ci.nsIMsgFolderNotificationService);
+ check_service("headerParser", Ci.nsIMsgHeaderParser);
+ check_service("mimeConverter", Ci.nsIMimeConverter);
+ check_service("tags", Ci.nsIMsgTagService);
+ check_service("filters", Ci.nsIMsgFilterService);
+ check_service("junk", Ci.nsIJunkMailPlugin);
+});
+
+add_task(function test_message_services() {
+ function check_message_service(uri) {
+ let service = MailServices.messageServiceFromURI(uri);
+ Assert.ok(
+ service instanceof Ci.nsIMsgMessageService,
+ `message service for ${uri.substring(
+ 0,
+ uri.indexOf(":")
+ )} URIs should exist`
+ );
+ }
+
+ check_message_service("file://it.does.not.matter/");
+ check_message_service("imap://it.does.not.matter/");
+ check_message_service("imap-message://it.does.not.matter/");
+ check_message_service("mailbox://it.does.not.matter/");
+ check_message_service("mailbox-message://it.does.not.matter/");
+ check_message_service("news://it.does.not.matter/");
+ check_message_service("news-message://it.does.not.matter/");
+
+ Assert.throws(
+ () => MailServices.messageServiceFromURI("fake://not.going.to.work/"),
+ () => true, // Accept any exception.
+ "message service for other URIs should not exist"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_mailstoreConverter.js b/comm/mailnews/base/test/unit/test_mailstoreConverter.js
new file mode 100644
index 0000000000..f440b9c8cd
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_mailstoreConverter.js
@@ -0,0 +1,376 @@
+/* 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 { convertMailStoreTo } = ChromeUtils.import(
+ "resource:///modules/mailstoreConverter.jsm"
+);
+
+Services.prefs.setCharPref(
+ "mail.serverDefaultStoreContractID",
+ "@mozilla.org/msgstore/berkeleystore;1"
+);
+
+// Test data for round-trip test.
+let testEmails = [
+ // Base64 encoded bodies.
+ "../../../data/01-plaintext.eml",
+ "../../../data/02-plaintext+attachment.eml",
+ "../../../data/03-HTML.eml",
+ "../../../data/04-HTML+attachment.eml",
+ "../../../data/05-HTML+embedded-image.eml",
+ "../../../data/06-plaintext+HMTL.eml",
+ "../../../data/07-plaintext+(HTML+embedded-image).eml",
+ "../../../data/08-plaintext+HTML+attachment.eml",
+ "../../../data/09-(HTML+embedded-image)+attachment.eml",
+ "../../../data/10-plaintext+(HTML+embedded-image)+attachment.eml",
+
+ // Bodies with non-ASCII characters in UTF-8 and other charsets.
+ "../../../data/11-plaintext.eml",
+ "../../../data/12-plaintext+attachment.eml", // using ISO-8859-7 (Greek)
+ "../../../data/13-HTML.eml",
+ "../../../data/14-HTML+attachment.eml",
+ "../../../data/15-HTML+embedded-image.eml",
+ "../../../data/16-plaintext+HMTL.eml", // text part is base64 encoded
+ "../../../data/17-plaintext+(HTML+embedded-image).eml", // HTML part is base64 encoded
+ "../../../data/18-plaintext+HTML+attachment.eml",
+ "../../../data/19-(HTML+embedded-image)+attachment.eml",
+ "../../../data/20-plaintext+(HTML+embedded-image)+attachment.eml", // using windows-1252
+
+ // Bodies with non-ASCII characters in UTF-8 and other charsets, all encoded with quoted printable.
+ "../../../data/21-plaintext.eml",
+ "../../../data/22-plaintext+attachment.eml", // using ISO-8859-7 (Greek)
+ "../../../data/23-HTML.eml",
+ "../../../data/24-HTML+attachment.eml",
+ "../../../data/25-HTML+embedded-image.eml",
+ "../../../data/26-plaintext+HMTL.eml", // text part is base64 encoded
+ "../../../data/27-plaintext+(HTML+embedded-image).eml", // HTML part is base64 encoded
+ "../../../data/28-plaintext+HTML+attachment.eml",
+ "../../../data/29-(HTML+embedded-image)+attachment.eml",
+ "../../../data/30-plaintext+(HTML+embedded-image)+attachment.eml", // using windows-1252
+];
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ add_task(async function () {
+ await doMboxTest("test1", "../../../data/mbox_modern", 2);
+ await doMboxTest("test2", "../../../data/mbox_mboxrd", 2);
+ await doMboxTest("test3", "../../../data/mbox_unquoted", 2);
+ await roundTripTest();
+ // Ideas for more tests:
+ // - check a really big mbox
+ // - check with really huge message (larger than one chunk)
+ // - check mbox with "From " line on chunk boundary
+ // - add tests for maildir->mbox conversion
+ // - check that conversions preserve message body (ie that the
+ // "From " line escaping scheme is reversible)
+ });
+
+ run_next_test();
+}
+
+/**
+ * Helper to create a server, account and inbox, and install an
+ * mbox file.
+ *
+ * @param {string} srvName - A unique server name to use for the test.
+ * @param {string} mboxFilename - mbox file to install and convert.
+ * @returns {nsIMsgIncomingServer} a server.
+ */
+function setupServer(srvName, mboxFilename) {
+ // {nsIMsgIncomingServer} pop server for the test.
+ let server = MailServices.accounts.createIncomingServer(
+ srvName,
+ "localhost",
+ "pop3"
+ );
+ let account = MailServices.accounts.createAccount();
+ account.incomingServer = server;
+ server.QueryInterface(Ci.nsIPop3IncomingServer);
+ server.valid = true;
+
+ let inbox = account.incomingServer.rootFolder.getFolderWithFlags(
+ Ci.nsMsgFolderFlags.Inbox
+ );
+
+ // install the mbox file
+ let mboxFile = do_get_file(mboxFilename);
+ mboxFile.copyTo(inbox.filePath.parent, inbox.filePath.leafName);
+
+ // TODO: is there some way to make folder rescan the mbox?
+ // We don't need it for this, but would be nice to do things properly.
+ return server;
+}
+
+/**
+ * Perform an mbox->maildir conversion test.
+ *
+ * @param {string} srvName - A unique server name to use for the test.
+ * @param {string} mboxFilename - mbox file to install and convert.
+ * @param {number} expectCnt - Number of messages expected.
+ * @returns {nsIMsgIncomingServer} a server.
+ */
+async function doMboxTest(srvName, mboxFilename, expectCnt) {
+ // set up an account+server+inbox and copy in the test mbox file
+ let server = setupServer(srvName, mboxFilename);
+
+ let mailstoreContractId = Services.prefs.getCharPref(
+ "mail.server." + server.key + ".storeContractID"
+ );
+
+ await convertMailStoreTo(mailstoreContractId, server, new EventTarget());
+
+ // Converted. Now find resulting Inbox/cur directory so
+ // we can count the messages there.
+
+ let inbox = server.rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Inbox);
+ // NOTE: the conversion updates the path of the root folder,
+ // but _not_ the path of the inbox...
+ // Ideally, we'd just use inbox.filePath here, but
+ // instead we'll have compose the path manually.
+
+ let curDir = server.rootFolder.filePath;
+ curDir.append(inbox.filePath.leafName);
+ curDir.append("cur");
+
+ // Sanity check.
+ Assert.ok(curDir.isDirectory(), "'cur' directory created");
+
+ // Check number of messages in Inbox/cur is what we expect.
+ let cnt = [...curDir.directoryEntries].length;
+
+ Assert.equal(
+ cnt,
+ expectCnt,
+ "expected number of messages (" + mboxFilename + ")"
+ );
+}
+
+/**
+ * Create a temporary directory. The caller is responsible for deleting it.
+ *
+ * @param {string} prefix - Generated dir name will be of the form:
+ * "<prefix><random_sequence>".
+ * @returns {string} full path of new directory.
+ */
+async function tempDir(prefix) {
+ if (!prefix) {
+ prefix = "";
+ }
+ let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile).path;
+ while (true) {
+ let name = prefix + Math.floor(Math.random() * 0xffffffff).toString(16);
+ let fullPath = PathUtils.join(tmpDir, name);
+ try {
+ await IOUtils.makeDirectory(fullPath, { ignoreExisting: false });
+ return fullPath;
+ } catch (e) {
+ // If directory already exists, try another name. Else bail out.
+ if (
+ !(DOMException.isInstance(e) && e.name === "NoModificationAllowedError")
+ ) {
+ throw e;
+ }
+ }
+ }
+}
+
+/**
+ * Test that messages survive unscathed in a roundtrip conversion,
+ * maildir -> mbox -> maildir.
+ * The final mailbox should have an identical set of files to the initial one,
+ * albeit with different filenames.
+ * Purely filesystem based.
+ *
+ * Would be nice to do a mbox->maildir->mbox roundtrip too, but that'd involve
+ * parsing the mbox files to compare them (can't just compare mbox files because
+ * message order and "From " lines can change).
+ */
+async function roundTripTest() {
+ // Set up initial maildir structure
+ let initialRoot = await tempDir("initial");
+
+ let inbox = PathUtils.join(initialRoot, "INBOX");
+ await IOUtils.makeDirectory(inbox);
+ // Create a couple of subdirs under INBOX
+ let subdir = PathUtils.join(initialRoot, "INBOX.sbd");
+ await IOUtils.makeDirectory(subdir);
+ let foodir = PathUtils.join(subdir, "foo");
+ await IOUtils.makeDirectory(foodir);
+ let bardir = PathUtils.join(subdir, "bar");
+ await IOUtils.makeDirectory(bardir);
+
+ // Populate all the folders with some test emails.
+ const absolutePaths = testEmails.map(path => do_get_file(path).path);
+ await populateMaildir(inbox, absolutePaths);
+ await populateMaildir(foodir, absolutePaths);
+ await populateMaildir(bardir, absolutePaths);
+
+ // Add a pick of "special" files, which should survive the trip verbatim.
+ for (let special of ["filterlog.html", "feeds.json", "rules.dat"]) {
+ let f = PathUtils.join(initialRoot, special);
+ await IOUtils.writeUTF8(f, f); // Use the filename for content.
+ }
+
+ // Create root dirs for intermediate and final result.
+ let mboxRoot = await tempDir("mbox");
+ let finalRoot = await tempDir("final");
+
+ // Convert: maildir -> mbox -> maildir
+ await doConvert("maildir", initialRoot, "mbox", mboxRoot);
+ await doConvert("mbox", mboxRoot, "maildir", finalRoot);
+
+ // compare results - use checksums, because filenames will differ.
+ await recursiveMaildirCompare(initialRoot, finalRoot);
+}
+
+/**
+ * Helper to adapt the callbacks from converterWorker into a promise.
+ *
+ * @param {string} srcType - type of source ("maildir", "mbox")
+ * @param {string} srcRoot - root directory containing the src folders.
+ * @param {string} destType - type of destination ("maildir", "mbox")
+ * @param {string} destRoot - root directory to place converted store.
+ * @returns {Promise} resolved when when conversion is complete.
+ */
+function doConvert(srcType, srcRoot, destType, destRoot) {
+ return new Promise(function (resolve, reject) {
+ let worker = new ChromeWorker("resource:///modules/converterWorker.js");
+ worker.addEventListener("message", function (ev) {
+ if (ev.data.msg == "success") {
+ resolve();
+ }
+ });
+ worker.addEventListener("error", function (ev) {
+ reject(ev.message);
+ });
+ // Go.
+ worker.postMessage({
+ srcType,
+ destType,
+ srcRoot,
+ destRoot,
+ });
+ });
+}
+
+/**
+ * Copy a list of email files (.eml) files into a maildir, creating "cur"
+ * and "tmp" subdirs if required.
+ *
+ * @param {string} maildir - Path to the maildir directory.
+ * @param {Array<string>} emailFiles - paths of source .eml files to copy.
+ */
+async function populateMaildir(maildir, emailFiles) {
+ let cur = PathUtils.join(maildir, "cur");
+ await IOUtils.makeDirectory(cur);
+ await IOUtils.makeDirectory(PathUtils.join(maildir, "tmp"));
+
+ // Normally maildir files would have a name derived from their msg-id field,
+ // but here we'll just use a timestamp-based one to save parsing them.
+ let ident = Date.now();
+ for (let src of emailFiles) {
+ let dest = PathUtils.join(cur, ident.toString() + ".eml");
+ ident += 1;
+ await IOUtils.copy(src, dest);
+ }
+}
+
+/*
+ * List files in a directory (excludes subdirectories).
+ *
+ * @param {String} dirPath - Full path of directory.
+ * @returns {Array<String} full paths of the files.
+ */
+async function listFiles(dirPath) {
+ let files = [];
+ // Note: IOUtils has no dir iterator at time of writing.
+ for (const path of await IOUtils.getChildren(dirPath)) {
+ let fileInfo = await IOUtils.stat(path);
+ if (fileInfo.type !== "directory") {
+ files.push(path);
+ }
+ }
+ return files;
+}
+
+/*
+ * Calculate md5 checksum for a file.
+ *
+ * @param {String} fileName - Full path to file.
+ * @returns {String} checksum of the file contents.
+ */
+async function md5Sum(fileName) {
+ let md5 = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
+ md5.init(Ci.nsICryptoHash.MD5);
+ let raw = await IOUtils.read(fileName);
+ md5.update(raw, raw.byteLength);
+ return md5.finish(true);
+}
+
+/**
+ * Compare all maildir directories in two directory trees.
+ * The comparison is per-maildir, by looking at the checksums of their emails.
+ * Asserts a test fail if any differences are found.
+ *
+ * @param {string} rootA - path to root of maildir store A.
+ * @param {string} rootB - path to root of maildir store B.
+ */
+async function recursiveMaildirCompare(rootA, rootB) {
+ let subdirs = [];
+ let maildirs = [];
+ let otherFiles = [];
+ for (let path of await IOUtils.getChildren(rootA)) {
+ let stat = await IOUtils.stat(path);
+ let name = PathUtils.filename(path);
+ if (stat.type === "directory") {
+ if (name.endsWith(".sbd")) {
+ subdirs.push(name);
+ } else {
+ // Assume all other dirs are maildirs.
+ maildirs.push(name);
+ }
+ } else {
+ otherFiles.push(name);
+ }
+ }
+
+ // Compare the maildirs we found here.
+ let md5DirContents = async function (dirPath) {
+ let checksums = [];
+ for (let f of await listFiles(dirPath)) {
+ checksums.push(await md5Sum(f));
+ }
+ return checksums;
+ };
+
+ for (let name of maildirs) {
+ let checksumsA = await md5DirContents(PathUtils.join(rootA, name, "cur"));
+ let checksumsB = await md5DirContents(PathUtils.join(rootB, name, "cur"));
+
+ checksumsA.sort();
+ checksumsB.sort();
+ let match = checksumsA.length == checksumsB.length;
+ for (let i = 0; match && i < checksumsA.length; i++) {
+ match = checksumsA[i] == checksumsB[i];
+ }
+ Assert.ok(match, "roundtrip preserves messages in maildir " + name);
+ }
+
+ // Make sure any "special" files survived the trip intact.
+ for (let name of otherFiles) {
+ let checksumA = await md5Sum(PathUtils.join(rootA, name));
+ let pathB = PathUtils.join(rootB, name);
+ let checksumB = (await IOUtils.exists(pathB)) ? await md5Sum(pathB) : null;
+ Assert.equal(checksumA, checksumB, "roundtrip preserves " + name);
+ }
+
+ // Recurse down into .sbd dirs.
+ for (let name of subdirs) {
+ await recursiveMaildirCompare(
+ PathUtils.join(rootA, name),
+ PathUtils.join(rootB, name)
+ );
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_mimemaltdetach.js b/comm/mailnews/base/test/unit/test_mimemaltdetach.js
new file mode 100644
index 0000000000..1f90566726
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_mimemaltdetach.js
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests nsIMessenger's detachAttachmentsWOPrompts of Mime multi-part
+ * alternative messages.
+ */
+
+var { MsgHdrToMimeMessage } = ChromeUtils.import(
+ "resource:///modules/gloda/MimeMessage.jsm"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+function SaveAttachmentCallback() {
+ this.attachments = null;
+ this._promise = new Promise((resolve, reject) => {
+ this._resolve = resolve;
+ this._reject = reject;
+ });
+}
+
+SaveAttachmentCallback.prototype = {
+ callback: function saveAttachmentCallback_callback(aMsgHdr, aMimeMessage) {
+ this.attachments = aMimeMessage.allAttachments;
+ this._resolve();
+ },
+ get promise() {
+ return this._promise;
+ },
+};
+var gCallbackObject = new SaveAttachmentCallback();
+
+add_setup(async function () {
+ if (!localAccountUtils.inboxFolder) {
+ localAccountUtils.loadLocalMailAccount();
+ }
+});
+
+add_task(async function startCopy() {
+ // Get a message into the local filestore.
+ let mailFile = do_get_file("../../../data/multipartmalt-detach");
+ let listener = new PromiseTestUtils.PromiseCopyListener();
+ MailServices.copy.copyFileMessage(
+ mailFile,
+ localAccountUtils.inboxFolder,
+ null,
+ false,
+ 0,
+ "",
+ listener,
+ null
+ );
+ await listener.promise;
+});
+
+// process the message through mime
+add_task(async function startMime() {
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ MsgHdrToMimeMessage(
+ msgHdr,
+ gCallbackObject,
+ gCallbackObject.callback,
+ true // allowDownload
+ );
+
+ await gCallbackObject.promise;
+});
+
+// detach any found attachments
+add_task(async function startDetach() {
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+ let msgURI = msgHdr.folder.generateMessageURI(msgHdr.messageKey);
+
+ let messenger = Cc["@mozilla.org/messenger;1"].createInstance(
+ Ci.nsIMessenger
+ );
+ let attachment = gCallbackObject.attachments[0];
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+
+ messenger.detachAttachmentsWOPrompts(
+ do_get_profile(),
+ [attachment.contentType],
+ [attachment.url],
+ [attachment.name],
+ [msgURI],
+ listener
+ );
+ await listener.promise;
+});
+
+// test that the detachment was successful
+add_task(async function testDetach() {
+ // The message contained a file "head_update.txt" which should
+ // now exist in the profile directory.
+ let checkFile = do_get_profile().clone();
+ checkFile.append("head_update.txt");
+ Assert.ok(checkFile.exists());
+ Assert.ok(checkFile.fileSize > 0);
+
+ // The message should now have a detached attachment. Read the message,
+ // and search for "AttachmentDetached" which is added on detachment.
+
+ // Get the message header
+ let msgHdr = mailTestUtils.firstMsgHdr(localAccountUtils.inboxFolder);
+
+ let messageContent = await getContentFromMessage(msgHdr);
+ Assert.ok(messageContent.includes("AttachmentDetached"));
+ // Make sure the body survived the detach.
+ Assert.ok(messageContent.includes("body hello"));
+});
+
+/**
+ * Get the full message content.
+ *
+ * @param {nsIMsgDBHdr} aMsgHdr - Message object whose text body will be read.
+ * @returns {Promise<string>} full message contents.
+ */
+function getContentFromMessage(aMsgHdr) {
+ let msgFolder = aMsgHdr.folder;
+ let msgUri = msgFolder.getUriForMsg(aMsgHdr);
+
+ return new Promise((resolve, reject) => {
+ let streamListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ sis: Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ ),
+ content: "",
+ onDataAvailable(request, inputStream, offset, count) {
+ this.sis.init(inputStream);
+ this.content += this.sis.read(count);
+ },
+ onStartRequest(request) {},
+ onStopRequest(request, statusCode) {
+ this.sis.close();
+ if (Components.isSuccessCode(statusCode)) {
+ resolve(this.content);
+ } else {
+ reject(new Error(statusCode));
+ }
+ },
+ };
+ MailServices.messageServiceFromURI(msgUri).streamMessage(
+ msgUri,
+ streamListener,
+ null,
+ null,
+ false,
+ "",
+ false
+ );
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_newMailNotification.js b/comm/mailnews/base/test/unit/test_newMailNotification.js
new file mode 100644
index 0000000000..d6e111d86a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_newMailNotification.js
@@ -0,0 +1,203 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Tests for platform-independent code to count new and unread messages and pass the
+ * information to platform-specific notification modules */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Register listener for a particular event, make sure it shows up in the right lists
+ * of listeners (and not the wrong ones) and doesn't show up after being removed
+ */
+add_test(function testListeners() {
+ let notif = MailServices.newMailNotification.wrappedJSObject;
+ let listener = { onCountChanged: () => {} };
+
+ notif.addListener(listener, Ci.mozINewMailNotificationService.count);
+ let list = notif.listenersForFlag(Ci.mozINewMailNotificationService.count);
+ Assert.equal(list.length, 1);
+ Assert.equal(list[0], listener);
+
+ let newlist = notif.listenersForFlag(
+ Ci.mozINewMailNotificationService.messages
+ );
+ Assert.equal(newlist.length, 0);
+
+ notif.removeListener(listener);
+ list = notif.listenersForFlag(Ci.mozINewMailNotificationService.count);
+ Assert.equal(list.length, 0);
+
+ run_next_test();
+});
+
+/*
+ * Register a listener for two types and another for one type, make sure they show up,
+ * remove one and make sure the other stays put
+ */
+add_test(function testMultiListeners() {
+ let notif = MailServices.newMailNotification.wrappedJSObject;
+ let l1 = { onCountChanged: () => {} };
+ let l2 = { b: 2 };
+
+ notif.addListener(
+ l1,
+ Ci.mozINewMailNotificationService.count |
+ Ci.mozINewMailNotificationService.messages
+ );
+ // do_check_eq(notif._listeners.length, 1);
+ notif.addListener(l2, Ci.mozINewMailNotificationService.messages);
+ // do_check_eq(notif._listeners.length, 2);
+ let list = notif.listenersForFlag(Ci.mozINewMailNotificationService.count);
+ Assert.equal(list.length, 1);
+ Assert.equal(list[0], l1);
+
+ let newlist = notif.listenersForFlag(
+ Ci.mozINewMailNotificationService.messages
+ );
+ Assert.equal(newlist.length, 2);
+
+ notif.removeListener(l1);
+ list = notif.listenersForFlag(Ci.mozINewMailNotificationService.count);
+ Assert.equal(list.length, 0);
+ newlist = notif.listenersForFlag(Ci.mozINewMailNotificationService.messages);
+ Assert.equal(newlist.length, 1);
+ Assert.equal(newlist[0], l2);
+ notif.removeListener(l2);
+
+ run_next_test();
+});
+
+/* Make sure we get a notification call when the unread count changes on an Inbox */
+add_test(function testNotifyInbox() {
+ let notified = false;
+ let count = 0;
+ let mockListener = {
+ onCountChanged: function TNU_onCountChanged(updatedCount) {
+ notified = true;
+ count = updatedCount;
+ },
+ };
+ let folder = {
+ URI: "Test Inbox",
+ flags: Ci.nsMsgFolderFlags.Mail | Ci.nsMsgFolderFlags.Inbox,
+ };
+
+ const notificationService = MailServices.newMailNotification.wrappedJSObject;
+
+ // Set up the notification service to start with a non-zero unread count to
+ // verify this value is correctly passed to new listeners. Do this before any
+ // listeners are added.
+ const startCount = 3;
+ notificationService.unreadCount = startCount;
+
+ // Add a listener for count updates.
+ notificationService.addListener(
+ mockListener,
+ Ci.mozINewMailNotificationService.count
+ );
+
+ // Verify that a new listener is notified of the current count.
+ Assert.ok(notified, "New listeners should be notified of count when added.");
+ Assert.equal(
+ count,
+ startCount,
+ "New listener notification should contain the current unread count."
+ );
+
+ // Verify that listeners are notified of subsequent changes.
+ notified = false;
+ const updatedInboxCount = 5;
+ notificationService.onFolderIntPropertyChanged(
+ folder,
+ "TotalUnreadMessages",
+ startCount,
+ updatedInboxCount
+ );
+ Assert.ok(
+ notified,
+ "Listeners should be notified of changes in inbox unread count."
+ );
+ Assert.equal(
+ count,
+ updatedInboxCount,
+ "Notification should contain updated inbox unread count."
+ );
+
+ // Sanity check.
+ Assert.ok(
+ Services.prefs.getBoolPref("mail.notification.count.inbox_only", false),
+ "`inbox_only` pref should be true for test."
+ );
+
+ // Verify that listeners are not notified of changes outside of the inbox.
+ let nonInbox = {
+ URI: "Test Non-Inbox",
+ flags: Ci.nsMsgFolderFlags.Mail,
+ };
+ notified = false;
+ notificationService.onFolderIntPropertyChanged(
+ nonInbox,
+ "TotalUnreadMessages",
+ 0,
+ 2
+ );
+ Assert.ok(
+ !notified,
+ "Listeners should not be notified of changes in unread count outside of inbox by default."
+ );
+ Assert.equal(
+ count,
+ updatedInboxCount,
+ "Total unread message count should not have changed."
+ );
+
+ // Verify that, when `inbox_only` is false, unread messages outside of the
+ // inbox are counted.
+ Services.prefs.setBoolPref("mail.notification.count.inbox_only", false);
+ notified = false;
+ const updatedNonInboxCount = 2;
+ const updatedTotalCount = updatedInboxCount + updatedNonInboxCount;
+ notificationService.onFolderIntPropertyChanged(
+ nonInbox,
+ "TotalUnreadMessages",
+ 0,
+ updatedNonInboxCount
+ );
+ Assert.ok(
+ notified,
+ "Listeners should be notified of changes in unread count outside of inbox when pref is set."
+ );
+ Assert.equal(
+ count,
+ updatedTotalCount,
+ "Notification should contain total unread count for all counted folders."
+ );
+
+ // Verify that listeners are never informed of updates in special folders.
+ let special = {
+ URI: "Test Special",
+ flags: Ci.nsMsgFolderFlags.Mail | Ci.nsMsgFolderFlags.Junk,
+ };
+ notified = false;
+ notificationService.onFolderIntPropertyChanged(
+ special,
+ "TotalUnreadMessages",
+ 0,
+ 2
+ );
+ Assert.ok(
+ !notified,
+ "Listeners should not be notified of changes in special folder unread count."
+ );
+ Assert.equal(
+ count,
+ updatedTotalCount,
+ "Total unread message count should not have changed."
+ );
+
+ run_next_test();
+});
diff --git a/comm/mailnews/base/test/unit/test_nsIFolderListener.js b/comm/mailnews/base/test/unit/test_nsIFolderListener.js
new file mode 100644
index 0000000000..aa1b19ecfb
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIFolderListener.js
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Test that adding nsIFolderListener in js does not cause any crash.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var folderListener = {
+ onFolderAdded() {},
+ onMessageAdded() {},
+ onFolderRemoved() {},
+ onMessageRemoved() {},
+ onFolderPropertyChanged() {},
+ onFolderIntPropertyChanged() {},
+ onFolderBoolPropertyChanged() {},
+ onFolderUnicharPropertyChanged() {},
+ onFolderPropertyFlagChanged() {},
+ onFolderEvent() {},
+};
+
+var targetFolder;
+var messageInjection;
+
+add_setup(async function () {
+ let msgGen = new MessageGenerator();
+ messageInjection = new MessageInjection({ mode: "local" }, msgGen);
+
+ targetFolder = await messageInjection.makeEmptyFolder();
+ targetFolder.AddFolderListener(folderListener);
+ registerCleanupFunction(function () {
+ targetFolder.RemoveFolderListener(folderListener);
+ });
+});
+
+add_task(async function create_new_message() {
+ await messageInjection.makeNewSetsInFolders([targetFolder], [{ count: 1 }]);
+});
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgContentPolicy.js b/comm/mailnews/base/test/unit/test_nsIMsgContentPolicy.js
new file mode 100644
index 0000000000..de49d5b7eb
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgContentPolicy.js
@@ -0,0 +1,68 @@
+/* -*- mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsIMsgContentPolicy to check we could add/remove customized protocol to
+ * nsMsgContentPolicy.
+ */
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+function makeURI(aURL) {
+ return Services.io.newURI(aURL);
+}
+
+function run_test() {
+ var content_policy = Cc["@mozilla.org/messenger/content-policy;1"].getService(
+ Ci.nsIContentPolicy
+ );
+
+ Assert.ok(content_policy);
+
+ var msg_content_policy = content_policy.QueryInterface(
+ Ci.nsIMsgContentPolicy
+ );
+
+ Assert.ok(msg_content_policy);
+
+ var req_uri = makeURI("custom-scheme://custom_url/1.emal");
+ Assert.ok(req_uri);
+
+ var content_uri = makeURI("custom-scheme://custom_content_url/1.jsp");
+ Assert.ok(content_uri);
+
+ let tmpChannel = NetUtil.newChannel({
+ uri: content_uri,
+ // Needs one of 'loadingNode', 'loadingPrincipal' or 'loadUsingSystemPrincipal' which we don't have.
+ // Even with `loadUsingSystemPrincipal: true` this fails with "unknown protocol". See bug 1446587.
+ securityFlags: Ci.nsILoadInfo.SEC_ONLY_FOR_EXPLICIT_CONTENTSEC_CHECK,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_IMAGE,
+ });
+ let tmpLoadInfo = tmpChannel.loadInfo;
+
+ var decision = content_policy.shouldLoad(
+ content_uri,
+ tmpLoadInfo,
+ "img/jpeg"
+ );
+ Assert.notEqual(
+ decision,
+ Ci.nsIContentPolicy.ACCEPT,
+ "customized protocol should not load"
+ );
+
+ msg_content_policy.addExposedProtocol("custom-scheme");
+
+ decision = content_policy.shouldLoad(content_uri, tmpLoadInfo, "img/jpeg");
+ Assert.equal(
+ decision,
+ Ci.nsIContentPolicy.ACCEPT,
+ "customized protocol should load"
+ );
+
+ msg_content_policy.removeExposedProtocol("custom-scheme");
+
+ decision = content_policy.shouldLoad(content_uri, tmpLoadInfo, "img/jpeg");
+ Assert.notEqual(
+ decision,
+ Ci.nsIContentPolicy.ACCEPT,
+ "customized protocol should not load"
+ );
+}
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgFolder.js b/comm/mailnews/base/test/unit/test_nsIMsgFolder.js
new file mode 100644
index 0000000000..9cfb57a07c
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgFolder.js
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsIMsgFolder functions.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Create a local mail account (we need this first)
+ MailServices.accounts.createLocalMailAccount();
+
+ // Get the account
+ let account = MailServices.accounts.accounts[0];
+
+ // Get the root folder
+ var root = account.incomingServer.rootFolder;
+
+ // Add a sub folder to ensure that we have some folders created
+ root.createSubfolder("folder1", null);
+
+ // Test - getChildNamed
+
+ var caught = false;
+ try {
+ root.getChildNamed("folder");
+ } catch (e) {
+ caught = true;
+ }
+ Assert.equal(caught, true);
+
+ caught = false;
+ try {
+ root.getChildNamed("Trash1");
+ } catch (e) {
+ caught = true;
+ }
+ Assert.equal(caught, true);
+
+ var folder1 = root.getChildNamed("folder1");
+
+ Assert.notEqual(folder1, folder2);
+ Assert.equal(folder1.prettyName, "folder1");
+
+ var folder2 = root.getChildNamed("FOLDER1");
+
+ Assert.equal(folder1, folder2);
+
+ // Check special folders aren't deletable, and that normal folders are.
+ if (!root.containsChildNamed("Inbox")) {
+ root.createSubfolder("Inbox", null);
+ }
+ var inbox = root.getChildNamed("Inbox");
+ inbox.setFlag(Ci.nsMsgFolderFlags.Inbox);
+ Assert.ok(!inbox.deletable);
+
+ if (!root.containsChildNamed("Drafts")) {
+ root.createSubfolder("Drafts", null);
+ }
+ var drafts = root.getChildNamed("Drafts");
+ drafts.setFlag(Ci.nsMsgFolderFlags.Drafts);
+ Assert.ok(!drafts.deletable);
+
+ if (!root.containsChildNamed("Templates")) {
+ root.createSubfolder("Templates", null);
+ }
+ var templates = root.getChildNamed("Templates");
+ templates.setFlag(Ci.nsMsgFolderFlags.Templates);
+ Assert.ok(!templates.deletable);
+
+ if (!root.containsChildNamed("Sent")) {
+ root.createSubfolder("Sent", null);
+ }
+ var sent = root.getChildNamed("Sent");
+ sent.setFlag(Ci.nsMsgFolderFlags.SentMail);
+ Assert.ok(!sent.deletable);
+
+ if (!root.containsChildNamed("Archives")) {
+ root.createSubfolder("Archives", null);
+ }
+ var archives = root.getChildNamed("Archives");
+ archives.setFlag(Ci.nsMsgFolderFlags.Archive);
+ Assert.ok(!archives.deletable);
+
+ if (!root.containsChildNamed("Trash")) {
+ root.createSubfolder("Trash", null);
+ }
+ var trash = root.getChildNamed("Trash");
+ trash.setFlag(Ci.nsMsgFolderFlags.Trash);
+ Assert.ok(!trash.deletable);
+
+ if (!root.containsChildNamed("Outbox")) {
+ root.createSubfolder("Outbox", null);
+ }
+ var outbox = root.getChildNamed("Outbox");
+ outbox.setFlag(Ci.nsMsgFolderFlags.Queue);
+ Assert.ok(!outbox.deletable);
+
+ // test a normal folder is deletable
+ Assert.ok(folder1.deletable);
+}
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgFolderCache.js b/comm/mailnews/base/test/unit/test_nsIMsgFolderCache.js
new file mode 100644
index 0000000000..9fd9688b53
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgFolderCache.js
@@ -0,0 +1,228 @@
+/* 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/. */
+
+/**
+ * Sanity checks for nsIMsgFolderCache/nsIMsgFolderCacheElement.
+ */
+add_task(function test_basics() {
+ let profileDir = do_get_profile();
+ let jsonFile = profileDir.clone();
+ jsonFile.append("folderCache.json");
+ let legacyFile = profileDir.clone();
+ legacyFile.append("panacea.dat");
+
+ // Create an empty cache object and start poking it.
+ {
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ // Neither of these files exist, and that's fine.
+ Assert.ok(!jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+ cache.init(jsonFile, legacyFile);
+
+ // getCacheElement has to be told to create non-existent keys.
+ Assert.throws(function () {
+ cache.getCacheElement("a/non/existent/key", false);
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ let e1 = cache.getCacheElement("/made/up/path/Inbox", true);
+
+ // Can set, get and modify Int32 values?
+ e1.setCachedInt32("wibble", -1);
+ Assert.equal(e1.getCachedInt32("wibble"), -1);
+ e1.setCachedInt32("wibble", 42);
+ Assert.equal(e1.getCachedInt32("wibble"), 42);
+
+ // Check some allowed conversions from Int32.
+ Assert.equal(e1.getCachedUInt32("wibble"), 42);
+ Assert.equal(e1.getCachedInt64("wibble"), 42);
+ Assert.equal(e1.getCachedString("wibble"), "42");
+
+ // Can set, get and modify UInt32 values?
+ e1.setCachedUInt32("pibble", 0xffffffff);
+ Assert.equal(e1.getCachedUInt32("pibble"), 0xffffffff);
+ e1.setCachedUInt32("pibble", 42);
+ Assert.equal(e1.getCachedUInt32("pibble"), 42);
+
+ // Check some allowed conversions from UInt32.
+ Assert.equal(e1.getCachedInt32("pibble"), 42);
+ Assert.equal(e1.getCachedInt64("pibble"), 42);
+ Assert.equal(e1.getCachedString("pibble"), "42");
+
+ // Can set, get and modify Int64 values?
+ e1.setCachedInt64("foo", 2305843009213694000);
+ Assert.equal(e1.getCachedInt64("foo"), 2305843009213694000);
+ e1.setCachedInt64("foo", -2305843009213694000);
+ Assert.equal(e1.getCachedInt64("foo"), -2305843009213694000);
+ e1.setCachedInt64("foo", 42);
+ Assert.equal(e1.getCachedInt64("foo"), 42);
+
+ // Check some allowed conversions from Int64.
+ Assert.equal(e1.getCachedInt32("foo"), 42);
+ Assert.equal(e1.getCachedUInt32("foo"), 42);
+ Assert.equal(e1.getCachedString("foo"), "42");
+
+ // Can set, get and modify String values?
+ e1.setCachedString("bar", "Before");
+ Assert.equal(e1.getCachedString("bar"), "Before");
+ e1.setCachedString("bar", "After");
+ Assert.equal(e1.getCachedString("bar"), "After");
+ e1.setCachedString("bar", "日本語");
+ Assert.equal(e1.getCachedString("bar"), "日本語");
+
+ // Check some disallowed conversions from String.
+ Assert.throws(function () {
+ e1.getCachedInt32("bar");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedUInt32("bar");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedInt64("bar");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+
+ // Trying to read missing properties is an error.
+ Assert.throws(function () {
+ e1.getCachedInt32("non-existent-property");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedUInt32("non-existent-property");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedInt64("non-existent-property");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+ Assert.throws(function () {
+ e1.getCachedString("non-existent-property");
+ }, /NS_ERROR_NOT_AVAILABLE/);
+
+ // Force a save to jsonFile. The changes we made will have queued up a
+ // cache autosave but we don't want to wait that long. The cache dtor
+ // would also save, but we don't want to second-guess JS garbage
+ // collection here.
+ cache.flush();
+ }
+
+ // Create a new cache object, reload jsonFile and make sure all the expected
+ // values are there.
+ {
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ // jsonFile is there now.
+ Assert.ok(jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+ cache.init(jsonFile, legacyFile);
+ // Make sure all the values we previously set are intact.
+ let e1 = cache.getCacheElement("/made/up/path/Inbox", true);
+ Assert.equal(e1.getCachedInt32("wibble"), 42);
+ Assert.equal(e1.getCachedUInt32("pibble"), 42);
+ Assert.equal(e1.getCachedInt64("foo"), 42);
+ Assert.equal(e1.getCachedString("bar"), "日本語");
+ }
+
+ // clean up for next test
+ jsonFile.remove(false);
+});
+
+add_task(async function test_null_entries() {
+ // Write out a trivial foldercache file with a null value.
+ let data = { "a-folder-key": { foo: null } };
+ let jsonFilename = PathUtils.join(PathUtils.tempDir, "foo.json");
+ await IOUtils.writeJSON(jsonFilename, data);
+
+ // Load it into an msIMsgFolderCache
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ let jsonFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ jsonFile.initWithPath(jsonFilename);
+ let morkFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ morkFile.initWithPath(
+ PathUtils.join(PathUtils.tempDir, "non-existent-file.dat")
+ );
+ cache.init(jsonFile, morkFile);
+
+ //
+ let e1 = cache.getCacheElement("a-folder-key", false);
+
+ // Make sure all accessors convert the null appropriately.
+ Assert.equal(e1.getCachedInt32("foo"), 0);
+ Assert.equal(e1.getCachedUInt32("foo"), 0);
+ Assert.equal(e1.getCachedInt64("foo"), 0);
+ Assert.equal(e1.getCachedString("foo"), "");
+});
+
+/**
+ * Test foldercache migration from mork DB (panacea.dat) to JSON.
+ */
+add_task(async function test_migration() {
+ let profileDir = do_get_profile();
+ let jsonFile = profileDir.clone();
+ jsonFile.append("folderCache.json");
+ let legacyFile = profileDir.clone();
+ legacyFile.append("panacea.dat");
+
+ Assert.ok(!jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+
+ // Install our test legacy file.
+ do_get_file("data/panacea.dat").copyTo(profileDir, legacyFile.leafName);
+
+ // Set up the cache.
+ {
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ cache.init(jsonFile, legacyFile);
+
+ // Migration should have occurred.
+ Assert.ok(jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+
+ // Done with the cache now.
+ }
+
+ // Compare the migrated json to the json we expect.
+ let raw = await IOUtils.readUTF8(jsonFile.path);
+ let got = JSON.parse(raw);
+
+ raw = await IOUtils.readUTF8(do_get_file("data/folderCache.json").path);
+ let expect = JSON.parse(raw);
+
+ Assert.deepEqual(got, expect);
+
+ // clean up for next test
+ jsonFile.remove(false);
+});
+
+/**
+ * Test foldercache migration doesn't crash with a dud panacea.dat.
+ */
+add_task(async function test_bad_pancea_dat() {
+ let profileDir = do_get_profile();
+ let jsonFile = profileDir.clone();
+ jsonFile.append("folderCache.json");
+ let legacyFile = profileDir.clone();
+ legacyFile.append("panacea.dat");
+
+ Assert.ok(!jsonFile.exists());
+ Assert.ok(!legacyFile.exists());
+
+ // Install our bad panacea.dat. It has only the first line - the mork magic
+ // cookie - so it's valid enough for mork to open, but doesn't have
+ // anything the migration is looking for.
+ do_get_file("data/panacea_empty.dat").copyTo(profileDir, legacyFile.leafName);
+
+ // Set up the cache.
+ let cache = Cc["@mozilla.org/messenger/msgFolderCache;1"].createInstance(
+ Ci.nsIMsgFolderCache
+ );
+ // init() returns OK even if migration fails - the show must go on!
+ cache.init(jsonFile, legacyFile);
+
+ // If we get this far, we didn't crash, which is good.
+ // The migration should have left everything as it was.
+ Assert.ok(legacyFile.exists());
+ Assert.ok(!jsonFile.exists());
+});
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgFolderListener.js b/comm/mailnews/base/test/unit/test_nsIMsgFolderListener.js
new file mode 100644
index 0000000000..c0a9b72e64
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgFolderListener.js
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test suite for basic functionality with nsIMsgFolderListeners.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var nsIMFNService = Ci.nsIMsgFolderNotificationService;
+
+var gIndividualFlags = [
+ nsIMFNService.msgAdded,
+ nsIMFNService.msgsClassified,
+ nsIMFNService.msgsJunkStatusChanged,
+ nsIMFNService.msgsDeleted,
+ nsIMFNService.msgsMoveCopyCompleted,
+ nsIMFNService.msgKeyChanged,
+ nsIMFNService.msgUnincorporatedMoved,
+ nsIMFNService.folderAdded,
+ nsIMFNService.folderDeleted,
+ nsIMFNService.folderMoveCopyCompleted,
+ nsIMFNService.folderRenamed,
+ nsIMFNService.folderCompactStart,
+ nsIMFNService.folderCompactFinish,
+ nsIMFNService.folderReindexTriggered,
+];
+
+// Our listener, which captures events.
+function gMFListener() {}
+gMFListener.prototype = {
+ mReceived: 0,
+ mRemoveSelf: false,
+ msgAdded(aMsg) {
+ Assert.equal(this.mReceived & nsIMFNService.msgAdded, 0);
+ this.mReceived |= nsIMFNService.msgAdded;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgsClassified(aMsgs, aJunkProcessed, aTraitProcessed) {
+ Assert.equal(this.mReceived & nsIMFNService.msgsClassified, 0);
+ this.mReceived |= nsIMFNService.msgsClassified;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgsJunkStatusChanged(messages) {
+ Assert.equal(this.mReceived & nsIMFNService.msgsJunkStatusChanged, 0);
+ this.mReceived |= nsIMFNService.msgsJunkStatusChanged;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgsDeleted(aMsgs) {
+ Assert.equal(this.mReceived & nsIMFNService.msgsDeleted, 0);
+ this.mReceived |= nsIMFNService.msgsDeleted;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgsMoveCopyCompleted(aMove, aSrcMsgs, aDestFolder, aDestMsgs) {
+ Assert.equal(this.mReceived & nsIMFNService.msgsMoveCopyCompleted, 0);
+ this.mReceived |= nsIMFNService.msgsMoveCopyCompleted;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgKeyChanged(aOldMsgKey, aNewMsgHdr) {
+ Assert.equal(this.mReceived & nsIMFNService.msgKeyChanged, 0);
+ this.mReceived |= nsIMFNService.msgKeyChanged;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ msgUnincorporatedMoved(srcFolder, msg) {
+ Assert.equal(this.mReceived & nsIMFNService.msgUnincorporatedMoved, 0);
+ this.mReceived |= nsIMFNService.msgUnincorporatedMoved;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderAdded(aFolder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderAdded, 0);
+ this.mReceived |= nsIMFNService.folderAdded;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderDeleted(aFolder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderDeleted, 0);
+ this.mReceived |= nsIMFNService.folderDeleted;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderMoveCopyCompleted, 0);
+ this.mReceived |= nsIMFNService.folderMoveCopyCompleted;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderRenamed(aOrigFolder, aNewFolder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderRenamed, 0);
+ this.mReceived |= nsIMFNService.folderRenamed;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderCompactStart(folder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderCompactStart, 0);
+ this.mReceived |= nsIMFNService.folderCompactStart;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderCompactFinish(folder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderCompactFinish, 0);
+ this.mReceived |= nsIMFNService.folderCompactFinish;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+
+ folderReindexTriggered(folder) {
+ Assert.equal(this.mReceived & nsIMFNService.folderReindexTriggered, 0);
+ this.mReceived |= nsIMFNService.folderReindexTriggered;
+ if (this.mRemoveSelf) {
+ MailServices.mfn.removeListener(this);
+ }
+ },
+};
+
+function NotifyMsgFolderListeners() {
+ MailServices.mfn.notifyMsgAdded(null);
+ MailServices.mfn.notifyMsgsClassified([], null, null);
+ MailServices.mfn.notifyMsgsJunkStatusChanged([]);
+ MailServices.mfn.notifyMsgsDeleted([]);
+ MailServices.mfn.notifyMsgsMoveCopyCompleted(null, [], null, []);
+ MailServices.mfn.notifyMsgKeyChanged(null, null);
+ MailServices.mfn.notifyMsgUnincorporatedMoved(null, null);
+ MailServices.mfn.notifyFolderAdded(null);
+ MailServices.mfn.notifyFolderDeleted(null);
+ MailServices.mfn.notifyFolderMoveCopyCompleted(null, null, null);
+ MailServices.mfn.notifyFolderRenamed(null, null);
+ MailServices.mfn.notifyFolderCompactStart(null);
+ MailServices.mfn.notifyFolderCompactFinish(null);
+ MailServices.mfn.notifyFolderReindexTriggered(null);
+}
+
+function run_test() {
+ // Test: Add listeners
+ var singleListeners = [];
+
+ var addAListener = function (flag) {
+ var listener = new gMFListener();
+ MailServices.mfn.addListener(listener, flag);
+ singleListeners.push(listener);
+ };
+
+ gIndividualFlags.forEach(addAListener);
+
+ // Test: Notify the listeners of all events.
+ NotifyMsgFolderListeners();
+
+ // Test: check whether the correct number of notifications have been received.
+ // Then remove the listeners
+ var checkFlag = function (flag) {
+ var listener = singleListeners.shift();
+ Assert.equal(listener.mReceived, flag);
+ listener.mRemoveSelf = true;
+ listener.mReceived = 0;
+ singleListeners.push(listener);
+ };
+ gIndividualFlags.forEach(checkFlag);
+
+ // We'll do one more set of notifications, and remove ourselves in the middle of them
+ NotifyMsgFolderListeners();
+
+ // Test: all listeners should be removed at this point
+ Assert.ok(!MailServices.mfn.hasListeners);
+
+ // Test: Send notifications again. Check that we don't receive any notifications.
+ singleListeners.forEach(function (listener) {
+ listener.mReceived = 0;
+ });
+
+ NotifyMsgFolderListeners();
+
+ var checkNotReceived = function () {
+ Assert.equal(singleListeners.shift().mReceived, 0);
+ };
+ gIndividualFlags.forEach(checkNotReceived);
+}
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgFolderListenerLocal.js b/comm/mailnews/base/test/unit/test_nsIMsgFolderListenerLocal.js
new file mode 100644
index 0000000000..aadbd75b8a
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgFolderListenerLocal.js
@@ -0,0 +1,444 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test suite for nsIMsgFolderListener events due to local mail folder
+ * operations.
+ *
+ * Currently tested:
+ * - Adding new folders
+ * - Copy messages from files into the db
+ * - Moving and copying one or more messages from one local folder to another
+ * - Moving folders, with and without subfolders
+ * - Renaming folders
+ * - Deleting messages and folders, to trash and from trash (permanently)
+ */
+
+/* import-globals-from ../../../test/resources/msgFolderListenerSetup.js */
+load("../../../resources/msgFolderListenerSetup.js");
+
+// Globals
+var gMsgFile1, gMsgFile2, gMsgFile3;
+var gRootFolder;
+var gLocalFolder2;
+var gLocalFolder3;
+var gLocalTrashFolder;
+
+// storeIn takes a string containing the variable to store the new folder in
+function addFolder(parent, folderName, storeIn) {
+ gExpectedEvents = [
+ [MailServices.mfn.folderAdded, parent, folderName, storeIn],
+ ];
+ // We won't receive a copy listener notification for this
+ gCurrStatus |= kStatus.onStopCopyDone;
+ parent.createSubfolder(folderName, null);
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+/**
+ * This will introduce a new message to the system which will generate an added
+ * notification and subsequently a classification notification. For the
+ * classification because no messages have yet been marked as junk and there
+ * are no traits configured, aJunkProcessed and aTraitProcessed will be false.
+ */
+function copyFileMessage(file, destFolder, isDraftOrTemplate) {
+ copyListener.mFolderStoredIn = destFolder;
+ gExpectedEvents = [
+ [MailServices.mfn.msgAdded, gHdrsReceived],
+ [MailServices.mfn.msgsClassified, gHdrsReceived, false, false],
+ ];
+ MailServices.copy.copyFileMessage(
+ file,
+ destFolder,
+ null,
+ isDraftOrTemplate,
+ 0,
+ "",
+ copyListener,
+ null
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function copyMessages(items, isMove, srcFolder, destFolder) {
+ gExpectedEvents = [
+ [MailServices.mfn.msgsMoveCopyCompleted, isMove, items, destFolder, true],
+ ];
+ MailServices.copy.copyMessages(
+ srcFolder,
+ items,
+ destFolder,
+ isMove,
+ copyListener,
+ null,
+ true
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function copyFolder(srcFolder, isMove, destFolder) {
+ gExpectedEvents = [
+ [MailServices.mfn.folderMoveCopyCompleted, isMove, [srcFolder], destFolder],
+ ];
+ MailServices.copy.copyFolder(
+ srcFolder,
+ destFolder,
+ isMove,
+ copyListener,
+ null
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function deleteMessages(srcFolder, items, deleteStorage, isMove) {
+ // We should only get the delete notification only if we are not moving, and are deleting from
+ // the storage/trash. We should get only the move/copy notification if we aren't.
+ var isTrashFolder = srcFolder.getFlag(Ci.nsMsgFolderFlags.Trash);
+ if (!isMove && (deleteStorage || isTrashFolder)) {
+ // We won't be getting any OnStopCopy notification in this case
+ gCurrStatus = kStatus.onStopCopyDone;
+ gExpectedEvents = [[MailServices.mfn.msgsDeleted, items]];
+ } else {
+ // We have to be getting a move notification, even if isMove is false
+ gExpectedEvents = [
+ [
+ MailServices.mfn.msgsMoveCopyCompleted,
+ true,
+ items,
+ gLocalTrashFolder,
+ true,
+ ],
+ ];
+ }
+
+ srcFolder.deleteMessages(
+ items,
+ null,
+ deleteStorage,
+ isMove,
+ copyListener,
+ true
+ );
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function renameFolder(folder, newName) {
+ gExpectedEvents = [[MailServices.mfn.folderRenamed, [folder], newName]];
+ gCurrStatus = kStatus.onStopCopyDone;
+ folder.rename(newName, null);
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function deleteFolder(folder, child) {
+ // We won't be getting any OnStopCopy notification at all
+ // XXX delete to trash should get one, but we'll need to pass the listener
+ // somehow to deleteSelf
+ gCurrStatus = kStatus.onStopCopyDone;
+ // If ancestor is trash, expect a folderDeleted, otherwise expect
+ // a folderMoveCopyCompleted.
+ if (gLocalTrashFolder.isAncestorOf(folder)) {
+ if (child) {
+ gExpectedEvents = [
+ [MailServices.mfn.folderDeleted, [child]],
+ [MailServices.mfn.folderDeleted, [folder]],
+ ];
+ } else {
+ gExpectedEvents = [[MailServices.mfn.folderDeleted, [folder]]];
+ }
+ } else {
+ gExpectedEvents = [
+ [
+ MailServices.mfn.folderMoveCopyCompleted,
+ true,
+ [folder],
+ gLocalTrashFolder,
+ ],
+ ];
+ }
+
+ folder.deleteSelf(null);
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+function compactFolder(folder) {
+ gExpectedEvents = [
+ [MailServices.mfn.folderCompactStart, folder],
+ [MailServices.mfn.folderCompactFinish, folder],
+ ];
+ // We won't receive a copy listener notification for this
+ gCurrStatus |= kStatus.onStopCopyDone;
+ folder.compact(null, null);
+ gCurrStatus |= kStatus.functionCallDone;
+ if (gCurrStatus == kStatus.everythingDone) {
+ resetStatusAndProceed();
+ }
+}
+
+/*
+ * TESTS
+ */
+
+// Beware before commenting out a test -- later tests might just depend on earlier ones
+var gTestArray = [
+ // Adding folders
+ // Create another folder to move and copy messages around, and force initialization.
+ function addFolder1() {
+ addFolder(gRootFolder, "folder2", function (folder) {
+ gLocalFolder2 = folder;
+ });
+ },
+ // Create a third folder for more testing.
+ function addFolder2() {
+ addFolder(gRootFolder, "folder3", function (folder) {
+ gLocalFolder3 = folder;
+ });
+ },
+ // Folder structure is now
+ // Inbox
+ // Trash
+ // folder2
+ // folder3
+ // Copying messages from files
+ function testCopyFileMessage1() {
+ copyFileMessage(gMsgFile1, localAccountUtils.inboxFolder, false);
+ },
+ function testCopyFileMessage2() {
+ copyFileMessage(gMsgFile2, localAccountUtils.inboxFolder, false);
+ },
+ function testCopyFileMessage3() {
+ copyFileMessage(gMsgFile3, localAccountUtils.inboxFolder, true);
+ },
+
+ // Moving/copying messages
+ function testCopyMessages1() {
+ copyMessages(
+ [gMsgHdrs[0].hdr],
+ false,
+ localAccountUtils.inboxFolder,
+ gLocalFolder2
+ );
+ },
+ function testCopyMessages2() {
+ copyMessages(
+ [gMsgHdrs[1].hdr, gMsgHdrs[2].hdr],
+ false,
+ localAccountUtils.inboxFolder,
+ gLocalFolder2
+ );
+ },
+ function testMoveMessages1() {
+ copyMessages(
+ [gMsgHdrs[0].hdr, gMsgHdrs[1].hdr],
+ true,
+ localAccountUtils.inboxFolder,
+ gLocalFolder3
+ );
+ },
+ function testMoveMessages2() {
+ copyMessages(
+ [gMsgHdrs[2].hdr],
+ true,
+ localAccountUtils.inboxFolder,
+ gLocalTrashFolder
+ );
+ },
+ function testMoveMessages3() {
+ // This is to test whether the notification is correct for moving from trash
+ gMsgHdrs[2].hdr = gLocalTrashFolder.msgDatabase.getMsgHdrForMessageID(
+ gMsgHdrs[2].ID
+ );
+ copyMessages([gMsgHdrs[2].hdr], true, gLocalTrashFolder, gLocalFolder3);
+ },
+ // Moving/copying folders
+ function testCopyFolder1() {
+ copyFolder(gLocalFolder3, false, gLocalFolder2);
+ },
+ function testMoveFolder1() {
+ copyFolder(gLocalFolder3, true, localAccountUtils.inboxFolder);
+ },
+ function testMoveFolder2() {
+ copyFolder(gLocalFolder2, true, localAccountUtils.inboxFolder);
+ },
+ // Folder structure should now be
+ // Inbox
+ // -folder2
+ // --folder3
+ // -folder3
+ // Trash
+
+ // Deleting messages
+ function testDeleteMessages1() {
+ // delete to trash
+ // Let's take a moment to re-initialize stuff that got moved
+ gLocalFolder2 = localAccountUtils.inboxFolder.getChildNamed("folder2");
+ gLocalFolder3 = gLocalFolder2.getChildNamed("folder3");
+ var folder3DB = gLocalFolder3.msgDatabase;
+ for (var i = 0; i < gMsgHdrs.length; i++) {
+ gMsgHdrs[i].hdr = folder3DB.getMsgHdrForMessageID(gMsgHdrs[i].ID);
+ }
+
+ // Now delete the message
+ deleteMessages(
+ gLocalFolder3,
+ [gMsgHdrs[0].hdr, gMsgHdrs[1].hdr],
+ false,
+ false
+ );
+ },
+ // shift delete
+ function testDeleteMessages2() {
+ deleteMessages(gLocalFolder3, [gMsgHdrs[2].hdr], true, false);
+ },
+ function testDeleteMessages3() {
+ // normal delete from trash
+ var trashDB = gLocalTrashFolder.msgDatabase;
+ for (var i = 0; i < gMsgHdrs.length; i++) {
+ gMsgHdrs[i].hdr = trashDB.getMsgHdrForMessageID(gMsgHdrs[i].ID);
+ }
+ deleteMessages(gLocalTrashFolder, [gMsgHdrs[0].hdr], false, false);
+ },
+ // shift delete from trash
+ function testDeleteMessages4() {
+ deleteMessages(gLocalTrashFolder, [gMsgHdrs[1].hdr], true, false);
+ },
+
+ // Renaming folders
+ function testRename1() {
+ renameFolder(gLocalFolder3, "folder4");
+ },
+ function testRename2() {
+ renameFolder(gLocalFolder2.getChildNamed("folder4"), "folder3");
+ },
+ function testRename3() {
+ renameFolder(gLocalFolder2, "folder4");
+ },
+ function testRename4() {
+ renameFolder(
+ localAccountUtils.inboxFolder.getChildNamed("folder4"),
+ "folder2"
+ );
+ },
+
+ // Folder structure should still be
+ // Inbox
+ // -folder2
+ // --folder3
+ // -folder3
+ // Trash
+
+ // Deleting folders (currently only one folder delete is supported through the UI)
+ function deleteFolder1() {
+ deleteFolder(localAccountUtils.inboxFolder.getChildNamed("folder3"), null);
+ },
+ // Folder structure should now be
+ // Inbox
+ // -folder2
+ // --folder3
+ // Trash
+ // -folder3
+ function deleteFolder2() {
+ deleteFolder(localAccountUtils.inboxFolder.getChildNamed("folder2"), null);
+ },
+ // Folder structure should now be
+ // Inbox
+ // Trash
+ // -folder2
+ // --folder3
+ // -folder3
+ function deleteFolder3() {
+ deleteFolder(gLocalTrashFolder.getChildNamed("folder3"), null);
+ },
+ // Folder structure should now be
+ // Inbox
+ // Trash
+ // -folder2
+ // --folder3
+ function deleteFolder4() {
+ // Let's take a moment to re-initialize stuff that got moved
+ gLocalFolder2 = gLocalTrashFolder.getChildNamed("folder2");
+ gLocalFolder3 = gLocalFolder2.getChildNamed("folder3");
+ deleteFolder(gLocalFolder2, gLocalFolder3);
+ },
+ function compactInbox() {
+ if (localAccountUtils.inboxFolder.msgStore.supportsCompaction) {
+ compactFolder(localAccountUtils.inboxFolder);
+ } else {
+ doTest(++gTest);
+ }
+ },
+];
+// Folder structure should just be
+// Inbox
+// Trash
+
+function run_test() {
+ localAccountUtils.loadLocalMailAccount();
+
+ // Add a listener.
+ MailServices.mfn.addListener(gMFListener, allTestedEvents);
+
+ // Load up some messages so that we can copy them in later.
+ gMsgFile1 = do_get_file("../../../data/bugmail10");
+ gMsgFile2 = do_get_file("../../../data/bugmail11");
+ gMsgFile3 = do_get_file("../../../data/draft1");
+
+ // "Trash" folder
+ gRootFolder = localAccountUtils.incomingServer.rootMsgFolder;
+ gLocalTrashFolder = gRootFolder.getChildNamed("Trash");
+
+ // "Master" do_test_pending(), paired with a do_test_finished() at the end of all the operations.
+ do_test_pending();
+
+ // Do the test.
+ doTest(1);
+}
+
+function doTest(test) {
+ if (test <= gTestArray.length) {
+ var testFn = gTestArray[test - 1];
+ // Set a limit of 10 seconds; if the notifications haven't arrived by then there's a problem.
+ do_timeout(10000, function () {
+ if (gTest == test) {
+ do_throw(
+ "Notifications not received in 10000 ms for operation " +
+ testFn.name +
+ ", current status is " +
+ gCurrStatus
+ );
+ }
+ });
+ dump("=== Test: " + testFn.name + "\n");
+ testFn();
+ } else {
+ gHdrsReceived = null;
+ gMsgHdrs = null;
+ MailServices.mfn.removeListener(gMFListener);
+ do_test_finished(); // for the one in run_test()
+ }
+}
diff --git a/comm/mailnews/base/test/unit/test_nsIMsgTagService.js b/comm/mailnews/base/test/unit/test_nsIMsgTagService.js
new file mode 100644
index 0000000000..52e36056ef
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsIMsgTagService.js
@@ -0,0 +1,113 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests of nsIMsgTagService.
+ *
+ * Specifically tests changes implemented in bug 217034
+ * Does not do comprehensive testing.
+ *
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // These are both tags and keys. Note keys are forced to be lower case
+ const tag1 = "istag";
+ const tag2 = "notistag";
+ const tag3 = "istagnot";
+ const tag4 = "istagtoo";
+
+ // add a tag
+ MailServices.tags.addTagForKey(tag1, tag1, null, null);
+
+ // delete any existing tags
+ let tagArray = MailServices.tags.getAllTags();
+ for (var i = 0; i < tagArray.length; i++) {
+ MailServices.tags.deleteKey(tagArray[i].key);
+ }
+
+ // make sure added tag is now gone
+ Assert.ok(!MailServices.tags.isValidKey(tag1));
+
+ // add single tag, and check again
+ MailServices.tags.addTagForKey(tag1, tag1, null, null);
+ Assert.ok(MailServices.tags.isValidKey(tag1));
+ Assert.ok(!MailServices.tags.isValidKey(tag4));
+
+ // add second tag and check
+ MailServices.tags.addTagForKey(tag4, tag4, null, null);
+ Assert.ok(MailServices.tags.isValidKey(tag1));
+ Assert.ok(!MailServices.tags.isValidKey(tag2));
+ Assert.ok(!MailServices.tags.isValidKey(tag3));
+ Assert.ok(MailServices.tags.isValidKey(tag4));
+
+ // delete a tag and check
+ MailServices.tags.deleteKey(tag1);
+ Assert.ok(!MailServices.tags.isValidKey(tag1));
+ Assert.ok(!MailServices.tags.isValidKey(tag2));
+ Assert.ok(!MailServices.tags.isValidKey(tag3));
+ Assert.ok(MailServices.tags.isValidKey(tag4));
+
+ // add many tags and check again
+ for (i = 0; i < 100; i++) {
+ MailServices.tags.addTagForKey(i, "lotsatags" + i, null, null);
+ }
+ Assert.ok(!MailServices.tags.isValidKey(tag1));
+ Assert.ok(!MailServices.tags.isValidKey(tag2));
+ Assert.ok(!MailServices.tags.isValidKey(tag3));
+ Assert.ok(MailServices.tags.isValidKey(tag4));
+
+ for (i = 0; i < 100; i++) {
+ Assert.ok(MailServices.tags.isValidKey(i));
+ // make sure it knows the difference betweens tags and keys
+ Assert.ok(!MailServices.tags.isValidKey("lotsatags" + i));
+ // are we confused by key at start of tag?
+ Assert.ok(!MailServices.tags.isValidKey(i + "lotsatags"));
+ }
+
+ // Test sort ordering for getAllTags() without ordinal.
+ for (let tag of MailServices.tags.getAllTags()) {
+ MailServices.tags.deleteKey(tag.key);
+ }
+ MailServices.tags.addTag("grapefruit", null, null);
+ MailServices.tags.addTag("orange", null, null);
+ MailServices.tags.addTag("lime", null, null);
+ MailServices.tags.addTag("lemon", null, null);
+
+ // Should be sorted by tag name.
+ let tagNames = MailServices.tags.getAllTags().map(t => t.tag);
+ Assert.deepEqual(
+ tagNames,
+ ["grapefruit", "lemon", "lime", "orange"],
+ "Sort without ordinals"
+ );
+
+ // Test sort ordering for getAllTags() with (some) ordinals.
+ for (let tag of MailServices.tags.getAllTags()) {
+ MailServices.tags.deleteKey(tag.key);
+ }
+ MailServices.tags.addTag("grapefruit", null, "3");
+ MailServices.tags.addTag("orange", null, "1");
+ MailServices.tags.addTag("lime", null, null);
+ MailServices.tags.addTag("lemon", null, "2");
+
+ // Should be sorted by ordinal, then tag name.
+ tagNames = MailServices.tags.getAllTags().map(t => t.tag);
+ Assert.deepEqual(
+ tagNames,
+ ["orange", "lemon", "grapefruit", "lime"],
+ "Sort with ordinals"
+ );
+}
+
+/*
+function printTags() {
+ for (let tag of MailServices.tags.getAllTags()) {
+ print(`# key [${tag.key}] tag [${tag.tag}] ordinal [${tag.ordinal}]`);
+ }
+}
+*/
diff --git a/comm/mailnews/base/test/unit/test_nsMailDirProvider.js b/comm/mailnews/base/test/unit/test_nsMailDirProvider.js
new file mode 100644
index 0000000000..19b848f3fd
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMailDirProvider.js
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsMailDirProvider to check we get the right directories and
+ * files.
+ */
+
+function run_test() {
+ const items = [
+ { key: "MailD", value: "Mail" },
+ { key: "IMapMD", value: "ImapMail" },
+ { key: "NewsD", value: "News" },
+ { key: "MLFCaF", value: "panacea.dat" }, // Legacy folder cache.
+ { key: "MFCaF", value: "folderCache.json" },
+ ];
+
+ items.forEach(function (item) {
+ var dir = Services.dirsvc.get(item.key, Ci.nsIFile);
+ dump(do_get_profile().path + " " + dir.path + "\n");
+ Assert.ok(do_get_profile().equals(dir.parent));
+
+ Assert.equal(dir.leafName, item.value);
+ });
+}
diff --git a/comm/mailnews/base/test/unit/test_nsMsgDBView.js b/comm/mailnews/base/test/unit/test_nsMsgDBView.js
new file mode 100644
index 0000000000..cb5527deab
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgDBView.js
@@ -0,0 +1,1212 @@
+/* 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/. */
+
+/*
+ * Attempt to test nsMsgDBView and descendents. Right now this means we:
+ * - Ensure sorting and grouping sorta works, including using custom columns.
+ *
+ * Things we really should do:
+ * - Test that secondary sorting works, especially when the primary column is
+ * a custom column.
+ *
+ * You may also want to look into the test_viewWrapper_*.js tests as well.
+ */
+
+var { MessageGenerator, MessageScenarioFactory, SyntheticMessageSet } =
+ ChromeUtils.import("resource://testing-common/mailnews/MessageGenerator.jsm");
+const { TreeSelection } = ChromeUtils.importESModule(
+ "chrome://messenger/content/tree-selection.mjs"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+var { dump_view_contents } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ViewHelpers.jsm"
+);
+
+// Items used to add messages to the folder
+var gMessageGenerator = new MessageGenerator();
+var gScenarioFactory = new MessageScenarioFactory(gMessageGenerator);
+var messageInjection = new MessageInjection({ mode: "local" });
+
+var gTestFolder;
+var gSiblingsMissingParentsSubject;
+var gMessages;
+
+function setup_messages() {
+ // build up a diverse list of messages
+ let messages = [];
+ messages = messages.concat(gScenarioFactory.directReply(10));
+ // the message generator uses a constanty incrementing counter, so we need to
+ // mix up the order of messages ourselves to ensure that the timestamp
+ // ordering is not already in order. (a poor test of sorting otherwise.)
+ messages = gScenarioFactory.directReply(6).concat(messages);
+
+ messages = messages.concat(gScenarioFactory.fullPyramid(3, 3));
+ let siblingMessages = gScenarioFactory.siblingsMissingParent();
+ // cut off "Re: " part
+ gSiblingsMissingParentsSubject = siblingMessages[0].subject.slice(4);
+ dump("siblings subect = " + gSiblingsMissingParentsSubject + "\n");
+ messages = messages.concat(siblingMessages);
+ messages = messages.concat(gScenarioFactory.missingIntermediary());
+ // This next line was found to be faulty during linting, but fixing it breaks the test.
+ // messages.concat(gMessageGenerator.makeMessage({age: {days: 2, hours: 1}}));
+
+ // build a hierarchy like this (the UID order corresponds to the date order)
+ // 1
+ // 2
+ // 4
+ // 3
+ let msg1 = gMessageGenerator.makeMessage();
+ let msg2 = gMessageGenerator.makeMessage({ inReplyTo: msg1 });
+ let msg3 = gMessageGenerator.makeMessage({ inReplyTo: msg1 });
+ let msg4 = gMessageGenerator.makeMessage({ inReplyTo: msg2 });
+ messages = messages.concat([msg1, msg2, msg3, msg4]);
+
+ // test bug 600140, make a thread that Reply message has smaller MsgKey
+ let msgBiggerKey = gMessageGenerator.makeMessage();
+ let msgSmallerKey = gMessageGenerator.makeMessage({
+ inReplyTo: msgBiggerKey,
+ });
+ messages = messages.concat([msgSmallerKey, msgBiggerKey]);
+ let msgSet = new SyntheticMessageSet(messages);
+ return msgSet;
+}
+
+/**
+ * Sets gTestFolder with msgSet. Ensure that gTestFolder is clean for each test.
+ *
+ * @param {SyntheticMessageSet} msgSet
+ */
+async function set_gTestFolder(msgSet) {
+ gTestFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+}
+
+/**
+ * Create a synthetic message by passing the provided aMessageArgs to
+ * the message generator, then add the resulting message to the given
+ * folder (or gTestFolder if no folder is provided).
+ */
+async function make_and_add_message(aMessageArgs) {
+ // create the message
+ let synMsg = gMessageGenerator.makeMessage(aMessageArgs);
+ let msgSet = new SyntheticMessageSet([synMsg]);
+ // this is synchronous for local stuff.
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+
+ return [synMsg, msgSet];
+}
+
+function view_throw(why) {
+ dump_view_contents();
+ do_throw(why);
+}
+
+/**
+ * Throw if gDBView has any rows.
+ */
+function assert_view_empty() {
+ if (gTreeView.rowCount != 0) {
+ view_throw(
+ "Expected view to be empty, but it was not! (" +
+ gTreeView.rowCount +
+ " rows)"
+ );
+ }
+}
+
+/**
+ * Throw if gDBView does not have aCount rows.
+ */
+function assert_view_row_count(aCount) {
+ if (gTreeView.rowCount != aCount) {
+ view_throw(
+ "Expected view to have " +
+ aCount +
+ " rows, but it had " +
+ gTreeView.rowCount +
+ " rows!"
+ );
+ }
+}
+
+/**
+ * Throw if any of the arguments (as view indices) do not correspond to dummy
+ * rows in gDBView.
+ */
+function assert_view_index_is_dummy(...aArgs) {
+ for (let viewIndex of aArgs) {
+ let flags = gDBView.getFlagsAt(viewIndex);
+ if (!(flags & MSG_VIEW_FLAG_DUMMY)) {
+ view_throw("Expected index " + viewIndex + " to be a dummy!");
+ }
+ }
+}
+
+/**
+ * Throw if any of the arguments (as view indices) correspond to dummy rows in
+ * gDBView.
+ */
+function assert_view_index_is_not_dummy(...aArgs) {
+ for (let viewIndex of aArgs) {
+ let flags = gDBView.getFlagsAt(viewIndex);
+ if (flags & MSG_VIEW_FLAG_DUMMY) {
+ view_throw("Expected index " + viewIndex + " to not be a dummy!");
+ }
+ }
+}
+
+function assert_view_level_is(index, level) {
+ if (gDBView.getLevel(index) != level) {
+ view_throw(
+ "Expected index " +
+ index +
+ " to be level " +
+ level +
+ " not " +
+ gDBView.getLevel(index)
+ );
+ }
+}
+
+/**
+ * Given a message, assert that it is present at the given indices.
+ *
+ * Usage:
+ * assert_view_message_at_indices(synMsg, 0);
+ * assert_view_message_at_indices(synMsg, 0, 1);
+ * assert_view_message_at_indices(aMsg, 0, bMsg, 1);
+ */
+function assert_view_message_at_indices(...aArgs) {
+ let curHdr;
+ for (let thing of aArgs) {
+ if (typeof thing == "number") {
+ let hdrAt = gDBView.getMsgHdrAt(thing);
+ if (curHdr != hdrAt) {
+ view_throw(
+ "Expected hdr at " +
+ thing +
+ " to be " +
+ curHdr.messageKey +
+ ":" +
+ curHdr.mime2DecodedSubject.substr(0, 30) +
+ " not " +
+ hdrAt.messageKey +
+ ":" +
+ hdrAt.mime2DecodedSubject.substr(0, 30)
+ );
+ }
+ } else {
+ // synthetic message, get the header...
+ curHdr = gTestFolder.msgDatabase.getMsgHdrForMessageID(thing.messageId);
+ }
+ }
+}
+
+var authorFirstLetterCustomColumn = {
+ getCellText(row, col) {
+ let msgHdr = this.dbView.getMsgHdrAt(row);
+ return msgHdr.mime2DecodedAuthor.charAt(0).toUpperCase() || "?";
+ },
+ getSortStringForRow(msgHdr) {
+ // charAt(0) is a quote, charAt(1) is the first letter!
+ return msgHdr.mime2DecodedAuthor.charAt(1).toUpperCase() || "?";
+ },
+ isString() {
+ return true;
+ },
+
+ getCellProperties(row, col) {
+ return "";
+ },
+ getRowProperties(row) {
+ return "";
+ },
+ getImageSrc(row, col) {
+ return null;
+ },
+ getSortLongForRow(hdr) {
+ return 0;
+ },
+};
+
+var gDBView;
+var gTreeView;
+
+var MSG_VIEW_FLAG_DUMMY = 0x20000000;
+
+var gFakeSelection = new TreeSelection(null);
+
+function setup_view(aViewType, aViewFlags, aTestFolder) {
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=" + aViewType;
+
+ if (aTestFolder == null) {
+ aTestFolder = gTestFolder;
+ }
+
+ // always start out fully expanded
+ aViewFlags |= Ci.nsMsgViewFlagsType.kExpandAll;
+
+ gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ gDBView.init(null, null, null);
+ var outCount = {};
+ gDBView.open(
+ aViewType != "search" ? aTestFolder : null,
+ Ci.nsMsgViewSortType.byDate,
+ aViewType != "search"
+ ? Ci.nsMsgViewSortOrder.ascending
+ : Ci.nsMsgViewSortOrder.descending,
+ aViewFlags,
+ outCount
+ );
+ // outCount is 0 if byCustom; view is built by addColumnHandler()
+ dump(" View Out Count: " + outCount.value + "\n");
+
+ // we need to cram messages into the search via nsIMsgSearchNotify interface
+ if (
+ aViewType == "search" ||
+ aViewType == "quicksearch" ||
+ aViewType == "xfvf"
+ ) {
+ let searchNotify = gDBView.QueryInterface(Ci.nsIMsgSearchNotify);
+ searchNotify.onNewSearch();
+ for (let msgHdr of aTestFolder.msgDatabase.enumerateMessages()) {
+ searchNotify.onSearchHit(msgHdr, msgHdr.folder);
+ }
+ searchNotify.onSearchDone(Cr.NS_OK);
+ }
+
+ gDBView.addColumnHandler(
+ "authorFirstLetterCol",
+ authorFirstLetterCustomColumn
+ );
+ // XXX this sets the custom column to use for sorting by the custom column.
+ // It has been argued (and is generally accepted) that this should not be
+ // so limited.
+ gDBView.curCustomColumn = "authorFirstLetterCol";
+
+ gTreeView = gDBView.QueryInterface(Ci.nsITreeView);
+ gTreeView.selection = gFakeSelection;
+ gFakeSelection.view = gTreeView;
+}
+
+function setup_group_view(aSortType, aSortOrder, aTestFolder) {
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=group";
+
+ if (aTestFolder == null) {
+ aTestFolder = gTestFolder;
+ }
+
+ // grouped view uses these flags
+ let viewFlags =
+ Ci.nsMsgViewFlagsType.kGroupBySort |
+ Ci.nsMsgViewFlagsType.kExpandAll |
+ Ci.nsMsgViewFlagsType.kThreadedDisplay;
+
+ gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ gDBView.init(null, null, null);
+ var outCount = {};
+ gDBView.open(aTestFolder, aSortType, aSortOrder, viewFlags, outCount);
+
+ gDBView.addColumnHandler(
+ "authorFirstLetterCol",
+ authorFirstLetterCustomColumn
+ );
+ gDBView.curCustomColumn = "authorFirstLetterCol";
+
+ gTreeView = gDBView.QueryInterface(Ci.nsITreeView);
+ gFakeSelection.view = gTreeView;
+ gTreeView.selection = gFakeSelection;
+}
+
+/**
+ * Comparison func for built-in types (including strings, so no subtraction.)
+ */
+function generalCmp(a, b) {
+ if (a < b) {
+ return -1;
+ } else if (a > b) {
+ return 1;
+ }
+ return 0;
+}
+
+/**
+ * Check that sort order and grouping logic (if applicable) are doing the right
+ * thing.
+ *
+ * In the case of groups (indicated by dummy headers), we want to ignore the
+ * dummies and 1) make sure all the values in the group have the same value,
+ * 2) verify that the headers meet our total ordering.
+ * In the case of threads, we want to ensure that each level of the hierarchy
+ * meets our ordering demands, recursing into children. Because the tree
+ * representation is rather quite horrible, the easiest thing for us is to
+ * track a per-level list of comparison values we have seen, nuking older
+ * values when changes in levels indicate closure of a level. (Namely,
+ * if we see a node at level N, then all levels >N are no longer valid.)
+ *
+ * @param {nsMsgViewType} aSortBy - The sort type.
+ * @param {nsMsgViewSortOrder} aDirection - The sort direction.
+ * @param {string|Function} aKeyOrValueGetter - A string naming the attribute on
+ * the message headerto retrieve, or if that is not sufficient a function that
+ * takes a message header and returns the sort value for it.
+ * @param {Function} [aGetGroupValue] - An optional function that takes a
+ * message header and returns the grouping value for the header.
+ * If omitted, it is assumed that the sort value is the grouping value.
+ */
+function ensure_view_ordering(
+ aSortBy,
+ aDirection,
+ aKeyOrValueGetter,
+ aGetGroupValue
+) {
+ if (!gTreeView.rowCount) {
+ do_throw("There are no rows in my folder! I can't test anything!");
+ }
+ dump(
+ " Ensuring sort order for " +
+ aSortBy +
+ " (Row count: " +
+ gTreeView.rowCount +
+ ")\n"
+ );
+ dump(" cur view flags: " + gDBView.viewFlags + "\n");
+
+ // standard grouping doesn't re-group when you sort. so we need to actually
+ // re-initialize the view.
+ // but search mode is special and does the right thing because asuth didn't
+ // realize that it shouldn't do the right thing, so it can just change the
+ // sort. (of course, under the hood, it is actually creating a new view...)
+ if (
+ gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort &&
+ gDBView.viewType != Ci.nsMsgViewType.eShowSearch
+ ) {
+ // we must close to re-open (or we could just use a new view)
+ let msgFolder = gDBView.msgFolder;
+ gDBView.close();
+ gDBView.open(msgFolder, aSortBy, aDirection, gDBView.viewFlags, {});
+ } else {
+ gDBView.sort(aSortBy, aDirection);
+ }
+
+ let comparisonValuesByLevel = [];
+ let expectedLevel0CmpResult =
+ aDirection == Ci.nsMsgViewSortOrder.ascending ? 1 : -1;
+ let comparator = generalCmp;
+
+ let dummyCount = 0,
+ emptyDummyCount = 0;
+
+ let valueGetter =
+ typeof aKeyOrValueGetter == "string"
+ ? function (msgHdr) {
+ return msgHdr[aKeyOrValueGetter];
+ }
+ : aKeyOrValueGetter;
+ let groupValueGetter = aGetGroupValue || valueGetter;
+
+ // don't do group testing until we see a dummy header (which we will see
+ // before we see any grouped headers, so it's fine to do this)
+ let inGroup = false;
+ // the current grouping value for the current group. this allows us to
+ // detect erroneous grouping of different group values together.
+ let curGroupValue = null;
+ // the set of group values observed before the current group. this allows
+ // us to detect improper grouping where there are multiple groups with the
+ // same grouping value.
+ let previouslySeenGroupValues = {};
+
+ for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) {
+ let msgHdr = gDBView.getMsgHdrAt(iViewIndex);
+ let msgViewFlags = gDBView.getFlagsAt(iViewIndex);
+
+ // ignore dummy headers; testing grouping logic happens elsewhere
+ if (msgViewFlags & MSG_VIEW_FLAG_DUMMY) {
+ if (dummyCount && curGroupValue == null) {
+ emptyDummyCount++;
+ }
+ dummyCount++;
+ if (curGroupValue != null) {
+ previouslySeenGroupValues[curGroupValue] = true;
+ }
+ curGroupValue = null;
+ inGroup = true;
+ continue;
+ }
+
+ // level is 0-based
+ let level = gTreeView.getLevel(iViewIndex);
+ // nuke existing comparison levels
+ if (level < comparisonValuesByLevel.length - 1) {
+ comparisonValuesByLevel.splice(level);
+ }
+
+ // get the value for comparison
+ let curValue = valueGetter(msgHdr);
+ if (inGroup) {
+ let groupValue = groupValueGetter(msgHdr);
+ if (groupValue in previouslySeenGroupValues) {
+ do_throw(`Group value ${groupValue} observed in more than one group!`);
+ }
+ if (curGroupValue == null) {
+ curGroupValue = groupValue;
+ } else if (curGroupValue != groupValue) {
+ do_throw(
+ "Inconsistent grouping! " + groupValue + " != " + curGroupValue
+ );
+ }
+ }
+
+ // is this level new to our comparisons? then track it...
+ if (level >= comparisonValuesByLevel.length) {
+ // null-fill any gaps (due to, say, dummy nodes)
+ while (comparisonValuesByLevel.length <= level) {
+ comparisonValuesByLevel.push(null);
+ }
+ comparisonValuesByLevel.push(curValue);
+ } else {
+ // otherwise compare it
+ let prevValue = comparisonValuesByLevel[level - 1];
+ let cmpResult = comparator(curValue, prevValue);
+ let expectedCmpResult = level > 0 ? 1 : expectedLevel0CmpResult;
+ if (cmpResult && cmpResult != expectedCmpResult) {
+ do_throw(
+ "Ordering failure on key " +
+ msgHdr.messageKey +
+ ". " +
+ curValue +
+ " should have been " +
+ (expectedCmpResult == 1 ? ">=" : "<=") +
+ " " +
+ prevValue +
+ " but was not."
+ );
+ }
+ }
+ }
+
+ if (inGroup && curGroupValue == null) {
+ emptyDummyCount++;
+ }
+ if (dummyCount) {
+ dump(
+ " saw " +
+ dummyCount +
+ " dummy headers (" +
+ emptyDummyCount +
+ " empty).\n"
+ );
+ }
+}
+
+/**
+ * Test sorting functionality.
+ */
+function test_sort_columns() {
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.descending,
+ "date",
+ function getDateAgeBucket(msgHdr) {
+ // so, this is a cop-out, but we know that the date age bucket for our
+ // generated messages is always more than 2-weeks ago!
+ return 5;
+ }
+ );
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ "date",
+ function getDateAgeBucket(msgHdr) {
+ // so, this is a cop-out, but we know that the date age bucket for our
+ // generated messages is always more than 2-weeks ago!
+ return 5;
+ }
+ );
+ // (note, subject doesn't use dummy groups and so won't have grouping tested)
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.bySubject,
+ Ci.nsMsgViewSortOrder.ascending,
+ "mime2DecodedSubject"
+ );
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byAuthor,
+ Ci.nsMsgViewSortOrder.ascending,
+ "mime2DecodedAuthor"
+ );
+ // Id
+ // Thread
+ // Priority
+ // Status
+ // Size
+ // Flagged
+ // Unread
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byRecipient,
+ Ci.nsMsgViewSortOrder.ascending,
+ "mime2DecodedRecipients"
+ );
+ // Location
+ // Tags
+ // JunkStatus
+ // Attachments
+ // Account
+ // Custom
+ ensure_view_ordering(
+ Ci.nsMsgViewSortType.byCustom,
+ Ci.nsMsgViewSortOrder.ascending,
+ function (msgHdr) {
+ return authorFirstLetterCustomColumn.getSortStringForRow(msgHdr);
+ }
+ );
+ // Received
+}
+
+function test_number_of_messages() {
+ // Bug 574799
+ if (gDBView.numMsgsInView != gTestFolder.getTotalMessages(false)) {
+ do_throw(
+ "numMsgsInView is " +
+ gDBView.numMsgsInView +
+ " but should be " +
+ gTestFolder.getTotalMessages(false) +
+ "\n"
+ );
+ }
+ // Bug 600140
+ // Maybe elided so open it, now only consider the first one
+ if (gDBView.isContainer(0) && !gDBView.isContainerOpen(0)) {
+ gDBView.toggleOpenState(0);
+ }
+ let numMsgInTree = gTreeView.rowCount;
+ if (gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort) {
+ for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) {
+ let flags = gDBView.getFlagsAt(iViewIndex);
+ if (flags & MSG_VIEW_FLAG_DUMMY) {
+ numMsgInTree--;
+ }
+ }
+ }
+ if (gDBView.numMsgsInView != numMsgInTree) {
+ view_throw(
+ "message in tree is " +
+ numMsgInTree +
+ " but should be " +
+ gDBView.numMsgsInView +
+ "\n"
+ );
+ }
+}
+
+function test_selected_messages() {
+ gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll);
+
+ // Select one message
+ gTreeView.selection.select(1);
+ let selectedMessages = gDBView.getSelectedMsgHdrs();
+
+ if (selectedMessages.length != 1) {
+ do_throw(
+ "getSelectedMsgHdrs.length is " +
+ selectedMessages.length +
+ " but should be 1\n"
+ );
+ }
+
+ let firstSelectedMsg = gDBView.hdrForFirstSelectedMessage;
+ if (selectedMessages[0] != firstSelectedMsg) {
+ do_throw(
+ "getSelectedMsgHdrs[0] is " +
+ selectedMessages[0].messageKey +
+ " but should be " +
+ firstSelectedMsg.messageKey +
+ "\n"
+ );
+ }
+
+ // Select all messages
+ gTreeView.selection.selectAll();
+ if (gDBView.numSelected != gTreeView.rowCount) {
+ do_throw(
+ "numSelected is " +
+ gDBView.numSelected +
+ " but should be " +
+ gTreeView.rowCount +
+ "\n"
+ );
+ }
+
+ selectedMessages = gDBView.getSelectedMsgHdrs();
+ if (selectedMessages.length != gTestFolder.getTotalMessages(false)) {
+ do_throw(
+ "getSelectedMsgHdrs.length is " +
+ selectedMessages.length +
+ " but should be " +
+ gTestFolder.getTotalMessages(false) +
+ "\n"
+ );
+ }
+
+ for (let i = 0; i < selectedMessages.length; i++) {
+ let expectedHdr = gDBView.getMsgHdrAt(i);
+ if (!selectedMessages.includes(expectedHdr)) {
+ view_throw(
+ "Expected " +
+ expectedHdr.messageKey +
+ ":" +
+ expectedHdr.mime2DecodedSubject.substr(0, 30) +
+ " to be selected, but it wasn't\n"
+ );
+ }
+ }
+
+ gTreeView.selection.clearSelection();
+}
+
+function test_insert_remove_view_rows() {
+ // Test insertion/removal into m_keys.
+ let startCount = gTreeView.rowCount;
+ let index = 0;
+ let rows = 3;
+ let msgKey = rows * 1000;
+ let flags = 0;
+ let level = 0;
+ let folder = null;
+ let xfview =
+ gDBView.viewType == Ci.nsMsgViewType.eShowSearch ||
+ gDBView.viewType == Ci.nsMsgViewType.eShowVirtualFolderResults;
+ if (xfview) {
+ folder = gDBView.getFolderForViewIndex(index);
+ }
+
+ gDBView.insertTreeRows(index, rows, msgKey, flags, level, folder);
+ assert_view_row_count(startCount + rows);
+ let key = gDBView.getKeyAt(rows - 1);
+ if (key != msgKey) {
+ view_throw("msgKey is " + key + " but should be " + msgKey + "\n");
+ }
+ gDBView.removeTreeRows(index, rows);
+ assert_view_row_count(startCount);
+
+ // These should fail.
+ try {
+ gDBView.insertTreeRows(startCount + 10, rows, msgKey, flags, level, folder);
+ view_throw("expected exception not caught; inserting at illegal index \n");
+ } catch (ex) {}
+ try {
+ gDBView.insertTreeRows(index, rows, msgKey, flags, level, folder);
+ gDBView.removeTreeRows(index, gTreeView.rowCount + 10);
+ view_throw("expected exception not caught; removing illegal rows \n");
+ } catch (ex) {
+ gDBView.removeTreeRows(index, rows);
+ }
+ if (xfview) {
+ try {
+ gDBView.insertTreeRows(index, rows, msgKey, flags, level, null);
+ view_throw(
+ "expected exception not caught; folder required for xfvf view \n"
+ );
+ } catch (ex) {}
+ }
+}
+
+async function test_msg_added_to_search_view() {
+ // if the view is a non-grouped search view, test adding a header to
+ // the search results, and verify it gets put at top.
+ if (!(gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort)) {
+ gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending);
+ let [synMsg] = await make_and_add_message();
+ let msgHdr = gTestFolder.msgDatabase.getMsgHdrForMessageID(
+ synMsg.messageId
+ );
+ gDBView
+ .QueryInterface(Ci.nsIMsgSearchNotify)
+ .onSearchHit(msgHdr, msgHdr.folder);
+ assert_view_message_at_indices(synMsg, 0);
+ }
+}
+
+function IsHdrChildOf(possibleParent, possibleChild) {
+ let parentHdrId = possibleParent.messageId;
+ let numRefs = possibleChild.numReferences;
+ for (let refIndex = 0; refIndex < numRefs; refIndex++) {
+ if (parentHdrId == possibleChild.getStringReference(refIndex)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// This could be part of ensure_view_ordering() but I don't want to make that
+// function any harder to read.
+function test_threading_levels() {
+ if (!gTreeView.rowCount) {
+ do_throw("There are no rows in my folder! I can't test anything!");
+ }
+ // only look at threaded, non-grouped views.
+ if (
+ gDBView.viewFlags & Ci.nsMsgViewFlagsType.kGroupBySort ||
+ !(gDBView.viewFlags & Ci.nsMsgViewFlagsType.kThreadedDisplay)
+ ) {
+ return;
+ }
+
+ let prevLevel = 1;
+ let prevMsgHdr;
+ for (let iViewIndex = 0; iViewIndex < gTreeView.rowCount; iViewIndex++) {
+ let msgHdr = gDBView.getMsgHdrAt(iViewIndex);
+ let level = gTreeView.getLevel(iViewIndex);
+ if (level > prevLevel && msgHdr.subject != gSiblingsMissingParentsSubject) {
+ if (!IsHdrChildOf(prevMsgHdr, msgHdr)) {
+ view_throw("indented message not child of parent");
+ }
+ }
+ prevLevel = level;
+ prevMsgHdr = msgHdr;
+ }
+}
+
+function test_expand_collapse() {
+ let oldRowCount = gDBView.rowCount;
+ let thirdChild = gDBView.getMsgHdrAt(3);
+ gDBView.toggleOpenState(0);
+ if (gDBView.rowCount != oldRowCount - 9) {
+ view_throw("collapsing first item should have removed 9 items");
+ }
+
+ // test that expand/collapse works with killed sub-thread.
+ oldRowCount = gDBView.rowCount;
+ gTestFolder.msgDatabase.markHeaderKilled(thirdChild, true, null);
+ gDBView.toggleOpenState(0);
+ if (gDBView.rowCount != oldRowCount + 2) {
+ view_throw("expanding first item should have aded 2 items");
+ }
+ gTestFolder.msgDatabase.markHeaderKilled(thirdChild, false, null);
+ oldRowCount = gDBView.rowCount;
+ gDBView.toggleOpenState(0);
+ if (gDBView.rowCount != oldRowCount - 2) {
+ view_throw("collapsing first item should have removed 2 items");
+ }
+}
+
+function test_qs_results() {
+ // This just tests that bug 505967 hasn't regressed.
+ if (gTreeView.getLevel(0) != 0) {
+ view_throw("first message should be at level 0");
+ }
+ if (gTreeView.getLevel(1) != 1) {
+ view_throw("second message should be at level 1");
+ }
+ if (gTreeView.getLevel(2) != 2) {
+ view_throw("third message should be at level 2");
+ }
+ test_threading_levels();
+}
+
+async function test_group_sort_collapseAll_expandAll_threading() {
+ // - start with an empty folder
+ gTestFolder = await messageInjection.makeEmptyFolder();
+
+ // - create a normal unthreaded view
+ setup_view("threaded", 0);
+
+ // - ensure it's empty
+ assert_view_empty();
+
+ // - add 3 messages:
+ // msg1: from A, custom column val A, to be starred
+ // msg2: from A, custom column val A
+ // msg3: from B, custom column val B
+ let [smsg1] = await make_and_add_message({ from: ["A", "A@a.invalid"] });
+ await make_and_add_message({ from: ["A", "A@a.invalid"] });
+ let [smsg3] = await make_and_add_message({ from: ["B", "B@b.invalid"] });
+
+ assert_view_row_count(3);
+ gDBView.getMsgHdrAt(0).markFlagged(true);
+ if (!gDBView.getMsgHdrAt(0).isFlagged) {
+ view_throw("Expected smsg1 to be flagged");
+ }
+
+ // - create grouped view; open folder in byFlagged AZ sort
+ setup_group_view(
+ Ci.nsMsgViewSortType.byFlagged,
+ Ci.nsMsgViewSortOrder.ascending,
+ gTestFolder
+ );
+ // - make sure there are 5 rows; index 0 and 2 are dummy, 1 is flagged message,
+ // 3-4 are messages
+ assert_view_row_count(5);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ assert_view_message_at_indices(smsg1, 1);
+ if (!gDBView.getMsgHdrAt(1).isFlagged) {
+ view_throw("Expected grouped smsg1 to be flagged");
+ }
+ assert_view_index_is_dummy(2);
+ assert_view_index_is_not_dummy(3);
+ assert_view_index_is_not_dummy(4);
+
+ // - collapse the grouped threads; there should be 2 dummy rows
+ gDBView.doCommand(Ci.nsMsgViewCommandType.collapseAll);
+ assert_view_row_count(2);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_dummy(1);
+
+ // - expand the grouped threads; there should be 5 rows
+ gDBView.doCommand(Ci.nsMsgViewCommandType.expandAll);
+ assert_view_row_count(5);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_dummy(2);
+
+ // - reverse sort; create grouped view; open folder in byFlagged ZA sort
+ setup_group_view(
+ Ci.nsMsgViewSortType.byFlagged,
+ Ci.nsMsgViewSortOrder.descending,
+ gTestFolder
+ );
+ // - make sure there are 5 rows; index 0 and 3 are dummy, 1-2 are messages,
+ // 4 is flagged message
+ assert_view_row_count(5);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ assert_view_index_is_not_dummy(2);
+ assert_view_index_is_dummy(3);
+ assert_view_index_is_not_dummy(4);
+ assert_view_message_at_indices(smsg1, 4);
+ if (!gDBView.getMsgHdrAt(4).isFlagged) {
+ view_throw("Expected reverse sorted grouped smsg1 to be flagged");
+ }
+
+ // - test grouped by custom column; the custCol is first letter of author
+ // - create grouped view; open folder in byCustom ZA sort
+ setup_group_view(
+ Ci.nsMsgViewSortType.byCustom,
+ Ci.nsMsgViewSortOrder.descending,
+ gTestFolder
+ );
+
+ // - make sure there are 5 rows; index 0 and 2 are dummy, 1 is B value message,
+ // 3-4 are messages with A value
+ assert_view_row_count(5);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ assert_view_message_at_indices(smsg3, 1);
+ if (
+ authorFirstLetterCustomColumn.getSortStringForRow(gDBView.getMsgHdrAt(1)) !=
+ "B"
+ ) {
+ view_throw(
+ "Expected grouped by custom column, ZA sortOrder smsg3 value to be B"
+ );
+ }
+ assert_view_index_is_dummy(2);
+ assert_view_index_is_not_dummy(3);
+ assert_view_index_is_not_dummy(4);
+ if (
+ authorFirstLetterCustomColumn.getSortStringForRow(gDBView.getMsgHdrAt(4)) !=
+ "A"
+ ) {
+ view_throw(
+ "Expected grouped by custom column, ZA sortOrder smsg2 value to be A"
+ );
+ }
+}
+
+async function test_group_dummies_under_mutation_by_date() {
+ // - start with an empty folder
+ gTestFolder = await messageInjection.makeEmptyFolder();
+
+ // - create the view
+ setup_view("group", Ci.nsMsgViewFlagsType.kGroupBySort);
+ gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.ascending);
+
+ // - ensure it's empty
+ assert_view_empty();
+
+ // - add a message from this week
+ // (we want to make sure all the messages end up in the same bucket and that
+ // the current day changing as we run the test does not change buckets
+ // either. bucket 1 is same day, bucket 2 is yesterday, bucket 3 is last
+ // week, so 2 days ago or older is always last week, even if we roll over
+ // and it becomes 3 days ago.)
+ let [smsg, synSet] = await make_and_add_message({
+ age: { days: 2, hours: 1 },
+ });
+
+ // - make sure the message and a dummy appear
+ assert_view_row_count(2);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ assert_view_message_at_indices(smsg, 0, 1);
+
+ // we used to display total in tag column - make sure we don't do that.
+ if (gDBView.cellTextForColumn(0, "tags") != "") {
+ view_throw("tag column shouldn't display total count in group view");
+ }
+
+ // - move the messages to the trash
+ await messageInjection.trashMessages(synSet);
+
+ // - make sure the message and dummy disappear
+ assert_view_empty();
+
+ // - add two messages from this week (same date bucket concerns)
+ let [newer, newerSet] = await make_and_add_message({
+ age: { days: 2, hours: 1 },
+ });
+ let [older] = await make_and_add_message({ age: { days: 2, hours: 2 } });
+
+ // - sanity check addition
+ assert_view_row_count(3); // 2 messages + 1 dummy
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1, 2);
+ // the dummy should be based off the older guy
+ assert_view_message_at_indices(older, 0, 1);
+ assert_view_message_at_indices(newer, 2);
+
+ // - delete the message right under the dummy
+ // (this will be the newer one)
+ await messageInjection.trashMessages(newerSet);
+
+ // - ensure we still have the dummy and the right child node
+ assert_view_row_count(2);
+ assert_view_index_is_dummy(0);
+ assert_view_index_is_not_dummy(1);
+ // now the dummy should be based off the remaining older one
+ assert_view_message_at_indices(older, 0, 1);
+}
+
+async function test_xfvf_threading() {
+ // - start with an empty folder
+ let save_gTestFolder = gTestFolder;
+ gTestFolder = await messageInjection.makeEmptyFolder();
+
+ let messages = [];
+ // Add messages such that ancestors arrive after their descendents in
+ // various interesting ways.
+ // build a hierarchy like this (the UID order corresponds to the date order)
+ // 3
+ // 1
+ // 4
+ // 2
+ // 5
+ let msg3 = gMessageGenerator.makeMessage({ age: { days: 2, hours: 5 } });
+ let msg1 = gMessageGenerator.makeMessage({
+ age: { days: 2, hours: 4 },
+ inReplyTo: msg3,
+ });
+ let msg4 = gMessageGenerator.makeMessage({
+ age: { days: 2, hours: 3 },
+ inReplyTo: msg1,
+ });
+ let msg2 = gMessageGenerator.makeMessage({
+ age: { days: 2, hours: 1 },
+ inReplyTo: msg4,
+ });
+ let msg5 = gMessageGenerator.makeMessage({
+ age: { days: 2, hours: 2 },
+ inReplyTo: msg1,
+ });
+ messages = messages.concat([msg1, msg2, msg3, msg4, msg5]);
+
+ let msgSet = new SyntheticMessageSet(messages);
+
+ gTestFolder = await messageInjection.makeEmptyFolder();
+
+ // - create the view
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+ setup_view("xfvf", Ci.nsMsgViewFlagsType.kThreadedDisplay);
+ assert_view_row_count(5);
+ gDBView.toggleOpenState(0);
+ gDBView.toggleOpenState(0);
+
+ assert_view_message_at_indices(msg3, 0);
+ assert_view_message_at_indices(msg1, 1);
+ assert_view_message_at_indices(msg4, 2);
+ assert_view_message_at_indices(msg2, 3);
+ assert_view_message_at_indices(msg5, 4);
+ assert_view_level_is(0, 0);
+ assert_view_level_is(1, 1);
+ assert_view_level_is(2, 2);
+ assert_view_level_is(3, 3);
+ assert_view_level_is(4, 2);
+ gTestFolder = save_gTestFolder;
+}
+
+/*
+ * Tests the sorting order of collapsed threads, not of messages within
+ * threads. Currently limited to testing the sort-threads-by-date case,
+ * sorting both by thread root and by newest message.
+ */
+async function test_thread_sorting() {
+ let save_gTestFolder = gTestFolder;
+ gTestFolder = await messageInjection.makeEmptyFolder();
+ let messages = [];
+ // build a hierarchy like this (the UID order corresponds to the date order)
+ // 1
+ // 4
+ // 2
+ // 5
+ // 3
+ let msg1 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 10 } });
+ let msg2 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 9 } });
+ let msg3 = gMessageGenerator.makeMessage({ age: { days: 1, hours: 8 } });
+ let msg4 = gMessageGenerator.makeMessage({
+ age: { days: 1, hours: 7 },
+ inReplyTo: msg1,
+ });
+ let msg5 = gMessageGenerator.makeMessage({
+ age: { days: 1, hours: 6 },
+ inReplyTo: msg2,
+ });
+ messages = messages.concat([msg1, msg2, msg3, msg4, msg5]);
+
+ let msgSet = new SyntheticMessageSet(messages);
+
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+
+ // test the non-default pref state first, so the pref gets left with its
+ // default value at the end
+ Services.prefs.setBoolPref("mailnews.sort_threads_by_root", true);
+ gDBView.open(
+ gTestFolder,
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ Ci.nsMsgViewFlagsType.kThreadedDisplay,
+ {}
+ );
+
+ assert_view_row_count(3);
+ assert_view_message_at_indices(msg1, 0);
+ assert_view_message_at_indices(msg2, 1);
+ assert_view_message_at_indices(msg3, 2);
+
+ gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending);
+ assert_view_message_at_indices(msg3, 0);
+ assert_view_message_at_indices(msg2, 1);
+ assert_view_message_at_indices(msg1, 2);
+
+ Services.prefs.clearUserPref("mailnews.sort_threads_by_root");
+ gDBView.open(
+ gTestFolder,
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ Ci.nsMsgViewFlagsType.kThreadedDisplay,
+ {}
+ );
+
+ assert_view_row_count(3);
+ assert_view_message_at_indices(msg3, 0);
+ assert_view_message_at_indices(msg1, 1);
+ assert_view_message_at_indices(msg2, 2);
+
+ gDBView.sort(Ci.nsMsgViewSortType.byDate, Ci.nsMsgViewSortOrder.descending);
+ assert_view_message_at_indices(msg2, 0);
+ assert_view_message_at_indices(msg1, 1);
+ assert_view_message_at_indices(msg3, 2);
+
+ gDBView.close();
+ gTestFolder = save_gTestFolder;
+}
+
+const VIEW_TYPES = [
+ ["threaded", Ci.nsMsgViewFlagsType.kThreadedDisplay],
+ ["quicksearch", Ci.nsMsgViewFlagsType.kThreadedDisplay],
+ ["search", Ci.nsMsgViewFlagsType.kThreadedDisplay],
+ ["search", Ci.nsMsgViewFlagsType.kGroupBySort],
+ ["xfvf", Ci.nsMsgViewFlagsType.kNone],
+ // group does unspeakable things to gTestFolder, so put it last.
+ ["group", Ci.nsMsgViewFlagsType.kGroupBySort],
+];
+
+/**
+ * These are tests which are for every test configuration.
+ */
+function tests_for_all_views() {
+ test_sort_columns();
+ test_number_of_messages();
+ test_selected_messages();
+ test_insert_remove_view_rows();
+}
+
+add_setup(function () {
+ gMessages = setup_messages();
+});
+
+add_task(async function test_threaded() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[0];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for threaded.
+ test_expand_collapse();
+ await test_thread_sorting();
+});
+
+add_task(async function test_quicksearch_threaded() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[1];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for quicksearch threaded.
+ test_qs_results();
+});
+
+add_task(async function test_search_threaded() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[2];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for search threaded.
+ await test_msg_added_to_search_view();
+});
+
+add_task(async function test_search_group_by_sort() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[3];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for search group by sort.
+ await test_msg_added_to_search_view();
+});
+
+add_task(async function test_xfvf() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[4];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for xfvf.
+ await test_xfvf_threading();
+});
+
+add_task(async function test_group() {
+ await set_gTestFolder(gMessages);
+ let [view_type, view_flag] = VIEW_TYPES[5];
+ setup_view(view_type, view_flag);
+
+ tests_for_all_views();
+
+ // Specific tests for group.
+ await test_group_sort_collapseAll_expandAll_threading;
+ await test_group_dummies_under_mutation_by_date;
+});
+
+add_task(function test_teardown() {
+ // Delete view reference to avoid a cycle leak.
+ gFakeSelection.view = null;
+});
diff --git a/comm/mailnews/base/test/unit/test_nsMsgDBView_headerValues.js b/comm/mailnews/base/test/unit/test_nsMsgDBView_headerValues.js
new file mode 100644
index 0000000000..796257a8a8
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgDBView_headerValues.js
@@ -0,0 +1,110 @@
+/* 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/. */
+
+/**
+ * Test that nsMsgDBView properly reports the values of messages in the display.
+ */
+
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var messageInjection = new MessageInjection({ mode: "local" });
+
+// This is an array of the actual test data. Each test datum is an array of two
+// elements: the first element is the argument into a simple message generator,
+// and the second element is a map of column names to expected values when
+// requesting the cell text for a given column name.
+var tests = [
+ [{ from: "John Doe <db@tinderbox.invalid>" }, { senderCol: "John Doe" }],
+ [{ from: '"Doe, John" <db@tinderbox.invalid>' }, { senderCol: "Doe, John" }],
+ // Multiple senders are indicated with 'et al.' suffix.
+ [
+ { from: "John Doe <db@tinderbox.invalid>, Sally Ann <db@null.invalid>" },
+ { senderCol: "John Doe et al." },
+ ],
+ [
+ { from: "=?UTF-8?Q?David_H=C3=A5s=C3=A4ther?= <db@null.invalid>" },
+ { senderCol: "David Håsäther" },
+ ],
+ [
+ { from: "=?UTF-8?Q?H=C3=A5s=C3=A4ther=2C_David?= <db@null.invalid>" },
+ { senderCol: "Håsäther, David" },
+ ],
+ [
+ { from: '"Håsäther, David" <db@null.invalid>' },
+ { senderCol: "Håsäther, David" },
+ ],
+ [
+ { from: "David Håsäther <db@null.invalid>" },
+ { senderCol: "David Håsäther" },
+ ],
+ [
+ {
+ from: "\xC2\xAB\xCE\xA0\xCE\x9F\xCE\x9B\xCE\x99\xCE\xA4\xCE\x97\xCE\xA3\xC2\xBB",
+ },
+ { senderCol: "«ΠΟΛΙΤΗΣ»" },
+ ],
+ [
+ {
+ from: "John Doe \xF5 <db@null.invalid>",
+ clobberHeaders: { "Content-type": "text/plain; charset=ISO-8859-1" },
+ },
+ { senderCol: "John Doe õ" },
+ ],
+ [
+ {
+ from: "John Doe \xF5 <db@null.invalid>",
+ clobberHeaders: { "Content-type": "text/plain; charset=ISO-8859-2" },
+ },
+ { senderCol: "John Doe Å‘" },
+ ],
+ [
+ {
+ from: "=?UTF-8?Q?H=C3=A5s=C3=A4ther=2C_David?= <db@null.invalid>",
+ clobberHeaders: { "Content-type": "text/plain; charset=ISO-8859-2" },
+ },
+ { senderCol: "Håsäther, David" },
+ ],
+];
+
+add_task(async function test_nsMsgDBView_headValues() {
+ // Add the messages to the folder
+ let msgGenerator = new MessageGenerator();
+ let genMessages = tests.map(data => msgGenerator.makeMessage(data[0]));
+ let folder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders(
+ [folder],
+ [new SyntheticMessageSet(genMessages)]
+ );
+
+ // Make the DB view
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=threaded";
+ let dbView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ dbView.init(null, null, null);
+ let outCount = {};
+ dbView.open(
+ folder,
+ Ci.nsMsgViewSortType.byDate,
+ Ci.nsMsgViewSortOrder.ascending,
+ 0,
+ outCount
+ );
+
+ // Did we add all the messages properly?
+ let treeView = dbView.QueryInterface(Ci.nsITreeView);
+ Assert.equal(treeView.rowCount, tests.length);
+
+ // For each test, make sure that the display is correct.
+ tests.forEach(function (data, i) {
+ info("Checking data for " + uneval(data));
+ let expected = data[1];
+ for (let column in expected) {
+ Assert.equal(dbView.cellTextForColumn(i, column), expected[column]);
+ }
+ });
+});
diff --git a/comm/mailnews/base/test/unit/test_nsMsgMailSession_Alerts.js b/comm/mailnews/base/test/unit/test_nsMsgMailSession_Alerts.js
new file mode 100644
index 0000000000..61c1cf3fe9
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgMailSession_Alerts.js
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsMsgMailSession functions relating to alerts and their
+ * listeners.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var gDialogTitle = null;
+var gText = null;
+
+function reset() {
+ gDialogTitle = null;
+ gText = null;
+}
+
+/* exported alert */
+// Used in alertTestUtils.
+function alert(aDialogTitle, aText) {
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ gDialogTitle = aDialogTitle;
+ gText = aText;
+}
+
+var msgWindow = {
+ get promptDialog() {
+ return alertUtilsPrompts;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgWindow"]),
+};
+
+var msgUrl = {
+ _msgWindow: null,
+
+ get msgWindow() {
+ return this._msgWindow;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgMailNewsUrl"]),
+};
+
+function alertListener() {}
+
+alertListener.prototype = {
+ mReturn: false,
+ mMessage: null,
+ mMsgWindow: null,
+
+ reset() {
+ this.mMessage = null;
+ this.mMsgWindow = null;
+ },
+
+ onAlert(aMessage, aMsgWindow) {
+ Assert.equal(this.mMessage, null);
+ Assert.equal(this.mMsgWindow, null);
+
+ this.mMessage = aMessage;
+ this.mMsgWindow = aMsgWindow;
+
+ return this.mReturn;
+ },
+ QueryInferface: ChromeUtils.generateQI([Ci.nsIMsgMailNewsUrl]),
+};
+
+function run_test() {
+ // Test - No listeners, check alert tries to alert the user.
+
+ reset();
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("test message", msgUrl);
+
+ // The dialog title doesn't get set at the moment.
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, "test message");
+
+ // Test - No listeners and no msgWindow, check no alerts.
+
+ reset();
+
+ msgUrl._msgWindow = null;
+
+ MailServices.mailSession.alertUser("test no message", msgUrl);
+
+ // The dialog title doesn't get set at the moment.
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ // Test - One listener, returning false (prompt should still happen).
+
+ reset();
+
+ var listener1 = new alertListener();
+ listener1.mReturn = false;
+
+ MailServices.mailSession.addUserFeedbackListener(listener1);
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("message test", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, "message test");
+
+ Assert.equal(listener1.mMessage, "message test");
+ Assert.notEqual(listener1.mMsgWindow, null);
+
+ // Test - One listener, returning false, no msg window (prompt shouldn't
+ // happen).
+
+ reset();
+ listener1.reset();
+
+ MailServices.mailSession.alertUser("message test no prompt", null);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ Assert.equal(listener1.mMessage, "message test no prompt");
+ Assert.equal(listener1.mMsgWindow, null);
+
+ // Test - Two listeners, both returning false (prompt should happen).
+
+ reset();
+ listener1.reset();
+
+ var listener2 = new alertListener();
+ listener2.mReturn = false;
+
+ MailServices.mailSession.addUserFeedbackListener(listener2);
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("two listeners", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, "two listeners");
+
+ Assert.equal(listener1.mMessage, "two listeners");
+ Assert.notEqual(listener1.mMsgWindow, null);
+
+ Assert.equal(listener2.mMessage, "two listeners");
+ Assert.notEqual(listener2.mMsgWindow, null);
+
+ // Test - Two listeners, one returning true (prompt shouldn't happen).
+
+ reset();
+ listener1.reset();
+ listener2.reset();
+
+ listener2.mReturn = true;
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("no prompt", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ Assert.equal(listener1.mMessage, "no prompt");
+ Assert.notEqual(listener1.mMsgWindow, null);
+
+ Assert.equal(listener2.mMessage, "no prompt");
+ Assert.notEqual(listener2.mMsgWindow, null);
+
+ // Test - Remove a listener.
+
+ reset();
+ listener1.reset();
+ listener2.reset();
+
+ MailServices.mailSession.removeUserFeedbackListener(listener1);
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("remove listener", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, null);
+
+ Assert.equal(listener1.mMessage, null);
+ Assert.equal(listener1.mMsgWindow, null);
+
+ Assert.equal(listener2.mMessage, "remove listener");
+ Assert.notEqual(listener2.mMsgWindow, null);
+
+ // Test - Remove the other listener.
+
+ reset();
+ listener1.reset();
+ listener2.reset();
+
+ MailServices.mailSession.removeUserFeedbackListener(listener2);
+
+ msgUrl._msgWindow = msgWindow;
+
+ MailServices.mailSession.alertUser("no listeners", msgUrl);
+
+ Assert.equal(gDialogTitle, null);
+ Assert.equal(gText, "no listeners");
+
+ Assert.equal(listener1.mMessage, null);
+ Assert.equal(listener1.mMsgWindow, null);
+
+ Assert.equal(listener2.mMessage, null);
+ Assert.equal(listener2.mMsgWindow, null);
+}
diff --git a/comm/mailnews/base/test/unit/test_nsMsgMailSession_Listeners.js b/comm/mailnews/base/test/unit/test_nsMsgMailSession_Listeners.js
new file mode 100644
index 0000000000..e6fa785d79
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgMailSession_Listeners.js
@@ -0,0 +1,161 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for nsMsgMailSession functions relating to listeners.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var numListenerFunctions = 8;
+
+// The MailSession also implements nsIFolderListener - used to relay
+// notifications onward to all the registered listeners.
+var gMailSessionNotifier = MailServices.mailSession.QueryInterface(
+ Ci.nsIFolderListener
+);
+
+var gFLAll;
+var gFLSingle = new Array(numListenerFunctions);
+
+function fL() {}
+
+fL.prototype = {
+ mReceived: 0,
+ mAutoRemoveItem: false,
+
+ onFolderAdded(parentFolder, child) {
+ this.mReceived |= Ci.nsIFolderListener.added;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onMessageAdded(parentFolder, msg) {
+ this.mReceived |= Ci.nsIFolderListener.added;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderRemoved(parentFolder, child) {
+ this.mReceived |= Ci.nsIFolderListener.removed;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onMessageRemoved(parentFolder, msg) {
+ this.mReceived |= Ci.nsIFolderListener.removed;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderPropertyChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.propertyChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderIntPropertyChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.intPropertyChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderBoolPropertyChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.boolPropertyChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderUnicharPropertyChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.unicharPropertyChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderPropertyFlagChanged(item, property, oldValue, newValue) {
+ this.mReceived |= Ci.nsIFolderListener.propertyFlagChanged;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+ onFolderEvent(parentItem, item) {
+ this.mReceived |= Ci.nsIFolderListener.event;
+ if (this.mAutoRemoveItem) {
+ MailServices.mailSession.RemoveFolderListener(this);
+ }
+ },
+};
+
+function NotifyMailSession() {
+ gMailSessionNotifier.onFolderAdded(null, null);
+ gMailSessionNotifier.onMessageAdded(null, null);
+ gMailSessionNotifier.onFolderRemoved(null, null);
+ gMailSessionNotifier.onMessageRemoved(null, null);
+ gMailSessionNotifier.onFolderPropertyChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderIntPropertyChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderBoolPropertyChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderUnicharPropertyChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderPropertyFlagChanged(null, null, null, null);
+ gMailSessionNotifier.onFolderEvent(null, null);
+}
+
+function run_test() {
+ var i;
+
+ Assert.ok(MailServices.mailSession != null);
+
+ // Test - Add a listener
+
+ gFLAll = new fL();
+
+ MailServices.mailSession.AddFolderListener(gFLAll, Ci.nsIFolderListener.all);
+
+ for (i = 0; i < numListenerFunctions; ++i) {
+ gFLSingle[i] = new fL();
+ MailServices.mailSession.AddFolderListener(gFLSingle[i], Math.pow(2, i));
+ }
+
+ // Test - Notify listener on all available items
+
+ NotifyMailSession();
+
+ Assert.equal(gFLAll.mReceived, Math.pow(2, numListenerFunctions) - 1);
+ gFLAll.mReceived = 0;
+
+ for (i = 0; i < numListenerFunctions; ++i) {
+ Assert.equal(gFLSingle[i].mReceived, Math.pow(2, i));
+ gFLSingle[i].mReceived = 0;
+
+ // And prepare for test 3.
+ gFLSingle[i].mAutoRemoveItem = true;
+ }
+
+ // Test - Remove Single Listeners as we go through the functions
+
+ // Check the for loop above for changes to the single listeners.
+
+ NotifyMailSession();
+
+ Assert.equal(gFLAll.mReceived, Math.pow(2, numListenerFunctions) - 1);
+ gFLAll.mReceived = 0;
+
+ for (i = 0; i < numListenerFunctions; ++i) {
+ Assert.equal(gFLSingle[i].mReceived, Math.pow(2, i));
+ gFLSingle[i].mReceived = 0;
+ }
+
+ // Test - Ensure the single listeners have been removed.
+
+ NotifyMailSession();
+
+ Assert.equal(gFLAll.mReceived, Math.pow(2, numListenerFunctions) - 1);
+ gFLAll.mReceived = 0;
+
+ for (i = 0; i < numListenerFunctions; ++i) {
+ Assert.equal(gFLSingle[i].mReceived, 0);
+ }
+
+ // Test - Remove main listener
+
+ MailServices.mailSession.RemoveFolderListener(gFLAll);
+}
diff --git a/comm/mailnews/base/test/unit/test_nsMsgTraitService.js b/comm/mailnews/base/test/unit/test_nsMsgTraitService.js
new file mode 100644
index 0000000000..f75b76ada6
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_nsMsgTraitService.js
@@ -0,0 +1,130 @@
+/* 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 ts = Cc["@mozilla.org/msg-trait-service;1"].getService(
+ Ci.nsIMsgTraitService
+);
+
+// junk-related traits set by default
+var kJunkId = "mailnews@mozilla.org#junk";
+var kGoodId = "mailnews@mozilla.org#good";
+var kGoodIndex = Ci.nsIJunkMailPlugin.GOOD_TRAIT;
+var kJunkIndex = Ci.nsIJunkMailPlugin.JUNK_TRAIT;
+
+// a dummy set of traits
+var proId = "TheProTrait";
+var proName = "ProName";
+var antiId = "TheAntiTrait";
+
+function run_test() {
+ // Check lastIndex prior to adding, 3 - 1000 are reserved for mailnews
+ Assert.equal(ts.lastIndex, 1000);
+
+ // basic junk as traits should be setup automatically
+ Assert.equal(
+ kGoodId,
+ Services.prefs.getCharPref("mailnews.traits.id." + kGoodIndex)
+ );
+ Assert.equal(
+ kJunkId,
+ Services.prefs.getCharPref("mailnews.traits.id." + kJunkIndex)
+ );
+ Assert.equal(
+ kGoodId,
+ Services.prefs.getCharPref("mailnews.traits.antiId." + kJunkIndex)
+ );
+ Assert.ok(
+ Services.prefs.getBoolPref("mailnews.traits.enabled." + kJunkIndex)
+ );
+
+ // add the pro and anti test traits
+ Assert.ok(!ts.isRegistered(proId));
+ var proIndex = ts.registerTrait(proId);
+ Assert.ok(ts.isRegistered(proId));
+ Assert.equal(proIndex, 1001);
+ Assert.equal(proIndex, ts.getIndex(proId));
+ Assert.equal(proId, ts.getId(proIndex));
+ var antiIndex = ts.registerTrait(antiId);
+ Assert.equal(proIndex, 1001);
+ Assert.equal(antiIndex, 1002);
+
+ // check setting and getting things through the service
+ ts.setName(proId, proName);
+ Assert.equal(proName, ts.getName(proId));
+ Assert.ok(!ts.getEnabled(proId));
+ ts.setEnabled(proId, true);
+ Assert.ok(ts.getEnabled(proId));
+ ts.setAntiId(proId, antiId);
+ Assert.equal(antiId, ts.getAntiId(proId));
+ let proArray = ts.getEnabledProIndices();
+ let antiArray = ts.getEnabledAntiIndices();
+ Assert.equal(proArray.length, 2);
+ Assert.equal(antiArray.length, 2);
+ Assert.equal(proArray[1], proIndex);
+ Assert.equal(antiArray[1], antiIndex);
+
+ // check of aliases
+ // add three random aliases
+ ts.addAlias(1, 501);
+ ts.addAlias(1, 502);
+ ts.addAlias(1, 601);
+ let aliases = ts.getAliases(1);
+ Assert.equal(aliases[0], 501);
+ Assert.equal(aliases[1], 502);
+ Assert.equal(aliases[2], 601);
+
+ // remove the middle one
+ ts.removeAlias(1, 502);
+ aliases = ts.getAliases(1);
+ Assert.equal(aliases.length, 2);
+ Assert.equal(aliases[0], 501);
+ Assert.equal(aliases[1], 601);
+
+ // try to add an existing value
+ ts.addAlias(1, 501);
+ aliases = ts.getAliases(1);
+ Assert.equal(aliases.length, 2);
+ Assert.equal(aliases[0], 501);
+ Assert.equal(aliases[1], 601);
+
+ // now let's make sure this got saved in preferences
+ Assert.equal(
+ proId,
+ Services.prefs.getCharPref("mailnews.traits.id." + proIndex)
+ );
+ Assert.equal(
+ proName,
+ Services.prefs.getCharPref("mailnews.traits.name." + proIndex)
+ );
+ Assert.ok(Services.prefs.getBoolPref("mailnews.traits.enabled." + proIndex));
+ Assert.equal(
+ antiId,
+ Services.prefs.getCharPref("mailnews.traits.antiId." + proIndex)
+ );
+
+ // remove the pro trait
+ ts.unRegisterTrait(proId);
+ Assert.ok(!ts.isRegistered(proId));
+
+ // check that this is also removed from prefs. The get calls should fail
+ try {
+ Services.prefs.getCharPref("mailnews.traits.id." + proIndex);
+ Assert.ok(false);
+ } catch (e) {}
+
+ try {
+ Services.prefs.getCharPref("mailnews.traits.name." + proIndex);
+ Assert.ok(false);
+ } catch (e) {}
+
+ try {
+ Services.prefs.getBoolPref("mailnews.traits.enabled." + proIndex);
+ Assert.ok(false);
+ } catch (e) {}
+
+ try {
+ Services.prefs.getCharPref("mailnews.traits.antiId." + proIndex);
+ Assert.ok(false);
+ } catch (e) {}
+}
diff --git a/comm/mailnews/base/test/unit/test_postPluginFilters.js b/comm/mailnews/base/test/unit/test_postPluginFilters.js
new file mode 100644
index 0000000000..de75307886
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_postPluginFilters.js
@@ -0,0 +1,223 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * tests post-plugin message filters as implemented in bug 198100
+ */
+
+/* import-globals-from ../../../test/resources/POP3pump.js */
+load("../../../resources/POP3pump.js");
+
+// Globals
+
+var gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+);
+
+// command functions for test data
+var kTrain = 0; // train a file as a trait
+var kClass = 1; // classify files with traits
+
+var gTest; // currently active test
+var gMsgHdr; // current message header
+
+var kJunkFile = "../../../data/bugmail1";
+var kGoodFile = "../../../data/draft1";
+
+var kPriorityLow = 3;
+var kPriorityHigh = 5;
+var gInboxListener; // database listener object
+
+var gTests = [
+ {
+ // train two different messages
+ command: kTrain,
+ fileName: kGoodFile,
+ traitId: MailServices.junk.GOOD_TRAIT,
+ },
+ {
+ command: kTrain,
+ fileName: kJunkFile,
+ traitId: MailServices.junk.JUNK_TRAIT,
+ },
+ {
+ // test a filter that acts on GOOD messages
+ command: kClass,
+ fileName: kGoodFile,
+ test() {
+ Assert.equal(kPriorityHigh, gMsgHdr.priority);
+ },
+ },
+ {
+ // test a filter that acts on JUNK messages
+ command: kClass,
+ fileName: kJunkFile,
+ test() {
+ Assert.equal(kPriorityLow, gMsgHdr.priority);
+ },
+ },
+];
+
+// main test
+function run_test() {
+ // Setup some incoming filters, setting junk priority low, and good high.
+
+ // Can't use the fake server, must use the deferredTo local server!
+ let filterList = localAccountUtils.incomingServer.getFilterList(null);
+
+ // junkIsLow filter
+ let filter = filterList.createFilter("junkIsLow");
+ let searchTerm = filter.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.JunkStatus;
+ let value = searchTerm.value;
+ value.attrib = Ci.nsMsgSearchAttrib.JunkStatus;
+ value.junkStatus = MailServices.junk.JUNK;
+ searchTerm.value = value;
+ searchTerm.op = Ci.nsMsgSearchOp.Is;
+ searchTerm.booleanAnd = false;
+ filter.appendTerm(searchTerm);
+ let action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.ChangePriority;
+ action.priority = kPriorityLow;
+ filter.appendAction(action);
+ filter.filterType = Ci.nsMsgFilterType.PostPlugin;
+ filter.enabled = true;
+ filterList.insertFilterAt(0, filter);
+
+ // goodIsHigh filter
+ filter = filterList.createFilter("goodIsHigh");
+ searchTerm = filter.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.JunkStatus;
+ value = searchTerm.value;
+ value.attrib = Ci.nsMsgSearchAttrib.JunkStatus;
+ value.junkStatus = MailServices.junk.GOOD;
+ searchTerm.value = value;
+ searchTerm.op = Ci.nsMsgSearchOp.Is;
+ searchTerm.booleanAnd = false;
+ filter.appendTerm(searchTerm);
+ action = filter.createAction();
+ action.type = Ci.nsMsgFilterAction.ChangePriority;
+ action.priority = kPriorityHigh;
+ filter.appendAction(action);
+ filter.filterType = Ci.nsMsgFilterType.PostPlugin;
+ filter.enabled = true;
+ filterList.insertFilterAt(1, filter);
+
+ // setup a db listener to grab the message headers. There's probably an
+ // easier way, but this works.
+ gInboxListener = new DBListener();
+ gDbService.registerPendingListener(
+ localAccountUtils.inboxFolder,
+ gInboxListener
+ );
+
+ do_test_pending();
+
+ startCommand();
+}
+
+function endTest() {
+ // Cleanup
+ dump(" Exiting mail tests\n");
+ if (gInboxListener) {
+ gDbService.unregisterPendingListener(gInboxListener);
+ }
+
+ gPOP3Pump = null;
+
+ do_test_finished(); // for the one in run_test()
+}
+
+var classifyListener = {
+ // nsIMsgTraitClassificationListener implementation
+ onMessageTraitsClassified(aMsgURI, aTraits, aPercents) {
+ // print("Message URI is " + aMsgURI);
+ if (!aMsgURI) {
+ // Ignore end-of-batch signal.
+ return;
+ }
+
+ startCommand();
+ },
+};
+
+/** @implements {nsIDBChangeListener} */
+function DBListener() {}
+
+DBListener.prototype = {
+ onHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, aInstigator) {},
+
+ onHdrDeleted(aHdrChanged, aParentKey, Flags, aInstigator) {},
+
+ onHdrAdded(aHdrChanged, aParentKey, aFlags, aInstigator) {
+ gMsgHdr = aHdrChanged;
+ },
+
+ onParentChanged(aKeyChanged, oldParent, newParent, aInstigator) {},
+
+ onAnnouncerGoingAway(instigator) {
+ if (gInboxListener) {
+ try {
+ POP3Pump.inbox.msgDatabase.removeListener(gInboxListener);
+ } catch (e) {
+ dump("listener not found\n");
+ }
+ }
+ },
+
+ onReadChanged(aInstigator) {},
+
+ onJunkScoreChanged(aInstigator) {},
+
+ onHdrPropertyChanged(aHdrToChange, aPreChange, aStatus, aInstigator) {},
+ onEvent(aDB, aEvent) {},
+};
+
+// start the next test command
+function startCommand() {
+ if (gTest && gTest.test) {
+ dump("doing test " + gTest.test.name + "\n");
+ gTest.test();
+ }
+ if (!gTests.length) {
+ // Do we have more commands?
+ // no, all done
+ endTest();
+ return;
+ }
+
+ gTest = gTests.shift();
+ switch (gTest.command) {
+ case kTrain:
+ // train message
+ var proArray = [];
+ proArray.push(gTest.traitId);
+
+ MailServices.junk.setMsgTraitClassification(
+ getSpec(gTest.fileName), // aMsgURI
+ [], // aOldTraits
+ proArray, // aNewTraits
+ classifyListener // aTraitListener
+ );
+ // null, // [optional] in nsIMsgWindow aMsgWindow
+ // null, // [optional] in nsIJunkMailClassificationListener aJunkListener
+ break;
+
+ case kClass:
+ // classify message
+ gPOP3Pump.files = [gTest.fileName];
+ gPOP3Pump.onDone = function () {
+ do_timeout(100, startCommand);
+ };
+ gPOP3Pump.run();
+ break;
+ }
+}
+
+function getSpec(aFileName) {
+ var file = do_get_file(aFileName);
+ var uri = Services.io.newFileURI(file).QueryInterface(Ci.nsIURL);
+ uri = uri.mutate().setQuery("type=application/x-message-display").finalize();
+ return uri.spec;
+}
diff --git a/comm/mailnews/base/test/unit/test_retention.js b/comm/mailnews/base/test/unit/test_retention.js
new file mode 100644
index 0000000000..38038015b1
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_retention.js
@@ -0,0 +1,66 @@
+/* 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/. */
+
+/*
+ * Simple tests for retention settings. In particular, we'd like to make
+ * sure that applying retention settings works with the new code that avoids
+ * opening db's to apply retention settings if the folder doesn't override
+ * the server defaults.
+ */
+
+var { MessageGenerator, MessageScenarioFactory, SyntheticMessageSet } =
+ ChromeUtils.import("resource://testing-common/mailnews/MessageGenerator.jsm");
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+
+var gMessageGenerator = new MessageGenerator();
+var gScenarioFactory = new MessageScenarioFactory(gMessageGenerator);
+var messageInjection = new MessageInjection({ mode: "local" });
+
+var gTestFolder;
+
+add_setup(async function () {
+ // Add 10 messages
+ let messages = [];
+ messages = messages.concat(gScenarioFactory.directReply(10));
+
+ let msgSet = new SyntheticMessageSet(messages);
+
+ gTestFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+});
+
+add_task(function test_retention() {
+ let numMessages = 10;
+ gTestFolder.msgDatabase = null;
+ gTestFolder.applyRetentionSettings();
+ const gDbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService(
+ Ci.nsIMsgDBService
+ );
+ // adding messages leaves some headers around as garbage - make sure
+ // those are cleaned up so the db will get closed.
+ Cu.forceGC();
+ Cu.forceCC();
+ Assert.equal(gDbService.cachedDBForFolder(gTestFolder), null);
+ // no retention settings, so we should have the same number of messages.
+ Assert.equal(numMessages, gTestFolder.msgDatabase.dBFolderInfo.numMessages);
+ let serverSettings = gTestFolder.server.retentionSettings;
+ serverSettings.retainByPreference =
+ Ci.nsIMsgRetentionSettings.nsMsgRetainByNumHeaders;
+ serverSettings.numHeadersToKeep = 9;
+ gTestFolder.server.retentionSettings = serverSettings;
+ gTestFolder.applyRetentionSettings();
+ // no retention settings, so we should have the same number of messages.
+ Assert.equal(9, gTestFolder.msgDatabase.dBFolderInfo.numMessages);
+ let folderSettings = gTestFolder.retentionSettings;
+ folderSettings.retainByPreference =
+ Ci.nsIMsgRetentionSettings.nsMsgRetainByNumHeaders;
+ folderSettings.numHeadersToKeep = 8;
+ folderSettings.useServerDefaults = false;
+ gTestFolder.retentionSettings = folderSettings;
+ gTestFolder.applyRetentionSettings();
+ // no retention settings, so we should have the same number of messages.
+ Assert.equal(8, gTestFolder.msgDatabase.dBFolderInfo.numMessages);
+});
diff --git a/comm/mailnews/base/test/unit/test_saveAs.js b/comm/mailnews/base/test/unit/test_saveAs.js
new file mode 100644
index 0000000000..39263d3854
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_saveAs.js
@@ -0,0 +1,172 @@
+/* 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/. */
+
+/**
+ * Invokes nsMessenger::saveAs with the bypass of the File Picker to check if
+ * the saved file as .eml .html or .txt contains certain strings (header, body, ...).
+ *
+ * See `checkedContent` for the compared strings.
+ */
+
+var { MessageGenerator } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { ImapMessage } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Imapd.jsm"
+);
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+var { FileTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/FileTestUtils.sys.mjs"
+);
+
+var messenger = Cc["@mozilla.org/messenger;1"].createInstance(Ci.nsIMessenger);
+
+setupIMAPPump();
+
+registerCleanupFunction(function () {
+ teardownIMAPPump();
+});
+
+/**
+ * Creates a SyntheticMessage and prepares it for loading it
+ * into a fake IMAP inbox.
+ *
+ * @returns {object[]}
+ * [0] is an {ImapMessage}
+ * [1] is an {SyntheticMessage}
+ */
+async function createMessage() {
+ let gMessageGenerator = new MessageGenerator();
+ let synthMessage = gMessageGenerator.makeMessage();
+
+ let msgURI = Services.io.newURI(
+ "data:text/plain;base64," + btoa(synthMessage.toMessageString())
+ );
+ let imapInbox = IMAPPump.daemon.getMailbox("INBOX");
+ let ImapMessageFromSynthMsg = new ImapMessage(
+ msgURI.spec,
+ imapInbox.uidnext++,
+ []
+ );
+ return [ImapMessageFromSynthMsg, synthMessage];
+}
+
+/**
+ * Adds a fake msg to a fake IMAP.
+ *
+ * @param {ImapMessage} fooMessage
+ */
+async function addImapMessage(fooMessage) {
+ IMAPPump.mailbox.addMessage(fooMessage);
+ let promiseUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, promiseUrlListener);
+ await promiseUrlListener.promise;
+}
+
+/**
+ * Saves the file and waits until its able to load.
+ * nsMessenger doesn't have an event for the loaded File.
+ *
+ * @param {string} fileEnding
+ * @returns {string}
+ */
+async function saveAndLoad(fileEnding) {
+ // getTempFile guarantees that the file doesn't exist.
+ let tmpFile = FileTestUtils.getTempFile(`someprefix${fileEnding}`);
+ Assert.ok(
+ tmpFile.path.endsWith(fileEnding),
+ "Sanity check if the file ending is intact"
+ );
+
+ // Get the ImapMessage.
+ let hdr = mailTestUtils.firstMsgHdr(IMAPPump.inbox);
+ let uri = IMAPPump.inbox.getUriForMsg(hdr);
+
+ // nsMessenger::saveAs
+ messenger.saveAs(uri, true, null, tmpFile.path, true);
+ info(`File saved at ${tmpFile.path}`);
+
+ await TestUtils.waitForCondition(
+ () => IOUtils.exists(tmpFile.path),
+ "wait for nsMessenger::saveAs file exists"
+ );
+ let fileContent = await IOUtils.readUTF8(tmpFile.path);
+ return fileContent;
+}
+
+/**
+ * All of these strings must appear in the saved file.
+ *
+ * @param {SyntheticMessage} synthMessage
+ * This message is the original message.
+ * @returns {object}
+ */
+function checkedContent(synthMessage) {
+ return {
+ // Skip dateCheck because of Formatting and Timezone.
+ // date: synthMessage.date.toString(),
+ fromName: synthMessage.from[0],
+ fromEmail: synthMessage.from[1],
+ subject: synthMessage.subject,
+ toName: synthMessage.toName,
+ toAddress: synthMessage.toAddress,
+ body: synthMessage.bodyPart.body,
+ };
+}
+
+async function emlTest(synthMessage) {
+ let loadedFileContent = await saveAndLoad(".eml");
+ let messageParts = checkedContent(synthMessage);
+ for (const msgPart in messageParts) {
+ Assert.stringContains(
+ loadedFileContent,
+ messageParts[msgPart],
+ `nsMessenger::saveAs with .eml should contain ${msgPart}`
+ );
+ }
+}
+
+async function htmlTest(synthMessage) {
+ let loadedFileContent = await saveAndLoad(".html");
+ let messageParts = checkedContent(synthMessage);
+ for (const msgPart in messageParts) {
+ Assert.stringContains(
+ loadedFileContent,
+ messageParts[msgPart],
+ `nsMessenger::saveAs with .html should contain ${msgPart}`
+ );
+ }
+}
+
+async function txtTest(synthMessage) {
+ let loadedFileContent = await saveAndLoad(".txt");
+ let messageParts = checkedContent(synthMessage);
+ for (const msgPart in messageParts) {
+ Assert.stringContains(
+ loadedFileContent,
+ messageParts[msgPart],
+ `nsMessenger::saveAs with .txt should contain ${msgPart}`
+ );
+ }
+}
+
+add_task(async function test_saveAs() {
+ let [fakedImapMessage, synthMessage] = await createMessage();
+ await addImapMessage(fakedImapMessage);
+ await emlTest(synthMessage);
+ await txtTest(synthMessage);
+ await htmlTest(synthMessage);
+});
diff --git a/comm/mailnews/base/test/unit/test_testsuite_base64.js b/comm/mailnews/base/test/unit/test_testsuite_base64.js
new file mode 100644
index 0000000000..a6faae3640
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_testsuite_base64.js
@@ -0,0 +1,22 @@
+/**
+ * Tests functions atob() and btoa() in mailnews/test/resources/MailTestUtils.jsm .
+ *
+ * Note:
+ * btoa() = base64 encode
+ * atob() = base64 decode
+ * (i.e. "binary" = plain, and "ascii" = encoded)
+ */
+
+function run_test() {
+ var plain = "testtesttest";
+ var encoded = "dGVzdHRlc3R0ZXN0";
+
+ // correct encoding according to spec
+ Assert.equal(btoa(plain), encoded); // encode
+ Assert.equal(atob(encoded), plain); // decode
+
+ // roundtrip works
+ Assert.equal(atob(btoa(plain)), plain);
+ Assert.equal(btoa(atob(encoded)), encoded);
+ return true;
+}
diff --git a/comm/mailnews/base/test/unit/test_testsuite_fakeserverAuth.js b/comm/mailnews/base/test/unit/test_testsuite_fakeserverAuth.js
new file mode 100644
index 0000000000..02181ae20b
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_testsuite_fakeserverAuth.js
@@ -0,0 +1,58 @@
+/**
+ * Tests functions in mailnews/test/fakeserver/Auth.jsm
+ * which are responsible for the authentication in the
+ * fakeserver.
+ *
+ * Do NOT essentially re-code the auth schemes here,
+ * just check roundtrips, against static values etc..
+ */
+
+var { AuthPLAIN, AuthCRAM } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Auth.jsm"
+);
+
+var kUsername = "fred1";
+var kPassword = "wilma2";
+
+function run_test() {
+ authPLAIN();
+ authCRAMMD5();
+ return true;
+}
+
+/**
+ * Test AUTH PLAIN
+ */
+function authPLAIN() {
+ // roundtrip works
+ var line = AuthPLAIN.encodeLine(kUsername, kPassword);
+ var req = AuthPLAIN.decodeLine(line);
+ Assert.equal(req.username, kUsername);
+ Assert.equal(req.password, kPassword);
+
+ // correct encoding
+ Assert.equal(line, "AGZyZWQxAHdpbG1hMg==");
+}
+
+/**
+ * Test AUTH CRAM-MD5
+ */
+function authCRAMMD5() {
+ // AuthCRAM.createChallenge() creates a different challenge each time
+ var hardcodedChallenge = btoa("<123@fake.invalid>");
+ var hardcodedResponse =
+ "ZnJlZDEgOTA5YjgwMmM3NTI5NTJlYzI2NjgyMTNmYTdjNWU0ZjQ=";
+
+ // correct encoding
+ var req = AuthCRAM.decodeLine(hardcodedResponse);
+ Assert.equal(req.username, kUsername);
+ var expectedDigest = AuthCRAM.encodeCRAMMD5(hardcodedChallenge, kPassword);
+ Assert.equal(req.digest, expectedDigest);
+
+ var challenge = AuthCRAM.createChallenge("fake.invalid");
+ challenge = atob(challenge); // decode. function currently returns it already encoded
+ var challengeSplit = challenge.split("@");
+ Assert.equal(challengeSplit.length, 2);
+ Assert.equal(challengeSplit[1], "fake.invalid>");
+ Assert.equal(challengeSplit[0][0], "<");
+}
diff --git a/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_gmail.js b/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_gmail.js
new file mode 100644
index 0000000000..a9eb9ae2d7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_gmail.js
@@ -0,0 +1,92 @@
+/* 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/. */
+
+// Test that Imapd.jsm fakeserver correctly emulates GMail server
+// That means X-GM-EXT-1 capability and GMail flavor XLIST
+// per https://developers.google.com/google-apps/gmail/imap_extensions
+
+// IMAP pump
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+setupIMAPPump("GMail");
+// create our own handler so that we can call imapd functions directly
+var handler;
+
+add_setup(function () {
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+});
+
+// mbox mailboxes cannot contain both child mailboxes and messages, so this will
+// be one test case.
+add_setup(async function () {
+ IMAPPump.mailbox.specialUseFlag = "\\Inbox";
+ IMAPPump.daemon.createMailbox("[Gmail]", { flags: ["\\Noselect"] });
+ IMAPPump.daemon.createMailbox("[Gmail]/All Mail", {
+ specialUseFlag: "\\AllMail",
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Drafts", {
+ specialUseFlag: "\\Drafts",
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Sent", { specialUseFlag: "\\Sent" });
+ IMAPPump.daemon.createMailbox("[Gmail]/Spam", { specialUseFlag: "\\Spam" });
+ IMAPPump.daemon.createMailbox("[Gmail]/Starred", {
+ specialUseFlag: "\\Starred",
+ });
+ IMAPPump.daemon.createMailbox("[Gmail]/Trash", { specialUseFlag: "\\Trash" });
+ IMAPPump.daemon.createMailbox("test", {});
+
+ handler = IMAPPump.server._handlerCreator(IMAPPump.daemon);
+ let response = handler.onError("1", "LOGIN user password");
+ Assert.ok(response.includes("OK"));
+ // wait for imap pump to do its thing or else we get memory leaks
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// test that 'XLIST "" "*"' returns the proper responses
+add_task(function testXlist() {
+ let response = handler.onError("2", 'XLIST "" "*"');
+
+ Assert.ok(response.includes('* LIST (\\HasNoChildren \\Inbox) "/" "INBOX"'));
+ Assert.ok(
+ response.includes('* LIST (\\Noselect \\HasChildren) "/" "[Gmail]"')
+ );
+ Assert.ok(
+ response.includes(
+ '* LIST (\\HasNoChildren \\AllMail) "/" "[Gmail]/All Mail"'
+ )
+ );
+ Assert.ok(
+ response.includes('* LIST (\\HasNoChildren \\Drafts) "/" "[Gmail]/Drafts"')
+ );
+ Assert.ok(
+ response.includes('* LIST (\\HasNoChildren \\Sent) "/" "[Gmail]/Sent"')
+ );
+ Assert.ok(
+ response.includes('* LIST (\\HasNoChildren \\Spam) "/" "[Gmail]/Spam"')
+ );
+ Assert.ok(
+ response.includes(
+ '* LIST (\\HasNoChildren \\Starred) "/" "[Gmail]/Starred"'
+ )
+ );
+ Assert.ok(
+ response.includes('* LIST (\\HasNoChildren \\Trash) "/" "[Gmail]/Trash"')
+ );
+ Assert.ok(response.includes('* LIST (\\HasNoChildren) "/" "test"'));
+});
+
+// Cleanup at end
+add_task(function endTest() {
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_list-extended.js b/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_list-extended.js
new file mode 100644
index 0000000000..89214b2b51
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_testsuite_fakeserver_imapd_list-extended.js
@@ -0,0 +1,150 @@
+/* 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/. */
+
+// Test that Imapd.jsm fakeserver correctly implements LIST-EXTENDED imap
+// extension (RFC 5258 - http://tools.ietf.org/html/rfc5258)
+
+// IMAP pump
+var { IMAPPump, setupIMAPPump, teardownIMAPPump } = ChromeUtils.import(
+ "resource://testing-common/mailnews/IMAPpump.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+// Globals
+
+// Dovecot is one of the servers that supports LIST-EXTENDED
+setupIMAPPump("Dovecot");
+// create our own handler so that we can call imapd functions directly
+var handler;
+
+add_setup(function () {
+ Services.prefs.setBoolPref(
+ "mail.server.server1.autosync_offline_stores",
+ false
+ );
+});
+
+// mbox mailboxes cannot contain both child mailboxes and messages, so this will
+// be one test case.
+add_setup(async function () {
+ IMAPPump.mailbox.flags = ["\\Marked", "\\NoInferiors"];
+ IMAPPump.mailbox.subscribed = true;
+ IMAPPump.daemon.createMailbox("Fruit", {});
+ IMAPPump.daemon.createMailbox("Fruit/Apple", {});
+ IMAPPump.daemon.createMailbox("Fruit/Banana", { subscribed: true });
+ IMAPPump.daemon.createMailbox("Fruit/Peach", {
+ nonExistent: true,
+ subscribed: true,
+ });
+ IMAPPump.daemon.createMailbox("Tofu", {});
+ IMAPPump.daemon.createMailbox("Vegetable", { subscribed: true });
+ IMAPPump.daemon.createMailbox("Vegetable/Broccoli", { subscribed: true });
+ IMAPPump.daemon.createMailbox("Vegetable/Corn", {});
+
+ handler = IMAPPump.server._handlerCreator(IMAPPump.daemon);
+ let response = handler.onError("1", "LOGIN user password");
+ Assert.ok(response.includes("OK"));
+ // wait for imap pump to do it's thing or else we get memory leaks
+ let listener = new PromiseTestUtils.PromiseUrlListener();
+ IMAPPump.inbox.updateFolderWithListener(null, listener);
+ await listener.promise;
+});
+
+// test that 'LIST "" "*"' returns the proper responses (standard LIST usage)
+add_task(function testList() {
+ let response = handler.onError("2", 'LIST "" "*"');
+
+ Assert.ok(response.includes('* LIST (\\Marked \\NoInferiors) "/" "INBOX"'));
+ Assert.ok(response.includes('* LIST () "/" "Fruit"'));
+ Assert.ok(response.includes('* LIST () "/" "Fruit/Apple"'));
+ Assert.ok(response.includes('* LIST () "/" "Fruit/Banana"'));
+ Assert.ok(response.includes('* LIST () "/" "Tofu"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Broccoli"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Corn"'));
+ Assert.ok(!response.includes("Peach"));
+});
+
+// test that 'LIST (SUBSCRIBED) "" "*"' returns the proper responses
+add_task(function testListSelectSubscribed() {
+ let response = handler.onError("3", 'LIST (SUBSCRIBED) "" "*"');
+
+ Assert.ok(
+ response.includes(
+ '* LIST (\\Marked \\NoInferiors \\Subscribed) "/" "INBOX"'
+ )
+ );
+ Assert.ok(response.includes('* LIST (\\Subscribed) "/" "Fruit/Banana"'));
+ Assert.ok(
+ response.includes('* LIST (\\Subscribed \\NonExistent) "/" "Fruit/Peach"')
+ );
+ Assert.ok(response.includes('* LIST (\\Subscribed) "/" "Vegetable"'));
+ Assert.ok(
+ response.includes('* LIST (\\Subscribed) "/" "Vegetable/Broccoli"')
+ );
+ Assert.ok(!response.includes('"Fruit"'));
+ Assert.ok(!response.includes("Apple"));
+ Assert.ok(!response.includes("Tofu"));
+ Assert.ok(!response.includes("Corn"));
+});
+
+// test that 'LIST "" "%" RETURN (CHILDEREN)' returns the proper responses
+add_task(function testListReturnChilderen() {
+ let response = handler.onError("4", 'LIST "" "%" RETURN (CHILDREN)');
+
+ Assert.ok(response.includes('* LIST (\\Marked \\NoInferiors) "/" "INBOX"'));
+ Assert.ok(response.includes('* LIST (\\HasChildren) "/" "Fruit"'));
+ Assert.ok(response.includes('* LIST (\\HasNoChildren) "/" "Tofu"'));
+ Assert.ok(response.includes('* LIST (\\HasChildren) "/" "Vegetable"'));
+ Assert.ok(!response.includes("Apple"));
+ Assert.ok(!response.includes("Banana"));
+ Assert.ok(!response.includes("Peach"));
+ Assert.ok(!response.includes("Broccoli"));
+ Assert.ok(!response.includes("Corn"));
+});
+
+// test that 'LIST "" "*" RETURN (SUBSCRIBED)' returns the proper responses
+add_task(function testListReturnSubscribed() {
+ let response = handler.onError("5", 'LIST "" "*" RETURN (SUBSCRIBED)');
+
+ Assert.ok(
+ response.includes(
+ '* LIST (\\Marked \\NoInferiors \\Subscribed) "/" "INBOX"'
+ )
+ );
+ Assert.ok(response.includes('* LIST () "/" "Fruit"'));
+ Assert.ok(response.includes('* LIST () "/" "Fruit/Apple"'));
+ Assert.ok(response.includes('* LIST (\\Subscribed) "/" "Fruit/Banana"'));
+ Assert.ok(response.includes('* LIST () "/" "Tofu"'));
+ Assert.ok(response.includes('* LIST (\\Subscribed) "/" "Vegetable"'));
+ Assert.ok(
+ response.includes('* LIST (\\Subscribed) "/" "Vegetable/Broccoli"')
+ );
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Corn"'));
+ Assert.ok(!response.includes("Peach"));
+});
+
+// test that 'LIST "" ("INBOX" "Tofu" "Vegetable/%")' returns the proper responses
+add_task(function testListSelectMultiple() {
+ let response = handler._dispatchCommand("LIST", [
+ "",
+ '("INBOX" "Tofu" "Vegetable/%")',
+ ]);
+
+ Assert.ok(response.includes('* LIST (\\Marked \\NoInferiors) "/" "INBOX"'));
+ Assert.ok(response.includes('* LIST () "/" "Tofu"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Broccoli"'));
+ Assert.ok(response.includes('* LIST () "/" "Vegetable/Corn"'));
+ Assert.ok(!response.includes('"Vegetable"'));
+ Assert.ok(!response.includes("Fruit"));
+ Assert.ok(!response.includes("Peach"));
+});
+
+// Cleanup at end
+add_task(function endTest() {
+ handler = null;
+ teardownIMAPPump();
+});
diff --git a/comm/mailnews/base/test/unit/test_viewSortByAddresses.js b/comm/mailnews/base/test/unit/test_viewSortByAddresses.js
new file mode 100644
index 0000000000..7235723bfd
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_viewSortByAddresses.js
@@ -0,0 +1,144 @@
+/* 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/. */
+
+/*
+ * Attempt to test nsMsgDBView's handling of sorting by sender/recipients
+ * when using a display name from the address book.
+ */
+
+/* import-globals-from ../../../test/resources/abSetup.js */
+load("../../../resources/abSetup.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MessageGenerator, SyntheticMessageSet } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageGenerator.jsm"
+);
+var { MessageInjection } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MessageInjection.jsm"
+);
+var { dump_view_contents } = ChromeUtils.import(
+ "resource://testing-common/mozmill/ViewHelpers.jsm"
+);
+
+var gMessageGenerator = new MessageGenerator();
+var messageInjection = new MessageInjection({ mode: "local" });
+
+Services.prefs.setBoolPref("mail.showCondensedAddresses", true);
+
+var gTestFolder;
+
+// Setup the display name to be opposite of alphabetic order of e-mail address.
+var cards = [
+ { email: "aaa@b.invalid", displayName: "4" },
+ { email: "ccc@d.invalid", displayName: "3" },
+ { email: "eee@f.invalid", displayName: "2" },
+ { email: "ggg@h.invalid", displayName: "1" },
+];
+
+add_setup(async function () {
+ // Ensure all the directories are initialised.
+ MailServices.ab.directories;
+
+ let ab = MailServices.ab.getDirectory(kPABData.URI);
+
+ function createAndAddCard(element) {
+ var card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(
+ Ci.nsIAbCard
+ );
+
+ card.primaryEmail = element.email;
+ card.displayName = element.displayName;
+
+ ab.addCard(card);
+ }
+
+ // Add address to addressbook so we can set display name and verify that
+ // the view uses the display name for display and sorting.
+ cards.forEach(createAndAddCard);
+
+ // build up a couple message with addresses in the ab.
+ let messages = [];
+ messages = messages.concat(
+ gMessageGenerator.makeMessage({
+ from: ["aaa", "aaa@b.invalid"],
+ to: [["ccc", "ccc@d.invalid"]],
+ })
+ );
+ messages = messages.concat(
+ gMessageGenerator.makeMessage({
+ from: ["eee", "eee@f.invalid"],
+ to: [["ggg", "ggg@h.invalid"]],
+ })
+ );
+
+ let msgSet = new SyntheticMessageSet(messages);
+ gTestFolder = await messageInjection.makeEmptyFolder();
+ await messageInjection.addSetsToFolders([gTestFolder], [msgSet]);
+});
+
+add_task(function test_view_sort_by_addresses() {
+ // - create the view
+ setup_view("threaded", Ci.nsMsgViewFlagsType.kNone);
+ // Check that sorting by sender uses the display name
+ gDBView.sort(Ci.nsMsgViewSortType.byAuthor, Ci.nsMsgViewSortOrder.ascending);
+ let sender1 = gDBView.cellTextForColumn(0, "senderCol");
+ let sender2 = gDBView.cellTextForColumn(1, "senderCol");
+
+ if (sender1 != 2) {
+ view_throw("expected sender 1 to be 2");
+ }
+ if (sender2 != 4) {
+ view_throw("expected sender 2 to be 4");
+ }
+
+ gDBView.sort(
+ Ci.nsMsgViewSortType.byRecipient,
+ Ci.nsMsgViewSortOrder.ascending
+ );
+ let recip1 = gDBView.cellTextForColumn(0, "recipientCol");
+ let recip2 = gDBView.cellTextForColumn(1, "recipientCol");
+
+ if (recip1 != 1) {
+ view_throw("expected recip 1 to be 1");
+ }
+ if (recip2 != 3) {
+ view_throw("expected recip 2 to be 3");
+ }
+});
+
+function view_throw(why) {
+ dump_view_contents();
+ do_throw(why);
+}
+var gDBView;
+var gTreeView;
+
+function setup_view(aViewType, aViewFlags, aTestFolder) {
+ let dbviewContractId = "@mozilla.org/messenger/msgdbview;1?type=" + aViewType;
+
+ if (aTestFolder == null) {
+ aTestFolder = gTestFolder;
+ }
+
+ // always start out fully expanded
+ aViewFlags |= Ci.nsMsgViewFlagsType.kExpandAll;
+
+ gDBView = Cc[dbviewContractId].createInstance(Ci.nsIMsgDBView);
+ gDBView.init(null, null, null);
+ var outCount = {};
+ gDBView.open(
+ aViewType != "search" ? aTestFolder : null,
+ Ci.nsMsgViewSortType.byDate,
+ aViewType != "search"
+ ? Ci.nsMsgViewSortOrder.ascending
+ : Ci.nsMsgViewSortOrder.descending,
+ aViewFlags,
+ outCount
+ );
+ dump(" View Out Count: " + outCount.value + "\n");
+
+ gTreeView = gDBView.QueryInterface(Ci.nsITreeView);
+}
diff --git a/comm/mailnews/base/test/unit/test_virtualFolders1.js b/comm/mailnews/base/test/unit/test_virtualFolders1.js
new file mode 100644
index 0000000000..5fe32de237
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_virtualFolders1.js
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that subfolders added to searched folders are also searched.
+ */
+
+const { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+
+add_task(function () {
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ let rootFolder = account.incomingServer.rootFolder;
+
+ // Create some real folders to test.
+ rootFolder.createSubfolder("test A", null);
+ let testFolderA = rootFolder.getChildNamed("test A");
+ rootFolder.createSubfolder("test B", null);
+ let testFolderB = rootFolder.getChildNamed("test B");
+ rootFolder.createSubfolder("test C", null);
+ let testFolderC = rootFolder.getChildNamed("test C");
+
+ // Create a virtual folder with no search folders.
+ let wrappedFolderZ = VirtualFolderHelper.createNewVirtualFolder(
+ "virtual Z",
+ rootFolder,
+ [],
+ "ANY",
+ false
+ );
+ Assert.equal(
+ wrappedFolderZ.virtualFolder,
+ rootFolder.getChildNamed("virtual Z")
+ );
+ Assert.equal(wrappedFolderZ.searchFolderURIs, "");
+ Assert.deepEqual(wrappedFolderZ.searchFolders, []);
+
+ // Create a virtual folder with one search folder.
+ let wrappedFolderY = VirtualFolderHelper.createNewVirtualFolder(
+ "virtual Y",
+ rootFolder,
+ [testFolderA],
+ "ANY",
+ false
+ );
+ Assert.equal(
+ wrappedFolderY.virtualFolder,
+ rootFolder.getChildNamed("virtual Y")
+ );
+ Assert.equal(wrappedFolderY.searchFolderURIs, testFolderA.URI);
+ Assert.deepEqual(wrappedFolderY.searchFolders, [testFolderA]);
+
+ // Create a virtual folder with two search folders.
+ let wrappedFolderX = VirtualFolderHelper.createNewVirtualFolder(
+ "virtual X",
+ rootFolder,
+ [testFolderB, testFolderC],
+ "ANY",
+ false
+ );
+ Assert.equal(
+ wrappedFolderX.virtualFolder,
+ rootFolder.getChildNamed("virtual X")
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderC.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [testFolderB, testFolderC]);
+
+ // Add a subfolder to real folder B. Check it is added to virtual folder X.
+ testFolderB.createSubfolder("test BB", null);
+ let testFolderBB = testFolderB.getChildNamed("test BB");
+ Assert.equal(
+ wrappedFolderZ.searchFolderURIs,
+ "",
+ "virtual folder Z should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderZ.searchFolders,
+ [],
+ "virtual folder Z should not change"
+ );
+ Assert.equal(
+ wrappedFolderY.searchFolderURIs,
+ testFolderA.URI,
+ "virtual folder Y should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderY.searchFolders,
+ [testFolderA],
+ "virtual folder Y should not change"
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderC.URI}|${testFolderBB.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [
+ testFolderB,
+ testFolderBB,
+ testFolderC,
+ ]);
+
+ // Add a subfolder to real folder BB. Check it is added to virtual folder X.
+ testFolderBB.createSubfolder("test BBB", null);
+ let testFolderBBB = testFolderBB.getChildNamed("test BBB");
+ Assert.equal(
+ wrappedFolderZ.searchFolderURIs,
+ "",
+ "virtual folder Z should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderZ.searchFolders,
+ [],
+ "virtual folder Z should not change"
+ );
+ Assert.equal(
+ wrappedFolderY.searchFolderURIs,
+ testFolderA.URI,
+ "virtual folder Y should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderY.searchFolders,
+ [testFolderA],
+ "virtual folder Y should not change"
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderC.URI}|${testFolderBB.URI}|${testFolderBBB.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [
+ testFolderB,
+ testFolderBB,
+ testFolderBBB,
+ testFolderC,
+ ]);
+
+ // Remove BB from virtual folder X.
+ wrappedFolderX.searchFolders = [testFolderB, testFolderBBB, testFolderC];
+ Assert.equal(
+ wrappedFolderZ.searchFolderURIs,
+ "",
+ "virtual folder Z should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderZ.searchFolders,
+ [],
+ "virtual folder Z should not change"
+ );
+ Assert.equal(
+ wrappedFolderY.searchFolderURIs,
+ testFolderA.URI,
+ "virtual folder Y should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderY.searchFolders,
+ [testFolderA],
+ "virtual folder Y should not change"
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderBBB.URI}|${testFolderC.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [
+ testFolderB,
+ testFolderBBB,
+ testFolderC,
+ ]);
+
+ // Add a second subfolder to the removed folder. Check it is not added to
+ // virtual folder X, because the parent folder is not in X.
+ testFolderBB.createSubfolder("test BBB two", null);
+ Assert.equal(
+ wrappedFolderZ.searchFolderURIs,
+ "",
+ "virtual folder Z should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderZ.searchFolders,
+ [],
+ "virtual folder Z should not change"
+ );
+ Assert.equal(
+ wrappedFolderY.searchFolderURIs,
+ testFolderA.URI,
+ "virtual folder Y should not change"
+ );
+ Assert.deepEqual(
+ wrappedFolderY.searchFolders,
+ [testFolderA],
+ "virtual folder Y should not change"
+ );
+ Assert.equal(
+ wrappedFolderX.searchFolderURIs,
+ `${testFolderB.URI}|${testFolderBBB.URI}|${testFolderC.URI}`
+ );
+ Assert.deepEqual(wrappedFolderX.searchFolders, [
+ testFolderB,
+ testFolderBBB,
+ testFolderC,
+ ]);
+});
diff --git a/comm/mailnews/base/test/unit/test_virtualFolders2.js b/comm/mailnews/base/test/unit/test_virtualFolders2.js
new file mode 100644
index 0000000000..07c75db97f
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_virtualFolders2.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests problems emptying a Trash folder that is searched by a virtual folder.
+ */
+
+const { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+
+add_task(function () {
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ let rootFolder = account.incomingServer.rootFolder;
+
+ // Create a real folders inside the trash folder.
+ let trashFolder = rootFolder.getFolderWithFlags(Ci.nsMsgFolderFlags.Trash);
+ trashFolder.createSubfolder("deleted", null);
+ let deletedFolder = trashFolder.getChildNamed("deleted");
+
+ // Create a virtual folder that searches the trash folder.
+ let wrappedVirtualFolder = VirtualFolderHelper.createNewVirtualFolder(
+ "virtual",
+ rootFolder,
+ [trashFolder, deletedFolder],
+ "ANY",
+ false
+ );
+ let virtualFolder = wrappedVirtualFolder.virtualFolder;
+ Assert.equal(virtualFolder, rootFolder.getChildNamed("virtual"));
+ Assert.equal(
+ wrappedVirtualFolder.searchFolderURIs,
+ `${trashFolder.URI}|${deletedFolder.URI}`
+ );
+ Assert.deepEqual(wrappedVirtualFolder.searchFolders, [
+ trashFolder,
+ deletedFolder,
+ ]);
+
+ // Create a smart virtual folder that searches the trash folder. This is the
+ // same as before except we'll set the searchFolderFlag property, as we do
+ // for the Unified Folders section of the UI.
+ let wrappedSmartFolder = VirtualFolderHelper.createNewVirtualFolder(
+ "smart",
+ rootFolder,
+ [trashFolder, deletedFolder],
+ "ANY",
+ false
+ );
+ let smartFolder = wrappedSmartFolder.virtualFolder;
+ smartFolder.msgDatabase.dBFolderInfo.setUint32Property(
+ "searchFolderFlag",
+ Ci.nsMsgFolderFlags.Trash
+ );
+ Assert.equal(smartFolder, rootFolder.getChildNamed("smart"));
+ Assert.equal(
+ wrappedSmartFolder.searchFolderURIs,
+ `${trashFolder.URI}|${deletedFolder.URI}`
+ );
+ Assert.deepEqual(wrappedSmartFolder.searchFolders, [
+ trashFolder,
+ deletedFolder,
+ ]);
+
+ // Empty the trash. The normal virtual folder should disappear, but the
+ // smart folder shouldn't.
+ trashFolder.emptyTrash(null);
+ Assert.equal(
+ virtualFolder.parent,
+ null,
+ "virtual folder should be removed with last search folder"
+ );
+ Assert.equal(
+ smartFolder.parent,
+ rootFolder,
+ "smart virtual folder should NOT be removed with last search folder"
+ );
+ Assert.equal(
+ wrappedSmartFolder.searchFolderURIs,
+ trashFolder.URI,
+ "smart virtual folder should still search the trash folder (only)"
+ );
+ Assert.deepEqual(
+ wrappedSmartFolder.searchFolders,
+ [trashFolder],
+ "smart virtual folder should still search the trash folder (only)"
+ );
+});
diff --git a/comm/mailnews/base/test/unit/test_virtualFolders3.js b/comm/mailnews/base/test/unit/test_virtualFolders3.js
new file mode 100644
index 0000000000..e3d1b9c73c
--- /dev/null
+++ b/comm/mailnews/base/test/unit/test_virtualFolders3.js
@@ -0,0 +1,229 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests that subfolders added to searched folders are also searched.
+ */
+
+const { VirtualFolderHelper } = ChromeUtils.import(
+ "resource:///modules/VirtualFolderWrapper.jsm"
+);
+
+let rootFolder;
+
+add_setup(function () {
+ MailServices.accounts.createLocalMailAccount();
+ let account = MailServices.accounts.accounts[0];
+ rootFolder = account.incomingServer.rootFolder;
+ rootFolder.QueryInterface(Ci.nsIMsgLocalMailFolder);
+
+ // Listen to folder events for debugging purposes.
+
+ MailServices.mailSession.AddFolderListener(
+ {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
+ onFolderAdded(parentFolder, childFolder) {
+ console.log(
+ `onFolderAdded: ${
+ childFolder?.URI
+ } (flags: ${childFolder.flags.toString(16)}) added to ${
+ parentFolder?.URI
+ }`
+ );
+ },
+ onMessageAdded(parentFolder, msg) {},
+ onFolderRemoved(parentFolder, childFolder) {
+ console.log(
+ `onFolderRemoved: ${
+ childFolder?.URI
+ } (flags: ${childFolder.flags.toString(16)}) removed from ${
+ parentFolder?.URI
+ }`
+ );
+ },
+ onMessageRemoved(parentFolder, msg) {},
+ onFolderPropertyChanged(folder, property, oldValue, newValue) {},
+ onFolderIntPropertyChanged(folder, property, oldValue, newValue) {
+ if (property == "FolderFlag") {
+ console.log(
+ `onFolderIntPropertyChanged: ${
+ folder.URI
+ } changed flags from ${oldValue.toString(
+ 16
+ )} to ${newValue.toString(16)}`
+ );
+ }
+ },
+ onFolderBoolPropertyChanged(folder, property, oldValue, newValue) {},
+ onFolderUnicharPropertyChanged(folder, property, oldValue, newValue) {},
+ onFolderPropertyFlagChanged(folder, property, oldFlag, newFlag) {},
+ onFolderEvent(folder, event) {},
+ },
+ Ci.nsIFolderListener.all
+ );
+});
+
+// Test each of the folder types.
+add_task(function testInbox() {
+ subtest("Inbox");
+});
+add_task(function testDrafts() {
+ subtest("Drafts");
+});
+add_task(function testTemplates() {
+ subtest("Templates");
+});
+add_task(function testSentMail() {
+ subtest("SentMail");
+});
+add_task(function testArchive() {
+ subtest("Archive");
+});
+add_task(function testJunk() {
+ subtest("Junk");
+});
+add_task(function testTrash() {
+ subtest("Trash");
+});
+
+function subtest(flag) {
+ // Create a virtual folder. This is very similar to the code in about3Pane.js.
+
+ let virtualFolder = rootFolder.createLocalSubfolder(`virtual${flag}`);
+ virtualFolder.flags |=
+ Ci.nsMsgFolderFlags.Virtual | Ci.nsMsgFolderFlags[flag];
+
+ let msgDatabase = virtualFolder.msgDatabase;
+ let folderInfo = msgDatabase.dBFolderInfo;
+
+ folderInfo.setCharProperty("searchStr", "ALL");
+ folderInfo.setUint32Property("searchFolderFlag", Ci.nsMsgFolderFlags[flag]);
+ folderInfo.setBooleanProperty("searchOnline", true);
+ msgDatabase.summaryValid = true;
+ msgDatabase.close(true);
+
+ function checkVirtualFolder(searchFolders, message) {
+ let wrappedVirtualFolder =
+ VirtualFolderHelper.wrapVirtualFolder(virtualFolder);
+ Assert.deepEqual(
+ wrappedVirtualFolder.searchFolderURIs.split("|").filter(Boolean).sort(),
+ searchFolders.map(f => f.URI).sort(),
+ message
+ );
+ Assert.deepEqual(
+ wrappedVirtualFolder.searchFolders,
+ searchFolders,
+ message
+ );
+ }
+
+ rootFolder.notifyFolderAdded(virtualFolder);
+ checkVirtualFolder([], "new virtual folder should have no search folders");
+
+ // Create a disconnected folder with some descendants, add the flag and then
+ // add it to the parent. The folder and descendants should all be added to
+ // the virtual folder.
+
+ let parent = MailServices.folderLookup.getOrCreateFolderForURL(
+ `${rootFolder.URI}/parent${flag}`
+ );
+ parent.setFlag(Ci.nsMsgFolderFlags[flag]);
+ let child = MailServices.folderLookup.getOrCreateFolderForURL(
+ `${rootFolder.URI}/parent${flag}/child`
+ );
+ parent.addSubfolder(child.name);
+ let grandchild = MailServices.folderLookup.getOrCreateFolderForURL(
+ `${rootFolder.URI}/parent${flag}/child/grandchild`
+ );
+ child.addSubfolder(grandchild.name);
+
+ rootFolder.addSubfolder(parent.name);
+ rootFolder.notifyFolderAdded(parent);
+ parent.notifyFolderAdded(child);
+ child.notifyFolderAdded(grandchild);
+
+ checkVirtualFolder(
+ [parent, child, grandchild],
+ "added folder and descendants should be added to the virtual folder"
+ );
+
+ // Create a subfolder of a real folder with some descendants, then set the
+ // flag. The folder and descendants should all be added to the virtual folder.
+
+ let more = rootFolder.createLocalSubfolder(`more${flag}`);
+ more.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ let evenMore = more.createLocalSubfolder("even more");
+ evenMore.QueryInterface(Ci.nsIMsgLocalMailFolder);
+ let yetMore = evenMore.createLocalSubfolder("yet more");
+ more.setFlag(Ci.nsMsgFolderFlags[flag]);
+
+ checkVirtualFolder(
+ [more, evenMore, yetMore, parent, child, grandchild],
+ "folder with changed flag and descendants should be added to the virtual folder"
+ );
+
+ // Test what happens if a folder of one type is not added to a parent in the
+ // virtual folder of another type. This should really only matter for inboxes
+ // containing other types of folders, which happens in some configurations.
+ // Other combinations shouldn't really exist, but let's test them anyway.
+
+ if (!["SentMail", "Archive"].includes(flag)) {
+ for (let otherFlag of [
+ "Inbox",
+ "Drafts",
+ "Templates",
+ "SentMail",
+ "Archive",
+ "Junk",
+ "Trash",
+ ]) {
+ if (otherFlag == flag) {
+ continue;
+ }
+ let otherFlagChild = MailServices.folderLookup.getOrCreateFolderForURL(
+ `${rootFolder.URI}/parent${flag}/other${otherFlag}Child`
+ );
+ otherFlagChild.setFlag(Ci.nsMsgFolderFlags[otherFlag]);
+ parent.addSubfolder(otherFlagChild.name);
+ parent.notifyFolderAdded(otherFlagChild);
+
+ if (flag == "Trash") {
+ checkVirtualFolder(
+ [more, evenMore, yetMore, parent, child, grandchild, otherFlagChild],
+ `folder with ${otherFlag} flag should be added to the virtual ${flag} folder`
+ );
+ } else {
+ checkVirtualFolder(
+ [more, evenMore, yetMore, parent, child, grandchild],
+ `folder with ${otherFlag} flag should not be added to the virtual ${flag} folder`
+ );
+ }
+
+ parent.propagateDelete(otherFlagChild, false);
+ }
+ }
+
+ // Now reverse the additions.
+
+ rootFolder.propagateDelete(parent, false);
+ checkVirtualFolder(
+ [more, evenMore, yetMore],
+ "deleted folder and descendants should be removed from the virtual folder"
+ );
+
+ if (flag != "Inbox") {
+ // Clearing the inbox flag from a folder that has it throws an assertion.
+ more.clearFlag(Ci.nsMsgFolderFlags[flag]);
+ checkVirtualFolder(
+ [],
+ "folder with changed flag and descendants should be removed from the virtual folder"
+ );
+
+ Assert.equal(
+ virtualFolder.parent,
+ rootFolder,
+ "smart virtual folder should NOT be removed with last search folder"
+ );
+ }
+}
diff --git a/comm/mailnews/base/test/unit/xpcshell-imap.ini b/comm/mailnews/base/test/unit/xpcshell-imap.ini
new file mode 100644
index 0000000000..2b343b30b7
--- /dev/null
+++ b/comm/mailnews/base/test/unit/xpcshell-imap.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+head = head_mailbase.js
+tail =
+prefs =
+ mailnews.imap.jsmodule=true
+dupe-manifest =
+
+[test_incomingServer.js]
diff --git a/comm/mailnews/base/test/unit/xpcshell.ini b/comm/mailnews/base/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..0658d7d1b3
--- /dev/null
+++ b/comm/mailnews/base/test/unit/xpcshell.ini
@@ -0,0 +1,77 @@
+[DEFAULT]
+head = head_mailbase.js
+tail =
+dupe-manifest =
+prefs =
+ mail.biff.use_new_count_in_badge=false
+support-files = nodelist_test.xml data/*
+
+[test_accountMgr.js]
+[test_accountMgr2.js]
+[test_accountMgrCustomTypes.js]
+[test_accountMgrMovedLocalFolders.js]
+[test_accountMgrRemoveDefault.js]
+[test_accountMigration.js]
+[test_acctRepair.js]
+[test_bccInDatabase.js]
+[test_bug428427.js]
+[test_bug434810.js]
+[test_bug471682.js]
+[test_bug514945.js]
+[test_closedDB.js]
+[test_compactColumnSave.js]
+[test_compactFailure.js]
+[test_converterDeferredAccount.js]
+[test_copyChaining.js]
+[test_copyToInvalidDB.js]
+[test_detachToFile.js]
+[test_emptyTrash.js]
+[test_fix_deferred_accounts.js]
+[test_folderCompact.js]
+[test_folderCompact2.js]
+[test_folderLookupService.js]
+[test_folderStringProperties.js]
+[test_formatFileSize.js]
+[test_getMsgTextFromStream.js]
+[test_headerFoldingInDatabase.js]
+[test_hostnameUtils.js]
+[test_identity.js]
+[test_imapPump.js]
+[test_incomingServer.js]
+[test_inheritedFolderProperties.js]
+[test_junkingWhenDisabled.js]
+[test_loadVirtualFolders.js]
+[test_mailServices.js]
+[test_mailstoreConverter.js]
+[test_mimemaltdetach.js]
+[test_MsgIncomingServer.js]
+[test_MsgKeySet.js]
+[test_newMailNotification.js]
+# Not yet working for non-Mac OS
+skip-if = os != 'mac'
+[test_nsIFolderListener.js]
+[test_nsIMsgContentPolicy.js]
+skip-if = true # See bug 1446587.
+[test_nsIMsgFolder.js]
+[test_nsIMsgFolderCache.js]
+[test_nsIMsgFolderListener.js]
+[test_nsIMsgFolderListenerLocal.js]
+[test_nsIMsgTagService.js]
+[test_nsMailDirProvider.js]
+[test_nsMsgDBView.js]
+[test_nsMsgDBView_headerValues.js]
+[test_nsMsgMailSession_Alerts.js]
+skip-if = true # See bug 1418063.
+[test_nsMsgMailSession_Listeners.js]
+[test_nsMsgTraitService.js]
+[test_postPluginFilters.js]
+[test_retention.js]
+[test_saveAs.js]
+[test_testsuite_base64.js]
+[test_testsuite_fakeserver_imapd_gmail.js]
+[test_testsuite_fakeserver_imapd_list-extended.js]
+[test_testsuite_fakeserverAuth.js]
+[test_viewSortByAddresses.js]
+[test_virtualFolders1.js]
+[test_virtualFolders2.js]
+[test_virtualFolders3.js]