diff options
Diffstat (limited to '')
38 files changed, 11598 insertions, 0 deletions
diff --git a/comm/mailnews/db/msgdb/.eslintrc.js b/comm/mailnews/db/msgdb/.eslintrc.js new file mode 100644 index 0000000000..5816519fbb --- /dev/null +++ b/comm/mailnews/db/msgdb/.eslintrc.js @@ -0,0 +1,5 @@ +"use strict"; + +module.exports = { + extends: ["plugin:mozilla/valid-jsdoc"], +}; diff --git a/comm/mailnews/db/msgdb/moz.build b/comm/mailnews/db/msgdb/moz.build new file mode 100644 index 0000000000..a49689ab64 --- /dev/null +++ b/comm/mailnews/db/msgdb/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/db/msgdb/public/moz.build b/comm/mailnews/db/msgdb/public/moz.build new file mode 100644 index 0000000000..5bed71ce9c --- /dev/null +++ b/comm/mailnews/db/msgdb/public/moz.build @@ -0,0 +1,25 @@ +# 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 += [ + "nsIDBChangeAnnouncer.idl", + "nsIDBChangeListener.idl", + "nsIDBFolderInfo.idl", + "nsIMsgDatabase.idl", + "nsIMsgOfflineImapOperation.idl", + "nsINewsDatabase.idl", +] + +XPIDL_MODULE = "msgdb" + +EXPORTS += [ + "nsDBFolderInfo.h", + "nsImapMailDatabase.h", + "nsMailDatabase.h", + "nsMsgDatabase.h", + "nsMsgHdr.h", + "nsMsgThread.h", + "nsNewsDatabase.h", +] diff --git a/comm/mailnews/db/msgdb/public/nsDBFolderInfo.h b/comm/mailnews/db/msgdb/public/nsDBFolderInfo.h new file mode 100644 index 0000000000..5b1150e4e7 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsDBFolderInfo.h @@ -0,0 +1,151 @@ +/* -*- 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/. */ + +/* This class encapsulates the global information about a folder stored in the + summary file. +*/ +#ifndef _nsDBFolderInfo_H +#define _nsDBFolderInfo_H + +#include "mozilla/MemoryReporting.h" +#include "nsString.h" +#include "MailNewsTypes.h" +#include "mdb.h" +#include "nsTArray.h" +#include "nsIDBFolderInfo.h" +#include <time.h> + +class nsMsgDatabase; + +// again, this could inherit from nsISupports, but I don't see the need as of +// yet. I'm not sure it needs to be ref-counted (but I think it does). + +// I think these getters and setters really need to go through mdb and not rely +// on the object caching the values. If this somehow turns out to be +// prohibitively expensive, we can invent some sort of dirty mechanism, but I +// think it turns out that these values will be cached by the MSG_FolderInfo's +// anyway. +class nsDBFolderInfo : public nsIDBFolderInfo { + public: + friend class nsMsgDatabase; + + explicit nsDBFolderInfo(nsMsgDatabase* mdb); + + NS_DECL_ISUPPORTS + // interface methods. + NS_DECL_NSIDBFOLDERINFO + // create the appropriate table and row in a new db. + nsresult AddToNewMDB(); + // accessor methods. + + bool TestFlag(int32_t flags); + int16_t GetIMAPHierarchySeparator(); + void SetIMAPHierarchySeparator(int16_t hierarchyDelimiter); + void ChangeImapTotalPendingMessages(int32_t delta); + void ChangeImapUnreadPendingMessages(int32_t delta); + + nsresult InitFromExistingDB(); + // get and set arbitrary property, aka row cell value. + nsresult SetPropertyWithToken(mdb_token aProperty, + const nsAString& propertyStr); + nsresult SetUint32PropertyWithToken(mdb_token aProperty, + uint32_t propertyValue); + nsresult SetInt64PropertyWithToken(mdb_token aProperty, + int64_t propertyValue); + nsresult SetInt32PropertyWithToken(mdb_token aProperty, + int32_t propertyValue); + nsresult GetPropertyWithToken(mdb_token aProperty, nsAString& propertyValue); + nsresult GetUint32PropertyWithToken(mdb_token aProperty, + uint32_t& propertyValue, + uint32_t defaultValue = 0); + nsresult GetInt32PropertyWithToken(mdb_token aProperty, + int32_t& propertyValue, + int32_t defaultValue = 0); + nsresult GetInt64PropertyWithToken(mdb_token aProperty, + int64_t& propertyValue, + int64_t defaultValue = 0); + + nsTArray<nsMsgKey> m_lateredKeys; // list of latered messages + + virtual size_t SizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + return m_lateredKeys.ShallowSizeOfExcludingThis(aMallocSizeOf); + } + virtual size_t SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + protected: + virtual ~nsDBFolderInfo(); + + // initialize from appropriate table and row in existing db. + nsresult InitMDBInfo(); + nsresult LoadMemberVariables(); + + nsresult AdjustHighWater(nsMsgKey highWater, bool force); + + void + ReleaseExternalReferences(); // let go of any references to other objects. + + int64_t m_folderSize; + int64_t m_expungedBytes; // sum of size of deleted messages in folder + uint32_t m_folderDate; + nsMsgKey m_highWaterMessageKey; // largest news article number or imap uid + // whose header we've seen + + // m_numUnreadMessages and m_numMessages can never be negative. 0 means 'no + // msgs'. + int32_t m_numUnreadMessages; + int32_t m_numMessages; // includes expunged and ignored messages + + int32_t m_flags; // folder specific flags. This holds things like re-use + // thread pane, + // configured for off-line use, use default retrieval, purge article/header + // options + + uint16_t m_version; // for upgrading... + int16_t m_IMAPHierarchySeparator; // imap path separator + + // mail only (for now) + + // IMAP only + int32_t m_ImapUidValidity; + int32_t m_totalPendingMessages; + int32_t m_unreadPendingMessages; + + // news only (for now) + nsMsgKey + m_expiredMark; // Highest invalid article number in group - for expiring + // the db folder info will have to know what db and row it belongs to, since + // it is really just a wrapper around the singleton folder info row in the + // mdb. + nsMsgDatabase* m_mdb; + nsIMdbTable* m_mdbTable; // singleton table in db + nsIMdbRow* m_mdbRow; // singleton row in table; + + bool m_mdbTokensInitialized; + + mdb_token m_rowScopeToken; + mdb_token m_tableKindToken; + // tokens for the pre-set columns - we cache these for speed, which may be + // silly + mdb_token m_mailboxNameColumnToken; + mdb_token m_numMessagesColumnToken; + mdb_token m_numUnreadMessagesColumnToken; + mdb_token m_flagsColumnToken; + mdb_token m_folderSizeColumnToken; + mdb_token m_expungedBytesColumnToken; + mdb_token m_folderDateColumnToken; + mdb_token m_highWaterMessageKeyColumnToken; + + mdb_token m_imapUidValidityColumnToken; + mdb_token m_totalPendingMessagesColumnToken; + mdb_token m_unreadPendingMessagesColumnToken; + mdb_token m_expiredMarkColumnToken; + mdb_token m_versionColumnToken; +}; + +#endif diff --git a/comm/mailnews/db/msgdb/public/nsIDBChangeAnnouncer.idl b/comm/mailnews/db/msgdb/public/nsIDBChangeAnnouncer.idl new file mode 100644 index 0000000000..afdb06f8ca --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsIDBChangeAnnouncer.idl @@ -0,0 +1,42 @@ +/* -*- 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 nsIDBChangeListener; +interface nsIMsgDBHdr; + +[scriptable, uuid(22baf00b-939d-42c3-ac51-21d99dfa1f05)] +interface nsIDBChangeAnnouncer : nsISupports { + void addListener(in nsIDBChangeListener listener); + void removeListener(in nsIDBChangeListener listener); + + void notifyHdrChangeAll(in nsIMsgDBHdr aHdrChanged, + in unsigned long aOldFlags, + in unsigned long aNewFlags, + in nsIDBChangeListener instigator); + + void notifyHdrAddedAll(in nsIMsgDBHdr aHdrAdded, + in nsMsgKey parentKey, + in long flags, + in nsIDBChangeListener instigator); + + void notifyHdrDeletedAll(in nsIMsgDBHdr aHdrDeleted, + in nsMsgKey parentKey, + in long flags, + in nsIDBChangeListener instigator); + + void notifyParentChangedAll(in nsMsgKey keyReparented, + in nsMsgKey oldParent, + in nsMsgKey newParent, + in nsIDBChangeListener instigator); + + void notifyReadChanged(in nsIDBChangeListener instigator); + + void notifyJunkScoreChanged(in nsIDBChangeListener aInstigator); + + void notifyAnnouncerGoingAway(); +}; diff --git a/comm/mailnews/db/msgdb/public/nsIDBChangeListener.idl b/comm/mailnews/db/msgdb/public/nsIDBChangeListener.idl new file mode 100644 index 0000000000..30c50db49c --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsIDBChangeListener.idl @@ -0,0 +1,117 @@ +/* -*- 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 nsIDBChangeAnnouncer; +interface nsIMsgDBHdr; +interface nsIMsgDatabase; + +/** + * These callbacks are provided to allow listeners to the message database + * to update their status when changes occur. + */ +[scriptable, uuid(21c56d34-71b9-42bb-9606-331a6a5f8210)] + +interface nsIDBChangeListener : nsISupports { + /** + * Callback when message flags are changed. + * + * @param aHdrChanged The changed header. + * @param aOldFlags Message flags prior to change. + * @param aNewFlags Message flags after change. + * @param aInstigator Object that initiated the change. + */ + void onHdrFlagsChanged(in nsIMsgDBHdr aHdrChanged, in unsigned long aOldFlags, + in unsigned long aNewFlags, in nsIDBChangeListener aInstigator); + + /** + * Callback when message is marked as deleted. + * + * @param aHdrChanged The message header that is going to be deleted. + * @param aParentKey Key of parent. + * @param aFlags Flags that message has before delete. + * @param aInstigator Object that initiated the change. Can be null. + */ + void onHdrDeleted(in nsIMsgDBHdr aHdrChanged, in nsMsgKey aParentKey, in long aFlags, + in nsIDBChangeListener aInstigator); + + /** + * Callback when message is added. + * + * @param aHdrChanged The message header that is added. + * @param aParentKey Parent key of message. + * @param aFlags Flags that new message will have. + * @param aInstigator Object that initiated the change. Can be null. + */ + void onHdrAdded(in nsIMsgDBHdr aHdrChanged, in nsMsgKey aParentKey, in long aFlags, + in nsIDBChangeListener aInstigator); + + /** + * Callback when message parent is changed. Parent is changed when message is deleted or moved. + * + * @param aKeyChanged The message key that parent key was changed. + * @param oldParent Old parent key. + * @param newParent New parent key. + * @param aInstigator Object that initiated the change. Can be null. + */ + void onParentChanged(in nsMsgKey aKeyChanged, in nsMsgKey oldParent, in nsMsgKey newParent, + in nsIDBChangeListener aInstigator); + + /** + * Callback when announcer is going away. This is good place to release strong pointers to announcer. + * + * @param instigator Object that initiated the change. Can be null. + */ + void onAnnouncerGoingAway(in nsIDBChangeAnnouncer instigator); + + /** + * Callback when read flag is changed. + * + * @param aInstigator Object that initiated the change. Can be null. + */ + void onReadChanged(in nsIDBChangeListener aInstigator); + + /** + * Callback used in case when "junkscore" property is changed. + * + * @param aInstigator Object that initiated the change. Can be null. + */ + void onJunkScoreChanged(in nsIDBChangeListener aInstigator); + + /** + * Callback used in the general case where any field may have changed. + * OnHdrPropertyChanged is called twice per change. On the first call, aPreChange + * is true, and aStatus is undefined. OnHdrPropertyChanged saves any required status in aStatus + * (such as a filter match). The calling function stores the value of aStatus, changes the + * header aHdrToChange, then calls OnHdrPropertyChanged again with aPreChange false. On this + * second call, the stored value of aStatus is provided, so that any changes may be noted. + * + * @param aHdrToChange the message header that is changing. + * @param aPreChange true on first call before change, false on second call after change + * @param aStatus storage location provided by calling routine for status + * @param aInstigator object that initiated the change + */ + void onHdrPropertyChanged(in nsIMsgDBHdr aHdrToChange, + in AUTF8String property, + in boolean aPreChange, + inout uint32_t aStatus, + in nsIDBChangeListener aInstigator); + + /** + * Generic notification for extensibility. Common events should be documented + * here so we have a hope of keeping the documentation up to date. + * Current events are: + * "DBOpened" - When a pending listener becomes real. This can happen when + * the existing db is force closed and a new one opened. Only + * registered pending listeners are notified. + * + * @param aDB the db for this event. + * @param aEvent type of event. + * + */ + void onEvent(in nsIMsgDatabase aDB, in string aEvent); +}; diff --git a/comm/mailnews/db/msgdb/public/nsIDBFolderInfo.idl b/comm/mailnews/db/msgdb/public/nsIDBFolderInfo.idl new file mode 100644 index 0000000000..cb41041268 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsIDBFolderInfo.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" +#include "MailNewsTypes2.idl" + +[scriptable, uuid(a72dab4b-b3bd-471e-9a38-1b242b385459)] +interface nsIDBFolderInfo : nsISupports { + attribute long flags; + + /** + * Or's aFlags into flags. + * + * @param - the flags(s) to set + * + * @return - the resulting flags. + */ + long orFlags(in long aFlags); + /** + * And's aFlags with flags, set flags to the result + * + * @param the flags(s) to AND + * + * @return the resulting flags. + */ + long andFlags(in long aFlags); + + /** + * Allows us to keep track of the highwater mark + * + * @param aNewKey If larger than the current highwater + * mark, sets the highwater mark to aNewKey. + */ + void onKeyAdded(in nsMsgKey aNewKey); + + attribute nsMsgKey highWater; + attribute nsMsgKey expiredMark; + attribute long long folderSize; + attribute unsigned long folderDate; + void changeNumUnreadMessages(in long aDelta); + void changeNumMessages(in long aDelta); + + // numUnreadMessages and numMessages will never return negative numbers. 0 means 'no msgs'. + attribute long numUnreadMessages; + attribute long numMessages; + + attribute long long expungedBytes; + attribute long imapUidValidity; + attribute unsigned long version; + attribute long imapTotalPendingMessages; + attribute long imapUnreadPendingMessages; + + attribute nsMsgViewTypeValue viewType; + attribute nsMsgViewFlagsTypeValue viewFlags; + attribute nsMsgViewSortTypeValue sortType; + attribute nsMsgViewSortOrderValue sortOrder; + + void changeExpungedBytes(in long aDelta); + + /** + * Gets a string property from the folder. Also used for URIs, hence the AUTF8String type. + * + * @param propertyName The name of the property for the value to retrieve. + */ + AUTF8String getCharProperty(in string propertyName); + + /** + * Sets a string property from the folder. Also used for URIs, hence the AUTF8String type. + * + * @param propertyName The name of the property for which to set a value + * @param propertyValue The new value of the property. + */ + void setCharProperty(in string aPropertyName, in AUTF8String aPropertyValue); + void setUint32Property(in string propertyName, in unsigned long propertyValue); + void setInt64Property(in string propertyName, in long long propertyValue); + unsigned long getUint32Property(in string propertyName, in unsigned long defaultValue); + long long getInt64Property(in string propertyName, in long long defaultValue); + boolean getBooleanProperty(in string propertyName, in boolean defaultValue); + void setBooleanProperty(in string propertyName, in boolean aPropertyValue); + nsIDBFolderInfo GetTransferInfo(); + void initFromTransferInfo(in nsIDBFolderInfo transferInfo); + + attribute AString locale; + attribute AString mailboxName; + + + AString getProperty(in string propertyName); + void setProperty(in string propertyName, in AString propertyStr); + + attribute string knownArtsSet; + attribute ACString folderName; +}; diff --git a/comm/mailnews/db/msgdb/public/nsIMsgDatabase.idl b/comm/mailnews/db/msgdb/public/nsIMsgDatabase.idl new file mode 100644 index 0000000000..e54c938c15 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsIMsgDatabase.idl @@ -0,0 +1,506 @@ +/* -*- 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/. */ + +/** + * @defgroup msgdb Mailnews message database + * This module is the access point to locally-stored databases. + * + * These databases are stored in .msf files. Each file contains useful cached + * information, like the message id or references, as well as the cc header or + * tag information. This cached information is encapsulated in nsIMsgDBHdr. + * + * Also included is threading information, mostly encapsulated in nsIMsgThread. + * The final component is the database folder info, which contains information + * on the view and basic information also stored in the folder cache such as the + * name or most recent update. + * + * What this module does not do is access individual messages. Access is + * strictly controlled by the nsIMsgFolder objects and their backends. + * @{ + */ +#include "nsISupports.idl" +#include "nsIDBChangeAnnouncer.idl" + +interface nsIMsgDatabase; +interface nsIDBChangeListener; +interface nsIMsgDBHdr; +interface nsIMsgEnumerator; +interface nsIMsgThread; +interface nsIMsgThreadEnumerator; +interface nsIDBFolderInfo; +interface nsIMsgOfflineImapOperation; +interface nsIMsgFolder; +interface nsIFile; +interface nsIMsgSearchTerm; + +typedef unsigned long nsMsgRetainByPreference; + + +[scriptable, uuid(fe8b7cec-eec8-4bcd-82ff-d8bb23cef3da)] + +interface nsIMsgRetentionSettings : nsISupports +{ + const unsigned long nsMsgRetainAll = 1; + const unsigned long nsMsgRetainByAge = 2; + const unsigned long nsMsgRetainByNumHeaders = 3; + + attribute boolean useServerDefaults; + attribute nsMsgRetainByPreference retainByPreference; + attribute unsigned long daysToKeepHdrs; + attribute unsigned long numHeadersToKeep; + + // this is for keeping offline bodies. + attribute boolean cleanupBodiesByDays; + attribute unsigned long daysToKeepBodies; + + /** + * Should retention settings be applied to flagged/starred messages? + * If false, flagged messages are never automatically deleted. + */ + attribute boolean applyToFlaggedMessages; +}; + +[scriptable, uuid(86a9da90-14f1-11d5-a5c0-0060b0fc04b7)] +interface nsIMsgDownloadSettings : nsISupports +{ + attribute boolean useServerDefaults; + attribute boolean downloadByDate; + attribute boolean downloadUnreadOnly; + attribute unsigned long ageLimitOfMsgsToDownload; +}; + +typedef long nsMsgDBCommit; + +[scriptable, uuid(15431853-e448-45dc-8978-9958bf74d9b7)] +interface nsMsgDBCommitType : nsISupports +{ + const long kLargeCommit = 1; + const long kSessionCommit = 2; + const long kCompressCommit = 3; +}; + +/** + * A service to open mail databases and manipulate listeners automatically. + * + * The contract ID for this component is + * <tt>\@mozilla.org/msgDatabase/msgDBService;1</tt>. + */ +[scriptable, uuid(4cbbf024-3760-402d-89f3-6ababafeb07d)] +interface nsIMsgDBService : nsISupports +{ + /** + * Opens a database for a given folder. + * + * This method is preferred over nsIMsgDBService::openMailDBFromFile if the + * caller has an actual nsIMsgFolder around. If the database detects that it + * is unreadable or out of date (using nsIMsgDatabase::outOfDate) it will + * destroy itself and prepare to be rebuilt, unless aLeaveInvalidDB is true. + * + * If one gets a NS_MSG_ERROR_FOLDER_SUMMARY_MISSING message, then one + * should call nsIMsgDBService::createNewDB to create the new database. + * + * @param aFolder The folder whose database should be returned. + * @param aLeaveInvalidDB Whether or not the database should be deleted if it + * is invalid. + * @return A new nsIMsgDatabase object representing the folder + * database that was opened. + * @exception NS_ERROR_FILE_NOT_FOUND + * The file could not be created. + * @exception NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE + * The database is present (and was opened), but the + * summary file is out of date. + * @exception NS_MSG_ERROR_FOLDER_SUMMARY_MISSING + * The database is present, but the summary file is + * missing. + * @see nsIMsgDatabase::Open + * @see nsIMsgDBService::createNewDB + */ + nsIMsgDatabase openFolderDB(in nsIMsgFolder aFolder, + in boolean aLeaveInvalidDB); + + /** + * Creates a new database for the given folder. + * + * If the database already exists, it will return the database, emit a + * warning, but not fully initialize it. For this reason, it should only be + * used when it is known that the database does not exist, such as when + * nsIMsgDBService::openFolderDB throws an error. + * + * @see nsIMsgDBService::openFolderDB + */ + nsIMsgDatabase createNewDB(in nsIMsgFolder aFolder); + + /** + * Opens or creates a database for a given file. + * + * This method should only be used if the caller does not have a folder + * instance, because the resulting db and message headers retrieved from the + * database would not know their owning folder, which limits their usefulness. + * For this reason, one should use nsIMsgDBService::openFolderDB instead + * except under special circumstances. + * + * Unlike nsIMsgDBService::openFolderDB, there is no corresponding method to + * create a new database if opening the database failed. However, this method + * will never throw NS_MSG_ERROR_FOLDER_SUMMARY_MISSING, so no corresponding + * method is needed. + * + * @param aFile The file for which the database should be returned. + * @param aFolder Folder the db corresponds to (may be null) + * @param aCreate Whether or not the file should be created. + * @param aLeaveInvalidDB Whether or not the database should be deleted if it + * is invalid. + * @return A new nsIMsgDatabase object encapsulating the file + * passed in. + * @exception NS_ERROR_FILE_NOT_FOUND + * The file could not be created. + * @see nsIMsgDBService::openFolderDB + * @see nsIMsgDatabase::Open + */ + nsIMsgDatabase openMailDBFromFile(in nsIFile aFile, + in nsIMsgFolder aFolder, + in boolean aCreate, + in boolean aLeaveInvalidDB); + /** + * Adds the given listener to the listener set for the folder. + * + * Since the message database will likely be opened and closed many times, by + * registering using this method, one will be guaranteed to see all subsequent + * modifications. This will also add the listener to the database if it is + * already opened. + * + * @param aFolder The folder to add a listener to. + * @param aListener The listener to add the folder to. + */ + void registerPendingListener(in nsIMsgFolder aFolder, + in nsIDBChangeListener aListener); + /** + * Removes the listener from all folder listener sets. + * + * @param aListener The listener to remove. + * @exception NS_ERROR_FAILURE + * The listener is not registered. + */ + void unregisterPendingListener(in nsIDBChangeListener aListener); + + /** + * Get the db for a folder, if already open. + * + * @param aFolder The folder to get the cached (open) db for. + * + * @returns null if the db isn't open, otherwise the db. + */ + nsIMsgDatabase cachedDBForFolder(in nsIMsgFolder aFolder); + + /** + * Close the db for a folder, if already open. + * + * @param aFolder The folder to close the cached (open) db for. + */ + void forceFolderDBClosed(in nsIMsgFolder aFolder); + + /// an enumerator to iterate over the open dbs. + readonly attribute Array<nsIMsgDatabase> openDBs; +}; + +[scriptable, uuid(b64e66f8-4717-423a-be42-482658fb2199)] +interface nsIMsgDatabase : nsIDBChangeAnnouncer { + void close(in boolean aForceCommit); + + void commit(in nsMsgDBCommit commitType); + // Force closed is evil, and we should see if we can do without it. + // In 4.x, it was mainly used to remove corrupted databases. + void forceClosed(); + void clearCachedHdrs(); + void resetHdrCacheSize(in unsigned long size); + + readonly attribute nsIDBFolderInfo dBFolderInfo; + + /// Size of the database file in bytes. + readonly attribute long long databaseSize; + + /// Folder this db was opened on. + readonly attribute nsIMsgFolder folder; + + /** + * This is used when deciding which db's to close to free up memory + * and other resources in an LRU manner. It doesn't track every operation + * on every object from the db, but high level things like open, commit, + * and perhaps some of the list methods. Commit should be a proxy for all + * the mutation methods. + * + * I'm allowing clients to set the last use time as well, so that + * nsIMsgFolder.msgDatabase can set the last use time. + */ + attribute PRTime lastUseTime; + + // get a message header for the given key. Caller must release()! + + nsIMsgDBHdr getMsgHdrForKey(in nsMsgKey key); + nsIMsgDBHdr getMsgHdrForMessageID(in string messageID); + + /** + * Get a message header for a Gmail message with the given X-GM-MSGID. + * @param {string} aGmailMessageID - The ID of the message to find. + * + * @returns the message, or null if not found (without throwing an error). + */ + nsIMsgDBHdr getMsgHdrForGMMsgID(in string aGmailMessageID); + //Returns whether or not this database contains the given key + boolean containsKey(in nsMsgKey key); + +/** + * Must call AddNewHdrToDB after creating. The idea is that you create + * a new header, fill in its properties, and then call AddNewHdrToDB. + * AddNewHdrToDB will send notifications to any listeners. + * + * @param aKey msgKey for the new header. If aKey is nsMsgKey_None, + * we will auto-assign a new key. + */ + nsIMsgDBHdr createNewHdr(in nsMsgKey aKey); + + void addNewHdrToDB(in nsIMsgDBHdr newHdr, in boolean notify); + + nsIMsgDBHdr copyHdrFromExistingHdr(in nsMsgKey key, in nsIMsgDBHdr existingHdr, in boolean addHdrToDB); + + /** + * Returns all message keys stored in the database. + * Keys are returned in the order as stored in the database. + * The caller should sort them if it needs to. + */ + Array<nsMsgKey> listAllKeys(); + + nsIMsgEnumerator enumerateMessages(); + nsIMsgEnumerator reverseEnumerateMessages(); + nsIMsgThreadEnumerator enumerateThreads(); + + /** + * Get an enumerator of messages matching the passed-in search terms. + * + * @param searchTerms Array of search terms to evaluate. + * @param reverse Start at the end, defaults to false. + * + * @returns An enumerator to iterate over matching messages. + */ + nsIMsgEnumerator getFilterEnumerator(in Array<nsIMsgSearchTerm> searchTerms, + [optional] in boolean reverse); + + // count the total and unread msgs, and adjust global count if needed + void syncCounts(); + + nsIMsgThread getThreadContainingMsgHdr(in nsIMsgDBHdr msgHdr) ; + + // helpers for user command functions like delete, mark read, etc. + + void markHdrRead(in nsIMsgDBHdr msgHdr, in boolean bRead, + in nsIDBChangeListener instigator); + + void markHdrReplied(in nsIMsgDBHdr msgHdr, in boolean bReplied, + in nsIDBChangeListener instigator); + + void markHdrMarked(in nsIMsgDBHdr msgHdr, in boolean mark, + in nsIDBChangeListener instigator); + /** + * Remove the new status from a message. + * + * @param aMsgHdr The database reference header for the message + * @param aInstigator Reference to original calling object + */ + void markHdrNotNew(in nsIMsgDBHdr aMsgHdr, + in nsIDBChangeListener aInstigator); + + // MDN support + void markMDNNeeded(in nsMsgKey key, in boolean bNeeded, + in nsIDBChangeListener instigator); + + void markMDNSent(in nsMsgKey key, in boolean bNeeded, + in nsIDBChangeListener instigator); + boolean isMDNSent(in nsMsgKey key); + + void markRead(in nsMsgKey key, in boolean bRead, + in nsIDBChangeListener instigator); + + void markReplied(in nsMsgKey key, in boolean bReplied, + in nsIDBChangeListener instigator); + + void markForwarded(in nsMsgKey key, in boolean bForwarded, + in nsIDBChangeListener instigator); + + void markRedirected(in nsMsgKey key, in boolean bRedirected, + in nsIDBChangeListener instigator); + + void markHasAttachments(in nsMsgKey key, in boolean bHasAttachments, + in nsIDBChangeListener instigator); + + Array<nsMsgKey> markThreadRead(in nsIMsgThread thread, in nsIDBChangeListener instigator); + + /// Mark the specified thread ignored. + void markThreadIgnored(in nsIMsgThread thread, in nsMsgKey threadKey, + in boolean bIgnored, + in nsIDBChangeListener instigator); + + /// Mark the specified thread watched. + void markThreadWatched(in nsIMsgThread thread, in nsMsgKey threadKey, + in boolean bWatched, + in nsIDBChangeListener instigator); + + /// Mark the specified subthread ignored. + void markHeaderKilled(in nsIMsgDBHdr msg, in boolean bIgnored, + in nsIDBChangeListener instigator); + + /// Is the message read. + boolean isRead(in nsMsgKey key); + /// Is the message part of an ignored thread. + boolean isIgnored(in nsMsgKey key); + /// Is the message part of a watched thread. + boolean isWatched(in nsMsgKey key); + /// Is the message flagged/starred. + boolean isMarked(in nsMsgKey key); + /// Does the message have attachments. + boolean hasAttachments(in nsMsgKey key); + + Array<nsMsgKey> markAllRead(); + + void deleteMessages(in Array<nsMsgKey> nsMsgKeys, + in nsIDBChangeListener instigator); + void deleteMessage(in nsMsgKey key, + in nsIDBChangeListener instigator, + in boolean commit); + void deleteHeader(in nsIMsgDBHdr msgHdr, in nsIDBChangeListener instigator, + in boolean commit, in boolean notify); + + /// Lower level routine that doesn't remove hdr from thread or adjust counts. + void removeHeaderMdbRow(in nsIMsgDBHdr msgHdr); + + void undoDelete(in nsIMsgDBHdr msgHdr); + + void markMarked(in nsMsgKey key, in boolean mark, + in nsIDBChangeListener instigator); + void markOffline(in nsMsgKey key, in boolean offline, + in nsIDBChangeListener instigator); + void setStringProperty(in nsMsgKey aKey, in string aProperty, in AUTF8String aValue); + /** + * Set the value of a string property in a message header + * + * @param msgHdr Header of the message whose property will be changed + * @param aProperty the property to change + * @param aValue new value for the property + */ + void setStringPropertyByHdr(in nsIMsgDBHdr msgHdr, in string aProperty, in AUTF8String aValue); + + /** + * Set the value of a uint32 property in a message header. + * + * @param aMsgHdr header of the message whose property will be changed + * @param aProperty the property to change + * @param aValue new value for the property + */ + void setUint32PropertyByHdr(in nsIMsgDBHdr aMsgHdr, + in string aProperty, in unsigned long aValue); + + void markImapDeleted(in nsMsgKey key, in boolean deleted, + in nsIDBChangeListener instigator); + + readonly attribute nsMsgKey firstNew; + + attribute nsIMsgRetentionSettings msgRetentionSettings; + // Purge unwanted message headers and/or bodies. If deleteViaFolder is + // true, we'll call nsIMsgFolder::DeleteMessages to delete the messages. + // Otherwise, we'll just delete them from the db. + void applyRetentionSettings(in nsIMsgRetentionSettings aMsgRetentionSettings, + in boolean aDeleteViaFolder); + + attribute nsIMsgDownloadSettings msgDownloadSettings; + + boolean hasNew(); + void clearNewList(in boolean notify); + void addToNewList(in nsMsgKey key); + + // Used mainly to force the timestamp of a local mail folder db to + // match the time stamp of the corresponding berkeley mail folder, + // but also useful to tell the summary to mark itself invalid + // Also, if a local folder is being reparsed, summary will be invalid + // until the reparsing is done. + attribute boolean summaryValid; + + Array<nsMsgKey> listAllOfflineMsgs(); + + void setAttributeOnPendingHdr(in nsIMsgDBHdr pendingHdr, in string property, + in string propertyVal); + + void setUint32AttributeOnPendingHdr(in nsIMsgDBHdr pendingHdr, in string property, + in unsigned long propertyVal); + + /** + * Sets a pending 64 bit attribute, which tells the DB that when a message + * which looks like the pendingHdr (e.g., same message-id) is added to the + * db, set the passed in property and value on the new header. This is + * usually because we've copied an imap message to a different folder, and + * want to carry forward attributes from the original message to the copy, + * but don't have the message hdr for the copy yet so we can't set + * attributes directly. + * + * @param aPendingHdr usually the source of the copy. + * @param aProperty name of property to set. + * @param aPropertyVal 64 bit value of property to set. + */ + void setUint64AttributeOnPendingHdr(in nsIMsgDBHdr aPendingHdr, + in string aProperty, + in unsigned long long aPropertyVal); + + /** + * Given a message header with its message-id set, update any pending + * attributes on the header. + * + * @param aNewHdr a new header that may have pending attributes. + */ + void updatePendingAttributes(in nsIMsgDBHdr aNewHdr); + + readonly attribute nsMsgKey lowWaterArticleNum; + readonly attribute nsMsgKey highWaterArticleNum; + attribute nsMsgKey nextPseudoMsgKey; //for undo-redo of move pop->imap + readonly attribute nsMsgKey nextFakeOfflineMsgKey; // for saving "fake" offline msg hdrs + // for sorting + Array<octet> createCollationKey(in AString sourceString); + long compareCollationKeys(in Array<octet> key1, in Array<octet> key2); + + // when creating a view, the default sort order and view flags + // use these for the default. (this allows news to override, so that + // news can be threaded by default) + readonly attribute nsMsgViewFlagsTypeValue defaultViewFlags; + readonly attribute nsMsgViewSortTypeValue defaultSortType; + readonly attribute nsMsgViewSortOrderValue defaultSortOrder; + + // for msg hdr hash table allocation. controllable by caller to improve folder loading performance. + attribute unsigned long msgHdrCacheSize; + + /** + * The list of messages currently in the NEW state. + */ + Array<nsMsgKey> getNewList(); + + // These are used for caching search hits in a db, to speed up saved search folders. + nsIMsgEnumerator getCachedHits(in AUTF8String aSearchFolderUri); + + /** + * Update search cache to ensure it contains aNewHits. + * + * @param aSearchFolderUri the target folder. + * @param aNewHits sorted list of new message keys. + * @returns list of keys of messages removed from cache. + */ + Array<nsMsgKey> refreshCache(in AUTF8String aSearchFolderUri, in Array<nsMsgKey> aNewHits); + void updateHdrInCache(in AUTF8String aSearchFolderUri, in nsIMsgDBHdr aHdr, in boolean aAdd); + boolean hdrIsInCache(in AUTF8String aSearchFolderUri, in nsIMsgDBHdr aHdr); +}; + +[scriptable, uuid(7f98410c-41b7-4a55-8e0c-02107e7f4c0f)] +interface nsIMsgOfflineOpsDatabase : nsIMsgDatabase { + // Has to be in nsMailDatabase, since local folders can be move destinations. + + nsIMsgOfflineImapOperation getOfflineOpForKey(in nsMsgKey messageKey, in boolean create); + void removeOfflineOp(in nsIMsgOfflineImapOperation op); + Array<nsMsgKey> listAllOfflineOpIds(); + Array<nsMsgKey> listAllOfflineDeletes(); +}; diff --git a/comm/mailnews/db/msgdb/public/nsIMsgOfflineImapOperation.idl b/comm/mailnews/db/msgdb/public/nsIMsgOfflineImapOperation.idl new file mode 100644 index 0000000000..bc2aecfa28 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsIMsgOfflineImapOperation.idl @@ -0,0 +1,50 @@ +/* -*- 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" +#include "nsIImapUrl.idl" // for imapMessageFlagsType + +typedef long nsOfflineImapOperationType; + +[scriptable, uuid(b5229a55-22bb-444b-be92-13d719353828)] + +interface nsIMsgOfflineImapOperation : nsISupports +{ +// type of stored imap operations + const long kFlagsChanged = 0x1; + const long kMsgMoved = 0x2; + const long kMsgCopy = 0x4; + const long kMoveResult = 0x8; + const long kAppendDraft = 0x10; + const long kAddedHeader = 0x20; + const long kDeletedMsg = 0x40; + const long kMsgMarkedDeleted = 0x80; + const long kAppendTemplate = 0x100; + const long kDeleteAllMsgs = 0x200; + const long kAddKeywords = 0x400; + const long kRemoveKeywords = 0x800; + + attribute nsOfflineImapOperationType operation; + void clearOperation(in nsOfflineImapOperationType operation); + attribute nsMsgKey messageKey; + + // for move/copy operations, the msg key of the source msg. + attribute nsMsgKey srcMessageKey; + + attribute imapMessageFlagsType flagOperation; + attribute imapMessageFlagsType newFlags; // for kFlagsChanged + attribute AUTF8String destinationFolderURI; // for move or copy + attribute AUTF8String sourceFolderURI; + void addKeywordToAdd(in string aKeyword); + void addKeywordToRemove(in string aKeyword); + readonly attribute string keywordsToAdd; + readonly attribute string keywordsToRemove; + readonly attribute long numberOfCopies; + void addMessageCopyOperation(in AUTF8String destinationBox); + string getCopyDestination(in long copyIndex); + attribute unsigned long msgSize; + attribute boolean playingBack; +}; diff --git a/comm/mailnews/db/msgdb/public/nsINewsDatabase.idl b/comm/mailnews/db/msgdb/public/nsINewsDatabase.idl new file mode 100644 index 0000000000..151a42f019 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsINewsDatabase.idl @@ -0,0 +1,18 @@ +/* -*- 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" + +%{C++ +#include "nsMsgKeySet.h" +%} + +[ptr] native nsMsgKeySetPtr(nsMsgKeySet); + +[scriptable, uuid(f700208a-1dd1-11b2-b947-e4e1e4fdf278)] + +interface nsINewsDatabase : nsISupports { + [noscript] attribute nsMsgKeySetPtr readSet; +}; diff --git a/comm/mailnews/db/msgdb/public/nsImapMailDatabase.h b/comm/mailnews/db/msgdb/public/nsImapMailDatabase.h new file mode 100644 index 0000000000..307ad2ed0a --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsImapMailDatabase.h @@ -0,0 +1,46 @@ +/* -*- 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 _nsImapMailDatabase_H_ +#define _nsImapMailDatabase_H_ + +#include "mozilla/Attributes.h" +#include "nsMailDatabase.h" + +class nsImapMailDatabase : public nsMailDatabase { + public: + // OK, it's dumb that this should require a fileSpec, since there is no file + // for the folder. This is mainly because we're deriving from nsMailDatabase; + // Perhaps we shouldn't... + nsImapMailDatabase(); + virtual ~nsImapMailDatabase(); + + NS_IMETHOD GetSummaryValid(bool* aResult) override; + NS_IMETHOD SetSummaryValid(bool valid = true) override; + virtual nsresult AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) override; + + NS_IMETHOD ForceClosed() override; + NS_IMETHOD AddNewHdrToDB(nsIMsgDBHdr* newHdr, bool notify) override; + NS_IMETHOD SetAttributeOnPendingHdr(nsIMsgDBHdr* pendingHdr, + const char* property, + const char* propertyVal) override; + NS_IMETHOD SetUint32AttributeOnPendingHdr(nsIMsgDBHdr* pendingHdr, + const char* property, + uint32_t propertyVal) override; + NS_IMETHOD SetUint64AttributeOnPendingHdr(nsIMsgDBHdr* aPendingHdr, + const char* aProperty, + uint64_t aPropertyVal) override; + NS_IMETHOD DeleteMessages(nsTArray<nsMsgKey> const& nsMsgKeys, + nsIDBChangeListener* instigator) override; + NS_IMETHOD UpdatePendingAttributes(nsIMsgDBHdr* aNewHdr) override; + + protected: + nsresult GetRowForPendingHdr(nsIMsgDBHdr* pendingHdr, nsIMdbRow** row); + nsresult GetAllPendingHdrsTable(); + mdb_token m_pendingHdrsRowScopeToken; + mdb_token m_pendingHdrsTableKindToken; + nsCOMPtr<nsIMdbTable> m_mdbAllPendingHdrsTable; +}; + +#endif diff --git a/comm/mailnews/db/msgdb/public/nsMailDatabase.h b/comm/mailnews/db/msgdb/public/nsMailDatabase.h new file mode 100644 index 0000000000..4e29b26322 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsMailDatabase.h @@ -0,0 +1,62 @@ +/* -*- 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 _nsMailDatabase_H_ +#define _nsMailDatabase_H_ + +#include "mozilla/Attributes.h" +#include "nsMsgDatabase.h" +#include "nsTArray.h" + +#include "nsIDBChangeListener.h" +#include "nsIMsgOfflineImapOperation.h" +#include "nsISimpleEnumerator.h" +#include "nsIFile.h" + +// This is the subclass of nsMsgDatabase that handles local mail messages. + +class nsMailDatabase : public nsMsgDatabase { + public: + nsMailDatabase(); + virtual ~nsMailDatabase(); + NS_IMETHOD ForceClosed() override; + NS_IMETHOD DeleteMessages(nsTArray<nsMsgKey> const& nsMsgKeys, + nsIDBChangeListener* instigator) override; + + nsresult Open(nsMsgDBService* aDBService, nsIFile* aSummaryFile, bool create, + bool upgrading) override; + virtual nsMailDatabase* GetMailDB() { return this; } + + virtual uint32_t GetCurVersion() override { return kMsgDBVersion; } + + NS_IMETHOD GetOfflineOpForKey(nsMsgKey opKey, bool create, + nsIMsgOfflineImapOperation** op) override; + NS_IMETHOD RemoveOfflineOp(nsIMsgOfflineImapOperation* op) override; + + NS_IMETHOD SetSummaryValid(bool valid) override; + NS_IMETHOD GetSummaryValid(bool* valid) override; + + NS_IMETHOD ListAllOfflineOpIds(nsTArray<nsMsgKey>& offlineOpIds) override; + NS_IMETHOD ListAllOfflineDeletes(nsTArray<nsMsgKey>& offlineDeletes) override; + + friend class nsMsgOfflineOpEnumerator; + + protected: + nsresult GetAllOfflineOpsTable(); // get this on demand + + // get the time and date of the mailbox file + void GetMailboxModProperties(int64_t* aSize, uint32_t* aDate); + + nsCOMPtr<nsIMdbTable> m_mdbAllOfflineOpsTable; + mdb_token m_offlineOpsRowScopeToken; + mdb_token m_offlineOpsTableKindToken; + + virtual void SetReparse(bool reparse); + + protected: + bool m_reparse; +}; + +#endif diff --git a/comm/mailnews/db/msgdb/public/nsMsgDatabase.h b/comm/mailnews/db/msgdb/public/nsMsgDatabase.h new file mode 100644 index 0000000000..f61b9a7b25 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsMsgDatabase.h @@ -0,0 +1,447 @@ +/* -*- 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 _nsMsgDatabase_H_ +#define _nsMsgDatabase_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Path.h" +#include "nsIFile.h" +#include "nsIMsgDatabase.h" +#include "nsMsgHdr.h" +#include "nsString.h" +#include "nsIDBChangeAnnouncer.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgFolder.h" +#include "nsDBFolderInfo.h" +#include "mozilla/intl/Collator.h" +#include "nsIMimeConverter.h" +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "PLDHashTable.h" +#include "nsTArray.h" +#include "nsTObserverArray.h" + +using mozilla::intl::Collator; + +class nsMsgThread; +class nsMsgDatabase; +class nsIMsgOfflineOpsDatabase; +class nsIMsgThread; +class nsMsgDBEnumerator; +class nsMsgDBThreadEnumerator; + +const int32_t kMsgDBVersion = 1; + +// Hopefully we're not opening up lots of databases at the same time, however +// this will give us a buffer before we need to start reallocating the cache +// array. +const uint32_t kInitialMsgDBCacheSize = 20; + +class nsMsgDBService final : public nsIMsgDBService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGDBSERVICE + + nsMsgDBService(); + + void AddToCache(nsMsgDatabase* pMessageDB); + void DumpCache(); + void EnsureCached(nsMsgDatabase* pMessageDB) { + if (!m_dbCache.Contains(pMessageDB)) m_dbCache.AppendElement(pMessageDB); + } + void RemoveFromCache(nsMsgDatabase* pMessageDB) { + m_dbCache.RemoveElement(pMessageDB); + } + + protected: + ~nsMsgDBService(); + void HookupPendingListeners(nsIMsgDatabase* db, nsIMsgFolder* folder); + void FinishDBOpen(nsIMsgFolder* aFolder, nsMsgDatabase* aMsgDB); + nsMsgDatabase* FindInCache(nsIFile* dbName); + + nsCOMArray<nsIMsgFolder> m_foldersPendingListeners; + nsCOMArray<nsIDBChangeListener> m_pendingListeners; + AutoTArray<nsMsgDatabase*, kInitialMsgDBCacheSize> m_dbCache; +}; + +namespace mozilla { +namespace mailnews { +class MsgDBReporter; +} +} // namespace mozilla + +class nsMsgDatabase : public nsIMsgOfflineOpsDatabase { + public: + friend class nsMsgDBService; + friend class nsMsgPropertyEnumerator; // accesses m_mdbEnv and m_mdbStore + + NS_DECL_ISUPPORTS + NS_DECL_NSIDBCHANGEANNOUNCER + NS_DECL_NSIMSGDATABASE + NS_DECL_NSIMSGOFFLINEOPSDATABASE + + /** + * Opens a database folder. + * + * @param aFolderName The name of the folder to create. + * @param aCreate Whether or not the file should be created. + * @param aLeaveInvalidDB Set to true if you do not want the database to be + * deleted if it is invalid. + * @exception NS_ERROR_FILE_NOT_FOUND + * The file could not be created. + * @exception NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE + * The database is present (and was opened), but the + * summary file is out of date. + * @exception NS_MSG_ERROR_FOLDER_SUMMARY_MISSING + * The database is present (and was opened), but the + * summary file is missing. + */ + virtual nsresult Open(nsMsgDBService* aDBService, nsIFile* aFolderName, + bool aCreate, bool aLeaveInvalidDB); + virtual nsresult IsHeaderRead(nsIMsgDBHdr* hdr, bool* pRead); + virtual nsresult MarkHdrReadInDB(nsIMsgDBHdr* msgHdr, bool bRead, + nsIDBChangeListener* instigator); + nsresult OpenInternal(nsMsgDBService* aDBService, nsIFile* aFolderName, + bool aCreate, bool aLeaveInvalidDB, bool sync); + nsresult CheckForErrors(nsresult err, bool sync, nsMsgDBService* aDBService, + nsIFile* summaryFile); + virtual nsresult OpenMDB(nsIFile* dbfile, bool create, bool sync); + virtual nsresult CloseMDB(bool commit); + virtual nsresult CreateMsgHdr(nsIMdbRow* hdrRow, nsMsgKey key, + nsIMsgDBHdr** result); + virtual nsresult GetThreadForMsgKey(nsMsgKey msgKey, nsIMsgThread** result); + virtual nsresult EnumerateMessagesWithFlag(nsIMsgEnumerator** result, + uint32_t* pFlag); + nsresult GetSearchResultsTable(const nsACString& searchFolderUri, + bool createIfMissing, nsIMdbTable** table); + + ////////////////////////////////////////////////////////////////////////////// + // nsMsgDatabase methods: + nsMsgDatabase(); + + nsresult GetMDBFactory(nsIMdbFactory** aMdbFactory); + nsIMdbEnv* GetEnv() { return m_mdbEnv; } + nsIMdbStore* GetStore() { return m_mdbStore; } + virtual uint32_t GetCurVersion(); + nsresult GetCollationKeyGenerator(); + nsIMimeConverter* GetMimeConverter(); + + nsresult GetTableCreateIfMissing(const char* scope, const char* kind, + nsIMdbTable** table, mdb_token& scopeToken, + mdb_token& kindToken); + + // helper function to fill in nsStrings from hdr row cell contents. + nsresult RowCellColumnTonsString(nsIMdbRow* row, mdb_token columnToken, + nsAString& resultStr); + nsresult RowCellColumnToUInt32(nsIMdbRow* row, mdb_token columnToken, + uint32_t* uint32Result, + uint32_t defaultValue = 0); + nsresult RowCellColumnToUInt32(nsIMdbRow* row, mdb_token columnToken, + uint32_t& uint32Result, + uint32_t defaultValue = 0); + nsresult RowCellColumnToUInt64(nsIMdbRow* row, mdb_token columnToken, + uint64_t* uint64Result, + uint64_t defaultValue = 0); + nsresult RowCellColumnToMime2DecodedString(nsIMdbRow* row, + mdb_token columnToken, + nsAString& resultStr); + nsresult RowCellColumnToCollationKey(nsIMdbRow* row, mdb_token columnToken, + nsTArray<uint8_t>& result); + nsresult RowCellColumnToConstCharPtr(nsIMdbRow* row, mdb_token columnToken, + const char** ptr); + nsresult RowCellColumnToAddressCollationKey(nsIMdbRow* row, + mdb_token colToken, + nsTArray<uint8_t>& result); + + nsresult GetEffectiveCharset(nsIMdbRow* row, nsACString& resultCharset); + + // these methods take the property name as a string, not a token. + // they should be used when the properties aren't accessed a lot + nsresult GetProperty(nsIMdbRow* row, const char* propertyName, char** result); + nsresult SetProperty(nsIMdbRow* row, const char* propertyName, + const char* propertyVal); + nsresult GetPropertyAsNSString(nsIMdbRow* row, const char* propertyName, + nsAString& result); + nsresult SetPropertyFromNSString(nsIMdbRow* row, const char* propertyName, + const nsAString& propertyVal); + nsresult GetUint32Property(nsIMdbRow* row, const char* propertyName, + uint32_t* result, uint32_t defaultValue = 0); + nsresult GetUint64Property(nsIMdbRow* row, const char* propertyName, + uint64_t* result, uint64_t defaultValue = 0); + nsresult SetUint32Property(nsIMdbRow* row, const char* propertyName, + uint32_t propertyVal); + nsresult SetUint64Property(nsIMdbRow* row, const char* propertyName, + uint64_t propertyVal); + nsresult GetBooleanProperty(nsIMdbRow* row, const char* propertyName, + bool* result, bool defaultValue = false); + nsresult SetBooleanProperty(nsIMdbRow* row, const char* propertyName, + bool propertyVal); + // helper function for once we have the token. + nsresult SetNSStringPropertyWithToken(nsIMdbRow* row, mdb_token aProperty, + const nsAString& propertyStr); + + // helper functions to put values in cells for the passed-in row + nsresult UInt32ToRowCellColumn(nsIMdbRow* row, mdb_token columnToken, + uint32_t value); + nsresult CharPtrToRowCellColumn(nsIMdbRow* row, mdb_token columnToken, + const char* charPtr); + nsresult RowCellColumnToCharPtr(nsIMdbRow* row, mdb_token columnToken, + char** result); + nsresult UInt64ToRowCellColumn(nsIMdbRow* row, mdb_token columnToken, + uint64_t value); + + // helper functions to copy an nsString to a yarn, int32 to yarn, and vice + // versa. + static struct mdbYarn* nsStringToYarn(struct mdbYarn* yarn, + const nsAString& str); + static struct mdbYarn* UInt32ToYarn(struct mdbYarn* yarn, uint32_t i); + static struct mdbYarn* UInt64ToYarn(struct mdbYarn* yarn, uint64_t i); + static void YarnTonsString(struct mdbYarn* yarn, nsAString& str); + static void YarnTonsCString(struct mdbYarn* yarn, nsACString& str); + static void YarnToUInt32(struct mdbYarn* yarn, uint32_t* i); + static void YarnToUInt64(struct mdbYarn* yarn, uint64_t* i); + +#ifdef DEBUG + virtual nsresult DumpContents(); +#endif + + friend class nsMsgHdr; // use this to get access to cached tokens for hdr + // fields + friend class nsMsgThread; // use this to get access to cached tokens for hdr + // fields + + friend class nsMsgDBEnumerator; + friend class nsMsgDBThreadEnumerator; + + protected: + virtual ~nsMsgDatabase(); + + // prefs stuff - in future, we might want to cache the prefs interface + nsresult GetBoolPref(const char* prefName, bool* result); + nsresult GetIntPref(const char* prefName, int32_t* result); + virtual void GetGlobalPrefs(); + // retrieval methods + nsIMsgThread* GetThreadForReference(nsCString& msgID, nsIMsgDBHdr** pMsgHdr); + nsIMsgThread* GetThreadForSubject(nsCString& subject); + nsIMsgThread* GetThreadForMessageId(nsCString& msgId); + nsIMsgThread* GetThreadForThreadId(nsMsgKey threadId); + nsMsgHdr* GetMsgHdrForReference(nsCString& reference); + nsIMsgDBHdr* GetMsgHdrForSubject(nsCString& subject); + // threading interfaces + virtual nsresult CreateNewThread(nsMsgKey key, const char* subject, + nsMsgThread** newThread); + virtual bool ThreadBySubjectWithoutRe(); + virtual bool UseStrictThreading(); + virtual bool UseCorrectThreading(); + virtual nsresult ThreadNewHdr(nsMsgHdr* hdr, bool& newThread); + virtual nsresult AddNewThread(nsMsgHdr* msgHdr); + virtual nsresult AddToThread(nsMsgHdr* newHdr, nsIMsgThread* thread, + nsIMsgDBHdr* pMsgHdr, bool threadInThread); + + static PRTime gLastUseTime; // global last use time + PRTime m_lastUseTime; // last use time for this db + // inline to make instrumentation as cheap as possible + inline void RememberLastUseTime() { gLastUseTime = m_lastUseTime = PR_Now(); } + + bool MatchDbName(nsIFile* dbName); // returns TRUE if they match + + // Flag handling routines + virtual nsresult SetKeyFlag(nsMsgKey key, bool set, nsMsgMessageFlagType flag, + nsIDBChangeListener* instigator = nullptr); + virtual nsresult SetMsgHdrFlag(nsIMsgDBHdr* msgHdr, bool set, + nsMsgMessageFlagType flag, + nsIDBChangeListener* instigator); + + virtual bool SetHdrFlag(nsIMsgDBHdr*, bool bSet, nsMsgMessageFlagType flag); + virtual bool SetHdrReadFlag(nsIMsgDBHdr*, bool pRead); + virtual uint32_t GetStatusFlags(nsIMsgDBHdr* msgHdr, + nsMsgMessageFlagType origFlags); + // helper function which doesn't involve thread object + + virtual nsresult RemoveHeaderFromDB(nsMsgHdr* msgHdr); + virtual nsresult RemoveHeaderFromThread(nsMsgHdr* msgHdr); + virtual nsresult AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr); + + mozilla::UniquePtr<mozilla::intl::Collator> m_collationKeyGenerator = nullptr; + nsCOMPtr<nsIMimeConverter> m_mimeConverter; + nsCOMPtr<nsIMsgRetentionSettings> m_retentionSettings; + nsCOMPtr<nsIMsgDownloadSettings> m_downloadSettings; + + nsresult FindMessagesOlderThan(uint32_t daysToKeepHdrs, + bool applyToFlaggedMessages, + nsTArray<RefPtr<nsIMsgDBHdr>>& hdrsToDelete); + nsresult FindExcessMessages(uint32_t numHeadersToKeep, + bool applyToFlaggedMessages, + nsTArray<RefPtr<nsIMsgDBHdr>>& hdrsToDelete); + + // mdb bookkeeping stuff + virtual nsresult InitExistingDB(); + virtual nsresult InitNewDB(); + virtual nsresult InitMDBInfo(); + + nsCOMPtr<nsIMsgFolder> m_folder; + RefPtr<nsDBFolderInfo> m_dbFolderInfo; + nsMsgKey m_nextPseudoMsgKey; + nsIMdbEnv* m_mdbEnv; // to be used in all the db calls. + nsIMdbStore* m_mdbStore; + nsIMdbTable* m_mdbAllMsgHeadersTable; + nsIMdbTable* m_mdbAllThreadsTable; + + // Used for asynchronous db opens. If non-null, we're still opening + // the underlying mork database. If null, the db has been completely opened. + nsCOMPtr<nsIMdbThumb> m_thumb; + // used to remember the args to Open for async open. + bool m_create; + bool m_leaveInvalidDB; + + nsCOMPtr<nsIFile> m_dbFile; + nsTArray<nsMsgKey> m_newSet; // new messages since last open. + bool m_mdbTokensInitialized; + nsTObserverArray<nsCOMPtr<nsIDBChangeListener>> m_ChangeListeners; + mdb_token m_hdrRowScopeToken; + mdb_token m_threadRowScopeToken; + mdb_token m_hdrTableKindToken; + mdb_token m_threadTableKindToken; + mdb_token m_allThreadsTableKindToken; + mdb_token m_subjectColumnToken; + mdb_token m_senderColumnToken; + mdb_token m_messageIdColumnToken; + mdb_token m_referencesColumnToken; + mdb_token m_recipientsColumnToken; + mdb_token m_dateColumnToken; + mdb_token m_messageSizeColumnToken; + mdb_token m_flagsColumnToken; + mdb_token m_priorityColumnToken; + mdb_token m_labelColumnToken; + mdb_token m_numLinesColumnToken; + mdb_token m_ccListColumnToken; + mdb_token m_bccListColumnToken; + mdb_token m_threadFlagsColumnToken; + mdb_token m_threadIdColumnToken; + mdb_token m_threadChildrenColumnToken; + mdb_token m_threadUnreadChildrenColumnToken; + mdb_token m_messageThreadIdColumnToken; + mdb_token m_threadSubjectColumnToken; + mdb_token m_messageCharSetColumnToken; + mdb_token m_threadParentColumnToken; + mdb_token m_threadRootKeyColumnToken; + mdb_token m_threadNewestMsgDateColumnToken; + mdb_token m_offlineMsgOffsetColumnToken; + mdb_token m_offlineMessageSizeColumnToken; + + // header caching stuff - MRU headers, keeps them around in memory + nsresult AddHdrToCache(nsIMsgDBHdr* hdr, nsMsgKey key); + nsresult ClearHdrCache(bool reInit); + nsresult RemoveHdrFromCache(nsIMsgDBHdr* hdr, nsMsgKey key); + // all headers currently instantiated, doesn't hold refs + // these get added when msg hdrs get constructed, and removed when they get + // destroyed. + nsresult GetHdrFromUseCache(nsMsgKey key, nsIMsgDBHdr** result); + nsresult AddHdrToUseCache(nsIMsgDBHdr* hdr, nsMsgKey key); + nsresult ClearUseHdrCache(); + nsresult RemoveHdrFromUseCache(nsIMsgDBHdr* hdr, nsMsgKey key); + + // not-reference holding array of threads we've handed out. + // If a db goes away, it will clean up the outstanding threads. + // We use an nsTArray because we don't expect to ever have very many + // of these, rarely more than 5. + nsTArray<nsMsgThread*> m_threads; + // Clear outstanding thread objects + void ClearThreads(); + nsMsgThread* FindExistingThread(nsMsgKey threadId); + + mdb_pos FindInsertIndexInSortedTable(nsIMdbTable* table, mdb_id idToInsert); + + void ClearCachedObjects(bool dbGoingAway); + void InvalidateEnumerators(); + // all instantiated headers, but doesn't hold refs. + PLDHashTable* m_headersInUse; + static PLDHashNumber HashKey(const void* aKey); + static bool MatchEntry(const PLDHashEntryHdr* aEntry, const void* aKey); + static void MoveEntry(PLDHashTable* aTable, const PLDHashEntryHdr* aFrom, + PLDHashEntryHdr* aTo); + static void ClearEntry(PLDHashTable* aTable, PLDHashEntryHdr* aEntry); + static PLDHashTableOps gMsgDBHashTableOps; + struct MsgHdrHashElement : public PLDHashEntryHdr { + nsMsgKey mKey; + nsIMsgDBHdr* mHdr; + }; + PLDHashTable* m_cachedHeaders; + bool m_bCacheHeaders; + nsMsgKey m_cachedThreadId; + nsCOMPtr<nsIMsgThread> m_cachedThread; + nsCOMPtr<nsIMdbFactory> mMdbFactory; + + // Message reference hash table + static PLDHashTableOps gRefHashTableOps; + struct RefHashElement : public PLDHashEntryHdr { + const char* mRef; // Hash entry key, must come first + nsMsgKey mThreadId; + uint32_t mCount; + }; + PLDHashTable* m_msgReferences; + nsresult GetRefFromHash(nsCString& reference, nsMsgKey* threadId); + nsresult AddRefToHash(nsCString& reference, nsMsgKey threadId); + nsresult AddMsgRefsToHash(nsIMsgDBHdr* msgHdr); + nsresult RemoveRefFromHash(nsCString& reference); + nsresult RemoveMsgRefsFromHash(nsIMsgDBHdr* msgHdr); + nsresult InitRefHash(); + + // The enumerators add themselves to these lists. + // If a db goes away - via destruction or ForceClosed() - it needs to + // invalidate any outstanding enumerators. + nsTArray<nsMsgDBEnumerator*> m_msgEnumerators; + nsTArray<nsMsgDBThreadEnumerator*> m_threadEnumerators; + + // Memory reporter details + public: + static size_t HeaderHashSizeOf(PLDHashEntryHdr* hdr, + mozilla::MallocSizeOf aMallocSizeOf, + void* arg); + virtual size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + virtual size_t SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + private: + uint32_t m_cacheSize; + RefPtr<mozilla::mailnews::MsgDBReporter> mMemReporter; +}; + +class nsMsgRetentionSettings : public nsIMsgRetentionSettings { + public: + nsMsgRetentionSettings(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGRETENTIONSETTINGS + protected: + virtual ~nsMsgRetentionSettings(); + nsMsgRetainByPreference m_retainByPreference; + uint32_t m_daysToKeepHdrs; + uint32_t m_numHeadersToKeep; + bool m_useServerDefaults; + bool m_cleanupBodiesByDays; + uint32_t m_daysToKeepBodies; + bool m_applyToFlaggedMessages; +}; + +class nsMsgDownloadSettings : public nsIMsgDownloadSettings { + public: + nsMsgDownloadSettings(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGDOWNLOADSETTINGS + protected: + virtual ~nsMsgDownloadSettings(); + bool m_useServerDefaults; + bool m_downloadUnreadOnly; + bool m_downloadByDate; + int32_t m_ageLimitOfMsgsToDownload; +}; + +#endif diff --git a/comm/mailnews/db/msgdb/public/nsMsgHdr.h b/comm/mailnews/db/msgdb/public/nsMsgHdr.h new file mode 100644 index 0000000000..94e5b1b8c8 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsMsgHdr.h @@ -0,0 +1,92 @@ +/* -*- 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 _nsMsgHdr_H +#define _nsMsgHdr_H + +#include "mozilla/MemoryReporting.h" +#include "nsIMsgHdr.h" +#include "nsString.h" +#include "MailNewsTypes.h" +#include "mdb.h" +#include "nsTArray.h" + +class nsMsgDatabase; +class nsIMsgThread; + +class nsMsgHdr : public nsIMsgDBHdr { + public: + NS_DECL_NSIMSGDBHDR + friend class nsMsgDatabase; + friend class nsImapMailDatabase; + friend class nsMsgPropertyEnumerator; + friend class nsMsgThread; + + //////////////////////////////////////////////////////////////////////////// + //////////////////////////////////////////////////////////////////////////// + // nsMsgHdr methods: + nsMsgHdr(nsMsgDatabase* db, nsIMdbRow* dbRow); + + NS_DECL_ISUPPORTS + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOfFun) const { + return m_references.ShallowSizeOfExcludingThis(aMallocSizeOfFun); + } + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOfFun) const { + return aMallocSizeOfFun(this) + SizeOfExcludingThis(aMallocSizeOfFun); + } + + protected: + nsIMdbRow* GetMDBRow() { return m_mdbRow; } + void ReleaseMDBRow() { NS_IF_RELEASE(m_mdbRow); } + nsMsgDatabase* GetMdb() { return m_mdb; } + void ClearCachedValues() { m_initedValues = 0; } + + virtual nsresult GetRawFlags(uint32_t* result); + + bool IsParentOf(nsIMsgDBHdr* possibleChild); + bool IsAncestorOf(nsIMsgDBHdr* possibleChild); + + private: + virtual ~nsMsgHdr(); + + void Init(); + virtual nsresult InitFlags(); + virtual nsresult InitCachedValues(); + + bool IsAncestorKilled(uint32_t ancestorsToCheck); + void ReparentInThread(nsIMsgThread* thread); + + nsresult SetStringColumn(const char* str, mdb_token token); + nsresult SetUInt32Column(uint32_t value, mdb_token token); + nsresult GetUInt32Column(mdb_token token, uint32_t* pvalue, + uint32_t defaultValue = 0); + nsresult SetUInt64Column(uint64_t value, mdb_token token); + nsresult GetUInt64Column(mdb_token token, uint64_t* pvalue, + uint64_t defaultValue = 0); + + // reference and threading stuff. + nsresult ParseReferences(const char* references); + const char* GetNextReference(const char* startNextRef, nsCString& reference, + bool acceptNonDelimitedReferences); + + nsMsgKey m_threadId; + nsMsgKey m_messageKey; // news: article number, local mail: key, imap: uid... + nsMsgKey m_threadParent; // message this is a reply to, in thread. + PRTime m_date; + uint32_t m_messageSize; // lines for news articles, bytes for mail messages + uint32_t m_flags; + // avoid parsing references every time we want one + nsTArray<nsCString> m_references; + + // nsMsgHdrs will have to know what db and row they belong to, since they are + // really just a wrapper around the msg row in the mdb. This could cause + // problems, though I hope not. + nsMsgDatabase* m_mdb; + nsIMdbRow* m_mdbRow; + uint32_t m_initedValues; +}; + +#endif diff --git a/comm/mailnews/db/msgdb/public/nsMsgThread.h b/comm/mailnews/db/msgdb/public/nsMsgThread.h new file mode 100644 index 0000000000..37add58082 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsMsgThread.h @@ -0,0 +1,65 @@ +/* -*- 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 _nsMsgThread_H +#define _nsMsgThread_H + +#include "nsIMsgThread.h" +#include "nsString.h" +#include "MailNewsTypes.h" +#include "mdb.h" + +class nsIMdbTable; +class nsIMsgDBHdr; +class nsMsgDatabase; + +class nsMsgThread : public nsIMsgThread { + public: + nsMsgThread(); + nsMsgThread(nsMsgDatabase* db, nsIMdbTable* table); + + friend class nsMsgThreadEnumerator; + friend class nsMsgDatabase; + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGTHREAD + + RefPtr<nsMsgDatabase> m_mdbDB; + + protected: + virtual ~nsMsgThread(); + + void Init(); + void Clear(); + virtual nsresult InitCachedValues(); + nsresult ChangeChildCount(int32_t delta); + nsresult ChangeUnreadChildCount(int32_t delta); + nsresult RemoveChild(nsMsgKey msgKey); + nsresult SetThreadRootKey(nsMsgKey threadRootKey); + nsresult GetChildHdrForKey(nsMsgKey desiredKey, nsIMsgDBHdr** result, + int32_t* resultIndex); + nsresult RerootThread(nsIMsgDBHdr* newParentOfOldRoot, nsIMsgDBHdr* oldRoot, + nsIDBChangeAnnouncer* announcer); + nsresult ReparentChildrenOf(nsMsgKey oldParent, nsMsgKey newParent, + nsIDBChangeAnnouncer* announcer); + + nsresult ReparentNonReferenceChildrenOf(nsIMsgDBHdr* topLevelHdr, + nsMsgKey newParentKey, + nsIDBChangeAnnouncer* announcer); + nsresult ReparentMsgsWithInvalidParent(uint32_t numChildren, + nsMsgKey threadParentKey); + + nsMsgKey m_threadKey; + uint32_t m_numChildren; + uint32_t m_numUnreadChildren; + uint32_t m_flags; + nsCOMPtr<nsIMdbTable> m_mdbTable; + nsCOMPtr<nsIMdbRow> m_metaRow; + bool m_cachedValuesInitialized; + nsMsgKey m_threadRootKey; + uint32_t m_newestMsgDate; +}; + +#endif diff --git a/comm/mailnews/db/msgdb/public/nsNewsDatabase.h b/comm/mailnews/db/msgdb/public/nsNewsDatabase.h new file mode 100644 index 0000000000..4a804d0d69 --- /dev/null +++ b/comm/mailnews/db/msgdb/public/nsNewsDatabase.h @@ -0,0 +1,57 @@ +/* -*- 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 _nsNewsDatabase_H_ +#define _nsNewsDatabase_H_ + +#include "mozilla/Attributes.h" +#include "nsMsgDatabase.h" +#include "nsINewsDatabase.h" +#include "nsTArray.h" +#include "nsIMsgHdr.h" + +// news group database + +class nsNewsDatabase : public nsMsgDatabase, public nsINewsDatabase { + public: + nsNewsDatabase(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSINEWSDATABASE + + NS_IMETHOD Close(bool forceCommit) override; + NS_IMETHOD ForceClosed() override; + NS_IMETHOD Commit(nsMsgDBCommit commitType) override; + virtual uint32_t GetCurVersion() override; + + // methods to get and set docsets for ids. + NS_IMETHOD IsRead(nsMsgKey key, bool* pRead) override; + virtual nsresult IsHeaderRead(nsIMsgDBHdr* msgHdr, bool* pRead) override; + + NS_IMETHOD GetHighWaterArticleNum(nsMsgKey* key) override; + NS_IMETHOD GetLowWaterArticleNum(nsMsgKey* key) override; + NS_IMETHOD MarkAllRead(nsTArray<nsMsgKey>& thoseMarked) override; + + virtual nsresult ExpireUpTo(nsMsgKey expireKey); + virtual nsresult ExpireRange(nsMsgKey startRange, nsMsgKey endRange); + + virtual bool SetHdrReadFlag(nsIMsgDBHdr* msgHdr, bool bRead) override; + + virtual nsresult AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) override; + nsresult SyncWithReadSet(); + + NS_IMETHOD GetDefaultViewFlags( + nsMsgViewFlagsTypeValue* aDefaultViewFlags) override; + NS_IMETHOD GetDefaultSortType( + nsMsgViewSortTypeValue* aDefaultSortType) override; + NS_IMETHOD GetDefaultSortOrder( + nsMsgViewSortOrderValue* aDefaultSortOrder) override; + + protected: + virtual ~nsNewsDatabase(); + // this is owned by the nsNewsFolder, which lives longer than the db. + nsMsgKeySet* m_readSet; +}; + +#endif diff --git a/comm/mailnews/db/msgdb/src/components.conf b/comm/mailnews/db/msgdb/src/components.conf new file mode 100644 index 0000000000..1d65e685f2 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/components.conf @@ -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/. + +Classes = [ + { + "cid": "{a86c86ae-e97f-11d2-a506-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/nsMsgDatabase/msgDB-mailbox"], + "type": "nsMailDatabase", + "headers": ["/comm/mailnews/db/msgdb/public/nsMailDatabase.h"], + }, + { + "cid": "{36414aa0-e980-11d2-a506-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/nsMsgDatabase/msgDB-news"], + "type": "nsNewsDatabase", + "headers": ["/comm/mailnews/db/msgdb/public/nsNewsDatabase.h"], + }, + { + "cid": "{9e4b07ee-e980-11d2-a506-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/nsMsgDatabase/msgDB-imap"], + "type": "nsImapMailDatabase", + "headers": ["/comm/mailnews/db/msgdb/public/nsImapMailDatabase.h"], + }, + { + "cid": "{1bd976d6-df44-11d4-a5b6-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/msgDatabase/retentionSettings;1"], + "type": "nsMsgRetentionSettings", + "headers": ["/comm/mailnews/db/msgdb/public/nsMsgDatabase.h"], + }, + { + "cid": "{4e3dae5a-157a-11d5-a5c0-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/msgDatabase/downloadSettings;1"], + "type": "nsMsgDownloadSettings", + "headers": ["/comm/mailnews/db/msgdb/public/nsMsgDatabase.h"], + }, + { + "cid": "{03223c50-1e88-45e8-ba1a-7ce792dc3fc3}", + "contract_ids": ["@mozilla.org/msgDatabase/msgDBService;1"], + "type": "nsMsgDBService", + "headers": ["/comm/mailnews/db/msgdb/public/nsMsgDatabase.h"], + "name": "DB", + "interfaces": ["nsIMsgDBService"], + }, +] diff --git a/comm/mailnews/db/msgdb/src/moz.build b/comm/mailnews/db/msgdb/src/moz.build new file mode 100644 index 0000000000..06c2b92475 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/moz.build @@ -0,0 +1,22 @@ +# 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/. + +SOURCES += [ + "nsDBFolderInfo.cpp", + "nsImapMailDatabase.cpp", + "nsMailDatabase.cpp", + "nsMsgDatabase.cpp", + "nsMsgDatabaseEnumerators.cpp", + "nsMsgHdr.cpp", + "nsMsgOfflineImapOperation.cpp", + "nsMsgThread.cpp", + "nsNewsDatabase.cpp", +] + +FINAL_LIBRARY = "mail" + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/comm/mailnews/db/msgdb/src/nsDBFolderInfo.cpp b/comm/mailnews/db/msgdb/src/nsDBFolderInfo.cpp new file mode 100644 index 0000000000..57118ffaf8 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsDBFolderInfo.cpp @@ -0,0 +1,749 @@ +/* -*- 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 "nsDBFolderInfo.h" +#include "nsMsgDatabase.h" +#include "nsMsgFolderFlags.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIPrefLocalizedString.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIMsgDBView.h" +#include "nsServiceManagerUtils.h" +#include "nsImapCore.h" + +static const char* kDBFolderInfoScope = "ns:msg:db:row:scope:dbfolderinfo:all"; +static const char* kDBFolderInfoTableKind = "ns:msg:db:table:kind:dbfolderinfo"; + +struct mdbOid gDBFolderInfoOID; + +static const char* kNumMessagesColumnName = "numMsgs"; +// have to leave this as numNewMsgs even though it's numUnread Msgs +static const char* kNumUnreadMessagesColumnName = "numNewMsgs"; +static const char* kFlagsColumnName = "flags"; +static const char* kFolderSizeColumnName = "folderSize"; +static const char* kExpungedBytesColumnName = "expungedBytes"; +static const char* kFolderDateColumnName = "folderDate"; +static const char* kHighWaterMessageKeyColumnName = "highWaterKey"; + +static const char* kImapUidValidityColumnName = "UIDValidity"; +static const char* kTotalPendingMessagesColumnName = "totPendingMsgs"; +static const char* kUnreadPendingMessagesColumnName = "unreadPendingMsgs"; +static const char* kMailboxNameColumnName = "mailboxName"; +static const char* kKnownArtsSetColumnName = "knownArts"; +static const char* kExpiredMarkColumnName = "expiredMark"; +static const char* kVersionColumnName = "version"; +static const char* kLocaleColumnName = "locale"; + +NS_IMPL_ADDREF(nsDBFolderInfo) +NS_IMPL_RELEASE(nsDBFolderInfo) + +NS_IMETHODIMP +nsDBFolderInfo::QueryInterface(REFNSIID iid, void** result) { + if (!result) return NS_ERROR_NULL_POINTER; + + *result = nullptr; + if (iid.Equals(NS_GET_IID(nsIDBFolderInfo)) || + iid.Equals(NS_GET_IID(nsISupports))) { + *result = static_cast<nsIDBFolderInfo*>(this); + AddRef(); + return NS_OK; + } + return NS_NOINTERFACE; +} + +nsDBFolderInfo::nsDBFolderInfo(nsMsgDatabase* mdb) + : m_flags(0), + m_expiredMark(0), + m_tableKindToken(0), + m_expiredMarkColumnToken(0) { + m_mdbTable = NULL; + m_mdbRow = NULL; + m_version = 1; // for upgrading... + m_IMAPHierarchySeparator = 0; // imap path separator + // mail only (for now) + m_folderSize = 0; + m_folderDate = 0; + m_expungedBytes = 0; // sum of size of deleted messages in folder + m_highWaterMessageKey = 0; + + m_numUnreadMessages = 0; + m_numMessages = 0; + // IMAP only + m_ImapUidValidity = kUidUnknown; + m_totalPendingMessages = 0; + m_unreadPendingMessages = 0; + + m_mdbTokensInitialized = false; + + m_mdb = mdb; + if (mdb) { + nsresult err; + + err = m_mdb->GetStore()->StringToToken(mdb->GetEnv(), kDBFolderInfoScope, + &m_rowScopeToken); + if (NS_SUCCEEDED(err)) { + err = m_mdb->GetStore()->StringToToken( + mdb->GetEnv(), kDBFolderInfoTableKind, &m_tableKindToken); + if (NS_SUCCEEDED(err)) { + gDBFolderInfoOID.mOid_Scope = m_rowScopeToken; + gDBFolderInfoOID.mOid_Id = 1; + } + } + InitMDBInfo(); + } +} + +nsDBFolderInfo::~nsDBFolderInfo() { + // nsMsgDatabase strictly owns nsDBFolderInfo, so don't ref-count db. + ReleaseExternalReferences(); +} + +// Release any objects we're holding onto. This needs to be safe +// to call multiple times. +void nsDBFolderInfo::ReleaseExternalReferences() { + if (m_mdb) { + if (m_mdbTable) { + NS_RELEASE(m_mdbTable); + m_mdbTable = nullptr; + } + if (m_mdbRow) { + NS_RELEASE(m_mdbRow); + m_mdbRow = nullptr; + } + m_mdb = nullptr; + } +} + +// this routine sets up a new db to know about the dbFolderInfo stuff... +nsresult nsDBFolderInfo::AddToNewMDB() { + nsresult ret = NS_OK; + if (m_mdb && m_mdb->GetStore()) { + nsIMdbStore* store = m_mdb->GetStore(); + // create the unique table for the dbFolderInfo. + nsresult err = + store->NewTable(m_mdb->GetEnv(), m_rowScopeToken, m_tableKindToken, + true, nullptr, &m_mdbTable); + + // create the singleton row for the dbFolderInfo. + err = store->NewRowWithOid(m_mdb->GetEnv(), &gDBFolderInfoOID, &m_mdbRow); + + // add the row to the singleton table. + if (m_mdbRow && NS_SUCCEEDED(err)) + err = m_mdbTable->AddRow(m_mdb->GetEnv(), m_mdbRow); + + ret = err; // what are we going to do about nsresult's? + } + return ret; +} + +nsresult nsDBFolderInfo::InitFromExistingDB() { + nsresult ret = NS_OK; + if (m_mdb && m_mdb->GetStore()) { + nsIMdbStore* store = m_mdb->GetStore(); + if (store) { + mdb_pos rowPos; + mdb_count outTableCount; // current number of such tables + mdb_bool mustBeUnique; // whether port can hold only one of these + mdb_bool hasOid; + ret = store->GetTableKind(m_mdb->GetEnv(), m_rowScopeToken, + m_tableKindToken, &outTableCount, &mustBeUnique, + &m_mdbTable); + // NS_ASSERTION(mustBeUnique && outTableCount == 1, "only one global db + // info allowed"); + + if (m_mdbTable) { + // find singleton row for global info. + ret = m_mdbTable->HasOid(m_mdb->GetEnv(), &gDBFolderInfoOID, &hasOid); + if (NS_SUCCEEDED(ret)) { + nsIMdbTableRowCursor* rowCursor; + rowPos = -1; + ret = m_mdbTable->GetTableRowCursor(m_mdb->GetEnv(), rowPos, + &rowCursor); + if (NS_SUCCEEDED(ret)) { + ret = rowCursor->NextRow(m_mdb->GetEnv(), &m_mdbRow, &rowPos); + NS_RELEASE(rowCursor); + if (!m_mdbRow) ret = NS_ERROR_FAILURE; + if (NS_SUCCEEDED(ret)) LoadMemberVariables(); + } + } + } else + ret = NS_ERROR_FAILURE; + } + } + return ret; +} + +nsresult nsDBFolderInfo::InitMDBInfo() { + nsresult ret = NS_OK; + if (!m_mdbTokensInitialized && m_mdb && m_mdb->GetStore()) { + nsIMdbStore* store = m_mdb->GetStore(); + nsIMdbEnv* env = m_mdb->GetEnv(); + + store->StringToToken(env, kNumMessagesColumnName, + &m_numMessagesColumnToken); + store->StringToToken(env, kNumUnreadMessagesColumnName, + &m_numUnreadMessagesColumnToken); + store->StringToToken(env, kFlagsColumnName, &m_flagsColumnToken); + store->StringToToken(env, kFolderSizeColumnName, &m_folderSizeColumnToken); + store->StringToToken(env, kExpungedBytesColumnName, + &m_expungedBytesColumnToken); + store->StringToToken(env, kFolderDateColumnName, &m_folderDateColumnToken); + + store->StringToToken(env, kHighWaterMessageKeyColumnName, + &m_highWaterMessageKeyColumnToken); + store->StringToToken(env, kMailboxNameColumnName, + &m_mailboxNameColumnToken); + + store->StringToToken(env, kImapUidValidityColumnName, + &m_imapUidValidityColumnToken); + store->StringToToken(env, kTotalPendingMessagesColumnName, + &m_totalPendingMessagesColumnToken); + store->StringToToken(env, kUnreadPendingMessagesColumnName, + &m_unreadPendingMessagesColumnToken); + store->StringToToken(env, kExpiredMarkColumnName, + &m_expiredMarkColumnToken); + store->StringToToken(env, kVersionColumnName, &m_versionColumnToken); + m_mdbTokensInitialized = true; + } + + return ret; +} + +nsresult nsDBFolderInfo::LoadMemberVariables() { + // it's really not an error for these properties to not exist... + GetInt32PropertyWithToken(m_numMessagesColumnToken, m_numMessages); + GetInt32PropertyWithToken(m_numUnreadMessagesColumnToken, + m_numUnreadMessages); + GetInt32PropertyWithToken(m_flagsColumnToken, m_flags); + GetInt64PropertyWithToken(m_folderSizeColumnToken, m_folderSize); + GetUint32PropertyWithToken(m_folderDateColumnToken, m_folderDate); + GetInt32PropertyWithToken(m_imapUidValidityColumnToken, m_ImapUidValidity, + kUidUnknown); + GetUint32PropertyWithToken(m_expiredMarkColumnToken, m_expiredMark); + GetInt64PropertyWithToken(m_expungedBytesColumnToken, m_expungedBytes); + GetUint32PropertyWithToken(m_highWaterMessageKeyColumnToken, + m_highWaterMessageKey); + int32_t version; + + GetInt32PropertyWithToken(m_versionColumnToken, version); + m_version = (uint16_t)version; + + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetVersion(uint32_t version) { + m_version = version; + return SetUint32PropertyWithToken(m_versionColumnToken, (uint32_t)m_version); +} + +NS_IMETHODIMP nsDBFolderInfo::GetVersion(uint32_t* version) { + *version = m_version; + return NS_OK; +} + +nsresult nsDBFolderInfo::AdjustHighWater(nsMsgKey highWater, bool force) { + if (force || m_highWaterMessageKey < highWater) { + m_highWaterMessageKey = highWater; + SetUint32PropertyWithToken(m_highWaterMessageKeyColumnToken, highWater); + } + + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetHighWater(nsMsgKey highWater) { + return AdjustHighWater(highWater, true); +} + +NS_IMETHODIMP nsDBFolderInfo::OnKeyAdded(nsMsgKey aNewKey) { + return AdjustHighWater(aNewKey, false); +} + +NS_IMETHODIMP +nsDBFolderInfo::GetFolderSize(int64_t* size) { + NS_ENSURE_ARG_POINTER(size); + *size = m_folderSize; + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetFolderSize(int64_t size) { + m_folderSize = size; + return SetInt64Property(kFolderSizeColumnName, m_folderSize); +} + +NS_IMETHODIMP +nsDBFolderInfo::GetFolderDate(uint32_t* folderDate) { + NS_ENSURE_ARG_POINTER(folderDate); + *folderDate = m_folderDate; + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetFolderDate(uint32_t folderDate) { + m_folderDate = folderDate; + return SetUint32PropertyWithToken(m_folderDateColumnToken, folderDate); +} + +NS_IMETHODIMP nsDBFolderInfo::GetHighWater(nsMsgKey* result) { + // Sanity check highwater - if it gets too big, other code + // can fail. Look through last 100 messages to recalculate + // the highwater mark. + *result = m_highWaterMessageKey; + if (m_highWaterMessageKey > 0xFFFFFF00 && m_mdb) { + nsCOMPtr<nsIMsgEnumerator> hdrs; + nsresult rv = m_mdb->ReverseEnumerateMessages(getter_AddRefs(hdrs)); + if (NS_FAILED(rv)) return rv; + bool hasMore = false; + nsCOMPtr<nsIMsgDBHdr> pHeader; + nsMsgKey recalculatedHighWater = 1; + int32_t i = 0; + while (i++ < 100 && NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && + hasMore) { + (void)hdrs->GetNext(getter_AddRefs(pHeader)); + if (pHeader) { + nsMsgKey msgKey; + pHeader->GetMessageKey(&msgKey); + if (msgKey > recalculatedHighWater) recalculatedHighWater = msgKey; + } + } + NS_ASSERTION(m_highWaterMessageKey >= recalculatedHighWater, + "highwater incorrect"); + m_highWaterMessageKey = recalculatedHighWater; + } + *result = m_highWaterMessageKey; + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetExpiredMark(nsMsgKey expiredKey) { + m_expiredMark = expiredKey; + return SetUint32PropertyWithToken(m_expiredMarkColumnToken, expiredKey); +} + +NS_IMETHODIMP nsDBFolderInfo::GetExpiredMark(nsMsgKey* result) { + *result = m_expiredMark; + return NS_OK; +} + +// The size of the argument depends on the maximum size of a single message +NS_IMETHODIMP nsDBFolderInfo::ChangeExpungedBytes(int32_t delta) { + return SetExpungedBytes(m_expungedBytes + delta); +} + +NS_IMETHODIMP nsDBFolderInfo::SetMailboxName(const nsAString& newBoxName) { + return SetPropertyWithToken(m_mailboxNameColumnToken, newBoxName); +} + +NS_IMETHODIMP nsDBFolderInfo::GetMailboxName(nsAString& boxName) { + return GetPropertyWithToken(m_mailboxNameColumnToken, boxName); +} + +NS_IMETHODIMP nsDBFolderInfo::ChangeNumUnreadMessages(int32_t delta) { + m_numUnreadMessages += delta; + // m_numUnreadMessages can never be set to negative. + if (m_numUnreadMessages < 0) { +#ifdef DEBUG_bienvenu1 + NS_ASSERTION(false, "Hardcoded assertion"); +#endif + m_numUnreadMessages = 0; + } + return SetUint32PropertyWithToken(m_numUnreadMessagesColumnToken, + m_numUnreadMessages); +} + +NS_IMETHODIMP nsDBFolderInfo::ChangeNumMessages(int32_t delta) { + m_numMessages += delta; + // m_numMessages can never be set to negative. + if (m_numMessages < 0) { +#ifdef DEBUG_bienvenu + NS_ASSERTION(false, "num messages can't be < 0"); +#endif + m_numMessages = 0; + } + return SetUint32PropertyWithToken(m_numMessagesColumnToken, m_numMessages); +} + +NS_IMETHODIMP nsDBFolderInfo::GetNumUnreadMessages(int32_t* result) { + *result = m_numUnreadMessages; + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetNumUnreadMessages(int32_t numUnreadMessages) { + m_numUnreadMessages = numUnreadMessages; + return SetUint32PropertyWithToken(m_numUnreadMessagesColumnToken, + m_numUnreadMessages); +} + +NS_IMETHODIMP nsDBFolderInfo::GetNumMessages(int32_t* result) { + *result = m_numMessages; + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetNumMessages(int32_t numMessages) { + m_numMessages = numMessages; + return SetUint32PropertyWithToken(m_numMessagesColumnToken, m_numMessages); +} + +NS_IMETHODIMP nsDBFolderInfo::GetExpungedBytes(int64_t* result) { + *result = m_expungedBytes; + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetExpungedBytes(int64_t expungedBytes) { + m_expungedBytes = expungedBytes; + return SetInt64PropertyWithToken(m_expungedBytesColumnToken, m_expungedBytes); +} + +NS_IMETHODIMP nsDBFolderInfo::GetFlags(int32_t* result) { + *result = m_flags; + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetFlags(int32_t flags) { + nsresult ret = NS_OK; + + if (m_flags != flags) { + NS_ASSERTION((m_flags & nsMsgFolderFlags::Inbox) == 0 || + (flags & nsMsgFolderFlags::Inbox) != 0, + "lost inbox flag"); + m_flags = flags; + ret = SetInt32PropertyWithToken(m_flagsColumnToken, m_flags); + } + return ret; +} + +NS_IMETHODIMP nsDBFolderInfo::OrFlags(int32_t flags, int32_t* result) { + m_flags |= flags; + *result = m_flags; + return SetInt32PropertyWithToken(m_flagsColumnToken, m_flags); +} + +NS_IMETHODIMP nsDBFolderInfo::AndFlags(int32_t flags, int32_t* result) { + m_flags &= flags; + *result = m_flags; + return SetInt32PropertyWithToken(m_flagsColumnToken, m_flags); +} + +NS_IMETHODIMP nsDBFolderInfo::GetImapUidValidity(int32_t* result) { + *result = m_ImapUidValidity; + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetImapUidValidity(int32_t uidValidity) { + m_ImapUidValidity = uidValidity; + return SetUint32PropertyWithToken(m_imapUidValidityColumnToken, + m_ImapUidValidity); +} + +bool nsDBFolderInfo::TestFlag(int32_t flags) { return (m_flags & flags) != 0; } + +NS_IMETHODIMP +nsDBFolderInfo::GetLocale(nsAString& result) { + GetProperty(kLocaleColumnName, result); + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetLocale(const nsAString& locale) { + return SetProperty(kLocaleColumnName, locale); +} + +NS_IMETHODIMP +nsDBFolderInfo::GetImapTotalPendingMessages(int32_t* result) { + NS_ENSURE_ARG_POINTER(result); + *result = m_totalPendingMessages; + return NS_OK; +} + +void nsDBFolderInfo::ChangeImapTotalPendingMessages(int32_t delta) { + m_totalPendingMessages += delta; + SetInt32PropertyWithToken(m_totalPendingMessagesColumnToken, + m_totalPendingMessages); +} + +NS_IMETHODIMP +nsDBFolderInfo::GetImapUnreadPendingMessages(int32_t* result) { + NS_ENSURE_ARG_POINTER(result); + *result = m_unreadPendingMessages; + return NS_OK; +} + +NS_IMETHODIMP nsDBFolderInfo::SetImapUnreadPendingMessages( + int32_t numUnreadPendingMessages) { + m_unreadPendingMessages = numUnreadPendingMessages; + return SetUint32PropertyWithToken(m_unreadPendingMessagesColumnToken, + m_unreadPendingMessages); +} + +NS_IMETHODIMP nsDBFolderInfo::SetImapTotalPendingMessages( + int32_t numTotalPendingMessages) { + m_totalPendingMessages = numTotalPendingMessages; + return SetUint32PropertyWithToken(m_totalPendingMessagesColumnToken, + m_totalPendingMessages); +} + +void nsDBFolderInfo::ChangeImapUnreadPendingMessages(int32_t delta) { + m_unreadPendingMessages += delta; + SetInt32PropertyWithToken(m_unreadPendingMessagesColumnToken, + m_unreadPendingMessages); +} + +/* attribute nsMsgViewTypeValue viewType; */ +NS_IMETHODIMP nsDBFolderInfo::GetViewType(nsMsgViewTypeValue* aViewType) { + uint32_t viewTypeValue; + nsresult rv = GetUint32Property("viewType", nsMsgViewType::eShowAllThreads, + &viewTypeValue); + *aViewType = viewTypeValue; + return rv; +} +NS_IMETHODIMP nsDBFolderInfo::SetViewType(nsMsgViewTypeValue aViewType) { + return SetUint32Property("viewType", aViewType); +} + +/* attribute nsMsgViewFlagsTypeValue viewFlags; */ +NS_IMETHODIMP nsDBFolderInfo::GetViewFlags( + nsMsgViewFlagsTypeValue* aViewFlags) { + nsMsgViewFlagsTypeValue defaultViewFlags; + nsresult rv = m_mdb->GetDefaultViewFlags(&defaultViewFlags); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t viewFlagsValue; + rv = GetUint32Property("viewFlags", defaultViewFlags, &viewFlagsValue); + *aViewFlags = viewFlagsValue; + return rv; +} +NS_IMETHODIMP nsDBFolderInfo::SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) { + return SetUint32Property("viewFlags", aViewFlags); +} + +/* attribute nsMsgViewSortTypeValue sortType; */ +NS_IMETHODIMP nsDBFolderInfo::GetSortType(nsMsgViewSortTypeValue* aSortType) { + nsMsgViewSortTypeValue defaultSortType; + nsresult rv = m_mdb->GetDefaultSortType(&defaultSortType); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t sortTypeValue; + rv = GetUint32Property("sortType", defaultSortType, &sortTypeValue); + *aSortType = sortTypeValue; + return rv; +} +NS_IMETHODIMP nsDBFolderInfo::SetSortType(nsMsgViewSortTypeValue aSortType) { + return SetUint32Property("sortType", aSortType); +} + +/* attribute nsMsgViewSortOrderValue sortOrder; */ +NS_IMETHODIMP nsDBFolderInfo::GetSortOrder( + nsMsgViewSortOrderValue* aSortOrder) { + nsMsgViewSortOrderValue defaultSortOrder; + nsresult rv = m_mdb->GetDefaultSortOrder(&defaultSortOrder); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t sortOrderValue; + rv = GetUint32Property("sortOrder", defaultSortOrder, &sortOrderValue); + *aSortOrder = sortOrderValue; + return rv; +} + +NS_IMETHODIMP nsDBFolderInfo::SetSortOrder(nsMsgViewSortOrderValue aSortOrder) { + return SetUint32Property("sortOrder", aSortOrder); +} + +NS_IMETHODIMP nsDBFolderInfo::SetKnownArtsSet(const char* newsArtSet) { + return m_mdb->SetProperty(m_mdbRow, kKnownArtsSetColumnName, newsArtSet); +} + +NS_IMETHODIMP nsDBFolderInfo::GetKnownArtsSet(char** newsArtSet) { + return m_mdb->GetProperty(m_mdbRow, kKnownArtsSetColumnName, newsArtSet); +} + +// get arbitrary property, aka row cell value. +NS_IMETHODIMP nsDBFolderInfo::GetProperty(const char* propertyName, + nsAString& resultProperty) { + return m_mdb->GetPropertyAsNSString(m_mdbRow, propertyName, resultProperty); +} + +NS_IMETHODIMP nsDBFolderInfo::SetCharProperty( + const char* aPropertyName, const nsACString& aPropertyValue) { + return m_mdb->SetProperty(m_mdbRow, aPropertyName, + PromiseFlatCString(aPropertyValue).get()); +} + +NS_IMETHODIMP nsDBFolderInfo::GetCharProperty(const char* propertyName, + nsACString& resultProperty) { + nsCString result; + nsresult rv = + m_mdb->GetProperty(m_mdbRow, propertyName, getter_Copies(result)); + if (NS_SUCCEEDED(rv)) resultProperty.Assign(result); + return rv; +} + +NS_IMETHODIMP nsDBFolderInfo::SetUint32Property(const char* propertyName, + uint32_t propertyValue) { + return m_mdb->SetUint32Property(m_mdbRow, propertyName, propertyValue); +} + +NS_IMETHODIMP nsDBFolderInfo::SetInt64Property(const char* propertyName, + int64_t propertyValue) { + return m_mdb->SetUint64Property(m_mdbRow, propertyName, + (uint64_t)propertyValue); +} + +NS_IMETHODIMP nsDBFolderInfo::SetProperty(const char* propertyName, + const nsAString& propertyStr) { + return m_mdb->SetPropertyFromNSString(m_mdbRow, propertyName, propertyStr); +} + +nsresult nsDBFolderInfo::SetPropertyWithToken(mdb_token aProperty, + const nsAString& propertyStr) { + return m_mdb->SetNSStringPropertyWithToken(m_mdbRow, aProperty, propertyStr); +} + +nsresult nsDBFolderInfo::SetUint32PropertyWithToken(mdb_token aProperty, + uint32_t propertyValue) { + return m_mdb->UInt32ToRowCellColumn(m_mdbRow, aProperty, propertyValue); +} + +nsresult nsDBFolderInfo::SetInt64PropertyWithToken(mdb_token aProperty, + int64_t propertyValue) { + return m_mdb->UInt64ToRowCellColumn(m_mdbRow, aProperty, + (uint64_t)propertyValue); +} + +nsresult nsDBFolderInfo::SetInt32PropertyWithToken(mdb_token aProperty, + int32_t propertyValue) { + nsAutoString propertyStr; + propertyStr.AppendInt(propertyValue, 16); + return SetPropertyWithToken(aProperty, propertyStr); +} + +nsresult nsDBFolderInfo::GetPropertyWithToken(mdb_token aProperty, + nsAString& resultProperty) { + return m_mdb->RowCellColumnTonsString(m_mdbRow, aProperty, resultProperty); +} + +nsresult nsDBFolderInfo::GetUint32PropertyWithToken(mdb_token aProperty, + uint32_t& propertyValue, + uint32_t defaultValue) { + return m_mdb->RowCellColumnToUInt32(m_mdbRow, aProperty, propertyValue, + defaultValue); +} + +nsresult nsDBFolderInfo::GetInt32PropertyWithToken(mdb_token aProperty, + int32_t& propertyValue, + int32_t defaultValue) { + return m_mdb->RowCellColumnToUInt32(m_mdbRow, aProperty, + (uint32_t&)propertyValue, defaultValue); +} + +NS_IMETHODIMP nsDBFolderInfo::GetUint32Property(const char* propertyName, + uint32_t defaultValue, + uint32_t* propertyValue) { + return m_mdb->GetUint32Property(m_mdbRow, propertyName, propertyValue, + defaultValue); +} + +NS_IMETHODIMP nsDBFolderInfo::GetInt64Property(const char* propertyName, + int64_t defaultValue, + int64_t* propertyValue) { + return m_mdb->GetUint64Property(m_mdbRow, propertyName, + (uint64_t*)&propertyValue, defaultValue); +} + +nsresult nsDBFolderInfo::GetInt64PropertyWithToken(mdb_token aProperty, + int64_t& propertyValue, + int64_t defaultValue) { + return m_mdb->RowCellColumnToUInt64(m_mdbRow, aProperty, + (uint64_t*)&propertyValue, defaultValue); +} + +NS_IMETHODIMP nsDBFolderInfo::GetBooleanProperty(const char* propertyName, + bool defaultValue, + bool* propertyValue) { + uint32_t defaultUint32Value = (defaultValue) ? 1 : 0; + uint32_t returnValue; + nsresult rv = m_mdb->GetUint32Property(m_mdbRow, propertyName, &returnValue, + defaultUint32Value); + *propertyValue = (returnValue != 0); + return rv; +} +NS_IMETHODIMP nsDBFolderInfo::SetBooleanProperty(const char* propertyName, + bool propertyValue) { + return m_mdb->SetUint32Property(m_mdbRow, propertyName, + propertyValue ? 1 : 0); +} + +NS_IMETHODIMP nsDBFolderInfo::GetFolderName(nsACString& folderName) { + return GetCharProperty("folderName", folderName); +} + +NS_IMETHODIMP nsDBFolderInfo::SetFolderName(const nsACString& folderName) { + return SetCharProperty("folderName", folderName); +} + +class nsTransferDBFolderInfo : public nsDBFolderInfo { + public: + nsTransferDBFolderInfo(); + virtual ~nsTransferDBFolderInfo(); + // parallel arrays of properties and values + nsTArray<nsCString> m_properties; + nsTArray<nsCString> m_values; +}; + +nsTransferDBFolderInfo::nsTransferDBFolderInfo() : nsDBFolderInfo(nullptr) {} + +nsTransferDBFolderInfo::~nsTransferDBFolderInfo() {} + +/* void GetTransferInfo (out nsIDBFolderInfo transferInfo); */ +NS_IMETHODIMP nsDBFolderInfo::GetTransferInfo(nsIDBFolderInfo** transferInfo) { + NS_ENSURE_ARG_POINTER(transferInfo); + NS_ENSURE_STATE(m_mdbRow); + + RefPtr<nsTransferDBFolderInfo> newInfo = new nsTransferDBFolderInfo; + + mdb_count numCells; + mdbYarn cellYarn; + mdb_column cellColumn; + char columnName[100]; + mdbYarn cellName = {columnName, 0, sizeof(columnName), 0, 0, nullptr}; + + m_mdbRow->GetCount(m_mdb->GetEnv(), &numCells); + // iterate over the cells in the dbfolderinfo remembering attribute names and + // values. + for (mdb_count cellIndex = 0; cellIndex < numCells; cellIndex++) { + nsresult err = m_mdbRow->SeekCellYarn(m_mdb->GetEnv(), cellIndex, + &cellColumn, nullptr); + if (NS_SUCCEEDED(err)) { + err = m_mdbRow->AliasCellYarn(m_mdb->GetEnv(), cellColumn, &cellYarn); + if (NS_SUCCEEDED(err)) { + m_mdb->GetStore()->TokenToString(m_mdb->GetEnv(), cellColumn, + &cellName); + newInfo->m_values.AppendElement( + Substring((const char*)cellYarn.mYarn_Buf, + (const char*)cellYarn.mYarn_Buf + cellYarn.mYarn_Fill)); + newInfo->m_properties.AppendElement( + Substring((const char*)cellName.mYarn_Buf, + (const char*)cellName.mYarn_Buf + cellName.mYarn_Fill)); + } + } + } + + newInfo.forget(transferInfo); + return NS_OK; +} + +/* void InitFromTransferInfo (in nsIDBFolderInfo transferInfo); */ +NS_IMETHODIMP nsDBFolderInfo::InitFromTransferInfo( + nsIDBFolderInfo* aTransferInfo) { + NS_ENSURE_ARG(aTransferInfo); + + nsTransferDBFolderInfo* transferInfo = + static_cast<nsTransferDBFolderInfo*>(aTransferInfo); + + for (uint32_t i = 0; i < transferInfo->m_values.Length(); i++) + SetCharProperty(transferInfo->m_properties[i].get(), + transferInfo->m_values[i]); + + LoadMemberVariables(); + return NS_OK; +} diff --git a/comm/mailnews/db/msgdb/src/nsImapMailDatabase.cpp b/comm/mailnews/db/msgdb/src/nsImapMailDatabase.cpp new file mode 100644 index 0000000000..7103d0c475 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsImapMailDatabase.cpp @@ -0,0 +1,217 @@ +/* -*- 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 <sys/stat.h> + +#include "msgCore.h" +#include "nsImapMailDatabase.h" +#include "nsDBFolderInfo.h" +#include "nsMsgMessageFlags.h" + +const char* kPendingHdrsScope = + "ns:msg:db:row:scope:pending:all"; // scope for all offine ops table +const char* kPendingHdrsTableKind = "ns:msg:db:table:kind:pending"; +struct mdbOid gAllPendingHdrsTableOID; + +nsImapMailDatabase::nsImapMailDatabase() + : m_pendingHdrsRowScopeToken(0), m_pendingHdrsTableKindToken(0) {} + +nsImapMailDatabase::~nsImapMailDatabase() {} + +NS_IMETHODIMP nsImapMailDatabase::GetSummaryValid(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + if (m_dbFolderInfo) { + uint32_t version; + m_dbFolderInfo->GetVersion(&version); + *aResult = (GetCurVersion() == version); + } else + *aResult = false; + + return NS_OK; +} + +NS_IMETHODIMP nsImapMailDatabase::SetSummaryValid(bool valid) { + if (m_dbFolderInfo) { + m_dbFolderInfo->SetVersion(valid ? GetCurVersion() : 0); + Commit(nsMsgDBCommitType::kLargeCommit); + } + return NS_OK; +} + +// We override this to avoid our parent class (nsMailDatabase)'s +// grabbing of the folder semaphore, and bailing on failure. +NS_IMETHODIMP nsImapMailDatabase::DeleteMessages( + nsTArray<nsMsgKey> const& nsMsgKeys, nsIDBChangeListener* instigator) { + return nsMsgDatabase::DeleteMessages(nsMsgKeys, instigator); +} + +nsresult nsImapMailDatabase::AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) { + uint32_t msgFlags; + msgHdr->GetFlags(&msgFlags); + if (msgFlags & nsMsgMessageFlags::Offline && m_dbFolderInfo) { + uint32_t size = 0; + (void)msgHdr->GetOfflineMessageSize(&size); + return m_dbFolderInfo->ChangeExpungedBytes(size); + } + return NS_OK; +} + +NS_IMETHODIMP nsImapMailDatabase::ForceClosed() { + m_mdbAllPendingHdrsTable = nullptr; + return nsMailDatabase::ForceClosed(); +} + +nsresult nsImapMailDatabase::GetAllPendingHdrsTable() { + nsresult rv = NS_OK; + if (!m_mdbAllPendingHdrsTable) + rv = GetTableCreateIfMissing(kPendingHdrsScope, kPendingHdrsTableKind, + getter_AddRefs(m_mdbAllPendingHdrsTable), + m_pendingHdrsRowScopeToken, + m_pendingHdrsTableKindToken); + return rv; +} + +NS_IMETHODIMP nsImapMailDatabase::AddNewHdrToDB(nsIMsgDBHdr* newHdr, + bool notify) { + nsresult rv = nsMsgDatabase::AddNewHdrToDB(newHdr, notify); + if (NS_SUCCEEDED(rv)) rv = UpdatePendingAttributes(newHdr); + return rv; +} + +NS_IMETHODIMP nsImapMailDatabase::UpdatePendingAttributes( + nsIMsgDBHdr* aNewHdr) { + nsresult rv = GetAllPendingHdrsTable(); + NS_ENSURE_SUCCESS(rv, rv); + mdb_count numPendingHdrs = 0; + m_mdbAllPendingHdrsTable->GetCount(GetEnv(), &numPendingHdrs); + if (numPendingHdrs > 0) { + mdbYarn messageIdYarn; + nsCOMPtr<nsIMdbRow> pendingRow; + mdbOid outRowId; + + nsCString messageId; + aNewHdr->GetMessageId(getter_Copies(messageId)); + messageIdYarn.mYarn_Buf = (void*)messageId.get(); + messageIdYarn.mYarn_Fill = messageId.Length(); + messageIdYarn.mYarn_Form = 0; + messageIdYarn.mYarn_Size = messageIdYarn.mYarn_Fill; + + m_mdbStore->FindRow(GetEnv(), m_pendingHdrsRowScopeToken, + m_messageIdColumnToken, &messageIdYarn, &outRowId, + getter_AddRefs(pendingRow)); + if (pendingRow) { + mdb_count numCells; + mdbYarn cellYarn; + mdb_column cellColumn; + uint32_t existingFlags; + + pendingRow->GetCount(GetEnv(), &numCells); + aNewHdr->GetFlags(&existingFlags); + // iterate over the cells in the pending hdr setting properties on the + // aNewHdr. we skip cell 0, which is the messageId; + nsMsgHdr* msgHdr = + static_cast<nsMsgHdr*>(aNewHdr); // closed system, cast ok + nsIMdbRow* row = msgHdr->GetMDBRow(); + for (mdb_count cellIndex = 1; cellIndex < numCells; cellIndex++) { + nsresult err = + pendingRow->SeekCellYarn(GetEnv(), cellIndex, &cellColumn, nullptr); + if (NS_SUCCEEDED(err)) { + err = pendingRow->AliasCellYarn(GetEnv(), cellColumn, &cellYarn); + if (NS_SUCCEEDED(err)) { + if (row) row->AddColumn(GetEnv(), cellColumn, &cellYarn); + } + } + } + // We might have changed some cached values, so force a refresh. + msgHdr->ClearCachedValues(); + uint32_t resultFlags; + msgHdr->OrFlags(existingFlags, &resultFlags); + m_mdbAllPendingHdrsTable->CutRow(GetEnv(), pendingRow); + pendingRow->CutAllColumns(GetEnv()); + } + } + return rv; +} + +nsresult nsImapMailDatabase::GetRowForPendingHdr(nsIMsgDBHdr* pendingHdr, + nsIMdbRow** row) { + nsresult rv = GetAllPendingHdrsTable(); + NS_ENSURE_SUCCESS(rv, rv); + + mdbYarn messageIdYarn; + nsCOMPtr<nsIMdbRow> pendingRow; + mdbOid outRowId; + nsCString messageId; + pendingHdr->GetMessageId(getter_Copies(messageId)); + messageIdYarn.mYarn_Buf = (void*)messageId.get(); + messageIdYarn.mYarn_Fill = messageId.Length(); + messageIdYarn.mYarn_Form = 0; + messageIdYarn.mYarn_Size = messageIdYarn.mYarn_Fill; + + rv = m_mdbStore->FindRow(GetEnv(), m_pendingHdrsRowScopeToken, + m_messageIdColumnToken, &messageIdYarn, &outRowId, + getter_AddRefs(pendingRow)); + + if (!pendingRow) + rv = m_mdbStore->NewRow(GetEnv(), m_pendingHdrsRowScopeToken, + getter_AddRefs(pendingRow)); + + NS_ENSURE_SUCCESS(rv, rv); + if (pendingRow) { + // now we need to add cells to the row to remember the messageid, property + // and property value, and flags. Then, when hdrs are added to the db, we'll + // check if they have a matching message-id, and if so, set the property and + // flags + // XXX we already fetched messageId from the pending hdr, could it have + // changed by the time we get here? + nsCString messageId; + pendingHdr->GetMessageId(getter_Copies(messageId)); + // we're just going to ignore messages without a message-id. They should be + // rare. If SPAM messages often didn't have message-id's, they'd be filtered + // on the server, most likely, and spammers would then start putting in + // message-id's. + if (!messageId.IsEmpty()) { + extern const char* kMessageIdColumnName; + m_mdbAllPendingHdrsTable->AddRow(GetEnv(), pendingRow); + // make sure this is the first cell so that when we ignore the first + // cell in nsImapMailDatabase::AddNewHdrToDB, we're ignoring the right one + (void)SetProperty(pendingRow, kMessageIdColumnName, messageId.get()); + pendingRow.forget(row); + } else + return NS_ERROR_FAILURE; + } + return rv; +} + +NS_IMETHODIMP nsImapMailDatabase::SetAttributeOnPendingHdr( + nsIMsgDBHdr* pendingHdr, const char* property, const char* propertyVal) { + NS_ENSURE_ARG_POINTER(pendingHdr); + nsCOMPtr<nsIMdbRow> pendingRow; + nsresult rv = GetRowForPendingHdr(pendingHdr, getter_AddRefs(pendingRow)); + NS_ENSURE_SUCCESS(rv, rv); + return SetProperty(pendingRow, property, propertyVal); +} + +NS_IMETHODIMP +nsImapMailDatabase::SetUint32AttributeOnPendingHdr(nsIMsgDBHdr* pendingHdr, + const char* property, + uint32_t propertyVal) { + NS_ENSURE_ARG_POINTER(pendingHdr); + nsCOMPtr<nsIMdbRow> pendingRow; + nsresult rv = GetRowForPendingHdr(pendingHdr, getter_AddRefs(pendingRow)); + NS_ENSURE_SUCCESS(rv, rv); + return SetUint32Property(pendingRow, property, propertyVal); +} + +NS_IMETHODIMP +nsImapMailDatabase::SetUint64AttributeOnPendingHdr(nsIMsgDBHdr* aPendingHdr, + const char* aProperty, + uint64_t aPropertyVal) { + NS_ENSURE_ARG_POINTER(aPendingHdr); + nsCOMPtr<nsIMdbRow> pendingRow; + nsresult rv = GetRowForPendingHdr(aPendingHdr, getter_AddRefs(pendingRow)); + NS_ENSURE_SUCCESS(rv, rv); + return SetUint64Property(pendingRow, aProperty, aPropertyVal); +} diff --git a/comm/mailnews/db/msgdb/src/nsMailDatabase.cpp b/comm/mailnews/db/msgdb/src/nsMailDatabase.cpp new file mode 100644 index 0000000000..c07294785a --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsMailDatabase.cpp @@ -0,0 +1,380 @@ +/* -*- 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 "nsMailDatabase.h" +#include "nsDBFolderInfo.h" +#include "nsMsgLocalFolderHdrs.h" +#include "nsNetUtil.h" +#include "nsMsgOfflineImapOperation.h" +#include "nsMsgFolderFlags.h" +#include "mozilla/Logging.h" +#include "prprf.h" +#include "nsMsgUtils.h" +#include "nsIMsgPluggableStore.h" +#include "nsSimpleEnumerator.h" + +using namespace mozilla; + +extern LazyLogModule IMAPOffline; // defined in nsMsgOfflineImapOperation.cpp + +// scope for all offine ops table +const char* kOfflineOpsScope = "ns:msg:db:row:scope:ops:all"; +const char* kOfflineOpsTableKind = "ns:msg:db:table:kind:ops"; +struct mdbOid gAllOfflineOpsTableOID; + +nsMailDatabase::nsMailDatabase() : m_reparse(false) { + m_mdbAllOfflineOpsTable = nullptr; + m_offlineOpsRowScopeToken = 0; + m_offlineOpsTableKindToken = 0; +} + +nsMailDatabase::~nsMailDatabase() {} + +// caller passes in upgrading==true if they want back a db even if the db is out +// of date. If so, they'll extract out the interesting info from the db, close +// it, delete it, and then try to open the db again, prior to reparsing. +nsresult nsMailDatabase::Open(nsMsgDBService* aDBService, nsIFile* aSummaryFile, + bool aCreate, bool aUpgrading) { +#ifdef DEBUG + nsString leafName; + aSummaryFile->GetLeafName(leafName); + if (!StringEndsWith(leafName, NS_LITERAL_STRING_FROM_CSTRING(SUMMARY_SUFFIX), + nsCaseInsensitiveStringComparator)) + NS_ERROR("non summary file passed into open"); +#endif + return nsMsgDatabase::Open(aDBService, aSummaryFile, aCreate, aUpgrading); +} + +NS_IMETHODIMP nsMailDatabase::ForceClosed() { + m_mdbAllOfflineOpsTable = nullptr; + return nsMsgDatabase::ForceClosed(); +} + +// get this on demand so that only db's that have offline ops will +// create the table. +nsresult nsMailDatabase::GetAllOfflineOpsTable() { + nsresult rv = NS_OK; + if (!m_mdbAllOfflineOpsTable) + rv = GetTableCreateIfMissing(kOfflineOpsScope, kOfflineOpsTableKind, + getter_AddRefs(m_mdbAllOfflineOpsTable), + m_offlineOpsRowScopeToken, + m_offlineOpsTableKindToken); + return rv; +} + +NS_IMETHODIMP nsMailDatabase::DeleteMessages( + nsTArray<nsMsgKey> const& nsMsgKeys, nsIDBChangeListener* instigator) { + nsresult rv; + if (m_folder) { + bool isLocked; + m_folder->GetLocked(&isLocked); + if (isLocked) { + NS_ASSERTION(false, "Some other operation is in progress"); + return NS_MSG_FOLDER_BUSY; + } + } + + rv = nsMsgDatabase::DeleteMessages(nsMsgKeys, instigator); + SetSummaryValid(true); + return rv; +} + +NS_IMETHODIMP nsMailDatabase::GetSummaryValid(bool* aResult) { + uint32_t version; + m_dbFolderInfo->GetVersion(&version); + if (GetCurVersion() != version) { + *aResult = false; + return NS_OK; + } + if (!m_folder) { + // If the folder is not set, we just return without checking the validity + // of the summary file. For now, this is an expected condition when the + // message database is being opened from a URL in + // nsMailboxUrl::GetMsgHdrForKey() which calls + // nsMsgDBService::OpenMailDBFromFile() without a folder. + // Returning an error here would lead to the deletion of the MSF in the + // caller nsMsgDatabase::CheckForErrors(). + *aResult = true; + return NS_OK; + } + + // If this is a virtual folder, there is no storage. + bool isVirtual = false; + m_folder->GetFlag(nsMsgFolderFlags::Virtual, &isVirtual); + if (isVirtual) { + *aResult = true; + return NS_OK; + } + + nsCOMPtr<nsIMsgPluggableStore> msgStore; + nsresult rv = m_folder->GetMsgStore(getter_AddRefs(msgStore)); + NS_ENSURE_SUCCESS(rv, rv); + return msgStore->IsSummaryFileValid(m_folder, this, aResult); +} + +NS_IMETHODIMP nsMailDatabase::SetSummaryValid(bool aValid) { + nsMsgDatabase::SetSummaryValid(aValid); + + if (!m_folder) return NS_ERROR_NULL_POINTER; + + // If this is a virtual folder, there is no storage. + bool flag; + m_folder->GetFlag(nsMsgFolderFlags::Virtual, &flag); + if (flag) return NS_OK; + + nsCOMPtr<nsIMsgPluggableStore> msgStore; + nsresult rv = m_folder->GetMsgStore(getter_AddRefs(msgStore)); + NS_ENSURE_SUCCESS(rv, rv); + return msgStore->SetSummaryFileValid(m_folder, this, aValid); +} + +NS_IMETHODIMP nsMailDatabase::RemoveOfflineOp(nsIMsgOfflineImapOperation* op) { + nsresult rv = GetAllOfflineOpsTable(); + NS_ENSURE_SUCCESS(rv, rv); + + if (!op || !m_mdbAllOfflineOpsTable) return NS_ERROR_NULL_POINTER; + nsMsgOfflineImapOperation* offlineOp = + static_cast<nsMsgOfflineImapOperation*>( + op); // closed system, so this is ok + nsIMdbRow* row = offlineOp->GetMDBRow(); + rv = m_mdbAllOfflineOpsTable->CutRow(GetEnv(), row); + row->CutAllColumns(GetEnv()); + return rv; +} + +NS_IMETHODIMP nsMailDatabase::GetOfflineOpForKey( + nsMsgKey msgKey, bool create, nsIMsgOfflineImapOperation** offlineOp) { + mdb_bool hasOid; + mdbOid rowObjectId; + nsresult err; + + nsresult rv = GetAllOfflineOpsTable(); + NS_ENSURE_SUCCESS(rv, rv); + + if (!offlineOp || !m_mdbAllOfflineOpsTable) return NS_ERROR_NULL_POINTER; + + *offlineOp = NULL; + + rowObjectId.mOid_Id = msgKey; + rowObjectId.mOid_Scope = m_offlineOpsRowScopeToken; + err = m_mdbAllOfflineOpsTable->HasOid(GetEnv(), &rowObjectId, &hasOid); + if (NS_SUCCEEDED(err) && m_mdbStore && (hasOid || create)) { + nsCOMPtr<nsIMdbRow> offlineOpRow; + err = m_mdbStore->GetRow(GetEnv(), &rowObjectId, + getter_AddRefs(offlineOpRow)); + + if (create) { + if (!offlineOpRow) { + err = m_mdbStore->NewRowWithOid(GetEnv(), &rowObjectId, + getter_AddRefs(offlineOpRow)); + NS_ENSURE_SUCCESS(err, err); + } + if (offlineOpRow && !hasOid) + m_mdbAllOfflineOpsTable->AddRow(GetEnv(), offlineOpRow); + } + + if (NS_SUCCEEDED(err) && offlineOpRow) { + NS_IF_ADDREF(*offlineOp = + new nsMsgOfflineImapOperation(this, offlineOpRow)); + (*offlineOp)->SetMessageKey(msgKey); + } + if (!hasOid && m_dbFolderInfo) { + // set initial value for flags so we don't lose them. + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr)); + if (msgHdr) { + uint32_t flags; + msgHdr->GetFlags(&flags); + (*offlineOp)->SetNewFlags(flags); + } + int32_t newFlags; + m_dbFolderInfo->OrFlags(nsMsgFolderFlags::OfflineEvents, &newFlags); + } + } + + return err; +} + +NS_IMETHODIMP nsMailDatabase::ListAllOfflineOpIds( + nsTArray<nsMsgKey>& offlineOpIds) { + nsresult rv = GetAllOfflineOpsTable(); + NS_ENSURE_SUCCESS(rv, rv); + nsIMdbTableRowCursor* rowCursor; + + if (m_mdbAllOfflineOpsTable) { + nsresult err = + m_mdbAllOfflineOpsTable->GetTableRowCursor(GetEnv(), -1, &rowCursor); + while (NS_SUCCEEDED(err) && rowCursor) { + mdbOid outOid; + mdb_pos outPos; + + err = rowCursor->NextRowOid(GetEnv(), &outOid, &outPos); + // is this right? Mork is returning a 0 id, but that should valid. + if (outPos < 0 || outOid.mOid_Id == (mdb_id)-1) break; + if (NS_SUCCEEDED(err)) { + offlineOpIds.AppendElement(outOid.mOid_Id); + if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info)) { + nsCOMPtr<nsIMsgOfflineImapOperation> offlineOp; + GetOfflineOpForKey(outOid.mOid_Id, false, getter_AddRefs(offlineOp)); + if (offlineOp) { + nsMsgOfflineImapOperation* logOp = + static_cast<nsMsgOfflineImapOperation*>( + static_cast<nsIMsgOfflineImapOperation*>(offlineOp.get())); + if (logOp) logOp->Log(); + } + } + } + } + // TODO: would it cause a problem to replace this with "rv = err;" ? + rv = (NS_SUCCEEDED(err)) ? NS_OK : NS_ERROR_FAILURE; + rowCursor->Release(); + } + + offlineOpIds.Sort(); + return rv; +} + +NS_IMETHODIMP nsMailDatabase::ListAllOfflineDeletes( + nsTArray<nsMsgKey>& offlineDeletes) { + nsresult rv = GetAllOfflineOpsTable(); + NS_ENSURE_SUCCESS(rv, rv); + nsIMdbTableRowCursor* rowCursor; + if (m_mdbAllOfflineOpsTable) { + nsresult err = + m_mdbAllOfflineOpsTable->GetTableRowCursor(GetEnv(), -1, &rowCursor); + while (NS_SUCCEEDED(err) && rowCursor) { + mdbOid outOid; + mdb_pos outPos; + nsIMdbRow* offlineOpRow; + + err = rowCursor->NextRow(GetEnv(), &offlineOpRow, &outPos); + // is this right? Mork is returning a 0 id, but that should valid. + if (outPos < 0 || offlineOpRow == nullptr) break; + if (NS_SUCCEEDED(err)) { + offlineOpRow->GetOid(GetEnv(), &outOid); + RefPtr<nsIMsgOfflineImapOperation> offlineOp = + new nsMsgOfflineImapOperation(this, offlineOpRow); + imapMessageFlagsType newFlags; + nsOfflineImapOperationType opType; + + offlineOp->GetOperation(&opType); + offlineOp->GetNewFlags(&newFlags); + if (opType & nsIMsgOfflineImapOperation::kMsgMoved || + ((opType & nsIMsgOfflineImapOperation::kFlagsChanged) && + (newFlags & nsIMsgOfflineImapOperation::kMsgMarkedDeleted))) + offlineDeletes.AppendElement(outOid.mOid_Id); + + offlineOpRow->Release(); + } + } + // TODO: would it cause a problem to replace this with "rv = err;" ? + rv = (NS_SUCCEEDED(err)) ? NS_OK : NS_ERROR_FAILURE; + rowCursor->Release(); + } + return rv; +} + +// This is used to remember that the db is out of sync with the mail folder +// and needs to be regenerated. +void nsMailDatabase::SetReparse(bool reparse) { m_reparse = reparse; } + +class nsMsgOfflineOpEnumerator : public nsSimpleEnumerator { + public: + const nsID& DefaultInterface() override { + return NS_GET_IID(nsIMsgOfflineImapOperation); + } + + // nsISimpleEnumerator methods: + NS_DECL_NSISIMPLEENUMERATOR + + explicit nsMsgOfflineOpEnumerator(nsMailDatabase* db); + + protected: + ~nsMsgOfflineOpEnumerator() override; + nsresult GetRowCursor(); + nsresult PrefetchNext(); + nsMailDatabase* mDB; + nsIMdbTableRowCursor* mRowCursor; + nsCOMPtr<nsIMsgOfflineImapOperation> mResultOp; + bool mDone; + bool mNextPrefetched; +}; + +nsMsgOfflineOpEnumerator::nsMsgOfflineOpEnumerator(nsMailDatabase* db) + : mDB(db), mRowCursor(nullptr), mDone(false) { + NS_ADDREF(mDB); + mNextPrefetched = false; +} + +nsMsgOfflineOpEnumerator::~nsMsgOfflineOpEnumerator() { + NS_IF_RELEASE(mRowCursor); + NS_RELEASE(mDB); +} + +nsresult nsMsgOfflineOpEnumerator::GetRowCursor() { + nsresult rv = NS_OK; + mDone = false; + + if (!mDB || !mDB->m_mdbAllOfflineOpsTable) return NS_ERROR_NULL_POINTER; + + rv = mDB->m_mdbAllOfflineOpsTable->GetTableRowCursor(mDB->GetEnv(), -1, + &mRowCursor); + return rv; +} + +NS_IMETHODIMP nsMsgOfflineOpEnumerator::GetNext(nsISupports** aItem) { + NS_ENSURE_ARG_POINTER(aItem); + + nsresult rv = NS_OK; + if (!mNextPrefetched) rv = PrefetchNext(); + if (NS_SUCCEEDED(rv)) { + if (mResultOp) { + NS_ADDREF(*aItem = mResultOp); + mNextPrefetched = false; + } + } + return rv; +} + +nsresult nsMsgOfflineOpEnumerator::PrefetchNext() { + nsresult rv = NS_OK; + nsIMdbRow* offlineOpRow; + mdb_pos rowPos; + + if (!mRowCursor) { + rv = GetRowCursor(); + if (NS_FAILED(rv)) return rv; + } + + rv = mRowCursor->NextRow(mDB->GetEnv(), &offlineOpRow, &rowPos); + if (!offlineOpRow) { + mDone = true; + return NS_ERROR_FAILURE; + } + if (NS_FAILED(rv)) { + mDone = true; + return rv; + } + + nsIMsgOfflineImapOperation* op = + new nsMsgOfflineImapOperation(mDB, offlineOpRow); + mResultOp = op; + if (!op) return NS_ERROR_OUT_OF_MEMORY; + + if (mResultOp) { + mNextPrefetched = true; + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgOfflineOpEnumerator::HasMoreElements(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + if (!mNextPrefetched) PrefetchNext(); + *aResult = !mDone; + return NS_OK; +} diff --git a/comm/mailnews/db/msgdb/src/nsMsgDatabase.cpp b/comm/mailnews/db/msgdb/src/nsMsgDatabase.cpp new file mode 100644 index 0000000000..779ca400a9 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsMsgDatabase.cpp @@ -0,0 +1,4730 @@ +/* -*- 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 file implements the nsMsgDatabase interface using the MDB Interface. + +#include "nscore.h" +#include "msgCore.h" +#include "nsIFile.h" +#include "nsMailDatabase.h" +#include "nsDBFolderInfo.h" +#include "nsIMsgNewsFolder.h" +#include "nsMsgThread.h" +#include "nsIMsgSearchTerm.h" +#include "nsIMdbFactoryFactory.h" +#include "mozilla/Logging.h" +#include "mozilla/Telemetry.h" +#include "prprf.h" +#include "nsMsgFolderFlags.h" +#include "nsIMsgAccountManager.h" +#include "nsIMsgDBView.h" +#include "nsIMsgFolderCache.h" +#include "nsIMsgFolderCacheElement.h" +#include "MailNewsTypes2.h" +#include "nsMsgUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMemory.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsPrintfCString.h" +#include "nsMsgDatabaseEnumerators.h" +#include "nsIMemoryReporter.h" +#include "nsIWeakReferenceUtils.h" +#include "nsMailDirServiceDefs.h" +#include "mozilla/Components.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "mozilla/intl/LocaleService.h" + +using namespace mozilla::mailnews; +using namespace mozilla; + +#if defined(DEBUG_sspitzer_) || defined(DEBUG_seth_) +# define DEBUG_MSGKEYSET 1 +#endif + +#define MSG_HASH_SIZE 512 + +// This will be used on discovery, since we don't know total. +const int32_t kMaxHdrsInCache = 512; + +// special keys +static const nsMsgKey kAllMsgHdrsTableKey = 1; +static const nsMsgKey kTableKeyForThreadOne = 0xfffffffe; +static const nsMsgKey kAllThreadsTableKey = 0xfffffffd; +static const nsMsgKey kFirstPseudoKey = 0xfffffff0; +static const nsMsgKey kIdStartOfFake = 0xffffff80; +static const nsMsgKey kForceReparseKey = 0xfffffff0; + +LazyLogModule DBLog("MsgDB"); + +PRTime nsMsgDatabase::gLastUseTime; + +/** + * 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(nsMsgDBService, nsIMsgDBService) + +nsMsgDBService::nsMsgDBService() {} + +nsMsgDBService::~nsMsgDBService() { +#ifdef DEBUG + // If you hit this warning, it means that some code is holding onto + // a db at shutdown. + NS_WARNING_ASSERTION(!m_dbCache.Length(), "some msg dbs left open"); +# ifndef MOZILLA_OFFICIAL + // Only print this on local builds since it causes crashes, + // see bug 1468691, bug 1377692 and bug 1342858. + for (uint32_t i = 0; i < m_dbCache.Length(); i++) { + nsMsgDatabase* pMessageDB = m_dbCache.ElementAt(i); + if (pMessageDB) + printf("db left open %s\n", + pMessageDB->m_dbFile->HumanReadablePath().get()); + } +# endif +#endif +} + +NS_IMETHODIMP nsMsgDBService::OpenFolderDB(nsIMsgFolder* aFolder, + bool aLeaveInvalidDB, + nsIMsgDatabase** _retval) { + NS_ENSURE_ARG(aFolder); + nsCOMPtr<nsIMsgIncomingServer> incomingServer; + nsresult rv = aFolder->GetServer(getter_AddRefs(incomingServer)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> summaryFilePath; + rv = aFolder->GetSummaryFile(getter_AddRefs(summaryFilePath)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgDatabase* cacheDB = FindInCache(summaryFilePath); + if (cacheDB) { + // this db could have ended up in the folder cache w/o an m_folder pointer + // via OpenMailDBFromFile. If so, take this chance to fix the folder. + if (!cacheDB->m_folder) cacheDB->m_folder = aFolder; + cacheDB->RememberLastUseTime(); + *_retval = cacheDB; // FindInCache already addRefed. + // if m_thumb is set, someone is asynchronously opening the db. But our + // caller wants to synchronously open it, so just do it. + if (cacheDB->m_thumb) + return cacheDB->Open(this, summaryFilePath, false, aLeaveInvalidDB); + return NS_OK; + } + + nsCString localDatabaseType; + incomingServer->GetLocalDatabaseType(localDatabaseType); + nsAutoCString dbContractID("@mozilla.org/nsMsgDatabase/msgDB-"); + dbContractID.Append(localDatabaseType.get()); + nsCOMPtr<nsIMsgDatabase> msgDB = do_CreateInstance(dbContractID.get(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't try to create the database yet--let the createNewDB call do that. + nsMsgDatabase* msgDatabase = static_cast<nsMsgDatabase*>(msgDB.get()); + msgDatabase->m_folder = aFolder; + rv = msgDatabase->Open(this, summaryFilePath, false, aLeaveInvalidDB); + if (NS_FAILED(rv) && rv != NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) return rv; + + NS_ADDREF(*_retval = msgDB); + + if (NS_FAILED(rv)) { +#ifdef DEBUG + // Doing these checks for debug only as we don't want to report certain + // errors in debug mode, but in release mode we wouldn't report them either + + // These errors are expected. + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING || + rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) + return rv; + + // If it isn't one of the expected errors, throw a warning. + NS_ENSURE_SUCCESS(rv, rv); +#endif + return rv; + } + + FinishDBOpen(aFolder, msgDatabase); + return rv; +} + +/** + * When a db is opened, we need to hook up any pending listeners for + * that db, and notify them. + */ +void nsMsgDBService::HookupPendingListeners(nsIMsgDatabase* db, + nsIMsgFolder* folder) { + for (int32_t listenerIndex = 0; + listenerIndex < m_foldersPendingListeners.Count(); listenerIndex++) { + // check if we have a pending listener on this db, and if so, add it. + if (m_foldersPendingListeners[listenerIndex] == folder) { + db->AddListener(m_pendingListeners.ObjectAt(listenerIndex)); + m_pendingListeners.ObjectAt(listenerIndex)->OnEvent(db, "DBOpened"); + } + } +} + +void nsMsgDBService::FinishDBOpen(nsIMsgFolder* aFolder, + nsMsgDatabase* aMsgDB) { + uint32_t folderFlags; + aFolder->GetFlags(&folderFlags); + + if (!(folderFlags & nsMsgFolderFlags::Virtual) && + aMsgDB->m_mdbAllMsgHeadersTable) { + mdb_count numHdrsInTable = 0; + int32_t numMessages; + aMsgDB->m_mdbAllMsgHeadersTable->GetCount(aMsgDB->GetEnv(), + &numHdrsInTable); + aMsgDB->m_dbFolderInfo->GetNumMessages(&numMessages); + if (numMessages != (int32_t)numHdrsInTable) aMsgDB->SyncCounts(); + } + HookupPendingListeners(aMsgDB, aFolder); + aMsgDB->RememberLastUseTime(); +} + +//---------------------------------------------------------------------- +// FindInCache - this addrefs the db it finds. +//---------------------------------------------------------------------- +nsMsgDatabase* nsMsgDBService::FindInCache(nsIFile* dbName) { + for (uint32_t i = 0; i < m_dbCache.Length(); i++) { + nsMsgDatabase* pMessageDB = m_dbCache[i]; + if (pMessageDB->MatchDbName(dbName)) { + if (pMessageDB->m_mdbStore) // don't return db without store + { + NS_ADDREF(pMessageDB); + return pMessageDB; + } + } + } + return nullptr; +} + +// This method is called when the caller is trying to create a db without +// having a corresponding nsIMsgFolder object. This happens in a few +// situations, including imap folder discovery, compacting local folders, +// and copying local folders. +NS_IMETHODIMP nsMsgDBService::OpenMailDBFromFile(nsIFile* aFolderName, + nsIMsgFolder* aFolder, + bool aCreate, + bool aLeaveInvalidDB, + nsIMsgDatabase** pMessageDB) { + if (!aFolderName) return NS_ERROR_NULL_POINTER; + + nsCOMPtr<nsIFile> dbPath; + nsresult rv = GetSummaryFileLocation(aFolderName, getter_AddRefs(dbPath)); + NS_ENSURE_SUCCESS(rv, rv); + + *pMessageDB = FindInCache(dbPath); + if (*pMessageDB) return NS_OK; + + RefPtr<nsMailDatabase> msgDB = new nsMailDatabase; + NS_ENSURE_TRUE(msgDB, NS_ERROR_OUT_OF_MEMORY); + rv = msgDB->Open(this, dbPath, aCreate, aLeaveInvalidDB); + if (rv == NS_ERROR_FILE_NOT_FOUND) return rv; + NS_IF_ADDREF(*pMessageDB = msgDB); + if (aCreate && msgDB && rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) rv = NS_OK; + if (NS_SUCCEEDED(rv)) msgDB->m_folder = aFolder; + return rv; +} + +NS_IMETHODIMP nsMsgDBService::CreateNewDB(nsIMsgFolder* aFolder, + nsIMsgDatabase** _retval) { + NS_ENSURE_ARG(aFolder); + + nsCOMPtr<nsIMsgIncomingServer> incomingServer; + nsresult rv = aFolder->GetServer(getter_AddRefs(incomingServer)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> summaryFilePath; + rv = aFolder->GetSummaryFile(getter_AddRefs(summaryFilePath)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString localDatabaseType; + incomingServer->GetLocalDatabaseType(localDatabaseType); + nsAutoCString dbContractID("@mozilla.org/nsMsgDatabase/msgDB-"); + dbContractID.Append(localDatabaseType.get()); + + nsCOMPtr<nsIMsgDatabase> msgDB = do_CreateInstance(dbContractID.get(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgDatabase* msgDatabase = static_cast<nsMsgDatabase*>(msgDB.get()); + + msgDatabase->m_folder = aFolder; + rv = msgDatabase->Open(this, summaryFilePath, true, true); + + // We are trying to create a new database, but that implies that it did not + // already exist. Open returns NS_MSG_ERROR_FOLDER_SUMMARY_MISSING for the + // successful creation of a new database. But if it existed for some + // reason, then we would get rv = NS_OK instead. That is a "failure" + // from our perspective, so we want to return a failure since we are not + // returning a valid database object. + NS_ENSURE_TRUE(rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING, + NS_SUCCEEDED(rv) ? NS_ERROR_FILE_ALREADY_EXISTS : rv); + + NS_ADDREF(*_retval = msgDB); + + HookupPendingListeners(msgDB, aFolder); + + msgDatabase->RememberLastUseTime(); + + return NS_OK; +} + +/* void registerPendingListener (in nsIMsgFolder aFolder, in nsIDBChangeListener + * aListener); */ +NS_IMETHODIMP nsMsgDBService::RegisterPendingListener( + nsIMsgFolder* aFolder, nsIDBChangeListener* aListener) { + // need to make sure we don't hold onto these forever. Maybe a shutdown + // listener? if there is a db open on this folder already, we should register + // the listener. + m_foldersPendingListeners.AppendObject(aFolder); + m_pendingListeners.AppendObject(aListener); + nsCOMPtr<nsIMsgDatabase> openDB; + CachedDBForFolder(aFolder, getter_AddRefs(openDB)); + if (openDB) openDB->AddListener(aListener); + return NS_OK; +} + +/* void unregisterPendingListener (in nsIDBChangeListener aListener); */ +NS_IMETHODIMP nsMsgDBService::UnregisterPendingListener( + nsIDBChangeListener* aListener) { + int32_t listenerIndex = m_pendingListeners.IndexOfObject(aListener); + if (listenerIndex != -1) { + nsCOMPtr<nsIMsgDatabase> msgDB; + CachedDBForFolder(m_foldersPendingListeners[listenerIndex], + getter_AddRefs(msgDB)); + if (msgDB) msgDB->RemoveListener(aListener); + m_foldersPendingListeners.RemoveObjectAt(listenerIndex); + m_pendingListeners.RemoveObjectAt(listenerIndex); + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgDBService::CachedDBForFolder(nsIMsgFolder* aFolder, + nsIMsgDatabase** aRetDB) { + NS_ENSURE_ARG_POINTER(aFolder); + NS_ENSURE_ARG_POINTER(aRetDB); + + nsCOMPtr<nsIFile> summaryFilePath; + nsresult rv = aFolder->GetSummaryFile(getter_AddRefs(summaryFilePath)); + NS_ENSURE_SUCCESS(rv, rv); + + *aRetDB = FindInCache(summaryFilePath); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBService::ForceFolderDBClosed(nsIMsgFolder* aFolder) { + nsCOMPtr<nsIMsgDatabase> mailDB; + nsresult rv = CachedDBForFolder(aFolder, getter_AddRefs(mailDB)); + if (mailDB) { + mailDB->ForceClosed(); + } + return rv; +} + +NS_IMETHODIMP nsMsgDBService::GetOpenDBs( + nsTArray<RefPtr<nsIMsgDatabase>>& aOpenDBs) { + aOpenDBs.Clear(); + aOpenDBs.SetCapacity(m_dbCache.Length()); + for (auto db : m_dbCache) { + aOpenDBs.AppendElement(db); + } + return NS_OK; +} + +static bool gGotGlobalPrefs = false; +static bool gThreadWithoutRe = true; +static bool gStrictThreading = false; +static bool gCorrectThreading = false; + +void nsMsgDatabase::GetGlobalPrefs() { + if (!gGotGlobalPrefs) { + GetBoolPref("mail.thread_without_re", &gThreadWithoutRe); + GetBoolPref("mail.strict_threading", &gStrictThreading); + GetBoolPref("mail.correct_threading", &gCorrectThreading); + gGotGlobalPrefs = true; + } +} + +nsresult nsMsgDatabase::AddHdrToCache( + nsIMsgDBHdr* hdr, nsMsgKey key) // do we want key? We could get it from hdr +{ + if (m_bCacheHeaders) { + if (!m_cachedHeaders) + m_cachedHeaders = new PLDHashTable( + &gMsgDBHashTableOps, sizeof(struct MsgHdrHashElement), m_cacheSize); + if (m_cachedHeaders) { + if (key == nsMsgKey_None) hdr->GetMessageKey(&key); + if (m_cachedHeaders->EntryCount() > m_cacheSize) ClearHdrCache(true); + PLDHashEntryHdr* entry = + m_cachedHeaders->Add((void*)(uintptr_t)key, mozilla::fallible); + if (!entry) return NS_ERROR_OUT_OF_MEMORY; // XXX out of memory + + MsgHdrHashElement* element = static_cast<MsgHdrHashElement*>(entry); + element->mHdr = hdr; + element->mKey = key; + NS_ADDREF(hdr); // make the cache hold onto the header + return NS_OK; + } + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgDatabase::SetMsgHdrCacheSize(uint32_t aSize) { + m_cacheSize = aSize; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetMsgHdrCacheSize(uint32_t* aSize) { + NS_ENSURE_ARG_POINTER(aSize); + *aSize = m_cacheSize; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetLastUseTime(PRTime* aTime) { + NS_ENSURE_ARG_POINTER(aTime); + *aTime = m_lastUseTime; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::SetLastUseTime(PRTime aTime) { + gLastUseTime = m_lastUseTime = aTime; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetDatabaseSize(int64_t* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + nsresult rv; + bool exists; + NS_ENSURE_TRUE(m_dbFile, NS_ERROR_NULL_POINTER); + rv = m_dbFile->Exists(&exists); + if (NS_SUCCEEDED(rv)) { + if (exists) + rv = m_dbFile->GetFileSize(_retval); + else + *_retval = 0; + } + + return rv; +} + +NS_IMETHODIMP nsMsgDatabase::ClearCachedHdrs() { + ClearCachedObjects(false); +#ifdef DEBUG_bienvenu1 + if (mRefCnt > 1) { + NS_ASSERTION(false, ""); + printf("someone's holding onto db - refs = %ld\n", mRefCnt); + } +#endif + return NS_OK; +} + +// Invalidate any outstanding message enumerators using this db. +void nsMsgDatabase::InvalidateEnumerators() { + RefPtr<nsMsgDatabase> kungFuDeathGrip(this); + // Work in reverse, as the enumerators remove themselves from the list. + { + auto n = m_msgEnumerators.Length(); + for (auto i = n; i > 0; --i) { + m_msgEnumerators[i - 1]->Invalidate(); + } + } + // And again for thread enumerators. + { + auto n = m_threadEnumerators.Length(); + for (auto i = n; i > 0; --i) { + m_threadEnumerators[i - 1]->Invalidate(); + } + } +} + +nsMsgThread* nsMsgDatabase::FindExistingThread(nsMsgKey threadId) { + uint32_t numThreads = m_threads.Length(); + for (uint32_t i = 0; i < numThreads; i++) + if (m_threads[i]->m_threadKey == threadId) return m_threads[i]; + + return nullptr; +} + +void nsMsgDatabase::ClearThreads() { + // clear out existing threads + nsTArray<nsMsgThread*> copyThreads; + copyThreads.SwapElements(m_threads); + + uint32_t numThreads = copyThreads.Length(); + for (uint32_t i = 0; i < numThreads; i++) copyThreads[i]->Clear(); +} + +void nsMsgDatabase::ClearCachedObjects(bool dbGoingAway) { + ClearHdrCache(false); +#ifdef DEBUG_DavidBienvenu + if (m_headersInUse && m_headersInUse->EntryCount() > 0) { + NS_ASSERTION(false, "leaking headers"); + printf("leaking %d headers in %s\n", m_headersInUse->EntryCount(), + m_dbFile->HumanReadablePath().get()); + } +#endif + m_cachedThread = nullptr; + m_cachedThreadId = nsMsgKey_None; + // We should only clear the use hdr cache when the db is going away, or we + // could end up with multiple copies of the same logical msg hdr, which will + // lead to ref-counting problems. + if (dbGoingAway) { + ClearUseHdrCache(); + ClearThreads(); + } + m_thumb = nullptr; +} + +nsresult nsMsgDatabase::ClearHdrCache(bool reInit) { + if (m_cachedHeaders) { + // save this away in case we renter this code. + PLDHashTable* saveCachedHeaders = m_cachedHeaders; + m_cachedHeaders = nullptr; + for (auto iter = saveCachedHeaders->Iter(); !iter.Done(); iter.Next()) { + auto element = static_cast<MsgHdrHashElement*>(iter.Get()); + if (element) NS_IF_RELEASE(element->mHdr); + } + + if (reInit) { + saveCachedHeaders->ClearAndPrepareForLength(m_cacheSize); + m_cachedHeaders = saveCachedHeaders; + } else { + delete saveCachedHeaders; + } + } + return NS_OK; +} + +nsresult nsMsgDatabase::RemoveHdrFromCache(nsIMsgDBHdr* hdr, nsMsgKey key) { + if (m_cachedHeaders) { + if (key == nsMsgKey_None) hdr->GetMessageKey(&key); + + PLDHashEntryHdr* entry = + m_cachedHeaders->Search((const void*)(uintptr_t)key); + if (entry) { + m_cachedHeaders->Remove((void*)(uintptr_t)key); + NS_RELEASE(hdr); // get rid of extra ref the cache was holding. + } + } + return NS_OK; +} + +nsresult nsMsgDatabase::GetHdrFromUseCache(nsMsgKey key, nsIMsgDBHdr** result) { + if (!result) return NS_ERROR_NULL_POINTER; + + nsresult rv = NS_ERROR_FAILURE; + + *result = nullptr; + + if (m_headersInUse) { + PLDHashEntryHdr* entry = + m_headersInUse->Search((const void*)(uintptr_t)key); + if (entry) { + MsgHdrHashElement* element = static_cast<MsgHdrHashElement*>(entry); + *result = element->mHdr; + } + if (*result) { + NS_ADDREF(*result); + rv = NS_OK; + } + } + return rv; +} + +PLDHashTableOps nsMsgDatabase::gMsgDBHashTableOps = { + HashKey, MatchEntry, MoveEntry, ClearEntry, nullptr}; + +// HashKey is supposed to maximize entropy in the low order bits, and the key +// as is, should do that. +PLDHashNumber nsMsgDatabase::HashKey(const void* aKey) { + return PLDHashNumber(NS_PTR_TO_INT32(aKey)); +} + +bool nsMsgDatabase::MatchEntry(const PLDHashEntryHdr* aEntry, + const void* aKey) { + const MsgHdrHashElement* hdr = static_cast<const MsgHdrHashElement*>(aEntry); + return aKey == (const void*)(uintptr_t) + hdr->mKey; // ### or get the key from the hdr... +} + +void nsMsgDatabase::MoveEntry(PLDHashTable* aTable, + const PLDHashEntryHdr* aFrom, + PLDHashEntryHdr* aTo) { + new (KnownNotNull, aTo) + MsgHdrHashElement(std::move(*((MsgHdrHashElement*)aFrom))); +} + +void nsMsgDatabase::ClearEntry(PLDHashTable* aTable, PLDHashEntryHdr* aEntry) { + MsgHdrHashElement* element = static_cast<MsgHdrHashElement*>(aEntry); + element->mHdr = nullptr; // eh? Need to release this or not? + element->mKey = nsMsgKey_None; // eh? +} + +nsresult nsMsgDatabase::AddHdrToUseCache(nsIMsgDBHdr* hdr, nsMsgKey key) { + if (!m_headersInUse) { + mdb_count numHdrs = MSG_HASH_SIZE; + if (m_mdbAllMsgHeadersTable) + m_mdbAllMsgHeadersTable->GetCount(GetEnv(), &numHdrs); + m_headersInUse = + new PLDHashTable(&gMsgDBHashTableOps, sizeof(struct MsgHdrHashElement), + std::max((mdb_count)MSG_HASH_SIZE, numHdrs)); + } + if (m_headersInUse) { + if (key == nsMsgKey_None) hdr->GetMessageKey(&key); + PLDHashEntryHdr* entry = + m_headersInUse->Add((void*)(uintptr_t)key, mozilla::fallible); + if (!entry) return NS_ERROR_OUT_OF_MEMORY; // XXX out of memory + + MsgHdrHashElement* element = static_cast<MsgHdrHashElement*>(entry); + element->mHdr = hdr; + element->mKey = key; + // the hash table won't add ref, we'll do it ourselves + // stand for the addref that CreateMsgHdr normally does. + NS_ADDREF(hdr); + return NS_OK; + } + + return NS_ERROR_OUT_OF_MEMORY; +} + +nsresult nsMsgDatabase::ClearUseHdrCache() { + if (m_headersInUse) { + // clear mdb row pointers of any headers still in use, because the + // underlying db is going away. + for (auto iter = m_headersInUse->Iter(); !iter.Done(); iter.Next()) { + auto element = static_cast<const MsgHdrHashElement*>(iter.Get()); + if (element && element->mHdr) { + nsMsgHdr* msgHdr = static_cast<nsMsgHdr*>( + element->mHdr); // closed system, so this is ok + // clear out m_mdbRow member variable - the db is going away, which + // means that this member variable might very well point to a mork db + // that is gone. + NS_IF_RELEASE(msgHdr->m_mdbRow); + // NS_IF_RELEASE(msgHdr->m_mdb); + } + } + delete m_headersInUse; + m_headersInUse = nullptr; + } + return NS_OK; +} + +nsresult nsMsgDatabase::RemoveHdrFromUseCache(nsIMsgDBHdr* hdr, nsMsgKey key) { + if (m_headersInUse) { + if (key == nsMsgKey_None) hdr->GetMessageKey(&key); + + m_headersInUse->Remove((void*)(uintptr_t)key); + } + return NS_OK; +} + +nsresult nsMsgDatabase::CreateMsgHdr(nsIMdbRow* hdrRow, nsMsgKey key, + nsIMsgDBHdr** result) { + NS_ENSURE_ARG_POINTER(hdrRow); + NS_ENSURE_ARG_POINTER(result); + + nsresult rv = GetHdrFromUseCache(key, result); + if (NS_SUCCEEDED(rv) && *result) { + hdrRow->Release(); + return rv; + } + + nsMsgHdr* msgHdr = new nsMsgHdr(this, hdrRow); + if (!msgHdr) return NS_ERROR_OUT_OF_MEMORY; + msgHdr->SetMessageKey(key); + // don't need to addref here; GetHdrFromUseCache addrefs. + *result = msgHdr; + + AddHdrToCache(msgHdr, key); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::AddListener(nsIDBChangeListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + m_ChangeListeners.AppendElementUnlessExists(aListener); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::RemoveListener(nsIDBChangeListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + m_ChangeListeners.RemoveElement(aListener); + return NS_OK; +} + +// XXX should we return rv for listener->propertyfunc_? +#define NOTIFY_LISTENERS(propertyfunc_, params_) \ + PR_BEGIN_MACRO \ + nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator iter( \ + m_ChangeListeners); \ + nsCOMPtr<nsIDBChangeListener> listener; \ + while (iter.HasMore()) { \ + listener = iter.GetNext(); \ + listener->propertyfunc_ params_; \ + } \ + PR_END_MACRO + +// change announcer methods - just broadcast to all listeners. +NS_IMETHODIMP nsMsgDatabase::NotifyHdrChangeAll( + nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) { + // We will only notify the change if the header exists in the database. + // This allows database functions to be usable in both the case where the + // header is in the db, or the header is not so no notifications should be + // given. + nsMsgKey key; + bool inDb = false; + if (aHdrChanged) { + aHdrChanged->GetMessageKey(&key); + ContainsKey(key, &inDb); + } + if (inDb) + NOTIFY_LISTENERS(OnHdrFlagsChanged, + (aHdrChanged, aOldFlags, aNewFlags, aInstigator)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::NotifyReadChanged( + nsIDBChangeListener* aInstigator) { + NOTIFY_LISTENERS(OnReadChanged, (aInstigator)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::NotifyJunkScoreChanged( + nsIDBChangeListener* aInstigator) { + NOTIFY_LISTENERS(OnJunkScoreChanged, (aInstigator)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::NotifyHdrDeletedAll( + nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, int32_t aFlags, + nsIDBChangeListener* aInstigator) { + NOTIFY_LISTENERS(OnHdrDeleted, + (aHdrDeleted, aParentKey, aFlags, aInstigator)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::NotifyHdrAddedAll( + nsIMsgDBHdr* aHdrAdded, nsMsgKey aParentKey, int32_t aFlags, + nsIDBChangeListener* aInstigator) { +#ifdef DEBUG_bienvenu1 + printf("notifying add of %ld parent %ld\n", keyAdded, parentKey); +#endif + NOTIFY_LISTENERS(OnHdrAdded, (aHdrAdded, aParentKey, aFlags, aInstigator)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::NotifyParentChangedAll( + nsMsgKey aKeyReparented, nsMsgKey aOldParent, nsMsgKey aNewParent, + nsIDBChangeListener* aInstigator) { + NOTIFY_LISTENERS(OnParentChanged, + (aKeyReparented, aOldParent, aNewParent, aInstigator)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::NotifyAnnouncerGoingAway(void) { + NOTIFY_LISTENERS(OnAnnouncerGoingAway, (this)); + return NS_OK; +} + +bool nsMsgDatabase::MatchDbName(nsIFile* dbFile) // returns true if they match +{ + NS_ENSURE_TRUE(m_dbFile, false); + return dbFile->NativePath().Equals(m_dbFile->NativePath()); +} + +void nsMsgDBService::AddToCache(nsMsgDatabase* pMessageDB) { +#ifdef DEBUG_David_Bienvenu + NS_ASSERTION(m_dbCache.Length() < 50, "50 or more open db's"); +#endif +#ifdef DEBUG + if (pMessageDB->m_folder) { + nsCOMPtr<nsIMsgDatabase> msgDB; + CachedDBForFolder(pMessageDB->m_folder, getter_AddRefs(msgDB)); + NS_ASSERTION(!msgDB, "shouldn't have db in cache"); + } +#endif + m_dbCache.AppendElement(pMessageDB); +} + +/** + * Log the open db's, and how many headers are in memory. + */ +void nsMsgDBService::DumpCache() { + nsMsgDatabase* db = nullptr; + MOZ_LOG(DBLog, LogLevel::Info, ("%zu open DBs", m_dbCache.Length())); + for (uint32_t i = 0; i < m_dbCache.Length(); i++) { + db = m_dbCache.ElementAt(i); + MOZ_LOG(DBLog, LogLevel::Info, + ("%s - %" PRIu32 " hdrs in use", + db->m_dbFile->HumanReadablePath().get(), + db->m_headersInUse ? db->m_headersInUse->EntryCount() : 0)); + } +} + +// Memory Reporting implementations + +size_t nsMsgDatabase::SizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t totalSize = 0; + if (m_dbFolderInfo) + totalSize += m_dbFolderInfo->SizeOfExcludingThis(aMallocSizeOf); + if (m_mdbEnv) { + nsIMdbHeap* morkHeap = nullptr; + m_mdbEnv->GetHeap(&morkHeap); + if (morkHeap) totalSize += morkHeap->GetUsedSize(); + } + totalSize += m_newSet.ShallowSizeOfExcludingThis(aMallocSizeOf); + totalSize += m_ChangeListeners.ShallowSizeOfExcludingThis(aMallocSizeOf); + totalSize += m_threads.ShallowSizeOfExcludingThis(aMallocSizeOf); + // We have two tables of header objects, but every header in m_cachedHeaders + // should be in m_headersInUse. + // double-counting... + size_t headerSize = 0; + if (m_headersInUse) { + headerSize = m_headersInUse->ShallowSizeOfIncludingThis(aMallocSizeOf); + for (auto iter = m_headersInUse->Iter(); !iter.Done(); iter.Next()) { + auto entry = static_cast<MsgHdrHashElement*>(iter.Get()); + // Sigh, this is dangerous, but so long as this is a closed system, this + // is safe. + headerSize += static_cast<nsMsgHdr*>(entry->mHdr) + ->SizeOfIncludingThis(aMallocSizeOf); + } + } + totalSize += headerSize; + if (m_msgReferences) + totalSize += m_msgReferences->ShallowSizeOfIncludingThis(aMallocSizeOf); + return totalSize; +} + +namespace mozilla { +namespace mailnews { + +MOZ_DEFINE_MALLOC_SIZE_OF(GetMallocSize) + +class MsgDBReporter final : public nsIMemoryReporter { + nsWeakPtr mDatabase; + + public: + explicit MsgDBReporter(nsMsgDatabase* db) + : mDatabase(do_GetWeakReference(db)) {} + + NS_DECL_ISUPPORTS + NS_IMETHOD GetName(nsACString& aName) { + aName.AssignLiteral("msg-database-objects"); + return NS_OK; + } + + NS_IMETHOD CollectReports(nsIHandleReportCallback* aCb, nsISupports* aClosure, + bool aAnonymize) override { + nsCString path; + GetPath(path, aAnonymize); + nsCOMPtr<nsIMsgDatabase> database = do_QueryReferent(mDatabase); + nsMsgDatabase* db = + database ? static_cast<nsMsgDatabase*>(database.get()) : nullptr; + return aCb->Callback(EmptyCString(), path, nsIMemoryReporter::KIND_HEAP, + nsIMemoryReporter::UNITS_BYTES, + db ? db->SizeOfIncludingThis(GetMallocSize) : 0, + "Memory used for the folder database."_ns, aClosure); + } + + void GetPath(nsACString& memoryPath, bool aAnonymize) { + memoryPath.AssignLiteral("explicit/maildb/database("); + nsCOMPtr<nsIMsgDatabase> database = do_QueryReferent(mDatabase); + nsCOMPtr<nsIMsgFolder> folder; + if (database) database->GetFolder(getter_AddRefs(folder)); + if (folder) { + if (aAnonymize) + memoryPath.AppendLiteral("<anonymized>"); + else { + nsAutoCString folderURL; + folder->GetFolderURL(folderURL); + folderURL.ReplaceChar('/', '\\'); + memoryPath += folderURL; + } + } else { + memoryPath.AppendLiteral("UNKNOWN-FOLDER"); + } + memoryPath.Append(')'); + } + + private: + ~MsgDBReporter() {} +}; + +NS_IMPL_ISUPPORTS(MsgDBReporter, nsIMemoryReporter) +} // namespace mailnews +} // namespace mozilla + +nsMsgDatabase::nsMsgDatabase() + : m_dbFolderInfo(nullptr), + m_nextPseudoMsgKey(kFirstPseudoKey), + m_mdbEnv(nullptr), + m_mdbStore(nullptr), + m_mdbAllMsgHeadersTable(nullptr), + m_mdbAllThreadsTable(nullptr), + m_create(false), + m_leaveInvalidDB(false), + m_mdbTokensInitialized(false), + m_hdrRowScopeToken(0), + m_hdrTableKindToken(0), + m_threadTableKindToken(0), + m_subjectColumnToken(0), + m_senderColumnToken(0), + m_messageIdColumnToken(0), + m_referencesColumnToken(0), + m_recipientsColumnToken(0), + m_dateColumnToken(0), + m_messageSizeColumnToken(0), + m_flagsColumnToken(0), + m_priorityColumnToken(0), + m_labelColumnToken(0), + m_numLinesColumnToken(0), + m_ccListColumnToken(0), + m_bccListColumnToken(0), + m_threadFlagsColumnToken(0), + m_threadIdColumnToken(0), + m_threadChildrenColumnToken(0), + m_threadUnreadChildrenColumnToken(0), + m_messageThreadIdColumnToken(0), + m_threadSubjectColumnToken(0), + m_messageCharSetColumnToken(0), + m_threadParentColumnToken(0), + m_threadRootKeyColumnToken(0), + m_threadNewestMsgDateColumnToken(0), + m_offlineMsgOffsetColumnToken(0), + m_offlineMessageSizeColumnToken(0), + m_headersInUse(nullptr), + m_cachedHeaders(nullptr), + m_bCacheHeaders(true), + m_cachedThreadId(nsMsgKey_None), + m_msgReferences(nullptr), + m_cacheSize(kMaxHdrsInCache) { + mMemReporter = new mozilla::mailnews::MsgDBReporter(this); + mozilla::RegisterWeakMemoryReporter(mMemReporter); +} + +nsMsgDatabase::~nsMsgDatabase() { + mozilla::UnregisterWeakMemoryReporter(mMemReporter); + mMemReporter = nullptr; + // Close(FALSE); // better have already been closed. + ClearCachedObjects(true); + InvalidateEnumerators(); + delete m_cachedHeaders; + delete m_headersInUse; + + if (m_msgReferences) { + delete m_msgReferences; + m_msgReferences = nullptr; + } + + MOZ_LOG(DBLog, LogLevel::Info, + ("closing database %s", m_dbFile->HumanReadablePath().get())); + + nsCOMPtr<nsIMsgDBService> serv( + do_GetService("@mozilla.org/msgDatabase/msgDBService;1")); + if (serv) static_cast<nsMsgDBService*>(serv.get())->RemoveFromCache(this); + + // if the db folder info refers to the mdb db, we must clear it because + // the reference will be a dangling one soon. + if (m_dbFolderInfo) m_dbFolderInfo->ReleaseExternalReferences(); + m_dbFolderInfo = nullptr; + + if (m_mdbAllMsgHeadersTable) m_mdbAllMsgHeadersTable->Release(); + + if (m_mdbAllThreadsTable) m_mdbAllThreadsTable->Release(); + + if (m_mdbStore) m_mdbStore->Release(); + + if (m_mdbEnv) { + m_mdbEnv->Release(); //??? is this right? + m_mdbEnv = nullptr; + } + m_ChangeListeners.Clear(); +} + +NS_IMPL_ISUPPORTS(nsMsgDatabase, nsIMsgDatabase, nsIMsgOfflineOpsDatabase, + nsIDBChangeAnnouncer) + +nsresult nsMsgDatabase::GetMDBFactory(nsIMdbFactory** aMdbFactory) { + if (!mMdbFactory) { + nsresult rv; + nsCOMPtr<nsIMdbFactoryService> mdbFactoryService = + do_GetService("@mozilla.org/db/mork;1", &rv); + if (NS_SUCCEEDED(rv) && mdbFactoryService) { + rv = mdbFactoryService->GetMdbFactory(getter_AddRefs(mMdbFactory)); + NS_ENSURE_SUCCESS(rv, rv); + if (!mMdbFactory) return NS_ERROR_FAILURE; + } + } + NS_ADDREF(*aMdbFactory = mMdbFactory); + return NS_OK; +} + +// aLeaveInvalidDB: true if caller wants back a db even out of date. +// If so, they'll extract out the interesting info from the db, close it, +// delete it, and then try to open the db again, prior to reparsing. +nsresult nsMsgDatabase::Open(nsMsgDBService* aDBService, nsIFile* aFolderName, + bool aCreate, bool aLeaveInvalidDB) { + return nsMsgDatabase::OpenInternal(aDBService, aFolderName, aCreate, + aLeaveInvalidDB, + true /* open synchronously */); +} + +nsresult nsMsgDatabase::OpenInternal(nsMsgDBService* aDBService, + nsIFile* summaryFile, bool aCreate, + bool aLeaveInvalidDB, bool sync) { + MOZ_LOG(DBLog, LogLevel::Info, + ("nsMsgDatabase::Open(%s, %s, %p, %s)", + summaryFile->HumanReadablePath().get(), aCreate ? "TRUE" : "FALSE", + this, aLeaveInvalidDB ? "TRUE" : "FALSE")); + + nsresult rv = OpenMDB(summaryFile, aCreate, sync); + if (NS_FAILED(rv)) + MOZ_LOG(DBLog, LogLevel::Info, + ("error opening db %" PRIx32, static_cast<uint32_t>(rv))); + + if (MOZ_LOG_TEST(DBLog, LogLevel::Debug)) aDBService->DumpCache(); + + if (rv == NS_ERROR_FILE_NOT_FOUND) return rv; + + m_create = aCreate; + m_leaveInvalidDB = aLeaveInvalidDB; + if (!sync && NS_SUCCEEDED(rv)) { + aDBService->AddToCache(this); + // remember open options for when the parsing is complete. + return rv; + } + return CheckForErrors(rv, true, aDBService, summaryFile); +} + +nsresult nsMsgDatabase::CheckForErrors(nsresult err, bool sync, + nsMsgDBService* aDBService, + nsIFile* summaryFile) { + nsCOMPtr<nsIDBFolderInfo> folderInfo; + bool summaryFileExists; + bool newFile = false; + bool deleteInvalidDB = false; + + bool exists; + int64_t fileSize = 0; + summaryFile->Exists(&exists); + if (exists) summaryFile->GetFileSize(&fileSize); + // if the old summary doesn't exist, we're creating a new one. + if ((!exists || !fileSize) && m_create) newFile = true; + + summaryFileExists = exists && fileSize > 0; + + if (NS_SUCCEEDED(err)) { + if (!m_dbFolderInfo) { + err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } else { + if (!newFile && summaryFileExists) { + bool valid = false; + nsresult rv = GetSummaryValid(&valid); + if (NS_FAILED(rv) || !valid) + err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } + // compare current version of db versus filed out version info. + uint32_t version; + m_dbFolderInfo->GetVersion(&version); + if (GetCurVersion() != version) + err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + + // Check if we should force a reparse because, for example, we have + // reached the key limit. + bool forceReparse; + m_dbFolderInfo->GetBooleanProperty("forceReparse", false, &forceReparse); + if (forceReparse) { + NS_WARNING("Forcing a reparse presumably because key limit reached"); + err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } + } + if (NS_FAILED(err) && !m_leaveInvalidDB) deleteInvalidDB = true; + } else if (err != NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) { + // No point declaring it out-of-date and trying to delete it + // if it's missing. + // We get here with NS_ERROR_FAILURE when Mork can't open the + // file due to too many open files. In this case there is no + // point to blow away the MSF file. + err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + if (!m_leaveInvalidDB) deleteInvalidDB = true; + } + + if (deleteInvalidDB) { + // this will make the db folder info release its ref to the mail db... + m_dbFolderInfo = nullptr; + ForceClosed(); + if (err == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) + summaryFile->Remove(false); + } + if (NS_FAILED(err) || newFile) { + // if we couldn't open file, or we have a blank one, and we're supposed + // to upgrade, upgrade it. + if (newFile && !m_leaveInvalidDB) // caller is upgrading, and we have empty + // summary file, + { // leave db around and open so caller can upgrade it. + err = NS_MSG_ERROR_FOLDER_SUMMARY_MISSING; + } else if (NS_FAILED(err) && + err != NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) { + Close(false); + summaryFile->Remove(false); // blow away the db if it's corrupt. + } + } + if (sync && (NS_SUCCEEDED(err) || err == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING)) + aDBService->AddToCache(this); + return (summaryFileExists) ? err : NS_MSG_ERROR_FOLDER_SUMMARY_MISSING; +} + +/** + * Open the MDB database synchronously or async based on sync argument. + * If successful, this routine will set up the m_mdbStore and m_mdbEnv of + * the database object so other database calls can work. + */ +nsresult nsMsgDatabase::OpenMDB(nsIFile* dbFile, bool create, bool sync) { + nsCOMPtr<nsIMdbFactory> mdbFactory; + nsresult ret = GetMDBFactory(getter_AddRefs(mdbFactory)); + NS_ENSURE_SUCCESS(ret, ret); + + ret = mdbFactory->MakeEnv(NULL, &m_mdbEnv); + if (NS_SUCCEEDED(ret)) { + nsIMdbHeap* dbHeap = nullptr; + + if (m_mdbEnv) m_mdbEnv->SetAutoClear(true); + PathString dbName = dbFile->NativePath(); + ret = dbFile->Clone(getter_AddRefs(m_dbFile)); + NS_ENSURE_SUCCESS(ret, ret); + bool exists = false; + ret = dbFile->Exists(&exists); + if (!exists) { + ret = NS_MSG_ERROR_FOLDER_SUMMARY_MISSING; + } + // If m_thumb is set, we're asynchronously opening the db already. + else if (!m_thumb) { + mdbOpenPolicy inOpenPolicy; + mdb_bool canOpen; + mdbYarn outFormatVersion; + + nsIMdbFile* oldFile = nullptr; + ret = mdbFactory->OpenOldFile( + m_mdbEnv, dbHeap, dbName.get(), + mdbBool_kFalse, // not readonly, we want modifiable + &oldFile); + if (oldFile) { + if (NS_SUCCEEDED(ret)) { + ret = mdbFactory->CanOpenFilePort(m_mdbEnv, + oldFile, // the file to investigate + &canOpen, &outFormatVersion); + if (NS_SUCCEEDED(ret) && canOpen) { + inOpenPolicy.mOpenPolicy_ScopePlan.mScopeStringSet_Count = 0; + inOpenPolicy.mOpenPolicy_MinMemory = 0; + inOpenPolicy.mOpenPolicy_MaxLazy = 0; + + ret = mdbFactory->OpenFileStore(m_mdbEnv, dbHeap, oldFile, + &inOpenPolicy, + getter_AddRefs(m_thumb)); + } else + ret = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } + NS_RELEASE(oldFile); // always release our file ref, store has own + } + } + if (NS_SUCCEEDED(ret) && m_thumb && sync) { + mdb_count outTotal; // total somethings to do in operation + mdb_count outCurrent; // subportion of total completed so far + mdb_bool outDone = false; // is operation finished? + mdb_bool outBroken; // is operation irreparably dead and broken? + do { + ret = m_thumb->DoMore(m_mdbEnv, &outTotal, &outCurrent, &outDone, + &outBroken); + if (NS_FAILED(ret)) { // mork isn't really doing NS errors yet. + outDone = true; + break; + } + } while (NS_SUCCEEDED(ret) && !outBroken && !outDone); + // m_mdbEnv->ClearErrors(); // ### temporary... + // only 0 is a non-error return. + if (NS_SUCCEEDED(ret) && outDone) { + ret = mdbFactory->ThumbToOpenStore(m_mdbEnv, m_thumb, &m_mdbStore); + if (NS_SUCCEEDED(ret)) + ret = (m_mdbStore) ? InitExistingDB() : NS_ERROR_FAILURE; + } +#ifdef DEBUG_bienvenu1 + DumpContents(); +#endif + m_thumb = nullptr; + } else if (create) // ### need error code saying why open file store failed + { + nsIMdbFile* newFile = 0; + ret = mdbFactory->CreateNewFile(m_mdbEnv, dbHeap, dbName.get(), &newFile); + if (NS_FAILED(ret)) ret = NS_ERROR_FILE_NOT_FOUND; + if (newFile) { + if (NS_SUCCEEDED(ret)) { + mdbOpenPolicy inOpenPolicy; + + inOpenPolicy.mOpenPolicy_ScopePlan.mScopeStringSet_Count = 0; + inOpenPolicy.mOpenPolicy_MinMemory = 0; + inOpenPolicy.mOpenPolicy_MaxLazy = 0; + + ret = mdbFactory->CreateNewFileStore(m_mdbEnv, dbHeap, newFile, + &inOpenPolicy, &m_mdbStore); + if (NS_SUCCEEDED(ret)) + ret = (m_mdbStore) ? InitNewDB() : NS_ERROR_FAILURE; + } + NS_RELEASE(newFile); // always release our file ref, store has own + } + } + } + + return ret; +} + +nsresult nsMsgDatabase::CloseMDB(bool commit) { + if (commit) Commit(nsMsgDBCommitType::kSessionCommit); + return (NS_OK); +} + +// force the database to close - this'll flush out anybody holding onto +// a database without having a listener! +// This is evil in the com world, but there are times we need to delete the +// file. +NS_IMETHODIMP nsMsgDatabase::ForceClosed() { + nsresult err = NS_OK; + + // make sure someone has a reference so object won't get deleted out from + // under us. + NS_ADDREF_THIS(); + NotifyAnnouncerGoingAway(); + // make sure dbFolderInfo isn't holding onto mork stuff because mork db is + // going away + if (m_dbFolderInfo) m_dbFolderInfo->ReleaseExternalReferences(); + m_dbFolderInfo = nullptr; + + err = CloseMDB(true); // Backup DB will try to recover info, so commit + ClearCachedObjects(true); + InvalidateEnumerators(); + if (m_mdbAllMsgHeadersTable) { + m_mdbAllMsgHeadersTable->Release(); + m_mdbAllMsgHeadersTable = nullptr; + } + if (m_mdbAllThreadsTable) { + m_mdbAllThreadsTable->Release(); + m_mdbAllThreadsTable = nullptr; + } + if (m_mdbStore) { + m_mdbStore->Release(); + m_mdbStore = nullptr; + } + + // There'd better not be any listeners, because we're going away. + NS_ASSERTION(m_ChangeListeners.IsEmpty(), + "shouldn't have any listeners left"); + m_ChangeListeners.Clear(); + + NS_RELEASE_THIS(); + return err; +} + +NS_IMETHODIMP nsMsgDatabase::GetDBFolderInfo(nsIDBFolderInfo** result) { + if (!m_dbFolderInfo) { + NS_ERROR("db must be corrupt"); + return NS_ERROR_NULL_POINTER; + } + NS_ADDREF(*result = m_dbFolderInfo); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetFolder(nsIMsgFolder** aFolder) { + NS_ENSURE_ARG_POINTER(aFolder); + NS_IF_ADDREF(*aFolder = m_folder); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::Commit(nsMsgDBCommit commitType) { + nsresult err = NS_OK; + nsCOMPtr<nsIMdbThumb> commitThumb; + + RememberLastUseTime(); + if (commitType == nsMsgDBCommitType::kLargeCommit || + commitType == nsMsgDBCommitType::kSessionCommit) { + mdb_percent outActualWaste = 0; + mdb_bool outShould; + if (m_mdbStore) { + err = + m_mdbStore->ShouldCompress(GetEnv(), 30, &outActualWaste, &outShould); + if (NS_SUCCEEDED(err) && outShould) + commitType = nsMsgDBCommitType::kCompressCommit; + } + } + // commitType = nsMsgDBCommitType::kCompressCommit; // ### until incremental + // writing works. + + if (m_mdbStore) { + switch (commitType) { + case nsMsgDBCommitType::kLargeCommit: + err = m_mdbStore->LargeCommit(GetEnv(), getter_AddRefs(commitThumb)); + break; + case nsMsgDBCommitType::kSessionCommit: + err = m_mdbStore->SessionCommit(GetEnv(), getter_AddRefs(commitThumb)); + break; + case nsMsgDBCommitType::kCompressCommit: + err = m_mdbStore->CompressCommit(GetEnv(), getter_AddRefs(commitThumb)); + break; + } + } + if (commitThumb) { + mdb_count outTotal = 0; // total somethings to do in operation + mdb_count outCurrent = 0; // subportion of total completed so far + mdb_bool outDone = false; // is operation finished? + mdb_bool outBroken = false; // is operation irreparably dead and broken? + while (!outDone && !outBroken && NS_SUCCEEDED(err)) { + err = commitThumb->DoMore(GetEnv(), &outTotal, &outCurrent, &outDone, + &outBroken); + } + } + // ### do something with error, but clear it now because mork errors out on + // commits. + if (GetEnv()) GetEnv()->ClearErrors(); + + 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) { + nsCOMPtr<nsIMsgFolderCacheElement> cacheElement; + nsCString persistentPath; + NS_ENSURE_TRUE(m_dbFile, NS_ERROR_NULL_POINTER); + rv = m_dbFile->GetPersistentDescriptor(persistentPath); + NS_ENSURE_SUCCESS(rv, err); + rv = folderCache->GetCacheElement(persistentPath, false, + getter_AddRefs(cacheElement)); + if (NS_SUCCEEDED(rv) && cacheElement && m_dbFolderInfo) { + int32_t totalMessages, unreadMessages, pendingMessages, + pendingUnreadMessages; + + m_dbFolderInfo->GetNumMessages(&totalMessages); + m_dbFolderInfo->GetNumUnreadMessages(&unreadMessages); + m_dbFolderInfo->GetImapUnreadPendingMessages(&pendingUnreadMessages); + m_dbFolderInfo->GetImapTotalPendingMessages(&pendingMessages); + cacheElement->SetCachedInt32("totalMsgs", totalMessages); + cacheElement->SetCachedInt32("totalUnreadMsgs", unreadMessages); + cacheElement->SetCachedInt32("pendingMsgs", pendingMessages); + cacheElement->SetCachedInt32("pendingUnreadMsgs", + pendingUnreadMessages); + } + } + } + + return err; +} + +NS_IMETHODIMP nsMsgDatabase::Close(bool forceCommit /* = TRUE */) { + InvalidateEnumerators(); + return CloseMDB(forceCommit); +} + +const char* kMsgHdrsScope = + "ns:msg:db:row:scope:msgs:all"; // scope for all headers table +const char* kMsgHdrsTableKind = "ns:msg:db:table:kind:msgs"; +const char* kThreadTableKind = "ns:msg:db:table:kind:thread"; +const char* kThreadHdrsScope = + "ns:msg:db:row:scope:threads:all"; // scope for all threads table +const char* kAllThreadsTableKind = + "ns:msg:db:table:kind:allthreads"; // kind for table of all threads +const char* kSubjectColumnName = "subject"; +const char* kSenderColumnName = "sender"; +const char* kMessageIdColumnName = "message-id"; +const char* kReferencesColumnName = "references"; +const char* kRecipientsColumnName = "recipients"; +const char* kDateColumnName = "date"; +const char* kMessageSizeColumnName = "size"; +const char* kFlagsColumnName = "flags"; +const char* kPriorityColumnName = "priority"; +const char* kLabelColumnName = "label"; +const char* kNumLinesColumnName = "numLines"; +const char* kCCListColumnName = "ccList"; +const char* kBCCListColumnName = "bccList"; +const char* kMessageThreadIdColumnName = "msgThreadId"; +const char* kThreadFlagsColumnName = "threadFlags"; +const char* kThreadIdColumnName = "threadId"; +const char* kThreadChildrenColumnName = "children"; +const char* kThreadUnreadChildrenColumnName = "unreadChildren"; +const char* kThreadSubjectColumnName = "threadSubject"; +const char* kMessageCharSetColumnName = "msgCharSet"; +const char* kThreadParentColumnName = "threadParent"; +const char* kThreadRootColumnName = "threadRoot"; +const char* kThreadNewestMsgDateColumnName = "threadNewestMsgDate"; +const char* kOfflineMsgOffsetColumnName = "msgOffset"; +const char* kOfflineMsgSizeColumnName = "offlineMsgSize"; +struct mdbOid gAllMsgHdrsTableOID; +struct mdbOid gAllThreadsTableOID; +const char* kFixedBadRefThreadingProp = "fixedBadRefThreading"; + +// set up empty tables, dbFolderInfo, etc. +nsresult nsMsgDatabase::InitNewDB() { + nsresult err = NS_OK; + + err = InitMDBInfo(); + if (NS_SUCCEEDED(err)) { + nsDBFolderInfo* dbFolderInfo = new nsDBFolderInfo(this); + if (dbFolderInfo) { + err = dbFolderInfo->AddToNewMDB(); + dbFolderInfo->SetVersion(GetCurVersion()); + dbFolderInfo->SetBooleanProperty("forceReparse", false); + dbFolderInfo->SetBooleanProperty(kFixedBadRefThreadingProp, true); + nsIMdbStore* store = GetStore(); + // create the unique table for the dbFolderInfo. + struct mdbOid allMsgHdrsTableOID; + struct mdbOid allThreadsTableOID; + if (!store) return NS_ERROR_NULL_POINTER; + + allMsgHdrsTableOID.mOid_Scope = m_hdrRowScopeToken; + allMsgHdrsTableOID.mOid_Id = kAllMsgHdrsTableKey; + allThreadsTableOID.mOid_Scope = m_threadRowScopeToken; + allThreadsTableOID.mOid_Id = kAllThreadsTableKey; + + // TODO: check this error value? + (void)store->NewTableWithOid(GetEnv(), &allMsgHdrsTableOID, + m_hdrTableKindToken, false, nullptr, + &m_mdbAllMsgHeadersTable); + + // error here is not fatal. + (void)store->NewTableWithOid(GetEnv(), &allThreadsTableOID, + m_allThreadsTableKindToken, false, nullptr, + &m_mdbAllThreadsTable); + + m_dbFolderInfo = dbFolderInfo; + + } else + err = NS_ERROR_OUT_OF_MEMORY; + } + return err; +} + +nsresult nsMsgDatabase::GetTableCreateIfMissing(const char* scope, + const char* kind, + nsIMdbTable** table, + mdb_token& scopeToken, + mdb_token& kindToken) { + struct mdbOid tableOID; + + if (!m_mdbStore) return NS_ERROR_FAILURE; + (void)m_mdbStore->StringToToken(GetEnv(), scope, &scopeToken); + (void)m_mdbStore->StringToToken(GetEnv(), kind, &kindToken); + tableOID.mOid_Scope = scopeToken; + tableOID.mOid_Id = 1; + + nsresult rv = m_mdbStore->GetTable(GetEnv(), &tableOID, table); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + + // create new all all offline ops table, if it doesn't exist. + if (NS_SUCCEEDED(rv) && !*table) { + rv = m_mdbStore->NewTable(GetEnv(), scopeToken, kindToken, false, nullptr, + table); + if (NS_FAILED(rv) || !*table) rv = NS_ERROR_FAILURE; + } + NS_ASSERTION(NS_SUCCEEDED(rv), "couldn't create offline ops table"); + return rv; +} + +nsresult nsMsgDatabase::InitExistingDB() { + nsresult err = NS_OK; + + err = InitMDBInfo(); + if (NS_SUCCEEDED(err)) { + err = GetStore()->GetTable(GetEnv(), &gAllMsgHdrsTableOID, + &m_mdbAllMsgHeadersTable); + if (NS_SUCCEEDED(err)) { + m_dbFolderInfo = new nsDBFolderInfo(this); + if (m_dbFolderInfo) { + err = m_dbFolderInfo->InitFromExistingDB(); + } + } else + err = NS_ERROR_FAILURE; + + NS_ASSERTION(NS_SUCCEEDED(err), "failed initing existing db"); + NS_ENSURE_SUCCESS(err, err); + // create new all msg hdrs table, if it doesn't exist. + if (NS_SUCCEEDED(err) && !m_mdbAllMsgHeadersTable) { + struct mdbOid allMsgHdrsTableOID; + allMsgHdrsTableOID.mOid_Scope = m_hdrRowScopeToken; + allMsgHdrsTableOID.mOid_Id = kAllMsgHdrsTableKey; + + nsresult mdberr = GetStore()->NewTableWithOid( + GetEnv(), &allMsgHdrsTableOID, m_hdrTableKindToken, false, nullptr, + &m_mdbAllMsgHeadersTable); + if (NS_FAILED(mdberr) || !m_mdbAllMsgHeadersTable) err = NS_ERROR_FAILURE; + } + struct mdbOid allThreadsTableOID; + allThreadsTableOID.mOid_Scope = m_threadRowScopeToken; + allThreadsTableOID.mOid_Id = kAllThreadsTableKey; + err = GetStore()->GetTable(GetEnv(), &gAllThreadsTableOID, + &m_mdbAllThreadsTable); + if (!m_mdbAllThreadsTable) { + nsresult mdberr = GetStore()->NewTableWithOid( + GetEnv(), &allThreadsTableOID, m_allThreadsTableKindToken, false, + nullptr, &m_mdbAllThreadsTable); + if (NS_FAILED(mdberr) || !m_mdbAllThreadsTable) err = NS_ERROR_FAILURE; + } + } + if (NS_SUCCEEDED(err) && m_dbFolderInfo) { + bool fixedBadRefThreading; + m_dbFolderInfo->GetBooleanProperty(kFixedBadRefThreadingProp, false, + &fixedBadRefThreading); + if (!fixedBadRefThreading) { + nsCOMPtr<nsIMsgEnumerator> enumerator; + err = EnumerateMessages(getter_AddRefs(enumerator)); + if (NS_SUCCEEDED(err) && enumerator) { + bool hasMore; + + while (NS_SUCCEEDED(err = enumerator->HasMoreElements(&hasMore)) && + hasMore) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + err = enumerator->GetNext(getter_AddRefs(msgHdr)); + if (msgHdr && NS_SUCCEEDED(err)) { + nsCString messageId; + nsAutoCString firstReference; + msgHdr->GetMessageId(getter_Copies(messageId)); + msgHdr->GetStringReference(0, firstReference); + if (messageId.Equals(firstReference)) { + err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + break; + } + } + } + } + + m_dbFolderInfo->SetBooleanProperty(kFixedBadRefThreadingProp, true); + } + } + return err; +} + +// initialize the various tokens and tables in our db's env +nsresult nsMsgDatabase::InitMDBInfo() { + nsresult err = NS_OK; + + if (!m_mdbTokensInitialized && GetStore()) { + m_mdbTokensInitialized = true; + err = + GetStore()->StringToToken(GetEnv(), kMsgHdrsScope, &m_hdrRowScopeToken); + if (NS_SUCCEEDED(err)) { + GetStore()->StringToToken(GetEnv(), kSubjectColumnName, + &m_subjectColumnToken); + GetStore()->StringToToken(GetEnv(), kSenderColumnName, + &m_senderColumnToken); + GetStore()->StringToToken(GetEnv(), kMessageIdColumnName, + &m_messageIdColumnToken); + // if we just store references as a string, we won't get any savings from + // the fact there's a lot of duplication. So we may want to break them up + // into multiple columns, r1, r2, etc. + GetStore()->StringToToken(GetEnv(), kReferencesColumnName, + &m_referencesColumnToken); + // similarly, recipients could be tokenized properties + GetStore()->StringToToken(GetEnv(), kRecipientsColumnName, + &m_recipientsColumnToken); + GetStore()->StringToToken(GetEnv(), kDateColumnName, &m_dateColumnToken); + GetStore()->StringToToken(GetEnv(), kMessageSizeColumnName, + &m_messageSizeColumnToken); + GetStore()->StringToToken(GetEnv(), kFlagsColumnName, + &m_flagsColumnToken); + GetStore()->StringToToken(GetEnv(), kPriorityColumnName, + &m_priorityColumnToken); + GetStore()->StringToToken(GetEnv(), kLabelColumnName, + &m_labelColumnToken); + GetStore()->StringToToken(GetEnv(), kNumLinesColumnName, + &m_numLinesColumnToken); + GetStore()->StringToToken(GetEnv(), kCCListColumnName, + &m_ccListColumnToken); + GetStore()->StringToToken(GetEnv(), kBCCListColumnName, + &m_bccListColumnToken); + GetStore()->StringToToken(GetEnv(), kMessageThreadIdColumnName, + &m_messageThreadIdColumnToken); + GetStore()->StringToToken(GetEnv(), kThreadIdColumnName, + &m_threadIdColumnToken); + GetStore()->StringToToken(GetEnv(), kThreadFlagsColumnName, + &m_threadFlagsColumnToken); + GetStore()->StringToToken(GetEnv(), kThreadNewestMsgDateColumnName, + &m_threadNewestMsgDateColumnToken); + GetStore()->StringToToken(GetEnv(), kThreadChildrenColumnName, + &m_threadChildrenColumnToken); + GetStore()->StringToToken(GetEnv(), kThreadUnreadChildrenColumnName, + &m_threadUnreadChildrenColumnToken); + GetStore()->StringToToken(GetEnv(), kThreadSubjectColumnName, + &m_threadSubjectColumnToken); + GetStore()->StringToToken(GetEnv(), kMessageCharSetColumnName, + &m_messageCharSetColumnToken); + err = GetStore()->StringToToken(GetEnv(), kMsgHdrsTableKind, + &m_hdrTableKindToken); + if (NS_SUCCEEDED(err)) + err = GetStore()->StringToToken(GetEnv(), kThreadTableKind, + &m_threadTableKindToken); + err = GetStore()->StringToToken(GetEnv(), kAllThreadsTableKind, + &m_allThreadsTableKindToken); + err = GetStore()->StringToToken(GetEnv(), kThreadHdrsScope, + &m_threadRowScopeToken); + err = GetStore()->StringToToken(GetEnv(), kThreadParentColumnName, + &m_threadParentColumnToken); + err = GetStore()->StringToToken(GetEnv(), kThreadRootColumnName, + &m_threadRootKeyColumnToken); + err = GetStore()->StringToToken(GetEnv(), kOfflineMsgOffsetColumnName, + &m_offlineMsgOffsetColumnToken); + err = GetStore()->StringToToken(GetEnv(), kOfflineMsgSizeColumnName, + &m_offlineMessageSizeColumnToken); + + if (NS_SUCCEEDED(err)) { + // The table of all message hdrs will have table id 1. + gAllMsgHdrsTableOID.mOid_Scope = m_hdrRowScopeToken; + gAllMsgHdrsTableOID.mOid_Id = kAllMsgHdrsTableKey; + gAllThreadsTableOID.mOid_Scope = m_threadRowScopeToken; + gAllThreadsTableOID.mOid_Id = kAllThreadsTableKey; + } + } + } + return err; +} + +// Returns if the db contains this key +NS_IMETHODIMP nsMsgDatabase::ContainsKey(nsMsgKey key, bool* containsKey) { + nsresult err = NS_OK; + mdb_bool hasOid; + mdbOid rowObjectId; + + if (!containsKey || !m_mdbAllMsgHeadersTable) return NS_ERROR_NULL_POINTER; + *containsKey = false; + + rowObjectId.mOid_Id = key; + rowObjectId.mOid_Scope = m_hdrRowScopeToken; + err = m_mdbAllMsgHeadersTable->HasOid(GetEnv(), &rowObjectId, &hasOid); + if (NS_SUCCEEDED(err)) *containsKey = hasOid; + + return err; +} + +// get a message header for the given key. Caller must release()! +NS_IMETHODIMP nsMsgDatabase::GetMsgHdrForKey(nsMsgKey key, + nsIMsgDBHdr** pmsgHdr) { + *pmsgHdr = nullptr; + NS_ENSURE_ARG_POINTER(pmsgHdr); + NS_ENSURE_STATE(m_folder); + NS_ENSURE_STATE(m_mdbAllMsgHeadersTable); + NS_ENSURE_STATE(m_mdbStore); + + // Because this may be called a lot, and we don't want gettimeofday() to show + // up in trace logs, we just remember the most recent time any db was used, + // which should be close enough for our purposes. + m_lastUseTime = gLastUseTime; + + nsresult rv = GetHdrFromUseCache(key, pmsgHdr); + if (NS_SUCCEEDED(rv) && *pmsgHdr) return rv; + + mdbOid rowObjectId; + rowObjectId.mOid_Id = key; + rowObjectId.mOid_Scope = m_hdrRowScopeToken; + mdb_bool hasOid; + rv = m_mdbAllMsgHeadersTable->HasOid(GetEnv(), &rowObjectId, &hasOid); + if (NS_SUCCEEDED(rv) /* && hasOid */) { + nsIMdbRow* hdrRow; + rv = m_mdbStore->GetRow(GetEnv(), &rowObjectId, &hdrRow); + if (NS_SUCCEEDED(rv)) { + if (!hdrRow) { + rv = NS_ERROR_NULL_POINTER; + } else { + rv = CreateMsgHdr(hdrRow, key, pmsgHdr); + } + } + } + return rv; +} + +NS_IMETHODIMP nsMsgDatabase::DeleteMessage(nsMsgKey key, + nsIDBChangeListener* instigator, + bool commit) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + return DeleteHeader(msgHdr, instigator, commit, true); +} + +NS_IMETHODIMP nsMsgDatabase::DeleteMessages(nsTArray<nsMsgKey> const& nsMsgKeys, + nsIDBChangeListener* instigator) { + nsresult err = NS_OK; + + uint32_t kindex; + for (kindex = 0; kindex < nsMsgKeys.Length(); kindex++) { + nsMsgKey key = nsMsgKeys[kindex]; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + + bool hasKey; + + if (NS_SUCCEEDED(ContainsKey(key, &hasKey)) && hasKey) { + GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (!msgHdr) { + err = NS_MSG_MESSAGE_NOT_FOUND; + break; + } + err = DeleteHeader(msgHdr, instigator, kindex % 300 == 0, true); + if (NS_FAILED(err)) break; + } + } + return err; +} + +nsresult nsMsgDatabase::AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) { + uint32_t size = 0; + (void)msgHdr->GetMessageSize(&size); + return m_dbFolderInfo->ChangeExpungedBytes(size); +} + +NS_IMETHODIMP nsMsgDatabase::DeleteHeader(nsIMsgDBHdr* msg, + nsIDBChangeListener* instigator, + bool commit, bool notify) { + if (!msg) return NS_ERROR_NULL_POINTER; + + nsMsgHdr* msgHdr = + static_cast<nsMsgHdr*>(msg); // closed system, so this is ok + nsMsgKey key; + (void)msg->GetMessageKey(&key); + // only need to do this for mail - will this speed up news expiration? + SetHdrFlag(msg, true, nsMsgMessageFlags::Expunged); // tell mailbox (mail) + + bool hdrWasNew = m_newSet.BinaryIndexOf(key) != m_newSet.NoIndex; + m_newSet.RemoveElement(key); + + if (m_dbFolderInfo) { + bool isRead; + m_dbFolderInfo->ChangeNumMessages(-1); + IsRead(key, &isRead); + if (!isRead) m_dbFolderInfo->ChangeNumUnreadMessages(-1); + AdjustExpungedBytesOnDelete(msg); + } + + uint32_t flags; + nsMsgKey threadParent; + + // Save off flags and threadparent since they will no longer exist after we + // remove the header from the db. + if (notify) { + (void)msg->GetFlags(&flags); + msg->GetThreadParent(&threadParent); + } + + RemoveHeaderFromThread(msgHdr); + if (notify) { + // If deleted hdr was new, restore the new flag on flags + // so saved searches will know to reduce their new msg count. + if (hdrWasNew) flags |= nsMsgMessageFlags::New; + NotifyHdrDeletedAll(msg, threadParent, flags, + instigator); // tell listeners + } + // if (!onlyRemoveFromThread) // to speed up expiration, try this. But + // really need to do this in RemoveHeaderFromDB + nsresult ret = RemoveHeaderFromDB(msgHdr); + + if (commit) + Commit(nsMsgDBCommitType::kLargeCommit); // ### dmb is this a good time to + // commit? + return ret; +} + +NS_IMETHODIMP +nsMsgDatabase::UndoDelete(nsIMsgDBHdr* aMsgHdr) { + if (aMsgHdr) { + // Force deleted flag, so SetHdrFlag won't bail out because deleted flag + // isn't set. + uint32_t result; + aMsgHdr->OrFlags(nsMsgMessageFlags::Expunged, &result); + SetHdrFlag(aMsgHdr, false, + nsMsgMessageFlags::Expunged); // Clear deleted flag in db. + } + return NS_OK; +} + +nsresult nsMsgDatabase::RemoveHeaderFromThread(nsMsgHdr* msgHdr) { + if (!msgHdr) return NS_ERROR_NULL_POINTER; + nsresult ret = NS_OK; + nsCOMPtr<nsIMsgThread> thread; + ret = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + if (NS_SUCCEEDED(ret) && thread) { + ret = thread->RemoveChildHdr(msgHdr, this); + } + return ret; +} + +NS_IMETHODIMP nsMsgDatabase::RemoveHeaderMdbRow(nsIMsgDBHdr* msg) { + NS_ENSURE_ARG_POINTER(msg); + nsMsgHdr* msgHdr = + static_cast<nsMsgHdr*>(msg); // closed system, so this is ok + return RemoveHeaderFromDB(msgHdr); +} + +// This is a lower level routine which doesn't send notifications or +// update folder info. One use is when a rule fires moving a header +// from one db to another, to remove it from the first db. + +nsresult nsMsgDatabase::RemoveHeaderFromDB(nsMsgHdr* msgHdr) { + if (!msgHdr) return NS_ERROR_NULL_POINTER; + nsresult ret = NS_OK; + + RemoveHdrFromCache(msgHdr, nsMsgKey_None); + if (UseCorrectThreading()) RemoveMsgRefsFromHash(msgHdr); + nsIMdbRow* row = msgHdr->GetMDBRow(); + if (row) { + ret = m_mdbAllMsgHeadersTable->CutRow(GetEnv(), row); + row->CutAllColumns(GetEnv()); + } + msgHdr->ClearCachedValues(); + return ret; +} + +nsresult nsMsgDatabase::IsRead(nsMsgKey key, bool* pRead) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + return IsHeaderRead(msgHdr, pRead); +} + +uint32_t nsMsgDatabase::GetStatusFlags(nsIMsgDBHdr* msgHdr, + nsMsgMessageFlagType origFlags) { + uint32_t statusFlags = origFlags; + bool isRead = true; + + nsMsgKey key; + (void)msgHdr->GetMessageKey(&key); + if ((!m_newSet.IsEmpty() && m_newSet[m_newSet.Length() - 1] == key) || + (m_newSet.BinaryIndexOf(key) != m_newSet.NoIndex)) + statusFlags |= nsMsgMessageFlags::New; + if (NS_SUCCEEDED(IsHeaderRead(msgHdr, &isRead)) && isRead) + statusFlags |= nsMsgMessageFlags::Read; + return statusFlags; +} + +nsresult nsMsgDatabase::IsHeaderRead(nsIMsgDBHdr* msgHdr, bool* pRead) { + if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + nsMsgHdr* hdr = static_cast<nsMsgHdr*>(msgHdr); // closed system, cast ok + // can't call GetFlags, because it will be recursive. + uint32_t flags; + hdr->GetRawFlags(&flags); + *pRead = !!(flags & nsMsgMessageFlags::Read); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::IsMarked(nsMsgKey key, bool* pMarked) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + + GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + uint32_t flags; + (void)msgHdr->GetFlags(&flags); + *pMarked = !!(flags & nsMsgMessageFlags::Marked); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::IsIgnored(nsMsgKey key, bool* pIgnored) { + NS_ENSURE_ARG_POINTER(pIgnored); + + nsCOMPtr<nsIMsgThread> threadHdr; + + nsresult rv = GetThreadForMsgKey(key, getter_AddRefs(threadHdr)); + // This should be very surprising, but we leave that up to the caller + // to determine for now. + if (!threadHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + uint32_t threadFlags; + threadHdr->GetFlags(&threadFlags); + *pIgnored = !!(threadFlags & nsMsgMessageFlags::Ignored); + return rv; +} + +NS_IMETHODIMP nsMsgDatabase::IsWatched(nsMsgKey key, bool* pWatched) { + NS_ENSURE_ARG_POINTER(pWatched); + + nsCOMPtr<nsIMsgThread> threadHdr; + + nsresult rv = GetThreadForMsgKey(key, getter_AddRefs(threadHdr)); + // This should be very surprising, but we leave that up to the caller + // to determine for now. + if (!threadHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + uint32_t threadFlags; + threadHdr->GetFlags(&threadFlags); + *pWatched = !!(threadFlags & nsMsgMessageFlags::Watched); + return rv; +} + +nsresult nsMsgDatabase::HasAttachments(nsMsgKey key, bool* pHasThem) { + NS_ENSURE_ARG_POINTER(pHasThem); + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + + GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + uint32_t flags; + (void)msgHdr->GetFlags(&flags); + *pHasThem = !!(flags & nsMsgMessageFlags::Attachment); + return NS_OK; +} + +bool nsMsgDatabase::SetHdrReadFlag(nsIMsgDBHdr* msgHdr, bool bRead) { + return SetHdrFlag(msgHdr, bRead, nsMsgMessageFlags::Read); +} + +nsresult nsMsgDatabase::MarkHdrReadInDB(nsIMsgDBHdr* msgHdr, bool bRead, + nsIDBChangeListener* instigator) { + nsresult rv; + nsMsgKey key; + uint32_t oldFlags; + bool hdrInDB; + (void)msgHdr->GetMessageKey(&key); + msgHdr->GetFlags(&oldFlags); + + m_newSet.RemoveElement(key); + (void)ContainsKey(key, &hdrInDB); + if (hdrInDB && m_dbFolderInfo) { + if (bRead) + m_dbFolderInfo->ChangeNumUnreadMessages(-1); + else + m_dbFolderInfo->ChangeNumUnreadMessages(1); + } + + SetHdrReadFlag(msgHdr, bRead); // this will cause a commit, at least for + // local mail, so do it after we change + // the folder counts above, so they will get committed too. + uint32_t flags; + rv = msgHdr->GetFlags(&flags); + flags &= ~nsMsgMessageFlags::New; + msgHdr->SetFlags(flags); + if (NS_FAILED(rv)) return rv; + + if (oldFlags == flags) return NS_OK; + + return NotifyHdrChangeAll(msgHdr, oldFlags, flags, instigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkRead(nsMsgKey key, bool bRead, + nsIDBChangeListener* instigator) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + + GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + return MarkHdrRead(msgHdr, bRead, instigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkReplied( + nsMsgKey key, bool bReplied, nsIDBChangeListener* instigator /* = NULL */) { + return SetKeyFlag(key, bReplied, nsMsgMessageFlags::Replied, instigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkForwarded( + nsMsgKey key, bool bForwarded, + nsIDBChangeListener* instigator /* = NULL */) { + return SetKeyFlag(key, bForwarded, nsMsgMessageFlags::Forwarded, instigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkRedirected( + nsMsgKey key, bool bRedirected, + nsIDBChangeListener* instigator /* = NULL */) { + return SetKeyFlag(key, bRedirected, nsMsgMessageFlags::Redirected, + instigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkHasAttachments( + nsMsgKey key, bool bHasAttachments, nsIDBChangeListener* instigator) { + return SetKeyFlag(key, bHasAttachments, nsMsgMessageFlags::Attachment, + instigator); +} + +NS_IMETHODIMP +nsMsgDatabase::MarkThreadRead(nsIMsgThread* thread, + nsIDBChangeListener* instigator, + nsTArray<nsMsgKey>& aThoseMarkedRead) { + NS_ENSURE_ARG_POINTER(thread); + aThoseMarkedRead.ClearAndRetainStorage(); + nsresult rv = NS_OK; + + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + aThoseMarkedRead.SetCapacity(numChildren); + for (uint32_t curChildIndex = 0; curChildIndex < numChildren; + curChildIndex++) { + nsCOMPtr<nsIMsgDBHdr> child; + + rv = thread->GetChildHdrAt(curChildIndex, getter_AddRefs(child)); + if (NS_SUCCEEDED(rv) && child) { + bool isRead = true; + IsHeaderRead(child, &isRead); + if (!isRead) { + nsMsgKey key; + if (NS_SUCCEEDED(child->GetMessageKey(&key))) + aThoseMarkedRead.AppendElement(key); + MarkHdrRead(child, true, instigator); + } + } + } + return rv; +} + +NS_IMETHODIMP +nsMsgDatabase::MarkThreadIgnored(nsIMsgThread* thread, nsMsgKey threadKey, + bool bIgnored, + nsIDBChangeListener* instigator) { + NS_ENSURE_ARG(thread); + uint32_t threadFlags; + thread->GetFlags(&threadFlags); + uint32_t oldThreadFlags = + threadFlags; // not quite right, since we probably want msg hdr flags. + if (bIgnored) { + threadFlags |= nsMsgMessageFlags::Ignored; + threadFlags &= ~nsMsgMessageFlags::Watched; // ignore is implicit un-watch + } else + threadFlags &= ~nsMsgMessageFlags::Ignored; + thread->SetFlags(threadFlags); + + nsCOMPtr<nsIMsgDBHdr> msg; + GetMsgHdrForKey(threadKey, getter_AddRefs(msg)); + NS_ENSURE_TRUE(msg, NS_MSG_MESSAGE_NOT_FOUND); + + // We'll add the message flags to the thread flags when notifying, since + // notifications are supposed to be about messages, not threads. + uint32_t msgFlags; + msg->GetFlags(&msgFlags); + + return NotifyHdrChangeAll(msg, oldThreadFlags | msgFlags, + threadFlags | msgFlags, instigator); +} + +NS_IMETHODIMP +nsMsgDatabase::MarkHeaderKilled(nsIMsgDBHdr* msg, bool bIgnored, + nsIDBChangeListener* instigator) { + uint32_t msgFlags; + msg->GetFlags(&msgFlags); + uint32_t oldFlags = msgFlags; + if (bIgnored) + msgFlags |= nsMsgMessageFlags::Ignored; + else + msgFlags &= ~nsMsgMessageFlags::Ignored; + msg->SetFlags(msgFlags); + + return NotifyHdrChangeAll(msg, oldFlags, msgFlags, instigator); +} + +NS_IMETHODIMP +nsMsgDatabase::MarkThreadWatched(nsIMsgThread* thread, nsMsgKey threadKey, + bool bWatched, + nsIDBChangeListener* instigator) { + NS_ENSURE_ARG(thread); + uint32_t threadFlags; + thread->GetFlags(&threadFlags); + uint32_t oldThreadFlags = + threadFlags; // not quite right, since we probably want msg hdr flags. + if (bWatched) { + threadFlags |= nsMsgMessageFlags::Watched; + threadFlags &= ~nsMsgMessageFlags::Ignored; // watch is implicit un-ignore + } else + threadFlags &= ~nsMsgMessageFlags::Watched; + thread->SetFlags(threadFlags); + + nsCOMPtr<nsIMsgDBHdr> msg; + GetMsgHdrForKey(threadKey, getter_AddRefs(msg)); + if (!msg) return NS_MSG_MESSAGE_NOT_FOUND; + + // We'll add the message flags to the thread flags when notifying, since + // notifications are supposed to be about messages, not threads. + uint32_t msgFlags; + msg->GetFlags(&msgFlags); + + return NotifyHdrChangeAll(msg, oldThreadFlags | msgFlags, + threadFlags | msgFlags, instigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkMarked(nsMsgKey key, bool mark, + nsIDBChangeListener* instigator) { + return SetKeyFlag(key, mark, nsMsgMessageFlags::Marked, instigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkOffline(nsMsgKey key, bool offline, + nsIDBChangeListener* instigator) { + return SetKeyFlag(key, offline, nsMsgMessageFlags::Offline, instigator); +} + +NS_IMETHODIMP nsMsgDatabase::SetStringProperty(nsMsgKey aKey, + const char* aProperty, + const nsACString& aValue) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetMsgHdrForKey(aKey, getter_AddRefs(msgHdr)); + if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND; + return SetStringPropertyByHdr(msgHdr, aProperty, aValue); +} + +NS_IMETHODIMP nsMsgDatabase::SetStringPropertyByHdr(nsIMsgDBHdr* msgHdr, + const char* aProperty, + const nsACString& aValue) { + // don't do notifications if message not yet added to database. + // Ignore errors (consequences of failure are minor). + bool notify = true; + nsMsgKey key = nsMsgKey_None; + msgHdr->GetMessageKey(&key); + ContainsKey(key, ¬ify); + + nsCString oldValue; + nsresult rv = msgHdr->GetStringProperty(aProperty, oldValue); + NS_ENSURE_SUCCESS(rv, rv); + + // if no change to this string property, bail out + if (oldValue.Equals(aValue)) return NS_OK; + + // Precall OnHdrPropertyChanged to store prechange status + nsTArray<uint32_t> statusArray(m_ChangeListeners.Length()); + nsCOMPtr<nsIDBChangeListener> listener; + if (notify) { + nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator listeners( + m_ChangeListeners); + while (listeners.HasMore()) { + listener = listeners.GetNext(); + // initialize |status| because some implementations of + // OnHdrPropertyChanged does not set the value. + uint32_t status = 0; + (void)listener->OnHdrPropertyChanged(msgHdr, nsCString(aProperty), true, + &status, nullptr); + // ignore errors, but append element to keep arrays in sync + statusArray.AppendElement(status); + } + } + + rv = msgHdr->SetStringProperty(aProperty, aValue); + NS_ENSURE_SUCCESS(rv, rv); + + // Postcall OnHdrPropertyChanged to process the change + if (notify) { + // if this is the junk score property notify, as long as we're not going + // from no value to non junk + if (!strcmp(aProperty, "junkscore") && + !(oldValue.IsEmpty() && aValue.Equals("0"))) + NotifyJunkScoreChanged(nullptr); + + nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator listeners( + m_ChangeListeners); + for (uint32_t i = 0; listeners.HasMore() && i < statusArray.Length(); i++) { + listener = listeners.GetNext(); + uint32_t status = statusArray[i]; + (void)listener->OnHdrPropertyChanged(msgHdr, nsCString(aProperty), false, + &status, nullptr); + // ignore errors + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDatabase::SetUint32PropertyByHdr(nsIMsgDBHdr* aMsgHdr, + const char* aProperty, uint32_t aValue) { + // If no change to this property, bail out. + uint32_t oldValue; + nsresult rv = aMsgHdr->GetUint32Property(aProperty, &oldValue); + NS_ENSURE_SUCCESS(rv, rv); + if (oldValue == aValue) return NS_OK; + + // Don't do notifications if message not yet added to database. + bool notify = true; + nsMsgKey key = nsMsgKey_None; + aMsgHdr->GetMessageKey(&key); + ContainsKey(key, ¬ify); + + // Precall OnHdrPropertyChanged to store prechange status. + nsTArray<uint32_t> statusArray(m_ChangeListeners.Length()); + nsCOMPtr<nsIDBChangeListener> listener; + if (notify) { + nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator listeners( + m_ChangeListeners); + while (listeners.HasMore()) { + listener = listeners.GetNext(); + // initialize |status| because some implementations of + // OnHdrPropertyChanged does not set the value. + uint32_t status = 0; + (void)listener->OnHdrPropertyChanged(aMsgHdr, nsCString(aProperty), true, + &status, nullptr); + // Ignore errors, but append element to keep arrays in sync. + statusArray.AppendElement(status); + } + } + + rv = aMsgHdr->SetUint32Property(aProperty, aValue); + NS_ENSURE_SUCCESS(rv, rv); + + // Postcall OnHdrPropertyChanged to process the change. + if (notify) { + nsTObserverArray<nsCOMPtr<nsIDBChangeListener>>::ForwardIterator listeners( + m_ChangeListeners); + for (uint32_t i = 0; listeners.HasMore(); i++) { + listener = listeners.GetNext(); + uint32_t status = statusArray[i]; + (void)listener->OnHdrPropertyChanged(aMsgHdr, nsCString(aProperty), false, + &status, nullptr); + // Ignore errors. + } + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::MarkImapDeleted(nsMsgKey key, bool deleted, + nsIDBChangeListener* instigator) { + return SetKeyFlag(key, deleted, nsMsgMessageFlags::IMAPDeleted, instigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkMDNNeeded( + nsMsgKey key, bool bNeeded, nsIDBChangeListener* instigator /* = NULL */) { + return SetKeyFlag(key, bNeeded, nsMsgMessageFlags::MDNReportNeeded, + instigator); +} + +nsresult nsMsgDatabase::MarkMDNSent( + nsMsgKey key, bool bSent, nsIDBChangeListener* instigator /* = NULL */) { + return SetKeyFlag(key, bSent, nsMsgMessageFlags::MDNReportSent, instigator); +} + +nsresult nsMsgDatabase::IsMDNSent(nsMsgKey key, bool* pSent) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + + GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + uint32_t flags; + (void)msgHdr->GetFlags(&flags); + *pSent = !!(flags & nsMsgMessageFlags::MDNReportSent); + return NS_OK; +} + +nsresult nsMsgDatabase::SetKeyFlag(nsMsgKey key, bool set, + nsMsgMessageFlagType flag, + nsIDBChangeListener* instigator) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + + GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (!msgHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + return SetMsgHdrFlag(msgHdr, set, flag, instigator); +} + +nsresult nsMsgDatabase::SetMsgHdrFlag(nsIMsgDBHdr* msgHdr, bool set, + nsMsgMessageFlagType flag, + nsIDBChangeListener* instigator) { + uint32_t oldFlags; + (void)msgHdr->GetFlags(&oldFlags); + + if (!SetHdrFlag(msgHdr, set, flag)) return NS_OK; + + uint32_t flags; + (void)msgHdr->GetFlags(&flags); + + return NotifyHdrChangeAll(msgHdr, oldFlags, flags, instigator); +} + +// Helper routine - lowest level of flag setting - returns true if flags change, +// false otherwise. +bool nsMsgDatabase::SetHdrFlag(nsIMsgDBHdr* msgHdr, bool bSet, + nsMsgMessageFlagType flag) { + uint32_t statusFlags; + (void)msgHdr->GetFlags(&statusFlags); + uint32_t currentStatusFlags = GetStatusFlags(msgHdr, statusFlags); + bool flagAlreadySet = (currentStatusFlags & flag) != 0; + + if ((flagAlreadySet && !bSet) || (!flagAlreadySet && bSet)) { + uint32_t resultFlags; + if (bSet) + msgHdr->OrFlags(flag, &resultFlags); + else + msgHdr->AndFlags(~flag, &resultFlags); + return true; + } + return false; +} + +NS_IMETHODIMP nsMsgDatabase::MarkHdrRead(nsIMsgDBHdr* msgHdr, bool bRead, + nsIDBChangeListener* instigator) { + bool isReadInDB = true; + nsresult rv = nsMsgDatabase::IsHeaderRead(msgHdr, &isReadInDB); + NS_ENSURE_SUCCESS(rv, rv); + + bool isRead = true; + rv = IsHeaderRead(msgHdr, &isRead); + NS_ENSURE_SUCCESS(rv, rv); + + // if the flag is already correct in the db, don't change it. + // Check msg flags as well as IsHeaderRead in case it's a newsgroup + // and the msghdr flags are out of sync with the newsrc settings. + // (we could override this method for news db's, but it's a trivial fix here. + if (bRead != isRead || isRead != isReadInDB) { + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + + bool inDB = false; + (void)ContainsKey(msgKey, &inDB); + + if (inDB) { + nsCOMPtr<nsIMsgThread> threadHdr; + rv = GetThreadForMsgKey(msgKey, getter_AddRefs(threadHdr)); + if (threadHdr) threadHdr->MarkChildRead(bRead); + } + +#ifndef MOZ_SUITE + if (bRead) { + Telemetry::ScalarAdd(Telemetry::ScalarID::TB_MAILS_READ, 1); + } +#endif + + return MarkHdrReadInDB(msgHdr, bRead, instigator); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::MarkHdrReplied(nsIMsgDBHdr* msgHdr, bool bReplied, + nsIDBChangeListener* instigator) { + return SetMsgHdrFlag(msgHdr, bReplied, nsMsgMessageFlags::Replied, + instigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkHdrMarked(nsIMsgDBHdr* msgHdr, bool mark, + nsIDBChangeListener* instigator) { + return SetMsgHdrFlag(msgHdr, mark, nsMsgMessageFlags::Marked, instigator); +} + +NS_IMETHODIMP +nsMsgDatabase::MarkHdrNotNew(nsIMsgDBHdr* aMsgHdr, + nsIDBChangeListener* aInstigator) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + nsMsgKey msgKey; + aMsgHdr->GetMessageKey(&msgKey); + m_newSet.RemoveElement(msgKey); + return SetMsgHdrFlag(aMsgHdr, false, nsMsgMessageFlags::New, aInstigator); +} + +NS_IMETHODIMP nsMsgDatabase::MarkAllRead(nsTArray<nsMsgKey>& aThoseMarked) { + aThoseMarked.ClearAndRetainStorage(); + + nsCOMPtr<nsIMsgEnumerator> hdrs; + nsresult rv = EnumerateMessages(getter_AddRefs(hdrs)); + NS_ENSURE_SUCCESS(rv, rv); + bool hasMore = false; + + while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> msg; + rv = hdrs->GetNext(getter_AddRefs(msg)); + if (NS_FAILED(rv)) break; + + bool isRead; + IsHeaderRead(msg, &isRead); + + if (!isRead) { + nsMsgKey key; + (void)msg->GetMessageKey(&key); + aThoseMarked.AppendElement(key); + rv = MarkHdrRead(msg, true, nullptr); // ### dmb - blow off error? + } + } + + // force num new to 0. + int32_t numUnreadMessages; + + rv = m_dbFolderInfo->GetNumUnreadMessages(&numUnreadMessages); + if (NS_SUCCEEDED(rv)) + m_dbFolderInfo->ChangeNumUnreadMessages(-numUnreadMessages); + // caller will Commit the db, so no need to do it here. + return rv; +} + +NS_IMETHODIMP nsMsgDatabase::AddToNewList(nsMsgKey key) { + // we add new keys in increasing order... + if (m_newSet.IsEmpty() || (m_newSet[m_newSet.Length() - 1] < key)) + m_newSet.AppendElement(key); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::ClearNewList(bool notify /* = FALSE */) { + if (notify && !m_newSet.IsEmpty()) // need to update view + { + nsTArray<nsMsgKey> saveNewSet; + // clear m_newSet so that the code that's listening to the key change + // doesn't think we have new messages and send notifications all over + // that we have new messages. + saveNewSet.SwapElements(m_newSet); + for (uint32_t elementIndex = saveNewSet.Length() - 1;; elementIndex--) { + nsMsgKey lastNewKey = saveNewSet.ElementAt(elementIndex); + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetMsgHdrForKey(lastNewKey, getter_AddRefs(msgHdr)); + if (msgHdr) { + uint32_t flags; + (void)msgHdr->GetFlags(&flags); + + if ((flags | nsMsgMessageFlags::New) != flags) { + msgHdr->AndFlags(~nsMsgMessageFlags::New, &flags); + NotifyHdrChangeAll(msgHdr, flags | nsMsgMessageFlags::New, flags, + nullptr); + } + } + if (elementIndex == 0) break; + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::HasNew(bool* _retval) { + if (!_retval) return NS_ERROR_NULL_POINTER; + + *_retval = (m_newSet.Length() > 0); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetFirstNew(nsMsgKey* result) { + bool hasnew; + nsresult rv = HasNew(&hasnew); + if (NS_FAILED(rv)) return rv; + *result = (hasnew) ? m_newSet.ElementAt(0) : nsMsgKey_None; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDatabase::EnumerateMessages(nsIMsgEnumerator** result) { + RememberLastUseTime(); + NS_ENSURE_ARG_POINTER(result); + NS_ADDREF(*result = new nsMsgDBEnumerator(this, m_mdbAllMsgHeadersTable, + nullptr, nullptr)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDatabase::ReverseEnumerateMessages(nsIMsgEnumerator** result) { + NS_ENSURE_ARG_POINTER(result); + NS_ADDREF(*result = new nsMsgDBEnumerator(this, m_mdbAllMsgHeadersTable, + nullptr, nullptr, false)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDatabase::GetFilterEnumerator( + const nsTArray<RefPtr<nsIMsgSearchTerm>>& searchTerms, bool aReverse, + nsIMsgEnumerator** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + RefPtr<nsMsgFilteredDBEnumerator> e = + new nsMsgFilteredDBEnumerator(this, m_mdbAllMsgHeadersTable, aReverse); + + NS_ENSURE_TRUE(e, NS_ERROR_OUT_OF_MEMORY); + nsresult rv = e->InitSearchSession(searchTerms, m_folder); + NS_ENSURE_SUCCESS(rv, rv); + + e.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDatabase::SyncCounts() { + nsCOMPtr<nsIMsgEnumerator> hdrs; + nsresult rv = EnumerateMessages(getter_AddRefs(hdrs)); + if (NS_FAILED(rv)) return rv; + bool hasMore = false; + + mdb_count numHdrsInTable = 0; + int32_t numUnread = 0; + int32_t numHdrs = 0; + + if (m_mdbAllMsgHeadersTable) + m_mdbAllMsgHeadersTable->GetCount(GetEnv(), &numHdrsInTable); + else + return NS_ERROR_NULL_POINTER; + + while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> header; + rv = hdrs->GetNext(getter_AddRefs(header)); + NS_ASSERTION(NS_SUCCEEDED(rv), "nsMsgDBEnumerator broken"); + if (NS_FAILED(rv)) break; + + bool isRead; + IsHeaderRead(header, &isRead); + if (!isRead) numUnread++; + numHdrs++; + } + + int32_t oldTotal, oldUnread; + (void)m_dbFolderInfo->GetNumUnreadMessages(&oldUnread); + (void)m_dbFolderInfo->GetNumMessages(&oldTotal); + if (oldUnread != numUnread) + m_dbFolderInfo->ChangeNumUnreadMessages(numUnread - oldUnread); + if (oldTotal != numHdrs) + m_dbFolderInfo->ChangeNumMessages(numHdrs - oldTotal); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::ListAllKeys(nsTArray<nsMsgKey>& keys) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMdbTableRowCursor> rowCursor; + RememberLastUseTime(); + keys.Clear(); + + if (m_mdbAllMsgHeadersTable) { + uint32_t numMsgs = 0; + m_mdbAllMsgHeadersTable->GetCount(GetEnv(), &numMsgs); + keys.SetCapacity(numMsgs); + rv = m_mdbAllMsgHeadersTable->GetTableRowCursor(GetEnv(), -1, + getter_AddRefs(rowCursor)); + while (NS_SUCCEEDED(rv) && rowCursor) { + mdbOid outOid; + mdb_pos outPos; + + rv = rowCursor->NextRowOid(GetEnv(), &outOid, &outPos); + // is this right? Mork is returning a 0 id, but that should valid. + if (outPos < 0 || outOid.mOid_Id == (mdb_id)-1) break; + if (NS_SUCCEEDED(rv)) keys.AppendElement(outOid.mOid_Id); + } + } + return rv; +} + +NS_IMETHODIMP +nsMsgDatabase::EnumerateThreads(nsIMsgThreadEnumerator** result) { + RememberLastUseTime(); + NS_ADDREF(*result = new nsMsgDBThreadEnumerator(this, nullptr)); + return NS_OK; +} + +// only return headers with a particular flag set +static nsresult nsMsgFlagSetFilter(nsIMsgDBHdr* msg, void* closure) { + uint32_t msgFlags, desiredFlags; + desiredFlags = *(uint32_t*)closure; + msg->GetFlags(&msgFlags); + return (msgFlags & desiredFlags) ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult nsMsgDatabase::EnumerateMessagesWithFlag(nsIMsgEnumerator** result, + uint32_t* pFlag) { + RememberLastUseTime(); + NS_ADDREF(*result = new nsMsgDBEnumerator(this, m_mdbAllMsgHeadersTable, + nsMsgFlagSetFilter, pFlag)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::CreateNewHdr(nsMsgKey key, nsIMsgDBHdr** pnewHdr) { + nsresult err = NS_OK; + nsIMdbRow* hdrRow = nullptr; + struct mdbOid allMsgHdrsTableOID; + + if (!pnewHdr || !m_mdbAllMsgHeadersTable || !m_mdbStore) + return NS_ERROR_NULL_POINTER; + + if (key != nsMsgKey_None) { + allMsgHdrsTableOID.mOid_Scope = m_hdrRowScopeToken; + allMsgHdrsTableOID.mOid_Id = key; // presumes 0 is valid key value + + err = m_mdbStore->GetRow(GetEnv(), &allMsgHdrsTableOID, &hdrRow); + if (!hdrRow) + err = m_mdbStore->NewRowWithOid(GetEnv(), &allMsgHdrsTableOID, &hdrRow); + } else { + // Mork will assign an ID to the new row, generally the next available ID. + err = m_mdbStore->NewRow(GetEnv(), m_hdrRowScopeToken, &hdrRow); + if (hdrRow) { + struct mdbOid oid; + hdrRow->GetOid(GetEnv(), &oid); + key = oid.mOid_Id; + } else { + // We failed to create a new row. That can happen if we run out of keys, + // which will force a reparse. + nsTArray<nsMsgKey> keys; + if (NS_SUCCEEDED(ListAllKeys(keys))) { + for (nsMsgKey key : keys) { + if (key >= kForceReparseKey) { + // Force a reparse. + if (m_dbFolderInfo) + m_dbFolderInfo->SetBooleanProperty("forceReparse", true); + break; + } + } + } + err = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } + } + if (NS_FAILED(err)) return err; + err = CreateMsgHdr(hdrRow, key, pnewHdr); + return err; +} + +NS_IMETHODIMP nsMsgDatabase::AddNewHdrToDB(nsIMsgDBHdr* newHdr, bool notify) { + NS_ENSURE_ARG_POINTER(newHdr); + nsMsgHdr* hdr = static_cast<nsMsgHdr*>(newHdr); // closed system, cast ok + bool newThread; + bool hasKey = false; + nsMsgKey msgKey = nsMsgKey_None; + (void)hdr->GetMessageKey(&msgKey); + (void)ContainsKey(msgKey, &hasKey); + if (hasKey) { + NS_ERROR("adding hdr that already exists"); + return NS_ERROR_FAILURE; + } + nsresult err = ThreadNewHdr(hdr, newThread); + // we thread header before we add it to the all headers table + // so that subject and reference threading will work (otherwise, + // when we try to find the first header with the same subject or + // reference, we get the new header!) + if (NS_SUCCEEDED(err)) { + nsMsgKey key; + uint32_t flags; + + newHdr->GetMessageKey(&key); + hdr->GetRawFlags(&flags); + // use raw flags instead of GetFlags, because GetFlags will + // pay attention to what's in m_newSet, and this new hdr isn't + // in m_newSet yet. + if (flags & nsMsgMessageFlags::New) { + uint32_t newFlags; + newHdr->AndFlags(~nsMsgMessageFlags::New, + &newFlags); // make sure not filed out + AddToNewList(key); + } + if (m_dbFolderInfo) { + m_dbFolderInfo->ChangeNumMessages(1); + bool isRead = true; + IsHeaderRead(newHdr, &isRead); + if (!isRead) m_dbFolderInfo->ChangeNumUnreadMessages(1); + m_dbFolderInfo->OnKeyAdded(key); + } + + err = m_mdbAllMsgHeadersTable->AddRow(GetEnv(), hdr->GetMDBRow()); + if (notify) { + nsMsgKey threadParent; + + newHdr->GetThreadParent(&threadParent); + NotifyHdrAddedAll(newHdr, threadParent, flags, NULL); + } + + if (UseCorrectThreading()) err = AddMsgRefsToHash(newHdr); + } + NS_ASSERTION(NS_SUCCEEDED(err), "error creating thread"); + return err; +} + +NS_IMETHODIMP nsMsgDatabase::CopyHdrFromExistingHdr(nsMsgKey key, + nsIMsgDBHdr* existingHdr, + bool addHdrToDB, + nsIMsgDBHdr** newHdr) { + nsresult err = NS_OK; + + if (existingHdr) { + nsMsgHdr* sourceMsgHdr = + static_cast<nsMsgHdr*>(existingHdr); // closed system, cast ok + nsMsgHdr* destMsgHdr = nullptr; + CreateNewHdr(key, (nsIMsgDBHdr**)&destMsgHdr); + nsIMdbRow* sourceRow = sourceMsgHdr->GetMDBRow(); + if (!destMsgHdr || !sourceRow) return NS_MSG_MESSAGE_NOT_FOUND; + + nsIMdbRow* destRow = destMsgHdr->GetMDBRow(); + if (!destRow) return NS_ERROR_UNEXPECTED; + + err = destRow->SetRow(GetEnv(), sourceRow); + if (NS_SUCCEEDED(err)) { + // we may have gotten the header from a cache - calling SetRow + // basically invalidates any cached values, so invalidate them. + destMsgHdr->ClearCachedValues(); + if (addHdrToDB) err = AddNewHdrToDB(destMsgHdr, true); + if (NS_SUCCEEDED(err) && newHdr) *newHdr = destMsgHdr; + } + } + return err; +} + +nsresult nsMsgDatabase::RowCellColumnTonsString(nsIMdbRow* hdrRow, + mdb_token columnToken, + nsAString& resultStr) { + NS_ENSURE_ARG_POINTER(hdrRow); + + struct mdbYarn yarn; + nsresult rv = hdrRow->AliasCellYarn(GetEnv(), columnToken, &yarn); + NS_ENSURE_SUCCESS(rv, rv); + YarnTonsString(&yarn, resultStr); + return NS_OK; +} + +// as long as the row still exists, and isn't changed, the returned const char +// ** will be valid. But be very careful using this data - the caller should +// never return it in turn to another caller. +nsresult nsMsgDatabase::RowCellColumnToConstCharPtr(nsIMdbRow* hdrRow, + mdb_token columnToken, + const char** ptr) { + NS_ENSURE_ARG_POINTER(hdrRow); + + struct mdbYarn yarn; + nsresult rv = hdrRow->AliasCellYarn(GetEnv(), columnToken, &yarn); + NS_ENSURE_SUCCESS(rv, rv); + *ptr = (const char*)yarn.mYarn_Buf; + return NS_OK; +} + +nsIMimeConverter* nsMsgDatabase::GetMimeConverter() { + if (!m_mimeConverter) { + // apply mime decode + m_mimeConverter = do_GetService("@mozilla.org/messenger/mimeconverter;1"); + } + return m_mimeConverter; +} + +nsresult nsMsgDatabase::GetEffectiveCharset(nsIMdbRow* row, + nsACString& resultCharset) { + resultCharset.Truncate(); + nsresult rv = RowCellColumnToCharPtr(row, m_messageCharSetColumnToken, + getter_Copies(resultCharset)); + if (NS_FAILED(rv) || resultCharset.IsEmpty() || + resultCharset.EqualsLiteral("us-ascii")) { + resultCharset.AssignLiteral("UTF-8"); + nsCOMPtr<nsIMsgNewsFolder> newsfolder(do_QueryInterface(m_folder)); + if (newsfolder) newsfolder->GetCharset(resultCharset); + } + return rv; +} + +nsresult nsMsgDatabase::RowCellColumnToMime2DecodedString( + nsIMdbRow* row, mdb_token columnToken, nsAString& resultStr) { + nsresult err = NS_OK; + const char* nakedString = nullptr; + err = RowCellColumnToConstCharPtr(row, columnToken, &nakedString); + if (NS_SUCCEEDED(err) && nakedString && strlen(nakedString)) { + GetMimeConverter(); + if (m_mimeConverter) { + nsAutoString decodedStr; + nsCString charSet; + GetEffectiveCharset(row, charSet); + + err = m_mimeConverter->DecodeMimeHeader(nakedString, charSet.get(), false, + true, resultStr); + } + } + return err; +} + +nsresult nsMsgDatabase::RowCellColumnToAddressCollationKey( + nsIMdbRow* row, mdb_token colToken, nsTArray<uint8_t>& result) { + nsString sender; + nsresult rv = RowCellColumnToMime2DecodedString(row, colToken, sender); + NS_ENSURE_SUCCESS(rv, rv); + + nsString name; + ExtractName(DecodedHeader(sender), name); + return CreateCollationKey(name, result); +} + +nsresult nsMsgDatabase::GetCollationKeyGenerator() { + if (!m_collationKeyGenerator) { + auto result = mozilla::intl::LocaleService::TryCreateComponent<Collator>(); + if (result.isErr()) { + NS_WARNING("Could not create mozilla::intl::Collation."); + return NS_ERROR_FAILURE; + } + + m_collationKeyGenerator = 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 = m_collationKeyGenerator->SetOptions(options); + + if (optResult.isErr()) { + NS_WARNING("Could not configure the mozilla::intl::Collation."); + m_collationKeyGenerator = nullptr; + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} + +nsresult nsMsgDatabase::RowCellColumnToCollationKey(nsIMdbRow* row, + mdb_token columnToken, + nsTArray<uint8_t>& result) { + const char* nakedString = nullptr; + nsresult err; + + err = RowCellColumnToConstCharPtr(row, columnToken, &nakedString); + if (!nakedString) nakedString = ""; + if (NS_SUCCEEDED(err)) { + GetMimeConverter(); + if (m_mimeConverter) { + nsCString decodedStr; + nsCString charSet; + GetEffectiveCharset(row, charSet); + + err = m_mimeConverter->DecodeMimeHeaderToUTF8( + nsDependentCString(nakedString), charSet.get(), false, true, + decodedStr); + if (NS_SUCCEEDED(err)) + err = CreateCollationKey(NS_ConvertUTF8toUTF16(decodedStr), result); + } + } + return err; +} + +NS_IMETHODIMP +nsMsgDatabase::CompareCollationKeys(const nsTArray<uint8_t>& key1, + const nsTArray<uint8_t>& key2, + int32_t* result) { + nsresult rv = GetCollationKeyGenerator(); + NS_ENSURE_SUCCESS(rv, rv); + if (!m_collationKeyGenerator) return NS_ERROR_FAILURE; + + *result = m_collationKeyGenerator->CompareSortKeys(key1, key2); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDatabase::CreateCollationKey(const nsAString& sourceString, + nsTArray<uint8_t>& key) { + nsresult err = GetCollationKeyGenerator(); + NS_ENSURE_SUCCESS(err, err); + if (!m_collationKeyGenerator) return NS_ERROR_FAILURE; + + nsTArrayU8Buffer buffer(key); + + auto result = m_collationKeyGenerator->GetSortKey(sourceString, buffer); + NS_ENSURE_TRUE(result.isOk(), NS_ERROR_FAILURE); + + return NS_OK; +} + +nsresult nsMsgDatabase::RowCellColumnToUInt32(nsIMdbRow* hdrRow, + mdb_token columnToken, + uint32_t& uint32Result, + uint32_t defaultValue) { + return RowCellColumnToUInt32(hdrRow, columnToken, &uint32Result, + defaultValue); +} + +nsresult nsMsgDatabase::RowCellColumnToUInt32(nsIMdbRow* hdrRow, + mdb_token columnToken, + uint32_t* uint32Result, + uint32_t defaultValue) { + nsresult err = NS_OK; + + if (uint32Result) *uint32Result = defaultValue; + if (hdrRow) // ### probably should be an error if hdrRow is NULL... + { + struct mdbYarn yarn; + err = hdrRow->AliasCellYarn(GetEnv(), columnToken, &yarn); + if (NS_SUCCEEDED(err)) YarnToUInt32(&yarn, uint32Result); + } + return err; +} + +nsresult nsMsgDatabase::UInt32ToRowCellColumn(nsIMdbRow* row, + mdb_token columnToken, + uint32_t value) { + struct mdbYarn yarn; + char yarnBuf[100]; + + if (!row) return NS_ERROR_NULL_POINTER; + + yarn.mYarn_Buf = (void*)yarnBuf; + yarn.mYarn_Size = sizeof(yarnBuf); + yarn.mYarn_Fill = yarn.mYarn_Size; + yarn.mYarn_Form = 0; + yarn.mYarn_Grow = NULL; + return row->AddColumn(GetEnv(), columnToken, UInt32ToYarn(&yarn, value)); +} + +nsresult nsMsgDatabase::UInt64ToRowCellColumn(nsIMdbRow* row, + mdb_token columnToken, + uint64_t value) { + NS_ENSURE_ARG_POINTER(row); + struct mdbYarn yarn; + char yarnBuf[17]; // max string is 16 bytes, + 1 for null. + + yarn.mYarn_Buf = (void*)yarnBuf; + yarn.mYarn_Size = sizeof(yarnBuf); + yarn.mYarn_Form = 0; + yarn.mYarn_Grow = NULL; + PR_snprintf((char*)yarn.mYarn_Buf, yarn.mYarn_Size, "%llx", value); + yarn.mYarn_Fill = PL_strlen((const char*)yarn.mYarn_Buf); + return row->AddColumn(GetEnv(), columnToken, &yarn); +} + +nsresult nsMsgDatabase::RowCellColumnToUInt64(nsIMdbRow* hdrRow, + mdb_token columnToken, + uint64_t* uint64Result, + uint64_t defaultValue) { + nsresult err = NS_OK; + + if (uint64Result) *uint64Result = defaultValue; + if (hdrRow) // ### probably should be an error if hdrRow is NULL... + { + struct mdbYarn yarn; + err = hdrRow->AliasCellYarn(GetEnv(), columnToken, &yarn); + if (NS_SUCCEEDED(err)) YarnToUInt64(&yarn, uint64Result); + } + return err; +} + +nsresult nsMsgDatabase::CharPtrToRowCellColumn(nsIMdbRow* row, + mdb_token columnToken, + const char* charPtr) { + if (!row) return NS_ERROR_NULL_POINTER; + + struct mdbYarn yarn; + yarn.mYarn_Buf = (void*)charPtr; + yarn.mYarn_Size = PL_strlen((const char*)yarn.mYarn_Buf) + 1; + yarn.mYarn_Fill = yarn.mYarn_Size - 1; + yarn.mYarn_Form = + 0; // what to do with this? we're storing csid in the msg hdr... + + return row->AddColumn(GetEnv(), columnToken, &yarn); +} + +// caller must free result +nsresult nsMsgDatabase::RowCellColumnToCharPtr(nsIMdbRow* row, + mdb_token columnToken, + char** result) { + nsresult err = NS_ERROR_NULL_POINTER; + + if (row && result) { + struct mdbYarn yarn; + err = row->AliasCellYarn(GetEnv(), columnToken, &yarn); + if (NS_SUCCEEDED(err)) { + *result = (char*)moz_xmalloc(yarn.mYarn_Fill + 1); + if (*result) { + if (yarn.mYarn_Fill > 0) + memcpy(*result, yarn.mYarn_Buf, yarn.mYarn_Fill); + (*result)[yarn.mYarn_Fill] = '\0'; + } else + err = NS_ERROR_OUT_OF_MEMORY; + } + } + return err; +} + +/* static */ struct mdbYarn* nsMsgDatabase::nsStringToYarn( + struct mdbYarn* yarn, const nsAString& str) { + yarn->mYarn_Buf = ToNewCString(NS_ConvertUTF16toUTF8(str)); + yarn->mYarn_Size = strlen((const char*)yarn->mYarn_Buf) + 1; + yarn->mYarn_Fill = yarn->mYarn_Size - 1; + yarn->mYarn_Form = + 0; // what to do with this? we're storing csid in the msg hdr... + return yarn; +} + +/* static */ struct mdbYarn* nsMsgDatabase::UInt32ToYarn(struct mdbYarn* yarn, + uint32_t i) { + PR_snprintf((char*)yarn->mYarn_Buf, yarn->mYarn_Size, "%lx", i); + yarn->mYarn_Fill = PL_strlen((const char*)yarn->mYarn_Buf); + yarn->mYarn_Form = + 0; // what to do with this? Should be parsed out of the mime2 header? + return yarn; +} + +/* static */ struct mdbYarn* nsMsgDatabase::UInt64ToYarn(struct mdbYarn* yarn, + uint64_t i) { + PR_snprintf((char*)yarn->mYarn_Buf, yarn->mYarn_Size, "%llx", i); + yarn->mYarn_Fill = PL_strlen((const char*)yarn->mYarn_Buf); + yarn->mYarn_Form = 0; + return yarn; +} + +/* static */ void nsMsgDatabase::YarnTonsString(struct mdbYarn* yarn, + nsAString& str) { + const char* buf = (const char*)yarn->mYarn_Buf; + if (buf) + CopyUTF8toUTF16(Substring(buf, buf + yarn->mYarn_Fill), str); + else + str.Truncate(); +} + +/* static */ void nsMsgDatabase::YarnTonsCString(struct mdbYarn* yarn, + nsACString& str) { + const char* buf = (const char*)yarn->mYarn_Buf; + if (buf) + str.Assign(buf, yarn->mYarn_Fill); + else + str.Truncate(); +} + +// WARNING - if yarn is empty, *pResult will not be changed!!!! +// this is so we can leave default values as they were. +/* static */ void nsMsgDatabase::YarnToUInt32(struct mdbYarn* yarn, + uint32_t* pResult) { + uint8_t numChars = std::min<mdb_fill>(8, yarn->mYarn_Fill); + + if (numChars == 0) return; + + *pResult = MsgUnhex((char*)yarn->mYarn_Buf, numChars); +} + +// WARNING - if yarn is empty, *pResult will not be changed!!!! +// this is so we can leave default values as they were. +/* static */ void nsMsgDatabase::YarnToUInt64(struct mdbYarn* yarn, + uint64_t* pResult) { + uint8_t numChars = std::min<mdb_fill>(16, yarn->mYarn_Fill); + + if (numChars == 0) return; + + *pResult = MsgUnhex((char*)yarn->mYarn_Buf, numChars); +} + +nsresult nsMsgDatabase::GetProperty(nsIMdbRow* row, const char* propertyName, + char** result) { + nsresult err = NS_OK; + mdb_token property_token; + + if (m_mdbStore) + err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token); + else + err = NS_ERROR_NULL_POINTER; + if (NS_SUCCEEDED(err)) + err = RowCellColumnToCharPtr(row, property_token, result); + + return err; +} + +nsresult nsMsgDatabase::SetProperty(nsIMdbRow* row, const char* propertyName, + const char* propertyVal) { + nsresult err = NS_OK; + mdb_token property_token; + + NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us. + if (!row) return NS_ERROR_NULL_POINTER; + + err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token); + if (NS_SUCCEEDED(err)) + CharPtrToRowCellColumn(row, property_token, propertyVal); + return err; +} + +nsresult nsMsgDatabase::GetPropertyAsNSString(nsIMdbRow* row, + const char* propertyName, + nsAString& result) { + nsresult err = NS_OK; + mdb_token property_token; + + NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us. + if (!row) return NS_ERROR_NULL_POINTER; + + err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token); + if (NS_SUCCEEDED(err)) + err = RowCellColumnTonsString(row, property_token, result); + + return err; +} + +nsresult nsMsgDatabase::SetPropertyFromNSString(nsIMdbRow* row, + const char* propertyName, + const nsAString& propertyVal) { + nsresult err = NS_OK; + mdb_token property_token; + + NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us. + if (!row) return NS_ERROR_NULL_POINTER; + + err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token); + if (NS_SUCCEEDED(err)) + return SetNSStringPropertyWithToken(row, property_token, propertyVal); + + return err; +} + +nsresult nsMsgDatabase::GetUint32Property(nsIMdbRow* row, + const char* propertyName, + uint32_t* result, + uint32_t defaultValue) { + nsresult err = NS_OK; + mdb_token property_token; + + NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us. + if (!row) return NS_ERROR_NULL_POINTER; + + err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token); + if (NS_SUCCEEDED(err)) + err = RowCellColumnToUInt32(row, property_token, result, defaultValue); + + return err; +} + +nsresult nsMsgDatabase::GetUint64Property(nsIMdbRow* row, + const char* propertyName, + uint64_t* result, + uint64_t defaultValue) { + nsresult err = NS_OK; + mdb_token property_token; + + NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us. + if (!row) return NS_ERROR_NULL_POINTER; + + err = m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token); + if (NS_SUCCEEDED(err)) + err = RowCellColumnToUInt64(row, property_token, result, defaultValue); + + return err; +} + +nsresult nsMsgDatabase::SetUint32Property(nsIMdbRow* row, + const char* propertyName, + uint32_t propertyVal) { + struct mdbYarn yarn; + char int32StrBuf[20]; + yarn.mYarn_Buf = int32StrBuf; + yarn.mYarn_Size = sizeof(int32StrBuf); + yarn.mYarn_Fill = sizeof(int32StrBuf); + + NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us. + if (!row) return NS_ERROR_NULL_POINTER; + + mdb_token property_token; + + nsresult err = + m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token); + if (NS_SUCCEEDED(err)) { + UInt32ToYarn(&yarn, propertyVal); + err = row->AddColumn(GetEnv(), property_token, &yarn); + } + return err; +} + +nsresult nsMsgDatabase::SetUint64Property(nsIMdbRow* row, + const char* propertyName, + uint64_t propertyVal) { + struct mdbYarn yarn; + char int64StrBuf[100]; + yarn.mYarn_Buf = int64StrBuf; + yarn.mYarn_Size = sizeof(int64StrBuf); + yarn.mYarn_Fill = sizeof(int64StrBuf); + + NS_ENSURE_STATE(m_mdbStore); // db might have been closed out from under us. + if (!row) return NS_ERROR_NULL_POINTER; + + mdb_token property_token; + + nsresult err = + m_mdbStore->StringToToken(GetEnv(), propertyName, &property_token); + if (NS_SUCCEEDED(err)) { + UInt64ToYarn(&yarn, propertyVal); + err = row->AddColumn(GetEnv(), property_token, &yarn); + } + return err; +} + +nsresult nsMsgDatabase::GetBooleanProperty(nsIMdbRow* row, + const char* propertyName, + bool* result, + bool defaultValue /* = false */) { + uint32_t res; + nsresult rv = + GetUint32Property(row, propertyName, &res, (uint32_t)defaultValue); + *result = !!res; + return rv; +} + +nsresult nsMsgDatabase::SetBooleanProperty(nsIMdbRow* row, + const char* propertyName, + bool propertyVal) { + return SetUint32Property(row, propertyName, (uint32_t)propertyVal); +} + +nsresult nsMsgDatabase::SetNSStringPropertyWithToken( + nsIMdbRow* row, mdb_token aProperty, const nsAString& propertyStr) { + NS_ENSURE_ARG(row); + struct mdbYarn yarn; + + yarn.mYarn_Grow = NULL; + nsresult err = + row->AddColumn(GetEnv(), aProperty, nsStringToYarn(&yarn, propertyStr)); + free((char*)yarn.mYarn_Buf); // won't need this when we have nsCString + return err; +} + +uint32_t nsMsgDatabase::GetCurVersion() { return kMsgDBVersion; } + +NS_IMETHODIMP nsMsgDatabase::SetSummaryValid(bool valid /* = true */) { + // If the file was invalid when opened (for example in folder compact), then + // it may + // not have been added to the cache. Add it now if missing. + if (valid) { + nsCOMPtr<nsIMsgDBService> serv(mozilla::components::DB::Service()); + static_cast<nsMsgDBService*>(serv.get())->EnsureCached(this); + } + // setting the version to 0 ought to make it pretty invalid. + if (m_dbFolderInfo) m_dbFolderInfo->SetVersion(valid ? GetCurVersion() : 0); + + // for default db (and news), there's no nothing to set to make it it valid + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetSummaryValid(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = true; + return NS_OK; +} + +// protected routines + +// should we thread messages with common subjects that don't start with Re: +// together? I imagine we might have separate preferences for mail and news, so +// this is a virtual method. +bool nsMsgDatabase::ThreadBySubjectWithoutRe() { + GetGlobalPrefs(); + return gThreadWithoutRe; +} + +bool nsMsgDatabase::UseStrictThreading() { + GetGlobalPrefs(); + return gStrictThreading; +} + +// Should we make sure messages are always threaded correctly (see bug 181446) +bool nsMsgDatabase::UseCorrectThreading() { + GetGlobalPrefs(); + return gCorrectThreading; +} + +// adapted from removed PL_DHashFreeStringKey +static void msg_DHashFreeStringKey(PLDHashTable* aTable, + PLDHashEntryHdr* aEntry) { + const PLDHashEntryStub* stub = (const PLDHashEntryStub*)aEntry; + free((void*)stub->key); + PLDHashTable::ClearEntryStub(aTable, aEntry); +} + +PLDHashTableOps nsMsgDatabase::gRefHashTableOps = { + PLDHashTable::HashStringKey, PLDHashTable::MatchStringKey, + PLDHashTable::MoveEntryStub, msg_DHashFreeStringKey, nullptr}; + +nsresult nsMsgDatabase::GetRefFromHash(nsCString& reference, + nsMsgKey* threadId) { + // Initialize the reference hash + if (!m_msgReferences) { + nsresult rv = InitRefHash(); + if (NS_FAILED(rv)) return rv; + } + + // Find reference from the hash + PLDHashEntryHdr* entry = + m_msgReferences->Search((const void*)reference.get()); + if (entry) { + RefHashElement* element = static_cast<RefHashElement*>(entry); + *threadId = element->mThreadId; + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +nsresult nsMsgDatabase::AddRefToHash(nsCString& reference, nsMsgKey threadId) { + if (m_msgReferences) { + PLDHashEntryHdr* entry = + m_msgReferences->Add((void*)reference.get(), mozilla::fallible); + if (!entry) return NS_ERROR_OUT_OF_MEMORY; // XXX out of memory + + RefHashElement* element = static_cast<RefHashElement*>(entry); + if (!element->mRef) { + element->mRef = + ToNewCString(reference); // Will be freed in msg_DHashFreeStringKey() + element->mThreadId = threadId; + element->mCount = 1; + } else + element->mCount++; + } + + return NS_OK; +} + +nsresult nsMsgDatabase::AddMsgRefsToHash(nsIMsgDBHdr* msgHdr) { + uint16_t numReferences = 0; + nsMsgKey threadId; + nsresult rv = NS_OK; + + msgHdr->GetThreadId(&threadId); + msgHdr->GetNumReferences(&numReferences); + + for (int32_t i = 0; i < numReferences; i++) { + nsAutoCString reference; + + msgHdr->GetStringReference(i, reference); + if (reference.IsEmpty()) break; + + rv = AddRefToHash(reference, threadId); + if (NS_FAILED(rv)) break; + } + + return rv; +} + +nsresult nsMsgDatabase::RemoveRefFromHash(nsCString& reference) { + if (m_msgReferences) { + PLDHashEntryHdr* entry = + m_msgReferences->Search((const void*)reference.get()); + if (entry) { + RefHashElement* element = static_cast<RefHashElement*>(entry); + if (--element->mCount == 0) + m_msgReferences->Remove((void*)reference.get()); + } + } + return NS_OK; +} + +// Filter only messages with one or more references +nsresult nsMsgDatabase::RemoveMsgRefsFromHash(nsIMsgDBHdr* 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; + } + + return rv; +} + +static nsresult nsReferencesOnlyFilter(nsIMsgDBHdr* msg, void* closure) { + uint16_t numReferences = 0; + msg->GetNumReferences(&numReferences); + return (numReferences) ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult nsMsgDatabase::InitRefHash() { + // Delete an existing table just in case + if (m_msgReferences) delete m_msgReferences; + + // Create new table + m_msgReferences = new PLDHashTable( + &gRefHashTableOps, sizeof(struct RefHashElement), MSG_HASH_SIZE); + if (!m_msgReferences) return NS_ERROR_OUT_OF_MEMORY; + + // Create enumerator to go through all messages with references + nsCOMPtr<nsIMsgEnumerator> enumerator; + enumerator = new nsMsgDBEnumerator(this, m_mdbAllMsgHeadersTable, + nsReferencesOnlyFilter, nullptr); + if (enumerator == nullptr) return NS_ERROR_OUT_OF_MEMORY; + + // Populate table with references of existing messages + bool hasMore; + nsresult rv = NS_OK; + while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = enumerator->GetNext(getter_AddRefs(msgHdr)); + if (msgHdr && NS_SUCCEEDED(rv)) rv = AddMsgRefsToHash(msgHdr); + if (NS_FAILED(rv)) break; + } + + return rv; +} + +nsresult nsMsgDatabase::CreateNewThread(nsMsgKey threadId, const char* subject, + nsMsgThread** pnewThread) { + nsresult err = NS_OK; + nsCOMPtr<nsIMdbTable> threadTable; + struct mdbOid threadTableOID; + struct mdbOid allThreadsTableOID; + + if (!pnewThread || !m_mdbStore) return NS_ERROR_NULL_POINTER; + + threadTableOID.mOid_Scope = m_hdrRowScopeToken; + threadTableOID.mOid_Id = threadId; + + // Under some circumstances, mork seems to reuse an old table when we create + // one. Prevent problems from that by finding any old table first, and + // deleting its rows. + nsresult res = GetStore()->GetTable(GetEnv(), &threadTableOID, + getter_AddRefs(threadTable)); + if (NS_SUCCEEDED(res) && threadTable) threadTable->CutAllRows(GetEnv()); + + err = GetStore()->NewTableWithOid(GetEnv(), &threadTableOID, + m_threadTableKindToken, false, nullptr, + getter_AddRefs(threadTable)); + if (NS_FAILED(err)) return err; + + allThreadsTableOID.mOid_Scope = m_threadRowScopeToken; + allThreadsTableOID.mOid_Id = threadId; + + // add a row for this thread in the table of all threads that we'll use + // to do our mapping between subject strings and threads. + nsCOMPtr<nsIMdbRow> threadRow; + + err = m_mdbStore->GetRow(GetEnv(), &allThreadsTableOID, + getter_AddRefs(threadRow)); + if (!threadRow) { + err = m_mdbStore->NewRowWithOid(GetEnv(), &allThreadsTableOID, + getter_AddRefs(threadRow)); + if (NS_SUCCEEDED(err) && threadRow) { + if (m_mdbAllThreadsTable) + m_mdbAllThreadsTable->AddRow(GetEnv(), threadRow); + err = CharPtrToRowCellColumn(threadRow, m_threadSubjectColumnToken, + subject); + } + } else { +#ifdef DEBUG_David_Bienvenu + NS_WARNING("odd that thread row already exists"); +#endif + threadRow->CutAllColumns(GetEnv()); + nsCOMPtr<nsIMdbRow> metaRow; + threadTable->GetMetaRow(GetEnv(), nullptr, nullptr, + getter_AddRefs(metaRow)); + if (metaRow) metaRow->CutAllColumns(GetEnv()); + + CharPtrToRowCellColumn(threadRow, m_threadSubjectColumnToken, subject); + } + + *pnewThread = new nsMsgThread(this, threadTable); + if (*pnewThread) { + (*pnewThread)->SetThreadKey(threadId); + m_cachedThread = *pnewThread; + m_cachedThreadId = threadId; + } + return err; +} + +nsIMsgThread* nsMsgDatabase::GetThreadForReference(nsCString& msgID, + nsIMsgDBHdr** pMsgHdr) { + nsMsgKey threadId; + nsIMsgDBHdr* msgHdr = nullptr; + GetMsgHdrForMessageID(msgID.get(), &msgHdr); + nsIMsgThread* thread = NULL; + + if (msgHdr != NULL) { + if (NS_SUCCEEDED(msgHdr->GetThreadId(&threadId))) { + // find thread header for header whose message id we matched. + thread = GetThreadForThreadId(threadId); + } + if (pMsgHdr) + *pMsgHdr = msgHdr; + else + msgHdr->Release(); + } + // Referenced message not found, check if there are messages that reference + // same message + else if (UseCorrectThreading()) { + if (NS_SUCCEEDED(GetRefFromHash(msgID, &threadId))) + thread = GetThreadForThreadId(threadId); + } + + return thread; +} + +nsIMsgThread* nsMsgDatabase::GetThreadForSubject(nsCString& subject) { + nsIMsgThread* thread = nullptr; + + mdbYarn subjectYarn; + + subjectYarn.mYarn_Buf = (void*)subject.get(); + subjectYarn.mYarn_Fill = PL_strlen(subject.get()); + subjectYarn.mYarn_Form = 0; + subjectYarn.mYarn_Size = subjectYarn.mYarn_Fill; + + nsCOMPtr<nsIMdbRow> threadRow; + mdbOid outRowId; + if (m_mdbStore) { + nsresult result = m_mdbStore->FindRow( + GetEnv(), m_threadRowScopeToken, m_threadSubjectColumnToken, + &subjectYarn, &outRowId, getter_AddRefs(threadRow)); + if (NS_SUCCEEDED(result) && threadRow) { + // Get key from row + mdbOid outOid; + nsMsgKey key = nsMsgKey_None; + if (NS_SUCCEEDED(threadRow->GetOid(GetEnv(), &outOid))) + key = outOid.mOid_Id; + // find thread header for header whose message id we matched. + // It is fine if key was not found, + // GetThreadForThreadId(nsMsgKey_None) returns nullptr. + thread = GetThreadForThreadId(key); + } +#ifdef DEBUG_bienvenu1 + else { + nsresult rv; + RefPtr<nsMsgThread> pThread; + + nsCOMPtr<nsIMdbPortTableCursor> tableCursor; + m_mdbStore->GetPortTableCursor(GetEnv(), m_hdrRowScopeToken, + m_threadTableKindToken, + getter_AddRefs(tableCursor)); + + nsCOMPtr<nsIMdbTable> table; + + while (true) { + rv = tableCursor->NextTable(GetEnv(), getter_AddRefs(table)); + if (!table) break; + if (NS_FAILED(rv)) break; + + pThread = new nsMsgThread(this, table); + if (pThread) { + nsCString curSubject; + pThread->GetSubject(curSubject); + if (subject.Equals(curSubject)) { + NS_ERROR("thread with subject exists, but FindRow didn't find it"); + break; + } + } else + break; + } + } +#endif + } + return thread; +} + +// Returns thread that contains a message that references the passed message ID +nsIMsgThread* nsMsgDatabase::GetThreadForMessageId(nsCString& msgId) { + nsIMsgThread* thread = NULL; + nsMsgKey threadId; + + if (NS_SUCCEEDED(GetRefFromHash(msgId, &threadId))) + thread = GetThreadForThreadId(threadId); + + return thread; +} + +nsresult nsMsgDatabase::ThreadNewHdr(nsMsgHdr* newHdr, bool& newThread) { + nsresult result = NS_ERROR_UNEXPECTED; + nsCOMPtr<nsIMsgThread> thread; + nsCOMPtr<nsIMsgDBHdr> replyToHdr; + nsMsgKey threadId = nsMsgKey_None, newHdrKey; + + if (!newHdr) return NS_ERROR_NULL_POINTER; + + newHdr->SetThreadParent( + nsMsgKey_None); // if we're undoing, could have a thread parent + uint16_t numReferences = 0; + uint32_t newHdrFlags = 0; + + // use raw flags instead of GetFlags, because GetFlags will + // pay attention to what's in m_newSet, and this new hdr isn't + // in m_newSet yet. + newHdr->GetRawFlags(&newHdrFlags); + newHdr->GetNumReferences(&numReferences); + newHdr->GetMessageKey(&newHdrKey); + + // try reference threading first + for (int32_t i = numReferences - 1; i >= 0; i--) { + nsAutoCString reference; + + newHdr->GetStringReference(i, reference); + // first reference we have hdr for is best top-level hdr. + // but we have to handle case of promoting new header to top-level + // in case the top-level header comes after a reply. + + if (reference.IsEmpty()) break; + + thread = dont_AddRef( + GetThreadForReference(reference, getter_AddRefs(replyToHdr))); + if (thread) { + if (replyToHdr) { + nsMsgKey replyToKey; + replyToHdr->GetMessageKey(&replyToKey); + // message claims to be a reply to itself - ignore that since it leads + // to corrupt threading. + if (replyToKey == newHdrKey) { + // bad references - throw them all away. + newHdr->SetMessageId(""); + thread = nullptr; + break; + } + } + thread->GetThreadKey(&threadId); + newHdr->SetThreadId(threadId); + result = AddToThread(newHdr, thread, replyToHdr, true); + break; + } + } + // if user hasn't said "only thread by ref headers", thread by subject + if (!thread && !UseStrictThreading()) { + // try subject threading if we couldn't find a reference and the subject + // starts with Re: + nsCString subject; + newHdr->GetSubject(subject); + if (ThreadBySubjectWithoutRe() || + (newHdrFlags & nsMsgMessageFlags::HasRe)) { + nsAutoCString cSubject(subject); + thread = dont_AddRef(GetThreadForSubject(cSubject)); + if (thread) { + thread->GetThreadKey(&threadId); + newHdr->SetThreadId(threadId); + // TRACE("threading based on subject %s\n", (const char *) + // msgHdr->m_subject); + // if we move this and do subject threading after, ref threading, + // don't thread within children, since we know it won't work. But for + // now, pass TRUE. + result = AddToThread(newHdr, thread, nullptr, true); + } + } + } + + // Check if this is a new parent to an existing message (that has a reference + // to this message) + if (!thread && UseCorrectThreading()) { + nsCString msgId; + newHdr->GetMessageId(getter_Copies(msgId)); + + thread = dont_AddRef(GetThreadForMessageId(msgId)); + if (thread) { + thread->GetThreadKey(&threadId); + newHdr->SetThreadId(threadId); + result = AddToThread(newHdr, thread, nullptr, true); + } + } + + if (!thread) { + // Not a parent or child, make it a new thread for now + result = AddNewThread(newHdr); + newThread = true; + } else { + newThread = false; + } + return result; +} + +nsresult nsMsgDatabase::AddToThread(nsMsgHdr* newHdr, nsIMsgThread* thread, + nsIMsgDBHdr* inReplyTo, + bool threadInThread) { + // don't worry about real threading yet. + return thread->AddChild(newHdr, inReplyTo, threadInThread, this); +} + +nsMsgHdr* nsMsgDatabase::GetMsgHdrForReference(nsCString& reference) { + NS_ASSERTION(false, "not implemented yet."); + return nullptr; +} + +NS_IMETHODIMP nsMsgDatabase::GetMsgHdrForMessageID(const char* aMsgID, + nsIMsgDBHdr** aHdr) { + NS_ENSURE_ARG_POINTER(aHdr); + NS_ENSURE_ARG_POINTER(aMsgID); + nsIMsgDBHdr* msgHdr = nullptr; + nsresult rv = NS_OK; + mdbYarn messageIdYarn; + + messageIdYarn.mYarn_Buf = (void*)aMsgID; + messageIdYarn.mYarn_Fill = PL_strlen(aMsgID); + messageIdYarn.mYarn_Form = 0; + messageIdYarn.mYarn_Size = messageIdYarn.mYarn_Fill; + + nsIMdbRow* hdrRow; + mdbOid outRowId; + nsresult result; + if (m_mdbStore) + result = m_mdbStore->FindRow(GetEnv(), m_hdrRowScopeToken, + m_messageIdColumnToken, &messageIdYarn, + &outRowId, &hdrRow); + else + return NS_ERROR_FAILURE; + if (NS_SUCCEEDED(result) && hdrRow) { + // Get key from row + mdbOid outOid; + nsMsgKey key = nsMsgKey_None; + rv = hdrRow->GetOid(GetEnv(), &outOid); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + key = outOid.mOid_Id; + + rv = CreateMsgHdr(hdrRow, key, &msgHdr); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } + *aHdr = msgHdr; // already addreffed above. + return NS_OK; // it's not an error not to find a msg hdr. +} + +NS_IMETHODIMP nsMsgDatabase::GetMsgHdrForGMMsgID(const char* aGMMsgId, + nsIMsgDBHdr** aHdr) { + NS_ENSURE_ARG_POINTER(aGMMsgId); + NS_ENSURE_ARG_POINTER(aHdr); + nsIMsgDBHdr* msgHdr = nullptr; + nsresult rv = NS_OK; + mdbYarn gMailMessageIdYarn; + gMailMessageIdYarn.mYarn_Buf = (void*)aGMMsgId; + gMailMessageIdYarn.mYarn_Fill = strlen(aGMMsgId); + gMailMessageIdYarn.mYarn_Form = 0; + gMailMessageIdYarn.mYarn_Size = gMailMessageIdYarn.mYarn_Fill; + + nsIMdbRow* hdrRow; + mdbOid outRowId; + nsresult result; + mdb_token property_token; + NS_ENSURE_TRUE(m_mdbStore, NS_ERROR_NULL_POINTER); + result = m_mdbStore->StringToToken(GetEnv(), "X-GM-MSGID", &property_token); + NS_ENSURE_SUCCESS(result, result); + result = m_mdbStore->FindRow(GetEnv(), m_hdrRowScopeToken, property_token, + &gMailMessageIdYarn, &outRowId, &hdrRow); + if (NS_SUCCEEDED(result) && hdrRow) { + // Get key from row + mdbOid outOid; + rv = hdrRow->GetOid(GetEnv(), &outOid); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgKey key = outOid.mOid_Id; + rv = CreateMsgHdr(hdrRow, key, &msgHdr); + if (NS_WARN_IF(NS_FAILED(rv))) return rv; + } + *aHdr = msgHdr; + return NS_OK; // it's not an error not to find a msg hdr. +} + +nsIMsgDBHdr* nsMsgDatabase::GetMsgHdrForSubject(nsCString& subject) { + nsIMsgDBHdr* msgHdr = nullptr; + nsresult rv = NS_OK; + mdbYarn subjectYarn; + + subjectYarn.mYarn_Buf = (void*)subject.get(); + subjectYarn.mYarn_Fill = PL_strlen(subject.get()); + subjectYarn.mYarn_Form = 0; + subjectYarn.mYarn_Size = subjectYarn.mYarn_Fill; + + nsIMdbRow* hdrRow; + mdbOid outRowId; + nsresult result = + GetStore()->FindRow(GetEnv(), m_hdrRowScopeToken, m_subjectColumnToken, + &subjectYarn, &outRowId, &hdrRow); + if (NS_SUCCEEDED(result) && hdrRow) { + // Get key from row + mdbOid outOid; + nsMsgKey key = nsMsgKey_None; + rv = hdrRow->GetOid(GetEnv(), &outOid); + if (NS_WARN_IF(NS_FAILED(rv))) return nullptr; + key = outOid.mOid_Id; + + rv = CreateMsgHdr(hdrRow, key, &msgHdr); + if (NS_WARN_IF(NS_FAILED(rv))) return nullptr; + } + return msgHdr; +} + +NS_IMETHODIMP nsMsgDatabase::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr, + nsIMsgThread** result) { + NS_ENSURE_ARG_POINTER(msgHdr); + NS_ENSURE_ARG_POINTER(result); + + *result = nullptr; + nsMsgKey threadId = nsMsgKey_None; + (void)msgHdr->GetThreadId(&threadId); + if (threadId != nsMsgKey_None) *result = GetThreadForThreadId(threadId); + + // if we can't find the thread, try using the msg key as the thread id, + // because the msg hdr might not have the thread id set correctly + // Or maybe the message was deleted? + if (!*result) { + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + *result = GetThreadForThreadId(msgKey); + } + // failure is normal when message was deleted + return (*result) ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult nsMsgDatabase::GetThreadForMsgKey(nsMsgKey msgKey, + nsIMsgThread** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + nsCOMPtr<nsIMsgDBHdr> msg; + GetMsgHdrForKey(msgKey, getter_AddRefs(msg)); + if (!msg) return NS_MSG_MESSAGE_NOT_FOUND; + + return GetThreadContainingMsgHdr(msg, aResult); +} + +// caller needs to unrefer. +nsIMsgThread* nsMsgDatabase::GetThreadForThreadId(nsMsgKey threadId) { + nsIMsgThread* retThread = (threadId == m_cachedThreadId && m_cachedThread) + ? m_cachedThread.get() + : FindExistingThread(threadId); + if (retThread) { + NS_ADDREF(retThread); + return retThread; + } + if (m_mdbStore) { + mdbOid tableId; + tableId.mOid_Id = threadId; + tableId.mOid_Scope = m_hdrRowScopeToken; + + nsCOMPtr<nsIMdbTable> threadTable; + nsresult res = + m_mdbStore->GetTable(GetEnv(), &tableId, getter_AddRefs(threadTable)); + + if (NS_SUCCEEDED(res) && threadTable) { + retThread = new nsMsgThread(this, threadTable); + if (retThread) { + NS_ADDREF(retThread); + m_cachedThread = retThread; + m_cachedThreadId = threadId; + } + } + } + return retThread; +} + +// make the passed in header a thread header +nsresult nsMsgDatabase::AddNewThread(nsMsgHdr* msgHdr) { + if (!msgHdr) return NS_ERROR_NULL_POINTER; + + nsMsgThread* threadHdr = nullptr; + + nsCString subject; + nsMsgKey threadKey; + msgHdr->GetMessageKey(&threadKey); + // can't have a thread with key 1 since that's the table id of the all msg hdr + // table, so give it kTableKeyForThreadOne (0xfffffffe). + if (threadKey == kAllMsgHdrsTableKey) threadKey = kTableKeyForThreadOne; + + nsresult err = msgHdr->GetSubject(subject); + + err = CreateNewThread(threadKey, subject.get(), &threadHdr); + msgHdr->SetThreadId(threadKey); + if (threadHdr) { + NS_ADDREF(threadHdr); + // err = msgHdr->GetSubject(subject); + // threadHdr->SetThreadKey(msgHdr->m_messageKey); + // threadHdr->SetSubject(subject.get()); + // need to add the thread table to the db. + AddToThread(msgHdr, threadHdr, nullptr, false); + NS_RELEASE(threadHdr); + } + return err; +} + +nsresult nsMsgDatabase::GetBoolPref(const char* prefName, bool* result) { + bool prefValue = false; + nsresult rv; + nsCOMPtr<nsIPrefBranch> pPrefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (pPrefBranch) { + rv = pPrefBranch->GetBoolPref(prefName, &prefValue); + *result = prefValue; + } + return rv; +} + +nsresult nsMsgDatabase::GetIntPref(const char* prefName, int32_t* result) { + int32_t prefValue = 0; + nsresult rv; + nsCOMPtr<nsIPrefBranch> pPrefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (pPrefBranch) { + rv = pPrefBranch->GetIntPref(prefName, &prefValue); + *result = prefValue; + } + return rv; +} + +NS_IMETHODIMP nsMsgDatabase::SetAttributeOnPendingHdr(nsIMsgDBHdr* pendingHdr, + const char* property, + const char* propertyVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDatabase::SetUint32AttributeOnPendingHdr( + nsIMsgDBHdr* pendingHdr, const char* property, uint32_t propertyVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDatabase::SetUint64AttributeOnPendingHdr(nsIMsgDBHdr* aPendingHdr, + const char* aProperty, + uint64_t aPropertyVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDatabase::UpdatePendingAttributes(nsIMsgDBHdr* aNewHdr) { return NS_OK; } + +NS_IMETHODIMP nsMsgDatabase::GetOfflineOpForKey( + nsMsgKey msgKey, bool create, nsIMsgOfflineImapOperation** offlineOp) { + NS_ASSERTION(false, "overridden by nsMailDatabase"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDatabase::RemoveOfflineOp(nsIMsgOfflineImapOperation* op) { + NS_ASSERTION(false, "overridden by nsMailDatabase"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDatabase::ListAllOfflineMsgs(nsTArray<nsMsgKey>& keys) { + keys.Clear(); + nsCOMPtr<nsIMsgEnumerator> enumerator; + uint32_t flag = nsMsgMessageFlags::Offline; + // if we change this routine to return an enumerator that generates the keys + // one by one, we'll need to somehow make a copy of flag for the enumerator + // to own, since the enumerator will persist past the life of flag on the + // stack. + nsresult rv = EnumerateMessagesWithFlag(getter_AddRefs(enumerator), &flag); + if (NS_SUCCEEDED(rv) && enumerator) { + bool hasMoreElements; + while (NS_SUCCEEDED(enumerator->HasMoreElements(&hasMoreElements)) && + hasMoreElements) { + // clear out db hdr, because it won't be valid when we get rid of the .msf + // file + nsCOMPtr<nsIMsgDBHdr> dbMessage; + rv = enumerator->GetNext(getter_AddRefs(dbMessage)); + if (NS_SUCCEEDED(rv) && dbMessage) { + nsMsgKey msgKey; + dbMessage->GetMessageKey(&msgKey); + keys.AppendElement(msgKey); + } + } + } + return rv; +} + +NS_IMETHODIMP nsMsgDatabase::ListAllOfflineOpIds( + nsTArray<nsMsgKey>& offlineOpIds) { + NS_ASSERTION(false, "overridden by nsMailDatabase"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDatabase::ListAllOfflineDeletes( + nsTArray<nsMsgKey>& offlineDeletes) { + // technically, notimplemented, but no one's putting offline ops in anyway. + return NS_OK; +} +NS_IMETHODIMP nsMsgDatabase::GetHighWaterArticleNum(nsMsgKey* key) { + if (!m_dbFolderInfo) return NS_ERROR_NULL_POINTER; + return m_dbFolderInfo->GetHighWater(key); +} + +NS_IMETHODIMP nsMsgDatabase::GetLowWaterArticleNum(nsMsgKey* key) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* attribute nsMsgKey NextPseudoMsgKey */ + +NS_IMETHODIMP nsMsgDatabase::GetNextPseudoMsgKey(nsMsgKey* nextPseudoMsgKey) { + NS_ENSURE_ARG_POINTER(nextPseudoMsgKey); + *nextPseudoMsgKey = m_nextPseudoMsgKey--; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::SetNextPseudoMsgKey(nsMsgKey nextPseudoMsgKey) { + m_nextPseudoMsgKey = nextPseudoMsgKey; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetNextFakeOfflineMsgKey( + nsMsgKey* nextFakeOfflineMsgKey) { + NS_ENSURE_ARG_POINTER(nextFakeOfflineMsgKey); + // iterate over hdrs looking for first non-existent fake offline msg key + nsMsgKey fakeMsgKey = kIdStartOfFake; + + bool containsKey; + do { + ContainsKey(fakeMsgKey, &containsKey); + if (!containsKey) break; + fakeMsgKey--; + } while (containsKey); + + *nextFakeOfflineMsgKey = fakeMsgKey; + return NS_OK; +} + +#ifdef DEBUG +nsresult nsMsgDatabase::DumpContents() { + nsTArray<nsMsgKey> keys; + nsresult rv = ListAllKeys(keys); + NS_ENSURE_SUCCESS(rv, rv); + for (nsMsgKey key : keys) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (msgHdr) { + nsCString author; + nsCString subject; + + msgHdr->GetMessageKey(&key); + msgHdr->GetAuthor(getter_Copies(author)); + msgHdr->GetSubject(subject); + printf("hdr key = %u, author = %s subject = %s\n", key, author.get(), + subject.get()); + } + } + + nsCOMPtr<nsIMsgThreadEnumerator> threads; + rv = EnumerateThreads(getter_AddRefs(threads)); + NS_ENSURE_SUCCESS(rv, rv); + bool hasMore = false; + while (NS_SUCCEEDED(rv = threads->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgThread> thread; + rv = threads->GetNext(getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgKey key; + thread->GetThreadKey(&key); + printf("thread key = %u\n", key); + // DumpThread(key); + } + return NS_OK; +} +#endif /* DEBUG */ + +NS_IMETHODIMP nsMsgDatabase::SetMsgRetentionSettings( + nsIMsgRetentionSettings* retentionSettings) { + m_retentionSettings = retentionSettings; + if (retentionSettings && m_dbFolderInfo) { + nsresult rv; + + nsMsgRetainByPreference retainByPreference; + uint32_t daysToKeepHdrs; + uint32_t numHeadersToKeep; + uint32_t daysToKeepBodies; + bool cleanupBodiesByDays; + bool useServerDefaults; + bool applyToFlaggedMessages; + + rv = retentionSettings->GetRetainByPreference(&retainByPreference); + NS_ENSURE_SUCCESS(rv, rv); + rv = retentionSettings->GetDaysToKeepHdrs(&daysToKeepHdrs); + NS_ENSURE_SUCCESS(rv, rv); + rv = retentionSettings->GetNumHeadersToKeep(&numHeadersToKeep); + NS_ENSURE_SUCCESS(rv, rv); + rv = retentionSettings->GetDaysToKeepBodies(&daysToKeepBodies); + NS_ENSURE_SUCCESS(rv, rv); + (void)retentionSettings->GetCleanupBodiesByDays(&cleanupBodiesByDays); + (void)retentionSettings->GetUseServerDefaults(&useServerDefaults); + rv = retentionSettings->GetApplyToFlaggedMessages(&applyToFlaggedMessages); + NS_ENSURE_SUCCESS(rv, rv); + // need to write this to the db. We'll just use the dbfolderinfo to write + // properties. + m_dbFolderInfo->SetUint32Property("retainBy", retainByPreference); + m_dbFolderInfo->SetUint32Property("daysToKeepHdrs", daysToKeepHdrs); + m_dbFolderInfo->SetUint32Property("numHdrsToKeep", numHeadersToKeep); + m_dbFolderInfo->SetUint32Property("daysToKeepBodies", daysToKeepBodies); + m_dbFolderInfo->SetBooleanProperty("cleanupBodies", cleanupBodiesByDays); + m_dbFolderInfo->SetBooleanProperty("useServerDefaults", useServerDefaults); + m_dbFolderInfo->SetBooleanProperty("applyToFlaggedMessages", + applyToFlaggedMessages); + } + Commit(nsMsgDBCommitType::kLargeCommit); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetMsgRetentionSettings( + nsIMsgRetentionSettings** retentionSettings) { + NS_ENSURE_ARG_POINTER(retentionSettings); + if (!m_retentionSettings) { + // create a new one, and initialize it from the db. + m_retentionSettings = new nsMsgRetentionSettings; + if (m_retentionSettings && m_dbFolderInfo) { + nsMsgRetainByPreference retainByPreference; + uint32_t daysToKeepHdrs = 0; + uint32_t numHeadersToKeep = 0; + bool useServerDefaults; + uint32_t daysToKeepBodies = 0; + bool cleanupBodiesByDays = false; + bool applyToFlaggedMessages; + + m_dbFolderInfo->GetUint32Property("retainBy", + nsIMsgRetentionSettings::nsMsgRetainAll, + &retainByPreference); + m_dbFolderInfo->GetUint32Property("daysToKeepHdrs", 0, &daysToKeepHdrs); + m_dbFolderInfo->GetUint32Property("numHdrsToKeep", 0, &numHeadersToKeep); + m_dbFolderInfo->GetUint32Property("daysToKeepBodies", 0, + &daysToKeepBodies); + m_dbFolderInfo->GetBooleanProperty("useServerDefaults", true, + &useServerDefaults); + m_dbFolderInfo->GetBooleanProperty("cleanupBodies", false, + &cleanupBodiesByDays); + m_dbFolderInfo->GetBooleanProperty("applyToFlaggedMessages", false, + &applyToFlaggedMessages); + m_retentionSettings->SetRetainByPreference(retainByPreference); + m_retentionSettings->SetDaysToKeepHdrs(daysToKeepHdrs); + m_retentionSettings->SetNumHeadersToKeep(numHeadersToKeep); + m_retentionSettings->SetDaysToKeepBodies(daysToKeepBodies); + m_retentionSettings->SetUseServerDefaults(useServerDefaults); + m_retentionSettings->SetCleanupBodiesByDays(cleanupBodiesByDays); + m_retentionSettings->SetApplyToFlaggedMessages(applyToFlaggedMessages); + } + } + NS_IF_ADDREF(*retentionSettings = m_retentionSettings); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::SetMsgDownloadSettings( + nsIMsgDownloadSettings* downloadSettings) { + m_downloadSettings = downloadSettings; + if (downloadSettings && m_dbFolderInfo) { + nsresult rv; + + bool useServerDefaults; + bool downloadByDate; + uint32_t ageLimitOfMsgsToDownload; + bool downloadUnreadOnly; + + rv = downloadSettings->GetUseServerDefaults(&useServerDefaults); + NS_ENSURE_SUCCESS(rv, rv); + rv = downloadSettings->GetDownloadByDate(&downloadByDate); + NS_ENSURE_SUCCESS(rv, rv); + rv = downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly); + NS_ENSURE_SUCCESS(rv, rv); + rv = downloadSettings->GetAgeLimitOfMsgsToDownload( + &ageLimitOfMsgsToDownload); + NS_ENSURE_SUCCESS(rv, rv); + // need to write this to the db. We'll just use the dbfolderinfo to write + // properties. + m_dbFolderInfo->SetBooleanProperty("useServerDefaults", useServerDefaults); + m_dbFolderInfo->SetBooleanProperty("downloadByDate", downloadByDate); + m_dbFolderInfo->SetBooleanProperty("downloadUnreadOnly", + downloadUnreadOnly); + m_dbFolderInfo->SetUint32Property("ageLimit", ageLimitOfMsgsToDownload); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetMsgDownloadSettings( + nsIMsgDownloadSettings** downloadSettings) { + NS_ENSURE_ARG_POINTER(downloadSettings); + if (!m_downloadSettings) { + // create a new one, and initialize it from the db. + m_downloadSettings = new nsMsgDownloadSettings; + if (m_downloadSettings && m_dbFolderInfo) { + bool useServerDefaults; + bool downloadByDate; + uint32_t ageLimitOfMsgsToDownload; + bool downloadUnreadOnly; + + m_dbFolderInfo->GetBooleanProperty("useServerDefaults", true, + &useServerDefaults); + m_dbFolderInfo->GetBooleanProperty("downloadByDate", false, + &downloadByDate); + m_dbFolderInfo->GetBooleanProperty("downloadUnreadOnly", false, + &downloadUnreadOnly); + m_dbFolderInfo->GetUint32Property("ageLimit", 0, + &ageLimitOfMsgsToDownload); + + m_downloadSettings->SetUseServerDefaults(useServerDefaults); + m_downloadSettings->SetDownloadByDate(downloadByDate); + m_downloadSettings->SetDownloadUnreadOnly(downloadUnreadOnly); + m_downloadSettings->SetAgeLimitOfMsgsToDownload(ageLimitOfMsgsToDownload); + } + } + NS_IF_ADDREF(*downloadSettings = m_downloadSettings); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::ApplyRetentionSettings( + nsIMsgRetentionSettings* aMsgRetentionSettings, bool aDeleteViaFolder) { + NS_ENSURE_ARG_POINTER(aMsgRetentionSettings); + nsresult rv = NS_OK; + + if (!m_folder) return NS_ERROR_NULL_POINTER; + + bool isDraftsTemplatesOutbox; + uint32_t dtoFlags = nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Templates | + nsMsgFolderFlags::Queue; + (void)m_folder->IsSpecialFolder(dtoFlags, true, &isDraftsTemplatesOutbox); + // Never apply retention settings to Drafts/Templates/Outbox. + if (isDraftsTemplatesOutbox) return NS_OK; + + nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsToDelete; + nsMsgRetainByPreference retainByPreference; + aMsgRetentionSettings->GetRetainByPreference(&retainByPreference); + + bool applyToFlaggedMessages = false; + aMsgRetentionSettings->GetApplyToFlaggedMessages(&applyToFlaggedMessages); + + uint32_t daysToKeepHdrs = 0; + uint32_t numHeadersToKeep = 0; + switch (retainByPreference) { + case nsIMsgRetentionSettings::nsMsgRetainAll: + break; + case nsIMsgRetentionSettings::nsMsgRetainByAge: + aMsgRetentionSettings->GetDaysToKeepHdrs(&daysToKeepHdrs); + rv = FindMessagesOlderThan(daysToKeepHdrs, applyToFlaggedMessages, + msgHdrsToDelete); + break; + case nsIMsgRetentionSettings::nsMsgRetainByNumHeaders: + aMsgRetentionSettings->GetNumHeadersToKeep(&numHeadersToKeep); + rv = FindExcessMessages(numHeadersToKeep, applyToFlaggedMessages, + msgHdrsToDelete); + break; + } + if (m_folder) { + // 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); + m_folder->SetStringProperty("LastPurgeTime", nsDependentCString(dateBuf)); + } + NS_ENSURE_SUCCESS(rv, rv); + + if (msgHdrsToDelete.IsEmpty()) { + return NS_OK; // No action required. + } + + if (aDeleteViaFolder) { + // The folder delete will also delete headers from the DB. + rv = m_folder->DeleteMessages(msgHdrsToDelete, nullptr, true, false, + nullptr, false); + } else { + // We're just deleting headers in the DB. + uint32_t kindex = 0; + for (nsIMsgDBHdr* hdr : msgHdrsToDelete) { + // Commit after every 300. + rv = DeleteHeader(hdr, nullptr, kindex % 300, true); + if (NS_FAILED(rv)) { + break; + } + } + // compress commit if we deleted more than 10 + if (msgHdrsToDelete.Length() > 10) { + Commit(nsMsgDBCommitType::kCompressCommit); + } else { + Commit(nsMsgDBCommitType::kLargeCommit); + } + } + return rv; +} + +nsresult nsMsgDatabase::FindMessagesOlderThan( + uint32_t daysToKeepHdrs, bool applyToFlaggedMessages, + nsTArray<RefPtr<nsIMsgDBHdr>>& hdrsToDelete) { + nsresult rv = NS_OK; + hdrsToDelete.Clear(); + + nsCOMPtr<nsIMsgEnumerator> hdrs; + rv = EnumerateMessages(getter_AddRefs(hdrs)); + NS_ENSURE_SUCCESS(rv, rv); + + // cutOffDay is the PRTime cut-off point. Any msg with a date less than + // that will get purged. + PRTime cutOffDay = PR_Now() - daysToKeepHdrs * PR_USEC_PER_DAY; + + bool hasMore = false; + while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> msg; + rv = hdrs->GetNext(getter_AddRefs(msg)); + NS_ASSERTION(NS_SUCCEEDED(rv), "nsMsgDBEnumerator broken"); + NS_ENSURE_SUCCESS(rv, rv); + + if (!applyToFlaggedMessages) { + uint32_t flags; + (void)msg->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Marked) { + continue; + } + } + + PRTime date; + msg->GetDate(&date); + if (date < cutOffDay) { + hdrsToDelete.AppendElement(msg); + } + } + + return NS_OK; +} + +nsresult nsMsgDatabase::FindExcessMessages( + uint32_t numHeadersToKeep, bool applyToFlaggedMessages, + nsTArray<RefPtr<nsIMsgDBHdr>>& hdrsToDelete) { + nsresult rv = NS_OK; + hdrsToDelete.Clear(); + + nsCOMPtr<nsIMsgEnumerator> hdrs; + rv = EnumerateMessages(getter_AddRefs(hdrs)); + NS_ENSURE_SUCCESS(rv, rv); + + mdb_count numHdrs = 0; + if (m_mdbAllMsgHeadersTable) + m_mdbAllMsgHeadersTable->GetCount(GetEnv(), &numHdrs); + else + return NS_ERROR_NULL_POINTER; + + bool hasMore = false; + while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> msg; + rv = hdrs->GetNext(getter_AddRefs(msg)); + NS_ASSERTION(NS_SUCCEEDED(rv), "nsMsgDBEnumerator broken"); + NS_ENSURE_SUCCESS(rv, rv); + + if (!applyToFlaggedMessages) { + uint32_t flags; + (void)msg->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Marked) { + continue; + } + } + + // this isn't quite right - we want to prefer unread messages (keep all of + // those we can) + if (numHdrs > numHeadersToKeep) { + numHdrs--; + hdrsToDelete.AppendElement(msg); + } + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsMsgRetentionSettings, nsIMsgRetentionSettings) + +// Initialise the member variables to reasonable defaults. +nsMsgRetentionSettings::nsMsgRetentionSettings() + : m_retainByPreference(1), + m_daysToKeepHdrs(0), + m_numHeadersToKeep(0), + m_useServerDefaults(true), + m_cleanupBodiesByDays(false), + m_daysToKeepBodies(0), + m_applyToFlaggedMessages(false) {} + +nsMsgRetentionSettings::~nsMsgRetentionSettings() {} + +/* attribute unsigned long retainByPreference */ + +NS_IMETHODIMP nsMsgRetentionSettings::GetRetainByPreference( + nsMsgRetainByPreference* retainByPreference) { + NS_ENSURE_ARG_POINTER(retainByPreference); + *retainByPreference = m_retainByPreference; + return NS_OK; +} + +NS_IMETHODIMP nsMsgRetentionSettings::SetRetainByPreference( + nsMsgRetainByPreference retainByPreference) { + m_retainByPreference = retainByPreference; + return NS_OK; +} + +/* attribute long daysToKeepHdrs; */ +NS_IMETHODIMP nsMsgRetentionSettings::GetDaysToKeepHdrs( + uint32_t* aDaysToKeepHdrs) { + NS_ENSURE_ARG_POINTER(aDaysToKeepHdrs); + *aDaysToKeepHdrs = m_daysToKeepHdrs; + return NS_OK; +} + +NS_IMETHODIMP nsMsgRetentionSettings::SetDaysToKeepHdrs( + uint32_t aDaysToKeepHdrs) { + m_daysToKeepHdrs = aDaysToKeepHdrs; + return NS_OK; +} + +/* attribute long numHeadersToKeep; */ +NS_IMETHODIMP nsMsgRetentionSettings::GetNumHeadersToKeep( + uint32_t* aNumHeadersToKeep) { + NS_ENSURE_ARG_POINTER(aNumHeadersToKeep); + *aNumHeadersToKeep = m_numHeadersToKeep; + return NS_OK; +} +NS_IMETHODIMP nsMsgRetentionSettings::SetNumHeadersToKeep( + uint32_t aNumHeadersToKeep) { + m_numHeadersToKeep = aNumHeadersToKeep; + return NS_OK; +} +/* attribute boolean useServerDefaults; */ +NS_IMETHODIMP nsMsgRetentionSettings::GetUseServerDefaults( + bool* aUseServerDefaults) { + NS_ENSURE_ARG_POINTER(aUseServerDefaults); + *aUseServerDefaults = m_useServerDefaults; + return NS_OK; +} +NS_IMETHODIMP nsMsgRetentionSettings::SetUseServerDefaults( + bool aUseServerDefaults) { + m_useServerDefaults = aUseServerDefaults; + return NS_OK; +} + +/* attribute boolean cleanupBodiesByDays; */ +NS_IMETHODIMP nsMsgRetentionSettings::GetCleanupBodiesByDays( + bool* aCleanupBodiesByDays) { + NS_ENSURE_ARG_POINTER(aCleanupBodiesByDays); + *aCleanupBodiesByDays = m_cleanupBodiesByDays; + return NS_OK; +} +NS_IMETHODIMP nsMsgRetentionSettings::SetCleanupBodiesByDays( + bool aCleanupBodiesByDays) { + m_cleanupBodiesByDays = aCleanupBodiesByDays; + return NS_OK; +} + +/* attribute long daysToKeepBodies; */ +NS_IMETHODIMP nsMsgRetentionSettings::GetDaysToKeepBodies( + uint32_t* aDaysToKeepBodies) { + NS_ENSURE_ARG_POINTER(aDaysToKeepBodies); + *aDaysToKeepBodies = m_daysToKeepBodies; + return NS_OK; +} +NS_IMETHODIMP nsMsgRetentionSettings::SetDaysToKeepBodies( + uint32_t aDaysToKeepBodies) { + m_daysToKeepBodies = aDaysToKeepBodies; + return NS_OK; +} + +/* attribute boolean applyToFlaggedMessages; */ +NS_IMETHODIMP nsMsgRetentionSettings::GetApplyToFlaggedMessages( + bool* aApplyToFlaggedMessages) { + NS_ENSURE_ARG_POINTER(aApplyToFlaggedMessages); + *aApplyToFlaggedMessages = m_applyToFlaggedMessages; + return NS_OK; +} +NS_IMETHODIMP nsMsgRetentionSettings::SetApplyToFlaggedMessages( + bool aApplyToFlaggedMessages) { + m_applyToFlaggedMessages = aApplyToFlaggedMessages; + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsMsgDownloadSettings, nsIMsgDownloadSettings) + +nsMsgDownloadSettings::nsMsgDownloadSettings() { + m_useServerDefaults = false; + m_downloadUnreadOnly = false; + m_downloadByDate = false; + m_ageLimitOfMsgsToDownload = 0; +} + +nsMsgDownloadSettings::~nsMsgDownloadSettings() {} + +/* attribute boolean useServerDefaults; */ +NS_IMETHODIMP nsMsgDownloadSettings::GetUseServerDefaults( + bool* aUseServerDefaults) { + NS_ENSURE_ARG_POINTER(aUseServerDefaults); + *aUseServerDefaults = m_useServerDefaults; + return NS_OK; +} +NS_IMETHODIMP nsMsgDownloadSettings::SetUseServerDefaults( + bool aUseServerDefaults) { + m_useServerDefaults = aUseServerDefaults; + return NS_OK; +} + +/* attribute boolean downloadUnreadOnly; */ +NS_IMETHODIMP nsMsgDownloadSettings::GetDownloadUnreadOnly( + bool* aDownloadUnreadOnly) { + NS_ENSURE_ARG_POINTER(aDownloadUnreadOnly); + *aDownloadUnreadOnly = m_downloadUnreadOnly; + return NS_OK; +} +NS_IMETHODIMP nsMsgDownloadSettings::SetDownloadUnreadOnly( + bool aDownloadUnreadOnly) { + m_downloadUnreadOnly = aDownloadUnreadOnly; + return NS_OK; +} + +/* attribute boolean downloadByDate; */ +NS_IMETHODIMP nsMsgDownloadSettings::GetDownloadByDate(bool* aDownloadByDate) { + NS_ENSURE_ARG_POINTER(aDownloadByDate); + *aDownloadByDate = m_downloadByDate; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDownloadSettings::SetDownloadByDate(bool aDownloadByDate) { + m_downloadByDate = aDownloadByDate; + return NS_OK; +} + +/* attribute long ageLimitOfMsgsToDownload; */ +NS_IMETHODIMP nsMsgDownloadSettings::GetAgeLimitOfMsgsToDownload( + uint32_t* ageLimitOfMsgsToDownload) { + NS_ENSURE_ARG_POINTER(ageLimitOfMsgsToDownload); + *ageLimitOfMsgsToDownload = m_ageLimitOfMsgsToDownload; + return NS_OK; +} +NS_IMETHODIMP nsMsgDownloadSettings::SetAgeLimitOfMsgsToDownload( + uint32_t ageLimitOfMsgsToDownload) { + m_ageLimitOfMsgsToDownload = ageLimitOfMsgsToDownload; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetDefaultViewFlags( + nsMsgViewFlagsTypeValue* aDefaultViewFlags) { + NS_ENSURE_ARG_POINTER(aDefaultViewFlags); + GetIntPref("mailnews.default_view_flags", aDefaultViewFlags); + if (*aDefaultViewFlags < nsMsgViewFlagsType::kNone || + *aDefaultViewFlags > + (nsMsgViewFlagsType::kThreadedDisplay | + nsMsgViewFlagsType::kShowIgnored | nsMsgViewFlagsType::kUnreadOnly | + nsMsgViewFlagsType::kExpandAll | nsMsgViewFlagsType::kGroupBySort)) + *aDefaultViewFlags = nsMsgViewFlagsType::kNone; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetDefaultSortType( + nsMsgViewSortTypeValue* aDefaultSortType) { + NS_ENSURE_ARG_POINTER(aDefaultSortType); + GetIntPref("mailnews.default_sort_type", aDefaultSortType); + if (*aDefaultSortType < nsMsgViewSortType::byDate || + *aDefaultSortType > nsMsgViewSortType::byAccount) + *aDefaultSortType = nsMsgViewSortType::byDate; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::GetDefaultSortOrder( + nsMsgViewSortOrderValue* aDefaultSortOrder) { + NS_ENSURE_ARG_POINTER(aDefaultSortOrder); + GetIntPref("mailnews.default_sort_order", aDefaultSortOrder); + if (*aDefaultSortOrder != nsMsgViewSortOrder::descending) + *aDefaultSortOrder = nsMsgViewSortOrder::ascending; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::ResetHdrCacheSize(uint32_t aSize) { + if (m_cacheSize > aSize) { + m_cacheSize = aSize; + ClearHdrCache(false); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDatabase::GetNewList(nsTArray<nsMsgKey>& aNewKeys) { + aNewKeys = m_newSet.Clone(); + return NS_OK; +} + +nsresult nsMsgDatabase::GetSearchResultsTable(const nsACString& searchFolderUri, + bool createIfMissing, + nsIMdbTable** table) { + mdb_kind kindToken; + mdb_count numTables; + mdb_bool mustBeUnique; + NS_ENSURE_TRUE(m_mdbStore, NS_ERROR_NULL_POINTER); + + nsresult err = m_mdbStore->StringToToken( + GetEnv(), PromiseFlatCString(searchFolderUri).get(), &kindToken); + err = m_mdbStore->GetTableKind(GetEnv(), m_hdrRowScopeToken, kindToken, + &numTables, &mustBeUnique, table); + if ((!*table || NS_FAILED(err)) && createIfMissing) + err = m_mdbStore->NewTable(GetEnv(), m_hdrRowScopeToken, kindToken, true, + nullptr, table); + + return *table ? err : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMsgDatabase::GetCachedHits(const nsACString& aSearchFolderUri, + nsIMsgEnumerator** aEnumerator) { + nsCOMPtr<nsIMdbTable> table; + (void)GetSearchResultsTable(aSearchFolderUri, false, getter_AddRefs(table)); + if (!table) return NS_ERROR_FAILURE; // expected result for no cached hits + NS_ADDREF(*aEnumerator = + new nsMsgDBEnumerator(this, table, nullptr, nullptr)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDatabase::RefreshCache(const nsACString& aSearchFolderUri, + nsTArray<nsMsgKey> const& aNewHits, + nsTArray<nsMsgKey>& aStaleHits) { + nsCOMPtr<nsIMdbTable> table; + nsresult err = + GetSearchResultsTable(aSearchFolderUri, true, getter_AddRefs(table)); + NS_ENSURE_SUCCESS(err, err); + // update the table so that it just contains aNewHits. + // And, keep track of the headers in the original table but not in aNewHits, + // so we can put those in aStaleHits. both aNewHits and the db table are + // sorted by uid/key. So, start at the beginning of the table and the aNewHits + // array. + uint32_t newHitIndex = 0; + uint32_t tableRowIndex = 0; + + uint32_t rowCount; + table->GetCount(GetEnv(), &rowCount); + aStaleHits.Clear(); + +#ifdef DEBUG + for (uint64_t i = 1; i < aNewHits.Length(); i++) { + NS_ASSERTION(aNewHits[i - 1] < aNewHits[i], + "cached hits for storage not sorted correctly"); + } +#endif + + while (newHitIndex < aNewHits.Length() || tableRowIndex < rowCount) { + mdbOid oid; + nsMsgKey tableRowKey = nsMsgKey_None; + if (tableRowIndex < rowCount) { + nsresult ret = table->PosToOid(GetEnv(), tableRowIndex, &oid); + if (NS_FAILED(ret)) { + tableRowIndex++; + continue; + } + tableRowKey = + oid.mOid_Id; // ### TODO need the real key for the 0th key problem. + } + + if (newHitIndex < aNewHits.Length() && + aNewHits[newHitIndex] == tableRowKey) { + newHitIndex++; + tableRowIndex++; + continue; + } else if (tableRowIndex >= rowCount || + (newHitIndex < aNewHits.Length() && + aNewHits[newHitIndex] < tableRowKey)) { + nsCOMPtr<nsIMdbRow> hdrRow; + mdbOid rowObjectId; + + rowObjectId.mOid_Id = aNewHits[newHitIndex]; + rowObjectId.mOid_Scope = m_hdrRowScopeToken; + err = m_mdbStore->GetRow(GetEnv(), &rowObjectId, getter_AddRefs(hdrRow)); + if (hdrRow) { + table->AddRow(GetEnv(), hdrRow); + mdb_pos newPos; + table->MoveRow(GetEnv(), hdrRow, rowCount, tableRowIndex, &newPos); + rowCount++; + tableRowIndex++; + } + newHitIndex++; + continue; + } else if (newHitIndex >= aNewHits.Length() || + aNewHits[newHitIndex] > tableRowKey) { + aStaleHits.AppendElement(tableRowKey); + table->CutOid(GetEnv(), &oid); + rowCount--; + continue; // don't increment tableRowIndex since we removed that row. + } + } + +#ifdef DEBUG_David_Bienvenu + printf("after refreshing cache\n"); + // iterate over table and assert that it's in id order + table->GetCount(GetEnv(), &rowCount); + mdbOid oid; + tableRowIndex = 0; + mdb_id prevId = 0; + while (tableRowIndex < rowCount) { + nsresult ret = table->PosToOid(m_mdbEnv, tableRowIndex++, &oid); + if (tableRowIndex > 1 && oid.mOid_Id <= prevId) { + NS_ASSERTION( + false, "inserting row into cached hits table, not sorted correctly"); + printf("key %lx is before or equal %lx\n", prevId, oid.mOid_Id); + } + prevId = oid.mOid_Id; + } + +#endif + Commit(nsMsgDBCommitType::kLargeCommit); + return NS_OK; +} + +// search sorted table +mdb_pos nsMsgDatabase::FindInsertIndexInSortedTable(nsIMdbTable* table, + mdb_id idToInsert) { + mdb_pos searchPos = 0; + uint32_t rowCount; + table->GetCount(GetEnv(), &rowCount); + mdb_pos hi = rowCount; + mdb_pos lo = 0; + + while (hi > lo) { + mdbOid outOid; + searchPos = (lo + hi - 1) / 2; + table->PosToOid(GetEnv(), searchPos, &outOid); + if (outOid.mOid_Id == idToInsert) { + NS_ASSERTION(false, "id shouldn't be in table"); + return hi; + } + if (outOid.mOid_Id > idToInsert) + hi = searchPos; + else // if (outOid.mOid_Id < idToInsert) + lo = searchPos + 1; + } + return hi; +} +NS_IMETHODIMP +nsMsgDatabase::UpdateHdrInCache(const nsACString& aSearchFolderUri, + nsIMsgDBHdr* aHdr, bool aAdd) { + nsCOMPtr<nsIMdbTable> table; + nsresult err = + GetSearchResultsTable(aSearchFolderUri, true, getter_AddRefs(table)); + NS_ENSURE_SUCCESS(err, err); + nsMsgKey key; + err = aHdr->GetMessageKey(&key); + nsMsgHdr* msgHdr = + static_cast<nsMsgHdr*>(aHdr); // closed system, so this is ok + nsIMdbRow* hdrRow = msgHdr->GetMDBRow(); + if (NS_SUCCEEDED(err) && m_mdbStore && hdrRow) { + if (!aAdd) { + table->CutRow(m_mdbEnv, hdrRow); + } else { + mdbOid rowId; + hdrRow->GetOid(m_mdbEnv, &rowId); + mdb_pos insertPos = FindInsertIndexInSortedTable(table, rowId.mOid_Id); + uint32_t rowCount; + table->GetCount(m_mdbEnv, &rowCount); + table->AddRow(m_mdbEnv, hdrRow); + mdb_pos newPos; + table->MoveRow(m_mdbEnv, hdrRow, rowCount, insertPos, &newPos); + } + } + + // if (aAdd) + // if we need to add this hdr, we need to insert it in key order. + return NS_OK; +} +NS_IMETHODIMP +nsMsgDatabase::HdrIsInCache(const nsACString& aSearchFolderUri, + nsIMsgDBHdr* aHdr, bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + nsCOMPtr<nsIMdbTable> table; + nsresult err = + GetSearchResultsTable(aSearchFolderUri, true, getter_AddRefs(table)); + NS_ENSURE_SUCCESS(err, err); + nsMsgKey key; + aHdr->GetMessageKey(&key); + mdbOid rowObjectId; + rowObjectId.mOid_Id = key; + rowObjectId.mOid_Scope = m_hdrRowScopeToken; + mdb_bool hasOid; + err = table->HasOid(GetEnv(), &rowObjectId, &hasOid); + *aResult = hasOid; + return err; +} diff --git a/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.cpp b/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.cpp new file mode 100644 index 0000000000..a64ef46f57 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.cpp @@ -0,0 +1,317 @@ +/* -*- 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 "nsMsgDatabaseEnumerators.h" +#include "nsMsgDatabase.h" +#include "nsIMsgHdr.h" +#include "nsMsgThread.h" + +/* + * nsMsgDBEnumerator implementation + */ + +nsMsgDBEnumerator::nsMsgDBEnumerator(nsMsgDatabase* db, nsIMdbTable* table, + nsMsgDBEnumeratorFilter filter, + void* closure, bool iterateForwards) + : mDB(db), + mDone(false), + mIterateForwards(iterateForwards), + mFilter(filter), + mClosure(closure) { + mTable = table; + mRowPos = 0; + mDB->m_msgEnumerators.AppendElement(this); +} + +nsMsgDBEnumerator::~nsMsgDBEnumerator() { Invalidate(); } + +void nsMsgDBEnumerator::Invalidate() { + // Order is important here. If the database is destroyed first, releasing + // the cursor will crash (due, I think, to a disconnect between XPCOM and + // Mork internal memory management). + mRowCursor = nullptr; + mTable = nullptr; + mResultHdr = nullptr; + mDone = true; + if (mDB) { + mDB->m_msgEnumerators.RemoveElement(this); + mDB = nullptr; + } +} + +nsresult nsMsgDBEnumerator::GetRowCursor() { + mDone = false; + + if (!mDB || !mTable) return NS_ERROR_NULL_POINTER; + + if (mIterateForwards) { + mRowPos = -1; + } else { + mdb_count numRows; + mTable->GetCount(mDB->GetEnv(), &numRows); + mRowPos = numRows; // startPos is 0 relative. + } + return mTable->GetTableRowCursor(mDB->GetEnv(), mRowPos, + getter_AddRefs(mRowCursor)); +} + +NS_IMETHODIMP nsMsgDBEnumerator::GetNext(nsIMsgDBHdr** aItem) { + if (!aItem) return NS_ERROR_NULL_POINTER; + *aItem = nullptr; + + // If we've already got one ready, return it. + if (mResultHdr) { + mResultHdr.forget(aItem); + return NS_OK; + } + + // Bail out if enumerator has been invalidated. + if (!mDB) { + return NS_ERROR_FAILURE; + } + + nsresult rv = InternalGetNext(aItem); + NS_ENSURE_SUCCESS(rv, rv); + return *aItem ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult nsMsgDBEnumerator::InternalGetNext(nsIMsgDBHdr** nextHdr) { + nsresult rv; + + *nextHdr = nullptr; + + if (!mRowCursor) { + rv = GetRowCursor(); + NS_ENSURE_SUCCESS(rv, rv); + } + + while (true) { + nsIMdbRow* hdrRow; + if (mIterateForwards) { + rv = mRowCursor->NextRow(mDB->GetEnv(), &hdrRow, &mRowPos); + } else { + rv = mRowCursor->PrevRow(mDB->GetEnv(), &hdrRow, &mRowPos); + } + NS_ENSURE_SUCCESS(rv, rv); + if (!hdrRow) { + // No more rows, so we're done. + *nextHdr = nullptr; + return NS_OK; + } + + // Get key from row + mdbOid outOid; + nsMsgKey key = nsMsgKey_None; + rv = hdrRow->GetOid(mDB->GetEnv(), &outOid); + NS_ENSURE_SUCCESS(rv, rv); + key = outOid.mOid_Id; + + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = mDB->CreateMsgHdr(hdrRow, key, getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + + // Ignore expunged messages. + uint32_t flags; + hdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Expunged) { + continue; + } + + // Ignore anything which doesn't pass the filter func (if there is one). + if (mFilter && NS_FAILED(mFilter(hdr, mClosure))) { + continue; + } + + // If we get this far, we've found it. + hdr.forget(nextHdr); + return NS_OK; + } +} + +NS_IMETHODIMP nsMsgDBEnumerator::HasMoreElements(bool* aResult) { + if (!aResult) return NS_ERROR_NULL_POINTER; + + if (!mResultHdr) { + // Bail out if enumerator has been invalidated. + if (!mDB) { + return NS_ERROR_FAILURE; + } + + nsresult rv = InternalGetNext(getter_AddRefs(mResultHdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (!mResultHdr) { + mDone = true; + } + } + + *aResult = !mDone; + return NS_OK; +} + +/* + * nsMsgFilteredDBEnumerator implementation + */ + +nsMsgFilteredDBEnumerator::nsMsgFilteredDBEnumerator(nsMsgDatabase* db, + nsIMdbTable* table, + bool reverse) + : nsMsgDBEnumerator(db, table, nullptr, nullptr, !reverse) {} + +nsMsgFilteredDBEnumerator::~nsMsgFilteredDBEnumerator() {} + +/** + * Create the search session for the enumerator, + * add the scope term for "folder" to the search session, and add the search + * terms in the array to the search session. + */ +nsresult nsMsgFilteredDBEnumerator::InitSearchSession( + const nsTArray<RefPtr<nsIMsgSearchTerm>>& searchTerms, + nsIMsgFolder* folder) { + nsresult rv; + m_searchSession = + do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail, folder); + for (auto searchTerm : searchTerms) { + m_searchSession->AppendTerm(searchTerm); + } + return NS_OK; +} + +nsresult nsMsgFilteredDBEnumerator::InternalGetNext(nsIMsgDBHdr** nextHdr) { + nsCOMPtr<nsIMsgDBHdr> hdr; + while (true) { + nsresult rv = nsMsgDBEnumerator::InternalGetNext(getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (!hdr) { + break; // No more. + } + bool matches; + rv = m_searchSession->MatchHdr(hdr, mDB, &matches); + NS_ENSURE_SUCCESS(rv, rv); + if (matches) { + break; // Found one! + } + } + hdr.forget(nextHdr); + return NS_OK; +} + +/* + * nsMsgDBThreadEnumerator implementation + */ + +nsMsgDBThreadEnumerator::nsMsgDBThreadEnumerator( + nsMsgDatabase* db, nsMsgDBThreadEnumeratorFilter filter) + : mDB(db), + mTableCursor(nullptr), + mResultThread(nullptr), + mDone(false), + mFilter(filter) { + mDB->m_threadEnumerators.AppendElement(this); + mNextPrefetched = false; +} + +nsMsgDBThreadEnumerator::~nsMsgDBThreadEnumerator() { Invalidate(); } + +void nsMsgDBThreadEnumerator::Invalidate() { + // Order is important here. If the database is destroyed first, releasing + // the cursor will crash (due, I think, to a disconnect between XPCOM and + // Mork internal memory management). + mTableCursor = nullptr; + mResultThread = nullptr; + mDone = true; + if (mDB) { + mDB->m_threadEnumerators.RemoveElement(this); + mDB = nullptr; + } +} + +nsresult nsMsgDBThreadEnumerator::GetTableCursor(void) { + nsresult rv = NS_OK; + + // DB might have disappeared. + if (!mDB || !mDB->m_mdbStore) return NS_ERROR_NULL_POINTER; + if (NS_FAILED(rv)) return rv; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBThreadEnumerator::HasMoreElements(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + if (!mNextPrefetched) { + PrefetchNext(); + } + *aResult = !mDone; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBThreadEnumerator::GetNext(nsIMsgThread** aItem) { + NS_ENSURE_ARG_POINTER(aItem); + + *aItem = nullptr; + nsresult rv = NS_OK; + if (!mNextPrefetched) rv = PrefetchNext(); + if (NS_SUCCEEDED(rv)) { + if (mResultThread) { + NS_ADDREF(*aItem = mResultThread); + mNextPrefetched = false; + } + } + return rv; +} + +nsresult nsMsgDBThreadEnumerator::PrefetchNext() { + nsresult rv; + + // DB might have disappeared. + if (!mDB || !mDB->m_mdbStore) { + return NS_ERROR_NULL_POINTER; + } + + if (!mTableCursor) { + rv = mDB->m_mdbStore->GetPortTableCursor( + mDB->GetEnv(), mDB->m_hdrRowScopeToken, mDB->m_threadTableKindToken, + getter_AddRefs(mTableCursor)); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIMdbTable> table; + while (true) { + mResultThread = nullptr; + rv = mTableCursor->NextTable(mDB->GetEnv(), getter_AddRefs(table)); + if (!table) { + mDone = true; + return NS_ERROR_FAILURE; + } + if (NS_FAILED(rv)) { + mDone = true; + return rv; + } + + mdbOid tableId; + table->GetOid(mDB->GetEnv(), &tableId); + + mResultThread = mDB->FindExistingThread(tableId.mOid_Id); + if (!mResultThread) mResultThread = new nsMsgThread(mDB, table); + + if (mResultThread) { + uint32_t numChildren = 0; + mResultThread->GetNumChildren(&numChildren); + // we've got empty thread; don't tell caller about it. + if (numChildren == 0) continue; + } + if (mFilter && NS_FAILED(mFilter(mResultThread))) + continue; + else + break; + } + if (mResultThread) { + mNextPrefetched = true; + return NS_OK; + } + return NS_ERROR_FAILURE; +} diff --git a/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.h b/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.h new file mode 100644 index 0000000000..f68b084611 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsMsgDatabaseEnumerators.h @@ -0,0 +1,133 @@ +/* -*- 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 _nsMsgDatabaseEnumerators_H_ +#define _nsMsgDatabaseEnumerators_H_ + +/* + * This file provides some enumerator classes, private to nsMsgDatabase. + * The outside world would only ever see these as nsIMsgEnumerator or + * nsIMsgThreadEnumerator. + * + * These enumerators automatically register themselves with the nsMsgDatabase + * during construction/destruction. This lets the database track all + * outstanding enumerators, so they can be invalidated if the database is + * destroyed or ForceClosed(). + * Due to this lifecycle coupling, we try to avoid using refcounted pointers + * here, as we don't want outstanding enumerators to lock an otherwise unused + * database in existence. + */ + +#include "nsMsgEnumerator.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "mdb.h" +#include "nsIDBChangeListener.h" + +#include "nsIMsgSearchTerm.h" +#include "nsIMsgSearchSession.h" + +class nsMsgDatabase; +class nsIMdbTable; +class nsIMdbTableRowCursor; +class nsIMsgFolder; + +/** + * Enumerates over messages, forwards or backward, with an optional filter fn. + */ +class nsMsgDBEnumerator : public nsBaseMsgEnumerator { + public: + // nsIMsgEnumerator support. + NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override; + NS_IMETHOD HasMoreElements(bool* aResult) override; + + // Function type for filtering which messages are enumerated. + typedef nsresult (*nsMsgDBEnumeratorFilter)(nsIMsgDBHdr* hdr, void* closure); + + nsMsgDBEnumerator(nsMsgDatabase* db, nsIMdbTable* table, + nsMsgDBEnumeratorFilter filter, void* closure, + bool iterateForwards = true); + // Called by db when no longer valid (db is being destroyed or ForcedClosed). + void Invalidate(); + + protected: + // internals + nsresult GetRowCursor(); + + // Returns next message or nullptr if none more. + virtual nsresult InternalGetNext(nsIMsgDBHdr** nextHdr); + + // Our source DB. Not refcounted, because we don't want to lock the DB + // in existence. The enumerator is registered with the DB, and the DB will + // call Invalidate() if it is destroyed or ForceClosed(). + nsMsgDatabase* mDB; + nsCOMPtr<nsIMdbTableRowCursor> mRowCursor; + mdb_pos mRowPos; + nsCOMPtr<nsIMsgDBHdr> mResultHdr; + bool mDone; + bool mIterateForwards; + nsMsgDBEnumeratorFilter mFilter; + nsIMdbTable* mTable; + void* mClosure; + + virtual ~nsMsgDBEnumerator() override; +}; + +/** + * Enumerate over messages which match the given search terms. + */ +class nsMsgFilteredDBEnumerator : public nsMsgDBEnumerator { + public: + nsMsgFilteredDBEnumerator(nsMsgDatabase* db, nsIMdbTable* table, + bool reverse); + virtual ~nsMsgFilteredDBEnumerator() override; + nsresult InitSearchSession( + const nsTArray<RefPtr<nsIMsgSearchTerm>>& searchTerms, + nsIMsgFolder* folder); + + protected: + virtual nsresult InternalGetNext(nsIMsgDBHdr** nextHdr) override; + + nsCOMPtr<nsIMsgSearchSession> m_searchSession; +}; + +/** + * Helper class for fetching message threads from a database. + * This enumerator automatically registers itself with the nsMsgDatabase. + * If the DB is destroyed or ForceClosed() it will call the enumerators + * Invalidate() method. + */ +class nsMsgDBThreadEnumerator : public nsBaseMsgThreadEnumerator { + public: + // nsIMsgThreadEnumerator support. + NS_IMETHOD GetNext(nsIMsgThread** aItem) override; + NS_IMETHOD HasMoreElements(bool* aResult) override; + + // Function type for filtering threads that appear in the enumeration. + typedef nsresult (*nsMsgDBThreadEnumeratorFilter)(nsIMsgThread* thread); + + nsMsgDBThreadEnumerator(nsMsgDatabase* db, + nsMsgDBThreadEnumeratorFilter filter); + + // Called by DB when being destroyed or ForcedClosed. + void Invalidate(); + + protected: + virtual ~nsMsgDBThreadEnumerator(); + nsresult GetTableCursor(void); + nsresult PrefetchNext(); + + // Our source DB. Not refcounted, because we don't want to lock the DB + // in existence. The enumerator is registered with the DB, and the DB will + // call Invalidate() if it is destroyed or ForceClosed(). + nsMsgDatabase* mDB; + nsCOMPtr<nsIMdbPortTableCursor> mTableCursor; + RefPtr<nsIMsgThread> mResultThread; + bool mDone; + bool mNextPrefetched; + nsMsgDBThreadEnumeratorFilter mFilter; +}; + +#endif // _nsMsgDatabaseEnumerators_H_ diff --git a/comm/mailnews/db/msgdb/src/nsMsgHdr.cpp b/comm/mailnews/db/msgdb/src/nsMsgHdr.cpp new file mode 100644 index 0000000000..c3d36d9b9b --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsMsgHdr.cpp @@ -0,0 +1,936 @@ +/* -*- 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 "mozilla/mailnews/MimeHeaderParser.h" +#include "nsMsgHdr.h" +#include "nsMsgDatabase.h" +#include "nsMsgUtils.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgThread.h" +#include "mozilla/Attributes.h" +#include "nsStringEnumerator.h" +#ifdef DEBUG +# include "nsPrintfCString.h" +#endif +using namespace mozilla::mailnews; + +NS_IMPL_ISUPPORTS(nsMsgHdr, nsIMsgDBHdr) + +#define FLAGS_INITED 0x1 +#define CACHED_VALUES_INITED 0x2 +#define REFERENCES_INITED 0x4 +#define THREAD_PARENT_INITED 0x8 + +nsMsgHdr::nsMsgHdr(nsMsgDatabase* db, nsIMdbRow* dbRow) { + m_mdb = db; + Init(); + m_mdbRow = dbRow; + if (m_mdb) { + NS_ADDREF(m_mdb); // Released in DTOR. + mdbOid outOid; + if (dbRow && NS_SUCCEEDED(dbRow->GetOid(m_mdb->GetEnv(), &outOid))) { + m_messageKey = outOid.mOid_Id; + m_mdb->AddHdrToUseCache((nsIMsgDBHdr*)this, m_messageKey); + } + } +} + +void nsMsgHdr::Init() { + m_initedValues = 0; + m_messageKey = nsMsgKey_None; + m_messageSize = 0; + m_date = 0; + m_flags = 0; + m_mdbRow = NULL; + m_threadId = nsMsgKey_None; + m_threadParent = nsMsgKey_None; +} + +nsresult nsMsgHdr::InitCachedValues() { + nsresult err = NS_OK; + + if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER; + + if (!(m_initedValues & CACHED_VALUES_INITED)) { + uint32_t uint32Value; + mdbOid outOid; + if (NS_SUCCEEDED(m_mdbRow->GetOid(m_mdb->GetEnv(), &outOid))) + m_messageKey = outOid.mOid_Id; + + err = GetUInt32Column(m_mdb->m_messageSizeColumnToken, &m_messageSize); + + err = GetUInt32Column(m_mdb->m_dateColumnToken, &uint32Value); + Seconds2PRTime(uint32Value, &m_date); + + err = GetUInt32Column(m_mdb->m_messageThreadIdColumnToken, &m_threadId); + + if (NS_SUCCEEDED(err)) m_initedValues |= CACHED_VALUES_INITED; + } + return err; +} + +nsresult nsMsgHdr::InitFlags() { + nsresult err = NS_OK; + + if (!m_mdb) return NS_ERROR_NULL_POINTER; + + if (!(m_initedValues & FLAGS_INITED)) { + err = GetUInt32Column(m_mdb->m_flagsColumnToken, &m_flags); + m_flags &= ~nsMsgMessageFlags::New; // don't get new flag from MDB + + if (NS_SUCCEEDED(err)) m_initedValues |= FLAGS_INITED; + } + + return err; +} + +nsMsgHdr::~nsMsgHdr() { + if (m_mdbRow) { + if (m_mdb) { + NS_RELEASE(m_mdbRow); + m_mdb->RemoveHdrFromUseCache((nsIMsgDBHdr*)this, m_messageKey); + } + } + NS_IF_RELEASE(m_mdb); +} + +NS_IMETHODIMP nsMsgHdr::GetMessageKey(nsMsgKey* result) { + if (m_messageKey == nsMsgKey_None && m_mdbRow != NULL) { + mdbOid outOid; + if (NS_SUCCEEDED(m_mdbRow->GetOid(m_mdb->GetEnv(), &outOid))) + m_messageKey = outOid.mOid_Id; + } + *result = m_messageKey; + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetThreadId(nsMsgKey* result) { + if (!(m_initedValues & CACHED_VALUES_INITED)) InitCachedValues(); + + if (result) { + *result = m_threadId; + return NS_OK; + } + return NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsMsgHdr::SetThreadId(nsMsgKey inKey) { + m_threadId = inKey; + return SetUInt32Column(m_threadId, m_mdb->m_messageThreadIdColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetMessageKey(nsMsgKey value) { + m_messageKey = value; + return NS_OK; +} + +nsresult nsMsgHdr::GetRawFlags(uint32_t* result) { + if (!(m_initedValues & FLAGS_INITED)) InitFlags(); + *result = m_flags; + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetFlags(uint32_t* result) { + if (!(m_initedValues & FLAGS_INITED)) InitFlags(); + if (m_mdb) + *result = m_mdb->GetStatusFlags(this, m_flags); + else + *result = m_flags; +#ifdef DEBUG_bienvenu + NS_ASSERTION(!(*result & (nsMsgMessageFlags::Elided)), + "shouldn't be set in db"); +#endif + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::SetFlags(uint32_t flags) { +#ifdef DEBUG_bienvenu + NS_ASSERTION(!(flags & (nsMsgMessageFlags::Elided)), + "shouldn't set this flag on db"); +#endif + m_initedValues |= FLAGS_INITED; + m_flags = flags; + // don't write out nsMsgMessageFlags::New to MDB. + return SetUInt32Column(m_flags & ~nsMsgMessageFlags::New, + m_mdb->m_flagsColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::OrFlags(uint32_t flags, uint32_t* result) { + if (!(m_initedValues & FLAGS_INITED)) InitFlags(); + if ((m_flags & flags) != flags) SetFlags(m_flags | flags); + *result = m_flags; + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::AndFlags(uint32_t flags, uint32_t* result) { + if (!(m_initedValues & FLAGS_INITED)) InitFlags(); + if ((m_flags & flags) != m_flags) SetFlags(m_flags & flags); + *result = m_flags; + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::MarkHasAttachments(bool bHasAttachments) { + nsresult rv = NS_OK; + + if (m_mdb) { + nsMsgKey key; + rv = GetMessageKey(&key); + if (NS_SUCCEEDED(rv)) + rv = m_mdb->MarkHasAttachments(key, bHasAttachments, nullptr); + } + return rv; +} + +NS_IMETHODIMP nsMsgHdr::MarkRead(bool bRead) { + nsresult rv = NS_OK; + + if (m_mdb) { + nsMsgKey key; + rv = GetMessageKey(&key); + if (NS_SUCCEEDED(rv)) rv = m_mdb->MarkRead(key, bRead, nullptr); + } + return rv; +} + +NS_IMETHODIMP nsMsgHdr::MarkFlagged(bool bFlagged) { + nsresult rv = NS_OK; + + if (m_mdb) { + nsMsgKey key; + rv = GetMessageKey(&key); + if (NS_SUCCEEDED(rv)) rv = m_mdb->MarkMarked(key, bFlagged, nullptr); + } + return rv; +} + +NS_IMETHODIMP nsMsgHdr::SetStringProperty(const char* propertyName, + const nsACString& propertyValue) { + NS_ENSURE_ARG_POINTER(propertyName); + if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER; + return m_mdb->SetProperty(m_mdbRow, propertyName, + PromiseFlatCString(propertyValue).get()); +} + +NS_IMETHODIMP nsMsgHdr::GetStringProperty(const char* propertyName, + nsACString& aPropertyValue) { + NS_ENSURE_ARG_POINTER(propertyName); + if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER; + return m_mdb->GetProperty(m_mdbRow, propertyName, + getter_Copies(aPropertyValue)); +} + +NS_IMETHODIMP nsMsgHdr::GetUint32Property(const char* propertyName, + uint32_t* pResult) { + NS_ENSURE_ARG_POINTER(propertyName); + if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER; + return m_mdb->GetUint32Property(m_mdbRow, propertyName, pResult); +} + +NS_IMETHODIMP nsMsgHdr::SetUint32Property(const char* propertyName, + uint32_t value) { + NS_ENSURE_ARG_POINTER(propertyName); + if (!m_mdb || !m_mdbRow) return NS_ERROR_NULL_POINTER; + return m_mdb->SetUint32Property(m_mdbRow, propertyName, value); +} + +NS_IMETHODIMP nsMsgHdr::GetNumReferences(uint16_t* result) { + if (!(m_initedValues & REFERENCES_INITED)) { + const char* references; + if (NS_SUCCEEDED(m_mdb->RowCellColumnToConstCharPtr( + GetMDBRow(), m_mdb->m_referencesColumnToken, &references))) + ParseReferences(references); + m_initedValues |= REFERENCES_INITED; + } + + if (result) *result = m_references.Length(); + // there is no real failure here; if there are no references, there are no + // references. + return NS_OK; +} + +nsresult nsMsgHdr::ParseReferences(const char* references) { + const char* startNextRef = references; + nsAutoCString resultReference; + nsCString messageId; + GetMessageId(getter_Copies(messageId)); + + while (startNextRef && *startNextRef) { + startNextRef = GetNextReference(startNextRef, resultReference, + startNextRef == references); + // Don't add self-references. + if (!resultReference.IsEmpty() && !resultReference.Equals(messageId)) + m_references.AppendElement(resultReference); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetStringReference(int32_t refNum, + nsACString& resultReference) { + nsresult err = NS_OK; + + if (!(m_initedValues & REFERENCES_INITED)) + GetNumReferences(nullptr); // it can handle the null + + if ((uint32_t)refNum < m_references.Length()) + resultReference = m_references.ElementAt(refNum); + else + err = NS_ERROR_ILLEGAL_VALUE; + return err; +} + +NS_IMETHODIMP nsMsgHdr::GetDate(PRTime* result) { + if (!(m_initedValues & CACHED_VALUES_INITED)) InitCachedValues(); + + *result = m_date; + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetDateInSeconds(uint32_t* aResult) { + return GetUInt32Column(m_mdb->m_dateColumnToken, aResult); +} + +NS_IMETHODIMP nsMsgHdr::SetMessageId(const char* messageId) { + if (messageId && *messageId == '<') { + nsAutoCString tempMessageID(messageId + 1); + if (tempMessageID.CharAt(tempMessageID.Length() - 1) == '>') + tempMessageID.SetLength(tempMessageID.Length() - 1); + return SetStringColumn(tempMessageID.get(), m_mdb->m_messageIdColumnToken); + } + return SetStringColumn(messageId, m_mdb->m_messageIdColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetSubject(const nsACString& subject) { + return SetStringColumn(PromiseFlatCString(subject).get(), + m_mdb->m_subjectColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetAuthor(const char* author) { + return SetStringColumn(author, m_mdb->m_senderColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetReferences(const nsACString& references) { + m_references.Clear(); + ParseReferences(PromiseFlatCString(references).get()); + + m_initedValues |= REFERENCES_INITED; + + return SetStringColumn(PromiseFlatCString(references).get(), + m_mdb->m_referencesColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetRecipients(const char* recipients) { + // need to put in rfc822 address parsing code here (or make caller do it...) + return SetStringColumn(recipients, m_mdb->m_recipientsColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetCcList(const char* ccList) { + return SetStringColumn(ccList, m_mdb->m_ccListColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetBccList(const char* bccList) { + return SetStringColumn(bccList, m_mdb->m_bccListColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetMessageSize(uint32_t messageSize) { + SetUInt32Column(messageSize, m_mdb->m_messageSizeColumnToken); + m_messageSize = messageSize; + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetOfflineMessageSize(uint32_t* result) { + uint32_t size; + nsresult res = GetUInt32Column(m_mdb->m_offlineMessageSizeColumnToken, &size); + + *result = size; + return res; +} + +NS_IMETHODIMP nsMsgHdr::SetOfflineMessageSize(uint32_t messageSize) { + return SetUInt32Column(messageSize, m_mdb->m_offlineMessageSizeColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetLineCount(uint32_t lineCount) { + SetUInt32Column(lineCount, m_mdb->m_numLinesColumnToken); + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::SetDate(PRTime date) { + m_date = date; + uint32_t seconds; + PRTime2Seconds(date, &seconds); + return SetUInt32Column((uint32_t)seconds, m_mdb->m_dateColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::SetPriority(nsMsgPriorityValue priority) { + SetUInt32Column((uint32_t)priority, m_mdb->m_priorityColumnToken); + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetPriority(nsMsgPriorityValue* result) { + if (!result) return NS_ERROR_NULL_POINTER; + + uint32_t priority = 0; + nsresult rv = GetUInt32Column(m_mdb->m_priorityColumnToken, &priority); + if (NS_FAILED(rv)) return rv; + + *result = (nsMsgPriorityValue)priority; + return NS_OK; +} + +// I'd like to not store the account key, if the msg is in +// the same account as it was received in, to save disk space and memory. +// This might be problematic when a message gets moved... +// And I'm not sure if we should short circuit it here, +// or at a higher level where it might be more efficient. +NS_IMETHODIMP nsMsgHdr::SetAccountKey(const char* aAccountKey) { + return SetStringProperty("account", nsDependentCString(aAccountKey)); +} + +NS_IMETHODIMP nsMsgHdr::GetAccountKey(char** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + nsCString key; + nsresult rv = GetStringProperty("account", key); + NS_ENSURE_SUCCESS(rv, rv); + *aResult = ToNewCString(key); + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetMessageOffset(uint64_t* result) { + NS_ENSURE_ARG(result); + + (void)GetUInt64Column(m_mdb->m_offlineMsgOffsetColumnToken, result, + (unsigned)-1); + if (*result == (unsigned)-1) { + // It's unset. Unfortunately there's not much we can do here. There's + // a lot of code which relies on being able to read .messageOffset, + // even if it doesn't require it to return anything sensible. + // (For example - in js unit tests - Assert.equals() stringifies the + // attributes of it's expected/actual values to produce an error + // message even if the assert passes). +#ifdef DEBUG + nsCString tok; + GetStringProperty("storeToken", tok); + nsPrintfCString err("Missing .messageOffset (key=%u, storeToken='%s')", + m_messageKey, tok.get()); + NS_WARNING(err.get()); +#endif + // Return something obviously broken. + *result = 12345678; + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::SetMessageOffset(uint64_t offset) { + SetUInt64Column(offset, m_mdb->m_offlineMsgOffsetColumnToken); + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetMessageSize(uint32_t* result) { + uint32_t size; + nsresult res = GetUInt32Column(m_mdb->m_messageSizeColumnToken, &size); + + *result = size; + return res; +} + +NS_IMETHODIMP nsMsgHdr::GetLineCount(uint32_t* result) { + uint32_t linecount; + nsresult res = GetUInt32Column(m_mdb->m_numLinesColumnToken, &linecount); + *result = linecount; + return res; +} + +NS_IMETHODIMP nsMsgHdr::GetAuthor(char** resultAuthor) { + return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_senderColumnToken, + resultAuthor); +} + +NS_IMETHODIMP nsMsgHdr::GetSubject(nsACString& resultSubject) { + return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_subjectColumnToken, + getter_Copies(resultSubject)); +} + +NS_IMETHODIMP nsMsgHdr::GetRecipients(char** resultRecipients) { + return m_mdb->RowCellColumnToCharPtr( + GetMDBRow(), m_mdb->m_recipientsColumnToken, resultRecipients); +} + +NS_IMETHODIMP nsMsgHdr::GetCcList(char** resultCCList) { + return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_ccListColumnToken, + resultCCList); +} + +NS_IMETHODIMP nsMsgHdr::GetBccList(char** resultBCCList) { + return m_mdb->RowCellColumnToCharPtr(GetMDBRow(), m_mdb->m_bccListColumnToken, + resultBCCList); +} + +NS_IMETHODIMP nsMsgHdr::GetMessageId(char** resultMessageId) { + return m_mdb->RowCellColumnToCharPtr( + GetMDBRow(), m_mdb->m_messageIdColumnToken, resultMessageId); +} + +NS_IMETHODIMP nsMsgHdr::GetMime2DecodedAuthor(nsAString& resultAuthor) { + return m_mdb->RowCellColumnToMime2DecodedString( + GetMDBRow(), m_mdb->m_senderColumnToken, resultAuthor); +} + +NS_IMETHODIMP nsMsgHdr::GetMime2DecodedSubject(nsAString& resultSubject) { + return m_mdb->RowCellColumnToMime2DecodedString( + GetMDBRow(), m_mdb->m_subjectColumnToken, resultSubject); +} + +NS_IMETHODIMP nsMsgHdr::GetMime2DecodedRecipients(nsAString& resultRecipients) { + return m_mdb->RowCellColumnToMime2DecodedString( + GetMDBRow(), m_mdb->m_recipientsColumnToken, resultRecipients); +} + +NS_IMETHODIMP nsMsgHdr::GetAuthorCollationKey(nsTArray<uint8_t>& resultAuthor) { + return m_mdb->RowCellColumnToAddressCollationKey( + GetMDBRow(), m_mdb->m_senderColumnToken, resultAuthor); +} + +NS_IMETHODIMP nsMsgHdr::GetSubjectCollationKey( + nsTArray<uint8_t>& resultSubject) { + return m_mdb->RowCellColumnToCollationKey( + GetMDBRow(), m_mdb->m_subjectColumnToken, resultSubject); +} + +NS_IMETHODIMP nsMsgHdr::GetRecipientsCollationKey( + nsTArray<uint8_t>& resultRecipients) { + return m_mdb->RowCellColumnToCollationKey( + GetMDBRow(), m_mdb->m_recipientsColumnToken, resultRecipients); +} + +NS_IMETHODIMP nsMsgHdr::GetCharset(char** aCharset) { + return m_mdb->RowCellColumnToCharPtr( + GetMDBRow(), m_mdb->m_messageCharSetColumnToken, aCharset); +} + +NS_IMETHODIMP nsMsgHdr::SetCharset(const char* aCharset) { + return SetStringColumn(aCharset, m_mdb->m_messageCharSetColumnToken); +} + +NS_IMETHODIMP nsMsgHdr::GetEffectiveCharset(nsACString& resultCharset) { + return m_mdb->GetEffectiveCharset(m_mdbRow, resultCharset); +} + +NS_IMETHODIMP nsMsgHdr::SetThreadParent(nsMsgKey inKey) { + m_threadParent = inKey; + if (inKey == m_messageKey) NS_ASSERTION(false, "can't be your own parent"); + SetUInt32Column(m_threadParent, m_mdb->m_threadParentColumnToken); + m_initedValues |= THREAD_PARENT_INITED; + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetThreadParent(nsMsgKey* result) { + nsresult res; + if (!(m_initedValues & THREAD_PARENT_INITED)) { + res = GetUInt32Column(m_mdb->m_threadParentColumnToken, &m_threadParent, + nsMsgKey_None); + if (NS_SUCCEEDED(res)) m_initedValues |= THREAD_PARENT_INITED; + } + *result = m_threadParent; + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetFolder(nsIMsgFolder** result) { + NS_ENSURE_ARG(result); + + if (m_mdb && m_mdb->m_folder) { + NS_ADDREF(*result = m_mdb->m_folder); + } else + *result = nullptr; + return NS_OK; +} + +nsresult nsMsgHdr::SetStringColumn(const char* str, mdb_token token) { + NS_ENSURE_ARG_POINTER(str); + return m_mdb->CharPtrToRowCellColumn(m_mdbRow, token, str); +} + +nsresult nsMsgHdr::SetUInt32Column(uint32_t value, mdb_token token) { + return m_mdb->UInt32ToRowCellColumn(m_mdbRow, token, value); +} + +nsresult nsMsgHdr::GetUInt32Column(mdb_token token, uint32_t* pvalue, + uint32_t defaultValue) { + return m_mdb->RowCellColumnToUInt32(GetMDBRow(), token, pvalue, defaultValue); +} + +nsresult nsMsgHdr::SetUInt64Column(uint64_t value, mdb_token token) { + return m_mdb->UInt64ToRowCellColumn(m_mdbRow, token, value); +} + +nsresult nsMsgHdr::GetUInt64Column(mdb_token token, uint64_t* pvalue, + uint64_t defaultValue) { + return m_mdb->RowCellColumnToUInt64(GetMDBRow(), token, pvalue, defaultValue); +} + +/** + * Roughly speaking, get the next message-id (starts with a '<' ends with a + * '>'). Except, we also try to handle the case where your reference is of + * a prehistoric vintage that just stuck any old random junk in there. Our + * old logic would (unintentionally?) just trim the whitespace off the front + * and hand you everything after that. We change things at all because that + * same behaviour does not make sense if we have already seen a proper message + * id. We keep the old behaviour at all because it would seem to have + * benefits. (See jwz's non-zero stats: http://www.jwz.org/doc/threading.html) + * So, to re-state, if there is a valid message-id in there at all, we only + * return valid message-id's (sans bracketing '<' and '>'). If there isn't, + * our result (via "references") is a left-trimmed copy of the string. If + * there is nothing in there, our result is an empty string.) We do require + * that you pass allowNonDelimitedReferences what it demands, though. + * For example: "<valid@stuff> this stuff is invalid" would net you + * "valid@stuff" and "this stuff is invalid" as results. We now only would + * provide "valid-stuff" and an empty string (which you should ignore) as + * results. However "this stuff is invalid" would return itself, allowing + * anything relying on that behaviour to keep working. + * + * Note: We accept anything inside the '<' and '>'; technically, we should want + * at least a '@' in there (per rfc 2822). But since we're going out of our + * way to support weird things... + * + * @param startNextRef The position to start at; this should either be the start + * of your references string or our return value from a previous call. + * @param reference You pass a nsCString by reference, we put the reference we + * find in it, if we find one. It may be empty! Beware! + * @param allowNonDelimitedReferences Should we support the + * pre-reasonable-standards form of In-Reply-To where it could be any + * arbitrary string and our behaviour was just to take off leading + * whitespace. It only makes sense to pass true for your first call to this + * function, as if you are around to make a second call, it means we found + * a properly formatted message-id and so we should only look for more + * properly formatted message-ids. + * NOTE: this option will also strip off a single leading '<' if there is + * one. Some examples: + * " foo" => "foo" + * " <bar" => "bar" + * "<<<foo" => "<<foo" + * "<foo@bar>" => "foo@bar" (completed message-id) + * @returns The next starting position of this routine, which may be pointing at + * a nul '\0' character to indicate termination. + */ +const char* nsMsgHdr::GetNextReference(const char* startNextRef, + nsCString& reference, + bool acceptNonDelimitedReferences) { + const char* ptr = startNextRef; + const char* whitespaceEndedAt = nullptr; + const char* firstMessageIdChar = nullptr; + + // make the reference result string empty by default; we will set it to + // something valid if the time comes. + reference.Truncate(); + + // walk until we find a '<', but keep track of the first point we found that + // was not whitespace (as defined by previous versions of this code.) + for (bool foundLessThan = false; !foundLessThan; ptr++) { + switch (*ptr) { + case '\0': + // if we are at the end of the string, we found some non-whitespace, and + // the caller requested that we accept non-delimited whitespace, + // give them that as their reference. (otherwise, leave it empty) + if (acceptNonDelimitedReferences && whitespaceEndedAt) + reference = whitespaceEndedAt; + return ptr; + case ' ': + case '\r': + case '\n': + case '\t': + // do nothing, make default case mean you didn't get whitespace + break; + case '<': + firstMessageIdChar = ptr + 1; // skip over the '<' + foundLessThan = true; // (flag to stop) + // Ensure whitespaceEndedAt skips the leading '<' and is set to + // a non-NULL value, just in case the message-id is not valid (no '>') + // and the old-school support is desired. + if (!whitespaceEndedAt) whitespaceEndedAt = ptr + 1; + break; + default: + if (!whitespaceEndedAt) whitespaceEndedAt = ptr; + break; + } + } + + // keep going until we hit a '>' or hit the end of the string + for (; *ptr; ptr++) { + if (*ptr == '>') { + // it's valid, update reference, making sure to stop before the '>' + reference.Assign(firstMessageIdChar, ptr - firstMessageIdChar); + // and return a start point just after the '>' + return ++ptr; + } + } + + // we did not have a fully-formed, valid message-id, so consider falling back + if (acceptNonDelimitedReferences && whitespaceEndedAt) + reference = whitespaceEndedAt; + return ptr; +} + +bool nsMsgHdr::IsParentOf(nsIMsgDBHdr* possibleChild) { + uint16_t referenceToCheck = 0; + possibleChild->GetNumReferences(&referenceToCheck); + nsAutoCString reference; + + nsCString messageId; + 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; + if (!m_mdb) break; + (void)m_mdb->GetMsgHdrForMessageID(reference.get(), getter_AddRefs(refHdr)); + if (refHdr) break; + referenceToCheck--; + } + return false; +} + +bool nsMsgHdr::IsAncestorOf(nsIMsgDBHdr* possibleChild) { + const char* references; + nsMsgHdr* curHdr = + static_cast<nsMsgHdr*>(possibleChild); // closed system, cast ok + m_mdb->RowCellColumnToConstCharPtr( + curHdr->GetMDBRow(), m_mdb->m_referencesColumnToken, &references); + if (!references) return false; + + nsCString messageId; + // should put < > around message id to make strstr strictly match + GetMessageId(getter_Copies(messageId)); + return (strstr(references, messageId.get()) != nullptr); +} + +NS_IMETHODIMP nsMsgHdr::GetIsRead(bool* isRead) { + NS_ENSURE_ARG_POINTER(isRead); + if (!(m_initedValues & FLAGS_INITED)) InitFlags(); + *isRead = !!(m_flags & nsMsgMessageFlags::Read); + return NS_OK; +} + +NS_IMETHODIMP nsMsgHdr::GetIsFlagged(bool* isFlagged) { + NS_ENSURE_ARG_POINTER(isFlagged); + if (!(m_initedValues & FLAGS_INITED)) InitFlags(); + *isFlagged = !!(m_flags & nsMsgMessageFlags::Marked); + return NS_OK; +} + +void nsMsgHdr::ReparentInThread(nsIMsgThread* thread) { + NS_WARNING("Borked message header, attempting to fix!"); + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + // bail out early for the singleton thread case. + if (numChildren == 1) { + SetThreadParent(nsMsgKey_None); + return; + } else { + nsCOMPtr<nsIMsgDBHdr> curHdr; + // loop through thread, looking for our proper parent. + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + thread->GetChildHdrAt(childIndex, getter_AddRefs(curHdr)); + // closed system, cast ok + nsMsgHdr* curMsgHdr = static_cast<nsMsgHdr*>(curHdr.get()); + if (curHdr && curMsgHdr->IsParentOf(this)) { + nsMsgKey curHdrKey; + curHdr->GetMessageKey(&curHdrKey); + SetThreadParent(curHdrKey); + return; + } + } + // we didn't find it. So either the root header is our parent, + // or we're the root. + nsCOMPtr<nsIMsgDBHdr> rootHdr; + thread->GetRootHdr(getter_AddRefs(rootHdr)); + NS_ASSERTION(rootHdr, "thread has no root hdr - shouldn't happen"); + if (rootHdr) { + nsMsgKey rootKey; + rootHdr->GetMessageKey(&rootKey); + // if we're the root, our thread parent is -1. + SetThreadParent(rootKey == m_messageKey ? nsMsgKey_None : rootKey); + } + } +} + +bool nsMsgHdr::IsAncestorKilled(uint32_t ancestorsToCheck) { + if (!(m_initedValues & FLAGS_INITED)) InitFlags(); + bool isKilled = m_flags & nsMsgMessageFlags::Ignored; + + if (!isKilled) { + nsMsgKey threadParent; + GetThreadParent(&threadParent); + + if (threadParent == m_messageKey) { + // isKilled is false by virtue of the enclosing if statement + NS_ERROR("Thread is parent of itself, please fix!"); + nsCOMPtr<nsIMsgThread> thread; + (void)m_mdb->GetThreadContainingMsgHdr(this, getter_AddRefs(thread)); + if (!thread) return false; + ReparentInThread(thread); + // Something's wrong, but the problem happened some time ago, so erroring + // out now is probably not a good idea. Ergo, we'll pretend to be OK, show + // the user the thread (err on the side of caution), and let the assertion + // alert debuggers to a problem. + return false; + } + if (threadParent != nsMsgKey_None) { + nsCOMPtr<nsIMsgDBHdr> parentHdr; + (void)m_mdb->GetMsgHdrForKey(threadParent, getter_AddRefs(parentHdr)); + + if (parentHdr) { + // More proofing against crashers. This crasher was derived from the + // fact that something got borked, leaving is in hand with a circular + // reference to borked headers inducing these loops. The defining + // characteristic of these headers is that they don't actually seat + // themselves in the thread properly. + nsCOMPtr<nsIMsgThread> thread; + (void)m_mdb->GetThreadContainingMsgHdr(this, getter_AddRefs(thread)); + if (thread) { + nsCOMPtr<nsIMsgDBHdr> claimant; + (void)thread->GetChild(threadParent, getter_AddRefs(claimant)); + if (!claimant) { + // attempt to reparent, and say the thread isn't killed, + // erring on the side of safety. + ReparentInThread(thread); + return false; + } + } + + if (!ancestorsToCheck) { + // We think we have a parent, but we have no more ancestors to check + NS_ASSERTION(false, "cycle in parent relationship, please fix!"); + return false; + } + // closed system, cast ok + nsMsgHdr* parent = static_cast<nsMsgHdr*>(parentHdr.get()); + return parent->IsAncestorKilled(ancestorsToCheck - 1); + } + } + } + return isKilled; +} + +NS_IMETHODIMP nsMsgHdr::GetIsKilled(bool* isKilled) { + NS_ENSURE_ARG_POINTER(isKilled); + *isKilled = false; + nsCOMPtr<nsIMsgThread> thread; + (void)m_mdb->GetThreadContainingMsgHdr(this, getter_AddRefs(thread)); + // if we can't find the thread, let's at least check one level; maybe + // the header hasn't been added to a thread yet. + uint32_t numChildren = 1; + if (thread) thread->GetNumChildren(&numChildren); + if (!numChildren) return NS_ERROR_FAILURE; + // We can't have as many ancestors as there are messages in the thread, + // so tell IsAncestorKilled to only check numChildren - 1 ancestors. + *isKilled = IsAncestorKilled(numChildren - 1); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// + +#include "nsIStringEnumerator.h" +#define NULL_MORK_COLUMN 0 +class nsMsgPropertyEnumerator : public nsStringEnumeratorBase { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIUTF8STRINGENUMERATOR + + using nsStringEnumeratorBase::GetNext; + + explicit nsMsgPropertyEnumerator(nsMsgHdr* aHdr); + void PrefetchNext(); + + protected: + virtual ~nsMsgPropertyEnumerator(); + nsCOMPtr<nsIMdbRowCellCursor> mRowCellCursor; + nsCOMPtr<nsIMdbEnv> m_mdbEnv; + nsCOMPtr<nsIMdbStore> m_mdbStore; + // Hold a reference to the hdr so it will hold an xpcom reference to the + // underlying mdb row. The row cell cursor will crash if the underlying + // row goes away. + RefPtr<nsMsgHdr> m_hdr; + bool mNextPrefetched; + mdb_column mNextColumn; +}; + +nsMsgPropertyEnumerator::nsMsgPropertyEnumerator(nsMsgHdr* aHdr) + : mNextPrefetched(false), mNextColumn(NULL_MORK_COLUMN) { + RefPtr<nsMsgDatabase> mdb; + nsCOMPtr<nsIMdbRow> mdbRow; + + if (aHdr && (mdbRow = aHdr->GetMDBRow()) && (m_hdr = aHdr) && + (mdb = aHdr->GetMdb()) && (m_mdbEnv = mdb->m_mdbEnv) && + (m_mdbStore = mdb->m_mdbStore)) { + mdbRow->GetRowCellCursor(m_mdbEnv, -1, getter_AddRefs(mRowCellCursor)); + } +} + +nsMsgPropertyEnumerator::~nsMsgPropertyEnumerator() { + // Need to clear this before the nsMsgHdr and its corresponding + // nsIMdbRow potentially go away. + mRowCellCursor = nullptr; +} + +NS_IMPL_ISUPPORTS(nsMsgPropertyEnumerator, nsIUTF8StringEnumerator, + nsIStringEnumerator) + +NS_IMETHODIMP nsMsgPropertyEnumerator::GetNext(nsACString& aItem) { + PrefetchNext(); + if (mNextColumn == NULL_MORK_COLUMN) + return NS_ERROR_FAILURE; // call HasMore first + if (!m_mdbStore || !m_mdbEnv) return NS_ERROR_NOT_INITIALIZED; + mNextPrefetched = false; + char columnName[100]; + struct mdbYarn colYarn = {columnName, 0, sizeof(columnName), 0, 0, nullptr}; + // Get the column of the cell + nsresult rv = m_mdbStore->TokenToString(m_mdbEnv, mNextColumn, &colYarn); + NS_ENSURE_SUCCESS(rv, rv); + + aItem.Assign(static_cast<char*>(colYarn.mYarn_Buf), colYarn.mYarn_Fill); + return NS_OK; +} + +NS_IMETHODIMP nsMsgPropertyEnumerator::HasMore(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + PrefetchNext(); + *aResult = (mNextColumn != NULL_MORK_COLUMN); + return NS_OK; +} + +void nsMsgPropertyEnumerator::PrefetchNext(void) { + if (!mNextPrefetched && m_mdbEnv && mRowCellCursor) { + mNextPrefetched = true; + nsCOMPtr<nsIMdbCell> cell; + mRowCellCursor->NextCell(m_mdbEnv, getter_AddRefs(cell), &mNextColumn, + nullptr); + if (mNextColumn == NULL_MORK_COLUMN) { + // free up references + m_mdbStore = nullptr; + m_mdbEnv = nullptr; + mRowCellCursor = nullptr; + } + } +} + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsMsgHdr::GetProperties(nsTArray<nsCString>& headers) { + nsCOMPtr<nsIUTF8StringEnumerator> propertyEnumerator = + new nsMsgPropertyEnumerator(this); + bool hasMore; + while (NS_SUCCEEDED(propertyEnumerator->HasMore(&hasMore)) && hasMore) { + nsAutoCString property; + propertyEnumerator->GetNext(property); + headers.AppendElement(property); + } + return NS_OK; +} diff --git a/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.cpp b/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.cpp new file mode 100644 index 0000000000..96410654d8 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.cpp @@ -0,0 +1,385 @@ +/* -*- 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 "nsMsgOfflineImapOperation.h" +#include "nsMsgUtils.h" +#include "mozilla/Logging.h" + +using namespace mozilla; + +LazyLogModule IMAPOffline("IMAPOffline"); + +/* Implementation file */ +NS_IMPL_ISUPPORTS(nsMsgOfflineImapOperation, nsIMsgOfflineImapOperation) + +// property names for offine imap operation fields. +#define PROP_OPERATION "op" +#define PROP_OPERATION_FLAGS "opFlags" +#define PROP_NEW_FLAGS "newFlags" +#define PROP_MESSAGE_KEY "msgKey" +#define PROP_SRC_MESSAGE_KEY "srcMsgKey" +#define PROP_SRC_FOLDER_URI "srcFolderURI" +#define PROP_MOVE_DEST_FOLDER_URI "moveDest" +#define PROP_NUM_COPY_DESTS "numCopyDests" +#define PROP_COPY_DESTS \ + "copyDests" // how to delimit these? Or should we do the "dest1","dest2" etc + // trick? But then we'd need to shuffle them around since we + // delete off the front first. +#define PROP_KEYWORD_ADD "addedKeywords" +#define PROP_KEYWORD_REMOVE "removedKeywords" +#define PROP_MSG_SIZE "msgSize" +#define PROP_PLAYINGBACK "inPlayback" + +nsMsgOfflineImapOperation::nsMsgOfflineImapOperation(nsMsgDatabase* db, + nsIMdbRow* row) { + NS_ASSERTION(db, "can't have null db"); + NS_ASSERTION(row, "can't have null row"); + m_operation = 0; + m_operationFlags = 0; + m_messageKey = nsMsgKey_None; + m_sourceMessageKey = nsMsgKey_None; + m_mdb = db; + NS_ADDREF(m_mdb); + m_mdbRow = row; + m_newFlags = 0; + m_mdb->GetUint32Property(m_mdbRow, PROP_OPERATION, (uint32_t*)&m_operation, + 0); + m_mdb->GetUint32Property(m_mdbRow, PROP_MESSAGE_KEY, &m_messageKey, 0); + m_mdb->GetUint32Property(m_mdbRow, PROP_OPERATION_FLAGS, &m_operationFlags, + 0); + m_mdb->GetUint32Property(m_mdbRow, PROP_NEW_FLAGS, (uint32_t*)&m_newFlags, 0); +} + +nsMsgOfflineImapOperation::~nsMsgOfflineImapOperation() { + // clear the row first, in case we're holding the last reference + // to the db. + m_mdbRow = nullptr; + NS_IF_RELEASE(m_mdb); +} + +/* attribute nsOfflineImapOperationType operation; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetOperation( + nsOfflineImapOperationType* aOperation) { + NS_ENSURE_ARG(aOperation); + *aOperation = m_operation; + return NS_OK; +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::SetOperation( + nsOfflineImapOperationType aOperation) { + if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info)) + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x setOperation was %x add %x", m_messageKey, m_operation, + aOperation)); + + m_operation |= aOperation; + return m_mdb->SetUint32Property(m_mdbRow, PROP_OPERATION, m_operation); +} + +/* void clearOperation (in nsOfflineImapOperationType operation); */ +NS_IMETHODIMP nsMsgOfflineImapOperation::ClearOperation( + nsOfflineImapOperationType aOperation) { + if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info)) + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x clearOperation was %x clear %x", m_messageKey, + m_operation, aOperation)); + m_operation &= ~aOperation; + switch (aOperation) { + case kMsgMoved: + case kAppendTemplate: + case kAppendDraft: + m_moveDestination.Truncate(); + break; + case kMsgCopy: + m_copyDestinations.RemoveElementAt(0); + break; + } + return m_mdb->SetUint32Property(m_mdbRow, PROP_OPERATION, m_operation); +} + +/* attribute nsMsgKey messageKey; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetMessageKey(nsMsgKey* aMessageKey) { + NS_ENSURE_ARG(aMessageKey); + *aMessageKey = m_messageKey; + return NS_OK; +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::SetMessageKey(nsMsgKey aMessageKey) { + m_messageKey = aMessageKey; + return m_mdb->SetUint32Property(m_mdbRow, PROP_MESSAGE_KEY, m_messageKey); +} + +/* attribute nsMsgKey srcMessageKey; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetSrcMessageKey( + nsMsgKey* aMessageKey) { + NS_ENSURE_ARG(aMessageKey); + return m_mdb->GetUint32Property(m_mdbRow, PROP_SRC_MESSAGE_KEY, aMessageKey, + nsMsgKey_None); +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::SetSrcMessageKey( + nsMsgKey aMessageKey) { + m_messageKey = aMessageKey; + return m_mdb->SetUint32Property(m_mdbRow, PROP_SRC_MESSAGE_KEY, m_messageKey); +} + +/* attribute imapMessageFlagsType flagOperation; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetFlagOperation( + imapMessageFlagsType* aFlagOperation) { + NS_ENSURE_ARG(aFlagOperation); + *aFlagOperation = m_operationFlags; + return NS_OK; +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::SetFlagOperation( + imapMessageFlagsType aFlagOperation) { + if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info)) + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x setFlagOperation was %x add %x", m_messageKey, + m_operationFlags, aFlagOperation)); + SetOperation(kFlagsChanged); + nsresult rv = SetNewFlags(aFlagOperation); + NS_ENSURE_SUCCESS(rv, rv); + m_operationFlags |= aFlagOperation; + return m_mdb->SetUint32Property(m_mdbRow, PROP_OPERATION_FLAGS, + m_operationFlags); +} + +/* attribute imapMessageFlagsType flagOperation; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetNewFlags( + imapMessageFlagsType* aNewFlags) { + NS_ENSURE_ARG(aNewFlags); + uint32_t flags; + nsresult rv = m_mdb->GetUint32Property(m_mdbRow, PROP_NEW_FLAGS, &flags, 0); + *aNewFlags = m_newFlags = (imapMessageFlagsType)flags; + return rv; +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::SetNewFlags( + imapMessageFlagsType aNewFlags) { + if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info) && m_newFlags != aNewFlags) + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x SetNewFlags was %x to %x", m_messageKey, m_newFlags, + aNewFlags)); + m_newFlags = aNewFlags; + return m_mdb->SetUint32Property(m_mdbRow, PROP_NEW_FLAGS, m_newFlags); +} + +/* attribute string destinationFolderURI; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetDestinationFolderURI( + nsACString& aDestinationFolderURI) { + (void)m_mdb->GetProperty(m_mdbRow, PROP_MOVE_DEST_FOLDER_URI, + getter_Copies(m_moveDestination)); + aDestinationFolderURI = m_moveDestination; + return NS_OK; +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::SetDestinationFolderURI( + const nsACString& aDestinationFolderURI) { + if (MOZ_LOG_TEST(IMAPOffline, LogLevel::Info)) + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x SetDestinationFolderURI to %s", m_messageKey, + PromiseFlatCString(aDestinationFolderURI).get())); + m_moveDestination = aDestinationFolderURI; + return m_mdb->SetProperty(m_mdbRow, PROP_MOVE_DEST_FOLDER_URI, + PromiseFlatCString(aDestinationFolderURI).get()); +} + +/* attribute string sourceFolderURI; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetSourceFolderURI( + nsACString& aSourceFolderURI) { + nsresult rv = m_mdb->GetProperty(m_mdbRow, PROP_SRC_FOLDER_URI, + getter_Copies(m_sourceFolder)); + aSourceFolderURI = m_sourceFolder; + return rv; +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::SetSourceFolderURI( + const nsACString& aSourceFolderURI) { + m_sourceFolder = aSourceFolderURI; + SetOperation(kMoveResult); + + return m_mdb->SetProperty(m_mdbRow, PROP_SRC_FOLDER_URI, + PromiseFlatCString(aSourceFolderURI).get()); +} + +/* attribute string keyword; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetKeywordsToAdd(char** aKeywords) { + NS_ENSURE_ARG(aKeywords); + nsresult rv = m_mdb->GetProperty(m_mdbRow, PROP_KEYWORD_ADD, + getter_Copies(m_keywordsToAdd)); + *aKeywords = ToNewCString(m_keywordsToAdd); + return rv; +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::AddKeywordToAdd(const char* aKeyword) { + SetOperation(kAddKeywords); + return AddKeyword(aKeyword, m_keywordsToAdd, PROP_KEYWORD_ADD, + m_keywordsToRemove, PROP_KEYWORD_REMOVE); +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::GetKeywordsToRemove(char** aKeywords) { + NS_ENSURE_ARG(aKeywords); + nsresult rv = m_mdb->GetProperty(m_mdbRow, PROP_KEYWORD_REMOVE, + getter_Copies(m_keywordsToRemove)); + *aKeywords = ToNewCString(m_keywordsToRemove); + return rv; +} + +nsresult nsMsgOfflineImapOperation::AddKeyword(const char* aKeyword, + nsCString& addList, + const char* addProp, + nsCString& removeList, + const char* removeProp) { + int32_t startOffset, keywordLength; + if (!MsgFindKeyword(nsDependentCString(aKeyword), addList, &startOffset, + &keywordLength)) { + if (!addList.IsEmpty()) addList.Append(' '); + addList.Append(aKeyword); + } + // if the keyword we're removing was in the list of keywords to add, + // cut it from that list. + if (MsgFindKeyword(nsDependentCString(aKeyword), removeList, &startOffset, + &keywordLength)) { + removeList.Cut(startOffset, keywordLength); + m_mdb->SetProperty(m_mdbRow, removeProp, removeList.get()); + } + return m_mdb->SetProperty(m_mdbRow, addProp, addList.get()); +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::AddKeywordToRemove( + const char* aKeyword) { + SetOperation(kRemoveKeywords); + return AddKeyword(aKeyword, m_keywordsToRemove, PROP_KEYWORD_REMOVE, + m_keywordsToAdd, PROP_KEYWORD_ADD); +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::AddMessageCopyOperation( + const nsACString& destinationBox) { + SetOperation(kMsgCopy); + nsresult rv = GetCopiesFromDB(); + NS_ENSURE_SUCCESS(rv, rv); + m_copyDestinations.AppendElement(destinationBox); + return SetCopiesToDB(); +} + +// we write out the folders as one string, separated by 0x1. +#define FOLDER_SEP_CHAR '\001' + +nsresult nsMsgOfflineImapOperation::GetCopiesFromDB() { + nsCString copyDests; + m_copyDestinations.Clear(); + nsresult rv = + m_mdb->GetProperty(m_mdbRow, PROP_COPY_DESTS, getter_Copies(copyDests)); + // use 0x1 as the delimiter between folder names since it's not a legal + // character + if (NS_SUCCEEDED(rv) && !copyDests.IsEmpty()) { + int32_t curCopyDestStart = 0; + int32_t nextCopyDestPos = 0; + + while (nextCopyDestPos != -1) { + nsCString curDest; + nextCopyDestPos = copyDests.FindChar(FOLDER_SEP_CHAR, curCopyDestStart); + if (nextCopyDestPos > 0) + curDest = Substring(copyDests, curCopyDestStart, + nextCopyDestPos - curCopyDestStart); + else + curDest = Substring(copyDests, curCopyDestStart, + copyDests.Length() - curCopyDestStart); + curCopyDestStart = nextCopyDestPos + 1; + m_copyDestinations.AppendElement(curDest); + } + } + return rv; +} + +nsresult nsMsgOfflineImapOperation::SetCopiesToDB() { + nsAutoCString copyDests; + + // use 0x1 as the delimiter between folders + for (uint32_t i = 0; i < m_copyDestinations.Length(); i++) { + if (i > 0) copyDests.Append(FOLDER_SEP_CHAR); + copyDests.Append(m_copyDestinations.ElementAt(i)); + } + return m_mdb->SetProperty(m_mdbRow, PROP_COPY_DESTS, copyDests.get()); +} + +/* attribute long numberOfCopies; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetNumberOfCopies( + int32_t* aNumberOfCopies) { + NS_ENSURE_ARG(aNumberOfCopies); + nsresult rv = GetCopiesFromDB(); + NS_ENSURE_SUCCESS(rv, rv); + *aNumberOfCopies = m_copyDestinations.Length(); + return NS_OK; +} + +/* string getCopyDestination (in long copyIndex); */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetCopyDestination(int32_t copyIndex, + char** retval) { + NS_ENSURE_ARG(retval); + nsresult rv = GetCopiesFromDB(); + NS_ENSURE_SUCCESS(rv, rv); + if (copyIndex >= (int32_t)m_copyDestinations.Length()) + return NS_ERROR_ILLEGAL_VALUE; + *retval = ToNewCString(m_copyDestinations.ElementAt(copyIndex)); + return (*retval) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +/* attribute unsigned log msgSize; */ +NS_IMETHODIMP nsMsgOfflineImapOperation::GetMsgSize(uint32_t* aMsgSize) { + NS_ENSURE_ARG(aMsgSize); + return m_mdb->GetUint32Property(m_mdbRow, PROP_MSG_SIZE, aMsgSize, 0); +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::SetMsgSize(uint32_t aMsgSize) { + return m_mdb->SetUint32Property(m_mdbRow, PROP_MSG_SIZE, aMsgSize); +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::SetPlayingBack(bool aPlayingBack) { + return m_mdb->SetBooleanProperty(m_mdbRow, PROP_PLAYINGBACK, aPlayingBack); +} + +NS_IMETHODIMP nsMsgOfflineImapOperation::GetPlayingBack(bool* aPlayingBack) { + NS_ENSURE_ARG(aPlayingBack); + return m_mdb->GetBooleanProperty(m_mdbRow, PROP_PLAYINGBACK, aPlayingBack); +} + +void nsMsgOfflineImapOperation::Log() { + if (!MOZ_LOG_TEST(IMAPOffline, LogLevel::Info)) return; + // const long kMoveResult = 0x8; + // const long kAppendDraft = 0x10; + // const long kAddedHeader = 0x20; + // const long kDeletedMsg = 0x40; + // const long kMsgMarkedDeleted = 0x80; + // const long kAppendTemplate = 0x100; + // const long kDeleteAllMsgs = 0x200; + if (m_operation & nsIMsgOfflineImapOperation::kFlagsChanged) + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x changeFlag:%x", m_messageKey, m_newFlags)); + if (m_operation & nsIMsgOfflineImapOperation::kMsgMoved) { + nsCString moveDestFolder; + GetDestinationFolderURI(moveDestFolder); + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x moveTo:%s", m_messageKey, moveDestFolder.get())); + } + if (m_operation & nsIMsgOfflineImapOperation::kMsgCopy) { + nsCString copyDests; + m_mdb->GetProperty(m_mdbRow, PROP_COPY_DESTS, getter_Copies(copyDests)); + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x moveTo:%s", m_messageKey, copyDests.get())); + } + if (m_operation & nsIMsgOfflineImapOperation::kAppendDraft) + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x append draft", m_messageKey)); + if (m_operation & nsIMsgOfflineImapOperation::kAddKeywords) + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x add keyword:%s", m_messageKey, m_keywordsToAdd.get())); + if (m_operation & nsIMsgOfflineImapOperation::kRemoveKeywords) + MOZ_LOG(IMAPOffline, LogLevel::Info, + ("msg id %x remove keyword:%s", m_messageKey, + m_keywordsToRemove.get())); +} diff --git a/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.h b/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.h new file mode 100644 index 0000000000..ba6ad3f079 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsMsgOfflineImapOperation.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 _nsMsgOfflineImapOperation_H_ + +# include "nsIMsgOfflineImapOperation.h" +# include "mdb.h" +# include "nsMsgDatabase.h" +# include "prlog.h" + +class nsMsgOfflineImapOperation : public nsIMsgOfflineImapOperation { + public: + /** Instance Methods **/ + nsMsgOfflineImapOperation(nsMsgDatabase* db, nsIMdbRow* row); + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGOFFLINEIMAPOPERATION + + nsIMdbRow* GetMDBRow() { return m_mdbRow; } + nsresult GetCopiesFromDB(); + nsresult SetCopiesToDB(); + void Log(); + + protected: + virtual ~nsMsgOfflineImapOperation(); + nsresult AddKeyword(const char* aKeyword, nsCString& addList, + const char* addProp, nsCString& removeList, + const char* removeProp); + + nsOfflineImapOperationType m_operation; + nsMsgKey m_messageKey; + nsMsgKey m_sourceMessageKey; + uint32_t m_operationFlags; // what to do on sync + imapMessageFlagsType m_newFlags; // used for kFlagsChanged + + // these are URI's, and are escaped. Thus, we can use a delimter like ' ' + // because the real spaces should be escaped. + nsCString m_sourceFolder; + nsCString m_moveDestination; + nsTArray<nsCString> m_copyDestinations; + + nsCString m_keywordsToAdd; + nsCString m_keywordsToRemove; + + // nsMsgOfflineImapOperation will have to know what db and row they belong to, + // since they are really just a wrapper around the offline operation row in + // the mdb. though I hope not. + nsMsgDatabase* m_mdb; + nsCOMPtr<nsIMdbRow> m_mdbRow; +}; + +#endif /* _nsMsgOfflineImapOperation_H_ */ diff --git a/comm/mailnews/db/msgdb/src/nsMsgThread.cpp b/comm/mailnews/db/msgdb/src/nsMsgThread.cpp new file mode 100644 index 0000000000..15398cf321 --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsMsgThread.cpp @@ -0,0 +1,1050 @@ +/* -*- 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 "nsMsgDatabase.h" +#include "nsCOMPtr.h" +#include "nsMsgThread.h" +#include "nsMsgMessageFlags.h" +#include "nsMsgEnumerator.h" +#include "MailNewsTypes2.h" +#include "mozilla/DebugOnly.h" + +NS_IMPL_ISUPPORTS(nsMsgThread, nsIMsgThread) + +nsMsgThread::nsMsgThread() { Init(); } + +nsMsgThread::nsMsgThread(nsMsgDatabase* db, nsIMdbTable* table) { + Init(); + m_mdbTable = table; + m_mdbDB = db; + if (db) + db->m_threads.AppendElement(this); + else + NS_ERROR("no db for thread"); +#ifdef DEBUG_David_Bienvenu + if (m_mdbDB->m_threads.Length() > 5) + printf("more than five outstanding threads\n"); +#endif + if (table && db) { + table->GetMetaRow(db->GetEnv(), nullptr, nullptr, + getter_AddRefs(m_metaRow)); + InitCachedValues(); + } +} + +void nsMsgThread::Init() { + m_threadKey = nsMsgKey_None; + m_threadRootKey = nsMsgKey_None; + m_numChildren = 0; + m_numUnreadChildren = 0; + m_flags = 0; + m_newestMsgDate = 0; + m_cachedValuesInitialized = false; +} + +nsMsgThread::~nsMsgThread() { + if (m_mdbDB) { + mozilla::DebugOnly<bool> found = m_mdbDB->m_threads.RemoveElement(this); + NS_ASSERTION(found, "removing thread not in threads array"); + } else // This can happen if db is forced closed + NS_WARNING("null db in thread"); + Clear(); +} + +void nsMsgThread::Clear() { + m_mdbTable = nullptr; + m_metaRow = nullptr; + m_mdbDB = nullptr; +} + +nsresult nsMsgThread::InitCachedValues() { + nsresult err = NS_OK; + + NS_ENSURE_TRUE(m_mdbDB && m_metaRow, NS_ERROR_INVALID_POINTER); + + if (!m_cachedValuesInitialized) { + err = m_mdbDB->RowCellColumnToUInt32( + m_metaRow, m_mdbDB->m_threadFlagsColumnToken, &m_flags); + err = m_mdbDB->RowCellColumnToUInt32( + m_metaRow, m_mdbDB->m_threadChildrenColumnToken, &m_numChildren); + err = m_mdbDB->RowCellColumnToUInt32( + m_metaRow, m_mdbDB->m_threadIdColumnToken, &m_threadKey, nsMsgKey_None); + err = m_mdbDB->RowCellColumnToUInt32( + m_metaRow, m_mdbDB->m_threadUnreadChildrenColumnToken, + &m_numUnreadChildren); + err = m_mdbDB->RowCellColumnToUInt32(m_metaRow, + m_mdbDB->m_threadRootKeyColumnToken, + &m_threadRootKey, nsMsgKey_None); + err = m_mdbDB->RowCellColumnToUInt32( + m_metaRow, m_mdbDB->m_threadNewestMsgDateColumnToken, &m_newestMsgDate, + 0); + // fix num children if it's wrong. this doesn't work - some DB's have a + // bogus thread table that is full of bogus headers - don't know why. + uint32_t rowCount = 0; + m_mdbTable->GetCount(m_mdbDB->GetEnv(), &rowCount); + // NS_ASSERTION(m_numChildren <= rowCount, "num children wrong - + // fixing"); + if (m_numChildren > rowCount) + ChangeChildCount((int32_t)rowCount - (int32_t)m_numChildren); + if ((int32_t)m_numUnreadChildren < 0) + ChangeUnreadChildCount(-(int32_t)m_numUnreadChildren); + if (NS_SUCCEEDED(err)) m_cachedValuesInitialized = true; + } + return err; +} + +NS_IMETHODIMP nsMsgThread::SetThreadKey(nsMsgKey threadKey) { + NS_ASSERTION(m_threadKey == nsMsgKey_None || m_threadKey == threadKey, + "shouldn't be changing thread key"); + m_threadKey = threadKey; + // by definition, the initial thread key is also the thread root key. + SetThreadRootKey(threadKey); + // gotta set column in meta row here. + return m_mdbDB->UInt32ToRowCellColumn( + m_metaRow, m_mdbDB->m_threadIdColumnToken, threadKey); +} + +NS_IMETHODIMP nsMsgThread::GetThreadKey(nsMsgKey* result) { + NS_ENSURE_ARG_POINTER(result); + nsresult res = m_mdbDB->RowCellColumnToUInt32( + m_metaRow, m_mdbDB->m_threadIdColumnToken, &m_threadKey); + *result = m_threadKey; + return res; +} + +NS_IMETHODIMP nsMsgThread::GetFlags(uint32_t* result) { + NS_ENSURE_ARG_POINTER(result); + nsresult res = m_mdbDB->RowCellColumnToUInt32( + m_metaRow, m_mdbDB->m_threadFlagsColumnToken, &m_flags); + *result = m_flags; + return res; +} + +NS_IMETHODIMP nsMsgThread::SetFlags(uint32_t flags) { + m_flags = flags; + return m_mdbDB->UInt32ToRowCellColumn( + m_metaRow, m_mdbDB->m_threadFlagsColumnToken, m_flags); +} + +NS_IMETHODIMP nsMsgThread::SetSubject(const nsACString& aSubject) { + return m_mdbDB->CharPtrToRowCellColumn(m_metaRow, + m_mdbDB->m_threadSubjectColumnToken, + PromiseFlatCString(aSubject).get()); +} + +NS_IMETHODIMP nsMsgThread::GetSubject(nsACString& aSubject) { + nsCString subjectStr; + nsresult rv = m_mdbDB->RowCellColumnToCharPtr( + m_metaRow, m_mdbDB->m_threadSubjectColumnToken, + getter_Copies(subjectStr)); + + aSubject.Assign(subjectStr); + return rv; +} + +NS_IMETHODIMP nsMsgThread::GetNumChildren(uint32_t* result) { + NS_ENSURE_ARG_POINTER(result); + *result = m_numChildren; + return NS_OK; +} + +NS_IMETHODIMP nsMsgThread::GetNumUnreadChildren(uint32_t* result) { + NS_ENSURE_ARG_POINTER(result); + *result = m_numUnreadChildren; + return NS_OK; +} + +nsresult nsMsgThread::RerootThread(nsIMsgDBHdr* newParentOfOldRoot, + nsIMsgDBHdr* oldRoot, + nsIDBChangeAnnouncer* announcer) { + nsresult rv = NS_OK; + mdb_pos outPos; + nsMsgKey newHdrAncestor; + nsCOMPtr<nsIMsgDBHdr> ancestorHdr = newParentOfOldRoot; + nsMsgKey newRoot; + + ancestorHdr->GetMessageKey(&newRoot); + // loop trying to find the oldest ancestor of this msg + // that is a parent of the root. The oldest ancestor will + // become the root of the thread. + do { + ancestorHdr->GetThreadParent(&newHdrAncestor); + if (newHdrAncestor != nsMsgKey_None && newHdrAncestor != m_threadRootKey && + newHdrAncestor != newRoot) { + newRoot = newHdrAncestor; + rv = m_mdbDB->GetMsgHdrForKey(newRoot, getter_AddRefs(ancestorHdr)); + } + } while (NS_SUCCEEDED(rv) && ancestorHdr && newHdrAncestor != nsMsgKey_None && + newHdrAncestor != m_threadRootKey && newHdrAncestor != newRoot); + SetThreadRootKey(newRoot); + ReparentNonReferenceChildrenOf(oldRoot, newRoot, announcer); + if (ancestorHdr) { + nsIMsgDBHdr* msgHdr = ancestorHdr; + nsMsgHdr* rootMsgHdr = + static_cast<nsMsgHdr*>(msgHdr); // closed system, cast ok + nsIMdbRow* newRootHdrRow = rootMsgHdr->GetMDBRow(); + // move the root hdr to pos 0. + m_mdbTable->MoveRow(m_mdbDB->GetEnv(), newRootHdrRow, -1, 0, &outPos); + ancestorHdr->SetThreadParent(nsMsgKey_None); + } + return rv; +} + +NS_IMETHODIMP nsMsgThread::AddChild(nsIMsgDBHdr* child, nsIMsgDBHdr* inReplyTo, + bool threadInThread, + nsIDBChangeAnnouncer* announcer) { + nsresult rv = NS_OK; + nsMsgHdr* hdr = static_cast<nsMsgHdr*>(child); // closed system, cast ok + uint32_t newHdrFlags = 0; + uint32_t msgDate; + nsMsgKey newHdrKey = 0; + bool parentKeyNeedsSetting = true; + + nsIMdbRow* hdrRow = hdr->GetMDBRow(); + NS_ENSURE_STATE(hdrRow); + hdr->GetRawFlags(&newHdrFlags); + hdr->GetMessageKey(&newHdrKey); + hdr->GetDateInSeconds(&msgDate); + if (msgDate > m_newestMsgDate) SetNewestMsgDate(msgDate); + + if (newHdrFlags & nsMsgMessageFlags::Watched) + SetFlags(m_flags | nsMsgMessageFlags::Watched); + + child->AndFlags(~(nsMsgMessageFlags::Watched), &newHdrFlags); + + // These are threading flags that the child may have set before being added + // to the database. + uint32_t protoThreadFlags; + child->GetUint32Property("ProtoThreadFlags", &protoThreadFlags); + SetFlags(m_flags | protoThreadFlags); + // Clear the flag so that it doesn't fudge anywhere else + child->SetUint32Property("ProtoThreadFlags", 0); + + uint32_t numChildren = 0; + // 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) SetThreadRootKey(newHdrKey); + + if (m_mdbTable) { + m_mdbTable->AddRow(m_mdbDB->GetEnv(), hdrRow); + ChangeChildCount(1); + if (!(newHdrFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(1); + } + if (inReplyTo) { + nsMsgKey parentKey; + inReplyTo->GetMessageKey(&parentKey); + child->SetThreadParent(parentKey); + parentKeyNeedsSetting = false; + } + + // check if this header is a parent of one of the messages in this thread + bool hdrMoved = false; + nsCOMPtr<nsIMsgDBHdr> curHdr; + uint32_t moveIndex = 0; + + PRTime newHdrDate; + child->GetDate(&newHdrDate); + + // This is an ugly but simple fix for a difficult problem. Basically, when we + // add a message to a thread, we have to run through the thread to see if the + // new message is a parent of an existing message in the thread, and adjust + // things accordingly. If you thread by subject, and you have a large folder + // with messages w/ all the same subject, this code can take a really long + // time. So the pragmatic thing is to say that for threads with more than 1000 + // messages, it's simply not worth dealing with the case where the parent + // comes in after the child. Threads with more than 1000 messages are pretty + // unwieldy anyway. See Bug 90452 + + if (numChildren < 1000) { + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsMsgKey msgKey = nsMsgKey_None; + + rv = GetChildHdrAt(childIndex, getter_AddRefs(curHdr)); + if (NS_SUCCEEDED(rv) && curHdr) { + if (hdr->IsParentOf(curHdr)) { + nsMsgKey oldThreadParent; + mdb_pos outPos; + // move this hdr before the current header. + if (!hdrMoved) { + m_mdbTable->MoveRow(m_mdbDB->GetEnv(), hdrRow, -1, childIndex, + &outPos); + hdrMoved = true; + curHdr->GetThreadParent(&oldThreadParent); + curHdr->GetMessageKey(&msgKey); + nsCOMPtr<nsIMsgDBHdr> curParent; + m_mdbDB->GetMsgHdrForKey(oldThreadParent, + getter_AddRefs(curParent)); + if (curParent && hdr->IsAncestorOf(curParent)) { + nsMsgKey curParentKey; + curParent->GetMessageKey(&curParentKey); + if (curParentKey == m_threadRootKey) { + m_mdbTable->MoveRow(m_mdbDB->GetEnv(), hdrRow, -1, 0, &outPos); + RerootThread(child, curParent, announcer); + parentKeyNeedsSetting = false; + } + } else if (msgKey == m_threadRootKey) { + RerootThread(child, curHdr, announcer); + parentKeyNeedsSetting = false; + } + } + curHdr->SetThreadParent(newHdrKey); + // TODO: what should be msgKey if hdrMoved was true above? + if (msgKey == newHdrKey) parentKeyNeedsSetting = false; + + // OK, this is a reparenting - need to send notification + if (announcer) + announcer->NotifyParentChangedAll(msgKey, oldThreadParent, + newHdrKey, nullptr); +#ifdef DEBUG_bienvenu1 + if (newHdrKey != m_threadKey) printf("adding second level child\n"); +#endif + } + // Calculate a position for this child in date order + else if (!hdrMoved && childIndex > 0 && moveIndex == 0) { + PRTime curHdrDate; + + curHdr->GetDate(&curHdrDate); + if (newHdrDate < curHdrDate) moveIndex = childIndex; + } + } + } + } + // If this header is not a reply to a header in the thread, and isn't a parent + // check to see if it starts with Re: - if not, and the first header does + // start with re, should we make this header the top level header? If it's + // date is less (or it's ID?), then yes. + if (numChildren > 0 && !(newHdrFlags & nsMsgMessageFlags::HasRe) && + !inReplyTo) { + PRTime topLevelHdrDate; + + nsCOMPtr<nsIMsgDBHdr> topLevelHdr; + rv = GetRootHdr(getter_AddRefs(topLevelHdr)); + if (NS_SUCCEEDED(rv) && topLevelHdr) { + topLevelHdr->GetDate(&topLevelHdrDate); + if (newHdrDate < topLevelHdrDate) { + RerootThread(child, topLevelHdr, announcer); + mdb_pos outPos; + m_mdbTable->MoveRow(m_mdbDB->GetEnv(), hdrRow, -1, 0, &outPos); + hdrMoved = true; + topLevelHdr->SetThreadParent(newHdrKey); + parentKeyNeedsSetting = false; + // ### need to get ancestor of new hdr here too. + SetThreadRootKey(newHdrKey); + child->SetThreadParent(nsMsgKey_None); + // argh, here we'd need to adjust all the headers that listed + // the demoted header as their thread parent, but only because + // of subject threading. Adjust them to point to the new parent, + // that is. + ReparentNonReferenceChildrenOf(topLevelHdr, newHdrKey, announcer); + } + } + } + // OK, check to see if we added this header, and didn't parent it. + + if (numChildren > 0 && parentKeyNeedsSetting) + child->SetThreadParent(m_threadRootKey); + + // Move child to keep thread sorted in ascending date order + if (!hdrMoved && moveIndex > 0) { + mdb_pos outPos; + m_mdbTable->MoveRow(m_mdbDB->GetEnv(), hdrRow, -1, moveIndex, &outPos); + } + + // do this after we've put the new hdr in the thread + bool isKilled; + child->GetIsKilled(&isKilled); + if ((m_flags & nsMsgMessageFlags::Ignored || isKilled) && m_mdbDB) + m_mdbDB->MarkHdrRead(child, true, nullptr); +#ifdef DEBUG_David_Bienvenu + nsMsgKey msgHdrThreadKey; + child->GetThreadId(&msgHdrThreadKey); + NS_ASSERTION(msgHdrThreadKey == m_threadKey, + "adding msg to thread it doesn't belong to"); +#endif + return rv; +} + +nsresult nsMsgThread::ReparentNonReferenceChildrenOf( + nsIMsgDBHdr* oldTopLevelHdr, nsMsgKey newParentKey, + nsIDBChangeAnnouncer* announcer) { + nsCOMPtr<nsIMsgDBHdr> curHdr; + uint32_t numChildren = 0; + + GetNumChildren(&numChildren); + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsMsgKey oldTopLevelHdrKey; + + oldTopLevelHdr->GetMessageKey(&oldTopLevelHdrKey); + nsresult rv = GetChildHdrAt(childIndex, getter_AddRefs(curHdr)); + if (NS_SUCCEEDED(rv) && curHdr) { + nsMsgKey oldThreadParent, curHdrKey; + nsMsgHdr* oldTopLevelMsgHdr = + static_cast<nsMsgHdr*>(oldTopLevelHdr); // closed system, cast ok + curHdr->GetThreadParent(&oldThreadParent); + curHdr->GetMessageKey(&curHdrKey); + if (oldThreadParent == oldTopLevelHdrKey && curHdrKey != newParentKey && + !oldTopLevelMsgHdr->IsParentOf(curHdr)) { + curHdr->GetThreadParent(&oldThreadParent); + curHdr->SetThreadParent(newParentKey); + // OK, this is a reparenting - need to send notification + if (announcer) + announcer->NotifyParentChangedAll(curHdrKey, oldThreadParent, + newParentKey, nullptr); + } + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgThread::GetChildKeyAt(uint32_t aIndex, nsMsgKey* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + nsresult rv; + + if (aIndex >= m_numChildren) { + *aResult = nsMsgKey_None; + return NS_ERROR_ILLEGAL_VALUE; + } + mdbOid oid; + rv = m_mdbTable->PosToOid(m_mdbDB->GetEnv(), aIndex, &oid); + NS_ENSURE_SUCCESS(rv, rv); + + *aResult = oid.mOid_Id; + return NS_OK; +} + +NS_IMETHODIMP nsMsgThread::GetChildHdrAt(uint32_t aIndex, + nsIMsgDBHdr** result) { + // mork doesn't seem to handle this correctly, so deal with going off + // the end here. + if (aIndex >= m_numChildren) return NS_MSG_MESSAGE_NOT_FOUND; + mdbOid oid; + nsresult rv = m_mdbTable->PosToOid(m_mdbDB->GetEnv(), aIndex, &oid); + NS_ENSURE_SUCCESS(rv, NS_MSG_MESSAGE_NOT_FOUND); + nsIMdbRow* hdrRow = nullptr; + rv = m_mdbTable->PosToRow(m_mdbDB->GetEnv(), aIndex, &hdrRow); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hdrRow, NS_ERROR_FAILURE); + // CreateMsgHdr takes ownership of the hdrRow reference. + rv = m_mdbDB->CreateMsgHdr(hdrRow, oid.mOid_Id, result); + return (NS_SUCCEEDED(rv)) ? NS_OK : NS_MSG_MESSAGE_NOT_FOUND; +} + +NS_IMETHODIMP nsMsgThread::GetChild(nsMsgKey msgKey, nsIMsgDBHdr** result) { + nsresult rv; + + mdb_bool hasOid; + mdbOid rowObjectId; + + NS_ENSURE_ARG_POINTER(result); + NS_ENSURE_TRUE(m_mdbTable, NS_ERROR_INVALID_POINTER); + + *result = NULL; + rowObjectId.mOid_Id = msgKey; + rowObjectId.mOid_Scope = m_mdbDB->m_hdrRowScopeToken; + rv = m_mdbTable->HasOid(m_mdbDB->GetEnv(), &rowObjectId, &hasOid); + + if (NS_SUCCEEDED(rv) && hasOid && m_mdbDB && m_mdbDB->m_mdbStore) { + nsIMdbRow* hdrRow = nullptr; + rv = m_mdbDB->m_mdbStore->GetRow(m_mdbDB->GetEnv(), &rowObjectId, &hdrRow); + NS_ENSURE_TRUE(NS_SUCCEEDED(rv) && hdrRow, NS_ERROR_FAILURE); + rv = m_mdbDB->CreateMsgHdr(hdrRow, msgKey, result); + } + + return rv; +} + +NS_IMETHODIMP nsMsgThread::RemoveChildAt(uint32_t aIndex) { return NS_OK; } + +nsresult nsMsgThread::RemoveChild(nsMsgKey msgKey) { + nsresult rv; + + mdbOid rowObjectId; + rowObjectId.mOid_Id = msgKey; + rowObjectId.mOid_Scope = m_mdbDB->m_hdrRowScopeToken; + rv = m_mdbTable->CutOid(m_mdbDB->GetEnv(), &rowObjectId); + // if this thread is empty, remove it from the all threads table. + if (m_numChildren == 0 && m_mdbDB->m_mdbAllThreadsTable) { + mdbOid rowID; + rowID.mOid_Id = m_threadKey; + rowID.mOid_Scope = m_mdbDB->m_threadRowScopeToken; + + m_mdbDB->m_mdbAllThreadsTable->CutOid(m_mdbDB->GetEnv(), &rowID); + } +#if 0 // this seems to cause problems + if (m_numChildren == 0 && m_metaRow && m_mdbDB) + m_metaRow->CutAllColumns(m_mdbDB->GetEnv()); +#endif + + return rv; +} + +NS_IMETHODIMP nsMsgThread::RemoveChildHdr(nsIMsgDBHdr* child, + nsIDBChangeAnnouncer* announcer) { + uint32_t flags; + nsMsgKey key; + nsMsgKey threadParent; + + NS_ENSURE_ARG_POINTER(child); + + child->GetFlags(&flags); + child->GetMessageKey(&key); + + child->GetThreadParent(&threadParent); + ReparentChildrenOf(key, threadParent, announcer); + + // 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); + ChangeChildCount(-1); + return RemoveChild(key); +} + +nsresult nsMsgThread::ReparentChildrenOf(nsMsgKey oldParent, nsMsgKey newParent, + nsIDBChangeAnnouncer* announcer) { + nsresult rv = NS_OK; + + uint32_t numChildren = 0; + GetNumChildren(&numChildren); + + nsCOMPtr<nsIMsgDBHdr> curHdr; + if (numChildren > 0) { + 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) { + SetThreadRootKey(curKey); + newParent = curKey; + } + } + } + } + } + return rv; +} + +NS_IMETHODIMP nsMsgThread::MarkChildRead(bool bRead) { + ChangeUnreadChildCount(bRead ? -1 : 1); + return NS_OK; +} + +/** + * Helper class for enumerating through the messages in a thread. + */ +class nsMsgThreadEnumerator : public nsBaseMsgEnumerator { + public: + // nsIMsgEnumerator support. + NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override; + NS_IMETHOD HasMoreElements(bool* aResult) override; + + // nsMsgThreadEnumerator methods: + typedef nsresult (*nsMsgThreadEnumeratorFilter)(nsIMsgDBHdr* hdr, + void* closure); + + nsMsgThreadEnumerator(nsMsgThread* thread, nsMsgKey startKey, + nsMsgThreadEnumeratorFilter filter, void* closure); + int32_t MsgKeyFirstChildIndex(nsMsgKey inMsgKey); + + protected: + ~nsMsgThreadEnumerator() override = default; + nsresult Prefetch(); + + nsIMdbTableRowCursor* mRowCursor; + nsCOMPtr<nsIMsgDBHdr> mResultHdr; + RefPtr<nsMsgThread> mThread; + nsMsgKey mThreadParentKey; + nsMsgKey mFirstMsgKey; + int32_t mChildIndex; + bool mDone; + bool mNeedToPrefetch; + nsMsgThreadEnumeratorFilter mFilter; + void* mClosure; + bool mFoundChildren; +}; + +nsMsgThreadEnumerator::nsMsgThreadEnumerator(nsMsgThread* thread, + nsMsgKey startKey, + nsMsgThreadEnumeratorFilter filter, + void* closure) + : mRowCursor(nullptr), + 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 = 0; + 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 +} + +int32_t nsMsgThreadEnumerator::MsgKeyFirstChildIndex(nsMsgKey inMsgKey) { + // if (msgKey != mThreadParentKey) + // mDone = true; + // 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. + + int32_t firstChildIndex = -1; + uint32_t numChildren = 0; + mThread->GetNumChildren(&numChildren); + + // if this is the first message in the thread, just check if there's more than + // one message in the thread. + // if (inMsgKey == mThread->m_threadRootKey) + // return (numChildren > 1) ? 1 : -1; + + for (uint32_t curChildIndex = 0; curChildIndex < numChildren; + curChildIndex++) { + nsCOMPtr<nsIMsgDBHdr> curHdr; + 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 nsMsgThreadEnumerator::GetNext(nsIMsgDBHdr** aItem) { + NS_ENSURE_ARG_POINTER(aItem); + nsresult rv; + + if (mNeedToPrefetch) { + rv = Prefetch(); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mResultHdr) { + NS_ADDREF(*aItem = mResultHdr); + mNeedToPrefetch = true; + } + return NS_OK; +} + +nsresult nsMsgThreadEnumerator::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 = 0; + mThread->GetNumChildren(&numChildren); + + while (mChildIndex < (int32_t)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 nsMsgThreadEnumerator::HasMoreElements(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + if (mNeedToPrefetch) Prefetch(); + *aResult = !mDone; + return NS_OK; +} + +NS_IMETHODIMP nsMsgThread::EnumerateMessages(nsMsgKey parentKey, + nsIMsgEnumerator** result) { + NS_ADDREF(*result = + new nsMsgThreadEnumerator(this, parentKey, nullptr, nullptr)); + return NS_OK; +} + +nsresult nsMsgThread::ReparentMsgsWithInvalidParent(uint32_t numChildren, + nsMsgKey threadParentKey) { + nsresult rv = NS_OK; + // run through looking for messages that don't have a correct parent, + // i.e., a parent that's in the thread! + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> curChild; + rv = GetChildHdrAt(childIndex, getter_AddRefs(curChild)); + if (NS_SUCCEEDED(rv) && curChild) { + nsMsgKey parentKey; + nsCOMPtr<nsIMsgDBHdr> parent; + + curChild->GetThreadParent(&parentKey); + + if (parentKey != nsMsgKey_None) { + GetChild(parentKey, getter_AddRefs(parent)); + if (!parent) + curChild->SetThreadParent(threadParentKey); + else { + nsMsgKey childKey; + curChild->GetMessageKey(&childKey); + // can't be your own parent; set parent to thread parent, + // or make ourselves the root if we are the root. + if (childKey == parentKey) + curChild->SetThreadParent( + m_threadRootKey == childKey ? nsMsgKey_None : m_threadRootKey); + } + } + } + } + return rv; +} + +NS_IMETHODIMP nsMsgThread::GetRootHdr(nsIMsgDBHdr** result) { + NS_ENSURE_ARG_POINTER(result); + + *result = nullptr; + int32_t resultIndex = -1; + nsresult rv = NS_OK; + + if (m_threadRootKey != nsMsgKey_None) { + rv = GetChildHdrForKey(m_threadRootKey, result, &resultIndex); + if (NS_SUCCEEDED(rv) && *result) { + // check that we're really the root key. + nsMsgKey parentKey; + (*result)->GetThreadParent(&parentKey); + if (parentKey == nsMsgKey_None) return rv; + // XXX Hack: since GetChildHdrForKey() addref'ed result, we need to + // release any unwanted result before continuing. + NS_RELEASE(*result); + } +#ifdef DEBUG_David_Bienvenu + printf("need to reset thread root key\n"); +#endif + nsMsgKey threadParentKey = nsMsgKey_None; + uint32_t numChildren = 0; + GetNumChildren(&numChildren); + + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> curChild; + rv = GetChildHdrAt(childIndex, getter_AddRefs(curChild)); + if (NS_SUCCEEDED(rv) && curChild) { + nsMsgKey parentKey; + + curChild->GetThreadParent(&parentKey); + if (parentKey == nsMsgKey_None) { + curChild->GetMessageKey(&threadParentKey); + if (*result) { + NS_WARNING("two top level msgs, not good"); + continue; + } + SetThreadRootKey(threadParentKey); + curChild.forget(result); + ReparentMsgsWithInvalidParent(numChildren, threadParentKey); + } + } + } + } + if (!*result) { + // 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. + rv = GetChildHdrAt(0, result); + } + if (!*result) return rv; + // Check that the thread id of the message is this thread. + nsMsgKey threadId = nsMsgKey_None; + (void)(*result)->GetThreadId(&threadId); + if (threadId != m_threadKey) (*result)->SetThreadId(m_threadKey); + return rv; +} + +nsresult nsMsgThread::ChangeChildCount(int32_t delta) { + nsresult rv; + + uint32_t childCount = 0; + m_mdbDB->RowCellColumnToUInt32( + m_metaRow, m_mdbDB->m_threadChildrenColumnToken, childCount); + + NS_WARNING_ASSERTION(childCount != 0 || delta > 0, + "child count gone negative"); + childCount += delta; + + NS_WARNING_ASSERTION((int32_t)childCount >= 0, + "child count gone to 0 or below"); + if ((int32_t)childCount < 0) // force child count to >= 0 + childCount = 0; + + rv = m_mdbDB->UInt32ToRowCellColumn( + m_metaRow, m_mdbDB->m_threadChildrenColumnToken, childCount); + m_numChildren = childCount; + return rv; +} + +nsresult nsMsgThread::ChangeUnreadChildCount(int32_t delta) { + nsresult rv; + + uint32_t childCount = 0; + m_mdbDB->RowCellColumnToUInt32( + m_metaRow, m_mdbDB->m_threadUnreadChildrenColumnToken, childCount); + childCount += delta; + if ((int32_t)childCount < 0) { +#ifdef DEBUG_bienvenu1 + NS_ASSERTION(false, "negative unread child count"); +#endif + childCount = 0; + } + rv = m_mdbDB->UInt32ToRowCellColumn( + m_metaRow, m_mdbDB->m_threadUnreadChildrenColumnToken, childCount); + m_numUnreadChildren = childCount; + return rv; +} + +nsresult nsMsgThread::SetThreadRootKey(nsMsgKey threadRootKey) { + m_threadRootKey = threadRootKey; + return m_mdbDB->UInt32ToRowCellColumn( + m_metaRow, m_mdbDB->m_threadRootKeyColumnToken, threadRootKey); +} + +nsresult nsMsgThread::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++) { + rv = GetChildHdrAt(childIndex, result); + if (NS_SUCCEEDED(rv) && *result) { + 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. + (*result)->GetMessageKey(&msgKey); + + if (msgKey == desiredKey) { + nsMsgKey threadKey; + (*result)->GetThreadId(&threadKey); + if (threadKey != m_threadKey) // this msg isn't in this thread + { + NS_WARNING("msg in wrong thread - this shouldn't happen"); + uint32_t msgSize; + (*result)->GetMessageSize(&msgSize); + if (msgSize == 0) // this is a phantom message - let's get rid of it. + { + RemoveChild(msgKey); + rv = NS_ERROR_UNEXPECTED; + } else { + // otherwise, let's try to figure out which thread + // this message really belongs to. + nsCOMPtr<nsIMsgThread> threadKeyThread = + dont_AddRef(m_mdbDB->GetThreadForThreadId(threadKey)); + if (threadKeyThread) { + nsCOMPtr<nsIMsgDBHdr> otherThreadHdr; + threadKeyThread->GetChild(msgKey, getter_AddRefs(otherThreadHdr)); + if (otherThreadHdr) { + // Message is in one thread but has a different thread id. + // Remove it from the thread and then rethread it. + RemoveChild(msgKey); + threadKeyThread->RemoveChildHdr(otherThreadHdr, nullptr); + bool newThread; + nsMsgHdr* msgHdr = static_cast<nsMsgHdr*>(otherThreadHdr.get()); + m_mdbDB->ThreadNewHdr(msgHdr, newThread); + } else { + (*result)->SetThreadId(m_threadKey); + } + } + } + } + break; + } + // XXX Hack: since GetChildHdrAt() addref'ed result, we need to + // release any unwanted result before continuing in the loop. + NS_RELEASE(*result); + } + } + if (resultIndex) *resultIndex = (int32_t)childIndex; + + return rv; +} + +NS_IMETHODIMP nsMsgThread::GetFirstUnreadChild(nsIMsgDBHdr** result) { + NS_ENSURE_ARG_POINTER(result); + + uint8_t minLevel = 0xff; + + uint32_t numChildren = 0; + GetNumChildren(&numChildren); + + nsCOMPtr<nsIMsgDBHdr> retHdr; + + 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_mdbDB->IsRead(msgKey, &isRead); + if (NS_SUCCEEDED(rv) && !isRead) { + // this is the root, so it's the best we're going to do. + if (msgKey == m_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) { + rv = m_mdbDB->GetMsgHdrForKey(parentId, getter_AddRefs(parent)); + if (parent) { + parent->GetThreadParent(&parentId); + level++; + } + } + if (level < minLevel) { + minLevel = level; + retHdr = child; + } + } + } + } + + retHdr.forget(result); + return (*result) ? NS_OK : NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsMsgThread::GetNewestMsgDate(uint32_t* aResult) { + // if this hasn't been set, figure it out by enumerating the msgs in the + // thread. + if (!m_newestMsgDate) { + nsresult rv; + uint32_t numChildren; + GetNumChildren(&numChildren); + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> child; + rv = GetChildHdrAt(childIndex, getter_AddRefs(child)); + if (NS_SUCCEEDED(rv)) { + uint32_t msgDate; + child->GetDateInSeconds(&msgDate); + if (msgDate > m_newestMsgDate) m_newestMsgDate = msgDate; + } + } + } + *aResult = m_newestMsgDate; + return NS_OK; +} + +NS_IMETHODIMP nsMsgThread::SetNewestMsgDate(uint32_t aNewestMsgDate) { + m_newestMsgDate = aNewestMsgDate; + return m_mdbDB->UInt32ToRowCellColumn( + m_metaRow, m_mdbDB->m_threadNewestMsgDateColumnToken, aNewestMsgDate); +} diff --git a/comm/mailnews/db/msgdb/src/nsNewsDatabase.cpp b/comm/mailnews/db/msgdb/src/nsNewsDatabase.cpp new file mode 100644 index 0000000000..5a5ba19d5e --- /dev/null +++ b/comm/mailnews/db/msgdb/src/nsNewsDatabase.cpp @@ -0,0 +1,307 @@ +/* -*- 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 "nsIMsgDBView.h" +#include "nsIMsgThread.h" +#include "nsNewsDatabase.h" +#include "nsMsgKeySet.h" +#include "nsMsgMessageFlags.h" +#include "nsCOMPtr.h" +#include "prlog.h" + +#if defined(DEBUG_sspitzer_) || defined(DEBUG_seth_) +# define DEBUG_NEWS_DATABASE 1 +#endif + +nsNewsDatabase::nsNewsDatabase() { m_readSet = nullptr; } + +nsNewsDatabase::~nsNewsDatabase() {} + +NS_IMPL_ADDREF_INHERITED(nsNewsDatabase, nsMsgDatabase) +NS_IMPL_RELEASE_INHERITED(nsNewsDatabase, nsMsgDatabase) + +NS_IMETHODIMP nsNewsDatabase::QueryInterface(REFNSIID aIID, + void** aInstancePtr) { + if (!aInstancePtr) return NS_ERROR_NULL_POINTER; + *aInstancePtr = nullptr; + + if (aIID.Equals(NS_GET_IID(nsINewsDatabase))) { + *aInstancePtr = static_cast<nsINewsDatabase*>(this); + } + + if (*aInstancePtr) { + AddRef(); + return NS_OK; + } + + return nsMsgDatabase::QueryInterface(aIID, aInstancePtr); +} + +nsresult nsNewsDatabase::Close(bool forceCommit) { + return nsMsgDatabase::Close(forceCommit); +} + +nsresult nsNewsDatabase::ForceClosed() { return nsMsgDatabase::ForceClosed(); } + +nsresult nsNewsDatabase::Commit(nsMsgDBCommit commitType) { + if (m_dbFolderInfo && m_readSet) { + // let's write out our idea of the read set so we can compare it with that + // of the .rc file next time we start up. + nsCString readSet; + m_readSet->Output(getter_Copies(readSet)); + m_dbFolderInfo->SetCharProperty("readSet", readSet); + } + return nsMsgDatabase::Commit(commitType); +} + +uint32_t nsNewsDatabase::GetCurVersion() { return kMsgDBVersion; } + +NS_IMETHODIMP nsNewsDatabase::IsRead(nsMsgKey key, bool* pRead) { + NS_ASSERTION(pRead, "null out param in IsRead"); + if (!pRead) return NS_ERROR_NULL_POINTER; + + if (!m_readSet) return NS_ERROR_FAILURE; + + *pRead = m_readSet->IsMember(key); + return NS_OK; +} + +nsresult nsNewsDatabase::IsHeaderRead(nsIMsgDBHdr* msgHdr, bool* pRead) { + nsresult rv; + nsMsgKey messageKey; + + if (!msgHdr || !pRead) return NS_ERROR_NULL_POINTER; + + rv = msgHdr->GetMessageKey(&messageKey); + if (NS_FAILED(rv)) return rv; + + rv = IsRead(messageKey, pRead); + return rv; +} + +// return highest article number we've seen. +NS_IMETHODIMP nsNewsDatabase::GetHighWaterArticleNum(nsMsgKey* key) { + NS_ASSERTION(m_dbFolderInfo, "null db folder info"); + if (!m_dbFolderInfo) return NS_ERROR_FAILURE; + return m_dbFolderInfo->GetHighWater(key); +} + +// return the key of the first article number we know about. +// Since the iterator iterates in id order, we can just grab the +// messagekey of the first header it returns. +// ### dmb +// This will not deal with the situation where we get holes in +// the headers we know about. Need to figure out how and when +// to solve that. This could happen if a transfer is interrupted. +// Do we need to keep track of known arts permanently? +NS_IMETHODIMP nsNewsDatabase::GetLowWaterArticleNum(nsMsgKey* key) { + nsresult rv; + + nsCOMPtr<nsIMsgEnumerator> hdrs; + rv = EnumerateMessages(getter_AddRefs(hdrs)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIMsgDBHdr> first; + rv = hdrs->GetNext(getter_AddRefs(first)); + NS_ASSERTION(NS_SUCCEEDED(rv), "nsMsgDBEnumerator broken"); + if (NS_FAILED(rv)) return rv; + + return first->GetMessageKey(key); +} + +nsresult nsNewsDatabase::ExpireUpTo(nsMsgKey expireKey) { + return NS_ERROR_NOT_IMPLEMENTED; +} +nsresult nsNewsDatabase::ExpireRange(nsMsgKey startRange, nsMsgKey endRange) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsNewsDatabase::GetReadSet(nsMsgKeySet** pSet) { + if (!pSet) return NS_ERROR_NULL_POINTER; + *pSet = m_readSet; + return NS_OK; +} + +NS_IMETHODIMP nsNewsDatabase::SetReadSet(nsMsgKeySet* pSet) { + m_readSet = pSet; + + if (m_readSet) { + // compare this read set with the one in the db folder info. + // If not equivalent, sync with this one. + nsCString dbReadSet; + if (m_dbFolderInfo) m_dbFolderInfo->GetCharProperty("readSet", dbReadSet); + nsCString newsrcReadSet; + m_readSet->Output(getter_Copies(newsrcReadSet)); + if (!dbReadSet.Equals(newsrcReadSet)) SyncWithReadSet(); + } + return NS_OK; +} + +bool nsNewsDatabase::SetHdrReadFlag(nsIMsgDBHdr* msgHdr, bool bRead) { + nsresult rv; + bool isRead; + rv = IsHeaderRead(msgHdr, &isRead); + + if (isRead == bRead) { + // give the base class a chance to update m_flags. + nsMsgDatabase::SetHdrReadFlag(msgHdr, bRead); + return false; + } else { + nsMsgKey messageKey; + + // give the base class a chance to update m_flags. + nsMsgDatabase::SetHdrReadFlag(msgHdr, bRead); + rv = msgHdr->GetMessageKey(&messageKey); + if (NS_FAILED(rv)) return false; + + NS_ASSERTION(m_readSet, "m_readSet is null"); + if (!m_readSet) return false; + + if (!bRead) { +#ifdef DEBUG_NEWS_DATABASE + printf("remove %d from the set\n", messageKey); +#endif + + m_readSet->Remove(messageKey); + + rv = NotifyReadChanged(nullptr); + if (NS_FAILED(rv)) return false; + } else { +#ifdef DEBUG_NEWS_DATABASE + printf("add %d to the set\n", messageKey); +#endif + + if (m_readSet->Add(messageKey) < 0) return false; + + rv = NotifyReadChanged(nullptr); + if (NS_FAILED(rv)) return false; + } + } + return true; +} + +NS_IMETHODIMP nsNewsDatabase::MarkAllRead(nsTArray<nsMsgKey>& aThoseMarked) { + nsMsgKey lowWater = nsMsgKey_None, highWater; + nsCString knownArts; + if (m_dbFolderInfo) { + m_dbFolderInfo->GetKnownArtsSet(getter_Copies(knownArts)); + RefPtr<nsMsgKeySet> knownKeys = nsMsgKeySet::Create(knownArts.get()); + if (knownKeys) lowWater = knownKeys->GetFirstMember(); + } + if (lowWater == nsMsgKey_None) GetLowWaterArticleNum(&lowWater); + GetHighWaterArticleNum(&highWater); + if (lowWater > 2) m_readSet->AddRange(1, lowWater - 1); + nsresult err = nsMsgDatabase::MarkAllRead(aThoseMarked); + if (NS_SUCCEEDED(err) && 1 <= highWater) + m_readSet->AddRange(1, highWater); // mark everything read in newsrc. + + return err; +} + +nsresult nsNewsDatabase::SyncWithReadSet() { + // The code below attempts to update the underlying nsMsgDatabase's idea + // of read/unread flags to match the read set in the .newsrc file. It should + // only be called when they don't match, e.g., we crashed after committing the + // db but before writing out the .newsrc + nsCOMPtr<nsIMsgEnumerator> hdrs; + nsresult rv = EnumerateMessages(getter_AddRefs(hdrs)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore = false, readInNewsrc, isReadInDB, changed = false; + int32_t numMessages = 0, numUnreadMessages = 0; + + // Scan all messages in DB + while (NS_SUCCEEDED(rv = hdrs->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> header; + rv = hdrs->GetNext(getter_AddRefs(header)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = nsMsgDatabase::IsHeaderRead(header, &isReadInDB); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgKey messageKey; + header->GetMessageKey(&messageKey); + IsRead(messageKey, &readInNewsrc); + + numMessages++; + if (!readInNewsrc) numUnreadMessages++; + + // If DB and readSet disagree on Read/Unread, fix DB + if (readInNewsrc != isReadInDB) { + MarkHdrRead(header, readInNewsrc, nullptr); + changed = true; + } + } + + // Update FolderInfo Counters + if (m_dbFolderInfo) { + do { + int32_t oldMessages, oldUnreadMessages; + rv = m_dbFolderInfo->GetNumMessages(&oldMessages); + if (NS_FAILED(rv)) break; + if (oldMessages != numMessages) { + changed = true; + m_dbFolderInfo->ChangeNumMessages(numMessages - oldMessages); + } + rv = m_dbFolderInfo->GetNumUnreadMessages(&oldUnreadMessages); + if (NS_FAILED(rv)) break; + if (oldUnreadMessages != numUnreadMessages) { + changed = true; + m_dbFolderInfo->ChangeNumUnreadMessages(numUnreadMessages - + oldUnreadMessages); + } + } while (false); + } + + if (changed) Commit(nsMsgDBCommitType::kLargeCommit); + + return rv; +} + +nsresult nsNewsDatabase::AdjustExpungedBytesOnDelete(nsIMsgDBHdr* msgHdr) { + uint32_t msgFlags; + msgHdr->GetFlags(&msgFlags); + if (msgFlags & nsMsgMessageFlags::Offline && m_dbFolderInfo) { + uint32_t size = 0; + (void)msgHdr->GetOfflineMessageSize(&size); + return m_dbFolderInfo->ChangeExpungedBytes(size); + } + return NS_OK; +} + +NS_IMETHODIMP +nsNewsDatabase::GetDefaultViewFlags( + nsMsgViewFlagsTypeValue* aDefaultViewFlags) { + NS_ENSURE_ARG_POINTER(aDefaultViewFlags); + GetIntPref("mailnews.default_news_view_flags", aDefaultViewFlags); + if (*aDefaultViewFlags < nsMsgViewFlagsType::kNone || + *aDefaultViewFlags > + (nsMsgViewFlagsType::kThreadedDisplay | + nsMsgViewFlagsType::kShowIgnored | nsMsgViewFlagsType::kUnreadOnly | + nsMsgViewFlagsType::kExpandAll | nsMsgViewFlagsType::kGroupBySort)) + *aDefaultViewFlags = nsMsgViewFlagsType::kThreadedDisplay; + return NS_OK; +} + +NS_IMETHODIMP +nsNewsDatabase::GetDefaultSortType(nsMsgViewSortTypeValue* aDefaultSortType) { + NS_ENSURE_ARG_POINTER(aDefaultSortType); + GetIntPref("mailnews.default_news_sort_type", aDefaultSortType); + if (*aDefaultSortType < nsMsgViewSortType::byDate || + *aDefaultSortType > nsMsgViewSortType::byAccount) + *aDefaultSortType = nsMsgViewSortType::byThread; + return NS_OK; +} + +NS_IMETHODIMP +nsNewsDatabase::GetDefaultSortOrder( + nsMsgViewSortOrderValue* aDefaultSortOrder) { + NS_ENSURE_ARG_POINTER(aDefaultSortOrder); + GetIntPref("mailnews.default_news_sort_order", aDefaultSortOrder); + if (*aDefaultSortOrder != nsMsgViewSortOrder::descending) + *aDefaultSortOrder = nsMsgViewSortOrder::ascending; + return NS_OK; +} diff --git a/comm/mailnews/db/msgdb/test/moz.build b/comm/mailnews/db/msgdb/test/moz.build new file mode 100644 index 0000000000..6b37fdbe09 --- /dev/null +++ b/comm/mailnews/db/msgdb/test/moz.build @@ -0,0 +1,6 @@ +# 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.ini"] diff --git a/comm/mailnews/db/msgdb/test/unit/head_maildb.js b/comm/mailnews/db/msgdb/test/unit/head_maildb.js new file mode 100644 index 0000000000..5b52dbb304 --- /dev/null +++ b/comm/mailnews/db/msgdb/test/unit/head_maildb.js @@ -0,0 +1,21 @@ +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(); + +registerCleanupFunction(function () { + load("../../../../../mailnews/resources/mailShutdown.js"); +}); diff --git a/comm/mailnews/db/msgdb/test/unit/test_enumerator_cleanup.js b/comm/mailnews/db/msgdb/test/unit/test_enumerator_cleanup.js new file mode 100644 index 0000000000..dfb5aa5285 --- /dev/null +++ b/comm/mailnews/db/msgdb/test/unit/test_enumerator_cleanup.js @@ -0,0 +1,56 @@ +/* + * Test nsMsgDatabase's cleanup of nsMsgDBEnumerators + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var anyOldMessage = do_get_file("../../../../data/bugmail1"); + +/** + * Test closing a db with an outstanding enumerator. + */ +function test_enumerator_cleanup() { + let db = localAccountUtils.inboxFolder.msgDatabase; + let enumerator = db.enumerateMessages(); + Cc["@mozilla.org/msgDatabase/msgDBService;1"] + .getService(Ci.nsIMsgDBService) + .forceFolderDBClosed(localAccountUtils.inboxFolder); + localAccountUtils.inboxFolder.msgDatabase = null; + db = null; + gc(); + [...enumerator]; + do_test_finished(); +} + +/* + * This infrastructure down here exists just to get + * test_references_header_parsing its message header. + */ + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + do_test_pending(); + MailServices.copy.copyFileMessage( + anyOldMessage, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + messageHeaderGetterListener, + null + ); + return true; +} + +var messageHeaderGetterListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + GetMessageId(aMessageId) {}, + SetMessageKey(aKey) {}, + OnStopCopy(aStatus) { + do_timeout(0, test_enumerator_cleanup); + }, +}; diff --git a/comm/mailnews/db/msgdb/test/unit/test_filter_enumerator.js b/comm/mailnews/db/msgdb/test/unit/test_filter_enumerator.js new file mode 100644 index 0000000000..914b5afd29 --- /dev/null +++ b/comm/mailnews/db/msgdb/test/unit/test_filter_enumerator.js @@ -0,0 +1,100 @@ +/* import-globals-from ../../../../test/resources/MessageGenerator.jsm */ +load("../../../../resources/MessageGenerator.jsm"); + +var gMessages = []; + +const kSetCount = 13; +const kNumExpectedMatches = 10; + +function setupGlobals() { + localAccountUtils.loadLocalMailAccount(); + // Create a message generator + let messageGenerator = new MessageGenerator(); + let localInbox = localAccountUtils.inboxFolder.QueryInterface( + Ci.nsIMsgLocalMailFolder + ); + + for (let i = 0; i < kSetCount; i++) { + let message = messageGenerator.makeMessage(); + gMessages.push(message); + localInbox.addMessage(message.toMboxString()); + } +} + +function run_test() { + setupGlobals(); + do_test_pending(); + let inboxDB = localAccountUtils.inboxFolder.msgDatabase; + + // give messages 1,3,5 gloda-ids. These won't end up in our search hits. + let msgHdr1 = inboxDB.getMsgHdrForMessageID(gMessages[0].messageId); + msgHdr1.setUint32Property("gloda-id", 11111); + let msgHdr3 = inboxDB.getMsgHdrForMessageID(gMessages[2].messageId); + msgHdr3.setUint32Property("gloda-id", 33333); + let msgHdr5 = inboxDB.getMsgHdrForMessageID(gMessages[4].messageId); + msgHdr5.setUint32Property("gloda-id", 5555); + // set up a search term array that will give us the array of messages + // that gloda should index, as defined by this function: + let searchSession = Cc[ + "@mozilla.org/messenger/searchSession;1" + ].createInstance(Ci.nsIMsgSearchSession); + let searchTerms = []; + + searchSession.addScopeTerm( + Ci.nsMsgSearchScope.offlineMail, + localAccountUtils.inboxFolder + ); + let searchTerm = searchSession.createTerm(); + + // Create the following search term: + // (folderFlag & Mail && folderFlag != ImapBox) && + // msg property.gloda-id isEmpty + + searchTerm.beginsGrouping = true; + searchTerm.booleanAnd = true; + searchTerm.attrib = Ci.nsMsgSearchAttrib.FolderFlag; + searchTerm.op = Ci.nsMsgSearchOp.Is; + let value = searchTerm.value; + value.status = Ci.nsMsgFolderFlags.Mail; + value.attrib = Ci.nsMsgSearchAttrib.FolderFlag; + searchTerm.value = value; + searchTerms.push(searchTerm); + + searchTerm = searchSession.createTerm(); + searchTerm.booleanAnd = true; + searchTerm.attrib = Ci.nsMsgSearchAttrib.FolderFlag; + searchTerm.op = Ci.nsMsgSearchOp.Isnt; + value = searchTerm.value; + value.status = Ci.nsMsgFolderFlags.ImapBox; + value.attrib = Ci.nsMsgSearchAttrib.FolderFlag; + searchTerm.value = value; + searchTerm.endsGrouping = true; + searchTerms.push(searchTerm); + + searchTerm = searchSession.createTerm(); + searchTerm.booleanAnd = true; + searchTerm.attrib = Ci.nsMsgSearchAttrib.HdrProperty; + searchTerm.hdrProperty = "gloda-id"; + searchTerm.op = Ci.nsMsgSearchOp.IsEmpty; + value = searchTerm.value; + value.str = "gloda-id"; + value.attrib = Ci.nsMsgSearchAttrib.HdrProperty; + searchTerm.value = value; + searchTerms.push(searchTerm); + + let msgEnumerator = inboxDB.getFilterEnumerator(searchTerms); + let matchingHdrs = [...msgEnumerator]; + Assert.equal(kNumExpectedMatches, matchingHdrs.length); + Assert.equal(matchingHdrs[0].messageId, gMessages[1].messageId); + Assert.equal(matchingHdrs[1].messageId, gMessages[3].messageId); + + // try it backwards, with roller skates: + msgEnumerator = inboxDB.getFilterEnumerator(searchTerms, true); + matchingHdrs = [...msgEnumerator]; + Assert.equal(kNumExpectedMatches, matchingHdrs.length); + Assert.equal(matchingHdrs[0].messageId, gMessages[12].messageId); + Assert.equal(matchingHdrs[1].messageId, gMessages[11].messageId); + Assert.equal(matchingHdrs[9].messageId, gMessages[1].messageId); + + do_test_finished(); +} diff --git a/comm/mailnews/db/msgdb/test/unit/test_mailTelemetry.js b/comm/mailnews/db/msgdb/test/unit/test_mailTelemetry.js new file mode 100644 index 0000000000..c0bc034bad --- /dev/null +++ b/comm/mailnews/db/msgdb/test/unit/test_mailTelemetry.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Test telemetry related to mails read. + */ + +let { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +/** + * Check that we're counting mails read. + */ +add_task(async function test_mails_read() { + Services.telemetry.clearScalars(); + + localAccountUtils.loadLocalMailAccount(); + + const NUM_MAILS = 5; + let headers = + "from: alice@t1.example.com\r\n" + + "to: bob@t2.example.net\r\n" + + "return-path: alice@t1.example.com\r\n" + + "Disposition-Notification-To: alice@t1.example.com\r\n"; + for (let i = 0; i < NUM_MAILS; i++) { + localAccountUtils.inboxFolder.addMessage( + "From \r\n" + headers + "\r\nhello\r\n" + ); + } + localAccountUtils.inboxFolder.markAllMessagesRead(null); + const scalars = TelemetryTestUtils.getProcessScalars("parent"); + Assert.equal( + scalars["tb.mails.read"], + NUM_MAILS, + "Count of mails read must be correct." + ); +}); diff --git a/comm/mailnews/db/msgdb/test/unit/test_maildb.js b/comm/mailnews/db/msgdb/test/unit/test_maildb.js new file mode 100644 index 0000000000..9b6bca9303 --- /dev/null +++ b/comm/mailnews/db/msgdb/test/unit/test_maildb.js @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* + * Test suite for msg database functions. + */ + +/* import-globals-from ../../../../test/resources/MessageGenerator.jsm */ +load("../../../../resources/MessageGenerator.jsm"); + +var dbService; +var gTestFolder; +var gCurTestNum = 0; +var kNumTestMessages = 10; + +var gTestArray = [ + function test_db_open() { + dbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService( + Ci.nsIMsgDBService + ); + // Get the root folder + let root = localAccountUtils.incomingServer.rootFolder; + root.createSubfolder("dbTest", null); + gTestFolder = root.getChildNamed("dbTest"); + let db = dbService.openFolderDB(gTestFolder, true); + Assert.notEqual(db, null); + db.dBFolderInfo.highWater = 10; + db.close(true); + db = dbService.openFolderDB(gTestFolder, true); + Assert.notEqual(db, null); + Assert.equal(db.dBFolderInfo.highWater, 10); + db.dBFolderInfo.onKeyAdded(15); + Assert.equal(db.dBFolderInfo.highWater, 15); + db.close(true); + db.forceClosed(); + db = null; + doTest(++gCurTestNum); + }, +]; + +function doTest(test) { + if (test <= gTestArray.length) { + dump("Doing test " + test + "\n"); + gCurTestNum = test; + + 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 (gCurTestNum == test) { + do_throw( + "Notifications not received in 10000 ms for operation " + testFn.name + ); + } + }); + try { + testFn(); + } catch (ex) { + do_throw(ex); + } + } else { + do_test_finished(); // for the one in run_test() + } +} + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + do_test_pending(); + doTest(1); +} diff --git a/comm/mailnews/db/msgdb/test/unit/test_propertyEnumerator.js b/comm/mailnews/db/msgdb/test/unit/test_propertyEnumerator.js new file mode 100644 index 0000000000..57fb2605bd --- /dev/null +++ b/comm/mailnews/db/msgdb/test/unit/test_propertyEnumerator.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/. */ + +// tests properties in nsIMsgDBHdr; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var gHdr; + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + // Get a message into the local filestore. + // Function continue_test() continues the testing after the copy. + var bugmail1 = do_get_file("../../../../data/bugmail1"); + do_test_pending(); + MailServices.copy.copyFileMessage( + bugmail1, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + copyListener, + null + ); +} + +var copyListener = { + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + SetMessageKey(aKey) { + gHdr = localAccountUtils.inboxFolder.GetMessageHeader(aKey); + }, + SetMessageId(aMessageId) {}, + OnStopCopy(aStatus) { + continue_test(); + }, +}; + +function continue_test() { + // test some of the default properties + let properties = gHdr.properties; + Assert.ok(properties.includes("flags")); + Assert.ok(properties.includes("size")); + // this will be added in the next section, but does not exist yet + Assert.ok(!properties.includes("iamnew")); + + // add a new property, and make sure that it appears + gHdr.setStringProperty("iamnew", "somevalue"); + + properties = []; + for (let property of gHdr.properties) { + // dump("\nProperty 2 is " + property); + properties.push(property); + } + Assert.ok(properties.includes("flags")); + Assert.ok(properties.includes("size")); + Assert.ok(properties.includes("iamnew")); + Assert.ok(!properties.includes("idonotexist")); + + gHdr = null; + do_test_finished(); +} diff --git a/comm/mailnews/db/msgdb/test/unit/test_references_parsing.js b/comm/mailnews/db/msgdb/test/unit/test_references_parsing.js new file mode 100644 index 0000000000..fdfa76dd6d --- /dev/null +++ b/comm/mailnews/db/msgdb/test/unit/test_references_parsing.js @@ -0,0 +1,124 @@ +/* + * Test nsMsgHdr's In-Reply-To/References parsing logic. + */ + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var anyOldMessage = do_get_file("../../../../data/bugmail1"); + +var refsAndResults = [ + // an empty string is not a reference. + ["", []], + // super valid things + ["<abc@def>", ["abc@def"]], + [ + "<up@down> <left@right> <ying@yang>", + ["up@down", "left@right", "ying@yang"], + ], + // whitespace type things + [" ", []], + [" <left@space>", ["left@space"]], + ["<space@right> ", ["space@right"]], + [" <space@space> ", ["space@space"]], + ["\t<tab@tab>\t", ["tab@tab"]], + ["<a@b>\n\t<tab@newline.n>", ["a@b", "tab@newline.n"]], + ["<a@b>\r\t<tab@newline.r>", ["a@b", "tab@newline.r"]], + ["<a@b>\n\t<tab@newline.nr>", ["a@b", "tab@newline.nr"]], + [ + "<a@1>\n<a@2> <a@3>\t <a@4>\n <a@5>\r\t<a@6>\r\n <a@7>\r\n\t ", + ["a@1", "a@2", "a@3", "a@4", "a@5", "a@6", "a@7"], + ], + // be backwards compatible with old-school things that make some sense + ["i am a stupid message-id", ["i am a stupid message-id"]], + [" those were spaces!", ["those were spaces!"]], + // be backwards compatible with things that make no sense + [" seriously\n who does this?", ["seriously\n who does this?"]], + // handle things we used to be stupid about + ["<z@1a> was an awesome message!", ["z@1a"]], + [" <z@1b> was an awesomer message!", ["z@1b"]], + ["I can't get enough of <z@2a>", ["z@2a"]], + [" nor of I can enough get <z@2b> ", ["z@2b"]], + ["let's talk about <z@3a> shall we", ["z@3a"]], + ["and then let us speak of <z@3b> and its\n many points", ["z@3b"]], + // be backwards compatible with things that just seem malicious + [" 4 < 5", ["4 < 5"]], + [" 6 > 3", ["6 > 3"]], + [" look ma!\n newlines!", ["look ma!\n newlines!"]], +]; + +/** + * Parse the references in refsAndResults and ensure their references match + * the corresponding results. + * + * @param {nsIMsgDBHdr} aMsgHdr - A message header that you don't mind if we + * mess with. + */ +function test_references_header_parsing(aMsgHdr) { + var iCase, iResult, refString, results; + for (iCase = 0; iCase < refsAndResults.length; iCase++) { + refString = refsAndResults[iCase][0]; + results = refsAndResults[iCase][1]; + + dump("Setting references to: '" + refString + "'\n"); + aMsgHdr.setReferences(refString); + if (aMsgHdr.numReferences != results.length) { + dump("Length mismatch! Was expecting:\n"); + for (iResult = 0; iResult < results.length; iResult++) { + dump("'" + results[iResult] + "'\n"); + } + + dump("Got:\n"); + + for (iResult = 0; iResult < aMsgHdr.numReferences; iResult++) { + dump("'" + aMsgHdr.getStringReference(iResult) + "'\n"); + } + + Assert.equal(aMsgHdr.numReferences, results.length); + } + + for (iResult = 0; iResult < results.length; iResult++) { + Assert.equal(aMsgHdr.getStringReference(iResult), results[iResult]); + } + } + + do_test_finished(); +} + +/* + * This infrastructure down here exists just to get + * test_references_header_parsing its message header. + */ + +function run_test() { + localAccountUtils.loadLocalMailAccount(); + do_test_pending(); + MailServices.copy.copyFileMessage( + anyOldMessage, + localAccountUtils.inboxFolder, + null, + false, + 0, + "", + messageHeaderGetterListener, + null + ); + return true; +} + +var messageHeaderGetterListener = { + msgKey: null, + + OnStartCopy() {}, + OnProgress(aProgress, aProgressMax) {}, + GetMessageId(aMessageId) {}, + SetMessageKey(aKey) { + this.msgKey = aKey; + }, + OnStopCopy(aStatus) { + test_references_header_parsing( + localAccountUtils.inboxFolder.GetMessageHeader(this.msgKey) + ); + }, +}; diff --git a/comm/mailnews/db/msgdb/test/unit/xpcshell.ini b/comm/mailnews/db/msgdb/test/unit/xpcshell.ini new file mode 100644 index 0000000000..d32c984408 --- /dev/null +++ b/comm/mailnews/db/msgdb/test/unit/xpcshell.ini @@ -0,0 +1,10 @@ +[DEFAULT] +head = head_maildb.js +tail = + +[test_enumerator_cleanup.js] +[test_filter_enumerator.js] +[test_maildb.js] +[test_mailTelemetry.js] +[test_propertyEnumerator.js] +[test_references_parsing.js] |