diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/base/src | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
154 files changed, 60938 insertions, 0 deletions
diff --git a/comm/mailnews/base/src/ABQueryUtils.jsm b/comm/mailnews/base/src/ABQueryUtils.jsm new file mode 100644 index 0000000000..4944971ddf --- /dev/null +++ b/comm/mailnews/base/src/ABQueryUtils.jsm @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file contains helper methods for dealing with addressbook search URIs. + */ + +const EXPORTED_SYMBOLS = [ + "getSearchTokens", + "getModelQuery", + "modelQueryHasUserValue", + "generateQueryURI", + "encodeABTermValue", +]; + +/** + * Parse the multiword search string to extract individual search terms + * (separated on the basis of spaces) or quoted exact phrases to search + * against multiple fields of the addressbook cards. + * + * @param {string} aSearchString - The full search string entered by the user. + * + * @returns {Array} Array of separated search terms from the full search string. + */ +function getSearchTokens(aSearchString) { + // Trim leading and trailing whitespace and comma(s) to prevent empty search + // words when splitting unquoted parts of search string below. + let searchString = aSearchString + .replace(/^[,\s]+/, "") + .replace(/[,\s]+$/, ""); + if (searchString == "") { + return []; + } + + let quotedTerms = []; + + // Split up multiple search words to create a *foo* and *bar* search against + // search fields, using the OR-search template from modelQuery for each word. + // If the search query has quoted terms like "foo bar", extract them as is. + let startIndex; + while ((startIndex = searchString.indexOf('"')) != -1) { + let endIndex = searchString.indexOf('"', startIndex + 1); + if (endIndex == -1) { + endIndex = searchString.length; + } + + quotedTerms.push(searchString.substring(startIndex + 1, endIndex)); + let query = searchString.substring(0, startIndex); + if (endIndex < searchString.length) { + query += searchString.substr(endIndex + 1); + } + + searchString = query.trim(); + } + + let searchWords = []; + if (searchString.length != 0) { + // Split non-quoted search terms on whitespace and comma(s): Allow flexible + // incremental searches, and prevent false negatives for |Last, First| with + // |View > Show Name As > Last, First|, where comma is not found in data. + searchWords = quotedTerms.concat(searchString.split(/[,\s]+/)); + } else { + searchWords = quotedTerms; + } + + return searchWords; +} + +/** + * For AB quicksearch or recipient autocomplete, get the normal or phonetic model + * query URL part from prefs, allowing users to customize these searches. + * + * @param {string} aBasePrefName - The full pref name of default, non-phonetic + * model query, e.g. mail.addr_book.quicksearchquery.format. If phonetic + * search is used, corresponding pref must exist: + * e.g. mail.addr_book.quicksearchquery.format.phonetic + * @returns {boolean} depending on mail.addr_book.show_phonetic_fields pref, + * the value of aBasePrefName or aBasePrefName + ".phonetic" + */ +function getModelQuery(aBasePrefName) { + let modelQuery = ""; + if ( + Services.prefs.getComplexValue( + "mail.addr_book.show_phonetic_fields", + Ci.nsIPrefLocalizedString + ).data == "true" + ) { + modelQuery = Services.prefs.getCharPref(aBasePrefName + ".phonetic"); + } else { + modelQuery = Services.prefs.getCharPref(aBasePrefName); + } + // remove leading "?" to migrate existing customized values for mail.addr_book.quicksearchquery.format + // todo: could this be done in a once-off migration at install time to avoid repetitive calls? + if (modelQuery.startsWith("?")) { + modelQuery = modelQuery.slice(1); + } + return modelQuery; +} + +/** + * Check if the currently used pref with the model query was customized by user. + * + * @param {string} aBasePrefName - The full pref name of default, non-phonetic + * model query, e.g. mail.addr_book.quicksearchquery.format + * If phonetic search is used, corresponding pref must exist: + * e.g. mail.addr_book.quicksearchquery.format.phonetic + * @returns {boolean} true or false + */ +function modelQueryHasUserValue(aBasePrefName) { + if ( + Services.prefs.getComplexValue( + "mail.addr_book.show_phonetic_fields", + Ci.nsIPrefLocalizedString + ).data == "true" + ) { + return Services.prefs.prefHasUserValue(aBasePrefName + ".phonetic"); + } + return Services.prefs.prefHasUserValue(aBasePrefName); +} + +/* + * Given a database model query and a list of search tokens, + * return query URI. + * + * @param aModelQuery database model query + * @param aSearchWords an array of search tokens. + * + * @return query URI. + */ +function generateQueryURI(aModelQuery, aSearchWords) { + // If there are no search tokens, we simply return an empty string. + if (!aSearchWords || aSearchWords.length == 0) { + return ""; + } + + let queryURI = ""; + aSearchWords.forEach( + searchWord => + (queryURI += aModelQuery.replace(/@V/g, encodeABTermValue(searchWord))) + ); + + // queryURI has all the (or(...)) searches, link them up with (and(...)). + queryURI = "?(and" + queryURI + ")"; + + return queryURI; +} + +/** + * Encode the string passed as value into an addressbook search term. + * The '(' and ')' characters are special for the addressbook + * search query language, but are not escaped in encodeURIComponent() + * so must be done manually on top of it. + */ +function encodeABTermValue(aString) { + return encodeURIComponent(aString) + .replace(/\(/g, "%28") + .replace(/\)/g, "%29"); +} diff --git a/comm/mailnews/base/src/FolderLookupService.jsm b/comm/mailnews/base/src/FolderLookupService.jsm new file mode 100644 index 0000000000..9530dc6e6e --- /dev/null +++ b/comm/mailnews/base/src/FolderLookupService.jsm @@ -0,0 +1,163 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This module implements the folder lookup service (nsIFolderLookupService). + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["FolderLookupService"]; + +// This ensures that the service is only created once. +var gCreated = false; + +/** + * FolderLookupService maintains an index of folders and provides + * lookup by folder URI. + * + * @class + */ +function FolderLookupService() { + if (gCreated) { + throw Components.Exception("", Cr.NS_ERROR_ALREADY_INITIALIZED); + } + this._map = new Map(); + gCreated = true; +} + +FolderLookupService.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIFolderLookupService"]), + + /** + * Fetch the folder corresponding to the given URI. + * Will only return folders which already exist and have a parent. If this + * not the case then null is returned. + * + * @param {string} uri - URI of folder to get. + * @returns {nsIMsgFolder|null} + */ + getFolderForURL(uri) { + let folder = this._getExisting(uri); + + if (folder && !this._isValidFolder(folder)) { + folder = null; // no dangling folders! + } + return folder; + }, + + /** + * Fetch the folder corresponding to the given URI, creating it if it does + * not exist. If the folder is created, it will be a "dangling" folder, + * without a parent and not part of a normal folder hierarchy. + * A lot of code relies on this behaviour, but for new code this + * call should probably be avoided. + * + * @param {string} uri - URI of folder to get. + * @returns {nsIMsgFolder} + */ + getOrCreateFolderForURL(uri) { + let folder = this._getExisting(uri); + if (folder) { + return folder; + } + + // Create new folder. + + // Check that uri has an active scheme, in case this folder is from + // an extension that is currently disabled or hasn't started up yet. + let schemeMatch = uri.match(/^([-+.\w]+):/); + if (!schemeMatch) { + return null; + } + let scheme = schemeMatch[1]; + let contractID = "@mozilla.org/mail/folder-factory;1?name=" + scheme; + if (!(contractID in Cc)) { + console.error( + "getOrCreateFolderForURL: factory not registered for " + uri + ); + return null; + } + + let factory = Components.manager.getClassObject( + Cc[contractID], + Ci.nsIFactory + ); + if (!factory) { + console.error( + "getOrCreateFolderForURL: failed to get factory for " + uri + ); + return null; + } + + folder = factory.createInstance(Ci.nsIMsgFolder); + if (folder) { + folder.Init(uri); + // Add the new folder to our map. Store a weak reference instead, so that + // the folder can be closed when necessary. + let weakRef = folder + .QueryInterface(Ci.nsISupportsWeakReference) + .GetWeakReference(); + this._map.set(uri, weakRef); + } + + return folder; + }, + + /** + * Set pretty name again from original name on all folders, + * typically used when locale changes. + */ + setPrettyNameFromOriginalAllFolders() { + for (const val of this._map.values()) { + try { + let folder = val.QueryReferent(Ci.nsIMsgFolder); + folder.setPrettyNameFromOriginal(); + } catch (e) {} + } + }, + + // "private" stuff starts here. + + /** + * Internal helper to find a folder (which may or may not be dangling). + * + * @param {string} uri - URI of folder to look up. + * + * @returns {nsIMsgFolder|null} - The folder, if in the index, else null. + */ + _getExisting(uri) { + let folder = null; + // already created? + if (this._map.has(uri)) { + try { + folder = this._map.get(uri).QueryReferent(Ci.nsIMsgFolder); + } catch (e) { + // The object was deleted, so we can drop it. + this._map.delete(uri); + } + } + return folder; + }, + + /** + * Internal helper function to test if a folder is dangling or parented. + * Because we can return folders that don't exist, and we may be working + * with a deleted folder but we're still holding on to the reference. For + * valid folders, one of two scenarios is true: either the folder has a parent + * (the deletion code clears the parent to indicate its nonvalidity), or the + * folder is a root folder of some server. Getting the root folder may throw + * an exception if we attempted to create a server that doesn't exist, so we + * need to guard for that error. + * + * @returns {boolean} - true if folder valid (and parented). + */ + _isValidFolder(folder) { + try { + return folder.parent != null || folder.rootFolder == folder; + } catch (e) { + return false; + } + }, +}; diff --git a/comm/mailnews/base/src/FolderUtils.jsm b/comm/mailnews/base/src/FolderUtils.jsm new file mode 100644 index 0000000000..a438aa7480 --- /dev/null +++ b/comm/mailnews/base/src/FolderUtils.jsm @@ -0,0 +1,364 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * This file contains helper methods for dealing with nsIMsgFolders. + */ + +const EXPORTED_SYMBOLS = ["FolderUtils"]; + +var FolderUtils = { + allAccountsSorted, + compareAccounts, + folderNameCompare, + getFolderIcon, + getFolderProperties, + getMostRecentFolders, + getSpecialFolderString, + canRenameDeleteJunkMail, + isSmartTagsFolder, + isSmartVirtualFolder, +}; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/** + * Returns a string representation of a folder's "special" type. + * + * @param {nsIMsgFolder} aFolder - The folder whose special type to return. + * @returns {string} the special type of the folder. + */ +function getSpecialFolderString(aFolder) { + let flags = aFolder.flags; + if (flags & Ci.nsMsgFolderFlags.Inbox) { + return "Inbox"; + } + if (flags & Ci.nsMsgFolderFlags.Trash) { + return "Trash"; + } + if (flags & Ci.nsMsgFolderFlags.Queue) { + return "Outbox"; + } + if (flags & Ci.nsMsgFolderFlags.SentMail) { + return "Sent"; + } + if (flags & Ci.nsMsgFolderFlags.Drafts) { + return "Drafts"; + } + if (flags & Ci.nsMsgFolderFlags.Templates) { + return "Templates"; + } + if (flags & Ci.nsMsgFolderFlags.Junk) { + return "Junk"; + } + if (flags & Ci.nsMsgFolderFlags.Archive) { + return "Archive"; + } + if (flags & Ci.nsMsgFolderFlags.Virtual) { + return "Virtual"; + } + return "none"; +} + +/** + * This function is meant to be used with trees. It returns the property list + * for all of the common properties that css styling is based off of. + * + * @param {nsIMsgFolder} aFolder - The folder whose properties should be + * returned as a string. + * @param {boolean} aOpen - Whether the folder is open (not expanded). + * + * @returns {string} A string of the property names, delimited by space. + */ +function getFolderProperties(aFolder, aOpen) { + const nsIMsgFolder = Ci.nsIMsgFolder; + let properties = []; + + properties.push("folderNameCol"); + + properties.push("serverType-" + aFolder.server.type); + + // set the SpecialFolder attribute + properties.push("specialFolder-" + getSpecialFolderString(aFolder)); + + // Now set the biffState + switch (aFolder.biffState) { + case nsIMsgFolder.nsMsgBiffState_NewMail: + properties.push("biffState-NewMail"); + break; + case nsIMsgFolder.nsMsgBiffState_NoMail: + properties.push("biffState-NoMail"); + break; + default: + properties.push("biffState-UnknownMail"); + } + + properties.push("isSecure-" + aFolder.server.isSecure); + + // A folder has new messages, or a closed folder or any subfolder has new messages. + if ( + aFolder.hasNewMessages || + (!aOpen && aFolder.hasSubFolders && aFolder.hasFolderOrSubfolderNewMessages) + ) { + properties.push("newMessages-true"); + } + + if (aFolder.isServer) { + properties.push("isServer-true"); + } else { + // We only set this if we're not a server + let shallowUnread = aFolder.getNumUnread(false); + if (shallowUnread > 0) { + properties.push("hasUnreadMessages-true"); + } else { + // Make sure that shallowUnread isn't negative + shallowUnread = 0; + } + let deepUnread = aFolder.getNumUnread(true); + if (deepUnread - shallowUnread > 0) { + properties.push("subfoldersHaveUnreadMessages-true"); + } + } + + properties.push("noSelect-" + aFolder.noSelect); + properties.push("imapShared-" + aFolder.imapShared); + + return properties.join(" "); +} + +/** + * Returns the sort order value based on the server type to be used for sorting. + * The servers (accounts) go in the following order: + * (0) default account, (1) other mail accounts, (2) Local Folders, + * (3) IM accounts, (4) RSS, (5) News, (9) others (no server) + * This ordering is encoded in the .sortOrder property of each server type. + * + * @param {nsIMsgIncomingServer} aServer -The server to get sort order for. + */ +function getServerSortOrder(aServer) { + // If there is no server sort this object to the end. + if (!aServer) { + return 999999999; + } + + // Otherwise get the server sort order from the Account manager. + return MailServices.accounts.getSortOrder(aServer); +} + +/** + * Compares the passed in accounts according to their precedence. + */ +function compareAccounts(aAccount1, aAccount2) { + return ( + getServerSortOrder(aAccount1.incomingServer) - + getServerSortOrder(aAccount2.incomingServer) + ); +} + +/** + * Returns a list of accounts sorted by server type. + * + * @param {boolean} aExcludeIMAccounts - Remove IM accounts from the list? + */ +function allAccountsSorted(aExcludeIMAccounts) { + // This is a HACK to work around bug 41133. If we have one of the + // dummy "news" accounts there, that account won't have an + // incomingServer attached to it, and everything will blow up. + let accountList = MailServices.accounts.accounts.filter( + a => a.incomingServer + ); + + // Remove IM servers. + if (aExcludeIMAccounts) { + accountList = accountList.filter(a => a.incomingServer.type != "im"); + } + + return accountList; +} + +/** + * Returns the most recently used/modified folders from the passed in list. + * + * @param {nsIMsgFolder[]} aFolderList - The array of folders to search + * for recent folders. + * @param {integer} aMaxHits - How many folders to return. + * @param {"MRMTime"|"MRUTime"} aTimeProperty - Which folder time property to + * use. Use "MRMTime" for most recently modified time. + * Use "MRUTime" for most recently used time. + */ +function getMostRecentFolders(aFolderList, aMaxHits, aTimeProperty) { + let recentFolders = []; + const monthOld = Math.floor((Date.now() - 31 * 24 * 60 * 60 * 1000) / 1000); + + /** + * This sub-function will add a folder to the recentFolders array if it + * is among the aMaxHits most recent. If we exceed aMaxHits folders, + * it will pop the oldest folder, ensuring that we end up with the + * right number. + * + * @param {nsIMsgFolders} aFolder - The folder to check for recency. + */ + let oldestTime = 0; + function addIfRecent(aFolder) { + let time = 0; + try { + time = Number(aFolder.getStringProperty(aTimeProperty)) || 0; + } catch (e) {} + if (time <= oldestTime || time < monthOld) { + return; + } + + if (recentFolders.length == aMaxHits) { + recentFolders.sort((a, b) => a.time < b.time); + recentFolders.pop(); + oldestTime = recentFolders[recentFolders.length - 1].time; + } + recentFolders.push({ folder: aFolder, time }); + } + + for (let folder of aFolderList) { + addIfRecent(folder); + } + + return recentFolders.map(f => f.folder); +} + +/** + * A locale dependent comparison function to produce a case-insensitive sort order + * used to sort folder names. + * + * @param {string} aString1 - First string to compare. + * @param {string} aString2 - Second string to compare. + * @returns {interger} A positive number if aString1 > aString2, + * negative number if aString1 > aString2, otherwise 0. + */ +function folderNameCompare(aString1, aString2) { + // TODO: improve this as described in bug 992651. + return aString1 + .toLocaleLowerCase() + .localeCompare(aString2.toLocaleLowerCase()); +} + +/** + * Get the icon to use for this folder. + * + * @param {nsIMsgFolder} folder - The folder to get icon for. + * @returns {string} URL of suitable icon. + */ +function getFolderIcon(folder) { + let iconName; + if (folder.isServer) { + switch (folder.server.type) { + case "nntp": + iconName = folder.server.isSecure ? "globe-secure.svg" : "globe.svg"; + break; + case "imap": + case "pop": + iconName = folder.server.isSecure ? "mail-secure.svg" : "mail.svg"; + break; + case "none": + iconName = "folder.svg"; + break; + case "rss": + iconName = "rss.svg"; + break; + default: + iconName = "mail.svg"; + break; + } + } else if (folder.server.type == "nntp") { + iconName = "newsletter.svg"; + } else { + switch (getSpecialFolderString(folder)) { + case "Virtual": + if (isSmartTagsFolder(folder)) { + iconName = "tag.svg"; + } else { + iconName = "folder-filter.svg"; + } + break; + case "Junk": + iconName = "spam.svg"; + break; + case "Templates": + iconName = "template.svg"; + break; + case "Archive": + iconName = "archive.svg"; + break; + case "Trash": + iconName = "trash.svg"; + break; + case "Drafts": + iconName = "draft.svg"; + break; + case "Outbox": + iconName = "outbox.svg"; + break; + case "Sent": + iconName = "sent.svg"; + break; + case "Inbox": + iconName = "inbox.svg"; + break; + default: + iconName = "folder.svg"; + break; + } + } + + return `chrome://messenger/skin/icons/new/compact/${iconName}`; +} + +/** + * Checks if `folder` is a virtual folder for the Unified Folders pane mode. + * + * @param {nsIMsgFolder} folder + * @returns {boolean} + */ +function isSmartVirtualFolder(folder) { + return ( + folder.isSpecialFolder(Ci.nsMsgFolderFlags.Virtual) && + folder.server.hostName == "smart mailboxes" && + folder.parent?.isServer + ); +} + +/** + * Checks if `folder` is a virtual folder for the Tags folder pane mode. + * + * @param {nsIMsgFolder} folder + * @returns {boolean} + */ +function isSmartTagsFolder(folder) { + return ( + folder.isSpecialFolder(Ci.nsMsgFolderFlags.Virtual) && + folder.server.hostName == "smart mailboxes" && + folder.parent?.name == "tags" + ); +} + +/** + * Checks if the configured junk mail can be renamed or deleted. + * + * @param {string} aFolderUri + */ +function canRenameDeleteJunkMail(aFolderUri) { + // Go through junk mail settings for all servers and see if the folder is set/used by anyone. + for (let server of MailServices.accounts.allServers) { + let settings = server.spamSettings; + // If junk mail control or move junk mail to folder option is disabled then + // allow the folder to be removed/renamed since the folder is not used in this case. + if (!settings.level || !settings.moveOnSpam) { + continue; + } + if (settings.spamFolderURI == aFolderUri) { + return false; + } + } + + return true; +} diff --git a/comm/mailnews/base/src/HeaderReader.h b/comm/mailnews/base/src/HeaderReader.h new file mode 100644 index 0000000000..da8defbbe0 --- /dev/null +++ b/comm/mailnews/base/src/HeaderReader.h @@ -0,0 +1,305 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef HeaderReader_h__ +#define HeaderReader_h__ + +#include <algorithm> +#include "LineReader.h" +#include "nsMsgUtils.h" +#include "nsString.h" +#include "mozilla/Span.h" + +/** + * HeaderReader parses mail headers from a buffer. + * The input is fed in via Parse(), and a callback function is invoked for + * each header encountered. + * + * General goals: + * + * - Incremental. Parse() can be called multiple times as a buffer grows. + * - Works in-place. Headers are returned as byte ranges within the data. + * - Works with a partial header block (e.g. sniffing the first N bytes + * of a message file). It won't mistakenly emit an incomplete header. + * - Track exact byte offsets for values, to support rewriting headers in + * place. This is needed to support X-Mozilla-Status et al. + * - Avoids copying data where possible. + * - Callback is inlined. + * - Callback can halt processing (by returning false). + * - Tolerant of real-world oddness in input data (for now, we just skip + * lines which don't make sense). + * + * Example usage: + * nsCString raw = "To: Alice\r\nFrom: Bob\r\n\r\n...Message body..."_ns; + * auto cb = [&](HeaderReader::Header const& hdr) { + * printf("-> '%s':'%s'\n", hdr.Name(raw), hdr.Value(raw)); + * return true; + * }; + * + * HeaderReader rdr; + * rdr.Parse(raw, cb); + * // -> 'To':'Alice' + * // -> 'From':'Bob' + * + * See TestHeaderReader.cpp for more examples. + */ +class HeaderReader { + public: + /** + * Parse() scans an input buffer and invokes a callback for each complete + * header found. + * + * It can be called any number of times - it'll pick up where it left off. + * The idea is that the caller can accumulate data in multiple chunks and + * call Parse() to extract headers incrementally as they come in. + * It does rely on data being a single contiguous allocation, but it + * doesn't require the data being located in the same memory location + * each time. So can it can be safely used on a growable buffer. + * + * Signature of callback is: + * bool hdrCallback(HeaderReader::Hdr const& hdr); + * + * The callback should return true to continue parsing, or false to halt. + * This allows, for example, an early-out if you're scanning for one + * specific header and don't care about the rest. + * + * Parse() stops when one of these conditions is true: + * 1. The end of the header block is reached (the final blank line marker + * is consumed). Subsequent calls to IsComplete() will return true. + * 2. The callback returns false. If Parse() is called again, it will + * safely pick up where it left off. + * 3. No more headers can be read. There may be some unconsumed data + * returned (eg a partial line). Parse() can be safely called again + * when more data becomes available. It will resume from the point it + * reached previously. + * + * It is safe to call Parse() on a truncated header block. It will only + * invoke the callback for headers which are unambiguously complete. + * + * @param data - bytes containing the header block to parse. + * @param hdrCallback - callback to invoke for each header found + * + * @returns a span containing the unconsumed (leftover) data. + */ + template <typename HeaderFn> + mozilla::Span<const char> Parse(mozilla::Span<const char> data, + HeaderFn hdrCallback); + + /** + * Complete() returns true if the header block has been fully parsed. + * Further calls to Parse() will consume no more data. + * The blank line which separates the header block from the body is consumed. + */ + bool IsComplete() const { return mFinished; } + + /** + * Hdr holds offsets to a name/value pair within a header block. + * The name starts at pos. + * The value starts at pos+rawValOffset. + */ + struct Hdr { + uint32_t pos{0}; // Start position of header within the block. + uint32_t len{0}; // Length of entire header, including final EOL. + uint32_t nameLen{0}; // Length of name. + uint32_t rawValOffset{0}; // Where the value starts, relative to pos. + uint32_t rawValLen{0}; // Excludes final EOL. + bool IsEmpty() const { return len == 0; } + + /** + * Access the header name as a string. + * + * @param data - the data originally passed into Parse(). + * @returns the name within data, wrapped for string access (so it is + * valid only as long as data is valid). + */ + nsDependentCSubstring Name(mozilla::Span<const char> data) const { + return nsDependentCSubstring(data.Elements() + pos, nameLen); + } + /** + * Access the raw value as a string. + * + * @param data - the data originally passed into Parse(). + * @returns the raw data, EOLs and all, wrapped for string access (so it + * is valid only as long as data is valid). + */ + nsDependentCSubstring RawValue(mozilla::Span<const char> data) const { + return nsDependentCSubstring(data.Elements() + pos + rawValOffset, + rawValLen); + } + /** + * Decode the 'cooked' value into a string. + * NOTE: handles unfolding multi-line values. No attempt (yet) at dealing + * with comments or quoted strings... + * + * @param data - the data originally passed into Parse(). + * @returns a new string containing the value. + */ + nsCString Value(mozilla::Span<const char> data) const { + nsCString val(RawValue(data)); + val.ReplaceSubstring("\r\n"_ns, ""_ns); + val.ReplaceSubstring("\n"_ns, ""_ns); + return val; + } + + /** + * EOL() returns a string containing the eol characters at the end of the + * header. It will be "\n" or "\r\n". + * Calling this on an empty hdr struct is unsupported. + */ + nsDependentCSubstring EOL(mozilla::Span<const char> data) const { + MOZ_ASSERT(len >= 2); // Empty or malformed? + + uint32_t i = pos + len; + int n = 0; + if (data[i - 1] == '\n') { + ++n; + if (data[i - 2] == '\r') { + ++n; + } + } + return nsDependentCSubstring(data.Elements() + pos + len - n, n); + } + }; + + private: + // How far Parse() has gone so far. + uint32_t mPos{0}; + + // The current header we're accumulating. + Hdr mHdr; + + // Number of EOL chars at the end of previous line (so we can strip it if the + // next line is folded). + int mEOLSize{0}; + + // Set when end of header block detected. + bool mFinished{false}; + + template <typename HeaderFn> + bool HandleLine(mozilla::Span<const char> line, HeaderFn hdrCallback); +}; + +// Parse() implementation. +template <typename HeaderFn> +mozilla::Span<const char> HeaderReader::Parse(mozilla::Span<const char> data, + HeaderFn hdrCallback) { + // If were're resuming, skip what we've already scanned. + auto remaining = mozilla::Span<const char>(data.cbegin() + mPos, data.cend()); + if (mFinished) { + return remaining; + } + // Iterate over all the lines of our input. + remaining = SplitLines(remaining, + [this, hdrCallback](mozilla::Span<const char> line) { + return HandleLine(line, hdrCallback); + }); + + if (!mFinished) { + // We didn't get to the end of the header block, but we may still be + // able to finalise a previously-started header... + if (!mHdr.IsEmpty()) { + if (remaining.Length() > 0 && remaining[0] != ' ' && + remaining[0] != '\t') { + // Next line isn't folded, so we know the header is complete. + mHdr.rawValLen -= mEOLSize; + hdrCallback(mHdr); + } else { + // Can't tell if header is complete. Rewind and try again next time. + mPos = mHdr.pos; + remaining = + mozilla::Span<const char>(data.cbegin() + mPos, data.cend()); + } + mHdr = Hdr(); + } + } + return remaining; +} + +// Helper function - we call this on each complete line we encounter. +template <typename HeaderFn> +bool HeaderReader::HandleLine(mozilla::Span<const char> line, + HeaderFn hdrCallback) { + // Should never be here if we've finished. + MOZ_ASSERT(!mFinished); + // we should _never_ see empty strings. + MOZ_ASSERT(!line.IsEmpty()); + + // Find the EOL sequence (CRLF or LF). + auto eol = line.cend(); + auto p = eol; + if (p > line.cbegin() && *(p - 1) == '\n') { + --eol; + if ((p - 1) > line.cbegin() && *(p - 2) == '\r') { + --eol; + } + } + // We should never have been called with a non-terminated line. + MOZ_ASSERT(eol != line.cend()); + + // Blank line indicates end of header block. + if (eol == line.cbegin()) { + if (!mHdr.IsEmpty()) { + // Emit the completed header. + mHdr.rawValLen -= mEOLSize; + hdrCallback(mHdr); + mHdr = Hdr(); + } + mFinished = true; + mPos += line.Length(); + return false; // Stop. + } + + // A folded line? + // Leading space or tab indicates continuation of previous value. + if (line[0] == ' ' || line[0] == '\t') { + if (!mHdr.IsEmpty()) { + // Grow the existing header. + mHdr.len += line.Length(); + mHdr.rawValLen += line.Length(); + mEOLSize = line.cend() - eol; + } else { + // UHOH - a folded value but we haven't started a header... + // Not much we can do, so we'll just ignore the line. + NS_WARNING("Malformed header (bare continuation)"); + } + mPos += line.Length(); + return true; // Next line, please. + } + + bool keepGoing = true; + // By now, we're expecting a "name: value" line, to start a fresh header. + if (!mHdr.IsEmpty()) { + // Flush previous header now we know it's complete. + mHdr.rawValLen -= mEOLSize; + keepGoing = hdrCallback(mHdr); + mHdr = Hdr(); + } + + auto colon = std::find(line.cbegin(), line.cend(), ':'); + if (colon == line.cend()) { + // UHOH. We were expecting a "name: value" line, but didn't find one. + // Just ignore this line. + NS_WARNING("Malformed header (expected 'name: value')"); + mPos += line.Length(); + return keepGoing; + } + auto val = colon + 1; + if (*val == ' ' || *val == '\t') { + // Skip single leading whitespace. + ++val; + } + + // Start filling out the new header (it may grow if folded lines come next). + mHdr.pos = mPos; + mHdr.len = line.Length(); + mHdr.nameLen = colon - line.cbegin(); + + mHdr.rawValOffset = val - line.cbegin(); + mHdr.rawValLen = line.cend() - val; + mEOLSize = line.cend() - eol; + mPos += line.Length(); + return keepGoing; +} + +#endif diff --git a/comm/mailnews/base/src/JXON.jsm b/comm/mailnews/base/src/JXON.jsm new file mode 100644 index 0000000000..00c1f2bb1f --- /dev/null +++ b/comm/mailnews/base/src/JXON.jsm @@ -0,0 +1,159 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This is a modification of the JXON parsers found on the page +// <https://developer.mozilla.org/en-US/docs/JXON> + +var EXPORTED_SYMBOLS = ["JXON"]; + +var JXON = new (function () { + const sValueProp = "value"; /* you can customize these values */ + const sAttributesProp = "attr"; + const sAttrPref = "@"; + const sElementListPrefix = "$"; + const sConflictSuffix = "_"; // used when there's a name conflict with special JXON properties + const aCache = []; + const rIsBool = /^(?:true|false)$/i; + + function parseText(sValue) { + if (rIsBool.test(sValue)) { + return sValue.toLowerCase() === "true"; + } + if (isFinite(sValue)) { + return parseFloat(sValue); + } + if (isFinite(Date.parse(sValue))) { + return new Date(sValue); + } + return sValue; + } + + function EmptyTree() {} + EmptyTree.prototype = { + toString() { + return "null"; + }, + valueOf() { + return null; + }, + }; + + function objectify(vValue) { + if (vValue === null) { + return new EmptyTree(); + } else if (vValue instanceof Object) { + return vValue; + } + return new vValue.constructor(vValue); // What does this? copy? + } + + function createObjTree(oParentNode, nVerb, bFreeze, bNesteAttr) { + const nLevelStart = aCache.length; + const bChildren = oParentNode.hasChildNodes(); + const bAttributes = oParentNode.attributes && oParentNode.attributes.length; + const bHighVerb = Boolean(nVerb & 2); + + var sProp = 0; + var vContent = 0; + var nLength = 0; + var sCollectedTxt = ""; + var vResult = bHighVerb + ? {} + : /* put here the default value for empty nodes: */ true; + + if (bChildren) { + for ( + var oNode, nItem = 0; + nItem < oParentNode.childNodes.length; + nItem++ + ) { + oNode = oParentNode.childNodes.item(nItem); + if (oNode.nodeType === 4) { + // CDATASection + sCollectedTxt += oNode.nodeValue; + } else if (oNode.nodeType === 3) { + // Text + sCollectedTxt += oNode.nodeValue; + } else if (oNode.nodeType === 1) { + // Element + aCache.push(oNode); + } + } + } + + const nLevelEnd = aCache.length; + const vBuiltVal = parseText(sCollectedTxt); + + if (!bHighVerb && (bChildren || bAttributes)) { + vResult = nVerb === 0 ? objectify(vBuiltVal) : {}; + } + + for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) { + sProp = aCache[nElId].nodeName; + if (sProp == sValueProp || sProp == sAttributesProp) { + sProp = sProp + sConflictSuffix; + } + vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr); + if (!vResult.hasOwnProperty(sProp)) { + vResult[sProp] = vContent; + vResult[sElementListPrefix + sProp] = []; + } + vResult[sElementListPrefix + sProp].push(vContent); + nLength++; + } + + if (bAttributes) { + const nAttrLen = oParentNode.attributes.length; + const sAPrefix = bNesteAttr ? "" : sAttrPref; + const oAttrParent = bNesteAttr ? {} : vResult; + + for (var oAttrib, nAttrib = 0; nAttrib < nAttrLen; nLength++, nAttrib++) { + oAttrib = oParentNode.attributes.item(nAttrib); + oAttrParent[sAPrefix + oAttrib.name] = parseText(oAttrib.value); + } + + if (bNesteAttr) { + if (bFreeze) { + Object.freeze(oAttrParent); + } + vResult[sAttributesProp] = oAttrParent; + nLength -= nAttrLen - 1; + } + } + + if ( + nVerb === 3 || + ((nVerb === 2 || (nVerb === 1 && nLength > 0)) && sCollectedTxt) + ) { + vResult[sValueProp] = vBuiltVal; + } else if (!bHighVerb && nLength === 0 && sCollectedTxt) { + vResult = vBuiltVal; + } + + if (bFreeze && (bHighVerb || nLength > 0)) { + Object.freeze(vResult); + } + + aCache.length = nLevelStart; + + return vResult; + } + + this.build = function ( + oXMLParent, + nVerbosity /* optional */, + bFreeze /* optional */, + bNesteAttributes /* optional */ + ) { + const _nVerb = + typeof nVerbosity === "number" + ? nVerbosity & 3 + : /* put here the default verbosity level: */ 1; + return createObjTree( + oXMLParent, + _nVerb, + bFreeze || false, + bNesteAttributes !== undefined ? bNesteAttributes : _nVerb === 3 + ); + }; +})(); diff --git a/comm/mailnews/base/src/LineReader.h b/comm/mailnews/base/src/LineReader.h new file mode 100644 index 0000000000..292c7ced7c --- /dev/null +++ b/comm/mailnews/base/src/LineReader.h @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef LineReader_h__ +#define LineReader_h__ + +#include <algorithm> +#include "mozilla/Span.h" +#include "mozilla/Vector.h" + +/** + * FirstLine() returns the first line of a span. + * The EOL sequence (CRLF or LF) is included in the returned line. + * If no lines are found an empty span is returned. + */ +inline mozilla::Span<const char> FirstLine( + mozilla::Span<const char> const& data) { + auto eol = std::find(data.cbegin(), data.cend(), '\n'); + if (eol == data.cend()) { + // no line ending found - return empty span. + return data.First(0); + } + ++eol; + return mozilla::Span<const char>(data.cbegin(), eol); +} + +/** + * LineReader breaks up continuous character streams into lines. + * Data is fed in by calling Feed() as often as required, and a + * callback function is invoked to handle each resulting line. + * + * The resulting lines include the end-of-line char(s), except for any + * non-terminated final line. + * LF ('\n') is used as the line terminator. CRLF-terminated lines will + * be handled correctly - the resultant lines will include the line + * terminators exactly as they appear in the input data. + * + * Goals for LineReader: + * - Byte exact. The bytes fed in will appear _exactly_ in the callback fn. + * - Callback can be inlined (due to templating). + * - Avoid copying data if possible. The internal buffer is only used when + * lines are split across incoming chunks of data. + * - Tries to avoid heap allocation. If the internal buffer is used, it'll + * only allocate memory for long lines (>80 chars). + * + * Example usage: + * + * auto callback = [](mozilla::Span<const char> line) { + * printf("%s\n", nsCString(line).get()); + * return true; + * }; + * + * LineReader c; + * c.Feed("Line 1\r\nLine 2\r\nLine 3", callback); + * // -> "Line 1\r\n" + * // -> "Line 2\r\n" + * c.Feed("\r\nLeftovers.", callback); + * // -> "Line 3\r\n" + * c.Flush(callback); + * // -> "Leftovers." + * + * See TestLineReader.cpp for more examples. + */ +class LineReader { + public: + /* + * Feed() takes in a chunk of data to be split up into lines. You can call + * this as often as required to feed in all your data. Don't forget to call + * Flush() after the last Feed(), in case the last line has no line endings! + * + * The callback will be invoked once for each full line extracted. + * It should have the form: + * The callback is of the form: + * bool callback(mozilla::Span<const char> line); + * + * The data in `line` should be considered valid only until the callback + * returns. So if the callback wants to retain data it needs to copy it. + * `line` will include any EOL character(s). + * The callback should return true to continue processing. + * If the callback returns false, processing will stop, even if there is + * more data available. + */ + template <typename LineFn> + void Feed(mozilla::Span<const char> data, LineFn callback) { + bool keepGoing = true; + while (!data.IsEmpty() && keepGoing) { + auto eol = std::find(data.cbegin(), data.cend(), '\n'); + if (eol == data.cend()) { + // No LF. Just collect and wait for more. + // TODO: limit maximum mBuf size, to stop maliciously-crafted input + // OOMing us? + if (!mBuf.append(data.data(), data.size())) { + NS_ERROR("OOM!"); + } + return; + } + + // Consume everything up to and including the LF. + ++eol; + mozilla::Span<const char> line(data.cbegin(), eol); + data = mozilla::Span<const char>(eol, data.cend()); + + if (mBuf.empty()) { + // Pass the data through directly, no copying. + keepGoing = callback(line); + } else { + // Complete the line we previously started. + if (!mBuf.append(line.data(), line.size())) { + NS_ERROR("OOM!"); + } + keepGoing = callback(mBuf); + mBuf.clear(); + } + } + } + + /* + * Flush() will invoke the callback with any leftover data, after the last + * Feed() call has completed. + * The line passed to the callback will be a partial line, without a final + * LF. If the input data has a final LF, there will be nothing to flush, + * and the callback will not be invoked. + */ + template <typename LineFn> + void Flush(LineFn callback) { + if (!mBuf.empty()) { + callback(mBuf); + mBuf.clear(); + } + } + + private: + // Growable buffer, to collect lines which come in as multiple parts. + // Can handle lines up to 80 chars before needing to reallocate. + mozilla::Vector<char, 80> mBuf; +}; + +/** + * SplitLines() invokes a callback for every complete line it finds in the + * input data. + * + * The callback is of the form: + * bool callback(mozilla::Span<const char> line); + * where line is a span pointing to the range of bytes in the input data + * which comprises the line. + * + * If the callback returns false, processing is halted. + * + * The lines passed to the callback include end-of-line (EOL) character(s). + * + * Lines are considered terminated by '\n' (LF) but this means CRLF-delimited + * data is also handled correctly. + * + * This function is byte-exact: if you concatenate all the line spans, along + * with the unconsumed data returned at the end, you'll end up with the exact + * same byte sequence as the original input data. + * + * @param data - The input bytes. + * @param callback - The callback to invoke for each line. + * + * @returns the unconsumed data. Usually this will be empty, or an incomplete + * line at the end (with no EOL). However if the callback returned + * false, all the unused data will be returned. + */ +template <typename LineFn> +mozilla::Span<const char> SplitLines(mozilla::Span<const char> data, + LineFn callback) { + while (!data.IsEmpty()) { + auto eol = std::find(data.cbegin(), data.cend(), '\n'); + if (eol == data.cend()) { + // No LF - we're done. May or may not be some leftover data. + break; + } + + // Consume everything up to and including the LF. + ++eol; + mozilla::Span<const char> line(data.cbegin(), eol); + data = mozilla::Span<const char>(eol, data.cend()); + + if (callback(line) == false) { + break; + } + } + return data; +} + +#endif diff --git a/comm/mailnews/base/src/LineReader.jsm b/comm/mailnews/base/src/LineReader.jsm new file mode 100644 index 0000000000..2417457e3c --- /dev/null +++ b/comm/mailnews/base/src/LineReader.jsm @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["LineReader"]; + +/** + * For a single request, mail servers may return several multi-line responses. A + * definition of multi-line responses can be found at rfc3977#section-3.1.1. + * + * This class helps dealing with multi-line responses by: + * - Break up a response to lines + * - Join incomplete line from a previous response with the current response + * - Remove stuffed dot (.. at the beginning of a line) + * - Detect the end of the response (\r\n.\r\n) + */ +class LineReader { + processingMultiLineResponse = false; + _data = ""; + + /** + * Read a multi-line response, emit each line through a callback. + * + * @param {string} data - A multi-line response received from the server. + * @param {Function} lineCallback - A line will be passed to the callback each + * time. + * @param {Function} doneCallback - A function to be called when data is ended. + */ + read(data, lineCallback, doneCallback) { + this._data += data; + if (this._data == ".\r\n" || this._data.endsWith("\r\n.\r\n")) { + this.processingMultiLineResponse = false; + this._data = this._data.slice(0, -3); + } else { + this.processingMultiLineResponse = true; + } + if (this._running) { + // This function can be called multiple times, but this._data should only + // be consumed once. + return; + } + + let i = 0; + this._running = true; + while (this._data) { + let index = this._data.indexOf("\r\n"); + if (index == -1) { + // Not enough data, save it for the next round. + break; + } + let line = this._data.slice(0, index + 2); + if (line.startsWith("..")) { + // Remove stuffed dot. + line = line.slice(1); + } + lineCallback(line); + this._data = this._data.slice(index + 2); + if (++i % 100 == 0) { + // Prevent blocking main process for too long. + Services.tm.spinEventLoopUntilEmpty(); + } + } + this._running = false; + if (!this.processingMultiLineResponse && !this._data) { + doneCallback(); + } + } +} diff --git a/comm/mailnews/base/src/MailAuthenticator.jsm b/comm/mailnews/base/src/MailAuthenticator.jsm new file mode 100644 index 0000000000..cf52a88f17 --- /dev/null +++ b/comm/mailnews/base/src/MailAuthenticator.jsm @@ -0,0 +1,468 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = [ + "SmtpAuthenticator", + "NntpAuthenticator", + "Pop3Authenticator", + "ImapAuthenticator", +]; + +var { MailCryptoUtils } = ChromeUtils.import( + "resource:///modules/MailCryptoUtils.jsm" +); +var { MailStringUtils } = ChromeUtils.import( + "resource:///modules/MailStringUtils.jsm" +); + +/** + * A base class for interfaces when authenticating a mail connection. + */ +class MailAuthenticator { + /** + * Get the hostname for a connection. + * + * @returns {string} + */ + get hostname() { + throw Components.Exception( + "hostname getter not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Get the username for a connection. + * + * @returns {string} + */ + get username() { + throw Components.Exception( + "username getter not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Forget cached password. + */ + forgetPassword() { + throw Components.Exception( + "forgetPassword not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Get the password for a connection. + * + * @returns {string} + */ + getPassword() { + throw Components.Exception( + "getPassword not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Get the CRAM-MD5 auth token for a connection. + * + * @param {string} password - The password, used as HMAC-MD5 secret. + * @param {string} challenge - The base64 encoded server challenge. + * @returns {string} + */ + getCramMd5Token(password, challenge) { + // Hash the challenge. + let signature = MailCryptoUtils.hmacMd5( + new TextEncoder().encode(password), + new TextEncoder().encode(atob(challenge)) + ); + // Get the hex form of the signature. + let hex = [...signature].map(x => x.toString(16).padStart(2, "0")).join(""); + return btoa(`${this.username} ${hex}`); + } + + /** + * Get the OAuth token for a connection. + * + * @returns {string} + */ + async getOAuthToken() { + throw Components.Exception( + "getOAuthToken not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Init a nsIMailAuthModule instance for GSSAPI auth. + * + * @param {('smtp'|'imap')} protocol - The protocol name. + */ + initGssapiAuth(protocol) { + this._authModule = Cc["@mozilla.org/mail/auth-module;1"].createInstance( + Ci.nsIMailAuthModule + ); + this._authModule.init( + "sasl-gssapi", // Auth module type + `${protocol}@${this.hostname}`, + 0, // nsIAuthModule::REQ_DEFAULT + null, // domain + this.username, + null // password + ); + } + + /** + * Get the next token in a sequence of GSSAPI auth steps. + * + * @param {string} inToken - A base64 encoded string, usually server challenge. + * @returns {string} + */ + getNextGssapiToken(inToken) { + return this._authModule.getNextToken(inToken); + } + + /** + * Init a nsIMailAuthModule instance for NTLM auth. + */ + initNtlmAuth() { + this._authModule = Cc["@mozilla.org/mail/auth-module;1"].createInstance( + Ci.nsIMailAuthModule + ); + this._authModule.init( + "ntlm", // Auth module type + null, // Service name + 0, // nsIAuthModule::REQ_DEFAULT + null, // domain + this.username, + this.getPassword() + ); + } + + /** + * Get the next token in a sequence of NTLM auth steps. + * + * @param {string} inToken - A base64 encoded string, usually server challenge. + * @returns {string} + */ + getNextNtlmToken(inToken) { + return this._authModule.getNextToken(inToken); + } + + /** + * Show a dialog for authentication failure. + * + * @returns {number} - 0: Retry; 1: Cancel; 2: New password. + */ + promptAuthFailed() { + throw Components.Exception( + "promptAuthFailed not implemented", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + /** + * Show a dialog for authentication failure. + * + * @param {nsIMsgWindow} msgWindow - The associated msg window. + * @param {string} accountname - A user defined account name or the server hostname. + * @returns {number} 0: Retry; 1: Cancel; 2: New password. + */ + _promptAuthFailed(msgWindow, accountname) { + let bundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + let message = bundle.formatStringFromName("mailServerLoginFailed2", [ + this.hostname, + this.username, + ]); + + let title = bundle.formatStringFromName( + "mailServerLoginFailedTitleWithAccount", + [accountname] + ); + + let retryButtonLabel = bundle.GetStringFromName( + "mailServerLoginFailedRetryButton" + ); + let newPasswordButtonLabel = bundle.GetStringFromName( + "mailServerLoginFailedEnterNewPasswordButton" + ); + let buttonFlags = + Ci.nsIPrompt.BUTTON_POS_0 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING + + Ci.nsIPrompt.BUTTON_POS_1 * Ci.nsIPrompt.BUTTON_TITLE_CANCEL + + Ci.nsIPrompt.BUTTON_POS_2 * Ci.nsIPrompt.BUTTON_TITLE_IS_STRING; + let dummyValue = { value: false }; + + return Services.prompt.confirmEx( + msgWindow?.domWindow, + title, + message, + buttonFlags, + retryButtonLabel, + null, + newPasswordButtonLabel, + null, + dummyValue + ); + } +} + +/** + * Collection of helper functions for authenticating an SMTP connection. + * + * @augments {MailAuthenticator} + */ +class SmtpAuthenticator extends MailAuthenticator { + /** + * @param {nsISmtpServer} server - The associated server instance. + */ + constructor(server) { + super(); + this._server = server; + } + + get hostname() { + return this._server.hostname; + } + + get username() { + return this._server.username; + } + + forgetPassword() { + this._server.forgetPassword(); + } + + getPassword() { + if (this._server.password) { + return this._server.password; + } + let composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/messengercompose/composeMsgs.properties" + ); + let username = this._server.username; + let promptString; + if (username) { + promptString = composeBundle.formatStringFromName( + "smtpEnterPasswordPromptWithUsername", + [this._server.hostname, username] + ); + } else { + promptString = composeBundle.formatStringFromName( + "smtpEnterPasswordPrompt", + [this._server.hostname] + ); + } + let promptTitle = composeBundle.formatStringFromName( + "smtpEnterPasswordPromptTitleWithHostname", + [this._server.hostname] + ); + return this._server.getPasswordWithUI(promptString, promptTitle); + } + + /** + * Get the ByteString form of the current password. + * + * @returns {string} + */ + getByteStringPassword() { + return MailStringUtils.stringToByteString(this.getPassword()); + } + + /** + * Get the PLAIN auth token for a connection. + * + * @returns {string} + */ + getPlainToken() { + // According to rfc4616#section-2, password should be UTF-8 BinaryString + // before base64 encoded. + return btoa("\0" + this.username + "\0" + this.getByteStringPassword()); + } + + async getOAuthToken() { + let oauth2Module = Cc["@mozilla.org/mail/oauth2-module;1"].createInstance( + Ci.msgIOAuth2Module + ); + if (!oauth2Module.initFromSmtp(this._server)) { + return Promise.reject(`initFromSmtp failed, hostname: ${this.hostname}`); + } + return new Promise((resolve, reject) => { + oauth2Module.connect(true, { + onSuccess: token => { + resolve(token); + }, + onFailure: e => { + reject(e); + }, + }); + }); + } + + promptAuthFailed() { + return this._promptAuthFailed( + null, + this._server.description || this.hostname + ); + } +} + +/** + * Collection of helper functions for authenticating an incoming server. + * + * @augments {MailAuthenticator} + */ +class IncomingServerAuthenticator extends MailAuthenticator { + /** + * @param {nsIMsgIncomingServer} server - The associated server instance. + */ + constructor(server) { + super(); + this._server = server; + } + + get hostname() { + return this._server.hostName; + } + + get username() { + return this._server.username; + } + + forgetPassword() { + this._server.forgetPassword(); + } + + /** + * Get the ByteString form of the current password. + * + * @returns {string} + */ + async getByteStringPassword() { + return MailStringUtils.stringToByteString(await this.getPassword()); + } + + /** + * Get the PLAIN auth token for a connection. + * + * @returns {string} + */ + async getPlainToken() { + // According to rfc4616#section-2, password should be UTF-8 BinaryString + // before base64 encoded. + return btoa( + "\0" + this.username + "\0" + (await this.getByteStringPassword()) + ); + } + + async getOAuthToken() { + let oauth2Module = Cc["@mozilla.org/mail/oauth2-module;1"].createInstance( + Ci.msgIOAuth2Module + ); + if (!oauth2Module.initFromMail(this._server)) { + return Promise.reject(`initFromMail failed, hostname: ${this.hostname}`); + } + return new Promise((resolve, reject) => { + oauth2Module.connect(true, { + onSuccess: token => { + resolve(token); + }, + onFailure: e => { + reject(e); + }, + }); + }); + } +} + +/** + * Collection of helper functions for authenticating a NNTP connection. + * + * @augments {IncomingServerAuthenticator} + */ +class NntpAuthenticator extends IncomingServerAuthenticator { + /** + * @returns {string} - NNTP server has no userName pref, need to pass it in. + */ + get username() { + return this._username; + } + + set username(value) { + this._username = value; + } + + promptAuthFailed() { + return this._promptAuthFailed(null, this._server.prettyName); + } +} + +/** + * Collection of helper functions for authenticating a POP connection. + * + * @augments {IncomingServerAuthenticator} + */ +class Pop3Authenticator extends IncomingServerAuthenticator { + async getPassword() { + if (this._server.password) { + return this._server.password; + } + let composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/localMsgs.properties" + ); + let params = [this._server.username, this._server.hostName]; + let promptString = composeBundle.formatStringFromName( + "pop3EnterPasswordPrompt", + params + ); + let promptTitle = composeBundle.formatStringFromName( + "pop3EnterPasswordPromptTitleWithUsername", + [this._server.hostName] + ); + return this._server.wrappedJSObject.getPasswordWithUIAsync( + promptString, + promptTitle + ); + } + + promptAuthFailed() { + return this._promptAuthFailed(null, this._server.prettyName); + } +} + +/** + * Collection of helper functions for authenticating an IMAP connection. + * + * @augments {IncomingServerAuthenticator} + */ +class ImapAuthenticator extends IncomingServerAuthenticator { + async getPassword() { + if (this._server.password) { + return this._server.password; + } + let composeBundle = Services.strings.createBundle( + "chrome://messenger/locale/imapMsgs.properties" + ); + let params = [this._server.username, this._server.hostName]; + let promptString = composeBundle.formatStringFromName( + "imapEnterServerPasswordPrompt", + params + ); + let promptTitle = composeBundle.formatStringFromName( + "imapEnterPasswordPromptTitleWithUsername", + [this._server.hostName] + ); + return this._server.wrappedJSObject.getPasswordWithUIAsync( + promptString, + promptTitle + ); + } + + promptAuthFailed() { + return this._promptAuthFailed(null, this._server.prettyName); + } +} diff --git a/comm/mailnews/base/src/MailChannel.sys.mjs b/comm/mailnews/base/src/MailChannel.sys.mjs new file mode 100644 index 0000000000..a5fbf9ee75 --- /dev/null +++ b/comm/mailnews/base/src/MailChannel.sys.mjs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * @see {nsIMailChannel} + */ +export class MailChannel { + _headerNames = []; + _headerValues = []; + _attachments = []; + _mailCharacterSet = null; + _progressListener = null; + + addHeaderFromMIME(name, value) { + this._headerNames.push(name); + this._headerValues.push(value); + } + + get headerNames() { + return this._headerNames; + } + + get headerValues() { + return this._headerValues; + } + + handleAttachmentFromMIME(contentType, url, displayName, uri, notDownloaded) { + let attachment = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag2 + ); + attachment.setPropertyAsAUTF8String("contentType", contentType); + attachment.setPropertyAsAUTF8String("url", url); + attachment.setPropertyAsAUTF8String("displayName", displayName); + attachment.setPropertyAsAUTF8String("uri", uri); + attachment.setPropertyAsBool("notDownloaded", notDownloaded); + this._attachments.push(attachment); + } + + addAttachmentFieldFromMIME(field, value) { + let attachment = this._attachments[this._attachments.length - 1]; + attachment.setPropertyAsAUTF8String(field, value); + } + + get attachments() { + return this._attachments.slice(); + } + + get mailCharacterSet() { + return this._mailCharacterSet; + } + + set mailCharacterSet(value) { + let ccm = Cc["@mozilla.org/charset-converter-manager;1"].getService( + Ci.nsICharsetConverterManager + ); + this._mailCharacterSet = ccm.getCharsetAlias(value); + } + + imipMethod = null; + imipItem = null; + smimeHeaderSink = null; + + get listener() { + return this._progressListener?.get(); + } + + set listener(listener) { + this._progressListener = Cu.getWeakReference(listener); + } +} diff --git a/comm/mailnews/base/src/MailCryptoUtils.jsm b/comm/mailnews/base/src/MailCryptoUtils.jsm new file mode 100644 index 0000000000..6c378e6703 --- /dev/null +++ b/comm/mailnews/base/src/MailCryptoUtils.jsm @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MailCryptoUtils"]; + +var MailCryptoUtils = { + /** + * Converts a binary string into a Uint8Array. + * + * @param {BinaryString} str - The string to convert. + * @returns {Uint8Array}. + */ + binaryStringToTypedArray(str) { + let arr = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + arr[i] = str.charCodeAt(i); + } + return arr; + }, + + /** + * The HMAC-MD5 transform works like: + * + * MD5(K XOR opad, MD5(K XOR ipad, m)) + * + * where + * K is an n byte key + * ipad is the byte 0x36 repeated 64 times + * opad is the byte 0x5c repeated 64 times + * m is the message being processed + + * @param {Uint8Array} key + * @param {Uint8Array} data + * @returns {Uint8Array} + */ + hmacMd5(key, data) { + let hasher = Cc["@mozilla.org/security/hash;1"].createInstance( + Ci.nsICryptoHash + ); + let digest; + + // If key is longer than 64 bytes, reset it to MD5(key). + if (key.length > 64) { + hasher.init(Ci.nsICryptoHash.MD5); + hasher.update(key, key.length); + digest = hasher.finish(false); + key = this.binaryStringToTypedArray(digest); + } + + // Generate innerPad and outerPad. + let innerPad = new Uint8Array(64); + let outerPad = new Uint8Array(64); + for (let i = 0; i < 64; i++) { + let base = key[i] || 0; + innerPad[i] = base ^ 0x36; + outerPad[i] = base ^ 0x5c; + } + + // Perform inner MD5. + hasher.init(Ci.nsICryptoHash.MD5); + hasher.update(innerPad, 64); + hasher.update(data, data.length); + digest = hasher.finish(false); + + let result = this.binaryStringToTypedArray(digest); + + // Perform outer MD5. + hasher.init(Ci.nsICryptoHash.MD5); + hasher.update(outerPad, 64); + hasher.update(result, result.length); + digest = hasher.finish(false); + + return this.binaryStringToTypedArray(digest); + }, +}; diff --git a/comm/mailnews/base/src/MailNewsDLF.cpp b/comm/mailnews/base/src/MailNewsDLF.cpp new file mode 100644 index 0000000000..8969992414 --- /dev/null +++ b/comm/mailnews/base/src/MailNewsDLF.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCOMPtr.h" +#include "MailNewsDLF.h" +#include "nsIChannel.h" +#include "plstr.h" +#include "nsString.h" +#include "nsICategoryManager.h" +#include "nsIServiceManager.h" +#include "nsIStreamConverterService.h" +#include "nsIStreamListener.h" +#include "nsNetCID.h" +#include "nsMsgUtils.h" + +namespace mozilla { +namespace mailnews { +NS_IMPL_ISUPPORTS(MailNewsDLF, nsIDocumentLoaderFactory) + +MailNewsDLF::MailNewsDLF() {} + +MailNewsDLF::~MailNewsDLF() {} + +NS_IMETHODIMP +MailNewsDLF::CreateInstance(const char* aCommand, nsIChannel* aChannel, + nsILoadGroup* aLoadGroup, + const nsACString& aContentType, + nsIDocShell* aContainer, nsISupports* aExtraInfo, + nsIStreamListener** aDocListener, + nsIContentViewer** aDocViewer) { + nsresult rv; + + bool viewSource = + (PL_strstr(PromiseFlatCString(aContentType).get(), "view-source") != 0); + + aChannel->SetContentType(nsLiteralCString(TEXT_HTML)); + + // Get the HTML category + nsCOMPtr<nsICategoryManager> catMan( + do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString contractID; + rv = catMan->GetCategoryEntry("Gecko-Content-Viewers", TEXT_HTML, contractID); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDocumentLoaderFactory> factory( + do_GetService(contractID.get(), &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStreamListener> listener; + + if (viewSource) { + rv = factory->CreateInstance( + "view-source", aChannel, aLoadGroup, + nsLiteralCString(TEXT_HTML "; x-view-type=view-source"), aContainer, + aExtraInfo, getter_AddRefs(listener), aDocViewer); + } else { + rv = factory->CreateInstance( + "view", aChannel, aLoadGroup, nsLiteralCString(TEXT_HTML), aContainer, + aExtraInfo, getter_AddRefs(listener), aDocViewer); + } + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStreamConverterService> scs( + do_GetService(NS_STREAMCONVERTERSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + return scs->AsyncConvertData(MESSAGE_RFC822, TEXT_HTML, listener, aChannel, + aDocListener); +} + +NS_IMETHODIMP +MailNewsDLF::CreateInstanceForDocument(nsISupports* aContainer, + mozilla::dom::Document* aDocument, + const char* aCommand, + nsIContentViewer** aDocViewer) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace mailnews +} // namespace mozilla diff --git a/comm/mailnews/base/src/MailNewsDLF.h b/comm/mailnews/base/src/MailNewsDLF.h new file mode 100644 index 0000000000..95455f4b56 --- /dev/null +++ b/comm/mailnews/base/src/MailNewsDLF.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MailNewsDLF_h__ +#define MailNewsDLF_h__ + +#include "nsIDocumentLoaderFactory.h" +#include "nsMimeTypes.h" + +namespace mozilla { +namespace mailnews { + +/* + * This factory is a thin wrapper around the text/html loader factory. All it + * does is convert message/rfc822 to text/html and delegate the rest of the + * work to the text/html factory. + */ +class MailNewsDLF : public nsIDocumentLoaderFactory { + public: + MailNewsDLF(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOCUMENTLOADERFACTORY + + private: + virtual ~MailNewsDLF(); +}; +} // namespace mailnews +} // namespace mozilla + +#define MAILNEWSDLF_CATEGORIES \ + {"Gecko-Content-Viewers", MESSAGE_RFC822, \ + "@mozilla.org/mailnews/document-loader-factory;1"}, + +#endif diff --git a/comm/mailnews/base/src/MailNotificationManager.jsm b/comm/mailnews/base/src/MailNotificationManager.jsm new file mode 100644 index 0000000000..c16e37eb2f --- /dev/null +++ b/comm/mailnews/base/src/MailNotificationManager.jsm @@ -0,0 +1,478 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MailNotificationManager"]; + +const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + MailUtils: "resource:///modules/MailUtils.jsm", + WinUnreadBadge: "resource:///modules/WinUnreadBadge.jsm", +}); + +XPCOMUtils.defineLazyGetter( + lazy, + "l10n", + () => new Localization(["messenger/messenger.ftl"]) +); + +/** + * A module that listens to folder change events, and show notifications for new + * mails if necessary. + */ +class MailNotificationManager { + QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsIFolderListener", + "mozINewMailListener", + ]); + + constructor() { + this._systemAlertAvailable = true; + this._unreadChatCount = 0; + this._unreadMailCount = 0; + // @type {Map<nsIMsgFolder, number>} - A map of folder and its last biff time. + this._folderBiffTime = new Map(); + // @type {Set<nsIMsgFolder>} - A set of folders to show alert for. + this._pendingFolders = new Set(); + + this._logger = console.createInstance({ + prefix: "mail.notification", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.notification.loglevel", + }); + this._bundle = Services.strings.createBundle( + "chrome://messenger/locale/messenger.properties" + ); + MailServices.mailSession.AddFolderListener( + this, + Ci.nsIFolderListener.intPropertyChanged + ); + + // Ensure that OS integration is defined before we attempt to initialize the + // system tray icon. + XPCOMUtils.defineLazyGetter(this, "_osIntegration", () => { + try { + return Cc["@mozilla.org/messenger/osintegration;1"].getService( + Ci.nsIMessengerOSIntegration + ); + } catch (e) { + // We don't have OS integration on all platforms. + return null; + } + }); + + if (["macosx", "win"].includes(AppConstants.platform)) { + // We don't have indicator for unread count on Linux yet. + Cc["@mozilla.org/newMailNotificationService;1"] + .getService(Ci.mozINewMailNotificationService) + .addListener(this, Ci.mozINewMailNotificationService.count); + + Services.obs.addObserver(this, "unread-im-count-changed"); + Services.obs.addObserver(this, "profile-before-change"); + } + + if (AppConstants.platform == "macosx") { + Services.obs.addObserver(this, "new-directed-incoming-message"); + } + + if (AppConstants.platform == "win") { + Services.obs.addObserver(this, "windows-refresh-badge-tray"); + Services.prefs.addObserver("mail.biff.show_badge", this); + Services.prefs.addObserver("mail.biff.show_tray_icon_always", this); + } + } + + observe(subject, topic, data) { + switch (topic) { + case "alertclickcallback": + // Display the associated message when an alert is clicked. + let msgHdr = Cc["@mozilla.org/messenger;1"] + .getService(Ci.nsIMessenger) + .msgHdrFromURI(data); + lazy.MailUtils.displayMessageInFolderTab(msgHdr, true); + return; + case "unread-im-count-changed": + this._logger.log( + `Unread chat count changed to ${this._unreadChatCount}` + ); + this._unreadChatCount = parseInt(data, 10) || 0; + this._updateUnreadCount(); + return; + case "new-directed-incoming-messenger": + this._animateDockIcon(); + return; + case "windows-refresh-badge-tray": + this._updateUnreadCount(); + return; + case "profile-before-change": + this._osIntegration?.onExit(); + return; + case "newmailalert-closed": + // newmailalert.xhtml is closed, try to show the next queued folder. + this._customizedAlertShown = false; + this._showCustomizedAlert(); + return; + case "nsPref:changed": + if ( + data == "mail.biff.show_badge" || + data == "mail.biff.show_tray_icon_always" + ) { + this._updateUnreadCount(); + } + } + } + + /** + * Following are nsIFolderListener interfaces. Do nothing about them. + */ + onFolderAdded() {} + onMessageAdded() {} + onFolderRemoved() {} + onMessageRemoved() {} + onFolderPropertyChanged() {} + /** + * The only nsIFolderListener interface we care about. + * + * @see nsIFolderListener + */ + onFolderIntPropertyChanged(folder, property, oldValue, newValue) { + if (!Services.prefs.getBoolPref("mail.biff.show_alert")) { + return; + } + + this._logger.debug( + `onFolderIntPropertyChanged; property=${property}: ${oldValue} => ${newValue}, folder.URI=${folder.URI}` + ); + + switch (property) { + case "BiffState": + if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) { + // The folder argument is a root folder. + this._fillAlertInfo(folder); + } + break; + case "NewMailReceived": + // The folder argument is a real folder. + this._fillAlertInfo(folder); + break; + } + } + onFolderBoolPropertyChanged() {} + onFolderUnicharPropertyChanged() {} + onFolderPropertyFlagChanged() {} + onFolderEvent() {} + + /** + * @see mozINewMailNotificationService + */ + onCountChanged(count) { + this._logger.log(`Unread mail count changed to ${count}`); + this._unreadMailCount = count; + this._updateUnreadCount(); + } + + /** + * Show an alert according to the changed folder. + * + * @param {nsIMsgFolder} changedFolder - The folder that emitted the change + * event, can be a root folder or a real folder. + */ + async _fillAlertInfo(changedFolder) { + let folder = this._getFirstRealFolderWithNewMail(changedFolder); + if (!folder) { + return; + } + + let newMsgKeys = this._getNewMsgKeysNotNotified(folder); + let numNewMessages = newMsgKeys.length; + if (!numNewMessages) { + return; + } + + this._logger.debug( + `Filling alert info; folder.URI=${folder.URI}, numNewMessages=${numNewMessages}` + ); + let firstNewMsgHdr = folder.msgDatabase.getMsgHdrForKey(newMsgKeys[0]); + + let title = this._getAlertTitle(folder, numNewMessages); + let body; + try { + body = await this._getAlertBody(folder, firstNewMsgHdr); + } catch (e) { + this._logger.error(e); + } + if (!title || !body) { + return; + } + this._showAlert(firstNewMsgHdr, title, body); + this._animateDockIcon(); + } + + /** + * Iterate the subfolders of changedFolder, return the first real folder with + * new mail. + * + * @param {nsIMsgFolder} changedFolder - The folder that emitted the change event. + * @returns {nsIMsgFolder} The first real folder. + */ + _getFirstRealFolderWithNewMail(changedFolder) { + let folders = changedFolder.descendants; + folders.unshift(changedFolder); + + for (let folder of folders) { + let flags = folder.flags; + if ( + !(flags & Ci.nsMsgFolderFlags.Inbox) && + flags & (Ci.nsMsgFolderFlags.SpecialUse | Ci.nsMsgFolderFlags.Virtual) + ) { + // Do not notify if the folder is not Inbox but one of + // Drafts|Trash|SentMail|Templates|Junk|Archive|Queue or Virtual. + continue; + } + + if (folder.getNumNewMessages(false) > 0) { + return folder; + } + } + return null; + } + + /** + * Get the title for the alert. + * + * @param {nsIMsgFolder} folder - The changed folder. + * @param {number} numNewMessages - The count of new messages. + * @returns {string} The alert title. + */ + _getAlertTitle(folder, numNewMessages) { + return this._bundle.formatStringFromName( + numNewMessages == 1 + ? "newMailNotification_message" + : "newMailNotification_messages", + [folder.server.prettyName, numNewMessages.toString()] + ); + } + + /** + * Get the body for the alert. + * + * @param {nsIMsgFolder} folder - The changed folder. + * @param {nsIMsgHdr} msgHdr - The nsIMsgHdr of the first new messages. + * @returns {string} The alert body. + */ + async _getAlertBody(folder, msgHdr) { + await new Promise((resolve, reject) => { + let isAsync = folder.fetchMsgPreviewText([msgHdr.messageKey], { + OnStartRunningUrl() {}, + // @see nsIUrlListener + OnStopRunningUrl(url, exitCode) { + Components.isSuccessCode(exitCode) ? resolve() : reject(); + }, + }); + if (!isAsync) { + resolve(); + } + }); + + let alertBody = ""; + + let subject = Services.prefs.getBoolPref("mail.biff.alert.show_subject") + ? msgHdr.mime2DecodedSubject + : ""; + let author = ""; + if (Services.prefs.getBoolPref("mail.biff.alert.show_sender")) { + let addressObjects = MailServices.headerParser.makeFromDisplayAddress( + msgHdr.mime2DecodedAuthor + ); + let { name, email } = addressObjects[0] || {}; + author = name || email; + } + if (subject && author) { + alertBody += this._bundle.formatStringFromName( + "newMailNotification_messagetitle", + [subject, author] + ); + } else if (subject) { + alertBody += subject; + } else if (author) { + alertBody += author; + } + let showPreview = Services.prefs.getBoolPref( + "mail.biff.alert.show_preview" + ); + if (showPreview) { + let previewLength = Services.prefs.getIntPref( + "mail.biff.alert.preview_length", + 40 + ); + let preview = msgHdr.getStringProperty("preview").slice(0, previewLength); + if (preview) { + alertBody += (alertBody ? "\n" : "") + preview; + } + } + return alertBody; + } + + /** + * Show the alert. + * + * @param {nsIMsgHdr} msgHdr - The nsIMsgHdr of the first new messages. + * @param {string} title - The alert title. + * @param {string} body - The alert body. + */ + _showAlert(msgHdr, title, body) { + let folder = msgHdr.folder; + + // Try to use system alert first. + if ( + Services.prefs.getBoolPref("mail.biff.use_system_alert", true) && + this._systemAlertAvailable + ) { + let alertsService = Cc["@mozilla.org/system-alerts-service;1"].getService( + Ci.nsIAlertsService + ); + let cookie = folder.generateMessageURI(msgHdr.messageKey); + try { + let alert = Cc["@mozilla.org/alert-notification;1"].createInstance( + Ci.nsIAlertNotification + ); + alert.init( + cookie, // name + "chrome://messenger/skin/icons/new-mail-alert.png", + title, + body, + true, // clickable + cookie + ); + alertsService.showAlert(alert, this); + return; + } catch (e) { + this._logger.error(e); + this._systemAlertAvailable = false; + } + } + + // The use_system_alert pref is false or showAlert somehow failed, use the + // customized alert window. + this._showCustomizedAlert(folder); + } + + /** + * Show a customized alert window (newmailalert.xhtml), if there is already + * one showing, do not show another one, because the newer one will block the + * older one. Instead, save the folder and newMsgKeys to this._pendingFolders. + * + * @param {nsIMsgFolder} [folder] - The folder containing new messages. + */ + _showCustomizedAlert(folder) { + if (this._customizedAlertShown) { + // Queue the folder. + this._pendingFolders.add(folder); + return; + } + if (!folder) { + // Get the next folder from the queue. + folder = this._pendingFolders.keys().next().value; + if (!folder) { + return; + } + this._pendingFolders.delete(folder); + } + + let newMsgKeys = this._getNewMsgKeysNotNotified(folder); + if (!newMsgKeys.length) { + // No NEW message in the current folder, try the next queued folder. + this._showCustomizedAlert(); + return; + } + + let args = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray); + args.appendElement(folder); + args.appendElement({ + wrappedJSObject: newMsgKeys, + }); + args.appendElement(this); + Services.ww.openWindow( + null, + "chrome://messenger/content/newmailalert.xhtml", + "_blank", + "chrome,dialog=yes,titlebar=no,popup=yes", + args + ); + this._customizedAlertShown = true; + this._folderBiffTime.set(folder, Date.now()); + } + + /** + * Get all NEW messages from a folder that we received after last biff time. + * + * @param {nsIMsgFolder} folder - The message folder to check. + * @returns {number[]} An array of message keys. + */ + _getNewMsgKeysNotNotified(folder) { + let msgDb = folder.msgDatabase; + let lastBiffTime = this._folderBiffTime.get(folder) || 0; + return msgDb + .getNewList() + .slice(-folder.getNumNewMessages(false)) + .filter(key => { + let msgHdr = msgDb.getMsgHdrForKey(key); + return msgHdr.dateInSeconds * 1000 > lastBiffTime; + }); + } + + async _updateUnreadCount() { + if (this._updatingUnreadCount) { + // _updateUnreadCount can be triggered faster than we finish rendering the + // badge. When that happens, set a flag and return. + this._pendingUpdate = true; + return; + } + this._updatingUnreadCount = true; + + this._logger.debug( + `Update unreadMailCount=${this._unreadMailCount}, unreadChatCount=${this._unreadChatCount}` + ); + let count = this._unreadMailCount + this._unreadChatCount; + let tooltip = ""; + if (AppConstants.platform == "win") { + if (!Services.prefs.getBoolPref("mail.biff.show_badge", true)) { + count = 0; + } + if (count > 0) { + tooltip = await lazy.l10n.formatValue("unread-messages-os-tooltip", { + count, + }); + } + await lazy.WinUnreadBadge.updateUnreadCount(count, tooltip); + } + this._osIntegration?.updateUnreadCount(count, tooltip); + + this._updatingUnreadCount = false; + if (this._pendingUpdate) { + // There was at least one _updateUnreadCount call while we were rendering + // the badge. Render one more time will ensure the badge reflects the + // current state. + this._pendingUpdate = false; + this._updateUnreadCount(); + } + } + + _animateDockIcon() { + if (Services.prefs.getBoolPref("mail.biff.animate_dock_icon", false)) { + Services.wm.getMostRecentWindow("mail:3pane")?.getAttention(); + } + } +} diff --git a/comm/mailnews/base/src/MailNotificationService.jsm b/comm/mailnews/base/src/MailNotificationService.jsm new file mode 100644 index 0000000000..8f2c57aad6 --- /dev/null +++ b/comm/mailnews/base/src/MailNotificationService.jsm @@ -0,0 +1,375 @@ +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Platform-independent code to count new and unread messages and pass the + * information to platform-specific notification modules. + */ + +var EXPORTED_SYMBOLS = ["NewMailNotificationService"]; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/** + * NewMailNotificationService. + * + * @implements {mozINewMailNotificationService} + * @implements {nsIFolderListener} + * @implements {nsIObserver} + */ +class NewMailNotificationService { + QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsIFolderListener", + "mozINewMailNotificationService", + ]); + + #unreadCount = 0; + #newCount = 0; + #listeners = []; + #log = null; + + constructor() { + this.#log = console.createInstance({ + prefix: "mail.notification", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.notification.loglevel", + }); + + Services.obs.addObserver(this, "profile-before-change"); + MailServices.mailSession.AddFolderListener( + this, + Ci.nsIFolderListener.intPropertyChanged | + Ci.nsIFolderListener.added | + Ci.nsIFolderListener.removed | + Ci.nsIFolderListener.propertyFlagChanged + ); + if (!this.useNewCountInBadge) { + let total = 0; + for (let server of MailServices.accounts.allServers) { + // Don't bother counting RSS or NNTP servers + let type = server.type; + if (type == "rss" || type == "nntp") { + continue; + } + + let rootFolder = server.rootFolder; + if (rootFolder) { + total += this.countUnread(rootFolder); + } + } + this.#unreadCount = total; + } + } + + get useNewCountInBadge() { + return Services.prefs.getBoolPref( + "mail.biff.use_new_count_in_badge", + false + ); + } + + /** Setter. Used for unit tests. */ + set unreadCount(count) { + this.#unreadCount = count; + } + + observe(subject, topic, data) { + if (topic == "profile-before-change") { + try { + MailServices.mailSession.RemoveFolderListener(this); + Services.obs.removeObserver(this, "profile-before-change"); + } catch (e) { + this.#log.error("Unable to deregister listeners at shutdown: " + e); + } + } + } + + // Count all the unread messages below the given folder + countUnread(folder) { + this.#log.debug(`countUnread for ${folder.URI}`); + let unreadCount = 0; + + let allFolders = [folder, ...folder.descendants]; + for (let folder of allFolders) { + if (this.confirmShouldCount(folder)) { + let count = folder.getNumUnread(false); + this.#log.debug(`${folder.URI} has ${count} unread`); + if (count > 0) { + unreadCount += count; + } + } + } + return unreadCount; + } + + /** + * Filter out special folders and then ask for observers to see if + * we should monitor unread messages in this folder. + * + * @param {nsIMsgFolder} aFolder - The folder we're asking about. + */ + confirmShouldCount(aFolder) { + let shouldCount = Cc["@mozilla.org/supports-PRBool;1"].createInstance( + Ci.nsISupportsPRBool + ); + shouldCount.data = true; + + // If it's not a mail folder we don't count it by default + if (!(aFolder.flags & Ci.nsMsgFolderFlags.Mail)) { + shouldCount.data = false; + } else if (aFolder.server?.type == "rss") { + // For whatever reason, RSS folders have the 'Mail' flag. + shouldCount.data = false; + } else if ( + aFolder.flags & Ci.nsMsgFolderFlags.SpecialUse && + !(aFolder.flags & Ci.nsMsgFolderFlags.Inbox) + ) { + // It's a special folder *other than the inbox*, don't count it by default. + shouldCount.data = false; + } else if (aFolder.flags & Ci.nsMsgFolderFlags.Virtual) { + shouldCount.data = false; + } else { + // If we're only counting inboxes and it's not an inbox... + let onlyCountInboxes = Services.prefs.getBoolPref( + "mail.notification.count.inbox_only", + true + ); + if (onlyCountInboxes && !(aFolder.flags & Ci.nsMsgFolderFlags.Inbox)) { + shouldCount.data = false; + } + } + + this.#log.debug(`${aFolder.URI}: shouldCount=${shouldCount.data}`); + Services.obs.notifyObservers( + shouldCount, + "before-count-unread-for-folder", + aFolder.URI + ); + return shouldCount.data; + } + + onFolderIntPropertyChanged(folder, property, oldValue, newValue) { + try { + if (property == "FolderSize") { + return; + } + this.#log.trace( + `Changed int ${property} of ${folder.folderURL}: ${oldValue} -> ${newValue}` + ); + if (property == "BiffState") { + this.#biffStateChanged(folder, oldValue, newValue); + } else if (property == "TotalUnreadMessages") { + this.#totalUnreadMessagesChanged(folder, oldValue, newValue); + } else if (property == "NewMailReceived") { + this.#newMailReceived(folder, oldValue, newValue); + } + } catch (error) { + this.#log.error("onFolderIntPropertyChanged: " + error); + } + } + + #biffStateChanged(folder, oldValue, newValue) { + if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NewMail) { + if (folder.server && !folder.server.performingBiff) { + this.#log.debug( + `${folder.URI} notified, but server not performing biff` + ); + return; + } + + // Biff notifications come in for the top level of the server, we need to + // look for the folder that actually contains the new mail. + + let allFolders = [folder, ...folder.descendants]; + + this.#log.debug(`${folder.URI} notified; will check subfolders`); + let newCount = 0; + + for (let folder of allFolders) { + if (this.confirmShouldCount(folder)) { + let folderNew = folder.getNumNewMessages(false); + this.#log.debug(`${folder.URI}: ${folderNew} new`); + if (folderNew > 0) { + newCount += folderNew; + } + } + } + if (newCount > 0) { + this.#newCount += newCount; + this.#log.debug(`${folder.URI}: new mail count ${this.#newCount}`); + if (this.useNewCountInBadge) { + this._notifyListeners( + Ci.mozINewMailNotificationService.count, + "onCountChanged", + this.#newCount + ); + } + } + } else if (newValue == Ci.nsIMsgFolder.nsMsgBiffState_NoMail) { + // Dodgy - when any folder tells us it has no mail, clear all unread mail + this.#newCount = 0; + this.#log.debug(`${folder.URI}: no new mail`); + if (this.useNewCountInBadge) { + this._notifyListeners( + Ci.mozINewMailNotificationService.count, + "onCountChanged", + this.#newCount + ); + } + } + } + + #newMailReceived(folder, oldValue, newValue) { + if (!this.confirmShouldCount(folder)) { + return; + } + + if (!oldValue || oldValue < 0) { + oldValue = 0; + } + this.#newCount += newValue - oldValue; + this.#log.debug(`#newMailReceived ${folder.URI} - ${this.#newCount} new`); + if (this.useNewCountInBadge) { + this._notifyListeners( + Ci.mozINewMailNotificationService.count, + "onCountChanged", + this.#newCount + ); + } + } + + #totalUnreadMessagesChanged(folder, oldValue, newValue) { + if (!this.confirmShouldCount(folder)) { + return; + } + + // treat "count unknown" as zero + if (oldValue < 0) { + oldValue = 0; + } + if (newValue < 0) { + newValue = 0; + } + + this.#unreadCount += newValue - oldValue; + if (!this.useNewCountInBadge) { + this._notifyListeners( + Ci.mozINewMailNotificationService.count, + "onCountChanged", + this.#unreadCount + ); + } + } + + onFolderAdded(parentFolder, child) { + if (child.rootFolder == child) { + this.#log.trace(`Added root folder ${child.folderURL}`); + } else { + this.#log.trace( + `Added child folder ${child.folderURL} to ${parentFolder.folderURL}` + ); + } + } + + onMessageAdded(parentFolder, msg) { + if (this.confirmShouldCount(msg.folder)) { + this.#log.trace(`Added <${msg.messageId}> to ${msg.folder.folderURL}`); + } + } + + onFolderPropertyFlagChanged(msg, property, oldFlag, newFlag) { + if ( + oldFlag & Ci.nsMsgMessageFlags.New && + !(newFlag & Ci.nsMsgMessageFlags.New) + ) { + this.#log.trace( + `<${msg.messageId}> marked read in ${msg.folder.folderURL}` + ); + } else if (newFlag & Ci.nsMsgMessageFlags.New) { + this.#log.trace( + `<${msg.messageId}> marked unread in ${msg.folder.folderURL}` + ); + } + } + + onFolderRemoved(parentFolder, child) { + if (child.rootFolder == child) { + this.#log.trace(`Removed root folder ${child.folderURL}`); + } else { + this.#log.trace( + `Removed child folder ${child.folderURL} from ${parentFolder?.folderURL}` + ); + } + } + + onMessageRemoved(parentFolder, msg) { + if (!msg.isRead) { + this.#log.trace( + `Removed unread <${msg.messageId}> from ${msg.folder.folderURL}` + ); + } + } + + // Implement mozINewMailNotificationService + + get messageCount() { + if (this.useNewCountInBadge) { + return this.#newCount; + } + return this.#unreadCount; + } + + addListener(aListener, flags) { + for (let i = 0; i < this.#listeners.length; i++) { + let l = this.#listeners[i]; + if (l.obj === aListener) { + l.flags = flags; + return; + } + } + + // Ensure that first-time listeners get an accurate mail count. + if (flags & Ci.mozINewMailNotificationService.count) { + const count = this.useNewCountInBadge + ? this.#newCount + : this.#unreadCount; + aListener.onCountChanged(count); + } + + // If we get here, the listener wasn't already in the list + this.#listeners.push({ obj: aListener, flags }); + } + + removeListener(aListener) { + for (let i = 0; i < this.#listeners.length; i++) { + let l = this.#listeners[i]; + if (l.obj === aListener) { + this.#listeners.splice(i, 1); + return; + } + } + } + + listenersForFlag(flag) { + let list = []; + for (let i = 0; i < this.#listeners.length; i++) { + let l = this.#listeners[i]; + if (l.flags & flag) { + list.push(l.obj); + } + } + return list; + } + + _notifyListeners(flag, func, value) { + let list = this.listenersForFlag(flag); + for (let i = 0; i < list.length; i++) { + list[i][func](value); + } + } +} diff --git a/comm/mailnews/base/src/MailServices.jsm b/comm/mailnews/base/src/MailServices.jsm new file mode 100644 index 0000000000..f316b16bde --- /dev/null +++ b/comm/mailnews/base/src/MailServices.jsm @@ -0,0 +1,169 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["MailServices"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +var MailServices = { + /** + * Gets the `nsIMsgMessageService` for a given message URI. This should have + * the same behaviour as `GetMessageServiceFromURI` (nsMsgUtils.cpp). + * + * @param {string} uri - The URI of a folder or message. + * @returns {nsIMsgMessageService} + */ + messageServiceFromURI(uri) { + let index = uri.indexOf(":"); + if (index == -1) { + throw new Components.Exception( + `Bad message URI: ${uri}`, + Cr.NS_ERROR_FAILURE + ); + } + + let protocol = uri.substring(0, index); + if (protocol == "file") { + protocol = "mailbox"; + } + return Cc[ + `@mozilla.org/messenger/messageservice;1?type=${protocol}` + ].getService(Ci.nsIMsgMessageService); + }, +}; + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "mailSession", + "@mozilla.org/messenger/services/session;1", + "nsIMsgMailSession" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "accounts", + "@mozilla.org/messenger/account-manager;1", + "nsIMsgAccountManager" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "pop3", + "@mozilla.org/messenger/popservice;1", + "nsIPop3Service" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "imap", + "@mozilla.org/messenger/imapservice;1", + "nsIImapService" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "nntp", + "@mozilla.org/messenger/nntpservice;1", + "nsINntpService" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "smtp", + "@mozilla.org/messengercompose/smtp;1", + "nsISmtpService" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "compose", + "@mozilla.org/messengercompose;1", + "nsIMsgComposeService" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "ab", + "@mozilla.org/abmanager;1", + "nsIAbManager" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "copy", + "@mozilla.org/messenger/messagecopyservice;1", + "nsIMsgCopyService" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "mfn", + "@mozilla.org/messenger/msgnotificationservice;1", + "nsIMsgFolderNotificationService" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "headerParser", + "@mozilla.org/messenger/headerparser;1", + "nsIMsgHeaderParser" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "mimeConverter", + "@mozilla.org/messenger/mimeconverter;1", + "nsIMimeConverter" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "tags", + "@mozilla.org/messenger/tagservice;1", + "nsIMsgTagService" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "filters", + "@mozilla.org/messenger/services/filters;1", + "nsIMsgFilterService" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "junk", + "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter", + "nsIJunkMailPlugin" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "newMailNotification", + "@mozilla.org/newMailNotificationService;1", + "mozINewMailNotificationService" +); + +XPCOMUtils.defineLazyServiceGetter( + MailServices, + "folderLookup", + "@mozilla.org/mail/folder-lookup;1", + "nsIFolderLookupService" +); + +// Clean up all of these references at shutdown, so that they don't appear as +// a memory leak in test logs. +Services.obs.addObserver( + { + observe() { + for (let key of Object.keys(MailServices)) { + delete MailServices[key]; + } + Services.obs.removeObserver(this, "xpcom-shutdown"); + }, + }, + "xpcom-shutdown" +); diff --git a/comm/mailnews/base/src/MailStringUtils.jsm b/comm/mailnews/base/src/MailStringUtils.jsm new file mode 100644 index 0000000000..c892c328e4 --- /dev/null +++ b/comm/mailnews/base/src/MailStringUtils.jsm @@ -0,0 +1,102 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MailStringUtils"]; + +var MailStringUtils = { + /** + * Convert a ByteString to a Uint8Array. + * + * @param {ByteString} str - The input string. + * @returns {Uint8Array} The output Uint8Array. + */ + byteStringToUint8Array(str) { + let arr = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + arr[i] = str.charCodeAt(i); + } + return arr; + }, + + /** + * Convert a Uint8Array to a ByteString. + * + * @param {Uint8Array} arr - The input Uint8Array. + * @returns {ByteString} The output string. + */ + uint8ArrayToByteString(arr) { + let str = ""; + for (let i = 0; i < arr.length; i += 65536) { + str += String.fromCharCode.apply(null, arr.subarray(i, i + 65536)); + } + return str; + }, + + /** + * Convert a ByteString to a string. + * + * @param {ByteString} str - The ByteString to convert. + * @returns {string} The converted string. + */ + byteStringToString(str) { + return new TextDecoder().decode(this.byteStringToUint8Array(str)); + }, + + /** + * Convert a string to a ByteString. + * + * @param {string} str - The string to convert. + * @returns {ByteString} The converted ByteString. + */ + stringToByteString(str) { + return this.uint8ArrayToByteString(new TextEncoder().encode(str)); + }, + + /** + * Detect the text encoding of a ByteString. + * + * @param {ByteString} str - The input string. + * @returns {string} The output charset name. + */ + detectCharset(str) { + // Check the BOM. + let charset = ""; + if (str.length >= 2) { + let byte0 = str.charCodeAt(0); + let byte1 = str.charCodeAt(1); + let byte2 = str.charCodeAt(2); + if (byte0 == 0xfe && byte1 == 0xff) { + charset = "UTF-16BE"; + } else if (byte0 == 0xff && byte1 == 0xfe) { + charset = "UTF-16LE"; + } else if (byte0 == 0xef && byte1 == 0xbb && byte2 == 0xbf) { + charset = "UTF-8"; + } + } + if (charset) { + return charset; + } + + // Use mozilla::EncodingDetector. + let compUtils = Cc[ + "@mozilla.org/messengercompose/computils;1" + ].createInstance(Ci.nsIMsgCompUtils); + return compUtils.detectCharset(str); + }, + + /** + * Read and detect the charset of a file, then convert the file content to + * DOMString. If you're absolutely sure it's a UTF-8 encoded file, use + * IOUtils.readUTF8 instead. + * + * @param {string} path - An absolute file path. + * @returns {DOMString} The file content. + */ + async readEncoded(path) { + let arr = await IOUtils.read(path); + let str = this.uint8ArrayToByteString(arr); + let charset = this.detectCharset(str); + return new TextDecoder(charset).decode(arr); + }, +}; diff --git a/comm/mailnews/base/src/MailnewsLoadContextInfo.cpp b/comm/mailnews/base/src/MailnewsLoadContextInfo.cpp new file mode 100644 index 0000000000..2e4efc72f5 --- /dev/null +++ b/comm/mailnews/base/src/MailnewsLoadContextInfo.cpp @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This was copied from netwerk/base/LoadContextInfo.cpp + +#include "MailnewsLoadContextInfo.h" + +#include "mozilla/dom/ToJSValue.h" +#include "nsIChannel.h" +#include "nsILoadContext.h" +#include "nsIWebNavigation.h" +#include "nsNetUtil.h" + +// MailnewsLoadContextInfo + +NS_IMPL_ISUPPORTS(MailnewsLoadContextInfo, nsILoadContextInfo) + +MailnewsLoadContextInfo::MailnewsLoadContextInfo( + bool aIsPrivate, bool aIsAnonymous, + mozilla::OriginAttributes aOriginAttributes) + : mIsPrivate(aIsPrivate), + mIsAnonymous(aIsAnonymous), + mOriginAttributes(aOriginAttributes) { + mOriginAttributes.SyncAttributesWithPrivateBrowsing(mIsPrivate); +} + +MailnewsLoadContextInfo::~MailnewsLoadContextInfo() {} + +NS_IMETHODIMP MailnewsLoadContextInfo::GetIsPrivate(bool* aIsPrivate) { + *aIsPrivate = mIsPrivate; + return NS_OK; +} + +NS_IMETHODIMP MailnewsLoadContextInfo::GetIsAnonymous(bool* aIsAnonymous) { + *aIsAnonymous = mIsAnonymous; + return NS_OK; +} + +mozilla::OriginAttributes const* +MailnewsLoadContextInfo::OriginAttributesPtr() { + return &mOriginAttributes; +} + +NS_IMETHODIMP MailnewsLoadContextInfo::GetOriginAttributes( + JSContext* aCx, JS::MutableHandle<JS::Value> aVal) { + if (NS_WARN_IF(!ToJSValue(aCx, mOriginAttributes, aVal))) { + return NS_ERROR_FAILURE; + } + return NS_OK; +} diff --git a/comm/mailnews/base/src/MailnewsLoadContextInfo.h b/comm/mailnews/base/src/MailnewsLoadContextInfo.h new file mode 100644 index 0000000000..b44f7ae43b --- /dev/null +++ b/comm/mailnews/base/src/MailnewsLoadContextInfo.h @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// This was copied from netwerk/base/LoadContextInfo.h + +#ifndef MailnewsLoadContextInfo_h__ +#define MailnewsLoadContextInfo_h__ + +#include "nsILoadContextInfo.h" + +class nsIChannel; +class nsILoadContext; + +class MailnewsLoadContextInfo : public nsILoadContextInfo { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSILOADCONTEXTINFO + + MailnewsLoadContextInfo(bool aIsPrivate, bool aIsAnonymous, + mozilla::OriginAttributes aOriginAttributes); + + private: + virtual ~MailnewsLoadContextInfo(); + + protected: + bool mIsPrivate : 1; + bool mIsAnonymous : 1; + mozilla::OriginAttributes mOriginAttributes; +}; + +#endif diff --git a/comm/mailnews/base/src/MailnewsMigrator.jsm b/comm/mailnews/base/src/MailnewsMigrator.jsm new file mode 100644 index 0000000000..bc6ad9c3ef --- /dev/null +++ b/comm/mailnews/base/src/MailnewsMigrator.jsm @@ -0,0 +1,352 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Migrate profile (prefs and other files) from older versions of Mailnews to + * current. + * This should be run at startup. It migrates as needed: each migration + * function should be written to be a no-op when the value is already migrated + * or was never used in the old version. + */ + +const EXPORTED_SYMBOLS = ["migrateMailnews"]; + +const { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +const lazy = {}; +ChromeUtils.defineModuleGetter( + lazy, + "migrateServerUris", + "resource:///modules/MsgIncomingServer.jsm" +); + +var kServerPrefVersion = 1; +var kSmtpPrefVersion = 1; +var kABRemoteContentPrefVersion = 1; + +function migrateMailnews() { + let migrations = [ + migrateProfileClientid, + migrateServerAuthPref, + migrateServerAndUserName, + migrateABRemoteContentSettings, + ]; + + for (let fn of migrations) { + try { + fn(); + } catch (e) { + console.error(e); + } + } +} + +/** + * Creates the server specific 'CLIENTID' prefs and tries to pair up any imap + * services with smtp services which are using the same username and hostname. + */ +function migrateProfileClientid() { + // Comma-separated list of all account ids. + let accounts = Services.prefs.getCharPref("mail.accountmanager.accounts", ""); + // Comma-separated list of all smtp servers. + let smtpServers = Services.prefs.getCharPref("mail.smtpservers", ""); + // If both accounts and smtpservers are empty then there is nothing to do. + if (accounts.length == 0 && smtpServers.length == 0) { + return; + } + // A cache to allow CLIENTIDs to be stored and shared across services that + // share a username and hostname. + let clientidCache = new Map(); + // There may be accounts but no smtpservers so check the length before + // trying to split the smtp servers and iterate in the loop below. + if (smtpServers.length > 0) { + // Now walk all smtp servers and generate any missing CLIENTIDS, caching + // all CLIENTIDS along the way to be reused for matching imap servers + // if possible. + + // Since the length of the smtpServers string is non-zero then we can split + // the string by comma and iterate each entry in the comma-separated list. + for (let key of smtpServers.split(",")) { + let server = "mail.smtpserver." + key + "."; + if ( + !Services.prefs.prefHasUserValue(server + "clientid") || + !Services.prefs.getCharPref(server + "clientid", "") + ) { + // Always give outgoing servers a new unique CLIENTID. + let newClientid = Services.uuid + .generateUUID() + .toString() + .replace(/[{}]/g, ""); + Services.prefs.setCharPref(server + "clientid", newClientid); + } + let username = Services.prefs.getCharPref(server + "username", ""); + if (!username) { + // Not all SMTP servers require a username. + continue; + } + + // Cache all CLIENTIDs from all outgoing servers to reuse them for any + // incoming servers which have a matching username and hostname. + let hostname = Services.prefs.getCharPref(server + "hostname"); + let combinedKey; + try { + combinedKey = + username + "@" + Services.eTLD.getBaseDomainFromHost(hostname); + } catch (e) { + combinedKey = username + "@" + hostname; + } + clientidCache.set( + combinedKey, + Services.prefs.getCharPref(server + "clientid") + ); + } + } + + // Now walk all imap accounts and generate any missing CLIENTIDS, reusing + // cached CLIENTIDS if possible. + for (let key of accounts.split(",")) { + let serverKey = Services.prefs.getCharPref( + "mail.account." + key + ".server" + ); + let server = "mail.server." + serverKey + "."; + // Check if this imap server needs the CLIENTID preference to be populated. + if ( + !Services.prefs.prefHasUserValue(server + "clientid") || + !Services.prefs.getCharPref(server + "clientid", "") + ) { + // Clientid should only be provisioned for imap accounts. + if (Services.prefs.getCharPref(server + "type", "") != "imap") { + continue; + } + // Grab username + hostname to check if a CLIENTID is cached. + let username = Services.prefs.getCharPref(server + "userName", ""); + if (!username) { + continue; + } + let hostname = Services.prefs.getCharPref(server + "hostname"); + let combinedKey; + try { + combinedKey = + username + "@" + Services.eTLD.getBaseDomainFromHost(hostname); + } catch (e) { + combinedKey = username + "@" + hostname; + } + if (!clientidCache.has(combinedKey)) { + // Generate a new CLIENTID if no matches were found from smtp servers. + let newClientid = Services.uuid + .generateUUID() + .toString() + .replace(/[{}]/g, ""); + Services.prefs.setCharPref(server + "clientid", newClientid); + } else { + // Otherwise if a cached CLIENTID was found for this username + hostname + // then we can just use the outgoing CLIENTID which was matching. + Services.prefs.setCharPref( + server + "clientid", + clientidCache.get(combinedKey) + ); + } + } + } +} + +/** + * Migrates from pref useSecAuth to pref authMethod + */ +function migrateServerAuthPref() { + // comma-separated list of all accounts. + var accounts = Services.prefs + .getCharPref("mail.accountmanager.accounts") + .split(","); + for (let i = 0; i < accounts.length; i++) { + let accountKey = accounts[i]; // e.g. "account1" + if (!accountKey) { + continue; + } + let serverKey = Services.prefs.getCharPref( + "mail.account." + accountKey + ".server" + ); + let server = "mail.server." + serverKey + "."; + if (Services.prefs.prefHasUserValue(server + "authMethod")) { + continue; + } + if ( + !Services.prefs.prefHasUserValue(server + "useSecAuth") && + !Services.prefs.prefHasUserValue(server + "auth_login") + ) { + continue; + } + if (Services.prefs.prefHasUserValue(server + "migrated")) { + continue; + } + // auth_login = false => old-style auth + // else: useSecAuth = true => "secure auth" + // else: cleartext pw + let auth_login = Services.prefs.getBoolPref(server + "auth_login", true); + // old default, default pref now removed + let useSecAuth = Services.prefs.getBoolPref(server + "useSecAuth", false); + + if (auth_login) { + if (useSecAuth) { + Services.prefs.setIntPref( + server + "authMethod", + Ci.nsMsgAuthMethod.secure + ); + } else { + Services.prefs.setIntPref( + server + "authMethod", + Ci.nsMsgAuthMethod.passwordCleartext + ); + } + } else { + Services.prefs.setIntPref(server + "authMethod", Ci.nsMsgAuthMethod.old); + } + Services.prefs.setIntPref(server + "migrated", kServerPrefVersion); + } + + // same again for SMTP servers + var smtpservers = Services.prefs.getCharPref("mail.smtpservers").split(","); + for (let i = 0; i < smtpservers.length; i++) { + if (!smtpservers[i]) { + continue; + } + let server = "mail.smtpserver." + smtpservers[i] + "."; + if (Services.prefs.prefHasUserValue(server + "authMethod")) { + continue; + } + if ( + !Services.prefs.prefHasUserValue(server + "useSecAuth") && + !Services.prefs.prefHasUserValue(server + "auth_method") + ) { + continue; + } + if (Services.prefs.prefHasUserValue(server + "migrated")) { + continue; + } + // auth_method = 0 => no auth + // else: useSecAuth = true => "secure auth" + // else: cleartext pw + let auth_method = Services.prefs.getIntPref(server + "auth_method", 1); + let useSecAuth = Services.prefs.getBoolPref(server + "useSecAuth", false); + + if (auth_method) { + if (useSecAuth) { + Services.prefs.setIntPref( + server + "authMethod", + Ci.nsMsgAuthMethod.secure + ); + } else { + Services.prefs.setIntPref( + server + "authMethod", + Ci.nsMsgAuthMethod.passwordCleartext + ); + } + } else { + Services.prefs.setIntPref(server + "authMethod", Ci.nsMsgAuthMethod.none); + } + Services.prefs.setIntPref(server + "migrated", kSmtpPrefVersion); + } +} + +/** + * For each mail.server.key. branch, + * - migrate realhostname to hostname + * - migrate realuserName to userName + */ +function migrateServerAndUserName() { + let branch = Services.prefs.getBranch("mail.server."); + + // Collect all the server keys. + let keySet = new Set(); + for (let name of branch.getChildList("")) { + keySet.add(name.split(".")[0]); + } + keySet.delete("default"); + + for (let key of keySet) { + let type = branch.getCharPref(`${key}.type`, ""); + let hostname = branch.getCharPref(`${key}.hostname`, ""); + let username = branch.getCharPref(`${key}.userName`, ""); + let realHostname = branch.getCharPref(`${key}.realhostname`, ""); + if (realHostname) { + branch.setCharPref(`${key}.hostname`, realHostname); + branch.clearUserPref(`${key}.realhostname`); + } + let realUsername = branch.getCharPref(`${key}.realuserName`, ""); + if (realUsername) { + branch.setCharPref(`${key}.userName`, realUsername); + branch.clearUserPref(`${key}.realuserName`); + } + // Previously, when hostname/username changed, LoginManager and many prefs + // still contain the old hostname/username, try to migrate them to use the + // new hostname/username. + if ( + ["imap", "pop3", "nntp"].includes(type) && + (realHostname || realUsername) + ) { + let localStoreType = { imap: "imap", pop3: "mailbox", nntp: "news" }[ + type + ]; + lazy.migrateServerUris( + localStoreType, + hostname, + username, + realHostname || hostname, + realUsername || username + ); + } + } +} + +/** + * The address book used to contain information about whether to allow remote + * content for a given contact. Now we use the permission manager for that. + * Do a one-time migration for it. + */ +function migrateABRemoteContentSettings() { + if (Services.prefs.prefHasUserValue("mail.ab_remote_content.migrated")) { + return; + } + + // Search through all of our local address books looking for a match. + for (let addrbook of MailServices.ab.directories) { + let migrateAddress = function (aEmail) { + let uri = Services.io.newURI( + "chrome://messenger/content/email=" + aEmail + ); + Services.perms.addFromPrincipal( + Services.scriptSecurityManager.createContentPrincipal(uri, {}), + "image", + Services.perms.ALLOW_ACTION + ); + }; + + try { + // If it's a read-only book, don't try to find a card as we we could never + // have set the AllowRemoteContent property. + if (addrbook.readOnly) { + continue; + } + + for (let card of addrbook.childCards) { + if (card.getProperty("AllowRemoteContent", "0") == "0") { + // Not allowed for this contact. + continue; + } + + for (let emailAddress of card.emailAddresses) { + migrateAddress(emailAddress); + } + } + } catch (e) { + console.error(e); + } + } + + Services.prefs.setIntPref( + "mail.ab_remote_content.migrated", + kABRemoteContentPrefVersion + ); +} diff --git a/comm/mailnews/base/src/MsgAsyncPrompter.jsm b/comm/mailnews/base/src/MsgAsyncPrompter.jsm new file mode 100644 index 0000000000..e04e9a9418 --- /dev/null +++ b/comm/mailnews/base/src/MsgAsyncPrompter.jsm @@ -0,0 +1,621 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["MsgAsyncPrompter", "MsgAuthPrompt"]; + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const LoginInfo = Components.Constructor( + "@mozilla.org/login-manager/loginInfo;1", + "nsILoginInfo", + "init" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + Deprecated: "resource://gre/modules/Deprecated.sys.mjs", + PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs", +}); + +XPCOMUtils.defineLazyGetter(lazy, "dialogsBundle", function () { + return Services.strings.createBundle( + "chrome://global/locale/commonDialogs.properties" + ); +}); + +XPCOMUtils.defineLazyGetter(lazy, "passwordsBundle", function () { + return Services.strings.createBundle( + "chrome://passwordmgr/locale/passwordmgr.properties" + ); +}); + +XPCOMUtils.defineLazyGetter(lazy, "brandFullName", function () { + return Services.strings + .createBundle("chrome://branding/locale/brand.properties") + .GetStringFromName("brandFullName"); +}); + +function runnablePrompter(asyncPrompter, hashKey) { + this._asyncPrompter = asyncPrompter; + this._hashKey = hashKey; +} + +runnablePrompter.prototype = { + _asyncPrompter: null, + _hashKey: null, + + _promiseAuthPrompt(listener) { + return new Promise((resolve, reject) => { + try { + listener.onPromptStartAsync({ onAuthResult: resolve }); + } catch (e) { + if (e.result == Cr.NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED) { + // Fall back to onPromptStart, for add-ons compat + lazy.Deprecated.warning( + "onPromptStart has been replaced by onPromptStartAsync", + "https://bugzilla.mozilla.org/show_bug.cgi?id=1176399" + ); + let ok = listener.onPromptStart(); + resolve(ok); + } else { + reject(e); + } + } + }); + }, + + async run() { + await Services.logins.initializationPromise; + this._asyncPrompter._log.debug("Running prompt for " + this._hashKey); + let prompter = this._asyncPrompter._pendingPrompts[this._hashKey]; + let ok = false; + try { + ok = await this._promiseAuthPrompt(prompter.first); + } catch (ex) { + console.error("runnablePrompter:run: " + ex + "\n"); + prompter.first.onPromptCanceled(); + } + + delete this._asyncPrompter._pendingPrompts[this._hashKey]; + + for (var consumer of prompter.consumers) { + try { + if (ok) { + consumer.onPromptAuthAvailable(); + } else { + consumer.onPromptCanceled(); + } + } catch (ex) { + // Log the error for extension devs and others to pick up. + console.error( + "runnablePrompter:run: consumer.onPrompt* reported an exception: " + + ex + + "\n" + ); + } + } + this._asyncPrompter._asyncPromptInProgress--; + + this._asyncPrompter._log.debug( + "Finished running prompter for " + this._hashKey + ); + this._asyncPrompter._doAsyncAuthPrompt(); + }, +}; + +function MsgAsyncPrompter() { + this._pendingPrompts = {}; + // By default, only log warnings to the error console + // You can use the preference: + // msgAsyncPrompter.loglevel + // To change this up. Values should be one of: + // Fatal/Error/Warn/Info/Config/Debug/Trace/All + this._log = console.createInstance({ + prefix: "mail.asyncprompter", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.asyncprompter.loglevel", + }); +} + +MsgAsyncPrompter.prototype = { + QueryInterface: ChromeUtils.generateQI(["nsIMsgAsyncPrompter"]), + + _pendingPrompts: null, + _asyncPromptInProgress: 0, + _log: null, + + queueAsyncAuthPrompt(aKey, aJumpQueue, aCaller) { + if (aKey in this._pendingPrompts) { + this._log.debug( + "Prompt bound to an existing one in the queue, key: " + aKey + ); + this._pendingPrompts[aKey].consumers.push(aCaller); + return; + } + + this._log.debug("Adding new prompt to the queue, key: " + aKey); + let asyncPrompt = { + first: aCaller, + consumers: [], + }; + + this._pendingPrompts[aKey] = asyncPrompt; + if (aJumpQueue) { + this._asyncPromptInProgress++; + + this._log.debug("Forcing runnablePrompter for " + aKey); + + let runnable = new runnablePrompter(this, aKey); + Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL); + } else { + this._doAsyncAuthPrompt(); + } + }, + + _doAsyncAuthPrompt() { + if (this._asyncPromptInProgress > 0) { + this._log.debug( + "_doAsyncAuthPrompt bypassed - prompt already in progress" + ); + return; + } + + // Find the first prompt key we have in the queue. + let hashKey = null; + for (hashKey in this._pendingPrompts) { + break; + } + + if (!hashKey) { + return; + } + + this._asyncPromptInProgress++; + + this._log.debug("Dispatching runnablePrompter for " + hashKey); + + let runnable = new runnablePrompter(this, hashKey); + Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL); + }, +}; + +/** + * An implementation of nsIAuthPrompt which is roughly the same as + * LoginManagerAuthPrompter was before the check box option was removed from + * nsIPromptService. + * + * Calls our own version of promptUsernameAndPassword/promptPassword, which + * directly open the prompt. + * + * @implements {nsIAuthPrompt} + */ +class MsgAuthPrompt { + QueryInterface = ChromeUtils.generateQI(["nsIAuthPrompt"]); + + _getFormattedOrigin(aURI) { + let uri; + if (aURI instanceof Ci.nsIURI) { + uri = aURI; + } else { + uri = Services.io.newURI(aURI); + } + + return uri.scheme + "://" + uri.displayHostPort; + } + + _getRealmInfo(aRealmString) { + let httpRealm = /^.+ \(.+\)$/; + if (httpRealm.test(aRealmString)) { + return [null, null, null]; + } + + let uri = Services.io.newURI(aRealmString); + let pathname = ""; + + if (uri.pathQueryRef != "/") { + pathname = uri.pathQueryRef; + } + + let formattedOrigin = this._getFormattedOrigin(uri); + + return [formattedOrigin, formattedOrigin + pathname, uri.username]; + } + + _getLocalizedString(key, formatArgs) { + if (formatArgs) { + return lazy.passwordsBundle.formatStringFromName(key, formatArgs); + } + return lazy.passwordsBundle.GetStringFromName(key); + } + + /** + * Wrapper around the prompt service prompt. Saving random fields here + * doesn't really make sense and therefore isn't implemented. + */ + prompt( + aDialogTitle, + aText, + aPasswordRealm, + aSavePassword, + aDefaultText, + aResult + ) { + if (aSavePassword != Ci.nsIAuthPrompt.SAVE_PASSWORD_NEVER) { + throw new Components.Exception( + "prompt only supports SAVE_PASSWORD_NEVER", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + if (aDefaultText) { + aResult.value = aDefaultText; + } + + return Services.prompt.prompt( + this._chromeWindow, + aDialogTitle, + aText, + aResult, + null, + {} + ); + } + + /** + * Looks up a username and password in the database. Will prompt the user + * with a dialog, even if a username and password are found. + */ + promptUsernameAndPassword( + aDialogTitle, + aText, + aPasswordRealm, + aSavePassword, + aUsername, + aPassword + ) { + if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) { + throw new Components.Exception( + "promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + let checkBox = { value: false }; + let checkBoxLabel = null; + let [origin, realm] = this._getRealmInfo(aPasswordRealm); + + // If origin is null, we can't save this login. + if (origin) { + let canRememberLogin = + aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY && + Services.logins.getLoginSavingEnabled(origin); + + // if checkBoxLabel is null, the checkbox won't be shown at all. + if (canRememberLogin) { + checkBoxLabel = this._getLocalizedString("rememberPassword"); + } + + for (let login of Services.logins.findLogins(origin, null, realm)) { + if (login.username == aUsername.value) { + checkBox.value = true; + aUsername.value = login.username; + // If the caller provided a password, prefer it. + if (!aPassword.value) { + aPassword.value = login.password; + } + } + } + } + + let ok = nsIPrompt_promptUsernameAndPassword( + aDialogTitle, + aText, + aUsername, + aPassword, + checkBoxLabel, + checkBox + ); + + if (!ok || !checkBox.value || !origin) { + return ok; + } + + let newLogin = new LoginInfo( + origin, + null, + realm, + aUsername.value, + aPassword.value + ); + Services.logins.addLogin(newLogin); + + return ok; + } + + /** + * If a password is found in the database for the password realm, it is + * returned straight away without displaying a dialog. + * + * If a password is not found in the database, the user will be prompted + * with a dialog with a text field and ok/cancel buttons. If the user + * allows it, then the password will be saved in the database. + */ + promptPassword( + aDialogTitle, + aText, + aPasswordRealm, + aSavePassword, + aPassword + ) { + if (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_FOR_SESSION) { + throw new Components.Exception( + "promptUsernameAndPassword doesn't support SAVE_PASSWORD_FOR_SESSION", + Cr.NS_ERROR_NOT_IMPLEMENTED + ); + } + + let checkBox = { value: false }; + let checkBoxLabel = null; + let [origin, realm, username] = this._getRealmInfo(aPasswordRealm); + + username = decodeURIComponent(username); + + // If origin is null, we can't save this login. + if (origin) { + let canRememberLogin = + aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY && + Services.logins.getLoginSavingEnabled(origin); + + // if checkBoxLabel is null, the checkbox won't be shown at all. + if (canRememberLogin) { + checkBoxLabel = this._getLocalizedString("rememberPassword"); + } + + if (!aPassword.value) { + // Look for existing logins. + for (let login of Services.logins.findLogins(origin, null, realm)) { + if (login.username == username) { + aPassword.value = login.password; + return true; + } + } + } + } + + let ok = nsIPrompt_promptPassword( + aDialogTitle, + aText, + aPassword, + checkBoxLabel, + checkBox + ); + + if (ok && checkBox.value && origin && aPassword.value) { + let newLogin = new LoginInfo( + origin, + null, + realm, + username, + aPassword.value + ); + + Services.logins.addLogin(newLogin); + } + + return ok; + } + + /** + * Implements nsIPrompt.promptPassword as it was before the check box option + * was removed. + * + * Puts up a dialog with a password field and an optional, labelled checkbox. + * + * @param {string} dialogTitle - Text to appear in the title of the dialog. + * @param {string} text - Text to appear in the body of the dialog. + * @param {?object} password - Contains the default value for the password + * field when this method is called (null value is ok). + * Upon return, if the user pressed OK, then this parameter contains a + * newly allocated string value. + * Otherwise, the parameter's value is unmodified. + * @param {?string} checkMsg - Text to appear with the checkbox. If null, + * check box will not be shown. + * @param {?object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state after + * this method returns. + * + * @returns {boolean} true for OK, false for Cancel. + */ + promptPassword2(dialogTitle, text, password, checkMsg, checkValue) { + return nsIPrompt_promptPassword( + dialogTitle, + text, + password, + checkMsg, + checkValue + ); + } + + /** + * Requests a username and a password. Implementations will commonly show a + * dialog with a username and password field, depending on flags also a + * domain field. + * + * @param {nsIChannel} channel - The channel that requires authentication. + * @param {number} level - One of the level constants from nsIAuthPrompt2. + * See there for descriptions of the levels. + * @param {nsIAuthInformation} authInfo - Authentication information object. + * The implementation should fill in this object with the information + * entered by the user before returning. + * @param {string} checkboxLabel + * Text to appear with the checkbox. If null, check box will not be shown. + * @param {object} checkValue + * Contains the initial checked state of the checkbox when this method + * is called and the final checked state after this method returns. + * @returns {boolean} true for OK, false for Cancel. + */ + promptAuth(channel, level, authInfo, checkboxLabel, checkValue) { + let title = lazy.dialogsBundle.formatStringFromName( + "PromptUsernameAndPassword3", + [lazy.brandFullName] + ); + let text = lazy.dialogsBundle.formatStringFromName( + "EnterUserPasswordFor2", + [`${channel.URI.scheme}://${channel.URI.host}`] + ); + + let username = { value: authInfo.username || "" }; + let password = { value: authInfo.password || "" }; + + let ok = nsIPrompt_promptUsernameAndPassword( + title, + text, + username, + password, + checkboxLabel, + checkValue + ); + + if (ok) { + authInfo.username = username.value; + authInfo.password = password.value; + } + + return ok; + } +} + +/** + * @param {string} dialogTitle - Text to appear in the title of the dialog. + * @param {string} text - Text to appear in the body of the dialog. + * @param {?object} username + * Contains the default value for the username field when this method + * is called (null value is ok). Upon return, if the user pressed OK, + * then this parameter contains a newly allocated string value. + * @param {?object} password - Contains the default value for the password + * field when this method is called (null value is ok). + * Upon return, if the user pressed OK, then this parameter contains a + * newly allocated string value. + * Otherwise, the parameter's value is unmodified. + * @param {?string} checkMsg - Text to appear with the checkbox. If null, + * check box will not be shown. + * @param {?object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state after + * this method returns. + * @returns {boolean} true for OK, false for Cancel. + */ +function nsIPrompt_promptUsernameAndPassword( + dialogTitle, + text, + username, + password, + checkMsg, + checkValue +) { + if (!dialogTitle) { + dialogTitle = lazy.dialogsBundle.formatStringFromName( + "PromptUsernameAndPassword3", + [lazy.brandFullName] + ); + } + + let args = { + promptType: "promptUserAndPass", + title: dialogTitle, + text, + user: username.value, + pass: password.value, + checkLabel: checkMsg, + checked: checkValue.value, + ok: false, + }; + + let propBag = lazy.PromptUtils.objectToPropBag(args); + Services.ww.openWindow( + Services.ww.activeWindow, + "chrome://global/content/commonDialog.xhtml", + "_blank", + "centerscreen,chrome,modal,titlebar", + propBag + ); + lazy.PromptUtils.propBagToObject(propBag, args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + checkValue.value = args.checked; + username.value = args.user; + password.value = args.pass; + } + + return ok; +} + +/** + * Implements nsIPrompt.promptPassword as it was before the check box option + * was removed. + * + * Puts up a dialog with a password field and an optional, labelled checkbox. + * + * @param {string} dialogTitle - Text to appear in the title of the dialog. + * @param {string} text - Text to appear in the body of the dialog. + * @param {?object} password - Contains the default value for the password + * field when this method is called (null value is ok). + * Upon return, if the user pressed OK, then this parameter contains a + * newly allocated string value. + * Otherwise, the parameter's value is unmodified. + * @param {?string} checkMsg - Text to appear with the checkbox. If null, + * check box will not be shown. + * @param {?object} checkValue - Contains the initial checked state of the + * checkbox when this method is called and the final checked state after + * this method returns. + * + * @returns {boolean} true for OK, false for Cancel. + */ +function nsIPrompt_promptPassword( + dialogTitle, + text, + password, + checkMsg, + checkValue +) { + if (!dialogTitle) { + dialogTitle = lazy.dialogsBundle.formatStringFromName( + "PromptUsernameAndPassword3", + [lazy.brandFullName] + ); + } + + let args = { + promptType: "promptPassword", + title: dialogTitle, + text, + pass: password.value, + checkLabel: checkMsg, + checked: checkValue.value, + ok: false, + }; + + let propBag = lazy.PromptUtils.objectToPropBag(args); + Services.ww.openWindow( + Services.ww.activeWindow, + "chrome://global/content/commonDialog.xhtml", + "_blank", + "centerscreen,chrome,modal,titlebar", + propBag + ); + lazy.PromptUtils.propBagToObject(propBag, args); + + // Did user click Ok or Cancel? + let ok = args.ok; + if (ok) { + checkValue.value = args.checked; + password.value = args.pass; + } + + return ok; +} diff --git a/comm/mailnews/base/src/MsgDBCacheManager.jsm b/comm/mailnews/base/src/MsgDBCacheManager.jsm new file mode 100644 index 0000000000..9506f08e5e --- /dev/null +++ b/comm/mailnews/base/src/MsgDBCacheManager.jsm @@ -0,0 +1,185 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Message DB Cache manager + */ + +/* :::::::: Constants and Helpers ::::::::::::::: */ + +const EXPORTED_SYMBOLS = ["msgDBCacheManager"]; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var log = console.createInstance({ + prefix: "mailnews.database.dbcache", + maxLogLevel: "Warn", + maxLogLevelPref: "mailnews.database.dbcache.loglevel", +}); + +/** + */ +var DBCACHE_INTERVAL_DEFAULT_MS = 60000; // 1 minute + +/* :::::::: The Module ::::::::::::::: */ + +var msgDBCacheManager = { + _initialized: false, + + _msgDBCacheTimer: null, + + _msgDBCacheTimerIntervalMS: DBCACHE_INTERVAL_DEFAULT_MS, + + _dbService: null, + + /** + * This is called on startup + */ + init() { + if (this._initialized) { + return; + } + + this._dbService = Cc["@mozilla.org/msgDatabase/msgDBService;1"].getService( + Ci.nsIMsgDBService + ); + + // we listen for "quit-application-granted" instead of + // "quit-application-requested" because other observers of the + // latter can cancel the shutdown. + Services.obs.addObserver(this, "quit-application-granted"); + + this.startPeriodicCheck(); + + this._initialized = true; + }, + + /* ........ Timer Callback ................*/ + + _dbCacheCheckTimerCallback() { + msgDBCacheManager.checkCachedDBs(); + }, + + /* ........ Observer Notification Handler ................*/ + + observe(aSubject, aTopic, aData) { + switch (aTopic) { + // This is observed before any windows start unloading if something other + // than the last 3pane window closing requested the application be + // shutdown. For example, when the user quits via the file menu. + case "quit-application-granted": + Services.obs.removeObserver(this, "quit-application-granted"); + this.stopPeriodicCheck(); + break; + } + }, + + /* ........ Public API ................*/ + + /** + * Stops db cache check + */ + stopPeriodicCheck() { + if (this._dbCacheCheckTimer) { + this._dbCacheCheckTimer.cancel(); + + delete this._dbCacheCheckTimer; + this._dbCacheCheckTimer = null; + } + }, + + /** + * Starts periodic db cache check + */ + startPeriodicCheck() { + if (!this._dbCacheCheckTimer) { + this._dbCacheCheckTimer = Cc["@mozilla.org/timer;1"].createInstance( + Ci.nsITimer + ); + + this._dbCacheCheckTimer.initWithCallback( + this._dbCacheCheckTimerCallback, + this._msgDBCacheTimerIntervalMS, + Ci.nsITimer.TYPE_REPEATING_SLACK + ); + } + }, + + /** + * Checks if any DBs need to be closed due to inactivity or too many of them open. + */ + checkCachedDBs() { + let idleLimit = Services.prefs.getIntPref("mail.db.idle_limit"); + let maxOpenDBs = Services.prefs.getIntPref("mail.db.max_open"); + + // db.lastUseTime below is in microseconds while Date.now and idleLimit pref + // is in milliseconds. + let closeThreshold = (Date.now() - idleLimit) * 1000; + let cachedDBs = this._dbService.openDBs; + log.info( + "Periodic check of cached folder databases (DBs), count=" + + cachedDBs.length + ); + // Count databases that are already closed or get closed now due to inactivity. + let numClosing = 0; + // Count databases whose folder is open in a window. + let numOpenInWindow = 0; + let dbs = []; + for (let db of cachedDBs) { + if (!db.folder?.databaseOpen) { + // The DB isn't really open anymore. + log.debug("Skipping, DB not open for folder: " + db.folder?.name); + numClosing++; + continue; + } + + if (MailServices.mailSession.IsFolderOpenInWindow(db.folder)) { + // The folder is open in a window so this DB must not be closed. + log.debug("Skipping, DB open in window for folder: " + db.folder.name); + numOpenInWindow++; + continue; + } + + if (db.lastUseTime < closeThreshold) { + // DB open too log without activity. + log.debug("Closing expired DB for folder: " + db.folder.name); + db.folder.msgDatabase = null; + numClosing++; + continue; + } + + // Database eligible for closing. + dbs.push(db); + } + log.info( + "DBs open in a window: " + + numOpenInWindow + + ", DBs open: " + + dbs.length + + ", DBs already closing: " + + numClosing + ); + let dbsToClose = Math.max( + dbs.length - Math.max(maxOpenDBs - numOpenInWindow, 0), + 0 + ); + if (dbsToClose > 0) { + // Close some DBs so that we do not have more than maxOpenDBs. + // However, we skipped DBs for folders that are open in a window + // so if there are so many windows open, it may be possible for + // more than maxOpenDBs folders to stay open after this loop. + log.info("Need to close " + dbsToClose + " more DBs"); + // Order databases by lowest lastUseTime (oldest) at the end. + dbs.sort((a, b) => b.lastUseTime - a.lastUseTime); + while (dbsToClose > 0) { + let db = dbs.pop(); + log.debug("Closing DB for folder: " + db.folder.name); + db.folder.msgDatabase = null; + dbsToClose--; + } + } + }, +}; diff --git a/comm/mailnews/base/src/MsgIncomingServer.jsm b/comm/mailnews/base/src/MsgIncomingServer.jsm new file mode 100644 index 0000000000..768fe9340e --- /dev/null +++ b/comm/mailnews/base/src/MsgIncomingServer.jsm @@ -0,0 +1,1268 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["migrateServerUris", "MsgIncomingServer"]; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +/** + * When hostname/username changes, update the corresponding entry in + * nsILoginManager. + * + * @param {string} localStoreType - The store type of the current server. + * @param {string} oldHostname - The hostname before the change. + * @param {string} oldUsername - The username before the change. + * @param {string} newHostname - The hostname after the change. + * @param {string} newUsername - The username after the change. + */ +function migratePassword( + localStoreType, + oldHostname, + oldUsername, + newHostname, + newUsername +) { + // When constructing nsIURI, need to wrap IPv6 address in []. + oldHostname = oldHostname.includes(":") ? `[${oldHostname}]` : oldHostname; + let oldServerUri = `${localStoreType}://${encodeURIComponent(oldHostname)}`; + newHostname = newHostname.includes(":") ? `[${newHostname}]` : newHostname; + let newServerUri = `${localStoreType}://${encodeURIComponent(newHostname)}`; + + let logins = Services.logins.findLogins(oldServerUri, "", oldServerUri); + for (let login of logins) { + if (login.username == oldUsername) { + // If a nsILoginInfo exists for the old hostname/username, update it to + // use the new hostname/username. + let newLogin = Cc[ + "@mozilla.org/login-manager/loginInfo;1" + ].createInstance(Ci.nsILoginInfo); + newLogin.init( + newServerUri, + null, + newServerUri, + newUsername, + login.password, + "", + "" + ); + Services.logins.modifyLogin(login, newLogin); + } + } +} + +/** + * When hostname/username changes, update the folder attributes in related + * identities. + * + * @param {string} oldServerUri - The server uri before the change. + * @param {string} newServerUri - The server uri after the change. + */ +function migrateIdentities(oldServerUri, newServerUri) { + for (let identity of MailServices.accounts.allIdentities) { + let attributes = [ + "fcc_folder", + "draft_folder", + "archive_folder", + "stationery_folder", + ]; + for (let attr of attributes) { + let folderUri = identity.getUnicharAttribute(attr); + if (folderUri.startsWith(oldServerUri)) { + identity.setUnicharAttribute( + attr, + folderUri.replace(oldServerUri, newServerUri) + ); + } + } + } +} + +/** + * When hostname/username changes, update .spamActionTargetAccount and + * .spamActionTargetFolder prefs. + * + * @param {string} oldServerUri - The server uri before the change. + * @param {string} newServerUri - The server uri after the change. + */ +function migrateSpamActions(oldServerUri, newServerUri) { + for (let server of MailServices.accounts.allServers) { + let targetAccount = server.getCharValue("spamActionTargetAccount"); + let targetFolder = server.getUnicharValue("spamActionTargetFolder"); + if (targetAccount.startsWith(oldServerUri)) { + server.setCharValue( + "spamActionTargetAccount", + targetAccount.replace(oldServerUri, newServerUri) + ); + } + if (targetFolder.startsWith(oldServerUri)) { + server.setUnicharValue( + "spamActionTargetFolder", + targetFolder.replace(oldServerUri, newServerUri) + ); + } + } +} + +/** + * When hostname/username changes, update targetFolderUri in related filters + * to the new folder uri. + * + * @param {string} oldServerUri - The server uri before the change. + * @param {string} newServerUri - The server uri after the change. + */ +function migrateFilters(oldServerUri, newServerUri) { + for (let server of MailServices.accounts.allServers) { + let filterList; + try { + filterList = server.getFilterList(null); + if (!server.canHaveFilters || !filterList) { + continue; + } + } catch (e) { + continue; + } + let changed = false; + for (let i = 0; i < filterList.filterCount; i++) { + let filter = filterList.getFilterAt(i); + for (let action of filter.sortedActionList) { + let targetFolderUri; + try { + targetFolderUri = action.targetFolderUri; + } catch (e) { + continue; + } + if (targetFolderUri.startsWith(oldServerUri)) { + action.targetFolderUri = targetFolderUri.replace( + oldServerUri, + newServerUri + ); + changed = true; + } + } + } + if (changed) { + filterList.saveToDefaultFile(); + } + } +} + +/** + * Migrate server uris in LoginManager and various account/folder prefs. + * + * @param {string} localStoreType - The store type of the current server. + * @param {string} oldHostname - The hostname before the change. + * @param {string} oldUsername - The username before the change. + * @param {string} newHostname - The hostname after the change. + * @param {string} newUsername - The username after the change. + */ +function migrateServerUris( + localStoreType, + oldHostname, + oldUsername, + newHostname, + newUsername +) { + try { + migratePassword( + localStoreType, + oldHostname, + oldUsername, + newHostname, + newUsername + ); + } catch (e) { + console.error(e); + } + + let oldAuth = oldUsername ? `${encodeURIComponent(oldUsername)}@` : ""; + let newAuth = newUsername ? `${encodeURIComponent(newUsername)}@` : ""; + // When constructing nsIURI, need to wrap IPv6 address in []. + oldHostname = oldHostname.includes(":") ? `[${oldHostname}]` : oldHostname; + let oldServerUri = `${localStoreType}://${oldAuth}${encodeURIComponent( + oldHostname + )}`; + newHostname = newHostname.includes(":") ? `[${newHostname}]` : newHostname; + let newServerUri = `${localStoreType}://${newAuth}${encodeURIComponent( + newHostname + )}`; + + try { + migrateIdentities(oldServerUri, newServerUri); + } catch (e) { + console.error(e); + } + try { + migrateSpamActions(oldServerUri, newServerUri); + } catch (e) { + console.error(e); + } + try { + migrateFilters(oldServerUri, newServerUri); + } catch (e) { + console.error(e); + } +} + +/** + * A base class for incoming server, should not be used directly. + * + * @implements {nsIMsgIncomingServer} + * @implements {nsISupportsWeakReference} + * @implements {nsIObserver} + * @abstract + */ +class MsgIncomingServer { + QueryInterface = ChromeUtils.generateQI([ + "nsIMsgIncomingServer", + "nsISupportsWeakReference", + "nsIObserver", + ]); + + constructor() { + // nsIMsgIncomingServer attributes that map directly to pref values. + this._mapAttrsToPrefs([ + ["Char", "type"], + ["Char", "clientid"], + ["Int", "authMethod"], + ["Int", "biffMinutes", "check_time"], + ["Int", "maxMessageSize", "max_size"], + ["Int", "incomingDuplicateAction", "dup_action"], + ["Bool", "clientidEnabled"], + ["Bool", "downloadOnBiff", "download_on_biff"], + ["Bool", "valid"], + ["Bool", "emptyTrashOnExit", "empty_trash_on_exit"], + ["Bool", "canDelete"], + ["Bool", "loginAtStartUp", "login_at_startup"], + [ + "Bool", + "defaultCopiesAndFoldersPrefsToServer", + "allows_specialfolders_usage", + ], + ["Bool", "canCreateFoldersOnServer", "canCreateFolders"], + ["Bool", "canFileMessagesOnServer", "canFileMessages"], + ["Bool", "limitOfflineMessageSize", "limit_offline_message_size"], + ["Bool", "hidden"], + ]); + + // nsIMsgIncomingServer attributes. + this.performingBiff = false; + this.accountManagerChrome = "am-main.xhtml"; + this.biffState = Ci.nsIMsgFolder.nsMsgBiffState_Unknown; + this.downloadMessagesAtStartup = false; + this.canHaveFilters = true; + this.canBeDefaultServer = false; + this.displayStartupPage = true; + this.supportsDiskSpace = true; + this.canCompactFoldersOnServer = true; + this.canUndoDeleteOnServer = true; + this.sortOrder = 100000000; + + // @type {Map<string, number>} - The key is MsgId+Subject, the value is + // this._hdrIndex. + this._knownHdrMap = new Map(); + this._hdrIndex = 0; + + Services.obs.addObserver(this, "passwordmgr-storage-changed"); + } + + /** + * Observe() receives notifications for all accounts, not just this server's + * account. So we ignore all notifications not intended for this server. + * When the state of the password manager changes we need to clear the + * this server's password from the cache in case the user just changed or + * removed the password or username. + * OAuth2 servers often automatically change the password manager's stored + * password (the token). + */ + observe(subject, topic, data) { + if (topic == "passwordmgr-storage-changed") { + // Check that the notification is for this server and user. + let otherFullName = ""; + let otherUsername = ""; + if (subject instanceof Ci.nsILoginInfo) { + // The login info for a server has been removed with data being + // "removeLogin" or "removeAllLogins". + otherFullName = subject.origin; + otherUsername = subject.username; + } else if (subject instanceof Ci.nsIArray) { + // Probably a 2 element array containing old and new login info due to + // data being "modifyLogin". E.g., a user has modified the password or + // username in the password manager or an OAuth2 token string has + // automatically changed. Only need to look at names in first array + // element (login info before any modification) since the user might + // have changed the username as found in the 2nd elements. (The + // hostname can't be modified in the password manager. + otherFullName = subject.queryElementAt(0, Ci.nsISupports).origin; + otherUsername = subject.queryElementAt(0, Ci.nsISupports).username; + } + if (otherFullName) { + if ( + otherFullName != "mailbox://" + this.hostName || + otherUsername != this.username + ) { + // Not for this server; keep this server's cached password. + return; + } + } else if (data != "hostSavingDisabled") { + // "hostSavingDisabled" only occurs during test_smtpServer.js and + // expects the password to be removed from memory cache. Otherwise, we + // don't have enough information to decide to remove the cached + // password, so keep it. + return; + } + // Remove the password for this server cached in memory. + this.password = ""; + } + } + + /** + * Set up getters/setters for attributes that map directly to pref values. + * + * @param {string[]} attributes - An array of attributes. Each attribute is + * defined by its type, name and corresponding prefName. + */ + _mapAttrsToPrefs(attributes) { + for (let [type, attrName, prefName] of attributes) { + prefName = prefName || attrName; + Object.defineProperty(this, attrName, { + configurable: true, + get: () => this[`get${type}Value`](prefName), + set: value => { + this[`set${type}Value`](prefName, value); + }, + }); + } + } + + get key() { + return this._key; + } + + set key(key) { + this._key = key; + this._prefs = Services.prefs.getBranch(`mail.server.${key}.`); + this._defaultPrefs = Services.prefs.getBranch("mail.server.default."); + } + + get UID() { + let uid = this._prefs.getStringPref("uid", ""); + if (uid) { + return uid; + } + return (this.UID = Services.uuid + .generateUUID() + .toString() + .substring(1, 37)); + } + + set UID(uid) { + if (this._prefs.prefHasUserValue("uid")) { + throw new Components.Exception("uid is already set", Cr.NS_ERROR_ABORT); + } + this._prefs.setStringPref("uid", uid); + } + + get hostName() { + let hostname = this.getUnicharValue("hostname"); + if (hostname.includes(":")) { + // Reformat the hostname if it contains a port number. + this.hostName = hostname; + return this.hostName; + } + return hostname; + } + + set hostName(value) { + let oldName = this.hostName; + this._setHostName("hostname", value); + + if (oldName && oldName != value) { + this.onUserOrHostNameChanged(oldName, value, true); + } + } + + _setHostName(prefName, value) { + let [host, port] = value.split(":"); + if (port) { + this.port = Number(port); + } + this.setUnicharValue(prefName, host); + } + + get username() { + return this.getUnicharValue("userName"); + } + + set username(value) { + let oldName = this.username; + if (oldName && oldName != value) { + this.setUnicharValue("userName", value); + this.onUserOrHostNameChanged(oldName, value, false); + } else { + this.setUnicharValue("userName", value); + } + } + + get port() { + let port = this.getIntValue("port"); + if (port > 1) { + return port; + } + + // If the port isn't set, use the default port based on the protocol. + return this.protocolInfo.getDefaultServerPort( + this.socketType == Ci.nsMsgSocketType.SSL + ); + } + + set port(value) { + this.setIntValue("port", value); + } + + get protocolInfo() { + return Cc[ + `@mozilla.org/messenger/protocol/info;1?type=${this.type}` + ].getService(Ci.nsIMsgProtocolInfo); + } + + get socketType() { + try { + return this._prefs.getIntPref("socketType"); + } catch (e) { + // socketType is set to default value. Look at isSecure setting. + if (this._prefs.getBoolPref("isSecure", false)) { + return Ci.nsMsgSocketType.SSL; + } + return this._defaultPrefs.getIntPref( + "socketType", + Ci.nsMsgSocketType.plain + ); + } + } + + set socketType(value) { + let wasSecure = this.isSecure; + this._prefs.setIntPref("socketType", value); + let isSecure = this.isSecure; + if (wasSecure != isSecure) { + this.rootFolder.NotifyBoolPropertyChanged( + "isSecure", + wasSecure, + isSecure + ); + } + } + + get isSecure() { + return [Ci.nsMsgSocketType.alwaysSTARTTLS, Ci.nsMsgSocketType.SSL].includes( + this.socketType + ); + } + + get serverURI() { + return this._getServerURI(true); + } + + /** + * Get server URI in the form of localStoreType://[user@]hostname. + * + * @param {boolean} includeUsername - Whether to include the username. + * @returns {string} + */ + _getServerURI(includeUsername) { + let auth = + includeUsername && this.username + ? `${encodeURIComponent(this.username)}@` + : ""; + // When constructing nsIURI, need to wrap IPv6 address in []. + let hostname = this.hostName.includes(":") + ? `[${this.hostName}]` + : this.hostName; + return `${this.localStoreType}://${auth}${encodeURIComponent(hostname)}`; + } + + get prettyName() { + return this.getUnicharValue("name") || this.constructedPrettyName; + } + + set prettyName(value) { + this.setUnicharValue("name", value); + this.rootFolder.prettyName = value; + } + + /** + * Construct a pretty name from username and hostname. + * + * @param {string} username - The user name. + * @param {string} hostname - The host name. + * @returns {string} + */ + _constructPrettyName(username, hostname) { + let prefix = username ? `${username} on ` : ""; + return `${prefix}${hostname}`; + } + + get constructedPrettyName() { + return this._constructPrettyName(this.username, this.hostName); + } + + get localPath() { + let localPath = this.getFileValue("directory-rel", "directory"); + if (localPath) { + // If the local path has already been set, use it. + return localPath; + } + + // Create the path using protocol info and hostname. + localPath = this.protocolInfo.defaultLocalPath; + if (!localPath.exists()) { + localPath.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + } + + localPath.append(this.hostName); + localPath.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + + this.localPath = localPath; + return localPath; + } + + set localPath(localPath) { + this.setFileValue("directory-rel", "directory", localPath); + } + + get rootFolder() { + if (!this._rootFolder) { + this._rootFolder = MailServices.folderLookup.getOrCreateFolderForURL( + this.serverURI + ); + } + return this._rootFolder; + } + + get rootMsgFolder() { + return this.rootFolder; + } + + get msgStore() { + if (!this._msgStore) { + let contractId = this.getCharValue("storeContractID"); + if (!contractId) { + contractId = "@mozilla.org/msgstore/berkeleystore;1"; + this.setCharValue("storeContractID", contractId); + } + + // After someone starts using the pluggable store, we can no longer + // change the value. + this.setBoolValue("canChangeStoreType", false); + + this._msgStore = Cc[contractId].createInstance(Ci.nsIMsgPluggableStore); + } + return this._msgStore; + } + + get doBiff() { + try { + return this._prefs.getBoolPref("check_new_mail"); + } catch (e) { + return this.protocolInfo.defaultDoBiff; + } + } + + set doBiff(value) { + let biffManager = Cc["@mozilla.org/messenger/biffManager;1"].getService( + Ci.nsIMsgBiffManager + ); + if (value) { + biffManager.addServerBiff(this); + } else { + biffManager.removeServerBiff(this); + } + this._prefs.setBoolPref("check_new_mail", value); + } + + /** + * type, attribute name, pref name + */ + _retentionSettingsPrefs = [ + ["Int", "retainByPreference", "retainBy"], + ["Int", "numHeadersToKeep", "numHdrsToKeep"], + ["Int", "daysToKeepHdrs"], + ["Int", "daysToKeepBodies"], + ["Bool", "cleanupBodiesByDays", "cleanupBodies"], + ["Bool", "applyToFlaggedMessages"], + ]; + + get retentionSettings() { + let settings = Cc[ + "@mozilla.org/msgDatabase/retentionSettings;1" + ].createInstance(Ci.nsIMsgRetentionSettings); + for (let [type, attrName, prefName] of this._retentionSettingsPrefs) { + prefName = prefName || attrName; + settings[attrName] = this[`get${type}Value`](prefName); + } + return settings; + } + + set retentionSettings(settings) { + for (let [type, attrName, prefName] of this._retentionSettingsPrefs) { + prefName = prefName || attrName; + this[`set${type}Value`](prefName, settings[attrName]); + } + } + + get spamSettings() { + if (!this.getCharValue("spamActionTargetAccount")) { + this.setCharValue("spamActionTargetAccount", this.serverURI); + } + if (!this._spamSettings) { + this._spamSettings = Cc[ + "@mozilla.org/messenger/spamsettings;1" + ].createInstance(Ci.nsISpamSettings); + try { + this._spamSettings.initialize(this); + } catch (e) { + console.error(e); + } + } + return this._spamSettings; + } + + get spamFilterPlugin() { + if (!this._spamFilterPlugin) { + this._spamFilterPlugin = Cc[ + "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter" + ].getService(Ci.nsIMsgFilterPlugin); + } + return this._spamFilterPlugin; + } + + get isDeferredTo() { + let account = MailServices.accounts.FindAccountForServer(this); + if (!account) { + return false; + } + return MailServices.accounts.allServers.some( + server => server.getCharValue("deferred_to_account") == account.key + ); + } + + get serverRequiresPasswordForBiff() { + return true; + } + + /** + * type, attribute name, pref name + */ + _downloadSettingsPrefs = [ + ["Int", "ageLimitOfMsgsToDownload", "ageLimit"], + ["Bool", "downloadUnreadOnly"], + ["Bool", "downloadByDate"], + ]; + + get downloadSettings() { + if (!this._downloadSettings) { + this._downloadSettings = Cc[ + "@mozilla.org/msgDatabase/downloadSettings;1" + ].createInstance(Ci.nsIMsgDownloadSettings); + for (let [type, attrName, prefName] of this._downloadSettingsPrefs) { + prefName = prefName || attrName; + this._downloadSettings[attrName] = this[`get${type}Value`](prefName); + } + } + return this._downloadSettings; + } + + set downloadSettings(settings) { + this._downloadSettings = settings; + for (let [type, attrName, prefName] of this._downloadSettingsPrefs) { + prefName = prefName || attrName; + this[`set${type}Value`](prefName, settings[attrName]); + } + } + + get offlineSupportLevel() { + const OFFLINE_SUPPORT_LEVEL_NONE = 0; + const OFFLINE_SUPPORT_LEVEL_UNDEFINED = -1; + let level = this.getIntValue("offline_support_level"); + return level == OFFLINE_SUPPORT_LEVEL_UNDEFINED + ? OFFLINE_SUPPORT_LEVEL_NONE + : level; + } + + set offlineSupportLevel(value) { + this.setIntValue("offline_support_level", value); + } + + get filterScope() { + return Ci.nsMsgSearchScope.offlineMailFilter; + } + + get searchScope() { + return Ci.nsMsgSearchScope.offlineMail; + } + + get passwordPromptRequired() { + if (!this.serverRequiresPasswordForBiff) { + // If the password is not even required for biff we don't need to check + // any further. + return false; + } + if (!this.password) { + // If the password is empty, check to see if it is stored. + this.password = this._getPasswordWithoutUI(); + } + if (this.password) { + return false; + } + return this.authMethod != Ci.nsMsgAuthMethod.OAuth2; + } + + getCharValue(prefName) { + try { + return this._prefs.getCharPref(prefName); + } catch (e) { + return this._defaultPrefs.getCharPref(prefName, ""); + } + } + + setCharValue(prefName, value) { + let defaultValue = this._defaultPrefs.getCharPref(prefName, ""); + if (!value || value == defaultValue) { + this._prefs.clearUserPref(prefName); + } else { + this._prefs.setCharPref(prefName, value); + } + } + + getUnicharValue(prefName) { + try { + return this._prefs.getStringPref(prefName); + } catch (e) { + return this._defaultPrefs.getStringPref(prefName, ""); + } + } + + setUnicharValue(prefName, value) { + let defaultValue = this._defaultPrefs.getStringPref(prefName, ""); + if (!value || value == defaultValue) { + this._prefs.clearUserPref(prefName); + } else { + this._prefs.setStringPref(prefName, value); + } + } + + getIntValue(prefName) { + try { + return this._prefs.getIntPref(prefName); + } catch (e) { + return this._defaultPrefs.getIntPref(prefName, 0); + } + } + + setIntValue(prefName, value) { + let defaultValue = this._defaultPrefs.getIntPref(prefName, value - 1); + if (defaultValue == value) { + this._prefs.clearUserPref(prefName); + } else { + this._prefs.setIntPref(prefName, value); + } + } + + getBoolValue(prefName) { + try { + return this._prefs.getBoolPref(prefName); + } catch (e) { + return this._defaultPrefs.getBoolPref(prefName, false); + } + } + + setBoolValue(prefName, value) { + let defaultValue = this._defaultPrefs.getBoolPref(prefName, !value); + if (defaultValue == value) { + this._prefs.clearUserPref(prefName); + } else { + this._prefs.setBoolPref(prefName, value); + } + } + + getFileValue(relPrefName, absPrefName) { + try { + let file = this._prefs.getComplexValue( + relPrefName, + Ci.nsIRelativeFilePref + ).file; + file.normalize(); + return file; + } catch (e) { + try { + let file = this._prefs.getComplexValue(absPrefName, Ci.nsIFile); + this._prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, { + QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]), + file, + relativeToKey: "ProfD", + }); + return file; + } catch (e) { + return null; + } + } + } + + setFileValue(relPrefName, absPrefName, file) { + this._prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, { + QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]), + file, + relativeToKey: "ProfD", + }); + this._prefs.setComplexValue(absPrefName, Ci.nsIFile, file); + } + + onUserOrHostNameChanged(oldValue, newValue, hostnameChanged) { + migrateServerUris( + this.localStoreType, + hostnameChanged ? oldValue : this.hostName, + hostnameChanged ? this.username : oldValue, + this.hostName, + this.username + ); + this._spamSettings = null; + + // Clear the clientid because the user or host have changed. + this.clientid = ""; + + let atIndex = newValue.indexOf("@"); + if (!this.prettyName || (!hostnameChanged && atIndex != -1)) { + // If new username contains @ then better not update the pretty name. + return; + } + + atIndex = this.prettyName.indexOf("@"); + if ( + !hostnameChanged && + atIndex != -1 && + oldValue == this.prettyName.slice(0, atIndex) + ) { + // If username changed and the pretty name has the old username before @, + // update to the new username. + this.prettyName = newValue + this.prettyName.slice(atIndex); + } else if ( + hostnameChanged && + oldValue == this.prettyName.slice(atIndex + 1) + ) { + // If hostname changed and the pretty name has the old hostname after @, + // update to the new hostname. + this.prettyName = this.prettyName.slice(0, atIndex + 1) + newValue; + } else { + // Set the `name` pref anyway, to make tests happy. + // eslint-disable-next-line no-self-assign + this.prettyName = this.prettyName; + } + } + + /** + * Try to get the password from nsILoginManager. + * + * @returns {string} + */ + _getPasswordWithoutUI() { + let serverURI = this._getServerURI(); + let logins = Services.logins.findLogins(serverURI, "", serverURI); + for (let login of logins) { + if (login.username == this.username) { + return login.password; + } + } + return null; + } + + getPasswordWithUI(promptMessage, promptTitle) { + let password = this._getPasswordWithoutUI(); + if (password) { + this.password = password; + return this.password; + } + let outUsername = {}; + let outPassword = {}; + let ok; + let authPrompt; + try { + // This prompt has a checkbox for saving password. + authPrompt = Cc["@mozilla.org/messenger/msgAuthPrompt;1"].getService( + Ci.nsIAuthPrompt + ); + } catch (e) { + // Often happens in tests. This prompt has no checkbox for saving password. + authPrompt = Services.ww.getNewAuthPrompter(null); + } + if (this.username) { + ok = authPrompt.promptPassword( + promptTitle, + promptMessage, + this.serverURI, + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, + outPassword + ); + } else { + ok = authPrompt.promptUsernameAndPassword( + promptTitle, + promptMessage, + this.serverURI, + Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY, + outUsername, + outPassword + ); + } + if (ok) { + if (outUsername.value) { + this.username = outUsername.value; + } + this.password = outPassword.value; + } else { + throw Components.Exception("Password dialog canceled", Cr.NS_ERROR_ABORT); + } + return this.password; + } + + forgetPassword() { + let serverURI = this._getServerURI(); + let logins = Services.logins.findLogins(serverURI, "", serverURI); + for (let login of logins) { + if (login.username == this.username) { + Services.logins.removeLogin(login); + } + } + this.password = ""; + } + + forgetSessionPassword() { + this.password = ""; + } + + closeCachedConnections() {} + + shutdown() { + this.closeCachedConnections(); + + if (this._filterList) { + this._filterList.logStream = null; + this._filterList = null; + } + if (this._spamSettings) { + this._spamSettings.logStream = null; + this._spamSettings = null; + } + } + + getFilterList(msgWindow) { + if (!this._filterList) { + if (!this.rootFolder.filePath.path) { + // Happens in tests. + return null; + } + let filterFile = this.rootFolder.filePath.clone(); + filterFile.append("msgFilterRules.dat"); + try { + this._filterList = MailServices.filters.OpenFilterList( + filterFile, + this.rootFolder, + msgWindow + ); + } catch (e) { + console.error(e); + const NS_ERROR_FILE_FS_CORRUPTED = 0x80520016; + if (e.result == NS_ERROR_FILE_FS_CORRUPTED && filterFile.exists()) { + // OpenFilterList will create a new one next time. + filterFile.renameTo(filterFile.parent, "msgFilterRules.dat.orig"); + } + } + } + return this._filterList; + } + + setFilterList(value) { + this._filterList = value; + } + + getEditableFilterList(msgWindow) { + if (!this._editableFilterList) { + return this.getFilterList(msgWindow); + } + return this._editableFilterList; + } + + setEditableFilterList(value) { + this._editableFilterList = value; + } + + setDefaultLocalPath(value) { + this.protocolInfo.setDefaultLocalPath(value); + } + + getNewMessages(folder, msgWindow, urlListener) { + folder.getNewMessages(msgWindow, urlListener); + } + + writeToFolderCache(folderCache) { + this.rootFolder.writeToFolderCache(folderCache, true); + } + + clearAllValues() { + for (let prefName of this._prefs.getChildList("")) { + this._prefs.clearUserPref(prefName); + } + } + + removeFiles() { + if (this.getCharValue("deferred_to_account") || this.isDeferredTo) { + throw Components.Exception( + "Should not remove files for a deferred account", + Cr.NS_ERROR_FAILURE + ); + } + this.localPath.remove(true); + } + + getMsgFolderFromURI(folder, uri) { + try { + return this.rootMsgFolder.getChildWithURI(uri, true, true) || folder; + } catch (e) { + return folder; + } + } + + isNewHdrDuplicate(newHdr) { + // If the message has been partially downloaded, the message should not + // be considered a duplicated message. See bug 714090. + if (newHdr.flags & Ci.nsMsgMessageFlags.Partial) { + return false; + } + + if (!newHdr.subject || !newHdr.messageId) { + return false; + } + + let key = `${newHdr.messageId}${newHdr.subject}`; + if (this._knownHdrMap.get(key)) { + return true; + } + + this._knownHdrMap.set(key, ++this._hdrIndex); + + const MAX_SIZE = 500; + if (this._knownHdrMap.size > MAX_SIZE) { + // Release the oldest half of downloaded hdrs. + for (let [k, v] of this._knownHdrMap) { + if (v < this._hdrIndex - MAX_SIZE / 2) { + this._knownHdrMap.delete(k); + } else if (this._knownHdrMap.size <= MAX_SIZE / 2) { + break; + } + } + } + return false; + } + + equals(server) { + return this.key == server.key; + } + + _configureTemporaryReturnReceiptsFilter(filterList) { + let identity = MailServices.accounts.getFirstIdentityForServer(this); + if (!identity) { + return; + } + let incorp = Ci.nsIMsgMdnGenerator.eIncorporateInbox; + if (identity.getBoolAttribute("use_custom_prefs")) { + incorp = this.getIntValue("incorporate_return_receipt"); + } else { + incorp = Services.prefs.getIntPref("mail.incorporate.return_receipt"); + } + + let enable = incorp == Ci.nsIMsgMdnGenerator.eIncorporateSent; + + const FILTER_NAME = "mozilla-temporary-internal-MDN-receipt-filter"; + let filter = filterList.getFilterNamed(FILTER_NAME); + + if (filter) { + filter.enabled = enable; + return; + } else if (!enable || !identity.fccFolder) { + return; + } + + filter = filterList.createFilter(FILTER_NAME); + if (!filter) { + return; + } + + filter.enabled = true; + filter.temporary = true; + + let term = filter.createTerm(); + let value = term.value; + value.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1; + value.str = "multipart/report"; + term.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1; + term.op = Ci.nsMsgSearchOp.Contains; + term.booleanAnd = true; + term.arbitraryHeader = "Content-Type"; + term.value = value; + filter.appendTerm(term); + + term = filter.createTerm(); + value = term.value; + value.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1; + value.str = "disposition-notification"; + term.attrib = Ci.nsMsgSearchAttrib.OtherHeader + 1; + term.op = Ci.nsMsgSearchOp.Contains; + term.booleanAnd = true; + term.arbitraryHeader = "Content-Type"; + term.value = value; + filter.appendTerm(term); + + let action = filter.createAction(); + action.type = Ci.nsMsgFilterAction.MoveToFolder; + action.targetFolderUri = identity.fccFolder; + filter.appendAction(action); + filterList.insertFilterAt(0, filter); + } + + _configureTemporaryServerSpamFilters(filterList) { + let spamSettings = this.spamSettings; + if (!spamSettings.useServerFilter) { + return; + } + let serverFilterName = spamSettings.serverFilterName; + let serverFilterTrustFlags = spamSettings.serverFilterTrustFlags; + if (!serverFilterName || !serverFilterName) { + return; + } + + // Check if filters have been setup already. + let yesFilterName = `${serverFilterName}Yes`; + let noFilterName = `${serverFilterName}No`; + let filter = filterList.getFilterNamed(yesFilterName); + if (!filter) { + filter = filterList.getFilterNamed(noFilterName); + } + if (filter) { + return; + } + + let serverFilterList = MailServices.filters.OpenFilterList( + spamSettings.serverFilterFile, + null, + null + ); + filter = serverFilterList.getFilterNamed(yesFilterName); + if (filter && serverFilterTrustFlags & Ci.nsISpamSettings.TRUST_POSITIVES) { + filter.temporary = true; + // Check if we're supposed to move junk mail to junk folder; if so, add + // filter action to do so. + let searchTerms = filter.searchTerms; + if (searchTerms.length) { + searchTerms[0].beginsGrouping = true; + searchTerms.at(-1).endsGrouping = true; + } + + // Create a new term, checking if the user set junk status. The term will + // search for junkscoreorigin != "user". + let term = filter.createTerm(); + term.attrib = Ci.nsMsgSearchAttrib.JunkScoreOrigin; + term.op = Ci.nsMsgSearchOp.Isnt; + term.booleanAnd = true; + let value = term.value; + value.attrib = Ci.nsMsgSearchAttrib.JunkScoreOrigin; + value.str = "user"; + term.value = value; + filter.appendTerm(term); + + if (spamSettings.moveOnSpam) { + let spamFolderURI = spamSettings.spamFolderURI; + if (spamFolderURI) { + let action = filter.createAction(); + action.type = Ci.nsMsgFilterAction.MoveToFolder; + action.targetFolderUri = spamFolderURI; + filter.appendAction(action); + } + } + + if (spamSettings.markAsReadOnSpam) { + let action = filter.createAction(); + action.type = Ci.nsMsgFilterAction.MarkRead; + filter.appendAction(action); + } + filterList.insertFilterAt(0, filter); + } + + filter = serverFilterList.getFilterNamed(noFilterName); + if (filter && serverFilterTrustFlags & Ci.nsISpamSettings.TRUST_NEGATIVES) { + filter.temporary = true; + filterList.insertFilterAt(0, filter); + } + } + + configureTemporaryFilters(filterList) { + this._configureTemporaryReturnReceiptsFilter(filterList); + this._configureTemporaryServerSpamFilters(filterList); + } + + clearTemporaryReturnReceiptsFilter() { + if (!this._filterList) { + return; + } + let filter = this._filterList.getFilterNamed( + "mozilla-temporary-internal-MDN-receipt-filter" + ); + if (filter) { + this._filterList.removeFilter(filter); + } + } + + getForcePropertyEmpty(name) { + return this.getCharValue(`${name}.empty`) == "true"; + } + + setForcePropertyEmpty(name, value) { + return this.setCharValue(`${name}.empty`, value ? "true" : ""); + } + + performExpand(msgWindow) {} + + get wrappedJSObject() { + return this; + } + + _passwordPromise = null; + + /** + * Show a password prompt. If a prompt is currently shown, just wait for it. + * + * @param {string} message - The text inside the prompt. + * @param {string} title - The title of the prompt. + */ + async getPasswordWithUIAsync(message, title) { + if (this._passwordPromise) { + await this._passwordPromise; + return this.password; + } + let deferred = {}; + this._passwordPromise = new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + try { + this.getPasswordWithUI(message, title); + } catch (e) { + deferred.reject(e); + throw e; + } finally { + this._passwordPromise = null; + } + deferred.resolve(); + return this.password; + } +} diff --git a/comm/mailnews/base/src/MsgKeySet.jsm b/comm/mailnews/base/src/MsgKeySet.jsm new file mode 100644 index 0000000000..bbbd580ba9 --- /dev/null +++ b/comm/mailnews/base/src/MsgKeySet.jsm @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const EXPORTED_SYMBOLS = ["MsgKeySet"]; + +/** + * A structure to represent a set of articles. This is usually for lines from + * the newsrc, which have article lists like + * + * 1-29627,29635,29658,32861-32863 + * + * so the data has these properties: + * + * - strictly increasing + * - large subsequences of monotonically increasing ranges + * - gaps in the set are usually small, but not always + * - consecutive ranges tend to be large + */ +class MsgKeySet { + /** + * @param {string} [str] - The raw string to represent a set of articles. + */ + constructor(str) { + // An array of tuples, each tuple contains the start and end value of a sub + // range. + // @type {Array<[number, number]>} + this._ranges = str + ? str.split(",").map(part => { + let [start, end] = part.split("-"); + return [+start, +end || +start]; + }) + : []; + } + + /** + * Add a value to the set. + * + * @param {number} value - The value to add. + */ + add(value) { + this.addRange(value, value); + } + + /** + * Add a range to the set. + * + * @param {number} low - The smallest value of the range. + * @param {number} high - The largest value of the range. + */ + addRange(low, high) { + let index = 0; + for (let [start] of this._ranges) { + if (start > low) { + break; + } + index++; + } + this._ranges.splice(index, 0, [low, high]); + this._rebuild(); + } + + /** + * Check if a value is in the set. + * + * @param {number} value - The value to check. + * @returns {boolean} + */ + has(value) { + return this._ranges.some(([start, end]) => + end ? start <= value && value <= end : start == value + ); + } + + /** + * Get the last range that is in the input range, but not in the key set. + * + * @param {number} low - The smallest value of the input range. + * @param {number} high - The largest value of the input range. + * @returns {number[]} - Array of lenght two with [low, high]. + */ + getLastMissingRange(low, high) { + let length = this._ranges.length; + for (let i = length - 1; i >= 0; i--) { + let [start, end] = this._ranges[i]; + if (end < high) { + return [Math.max(low, end + 1), high]; + } else if (low < start && high > start) { + high = start - 1; + } else { + return []; + } + } + return [low, high]; + } + + /** + * Get the string representation of the key set. + * + * @returns {string} + */ + toString() { + return this._ranges + .map(([start, end]) => (start == end ? start : `${start}-${end}`)) + .join(","); + } + + /** + * Sub ranges may become overlapped after some operations. This method merges + * them if needed. + */ + _rebuild() { + if (this._ranges.length < 2) { + return; + } + let newRanges = []; + let [cursorStart, cursorEnd] = this._ranges[0]; + for (let [start, end] of this._ranges.slice(1)) { + if (cursorEnd < start - 1) { + // No overlap between the two ranges. + newRanges.push([cursorStart, cursorEnd]); + cursorStart = start; + cursorEnd = end; + } else { + // Overlapped, merge them. + cursorEnd = end; + } + } + newRanges.push([cursorStart, cursorEnd]); + this._ranges = newRanges; + } +} diff --git a/comm/mailnews/base/src/MsgProtocolInfo.sys.mjs b/comm/mailnews/base/src/MsgProtocolInfo.sys.mjs new file mode 100644 index 0000000000..7c9088e12e --- /dev/null +++ b/comm/mailnews/base/src/MsgProtocolInfo.sys.mjs @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * @see {nsIMsgProtocolInfo} + */ +export class MsgProtocolInfo { + get defaultLocalPath() { + let file = this._getFileValue(this.RELATIVE_PREF, this.ABSOLUTE_PREF); + if (!file) { + file = Services.dirsvc.get(this.DIR_SERVICE_PROP, Ci.nsIFile); + this._setFileValue(this.RELATIVE_PREF, this.ABSOLUTE_PREF, file); + } + if (!file.exists()) { + file.createUnique(Ci.nsIFile.DIRECTORY_TYPE, 0o775); + } + file.normalize(); + return file; + } + + set defaultLocalPath(value) { + this._setFileValue(this.RELATIVE_PREF, this.ABSOLUTE_PREF, value); + } + + _getFileValue(relPrefName, absPrefName) { + try { + return Services.prefs.getComplexValue(relPrefName, Ci.nsIRelativeFilePref) + .file; + } catch (e) { + try { + let file = Services.prefs.getComplexValue(absPrefName, Ci.nsIFile); + Services.prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, { + QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]), + file, + relativeToKey: "ProfD", + }); + return file; + } catch (e) { + return null; + } + } + } + + _setFileValue(relPrefName, absPrefName, file) { + Services.prefs.setComplexValue(relPrefName, Ci.nsIRelativeFilePref, { + QueryInterface: ChromeUtils.generateQI(["nsIRelativeFilePref"]), + file, + relativeToKey: "ProfD", + }); + Services.prefs.setComplexValue(absPrefName, Ci.nsIFile, file); + } +} diff --git a/comm/mailnews/base/src/OAuth2.jsm b/comm/mailnews/base/src/OAuth2.jsm new file mode 100644 index 0000000000..c5148d41a7 --- /dev/null +++ b/comm/mailnews/base/src/OAuth2.jsm @@ -0,0 +1,364 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Provides OAuth 2.0 authentication. + * + * @see RFC 6749 + */ +var EXPORTED_SYMBOLS = ["OAuth2"]; + +var { CryptoUtils } = ChromeUtils.importESModule( + "resource://services-crypto/utils.sys.mjs" +); + +// Only allow one connecting window per endpoint. +var gConnecting = {}; + +/** + * Constructor for the OAuth2 object. + * + * @class + * @param {?string} scope - The scope as specified by RFC 6749 Section 3.3. + * Will not be included in the requests if falsy. + * @param {object} issuerDetails + * @param {string} issuerDetails.authorizationEndpoint - The authorization + * endpoint as defined by RFC 6749 Section 3.1. + * @param {string} issuerDetails.clientId - The client_id as specified by RFC + * 6749 Section 2.3.1. + * @param {string} issuerDetails.clientSecret - The client_secret as specified + * in RFC 6749 section 2.3.1. Will not be included in the requests if null. + * @param {boolean} issuerDetails.usePKCE - Whether to use PKCE as specified + * in RFC 7636 during the oauth registration process + * @param {string} issuerDetails.redirectionEndpoint - The redirect_uri as + * specified by RFC 6749 section 3.1.2. + * @param {string} issuerDetails.tokenEndpoint - The token endpoint as defined + * by RFC 6749 Section 3.2. + */ +function OAuth2(scope, issuerDetails) { + this.scope = scope; + this.authorizationEndpoint = issuerDetails.authorizationEndpoint; + this.clientId = issuerDetails.clientId; + this.consumerSecret = issuerDetails.clientSecret || null; + this.usePKCE = issuerDetails.usePKCE; + this.redirectionEndpoint = + issuerDetails.redirectionEndpoint || "http://localhost"; + this.tokenEndpoint = issuerDetails.tokenEndpoint; + + this.extraAuthParams = []; + + this.log = console.createInstance({ + prefix: "mailnews.oauth", + maxLogLevel: "Warn", + maxLogLevelPref: "mailnews.oauth.loglevel", + }); +} + +OAuth2.prototype = { + clientId: null, + consumerSecret: null, + requestWindowURI: "chrome://messenger/content/browserRequest.xhtml", + requestWindowFeatures: "chrome,centerscreen,width=980,height=750", + requestWindowTitle: "", + scope: null, + usePKCE: false, + codeChallenge: null, + + accessToken: null, + refreshToken: null, + tokenExpires: 0, + + connect(aSuccess, aFailure, aWithUI, aRefresh) { + this.connectSuccessCallback = aSuccess; + this.connectFailureCallback = aFailure; + + if (this.accessToken && !this.tokenExpired && !aRefresh) { + aSuccess(); + } else if (this.refreshToken) { + this.requestAccessToken(this.refreshToken, true); + } else { + if (!aWithUI) { + aFailure('{ "error": "auth_noui" }'); + return; + } + if (gConnecting[this.authorizationEndpoint]) { + aFailure("Window already open"); + return; + } + this.requestAuthorization(); + } + }, + + /** + * True if the token has expired, or will expire within the grace time. + */ + get tokenExpired() { + // 30 seconds to allow for network inefficiency, clock drift, etc. + const OAUTH_GRACE_TIME_MS = 30 * 1000; + return this.tokenExpires - OAUTH_GRACE_TIME_MS < Date.now(); + }, + + requestAuthorization() { + let params = new URLSearchParams({ + response_type: "code", + client_id: this.clientId, + redirect_uri: this.redirectionEndpoint, + }); + + // The scope is optional. + if (this.scope) { + params.append("scope", this.scope); + } + + // See rfc7636 + if (this.usePKCE) { + // Convert base64 to base64url (rfc4648#section-5) + const to_b64url = b => + b.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, ""); + + params.append("code_challenge_method", "S256"); + + // rfc7636#section-4.1 + // code_verifier = high-entropy cryptographic random STRING ... with a minimum + // length of 43 characters and a maximum length of 128 characters. + const code_verifier = to_b64url( + btoa(CryptoUtils.generateRandomBytesLegacy(64)) + ); + this.codeVerifier = code_verifier; + + // rfc7636#section-4.2 + // code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) + const code_challenge = to_b64url(CryptoUtils.sha256Base64(code_verifier)); + params.append("code_challenge", code_challenge); + } + + for (let [name, value] of this.extraAuthParams) { + params.append(name, value); + } + + let authEndpointURI = this.authorizationEndpoint + "?" + params.toString(); + this.log.info( + "Interacting with the resource owner to obtain an authorization grant " + + "from the authorization endpoint: " + + authEndpointURI + ); + + this._browserRequest = { + account: this, + url: authEndpointURI, + _active: true, + iconURI: "", + cancelled() { + if (!this._active) { + return; + } + + this.account.finishAuthorizationRequest(); + this.account.onAuthorizationFailed( + Cr.NS_ERROR_ABORT, + '{ "error": "cancelled"}' + ); + }, + + loaded(aWindow, aWebProgress) { + if (!this._active) { + return; + } + + this._listener = { + window: aWindow, + webProgress: aWebProgress, + _parent: this.account, + + QueryInterface: ChromeUtils.generateQI([ + "nsIWebProgressListener", + "nsISupportsWeakReference", + ]), + + _cleanUp() { + this.webProgress.removeProgressListener(this); + this.window.close(); + delete this.window; + }, + + _checkForRedirect(url) { + if (!url.startsWith(this._parent.redirectionEndpoint)) { + return; + } + + this._parent.finishAuthorizationRequest(); + this._parent.onAuthorizationReceived(url); + }, + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) { + const wpl = Ci.nsIWebProgressListener; + if (aStateFlags & (wpl.STATE_START | wpl.STATE_IS_NETWORK)) { + let channel = aRequest.QueryInterface(Ci.nsIChannel); + this._checkForRedirect(channel.URI.spec); + } + }, + onLocationChange(aWebProgress, aRequest, aLocation) { + this._checkForRedirect(aLocation.spec); + }, + onProgressChange() {}, + onStatusChange() {}, + onSecurityChange() {}, + }; + aWebProgress.addProgressListener( + this._listener, + Ci.nsIWebProgress.NOTIFY_ALL + ); + aWindow.document.title = this.account.requestWindowTitle; + }, + }; + + const windowPrivacy = Services.prefs.getBoolPref( + "mailnews.oauth.usePrivateBrowser", + false + ) + ? "private" + : "non-private"; + const windowFeatures = `${this.requestWindowFeatures},${windowPrivacy}`; + + this.wrappedJSObject = this._browserRequest; + gConnecting[this.authorizationEndpoint] = true; + Services.ww.openWindow( + null, + this.requestWindowURI, + null, + windowFeatures, + this + ); + }, + finishAuthorizationRequest() { + gConnecting[this.authorizationEndpoint] = false; + if (!("_browserRequest" in this)) { + return; + } + + this._browserRequest._active = false; + if ("_listener" in this._browserRequest) { + this._browserRequest._listener._cleanUp(); + } + delete this._browserRequest; + }, + + /** + * @param {string} aURL - Redirection URI with additional parameters. + */ + onAuthorizationReceived(aURL) { + this.log.info("OAuth2 authorization response received: url=" + aURL); + const url = new URL(aURL); + if (url.searchParams.has("code")) { + // @see RFC 6749 section 4.1.2: Authorization Response + this.requestAccessToken(url.searchParams.get("code"), false); + } else { + // @see RFC 6749 section 4.1.2.1: Error Response + if (url.searchParams.has("error")) { + let error = url.searchParams.get("error"); + let errorDescription = url.searchParams.get("error_description") || ""; + if (error == "invalid_scope") { + errorDescription += ` Invalid scope: ${this.scope}.`; + } + if (url.searchParams.has("error_uri")) { + errorDescription += ` See ${url.searchParams.get("error_uri")}.`; + } + this.log.error(`Authorization error [${error}]: ${errorDescription}`); + } + this.onAuthorizationFailed(null, aURL); + } + }, + + onAuthorizationFailed(aError, aData) { + this.connectFailureCallback(aData); + }, + + /** + * Request a new access token, or refresh an existing one. + * + * @param {string} aCode - The token issued to the client. + * @param {boolean} aRefresh - Whether it's a refresh of a token or not. + */ + requestAccessToken(aCode, aRefresh) { + // @see RFC 6749 section 4.1.3. Access Token Request + // @see RFC 6749 section 6. Refreshing an Access Token + + let data = new URLSearchParams(); + data.append("client_id", this.clientId); + if (this.consumerSecret !== null) { + // Section 2.3.1. of RFC 6749 states that empty secrets MAY be omitted + // by the client. This OAuth implementation delegates this decision to + // the caller: If the secret is null, it will be omitted. + data.append("client_secret", this.consumerSecret); + } + + if (aRefresh) { + this.log.info( + `Making a refresh request to the token endpoint: ${this.tokenEndpoint}` + ); + data.append("grant_type", "refresh_token"); + data.append("refresh_token", aCode); + } else { + this.log.info( + `Making access token request to the token endpoint: ${this.tokenEndpoint}` + ); + data.append("grant_type", "authorization_code"); + data.append("code", aCode); + data.append("redirect_uri", this.redirectionEndpoint); + if (this.usePKCE) { + data.append("code_verifier", this.codeVerifier); + } + } + + fetch(this.tokenEndpoint, { + method: "POST", + cache: "no-cache", + body: data, + }) + .then(response => response.json()) + .then(result => { + let resultStr = JSON.stringify(result, null, 2); + if ("error" in result) { + // RFC 6749 section 5.2. Error Response + let err = result.error; + if ("error_description" in result) { + err += "; " + result.error_description; + } + if ("error_uri" in result) { + err += "; " + result.error_uri; + } + this.log.warn(`Error response from the authorization server: ${err}`); + this.log.info(`Error response details: ${resultStr}`); + + // Typically in production this would be {"error": "invalid_grant"}. + // That is, the token expired or was revoked (user changed password?). + // Reset the tokens we have and call success so that the auth flow + // will be re-triggered. + this.accessToken = null; + this.refreshToken = null; + this.connectSuccessCallback(); + return; + } + + // RFC 6749 section 5.1. Successful Response + this.log.info( + `Successful response from the authorization server: ${resultStr}` + ); + this.accessToken = result.access_token; + if ("refresh_token" in result) { + this.refreshToken = result.refresh_token; + } + if ("expires_in" in result) { + this.tokenExpires = new Date().getTime() + result.expires_in * 1000; + } else { + this.tokenExpires = Number.MAX_VALUE; + } + this.connectSuccessCallback(); + }) + .catch(err => { + this.log.info(`Connection to authorization server failed: ${err}`); + this.connectFailureCallback(err); + }); + }, +}; diff --git a/comm/mailnews/base/src/OAuth2Module.jsm b/comm/mailnews/base/src/OAuth2Module.jsm new file mode 100644 index 0000000000..79826779c4 --- /dev/null +++ b/comm/mailnews/base/src/OAuth2Module.jsm @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["OAuth2Module"]; + +var { OAuth2 } = ChromeUtils.import("resource:///modules/OAuth2.jsm"); +var { OAuth2Providers } = ChromeUtils.import( + "resource:///modules/OAuth2Providers.jsm" +); + +/** + * OAuth2Module is the glue layer that gives XPCOM access to an OAuth2 + * bearer token it can use to authenticate in SASL steps. + * It also takes care of persising the refreshToken for later usage. + * + * @implements {msgIOAuth2Module} + */ +function OAuth2Module() {} +OAuth2Module.prototype = { + QueryInterface: ChromeUtils.generateQI(["msgIOAuth2Module"]), + + initFromSmtp(aServer) { + return this._initPrefs( + "mail.smtpserver." + aServer.key + ".", + aServer.username, + aServer.hostname + ); + }, + initFromMail(aServer) { + return this._initPrefs( + "mail.server." + aServer.key + ".", + aServer.username, + aServer.hostName + ); + }, + initFromABDirectory(aDirectory, aHostname) { + this._initPrefs( + aDirectory.dirPrefId + ".", + aDirectory.getStringValue("carddav.username", "") || aDirectory.UID, + aHostname + ); + }, + _initPrefs(root, aUsername, aHostname) { + let issuer = Services.prefs.getStringPref(root + "oauth2.issuer", null); + let scope = Services.prefs.getStringPref(root + "oauth2.scope", null); + + let details = OAuth2Providers.getHostnameDetails(aHostname); + if ( + details && + (details[0] != issuer || + !scope?.split(" ").every(s => details[1].split(" ").includes(s))) + ) { + // Found in the list of hardcoded providers. Use the hardcoded values. + // But only if what we had wasn't a narrower scope of current + // defaults. Updating scope would cause re-authorization. + [issuer, scope] = details; + // Store them for the future, can be useful once we support + // dynamic registration. + Services.prefs.setStringPref(root + "oauth2.issuer", issuer); + Services.prefs.setStringPref(root + "oauth2.scope", scope); + } + if (!issuer || !scope) { + // We need these properties for OAuth2 support. + return false; + } + + // Find the app key we need for the OAuth2 string. Eventually, this should + // be using dynamic client registration, but there are no current + // implementations that we can test this with. + const issuerDetails = OAuth2Providers.getIssuerDetails(issuer); + if (!issuerDetails.clientId) { + return false; + } + + // Username is needed to generate the XOAUTH2 string. + this._username = aUsername; + // loginOrigin is needed to save the refresh token in the password manager. + this._loginOrigin = "oauth://" + issuer; + // We use the scope to indicate realm when storing in the password manager. + this._scope = scope; + + // Define the OAuth property and store it. + this._oauth = new OAuth2(scope, issuerDetails); + + // Try hinting the username... + this._oauth.extraAuthParams = [["login_hint", aUsername]]; + + // Set the window title to something more useful than "Unnamed" + this._oauth.requestWindowTitle = Services.strings + .createBundle("chrome://messenger/locale/messenger.properties") + .formatStringFromName("oauth2WindowTitle", [aUsername, aHostname]); + + // This stores the refresh token in the login manager. + Object.defineProperty(this._oauth, "refreshToken", { + get: () => this.refreshToken, + set: token => { + this.refreshToken = token; + }, + }); + + return true; + }, + + get refreshToken() { + for (let login of Services.logins.findLogins(this._loginOrigin, null, "")) { + if ( + login.username == this._username && + (login.httpRealm == this._scope || + login.httpRealm.split(" ").includes(this._scope)) + ) { + return login.password; + } + } + return ""; + }, + set refreshToken(token) { + // Check if we already have a login with this username, and modify the + // password on that, if we do. + let logins = Services.logins.findLogins( + this._loginOrigin, + null, + this._scope + ); + for (let login of logins) { + if (login.username == this._username) { + if (token) { + if (token != login.password) { + let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance( + Ci.nsIWritablePropertyBag + ); + propBag.setProperty("password", token); + Services.logins.modifyLogin(login, propBag); + } + } else { + Services.logins.removeLogin(login); + } + return; + } + } + + // Unless the token is null, we need to create and fill in a new login + if (token) { + let login = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance( + Ci.nsILoginInfo + ); + login.init( + this._loginOrigin, + null, + this._scope, + this._username, + token, + "", + "" + ); + Services.logins.addLogin(login); + } + }, + + connect(aWithUI, aListener) { + let oauth = this._oauth; + let promptlistener = { + onPromptStartAsync(callback) { + this.onPromptAuthAvailable(callback); + }, + + onPromptAuthAvailable: callback => { + oauth.connect( + () => { + aListener.onSuccess( + btoa( + `user=${this._username}\x01auth=Bearer ${oauth.accessToken}\x01\x01` + ) + ); + if (callback) { + callback.onAuthResult(true); + } + }, + () => { + aListener.onFailure(Cr.NS_ERROR_ABORT); + if (callback) { + callback.onAuthResult(false); + } + }, + aWithUI, + false + ); + }, + onPromptCanceled() { + aListener.onFailure(Cr.NS_ERROR_ABORT); + }, + onPromptStart() {}, + }; + + let asyncprompter = Cc[ + "@mozilla.org/messenger/msgAsyncPrompter;1" + ].getService(Ci.nsIMsgAsyncPrompter); + let promptkey = this._loginOrigin + "/" + this._username; + asyncprompter.queueAsyncAuthPrompt(promptkey, false, promptlistener); + }, +}; diff --git a/comm/mailnews/base/src/OAuth2Providers.jsm b/comm/mailnews/base/src/OAuth2Providers.jsm new file mode 100644 index 0000000000..86300b2ead --- /dev/null +++ b/comm/mailnews/base/src/OAuth2Providers.jsm @@ -0,0 +1,259 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Details of supported OAuth2 Providers. + */ +var EXPORTED_SYMBOLS = ["OAuth2Providers"]; + +// When we add a Google mail account, ask for address book and calendar scopes +// as well. Then we can add an address book or calendar without asking again. +// +// Don't ask for all the scopes when adding an address book or calendar +// independently of the mail set-up process. If a mail account already exists, +// we already have a token, and if it doesn't the user is likely to be setting +// up an address book/calendar without wanting mail. +const GOOGLE_SCOPES = + "https://mail.google.com/ https://www.googleapis.com/auth/carddav https://www.googleapis.com/auth/calendar"; +const FASTMAIL_SCOPES = + "https://www.fastmail.com/dev/protocol-imap https://www.fastmail.com/dev/protocol-pop https://www.fastmail.com/dev/protocol-smtp https://www.fastmail.com/dev/protocol-carddav https://www.fastmail.com/dev/protocol-caldav"; +const COMCAST_SCOPES = "https://email.comcast.net/ profile openid"; + +/** + * Map of hostnames to [issuer, scope]. + */ +var kHostnames = new Map([ + ["imap.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]], + ["smtp.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]], + ["pop.googlemail.com", ["accounts.google.com", GOOGLE_SCOPES]], + ["imap.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]], + ["smtp.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]], + ["pop.gmail.com", ["accounts.google.com", GOOGLE_SCOPES]], + [ + "www.googleapis.com", + ["accounts.google.com", "https://www.googleapis.com/auth/carddav"], + ], + + ["imap.mail.ru", ["o2.mail.ru", "mail.imap"]], + ["smtp.mail.ru", ["o2.mail.ru", "mail.imap"]], + + ["imap.yandex.com", ["oauth.yandex.com", "mail:imap_full"]], + ["smtp.yandex.com", ["oauth.yandex.com", "mail:smtp"]], + + ["imap.mail.yahoo.com", ["login.yahoo.com", "mail-w"]], + ["pop.mail.yahoo.com", ["login.yahoo.com", "mail-w"]], + ["smtp.mail.yahoo.com", ["login.yahoo.com", "mail-w"]], + + ["imap.aol.com", ["login.aol.com", "mail-w"]], + ["pop.aol.com", ["login.aol.com", "mail-w"]], + ["smtp.aol.com", ["login.aol.com", "mail-w"]], + + [ + "outlook.office365.com", + [ + "login.microsoftonline.com", + "https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access", + ], + ], + [ + "smtp.office365.com", + [ + "login.microsoftonline.com", + "https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access", + ], + ], + + ["imap.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]], + ["pop.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]], + ["smtp.fastmail.com", ["www.fastmail.com", FASTMAIL_SCOPES]], + [ + "carddav.fastmail.com", + ["www.fastmail.com", "https://www.fastmail.com/dev/protocol-carddav"], + ], + + ["imap.comcast.net", ["comcast.net", COMCAST_SCOPES]], + ["pop.comcast.net", ["comcast.net", COMCAST_SCOPES]], + ["smtp.comcast.net", ["comcast.net", COMCAST_SCOPES]], + + // For testing purposes. + ["mochi.test", ["mochi.test", "test_scope"]], +]); + +/** + * Map of issuers to clientId, clientSecret, authorizationEndpoint, tokenEndpoint, + * and usePKCE (RFC7636). + * Issuer is a unique string for the organization that a Thunderbird account + * was registered at. + * + * For the moment these details are hard-coded, since dynamic client + * registration is not yet supported. Don't copy these values for your + * own application - register one for yourself! This code (and possibly even the + * registration itself) will disappear when this is switched to dynamic + * client registration. + */ +var kIssuers = new Map([ + [ + "accounts.google.com", + { + clientId: + "406964657835-aq8lmia8j95dhl1a2bvharmfk3t1hgqj.apps.googleusercontent.com", + clientSecret: "kSmqreRr0qwBWJgbf5Y-PjSU", + authorizationEndpoint: "https://accounts.google.com/o/oauth2/auth", + tokenEndpoint: "https://www.googleapis.com/oauth2/v3/token", + }, + ], + [ + "o2.mail.ru", + { + clientId: "thunderbird", + clientSecret: "I0dCAXrcaNFujaaY", + authorizationEndpoint: "https://o2.mail.ru/login", + tokenEndpoint: "https://o2.mail.ru/token", + }, + ], + [ + "oauth.yandex.com", + { + clientId: "2a00bba7374047a6ab79666485ffce31", + clientSecret: "3ded85b4ec574c2187a55dc49d361280", + authorizationEndpoint: "https://oauth.yandex.com/authorize", + tokenEndpoint: "https://oauth.yandex.com/token", + }, + ], + [ + "login.yahoo.com", + { + clientId: + "dj0yJmk9NUtCTWFMNVpTaVJmJmQ9WVdrOVJ6UjVTa2xJTXpRbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD0yYw--", + clientSecret: "f2de6a30ae123cdbc258c15e0812799010d589cc", + authorizationEndpoint: "https://api.login.yahoo.com/oauth2/request_auth", + tokenEndpoint: "https://api.login.yahoo.com/oauth2/get_token", + }, + ], + [ + "login.aol.com", + { + clientId: + "dj0yJmk9OXRHc1FqZHRQYzVvJmQ9WVdrOU1UQnJOR0pvTjJrbWNHbzlNQS0tJnM9Y29uc3VtZXJzZWNyZXQmeD02NQ--", + clientSecret: "79c1c11991d148ddd02a919000d69879942fc278", + authorizationEndpoint: "https://api.login.aol.com/oauth2/request_auth", + tokenEndpoint: "https://api.login.aol.com/oauth2/get_token", + }, + ], + + [ + "login.microsoftonline.com", + { + clientId: "9e5f94bc-e8a4-4e73-b8be-63364c29d753", // Application (client) ID + // https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints + authorizationEndpoint: + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", + tokenEndpoint: + "https://login.microsoftonline.com/common/oauth2/v2.0/token", + redirectionEndpoint: "https://localhost", + }, + ], + + [ + "www.fastmail.com", + { + clientId: "35f141ae", + authorizationEndpoint: "https://api.fastmail.com/oauth/authorize", + tokenEndpoint: "https://api.fastmail.com/oauth/refresh", + usePKCE: true, + }, + ], + + [ + "comcast.net", + { + clientId: "thunderbird-oauth", + clientSecret: "fc5d0a314549bb3d059e0cec751fa4bd40a9cc7b", + authorizationEndpoint: "https://oauth.xfinity.com/oauth/authorize", + tokenEndpoint: "https://oauth.xfinity.com/oauth/token", + usePKCE: true, + }, + ], + + // For testing purposes. + [ + "mochi.test", + { + clientId: "test_client_id", + clientSecret: "test_secret", + authorizationEndpoint: + "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/redirect_auto.sjs", + tokenEndpoint: + "http://mochi.test:8888/browser/comm/mail/components/addrbook/test/browser/data/token.sjs", + // I don't know why, but tests refuse to work with a plain HTTP endpoint + // (the request is redirected to HTTPS, which we're not listening to). + // Just use an HTTPS endpoint. + redirectionEndpoint: "https://localhost", + }, + ], +]); + +/** + * OAuth2Providers: Methods to lookup OAuth2 parameters for supported OAuth2 + * providers. + */ +var OAuth2Providers = { + /** + * Map a hostname to the relevant issuer and scope. + * + * @param {string} hostname - The hostname of the server. For example + * "imap.googlemail.com". + * + * @returns {Array} An array containing [issuer, scope] for the hostname, or + * undefined if not found. + * - issuer is a string representing the organization + * - scope is an OAuth2 parameter describing the required access level + */ + getHostnameDetails(hostname) { + // During CardDAV SRV autodiscovery, rfc6764#section-6 says: + // + // * The client will need to make authenticated HTTP requests to + // the service. Typically, a "user identifier" is required for + // some form of user/password authentication. When a user + // identifier is required, clients MUST first use the "mailbox" + // + // However macOS Contacts does not do this and just uses the "localpart" + // instead. To work around this bug, during SRV autodiscovery Fastmail + // returns SRV records of the form '0 1 443 d[0-9]+.carddav.fastmail.com.' + // which encodes the internal domainid of the queried SRV domain in the + // sub-domain of the Target (rfc2782) of the SRV result. This can + // then be extracted from the Host header on each DAV request, the + // original domain looked up and attached to the "localpart" to create + // a full "mailbox", allowing autodiscovery to just work for usernames + // in any domain including self hosted domains. + // + // So for this hostname -> issuer/scope lookup to work, we need to + // look not just at the hostname, but also any sub-domains of this + // hostname. + while (hostname.includes(".")) { + let foundHost = kHostnames.get(hostname); + if (foundHost) { + return foundHost; + } + hostname = hostname.replace(/^[^.]*[.]/, ""); + } + return undefined; + }, + + /** + * Map an issuer to OAuth2 account details. + * + * @param {string} issuer - The organization issuing OAuth2 parameters, e.g. + * "accounts.google.com". + * + * @returns {Array} An array containing [clientId, clientSecret, authorizationEndpoint, tokenEndpoint]. + * clientId and clientSecret are strings representing the account registered + * for Thunderbird with the organization. + * authorizationEndpoint and tokenEndpoint are url strings representing + * endpoints to access OAuth2 authentication. + */ + getIssuerDetails(issuer) { + return kIssuers.get(issuer); + }, +}; diff --git a/comm/mailnews/base/src/TemplateUtils.jsm b/comm/mailnews/base/src/TemplateUtils.jsm new file mode 100644 index 0000000000..fca56fca67 --- /dev/null +++ b/comm/mailnews/base/src/TemplateUtils.jsm @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["PluralStringFormatter", "makeFriendlyDateAgo"]; + +var { PluralForm } = ChromeUtils.importESModule( + "resource://gre/modules/PluralForm.sys.mjs" +); + +function PluralStringFormatter(aBundleURI) { + this._bundle = Services.strings.createBundle(aBundleURI); +} + +PluralStringFormatter.prototype = { + get(aStringName, aReplacements, aPluralCount) { + let str = this._bundle.GetStringFromName(aStringName); + if (aPluralCount !== undefined) { + str = PluralForm.get(aPluralCount, str); + } + if (aReplacements !== undefined) { + for (let i = 0; i < aReplacements.length; i++) { + str = str.replace("#" + (i + 1), aReplacements[i]); + } + } + return str; + }, +}; + +var gTemplateUtilsStrings = new PluralStringFormatter( + "chrome://messenger/locale/templateUtils.properties" +); + +const _dateFormatter = new Services.intl.DateTimeFormat(undefined, { + dateStyle: "short", +}); +const _dayMonthFormatter = new Services.intl.DateTimeFormat(undefined, { + month: "long", + day: "numeric", +}); +const _timeFormatter = new Services.intl.DateTimeFormat(undefined, { + timeStyle: "short", +}); +const _weekdayFormatter = new Services.intl.DateTimeFormat(undefined, { + weekday: "long", +}); + +/** + * Helper function to generate a localized "friendly" representation of + * time relative to the present. If the time input is "today", it returns + * a string corresponding to just the time. If it's yesterday, it returns + * "yesterday" (localized). If it's in the last week, it returns the day + * of the week. If it's before that, it returns the date. + * + * @param {Date} time - The time (better be in the past!) + * @returns {string} A "human-friendly" representation of that time + * relative to now. + */ +function makeFriendlyDateAgo(time) { + // TODO: use Intl.RelativeTimeFormat instead. + // Figure out when today begins + let now = new Date(); + let today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + + // Get the end time to display + let end = time; + + // Figure out if the end time is from today, yesterday, + // this week, etc. + let dateTime; + let kDayInMsecs = 24 * 60 * 60 * 1000; + let k6DaysInMsecs = 6 * kDayInMsecs; + if (end >= today) { + // activity finished after today started, show the time + dateTime = _timeFormatter.format(end); + } else if (today - end < kDayInMsecs) { + // activity finished after yesterday started, show yesterday + dateTime = gTemplateUtilsStrings.get("yesterday"); + } else if (today - end < k6DaysInMsecs) { + // activity finished after last week started, show day of week + dateTime = _weekdayFormatter.format(end); + } else if (now.getFullYear() == end.getFullYear()) { + // activity must have been from some time ago.. show month/day + dateTime = _dayMonthFormatter.format(end); + } else { + // not this year, so show full date format + dateTime = _dateFormatter.format(end); + } + return dateTime; +} diff --git a/comm/mailnews/base/src/UrlListener.cpp b/comm/mailnews/base/src/UrlListener.cpp new file mode 100644 index 0000000000..ce2ef4904a --- /dev/null +++ b/comm/mailnews/base/src/UrlListener.cpp @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "UrlListener.h" + +NS_IMPL_ISUPPORTS(UrlListener, nsIUrlListener) + +NS_IMETHODIMP UrlListener::OnStartRunningUrl(nsIURI* url) { + if (!mStartFn) { + return NS_OK; + } + return mStartFn(url); +} + +NS_IMETHODIMP UrlListener::OnStopRunningUrl(nsIURI* url, nsresult exitCode) { + if (!mStopFn) { + return NS_OK; + } + return mStopFn(url, exitCode); +} diff --git a/comm/mailnews/base/src/UrlListener.h b/comm/mailnews/base/src/UrlListener.h new file mode 100644 index 0000000000..22a0597854 --- /dev/null +++ b/comm/mailnews/base/src/UrlListener.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef UrlListener_h__ +#define UrlListener_h__ + +#include <functional> // For std::function. +#include "nsIUrlListener.h" +class nsIURI; + +/** + * UrlListener is a small nsIUrlListener implementation which allows + * callable objects (including lambdas) to be plugged in instead of deriving + * your own nsIUrlListener. + * + * The aim is to encourage more readable code by allowing the start/stop + * notifications of a long-running operation to be handled near to where the + * operation was initiated. + * + * A contrived example: + * + * void Kick() { + * UrlListener* listener = new UrlListener; + * listener->mStopFn = [](nsIURI* url, nsresult status) -> nsresult { + * // Note that we may get here waaaaaaay after Kick() has returned... + * printf("LongRunningOperation is finished.\n"); + * return NS_OK; + * }; + * thingService.startLongRunningOperation(listener); + * //...continue doing other stuff while operation is ongoing... + * } + * + * Traditionally, c-c code has tended to use multiple inheritance to add + * listener callbacks to the class of the object initiating the operation. + * This has a couple of undesirable side effects: + * + * 1) It separates out the onStopRunningUrl handling into some other + * part of the code, which makes the order of things much harder to follow. + * 2) Often the same onStopRunningUrl handler will be used for many different + * kinds of operations (see nsImapMailFolder::OnStopRunningUrl(), for + * example). + * 3) It exposes implementation details as part of the public interface + * e.g see all the listener types nsMsgDBFolder derives from to implement + * it's internals. That's all just confusing noise that shouldn't be seen + * from outside the class. + * + * Just as PromiseTestUtils.jsm brings the Javascript side up from callback + * hell to async lovelyness, this can be used to raise the C++ side from + * callback-somewhere-else-maybe-in-this-class-but-who-can-really-tell hell + * up to normal callback hell :-) + * + */ +class UrlListener : public nsIUrlListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIURLLISTENER + + UrlListener() {} + /** + * mStartFn and mStopFn are the OnStartRunning() and OnStopRunningUrl() + * handlers. It's fine for them to be null (often you'll only need mStopFn). + */ + std::function<nsresult(nsIURI*)> mStartFn; + std::function<nsresult(nsIURI*, nsresult)> mStopFn; + + protected: + virtual ~UrlListener() {} +}; + +#endif // UrlListener_h__ diff --git a/comm/mailnews/base/src/VirtualFolderWrapper.jsm b/comm/mailnews/base/src/VirtualFolderWrapper.jsm new file mode 100644 index 0000000000..6fc86c8d1e --- /dev/null +++ b/comm/mailnews/base/src/VirtualFolderWrapper.jsm @@ -0,0 +1,257 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Wrap everything about virtual folders. + */ + +const EXPORTED_SYMBOLS = ["VirtualFolderHelper"]; + +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +var VirtualFolderHelper = { + /** + * Create a new virtual folder (an actual nsIMsgFolder that did not previously + * exist), wrapping it in a VirtualFolderWrapper, and returning that wrapper. + * + * If the call to addSubfolder fails (and therefore throws), we will NOT catch + * it. + * + * @param {string} aFolderName - The name of the new folder to create. + * @param {nsIMsgFolder} aParentFolder - The folder in which to create the + * search folder. + * @param {nsIMsgFolder[]} aSearchFolders A list of nsIMsgFolders that you + * want to use as the sources for the virtual folder OR a string that is + * the already '|' delimited list of folder URIs to use. + * @param {nsIMsgSearchTerms[]} aSearchTerms - The search terms to + * use for the virtual folder. + * @param {boolean} aOnlineSearch Should the search attempt to use the + * server's search capabilities when possible and appropriate? + * @returns {VirtualFolderWrapper} The VirtualFolderWrapper wrapping the + * newly created folder. You would probably only want this for its + * virtualFolder attribute which has the nsIMsgFolder we created. + * Be careful about accessing any of the other attributes, as they will + * bring its message database back to life. + */ + createNewVirtualFolder( + aFolderName, + aParentFolder, + aSearchFolders, + aSearchTerms, + aOnlineSearch + ) { + let msgFolder = aParentFolder.addSubfolder(aFolderName); + msgFolder.prettyName = aFolderName; + msgFolder.setFlag(Ci.nsMsgFolderFlags.Virtual); + + let wrappedVirt = new VirtualFolderWrapper(msgFolder); + wrappedVirt.searchTerms = aSearchTerms; + wrappedVirt.searchFolders = aSearchFolders; + wrappedVirt.onlineSearch = aOnlineSearch; + + let msgDatabase = msgFolder.msgDatabase; + msgDatabase.summaryValid = true; + msgDatabase.close(true); + + aParentFolder.notifyFolderAdded(msgFolder); + MailServices.accounts.saveVirtualFolders(); + + return wrappedVirt; + }, + + /** + * Given an existing nsIMsgFolder that is a virtual folder, wrap it into a + * VirtualFolderWrapper. + * + * @param {nsIMsgFolder} aMsgFolder - The folder to use. + */ + wrapVirtualFolder(aMsgFolder) { + return new VirtualFolderWrapper(aMsgFolder); + }, +}; + +/** + * Abstracts dealing with the properties of a virtual folder that differentiate + * it from a non-virtual folder. A virtual folder is an odd duck. When + * holding an nsIMsgFolder that is a virtual folder, it is distinguished by + * the virtual flag and a number of properties that tell us the string + * representation of its search, the folders it searches over, and whether we + * use online searching or not. + * Virtual folders and their defining attributes are loaded from + * virtualFolders.dat (in the profile directory) by the account manager at + * startup, (re-)creating them if need be. It also saves them back to the + * file at shutdown. The most important thing the account manager does is to + * create VirtualFolderChangeListener instances that are registered with the + * message database service. This means that if one of the databases for the + * folders that the virtual folder includes is opened for some reason (for + * example, new messages are added to the folder because of a filter or they + * are delivered there), the virtual folder gets a chance to know about this + * and update the virtual folder's "cache" of information, such as the message + * counts or the presence of the message in the folder. + * The odd part is that a lot of the virtual folder logic also happens as a + * result of the nsMsgDBView subclasses being told the search query and the + * underlying folders. This makes for an odd collaboration of UI and backend + * logic. + * + * Justification for this class: Virtual folders aren't all that complex, but + * they are complex enough that we don't want to have the same code duplicated + * all over the place. We also don't want to have a loose assembly of global + * functions for working with them. So here we are. + * + * Important! Accessing any of our attributes results in the message database + * being loaded so that we can access the dBFolderInfo associated with the + * database. The message database is not automatically forgotten by the + * folder, which can lead to an (effective) memory leak. Please make sure + * that you are playing your part in not leaking memory by only using the + * wrapper when you have a serious need to access the database, and by + * forcing the folder to forget about the database when you are done by + * setting the database to null (unless you know with confidence someone else + * definitely wants the database around and will clean it up.) + * + * @param {nsIMsgFolder} aVirtualFolder - Folder to wrap. + */ +function VirtualFolderWrapper(aVirtualFolder) { + this.virtualFolder = aVirtualFolder; +} +VirtualFolderWrapper.prototype = { + /** + * @returns {nsIMsgFolders[]} The list of nsIMsgFolders that this virtual + * folder is a search over. + */ + get searchFolders() { + return this.dbFolderInfo + .getCharProperty("searchFolderUri") + .split("|") + .sort() // Put folders in URI order so a parent is always before a child. + .map(uri => MailServices.folderLookup.getOrCreateFolderForURL(uri)) + .filter(Boolean); + }, + /** + * Set the search folders that back this virtual folder. + * + * @param {string|nsIMsgFolder[]} aFolders - Either a "|"-delimited string of + * folder URIs or a list of folders. + */ + set searchFolders(aFolders) { + if (typeof aFolders == "string") { + this.dbFolderInfo.setCharProperty("searchFolderUri", aFolders); + } else { + let uris = aFolders.map(folder => folder.URI); + this.dbFolderInfo.setCharProperty("searchFolderUri", uris.join("|")); + } + Services.obs.notifyObservers(this.virtualFolder, "search-folders-changed"); + }, + + /** + * @returns {string} a "|"-delimited string containing the URIs of the folders + * that back this virtual folder. + */ + get searchFolderURIs() { + return this.dbFolderInfo.getCharProperty("searchFolderUri"); + }, + + /** + * @returns {nsIMsgSearchTerm[]} The list of search terms that define this + * virtual folder. + */ + get searchTerms() { + return this.searchTermsSession.searchTerms; + }, + /** + * @returns {nsIMsgFilterList} A newly created filter with the search terms + * loaded into it that define this virtual folder. The filter is apparently + * useful as an nsIMsgSearchSession stand-in to some code. + */ + get searchTermsSession() { + // Temporary means it doesn't get exposed to the UI and doesn't get saved to + // disk. Which is good, because this is just a trick to parse the string + // into search terms. + let filterList = MailServices.filters.getTempFilterList(this.virtualFolder); + let tempFilter = filterList.createFilter("temp"); + filterList.parseCondition(tempFilter, this.searchString); + return tempFilter; + }, + + /** + * Set the search string for this virtual folder to the stringified version of + * the provided list of nsIMsgSearchTerm search terms. If you already have + * a strinigified version of the search constraint, just set |searchString| + * directly. + * + * @param {string[]} aTerms - a list of search terms + */ + set searchTerms(aTerms) { + let condition = ""; + for (let term of aTerms) { + if (condition) { + condition += " "; + } + if (term.matchAll) { + condition = "ALL"; + break; + } + condition += term.booleanAnd ? "AND (" : "OR ("; + condition += term.termAsString + ")"; + } + this.searchString = condition; + }, + + /** + * @returns {string} the set of search terms that define this virtual folder + * as a string. You may prefer to use |searchTerms| which converts them + * into a list of nsIMsgSearchTerms instead. + */ + get searchString() { + return this.dbFolderInfo.getCharProperty("searchStr"); + }, + /** + * Set the search that defines this virtual folder from a string. If you have + * a list of nsIMsgSearchTerms, you should use |searchTerms| instead. + * + * @param {string} aSearchString + */ + set searchString(aSearchString) { + this.dbFolderInfo.setCharProperty("searchStr", aSearchString); + }, + + /** + * @returns {boolean} whether the virtual folder is configured for online search. + */ + get onlineSearch() { + return this.dbFolderInfo.getBooleanProperty("searchOnline", false); + }, + /** + * Set whether the virtual folder is configured for online search. + * + * @param {boolean} aOnlineSearch + */ + set onlineSearch(aOnlineSearch) { + this.dbFolderInfo.setBooleanProperty("searchOnline", aOnlineSearch); + }, + + /** + * @returns {?nsIDBFolderInfo} The dBFolderInfo associated with the virtual + * folder directly. Maybe null. Will cause the message database to be + * opened, which may have memory bloat/leak ramifications, so make sure + * the folder's database was already going to be opened anyways or that you + * call |cleanUpMessageDatabase|. + */ + get dbFolderInfo() { + let msgDatabase = this.virtualFolder.msgDatabase; + return msgDatabase && msgDatabase.dBFolderInfo; + }, + + /** + * Avoid memory bloat by making the virtual folder forget about its database. + * If the database is actually in use (read: someone is keeping it alive by + * having references to it from places other than the nsIMsgFolder), the + * folder will be able to re-establish the reference for minimal cost. + */ + cleanUpMessageDatabase() { + this.virtualFolder.msgDatabase.close(true); + this.virtualFolder.msgDatabase = null; + }, +}; diff --git a/comm/mailnews/base/src/WinUnreadBadge.jsm b/comm/mailnews/base/src/WinUnreadBadge.jsm new file mode 100644 index 0000000000..819d8c5719 --- /dev/null +++ b/comm/mailnews/base/src/WinUnreadBadge.jsm @@ -0,0 +1,246 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * Based on https://github.com/bstreiff/unread-badge. + * + * Copyright (c) 2013-2020 Brandon Streiff + */ + +const EXPORTED_SYMBOLS = ["WinUnreadBadge"]; + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +XPCOMUtils.defineLazyModuleGetters(lazy, { + NetUtil: "resource://gre/modules/NetUtil.jsm", +}); + +XPCOMUtils.defineLazyServiceGetters(lazy, { + imgTools: ["@mozilla.org/image/tools;1", "imgITools"], + taskbar: ["@mozilla.org/windows-taskbar;1", "nsIWinTaskbar"], +}); + +/** + * Get an imgIContainer instance from a canvas element. + * + * @param {HTMLCanvasElement} canvas - The canvas element. + * @param {number} width - The width of the canvas to use. + * @param {number} height - The height of the canvas to use. + * @returns {imgIContainer} + */ +function getCanvasAsImgContainer(canvas, width, height) { + let imageData = canvas.getContext("2d").getImageData(0, 0, width, height); + + // Create an imgIEncoder so we can turn the image data into a PNG stream. + let imgEncoder = Cc["@mozilla.org/image/encoder;2?type=image/png"].getService( + Ci.imgIEncoder + ); + imgEncoder.initFromData( + imageData.data, + imageData.data.length, + imageData.width, + imageData.height, + imageData.width * 4, + imgEncoder.INPUT_FORMAT_RGBA, + "" + ); + + // Now turn the PNG stream into an imgIContainer. + let imgBuffer = lazy.NetUtil.readInputStreamToString( + imgEncoder, + imgEncoder.available() + ); + let iconImage = lazy.imgTools.decodeImageFromBuffer( + imgBuffer, + imgBuffer.length, + "image/png" + ); + + // Close the PNG stream. + imgEncoder.close(); + return iconImage; +} + +/** + * Draw text centered in the middle of a CanvasRenderingContext2D. + * + * @param {CanvasRenderingContext2D} cxt - The canvas context to operate on. + * @param {string} text - The text to draw. + */ +function drawUnreadCountText(cxt, text) { + cxt.save(); + + let imageSize = cxt.canvas.width; + + // Use smaller fonts for longer text to try and squeeze it in. + let fontSize = imageSize * (0.95 - 0.15 * text.length); + + cxt.font = "500 " + fontSize + "px Calibri"; + cxt.fillStyle = "#ffffff"; + cxt.textAlign = "center"; + + // TODO: There isn't a textBaseline for accurate vertical centering ('middle' is the + // middle of the 'em block', and digits extend higher than 'm'), and the Mozilla core + // does not currently support computation of ascenders and descenters in measureText(). + // So, we just assume that the font is 70% of the 'px' height we requested, then + // compute where the baseline ought to be located. + let approximateHeight = fontSize * 0.7; + + cxt.textBaseline = "alphabetic"; + cxt.fillText( + text, + imageSize / 2, + imageSize - (imageSize - approximateHeight) / 2 + ); + + cxt.restore(); +} + +/** + * Create a flat badge, as is the Windows 8/10 style. + * + * @param {HTMLCanvasElement} canvas - The canvas element to draw the badge. + * @param {string} text - The text to draw in the badge. + */ +function createModernBadgeStyle(canvas, text) { + let cxt = canvas.getContext("2d"); + let iconSize = canvas.width; + + // Draw the background. + cxt.save(); + // Solid color first. + cxt.fillStyle = "#ff0039"; + cxt.shadowOffsetX = 0; + cxt.shadowOffsetY = 0; + cxt.shadowColor = "rgba(0,0,0,0.7)"; + cxt.shadowBlur = iconSize / 10; + cxt.beginPath(); + cxt.arc(iconSize / 2, iconSize / 2, iconSize / 2.25, 0, Math.PI * 2, true); + cxt.fill(); + cxt.clip(); + cxt.closePath(); + cxt.restore(); + + drawUnreadCountText(cxt, text); +} + +/** + * Downsample by 4X with simple averaging. + * + * Drawing at 4X and then downscaling like this gives us better results than + * using either CanvasRenderingContext2D.drawImage() to resize or letting + * the Windows taskbar service handle the resize, both of which seem to just + * give us a simple point resize. + * + * @param {Window} window - The DOM window. + * @param {HTMLCanvasElement} canvas - The input canvas element to resize. + * @returns {HTMLCanvasElement} The resized canvas element. + */ +function downsampleBy4X(window, canvas) { + let resizedCanvas = window.document.createElement("canvas"); + resizedCanvas.width = resizedCanvas.height = canvas.width / 4; + resizedCanvas.style.width = resizedCanvas.style.height = + resizedCanvas.width + "px"; + + let source = canvas + .getContext("2d") + .getImageData(0, 0, canvas.width, canvas.height); + let downsampled = resizedCanvas + .getContext("2d") + .createImageData(resizedCanvas.width, resizedCanvas.height); + + for (let y = 0; y < resizedCanvas.height; ++y) { + for (let x = 0; x < resizedCanvas.width; ++x) { + let r = 0, + g = 0, + b = 0, + a = 0; + let index; + + for (let i = 0; i < 4; ++i) { + for (let j = 0; j < 4; ++j) { + index = ((y * 4 + i) * source.width + (x * 4 + j)) * 4; + r += source.data[index]; + g += source.data[index + 1]; + b += source.data[index + 2]; + a += source.data[index + 3]; + } + } + + index = (y * downsampled.width + x) * 4; + downsampled.data[index] = Math.round(r / 16); + downsampled.data[index + 1] = Math.round(g / 16); + downsampled.data[index + 2] = Math.round(b / 16); + downsampled.data[index + 3] = Math.round(a / 16); + } + } + + resizedCanvas.getContext("2d").putImageData(downsampled, 0, 0); + + return resizedCanvas; +} + +/** + * A module to manage the unread badge icon on Windows. + */ +var WinUnreadBadge = { + /** + * Keeping an instance of nsITaskbarOverlayIconController alive + * to show a taskbar icon after the updateUnreadCount method exits. + */ + _controller: null, + + /** + * Update the unread badge. + * + * @param {number} unreadCount - Unread message count. + * @param {number} unreadTooltip - Unread message count tooltip. + */ + async updateUnreadCount(unreadCount, unreadTooltip) { + let window = Services.wm.getMostRecentBrowserWindow(); + if (!window) { + return; + } + if (!this._controller) { + this._controller = lazy.taskbar.getOverlayIconController(window.docShell); + } + if (unreadCount == 0) { + // Remove the badge if no unread. + this._controller.setOverlayIcon(null, ""); + return; + } + + // Draw the badge in a canvas. + let smallIconSize = Cc["@mozilla.org/windows-ui-utils;1"].getService( + Ci.nsIWindowsUIUtils + ).systemSmallIconSize; + let iconSize = Math.floor( + (window.windowUtils.displayDPI / 96) * smallIconSize + ); + let iconSize4X = iconSize * 4; + let badge = window.document.createElement("canvas"); + badge.width = badge.height = iconSize4X; + badge.style.width = badge.style.height = badge.width + "px"; + + createModernBadgeStyle( + badge, + unreadCount < 100 ? unreadCount.toString() : "99+" + ); + + badge = downsampleBy4X(window, badge); + let icon = getCanvasAsImgContainer(badge, iconSize, iconSize); + // Purge image from cache to force encodeImage() to not be lazy + icon.requestDiscard(); + // Side effect of encodeImage() is that it decodes original image + lazy.imgTools.encodeImage(icon, "image/png"); + // Somehow this is needed to prevent NS_ERROR_NOT_AVAILABLE error in + // setOverlayIcon. + await new Promise(resolve => window.setTimeout(resolve)); + + this._controller.setOverlayIcon(icon, unreadTooltip); + }, +}; diff --git a/comm/mailnews/base/src/components.conf b/comm/mailnews/base/src/components.conf new file mode 100644 index 0000000000..776148e132 --- /dev/null +++ b/comm/mailnews/base/src/components.conf @@ -0,0 +1,359 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +Classes = [ + { + "cid": "{a30be08c-afc8-4fed-9af7-79778a23db23}", + "contract_ids": ["@mozilla.org/mail/folder-lookup;1"], + "jsm": "resource:///modules/FolderLookupService.jsm", + "constructor": "FolderLookupService", + }, + { + "cid": "{49b04761-23dd-45d7-903d-619418a4d319}", + "contract_ids": ["@mozilla.org/messenger/msgAsyncPrompter;1"], + "jsm": "resource:///modules/MsgAsyncPrompter.jsm", + "constructor": "MsgAsyncPrompter", + }, + { + "cid": "{26ebf3a7-3e52-4b7a-a89b-fa7c0b7506f9}", + "contract_ids": ["@mozilla.org/messenger/msgAuthPrompt;1"], + "jsm": "resource:///modules/MsgAsyncPrompter.jsm", + "constructor": "MsgAuthPrompt", + }, + { + "cid": "{b63d8e4c-bf60-439b-be0e-7c9f67291042}", + "contract_ids": ["@mozilla.org/mail/oauth2-module;1"], + "jsm": "resource:///modules/OAuth2Module.jsm", + "constructor": "OAuth2Module", + }, + { + "cid": "{740880e6-e299-4165-b82f-df1dcab3ae22}", + "contract_ids": ["@mozilla.org/newMailNotificationService;1"], + "jsm": "resource:///modules/MailNotificationService.jsm", + "constructor": "NewMailNotificationService", + "name": "MailNotification", + "interfaces": ["mozINewMailNotificationService"], + }, + { + "cid": "{37246055-3596-4bfa-911f-3d2977e8d284}", + "contract_ids": ["@mozilla.org/mail/auth-module;1"], + "type": "nsMailAuthModule", + "headers": ["/comm/mailnews/base/src/nsMailAuthModule.h"], + }, + { + "cid": "{e9aef539-29db-4936-9fdc-40ba11c70cb3}", + "contract_ids": ["@mozilla.org/mail/notification-manager;1"], + "jsm": "resource:///modules/MailNotificationManager.jsm", + "constructor": "MailNotificationManager", + }, + { + "cid": "{4a85a5d0-cddd-11d2-b7f6-00805f05ffa5}", + "contract_ids": [ + "@mozilla.org/appshell/component/messenger;1", + "@mozilla.org/messenger/windowservice;1", + ], + "type": "nsMessengerBootstrap", + "headers": ["/comm/mailnews/base/src/nsMessengerBootstrap.h"], + }, + { + "cid": "{d5124441-d59e-11d2-806a-006080128c4e}", + "contract_ids": ["@mozilla.org/messenger/services/session;1"], + "type": "nsMsgMailSession", + "init_method": "Init", + "headers": ["/comm/mailnews/base/src/nsMsgMailSession.h"], + "name": "MailSession", + "interfaces": ["nsIMsgMailSession"], + }, + { + "cid": "{f436a174-e2c0-4955-9afe-e3feb68aee56}", + "contract_ids": ["@mozilla.org/messenger;1"], + "type": "nsMessenger", + "headers": ["/comm/mailnews/base/src/nsMessenger.h"], + }, + { + "cid": "{d2876e50-e62c-11d2-b7fc-00805f05ffa5}", + "contract_ids": ["@mozilla.org/messenger/account-manager;1"], + "type": "nsMsgAccountManager", + "init_method": "Init", + "headers": ["/comm/mailnews/base/src/nsMsgAccountManager.h"], + "name": "AccountManager", + "interfaces": ["nsIMsgAccountManager"], + }, + { + "cid": "{68b25510-e641-11d2-b7fc-00805f05ffa5}", + "contract_ids": ["@mozilla.org/messenger/account;1"], + "type": "nsMsgAccount", + "headers": ["/comm/mailnews/base/src/nsMsgAccount.h"], + }, + { + "cid": "{8fbf6ac0-ebcc-11d2-b7fc-00805f05ffa5}", + "contract_ids": ["@mozilla.org/messenger/identity;1"], + "type": "nsMsgIdentity", + "headers": ["/comm/mailnews/base/src/nsMsgIdentity.h"], + }, + { + "cid": "{4a374e7e-190f-11d3-8a88-0060b0fc04d2}", + "contract_ids": ["@mozilla.org/messenger/biffManager;1"], + "type": "nsMsgBiffManager", + "init_method": "Init", + "headers": ["/comm/mailnews/base/src/nsMsgBiffManager.h"], + }, + { + "cid": "{a687b474-afd8-418f-8ad9-f362202ae9a9}", + "contract_ids": ["@mozilla.org/messenger/purgeService;1"], + "type": "nsMsgPurgeService", + "headers": ["/comm/mailnews/base/src/nsMsgPurgeService.h"], + }, + { + "cid": "{7f9a9fb0-4161-11d4-9876-00c04fa0d2a6}", + "contract_ids": ["@mozilla.org/messenger/statusBarBiffManager;1"], + "type": "nsStatusBarBiffManager", + "init_method": "Init", + "headers": ["/comm/mailnews/base/src/nsStatusBarBiffManager.h"], + }, + { + "cid": "{7741daed-2125-11d3-8a90-0060b0fc04d2}", + "contract_ids": ["@mozilla.org/messenger/copymessagestreamlistener;1"], + "type": "nsCopyMessageStreamListener", + "headers": ["/comm/mailnews/base/src/nsCopyMessageStreamListener.h"], + }, + { + "cid": "{c766e666-29bd-11d3-afb3-001083002da8}", + "contract_ids": ["@mozilla.org/messenger/messagecopyservice;1"], + "type": "nsMsgCopyService", + "headers": ["/comm/mailnews/base/src/nsMsgCopyService.h"], + "name": "Copy", + "interfaces": ["nsIMsgCopyService"], + }, + { + "cid": "{bcdca970-3b22-11d3-8d76-0080f58a6617}", + "contract_ids": ["@mozilla.org/messenger/msgFolderCache;1"], + "type": "nsMsgFolderCache", + "headers": ["/comm/mailnews/base/src/nsMsgFolderCache.h"], + }, + { + "cid": "{bd85a417-5433-11d3-8ac5-0060b0fc04d2}", + "contract_ids": ["@mozilla.org/messenger/statusfeedback;1"], + "type": "nsMsgStatusFeedback", + "headers": ["/comm/mailnews/base/src/nsMsgStatusFeedback.h"], + }, + { + "cid": "{bb460dff-8bf0-11d3-8afe-0060b0fc04d2}", + "contract_ids": ["@mozilla.org/messenger/msgwindow;1"], + "type": "nsMsgWindow", + "init_method": "Init", + "headers": ["/comm/mailnews/base/src/nsMsgWindow.h"], + }, + { + "cid": "{8510876a-1dd2-11b2-8253-91f71b348a25}", + "contract_ids": ["@mozilla.org/messenger/subscribableserver;1"], + "type": "nsSubscribableServer", + "init_method": "Init", + "headers": ["/comm/mailnews/base/src/nsSubscribableServer.h"], + }, + { + "cid": "{56c4c2ac-fe4a-4528-aa78-f8fb579b029c}", + "contract_ids": ["@mozilla.org/messenger/foldercompactor;1"], + "type": "nsMsgFolderCompactor", + "headers": ["/comm/mailnews/base/src/nsMsgFolderCompactor.h"], + }, + { + "cid": "{52f860e0-1dd2-11b2-aa72-bb751981bd00}", + "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=threaded"], + "type": "nsMsgThreadedDBView", + "headers": ["/comm/mailnews/base/src/nsMsgThreadedDBView.h"], + }, + { + "cid": "{ca79a00e-010d-11d5-a5be-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=threadswithunread"], + "type": "nsMsgThreadsWithUnreadDBView", + "headers": ["/comm/mailnews/base/src/nsMsgSpecialViews.h"], + }, + { + "cid": "{597e1ffe-0123-11d5-a5be-0060b0fc04b7}", + "contract_ids": [ + "@mozilla.org/messenger/msgdbview;1?type=watchedthreadswithunread" + ], + "type": "nsMsgWatchedThreadsWithUnreadDBView", + "headers": ["/comm/mailnews/base/src/nsMsgSpecialViews.h"], + }, + { + "cid": "{aeac118c-0823-11d5-a5bf-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=search"], + "type": "nsMsgSearchDBView", + "headers": ["/comm/mailnews/base/src/nsMsgSearchDBView.h"], + }, + { + "cid": "{2dd9d0fe-b609-11d6-bacc-00108335748d}", + "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=quicksearch"], + "type": "nsMsgQuickSearchDBView", + "headers": ["/comm/mailnews/base/src/nsMsgQuickSearchDBView.h"], + }, + { + "cid": "{2af6e050-04f6-495a-8387-86b0aeb1863c}", + "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=xfvf"], + "type": "nsMsgXFVirtualFolderDBView", + "headers": ["/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h"], + }, + { + "cid": "{e4603d6c-0a74-47c5-b69e-2f8876990304}", + "contract_ids": ["@mozilla.org/messenger/msgdbview;1?type=group"], + "type": "nsMsgGroupView", + "headers": ["/comm/mailnews/base/src/nsMsgGroupView.h"], + }, + { + "cid": "{bcf6afbe-7d4f-11ec-9092-eb4fed0a5aaa}", + "contract_ids": ["@mozilla.org/msgDBView/msgDBViewService;1"], + "type": "nsMsgDBViewService", + "headers": ["/comm/mailnews/base/src/nsMsgDBView.h"], + "name": "DBView", + "interfaces": ["nsIMsgDBViewService"], + }, + { + "cid": "{ac6c518a-09b2-11d5-a5bf-0060b0fc04b7}", + "contract_ids": ["@mozilla.org/messenger/offline-manager;1"], + "type": "nsMsgOfflineManager", + "headers": ["/comm/mailnews/base/src/nsMsgOfflineManager.h"], + }, + { + "cid": "{9f4dd201-3b1f-11d5-9daa-c345c9453d3c}", + "contract_ids": ["@mozilla.org/messenger/progress;1"], + "type": "nsMsgProgress", + "headers": ["/comm/mailnews/base/src/nsMsgProgress.h"], + }, + { + "cid": "{ce6038ae-e5e0-4372-9cff-2a6633333b2b}", + "contract_ids": ["@mozilla.org/messenger/spamsettings;1"], + "type": "nsSpamSettings", + "headers": ["/comm/mailnews/base/src/nsSpamSettings.h"], + }, + { + "cid": "{b3db9392-1b15-48ba-a136-0cc3db13d87b}", + "contract_ids": ["@mozilla.org/network/protocol;1?name=cid"], + "type": "nsCidProtocolHandler", + "headers": ["/comm/mailnews/base/src/nsCidProtocolHandler.h"], + "protocol_config": { + "scheme": "cid", + "flags": [ + "URI_DANGEROUS_TO_LOAD" + ], + }, + }, + { + "cid": "{b897da55-8256-4cf5-892b-32e77bc7c50b}", + "contract_ids": ["@mozilla.org/messenger/tagservice;1"], + "type": "nsMsgTagService", + "headers": ["/comm/mailnews/base/src/nsMsgTagService.h"], + "name": "Tag", + "interfaces": ["nsIMsgTagService"], + }, + { + "cid": "{0c8ec907-49c7-49bc-8bdf-b16e29bd6c47}", + "contract_ids": ["@mozilla.org/msgFolder/msgFolderService;1"], + "type": "nsMsgFolderService", + "headers": ["/comm/mailnews/base/src/nsMsgDBFolder.h"], + "name": "Folder", + "interfaces": ["nsIMsgFolderService"], + }, + { + "cid": "{f1f7cbcd-d5e3-45a0-aa2d-cecf1a95ab03}", + "contract_ids": ["@mozilla.org/messenger/msgnotificationservice;1"], + "type": "nsMsgFolderNotificationService", + "headers": ["/comm/mailnews/base/src/nsMsgFolderNotificationService.h"], + "name": "FolderNotification", + "interfaces": ["nsIMsgFolderNotificationService"], + }, + { + "cid": "{dbfcfdf0-4489-4faa-8122-190fd1efa16c}", + "contract_ids": ["@mozilla.org/messenger/content-policy;1"], + "type": "nsMsgContentPolicy", + "init_method": "Init", + "headers": ["/comm/mailnews/base/src/nsMsgContentPolicy.h"], + "categories": {"content-policy": "@mozilla.org/messenger/content-policy;1"}, + }, + { + "cid": "{483c8abb-ecf9-48a3-a394-2c604b603bd5}", + "contract_ids": ["@mozilla.org/messenger/msgshutdownservice;1"], + "type": "nsMsgShutdownService", + "headers": ["/comm/mailnews/base/src/nsMsgMailSession.h"], + }, + { + "cid": "{03f9bb53-a680-4349-8de9-d26864d9ffd9}", + "contract_ids": ["@mozilla.org/mail/dir-provider;1"], + "type": "nsMailDirProvider", + "headers": ["/comm/mailnews/base/src/nsMailDirProvider.h"], + "categories": {"xpcom-directory-providers": "mail-directory-provider"}, + }, + { + "cid": "{6ef7eafd-72d0-4c56-9409-67e16d0f255b}", + "contract_ids": ["@mozilla.org/stopwatch;1"], + "type": "nsStopwatch", + "headers": ["/comm/mailnews/base/src/nsStopwatch.h"], + }, + { + "cid": "{de0f34a9-a87f-4f4c-b978-6187db187b90}", + "contract_ids": ["@mozilla.org/mailnews/document-loader-factory;1"], + "type": "mailnews::MailNewsDLF", + "headers": ["/comm/mailnews/base/src/MailNewsDLF.h"], + "categories": {"Gecko-Content-Viewers": "message/rfc822"}, + }, + { + "cid": "{bf88b48c-fd8e-40b4-ba36-c7c3ad6d8ac9}", + "contract_ids": ["@mozilla.org/embedcomp/base-command-controller;1"], + "type": "nsBaseCommandController", + "headers": ["/dom/commandhandler/nsBaseCommandController.h"], + }, + { + "cid": "{9c8f9601-801a-11d2-98ba-00805f297d89}", + "contract_ids": ["@mozilla.org/transactionmanager;1"], + "type": "TransactionManager", + "headers": ["/editor/txmgr/TransactionManager.h"], + }, + { + "cid": "{14c13684-1dd2-11b2-9463-bb10ba742554}", + "contract_ids": ["@mozilla.org/userinfo;1"], + "type": "nsUserInfo", + "headers": ["/comm/mailnews/base/src/nsUserInfo.h"], + }, + # The XPCOM registration for nsSyncStreamListener was moved to comm-central + # in bug 1501718. In bug 1800606 all uses were removed from comm-central. + # The registration is maintained for some MailExtension Experiments. + # Use of this service is discouraged due to its synchronous processing and + # probable deprecation in the future. Instead use asynchronous code like in + # `get_msg_source()` in ComposeHelpers.jsm. + { + "cid": "{439400d3-6f23-43db-8b06-8aafe1869bd8}", + "contract_ids": ["@mozilla.org/network/sync-stream-listener;1"], + "constructor": "SyncStreamListenerCreate", + "headers": ["/comm/mailnews/base/src/nsMsgUtils.h"], + }, +] + +if buildconfig.substs["OS_ARCH"] == "Darwin": + Classes += [ + { + "cid": "{746b28a5-d239-4719-b1a2-cf8093332ae3}", + "contract_ids": ["@mozilla.org/messenger/osintegration;1"], + "type": "nsMessengerOSXIntegration", + "headers": ["/comm/mailnews/base/src/nsMessengerOSXIntegration.h"], + }, + ] + + Categories = { + "app-startup": { + "OS Integration": "@mozilla.org/messenger/osintegration;1", + } + } + +if buildconfig.substs["OS_ARCH"] == "WINNT": + Classes += [ + { + "cid": "{a74dd1d6-2ec4-4985-98f3-f69e18d20811}", + "contract_ids": ["@mozilla.org/messenger/osintegration;1"], + "type": "nsMessengerWinIntegration", + "headers": ["/comm/mailnews/base/src/nsMessengerWinIntegration.h"], + }, + ] diff --git a/comm/mailnews/base/src/converterWorker.js b/comm/mailnews/base/src/converterWorker.js new file mode 100644 index 0000000000..188476c1e1 --- /dev/null +++ b/comm/mailnews/base/src/converterWorker.js @@ -0,0 +1,533 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-env mozilla/chrome-worker, node */ + +/** + * This worker will perform mbox<->maildir conversions on a tree of + * directories. It operates purely at the filesystem level. + * + * The initial message data should pass in these params to control + * the conversion: + * + * srcType - source mailstore type ('mbox' or 'maildir') + * destType - destination mailstore type ('maildir' or 'mbox') + * srcRoot - root path of source (eg ".../ImapMail/imap.example.com") + * destRoot - root path of destination (eg "/tmp/imap.example.com-maildir") + * + * The conversion is non-destructive - srcRoot will be left untouched. + * + * The worker will post progress messages back to the main thread of + * the form: + * + * {"msg": "progress", "val": val, "total": total} + * + * Where `val` is the current progress, out of `total`. + * The units used for val and total are undefined. + * + * When the conversion is complete, before exiting, the worker sends a + * message of the form: + * + * {"msg": "success"} + * + * Errors are posted back to the main thread via the standard + * "error" event. + * + */ + +/** + * Merge all the messages in a maildir into a single mbox file. + * + * @param {string} maildir - Path to the source maildir. + * @param {string} mboxFilename - Path of the mbox file to create. + * @param {Function(number)} progressFn - Function to be invoked regularly with + * progress updates. Param is number of + * "units" processed since last update. + */ +async function maildirToMBox(maildir, mboxFilename, progressFn) { + // Helper to format dates + // eg "Thu Jan 18 12:34:56 2018" + let fmtUTC = function (d) { + const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + const monthNames = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + return ( + dayNames[d.getUTCDay()] + + " " + + monthNames[d.getUTCMonth()] + + " " + + d.getUTCDate().toString().padStart(2) + + " " + + d.getUTCHours().toString().padStart(2, "0") + + ":" + + d.getUTCMinutes().toString().padStart(2, "0") + + ":" + + d.getUTCSeconds().toString().padStart(2, "0") + + " " + + d.getUTCFullYear() + ); + }; + + // Initialize mbox file + await IOUtils.write(mboxFilename, new Uint8Array(), { + mode: "create", + }); + + // Iterate over all the message files in "cur". + let curPath = PathUtils.join(maildir, "cur"); + let paths = await IOUtils.getChildren(curPath); + let files = await Promise.all( + paths.map(async path => { + let stat = await IOUtils.stat(path); + return { + path, + creationDate: stat.creationTime, + }; + }) + ); + // We write out the mbox messages ordered by creation time. + // Not ideal, but best we can do without parsing message. + files.sort(function (a, b) { + return a.creationDate - b.creationDate; + }); + + for (let ent of files) { + let raw = await IOUtils.read(ent.path); + // Old converter had a bug where maildir messages included the + // leading "From " marker, so we need to cope with any + // cases of this left in the wild. + if (String.fromCharCode.apply(null, raw.slice(0, 5)) != "From ") { + // Write the separator line. + // Technically, timestamp should be the reception time of the + // message, but we don't really want to have to parse the + // message here and nothing is likely to rely on it. + let sepLine = "From - " + fmtUTC(new Date()) + "\n"; + await IOUtils.writeUTF8(mboxFilename, sepLine, { + mode: "append", + }); + } + + await IOUtils.write(mboxFilename, raw, { + mode: "append", + }); + // Maildir progress is one per message. + progressFn(1); + } +} + +/** + * Split an mbox file up into a maildir. + * + * @param {string} mboxPath - Path of the mbox file to split. + * @param {string} maildirPath - Path of the maildir to create. + * @param {Function(number)} progressFn - Function to be invoked regularly with + * progress updates. One parameter is + * passed - the number of "cost units" + * since the previous update. + */ +async function mboxToMaildir(mboxPath, maildirPath, progressFn) { + // Create the maildir structure. + await IOUtils.makeDirectory(maildirPath); + let curDirPath = PathUtils.join(maildirPath, "cur"); + let tmpDirPath = PathUtils.join(maildirPath, "tmp"); + await IOUtils.makeDirectory(curDirPath); + await IOUtils.makeDirectory(tmpDirPath); + + const CHUNK_SIZE = 1000000; + // SAFE_MARGIN is how much to keep back between chunks in order to + // cope with separator lines which might span chunks. + const SAFE_MARGIN = 100; + + // A regexp to match mbox separator lines. Separator lines in the wild can + // have all sorts of forms, for example: + // + // "From " + // "From MAILER-DAEMON Fri Jul 8 12:08:34 2011" + // "From - Mon Jul 11 12:08:34 2011" + // "From bob@example.com Fri Jul 8 12:08:34 2011" + // + // So we accept any line beginning with "From " and ignore the rest of it. + // + // We also require a message header on the next line, in order + // to better cope with unescaped "From " lines in the message body. + // note: the first subexpression matches the separator line, so + // that it can be removed from the input. + let sepRE = /^(From (?:.*?)\r?\n)[\x21-\x7E]+:/gm; + + // Use timestamp as starting name for output messages, incrementing + // by one for each. + let ident = Date.now(); + + /** + * Helper. Convert a string into a Uint8Array, using no encoding. The low + * byte of each 16 bit character will be used, the high byte discarded. + * + * @param {string} str - Input string with chars in 0-255 range. + * @returns {Uint8Array} The output bytes. + */ + let stringToBytes = function (str) { + var bytes = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes; + }; + + /** + * Helper. Convert a Uint8Array directly into a string, using each byte + * directly as a character code. So all characters in the resulting string + * will range from 0 to 255, even though they are 16 bit values. + * + * @param {Uint8Array} bytes - The bytes to convert. + * @returns {string} The byte values in string form. + */ + let bytesToString = function (bytes) { + return bytes.reduce(function (str, b) { + return str + String.fromCharCode(b); + }, ""); + }; + + let outPath; + + /** + * Helper. Write out a block of bytes to the current message file, starting + * a new file if required. + * + * @param {string} str - The bytes to append (as chars in range 0-255). + */ + let writeToMsg = async function (str) { + let mode = "append"; + if (!outPath) { + outPath = PathUtils.join(curDirPath, ident.toString() + ".eml"); + ident += 1; + mode = "create"; + } + // We know that str is really raw 8-bit data, not UTF-16. So we can + // discard the upper byte and just keep the low byte of each char. + let raw = stringToBytes(str); + await IOUtils.write(outPath, raw, { mode }); + // For mbox->maildir conversion, progress is measured in bytes. + progressFn(raw.byteLength); + }; + + let buf = ""; + let eof = false; + let offset = 0; + while (!eof) { + let rawBytes = await IOUtils.read(mboxPath, { + offset, + maxBytes: CHUNK_SIZE, + }); + // We're using JavaScript strings (which hold 16bit characters) to store + // 8 bit data. This sucks, but is faster than trying to operate directly + // upon Uint8Arrays. A lot of work goes into optimising JavaScript strings. + buf += bytesToString(rawBytes); + offset += rawBytes.byteLength; + eof = rawBytes.byteLength < CHUNK_SIZE; + + let pos = 0; + sepRE.lastIndex = 0; // start at beginning of buf + let m = null; + while ((m = sepRE.exec(buf)) !== null) { + // Output everything up to the line separator. + if (m.index > pos) { + await writeToMsg(buf.substring(pos, m.index)); + } + pos = m.index; + pos += m[1].length; // skip the "From " line + // Reset the current message file path if any. + if (outPath) { + outPath = null; + } + } + + // Deal with whatever is left in the buffer. + let endPos = buf.length; + if (!eof) { + // Keep back enough to cope with separator lines crossing + // chunk boundaries. + endPos -= SAFE_MARGIN; + if (endPos < pos) { + endPos = pos; + } + } + + if (endPos > pos) { + await writeToMsg(buf.substring(pos, endPos)); + } + buf = buf.substring(endPos); + } +} + +/** + * Check if directory is a subfolder directory. + * + * @param {string} name - Name of directory to check. + * @returns {boolean} - true if subfolder. + */ +function isSBD(name) { + return name.substr(-4) == ".sbd"; +} + +/** + * Check if file is a type which should be copied verbatim as part of a + * conversion. + * See also: nsMsgLocalStoreUtils::nsShouldIgnoreFile(). + * + * @param {string} name - Name of file to check. + * @returns {boolean} - true if file should be copied verbatim. + */ +function isFileToCopy(name) { + let ext4 = name.substr(-4); + // Database and config files. + if (ext4 == ".msf" || ext4 == ".dat") { + return true; + } + // Summary files. + if (ext4 == ".snm" || ext4 == ".toc") { + return true; + } + // A few files we know might be lurking there. + const SPECIAL_FILES = [ + "filterlog.html", + "junklog.html", + "feeds.json", + "feeds.json.tmp", + "feeds.json.backup", + "feeds.json.corrupt", + "feeditems.json", + "feeditems.json.tmp", + "feeditems.json.backup", + "feeditems.json.corrupt", + "mailfilt.log", + "filters.js", + ]; + if (SPECIAL_FILES.includes(name)) { + return true; + } + return false; +} + +/** + * Check if file is an mbox. + * (actually we can't really tell if it's an mbox or not just from the name. + * we just assume it is, if it's not .msf or .dat). + * + * @param {string} name - Name of file to check. + * @returns {boolean} - true if file is an mbox + */ +function isMBoxName(name) { + // If it's not a "special" file, assume it's mbox. + return !isFileToCopy(name); +} + +/** + * Check if directory is a maildir (by looking for a "cur" subdir). + * + * @param {string} dir - Path of directory to check. + * @returns {Promise<boolean>} - true if directory is a maildir. + */ +async function isMaildir(dir) { + try { + let cur = PathUtils.join(dir, "cur"); + let fi = await IOUtils.stat(cur); + return fi.type === "directory"; + } catch (ex) { + if (ex instanceof DOMException && ex.name === "NotFoundError") { + // "cur" does not exist - not a maildir. + return false; + } + throw ex; // Other error. + } +} + +/** + * Count the number of messages in the "cur" dir of maildir. + * + * @param {string} maildir - Path of maildir. + * @returns {Promise<number>} - number of messages found. + */ +async function countMaildirMsgs(maildir) { + let cur = PathUtils.join(maildir, "cur"); + let paths = await IOUtils.getChildren(cur); + return paths.length; +} + +/** + * Recursively calculate the 'cost' of a hierarchy of maildir folders. + * This is the figure used for progress updates. + * For maildir, cost is 1 per message. + * + * @param {string} srcPath - Path of root dir containing maildirs. + * @returns {Promise<number>} - calculated conversion cost. + */ +async function calcMaildirCost(srcPath) { + let cost = 0; + for (let path of await IOUtils.getChildren(srcPath)) { + let stat = await IOUtils.stat(path); + if (stat.type === "directory") { + let name = PathUtils.filename(path); + if (isSBD(name)) { + // Recurse into subfolder. + cost += await calcMaildirCost(path); + } else if (await isMaildir(path)) { + // Looks like a maildir. Cost is number of messages. + cost += await countMaildirMsgs(path); + } + } + } + return cost; +} + +/** + * Recursively calculate the 'cost' of a hierarchy of mbox folders. + * This is the figure used for progress updates. + * For mbox, cost is the total byte size of data. This avoids the need to + * parse the mbox files to count the number of messages. + * Note that this byte count cost is not 100% accurate because it includes + * the "From " lines which are not written into the maildir files. But it's + * definitely close enough to give good user feedback. + * + * @param {string} srcPath - Path of root dir containing maildirs. + * @returns {Promise<number>} - calculated conversion cost. + */ +async function calcMBoxCost(srcPath) { + let cost = 0; + for (const path of await IOUtils.getChildren(srcPath)) { + let stat = await IOUtils.stat(path); + let name = PathUtils.filename(path); + if (stat.type === "directory") { + if (isSBD(name)) { + // Recurse into .sbd subfolder. + cost += await calcMBoxCost(path); + } + } else if (isMBoxName(name)) { + cost += stat.size; + } + } + return cost; +} + +/** + * Recursively convert a tree of mbox-based folders to maildirs. + * + * @param {string} srcPath - Root path containing mboxes. + * @param {string} destPath - Where to create destination root. + * @param {Function(number)} progressFn - Function to be invoked regularly with + * progress updates (called with number of + * cost "units" since last update) + */ +async function convertTreeMBoxToMaildir(srcPath, destPath, progressFn) { + await IOUtils.makeDirectory(destPath); + + for (const path of await IOUtils.getChildren(srcPath)) { + let name = PathUtils.filename(path); + let dest = PathUtils.join(destPath, name); + let stat = await IOUtils.stat(path); + if (stat.type === "directory") { + if (isSBD(name)) { + // Recurse into .sbd subfolder. + await convertTreeMBoxToMaildir(path, dest, progressFn); + } + } else if (isFileToCopy(name)) { + await IOUtils.copy(path, dest); + } else if (isMBoxName(name)) { + // It's an mbox. Convert it. + await mboxToMaildir(path, dest, progressFn); + } + } +} + +/** + * Recursively convert a tree of maildir-based folders to mbox. + * + * @param {string} srcPath - Root path containing maildirs. + * @param {string} destPath - Where to create destination root. + * @param {Function(number)} progressFn - Function to be invoked regularly with + * progress updates (called with number of + * cost "units" since last update) + */ +async function convertTreeMaildirToMBox(srcPath, destPath, progressFn) { + await IOUtils.makeDirectory(destPath); + + for (let path of await IOUtils.getChildren(srcPath)) { + let name = PathUtils.filename(path); + let dest = PathUtils.join(destPath, name); + let stat = await IOUtils.stat(path); + if (stat.type === "directory") { + if (isSBD(name)) { + // Recurse into .sbd subfolder. + await convertTreeMaildirToMBox(path, dest, progressFn); + } else if (await isMaildir(path)) { + // It's a maildir - convert it. + await maildirToMBox(path, dest, progressFn); + } + } else if (isFileToCopy(name)) { + await IOUtils.copy(path, dest); + } + } +} + +// propagate unhandled rejections to the error handler on the main thread +self.addEventListener("unhandledrejection", function (error) { + throw error.reason; +}); + +self.addEventListener("message", function (e) { + // Unpack the request params from the main thread. + let srcType = e.data.srcType; + let destType = e.data.destType; + let srcRoot = e.data.srcRoot; + let destRoot = e.data.destRoot; + // destRoot will be a temporary dir, so if it all goes pear-shaped + // we can just bail out without cleaning up. + + // Configure the conversion. + let costFn = null; + let convertFn = null; + if (srcType == "maildir" && destType == "mbox") { + costFn = calcMaildirCost; + convertFn = convertTreeMaildirToMBox; + } else if (srcType == "mbox" && destType == "maildir") { + costFn = calcMBoxCost; + convertFn = convertTreeMBoxToMaildir; + } else { + throw new Error(`Unsupported conversion: ${srcType} => ${destType}`); + } + + // Go! + costFn(srcRoot).then(totalCost => { + let v = 0; + let progressFn = function (n) { + v += n; + self.postMessage({ msg: "progress", val: v, total: totalCost }); + }; + convertFn(srcRoot, destRoot, progressFn).then(() => { + // We fake a final progress update, with exactly 100% completed. + // Our byte-counting on mbox->maildir conversion will fall slightly short: + // The total is estimated from the mbox filesize, but progress is tracked + // by counting bytes as they are written out - and the mbox "From " lines + // are _not_ written out to the maildir files. + // This is still accurate enough to provide progress to the user, but we + // don't want the GUI left showing "progress 97% - conversion complete!" + // or anything silly like that. + self.postMessage({ msg: "progress", val: totalCost, total: totalCost }); + + // Let the main thread know we succeeded. + self.postMessage({ msg: "success" }); + }); + }); +}); diff --git a/comm/mailnews/base/src/hostnameUtils.jsm b/comm/mailnews/base/src/hostnameUtils.jsm new file mode 100644 index 0000000000..e80c210b2e --- /dev/null +++ b/comm/mailnews/base/src/hostnameUtils.jsm @@ -0,0 +1,366 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * Generic shared utility code for checking of IP and hostname validity. + */ + +const EXPORTED_SYMBOLS = [ + "isLegalHostNameOrIP", + "isLegalHostName", + "isLegalIPv4Address", + "isLegalIPv6Address", + "isLegalIPAddress", + "isLegalLocalIPAddress", + "cleanUpHostName", + "kMinPort", + "kMaxPort", +]; + +var kMinPort = 1; +var kMaxPort = 65535; + +/** + * Check if aHostName is an IP address or a valid hostname. + * + * @param {string} aHostName - The string to check for validity. + * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in addition to decimal. + * @returns {?string} Unobscured host name if aHostName is valid. + * Returns null if it's not. + */ +function isLegalHostNameOrIP(aHostName, aAllowExtendedIPFormats) { + /* + RFC 1123: + Whenever a user inputs the identity of an Internet host, it SHOULD + be possible to enter either (1) a host domain name or (2) an IP + address in dotted-decimal ("#.#.#.#") form. The host SHOULD check + the string syntactically for a dotted-decimal number before + looking it up in the Domain Name System. + */ + + return ( + isLegalIPAddress(aHostName, aAllowExtendedIPFormats) || + isLegalHostName(aHostName) + ); +} + +/** + * Check if aHostName is a valid hostname. + * + * @returns {?string} The host name if it is valid. Returns null if it's not. + */ +function isLegalHostName(aHostName) { + /* + RFC 952: + A "name" (Net, Host, Gateway, or Domain name) is a text string up + to 24 characters drawn from the alphabet (A-Z), digits (0-9), minus + sign (-), and period (.). Note that periods are only allowed when + they serve to delimit components of "domain style names". (See + RFC-921, "Domain Name System Implementation Schedule", for + background). No blank or space characters are permitted as part of a + name. No distinction is made between upper and lower case. The first + character must be an alpha character. The last character must not be + a minus sign or period. + + RFC 1123: + The syntax of a legal Internet host name was specified in RFC-952 + [DNS:4]. One aspect of host name syntax is hereby changed: the + restriction on the first character is relaxed to allow either a + letter or a digit. Host software MUST support this more liberal + syntax. + + Host software MUST handle host names of up to 63 characters and + SHOULD handle host names of up to 255 characters. + + RFC 1034: + Relative names are either taken relative to a well known origin, or to a + list of domains used as a search list. Relative names appear mostly at + the user interface, where their interpretation varies from + implementation to implementation, and in master files, where they are + relative to a single origin domain name. The most common interpretation + uses the root "." as either the single origin or as one of the members + of the search list, so a multi-label relative name is often one where + the trailing dot has been omitted to save typing. + + Since a complete domain name ends with the root label, this leads to + a printed form which ends in a dot. + */ + + const hostPattern = + /^(([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.)*([a-z0-9]|[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])\.?$/i; + return aHostName.length <= 255 && hostPattern.test(aHostName) + ? aHostName + : null; +} + +/** + * Check if aHostName is a valid IPv4 address. + * + * @param {string} aHostName - The string to check for validity. + * @param {boolean} aAllowExtendedIPFormats - If false, only IPv4 addresses + * in the common decimal format (4 components, each up to 255) + * will be accepted, no hex/octal formats. + * @returns {string} Unobscured canonicalized address if aHostName is an + * IPv4 address. Returns null if it's not. + */ +function isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) { + // Scammers frequently obscure the IP address by encoding each component as + // decimal, octal, hex or in some cases a mix match of each. There can even + // be less than 4 components where the last number covers the missing components. + // See the test at mailnews/base/test/unit/test_hostnameUtils.js for possible + // combinations. + + if (!aHostName) { + return null; + } + + // Break the IP address down into individual components. + let ipComponents = aHostName.split("."); + let componentCount = ipComponents.length; + if (componentCount > 4 || (componentCount < 4 && !aAllowExtendedIPFormats)) { + return null; + } + + /** + * Checks validity of an IP address component. + * + * @param {string} aValue - The component string. + * @param {integer} aWidth - How many components does this string cover. + * @returns {integer|null} The value of the component in decimal if it is valid. + * Returns null if it's not. + */ + const kPowersOf256 = [1, 256, 65536, 16777216, 4294967296]; + function isLegalIPv4Component(aValue, aWidth) { + let component; + // Is the component decimal? + if (/^(0|([1-9][0-9]{0,9}))$/.test(aValue)) { + component = parseInt(aValue, 10); + } else if (aAllowExtendedIPFormats) { + // Is the component octal? + if (/^(0[0-7]{1,12})$/.test(aValue)) { + component = parseInt(aValue, 8); + } else if (/^(0x[0-9a-f]{1,8})$/i.test(aValue)) { + // The component is hex. + component = parseInt(aValue, 16); + } else { + return null; + } + } else { + return null; + } + + // Make sure the component in not larger than the expected maximum. + if (component >= kPowersOf256[aWidth]) { + return null; + } + + return component; + } + + for (let i = 0; i < componentCount; i++) { + // If we are on the last supplied component but we do not have 4, + // the last one covers the remaining ones. + let componentWidth = i == componentCount - 1 ? 4 - i : 1; + let componentValue = isLegalIPv4Component(ipComponents[i], componentWidth); + if (componentValue == null) { + return null; + } + + // If we have a component spanning multiple ones, split it. + for (let j = 0; j < componentWidth; j++) { + ipComponents[i + j] = + (componentValue >> ((componentWidth - 1 - j) * 8)) & 255; + } + } + + // First component of zero is not valid. + if (ipComponents[0] == 0) { + return null; + } + + return ipComponents.join("."); +} + +/** + * Check if aHostName is a valid IPv6 address. + * + * @param {string} aHostName - The string to check for validity. + * @returns {string} Unobscured canonicalized address if aHostName is an + * IPv6 address. Returns null if it's not. + */ +function isLegalIPv6Address(aHostName) { + if (!aHostName) { + return null; + } + + // Break the IP address down into individual components. + let ipComponents = aHostName.toLowerCase().split(":"); + + // Make sure there are at least 3 components. + if (ipComponents.length < 3) { + return null; + } + + let ipLength = ipComponents.length - 1; + + // Take care if the last part is written in decimal using dots as separators. + let lastPart = isLegalIPv4Address(ipComponents[ipLength], false); + if (lastPart) { + let lastPartComponents = lastPart.split("."); + // Convert it into standard IPv6 components. + ipComponents[ipLength] = ( + (lastPartComponents[0] << 8) | + lastPartComponents[1] + ).toString(16); + ipComponents[ipLength + 1] = ( + (lastPartComponents[2] << 8) | + lastPartComponents[3] + ).toString(16); + } + + // Make sure that there is only one empty component. + let emptyIndex; + for (let i = 1; i < ipComponents.length - 1; i++) { + if (ipComponents[i] == "") { + // If we already found an empty component return null. + if (emptyIndex) { + return null; + } + + emptyIndex = i; + } + } + + // If we found an empty component, extend it. + if (emptyIndex) { + ipComponents[emptyIndex] = 0; + + // Add components so we have a total of 8. + for (let count = ipComponents.length; count < 8; count++) { + ipComponents.splice(emptyIndex, 0, 0); + } + } + + // Make sure there are 8 components. + if (ipComponents.length != 8) { + return null; + } + + // Format all components to 4 character hex value. + for (let i = 0; i < ipComponents.length; i++) { + if (ipComponents[i] == "") { + ipComponents[i] = 0; + } + + // Make sure the component is a number and it isn't larger than 0xffff. + if (/^[0-9a-f]{1,4}$/.test(ipComponents[i])) { + ipComponents[i] = parseInt(ipComponents[i], 16); + if (isNaN(ipComponents[i]) || ipComponents[i] > 0xffff) { + return null; + } + } else { + return null; + } + + // Pad the component with 0:s. + ipComponents[i] = ("0000" + ipComponents[i].toString(16)).substr(-4); + } + + // TODO: support Zone indices in Link-local addresses? Currently they are rejected. + // http://en.wikipedia.org/wiki/IPv6_address#Link-local_addresses_and_zone_indices + + let hostName = ipComponents.join(":"); + // Treat 0000:0000:0000:0000:0000:0000:0000:0000 as an invalid IPv6 address. + return hostName != "0000:0000:0000:0000:0000:0000:0000:0000" + ? hostName + : null; +} + +/** + * Check if aHostName is a valid IP address (IPv4 or IPv6). + * + * @param {string} aHostName - The string to check for validity. + * @param {boolean} aAllowExtendedIPFormats - Allow hex/octal formats in + * addition to decimal. + * @returns {?string} Unobscured canonicalized IPv4 or IPv6 address if it is + * valid, otherwise null. + */ +function isLegalIPAddress(aHostName, aAllowExtendedIPFormats) { + return ( + isLegalIPv4Address(aHostName, aAllowExtendedIPFormats) || + isLegalIPv6Address(aHostName) + ); +} + +/** + * Check if aIPAddress is a local or private IP address. + * Note: if the passed in address is not in canonical (unobscured form), + * the result may be wrong. + * + * @param {string} aIPAddress - A valid IP address literal in canonical + * (unobscured) form. + * @returns {boolean} frue if it is a local/private IPv4 or IPv6 address. + */ +function isLegalLocalIPAddress(aIPAddress) { + // IPv4 address? + let ipComponents = aIPAddress.split("."); + if (ipComponents.length == 4) { + // Check if it's a local or private IPv4 address. + return ( + ipComponents[0] == 10 || + ipComponents[0] == 127 || // loopback address + (ipComponents[0] == 192 && ipComponents[1] == 168) || + (ipComponents[0] == 169 && ipComponents[1] == 254) || + (ipComponents[0] == 172 && ipComponents[1] >= 16 && ipComponents[1] < 32) + ); + } + + // IPv6 address? + ipComponents = aIPAddress.split(":"); + if (ipComponents.length == 8) { + // ::1/128 - localhost + if ( + ipComponents[0] == "0000" && + ipComponents[1] == "0000" && + ipComponents[2] == "0000" && + ipComponents[3] == "0000" && + ipComponents[4] == "0000" && + ipComponents[5] == "0000" && + ipComponents[6] == "0000" && + ipComponents[7] == "0001" + ) { + return true; + } + + // fe80::/10 - link local addresses + if (ipComponents[0] == "fe80") { + return true; + } + + // fc00::/7 - unique local addresses + if ( + ipComponents[0].startsWith("fc") || // usage has not been defined yet + ipComponents[0].startsWith("fd") + ) { + return true; + } + + return false; + } + + return false; +} + +/** + * Clean up the hostname or IP. Usually used to sanitize a value input by the user. + * It is usually applied before we know if the hostname is even valid. + * + * @param {string} aHostName - The hostname or IP string to clean up. + */ +function cleanUpHostName(aHostName) { + // TODO: Bug 235312: if UTF8 string was input, convert to punycode using convertUTF8toACE() + // but bug 563172 needs resolving first. + return aHostName.trim(); +} diff --git a/comm/mailnews/base/src/mailstoreConverter.jsm b/comm/mailnews/base/src/mailstoreConverter.jsm new file mode 100644 index 0000000000..6e5be5ebe1 --- /dev/null +++ b/comm/mailnews/base/src/mailstoreConverter.jsm @@ -0,0 +1,339 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +var EXPORTED_SYMBOLS = ["convertMailStoreTo", "terminateWorkers"]; + +const { FileUtils } = ChromeUtils.importESModule( + "resource://gre/modules/FileUtils.sys.mjs" +); + +let log = console.createInstance({ + prefix: "mail.mailstoreconverter", + maxLogLevel: "Warn", + maxLogLevelPref: "mail.mailstoreconverter.loglevel", +}); + +let gConverterWorker = null; + +/** + * Sets a server to use a different type of mailstore, converting + * all the existing data. + * + * @param {string} aMailstoreContractId - XPCOM id of new mailstore type. + * @param {nsIMsgServer} aServer - server to migrate. + * @param {?Element} aEventTarget - If set, element to send progress events. + * + * @returns {Promise<string>} - Resolves with a string containing the new root + * directory for the migrated server. + * Rejects with an error message. + */ +function convertMailStoreTo(aMailstoreContractId, aServer, aEventTarget) { + let accountRootFolder = aServer.rootFolder.filePath; + + let srcType = null; + let destType = null; + if (aMailstoreContractId == "@mozilla.org/msgstore/maildirstore;1") { + srcType = "maildir"; + destType = "mbox"; + } else { + srcType = "mbox"; + destType = "maildir"; + } + + // Go offline before conversion, so there aren't messages coming in during + // the process. + Services.io.offline = true; + let destDir = createTmpConverterFolder( + accountRootFolder, + aMailstoreContractId + ); + + // Return a promise that will complete once the worker is done. + return new Promise(function (resolve, reject) { + let worker = new ChromeWorker("resource:///modules/converterWorker.js"); + gConverterWorker = worker; + + // Helper to log error, clean up and reject with error message. + let bailout = function (errmsg) { + log.error("bailing out (" + errmsg + ")"); + // Cleanup. + log.info("Trying to remove converter folder: " + destDir.path); + destDir.remove(true); + reject(errmsg); + }; + + // Handle exceptions thrown by the worker thread. + worker.addEventListener("error", function (e) { + // (e is type ErrorEvent) + + // if we're lucky, the error will contain location info + if (e.filename && e.lineno) { + bailout(e.filename + ":" + e.lineno + ": " + e.message); + } else { + bailout(e.message); + } + }); + + // Handle updates from the worker thread. + worker.addEventListener("message", function (e) { + let response = e.data; + // log.debug("WORKER SAYS: " + JSON.stringify(response) + "\n"); + if (response.msg == "progress") { + let val = response.val; + let total = response.total; + + // Send the percentage completion to the GUI. + // XXX TODO: should probably check elapsed time, and throttle + // the events to avoid spending all our time drawing! + let ev = new Event("progress"); + ev.detail = parseInt((val / total) * 100); + if (aEventTarget) { + aEventTarget.dispatchEvent(ev); + } + } + if (response.msg == "success") { + // If we receive this, the worker has completed, without errors. + let storeTypeIDs = { + mbox: "@mozilla.org/msgstore/berkeleystore;1", + maildir: "@mozilla.org/msgstore/maildirstore;1", + }; + let newStoreTypeID = storeTypeIDs[destType]; + + try { + let finalRoot = installNewRoot(aServer, destDir, newStoreTypeID); + log.info( + "Conversion complete. Converted dir installed as: " + finalRoot + ); + resolve(finalRoot); + } catch (e) { + bailout("installNewRoot() failed"); + } + } + }); + + // Kick off the worker. + worker.postMessage({ + srcType, + destType, + srcRoot: accountRootFolder.path, + destRoot: destDir.path, + }); + }); +} + +/** + * Checks if Converter folder exists in tmp dir, removes it and creates a new + * "Converter" folder. + * + * @param {nsIFile} aFolder - account root folder. + * @param {string} aMailstoreContractId - XPCOM id of dest mailstore type + * + * @returns {nsIFile} - the new tmp directory to use as converter dest. + */ +function createTmpConverterFolder(aFolder, aMailstoreContractId) { + let tmpDir = FileUtils.getDir("TmpD", [], false); + let tmpFolder; + switch (aMailstoreContractId) { + case "@mozilla.org/msgstore/maildirstore;1": { + if (aFolder.leafName.substr(-8) == "-maildir") { + tmpFolder = new FileUtils.File( + PathUtils.join( + tmpDir.path, + aFolder.leafName.substr(0, aFolder.leafName.length - 8) + "-mbox" + ) + ); + } else { + tmpFolder = new FileUtils.File( + PathUtils.join(tmpDir.path, aFolder.leafName + "-mbox") + ); + } + + if (tmpFolder.exists()) { + log.info( + "Temporary Converter folder " + + tmpFolder.path + + " exists in tmp dir. Removing it" + ); + tmpFolder.remove(true); + } + return FileUtils.getDir("TmpD", [tmpFolder.leafName], true); + } + + case "@mozilla.org/msgstore/berkeleystore;1": { + if (aFolder.leafName.substr(-5) == "-mbox") { + tmpFolder = new FileUtils.File( + PathUtils.join( + tmpDir.path, + aFolder.leafName.substr(0, aFolder.leafName.length - 5) + "-maildir" + ) + ); + } else { + tmpFolder = new FileUtils.File( + PathUtils.join(tmpDir.path, aFolder.leafName + "-maildir") + ); + } + + if (tmpFolder.exists()) { + log.info( + "Temporary Converter folder " + + tmpFolder.path + + "exists in tmp dir. Removing it" + ); + tmpFolder.remove(true); + } + return FileUtils.getDir("TmpD", [tmpFolder.leafName], true); + } + + default: { + throw new Error( + "Unexpected mailstoreContractId: " + aMailstoreContractId + ); + } + } +} + +/** + * Switch server over to use the newly-converted directory tree. + * Moves the converted directory into an appropriate place for the server. + * + * @param {nsIMsgServer} server - server to migrate. + * @param {string} dir - dir of converted mailstore to install + * (will be moved by this function). + * @param {string} newStoreTypeID - XPCOM id of new mailstore type. + * @returns {string} new location of dir. + */ +function installNewRoot(server, dir, newStoreTypeID) { + let accountRootFolder = server.rootFolder.filePath; + + // Migration is complete, get path of parent of account root + // folder into "parentPath" check if Converter folder already + // exists in "parentPath". If yes, remove it. + let lastSlash = accountRootFolder.path.lastIndexOf("/"); + let parentPath = accountRootFolder.parent.path; + log.info("Path to parent folder of account root folder: " + parentPath); + + let parent = new FileUtils.File(parentPath); + log.info("Path to parent folder of account root folder: " + parent.path); + + let converterFolder = new FileUtils.File( + PathUtils.join(parent.path, dir.leafName) + ); + if (converterFolder.exists()) { + log.info( + "Converter folder exists in " + + parentPath + + ". Removing already existing folder" + ); + converterFolder.remove(true); + } + + // Move Converter folder into the parent of account root folder. + try { + dir.moveTo(parent, dir.leafName); + // {nsIFile} new account root folder. + log.info("Path to new account root folder: " + converterFolder.path); + } catch (e) { + // Cleanup. + log.error(e); + log.error("Trying to remove converter folder: " + converterFolder.path); + converterFolder.remove(true); + throw e; + } + + // If the account is imap then copy the msf file for the original + // root folder and rename the copy with the name of the new root + // folder. + if (server.type != "pop3" && server.type != "none") { + let converterFolderMsf = new FileUtils.File( + PathUtils.join(parent.path, dir.leafName + ".msf") + ); + if (converterFolderMsf.exists()) { + converterFolderMsf.remove(true); + } + + let oldRootFolderMsf = new FileUtils.File( + PathUtils.join(parent.path, accountRootFolder.leafName + ".msf") + ); + if (oldRootFolderMsf.exists()) { + oldRootFolderMsf.copyTo(parent, converterFolderMsf.leafName); + } + } + + if (server.type == "nntp") { + let converterFolderNewsrc = new FileUtils.File( + PathUtils.join(parent.path, "newsrc-" + dir.leafName) + ); + if (converterFolderNewsrc.exists()) { + converterFolderNewsrc.remove(true); + } + let oldNewsrc = new FileUtils.File( + PathUtils.join(parent.path, "newsrc-" + accountRootFolder.leafName) + ); + if (oldNewsrc.exists()) { + oldNewsrc.copyTo(parent, converterFolderNewsrc.leafName); + } + } + + server.rootFolder.filePath = converterFolder; + server.localPath = converterFolder; + log.info("Path to account root folder: " + server.rootFolder.filePath.path); + + // Set various preferences. + let p1 = "mail.server." + server.key + ".directory"; + let p2 = "mail.server." + server.key + ".directory-rel"; + let p3 = "mail.server." + server.key + ".newsrc.file"; + let p4 = "mail.server." + server.key + ".newsrc.file-rel"; + let p5 = "mail.server." + server.key + ".storeContractID"; + + Services.prefs.setCharPref(p1, converterFolder.path); + log.info(p1 + ": " + converterFolder.path); + + // The directory-rel pref is of the form "[ProfD]Mail/pop.gmail.com + // " (pop accounts) or "[ProfD]ImapMail/imap.gmail.com" (imap + // accounts) ie the last slash "/" is followed by the root folder + // name. So, replace the old root folder name that follows the last + // slash with the new root folder name to set the correct value of + // directory-rel pref. + let directoryRel = Services.prefs.getCharPref(p2); + lastSlash = directoryRel.lastIndexOf("/"); + directoryRel = + directoryRel.slice(0, lastSlash) + "/" + converterFolder.leafName; + Services.prefs.setCharPref(p2, directoryRel); + log.info(p2 + ": " + directoryRel); + + if (server.type == "nntp") { + let newNewsrc = FileUtils.File( + PathUtils.join(parent.path, "newsrc-" + converterFolder.leafName) + ); + Services.prefs.setCharPref(p3, newNewsrc.path); + + // The newsrc.file-rel pref is of the form "[ProfD]News/newsrc- + // news.mozilla.org" ie the last slash "/" is followed by the + // newsrc file name. So, replace the old newsrc file name that + // follows the last slash with the new newsrc file name to set + // the correct value of newsrc.file-rel pref. + let newsrcRel = Services.prefs.getCharPref(p4); + lastSlash = newsrcRel.lastIndexOf("/"); + newsrcRel = newsrcRel.slice(0, lastSlash) + "/" + newNewsrc.leafName; + Services.prefs.setCharPref(p4, newsrcRel); + log.info(p4 + ": " + newsrcRel); + } + + Services.prefs.setCharPref(p5, newStoreTypeID); + + Services.prefs.savePrefFile(null); + + return converterFolder.path; +} + +/** + * Terminate any workers involved in the conversion process. + */ +function terminateWorkers() { + // We're only using a single worker right now. + if (gConverterWorker !== null) { + gConverterWorker.terminate(); + gConverterWorker = null; + } +} diff --git a/comm/mailnews/base/src/moz.build b/comm/mailnews/base/src/moz.build new file mode 100644 index 0000000000..8255f50c67 --- /dev/null +++ b/comm/mailnews/base/src/moz.build @@ -0,0 +1,154 @@ +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +EXPORTS += [ + "HeaderReader.h", + "LineReader.h", + "nsImapMoveCoalescer.h", + "nsMailAuthModule.h", + "nsMailChannel.h", + "nsMailDirServiceDefs.h", + "nsMsgCompressIStream.h", + "nsMsgCompressOStream.h", + "nsMsgDBFolder.h", + "nsMsgEnumerator.h", + "nsMsgI18N.h", + "nsMsgIdentity.h", + "nsMsgIncomingServer.h", + "nsMsgKeySet.h", + "nsMsgLineBuffer.h", + "nsMsgMailNewsUrl.h", + "nsMsgProtocol.h", + "nsMsgReadStateTxn.h", + "nsMsgTxn.h", + "nsMsgUtils.h", + "nsNewMailnewsURI.h", + "nsQuarantinedOutputStream.h", + "UrlListener.h", +] + +SOURCES += [ + "MailNewsDLF.cpp", + "MailnewsLoadContextInfo.cpp", + "nsCidProtocolHandler.cpp", + "nsCopyMessageStreamListener.cpp", + "nsImapMoveCoalescer.cpp", + "nsMailAuthModule.cpp", + "nsMailChannel.cpp", + "nsMailDirProvider.cpp", + "nsMessenger.cpp", + "nsMessengerBootstrap.cpp", + "nsMsgAccount.cpp", + "nsMsgAccountManager.cpp", + "nsMsgBiffManager.cpp", + "nsMsgCompressIStream.cpp", + "nsMsgCompressOStream.cpp", + "nsMsgContentPolicy.cpp", + "nsMsgCopyService.cpp", + "nsMsgDBFolder.cpp", + "nsMsgDBView.cpp", + "nsMsgEnumerator.cpp", + "nsMsgFileStream.cpp", + "nsMsgFolderCache.cpp", + "nsMsgFolderCompactor.cpp", + "nsMsgFolderNotificationService.cpp", + "nsMsgGroupThread.cpp", + "nsMsgGroupView.cpp", + "nsMsgI18N.cpp", + "nsMsgIdentity.cpp", + "nsMsgIncomingServer.cpp", + "nsMsgKeySet.cpp", + "nsMsgLineBuffer.cpp", + "nsMsgMailNewsUrl.cpp", + "nsMsgMailSession.cpp", + "nsMsgOfflineManager.cpp", + "nsMsgProgress.cpp", + "nsMsgProtocol.cpp", + "nsMsgPurgeService.cpp", + "nsMsgQuickSearchDBView.cpp", + "nsMsgReadStateTxn.cpp", + "nsMsgSearchDBView.cpp", + "nsMsgSpecialViews.cpp", + "nsMsgStatusFeedback.cpp", + "nsMsgTagService.cpp", + "nsMsgThreadedDBView.cpp", + "nsMsgTxn.cpp", + "nsMsgUtils.cpp", + "nsMsgWindow.cpp", + "nsMsgXFViewThread.cpp", + "nsMsgXFVirtualFolderDBView.cpp", + "nsNewMailnewsURI.cpp", + "nsQuarantinedOutputStream.cpp", + "nsSpamSettings.cpp", + "nsStatusBarBiffManager.cpp", + "nsStopwatch.cpp", + "nsSubscribableServer.cpp", + "UrlListener.cpp", +] + +if CONFIG["OS_ARCH"] == "WINNT": + SOURCES += [ + "nsMessengerWinIntegration.cpp", + # This file cannot be built in unified mode because of name clashes with Windows headers. + "nsUserInfoWin.cpp", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk": + SOURCES += [ + "nsMessengerUnixIntegration.cpp", + "nsUserInfoUnix.cpp", + ] +elif CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa": + SOURCES += [ + "nsMessengerOSXIntegration.mm", + "nsUserInfoMac.mm", + ] + +EXTRA_JS_MODULES += [ + "ABQueryUtils.jsm", + "converterWorker.js", + "FolderLookupService.jsm", + "FolderUtils.jsm", + "hostnameUtils.jsm", + "JXON.jsm", + "LineReader.jsm", + "MailAuthenticator.jsm", + "MailChannel.sys.mjs", + "MailCryptoUtils.jsm", + "MailnewsMigrator.jsm", + "MailNotificationManager.jsm", + "MailNotificationService.jsm", + "MailServices.jsm", + "mailstoreConverter.jsm", + "MailStringUtils.jsm", + "MsgAsyncPrompter.jsm", + "MsgDBCacheManager.jsm", + "MsgIncomingServer.jsm", + "MsgKeySet.jsm", + "MsgProtocolInfo.sys.mjs", + "OAuth2.jsm", + "OAuth2Module.jsm", + "OAuth2Providers.jsm", + "TemplateUtils.jsm", + "VirtualFolderWrapper.jsm", + "WinUnreadBadge.jsm", +] + +USE_LIBS += [ + "jsoncpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/netwerk/base", + "/toolkit/components/jsoncpp/include", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "mail" + +XPCOM_MANIFESTS += [ + "components.conf", +] diff --git a/comm/mailnews/base/src/nsCidProtocolHandler.cpp b/comm/mailnews/base/src/nsCidProtocolHandler.cpp new file mode 100644 index 0000000000..50f14bd782 --- /dev/null +++ b/comm/mailnews/base/src/nsCidProtocolHandler.cpp @@ -0,0 +1,49 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCidProtocolHandler.h" +#include "nsString.h" +#include "nsIURI.h" +#include "nsNetCID.h" +#include "nsNetUtil.h" +#include "nsComponentManagerUtils.h" + +nsCidProtocolHandler::nsCidProtocolHandler() {} + +nsCidProtocolHandler::~nsCidProtocolHandler() {} + +NS_IMPL_ISUPPORTS(nsCidProtocolHandler, nsIProtocolHandler) + +NS_IMETHODIMP nsCidProtocolHandler::GetScheme(nsACString& aScheme) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsCidProtocolHandler::NewURI(const nsACString& aSpec, + const char* aOriginCharset, + nsIURI* aBaseURI, nsIURI** _retval) { + // the right fix is to use the baseSpec (or aBaseUri) + // and specify the cid, and then fix mime + // to handle that, like it does with "...&part=1.2" + // for now, do about blank to prevent spam + // from popping up annoying alerts about not implementing the cid + // protocol + nsCOMPtr<nsIURI> url; + nsresult rv = NS_NewURI(getter_AddRefs(url), "about:blank"); + NS_ENSURE_SUCCESS(rv, rv); + + url.forget(_retval); + return NS_OK; +} + +NS_IMETHODIMP nsCidProtocolHandler::NewChannel(nsIURI* aURI, + nsILoadInfo* aLoadInfo, + nsIChannel** _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsCidProtocolHandler::AllowPort(int32_t port, const char* scheme, + bool* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/comm/mailnews/base/src/nsCidProtocolHandler.h b/comm/mailnews/base/src/nsCidProtocolHandler.h new file mode 100644 index 0000000000..9d84812ee7 --- /dev/null +++ b/comm/mailnews/base/src/nsCidProtocolHandler.h @@ -0,0 +1,25 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsCidProtocolHandler_h__ +#define nsCidProtocolHandler_h__ + +#include "nsCOMPtr.h" +#include "nsIProtocolHandler.h" + +class nsCidProtocolHandler : public nsIProtocolHandler { + public: + nsCidProtocolHandler(); + static nsresult NewURI(const nsACString& aSpec, const char* aOriginCharset, + nsIURI* aBaseURI, nsIURI** _retval); + + NS_DECL_ISUPPORTS + NS_DECL_NSIPROTOCOLHANDLER + + private: + virtual ~nsCidProtocolHandler(); +}; + +#endif /* nsCidProtocolHandler_h__ */ diff --git a/comm/mailnews/base/src/nsCopyMessageStreamListener.cpp b/comm/mailnews/base/src/nsCopyMessageStreamListener.cpp new file mode 100644 index 0000000000..343c27ac54 --- /dev/null +++ b/comm/mailnews/base/src/nsCopyMessageStreamListener.cpp @@ -0,0 +1,106 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsCopyMessageStreamListener.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIMsgImapMailFolder.h" +#include "nsIChannel.h" +#include "nsIURI.h" + +NS_IMPL_ISUPPORTS(nsCopyMessageStreamListener, nsIStreamListener, + nsIRequestObserver, nsICopyMessageStreamListener) + +nsCopyMessageStreamListener::nsCopyMessageStreamListener() {} + +nsCopyMessageStreamListener::~nsCopyMessageStreamListener() { + // All member variables are nsCOMPtr's. +} + +NS_IMETHODIMP nsCopyMessageStreamListener::Init( + nsICopyMessageListener* destination) { + mDestination = destination; + return NS_OK; +} + +NS_IMETHODIMP nsCopyMessageStreamListener::StartMessage() { + if (mDestination) { + return mDestination->StartMessage(); + } + return NS_OK; +} + +NS_IMETHODIMP nsCopyMessageStreamListener::EndMessage(nsMsgKey key) { + if (mDestination) { + return mDestination->EndMessage(key); + } + return NS_OK; +} + +NS_IMETHODIMP nsCopyMessageStreamListener::OnDataAvailable( + nsIRequest* /* request */, nsIInputStream* aIStream, uint64_t sourceOffset, + uint32_t aLength) { + return mDestination->CopyData(aIStream, aLength); +} + +NS_IMETHODIMP nsCopyMessageStreamListener::OnStartRequest(nsIRequest* request) { + nsresult rv = NS_OK; + nsCOMPtr<nsIURI> uri; + + // We know the request is an nsIChannel we can get a URI from, but this is + // probably bad form. See Bug 1528662. + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "error QI nsIRequest to nsIChannel failed"); + if (NS_SUCCEEDED(rv)) rv = channel->GetURI(getter_AddRefs(uri)); + if (NS_SUCCEEDED(rv)) rv = mDestination->BeginCopy(); + + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +NS_IMETHODIMP nsCopyMessageStreamListener::EndCopy(nsIURI* uri, + nsresult status) { + nsresult rv; + bool copySucceeded = (status == NS_BINDING_SUCCEEDED); + rv = mDestination->EndCopy(copySucceeded); + // If this is a move and we finished the copy, delete the old message. + bool moveMessage = false; + + nsCOMPtr<nsIMsgMailNewsUrl> mailURL(do_QueryInterface(uri)); + if (mailURL) rv = mailURL->IsUrlType(nsIMsgMailNewsUrl::eMove, &moveMessage); + + if (NS_FAILED(rv)) moveMessage = false; + + // OK, this is wrong if we're moving to an imap folder, for example. This + // really says that we were able to pull the message from the source, NOT that + // we were able to put it in the destination! + if (moveMessage) { + // don't do this if we're moving to an imap folder - that's handled + // elsewhere. + nsCOMPtr<nsIMsgImapMailFolder> destImap = do_QueryInterface(mDestination); + // if the destination is a local folder, it will handle the delete from the + // source in EndMove + if (!destImap) rv = mDestination->EndMove(copySucceeded); + } + // Even if the above actions failed we probably still want to return NS_OK. + // There should probably be some error dialog if either the copy or delete + // failed. + return NS_OK; +} + +NS_IMETHODIMP nsCopyMessageStreamListener::OnStopRequest(nsIRequest* request, + nsresult aStatus) { + nsresult rv; + // We know the request is an nsIChannel we can get a URI from, but this is + // probably bad form. See Bug 1528662. + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "error QI nsIRequest to nsIChannel failed"); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> uri; + rv = channel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + return EndCopy(uri, aStatus); +} diff --git a/comm/mailnews/base/src/nsCopyMessageStreamListener.h b/comm/mailnews/base/src/nsCopyMessageStreamListener.h new file mode 100644 index 0000000000..509dcef019 --- /dev/null +++ b/comm/mailnews/base/src/nsCopyMessageStreamListener.h @@ -0,0 +1,29 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NSCOPYMESSAGESTREAMLISTENER_H +#define NSCOPYMESSAGESTREAMLISTENER_H + +#include "nsICopyMessageStreamListener.h" +#include "nsIStreamListener.h" +#include "nsICopyMessageListener.h" +#include "nsCOMPtr.h" + +class nsCopyMessageStreamListener : public nsIStreamListener, + public nsICopyMessageStreamListener { + public: + nsCopyMessageStreamListener(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSICOPYMESSAGESTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + protected: + virtual ~nsCopyMessageStreamListener(); + nsCOMPtr<nsICopyMessageListener> mDestination; +}; + +#endif diff --git a/comm/mailnews/base/src/nsImapMoveCoalescer.cpp b/comm/mailnews/base/src/nsImapMoveCoalescer.cpp new file mode 100644 index 0000000000..160f204a61 --- /dev/null +++ b/comm/mailnews/base/src/nsImapMoveCoalescer.cpp @@ -0,0 +1,198 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsImapMoveCoalescer.h" +#include "nsIImapService.h" +#include "nsIMsgCopyService.h" +#include "nsIMsgFolder.h" // TO include biffState enum. Change to bool later... +#include "nsMsgFolderFlags.h" +#include "nsIMsgHdr.h" +#include "nsIMsgImapMailFolder.h" +#include "nsThreadUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "mozilla/ArrayUtils.h" + +NS_IMPL_ISUPPORTS(nsImapMoveCoalescer, nsIUrlListener) + +nsImapMoveCoalescer::nsImapMoveCoalescer(nsIMsgFolder* sourceFolder, + nsIMsgWindow* msgWindow) { + m_sourceFolder = sourceFolder; + m_msgWindow = msgWindow; + m_hasPendingMoves = false; +} + +nsImapMoveCoalescer::~nsImapMoveCoalescer() {} + +nsresult nsImapMoveCoalescer::AddMove(nsIMsgFolder* folder, nsMsgKey key) { + m_hasPendingMoves = true; + int32_t folderIndex = m_destFolders.IndexOf(folder); + nsTArray<nsMsgKey>* keysToAdd = nullptr; + + if (folderIndex >= 0) + keysToAdd = &(m_sourceKeyArrays[folderIndex]); + else { + m_destFolders.AppendObject(folder); + keysToAdd = m_sourceKeyArrays.AppendElement(); + if (!keysToAdd) return NS_ERROR_OUT_OF_MEMORY; + } + + if (!keysToAdd->Contains(key)) keysToAdd->AppendElement(key); + + return NS_OK; +} + +nsresult nsImapMoveCoalescer::PlaybackMoves( + bool doNewMailNotification /* = false */) { + int32_t numFolders = m_destFolders.Count(); + // Nothing to do, so don't change the member variables. + if (numFolders == 0) return NS_OK; + + nsresult rv = NS_OK; + m_hasPendingMoves = false; + m_doNewMailNotification = doNewMailNotification; + m_outstandingMoves = 0; + + for (int32_t i = 0; i < numFolders; ++i) { + // XXX TODO + // JUNK MAIL RELATED + // is this the right place to make sure dest folder exists + // (and has proper flags?), before we start copying? + nsCOMPtr<nsIMsgFolder> destFolder(m_destFolders[i]); + nsTArray<nsMsgKey>& keysToAdd = m_sourceKeyArrays[i]; + int32_t numNewMessages = 0; + int32_t numKeysToAdd = keysToAdd.Length(); + if (numKeysToAdd == 0) continue; + + nsTArray<RefPtr<nsIMsgDBHdr>> messages(keysToAdd.Length()); + for (uint32_t keyIndex = 0; keyIndex < keysToAdd.Length(); keyIndex++) { + nsCOMPtr<nsIMsgDBHdr> mailHdr = nullptr; + rv = m_sourceFolder->GetMessageHeader(keysToAdd.ElementAt(keyIndex), + getter_AddRefs(mailHdr)); + if (NS_SUCCEEDED(rv) && mailHdr) { + messages.AppendElement(mailHdr); + bool isRead = false; + mailHdr->GetIsRead(&isRead); + if (!isRead) numNewMessages++; + } + } + uint32_t destFlags; + destFolder->GetFlags(&destFlags); + if (!(destFlags & + nsMsgFolderFlags::Junk)) // don't set has new on junk folder + { + destFolder->SetNumNewMessages(numNewMessages); + if (numNewMessages > 0) destFolder->SetHasNewMessages(true); + } + // adjust the new message count on the source folder + int32_t oldNewMessageCount = 0; + m_sourceFolder->GetNumNewMessages(false, &oldNewMessageCount); + if (oldNewMessageCount >= numKeysToAdd) + oldNewMessageCount -= numKeysToAdd; + else + oldNewMessageCount = 0; + + m_sourceFolder->SetNumNewMessages(oldNewMessageCount); + + keysToAdd.Clear(); + nsCOMPtr<nsIMsgCopyService> copySvc = + do_GetService("@mozilla.org/messenger/messagecopyservice;1"); + if (copySvc) { + nsCOMPtr<nsIMsgCopyServiceListener> listener; + if (m_doNewMailNotification) { + nsMoveCoalescerCopyListener* copyListener = + new nsMoveCoalescerCopyListener(this, destFolder); + if (copyListener) listener = copyListener; + } + rv = copySvc->CopyMessages(m_sourceFolder, messages, destFolder, true, + listener, m_msgWindow, false /*allowUndo*/); + if (NS_SUCCEEDED(rv)) m_outstandingMoves++; + } + } + return rv; +} + +NS_IMETHODIMP +nsImapMoveCoalescer::OnStartRunningUrl(nsIURI* aUrl) { + NS_ASSERTION(aUrl, "just a sanity check"); + return NS_OK; +} + +NS_IMETHODIMP +nsImapMoveCoalescer::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) { + m_outstandingMoves--; + if (m_doNewMailNotification && !m_outstandingMoves) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = + do_QueryInterface(m_sourceFolder); + if (imapFolder) imapFolder->NotifyIfNewMail(); + } + return NS_OK; +} + +nsTArray<nsMsgKey>* nsImapMoveCoalescer::GetKeyBucket(uint32_t keyArrayIndex) { + NS_ASSERTION(keyArrayIndex < MOZ_ARRAY_LENGTH(m_keyBuckets), "invalid index"); + + return keyArrayIndex < mozilla::ArrayLength(m_keyBuckets) + ? &(m_keyBuckets[keyArrayIndex]) + : nullptr; +} + +NS_IMPL_ISUPPORTS(nsMoveCoalescerCopyListener, nsIMsgCopyServiceListener) + +nsMoveCoalescerCopyListener::nsMoveCoalescerCopyListener( + nsImapMoveCoalescer* coalescer, nsIMsgFolder* destFolder) { + m_destFolder = destFolder; + m_coalescer = coalescer; +} + +nsMoveCoalescerCopyListener::~nsMoveCoalescerCopyListener() {} + +NS_IMETHODIMP nsMoveCoalescerCopyListener::OnStartCopy() { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void OnProgress (in uint32_t aProgress, in uint32_t aProgressMax); */ +NS_IMETHODIMP nsMoveCoalescerCopyListener::OnProgress(uint32_t aProgress, + uint32_t aProgressMax) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void SetMessageKey (in uint32_t aKey); */ +NS_IMETHODIMP nsMoveCoalescerCopyListener::SetMessageKey(uint32_t aKey) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void GetMessageId (in nsACString aMessageId); */ +NS_IMETHODIMP nsMoveCoalescerCopyListener::GetMessageId(nsACString& messageId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void OnStopCopy (in nsresult aStatus); */ +NS_IMETHODIMP nsMoveCoalescerCopyListener::OnStopCopy(nsresult aStatus) { + nsresult rv = NS_OK; + if (NS_SUCCEEDED(aStatus)) { + // if the dest folder is imap, update it. + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_destFolder); + if (imapFolder) { + uint32_t folderFlags; + m_destFolder->GetFlags(&folderFlags); + if (!(folderFlags & (nsMsgFolderFlags::Junk | nsMsgFolderFlags::Trash))) { + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> url; + rv = imapService->SelectFolder(m_destFolder, m_coalescer, nullptr, + getter_AddRefs(url)); + } + } else // give junk filters a chance to run on new msgs in destination + // local folder + { + bool filtersRun; + m_destFolder->CallFilterPlugins(nullptr, &filtersRun); + } + } + return rv; +} diff --git a/comm/mailnews/base/src/nsImapMoveCoalescer.h b/comm/mailnews/base/src/nsImapMoveCoalescer.h new file mode 100644 index 0000000000..5471c4bd88 --- /dev/null +++ b/comm/mailnews/base/src/nsImapMoveCoalescer.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsImapMoveCoalescer_H +#define _nsImapMoveCoalescer_H + +#include "msgCore.h" +#include "nsCOMArray.h" +#include "nsIMsgWindow.h" +#include "nsCOMPtr.h" +#include "MailNewsTypes.h" +#include "nsTArray.h" +#include "nsIUrlListener.h" +#include "nsIMsgCopyServiceListener.h" + +// imap move coalescer class - in order to keep nsImapMailFolder from growing +// like Topsy Logically, we want to keep track of an nsTArray<nsMsgKey> per +// nsIMsgFolder, and then be able to retrieve them one by one and play back the +// moves. This utility class will be used by both the filter code and the +// offline playback code, to avoid multiple moves to the same folder. + +class nsImapMoveCoalescer : public nsIUrlListener { + public: + friend class nsMoveCoalescerCopyListener; + + NS_DECL_ISUPPORTS + NS_DECL_NSIURLLISTENER + + nsImapMoveCoalescer(nsIMsgFolder* sourceFolder, nsIMsgWindow* msgWindow); + + nsresult AddMove(nsIMsgFolder* folder, nsMsgKey key); + nsresult PlaybackMoves(bool doNewMailNotification = false); + // this lets the caller store keys in an arbitrary number of buckets. If the + // bucket for the passed in index doesn't exist, it will get created. + nsTArray<nsMsgKey>* GetKeyBucket(uint32_t keyArrayIndex); + nsIMsgWindow* GetMsgWindow() { return m_msgWindow; } + bool HasPendingMoves() { return m_hasPendingMoves; } + + protected: + virtual ~nsImapMoveCoalescer(); + // m_sourceKeyArrays and m_destFolders are parallel arrays. + nsTArray<nsTArray<nsMsgKey> > m_sourceKeyArrays; + nsCOMArray<nsIMsgFolder> m_destFolders; + nsCOMPtr<nsIMsgWindow> m_msgWindow; + nsCOMPtr<nsIMsgFolder> m_sourceFolder; + bool m_doNewMailNotification; + bool m_hasPendingMoves; + nsTArray<nsMsgKey> m_keyBuckets[2]; + int32_t m_outstandingMoves; +}; + +class nsMoveCoalescerCopyListener final : public nsIMsgCopyServiceListener { + public: + nsMoveCoalescerCopyListener(nsImapMoveCoalescer* coalescer, + nsIMsgFolder* destFolder); + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGCOPYSERVICELISTENER + + nsCOMPtr<nsIMsgFolder> m_destFolder; + + nsImapMoveCoalescer* m_coalescer; + // when we get OnStopCopy, update the folder. When we've finished all the + // copies, send the biff notification. + + private: + ~nsMoveCoalescerCopyListener(); +}; + +#endif // _nsImapMoveCoalescer_H diff --git a/comm/mailnews/base/src/nsMailAuthModule.cpp b/comm/mailnews/base/src/nsMailAuthModule.cpp new file mode 100644 index 0000000000..69089737df --- /dev/null +++ b/comm/mailnews/base/src/nsMailAuthModule.cpp @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIAuthModule.h" +#include "nsIMailAuthModule.h" +#include "nsMailAuthModule.h" +#include "nsString.h" +#include "plbase64.h" + +NS_IMPL_ISUPPORTS(nsMailAuthModule, nsIMailAuthModule) + +nsMailAuthModule::nsMailAuthModule() {} + +nsMailAuthModule::~nsMailAuthModule() {} + +/** + * A simple wrap of CreateInstance and Init of nsIAuthModule. + */ +NS_IMETHODIMP +nsMailAuthModule::Init(const char* type, const nsACString& serviceName, + uint32_t serviceFlags, const nsAString& domain, + const nsAString& username, const nsAString& password) { + mAuthModule = nsIAuthModule::CreateInstance(type); + return mAuthModule->Init(serviceName, serviceFlags, domain, username, + password); +} + +/** + * A wrap of nsIAuthModule::GetNextToken with two extra processings: + * 1. inToken is base64 decoded then passed to nsIAuthModule::GetNextToken. + * 2. The out value of nsIAuthModule::GetNextToken is base64 encoded then + * assigned to outToken. + */ +NS_IMETHODIMP +nsMailAuthModule::GetNextToken(const nsACString& inToken, + nsACString& outToken) { + nsresult rv; + void *inBuf, *outBuf; + uint32_t inBufLen = 0, outBufLen = 0; + uint32_t len = inToken.Length(); + if (len > 0) { + // Decode into the input buffer. + inBufLen = (len * 3) / 4; // sufficient size (see plbase64.h) + inBuf = moz_xmalloc(inBufLen); + + // Strip off any padding (see bug 230351). + char* challenge = ToNewCString(inToken); + while (challenge[len - 1] == '=') len--; + + // We need to know the exact length of the decoded string to give to + // the GSSAPI libraries. But NSPR's base64 routine doesn't seem capable + // of telling us that. So, we figure it out for ourselves. + + // For every 4 characters, add 3 to the destination + // If there are 3 remaining, add 2 + // If there are 2 remaining, add 1 + // 1 remaining is an error + inBufLen = + (len / 4) * 3 + ((len % 4 == 3) ? 2 : 0) + ((len % 4 == 2) ? 1 : 0); + PL_Base64Decode(challenge, len, (char*)inBuf); + free(challenge); + } else { + inBufLen = 0; + inBuf = NULL; + } + + rv = mAuthModule->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen); + free(inBuf); + NS_ENSURE_SUCCESS(rv, rv); + + // It's not an error if outBuf is empty, return an empty string as reply to + // the server. + if (outBuf) { + char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr); + if (base64Str) { + outToken.Adopt(base64Str); + } else { + rv = NS_ERROR_OUT_OF_MEMORY; + } + free(outBuf); + } + + return rv; +} diff --git a/comm/mailnews/base/src/nsMailAuthModule.h b/comm/mailnews/base/src/nsMailAuthModule.h new file mode 100644 index 0000000000..e6bca30623 --- /dev/null +++ b/comm/mailnews/base/src/nsMailAuthModule.h @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMailAuthModule_h__ +#define nsMailAuthModule_h__ + +#include "nsCOMPtr.h" +#include "nsIAuthModule.h" +#include "nsIMailAuthModule.h" + +class nsMailAuthModule : public nsIMailAuthModule { + public: + nsMailAuthModule(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMAILAUTHMODULE + + protected: + virtual ~nsMailAuthModule(); + + private: + nsCOMPtr<nsIAuthModule> mAuthModule; +}; + +#endif /* nsMailAuthModule_h__ */ diff --git a/comm/mailnews/base/src/nsMailChannel.cpp b/comm/mailnews/base/src/nsMailChannel.cpp new file mode 100644 index 0000000000..1e427718e0 --- /dev/null +++ b/comm/mailnews/base/src/nsMailChannel.cpp @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMailChannel.h" +#include "nsHashPropertyBag.h" +#include "nsServiceManagerUtils.h" +#include "nsICharsetConverterManager.h" + +NS_IMETHODIMP +nsMailChannel::AddHeaderFromMIME(const nsACString& name, + const nsACString& value) { + mHeaderNames.AppendElement(name); + mHeaderValues.AppendElement(value); + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::GetHeaderNames(nsTArray<nsCString>& aHeaderNames) { + aHeaderNames = mHeaderNames.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::GetHeaderValues(nsTArray<nsCString>& aHeaderValues) { + aHeaderValues = mHeaderValues.Clone(); + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::HandleAttachmentFromMIME(const nsACString& contentType, + const nsACString& url, + const nsACString& displayName, + const nsACString& uri, + bool notDownloaded) { + RefPtr<nsIWritablePropertyBag2> attachment = new nsHashPropertyBag(); + attachment->SetPropertyAsAUTF8String(u"contentType"_ns, contentType); + attachment->SetPropertyAsAUTF8String(u"url"_ns, url); + attachment->SetPropertyAsAUTF8String(u"displayName"_ns, displayName); + attachment->SetPropertyAsAUTF8String(u"uri"_ns, uri); + attachment->SetPropertyAsBool(u"notDownloaded"_ns, notDownloaded); + mAttachments.AppendElement(attachment); + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::AddAttachmentFieldFromMIME(const nsACString& field, + const nsACString& value) { + nsIWritablePropertyBag2* attachment = mAttachments.LastElement(); + attachment->SetPropertyAsAUTF8String(NS_ConvertUTF8toUTF16(nsCString(field)), + value); + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::GetAttachments( + nsTArray<RefPtr<nsIPropertyBag2> >& aAttachments) { + aAttachments.Clear(); + for (nsIWritablePropertyBag2* attachment : mAttachments) { + aAttachments.AppendElement(static_cast<nsIPropertyBag2*>(attachment)); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::GetMailCharacterSet(nsACString& aMailCharacterSet) { + aMailCharacterSet = mMailCharacterSet; + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::SetMailCharacterSet(const nsACString& aMailCharacterSet) { + mMailCharacterSet = aMailCharacterSet; + + // Convert to a canonical charset name instead of using the charset name from + // the message header as is. This is needed for charset menu item to have a + // check mark correctly. + nsresult rv; + nsCOMPtr<nsICharsetConverterManager> ccm = + do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return ccm->GetCharsetAlias(PromiseFlatCString(aMailCharacterSet).get(), + mMailCharacterSet); +} + +NS_IMETHODIMP +nsMailChannel::GetImipMethod(nsACString& aImipMethod) { + aImipMethod = mImipMethod; + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::SetImipMethod(const nsACString& aImipMethod) { + mImipMethod = aImipMethod; + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::GetImipItem(calIItipItem** aImipItem) { + NS_IF_ADDREF(*aImipItem = mImipItem); + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::SetImipItem(calIItipItem* aImipItem) { + mImipItem = aImipItem; + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::GetSmimeHeaderSink(nsIMsgSMIMEHeaderSink** aSmimeHeaderSink) { + NS_IF_ADDREF(*aSmimeHeaderSink = mSmimeHeaderSink); + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::SetSmimeHeaderSink(nsIMsgSMIMEHeaderSink* aSmimeHeaderSink) { + mSmimeHeaderSink = aSmimeHeaderSink; + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::GetListener(nsIMailProgressListener** aListener) { + nsCOMPtr<nsIMailProgressListener> listener = do_QueryReferent(mListener); + if (listener) { + NS_IF_ADDREF(*aListener = listener); + } else { + *aListener = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMailChannel::SetListener(nsIMailProgressListener* aListener) { + nsresult rv; + mListener = do_GetWeakReference(aListener, &rv); + return rv; +} diff --git a/comm/mailnews/base/src/nsMailChannel.h b/comm/mailnews/base/src/nsMailChannel.h new file mode 100644 index 0000000000..91cf59705a --- /dev/null +++ b/comm/mailnews/base/src/nsMailChannel.h @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMailChannel_h__ +#define nsMailChannel_h__ + +#include "nsIMailChannel.h" +#include "nsIWritablePropertyBag2.h" +#include "nsTArray.h" +#include "nsTString.h" +#include "calIItipItem.h" +#include "nsIWeakReferenceUtils.h" + +class nsMailChannel : public nsIMailChannel { + public: + NS_DECL_NSIMAILCHANNEL + + protected: + nsTArray<nsCString> mHeaderNames; + nsTArray<nsCString> mHeaderValues; + nsTArray<RefPtr<nsIWritablePropertyBag2>> mAttachments; + nsCString mMailCharacterSet; + nsCString mImipMethod; + nsCOMPtr<calIItipItem> mImipItem; + nsCOMPtr<nsIMsgSMIMEHeaderSink> mSmimeHeaderSink; + nsWeakPtr mListener; +}; + +#endif /* nsMailChannel_h__ */ diff --git a/comm/mailnews/base/src/nsMailDirProvider.cpp b/comm/mailnews/base/src/nsMailDirProvider.cpp new file mode 100644 index 0000000000..5d092203cf --- /dev/null +++ b/comm/mailnews/base/src/nsMailDirProvider.cpp @@ -0,0 +1,160 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMailDirProvider.h" +#include "nsMailDirServiceDefs.h" +#include "nsXULAppAPI.h" +#include "nsCOMArray.h" +#include "nsEnumeratorUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsIChromeRegistry.h" +#include "nsICategoryManager.h" +#include "nsServiceManagerUtils.h" +#include "nsDirectoryServiceUtils.h" + +#define MAIL_DIR_50_NAME "Mail" +#define IMAP_MAIL_DIR_50_NAME "ImapMail" +#define NEWS_DIR_50_NAME "News" + +nsresult nsMailDirProvider::EnsureDirectory(nsIFile* aDirectory) { + bool exists; + nsresult rv = aDirectory->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + + if (!exists) rv = aDirectory->Create(nsIFile::DIRECTORY_TYPE, 0700); + + return rv; +} + +NS_IMPL_ISUPPORTS(nsMailDirProvider, nsIDirectoryServiceProvider, + nsIDirectoryServiceProvider2) + +NS_IMETHODIMP +nsMailDirProvider::GetFile(const char* aKey, bool* aPersist, + nsIFile** aResult) { + // NOTE: This function can be reentrant through the NS_GetSpecialDirectory + // call, so be careful not to cause infinite recursion. + // i.e. the check for supported files must come first. + const char* leafName = nullptr; + bool isDirectory = true; + + if (!strcmp(aKey, NS_APP_MAIL_50_DIR)) { + leafName = MAIL_DIR_50_NAME; + } else if (!strcmp(aKey, NS_APP_IMAP_MAIL_50_DIR)) { + leafName = IMAP_MAIL_DIR_50_NAME; + } else if (!strcmp(aKey, NS_APP_NEWS_50_DIR)) { + leafName = NEWS_DIR_50_NAME; + } else if (!strcmp(aKey, NS_APP_MESSENGER_FOLDER_CACHE_50_FILE)) { + isDirectory = false; + leafName = "folderCache.json"; + } else if (!strcmp(aKey, NS_APP_MESSENGER_LEGACY_FOLDER_CACHE_50_FILE)) { + isDirectory = false; + leafName = "panacea.dat"; + } else { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIFile> parentDir; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(parentDir)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIFile> file; + rv = parentDir->Clone(getter_AddRefs(file)); + if (NS_FAILED(rv)) return rv; + + nsDependentCString leafStr(leafName); + rv = file->AppendNative(leafStr); + if (NS_FAILED(rv)) return rv; + + bool exists; + if (isDirectory && NS_SUCCEEDED(file->Exists(&exists)) && !exists) + rv = EnsureDirectory(file); + + *aPersist = true; + file.forget(aResult); + + return rv; +} + +NS_IMETHODIMP +nsMailDirProvider::GetFiles(const char* aKey, nsISimpleEnumerator** aResult) { + if (strcmp(aKey, ISP_DIRECTORY_LIST) != 0) return NS_ERROR_FAILURE; + + // The list of isp directories includes the isp directory + // in the current process dir (i.e. <path to thunderbird.exe>\isp and + // <path to thunderbird.exe>\isp\locale + // and isp and isp\locale for each active extension + + nsCOMPtr<nsIProperties> dirSvc = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID); + if (!dirSvc) return NS_ERROR_FAILURE; + + nsCOMPtr<nsIFile> currentProcessDir; + nsresult rv = dirSvc->Get(NS_XPCOM_CURRENT_PROCESS_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(currentProcessDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISimpleEnumerator> directoryEnumerator; + rv = NS_NewSingletonEnumerator(getter_AddRefs(directoryEnumerator), + currentProcessDir); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ADDREF(*aResult = new AppendingEnumerator(directoryEnumerator)); + return NS_SUCCESS_AGGREGATE_RESULT; +} + +NS_IMETHODIMP +nsMailDirProvider::AppendingEnumerator::HasMoreElements(bool* aResult) { + *aResult = mNext || mNextWithLocale ? true : false; + return NS_OK; +} + +NS_IMETHODIMP +nsMailDirProvider::AppendingEnumerator::GetNext(nsISupports** aResult) { + // Set the return value to the next directory we want to enumerate over + if (aResult) NS_ADDREF(*aResult = mNext); + + if (mNextWithLocale) { + mNext = mNextWithLocale; + mNextWithLocale = nullptr; + return NS_OK; + } + + mNext = nullptr; + + // Ignore all errors + + bool more; + while (NS_SUCCEEDED(mBase->HasMoreElements(&more)) && more) { + nsCOMPtr<nsISupports> nextbasesupp; + mBase->GetNext(getter_AddRefs(nextbasesupp)); + + nsCOMPtr<nsIFile> nextbase(do_QueryInterface(nextbasesupp)); + if (!nextbase) continue; + + nextbase->Clone(getter_AddRefs(mNext)); + if (!mNext) continue; + + mNext->AppendNative("isp"_ns); + bool exists; + nsresult rv = mNext->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) { + break; + } + + mNext = nullptr; + } + + return NS_OK; +} + +nsMailDirProvider::AppendingEnumerator::AppendingEnumerator( + nsISimpleEnumerator* aBase) + : mBase(aBase) { + // Initialize mNext to begin + GetNext(nullptr); +} diff --git a/comm/mailnews/base/src/nsMailDirProvider.h b/comm/mailnews/base/src/nsMailDirProvider.h new file mode 100644 index 0000000000..e67c87a9e3 --- /dev/null +++ b/comm/mailnews/base/src/nsMailDirProvider.h @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMailDirProvider_h__ +#define nsMailDirProvider_h__ + +#include "nsIDirectoryService.h" +#include "nsSimpleEnumerator.h" +#include "nsString.h" +#include "nsCOMPtr.h" +#include "nsIFile.h" + +class nsMailDirProvider final : public nsIDirectoryServiceProvider2 { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIDIRECTORYSERVICEPROVIDER + NS_DECL_NSIDIRECTORYSERVICEPROVIDER2 + + private: + ~nsMailDirProvider() {} + + nsresult EnsureDirectory(nsIFile* aDirectory); + + class AppendingEnumerator final : public nsSimpleEnumerator { + public: + const nsID& DefaultInterface() override { return NS_GET_IID(nsIFile); } + + NS_DECL_NSISIMPLEENUMERATOR + + explicit AppendingEnumerator(nsISimpleEnumerator* aBase); + + private: + ~AppendingEnumerator() override = default; + nsCOMPtr<nsISimpleEnumerator> mBase; + nsCOMPtr<nsIFile> mNext; + nsCOMPtr<nsIFile> mNextWithLocale; + }; +}; + +#endif // nsMailDirProvider_h__ diff --git a/comm/mailnews/base/src/nsMailDirServiceDefs.h b/comm/mailnews/base/src/nsMailDirServiceDefs.h new file mode 100644 index 0000000000..3753c96782 --- /dev/null +++ b/comm/mailnews/base/src/nsMailDirServiceDefs.h @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMailDirectoryServiceDefs_h___ +#define nsMailDirectoryServiceDefs_h___ + +//============================================================================= +// +// Defines property names for directories available from the mail-specific +// nsMailDirProvider. +// +// System and XPCOM properties are defined in nsDirectoryServiceDefs.h. +// General application properties are defined in nsAppDirectoryServiceDefs.h. +// +//============================================================================= + +// ---------------------------------------------------------------------------- +// Files and directories that exist on a per-profile basis. +// ---------------------------------------------------------------------------- + +#define NS_APP_MAIL_50_DIR "MailD" +#define NS_APP_IMAP_MAIL_50_DIR "IMapMD" +#define NS_APP_NEWS_50_DIR "NewsD" + +#define NS_APP_MESSENGER_LEGACY_FOLDER_CACHE_50_FILE "MLFCaF" +#define NS_APP_MESSENGER_FOLDER_CACHE_50_FILE "MFCaF" + +#define ISP_DIRECTORY_LIST "ISPDL" + +#endif diff --git a/comm/mailnews/base/src/nsMessenger.cpp b/comm/mailnews/base/src/nsMessenger.cpp new file mode 100644 index 0000000000..7fc5c05934 --- /dev/null +++ b/comm/mailnews/base/src/nsMessenger.cpp @@ -0,0 +1,2446 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "prsystem.h" + +#include "nsMessenger.h" + +// xpcom +#include "nsIComponentManager.h" +#include "nsIServiceManager.h" +#include "nsIStringStream.h" +#include "nsLocalFile.h" +#include "nsDirectoryServiceDefs.h" +#include "nsQuickSort.h" +#include "nsNativeCharsetUtils.h" +#include "mozilla/Path.h" +#include "mozilla/Components.h" +#include "mozilla/dom/LoadURIOptionsBinding.h" + +// necko +#include "nsMimeTypes.h" +#include "nsIURL.h" +#include "nsIPrompt.h" +#include "nsIStreamListener.h" +#include "nsIStreamConverterService.h" +#include "nsNetUtil.h" +#include "nsIFileURL.h" +#include "nsIMIMEInfo.h" + +// gecko +#include "nsLayoutCID.h" +#include "nsIContentViewer.h" + +/* for access to docshell */ +#include "nsPIDOMWindow.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIWebNavigation.h" +#include "nsContentUtils.h" +#include "nsDocShellLoadState.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/XULFrameElement.h" +#include "nsFrameLoader.h" +#include "mozilla/dom/Document.h" + +// mail +#include "nsIMsgMailNewsUrl.h" +#include "nsIMsgAccountManager.h" +#include "nsIMsgMailSession.h" +#include "nsIMailboxUrl.h" +#include "nsIMsgFolder.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgIncomingServer.h" + +#include "nsIMsgMessageService.h" + +#include "nsIMsgHdr.h" +// compose +#include "nsNativeCharsetUtils.h" + +// draft/folders/sendlater/etc +#include "nsIMsgCopyService.h" +#include "nsIMsgCopyServiceListener.h" +#include "nsIUrlListener.h" +#include "UrlListener.h" + +// undo +#include "nsITransaction.h" +#include "nsMsgTxn.h" + +// charset conversions +#include "nsIMimeConverter.h" + +// Save As +#include "nsIStringBundle.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsCExternalHandlerService.h" +#include "nsIExternalProtocolService.h" +#include "nsIMIMEService.h" +#include "nsITransfer.h" + +#define MESSENGER_SAVE_DIR_PREF_NAME "messenger.save.dir" +#define MIMETYPE_DELETED "text/x-moz-deleted" +#define ATTACHMENT_PERMISSION 00664 + +// +// Convert an nsString buffer to plain text... +// +#include "nsMsgUtils.h" +#include "nsCharsetSource.h" +#include "nsIChannel.h" +#include "nsIOutputStream.h" +#include "nsIPrincipal.h" + +#include "mozilla/dom/BrowserParent.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" + +#include "mozilla/NullPrincipal.h" +#include "mozilla/dom/RemoteType.h" +#include "nsQueryObject.h" +#include "mozilla/JSONStringWriteFuncs.h" + +using namespace mozilla; +using namespace mozilla::dom; + +static void ConvertAndSanitizeFileName(const nsACString& displayName, + nsString& aResult) { + nsCString unescapedName; + + /* we need to convert the UTF-8 fileName to platform specific character set. + The display name is in UTF-8 because it has been escaped from JS + */ + MsgUnescapeString(displayName, 0, unescapedName); + CopyUTF8toUTF16(unescapedName, aResult); + + // replace platform specific path separator and illegale characters to avoid + // any confusion + aResult.ReplaceChar(u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, u'-'); +} + +// *************************************************** +// jefft - this is a rather obscured class serves for Save Message As File, +// Save Message As Template, and Save Attachment to a file +// It's used to save out a single item. If multiple items are to be saved, +// a nsSaveAllAttachmentsState should be set, which holds a list of items. +// +class nsSaveAllAttachmentsState; + +class nsSaveMsgListener : public nsIUrlListener, + public nsIMsgCopyServiceListener, + public nsIStreamListener, + public nsICancelable { + using PathChar = mozilla::filesystem::Path::value_type; + + public: + nsSaveMsgListener(nsIFile* file, nsMessenger* aMessenger, + nsIUrlListener* aListener); + + NS_DECL_ISUPPORTS + + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGCOPYSERVICELISTENER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSICANCELABLE + + nsCOMPtr<nsIFile> m_file; + nsCOMPtr<nsIOutputStream> m_outputStream; + char m_dataBuffer[FILE_IO_BUFFER_SIZE]; + nsCOMPtr<nsIChannel> m_channel; + nsCString m_templateUri; + RefPtr<nsMessenger> m_messenger; + nsSaveAllAttachmentsState* m_saveAllAttachmentsState; + + // rhp: For character set handling + bool m_doCharsetConversion; + nsString m_charset; + enum { eUnknown, ePlainText, eHTML } m_outputFormat; + nsCString m_msgBuffer; + + nsCString m_contentType; // used only when saving attachment + + nsCOMPtr<nsITransfer> mTransfer; + nsCOMPtr<nsIUrlListener> mListener; + nsCOMPtr<nsIURI> mListenerUri; + int64_t mProgress; + int64_t mMaxProgress; + bool mCanceled; + bool mInitialized; + bool mUrlHasStopped; + bool mRequestHasStopped; + nsresult InitializeDownload(nsIRequest* aRequest); + + private: + virtual ~nsSaveMsgListener(); +}; + +// This helper class holds a list of attachments to be saved and (optionally) +// detached. It's used by nsSaveMsgListener (which only sticks around for a +// single item, then passes the nsSaveAllAttachmentsState along to the next +// SaveAttachment() call). +class nsSaveAllAttachmentsState { + using PathChar = mozilla::filesystem::Path::value_type; + + public: + nsSaveAllAttachmentsState(const nsTArray<nsCString>& contentTypeArray, + const nsTArray<nsCString>& urlArray, + const nsTArray<nsCString>& displayNameArray, + const nsTArray<nsCString>& messageUriArray, + const PathChar* directoryName, + bool detachingAttachments, + nsIUrlListener* overallListener); + virtual ~nsSaveAllAttachmentsState(); + + uint32_t m_count; + uint32_t m_curIndex; + PathChar* m_directoryName; + nsTArray<nsCString> m_contentTypeArray; + nsTArray<nsCString> m_urlArray; + nsTArray<nsCString> m_displayNameArray; + nsTArray<nsCString> m_messageUriArray; + bool m_detachingAttachments; + // The listener to invoke when all the items have been saved. + nsCOMPtr<nsIUrlListener> m_overallListener; + // if detaching, do without warning? Will create unique files instead of + // prompting if duplicate files exist. + bool m_withoutWarning; + // if detaching first, remember where we saved to. + nsTArray<nsCString> m_savedFiles; +}; + +// +// nsMessenger +// +nsMessenger::nsMessenger() {} + +nsMessenger::~nsMessenger() {} + +NS_IMPL_ISUPPORTS(nsMessenger, nsIMessenger, nsISupportsWeakReference) + +NS_IMETHODIMP nsMessenger::SetWindow(mozIDOMWindowProxy* aWin, + nsIMsgWindow* aMsgWindow) { + nsresult rv; + + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (aWin) { + aMsgWindow->GetTransactionManager(getter_AddRefs(mTxnMgr)); + mMsgWindow = aMsgWindow; + mWindow = aWin; + + NS_ENSURE_TRUE(aWin, NS_ERROR_FAILURE); + nsCOMPtr<nsPIDOMWindowOuter> win = nsPIDOMWindowOuter::From(aWin); + mDocShell = win->GetDocShell(); + } else { + mWindow = nullptr; + mDocShell = nullptr; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsMessenger::nsFilePickerShownCallback, + nsIFilePickerShownCallback) +nsMessenger::nsFilePickerShownCallback::nsFilePickerShownCallback() { + mResult = nsIFilePicker::returnOK; + mPickerDone = false; +} + +NS_IMETHODIMP +nsMessenger::nsFilePickerShownCallback::Done( + nsIFilePicker::ResultCode aResult) { + mResult = aResult; + mPickerDone = true; + return NS_OK; +} + +nsresult nsMessenger::ShowPicker(nsIFilePicker* aPicker, + nsIFilePicker::ResultCode* aResult) { + nsCOMPtr<nsIFilePickerShownCallback> callback = + new nsMessenger::nsFilePickerShownCallback(); + nsFilePickerShownCallback* cb = + static_cast<nsFilePickerShownCallback*>(callback.get()); + + nsresult rv; + rv = aPicker->Open(callback); + NS_ENSURE_SUCCESS(rv, rv); + + // Spin the event loop until the callback was called. + nsCOMPtr<nsIThread> thread(do_GetCurrentThread()); + while (!cb->mPickerDone) { + NS_ProcessNextEvent(thread, true); + } + + *aResult = cb->mResult; + return NS_OK; +} + +nsresult nsMessenger::PromptIfFileExists(nsIFile* file) { + nsresult rv = NS_ERROR_FAILURE; + bool exists; + file->Exists(&exists); + if (!exists) return NS_OK; + + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(mDocShell)); + if (!dialog) return rv; + nsAutoString path; + bool dialogResult = false; + nsString errorMessage; + + file->GetPath(path); + AutoTArray<nsString, 1> pathFormatStrings = {path}; + + if (!mStringBundle) { + rv = InitStringBundle(); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = mStringBundle->FormatStringFromName("fileExists", pathFormatStrings, + errorMessage); + NS_ENSURE_SUCCESS(rv, rv); + rv = dialog->Confirm(nullptr, errorMessage.get(), &dialogResult); + NS_ENSURE_SUCCESS(rv, rv); + + if (dialogResult) return NS_OK; // user says okay to replace + + // if we don't re-init the path for redisplay the picker will + // show the full path, not just the file name + nsCOMPtr<nsIFile> currentFile = + do_CreateInstance("@mozilla.org/file/local;1"); + if (!currentFile) return NS_ERROR_FAILURE; + + rv = currentFile->InitWithPath(path); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString leafName; + currentFile->GetLeafName(leafName); + if (!leafName.IsEmpty()) + path.Assign(leafName); // path should be a copy of leafName + + nsCOMPtr<nsIFilePicker> filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsString saveAttachmentStr; + GetString(u"SaveAttachment"_ns, saveAttachmentStr); + filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeSave); + filePicker->SetDefaultString(path); + filePicker->AppendFilters(nsIFilePicker::filterAll); + + nsCOMPtr<nsIFile> lastSaveDir; + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) { + filePicker->SetDisplayDirectory(lastSaveDir); + } + + nsIFilePicker::ResultCode dialogReturn; + rv = ShowPicker(filePicker, &dialogReturn); + if (NS_FAILED(rv) || dialogReturn == nsIFilePicker::returnCancel) { + // XXX todo + // don't overload the return value like this + // change this function to have an out boolean + // that we check to see if the user cancelled + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIFile> localFile; + + rv = filePicker->GetFile(getter_AddRefs(localFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetLastSaveDirectory(localFile); + NS_ENSURE_SUCCESS(rv, rv); + + // reset the file to point to the new path + return file->InitWithFile(localFile); +} + +NS_IMETHODIMP nsMessenger::SaveAttachmentToFile(nsIFile* aFile, + const nsACString& aURL, + const nsACString& aMessageUri, + const nsACString& aContentType, + nsIUrlListener* aListener) { + return SaveAttachment(aFile, aURL, aMessageUri, aContentType, nullptr, + aListener); +} + +NS_IMETHODIMP +nsMessenger::DetachAttachmentsWOPrompts( + nsIFile* aDestFolder, const nsTArray<nsCString>& aContentTypeArray, + const nsTArray<nsCString>& aUrlArray, + const nsTArray<nsCString>& aDisplayNameArray, + const nsTArray<nsCString>& aMessageUriArray, nsIUrlListener* aListener) { + NS_ENSURE_ARG_POINTER(aDestFolder); + MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() && + aUrlArray.Length() == aDisplayNameArray.Length() && + aDisplayNameArray.Length() == aMessageUriArray.Length()); + + if (!aContentTypeArray.Length()) return NS_OK; + nsCOMPtr<nsIFile> attachmentDestination; + nsresult rv = aDestFolder->Clone(getter_AddRefs(attachmentDestination)); + NS_ENSURE_SUCCESS(rv, rv); + + PathString path = attachmentDestination->NativePath(); + + nsAutoString unescapedFileName; + ConvertAndSanitizeFileName(aDisplayNameArray[0], unescapedFileName); + rv = attachmentDestination->Append(unescapedFileName); + NS_ENSURE_SUCCESS(rv, rv); + rv = attachmentDestination->CreateUnique(nsIFile::NORMAL_FILE_TYPE, + ATTACHMENT_PERMISSION); + NS_ENSURE_SUCCESS(rv, rv); + + // Set up to detach the attachments once they've been saved out. + // NOTE: nsSaveAllAttachmentsState has a detach option, but I'd like to + // phase it out, so we set up a listener to call DetachAttachments() + // instead. + UrlListener* listener = new UrlListener; + nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState( + aContentTypeArray, aUrlArray, aDisplayNameArray, aMessageUriArray, + path.get(), + false, // detach = false + listener); + + // Note: saveState is kept in existence by SaveAttachment() until after + // the last item is saved. + listener->mStopFn = [saveState, self = RefPtr<nsMessenger>(this), + originalListener = nsCOMPtr<nsIUrlListener>(aListener)]( + nsIURI* url, nsresult status) -> nsresult { + if (NS_SUCCEEDED(status)) { + status = self->DetachAttachments( + saveState->m_contentTypeArray, saveState->m_urlArray, + saveState->m_displayNameArray, saveState->m_messageUriArray, + &saveState->m_savedFiles, originalListener, + saveState->m_withoutWarning); + } + if (NS_FAILED(status) && originalListener) { + return originalListener->OnStopRunningUrl(nullptr, status); + } + return NS_OK; + }; + + // This method is used in filters, where we don't want to warn + saveState->m_withoutWarning = true; + + rv = SaveAttachment(attachmentDestination, aUrlArray[0], aMessageUriArray[0], + aContentTypeArray[0], saveState, nullptr); + return rv; +} + +// Internal helper for Saving attachments. +// It handles a single attachment, but multiple attachments can be saved +// by passing in an nsSaveAllAttachmentsState. In this case, SaveAttachment() +// will be called for each attachment, and the saveState keeps track of which +// one we're up to. +// +// aListener is invoked to cover this single attachment save. +// If a saveState is used, it can also contain a nsIUrlListener which +// will be invoked when _all_ the saves are complete. +// +// SaveAttachment() takes ownership of the saveState passed in. +// If SaveAttachment() fails, then +// saveState->m_overallListener->OnStopRunningUrl() +// will be invoked and saveState itself will be deleted. +// +// Even though SaveAttachment() takes ownership of saveState, +// nsSaveMsgListener is responsible for finally deleting it when the +// last save operation successfully completes. +// +// Yes, this is convoluted. Bug 1788159 covers simplifying all this stuff. +nsresult nsMessenger::SaveAttachment(nsIFile* aFile, const nsACString& aURL, + const nsACString& aMessageUri, + const nsACString& aContentType, + nsSaveAllAttachmentsState* saveState, + nsIUrlListener* aListener) { + nsCOMPtr<nsIMsgMessageService> messageService; + nsCOMPtr<nsIMsgMessageFetchPartService> fetchService; + nsAutoCString urlString; + nsAutoCString fullMessageUri(aMessageUri); + + nsresult rv = NS_OK; + + // This instance will be held onto by the listeners, and will be released once + // the transfer has been completed. + RefPtr<nsSaveMsgListener> saveListener( + new nsSaveMsgListener(aFile, this, aListener)); + + saveListener->m_contentType = aContentType; + if (saveState) { + if (saveState->m_overallListener && saveState->m_curIndex == 0) { + // This is the first item, so tell the caller we're starting. + saveState->m_overallListener->OnStartRunningUrl(nullptr); + } + saveListener->m_saveAllAttachmentsState = saveState; + // Record the resultant file:// URL for each saved attachment as we go + // along. It'll be used later if we want to also detach them from the email. + // Placeholder text will be inserted into the email to replace the + // removed attachment pointing at it's final resting place. + nsCOMPtr<nsIURI> outputURI; + rv = NS_NewFileURI(getter_AddRefs(outputURI), aFile); + if (NS_SUCCEEDED(rv)) { + nsAutoCString fileUriSpec; + rv = outputURI->GetSpec(fileUriSpec); + if NS_SUCCEEDED (rv) { + saveState->m_savedFiles.AppendElement(fileUriSpec); + } + } + } + + nsCOMPtr<nsIURI> URL; + if (NS_SUCCEEDED(rv)) { + urlString = aURL; + // strip out ?type=application/x-message-display because it confuses libmime + + int32_t typeIndex = urlString.Find("?type=application/x-message-display"); + if (typeIndex != kNotFound) { + urlString.Cut(typeIndex, + sizeof("?type=application/x-message-display") - 1); + // we also need to replace the next '&' with '?' + int32_t firstPartIndex = urlString.FindChar('&'); + if (firstPartIndex != kNotFound) urlString.SetCharAt('?', firstPartIndex); + } + + urlString.ReplaceSubstring("/;section", "?section"); + rv = NS_NewURI(getter_AddRefs(URL), urlString); + } + + if (NS_SUCCEEDED(rv)) { + rv = GetMessageServiceFromURI(aMessageUri, getter_AddRefs(messageService)); + if (NS_SUCCEEDED(rv)) { + fetchService = do_QueryInterface(messageService); + // if the message service has a fetch part service then we know we can + // fetch mime parts... + if (fetchService) { + int32_t partPos = urlString.FindChar('?'); + if (partPos == kNotFound) return NS_ERROR_FAILURE; + fullMessageUri.Append(Substring(urlString, partPos)); + } + + nsCOMPtr<nsIStreamListener> convertedListener; + saveListener->QueryInterface(NS_GET_IID(nsIStreamListener), + getter_AddRefs(convertedListener)); + + nsCOMPtr<nsIURI> dummyNull; + if (fetchService) + rv = fetchService->FetchMimePart(URL, fullMessageUri, convertedListener, + mMsgWindow, saveListener, + getter_AddRefs(dummyNull)); + else + rv = messageService->LoadMessage(fullMessageUri, convertedListener, + mMsgWindow, nullptr, false); + } // if we got a message service + } // if we created a url + + if (NS_FAILED(rv)) { + if (saveState) { + // If we had a listener, make sure it sees the failure! + if (saveState->m_overallListener) { + saveState->m_overallListener->OnStopRunningUrl(nullptr, rv); + } + // Ugh. Ownership is all over the place here! + // Usually nsSaveMsgListener is responsible for cleaning up + // nsSaveAllAttachmentsState... but we're not getting + // that far, so have to clean it up here! + delete saveState; + saveListener->m_saveAllAttachmentsState = nullptr; + } + Alert("saveAttachmentFailed"); + } + return rv; +} + +NS_IMETHODIMP +nsMessenger::SaveAttachmentToFolder(const nsACString& contentType, + const nsACString& url, + const nsACString& displayName, + const nsACString& messageUri, + nsIFile* aDestFolder, nsIFile** aOutFile) { + NS_ENSURE_ARG_POINTER(aDestFolder); + nsresult rv; + + nsCOMPtr<nsIFile> attachmentDestination; + rv = aDestFolder->Clone(getter_AddRefs(attachmentDestination)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString unescapedFileName; + ConvertAndSanitizeFileName(displayName, unescapedFileName); + rv = attachmentDestination->Append(unescapedFileName); + NS_ENSURE_SUCCESS(rv, rv); +#ifdef XP_MACOSX + rv = attachmentDestination->CreateUnique(nsIFile::NORMAL_FILE_TYPE, + ATTACHMENT_PERMISSION); + NS_ENSURE_SUCCESS(rv, rv); +#endif + + rv = SaveAttachment(attachmentDestination, url, messageUri, contentType, + nullptr, nullptr); + attachmentDestination.forget(aOutFile); + return rv; +} + +NS_IMETHODIMP +nsMessenger::SaveAttachment(const nsACString& aContentType, + const nsACString& aURL, + const nsACString& aDisplayName, + const nsACString& aMessageUri, + bool aIsExternalAttachment) { + return SaveOneAttachment(aContentType, aURL, aDisplayName, aMessageUri, + false); +} + +nsresult nsMessenger::SaveOneAttachment(const nsACString& aContentType, + const nsACString& aURL, + const nsACString& aDisplayName, + const nsACString& aMessageUri, + bool detaching) { + nsresult rv = NS_ERROR_OUT_OF_MEMORY; + nsCOMPtr<nsIFilePicker> filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsIFilePicker::ResultCode dialogResult; + nsCOMPtr<nsIFile> localFile; + nsCOMPtr<nsIFile> lastSaveDir; + nsCString filePath; + nsString saveAttachmentStr; + nsString defaultDisplayString; + ConvertAndSanitizeFileName(aDisplayName, defaultDisplayString); + + if (detaching) { + GetString(u"DetachAttachment"_ns, saveAttachmentStr); + } else { + GetString(u"SaveAttachment"_ns, saveAttachmentStr); + } + filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeSave); + filePicker->SetDefaultString(defaultDisplayString); + + // Check if the attachment file name has an extension (which must not + // contain spaces) and set it as the default extension for the attachment. + int32_t extensionIndex = defaultDisplayString.RFindChar('.'); + if (extensionIndex > 0 && + defaultDisplayString.FindChar(' ', extensionIndex) == kNotFound) { + nsString extension; + extension = Substring(defaultDisplayString, extensionIndex + 1); + filePicker->SetDefaultExtension(extension); + if (!mStringBundle) { + rv = InitStringBundle(); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsString filterName; + AutoTArray<nsString, 1> extensionParam = {extension}; + rv = mStringBundle->FormatStringFromName("saveAsType", extensionParam, + filterName); + NS_ENSURE_SUCCESS(rv, rv); + + extension.InsertLiteral(u"*.", 0); + filePicker->AppendFilter(filterName, extension); + } + + filePicker->AppendFilters(nsIFilePicker::filterAll); + + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) + filePicker->SetDisplayDirectory(lastSaveDir); + + rv = ShowPicker(filePicker, &dialogResult); + if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) return rv; + + rv = filePicker->GetFile(getter_AddRefs(localFile)); + NS_ENSURE_SUCCESS(rv, rv); + + SetLastSaveDirectory(localFile); + + PathString dirName = localFile->NativePath(); + + AutoTArray<nsCString, 1> contentTypeArray = { + PromiseFlatCString(aContentType)}; + AutoTArray<nsCString, 1> urlArray = {PromiseFlatCString(aURL)}; + AutoTArray<nsCString, 1> displayNameArray = { + PromiseFlatCString(aDisplayName)}; + AutoTArray<nsCString, 1> messageUriArray = {PromiseFlatCString(aMessageUri)}; + nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState( + contentTypeArray, urlArray, displayNameArray, messageUriArray, + dirName.get(), detaching, nullptr); + + // SaveAttachment takes ownership of saveState. + return SaveAttachment(localFile, aURL, aMessageUri, aContentType, saveState, + nullptr); +} + +NS_IMETHODIMP +nsMessenger::SaveAllAttachments(const nsTArray<nsCString>& contentTypeArray, + const nsTArray<nsCString>& urlArray, + const nsTArray<nsCString>& displayNameArray, + const nsTArray<nsCString>& messageUriArray) { + uint32_t len = contentTypeArray.Length(); + NS_ENSURE_TRUE(urlArray.Length() == len, NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(displayNameArray.Length() == len, NS_ERROR_INVALID_ARG); + NS_ENSURE_TRUE(messageUriArray.Length() == len, NS_ERROR_INVALID_ARG); + if (len == 0) { + return NS_OK; + } + return SaveAllAttachments(contentTypeArray, urlArray, displayNameArray, + messageUriArray, false); +} + +nsresult nsMessenger::SaveAllAttachments( + const nsTArray<nsCString>& contentTypeArray, + const nsTArray<nsCString>& urlArray, + const nsTArray<nsCString>& displayNameArray, + const nsTArray<nsCString>& messageUriArray, bool detaching) { + nsresult rv = NS_ERROR_OUT_OF_MEMORY; + nsCOMPtr<nsIFilePicker> filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + nsCOMPtr<nsIFile> localFile; + nsCOMPtr<nsIFile> lastSaveDir; + nsIFilePicker::ResultCode dialogResult; + nsString saveAttachmentStr; + + NS_ENSURE_SUCCESS(rv, rv); + if (detaching) { + GetString(u"DetachAllAttachments"_ns, saveAttachmentStr); + } else { + GetString(u"SaveAllAttachments"_ns, saveAttachmentStr); + } + filePicker->Init(mWindow, saveAttachmentStr, nsIFilePicker::modeGetFolder); + + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) + filePicker->SetDisplayDirectory(lastSaveDir); + + rv = ShowPicker(filePicker, &dialogResult); + if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) return rv; + + rv = filePicker->GetFile(getter_AddRefs(localFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetLastSaveDirectory(localFile); + NS_ENSURE_SUCCESS(rv, rv); + + PathString dirName = localFile->NativePath(); + + nsString unescapedName; + ConvertAndSanitizeFileName(displayNameArray[0], unescapedName); + rv = localFile->Append(unescapedName); + NS_ENSURE_SUCCESS(rv, rv); + + rv = PromptIfFileExists(localFile); + NS_ENSURE_SUCCESS(rv, rv); + + nsSaveAllAttachmentsState* saveState = new nsSaveAllAttachmentsState( + contentTypeArray, urlArray, displayNameArray, messageUriArray, + dirName.get(), detaching, nullptr); + // SaveAttachment takes ownership of saveState. + rv = SaveAttachment(localFile, urlArray[0], messageUriArray[0], + contentTypeArray[0], saveState, nullptr); + return rv; +} + +enum MESSENGER_SAVEAS_FILE_TYPE { + EML_FILE_TYPE = 0, + HTML_FILE_TYPE = 1, + TEXT_FILE_TYPE = 2, + ANY_FILE_TYPE = 3 +}; +#define HTML_FILE_EXTENSION ".htm" +#define HTML_FILE_EXTENSION2 ".html" +#define TEXT_FILE_EXTENSION ".txt" + +/** + * Adjust the file name, removing characters from the middle of the name if + * the name would otherwise be too long - too long for what file systems + * usually support. + */ +nsresult nsMessenger::AdjustFileIfNameTooLong(nsIFile* aFile) { + NS_ENSURE_ARG_POINTER(aFile); + nsAutoString path; + nsresult rv = aFile->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + // Most common file systems have a max filename length of 255. On windows, the + // total path length is (at least for all practical purposees) limited to 255. + // Let's just don't allow paths longer than that elsewhere either for + // simplicity. + uint32_t MAX = 255; + if (path.Length() > MAX) { + nsAutoString leafName; + rv = aFile->GetLeafName(leafName); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t pathLengthUpToLeaf = path.Length() - leafName.Length(); + if (pathLengthUpToLeaf >= MAX - 8) { // want at least 8 chars for name + return NS_ERROR_FILE_NAME_TOO_LONG; + } + uint32_t x = MAX - pathLengthUpToLeaf; // x = max leaf size + nsAutoString truncatedLeaf; + truncatedLeaf.Append(Substring(leafName, 0, x / 2)); + truncatedLeaf.AppendLiteral("..."); + truncatedLeaf.Append( + Substring(leafName, leafName.Length() - x / 2 + 3, leafName.Length())); + rv = aFile->SetLeafName(truncatedLeaf); + } + return rv; +} + +NS_IMETHODIMP +nsMessenger::SaveAs(const nsACString& aURI, bool aAsFile, + nsIMsgIdentity* aIdentity, const nsAString& aMsgFilename, + bool aBypassFilePicker) { + nsCOMPtr<nsIMsgMessageService> messageService; + nsCOMPtr<nsIUrlListener> urlListener; + RefPtr<nsSaveMsgListener> saveListener; + nsCOMPtr<nsIStreamListener> convertedListener; + int32_t saveAsFileType = EML_FILE_TYPE; + + nsresult rv = GetMessageServiceFromURI(aURI, getter_AddRefs(messageService)); + if (NS_FAILED(rv)) goto done; + + if (aAsFile) { + nsCOMPtr<nsIFile> saveAsFile; + // show the file picker if BypassFilePicker is not specified (null) or false + if (!aBypassFilePicker) { + rv = GetSaveAsFile(aMsgFilename, &saveAsFileType, + getter_AddRefs(saveAsFile)); + // A null saveAsFile means that the user canceled the save as + if (NS_FAILED(rv) || !saveAsFile) goto done; + } else { + saveAsFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + rv = saveAsFile->InitWithPath(aMsgFilename); + if (NS_FAILED(rv)) goto done; + if (StringEndsWith(aMsgFilename, + NS_LITERAL_STRING_FROM_CSTRING(TEXT_FILE_EXTENSION), + nsCaseInsensitiveStringComparator)) + saveAsFileType = TEXT_FILE_TYPE; + else if ((StringEndsWith( + aMsgFilename, + NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION), + nsCaseInsensitiveStringComparator)) || + (StringEndsWith( + aMsgFilename, + NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION2), + nsCaseInsensitiveStringComparator))) + saveAsFileType = HTML_FILE_TYPE; + else + saveAsFileType = EML_FILE_TYPE; + } + + rv = AdjustFileIfNameTooLong(saveAsFile); + NS_ENSURE_SUCCESS(rv, rv); + + rv = PromptIfFileExists(saveAsFile); + if (NS_FAILED(rv)) { + goto done; + } + + // After saveListener goes out of scope, the listener will be owned by + // whoever the listener is registered with, usually a URL. + saveListener = new nsSaveMsgListener(saveAsFile, this, nullptr); + rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener), + getter_AddRefs(urlListener)); + if (NS_FAILED(rv)) goto done; + + if (saveAsFileType == EML_FILE_TYPE) { + nsCOMPtr<nsIURI> dummyNull; + rv = messageService->SaveMessageToDisk( + aURI, saveAsFile, false, urlListener, getter_AddRefs(dummyNull), true, + mMsgWindow); + } else { + nsAutoCString urlString(aURI); + + // we can't go RFC822 to TXT until bug #1775 is fixed + // so until then, do the HTML to TXT conversion in + // nsSaveMsgListener::OnStopRequest(), see ConvertBufToPlainText() + // + // Setup the URL for a "Save As..." Operation... + // For now, if this is a save as TEXT operation, then do + // a "printing" operation + if (saveAsFileType == TEXT_FILE_TYPE) { + saveListener->m_outputFormat = nsSaveMsgListener::ePlainText; + saveListener->m_doCharsetConversion = true; + urlString.AppendLiteral("?header=print"); + } else { + saveListener->m_outputFormat = nsSaveMsgListener::eHTML; + saveListener->m_doCharsetConversion = false; + urlString.AppendLiteral("?header=saveas"); + } + + nsCOMPtr<nsIURI> url; + rv = NS_NewURI(getter_AddRefs(url), urlString); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewURI failed"); + if (NS_FAILED(rv)) goto done; + + nsCOMPtr<nsIPrincipal> nullPrincipal = + NullPrincipal::CreateWithoutOriginAttributes(); + + saveListener->m_channel = nullptr; + rv = NS_NewInputStreamChannel( + getter_AddRefs(saveListener->m_channel), url, nullptr, nullPrincipal, + nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL, + nsIContentPolicy::TYPE_OTHER); + NS_ASSERTION(NS_SUCCEEDED(rv), "NS_NewChannel failed"); + if (NS_FAILED(rv)) goto done; + + nsCOMPtr<nsIStreamConverterService> streamConverterService = + do_GetService("@mozilla.org/streamConverters;1"); + nsCOMPtr<nsISupports> channelSupport = + do_QueryInterface(saveListener->m_channel); + + // we can't go RFC822 to TXT until bug #1775 is fixed + // so until then, do the HTML to TXT conversion in + // nsSaveMsgListener::OnStopRequest(), see ConvertBufToPlainText() + rv = streamConverterService->AsyncConvertData( + MESSAGE_RFC822, TEXT_HTML, saveListener, channelSupport, + getter_AddRefs(convertedListener)); + NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncConvertData failed"); + if (NS_FAILED(rv)) goto done; + + rv = messageService->LoadMessage(urlString, convertedListener, mMsgWindow, + nullptr, false); + } + } else { + // ** save as Template + nsCOMPtr<nsIFile> tmpFile; + nsresult rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "nsmail.tmp", + getter_AddRefs(tmpFile)); + + NS_ENSURE_SUCCESS(rv, rv); + + // For temp file, we should use restrictive 00600 instead of + // ATTACHMENT_PERMISSION + rv = tmpFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600); + if (NS_FAILED(rv)) goto done; + + // The saveListener is owned by whoever we ultimately register the + // listener with, generally a URL. + saveListener = new nsSaveMsgListener(tmpFile, this, nullptr); + + if (aIdentity) + rv = aIdentity->GetStationeryFolder(saveListener->m_templateUri); + if (NS_FAILED(rv)) goto done; + + bool needDummyHeader = + StringBeginsWith(saveListener->m_templateUri, "mailbox://"_ns); + bool canonicalLineEnding = + StringBeginsWith(saveListener->m_templateUri, "imap://"_ns); + + rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener), + getter_AddRefs(urlListener)); + if (NS_FAILED(rv)) goto done; + + nsCOMPtr<nsIURI> dummyNull; + rv = messageService->SaveMessageToDisk( + aURI, tmpFile, needDummyHeader, urlListener, getter_AddRefs(dummyNull), + canonicalLineEnding, mMsgWindow); + } + +done: + if (NS_FAILED(rv)) { + Alert("saveMessageFailed"); + } + return rv; +} + +nsresult nsMessenger::GetSaveAsFile(const nsAString& aMsgFilename, + int32_t* aSaveAsFileType, + nsIFile** aSaveAsFile) { + nsresult rv; + nsCOMPtr<nsIFilePicker> filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsString saveMailAsStr; + GetString(u"SaveMailAs"_ns, saveMailAsStr); + filePicker->Init(mWindow, saveMailAsStr, nsIFilePicker::modeSave); + + // if we have a non-null filename use it, otherwise use default save message + // one + if (aMsgFilename.IsEmpty()) { + nsString saveMsgStr; + GetString(u"defaultSaveMessageAsFileName"_ns, saveMsgStr); + filePicker->SetDefaultString(saveMsgStr); + } else { + filePicker->SetDefaultString(aMsgFilename); + } + + // because we will be using GetFilterIndex() + // we must call AppendFilters() one at a time, + // in MESSENGER_SAVEAS_FILE_TYPE order + nsString emlFilesStr; + GetString(u"EMLFiles"_ns, emlFilesStr); + filePicker->AppendFilter(emlFilesStr, u"*.eml"_ns); + filePicker->AppendFilters(nsIFilePicker::filterHTML); + filePicker->AppendFilters(nsIFilePicker::filterText); + filePicker->AppendFilters(nsIFilePicker::filterAll); + + // Save as the "All Files" file type by default. We want to save as .eml by + // default, but the filepickers on some platforms don't switch extensions + // based on the file type selected (bug 508597). + filePicker->SetFilterIndex(ANY_FILE_TYPE); + // Yes, this is fine even if we ultimately save as HTML or text. On Windows, + // this actually is a boolean telling the file picker to automatically add + // the correct extension depending on the filter. On Mac or Linux this is a + // no-op. + filePicker->SetDefaultExtension(u"eml"_ns); + + nsIFilePicker::ResultCode dialogResult; + + nsCOMPtr<nsIFile> lastSaveDir; + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) + filePicker->SetDisplayDirectory(lastSaveDir); + + nsCOMPtr<nsIFile> localFile; + rv = ShowPicker(filePicker, &dialogResult); + NS_ENSURE_SUCCESS(rv, rv); + if (dialogResult == nsIFilePicker::returnCancel) { + // We'll indicate this by setting the outparam to null. + *aSaveAsFile = nullptr; + return NS_OK; + } + + rv = filePicker->GetFile(getter_AddRefs(localFile)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetLastSaveDirectory(localFile); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t selectedSaveAsFileType; + rv = filePicker->GetFilterIndex(&selectedSaveAsFileType); + NS_ENSURE_SUCCESS(rv, rv); + + // If All Files was selected, look at the extension + if (selectedSaveAsFileType == ANY_FILE_TYPE) { + nsAutoString fileName; + rv = localFile->GetLeafName(fileName); + NS_ENSURE_SUCCESS(rv, rv); + + if (StringEndsWith(fileName, + NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION), + nsCaseInsensitiveStringComparator) || + StringEndsWith(fileName, + NS_LITERAL_STRING_FROM_CSTRING(HTML_FILE_EXTENSION2), + nsCaseInsensitiveStringComparator)) + *aSaveAsFileType = HTML_FILE_TYPE; + else if (StringEndsWith(fileName, + NS_LITERAL_STRING_FROM_CSTRING(TEXT_FILE_EXTENSION), + nsCaseInsensitiveStringComparator)) + *aSaveAsFileType = TEXT_FILE_TYPE; + else + // The default is .eml + *aSaveAsFileType = EML_FILE_TYPE; + } else { + *aSaveAsFileType = selectedSaveAsFileType; + } + + if (dialogResult == nsIFilePicker::returnReplace) { + // be extra safe and only delete when the file is really a file + bool isFile; + rv = localFile->IsFile(&isFile); + if (NS_SUCCEEDED(rv) && isFile) { + rv = localFile->Remove(false /* recursive delete */); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // We failed, or this isn't a file. We can't do anything about it. + return NS_ERROR_FAILURE; + } + } + + *aSaveAsFile = nullptr; + localFile.forget(aSaveAsFile); + return NS_OK; +} + +/** + * Show a Save All dialog allowing the user to pick which folder to save + * messages to. + * @param [out] aSaveDir directory to save to. Will be null on cancel. + */ +nsresult nsMessenger::GetSaveToDir(nsIFile** aSaveDir) { + nsresult rv; + nsCOMPtr<nsIFilePicker> filePicker = + do_CreateInstance("@mozilla.org/filepicker;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsString chooseFolderStr; + GetString(u"ChooseFolder"_ns, chooseFolderStr); + filePicker->Init(mWindow, chooseFolderStr, nsIFilePicker::modeGetFolder); + + nsCOMPtr<nsIFile> lastSaveDir; + rv = GetLastSaveDirectory(getter_AddRefs(lastSaveDir)); + if (NS_SUCCEEDED(rv) && lastSaveDir) + filePicker->SetDisplayDirectory(lastSaveDir); + + nsIFilePicker::ResultCode dialogResult; + rv = ShowPicker(filePicker, &dialogResult); + if (NS_FAILED(rv) || dialogResult == nsIFilePicker::returnCancel) { + // We'll indicate this by setting the outparam to null. + *aSaveDir = nullptr; + return NS_OK; + } + + nsCOMPtr<nsIFile> dir; + rv = filePicker->GetFile(getter_AddRefs(dir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetLastSaveDirectory(dir); + NS_ENSURE_SUCCESS(rv, rv); + + *aSaveDir = nullptr; + dir.forget(aSaveDir); + return NS_OK; +} + +NS_IMETHODIMP +nsMessenger::SaveMessages(const nsTArray<nsString>& aFilenameArray, + const nsTArray<nsCString>& aMessageUriArray) { + MOZ_ASSERT(aFilenameArray.Length() == aMessageUriArray.Length()); + + nsresult rv; + + nsCOMPtr<nsIFile> saveDir; + rv = GetSaveToDir(getter_AddRefs(saveDir)); + NS_ENSURE_SUCCESS(rv, rv); + if (!saveDir) // A null saveDir means that the user canceled the save. + return NS_OK; + + for (uint32_t i = 0; i < aFilenameArray.Length(); i++) { + nsCOMPtr<nsIFile> saveToFile = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = saveToFile->InitWithFile(saveDir); + NS_ENSURE_SUCCESS(rv, rv); + + rv = saveToFile->Append(aFilenameArray[i]); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AdjustFileIfNameTooLong(saveToFile); + NS_ENSURE_SUCCESS(rv, rv); + + rv = PromptIfFileExists(saveToFile); + if (NS_FAILED(rv)) continue; + + nsCOMPtr<nsIMsgMessageService> messageService; + nsCOMPtr<nsIUrlListener> urlListener; + + rv = GetMessageServiceFromURI(aMessageUriArray[i], + getter_AddRefs(messageService)); + if (NS_FAILED(rv)) { + Alert("saveMessageFailed"); + return rv; + } + + RefPtr<nsSaveMsgListener> saveListener = + new nsSaveMsgListener(saveToFile, this, nullptr); + + rv = saveListener->QueryInterface(NS_GET_IID(nsIUrlListener), + getter_AddRefs(urlListener)); + if (NS_FAILED(rv)) { + Alert("saveMessageFailed"); + return rv; + } + + // Ok, now save the message. + nsCOMPtr<nsIURI> dummyNull; + rv = messageService->SaveMessageToDisk( + aMessageUriArray[i], saveToFile, false, urlListener, + getter_AddRefs(dummyNull), true, mMsgWindow); + if (NS_FAILED(rv)) { + Alert("saveMessageFailed"); + return rv; + } + } + return rv; +} + +nsresult nsMessenger::Alert(const char* stringName) { + nsresult rv = NS_OK; + + if (mDocShell) { + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(mDocShell)); + + if (dialog) { + nsString alertStr; + GetString(NS_ConvertASCIItoUTF16(stringName), alertStr); + rv = dialog->Alert(nullptr, alertStr.get()); + } + } + return rv; +} + +NS_IMETHODIMP +nsMessenger::MsgHdrFromURI(const nsACString& aUri, nsIMsgDBHdr** aMsgHdr) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + nsCOMPtr<nsIMsgMessageService> msgService; + nsresult rv; + + rv = GetMessageServiceFromURI(aUri, getter_AddRefs(msgService)); + NS_ENSURE_SUCCESS(rv, rv); + return msgService->MessageURIToMsgHdr(aUri, aMsgHdr); +} + +NS_IMETHODIMP nsMessenger::GetUndoTransactionType(uint32_t* txnType) { + NS_ENSURE_TRUE(txnType && mTxnMgr, NS_ERROR_NULL_POINTER); + + nsresult rv; + *txnType = nsMessenger::eUnknown; + nsCOMPtr<nsITransaction> txn; + rv = mTxnMgr->PeekUndoStack(getter_AddRefs(txn)); + if (NS_SUCCEEDED(rv) && txn) { + nsCOMPtr<nsIPropertyBag2> propertyBag = do_QueryInterface(txn, &rv); + NS_ENSURE_SUCCESS(rv, rv); + return propertyBag->GetPropertyAsUint32(u"type"_ns, txnType); + } + return rv; +} + +NS_IMETHODIMP nsMessenger::CanUndo(bool* bValue) { + NS_ENSURE_TRUE(bValue && mTxnMgr, NS_ERROR_NULL_POINTER); + + nsresult rv; + *bValue = false; + int32_t count = 0; + rv = mTxnMgr->GetNumberOfUndoItems(&count); + if (NS_SUCCEEDED(rv) && count > 0) *bValue = true; + return rv; +} + +NS_IMETHODIMP nsMessenger::GetRedoTransactionType(uint32_t* txnType) { + NS_ENSURE_TRUE(txnType && mTxnMgr, NS_ERROR_NULL_POINTER); + + nsresult rv; + *txnType = nsMessenger::eUnknown; + nsCOMPtr<nsITransaction> txn; + rv = mTxnMgr->PeekRedoStack(getter_AddRefs(txn)); + if (NS_SUCCEEDED(rv) && txn) { + nsCOMPtr<nsIPropertyBag2> propertyBag = do_QueryInterface(txn, &rv); + NS_ENSURE_SUCCESS(rv, rv); + return propertyBag->GetPropertyAsUint32(u"type"_ns, txnType); + } + return rv; +} + +NS_IMETHODIMP nsMessenger::CanRedo(bool* bValue) { + NS_ENSURE_TRUE(bValue && mTxnMgr, NS_ERROR_NULL_POINTER); + + nsresult rv; + *bValue = false; + int32_t count = 0; + rv = mTxnMgr->GetNumberOfRedoItems(&count); + if (NS_SUCCEEDED(rv) && count > 0) *bValue = true; + return rv; +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +nsMessenger::Undo(nsIMsgWindow* msgWindow) { + nsresult rv = NS_OK; + if (mTxnMgr) { + int32_t numTxn = 0; + rv = mTxnMgr->GetNumberOfUndoItems(&numTxn); + if (NS_SUCCEEDED(rv) && numTxn > 0) { + nsCOMPtr<nsITransaction> txn; + rv = mTxnMgr->PeekUndoStack(getter_AddRefs(txn)); + if (NS_SUCCEEDED(rv) && txn) { + static_cast<nsMsgTxn*>(static_cast<nsITransaction*>(txn.get())) + ->SetMsgWindow(msgWindow); + } + nsCOMPtr<nsITransactionManager> txnMgr = mTxnMgr; + txnMgr->UndoTransaction(); + } + } + return rv; +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP +nsMessenger::Redo(nsIMsgWindow* msgWindow) { + nsresult rv = NS_OK; + if (mTxnMgr) { + int32_t numTxn = 0; + rv = mTxnMgr->GetNumberOfRedoItems(&numTxn); + if (NS_SUCCEEDED(rv) && numTxn > 0) { + nsCOMPtr<nsITransaction> txn; + rv = mTxnMgr->PeekRedoStack(getter_AddRefs(txn)); + if (NS_SUCCEEDED(rv) && txn) { + static_cast<nsMsgTxn*>(static_cast<nsITransaction*>(txn.get())) + ->SetMsgWindow(msgWindow); + } + nsCOMPtr<nsITransactionManager> txnMgr = mTxnMgr; + txnMgr->RedoTransaction(); + } + } + return rv; +} + +NS_IMETHODIMP +nsMessenger::GetTransactionManager(nsITransactionManager** aTxnMgr) { + NS_ENSURE_TRUE(mTxnMgr && aTxnMgr, NS_ERROR_NULL_POINTER); + NS_ADDREF(*aTxnMgr = mTxnMgr); + return NS_OK; +} + +nsSaveMsgListener::nsSaveMsgListener(nsIFile* aFile, nsMessenger* aMessenger, + nsIUrlListener* aListener) { + m_file = aFile; + m_messenger = aMessenger; + mListener = aListener; + mUrlHasStopped = false; + mRequestHasStopped = false; + + // rhp: for charset handling + m_doCharsetConversion = false; + m_saveAllAttachmentsState = nullptr; + mProgress = 0; + mMaxProgress = -1; + mCanceled = false; + m_outputFormat = eUnknown; + mInitialized = false; +} + +nsSaveMsgListener::~nsSaveMsgListener() {} + +// +// nsISupports +// +NS_IMPL_ISUPPORTS(nsSaveMsgListener, nsIUrlListener, nsIMsgCopyServiceListener, + nsIStreamListener, nsIRequestObserver, nsICancelable) + +NS_IMETHODIMP +nsSaveMsgListener::Cancel(nsresult status) { + mCanceled = true; + return NS_OK; +} + +// +// nsIUrlListener +// +NS_IMETHODIMP +nsSaveMsgListener::OnStartRunningUrl(nsIURI* url) { + if (mListener) mListener->OnStartRunningUrl(url); + return NS_OK; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStopRunningUrl(nsIURI* url, nsresult exitCode) { + nsresult rv = exitCode; + mUrlHasStopped = true; + + // ** save as template goes here + if (!m_templateUri.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> templateFolder; + rv = GetOrCreateFolder(m_templateUri, getter_AddRefs(templateFolder)); + if (NS_FAILED(rv)) goto done; + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1"); + if (copyService) { + nsCOMPtr<nsIFile> clone; + m_file->Clone(getter_AddRefs(clone)); + rv = copyService->CopyFileMessage(clone, templateFolder, nullptr, true, + nsMsgMessageFlags::Read, EmptyCString(), + this, nullptr); + // Clear this so we don't end up in a loop if OnStopRunningUrl gets + // called again. + m_templateUri.Truncate(); + } + } else if (m_outputStream && mRequestHasStopped) { + m_outputStream->Close(); + m_outputStream = nullptr; + } + +done: + if (NS_FAILED(rv)) { + if (m_file) m_file->Remove(false); + if (m_messenger) m_messenger->Alert("saveMessageFailed"); + } + + if (mRequestHasStopped && mListener) + mListener->OnStopRunningUrl(url, exitCode); + else + mListenerUri = url; + + return rv; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStartCopy(void) { return NS_OK; } + +NS_IMETHODIMP +nsSaveMsgListener::OnProgress(uint32_t aProgress, uint32_t aProgressMax) { + return NS_OK; +} + +NS_IMETHODIMP +nsSaveMsgListener::SetMessageKey(nsMsgKey aKey) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsSaveMsgListener::GetMessageId(nsACString& aMessageId) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStopCopy(nsresult aStatus) { + if (m_file) m_file->Remove(false); + return aStatus; +} + +// initializes the progress window if we are going to show one +// and for OSX, sets creator flags on the output file +nsresult nsSaveMsgListener::InitializeDownload(nsIRequest* aRequest) { + nsresult rv = NS_OK; + + mInitialized = true; + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + + if (!channel) return rv; + + // Get the max progress from the URL if we haven't already got it. + if (mMaxProgress == -1) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(uri)); + if (mailnewsUrl) mailnewsUrl->GetMaxProgress(&mMaxProgress); + } + + if (!m_contentType.IsEmpty()) { + nsCOMPtr<nsIMIMEService> mimeService( + do_GetService(NS_MIMESERVICE_CONTRACTID)); + nsCOMPtr<nsIMIMEInfo> mimeinfo; + + mimeService->GetFromTypeAndExtension(m_contentType, EmptyCString(), + getter_AddRefs(mimeinfo)); + + // create a download progress window + + // Set saveToDisk explicitly to avoid launching the saved file. + // See + // https://hg.mozilla.org/mozilla-central/file/814a6f071472/toolkit/components/jsdownloads/src/DownloadLegacy.js#l164 + mimeinfo->SetPreferredAction(nsIHandlerInfo::saveToDisk); + + // When we don't allow warnings, also don't show progress, as this + // is an environment (typically filters) where we don't want + // interruption. + bool allowProgress = true; + if (m_saveAllAttachmentsState) + allowProgress = !m_saveAllAttachmentsState->m_withoutWarning; + if (allowProgress) { + nsCOMPtr<nsITransfer> tr = do_CreateInstance(NS_TRANSFER_CONTRACTID, &rv); + if (tr && m_file) { + PRTime timeDownloadStarted = PR_Now(); + + nsCOMPtr<nsIURI> outputURI; + NS_NewFileURI(getter_AddRefs(outputURI), m_file); + + nsCOMPtr<nsIURI> url; + channel->GetURI(getter_AddRefs(url)); + rv = tr->Init(url, nullptr, outputURI, EmptyString(), mimeinfo, + timeDownloadStarted, nullptr, this, false, + nsITransfer::DOWNLOAD_ACCEPTABLE, nullptr, false); + + // now store the web progresslistener + mTransfer = tr; + } + } + } + return rv; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStartRequest(nsIRequest* request) { + if (m_file) + MsgNewBufferedFileOutputStream(getter_AddRefs(m_outputStream), m_file, -1, + ATTACHMENT_PERMISSION); + if (!m_outputStream) { + mCanceled = true; + if (m_messenger) m_messenger->Alert("saveAttachmentFailed"); + } + return NS_OK; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnStopRequest(nsIRequest* request, nsresult status) { + nsresult rv = NS_OK; + mRequestHasStopped = true; + + // rhp: If we are doing the charset conversion magic, this is different + // processing, otherwise, its just business as usual. + // If we need text/plain, then we need to convert the HTML and then convert + // to the systems charset. + if (m_doCharsetConversion && m_outputStream) { + // For HTML, code is emitted immediately in OnDataAvailable. + MOZ_ASSERT(m_outputFormat == ePlainText, + "For HTML, m_doCharsetConversion shouldn't be set"); + NS_ConvertUTF8toUTF16 utf16Buffer(m_msgBuffer); + ConvertBufToPlainText(utf16Buffer, false, false, false); + + nsCString outCString; + // NS_CopyUnicodeToNative() doesn't return an error, so we have no choice + // but to always use UTF-8. + CopyUTF16toUTF8(utf16Buffer, outCString); + uint32_t writeCount; + rv = m_outputStream->Write(outCString.get(), outCString.Length(), + &writeCount); + if (outCString.Length() != writeCount) rv = NS_ERROR_FAILURE; + } + + if (m_outputStream) { + m_outputStream->Close(); + m_outputStream = nullptr; + } + + // Are there more attachments to deal with? + nsSaveAllAttachmentsState* state = m_saveAllAttachmentsState; + if (state) { + state->m_curIndex++; + if (!mCanceled && state->m_curIndex < state->m_count) { + // Yes, start on the next attachment. + uint32_t i = state->m_curIndex; + nsString unescapedName; + RefPtr<nsLocalFile> localFile = + new nsLocalFile(nsTDependentString<PathChar>(state->m_directoryName)); + if (localFile->NativePath().IsEmpty()) { + rv = NS_ERROR_FAILURE; + goto done; + } + + ConvertAndSanitizeFileName(state->m_displayNameArray[i], unescapedName); + rv = localFile->Append(unescapedName); + if (NS_FAILED(rv)) goto done; + + // When we are running with no warnings (typically filters and other + // automatic uses), then don't prompt for duplicates, but create a unique + // file instead. + if (!state->m_withoutWarning) { + rv = m_messenger->PromptIfFileExists(localFile); + if (NS_FAILED(rv)) goto done; + } else { + rv = localFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, + ATTACHMENT_PERMISSION); + if (NS_FAILED(rv)) goto done; + } + // Start the next attachment saving. + // NOTE: null listener passed in on subsequent saves! The original + // listener will already have been invoked. + // See Bug 1789565 + rv = m_messenger->SaveAttachment( + localFile, state->m_urlArray[i], state->m_messageUriArray[i], + state->m_contentTypeArray[i], state, nullptr); + if (NS_FAILED(rv)) { + // If SaveAttachment() fails, state will have been deleted, and + // m_overallListener->OnStopRunningUrl() will have been called. + state = nullptr; + m_saveAllAttachmentsState = nullptr; + } + done: + if (NS_FAILED(rv) && state) { + if (state->m_overallListener) { + state->m_overallListener->OnStopRunningUrl(nullptr, rv); + } + delete state; + m_saveAllAttachmentsState = nullptr; + } + } else { + // All attachments have been saved. + if (state->m_overallListener) { + state->m_overallListener->OnStopRunningUrl( + nullptr, mCanceled ? NS_ERROR_FAILURE : NS_OK); + } + // Check if we're supposed to be detaching attachments after saving them. + if (state->m_detachingAttachments && !mCanceled) { + m_messenger->DetachAttachments( + state->m_contentTypeArray, state->m_urlArray, + state->m_displayNameArray, state->m_messageUriArray, + &state->m_savedFiles, nullptr, state->m_withoutWarning); + } + delete m_saveAllAttachmentsState; + m_saveAllAttachmentsState = nullptr; + } + } + + if (mTransfer) { + mTransfer->OnProgressChange64(nullptr, nullptr, mMaxProgress, mMaxProgress, + mMaxProgress, mMaxProgress); + mTransfer->OnStateChange(nullptr, nullptr, + nsIWebProgressListener::STATE_STOP | + nsIWebProgressListener::STATE_IS_NETWORK, + NS_OK); + mTransfer = nullptr; // break any circular dependencies between the + // progress dialog and use + } + + if (mUrlHasStopped && mListener) + mListener->OnStopRunningUrl(mListenerUri, rv); + + return NS_OK; +} + +NS_IMETHODIMP +nsSaveMsgListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStream, uint64_t srcOffset, + uint32_t count) { + nsresult rv = NS_ERROR_FAILURE; + // first, check to see if we've been canceled.... + if (mCanceled) // then go cancel our underlying channel too + return request->Cancel(NS_BINDING_ABORTED); + + if (!mInitialized) InitializeDownload(request); + + if (m_outputStream) { + mProgress += count; + uint64_t available; + uint32_t readCount, maxReadCount = sizeof(m_dataBuffer); + uint32_t writeCount; + rv = inStream->Available(&available); + while (NS_SUCCEEDED(rv) && available) { + if (maxReadCount > available) maxReadCount = (uint32_t)available; + rv = inStream->Read(m_dataBuffer, maxReadCount, &readCount); + + // rhp: + // Ok, now we do one of two things. If we are sending out HTML, then + // just write it to the HTML stream as it comes along...but if this is + // a save as TEXT operation, we need to buffer this up for conversion + // when we are done. When the stream converter for HTML-TEXT gets in + // place, this magic can go away. + // + if (NS_SUCCEEDED(rv)) { + if ((m_doCharsetConversion) && (m_outputFormat == ePlainText)) + m_msgBuffer.Append(Substring(m_dataBuffer, m_dataBuffer + readCount)); + else + rv = m_outputStream->Write(m_dataBuffer, readCount, &writeCount); + + available -= readCount; + } + } + + if (NS_SUCCEEDED(rv) && mTransfer) // Send progress notification. + mTransfer->OnProgressChange64(nullptr, request, mProgress, mMaxProgress, + mProgress, mMaxProgress); + } + return rv; +} + +#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties" + +nsresult nsMessenger::InitStringBundle() { + if (mStringBundle) return NS_OK; + + const char propertyURL[] = MESSENGER_STRING_URL; + nsCOMPtr<nsIStringBundleService> sBundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED); + return sBundleService->CreateBundle(propertyURL, + getter_AddRefs(mStringBundle)); +} + +void nsMessenger::GetString(const nsString& aStringName, nsString& aValue) { + nsresult rv; + aValue.Truncate(); + + if (!mStringBundle) InitStringBundle(); + + if (mStringBundle) + rv = mStringBundle->GetStringFromName( + NS_ConvertUTF16toUTF8(aStringName).get(), aValue); + else + rv = NS_ERROR_FAILURE; + + if (NS_FAILED(rv) || aValue.IsEmpty()) aValue = aStringName; + return; +} + +nsSaveAllAttachmentsState::nsSaveAllAttachmentsState( + const nsTArray<nsCString>& contentTypeArray, + const nsTArray<nsCString>& urlArray, + const nsTArray<nsCString>& displayNameArray, + const nsTArray<nsCString>& messageUriArray, const PathChar* dirName, + bool detachingAttachments, nsIUrlListener* overallListener) + : m_contentTypeArray(contentTypeArray.Clone()), + m_urlArray(urlArray.Clone()), + m_displayNameArray(displayNameArray.Clone()), + m_messageUriArray(messageUriArray.Clone()), + m_detachingAttachments(detachingAttachments), + m_overallListener(overallListener), + m_withoutWarning(false) { + m_count = contentTypeArray.Length(); + m_curIndex = 0; + m_directoryName = NS_xstrdup(dirName); +} + +nsSaveAllAttachmentsState::~nsSaveAllAttachmentsState() { + free(m_directoryName); +} + +nsresult nsMessenger::GetLastSaveDirectory(nsIFile** aLastSaveDir) { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // this can fail, and it will, on the first time we call it, as there is no + // default for this pref. + nsCOMPtr<nsIFile> localFile; + rv = prefBranch->GetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME, + NS_GET_IID(nsIFile), + getter_AddRefs(localFile)); + if (NS_SUCCEEDED(rv)) localFile.forget(aLastSaveDir); + return rv; +} + +nsresult nsMessenger::SetLastSaveDirectory(nsIFile* aLocalFile) { + NS_ENSURE_ARG_POINTER(aLocalFile); + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // if the file is a directory, just use it for the last dir chosen + // otherwise, use the parent of the file as the last dir chosen. + // IsDirectory() will return error on saving a file, as the + // file doesn't exist yet. + bool isDirectory; + rv = aLocalFile->IsDirectory(&isDirectory); + if (NS_SUCCEEDED(rv) && isDirectory) { + rv = prefBranch->SetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME, + NS_GET_IID(nsIFile), aLocalFile); + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsCOMPtr<nsIFile> parent; + rv = aLocalFile->GetParent(getter_AddRefs(parent)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = prefBranch->SetComplexValue(MESSENGER_SAVE_DIR_PREF_NAME, + NS_GET_IID(nsIFile), parent); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMessenger::FormatFileSize(uint64_t aSize, bool aUseKB, + nsAString& aFormattedSize) { + return ::FormatFileSize(aSize, aUseKB, aFormattedSize); +} + +/////////////////////////////////////////////////////////////////////////////// +// Detach/Delete Attachments +/////////////////////////////////////////////////////////////////////////////// + +static const char* GetAttachmentPartId(const char* aAttachmentUrl) { + static const char partIdPrefix[] = "part="; + const char* partId = PL_strstr(aAttachmentUrl, partIdPrefix); + return partId ? (partId + sizeof(partIdPrefix) - 1) : nullptr; +} + +static int CompareAttachmentPartId(const char* aAttachUrlLeft, + const char* aAttachUrlRight) { + // part ids are numbers separated by periods, like "1.2.3.4". + // we sort by doing a numerical comparison on each item in turn. e.g. "1.4" < + // "1.25" shorter entries come before longer entries. e.g. "1.4" < "1.4.1.2" + // return values: + // -2 left is a parent of right + // -1 left is less than right + // 0 left == right + // 1 right is greater than left + // 2 right is a parent of left + + const char* partIdLeft = GetAttachmentPartId(aAttachUrlLeft); + const char* partIdRight = GetAttachmentPartId(aAttachUrlRight); + + // for detached attachments the URL does not contain any "part=xx" + if (!partIdLeft) partIdLeft = "0"; + + if (!partIdRight) partIdRight = "0"; + + long idLeft, idRight; + do { + MOZ_ASSERT(partIdLeft && IS_DIGIT(*partIdLeft), + "Invalid character in part id string"); + MOZ_ASSERT(partIdRight && IS_DIGIT(*partIdRight), + "Invalid character in part id string"); + + // if the part numbers are different then the numerically smaller one is + // first + char* fixConstLoss; + idLeft = strtol(partIdLeft, &fixConstLoss, 10); + partIdLeft = fixConstLoss; + idRight = strtol(partIdRight, &fixConstLoss, 10); + partIdRight = fixConstLoss; + if (idLeft != idRight) return idLeft < idRight ? -1 : 1; + + // if one part id is complete but the other isn't, then the shortest one + // is first (parents before children) + if (*partIdLeft != *partIdRight) return *partIdRight ? -2 : 2; + + // if both part ids are complete (*partIdLeft == *partIdRight now) then + // they are equal + if (!*partIdLeft) return 0; + + MOZ_ASSERT(*partIdLeft == '.', "Invalid character in part id string"); + MOZ_ASSERT(*partIdRight == '.', "Invalid character in part id string"); + + ++partIdLeft; + ++partIdRight; + } while (true); +} + +// ------------------------------------ + +// struct on purpose -> show that we don't ever want a vtable +struct msgAttachment { + msgAttachment(const nsACString& aContentType, const nsACString& aUrl, + const nsACString& aDisplayName, const nsACString& aMessageUri) + : mContentType(aContentType), + mUrl(aUrl), + mDisplayName(aDisplayName), + mMessageUri(aMessageUri) {} + + nsCString mContentType; + nsCString mUrl; + nsCString mDisplayName; + nsCString mMessageUri; +}; + +// ------------------------------------ + +class nsAttachmentState { + public: + nsAttachmentState(); + nsresult Init(const nsTArray<nsCString>& aContentTypeArray, + const nsTArray<nsCString>& aUrlArray, + const nsTArray<nsCString>& aDisplayNameArray, + const nsTArray<nsCString>& aMessageUriArray); + nsresult PrepareForAttachmentDelete(); + + private: + static int CompareAttachmentsByPartId(const void* aLeft, const void* aRight); + + public: + uint32_t mCurIndex; + nsTArray<msgAttachment> mAttachmentArray; +}; + +nsAttachmentState::nsAttachmentState() : mCurIndex(0) {} + +nsresult nsAttachmentState::Init(const nsTArray<nsCString>& aContentTypeArray, + const nsTArray<nsCString>& aUrlArray, + const nsTArray<nsCString>& aDisplayNameArray, + const nsTArray<nsCString>& aMessageUriArray) { + MOZ_ASSERT(aContentTypeArray.Length() > 0); + MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() && + aUrlArray.Length() == aDisplayNameArray.Length() && + aDisplayNameArray.Length() == aMessageUriArray.Length()); + + uint32_t count = aContentTypeArray.Length(); + mCurIndex = 0; + mAttachmentArray.Clear(); + mAttachmentArray.SetCapacity(count); + + for (uint32_t u = 0; u < count; ++u) { + mAttachmentArray.AppendElement( + msgAttachment(aContentTypeArray[u], aUrlArray[u], aDisplayNameArray[u], + aMessageUriArray[u])); + } + + return NS_OK; +} + +nsresult nsAttachmentState::PrepareForAttachmentDelete() { + // this must be called before any processing + if (mCurIndex != 0) return NS_ERROR_FAILURE; + + // this prepares the attachment list for use in deletion. In order to prepare, + // we sort the attachments in numerical ascending order on their part id, + // remove all duplicates and remove any subparts which will be removed + // automatically by the removal of the parent. + // + // e.g. the attachment list processing (showing only part ids) + // before: 1.11, 1.3, 1.2, 1.2.1.3, 1.4.1.2 + // sorted: 1.2, 1.2.1.3, 1.3, 1.4.1.2, 1.11 + // after: 1.2, 1.3, 1.4.1.2, 1.11 + + // sort + qsort(mAttachmentArray.Elements(), mAttachmentArray.Length(), + sizeof(msgAttachment), CompareAttachmentsByPartId); + + // remove duplicates and sub-items + int nCompare; + for (uint32_t u = 1; u < mAttachmentArray.Length();) { + nCompare = ::CompareAttachmentPartId(mAttachmentArray[u - 1].mUrl.get(), + mAttachmentArray[u].mUrl.get()); + if (nCompare == 0 || + nCompare == -2) // [u-1] is the same as or a parent of [u] + { + // shuffle the array down (and thus keeping the sorted order) + mAttachmentArray.RemoveElementAt(u); + } else { + ++u; + } + } + + return NS_OK; +} + +// Static compare callback for sorting. +int nsAttachmentState::CompareAttachmentsByPartId(const void* aLeft, + const void* aRight) { + msgAttachment& attachLeft = *((msgAttachment*)aLeft); + msgAttachment& attachRight = *((msgAttachment*)aRight); + return ::CompareAttachmentPartId(attachLeft.mUrl.get(), + attachRight.mUrl.get()); +} + +// ------------------------------------ +// Helper class to coordinate deleting attachments from a message. +// +// Implementation notes: +// The basic technique is to use nsIMsgMessageService.streamMessage() to +// stream the message through a streamconverter which is set up to strip +// out the attachments. The result is written out to a temporary file, +// which is then copied over the old message using +// nsIMsgCopyService.copyFileMessage() and the old message deleted with +// nsIMsgFolder.deleteMessages(). Phew. +// +// The nsIStreamListener, nsIUrlListener and nsIMsgCopyServiceListener +// inheritances here are just unfortunately-exposed implementation details. +// And they are a bit of a mess. Some are used multiple times, for different +// phases of the operation. So we use m_state to keep track. +class AttachmentDeleter : public nsIStreamListener, + public nsIUrlListener, + public nsIMsgCopyServiceListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGCOPYSERVICELISTENER + + public: + AttachmentDeleter(); + nsresult StartProcessing(nsMessenger* aMessenger, nsIMsgWindow* aMsgWindow, + nsAttachmentState* aAttach, bool aSaveFirst); + + public: + nsAttachmentState* mAttach; // list of attachments to process + bool mSaveFirst; // detach (true) or delete (false) + nsCOMPtr<nsIFile> mMsgFile; // temporary file (processed mail) + nsCOMPtr<nsIOutputStream> mMsgFileStream; // temporary file (processed mail) + nsCOMPtr<nsIMsgMessageService> mMessageService; // original message service + nsCOMPtr<nsIMsgDBHdr> mOriginalMessage; // original message header + nsCOMPtr<nsIMsgFolder> mMessageFolder; // original message folder + nsCOMPtr<nsIMessenger> mMessenger; // our messenger instance + nsCOMPtr<nsIMsgWindow> mMsgWindow; // our UI window + nsMsgKey mOriginalMessageKey; // old message key + nsMsgKey mNewMessageKey; // new message key + uint32_t mOrigMsgFlags; + + enum { + eStarting, + eCopyingNewMsg, + eUpdatingFolder, // for IMAP + eDeletingOldMessage, + eSelectingNewMessage + } m_state; + // temp + nsTArray<nsCString> mDetachedFileUris; + + // The listener to invoke when the full operation is complete. + nsCOMPtr<nsIUrlListener> mListener; + + private: + nsresult InternalStartProcessing(nsMessenger* aMessenger, + nsIMsgWindow* aMsgWindow, + nsAttachmentState* aAttach, bool aSaveFirst); + nsresult DeleteOriginalMessage(); + virtual ~AttachmentDeleter(); +}; + +// +// nsISupports +// +NS_IMPL_ISUPPORTS(AttachmentDeleter, nsIStreamListener, nsIRequestObserver, + nsIUrlListener, nsIMsgCopyServiceListener) + +// +// nsIRequestObserver +// +NS_IMETHODIMP +AttachmentDeleter::OnStartRequest(nsIRequest* aRequest) { + // called when we start processing the StreamMessage request. + // This is called after OnStartRunningUrl(). + return NS_OK; +} + +NS_IMETHODIMP +AttachmentDeleter::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) { + // called when we have completed processing the StreamMessage request. + // This is called before OnStopRunningUrl(). This means that we have now + // received all data of the message and we have completed processing. + // We now start to copy the processed message from the temporary file + // back into the message store, replacing the original message. + + mMessageFolder->CopyDataDone(); + if (NS_FAILED(aStatusCode)) return aStatusCode; + + // copy the file back into the folder. Note: setting msgToReplace only copies + // metadata, so we do the delete ourselves + nsCOMPtr<nsIMsgCopyServiceListener> listenerCopyService; + nsresult rv = this->QueryInterface(NS_GET_IID(nsIMsgCopyServiceListener), + getter_AddRefs(listenerCopyService)); + NS_ENSURE_SUCCESS(rv, rv); + + mMsgFileStream->Close(); + mMsgFileStream = nullptr; + mNewMessageKey = nsMsgKey_None; + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1"); + m_state = eCopyingNewMsg; + // clone file because nsIFile on Windows caches the wrong file size. + nsCOMPtr<nsIFile> clone; + mMsgFile->Clone(getter_AddRefs(clone)); + if (copyService) { + nsCString originalKeys; + mOriginalMessage->GetStringProperty("keywords", originalKeys); + rv = copyService->CopyFileMessage(clone, mMessageFolder, mOriginalMessage, + false, mOrigMsgFlags, originalKeys, + listenerCopyService, mMsgWindow); + } + return rv; +} + +// +// nsIStreamListener +// + +NS_IMETHODIMP +AttachmentDeleter::OnDataAvailable(nsIRequest* aRequest, + nsIInputStream* aInStream, + uint64_t aSrcOffset, uint32_t aCount) { + if (!mMsgFileStream) return NS_ERROR_NULL_POINTER; + return mMessageFolder->CopyDataToOutputStreamForAppend(aInStream, aCount, + mMsgFileStream); +} + +// +// nsIUrlListener +// + +NS_IMETHODIMP +AttachmentDeleter::OnStartRunningUrl(nsIURI* aUrl) { + // called when we start processing the StreamMessage request. This is + // called before OnStartRequest(). + return NS_OK; +} + +nsresult AttachmentDeleter::DeleteOriginalMessage() { + nsCOMPtr<nsIMsgCopyServiceListener> listenerCopyService; + QueryInterface(NS_GET_IID(nsIMsgCopyServiceListener), + getter_AddRefs(listenerCopyService)); + + mOriginalMessage->SetUint32Property("attachmentDetached", 1); + RefPtr<nsIMsgDBHdr> doomed(mOriginalMessage); + mOriginalMessage = nullptr; + m_state = eDeletingOldMessage; + return mMessageFolder->DeleteMessages({doomed}, // messages + mMsgWindow, // msgWindow + true, // deleteStorage + false, // isMove + listenerCopyService, // listener + false); // allowUndo +} + +// This is called (potentially) multiple times. +// Firstly, as a result of StreamMessage() (when the message is being passed +// through a streamconverter to strip the attachments). +// Secondly, after the DeleteMessages() call. But maybe not for IMAP? +// Maybe also after CopyFileMessage()? Gah. +NS_IMETHODIMP +AttachmentDeleter::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) { + nsresult rv = NS_OK; + if (mOriginalMessage && m_state == eUpdatingFolder) + rv = DeleteOriginalMessage(); + + return rv; +} + +// +// nsIMsgCopyServiceListener +// + +NS_IMETHODIMP +AttachmentDeleter::OnStartCopy(void) { + // never called? + return NS_OK; +} + +NS_IMETHODIMP +AttachmentDeleter::OnProgress(uint32_t aProgress, uint32_t aProgressMax) { + // never called? + return NS_OK; +} + +NS_IMETHODIMP +AttachmentDeleter::SetMessageKey(nsMsgKey aKey) { + // called during the copy of the modified message back into the message + // store to notify us of the message key of the newly created message. + mNewMessageKey = aKey; + + nsCString folderURI; + nsresult rv = mMessageFolder->GetURI(folderURI); + NS_ENSURE_SUCCESS(rv, rv); + + mozilla::JSONStringWriteFunc<nsCString> jsonString; + mozilla::JSONWriter data(jsonString); + data.Start(); + data.IntProperty("oldMessageKey", mOriginalMessageKey); + data.IntProperty("newMessageKey", aKey); + data.StringProperty("folderURI", folderURI); + data.End(); + + nsCOMPtr<nsIObserverService> obs = services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "attachment-delete-msgkey-changed", + NS_ConvertUTF8toUTF16(jsonString.StringCRef()).get()); + } + return NS_OK; +} + +NS_IMETHODIMP +AttachmentDeleter::GetMessageId(nsACString& aMessageId) { + // never called? + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +AttachmentDeleter::OnStopCopy(nsresult aStatus) { + // This is called via `CopyFileMessage()` and `DeleteMessages()`. + // `m_state` tells us which callback it is. + if (m_state == eDeletingOldMessage) { + m_state = eSelectingNewMessage; + + // OK... that's it. The entire operation is now done. + // (there may still be another call to OnStopRunningUrl(), but that'll be + // a no-op in this state). + if (mListener) { + mListener->OnStopRunningUrl(nullptr, aStatus); + } + return NS_OK; + } + + // For non-IMAP messages, the original is deleted here, for IMAP messages + // that happens in `OnStopRunningUrl()` which isn't called for non-IMAP + // messages. + const nsACString& messageUri = mAttach->mAttachmentArray[0].mMessageUri; + if (mOriginalMessage && + !Substring(messageUri, 0, 13).EqualsLiteral("imap-message:")) { + return DeleteOriginalMessage(); + } else { + // Arrange for the message to be deleted in the next `OnStopRunningUrl()` + // call. + m_state = eUpdatingFolder; + } + + return NS_OK; +} + +// +// local methods +// + +AttachmentDeleter::AttachmentDeleter() + : mAttach(nullptr), + mSaveFirst(false), + mOriginalMessageKey(nsMsgKey_None), + mNewMessageKey(nsMsgKey_None), + mOrigMsgFlags(0), + m_state(eStarting) {} + +AttachmentDeleter::~AttachmentDeleter() { + if (mAttach) { + delete mAttach; + } + if (mMsgFileStream) { + mMsgFileStream->Close(); + mMsgFileStream = nullptr; + } + if (mMsgFile) { + mMsgFile->Remove(false); + } +} + +nsresult AttachmentDeleter::StartProcessing(nsMessenger* aMessenger, + nsIMsgWindow* aMsgWindow, + nsAttachmentState* aAttach, + bool detaching) { + if (mListener) { + mListener->OnStartRunningUrl(nullptr); + } + + nsresult rv = + InternalStartProcessing(aMessenger, aMsgWindow, aAttach, detaching); + if (NS_FAILED(rv)) { + if (mListener) { + mListener->OnStopRunningUrl(nullptr, rv); + } + } + return rv; +} + +nsresult AttachmentDeleter::InternalStartProcessing(nsMessenger* aMessenger, + nsIMsgWindow* aMsgWindow, + nsAttachmentState* aAttach, + bool detaching) { + aMessenger->QueryInterface(NS_GET_IID(nsIMessenger), + getter_AddRefs(mMessenger)); + mMsgWindow = aMsgWindow; + mAttach = aAttach; + + nsresult rv; + + // all attachments refer to the same message + const nsCString& messageUri = mAttach->mAttachmentArray[0].mMessageUri; + + // get the message service, original message and folder for this message + rv = GetMessageServiceFromURI(messageUri, getter_AddRefs(mMessageService)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mMessageService->MessageURIToMsgHdr(messageUri, + getter_AddRefs(mOriginalMessage)); + NS_ENSURE_SUCCESS(rv, rv); + rv = mOriginalMessage->GetMessageKey(&mOriginalMessageKey); + NS_ENSURE_SUCCESS(rv, rv); + rv = mOriginalMessage->GetFolder(getter_AddRefs(mMessageFolder)); + NS_ENSURE_SUCCESS(rv, rv); + mOriginalMessage->GetFlags(&mOrigMsgFlags); + + // ensure that we can store and delete messages in this folder, if we + // can't then we can't do attachment deleting + bool canDelete = false; + mMessageFolder->GetCanDeleteMessages(&canDelete); + bool canFile = false; + mMessageFolder->GetCanFileMessages(&canFile); + if (!canDelete || !canFile) return NS_ERROR_FAILURE; + + // create an output stream on a temporary file. This stream will save the + // modified message data to a file which we will later use to replace the + // existing message. The file is removed in the destructor. + rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "nsmail.tmp", + getter_AddRefs(mMsgFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // For temp file, we should use restrictive 00600 instead of + // ATTACHMENT_PERMISSION + rv = mMsgFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600); + NS_ENSURE_SUCCESS(rv, rv); + + rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mMsgFileStream), mMsgFile, + -1, ATTACHMENT_PERMISSION); + + // Create the additional header for data conversion. This will tell the stream + // converter which MIME emitter we want to use, and it will tell the MIME + // emitter which attachments should be deleted. + // It also supplies the path of the already-saved attachments, so that + // path can be noted in the message, where those attachements are removed. + // The X-Mozilla-External-Attachment-URL header will be added, with the + // location of the saved attachment. + const char* partId; + const char* nextField; + nsAutoCString sHeader("attach&del="); + nsAutoCString detachToHeader("&detachTo="); + for (uint32_t u = 0; u < mAttach->mAttachmentArray.Length(); ++u) { + if (u > 0) { + sHeader.Append(','); + if (detaching) detachToHeader.Append(','); + } + partId = GetAttachmentPartId(mAttach->mAttachmentArray[u].mUrl.get()); + if (partId) { + nextField = PL_strchr(partId, '&'); + sHeader.Append(partId, nextField ? nextField - partId : -1); + } + if (detaching) { + // The URI can contain commas, so percent-encode those first. + nsAutoCString uri(mDetachedFileUris[u]); + int ind = uri.FindChar(','); + while (ind != kNotFound) { + uri.Replace(ind, 1, "%2C"); + ind = uri.FindChar(','); + } + detachToHeader.Append(uri); + } + } + + if (detaching) sHeader.Append(detachToHeader); + // stream this message to our listener converting it via the attachment mime + // converter. The listener will just write the converted message straight to + // disk. + nsCOMPtr<nsISupports> listenerSupports; + rv = this->QueryInterface(NS_GET_IID(nsISupports), + getter_AddRefs(listenerSupports)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIUrlListener> listenerUrlListener = + do_QueryInterface(listenerSupports, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURI> dummyNull; + rv = mMessageService->StreamMessage(messageUri, listenerSupports, mMsgWindow, + listenerUrlListener, true, sHeader, false, + getter_AddRefs(dummyNull)); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// ------------------------------------ + +NS_IMETHODIMP +nsMessenger::DetachAttachment(const nsACString& aContentType, + const nsACString& aURL, + const nsACString& aDisplayName, + const nsACString& aMessageUri, bool aSaveFirst, + bool withoutWarning = false) { + if (aSaveFirst) + return SaveOneAttachment(aContentType, aURL, aDisplayName, aMessageUri, + true); + AutoTArray<nsCString, 1> contentTypeArray = { + PromiseFlatCString(aContentType)}; + AutoTArray<nsCString, 1> urlArray = {PromiseFlatCString(aURL)}; + AutoTArray<nsCString, 1> displayNameArray = { + PromiseFlatCString(aDisplayName)}; + AutoTArray<nsCString, 1> messageUriArray = {PromiseFlatCString(aMessageUri)}; + return DetachAttachments(contentTypeArray, urlArray, displayNameArray, + messageUriArray, nullptr, nullptr, withoutWarning); +} + +NS_IMETHODIMP +nsMessenger::DetachAllAttachments(const nsTArray<nsCString>& aContentTypeArray, + const nsTArray<nsCString>& aUrlArray, + const nsTArray<nsCString>& aDisplayNameArray, + const nsTArray<nsCString>& aMessageUriArray, + bool aSaveFirst, + bool withoutWarning = false) { + NS_ENSURE_ARG_MIN(aContentTypeArray.Length(), 1); + MOZ_ASSERT(aContentTypeArray.Length() == aUrlArray.Length() && + aUrlArray.Length() == aDisplayNameArray.Length() && + aDisplayNameArray.Length() == aMessageUriArray.Length()); + + if (aSaveFirst) + return SaveAllAttachments(aContentTypeArray, aUrlArray, aDisplayNameArray, + aMessageUriArray, true); + else + return DetachAttachments(aContentTypeArray, aUrlArray, aDisplayNameArray, + aMessageUriArray, nullptr, nullptr, + withoutWarning); +} + +nsresult nsMessenger::DetachAttachments( + const nsTArray<nsCString>& aContentTypeArray, + const nsTArray<nsCString>& aUrlArray, + const nsTArray<nsCString>& aDisplayNameArray, + const nsTArray<nsCString>& aMessageUriArray, + nsTArray<nsCString>* saveFileUris, nsIUrlListener* aListener, + bool withoutWarning) { + // if withoutWarning no dialog for user + if (!withoutWarning && NS_FAILED(PromptIfDeleteAttachments( + saveFileUris != nullptr, aDisplayNameArray))) + return NS_OK; + + nsresult rv = NS_OK; + + // ensure that our arguments are valid + // char * partId; + for (uint32_t u = 0; u < aContentTypeArray.Length(); ++u) { + // ensure all of the message URI are the same, we cannot process + // attachments from different messages + if (u > 0 && aMessageUriArray[0] != aMessageUriArray[u]) { + rv = NS_ERROR_INVALID_ARG; + break; + } + + // ensure that we don't have deleted messages in this list + if (aContentTypeArray[u].EqualsLiteral(MIMETYPE_DELETED)) { + rv = NS_ERROR_INVALID_ARG; + break; + } + + // for the moment we prevent any attachments other than root level + // attachments being deleted (i.e. you can't delete attachments from a + // email forwarded as an attachment). We do this by ensuring that the + // part id only has a single period in it (e.g. "1.2"). + // TODO: support non-root level attachment delete + // partId = ::GetAttachmentPartId(aUrlArray[u]); + // if (!partId || PL_strchr(partId, '.') != PL_strrchr(partId, '.')) + // { + // rv = NS_ERROR_INVALID_ARG; + // break; + // } + } + if (NS_FAILED(rv)) { + Alert("deleteAttachmentFailure"); + return rv; + } + + // TODO: ensure that nothing else is processing this message uri at the same + // time + + // TODO: if any of the selected attachments are messages that contain other + // attachments we need to warn the user that all sub-attachments of those + // messages will also be deleted. Best to display a list of them. + + RefPtr<AttachmentDeleter> deleter = new AttachmentDeleter; + deleter->mListener = aListener; + if (saveFileUris) { + deleter->mDetachedFileUris = saveFileUris->Clone(); + } + // create the attachments for use by the deleter + nsAttachmentState* attach = new nsAttachmentState; + rv = attach->Init(aContentTypeArray, aUrlArray, aDisplayNameArray, + aMessageUriArray); + if (NS_SUCCEEDED(rv)) rv = attach->PrepareForAttachmentDelete(); + if (NS_FAILED(rv)) { + delete attach; + return rv; + } + + // initialize our deleter with the attachments and details. The deleter + // takes ownership of 'attach' immediately irrespective of the return value + // (error or not). + return deleter->StartProcessing(this, mMsgWindow, attach, + saveFileUris != nullptr); +} + +nsresult nsMessenger::PromptIfDeleteAttachments( + bool aSaveFirst, const nsTArray<nsCString>& aDisplayNameArray) { + nsresult rv = NS_ERROR_FAILURE; + + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(mDocShell)); + if (!dialog) return rv; + + if (!mStringBundle) { + rv = InitStringBundle(); + NS_ENSURE_SUCCESS(rv, rv); + } + + // create the list of attachments we are removing + nsString displayString; + nsString attachmentList; + for (uint32_t u = 0; u < aDisplayNameArray.Length(); ++u) { + ConvertAndSanitizeFileName(aDisplayNameArray[u], displayString); + attachmentList.Append(displayString); + attachmentList.Append(char16_t('\n')); + } + AutoTArray<nsString, 1> formatStrings = {attachmentList}; + + // format the message and display + nsString promptMessage; + const char* propertyName = + aSaveFirst ? "detachAttachments" : "deleteAttachments"; + rv = mStringBundle->FormatStringFromName(propertyName, formatStrings, + promptMessage); + NS_ENSURE_SUCCESS(rv, rv); + + bool dialogResult = false; + rv = dialog->Confirm(nullptr, promptMessage.get(), &dialogResult); + NS_ENSURE_SUCCESS(rv, rv); + + return dialogResult ? NS_OK : NS_ERROR_FAILURE; +} diff --git a/comm/mailnews/base/src/nsMessenger.h b/comm/mailnews/base/src/nsMessenger.h new file mode 100644 index 0000000000..b6eab3f179 --- /dev/null +++ b/comm/mailnews/base/src/nsMessenger.h @@ -0,0 +1,118 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsMsgAppCore_h +#define __nsMsgAppCore_h + +#include "nscore.h" +#include "nsIMessenger.h" +#include "nsCOMPtr.h" +#include "nsITransactionManager.h" +#include "nsIFile.h" +#include "nsIDocShell.h" +#include "nsString.h" +#include "nsIStringBundle.h" +#include "nsIFile.h" +#include "nsIFilePicker.h" +#include "nsWeakReference.h" +#include "mozIDOMWindow.h" +#include "nsTArray.h" +#include "nsIMsgStatusFeedback.h" + +class nsSaveAllAttachmentsState; + +class nsMessenger : public nsIMessenger, public nsSupportsWeakReference { + using PathString = mozilla::PathString; + + public: + nsMessenger(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMESSENGER + + nsresult Alert(const char* stringName); + + nsresult SaveAttachment(nsIFile* file, const nsACString& unescapedUrl, + const nsACString& messageUri, + const nsACString& contentType, + nsSaveAllAttachmentsState* saveState, + nsIUrlListener* aListener); + nsresult PromptIfFileExists(nsIFile* file); + nsresult DetachAttachments(const nsTArray<nsCString>& aContentTypeArray, + const nsTArray<nsCString>& aUrlArray, + const nsTArray<nsCString>& aDisplayNameArray, + const nsTArray<nsCString>& aMessageUriArray, + nsTArray<nsCString>* saveFileUris, + nsIUrlListener* aListener, + bool withoutWarning = false); + nsresult SaveAllAttachments(const nsTArray<nsCString>& contentTypeArray, + const nsTArray<nsCString>& urlArray, + const nsTArray<nsCString>& displayNameArray, + const nsTArray<nsCString>& messageUriArray, + bool detaching); + nsresult SaveOneAttachment(const nsACString& aContentType, + const nsACString& aURL, + const nsACString& aDisplayName, + const nsACString& aMessageUri, bool detaching); + + protected: + virtual ~nsMessenger(); + + void GetString(const nsString& aStringName, nsString& stringValue); + nsresult InitStringBundle(); + nsresult PromptIfDeleteAttachments( + bool saveFirst, const nsTArray<nsCString>& displayNameArray); + + private: + nsresult GetLastSaveDirectory(nsIFile** aLastSaveAsDir); + // if aLocalFile is a dir, we use it. otherwise, we use the parent of + // aLocalFile. + nsresult SetLastSaveDirectory(nsIFile* aLocalFile); + + nsresult AdjustFileIfNameTooLong(nsIFile* aFile); + + nsresult GetSaveAsFile(const nsAString& aMsgFilename, + int32_t* aSaveAsFileType, nsIFile** aSaveAsFile); + + nsresult GetSaveToDir(nsIFile** aSaveToDir); + nsresult ShowPicker(nsIFilePicker* aPicker, + nsIFilePicker::ResultCode* aResult); + + class nsFilePickerShownCallback : public nsIFilePickerShownCallback { + virtual ~nsFilePickerShownCallback() {} + + public: + nsFilePickerShownCallback(); + NS_DECL_ISUPPORTS + + NS_IMETHOD Done(nsIFilePicker::ResultCode aResult) override; + + public: + bool mPickerDone; + nsIFilePicker::ResultCode mResult; + }; + + nsString mId; + nsCOMPtr<nsITransactionManager> mTxnMgr; + + /* rhp - need this to drive message display */ + nsCOMPtr<mozIDOMWindowProxy> mWindow; + nsCOMPtr<nsIMsgWindow> mMsgWindow; + nsCOMPtr<nsIDocShell> mDocShell; + + // String bundles... + nsCOMPtr<nsIStringBundle> mStringBundle; + + nsCOMPtr<nsISupports> mSearchContext; +}; + +#define NS_MESSENGER_CID \ + { /* f436a174-e2c0-4955-9afe-e3feb68aee56 */ \ + 0xf436a174, 0xe2c0, 0x4955, { \ + 0x9a, 0xfe, 0xe3, 0xfe, 0xb6, 0x8a, 0xee, 0x56 \ + } \ + } + +#endif diff --git a/comm/mailnews/base/src/nsMessengerBootstrap.cpp b/comm/mailnews/base/src/nsMessengerBootstrap.cpp new file mode 100644 index 0000000000..9d01b5380d --- /dev/null +++ b/comm/mailnews/base/src/nsMessengerBootstrap.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMessengerBootstrap.h" +#include "nsCOMPtr.h" + +#include "nsIMutableArray.h" +#include "nsIMsgFolder.h" +#include "nsIWindowWatcher.h" +#include "nsMsgUtils.h" +#include "nsISupportsPrimitives.h" +#include "mozIDOMWindow.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" + +NS_IMPL_ISUPPORTS(nsMessengerBootstrap, nsIMessengerWindowService) + +nsMessengerBootstrap::nsMessengerBootstrap() {} + +nsMessengerBootstrap::~nsMessengerBootstrap() {} + +NS_IMETHODIMP nsMessengerBootstrap::OpenMessengerWindowWithUri( + const char* windowType, const nsACString& aFolderURI, + nsMsgKey aMessageKey) { + bool standAloneMsgWindow = false; + nsAutoCString chromeUrl("chrome://messenger/content/"); + if (windowType && !strcmp(windowType, "mail:messageWindow")) { + chromeUrl.AppendLiteral("messageWindow.xhtml"); + standAloneMsgWindow = true; + } else { + chromeUrl.AppendLiteral("messenger.xhtml"); + } + nsresult rv; + nsCOMPtr<nsIMutableArray> argsArray( + do_CreateInstance(NS_ARRAY_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // create scriptable versions of our strings that we can store in our + // nsIMutableArray.... + if (!aFolderURI.IsEmpty()) { + if (standAloneMsgWindow) { + nsCOMPtr<nsIMsgFolder> folder; + rv = GetExistingFolder(aFolderURI, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString msgUri; + folder->GetBaseMessageURI(msgUri); + + nsCOMPtr<nsISupportsCString> scriptableMsgURI( + do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableMsgURI, NS_ERROR_FAILURE); + msgUri.Append('#'); + msgUri.AppendInt(aMessageKey, 10); + scriptableMsgURI->SetData(msgUri); + argsArray->AppendElement(scriptableMsgURI); + } + nsCOMPtr<nsISupportsCString> scriptableFolderURI( + do_CreateInstance(NS_SUPPORTS_CSTRING_CONTRACTID)); + NS_ENSURE_TRUE(scriptableFolderURI, NS_ERROR_FAILURE); + + scriptableFolderURI->SetData(aFolderURI); + argsArray->AppendElement(scriptableFolderURI); + + if (!standAloneMsgWindow) { + nsCOMPtr<nsISupportsPRUint32> scriptableMessageKey( + do_CreateInstance(NS_SUPPORTS_PRUINT32_CONTRACTID)); + NS_ENSURE_TRUE(scriptableMessageKey, NS_ERROR_FAILURE); + scriptableMessageKey->SetData(aMessageKey); + argsArray->AppendElement(scriptableMessageKey); + } + } + + nsCOMPtr<nsIWindowWatcher> wwatch( + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // we need to use the "mailnews.reuse_thread_window2" pref + // to determine if we should open a new window, or use an existing one. + nsCOMPtr<mozIDOMWindowProxy> newWindow; + return wwatch->OpenWindow(0, chromeUrl, "_blank"_ns, + "chrome,all,dialog=no"_ns, argsArray, + getter_AddRefs(newWindow)); +} diff --git a/comm/mailnews/base/src/nsMessengerBootstrap.h b/comm/mailnews/base/src/nsMessengerBootstrap.h new file mode 100644 index 0000000000..a81e81eeb9 --- /dev/null +++ b/comm/mailnews/base/src/nsMessengerBootstrap.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsMessenger_h +#define __nsMessenger_h + +#include "nscore.h" +#include "nsIMessengerWindowService.h" + +#define NS_MESSENGERBOOTSTRAP_CID \ + { /* 4a85a5d0-cddd-11d2-b7f6-00805f05ffa5 */ \ + 0x4a85a5d0, 0xcddd, 0x11d2, { \ + 0xb7, 0xf6, 0x00, 0x80, 0x5f, 0x05, 0xff, 0xa5 \ + } \ + } + +class nsMessengerBootstrap : public nsIMessengerWindowService { + public: + nsMessengerBootstrap(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMESSENGERWINDOWSERVICE + + private: + virtual ~nsMessengerBootstrap(); +}; + +#endif diff --git a/comm/mailnews/base/src/nsMessengerOSXIntegration.h b/comm/mailnews/base/src/nsMessengerOSXIntegration.h new file mode 100644 index 0000000000..e4b503cb12 --- /dev/null +++ b/comm/mailnews/base/src/nsMessengerOSXIntegration.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsMessengerOSXIntegration_h +#define __nsMessengerOSXIntegration_h + +#include "nsIMessengerOSIntegration.h" + +class nsMessengerOSXIntegration : public nsIMessengerOSIntegration { + public: + nsMessengerOSXIntegration(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMESSENGEROSINTEGRATION + + private: + virtual ~nsMessengerOSXIntegration(); + + nsresult RestoreDockIcon(); +}; + +#endif // __nsMessengerOSXIntegration_h diff --git a/comm/mailnews/base/src/nsMessengerOSXIntegration.mm b/comm/mailnews/base/src/nsMessengerOSXIntegration.mm new file mode 100644 index 0000000000..858e1417df --- /dev/null +++ b/comm/mailnews/base/src/nsMessengerOSXIntegration.mm @@ -0,0 +1,63 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMessengerOSXIntegration.h" +#include "nsObjCExceptions.h" +#include "nsString.h" +#include "mozilla/ErrorResult.h" + +#include <Carbon/Carbon.h> +#import <Cocoa/Cocoa.h> + +nsMessengerOSXIntegration::nsMessengerOSXIntegration() {} + +nsMessengerOSXIntegration::~nsMessengerOSXIntegration() {} + +NS_IMPL_ADDREF(nsMessengerOSXIntegration) +NS_IMPL_RELEASE(nsMessengerOSXIntegration) + +NS_INTERFACE_MAP_BEGIN(nsMessengerOSXIntegration) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMessengerOSIntegration) + NS_INTERFACE_MAP_ENTRY(nsIMessengerOSIntegration) +NS_INTERFACE_MAP_END + +nsresult nsMessengerOSXIntegration::RestoreDockIcon() { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + id tile = [[NSApplication sharedApplication] dockTile]; + [tile setBadgeLabel:nil]; + + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +NS_IMETHODIMP +nsMessengerOSXIntegration::UpdateUnreadCount(uint32_t unreadCount, const nsAString& unreadTooltip) { + NS_OBJC_BEGIN_TRY_BLOCK_RETURN; + + if (unreadCount == 0) { + RestoreDockIcon(); + return NS_OK; + } + + nsAutoString total; + if (unreadCount > 99) { + total.AppendLiteral("99+"); + } else { + total.AppendInt(unreadCount); + } + id tile = [[NSApplication sharedApplication] dockTile]; + [tile setBadgeLabel:[NSString stringWithFormat:@"%S", (const unichar*)total.get()]]; + return NS_OK; + + NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); +} + +NS_IMETHODIMP +nsMessengerOSXIntegration::OnExit() { + RestoreDockIcon(); + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMessengerUnixIntegration.cpp b/comm/mailnews/base/src/nsMessengerUnixIntegration.cpp new file mode 100644 index 0000000000..96ddb9c48a --- /dev/null +++ b/comm/mailnews/base/src/nsMessengerUnixIntegration.cpp @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMessengerUnixIntegration.h" +#include "nsString.h" + +/** + * This is only a placeholder for now, register it in components.conf later if + * needed. + */ +nsMessengerUnixIntegration::nsMessengerUnixIntegration() {} + +NS_IMPL_ISUPPORTS(nsMessengerUnixIntegration, nsIMessengerOSIntegration) + +NS_IMETHODIMP +nsMessengerUnixIntegration::UpdateUnreadCount(uint32_t unreadCount, + const nsAString& unreadTooltip) { + return NS_OK; +} + +NS_IMETHODIMP +nsMessengerUnixIntegration::OnExit() { return NS_OK; } diff --git a/comm/mailnews/base/src/nsMessengerUnixIntegration.h b/comm/mailnews/base/src/nsMessengerUnixIntegration.h new file mode 100644 index 0000000000..180f268560 --- /dev/null +++ b/comm/mailnews/base/src/nsMessengerUnixIntegration.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsMessengerUnixIntegration_h +#define __nsMessengerUnixIntegration_h + +#include "nsIMessengerOSIntegration.h" + +class nsMessengerUnixIntegration : public nsIMessengerOSIntegration { + public: + nsMessengerUnixIntegration(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMESSENGEROSINTEGRATION + + private: + virtual ~nsMessengerUnixIntegration() {} +}; + +#endif // __nsMessengerUnixIntegration_h diff --git a/comm/mailnews/base/src/nsMessengerWinIntegration.cpp b/comm/mailnews/base/src/nsMessengerWinIntegration.cpp new file mode 100644 index 0000000000..7db74af5e0 --- /dev/null +++ b/comm/mailnews/base/src/nsMessengerWinIntegration.cpp @@ -0,0 +1,379 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <shellapi.h> +#include <strsafe.h> + +#include "mozilla/Components.h" +#include "mozilla/Services.h" +#include "mozIDOMWindow.h" +#include "nsIBaseWindow.h" +#include "nsIDocShell.h" +#include "nsIMsgWindow.h" +#include "nsIObserverService.h" +#include "nsIPrefService.h" +#include "nsIWidget.h" +#include "nsIWindowMediator.h" +#include "nsMessengerWinIntegration.h" +#include "nsMsgDBFolder.h" +#include "nsPIDOMWindow.h" + +#define IDI_MAILBIFF 32576 +#define SHOW_TRAY_ICON_PREF "mail.biff.show_tray_icon" +#define SHOW_TRAY_ICON_ALWAYS_PREF "mail.biff.show_tray_icon_always" + +// since we are including windows.h in this file, undefine get user name.... +#ifdef GetUserName +# undef GetUserName +#endif + +#ifndef NIIF_USER +# define NIIF_USER 0x00000004 +#endif + +#ifndef NIIF_NOSOUND +# define NIIF_NOSOUND 0x00000010 +#endif + +using namespace mozilla; + +nsMessengerWinIntegration::nsMessengerWinIntegration() {} + +nsMessengerWinIntegration::~nsMessengerWinIntegration() {} + +NS_IMPL_ADDREF(nsMessengerWinIntegration) +NS_IMPL_RELEASE(nsMessengerWinIntegration) + +NS_INTERFACE_MAP_BEGIN(nsMessengerWinIntegration) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMessengerOSIntegration) + NS_INTERFACE_MAP_ENTRY(nsIMessengerWindowsIntegration) + NS_INTERFACE_MAP_ENTRY(nsIMessengerOSIntegration) +NS_INTERFACE_MAP_END + +static HWND hwndForDOMWindow(mozIDOMWindowProxy* window) { + if (!window) { + return 0; + } + nsCOMPtr<nsPIDOMWindowOuter> pidomwindow = nsPIDOMWindowOuter::From(window); + + nsCOMPtr<nsIBaseWindow> ppBaseWindow = + do_QueryInterface(pidomwindow->GetDocShell()); + if (!ppBaseWindow) return 0; + + nsCOMPtr<nsIWidget> ppWidget; + ppBaseWindow->GetMainWidget(getter_AddRefs(ppWidget)); + + return (HWND)(ppWidget->GetNativeData(NS_NATIVE_WIDGET)); +} + +static void activateWindow(mozIDOMWindowProxy* win) { + // Try to get native window handle. + HWND hwnd = hwndForDOMWindow(win); + if (hwnd) { + // Restore the window if it is minimized. + if (::IsIconic(hwnd)) ::ShowWindow(hwnd, SW_RESTORE); + // Use the OS call, if possible. + ::SetForegroundWindow(hwnd); + } else { + // Use internal method. + nsCOMPtr<nsPIDOMWindowOuter> privateWindow = nsPIDOMWindowOuter::From(win); + privateWindow->Focus(mozilla::dom::CallerType::System); + } +} + +NOTIFYICONDATAW sMailIconData = { + /* cbSize */ (DWORD)NOTIFYICONDATAW_V2_SIZE, + /* hWnd */ 0, + /* uID */ 2, + /* uFlags */ NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_INFO, + /* uCallbackMessage */ WM_USER, + /* hIcon */ 0, + /* szTip */ L"", + /* dwState */ 0, + /* dwStateMask */ 0, + /* szInfo */ L"", + /* uVersion */ {30000}, + /* szInfoTitle */ L"", + /* dwInfoFlags */ NIIF_USER | NIIF_NOSOUND}; + +static nsCOMArray<nsIBaseWindow> sHiddenWindows; +static HWND sIconWindow; +static uint32_t sUnreadCount; +/* static */ +LRESULT CALLBACK nsMessengerWinIntegration::IconWindowProc(HWND msgWindow, + UINT msg, WPARAM wp, + LPARAM lp) { + nsresult rv; + static UINT sTaskbarRecreated; + + switch (msg) { + case WM_USER: + if (msg == WM_USER && lp == WM_LBUTTONDOWN) { + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, FALSE); + bool showTrayIcon; + rv = prefBranch->GetBoolPref(SHOW_TRAY_ICON_PREF, &showTrayIcon); + NS_ENSURE_SUCCESS(rv, FALSE); + bool showTrayIconAlways; + if (NS_FAILED(prefBranch->GetBoolPref(SHOW_TRAY_ICON_ALWAYS_PREF, + &showTrayIconAlways))) { + showTrayIconAlways = false; + } + if ((!showTrayIcon || !sUnreadCount) && !showTrayIconAlways) { + ::Shell_NotifyIconW(NIM_DELETE, &sMailIconData); + if (auto instance = reinterpret_cast<nsMessengerWinIntegration*>( + ::GetWindowLongPtrW(msgWindow, GWLP_USERDATA))) { + instance->mTrayIconShown = false; + } + } + + // No minimzed window, bring the most recent 3pane window to the front. + if (sHiddenWindows.Length() == 0) { + nsCOMPtr<nsIWindowMediator> windowMediator = + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, FALSE); + + nsCOMPtr<mozIDOMWindowProxy> domWindow; + rv = windowMediator->GetMostRecentBrowserWindow( + getter_AddRefs(domWindow)); + NS_ENSURE_SUCCESS(rv, FALSE); + if (domWindow) { + activateWindow(domWindow); + return TRUE; + } + } + + // Bring the minimized windows to the front. + for (uint32_t i = 0; i < sHiddenWindows.Length(); i++) { + auto window = sHiddenWindows.SafeElementAt(i); + if (!window) { + continue; + } + window->SetVisibility(true); + + nsCOMPtr<nsIWidget> widget; + window->GetMainWidget(getter_AddRefs(widget)); + if (!widget) { + continue; + } + + HWND hwnd = (HWND)(widget->GetNativeData(NS_NATIVE_WIDGET)); + ::ShowWindow(hwnd, SW_RESTORE); + ::SetForegroundWindow(hwnd); + + nsCOMPtr<nsIObserverService> obs = + mozilla::services::GetObserverService(); + obs->NotifyObservers(window, "windows-refresh-badge-tray", 0); + } + + sHiddenWindows.Clear(); + } + break; + case WM_CREATE: + sTaskbarRecreated = ::RegisterWindowMessageW(L"TaskbarCreated"); + break; + default: + if (msg == sTaskbarRecreated) { + // When taskbar is recreated (e.g. by restarting Windows Explorer), all + // tray icons are removed. If there are windows minimized to tray icon, + // we have to recreate the tray icon, otherwise the windows can't be + // restored. + if (auto instance = reinterpret_cast<nsMessengerWinIntegration*>( + ::GetWindowLongPtrW(msgWindow, GWLP_USERDATA))) { + instance->mTrayIconShown = false; + } + for (uint32_t i = 0; i < sHiddenWindows.Length(); i++) { + auto window = sHiddenWindows.SafeElementAt(i); + if (!window) { + continue; + } + nsCOMPtr<nsIObserverService> obs = + mozilla::services::GetObserverService(); + obs->NotifyObservers(window, "windows-refresh-badge-tray", 0); + } + } + break; + } + return ::DefWindowProc(msgWindow, msg, wp, lp); +} + +nsresult nsMessengerWinIntegration::HideWindow(nsIBaseWindow* aWindow) { + NS_ENSURE_ARG(aWindow); + aWindow->SetVisibility(false); + sHiddenWindows.AppendElement(aWindow); + + nsresult rv; + rv = CreateIconWindow(); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mTrayIconShown) { + auto idi = IDI_APPLICATION; + if (sUnreadCount > 0) { + idi = MAKEINTRESOURCE(IDI_MAILBIFF); + } + sMailIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), idi); + nsresult rv = SetTooltip(); + NS_ENSURE_SUCCESS(rv, rv); + + ::Shell_NotifyIconW(NIM_ADD, &sMailIconData); + ::Shell_NotifyIconW(NIM_SETVERSION, &sMailIconData); + mTrayIconShown = true; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMessengerWinIntegration::ShowWindow(mozIDOMWindowProxy* aWindow) { + activateWindow(aWindow); + return NS_OK; +} + +NS_IMETHODIMP +nsMessengerWinIntegration::UpdateUnreadCount(uint32_t unreadCount, + const nsAString& unreadTooltip) { + sUnreadCount = unreadCount; + mUnreadTooltip = unreadTooltip; + nsresult rv = UpdateTrayIcon(); + return rv; +} + +NS_IMETHODIMP +nsMessengerWinIntegration::OnExit() { + if (mTrayIconShown) { + ::Shell_NotifyIconW(NIM_DELETE, &sMailIconData); + mTrayIconShown = false; + } + return NS_OK; +} + +/** + * Set a tooltip to the tray icon. Including the brand short name, and unread + * message count. + */ +nsresult nsMessengerWinIntegration::SetTooltip() { + nsresult rv = NS_OK; + if (mBrandShortName.IsEmpty()) { + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> bundle; + rv = bundleService->CreateBundle( + "chrome://branding/locale/brand.properties", getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + rv = bundle->GetStringFromName("brandShortName", mBrandShortName); + NS_ENSURE_SUCCESS(rv, rv); + } + nsString tooltip = mBrandShortName; + if (!mUnreadTooltip.IsEmpty()) { + tooltip.AppendLiteral("\n"); + tooltip.Append(mUnreadTooltip); + } + size_t destLength = + sizeof sMailIconData.szTip / (sizeof sMailIconData.szTip[0]); + ::StringCchCopyNW(sMailIconData.szTip, destLength, tooltip.get(), + tooltip.Length()); + return rv; +} + +/** + * Create a custom window for the taskbar icon if it's not created yet. + */ +nsresult nsMessengerWinIntegration::CreateIconWindow() { + if (sMailIconData.hWnd) { + return NS_OK; + } + + const wchar_t kClassName[] = L"IconWindowClass"; + WNDCLASS classStruct = {/* style */ 0, + /* lpfnWndProc */ &IconWindowProc, + /* cbClsExtra */ 0, + /* cbWndExtra */ 0, + /* hInstance */ 0, + /* hIcon */ 0, + /* hCursor */ 0, + /* hbrBackground */ 0, + /* lpszMenuName */ 0, + /* lpszClassName */ kClassName}; + + // Register the window class. + NS_ENSURE_TRUE(::RegisterClass(&classStruct), NS_ERROR_FAILURE); + // Create the window. + NS_ENSURE_TRUE(sIconWindow = ::CreateWindow( + /* className */ kClassName, + /* title */ 0, + /* style */ WS_CAPTION, + /* x, y, cx, cy */ 0, 0, 0, 0, + /* parent */ 0, + /* menu */ 0, + /* instance */ 0, + /* create struct */ 0), + NS_ERROR_FAILURE); + NS_ENSURE_TRUE(::SetWindowLongPtrW(sIconWindow, GWLP_USERDATA, + reinterpret_cast<LONG_PTR>(this)) == 0, + NS_ERROR_FAILURE); + + sMailIconData.hWnd = sIconWindow; + return NS_OK; +} + +/** + * Update the tray icon according to the current unread count and pref value. + */ +nsresult nsMessengerWinIntegration::UpdateTrayIcon() { + nsresult rv; + + rv = CreateIconWindow(); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mPrefBranch) { + mPrefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = SetTooltip(); + NS_ENSURE_SUCCESS(rv, rv); + + bool showTrayIconAlways; + if (NS_FAILED(mPrefBranch->GetBoolPref(SHOW_TRAY_ICON_ALWAYS_PREF, + &showTrayIconAlways))) { + showTrayIconAlways = false; + } + if (sUnreadCount > 0 || showTrayIconAlways) { + auto idi = IDI_APPLICATION; + if (sUnreadCount > 0) { + // Only showing the new mail marker when there are actual unread mail + idi = MAKEINTRESOURCE(IDI_MAILBIFF); + } + sMailIconData.hIcon = ::LoadIcon(::GetModuleHandle(NULL), idi); + if (mTrayIconShown) { + // If the tray icon is already shown, just modify it. + ::Shell_NotifyIconW(NIM_MODIFY, &sMailIconData); + } else { + bool showTrayIcon; + rv = mPrefBranch->GetBoolPref(SHOW_TRAY_ICON_PREF, &showTrayIcon); + NS_ENSURE_SUCCESS(rv, rv); + if (showTrayIcon) { + // Show a tray icon only if the pref value is true. + ::Shell_NotifyIconW(NIM_ADD, &sMailIconData); + ::Shell_NotifyIconW(NIM_SETVERSION, &sMailIconData); + mTrayIconShown = true; + } + } + } else if (mTrayIconShown) { + if (sHiddenWindows.Length() > 0) { + // At least one window is minimized, modify the icon only. + sMailIconData.hIcon = + ::LoadIcon(::GetModuleHandle(NULL), IDI_APPLICATION); + ::Shell_NotifyIconW(NIM_MODIFY, &sMailIconData); + } else if (!showTrayIconAlways) { + // No unread, no need to show the tray icon. + ::Shell_NotifyIconW(NIM_DELETE, &sMailIconData); + mTrayIconShown = false; + } + } + return rv; +} diff --git a/comm/mailnews/base/src/nsMessengerWinIntegration.h b/comm/mailnews/base/src/nsMessengerWinIntegration.h new file mode 100644 index 0000000000..0fb9f1a718 --- /dev/null +++ b/comm/mailnews/base/src/nsMessengerWinIntegration.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef __nsMessengerWinIntegration_h +#define __nsMessengerWinIntegration_h + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsIMessengerWindowsIntegration.h" +#include "nsIStringBundle.h" +#include "nsIPrefBranch.h" + +class nsMessengerWinIntegration : public nsIMessengerWindowsIntegration { + public: + nsMessengerWinIntegration(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMESSENGERWINDOWSINTEGRATION + NS_DECL_NSIMESSENGEROSINTEGRATION + + private: + static LRESULT CALLBACK IconWindowProc(HWND msgWindow, UINT msg, WPARAM wp, + LPARAM lp); + + virtual ~nsMessengerWinIntegration(); + + nsresult CreateIconWindow(); + nsresult SetTooltip(); + nsresult UpdateTrayIcon(); + + nsCOMPtr<nsIPrefBranch> mPrefBranch; + bool mTrayIconShown = false; + nsString mBrandShortName; + nsString mUnreadTooltip; +}; + +#endif // __nsMessengerWinIntegration_h diff --git a/comm/mailnews/base/src/nsMsgAccount.cpp b/comm/mailnews/base/src/nsMsgAccount.cpp new file mode 100644 index 0000000000..8751e4cdde --- /dev/null +++ b/comm/mailnews/base/src/nsMsgAccount.cpp @@ -0,0 +1,413 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "prprf.h" +#include "plstr.h" +#include "prmem.h" +#include "nsIComponentManager.h" +#include "nsIServiceManager.h" +#include "nsCRTGlue.h" +#include "nsCOMPtr.h" +#include "nsIMsgFolderNotificationService.h" +#include "nsPrintfCString.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsMsgAccount.h" +#include "nsIMsgAccount.h" +#include "nsIMsgAccountManager.h" +#include "nsIObserverService.h" +#include "mozilla/Services.h" +#include "nsServiceManagerUtils.h" +#include "nsMemory.h" +#include "nsComponentManagerUtils.h" +#include "nsMsgUtils.h" + +NS_IMPL_ISUPPORTS(nsMsgAccount, nsIMsgAccount) + +nsMsgAccount::nsMsgAccount() + : m_identitiesValid(false), mTriedToGetServer(false) {} + +nsMsgAccount::~nsMsgAccount() {} + +nsresult nsMsgAccount::getPrefService() { + if (m_prefs) return NS_OK; + + nsresult rv; + NS_ENSURE_FALSE(m_accountKey.IsEmpty(), NS_ERROR_NOT_INITIALIZED); + nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString accountRoot("mail.account."); + accountRoot.Append(m_accountKey); + accountRoot.Append('.'); + return prefs->GetBranch(accountRoot.get(), getter_AddRefs(m_prefs)); +} + +NS_IMETHODIMP +nsMsgAccount::GetIncomingServer(nsIMsgIncomingServer** aIncomingServer) { + NS_ENSURE_ARG_POINTER(aIncomingServer); + + // create the incoming server lazily + if (!mTriedToGetServer && !m_incomingServer) { + mTriedToGetServer = true; + // ignore the error (and return null), but it's still bad so warn + mozilla::DebugOnly<nsresult> rv = createIncomingServer(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "couldn't lazily create the server\n"); + } + + NS_IF_ADDREF(*aIncomingServer = m_incomingServer); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccount::CreateServer() { + if (m_incomingServer) return NS_ERROR_ALREADY_INITIALIZED; + return createIncomingServer(); +} + +nsresult nsMsgAccount::createIncomingServer() { + // from here, load mail.account.myaccount.server + // Load the incoming server + // + // ex) mail.account.myaccount.server = "myserver" + + nsresult rv = getPrefService(); + NS_ENSURE_SUCCESS(rv, rv); + + // get the "server" pref + nsCString serverKey; + rv = m_prefs->GetCharPref("server", serverKey); + NS_ENSURE_SUCCESS(rv, rv); + + // get the server from the account manager + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = accountManager->GetIncomingServer(serverKey, getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString hostname; + rv = server->GetHostName(hostname); + NS_ENSURE_SUCCESS(rv, rv); + if (hostname.IsEmpty()) { + NS_WARNING( + nsPrintfCString("Server had no hostname; key=%s", serverKey.get()) + .get()); + return NS_ERROR_UNEXPECTED; + } + + // store the server in this structure + m_incomingServer = server; + accountManager->NotifyServerLoaded(server); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccount::SetIncomingServer(nsIMsgIncomingServer* aIncomingServer) { + NS_ENSURE_ARG_POINTER(aIncomingServer); + + nsCString key; + nsresult rv = aIncomingServer->GetKey(key); + + if (NS_SUCCEEDED(rv)) { + rv = getPrefService(); + NS_ENSURE_SUCCESS(rv, rv); + m_prefs->SetCharPref("server", key); + } + + m_incomingServer = aIncomingServer; + + bool serverValid; + (void)aIncomingServer->GetValid(&serverValid); + // only notify server loaded if server is valid so + // account manager only gets told about finished accounts. + if (serverValid) { + // this is the point at which we can notify listeners about the + // creation of the root folder, which implies creation of the new server. + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = aIncomingServer->GetRootFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIFolderListener> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + mailSession->OnFolderAdded(nullptr, rootFolder); + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + notifier->NotifyFolderAdded(rootFolder); + + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + if (NS_SUCCEEDED(rv)) accountManager->NotifyServerLoaded(aIncomingServer); + + // Force built-in folders to be created and discovered. Then, notify + // listeners about them. + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + rv = rootFolder->GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIMsgFolder* msgFolder : subFolders) { + mailSession->OnFolderAdded(rootFolder, msgFolder); + notifier->NotifyFolderAdded(msgFolder); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccount::GetIdentities(nsTArray<RefPtr<nsIMsgIdentity>>& identities) { + NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE); + identities.Clear(); + identities.AppendElements(m_identities); + return NS_OK; +} + +/* + * set up the m_identities array + * do not call this more than once or we'll leak. + */ +nsresult nsMsgAccount::createIdentities() { + NS_ENSURE_FALSE(m_identitiesValid, NS_ERROR_FAILURE); + + nsresult rv; + m_identities.Clear(); + + nsCString identityKey; + rv = getPrefService(); + NS_ENSURE_SUCCESS(rv, rv); + + m_prefs->GetCharPref("identities", identityKey); + if (identityKey.IsEmpty()) { + // not an error if no identities, but strtok will be unhappy. + m_identitiesValid = true; + return NS_OK; + } + // get the server from the account manager + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + char* newStr = identityKey.BeginWriting(); + char* token = NS_strtok(",", &newStr); + + // temporaries used inside the loop + nsCOMPtr<nsIMsgIdentity> identity; + nsAutoCString key; + + // iterate through id1,id2, etc + while (token) { + key = token; + key.StripWhitespace(); + + // create the account + rv = accountManager->GetIdentity(key, getter_AddRefs(identity)); + if (NS_SUCCEEDED(rv)) { + m_identities.AppendElement(identity); + } + + // advance to next key, if any + token = NS_strtok(",", &newStr); + } + + m_identitiesValid = true; + return rv; +} + +/* attribute nsIMsgIdentity defaultIdentity; */ +NS_IMETHODIMP +nsMsgAccount::GetDefaultIdentity(nsIMsgIdentity** aDefaultIdentity) { + NS_ENSURE_ARG_POINTER(aDefaultIdentity); + NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_NOT_INITIALIZED); + + // Default identity is the first in the list. + if (m_identities.IsEmpty()) { + *aDefaultIdentity = nullptr; + } else { + NS_IF_ADDREF(*aDefaultIdentity = m_identities[0]); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccount::SetDefaultIdentity(nsIMsgIdentity* aDefaultIdentity) { + NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE); + + auto position = m_identities.IndexOf(aDefaultIdentity); + if (position == m_identities.NoIndex) { + return NS_ERROR_FAILURE; + } + + // Move it to the front of the list. + m_identities.RemoveElementAt(position); + m_identities.InsertElementAt(0, aDefaultIdentity); + + nsresult rv = saveIdentitiesPref(); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(aDefaultIdentity, "account-default-identity-changed", + NS_ConvertUTF8toUTF16(m_accountKey).get()); + } + + return NS_OK; +} + +/* void addIdentity (in nsIMsgIdentity identity); */ +NS_IMETHODIMP +nsMsgAccount::AddIdentity(nsIMsgIdentity* identity) { + NS_ENSURE_ARG_POINTER(identity); + NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE); + + // hack hack - need to add this to the list of identities. + // for now just treat this as a Setxxx accessor + // when this is actually implemented, don't refcount the default identity + nsCString key; + nsresult rv = identity->GetKey(key); + + if (NS_SUCCEEDED(rv)) { + nsCString identityList; + m_prefs->GetCharPref("identities", identityList); + + nsAutoCString newIdentityList(identityList); + + nsAutoCString testKey; // temporary to strip whitespace + bool foundIdentity = false; // if the input identity is found + + if (!identityList.IsEmpty()) { + char* newStr = identityList.BeginWriting(); + char* token = NS_strtok(",", &newStr); + + // look for the identity key that we're adding + while (token) { + testKey = token; + testKey.StripWhitespace(); + + if (testKey.Equals(key)) foundIdentity = true; + + token = NS_strtok(",", &newStr); + } + } + + // if it didn't already exist, append it + if (!foundIdentity) { + if (newIdentityList.IsEmpty()) + newIdentityList = key; + else { + newIdentityList.Append(','); + newIdentityList.Append(key); + } + } + + m_prefs->SetCharPref("identities", newIdentityList); + + // now add it to the in-memory list + m_identities.AppendElement(identity); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(identity, "account-identity-added", + NS_ConvertUTF8toUTF16(key).get()); + } + } + + return NS_OK; +} + +/* void removeIdentity (in nsIMsgIdentity identity); */ +NS_IMETHODIMP +nsMsgAccount::RemoveIdentity(nsIMsgIdentity* aIdentity) { + NS_ENSURE_ARG_POINTER(aIdentity); + NS_ENSURE_TRUE(m_identitiesValid, NS_ERROR_FAILURE); + + // At least one identity must stay after the delete. + NS_ENSURE_TRUE(m_identities.Length() > 1, NS_ERROR_FAILURE); + + nsCString key; + nsresult rv = aIdentity->GetKey(key); + NS_ENSURE_SUCCESS(rv, rv); + + if (!m_identities.RemoveElement(aIdentity)) { + return NS_ERROR_FAILURE; + } + + // Notify before clearing the pref values, so we do not get the superfluous + // update notifications. + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(aIdentity, "account-identity-removed", + NS_ConvertUTF8toUTF16(key).get()); + } + + // Clear out the actual pref values associated with the identity. + aIdentity->ClearAllValues(); + return saveIdentitiesPref(); +} + +nsresult nsMsgAccount::saveIdentitiesPref() { + nsAutoCString newIdentityList; + + // Iterate over the existing identities and build the pref value, + // a string of identity keys: id1, id2, idX... + nsCString key; + bool first = true; + for (auto identity : m_identities) { + identity->GetKey(key); + + if (first) { + newIdentityList = key; + first = false; + } else { + newIdentityList.Append(','); + newIdentityList.Append(key); + } + } + + // Save the pref. + m_prefs->SetCharPref("identities", newIdentityList); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccount::GetKey(nsACString& accountKey) { + accountKey = m_accountKey; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccount::SetKey(const nsACString& accountKey) { + m_accountKey = accountKey; + m_prefs = nullptr; + m_identitiesValid = false; + m_identities.Clear(); + return createIdentities(); +} + +NS_IMETHODIMP +nsMsgAccount::ToString(nsAString& aResult) { + nsAutoString val; + aResult.AssignLiteral("[nsIMsgAccount: "); + aResult.Append(NS_ConvertASCIItoUTF16(m_accountKey)); + aResult.Append(']'); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccount::ClearAllValues() { + nsTArray<nsCString> prefNames; + nsresult rv = m_prefs->GetChildList("", prefNames); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto& prefName : prefNames) { + m_prefs->ClearUserPref(prefName.get()); + } + + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgAccount.h b/comm/mailnews/base/src/nsMsgAccount.h new file mode 100644 index 0000000000..60b8005390 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgAccount.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nscore.h" +#include "nsIMsgAccount.h" +#include "nsIPrefBranch.h" +#include "nsString.h" + +class nsMsgAccount : public nsIMsgAccount { + public: + nsMsgAccount(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGACCOUNT + + private: + virtual ~nsMsgAccount(); + nsCString m_accountKey; + nsCOMPtr<nsIPrefBranch> m_prefs; + nsCOMPtr<nsIMsgIncomingServer> m_incomingServer; + + bool m_identitiesValid; + nsTArray<nsCOMPtr<nsIMsgIdentity>> m_identities; + + nsresult getPrefService(); + nsresult createIncomingServer(); + nsresult createIdentities(); + nsresult saveIdentitiesPref(); + + // Have we tried to get the server yet? + bool mTriedToGetServer; +}; diff --git a/comm/mailnews/base/src/nsMsgAccountManager.cpp b/comm/mailnews/base/src/nsMsgAccountManager.cpp new file mode 100644 index 0000000000..5352486cb4 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgAccountManager.cpp @@ -0,0 +1,3546 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/** + * The account manager service - manages all accounts, servers, and identities + */ + +#include "nsCOMPtr.h" +#include "nsISupports.h" +#include "nsIThread.h" +#include "nscore.h" +#include "mozilla/Assertions.h" +#include "mozilla/Likely.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/RefCountType.h" +#include "mozilla/RefPtr.h" +#include "nsIComponentManager.h" +#include "nsIServiceManager.h" +#include "nsMsgAccountManager.h" +#include "prmem.h" +#include "prcmon.h" +#include "prthread.h" +#include "plstr.h" +#include "nsString.h" +#include "nsMemory.h" +#include "nsUnicharUtils.h" +#include "nscore.h" +#include "prprf.h" +#include "nsIMsgFolderCache.h" +#include "nsMsgFolderCache.h" +#include "nsMsgUtils.h" +#include "nsMsgDBFolder.h" +#include "nsIFile.h" +#include "nsIURL.h" +#include "nsNetCID.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsISmtpService.h" +#include "nsIMsgBiffManager.h" +#include "nsIMsgPurgeService.h" +#include "nsIObserverService.h" +#include "nsINoIncomingServer.h" +#include "nsIMsgMailSession.h" +#include "nsIDirectoryService.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsMailDirServiceDefs.h" +#include "nsMsgFolderFlags.h" +#include "nsIMsgFolderNotificationService.h" +#include "nsIImapIncomingServer.h" +#include "nsIImapUrl.h" +#include "nsICategoryManager.h" +#include "nsISupportsPrimitives.h" +#include "nsIMsgFilterService.h" +#include "nsIMsgFilter.h" +#include "nsIMsgSearchSession.h" +#include "nsIMsgSearchTerm.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgHdr.h" +#include "nsILineInputStream.h" +#include "nsThreadUtils.h" +#include "nsNetUtil.h" +#include "nsIStringBundle.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgFilterList.h" +#include "nsDirectoryServiceUtils.h" +#include "mozilla/Components.h" +#include "mozilla/Services.h" +#include "nsIFileStreams.h" +#include "nsIOutputStream.h" +#include "nsISafeOutputStream.h" +#include "nsXULAppAPI.h" +#include "nsICacheStorageService.h" +#include "UrlListener.h" +#include "nsIIDNService.h" + +#define PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS "mail.accountmanager.accounts" +#define PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT \ + "mail.accountmanager.defaultaccount" +#define PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER \ + "mail.accountmanager.localfoldersserver" +#define PREF_MAIL_SERVER_PREFIX "mail.server." +#define ACCOUNT_PREFIX "account" +#define SERVER_PREFIX "server" +#define ID_PREFIX "id" +#define ABOUT_TO_GO_OFFLINE_TOPIC "network:offline-about-to-go-offline" +#define ACCOUNT_DELIMITER ',' +#define APPEND_ACCOUNTS_VERSION_PREF_NAME "append_preconfig_accounts.version" +#define MAILNEWS_ROOT_PREF "mailnews." +#define PREF_MAIL_ACCOUNTMANAGER_APPEND_ACCOUNTS \ + "mail.accountmanager.appendaccounts" + +#define NS_MSGACCOUNT_CID \ + { \ + 0x68b25510, 0xe641, 0x11d2, { \ + 0xb7, 0xfc, 0x0, 0x80, 0x5f, 0x5, 0xff, 0xa5 \ + } \ + } +static NS_DEFINE_CID(kMsgAccountCID, NS_MSGACCOUNT_CID); + +#define SEARCH_FOLDER_FLAG "searchFolderFlag" +#define SEARCH_FOLDER_FLAG_LEN (sizeof(SEARCH_FOLDER_FLAG) - 1) + +const char* kSearchFolderUriProp = "searchFolderUri"; + +bool nsMsgAccountManager::m_haveShutdown = false; +bool nsMsgAccountManager::m_shutdownInProgress = false; + +NS_IMPL_ISUPPORTS(nsMsgAccountManager, nsIMsgAccountManager, nsIObserver, + nsISupportsWeakReference, nsIFolderListener) + +nsMsgAccountManager::nsMsgAccountManager() + : m_accountsLoaded(false), + m_emptyTrashInProgress(false), + m_cleanupInboxInProgress(false), + m_userAuthenticated(false), + m_loadingVirtualFolders(false), + m_virtualFoldersLoaded(false), + m_lastFindServerPort(0) {} + +nsMsgAccountManager::~nsMsgAccountManager() { + if (!m_haveShutdown) { + Shutdown(); + // Don't remove from Observer service in Shutdown because Shutdown also gets + // called from xpcom shutdown observer. And we don't want to remove from + // the service in that case. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, "search-folders-changed"); + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + observerService->RemoveObserver(this, "quit-application-granted"); + observerService->RemoveObserver(this, ABOUT_TO_GO_OFFLINE_TOPIC); + observerService->RemoveObserver(this, "sleep_notification"); + } + } +} + +nsresult nsMsgAccountManager::Init() { + if (!XRE_IsParentProcess()) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv; + + m_prefs = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "search-folders-changed", true); + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); + observerService->AddObserver(this, "quit-application-granted", true); + observerService->AddObserver(this, ABOUT_TO_GO_OFFLINE_TOPIC, true); + observerService->AddObserver(this, "profile-before-change", true); + observerService->AddObserver(this, "sleep_notification", true); + } + + // Make sure PSM gets initialized before any accounts use certificates. + net_EnsurePSMInit(); + + return NS_OK; +} + +nsresult nsMsgAccountManager::Shutdown() { + if (m_haveShutdown) // do not shutdown twice + return NS_OK; + + nsresult rv; + + SaveVirtualFolders(); + + if (m_dbService) { + nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter( + m_virtualFolderListeners); + RefPtr<VirtualFolderChangeListener> listener; + + while (iter.HasMore()) { + listener = iter.GetNext(); + m_dbService->UnregisterPendingListener(listener); + } + + m_dbService = nullptr; + } + m_virtualFolders.Clear(); + if (m_msgFolderCache) WriteToFolderCache(m_msgFolderCache); + (void)ShutdownServers(); + (void)UnloadAccounts(); + + // shutdown removes nsIIncomingServer listener from biff manager, so do it + // after accounts have been unloaded + nsCOMPtr<nsIMsgBiffManager> biffService = + do_GetService("@mozilla.org/messenger/biffManager;1", &rv); + if (NS_SUCCEEDED(rv) && biffService) biffService->Shutdown(); + + // shutdown removes nsIIncomingServer listener from purge service, so do it + // after accounts have been unloaded + nsCOMPtr<nsIMsgPurgeService> purgeService = + do_GetService("@mozilla.org/messenger/purgeService;1", &rv); + if (NS_SUCCEEDED(rv) && purgeService) purgeService->Shutdown(); + + if (m_msgFolderCache) { + // The DTOR is meant to do the flushing, but observed behaviour is + // that it doesn't always get called. So flush explicitly. + m_msgFolderCache->Flush(); + m_msgFolderCache = nullptr; + } + + m_haveShutdown = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetShutdownInProgress(bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = m_shutdownInProgress; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetUserNeedsToAuthenticate(bool* aRetval) { + NS_ENSURE_ARG_POINTER(aRetval); + if (!m_userAuthenticated) + return m_prefs->GetBoolPref("mail.password_protect_local_cache", aRetval); + *aRetval = !m_userAuthenticated; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::SetUserNeedsToAuthenticate(bool aUserNeedsToAuthenticate) { + m_userAuthenticated = !aUserNeedsToAuthenticate; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* someData) { + if (!strcmp(aTopic, "search-folders-changed")) { + nsCOMPtr<nsIMsgFolder> virtualFolder = do_QueryInterface(aSubject); + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(db)); + nsCString srchFolderUris; + dbFolderInfo->GetCharProperty(kSearchFolderUriProp, srchFolderUris); + AddVFListenersForVF(virtualFolder, srchFolderUris); + return NS_OK; + } + if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { + Shutdown(); + return NS_OK; + } + if (!strcmp(aTopic, "quit-application-granted")) { + // CleanupOnExit will set m_shutdownInProgress to true. + CleanupOnExit(); + return NS_OK; + } + if (!strcmp(aTopic, ABOUT_TO_GO_OFFLINE_TOPIC)) { + nsAutoString dataString(u"offline"_ns); + if (someData) { + nsAutoString someDataString(someData); + if (dataString.Equals(someDataString)) CloseCachedConnections(); + } + return NS_OK; + } + if (!strcmp(aTopic, "sleep_notification")) return CloseCachedConnections(); + + if (!strcmp(aTopic, "profile-before-change")) { + Shutdown(); + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetUniqueAccountKey(nsACString& aResult) { + int32_t lastKey = 0; + nsresult rv; + nsCOMPtr<nsIPrefService> prefservice( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIPrefBranch> prefBranch; + prefservice->GetBranch("", getter_AddRefs(prefBranch)); + + rv = prefBranch->GetIntPref("mail.account.lastKey", &lastKey); + if (NS_FAILED(rv) || lastKey == 0) { + // If lastKey pref does not contain a valid value, loop over existing + // pref names mail.account.* . + nsCOMPtr<nsIPrefBranch> prefBranchAccount; + rv = prefservice->GetBranch("mail.account.", + getter_AddRefs(prefBranchAccount)); + if (NS_SUCCEEDED(rv)) { + nsTArray<nsCString> prefList; + rv = prefBranchAccount->GetChildList("", prefList); + if (NS_SUCCEEDED(rv)) { + // Pref names are of the format accountX. + // Find the maximum value of 'X' used so far. + for (auto& prefName : prefList) { + if (StringBeginsWith(prefName, nsLiteralCString(ACCOUNT_PREFIX))) { + int32_t dotPos = prefName.FindChar('.'); + if (dotPos != kNotFound) { + nsCString keyString(Substring(prefName, strlen(ACCOUNT_PREFIX), + dotPos - strlen(ACCOUNT_PREFIX))); + int32_t thisKey = keyString.ToInteger(&rv); + if (NS_SUCCEEDED(rv)) lastKey = std::max(lastKey, thisKey); + } + } + } + } + } + } + + // Use next available key and store the value in the pref. + aResult.Assign(ACCOUNT_PREFIX); + aResult.AppendInt(++lastKey); + rv = prefBranch->SetIntPref("mail.account.lastKey", lastKey); + } else { + // If pref service is not working, try to find a free accountX key + // by checking which keys exist. + int32_t i = 1; + nsCOMPtr<nsIMsgAccount> account; + + do { + aResult = ACCOUNT_PREFIX; + aResult.AppendInt(i++); + GetAccount(aResult, getter_AddRefs(account)); + } while (account); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetUniqueServerKey(nsACString& aResult) { + nsAutoCString prefResult; + bool usePrefsScan = true; + nsresult rv; + nsCOMPtr<nsIPrefService> prefService( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) usePrefsScan = false; + + // Loop over existing pref names mail.server.server(lastKey).type + nsCOMPtr<nsIPrefBranch> prefBranchServer; + if (prefService) { + rv = prefService->GetBranch(PREF_MAIL_SERVER_PREFIX, + getter_AddRefs(prefBranchServer)); + if (NS_FAILED(rv)) usePrefsScan = false; + } + + if (usePrefsScan) { + nsAutoCString type; + nsAutoCString typeKey; + for (uint32_t lastKey = 1;; lastKey++) { + aResult.AssignLiteral(SERVER_PREFIX); + aResult.AppendInt(lastKey); + typeKey.Assign(aResult); + typeKey.AppendLiteral(".type"); + prefBranchServer->GetCharPref(typeKey.get(), type); + if (type.IsEmpty()) // a server slot with no type is considered empty + return NS_OK; + } + } else { + // If pref service fails, try to find a free serverX key + // by checking which keys exist. + nsAutoCString internalResult; + nsCOMPtr<nsIMsgIncomingServer> server; + uint32_t i = 1; + do { + aResult.AssignLiteral(SERVER_PREFIX); + aResult.AppendInt(i++); + m_incomingServers.Get(aResult, getter_AddRefs(server)); + } while (server); + return NS_OK; + } +} + +nsresult nsMsgAccountManager::CreateIdentity(nsIMsgIdentity** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + nsresult rv; + nsAutoCString key; + nsCOMPtr<nsIMsgIdentity> identity; + int32_t i = 1; + do { + key.AssignLiteral(ID_PREFIX); + key.AppendInt(i++); + m_identities.Get(key, getter_AddRefs(identity)); + } while (identity); + + rv = createKeyedIdentity(key, _retval); + return rv; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetIdentity(const nsACString& key, + nsIMsgIdentity** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + nsresult rv = NS_OK; + *_retval = nullptr; + + if (!key.IsEmpty()) { + nsCOMPtr<nsIMsgIdentity> identity; + m_identities.Get(key, getter_AddRefs(identity)); + if (identity) + identity.forget(_retval); + else // identity doesn't exist. create it. + rv = createKeyedIdentity(key, _retval); + } + + return rv; +} + +/* + * the shared identity-creation code + * create an identity and add it to the accountmanager's list. + */ +nsresult nsMsgAccountManager::createKeyedIdentity(const nsACString& key, + nsIMsgIdentity** aIdentity) { + nsresult rv; + nsCOMPtr<nsIMsgIdentity> identity = + do_CreateInstance("@mozilla.org/messenger/identity;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + identity->SetKey(key); + m_identities.InsertOrUpdate(key, identity); + identity.forget(aIdentity); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::CreateIncomingServer(const nsACString& username, + const nsACString& hostname, + const nsACString& type, + nsIMsgIncomingServer** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + nsresult rv = LoadAccounts(); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString key; + GetUniqueServerKey(key); + rv = createKeyedServer(key, username, hostname, type, _retval); + if (*_retval) { + nsCString defaultStore; + m_prefs->GetCharPref("mail.serverDefaultStoreContractID", defaultStore); + (*_retval)->SetCharValue("storeContractID", defaultStore); + + // From when we first create the account until we have created some folders, + // we can change the store type. + (*_retval)->SetBoolValue("canChangeStoreType", true); + } + return rv; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetIncomingServer(const nsACString& key, + nsIMsgIncomingServer** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + nsresult rv; + + if (m_incomingServers.Get(key, _retval)) return NS_OK; + + // server doesn't exist, so create it + // this is really horrible because we are doing our own prefname munging + // instead of leaving it up to the incoming server. + // this should be fixed somehow so that we can create the incoming server + // and then read from the incoming server's attributes + + // in order to create the right kind of server, we have to look + // at the pref for this server to get the username, hostname, and type + nsAutoCString serverPrefPrefix(PREF_MAIL_SERVER_PREFIX); + serverPrefPrefix.Append(key); + + nsCString serverType; + nsAutoCString serverPref(serverPrefPrefix); + serverPref.AppendLiteral(".type"); + rv = m_prefs->GetCharPref(serverPref.get(), serverType); + NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_INITIALIZED); + + // + // .userName + serverPref = serverPrefPrefix; + serverPref.AppendLiteral(".userName"); + nsCString username; + rv = m_prefs->GetCharPref(serverPref.get(), username); + + // .hostname + serverPref = serverPrefPrefix; + serverPref.AppendLiteral(".hostname"); + nsCString hostname; + rv = m_prefs->GetCharPref(serverPref.get(), hostname); + NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_INITIALIZED); + + return createKeyedServer(key, username, hostname, serverType, _retval); +} + +NS_IMETHODIMP +nsMsgAccountManager::RemoveIncomingServer(nsIMsgIncomingServer* aServer, + bool aRemoveFiles) { + NS_ENSURE_ARG_POINTER(aServer); + + nsCString serverKey; + nsresult rv = aServer->GetKey(serverKey); + NS_ENSURE_SUCCESS(rv, rv); + + // close cached connections and forget session password + LogoutOfServer(aServer); + + // invalidate the FindServer() cache if we are removing the cached server + if (m_lastFindServerResult == aServer) + SetLastServerFound(nullptr, EmptyCString(), EmptyCString(), 0, + EmptyCString()); + + m_incomingServers.Remove(serverKey); + + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = aServer->GetRootFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgFolder>> allDescendants; + rv = rootFolder->GetDescendants(allDescendants); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolderNotificationService> notifier = + do_GetService("@mozilla.org/messenger/msgnotificationservice;1"); + nsCOMPtr<nsIFolderListener> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1"); + + for (auto folder : allDescendants) { + folder->ForceDBClosed(); + if (notifier) notifier->NotifyFolderDeleted(folder); + if (mailSession) { + nsCOMPtr<nsIMsgFolder> parentFolder; + folder->GetParent(getter_AddRefs(parentFolder)); + mailSession->OnFolderRemoved(parentFolder, folder); + } + } + if (notifier) notifier->NotifyFolderDeleted(rootFolder); + if (mailSession) mailSession->OnFolderRemoved(nullptr, rootFolder); + + removeListenersFromFolder(rootFolder); + NotifyServerUnloaded(aServer); + if (aRemoveFiles) { + rv = aServer->RemoveFiles(); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(aServer, "message-server-removed", + NS_ConvertUTF8toUTF16(serverKey).get()); + } + + // now clear out the server once and for all. + // watch out! could be scary + aServer->ClearAllValues(); + rootFolder->Shutdown(true); + return rv; +} + +/* + * create a server when you know the key and the type + */ +nsresult nsMsgAccountManager::createKeyedServer( + const nsACString& key, const nsACString& username, + const nsACString& hostname, const nsACString& type, + nsIMsgIncomingServer** aServer) { + nsresult rv; + *aServer = nullptr; + + // construct the contractid + nsAutoCString serverContractID("@mozilla.org/messenger/server;1?type="); + serverContractID += type; + + // finally, create the server + // (This will fail if type is from an extension that has been removed) + nsCOMPtr<nsIMsgIncomingServer> server = + do_CreateInstance(serverContractID.get(), &rv); + NS_ENSURE_SUCCESS(rv, NS_ERROR_NOT_AVAILABLE); + + int32_t port; + nsCOMPtr<nsIMsgIncomingServer> existingServer; + server->SetKey(key); + server->SetType(type); + server->SetUsername(username); + server->SetHostName(hostname); + server->GetPort(&port); + FindServer(username, hostname, type, port, getter_AddRefs(existingServer)); + // don't allow duplicate servers. + if (existingServer) return NS_ERROR_FAILURE; + + m_incomingServers.InsertOrUpdate(key, server); + + // now add all listeners that are supposed to be + // waiting on root folders + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = server->GetRootFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsTObserverArray<nsCOMPtr<nsIFolderListener>>::ForwardIterator iter( + mFolderListeners); + while (iter.HasMore()) { + rootFolder->AddFolderListener(iter.GetNext()); + } + + server.forget(aServer); + return NS_OK; +} + +void nsMsgAccountManager::removeListenersFromFolder(nsIMsgFolder* aFolder) { + nsTObserverArray<nsCOMPtr<nsIFolderListener>>::ForwardIterator iter( + mFolderListeners); + while (iter.HasMore()) { + aFolder->RemoveFolderListener(iter.GetNext()); + } +} + +NS_IMETHODIMP +nsMsgAccountManager::RemoveAccount(nsIMsgAccount* aAccount, + bool aRemoveFiles = false) { + NS_ENSURE_ARG_POINTER(aAccount); + // Hold account in scope while we tidy up potentially-shared identities. + nsresult rv = LoadAccounts(); + NS_ENSURE_SUCCESS(rv, rv); + + if (!m_accounts.RemoveElement(aAccount)) { + return NS_ERROR_INVALID_ARG; + } + + rv = OutputAccountsPref(); + // If we couldn't write out the pref, restore the account. + if (NS_FAILED(rv)) { + m_accounts.AppendElement(aAccount); + return rv; + } + + // If it's the default, choose a new default account. + if (m_defaultAccount == aAccount) AutosetDefaultAccount(); + + // XXX - need to figure out if this is the last time this server is + // being used, and only send notification then. + // (and only remove from hashtable then too!) + nsCOMPtr<nsIMsgIncomingServer> server; + rv = aAccount->GetIncomingServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) RemoveIncomingServer(server, aRemoveFiles); + + nsTArray<RefPtr<nsIMsgIdentity>> identities; + rv = aAccount->GetIdentities(identities); + if (NS_SUCCEEDED(rv)) { + for (auto identity : identities) { + bool identityStillUsed = false; + // for each identity, see if any remaining account still uses it, + // and if not, clear it. + // Note that we are also searching here accounts with missing servers from + // unloaded extension types. + for (auto account : m_accounts) { + nsTArray<RefPtr<nsIMsgIdentity>> existingIdentities; + account->GetIdentities(existingIdentities); + auto pos = existingIdentities.IndexOf(identity); + if (pos != existingIdentities.NoIndex) { + identityStillUsed = true; + break; + } + } + // clear out all identity information if no other account uses it. + if (!identityStillUsed) identity->ClearAllValues(); + } + } + + nsCString accountKey; + aAccount->GetKey(accountKey); + + // It is not a critical problem if this fails as the account was already + // removed from the list of accounts so should not ever be referenced. + // Just print it out for debugging. + rv = aAccount->ClearAllValues(); + NS_ASSERTION(NS_SUCCEEDED(rv), "removing of account prefs failed"); + + nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); + if (obs) { + obs->NotifyObservers(nullptr, "message-account-removed", + NS_ConvertUTF8toUTF16(accountKey).get()); + } + return NS_OK; +} + +nsresult nsMsgAccountManager::OutputAccountsPref() { + nsCString accountKey; + mAccountKeyList.Truncate(); + + for (uint32_t index = 0; index < m_accounts.Length(); index++) { + m_accounts[index]->GetKey(accountKey); + if (index) mAccountKeyList.Append(ACCOUNT_DELIMITER); + mAccountKeyList.Append(accountKey); + } + return m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS, + mAccountKeyList); +} + +/** + * Get the default account. If no default account, return null. + */ +NS_IMETHODIMP +nsMsgAccountManager::GetDefaultAccount(nsIMsgAccount** aDefaultAccount) { + NS_ENSURE_ARG_POINTER(aDefaultAccount); + + nsresult rv = LoadAccounts(); + NS_ENSURE_SUCCESS(rv, rv); + + if (!m_defaultAccount) { + nsCString defaultKey; + rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT, + defaultKey); + if (NS_SUCCEEDED(rv)) { + rv = GetAccount(defaultKey, getter_AddRefs(m_defaultAccount)); + if (NS_SUCCEEDED(rv) && m_defaultAccount) { + bool canBeDefault = false; + rv = CheckDefaultAccount(m_defaultAccount, canBeDefault); + if (NS_FAILED(rv) || !canBeDefault) m_defaultAccount = nullptr; + } + } + } + + NS_IF_ADDREF(*aDefaultAccount = m_defaultAccount); + return NS_OK; +} + +/** + * Check if the given account can be default. + */ +nsresult nsMsgAccountManager::CheckDefaultAccount(nsIMsgAccount* aAccount, + bool& aCanBeDefault) { + aCanBeDefault = false; + nsCOMPtr<nsIMsgIncomingServer> server; + // Server could be null if created by an unloaded extension. + nsresult rv = aAccount->GetIncomingServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + if (server) { + // Check if server can be default. + rv = server->GetCanBeDefaultServer(&aCanBeDefault); + } + return rv; +} + +/** + * Pick the first account that can be default and make it the default. + */ +nsresult nsMsgAccountManager::AutosetDefaultAccount() { + for (nsIMsgAccount* account : m_accounts) { + bool canBeDefault = false; + nsresult rv = CheckDefaultAccount(account, canBeDefault); + if (NS_SUCCEEDED(rv) && canBeDefault) { + return SetDefaultAccount(account); + } + } + + // No accounts can be the default. Clear it. + if (m_defaultAccount) { + nsCOMPtr<nsIMsgAccount> oldAccount = m_defaultAccount; + m_defaultAccount = nullptr; + (void)setDefaultAccountPref(nullptr); + (void)notifyDefaultServerChange(oldAccount, nullptr); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::SetDefaultAccount(nsIMsgAccount* aDefaultAccount) { + if (!aDefaultAccount) return NS_ERROR_INVALID_ARG; + + if (m_defaultAccount != aDefaultAccount) { + bool canBeDefault = false; + nsresult rv = CheckDefaultAccount(aDefaultAccount, canBeDefault); + if (NS_FAILED(rv) || !canBeDefault) { + // Report failure if we were explicitly asked to use an unusable server. + return NS_ERROR_INVALID_ARG; + } + nsCOMPtr<nsIMsgAccount> oldAccount = m_defaultAccount; + m_defaultAccount = aDefaultAccount; + (void)setDefaultAccountPref(aDefaultAccount); + (void)notifyDefaultServerChange(oldAccount, aDefaultAccount); + } + return NS_OK; +} + +// fire notifications +nsresult nsMsgAccountManager::notifyDefaultServerChange( + nsIMsgAccount* aOldAccount, nsIMsgAccount* aNewAccount) { + nsresult rv; + + nsCOMPtr<nsIMsgIncomingServer> server; + nsCOMPtr<nsIMsgFolder> rootFolder; + + // first tell old server it's no longer the default + if (aOldAccount) { + rv = aOldAccount->GetIncomingServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) { + rv = server->GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) + rootFolder->NotifyBoolPropertyChanged(kDefaultServer, true, false); + } + } + + // now tell new server it is. + if (aNewAccount) { + rv = aNewAccount->GetIncomingServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) { + rv = server->GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) + rootFolder->NotifyBoolPropertyChanged(kDefaultServer, false, true); + } + } + + // only notify if the user goes and changes default account + if (aOldAccount && aNewAccount) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + if (observerService) + observerService->NotifyObservers(nullptr, "mailDefaultAccountChanged", + nullptr); + } + + return NS_OK; +} + +nsresult nsMsgAccountManager::setDefaultAccountPref( + nsIMsgAccount* aDefaultAccount) { + nsresult rv; + + if (aDefaultAccount) { + nsCString key; + rv = aDefaultAccount->GetKey(key); + NS_ENSURE_SUCCESS(rv, rv); + + rv = m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT, key); + NS_ENSURE_SUCCESS(rv, rv); + } else + m_prefs->ClearUserPref(PREF_MAIL_ACCOUNTMANAGER_DEFAULTACCOUNT); + + return NS_OK; +} + +void nsMsgAccountManager::LogoutOfServer(nsIMsgIncomingServer* aServer) { + if (!aServer) return; + mozilla::DebugOnly<nsresult> rv = aServer->Shutdown(); + NS_ASSERTION(NS_SUCCEEDED(rv), "Shutdown of server failed"); + rv = aServer->ForgetSessionPassword(false); + NS_ASSERTION(NS_SUCCEEDED(rv), + "failed to remove the password associated with server"); +} + +NS_IMETHODIMP nsMsgAccountManager::GetFolderCache( + nsIMsgFolderCache** aFolderCache) { + NS_ENSURE_ARG_POINTER(aFolderCache); + + if (m_msgFolderCache) { + NS_IF_ADDREF(*aFolderCache = m_msgFolderCache); + return NS_OK; + } + + // Create the foldercache. + nsCOMPtr<nsIFile> cacheFile; + nsresult rv = NS_GetSpecialDirectory(NS_APP_MESSENGER_FOLDER_CACHE_50_FILE, + getter_AddRefs(cacheFile)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIFile> legacyFile; + rv = NS_GetSpecialDirectory(NS_APP_MESSENGER_LEGACY_FOLDER_CACHE_50_FILE, + getter_AddRefs(legacyFile)); + NS_ENSURE_SUCCESS(rv, rv); + m_msgFolderCache = new nsMsgFolderCache(); + m_msgFolderCache->Init(cacheFile, legacyFile); + NS_IF_ADDREF(*aFolderCache = m_msgFolderCache); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetAccounts(nsTArray<RefPtr<nsIMsgAccount>>& accounts) { + nsresult rv = LoadAccounts(); + NS_ENSURE_SUCCESS(rv, rv); + + accounts.Clear(); + accounts.SetCapacity(m_accounts.Length()); + for (auto existingAccount : m_accounts) { + nsCOMPtr<nsIMsgIncomingServer> server; + existingAccount->GetIncomingServer(getter_AddRefs(server)); + if (!server) continue; + if (server) { + bool hidden = false; + server->GetHidden(&hidden); + if (hidden) continue; + } + accounts.AppendElement(existingAccount); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetAllIdentities( + nsTArray<RefPtr<nsIMsgIdentity>>& result) { + nsresult rv = LoadAccounts(); + NS_ENSURE_SUCCESS(rv, rv); + + result.Clear(); + + for (auto account : m_accounts) { + nsTArray<RefPtr<nsIMsgIdentity>> identities; + rv = account->GetIdentities(identities); + if (NS_FAILED(rv)) continue; + + for (auto identity : identities) { + // Have we already got this identity? + nsAutoCString key; + rv = identity->GetKey(key); + if (NS_FAILED(rv)) continue; + + bool found = false; + for (auto thisIdentity : result) { + nsAutoCString thisKey; + rv = thisIdentity->GetKey(thisKey); + if (NS_FAILED(rv)) continue; + + if (key == thisKey) { + found = true; + break; + } + } + + if (!found) result.AppendElement(identity); + } + } + return rv; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetAllServers( + nsTArray<RefPtr<nsIMsgIncomingServer>>& servers) { + servers.Clear(); + nsresult rv = LoadAccounts(); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data(); + if (!server) continue; + + bool hidden = false; + server->GetHidden(&hidden); + if (hidden) continue; + + nsCString type; + if (NS_FAILED(server->GetType(type))) { + NS_WARNING("server->GetType() failed"); + continue; + } + + if (!type.EqualsLiteral("im")) { + servers.AppendElement(server); + } + } + return NS_OK; +} + +nsresult nsMsgAccountManager::LoadAccounts() { + nsresult rv; + + // for now safeguard multiple calls to this function + if (m_accountsLoaded) return NS_OK; + + // If we have code trying to do things after we've unloaded accounts, + // ignore it. + if (m_shutdownInProgress || m_haveShutdown) return NS_ERROR_FAILURE; + + // Make sure correct modules are loaded before creating any server. + nsCOMPtr<nsIObserver> moduleLoader; + moduleLoader = + do_GetService("@mozilla.org/messenger/imap-module-loader;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + + if (NS_SUCCEEDED(rv)) + mailSession->AddFolderListener( + this, nsIFolderListener::added | nsIFolderListener::removed | + nsIFolderListener::intPropertyChanged); + + // Ensure biff service has started + nsCOMPtr<nsIMsgBiffManager> biffService = + do_GetService("@mozilla.org/messenger/biffManager;1", &rv); + + if (NS_SUCCEEDED(rv)) biffService->Init(); + + // Ensure purge service has started + nsCOMPtr<nsIMsgPurgeService> purgeService = + do_GetService("@mozilla.org/messenger/purgeService;1", &rv); + + if (NS_SUCCEEDED(rv)) purgeService->Init(); + + nsCOMPtr<nsIPrefService> prefservice( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // mail.accountmanager.accounts is the main entry point for all accounts + nsCString accountList; + rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS, accountList); + + /** + * Check to see if we need to add pre-configured accounts. + * Following prefs are important to note in understanding the procedure here. + * + * 1. pref("mailnews.append_preconfig_accounts.version", version number); + * This pref registers the current version in the user prefs file. A default + * value is stored in mailnews.js file. If a given vendor needs to add more + * preconfigured accounts, the default version number can be increased. + * Comparing version number from user's prefs file and the default one from + * mailnews.js, we can add new accounts and any other version level changes + * that need to be done. + * + * 2. pref("mail.accountmanager.appendaccounts", <comma sep. account list>); + * This pref contains the list of pre-configured accounts that ISP/Vendor + * wants to add to the existing accounts list. + */ + nsCOMPtr<nsIPrefBranch> defaultsPrefBranch; + rv = prefservice->GetDefaultBranch(MAILNEWS_ROOT_PREF, + getter_AddRefs(defaultsPrefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch; + rv = prefservice->GetBranch(MAILNEWS_ROOT_PREF, getter_AddRefs(prefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t appendAccountsCurrentVersion = 0; + int32_t appendAccountsDefaultVersion = 0; + rv = prefBranch->GetIntPref(APPEND_ACCOUNTS_VERSION_PREF_NAME, + &appendAccountsCurrentVersion); + NS_ENSURE_SUCCESS(rv, rv); + + rv = defaultsPrefBranch->GetIntPref(APPEND_ACCOUNTS_VERSION_PREF_NAME, + &appendAccountsDefaultVersion); + NS_ENSURE_SUCCESS(rv, rv); + + // Update the account list if needed + if ((appendAccountsCurrentVersion <= appendAccountsDefaultVersion)) { + // Get a list of pre-configured accounts + nsCString appendAccountList; + rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_APPEND_ACCOUNTS, + appendAccountList); + appendAccountList.StripWhitespace(); + + // If there are pre-configured accounts, we need to add them to the + // existing list. + if (!appendAccountList.IsEmpty()) { + if (!accountList.IsEmpty()) { + // Tokenize the data and add each account + // in the user's current mailnews account list + nsTArray<nsCString> accountsArray; + ParseString(accountList, ACCOUNT_DELIMITER, accountsArray); + uint32_t i = accountsArray.Length(); + + // Append each account in the pre-configured account list + ParseString(appendAccountList, ACCOUNT_DELIMITER, accountsArray); + + // Now add each account that does not already appear in the list + for (; i < accountsArray.Length(); i++) { + if (accountsArray.IndexOf(accountsArray[i]) == i) { + accountList.Append(ACCOUNT_DELIMITER); + accountList.Append(accountsArray[i]); + } + } + } else { + accountList = appendAccountList; + } + // Increase the version number so that updates will happen as and when + // needed + rv = prefBranch->SetIntPref(APPEND_ACCOUNTS_VERSION_PREF_NAME, + appendAccountsCurrentVersion + 1); + } + } + + // It is ok to return null accounts like when we create new profile. + m_accountsLoaded = true; + m_haveShutdown = false; + + if (accountList.IsEmpty()) return NS_OK; + + /* parse accountList and run loadAccount on each string, comma-separated */ + nsCOMPtr<nsIMsgAccount> account; + // Tokenize the data and add each account + // in the user's current mailnews account list + nsTArray<nsCString> accountsArray; + ParseString(accountList, ACCOUNT_DELIMITER, accountsArray); + + // These are the duplicate accounts we found. We keep track of these + // because if any other server defers to one of these accounts, we need + // to defer to the correct account. + nsCOMArray<nsIMsgAccount> dupAccounts; + + // Now add each account that does not already appear in the list + for (uint32_t i = 0; i < accountsArray.Length(); i++) { + // if we've already seen this exact account, advance to the next account. + // After the loop, we'll notice that we don't have as many actual accounts + // as there were accounts in the pref, and rewrite the pref. + if (accountsArray.IndexOf(accountsArray[i]) != i) continue; + + // get the "server" pref to see if we already have an account with this + // server. If we do, we ignore this account. + nsAutoCString serverKeyPref("mail.account."); + serverKeyPref += accountsArray[i]; + + nsCOMPtr<nsIPrefBranch> accountPrefBranch; + rv = prefservice->GetBranch(serverKeyPref.get(), + getter_AddRefs(accountPrefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + serverKeyPref += ".server"; + nsCString serverKey; + rv = m_prefs->GetCharPref(serverKeyPref.get(), serverKey); + if (NS_FAILED(rv)) continue; + + nsCOMPtr<nsIMsgAccount> serverAccount; + findAccountByServerKey(serverKey, getter_AddRefs(serverAccount)); + // If we have an existing account with the same server, ignore this account + if (serverAccount) continue; + + if (NS_FAILED(createKeyedAccount(accountsArray[i], true, + getter_AddRefs(account))) || + !account) { + NS_WARNING("unexpected entry in account list; prefs corrupt?"); + continue; + } + + // See nsIMsgAccount.idl for a description of the secondsToLeaveUnavailable + // and timeFoundUnavailable preferences + nsAutoCString toLeavePref(PREF_MAIL_SERVER_PREFIX); + toLeavePref.Append(serverKey); + nsAutoCString unavailablePref( + toLeavePref); // this is the server-specific prefix + unavailablePref.AppendLiteral(".timeFoundUnavailable"); + toLeavePref.AppendLiteral(".secondsToLeaveUnavailable"); + int32_t secondsToLeave = 0; + int32_t timeUnavailable = 0; + + m_prefs->GetIntPref(toLeavePref.get(), &secondsToLeave); + + // force load of accounts (need to find a better way to do this) + nsTArray<RefPtr<nsIMsgIdentity>> unused; + account->GetIdentities(unused); + + rv = account->CreateServer(); + bool deleteAccount = NS_FAILED(rv); + + if (secondsToLeave) { // we need to process timeUnavailable + if (NS_SUCCEEDED(rv)) // clear the time if server is available + { + m_prefs->ClearUserPref(unavailablePref.get()); + } + // NS_ERROR_NOT_AVAILABLE signifies a server that could not be + // instantiated, presumably because of an invalid type. + else if (rv == NS_ERROR_NOT_AVAILABLE) { + m_prefs->GetIntPref(unavailablePref.get(), &timeUnavailable); + if (!timeUnavailable) { // we need to set it, this must be the first + // time unavailable + uint32_t nowSeconds; + PRTime2Seconds(PR_Now(), &nowSeconds); + m_prefs->SetIntPref(unavailablePref.get(), nowSeconds); + deleteAccount = false; + } + } + } + + // Our server is still unavailable. Have we timed out yet? + if (rv == NS_ERROR_NOT_AVAILABLE && timeUnavailable != 0) { + uint32_t nowSeconds; + PRTime2Seconds(PR_Now(), &nowSeconds); + if ((int32_t)nowSeconds < timeUnavailable + secondsToLeave) + deleteAccount = false; + } + + if (deleteAccount) { + dupAccounts.AppendObject(account); + m_accounts.RemoveElement(account); + } + } + + // Check if we removed one or more of the accounts in the pref string. + // If so, rewrite the pref string. + if (accountsArray.Length() != m_accounts.Length()) OutputAccountsPref(); + + int32_t cnt = dupAccounts.Count(); + nsCOMPtr<nsIMsgAccount> dupAccount; + + // Go through the accounts seeing if any existing server is deferred to + // an account we removed. If so, fix the deferral. Then clean up the prefs + // for the removed account. + for (int32_t i = 0; i < cnt; i++) { + dupAccount = dupAccounts[i]; + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + /* + * This loop gets run for every incoming server, and is passed a + * duplicate account. It checks that the server is not deferred to the + * duplicate account. If it is, then it looks up the information for the + * duplicate account's server (username, hostName, type), and finds an + * account with a server with the same username, hostname, and type, and + * if it finds one, defers to that account instead. Generally, this will + * be a Local Folders account, since 2.0 has a bug where duplicate Local + * Folders accounts are created. + */ + nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data(); + nsCString type; + server->GetType(type); + if (type.EqualsLiteral("pop3")) { + nsCString deferredToAccount; + // Get the pref directly, because the GetDeferredToAccount accessor + // attempts to fix broken deferrals, but we know more about what the + // deferred to account was. + server->GetCharValue("deferred_to_account", deferredToAccount); + if (!deferredToAccount.IsEmpty()) { + nsCString dupAccountKey; + dupAccount->GetKey(dupAccountKey); + if (deferredToAccount.Equals(dupAccountKey)) { + nsresult rv; + nsCString accountPref("mail.account."); + nsCString dupAccountServerKey; + accountPref.Append(dupAccountKey); + accountPref.AppendLiteral(".server"); + nsCOMPtr<nsIPrefService> prefservice( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + continue; + } + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) { + continue; + } + rv = + prefBranch->GetCharPref(accountPref.get(), dupAccountServerKey); + if (NS_FAILED(rv)) { + continue; + } + nsCOMPtr<nsIPrefBranch> serverPrefBranch; + nsCString serverKeyPref(PREF_MAIL_SERVER_PREFIX); + serverKeyPref.Append(dupAccountServerKey); + serverKeyPref.Append('.'); + rv = prefservice->GetBranch(serverKeyPref.get(), + getter_AddRefs(serverPrefBranch)); + if (NS_FAILED(rv)) { + continue; + } + nsCString userName; + nsCString hostName; + nsCString type; + serverPrefBranch->GetCharPref("userName", userName); + serverPrefBranch->GetCharPref("hostname", hostName); + serverPrefBranch->GetCharPref("type", type); + // Find a server with the same info. + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + if (NS_FAILED(rv)) { + continue; + } + nsCOMPtr<nsIMsgIncomingServer> server; + accountManager->FindServer(userName, hostName, type, 0, + getter_AddRefs(server)); + if (server) { + nsCOMPtr<nsIMsgAccount> replacement; + accountManager->FindAccountForServer(server, + getter_AddRefs(replacement)); + if (replacement) { + nsCString accountKey; + replacement->GetKey(accountKey); + if (!accountKey.IsEmpty()) + server->SetCharValue("deferred_to_account", accountKey); + } + } + } + } + } + } + + nsAutoCString accountKeyPref("mail.account."); + nsCString dupAccountKey; + dupAccount->GetKey(dupAccountKey); + if (dupAccountKey.IsEmpty()) continue; + accountKeyPref.Append(dupAccountKey); + accountKeyPref.Append('.'); + + nsCOMPtr<nsIPrefBranch> accountPrefBranch; + rv = prefservice->GetBranch(accountKeyPref.get(), + getter_AddRefs(accountPrefBranch)); + if (accountPrefBranch) { + nsTArray<nsCString> prefNames; + nsresult rv = accountPrefBranch->GetChildList("", prefNames); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto& prefName : prefNames) { + accountPrefBranch->ClearUserPref(prefName.get()); + } + } + } + + // Make sure we have an account that points at the local folders server + nsCString localFoldersServerKey; + rv = m_prefs->GetCharPref(PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER, + localFoldersServerKey); + + if (!localFoldersServerKey.IsEmpty()) { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetIncomingServer(localFoldersServerKey, getter_AddRefs(server)); + if (server) { + nsCOMPtr<nsIMsgAccount> localFoldersAccount; + findAccountByServerKey(localFoldersServerKey, + getter_AddRefs(localFoldersAccount)); + // If we don't have an existing account pointing at the local folders + // server, we're going to add one. + if (!localFoldersAccount) { + nsCOMPtr<nsIMsgAccount> account; + (void)CreateAccount(getter_AddRefs(account)); + if (account) account->SetIncomingServer(server); + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::ReactivateAccounts() { + for (nsIMsgAccount* account : m_accounts) { + // This will error out if the account already has its server, or + // if this isn't the account that the extension is trying to reactivate. + if (NS_SUCCEEDED(account->CreateServer())) { + nsCOMPtr<nsIMsgIncomingServer> server; + account->GetIncomingServer(getter_AddRefs(server)); + // This triggers all of the notifications required by the UI. + account->SetIncomingServer(server); + } + } + return NS_OK; +} + +// this routine goes through all the identities and makes sure +// that the special folders for each identity have the +// correct special folder flags set, e.g, the Sent folder has +// the sent flag set. +// +// it also goes through all the spam settings for each account +// and makes sure the folder flags are set there, too +NS_IMETHODIMP +nsMsgAccountManager::SetSpecialFolders() { + nsTArray<RefPtr<nsIMsgIdentity>> identities; + GetAllIdentities(identities); + + for (auto identity : identities) { + nsresult rv; + nsCString folderUri; + nsCOMPtr<nsIMsgFolder> folder; + + identity->GetFccFolder(folderUri); + if (!folderUri.IsEmpty() && + NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) { + nsCOMPtr<nsIMsgFolder> parent; + rv = folder->GetParent(getter_AddRefs(parent)); + if (NS_SUCCEEDED(rv) && parent) { + rv = folder->SetFlag(nsMsgFolderFlags::SentMail); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + identity->GetDraftFolder(folderUri); + if (!folderUri.IsEmpty() && + NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) { + nsCOMPtr<nsIMsgFolder> parent; + rv = folder->GetParent(getter_AddRefs(parent)); + if (NS_SUCCEEDED(rv) && parent) { + rv = folder->SetFlag(nsMsgFolderFlags::Drafts); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + identity->GetArchiveFolder(folderUri); + if (!folderUri.IsEmpty() && + NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) { + nsCOMPtr<nsIMsgFolder> parent; + rv = folder->GetParent(getter_AddRefs(parent)); + if (NS_SUCCEEDED(rv) && parent) { + bool archiveEnabled; + identity->GetArchiveEnabled(&archiveEnabled); + if (archiveEnabled) + rv = folder->SetFlag(nsMsgFolderFlags::Archive); + else + rv = folder->ClearFlag(nsMsgFolderFlags::Archive); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + identity->GetStationeryFolder(folderUri); + if (!folderUri.IsEmpty() && + NS_SUCCEEDED(GetOrCreateFolder(folderUri, getter_AddRefs(folder)))) { + nsCOMPtr<nsIMsgFolder> parent; + rv = folder->GetParent(getter_AddRefs(parent)); + if (NS_SUCCEEDED(rv) && parent) { + folder->SetFlag(nsMsgFolderFlags::Templates); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + + // XXX todo + // get all servers + // get all spam settings for each server + // set the JUNK folder flag on the spam folders, right? + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::UnloadAccounts() { + // release the default account + m_defaultAccount = nullptr; + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data(); + if (!server) continue; + nsresult rv; + NotifyServerUnloaded(server); + + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = server->GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv)) { + removeListenersFromFolder(rootFolder); + + rootFolder->Shutdown(true); + } + } + + m_accounts.Clear(); // will release all elements + m_identities.Clear(); + m_incomingServers.Clear(); + mAccountKeyList.Truncate(); + SetLastServerFound(nullptr, EmptyCString(), EmptyCString(), 0, + EmptyCString()); + + if (m_accountsLoaded) { + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1"); + if (mailSession) mailSession->RemoveFolderListener(this); + m_accountsLoaded = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::ShutdownServers() { + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data(); + if (server) server->Shutdown(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::CloseCachedConnections() { + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data(); + if (server) server->CloseCachedConnections(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::CleanupOnExit() { + // This can get called multiple times, and potentially re-entrantly. + // So add some protection against that. + if (m_shutdownInProgress) return NS_OK; + + m_shutdownInProgress = true; + + nsresult rv; + // If enabled, clear cache on shutdown. This is common to all accounts. + bool clearCache = false; + m_prefs->GetBoolPref("privacy.clearOnShutdown.cache", &clearCache); + if (clearCache) { + nsCOMPtr<nsICacheStorageService> cacheStorageService = + do_GetService("@mozilla.org/netwerk/cache-storage-service;1", &rv); + if (NS_SUCCEEDED(rv)) cacheStorageService->Clear(); + } + + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data(); + + bool emptyTrashOnExit = false; + bool cleanupInboxOnExit = false; + + if (WeAreOffline()) break; + + if (!server) continue; + + server->GetEmptyTrashOnExit(&emptyTrashOnExit); + nsCOMPtr<nsIImapIncomingServer> imapserver = do_QueryInterface(server); + if (imapserver) { + imapserver->GetCleanupInboxOnExit(&cleanupInboxOnExit); + imapserver->SetShuttingDown(true); + } + if (emptyTrashOnExit || cleanupInboxOnExit) { + nsCOMPtr<nsIMsgFolder> root; + server->GetRootFolder(getter_AddRefs(root)); + nsCString type; + server->GetType(type); + if (root) { + nsString passwd; + int32_t authMethod = 0; + bool serverRequiresPasswordForAuthentication = true; + bool isImap = type.EqualsLiteral("imap"); + if (isImap) { + server->GetServerRequiresPasswordForBiff( + &serverRequiresPasswordForAuthentication); + server->GetPassword(passwd); + server->GetAuthMethod(&authMethod); + } + if (!isImap || (isImap && (!serverRequiresPasswordForAuthentication || + !passwd.IsEmpty() || + authMethod == nsMsgAuthMethod::OAuth2))) { + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + if (NS_FAILED(rv)) continue; + + if (isImap && cleanupInboxOnExit) { + // Find the inbox. + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + rv = root->GetSubFolders(subFolders); + if (NS_SUCCEEDED(rv)) { + for (nsIMsgFolder* folder : subFolders) { + uint32_t flags; + folder->GetFlags(&flags); + if (flags & nsMsgFolderFlags::Inbox) { + // This is inbox, so Compact() it. There's an implied + // Expunge too, because this is IMAP. + RefPtr<UrlListener> cleanupListener = new UrlListener(); + RefPtr<nsMsgAccountManager> self = this; + // This runs when the compaction (+expunge) is complete. + cleanupListener->mStopFn = + [self](nsIURI* url, nsresult status) -> nsresult { + if (self->m_folderDoingCleanupInbox) { + PR_CEnterMonitor(self->m_folderDoingCleanupInbox); + PR_CNotifyAll(self->m_folderDoingCleanupInbox); + self->m_cleanupInboxInProgress = false; + PR_CExitMonitor(self->m_folderDoingCleanupInbox); + self->m_folderDoingCleanupInbox = nullptr; + } + return NS_OK; + }; + + rv = folder->Compact(cleanupListener, nullptr); + if (NS_SUCCEEDED(rv)) + accountManager->SetFolderDoingCleanupInbox(folder); + break; + } + } + } + } + + if (emptyTrashOnExit) { + RefPtr<UrlListener> emptyTrashListener = new UrlListener(); + RefPtr<nsMsgAccountManager> self = this; + // This runs when the trash-emptying is complete. + // (It'll be a nsIImapUrl::nsImapDeleteAllMsgs url). + emptyTrashListener->mStopFn = [self](nsIURI* url, + nsresult status) -> nsresult { + if (self->m_folderDoingEmptyTrash) { + PR_CEnterMonitor(self->m_folderDoingEmptyTrash); + PR_CNotifyAll(self->m_folderDoingEmptyTrash); + self->m_emptyTrashInProgress = false; + PR_CExitMonitor(self->m_folderDoingEmptyTrash); + self->m_folderDoingEmptyTrash = nullptr; + } + return NS_OK; + }; + + rv = root->EmptyTrash(emptyTrashListener); + if (isImap && NS_SUCCEEDED(rv)) + accountManager->SetFolderDoingEmptyTrash(root); + } + + if (isImap) { + nsCOMPtr<nsIThread> thread(do_GetCurrentThread()); + + // Pause until any possible inbox-compaction and trash-emptying + // are complete (or time out). + bool inProgress = false; + if (cleanupInboxOnExit) { + int32_t loopCount = 0; // used to break out after 5 seconds + accountManager->GetCleanupInboxInProgress(&inProgress); + while (inProgress && loopCount++ < 5000) { + accountManager->GetCleanupInboxInProgress(&inProgress); + PR_CEnterMonitor(root); + PR_CWait(root, PR_MicrosecondsToInterval(1000UL)); + PR_CExitMonitor(root); + NS_ProcessPendingEvents(thread, + PR_MicrosecondsToInterval(1000UL)); + } + } + if (emptyTrashOnExit) { + accountManager->GetEmptyTrashInProgress(&inProgress); + int32_t loopCount = 0; + while (inProgress && loopCount++ < 5000) { + accountManager->GetEmptyTrashInProgress(&inProgress); + PR_CEnterMonitor(root); + PR_CWait(root, PR_MicrosecondsToInterval(1000UL)); + PR_CExitMonitor(root); + NS_ProcessPendingEvents(thread, + PR_MicrosecondsToInterval(1000UL)); + } + } + } + } + } + } + } + + // Try to do this early on in the shutdown process before + // necko shuts itself down. + CloseCachedConnections(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::WriteToFolderCache(nsIMsgFolderCache* folderCache) { + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + iter.Data()->WriteToFolderCache(folderCache); + } + return NS_OK; +} + +nsresult nsMsgAccountManager::createKeyedAccount(const nsCString& key, + bool forcePositionToEnd, + nsIMsgAccount** aAccount) { + nsresult rv; + nsCOMPtr<nsIMsgAccount> account = do_CreateInstance(kMsgAccountCID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + account->SetKey(key); + + nsCString localFoldersAccountKey; + nsCString lastFolderAccountKey; + if (!forcePositionToEnd) { + nsCOMPtr<nsIMsgIncomingServer> localFoldersServer; + rv = GetLocalFoldersServer(getter_AddRefs(localFoldersServer)); + if (NS_SUCCEEDED(rv)) { + for (auto account : m_accounts) { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = account->GetIncomingServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server == localFoldersServer) { + account->GetKey(localFoldersAccountKey); + break; + } + } + } + + // Extracting the account key of the last mail acoount. + for (int32_t index = m_accounts.Length() - 1; index >= 0; index--) { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = m_accounts[index]->GetIncomingServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) { + nsCString accountType; + rv = server->GetType(accountType); + if (NS_SUCCEEDED(rv) && !accountType.EqualsLiteral("im")) { + m_accounts[index]->GetKey(lastFolderAccountKey); + break; + } + } + } + } + + if (!forcePositionToEnd && !localFoldersAccountKey.IsEmpty() && + !lastFolderAccountKey.IsEmpty() && + lastFolderAccountKey == localFoldersAccountKey) { + // Insert account before Local Folders if that is the last account. + m_accounts.InsertElementAt(m_accounts.Length() - 1, account); + } else { + m_accounts.AppendElement(account); + } + + nsCString newAccountKeyList; + nsCString accountKey; + for (uint32_t index = 0; index < m_accounts.Length(); index++) { + m_accounts[index]->GetKey(accountKey); + if (index) newAccountKeyList.Append(ACCOUNT_DELIMITER); + newAccountKeyList.Append(accountKey); + } + mAccountKeyList = newAccountKeyList; + + m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_ACCOUNTS, mAccountKeyList); + account.forget(aAccount); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::CreateAccount(nsIMsgAccount** _retval) { + NS_ENSURE_ARG_POINTER(_retval); + + nsAutoCString key; + GetUniqueAccountKey(key); + + return createKeyedAccount(key, false, _retval); +} + +NS_IMETHODIMP +nsMsgAccountManager::GetAccount(const nsACString& aKey, + nsIMsgAccount** aAccount) { + NS_ENSURE_ARG_POINTER(aAccount); + *aAccount = nullptr; + + for (uint32_t i = 0; i < m_accounts.Length(); ++i) { + nsCOMPtr<nsIMsgAccount> account(m_accounts[i]); + nsCString key; + account->GetKey(key); + if (key.Equals(aKey)) { + account.forget(aAccount); + break; + } + } + + // If not found, create on demand. + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::FindServerIndex(nsIMsgIncomingServer* server, + int32_t* result) { + NS_ENSURE_ARG_POINTER(server); + NS_ENSURE_ARG_POINTER(result); + + nsCString key; + nsresult rv = server->GetKey(key); + NS_ENSURE_SUCCESS(rv, rv); + + // do this by account because the account list is in order + uint32_t i; + for (i = 0; i < m_accounts.Length(); ++i) { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = m_accounts[i]->GetIncomingServer(getter_AddRefs(server)); + if (!server || NS_FAILED(rv)) continue; + + nsCString serverKey; + rv = server->GetKey(serverKey); + if (NS_FAILED(rv)) continue; + + // stop when found, + // index will be set to the current index + if (serverKey.Equals(key)) break; + } + + // Even if the search failed, we can return index. + // This means that all servers not in the array return an index higher + // than all "registered" servers. + *result = i; + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::AddIncomingServerListener( + nsIIncomingServerListener* serverListener) { + m_incomingServerListeners.AppendObject(serverListener); + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::RemoveIncomingServerListener( + nsIIncomingServerListener* serverListener) { + m_incomingServerListeners.RemoveObject(serverListener); + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::NotifyServerLoaded( + nsIMsgIncomingServer* server) { + int32_t count = m_incomingServerListeners.Count(); + for (int32_t i = 0; i < count; i++) { + nsIIncomingServerListener* listener = m_incomingServerListeners[i]; + listener->OnServerLoaded(server); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::NotifyServerUnloaded( + nsIMsgIncomingServer* server) { + NS_ENSURE_ARG_POINTER(server); + + int32_t count = m_incomingServerListeners.Count(); + // Clear this to cut shutdown leaks. We are always passing valid non-null + // server here. + server->SetFilterList(nullptr); + + for (int32_t i = 0; i < count; i++) { + nsIIncomingServerListener* listener = m_incomingServerListeners[i]; + listener->OnServerUnloaded(server); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::NotifyServerChanged( + nsIMsgIncomingServer* server) { + int32_t count = m_incomingServerListeners.Count(); + for (int32_t i = 0; i < count; i++) { + nsIIncomingServerListener* listener = m_incomingServerListeners[i]; + listener->OnServerChanged(server); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::FindServerByURI(nsIURI* aURI, + nsIMsgIncomingServer** aResult) { + NS_ENSURE_ARG_POINTER(aURI); + + nsresult rv = LoadAccounts(); + NS_ENSURE_SUCCESS(rv, rv); + + // Get username and hostname and port so we can get the server + nsAutoCString username; + nsAutoCString escapedUsername; + rv = aURI->GetUserPass(escapedUsername); + if (NS_SUCCEEDED(rv) && !escapedUsername.IsEmpty()) + MsgUnescapeString(escapedUsername, 0, username); + + nsAutoCString hostname; + nsAutoCString escapedHostname; + rv = aURI->GetHost(escapedHostname); + if (NS_SUCCEEDED(rv) && !escapedHostname.IsEmpty()) { + MsgUnescapeString(escapedHostname, 0, hostname); + } + + nsAutoCString type; + rv = aURI->GetScheme(type); + if (NS_SUCCEEDED(rv) && !type.IsEmpty()) { + // Remove "-message" from the scheme in case we get called with + // "imap-message", "mailbox-message", or friends. + if (StringEndsWith(type, "-message"_ns)) type.SetLength(type.Length() - 8); + // now modify type if pop or news + if (type.EqualsLiteral("pop")) type.AssignLiteral("pop3"); + // we use "nntp" in the server list so translate it here. + else if (type.EqualsLiteral("news")) + type.AssignLiteral("nntp"); + // we use "any" as the wildcard type. + else if (type.EqualsLiteral("any")) + type.Truncate(); + } + + int32_t port = 0; + // check the port of the scheme is not 'none' or blank + if (!(type.EqualsLiteral("none") || type.IsEmpty())) { + rv = aURI->GetPort(&port); + // Set the port to zero if we got a -1 (use default) + if (NS_SUCCEEDED(rv) && (port == -1)) port = 0; + } + + return findServerInternal(username, hostname, type, port, aResult); +} + +nsresult nsMsgAccountManager::findServerInternal( + const nsACString& username, const nsACString& serverHostname, + const nsACString& type, int32_t port, nsIMsgIncomingServer** aResult) { + if ((m_lastFindServerUserName.Equals(username)) && + (m_lastFindServerHostName.Equals(serverHostname)) && + (m_lastFindServerType.Equals(type)) && (m_lastFindServerPort == port) && + m_lastFindServerResult) { + NS_ADDREF(*aResult = m_lastFindServerResult); + return NS_OK; + } + + nsresult rv; + nsCString hostname; + nsCOMPtr<nsIIDNService> idnService = + do_GetService("@mozilla.org/network/idn-service;1"); + rv = idnService->Normalize(serverHostname, hostname); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + // Find matching server by user+host+type+port. + nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data(); + + if (!server) continue; + + nsCString thisHostname; + rv = server->GetHostName(thisHostname); + if (NS_FAILED(rv)) continue; + + rv = idnService->Normalize(thisHostname, thisHostname); + if (NS_FAILED(rv)) continue; + + // If the hostname was a IP with trailing dot, that dot gets removed + // during URI mutation. We may well be here in findServerInternal to + // find a server from a folder URI. Remove the trailing dot so we can + // find the server. + nsCString thisHostnameNoDot(thisHostname); + if (!thisHostname.IsEmpty() && + thisHostname.CharAt(thisHostname.Length() - 1) == '.') { + thisHostnameNoDot.Cut(thisHostname.Length() - 1, 1); + } + + nsCString thisUsername; + rv = server->GetUsername(thisUsername); + if (NS_FAILED(rv)) continue; + + nsCString thisType; + rv = server->GetType(thisType); + if (NS_FAILED(rv)) continue; + + int32_t thisPort = -1; // use the default port identifier + // Don't try and get a port for the 'none' scheme + if (!thisType.EqualsLiteral("none")) { + rv = server->GetPort(&thisPort); + if (NS_FAILED(rv)) { + continue; + } + } + + // treat "" as a wild card, so if the caller passed in "" for the desired + // attribute treat it as a match + if ((type.IsEmpty() || thisType.Equals(type)) && + (hostname.IsEmpty() || + thisHostname.Equals(hostname, nsCaseInsensitiveCStringComparator) || + thisHostnameNoDot.Equals(hostname, + nsCaseInsensitiveCStringComparator)) && + (!(port != 0) || (port == thisPort)) && + (username.IsEmpty() || thisUsername.Equals(username))) { + // stop on first find; cache for next time + SetLastServerFound(server, hostname, username, port, type); + + NS_ADDREF(*aResult = server); // Was populated from member variable. + return NS_OK; + } + } + + return NS_ERROR_UNEXPECTED; +} + +// Always return NS_OK; +NS_IMETHODIMP +nsMsgAccountManager::FindServer(const nsACString& username, + const nsACString& hostname, + const nsACString& type, int32_t port, + nsIMsgIncomingServer** aResult) { + *aResult = nullptr; + findServerInternal(username, hostname, type, port, aResult); + return NS_OK; +} + +void nsMsgAccountManager::findAccountByServerKey(const nsCString& aKey, + nsIMsgAccount** aResult) { + *aResult = nullptr; + + for (uint32_t i = 0; i < m_accounts.Length(); ++i) { + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = m_accounts[i]->GetIncomingServer(getter_AddRefs(server)); + if (!server || NS_FAILED(rv)) continue; + + nsCString key; + rv = server->GetKey(key); + if (NS_FAILED(rv)) continue; + + // if the keys are equal, the servers are equal + if (key.Equals(aKey)) { + NS_ADDREF(*aResult = m_accounts[i]); + break; // stop on first found account + } + } +} + +NS_IMETHODIMP +nsMsgAccountManager::FindAccountForServer(nsIMsgIncomingServer* server, + nsIMsgAccount** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + if (!server) { + (*aResult) = nullptr; + return NS_OK; + } + + nsresult rv; + + nsCString key; + rv = server->GetKey(key); + NS_ENSURE_SUCCESS(rv, rv); + + findAccountByServerKey(key, aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetFirstIdentityForServer(nsIMsgIncomingServer* aServer, + nsIMsgIdentity** aIdentity) { + NS_ENSURE_ARG_POINTER(aServer); + NS_ENSURE_ARG_POINTER(aIdentity); + + nsTArray<RefPtr<nsIMsgIdentity>> identities; + nsresult rv = GetIdentitiesForServer(aServer, identities); + NS_ENSURE_SUCCESS(rv, rv); + + // not all servers have identities + // for example, Local Folders + if (identities.IsEmpty()) { + *aIdentity = nullptr; + } else { + NS_IF_ADDREF(*aIdentity = identities[0]); + } + return rv; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetIdentitiesForServer( + nsIMsgIncomingServer* server, + nsTArray<RefPtr<nsIMsgIdentity>>& identities) { + NS_ENSURE_ARG_POINTER(server); + nsresult rv = LoadAccounts(); + NS_ENSURE_SUCCESS(rv, rv); + + identities.Clear(); + + nsAutoCString serverKey; + rv = server->GetKey(serverKey); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto account : m_accounts) { + nsCOMPtr<nsIMsgIncomingServer> thisServer; + rv = account->GetIncomingServer(getter_AddRefs(thisServer)); + if (NS_FAILED(rv) || !thisServer) continue; + + nsAutoCString thisServerKey; + rv = thisServer->GetKey(thisServerKey); + if (serverKey.Equals(thisServerKey)) { + nsTArray<RefPtr<nsIMsgIdentity>> theseIdentities; + rv = account->GetIdentities(theseIdentities); + NS_ENSURE_SUCCESS(rv, rv); + identities.AppendElements(theseIdentities); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetServersForIdentity( + nsIMsgIdentity* aIdentity, + nsTArray<RefPtr<nsIMsgIncomingServer>>& servers) { + NS_ENSURE_ARG_POINTER(aIdentity); + servers.Clear(); + + nsresult rv; + rv = LoadAccounts(); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto account : m_accounts) { + nsTArray<RefPtr<nsIMsgIdentity>> identities; + if (NS_FAILED(account->GetIdentities(identities))) continue; + + nsCString identityKey; + aIdentity->GetKey(identityKey); + for (auto thisIdentity : identities) { + nsCString thisIdentityKey; + rv = thisIdentity->GetKey(thisIdentityKey); + + if (NS_SUCCEEDED(rv) && identityKey.Equals(thisIdentityKey)) { + nsCOMPtr<nsIMsgIncomingServer> thisServer; + rv = account->GetIncomingServer(getter_AddRefs(thisServer)); + if (thisServer && NS_SUCCEEDED(rv)) { + servers.AppendElement(thisServer); + break; + } + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::AddRootFolderListener(nsIFolderListener* aListener) { + NS_ENSURE_TRUE(aListener, NS_OK); + mFolderListeners.AppendElement(aListener); + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = iter.Data()->GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_FAILED(rv)) { + continue; + } + rv = rootFolder->AddFolderListener(aListener); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::RemoveRootFolderListener(nsIFolderListener* aListener) { + NS_ENSURE_TRUE(aListener, NS_OK); + mFolderListeners.RemoveElement(aListener); + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = iter.Data()->GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_FAILED(rv)) { + continue; + } + rv = rootFolder->RemoveFolderListener(aListener); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::SetLocalFoldersServer( + nsIMsgIncomingServer* aServer) { + NS_ENSURE_ARG_POINTER(aServer); + nsCString key; + nsresult rv = aServer->GetKey(key); + NS_ENSURE_SUCCESS(rv, rv); + + return m_prefs->SetCharPref(PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER, key); +} + +NS_IMETHODIMP nsMsgAccountManager::GetLocalFoldersServer( + nsIMsgIncomingServer** aServer) { + NS_ENSURE_ARG_POINTER(aServer); + + nsCString serverKey; + + nsresult rv = m_prefs->GetCharPref( + PREF_MAIL_ACCOUNTMANAGER_LOCALFOLDERSSERVER, serverKey); + + if (NS_SUCCEEDED(rv) && !serverKey.IsEmpty()) { + rv = GetIncomingServer(serverKey, aServer); + if (NS_SUCCEEDED(rv)) return rv; + // otherwise, we're going to fall through to looking for an existing local + // folders account, because now we fail creating one if one already exists. + } + + // try ("nobody","Local Folders","none"), and work down to any "none" server. + rv = findServerInternal("nobody"_ns, "Local Folders"_ns, "none"_ns, 0, + aServer); + if (NS_FAILED(rv) || !*aServer) { + rv = findServerInternal("nobody"_ns, EmptyCString(), "none"_ns, 0, aServer); + if (NS_FAILED(rv) || !*aServer) { + rv = findServerInternal(EmptyCString(), "Local Folders"_ns, "none"_ns, 0, + aServer); + if (NS_FAILED(rv) || !*aServer) + rv = findServerInternal(EmptyCString(), EmptyCString(), "none"_ns, 0, + aServer); + } + } + + NS_ENSURE_SUCCESS(rv, rv); + if (!*aServer) return NS_ERROR_FAILURE; + + // we don't want the Smart Mailboxes server to be the local server. + bool hidden; + (*aServer)->GetHidden(&hidden); + if (hidden) return NS_ERROR_FAILURE; + + rv = SetLocalFoldersServer(*aServer); + return rv; +} + +nsresult nsMsgAccountManager::GetLocalFoldersPrettyName( + nsString& localFoldersName) { + // we don't want "nobody at Local Folders" to show up in the + // folder pane, so we set the pretty name to a localized "Local Folders" + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv; + nsCOMPtr<nsIStringBundleService> sBundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED); + + rv = sBundleService->CreateBundle( + "chrome://messenger/locale/messenger.properties", getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + return bundle->GetStringFromName("localFolders", localFoldersName); +} + +NS_IMETHODIMP +nsMsgAccountManager::CreateLocalMailAccount() { + // create the server + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = CreateIncomingServer("nobody"_ns, "Local Folders"_ns, "none"_ns, + getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString localFoldersName; + rv = GetLocalFoldersPrettyName(localFoldersName); + NS_ENSURE_SUCCESS(rv, rv); + server->SetPrettyName(localFoldersName); + + nsCOMPtr<nsINoIncomingServer> noServer; + noServer = do_QueryInterface(server, &rv); + if (NS_FAILED(rv)) return rv; + + // create the directory structure for old 4.x "Local Mail" + // under <profile dir>/Mail/Local Folders or + // <"mail.directory" pref>/Local Folders + nsCOMPtr<nsIFile> mailDir; + bool dirExists; + + // we want <profile>/Mail + rv = NS_GetSpecialDirectory(NS_APP_MAIL_50_DIR, getter_AddRefs(mailDir)); + if (NS_FAILED(rv)) return rv; + + rv = mailDir->Exists(&dirExists); + if (NS_SUCCEEDED(rv) && !dirExists) + rv = mailDir->Create(nsIFile::DIRECTORY_TYPE, 0775); + if (NS_FAILED(rv)) return rv; + + // set the default local path for "none" + rv = server->SetDefaultLocalPath(mailDir); + if (NS_FAILED(rv)) return rv; + + // Create an account when valid server values are established. + // This will keep the status of accounts sane by avoiding the addition of + // incomplete accounts. + nsCOMPtr<nsIMsgAccount> account; + rv = CreateAccount(getter_AddRefs(account)); + if (NS_FAILED(rv)) return rv; + + // notice, no identity for local mail + // hook the server to the account + // after we set the server's local path + // (see bug #66018) + account->SetIncomingServer(server); + + // remember this as the local folders server + return SetLocalFoldersServer(server); +} + +NS_IMETHODIMP +nsMsgAccountManager::SetFolderDoingEmptyTrash(nsIMsgFolder* folder) { + m_folderDoingEmptyTrash = folder; + m_emptyTrashInProgress = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetEmptyTrashInProgress(bool* bVal) { + NS_ENSURE_ARG_POINTER(bVal); + *bVal = m_emptyTrashInProgress; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::SetFolderDoingCleanupInbox(nsIMsgFolder* folder) { + m_folderDoingCleanupInbox = folder; + m_cleanupInboxInProgress = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetCleanupInboxInProgress(bool* bVal) { + NS_ENSURE_ARG_POINTER(bVal); + *bVal = m_cleanupInboxInProgress; + return NS_OK; +} + +void nsMsgAccountManager::SetLastServerFound(nsIMsgIncomingServer* server, + const nsACString& hostname, + const nsACString& username, + const int32_t port, + const nsACString& type) { + m_lastFindServerResult = server; + m_lastFindServerHostName = hostname; + m_lastFindServerUserName = username; + m_lastFindServerPort = port; + m_lastFindServerType = type; +} + +NS_IMETHODIMP +nsMsgAccountManager::SaveAccountInfo() { + nsresult rv; + nsCOMPtr<nsIPrefService> pref(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return pref->SavePrefFile(nullptr); +} + +NS_IMETHODIMP +nsMsgAccountManager::GetChromePackageName(const nsACString& aExtensionName, + nsACString& aChromePackageName) { + nsresult rv; + nsCOMPtr<nsICategoryManager> catman = + do_GetService(NS_CATEGORYMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISimpleEnumerator> e; + rv = catman->EnumerateCategory(MAILNEWS_ACCOUNTMANAGER_EXTENSIONS, + getter_AddRefs(e)); + if (NS_SUCCEEDED(rv) && e) { + while (true) { + nsCOMPtr<nsISupports> supports; + rv = e->GetNext(getter_AddRefs(supports)); + nsCOMPtr<nsISupportsCString> catEntry = do_QueryInterface(supports); + if (NS_FAILED(rv) || !catEntry) break; + + nsAutoCString entryString; + rv = catEntry->GetData(entryString); + if (NS_FAILED(rv)) break; + + nsCString contractidString; + rv = catman->GetCategoryEntry(MAILNEWS_ACCOUNTMANAGER_EXTENSIONS, + entryString, contractidString); + if (NS_FAILED(rv)) break; + + nsCOMPtr<nsIMsgAccountManagerExtension> extension = + do_GetService(contractidString.get(), &rv); + if (NS_FAILED(rv) || !extension) break; + + nsCString name; + rv = extension->GetName(name); + if (NS_FAILED(rv)) break; + + if (name.Equals(aExtensionName)) + return extension->GetChromePackageName(aChromePackageName); + } + } + return NS_ERROR_UNEXPECTED; +} + +class VFChangeListenerEvent : public mozilla::Runnable { + public: + VFChangeListenerEvent(VirtualFolderChangeListener* vfChangeListener, + nsIMsgFolder* virtFolder, nsIMsgDatabase* virtDB) + : mozilla::Runnable("VFChangeListenerEvent"), + mVFChangeListener(vfChangeListener), + mFolder(virtFolder), + mDB(virtDB) {} + + NS_IMETHOD Run() { + if (mVFChangeListener) mVFChangeListener->ProcessUpdateEvent(mFolder, mDB); + return NS_OK; + } + + private: + RefPtr<VirtualFolderChangeListener> mVFChangeListener; + nsCOMPtr<nsIMsgFolder> mFolder; + nsCOMPtr<nsIMsgDatabase> mDB; +}; + +NS_IMPL_ISUPPORTS(VirtualFolderChangeListener, nsIDBChangeListener) + +VirtualFolderChangeListener::VirtualFolderChangeListener() + : m_searchOnMsgStatus(false), m_batchingEvents(false) {} + +nsresult VirtualFolderChangeListener::Init() { + nsCOMPtr<nsIMsgDatabase> msgDB; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + + nsresult rv = m_virtualFolder->GetDBFolderInfoAndDB( + getter_AddRefs(dbFolderInfo), getter_AddRefs(msgDB)); + if (NS_SUCCEEDED(rv) && msgDB) { + nsCString searchTermString; + dbFolderInfo->GetCharProperty("searchStr", searchTermString); + nsCOMPtr<nsIMsgFilterService> filterService = + do_GetService("@mozilla.org/messenger/services/filters;1", &rv); + nsCOMPtr<nsIMsgFilterList> filterList; + rv = filterService->GetTempFilterList(m_virtualFolder, + getter_AddRefs(filterList)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgFilter> tempFilter; + filterList->CreateFilter(u"temp"_ns, getter_AddRefs(tempFilter)); + NS_ENSURE_SUCCESS(rv, rv); + filterList->ParseCondition(tempFilter, searchTermString.get()); + NS_ENSURE_SUCCESS(rv, rv); + m_searchSession = + do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms; + rv = tempFilter->GetSearchTerms(searchTerms); + NS_ENSURE_SUCCESS(rv, rv); + + // we add the search scope right before we match the header, + // because we don't want the search scope caching the body input + // stream, because that holds onto the mailbox file, breaking + // compaction. + + // add each search term to the search session + for (nsIMsgSearchTerm* searchTerm : searchTerms) { + nsMsgSearchAttribValue attrib; + searchTerm->GetAttrib(&attrib); + if (attrib == nsMsgSearchAttrib::MsgStatus) m_searchOnMsgStatus = true; + m_searchSession->AppendTerm(searchTerm); + } + } + return rv; +} + +/** + * nsIDBChangeListener + */ + +NS_IMETHODIMP +VirtualFolderChangeListener::OnHdrPropertyChanged( + nsIMsgDBHdr* aHdrChanged, const nsACString& property, bool aPreChange, + uint32_t* aStatus, nsIDBChangeListener* aInstigator) { + const uint32_t kMatch = 0x1; + const uint32_t kRead = 0x2; + const uint32_t kNew = 0x4; + NS_ENSURE_ARG_POINTER(aHdrChanged); + NS_ENSURE_ARG_POINTER(aStatus); + + uint32_t flags; + bool match; + nsCOMPtr<nsIMsgDatabase> msgDB; + nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB)); + NS_ENSURE_SUCCESS(rv, rv); + // we don't want any early returns from this function, until we've + // called ClearScopes on the search session. + m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail, + m_folderWatching); + rv = m_searchSession->MatchHdr(aHdrChanged, msgDB, &match); + m_searchSession->ClearScopes(); + NS_ENSURE_SUCCESS(rv, rv); + aHdrChanged->GetFlags(&flags); + + if (aPreChange) // We're looking at the old header, save status + { + *aStatus = 0; + if (match) *aStatus |= kMatch; + if (flags & nsMsgMessageFlags::Read) *aStatus |= kRead; + if (flags & nsMsgMessageFlags::New) *aStatus |= kNew; + return NS_OK; + } + + // This is the post change section where changes are detected + + bool wasMatch = *aStatus & kMatch; + if (!match && !wasMatch) // header not in virtual folder + return NS_OK; + + int32_t totalDelta = 0, unreadDelta = 0, newDelta = 0; + + if (match) { + totalDelta++; + if (!(flags & nsMsgMessageFlags::Read)) unreadDelta++; + if (flags & nsMsgMessageFlags::New) newDelta++; + } + + if (wasMatch) { + totalDelta--; + if (!(*aStatus & kRead)) unreadDelta--; + if (*aStatus & kNew) newDelta--; + } + + if (!(unreadDelta || totalDelta || newDelta)) return NS_OK; + + nsCOMPtr<nsIMsgDatabase> virtDatabase; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(virtDatabase)); + NS_ENSURE_SUCCESS(rv, rv); + + if (unreadDelta) dbFolderInfo->ChangeNumUnreadMessages(unreadDelta); + + if (newDelta) { + int32_t numNewMessages; + m_virtualFolder->GetNumNewMessages(false, &numNewMessages); + m_virtualFolder->SetNumNewMessages(numNewMessages + newDelta); + m_virtualFolder->SetHasNewMessages(numNewMessages + newDelta > 0); + } + + if (totalDelta) { + dbFolderInfo->ChangeNumMessages(totalDelta); + nsCString searchUri; + m_virtualFolder->GetURI(searchUri); + msgDB->UpdateHdrInCache(searchUri, aHdrChanged, totalDelta == 1); + } + + PostUpdateEvent(m_virtualFolder, virtDatabase); + + return NS_OK; +} + +void VirtualFolderChangeListener::DecrementNewMsgCount() { + int32_t numNewMessages; + m_virtualFolder->GetNumNewMessages(false, &numNewMessages); + if (numNewMessages > 0) numNewMessages--; + m_virtualFolder->SetNumNewMessages(numNewMessages); + if (!numNewMessages) m_virtualFolder->SetHasNewMessages(false); +} + +NS_IMETHODIMP VirtualFolderChangeListener::OnHdrFlagsChanged( + nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) { + nsCOMPtr<nsIMsgDatabase> msgDB; + + nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB)); + bool oldMatch = false, newMatch = false; + // we don't want any early returns from this function, until we've + // called ClearScopes 0n the search session. + m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail, + m_folderWatching); + rv = m_searchSession->MatchHdr(aHdrChanged, msgDB, &newMatch); + if (NS_SUCCEEDED(rv) && m_searchOnMsgStatus) { + // if status is a search criteria, check if the header matched before + // it changed, in order to determine if we need to bump the counts. + aHdrChanged->SetFlags(aOldFlags); + rv = m_searchSession->MatchHdr(aHdrChanged, msgDB, &oldMatch); + // restore new flags even on match failure. + aHdrChanged->SetFlags(aNewFlags); + } else + oldMatch = newMatch; + m_searchSession->ClearScopes(); + NS_ENSURE_SUCCESS(rv, rv); + // we don't want to change the total counts if this virtual folder is open in + // a view, because we won't remove the header from view while it's open. On + // the other hand, it's hard to fix the count when the user clicks away to + // another folder, w/o re-running the search, or setting some sort of pending + // count change. Maybe this needs to be handled in the view code...the view + // could do the same calculation and also keep track of the counts changed. + // Then, when the view was closed, if it's a virtual folder, it could update + // the counts for the db. + if (oldMatch != newMatch || + (oldMatch && (aOldFlags & nsMsgMessageFlags::Read) != + (aNewFlags & nsMsgMessageFlags::Read))) { + nsCOMPtr<nsIMsgDatabase> virtDatabase; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + + rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(virtDatabase)); + NS_ENSURE_SUCCESS(rv, rv); + int32_t totalDelta = 0, unreadDelta = 0; + if (oldMatch != newMatch) { + // bool isOpen = false; + // nsCOMPtr<nsIMsgMailSession> mailSession = + // do_GetService("@mozilla.org/messenger/services/session;1"); + // if (mailSession && aFolder) + // mailSession->IsFolderOpenInWindow(m_virtualFolder, &isOpen); + // we can't remove headers that no longer match - but we might add headers + // that newly match, someday. + // if (!isOpen) + totalDelta = (oldMatch) ? -1 : 1; + } + bool msgHdrIsRead; + aHdrChanged->GetIsRead(&msgHdrIsRead); + if (oldMatch == newMatch) // read flag changed state + unreadDelta = (msgHdrIsRead) ? -1 : 1; + else if (oldMatch) // else header should removed + unreadDelta = (aOldFlags & nsMsgMessageFlags::Read) ? 0 : -1; + else // header should be added + unreadDelta = (aNewFlags & nsMsgMessageFlags::Read) ? 0 : 1; + if (unreadDelta) dbFolderInfo->ChangeNumUnreadMessages(unreadDelta); + if (totalDelta) dbFolderInfo->ChangeNumMessages(totalDelta); + if (unreadDelta == -1 && aOldFlags & nsMsgMessageFlags::New) + DecrementNewMsgCount(); + + if (totalDelta) { + nsCString searchUri; + m_virtualFolder->GetURI(searchUri); + msgDB->UpdateHdrInCache(searchUri, aHdrChanged, totalDelta == 1); + } + + PostUpdateEvent(m_virtualFolder, virtDatabase); + } else if (oldMatch && (aOldFlags & nsMsgMessageFlags::New) && + !(aNewFlags & nsMsgMessageFlags::New)) + DecrementNewMsgCount(); + + return rv; +} + +NS_IMETHODIMP VirtualFolderChangeListener::OnHdrDeleted( + nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, int32_t aFlags, + nsIDBChangeListener* aInstigator) { + nsCOMPtr<nsIMsgDatabase> msgDB; + + nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB)); + NS_ENSURE_SUCCESS(rv, rv); + bool match = false; + m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail, + m_folderWatching); + // Since the notifier went to the trouble of passing in the msg flags, + // we should use them when doing the match. + uint32_t msgFlags; + aHdrDeleted->GetFlags(&msgFlags); + aHdrDeleted->SetFlags(aFlags); + rv = m_searchSession->MatchHdr(aHdrDeleted, msgDB, &match); + aHdrDeleted->SetFlags(msgFlags); + m_searchSession->ClearScopes(); + if (match) { + nsCOMPtr<nsIMsgDatabase> virtDatabase; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + + rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(virtDatabase)); + NS_ENSURE_SUCCESS(rv, rv); + bool msgHdrIsRead; + aHdrDeleted->GetIsRead(&msgHdrIsRead); + if (!msgHdrIsRead) dbFolderInfo->ChangeNumUnreadMessages(-1); + dbFolderInfo->ChangeNumMessages(-1); + if (aFlags & nsMsgMessageFlags::New) { + int32_t numNewMessages; + m_virtualFolder->GetNumNewMessages(false, &numNewMessages); + m_virtualFolder->SetNumNewMessages(numNewMessages - 1); + if (numNewMessages == 1) m_virtualFolder->SetHasNewMessages(false); + } + + nsCString searchUri; + m_virtualFolder->GetURI(searchUri); + msgDB->UpdateHdrInCache(searchUri, aHdrDeleted, false); + + PostUpdateEvent(m_virtualFolder, virtDatabase); + } + return rv; +} + +NS_IMETHODIMP VirtualFolderChangeListener::OnHdrAdded( + nsIMsgDBHdr* aNewHdr, nsMsgKey aParentKey, int32_t aFlags, + nsIDBChangeListener* aInstigator) { + nsCOMPtr<nsIMsgDatabase> msgDB; + + nsresult rv = m_folderWatching->GetMsgDatabase(getter_AddRefs(msgDB)); + NS_ENSURE_SUCCESS(rv, rv); + bool match = false; + if (!m_searchSession) return NS_ERROR_NULL_POINTER; + + m_searchSession->AddScopeTerm(nsMsgSearchScope::offlineMail, + m_folderWatching); + rv = m_searchSession->MatchHdr(aNewHdr, msgDB, &match); + m_searchSession->ClearScopes(); + if (match) { + nsCOMPtr<nsIMsgDatabase> virtDatabase; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + + rv = m_virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(virtDatabase)); + NS_ENSURE_SUCCESS(rv, rv); + bool msgHdrIsRead; + uint32_t msgFlags; + aNewHdr->GetIsRead(&msgHdrIsRead); + aNewHdr->GetFlags(&msgFlags); + if (!msgHdrIsRead) dbFolderInfo->ChangeNumUnreadMessages(1); + if (msgFlags & nsMsgMessageFlags::New) { + int32_t numNewMessages; + m_virtualFolder->GetNumNewMessages(false, &numNewMessages); + m_virtualFolder->SetHasNewMessages(true); + m_virtualFolder->SetNumNewMessages(numNewMessages + 1); + } + nsCString searchUri; + m_virtualFolder->GetURI(searchUri); + msgDB->UpdateHdrInCache(searchUri, aNewHdr, true); + dbFolderInfo->ChangeNumMessages(1); + PostUpdateEvent(m_virtualFolder, virtDatabase); + } + return rv; +} + +NS_IMETHODIMP VirtualFolderChangeListener::OnParentChanged( + nsMsgKey aKeyChanged, nsMsgKey oldParent, nsMsgKey newParent, + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +NS_IMETHODIMP VirtualFolderChangeListener::OnAnnouncerGoingAway( + nsIDBChangeAnnouncer* instigator) { + nsCOMPtr<nsIMsgDatabase> msgDB = do_QueryInterface(instigator); + if (msgDB) msgDB->RemoveListener(this); + return NS_OK; +} + +NS_IMETHODIMP +VirtualFolderChangeListener::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) { + return NS_OK; +} + +NS_IMETHODIMP VirtualFolderChangeListener::OnReadChanged( + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +NS_IMETHODIMP VirtualFolderChangeListener::OnJunkScoreChanged( + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +nsresult VirtualFolderChangeListener::PostUpdateEvent( + nsIMsgFolder* virtualFolder, nsIMsgDatabase* virtDatabase) { + if (m_batchingEvents) return NS_OK; + m_batchingEvents = true; + nsCOMPtr<nsIRunnable> event = + new VFChangeListenerEvent(this, virtualFolder, virtDatabase); + return NS_DispatchToCurrentThread(event); +} + +void VirtualFolderChangeListener::ProcessUpdateEvent(nsIMsgFolder* virtFolder, + nsIMsgDatabase* virtDB) { + m_batchingEvents = false; + virtFolder->UpdateSummaryTotals(true); // force update from db. + virtDB->Commit(nsMsgDBCommitType::kLargeCommit); +} + +nsresult nsMsgAccountManager::GetVirtualFoldersFile(nsCOMPtr<nsIFile>& aFile) { + nsCOMPtr<nsIFile> profileDir; + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profileDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = profileDir->AppendNative("virtualFolders.dat"_ns); + if (NS_SUCCEEDED(rv)) aFile = profileDir; + return rv; +} + +NS_IMETHODIMP nsMsgAccountManager::LoadVirtualFolders() { + nsCOMPtr<nsIFile> file; + GetVirtualFoldersFile(file); + if (!file) return NS_ERROR_FAILURE; + + if (m_virtualFoldersLoaded) return NS_OK; + + m_loadingVirtualFolders = true; + + // Before loading virtual folders, ensure that all real folders exist. + // Some may not have been created yet, which would break virtual folders + // that depend on them. + nsTArray<RefPtr<nsIMsgIncomingServer>> allServers; + nsresult rv = GetAllServers(allServers); + NS_ENSURE_SUCCESS(rv, rv); + for (auto server : allServers) { + if (server) { + nsCOMPtr<nsIMsgFolder> rootFolder; + server->GetRootFolder(getter_AddRefs(rootFolder)); + if (rootFolder) { + nsTArray<RefPtr<nsIMsgFolder>> dummy; + rootFolder->GetSubFolders(dummy); + } + } + } + + if (!m_dbService) { + m_dbService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIFileInputStream> fileStream = + do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = fileStream->Init(file, PR_RDONLY, 0664, false); + nsCOMPtr<nsILineInputStream> lineInputStream(do_QueryInterface(fileStream)); + + bool isMore = true; + nsAutoCString buffer; + int32_t version = -1; + nsCOMPtr<nsIMsgFolder> virtualFolder; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + + while (isMore && NS_SUCCEEDED(lineInputStream->ReadLine(buffer, &isMore))) { + if (!buffer.IsEmpty()) { + if (version == -1) { + buffer.Cut(0, 8); + nsresult irv; + version = buffer.ToInteger(&irv); + continue; + } + if (StringBeginsWith(buffer, "uri="_ns)) { + buffer.Cut(0, 4); + dbFolderInfo = nullptr; + + rv = GetOrCreateFolder(buffer, getter_AddRefs(virtualFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + virtualFolder->SetFlag(nsMsgFolderFlags::Virtual); + + nsCOMPtr<nsIMsgFolder> grandParent; + nsCOMPtr<nsIMsgFolder> oldParent; + nsCOMPtr<nsIMsgFolder> parentFolder; + bool isServer; + // This loop handles creating virtual folders without an existing + // parent. + do { + // need to add the folder as a sub-folder of its parent. + int32_t lastSlash = buffer.RFindChar('/'); + if (lastSlash == kNotFound) break; + nsDependentCSubstring parentUri(buffer, 0, lastSlash); + // hold a reference so it won't get deleted before it's parented. + oldParent = parentFolder; + + rv = GetOrCreateFolder(parentUri, getter_AddRefs(parentFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString currentFolderNameStr; + nsAutoCString currentFolderNameCStr; + MsgUnescapeString( + nsCString(Substring(buffer, lastSlash + 1, buffer.Length())), 0, + currentFolderNameCStr); + CopyUTF8toUTF16(currentFolderNameCStr, currentFolderNameStr); + nsCOMPtr<nsIMsgFolder> childFolder; + nsCOMPtr<nsIMsgDatabase> db; + // force db to get created. + // XXX TODO: is this SetParent() right? Won't it screw up if virtual + // folder is nested >2 deep? Leave for now, but revisit when getting + // rid of dangling folders (BenC). + virtualFolder->SetParent(parentFolder); + rv = virtualFolder->GetMsgDatabase(getter_AddRefs(db)); + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) + m_dbService->CreateNewDB(virtualFolder, getter_AddRefs(db)); + if (db) + rv = db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + else + break; + + parentFolder->AddSubfolder(currentFolderNameStr, + getter_AddRefs(childFolder)); + if (childFolder) parentFolder->NotifyFolderAdded(childFolder); + // here we make sure if our parent is rooted - if not, we're + // going to loop and add our parent as a child of its grandparent + // and repeat until we get to the server, or a folder that + // has its parent set. + parentFolder->GetParent(getter_AddRefs(grandParent)); + parentFolder->GetIsServer(&isServer); + buffer.SetLength(lastSlash); + } while (!grandParent && !isServer); + } else if (dbFolderInfo && StringBeginsWith(buffer, "scope="_ns)) { + buffer.Cut(0, 6); + // if this is a cross folder virtual folder, we have a list of folders + // uris, and we have to add a pending listener for each of them. + if (!buffer.IsEmpty()) { + ParseAndVerifyVirtualFolderScope(buffer); + dbFolderInfo->SetCharProperty(kSearchFolderUriProp, buffer); + AddVFListenersForVF(virtualFolder, buffer); + } + } else if (dbFolderInfo && StringBeginsWith(buffer, "terms="_ns)) { + buffer.Cut(0, 6); + dbFolderInfo->SetCharProperty("searchStr", buffer); + } else if (dbFolderInfo && StringBeginsWith(buffer, "searchOnline="_ns)) { + buffer.Cut(0, 13); + dbFolderInfo->SetBooleanProperty("searchOnline", + buffer.EqualsLiteral("true")); + } else if (dbFolderInfo && + Substring(buffer, 0, SEARCH_FOLDER_FLAG_LEN + 1) + .Equals(SEARCH_FOLDER_FLAG "=")) { + buffer.Cut(0, SEARCH_FOLDER_FLAG_LEN + 1); + dbFolderInfo->SetCharProperty(SEARCH_FOLDER_FLAG, buffer); + } + } + } + + m_loadingVirtualFolders = false; + m_virtualFoldersLoaded = true; + return rv; +} + +NS_IMETHODIMP nsMsgAccountManager::SaveVirtualFolders() { + if (!m_virtualFoldersLoaded) return NS_OK; + + nsCOMPtr<nsIFile> file; + GetVirtualFoldersFile(file); + + // Open a buffered, safe output stream + nsCOMPtr<nsIOutputStream> outStream; + nsresult rv = MsgNewSafeBufferedFileOutputStream( + getter_AddRefs(outStream), file, PR_CREATE_FILE | PR_WRONLY | PR_TRUNCATE, + 0664); + NS_ENSURE_SUCCESS(rv, rv); + + WriteLineToOutputStream("version=", "1", outStream); + for (auto iter = m_incomingServers.Iter(); !iter.Done(); iter.Next()) { + nsCOMPtr<nsIMsgIncomingServer>& server = iter.Data(); + if (server) { + nsCOMPtr<nsIMsgFolder> rootFolder; + server->GetRootFolder(getter_AddRefs(rootFolder)); + if (rootFolder) { + nsTArray<RefPtr<nsIMsgFolder>> virtualFolders; + nsresult rv = rootFolder->GetFoldersWithFlags(nsMsgFolderFlags::Virtual, + virtualFolders); + if (NS_FAILED(rv)) { + continue; + } + for (auto msgFolder : virtualFolders) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + rv = msgFolder->GetDBFolderInfoAndDB( + getter_AddRefs(dbFolderInfo), + getter_AddRefs(db)); // force db to get created. + if (dbFolderInfo) { + nsCString srchFolderUri; + nsCString searchTerms; + nsCString regexScope; + nsCString vfFolderFlag; + bool searchOnline = false; + dbFolderInfo->GetBooleanProperty("searchOnline", false, + &searchOnline); + dbFolderInfo->GetCharProperty(kSearchFolderUriProp, srchFolderUri); + dbFolderInfo->GetCharProperty("searchStr", searchTerms); + // logically searchFolderFlag is an int, but since we want to + // write out a string, get it as a string. + dbFolderInfo->GetCharProperty(SEARCH_FOLDER_FLAG, vfFolderFlag); + nsCString uri; + msgFolder->GetURI(uri); + if (!srchFolderUri.IsEmpty() && !searchTerms.IsEmpty()) { + WriteLineToOutputStream("uri=", uri.get(), outStream); + if (!vfFolderFlag.IsEmpty()) + WriteLineToOutputStream(SEARCH_FOLDER_FLAG "=", + vfFolderFlag.get(), outStream); + WriteLineToOutputStream("scope=", srchFolderUri.get(), outStream); + WriteLineToOutputStream("terms=", searchTerms.get(), outStream); + WriteLineToOutputStream( + "searchOnline=", searchOnline ? "true" : "false", outStream); + } + } + } + } + } + } + + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(outStream, &rv); + NS_ASSERTION(safeStream, "expected a safe output stream!"); + if (safeStream) { + rv = safeStream->Finish(); + if (NS_FAILED(rv)) { + NS_WARNING("failed to save personal dictionary file! possible data loss"); + } + } + return rv; +} + +nsresult nsMsgAccountManager::WriteLineToOutputStream( + const char* prefix, const char* line, nsIOutputStream* outputStream) { + uint32_t writeCount; + outputStream->Write(prefix, strlen(prefix), &writeCount); + outputStream->Write(line, strlen(line), &writeCount); + outputStream->Write("\n", 1, &writeCount); + return NS_OK; +} + +/** + * Parse the '|' separated folder uri string into individual folders, verify + * that the folders are real. If we were to add things like wildcards, we + * could implement the expansion into real folders here. + * + * @param buffer On input, list of folder uri's, on output, verified list. + */ +void nsMsgAccountManager::ParseAndVerifyVirtualFolderScope(nsCString& buffer) { + if (buffer.Equals("*")) { + // This is a special virtual folder that searches all folders in all + // accounts. Folders are chosen by the front end at search time. + return; + } + nsCString verifiedFolders; + nsTArray<nsCString> folderUris; + ParseString(buffer, '|', folderUris); + nsCOMPtr<nsIMsgIncomingServer> server; + nsCOMPtr<nsIMsgFolder> parent; + + for (uint32_t i = 0; i < folderUris.Length(); i++) { + nsCOMPtr<nsIMsgFolder> realFolder; + nsresult rv = GetOrCreateFolder(folderUris[i], getter_AddRefs(realFolder)); + if (!NS_SUCCEEDED(rv)) { + continue; + } + realFolder->GetParent(getter_AddRefs(parent)); + if (!parent) continue; + realFolder->GetServer(getter_AddRefs(server)); + if (!server) continue; + if (!verifiedFolders.IsEmpty()) verifiedFolders.Append('|'); + verifiedFolders.Append(folderUris[i]); + } + buffer.Assign(verifiedFolders); +} + +// This conveniently works to add a single folder as well. +nsresult nsMsgAccountManager::AddVFListenersForVF( + nsIMsgFolder* virtualFolder, const nsCString& srchFolderUris) { + if (srchFolderUris.Equals("*")) { + return NS_OK; + } + + nsresult rv; + if (!m_dbService) { + m_dbService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Avoid any possible duplicate listeners for this virtual folder. + RemoveVFListenerForVF(virtualFolder, nullptr); + + nsTArray<nsCString> folderUris; + ParseString(srchFolderUris, '|', folderUris); + + for (uint32_t i = 0; i < folderUris.Length(); i++) { + nsCOMPtr<nsIMsgFolder> realFolder; + rv = GetOrCreateFolder(folderUris[i], getter_AddRefs(realFolder)); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr<VirtualFolderChangeListener> dbListener = + new VirtualFolderChangeListener(); + NS_ENSURE_TRUE(dbListener, NS_ERROR_OUT_OF_MEMORY); + dbListener->m_virtualFolder = virtualFolder; + dbListener->m_folderWatching = realFolder; + if (NS_FAILED(dbListener->Init())) { + dbListener = nullptr; + continue; + } + m_virtualFolderListeners.AppendElement(dbListener); + m_dbService->RegisterPendingListener(realFolder, dbListener); + } + if (!m_virtualFolders.Contains(virtualFolder)) { + m_virtualFolders.AppendElement(virtualFolder); + } + return NS_OK; +} + +// This is called if a folder that's part of the scope of a saved search +// has gone away. +nsresult nsMsgAccountManager::RemoveVFListenerForVF(nsIMsgFolder* virtualFolder, + nsIMsgFolder* folder) { + nsresult rv; + if (!m_dbService) { + m_dbService = do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter( + m_virtualFolderListeners); + RefPtr<VirtualFolderChangeListener> listener; + + while (iter.HasMore()) { + listener = iter.GetNext(); + if (listener->m_virtualFolder == virtualFolder && + (!folder || listener->m_folderWatching == folder)) { + m_dbService->UnregisterPendingListener(listener); + m_virtualFolderListeners.RemoveElement(listener); + if (folder) { + break; + } + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::GetAllFolders( + nsTArray<RefPtr<nsIMsgFolder>>& aAllFolders) { + aAllFolders.Clear(); + nsTArray<RefPtr<nsIMsgIncomingServer>> allServers; + nsresult rv = GetAllServers(allServers); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto server : allServers) { + if (server) { + nsCOMPtr<nsIMsgFolder> rootFolder; + server->GetRootFolder(getter_AddRefs(rootFolder)); + if (rootFolder) { + nsTArray<RefPtr<nsIMsgFolder>> descendents; + rootFolder->GetDescendants(descendents); + aAllFolders.AppendElements(descendents); + } + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::OnFolderAdded(nsIMsgFolder* parent, + nsIMsgFolder* folder) { + if (!parent) { + // This method gets called for folders that aren't connected to anything, + // such as a junk folder that appears when an IMAP account is created. We + // don't want these added to the virtual folder. + return NS_OK; + } + + uint32_t folderFlags; + folder->GetFlags(&folderFlags); + + bool addToSmartFolders = false; + folder->IsSpecialFolder(nsMsgFolderFlags::Inbox | + nsMsgFolderFlags::Templates | + nsMsgFolderFlags::Trash | + nsMsgFolderFlags::Drafts | nsMsgFolderFlags::Junk, + false, &addToSmartFolders); + // For Sent/Archives/Trash, we treat sub-folders of those folders as + // "special", and want to add them the smart folders search scope. + // So we check if this is a sub-folder of one of those special folders + // and set the corresponding folderFlag if so. + if (!addToSmartFolders) { + bool isSpecial = false; + folder->IsSpecialFolder(nsMsgFolderFlags::SentMail, true, &isSpecial); + if (isSpecial) { + addToSmartFolders = true; + folderFlags |= nsMsgFolderFlags::SentMail; + } + folder->IsSpecialFolder(nsMsgFolderFlags::Archive, true, &isSpecial); + if (isSpecial) { + addToSmartFolders = true; + folderFlags |= nsMsgFolderFlags::Archive; + } + folder->IsSpecialFolder(nsMsgFolderFlags::Trash, true, &isSpecial); + if (isSpecial) { + addToSmartFolders = true; + folderFlags |= nsMsgFolderFlags::Trash; + } + } + nsresult rv = NS_OK; + // if this is a special folder, check if we have a saved search over + // folders with this flag, and if so, add this folder to the scope. + if (addToSmartFolders) { + // quick way to enumerate the saved searches. + for (nsCOMPtr<nsIMsgFolder> virtualFolder : m_virtualFolders) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(db)); + if (dbFolderInfo) { + uint32_t vfFolderFlag; + dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag); + // found a saved search over folders w/ the same flag as the new folder. + if (vfFolderFlag & folderFlags) { + nsCString searchURI; + dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI); + + // "normalize" searchURI so we can search for |folderURI|. + if (!searchURI.IsEmpty()) { + searchURI.Insert('|', 0); + searchURI.Append('|'); + } + nsCString folderURI; + folder->GetURI(folderURI); + folderURI.Insert('|', 0); + folderURI.Append('|'); + + int32_t index = searchURI.Find(folderURI); + if (index == kNotFound) { + searchURI.Cut(0, 1); + folderURI.Cut(0, 1); + folderURI.SetLength(folderURI.Length() - 1); + searchURI.Append(folderURI); + dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI); + nsCOMPtr<nsIObserverService> obs = + mozilla::services::GetObserverService(); + obs->NotifyObservers(virtualFolder, "search-folders-changed", + nullptr); + } + + // Add sub-folders to smart folder. + nsTArray<RefPtr<nsIMsgFolder>> allDescendants; + rv = folder->GetDescendants(allDescendants); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> parentFolder; + for (auto subFolder : allDescendants) { + subFolder->GetParent(getter_AddRefs(parentFolder)); + OnFolderAdded(parentFolder, subFolder); + } + } + } + } + } + + // Find any virtual folders that search `parent`, and add `folder` to them. + if (!(folderFlags & nsMsgFolderFlags::Virtual)) { + nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter( + m_virtualFolderListeners); + RefPtr<VirtualFolderChangeListener> listener; + + while (iter.HasMore()) { + listener = iter.GetNext(); + if (listener->m_folderWatching == parent) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + listener->m_virtualFolder->GetDBFolderInfoAndDB( + getter_AddRefs(dbFolderInfo), getter_AddRefs(db)); + + uint32_t vfFolderFlag; + dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag); + if (addToSmartFolders && vfFolderFlag && + !(vfFolderFlag & nsMsgFolderFlags::Trash)) { + // Don't add folders of one type to the unified folder of another + // type, unless it's the Trash unified folder. + continue; + } + nsCString searchURI; + dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI); + + // "normalize" searchURI so we can search for |folderURI|. + if (!searchURI.IsEmpty()) { + searchURI.Insert('|', 0); + searchURI.Append('|'); + } + nsCString folderURI; + folder->GetURI(folderURI); + + int32_t index = searchURI.Find(folderURI); + if (index == kNotFound) { + searchURI.Cut(0, 1); + searchURI.Append(folderURI); + dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI); + nsCOMPtr<nsIObserverService> obs = + mozilla::services::GetObserverService(); + obs->NotifyObservers(listener->m_virtualFolder, + "search-folders-changed", nullptr); + } + } + } + } + + // need to make sure this isn't happening during loading of virtualfolders.dat + if (folderFlags & nsMsgFolderFlags::Virtual && !m_loadingVirtualFolders) { + // When a new virtual folder is added, need to create a db Listener for it. + nsCOMPtr<nsIMsgDatabase> virtDatabase; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + rv = folder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(virtDatabase)); + NS_ENSURE_SUCCESS(rv, rv); + nsCString srchFolderUri; + dbFolderInfo->GetCharProperty(kSearchFolderUriProp, srchFolderUri); + AddVFListenersForVF(folder, srchFolderUri); + rv = SaveVirtualFolders(); + } + return rv; +} + +NS_IMETHODIMP nsMsgAccountManager::OnMessageAdded(nsIMsgFolder* parent, + nsIMsgDBHdr* msg) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::OnFolderRemoved(nsIMsgFolder* parentFolder, + nsIMsgFolder* folder) { + nsresult rv = NS_OK; + uint32_t folderFlags; + folder->GetFlags(&folderFlags); + // if we removed a VF, flush VF list to disk. + if (folderFlags & nsMsgFolderFlags::Virtual) { + RemoveVFListenerForVF(folder, nullptr); + m_virtualFolders.RemoveElement(folder); + rv = SaveVirtualFolders(); + // clear flags on deleted folder if it's a virtual folder, so that creating + // a new folder with the same name doesn't cause confusion. + folder->SetFlags(0); + return rv; + } + // need to update the saved searches to check for a few things: + // 1. Folder removed was in the scope of a saved search - if so, remove the + // uri from the scope of the saved search. + // 2. If the scope is now empty, remove the saved search. + + // build a "normalized" uri that we can do a find on. + nsCString removedFolderURI; + folder->GetURI(removedFolderURI); + removedFolderURI.Insert('|', 0); + removedFolderURI.Append('|'); + + // Enumerate the saved searches. + nsTObserverArray<RefPtr<VirtualFolderChangeListener>>::ForwardIterator iter( + m_virtualFolderListeners); + RefPtr<VirtualFolderChangeListener> listener; + + while (iter.HasMore()) { + listener = iter.GetNext(); + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + nsCOMPtr<nsIMsgFolder> savedSearch = listener->m_virtualFolder; + savedSearch->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(db)); + if (dbFolderInfo) { + nsCString searchURI; + dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI); + // "normalize" searchURI so we can search for |folderURI|. + searchURI.Insert('|', 0); + searchURI.Append('|'); + int32_t index = searchURI.Find(removedFolderURI); + if (index != kNotFound) { + RemoveVFListenerForVF(savedSearch, folder); + + // remove |folderURI + searchURI.Cut(index, removedFolderURI.Length() - 1); + // remove last '|' we added + searchURI.SetLength(searchURI.Length() - 1); + + uint32_t vfFolderFlag; + dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag); + + // If saved search is empty now, delete it. But not if it's a smart + // folder. + if (searchURI.IsEmpty() && !vfFolderFlag) { + db = nullptr; + dbFolderInfo = nullptr; + + nsCOMPtr<nsIMsgFolder> parent; + rv = savedSearch->GetParent(getter_AddRefs(parent)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!parent) continue; + parent->PropagateDelete(savedSearch, true); + } else { + if (!searchURI.IsEmpty()) { + // Remove leading '|' we added (or one after |folderURI, if first + // URI). + searchURI.Cut(0, 1); + } + dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI); + nsCOMPtr<nsIObserverService> obs = + mozilla::services::GetObserverService(); + obs->NotifyObservers(savedSearch, "search-folders-changed", nullptr); + } + } + } + } + + return rv; +} + +NS_IMETHODIMP nsMsgAccountManager::OnMessageRemoved(nsIMsgFolder* parent, + nsIMsgDBHdr* msg) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::OnFolderPropertyChanged( + nsIMsgFolder* folder, const nsACString& property, + const nsACString& oldValue, const nsACString& newValue) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgAccountManager::OnFolderIntPropertyChanged(nsIMsgFolder* aFolder, + const nsACString& aProperty, + int64_t oldValue, + int64_t newValue) { + if (aProperty.Equals(kFolderFlag)) { + if (newValue & nsMsgFolderFlags::Virtual) { + // This is a virtual folder, let's get out of here. + return NS_OK; + } + uint32_t smartFlagsChanged = + (oldValue ^ newValue) & + (nsMsgFolderFlags::SpecialUse & ~nsMsgFolderFlags::Queue); + if (smartFlagsChanged) { + if (smartFlagsChanged & newValue) { + // if the smart folder flag was set, calling OnFolderAdded will + // do the right thing. + nsCOMPtr<nsIMsgFolder> parent; + aFolder->GetParent(getter_AddRefs(parent)); + nsresult rv = OnFolderAdded(parent, aFolder); + NS_ENSURE_SUCCESS(rv, rv); + + // This folder has one of the smart folder flags. + // Remove it from any other smart folders it might've been included in + // because of the flags of its ancestors. + RemoveFolderFromSmartFolder( + aFolder, (nsMsgFolderFlags::SpecialUse & ~nsMsgFolderFlags::Queue) & + ~newValue); + return NS_OK; + } + RemoveFolderFromSmartFolder(aFolder, smartFlagsChanged); + + nsTArray<RefPtr<nsIMsgFolder>> allDescendants; + nsresult rv = aFolder->GetDescendants(allDescendants); + NS_ENSURE_SUCCESS(rv, rv); + for (auto subFolder : allDescendants) { + RemoveFolderFromSmartFolder(subFolder, smartFlagsChanged); + } + } + } + return NS_OK; +} + +nsresult nsMsgAccountManager::RemoveFolderFromSmartFolder( + nsIMsgFolder* aFolder, uint32_t flagsChanged) { + nsCString removedFolderURI; + aFolder->GetURI(removedFolderURI); + removedFolderURI.Insert('|', 0); + removedFolderURI.Append('|'); + uint32_t flags; + aFolder->GetFlags(&flags); + NS_ASSERTION(!(flags & flagsChanged), "smart folder flag should not be set"); + // Flag was removed. Look for smart folder based on that flag, + // and remove this folder from its scope. + for (nsCOMPtr<nsIMsgFolder> virtualFolder : m_virtualFolders) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + virtualFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(db)); + if (dbFolderInfo) { + uint32_t vfFolderFlag; + dbFolderInfo->GetUint32Property("searchFolderFlag", 0, &vfFolderFlag); + // found a smart folder over the removed flag + if (vfFolderFlag & flagsChanged) { + nsCString searchURI; + dbFolderInfo->GetCharProperty(kSearchFolderUriProp, searchURI); + // "normalize" searchURI so we can search for |folderURI|. + searchURI.Insert('|', 0); + searchURI.Append('|'); + int32_t index = searchURI.Find(removedFolderURI); + if (index != kNotFound) { + RemoveVFListenerForVF(virtualFolder, aFolder); + + // remove |folderURI + searchURI.Cut(index, removedFolderURI.Length() - 1); + // remove last '|' we added + searchURI.SetLength(searchURI.Length() - 1); + + // remove leading '|' we added (or one after |folderURI, if first URI) + searchURI.Cut(0, 1); + dbFolderInfo->SetCharProperty(kSearchFolderUriProp, searchURI); + nsCOMPtr<nsIObserverService> obs = + mozilla::services::GetObserverService(); + obs->NotifyObservers(virtualFolder, "search-folders-changed", + nullptr); + } + } + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgAccountManager::OnFolderBoolPropertyChanged( + nsIMsgFolder* folder, const nsACString& property, bool oldValue, + bool newValue) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgAccountManager::OnFolderUnicharPropertyChanged( + nsIMsgFolder* folder, const nsACString& property, const nsAString& oldValue, + const nsAString& newValue) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgAccountManager::OnFolderPropertyFlagChanged( + nsIMsgDBHdr* msg, const nsACString& property, uint32_t oldFlag, + uint32_t newFlag) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgAccountManager::OnFolderEvent(nsIMsgFolder* aFolder, + const nsACString& aEvent) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgAccountManager::FolderUriForPath(nsIFile* aLocalPath, + nsACString& aMailboxUri) { + NS_ENSURE_ARG_POINTER(aLocalPath); + bool equals; + if (m_lastPathLookedUp && + NS_SUCCEEDED(aLocalPath->Equals(m_lastPathLookedUp, &equals)) && equals) { + aMailboxUri = m_lastFolderURIForPath; + return NS_OK; + } + nsTArray<RefPtr<nsIMsgFolder>> folders; + nsresult rv = GetAllFolders(folders); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto folder : folders) { + nsCOMPtr<nsIFile> folderPath; + rv = folder->GetFilePath(getter_AddRefs(folderPath)); + NS_ENSURE_SUCCESS(rv, rv); + + // Check if we're equal + rv = folderPath->Equals(aLocalPath, &equals); + NS_ENSURE_SUCCESS(rv, rv); + + if (equals) { + rv = folder->GetURI(aMailboxUri); + m_lastFolderURIForPath = aMailboxUri; + aLocalPath->Clone(getter_AddRefs(m_lastPathLookedUp)); + return rv; + } + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMsgAccountManager::GetSortOrder(nsIMsgIncomingServer* aServer, + int32_t* aSortOrder) { + NS_ENSURE_ARG_POINTER(aServer); + NS_ENSURE_ARG_POINTER(aSortOrder); + + // If the passed in server is the default, return its sort order as 0 + // regardless of its server sort order. + + nsCOMPtr<nsIMsgAccount> defaultAccount; + nsresult rv = GetDefaultAccount(getter_AddRefs(defaultAccount)); + if (NS_SUCCEEDED(rv) && defaultAccount) { + nsCOMPtr<nsIMsgIncomingServer> defaultServer; + rv = m_defaultAccount->GetIncomingServer(getter_AddRefs(defaultServer)); + if (NS_SUCCEEDED(rv) && (aServer == defaultServer)) { + *aSortOrder = 0; + return NS_OK; + } + // It is OK if there is no default account. + } + + // This function returns the sort order by querying the server object for its + // sort order value and then incrementing it by the position of the server in + // the accounts list. This ensures that even when several accounts have the + // same sort order value, the returned value is not the same and keeps + // their relative order in the account list when and unstable sort is run + // on the returned sort order values. + int32_t sortOrder; + int32_t serverIndex; + + rv = aServer->GetSortOrder(&sortOrder); + if (NS_SUCCEEDED(rv)) rv = FindServerIndex(aServer, &serverIndex); + + if (NS_FAILED(rv)) { + *aSortOrder = 999999999; + } else { + *aSortOrder = sortOrder + serverIndex; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgAccountManager::ReorderAccounts(const nsTArray<nsCString>& newAccounts) { + nsTArray<nsCString> allNewAccounts = newAccounts.Clone(); + + // Add all hidden accounts to the list of new accounts. + nsresult rv; + for (auto account : m_accounts) { + nsCString key; + account->GetKey(key); + nsCOMPtr<nsIMsgIncomingServer> server; + rv = account->GetIncomingServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) { + bool hidden = false; + rv = server->GetHidden(&hidden); + if (NS_SUCCEEDED(rv) && hidden && !allNewAccounts.Contains(key)) { + allNewAccounts.AppendElement(key); + } + } + } + + // Check that the new account list contains all the existing accounts, + // just in a different order. + if (allNewAccounts.Length() != m_accounts.Length()) + return NS_ERROR_INVALID_ARG; + + for (uint32_t i = 0; i < m_accounts.Length(); i++) { + nsCString accountKey; + m_accounts[i]->GetKey(accountKey); + if (!allNewAccounts.Contains(accountKey)) return NS_ERROR_INVALID_ARG; + } + + // In-place swap the elements in m_accounts to the order defined in + // newAccounts. + for (uint32_t i = 0; i < allNewAccounts.Length(); i++) { + nsCString newKey = allNewAccounts[i]; + for (uint32_t j = i; j < m_accounts.Length(); j++) { + nsCString oldKey; + m_accounts[j]->GetKey(oldKey); + if (newKey.Equals(oldKey)) { + if (i != j) std::swap(m_accounts[i], m_accounts[j]); + break; + } + } + } + + return OutputAccountsPref(); +} diff --git a/comm/mailnews/base/src/nsMsgAccountManager.h b/comm/mailnews/base/src/nsMsgAccountManager.h new file mode 100644 index 0000000000..9dc1e45da3 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgAccountManager.h @@ -0,0 +1,211 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * This Original Code has been modified by IBM Corporation. Modifications made + * by IBM described herein are Copyright (c) International Business Machines + * Corporation, 2000. Modifications to Mozilla code or documentation identified + * per MPL Section 3.3 + * + * Date Modified by Description of modification + * 04/20/2000 IBM Corp. OS/2 VisualAge build. + */ + +#include "nscore.h" +#include "nsIMsgAccountManager.h" +#include "nsCOMPtr.h" +#include "nsISmtpServer.h" +#include "nsIPrefBranch.h" +#include "nsIMsgFolderCache.h" +#include "nsIMsgFolder.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nsIUrlListener.h" +#include "nsCOMArray.h" +#include "nsIMsgSearchSession.h" +#include "nsInterfaceHashtable.h" +#include "nsIMsgDatabase.h" +#include "nsIDBChangeListener.h" +#include "nsTObserverArray.h" + +class VirtualFolderChangeListener final : public nsIDBChangeListener { + public: + VirtualFolderChangeListener(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIDBCHANGELISTENER + + nsresult Init(); + /** + * Posts an event to update the summary totals and commit the db. + * We post the event to avoid committing each time we're called + * in a synchronous loop. + */ + nsresult PostUpdateEvent(nsIMsgFolder* folder, nsIMsgDatabase* db); + /// Handles event posted to event queue to batch notifications. + void ProcessUpdateEvent(nsIMsgFolder* folder, nsIMsgDatabase* db); + + void DecrementNewMsgCount(); + + // folder we're listening to db changes on behalf of. + nsCOMPtr<nsIMsgFolder> m_virtualFolder; + // folder whose db we're listening to. + nsCOMPtr<nsIMsgFolder> m_folderWatching; + nsCOMPtr<nsIMsgSearchSession> m_searchSession; + bool m_searchOnMsgStatus; + bool m_batchingEvents; + + private: + ~VirtualFolderChangeListener() {} +}; + +class nsMsgAccountManager : public nsIMsgAccountManager, + public nsIObserver, + public nsSupportsWeakReference, + public nsIFolderListener { + public: + nsMsgAccountManager(); + + NS_DECL_THREADSAFE_ISUPPORTS + + /* nsIMsgAccountManager methods */ + + NS_DECL_NSIMSGACCOUNTMANAGER + NS_DECL_NSIOBSERVER + NS_DECL_NSIFOLDERLISTENER + + nsresult Init(); + nsresult Shutdown(); + void LogoutOfServer(nsIMsgIncomingServer* aServer); + + private: + virtual ~nsMsgAccountManager(); + + bool m_accountsLoaded; + nsCOMPtr<nsIMsgFolderCache> m_msgFolderCache; + nsTArray<nsCOMPtr<nsIMsgAccount>> m_accounts; + nsInterfaceHashtable<nsCStringHashKey, nsIMsgIdentity> m_identities; + nsInterfaceHashtable<nsCStringHashKey, nsIMsgIncomingServer> + m_incomingServers; + nsCOMPtr<nsIMsgAccount> m_defaultAccount; + nsCOMArray<nsIIncomingServerListener> m_incomingServerListeners; + nsTObserverArray<RefPtr<VirtualFolderChangeListener>> + m_virtualFolderListeners; + nsTArray<nsCOMPtr<nsIMsgFolder>> m_virtualFolders; + nsCOMPtr<nsIMsgFolder> m_folderDoingEmptyTrash; + nsCOMPtr<nsIMsgFolder> m_folderDoingCleanupInbox; + bool m_emptyTrashInProgress; + bool m_cleanupInboxInProgress; + + nsCString mAccountKeyList; + + // These are static because the account manager may go away during + // shutdown, and get recreated. + static bool m_haveShutdown; + static bool m_shutdownInProgress; + + bool m_userAuthenticated; + bool m_loadingVirtualFolders; + bool m_virtualFoldersLoaded; + + /* we call FindServer() a lot. so cache the last server found */ + nsCOMPtr<nsIMsgIncomingServer> m_lastFindServerResult; + nsCString m_lastFindServerHostName; + nsCString m_lastFindServerUserName; + int32_t m_lastFindServerPort; + nsCString m_lastFindServerType; + + void SetLastServerFound(nsIMsgIncomingServer* server, + const nsACString& hostname, + const nsACString& username, const int32_t port, + const nsACString& type); + + // Cache the results of the last call to FolderUriFromDirInProfile + nsCOMPtr<nsIFile> m_lastPathLookedUp; + nsCString m_lastFolderURIForPath; + + /* internal creation routines - updates m_identities and m_incomingServers */ + nsresult createKeyedAccount(const nsCString& key, bool forcePositionToEnd, + nsIMsgAccount** _retval); + nsresult createKeyedServer(const nsACString& key, const nsACString& username, + const nsACString& password, const nsACString& type, + nsIMsgIncomingServer** _retval); + + nsresult createKeyedIdentity(const nsACString& key, nsIMsgIdentity** _retval); + + nsresult GetLocalFoldersPrettyName(nsString& localFoldersName); + + /** + * Check if the given account can be the set as the default account. + */ + nsresult CheckDefaultAccount(nsIMsgAccount* aAccount, bool& aCanBeDefault); + + /** + * Find a new account that can serve as default. + */ + nsresult AutosetDefaultAccount(); + + // sets the pref for the default server + nsresult setDefaultAccountPref(nsIMsgAccount* aDefaultAccount); + + // Write out the accounts pref from the m_accounts list of accounts. + nsresult OutputAccountsPref(); + + // fires notifications to the appropriate root folders + nsresult notifyDefaultServerChange(nsIMsgAccount* aOldAccount, + nsIMsgAccount* aNewAccount); + + // + // account enumerators + // ("element" is always an account) + // + + // find the servers that correspond to the given identity + static bool findServersForIdentity(nsISupports* element, void* aData); + + void findAccountByServerKey(const nsCString& aKey, nsIMsgAccount** aResult); + + // + // server enumerators + // ("element" is always a server) + // + + nsresult findServerInternal(const nsACString& username, + const nsACString& hostname, + const nsACString& type, int32_t port, + nsIMsgIncomingServer** aResult); + + // handle virtual folders + static nsresult GetVirtualFoldersFile(nsCOMPtr<nsIFile>& file); + static nsresult WriteLineToOutputStream(const char* prefix, const char* line, + nsIOutputStream* outputStream); + void ParseAndVerifyVirtualFolderScope(nsCString& buffer); + nsresult AddVFListenersForVF(nsIMsgFolder* virtualFolder, + const nsCString& srchFolderUris); + + nsresult RemoveVFListenerForVF(nsIMsgFolder* virtualFolder, + nsIMsgFolder* folder); + + nsresult RemoveFolderFromSmartFolder(nsIMsgFolder* aFolder, + uint32_t flagsChanged); + + nsresult SetSendLaterUriPref(nsIMsgIncomingServer* server); + + nsCOMPtr<nsIPrefBranch> m_prefs; + nsCOMPtr<nsIMsgDBService> m_dbService; + + // + // root folder listener stuff + // + + // this array is for folder listeners that are supposed to be listening + // on the root folders. + // When a new server is created, all of the the folder listeners + // should be added to the new server + // When a new listener is added, it should be added to all root folders. + // similar for when servers are deleted or listeners removed + nsTObserverArray<nsCOMPtr<nsIFolderListener>> mFolderListeners; + + void removeListenersFromFolder(nsIMsgFolder* aFolder); +}; diff --git a/comm/mailnews/base/src/nsMsgBiffManager.cpp b/comm/mailnews/base/src/nsMsgBiffManager.cpp new file mode 100644 index 0000000000..9d990a30bd --- /dev/null +++ b/comm/mailnews/base/src/nsMsgBiffManager.cpp @@ -0,0 +1,341 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgBiffManager.h" +#include "nsIMsgAccountManager.h" +#include "nsStatusBarBiffManager.h" +#include "nsCOMArray.h" +#include "mozilla/Logging.h" +#include "nspr.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIObserverService.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMsgUtils.h" +#include "nsITimer.h" +#include "mozilla/Services.h" + +#define PREF_BIFF_JITTER "mail.biff.add_interval_jitter" + +#define NS_STATUSBARBIFFMANAGER_CID \ + { \ + 0x7f9a9fb0, 0x4161, 0x11d4, { \ + 0x98, 0x76, 0x00, 0xc0, 0x4f, 0xa0, 0xd2, 0xa6 \ + } \ + } +static NS_DEFINE_CID(kStatusBarBiffManagerCID, NS_STATUSBARBIFFMANAGER_CID); + +static mozilla::LazyLogModule MsgBiffLogModule("MsgBiff"); + +NS_IMPL_ISUPPORTS(nsMsgBiffManager, nsIMsgBiffManager, + nsIIncomingServerListener, nsIObserver, + nsISupportsWeakReference) + +void OnBiffTimer(nsITimer* timer, void* aBiffManager) { + nsMsgBiffManager* biffManager = (nsMsgBiffManager*)aBiffManager; + biffManager->PerformBiff(); +} + +nsMsgBiffManager::nsMsgBiffManager() { + mHaveShutdown = false; + mInited = false; +} + +nsMsgBiffManager::~nsMsgBiffManager() { + if (mBiffTimer) mBiffTimer->Cancel(); + + if (!mHaveShutdown) Shutdown(); + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, "wake_notification"); + observerService->RemoveObserver(this, "sleep_notification"); + } +} + +NS_IMETHODIMP nsMsgBiffManager::Init() { + if (mInited) return NS_OK; + + mInited = true; + nsresult rv; + + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + if (NS_SUCCEEDED(rv)) accountManager->AddIncomingServerListener(this); + + // in turbo mode on profile change we don't need to do anything below this + if (mHaveShutdown) { + mHaveShutdown = false; + return NS_OK; + } + + // Ensure status bar biff service has started + nsCOMPtr<nsIFolderListener> statusBarBiffService = + do_GetService(kStatusBarBiffManagerCID, &rv); + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "sleep_notification", true); + observerService->AddObserver(this, "wake_notification", true); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgBiffManager::Shutdown() { + if (mBiffTimer) { + mBiffTimer->Cancel(); + mBiffTimer = nullptr; + } + + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + if (NS_SUCCEEDED(rv)) accountManager->RemoveIncomingServerListener(this); + + mHaveShutdown = true; + mInited = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgBiffManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* someData) { + if (!strcmp(aTopic, "sleep_notification") && mBiffTimer) { + mBiffTimer->Cancel(); + mBiffTimer = nullptr; + } else if (!strcmp(aTopic, "wake_notification")) { + // wait 10 seconds after waking up to start biffing again. + nsresult rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(mBiffTimer), OnBiffTimer, (void*)this, 10000, + nsITimer::TYPE_ONE_SHOT, "nsMsgBiffManager::OnBiffTimer", nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("Could not start mBiffTimer timer"); + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgBiffManager::AddServerBiff(nsIMsgIncomingServer* server) { + NS_ENSURE_ARG_POINTER(server); + + int32_t biffMinutes; + + nsresult rv = server->GetBiffMinutes(&biffMinutes); + NS_ENSURE_SUCCESS(rv, rv); + + // Don't add if biffMinutes isn't > 0 + if (biffMinutes > 0) { + int32_t serverIndex = FindServer(server); + // Only add it if it hasn't been added already. + if (serverIndex == -1) { + nsBiffEntry biffEntry; + biffEntry.server = server; + rv = SetNextBiffTime(biffEntry, PR_Now()); + NS_ENSURE_SUCCESS(rv, rv); + + AddBiffEntry(biffEntry); + SetupNextBiff(); + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgBiffManager::RemoveServerBiff(nsIMsgIncomingServer* server) { + int32_t pos = FindServer(server); + if (pos != -1) mBiffArray.RemoveElementAt(pos); + + // Should probably reset biff time if this was the server that gets biffed + // next. + return NS_OK; +} + +NS_IMETHODIMP nsMsgBiffManager::ForceBiff(nsIMsgIncomingServer* server) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgBiffManager::ForceBiffAll() { return NS_OK; } + +NS_IMETHODIMP nsMsgBiffManager::OnServerLoaded(nsIMsgIncomingServer* server) { + NS_ENSURE_ARG_POINTER(server); + + bool doBiff = false; + nsresult rv = server->GetDoBiff(&doBiff); + + if (NS_SUCCEEDED(rv) && doBiff) rv = AddServerBiff(server); + + return rv; +} + +NS_IMETHODIMP nsMsgBiffManager::OnServerUnloaded(nsIMsgIncomingServer* server) { + return RemoveServerBiff(server); +} + +NS_IMETHODIMP nsMsgBiffManager::OnServerChanged(nsIMsgIncomingServer* server) { + // nothing required. If the hostname or username changed + // the next time biff fires, we'll ping the right server + return NS_OK; +} + +int32_t nsMsgBiffManager::FindServer(nsIMsgIncomingServer* server) { + uint32_t count = mBiffArray.Length(); + for (uint32_t i = 0; i < count; i++) { + if (server == mBiffArray[i].server.get()) return i; + } + return -1; +} + +nsresult nsMsgBiffManager::AddBiffEntry(nsBiffEntry& biffEntry) { + uint32_t i; + uint32_t count = mBiffArray.Length(); + for (i = 0; i < count; i++) { + if (biffEntry.nextBiffTime < mBiffArray[i].nextBiffTime) break; + } + MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info, + ("inserting biff entry at %d", i)); + mBiffArray.InsertElementAt(i, biffEntry); + return NS_OK; +} + +nsresult nsMsgBiffManager::SetNextBiffTime(nsBiffEntry& biffEntry, + PRTime currentTime) { + nsIMsgIncomingServer* server = biffEntry.server; + NS_ENSURE_TRUE(server, NS_ERROR_FAILURE); + + int32_t biffInterval; + nsresult rv = server->GetBiffMinutes(&biffInterval); + NS_ENSURE_SUCCESS(rv, rv); + + // Add biffInterval, converted in microseconds, to current time. + // Force 64-bit multiplication. + PRTime chosenTimeInterval = biffInterval * 60000000LL; + biffEntry.nextBiffTime = currentTime + chosenTimeInterval; + + // Check if we should jitter. + nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID); + if (prefs) { + bool shouldUseBiffJitter = false; + prefs->GetBoolPref(PREF_BIFF_JITTER, &shouldUseBiffJitter); + if (shouldUseBiffJitter) { + // Calculate a jitter of +/-5% on chosenTimeInterval + // - minimum 1 second (to avoid a modulo with 0) + // - maximum 30 seconds (to avoid problems when biffInterval is very + // large) + int64_t jitter = (int64_t)(0.05 * (int64_t)chosenTimeInterval); + jitter = + std::max<int64_t>(1000000LL, std::min<int64_t>(jitter, 30000000LL)); + jitter = ((rand() % 2) ? 1 : -1) * (rand() % jitter); + + biffEntry.nextBiffTime += jitter; + } + } + + return NS_OK; +} + +nsresult nsMsgBiffManager::SetupNextBiff() { + if (mBiffArray.Length() > 0) { + // Get the next biff entry + const nsBiffEntry& biffEntry = mBiffArray[0]; + PRTime currentTime = PR_Now(); + int64_t biffDelay; + int64_t ms(1000); + + if (currentTime > biffEntry.nextBiffTime) { + // Let's wait 30 seconds before firing biff again + biffDelay = 30 * PR_USEC_PER_SEC; + } else + biffDelay = biffEntry.nextBiffTime - currentTime; + + // Convert biffDelay into milliseconds + int64_t timeInMS = biffDelay / ms; + uint32_t timeInMSUint32 = (uint32_t)timeInMS; + + // Can't currently reset a timer when it's in the process of + // calling Notify. So, just release the timer here and create a new one. + if (mBiffTimer) mBiffTimer->Cancel(); + + MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info, + ("setting %d timer", timeInMSUint32)); + + nsresult rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(mBiffTimer), OnBiffTimer, (void*)this, timeInMSUint32, + nsITimer::TYPE_ONE_SHOT, "nsMsgBiffManager::OnBiffTimer", nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("Could not start mBiffTimer timer"); + } + } + return NS_OK; +} + +// This is the function that does a biff on all of the servers whose time it is +// to biff. +nsresult nsMsgBiffManager::PerformBiff() { + PRTime currentTime = PR_Now(); + nsCOMArray<nsIMsgFolder> targetFolders; + MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info, ("performing biffs")); + + uint32_t count = mBiffArray.Length(); + for (uint32_t i = 0; i < count; i++) { + // Take a copy of the entry rather than the a reference so that we can + // remove and add if necessary, but keep the references and memory alive. + nsBiffEntry current = mBiffArray[i]; + if (current.nextBiffTime < currentTime) { + bool serverBusy = false; + bool serverRequiresPassword = true; + bool passwordPromptRequired; + + current.server->GetPasswordPromptRequired(&passwordPromptRequired); + current.server->GetServerBusy(&serverBusy); + current.server->GetServerRequiresPasswordForBiff(&serverRequiresPassword); + // find the dest folder we're actually downloading to... + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + current.server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + int32_t targetFolderIndex = targetFolders.IndexOfObject(rootMsgFolder); + if (targetFolderIndex == kNotFound) + targetFolders.AppendObject(rootMsgFolder); + + // so if we need to be authenticated to biff, check that we are + // (since we don't want to prompt the user for password UI) + // and make sure the server isn't already in the middle of downloading + // new messages + if (!serverBusy && (!serverRequiresPassword || !passwordPromptRequired) && + targetFolderIndex == kNotFound) { + nsCString serverKey; + current.server->GetKey(serverKey); + nsresult rv = current.server->PerformBiff(nullptr); + MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info, + ("biffing server %s rv = %" PRIx32, serverKey.get(), + static_cast<uint32_t>(rv))); + } else { + MOZ_LOG(MsgBiffLogModule, mozilla::LogLevel::Info, + ("not biffing server serverBusy = %d requirespassword = %d " + "password prompt required = %d targetFolderIndex = %d", + serverBusy, serverRequiresPassword, passwordPromptRequired, + targetFolderIndex)); + } + // if we didn't do this server because the destination server was already + // being biffed into, leave this server in the biff array so it will fire + // next. + if (targetFolderIndex == kNotFound) { + mBiffArray.RemoveElementAt(i); + i--; // Because we removed it we need to look at the one that just + // moved up. + SetNextBiffTime(current, currentTime); + AddBiffEntry(current); + } +#ifdef DEBUG_David_Bienvenu + else + printf("dest account performing biff\n"); +#endif + } else + // since we're in biff order, there's no reason to keep checking + break; + } + SetupNextBiff(); + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgBiffManager.h b/comm/mailnews/base/src/nsMsgBiffManager.h new file mode 100644 index 0000000000..e334429d71 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgBiffManager.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NSMSGBIFFMANAGER_H +#define NSMSGBIFFMANAGER_H + +#include "msgCore.h" +#include "nsIMsgBiffManager.h" +#include "nsITimer.h" +#include "nsTArray.h" +#include "nsCOMPtr.h" +#include "nsIIncomingServerListener.h" +#include "nsWeakReference.h" +#include "nsIObserver.h" + +typedef struct { + nsCOMPtr<nsIMsgIncomingServer> server; + PRTime nextBiffTime; +} nsBiffEntry; + +class nsMsgBiffManager : public nsIMsgBiffManager, + public nsIIncomingServerListener, + public nsIObserver, + public nsSupportsWeakReference { + public: + nsMsgBiffManager(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGBIFFMANAGER + NS_DECL_NSIINCOMINGSERVERLISTENER + NS_DECL_NSIOBSERVER + + nsresult PerformBiff(); + + protected: + virtual ~nsMsgBiffManager(); + + int32_t FindServer(nsIMsgIncomingServer* server); + nsresult SetNextBiffTime(nsBiffEntry& biffEntry, PRTime currentTime); + nsresult SetupNextBiff(); + nsresult AddBiffEntry(nsBiffEntry& biffEntry); + + protected: + nsCOMPtr<nsITimer> mBiffTimer; + nsTArray<nsBiffEntry> mBiffArray; + bool mHaveShutdown; + bool mInited; +}; + +#endif // NSMSGBIFFMANAGER_H diff --git a/comm/mailnews/base/src/nsMsgCompressIStream.cpp b/comm/mailnews/base/src/nsMsgCompressIStream.cpp new file mode 100644 index 0000000000..a0dd6b7776 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgCompressIStream.cpp @@ -0,0 +1,203 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgCompressIStream.h" +#include "prio.h" +#include "prmem.h" +#include <algorithm> + +#define BUFFER_SIZE 16384 + +nsMsgCompressIStream::nsMsgCompressIStream() + : m_dataptr(nullptr), m_dataleft(0), m_inflateAgain(false) {} + +nsMsgCompressIStream::~nsMsgCompressIStream() { Close(); } + +NS_IMPL_ISUPPORTS(nsMsgCompressIStream, nsIInputStream, nsIAsyncInputStream) + +nsresult nsMsgCompressIStream::InitInputStream(nsIInputStream* rawStream) { + // protect against repeat calls + if (m_iStream) return NS_ERROR_UNEXPECTED; + + // allocate some memory for buffering + m_zbuf = mozilla::MakeUnique<char[]>(BUFFER_SIZE); + if (!m_zbuf) return NS_ERROR_OUT_OF_MEMORY; + + // allocate some memory for buffering + m_databuf = mozilla::MakeUnique<char[]>(BUFFER_SIZE); + if (!m_databuf) return NS_ERROR_OUT_OF_MEMORY; + + // set up zlib object + m_zstream.zalloc = Z_NULL; + m_zstream.zfree = Z_NULL; + m_zstream.opaque = Z_NULL; + + // http://zlib.net/manual.html is rather silent on the topic, but + // perl's Compress::Raw::Zlib manual says: + // -WindowBits + // To compress an RFC 1951 data stream, set WindowBits to -MAX_WBITS. + if (inflateInit2(&m_zstream, -MAX_WBITS) != Z_OK) return NS_ERROR_FAILURE; + + m_iStream = rawStream; + + return NS_OK; +} + +nsresult nsMsgCompressIStream::DoInflation() { + // if there's something in the input buffer of the zstream, process it. + m_zstream.next_out = (Bytef*)m_databuf.get(); + m_zstream.avail_out = BUFFER_SIZE; + int zr = inflate(&m_zstream, Z_SYNC_FLUSH); + + // inflate() should normally be called until it returns + // Z_STREAM_END or an error, and Z_BUF_ERROR just means + // unable to progress any further (possible if we filled + // an output buffer exactly) + if (zr == Z_BUF_ERROR || zr == Z_STREAM_END) zr = Z_OK; + + // otherwise it's an error + if (zr != Z_OK) return NS_ERROR_FAILURE; + + // http://www.zlib.net/manual.html says: + // If inflate returns Z_OK and with zero avail_out, it must be called + // again after making room in the output buffer because there might be + // more output pending. + m_inflateAgain = m_zstream.avail_out ? false : true; + + // set the pointer to the start of the buffer, and the count to how + // based on how many bytes are left unconsumed. + m_dataptr = m_databuf.get(); + m_dataleft = BUFFER_SIZE - m_zstream.avail_out; + + return NS_OK; +} + +/* void close (); */ +NS_IMETHODIMP nsMsgCompressIStream::Close() { return CloseWithStatus(NS_OK); } + +NS_IMETHODIMP nsMsgCompressIStream::CloseWithStatus(nsresult reason) { + nsresult rv = NS_OK; + + if (m_iStream) { + // pass the status through to our wrapped stream + nsCOMPtr<nsIAsyncInputStream> asyncInputStream = + do_QueryInterface(m_iStream); + if (asyncInputStream) rv = asyncInputStream->CloseWithStatus(reason); + + // tidy up + m_iStream = nullptr; + inflateEnd(&m_zstream); + } + + // clean up all the buffers + m_zbuf = nullptr; + m_databuf = nullptr; + m_dataptr = nullptr; + m_dataleft = 0; + + return rv; +} + +/* unsigned long long available (); */ +NS_IMETHODIMP nsMsgCompressIStream::Available(uint64_t* aResult) { + if (!m_iStream) return NS_BASE_STREAM_CLOSED; + + // check if there's anything still in flight + if (!m_dataleft && m_inflateAgain) { + nsresult rv = DoInflation(); + NS_ENSURE_SUCCESS(rv, rv); + } + + // we'll be returning this many to the next read, guaranteed + if (m_dataleft) { + *aResult = m_dataleft; + return NS_OK; + } + + // this value isn't accurate, but will give a good true/false + // indication for idle purposes, and next read will fill + // m_dataleft, so we'll have an accurate count for the next call. + return m_iStream->Available(aResult); +} + +/* [noscript] unsigned long read (in charPtr aBuf, in unsigned long aCount); */ +NS_IMETHODIMP nsMsgCompressIStream::Read(char* aBuf, uint32_t aCount, + uint32_t* aResult) { + if (!m_iStream) { + *aResult = 0; + return NS_OK; + } + + // There are two stages of buffering: + // * m_zbuf contains the compressed data from the remote server + // * m_databuf contains the uncompressed raw bytes for consumption + // by the local client. + // + // Each buffer will only be filled when the following buffers + // have been entirely consumed. + // + // m_dataptr and m_dataleft are respectively a pointer to the + // unconsumed portion of m_databuf and the number of bytes + // of uncompressed data remaining in m_databuf. + // + // both buffers have a maximum size of BUFFER_SIZE, so it is + // possible that multiple inflate passes will be required to + // consume all of m_zbuf. + while (!m_dataleft) { + // get some more data if we don't already have any + if (!m_inflateAgain) { + uint32_t bytesRead; + nsresult rv = + m_iStream->Read(m_zbuf.get(), (uint32_t)BUFFER_SIZE, &bytesRead); + NS_ENSURE_SUCCESS(rv, rv); + if (!bytesRead) return NS_BASE_STREAM_CLOSED; + m_zstream.next_in = (Bytef*)m_zbuf.get(); + m_zstream.avail_in = bytesRead; + } + + nsresult rv = DoInflation(); + NS_ENSURE_SUCCESS(rv, rv); + } + + *aResult = std::min(m_dataleft, aCount); + + if (*aResult) { + memcpy(aBuf, m_dataptr, *aResult); + m_dataptr += *aResult; + m_dataleft -= *aResult; + } + + return NS_OK; +} + +/* [noscript] unsigned long readSegments (in nsWriteSegmentFun aWriter, in + * voidPtr aClosure, in unsigned long aCount); */ +NS_IMETHODIMP nsMsgCompressIStream::ReadSegments(nsWriteSegmentFun aWriter, + void* aClosure, + uint32_t aCount, + uint32_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgCompressIStream::AsyncWait(nsIInputStreamCallback* callback, + uint32_t flags, uint32_t amount, + nsIEventTarget* target) { + if (!m_iStream) return NS_BASE_STREAM_CLOSED; + + nsCOMPtr<nsIAsyncInputStream> asyncInputStream = do_QueryInterface(m_iStream); + if (asyncInputStream) + return asyncInputStream->AsyncWait(callback, flags, amount, target); + + return NS_OK; +} + +/* boolean isNonBlocking (); */ +NS_IMETHODIMP nsMsgCompressIStream::IsNonBlocking(bool* aNonBlocking) { + *aNonBlocking = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompressIStream::StreamStatus() { + return m_iStream->StreamStatus(); +} diff --git a/comm/mailnews/base/src/nsMsgCompressIStream.h b/comm/mailnews/base/src/nsMsgCompressIStream.h new file mode 100644 index 0000000000..642d35f9fa --- /dev/null +++ b/comm/mailnews/base/src/nsMsgCompressIStream.h @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsIAsyncInputStream.h" +#include "nsIInputStream.h" +#include "nsCOMPtr.h" +#include "mozilla/UniquePtr.h" +#include "zlib.h" + +class nsMsgCompressIStream final : public nsIAsyncInputStream { + public: + nsMsgCompressIStream(); + + NS_DECL_THREADSAFE_ISUPPORTS + + NS_DECL_NSIINPUTSTREAM + NS_DECL_NSIASYNCINPUTSTREAM + + nsresult InitInputStream(nsIInputStream* rawStream); + + protected: + ~nsMsgCompressIStream(); + nsresult DoInflation(); + nsCOMPtr<nsIInputStream> m_iStream; + mozilla::UniquePtr<char[]> m_zbuf; + mozilla::UniquePtr<char[]> m_databuf; + char* m_dataptr; + uint32_t m_dataleft; + bool m_inflateAgain; + z_stream m_zstream; +}; diff --git a/comm/mailnews/base/src/nsMsgCompressOStream.cpp b/comm/mailnews/base/src/nsMsgCompressOStream.cpp new file mode 100644 index 0000000000..fd490274ca --- /dev/null +++ b/comm/mailnews/base/src/nsMsgCompressOStream.cpp @@ -0,0 +1,128 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgCompressOStream.h" +#include "prio.h" +#include "prmem.h" + +#define BUFFER_SIZE 16384 + +nsMsgCompressOStream::nsMsgCompressOStream() : m_zbuf(nullptr) {} + +nsMsgCompressOStream::~nsMsgCompressOStream() { Close(); } + +NS_IMPL_ISUPPORTS(nsMsgCompressOStream, nsIOutputStream) + +nsresult nsMsgCompressOStream::InitOutputStream(nsIOutputStream* rawStream) { + // protect against repeat calls + if (m_oStream) return NS_ERROR_UNEXPECTED; + + // allocate some memory for a buffer + m_zbuf = mozilla::MakeUnique<char[]>(BUFFER_SIZE); + if (!m_zbuf) return NS_ERROR_OUT_OF_MEMORY; + + // set up the zlib object + m_zstream.zalloc = Z_NULL; + m_zstream.zfree = Z_NULL; + m_zstream.opaque = Z_NULL; + + // http://zlib.net/manual.html is rather silent on the topic, but + // perl's Compress::Raw::Zlib manual says: + // -WindowBits [...] + // To compress an RFC 1951 data stream, set WindowBits to -MAX_WBITS. + if (deflateInit2(&m_zstream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, + MAX_MEM_LEVEL, Z_DEFAULT_STRATEGY) != Z_OK) + return NS_ERROR_FAILURE; + + m_oStream = rawStream; + + return NS_OK; +} + +/* void close (); */ +NS_IMETHODIMP nsMsgCompressOStream::Close() { + if (m_oStream) { + m_oStream = nullptr; + deflateEnd(&m_zstream); + } + m_zbuf = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompressOStream::Write(const char* buf, uint32_t count, uint32_t* result) { + if (!m_oStream) return NS_BASE_STREAM_CLOSED; + + m_zstream.next_in = (Bytef*)buf; + m_zstream.avail_in = count; + + // keep looping until the buffer doesn't get filled + do { + m_zstream.next_out = (Bytef*)m_zbuf.get(); + m_zstream.avail_out = BUFFER_SIZE; + // Using "Z_SYNC_FLUSH" may cause excess flushes if the calling + // code does a lot of small writes. An option with the IMAP + // protocol is to check the buffer for "\n" at the end, but + // in the interests of keeping this generic, don't optimise + // yet. An alternative is to require ->Flush always, but that + // is likely to break callers. + int zr = deflate(&m_zstream, Z_SYNC_FLUSH); + if (zr == Z_STREAM_END || zr == Z_BUF_ERROR) + zr = Z_OK; // not an error for our purposes + if (zr != Z_OK) return NS_ERROR_FAILURE; + + uint32_t out_size = BUFFER_SIZE - m_zstream.avail_out; + const char* out_buf = m_zbuf.get(); + + // push everything in the buffer before repeating + while (out_size) { + uint32_t out_result; + nsresult rv = m_oStream->Write(out_buf, out_size, &out_result); + NS_ENSURE_SUCCESS(rv, rv); + if (!out_result) return NS_BASE_STREAM_CLOSED; + out_size -= out_result; + out_buf += out_result; + } + + // http://www.zlib.net/manual.html says: + // If deflate returns with avail_out == 0, this function must be + // called again with the same value of the flush parameter and + // more output space (updated avail_out), until the flush is + // complete (deflate returns with non-zero avail_out). + } while (!m_zstream.avail_out); + + *result = count; + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgCompressOStream::Flush(void) { + if (!m_oStream) return NS_BASE_STREAM_CLOSED; + + return m_oStream->Flush(); +} + +NS_IMETHODIMP +nsMsgCompressOStream::WriteFrom(nsIInputStream* inStr, uint32_t count, + uint32_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgCompressOStream::WriteSegments(nsReadSegmentFun reader, void* closure, + uint32_t count, uint32_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* boolean isNonBlocking (); */ +NS_IMETHODIMP nsMsgCompressOStream::IsNonBlocking(bool* aNonBlocking) { + *aNonBlocking = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgCompressOStream::StreamStatus() { + return m_oStream->StreamStatus(); +} diff --git a/comm/mailnews/base/src/nsMsgCompressOStream.h b/comm/mailnews/base/src/nsMsgCompressOStream.h new file mode 100644 index 0000000000..96d38ad9c0 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgCompressOStream.h @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsIOutputStream.h" +#include "nsCOMPtr.h" +#include "mozilla/UniquePtr.h" +#include "zlib.h" + +class nsMsgCompressOStream final : public nsIOutputStream { + public: + nsMsgCompressOStream(); + + NS_DECL_THREADSAFE_ISUPPORTS + + NS_DECL_NSIOUTPUTSTREAM + + nsresult InitOutputStream(nsIOutputStream* rawStream); + + protected: + ~nsMsgCompressOStream(); + nsCOMPtr<nsIOutputStream> m_oStream; + mozilla::UniquePtr<char[]> m_zbuf; + z_stream m_zstream; +}; diff --git a/comm/mailnews/base/src/nsMsgContentPolicy.cpp b/comm/mailnews/base/src/nsMsgContentPolicy.cpp new file mode 100644 index 0000000000..6494732cad --- /dev/null +++ b/comm/mailnews/base/src/nsMsgContentPolicy.cpp @@ -0,0 +1,928 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgContentPolicy.h" +#include "nsIMsgMailSession.h" +#include "nsIPermissionManager.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIAbManager.h" +#include "nsIAbDirectory.h" +#include "nsIAbCard.h" +#include "nsIMsgWindow.h" +#include "nsIMsgHdr.h" +#include "nsIEncryptedSMIMEURIsSrvc.h" +#include "nsNetUtil.h" +#include "nsIMsgComposeService.h" +#include "nsIDocShellTreeItem.h" +#include "nsIWebNavigation.h" +#include "nsContentPolicyUtils.h" +#include "nsFrameLoaderOwner.h" +#include "nsFrameLoader.h" +#include "nsMsgUtils.h" +#include "nsThreadUtils.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "mozilla/dom/HTMLImageElement.h" +#include "nsINntpUrl.h" +#include "nsILoadInfo.h" +#include "nsSandboxFlags.h" +#include "nsQueryObject.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "mozilla/SyncRunnable.h" +#include "nsIObserverService.h" + +static const char kBlockRemoteImages[] = + "mailnews.message_display.disable_remote_image"; +static const char kTrustedDomains[] = "mail.trusteddomains"; + +using namespace mozilla; +using namespace mozilla::mailnews; + +// Per message headder flags to keep track of whether the user is allowing +// remote content for a particular message. if you change or add more values to +// these constants, be sure to modify the corresponding definitions in +// mailWindowOverlay.js +#define kNoRemoteContentPolicy 0 +#define kBlockRemoteContent 1 +#define kAllowRemoteContent 2 + +NS_IMPL_ISUPPORTS(nsMsgContentPolicy, nsIContentPolicy, nsIMsgContentPolicy, + nsIObserver, nsISupportsWeakReference) + +nsMsgContentPolicy::nsMsgContentPolicy() { mBlockRemoteImages = true; } + +nsMsgContentPolicy::~nsMsgContentPolicy() { + // hey, we are going away...clean up after ourself....unregister our observer + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefInternal = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv)) { + prefInternal->RemoveObserver(kBlockRemoteImages, this); + } +} + +nsresult nsMsgContentPolicy::Init() { + nsresult rv; + + // register ourself as an observer on the mail preference to block remote + // images + nsCOMPtr<nsIPrefBranch> prefInternal = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + prefInternal->AddObserver(kBlockRemoteImages, this, true); + + prefInternal->GetCharPref(kTrustedDomains, mTrustedMailDomains); + prefInternal->GetBoolPref(kBlockRemoteImages, &mBlockRemoteImages); + + // Grab a handle on the PermissionManager service for managing allowed remote + // content senders. + mPermissionManager = do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +/** + * @returns true if the sender referenced by aMsgHdr is explicitly allowed to + * load remote images according to the PermissionManager + */ +bool nsMsgContentPolicy::ShouldAcceptRemoteContentForSender( + nsIMsgDBHdr* aMsgHdr) { + if (!aMsgHdr) return false; + + // extract the e-mail address from the msg hdr + nsCString author; + nsresult rv = aMsgHdr->GetAuthor(getter_Copies(author)); + NS_ENSURE_SUCCESS(rv, false); + + nsCString emailAddress; + ExtractEmail(EncodedHeader(author), emailAddress); + if (emailAddress.IsEmpty()) return false; + + nsCOMPtr<nsIIOService> ios = + do_GetService("@mozilla.org/network/io-service;1", &rv); + NS_ENSURE_SUCCESS(rv, false); + nsCOMPtr<nsIURI> mailURI; + emailAddress.InsertLiteral("chrome://messenger/content/email=", 0); + rv = ios->NewURI(emailAddress, nullptr, nullptr, getter_AddRefs(mailURI)); + NS_ENSURE_SUCCESS(rv, false); + + // check with permission manager + uint32_t permission = 0; + mozilla::OriginAttributes attrs; + RefPtr<mozilla::BasePrincipal> principal = + mozilla::BasePrincipal::CreateContentPrincipal(mailURI, attrs); + rv = mPermissionManager->TestPermissionFromPrincipal(principal, "image"_ns, + &permission); + NS_ENSURE_SUCCESS(rv, false); + + // Only return true if the permission manager has an explicit allow + return (permission == nsIPermissionManager::ALLOW_ACTION); +} + +/** + * Extract the host name from aContentLocation, and look it up in our list + * of trusted domains. + */ +bool nsMsgContentPolicy::IsTrustedDomain(nsIURI* aContentLocation) { + bool trustedDomain = false; + // get the host name of the server hosting the remote image + nsAutoCString host; + nsresult rv = aContentLocation->GetHost(host); + + if (NS_SUCCEEDED(rv) && !mTrustedMailDomains.IsEmpty()) + trustedDomain = MsgHostDomainIsTrusted(host, mTrustedMailDomains); + + return trustedDomain; +} + +NS_IMETHODIMP +nsMsgContentPolicy::ShouldLoad(nsIURI* aContentLocation, nsILoadInfo* aLoadInfo, + const nsACString& aMimeGuess, + int16_t* aDecision) { + nsresult rv = NS_OK; + ExtContentPolicyType aContentType = aLoadInfo->GetExternalContentPolicyType(); + nsCOMPtr<nsISupports> aRequestingContext; + if (aContentType == ExtContentPolicy::TYPE_DOCUMENT) + aRequestingContext = aLoadInfo->ContextForTopLevelLoad(); + else + aRequestingContext = aLoadInfo->LoadingNode(); + nsCOMPtr<nsIPrincipal> loadingPrincipal = aLoadInfo->GetLoadingPrincipal(); + nsCOMPtr<nsIURI> aRequestingLocation; + if (loadingPrincipal) { + BasePrincipal::Cast(loadingPrincipal) + ->GetURI(getter_AddRefs(aRequestingLocation)); + } + + // The default decision at the start of the function is to accept the load. + // Once we have checked the content type and the requesting location, then + // we switch it to reject. + // + // Be very careful about returning error codes - if this method returns an + // NS_ERROR_*, any decision made here will be ignored, and the document could + // be accepted when we don't want it to be. + // + // In most cases if an error occurs, its something we didn't expect so we + // should be rejecting the document anyway. + *aDecision = nsIContentPolicy::ACCEPT; + + NS_ENSURE_ARG_POINTER(aContentLocation); + +#ifdef DEBUG_MsgContentPolicy + fprintf(stderr, "aContentType: %d\naContentLocation = %s\n", aContentType, + aContentLocation->GetSpecOrDefault().get()); + fprintf(stderr, "aRequestingContext is %s\n", + aRequestingContext ? "not null" : "null"); +#endif + +#ifndef MOZ_THUNDERBIRD + // Go find out if we are dealing with mailnews. Anything else + // isn't our concern and we accept content. + nsCOMPtr<nsIDocShell> rootDocShell; + rv = GetRootDocShellForContext(aRequestingContext, + getter_AddRefs(rootDocShell)); + NS_ENSURE_SUCCESS(rv, rv); + + // We only want to deal with mailnews + if (rootDocShell->GetAppType() != nsIDocShell::APP_TYPE_MAIL) return NS_OK; +#endif + + switch (aContentType) { + // Plugins (nsIContentPolicy::TYPE_OBJECT) are blocked on document load. + case ExtContentPolicy::TYPE_DOCUMENT: + // At this point, we have no intention of supporting a different JS + // setting on a subdocument, so we don't worry about TYPE_SUBDOCUMENT + // here. + + if (NS_IsMainThread()) { + rv = SetDisableItemsOnMailNewsUrlDocshells(aContentLocation, aLoadInfo); + } else { + auto SetDisabling = [&, location = nsCOMPtr(aContentLocation), + loadInfo = nsCOMPtr(aLoadInfo)]() -> auto { + rv = SetDisableItemsOnMailNewsUrlDocshells(location, loadInfo); + }; + nsCOMPtr<nsIRunnable> task = + NS_NewRunnableFunction("SetDisabling", SetDisabling); + mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), task); + } + // if something went wrong during the tweaking, reject this content + if (NS_FAILED(rv)) { + NS_WARNING("Failed to set disable items on docShells"); + *aDecision = nsIContentPolicy::REJECT_TYPE; + return NS_OK; + } + break; + + case ExtContentPolicy::TYPE_CSP_REPORT: + // We cannot block CSP reports. + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + break; + + default: + break; + } + + // NOTE: Not using NS_ENSURE_ARG_POINTER because this is a legitimate case + // that can happen. Also keep in mind that the default policy used for a + // failure code is ACCEPT. + if (!aRequestingLocation) return NS_ERROR_INVALID_POINTER; + +#ifdef DEBUG_MsgContentPolicy + fprintf(stderr, "aRequestingLocation = %s\n", + aRequestingLocation->GetSpecOrDefault().get()); +#endif + + // If the requesting location is safe, accept the content location request. + if (IsSafeRequestingLocation(aRequestingLocation)) return rv; + + // Now default to reject so early returns via NS_ENSURE_SUCCESS + // cause content to be rejected. + *aDecision = nsIContentPolicy::REJECT_REQUEST; + + // We want to establish the following: + // \--------\ requester | | | + // content \------------\ | | | + // requested \| mail message | news message | http(s)/data etc. + // -------------------------+---------------+--------------+------------------ + // mail message content | load if same | don't load | don't load + // mailbox, imap, JsAccount | message (1) | (2) | (3) + // -------------------------+---------------+--------------+------------------ + // news message | don't load (4)| load (5) | load (6) + // -------------------------+---------------+--------------+------------------ + // http(s)/data, etc. | (default) | (default) | (default) + // -------------------------+---------------+--------------+------------------ + nsCOMPtr<nsIMsgMessageUrl> contentURL(do_QueryInterface(aContentLocation)); + if (contentURL) { + nsCOMPtr<nsINntpUrl> contentNntpURL(do_QueryInterface(aContentLocation)); + if (!contentNntpURL) { + // Mail message (mailbox, imap or JsAccount) content requested, for + // example a message part, like an image: To load mail message content the + // requester must have the same "normalized" principal. This is basically + // a "same origin" test, it protects against cross-loading of mail message + // content from other mail or news messages. + nsCOMPtr<nsIMsgMessageUrl> requestURL( + do_QueryInterface(aRequestingLocation)); + // If the request URL is not also a message URL, then we don't accept. + if (requestURL) { + nsCString contentPrincipalSpec, requestPrincipalSpec; + nsresult rv1 = contentURL->GetNormalizedSpec(contentPrincipalSpec); + nsresult rv2 = requestURL->GetNormalizedSpec(requestPrincipalSpec); + if (NS_SUCCEEDED(rv1) && NS_SUCCEEDED(rv2) && + contentPrincipalSpec.Equals(requestPrincipalSpec)) + *aDecision = nsIContentPolicy::ACCEPT; // (1) + } + return NS_OK; // (2) and (3) + } + + // News message content requested. Don't accept request coming + // from a mail message since it would access the news server. + nsCOMPtr<nsIMsgMessageUrl> requestURL( + do_QueryInterface(aRequestingLocation)); + if (requestURL) { + nsCOMPtr<nsINntpUrl> requestNntpURL( + do_QueryInterface(aRequestingLocation)); + if (!requestNntpURL) return NS_OK; // (4) + } + *aDecision = nsIContentPolicy::ACCEPT; // (5) and (6) + return NS_OK; + } + + // If exposed protocol not covered by the test above or protocol that has been + // specifically exposed by an add-on, or is a chrome url, then allow the load. + if (IsExposedProtocol(aContentLocation)) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + + // Never load unexposed protocols except for web protocols and file. + // Protocols like ftp are always blocked. + if (ShouldBlockUnexposedProtocol(aContentLocation)) return NS_OK; + + // Mailnews URIs are not loaded in child processes, so I think that beyond + // here, if we're in a child process, the decision will always be accept. + // + // targetContext->Canonical does not work in a child process, so we can't + // really move on anyway. + if (!XRE_IsParentProcess()) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + + // Find out the URI that originally initiated the set of requests for this + // context. + RefPtr<mozilla::dom::BrowsingContext> targetContext; + rv = aLoadInfo->GetTargetBrowsingContext(getter_AddRefs(targetContext)); + NS_ENSURE_SUCCESS(rv, NS_OK); + + if (!targetContext) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + + nsCOMPtr<nsIURI> originatorLocation; + dom::CanonicalBrowsingContext* cbc = targetContext->Canonical(); + if (cbc) { + dom::WindowGlobalParent* wgp = cbc->GetCurrentWindowGlobal(); + if (wgp) { + originatorLocation = wgp->GetDocumentURI(); + } + } + if (!originatorLocation) { + return NS_OK; + } + +#ifdef DEBUG_MsgContentPolicy + fprintf(stderr, "originatorLocation = %s\n", + originatorLocation->GetSpecOrDefault().get()); +#endif + + // Don't load remote content for encrypted messages. + nsCOMPtr<nsIEncryptedSMIMEURIsService> encryptedURIService = do_GetService( + "@mozilla.org/messenger-smime/smime-encrypted-uris-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + bool isEncrypted; + rv = encryptedURIService->IsEncrypted(aRequestingLocation->GetSpecOrDefault(), + &isEncrypted); + NS_ENSURE_SUCCESS(rv, rv); + if (isEncrypted) { + *aDecision = nsIContentPolicy::REJECT_REQUEST; + NotifyContentWasBlocked(targetContext->Id(), aContentLocation); + return NS_OK; + } + + // If we are allowing all remote content... + if (!mBlockRemoteImages) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + + uint32_t permission; + mozilla::OriginAttributes attrs; + RefPtr<mozilla::BasePrincipal> principal = + mozilla::BasePrincipal::CreateContentPrincipal(aContentLocation, attrs); + mPermissionManager->TestPermissionFromPrincipal(principal, "image"_ns, + &permission); + switch (permission) { + case nsIPermissionManager::UNKNOWN_ACTION: { + // No exception was found for this location. + break; + } + case nsIPermissionManager::ALLOW_ACTION: { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + case nsIPermissionManager::DENY_ACTION: { + *aDecision = nsIContentPolicy::REJECT_REQUEST; + return NS_OK; + } + } + + // Handle compose windows separately from mail. Work out if we're in a compose + // window or not. + nsCOMPtr<nsIMsgCompose> msgCompose = + GetMsgComposeForBrowsingContext(targetContext); + if (msgCompose) { + ComposeShouldLoad(msgCompose, aRequestingContext, originatorLocation, + aContentLocation, aDecision); + return NS_OK; + } + + // Allow content when using a remote page. + bool isHttp; + bool isHttps; + rv = originatorLocation->SchemeIs("http", &isHttp); + nsresult rv2 = originatorLocation->SchemeIs("https", &isHttps); + if (NS_SUCCEEDED(rv) && NS_SUCCEEDED(rv2) && (isHttp || isHttps)) { + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; + } + + // The default decision is still to reject. + ShouldAcceptContentForPotentialMsg(targetContext->Id(), aRequestingLocation, + aContentLocation, aDecision); + return NS_OK; +} + +/** + * Determines if the requesting location is a safe one, i.e. its under the + * app/user's control - so file, about, chrome etc. + */ +bool nsMsgContentPolicy::IsSafeRequestingLocation(nsIURI* aRequestingLocation) { + if (!aRequestingLocation) return false; + + // If aRequestingLocation is one of chrome, resource, file or view-source, + // allow aContentLocation to load. + bool isChrome; + bool isRes; + bool isFile; + bool isViewSource; + + nsresult rv = aRequestingLocation->SchemeIs("chrome", &isChrome); + NS_ENSURE_SUCCESS(rv, false); + rv = aRequestingLocation->SchemeIs("resource", &isRes); + NS_ENSURE_SUCCESS(rv, false); + rv = aRequestingLocation->SchemeIs("file", &isFile); + NS_ENSURE_SUCCESS(rv, false); + rv = aRequestingLocation->SchemeIs("view-source", &isViewSource); + NS_ENSURE_SUCCESS(rv, false); + + if (isChrome || isRes || isFile || isViewSource) return true; + + // Only allow about: to load anything if the requesting location is not the + // special about:blank one. + bool isAbout; + rv = aRequestingLocation->SchemeIs("about", &isAbout); + NS_ENSURE_SUCCESS(rv, false); + + if (!isAbout) return false; + + nsCString fullSpec; + rv = aRequestingLocation->GetSpec(fullSpec); + NS_ENSURE_SUCCESS(rv, false); + + return !fullSpec.EqualsLiteral("about:blank"); +} + +/** + * Determines if the content location is a scheme that we're willing to expose + * for unlimited loading of content. + */ +bool nsMsgContentPolicy::IsExposedProtocol(nsIURI* aContentLocation) { + nsAutoCString contentScheme; + nsresult rv = aContentLocation->GetScheme(contentScheme); + NS_ENSURE_SUCCESS(rv, false); + + // Check some exposed protocols. Not all protocols in the list of + // network.protocol-handler.expose.* prefs in all-thunderbird.js are + // admitted purely based on their scheme. + // news, snews, nntp, imap and mailbox are checked before the call + // to this function by matching content location and requesting location. + if (contentScheme.LowerCaseEqualsLiteral("mailto")) return true; + + if (contentScheme.LowerCaseEqualsLiteral("about")) { + // We want to allow about pages to load content freely. But not about:blank. + nsAutoCString fullSpec; + rv = aContentLocation->GetSpec(fullSpec); + NS_ENSURE_SUCCESS(rv, false); + if (fullSpec.EqualsLiteral("about:blank")) { + return false; + } + return true; + } + + // check if customized exposed scheme + if (mCustomExposedProtocols.Contains(contentScheme)) return true; + + bool isChrome; + rv = aContentLocation->SchemeIs("chrome", &isChrome); + NS_ENSURE_SUCCESS(rv, false); + + bool isRes; + rv = aContentLocation->SchemeIs("resource", &isRes); + NS_ENSURE_SUCCESS(rv, false); + + bool isData; + rv = aContentLocation->SchemeIs("data", &isData); + NS_ENSURE_SUCCESS(rv, false); + + bool isMozExtension; + rv = aContentLocation->SchemeIs("moz-extension", &isMozExtension); + NS_ENSURE_SUCCESS(rv, false); + + return isChrome || isRes || isData || isMozExtension; +} + +/** + * We block most unexposed protocols that access remote data + * - apart from web protocols, and file. + */ +bool nsMsgContentPolicy::ShouldBlockUnexposedProtocol( + nsIURI* aContentLocation) { + // Error condition - we must return true so that we block. + + // about:blank is "web", it should not be blocked. + nsAutoCString fullSpec; + nsresult rv = aContentLocation->GetSpec(fullSpec); + NS_ENSURE_SUCCESS(rv, true); + if (fullSpec.EqualsLiteral("about:blank")) { + return false; + } + + bool isHttp; + rv = aContentLocation->SchemeIs("http", &isHttp); + NS_ENSURE_SUCCESS(rv, true); + + bool isHttps; + rv = aContentLocation->SchemeIs("https", &isHttps); + NS_ENSURE_SUCCESS(rv, true); + + bool isWs; // websocket + rv = aContentLocation->SchemeIs("ws", &isWs); + NS_ENSURE_SUCCESS(rv, true); + + bool isWss; // secure websocket + rv = aContentLocation->SchemeIs("wss", &isWss); + NS_ENSURE_SUCCESS(rv, true); + + bool isBlob; + rv = aContentLocation->SchemeIs("blob", &isBlob); + NS_ENSURE_SUCCESS(rv, true); + + bool isFile; + rv = aContentLocation->SchemeIs("file", &isFile); + NS_ENSURE_SUCCESS(rv, true); + + return !isHttp && !isHttps && !isWs && !isWss && !isBlob && !isFile; +} + +/** + * The default for this function will be to reject the content request. + * When determining if to allow the request for a given msg hdr, the function + * will go through the list of remote content blocking criteria: + * + * #1 Allow if there is a db header for a manual override. + * #2 Allow if the message is in an RSS folder. + * #3 Allow if the domain for the remote image in our white list. + * #4 Allow if the author has been specifically white listed. + */ +int16_t nsMsgContentPolicy::ShouldAcceptRemoteContentForMsgHdr( + nsIMsgDBHdr* aMsgHdr, nsIURI* aRequestingLocation, + nsIURI* aContentLocation) { + if (!aMsgHdr) return static_cast<int16_t>(nsIContentPolicy::REJECT_REQUEST); + + // Case #1, check the db hdr for the remote content policy on this particular + // message. + uint32_t remoteContentPolicy = kNoRemoteContentPolicy; + aMsgHdr->GetUint32Property("remoteContentPolicy", &remoteContentPolicy); + + // Case #2, check if the message is in an RSS folder + bool isRSS = false; + IsRSSArticle(aRequestingLocation, &isRSS); + + // Case #3, the domain for the remote image is in our white list + bool trustedDomain = IsTrustedDomain(aContentLocation); + + // Case 4 means looking up items in the permissions database. So if + // either of the two previous items means we load the data, just do it. + if (isRSS || remoteContentPolicy == kAllowRemoteContent || trustedDomain) + return nsIContentPolicy::ACCEPT; + + // Case #4, author is in our white list.. + bool allowForSender = ShouldAcceptRemoteContentForSender(aMsgHdr); + + int16_t result = allowForSender + ? static_cast<int16_t>(nsIContentPolicy::ACCEPT) + : static_cast<int16_t>(nsIContentPolicy::REJECT_REQUEST); + + // kNoRemoteContentPolicy means we have never set a value on the message + if (result == nsIContentPolicy::REJECT_REQUEST && !remoteContentPolicy) + aMsgHdr->SetUint32Property("remoteContentPolicy", kBlockRemoteContent); + + return result; +} + +class RemoteContentNotifierEvent : public mozilla::Runnable { + public: + RemoteContentNotifierEvent(uint64_t aBrowsingContextId, nsIURI* aContentURI) + : mozilla::Runnable("RemoteContentNotifierEvent"), + mBrowsingContextId(aBrowsingContextId), + mContentURI(aContentURI) {} + + NS_IMETHOD Run() { + nsAutoString data; + data.AppendInt(mBrowsingContextId); + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + observerService->NotifyObservers(mContentURI, "remote-content-blocked", + data.get()); + return NS_OK; + } + + private: + uint64_t mBrowsingContextId; + nsCOMPtr<nsIURI> mContentURI; +}; + +/** + * This function is used to show a blocked remote content notification. + */ +void nsMsgContentPolicy::NotifyContentWasBlocked(uint64_t aBrowsingContextId, + nsIURI* aContentLocation) { + // Post this as an event because it can cause dom mutations, and we + // get called at a bad time to be causing dom mutations. + NS_DispatchToCurrentThread( + new RemoteContentNotifierEvent(aBrowsingContextId, aContentLocation)); +} + +/** + * This function is used to determine if we allow content for a remote message. + * If we reject loading remote content, then we'll inform the message window + * that this message has remote content (and hence we are not loading it). + * + * See ShouldAcceptRemoteContentForMsgHdr for the actual decisions that + * determine if we are going to allow remote content. + */ +void nsMsgContentPolicy::ShouldAcceptContentForPotentialMsg( + uint64_t aBrowsingContextId, nsIURI* aRequestingLocation, + nsIURI* aContentLocation, int16_t* aDecision) { + NS_ASSERTION( + *aDecision == nsIContentPolicy::REJECT_REQUEST, + "AllowContentForPotentialMessage expects default decision to be reject!"); + + // Is it a mailnews url? + nsresult rv; + nsCOMPtr<nsIMsgMessageUrl> msgUrl( + do_QueryInterface(aRequestingLocation, &rv)); + if (NS_FAILED(rv)) { + // It isn't a mailnews url - so we accept the load here, and let other + // content policies make the decision if we should be loading it or not. + *aDecision = nsIContentPolicy::ACCEPT; + return; + } + + nsCString resourceURI; + rv = msgUrl->GetUri(resourceURI); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgDBHdrFromURI(resourceURI, getter_AddRefs(msgHdr)); + + // Get a decision on whether or not to allow remote content for this message + // header. + *aDecision = ShouldAcceptRemoteContentForMsgHdr(msgHdr, aRequestingLocation, + aContentLocation); + + // If we're not allowing the remote content, tell the nsIMsgWindow loading + // this url that this is the case, so that the UI knows to show the remote + // content header bar, so the user can override if they wish. + if (*aDecision == nsIContentPolicy::REJECT_REQUEST) { + NotifyContentWasBlocked(aBrowsingContextId, aContentLocation); + } +} + +/** + * Content policy logic for compose windows + */ +void nsMsgContentPolicy::ComposeShouldLoad(nsIMsgCompose* aMsgCompose, + nsISupports* aRequestingContext, + nsIURI* aOriginatorLocation, + nsIURI* aContentLocation, + int16_t* aDecision) { + NS_ASSERTION(*aDecision == nsIContentPolicy::REJECT_REQUEST, + "ComposeShouldLoad expects default decision to be reject!"); + + nsCString originalMsgURI; + nsresult rv = aMsgCompose->GetOriginalMsgURI(originalMsgURI); + NS_ENSURE_SUCCESS_VOID(rv); + + if (!originalMsgURI.IsEmpty()) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgDBHdrFromURI(originalMsgURI, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS_VOID(rv); + *aDecision = + ShouldAcceptRemoteContentForMsgHdr(msgHdr, nullptr, aContentLocation); + + if (!aOriginatorLocation->GetSpecOrDefault().EqualsLiteral( + "about:blank?compose")) { + return; + } + } + + // We want to allow the user to add remote content, but do that only when + // the allowRemoteContent was set. This way quoted remoted content won't + // automatically load, but e.g. pasted content will load because the UI + // code toggles the flag. + nsCOMPtr<mozilla::dom::Element> element = + do_QueryInterface(aRequestingContext); + RefPtr<mozilla::dom::HTMLImageElement> image = + mozilla::dom::HTMLImageElement::FromNodeOrNull(element); + if (image) { + // Special case image elements. + bool allowRemoteContent = false; + aMsgCompose->GetAllowRemoteContent(&allowRemoteContent); + if (allowRemoteContent) { + *aDecision = nsIContentPolicy::ACCEPT; + return; + } + } +} + +already_AddRefed<nsIMsgCompose> +nsMsgContentPolicy::GetMsgComposeForBrowsingContext( + mozilla::dom::BrowsingContext* aBrowsingContext) { + nsresult rv; + + nsIDocShell* shell = aBrowsingContext->GetDocShell(); + if (!shell) return nullptr; + nsCOMPtr<nsIDocShellTreeItem> docShellTreeItem(shell); + + nsCOMPtr<nsIDocShellTreeItem> rootItem; + rv = docShellTreeItem->GetInProcessSameTypeRootTreeItem( + getter_AddRefs(rootItem)); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(rootItem, &rv)); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr<nsIMsgComposeService> composeService( + do_GetService("@mozilla.org/messengercompose;1", &rv)); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr<nsIMsgCompose> msgCompose; + // Don't bother checking rv, as GetMsgComposeForDocShell returns + // NS_ERROR_FAILURE for not found. + composeService->GetMsgComposeForDocShell(docShell, + getter_AddRefs(msgCompose)); + return msgCompose.forget(); +} + +nsresult nsMsgContentPolicy::SetDisableItemsOnMailNewsUrlDocshells( + nsIURI* aContentLocation, nsILoadInfo* aLoadInfo) { + // XXX if this class changes so that this method can be called from + // ShouldProcess, and if it's possible for this to be null when called from + // ShouldLoad, but not in the corresponding ShouldProcess call, + // we need to re-think the assumptions underlying this code. + + NS_ENSURE_ARG_POINTER(aContentLocation); + NS_ENSURE_ARG_POINTER(aLoadInfo); + + RefPtr<mozilla::dom::BrowsingContext> browsingContext = + aLoadInfo->GetTargetBrowsingContext(); + if (!browsingContext) { + return NS_OK; + } + + // We're only worried about policy settings in content docshells. + if (!browsingContext->IsContent()) { + return NS_OK; + } + + nsCOMPtr<nsIDocShell> docShell = browsingContext->GetDocShell(); + if (!docShell) { + // If there's no docshell to get to, there's nowhere for the JavaScript to + // run, so we're already safe and don't need to disable anything. + return NS_OK; + } + + // Ensure starting off unsandboxed. We sandbox later if needed. + MOZ_ALWAYS_SUCCEEDS(browsingContext->SetSandboxFlags(SANDBOXED_NONE)); + + nsresult rv; + bool isAllowedContent = !ShouldBlockUnexposedProtocol(aContentLocation); + nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(aContentLocation); + if (!msgUrl && !isAllowedContent) { + // If it's not a mailnews url or allowed content url (http[s]|file) then + // bail; otherwise set whether JavaScript is allowed. + return NS_OK; + } + + if (!isAllowedContent) { + // Disable JavaScript on message URLs. + rv = browsingContext->SetAllowJavascript(false); + NS_ENSURE_SUCCESS(rv, rv); + rv = browsingContext->SetAllowContentRetargetingOnChildren(false); + NS_ENSURE_SUCCESS(rv, rv); + // NOTE! Do not set single sandboxing flags only. Sandboxing - when used - + // starts off with all things sandboxed, and individual sandbox keywords + // will *allow* the specific feature. + // Disabling by setting single flags without starting off with all things + // sandboxed would the normal assumptions about sandboxing. + // The flags - contrary to the keywords - *prevent* a given feature. + uint32_t sandboxFlags = SANDBOX_ALL_FLAGS; + + // Do not block links with target attribute from opening (at all). + // xref bug 421310 - we would like to prevent using target, but *handle* + // links like the target wasn't there. + sandboxFlags &= ~SANDBOXED_AUXILIARY_NAVIGATION; + + // For some unexplicable reason, when SANDBOXED_ORIGIN is in affect, then + // images will not work with test --verify. So unset it. + sandboxFlags &= ~SANDBOXED_ORIGIN; + + // Having both SANDBOXED_TOPLEVEL_NAVIGATION and + // SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION will generate a warning, + // see BothAllowTopNavigationAndUserActivationPresent. So unset it. + sandboxFlags &= ~SANDBOXED_TOPLEVEL_NAVIGATION_USER_ACTIVATION; + + rv = browsingContext->SetSandboxFlags(sandboxFlags); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // JavaScript is allowed on non-message URLs. + rv = browsingContext->SetAllowJavascript(true); + NS_ENSURE_SUCCESS(rv, rv); + rv = browsingContext->SetAllowContentRetargetingOnChildren(true); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = docShell->SetAllowPlugins(false); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +/** + * Gets the root docshell from a requesting context. + */ +nsresult nsMsgContentPolicy::GetRootDocShellForContext( + nsISupports* aRequestingContext, nsIDocShell** aDocShell) { + NS_ENSURE_ARG_POINTER(aRequestingContext); + nsresult rv; + + nsIDocShell* shell = NS_CP_GetDocShellFromContext(aRequestingContext); + NS_ENSURE_TRUE(shell, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIDocShellTreeItem> docshellTreeItem(shell); + + nsCOMPtr<nsIDocShellTreeItem> rootItem; + rv = docshellTreeItem->GetInProcessRootTreeItem(getter_AddRefs(rootItem)); + NS_ENSURE_SUCCESS(rv, rv); + + return CallQueryInterface(rootItem, aDocShell); +} + +/** + * Gets the originating URI that started off a set of requests, accounting + * for multiple iframes. + * + * Navigates up the docshell tree from aRequestingContext and finds the + * highest parent with the same type docshell as aRequestingContext, then + * returns the URI associated with that docshell. + */ +nsresult nsMsgContentPolicy::GetOriginatingURIForContext( + nsISupports* aRequestingContext, nsIURI** aURI) { + NS_ENSURE_ARG_POINTER(aRequestingContext); + nsresult rv; + + nsIDocShell* shell = NS_CP_GetDocShellFromContext(aRequestingContext); + if (!shell) { + *aURI = nullptr; + return NS_OK; + } + nsCOMPtr<nsIDocShellTreeItem> docshellTreeItem(shell); + + nsCOMPtr<nsIDocShellTreeItem> rootItem; + rv = docshellTreeItem->GetInProcessSameTypeRootTreeItem( + getter_AddRefs(rootItem)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIWebNavigation> webNavigation(do_QueryInterface(rootItem, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + return webNavigation->GetCurrentURI(aURI); +} + +NS_IMETHODIMP +nsMsgContentPolicy::ShouldProcess(nsIURI* aContentLocation, + nsILoadInfo* aLoadInfo, + const nsACString& aMimeGuess, + int16_t* aDecision) { + // XXX Returning ACCEPT is presumably only a reasonable thing to do if we + // think that ShouldLoad is going to catch all possible cases (i.e. that + // everything we use to make decisions is going to be available at + // ShouldLoad time, and not only become available in time for ShouldProcess). + // Do we think that's actually the case? + *aDecision = nsIContentPolicy::ACCEPT; + return NS_OK; +} + +NS_IMETHODIMP nsMsgContentPolicy::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + if (!strcmp(NS_PREFBRANCH_PREFCHANGE_TOPIC_ID, aTopic)) { + NS_LossyConvertUTF16toASCII pref(aData); + + nsresult rv; + + nsCOMPtr<nsIPrefBranch> prefBranchInt = do_QueryInterface(aSubject, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + if (pref.Equals(kBlockRemoteImages)) + prefBranchInt->GetBoolPref(kBlockRemoteImages, &mBlockRemoteImages); + } + + return NS_OK; +} + +/** + * Implementation of nsIMsgContentPolicy + * + */ +NS_IMETHODIMP +nsMsgContentPolicy::AddExposedProtocol(const nsACString& aScheme) { + if (mCustomExposedProtocols.Contains(nsCString(aScheme))) return NS_OK; + + mCustomExposedProtocols.AppendElement(aScheme); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgContentPolicy::RemoveExposedProtocol(const nsACString& aScheme) { + mCustomExposedProtocols.RemoveElement(nsCString(aScheme)); + + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgContentPolicy.h b/comm/mailnews/base/src/nsMsgContentPolicy.h new file mode 100644 index 0000000000..e6d3d10e79 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgContentPolicy.h @@ -0,0 +1,95 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/****************************************************************************** + * nsMsgContentPolicy enforces the specified content policy on images, js, + * plugins, etc. This is the class used to determine what elements in a message + * should be loaded. + * + * nsMsgCookiePolicy enforces our cookie policy for mail and RSS messages. + ******************************************************************************/ + +#ifndef _nsMsgContentPolicy_H_ +#define _nsMsgContentPolicy_H_ + +#include "nsIContentPolicy.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nsString.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIMsgCompose.h" +#include "nsIDocShell.h" +#include "nsIPermissionManager.h" +#include "nsIMsgContentPolicy.h" +#include "nsTArray.h" + +/* DBFCFDF0-4489-4faa-8122-190FD1EFA16C */ +#define NS_MSGCONTENTPOLICY_CID \ + { \ + 0xdbfcfdf0, 0x4489, 0x4faa, { \ + 0x81, 0x22, 0x19, 0xf, 0xd1, 0xef, 0xa1, 0x6c \ + } \ + } + +#define NS_MSGCONTENTPOLICY_CONTRACTID "@mozilla.org/messenger/content-policy;1" + +class nsIMsgDBHdr; +class nsIDocShell; + +class nsMsgContentPolicy : public nsIContentPolicy, + public nsIObserver, + public nsIMsgContentPolicy, + public nsSupportsWeakReference { + public: + nsMsgContentPolicy(); + + nsresult Init(); + + NS_DECL_ISUPPORTS + NS_DECL_NSICONTENTPOLICY + NS_DECL_NSIOBSERVER + NS_DECL_NSIMSGCONTENTPOLICY + + protected: + virtual ~nsMsgContentPolicy(); + + bool mBlockRemoteImages; + nsCString mTrustedMailDomains; + nsCOMPtr<nsIPermissionManager> mPermissionManager; + + bool IsTrustedDomain(nsIURI* aContentLocation); + bool IsSafeRequestingLocation(nsIURI* aRequestingLocation); + bool IsExposedProtocol(nsIURI* aContentLocation); + bool IsExposedChromeProtocol(nsIURI* aContentLocation); + bool ShouldBlockUnexposedProtocol(nsIURI* aContentLocation); + + bool ShouldAcceptRemoteContentForSender(nsIMsgDBHdr* aMsgHdr); + int16_t ShouldAcceptRemoteContentForMsgHdr(nsIMsgDBHdr* aMsgHdr, + nsIURI* aRequestingLocation, + nsIURI* aContentLocation); + void NotifyContentWasBlocked(uint64_t aBrowsingContextId, + nsIURI* aContentLocation); + void ShouldAcceptContentForPotentialMsg(uint64_t aBrowsingContextId, + nsIURI* aOriginatorLocation, + nsIURI* aContentLocation, + int16_t* aDecision); + void ComposeShouldLoad(nsIMsgCompose* aMsgCompose, + nsISupports* aRequestingContext, + nsIURI* aOriginatorLocation, nsIURI* aContentLocation, + int16_t* aDecision); + already_AddRefed<nsIMsgCompose> GetMsgComposeForBrowsingContext( + mozilla::dom::BrowsingContext* aRequestingContext); + + nsresult GetRootDocShellForContext(nsISupports* aRequestingContext, + nsIDocShell** aDocShell); + nsresult GetOriginatingURIForContext(nsISupports* aRequestingContext, + nsIURI** aURI); + nsresult SetDisableItemsOnMailNewsUrlDocshells(nsIURI* aContentLocation, + nsILoadInfo* aLoadInfo); + + nsTArray<nsCString> mCustomExposedProtocols; +}; + +#endif // _nsMsgContentPolicy_H_ diff --git a/comm/mailnews/base/src/nsMsgCopyService.cpp b/comm/mailnews/base/src/nsMsgCopyService.cpp new file mode 100644 index 0000000000..22df6bb6e9 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgCopyService.cpp @@ -0,0 +1,587 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgCopyService.h" +#include "nsCOMArray.h" +#include "nspr.h" +#include "nsIFile.h" +#include "nsIMsgFolderNotificationService.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMsgUtils.h" +#include "mozilla/Logging.h" + +static mozilla::LazyLogModule gCopyServiceLog("MsgCopyService"); + +// ******************** nsCopySource ****************** + +nsCopySource::nsCopySource() : m_processed(false) { + MOZ_COUNT_CTOR(nsCopySource); +} + +nsCopySource::nsCopySource(nsIMsgFolder* srcFolder) : m_processed(false) { + MOZ_COUNT_CTOR(nsCopySource); + m_msgFolder = srcFolder; +} + +nsCopySource::~nsCopySource() { MOZ_COUNT_DTOR(nsCopySource); } + +void nsCopySource::AddMessage(nsIMsgDBHdr* aMsg) { + m_messageArray.AppendElement(aMsg); +} + +// ************ nsCopyRequest ***************** +// + +nsCopyRequest::nsCopyRequest() + : m_requestType(nsCopyMessagesType), + m_isMoveOrDraftOrTemplate(false), + m_processed(false), + m_newMsgFlags(0) { + MOZ_COUNT_CTOR(nsCopyRequest); +} + +nsCopyRequest::~nsCopyRequest() { + MOZ_COUNT_DTOR(nsCopyRequest); + + int32_t j = m_copySourceArray.Length(); + while (j-- > 0) delete m_copySourceArray.ElementAt(j); +} + +nsresult nsCopyRequest::Init(nsCopyRequestType type, nsISupports* aSupport, + nsIMsgFolder* dstFolder, bool bVal, + uint32_t newMsgFlags, + const nsACString& newMsgKeywords, + nsIMsgCopyServiceListener* listener, + nsIMsgWindow* msgWindow, bool allowUndo) { + nsresult rv = NS_OK; + m_requestType = type; + m_srcSupport = aSupport; + m_dstFolder = dstFolder; + m_isMoveOrDraftOrTemplate = bVal; + m_allowUndo = allowUndo; + m_newMsgFlags = newMsgFlags; + m_newMsgKeywords = newMsgKeywords; + + if (listener) m_listener = listener; + if (msgWindow) { + m_msgWindow = msgWindow; + if (m_allowUndo) msgWindow->GetTransactionManager(getter_AddRefs(m_txnMgr)); + } + if (type == nsCopyFoldersType) { + // To support multiple copy folder operations to the same destination, we + // need to save the leaf name of the src file spec so that FindRequest() is + // able to find the right request when copy finishes. + nsCOMPtr<nsIMsgFolder> srcFolder = do_QueryInterface(aSupport, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsString folderName; + rv = srcFolder->GetName(folderName); + NS_ENSURE_SUCCESS(rv, rv); + m_dstFolderName = folderName; + } + + return rv; +} + +nsCopySource* nsCopyRequest::AddNewCopySource(nsIMsgFolder* srcFolder) { + nsCopySource* newSrc = new nsCopySource(srcFolder); + if (newSrc) { + m_copySourceArray.AppendElement(newSrc); + if (srcFolder == m_dstFolder) newSrc->m_processed = true; + } + return newSrc; +} + +// ************* nsMsgCopyService **************** +// + +nsMsgCopyService::nsMsgCopyService() {} + +nsMsgCopyService::~nsMsgCopyService() { + int32_t i = m_copyRequests.Length(); + + while (i-- > 0) ClearRequest(m_copyRequests.ElementAt(i), NS_ERROR_FAILURE); +} + +void nsMsgCopyService::LogCopyCompletion(nsISupports* aSrc, + nsIMsgFolder* aDest) { + nsCString srcFolderUri, destFolderUri; + nsCOMPtr<nsIMsgFolder> srcFolder(do_QueryInterface(aSrc)); + if (srcFolder) srcFolder->GetURI(srcFolderUri); + aDest->GetURI(destFolderUri); + MOZ_LOG(gCopyServiceLog, mozilla::LogLevel::Info, + ("NotifyCompletion - src %s dest %s\n", srcFolderUri.get(), + destFolderUri.get())); +} + +void nsMsgCopyService::LogCopyRequest(const char* logMsg, + nsCopyRequest* aRequest) { + nsCString srcFolderUri, destFolderUri; + nsCOMPtr<nsIMsgFolder> srcFolder(do_QueryInterface(aRequest->m_srcSupport)); + if (srcFolder) srcFolder->GetURI(srcFolderUri); + aRequest->m_dstFolder->GetURI(destFolderUri); + uint32_t numMsgs = 0; + if (aRequest->m_requestType == nsCopyMessagesType && + aRequest->m_copySourceArray.Length() > 0) { + numMsgs = aRequest->m_copySourceArray[0]->m_messageArray.Length(); + } + MOZ_LOG(gCopyServiceLog, mozilla::LogLevel::Info, + ("request %p %s - src %s dest %s numItems %d type=%d", aRequest, + logMsg, srcFolderUri.get(), destFolderUri.get(), numMsgs, + aRequest->m_requestType)); +} + +nsresult nsMsgCopyService::ClearRequest(nsCopyRequest* aRequest, nsresult rv) { + if (aRequest) { + if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info)) + LogCopyRequest( + NS_SUCCEEDED(rv) ? "Clearing OK request" : "Clearing failed request", + aRequest); + + if (NS_SUCCEEDED(rv) && aRequest->m_requestType == nsCopyFoldersType) { + // Send folder copy/move notifications to nsIMsgFolderListeners. + // BAD SMELL ALERT: Seems odd that this is the only place the folder + // notification is invoked from the copyService. + // For message copy/move operations, the folder code handles the + // notification (to take one example). + // This suggests lack of clarity of responsibility. + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) { + for (nsCopySource* copySource : aRequest->m_copySourceArray) { + notifier->NotifyFolderMoveCopyCompleted( + aRequest->m_isMoveOrDraftOrTemplate, copySource->m_msgFolder, + aRequest->m_dstFolder); + } + } + } + + // undo stuff + if (aRequest->m_allowUndo && aRequest->m_copySourceArray.Length() > 1 && + aRequest->m_txnMgr) + aRequest->m_txnMgr->EndBatch(false); + + m_copyRequests.RemoveElement(aRequest); + if (aRequest->m_listener) aRequest->m_listener->OnStopCopy(rv); + delete aRequest; + } + + return rv; +} + +nsresult nsMsgCopyService::QueueRequest(nsCopyRequest* aRequest, + bool* aCopyImmediately) { + NS_ENSURE_ARG_POINTER(aRequest); + NS_ENSURE_ARG_POINTER(aCopyImmediately); + *aCopyImmediately = true; + nsCopyRequest* copyRequest; + + // Check through previous requests to see if the copy can start immediately. + uint32_t cnt = m_copyRequests.Length(); + + for (uint32_t i = 0; i < cnt; i++) { + copyRequest = m_copyRequests.ElementAt(i); + if (aRequest->m_requestType == nsCopyFoldersType) { + // For copy folder, see if both destination folder (root) + // (ie, Local Folder) and folder name (ie, abc) are the same. + if (copyRequest->m_dstFolderName == aRequest->m_dstFolderName && + SameCOMIdentity(copyRequest->m_dstFolder, aRequest->m_dstFolder)) { + *aCopyImmediately = false; + break; + } + } else if (SameCOMIdentity(copyRequest->m_dstFolder, + aRequest->m_dstFolder)) { + // If dst are same and we already have a request, we cannot copy + // immediately. + *aCopyImmediately = false; + break; + } + } + + // Queue it. + m_copyRequests.AppendElement(aRequest); + return NS_OK; +} + +nsresult nsMsgCopyService::DoCopy(nsCopyRequest* aRequest) { + NS_ENSURE_ARG(aRequest); + bool copyImmediately; + QueueRequest(aRequest, ©Immediately); + if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info)) + LogCopyRequest(copyImmediately ? "DoCopy" : "QueueRequest", aRequest); + + // if no active request for this dest folder then we can copy immediately + if (copyImmediately) return DoNextCopy(); + + return NS_OK; +} + +nsresult nsMsgCopyService::DoNextCopy() { + nsresult rv = NS_OK; + nsCopyRequest* copyRequest = nullptr; + nsCopySource* copySource = nullptr; + uint32_t i, j, scnt; + + uint32_t cnt = m_copyRequests.Length(); + if (cnt > 0) { + nsCOMArray<nsIMsgFolder> activeTargets; + + // ** jt -- always FIFO + for (i = 0; i < cnt; i++) { + copyRequest = m_copyRequests.ElementAt(i); + copySource = nullptr; + scnt = copyRequest->m_copySourceArray.Length(); + if (!copyRequest->m_processed) { + // if the target folder of this request already has an active + // copy request, skip this request for now. + if (activeTargets.ContainsObject(copyRequest->m_dstFolder)) { + copyRequest = nullptr; + continue; + } + if (scnt <= 0) goto found; // must be CopyFileMessage + for (j = 0; j < scnt; j++) { + copySource = copyRequest->m_copySourceArray.ElementAt(j); + if (!copySource->m_processed) goto found; + } + if (j >= scnt) // all processed set the value + copyRequest->m_processed = true; + } + if (copyRequest->m_processed) { + // Keep track of folders actively getting copied to. + activeTargets.AppendObject(copyRequest->m_dstFolder); + } + } + found: + if (copyRequest && !copyRequest->m_processed) { + if (copyRequest->m_listener) copyRequest->m_listener->OnStartCopy(); + if (copyRequest->m_requestType == nsCopyMessagesType && copySource) { + copySource->m_processed = true; + rv = copyRequest->m_dstFolder->CopyMessages( + copySource->m_msgFolder, copySource->m_messageArray, + copyRequest->m_isMoveOrDraftOrTemplate, copyRequest->m_msgWindow, + copyRequest->m_listener, false, + copyRequest->m_allowUndo); // isFolder operation false + + } else if (copyRequest->m_requestType == nsCopyFoldersType) { + NS_ENSURE_STATE(copySource); + copySource->m_processed = true; + + nsCOMPtr<nsIMsgFolder> dstFolder = copyRequest->m_dstFolder; + nsCOMPtr<nsIMsgFolder> srcFolder = copySource->m_msgFolder; + + // If folder transfer is not within the same server and if a folder + // move was requested, set the request move flag false to avoid + // removing the list of marked deleted messages in the source folder. + bool isMove = copyRequest->m_isMoveOrDraftOrTemplate; + if (copyRequest->m_isMoveOrDraftOrTemplate) { + bool sameServer; + IsOnSameServer(dstFolder, srcFolder, &sameServer); + if (!sameServer) copyRequest->m_isMoveOrDraftOrTemplate = false; + } + + // NOTE: The folder invokes NotifyCompletion() when the operation is + // complete. Some folders (localfolder!) invoke it before CopyFolder() + // even returns. This will likely delete the request object, so + // you have to assume that copyRequest is invalid when CopyFolder() + // returns. + rv = dstFolder->CopyFolder(srcFolder, isMove, copyRequest->m_msgWindow, + copyRequest->m_listener); + // If CopyFolder() fails (e.g. destination folder already exists), + // it won't send a completion notification (NotifyCompletion()). + // So copyRequest will still exist, and we need to ditch it. + if (NS_FAILED(rv)) { + ClearRequest(copyRequest, rv); + } + } else if (copyRequest->m_requestType == nsCopyFileMessageType) { + nsCOMPtr<nsIFile> aFile( + do_QueryInterface(copyRequest->m_srcSupport, &rv)); + if (NS_SUCCEEDED(rv)) { + // ** in case of saving draft/template; the very first + // time we may not have the original message to replace + // with; if we do we shall have an instance of copySource + nsCOMPtr<nsIMsgDBHdr> aMessage; + if (copySource) { + aMessage = copySource->m_messageArray[0]; + copySource->m_processed = true; + } + copyRequest->m_processed = true; + rv = copyRequest->m_dstFolder->CopyFileMessage( + aFile, aMessage, copyRequest->m_isMoveOrDraftOrTemplate, + copyRequest->m_newMsgFlags, copyRequest->m_newMsgKeywords, + copyRequest->m_msgWindow, copyRequest->m_listener); + } + } + } + } + return rv; +} + +/** + * Find a request in m_copyRequests which matches the passed in source + * and destination folders. + * + * @param aSupport the iSupports of the source folder. + * @param dstFolder the destination folder of the copy request. + */ +nsCopyRequest* nsMsgCopyService::FindRequest(nsISupports* aSupport, + nsIMsgFolder* dstFolder) { + nsCopyRequest* copyRequest = nullptr; + uint32_t cnt = m_copyRequests.Length(); + for (uint32_t i = 0; i < cnt; i++) { + copyRequest = m_copyRequests.ElementAt(i); + if (SameCOMIdentity(copyRequest->m_srcSupport, aSupport) && + SameCOMIdentity(copyRequest->m_dstFolder.get(), dstFolder)) + break; + + // When copying folders the notification of the message copy serves as a + // proxy for the folder copy. Check for that here. + if (copyRequest->m_requestType == nsCopyFoldersType) { + // If the src is different then check next request. + if (!SameCOMIdentity(copyRequest->m_srcSupport, aSupport)) { + copyRequest = nullptr; + continue; + } + + // See if the parent of the copied folder is the same as the one when the + // request was made. Note if the destination folder is already a server + // folder then no need to get parent. + nsCOMPtr<nsIMsgFolder> parentMsgFolder; + nsresult rv = NS_OK; + bool isServer = false; + dstFolder->GetIsServer(&isServer); + if (!isServer) rv = dstFolder->GetParent(getter_AddRefs(parentMsgFolder)); + if ((NS_FAILED(rv)) || (!parentMsgFolder && !isServer) || + (copyRequest->m_dstFolder.get() != parentMsgFolder)) { + copyRequest = nullptr; + continue; + } + + // Now checks if the folder name is the same. + nsString folderName; + rv = dstFolder->GetName(folderName); + if (NS_FAILED(rv)) { + copyRequest = nullptr; + continue; + } + + if (copyRequest->m_dstFolderName == folderName) break; + } else + copyRequest = nullptr; + } + + return copyRequest; +} + +NS_IMPL_ISUPPORTS(nsMsgCopyService, nsIMsgCopyService) + +MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP nsMsgCopyService::CopyMessages( + nsIMsgFolder* srcFolder, /* UI src folder */ + nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgFolder* dstFolder, + bool isMove, nsIMsgCopyServiceListener* listener, nsIMsgWindow* window, + bool allowUndo) { + NS_ENSURE_ARG_POINTER(srcFolder); + NS_ENSURE_ARG_POINTER(dstFolder); + + MOZ_LOG(gCopyServiceLog, mozilla::LogLevel::Debug, ("CopyMessages")); + + if (srcFolder == dstFolder) { + NS_ERROR("src and dest folders for msg copy can't be the same"); + return NS_ERROR_FAILURE; + } + nsCopyRequest* copyRequest; + nsCopySource* copySource = nullptr; + nsIMsgDBHdr* msg; + nsCOMPtr<nsIMsgFolder> curFolder; + nsCOMPtr<nsISupports> aSupport; + int cnt; + nsresult rv; + + // XXX TODO + // JUNK MAIL RELATED + // make sure dest folder exists + // and has proper flags, before we start copying? + + // bail early if nothing to do + if (messages.IsEmpty()) { + if (listener) { + listener->OnStartCopy(); + listener->OnStopCopy(NS_OK); + } + return NS_OK; + } + + copyRequest = new nsCopyRequest(); + if (!copyRequest) return NS_ERROR_OUT_OF_MEMORY; + + nsTArray<RefPtr<nsIMsgDBHdr>> unprocessed = messages.Clone(); + aSupport = srcFolder; + + rv = copyRequest->Init(nsCopyMessagesType, aSupport, dstFolder, isMove, + 0 /* new msg flags, not used */, EmptyCString(), + listener, window, allowUndo); + if (NS_FAILED(rv)) goto done; + + if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info)) + LogCopyRequest("CopyMessages request", copyRequest); + + // Build up multiple nsCopySource objects. Each holds a single source folder + // and all the messages in the folder that are to be copied. + cnt = unprocessed.Length(); + while (cnt-- > 0) { + msg = unprocessed[cnt]; + rv = msg->GetFolder(getter_AddRefs(curFolder)); + + if (NS_FAILED(rv)) goto done; + if (!copySource) { + // Begin a folder grouping. + copySource = copyRequest->AddNewCopySource(curFolder); + if (!copySource) { + rv = NS_ERROR_OUT_OF_MEMORY; + goto done; + } + } + + // Stash message if in the current folder grouping. + if (curFolder == copySource->m_msgFolder) { + copySource->AddMessage(msg); + unprocessed.RemoveElementAt((size_t)cnt); + } + + if (cnt == 0) { + // Finished a folder. Start a new pass to handle any remaining messages + // in other folders. + cnt = unprocessed.Length(); + if (cnt > 0) { + // Force to create a new one and continue grouping the messages. + copySource = nullptr; + } + } + } + + // undo stuff + if (NS_SUCCEEDED(rv) && copyRequest->m_allowUndo && + copyRequest->m_copySourceArray.Length() > 1 && copyRequest->m_txnMgr) { + nsCOMPtr<nsITransactionManager> txnMgr = copyRequest->m_txnMgr; + txnMgr->BeginBatch(nullptr); + } + +done: + + if (NS_FAILED(rv)) + delete copyRequest; + else + rv = DoCopy(copyRequest); + + return rv; +} + +NS_IMETHODIMP +nsMsgCopyService::CopyFolder(nsIMsgFolder* srcFolder, nsIMsgFolder* dstFolder, + bool isMove, nsIMsgCopyServiceListener* listener, + nsIMsgWindow* window) { + NS_ENSURE_ARG_POINTER(srcFolder); + NS_ENSURE_ARG_POINTER(dstFolder); + nsCopyRequest* copyRequest; + nsresult rv; + nsCOMPtr<nsIMsgFolder> curFolder; + + copyRequest = new nsCopyRequest(); + rv = copyRequest->Init(nsCopyFoldersType, srcFolder, dstFolder, isMove, + 0 /* new msg flags, not used */, EmptyCString(), + listener, window, false); + NS_ENSURE_SUCCESS(rv, rv); + + copyRequest->AddNewCopySource(srcFolder); + return DoCopy(copyRequest); +} + +NS_IMETHODIMP +nsMsgCopyService::CopyFileMessage(nsIFile* file, nsIMsgFolder* dstFolder, + nsIMsgDBHdr* msgToReplace, bool isDraft, + uint32_t aMsgFlags, + const nsACString& aNewMsgKeywords, + nsIMsgCopyServiceListener* listener, + nsIMsgWindow* window) { + nsresult rv = NS_ERROR_NULL_POINTER; + nsCopyRequest* copyRequest; + nsCopySource* copySource = nullptr; + + NS_ENSURE_ARG_POINTER(file); + NS_ENSURE_ARG_POINTER(dstFolder); + + copyRequest = new nsCopyRequest(); + if (!copyRequest) return rv; + + rv = copyRequest->Init(nsCopyFileMessageType, file, dstFolder, isDraft, + aMsgFlags, aNewMsgKeywords, listener, window, false); + if (NS_FAILED(rv)) goto done; + + if (msgToReplace) { + // The actual source of the message is a file not a folder, but + // we still need an nsCopySource to reference the old message header + // which will be used to recover message metadata. + copySource = copyRequest->AddNewCopySource(nullptr); + if (!copySource) { + rv = NS_ERROR_OUT_OF_MEMORY; + goto done; + } + copySource->AddMessage(msgToReplace); + } + +done: + if (NS_FAILED(rv)) { + delete copyRequest; + } else { + rv = DoCopy(copyRequest); + } + + return rv; +} + +NS_IMETHODIMP +nsMsgCopyService::NotifyCompletion(nsISupports* aSupport, + nsIMsgFolder* dstFolder, nsresult result) { + if (MOZ_LOG_TEST(gCopyServiceLog, mozilla::LogLevel::Info)) + LogCopyCompletion(aSupport, dstFolder); + nsCopyRequest* copyRequest = nullptr; + uint32_t numOrigRequests = m_copyRequests.Length(); + do { + // loop for copy requests, because if we do a cross server folder copy, + // we'll have a copy request for the folder copy, which will in turn + // generate a copy request for the messages in the folder, which + // will have the same src support. + copyRequest = FindRequest(aSupport, dstFolder); + + if (copyRequest) { + // ClearRequest can cause a new request to get added to m_copyRequests + // with matching source and dest folders if the copy listener starts + // a new copy. We want to ignore any such request here, because it wasn't + // the one that was completed. So we keep track of how many original + // requests there were. + if (m_copyRequests.IndexOf(copyRequest) >= numOrigRequests) break; + // check if this copy request is done by making sure all the + // sources have been processed. + int32_t sourceIndex, sourceCount; + sourceCount = copyRequest->m_copySourceArray.Length(); + for (sourceIndex = 0; sourceIndex < sourceCount;) { + if (!(copyRequest->m_copySourceArray.ElementAt(sourceIndex)) + ->m_processed) + break; + sourceIndex++; + } + // if all sources processed, mark the request as processed + if (sourceIndex >= sourceCount) copyRequest->m_processed = true; + // if this request is done, or failed, clear it. + if (copyRequest->m_processed || NS_FAILED(result)) { + ClearRequest(copyRequest, result); + numOrigRequests--; + } else + break; + } else + break; + } while (copyRequest); + + return DoNextCopy(); +} diff --git a/comm/mailnews/base/src/nsMsgCopyService.h b/comm/mailnews/base/src/nsMsgCopyService.h new file mode 100644 index 0000000000..ab9be438d0 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgCopyService.h @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgCopyService_h__ +#define nsMsgCopyService_h__ + +#include "nscore.h" +#include "nsIMsgCopyService.h" +#include "nsCOMPtr.h" +#include "nsIMsgFolder.h" +#include "nsIMsgHdr.h" +#include "nsIMsgWindow.h" +#include "nsITransactionManager.h" +#include "nsTArray.h" + +typedef enum _nsCopyRequestType { + nsCopyMessagesType = 0x0, + nsCopyFileMessageType = 0x1, + nsCopyFoldersType = 0x2 +} nsCopyRequestType; + +class nsCopyRequest; + +// nsCopySource represents either: +// 1. a bundle of messages to be copied, all from the same folder. +// or +// 2. a folder to be copied (and no messages). +class nsCopySource { + public: + nsCopySource(); + explicit nsCopySource(nsIMsgFolder* srcFolder); + ~nsCopySource(); + void AddMessage(nsIMsgDBHdr* aMsg); + + nsCOMPtr<nsIMsgFolder> m_msgFolder; + nsTArray<RefPtr<nsIMsgDBHdr>> m_messageArray; + bool m_processed; +}; + +class nsCopyRequest { + public: + nsCopyRequest(); + ~nsCopyRequest(); + + nsresult Init(nsCopyRequestType type, nsISupports* aSupport, + nsIMsgFolder* dstFolder, bool bVal, uint32_t newMsgFlags, + const nsACString& newMsgKeywords, + nsIMsgCopyServiceListener* listener, nsIMsgWindow* msgWindow, + bool allowUndo); + nsCopySource* AddNewCopySource(nsIMsgFolder* srcFolder); + + nsCOMPtr<nsISupports> m_srcSupport; // ui source folder or file spec + nsCOMPtr<nsIMsgFolder> m_dstFolder; + nsCOMPtr<nsIMsgWindow> m_msgWindow; + nsCOMPtr<nsIMsgCopyServiceListener> m_listener; + nsCOMPtr<nsITransactionManager> m_txnMgr; + nsCopyRequestType m_requestType; + bool m_isMoveOrDraftOrTemplate; + bool m_allowUndo; + bool m_processed; + uint32_t m_newMsgFlags; + nsCString m_newMsgKeywords; + nsString m_dstFolderName; // used for copy folder. + nsTArray<nsCopySource*> m_copySourceArray; // array of nsCopySource +}; + +class nsMsgCopyService : public nsIMsgCopyService { + public: + nsMsgCopyService(); + + NS_DECL_THREADSAFE_ISUPPORTS + + NS_DECL_NSIMSGCOPYSERVICE + + private: + virtual ~nsMsgCopyService(); + + nsresult ClearRequest(nsCopyRequest* aRequest, nsresult rv); + nsresult DoCopy(nsCopyRequest* aRequest); + nsresult DoNextCopy(); + nsCopyRequest* FindRequest(nsISupports* aSupport, nsIMsgFolder* dstFolder); + nsresult QueueRequest(nsCopyRequest* aRequest, bool* aCopyImmediately); + void LogCopyCompletion(nsISupports* aSrc, nsIMsgFolder* aDest); + void LogCopyRequest(const char* logMsg, nsCopyRequest* aRequest); + + nsTArray<nsCopyRequest*> m_copyRequests; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgDBFolder.cpp b/comm/mailnews/base/src/nsMsgDBFolder.cpp new file mode 100644 index 0000000000..8e82ce1a5c --- /dev/null +++ b/comm/mailnews/base/src/nsMsgDBFolder.cpp @@ -0,0 +1,5573 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsUnicharUtils.h" +#include "nsMsgDBFolder.h" +#include "nsMsgFolderFlags.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsNetUtil.h" +#include "nsIMsgFolderCache.h" +#include "nsIMsgFolderCacheElement.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsMsgDatabase.h" +#include "nsIMsgAccountManager.h" +#include "nsISeekableStream.h" +#include "nsNativeCharsetUtils.h" +#include "nsIChannel.h" +#include "nsITransport.h" +#include "nsIWindowWatcher.h" +#include "nsIMsgFolderCompactor.h" +#include "nsIDocShell.h" +#include "nsIMsgWindow.h" +#include "nsIPrompt.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIAbCard.h" +#include "nsIAbDirectory.h" +#include "nsISpamSettings.h" +#include "nsIMsgFilterPlugin.h" +#include "nsIMsgMailSession.h" +#include "nsTextFormatter.h" +#include "nsReadLine.h" +#include "nsLayoutCID.h" +#include "nsIParserUtils.h" +#include "nsIDocumentEncoder.h" +#include "nsMsgI18N.h" +#include "nsIMIMEHeaderParam.h" +#include "plbase64.h" +#include <time.h> +#include "nsIMsgFolderNotificationService.h" +#include "nsIMimeHeaders.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIMsgTraitService.h" +#include "nsIMessenger.h" +#include "nsThreadUtils.h" +#include "nsITransactionManager.h" +#include "nsMsgReadStateTxn.h" +#include "prmem.h" +#include "nsIPK11TokenDB.h" +#include "nsIPK11Token.h" +#include "nsMsgLocalFolderHdrs.h" +#define oneHour 3600000000U +#include "nsMsgUtils.h" +#include "nsIMsgFilterService.h" +#include "nsDirectoryServiceUtils.h" +#include "nsMimeTypes.h" +#include "nsIMsgFilter.h" +#include "nsIScriptError.h" +#include "nsIURIMutator.h" +#include "nsIXULAppInfo.h" +#include "nsPrintfCString.h" +#include "mozilla/Components.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/Logging.h" +#include "mozilla/SlicedInputStream.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Utf8.h" +#include "nsIPromptService.h" +#include "nsEmbedCID.h" + +using namespace mozilla; + +extern LazyLogModule FILTERLOGMODULE; +extern LazyLogModule DBLog; + +static PRTime gtimeOfLastPurgeCheck; // variable to know when to check for + // purge threshold + +#define PREF_MAIL_PROMPT_PURGE_THRESHOLD "mail.prompt_purge_threshhold" +#define PREF_MAIL_PURGE_THRESHOLD "mail.purge_threshhold" +#define PREF_MAIL_PURGE_THRESHOLD_MB "mail.purge_threshhold_mb" +#define PREF_MAIL_PURGE_MIGRATED "mail.purge_threshold_migrated" +#define PREF_MAIL_PURGE_ASK "mail.purge.ask" +#define PREF_MAIL_WARN_FILTER_CHANGED "mail.warn_filter_changed" + +const char* kUseServerRetentionProp = "useServerRetention"; + +/** + * mozilla::intl APIs require sizeable buffers. This class abstracts over + * the nsTArray. + */ +class nsTArrayU8Buffer { + public: + using CharType = uint8_t; + + // Do not allow copy or move. Move could be added in the future if needed. + nsTArrayU8Buffer(const nsTArrayU8Buffer&) = delete; + nsTArrayU8Buffer& operator=(const nsTArrayU8Buffer&) = delete; + + explicit nsTArrayU8Buffer(nsTArray<CharType>& aArray) : mArray(aArray) {} + + /** + * Ensures the buffer has enough space to accommodate |size| elements. + */ + [[nodiscard]] bool reserve(size_t size) { + mArray.SetCapacity(size); + // nsTArray::SetCapacity returns void, return true to keep the API the same + // as the other Buffer implementations. + return true; + } + + /** + * Returns the raw data inside the buffer. + */ + CharType* data() { return mArray.Elements(); } + + /** + * Returns the count of elements written into the buffer. + */ + size_t length() const { return mArray.Length(); } + + /** + * Returns the buffer's overall capacity. + */ + size_t capacity() const { return mArray.Capacity(); } + + /** + * Resizes the buffer to the given amount of written elements. + */ + void written(size_t amount) { + MOZ_ASSERT(amount <= mArray.Capacity()); + // This sets |mArray|'s internal size so that it matches how much was + // written. This is necessary because the write happens across FFI + // boundaries. + mArray.SetLengthAndRetainStorage(amount); + } + + private: + nsTArray<CharType>& mArray; +}; + +NS_IMPL_ISUPPORTS(nsMsgFolderService, nsIMsgFolderService) + +// This method serves the only purpose to re-initialize the +// folder name strings when UI initialization is done. +// XXX TODO: This can be removed when the localization system gets +// initialized in M-C code before, for example, the permission manager +// triggers folder creation during imap: URI creation. +// In fact, the entire class together with nsMsgDBFolder::FolderNamesReady() +// can be removed. +NS_IMETHODIMP nsMsgFolderService::InitializeFolderStrings() { + nsMsgDBFolder::initializeStrings(); + nsMsgDBFolder::gInitializeStringsDone = true; + nsMsgDBFolder::gIsEnglishApp = -1; + return NS_OK; +} + +mozilla::UniquePtr<mozilla::intl::Collator> + nsMsgDBFolder::gCollationKeyGenerator = nullptr; + +nsString nsMsgDBFolder::kLocalizedInboxName; +nsString nsMsgDBFolder::kLocalizedTrashName; +nsString nsMsgDBFolder::kLocalizedSentName; +nsString nsMsgDBFolder::kLocalizedDraftsName; +nsString nsMsgDBFolder::kLocalizedTemplatesName; +nsString nsMsgDBFolder::kLocalizedUnsentName; +nsString nsMsgDBFolder::kLocalizedJunkName; +nsString nsMsgDBFolder::kLocalizedArchivesName; + +nsString nsMsgDBFolder::kLocalizedBrandShortName; + +nsrefcnt nsMsgDBFolder::mInstanceCount = 0; +bool nsMsgDBFolder::gInitializeStringsDone = false; +// This is used in `nonEnglishApp()` to determine localised +// folders strings. +// -1: not retrieved yet, 1: English, 0: non-English. +int nsMsgDBFolder::gIsEnglishApp; + +// We define strings for folder properties and events. +// Properties: +constexpr nsLiteralCString kBiffState = "BiffState"_ns; +constexpr nsLiteralCString kCanFileMessages = "CanFileMessages"_ns; +constexpr nsLiteralCString kDefaultServer = "DefaultServer"_ns; +constexpr nsLiteralCString kFlagged = "Flagged"_ns; +constexpr nsLiteralCString kFolderFlag = "FolderFlag"_ns; +constexpr nsLiteralCString kFolderSize = "FolderSize"_ns; +constexpr nsLiteralCString kIsDeferred = "isDeferred"_ns; +constexpr nsLiteralCString kIsSecure = "isSecure"_ns; +constexpr nsLiteralCString kJunkStatusChanged = "JunkStatusChanged"_ns; +constexpr nsLiteralCString kKeywords = "Keywords"_ns; +constexpr nsLiteralCString kMRMTimeChanged = "MRMTimeChanged"_ns; +constexpr nsLiteralCString kMsgLoaded = "msgLoaded"_ns; +constexpr nsLiteralCString kName = "Name"_ns; +constexpr nsLiteralCString kNewMailReceived = "NewMailReceived"_ns; +constexpr nsLiteralCString kNewMessages = "NewMessages"_ns; +constexpr nsLiteralCString kOpen = "open"_ns; +constexpr nsLiteralCString kSortOrder = "SortOrder"_ns; +constexpr nsLiteralCString kStatus = "Status"_ns; +constexpr nsLiteralCString kSynchronize = "Synchronize"_ns; +constexpr nsLiteralCString kTotalMessages = "TotalMessages"_ns; +constexpr nsLiteralCString kTotalUnreadMessages = "TotalUnreadMessages"_ns; + +// Events: +constexpr nsLiteralCString kAboutToCompact = "AboutToCompact"_ns; +constexpr nsLiteralCString kCompactCompleted = "CompactCompleted"_ns; +constexpr nsLiteralCString kDeleteOrMoveMsgCompleted = + "DeleteOrMoveMsgCompleted"_ns; +constexpr nsLiteralCString kDeleteOrMoveMsgFailed = "DeleteOrMoveMsgFailed"_ns; +constexpr nsLiteralCString kFiltersApplied = "FiltersApplied"_ns; +constexpr nsLiteralCString kFolderCreateCompleted = "FolderCreateCompleted"_ns; +constexpr nsLiteralCString kFolderCreateFailed = "FolderCreateFailed"_ns; +constexpr nsLiteralCString kFolderLoaded = "FolderLoaded"_ns; +constexpr nsLiteralCString kNumNewBiffMessages = "NumNewBiffMessages"_ns; +constexpr nsLiteralCString kRenameCompleted = "RenameCompleted"_ns; + +NS_IMPL_ISUPPORTS(nsMsgDBFolder, nsISupportsWeakReference, nsIMsgFolder, + nsIDBChangeListener, nsIUrlListener, + nsIJunkMailClassificationListener, + nsIMsgTraitClassificationListener) + +nsMsgDBFolder::nsMsgDBFolder(void) + : mAddListener(true), + mNewMessages(false), + mGettingNewMessages(false), + mLastMessageLoaded(nsMsgKey_None), + m_numOfflineMsgLines(0), + m_bytesAddedToLocalMsg(0), + m_tempMessageStreamBytesWritten(0), + mFlags(0), + mNumUnreadMessages(-1), + mNumTotalMessages(-1), + mNotifyCountChanges(true), + mExpungedBytes(0), + mInitializedFromCache(false), + mSemaphoreHolder(nullptr), + mNumPendingUnreadMessages(0), + mNumPendingTotalMessages(0), + mFolderSize(kSizeUnknown), + mNumNewBiffMessages(0), + mHaveParsedURI(false), + mIsServerIsValid(false), + mIsServer(false), + mBayesJunkClassifying(false), + mBayesTraitClassifying(false) { + if (mInstanceCount++ <= 0) { + initializeStrings(); + + do { + nsresult rv; + // We need to check whether we're running under xpcshell, + // in that case, we always assume that the strings are good. + // XXX TODO: This hack can be removed when the localization system gets + // initialized in M-C code before, for example, the permission manager + // triggers folder creation during imap: URI creation. + nsCOMPtr<nsIXULAppInfo> appinfo = + do_GetService("@mozilla.org/xre/app-info;1", &rv); + if (NS_FAILED(rv)) break; + nsAutoCString appName; + rv = appinfo->GetName(appName); + if (NS_FAILED(rv)) break; + if (appName.Equals("xpcshell")) gInitializeStringsDone = true; + } while (false); + + createCollationKeyGenerator(); + gtimeOfLastPurgeCheck = 0; + } + + mProcessingFlag[0].bit = nsMsgProcessingFlags::ClassifyJunk; + mProcessingFlag[1].bit = nsMsgProcessingFlags::ClassifyTraits; + mProcessingFlag[2].bit = nsMsgProcessingFlags::TraitsDone; + mProcessingFlag[3].bit = nsMsgProcessingFlags::FiltersDone; + mProcessingFlag[4].bit = nsMsgProcessingFlags::FilterToMove; + mProcessingFlag[5].bit = nsMsgProcessingFlags::NotReportedClassified; + for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++) + mProcessingFlag[i].keys = nsMsgKeySetU::Create(); +} + +nsMsgDBFolder::~nsMsgDBFolder(void) { + for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++) + delete mProcessingFlag[i].keys; + + if (--mInstanceCount == 0) { + nsMsgDBFolder::gCollationKeyGenerator = nullptr; + } + // shutdown but don't shutdown children. + Shutdown(false); +} + +NS_IMETHODIMP nsMsgDBFolder::FolderNamesReady(bool* aReady) { + *aReady = gInitializeStringsDone; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::Shutdown(bool shutdownChildren) { + if (mDatabase) { + mDatabase->RemoveListener(this); + mDatabase->ForceClosed(); + mDatabase = nullptr; + if (mBackupDatabase) { + mBackupDatabase->ForceClosed(); + mBackupDatabase = nullptr; + } + } + + if (shutdownChildren) { + int32_t count = mSubFolders.Count(); + + for (int32_t i = 0; i < count; i++) mSubFolders[i]->Shutdown(true); + + // Reset incoming server pointer and pathname. + mServer = nullptr; + mPath = nullptr; + mHaveParsedURI = false; + mName.Truncate(); + mSubFolders.Clear(); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::ForceDBClosed() { + int32_t count = mSubFolders.Count(); + for (int32_t i = 0; i < count; i++) mSubFolders[i]->ForceDBClosed(); + + if (mDatabase) { + mDatabase->ForceClosed(); + mDatabase = nullptr; + } else { + nsCOMPtr<nsIMsgDBService> mailDBFactory( + do_GetService("@mozilla.org/msgDatabase/msgDBService;1")); + if (mailDBFactory) mailDBFactory->ForceFolderDBClosed(this); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::CloseAndBackupFolderDB(const nsACString& newName) { + ForceDBClosed(); + + // We only support backup for mail at the moment + if (!(mFlags & nsMsgFolderFlags::Mail)) return NS_OK; + + nsCOMPtr<nsIFile> folderPath; + nsresult rv = GetFilePath(getter_AddRefs(folderPath)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> dbFile; + rv = GetSummaryFileLocation(folderPath, getter_AddRefs(dbFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> backupDir; + rv = CreateBackupDirectory(getter_AddRefs(backupDir)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> backupDBFile; + rv = GetBackupSummaryFile(getter_AddRefs(backupDBFile), newName); + NS_ENSURE_SUCCESS(rv, rv); + + if (mBackupDatabase) { + mBackupDatabase->ForceClosed(); + mBackupDatabase = nullptr; + } + + backupDBFile->Remove(false); + bool backupExists; + backupDBFile->Exists(&backupExists); + NS_ASSERTION(!backupExists, "Couldn't delete database backup"); + if (backupExists) return NS_ERROR_FAILURE; + + if (!newName.IsEmpty()) { + nsAutoCString backupName; + rv = backupDBFile->GetNativeLeafName(backupName); + NS_ENSURE_SUCCESS(rv, rv); + return dbFile->CopyToNative(backupDir, backupName); + } else + return dbFile->CopyToNative(backupDir, EmptyCString()); +} + +NS_IMETHODIMP nsMsgDBFolder::OpenBackupMsgDatabase() { + if (mBackupDatabase) return NS_OK; + nsCOMPtr<nsIFile> folderPath; + nsresult rv = GetFilePath(getter_AddRefs(folderPath)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString folderName; + rv = folderPath->GetLeafName(folderName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> backupDir; + rv = CreateBackupDirectory(getter_AddRefs(backupDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // We use a dummy message folder file so we can use + // GetSummaryFileLocation to get the db file name + nsCOMPtr<nsIFile> backupDBDummyFolder; + rv = CreateBackupDirectory(getter_AddRefs(backupDBDummyFolder)); + NS_ENSURE_SUCCESS(rv, rv); + rv = backupDBDummyFolder->Append(folderName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = msgDBService->OpenMailDBFromFile(backupDBDummyFolder, this, false, true, + getter_AddRefs(mBackupDatabase)); + // we add a listener so that we can close the db during OnAnnouncerGoingAway. + // There should not be any other calls to the listener with the backup + // database + if (NS_SUCCEEDED(rv) && mBackupDatabase) mBackupDatabase->AddListener(this); + + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) + // this is normal in reparsing + rv = NS_OK; + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::RemoveBackupMsgDatabase() { + nsCOMPtr<nsIFile> folderPath; + nsresult rv = GetFilePath(getter_AddRefs(folderPath)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString folderName; + rv = folderPath->GetLeafName(folderName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> backupDir; + rv = CreateBackupDirectory(getter_AddRefs(backupDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // We use a dummy message folder file so we can use + // GetSummaryFileLocation to get the db file name + nsCOMPtr<nsIFile> backupDBDummyFolder; + rv = CreateBackupDirectory(getter_AddRefs(backupDBDummyFolder)); + NS_ENSURE_SUCCESS(rv, rv); + rv = backupDBDummyFolder->Append(folderName); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> backupDBFile; + rv = + GetSummaryFileLocation(backupDBDummyFolder, getter_AddRefs(backupDBFile)); + NS_ENSURE_SUCCESS(rv, rv); + + if (mBackupDatabase) { + mBackupDatabase->ForceClosed(); + mBackupDatabase = nullptr; + } + + return backupDBFile->Remove(false); +} + +NS_IMETHODIMP nsMsgDBFolder::StartFolderLoading(void) { + if (mDatabase) mDatabase->RemoveListener(this); + mAddListener = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::EndFolderLoading(void) { + if (mDatabase) mDatabase->AddListener(this); + mAddListener = true; + UpdateSummaryTotals(true); + + // GGGG check for new mail here and call SetNewMessages...?? -- ONE OF + // THE 2 PLACES + if (mDatabase) m_newMsgs.Clear(); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetExpungedBytes(int64_t* count) { + NS_ENSURE_ARG_POINTER(count); + + if (mDatabase) { + nsresult rv; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + rv = mDatabase->GetDBFolderInfo(getter_AddRefs(folderInfo)); + if (NS_FAILED(rv)) return rv; + rv = folderInfo->GetExpungedBytes(count); + if (NS_SUCCEEDED(rv)) mExpungedBytes = *count; // sync up with the database + return rv; + } else { + ReadDBFolderInfo(false); + *count = mExpungedBytes; + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetHasNewMessages(bool* hasNewMessages) { + NS_ENSURE_ARG_POINTER(hasNewMessages); + *hasNewMessages = mNewMessages; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::SetHasNewMessages(bool curNewMessages) { + if (curNewMessages != mNewMessages) { + // Only change mru time if we're going from doesn't have new to has new. + // technically, we should probably update mru time for every new message + // but we would pay a performance penalty for that. If the user + // opens the folder, the mrutime will get updated anyway. + if (curNewMessages) SetMRUTime(); + bool oldNewMessages = mNewMessages; + mNewMessages = curNewMessages; + NotifyBoolPropertyChanged(kNewMessages, oldNewMessages, curNewMessages); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetHasFolderOrSubfolderNewMessages(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + bool hasNewMessages = mNewMessages; + + if (!hasNewMessages) { + int32_t count = mSubFolders.Count(); + for (int32_t i = 0; i < count; i++) { + bool hasNew = false; + mSubFolders[i]->GetHasFolderOrSubfolderNewMessages(&hasNew); + if (hasNew) { + hasNewMessages = true; + break; + } + } + } + + *aResult = hasNewMessages; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetGettingNewMessages(bool* gettingNewMessages) { + NS_ENSURE_ARG_POINTER(gettingNewMessages); + *gettingNewMessages = mGettingNewMessages; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::SetGettingNewMessages(bool gettingNewMessages) { + mGettingNewMessages = gettingNewMessages; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetFirstNewMessage(nsIMsgDBHdr** firstNewMessage) { + // If there's not a db then there can't be new messages. Return failure since + // you should use HasNewMessages first. + if (!mDatabase) return NS_ERROR_FAILURE; + + nsresult rv; + nsMsgKey key; + rv = mDatabase->GetFirstNew(&key); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(hdr)); + if (NS_FAILED(rv)) return rv; + + return mDatabase->GetMsgHdrForKey(key, firstNewMessage); +} + +NS_IMETHODIMP nsMsgDBFolder::ClearNewMessages() { + nsresult rv = NS_OK; + bool dbWasCached = mDatabase != nullptr; + if (!dbWasCached) GetDatabase(); + + if (mDatabase) { + mDatabase->GetNewList(m_saveNewMsgs); + mDatabase->ClearNewList(true); + } + if (!dbWasCached) SetMsgDatabase(nullptr); + + m_newMsgs.Clear(); + mNumNewBiffMessages = 0; + return rv; +} + +void nsMsgDBFolder::UpdateNewMessages() { + if (!(mFlags & nsMsgFolderFlags::Virtual)) { + bool hasNewMessages = false; + for (uint32_t keyIndex = 0; keyIndex < m_newMsgs.Length(); keyIndex++) { + bool containsKey = false; + mDatabase->ContainsKey(m_newMsgs[keyIndex], &containsKey); + if (!containsKey) continue; + bool isRead = false; + nsresult rv2 = mDatabase->IsRead(m_newMsgs[keyIndex], &isRead); + if (NS_SUCCEEDED(rv2) && !isRead) { + hasNewMessages = true; + mDatabase->AddToNewList(m_newMsgs[keyIndex]); + } + } + SetHasNewMessages(hasNewMessages); + } +} + +// helper function that gets the cache element that corresponds to the passed in +// file spec. This could be static, or could live in another class - it's not +// specific to the current nsMsgDBFolder. If it lived at a higher level, we +// could cache the account manager and folder cache. +nsresult nsMsgDBFolder::GetFolderCacheElemFromFile( + nsIFile* file, nsIMsgFolderCacheElement** cacheElement) { + nsresult result; + NS_ENSURE_ARG_POINTER(file); + NS_ENSURE_ARG_POINTER(cacheElement); + nsCOMPtr<nsIMsgFolderCache> folderCache; +#ifdef DEBUG_bienvenu1 + bool exists; + NS_ASSERTION(NS_SUCCEEDED(fileSpec->Exists(&exists)) && exists, + "whoops, file doesn't exist, mac will break"); +#endif + nsCOMPtr<nsIMsgAccountManager> accountMgr = + do_GetService("@mozilla.org/messenger/account-manager;1", &result); + if (NS_SUCCEEDED(result)) { + result = accountMgr->GetFolderCache(getter_AddRefs(folderCache)); + if (NS_SUCCEEDED(result) && folderCache) { + nsCString persistentPath; + result = file->GetPersistentDescriptor(persistentPath); + NS_ENSURE_SUCCESS(result, result); + result = + folderCache->GetCacheElement(persistentPath, false, cacheElement); + } + } + return result; +} + +nsresult nsMsgDBFolder::ReadDBFolderInfo(bool force) { + // Since it turns out to be pretty expensive to open and close + // the DBs all the time, if we have to open it once, get everything + // we might need while we're here + nsresult result = NS_OK; + + // If we reload the cache we might get stale info, so don't do it. + if (!mInitializedFromCache) { + // Path is used as a key into the foldercache. + nsCOMPtr<nsIFile> dbPath; + result = GetFolderCacheKey(getter_AddRefs(dbPath)); + if (dbPath) { + nsCOMPtr<nsIMsgFolderCacheElement> cacheElement; + result = GetFolderCacheElemFromFile(dbPath, getter_AddRefs(cacheElement)); + if (NS_SUCCEEDED(result) && cacheElement) { + if (NS_SUCCEEDED(ReadFromFolderCacheElem(cacheElement))) { + mInitializedFromCache = true; + } + } + } + } + + if (force || !mInitializedFromCache) { + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> db; + bool weOpenedDB = !mDatabase; + result = + GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db)); + if (NS_SUCCEEDED(result)) { + if (folderInfo) { + if (!mInitializedFromCache) { + folderInfo->GetFlags((int32_t*)&mFlags); +#ifdef DEBUG_bienvenu1 + nsString name; + GetName(name); + NS_ASSERTION(Compare(name, kLocalizedTrashName) || + (mFlags & nsMsgFolderFlags::Trash), + "lost trash flag"); +#endif + mInitializedFromCache = true; + } + + folderInfo->GetNumMessages(&mNumTotalMessages); + folderInfo->GetNumUnreadMessages(&mNumUnreadMessages); + folderInfo->GetExpungedBytes(&mExpungedBytes); + + nsCString utf8Name; + folderInfo->GetFolderName(utf8Name); + if (!utf8Name.IsEmpty()) CopyUTF8toUTF16(utf8Name, mName); + + // These should be put in IMAP folder only. + // folderInfo->GetImapTotalPendingMessages(&mNumPendingTotalMessages); + // folderInfo->GetImapUnreadPendingMessages(&mNumPendingUnreadMessages); + + if (db) { + bool hasnew; + nsresult rv; + rv = db->HasNew(&hasnew); + if (NS_FAILED(rv)) return rv; + } + if (weOpenedDB) CloseDBIfFolderNotOpen(false); + } + } else { + // we tried to open DB but failed - don't keep trying. + // If a DB is created, we will call this method with force == TRUE, + // and read from the db that way. + mInitializedFromCache = true; + } + } + return result; +} + +nsresult nsMsgDBFolder::SendFlagNotifications(nsIMsgDBHdr* item, + uint32_t oldFlags, + uint32_t newFlags) { + nsresult rv = NS_OK; + uint32_t changedFlags = oldFlags ^ newFlags; + if ((changedFlags & nsMsgMessageFlags::Read) && + (changedFlags & nsMsgMessageFlags::New)) { + //..so..if the msg is read in the folder and the folder has new msgs clear + // the account level and status bar biffs. + rv = NotifyPropertyFlagChanged(item, kStatus, oldFlags, newFlags); + rv = SetBiffState(nsMsgBiffState_NoMail); + } else if (changedFlags & + (nsMsgMessageFlags::Read | nsMsgMessageFlags::Replied | + nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::IMAPDeleted | + nsMsgMessageFlags::New | nsMsgMessageFlags::Offline)) + rv = NotifyPropertyFlagChanged(item, kStatus, oldFlags, newFlags); + else if ((changedFlags & nsMsgMessageFlags::Marked)) + rv = NotifyPropertyFlagChanged(item, kFlagged, oldFlags, newFlags); + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::DownloadMessagesForOffline( + nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgWindow*) { + NS_ASSERTION(false, "imap and news need to override this"); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::DownloadAllForOffline(nsIUrlListener* listener, + nsIMsgWindow* msgWindow) { + NS_ASSERTION(false, "imap and news need to override this"); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetMsgStore(nsIMsgPluggableStore** aStore) { + NS_ENSURE_ARG_POINTER(aStore); + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER); + return server->GetMsgStore(aStore); +} + +nsresult nsMsgDBFolder::GetOfflineFileStream(nsMsgKey msgKey, uint64_t* offset, + uint32_t* size, + nsIInputStream** aFileStream) { + NS_ENSURE_ARG(aFileStream); + + *offset = 0; + *size = 0; + + // Initialise to nullptr since this is checked by some callers for the success + // of the function. + *aFileStream = nullptr; + + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + hdr->GetOfflineMessageSize(size); + NS_ENSURE_TRUE(*size != 0, NS_ERROR_UNEXPECTED); + rv = GetMsgInputStream(hdr, aFileStream); + if (NS_FAILED(rv)) { + NS_WARNING(nsPrintfCString( + "(debug) nsMsgDBFolder::GetOfflineFileStream: " + "GetMsgInputStream(hdr, aFileStream); rv=0x%" PRIx32 "\n", + static_cast<uint32_t>(rv)) + .get()); + // Return early: If we could not find an offline stream, clear the offline + // flag which will fall back to reading the message from the server. + if (mDatabase) mDatabase->MarkOffline(msgKey, false, nullptr); + + return rv; + } + + // Check if the database has the correct offset into the offline store by + // reading up to 300 bytes. If it is incorrect, clear the offline flag on the + // message and return false. This will cause a fall back to reading the + // message from the server. We will also advance the offset past the envelope + // header ("From " or "FCC") and "X-Mozilla-Status*" lines so these line are + // not included when the message is read from the file. + // Note: This occurs for both mbox and maildir offline store and probably any + // future pluggable store that may be supported. + nsCOMPtr<nsISeekableStream> seekableStream = do_QueryInterface(*aFileStream); + if (seekableStream) { + int64_t o; + seekableStream->Tell(&o); + *offset = uint64_t(o); + char startOfMsg[301]; + uint32_t bytesRead = 0; + uint32_t bytesToRead = sizeof(startOfMsg) - 1; + rv = (*aFileStream)->Read(startOfMsg, bytesToRead, &bytesRead); + startOfMsg[bytesRead] = '\0'; + uint32_t msgOffset = 0; + uint32_t keepMsgOffset = 0; + char* headerLine = startOfMsg; + int32_t findPos; + // Check a few lines in startOfMsg[] to verify message record validity. + bool line1 = true; + bool foundError = false; + // If Read() above fails, don't check any lines and set record bad. + // Even if Read() succeeds, don't enter the loop below if bytesRead is 0. + bool foundNextLine = NS_SUCCEEDED(rv) && (bytesRead > 0) ? true : false; + while (foundNextLine) { + headerLine = startOfMsg + msgOffset; + // Ignore lines beginning X-Mozilla-Status or X-Mozilla-Status2 + if (!strncmp(headerLine, X_MOZILLA_STATUS, X_MOZILLA_STATUS_LEN) || + !strncmp(headerLine, X_MOZILLA_STATUS2, X_MOZILLA_STATUS2_LEN)) { + // If there is an invalid line ahead of X-Mozilla-Status lines, + // immediately flag this a bad record. Only the "From " or "FCC" + // delimiter line is expected and OK before this. + if (foundError) { + break; + } + foundNextLine = + MsgAdvanceToNextLine(startOfMsg, msgOffset, bytesRead - 1); + line1 = false; + continue; + } + if (line1) { + // Ignore "From " and, for Drafts, "FCC" when on first line. + if ((!strncmp(headerLine, "From ", 5) || + ((mFlags & nsMsgFolderFlags::Drafts) && + !strncmp(headerLine, "FCC", 3)))) { + foundNextLine = + MsgAdvanceToNextLine(startOfMsg, msgOffset, bytesRead - 1); + line1 = false; + continue; + } + } + bool validOrFrom = false; + // Check if line looks like a valid header (just check for a colon). Also + // a line beginning with "From " as is sometimes returned by broken IMAP + // servers is also acceptable. Also, size of message must be greater than + // the offset of the first header line into the message (msgOffset). + findPos = MsgFindCharInSet(nsDependentCString(headerLine), ":\n\r", 0); + if (((findPos != kNotFound && headerLine[findPos] == ':') || + !strncmp(headerLine, "From ", 5)) && + *size > msgOffset) { + validOrFrom = true; + } + if (!foundError) { + if (validOrFrom) { + // Record looks OK, accept it. + break; + } else { + foundError = true; + keepMsgOffset = msgOffset; + foundNextLine = + MsgAdvanceToNextLine(startOfMsg, msgOffset, bytesRead - 1); + if (MOZ_LOG_TEST(DBLog, LogLevel::Info)) { + char save; + if (foundNextLine) { + // Temporarily null terminate the bad header line for logging. + save = startOfMsg[msgOffset - 1]; // should be \r or \n + startOfMsg[msgOffset - 1] = 0; + + // DEBUG: the error happened while checking network outage + // condition. + // XXX TODO We may need to check if dovecot and other imap + // servers are returning funny "From " line, etc. + NS_WARNING( + nsPrintfCString("Strange startOfMsg: |%s|\n", startOfMsg) + .get()); + } + MOZ_LOG(DBLog, LogLevel::Info, + ("Invalid header line in offline store: %s", + startOfMsg + keepMsgOffset)); + if (foundNextLine) startOfMsg[msgOffset - 1] = save; + } + line1 = false; + continue; + } + } else { + if (validOrFrom) { + // Previous was bad, this is good, accept the record at bad line. + foundError = false; + msgOffset = keepMsgOffset; + break; + } + // If reached, two consecutive lines bad, reject the record + break; + } + } // while (foundNextLine) + + if (!foundNextLine) { + // Can't find a valid header line in the buffer or buffer read() failed. + foundError = true; + } + + if (!foundError) { + *offset += msgOffset; + *size -= msgOffset; + seekableStream->Seek(nsISeekableStream::NS_SEEK_SET, *offset); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } else { + // Offline store message record looks bad. Cause message fetch from + // server and store in RAM cache. + MOZ_ASSERT(mDatabase); // Would have crashed above if mDatabase null! + mDatabase->MarkOffline(msgKey, false, nullptr); + MOZ_LOG(DBLog, LogLevel::Error, + ("Leading offline store file content appears invalid, will fetch " + "message from server.")); + MOZ_LOG( + DBLog, LogLevel::Error, + ("First 300 bytes of offline store content are:\n%s", startOfMsg)); + rv = NS_ERROR_FAILURE; + } + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::GetLocalMsgStream(nsIMsgDBHdr* hdr, + nsIInputStream** stream) { + // Eventually this will be purely a matter of fetching the storeToken + // from the header and asking the msgStore for an inputstream. + // But for now, the InputStream returned by the mbox msgStore doesn't + // EOF at the end of the message, so we need to jump through hoops here. + // For now, we implement it in the derived classes, as the message size + // is stored in different msgHdr attributes depending on folder type. + // See Bug 1764857. + // Also, IMAP has a gmail hack to work with (multiple msgHdrs referrring + // to the same locally-stored message). + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetOfflineStoreOutputStream(nsIMsgDBHdr* aHdr, + nsIOutputStream** aOutputStream) { + NS_ENSURE_ARG_POINTER(aOutputStream); + NS_ENSURE_ARG_POINTER(aHdr); + + nsCOMPtr<nsIMsgPluggableStore> offlineStore; + nsresult rv = GetMsgStore(getter_AddRefs(offlineStore)); + NS_ENSURE_SUCCESS(rv, rv); + rv = offlineStore->GetNewMsgOutputStream(this, &aHdr, aOutputStream); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetMsgInputStream(nsIMsgDBHdr* aMsgHdr, + nsIInputStream** aInputStream) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + NS_ENSURE_ARG_POINTER(aInputStream); + nsCOMPtr<nsIMsgPluggableStore> msgStore; + nsresult rv = GetMsgStore(getter_AddRefs(msgStore)); + NS_ENSURE_SUCCESS(rv, rv); + nsCString storeToken; + rv = aMsgHdr->GetStringProperty("storeToken", storeToken); + NS_ENSURE_SUCCESS(rv, rv); + + // Handle legacy DB which has mbox offset but no storeToken. + // If this is still needed (open question), it should be done as separate + // migration pass, probably at folder creation when store and DB are set + // up (but that's tricky at the moment, because the DB is created + // on-demand). + if (storeToken.IsEmpty()) { + nsAutoCString storeType; + msgStore->GetStoreType(storeType); + if (!storeType.EqualsLiteral("mbox")) { + return NS_ERROR_FAILURE; // DB is missing storeToken. + } + uint64_t offset; + aMsgHdr->GetMessageOffset(&offset); + storeToken = nsPrintfCString("%" PRIu64, offset); + rv = aMsgHdr->SetStringProperty("storeToken", storeToken); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = msgStore->GetMsgInputStream(this, storeToken, aInputStream); + if (NS_FAILED(rv)) { + NS_WARNING(nsPrintfCString( + "(debug) nsMsgDBFolder::GetMsgInputStream: msgStore->" + "GetMsgInputStream(this, ...) returned error rv=0x%" PRIx32 + "\n", + static_cast<uint32_t>(rv)) + .get()); + } + + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +// path coming in is the root path without the leaf name, +// on the way out, it's the whole path. +nsresult nsMsgDBFolder::CreateFileForDB(const nsAString& userLeafName, + nsIFile* path, nsIFile** dbFile) { + NS_ENSURE_ARG_POINTER(dbFile); + + nsAutoString proposedDBName(userLeafName); + NS_MsgHashIfNecessary(proposedDBName); + + // (note, the caller of this will be using the dbFile to call db->Open() + // will turn the path into summary file path, and append the ".msf" extension) + // + // we want db->Open() to create a new summary file + // so we have to jump through some hoops to make sure the .msf it will + // create is unique. now that we've got the "safe" proposedDBName, + // we append ".msf" to see if the file exists. if so, we make the name + // unique and then string off the ".msf" so that we pass the right thing + // into Open(). this isn't ideal, since this is not atomic + // but it will make do. + nsresult rv; + nsCOMPtr<nsIFile> dbPath = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + dbPath->InitWithFile(path); + proposedDBName.AppendLiteral(SUMMARY_SUFFIX); + dbPath->Append(proposedDBName); + bool exists; + dbPath->Exists(&exists); + if (exists) { + rv = dbPath->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600); + NS_ENSURE_SUCCESS(rv, rv); + dbPath->GetLeafName(proposedDBName); + } + // now, take the ".msf" off + proposedDBName.SetLength(proposedDBName.Length() - SUMMARY_SUFFIX_LENGTH); + dbPath->SetLeafName(proposedDBName); + + dbPath.forget(dbFile); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetMsgDatabase(nsIMsgDatabase** aMsgDatabase) { + NS_ENSURE_ARG_POINTER(aMsgDatabase); + GetDatabase(); + if (!mDatabase) return NS_ERROR_FAILURE; + NS_ADDREF(*aMsgDatabase = mDatabase); + mDatabase->SetLastUseTime(PR_Now()); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::SetMsgDatabase(nsIMsgDatabase* aMsgDatabase) { + if (mDatabase) { + // commit here - db might go away when all these refs are released. + mDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + mDatabase->RemoveListener(this); + mDatabase->ClearCachedHdrs(); + if (!aMsgDatabase) { + mDatabase->GetNewList(m_newMsgs); + } + } + mDatabase = aMsgDatabase; + + if (aMsgDatabase) aMsgDatabase->AddListener(this); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetDatabaseOpen(bool* aOpen) { + NS_ENSURE_ARG_POINTER(aOpen); + + *aOpen = (mDatabase != nullptr); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetBackupMsgDatabase(nsIMsgDatabase** aMsgDatabase) { + NS_ENSURE_ARG_POINTER(aMsgDatabase); + nsresult rv = OpenBackupMsgDatabase(); + if (NS_FAILED(rv)) { + NS_WARNING(nsPrintfCString( + "(debug) OpenBackupMsgDatabase(); returns error=0x%" PRIx32 + "\n", + static_cast<uint32_t>(rv)) + .get()); + } + NS_ENSURE_SUCCESS(rv, rv); + if (!mBackupDatabase) return NS_ERROR_FAILURE; + + NS_ADDREF(*aMsgDatabase = mBackupDatabase); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo, + nsIMsgDatabase** database) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBFolder::OnReadChanged(nsIDBChangeListener* aInstigator) { + /* do nothing. if you care about this, override it. see nsNewsFolder.cpp */ + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::OnJunkScoreChanged(nsIDBChangeListener* aInstigator) { + NotifyFolderEvent(kJunkStatusChanged); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange, + const nsACString& property, bool aPreChange, + uint32_t* aStatus, + nsIDBChangeListener* aInstigator) { + /* do nothing. if you care about this, override it.*/ + return NS_OK; +} + +// 1. When the status of a message changes. +NS_IMETHODIMP nsMsgDBFolder::OnHdrFlagsChanged( + nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) { + if (aHdrChanged) { + SendFlagNotifications(aHdrChanged, aOldFlags, aNewFlags); + UpdateSummaryTotals(true); + } + + // The old state was new message state + // We check and see if this state has changed + if (aOldFlags & nsMsgMessageFlags::New) { + // state changing from new to something else + if (!(aNewFlags & nsMsgMessageFlags::New)) + CheckWithNewMessagesStatus(false); + } + + return NS_OK; +} + +nsresult nsMsgDBFolder::CheckWithNewMessagesStatus(bool messageAdded) { + bool hasNewMessages; + if (messageAdded) + SetHasNewMessages(true); + else // message modified or deleted + { + if (mDatabase) { + nsresult rv = mDatabase->HasNew(&hasNewMessages); + NS_ENSURE_SUCCESS(rv, rv); + SetHasNewMessages(hasNewMessages); + } + } + + return NS_OK; +} + +// 3. When a message gets deleted, we need to see if it was new +// When we lose a new message we need to check if there are still new +// messages +NS_IMETHODIMP nsMsgDBFolder::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged, + nsMsgKey aParentKey, int32_t aFlags, + nsIDBChangeListener* aInstigator) { + // check to see if a new message is being deleted + // as in this case, if there is only one new message and it's being deleted + // the folder newness has to be cleared. + CheckWithNewMessagesStatus(false); + // Remove all processing flags. This is generally a good thing although + // undo-ing a message back into position will not re-gain the flags. + nsMsgKey msgKey; + aHdrChanged->GetMessageKey(&msgKey); + AndProcessingFlags(msgKey, 0); + return OnHdrAddedOrDeleted(aHdrChanged, false); +} + +// 2. When a new messages gets added, we need to see if it's new. +NS_IMETHODIMP nsMsgDBFolder::OnHdrAdded(nsIMsgDBHdr* aHdrChanged, + nsMsgKey aParentKey, int32_t aFlags, + nsIDBChangeListener* aInstigator) { + if (aFlags & nsMsgMessageFlags::New) CheckWithNewMessagesStatus(true); + return OnHdrAddedOrDeleted(aHdrChanged, true); +} + +nsresult nsMsgDBFolder::OnHdrAddedOrDeleted(nsIMsgDBHdr* aHdrChanged, + bool added) { + if (added) + NotifyMessageAdded(aHdrChanged); + else + NotifyMessageRemoved(aHdrChanged); + UpdateSummaryTotals(true); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::OnParentChanged(nsMsgKey aKeyChanged, + nsMsgKey oldParent, + nsMsgKey newParent, + nsIDBChangeListener* aInstigator) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgDBHdr> hdrChanged; + mDatabase->GetMsgHdrForKey(aKeyChanged, getter_AddRefs(hdrChanged)); + // In reality we probably want to just change the parent because otherwise we + // will lose things like selection. + if (hdrChanged) { + // First delete the child from the old threadParent + OnHdrAddedOrDeleted(hdrChanged, false); + // Then add it to the new threadParent + OnHdrAddedOrDeleted(hdrChanged, true); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::OnAnnouncerGoingAway( + nsIDBChangeAnnouncer* instigator) { + if (mBackupDatabase && instigator == mBackupDatabase) { + mBackupDatabase->RemoveListener(this); + mBackupDatabase = nullptr; + } else if (mDatabase) { + mDatabase->RemoveListener(this); + mDatabase = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetManyHeadersToDownload(bool* retval) { + NS_ENSURE_ARG_POINTER(retval); + int32_t numTotalMessages; + + // is there any reason to return false? + if (!mDatabase) + *retval = true; + else if (NS_SUCCEEDED(GetTotalMessages(false, &numTotalMessages)) && + numTotalMessages <= 0) + *retval = true; + else + *retval = false; + return NS_OK; +} + +nsresult nsMsgDBFolder::MsgFitsDownloadCriteria(nsMsgKey msgKey, bool* result) { + if (!mDatabase) return NS_ERROR_FAILURE; + + nsresult rv; + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr)); + if (NS_FAILED(rv)) return rv; + + if (hdr) { + uint32_t msgFlags = 0; + hdr->GetFlags(&msgFlags); + // check if we already have this message body offline + if (!(msgFlags & nsMsgMessageFlags::Offline)) { + *result = true; + // check against the server download size limit . + nsCOMPtr<nsIMsgIncomingServer> incomingServer; + rv = GetServer(getter_AddRefs(incomingServer)); + if (NS_SUCCEEDED(rv) && incomingServer) { + bool limitDownloadSize = false; + rv = incomingServer->GetLimitOfflineMessageSize(&limitDownloadSize); + NS_ENSURE_SUCCESS(rv, rv); + if (limitDownloadSize) { + int32_t maxDownloadMsgSize = 0; + uint32_t msgSize; + hdr->GetMessageSize(&msgSize); + rv = incomingServer->GetMaxMessageSize(&maxDownloadMsgSize); + NS_ENSURE_SUCCESS(rv, rv); + maxDownloadMsgSize *= 1024; + if (msgSize > (uint32_t)maxDownloadMsgSize) *result = false; + } + } + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetSupportsOffline(bool* aSupportsOffline) { + NS_ENSURE_ARG_POINTER(aSupportsOffline); + if (mFlags & nsMsgFolderFlags::Virtual) { + *aSupportsOffline = false; + return NS_OK; + } + + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + if (!server) return NS_ERROR_FAILURE; + + int32_t offlineSupportLevel; + rv = server->GetOfflineSupportLevel(&offlineSupportLevel); + NS_ENSURE_SUCCESS(rv, rv); + + *aSupportsOffline = (offlineSupportLevel >= OFFLINE_SUPPORT_LEVEL_REGULAR); + return NS_OK; +} + +// Note: this probably always returns false for local folders! +// Looks like it's only ever used for IMAP folders. +NS_IMETHODIMP nsMsgDBFolder::ShouldStoreMsgOffline(nsMsgKey msgKey, + bool* result) { + NS_ENSURE_ARG(result); + uint32_t flags = 0; + *result = false; + GetFlags(&flags); + return flags & nsMsgFolderFlags::Offline + ? MsgFitsDownloadCriteria(msgKey, result) + : NS_OK; +} + +// Looks like this implementation is only ever used for IMAP folders. +NS_IMETHODIMP nsMsgDBFolder::HasMsgOffline(nsMsgKey msgKey, bool* result) { + NS_ENSURE_ARG(result); + *result = false; + GetDatabase(); + if (!mDatabase) return NS_ERROR_FAILURE; + + nsresult rv; + nsCOMPtr<nsIMsgDBHdr> hdr; + rv = mDatabase->GetMsgHdrForKey(msgKey, getter_AddRefs(hdr)); + if (NS_FAILED(rv)) return rv; + + if (hdr) { + uint32_t msgFlags = 0; + hdr->GetFlags(&msgFlags); + // check if we already have this message body offline + if ((msgFlags & nsMsgMessageFlags::Offline)) *result = true; + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetFlags(uint32_t* _retval) { + ReadDBFolderInfo(false); + *_retval = mFlags; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::ReadFromFolderCacheElem( + nsIMsgFolderCacheElement* element) { + nsresult rv = NS_OK; + + element->GetCachedUInt32("flags", &mFlags); + element->GetCachedInt32("totalMsgs", &mNumTotalMessages); + element->GetCachedInt32("totalUnreadMsgs", &mNumUnreadMessages); + element->GetCachedInt32("pendingUnreadMsgs", &mNumPendingUnreadMessages); + element->GetCachedInt32("pendingMsgs", &mNumPendingTotalMessages); + element->GetCachedInt64("expungedBytes", &mExpungedBytes); + element->GetCachedInt64("folderSize", &mFolderSize); + +#ifdef DEBUG_bienvenu1 + nsCString uri; + GetURI(uri); + printf("read total %ld for %s\n", mNumTotalMessages, uri.get()); +#endif + return rv; +} + +nsresult nsMsgDBFolder::GetFolderCacheKey(nsIFile** aFile) { + nsresult rv; + bool isServer = false; + GetIsServer(&isServer); + + // if it's a server, we don't need the .msf appended to the name + nsCOMPtr<nsIFile> dbPath; + if (isServer) { + rv = GetFilePath(getter_AddRefs(dbPath)); + } else { + rv = GetSummaryFile(getter_AddRefs(dbPath)); + } + NS_ENSURE_SUCCESS(rv, rv); + dbPath.forget(aFile); + return NS_OK; +} + +nsresult nsMsgDBFolder::FlushToFolderCache() { + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + if (NS_SUCCEEDED(rv) && accountManager) { + nsCOMPtr<nsIMsgFolderCache> folderCache; + rv = accountManager->GetFolderCache(getter_AddRefs(folderCache)); + if (NS_SUCCEEDED(rv) && folderCache) + rv = WriteToFolderCache(folderCache, false); + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::WriteToFolderCache(nsIMsgFolderCache* folderCache, + bool deep) { + nsresult rv = NS_OK; + + if (folderCache) { + nsCOMPtr<nsIMsgFolderCacheElement> cacheElement; + nsCOMPtr<nsIFile> dbPath; + rv = GetFolderCacheKey(getter_AddRefs(dbPath)); +#ifdef DEBUG_bienvenu1 + bool exists; + NS_ASSERTION(NS_SUCCEEDED(dbPath->Exists(&exists)) && exists, + "file spec we're adding to cache should exist"); +#endif + if (NS_SUCCEEDED(rv) && dbPath) { + nsCString persistentPath; + rv = dbPath->GetPersistentDescriptor(persistentPath); + NS_ENSURE_SUCCESS(rv, rv); + rv = folderCache->GetCacheElement(persistentPath, true, + getter_AddRefs(cacheElement)); + if (NS_SUCCEEDED(rv) && cacheElement) + rv = WriteToFolderCacheElem(cacheElement); + } + + if (deep) { + for (nsIMsgFolder* msgFolder : mSubFolders) { + rv = msgFolder->WriteToFolderCache(folderCache, true); + if (NS_FAILED(rv)) break; + } + } + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::WriteToFolderCacheElem( + nsIMsgFolderCacheElement* element) { + nsresult rv = NS_OK; + + element->SetCachedUInt32("flags", mFlags); + element->SetCachedInt32("totalMsgs", mNumTotalMessages); + element->SetCachedInt32("totalUnreadMsgs", mNumUnreadMessages); + element->SetCachedInt32("pendingUnreadMsgs", mNumPendingUnreadMessages); + element->SetCachedInt32("pendingMsgs", mNumPendingTotalMessages); + element->SetCachedInt64("expungedBytes", mExpungedBytes); + element->SetCachedInt64("folderSize", mFolderSize); + +#ifdef DEBUG_bienvenu1 + nsCString uri; + GetURI(uri); + printf("writing total %ld for %s\n", mNumTotalMessages, uri.get()); +#endif + return rv; +} + +NS_IMETHODIMP +nsMsgDBFolder::AddMessageDispositionState( + nsIMsgDBHdr* aMessage, nsMsgDispositionState aDispositionFlag) { + NS_ENSURE_ARG_POINTER(aMessage); + + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, NS_OK); + + nsMsgKey msgKey; + aMessage->GetMessageKey(&msgKey); + + if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Replied) + mDatabase->MarkReplied(msgKey, true, nullptr); + else if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Forwarded) + mDatabase->MarkForwarded(msgKey, true, nullptr); + else if (aDispositionFlag == nsIMsgFolder::nsMsgDispositionState_Redirected) + mDatabase->MarkRedirected(msgKey, true, nullptr); + return NS_OK; +} + +nsresult nsMsgDBFolder::AddMarkAllReadUndoAction(nsIMsgWindow* msgWindow, + nsMsgKey* thoseMarked, + uint32_t numMarked) { + RefPtr<nsMsgReadStateTxn> readStateTxn = new nsMsgReadStateTxn(); + if (!readStateTxn) return NS_ERROR_OUT_OF_MEMORY; + + nsresult rv = readStateTxn->Init(this, numMarked, thoseMarked); + NS_ENSURE_SUCCESS(rv, rv); + + rv = readStateTxn->SetTransactionType(nsIMessenger::eMarkAllMsg); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsITransactionManager> txnMgr; + rv = msgWindow->GetTransactionManager(getter_AddRefs(txnMgr)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = txnMgr->DoTransaction(readStateTxn); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +NS_IMETHODIMP +nsMsgDBFolder::MarkAllMessagesRead(nsIMsgWindow* aMsgWindow) { + nsresult rv = GetDatabase(); + m_newMsgs.Clear(); + + if (NS_SUCCEEDED(rv)) { + EnableNotifications(allMessageCountNotifications, false); + nsTArray<nsMsgKey> thoseMarked; + rv = mDatabase->MarkAllRead(thoseMarked); + EnableNotifications(allMessageCountNotifications, true); + NS_ENSURE_SUCCESS(rv, rv); + + // Setup a undo-state + if (aMsgWindow && thoseMarked.Length() > 0) + rv = AddMarkAllReadUndoAction(aMsgWindow, thoseMarked.Elements(), + thoseMarked.Length()); + } + + SetHasNewMessages(false); + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::MarkThreadRead(nsIMsgThread* thread) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + nsTArray<nsMsgKey> keys; + return mDatabase->MarkThreadRead(thread, nullptr, keys); +} + +NS_IMETHODIMP +nsMsgDBFolder::OnStartRunningUrl(nsIURI* aUrl) { return NS_OK; } + +NS_IMETHODIMP +nsMsgDBFolder::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) { + NS_ENSURE_ARG_POINTER(aUrl); + nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl); + if (mailUrl) { + bool updatingFolder = false; + if (NS_SUCCEEDED(mailUrl->GetUpdatingFolder(&updatingFolder)) && + updatingFolder) + NotifyFolderEvent(kFolderLoaded); + + // be sure to remove ourselves as a url listener + mailUrl->UnRegisterListener(this); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetRetentionSettings(nsIMsgRetentionSettings** settings) { + NS_ENSURE_ARG_POINTER(settings); + *settings = nullptr; + nsresult rv = NS_OK; + bool useServerDefaults = false; + if (!m_retentionSettings) { + nsCString useServerRetention; + GetStringProperty(kUseServerRetentionProp, useServerRetention); + if (useServerRetention.EqualsLiteral("1")) { + nsCOMPtr<nsIMsgIncomingServer> incomingServer; + rv = GetServer(getter_AddRefs(incomingServer)); + if (NS_SUCCEEDED(rv) && incomingServer) { + rv = incomingServer->GetRetentionSettings(settings); + useServerDefaults = true; + } + } else { + GetDatabase(); + if (mDatabase) { + // get the settings from the db - if the settings from the db say the + // folder is not overriding the incoming server settings, get the + // settings from the server. + rv = mDatabase->GetMsgRetentionSettings(settings); + if (NS_SUCCEEDED(rv) && *settings) { + (*settings)->GetUseServerDefaults(&useServerDefaults); + if (useServerDefaults) { + nsCOMPtr<nsIMsgIncomingServer> incomingServer; + rv = GetServer(getter_AddRefs(incomingServer)); + NS_IF_RELEASE(*settings); + if (NS_SUCCEEDED(rv) && incomingServer) + incomingServer->GetRetentionSettings(settings); + } + if (useServerRetention.EqualsLiteral("1") != useServerDefaults) { + if (useServerDefaults) + useServerRetention.Assign('1'); + else + useServerRetention.Assign('0'); + SetStringProperty(kUseServerRetentionProp, useServerRetention); + } + } + } else + return NS_ERROR_FAILURE; + } + // Only cache the retention settings if we've overridden the server + // settings (otherwise, we won't notice changes to the server settings). + if (!useServerDefaults) m_retentionSettings = *settings; + } else + NS_IF_ADDREF(*settings = m_retentionSettings); + + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::SetRetentionSettings( + nsIMsgRetentionSettings* settings) { + bool useServerDefaults; + nsCString useServerRetention; + + settings->GetUseServerDefaults(&useServerDefaults); + if (useServerDefaults) { + useServerRetention.Assign('1'); + m_retentionSettings = nullptr; + } else { + useServerRetention.Assign('0'); + m_retentionSettings = settings; + } + SetStringProperty(kUseServerRetentionProp, useServerRetention); + GetDatabase(); + if (mDatabase) mDatabase->SetMsgRetentionSettings(settings); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetDownloadSettings( + nsIMsgDownloadSettings** settings) { + NS_ENSURE_ARG_POINTER(settings); + nsresult rv = NS_OK; + if (!m_downloadSettings) { + GetDatabase(); + if (mDatabase) { + // get the settings from the db - if the settings from the db say the + // folder is not overriding the incoming server settings, get the settings + // from the server. + rv = + mDatabase->GetMsgDownloadSettings(getter_AddRefs(m_downloadSettings)); + if (NS_SUCCEEDED(rv) && m_downloadSettings) { + bool useServerDefaults; + m_downloadSettings->GetUseServerDefaults(&useServerDefaults); + if (useServerDefaults) { + nsCOMPtr<nsIMsgIncomingServer> incomingServer; + rv = GetServer(getter_AddRefs(incomingServer)); + if (NS_SUCCEEDED(rv) && incomingServer) + incomingServer->GetDownloadSettings( + getter_AddRefs(m_downloadSettings)); + } + } + } + } + NS_IF_ADDREF(*settings = m_downloadSettings); + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::SetDownloadSettings( + nsIMsgDownloadSettings* settings) { + m_downloadSettings = settings; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::IsCommandEnabled(const nsACString& command, + bool* result) { + NS_ENSURE_ARG_POINTER(result); + *result = true; + return NS_OK; +} + +nsresult nsMsgDBFolder::WriteStartOfNewLocalMessage() { + nsAutoCString result; + uint32_t writeCount; + time_t now = time((time_t*)0); + char* ct = ctime(&now); + ct[24] = 0; + result = "From - "; + result += ct; + result += MSG_LINEBREAK; + m_bytesAddedToLocalMsg = result.Length(); + + MOZ_ASSERT(m_tempMessageStream, + "Temporary message stream must not be nullptr"); + + nsresult rv = + m_tempMessageStream->Write(result.get(), result.Length(), &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + m_tempMessageStreamBytesWritten += writeCount; + + constexpr auto MozillaStatus = "X-Mozilla-Status: 0001"_ns MSG_LINEBREAK; + rv = m_tempMessageStream->Write(MozillaStatus.get(), MozillaStatus.Length(), + &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + m_tempMessageStreamBytesWritten += writeCount; + m_bytesAddedToLocalMsg += writeCount; + constexpr auto MozillaStatus2 = + "X-Mozilla-Status2: 00000000"_ns MSG_LINEBREAK; + m_bytesAddedToLocalMsg += MozillaStatus2.Length(); + rv = m_tempMessageStream->Write(MozillaStatus2.get(), MozillaStatus2.Length(), + &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + m_tempMessageStreamBytesWritten += writeCount; + return NS_OK; +} + +nsresult nsMsgDBFolder::StartNewOfflineMessage() { + bool isLocked; + GetLocked(&isLocked); + bool hasSemaphore = false; + if (isLocked) { + // it's OK if we, the folder, have the semaphore. + TestSemaphore(static_cast<nsIMsgFolder*>(this), &hasSemaphore); + if (!hasSemaphore) { + NS_WARNING("folder locked trying to download offline"); + return NS_MSG_FOLDER_BUSY; + } + } + m_tempMessageStreamBytesWritten = 0; + nsresult rv = GetOfflineStoreOutputStream( + m_offlineHeader, getter_AddRefs(m_tempMessageStream)); + if (NS_SUCCEEDED(rv) && !hasSemaphore) + AcquireSemaphore(static_cast<nsIMsgFolder*>(this)); + if (NS_SUCCEEDED(rv)) WriteStartOfNewLocalMessage(); + m_numOfflineMsgLines = 0; + return rv; +} + +nsresult nsMsgDBFolder::EndNewOfflineMessage(nsresult status) { + int64_t curStorePos; + uint64_t messageOffset; + uint32_t messageSize; + + nsMsgKey messageKey; + + nsresult rv1, rv2; + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + m_offlineHeader->GetMessageKey(&messageKey); + + nsCOMPtr<nsIMsgPluggableStore> msgStore; + rv = GetMsgStore(getter_AddRefs(msgStore)); + NS_ENSURE_SUCCESS(rv, rv); + + // Are we being asked to abort and clean up? + if (NS_FAILED(status)) { + mDatabase->MarkOffline(messageKey, false, nullptr); + if (m_tempMessageStream) { + msgStore->DiscardNewMessage(m_tempMessageStream, m_offlineHeader); + } + m_tempMessageStream = nullptr; + m_offlineHeader = nullptr; + return NS_OK; + } + + if (m_tempMessageStream) { + m_tempMessageStream->Flush(); + } + + // Some sanity checking. + // This will go away once nsIMsgPluggableStore stops serving up seekable + // output streams. + // If quarantining (mailnews.downloadToTempFile == true) is on we'll already + // have a non-seekable stream. + nsCOMPtr<nsISeekableStream> seekable; + if (m_tempMessageStream) seekable = do_QueryInterface(m_tempMessageStream); + if (seekable) { + int64_t tellPos; + seekable->Tell(&tellPos); + curStorePos = tellPos; + + // N.B. This only works if we've set the offline flag for the message, + // so be careful about moving the call to MarkOffline above. + m_offlineHeader->GetMessageOffset(&messageOffset); + curStorePos -= messageOffset; + m_offlineHeader->GetMessageSize(&messageSize); + messageSize += m_bytesAddedToLocalMsg; + // unix/mac has a one byte line ending, but the imap server returns + // crlf terminated lines. + if (MSG_LINEBREAK_LEN == 1) messageSize -= m_numOfflineMsgLines; + + // We clear the offline flag on the message if the size + // looks wrong. Check if we're off by more than one byte per line. + if (messageSize > (uint32_t)curStorePos && + (messageSize - (uint32_t)curStorePos) > + (uint32_t)m_numOfflineMsgLines) { + mDatabase->MarkOffline(messageKey, false, nullptr); + // we should truncate the offline store at messageOffset + ReleaseSemaphore(static_cast<nsIMsgFolder*>(this)); + rv1 = rv2 = NS_OK; + if (msgStore) { + // DiscardNewMessage now closes the stream all the time. + rv1 = msgStore->DiscardNewMessage(m_tempMessageStream, m_offlineHeader); + m_tempMessageStream = nullptr; // avoid accessing closed stream + } else { + rv2 = m_tempMessageStream->Close(); + m_tempMessageStream = nullptr; // ditto + } + // XXX We should check for errors of rv1 and rv2. + if (NS_FAILED(rv1)) NS_WARNING("DiscardNewMessage returned error"); + if (NS_FAILED(rv2)) + NS_WARNING("m_tempMessageStream->Close() returned error"); +#ifdef _DEBUG + nsAutoCString message("Offline message too small: messageSize="); + message.AppendInt(messageSize); + message.AppendLiteral(" curStorePos="); + message.AppendInt(curStorePos); + message.AppendLiteral(" numOfflineMsgLines="); + message.AppendInt(m_numOfflineMsgLines); + message.AppendLiteral(" bytesAdded="); + message.AppendInt(m_bytesAddedToLocalMsg); + NS_ERROR(message.get()); +#endif + m_offlineHeader = nullptr; + return NS_ERROR_FAILURE; + } + } // seekable + + // Success! Finalise the message. + mDatabase->MarkOffline(messageKey, true, nullptr); + m_offlineHeader->SetOfflineMessageSize(m_tempMessageStreamBytesWritten); + m_offlineHeader->SetLineCount(m_numOfflineMsgLines); + + // (But remember, stream might be buffered and closing/flushing could still + // fail!) + + rv1 = rv2 = NS_OK; + if (msgStore) { + rv1 = msgStore->FinishNewMessage(m_tempMessageStream, m_offlineHeader); + m_tempMessageStream = nullptr; + } + + // We can not let this happen: I think the code assumes this. + // That is the if-expression above is always true. + NS_ASSERTION(msgStore, "msgStore is nullptr"); + + // Notify users of the errors for now, just use NS_WARNING. + if (NS_FAILED(rv1)) NS_WARNING("FinishNewMessage returned error"); + if (NS_FAILED(rv2)) NS_WARNING("m_tempMessageStream->Close() returned error"); + + m_tempMessageStream = nullptr; + m_offlineHeader = nullptr; + + if (NS_FAILED(rv1)) return rv1; + if (NS_FAILED(rv2)) return rv2; + + return rv; +} + +class AutoCompactEvent : public mozilla::Runnable { + public: + AutoCompactEvent(nsIMsgWindow* aMsgWindow, nsMsgDBFolder* aFolder) + : mozilla::Runnable("AutoCompactEvent"), + mMsgWindow(aMsgWindow), + mFolder(aFolder) {} + + NS_IMETHOD Run() { + if (mFolder) mFolder->HandleAutoCompactEvent(mMsgWindow); + return NS_OK; + } + + private: + nsCOMPtr<nsIMsgWindow> mMsgWindow; + RefPtr<nsMsgDBFolder> mFolder; +}; + +nsresult nsMsgDBFolder::HandleAutoCompactEvent(nsIMsgWindow* aWindow) { + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountMgr = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + if (NS_SUCCEEDED(rv)) { + nsTArray<RefPtr<nsIMsgIncomingServer>> allServers; + rv = accountMgr->GetAllServers(allServers); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numServers = allServers.Length(); + if (numServers > 0) { + nsTArray<RefPtr<nsIMsgFolder>> folderArray; + nsTArray<RefPtr<nsIMsgFolder>> offlineFolderArray; + int64_t totalExpungedBytes = 0; + int64_t offlineExpungedBytes = 0; + int64_t localExpungedBytes = 0; + uint32_t serverIndex = 0; + do { + nsCOMPtr<nsIMsgIncomingServer> server(allServers[serverIndex]); + nsCOMPtr<nsIMsgPluggableStore> msgStore; + rv = server->GetMsgStore(getter_AddRefs(msgStore)); + NS_ENSURE_SUCCESS(rv, rv); + if (!msgStore) continue; + bool supportsCompaction; + msgStore->GetSupportsCompaction(&supportsCompaction); + if (!supportsCompaction) continue; + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = server->GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder) { + int32_t offlineSupportLevel; + rv = server->GetOfflineSupportLevel(&offlineSupportLevel); + NS_ENSURE_SUCCESS(rv, rv); + nsTArray<RefPtr<nsIMsgFolder>> allDescendants; + rootFolder->GetDescendants(allDescendants); + int64_t expungedBytes = 0; + if (offlineSupportLevel > 0) { + uint32_t flags; + for (auto folder : allDescendants) { + expungedBytes = 0; + folder->GetFlags(&flags); + if (flags & nsMsgFolderFlags::Offline) + folder->GetExpungedBytes(&expungedBytes); + if (expungedBytes > 0) { + offlineFolderArray.AppendElement(folder); + offlineExpungedBytes += expungedBytes; + } + } + } else // pop or local + { + for (auto folder : allDescendants) { + expungedBytes = 0; + folder->GetExpungedBytes(&expungedBytes); + if (expungedBytes > 0) { + folderArray.AppendElement(folder); + localExpungedBytes += expungedBytes; + } + } + } + } + } while (++serverIndex < numServers); + totalExpungedBytes = localExpungedBytes + offlineExpungedBytes; + int32_t purgeThreshold; + rv = GetPurgeThreshold(&purgeThreshold); + NS_ENSURE_SUCCESS(rv, rv); + if (totalExpungedBytes > ((int64_t)purgeThreshold * 1024)) { + bool okToCompact = false; + nsCOMPtr<nsIPrefService> pref = + do_GetService(NS_PREFSERVICE_CONTRACTID); + nsCOMPtr<nsIPrefBranch> branch; + pref->GetBranch("", getter_AddRefs(branch)); + + bool askBeforePurge; + branch->GetBoolPref(PREF_MAIL_PURGE_ASK, &askBeforePurge); + if (askBeforePurge && aWindow) { + nsCOMPtr<nsIStringBundle> bundle; + rv = GetBaseStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoString compactSize; + FormatFileSize(totalExpungedBytes, true, compactSize); + + bool neverAsk = false; // "Do not ask..." - unchecked by default. + int32_t buttonPressed = 0; + + nsCOMPtr<nsIWindowWatcher> ww( + do_GetService(NS_WINDOWWATCHER_CONTRACTID)); + nsCOMPtr<nsIWritablePropertyBag2> props( + do_CreateInstance("@mozilla.org/hash-property-bag;1")); + props->SetPropertyAsAString(u"compactSize"_ns, compactSize); + nsCOMPtr<mozIDOMWindowProxy> migrateWizard; + rv = ww->OpenWindow( + nullptr, + "chrome://messenger/content/compactFoldersDialog.xhtml"_ns, + "_blank"_ns, "chrome,dialog,modal,centerscreen"_ns, props, + getter_AddRefs(migrateWizard)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = props->GetPropertyAsBool(u"checked"_ns, &neverAsk); + NS_ENSURE_SUCCESS(rv, rv); + + rv = + props->GetPropertyAsInt32(u"buttonNumClicked"_ns, &buttonPressed); + NS_ENSURE_SUCCESS(rv, rv); + + if (buttonPressed == 0) { + okToCompact = true; + if (neverAsk) // [X] Remove deletions automatically and do not ask + branch->SetBoolPref(PREF_MAIL_PURGE_ASK, false); + } + } else + okToCompact = aWindow || !askBeforePurge; + + if (okToCompact) { + NotifyFolderEvent(kAboutToCompact); + + if (localExpungedBytes > 0 || offlineExpungedBytes > 0) { + nsCOMPtr<nsIMsgFolderCompactor> folderCompactor = do_CreateInstance( + "@mozilla.org/messenger/foldercompactor;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + for (nsIMsgFolder* f : offlineFolderArray) { + folderArray.AppendElement(f); + } + rv = folderCompactor->CompactFolders(folderArray, nullptr, aWindow); + } + } + } + } + } + return rv; +} + +nsresult nsMsgDBFolder::AutoCompact(nsIMsgWindow* aWindow) { + // we don't check for null aWindow, because this routine can get called + // in unit tests where we have no window. Just assume not OK if no window. + bool prompt; + nsresult rv = GetPromptPurgeThreshold(&prompt); + NS_ENSURE_SUCCESS(rv, rv); + PRTime timeNow = PR_Now(); // time in microseconds + PRTime timeAfterOneHourOfLastPurgeCheck = gtimeOfLastPurgeCheck + oneHour; + if (timeAfterOneHourOfLastPurgeCheck < timeNow && prompt) { + gtimeOfLastPurgeCheck = timeNow; + nsCOMPtr<nsIRunnable> event = new AutoCompactEvent(aWindow, this); + // Post this as an event because it can put up an alert, which + // might cause issues depending on the stack when we are called. + if (event) NS_DispatchToCurrentThread(event); + } + return rv; +} + +nsresult nsMsgDBFolder::GetPromptPurgeThreshold(bool* aPrompt) { + NS_ENSURE_ARG(aPrompt); + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv) && prefBranch) { + rv = prefBranch->GetBoolPref(PREF_MAIL_PROMPT_PURGE_THRESHOLD, aPrompt); + if (NS_FAILED(rv)) { + *aPrompt = false; + rv = NS_OK; + } + } + return rv; +} + +nsresult nsMsgDBFolder::GetPurgeThreshold(int32_t* aThreshold) { + NS_ENSURE_ARG(aThreshold); + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + if (NS_SUCCEEDED(rv) && prefBranch) { + int32_t thresholdMB = 200; + bool thresholdMigrated = false; + prefBranch->GetIntPref(PREF_MAIL_PURGE_THRESHOLD_MB, &thresholdMB); + prefBranch->GetBoolPref(PREF_MAIL_PURGE_MIGRATED, &thresholdMigrated); + if (!thresholdMigrated) { + *aThreshold = 20480; + (void)prefBranch->GetIntPref(PREF_MAIL_PURGE_THRESHOLD, aThreshold); + if (*aThreshold / 1024 != thresholdMB) { + thresholdMB = std::max(1, *aThreshold / 1024); + prefBranch->SetIntPref(PREF_MAIL_PURGE_THRESHOLD_MB, thresholdMB); + } + prefBranch->SetBoolPref(PREF_MAIL_PURGE_MIGRATED, true); + } + *aThreshold = thresholdMB * 1024; + } + return rv; +} + +NS_IMETHODIMP // called on the folder that is renamed or about to be deleted +nsMsgDBFolder::MatchOrChangeFilterDestination(nsIMsgFolder* newFolder, + bool caseInsensitive, + bool* found) { + NS_ENSURE_ARG_POINTER(found); + *found = false; + nsCString oldUri; + nsresult rv = GetURI(oldUri); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString newUri; + if (newFolder) // for matching uri's this will be null + { + rv = newFolder->GetURI(newUri); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIMsgFilterList> filterList; + nsCOMPtr<nsIMsgAccountManager> accountMgr = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgIncomingServer>> allServers; + rv = accountMgr->GetAllServers(allServers); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto server : allServers) { + if (server) { + bool canHaveFilters; + rv = server->GetCanHaveFilters(&canHaveFilters); + if (NS_SUCCEEDED(rv) && canHaveFilters) { + // update the filterlist to match the new folder name + rv = server->GetFilterList(nullptr, getter_AddRefs(filterList)); + if (NS_SUCCEEDED(rv) && filterList) { + bool match; + rv = filterList->MatchOrChangeFilterTarget(oldUri, newUri, + caseInsensitive, &match); + if (NS_SUCCEEDED(rv) && match) { + *found = true; + if (newFolder && !newUri.IsEmpty()) + rv = filterList->SaveToDefaultFile(); + } + } + // update the editable filterlist to match the new folder name + rv = server->GetEditableFilterList(nullptr, getter_AddRefs(filterList)); + if (NS_SUCCEEDED(rv) && filterList) { + bool match; + rv = filterList->MatchOrChangeFilterTarget(oldUri, newUri, + caseInsensitive, &match); + if (NS_SUCCEEDED(rv) && match) { + *found = true; + if (newFolder && !newUri.IsEmpty()) + rv = filterList->SaveToDefaultFile(); + } + } + } + } + } + return rv; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetDBTransferInfo(nsIDBFolderInfo** aTransferInfo) { + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + nsCOMPtr<nsIMsgDatabase> db; + GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), getter_AddRefs(db)); + NS_ENSURE_STATE(dbFolderInfo); + return dbFolderInfo->GetTransferInfo(aTransferInfo); +} + +NS_IMETHODIMP +nsMsgDBFolder::SetDBTransferInfo(nsIDBFolderInfo* aTransferInfo) { + NS_ENSURE_ARG(aTransferInfo); + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + nsCOMPtr<nsIMsgDatabase> db; + GetMsgDatabase(getter_AddRefs(db)); + if (db) { + db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (dbFolderInfo) { + dbFolderInfo->InitFromTransferInfo(aTransferInfo); + dbFolderInfo->SetBooleanProperty("forceReparse", false); + } + db->SetSummaryValid(true); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetStringProperty(const char* propertyName, + nsACString& propertyValue) { + NS_ENSURE_ARG_POINTER(propertyName); + nsCOMPtr<nsIFile> dbPath; + nsresult rv = GetFolderCacheKey(getter_AddRefs(dbPath)); + if (dbPath) { + nsCOMPtr<nsIMsgFolderCacheElement> cacheElement; + rv = GetFolderCacheElemFromFile(dbPath, getter_AddRefs(cacheElement)); + if (cacheElement) // try to get from cache + rv = cacheElement->GetCachedString(propertyName, propertyValue); + if (NS_FAILED(rv)) // if failed, then try to get from db, usually. + { + if (strcmp(propertyName, MRU_TIME_PROPERTY) == 0 || + strcmp(propertyName, MRM_TIME_PROPERTY) == 0 || + strcmp(propertyName, "LastPurgeTime") == 0) { + // Don't open DB for missing time properties. + // Missing time properties can happen if the folder was never + // accessed, for exaple after an import. They happen if + // folderCache.json is removed or becomes invalid after moving + // a profile (see bug 1726660). + propertyValue.Truncate(); + return NS_OK; + } + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> db; + bool exists; + rv = dbPath->Exists(&exists); + if (NS_FAILED(rv) || !exists) return NS_MSG_ERROR_FOLDER_MISSING; + bool weOpenedDB = !mDatabase; + rv = GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db)); + if (NS_SUCCEEDED(rv)) + rv = folderInfo->GetCharProperty(propertyName, propertyValue); + if (weOpenedDB) CloseDBIfFolderNotOpen(false); + if (NS_SUCCEEDED(rv)) { + // Now that we have the value, store it in our cache. + if (cacheElement) { + cacheElement->SetCachedString(propertyName, propertyValue); + } + } + } + } + return rv; +} + +NS_IMETHODIMP +nsMsgDBFolder::SetStringProperty(const char* propertyName, + const nsACString& propertyValue) { + NS_ENSURE_ARG_POINTER(propertyName); + nsCOMPtr<nsIFile> dbPath; + GetFolderCacheKey(getter_AddRefs(dbPath)); + if (dbPath) { + nsCOMPtr<nsIMsgFolderCacheElement> cacheElement; + GetFolderCacheElemFromFile(dbPath, getter_AddRefs(cacheElement)); + if (cacheElement) // try to set in the cache + cacheElement->SetCachedString(propertyName, propertyValue); + } + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = + GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db)); + if (NS_SUCCEEDED(rv)) { + folderInfo->SetCharProperty(propertyName, propertyValue); + db->Commit(nsMsgDBCommitType::kLargeCommit); // committing the db also + // commits the cache + } + return NS_OK; +} + +// Get/Set ForcePropertyEmpty is only used with inherited properties +NS_IMETHODIMP +nsMsgDBFolder::GetForcePropertyEmpty(const char* aPropertyName, bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + nsAutoCString nameEmpty(aPropertyName); + nameEmpty.AppendLiteral(".empty"); + nsCString value; + GetStringProperty(nameEmpty.get(), value); + *_retval = value.EqualsLiteral("true"); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::SetForcePropertyEmpty(const char* aPropertyName, bool aValue) { + nsAutoCString nameEmpty(aPropertyName); + nameEmpty.AppendLiteral(".empty"); + return SetStringProperty(nameEmpty.get(), aValue ? "true"_ns : ""_ns); +} + +NS_IMETHODIMP +nsMsgDBFolder::GetInheritedStringProperty(const char* aPropertyName, + nsACString& aPropertyValue) { + NS_ENSURE_ARG_POINTER(aPropertyName); + nsCString value; + nsCOMPtr<nsIMsgIncomingServer> server; + + bool forceEmpty = false; + + if (!mIsServer) { + GetForcePropertyEmpty(aPropertyName, &forceEmpty); + } else { + // root folders must get their values from the server + GetServer(getter_AddRefs(server)); + if (server) server->GetForcePropertyEmpty(aPropertyName, &forceEmpty); + } + + if (forceEmpty) { + aPropertyValue.Truncate(); + return NS_OK; + } + + // servers will automatically inherit from the preference + // mail.server.default.(propertyName) + if (server) return server->GetCharValue(aPropertyName, aPropertyValue); + + GetStringProperty(aPropertyName, value); + if (value.IsEmpty()) { + // inherit from the parent + nsCOMPtr<nsIMsgFolder> parent; + GetParent(getter_AddRefs(parent)); + if (parent) + return parent->GetInheritedStringProperty(aPropertyName, aPropertyValue); + } + + aPropertyValue.Assign(value); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::OnMessageClassified(const nsACString& aMsgURI, + nsMsgJunkStatus aClassification, + uint32_t aJunkPercent) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, NS_OK); + + if (aMsgURI.IsEmpty()) // This signifies end of batch. + { + // Apply filters if needed. + if (!mPostBayesMessagesToFilter.IsEmpty()) { + // Apply post-bayes filtering. + nsCOMPtr<nsIMsgFilterService> filterService( + do_GetService("@mozilla.org/messenger/services/filters;1", &rv)); + if (NS_SUCCEEDED(rv)) + // We use a null nsIMsgWindow because we don't want some sort of ui + // appearing in the middle of automatic filtering (plus I really don't + // want to propagate that value.) + rv = filterService->ApplyFilters(nsMsgFilterType::PostPlugin, + mPostBayesMessagesToFilter, this, + nullptr, nullptr); + mPostBayesMessagesToFilter.Clear(); + } + + // If we classified any messages, send out a notification. + nsTArray<RefPtr<nsIMsgDBHdr>> hdrs; + rv = MsgGetHeadersFromKeys(mDatabase, mClassifiedMsgKeys, hdrs); + NS_ENSURE_SUCCESS(rv, rv); + if (!hdrs.IsEmpty()) { + nsCOMPtr<nsIMsgFolderNotificationService> notifier(do_GetService( + "@mozilla.org/messenger/msgnotificationservice;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + notifier->NotifyMsgsClassified(hdrs, mBayesJunkClassifying, + mBayesTraitClassifying); + } + return rv; + } + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISpamSettings> spamSettings; + rv = server->GetSpamSettings(getter_AddRefs(spamSettings)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgKey msgKey; + rv = msgHdr->GetMessageKey(&msgKey); + NS_ENSURE_SUCCESS(rv, rv); + + // check if this message needs junk classification + uint32_t processingFlags; + GetProcessingFlags(msgKey, &processingFlags); + + if (processingFlags & nsMsgProcessingFlags::ClassifyJunk) { + mClassifiedMsgKeys.AppendElement(msgKey); + AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::ClassifyJunk); + + nsAutoCString msgJunkScore; + msgJunkScore.AppendInt(aClassification == nsIJunkMailPlugin::JUNK + ? nsIJunkMailPlugin::IS_SPAM_SCORE + : nsIJunkMailPlugin::IS_HAM_SCORE); + mDatabase->SetStringProperty(msgKey, "junkscore", msgJunkScore); + mDatabase->SetStringProperty(msgKey, "junkscoreorigin", "plugin"_ns); + + nsAutoCString strPercent; + strPercent.AppendInt(aJunkPercent); + mDatabase->SetStringProperty(msgKey, "junkpercent", strPercent); + + if (aClassification == nsIJunkMailPlugin::JUNK) { + // IMAP has its own way of marking read. + if (!(mFlags & nsMsgFolderFlags::ImapBox)) { + bool markAsReadOnSpam; + (void)spamSettings->GetMarkAsReadOnSpam(&markAsReadOnSpam); + if (markAsReadOnSpam) { + rv = mDatabase->MarkRead(msgKey, true, this); + if (!NS_SUCCEEDED(rv)) + NS_WARNING("failed marking spam message as read"); + } + } + // mail folders will log junk hits with move info. Perhaps we should + // add a log here for non-mail folders as well, that don't override + // onMessageClassified + // rv = spamSettings->LogJunkHit(msgHdr, false); + // NS_ENSURE_SUCCESS(rv,rv); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::OnMessageTraitsClassified(const nsACString& aMsgURI, + const nsTArray<uint32_t>& aTraits, + const nsTArray<uint32_t>& aPercents) { + if (aMsgURI.IsEmpty()) // This signifies end of batch + return NS_OK; // We are not handling batching + + MOZ_ASSERT(aTraits.Length() == aPercents.Length()); + + nsresult rv; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgDBHdrFromURI(aMsgURI, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgKey msgKey; + rv = msgHdr->GetMessageKey(&msgKey); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t processingFlags; + GetProcessingFlags(msgKey, &processingFlags); + if (!(processingFlags & nsMsgProcessingFlags::ClassifyTraits)) return NS_OK; + + AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::ClassifyTraits); + + nsCOMPtr<nsIMsgTraitService> traitService; + traitService = do_GetService("@mozilla.org/msg-trait-service;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + for (uint32_t i = 0; i < aTraits.Length(); i++) { + if (aTraits[i] == nsIJunkMailPlugin::JUNK_TRAIT) + continue; // junk is processed by the junk listener + nsAutoCString traitId; + rv = traitService->GetId(aTraits[i], traitId); + traitId.InsertLiteral("bayespercent/", 0); + nsAutoCString strPercent; + strPercent.AppendInt(aPercents[i]); + mDatabase->SetStringPropertyByHdr(msgHdr, traitId.get(), strPercent); + } + return NS_OK; +} + +/** + * Call the filter plugins (XXX currently just one) + */ +NS_IMETHODIMP +nsMsgDBFolder::CallFilterPlugins(nsIMsgWindow* aMsgWindow, bool* aFiltersRun) { + NS_ENSURE_ARG_POINTER(aFiltersRun); + + nsString folderName; + GetPrettyName(folderName); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Running filter plugins on folder '%s'", + NS_ConvertUTF16toUTF8(folderName).get())); + + *aFiltersRun = false; + nsCOMPtr<nsIMsgIncomingServer> server; + nsCOMPtr<nsISpamSettings> spamSettings; + int32_t spamLevel = 0; + + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString serverType; + server->GetType(serverType); + + rv = server->GetSpamSettings(getter_AddRefs(spamSettings)); + nsCOMPtr<nsIMsgFilterPlugin> filterPlugin; + server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin)); + if (!filterPlugin) // it's not an error not to have the filter plugin. + return NS_OK; + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIJunkMailPlugin> junkMailPlugin = do_QueryInterface(filterPlugin); + if (!junkMailPlugin) // we currently only support the junk mail plugin + return NS_OK; + + // if it's a news folder, then we really don't support junk in the ui + // yet the legacy spamLevel seems to think we should analyze it. + // Maybe we should upgrade that, but for now let's not analyze. We'll + // let an extension set an inherited property if they really want us to + // analyze this. We need that anyway to allow extension-based overrides. + // When we finalize adding junk in news to core, we'll deal with the + // spamLevel issue + + // if this is the junk folder, or the trash folder + // don't analyze for spam, because we don't care + // + // if it's the sent, unsent, templates, or drafts, + // don't analyze for spam, because the user + // created that message + // + // if it's a public imap folder, or another users + // imap folder, don't analyze for spam, because + // it's not ours to analyze + // + + bool filterForJunk = true; + if (serverType.EqualsLiteral("rss") || + (mFlags & + (nsMsgFolderFlags::SpecialUse | nsMsgFolderFlags::ImapPublic | + nsMsgFolderFlags::Newsgroup | nsMsgFolderFlags::ImapOtherUser) && + !(mFlags & nsMsgFolderFlags::Inbox))) + filterForJunk = false; + + spamSettings->GetLevel(&spamLevel); + if (!spamLevel) filterForJunk = false; + + /* + * We'll use inherited folder properties for the junk trait to override the + * standard server-based activation of junk processing. This provides a + * hook for extensions to customize the application of junk filtering. + * Set inherited property "dobayes.mailnews@mozilla.org#junk" to "true" + * to force junk processing, and "false" to skip junk processing. + */ + + nsAutoCString junkEnableOverride; + GetInheritedStringProperty("dobayes.mailnews@mozilla.org#junk", + junkEnableOverride); + if (junkEnableOverride.EqualsLiteral("true")) + filterForJunk = true; + else if (junkEnableOverride.EqualsLiteral("false")) + filterForJunk = false; + + bool userHasClassified = false; + // if the user has not classified any messages yet, then we shouldn't bother + // running the junk mail controls. This creates a better first use experience. + // See Bug #250084. + junkMailPlugin->GetUserHasClassified(&userHasClassified); + if (!userHasClassified) filterForJunk = false; + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Will run Spam filter: %s", filterForJunk ? "true" : "false")); + + nsCOMPtr<nsIMsgDatabase> database(mDatabase); + rv = GetMsgDatabase(getter_AddRefs(database)); + NS_ENSURE_SUCCESS(rv, rv); + + // check if trait processing needed + + nsCOMPtr<nsIMsgTraitService> traitService( + do_GetService("@mozilla.org/msg-trait-service;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<uint32_t> proIndices; + rv = traitService->GetEnabledProIndices(proIndices); + bool filterForOther = false; + // We just skip this on failure, since it is rarely used. + if (NS_SUCCEEDED(rv)) { + for (uint32_t i = 0; i < proIndices.Length(); ++i) { + // The trait service determines which traits are globally enabled or + // disabled. If a trait is enabled, it can still be made inactive + // on a particular folder using an inherited property. To do that, + // set "dobayes." + trait proID as an inherited folder property with + // the string value "false" + // + // If any non-junk traits are active on the folder, then the bayes + // processing will calculate probabilities for all enabled traits. + + if (proIndices[i] != nsIJunkMailPlugin::JUNK_TRAIT) { + filterForOther = true; + nsAutoCString traitId; + nsAutoCString property("dobayes."); + traitService->GetId(proIndices[i], traitId); + property.Append(traitId); + nsAutoCString isEnabledOnFolder; + GetInheritedStringProperty(property.get(), isEnabledOnFolder); + if (isEnabledOnFolder.EqualsLiteral("false")) filterForOther = false; + // We might have to allow a "true" override in the future, but + // for now there is no way for that to affect the processing + break; + } + } + } + + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Will run Trait classification: %s", filterForOther ? "true" : "false")); + // clang-format on + + // Do we need to apply message filters? + bool filterPostPlugin = false; // Do we have a post-analysis filter? + nsCOMPtr<nsIMsgFilterList> filterList; + GetFilterList(aMsgWindow, getter_AddRefs(filterList)); + if (filterList) { + uint32_t filterCount = 0; + filterList->GetFilterCount(&filterCount); + for (uint32_t index = 0; index < filterCount && !filterPostPlugin; + ++index) { + nsCOMPtr<nsIMsgFilter> filter; + filterList->GetFilterAt(index, getter_AddRefs(filter)); + if (!filter) continue; + nsMsgFilterTypeType filterType; + filter->GetFilterType(&filterType); + if (!(filterType & nsMsgFilterType::PostPlugin)) continue; + bool enabled = false; + filter->GetEnabled(&enabled); + if (!enabled) continue; + filterPostPlugin = true; + } + } + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Will run Post-classification filters: %s", + filterPostPlugin ? "true" : "false")); + + // If there is nothing to do, leave now but let NotifyHdrsNotBeingClassified + // generate the msgsClassified notification for all newly added messages as + // tracked by the NotReportedClassified processing flag. + if (!filterForOther && !filterForJunk && !filterPostPlugin) { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("No filters need to be run")); + NotifyHdrsNotBeingClassified(); + return NS_OK; + } + + // get the list of new messages + // + nsTArray<nsMsgKey> newKeys; + rv = database->GetNewList(newKeys); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Running filters on %" PRIu32 " new messages", + (uint32_t)newKeys.Length())); + + nsTArray<nsMsgKey> newMessageKeys; + // Start from m_saveNewMsgs (and clear its current state). m_saveNewMsgs is + // where we stash the list of new messages when we are told to clear the list + // of new messages by the UI (which purges the list from the nsMsgDatabase). + newMessageKeys.SwapElements(m_saveNewMsgs); + newMessageKeys.AppendElements(newKeys); + + // build up list of keys to classify + nsTArray<nsMsgKey> classifyMsgKeys; + nsCString uri; + + uint32_t numNewMessages = newMessageKeys.Length(); + for (uint32_t i = 0; i < numNewMessages; ++i) { + nsMsgKey msgKey = newMessageKeys[i]; + // clang-format off + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Running filters on message with key %" PRIu32, msgKeyToInt(msgKey))); + // clang-format on + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = database->GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr)); + if (!NS_SUCCEEDED(rv)) continue; + // per-message junk tests. + bool filterMessageForJunk = false; + while (filterForJunk) // we'll break from this at the end + { + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("Spam filter")); + nsCString junkScore; + msgHdr->GetStringProperty("junkscore", junkScore); + if (!junkScore.IsEmpty()) { + // ignore already scored messages. + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Message already scored previously, skipping")); + break; + } + + bool whiteListMessage = false; + spamSettings->CheckWhiteList(msgHdr, &whiteListMessage); + if (whiteListMessage) { + // mark this msg as non-junk, because we whitelisted it. + nsAutoCString msgJunkScore; + msgJunkScore.AppendInt(nsIJunkMailPlugin::IS_HAM_SCORE); + database->SetStringProperty(msgKey, "junkscore", msgJunkScore); + database->SetStringProperty(msgKey, "junkscoreorigin", "whitelist"_ns); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Message whitelisted, skipping")); + break; // skip this msg since it's in the white list + } + filterMessageForJunk = true; + + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, ("Message is to be classified")); + OrProcessingFlags(msgKey, nsMsgProcessingFlags::ClassifyJunk); + // Since we are junk processing, we want to defer the msgsClassified + // notification until the junk classification has occurred. The event + // is sufficiently reliable that we know this will be handled in + // OnMessageClassified at the end of the batch. We clear the + // NotReportedClassified flag since we know the message is in good hands. + AndProcessingFlags(msgKey, ~nsMsgProcessingFlags::NotReportedClassified); + break; + } + + uint32_t processingFlags; + GetProcessingFlags(msgKey, &processingFlags); + + bool filterMessageForOther = false; + // trait processing + if (!(processingFlags & nsMsgProcessingFlags::TraitsDone)) { + // don't do trait processing on this message again + OrProcessingFlags(msgKey, nsMsgProcessingFlags::TraitsDone); + if (filterForOther) { + filterMessageForOther = true; + OrProcessingFlags(msgKey, nsMsgProcessingFlags::ClassifyTraits); + } + } + + if (filterMessageForJunk || filterMessageForOther) + classifyMsgKeys.AppendElement(newMessageKeys[i]); + + // Set messages to filter post-bayes. + // Have we already filtered this message? + if (!(processingFlags & nsMsgProcessingFlags::FiltersDone)) { + if (filterPostPlugin) { + // Don't do filters on this message again. + // (Only set this if we are actually filtering since this is + // tantamount to a memory leak.) + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Filters done on this message")); + OrProcessingFlags(msgKey, nsMsgProcessingFlags::FiltersDone); + mPostBayesMessagesToFilter.AppendElement(msgHdr); + } + } + } + + NotifyHdrsNotBeingClassified(); + // If there weren't any new messages, just return. + if (newMessageKeys.IsEmpty()) return NS_OK; + + // If we do not need to do any work, leave. + // (We needed to get the list of new messages so we could get their headers so + // we can send notifications about them here.) + + if (!classifyMsgKeys.IsEmpty()) { + // Remember what classifications are the source of this decision for when + // we perform the notification in OnMessageClassified at the conclusion of + // classification. + mBayesJunkClassifying = filterForJunk; + mBayesTraitClassifying = filterForOther; + + uint32_t numMessagesToClassify = classifyMsgKeys.Length(); + MOZ_LOG(FILTERLOGMODULE, LogLevel::Info, + ("Running Spam classification on %" PRIu32 " messages", + numMessagesToClassify)); + + nsTArray<nsCString> messageURIs(numMessagesToClassify); + for (uint32_t msgIndex = 0; msgIndex < numMessagesToClassify; ++msgIndex) { + nsCString tmpStr; + rv = GenerateMessageURI(classifyMsgKeys[msgIndex], tmpStr); + if (NS_SUCCEEDED(rv)) { + messageURIs.AppendElement(tmpStr); + } else { + NS_WARNING( + "nsMsgDBFolder::CallFilterPlugins(): could not" + " generate URI for message"); + } + } + // filterMsgs + *aFiltersRun = true; + + // Already got proIndices, but need antiIndices too. + nsTArray<uint32_t> antiIndices; + rv = traitService->GetEnabledAntiIndices(antiIndices); + NS_ENSURE_SUCCESS(rv, rv); + + rv = junkMailPlugin->ClassifyTraitsInMessages( + messageURIs, proIndices, antiIndices, this, aMsgWindow, this); + } else if (filterPostPlugin) { + // Nothing to classify, so need to end batch ourselves. We do this so that + // post analysis filters will run consistently on a folder, even if + // disabled junk processing, which could be dynamic through whitelisting, + // makes the bayes analysis unnecessary. + OnMessageClassified(EmptyCString(), nsIJunkMailPlugin::UNCLASSIFIED, 0); + } + + return rv; +} + +/** + * Adds the messages in the NotReportedClassified mProcessing set to the + * (possibly empty) array of msgHdrsNotBeingClassified, and send the + * nsIMsgFolderNotificationService notification. + */ +nsresult nsMsgDBFolder::NotifyHdrsNotBeingClassified() { + if (mProcessingFlag[5].keys) { + nsTArray<nsMsgKey> keys; + mProcessingFlag[5].keys->ToMsgKeyArray(keys); + if (keys.Length()) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsNotBeingClassified; + rv = MsgGetHeadersFromKeys(mDatabase, keys, msgHdrsNotBeingClassified); + NS_ENSURE_SUCCESS(rv, rv); + + // Since we know we've handled all the NotReportedClassified messages, + // we clear the set by deleting and recreating it. + delete mProcessingFlag[5].keys; + mProcessingFlag[5].keys = nsMsgKeySetU::Create(); + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) + notifier->NotifyMsgsClassified(msgHdrsNotBeingClassified, + // no classification is being performed + false, false); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetLastMessageLoaded(nsMsgKey* aMsgKey) { + NS_ENSURE_ARG_POINTER(aMsgKey); + *aMsgKey = mLastMessageLoaded; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::SetLastMessageLoaded(nsMsgKey aMsgKey) { + mLastMessageLoaded = aMsgKey; + return NS_OK; +} + +// Returns true if: a) there is no need to prompt or b) the user is already +// logged in or c) the user logged in successfully. +bool nsMsgDBFolder::PromptForMasterPasswordIfNecessary() { + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, false); + + bool userNeedsToAuthenticate = false; + // if we're PasswordProtectLocalCache, then we need to find out if the server + // is authenticated. + (void)accountManager->GetUserNeedsToAuthenticate(&userNeedsToAuthenticate); + if (!userNeedsToAuthenticate) return true; + + // Do we have a master password? + nsCOMPtr<nsIPK11TokenDB> tokenDB = + do_GetService(NS_PK11TOKENDB_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, false); + + nsCOMPtr<nsIPK11Token> token; + rv = tokenDB->GetInternalKeyToken(getter_AddRefs(token)); + NS_ENSURE_SUCCESS(rv, false); + + bool result; + rv = token->CheckPassword(EmptyCString(), &result); + NS_ENSURE_SUCCESS(rv, false); + + if (result) { + // We don't have a master password, so this function isn't supported, + // therefore just tell account manager we've authenticated and return true. + accountManager->SetUserNeedsToAuthenticate(false); + return true; + } + + // We have a master password, so try and login to the slot. + rv = token->Login(false); + if (NS_FAILED(rv)) + // Login failed, so we didn't get a password (e.g. prompt cancelled). + return false; + + // Double-check that we are now logged in + rv = token->IsLoggedIn(&result); + NS_ENSURE_SUCCESS(rv, false); + + accountManager->SetUserNeedsToAuthenticate(!result); + return result; +} + +// this gets called after the last junk mail classification has run. +nsresult nsMsgDBFolder::PerformBiffNotifications(void) { + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + int32_t numBiffMsgs = 0; + nsCOMPtr<nsIMsgFolder> root; + rv = GetRootFolder(getter_AddRefs(root)); + root->GetNumNewMessages(true, &numBiffMsgs); + if (numBiffMsgs > 0) { + server->SetPerformingBiff(true); + SetBiffState(nsIMsgFolder::nsMsgBiffState_NewMail); + server->SetPerformingBiff(false); + } + return NS_OK; +} + +nsresult nsMsgDBFolder::initializeStrings() { + nsresult rv; + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> bundle; + rv = bundleService->CreateBundle( + "chrome://messenger/locale/messenger.properties", getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + bundle->GetStringFromName("inboxFolderName", kLocalizedInboxName); + bundle->GetStringFromName("trashFolderName", kLocalizedTrashName); + bundle->GetStringFromName("sentFolderName", kLocalizedSentName); + bundle->GetStringFromName("draftsFolderName", kLocalizedDraftsName); + bundle->GetStringFromName("templatesFolderName", kLocalizedTemplatesName); + bundle->GetStringFromName("junkFolderName", kLocalizedJunkName); + bundle->GetStringFromName("outboxFolderName", kLocalizedUnsentName); + bundle->GetStringFromName("archivesFolderName", kLocalizedArchivesName); + + nsCOMPtr<nsIStringBundle> brandBundle; + rv = bundleService->CreateBundle("chrome://branding/locale/brand.properties", + getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + bundle->GetStringFromName("brandShortName", kLocalizedBrandShortName); + return NS_OK; +} + +nsresult nsMsgDBFolder::createCollationKeyGenerator() { + if (!gCollationKeyGenerator) { + auto result = mozilla::intl::LocaleService::TryCreateComponent<Collator>(); + if (result.isErr()) { + NS_WARNING("Could not create mozilla::intl::Collation."); + return NS_ERROR_FAILURE; + } + + gCollationKeyGenerator = result.unwrap(); + + // Sort in a case-insensitive way, where "base" letters are considered + // equal, e.g: a = á, a = A, a ≠b. + Collator::Options options{}; + options.sensitivity = Collator::Sensitivity::Base; + auto optResult = gCollationKeyGenerator->SetOptions(options); + + if (optResult.isErr()) { + NS_WARNING("Could not configure the mozilla::intl::Collation."); + gCollationKeyGenerator = nullptr; + return NS_ERROR_FAILURE; + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::Init(const nsACString& uri) { + mURI = uri; + return CreateBaseMessageURI(uri); +} + +nsresult nsMsgDBFolder::CreateBaseMessageURI(const nsACString& aURI) { + // Each folder needs to implement this. + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetURI(nsACString& name) { + name = mURI; + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +#if 0 +typedef bool +(*nsArrayFilter)(nsISupports* element, void* data); +#endif +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsMsgDBFolder::GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) { + folders.ClearAndRetainStorage(); + folders.SetCapacity(mSubFolders.Length()); + for (nsIMsgFolder* f : mSubFolders) { + folders.AppendElement(f); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::FindSubFolder(const nsACString& aEscapedSubFolderName, + nsIMsgFolder** aFolder) { + // XXX use necko here + nsAutoCString uri; + uri.Append(mURI); + uri.Append('/'); + uri.Append(aEscapedSubFolderName); + + return GetOrCreateFolder(uri, aFolder); +} + +NS_IMETHODIMP +nsMsgDBFolder::GetHasSubFolders(bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = mSubFolders.Count() > 0; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetNumSubFolders(uint32_t* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = mSubFolders.Count(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::AddFolderListener(nsIFolderListener* listener) { + NS_ENSURE_ARG_POINTER(listener); + mListeners.AppendElement(listener); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::RemoveFolderListener(nsIFolderListener* listener) { + NS_ENSURE_ARG_POINTER(listener); + mListeners.RemoveElement(listener); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::SetParent(nsIMsgFolder* aParent) { + mParent = do_GetWeakReference(aParent); + if (aParent) { + nsresult rv; + // servers do not have parents, so we must not be a server + mIsServer = false; + mIsServerIsValid = true; + + // also set the server itself while we're here. + nsCOMPtr<nsIMsgIncomingServer> server; + rv = aParent->GetServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) mServer = do_GetWeakReference(server); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetParent(nsIMsgFolder** aParent) { + NS_ENSURE_ARG_POINTER(aParent); + nsCOMPtr<nsIMsgFolder> parent = do_QueryReferent(mParent); + parent.forget(aParent); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetMessages(nsIMsgEnumerator** result) { + NS_ENSURE_ARG_POINTER(result); + // Make sure mDatabase is set. + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + return mDatabase->EnumerateMessages(result); +} + +NS_IMETHODIMP +nsMsgDBFolder::UpdateFolder(nsIMsgWindow*) { return NS_OK; } + +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsMsgDBFolder::GetFolderURL(nsACString& url) { + url.Assign(EmptyCString()); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetServer(nsIMsgIncomingServer** aServer) { + NS_ENSURE_ARG_POINTER(aServer); + nsresult rv; + // short circuit the server if we have it. + nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(mServer, &rv); + if (NS_FAILED(rv)) { + // try again after parsing the URI + rv = parseURI(true); + server = do_QueryReferent(mServer); + } + server.forget(aServer); + return *aServer ? NS_OK : NS_ERROR_FAILURE; +} + +nsresult nsMsgDBFolder::parseURI(bool needServer) { + nsresult rv; + nsCOMPtr<nsIURL> url; + rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(mURI) + .Finalize(url); + NS_ENSURE_SUCCESS(rv, rv); + + // empty path tells us it's a server. + if (!mIsServerIsValid) { + nsAutoCString path; + rv = url->GetPathQueryRef(path); + if (NS_SUCCEEDED(rv)) mIsServer = path.EqualsLiteral("/"); + mIsServerIsValid = true; + } + + // grab the name off the leaf of the server + if (mName.IsEmpty()) { + // mName: + // the name is the trailing directory in the path + nsAutoCString fileName; + nsAutoCString escapedFileName; + url->GetFileName(escapedFileName); + if (!escapedFileName.IsEmpty()) { + // XXX conversion to unicode here? is fileName in UTF8? + // yes, let's say it is in utf8 + MsgUnescapeString(escapedFileName, 0, fileName); + NS_ASSERTION(mozilla::IsUtf8(fileName), "fileName is not in UTF-8"); + CopyUTF8toUTF16(fileName, mName); + } + } + + // grab the server by parsing the URI and looking it up + // in the account manager... + // But avoid this extra work by first asking the parent, if any + nsCOMPtr<nsIMsgIncomingServer> server = do_QueryReferent(mServer, &rv); + if (NS_FAILED(rv)) { + // first try asking the parent instead of the URI + nsCOMPtr<nsIMsgFolder> parentMsgFolder; + GetParent(getter_AddRefs(parentMsgFolder)); + + if (parentMsgFolder) + rv = parentMsgFolder->GetServer(getter_AddRefs(server)); + + // no parent. do the extra work of asking + if (!server && needServer) { + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString serverType; + GetIncomingServerType(serverType); + if (serverType.IsEmpty()) { + NS_WARNING("can't determine folder's server type"); + return NS_ERROR_FAILURE; + } + + rv = NS_MutateURI(url).SetScheme(serverType).Finalize(url); + NS_ENSURE_SUCCESS(rv, rv); + rv = accountManager->FindServerByURI(url, getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + } + mServer = do_GetWeakReference(server); + } /* !mServer */ + + // now try to find the local path for this folder + if (server) { + nsAutoCString newPath; + nsAutoCString escapedUrlPath; + nsAutoCString urlPath; + url->GetFilePath(escapedUrlPath); + if (!escapedUrlPath.IsEmpty()) { + MsgUnescapeString(escapedUrlPath, 0, urlPath); + + // transform the filepath from the URI, such as + // "/folder1/folder2/foldern" + // to + // "folder1.sbd/folder2.sbd/foldern" + // (remove leading / and add .sbd to first n-1 folders) + // to be appended onto the server's path + bool isNewsFolder = false; + nsAutoCString scheme; + if (NS_SUCCEEDED(url->GetScheme(scheme))) { + isNewsFolder = scheme.EqualsLiteral("news") || + scheme.EqualsLiteral("snews") || + scheme.EqualsLiteral("nntp"); + } + NS_MsgCreatePathStringFromFolderURI(urlPath.get(), newPath, scheme, + isNewsFolder); + } + + // now append munged path onto server path + nsCOMPtr<nsIFile> serverPath; + rv = server->GetLocalPath(getter_AddRefs(serverPath)); + if (NS_FAILED(rv)) return rv; + + if (!mPath && serverPath) { + if (!newPath.IsEmpty()) { + // I hope this is temporary - Ultimately, + // NS_MsgCreatePathStringFromFolderURI will need to be fixed. +#if defined(XP_WIN) + newPath.ReplaceChar('/', '\\'); +#endif + rv = serverPath->AppendRelativeNativePath(newPath); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to append to the serverPath"); + if (NS_FAILED(rv)) { + mPath = nullptr; + return rv; + } + } + mPath = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + mPath->InitWithFile(serverPath); + } + // URI is completely parsed when we've attempted to get the server + mHaveParsedURI = true; + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetIsServer(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + // make sure we've parsed the URI + if (!mIsServerIsValid) { + nsresult rv = parseURI(); + if (NS_FAILED(rv) || !mIsServerIsValid) return NS_ERROR_FAILURE; + } + + *aResult = mIsServer; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetNoSelect(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetImapShared(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + return GetFlag(nsMsgFolderFlags::PersonalShared, aResult); +} + +NS_IMETHODIMP +nsMsgDBFolder::GetCanSubscribe(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + // by default, you can't subscribe. + // if otherwise, override it. + *aResult = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetCanFileMessages(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + // varada - checking folder flag to see if it is the "Unsent Messages" + // and if so return FALSE + if (mFlags & (nsMsgFolderFlags::Queue | nsMsgFolderFlags::Virtual)) { + *aResult = false; + return NS_OK; + } + + bool isServer = false; + nsresult rv = GetIsServer(&isServer); + if (NS_FAILED(rv)) return rv; + + // by default, you can't file messages into servers, only to folders + // if otherwise, override it. + *aResult = !isServer; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetCanDeleteMessages(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetCanCreateSubfolders(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + // Checking folder flag to see if it is the "Unsent Messages" + // or a virtual folder, and if so return FALSE + if (mFlags & (nsMsgFolderFlags::Queue | nsMsgFolderFlags::Virtual)) { + *aResult = false; + return NS_OK; + } + + // by default, you can create subfolders on server and folders + // if otherwise, override it. + *aResult = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetCanRename(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + bool isServer = false; + nsresult rv = GetIsServer(&isServer); + if (NS_FAILED(rv)) return rv; + // by default, you can't rename servers, only folders + // if otherwise, override it. + // + // check if the folder is a special folder + // (Trash, Drafts, Unsent Messages, Inbox, Sent, Templates, Junk, Archives) + // if it is, don't allow the user to rename it + // (which includes dnd moving it with in the same server) + // + // this errors on the side of caution. we'll return false a lot + // more often if we use flags, + // instead of checking if the folder really is being used as a + // special folder by looking at the "copies and folders" prefs on the + // identities. + *aResult = !(isServer || (mFlags & nsMsgFolderFlags::SpecialUse)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetCanCompact(bool* canCompact) { + NS_ENSURE_ARG_POINTER(canCompact); + bool isServer = false; + nsresult rv = GetIsServer(&isServer); + NS_ENSURE_SUCCESS(rv, rv); + // servers (root folder) cannot be compacted + // virtual search folders cannot be compacted + *canCompact = !isServer && !(mFlags & nsMsgFolderFlags::Virtual); + // If *canCompact now true and folder is imap, keep *canCompact true and + // return; otherwise, when not imap, type of store controls it. E.g., mbox + // sets *canCompact true, maildir sets it false. + if (*canCompact && !(mFlags & nsMsgFolderFlags::ImapBox)) { + // Check if the storage type supports compaction + nsCOMPtr<nsIMsgPluggableStore> msgStore; + GetMsgStore(getter_AddRefs(msgStore)); + if (msgStore) msgStore->GetSupportsCompaction(canCompact); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetPrettyName(nsAString& name) { + return GetName(name); +} + +static bool nonEnglishApp() { + if (nsMsgDBFolder::gIsEnglishApp == -1) { + nsAutoCString locale; + mozilla::intl::LocaleService::GetInstance()->GetAppLocaleAsBCP47(locale); + nsMsgDBFolder::gIsEnglishApp = + (locale.EqualsLiteral("en") || StringBeginsWith(locale, "en-"_ns)) ? 1 + : 0; + } + return nsMsgDBFolder::gIsEnglishApp ? false : true; +} + +static bool hasTrashName(const nsAString& name) { + // Microsoft calls the folder "Deleted". If the application is non-English, + // we want to use the localised name instead. + return name.LowerCaseEqualsLiteral("trash") || + (name.LowerCaseEqualsLiteral("deleted") && nonEnglishApp()); +} + +static bool hasDraftsName(const nsAString& name) { + // Some IMAP providers call the folder "Draft". If the application is + // non-English, we want to use the localised name instead. + return name.LowerCaseEqualsLiteral("drafts") || + (name.LowerCaseEqualsLiteral("draft") && nonEnglishApp()); +} + +static bool hasSentName(const nsAString& name) { + // Some IMAP providers call the folder for sent messages "Outbox". That IMAP + // folder is not related to Thunderbird's local folder for queued messages. + // If we find such a folder with the 'SentMail' flag, we can safely localize + // its name if the application is non-English. + return name.LowerCaseEqualsLiteral("sent") || + (name.LowerCaseEqualsLiteral("outbox") && nonEnglishApp()); +} + +NS_IMETHODIMP nsMsgDBFolder::SetPrettyName(const nsAString& name) { + nsresult rv; + // Keep original name. + mOriginalName = name; + + // Set pretty name only if special flag is set and if it the default folder + // name + if (mFlags & nsMsgFolderFlags::Inbox && name.LowerCaseEqualsLiteral("inbox")) + rv = SetName(kLocalizedInboxName); + else if (mFlags & nsMsgFolderFlags::SentMail && hasSentName(name)) + rv = SetName(kLocalizedSentName); + else if (mFlags & nsMsgFolderFlags::Drafts && hasDraftsName(name)) + rv = SetName(kLocalizedDraftsName); + else if (mFlags & nsMsgFolderFlags::Templates && + name.LowerCaseEqualsLiteral("templates")) + rv = SetName(kLocalizedTemplatesName); + else if (mFlags & nsMsgFolderFlags::Trash && hasTrashName(name)) + rv = SetName(kLocalizedTrashName); + else if (mFlags & nsMsgFolderFlags::Queue && + name.LowerCaseEqualsLiteral("unsent messages")) + rv = SetName(kLocalizedUnsentName); + else if (mFlags & nsMsgFolderFlags::Junk && + name.LowerCaseEqualsLiteral("junk")) + rv = SetName(kLocalizedJunkName); + else if (mFlags & nsMsgFolderFlags::Archive && + name.LowerCaseEqualsLiteral("archives")) + rv = SetName(kLocalizedArchivesName); + else + rv = SetName(name); + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::SetPrettyNameFromOriginal(void) { + if (mOriginalName.IsEmpty()) return NS_OK; + return SetPrettyName(mOriginalName); +} + +NS_IMETHODIMP nsMsgDBFolder::GetName(nsAString& name) { + nsresult rv; + if (!mHaveParsedURI && mName.IsEmpty()) { + rv = parseURI(); + if (NS_FAILED(rv)) return rv; + } + + // if it's a server, just forward the call + if (mIsServer) { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) return server->GetPrettyName(name); + } + + name = mName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::SetName(const nsAString& name) { + // override the URI-generated name + if (!mName.Equals(name)) { + mName = name; + // old/new value doesn't matter here + NotifyUnicharPropertyChanged(kName, name, name); + } + return NS_OK; +} + +// For default, just return name +NS_IMETHODIMP nsMsgDBFolder::GetAbbreviatedName(nsAString& aAbbreviatedName) { + return GetName(aAbbreviatedName); +} + +NS_IMETHODIMP +nsMsgDBFolder::GetChildNamed(const nsAString& aName, nsIMsgFolder** aChild) { + NS_ENSURE_ARG_POINTER(aChild); + nsTArray<RefPtr<nsIMsgFolder>> dummy; + GetSubFolders(dummy); // initialize mSubFolders + *aChild = nullptr; + + for (nsIMsgFolder* child : mSubFolders) { + nsString folderName; + nsresult rv = child->GetName(folderName); + // case-insensitive compare is probably LCD across OS filesystems + if (NS_SUCCEEDED(rv) && + folderName.Equals(aName, nsCaseInsensitiveStringComparator)) { + NS_ADDREF(*aChild = child); + return NS_OK; + } + } + // don't return NS_OK if we didn't find the folder + // see http://bugzilla.mozilla.org/show_bug.cgi?id=210089#c15 + // and http://bugzilla.mozilla.org/show_bug.cgi?id=210089#c17 + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgDBFolder::GetChildWithURI(const nsACString& uri, bool deep, + bool caseInsensitive, + nsIMsgFolder** child) { + NS_ENSURE_ARG_POINTER(child); + // will return nullptr if we can't find it + *child = nullptr; + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + nsresult rv = GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIMsgFolder* folder : subFolders) { + nsCString folderURI; + rv = folder->GetURI(folderURI); + NS_ENSURE_SUCCESS(rv, rv); + bool equal = + (caseInsensitive + ? uri.Equals(folderURI, nsCaseInsensitiveCStringComparator) + : uri.Equals(folderURI)); + if (equal) { + NS_ADDREF(*child = folder); + return NS_OK; + } + if (deep) { + rv = folder->GetChildWithURI(uri, deep, caseInsensitive, child); + if (NS_FAILED(rv)) return rv; + + if (*child) return NS_OK; + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetShowDeletedMessages(bool* showDeletedMessages) { + NS_ENSURE_ARG_POINTER(showDeletedMessages); + *showDeletedMessages = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::DeleteStorage() { + ForceDBClosed(); + + // Delete the .msf file. + // NOTE: this doesn't remove .msf files in subfolders, but + // both nsMsgBrkMBoxStore::DeleteFolder() and + // nsMsgMaildirStore::DeleteFolder() will remove those .msf files + // as a side-effect of deleting the .sbd directory. + nsCOMPtr<nsIFile> summaryFile; + nsresult rv = GetSummaryFile(getter_AddRefs(summaryFile)); + NS_ENSURE_SUCCESS(rv, rv); + bool exists = false; + summaryFile->Exists(&exists); + if (exists) { + rv = summaryFile->Remove(false); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Ask the msgStore to delete the actual storage (mbox, maildir or whatever + // else may be supported in future). + nsCOMPtr<nsIMsgPluggableStore> msgStore; + rv = GetMsgStore(getter_AddRefs(msgStore)); + NS_ENSURE_SUCCESS(rv, rv); + return msgStore->DeleteFolder(this); +} + +NS_IMETHODIMP nsMsgDBFolder::DeleteSelf(nsIMsgWindow* msgWindow) { + nsCOMPtr<nsIMsgFolder> parent; + GetParent(getter_AddRefs(parent)); + if (!parent) { + return NS_ERROR_FAILURE; + } + return parent->PropagateDelete(this, true); +} + +NS_IMETHODIMP nsMsgDBFolder::CreateStorageIfMissing( + nsIUrlListener* /* urlListener */) { + NS_ASSERTION(false, "needs to be overridden"); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::PropagateDelete(nsIMsgFolder* folder, + bool deleteStorage) { + // first, find the folder we're looking to delete + nsresult rv = NS_OK; + + int32_t count = mSubFolders.Count(); + for (int32_t i = 0; i < count; i++) { + nsCOMPtr<nsIMsgFolder> child(mSubFolders[i]); + if (folder == child.get()) { + // Remove self as parent + child->SetParent(nullptr); + // maybe delete disk storage for it, and its subfolders + rv = child->RecursiveDelete(deleteStorage); + if (NS_SUCCEEDED(rv)) { + // Remove from list of subfolders. + mSubFolders.RemoveObjectAt(i); + NotifyFolderRemoved(child); + break; + } else // setting parent back if we failed + child->SetParent(this); + } else + rv = child->PropagateDelete(folder, deleteStorage); + } + + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::RecursiveDelete(bool deleteStorage) { + // If deleteStorage is true, recursively deletes disk storage for this folder + // and all its subfolders. + // Regardless of deleteStorage, always unlinks them from the children lists + // and frees memory for the subfolders but NOT for _this_ + // and does not remove _this_ from the parent's list of children. + + nsCOMPtr<nsIFile> dbPath; + // first remove the deleted folder from the folder cache; + nsresult rv = GetFolderCacheKey(getter_AddRefs(dbPath)); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgAccountManager> accountMgr = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + nsCOMPtr<nsIMsgFolderCache> folderCache; + rv = accountMgr->GetFolderCache(getter_AddRefs(folderCache)); + if (NS_SUCCEEDED(rv) && folderCache) { + nsCString persistentPath; + rv = dbPath->GetPersistentDescriptor(persistentPath); + if (NS_SUCCEEDED(rv)) folderCache->RemoveElement(persistentPath); + } + } + + int32_t count = mSubFolders.Count(); + while (count > 0) { + nsCOMPtr<nsIMsgFolder> child(mSubFolders[0]); + child->SetParent(nullptr); + rv = child->RecursiveDelete(deleteStorage); + if (NS_SUCCEEDED(rv)) { + // unlink it from this child's list + mSubFolders.RemoveObjectAt(0); + NotifyFolderRemoved(child); + } else { + // setting parent back if we failed for some reason + child->SetParent(this); + break; + } + + count--; + } + + // now delete the disk storage for _this_ + if (deleteStorage && NS_SUCCEEDED(rv)) { + // All delete commands use deleteStorage = true, and local moves use false. + // IMAP moves use true, leaving this here in the hope that bug 439108 + // works out. + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) notifier->NotifyFolderDeleted(this); + rv = DeleteStorage(); + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::CreateSubfolder(const nsAString& folderName, + nsIMsgWindow* msgWindow) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::AddSubfolder(const nsAString& name, + nsIMsgFolder** child) { + NS_ENSURE_ARG_POINTER(child); + + int32_t flags = 0; + nsresult rv; + + nsAutoCString uri(mURI); + uri.Append('/'); + + // URI should use UTF-8 + // (see RFC2396 Uniform Resource Identifiers (URI): Generic Syntax) + nsAutoCString escapedName; + rv = NS_MsgEscapeEncodeURLPath(name, escapedName); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensure the containing (.sbd) dir exists. + nsCOMPtr<nsIFile> path; + rv = CreateDirectoryForFolder(getter_AddRefs(path)); + NS_ENSURE_SUCCESS(rv, rv); + + // fix for #192780 + // if this is the root folder + // make sure the the special folders + // have the right uri. + // on disk, host\INBOX should be a folder with the uri + // mailbox://user@host/Inbox" as mailbox://user@host/Inbox != + // mailbox://user@host/INBOX + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = GetRootFolder(getter_AddRefs(rootFolder)); + if (NS_SUCCEEDED(rv) && rootFolder && + (rootFolder.get() == (nsIMsgFolder*)this)) { + if (escapedName.LowerCaseEqualsLiteral("inbox")) + uri += "Inbox"; + else if (escapedName.LowerCaseEqualsLiteral("unsent%20messages")) + uri += "Unsent%20Messages"; + else if (escapedName.LowerCaseEqualsLiteral("drafts")) + uri += "Drafts"; + else if (escapedName.LowerCaseEqualsLiteral("trash")) + uri += "Trash"; + else if (escapedName.LowerCaseEqualsLiteral("sent")) + uri += "Sent"; + else if (escapedName.LowerCaseEqualsLiteral("templates")) + uri += "Templates"; + else if (escapedName.LowerCaseEqualsLiteral("archives")) + uri += "Archives"; + else + uri += escapedName.get(); + } else + uri += escapedName.get(); + + nsCOMPtr<nsIMsgFolder> msgFolder; + rv = GetChildWithURI(uri, false /*deep*/, true /*case Insensitive*/, + getter_AddRefs(msgFolder)); + if (NS_SUCCEEDED(rv) && msgFolder) return NS_MSG_FOLDER_EXISTS; + + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(uri, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + + folder->GetFlags((uint32_t*)&flags); + flags |= nsMsgFolderFlags::Mail; + folder->SetParent(this); + + bool isServer; + rv = GetIsServer(&isServer); + + // Only set these if these are top level children. + if (NS_SUCCEEDED(rv) && isServer) { + if (name.LowerCaseEqualsLiteral("inbox")) { + flags |= nsMsgFolderFlags::Inbox; + SetBiffState(nsIMsgFolder::nsMsgBiffState_Unknown); + } else if (name.LowerCaseEqualsLiteral("trash")) + flags |= nsMsgFolderFlags::Trash; + else if (name.LowerCaseEqualsLiteral("unsent messages") || + name.LowerCaseEqualsLiteral("outbox")) + flags |= nsMsgFolderFlags::Queue; + } + + folder->SetFlags(flags); + + if (folder) mSubFolders.AppendObject(folder); + + folder.forget(child); + // at this point we must be ok and we don't want to return failure in case + // GetIsServer failed. + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::Compact(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::CompactAll(nsIUrlListener* aListener, + nsIMsgWindow* aMsgWindow) { + NS_ASSERTION(false, "should be overridden by child class"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::EmptyTrash(nsIUrlListener* aListener) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsresult nsMsgDBFolder::CheckIfFolderExists(const nsAString& newFolderName, + nsIMsgFolder* parentFolder, + nsIMsgWindow* msgWindow) { + NS_ENSURE_ARG_POINTER(parentFolder); + nsTArray<RefPtr<nsIMsgFolder>> subFolders; + nsresult rv = parentFolder->GetSubFolders(subFolders); + NS_ENSURE_SUCCESS(rv, rv); + + for (nsIMsgFolder* msgFolder : subFolders) { + nsString folderName; + + msgFolder->GetName(folderName); + if (folderName.Equals(newFolderName, nsCaseInsensitiveStringComparator)) { + ThrowAlertMsg("folderExists", msgWindow); + return NS_MSG_FOLDER_EXISTS; + } + } + return NS_OK; +} + +bool nsMsgDBFolder::ConfirmAutoFolderRename(nsIMsgWindow* msgWindow, + const nsString& aOldName, + const nsString& aNewName) { + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + nsString folderName; + GetName(folderName); + AutoTArray<nsString, 3> formatStrings = {aOldName, folderName, aNewName}; + + nsString confirmString; + rv = bundle->FormatStringFromName("confirmDuplicateFolderRename", + formatStrings, confirmString); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + bool confirmed = false; + rv = ThrowConfirmationPrompt(msgWindow, confirmString, &confirmed); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + return confirmed; +} + +nsresult nsMsgDBFolder::AddDirectorySeparator(nsIFile* path) { + nsAutoString leafName; + path->GetLeafName(leafName); + leafName.AppendLiteral(FOLDER_SUFFIX); + return path->SetLeafName(leafName); +} + +/* Finds the subdirectory associated with this folder. That is if the path is + c:\Inbox, it will return c:\Inbox.sbd if it succeeds. If that path doesn't + currently exist then it will create it. Path is strictly an out parameter. + */ +nsresult nsMsgDBFolder::CreateDirectoryForFolder(nsIFile** resultFile) { + nsCOMPtr<nsIFile> path; + nsresult rv = GetFilePath(getter_AddRefs(path)); + NS_ENSURE_SUCCESS(rv, rv); + + bool isServer; + rv = GetIsServer(&isServer); + NS_ENSURE_SUCCESS(rv, rv); + if (isServer) { + // Server dir doesn't have .sbd suffix. + // Ensure it exists and is a directory. + bool pathExists; + path->Exists(&pathExists); + NS_ENSURE_SUCCESS(rv, rv); + if (!pathExists) { + rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + } else { + bool isDir; + rv = path->IsDirectory(&isDir); + NS_ENSURE_SUCCESS(rv, rv); + if (!isDir) { + return NS_ERROR_UNEXPECTED; + } + } + path.forget(resultFile); + return NS_OK; + } + + // Append .sbd suffix. + rv = AddDirectorySeparator(path); + NS_ENSURE_SUCCESS(rv, rv); + + // Already exists? + bool exists; + rv = path->Exists(&exists); + NS_ENSURE_SUCCESS(rv, rv); + if (exists) { + bool isDir; + rv = path->IsDirectory(&isDir); + NS_ENSURE_SUCCESS(rv, rv); + if (!isDir) { + // Uhoh. Not the dir we were expecting! + return NS_MSG_COULD_NOT_CREATE_DIRECTORY; + } + // Already been created. + path.forget(resultFile); + return NS_OK; + } + + // Need to create it. + rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700); + NS_ENSURE_SUCCESS(rv, rv); + path.forget(resultFile); + return NS_OK; +} + +/* Finds the backup directory associated with this folder, stored on the temp + drive. If that path doesn't currently exist then it will create it. Path is + strictly an out parameter. + */ +nsresult nsMsgDBFolder::CreateBackupDirectory(nsIFile** resultFile) { + nsCOMPtr<nsIFile> path; + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(path)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = path->Append(u"MozillaMailnews"_ns); + bool pathIsDirectory; + path->IsDirectory(&pathIsDirectory); + + // If that doesn't exist, then we have to create this directory + if (!pathIsDirectory) { + bool pathExists; + path->Exists(&pathExists); + // If for some reason there's a file with the directory separator + // then we are going to fail. + rv = pathExists ? NS_MSG_COULD_NOT_CREATE_DIRECTORY + : path->Create(nsIFile::DIRECTORY_TYPE, 0700); + } + if (NS_SUCCEEDED(rv)) path.forget(resultFile); + return rv; +} + +nsresult nsMsgDBFolder::GetBackupSummaryFile(nsIFile** aBackupFile, + const nsACString& newName) { + nsCOMPtr<nsIFile> backupDir; + nsresult rv = CreateBackupDirectory(getter_AddRefs(backupDir)); + NS_ENSURE_SUCCESS(rv, rv); + + // We use a dummy message folder file so we can use + // GetSummaryFileLocation to get the db file name + nsCOMPtr<nsIFile> backupDBDummyFolder; + rv = CreateBackupDirectory(getter_AddRefs(backupDBDummyFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + if (!newName.IsEmpty()) { + rv = backupDBDummyFolder->AppendNative(newName); + } else // if newName is null, use the folder name + { + nsCOMPtr<nsIFile> folderPath; + rv = GetFilePath(getter_AddRefs(folderPath)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString folderName; + rv = folderPath->GetNativeLeafName(folderName); + NS_ENSURE_SUCCESS(rv, rv); + rv = backupDBDummyFolder->AppendNative(folderName); + } + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> backupDBFile; + rv = + GetSummaryFileLocation(backupDBDummyFolder, getter_AddRefs(backupDBFile)); + NS_ENSURE_SUCCESS(rv, rv); + + backupDBFile.forget(aBackupFile); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::Rename(const nsAString& aNewName, + nsIMsgWindow* msgWindow) { + nsCOMPtr<nsIFile> oldPathFile; + nsresult rv = GetFilePath(getter_AddRefs(oldPathFile)); + if (NS_FAILED(rv)) return rv; + nsCOMPtr<nsIMsgFolder> parentFolder; + rv = GetParent(getter_AddRefs(parentFolder)); + if (!parentFolder) return NS_ERROR_FAILURE; + nsCOMPtr<nsISupports> parentSupport = do_QueryInterface(parentFolder); + nsCOMPtr<nsIFile> oldSummaryFile; + rv = GetSummaryFileLocation(oldPathFile, getter_AddRefs(oldSummaryFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> dirFile; + int32_t count = mSubFolders.Count(); + + if (count > 0) rv = CreateDirectoryForFolder(getter_AddRefs(dirFile)); + + nsAutoString newDiskName(aNewName); + NS_MsgHashIfNecessary(newDiskName); + + if (mName.Equals(aNewName, nsCaseInsensitiveStringComparator)) { + rv = ThrowAlertMsg("folderExists", msgWindow); + return NS_MSG_FOLDER_EXISTS; + } else { + nsCOMPtr<nsIFile> parentPathFile; + parentFolder->GetFilePath(getter_AddRefs(parentPathFile)); + NS_ENSURE_SUCCESS(rv, rv); + bool isDirectory = false; + parentPathFile->IsDirectory(&isDirectory); + if (!isDirectory) AddDirectorySeparator(parentPathFile); + + rv = CheckIfFolderExists(aNewName, parentFolder, msgWindow); + if (NS_FAILED(rv)) return rv; + } + + ForceDBClosed(); + + // Save of dir name before appending .msf + nsAutoString newNameDirStr(newDiskName); + + if (!(mFlags & nsMsgFolderFlags::Virtual)) + rv = oldPathFile->MoveTo(nullptr, newDiskName); + if (NS_SUCCEEDED(rv)) { + newDiskName.AppendLiteral(SUMMARY_SUFFIX); + oldSummaryFile->MoveTo(nullptr, newDiskName); + } else { + ThrowAlertMsg("folderRenameFailed", msgWindow); + return rv; + } + + if (NS_SUCCEEDED(rv) && count > 0) { + // rename "*.sbd" directory + newNameDirStr.AppendLiteral(FOLDER_SUFFIX); + dirFile->MoveTo(nullptr, newNameDirStr); + } + + nsCOMPtr<nsIMsgFolder> newFolder; + if (parentSupport) { + rv = parentFolder->AddSubfolder(aNewName, getter_AddRefs(newFolder)); + if (newFolder) { + newFolder->SetPrettyName(EmptyString()); + newFolder->SetPrettyName(aNewName); + newFolder->SetFlags(mFlags); + bool changed = false; + MatchOrChangeFilterDestination(newFolder, true /*case-insensitive*/, + &changed); + if (changed) AlertFilterChanged(msgWindow); + + if (count > 0) newFolder->RenameSubFolders(msgWindow, this); + + if (parentFolder) { + SetParent(nullptr); + parentFolder->PropagateDelete(this, false); + parentFolder->NotifyFolderAdded(newFolder); + } + newFolder->NotifyFolderEvent(kRenameCompleted); + } + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::RenameSubFolders(nsIMsgWindow* msgWindow, + nsIMsgFolder* oldFolder) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::ContainsChildNamed(const nsAString& name, + bool* containsChild) { + NS_ENSURE_ARG_POINTER(containsChild); + nsCOMPtr<nsIMsgFolder> child; + GetChildNamed(name, getter_AddRefs(child)); + *containsChild = child != nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::IsAncestorOf(nsIMsgFolder* child, + bool* isAncestor) { + NS_ENSURE_ARG_POINTER(isAncestor); + nsresult rv = NS_OK; + + int32_t count = mSubFolders.Count(); + + for (int32_t i = 0; i < count; i++) { + nsCOMPtr<nsIMsgFolder> folder(mSubFolders[i]); + if (folder.get() == child) + *isAncestor = true; + else + folder->IsAncestorOf(child, isAncestor); + + if (*isAncestor) return NS_OK; + } + *isAncestor = false; + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::GenerateUniqueSubfolderName( + const nsAString& prefix, nsIMsgFolder* otherFolder, nsAString& name) { + /* only try 256 times */ + for (int count = 0; count < 256; count++) { + nsAutoString uniqueName; + uniqueName.Assign(prefix); + if (count > 0) { + uniqueName.AppendInt(count); + } + bool containsChild; + bool otherContainsChild = false; + ContainsChildNamed(uniqueName, &containsChild); + if (otherFolder) + otherFolder->ContainsChildNamed(uniqueName, &otherContainsChild); + + if (!containsChild && !otherContainsChild) { + name = uniqueName; + break; + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::UpdateSummaryTotals(bool force) { + if (!mNotifyCountChanges) return NS_OK; + + int32_t oldUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages; + int32_t oldTotalMessages = mNumTotalMessages + mNumPendingTotalMessages; + // We need to read this info from the database + nsresult rv = ReadDBFolderInfo(force); + + if (NS_SUCCEEDED(rv)) { + int32_t newUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages; + int32_t newTotalMessages = mNumTotalMessages + mNumPendingTotalMessages; + + // Need to notify listeners that total count changed. + if (oldTotalMessages != newTotalMessages) + NotifyIntPropertyChanged(kTotalMessages, oldTotalMessages, + newTotalMessages); + + if (oldUnreadMessages != newUnreadMessages) + NotifyIntPropertyChanged(kTotalUnreadMessages, oldUnreadMessages, + newUnreadMessages); + + FlushToFolderCache(); + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::SummaryChanged() { + UpdateSummaryTotals(false); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetNumUnread(bool deep, int32_t* numUnread) { + NS_ENSURE_ARG_POINTER(numUnread); + + bool isServer = false; + nsresult rv = GetIsServer(&isServer); + NS_ENSURE_SUCCESS(rv, rv); + int32_t total = isServer ? 0 : mNumUnreadMessages + mNumPendingUnreadMessages; + + if (deep) { + if (total < 0) // deep search never returns negative counts + total = 0; + int32_t count = mSubFolders.Count(); + for (int32_t i = 0; i < count; i++) { + nsCOMPtr<nsIMsgFolder> folder(mSubFolders[i]); + int32_t num; + uint32_t folderFlags; + folder->GetFlags(&folderFlags); + if (!(folderFlags & nsMsgFolderFlags::Virtual)) { + folder->GetNumUnread(deep, &num); + total += num; + } + } + } + *numUnread = total; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetTotalMessages(bool deep, + int32_t* totalMessages) { + NS_ENSURE_ARG_POINTER(totalMessages); + + bool isServer = false; + nsresult rv = GetIsServer(&isServer); + NS_ENSURE_SUCCESS(rv, rv); + int32_t total = isServer ? 0 : mNumTotalMessages + mNumPendingTotalMessages; + + if (deep) { + if (total < 0) // deep search never returns negative counts + total = 0; + int32_t count = mSubFolders.Count(); + for (int32_t i = 0; i < count; i++) { + nsCOMPtr<nsIMsgFolder> folder(mSubFolders[i]); + int32_t num; + uint32_t folderFlags; + folder->GetFlags(&folderFlags); + if (!(folderFlags & nsMsgFolderFlags::Virtual)) { + folder->GetTotalMessages(deep, &num); + total += num; + } + } + } + *totalMessages = total; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetNumPendingUnread(int32_t* aPendingUnread) { + *aPendingUnread = mNumPendingUnreadMessages; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetNumPendingTotalMessages( + int32_t* aPendingTotal) { + *aPendingTotal = mNumPendingTotalMessages; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::ChangeNumPendingUnread(int32_t delta) { + if (delta) { + int32_t oldUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages; + mNumPendingUnreadMessages += delta; + int32_t newUnreadMessages = mNumUnreadMessages + mNumPendingUnreadMessages; + NS_ASSERTION(newUnreadMessages >= 0, + "shouldn't have negative unread message count"); + if (newUnreadMessages >= 0) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsresult rv = + GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && folderInfo) + folderInfo->SetImapUnreadPendingMessages(mNumPendingUnreadMessages); + NotifyIntPropertyChanged(kTotalUnreadMessages, oldUnreadMessages, + newUnreadMessages); + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::ChangeNumPendingTotalMessages(int32_t delta) { + if (delta) { + int32_t oldTotalMessages = mNumTotalMessages + mNumPendingTotalMessages; + mNumPendingTotalMessages += delta; + int32_t newTotalMessages = mNumTotalMessages + mNumPendingTotalMessages; + + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsresult rv = + GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && folderInfo) + folderInfo->SetImapTotalPendingMessages(mNumPendingTotalMessages); + NotifyIntPropertyChanged(kTotalMessages, oldTotalMessages, + newTotalMessages); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::SetFlag(uint32_t flag) { + // If calling this function causes us to open the db (i.e., it was not + // open before), we're going to close the db before returning. + bool dbWasOpen = mDatabase != nullptr; + + ReadDBFolderInfo(false); + // OnFlagChange can be expensive, so don't call it if we don't need to + bool flagSet; + nsresult rv; + + if (NS_FAILED(rv = GetFlag(flag, &flagSet))) return rv; + + if (!flagSet) { + mFlags |= flag; + OnFlagChange(flag); + } + if (!dbWasOpen && mDatabase) SetMsgDatabase(nullptr); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::ClearFlag(uint32_t flag) { + // OnFlagChange can be expensive, so don't call it if we don't need to + bool flagSet; + nsresult rv; + + if (NS_FAILED(rv = GetFlag(flag, &flagSet))) return rv; + + if (flagSet) { + mFlags &= ~flag; + OnFlagChange(flag); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetFlag(uint32_t flag, bool* _retval) { + *_retval = ((mFlags & flag) != 0); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::ToggleFlag(uint32_t flag) { + mFlags ^= flag; + OnFlagChange(flag); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::OnFlagChange(uint32_t flag) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + rv = GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && folderInfo) { +#ifdef DEBUG_bienvenu1 + nsString name; + rv = GetName(name); + NS_ASSERTION(Compare(name, kLocalizedTrashName) || + (mFlags & nsMsgFolderFlags::Trash), + "lost trash flag"); +#endif + folderInfo->SetFlags((int32_t)mFlags); + if (db) db->Commit(nsMsgDBCommitType::kLargeCommit); + + if (mFlags & flag) + NotifyIntPropertyChanged(kFolderFlag, mFlags & ~flag, mFlags); + else + NotifyIntPropertyChanged(kFolderFlag, mFlags | flag, mFlags); + + if (flag & nsMsgFolderFlags::Offline) { + bool newValue = mFlags & nsMsgFolderFlags::Offline; + rv = NotifyBoolPropertyChanged(kSynchronize, !newValue, !!newValue); + } else if (flag & nsMsgFolderFlags::Elided) { + bool newValue = mFlags & nsMsgFolderFlags::Elided; + rv = NotifyBoolPropertyChanged(kOpen, !!newValue, !newValue); + } + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::SetFlags(uint32_t aFlags) { + if (mFlags != aFlags) { + uint32_t changedFlags = aFlags ^ mFlags; + mFlags = aFlags; + OnFlagChange(changedFlags); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetFolderWithFlags(uint32_t aFlags, + nsIMsgFolder** aResult) { + if ((mFlags & aFlags) == aFlags) { + NS_ADDREF(*aResult = this); + return NS_OK; + } + + nsTArray<RefPtr<nsIMsgFolder>> dummy; + GetSubFolders(dummy); // initialize mSubFolders + + int32_t count = mSubFolders.Count(); + *aResult = nullptr; + for (int32_t i = 0; !*aResult && i < count; ++i) + mSubFolders[i]->GetFolderWithFlags(aFlags, aResult); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetFoldersWithFlags( + uint32_t aFlags, nsTArray<RefPtr<nsIMsgFolder>>& aResult) { + aResult.Clear(); + + // Ensure initialisation of mSubFolders. + nsTArray<RefPtr<nsIMsgFolder>> dummy; + GetSubFolders(dummy); + + if ((mFlags & aFlags) == aFlags) { + aResult.AppendElement(this); + } + + // Recurse down through children. + for (nsIMsgFolder* child : mSubFolders) { + nsTArray<RefPtr<nsIMsgFolder>> subMatches; + child->GetFoldersWithFlags(aFlags, subMatches); + aResult.AppendElements(subMatches); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::IsSpecialFolder(uint32_t aFlags, + bool aCheckAncestors, + bool* aIsSpecial) { + NS_ENSURE_ARG_POINTER(aIsSpecial); + + if ((mFlags & aFlags) == 0) { + nsCOMPtr<nsIMsgFolder> parentMsgFolder; + GetParent(getter_AddRefs(parentMsgFolder)); + + if (parentMsgFolder && aCheckAncestors) + parentMsgFolder->IsSpecialFolder(aFlags, aCheckAncestors, aIsSpecial); + else + *aIsSpecial = false; + } else { + // The user can set their INBOX to be their SENT folder. + // in that case, we want this folder to act like an INBOX, + // and not a SENT folder + *aIsSpecial = !((aFlags & nsMsgFolderFlags::SentMail) && + (mFlags & nsMsgFolderFlags::Inbox)); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetDeletable(bool* deletable) { + NS_ENSURE_ARG_POINTER(deletable); + *deletable = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetDisplayRecipients(bool* displayRecipients) { + *displayRecipients = false; + if (mFlags & nsMsgFolderFlags::SentMail && + !(mFlags & nsMsgFolderFlags::Inbox)) + *displayRecipients = true; + else if (mFlags & nsMsgFolderFlags::Queue) + *displayRecipients = true; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::AcquireSemaphore(nsISupports* semHolder) { + nsresult rv = NS_OK; + if (mSemaphoreHolder == NULL) + mSemaphoreHolder = semHolder; // Don't AddRef due to ownership issues. + else + rv = NS_MSG_FOLDER_BUSY; + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::ReleaseSemaphore(nsISupports* semHolder) { + if (!mSemaphoreHolder || mSemaphoreHolder == semHolder) + mSemaphoreHolder = NULL; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::TestSemaphore(nsISupports* semHolder, + bool* result) { + NS_ENSURE_ARG_POINTER(result); + *result = (mSemaphoreHolder == semHolder); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetLocked(bool* isLocked) { + *isLocked = mSemaphoreHolder != NULL; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetRelativePathName(nsACString& pathName) { + pathName.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetSizeOnDisk(int64_t* size) { + NS_ENSURE_ARG_POINTER(size); + *size = kSizeUnknown; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::SetSizeOnDisk(int64_t aSizeOnDisk) { + NotifyIntPropertyChanged(kFolderSize, mFolderSize, aSizeOnDisk); + mFolderSize = aSizeOnDisk; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetUsername(nsACString& userName) { + nsresult rv; + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->GetUsername(userName); +} + +NS_IMETHODIMP nsMsgDBFolder::GetHostname(nsACString& hostName) { + nsresult rv; + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->GetHostName(hostName); +} + +NS_IMETHODIMP nsMsgDBFolder::GetNewMessages(nsIMsgWindow*, + nsIUrlListener* /* aListener */) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::GetBiffState(uint32_t* aBiffState) { + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->GetBiffState(aBiffState); +} + +NS_IMETHODIMP nsMsgDBFolder::SetBiffState(uint32_t aBiffState) { + uint32_t oldBiffState = nsMsgBiffState_Unknown; + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) server->GetBiffState(&oldBiffState); + + if (oldBiffState != aBiffState) { + // Get the server and notify it and not inbox. + if (!mIsServer) { + nsCOMPtr<nsIMsgFolder> folder; + rv = GetRootFolder(getter_AddRefs(folder)); + if (NS_SUCCEEDED(rv) && folder) return folder->SetBiffState(aBiffState); + } + if (server) server->SetBiffState(aBiffState); + + NotifyIntPropertyChanged(kBiffState, oldBiffState, aBiffState); + } else if (aBiffState == oldBiffState && + aBiffState == nsMsgBiffState_NewMail) { + // The folder has been updated, so update the MRUTime + SetMRUTime(); + // biff is already set, but notify that there is additional new mail for the + // folder + NotifyIntPropertyChanged(kNewMailReceived, 0, mNumNewBiffMessages); + } else if (aBiffState == nsMsgBiffState_NoMail) { + // even if the old biff state equals the new biff state, it is still + // possible that we've never cleared the number of new messages for this + // particular folder. This happens when the new mail state got cleared by + // viewing a new message in folder that is different from this one. Biff + // state is stored per server + // the num. of new messages is per folder. + SetNumNewMessages(0); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetNumNewMessages(bool deep, + int32_t* aNumNewMessages) { + NS_ENSURE_ARG_POINTER(aNumNewMessages); + + int32_t numNewMessages = (!deep || !(mFlags & nsMsgFolderFlags::Virtual)) + ? mNumNewBiffMessages + : 0; + if (deep) { + int32_t count = mSubFolders.Count(); + for (int32_t i = 0; i < count; i++) { + int32_t num; + mSubFolders[i]->GetNumNewMessages(deep, &num); + if (num > 0) // it's legal for counts to be negative if we don't know + numNewMessages += num; + } + } + *aNumNewMessages = numNewMessages; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::SetNumNewMessages(int32_t aNumNewMessages) { + if (aNumNewMessages != mNumNewBiffMessages) { + int32_t oldNumMessages = mNumNewBiffMessages; + mNumNewBiffMessages = aNumNewMessages; + + nsAutoCString oldNumMessagesStr; + oldNumMessagesStr.AppendInt(oldNumMessages); + nsAutoCString newNumMessagesStr; + newNumMessagesStr.AppendInt(aNumNewMessages); + NotifyPropertyChanged(kNumNewBiffMessages, oldNumMessagesStr, + newNumMessagesStr); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetRootFolder(nsIMsgFolder** aRootFolder) { + NS_ENSURE_ARG_POINTER(aRootFolder); + nsresult rv; + nsCOMPtr<nsIMsgIncomingServer> server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->GetRootMsgFolder(aRootFolder); +} + +NS_IMETHODIMP +nsMsgDBFolder::SetFilePath(nsIFile* aFile) { + mPath = aFile; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetFilePath(nsIFile** aFile) { + NS_ENSURE_ARG_POINTER(aFile); + nsresult rv; + // make a new nsIFile object in case the caller + // alters the underlying file object. + nsCOMPtr<nsIFile> file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + if (!mPath) { + rv = parseURI(true); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = file->InitWithFile(mPath); + file.forget(aFile); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetSummaryFile(nsIFile** aSummaryFile) { + NS_ENSURE_ARG_POINTER(aSummaryFile); + + nsresult rv; + nsCOMPtr<nsIFile> newSummaryLocation = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> pathFile; + rv = GetFilePath(getter_AddRefs(pathFile)); + NS_ENSURE_SUCCESS(rv, rv); + + newSummaryLocation->InitWithFile(pathFile); + + nsString fileName; + rv = newSummaryLocation->GetLeafName(fileName); + NS_ENSURE_SUCCESS(rv, rv); + + fileName.AppendLiteral(SUMMARY_SUFFIX); + rv = newSummaryLocation->SetLeafName(fileName); + NS_ENSURE_SUCCESS(rv, rv); + + newSummaryLocation.forget(aSummaryFile); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::MarkMessagesRead(const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, + bool markRead) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto message : messages) { + rv = message->MarkRead(markRead); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::MarkMessagesFlagged( + const nsTArray<RefPtr<nsIMsgDBHdr>>& messages, bool markFlagged) { + nsresult rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto message : messages) { + rv = message->MarkFlagged(markFlagged); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::SetJunkScoreForMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, + const nsACString& junkScore) { + GetDatabase(); + if (mDatabase) { + for (auto message : aMessages) { + nsMsgKey msgKey; + (void)message->GetMessageKey(&msgKey); + mDatabase->SetStringProperty(msgKey, "junkscore", junkScore); + mDatabase->SetStringProperty(msgKey, "junkscoreorigin", "filter"_ns); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::ApplyRetentionSettings() { return ApplyRetentionSettings(true); } + +nsresult nsMsgDBFolder::ApplyRetentionSettings(bool deleteViaFolder) { + if (mFlags & nsMsgFolderFlags::Virtual) // ignore virtual folders. + return NS_OK; + bool weOpenedDB = !mDatabase; + nsCOMPtr<nsIMsgRetentionSettings> retentionSettings; + nsresult rv = GetRetentionSettings(getter_AddRefs(retentionSettings)); + if (NS_SUCCEEDED(rv)) { + nsMsgRetainByPreference retainByPreference = + nsIMsgRetentionSettings::nsMsgRetainAll; + + retentionSettings->GetRetainByPreference(&retainByPreference); + if (retainByPreference != nsIMsgRetentionSettings::nsMsgRetainAll) { + rv = GetDatabase(); + NS_ENSURE_SUCCESS(rv, rv); + if (mDatabase) + rv = mDatabase->ApplyRetentionSettings(retentionSettings, + deleteViaFolder); + } + } + // we don't want applying retention settings to keep the db open, because + // if we try to purge a bunch of folders, that will leave the dbs all open. + // So if we opened the db, close it. + if (weOpenedDB) CloseDBIfFolderNotOpen(false); + return rv; +} + +NS_IMETHODIMP +nsMsgDBFolder::DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + nsIMsgWindow* msgWindow, bool deleteStorage, + bool isMove, nsIMsgCopyServiceListener* listener, + bool allowUndo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBFolder::CopyMessages(nsIMsgFolder* srcFolder, + nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, + bool isMove, nsIMsgWindow* window, + nsIMsgCopyServiceListener* listener, bool isFolder, + bool allowUndo) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBFolder::CopyFolder(nsIMsgFolder* srcFolder, bool isMoveFolder, + nsIMsgWindow* window, + nsIMsgCopyServiceListener* listener) { + NS_ASSERTION(false, "should be overridden by child class"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBFolder::CopyFileMessage(nsIFile* aFile, nsIMsgDBHdr* messageToReplace, + bool isDraftOrTemplate, uint32_t aNewMsgFlags, + const nsACString& aNewMsgKeywords, + nsIMsgWindow* window, + nsIMsgCopyServiceListener* listener) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::CopyDataToOutputStreamForAppend( + nsIInputStream* aInStream, int32_t aLength, + nsIOutputStream* aOutputStream) { + if (!aInStream) return NS_OK; + + uint32_t uiWritten; + return aOutputStream->WriteFrom(aInStream, aLength, &uiWritten); +} + +NS_IMETHODIMP nsMsgDBFolder::CopyDataDone() { return NS_OK; } + +#define NOTIFY_LISTENERS(propertyfunc_, params_) \ + PR_BEGIN_MACRO \ + nsTObserverArray<nsCOMPtr<nsIFolderListener>>::ForwardIterator iter( \ + mListeners); \ + nsCOMPtr<nsIFolderListener> listener; \ + while (iter.HasMore()) { \ + listener = iter.GetNext(); \ + listener->propertyfunc_ params_; \ + } \ + PR_END_MACRO + +NS_IMETHODIMP +nsMsgDBFolder::NotifyPropertyChanged(const nsACString& aProperty, + const nsACString& aOldValue, + const nsACString& aNewValue) { + NOTIFY_LISTENERS(OnFolderPropertyChanged, + (this, aProperty, aOldValue, aNewValue)); + + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return folderListenerManager->OnFolderPropertyChanged(this, aProperty, + aOldValue, aNewValue); +} + +NS_IMETHODIMP +nsMsgDBFolder::NotifyUnicharPropertyChanged(const nsACString& aProperty, + const nsAString& aOldValue, + const nsAString& aNewValue) { + NOTIFY_LISTENERS(OnFolderUnicharPropertyChanged, + (this, aProperty, aOldValue, aNewValue)); + + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return folderListenerManager->OnFolderUnicharPropertyChanged( + this, aProperty, aOldValue, aNewValue); +} + +NS_IMETHODIMP +nsMsgDBFolder::NotifyIntPropertyChanged(const nsACString& aProperty, + int64_t aOldValue, int64_t aNewValue) { + // Don't send off count notifications if they are turned off. + if (!mNotifyCountChanges && (aProperty.Equals(kTotalMessages) || + aProperty.Equals(kTotalUnreadMessages))) + return NS_OK; + + NOTIFY_LISTENERS(OnFolderIntPropertyChanged, + (this, aProperty, aOldValue, aNewValue)); + + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return folderListenerManager->OnFolderIntPropertyChanged( + this, aProperty, aOldValue, aNewValue); +} + +NS_IMETHODIMP +nsMsgDBFolder::NotifyBoolPropertyChanged(const nsACString& aProperty, + bool aOldValue, bool aNewValue) { + NOTIFY_LISTENERS(OnFolderBoolPropertyChanged, + (this, aProperty, aOldValue, aNewValue)); + + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return folderListenerManager->OnFolderBoolPropertyChanged( + this, aProperty, aOldValue, aNewValue); +} + +NS_IMETHODIMP +nsMsgDBFolder::NotifyPropertyFlagChanged(nsIMsgDBHdr* aItem, + const nsACString& aProperty, + uint32_t aOldValue, + uint32_t aNewValue) { + NOTIFY_LISTENERS(OnFolderPropertyFlagChanged, + (aItem, aProperty, aOldValue, aNewValue)); + + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return folderListenerManager->OnFolderPropertyFlagChanged( + aItem, aProperty, aOldValue, aNewValue); +} + +NS_IMETHODIMP nsMsgDBFolder::NotifyMessageAdded(nsIMsgDBHdr* msg) { + // Notify our directly-registered listeners. + NOTIFY_LISTENERS(OnMessageAdded, (this, msg)); + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = folderListenerManager->OnMessageAdded(this, msg); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult nsMsgDBFolder::NotifyMessageRemoved(nsIMsgDBHdr* msg) { + // Notify our directly-registered listeners. + NOTIFY_LISTENERS(OnMessageRemoved, (this, msg)); + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = folderListenerManager->OnMessageRemoved(this, msg); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::NotifyFolderAdded(nsIMsgFolder* child) { + NOTIFY_LISTENERS(OnFolderAdded, (this, child)); + + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return folderListenerManager->OnFolderAdded(this, child); +} + +nsresult nsMsgDBFolder::NotifyFolderRemoved(nsIMsgFolder* child) { + NOTIFY_LISTENERS(OnFolderRemoved, (this, child)); + + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return folderListenerManager->OnFolderRemoved(this, child); +} + +nsresult nsMsgDBFolder::NotifyFolderEvent(const nsACString& aEvent) { + NOTIFY_LISTENERS(OnFolderEvent, (this, aEvent)); + + // Notify listeners who listen to every folder + nsresult rv; + nsCOMPtr<nsIFolderListener> folderListenerManager = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return folderListenerManager->OnFolderEvent(this, aEvent); +} + +NS_IMETHODIMP +nsMsgDBFolder::GetFilterList(nsIMsgWindow* aMsgWindow, + nsIMsgFilterList** aResult) { + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->GetFilterList(aMsgWindow, aResult); +} + +NS_IMETHODIMP +nsMsgDBFolder::SetFilterList(nsIMsgFilterList* aFilterList) { + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->SetFilterList(aFilterList); +} + +NS_IMETHODIMP +nsMsgDBFolder::GetEditableFilterList(nsIMsgWindow* aMsgWindow, + nsIMsgFilterList** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->GetEditableFilterList(aMsgWindow, aResult); +} + +NS_IMETHODIMP +nsMsgDBFolder::SetEditableFilterList(nsIMsgFilterList* aFilterList) { + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->SetEditableFilterList(aFilterList); +} + +/* void enableNotifications (in long notificationType, in boolean enable); */ +NS_IMETHODIMP nsMsgDBFolder::EnableNotifications(int32_t notificationType, + bool enable) { + if (notificationType == nsIMsgFolder::allMessageCountNotifications) { + mNotifyCountChanges = enable; + if (enable) { + UpdateSummaryTotals(true); + } + return NS_OK; + } + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::GetMessageHeader(nsMsgKey msgKey, + nsIMsgDBHdr** aMsgHdr) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + nsCOMPtr<nsIMsgDatabase> database; + nsresult rv = GetMsgDatabase(getter_AddRefs(database)); + NS_ENSURE_SUCCESS(rv, rv); + return (database) ? database->GetMsgHdrForKey(msgKey, aMsgHdr) + : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgDBFolder::GetDescendants( + nsTArray<RefPtr<nsIMsgFolder>>& aDescendants) { + aDescendants.Clear(); + for (nsIMsgFolder* child : mSubFolders) { + aDescendants.AppendElement(child); + nsTArray<RefPtr<nsIMsgFolder>> grandchildren; + child->GetDescendants(grandchildren); + aDescendants.AppendElements(grandchildren); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetBaseMessageURI(nsACString& baseMessageURI) { + if (mBaseMessageURI.IsEmpty()) return NS_ERROR_FAILURE; + baseMessageURI = mBaseMessageURI; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetUriForMsg(nsIMsgDBHdr* msgHdr, + nsACString& aURI) { + NS_ENSURE_ARG(msgHdr); + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + nsAutoCString uri; + uri.Assign(mBaseMessageURI); + + // append a "#" followed by the message key. + uri.Append('#'); + uri.AppendInt(msgKey); + aURI = uri; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GenerateMessageURI(nsMsgKey msgKey, + nsACString& aURI) { + nsCString uri; + nsresult rv = GetBaseMessageURI(uri); + NS_ENSURE_SUCCESS(rv, rv); + + // append a "#" followed by the message key. + uri.Append('#'); + uri.AppendInt(msgKey); + aURI = uri; + return NS_OK; +} + +nsresult nsMsgDBFolder::GetBaseStringBundle(nsIStringBundle** aBundle) { + NS_ENSURE_ARG_POINTER(aBundle); + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> bundle; + bundleService->CreateBundle("chrome://messenger/locale/messenger.properties", + getter_AddRefs(bundle)); + bundle.forget(aBundle); + return NS_OK; +} + +// Do not use this routine if you have to call it very often because +// it creates a new bundle each time +nsresult nsMsgDBFolder::GetStringFromBundle(const char* msgName, + nsString& aResult) { + nsresult rv; + nsCOMPtr<nsIStringBundle> bundle; + rv = GetBaseStringBundle(getter_AddRefs(bundle)); + if (NS_SUCCEEDED(rv) && bundle) + rv = bundle->GetStringFromName(msgName, aResult); + return rv; +} + +nsresult nsMsgDBFolder::ThrowConfirmationPrompt(nsIMsgWindow* msgWindow, + const nsAString& confirmString, + bool* confirmed) { + if (msgWindow) { + nsCOMPtr<nsIDocShell> docShell; + msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + if (docShell) { + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell)); + if (dialog && !confirmString.IsEmpty()) + dialog->Confirm(nullptr, nsString(confirmString).get(), confirmed); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBFolder::GetStringWithFolderNameFromBundle(const char* msgName, + nsAString& aResult) { + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle)); + if (NS_SUCCEEDED(rv) && bundle) { + nsString folderName; + GetName(folderName); + AutoTArray<nsString, 2> formatStrings = {folderName, + kLocalizedBrandShortName}; + nsString resultStr; + rv = bundle->FormatStringFromName(msgName, formatStrings, resultStr); + if (NS_SUCCEEDED(rv)) aResult.Assign(resultStr); + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::ConfirmFolderDeletionForFilter( + nsIMsgWindow* msgWindow, bool* confirmed) { + nsString confirmString; + nsresult rv = GetStringWithFolderNameFromBundle( + "confirmFolderDeletionForFilter", confirmString); + NS_ENSURE_SUCCESS(rv, rv); + return ThrowConfirmationPrompt(msgWindow, confirmString, confirmed); +} + +NS_IMETHODIMP nsMsgDBFolder::ThrowAlertMsg(const char* msgName, + nsIMsgWindow* msgWindow) { + if (!msgWindow) { + return NS_OK; + } + + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + // Assemble a pretty folder identifier, e.g. "Trash on bob@example.com". + nsAutoString ident; + nsAutoString folderName; + GetName(folderName); + nsAutoString serverName; + nsCOMPtr<nsIMsgIncomingServer> server; + if (NS_SUCCEEDED(GetServer(getter_AddRefs(server)))) { + server->GetPrettyName(serverName); + bundle->FormatStringFromName("verboseFolderFormat", + {folderName, serverName}, ident); + } + if (ident.IsEmpty()) { + ident = folderName; // Fallback, just in case. + } + + // Format the actual error message (NOTE: not all error messages use the + // params - extra values are just ignored). + nsAutoString alertString; + rv = bundle->FormatStringFromName(msgName, {ident, kLocalizedBrandShortName}, + alertString); + NS_ENSURE_SUCCESS(rv, rv); + + // Include the folder identifier in the alert title for good measure, + // because not all the error messages include the folder. + nsAutoString title; + bundle->FormatStringFromName("folderErrorAlertTitle", {ident}, title); + + nsCOMPtr<mozIDOMWindowProxy> domWindow; + rv = msgWindow->GetDomWindow(getter_AddRefs(domWindow)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPromptService> dlgService( + do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + return dlgService->Alert(domWindow, title.IsEmpty() ? nullptr : title.get(), + alertString.get()); +} + +NS_IMETHODIMP nsMsgDBFolder::AlertFilterChanged(nsIMsgWindow* msgWindow) { + NS_ENSURE_ARG(msgWindow); + nsresult rv = NS_OK; + bool checkBox = false; + GetWarnFilterChanged(&checkBox); + if (!checkBox) { + nsCOMPtr<nsIDocShell> docShell; + msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + nsString alertString; + rv = GetStringFromBundle("alertFilterChanged", alertString); + nsString alertCheckbox; + rv = GetStringFromBundle("alertFilterCheckbox", alertCheckbox); + if (!alertString.IsEmpty() && !alertCheckbox.IsEmpty() && docShell) { + nsCOMPtr<nsIPrompt> dialog(do_GetInterface(docShell)); + if (dialog) { + dialog->AlertCheck(nullptr, alertString.get(), alertCheckbox.get(), + &checkBox); + SetWarnFilterChanged(checkBox); + } + } + } + return rv; +} + +nsresult nsMsgDBFolder::GetWarnFilterChanged(bool* aVal) { + NS_ENSURE_ARG(aVal); + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = prefBranch->GetBoolPref(PREF_MAIL_WARN_FILTER_CHANGED, aVal); + if (NS_FAILED(rv)) *aVal = false; + return NS_OK; +} + +nsresult nsMsgDBFolder::SetWarnFilterChanged(bool aVal) { + nsresult rv = NS_OK; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + return prefBranch->SetBoolPref(PREF_MAIL_WARN_FILTER_CHANGED, aVal); +} + +NS_IMETHODIMP nsMsgDBFolder::NotifyCompactCompleted() { + NS_ASSERTION(false, "should be overridden by child class"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::CloseDBIfFolderNotOpen(bool aForceClosed) { + nsresult rv; + nsCOMPtr<nsIMsgMailSession> session = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + bool folderOpen; + session->IsFolderOpenInWindow(this, &folderOpen); + if (!folderOpen && + !(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox))) { + if (aForceClosed && mDatabase) mDatabase->ForceClosed(); + SetMsgDatabase(nullptr); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::SetSortOrder(int32_t order) { + NS_ASSERTION(false, "not implemented"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::GetSortOrder(int32_t* order) { + NS_ENSURE_ARG_POINTER(order); + + uint32_t flags; + nsresult rv = GetFlags(&flags); + NS_ENSURE_SUCCESS(rv, rv); + + if (flags & nsMsgFolderFlags::Inbox) + *order = 0; + else if (flags & nsMsgFolderFlags::Drafts) + *order = 1; + else if (flags & nsMsgFolderFlags::Templates) + *order = 2; + else if (flags & nsMsgFolderFlags::SentMail) + *order = 3; + else if (flags & nsMsgFolderFlags::Archive) + *order = 4; + else if (flags & nsMsgFolderFlags::Junk) + *order = 5; + else if (flags & nsMsgFolderFlags::Trash) + *order = 6; + else if (flags & nsMsgFolderFlags::Virtual) + *order = 7; + else if (flags & nsMsgFolderFlags::Queue) + *order = 8; + else + *order = 9; + + return NS_OK; +} + +// static Helper function for CompareSortKeys(). +// Builds a collation key for a given folder based on "{sortOrder}{name}" +nsresult nsMsgDBFolder::BuildFolderSortKey(nsIMsgFolder* aFolder, + nsTArray<uint8_t>& aKey) { + aKey.Clear(); + int32_t order; + nsresult rv = aFolder->GetSortOrder(&order); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoString orderString; + orderString.AppendInt(order); + nsString folderName; + rv = aFolder->GetName(folderName); + NS_ENSURE_SUCCESS(rv, rv); + orderString.Append(folderName); + NS_ENSURE_TRUE(gCollationKeyGenerator, NS_ERROR_NULL_POINTER); + + nsTArrayU8Buffer buffer(aKey); + + auto result = gCollationKeyGenerator->GetSortKey(orderString, buffer); + NS_ENSURE_TRUE(result.isOk(), NS_ERROR_FAILURE); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::CompareSortKeys(nsIMsgFolder* aFolder, + int32_t* sortOrder) { + nsTArray<uint8_t> sortKey1; + nsTArray<uint8_t> sortKey2; + nsresult rv = BuildFolderSortKey(this, sortKey1); + NS_ENSURE_SUCCESS(rv, rv); + rv = BuildFolderSortKey(aFolder, sortKey2); + NS_ENSURE_SUCCESS(rv, rv); + *sortOrder = gCollationKeyGenerator->CompareSortKeys(sortKey1, sortKey2); + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::FetchMsgPreviewText( + nsTArray<nsMsgKey> const& aKeysToFetch, nsIUrlListener* aUrlListener, + bool* aAsyncResults) { + NS_ENSURE_ARG_POINTER(aAsyncResults); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgDBFolder::GetMsgTextFromStream( + nsIInputStream* stream, const nsACString& aCharset, uint32_t bytesToRead, + uint32_t aMaxOutputLen, bool aCompressQuotes, bool aStripHTMLTags, + nsACString& aContentType, nsACString& aMsgText) { + /* + 1. non mime message - the message body starts after the blank line + following the headers. + 2. mime message, multipart/alternative - we could simply scan for the + boundary line, advance past its headers, and treat the next few lines + as the text. + 3. mime message, text/plain - body follows headers + 4. multipart/mixed - scan past boundary, treat next part as body. + */ + + UniquePtr<nsLineBuffer<char>> lineBuffer(new nsLineBuffer<char>); + + nsAutoCString msgText; + nsAutoString contentType; + nsAutoString encoding; + nsAutoCString curLine; + nsAutoCString charset(aCharset); + + // might want to use a state var instead of bools. + bool msgBodyIsHtml = false; + bool more = true; + bool reachedEndBody = false; + bool isBase64 = false; + bool inMsgBody = false; + bool justPassedEndBoundary = false; + + uint32_t bytesRead = 0; + + nsresult rv; + + // Both are used to extract data from the headers + nsCOMPtr<nsIMimeHeaders> mimeHeaders( + do_CreateInstance(NS_IMIMEHEADERS_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMIMEHeaderParam> mimeHdrParam( + do_GetService(NS_MIMEHEADERPARAM_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Stack of boundaries, used to figure out where we are + nsTArray<nsCString> boundaryStack; + + while (!inMsgBody && bytesRead <= bytesToRead) { + nsAutoCString msgHeaders; + // We want to NS_ReadLine until we get to a blank line (the end of the + // headers) + while (more) { + rv = NS_ReadLine(stream, lineBuffer.get(), curLine, &more); + NS_ENSURE_SUCCESS(rv, rv); + if (curLine.IsEmpty()) break; + msgHeaders.Append(curLine); + msgHeaders.AppendLiteral("\r\n"); + bytesRead += curLine.Length(); + if (bytesRead > bytesToRead) break; + } + + // There's no point in processing if we can't get the body + if (bytesRead > bytesToRead) break; + + // Process the headers, looking for things we need + rv = mimeHeaders->Initialize(msgHeaders); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString contentTypeHdr; + mimeHeaders->ExtractHeader("Content-Type", false, contentTypeHdr); + + // Get the content type + // If we don't have a content type, then we assign text/plain + // this is in violation of the RFC for multipart/digest, though + // Also, if we've just passed an end boundary, we're going to ignore this. + if (!justPassedEndBoundary && contentTypeHdr.IsEmpty()) + contentType.AssignLiteral(u"text/plain"); + else + mimeHdrParam->GetParameter(contentTypeHdr, nullptr, EmptyCString(), false, + nullptr, contentType); + + justPassedEndBoundary = false; + + // If we are multipart, then we need to get the boundary + if (StringBeginsWith(contentType, u"multipart/"_ns, + nsCaseInsensitiveStringComparator)) { + nsAutoString boundaryParam; + mimeHdrParam->GetParameter(contentTypeHdr, "boundary", EmptyCString(), + false, nullptr, boundaryParam); + if (!boundaryParam.IsEmpty()) { + nsAutoCString boundary("--"_ns); + boundary.Append(NS_ConvertUTF16toUTF8(boundaryParam)); + boundaryStack.AppendElement(boundary); + } + } + + // If we are message/rfc822, then there's another header block coming up + else if (contentType.LowerCaseEqualsLiteral("message/rfc822")) + continue; + + // If we are a text part, then we want it + else if (StringBeginsWith(contentType, u"text/"_ns, + nsCaseInsensitiveStringComparator)) { + inMsgBody = true; + + if (contentType.LowerCaseEqualsLiteral("text/html")) msgBodyIsHtml = true; + + // Also get the charset if required + if (charset.IsEmpty()) { + nsAutoString charsetW; + mimeHdrParam->GetParameter(contentTypeHdr, "charset", EmptyCString(), + false, nullptr, charsetW); + charset.Assign(NS_ConvertUTF16toUTF8(charsetW)); + } + + // Finally, get the encoding + nsAutoCString encodingHdr; + mimeHeaders->ExtractHeader("Content-Transfer-Encoding", false, + encodingHdr); + if (!encodingHdr.IsEmpty()) + mimeHdrParam->GetParameter(encodingHdr, nullptr, EmptyCString(), false, + nullptr, encoding); + + if (encoding.LowerCaseEqualsLiteral(ENCODING_BASE64)) isBase64 = true; + } + + // We need to consume the rest, until the next headers + uint32_t count = boundaryStack.Length(); + nsAutoCString boundary; + nsAutoCString endBoundary; + if (count) { + boundary.Assign(boundaryStack.ElementAt(count - 1)); + endBoundary.Assign(boundary); + endBoundary.AppendLiteral("--"); + } + while (more) { + rv = NS_ReadLine(stream, lineBuffer.get(), curLine, &more); + NS_ENSURE_SUCCESS(rv, rv); + + if (count) { + // If we've reached a MIME final delimiter, pop and break + if (StringBeginsWith(curLine, endBoundary)) { + if (inMsgBody) reachedEndBody = true; + boundaryStack.RemoveElementAt(count - 1); + justPassedEndBoundary = true; + break; + } + // If we've reached the end of this MIME part, we can break + if (StringBeginsWith(curLine, boundary)) { + if (inMsgBody) reachedEndBody = true; + break; + } + } + + // Only append the text if we're actually in the message body + if (inMsgBody) { + msgText.Append(curLine); + if (!isBase64) msgText.AppendLiteral("\r\n"); + } + + bytesRead += curLine.Length(); + if (bytesRead > bytesToRead) break; + } + } + lineBuffer.reset(); + + // if the snippet is encoded, decode it + if (!encoding.IsEmpty()) + decodeMsgSnippet(NS_ConvertUTF16toUTF8(encoding), !reachedEndBody, msgText); + + // In order to turn our snippet into unicode, we need to convert it from the + // charset we detected earlier. + nsString unicodeMsgBodyStr; + nsMsgI18NConvertToUnicode(charset, msgText, unicodeMsgBodyStr); + + // now we've got a msg body. If it's html, convert it to plain text. + if (msgBodyIsHtml && aStripHTMLTags) + ConvertMsgSnippetToPlainText(unicodeMsgBodyStr, unicodeMsgBodyStr); + + // We want to remove any whitespace from the beginning and end of the string + unicodeMsgBodyStr.Trim(" \t\r\n", true, true); + + // step 3, optionally remove quoted text from the snippet + nsString compressedQuotesMsgStr; + if (aCompressQuotes) + compressQuotesInMsgSnippet(unicodeMsgBodyStr, compressedQuotesMsgStr); + + // now convert back to utf-8 which is more convenient for storage + CopyUTF16toUTF8(aCompressQuotes ? compressedQuotesMsgStr : unicodeMsgBodyStr, + aMsgText); + + // finally, truncate the string based on aMaxOutputLen + if (aMsgText.Length() > aMaxOutputLen) { + if (NS_IsAscii(aMsgText.BeginReading())) + aMsgText.SetLength(aMaxOutputLen); + else + nsMsgI18NShrinkUTF8Str(nsCString(aMsgText), aMaxOutputLen, aMsgText); + } + + // Also assign the content type being returned + aContentType.Assign(NS_ConvertUTF16toUTF8(contentType)); + return rv; +} + +/** + * decodeMsgSnippet - helper function which applies the appropriate transfer + * decoding to the message snippet based on aEncodingType. Currently handles + * base64 and quoted-printable. If aEncodingType refers to an encoding we + * don't handle, the message data is passed back unmodified. + * @param aEncodingType the encoding type (base64, quoted-printable) + * @param aIsComplete the snippet is actually the entire message so the + * decoder doesn't have to worry about partial data + * @param aMsgSnippet in/out argument. The encoded msg snippet and then the + * decoded snippet + */ +void nsMsgDBFolder::decodeMsgSnippet(const nsACString& aEncodingType, + bool aIsComplete, nsCString& aMsgSnippet) { + if (aEncodingType.LowerCaseEqualsLiteral(ENCODING_BASE64)) { + int32_t base64Len = aMsgSnippet.Length(); + if (aIsComplete) base64Len -= base64Len % 4; + char* decodedBody = PL_Base64Decode(aMsgSnippet.get(), base64Len, nullptr); + if (decodedBody) aMsgSnippet.Adopt(decodedBody); + } else if (aEncodingType.LowerCaseEqualsLiteral(ENCODING_QUOTED_PRINTABLE)) { + MsgStripQuotedPrintable(aMsgSnippet); + } +} + +/** + * stripQuotesFromMsgSnippet - Reduces quoted reply text including the citation + * (Scott wrote:) from the message snippet to " ... ". Assumes the snippet has + * been decoded and converted to plain text. + * @param aMsgSnippet in/out argument. The string to strip quotes from. + */ +void nsMsgDBFolder::compressQuotesInMsgSnippet(const nsString& aMsgSnippet, + nsAString& aCompressedQuotes) { + int32_t msgBodyStrLen = aMsgSnippet.Length(); + bool lastLineWasAQuote = false; + int32_t offset = 0; + int32_t lineFeedPos = 0; + while (offset < msgBodyStrLen) { + lineFeedPos = aMsgSnippet.FindChar('\n', offset); + if (lineFeedPos != -1) { + const nsAString& currentLine = + Substring(aMsgSnippet, offset, lineFeedPos - offset); + // this catches quoted text ("> "), nested quotes of any level (">> ", + // ">>> ", ...) it also catches empty line quoted text (">"). It might be + // over aggressive and require tweaking later. Try to strip the citation. + // If the current line ends with a ':' and the next line looks like a + // quoted reply (starts with a ">") skip the current line + if (StringBeginsWith(currentLine, u">"_ns) || + (lineFeedPos + 1 < msgBodyStrLen && lineFeedPos && + aMsgSnippet[lineFeedPos - 1] == char16_t(':') && + aMsgSnippet[lineFeedPos + 1] == char16_t('>'))) { + lastLineWasAQuote = true; + } else if (!currentLine.IsEmpty()) { + if (lastLineWasAQuote) { + aCompressedQuotes += u" ... "_ns; + lastLineWasAQuote = false; + } + + aCompressedQuotes += currentLine; + // Don't forget to substitute a space for the line feed. + aCompressedQuotes += char16_t(' '); + } + + offset = lineFeedPos + 1; + } else { + aCompressedQuotes.Append( + Substring(aMsgSnippet, offset, msgBodyStrLen - offset)); + break; + } + } +} + +NS_IMETHODIMP nsMsgDBFolder::ConvertMsgSnippetToPlainText( + const nsAString& aMessageText, nsAString& aOutText) { + uint32_t flags = nsIDocumentEncoder::OutputLFLineBreak | + nsIDocumentEncoder::OutputNoScriptContent | + nsIDocumentEncoder::OutputNoFramesContent | + nsIDocumentEncoder::OutputBodyOnly; + nsCOMPtr<nsIParserUtils> utils = do_GetService(NS_PARSERUTILS_CONTRACTID); + return utils->ConvertToPlainText(aMessageText, flags, 80, aOutText); +} + +nsresult nsMsgDBFolder::GetMsgPreviewTextFromStream(nsIMsgDBHdr* msgHdr, + nsIInputStream* stream) { + nsCString msgBody; + nsAutoCString charset; + msgHdr->GetCharset(getter_Copies(charset)); + nsAutoCString contentType; + nsresult rv = GetMsgTextFromStream(stream, charset, 4096, 255, true, true, + contentType, msgBody); + // replaces all tabs and line returns with a space, + // then trims off leading and trailing white space + msgBody.CompressWhitespace(); + msgHdr->SetStringProperty("preview", msgBody); + return rv; +} + +void nsMsgDBFolder::UpdateTimestamps(bool allowUndo) { + if (!(mFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Junk))) { + SetMRUTime(); + if (allowUndo) // This is a proxy for a user-initiated act. + { + bool isArchive; + IsSpecialFolder(nsMsgFolderFlags::Archive, true, &isArchive); + if (!isArchive) { + SetMRMTime(); + NotifyFolderEvent(kMRMTimeChanged); + } + } + } +} + +void nsMsgDBFolder::SetMRUTime() { + uint32_t seconds; + PRTime2Seconds(PR_Now(), &seconds); + nsAutoCString nowStr; + nowStr.AppendInt(seconds); + SetStringProperty(MRU_TIME_PROPERTY, nowStr); +} + +void nsMsgDBFolder::SetMRMTime() { + uint32_t seconds; + PRTime2Seconds(PR_Now(), &seconds); + nsAutoCString nowStr; + nowStr.AppendInt(seconds); + SetStringProperty(MRM_TIME_PROPERTY, nowStr); +} + +NS_IMETHODIMP nsMsgDBFolder::AddKeywordsToMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, + const nsACString& aKeywords) { + nsresult rv = NS_OK; + GetDatabase(); + if (mDatabase) { + nsCString keywords; + + for (auto message : aMessages) { + message->GetStringProperty("keywords", keywords); + nsTArray<nsCString> keywordArray; + ParseString(aKeywords, ' ', keywordArray); + uint32_t addCount = 0; + for (uint32_t j = 0; j < keywordArray.Length(); j++) { + int32_t start, length; + if (!MsgFindKeyword(keywordArray[j], keywords, &start, &length)) { + if (!keywords.IsEmpty()) keywords.Append(' '); + keywords.Append(keywordArray[j]); + addCount++; + } + } + // avoid using the message key to set the string property, because + // in the case of filters running on incoming pop3 mail with quarantining + // turned on, the message key is wrong. + mDatabase->SetStringPropertyByHdr(message, "keywords", keywords); + + if (addCount) NotifyPropertyFlagChanged(message, kKeywords, 0, addCount); + } + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::RemoveKeywordsFromMessages( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMessages, + const nsACString& aKeywords) { + nsresult rv = NS_OK; + GetDatabase(); + if (mDatabase) { + NS_ENSURE_SUCCESS(rv, rv); + nsTArray<nsCString> keywordArray; + ParseString(aKeywords, ' ', keywordArray); + nsCString keywords; + // If the tag is also a label, we should remove the label too... + + for (auto message : aMessages) { + rv = message->GetStringProperty("keywords", keywords); + uint32_t removeCount = 0; + for (uint32_t j = 0; j < keywordArray.Length(); j++) { + int32_t startOffset, length; + if (MsgFindKeyword(keywordArray[j], keywords, &startOffset, &length)) { + // delete any leading space delimiters + while (startOffset && keywords.CharAt(startOffset - 1) == ' ') { + startOffset--; + length++; + } + // but if the keyword is at the start then delete the following space + if (!startOffset && + length < static_cast<int32_t>(keywords.Length()) && + keywords.CharAt(length) == ' ') + length++; + keywords.Cut(startOffset, length); + removeCount++; + } + } + + if (removeCount) { + mDatabase->SetStringPropertyByHdr(message, "keywords", keywords); + NotifyPropertyFlagChanged(message, kKeywords, removeCount, 0); + } + } + } + return rv; +} + +NS_IMETHODIMP nsMsgDBFolder::GetCustomIdentity(nsIMsgIdentity** aIdentity) { + NS_ENSURE_ARG_POINTER(aIdentity); + *aIdentity = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::GetProcessingFlags(nsMsgKey aKey, + uint32_t* aFlags) { + NS_ENSURE_ARG_POINTER(aFlags); + *aFlags = 0; + for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++) + if (mProcessingFlag[i].keys && mProcessingFlag[i].keys->IsMember(aKey)) + *aFlags |= mProcessingFlag[i].bit; + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::OrProcessingFlags(nsMsgKey aKey, uint32_t mask) { + for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++) + if (mProcessingFlag[i].bit & mask && mProcessingFlag[i].keys) + mProcessingFlag[i].keys->Add(aKey); + return NS_OK; +} + +NS_IMETHODIMP nsMsgDBFolder::AndProcessingFlags(nsMsgKey aKey, uint32_t mask) { + for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++) + if (!(mProcessingFlag[i].bit & mask) && mProcessingFlag[i].keys) + mProcessingFlag[i].keys->Remove(aKey); + return NS_OK; +} + +// Each implementation must provide an override of this, connecting the folder +// type to the corresponding incoming server type. +NS_IMETHODIMP nsMsgDBFolder::GetIncomingServerType( + nsACString& aIncomingServerType) { + NS_ASSERTION(false, "subclasses need to override this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +void nsMsgDBFolder::ClearProcessingFlags() { + for (uint32_t i = 0; i < nsMsgProcessingFlags::NumberOfFlags; i++) { + // There is no clear method so we need to delete and re-create. + delete mProcessingFlag[i].keys; + mProcessingFlag[i].keys = nsMsgKeySetU::Create(); + } +} + +nsresult nsMsgDBFolder::MessagesInKeyOrder( + nsTArray<nsMsgKey> const& aKeyArray, nsIMsgFolder* srcFolder, + nsTArray<RefPtr<nsIMsgDBHdr>>& messages) { + messages.Clear(); + messages.SetCapacity(aKeyArray.Length()); + + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = srcFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && db) { + for (auto key : aKeyArray) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (msgHdr) messages.AppendElement(msgHdr); + } + } + return rv; +} + +/* static */ nsMsgKeySetU* nsMsgKeySetU::Create() { + nsMsgKeySetU* set = new nsMsgKeySetU; + if (set) { + set->loKeySet = nsMsgKeySet::Create(); + set->hiKeySet = nsMsgKeySet::Create(); + if (!(set->loKeySet && set->hiKeySet)) { + delete set; + set = nullptr; + } + } + return set; +} + +nsMsgKeySetU::nsMsgKeySetU() : hiKeySet(nullptr) {} + +nsMsgKeySetU::~nsMsgKeySetU() {} + +const uint32_t kLowerBits = 0x7fffffff; + +int nsMsgKeySetU::Add(nsMsgKey aKey) { + int32_t intKey = static_cast<int32_t>(aKey); + if (intKey >= 0) return loKeySet->Add(intKey); + return hiKeySet->Add(intKey & kLowerBits); +} + +int nsMsgKeySetU::Remove(nsMsgKey aKey) { + int32_t intKey = static_cast<int32_t>(aKey); + if (intKey >= 0) return loKeySet->Remove(intKey); + return hiKeySet->Remove(intKey & kLowerBits); +} + +bool nsMsgKeySetU::IsMember(nsMsgKey aKey) { + int32_t intKey = static_cast<int32_t>(aKey); + if (intKey >= 0) return loKeySet->IsMember(intKey); + return hiKeySet->IsMember(intKey & kLowerBits); +} + +nsresult nsMsgKeySetU::ToMsgKeyArray(nsTArray<nsMsgKey>& aArray) { + nsresult rv = loKeySet->ToMsgKeyArray(aArray); + NS_ENSURE_SUCCESS(rv, rv); + return hiKeySet->ToMsgKeyArray(aArray); +} diff --git a/comm/mailnews/base/src/nsMsgDBFolder.h b/comm/mailnews/base/src/nsMsgDBFolder.h new file mode 100644 index 0000000000..1cd01199f6 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgDBFolder.h @@ -0,0 +1,366 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgDBFolder_h__ +#define nsMsgDBFolder_h__ + +#include "mozilla/Attributes.h" +#include "msgCore.h" +#include "nsIMsgFolder.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgIncomingServer.h" +#include "nsCOMPtr.h" +#include "nsIDBChangeListener.h" +#include "nsIMsgPluggableStore.h" +#include "nsIURL.h" +#include "nsIFile.h" +#include "nsWeakReference.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIMsgFilterList.h" +#include "nsIUrlListener.h" +#include "nsIMsgHdr.h" +#include "nsIOutputStream.h" +#include "nsITransport.h" +#include "nsIStringBundle.h" +#include "nsTObserverArray.h" +#include "nsCOMArray.h" +#include "nsMsgKeySet.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgFilterPlugin.h" +#include "mozilla/intl/Collator.h" + +// We declare strings for folder properties and events. +// Properties: +extern const nsLiteralCString kBiffState; +extern const nsLiteralCString kCanFileMessages; +extern const nsLiteralCString kDefaultServer; +extern const nsLiteralCString kFlagged; +extern const nsLiteralCString kFolderFlag; +extern const nsLiteralCString kFolderSize; +extern const nsLiteralCString kIsDeferred; +extern const nsLiteralCString kIsSecure; +extern const nsLiteralCString kJunkStatusChanged; +extern const nsLiteralCString kKeywords; +extern const nsLiteralCString kMRMTimeChanged; +extern const nsLiteralCString kMsgLoaded; +extern const nsLiteralCString kName; +extern const nsLiteralCString kNewMailReceived; +extern const nsLiteralCString kNewMessages; +extern const nsLiteralCString kOpen; +extern const nsLiteralCString kSortOrder; +extern const nsLiteralCString kStatus; +extern const nsLiteralCString kSynchronize; +extern const nsLiteralCString kTotalMessages; +extern const nsLiteralCString kTotalUnreadMessages; + +// Events: +extern const nsLiteralCString kAboutToCompact; +extern const nsLiteralCString kCompactCompleted; +extern const nsLiteralCString kDeleteOrMoveMsgCompleted; +extern const nsLiteralCString kDeleteOrMoveMsgFailed; +extern const nsLiteralCString kFiltersApplied; +extern const nsLiteralCString kFolderCreateCompleted; +extern const nsLiteralCString kFolderCreateFailed; +extern const nsLiteralCString kFolderLoaded; +extern const nsLiteralCString kNumNewBiffMessages; +extern const nsLiteralCString kRenameCompleted; + +using mozilla::intl::Collator; + +class nsIMsgFolderCacheElement; +class nsMsgKeySetU; + +class nsMsgFolderService final : public nsIMsgFolderService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGFOLDERSERVICE + + nsMsgFolderService(){}; + + protected: + ~nsMsgFolderService(){}; +}; + +/* + * nsMsgDBFolder + * class derived from nsMsgFolder for those folders that use an nsIMsgDatabase + */ +class nsMsgDBFolder : public nsSupportsWeakReference, + public nsIMsgFolder, + public nsIDBChangeListener, + public nsIUrlListener, + public nsIJunkMailClassificationListener, + public nsIMsgTraitClassificationListener { + public: + friend class nsMsgFolderService; + + nsMsgDBFolder(void); + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMSGFOLDER + NS_DECL_NSIDBCHANGELISTENER + NS_DECL_NSIURLLISTENER + NS_DECL_NSIJUNKMAILCLASSIFICATIONLISTENER + NS_DECL_NSIMSGTRAITCLASSIFICATIONLISTENER + + NS_IMETHOD WriteToFolderCacheElem(nsIMsgFolderCacheElement* element); + NS_IMETHOD ReadFromFolderCacheElem(nsIMsgFolderCacheElement* element); + + nsresult CreateDirectoryForFolder(nsIFile** result); + nsresult CreateBackupDirectory(nsIFile** result); + nsresult GetBackupSummaryFile(nsIFile** result, const nsACString& newName); + nsresult GetMsgPreviewTextFromStream(nsIMsgDBHdr* msgHdr, + nsIInputStream* stream); + nsresult HandleAutoCompactEvent(nsIMsgWindow* aMsgWindow); + static int gIsEnglishApp; + + protected: + virtual ~nsMsgDBFolder(); + + virtual nsresult CreateBaseMessageURI(const nsACString& aURI); + + void compressQuotesInMsgSnippet(const nsString& aMessageText, + nsAString& aCompressedQuotesStr); + void decodeMsgSnippet(const nsACString& aEncodingType, bool aIsComplete, + nsCString& aMsgSnippet); + + // helper routine to parse the URI and update member variables + nsresult parseURI(bool needServer = false); + nsresult GetBaseStringBundle(nsIStringBundle** aBundle); + nsresult GetStringFromBundle(const char* msgName, nsString& aResult); + nsresult ThrowConfirmationPrompt(nsIMsgWindow* msgWindow, + const nsAString& confirmString, + bool* confirmed); + nsresult GetWarnFilterChanged(bool* aVal); + nsresult SetWarnFilterChanged(bool aVal); + nsresult CreateCollationKey(const nsString& aSource, uint8_t** aKey, + uint32_t* aLength); + + // All children will override this to create the right class of object. + virtual nsresult CreateChildFromURI(const nsACString& uri, + nsIMsgFolder** folder) = 0; + virtual nsresult ReadDBFolderInfo(bool force); + virtual nsresult FlushToFolderCache(); + virtual nsresult GetDatabase() = 0; + virtual nsresult SendFlagNotifications(nsIMsgDBHdr* item, uint32_t oldFlags, + uint32_t newFlags); + + // Overriden by IMAP to handle gmail hack. + virtual nsresult GetOfflineFileStream(nsMsgKey msgKey, uint64_t* offset, + uint32_t* size, + nsIInputStream** aFileStream); + + nsresult CheckWithNewMessagesStatus(bool messageAdded); + void UpdateNewMessages(); + nsresult OnHdrAddedOrDeleted(nsIMsgDBHdr* hdrChanged, bool added); + nsresult CreateFileForDB(const nsAString& userLeafName, nsIFile* baseDir, + nsIFile** dbFile); + + nsresult GetFolderCacheKey(nsIFile** aFile); + nsresult GetFolderCacheElemFromFile(nsIFile* file, + nsIMsgFolderCacheElement** cacheElement); + nsresult AddDirectorySeparator(nsIFile* path); + nsresult CheckIfFolderExists(const nsAString& newFolderName, + nsIMsgFolder* parentFolder, + nsIMsgWindow* msgWindow); + bool ConfirmAutoFolderRename(nsIMsgWindow* aMsgWindow, + const nsString& aOldName, + const nsString& aNewName); + + // Returns true if: a) there is no need to prompt or b) the user is already + // logged in or c) the user logged in successfully. + static bool PromptForMasterPasswordIfNecessary(); + + // Offline support methods. Used by IMAP and News folders, but not local + // folders. + nsresult StartNewOfflineMessage(); + nsresult WriteStartOfNewLocalMessage(); + nsresult EndNewOfflineMessage(nsresult status); + + nsresult AutoCompact(nsIMsgWindow* aWindow); + // this is a helper routine that ignores whether nsMsgMessageFlags::Offline is + // set for the folder + nsresult MsgFitsDownloadCriteria(nsMsgKey msgKey, bool* result); + nsresult GetPromptPurgeThreshold(bool* aPrompt); + nsresult GetPurgeThreshold(int32_t* aThreshold); + nsresult ApplyRetentionSettings(bool deleteViaFolder); + MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult AddMarkAllReadUndoAction( + nsIMsgWindow* msgWindow, nsMsgKey* thoseMarked, uint32_t numMarked); + + nsresult PerformBiffNotifications( + void); // if there are new, non spam messages, do biff + + // Helper function for Move code to call to update the MRU and MRM time. + void UpdateTimestamps(bool allowUndo); + void SetMRUTime(); + void SetMRMTime(); + /** + * Clear all processing flags, presumably because message keys are no longer + * valid. + */ + void ClearProcessingFlags(); + + nsresult NotifyHdrsNotBeingClassified(); + static nsresult BuildFolderSortKey(nsIMsgFolder* aFolder, + nsTArray<uint8_t>& aKey); + /** + * Produce an array of messages ordered like the input keys. + */ + nsresult MessagesInKeyOrder(nsTArray<nsMsgKey> const& aKeyArray, + nsIMsgFolder* srcFolder, + nsTArray<RefPtr<nsIMsgDBHdr>>& messages); + nsCString mURI; + + nsCOMPtr<nsIMsgDatabase> mDatabase; + nsCOMPtr<nsIMsgDatabase> mBackupDatabase; + bool mAddListener; + bool mNewMessages; + bool mGettingNewMessages; + nsMsgKey mLastMessageLoaded; + + /* + * Start of offline-message-writing vars. + * These track offline message writing for IMAP and News folders. + * But *not* for local folders, which do their own thing. + * They are set up by StartNewOfflineMessage() and cleaned up + * by EndNewOfflineMessage(). + * IMAP folder also uses these vars when saving messages to disk. + */ + + // The header of the message currently being written. + nsCOMPtr<nsIMsgDBHdr> m_offlineHeader; + int32_t m_numOfflineMsgLines; + // Number of bytes added due to add X-Mozilla-* headers. + int32_t m_bytesAddedToLocalMsg; + // This is currently used when we do a save as of an imap or news message.. + // Also used by IMAP/News offline messsage writing. + nsCOMPtr<nsIOutputStream> m_tempMessageStream; + // The number of bytes written to m_tempMessageStream so far. + uint32_t m_tempMessageStreamBytesWritten; + + /* + * End of offline message tracking vars + */ + + nsCOMPtr<nsIMsgRetentionSettings> m_retentionSettings; + nsCOMPtr<nsIMsgDownloadSettings> m_downloadSettings; + static nsrefcnt mInstanceCount; + + uint32_t mFlags; + nsWeakPtr mParent; // This won't be refcounted for ownership reasons. + int32_t mNumUnreadMessages; /* count of unread messages (-1 means unknown; -2 + means unknown but we already tried to find + out.) */ + int32_t mNumTotalMessages; /* count of existing messages. */ + bool mNotifyCountChanges; + int64_t mExpungedBytes; + nsCOMArray<nsIMsgFolder> mSubFolders; + nsTObserverArray<nsCOMPtr<nsIFolderListener>> mListeners; + + bool mInitializedFromCache; + nsISupports* mSemaphoreHolder; // set when the folder is being written to + // Due to ownership issues, this won't be + // AddRef'd. + + nsWeakPtr mServer; + + // These values are used for tricking the front end into thinking that we have + // more messages than are really in the DB. This is usually after and IMAP + // message copy where we don't want to do an expensive select until the user + // actually opens that folder + int32_t mNumPendingUnreadMessages; + int32_t mNumPendingTotalMessages; + int64_t mFolderSize; + + int32_t mNumNewBiffMessages; + + // these are previous set of new msgs, which we might + // want to run junk controls on. This is in addition to "new" hdrs + // in the db, which might get cleared because the user clicked away + // from the folder. + nsTArray<nsMsgKey> m_saveNewMsgs; + + // These are the set of new messages for a folder who has had + // its db closed, without the user reading the folder. This + // happens with pop3 mail filtered to a different local folder. + nsTArray<nsMsgKey> m_newMsgs; + + // + // stuff from the uri + // + bool mHaveParsedURI; // is the URI completely parsed? + bool mIsServerIsValid; + bool mIsServer; + nsString mName; + nsString mOriginalName; + nsCOMPtr<nsIFile> mPath; + nsCString mBaseMessageURI; // The uri with the message scheme + + // static stuff for cross-instance objects like atoms + static nsrefcnt gInstanceCount; + + static nsresult initializeStrings(); + static nsresult createCollationKeyGenerator(); + + static nsString kLocalizedInboxName; + static nsString kLocalizedTrashName; + static nsString kLocalizedSentName; + static nsString kLocalizedDraftsName; + static nsString kLocalizedTemplatesName; + static nsString kLocalizedUnsentName; + static nsString kLocalizedJunkName; + static nsString kLocalizedArchivesName; + + static nsString kLocalizedBrandShortName; + + static mozilla::UniquePtr<mozilla::intl::Collator> gCollationKeyGenerator; + static bool gInitializeStringsDone; + + // store of keys that have a processing flag set + struct { + uint32_t bit; + nsMsgKeySetU* keys; + } mProcessingFlag[nsMsgProcessingFlags::NumberOfFlags]; + + // list of nsIMsgDBHdrs for messages to process post-bayes + nsTArray<RefPtr<nsIMsgDBHdr>> mPostBayesMessagesToFilter; + + /** + * The list of message keys that have been classified for msgsClassified + * batch notification purposes. We add to this list in OnMessageClassified + * when we are told about a classified message (a URI is provided), and we + * notify for the list and clear it when we are told all the messages in + * the batch were classified (a URI is not provided). + */ + nsTArray<nsMsgKey> mClassifiedMsgKeys; + // Is the current bayes filtering doing junk classification? + bool mBayesJunkClassifying; + // Is the current bayes filtering doing trait classification? + bool mBayesTraitClassifying; +}; + +// This class is a kludge to allow nsMsgKeySet to be used with uint32_t keys +class nsMsgKeySetU { + public: + // Creates an empty set. + static nsMsgKeySetU* Create(); + ~nsMsgKeySetU(); + // IsMember() returns whether the given key is a member of this set. + bool IsMember(nsMsgKey key); + // Add() adds the given key to the set. (Returns 1 if a change was + // made, 0 if it was already there, and negative on error.) + int Add(nsMsgKey key); + // Remove() removes the given article from the set. + int Remove(nsMsgKey key); + // Add the keys in the set to aArray. + nsresult ToMsgKeyArray(nsTArray<nsMsgKey>& aArray); + + protected: + nsMsgKeySetU(); + RefPtr<nsMsgKeySet> loKeySet; + RefPtr<nsMsgKeySet> hiKeySet; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgDBView.cpp b/comm/mailnews/base/src/nsMsgDBView.cpp new file mode 100644 index 0000000000..6532b52e0e --- /dev/null +++ b/comm/mailnews/base/src/nsMsgDBView.cpp @@ -0,0 +1,7411 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <algorithm> +#include "msgCore.h" +#include "prmem.h" +#include "nsArrayUtils.h" +#include "nsIMsgCustomColumnHandler.h" +#include "nsMsgDBView.h" +#include "nsISupports.h" +#include "nsIMsgFolder.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgFolder.h" +#include "MailNewsTypes2.h" +#include "nsIMsgImapMailFolder.h" +#include "nsImapCore.h" +#include "nsMsgFolderFlags.h" +#include "nsIMsgLocalMailFolder.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIPrefLocalizedString.h" +#include "nsIMsgSearchSession.h" +#include "nsIMsgCopyService.h" +#include "nsISpamSettings.h" +#include "nsIMsgAccountManager.h" +#include "nsTreeColumns.h" +#include "nsTextFormatter.h" +#include "nsIMimeConverter.h" +#include "nsMsgMessageFlags.h" +#include "nsIPrompt.h" +#include "nsIWindowWatcher.h" +#include "nsIMsgFolderNotificationService.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsMemory.h" +#include "nsIAbManager.h" +#include "nsIAbDirectory.h" +#include "nsIAbCard.h" +#include "mozilla/Components.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/DataTransfer.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "nsTArray.h" +#include "mozilla/intl/OSPreferences.h" +#include "mozilla/intl/LocaleService.h" +#include "mozilla/intl/AppDateTimeFormat.h" + +using namespace mozilla::mailnews; + +nsString nsMsgDBView::kHighestPriorityString; +nsString nsMsgDBView::kHighPriorityString; +nsString nsMsgDBView::kLowestPriorityString; +nsString nsMsgDBView::kLowPriorityString; +nsString nsMsgDBView::kNormalPriorityString; + +nsString nsMsgDBView::kReadString; +nsString nsMsgDBView::kRepliedString; +nsString nsMsgDBView::kForwardedString; +nsString nsMsgDBView::kRedirectedString; +nsString nsMsgDBView::kNewString; + +nsString nsMsgDBView::kTodayString; +nsString nsMsgDBView::kYesterdayString; +nsString nsMsgDBView::kLastWeekString; +nsString nsMsgDBView::kTwoWeeksAgoString; +nsString nsMsgDBView::kOldMailString; +nsString nsMsgDBView::kFutureDateString; + +bool nsMsgDBView::m_dateFormatsInitialized = false; +nsDateFormatSelectorComm nsMsgDBView::m_dateFormatDefault = kDateFormatShort; +nsDateFormatSelectorComm nsMsgDBView::m_dateFormatThisWeek = kDateFormatShort; +nsDateFormatSelectorComm nsMsgDBView::m_dateFormatToday = kDateFormatNone; + +nsString nsMsgDBView::m_connectorPattern; +nsCOMPtr<nsIStringBundle> nsMsgDBView::mMessengerStringBundle; + +static const uint32_t kMaxNumSortColumns = 2; + +static void GetCachedName(const nsCString& unparsedString, + int32_t displayVersion, nsACString& cachedName); + +static void UpdateCachedName(nsIMsgDBHdr* aHdr, const char* header_field, + const nsAString& newName); + +// viewSortInfo is context data passed into the sort comparison functions - +// FnSortIdUint32 for comparing numeric fields, FnSortIdKey for everything +// else. If a comparison function finds two elements with equal primary +// ordering, it'll call SecondaryCompare() to break the deadlock. +// SecondaryCompare() uses the same comparison functions again, but using the +// secondary key and potentially with different criteria (eg secondary sort +// order might be different to primary). The viewSortInfo::isSecondarySort +// flag lets the comparison function know not to call SecondaryCompare() +// again (and again and again)... +class viewSortInfo { + public: + nsMsgDBView* view; + nsIMsgDatabase* db; // Which db to use for collation compares. + bool isSecondarySort; + bool ascendingSort; +}; + +NS_IMPL_ISUPPORTS(nsMsgDBViewService, nsIMsgDBViewService) +NS_IMETHODIMP nsMsgDBViewService::InitializeDBViewStrings() { + nsMsgDBView::InitializeLiterals(); + nsMsgDBView::m_connectorPattern.Truncate(); + nsMsgDBView::mMessengerStringBundle = nullptr; + // Initialize date display format. + if (!nsMsgDBView::m_dateFormatsInitialized) { + nsMsgDBView::InitDisplayFormats(); + } + return NS_OK; +} + +NS_IMPL_ADDREF(nsMsgDBView) +NS_IMPL_RELEASE(nsMsgDBView) + +NS_INTERFACE_MAP_BEGIN(nsMsgDBView) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMsgDBView) + NS_INTERFACE_MAP_ENTRY(nsIMsgDBView) + NS_INTERFACE_MAP_ENTRY(nsIDBChangeListener) + NS_INTERFACE_MAP_ENTRY(nsITreeView) + NS_INTERFACE_MAP_ENTRY(nsIJunkMailClassificationListener) +NS_INTERFACE_MAP_END + +nsMsgDBView::nsMsgDBView() { + // Member initializers and constructor code. + m_sortValid = false; + m_checkedCustomColumns = false; + m_sortOrder = nsMsgViewSortOrder::none; + m_sortType = nsMsgViewSortType::byNone; + m_viewFlags = nsMsgViewFlagsType::kNone; + m_secondarySort = nsMsgViewSortType::byId; + m_secondarySortOrder = nsMsgViewSortOrder::ascending; + m_cachedMsgKey = nsMsgKey_None; + m_currentlyDisplayedMsgKey = nsMsgKey_None; + m_currentlyDisplayedViewIndex = nsMsgViewIndex_None; + mNumSelectedRows = 0; + mSuppressMsgDisplay = false; + mSuppressCommandUpdating = false; + mSuppressChangeNotification = false; + mSummarizeFailed = false; + mSelectionSummarized = false; + + mIsNews = false; + mIsRss = false; + mIsXFVirtual = false; + mDeleteModel = nsMsgImapDeleteModels::MoveToTrash; + m_deletingRows = false; + mNumMessagesRemainingInBatch = 0; + mShowSizeInLines = false; + mSortThreadsByRoot = false; + + // mCommandsNeedDisablingBecauseOfSelection - A boolean that tell us if we + // needed to disable commands because of what's selected. If we're offline + // w/o a downloaded msg selected, or a dummy message was selected. + mCommandsNeedDisablingBecauseOfSelection = false; + mRemovingRow = false; + m_saveRestoreSelectionDepth = 0; + mRecentlyDeletedArrayIndex = 0; +} + +void nsMsgDBView::InitializeLiterals() { + // Priority strings. + GetString(u"priorityHighest", kHighestPriorityString); + GetString(u"priorityHigh", kHighPriorityString); + GetString(u"priorityLowest", kLowestPriorityString); + GetString(u"priorityLow", kLowPriorityString); + GetString(u"priorityNormal", kNormalPriorityString); + + GetString(u"read", kReadString); + GetString(u"replied", kRepliedString); + GetString(u"forwarded", kForwardedString); + GetString(u"redirected", kRedirectedString); + GetString(u"new", kNewString); + + GetString(u"today", kTodayString); + GetString(u"yesterday", kYesterdayString); + GetString(u"last7Days", kLastWeekString); + GetString(u"last14Days", kTwoWeeksAgoString); + GetString(u"older", kOldMailString); + GetString(u"futureDate", kFutureDateString); +} + +nsMsgDBView::~nsMsgDBView() { + if (m_db) m_db->RemoveListener(this); +} + +// Helper function used to fetch strings from the messenger string bundle +void nsMsgDBView::GetString(const char16_t* aStringName, nsAString& aValue) { + nsresult res = NS_ERROR_UNEXPECTED; + + if (!nsMsgDBView::mMessengerStringBundle) { + static const char propertyURL[] = MESSENGER_STRING_URL; + nsCOMPtr<nsIStringBundleService> sBundleService = + mozilla::components::StringBundle::Service(); + + if (sBundleService) + res = sBundleService->CreateBundle( + propertyURL, getter_AddRefs(nsMsgDBView::mMessengerStringBundle)); + } + + if (nsMsgDBView::mMessengerStringBundle) + res = mMessengerStringBundle->GetStringFromName( + NS_ConvertUTF16toUTF8(aStringName).get(), aValue); + + if (NS_FAILED(res)) { + aValue = aStringName; + } +} + +// Helper function used to fetch localized strings from the prefs +nsresult nsMsgDBView::GetPrefLocalizedString(const char* aPrefName, + nsString& aResult) { + nsresult rv = NS_OK; + nsCOMPtr<nsIPrefBranch> prefBranch; + nsCOMPtr<nsIPrefLocalizedString> pls; + nsString ucsval; + + prefBranch = do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = prefBranch->GetComplexValue( + aPrefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(pls)); + NS_ENSURE_SUCCESS(rv, rv); + pls->ToString(getter_Copies(ucsval)); + aResult = ucsval.get(); + return rv; +} + +nsresult nsMsgDBView::AppendKeywordProperties(const nsACString& keywords, + nsAString& properties, + bool* tagAdded) { + *tagAdded = false; + // Get the top most keyword's CSS selector and append that as a property. + nsresult rv; + if (!mTagService) { + mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCString topKey; + rv = mTagService->GetTopKey(keywords, topKey); + NS_ENSURE_SUCCESS(rv, rv); + if (topKey.IsEmpty()) return NS_OK; + + nsString selector; + rv = mTagService->GetSelectorForKey(topKey, selector); + if (NS_SUCCEEDED(rv)) { + *tagAdded = true; + properties.Append(' '); + properties.Append(selector); + } + return rv; +} + +/////////////////////////////////////////////////////////////////////////// +// nsITreeView Implementation Methods (and helper methods) +/////////////////////////////////////////////////////////////////////////// + +static nsresult GetDisplayNameInAddressBook(const nsACString& emailAddress, + nsAString& displayName) { + nsresult rv; + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIAbCard> cardForAddress; + rv = abManager->CardForEmailAddress(emailAddress, + getter_AddRefs(cardForAddress)); + NS_ENSURE_SUCCESS(rv, rv); + + if (cardForAddress) { + bool preferDisplayName = true; + rv = cardForAddress->GetPropertyAsBool("PreferDisplayName", true, + &preferDisplayName); + + if (NS_FAILED(rv) || preferDisplayName) + rv = cardForAddress->GetDisplayName(displayName); + } + + return rv; +} + +/* + * The unparsedString has following format: + * "version|displayname" + */ +static void GetCachedName(const nsCString& unparsedString, + int32_t displayVersion, nsACString& cachedName) { + nsresult err; + + // Get version #. + int32_t cachedVersion = unparsedString.ToInteger(&err); + if (cachedVersion != displayVersion) return; + + // Get cached name. + int32_t pos = unparsedString.FindChar('|'); + if (pos != kNotFound) cachedName = Substring(unparsedString, pos + 1); +} + +static void UpdateCachedName(nsIMsgDBHdr* aHdr, const char* header_field, + const nsAString& newName) { + nsCString newCachedName; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + int32_t currentDisplayNameVersion = 0; + + prefs->GetIntPref("mail.displayname.version", ¤tDisplayNameVersion); + + // Save version number. + newCachedName.AppendInt(currentDisplayNameVersion); + newCachedName.Append('|'); + + // Save name. + newCachedName.Append(NS_ConvertUTF16toUTF8(newName)); + + aHdr->SetStringProperty(header_field, newCachedName); +} + +nsresult nsMsgDBView::FetchAuthor(nsIMsgDBHdr* aHdr, nsAString& aSenderString) { + nsCString unparsedAuthor; + bool showCondensedAddresses = false; + int32_t currentDisplayNameVersion = 0; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + + prefs->GetIntPref("mail.displayname.version", ¤tDisplayNameVersion); + prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses); + + aHdr->GetStringProperty("sender_name", unparsedAuthor); + + // If the author is already computed, use it. + if (!unparsedAuthor.IsEmpty()) { + nsCString cachedDisplayName; + GetCachedName(unparsedAuthor, currentDisplayNameVersion, cachedDisplayName); + if (!cachedDisplayName.IsEmpty()) { + CopyUTF8toUTF16(cachedDisplayName, aSenderString); + return NS_OK; + } + } + + nsCString author; + (void)aHdr->GetAuthor(getter_Copies(author)); + + nsCString headerCharset; + aHdr->GetEffectiveCharset(headerCharset); + + nsString name; + nsCString emailAddress; + nsCOMArray<msgIAddressObject> addresses = + EncodedHeader(author, headerCharset.get()); + bool multipleAuthors = addresses.Length() > 1; + + ExtractFirstAddress(addresses, name, emailAddress); + + if (showCondensedAddresses) + GetDisplayNameInAddressBook(emailAddress, aSenderString); + + if (aSenderString.IsEmpty()) { + // We can't use the display name in the card; use the name contained in + // the header or email address. + if (name.IsEmpty()) { + CopyUTF8toUTF16(emailAddress, aSenderString); + } else { + int32_t atPos; + if ((atPos = name.FindChar('@')) == kNotFound || + name.FindChar('.', atPos) == kNotFound) { + aSenderString = name; + } else { + // Found @ followed by a dot, so this looks like a spoofing case. + aSenderString = name; + aSenderString.AppendLiteral(" <"); + AppendUTF8toUTF16(emailAddress, aSenderString); + aSenderString.Append('>'); + } + } + } + + if (multipleAuthors) { + aSenderString.AppendLiteral(" "); + nsAutoString val; + GetString(u"andOthers", val); + aSenderString.Append(val); + } + + UpdateCachedName(aHdr, "sender_name", aSenderString); + + return NS_OK; +} + +nsresult nsMsgDBView::FetchAccount(nsIMsgDBHdr* aHdr, nsAString& aAccount) { + nsCString accountKey; + nsresult rv = aHdr->GetAccountKey(getter_Copies(accountKey)); + + // Cache the account manager? + nsCOMPtr<nsIMsgAccountManager> accountManager( + do_GetService("@mozilla.org/messenger/account-manager;1", &rv)); + + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgAccount> account; + nsCOMPtr<nsIMsgIncomingServer> server; + if (!accountKey.IsEmpty()) + rv = accountManager->GetAccount(accountKey, getter_AddRefs(account)); + + if (account) { + account->GetIncomingServer(getter_AddRefs(server)); + } else { + nsCOMPtr<nsIMsgFolder> folder; + aHdr->GetFolder(getter_AddRefs(folder)); + if (folder) folder->GetServer(getter_AddRefs(server)); + } + + if (server) + server->GetPrettyName(aAccount); + else + CopyASCIItoUTF16(accountKey, aAccount); + + return NS_OK; +} + +nsresult nsMsgDBView::FetchRecipients(nsIMsgDBHdr* aHdr, + nsAString& aRecipientsString) { + nsCString recipients; + int32_t currentDisplayNameVersion = 0; + bool showCondensedAddresses = false; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + + prefs->GetIntPref("mail.displayname.version", ¤tDisplayNameVersion); + prefs->GetBoolPref("mail.showCondensedAddresses", &showCondensedAddresses); + + aHdr->GetStringProperty("recipient_names", recipients); + + if (!recipients.IsEmpty()) { + nsCString cachedRecipients; + GetCachedName(recipients, currentDisplayNameVersion, cachedRecipients); + + // Recipients have already been cached, check if the addressbook + // was changed after cache. + if (!cachedRecipients.IsEmpty()) { + CopyUTF8toUTF16(cachedRecipients, aRecipientsString); + return NS_OK; + } + } + + nsCString unparsedRecipients; + nsresult rv = aHdr->GetRecipients(getter_Copies(unparsedRecipients)); + + nsCString headerCharset; + aHdr->GetEffectiveCharset(headerCharset); + + nsTArray<nsString> names; + nsTArray<nsCString> emails; + ExtractAllAddresses(EncodedHeader(unparsedRecipients, headerCharset.get()), + names, UTF16ArrayAdapter<>(emails)); + + uint32_t numAddresses = names.Length(); + + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + + NS_ENSURE_SUCCESS(rv, NS_OK); + + // Go through each email address in the recipients and compute its + // display name. + for (uint32_t i = 0; i < numAddresses; i++) { + nsString recipient; + nsCString& curAddress = emails[i]; + nsString& curName = names[i]; + + if (showCondensedAddresses) + GetDisplayNameInAddressBook(curAddress, recipient); + + if (recipient.IsEmpty()) { + // We can't use the display name in the card; use the name contained in + // the header or email address. + if (curName.IsEmpty()) { + CopyUTF8toUTF16(curAddress, recipient); + } else { + int32_t atPos; + if ((atPos = curName.FindChar('@')) == kNotFound || + curName.FindChar('.', atPos) == kNotFound) { + recipient = curName; + } else { + // Found @ followed by a dot, so this looks like a spoofing case. + recipient = curName; + recipient.AppendLiteral(" <"); + AppendUTF8toUTF16(curAddress, recipient); + recipient.Append('>'); + } + } + } + + // Add ', ' between each recipient. + if (i != 0) aRecipientsString.AppendLiteral(u", "); + + aRecipientsString.Append(recipient); + } + + if (numAddresses == 0 && unparsedRecipients.FindChar(':') != kNotFound) { + // No addresses and a colon, so an empty group like + // "undisclosed-recipients: ;". + // Add group name so at least something displays. + nsString group; + CopyUTF8toUTF16(unparsedRecipients, group); + aRecipientsString.Assign(group); + } + + UpdateCachedName(aHdr, "recipient_names", aRecipientsString); + + return NS_OK; +} + +nsresult nsMsgDBView::FetchSubject(nsIMsgDBHdr* aMsgHdr, uint32_t aFlags, + nsAString& aValue) { + if (aFlags & nsMsgMessageFlags::HasRe) { + nsString subject; + aMsgHdr->GetMime2DecodedSubject(subject); + aValue.AssignLiteral("Re: "); + aValue.Append(subject); + } else { + aMsgHdr->GetMime2DecodedSubject(aValue); + } + + return NS_OK; +} + +// In case we want to play around with the date string, I've broken it out into +// a separate routine. Set rcvDate to true to get the Received: date instead +// of the Date: date. +nsresult nsMsgDBView::FetchDate(nsIMsgDBHdr* aHdr, nsAString& aDateString, + bool rcvDate) { + PRTime dateOfMsg; + PRTime dateOfMsgLocal; + uint32_t rcvDateSecs; + nsresult rv; + + // Silently return Date: instead if Received: is unavailable. + if (rcvDate) { + rv = aHdr->GetUint32Property("dateReceived", &rcvDateSecs); + if (rcvDateSecs != 0) Seconds2PRTime(rcvDateSecs, &dateOfMsg); + } + + if (!rcvDate || rcvDateSecs == 0) rv = aHdr->GetDate(&dateOfMsg); + NS_ENSURE_SUCCESS(rv, rv); + + PRTime currentTime = PR_Now(); + PRExplodedTime explodedCurrentTime; + PR_ExplodeTime(currentTime, PR_LocalTimeParameters, &explodedCurrentTime); + PRExplodedTime explodedMsgTime; + PR_ExplodeTime(dateOfMsg, PR_LocalTimeParameters, &explodedMsgTime); + + // If the message is from today, don't show the date, only the time (3:15 pm). + // If the message is from the last week, show the day of the week + // (Mon 3:15 pm). In all other cases, show the full date (03/19/01 3:15 pm). + + nsDateFormatSelectorComm dateFormat = m_dateFormatDefault; + if (explodedCurrentTime.tm_year == explodedMsgTime.tm_year && + explodedCurrentTime.tm_month == explodedMsgTime.tm_month && + explodedCurrentTime.tm_mday == explodedMsgTime.tm_mday) { + // Same day. + dateFormat = m_dateFormatToday; + } else if (currentTime > dateOfMsg) { + // The following chunk of code allows us to show a day instead of a number + // if the message was received within the last 7 days. i.e. Mon 5:10pm + // (depending on the mail.ui.display.dateformat.thisweek pref). + // The concrete format used is dependent on a preference setting + // (see InitDisplayFormats). + // Convert the times from GMT to local time + int64_t GMTLocalTimeShift = + PR_USEC_PER_SEC * int64_t(explodedCurrentTime.tm_params.tp_gmt_offset + + explodedCurrentTime.tm_params.tp_dst_offset); + currentTime += GMTLocalTimeShift; + dateOfMsgLocal = dateOfMsg + GMTLocalTimeShift; + + // Find the most recent midnight. + int64_t todaysMicroSeconds = currentTime % PR_USEC_PER_DAY; + int64_t mostRecentMidnight = currentTime - todaysMicroSeconds; + + // Most recent midnight minus 6 days. + int64_t mostRecentWeek = mostRecentMidnight - (PR_USEC_PER_DAY * 6); + + // Was the message sent during the last week? + if (dateOfMsgLocal >= mostRecentWeek) dateFormat = m_dateFormatThisWeek; + } + + mozilla::intl::DateTimeFormat::StyleBag style; + style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + switch (dateFormat) { + case kDateFormatNone: + rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg, + aDateString); + NS_ENSURE_SUCCESS(rv, rv); + break; + case kDateFormatLong: + style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long); + rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg, + aDateString); + NS_ENSURE_SUCCESS(rv, rv); + break; + case kDateFormatShort: + style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg, + aDateString); + NS_ENSURE_SUCCESS(rv, rv); + break; + case kDateFormatWeekday: { + // We want weekday + time. + nsAutoString timeString; + nsAutoString weekdayString; + rv = mozilla::intl::AppDateTimeFormat::Format(style, dateOfMsg, + timeString); + NS_ENSURE_SUCCESS(rv, rv); + + mozilla::intl::DateTimeFormat::ComponentsBag components{}; + components.weekday = + mozilla::Some(mozilla::intl::DateTimeFormat::Text::Short); + rv = mozilla::intl::AppDateTimeFormat::Format( + components, &explodedMsgTime, weekdayString); + NS_ENSURE_SUCCESS(rv, rv); + + if (nsMsgDBView::m_connectorPattern.IsEmpty()) { + nsAutoCString locale; + AutoTArray<nsCString, 10> regionalPrefsLocales; + mozilla::intl::LocaleService::GetInstance()->GetRegionalPrefsLocales( + regionalPrefsLocales); + locale.Assign(regionalPrefsLocales[0]); + nsAutoCString str; + mozilla::intl::OSPreferences::GetInstance() + ->GetDateTimeConnectorPattern(locale, str); + nsMsgDBView::m_connectorPattern = NS_ConvertUTF8toUTF16(str); + } + + nsAutoString pattern(nsMsgDBView::m_connectorPattern); + int32_t ind = pattern.Find(u"{1}"_ns); + if (ind != kNotFound) { + pattern.Replace(ind, 3, weekdayString); + } + ind = pattern.Find(u"{0}"_ns); + if (ind != kNotFound) { + pattern.Replace(ind, 3, timeString); + } + aDateString = pattern; + break; + } + + default: + break; + } + + return rv; +} + +nsresult nsMsgDBView::FetchStatus(uint32_t aFlags, nsAString& aStatusString) { + if (aFlags & nsMsgMessageFlags::Replied) + aStatusString = kRepliedString; + else if (aFlags & nsMsgMessageFlags::Forwarded) + aStatusString = kForwardedString; + else if (aFlags & nsMsgMessageFlags::Redirected) + aStatusString = kRedirectedString; + else if (aFlags & nsMsgMessageFlags::New) + aStatusString = kNewString; + else if (aFlags & nsMsgMessageFlags::Read) + aStatusString = kReadString; + + return NS_OK; +} + +nsresult nsMsgDBView::FetchSize(nsIMsgDBHdr* aHdr, nsAString& aSizeString) { + nsresult rv; + nsAutoString formattedSizeString; + uint32_t msgSize = 0; + + // For news, show the line count, not the size if the user wants so. + if (mShowSizeInLines) { + aHdr->GetLineCount(&msgSize); + formattedSizeString.AppendInt(msgSize); + } else { + uint32_t flags = 0; + + aHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Partial) + aHdr->GetUint32Property("onlineSize", &msgSize); + + if (msgSize == 0) aHdr->GetMessageSize(&msgSize); + + rv = FormatFileSize(msgSize, true, formattedSizeString); + NS_ENSURE_SUCCESS(rv, rv); + } + + aSizeString = formattedSizeString; + // The formattingString Length includes the null terminator byte! + if (!formattedSizeString.Last()) + aSizeString.SetLength(formattedSizeString.Length() - 1); + + return NS_OK; +} + +nsresult nsMsgDBView::FetchPriority(nsIMsgDBHdr* aHdr, + nsAString& aPriorityString) { + nsMsgPriorityValue priority = nsMsgPriority::notSet; + aHdr->GetPriority(&priority); + + switch (priority) { + case nsMsgPriority::highest: + aPriorityString = kHighestPriorityString; + break; + case nsMsgPriority::high: + aPriorityString = kHighPriorityString; + break; + case nsMsgPriority::low: + aPriorityString = kLowPriorityString; + break; + case nsMsgPriority::lowest: + aPriorityString = kLowestPriorityString; + break; + case nsMsgPriority::normal: + aPriorityString = kNormalPriorityString; + break; + default: + break; + } + + return NS_OK; +} + +nsresult nsMsgDBView::FetchKeywords(nsIMsgDBHdr* aHdr, + nsACString& keywordString) { + NS_ENSURE_ARG_POINTER(aHdr); + nsresult rv = NS_OK; + if (!mTagService) { + mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + nsCString keywords; + aHdr->GetStringProperty("keywords", keywords); + keywordString = keywords; + return NS_OK; +} + +// If the row is a collapsed thread, we optionally roll-up the keywords in all +// the messages in the thread, otherwise, return just the keywords for the row. +nsresult nsMsgDBView::FetchRowKeywords(nsMsgViewIndex aRow, nsIMsgDBHdr* aHdr, + nsACString& keywordString) { + nsresult rv = FetchKeywords(aHdr, keywordString); + NS_ENSURE_SUCCESS(rv, rv); + + bool cascadeKeywordsUp = true; + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + prefs->GetBoolPref("mailnews.display_reply_tag_colors_for_collapsed_threads", + &cascadeKeywordsUp); + + if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + cascadeKeywordsUp) { + if ((m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) && + (m_flags[aRow] & nsMsgMessageFlags::Elided)) { + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCString moreKeywords; + for (uint32_t index = 0; index < numChildren; index++) { + thread->GetChildHdrAt(index, getter_AddRefs(msgHdr)); + rv = FetchKeywords(msgHdr, moreKeywords); + NS_ENSURE_SUCCESS(rv, rv); + + if (!keywordString.IsEmpty() && !moreKeywords.IsEmpty()) + keywordString.Append(' '); + + keywordString.Append(moreKeywords); + } + } + } + } + + return rv; +} + +nsresult nsMsgDBView::FetchTags(nsIMsgDBHdr* aHdr, nsAString& aTagString) { + NS_ENSURE_ARG_POINTER(aHdr); + nsresult rv = NS_OK; + if (!mTagService) { + mTagService = do_GetService("@mozilla.org/messenger/tagservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsString tags; + nsCString keywords; + aHdr->GetStringProperty("keywords", keywords); + + nsTArray<nsCString> keywordsArray; + ParseString(keywords, ' ', keywordsArray); + nsAutoString tag; + + for (uint32_t i = 0; i < keywordsArray.Length(); i++) { + rv = mTagService->GetTagForKey(keywordsArray[i], tag); + if (NS_SUCCEEDED(rv) && !tag.IsEmpty()) { + if (!tags.IsEmpty()) tags.Append((char16_t)' '); + + tags.Append(tag); + } + } + + aTagString = tags; + return NS_OK; +} + +/** + * Lowercase the email and remove a possible plus addressing part. + * E.g. John+test@example.com -> john@example.com. + */ +static void ToLowerCaseDropPlusAddessing(nsCString& aEmail) { + ToLowerCase(aEmail); + int32_t indPlus; + if ((indPlus = aEmail.FindChar('+')) == kNotFound) return; + int32_t indAt; + indAt = aEmail.FindChar('@', indPlus); + if (indAt == kNotFound) return; + aEmail.ReplaceLiteral(indPlus, indAt - indPlus, ""); +} + +bool nsMsgDBView::IsOutgoingMsg(nsIMsgDBHdr* aHdr) { + nsString author; + aHdr->GetMime2DecodedAuthor(author); + + nsCString emailAddress; + nsString name; + ExtractFirstAddress(DecodedHeader(author), name, emailAddress); + ToLowerCaseDropPlusAddessing(emailAddress); + return mEmails.Contains(emailAddress); +} + +// If you call SaveAndClearSelection make sure to call RestoreSelection(), +// otherwise m_saveRestoreSelectionDepth will be incorrect and will lead to +// selection msg problems. +nsresult nsMsgDBView::SaveAndClearSelection(nsMsgKey* aCurrentMsgKey, + nsTArray<nsMsgKey>& aMsgKeyArray) { + // Always return a value in the first parameter. + if (aCurrentMsgKey) *aCurrentMsgKey = nsMsgKey_None; + + // We don't do anything on nested Save / Restore calls. + m_saveRestoreSelectionDepth++; + if (m_saveRestoreSelectionDepth != 1) return NS_OK; + + if (!mTreeSelection) return NS_OK; + + // First, freeze selection. + mTreeSelection->SetSelectEventsSuppressed(true); + + // Second, save the current index. + if (aCurrentMsgKey) { + int32_t currentIndex; + if (NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(¤tIndex)) && + currentIndex >= 0 && uint32_t(currentIndex) < GetSize()) + *aCurrentMsgKey = m_keys[currentIndex]; + else + *aCurrentMsgKey = nsMsgKey_None; + } + + // Third, get an array of view indices for the selection. + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + int32_t numIndices = selection.Length(); + aMsgKeyArray.SetLength(numIndices); + + // Now store the msg key for each selected item. + nsMsgKey msgKey; + for (int32_t index = 0; index < numIndices; index++) { + msgKey = m_keys[selection[index]]; + aMsgKeyArray[index] = msgKey; + } + + // Clear the selection, we'll manually restore it later. + if (mTreeSelection) mTreeSelection->ClearSelection(); + + return NS_OK; +} + +nsresult nsMsgDBView::RestoreSelection(nsMsgKey aCurrentMsgKey, + nsTArray<nsMsgKey>& aMsgKeyArray) { + // We don't do anything on nested Save / Restore calls. + m_saveRestoreSelectionDepth--; + if (m_saveRestoreSelectionDepth) return NS_OK; + + // Don't assert. + if (!mTreeSelection) return NS_OK; + + // Turn our message keys into corresponding view indices. + int32_t arraySize = aMsgKeyArray.Length(); + nsMsgViewIndex currentViewPosition = nsMsgViewIndex_None; + nsMsgViewIndex newViewPosition = nsMsgViewIndex_None; + + // If we are threaded, we need to do a little more work + // we need to find (and expand) all the threads that contain messages + // that we had selected before. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + for (int32_t index = 0; index < arraySize; index++) + FindKey(aMsgKeyArray[index], true /* expand */); + } + + for (int32_t index = 0; index < arraySize; index++) { + newViewPosition = FindKey(aMsgKeyArray[index], false); + // Add the index back to the selection. + if (newViewPosition != nsMsgViewIndex_None) + mTreeSelection->ToggleSelect(newViewPosition); + } + + // Make sure the currentView was preserved. + if (aCurrentMsgKey != nsMsgKey_None) + currentViewPosition = FindKey(aCurrentMsgKey, true); + + if (mJSTree) mJSTree->SetCurrentIndex(currentViewPosition); + + // Make sure the current message is once again visible in the thread pane + // so we don't have to go search for it in the thread pane + if (currentViewPosition != nsMsgViewIndex_None) { + if (mJSTree) { + mJSTree->EnsureRowIsVisible(currentViewPosition); + } else if (mTree) { + mTree->EnsureRowIsVisible(currentViewPosition); + } + } + + // Unfreeze selection. + mTreeSelection->SetSelectEventsSuppressed(false); + return NS_OK; +} + +nsresult nsMsgDBView::GenerateURIForMsgKey(nsMsgKey aMsgKey, + nsIMsgFolder* folder, + nsACString& aURI) { + NS_ENSURE_ARG(folder); + return folder->GenerateMessageURI(aMsgKey, aURI); +} + +nsresult nsMsgDBView::GetMessageEnumerator(nsIMsgEnumerator** enumerator) { + return m_db->EnumerateMessages(enumerator); +} + +NS_IMETHODIMP +nsMsgDBView::IsEditable(int32_t row, nsTreeColumn* col, bool* _retval) { + NS_ENSURE_ARG_POINTER(col); + NS_ENSURE_ARG_POINTER(_retval); + // Attempt to retrieve a custom column handler. If it exists call it and + // return. + const nsAString& colID = col->GetId(); + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) { + colHandler->IsEditable(row, col, _retval); + return NS_OK; + } + + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetCellValue(int32_t row, nsTreeColumn* col, + const nsAString& value) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetCellText(int32_t row, nsTreeColumn* col, + const nsAString& value) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetRowCount(int32_t* aRowCount) { + *aRowCount = GetSize(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSelection(nsITreeSelection** aSelection) { + NS_IF_ADDREF(*aSelection = mTreeSelection); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSelection(nsITreeSelection* aSelection) { + mTreeSelection = aSelection; + return NS_OK; +} + +nsresult nsMsgDBView::UpdateDisplayMessage(nsMsgViewIndex viewPosition) { + nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater( + do_QueryReferent(mCommandUpdater)); + if (!commandUpdater) return NS_OK; + + if (!IsValidIndex(viewPosition)) return NS_MSG_INVALID_DBVIEW_INDEX; + + // Get the subject and the folder for the message and inform the front + // end that we changed the message we are currently displaying. + nsresult rv; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(viewPosition, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString subject; + if (viewPosition >= (nsMsgViewIndex)m_flags.Length()) + return NS_MSG_INVALID_DBVIEW_INDEX; + FetchSubject(msgHdr, m_flags[viewPosition], subject); + + nsCString keywords; + rv = msgHdr->GetStringProperty("keywords", keywords); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> folder = m_viewFolder ? m_viewFolder : m_folder; + + commandUpdater->DisplayMessageChanged(folder, subject, keywords); + + if (folder) { + if (viewPosition >= (nsMsgViewIndex)m_keys.Length()) + return NS_MSG_INVALID_DBVIEW_INDEX; + rv = folder->SetLastMessageLoaded(m_keys[viewPosition]); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SelectionChangedXPCOM() { + // If the currentSelection changed then we have a message to display - + // not if we are in the middle of deleting rows. + if (m_deletingRows) return NS_OK; + + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + + bool commandsNeedDisablingBecauseOfSelection = false; + + if (!selection.IsEmpty()) { + if (WeAreOffline()) + commandsNeedDisablingBecauseOfSelection = !OfflineMsgSelected(selection); + + if (!NonDummyMsgSelected(selection)) + commandsNeedDisablingBecauseOfSelection = true; + } + + bool selectionSummarized = false; + mSummarizeFailed = false; + // Let the front-end adjust the message pane appropriately with either + // the message body, or a summary of the selection. + nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater( + do_QueryReferent(mCommandUpdater)); + if (commandUpdater) { + commandUpdater->SummarizeSelection(&selectionSummarized); + // Check if the selection was not summarized, but we expected it to be, + // and if so, remember it so GetHeadersFromSelection won't include + // the messages in collapsed threads. + if (!selectionSummarized && + (selection.Length() > 1 || + (selection.Length() == 1 && + m_flags[selection[0]] & nsMsgMessageFlags::Elided && + OperateOnMsgsInCollapsedThreads()))) { + mSummarizeFailed = true; + } + } + + bool summaryStateChanged = selectionSummarized != mSelectionSummarized; + mSelectionSummarized = selectionSummarized; + + if (!mTreeSelection || selection.Length() != 1 || selectionSummarized) { + // If we have zero or multiple items selected, we shouldn't be displaying + // any message. + m_currentlyDisplayedMsgKey = nsMsgKey_None; + m_currentlyDisplayedMsgUri.Truncate(); + m_currentlyDisplayedViewIndex = nsMsgViewIndex_None; + } + + // Determine if we need to push command update notifications out to the UI. + // We need to push a command update notification iff, one of the following + // conditions are met + // (1) the selection went from 0 to 1 + // (2) it went from 1 to 0 + // (3) it went from 1 to many + // (4) it went from many to 1 or 0 + // (5) a different msg was selected - perhaps it was offline or not, + // matters only when we are offline + // (6) we did a forward/back, or went from having no history to having + // history - not sure how to tell this. + // (7) whether the selection was summarized or not changed. + + // I think we're going to need to keep track of whether forward/back were + // enabled/should be enabled, and when this changes, force a command update. + + if (!summaryStateChanged && + (selection.Length() == mNumSelectedRows || + (selection.Length() > 1 && mNumSelectedRows > 1)) && + commandsNeedDisablingBecauseOfSelection == + mCommandsNeedDisablingBecauseOfSelection) { + // Don't update commands if we're suppressing them, or if we're removing + // rows, unless it was the last row. + } else if (!mSuppressCommandUpdating && commandUpdater && + (!mRemovingRow || GetSize() == 0)) { + commandUpdater->UpdateCommandStatus(); + } + + mCommandsNeedDisablingBecauseOfSelection = + commandsNeedDisablingBecauseOfSelection; + mNumSelectedRows = selection.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetRowProperties(int32_t index, nsAString& properties) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + // This is where we tell the tree to apply styles to a particular row. + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = NS_OK; + + rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + if (IsOutgoingMsg(msgHdr)) properties.AppendLiteral(" outgoing"); + + nsCString keywordProperty; + FetchRowKeywords(index, msgHdr, keywordProperty); + bool tagAdded = false; + if (!keywordProperty.IsEmpty()) { + AppendKeywordProperties(keywordProperty, properties, &tagAdded); + } + if (tagAdded) { + properties.AppendLiteral(" tagged"); + } else { + properties.AppendLiteral(" untagged"); + } + + uint32_t flags; + msgHdr->GetFlags(&flags); + + if (!(flags & nsMsgMessageFlags::Read)) + properties.AppendLiteral(" unread"); + else + properties.AppendLiteral(" read"); + + if (flags & nsMsgMessageFlags::Replied) properties.AppendLiteral(" replied"); + + if (flags & nsMsgMessageFlags::Forwarded) + properties.AppendLiteral(" forwarded"); + + if (flags & nsMsgMessageFlags::Redirected) + properties.AppendLiteral(" redirected"); + + if (flags & nsMsgMessageFlags::New) properties.AppendLiteral(" new"); + + if (m_flags[index] & nsMsgMessageFlags::Marked) + properties.AppendLiteral(" flagged"); + + // Give the custom column handlers a chance to style the row. + for (int i = 0; i < m_customColumnHandlers.Count(); i++) { + nsString extra; + m_customColumnHandlers[i]->GetRowProperties(index, extra); + if (!extra.IsEmpty()) { + properties.Append(' '); + properties.Append(extra); + } + } + + // For threaded display add the ignoreSubthread property to the + // subthread top row (this row). For non-threaded add it to all rows. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + (flags & nsMsgMessageFlags::Ignored)) { + properties.AppendLiteral(" ignoreSubthread"); + } else { + bool ignored; + msgHdr->GetIsKilled(&ignored); + if (ignored) properties.AppendLiteral(" ignoreSubthread"); + } + + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder); + + if ((flags & nsMsgMessageFlags::Offline) || + (localFolder && !(flags & nsMsgMessageFlags::Partial))) + properties.AppendLiteral(" offline"); + + if (flags & nsMsgMessageFlags::Attachment) + properties.AppendLiteral(" attach"); + + if ((mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) && + (flags & nsMsgMessageFlags::IMAPDeleted)) + properties.AppendLiteral(" imapdeleted"); + + nsCString imageSize; + msgHdr->GetStringProperty("imageSize", imageSize); + if (!imageSize.IsEmpty()) properties.AppendLiteral(" hasimage"); + + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", junkScoreStr); + if (!junkScoreStr.IsEmpty()) { + if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE) + properties.AppendLiteral(" junk"); + else + properties.AppendLiteral(" notjunk"); + + NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed."); + } + + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingIndex(index, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + uint32_t numUnreadChildren; + thread->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) properties.AppendLiteral(" hasUnread"); + + // For threaded display add the ignore/watch properties to the + // thread top row. For non-threaded add it to all rows. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) || + ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + (m_flags[index] & MSG_VIEW_FLAG_ISTHREAD))) { + thread->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Watched) + properties.AppendLiteral(" watch"); + if (flags & nsMsgMessageFlags::Ignored) + properties.AppendLiteral(" ignore"); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetColumnProperties(nsTreeColumn* col, nsAString& properties) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetCellProperties(int32_t aRow, nsTreeColumn* col, + nsAString& properties) { + if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX; + + // This is where we tell the tree to apply styles to a particular row + // i.e. if the row is an unread message... + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = NS_OK; + + rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + const nsAString& colID = col->GetId(); + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + if (colHandler != nullptr) { + colHandler->GetCellProperties(aRow, col, properties); + } else if (colID[0] == 'c') { + // Correspondent. + if (IsOutgoingMsg(msgHdr)) + properties.AssignLiteral("outgoing"); + else + properties.AssignLiteral("incoming"); + } + + if (!properties.IsEmpty()) properties.Append(' '); + + properties.Append(mMessageType); + + uint32_t flags; + msgHdr->GetFlags(&flags); + + if (!(flags & nsMsgMessageFlags::Read)) + properties.AppendLiteral(" unread"); + else + properties.AppendLiteral(" read"); + + if (flags & nsMsgMessageFlags::Replied) properties.AppendLiteral(" replied"); + + if (flags & nsMsgMessageFlags::Forwarded) + properties.AppendLiteral(" forwarded"); + + if (flags & nsMsgMessageFlags::Redirected) + properties.AppendLiteral(" redirected"); + + if (flags & nsMsgMessageFlags::New) properties.AppendLiteral(" new"); + + if (m_flags[aRow] & nsMsgMessageFlags::Marked) + properties.AppendLiteral(" flagged"); + + // For threaded display add the ignoreSubthread property to the + // subthread top row (this row). For non-threaded add it to all rows. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + (flags & nsMsgMessageFlags::Ignored)) { + properties.AppendLiteral(" ignoreSubthread"); + } else { + bool ignored; + msgHdr->GetIsKilled(&ignored); + if (ignored) properties.AppendLiteral(" ignoreSubthread"); + } + + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder); + + if ((flags & nsMsgMessageFlags::Offline) || + (localFolder && !(flags & nsMsgMessageFlags::Partial))) + properties.AppendLiteral(" offline"); + + if (flags & nsMsgMessageFlags::Attachment) + properties.AppendLiteral(" attach"); + + if ((mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) && + (flags & nsMsgMessageFlags::IMAPDeleted)) + properties.AppendLiteral(" imapdeleted"); + + nsCString imageSize; + msgHdr->GetStringProperty("imageSize", imageSize); + if (!imageSize.IsEmpty()) properties.AppendLiteral(" hasimage"); + + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", junkScoreStr); + if (!junkScoreStr.IsEmpty()) { + if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE) + properties.AppendLiteral(" junk"); + else + properties.AppendLiteral(" notjunk"); + + NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed."); + } + + nsCString keywords; + FetchRowKeywords(aRow, msgHdr, keywords); + bool tagAdded = false; + if (!keywords.IsEmpty()) { + AppendKeywordProperties(keywords, properties, &tagAdded); + } + if (tagAdded) { + properties.AppendLiteral(" tagged"); + } else { + properties.AppendLiteral(" untagged"); + } + + // This is a double fetch of the keywords property since we also fetch + // it for the tags - do we want to do this? + // I'm not sure anyone uses the kw- property, though it could be nice + // for people wanting to extend the thread pane. + nsCString keywordProperty; + msgHdr->GetStringProperty("keywords", keywordProperty); + if (!keywordProperty.IsEmpty()) { + NS_ConvertUTF8toUTF16 keywords(keywordProperty); + int32_t spaceIndex = 0; + do { + spaceIndex = keywords.FindChar(' '); + int32_t endOfKeyword = + (spaceIndex == -1) ? keywords.Length() : spaceIndex; + properties.AppendLiteral(" kw-"); + properties.Append(StringHead(keywords, endOfKeyword)); + if (spaceIndex > 0) keywords.Cut(0, endOfKeyword + 1); + } while (spaceIndex > 0); + } + +#ifdef SUPPORT_PRIORITY_COLORS + // Add special styles for priority. + nsMsgPriorityValue priority; + msgHdr->GetPriority(&priority); + switch (priority) { + case nsMsgPriority::highest: + properties.append(" priority-highest"); + break; + case nsMsgPriority::high: + properties.append(" priority-high"); + break; + case nsMsgPriority::low: + properties.append(" priority-low"); + break; + case nsMsgPriority::lowest: + properties.append(" priority-lowest"); + break; + default: + break; + } +#endif + + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + uint32_t numUnreadChildren; + thread->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) properties.AppendLiteral(" hasUnread"); + + // For threaded display add the ignore/watch properties to the + // thread top row. For non-threaded add it to all rows. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) || + ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD))) { + thread->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Watched) + properties.AppendLiteral(" watch"); + if (flags & nsMsgMessageFlags::Ignored) + properties.AppendLiteral(" ignore"); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::IsContainer(int32_t index, bool* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + uint32_t flags = m_flags[index]; + *_retval = !!(flags & MSG_VIEW_FLAG_HASCHILDREN); + } else { + *_retval = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::IsContainerOpen(int32_t index, bool* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + uint32_t flags = m_flags[index]; + *_retval = (flags & MSG_VIEW_FLAG_HASCHILDREN) && + !(flags & nsMsgMessageFlags::Elided); + } else { + *_retval = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::IsContainerEmpty(int32_t index, bool* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + uint32_t flags = m_flags[index]; + *_retval = !(flags & MSG_VIEW_FLAG_HASCHILDREN); + } else { + *_retval = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::IsSeparator(int32_t index, bool* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + *_retval = false; + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetParentIndex(int32_t rowIndex, int32_t* _retval) { + *_retval = -1; + + int32_t rowIndexLevel; + nsresult rv = GetLevel(rowIndex, &rowIndexLevel); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t i; + for (i = rowIndex; i >= 0; i--) { + int32_t l; + GetLevel(i, &l); + if (l < rowIndexLevel) { + *_retval = i; + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::HasNextSibling(int32_t rowIndex, int32_t afterIndex, + bool* _retval) { + *_retval = false; + + int32_t rowIndexLevel; + GetLevel(rowIndex, &rowIndexLevel); + + int32_t i; + int32_t count; + GetRowCount(&count); + for (i = afterIndex + 1; i < count; i++) { + int32_t l; + GetLevel(i, &l); + if (l < rowIndexLevel) break; + + if (l == rowIndexLevel) { + *_retval = true; + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetLevel(int32_t index, int32_t* _retval) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) + *_retval = m_levels[index]; + else + *_retval = 0; + + return NS_OK; +} + +// Search view will override this since headers can span db's. +nsresult nsMsgDBView::GetMsgHdrForViewIndex(nsMsgViewIndex index, + nsIMsgDBHdr** msgHdr) { + nsresult rv = NS_OK; + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsMsgKey key = m_keys[index]; + if (key == nsMsgKey_None || !m_db) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (key == m_cachedMsgKey) { + NS_IF_ADDREF(*msgHdr = m_cachedHdr); + } else { + rv = m_db->GetMsgHdrForKey(key, msgHdr); + if (NS_SUCCEEDED(rv)) { + m_cachedHdr = *msgHdr; + m_cachedMsgKey = key; + } + } + + return rv; +} + +void nsMsgDBView::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr, + nsMsgKey msgKey, uint32_t flags, + uint32_t level) { + if ((int32_t)index < 0 || index > m_keys.Length()) { + // Something's gone wrong in a caller, but we have no clue why. + // Return without adding the header to the view. + NS_ERROR("Index for message header insertion out of array range!"); + return; + } + + m_keys.InsertElementAt(index, msgKey); + m_flags.InsertElementAt(index, flags); + m_levels.InsertElementAt(index, level); +} + +void nsMsgDBView::SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index, + nsMsgKey msgKey, uint32_t flags, uint32_t level) { + m_keys[index] = msgKey; + m_flags[index] = flags; + m_levels[index] = level; +} + +nsresult nsMsgDBView::GetFolderForViewIndex(nsMsgViewIndex index, + nsIMsgFolder** aFolder) { + NS_IF_ADDREF(*aFolder = m_folder); + return NS_OK; +} + +nsresult nsMsgDBView::GetDBForViewIndex(nsMsgViewIndex index, + nsIMsgDatabase** db) { + NS_IF_ADDREF(*db = m_db); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetImageSrc(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) { + NS_ENSURE_ARG_POINTER(aCol); + // Attempt to retrieve a custom column handler. If it exists call it and + // return. + const nsAString& colID = aCol->GetId(); + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) { + colHandler->GetImageSrc(aRow, aCol, aValue); + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetCellValue(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) { + if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + const nsAString& colID = aCol->GetId(); + + aValue.Truncate(); + if (colID.IsEmpty()) return NS_OK; + + uint32_t flags; + msgHdr->GetFlags(&flags); + + // Provide a string "value" for cells that do not normally have text. + // Use empty string for the normal states "Read", "Not Starred", + // "No Attachment" and "Not Junk". + switch (colID.First()) { + case 'a': + if (colID.EqualsLiteral("attachmentCol") && + flags & nsMsgMessageFlags::Attachment) { + GetString(u"messageHasAttachment", aValue); + } + break; + case 'f': + if (colID.EqualsLiteral("flaggedCol") && + flags & nsMsgMessageFlags::Marked) { + GetString(u"messageHasFlag", aValue); + } + break; + case 'j': + if (colID.EqualsLiteral("junkStatusCol") && JunkControlsEnabled(aRow)) { + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", junkScoreStr); + // Only need to assign a real value for junk, it's empty already + // as it should be for non-junk. + if (!junkScoreStr.IsEmpty() && + (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE)) + aValue.AssignLiteral("messageJunk"); + + NS_ASSERTION(NS_SUCCEEDED(rv), + "Converting junkScore to integer failed."); + } + break; + case 't': + if (colID.EqualsLiteral("threadCol") && + (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + // thread column + bool isContainer, isContainerEmpty, isContainerOpen; + IsContainer(aRow, &isContainer); + if (isContainer) { + IsContainerEmpty(aRow, &isContainerEmpty); + if (!isContainerEmpty) { + IsContainerOpen(aRow, &isContainerOpen); + GetString( + isContainerOpen ? u"messageExpanded" : u"messageCollapsed", + aValue); + } + } + } + break; + case 'u': + if (colID.EqualsLiteral("unreadButtonColHeader") && + !(flags & nsMsgMessageFlags::Read)) { + GetString(u"messageUnread", aValue); + } + break; + default: + aValue.Assign(colID); + break; + } + + return rv; +} + +void nsMsgDBView::RememberDeletedMsgHdr(nsIMsgDBHdr* msgHdr) { + nsCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + if (mRecentlyDeletedArrayIndex >= mRecentlyDeletedMsgIds.Length()) + mRecentlyDeletedMsgIds.AppendElement(messageId); + else + mRecentlyDeletedMsgIds[mRecentlyDeletedArrayIndex] = messageId; + + // Only remember last 20 deleted msgs. + mRecentlyDeletedArrayIndex = (mRecentlyDeletedArrayIndex + 1) % 20; +} + +bool nsMsgDBView::WasHdrRecentlyDeleted(nsIMsgDBHdr* msgHdr) { + nsCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + return mRecentlyDeletedMsgIds.Contains(messageId); +} + +/** + * CUSTOM COLUMNS. + */ + +// Add a custom column handler. +NS_IMETHODIMP +nsMsgDBView::AddColumnHandler(const nsAString& column, + nsIMsgCustomColumnHandler* handler) { + bool custColInSort = false; + size_t index = m_customColumnHandlerIDs.IndexOf(column); + + nsAutoString strColID(column); + + // Does not exist. + if (index == m_customColumnHandlerIDs.NoIndex) { + m_customColumnHandlerIDs.AppendElement(strColID); + m_customColumnHandlers.AppendObject(handler); + } else { + // Insert new handler into the appropriate place in the COMPtr array; + // no need to replace the column ID (it's the same). + m_customColumnHandlers.ReplaceObjectAt(handler, index); + } + + // Check if the column name matches any of the columns in + // m_sortColumns, and if so, set m_sortColumns[i].mColHandler + for (uint32_t i = 0; i < m_sortColumns.Length(); i++) { + MsgViewSortColumnInfo& sortInfo = m_sortColumns[i]; + if (sortInfo.mSortType == nsMsgViewSortType::byCustom && + sortInfo.mCustomColumnName.Equals(column)) { + custColInSort = true; + sortInfo.mColHandler = handler; + } + } + + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) + // Grouped view has its own ways. + return NS_OK; + + // This cust col is in sort columns, and all are now registered, so sort. + if (custColInSort && !CustomColumnsInSortAndNotRegistered()) + Sort(m_sortType, m_sortOrder); + + return NS_OK; +} + +// Remove a custom column handler. +NS_IMETHODIMP +nsMsgDBView::RemoveColumnHandler(const nsAString& aColID) { + // Here we should check if the column name matches any of the columns in + // m_sortColumns, and if so, clear m_sortColumns[i].mColHandler. + size_t index = m_customColumnHandlerIDs.IndexOf(aColID); + + if (index != m_customColumnHandlerIDs.NoIndex) { + m_customColumnHandlerIDs.RemoveElementAt(index); + m_customColumnHandlers.RemoveObjectAt(index); + // Check if the column name matches any of the columns in + // m_sortColumns, and if so, clear m_sortColumns[i].mColHandler. + for (uint32_t i = 0; i < m_sortColumns.Length(); i++) { + MsgViewSortColumnInfo& sortInfo = m_sortColumns[i]; + if (sortInfo.mSortType == nsMsgViewSortType::byCustom && + sortInfo.mCustomColumnName.Equals(aColID)) + sortInfo.mColHandler = nullptr; + } + + return NS_OK; + } + + // Can't remove a column that isn't currently custom handled. + return NS_ERROR_FAILURE; +} + +// TODO: NS_ENSURE_SUCCESS +nsIMsgCustomColumnHandler* nsMsgDBView::GetCurColumnHandler() { + return GetColumnHandler(m_curCustomColumn); +} + +NS_IMETHODIMP +nsMsgDBView::SetCurCustomColumn(const nsAString& aColID) { + m_curCustomColumn = aColID; + if (m_viewFolder) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + folderInfo->SetProperty("customSortCol", aColID); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetCurCustomColumn(nsAString& result) { + result = m_curCustomColumn; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSecondaryCustomColumn(nsAString& result) { + result = m_secondaryCustomColumn; + return NS_OK; +} + +nsIMsgCustomColumnHandler* nsMsgDBView::GetColumnHandler( + const nsAString& colID) { + size_t index = m_customColumnHandlerIDs.IndexOf(colID); + return (index != m_customColumnHandlerIDs.NoIndex) + ? m_customColumnHandlers[index] + : nullptr; +} + +NS_IMETHODIMP +nsMsgDBView::GetColumnHandler(const nsAString& aColID, + nsIMsgCustomColumnHandler** aHandler) { + NS_ENSURE_ARG_POINTER(aHandler); + nsAutoString column(aColID); + NS_IF_ADDREF(*aHandler = GetColumnHandler(column)); + return (*aHandler) ? NS_OK : NS_ERROR_FAILURE; +} + +// Check if any active sort columns are custom. If none are custom, return false +// and go on as always. If any are custom, and all are not registered yet, +// return true (so that the caller can postpone sort). When the custom column +// observer is notified with MsgCreateDBView and registers the handler, +// AddColumnHandler will sort once all required handlers are set. +bool nsMsgDBView::CustomColumnsInSortAndNotRegistered() { + // The initial sort on view open has been started, subsequent user initiated + // sort callers can ignore verifying cust col registration. + m_checkedCustomColumns = true; + + // DecodeColumnSort must have already created m_sortColumns, otherwise we + // can't know, but go on anyway. + if (!m_sortColumns.Length()) return false; + + bool custColNotRegistered = false; + for (uint32_t i = 0; i < m_sortColumns.Length() && !custColNotRegistered; + i++) { + if (m_sortColumns[i].mSortType == nsMsgViewSortType::byCustom && + m_sortColumns[i].mColHandler == nullptr) + custColNotRegistered = true; + } + + return custColNotRegistered; +} +// END CUSTOM COLUMNS. + +NS_IMETHODIMP +nsMsgDBView::GetCellText(int32_t aRow, nsTreeColumn* aCol, nsAString& aValue) { + const nsAString& colID = aCol->GetId(); + + if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX; + + aValue.Truncate(); + + // Attempt to retrieve a custom column handler. If it exists call it and + // return. + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) { + colHandler->GetCellText(aRow, aCol, aValue); + return NS_OK; + } + + return CellTextForColumn(aRow, colID, aValue); +} + +NS_IMETHODIMP +nsMsgDBView::CellTextForColumn(int32_t aRow, const nsAString& aColumnName, + nsAString& aValue) { + if (aColumnName.IsEmpty()) { + aValue.Truncate(); + return NS_OK; + } + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + + if (NS_FAILED(rv) || !msgHdr) { + ClearHdrCache(); + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + nsCOMPtr<nsIMsgThread> thread; + + switch (aColumnName.First()) { + case 's': + if (aColumnName.EqualsLiteral("subjectCol")) + rv = FetchSubject(msgHdr, m_flags[aRow], aValue); + else if (aColumnName.EqualsLiteral("senderCol")) + rv = FetchAuthor(msgHdr, aValue); + else if (aColumnName.EqualsLiteral("sizeCol")) + rv = FetchSize(msgHdr, aValue); + else if (aColumnName.EqualsLiteral("statusCol")) { + uint32_t flags; + msgHdr->GetFlags(&flags); + rv = FetchStatus(flags, aValue); + } + break; + case 'r': + if (aColumnName.EqualsLiteral("recipientCol")) + rv = FetchRecipients(msgHdr, aValue); + else if (aColumnName.EqualsLiteral("receivedCol")) + rv = FetchDate(msgHdr, aValue, true); + break; + case 'd': + if (aColumnName.EqualsLiteral("dateCol")) rv = FetchDate(msgHdr, aValue); + break; + case 'c': + if (aColumnName.EqualsLiteral("correspondentCol")) { + if (IsOutgoingMsg(msgHdr)) + rv = FetchRecipients(msgHdr, aValue); + else + rv = FetchAuthor(msgHdr, aValue); + } + break; + case 'p': + if (aColumnName.EqualsLiteral("priorityCol")) + rv = FetchPriority(msgHdr, aValue); + break; + case 'a': + if (aColumnName.EqualsLiteral("accountCol")) + rv = FetchAccount(msgHdr, aValue); + break; + case 't': + // total msgs in thread column + if (aColumnName.EqualsLiteral("totalCol") && + m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + if (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) { + rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + nsAutoString formattedCountString; + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + formattedCountString.AppendInt(numChildren); + aValue.Assign(formattedCountString); + } + } + } else if (aColumnName.EqualsLiteral("tagsCol")) { + rv = FetchTags(msgHdr, aValue); + } + break; + case 'u': + // unread msgs in thread col + if (aColumnName.EqualsLiteral("unreadCol") && + m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + if (m_flags[aRow] & MSG_VIEW_FLAG_ISTHREAD) { + rv = GetThreadContainingIndex(aRow, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv) && thread) { + nsAutoString formattedCountString; + uint32_t numUnreadChildren; + thread->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) { + formattedCountString.AppendInt(numUnreadChildren); + aValue.Assign(formattedCountString); + } + } + } + } + break; + case 'j': { + if (aColumnName.EqualsLiteral("junkStatusCol")) { + nsCString junkScoreStr; + msgHdr->GetStringProperty("junkscore", junkScoreStr); + CopyASCIItoUTF16(junkScoreStr, aValue); + } + break; + } + case 'i': { + if (aColumnName.EqualsLiteral("idCol")) { + nsAutoString keyString; + nsMsgKey key; + msgHdr->GetMessageKey(&key); + keyString.AppendInt((int64_t)key); + aValue.Assign(keyString); + } + break; + } + case 'l': { + if (aColumnName.EqualsLiteral("locationCol")) { + nsCOMPtr<nsIMsgFolder> folder; + nsresult rv = GetFolderForViewIndex(aRow, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + folder->GetPrettyName(aValue); + } + break; + } + default: + break; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::CellDataForColumns(int32_t aRow, + const nsTArray<nsString>& aColumnNames, + nsAString& aProperties, int32_t* aThreadLevel, + nsTArray<nsString>& _retval) { + nsresult rv; + _retval.Clear(); + + uint32_t count = aColumnNames.Length(); + _retval.SetCapacity(count); + for (nsString column : aColumnNames) { + nsString text; + rv = CellTextForColumn(aRow, column, text); + if (NS_FAILED(rv)) { + _retval.Clear(); + return rv; + } + _retval.AppendElement(text); + } + + rv = GetRowProperties(aRow, aProperties); + if (NS_FAILED(rv)) { + _retval.Clear(); + return rv; + } + + rv = GetLevel(aRow, aThreadLevel); + if (NS_FAILED(rv)) { + _retval.Clear(); + aProperties.Truncate(); + } + + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::SetTree(mozilla::dom::XULTreeElement* tree) { + mTree = tree; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetJSTree(nsIMsgJSTree* tree) { + mJSTree = tree; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::ToggleOpenState(int32_t index) { + uint32_t numChanged; + nsresult rv = ToggleExpansion(index, &numChanged); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::CycleHeader(nsTreeColumn* aCol) { + // Let HandleColumnClick() in threadPane.js handle it + // since it will set / clear the sort indicators. + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::CycleCell(int32_t row, nsTreeColumn* col) { + if (!IsValidIndex(row)) { + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + const nsAString& colID = col->GetId(); + + // Attempt to retrieve a custom column handler. If it exists call it and + // return. + nsIMsgCustomColumnHandler* colHandler = GetColumnHandler(colID); + + if (colHandler) { + colHandler->CycleCell(row, col); + return NS_OK; + } + + // The cyclers below don't work for the grouped header dummy row, currently. + // A future implementation should consider both collapsed and expanded state. + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort && + m_flags[row] & MSG_VIEW_FLAG_DUMMY) + return NS_OK; + + if (colID.IsEmpty()) return NS_OK; + + switch (colID.First()) { + case 'u': + if (colID.EqualsLiteral("unreadButtonColHeader")) { + ApplyCommandToIndices(nsMsgViewCommandType::toggleMessageRead, + {(nsMsgViewIndex)row}); + } + break; + case 't': + if (colID.EqualsLiteral("threadCol")) { + ExpandAndSelectThreadByIndex(row, false); + } else if (colID.EqualsLiteral("tagsCol")) { + // XXX Do we want to keep this behaviour but switch it to tags? + // We could enumerate over the tags and go to the next one - it looks + // to me like this wasn't working before tags landed, so maybe not + // worth bothering with. + } + break; + case 'f': + if (colID.EqualsLiteral("flaggedCol")) { + // toggle the flagged status of the element at row. + if (m_flags[row] & nsMsgMessageFlags::Marked) { + ApplyCommandToIndices(nsMsgViewCommandType::unflagMessages, + {(nsMsgViewIndex)row}); + } else { + ApplyCommandToIndices(nsMsgViewCommandType::flagMessages, + {(nsMsgViewIndex)row}); + } + } + break; + case 'j': { + if (!colID.EqualsLiteral("junkStatusCol") || !JunkControlsEnabled(row)) { + return NS_OK; + } + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(row, getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv) && msgHdr) { + nsCString junkScoreStr; + rv = msgHdr->GetStringProperty("junkscore", junkScoreStr); + if (junkScoreStr.IsEmpty() || + (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_HAM_SCORE)) { + ApplyCommandToIndices(nsMsgViewCommandType::junk, + {(nsMsgViewIndex)row}); + } else { + ApplyCommandToIndices(nsMsgViewCommandType::unjunk, + {(nsMsgViewIndex)row}); + } + NS_ASSERTION(NS_SUCCEEDED(rv), + "Converting junkScore to integer failed."); + } + break; + } + default: + break; + } + + return NS_OK; +} + +/////////////////////////////////////////////////////////////////////////// +// end nsITreeView Implementation Methods +/////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsMsgDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) { + m_viewFlags = viewFlags; + m_sortOrder = sortOrder; + m_sortType = sortType; + + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + + NS_ENSURE_SUCCESS(rv, rv); + bool userNeedsToAuthenticate = false; + // If we're PasswordProtectLocalCache, then we need to find out if the + // server is authenticated. + (void)accountManager->GetUserNeedsToAuthenticate(&userNeedsToAuthenticate); + if (userNeedsToAuthenticate) return NS_MSG_USER_NOT_AUTHENTICATED; + + if (folder) { + // Search view will have a null folder. + nsCOMPtr<nsIDBFolderInfo> folderInfo; + rv = folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(m_db)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgDBService->RegisterPendingListener(folder, this); + m_folder = folder; + + if (!m_viewFolder) { + // There is never a viewFolder already set except for the single folder + // saved search case, where the backing folder m_folder is different from + // the m_viewFolder with its own dbFolderInfo state. + m_viewFolder = folder; + } + + SetMRUTimeForFolder(m_viewFolder); + + RestoreSortInfo(); + + // Determine if we are in a news folder or not. If yes, we'll show lines + // instead of size, and special icons in the thread pane. + nsCOMPtr<nsIMsgIncomingServer> server; + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsCString type; + rv = server->GetType(type); + NS_ENSURE_SUCCESS(rv, rv); + + // I'm not sure this is correct, because XF virtual folders with mixed news + // and mail can have this set. + mIsNews = type.LowerCaseEqualsLiteral("nntp"); + + // Default to a virtual folder if folder not set, since synthetic search + // views may not have a folder. + uint32_t folderFlags = nsMsgFolderFlags::Virtual; + if (folder) folder->GetFlags(&folderFlags); + + mIsXFVirtual = folderFlags & nsMsgFolderFlags::Virtual; + if (!mIsXFVirtual && type.LowerCaseEqualsLiteral("rss")) mIsRss = true; + + // Special case nntp --> news since we'll break themes if we try to be + // consistent. + if (mIsNews) + mMessageType.AssignLiteral("news"); + else + CopyUTF8toUTF16(type, mMessageType); + + GetImapDeleteModel(nullptr); + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) { + prefs->GetBoolPref("mailnews.sort_threads_by_root", &mSortThreadsByRoot); + if (mIsNews) + prefs->GetBoolPref("news.show_size_in_lines", &mShowSizeInLines); + } + } + + nsTArray<RefPtr<nsIMsgIdentity>> identities; + rv = accountManager->GetAllIdentities(identities); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto identity : identities) { + if (!identity) continue; + + nsCString email; + identity->GetEmail(email); + if (!email.IsEmpty()) { + ToLowerCaseDropPlusAddessing(email); + mEmails.PutEntry(email); + } + + identity->GetReplyTo(email); + if (!email.IsEmpty()) { + ToLowerCaseDropPlusAddessing(email); + mEmails.PutEntry(email); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::Close() { + int32_t oldSize = GetSize(); + // This is important, because the tree will ask us for our row count, which + // gets determined from the number of keys. + m_keys.Clear(); + // Be consistent. + m_flags.Clear(); + m_levels.Clear(); + + // Clear these out since they no longer apply if we're switching a folder + mJunkHdrs.Clear(); + + // This needs to happen after we remove all the keys, since RowCountChanged() + // will call our GetRowCount(). + if (mTree) mTree->RowCountChanged(0, -oldSize); + if (mJSTree) mJSTree->RowCountChanged(0, -oldSize); + + ClearHdrCache(); + if (m_db) { + m_db->RemoveListener(this); + m_db = nullptr; + } + if (m_folder) { + nsresult rv; + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgDBService->UnregisterPendingListener(this); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders, + nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, + nsMsgViewFlagsTypeValue aViewFlags, int32_t* aCount) { + NS_ASSERTION(false, "not implemented"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBView::Init(nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) { + mMessengerWeak = do_GetWeakReference(aMessengerInstance); + mMsgWindowWeak = do_GetWeakReference(aMsgWindow); + mCommandUpdater = do_GetWeakReference(aCmdUpdater); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSuppressCommandUpdating(bool aSuppressCommandUpdating) { + mSuppressCommandUpdating = aSuppressCommandUpdating; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSuppressCommandUpdating(bool* aSuppressCommandUpdating) { + *aSuppressCommandUpdating = mSuppressCommandUpdating; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSuppressMsgDisplay(bool aSuppressDisplay) { + mSuppressMsgDisplay = aSuppressDisplay; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSuppressMsgDisplay(bool* aSuppressDisplay) { + *aSuppressDisplay = mSuppressMsgDisplay; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetUsingLines(bool* aUsingLines) { + *aUsingLines = mShowSizeInLines; + return NS_OK; +} + +int CompareViewIndices(const void* v1, const void* v2, void*) { + nsMsgViewIndex i1 = *(nsMsgViewIndex*)v1; + nsMsgViewIndex i2 = *(nsMsgViewIndex*)v2; + return i1 - i2; +} + +// Array<nsMsgViewIndex> getIndicesForSelection(); +NS_IMETHODIMP +nsMsgDBView::GetIndicesForSelection(nsTArray<nsMsgViewIndex>& indices) { + indices.Clear(); + if (mTreeSelection) { + int32_t viewSize = GetSize(); + int32_t count; + mTreeSelection->GetCount(&count); + indices.SetCapacity(count); + int32_t selectionCount; + mTreeSelection->GetRangeCount(&selectionCount); + for (int32_t i = 0; i < selectionCount; i++) { + int32_t startRange = -1; + int32_t endRange = -1; + mTreeSelection->GetRangeAt(i, &startRange, &endRange); + if (startRange >= 0 && startRange < viewSize) { + for (int32_t rangeIndex = startRange; + rangeIndex <= endRange && rangeIndex < viewSize; rangeIndex++) { + indices.AppendElement(rangeIndex); + } + } + } + + NS_ASSERTION(indices.Length() == uint32_t(count), + "selection count is wrong"); + } else { + // If there is no tree selection object then we must be in stand alone + // message mode. In that case the selected indices are really just the + // current message key. + nsMsgViewIndex viewIndex = FindViewIndex(m_currentlyDisplayedMsgKey); + if (viewIndex != nsMsgViewIndex_None) indices.AppendElement(viewIndex); + } + + return NS_OK; +} + +// Array<nsIMsgDBHdr> getSelectedMsgHdrs(); +NS_IMETHODIMP +nsMsgDBView::GetSelectedMsgHdrs(nsTArray<RefPtr<nsIMsgDBHdr>>& aResult) { + nsMsgViewIndexArray selection; + aResult.Clear(); + nsresult rv = GetIndicesForSelection(selection); + NS_ENSURE_SUCCESS(rv, rv); + return GetHeadersFromSelection(selection, aResult); +} + +NS_IMETHODIMP +nsMsgDBView::GetURIsForSelection(nsTArray<nsCString>& uris) { + uris.Clear(); + AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages; + nsresult rv = GetSelectedMsgHdrs(messages); + NS_ENSURE_SUCCESS(rv, rv); + uris.SetCapacity(messages.Length()); + for (nsIMsgDBHdr* msgHdr : messages) { + nsCString tmpUri; + nsCOMPtr<nsIMsgFolder> folder; + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFolder(getter_AddRefs(folder)); + rv = GenerateURIForMsgKey(msgKey, folder, tmpUri); + NS_ENSURE_SUCCESS(rv, rv); + uris.AppendElement(tmpUri); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetURIForViewIndex(nsMsgViewIndex index, nsACString& result) { + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder = m_folder; + if (!folder) { + rv = GetFolderForViewIndex(index, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (index == nsMsgViewIndex_None || index >= m_flags.Length() || + m_flags[index] & MSG_VIEW_FLAG_DUMMY) { + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + return GenerateURIForMsgKey(m_keys[index], folder, result); +} + +NS_IMETHODIMP +nsMsgDBView::DoCommandWithFolder(nsMsgViewCommandTypeValue command, + nsIMsgFolder* destFolder) { + NS_ENSURE_ARG_POINTER(destFolder); + + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + + nsresult rv = NS_OK; + switch (command) { + case nsMsgViewCommandType::copyMessages: + case nsMsgViewCommandType::moveMessages: + rv = ApplyCommandToIndicesWithFolder(command, selection, destFolder); + NoteChange(0, 0, nsMsgViewNotificationCode::none); + break; + default: + NS_ASSERTION(false, "invalid command type"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::DoCommand(nsMsgViewCommandTypeValue command) { + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + + nsresult rv = NS_OK; + switch (command) { + case nsMsgViewCommandType::downloadSelectedForOffline: + return DownloadForOffline(msgWindow, selection); + case nsMsgViewCommandType::downloadFlaggedForOffline: + return DownloadFlaggedForOffline(msgWindow); + case nsMsgViewCommandType::markMessagesRead: + case nsMsgViewCommandType::markMessagesUnread: + case nsMsgViewCommandType::toggleMessageRead: + case nsMsgViewCommandType::flagMessages: + case nsMsgViewCommandType::unflagMessages: + case nsMsgViewCommandType::deleteMsg: + case nsMsgViewCommandType::undeleteMsg: + case nsMsgViewCommandType::deleteNoTrash: + case nsMsgViewCommandType::markThreadRead: + case nsMsgViewCommandType::junk: + case nsMsgViewCommandType::unjunk: + rv = ApplyCommandToIndices(command, selection); + NoteChange(0, 0, nsMsgViewNotificationCode::none); + break; + case nsMsgViewCommandType::selectAll: + if (mTreeSelection) { + // If in threaded mode, we need to expand all before selecting. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) + rv = ExpandAll(); + + mTreeSelection->SelectAll(); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + } + break; + case nsMsgViewCommandType::selectThread: + rv = ExpandAndSelectThread(); + break; + case nsMsgViewCommandType::selectFlagged: + if (!mTreeSelection) { + rv = NS_ERROR_UNEXPECTED; + } else { + mTreeSelection->SetSelectEventsSuppressed(true); + mTreeSelection->ClearSelection(); + // XXX ExpandAll? + uint32_t numIndices = GetSize(); + for (uint32_t curIndex = 0; curIndex < numIndices; curIndex++) { + if (m_flags[curIndex] & nsMsgMessageFlags::Marked) + mTreeSelection->ToggleSelect(curIndex); + } + + mTreeSelection->SetSelectEventsSuppressed(false); + } + break; + case nsMsgViewCommandType::markAllRead: + if (m_folder) { + SetSuppressChangeNotifications(true); + rv = m_folder->MarkAllMessagesRead(msgWindow); + SetSuppressChangeNotifications(false); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + } + break; + case nsMsgViewCommandType::toggleThreadWatched: + rv = ToggleWatched(selection); + break; + case nsMsgViewCommandType::expandAll: + rv = ExpandAll(); + m_viewFlags |= nsMsgViewFlagsType::kExpandAll; + SetViewFlags(m_viewFlags); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + + break; + case nsMsgViewCommandType::collapseAll: + rv = CollapseAll(); + m_viewFlags &= ~nsMsgViewFlagsType::kExpandAll; + SetViewFlags(m_viewFlags); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + + break; + default: + NS_ASSERTION(false, "invalid command type"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + return rv; +} + +bool nsMsgDBView::ServerSupportsFilterAfterTheFact() { + // Cross folder virtual folders might not have a folder set. + if (!m_folder) return false; + + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = m_folder->GetServer(getter_AddRefs(server)); + // Unexpected. + if (NS_FAILED(rv)) return false; + + // Filter after the fact is implement using search so if you can't search, + // you can't filter after the fact. + bool canSearch; + rv = server->GetCanSearchMessages(&canSearch); + // Unexpected. + if (NS_FAILED(rv)) return false; + + return canSearch; +} + +NS_IMETHODIMP +nsMsgDBView::GetCommandStatus(nsMsgViewCommandTypeValue command, + bool* selectable_p, + nsMsgViewCommandCheckStateValue* selected_p) { + nsresult rv = NS_OK; + + bool haveSelection; + int32_t rangeCount; + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + // If range count is non-zero, we have at least one item selected, so we + // have a selection. + if (mTreeSelection && + NS_SUCCEEDED(mTreeSelection->GetRangeCount(&rangeCount)) && + rangeCount > 0) { + haveSelection = NonDummyMsgSelected(selection); + } else { + // If we don't have a tree selection we must be in stand alone mode. + haveSelection = IsValidIndex(m_currentlyDisplayedViewIndex); + } + + switch (command) { + case nsMsgViewCommandType::deleteMsg: + case nsMsgViewCommandType::deleteNoTrash: { + bool canDelete; + if (m_folder && + NS_SUCCEEDED(m_folder->GetCanDeleteMessages(&canDelete)) && + !canDelete) { + *selectable_p = false; + } else { + *selectable_p = haveSelection; + } + break; + } + case nsMsgViewCommandType::applyFilters: + // Disable if no messages. + // XXX todo, check that we have filters, and at least one is enabled. + *selectable_p = GetSize(); + if (*selectable_p) *selectable_p = ServerSupportsFilterAfterTheFact(); + + break; + case nsMsgViewCommandType::runJunkControls: + // Disable if no messages. + // XXX todo, check that we have JMC enabled? + *selectable_p = GetSize() && JunkControlsEnabled(nsMsgViewIndex_None); + break; + case nsMsgViewCommandType::deleteJunk: { + // Disable if no messages, or if we can't delete (like news and + // certain imap folders). + bool canDelete; + *selectable_p = + GetSize() && m_folder && + NS_SUCCEEDED(m_folder->GetCanDeleteMessages(&canDelete)) && canDelete; + break; + } + case nsMsgViewCommandType::markMessagesRead: + case nsMsgViewCommandType::markMessagesUnread: + case nsMsgViewCommandType::toggleMessageRead: + case nsMsgViewCommandType::flagMessages: + case nsMsgViewCommandType::unflagMessages: + case nsMsgViewCommandType::toggleThreadWatched: + case nsMsgViewCommandType::markThreadRead: + case nsMsgViewCommandType::downloadSelectedForOffline: + *selectable_p = haveSelection; + break; + case nsMsgViewCommandType::junk: + case nsMsgViewCommandType::unjunk: + *selectable_p = haveSelection && !selection.IsEmpty() && + JunkControlsEnabled(selection[0]); + break; + case nsMsgViewCommandType::cmdRequiringMsgBody: + *selectable_p = + haveSelection && (!WeAreOffline() || OfflineMsgSelected(selection)); + break; + case nsMsgViewCommandType::downloadFlaggedForOffline: + case nsMsgViewCommandType::markAllRead: + *selectable_p = true; + break; + default: + NS_ASSERTION(false, "invalid command type"); + rv = NS_ERROR_FAILURE; + } + + return rv; +} + +// This method needs to be overridden by the various view classes +// that have different kinds of threads. For example, in a +// threaded quick search db view, we'd only want to include children +// of the thread that fit the view (IMO). And when we have threaded +// cross folder views, we would include all the children of the +// cross-folder thread. +nsresult nsMsgDBView::ListCollapsedChildren( + nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgThread> thread; + GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr)); + if (!msgHdr) { + NS_ASSERTION(false, "couldn't find message to expand"); + return NS_MSG_MESSAGE_NOT_FOUND; + } + + nsresult rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + for (uint32_t i = 1; i < numChildren && NS_SUCCEEDED(rv); i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = thread->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + if (msgHdr) { + messageArray.AppendElement(msgHdr); + } + } + + return rv; +} + +bool nsMsgDBView::OperateOnMsgsInCollapsedThreads() { + if (!mJSTree && mTreeSelection) { + RefPtr<mozilla::dom::XULTreeElement> selTree; + mTreeSelection->GetTree(getter_AddRefs(selTree)); + // No tree means stand-alone message window. + if (!selTree) return false; + } + + nsresult rv = NS_OK; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, false); + + bool includeCollapsedMsgs = false; + prefBranch->GetBoolPref("mail.operate_on_msgs_in_collapsed_threads", + &includeCollapsedMsgs); + return includeCollapsedMsgs; +} + +nsresult nsMsgDBView::GetHeadersFromSelection( + nsTArray<nsMsgViewIndex> const& selection, + nsTArray<RefPtr<nsIMsgDBHdr>>& hdrs) { + hdrs.Clear(); + hdrs.SetCapacity(selection.Length()); // Best guess. + nsresult rv = NS_OK; + + // Don't include collapsed messages if the front end failed to summarize + // the selection. + bool includeCollapsedMsgs = + OperateOnMsgsInCollapsedThreads() && !mSummarizeFailed; + + for (nsMsgViewIndex viewIndex : selection) { + if (NS_FAILED(rv)) { + break; + } + if (viewIndex == nsMsgViewIndex_None) { + continue; + } + + uint32_t viewIndexFlags = m_flags[viewIndex]; + if (viewIndexFlags & MSG_VIEW_FLAG_DUMMY) { + // If collapsed dummy header selected, list its children. + if (includeCollapsedMsgs && viewIndexFlags & nsMsgMessageFlags::Elided && + m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) + rv = ListCollapsedChildren(viewIndex, hdrs); + + continue; + } + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv) && msgHdr) { + hdrs.AppendElement(msgHdr); + if (includeCollapsedMsgs && viewIndexFlags & nsMsgMessageFlags::Elided && + viewIndexFlags & MSG_VIEW_FLAG_HASCHILDREN && + m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + rv = ListCollapsedChildren(viewIndex, hdrs); + } + } + } + + return rv; +} + +nsresult nsMsgDBView::CopyMessages(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection, + bool isMove, nsIMsgFolder* destFolder) { + if (m_deletingRows) { + NS_ASSERTION(false, "Last move did not complete"); + return NS_OK; + } + + nsresult rv; + NS_ENSURE_ARG_POINTER(destFolder); + + AutoTArray<RefPtr<nsIMsgDBHdr>, 1> hdrs; + rv = GetHeadersFromSelection(selection, hdrs); + NS_ENSURE_SUCCESS(rv, rv); + + m_deletingRows = isMove && mDeleteModel != nsMsgImapDeleteModels::IMAPDelete; + if (m_deletingRows) { + mIndicesToNoteChange.AppendElements(selection); + } + + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return copyService->CopyMessages(m_folder /* source folder */, hdrs, + destFolder, isMove, nullptr /* listener */, + window, true /* allow Undo */); +} + +nsresult nsMsgDBView::ApplyCommandToIndicesWithFolder( + nsMsgViewCommandTypeValue command, + nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder) { + nsresult rv = NS_OK; + NS_ENSURE_ARG_POINTER(destFolder); + + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + switch (command) { + case nsMsgViewCommandType::copyMessages: + NS_ASSERTION(!(m_folder == destFolder), + "The source folder and the destination folder are the same"); + if (m_folder != destFolder) + rv = CopyMessages(msgWindow, selection, false /* isMove */, destFolder); + + break; + case nsMsgViewCommandType::moveMessages: + NS_ASSERTION(!(m_folder == destFolder), + "The source folder and the destination folder are the same"); + if (m_folder != destFolder) + rv = CopyMessages(msgWindow, selection, true /* isMove */, destFolder); + + break; + default: + NS_ASSERTION(false, "unhandled command"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::ApplyCommandToIndices(nsMsgViewCommandTypeValue command, + nsTArray<nsMsgViewIndex> const& selection) { + if (selection.IsEmpty()) { + // Return quietly, just in case/ + return NS_OK; + } + + nsCOMPtr<nsIMsgFolder> folder; + nsresult rv = GetFolderForViewIndex(selection[0], getter_AddRefs(folder)); + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + if (command == nsMsgViewCommandType::deleteMsg) + return DeleteMessages(msgWindow, selection, false); + + if (command == nsMsgViewCommandType::deleteNoTrash) + return DeleteMessages(msgWindow, selection, true); + + nsTArray<nsMsgKey> imapUids; + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder); + bool thisIsImapFolder = (imapFolder != nullptr); + nsCOMPtr<nsIJunkMailPlugin> junkPlugin; + + // If this is a junk command, get the junk plugin. + if (command == nsMsgViewCommandType::junk || + command == nsMsgViewCommandType::unjunk) { + // Get the folder from the first item; we assume that + // all messages in the view are from the same folder (no + // more junk status column in the 'search messages' dialog + // like in earlier versions...). + nsCOMPtr<nsIMsgIncomingServer> server; + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFilterPlugin> filterPlugin; + rv = server->GetSpamFilterPlugin(getter_AddRefs(filterPlugin)); + NS_ENSURE_SUCCESS(rv, rv); + + junkPlugin = do_QueryInterface(filterPlugin, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, + false); + + // No sense going through the code that handles messages in collasped threads + // for mark thread read. + if (command == nsMsgViewCommandType::markThreadRead) { + for (nsMsgViewIndex viewIndex : selection) { + SetThreadOfMsgReadByIndex(viewIndex, imapUids, true); + } + } else { + // Turn the selection into an array of msg hdrs. This may include messages + // in collapsed threads + AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages; + rv = GetHeadersFromSelection(selection, messages); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t length = messages.Length(); + + if (thisIsImapFolder) { + imapUids.SetLength(length); + } + + for (uint32_t i = 0; i < length; i++) { + nsMsgKey msgKey; + nsCOMPtr<nsIMsgDBHdr> msgHdr(messages[i]); + msgHdr->GetMessageKey(&msgKey); + if (thisIsImapFolder) imapUids[i] = msgKey; + + switch (command) { + case nsMsgViewCommandType::junk: + mNumMessagesRemainingInBatch++; + mJunkHdrs.AppendElement(msgHdr); + rv = SetMsgHdrJunkStatus(junkPlugin.get(), msgHdr, + nsIJunkMailPlugin::JUNK); + break; + case nsMsgViewCommandType::unjunk: + mNumMessagesRemainingInBatch++; + mJunkHdrs.AppendElement(msgHdr); + rv = SetMsgHdrJunkStatus(junkPlugin.get(), msgHdr, + nsIJunkMailPlugin::GOOD); + break; + case nsMsgViewCommandType::toggleMessageRead: + case nsMsgViewCommandType::undeleteMsg: + case nsMsgViewCommandType::markMessagesRead: + case nsMsgViewCommandType::markMessagesUnread: + case nsMsgViewCommandType::unflagMessages: + case nsMsgViewCommandType::flagMessages: + // This is completely handled in the code below. + break; + default: + NS_ERROR("unhandled command"); + break; + } + } + + switch (command) { + case nsMsgViewCommandType::toggleMessageRead: { + if (messages.IsEmpty()) break; + + uint32_t msgFlags; + messages[0]->GetFlags(&msgFlags); + folder->MarkMessagesRead(messages, + !(msgFlags & nsMsgMessageFlags::Read)); + break; + } + case nsMsgViewCommandType::markMessagesRead: + case nsMsgViewCommandType::markMessagesUnread: + folder->MarkMessagesRead( + messages, command == nsMsgViewCommandType::markMessagesRead); + break; + case nsMsgViewCommandType::unflagMessages: + case nsMsgViewCommandType::flagMessages: + folder->MarkMessagesFlagged( + messages, command == nsMsgViewCommandType::flagMessages); + break; + default: + break; + } + + // Provide junk-related batch notifications. + if (command == nsMsgViewCommandType::junk || + command == nsMsgViewCommandType::unjunk) { + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) { + notifier->NotifyMsgsJunkStatusChanged(messages); + } + } + } + + folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, true); + + if (thisIsImapFolder) { + imapMessageFlagsType flags = kNoImapMsgFlag; + bool addFlags = false; + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + switch (command) { + case nsMsgViewCommandType::markThreadRead: + flags |= kImapMsgSeenFlag; + addFlags = true; + break; + case nsMsgViewCommandType::undeleteMsg: + flags = kImapMsgDeletedFlag; + addFlags = false; + break; + case nsMsgViewCommandType::junk: + return imapFolder->StoreCustomKeywords(msgWindow, "Junk"_ns, + "NonJunk"_ns, imapUids, nullptr); + case nsMsgViewCommandType::unjunk: { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetHdrForFirstSelectedMessage(getter_AddRefs(msgHdr)); + uint32_t msgFlags = 0; + if (msgHdr) msgHdr->GetFlags(&msgFlags); + + if (msgFlags & nsMsgMessageFlags::IMAPDeleted) + imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, imapUids, + nullptr); + + return imapFolder->StoreCustomKeywords(msgWindow, "NonJunk"_ns, + "Junk"_ns, imapUids, nullptr); + } + default: + break; + } + + // Can't get here without thisIsImapThreadPane == TRUE. + if (flags != kNoImapMsgFlag) { + imapFolder->StoreImapFlags(flags, addFlags, imapUids, nullptr); + } + } + + return rv; +} + +/** + * View modifications methods by index. + */ + +// This method just removes the specified line from the view. It does +// NOT delete it from the database. +nsresult nsMsgDBView::RemoveByIndex(nsMsgViewIndex index) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + m_keys.RemoveElementAt(index); + m_flags.RemoveElementAt(index); + m_levels.RemoveElementAt(index); + + // The call to NoteChange() has to happen after we remove the key as + // NoteChange() will call RowCountChanged() which will call our GetRowCount(). + // An example where view is not the listener - D&D messages. + if (!m_deletingRows) + NoteChange(index, -1, nsMsgViewNotificationCode::insertOrDelete); + + return NS_OK; +} + +nsresult nsMsgDBView::DeleteMessages(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection, + bool deleteStorage) { + if (m_deletingRows) { + NS_WARNING("Last delete did not complete"); + return NS_OK; + } + + nsresult rv; + AutoTArray<RefPtr<nsIMsgDBHdr>, 1> hdrs; + rv = GetHeadersFromSelection(selection, hdrs); + NS_ENSURE_SUCCESS(rv, rv); + + const char* warnCollapsedPref = "mail.warn_on_collapsed_thread_operation"; + const char* warnShiftDelPref = "mail.warn_on_shift_delete"; + const char* warnNewsPref = "news.warn_on_delete"; + const char* warnTrashDelPref = "mail.warn_on_delete_from_trash"; + const char* activePref = nullptr; + nsString warningName; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + bool trashFolder = false; + rv = m_folder->GetFlag(nsMsgFolderFlags::Trash, &trashFolder); + NS_ENSURE_SUCCESS(rv, rv); + + if (trashFolder) { + bool pref = false; + prefBranch->GetBoolPref(warnTrashDelPref, &pref); + if (pref) { + activePref = warnTrashDelPref; + warningName.AssignLiteral("confirmMsgDelete.deleteFromTrash.desc"); + } + } + + if (!activePref && (selection.Length() != hdrs.Length())) { + bool pref = false; + prefBranch->GetBoolPref(warnCollapsedPref, &pref); + if (pref) { + activePref = warnCollapsedPref; + warningName.AssignLiteral("confirmMsgDelete.collapsed.desc"); + } + } + + if (!activePref && deleteStorage && !trashFolder) { + bool pref = false; + prefBranch->GetBoolPref(warnShiftDelPref, &pref); + if (pref) { + activePref = warnShiftDelPref; + warningName.AssignLiteral("confirmMsgDelete.deleteNoTrash.desc"); + } + } + + if (!activePref && mIsNews) { + bool pref = false; + prefBranch->GetBoolPref(warnNewsPref, &pref); + if (pref) { + activePref = warnNewsPref; + warningName.AssignLiteral("confirmMsgDelete.deleteNoTrash.desc"); + } + } + + if (activePref) { + nsCOMPtr<nsIPrompt> dialog; + + nsCOMPtr<nsIWindowWatcher> wwatch( + do_GetService(NS_WINDOWWATCHER_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = wwatch->GetNewPrompter(0, getter_AddRefs(dialog)); + NS_ENSURE_SUCCESS(rv, rv); + // "Don't ask..." - unchecked by default. + bool dontAsk = false; + int32_t buttonPressed = 0; + + nsString dialogTitle; + nsString confirmString; + nsString checkboxText; + nsString buttonApplyNowText; + GetString(u"confirmMsgDelete.title", dialogTitle); + GetString(u"confirmMsgDelete.dontAsk.label", checkboxText); + GetString(u"confirmMsgDelete.delete.label", buttonApplyNowText); + + GetString(warningName.get(), confirmString); + + const uint32_t buttonFlags = + (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) + + (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1); + + rv = dialog->ConfirmEx(dialogTitle.get(), confirmString.get(), buttonFlags, + buttonApplyNowText.get(), nullptr, nullptr, + checkboxText.get(), &dontAsk, &buttonPressed); + NS_ENSURE_SUCCESS(rv, rv); + if (buttonPressed) return NS_ERROR_FAILURE; + + if (dontAsk) prefBranch->SetBoolPref(activePref, false); + } + + if (!deleteStorage) { + rv = m_folder->MarkMessagesRead(hdrs, true); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete) { + m_deletingRows = true; + } + + if (m_deletingRows) { + mIndicesToNoteChange.AppendElements(selection); + } + + rv = m_folder->DeleteMessages(hdrs, window, deleteStorage, false, nullptr, + true /* allow Undo */); + if (NS_FAILED(rv)) { + m_deletingRows = false; + } + + return rv; +} + +nsresult nsMsgDBView::DownloadForOffline( + nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection) { + nsresult rv = NS_OK; + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + for (nsMsgViewIndex viewIndex : selection) { + nsMsgKey key = m_keys[viewIndex]; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = m_db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (msgHdr) { + uint32_t flags; + msgHdr->GetFlags(&flags); + if (!(flags & nsMsgMessageFlags::Offline)) { + messages.AppendElement(msgHdr); + } + } + } + + m_folder->DownloadMessagesForOffline(messages, window); + return rv; +} + +nsresult nsMsgDBView::DownloadFlaggedForOffline(nsIMsgWindow* window) { + nsresult rv = NS_OK; + nsTArray<RefPtr<nsIMsgDBHdr>> messages; + nsCOMPtr<nsIMsgEnumerator> enumerator; + rv = GetMessageEnumerator(getter_AddRefs(enumerator)); + if (NS_SUCCEEDED(rv) && enumerator) { + bool hasMore; + while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) && + hasMore) { + nsCOMPtr<nsIMsgDBHdr> header; + rv = enumerator->GetNext(getter_AddRefs(header)); + if (header && NS_SUCCEEDED(rv)) { + uint32_t flags; + header->GetFlags(&flags); + if ((flags & nsMsgMessageFlags::Marked) && + !(flags & nsMsgMessageFlags::Offline)) { + messages.AppendElement(header); + } + } + } + } + + m_folder->DownloadMessagesForOffline(messages, window); + return rv; +} + +// Read/unread handling. +nsresult nsMsgDBView::ToggleReadByIndex(nsMsgViewIndex index) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + return SetReadByIndex(index, !(m_flags[index] & nsMsgMessageFlags::Read)); +} + +nsresult nsMsgDBView::SetReadByIndex(nsMsgViewIndex index, bool read) { + nsresult rv; + + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (read) { + OrExtraFlag(index, nsMsgMessageFlags::Read); + // MarkRead() will clear this flag in the db and then call OnKeyChange(), + // but because we are the instigator of the change we'll ignore the change. + // So we need to clear it in m_flags to keep the db and m_flags in sync. + AndExtraFlag(index, ~nsMsgMessageFlags::New); + } else { + AndExtraFlag(index, ~nsMsgMessageFlags::Read); + } + + nsCOMPtr<nsIMsgDatabase> dbToUse; + rv = GetDBForViewIndex(index, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = dbToUse->MarkRead(m_keys[index], read, this); + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + nsMsgViewIndex threadIndex = GetThreadIndex(index); + if (threadIndex != index) + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + } + + return rv; +} + +nsresult nsMsgDBView::SetThreadOfMsgReadByIndex( + nsMsgViewIndex index, nsTArray<nsMsgKey>& keysMarkedRead, bool /*read*/) { + nsresult rv; + + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + rv = MarkThreadOfMsgRead(m_keys[index], index, keysMarkedRead, true); + return rv; +} + +nsresult nsMsgDBView::SetFlaggedByIndex(nsMsgViewIndex index, bool mark) { + nsresult rv; + + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsCOMPtr<nsIMsgDatabase> dbToUse; + rv = GetDBForViewIndex(index, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + + if (mark) + OrExtraFlag(index, nsMsgMessageFlags::Marked); + else + AndExtraFlag(index, ~nsMsgMessageFlags::Marked); + + rv = dbToUse->MarkMarked(m_keys[index], mark, this); + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + return rv; +} + +nsresult nsMsgDBView::SetMsgHdrJunkStatus(nsIJunkMailPlugin* aJunkPlugin, + nsIMsgDBHdr* aMsgHdr, + nsMsgJunkStatus aNewClassification) { + // Get the old junk score. + nsCString junkScoreStr; + nsresult rv = aMsgHdr->GetStringProperty("junkscore", junkScoreStr); + + // And the old origin. + nsCString oldOriginStr; + rv = aMsgHdr->GetStringProperty("junkscoreorigin", oldOriginStr); + + // If this was not classified by the user, say so. + nsMsgJunkStatus oldUserClassification; + if (oldOriginStr.get()[0] != 'u') { + oldUserClassification = nsIJunkMailPlugin::UNCLASSIFIED; + } else { + // Otherwise, pass the actual user classification. + if (junkScoreStr.IsEmpty()) + oldUserClassification = nsIJunkMailPlugin::UNCLASSIFIED; + else if (junkScoreStr.ToInteger(&rv) == nsIJunkMailPlugin::IS_SPAM_SCORE) + oldUserClassification = nsIJunkMailPlugin::JUNK; + else + oldUserClassification = nsIJunkMailPlugin::GOOD; + + NS_ASSERTION(NS_SUCCEEDED(rv), "Converting junkScore to integer failed."); + } + + // Get the URI for this message so we can pass it to the plugin. + nsCString uri; + nsMsgKey msgKey; + nsCOMPtr<nsIMsgFolder> folder; + nsCOMPtr<nsIMsgDatabase> db; + aMsgHdr->GetMessageKey(&msgKey); + rv = aMsgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + GenerateURIForMsgKey(msgKey, folder, uri); + NS_ENSURE_SUCCESS(rv, rv); + rv = folder->GetMsgDatabase(getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + + // Tell the plugin about this change, so that it can (potentially) + // adjust its database appropriately. + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + rv = aJunkPlugin->SetMessageClassification( + uri, oldUserClassification, aNewClassification, msgWindow, this); + NS_ENSURE_SUCCESS(rv, rv); + + // This routine is only reached if the user someone touched the UI + // and told us the junk status of this message. + // Set origin first so that listeners on the junkscore will + // know the correct origin. + rv = db->SetStringProperty(msgKey, "junkscoreorigin", "user"_ns); + NS_ASSERTION(NS_SUCCEEDED(rv), "SetStringPropertyByIndex failed"); + + // Set the junk score on the message itself. + nsAutoCString msgJunkScore; + msgJunkScore.AppendInt(aNewClassification == nsIJunkMailPlugin::JUNK + ? nsIJunkMailPlugin::IS_SPAM_SCORE + : nsIJunkMailPlugin::IS_HAM_SCORE); + db->SetStringProperty(msgKey, "junkscore", msgJunkScore); + NS_ENSURE_SUCCESS(rv, rv); + + return rv; +} + +nsresult nsMsgDBView::GetFolderFromMsgURI(const nsACString& aMsgURI, + nsIMsgFolder** aFolder) { + NS_IF_ADDREF(*aFolder = m_folder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnMessageClassified(const nsACString& aMsgURI, + nsMsgJunkStatus aClassification, + uint32_t aJunkPercent) + +{ + // Note: we know all messages in a batch have the same + // classification, since unlike OnMessageClassified + // methods in other classes (such as nsLocalMailFolder + // and nsImapMailFolder), this class, nsMsgDBView, currently + // only triggers message classifications due to a command to + // mark some of the messages in the view as junk, or as not + // junk - so the classification is dictated to the filter, + // not suggested by it. + // + // For this reason the only thing we (may) have to do is + // perform the action on all of the junk messages. + + uint32_t numJunk = mJunkHdrs.Length(); + NS_ASSERTION(aClassification == nsIJunkMailPlugin::GOOD || numJunk, + "the classification of a manually-marked junk message has " + "been classified as junk, yet there seem to be no such " + "outstanding messages"); + + // Is this the last message in the batch? + if (--mNumMessagesRemainingInBatch == 0 && numJunk > 0) { + PerformActionsOnJunkMsgs(aClassification == nsIJunkMailPlugin::JUNK); + mJunkHdrs.Clear(); + } + + return NS_OK; +} + +nsresult nsMsgDBView::PerformActionsOnJunkMsgs(bool msgsAreJunk) { + uint32_t numJunkHdrs = mJunkHdrs.Length(); + if (!numJunkHdrs) { + NS_ERROR("no indices of marked-as-junk messages to act on"); + return NS_OK; + } + + nsCOMPtr<nsIMsgFolder> srcFolder; + mJunkHdrs[0]->GetFolder(getter_AddRefs(srcFolder)); + + bool moveMessages, changeReadState; + nsCOMPtr<nsIMsgFolder> targetFolder; + + nsresult rv = DetermineActionsForJunkChange(msgsAreJunk, srcFolder, + moveMessages, changeReadState, + getter_AddRefs(targetFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + // Nothing to do, bail out. + if (!(moveMessages || changeReadState)) return NS_OK; + + if (changeReadState) { + // Notes on marking junk as read: + // 1. There are 2 occasions on which junk messages are marked as + // read: after a manual marking (here and in the front end) and after + // automatic classification by the bayesian filter (see code for local + // mail folders and for imap mail folders). The server-specific + // markAsReadOnSpam pref only applies to the latter, the former is + // controlled by "mailnews.ui.junk.manualMarkAsJunkMarksRead". + // 2. Even though move/delete on manual mark may be + // turned off, we might still need to mark as read. + + rv = srcFolder->MarkMessagesRead(mJunkHdrs, msgsAreJunk); + NoteChange(0, 0, nsMsgViewNotificationCode::none); + NS_ASSERTION(NS_SUCCEEDED(rv), + "marking marked-as-junk messages as read failed"); + } + + if (moveMessages) { + // Check if one of the messages to be junked is actually selected. + // If more than one message being junked, one must be selected. + // If no tree selection at all, must be in stand-alone message window. + bool junkedMsgSelected = numJunkHdrs > 1 || !mTreeSelection; + for (nsMsgViewIndex junkIndex = 0; + !junkedMsgSelected && junkIndex < numJunkHdrs; junkIndex++) { + nsMsgViewIndex hdrIndex = FindHdr(mJunkHdrs[junkIndex]); + if (hdrIndex != nsMsgViewIndex_None) + mTreeSelection->IsSelected(hdrIndex, &junkedMsgSelected); + } + + // If a junked msg is selected, tell the FE to call + // SetNextMessageAfterDelete() because a delete is coming. + if (junkedMsgSelected) { + nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater( + do_QueryReferent(mCommandUpdater)); + if (commandUpdater) { + rv = commandUpdater->UpdateNextMessageAfterDelete(); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + if (targetFolder) { + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = copyService->CopyMessages(srcFolder, mJunkHdrs, targetFolder, true, + nullptr, msgWindow, true); + } else if (msgsAreJunk) { + if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) { + // Unfortunately the DeleteMessages in this case is interpreted by + // IMAP as a delete toggle. So what we have to do is to assemble a + // new delete array, keeping only those that are not deleted. + nsTArray<RefPtr<nsIMsgDBHdr>> hdrsToDelete; + for (nsIMsgDBHdr* msgHdr : mJunkHdrs) { + if (msgHdr) { + uint32_t flags; + msgHdr->GetFlags(&flags); + if (!(flags & nsMsgMessageFlags::IMAPDeleted)) { + hdrsToDelete.AppendElement(msgHdr); + } + } + } + + if (!hdrsToDelete.IsEmpty()) + rv = srcFolder->DeleteMessages(hdrsToDelete, msgWindow, false, false, + nullptr, true); + } else { + rv = srcFolder->DeleteMessages(mJunkHdrs, msgWindow, false, false, + nullptr, true); + } + } else if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) { + nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(srcFolder)); + nsTArray<nsMsgKey> imapUids(numJunkHdrs); + for (nsIMsgDBHdr* msgHdr : mJunkHdrs) { + nsMsgKey key; + msgHdr->GetMessageKey(&key); + imapUids.AppendElement(key); + } + + imapFolder->StoreImapFlags(kImapMsgDeletedFlag, false, imapUids, nullptr); + } + + NoteChange(0, 0, nsMsgViewNotificationCode::none); + + NS_ASSERTION(NS_SUCCEEDED(rv), + "move or deletion of message marked-as-junk/non junk failed"); + } + + return rv; +} + +nsresult nsMsgDBView::DetermineActionsForJunkChange( + bool msgsAreJunk, nsIMsgFolder* srcFolder, bool& moveMessages, + bool& changeReadState, nsIMsgFolder** targetFolder) { + // There are two possible actions which may be performed + // on messages marked as spam: marking as read and moving + // somewhere. When a message is marked as non junk, + // it may be moved to the inbox, and marked unread. + moveMessages = false; + changeReadState = false; + + // The 'somewhere', junkTargetFolder, can be a folder, + // but if it remains null we'll delete the messages. + *targetFolder = nullptr; + + uint32_t folderFlags; + srcFolder->GetFlags(&folderFlags); + + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = srcFolder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Handle the easy case of marking a junk message as good first. + // Set the move target folder to the inbox, if any. + if (!msgsAreJunk) { + if (folderFlags & nsMsgFolderFlags::Junk) { + prefBranch->GetBoolPref("mail.spam.markAsNotJunkMarksUnRead", + &changeReadState); + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + rv = server->GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + rootMsgFolder->GetFolderWithFlags(nsMsgFolderFlags::Inbox, targetFolder); + moveMessages = *targetFolder != nullptr; + } + + return NS_OK; + } + + nsCOMPtr<nsISpamSettings> spamSettings; + rv = server->GetSpamSettings(getter_AddRefs(spamSettings)); + NS_ENSURE_SUCCESS(rv, rv); + + // When the user explicitly marks a message as junk, we can mark it as read, + // too. This is independent of the "markAsReadOnSpam" pref, which applies + // only to automatically-classified messages. + // Note that this behaviour should match the one in the front end for marking + // as junk via toolbar/context menu. + prefBranch->GetBoolPref("mailnews.ui.junk.manualMarkAsJunkMarksRead", + &changeReadState); + + // Now let's determine whether we'll be taking the second action, + // the move / deletion (and also determine which of these two). + bool manualMark; + (void)spamSettings->GetManualMark(&manualMark); + if (!manualMark) return NS_OK; + + int32_t manualMarkMode; + (void)spamSettings->GetManualMarkMode(&manualMarkMode); + NS_ASSERTION(manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_MOVE || + manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_DELETE, + "bad manual mark mode"); + + if (manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_MOVE) { + // If this is a junk folder (not only "the" junk folder for this account) + // don't do the move. + if (folderFlags & nsMsgFolderFlags::Junk) return NS_OK; + + nsCString spamFolderURI; + rv = spamSettings->GetSpamFolderURI(spamFolderURI); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(!spamFolderURI.IsEmpty(), + "spam folder URI is empty, can't move"); + if (!spamFolderURI.IsEmpty()) { + rv = FindFolder(spamFolderURI, targetFolder); + NS_ENSURE_SUCCESS(rv, rv); + if (*targetFolder) { + moveMessages = true; + } else { + // XXX TODO: GetOrCreateJunkFolder will only create a folder with + // localized name "Junk" regardless of spamFolderURI. So if someone + // sets the junk folder to an existing folder of a different name, + // then deletes that folder, this will fail to create the correct + // folder. + rv = GetOrCreateJunkFolder(spamFolderURI, nullptr /* aListener */); + if (NS_SUCCEEDED(rv)) + rv = GetExistingFolder(spamFolderURI, targetFolder); + + NS_ASSERTION(NS_SUCCEEDED(rv), "GetOrCreateJunkFolder failed"); + } + } + + return NS_OK; + } + + // At this point manualMarkMode == nsISpamSettings::MANUAL_MARK_MODE_DELETE). + + // If this is in the trash, let's not delete. + if (folderFlags & nsMsgFolderFlags::Trash) return NS_OK; + + return srcFolder->GetCanDeleteMessages(&moveMessages); +} + +// Reversing threads involves reversing the threads but leaving the +// expanded messages ordered relative to the thread, so we +// make a copy of each array and copy them over. +void nsMsgDBView::ReverseThreads() { + nsTArray<uint32_t> newFlagArray; + nsTArray<nsMsgKey> newKeyArray; + nsTArray<uint8_t> newLevelArray; + + uint32_t viewSize = GetSize(); + uint32_t startThread = viewSize; + uint32_t nextThread = viewSize; + uint32_t destIndex = 0; + + newKeyArray.SetLength(m_keys.Length()); + newFlagArray.SetLength(m_flags.Length()); + newLevelArray.SetLength(m_levels.Length()); + + while (startThread) { + startThread--; + + if (m_flags[startThread] & MSG_VIEW_FLAG_ISTHREAD) { + for (uint32_t sourceIndex = startThread; sourceIndex < nextThread; + sourceIndex++) { + newKeyArray[destIndex] = m_keys[sourceIndex]; + newFlagArray[destIndex] = m_flags[sourceIndex]; + newLevelArray[destIndex] = m_levels[sourceIndex]; + destIndex++; + } + // Because we're copying in reverse order. + nextThread = startThread; + } + } + + m_keys.SwapElements(newKeyArray); + m_flags.SwapElements(newFlagArray); + m_levels.SwapElements(newLevelArray); +} + +void nsMsgDBView::ReverseSort() { + uint32_t topIndex = GetSize(); + + nsCOMArray<nsIMsgFolder>* folders = GetFolders(); + + // Go up half the array swapping values. + for (uint32_t bottomIndex = 0; bottomIndex < --topIndex; bottomIndex++) { + // Swap flags. + uint32_t tempFlags = m_flags[bottomIndex]; + m_flags[bottomIndex] = m_flags[topIndex]; + m_flags[topIndex] = tempFlags; + + // Swap keys. + nsMsgKey tempKey = m_keys[bottomIndex]; + m_keys[bottomIndex] = m_keys[topIndex]; + m_keys[topIndex] = tempKey; + + if (folders) { + // Swap folders -- needed when search is done across multiple folders. + nsIMsgFolder* bottomFolder = folders->ObjectAt(bottomIndex); + nsIMsgFolder* topFolder = folders->ObjectAt(topIndex); + folders->ReplaceObjectAt(topFolder, bottomIndex); + folders->ReplaceObjectAt(bottomFolder, topIndex); + } + + // No need to swap elements in m_levels; since we only call + // ReverseSort in non-threaded mode, m_levels are all the same. + } +} + +int nsMsgDBView::FnSortIdKey(const IdKey* pItem1, const IdKey* pItem2, + viewSortInfo* sortInfo) { + int32_t retVal = 0; + + nsIMsgDatabase* db = sortInfo->db; + + mozilla::DebugOnly<nsresult> rv = + db->CompareCollationKeys(pItem1->key, pItem2->key, &retVal); + NS_ASSERTION(NS_SUCCEEDED(rv), "compare failed"); + + if (retVal) return sortInfo->ascendingSort ? retVal : -retVal; + + return sortInfo->view->SecondaryCompare(pItem1->id, pItem1->folder, + pItem2->id, pItem2->folder, sortInfo); +} + +int nsMsgDBView::FnSortIdUint32(const IdUint32* pItem1, const IdUint32* pItem2, + viewSortInfo* sortInfo) { + if (pItem1->dword > pItem2->dword) { + return (sortInfo->ascendingSort) ? 1 : -1; + } + + if (pItem1->dword < pItem2->dword) { + return (sortInfo->ascendingSort) ? -1 : 1; + } + + return sortInfo->view->SecondaryCompare(pItem1->id, pItem1->folder, + pItem2->id, pItem2->folder, sortInfo); +} + +// XXX are these still correct? +// To compensate for memory alignment required for systems such as HP-UX, these +// values must be 4 bytes aligned. Don't break this when modifying the +// constants. +const int kMaxSubjectKey = 160; +const int kMaxLocationKey = 160; // Also used for account. +const int kMaxAuthorKey = 160; +const int kMaxRecipientKey = 80; + +// There are cases when pFieldType is not set: +// one case returns NS_ERROR_UNEXPECTED; +// the other case now return NS_ERROR_NULL_POINTER (this is only when +// colHandler below is null, but is very unlikely). +// The latter case used to return NS_OK, which was incorrect. +nsresult nsMsgDBView::GetFieldTypeAndLenForSort( + nsMsgViewSortTypeValue sortType, uint16_t* pMaxLen, eFieldType* pFieldType, + nsIMsgCustomColumnHandler* colHandler) { + NS_ENSURE_ARG_POINTER(pMaxLen); + NS_ENSURE_ARG_POINTER(pFieldType); + + switch (sortType) { + case nsMsgViewSortType::bySubject: + *pFieldType = kCollationKey; + *pMaxLen = kMaxSubjectKey; + break; + case nsMsgViewSortType::byAccount: + case nsMsgViewSortType::byTags: + case nsMsgViewSortType::byLocation: + *pFieldType = kCollationKey; + *pMaxLen = kMaxLocationKey; + break; + case nsMsgViewSortType::byRecipient: + case nsMsgViewSortType::byCorrespondent: + *pFieldType = kCollationKey; + *pMaxLen = kMaxRecipientKey; + break; + case nsMsgViewSortType::byAuthor: + *pFieldType = kCollationKey; + *pMaxLen = kMaxAuthorKey; + break; + case nsMsgViewSortType::byDate: + case nsMsgViewSortType::byReceived: + case nsMsgViewSortType::byPriority: + case nsMsgViewSortType::byThread: + case nsMsgViewSortType::byId: + case nsMsgViewSortType::bySize: + case nsMsgViewSortType::byFlagged: + case nsMsgViewSortType::byUnread: + case nsMsgViewSortType::byStatus: + case nsMsgViewSortType::byJunkStatus: + case nsMsgViewSortType::byAttachments: + *pFieldType = kU32; + *pMaxLen = 0; + break; + case nsMsgViewSortType::byCustom: { + if (colHandler == nullptr) { + NS_WARNING("colHandler is null. *pFieldType is not set."); + return NS_ERROR_NULL_POINTER; + } + + bool isString; + colHandler->IsString(&isString); + + if (isString) { + *pFieldType = kCollationKey; + // 80 - do we need a separate k? + *pMaxLen = kMaxRecipientKey; + } else { + *pFieldType = kU32; + *pMaxLen = 0; + } + break; + } + case nsMsgViewSortType::byNone: + // Bug 901948. + return NS_ERROR_INVALID_ARG; + default: { + nsAutoCString message("unexpected switch value: sortType="); + message.AppendInt(sortType); + NS_WARNING(message.get()); + return NS_ERROR_UNEXPECTED; + } + } + + return NS_OK; +} + +#define MSG_STATUS_MASK \ + (nsMsgMessageFlags::Replied | nsMsgMessageFlags::Forwarded) + +nsresult nsMsgDBView::GetStatusSortValue(nsIMsgDBHdr* msgHdr, + uint32_t* result) { + NS_ENSURE_ARG_POINTER(msgHdr); + NS_ENSURE_ARG_POINTER(result); + + uint32_t messageFlags; + nsresult rv = msgHdr->GetFlags(&messageFlags); + NS_ENSURE_SUCCESS(rv, rv); + + if (messageFlags & nsMsgMessageFlags::New) { + // Happily, new by definition stands alone. + *result = 0; + return NS_OK; + } + + switch (messageFlags & MSG_STATUS_MASK) { + case nsMsgMessageFlags::Replied: + *result = 2; + break; + case nsMsgMessageFlags::Forwarded | nsMsgMessageFlags::Replied: + *result = 1; + break; + case nsMsgMessageFlags::Forwarded: + *result = 3; + break; + default: + *result = (messageFlags & nsMsgMessageFlags::Read) ? 4 : 5; + break; + } + + return NS_OK; +} + +nsresult nsMsgDBView::GetLongField(nsIMsgDBHdr* msgHdr, + nsMsgViewSortTypeValue sortType, + uint32_t* result, + nsIMsgCustomColumnHandler* colHandler) { + nsresult rv; + NS_ENSURE_ARG_POINTER(msgHdr); + NS_ENSURE_ARG_POINTER(result); + + bool isRead; + uint32_t bits; + + switch (sortType) { + case nsMsgViewSortType::bySize: + rv = (mShowSizeInLines) ? msgHdr->GetLineCount(result) + : msgHdr->GetMessageSize(result); + break; + case nsMsgViewSortType::byPriority: + nsMsgPriorityValue priority; + rv = msgHdr->GetPriority(&priority); + // Treat "none" as "normal" when sorting. + if (priority == nsMsgPriority::none) priority = nsMsgPriority::normal; + + // We want highest priority to have lowest value + // so ascending sort will have highest priority first. + *result = nsMsgPriority::highest - priority; + break; + case nsMsgViewSortType::byStatus: + rv = GetStatusSortValue(msgHdr, result); + break; + case nsMsgViewSortType::byFlagged: + bits = 0; + rv = msgHdr->GetFlags(&bits); + // Make flagged come out on top. + *result = !(bits & nsMsgMessageFlags::Marked); + break; + case nsMsgViewSortType::byUnread: + rv = msgHdr->GetIsRead(&isRead); + if (NS_SUCCEEDED(rv)) *result = !isRead; + + break; + case nsMsgViewSortType::byJunkStatus: { + nsCString junkScoreStr; + rv = msgHdr->GetStringProperty("junkscore", junkScoreStr); + // Unscored messages should come before messages that are scored + // junkScoreStr is "", and "0" - "100"; normalize to 0 - 101. + *result = junkScoreStr.IsEmpty() ? (0) : atoi(junkScoreStr.get()) + 1; + break; + } + case nsMsgViewSortType::byAttachments: + bits = 0; + rv = msgHdr->GetFlags(&bits); + *result = !(bits & nsMsgMessageFlags::Attachment); + break; + case nsMsgViewSortType::byDate: + // When sorting threads by date, we may want the date of the newest msg + // in the thread. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) && + !mSortThreadsByRoot) { + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + if (NS_SUCCEEDED(rv)) { + thread->GetNewestMsgDate(result); + break; + } + } + rv = msgHdr->GetDateInSeconds(result); + break; + case nsMsgViewSortType::byReceived: + // When sorting threads by received date, we may want the received date + // of the newest msg in the thread. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) && + !mSortThreadsByRoot) { + nsCOMPtr<nsIMsgThread> thread; + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + thread->GetNewestMsgDate(result); + } else { + // Already in seconds. + rv = msgHdr->GetUint32Property("dateReceived", result); + if (*result == 0) + // Use Date instead, we have no Received property + rv = msgHdr->GetDateInSeconds(result); + } + break; + case nsMsgViewSortType::byCustom: + if (colHandler != nullptr) { + colHandler->GetSortLongForRow(msgHdr, result); + rv = NS_OK; + } else { + NS_ASSERTION(false, + "should not be here (Sort Type: byCustom (Long), but no " + "custom handler)"); + rv = NS_ERROR_UNEXPECTED; + } + break; + case nsMsgViewSortType::byNone: + // Bug 901948. + return NS_ERROR_INVALID_ARG; + + case nsMsgViewSortType::byId: + // Handled by caller, since caller knows the key. + default: + NS_ERROR("should not be here"); + rv = NS_ERROR_UNEXPECTED; + break; + } + + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +MsgViewSortColumnInfo::MsgViewSortColumnInfo( + const MsgViewSortColumnInfo& other) { + mSortType = other.mSortType; + mSortOrder = other.mSortOrder; + mCustomColumnName = other.mCustomColumnName; + mColHandler = other.mColHandler; +} + +bool MsgViewSortColumnInfo::operator==( + const MsgViewSortColumnInfo& other) const { + return (mSortType == nsMsgViewSortType::byCustom) + ? mCustomColumnName.Equals(other.mCustomColumnName) + : mSortType == other.mSortType; +} + +nsresult nsMsgDBView::EncodeColumnSort(nsString& columnSortString) { + for (uint32_t i = 0; i < m_sortColumns.Length(); i++) { + MsgViewSortColumnInfo& sortInfo = m_sortColumns[i]; + columnSortString.Append((char)sortInfo.mSortType); + columnSortString.Append((char)sortInfo.mSortOrder + '0'); + if (sortInfo.mSortType == nsMsgViewSortType::byCustom) { + columnSortString.Append(sortInfo.mCustomColumnName); + columnSortString.Append((char16_t)'\r'); + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::DecodeColumnSort(nsString& columnSortString) { + const char16_t* stringPtr = columnSortString.BeginReading(); + while (*stringPtr) { + MsgViewSortColumnInfo sortColumnInfo; + sortColumnInfo.mSortType = (nsMsgViewSortTypeValue)*stringPtr++; + sortColumnInfo.mSortOrder = (nsMsgViewSortOrderValue)(*stringPtr++) - '0'; + if (sortColumnInfo.mSortType == nsMsgViewSortType::byCustom) { + while (*stringPtr && *stringPtr != '\r') + sortColumnInfo.mCustomColumnName.Append(*stringPtr++); + + sortColumnInfo.mColHandler = + GetColumnHandler(sortColumnInfo.mCustomColumnName); + + // Advance past '\r'. + if (*stringPtr) stringPtr++; + } + + m_sortColumns.AppendElement(sortColumnInfo); + } + + return NS_OK; +} + +// Secondary Sort Key: when you select a column to sort, that +// becomes the new Primary sort key, and all previous sort keys +// become secondary. For example, if you first click on Date, +// the messages are sorted by Date; then click on From, and now the +// messages are sorted by From, and for each value of From the +// messages are in Date order. + +void nsMsgDBView::PushSort(const MsgViewSortColumnInfo& newSort) { + // Handle byNone (bug 901948) ala a mail/base/modules/DBViewerWrapper.jsm + // where we don't push the secondary sort type if it's ::byNone; + // (and secondary sort type is NOT the same as the first sort type + // there). This code should behave the same way. + + // We don't expect to be passed sort type ::byNone, + // but if we are it's safe to ignore it. + if (newSort.mSortType == nsMsgViewSortType::byNone) return; + + // byId is a unique key (misnamed as Order Received). If we are sorting byId, + // we don't need to keep any secondary sort keys. + if (newSort.mSortType == nsMsgViewSortType::byId) m_sortColumns.Clear(); + + m_sortColumns.RemoveElement(newSort); + m_sortColumns.InsertElementAt(0, newSort); + if (m_sortColumns.Length() > kMaxNumSortColumns) + m_sortColumns.RemoveElementAt(kMaxNumSortColumns); +} + +nsresult nsMsgDBView::GetCollationKey(nsIMsgDBHdr* msgHdr, + nsMsgViewSortTypeValue sortType, + nsTArray<uint8_t>& result, + nsIMsgCustomColumnHandler* colHandler) { + nsresult rv = NS_ERROR_UNEXPECTED; + NS_ENSURE_ARG_POINTER(msgHdr); + + switch (sortType) { + case nsMsgViewSortType::bySubject: + rv = msgHdr->GetSubjectCollationKey(result); + break; + case nsMsgViewSortType::byLocation: + rv = GetLocationCollationKey(msgHdr, result); + break; + case nsMsgViewSortType::byRecipient: { + nsString recipients; + rv = FetchRecipients(msgHdr, recipients); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = dbToUse->CreateCollationKey(recipients, result); + } + break; + } + case nsMsgViewSortType::byAuthor: { + rv = msgHdr->GetAuthorCollationKey(result); + nsString author; + rv = FetchAuthor(msgHdr, author); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = dbToUse->CreateCollationKey(author, result); + } + break; + } + case nsMsgViewSortType::byAccount: + case nsMsgViewSortType::byTags: { + nsString str; + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + + if (!dbToUse) + // Probably a search view. + GetDBForViewIndex(0, getter_AddRefs(dbToUse)); + + rv = (sortType == nsMsgViewSortType::byAccount) + ? FetchAccount(msgHdr, str) + : FetchTags(msgHdr, str); + if (NS_SUCCEEDED(rv) && dbToUse) + rv = dbToUse->CreateCollationKey(str, result); + + break; + } + case nsMsgViewSortType::byCustom: + if (colHandler != nullptr) { + nsAutoString strKey; + rv = colHandler->GetSortStringForRow(msgHdr, strKey); + NS_ASSERTION(NS_SUCCEEDED(rv), + "failed to get sort string for custom row"); + nsAutoString strTemp(strKey); + + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = dbToUse->CreateCollationKey(strKey, result); + } else { + NS_ERROR( + "should not be here (Sort Type: byCustom (String), but no custom " + "handler)"); + rv = NS_ERROR_UNEXPECTED; + } + break; + case nsMsgViewSortType::byCorrespondent: { + nsString value; + if (IsOutgoingMsg(msgHdr)) + rv = FetchRecipients(msgHdr, value); + else + rv = FetchAuthor(msgHdr, value); + + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + rv = GetDBForHeader(msgHdr, getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = dbToUse->CreateCollationKey(value, result); + } + break; + } + default: + rv = NS_ERROR_UNEXPECTED; + break; + } + + // Bailing out with failure will stop the sort and leave us in + // a bad state. Try to continue on, instead. + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to get the collation key"); + if (NS_FAILED(rv)) { + result.Clear(); + } + + return NS_OK; +} + +// As the location collation key is created getting folder from the msgHdr, +// it is defined in this file and not from the db. +nsresult nsMsgDBView::GetLocationCollationKey(nsIMsgDBHdr* msgHdr, + nsTArray<uint8_t>& result) { + nsCOMPtr<nsIMsgFolder> folder; + + nsresult rv = msgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgDatabase> dbToUse; + rv = folder->GetMsgDatabase(getter_AddRefs(dbToUse)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString locationString; + rv = folder->GetPrettyName(locationString); + NS_ENSURE_SUCCESS(rv, rv); + + return dbToUse->CreateCollationKey(locationString, result); +} + +nsresult nsMsgDBView::SaveSortInfo(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) { + if (m_viewFolder) { + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && folderInfo) { + // Save off sort type and order, view type and flags. + folderInfo->SetSortType(sortType); + folderInfo->SetSortOrder(sortOrder); + + nsString sortColumnsString; + rv = EncodeColumnSort(sortColumnsString); + NS_ENSURE_SUCCESS(rv, rv); + folderInfo->SetProperty("sortColumns", sortColumnsString); + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::RestoreSortInfo() { + if (!m_viewFolder) return NS_OK; + + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && folderInfo) { + // Restore m_sortColumns from db. + nsString sortColumnsString; + folderInfo->GetProperty("sortColumns", sortColumnsString); + DecodeColumnSort(sortColumnsString); + if (m_sortColumns.Length() > 1) { + m_secondarySort = m_sortColumns[1].mSortType; + m_secondarySortOrder = m_sortColumns[1].mSortOrder; + m_secondaryCustomColumn = m_sortColumns[1].mCustomColumnName; + } + + // Restore curCustomColumn from db. + folderInfo->GetProperty("customSortCol", m_curCustomColumn); + } + + return NS_OK; +} + +// Called by msgDBView::Sort, at which point any persisted active custom +// columns must be registered. If not, reset their m_sortColumns entries +// to byDate; Sort will fill in values if necessary based on new user sort. +void nsMsgDBView::EnsureCustomColumnsValid() { + if (!m_sortColumns.Length()) return; + + for (uint32_t i = 0; i < m_sortColumns.Length(); i++) { + if (m_sortColumns[i].mSortType == nsMsgViewSortType::byCustom && + m_sortColumns[i].mColHandler == nullptr) { + m_sortColumns[i].mSortType = nsMsgViewSortType::byDate; + m_sortColumns[i].mCustomColumnName.Truncate(); + // There are only two... + if (i == 0 && m_sortType != nsMsgViewSortType::byCustom) + SetCurCustomColumn(EmptyString()); + if (i == 1) m_secondaryCustomColumn.Truncate(); + } + } +} + +int32_t nsMsgDBView::SecondaryCompare(nsMsgKey key1, nsIMsgFolder* folder1, + nsMsgKey key2, nsIMsgFolder* folder2, + viewSortInfo* comparisonContext) { + nsMsgViewSortTypeValue sortType = comparisonContext->view->m_secondarySort; + bool isAscendingSort = comparisonContext->view->m_secondarySortOrder == + nsMsgViewSortOrder::ascending; + + // We need to make sure that in the case of the secondary sort field also + // matching, we don't recurse. + if (comparisonContext->isSecondarySort || + sortType == nsMsgViewSortType::byId) { + if (key1 > key2) { + return isAscendingSort ? 1 : -1; + } + + if (key1 < key2) { + return isAscendingSort ? -1 : 1; + } + + return 0; + } + + nsCOMPtr<nsIMsgDBHdr> hdr1, hdr2; + nsresult rv = folder1->GetMessageHeader(key1, getter_AddRefs(hdr1)); + NS_ENSURE_SUCCESS(rv, 0); + rv = folder2->GetMessageHeader(key2, getter_AddRefs(hdr2)); + NS_ENSURE_SUCCESS(rv, 0); + IdKey EntryInfo1, EntryInfo2; + + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the *secondary* sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = nullptr; + if (sortType == nsMsgViewSortType::byCustom && + comparisonContext->view->m_sortColumns.Length() > 1) { + colHandler = comparisonContext->view->m_sortColumns[1].mColHandler; + } + + // The following may leave fieldType undefined. + // In this case, we can return 0 right away since + // it is the value returned in the default case of + // switch (fieldType) statement below. + rv = GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler); + NS_ENSURE_SUCCESS(rv, 0); + + hdr1->GetMessageKey(&EntryInfo1.id); + hdr2->GetMessageKey(&EntryInfo2.id); + + // Set up new viewSortInfo data for our secondary comparison. + viewSortInfo ctx = { + .view = comparisonContext->view, + .db = comparisonContext->db, + .isSecondarySort = true, // To avoid recursing back here! + .ascendingSort = isAscendingSort, + }; + + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(hdr1, sortType, EntryInfo1.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + rv = GetCollationKey(hdr2, sortType, EntryInfo2.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + + return FnSortIdKey(&EntryInfo1, &EntryInfo2, &ctx); + case kU32: + if (sortType == nsMsgViewSortType::byId) { + EntryInfo1.dword = EntryInfo1.id; + EntryInfo2.dword = EntryInfo2.id; + } else { + GetLongField(hdr1, sortType, &EntryInfo1.dword, colHandler); + GetLongField(hdr2, sortType, &EntryInfo2.dword, colHandler); + } + return FnSortIdUint32(&EntryInfo1, &EntryInfo2, &ctx); + default: + return 0; + } +} + +NS_IMETHODIMP nsMsgDBView::Sort(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) { + EnsureCustomColumnsValid(); + + // If we're doing a stable sort, we can't just reverse the messages. + // Check also that the custom column we're sorting on hasn't changed. + // Otherwise, to be on the safe side, resort. + // Note: m_curCustomColumn is the desired (possibly new) custom column name, + // while m_sortColumns[0].mCustomColumnName is the name for the last completed + // sort, since these are persisted after each sort. + if (m_sortType == sortType && m_sortValid && + (sortType != nsMsgViewSortType::byCustom || + (sortType == nsMsgViewSortType::byCustom && m_sortColumns.Length() && + m_sortColumns[0].mCustomColumnName.Equals(m_curCustomColumn))) && + m_sortColumns.Length() < 2) { + // Same as it ever was. Do nothing. + if (m_sortOrder == sortOrder) return NS_OK; + + // For secondary sort, remember the sort order on a per column basis. + if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = sortOrder; + + SaveSortInfo(sortType, sortOrder); + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + ReverseThreads(); + } else { + ReverseSort(); + } + + m_sortOrder = sortOrder; + // We just reversed the sort order, we still need to invalidate the view. + return NS_OK; + } + + if (sortType == nsMsgViewSortType::byThread) return NS_OK; + + // If a sortType has changed, or the sortType is byCustom and a column has + // changed, this is the new primary sortColumnInfo. + // Note: m_curCustomColumn is the desired (possibly new) custom column name, + // while m_sortColumns[0].mCustomColumnName is the name for the last completed + // sort, since these are persisted after each sort. + if (m_sortType != sortType || + (sortType == nsMsgViewSortType::byCustom && m_sortColumns.Length() && + !m_sortColumns[0].mCustomColumnName.Equals(m_curCustomColumn))) { + // For secondary sort, remember the sort order of the original primary sort! + if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = m_sortOrder; + + MsgViewSortColumnInfo sortColumnInfo; + sortColumnInfo.mSortType = sortType; + sortColumnInfo.mSortOrder = sortOrder; + if (sortType == nsMsgViewSortType::byCustom) { + GetCurCustomColumn(sortColumnInfo.mCustomColumnName); + sortColumnInfo.mColHandler = GetCurColumnHandler(); + } + + PushSort(sortColumnInfo); + } else { + // For primary sort, remember the sort order on a per column basis. + if (m_sortColumns.Length()) m_sortColumns[0].mSortOrder = sortOrder; + } + + if (m_sortColumns.Length() > 1) { + m_secondarySort = m_sortColumns[1].mSortType; + m_secondarySortOrder = m_sortColumns[1].mSortOrder; + m_secondaryCustomColumn = m_sortColumns[1].mCustomColumnName; + } + + SaveSortInfo(sortType, sortOrder); + // Figure out how much memory we'll need, and then malloc it. + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // If we did not obtain proper fieldType, it needs to be checked + // because the subsequent code does not handle it very well. + nsresult rv = + GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler); + + // Don't sort if the field type is not supported: Bug 901948. + if (NS_FAILED(rv)) return NS_OK; + + nsTArray<void*> ptrs; + uint32_t arraySize = GetSize(); + + if (!arraySize) return NS_OK; + + nsCOMArray<nsIMsgFolder>* folders = GetFolders(); + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + // Probably a search view. + if (!dbToUse) { + GetDBForViewIndex(0, getter_AddRefs(dbToUse)); + if (!dbToUse) return NS_ERROR_FAILURE; + } + + viewSortInfo qsPrivateData{ + .view = this, + .db = dbToUse, + .isSecondarySort = false, + .ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending), + }; + + switch (fieldType) { + case kCollationKey: { + // Sort on a non-numeric field. We'll be calculating a collation key for + // each message. + nsTArray<IdKey> entries; + entries.SetLength(arraySize); + nsTArray<IdKey*> pPtrBase; + pPtrBase.SetLength(arraySize); + for (uint32_t i = 0; i < arraySize; ++i) { + IdKey* info = &entries[i]; + pPtrBase[i] = info; + info->id = m_keys[i]; + info->bits = m_flags[i]; + info->dword = 0; + info->folder = folders ? folders->ObjectAt(i) : m_folder.get(); + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "header not found"); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetCollationKey(msgHdr, sortType, info->key, colHandler); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Perform the sort. + std::sort(pPtrBase.begin(), pPtrBase.end(), + [&qsPrivateData](const auto& lhs, const auto& rhs) { + return FnSortIdKey(lhs, rhs, &qsPrivateData) < 0; + }); + + // Now update the view state to reflect the new order. + for (uint32_t i = 0; i < arraySize; ++i) { + m_keys[i] = pPtrBase[i]->id; + m_flags[i] = pPtrBase[i]->bits; + if (folders) folders->ReplaceObjectAt(pPtrBase[i]->folder, i); + } + m_sortType = sortType; + m_sortOrder = sortOrder; + m_sortValid = true; + return NS_OK; + } + case kU32: { + // Sort on a numeric field. + nsTArray<IdUint32> entries; + entries.SetLength(arraySize); + nsTArray<IdUint32*> pPtrBase; + pPtrBase.SetLength(arraySize); + for (uint32_t i = 0; i < arraySize; ++i) { + IdUint32* info = &entries[i]; + pPtrBase[i] = info; + info->id = m_keys[i]; + info->bits = m_flags[i]; + info->folder = folders ? folders->ObjectAt(i) : m_folder.get(); + if (sortType == nsMsgViewSortType::byId) { + info->dword = info->id; // No msgHdr required. + } else { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + NS_ASSERTION(NS_SUCCEEDED(rv) && msgHdr, "header not found"); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetLongField(msgHdr, sortType, &info->dword, colHandler); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Perform the sort. + std::sort(pPtrBase.begin(), pPtrBase.end(), + [&qsPrivateData](const auto& lhs, const auto& rhs) { + return FnSortIdUint32(lhs, rhs, &qsPrivateData) < 0; + }); + + // Now update the view state to reflect the new order. + for (uint32_t i = 0; i < arraySize; ++i) { + m_keys[i] = pPtrBase[i]->id; + m_flags[i] = pPtrBase[i]->bits; + if (folders) folders->ReplaceObjectAt(pPtrBase[i]->folder, i); + } + m_sortType = sortType; + m_sortOrder = sortOrder; + m_sortValid = true; + return NS_OK; + } + default: + // If we get this far, we've got a bad fieldType. + return NS_ERROR_UNEXPECTED; + } +} + +nsMsgViewIndex nsMsgDBView::GetIndexOfFirstDisplayedKeyInThread( + nsIMsgThread* threadHdr, bool allowDummy) { + nsMsgViewIndex retIndex = nsMsgViewIndex_None; + uint32_t childIndex = 0; + // We could speed up the unreadOnly view by starting our search with the first + // unread message in the thread. Sometimes, that will be wrong, however, so + // let's skip it until we're sure it's necessary. + // (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) + // ? threadHdr->GetFirstUnreadKey(m_db) : threadHdr->GetChildAt(0); + uint32_t numThreadChildren; + threadHdr->GetNumChildren(&numThreadChildren); + while (retIndex == nsMsgViewIndex_None && childIndex < numThreadChildren) { + nsCOMPtr<nsIMsgDBHdr> childHdr; + threadHdr->GetChildHdrAt(childIndex++, getter_AddRefs(childHdr)); + if (childHdr) retIndex = FindHdr(childHdr, 0, allowDummy); + } + + return retIndex; +} + +nsresult nsMsgDBView::GetFirstMessageHdrToDisplayInThread( + nsIMsgThread* threadHdr, nsIMsgDBHdr** result) { + nsresult rv; + + if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) + rv = threadHdr->GetFirstUnreadChild(result); + else + rv = threadHdr->GetChildHdrAt(0, result); + + return rv; +} + +// Find the view index of the thread containing the passed msgKey, if +// the thread is in the view. MsgIndex is passed in as a shortcut if +// it turns out the msgKey is the first message in the thread, +// then we can avoid looking for the msgKey. +nsMsgViewIndex nsMsgDBView::ThreadIndexOfMsg( + nsMsgKey msgKey, nsMsgViewIndex msgIndex /* = nsMsgViewIndex_None */, + int32_t* pThreadCount /* = NULL */, uint32_t* pFlags /* = NULL */) { + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) + return nsMsgViewIndex_None; + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = m_db->GetMsgHdrForKey(msgKey, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None); + return ThreadIndexOfMsgHdr(msgHdr, msgIndex, pThreadCount, pFlags); +} + +nsMsgViewIndex nsMsgDBView::GetThreadIndex(nsMsgViewIndex msgIndex) { + if (!IsValidIndex(msgIndex)) return nsMsgViewIndex_None; + + // Scan up looking for level 0 message. + while (m_levels[msgIndex] && msgIndex) --msgIndex; + + return msgIndex; +} + +nsMsgViewIndex nsMsgDBView::ThreadIndexOfMsgHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex msgIndex, + int32_t* pThreadCount, + uint32_t* pFlags) { + nsCOMPtr<nsIMsgThread> threadHdr; + nsresult rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None); + + nsMsgViewIndex retIndex = nsMsgViewIndex_None; + + if (threadHdr != nullptr) { + if (msgIndex == nsMsgViewIndex_None) msgIndex = FindHdr(msgHdr, 0, true); + + // Hdr is not in view, need to find by thread. + if (msgIndex == nsMsgViewIndex_None) { + msgIndex = GetIndexOfFirstDisplayedKeyInThread(threadHdr, true); + // nsMsgKey threadKey = (msgIndex == nsMsgViewIndex_None) ? nsMsgKey_None + // : + // GetAt(msgIndex); + if (pFlags) threadHdr->GetFlags(pFlags); + } + + nsMsgViewIndex startOfThread = msgIndex; + while ((int32_t)startOfThread >= 0 && m_levels[startOfThread] != 0) + startOfThread--; + + retIndex = startOfThread; + if (pThreadCount) { + int32_t numChildren = 0; + nsMsgViewIndex threadIndex = startOfThread; + do { + threadIndex++; + numChildren++; + } while (threadIndex < m_levels.Length() && m_levels[threadIndex] != 0); + + *pThreadCount = numChildren; + } + } + + return retIndex; +} + +nsMsgKey nsMsgDBView::GetKeyOfFirstMsgInThread(nsMsgKey key) { + // Just report no key for any failure. This can occur when a + // message is deleted from a threaded view. + nsCOMPtr<nsIMsgThread> pThread; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = m_db->GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + if (NS_FAILED(rv)) return nsMsgKey_None; + + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread)); + if (NS_FAILED(rv)) return nsMsgKey_None; + + nsMsgKey firstKeyInThread = nsMsgKey_None; + + if (!pThread) return firstKeyInThread; + + // ### dmb UnreadOnly - this is wrong. But didn't seem to matter in 4.x + pThread->GetChildKeyAt(0, &firstKeyInThread); + return firstKeyInThread; +} + +NS_IMETHODIMP +nsMsgDBView::GetKeyAt(nsMsgViewIndex index, nsMsgKey* result) { + NS_ENSURE_ARG(result); + *result = GetAt(index); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetFlagsAt(nsMsgViewIndex aIndex, uint32_t* aResult) { + NS_ENSURE_ARG(aResult); + if (!IsValidIndex(aIndex)) return NS_MSG_INVALID_DBVIEW_INDEX; + + *aResult = m_flags[aIndex]; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetMsgHdrAt(nsMsgViewIndex aIndex, nsIMsgDBHdr** aResult) { + NS_ENSURE_ARG(aResult); + if (!IsValidIndex(aIndex)) return NS_MSG_INVALID_DBVIEW_INDEX; + + return GetMsgHdrForViewIndex(aIndex, aResult); +} + +nsMsgViewIndex nsMsgDBView::FindHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startIndex, + bool allowDummy) { + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + nsMsgViewIndex viewIndex = m_keys.IndexOf(msgKey, startIndex); + if (viewIndex == nsMsgViewIndex_None) return viewIndex; + + // If we're supposed to allow dummies, and the previous index is a dummy that + // is not elided, then it must be the dummy corresponding to our node and + // we should return that instead. + if (allowDummy && viewIndex && + (m_flags[viewIndex - 1] & MSG_VIEW_FLAG_DUMMY) && + !(m_flags[viewIndex - 1] & nsMsgMessageFlags::Elided)) { + viewIndex--; + } else if (!allowDummy && m_flags[viewIndex] & MSG_VIEW_FLAG_DUMMY) { + // We're not allowing dummies, and we found a dummy, look again + // one past the dummy. + return m_keys.IndexOf(msgKey, viewIndex + 1); + } + + // Check that the message we found matches the message we were looking for. + if (viewIndex != nsMsgViewIndex_None) { + nsCOMPtr<nsIMsgDBHdr> foundMsgHdr; + nsresult rv = GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(foundMsgHdr)); + if (NS_FAILED(rv) || foundMsgHdr != msgHdr) { + viewIndex = nsMsgViewIndex_None; + } + } + + return viewIndex; +} + +nsMsgViewIndex nsMsgDBView::FindKey(nsMsgKey key, bool expand) { + nsMsgViewIndex retIndex = nsMsgViewIndex_None; + retIndex = (nsMsgViewIndex)(m_keys.IndexOf(key)); + // For dummy headers, try to expand if the caller says so. And if the thread + // is expanded, ignore the dummy header and return the real header index. + if (retIndex != nsMsgViewIndex_None && + m_flags[retIndex] & MSG_VIEW_FLAG_DUMMY && + !(m_flags[retIndex] & nsMsgMessageFlags::Elided)) { + return (nsMsgViewIndex)m_keys.IndexOf(key, retIndex + 1); + } + + if (key != nsMsgKey_None && + (retIndex == nsMsgViewIndex_None || + m_flags[retIndex] & MSG_VIEW_FLAG_DUMMY) && + expand && m_db) { + nsMsgKey threadKey = GetKeyOfFirstMsgInThread(key); + if (threadKey != nsMsgKey_None) { + nsMsgViewIndex threadIndex = FindKey(threadKey, false); + if (threadIndex != nsMsgViewIndex_None) { + uint32_t flags = m_flags[threadIndex]; + if ((flags & nsMsgMessageFlags::Elided && + NS_SUCCEEDED(ExpandByIndex(threadIndex, nullptr))) || + flags & MSG_VIEW_FLAG_DUMMY) { + retIndex = (nsMsgViewIndex)m_keys.IndexOf(key, threadIndex + 1); + } + } + } + } + + return retIndex; +} + +nsresult nsMsgDBView::GetThreadCount(nsMsgViewIndex index, + uint32_t* pThreadCount) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgThread> pThread; + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread)); + if (NS_SUCCEEDED(rv) && pThread != nullptr) + rv = pThread->GetNumChildren(pThreadCount); + + return rv; +} + +// This counts the number of messages in an expanded thread, given the +// index of the first message in the thread. +int32_t nsMsgDBView::CountExpandedThread(nsMsgViewIndex index) { + int32_t numInThread = 0; + nsMsgViewIndex startOfThread = index; + while ((int32_t)startOfThread >= 0 && m_levels[startOfThread] != 0) + startOfThread--; + + nsMsgViewIndex threadIndex = startOfThread; + do { + threadIndex++; + numInThread++; + } while (threadIndex < m_levels.Length() && m_levels[threadIndex] != 0); + + return numInThread; +} + +// Returns the number of lines that would be added (> 0) or removed (< 0) +// if we were to try to expand/collapse the passed index. +nsresult nsMsgDBView::ExpansionDelta(nsMsgViewIndex index, + int32_t* expansionDelta) { + uint32_t numChildren; + nsresult rv; + + *expansionDelta = 0; + if (index >= ((nsMsgViewIndex)m_keys.Length())) + return NS_MSG_MESSAGE_NOT_FOUND; + + char flags = m_flags[index]; + + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) return NS_OK; + + // The client can pass in the key of any message + // in a thread and get the expansion delta for the thread. + + if (flags & nsMsgMessageFlags::Elided) { + rv = GetThreadCount(index, &numChildren); + NS_ENSURE_SUCCESS(rv, rv); + *expansionDelta = numChildren - 1; + } else { + numChildren = CountExpandedThread(index); + *expansionDelta = -(int32_t)(numChildren - 1); + } + + return NS_OK; +} + +nsresult nsMsgDBView::ToggleExpansion(nsMsgViewIndex index, + uint32_t* numChanged) { + nsresult rv; + NS_ENSURE_ARG(numChanged); + *numChanged = 0; + nsMsgViewIndex threadIndex = GetThreadIndex(index); + if (threadIndex == nsMsgViewIndex_None) { + NS_ASSERTION(false, "couldn't find thread"); + return NS_MSG_MESSAGE_NOT_FOUND; + } + + int32_t flags = m_flags[threadIndex]; + + // If not a thread, or doesn't have children, no expand/collapse. + // If we add sub-thread expand collapse, this will need to be relaxed. + if (!(flags & MSG_VIEW_FLAG_ISTHREAD) || !(flags & MSG_VIEW_FLAG_HASCHILDREN)) + return NS_MSG_MESSAGE_NOT_FOUND; + + if (flags & nsMsgMessageFlags::Elided) + rv = ExpandByIndex(threadIndex, numChanged); + else + rv = CollapseByIndex(threadIndex, numChanged); + + // If we collaps/uncollapse a thread, this changes the selected URIs. + SelectionChangedXPCOM(); + return rv; +} + +nsresult nsMsgDBView::ExpandAndSelectThread() { + nsresult rv; + + NS_ASSERTION(mTreeSelection, "no tree selection"); + if (!mTreeSelection) return NS_ERROR_UNEXPECTED; + + int32_t index; + rv = mTreeSelection->GetCurrentIndex(&index); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ExpandAndSelectThreadByIndex(index, false); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +nsresult nsMsgDBView::ExpandAndSelectThreadByIndex(nsMsgViewIndex index, + bool augment) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsresult rv; + + nsMsgViewIndex threadIndex; + bool inThreadedMode = (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay); + + if (inThreadedMode) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + threadIndex = ThreadIndexOfMsgHdr(msgHdr, index); + if (threadIndex == nsMsgViewIndex_None) { + NS_ASSERTION(false, "couldn't find thread"); + return NS_MSG_MESSAGE_NOT_FOUND; + } + } else { + threadIndex = index; + } + + int32_t flags = m_flags[threadIndex]; + int32_t count = 0; + + if (inThreadedMode && flags & MSG_VIEW_FLAG_ISTHREAD && + flags & MSG_VIEW_FLAG_HASCHILDREN) { + // If closed, expand this thread. + if (flags & nsMsgMessageFlags::Elided) { + uint32_t numExpanded; + rv = ExpandByIndex(threadIndex, &numExpanded); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Get the number of messages in the expanded thread so we know how many + // to select. + count = CountExpandedThread(threadIndex); + } else { + count = 1; + } + + NS_ASSERTION(count > 0, "bad count"); + + // Update the selection. + + NS_ASSERTION(mTreeSelection, "no tree selection"); + if (!mTreeSelection) return NS_ERROR_UNEXPECTED; + + // The count should be 1 or greater. If there was only one message in the + // thread, we just select it. If more, we select all of them. + mTreeSelection->RangedSelect(threadIndex + count - 1, threadIndex, augment); + return NS_OK; +} + +nsresult nsMsgDBView::ExpandAll() { + if (mTree) mTree->BeginUpdateBatch(); + if (mJSTree) mJSTree->BeginUpdateBatch(); + + for (int32_t i = GetSize() - 1; i >= 0; i--) { + uint32_t numExpanded; + uint32_t flags = m_flags[i]; + if (flags & nsMsgMessageFlags::Elided) ExpandByIndex(i, &numExpanded); + } + + if (mTree) mTree->EndUpdateBatch(); + if (mJSTree) mJSTree->EndUpdateBatch(); + + SelectionChangedXPCOM(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr, + nsIMsgThread** pThread) { + NS_ENSURE_ARG_POINTER(msgHdr); + NS_ENSURE_ARG_POINTER(pThread); + if (!m_db) return NS_ERROR_FAILURE; + return m_db->GetThreadContainingMsgHdr(msgHdr, pThread); +} + +nsresult nsMsgDBView::ExpandByIndex(nsMsgViewIndex index, + uint32_t* pNumExpanded) { + if ((uint32_t)index >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND; + + uint32_t flags = m_flags[index]; + uint32_t numExpanded = 0; + + NS_ASSERTION(flags & nsMsgMessageFlags::Elided, + "can't expand an already expanded thread"); + flags &= ~nsMsgMessageFlags::Elided; + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgThread> pThread; + nsresult rv = GetThreadContainingIndex(index, getter_AddRefs(pThread)); + NS_ENSURE_SUCCESS(rv, rv); + if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) { + // Keep top level hdr in thread, even though read. + if (flags & nsMsgMessageFlags::Read) { + m_levels.AppendElement(0); + } + + rv = ListUnreadIdsInThread(pThread, index, &numExpanded); + } else { + rv = ListIdsInThread(pThread, index, &numExpanded); + } + + if (numExpanded > 0) { + m_flags[index] = flags; + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + } + + NoteChange(index + 1, numExpanded, nsMsgViewNotificationCode::insertOrDelete); + + if (pNumExpanded != nullptr) *pNumExpanded = numExpanded; + + return rv; +} + +nsresult nsMsgDBView::CollapseAll() { + if (mJSTree) mJSTree->BeginUpdateBatch(); + for (uint32_t i = 0; i < GetSize(); i++) { + uint32_t numExpanded; + uint32_t flags = m_flags[i]; + if (!(flags & nsMsgMessageFlags::Elided) && + (flags & MSG_VIEW_FLAG_HASCHILDREN)) + CollapseByIndex(i, &numExpanded); + } + + if (mJSTree) mJSTree->EndUpdateBatch(); + SelectionChangedXPCOM(); + return NS_OK; +} + +nsresult nsMsgDBView::CollapseByIndex(nsMsgViewIndex index, + uint32_t* pNumCollapsed) { + nsresult rv; + int32_t flags = m_flags[index]; + int32_t rowDelta = 0; + + if (flags & nsMsgMessageFlags::Elided || + !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) || + !(flags & MSG_VIEW_FLAG_HASCHILDREN)) { + return NS_OK; + } + + if (index > m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND; + + rv = ExpansionDelta(index, &rowDelta); + NS_ENSURE_SUCCESS(rv, rv); + + flags |= nsMsgMessageFlags::Elided; + + m_flags[index] = flags; + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + + // Don't count first header in thread. + int32_t numRemoved = -rowDelta; + if (index + 1 + numRemoved > m_keys.Length()) { + NS_ERROR("trying to remove too many rows"); + numRemoved -= (index + 1 + numRemoved) - m_keys.Length(); + if (numRemoved <= 0) return NS_MSG_MESSAGE_NOT_FOUND; + } + + // Start at first id after thread. + RemoveRows(index + 1, numRemoved); + if (pNumCollapsed != nullptr) *pNumCollapsed = numRemoved; + + NoteChange(index + 1, rowDelta, nsMsgViewNotificationCode::insertOrDelete); + + return rv; +} + +nsresult nsMsgDBView::OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey, + bool /*ensureListed*/) { + nsresult rv = NS_OK; + // Views can override this behaviour, which is to append to view. + // This is the mail behaviour, but threaded views will want + // to insert in order... + if (newHdr) rv = AddHdr(newHdr); + + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::GetThreadContainingIndex(nsMsgViewIndex index, + nsIMsgThread** resultThread) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + return GetThreadContainingMsgHdr(msgHdr, resultThread); +} + +nsMsgViewIndex nsMsgDBView::GetIndexForThread(nsIMsgDBHdr* msgHdr) { + // Take advantage of the fact that we're already sorted + // and find the insert index via a binary search, though expanded threads + // make that tricky. + + nsMsgViewIndex highIndex = m_keys.Length(); + nsMsgViewIndex lowIndex = 0; + IdKey EntryInfo1, EntryInfo2; + + nsresult rv; + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // The following may leave fieldType undefined. + // In this case, we can return highIndex right away since + // it is the value returned in the default case of + // switch (fieldType) statement below. + rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler); + NS_ENSURE_SUCCESS(rv, highIndex); + + int retStatus = 0; + msgHdr->GetMessageKey(&EntryInfo1.id); + msgHdr->GetFolder(&EntryInfo1.folder); + EntryInfo1.folder->Release(); + + viewSortInfo comparisonContext{ + .view = this, + .isSecondarySort = false, + .ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending), + }; + + nsCOMPtr<nsIMsgDatabase> hdrDB; + EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB)); + comparisonContext.db = hdrDB.get(); + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(msgHdr, m_sortType, EntryInfo1.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + break; + case kU32: + if (m_sortType == nsMsgViewSortType::byId) { + EntryInfo1.dword = EntryInfo1.id; + } else { + GetLongField(msgHdr, m_sortType, &EntryInfo1.dword, colHandler); + } + + break; + default: + return highIndex; + } + + while (highIndex > lowIndex) { + nsMsgViewIndex tryIndex = (lowIndex + highIndex) / 2; + // Need to adjust tryIndex if it's not a thread. + while (m_levels[tryIndex] && tryIndex) tryIndex--; + + if (tryIndex < lowIndex) { + NS_ERROR("try index shouldn't be less than low index"); + break; + } + + EntryInfo2.id = m_keys[tryIndex]; + GetFolderForViewIndex(tryIndex, &EntryInfo2.folder); + EntryInfo2.folder->Release(); + + nsCOMPtr<nsIMsgDBHdr> tryHdr; + nsCOMPtr<nsIMsgDatabase> db; + // ### this should get the db from the folder... + GetDBForViewIndex(tryIndex, getter_AddRefs(db)); + if (db) db->GetMsgHdrForKey(EntryInfo2.id, getter_AddRefs(tryHdr)); + + if (!tryHdr) break; + + if (tryHdr == msgHdr) { + NS_WARNING("didn't expect header to already be in view"); + highIndex = tryIndex; + break; + } + + if (fieldType == kCollationKey) { + rv = GetCollationKey(tryHdr, m_sortType, EntryInfo2.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + + retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext); + } else if (fieldType == kU32) { + if (m_sortType == nsMsgViewSortType::byId) { + EntryInfo2.dword = EntryInfo2.id; + } else { + GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler); + } + + retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext); + } + + if (retStatus == 0) { + highIndex = tryIndex; + break; + } + + if (retStatus < 0) { + highIndex = tryIndex; + // We already made sure tryIndex was at a thread at the top of the loop. + } else { + lowIndex = tryIndex + 1; + while (lowIndex < GetSize() && m_levels[lowIndex]) lowIndex++; + } + } + + return highIndex; +} + +nsMsgViewIndex nsMsgDBView::GetInsertIndexHelper( + nsIMsgDBHdr* msgHdr, nsTArray<nsMsgKey>& keys, + nsCOMArray<nsIMsgFolder>* folders, nsMsgViewSortOrderValue sortOrder, + nsMsgViewSortTypeValue sortType) { + nsMsgViewIndex highIndex = keys.Length(); + nsMsgViewIndex lowIndex = 0; + IdKey EntryInfo1, EntryInfo2; + + nsresult rv; + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // The following may leave fieldType undefined. + // In this case, we can return highIndex right away since + // it is the value returned in the default case of + // switch (fieldType) statement below. + rv = GetFieldTypeAndLenForSort(sortType, &maxLen, &fieldType, colHandler); + NS_ENSURE_SUCCESS(rv, highIndex); + + int retStatus = 0; + msgHdr->GetMessageKey(&EntryInfo1.id); + msgHdr->GetFolder(&EntryInfo1.folder); + EntryInfo1.folder->Release(); + + viewSortInfo comparisonContext{ + .view = this, + .isSecondarySort = false, + .ascendingSort = (sortOrder == nsMsgViewSortOrder::ascending), + }; + + rv = EntryInfo1.folder->GetMsgDatabase(&comparisonContext.db); + NS_ENSURE_SUCCESS(rv, highIndex); + comparisonContext.db->Release(); + + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(msgHdr, sortType, EntryInfo1.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + break; + case kU32: + if (sortType == nsMsgViewSortType::byId) { + EntryInfo1.dword = EntryInfo1.id; + } else { + GetLongField(msgHdr, sortType, &EntryInfo1.dword, colHandler); + } + + break; + default: + return highIndex; + } + + while (highIndex > lowIndex) { + nsMsgViewIndex tryIndex = (lowIndex + highIndex - 1) / 2; + EntryInfo2.id = keys[tryIndex]; + EntryInfo2.folder = folders ? folders->ObjectAt(tryIndex) : m_folder.get(); + + nsCOMPtr<nsIMsgDBHdr> tryHdr; + EntryInfo2.folder->GetMessageHeader(EntryInfo2.id, getter_AddRefs(tryHdr)); + if (!tryHdr) break; + + if (fieldType == kCollationKey) { + rv = GetCollationKey(tryHdr, sortType, EntryInfo2.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + + retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext); + } else if (fieldType == kU32) { + if (sortType == nsMsgViewSortType::byId) { + EntryInfo2.dword = EntryInfo2.id; + } else { + GetLongField(tryHdr, sortType, &EntryInfo2.dword, colHandler); + } + + retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext); + } + + if (retStatus == 0) { + highIndex = tryIndex; + break; + } + + if (retStatus < 0) { + highIndex = tryIndex; + } else { + lowIndex = tryIndex + 1; + } + } + + return highIndex; +} + +nsMsgViewIndex nsMsgDBView::GetInsertIndex(nsIMsgDBHdr* msgHdr) { + if (!GetSize()) return 0; + + if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) != 0 && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort) && + m_sortOrder != nsMsgViewSortType::byId) { + return GetIndexForThread(msgHdr); + } + + return GetInsertIndexHelper(msgHdr, m_keys, GetFolders(), m_sortOrder, + m_sortType); +} + +nsresult nsMsgDBView::AddHdr(nsIMsgDBHdr* msgHdr, nsMsgViewIndex* resultIndex) { + uint32_t flags = 0; +#ifdef DEBUG_bienvenu + NS_ASSERTION(m_keys.Length() == m_flags.Length() && + (int)m_keys.Length() == m_levels.Length(), + "view arrays out of sync!"); +#endif + + if (resultIndex) *resultIndex = nsMsgViewIndex_None; + + if (!GetShowingIgnored()) { + nsCOMPtr<nsIMsgThread> thread; + GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(thread)); + if (thread) { + thread->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Ignored) return NS_OK; + } + + bool ignored; + msgHdr->GetIsKilled(&ignored); + if (ignored) return NS_OK; + } + + nsMsgKey msgKey, threadId; + nsMsgKey threadParent; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetThreadId(&threadId); + msgHdr->GetThreadParent(&threadParent); + + msgHdr->GetFlags(&flags); + // XXX this isn't quite right, is it? + // Should be checking that our thread parent key is none? + if (threadParent == nsMsgKey_None) flags |= MSG_VIEW_FLAG_ISTHREAD; + + nsMsgViewIndex insertIndex = GetInsertIndex(msgHdr); + if (insertIndex == nsMsgViewIndex_None) { + // If unreadonly, level is 0 because we must be the only msg in the thread. + int32_t levelToAdd = 0; + + if (m_sortOrder == nsMsgViewSortOrder::ascending) { + InsertMsgHdrAt(GetSize(), msgHdr, msgKey, flags, levelToAdd); + if (resultIndex) *resultIndex = GetSize() - 1; + + // The call to NoteChange() has to happen after we add the key as + // NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + NoteChange(GetSize() - 1, 1, nsMsgViewNotificationCode::insertOrDelete); + } else { + InsertMsgHdrAt(0, msgHdr, msgKey, flags, levelToAdd); + if (resultIndex) *resultIndex = 0; + + // The call to NoteChange() has to happen after we insert the key as + // NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + NoteChange(0, 1, nsMsgViewNotificationCode::insertOrDelete); + } + + m_sortValid = false; + } else { + InsertMsgHdrAt(insertIndex, msgHdr, msgKey, flags, 0); + if (resultIndex) *resultIndex = insertIndex; + + // The call to NoteChange() has to happen after we add the key as + // NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete); + } + + OnHeaderAddedOrDeleted(); + return NS_OK; +} + +bool nsMsgDBView::WantsThisThread(nsIMsgThread* /*threadHdr*/) { + // Default is to want all threads. + return true; +} + +nsMsgViewIndex nsMsgDBView::FindParentInThread( + nsMsgKey parentKey, nsMsgViewIndex startOfThreadViewIndex) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + while (parentKey != nsMsgKey_None) { + nsMsgViewIndex parentIndex = + m_keys.IndexOf(parentKey, startOfThreadViewIndex); + if (parentIndex != nsMsgViewIndex_None) return parentIndex; + + if (NS_FAILED(m_db->GetMsgHdrForKey(parentKey, getter_AddRefs(msgHdr)))) + break; + + msgHdr->GetThreadParent(&parentKey); + } + + return startOfThreadViewIndex; +} + +nsresult nsMsgDBView::ListIdsInThreadOrder(nsIMsgThread* threadHdr, + nsMsgKey parentKey, uint32_t level, + nsMsgViewIndex* viewIndex, + uint32_t* pNumListed) { + nsCOMPtr<nsIMsgEnumerator> msgEnumerator; + nsresult rv = + threadHdr->EnumerateMessages(parentKey, getter_AddRefs(msgEnumerator)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numChildren; + (void)threadHdr->GetNumChildren(&numChildren); + NS_ASSERTION(numChildren, "Empty thread in view/db"); + // Bogus, but harmless. + if (!numChildren) return NS_OK; + + // Account for the existing thread root. + numChildren--; + + // Skip the first one. + bool hasMore; + while (NS_SUCCEEDED(msgEnumerator->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = msgEnumerator->GetNext(getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + if (*pNumListed == numChildren) { + MOZ_ASSERT_UNREACHABLE("thread corrupt in db"); + // If we've listed more messages than are in the thread, then the db + // is corrupt, and we should invalidate it. + // We'll use this rv to indicate there's something wrong with the db + // though for now it probably won't get paid attention to. + m_db->SetSummaryValid(false); + return NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } + + if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) { + bool ignored; + msgHdr->GetIsKilled(&ignored); + // We are not going to process subthreads, horribly invalidating the + // numChildren characteristic. + if (ignored) continue; + } + + nsMsgKey msgKey; + uint32_t msgFlags, newFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + AdjustReadFlag(msgHdr, &msgFlags); + SetMsgHdrAt(msgHdr, *viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, level); + // Turn off thread or elided bit if they got turned on (maybe from new + // only view?) + msgHdr->AndFlags(~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided), + &newFlags); + (*pNumListed)++; + (*viewIndex)++; + rv = ListIdsInThreadOrder(threadHdr, msgKey, level + 1, viewIndex, + pNumListed); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +void nsMsgDBView::InsertEmptyRows(nsMsgViewIndex viewIndex, int32_t numRows) { + m_keys.InsertElementsAt(viewIndex, numRows, 0); + m_flags.InsertElementsAt(viewIndex, numRows, 0); + m_levels.InsertElementsAt(viewIndex, numRows, 1); +} + +void nsMsgDBView::RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) { + m_keys.RemoveElementsAt(viewIndex, numRows); + m_flags.RemoveElementsAt(viewIndex, numRows); + m_levels.RemoveElementsAt(viewIndex, numRows); +} + +NS_IMETHODIMP +nsMsgDBView::InsertTreeRows(nsMsgViewIndex aIndex, uint32_t aNumRows, + nsMsgKey aKey, nsMsgViewFlagsTypeValue aFlags, + uint32_t aLevel, nsIMsgFolder* aFolder) { + if (GetSize() < aIndex) return NS_ERROR_UNEXPECTED; + + nsCOMArray<nsIMsgFolder>* folders = GetFolders(); + if (folders) { + // In a search/xfvf view only, a folder is required. + NS_ENSURE_ARG_POINTER(aFolder); + for (size_t i = 0; i < aNumRows; i++) + // Insert into m_folders. + if (!folders->InsertObjectAt(aFolder, aIndex + i)) + return NS_ERROR_UNEXPECTED; + } + + m_keys.InsertElementsAt(aIndex, aNumRows, aKey); + m_flags.InsertElementsAt(aIndex, aNumRows, aFlags); + m_levels.InsertElementsAt(aIndex, aNumRows, aLevel); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::RemoveTreeRows(nsMsgViewIndex aIndex, uint32_t aNumRows) { + // Prevent a crash if attempting to remove rows which don't exist. + if (GetSize() < aIndex + aNumRows) return NS_ERROR_UNEXPECTED; + + nsMsgDBView::RemoveRows(aIndex, aNumRows); + + nsCOMArray<nsIMsgFolder>* folders = GetFolders(); + if (folders) + // In a search/xfvf view only, remove from m_folders. + if (!folders->RemoveObjectsAt(aIndex, aNumRows)) return NS_ERROR_UNEXPECTED; + + return NS_OK; +} + +nsresult nsMsgDBView::ListIdsInThread(nsIMsgThread* threadHdr, + nsMsgViewIndex startOfThreadViewIndex, + uint32_t* pNumListed) { + NS_ENSURE_ARG(threadHdr); + // These children ids should be in thread order. + nsresult rv = NS_OK; + uint32_t i; + nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1; + *pNumListed = 0; + + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + NS_ASSERTION(numChildren, "Empty thread in view/db"); + if (!numChildren) return NS_OK; + + // Account for the existing thread root. + numChildren--; + InsertEmptyRows(viewIndex, numChildren); + + // ### need to rework this when we implemented threading in group views. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) { + nsMsgKey parentKey = m_keys[startOfThreadViewIndex]; + // If the thread is bigger than the hdr cache, expanding the thread + // can be slow. Increasing the hdr cache size will help a fair amount. + uint32_t hdrCacheSize; + m_db->GetMsgHdrCacheSize(&hdrCacheSize); + if (numChildren > hdrCacheSize) m_db->SetMsgHdrCacheSize(numChildren); + + // If this fails, *pNumListed will be 0, and we'll fall back to just + // enumerating the messages in the thread below. + rv = ListIdsInThreadOrder(threadHdr, parentKey, 1, &viewIndex, pNumListed); + if (numChildren > hdrCacheSize) m_db->SetMsgHdrCacheSize(hdrCacheSize); + } + + if (!*pNumListed) { + uint32_t ignoredHeaders = 0; + // If we're not threaded, just list em out in db order. + for (i = 1; i <= numChildren; i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + + if (msgHdr != nullptr) { + if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) { + bool killed; + msgHdr->GetIsKilled(&killed); + if (killed) { + ignoredHeaders++; + continue; + } + } + + nsMsgKey msgKey; + uint32_t msgFlags, newFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + AdjustReadFlag(msgHdr, &msgFlags); + SetMsgHdrAt(msgHdr, viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, 1); + // Here, we're either flat, or we're grouped - in either case, + // level is 1. Turn off thread or elided bit if they got turned on + // (maybe from new only view?). + if (i > 0) + msgHdr->AndFlags( + ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided), &newFlags); + + (*pNumListed)++; + viewIndex++; + } + } + + if (ignoredHeaders + *pNumListed < numChildren) { + MOZ_ASSERT_UNREACHABLE("thread corrupt in db"); + // If we've listed fewer messages than are in the thread, then the db + // is corrupt, and we should invalidate it. + // We'll use this rv to indicate there's something wrong with the db + // though for now it probably won't get paid attention to. + m_db->SetSummaryValid(false); + rv = NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } + } + + // We may have added too many elements (i.e., subthreads were cut). + // XXX Fix for cross folder view case. + if (*pNumListed < numChildren) + RemoveRows(viewIndex, numChildren - *pNumListed); + + return rv; +} + +int32_t nsMsgDBView::FindLevelInThread(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startOfThread, + nsMsgViewIndex viewIndex) { + nsCOMPtr<nsIMsgDBHdr> curMsgHdr = msgHdr; + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + + // Look through the ancestors of the passed in msgHdr in turn, looking for + // them in the view, up to the start of the thread. If we find an ancestor, + // then our level is one greater than the level of the ancestor. + while (curMsgHdr) { + nsMsgKey parentKey; + curMsgHdr->GetThreadParent(&parentKey); + if (parentKey == nsMsgKey_None) break; + + // Scan up to find view index of ancestor, if any. + for (nsMsgViewIndex indexToTry = viewIndex; + indexToTry && indexToTry-- >= startOfThread;) { + if (m_keys[indexToTry] == parentKey) return m_levels[indexToTry] + 1; + } + + // If msgHdr's key is its parentKey, we'll loop forever, so protect + // against that corruption. + if (msgKey == parentKey || NS_FAILED(m_db->GetMsgHdrForKey( + parentKey, getter_AddRefs(curMsgHdr)))) { + NS_ERROR( + "msgKey == parentKey, or GetMsgHdrForKey failed, this used to be an " + "infinite loop condition"); + curMsgHdr = nullptr; + } else { + // Need to update msgKey so the check for a msgHdr with matching + // key+parentKey will work after first time through loop. + curMsgHdr->GetMessageKey(&msgKey); + } + } + + return 1; +} + +// XXX Can this be combined with GetIndexForThread?? +nsMsgViewIndex nsMsgDBView::GetThreadRootIndex(nsIMsgDBHdr* msgHdr) { + if (!msgHdr) { + NS_WARNING("null msgHdr parameter"); + return nsMsgViewIndex_None; + } + + // Take advantage of the fact that we're already sorted + // and find the thread root via a binary search. + + nsMsgViewIndex highIndex = m_keys.Length(); + nsMsgViewIndex lowIndex = 0; + IdKey EntryInfo1, EntryInfo2; + + nsresult rv; + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // The following may leave fieldType undefined. + // In this case, we can return highIndex right away since + // it is the value returned in the default case of + // switch (fieldType) statement below. + rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler); + NS_ENSURE_SUCCESS(rv, highIndex); + + int retStatus = 0; + msgHdr->GetMessageKey(&EntryInfo1.id); + msgHdr->GetFolder(&EntryInfo1.folder); + EntryInfo1.folder->Release(); + + viewSortInfo comparisonContext{ + .view = this, + .isSecondarySort = false, + .ascendingSort = (m_sortOrder == nsMsgViewSortOrder::ascending), + }; + + nsCOMPtr<nsIMsgDatabase> hdrDB; + EntryInfo1.folder->GetMsgDatabase(getter_AddRefs(hdrDB)); + comparisonContext.db = hdrDB.get(); + + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(msgHdr, m_sortType, EntryInfo1.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + break; + case kU32: + if (m_sortType == nsMsgViewSortType::byId) { + EntryInfo1.dword = EntryInfo1.id; + } else { + GetLongField(msgHdr, m_sortType, &EntryInfo1.dword, colHandler); + } + + break; + default: + return highIndex; + } + + while (highIndex > lowIndex) { + nsMsgViewIndex tryIndex = (lowIndex + highIndex) / 2; + // Need to adjust tryIndex if it's not a thread. + while (m_levels[tryIndex] && tryIndex) tryIndex--; + + if (tryIndex < lowIndex) { + NS_ERROR("try index shouldn't be less than low index"); + break; + } + + EntryInfo2.id = m_keys[tryIndex]; + GetFolderForViewIndex(tryIndex, &EntryInfo2.folder); + EntryInfo2.folder->Release(); + + nsCOMPtr<nsIMsgDBHdr> tryHdr; + nsCOMPtr<nsIMsgDatabase> db; + // ### this should get the db from the folder... + GetDBForViewIndex(tryIndex, getter_AddRefs(db)); + if (db) db->GetMsgHdrForKey(EntryInfo2.id, getter_AddRefs(tryHdr)); + + if (!tryHdr) break; + + if (tryHdr == msgHdr) { + highIndex = tryIndex; + break; + } + + if (fieldType == kCollationKey) { + rv = GetCollationKey(tryHdr, m_sortType, EntryInfo2.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + retStatus = FnSortIdKey(&EntryInfo1, &EntryInfo2, &comparisonContext); + } else if (fieldType == kU32) { + if (m_sortType == nsMsgViewSortType::byId) { + EntryInfo2.dword = EntryInfo2.id; + } else { + GetLongField(tryHdr, m_sortType, &EntryInfo2.dword, colHandler); + } + + retStatus = FnSortIdUint32(&EntryInfo1, &EntryInfo2, &comparisonContext); + } + + if (retStatus == 0) { + highIndex = tryIndex; + break; + } + + if (retStatus < 0) { + highIndex = tryIndex; + // We already made sure tryIndex was at a thread at the top of the loop. + } else { + lowIndex = tryIndex + 1; + while (lowIndex < GetSize() && m_levels[lowIndex]) lowIndex++; + } + } + + nsCOMPtr<nsIMsgDBHdr> resultHdr; + GetMsgHdrForViewIndex(highIndex, getter_AddRefs(resultHdr)); + + if (resultHdr != msgHdr) { + NS_WARNING("didn't find hdr"); + highIndex = FindHdr(msgHdr); +#ifdef DEBUG_David_Bienvenu + if (highIndex != nsMsgViewIndex_None) { + NS_WARNING("but find hdr did"); + printf("level of found hdr = %d\n", m_levels[highIndex]); + ValidateSort(); + } +#endif + return highIndex; + } + + return msgHdr == resultHdr ? highIndex : nsMsgViewIndex_None; +} + +#ifdef DEBUG_David_Bienvenu + +void nsMsgDBView::InitEntryInfoForIndex(nsMsgViewIndex i, IdKey& EntryInfo) { + nsresult rv; + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // The following may leave fieldType undefined. + rv = GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to obtain fieldType"); + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + + msgHdr->GetMessageKey(&EntryInfo.id); + msgHdr->GetFolder(&EntryInfo.folder); + EntryInfo.folder->Release(); + + nsCOMPtr<nsIMsgDatabase> hdrDB; + EntryInfo.folder->GetMsgDatabase(getter_AddRefs(hdrDB)); + switch (fieldType) { + case kCollationKey: + rv = GetCollationKey(msgHdr, m_sortType, EntryInfo.key, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to create collation key"); + break; + case kU32: + if (m_sortType == nsMsgViewSortType::byId) + EntryInfo.dword = EntryInfo.id; + else + GetLongField(msgHdr, m_sortType, &EntryInfo.dword, colHandler); + + break; + default: + NS_ERROR("invalid field type"); + } +} + +void nsMsgDBView::ValidateSort() { + IdKey EntryInfo1, EntryInfo2; + nsCOMPtr<nsIMsgDBHdr> hdr1, hdr2; + + uint16_t maxLen; + eFieldType fieldType; + + // Get the custom column handler for the primary sort and pass it first + // to GetFieldTypeAndLenForSort to get the fieldType and then either + // GetCollationKey or GetLongField. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + + // It is not entirely clear what we should do since, + // if fieldType is not available, there is no way to know + // how to compare the field to check for sorting. + // So we bomb out here. It is OK since this is debug code + // inside #ifdef DEBUG_David_Bienvenu + nsresult rv = + GetFieldTypeAndLenForSort(m_sortType, &maxLen, &fieldType, colHandler); + NS_ASSERTION(NS_SUCCEEDED(rv), "failed to obtain fieldType"); + + viewSortInfo comparisonContext; + comparisonContext.view = this; + comparisonContext.isSecondarySort = false; + comparisonContext.ascendingSort = + (m_sortOrder == nsMsgViewSortOrder::ascending); + nsCOMPtr<nsIMsgDatabase> db; + GetDBForViewIndex(0, getter_AddRefs(db)); + // This is only for comparing collation keys - it could be any db. + comparisonContext.db = db.get(); + + for (nsMsgViewIndex i = 0; i < m_keys.Length();) { + // Ignore non threads. + if (m_levels[i]) { + i++; + continue; + } + + // Find next header. + nsMsgViewIndex j = i + 1; + while (j < m_keys.Length() && m_levels[j]) j++; + + if (j == m_keys.Length()) break; + + InitEntryInfoForIndex(i, EntryInfo1); + InitEntryInfoForIndex(j, EntryInfo2); + const void *pValue1 = &EntryInfo1, *pValue2 = &EntryInfo2; + int retStatus = 0; + if (fieldType == kCollationKey) + retStatus = FnSortIdKey(&pValue1, &pValue2, &comparisonContext); + else if (fieldType == kU32) + retStatus = FnSortIdUint32(&pValue1, &pValue2, &comparisonContext); + + if (retStatus && + (retStatus < 0) == (m_sortOrder == nsMsgViewSortOrder::ascending)) { + NS_ERROR("view not sorted correctly"); + break; + } + // j is the new i. + i = j; + } +} + +#endif + +nsresult nsMsgDBView::ListUnreadIdsInThread( + nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex, + uint32_t* pNumListed) { + NS_ENSURE_ARG(threadHdr); + // These children ids should be in thread order. + nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1; + *pNumListed = 0; + nsMsgKey topLevelMsgKey = m_keys[startOfThreadViewIndex]; + + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + uint32_t i; + for (i = 0; i < numChildren; i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + if (msgHdr != nullptr) { + if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) { + bool killed; + msgHdr->GetIsKilled(&killed); + if (killed) continue; + } + + nsMsgKey msgKey; + uint32_t msgFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + bool isRead = AdjustReadFlag(msgHdr, &msgFlags); + if (!isRead) { + // Just make sure flag is right in db. + m_db->MarkHdrRead(msgHdr, false, nullptr); + if (msgKey != topLevelMsgKey) { + InsertMsgHdrAt( + viewIndex, msgHdr, msgKey, msgFlags, + FindLevelInThread(msgHdr, startOfThreadViewIndex, viewIndex)); + viewIndex++; + (*pNumListed)++; + } + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, + uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) { + // If we're not the instigator, update flags if this key is in our view. + if (aInstigator != this) { + NS_ENSURE_ARG_POINTER(aHdrChanged); + nsMsgKey msgKey; + aHdrChanged->GetMessageKey(&msgKey); + nsMsgViewIndex index = FindHdr(aHdrChanged); + if (index != nsMsgViewIndex_None) { + uint32_t viewOnlyFlags = + m_flags[index] & (MSG_VIEW_FLAGS | nsMsgMessageFlags::Elided); + + // XXX what about saving the old view only flags, like IsThread and + // HasChildren? + // I think we'll want to save those away. + m_flags[index] = aNewFlags | viewOnlyFlags; + // Tell the view the extra flag changed, so it can + // update the previous view, if any. + OnExtraFlagChanged(index, aNewFlags); + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + } + + uint32_t deltaFlags = (aOldFlags ^ aNewFlags); + if (deltaFlags & (nsMsgMessageFlags::Read | nsMsgMessageFlags::New)) { + nsMsgViewIndex threadIndex = + ThreadIndexOfMsgHdr(aHdrChanged, index, nullptr, nullptr); + + // May need to fix thread counts. + if (threadIndex != nsMsgViewIndex_None && threadIndex != index) + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + } + } + + // Don't need to propagate notifications, right? + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey, + int32_t aFlags, nsIDBChangeListener* aInstigator) { + nsMsgViewIndex deletedIndex = FindHdr(aHdrChanged); + if (IsValidIndex(deletedIndex)) { + // Check if this message is currently selected. If it is, tell the frontend + // to be prepared for a delete. + nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater( + do_QueryReferent(mCommandUpdater)); + bool isMsgSelected = false; + if (mTreeSelection && commandUpdater) { + mTreeSelection->IsSelected(deletedIndex, &isMsgSelected); + if (isMsgSelected) commandUpdater->UpdateNextMessageAfterDelete(); + } + + RemoveByIndex(deletedIndex); + + if (isMsgSelected) { + // Now tell the front end that the delete happened. + commandUpdater->SelectedMessageRemoved(); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnHdrAdded(nsIMsgDBHdr* aHdrChanged, nsMsgKey aParentKey, + int32_t aFlags, nsIDBChangeListener* aInstigator) { + return OnNewHeader(aHdrChanged, aParentKey, false); + // Probably also want to pass that parent key in, since we went to the + // trouble of figuring out what it is. +} + +NS_IMETHODIMP +nsMsgDBView::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange, + const nsACString& property, bool aPreChange, + uint32_t* aStatus, + nsIDBChangeListener* aInstigator) { + if (aPreChange) return NS_OK; + + if (aHdrToChange) { + nsMsgViewIndex index = FindHdr(aHdrToChange); + if (index != nsMsgViewIndex_None) + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent, + nsMsgKey newParent, + nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) { + if (m_db) { + m_db->RemoveListener(this); + m_db = nullptr; + } + + int32_t saveSize = GetSize(); + ClearHdrCache(); + + // This is important, because the tree will ask us for our + // row count, which get determine from the number of keys. + m_keys.Clear(); + // Be consistent. + m_flags.Clear(); + m_levels.Clear(); + + // Tell the tree all the rows have gone away. + if (mTree) mTree->RowCountChanged(0, -saveSize); + if (mJSTree) mJSTree->RowCountChanged(0, -saveSize); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnEvent(nsIMsgDatabase* aDB, const char* aEvent) { + if (!strcmp(aEvent, "DBOpened")) m_db = aDB; + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::OnReadChanged(nsIDBChangeListener* aInstigator) { return NS_OK; } + +NS_IMETHODIMP +nsMsgDBView::OnJunkScoreChanged(nsIDBChangeListener* aInstigator) { + return NS_OK; +} + +void nsMsgDBView::ClearHdrCache() { + m_cachedHdr = nullptr; + m_cachedMsgKey = nsMsgKey_None; +} + +NS_IMETHODIMP +nsMsgDBView::SetSuppressChangeNotifications(bool aSuppressChangeNotifications) { + mSuppressChangeNotification = aSuppressChangeNotifications; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSuppressChangeNotifications( + bool* aSuppressChangeNotifications) { + NS_ENSURE_ARG_POINTER(aSuppressChangeNotifications); + *aSuppressChangeNotifications = mSuppressChangeNotification; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::NoteChange(nsMsgViewIndex firstLineChanged, int32_t numChanged, + nsMsgViewNotificationCodeValue changeType) { + if ((mTree || mJSTree) && !mSuppressChangeNotification) { + switch (changeType) { + case nsMsgViewNotificationCode::changed: + if (mTree) + mTree->InvalidateRange(firstLineChanged, + firstLineChanged + numChanged - 1); + if (mJSTree) + mJSTree->InvalidateRange(firstLineChanged, + firstLineChanged + numChanged - 1); + break; + case nsMsgViewNotificationCode::insertOrDelete: + if (numChanged < 0) mRemovingRow = true; + + // The caller needs to have adjusted m_keys before getting here, since + // RowCountChanged() will call our GetRowCount(). + if (mTree) mTree->RowCountChanged(firstLineChanged, numChanged); + if (mJSTree) mJSTree->RowCountChanged(firstLineChanged, numChanged); + mRemovingRow = false; + [[fallthrough]]; + case nsMsgViewNotificationCode::all: + ClearHdrCache(); + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSortOrder(nsMsgViewSortOrderValue* aSortOrder) { + NS_ENSURE_ARG_POINTER(aSortOrder); + *aSortOrder = m_sortOrder; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSortType(nsMsgViewSortTypeValue* aSortType) { + NS_ENSURE_ARG_POINTER(aSortType); + *aSortType = m_sortType; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSortType(nsMsgViewSortTypeValue aSortType) { + m_sortType = aSortType; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetViewType(nsMsgViewTypeValue* aViewType) { + NS_ERROR("you should be overriding this"); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +nsMsgDBView::GetSecondarySortOrder(nsMsgViewSortOrderValue* aSortOrder) { + NS_ENSURE_ARG_POINTER(aSortOrder); + *aSortOrder = m_secondarySortOrder; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSecondarySortOrder(nsMsgViewSortOrderValue aSortOrder) { + m_secondarySortOrder = aSortOrder; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSecondarySortType(nsMsgViewSortTypeValue* aSortType) { + NS_ENSURE_ARG_POINTER(aSortType); + *aSortType = m_secondarySort; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetSecondarySortType(nsMsgViewSortTypeValue aSortType) { + m_secondarySort = aSortType; + return NS_OK; +} + +nsresult nsMsgDBView::PersistFolderInfo(nsIDBFolderInfo** dbFolderInfo) { + nsresult rv = m_db->GetDBFolderInfo(dbFolderInfo); + NS_ENSURE_SUCCESS(rv, rv); + // Save off sort type and order, view type and flags. + (*dbFolderInfo)->SetSortType(m_sortType); + (*dbFolderInfo)->SetSortOrder(m_sortOrder); + (*dbFolderInfo)->SetViewFlags(m_viewFlags); + nsMsgViewTypeValue viewType; + GetViewType(&viewType); + (*dbFolderInfo)->SetViewType(viewType); + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::GetViewFlags(nsMsgViewFlagsTypeValue* aViewFlags) { + NS_ENSURE_ARG_POINTER(aViewFlags); + *aViewFlags = m_viewFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) { + // If we're turning off threaded display, we need to expand all so that all + // messages will be displayed. + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(aViewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + ExpandAll(); + // Invalidate the sort so sorting will do something. + m_sortValid = false; + } + + m_viewFlags = aViewFlags; + + if (m_viewFolder) { + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + return folderInfo->SetViewFlags(aViewFlags); + } else + return NS_OK; +} + +nsresult nsMsgDBView::MarkThreadOfMsgRead(nsMsgKey msgId, + nsMsgViewIndex msgIndex, + nsTArray<nsMsgKey>& idsMarkedRead, + bool bRead) { + nsCOMPtr<nsIMsgThread> threadHdr; + nsresult rv = GetThreadContainingIndex(msgIndex, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgViewIndex threadIndex; + + NS_ASSERTION(threadHdr, "threadHdr is null"); + if (!threadHdr) return NS_MSG_MESSAGE_NOT_FOUND; + + nsCOMPtr<nsIMsgDBHdr> firstHdr; + rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(firstHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgKey firstHdrId; + firstHdr->GetMessageKey(&firstHdrId); + if (msgId != firstHdrId) + threadIndex = GetIndexOfFirstDisplayedKeyInThread(threadHdr); + else + threadIndex = msgIndex; + + return MarkThreadRead(threadHdr, threadIndex, idsMarkedRead, bRead); +} + +nsresult nsMsgDBView::MarkThreadRead(nsIMsgThread* threadHdr, + nsMsgViewIndex threadIndex, + nsTArray<nsMsgKey>& idsMarkedRead, + bool bRead) { + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + idsMarkedRead.SetCapacity(numChildren); + for (int32_t childIndex = 0; childIndex < (int32_t)numChildren; + childIndex++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(childIndex, getter_AddRefs(msgHdr)); + NS_ASSERTION(msgHdr, "msgHdr is null"); + if (!msgHdr) continue; + + bool isRead; + nsMsgKey hdrMsgId; + msgHdr->GetMessageKey(&hdrMsgId); + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = GetDBForHeader(msgHdr, getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + db->IsRead(hdrMsgId, &isRead); + + if (isRead != bRead) { + // MarkHdrRead will change the unread count on the thread. + db->MarkHdrRead(msgHdr, bRead, nullptr); + // Insert at the front. Should we insert at the end? + idsMarkedRead.InsertElementAt(0, hdrMsgId); + } + } + + return NS_OK; +} + +bool nsMsgDBView::AdjustReadFlag(nsIMsgDBHdr* msgHdr, uint32_t* msgFlags) { + // If we're a cross-folder view, just bail on this. + if (GetFolders()) return *msgFlags & nsMsgMessageFlags::Read; + + bool isRead = false; + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + m_db->IsRead(msgKey, &isRead); + // Just make sure flag is right in db. +#ifdef DEBUG_David_Bienvenu + NS_ASSERTION(isRead == ((*msgFlags & nsMsgMessageFlags::Read) != 0), + "msgFlags out of sync"); +#endif + if (isRead) + *msgFlags |= nsMsgMessageFlags::Read; + else + *msgFlags &= ~nsMsgMessageFlags::Read; + + m_db->MarkHdrRead(msgHdr, isRead, nullptr); + return isRead; +} + +// Starting from startIndex, performs the passed in navigation, including +// any marking read needed, and returns the resultId and resultIndex of the +// destination of the navigation. If no message is found in the view, +// it returns a resultId of nsMsgKey_None and an resultIndex of +// nsMsgViewIndex_None. +NS_IMETHODIMP +nsMsgDBView::ViewNavigate(nsMsgNavigationTypeValue motion, nsMsgKey* pResultKey, + nsMsgViewIndex* pResultIndex, + nsMsgViewIndex* pThreadIndex, bool wrap) { + NS_ENSURE_ARG_POINTER(pResultKey); + NS_ENSURE_ARG_POINTER(pResultIndex); + NS_ENSURE_ARG_POINTER(pThreadIndex); + + int32_t currentIndex; + nsMsgViewIndex startIndex; + + if (!mTreeSelection) { + // We must be in stand alone message mode. + currentIndex = FindViewIndex(m_currentlyDisplayedMsgKey); + } else { + nsresult rv = mTreeSelection->GetCurrentIndex(¤tIndex); + NS_ENSURE_SUCCESS(rv, rv); + } + + startIndex = currentIndex; + return nsMsgDBView::NavigateFromPos(motion, startIndex, pResultKey, + pResultIndex, pThreadIndex, wrap); +} + +nsresult nsMsgDBView::NavigateFromPos(nsMsgNavigationTypeValue motion, + nsMsgViewIndex startIndex, + nsMsgKey* pResultKey, + nsMsgViewIndex* pResultIndex, + nsMsgViewIndex* pThreadIndex, bool wrap) { + nsresult rv = NS_OK; + nsMsgKey resultThreadKey; + nsMsgViewIndex curIndex; + nsMsgViewIndex lastIndex = + (GetSize() > 0) ? (nsMsgViewIndex)GetSize() - 1 : nsMsgViewIndex_None; + nsMsgViewIndex threadIndex = nsMsgViewIndex_None; + + // If there aren't any messages in the view, bail out. + if (GetSize() <= 0) { + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + return NS_OK; + } + *pResultKey = nsMsgKey_None; + + switch (motion) { + case nsMsgNavigationType::firstMessage: + *pResultIndex = 0; + *pResultKey = m_keys[0]; + break; + case nsMsgNavigationType::nextMessage: + // Return same index and id on next on last message. + *pResultIndex = std::min(startIndex + 1, lastIndex); + *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::previousMessage: + if (startIndex != nsMsgViewIndex_None && startIndex > 0) { + *pResultIndex = startIndex - 1; + } + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::lastMessage: + *pResultIndex = lastIndex; + *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::firstFlagged: + rv = FindFirstFlagged(pResultIndex); + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::nextFlagged: + rv = FindNextFlagged(startIndex + 1, pResultIndex); + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::previousFlagged: + rv = FindPrevFlagged(startIndex, pResultIndex); + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::firstNew: + rv = FindFirstNew(pResultIndex); + if (IsValidIndex(*pResultIndex)) *pResultKey = m_keys[*pResultIndex]; + break; + case nsMsgNavigationType::firstUnreadMessage: + startIndex = nsMsgViewIndex_None; + // Note fall through - is this motion ever used? + [[fallthrough]]; + case nsMsgNavigationType::nextUnreadMessage: + for (curIndex = (startIndex == nsMsgViewIndex_None) ? 0 : startIndex; + curIndex <= lastIndex && lastIndex != nsMsgViewIndex_None; + curIndex++) { + uint32_t flags = m_flags[curIndex]; + // Don't return start index since navigate should move. + if (!(flags & (nsMsgMessageFlags::Read | MSG_VIEW_FLAG_DUMMY)) && + (curIndex != startIndex)) { + *pResultIndex = curIndex; + *pResultKey = m_keys[*pResultIndex]; + break; + } + + // Check for collapsed thread with new children. + if ((m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) && + flags & MSG_VIEW_FLAG_ISTHREAD && + flags & nsMsgMessageFlags::Elided) { + nsCOMPtr<nsIMsgThread> threadHdr; + GetThreadContainingIndex(curIndex, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(threadHdr, "threadHdr is null"); + if (!threadHdr) continue; + + uint32_t numUnreadChildren; + threadHdr->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) { + uint32_t numExpanded; + ExpandByIndex(curIndex, &numExpanded); + lastIndex += numExpanded; + if (pThreadIndex) *pThreadIndex = curIndex; + } + } + } + + if (curIndex > lastIndex) { + // Wrap around by starting at index 0. + if (wrap) { + nsMsgKey startKey = GetAt(startIndex); + rv = NavigateFromPos(nsMsgNavigationType::nextUnreadMessage, + nsMsgViewIndex_None, pResultKey, pResultIndex, + pThreadIndex, false); + + if (*pResultKey == startKey) { + // wrapped around and found start message! + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + } + } else { + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + } + } + break; + case nsMsgNavigationType::previousUnreadMessage: + if (!IsValidIndex(startIndex)) break; + + rv = FindPrevUnread(m_keys[startIndex], pResultKey, &resultThreadKey); + if (NS_SUCCEEDED(rv)) { + *pResultIndex = FindViewIndex(*pResultKey); + if (*pResultKey != resultThreadKey && + (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + threadIndex = GetThreadIndex(*pResultIndex); + if (*pResultIndex == nsMsgViewIndex_None) { + nsCOMPtr<nsIMsgThread> threadHdr; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = m_db->GetMsgHdrForKey(*pResultKey, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(threadHdr, "threadHdr is null"); + if (threadHdr) break; + + uint32_t numUnreadChildren; + threadHdr->GetNumUnreadChildren(&numUnreadChildren); + if (numUnreadChildren > 0) { + uint32_t numExpanded; + ExpandByIndex(threadIndex, &numExpanded); + } + + *pResultIndex = FindViewIndex(*pResultKey); + } + } + + if (pThreadIndex) *pThreadIndex = threadIndex; + } + break; + case nsMsgNavigationType::lastUnreadMessage: + break; + case nsMsgNavigationType::nextUnreadThread: + if (startIndex != nsMsgViewIndex_None) { + ApplyCommandToIndices(nsMsgViewCommandType::markThreadRead, + {startIndex}); + } + + return NavigateFromPos(nsMsgNavigationType::nextUnreadMessage, startIndex, + pResultKey, pResultIndex, pThreadIndex, true); + case nsMsgNavigationType::toggleThreadKilled: { + bool resultKilled; + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + ToggleIgnored(selection, &threadIndex, &resultKilled); + if (resultKilled) { + return NavigateFromPos(nsMsgNavigationType::nextUnreadThread, + threadIndex, pResultKey, pResultIndex, + pThreadIndex, true); + } else { + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + return NS_OK; + } + } + case nsMsgNavigationType::toggleSubthreadKilled: { + bool resultKilled; + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + ToggleMessageKilled(selection, &threadIndex, &resultKilled); + if (resultKilled) { + return NavigateFromPos(nsMsgNavigationType::nextUnreadMessage, + threadIndex, pResultKey, pResultIndex, + pThreadIndex, true); + } else { + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + return NS_OK; + } + } + // Check where navigate says this will take us. If we have the message + // in the view, return it. Otherwise, return an error. + case nsMsgNavigationType::back: + case nsMsgNavigationType::forward: + // Handled purely in JS. + *pResultIndex = nsMsgViewIndex_None; + *pResultKey = nsMsgKey_None; + break; + default: + NS_ERROR("unsupported motion"); + break; + } + + return NS_OK; +} + +// Note that these routines do NOT expand collapsed threads! This mimics the +// old behaviour, but it's also because we don't remember whether a thread +// contains a flagged message the same way we remember if a thread contains +// new messages. It would be painful to dive down into each collapsed thread +// to update navigate status. We could cache this info, but it would still be +// expensive the first time this status needs to get updated. +nsresult nsMsgDBView::FindNextFlagged(nsMsgViewIndex startIndex, + nsMsgViewIndex* pResultIndex) { + nsMsgViewIndex lastIndex = (nsMsgViewIndex)GetSize() - 1; + nsMsgViewIndex curIndex; + + *pResultIndex = nsMsgViewIndex_None; + + if (GetSize() > 0) { + for (curIndex = startIndex; curIndex <= lastIndex; curIndex++) { + uint32_t flags = m_flags[curIndex]; + if (flags & nsMsgMessageFlags::Marked) { + *pResultIndex = curIndex; + break; + } + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::FindFirstNew(nsMsgViewIndex* pResultIndex) { + if (m_db) { + nsMsgKey firstNewKey = nsMsgKey_None; + m_db->GetFirstNew(&firstNewKey); + *pResultIndex = (firstNewKey != nsMsgKey_None) ? FindKey(firstNewKey, true) + : nsMsgViewIndex_None; + } + + return NS_OK; +} + +nsresult nsMsgDBView::FindPrevUnread(nsMsgKey startKey, nsMsgKey* pResultKey, + nsMsgKey* resultThreadId) { + nsMsgViewIndex startIndex = FindViewIndex(startKey); + nsMsgViewIndex curIndex = startIndex; + nsresult rv = NS_MSG_MESSAGE_NOT_FOUND; + + if (startIndex == nsMsgViewIndex_None) return NS_MSG_MESSAGE_NOT_FOUND; + + *pResultKey = nsMsgKey_None; + if (resultThreadId) *resultThreadId = nsMsgKey_None; + + for (; (int)curIndex >= 0 && (*pResultKey == nsMsgKey_None); curIndex--) { + uint32_t flags = m_flags[curIndex]; + if (curIndex != startIndex && flags & MSG_VIEW_FLAG_ISTHREAD && + flags & nsMsgMessageFlags::Elided) { + NS_ERROR("fix this"); + // nsMsgKey threadId = m_keys[curIndex]; + // rv = m_db->GetUnreadKeyInThread(threadId, pResultKey, resultThreadId); + if (NS_SUCCEEDED(rv) && (*pResultKey != nsMsgKey_None)) break; + } + + if (!(flags & (nsMsgMessageFlags::Read | MSG_VIEW_FLAG_DUMMY)) && + (curIndex != startIndex)) { + *pResultKey = m_keys[curIndex]; + rv = NS_OK; + break; + } + } + + // Found unread message but we don't know the thread. + NS_ASSERTION(!(*pResultKey != nsMsgKey_None && resultThreadId && + *resultThreadId == nsMsgKey_None), + "fix this"); + return rv; +} + +nsresult nsMsgDBView::FindFirstFlagged(nsMsgViewIndex* pResultIndex) { + return FindNextFlagged(0, pResultIndex); +} + +nsresult nsMsgDBView::FindPrevFlagged(nsMsgViewIndex startIndex, + nsMsgViewIndex* pResultIndex) { + nsMsgViewIndex curIndex; + + *pResultIndex = nsMsgViewIndex_None; + + if (GetSize() > 0 && IsValidIndex(startIndex)) { + curIndex = startIndex; + do { + if (curIndex != 0) curIndex--; + + uint32_t flags = m_flags[curIndex]; + if (flags & nsMsgMessageFlags::Marked) { + *pResultIndex = curIndex; + break; + } + } while (curIndex != 0); + } + + return NS_OK; +} + +bool nsMsgDBView::IsValidIndex(nsMsgViewIndex index) { + return index != nsMsgViewIndex_None && + (index < (nsMsgViewIndex)m_keys.Length()); +} + +nsresult nsMsgDBView::OrExtraFlag(nsMsgViewIndex index, uint32_t orflag) { + uint32_t flag; + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + flag = m_flags[index]; + flag |= orflag; + m_flags[index] = flag; + OnExtraFlagChanged(index, flag); + return NS_OK; +} + +nsresult nsMsgDBView::AndExtraFlag(nsMsgViewIndex index, uint32_t andflag) { + uint32_t flag; + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + flag = m_flags[index]; + flag &= andflag; + m_flags[index] = flag; + OnExtraFlagChanged(index, flag); + return NS_OK; +} + +nsresult nsMsgDBView::SetExtraFlag(nsMsgViewIndex index, uint32_t extraflag) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + m_flags[index] = extraflag; + OnExtraFlagChanged(index, extraflag); + return NS_OK; +} + +nsresult nsMsgDBView::ToggleIgnored(nsTArray<nsMsgViewIndex> const& selection, + nsMsgViewIndex* resultIndex, + bool* resultToggleState) { + nsCOMPtr<nsIMsgThread> thread; + + // Ignored state is toggled based on the first selected thread. + nsMsgViewIndex threadIndex = + GetThreadFromMsgIndex(selection[0], getter_AddRefs(thread)); + NS_ENSURE_STATE(thread); + uint32_t threadFlags; + thread->GetFlags(&threadFlags); + uint32_t ignored = threadFlags & nsMsgMessageFlags::Ignored; + + // Process threads in reverse order. + // Otherwise collapsing the threads will invalidate the indices. + threadIndex = nsMsgViewIndex_None; + uint32_t numIndices = selection.Length(); + while (numIndices) { + numIndices--; + if (selection[numIndices] < threadIndex) { + threadIndex = + GetThreadFromMsgIndex(selection[numIndices], getter_AddRefs(thread)); + thread->GetFlags(&threadFlags); + if ((threadFlags & nsMsgMessageFlags::Ignored) == ignored) + SetThreadIgnored(thread, threadIndex, !ignored); + } + } + + if (resultIndex) *resultIndex = threadIndex; + + if (resultToggleState) *resultToggleState = !ignored; + + return NS_OK; +} + +nsresult nsMsgDBView::ToggleMessageKilled( + nsTArray<nsMsgViewIndex> const& selection, nsMsgViewIndex* resultIndex, + bool* resultToggleState) { + NS_ENSURE_ARG_POINTER(resultToggleState); + + nsCOMPtr<nsIMsgDBHdr> header; + // Ignored state is toggled based on the first selected message. + nsresult rv = GetMsgHdrForViewIndex(selection[0], getter_AddRefs(header)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t msgFlags; + header->GetFlags(&msgFlags); + uint32_t ignored = msgFlags & nsMsgMessageFlags::Ignored; + + // Process messages in reverse order. + // Otherwise the indices may be invalidated. + nsMsgViewIndex msgIndex = nsMsgViewIndex_None; + uint32_t numIndices = selection.Length(); + while (numIndices) { + numIndices--; + if (selection[numIndices] < msgIndex) { + msgIndex = selection[numIndices]; + rv = GetMsgHdrForViewIndex(msgIndex, getter_AddRefs(header)); + NS_ENSURE_SUCCESS(rv, rv); + header->GetFlags(&msgFlags); + if ((msgFlags & nsMsgMessageFlags::Ignored) == ignored) + SetSubthreadKilled(header, msgIndex, !ignored); + } + } + + if (resultIndex) *resultIndex = msgIndex; + + if (resultToggleState) *resultToggleState = !ignored; + + return NS_OK; +} + +nsMsgViewIndex nsMsgDBView::GetThreadFromMsgIndex(nsMsgViewIndex index, + nsIMsgThread** threadHdr) { + if (threadHdr == nullptr) return nsMsgViewIndex_None; + nsMsgKey msgKey = GetAt(index); + + nsresult rv = GetThreadContainingIndex(index, threadHdr); + NS_ENSURE_SUCCESS(rv, nsMsgViewIndex_None); + + if (*threadHdr == nullptr) return nsMsgViewIndex_None; + + nsMsgKey threadKey; + (*threadHdr)->GetThreadKey(&threadKey); + nsMsgViewIndex threadIndex; + if (msgKey != threadKey) + threadIndex = GetIndexOfFirstDisplayedKeyInThread(*threadHdr); + else + threadIndex = index; + return threadIndex; +} + +nsresult nsMsgDBView::ToggleWatched(nsTArray<nsMsgViewIndex> const& selection) { + MOZ_ASSERT(!selection.IsEmpty()); + nsCOMPtr<nsIMsgThread> thread; + + // Watched state is toggled based on the first selected thread. + nsMsgViewIndex threadIndex = + GetThreadFromMsgIndex(selection[0], getter_AddRefs(thread)); + NS_ENSURE_STATE(thread); + uint32_t threadFlags; + thread->GetFlags(&threadFlags); + uint32_t watched = threadFlags & nsMsgMessageFlags::Watched; + + // Process threads in reverse order for consistency with ToggleIgnored. + threadIndex = nsMsgViewIndex_None; + uint32_t numIndices = selection.Length(); + while (numIndices) { + numIndices--; + if (selection[numIndices] < threadIndex) { + threadIndex = + GetThreadFromMsgIndex(selection[numIndices], getter_AddRefs(thread)); + thread->GetFlags(&threadFlags); + if ((threadFlags & nsMsgMessageFlags::Watched) == watched) + SetThreadWatched(thread, threadIndex, !watched); + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::SetThreadIgnored(nsIMsgThread* thread, + nsMsgViewIndex threadIndex, + bool ignored) { + if (!IsValidIndex(threadIndex)) return NS_MSG_INVALID_DBVIEW_INDEX; + + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + if (ignored) { + nsTArray<nsMsgKey> idsMarkedRead; + MarkThreadRead(thread, threadIndex, idsMarkedRead, true); + CollapseByIndex(threadIndex, nullptr); + } + + if (!m_db) return NS_ERROR_FAILURE; + + return m_db->MarkThreadIgnored(thread, m_keys[threadIndex], ignored, this); +} + +nsresult nsMsgDBView::SetSubthreadKilled(nsIMsgDBHdr* header, + nsMsgViewIndex msgIndex, + bool ignored) { + if (!IsValidIndex(msgIndex)) return NS_MSG_INVALID_DBVIEW_INDEX; + + NoteChange(msgIndex, 1, nsMsgViewNotificationCode::changed); + + if (!m_db) return NS_ERROR_FAILURE; + + nsresult rv = m_db->MarkHeaderKilled(header, ignored, this); + NS_ENSURE_SUCCESS(rv, rv); + + if (ignored) { + nsCOMPtr<nsIMsgThread> thread; + nsresult rv; + rv = GetThreadContainingMsgHdr(header, getter_AddRefs(thread)); + // So we didn't mark threads read. + if (NS_FAILED(rv)) return NS_OK; + + uint32_t children, current; + thread->GetNumChildren(&children); + + nsMsgKey headKey; + header->GetMessageKey(&headKey); + + for (current = 0; current < children; current++) { + nsMsgKey newKey; + thread->GetChildKeyAt(current, &newKey); + if (newKey == headKey) break; + } + + // Process all messages, starting with this message. + for (; current < children; current++) { + nsCOMPtr<nsIMsgDBHdr> nextHdr; + bool isKilled; + + thread->GetChildHdrAt(current, getter_AddRefs(nextHdr)); + nextHdr->GetIsKilled(&isKilled); + + // Ideally, the messages should stop processing here. + // However, the children are ordered not by thread... + if (isKilled) nextHdr->MarkRead(true); + } + } + + return NS_OK; +} + +nsresult nsMsgDBView::SetThreadWatched(nsIMsgThread* thread, + nsMsgViewIndex index, bool watched) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + nsresult rv = m_db->MarkThreadWatched(thread, m_keys[index], watched, this); + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::GetMsgFolder(nsIMsgFolder** aMsgFolder) { + NS_ENSURE_ARG_POINTER(aMsgFolder); + NS_IF_ADDREF(*aMsgFolder = m_folder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SetViewFolder(nsIMsgFolder* aMsgFolder) { + m_viewFolder = aMsgFolder; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetViewFolder(nsIMsgFolder** aMsgFolder) { + NS_ENSURE_ARG_POINTER(aMsgFolder); + NS_IF_ADDREF(*aMsgFolder = m_viewFolder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetNumSelected(uint32_t* aNumSelected) { + NS_ENSURE_ARG_POINTER(aNumSelected); + + if (!mTreeSelection) { + // No tree selection can mean we're in the stand alone mode. + *aNumSelected = (m_currentlyDisplayedMsgKey != nsMsgKey_None) ? 1 : 0; + return NS_OK; + } + + bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads(); + + // We call this a lot from the front end JS, so make it fast. + nsresult rv = mTreeSelection->GetCount((int32_t*)aNumSelected); + if (!*aNumSelected || !includeCollapsedMsgs || + !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) + return rv; + + int32_t numSelectedIncludingCollapsed = *aNumSelected; + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + int32_t numIndices = selection.Length(); + // Iterate over the selection, counting up the messages in collapsed + // threads. + for (int32_t i = 0; i < numIndices; i++) { + if (m_flags[selection[i]] & nsMsgMessageFlags::Elided) { + int32_t collapsedCount; + ExpansionDelta(selection[i], &collapsedCount); + numSelectedIncludingCollapsed += collapsedCount; + } + } + + *aNumSelected = numSelectedIncludingCollapsed; + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::GetNumMsgsInView(int32_t* aNumMsgs) { + NS_ENSURE_ARG_POINTER(aNumMsgs); + return (m_folder) ? m_folder->GetTotalMessages(false, aNumMsgs) + : NS_ERROR_FAILURE; +} + +/** + * @note For the IMAP delete model, this applies to both deleting and + * undeleting a message. + */ +NS_IMETHODIMP +nsMsgDBView::GetMsgToSelectAfterDelete(nsMsgViewIndex* msgToSelectAfterDelete) { + NS_ENSURE_ARG_POINTER(msgToSelectAfterDelete); + *msgToSelectAfterDelete = nsMsgViewIndex_None; + + bool isMultiSelect = false; + int32_t startFirstRange = nsMsgViewIndex_None; + int32_t endFirstRange = nsMsgViewIndex_None; + if (!mTreeSelection) { + // If we don't have a tree selection then we must be in stand alone mode. + // return the index of the current message key as the first selected index. + *msgToSelectAfterDelete = FindViewIndex(m_currentlyDisplayedMsgKey); + } else { + int32_t selectionCount; + int32_t startRange; + int32_t endRange; + nsresult rv = mTreeSelection->GetRangeCount(&selectionCount); + NS_ENSURE_SUCCESS(rv, rv); + for (int32_t i = 0; i < selectionCount; i++) { + rv = mTreeSelection->GetRangeAt(i, &startRange, &endRange); + NS_ENSURE_SUCCESS(rv, rv); + + // Save off the first range in case we need it later. + if (i == 0) { + startFirstRange = startRange; + endFirstRange = endRange; + } else { + // If the tree selection is goofy (eg adjacent or overlapping ranges), + // complain about it, but don't try and cope. Just live with the fact + // that one of the deleted messages is going to end up selected. + NS_WARNING_ASSERTION( + endFirstRange != startRange, + "goofy tree selection state: two ranges are adjacent!"); + } + + *msgToSelectAfterDelete = + std::min(*msgToSelectAfterDelete, (nsMsgViewIndex)startRange); + } + + // Multiple selection either using Ctrl, Shift, or one of the affordances + // to select an entire thread. + isMultiSelect = (selectionCount > 1 || (endRange - startRange) > 0); + } + + if (*msgToSelectAfterDelete == nsMsgViewIndex_None) return NS_OK; + + nsCOMPtr<nsIMsgFolder> folder; + GetMsgFolder(getter_AddRefs(folder)); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(folder); + bool thisIsImapFolder = (imapFolder != nullptr); + // Need to update the imap-delete model, can change more than once in a + // session. + if (thisIsImapFolder) GetImapDeleteModel(nullptr); + + // If mail.delete_matches_sort_order is true, + // for views sorted in descending order (newest at the top), make + // msgToSelectAfterDelete advance in the same direction as the sort order. + bool deleteMatchesSort = false; + if (m_sortOrder == nsMsgViewSortOrder::descending && + *msgToSelectAfterDelete) { + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + prefBranch->GetBoolPref("mail.delete_matches_sort_order", + &deleteMatchesSort); + } + + if (mDeleteModel == nsMsgImapDeleteModels::IMAPDelete) { + if (isMultiSelect) { + if (deleteMatchesSort) + *msgToSelectAfterDelete = startFirstRange - 1; + else + *msgToSelectAfterDelete = endFirstRange + 1; + } else { + if (deleteMatchesSort) + *msgToSelectAfterDelete -= 1; + else + *msgToSelectAfterDelete += 1; + } + } else if (deleteMatchesSort) { + *msgToSelectAfterDelete -= 1; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetRemoveRowOnMoveOrDelete(bool* aRemoveRowOnMoveOrDelete) { + NS_ENSURE_ARG_POINTER(aRemoveRowOnMoveOrDelete); + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_folder); + if (!imapFolder) { + *aRemoveRowOnMoveOrDelete = true; + return NS_OK; + } + + // Need to update the imap-delete model, can change more than once in a + // session. + GetImapDeleteModel(nullptr); + + // Unlike the other imap delete models, "mark as deleted" does not remove + // rows on delete (or move). + *aRemoveRowOnMoveOrDelete = + (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetCurrentlyDisplayedMessage( + nsMsgViewIndex* currentlyDisplayedMessage) { + NS_ENSURE_ARG_POINTER(currentlyDisplayedMessage); + *currentlyDisplayedMessage = FindViewIndex(m_currentlyDisplayedMsgKey); + return NS_OK; +} + +// If nothing selected, return an NS_ERROR. +NS_IMETHODIMP +nsMsgDBView::GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) { + NS_ENSURE_ARG_POINTER(hdr); + + nsresult rv; + nsMsgKey key; + rv = GetKeyForFirstSelectedMessage(&key); + // Don't assert, it is legal for nothing to be selected. + if (NS_FAILED(rv)) return rv; + + if (key == nsMsgKey_None) { + *hdr = nullptr; + return NS_OK; + } + + if (!m_db) return NS_MSG_MESSAGE_NOT_FOUND; + + rv = m_db->GetMsgHdrForKey(key, hdr); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +// If nothing selected, return an NS_ERROR. +NS_IMETHODIMP +nsMsgDBView::GetURIForFirstSelectedMessage(nsACString& uri) { + nsresult rv; + nsMsgViewIndex viewIndex; + rv = GetViewIndexForFirstSelectedMsg(&viewIndex); + // Don't assert, it is legal for nothing to be selected. + if (NS_FAILED(rv)) return rv; + + return GetURIForViewIndex(viewIndex, uri); +} + +NS_IMETHODIMP +nsMsgDBView::OnDeleteCompleted(bool aSucceeded) { + if (m_deletingRows && aSucceeded) { + uint32_t numIndices = mIndicesToNoteChange.Length(); + if (numIndices && (mTree || mJSTree)) { + if (numIndices > 1) mIndicesToNoteChange.Sort(); + + // The call to NoteChange() has to happen after we are done removing the + // keys as NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + if (numIndices > 1) { + if (mTree) mTree->BeginUpdateBatch(); + if (mJSTree) mJSTree->BeginUpdateBatch(); + } + + for (uint32_t i = 0; i < numIndices; i++) + NoteChange(mIndicesToNoteChange[i], -1, + nsMsgViewNotificationCode::insertOrDelete); + + if (numIndices > 1) { + if (mTree) mTree->EndUpdateBatch(); + if (mJSTree) mJSTree->EndUpdateBatch(); + } + } + + mIndicesToNoteChange.Clear(); + } + + m_deletingRows = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetDb(nsIMsgDatabase** aDB) { + NS_ENSURE_ARG_POINTER(aDB); + NS_IF_ADDREF(*aDB = m_db); + return NS_OK; +} + +bool nsMsgDBView::OfflineMsgSelected( + nsTArray<nsMsgViewIndex> const& selection) { + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(m_folder); + if (localFolder) { + return true; + } + + for (nsMsgViewIndex viewIndex : selection) { + // For cross-folder saved searches, we need to check if any message + // is in a local folder. + if (!m_folder) { + nsCOMPtr<nsIMsgFolder> folder; + GetFolderForViewIndex(viewIndex, getter_AddRefs(folder)); + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(folder); + if (localFolder) { + return true; + } + } + + uint32_t flags = m_flags[viewIndex]; + if ((flags & nsMsgMessageFlags::Offline)) { + return true; + } + } + + return false; +} + +bool nsMsgDBView::NonDummyMsgSelected( + nsTArray<nsMsgViewIndex> const& selection) { + bool includeCollapsedMsgs = OperateOnMsgsInCollapsedThreads(); + + for (nsMsgViewIndex viewIndex : selection) { + uint32_t flags = m_flags[viewIndex]; + // We now treat having a collapsed dummy message selected as if + // the whole group was selected so we can apply commands to the group. + if (!(flags & MSG_VIEW_FLAG_DUMMY) || + (flags & nsMsgMessageFlags::Elided && includeCollapsedMsgs)) { + return true; + } + } + + return false; +} + +NS_IMETHODIMP +nsMsgDBView::GetViewIndexForFirstSelectedMsg(nsMsgViewIndex* aViewIndex) { + NS_ENSURE_ARG_POINTER(aViewIndex); + // If we don't have a tree selection we must be in stand alone mode... + if (!mTreeSelection) { + *aViewIndex = m_currentlyDisplayedViewIndex; + return NS_OK; + } + + int32_t startRange; + int32_t endRange; + nsresult rv = mTreeSelection->GetRangeAt(0, &startRange, &endRange); + // Don't assert, it is legal for nothing to be selected. + if (NS_FAILED(rv)) return rv; + + // Check that the first index is valid, it may not be if nothing is selected. + if (startRange < 0 || uint32_t(startRange) >= GetSize()) + return NS_ERROR_UNEXPECTED; + + *aViewIndex = startRange; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetKeyForFirstSelectedMessage(nsMsgKey* key) { + NS_ENSURE_ARG_POINTER(key); + // If we don't have a tree selection we must be in stand alone mode... + if (!mTreeSelection) { + *key = m_currentlyDisplayedMsgKey; + return NS_OK; + } + + int32_t selectionCount; + mTreeSelection->GetRangeCount(&selectionCount); + if (selectionCount == 0) { + *key = nsMsgKey_None; + return NS_OK; + } + + int32_t startRange; + int32_t endRange; + nsresult rv = mTreeSelection->GetRangeAt(0, &startRange, &endRange); + // Don't assert, it is legal for nothing to be selected. + if (NS_FAILED(rv)) return rv; + + // Check that the first index is valid, it may not be if nothing is selected. + if (startRange < 0 || uint32_t(startRange) >= GetSize()) + return NS_ERROR_UNEXPECTED; + + if (m_flags[startRange] & MSG_VIEW_FLAG_DUMMY) + return NS_MSG_INVALID_DBVIEW_INDEX; + + *key = m_keys[startRange]; + return NS_OK; +} + +nsCOMArray<nsIMsgFolder>* nsMsgDBView::GetFolders() { return nullptr; } + +nsresult nsMsgDBView::AdjustRowCount(int32_t rowCountBeforeSort, + int32_t rowCountAfterSort) { + int32_t rowChange = rowCountAfterSort - rowCountBeforeSort; + + if (rowChange) { + // This is not safe to use when you have a selection. + // RowCountChanged() will call AdjustSelection(). + uint32_t numSelected = 0; + GetNumSelected(&numSelected); + NS_ASSERTION( + numSelected == 0, + "it is not save to call AdjustRowCount() when you have a selection"); + + if (mTree) mTree->RowCountChanged(0, rowChange); + if (mJSTree) mJSTree->RowCountChanged(0, rowChange); + } + + return NS_OK; +} + +nsresult nsMsgDBView::GetImapDeleteModel(nsIMsgFolder* folder) { + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgIncomingServer> server; + // For the search view. + if (folder) + folder->GetServer(getter_AddRefs(server)); + else if (m_folder) + m_folder->GetServer(getter_AddRefs(server)); + + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server, &rv); + if (NS_SUCCEEDED(rv) && imapServer) imapServer->GetDeleteModel(&mDeleteModel); + + return rv; +} + +// +// CanDrop +// +// Can't drop on the thread pane. +// +NS_IMETHODIMP +nsMsgDBView::CanDrop(int32_t index, int32_t orient, + mozilla::dom::DataTransfer* dataTransfer, bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + *_retval = false; + + return NS_OK; +} + +// +// Drop +// +// Can't drop on the thread pane. +// +NS_IMETHODIMP +nsMsgDBView::Drop(int32_t row, int32_t orient, + mozilla::dom::DataTransfer* dataTransfer) { + return NS_OK; +} + +// +// IsSorted +// +// ... +// +NS_IMETHODIMP +nsMsgDBView::IsSorted(bool* _retval) { + *_retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SelectFolderMsgByKey(nsIMsgFolder* aFolder, nsMsgKey aKey) { + NS_ENSURE_ARG_POINTER(aFolder); + if (aKey == nsMsgKey_None) return NS_ERROR_FAILURE; + + // This is OK for non search views. + + nsMsgViewIndex viewIndex = FindKey(aKey, true /* expand */); + + if (mTree) mTreeSelection->SetCurrentIndex(viewIndex); + + // Make sure the current message is once again visible in the thread pane + // so we don't have to go search for it in the thread pane. + if (mTree && viewIndex != nsMsgViewIndex_None) { + mTreeSelection->Select(viewIndex); + mTree->EnsureRowIsVisible(viewIndex); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::SelectMsgByKey(nsMsgKey aKey) { + NS_ASSERTION(aKey != nsMsgKey_None, "bad key"); + if (aKey == nsMsgKey_None) return NS_OK; + + // Use SaveAndClearSelection() + // and RestoreSelection() so that we'll clear the current selection + // but pass in a different key array so that we'll + // select (and load) the desired message. + + AutoTArray<nsMsgKey, 1> preservedSelection; + nsresult rv = SaveAndClearSelection(nullptr, preservedSelection); + NS_ENSURE_SUCCESS(rv, rv); + + // Now, restore our desired selection. + AutoTArray<nsMsgKey, 1> keyArray; + keyArray.AppendElement(aKey); + + // If the key was not found + // (this can happen with "remember last selected message") + // nothing will be selected. + rv = RestoreSelection(aKey, keyArray); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, + nsIMsgDBView** _retval) { + nsMsgDBView* newMsgDBView = new nsMsgDBView(); + + nsresult rv = + CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*_retval = newMsgDBView); + return NS_OK; +} + +nsresult nsMsgDBView::CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) { + NS_ENSURE_ARG_POINTER(aNewMsgDBView); + if (aMsgWindow) { + aNewMsgDBView->mMsgWindowWeak = do_GetWeakReference(aMsgWindow); + aMsgWindow->SetOpenFolder(m_viewFolder ? m_viewFolder : m_folder); + } + + aNewMsgDBView->mMessengerWeak = do_GetWeakReference(aMessengerInstance); + aNewMsgDBView->mCommandUpdater = do_GetWeakReference(aCmdUpdater); + aNewMsgDBView->m_folder = m_folder; + aNewMsgDBView->m_viewFlags = m_viewFlags; + aNewMsgDBView->m_sortOrder = m_sortOrder; + aNewMsgDBView->m_sortType = m_sortType; + aNewMsgDBView->m_curCustomColumn = m_curCustomColumn; + aNewMsgDBView->m_secondarySort = m_secondarySort; + aNewMsgDBView->m_secondarySortOrder = m_secondarySortOrder; + aNewMsgDBView->m_secondaryCustomColumn = m_secondaryCustomColumn; + aNewMsgDBView->m_db = m_db; + if (m_db) aNewMsgDBView->m_db->AddListener(aNewMsgDBView); + + aNewMsgDBView->mIsNews = mIsNews; + aNewMsgDBView->mIsRss = mIsRss; + aNewMsgDBView->mIsXFVirtual = mIsXFVirtual; + aNewMsgDBView->mShowSizeInLines = mShowSizeInLines; + aNewMsgDBView->mDeleteModel = mDeleteModel; + aNewMsgDBView->m_flags = m_flags.Clone(); + aNewMsgDBView->m_levels = m_levels.Clone(); + aNewMsgDBView->m_keys = m_keys.Clone(); + + aNewMsgDBView->m_customColumnHandlerIDs = m_customColumnHandlerIDs.Clone(); + aNewMsgDBView->m_customColumnHandlers.AppendObjects(m_customColumnHandlers); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::GetSearchSession(nsIMsgSearchSession** aSession) { + NS_ASSERTION(false, "should be overridden by child class"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBView::SetSearchSession(nsIMsgSearchSession* aSession) { + NS_ASSERTION(false, "should be overridden by child class"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgDBView::GetSupportsThreading(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::FindIndexFromKey(nsMsgKey aMsgKey, bool aExpand, + nsMsgViewIndex* aIndex) { + NS_ENSURE_ARG_POINTER(aIndex); + *aIndex = FindKey(aMsgKey, aExpand); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgDBView::FindIndexOfMsgHdr(nsIMsgDBHdr* aMsgHdr, bool aExpand, + nsMsgViewIndex* aIndex) { + NS_ENSURE_ARG(aMsgHdr); + NS_ENSURE_ARG_POINTER(aIndex); + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + nsMsgViewIndex threadIndex = ThreadIndexOfMsgHdr(aMsgHdr); + if (threadIndex != nsMsgViewIndex_None) { + if (aExpand && (m_flags[threadIndex] & nsMsgMessageFlags::Elided)) + ExpandByIndex(threadIndex, nullptr); + + *aIndex = FindHdr(aMsgHdr, threadIndex); + } else { + *aIndex = nsMsgViewIndex_None; + } + } else { + *aIndex = FindHdr(aMsgHdr); + } + + return NS_OK; +} + +static void getDateFormatPref(nsIPrefBranch* _prefBranch, + const char* _prefLocalName, + nsDateFormatSelectorComm& _format) { + // Read. + int32_t nFormatSetting(0); + nsresult result = _prefBranch->GetIntPref(_prefLocalName, &nFormatSetting); + if (NS_SUCCEEDED(result)) { + // Translate. + nsDateFormatSelectorComm res; + res = static_cast<nsDateFormatSelectorComm>(nFormatSetting); + // Transfer if valid. + if (res >= kDateFormatNone && res <= kDateFormatShort) + _format = res; + else if (res == kDateFormatWeekday) + _format = res; + } +} + +nsresult nsMsgDBView::InitDisplayFormats() { + m_dateFormatsInitialized = true; + + nsresult rv = NS_OK; + nsCOMPtr<nsIPrefService> prefs = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIPrefBranch> dateFormatPrefs; + rv = prefs->GetBranch("mail.ui.display.dateformat.", + getter_AddRefs(dateFormatPrefs)); + NS_ENSURE_SUCCESS(rv, rv); + + getDateFormatPref(dateFormatPrefs, "default", m_dateFormatDefault); + getDateFormatPref(dateFormatPrefs, "thisweek", m_dateFormatThisWeek); + getDateFormatPref(dateFormatPrefs, "today", m_dateFormatToday); + return rv; +} + +void nsMsgDBView::SetMRUTimeForFolder(nsIMsgFolder* folder) { + uint32_t seconds; + PRTime2Seconds(PR_Now(), &seconds); + nsAutoCString nowStr; + nowStr.AppendInt(seconds); + folder->SetStringProperty(MRU_TIME_PROPERTY, nowStr); +} + +nsMsgDBView::nsMsgViewHdrEnumerator::nsMsgViewHdrEnumerator(nsMsgDBView* view) { + // We need to clone the view because the caller may clear the + // current view immediately. It also makes it easier to expand all + // if we're working on a copy. + nsCOMPtr<nsIMsgDBView> clonedView; + view->CloneDBView(nullptr, nullptr, nullptr, getter_AddRefs(clonedView)); + m_view = static_cast<nsMsgDBView*>(clonedView.get()); + // Make sure we enumerate over collapsed threads by expanding all. + m_view->ExpandAll(); + m_curHdrIndex = 0; +} + +nsMsgDBView::nsMsgViewHdrEnumerator::~nsMsgViewHdrEnumerator() { + if (m_view) m_view->Close(); +} + +NS_IMETHODIMP +nsMsgDBView::nsMsgViewHdrEnumerator::GetNext(nsIMsgDBHdr** aItem) { + NS_ENSURE_ARG_POINTER(aItem); + + if (m_curHdrIndex >= m_view->GetSize()) return NS_ERROR_FAILURE; + + // Ignore dummy header. We won't have empty groups, so + // we know the view index is good. + if (m_view->m_flags[m_curHdrIndex] & MSG_VIEW_FLAG_DUMMY) ++m_curHdrIndex; + + nsCOMPtr<nsIMsgDBHdr> nextHdr; + + nsresult rv = + m_view->GetMsgHdrForViewIndex(m_curHdrIndex++, getter_AddRefs(nextHdr)); + nextHdr.forget(aItem); + return rv; +} + +NS_IMETHODIMP +nsMsgDBView::nsMsgViewHdrEnumerator::HasMoreElements(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = m_curHdrIndex < m_view->GetSize(); + return NS_OK; +} + +nsresult nsMsgDBView::GetViewEnumerator(nsIMsgEnumerator** enumerator) { + NS_IF_ADDREF(*enumerator = new nsMsgViewHdrEnumerator(this)); + return (*enumerator) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +nsresult nsMsgDBView::GetDBForHeader(nsIMsgDBHdr* msgHdr, nsIMsgDatabase** db) { + nsCOMPtr<nsIMsgFolder> folder; + nsresult rv = msgHdr->GetFolder(getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + return folder->GetMsgDatabase(db); +} + +/** + * Determine whether junk commands should be enabled on this view. + * Junk commands are always enabled for mail. For nntp and rss, they + * may be selectively enabled using an inherited folder property. + * + * @param aViewIndex view index of the message to check + * @return true if junk controls should be enabled + */ +bool nsMsgDBView::JunkControlsEnabled(nsMsgViewIndex aViewIndex) { + // For normal mail, junk commands are always enabled. + if (!(mIsNews || mIsRss || mIsXFVirtual)) return true; + + // We need to check per message or folder. + nsCOMPtr<nsIMsgFolder> folder = m_folder; + if (!folder && IsValidIndex(aViewIndex)) + GetFolderForViewIndex(aViewIndex, getter_AddRefs(folder)); + + if (folder) { + // Check if this is a mail message in search folders. + if (mIsXFVirtual) { + nsCOMPtr<nsIMsgIncomingServer> server; + folder->GetServer(getter_AddRefs(server)); + nsAutoCString type; + if (server) server->GetType(type); + + if (!(type.LowerCaseEqualsLiteral("nntp") || + type.LowerCaseEqualsLiteral("rss"))) + return true; + } + + // For rss and news, check the inherited folder property. + nsAutoCString junkEnableOverride; + folder->GetInheritedStringProperty("dobayes.mailnews@mozilla.org#junk", + junkEnableOverride); + if (junkEnableOverride.EqualsLiteral("true")) return true; + } + + return false; +} diff --git a/comm/mailnews/base/src/nsMsgDBView.h b/comm/mailnews/base/src/nsMsgDBView.h new file mode 100644 index 0000000000..6033a490bc --- /dev/null +++ b/comm/mailnews/base/src/nsMsgDBView.h @@ -0,0 +1,558 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgDBView_H_ +#define _nsMsgDBView_H_ + +#include "nsIMsgDBView.h" +#include "nsIMsgWindow.h" +#include "nsIMessenger.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgHdr.h" +#include "MailNewsTypes.h" +#include "nsIDBChangeListener.h" +#include "nsITreeView.h" +#include "mozilla/dom/XULTreeElement.h" +#include "nsITreeSelection.h" +#include "nsIMsgFolder.h" +#include "nsIMsgThread.h" +#include "nsMsgUtils.h" +#include "nsIImapIncomingServer.h" +#include "nsIMsgFilterPlugin.h" +#include "nsIStringBundle.h" +#include "nsMsgTagService.h" +#include "nsCOMArray.h" +#include "nsTArray.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsIMsgCustomColumnHandler.h" +#include "nsIWeakReferenceUtils.h" +#include "nsMsgEnumerator.h" + +#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties" + +typedef AutoTArray<nsMsgViewIndex, 1> nsMsgViewIndexArray; +static_assert(nsMsgViewIndex(nsMsgViewIndexArray::NoIndex) == + nsMsgViewIndex_None, + "These need to be the same value."); + +enum eFieldType { kCollationKey, kU32 }; + +// This is used in an nsTArray<> to keep track of a multi-column sort. +class MsgViewSortColumnInfo { + public: + MsgViewSortColumnInfo(const MsgViewSortColumnInfo& other); + MsgViewSortColumnInfo() + : mSortType(nsMsgViewSortType::byNone), + mSortOrder(nsMsgViewSortOrder::none) {} + bool operator==(const MsgViewSortColumnInfo& other) const; + nsMsgViewSortTypeValue mSortType; + nsMsgViewSortOrderValue mSortOrder; + // If mSortType == byCustom, info about the custom column sort. + nsString mCustomColumnName; + nsCOMPtr<nsIMsgCustomColumnHandler> mColHandler; +}; + +// Reserve some bits in the msg flags for the view-only flags. +// NOTE: this bit space is shared by nsMsgMessageFlags (and labels). +#define MSG_VIEW_FLAG_ISTHREAD 0x8000000 +#define MSG_VIEW_FLAG_DUMMY 0x20000000 +#define MSG_VIEW_FLAG_HASCHILDREN 0x40000000 +#define MSG_VIEW_FLAGS \ + (MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_DUMMY | MSG_VIEW_FLAG_ISTHREAD) + +// Helper struct for sorting by numeric fields. +// Associates a message with a key for ordering it in the view. +struct IdUint32 { + nsMsgKey id; + uint32_t bits; + uint32_t dword; // The numeric key. + nsIMsgFolder* folder; +}; + +// Extends IdUint32 for sorting by a collation key field (eg subject). +// (Also used as IdUint32 a couple of places to simplify the code, where +// the overhead of an unused nsTArray isn't a big deal). +struct IdKey : public IdUint32 { + nsTArray<uint8_t> key; +}; + +class nsMsgDBViewService final : public nsIMsgDBViewService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGDBVIEWSERVICE + + nsMsgDBViewService(){}; + + protected: + ~nsMsgDBViewService(){}; +}; + +// This is an abstract implementation class. +// The actual view objects will be instances of sub-classes of this class. +class nsMsgDBView : public nsIMsgDBView, + public nsIDBChangeListener, + public nsITreeView, + public nsIJunkMailClassificationListener { + public: + friend class nsMsgDBViewService; + nsMsgDBView(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGDBVIEW + NS_DECL_NSIDBCHANGELISTENER + NS_DECL_NSITREEVIEW + NS_DECL_NSIJUNKMAILCLASSIFICATIONLISTENER + + nsMsgViewIndex GetInsertIndexHelper(nsIMsgDBHdr* msgHdr, + nsTArray<nsMsgKey>& keys, + nsCOMArray<nsIMsgFolder>* folders, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewSortTypeValue sortType); + int32_t SecondaryCompare(nsMsgKey key1, nsIMsgFolder* folder1, nsMsgKey key2, + nsIMsgFolder* folder2, + class viewSortInfo* comparisonContext); + + protected: + virtual ~nsMsgDBView(); + + static nsString kHighestPriorityString; + static nsString kHighPriorityString; + static nsString kLowestPriorityString; + static nsString kLowPriorityString; + static nsString kNormalPriorityString; + + static nsString kReadString; + static nsString kRepliedString; + static nsString kForwardedString; + static nsString kRedirectedString; + static nsString kNewString; + + // Used for group views. + static nsString kTodayString; + static nsString kYesterdayString; + static nsString kLastWeekString; + static nsString kTwoWeeksAgoString; + static nsString kOldMailString; + static nsString kFutureDateString; + + RefPtr<mozilla::dom::XULTreeElement> mTree; + nsCOMPtr<nsIMsgJSTree> mJSTree; + nsCOMPtr<nsITreeSelection> mTreeSelection; + // We cache this to determine when to push command status notifications. + uint32_t mNumSelectedRows; + // Set when the message pane is collapsed. + bool mSuppressMsgDisplay; + bool mSuppressCommandUpdating; + // Set when we're telling the outline a row is being removed. Used to + // suppress msg loading during delete/move operations. + bool mRemovingRow; + bool mCommandsNeedDisablingBecauseOfSelection; + bool mSuppressChangeNotification; + + virtual const char* GetViewName(void) { return "MsgDBView"; } + nsresult FetchAuthor(nsIMsgDBHdr* aHdr, nsAString& aAuthorString); + nsresult FetchRecipients(nsIMsgDBHdr* aHdr, nsAString& aRecipientsString); + nsresult FetchSubject(nsIMsgDBHdr* aMsgHdr, uint32_t aFlags, + nsAString& aValue); + nsresult FetchDate(nsIMsgDBHdr* aHdr, nsAString& aDateString, + bool rcvDate = false); + nsresult FetchStatus(uint32_t aFlags, nsAString& aStatusString); + nsresult FetchSize(nsIMsgDBHdr* aHdr, nsAString& aSizeString); + nsresult FetchPriority(nsIMsgDBHdr* aHdr, nsAString& aPriorityString); + nsresult FetchLabel(nsIMsgDBHdr* aHdr, nsAString& aLabelString); + nsresult FetchTags(nsIMsgDBHdr* aHdr, nsAString& aTagString); + nsresult FetchKeywords(nsIMsgDBHdr* aHdr, nsACString& keywordString); + nsresult FetchRowKeywords(nsMsgViewIndex aRow, nsIMsgDBHdr* aHdr, + nsACString& keywordString); + nsresult FetchAccount(nsIMsgDBHdr* aHdr, nsAString& aAccount); + bool IsOutgoingMsg(nsIMsgDBHdr* aHdr); + + // The default enumerator is over the db, but things like + // quick search views will enumerate just the displayed messages. + virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator); + // this is a message enumerator that enumerates based on the view contents + virtual nsresult GetViewEnumerator(nsIMsgEnumerator** enumerator); + + // Save and Restore Selection are a pair of routines you should + // use when performing an operation which is going to change the view + // and you want to remember the selection. (i.e. for sorting). + // Call SaveAndClearSelection and we'll give you an array of msg keys for + // the current selection. We also freeze and clear the selection. + // When you are done changing the view, + // call RestoreSelection passing in the same array + // and we'll restore the selection AND unfreeze selection in the UI. + nsresult SaveAndClearSelection(nsMsgKey* aCurrentMsgKey, + nsTArray<nsMsgKey>& aMsgKeyArray); + nsresult RestoreSelection(nsMsgKey aCurrentmsgKey, + nsTArray<nsMsgKey>& aMsgKeyArray); + + // This is not safe to use when you have a selection. + // RowCountChanged() will call AdjustSelection(). + // It should be called after SaveAndClearSelection() and before + // RestoreSelection(). + nsresult AdjustRowCount(int32_t rowCountBeforeSort, + int32_t rowCountAfterSort); + + nsresult GenerateURIForMsgKey(nsMsgKey aMsgKey, nsIMsgFolder* folder, + nsACString& aURI); + + // Routines used in building up view. + virtual bool WantsThisThread(nsIMsgThread* thread); + virtual nsresult AddHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex* resultIndex = nullptr); + bool GetShowingIgnored() { + return (m_viewFlags & nsMsgViewFlagsType::kShowIgnored) != 0; + } + bool OperateOnMsgsInCollapsedThreads(); + + virtual nsresult OnNewHeader(nsIMsgDBHdr* aNewHdr, nsMsgKey parentKey, + bool ensureListed); + virtual nsMsgViewIndex GetInsertIndex(nsIMsgDBHdr* msgHdr); + nsMsgViewIndex GetIndexForThread(nsIMsgDBHdr* hdr); + nsMsgViewIndex GetThreadRootIndex(nsIMsgDBHdr* msgHdr); + virtual nsresult GetMsgHdrForViewIndex(nsMsgViewIndex index, + nsIMsgDBHdr** msgHdr); + // Given a view index, return the index of the top-level msg in the thread. + nsMsgViewIndex GetThreadIndex(nsMsgViewIndex msgIndex); + + virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr, + nsMsgKey msgKey, uint32_t flags, uint32_t level); + virtual void SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index, + nsMsgKey msgKey, uint32_t flags, uint32_t level); + virtual void InsertEmptyRows(nsMsgViewIndex viewIndex, int32_t numRows); + virtual void RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows); + nsresult ToggleExpansion(nsMsgViewIndex index, uint32_t* numChanged); + nsresult ExpandByIndex(nsMsgViewIndex index, uint32_t* pNumExpanded); + nsresult CollapseByIndex(nsMsgViewIndex index, uint32_t* pNumCollapsed); + nsresult ExpandAll(); + nsresult CollapseAll(); + nsresult ExpandAndSelectThread(); + + // Helper routines for thread expanding and collapsing. + nsresult GetThreadCount(nsMsgViewIndex viewIndex, uint32_t* pThreadCount); + /** + * Retrieve the view index of the first displayed message in the given thread. + * @param threadHdr The thread you care about. + * @param allowDummy Should dummy headers be returned when the non-dummy + * header is available? If the root node of the thread is a dummy header + * and you pass false, then we will return the first child of the thread + * unless the thread is elided, in which case we will return the root. + * If you pass true, we will always return the root. + * @return the view index of the first message in the thread, if any. + */ + nsMsgViewIndex GetIndexOfFirstDisplayedKeyInThread(nsIMsgThread* threadHdr, + bool allowDummy = false); + virtual nsresult GetFirstMessageHdrToDisplayInThread(nsIMsgThread* threadHdr, + nsIMsgDBHdr** result); + virtual nsMsgViewIndex ThreadIndexOfMsg( + nsMsgKey msgKey, nsMsgViewIndex msgIndex = nsMsgViewIndex_None, + int32_t* pThreadCount = nullptr, uint32_t* pFlags = nullptr); + nsMsgViewIndex ThreadIndexOfMsgHdr( + nsIMsgDBHdr* msgHdr, nsMsgViewIndex msgIndex = nsMsgViewIndex_None, + int32_t* pThreadCount = nullptr, uint32_t* pFlags = nullptr); + nsMsgKey GetKeyOfFirstMsgInThread(nsMsgKey key); + int32_t CountExpandedThread(nsMsgViewIndex index); + virtual nsresult ExpansionDelta(nsMsgViewIndex index, + int32_t* expansionDelta); + void ReverseSort(); + void ReverseThreads(); + nsresult SaveSortInfo(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder); + nsresult RestoreSortInfo(); + nsresult PersistFolderInfo(nsIDBFolderInfo** dbFolderInfo); + void SetMRUTimeForFolder(nsIMsgFolder* folder); + + nsMsgKey GetAt(nsMsgViewIndex index) { + return m_keys.SafeElementAt(index, nsMsgKey_None); + } + + nsMsgViewIndex FindViewIndex(nsMsgKey key) { return FindKey(key, false); } + /** + * Find the message header if it is visible in this view. (Messages in + * threads/groups that are elided will not be + * @param msgHdr Message header to look for. + * @param startIndex The index to start looking from. + * @param allowDummy Are dummy headers acceptable? If yes, then for a group + * with a dummy header, we return the root of the thread (the dummy + * header), otherwise we return the actual "content" header for the + * message. + * @return The view index of the header found, if any. + */ + virtual nsMsgViewIndex FindHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startIndex = 0, + bool allowDummy = false); + virtual nsMsgViewIndex FindKey(nsMsgKey key, bool expand); + virtual nsresult GetDBForViewIndex(nsMsgViewIndex index, nsIMsgDatabase** db); + virtual nsCOMArray<nsIMsgFolder>* GetFolders(); + virtual nsresult GetFolderFromMsgURI(const nsACString& aMsgURI, + nsIMsgFolder** aFolder); + + virtual nsresult ListIdsInThread(nsIMsgThread* threadHdr, + nsMsgViewIndex viewIndex, + uint32_t* pNumListed); + nsresult ListUnreadIdsInThread(nsIMsgThread* threadHdr, + nsMsgViewIndex startOfThreadViewIndex, + uint32_t* pNumListed); + nsMsgViewIndex FindParentInThread(nsMsgKey parentKey, + nsMsgViewIndex startOfThreadViewIndex); + virtual nsresult ListIdsInThreadOrder(nsIMsgThread* threadHdr, + nsMsgKey parentKey, uint32_t level, + nsMsgViewIndex* viewIndex, + uint32_t* pNumListed); + uint32_t GetSize(void) { return (m_keys.Length()); } + + // For commands. + virtual nsresult ApplyCommandToIndicesWithFolder( + nsMsgViewCommandTypeValue command, + nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder); + virtual nsresult CopyMessages(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection, + bool isMove, nsIMsgFolder* destFolder); + virtual nsresult DeleteMessages(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection, + bool deleteStorage); + nsresult GetHeadersFromSelection(nsTArray<nsMsgViewIndex> const& selection, + nsTArray<RefPtr<nsIMsgDBHdr>>& hdrs); + // ListCollapsedChildren() adds to messageArray (rather than replacing it). + virtual nsresult ListCollapsedChildren( + nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray); + + nsresult SetMsgHdrJunkStatus(nsIJunkMailPlugin* aJunkPlugin, + nsIMsgDBHdr* aMsgHdr, + nsMsgJunkStatus aNewClassification); + nsresult ToggleReadByIndex(nsMsgViewIndex index); + nsresult SetReadByIndex(nsMsgViewIndex index, bool read); + nsresult SetThreadOfMsgReadByIndex(nsMsgViewIndex index, + nsTArray<nsMsgKey>& keysMarkedRead, + bool read); + nsresult SetFlaggedByIndex(nsMsgViewIndex index, bool mark); + nsresult OrExtraFlag(nsMsgViewIndex index, uint32_t orflag); + nsresult AndExtraFlag(nsMsgViewIndex index, uint32_t andflag); + nsresult SetExtraFlag(nsMsgViewIndex index, uint32_t extraflag); + virtual nsresult RemoveByIndex(nsMsgViewIndex index); + virtual void OnExtraFlagChanged(nsMsgViewIndex /*index*/, + uint32_t /*extraFlag*/) {} + virtual void OnHeaderAddedOrDeleted() {} + nsresult ToggleWatched(nsTArray<nsMsgViewIndex> const& selection); + nsresult SetThreadWatched(nsIMsgThread* thread, nsMsgViewIndex index, + bool watched); + nsresult SetThreadIgnored(nsIMsgThread* thread, nsMsgViewIndex threadIndex, + bool ignored); + nsresult SetSubthreadKilled(nsIMsgDBHdr* header, nsMsgViewIndex msgIndex, + bool ignored); + nsresult DownloadForOffline(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection); + nsresult DownloadFlaggedForOffline(nsIMsgWindow* window); + nsMsgViewIndex GetThreadFromMsgIndex(nsMsgViewIndex index, + nsIMsgThread** threadHdr); + /// Should junk commands be enabled for the current message in the view? + bool JunkControlsEnabled(nsMsgViewIndex aViewIndex); + + // For sorting. + nsresult GetFieldTypeAndLenForSort( + nsMsgViewSortTypeValue sortType, uint16_t* pMaxLen, + eFieldType* pFieldType, nsIMsgCustomColumnHandler* colHandler = nullptr); + nsresult GetCollationKey(nsIMsgDBHdr* msgHdr, nsMsgViewSortTypeValue sortType, + nsTArray<uint8_t>& result, + nsIMsgCustomColumnHandler* colHandler = nullptr); + nsresult GetLongField(nsIMsgDBHdr* msgHdr, nsMsgViewSortTypeValue sortType, + uint32_t* result, + nsIMsgCustomColumnHandler* colHandler = nullptr); + + static int FnSortIdKey(const IdKey* pItem1, const IdKey* pItem2, + viewSortInfo* sortInfo); + static int FnSortIdUint32(const IdUint32* pItem1, const IdUint32* pItem2, + viewSortInfo* sortInfo); + + nsresult GetStatusSortValue(nsIMsgDBHdr* msgHdr, uint32_t* result); + nsresult GetLocationCollationKey(nsIMsgDBHdr* msgHdr, + nsTArray<uint8_t>& result); + void PushSort(const MsgViewSortColumnInfo& newSort); + nsresult EncodeColumnSort(nsString& columnSortString); + nsresult DecodeColumnSort(nsString& columnSortString); + // For view navigation. + nsresult NavigateFromPos(nsMsgNavigationTypeValue motion, + nsMsgViewIndex startIndex, nsMsgKey* pResultKey, + nsMsgViewIndex* pResultIndex, + nsMsgViewIndex* pThreadIndex, bool wrap); + nsresult FindNextFlagged(nsMsgViewIndex startIndex, + nsMsgViewIndex* pResultIndex); + nsresult FindFirstNew(nsMsgViewIndex* pResultIndex); + nsresult FindPrevUnread(nsMsgKey startKey, nsMsgKey* pResultKey, + nsMsgKey* resultThreadId); + nsresult FindFirstFlagged(nsMsgViewIndex* pResultIndex); + nsresult FindPrevFlagged(nsMsgViewIndex startIndex, + nsMsgViewIndex* pResultIndex); + nsresult MarkThreadOfMsgRead(nsMsgKey msgId, nsMsgViewIndex msgIndex, + nsTArray<nsMsgKey>& idsMarkedRead, bool bRead); + nsresult MarkThreadRead(nsIMsgThread* threadHdr, nsMsgViewIndex threadIndex, + nsTArray<nsMsgKey>& idsMarkedRead, bool bRead); + bool IsValidIndex(nsMsgViewIndex index); + nsresult ToggleIgnored(nsTArray<nsMsgViewIndex> const& selection, + nsMsgViewIndex* resultIndex, bool* resultToggleState); + nsresult ToggleMessageKilled(nsTArray<nsMsgViewIndex> const& selection, + nsMsgViewIndex* resultIndex, + bool* resultToggleState); + bool OfflineMsgSelected(nsTArray<nsMsgViewIndex> const& selection); + bool NonDummyMsgSelected(nsTArray<nsMsgViewIndex> const& selection); + static void GetString(const char16_t* aStringName, nsAString& aValue); + static nsresult GetPrefLocalizedString(const char* aPrefName, + nsString& aResult); + nsresult AppendKeywordProperties(const nsACString& keywords, + nsAString& properties, bool* tagAdded); + static nsresult InitLabelStrings(void); + nsresult CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater); + static void InitializeLiterals(); + virtual int32_t FindLevelInThread(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startOfThread, + nsMsgViewIndex viewIndex); + nsresult GetImapDeleteModel(nsIMsgFolder* folder); + nsresult UpdateDisplayMessage(nsMsgViewIndex viewPosition); + nsresult GetDBForHeader(nsIMsgDBHdr* msgHdr, nsIMsgDatabase** db); + + bool AdjustReadFlag(nsIMsgDBHdr* msgHdr, uint32_t* msgFlags); + void FreeAll(nsTArray<void*>* ptrs); + void ClearHdrCache(); + + // The message held in each row. + nsTArray<nsMsgKey> m_keys; + // Flags for each row, combining nsMsgMessageFlags and MSG_VIEW_FLAGS. + nsTArray<uint32_t> m_flags; + // Threading level of each row (1=top) + nsTArray<uint8_t> m_levels; + + nsMsgImapDeleteModel mDeleteModel; + + // Cache the most recently asked for header and corresponding msgKey. + nsCOMPtr<nsIMsgDBHdr> m_cachedHdr; + nsMsgKey m_cachedMsgKey; + + // We need to store the message key for the message we are currently + // displaying to ensure we don't try to redisplay the same message just + // because the selection changed (i.e. after a sort). + nsMsgKey m_currentlyDisplayedMsgKey; + nsCString m_currentlyDisplayedMsgUri; + nsMsgViewIndex m_currentlyDisplayedViewIndex; + // If we're deleting messages, we want to hold off loading messages on + // selection changed until the delete is done and we want to batch + // notifications. + bool m_deletingRows; + // For certain special folders and descendants of those folders + // (like the "Sent" folder, "Sent/Old Sent"). + // The Sender column really shows recipients. + + // Server types for this view's folder + bool mIsNews; // We have special icons for news. + bool mIsRss; // RSS affects enabling of junk commands. + bool mIsXFVirtual; // A virtual folder with multiple folders. + + bool mShowSizeInLines; // For news we show lines instead of size when true. + bool mSortThreadsByRoot; // As opposed to by the newest message. + bool m_sortValid; + bool m_checkedCustomColumns; + bool mSelectionSummarized; + // We asked the front end to summarize the selection and it did not. + bool mSummarizeFailed; + uint8_t m_saveRestoreSelectionDepth; + + nsCOMPtr<nsIMsgDatabase> m_db; + nsCOMPtr<nsIMsgFolder> m_folder; + // For virtual folders, the VF db. + nsCOMPtr<nsIMsgFolder> m_viewFolder; + nsString mMessageType; + nsTArray<MsgViewSortColumnInfo> m_sortColumns; + nsMsgViewSortTypeValue m_sortType; + nsMsgViewSortOrderValue m_sortOrder; + nsString m_curCustomColumn; + nsMsgViewSortTypeValue m_secondarySort; + nsMsgViewSortOrderValue m_secondarySortOrder; + nsString m_secondaryCustomColumn; + nsMsgViewFlagsTypeValue m_viewFlags; + + // I18N date formatter service which we'll want to cache locally. + nsCOMPtr<nsIMsgTagService> mTagService; + nsWeakPtr mMessengerWeak; + nsWeakPtr mMsgWindowWeak; + // We push command update notifications to the UI from this. + nsWeakPtr mCommandUpdater; + static nsCOMPtr<nsIStringBundle> mMessengerStringBundle; + + // Used to determine when to start and end junk plugin batches. + uint32_t mNumMessagesRemainingInBatch; + + // These are the headers of the messages in the current + // batch/series of batches of messages manually marked + // as junk. + nsTArray<RefPtr<nsIMsgDBHdr>> mJunkHdrs; + + nsTArray<uint32_t> mIndicesToNoteChange; + + nsTHashtable<nsCStringHashKey> mEmails; + + // The saved search views keep track of the XX most recently deleted msg ids, + // so that if the delete is undone, we can add the msg back to the search + // results, even if it no longer matches the search criteria (e.g., a saved + // search over unread messages). We use mRecentlyDeletedArrayIndex to treat + // the array as a list of the XX most recently deleted msgs. + nsTArray<nsCString> mRecentlyDeletedMsgIds; + uint32_t mRecentlyDeletedArrayIndex; + void RememberDeletedMsgHdr(nsIMsgDBHdr* msgHdr); + bool WasHdrRecentlyDeleted(nsIMsgDBHdr* msgHdr); + + // These hold pointers (and IDs) for the nsIMsgCustomColumnHandler object + // that constitutes the custom column handler. + nsCOMArray<nsIMsgCustomColumnHandler> m_customColumnHandlers; + nsTArray<nsString> m_customColumnHandlerIDs; + + nsIMsgCustomColumnHandler* GetColumnHandler(const nsAString& colID); + nsIMsgCustomColumnHandler* GetCurColumnHandler(); + bool CustomColumnsInSortAndNotRegistered(); + void EnsureCustomColumnsValid(); + +#ifdef DEBUG_David_Bienvenu + void InitEntryInfoForIndex(nsMsgViewIndex i, IdKey& EntryInfo); + void ValidateSort(); +#endif + + protected: + static nsresult InitDisplayFormats(); + + private: + static bool m_dateFormatsInitialized; + static nsDateFormatSelectorComm m_dateFormatDefault; + static nsDateFormatSelectorComm m_dateFormatThisWeek; + static nsDateFormatSelectorComm m_dateFormatToday; + static nsString m_connectorPattern; + + bool ServerSupportsFilterAfterTheFact(); + + nsresult PerformActionsOnJunkMsgs(bool msgsAreJunk); + nsresult DetermineActionsForJunkChange(bool msgsAreJunk, + nsIMsgFolder* srcFolder, + bool& moveMessages, + bool& changeReadState, + nsIMsgFolder** targetFolder); + + class nsMsgViewHdrEnumerator final : public nsBaseMsgEnumerator { + public: + explicit nsMsgViewHdrEnumerator(nsMsgDBView* view); + + // nsIMsgEnumerator support. + NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override; + NS_IMETHOD HasMoreElements(bool* aResult) override; + + RefPtr<nsMsgDBView> m_view; + nsMsgViewIndex m_curHdrIndex; + + private: + virtual ~nsMsgViewHdrEnumerator() override; + }; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgEnumerator.cpp b/comm/mailnews/base/src/nsMsgEnumerator.cpp new file mode 100644 index 0000000000..5763a8d4dc --- /dev/null +++ b/comm/mailnews/base/src/nsMsgEnumerator.cpp @@ -0,0 +1,138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgEnumerator.h" + +#include "mozilla/dom/IteratorResultBinding.h" +#include "mozilla/dom/RootedDictionary.h" +#include "mozilla/dom/ToJSValue.h" +#include "mozilla/ResultExtensions.h" +#include "nsContentUtils.h" + +#include "nsIMsgHdr.h" +#include "nsIMsgThread.h" + +using namespace mozilla; +using namespace mozilla::dom; + +/** + * Internal class to support iteration over nsBaseMsgEnumerator in javascript. + */ +class JSMsgIterator final : public nsIJSIterator { + NS_DECL_ISUPPORTS + NS_DECL_NSIJSITERATOR + + explicit JSMsgIterator(nsBaseMsgEnumerator* aEnumerator) + : mEnumerator(aEnumerator) {} + + private: + ~JSMsgIterator() = default; + RefPtr<nsBaseMsgEnumerator> mEnumerator; +}; + +NS_IMETHODIMP JSMsgIterator::Next(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + // result is object of the form: {value: ..., done: ...} + RootedDictionary<IteratorResult> result(aCx); + + // We're really using the enumerator itself as the iterator. + nsCOMPtr<nsIMsgDBHdr> msg; + if (NS_FAILED(mEnumerator->GetNext(getter_AddRefs(msg)))) { + result.mDone = true; + // Leave value unset. + } else { + result.mDone = false; + + JS::Rooted<JS::Value> value(aCx); + MOZ_TRY( + nsContentUtils::WrapNative(aCx, msg, &NS_GET_IID(nsIMsgDBHdr), &value)); + result.mValue = value; + } + + if (!ToJSValue(aCx, result, aResult)) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; +} + +NS_IMPL_ISUPPORTS(JSMsgIterator, nsIJSIterator) + +// nsBaseMsgEnumerator implementation. + +NS_IMETHODIMP nsBaseMsgEnumerator::Iterator(nsIJSIterator** aResult) { + auto result = MakeRefPtr<JSMsgIterator>(this); + result.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP nsBaseMsgEnumerator::GetNext(nsIMsgDBHdr** aItem) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsBaseMsgEnumerator::HasMoreElements(bool* aResult) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMPL_ISUPPORTS(nsBaseMsgEnumerator, nsIMsgEnumerator) + +/** + * Internal class to support iteration over nsBaseMsgThreadEnumerator in + * javascript. + */ +class JSThreadIterator final : public nsIJSIterator { + NS_DECL_ISUPPORTS + NS_DECL_NSIJSITERATOR + + explicit JSThreadIterator(nsBaseMsgThreadEnumerator* aEnumerator) + : mEnumerator(aEnumerator) {} + + private: + ~JSThreadIterator() = default; + RefPtr<nsBaseMsgThreadEnumerator> mEnumerator; +}; + +NS_IMETHODIMP JSThreadIterator::Next(JSContext* aCx, + JS::MutableHandle<JS::Value> aResult) { + // result is object of the form: {value: ..., done: ...} + RootedDictionary<IteratorResult> result(aCx); + + // We're really using the enumerator itself as the iterator. + nsCOMPtr<nsIMsgThread> msg; + if (NS_FAILED(mEnumerator->GetNext(getter_AddRefs(msg)))) { + result.mDone = true; + // Leave value unset. + } else { + result.mDone = false; + + JS::Rooted<JS::Value> value(aCx); + MOZ_TRY(nsContentUtils::WrapNative(aCx, msg, &NS_GET_IID(nsIMsgThread), + &value)); + result.mValue = value; + } + + if (!ToJSValue(aCx, result, aResult)) { + return NS_ERROR_OUT_OF_MEMORY; + } + return NS_OK; +} + +NS_IMPL_ISUPPORTS(JSThreadIterator, nsIJSIterator) + +// nsBaseMsgThreadEnumerator implementation. + +NS_IMETHODIMP nsBaseMsgThreadEnumerator::Iterator(nsIJSIterator** aResult) { + auto result = MakeRefPtr<JSThreadIterator>(this); + result.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP nsBaseMsgThreadEnumerator::GetNext(nsIMsgThread** aItem) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsBaseMsgThreadEnumerator::HasMoreElements(bool* aResult) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMPL_ISUPPORTS(nsBaseMsgThreadEnumerator, nsIMsgThreadEnumerator) diff --git a/comm/mailnews/base/src/nsMsgEnumerator.h b/comm/mailnews/base/src/nsMsgEnumerator.h new file mode 100644 index 0000000000..306c3e3d72 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgEnumerator.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgEnumerator_H_ +#define _nsMsgEnumerator_H_ + +#include "nsIMsgEnumerator.h" + +/** + * A base implementation nsIMsgEnumerator for stepping over an ordered set + * of nsIMsgDBHdr objects. + * This provides the javascript iterable protocol (to support for...of + * constructs), but getNext() and hasMoreElements() must be implemented by + * derived classes. + */ +class nsBaseMsgEnumerator : public nsIMsgEnumerator { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGENUMERATOR + nsBaseMsgEnumerator(){}; + + protected: + virtual ~nsBaseMsgEnumerator(){}; +}; + +/** + * A base implementation nsIMsgThreadEnumerator for stepping over an ordered + * set of nsIMsgThread objects. + * This provides the javascript iterable protocol (to support for...of + * constructs), but getNext() and hasMoreElements() must be implemented by + * derived classes. + */ +class nsBaseMsgThreadEnumerator : public nsIMsgThreadEnumerator { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGTHREADENUMERATOR + nsBaseMsgThreadEnumerator(){}; + + protected: + virtual ~nsBaseMsgThreadEnumerator(){}; +}; + +#endif /* _nsMsgEnumerator_H_ */ diff --git a/comm/mailnews/base/src/nsMsgFileStream.cpp b/comm/mailnews/base/src/nsMsgFileStream.cpp new file mode 100644 index 0000000000..9e18ae70f8 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgFileStream.cpp @@ -0,0 +1,190 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsIFile.h" +#include "nsMsgFileStream.h" +#include "prerr.h" +#include "prerror.h" + +/* From nsDebugImpl.cpp: */ +static nsresult ErrorAccordingToNSPR() { + PRErrorCode err = PR_GetError(); + switch (err) { + case PR_OUT_OF_MEMORY_ERROR: + return NS_ERROR_OUT_OF_MEMORY; + case PR_WOULD_BLOCK_ERROR: + return NS_BASE_STREAM_WOULD_BLOCK; + case PR_FILE_NOT_FOUND_ERROR: + return NS_ERROR_FILE_NOT_FOUND; + case PR_READ_ONLY_FILESYSTEM_ERROR: + return NS_ERROR_FILE_READ_ONLY; + case PR_NOT_DIRECTORY_ERROR: + return NS_ERROR_FILE_NOT_DIRECTORY; + case PR_IS_DIRECTORY_ERROR: + return NS_ERROR_FILE_IS_DIRECTORY; + case PR_LOOP_ERROR: + return NS_ERROR_FILE_UNRESOLVABLE_SYMLINK; + case PR_FILE_EXISTS_ERROR: + return NS_ERROR_FILE_ALREADY_EXISTS; + case PR_FILE_IS_LOCKED_ERROR: + return NS_ERROR_FILE_IS_LOCKED; + case PR_FILE_TOO_BIG_ERROR: + return NS_ERROR_FILE_TOO_BIG; + case PR_NO_DEVICE_SPACE_ERROR: + return NS_ERROR_FILE_NO_DEVICE_SPACE; + case PR_NAME_TOO_LONG_ERROR: + return NS_ERROR_FILE_NAME_TOO_LONG; + case PR_DIRECTORY_NOT_EMPTY_ERROR: + return NS_ERROR_FILE_DIR_NOT_EMPTY; + case PR_NO_ACCESS_RIGHTS_ERROR: + return NS_ERROR_FILE_ACCESS_DENIED; + default: + return NS_ERROR_FAILURE; + } +} + +nsMsgFileStream::nsMsgFileStream() { + mFileDesc = nullptr; + mSeekedToEnd = false; +} + +nsMsgFileStream::~nsMsgFileStream() { + if (mFileDesc) PR_Close(mFileDesc); +} + +NS_IMPL_ISUPPORTS(nsMsgFileStream, nsIInputStream, nsIOutputStream, + nsITellableStream, nsISeekableStream) + +nsresult nsMsgFileStream::InitWithFile(nsIFile* file) { + return file->OpenNSPRFileDesc(PR_RDWR | PR_CREATE_FILE, 0664, &mFileDesc); +} + +NS_IMETHODIMP +nsMsgFileStream::Seek(int32_t whence, int64_t offset) { + if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED; + + bool seekingToEnd = whence == PR_SEEK_END && offset == 0; + if (seekingToEnd && mSeekedToEnd) return NS_OK; + + int64_t cnt = PR_Seek64(mFileDesc, offset, (PRSeekWhence)whence); + if (cnt == int64_t(-1)) { + return ErrorAccordingToNSPR(); + } + + mSeekedToEnd = seekingToEnd; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFileStream::Tell(int64_t* result) { + if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED; + + int64_t cnt = PR_Seek64(mFileDesc, 0, PR_SEEK_CUR); + if (cnt == int64_t(-1)) { + return ErrorAccordingToNSPR(); + } + *result = cnt; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFileStream::SetEOF() { + if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED; + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* void close (); */ +NS_IMETHODIMP nsMsgFileStream::Close() { + nsresult rv = NS_OK; + if (mFileDesc && (PR_Close(mFileDesc) == PR_FAILURE)) + rv = NS_BASE_STREAM_OSERROR; + mFileDesc = nullptr; + return rv; +} + +/* unsigned long long available (); */ +NS_IMETHODIMP nsMsgFileStream::Available(uint64_t* aResult) { + if (!mFileDesc) return NS_BASE_STREAM_CLOSED; + + int64_t avail = PR_Available64(mFileDesc); + if (avail == -1) return ErrorAccordingToNSPR(); + + *aResult = avail; + return NS_OK; +} + +/* [noscript] unsigned long read (in charPtr aBuf, in unsigned long aCount); */ +NS_IMETHODIMP nsMsgFileStream::Read(char* aBuf, uint32_t aCount, + uint32_t* aResult) { + if (!mFileDesc) { + *aResult = 0; + return NS_OK; + } + + int32_t bytesRead = PR_Read(mFileDesc, aBuf, aCount); + if (bytesRead == -1) return ErrorAccordingToNSPR(); + + *aResult = bytesRead; + return NS_OK; +} + +/* [noscript] unsigned long readSegments (in nsWriteSegmentFun aWriter, in + * voidPtr aClosure, in unsigned long aCount); */ +NS_IMETHODIMP nsMsgFileStream::ReadSegments(nsWriteSegmentFun aWriter, + void* aClosure, uint32_t aCount, + uint32_t* _retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/* boolean isNonBlocking (); */ +NS_IMETHODIMP nsMsgFileStream::IsNonBlocking(bool* aNonBlocking) { + *aNonBlocking = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFileStream::Write(const char* buf, uint32_t count, uint32_t* result) { + if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED; + + int32_t cnt = PR_Write(mFileDesc, buf, count); + if (cnt == -1) { + return ErrorAccordingToNSPR(); + } + *result = cnt; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFileStream::Flush(void) { + if (mFileDesc == nullptr) return NS_BASE_STREAM_CLOSED; + + int32_t cnt = PR_Sync(mFileDesc); + if (cnt == -1) return ErrorAccordingToNSPR(); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFileStream::WriteFrom(nsIInputStream* inStr, uint32_t count, + uint32_t* _retval) { + MOZ_ASSERT_UNREACHABLE("WriteFrom (see source comment)"); + return NS_ERROR_NOT_IMPLEMENTED; + // File streams intentionally do not support this method. + // If you need something like this, then you should wrap + // the file stream using nsIBufferedOutputStream +} + +NS_IMETHODIMP +nsMsgFileStream::WriteSegments(nsReadSegmentFun reader, void* closure, + uint32_t count, uint32_t* _retval) { + MOZ_ASSERT_UNREACHABLE("WriteSegments (see source comment)"); + return NS_ERROR_NOT_IMPLEMENTED; + // File streams intentionally do not support this method. + // If you need something like this, then you should wrap + // the file stream using nsIBufferedOutputStream +} + +NS_IMETHODIMP nsMsgFileStream::StreamStatus() { + return mFileDesc ? NS_OK : NS_BASE_STREAM_CLOSED; +} diff --git a/comm/mailnews/base/src/nsMsgFileStream.h b/comm/mailnews/base/src/nsMsgFileStream.h new file mode 100644 index 0000000000..11b5c24e91 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgFileStream.h @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Attributes.h" +#include "msgCore.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsISeekableStream.h" +#include "prio.h" + +class nsMsgFileStream final : public nsIInputStream, + public nsIOutputStream, + public nsISeekableStream { + public: + nsMsgFileStream(); + + NS_DECL_ISUPPORTS + + NS_IMETHOD Available(uint64_t* _retval) override; + NS_IMETHOD Read(char* aBuf, uint32_t aCount, uint32_t* _retval) override; + NS_IMETHOD ReadSegments(nsWriteSegmentFun aWriter, void* aClosure, + uint32_t aCount, uint32_t* _retval) override; + NS_DECL_NSIOUTPUTSTREAM + NS_DECL_NSISEEKABLESTREAM + NS_DECL_NSITELLABLESTREAM + + nsresult InitWithFile(nsIFile* localFile); + + protected: + ~nsMsgFileStream(); + + PRFileDesc* mFileDesc; + bool mSeekedToEnd; +}; diff --git a/comm/mailnews/base/src/nsMsgFolderCache.cpp b/comm/mailnews/base/src/nsMsgFolderCache.cpp new file mode 100644 index 0000000000..c843730f24 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgFolderCache.cpp @@ -0,0 +1,570 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgFolderCache.h" +#include "nsIMsgFolderCacheElement.h" +#include "nsNetUtil.h" +#include "nsStreamUtils.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsISafeOutputStream.h" +#include "prprf.h" +#include "mozilla/Logging.h" +// Mork-related includes. +#include "nsIMdbFactoryFactory.h" +#include "mdb.h" +// Includes for jsoncpp. +#include "json/json.h" +#include <string> + +using namespace mozilla; + +static LazyLogModule sFolderCacheLog("MsgFolderCache"); + +// Helper functions for migration of legacy pancea.dat files. +static nsresult importFromMork(PathString const& dbName, Json::Value& root); +static nsresult convertTable(nsIMdbEnv* env, nsIMdbStore* store, + nsIMdbTable* table, Json::Value& root); +static void applyEntry(nsCString const& name, nsCString const& val, + Json::Value& obj); + +/* + * nsMsgFolderCacheElement + * Folders are given this to let them manipulate their cache data. + */ +class nsMsgFolderCacheElement : public nsIMsgFolderCacheElement { + public: + nsMsgFolderCacheElement(nsMsgFolderCache* owner, nsACString const& key) + : mOwner(owner), mKey(key) {} + nsMsgFolderCacheElement() = delete; + + NS_DECL_ISUPPORTS + NS_IMETHOD GetKey(nsACString& key) override { + key = mKey; + return NS_OK; + } + + NS_IMETHOD GetCachedString(const char* name, nsACString& _retval) override { + if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE; + Json::Value& o = Obj()[name]; + if (o.isConvertibleTo(Json::stringValue)) { + _retval = o.asString().c_str(); + return NS_OK; + } + // Leave _retval unchanged if an error occurs. + return NS_ERROR_NOT_AVAILABLE; + } + + NS_IMETHOD GetCachedInt32(const char* name, int32_t* _retval) override { + if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE; + Json::Value& o = Obj()[name]; + if (o.isConvertibleTo(Json::intValue)) { + *_retval = o.asInt(); + return NS_OK; + } + // Leave _retval unchanged if an error occurs. + return NS_ERROR_NOT_AVAILABLE; + } + + NS_IMETHOD GetCachedUInt32(const char* name, uint32_t* _retval) override { + if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE; + Json::Value& o = Obj()[name]; + if (o.isConvertibleTo(Json::uintValue)) { + *_retval = o.asUInt(); + return NS_OK; + } + // Leave _retval unchanged if an error occurs. + return NS_ERROR_NOT_AVAILABLE; + } + + NS_IMETHOD GetCachedInt64(const char* name, int64_t* _retval) override { + if (!Obj().isMember(name)) return NS_ERROR_NOT_AVAILABLE; + Json::Value& o = Obj()[name]; + // isConvertibleTo() doesn't seem to support Int64. Hence multiple checks. + if (o.isNumeric() || o.isNull() || o.isBool()) { + *_retval = o.asInt64(); + return NS_OK; + } + // Leave _retval unchanged if an error occurs. + return NS_ERROR_NOT_AVAILABLE; + } + + NS_IMETHOD SetCachedString(const char* name, + const nsACString& value) override { + if (Obj()[name] != PromiseFlatCString(value).get()) { + Obj()[name] = PromiseFlatCString(value).get(); + mOwner->SetModified(); + } + return NS_OK; + } + + NS_IMETHOD SetCachedInt32(const char* name, int32_t value) override { + if (Obj()[name] != value) { + Obj()[name] = value; + mOwner->SetModified(); + } + return NS_OK; + } + + NS_IMETHOD SetCachedUInt32(const char* name, uint32_t value) override { + if (Obj()[name] != value) { + Obj()[name] = value; + mOwner->SetModified(); + } + return NS_OK; + } + NS_IMETHOD SetCachedInt64(const char* name, int64_t value) override { + if (Obj()[name] != value) { + Obj()[name] = value; + mOwner->SetModified(); + } + return NS_OK; + } + + protected: + virtual ~nsMsgFolderCacheElement() {} + RefPtr<nsMsgFolderCache> mOwner; + nsAutoCString mKey; + + // Helper to get the Json object for this nsFolderCacheElement, + // creating it if it doesn't already exist. + Json::Value& Obj() { + Json::Value& root = *mOwner->mRoot; + // This will create an empty object if it doesn't already exist. + Json::Value& v = root[mKey.get()]; + if (v.isObject()) { + return v; + } + // uhoh... either the folder entry doesn't exist (expected) or + // the json file wasn't the structure we were expecting. + // We _really_ don't want jsoncpp to be throwing exceptions, so in either + // case we'll create a fresh new empty object there. + root[mKey.get()] = Json::Value(Json::objectValue); + return root[mKey.get()]; + } +}; + +NS_IMPL_ISUPPORTS(nsMsgFolderCacheElement, nsIMsgFolderCacheElement) + +/* + * nsMsgFolderCache implementation + */ + +NS_IMPL_ISUPPORTS(nsMsgFolderCache, nsIMsgFolderCache) + +// mRoot dynamically allocated here to avoid exposing Json in header file. +nsMsgFolderCache::nsMsgFolderCache() + : mRoot(new Json::Value(Json::objectValue)), + mSavePending(false), + mSaveTimer(NS_NewTimer()) {} + +NS_IMETHODIMP nsMsgFolderCache::Init(nsIFile* cacheFile, nsIFile* legacyFile) { + mCacheFile = cacheFile; + // Is there a JSON file to load? + bool exists; + nsresult rv = cacheFile->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) { + rv = LoadFolderCache(cacheFile); + if (NS_FAILED(rv)) { + MOZ_LOG( + sFolderCacheLog, LogLevel::Error, + ("Failed to load %s (code 0x%x)", + cacheFile->HumanReadablePath().get(), static_cast<uint32_t>(rv))); + } + // Ignore error. If load fails, we'll just start off with empty cache. + return NS_OK; + } + + MOZ_LOG(sFolderCacheLog, LogLevel::Debug, ("No cache file found.")); + + // No sign of new-style JSON file. Maybe there's an old panacea.dat we can + // migrate? + rv = legacyFile->Exists(&exists); + if (NS_SUCCEEDED(rv) && exists) { + MOZ_LOG(sFolderCacheLog, LogLevel::Debug, + ("Found %s. Attempting migration.", + legacyFile->HumanReadablePath().get())); + Json::Value root(Json::objectValue); + rv = importFromMork(legacyFile->NativePath(), root); + if (NS_SUCCEEDED(rv)) { + *mRoot = root; + MOZ_LOG(sFolderCacheLog, LogLevel::Debug, + ("Migration: Legacy cache imported")); + // Migrate it to JSON. + rv = SaveFolderCache(cacheFile); + if (NS_SUCCEEDED(rv)) { + // We're done with the legacy panacea.dat - remove it. + legacyFile->Remove(false); + } else { + MOZ_LOG( + sFolderCacheLog, LogLevel::Error, + ("Migration: save failed (code 0x%x)", static_cast<uint32_t>(rv))); + } + } else { + MOZ_LOG( + sFolderCacheLog, LogLevel::Error, + ("Migration: import failed (code 0x%x)", static_cast<uint32_t>(rv))); + } + } + // Never fails. + return NS_OK; +} + +nsMsgFolderCache::~nsMsgFolderCache() { + Flush(); + delete mRoot; +} + +NS_IMETHODIMP nsMsgFolderCache::Flush() { + if (mSavePending) { + mSaveTimer->Cancel(); + mSavePending = false; + MOZ_LOG(sFolderCacheLog, LogLevel::Debug, ("Forced save.")); + nsresult rv = SaveFolderCache(mCacheFile); + if (NS_FAILED(rv)) { + MOZ_LOG( + sFolderCacheLog, LogLevel::Error, + ("Failed to write to %s (code 0x%x)", + mCacheFile->HumanReadablePath().get(), static_cast<uint32_t>(rv))); + } + } + return NS_OK; +} + +// Read the cache data from inFile. +// It's atomic - if a failure occurs, the cache data will be left unchanged. +nsresult nsMsgFolderCache::LoadFolderCache(nsIFile* inFile) { + MOZ_LOG(sFolderCacheLog, LogLevel::Debug, + ("Loading %s", inFile->HumanReadablePath().get())); + + nsCOMPtr<nsIInputStream> inStream; + nsresult rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream), inFile); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString data; + rv = NS_ConsumeStream(inStream, UINT32_MAX, data); + if (NS_FAILED(rv)) { + MOZ_LOG(sFolderCacheLog, LogLevel::Error, ("Read failed.")); + return rv; + } + + Json::Value root; + Json::CharReaderBuilder builder; + std::unique_ptr<Json::CharReader> const reader(builder.newCharReader()); + if (!reader->parse(data.BeginReading(), data.EndReading(), &root, nullptr)) { + MOZ_LOG(sFolderCacheLog, LogLevel::Error, ("Error parsing JSON")); + return NS_ERROR_FAILURE; // parsing failed. + } + if (!root.isObject()) { + MOZ_LOG(sFolderCacheLog, LogLevel::Error, ("JSON root is not an object")); + return NS_ERROR_FAILURE; // bad format. + } + *mRoot = root; + return NS_OK; +} + +// Write the cache data to outFile. +nsresult nsMsgFolderCache::SaveFolderCache(nsIFile* outFile) { + MOZ_LOG(sFolderCacheLog, LogLevel::Debug, + ("Save to %s", outFile->HumanReadablePath().get())); + + // Serialise the data. + Json::StreamWriterBuilder b; + // b["indentation"] = ""; + std::string out = Json::writeString(b, *mRoot); + + // Safe stream, writes to a tempfile first then moves into proper place when + // Finish() is called. Could use NS_NewAtomicFileOutputStream, but seems hard + // to justify a full filesystem flush). + nsCOMPtr<nsIOutputStream> outputStream; + nsresult rv = + NS_NewSafeLocalFileOutputStream(getter_AddRefs(outputStream), outFile, + PR_CREATE_FILE | PR_TRUNCATE | PR_WRONLY); + NS_ENSURE_SUCCESS(rv, rv); + + const char* ptr = out.data(); + uint32_t remaining = out.length(); + while (remaining > 0) { + uint32_t written = 0; + rv = outputStream->Write(ptr, remaining, &written); + NS_ENSURE_SUCCESS(rv, rv); + remaining -= written; + ptr += written; + } + nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(outputStream); + MOZ_ASSERT(safeStream); + rv = safeStream->Finish(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderCache::GetCacheElement( + const nsACString& pathKey, bool createIfMissing, + nsIMsgFolderCacheElement** result) { + nsAutoCString key(pathKey); + if (mRoot->isMember(key.get()) || createIfMissing) { + nsCOMPtr<nsIMsgFolderCacheElement> element = + new nsMsgFolderCacheElement(this, pathKey); + element.forget(result); + return NS_OK; + } + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsMsgFolderCache::RemoveElement(const nsACString& key) { + mRoot->removeMember(PromiseFlatCString(key).get()); + return NS_OK; +} + +void nsMsgFolderCache::SetModified() { + if (mSavePending) { + return; + } + nsresult rv = mSaveTimer->InitWithNamedFuncCallback( + doSave, (void*)this, kSaveDelayMs, nsITimer::TYPE_ONE_SHOT, + "msgFolderCache::doSave"); + if (NS_SUCCEEDED(rv)) { + MOZ_LOG(sFolderCacheLog, LogLevel::Debug, + ("AutoSave in %ds", kSaveDelayMs / 1000)); + mSavePending = true; + } +} + +// static +void nsMsgFolderCache::doSave(nsITimer*, void* closure) { + MOZ_LOG(sFolderCacheLog, LogLevel::Debug, ("AutoSave")); + nsMsgFolderCache* that = static_cast<nsMsgFolderCache*>(closure); + nsresult rv = that->SaveFolderCache(that->mCacheFile); + if (NS_FAILED(rv)) { + MOZ_LOG(sFolderCacheLog, LogLevel::Error, + ("Failed writing %s (code 0x%x)", + that->mCacheFile->HumanReadablePath().get(), + static_cast<uint32_t>(rv))); + } + that->mSavePending = false; +} + +// Helper to apply a legacy property to the new JSON format. +static void applyEntry(nsCString const& name, nsCString const& val, + Json::Value& obj) { + // The old mork version stored all numbers as hex, so we need to convert + // them into proper Json numeric types. But there's no type info in the + // database so we just have to know which values to convert. + // We can find a list of all the numeric values by grepping the codebase + // for GetCacheInt32/GetCachedInt64. We treat everything else as a string. + // It's much harder to get a definitive list of possible keys for strings, + // because nsIMsgFolderCache is also used to cache nsDBFolderInfo data - + // see nsMsgDBFolder::GetStringProperty(). + + // One of the Int32 properties? + if (name.EqualsLiteral("hierDelim") || + name.EqualsLiteral("lastSyncTimeInSec") || + name.EqualsLiteral("nextUID") || name.EqualsLiteral("pendingMsgs") || + name.EqualsLiteral("pendingUnreadMsgs") || + name.EqualsLiteral("serverRecent") || name.EqualsLiteral("serverTotal") || + name.EqualsLiteral("serverUnseen") || name.EqualsLiteral("totalMsgs") || + name.EqualsLiteral("totalUnreadMsgs")) { + if (val.IsEmpty()) { + return; + } + int32_t i32; + if (PR_sscanf(val.get(), "%x", &i32) != 1) { + return; + } + obj[name.get()] = i32; + return; + } + + // Flags were int32. But the upper bit can be set, meaning we'll get + // annoying negative values, which isn't what we want. Not so much of an + // issue with legacy pancea.dat as it was all hex strings anyway. But let's + // fix it up as we go to JSON. + if (name.EqualsLiteral("aclFlags") || name.EqualsLiteral("boxFlags") || + name.EqualsLiteral("flags")) { + uint32_t u32; + if (PR_sscanf(val.get(), "%x", &u32) != 1) { + return; + } + obj[name.get()] = u32; + return; + } + + // One of the Int64 properties? + if (name.EqualsLiteral("expungedBytes") || name.EqualsLiteral("folderSize")) { + if (val.IsEmpty()) { + return; + } + int64_t i64; + if (PR_sscanf(val.get(), "%llx", &i64) != 1) { + return; + } + obj[name.get()] = i64; + return; + } + + // Assume anything else is a string. + obj[name.get()] = val.get(); +} + +// Import an old panacea.dat mork file, converting it into our JSON form. +// The flow of this is taken from the old implementation. There are a couple +// of steps that may not be strictly required, but have been left in anyway +// on the grounds that it works. +static nsresult importFromMork(PathString const& dbName, Json::Value& root) { + nsresult rv; + nsCOMPtr<nsIMdbFactoryService> factoryService = + do_GetService("@mozilla.org/db/mork;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMdbFactory> factory; + rv = factoryService->GetMdbFactory(getter_AddRefs(factory)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMdbEnv> env; + rv = factory->MakeEnv(nullptr, getter_AddRefs(env)); + NS_ENSURE_SUCCESS(rv, rv); + + env->SetAutoClear(true); + nsCOMPtr<nsIMdbFile> dbFile; + rv = factory->OpenOldFile(env, + nullptr, // Use default heap alloc fns. + dbName.get(), + mdbBool_kTrue, // Frozen (read only). + getter_AddRefs(dbFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // Unsure if we actually need this... + mdb_bool canOpen; + mdbYarn outFormatVersion; + rv = factory->CanOpenFilePort(env, dbFile, &canOpen, &outFormatVersion); + NS_ENSURE_SUCCESS(rv, rv); + if (!canOpen) { + return NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE; + } + + mdbOpenPolicy inOpenPolicy; + inOpenPolicy.mOpenPolicy_ScopePlan.mScopeStringSet_Count = 0; + inOpenPolicy.mOpenPolicy_MinMemory = 0; + inOpenPolicy.mOpenPolicy_MaxLazy = 0; + + nsCOMPtr<nsIMdbThumb> thumb; + rv = factory->OpenFileStore(env, + nullptr, // Use default heap alloc fns. + dbFile, &inOpenPolicy, getter_AddRefs(thumb)); + NS_ENSURE_SUCCESS(rv, rv); + + // Unsure what this is doing. Applying appended-but-unapplied writes? + { + mdb_count outTotal; // total somethings to do in operation + mdb_count outCurrent; // subportion of total completed so far + mdb_bool outDone; // is operation finished? + mdb_bool outBroken; // is operation irreparably dead and broken? + do { + rv = thumb->DoMore(env, &outTotal, &outCurrent, &outDone, &outBroken); + NS_ENSURE_SUCCESS(rv, rv); + } while (!outBroken && !outDone); + } + + // Finally, open the store. + nsCOMPtr<nsIMdbStore> store; + rv = factory->ThumbToOpenStore(env, thumb, getter_AddRefs(store)); + NS_ENSURE_SUCCESS(rv, rv); + + // Resolve some tokens we'll need. + const char* kFoldersScope = "ns:msg:db:row:scope:folders:all"; + mdb_token folderRowScopeToken; + rv = store->StringToToken(env, kFoldersScope, &folderRowScopeToken); + NS_ENSURE_SUCCESS(rv, rv); + + // Find the table. Only one, and we assume id=1. Eek! But original code + // did this too... + mdbOid allFoldersTableOID{folderRowScopeToken, 1}; + nsCOMPtr<nsIMdbTable> allFoldersTable; + rv = store->GetTable(env, &allFoldersTableOID, + getter_AddRefs(allFoldersTable)); + NS_ENSURE_SUCCESS(rv, rv); + // GetTable() can return null even without an error. + NS_ENSURE_STATE(allFoldersTable); + + rv = convertTable(env, store, allFoldersTable, root); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// The legacy panacea.dat mork db has a single table, with a row per +// folder. This function reads it in and writes it into our Json::Value +// object. +static nsresult convertTable(nsIMdbEnv* env, nsIMdbStore* store, + nsIMdbTable* table, Json::Value& root) { + MOZ_ASSERT(root.isObject()); + MOZ_ASSERT(table); + + nsresult rv; + nsCOMPtr<nsIMdbTableRowCursor> rowCursor; + rv = table->GetTableRowCursor(env, -1, getter_AddRefs(rowCursor)); + NS_ENSURE_SUCCESS(rv, rv); + // For each row in the table... + while (true) { + nsCOMPtr<nsIMdbRow> row; + mdb_pos pos; + rv = rowCursor->NextRow(env, getter_AddRefs(row), &pos); + NS_ENSURE_SUCCESS(rv, rv); + if (!row) { + break; // That's all the rows done. + } + + nsCOMPtr<nsIMdbRowCellCursor> cellCursor; + rv = row->GetRowCellCursor(env, -1, getter_AddRefs(cellCursor)); + NS_ENSURE_SUCCESS(rv, rv); + + Json::Value obj(Json::objectValue); + // For each cell in the row... + nsAutoCString rowKey; + while (true) { + nsCOMPtr<nsIMdbCell> cell; + mdb_column column; + rv = cellCursor->NextCell(env, getter_AddRefs(cell), &column, nullptr); + NS_ENSURE_SUCCESS(rv, rv); + if (!cell) { + break; // No more cells. + } + + // Get the column name + nsAutoCString colName; + { + char buf[100]; + mdbYarn colYarn{buf, 0, sizeof(buf), 0, 0, nullptr}; + // Get the column of the cell + nsresult rv = store->TokenToString(env, column, &colYarn); + NS_ENSURE_SUCCESS(rv, rv); + + colName.Assign((const char*)colYarn.mYarn_Buf, colYarn.mYarn_Fill); + } + // Get the value + nsAutoCString colValue; + { + mdbYarn yarn; + cell->AliasYarn(env, &yarn); + colValue.Assign((const char*)yarn.mYarn_Buf, yarn.mYarn_Fill); + } + if (colName.EqualsLiteral("key")) { + rowKey = colValue; + } else { + applyEntry(colName, colValue, obj); + } + } + if (rowKey.IsEmpty()) { + continue; + } + + MOZ_LOG(sFolderCacheLog, LogLevel::Debug, + ("Migration: migrated key '%s' (%d properties)", rowKey.get(), + (int)obj.size())); + root[rowKey.get()] = obj; + } + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgFolderCache.h b/comm/mailnews/base/src/nsMsgFolderCache.h new file mode 100644 index 0000000000..480814ad10 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgFolderCache.h @@ -0,0 +1,60 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgFolderCache_H +#define nsMsgFolderCache_H + +#include "nsIMsgFolderCache.h" +#include "nsIFile.h" +#include "nsITimer.h" + +namespace Json { +class Value; +}; + +/** + * nsMsgFolderCache implements the folder cache, which stores values which + * might be slow for the folder to calculate. + * It persists the cache data by dumping it out to a .json file when changes + * are made. To avoid huge numbers of writes, this autosaving is deferred - + * when a cached value is changed, it'll wait a minute or so before + * writing, to collect any other changes that occur during that time. + * If any changes are outstanding at destruction time, it'll perform an + * immediate save then. + */ +class nsMsgFolderCache : public nsIMsgFolderCache { + public: + friend class nsMsgFolderCacheElement; + + nsMsgFolderCache(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGFOLDERCACHE + + protected: + virtual ~nsMsgFolderCache(); + + nsresult LoadFolderCache(nsIFile* jsonFile); + nsresult SaveFolderCache(nsIFile* jsonFile); + // Flag that a save is required. It'll be deferred by kAutoSaveDelayMs. + void SetModified(); + static constexpr uint32_t kSaveDelayMs = 1000 * 60 * 1; // 1 minute. + static void doSave(nsITimer*, void* closure); + + // Path to the JSON file backing the cache. + nsCOMPtr<nsIFile> mCacheFile; + + // This is our data store. Kept as a Json::Value for ease of saving, but + // it's actually not a bad format for access (it's basically a std::map). + // Using a pointer to allow forward declaration. The json headers aren't + // in the include path for other modules, so we don't want to expose them + // here. + Json::Value* mRoot; + + bool mSavePending; + nsCOMPtr<nsITimer> mSaveTimer; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgFolderCompactor.cpp b/comm/mailnews/base/src/nsMsgFolderCompactor.cpp new file mode 100644 index 0000000000..c9740bedb8 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgFolderCompactor.cpp @@ -0,0 +1,1391 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // precompiled header... +#include "nsCOMPtr.h" +#include "nsIMsgFolder.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "nsIMsgHdr.h" +#include "nsIChannel.h" +#include "nsIStreamListener.h" +#include "nsIMsgMessageService.h" +#include "nsMsgUtils.h" +#include "nsISeekableStream.h" +#include "nsIDBFolderInfo.h" +#include "nsIPrompt.h" +#include "nsIMsgLocalMailFolder.h" +#include "nsIMsgImapMailFolder.h" +#include "nsMailHeaders.h" +#include "nsMsgLocalFolderHdrs.h" +#include "nsIMsgDatabase.h" +#include "nsMsgMessageFlags.h" +#include "nsMsgFolderFlags.h" +#include "nsIMsgStatusFeedback.h" +#include "nsIMsgFolderNotificationService.h" +#include "nsMsgFolderCompactor.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsPrintfCString.h" +#include "nsIStringBundle.h" +#include "nsICopyMessageStreamListener.h" +#include "nsIMsgWindow.h" +#include "nsIMsgPluggableStore.h" +#include "mozilla/Buffer.h" +#include "HeaderReader.h" +#include "LineReader.h" +#include "mozilla/Components.h" + +static nsresult GetBaseStringBundle(nsIStringBundle** aBundle) { + NS_ENSURE_ARG_POINTER(aBundle); + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIStringBundle> bundle; + return bundleService->CreateBundle( + "chrome://messenger/locale/messenger.properties", aBundle); +} + +#define COMPACTOR_READ_BUFF_SIZE 16384 + +/** + * nsFolderCompactState is a helper class for nsFolderCompactor, which + * handles compacting the mbox for a single local folder. + * + * This class also patches X-Mozilla-* headers where required. Usually + * these headers are edited in-place without changing the overall size, + * but sometimes there's not enough room. So as compaction involves + * rewriting the whole file anyway, we take the opportunity to make some + * more space and correct those headers. + * + * NOTE (for future cleanups): + * + * This base class calls nsIMsgMessageService.copyMessages() to iterate + * through messages, passing itself in as a listener. Callbacks from + * both nsICopyMessageStreamListener and nsIStreamListener are invoked. + * + * nsOfflineStoreCompactState uses a different mechanism - see separate + * notes below. + * + * The way the service invokes the listener callbacks is pretty quirky + * and probably needs a good sorting out, but for now I'll just document what + * I've observed here: + * + * - The service calls OnStartRequest() at the start of the first message. + * - StartMessage() is called at the start of subsequent messages. + * - EndCopy() is called at the end of every message except the last one, + * where OnStopRequest() is invoked instead. + * - OnDataAvailable() is called to pass the message body of each message + * (in multiple calls if the message is big enough). + * - EndCopy() doesn't ever seem to be passed a failing error code from + * what I can see, and its own return code is ignored by upstream code. + */ +class nsFolderCompactState : public nsIStreamListener, + public nsICopyMessageStreamListener, + public nsIUrlListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSICOPYMESSAGESTREAMLISTENER + NS_DECL_NSIURLLISTENER + + nsFolderCompactState(void); + + nsresult Compact(nsIMsgFolder* folder, + std::function<void(nsresult, uint64_t)> completionFn, + nsIMsgWindow* msgWindow); + + protected: + virtual ~nsFolderCompactState(void); + + virtual nsresult InitDB(nsIMsgDatabase* db); + virtual nsresult StartCompacting(); + virtual nsresult FinishCompact(); + void CloseOutputStream(); + void CleanupTempFilesAfterError(); + nsresult FlushBuffer(); + + nsresult Init(nsIMsgFolder* aFolder, const char* aBaseMsgUri, + nsIMsgDatabase* aDb, nsIFile* aPath, nsIMsgWindow* aMsgWindow); + nsresult BuildMessageURI(const char* baseURI, nsMsgKey key, nsCString& uri); + nsresult ShowStatusMsg(const nsString& aMsg); + nsresult ReleaseFolderLock(); + void ShowCompactingStatusMsg(); + + nsCString m_baseMessageUri; // base message uri + nsCString m_messageUri; // current message uri being copy + nsCOMPtr<nsIMsgFolder> m_folder; // current folder being compact + nsCOMPtr<nsIMsgDatabase> m_db; // new database for the compact folder + nsCOMPtr<nsIFile> m_file; // new mailbox for the compact folder + nsCOMPtr<nsIOutputStream> m_fileStream; // output file stream for writing + // All message keys that need to be copied over. + nsTArray<nsMsgKey> m_keys; + + // Sum of the sizes of the messages, accumulated as we visit each msg. + uint64_t m_totalMsgSize{0}; + // Number of bytes that can be expunged while compacting. + uint64_t m_totalExpungedBytes{0}; + // Index of the current copied message key in key array. + uint32_t m_curIndex{0}; + // Offset in mailbox of new message. + uint64_t m_startOfNewMsg{0}; + mozilla::Buffer<char> m_buffer{COMPACTOR_READ_BUFF_SIZE}; + uint32_t m_bufferCount{0}; + + // We'll use this if we need to output any EOLs - we try to preserve the + // convention found in the input data. + nsCString m_eolSeq{MSG_LINEBREAK}; + + // The status of the copying operation. + nsresult m_status{NS_OK}; + nsCOMPtr<nsIMsgMessageService> m_messageService; // message service for + // copying + nsCOMPtr<nsIMsgWindow> m_window; + nsCOMPtr<nsIMsgDBHdr> m_curSrcHdr; + // Flag set when we're waiting for local folder to complete parsing. + bool m_parsingFolder; + // Flag to indicate we're starting a new message, and that no data has + // been written for it yet. + bool m_startOfMsg; + // Function which will be run when the folder compaction completes. + // Takes a result code and the number of bytes which were expunged. + std::function<void(nsresult, uint64_t)> m_completionFn; + bool m_alreadyWarnedDiskSpace{false}; +}; + +NS_IMPL_ISUPPORTS(nsFolderCompactState, nsIRequestObserver, nsIStreamListener, + nsICopyMessageStreamListener, nsIUrlListener) + +nsFolderCompactState::nsFolderCompactState() { + m_parsingFolder = false; + m_startOfMsg = true; +} + +nsFolderCompactState::~nsFolderCompactState() { + CloseOutputStream(); + if (NS_FAILED(m_status)) { + CleanupTempFilesAfterError(); + // if for some reason we failed remove the temp folder and database + } +} + +void nsFolderCompactState::CloseOutputStream() { + if (m_fileStream) { + m_fileStream->Close(); + m_fileStream = nullptr; + } +} + +void nsFolderCompactState::CleanupTempFilesAfterError() { + CloseOutputStream(); + if (m_db) m_db->ForceClosed(); + nsCOMPtr<nsIFile> summaryFile; + GetSummaryFileLocation(m_file, getter_AddRefs(summaryFile)); + m_file->Remove(false); + summaryFile->Remove(false); +} + +nsresult nsFolderCompactState::BuildMessageURI(const char* baseURI, + nsMsgKey key, nsCString& uri) { + uri.Append(baseURI); + uri.Append('#'); + uri.AppendInt(key); + + return NS_OK; +} + +nsresult nsFolderCompactState::InitDB(nsIMsgDatabase* db) { + nsCOMPtr<nsIMsgDatabase> mailDBFactory; + nsresult rv = db->ListAllKeys(m_keys); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = msgDBService->OpenMailDBFromFile(m_file, m_folder, true, false, + getter_AddRefs(m_db)); + + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE || + rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING) + // if it's out of date then reopen with upgrade. + return msgDBService->OpenMailDBFromFile(m_file, m_folder, true, true, + getter_AddRefs(m_db)); + return rv; +} + +nsresult nsFolderCompactState::Compact( + nsIMsgFolder* folder, std::function<void(nsresult, uint64_t)> completionFn, + nsIMsgWindow* msgWindow) { + NS_ENSURE_ARG_POINTER(folder); + m_completionFn = completionFn; + m_window = msgWindow; + nsresult rv; + nsCOMPtr<nsIMsgDatabase> db; + nsCOMPtr<nsIFile> path; + nsCString baseMessageURI; + + nsCOMPtr<nsIMsgLocalMailFolder> localFolder = do_QueryInterface(folder, &rv); + if (NS_SUCCEEDED(rv) && localFolder) { + rv = localFolder->GetDatabaseWOReparse(getter_AddRefs(db)); + if (NS_FAILED(rv) || !db) { + if (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING || + rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) { + m_folder = folder; // will be used to compact + m_parsingFolder = true; + rv = localFolder->ParseFolder(m_window, this); + } + return rv; + } else { + bool valid; + rv = db->GetSummaryValid(&valid); + if (!valid) // we are probably parsing the folder because we selected it. + { + folder->NotifyCompactCompleted(); + if (m_completionFn) { + m_completionFn(NS_OK, m_totalExpungedBytes); + } + return NS_OK; + } + } + } else { + rv = folder->GetMsgDatabase(getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = folder->GetFilePath(getter_AddRefs(path)); + NS_ENSURE_SUCCESS(rv, rv); + + do { + bool exists = false; + rv = path->Exists(&exists); + if (!exists) { + // No need to compact if the local file does not exist. + // Can happen e.g. on IMAP when the folder is not marked for offline use. + break; + } + + int64_t expunged = 0; + folder->GetExpungedBytes(&expunged); + if (expunged == 0) { + // No need to compact if nothing would be expunged. + break; + } + + int64_t diskSize; + rv = folder->GetSizeOnDisk(&diskSize); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t diskFree; + rv = path->GetDiskSpaceAvailable(&diskFree); + if (NS_FAILED(rv)) { + // If GetDiskSpaceAvailable() failed, better bail out fast. + if (rv != NS_ERROR_NOT_IMPLEMENTED) return rv; + // Some platforms do not have GetDiskSpaceAvailable implemented. + // In that case skip the preventive free space analysis and let it + // fail in compact later if space actually wasn't available. + } else { + // Let's try to not even start compact if there is really low free space. + // It may still fail later as we do not know how big exactly the folder DB + // will end up being. The DB already doesn't contain references to + // messages that are already deleted. So theoretically it shouldn't shrink + // with compact. But in practice, the automatic shrinking of the DB may + // still have not yet happened. So we cap the final size at 1KB per + // message. + db->Commit(nsMsgDBCommitType::kCompressCommit); + + int64_t dbSize; + rv = db->GetDatabaseSize(&dbSize); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t totalMsgs; + rv = folder->GetTotalMessages(false, &totalMsgs); + NS_ENSURE_SUCCESS(rv, rv); + int64_t expectedDBSize = + std::min<int64_t>(dbSize, ((int64_t)totalMsgs) * 1024); + if (diskFree < diskSize - expunged + expectedDBSize) { + if (!m_alreadyWarnedDiskSpace) { + folder->ThrowAlertMsg("compactFolderInsufficientSpace", m_window); + m_alreadyWarnedDiskSpace = true; + } + break; + } + } + + rv = folder->GetBaseMessageURI(baseMessageURI); + NS_ENSURE_SUCCESS(rv, rv); + + rv = Init(folder, baseMessageURI.get(), db, path, m_window); + NS_ENSURE_SUCCESS(rv, rv); + + bool isLocked = true; + m_folder->GetLocked(&isLocked); + if (isLocked) { + CleanupTempFilesAfterError(); + m_folder->ThrowAlertMsg("compactFolderDeniedLock", m_window); + break; + } + + // If we got here start the real compacting. + nsCOMPtr<nsISupports> supports; + QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(supports)); + m_folder->AcquireSemaphore(supports); + m_totalExpungedBytes += expunged; + return StartCompacting(); + + } while (false); // block for easy skipping the compaction using 'break' + + // Skipped folder, for whatever reason. + folder->NotifyCompactCompleted(); + if (m_completionFn) { + m_completionFn(NS_OK, m_totalExpungedBytes); + } + return NS_OK; +} + +nsresult nsFolderCompactState::ShowStatusMsg(const nsString& aMsg) { + if (!m_window || aMsg.IsEmpty()) return NS_OK; + + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + nsresult rv = m_window->GetStatusFeedback(getter_AddRefs(statusFeedback)); + if (NS_FAILED(rv) || !statusFeedback) return NS_OK; + + // Try to prepend account name to the message. + nsString statusMessage; + do { + nsCOMPtr<nsIMsgIncomingServer> server; + rv = m_folder->GetServer(getter_AddRefs(server)); + if (NS_FAILED(rv)) break; + nsAutoString accountName; + rv = server->GetPrettyName(accountName); + if (NS_FAILED(rv)) break; + nsCOMPtr<nsIStringBundle> bundle; + rv = GetBaseStringBundle(getter_AddRefs(bundle)); + if (NS_FAILED(rv)) break; + AutoTArray<nsString, 2> params = {accountName, aMsg}; + rv = bundle->FormatStringFromName("statusMessage", params, statusMessage); + } while (false); + + // If fetching any of the needed info failed, just show the original message. + if (NS_FAILED(rv)) statusMessage.Assign(aMsg); + return statusFeedback->SetStatusString(statusMessage); +} + +nsresult nsFolderCompactState::Init(nsIMsgFolder* folder, + const char* baseMsgUri, nsIMsgDatabase* db, + nsIFile* path, nsIMsgWindow* aMsgWindow) { + nsresult rv; + + m_folder = folder; + m_baseMessageUri = baseMsgUri; + m_file = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + m_file->InitWithFile(path); + + m_file->SetNativeLeafName("nstmp"_ns); + // Make sure we are not crunching existing nstmp file. + rv = m_file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 00600); + NS_ENSURE_SUCCESS(rv, rv); + + m_window = aMsgWindow; + m_totalMsgSize = 0; + rv = InitDB(db); + if (NS_FAILED(rv)) { + CleanupTempFilesAfterError(); + return rv; + } + + m_curIndex = 0; + + rv = MsgNewBufferedFileOutputStream(getter_AddRefs(m_fileStream), m_file, -1, + 00600); + if (NS_FAILED(rv)) + m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window); + else + rv = GetMessageServiceFromURI(nsDependentCString(baseMsgUri), + getter_AddRefs(m_messageService)); + if (NS_FAILED(rv)) { + m_status = rv; + } + return rv; +} + +void nsFolderCompactState::ShowCompactingStatusMsg() { + nsString statusString; + nsresult rv = m_folder->GetStringWithFolderNameFromBundle("compactingFolder", + statusString); + if (!statusString.IsEmpty() && NS_SUCCEEDED(rv)) ShowStatusMsg(statusString); +} + +NS_IMETHODIMP nsFolderCompactState::OnStartRunningUrl(nsIURI* url) { + return NS_OK; +} + +// If we had to kick off a folder parse, this will be called when it +// completes. +NS_IMETHODIMP nsFolderCompactState::OnStopRunningUrl(nsIURI* url, + nsresult status) { + if (m_parsingFolder) { + m_parsingFolder = false; + if (NS_SUCCEEDED(status)) { + // Folder reparse succeeded. Start compacting it. + status = Compact(m_folder, m_completionFn, m_window); + if (NS_SUCCEEDED(status)) { + return NS_OK; + } + } + } + + // This is from bug 249754. The aim is to close the DB file to avoid + // running out of filehandles when large numbers of folders are compacted. + // But it seems like filehandle management would be better off being + // handled by the DB class itself (it might be already, but it's hard to + // tell)... + m_folder->SetMsgDatabase(nullptr); + + if (m_completionFn) { + m_completionFn(status, m_totalExpungedBytes); + } + return NS_OK; +} + +nsresult nsFolderCompactState::StartCompacting() { + nsresult rv = NS_OK; + // Notify that compaction is beginning. We do this even if there are no + // messages to be copied because the summary database still gets blown away + // which is still pretty interesting. (And we like consistency.) + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) { + notifier->NotifyFolderCompactStart(m_folder); + } + + // TODO: test whether sorting the messages (m_keys) by messageOffset + // would improve performance on large files (less seeks). + // The m_keys array is in the order as stored in DB and on IMAP or News + // the messages stored on the mbox file are not necessarily in the same order. + if (m_keys.Length() > 0) { + nsCOMPtr<nsIURI> notUsed; + ShowCompactingStatusMsg(); + NS_ADDREF_THIS(); + rv = m_messageService->CopyMessages(m_keys, m_folder, this, false, nullptr, + m_window, getter_AddRefs(notUsed)); + } else { // no messages to copy with + FinishCompact(); + } + return rv; +} + +nsresult nsFolderCompactState::FinishCompact() { + NS_ENSURE_TRUE(m_folder, NS_ERROR_NOT_INITIALIZED); + NS_ENSURE_TRUE(m_file, NS_ERROR_NOT_INITIALIZED); + + // All okay time to finish up the compact process + nsCOMPtr<nsIFile> path; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + + // get leaf name and database name of the folder + nsresult rv = m_folder->GetFilePath(getter_AddRefs(path)); + nsCOMPtr<nsIFile> folderPath = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = folderPath->InitWithFile(path); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> oldSummaryFile; + rv = GetSummaryFileLocation(folderPath, getter_AddRefs(oldSummaryFile)); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString dbName; + oldSummaryFile->GetNativeLeafName(dbName); + nsAutoCString folderName; + path->GetNativeLeafName(folderName); + + // close down the temp file stream; preparing for deleting the old folder + // and its database; then rename the temp folder and database + if (m_fileStream) { + m_fileStream->Flush(); + m_fileStream->Close(); + m_fileStream = nullptr; + } + + // make sure the new database is valid. + // Close it so we can rename the .msf file. + if (m_db) { + m_db->ForceClosed(); + m_db = nullptr; + } + + nsCOMPtr<nsIFile> newSummaryFile; + rv = GetSummaryFileLocation(m_file, getter_AddRefs(newSummaryFile)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDBFolderInfo> transferInfo; + m_folder->GetDBTransferInfo(getter_AddRefs(transferInfo)); + + // close down database of the original folder + m_folder->ForceDBClosed(); + + nsCOMPtr<nsIFile> cloneFile; + int64_t fileSize = 0; + rv = m_file->Clone(getter_AddRefs(cloneFile)); + if (NS_SUCCEEDED(rv)) rv = cloneFile->GetFileSize(&fileSize); + bool tempFileRightSize = ((uint64_t)fileSize == m_totalMsgSize); + NS_WARNING_ASSERTION(tempFileRightSize, + "temp file not of expected size in compact"); + + bool folderRenameSucceeded = false; + bool msfRenameSucceeded = false; + if (NS_SUCCEEDED(rv) && tempFileRightSize) { + // First we're going to try and move the old summary file out the way. + // We don't delete it yet, as we want to keep the files in sync. + nsCOMPtr<nsIFile> tempSummaryFile; + rv = oldSummaryFile->Clone(getter_AddRefs(tempSummaryFile)); + if (NS_SUCCEEDED(rv)) + rv = tempSummaryFile->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + + nsAutoCString tempSummaryFileName; + if (NS_SUCCEEDED(rv)) + rv = tempSummaryFile->GetNativeLeafName(tempSummaryFileName); + + if (NS_SUCCEEDED(rv)) + rv = oldSummaryFile->MoveToNative((nsIFile*)nullptr, tempSummaryFileName); + + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "error moving compacted folder's db out of the way"); + if (NS_SUCCEEDED(rv)) { + // Now we've successfully moved the summary file out the way, try moving + // the newly compacted message file over the old one. + rv = m_file->MoveToNative((nsIFile*)nullptr, folderName); + folderRenameSucceeded = NS_SUCCEEDED(rv); + NS_WARNING_ASSERTION(folderRenameSucceeded, + "error renaming compacted folder"); + if (folderRenameSucceeded) { + // That worked, so land the new summary file in the right place. + nsCOMPtr<nsIFile> renamedCompactedSummaryFile; + newSummaryFile->Clone(getter_AddRefs(renamedCompactedSummaryFile)); + if (renamedCompactedSummaryFile) { + rv = renamedCompactedSummaryFile->MoveToNative((nsIFile*)nullptr, + dbName); + msfRenameSucceeded = NS_SUCCEEDED(rv); + } + NS_WARNING_ASSERTION(msfRenameSucceeded, + "error renaming compacted folder's db"); + } + + if (!msfRenameSucceeded) { + // Do our best to put the summary file back to where it was + rv = tempSummaryFile->MoveToNative((nsIFile*)nullptr, dbName); + if (NS_SUCCEEDED(rv)) { + // Flagging that a renamed db no longer exists. + tempSummaryFile = nullptr; + } else { + NS_WARNING("error restoring uncompacted folder's db"); + } + } + } + // We don't want any temporarily renamed summary file to lie around + if (tempSummaryFile) tempSummaryFile->Remove(false); + } + + NS_WARNING_ASSERTION(msfRenameSucceeded, "compact failed"); + nsresult rvReleaseFolderLock = ReleaseFolderLock(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rvReleaseFolderLock), + "folder lock not released successfully"); + rv = NS_FAILED(rv) ? rv : rvReleaseFolderLock; + + // Cleanup of nstmp-named compacted files if failure + if (!folderRenameSucceeded) { + // remove the abandoned compacted version with the wrong name + m_file->Remove(false); + } + if (!msfRenameSucceeded) { + // remove the abandoned compacted summary file + newSummaryFile->Remove(false); + } + + if (msfRenameSucceeded) { + // Transfer local db information from transferInfo + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = msgDBService->OpenFolderDB(m_folder, true, getter_AddRefs(m_db)); + NS_ENSURE_TRUE(m_db, NS_FAILED(rv) ? rv : NS_ERROR_FAILURE); + // These errors are expected. + rv = (rv == NS_MSG_ERROR_FOLDER_SUMMARY_MISSING || + rv == NS_MSG_ERROR_FOLDER_SUMMARY_OUT_OF_DATE) + ? NS_OK + : rv; + m_db->SetSummaryValid(true); + if (transferInfo) m_folder->SetDBTransferInfo(transferInfo); + + // since we're transferring info from the old db, we need to reset the + // expunged bytes + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (dbFolderInfo) dbFolderInfo->SetExpungedBytes(0); + } + if (m_db) m_db->Close(true); + m_db = nullptr; + + // Notify that compaction of the folder is completed. + nsCOMPtr<nsIMsgFolderNotificationService> notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) { + notifier->NotifyFolderCompactFinish(m_folder); + } + + m_folder->NotifyCompactCompleted(); + if (m_completionFn) { + m_completionFn(rv, m_totalExpungedBytes); + } + + return NS_OK; +} + +nsresult nsFolderCompactState::ReleaseFolderLock() { + nsresult result = NS_OK; + if (!m_folder) return result; + bool haveSemaphore; + nsCOMPtr<nsISupports> supports; + QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(supports)); + result = m_folder->TestSemaphore(supports, &haveSemaphore); + if (NS_SUCCEEDED(result) && haveSemaphore) + result = m_folder->ReleaseSemaphore(supports); + return result; +} + +NS_IMETHODIMP +nsFolderCompactState::OnStartRequest(nsIRequest* request) { + return StartMessage(); +} + +NS_IMETHODIMP +nsFolderCompactState::OnStopRequest(nsIRequest* request, nsresult status) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + if (NS_FAILED(status)) { + // Set m_status to status so the destructor can remove the temp folder and + // database. + m_status = status; + CleanupTempFilesAfterError(); + m_folder->NotifyCompactCompleted(); + ReleaseFolderLock(); + m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window); + } else { + // XXX TODO: Error checking and handling missing here. + EndCopy(nullptr, status); + if (m_curIndex >= m_keys.Length()) { + msgHdr = nullptr; + // no more to copy finish it up + FinishCompact(); + } else { + // in case we're not getting an error, we still need to pretend we did get + // an error, because the compact did not successfully complete. + m_folder->NotifyCompactCompleted(); + CleanupTempFilesAfterError(); + ReleaseFolderLock(); + } + } + NS_RELEASE_THIS(); // kill self + return status; +} + +// Handle the message data. +// (NOTE: nsOfflineStoreCompactState overrides this) +NS_IMETHODIMP +nsFolderCompactState::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) { + MOZ_ASSERT(m_fileStream); + MOZ_ASSERT(inStr); + + nsresult rv = NS_OK; + + // TODO: This block should be moved in to the callback that indicates the + // start of a new message, but it's complicated because of the derived + // nsOfflineStoreCompactState and also the odd message copy listener + // orderings. Leaving it here for now, but it's ripe for tidying up in + // future. + if (m_startOfMsg) { + m_messageUri.Truncate(); // clear the previous message uri + if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex], + m_messageUri))) { + rv = m_messageService->MessageURIToMsgHdr(m_messageUri, + getter_AddRefs(m_curSrcHdr)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + while (count > 0) { + uint32_t maxReadCount = + std::min((uint32_t)m_buffer.Length() - m_bufferCount, count); + uint32_t readCount; + rv = inStr->Read(m_buffer.Elements() + m_bufferCount, maxReadCount, + &readCount); + NS_ENSURE_SUCCESS(rv, rv); + + count -= readCount; + m_bufferCount += readCount; + if (m_bufferCount == m_buffer.Length()) { + rv = FlushBuffer(); + NS_ENSURE_SUCCESS(rv, rv); + } + } + if (m_bufferCount > 0) { + rv = FlushBuffer(); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +// Helper to write data to an outputstream, until complete or error. +static nsresult WriteSpan(nsIOutputStream* writeable, + mozilla::Span<const char> data) { + while (!data.IsEmpty()) { + uint32_t n; + nsresult rv = writeable->Write(data.Elements(), data.Length(), &n); + NS_ENSURE_SUCCESS(rv, rv); + data = data.Last(data.Length() - n); + } + return NS_OK; +} + +// Flush contents of m_buffer to the output file. +// (NOTE: not used by nsOfflineStoreCompactState) +// More complicated than it should be because we need to fiddle with +// some of the X-Mozilla-* headers on the fly. +nsresult nsFolderCompactState::FlushBuffer() { + MOZ_ASSERT(m_fileStream); + nsresult rv; + auto buf = m_buffer.AsSpan().First(m_bufferCount); + if (!m_startOfMsg) { + // We only do header twiddling for the first chunk. So from now on we + // just copy data verbatim. + rv = WriteSpan(m_fileStream, buf); + NS_ENSURE_SUCCESS(rv, rv); + m_bufferCount = 0; + return NS_OK; + } + + // This is the first chunk of a new message. We'll update the + // X-Mozilla-(Status|Status2|Keys) headers as we go. + m_startOfMsg = false; + + // Sniff the data to see if we can spot any CRs. + // If so, we'll use CRLFs instead of platform-native EOLs. + auto sniffChunk = buf.First(std::min<size_t>(buf.Length(), 512)); + auto cr = std::find(sniffChunk.cbegin(), sniffChunk.cend(), '\r'); + if (cr != sniffChunk.cend()) { + m_eolSeq.Assign("\r\n"_ns); + } + + // Add a "From " line if missing. + // NOTE: Ultimately we should never see "From " lines in this data - it's an + // mbox detail the message streaming should filter out. But for now we'll + // handle it optionally. + nsAutoCString fromLine; + auto l = FirstLine(buf); + if (l.Length() > 5 && + nsDependentCSubstring(l.Elements(), 5).EqualsLiteral("From ")) { + fromLine = nsDependentCSubstring(l); + buf = buf.From(l.Length()); + } else { + fromLine = "From "_ns + m_eolSeq; + } + rv = WriteSpan(m_fileStream, fromLine); + NS_ENSURE_SUCCESS(rv, rv); + + // Read as many headers as we can. We might not have the complete header + // block our in buffer, but that's OK - the X-Mozilla-* ones should be + // right at the start). + nsTArray<HeaderReader::Hdr> headers; + HeaderReader rdr; + auto leftover = rdr.Parse(buf, [&](auto const& hdr) -> bool { + auto const& name = hdr.Name(buf); + if (!name.EqualsLiteral(HEADER_X_MOZILLA_STATUS) && + !name.EqualsLiteral(HEADER_X_MOZILLA_STATUS2) && + !name.EqualsLiteral(HEADER_X_MOZILLA_KEYWORDS)) { + headers.AppendElement(hdr); + } + return true; + }); + + // Write out X-Mozilla-* headers first - we'll create these from scratch. + uint32_t msgFlags = 0; + nsAutoCString keywords; + if (m_curSrcHdr) { + m_curSrcHdr->GetFlags(&msgFlags); + m_curSrcHdr->GetStringProperty("keywords", keywords); + // growKeywords is set if msgStore didn't have enough room to edit + // X-Mozilla-* headers in situ. We'll rewrite all those headers + // regardless but we still want to clear it. + uint32_t grow; + m_curSrcHdr->GetUint32Property("growKeywords", &grow); + if (grow) { + m_curSrcHdr->SetUint32Property("growKeywords", 0); + } + } + + auto out = + nsPrintfCString(HEADER_X_MOZILLA_STATUS ": %4.4x", msgFlags & 0xFFFF); + out.Append(m_eolSeq); + rv = WriteSpan(m_fileStream, out); + NS_ENSURE_SUCCESS(rv, rv); + + out = nsPrintfCString(HEADER_X_MOZILLA_STATUS2 ": %8.8x", + msgFlags & 0xFFFF0000); + out.Append(m_eolSeq); + rv = WriteSpan(m_fileStream, out); + NS_ENSURE_SUCCESS(rv, rv); + + // Try to leave room for future in-place keyword edits. + while (keywords.Length() < X_MOZILLA_KEYWORDS_BLANK_LEN) { + keywords.Append(' '); + } + out = nsPrintfCString(HEADER_X_MOZILLA_KEYWORDS ": %s", keywords.get()); + out.Append(m_eolSeq); + rv = WriteSpan(m_fileStream, out); + NS_ENSURE_SUCCESS(rv, rv); + + // Write out the rest of the headers. + for (auto const& hdr : headers) { + rv = WriteSpan(m_fileStream, buf.Subspan(hdr.pos, hdr.len)); + NS_ENSURE_SUCCESS(rv, rv); + } + + // The header parser consumes the blank line, If we've completed parsing + // we need to output it now. + // If we haven't parsed all the headers yet then the blank line will be + // safely copied verbatim as part of the remaining data. + if (rdr.IsComplete()) { + rv = WriteSpan(m_fileStream, m_eolSeq); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Write out everything else in the buffer verbatim. + if (leftover.Length() > 0) { + rv = WriteSpan(m_fileStream, leftover); + NS_ENSURE_SUCCESS(rv, rv); + } + m_bufferCount = 0; + return NS_OK; +} + +/** + * nsOfflineStoreCompactState is a helper class for nsFolderCompactor which + * handles compacting the mbox for a single offline IMAP folder. + * + * nsOfflineStoreCompactState does *not* do any special X-Mozilla-* header + * handling, unlike the base class. + * + * NOTE (for future cleanups): + * This class uses a different mechanism to iterate through messages. It uses + * nsIMsgMessageService.streamMessage() to stream each message in turn, + * passing itself in as an nsIStreamListener. The nsICopyMessageStreamListener + * callbacks implemented in the base class are _not_ used here. + * For each message, the standard OnStartRequest(), OnDataAvailable()..., + * OnStopRequest() sequence is seen. + * Nothing too fancy, but it's not always clear where code from the base class + * is being used and when it is not, so it can be complicated to pick through. + * + */ +class nsOfflineStoreCompactState : public nsFolderCompactState { + public: + nsOfflineStoreCompactState(void); + virtual ~nsOfflineStoreCompactState(void); + NS_IMETHOD OnStopRequest(nsIRequest* request, nsresult status) override; + NS_IMETHODIMP OnDataAvailable(nsIRequest* request, nsIInputStream* inStr, + uint64_t sourceOffset, uint32_t count) override; + + protected: + nsresult CopyNextMessage(bool& done); + virtual nsresult InitDB(nsIMsgDatabase* db) override; + virtual nsresult StartCompacting() override; + virtual nsresult FinishCompact() override; + + char m_dataBuffer[COMPACTOR_READ_BUFF_SIZE + 1]; // temp data buffer for + // copying message + uint32_t m_offlineMsgSize; +}; + +nsOfflineStoreCompactState::nsOfflineStoreCompactState() + : m_offlineMsgSize(0) {} + +nsOfflineStoreCompactState::~nsOfflineStoreCompactState() {} + +nsresult nsOfflineStoreCompactState::InitDB(nsIMsgDatabase* db) { + // Start with the list of messages we have offline as the possible + // message to keep when compacting the offline store. + db->ListAllOfflineMsgs(m_keys); + m_db = db; + return NS_OK; +} + +/** + * This will copy one message to the offline store, but if it fails to + * copy the next message, it will keep trying messages until it finds one + * it can copy, or it runs out of messages. + */ +nsresult nsOfflineStoreCompactState::CopyNextMessage(bool& done) { + while (m_curIndex < m_keys.Length()) { + // Filter out msgs that have the "pendingRemoval" attribute set. + nsCOMPtr<nsIMsgDBHdr> hdr; + nsCString pendingRemoval; + nsresult rv = + m_db->GetMsgHdrForKey(m_keys[m_curIndex], getter_AddRefs(hdr)); + NS_ENSURE_SUCCESS(rv, rv); + hdr->GetStringProperty("pendingRemoval", pendingRemoval); + if (!pendingRemoval.IsEmpty()) { + m_curIndex++; + // Turn off offline flag for message, since after the compact is + // completed; we won't have the message in the offline store. + uint32_t resultFlags; + hdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags); + // We need to clear this in case the user changes the offline retention + // settings. + hdr->SetStringProperty("pendingRemoval", ""_ns); + continue; + } + m_messageUri.Truncate(); // clear the previous message uri + rv = BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex], + m_messageUri); + NS_ENSURE_SUCCESS(rv, rv); + m_startOfMsg = true; + nsCOMPtr<nsISupports> thisSupports; + QueryInterface(NS_GET_IID(nsISupports), getter_AddRefs(thisSupports)); + nsCOMPtr<nsIURI> dummyNull; + rv = m_messageService->StreamMessage(m_messageUri, thisSupports, m_window, + nullptr, false, EmptyCString(), true, + getter_AddRefs(dummyNull)); + // if copy fails, we clear the offline flag on the source message. + if (NS_FAILED(rv)) { + nsCOMPtr<nsIMsgDBHdr> hdr; + m_messageService->MessageURIToMsgHdr(m_messageUri, getter_AddRefs(hdr)); + if (hdr) { + uint32_t resultFlags; + hdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags); + } + m_curIndex++; + continue; + } else + break; + } + done = m_curIndex >= m_keys.Length(); + // In theory, we might be able to stream the next message, so + // return NS_OK, not rv. + return NS_OK; +} + +NS_IMETHODIMP +nsOfflineStoreCompactState::OnStopRequest(nsIRequest* request, + nsresult status) { + nsresult rv = status; + nsCOMPtr<nsIURI> uri; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + nsCOMPtr<nsIChannel> channel; + bool done = false; + + // The NS_MSG_ERROR_MSG_NOT_OFFLINE error should allow us to continue, so we + // check for it specifically and don't terminate the compaction. + if (NS_FAILED(rv) && rv != NS_MSG_ERROR_MSG_NOT_OFFLINE) goto done; + + // We know the request is an nsIChannel we can get a URI from, but this is + // probably bad form. See Bug 1528662. + channel = do_QueryInterface(request, &rv); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "error QI nsIRequest to nsIChannel failed"); + if (NS_FAILED(rv)) goto done; + rv = channel->GetURI(getter_AddRefs(uri)); + if (NS_FAILED(rv)) goto done; + rv = m_messageService->MessageURIToMsgHdr(m_messageUri, + getter_AddRefs(msgHdr)); + if (NS_FAILED(rv)) goto done; + + // This is however an unexpected condition, so let's print a warning. + if (rv == NS_MSG_ERROR_MSG_NOT_OFFLINE) { + nsAutoCString spec; + uri->GetSpec(spec); + nsPrintfCString msg("Message expectedly not available offline: %s", + spec.get()); + NS_WARNING(msg.get()); + } + + if (msgHdr) { + if (NS_SUCCEEDED(status)) { + msgHdr->SetMessageOffset(m_startOfNewMsg); + nsCString storeToken = nsPrintfCString("%" PRIu64, m_startOfNewMsg); + msgHdr->SetStringProperty("storeToken", storeToken); + msgHdr->SetOfflineMessageSize(m_offlineMsgSize); + } else { + uint32_t resultFlags; + msgHdr->AndFlags(~nsMsgMessageFlags::Offline, &resultFlags); + } + } + + if (m_window) { + m_window->GetStatusFeedback(getter_AddRefs(statusFeedback)); + if (statusFeedback) + statusFeedback->ShowProgress(100 * m_curIndex / m_keys.Length()); + } + // advance to next message + m_curIndex++; + rv = CopyNextMessage(done); + if (done) { + m_db->Commit(nsMsgDBCommitType::kCompressCommit); + msgHdr = nullptr; + // no more to copy finish it up + ReleaseFolderLock(); + FinishCompact(); + NS_RELEASE_THIS(); // kill self + } + +done: + if (NS_FAILED(rv)) { + m_status = rv; // set the status to rv so the destructor can remove the + // temp folder and database + ReleaseFolderLock(); + NS_RELEASE_THIS(); // kill self + + if (m_completionFn) { + m_completionFn(m_status, m_totalExpungedBytes); + } + return rv; + } + return rv; +} + +nsresult nsOfflineStoreCompactState::FinishCompact() { + // All okay time to finish up the compact process + nsCOMPtr<nsIFile> path; + uint32_t flags; + + // get leaf name and database name of the folder + m_folder->GetFlags(&flags); + nsresult rv = m_folder->GetFilePath(getter_AddRefs(path)); + + nsCString leafName; + path->GetNativeLeafName(leafName); + + if (m_fileStream) { + // close down the temp file stream; preparing for deleting the old folder + // and its database; then rename the temp folder and database + m_fileStream->Flush(); + m_fileStream->Close(); + m_fileStream = nullptr; + } + + // make sure the new database is valid + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (dbFolderInfo) dbFolderInfo->SetExpungedBytes(0); + // this forces the m_folder to update mExpungedBytes from the db folder info. + int64_t expungedBytes; + m_folder->GetExpungedBytes(&expungedBytes); + m_folder->UpdateSummaryTotals(true); + m_db->SetSummaryValid(true); + + // remove the old folder + path->Remove(false); + + // rename the copied folder to be the original folder + m_file->MoveToNative((nsIFile*)nullptr, leafName); + + ShowStatusMsg(EmptyString()); + m_folder->NotifyCompactCompleted(); + if (m_completionFn) { + m_completionFn(NS_OK, m_totalExpungedBytes); + } + return rv; +} + +NS_IMETHODIMP +nsFolderCompactState::Init(nsICopyMessageListener* destination) { + return NS_OK; +} + +NS_IMETHODIMP +nsFolderCompactState::StartMessage() { + nsresult rv = NS_ERROR_FAILURE; + NS_ASSERTION(m_fileStream, "Fatal, null m_fileStream..."); + if (m_fileStream) { + nsCOMPtr<nsISeekableStream> seekableStream = + do_QueryInterface(m_fileStream, &rv); + NS_ENSURE_SUCCESS(rv, rv); + // this will force an internal flush, but not a sync. Tell should really do + // an internal flush, but it doesn't, and I'm afraid to change that + // nsIFileStream.cpp code anymore. + seekableStream->Seek(nsISeekableStream::NS_SEEK_CUR, 0); + // record the new message key for the message + int64_t curStreamPos; + seekableStream->Tell(&curStreamPos); + m_startOfNewMsg = curStreamPos; + rv = NS_OK; + } + return rv; +} + +NS_IMETHODIMP +nsFolderCompactState::EndMessage(nsMsgKey key) { return NS_OK; } + +// XXX TODO: This function is sadly lacking all status checking, it always +// returns NS_OK and moves onto the next message. +NS_IMETHODIMP +nsFolderCompactState::EndCopy(nsIURI* uri, nsresult status) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgDBHdr> newMsgHdr; + + if (m_curIndex >= m_keys.Length()) { + NS_ASSERTION(false, "m_curIndex out of bounds"); + return NS_OK; + } + + // Take note of the end offset of the message (without the trailing blank + // line). + nsCOMPtr<nsITellableStream> tellable(do_QueryInterface(m_fileStream)); + MOZ_ASSERT(tellable); + int64_t endOfMsg; + nsresult rv = tellable->Tell(&endOfMsg); + NS_ENSURE_SUCCESS(rv, rv); + + /* Messages need to have trailing blank lines */ + rv = WriteSpan(m_fileStream, m_eolSeq); + NS_ENSURE_SUCCESS(rv, rv); + + /* + * Done with the current message; copying the existing message header + * to the new database. + */ + if (m_curSrcHdr) { + nsMsgKey key; + m_curSrcHdr->GetMessageKey(&key); + m_db->CopyHdrFromExistingHdr(key, m_curSrcHdr, true, + getter_AddRefs(newMsgHdr)); + } + m_curSrcHdr = nullptr; + if (newMsgHdr) { + nsCString storeToken = nsPrintfCString("%" PRIu64, m_startOfNewMsg); + newMsgHdr->SetStringProperty("storeToken", storeToken); + newMsgHdr->SetMessageOffset(m_startOfNewMsg); + uint64_t msgSize = endOfMsg - m_startOfNewMsg; + newMsgHdr->SetMessageSize(msgSize); + + m_totalMsgSize += msgSize + m_eolSeq.Length(); + } + + // m_db->Commit(nsMsgDBCommitType::kLargeCommit); // no sense committing + // until the end + // advance to next message + m_curIndex++; + m_startOfMsg = true; + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + if (m_window) { + m_window->GetStatusFeedback(getter_AddRefs(statusFeedback)); + if (statusFeedback) + statusFeedback->ShowProgress(100 * m_curIndex / m_keys.Length()); + } + return NS_OK; +} + +nsresult nsOfflineStoreCompactState::StartCompacting() { + nsresult rv = NS_OK; + if (m_keys.Length() > 0 && m_curIndex == 0) { + NS_ADDREF_THIS(); // we own ourselves, until we're done, anyway. + ShowCompactingStatusMsg(); + bool done = false; + rv = CopyNextMessage(done); + if (!done) return rv; + } + ReleaseFolderLock(); + FinishCompact(); + return rv; +} + +NS_IMETHODIMP +nsOfflineStoreCompactState::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, + uint32_t count) { + if (!m_fileStream || !inStr) return NS_ERROR_FAILURE; + + nsresult rv = NS_OK; + + if (m_startOfMsg) { + m_offlineMsgSize = 0; + m_messageUri.Truncate(); // clear the previous message uri + if (NS_SUCCEEDED(BuildMessageURI(m_baseMessageUri.get(), m_keys[m_curIndex], + m_messageUri))) { + rv = m_messageService->MessageURIToMsgHdr(m_messageUri, + getter_AddRefs(m_curSrcHdr)); + NS_ENSURE_SUCCESS(rv, rv); + } + } + uint32_t maxReadCount, readCount, writeCount; + uint32_t bytesWritten; + + while (NS_SUCCEEDED(rv) && (int32_t)count > 0) { + maxReadCount = + count > sizeof(m_dataBuffer) - 1 ? sizeof(m_dataBuffer) - 1 : count; + writeCount = 0; + rv = inStr->Read(m_dataBuffer, maxReadCount, &readCount); + + if (NS_SUCCEEDED(rv)) { + if (m_startOfMsg) { + m_startOfMsg = false; + // check if there's an envelope header; if not, write one. + if (strncmp(m_dataBuffer, "From ", 5)) { + m_fileStream->Write("From " CRLF, 7, &bytesWritten); + m_offlineMsgSize += bytesWritten; + } + } + m_fileStream->Write(m_dataBuffer, readCount, &bytesWritten); + m_offlineMsgSize += bytesWritten; + writeCount += bytesWritten; + count -= readCount; + if (writeCount != readCount) { + m_folder->ThrowAlertMsg("compactFolderWriteFailed", m_window); + return NS_MSG_ERROR_WRITING_MAIL_FOLDER; + } + } + } + return rv; +} + +////////////////////////////////////////////////////////////////////////////// +// nsMsgFolderCompactor implementation +////////////////////////////////////////////////////////////////////////////// + +NS_IMPL_ISUPPORTS(nsMsgFolderCompactor, nsIMsgFolderCompactor) + +nsMsgFolderCompactor::nsMsgFolderCompactor() {} + +nsMsgFolderCompactor::~nsMsgFolderCompactor() {} + +NS_IMETHODIMP nsMsgFolderCompactor::CompactFolders( + const nsTArray<RefPtr<nsIMsgFolder>>& folders, nsIUrlListener* listener, + nsIMsgWindow* window) { + MOZ_ASSERT(mQueue.IsEmpty()); + mWindow = window; + mListener = listener; + mTotalBytesGained = 0; + mQueue = folders.Clone(); + mQueue.Reverse(); + + // Can't guarantee that anyone will keep us in scope until we're done, so... + MOZ_ASSERT(!mKungFuDeathGrip); + mKungFuDeathGrip = this; + + // nsIMsgFolderCompactor idl states this isn't called... + // but maybe it should be? + // if (mListener) { + // mListener->OnStartRunningUrl(nullptr); + // } + + NextFolder(); + + return NS_OK; +} + +void nsMsgFolderCompactor::NextFolder() { + while (!mQueue.IsEmpty()) { + // Should only ever have one compactor running. + MOZ_ASSERT(mCompactor == nullptr); + + nsCOMPtr<nsIMsgFolder> folder = mQueue.PopLastElement(); + + // Sanity check - should we be compacting this folder? + nsCOMPtr<nsIMsgPluggableStore> msgStore; + nsresult rv = folder->GetMsgStore(getter_AddRefs(msgStore)); + if (NS_FAILED(rv)) { + NS_WARNING("Skipping folder with no msgStore"); + continue; + } + bool storeSupportsCompaction; + msgStore->GetSupportsCompaction(&storeSupportsCompaction); + if (!storeSupportsCompaction) { + NS_WARNING("Trying to compact a non-mbox folder"); + continue; // just skip it. + } + + nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(folder)); + if (imapFolder) { + uint32_t flags; + folder->GetFlags(&flags); + if (flags & nsMsgFolderFlags::Offline) { + mCompactor = new nsOfflineStoreCompactState(); + } + } else { + mCompactor = new nsFolderCompactState(); + } + if (!mCompactor) { + NS_WARNING("skipping compact of non-offline folder"); + continue; + } + nsCString uri; + folder->GetURI(uri); + + // Callback for when a folder compaction completes. + auto completionFn = [self = RefPtr<nsMsgFolderCompactor>(this), + compactState = mCompactor](nsresult status, + uint64_t expungedBytes) { + if (NS_SUCCEEDED(status)) { + self->mTotalBytesGained += expungedBytes; + } else { + // Failed. We want to keep going with the next folder, but make sure + // we return a failing code upon overall completion. + self->mOverallStatus = status; + NS_WARNING("folder compact failed."); + } + + // Release our lock on the compactor - it's done. + self->mCompactor = nullptr; + self->NextFolder(); + }; + + rv = mCompactor->Compact(folder, completionFn, mWindow); + if (NS_SUCCEEDED(rv)) { + // Now wait for the compactor to let us know it's finished, + // via the completion callback fn. + return; + } + mOverallStatus = rv; + mCompactor = nullptr; + NS_WARNING("folder compact failed - skipping folder"); + } + + // Done. No more folders to compact. + + if (mListener) { + // If there were multiple failures, this will communicate only the + // last one, but that's OK. Main thing is to indicate that _something_ + // went wrong. + mListener->OnStopRunningUrl(nullptr, mOverallStatus); + } + ShowDoneStatus(); + + // We're not needed any more. + mKungFuDeathGrip = nullptr; + return; +} + +void nsMsgFolderCompactor::ShowDoneStatus() { + if (!mWindow) { + return; + } + nsCOMPtr<nsIStringBundle> bundle; + nsresult rv = GetBaseStringBundle(getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS_VOID(rv); + nsAutoString expungedAmount; + FormatFileSize(mTotalBytesGained, true, expungedAmount); + AutoTArray<nsString, 1> params = {expungedAmount}; + nsString msg; + rv = bundle->FormatStringFromName("compactingDone", params, msg); + NS_ENSURE_SUCCESS_VOID(rv); + + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + mWindow->GetStatusFeedback(getter_AddRefs(statusFeedback)); + if (statusFeedback) { + statusFeedback->SetStatusString(msg); + } +} diff --git a/comm/mailnews/base/src/nsMsgFolderCompactor.h b/comm/mailnews/base/src/nsMsgFolderCompactor.h new file mode 100644 index 0000000000..b9de16be12 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgFolderCompactor.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgFolderCompactor_h +#define _nsMsgFolderCompactor_h + +#include "nsIMsgFolderCompactor.h" + +class nsIMsgFolder; +class nsIMsgWindow; +class nsFolderCompactState; + +/** + * nsMsgFolderCompactor implements nsIMsgFolderCompactor, which allows the + * caller to kick off a batch of folder compactions (via compactFolders()). + */ +class nsMsgFolderCompactor : public nsIMsgFolderCompactor { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGFOLDERCOMPACTOR + + nsMsgFolderCompactor(); + + protected: + virtual ~nsMsgFolderCompactor(); + // The folders waiting to be compacted. + nsTArray<RefPtr<nsIMsgFolder>> mQueue; + + // If any individual folders fail to compact, we stash the latest fail code + // here (to return via listener upon overall completion). + nsresult mOverallStatus{NS_OK}; + + // If set, OnStopRunningUrl() will be called when all folders done. + nsCOMPtr<nsIUrlListener> mListener; + // If set, progress status updates will be sent here. + nsCOMPtr<nsIMsgWindow> mWindow; + RefPtr<nsMsgFolderCompactor> mKungFuDeathGrip; + uint64_t mTotalBytesGained{0}; + + // The currently-running compactor. + RefPtr<nsFolderCompactState> mCompactor; + + void NextFolder(); + void ShowDoneStatus(); +}; +#endif diff --git a/comm/mailnews/base/src/nsMsgFolderNotificationService.cpp b/comm/mailnews/base/src/nsMsgFolderNotificationService.cpp new file mode 100644 index 0000000000..7983a8ec56 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgFolderNotificationService.cpp @@ -0,0 +1,174 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgFolderNotificationService.h" +#include "nsIMsgHdr.h" +#include "nsIMsgFolder.h" +#include "nsIMsgImapMailFolder.h" +#include "nsIImapIncomingServer.h" + +// +// nsMsgFolderNotificationService +// +NS_IMPL_ISUPPORTS(nsMsgFolderNotificationService, + nsIMsgFolderNotificationService) + +nsMsgFolderNotificationService::nsMsgFolderNotificationService() {} + +nsMsgFolderNotificationService::~nsMsgFolderNotificationService() { + /* destructor code */ +} + +NS_IMETHODIMP nsMsgFolderNotificationService::GetHasListeners( + bool* aHasListeners) { + NS_ENSURE_ARG_POINTER(aHasListeners); + *aHasListeners = mListeners.Length() > 0; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::AddListener( + nsIMsgFolderListener* aListener, msgFolderListenerFlag aFlags) { + NS_ENSURE_ARG_POINTER(aListener); + MsgFolderListener listener(aListener, aFlags); + mListeners.AppendElementUnlessExists(listener); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::RemoveListener( + nsIMsgFolderListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + + mListeners.RemoveElement(aListener); + return NS_OK; +} + +#define NOTIFY_MSGFOLDER_LISTENERS(propertyflag_, propertyfunc_, params_) \ + PR_BEGIN_MACRO \ + nsTObserverArray<MsgFolderListener>::ForwardIterator iter(mListeners); \ + while (iter.HasMore()) { \ + const MsgFolderListener& listener = iter.GetNext(); \ + if (listener.mFlags & propertyflag_) \ + listener.mListener->propertyfunc_ params_; \ + } \ + PR_END_MACRO + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgAdded( + nsIMsgDBHdr* aMsg) { + NOTIFY_MSGFOLDER_LISTENERS(msgAdded, MsgAdded, (aMsg)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsClassified( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgs, bool aJunkProcessed, + bool aTraitProcessed) { + NOTIFY_MSGFOLDER_LISTENERS(msgsClassified, MsgsClassified, + (aMsgs, aJunkProcessed, aTraitProcessed)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsJunkStatusChanged( + const nsTArray<RefPtr<nsIMsgDBHdr>>& messages) { + NOTIFY_MSGFOLDER_LISTENERS(msgsJunkStatusChanged, MsgsJunkStatusChanged, + (messages)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsDeleted( + const nsTArray<RefPtr<nsIMsgDBHdr>>& aMsgs) { + NOTIFY_MSGFOLDER_LISTENERS(msgsDeleted, MsgsDeleted, (aMsgs)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyMsgsMoveCopyCompleted( + bool aMove, const nsTArray<RefPtr<nsIMsgDBHdr>>& aSrcMsgs, + nsIMsgFolder* aDestFolder, const nsTArray<RefPtr<nsIMsgDBHdr>>& aDestMsgs) { + // IMAP delete model means that a "move" isn't really a move, it is a copy, + // followed by storing the IMAP deleted flag on the message. + bool isReallyMove = aMove; + if (aMove && !mListeners.IsEmpty() && !aSrcMsgs.IsEmpty()) { + nsresult rv; + // Assume that all the source messages are from the same server. + nsCOMPtr<nsIMsgFolder> msgFolder; + rv = aSrcMsgs[0]->GetFolder(getter_AddRefs(msgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgImapMailFolder> imapFolder(do_QueryInterface(msgFolder)); + if (imapFolder) { + nsCOMPtr<nsIImapIncomingServer> imapServer; + imapFolder->GetImapIncomingServer(getter_AddRefs(imapServer)); + if (imapServer) { + nsMsgImapDeleteModel deleteModel; + imapServer->GetDeleteModel(&deleteModel); + if (deleteModel == nsMsgImapDeleteModels::IMAPDelete) + isReallyMove = false; + } + } + } + + NOTIFY_MSGFOLDER_LISTENERS(msgsMoveCopyCompleted, MsgsMoveCopyCompleted, + (isReallyMove, aSrcMsgs, aDestFolder, aDestMsgs)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFolderNotificationService::NotifyMsgKeyChanged(nsMsgKey aOldKey, + nsIMsgDBHdr* aNewHdr) { + NOTIFY_MSGFOLDER_LISTENERS(msgKeyChanged, MsgKeyChanged, (aOldKey, aNewHdr)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgFolderNotificationService::NotifyMsgUnincorporatedMoved( + nsIMsgFolder* srcFolder, nsIMsgDBHdr* msg) { + NOTIFY_MSGFOLDER_LISTENERS(msgUnincorporatedMoved, MsgUnincorporatedMoved, + (srcFolder, msg)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderAdded( + nsIMsgFolder* aFolder) { + NOTIFY_MSGFOLDER_LISTENERS(folderAdded, FolderAdded, (aFolder)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderDeleted( + nsIMsgFolder* aFolder) { + NOTIFY_MSGFOLDER_LISTENERS(folderDeleted, FolderDeleted, (aFolder)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderMoveCopyCompleted( + bool aMove, nsIMsgFolder* aSrcFolder, nsIMsgFolder* aDestFolder) { + NOTIFY_MSGFOLDER_LISTENERS(folderMoveCopyCompleted, FolderMoveCopyCompleted, + (aMove, aSrcFolder, aDestFolder)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderRenamed( + nsIMsgFolder* aOrigFolder, nsIMsgFolder* aNewFolder) { + NOTIFY_MSGFOLDER_LISTENERS(folderRenamed, FolderRenamed, + (aOrigFolder, aNewFolder)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderCompactStart( + nsIMsgFolder* folder) { + NOTIFY_MSGFOLDER_LISTENERS(folderCompactStart, FolderCompactStart, (folder)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderCompactFinish( + nsIMsgFolder* folder) { + NOTIFY_MSGFOLDER_LISTENERS(folderCompactFinish, FolderCompactFinish, + (folder)); + return NS_OK; +} +NS_IMETHODIMP nsMsgFolderNotificationService::NotifyFolderReindexTriggered( + nsIMsgFolder* folder) { + NOTIFY_MSGFOLDER_LISTENERS(folderReindexTriggered, FolderReindexTriggered, + (folder)); + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgFolderNotificationService.h b/comm/mailnews/base/src/nsMsgFolderNotificationService.h new file mode 100644 index 0000000000..fecf3b3064 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgFolderNotificationService.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgFolderNotificationService_h__ +#define nsMsgFolderNotificationService_h__ + +#include "nsIMsgFolderNotificationService.h" +#include "nsIMsgFolderListener.h" +#include "nsTObserverArray.h" +#include "nsCOMPtr.h" + +class nsMsgFolderNotificationService final + : public nsIMsgFolderNotificationService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGFOLDERNOTIFICATIONSERVICE + + nsMsgFolderNotificationService(); + + private: + ~nsMsgFolderNotificationService(); + struct MsgFolderListener { + nsCOMPtr<nsIMsgFolderListener> mListener; + msgFolderListenerFlag mFlags; + + MsgFolderListener(nsIMsgFolderListener* aListener, + msgFolderListenerFlag aFlags) + : mListener(aListener), mFlags(aFlags) {} + MsgFolderListener(const MsgFolderListener& aListener) + : mListener(aListener.mListener), mFlags(aListener.mFlags) {} + ~MsgFolderListener() {} + + int operator==(nsIMsgFolderListener* aListener) const { + return mListener == aListener; + } + int operator==(const MsgFolderListener& aListener) const { + return mListener == aListener.mListener; + } + }; + + nsTObserverArray<MsgFolderListener> mListeners; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgGroupThread.cpp b/comm/mailnews/base/src/nsMsgGroupThread.cpp new file mode 100644 index 0000000000..32a132d27a --- /dev/null +++ b/comm/mailnews/base/src/nsMsgGroupThread.cpp @@ -0,0 +1,731 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgGroupThread.h" +#include "nsMsgDBView.h" +#include "nsMsgMessageFlags.h" +#include "nsMsgUtils.h" +#include "nsSimpleEnumerator.h" + +NS_IMPL_ISUPPORTS(nsMsgGroupThread, nsIMsgThread) + +nsMsgGroupThread::nsMsgGroupThread() { Init(); } + +nsMsgGroupThread::nsMsgGroupThread(nsIMsgDatabase* db) { + m_db = db; + Init(); +} + +void nsMsgGroupThread::Init() { + m_threadKey = nsMsgKey_None; + m_threadRootKey = nsMsgKey_None; + m_numUnreadChildren = 0; + m_flags = 0; + m_newestMsgDate = 0; + m_dummy = false; +} + +nsMsgGroupThread::~nsMsgGroupThread() {} + +NS_IMETHODIMP nsMsgGroupThread::SetThreadKey(nsMsgKey threadKey) { + m_threadKey = threadKey; + // by definition, the initial thread key is also the thread root key. + m_threadRootKey = threadKey; + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupThread::GetThreadKey(nsMsgKey* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = m_threadKey; + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupThread::GetFlags(uint32_t* aFlags) { + NS_ENSURE_ARG_POINTER(aFlags); + *aFlags = m_flags; + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupThread::SetFlags(uint32_t aFlags) { + m_flags = aFlags; + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupThread::SetSubject(const nsACString& aSubject) { + NS_ASSERTION(false, "shouldn't call this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgGroupThread::GetSubject(nsACString& result) { + NS_ASSERTION(false, "shouldn't call this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgGroupThread::GetNumChildren(uint32_t* aNumChildren) { + NS_ENSURE_ARG_POINTER(aNumChildren); + *aNumChildren = m_keys.Length(); // - ((m_dummy) ? 1 : 0); + return NS_OK; +} + +uint32_t nsMsgGroupThread::NumRealChildren() { + return m_keys.Length() - ((m_dummy) ? 1 : 0); +} + +NS_IMETHODIMP nsMsgGroupThread::GetNumUnreadChildren( + uint32_t* aNumUnreadChildren) { + NS_ENSURE_ARG_POINTER(aNumUnreadChildren); + *aNumUnreadChildren = m_numUnreadChildren; + return NS_OK; +} + +void nsMsgGroupThread::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) { + nsMsgKey msgKey; + hdr->GetMessageKey(&msgKey); + m_keys.InsertElementAt(index, msgKey); +} + +void nsMsgGroupThread::SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) { + nsMsgKey msgKey; + hdr->GetMessageKey(&msgKey); + m_keys[index] = msgKey; +} + +nsMsgViewIndex nsMsgGroupThread::FindMsgHdr(nsIMsgDBHdr* hdr) { + nsMsgKey msgKey; + hdr->GetMessageKey(&msgKey); + return (nsMsgViewIndex)m_keys.IndexOf(msgKey); +} + +NS_IMETHODIMP nsMsgGroupThread::AddChild(nsIMsgDBHdr* child, + nsIMsgDBHdr* inReplyTo, + bool threadInThread, + nsIDBChangeAnnouncer* announcer) { + NS_ASSERTION(false, "shouldn't call this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +nsMsgViewIndex nsMsgGroupThread::AddMsgHdrInDateOrder(nsIMsgDBHdr* child, + nsMsgDBView* view) { + nsMsgKey newHdrKey; + child->GetMessageKey(&newHdrKey); + uint32_t insertIndex = 0; + // since we're sorted by date, we could do a binary search for the + // insert point. Or, we could start at the end... + if (m_keys.Length() > 0) { + nsMsgViewSortTypeValue sortType; + nsMsgViewSortOrderValue sortOrder; + (void)view->GetSortType(&sortType); + (void)view->GetSortOrder(&sortOrder); + // historical behavior is ascending date order unless our primary sort is + // on date + nsMsgViewSortOrderValue threadSortOrder = + (sortType == nsMsgViewSortType::byDate && + sortOrder == nsMsgViewSortOrder::descending) + ? nsMsgViewSortOrder::descending + : nsMsgViewSortOrder::ascending; + // new behavior is tricky and uses the secondary sort order if the secondary + // sort is on the date + nsMsgViewSortTypeValue secondarySortType; + nsMsgViewSortOrderValue secondarySortOrder; + (void)view->GetSecondarySortType(&secondarySortType); + (void)view->GetSecondarySortOrder(&secondarySortOrder); + if (secondarySortType == nsMsgViewSortType::byDate) + threadSortOrder = secondarySortOrder; + // sort by date within group. + insertIndex = GetInsertIndexFromView(view, child, threadSortOrder); + } + m_keys.InsertElementAt(insertIndex, newHdrKey); + if (!insertIndex) m_threadRootKey = newHdrKey; + return insertIndex; +} + +nsMsgViewIndex nsMsgGroupThread::GetInsertIndexFromView( + nsMsgDBView* view, nsIMsgDBHdr* child, + nsMsgViewSortOrderValue threadSortOrder) { + return view->GetInsertIndexHelper(child, m_keys, nullptr, threadSortOrder, + nsMsgViewSortType::byDate); +} + +nsMsgViewIndex nsMsgGroupThread::AddChildFromGroupView(nsIMsgDBHdr* child, + nsMsgDBView* view) { + uint32_t newHdrFlags = 0; + uint32_t msgDate; + nsMsgKey newHdrKey = 0; + + child->GetFlags(&newHdrFlags); + child->GetMessageKey(&newHdrKey); + child->GetDateInSeconds(&msgDate); + if (msgDate > m_newestMsgDate) SetNewestMsgDate(msgDate); + + child->AndFlags(~(nsMsgMessageFlags::Watched), &newHdrFlags); + uint32_t numChildren; + + // get the num children before we add the new header. + GetNumChildren(&numChildren); + + // if this is an empty thread, set the root key to this header's key + if (numChildren == 0) m_threadRootKey = newHdrKey; + + if (!(newHdrFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(1); + + return AddMsgHdrInDateOrder(child, view); +} + +nsresult nsMsgGroupThread::ReparentNonReferenceChildrenOf( + nsIMsgDBHdr* topLevelHdr, nsMsgKey newParentKey, + nsIDBChangeAnnouncer* announcer) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupThread::GetChildKeyAt(uint32_t aIndex, + nsMsgKey* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + if (aIndex >= m_keys.Length()) return NS_ERROR_INVALID_ARG; + *aResult = m_keys[aIndex]; + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupThread::GetChildHdrAt(uint32_t aIndex, + nsIMsgDBHdr** aResult) { + if (aIndex >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND; + return m_db->GetMsgHdrForKey(m_keys[aIndex], aResult); +} + +NS_IMETHODIMP nsMsgGroupThread::GetChild(nsMsgKey msgKey, + nsIMsgDBHdr** aResult) { + return GetChildHdrAt(m_keys.IndexOf(msgKey), aResult); +} + +NS_IMETHODIMP nsMsgGroupThread::RemoveChildAt(uint32_t aIndex) { + NS_ENSURE_TRUE(aIndex < m_keys.Length(), NS_MSG_MESSAGE_NOT_FOUND); + + m_keys.RemoveElementAt(aIndex); + return NS_OK; +} + +nsresult nsMsgGroupThread::RemoveChild(nsMsgKey msgKey) { + m_keys.RemoveElement(msgKey); + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupThread::RemoveChildHdr( + nsIMsgDBHdr* child, nsIDBChangeAnnouncer* announcer) { + NS_ENSURE_ARG_POINTER(child); + + uint32_t flags; + nsMsgKey key; + + child->GetFlags(&flags); + child->GetMessageKey(&key); + + // if this was the newest msg, clear the newest msg date so we'll recalc. + uint32_t date; + child->GetDateInSeconds(&date); + if (date == m_newestMsgDate) SetNewestMsgDate(0); + + if (!(flags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(-1); + nsMsgViewIndex threadIndex = FindMsgHdr(child); + bool wasFirstChild = threadIndex == 0; + nsresult rv = RemoveChildAt(threadIndex); + // if we're deleting the root of a dummy thread, need to update the threadKey + // and the dummy header at position 0 + if (m_dummy && wasFirstChild && m_keys.Length() > 1) { + nsIMsgDBHdr* newRootChild; + rv = GetChildHdrAt(1, &newRootChild); + NS_ENSURE_SUCCESS(rv, rv); + SetMsgHdrAt(0, newRootChild); + } + + return rv; +} + +nsresult nsMsgGroupThread::ReparentChildrenOf(nsMsgKey oldParent, + nsMsgKey newParent, + nsIDBChangeAnnouncer* announcer) { + nsresult rv = NS_OK; + + uint32_t numChildren = 0; + GetNumChildren(&numChildren); + + if (numChildren > 0) { + nsCOMPtr<nsIMsgDBHdr> curHdr; + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + rv = GetChildHdrAt(childIndex, getter_AddRefs(curHdr)); + if (NS_SUCCEEDED(rv) && curHdr) { + nsMsgKey threadParent; + + curHdr->GetThreadParent(&threadParent); + if (threadParent == oldParent) { + nsMsgKey curKey; + + curHdr->SetThreadParent(newParent); + curHdr->GetMessageKey(&curKey); + if (announcer) + announcer->NotifyParentChangedAll(curKey, oldParent, newParent, + nullptr); + // if the old parent was the root of the thread, then only the first + // child gets promoted to root, and other children become children of + // the new root. + if (newParent == nsMsgKey_None) { + m_threadRootKey = curKey; + newParent = curKey; + } + } + } + } + } + return rv; +} + +NS_IMETHODIMP nsMsgGroupThread::MarkChildRead(bool bRead) { + ChangeUnreadChildCount(bRead ? -1 : 1); + return NS_OK; +} + +// this could be moved into utils, because I think it's the same as the db impl. +class nsMsgGroupThreadEnumerator : public nsBaseMsgEnumerator { + public: + // nsIMsgEnumerator support. + NS_IMETHOD GetNext(nsIMsgDBHdr** aItem) override; + NS_IMETHOD HasMoreElements(bool* aResult) override; + + // nsMsgGroupThreadEnumerator methods: + typedef nsresult (*nsMsgGroupThreadEnumeratorFilter)(nsIMsgDBHdr* hdr, + void* closure); + + nsMsgGroupThreadEnumerator(nsMsgGroupThread* thread, nsMsgKey startKey, + nsMsgGroupThreadEnumeratorFilter filter, + void* closure); + int32_t MsgKeyFirstChildIndex(nsMsgKey inMsgKey); + + protected: + virtual ~nsMsgGroupThreadEnumerator(); + + nsresult Prefetch(); + + nsCOMPtr<nsIMsgDBHdr> mResultHdr; + RefPtr<nsMsgGroupThread> mThread; + nsMsgKey mThreadParentKey; + nsMsgKey mFirstMsgKey; + int32_t mChildIndex; + bool mDone; + bool mNeedToPrefetch; + nsMsgGroupThreadEnumeratorFilter mFilter; + void* mClosure; + bool mFoundChildren; +}; + +nsMsgGroupThreadEnumerator::nsMsgGroupThreadEnumerator( + nsMsgGroupThread* thread, nsMsgKey startKey, + nsMsgGroupThreadEnumeratorFilter filter, void* closure) + : mDone(false), mFilter(filter), mClosure(closure), mFoundChildren(false) { + mThreadParentKey = startKey; + mChildIndex = 0; + mThread = thread; + mNeedToPrefetch = true; + mFirstMsgKey = nsMsgKey_None; + + nsresult rv = mThread->GetRootHdr(getter_AddRefs(mResultHdr)); + if (NS_SUCCEEDED(rv) && mResultHdr) mResultHdr->GetMessageKey(&mFirstMsgKey); + + uint32_t numChildren; + mThread->GetNumChildren(&numChildren); + + if (mThreadParentKey != nsMsgKey_None) { + nsMsgKey msgKey = nsMsgKey_None; + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + rv = mThread->GetChildHdrAt(childIndex, getter_AddRefs(mResultHdr)); + if (NS_SUCCEEDED(rv) && mResultHdr) { + mResultHdr->GetMessageKey(&msgKey); + if (msgKey == startKey) { + mChildIndex = MsgKeyFirstChildIndex(msgKey); + mDone = (mChildIndex < 0); + break; + } + + if (mDone) break; + } else + NS_ASSERTION(false, "couldn't get child from thread"); + } + } + +#ifdef DEBUG_bienvenu1 + nsCOMPtr<nsIMsgDBHdr> child; + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + rv = mThread->GetChildHdrAt(childIndex, getter_AddRefs(child)); + if (NS_SUCCEEDED(rv) && child) { + nsMsgKey threadParent; + nsMsgKey msgKey; + // we're only doing one level of threading, so check if caller is + // asking for children of the first message in the thread or not. + // if not, we will tell him there are no children. + child->GetMessageKey(&msgKey); + child->GetThreadParent(&threadParent); + + printf("index = %ld key = %ld parent = %lx\n", childIndex, msgKey, + threadParent); + } + } +#endif +} + +nsMsgGroupThreadEnumerator::~nsMsgGroupThreadEnumerator() {} + +int32_t nsMsgGroupThreadEnumerator::MsgKeyFirstChildIndex(nsMsgKey inMsgKey) { + // look through rest of thread looking for a child of this message. + // If the inMsgKey is the first message in the thread, then all children + // without parents are considered to be children of inMsgKey. + // Otherwise, only true children qualify. + uint32_t numChildren; + nsCOMPtr<nsIMsgDBHdr> curHdr; + int32_t firstChildIndex = -1; + + mThread->GetNumChildren(&numChildren); + + for (uint32_t curChildIndex = 0; curChildIndex < numChildren; + curChildIndex++) { + nsresult rv = mThread->GetChildHdrAt(curChildIndex, getter_AddRefs(curHdr)); + if (NS_SUCCEEDED(rv) && curHdr) { + nsMsgKey parentKey; + + curHdr->GetThreadParent(&parentKey); + if (parentKey == inMsgKey) { + firstChildIndex = curChildIndex; + break; + } + } + } +#ifdef DEBUG_bienvenu1 + printf("first child index of %ld = %ld\n", inMsgKey, firstChildIndex); +#endif + return firstChildIndex; +} + +NS_IMETHODIMP nsMsgGroupThreadEnumerator::GetNext(nsIMsgDBHdr** aItem) { + NS_ENSURE_ARG_POINTER(aItem); + nsresult rv = NS_OK; + + if (mNeedToPrefetch) rv = Prefetch(); + + if (NS_SUCCEEDED(rv) && mResultHdr) { + NS_ADDREF(*aItem = mResultHdr); + mNeedToPrefetch = true; + } + return rv; +} + +nsresult nsMsgGroupThreadEnumerator::Prefetch() { + nsresult rv = NS_OK; // XXX or should this default to an error? + mResultHdr = nullptr; + if (mThreadParentKey == nsMsgKey_None) { + rv = mThread->GetRootHdr(getter_AddRefs(mResultHdr)); + NS_ASSERTION(NS_SUCCEEDED(rv) && mResultHdr, + "better be able to get root hdr"); + mChildIndex = 0; // since root can be anywhere, set mChildIndex to 0. + } else if (!mDone) { + uint32_t numChildren; + mThread->GetNumChildren(&numChildren); + + while ((uint32_t)mChildIndex < numChildren) { + rv = mThread->GetChildHdrAt(mChildIndex++, getter_AddRefs(mResultHdr)); + if (NS_SUCCEEDED(rv) && mResultHdr) { + nsMsgKey parentKey; + nsMsgKey curKey; + + if (mFilter && NS_FAILED(mFilter(mResultHdr, mClosure))) { + mResultHdr = nullptr; + continue; + } + + mResultHdr->GetThreadParent(&parentKey); + mResultHdr->GetMessageKey(&curKey); + // if the parent is the same as the msg we're enumerating over, + // or the parentKey isn't set, and we're iterating over the top + // level message in the thread, then leave mResultHdr set to cur msg. + if (parentKey == mThreadParentKey || + (parentKey == nsMsgKey_None && mThreadParentKey == mFirstMsgKey && + curKey != mThreadParentKey)) + break; + mResultHdr = nullptr; + } else + NS_ASSERTION(false, "better be able to get child"); + } + // if (!mResultHdr && mThreadParentKey == mFirstMsgKey && !mFoundChildren && + // numChildren > 1) { + // mThread->ReparentMsgsWithInvalidParent(numChildren, mThreadParentKey); + // } + } + if (!mResultHdr) { + mDone = true; + return NS_ERROR_FAILURE; + } + if (NS_FAILED(rv)) { + mDone = true; + return rv; + } else + mNeedToPrefetch = false; + mFoundChildren = true; + +#ifdef DEBUG_bienvenu1 + nsMsgKey debugMsgKey; + mResultHdr->GetMessageKey(&debugMsgKey); + printf("next for %ld = %ld\n", mThreadParentKey, debugMsgKey); +#endif + + return rv; +} + +NS_IMETHODIMP nsMsgGroupThreadEnumerator::HasMoreElements(bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + if (mNeedToPrefetch) Prefetch(); + *aResult = !mDone; + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupThread::EnumerateMessages(nsMsgKey parentKey, + nsIMsgEnumerator** result) { + NS_ADDREF(*result = new nsMsgGroupThreadEnumerator(this, parentKey, nullptr, + nullptr)); + return NS_OK; +} + +#if 0 +nsresult nsMsgGroupThread::ReparentMsgsWithInvalidParent(uint32_t numChildren, nsMsgKey threadParentKey) +{ + nsresult ret = NS_OK; + // run through looking for messages that don't have a correct parent, + // i.e., a parent that's in the thread! + for (int32_t childIndex = 0; childIndex < (int32_t) numChildren; childIndex++) + { + nsCOMPtr<nsIMsgDBHdr> curChild; + ret = GetChildHdrAt(childIndex, getter_AddRefs(curChild)); + if (NS_SUCCEEDED(ret) && curChild) + { + nsMsgKey parentKey; + nsCOMPtr<nsIMsgDBHdr> parent; + + curChild->GetThreadParent(&parentKey); + + if (parentKey != nsMsgKey_None) + { + GetChild(parentKey, getter_AddRefs(parent)); + if (!parent) + curChild->SetThreadParent(threadParentKey); + } + } + } + return ret; +} +#endif + +NS_IMETHODIMP nsMsgGroupThread::GetRootHdr(nsIMsgDBHdr** result) { + NS_ENSURE_ARG_POINTER(result); + + *result = nullptr; + int32_t resultIndex = -1; + + if (m_threadRootKey != nsMsgKey_None) { + nsresult ret = GetChildHdrForKey(m_threadRootKey, result, &resultIndex); + if (NS_SUCCEEDED(ret) && *result) + return ret; + else { + printf("need to reset thread root key\n"); + uint32_t numChildren; + nsMsgKey threadParentKey = nsMsgKey_None; + GetNumChildren(&numChildren); + + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> curChild; + ret = GetChildHdrAt(childIndex, getter_AddRefs(curChild)); + if (NS_SUCCEEDED(ret) && curChild) { + nsMsgKey parentKey; + + curChild->GetThreadParent(&parentKey); + if (parentKey == nsMsgKey_None) { + NS_ASSERTION(!(*result), "two top level msgs, not good"); + curChild->GetMessageKey(&threadParentKey); + m_threadRootKey = threadParentKey; + curChild.forget(result); + } + } + } + if (*result) { + return NS_OK; + } + } + // if we can't get the thread root key, we'll just get the first hdr. + // there's a bug where sometimes we weren't resetting the thread root key + // when removing the thread root key. + } + return GetChildHdrAt(0, result); +} + +nsresult nsMsgGroupThread::ChangeUnreadChildCount(int32_t delta) { + m_numUnreadChildren += delta; + return NS_OK; +} + +nsresult nsMsgGroupThread::GetChildHdrForKey(nsMsgKey desiredKey, + nsIMsgDBHdr** result, + int32_t* resultIndex) { + NS_ENSURE_ARG_POINTER(result); + + nsresult rv = NS_OK; // XXX or should this default to an error? + uint32_t numChildren = 0; + GetNumChildren(&numChildren); + + uint32_t childIndex; + for (childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> child; + rv = GetChildHdrAt(childIndex, getter_AddRefs(child)); + if (NS_SUCCEEDED(rv) && child) { + nsMsgKey msgKey; + // we're only doing one level of threading, so check if caller is + // asking for children of the first message in the thread or not. + // if not, we will tell him there are no children. + child->GetMessageKey(&msgKey); + + if (msgKey == desiredKey) { + child.forget(result); + break; + } + } + } + if (resultIndex) *resultIndex = (int32_t)childIndex; + + return rv; +} + +NS_IMETHODIMP nsMsgGroupThread::GetFirstUnreadChild(nsIMsgDBHdr** result) { + NS_ENSURE_ARG_POINTER(result); + + uint32_t numChildren = 0; + GetNumChildren(&numChildren); + + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> child; + nsresult rv = GetChildHdrAt(childIndex, getter_AddRefs(child)); + if (NS_SUCCEEDED(rv) && child) { + nsMsgKey msgKey; + child->GetMessageKey(&msgKey); + + bool isRead; + rv = m_db->IsRead(msgKey, &isRead); + if (NS_SUCCEEDED(rv) && !isRead) { + child.forget(result); + break; + } + } + } + + return (*result) ? NS_OK : NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsMsgGroupThread::GetNewestMsgDate(uint32_t* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + // if this hasn't been set, figure it out by enumerating the msgs in the + // thread. + if (!m_newestMsgDate) { + nsresult rv = NS_OK; + + uint32_t numChildren = 0; + GetNumChildren(&numChildren); + + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> child; + rv = GetChildHdrAt(childIndex, getter_AddRefs(child)); + if (NS_SUCCEEDED(rv) && child) { + uint32_t msgDate; + child->GetDateInSeconds(&msgDate); + if (msgDate > m_newestMsgDate) m_newestMsgDate = msgDate; + } + } + } + *aResult = m_newestMsgDate; + return NS_OK; +} + +NS_IMETHODIMP nsMsgGroupThread::SetNewestMsgDate(uint32_t aNewestMsgDate) { + m_newestMsgDate = aNewestMsgDate; + return NS_OK; +} + +nsMsgXFGroupThread::nsMsgXFGroupThread() {} + +nsMsgXFGroupThread::~nsMsgXFGroupThread() {} + +NS_IMETHODIMP nsMsgXFGroupThread::GetNumChildren(uint32_t* aNumChildren) { + NS_ENSURE_ARG_POINTER(aNumChildren); + *aNumChildren = m_folders.Length(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgXFGroupThread::GetChildHdrAt(uint32_t aIndex, + nsIMsgDBHdr** aResult) { + if (aIndex >= m_folders.Length()) return NS_MSG_MESSAGE_NOT_FOUND; + return m_folders.ObjectAt(aIndex)->GetMessageHeader(m_keys[aIndex], aResult); +} + +NS_IMETHODIMP nsMsgXFGroupThread::GetChildKeyAt(uint32_t aIndex, + nsMsgKey* aResult) { + NS_ASSERTION(false, "shouldn't call this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgXFGroupThread::RemoveChildAt(uint32_t aIndex) { + NS_ENSURE_TRUE(aIndex < m_folders.Length(), NS_MSG_MESSAGE_NOT_FOUND); + + nsresult rv = nsMsgGroupThread::RemoveChildAt(aIndex); + NS_ENSURE_SUCCESS(rv, rv); + m_folders.RemoveElementAt(aIndex); + return NS_OK; +} + +void nsMsgXFGroupThread::InsertMsgHdrAt(nsMsgViewIndex index, + nsIMsgDBHdr* hdr) { + nsCOMPtr<nsIMsgFolder> folder; + hdr->GetFolder(getter_AddRefs(folder)); + m_folders.InsertObjectAt(folder, index); + nsMsgGroupThread::InsertMsgHdrAt(index, hdr); +} + +void nsMsgXFGroupThread::SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) { + nsCOMPtr<nsIMsgFolder> folder; + hdr->GetFolder(getter_AddRefs(folder)); + m_folders.ReplaceObjectAt(folder, index); + nsMsgGroupThread::SetMsgHdrAt(index, hdr); +} + +nsMsgViewIndex nsMsgXFGroupThread::FindMsgHdr(nsIMsgDBHdr* hdr) { + nsMsgKey msgKey; + hdr->GetMessageKey(&msgKey); + nsCOMPtr<nsIMsgFolder> folder; + hdr->GetFolder(getter_AddRefs(folder)); + size_t index = 0; + while (true) { + index = m_keys.IndexOf(msgKey, index); + if (index == m_keys.NoIndex || m_folders[index] == folder) break; + index++; + } + return (nsMsgViewIndex)index; +} + +nsMsgViewIndex nsMsgXFGroupThread::AddMsgHdrInDateOrder(nsIMsgDBHdr* child, + nsMsgDBView* view) { + nsMsgViewIndex insertIndex = + nsMsgGroupThread::AddMsgHdrInDateOrder(child, view); + nsCOMPtr<nsIMsgFolder> folder; + child->GetFolder(getter_AddRefs(folder)); + m_folders.InsertObjectAt(folder, insertIndex); + return insertIndex; +} +nsMsgViewIndex nsMsgXFGroupThread::GetInsertIndexFromView( + nsMsgDBView* view, nsIMsgDBHdr* child, + nsMsgViewSortOrderValue threadSortOrder) { + return view->GetInsertIndexHelper(child, m_keys, &m_folders, threadSortOrder, + nsMsgViewSortType::byDate); +} diff --git a/comm/mailnews/base/src/nsMsgGroupThread.h b/comm/mailnews/base/src/nsMsgGroupThread.h new file mode 100644 index 0000000000..735b9580e3 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgGroupThread.h @@ -0,0 +1,88 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/Attributes.h" +#include "msgCore.h" +#include "nsCOMArray.h" +#include "nsIMsgThread.h" +#include "MailNewsTypes.h" +#include "nsTArray.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgHdr.h" +#include "nsMsgDBView.h" + +class nsMsgGroupView; + +class nsMsgGroupThread : public nsIMsgThread { + public: + friend class nsMsgGroupView; + + nsMsgGroupThread(); + explicit nsMsgGroupThread(nsIMsgDatabase* db); + + NS_DECL_NSIMSGTHREAD + NS_DECL_ISUPPORTS + + protected: + virtual ~nsMsgGroupThread(); + + void Init(); + nsMsgViewIndex AddChildFromGroupView(nsIMsgDBHdr* child, nsMsgDBView* view); + nsresult RemoveChild(nsMsgKey msgKey); + nsresult RerootThread(nsIMsgDBHdr* newParentOfOldRoot, nsIMsgDBHdr* oldRoot, + nsIDBChangeAnnouncer* announcer); + + virtual nsMsgViewIndex AddMsgHdrInDateOrder(nsIMsgDBHdr* child, + nsMsgDBView* view); + virtual nsMsgViewIndex GetInsertIndexFromView( + nsMsgDBView* view, nsIMsgDBHdr* child, + nsMsgViewSortOrderValue threadSortOrder); + nsresult ReparentNonReferenceChildrenOf(nsIMsgDBHdr* topLevelHdr, + nsMsgKey newParentKey, + nsIDBChangeAnnouncer* announcer); + + nsresult ReparentChildrenOf(nsMsgKey oldParent, nsMsgKey newParent, + nsIDBChangeAnnouncer* announcer); + nsresult ChangeUnreadChildCount(int32_t delta); + nsresult GetChildHdrForKey(nsMsgKey desiredKey, nsIMsgDBHdr** result, + int32_t* resultIndex); + uint32_t NumRealChildren(); + virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr); + virtual void SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr); + virtual nsMsgViewIndex FindMsgHdr(nsIMsgDBHdr* hdr); + + nsMsgKey m_threadKey; + uint32_t m_numUnreadChildren; + uint32_t m_flags; + nsMsgKey m_threadRootKey; + uint32_t m_newestMsgDate; + nsTArray<nsMsgKey> m_keys; + bool m_dummy; // top level msg is a dummy, e.g., grouped by age. + nsCOMPtr<nsIMsgDatabase> m_db; // should we make a weak ref or just a ptr? +}; + +class nsMsgXFGroupThread : public nsMsgGroupThread { + public: + nsMsgXFGroupThread(); + + NS_IMETHOD GetNumChildren(uint32_t* aNumChildren) override; + NS_IMETHOD GetChildKeyAt(uint32_t aIndex, nsMsgKey* aResult) override; + NS_IMETHOD GetChildHdrAt(uint32_t aIndex, nsIMsgDBHdr** aResult) override; + NS_IMETHOD RemoveChildAt(uint32_t aIndex) override; + + protected: + virtual ~nsMsgXFGroupThread(); + + virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) override; + virtual void SetMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr) override; + virtual nsMsgViewIndex FindMsgHdr(nsIMsgDBHdr* hdr) override; + virtual nsMsgViewIndex AddMsgHdrInDateOrder(nsIMsgDBHdr* child, + nsMsgDBView* view) override; + virtual nsMsgViewIndex GetInsertIndexFromView( + nsMsgDBView* view, nsIMsgDBHdr* child, + nsMsgViewSortOrderValue threadSortOrder) override; + + nsCOMArray<nsIMsgFolder> m_folders; +}; diff --git a/comm/mailnews/base/src/nsMsgGroupView.cpp b/comm/mailnews/base/src/nsMsgGroupView.cpp new file mode 100644 index 0000000000..ca42d01a5a --- /dev/null +++ b/comm/mailnews/base/src/nsMsgGroupView.cpp @@ -0,0 +1,941 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgUtils.h" +#include "nsMsgGroupView.h" +#include "nsIMsgHdr.h" +#include "nsIMsgThread.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgSearchSession.h" +#include "nsMsgGroupThread.h" +#include "nsTreeColumns.h" +#include "nsMsgMessageFlags.h" +#include <plhash.h> +#include "mozilla/Attributes.h" + +// Allocate this more to avoid reallocation on new mail. +#define MSGHDR_CACHE_LOOK_AHEAD_SIZE 25 +// Max msghdr cache entries. +#define MSGHDR_CACHE_MAX_SIZE 8192 +#define MSGHDR_CACHE_DEFAULT_SIZE 100 + +nsMsgGroupView::nsMsgGroupView() { m_dayChanged = false; } + +nsMsgGroupView::~nsMsgGroupView() {} + +NS_IMETHODIMP +nsMsgGroupView::Open(nsIMsgFolder* aFolder, nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, + nsMsgViewFlagsTypeValue aViewFlags, int32_t* aCount) { + nsresult rv = + nsMsgDBView::Open(aFolder, aSortType, aSortOrder, aViewFlags, aCount); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + PersistFolderInfo(getter_AddRefs(dbFolderInfo)); + + nsCOMPtr<nsIMsgEnumerator> headers; + rv = m_db->EnumerateMessages(getter_AddRefs(headers)); + NS_ENSURE_SUCCESS(rv, rv); + + return OpenWithHdrs(headers, aSortType, aSortOrder, aViewFlags, aCount); +} + +void nsMsgGroupView::InternalClose() { + m_groupsTable.Clear(); + // Nothing to do if we're not grouped. + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) return; + + bool rcvDate = false; + + if (m_sortType == nsMsgViewSortType::byReceived) rcvDate = true; + + if (m_db && ((m_sortType == nsMsgViewSortType::byDate) || + (m_sortType == nsMsgViewSortType::byReceived))) { + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + if (dbFolderInfo) { + uint32_t expandFlags = 0; + uint32_t num = GetSize(); + + for (uint32_t i = 0; i < num; i++) { + if (m_flags[i] & MSG_VIEW_FLAG_ISTHREAD && + !(m_flags[i] & nsMsgMessageFlags::Elided)) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + if (msgHdr) { + uint32_t ageBucket; + nsresult rv = GetAgeBucketValue(msgHdr, &ageBucket, rcvDate); + if (NS_SUCCEEDED(rv)) expandFlags |= 1 << ageBucket; + } + } + } + dbFolderInfo->SetUint32Property("dateGroupFlags", expandFlags); + } + } +} + +NS_IMETHODIMP +nsMsgGroupView::Close() { + InternalClose(); + return nsMsgDBView::Close(); +} + +// Set rcvDate to true to get the Received: date instead of the Date: date. +nsresult nsMsgGroupView::GetAgeBucketValue(nsIMsgDBHdr* aMsgHdr, + uint32_t* aAgeBucket, bool rcvDate) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + NS_ENSURE_ARG_POINTER(aAgeBucket); + + PRTime dateOfMsg; + nsresult rv; + if (!rcvDate) + rv = aMsgHdr->GetDate(&dateOfMsg); + else { + uint32_t rcvDateSecs; + rv = aMsgHdr->GetUint32Property("dateReceived", &rcvDateSecs); + Seconds2PRTime(rcvDateSecs, &dateOfMsg); + } + NS_ENSURE_SUCCESS(rv, rv); + + PRTime currentTime = PR_Now(); + PRExplodedTime currentExplodedTime; + PR_ExplodeTime(currentTime, PR_LocalTimeParameters, ¤tExplodedTime); + PRExplodedTime explodedMsgTime; + PR_ExplodeTime(dateOfMsg, PR_LocalTimeParameters, &explodedMsgTime); + + if (m_lastCurExplodedTime.tm_mday && + m_lastCurExplodedTime.tm_mday != currentExplodedTime.tm_mday) + // This will cause us to rebuild the view. + m_dayChanged = true; + + m_lastCurExplodedTime = currentExplodedTime; + if (currentExplodedTime.tm_year == explodedMsgTime.tm_year && + currentExplodedTime.tm_month == explodedMsgTime.tm_month && + currentExplodedTime.tm_mday == explodedMsgTime.tm_mday) { + // Same day. + *aAgeBucket = 1; + } + // Figure out how many days ago this msg arrived. + else if (currentTime > dateOfMsg) { + // Setting the time variables to local time. + int64_t GMTLocalTimeShift = currentExplodedTime.tm_params.tp_gmt_offset + + currentExplodedTime.tm_params.tp_dst_offset; + GMTLocalTimeShift *= PR_USEC_PER_SEC; + currentTime += GMTLocalTimeShift; + dateOfMsg += GMTLocalTimeShift; + + // The most recent midnight, counting from current time. + int64_t mostRecentMidnight = currentTime - currentTime % PR_USEC_PER_DAY; + int64_t yesterday = mostRecentMidnight - PR_USEC_PER_DAY; + // Most recent midnight minus 6 days. + int64_t mostRecentWeek = mostRecentMidnight - (PR_USEC_PER_DAY * 6); + + // Was the message sent yesterday? + if (dateOfMsg >= yesterday) + *aAgeBucket = 2; + else if (dateOfMsg >= mostRecentWeek) + *aAgeBucket = 3; + else { + int64_t lastTwoWeeks = mostRecentMidnight - PR_USEC_PER_DAY * 13; + *aAgeBucket = (dateOfMsg >= lastTwoWeeks) ? 4 : 5; + } + } else { + // All that remains is a future date. + *aAgeBucket = 6; + } + return NS_OK; +} + +nsresult nsMsgGroupView::HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey) { + nsCString cStringKey; + aHashKey.Truncate(); + nsresult rv = NS_OK; + bool rcvDate = false; + + switch (m_sortType) { + case nsMsgViewSortType::bySubject: + (void)msgHdr->GetSubject(cStringKey); + CopyASCIItoUTF16(cStringKey, aHashKey); + break; + case nsMsgViewSortType::byAuthor: + rv = nsMsgDBView::FetchAuthor(msgHdr, aHashKey); + break; + case nsMsgViewSortType::byRecipient: + (void)msgHdr->GetRecipients(getter_Copies(cStringKey)); + CopyASCIItoUTF16(cStringKey, aHashKey); + break; + case nsMsgViewSortType::byAccount: + case nsMsgViewSortType::byTags: { + nsCOMPtr<nsIMsgDatabase> dbToUse = m_db; + if (!dbToUse) + // Probably a search view. + GetDBForViewIndex(0, getter_AddRefs(dbToUse)); + + rv = (m_sortType == nsMsgViewSortType::byAccount) + ? FetchAccount(msgHdr, aHashKey) + : FetchTags(msgHdr, aHashKey); + } break; + case nsMsgViewSortType::byAttachments: { + uint32_t flags; + msgHdr->GetFlags(&flags); + aHashKey.Assign(flags & nsMsgMessageFlags::Attachment ? '1' : '0'); + break; + } + case nsMsgViewSortType::byFlagged: { + uint32_t flags; + msgHdr->GetFlags(&flags); + aHashKey.Assign(flags & nsMsgMessageFlags::Marked ? '1' : '0'); + break; + } + case nsMsgViewSortType::byPriority: { + nsMsgPriorityValue priority; + msgHdr->GetPriority(&priority); + aHashKey.AppendInt(priority); + } break; + case nsMsgViewSortType::byStatus: { + uint32_t status = 0; + GetStatusSortValue(msgHdr, &status); + aHashKey.AppendInt(status); + } break; + case nsMsgViewSortType::byReceived: + rcvDate = true; + [[fallthrough]]; + case nsMsgViewSortType::byDate: { + uint32_t ageBucket; + rv = GetAgeBucketValue(msgHdr, &ageBucket, rcvDate); + if (NS_SUCCEEDED(rv)) aHashKey.AppendInt(ageBucket); + + break; + } + case nsMsgViewSortType::byCustom: { + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + if (colHandler) { + bool isString; + colHandler->IsString(&isString); + if (isString) { + rv = colHandler->GetSortStringForRow(msgHdr, aHashKey); + } else { + uint32_t intKey; + rv = colHandler->GetSortLongForRow(msgHdr, &intKey); + aHashKey.AppendInt(intKey); + } + } + break; + } + case nsMsgViewSortType::byCorrespondent: + if (IsOutgoingMsg(msgHdr)) + rv = FetchRecipients(msgHdr, aHashKey); + else + rv = FetchAuthor(msgHdr, aHashKey); + + break; + default: + NS_ASSERTION(false, "no hash key for this type"); + rv = NS_ERROR_FAILURE; + } + return rv; +} + +nsMsgGroupThread* nsMsgGroupView::CreateGroupThread(nsIMsgDatabase* db) { + return new nsMsgGroupThread(db); +} + +nsMsgGroupThread* nsMsgGroupView::AddHdrToThread(nsIMsgDBHdr* msgHdr, + bool* pNewThread) { + nsMsgKey msgKey; + uint32_t msgFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + nsString hashKey; + nsresult rv = HashHdr(msgHdr, hashKey); + if (NS_FAILED(rv)) return nullptr; + + // if (m_sortType == nsMsgViewSortType::byDate) + // msgKey = ((nsPRUint32Key *)hashKey)->GetValue(); + nsCOMPtr<nsIMsgThread> msgThread; + m_groupsTable.Get(hashKey, getter_AddRefs(msgThread)); + bool newThread = !msgThread; + *pNewThread = newThread; + // Index of first message in thread in view. + nsMsgViewIndex viewIndexOfThread; + // Index of newly added header in thread. + nsMsgViewIndex threadInsertIndex; + + nsMsgGroupThread* foundThread = + static_cast<nsMsgGroupThread*>(msgThread.get()); + if (foundThread) { + // Find the view index of the root node of the thread in the view. + viewIndexOfThread = GetIndexOfFirstDisplayedKeyInThread(foundThread, true); + if (viewIndexOfThread == nsMsgViewIndex_None) { + // Something is wrong with the group table. Remove the old group and + // insert a new one. + m_groupsTable.Remove(hashKey); + foundThread = nullptr; + *pNewThread = newThread = true; + } + } + + // If the thread does not already exist, create one + if (!foundThread) { + foundThread = CreateGroupThread(m_db); + msgThread = foundThread; + m_groupsTable.InsertOrUpdate(hashKey, msgThread); + if (GroupViewUsesDummyRow()) { + foundThread->m_dummy = true; + msgFlags |= MSG_VIEW_FLAG_DUMMY | MSG_VIEW_FLAG_HASCHILDREN; + } + + viewIndexOfThread = GetInsertIndex(msgHdr); + if (viewIndexOfThread == nsMsgViewIndex_None) + viewIndexOfThread = m_keys.Length(); + + // Add the thread root node to the view. + InsertMsgHdrAt( + viewIndexOfThread, msgHdr, msgKey, + msgFlags | MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided, 0); + + // For dummy rows, Have the header serve as the dummy node (it will be + // added again for its actual content later). + if (GroupViewUsesDummyRow()) foundThread->InsertMsgHdrAt(0, msgHdr); + + // Calculate the (integer thread key); this really only needs to be done for + // the byDate case where the expanded state of the groups can be easily + // persisted and restored because of the bounded, consecutive value space + // occupied. We calculate an integer value in all cases mainly because + // it's the sanest choice available... + // (The thread key needs to be an integer, so parse hash keys that are + // stringified integers to real integers, and hash actual strings into + // integers.) + if ((m_sortType == nsMsgViewSortType::byAttachments) || + (m_sortType == nsMsgViewSortType::byFlagged) || + (m_sortType == nsMsgViewSortType::byPriority) || + (m_sortType == nsMsgViewSortType::byStatus) || + (m_sortType == nsMsgViewSortType::byReceived) || + (m_sortType == nsMsgViewSortType::byDate)) + foundThread->m_threadKey = + atoi(NS_LossyConvertUTF16toASCII(hashKey).get()); + else + foundThread->m_threadKey = + (nsMsgKey)PL_HashString(NS_LossyConvertUTF16toASCII(hashKey).get()); + } + + // Add the message to the thread as an actual content-bearing header. + // (If we use dummy rows, it was already added to the thread during creation.) + threadInsertIndex = foundThread->AddChildFromGroupView(msgHdr, this); + // Check if new hdr became thread root. + if (!newThread && threadInsertIndex == 0) { + // Update the root node's header (in the view) to be the same as the root + // node in the thread. + SetMsgHdrAt(msgHdr, viewIndexOfThread, msgKey, + (msgFlags & ~(nsMsgMessageFlags::Elided)) | + // Maintain elided flag and dummy flag. + (m_flags[viewIndexOfThread] & + (nsMsgMessageFlags::Elided | MSG_VIEW_FLAG_DUMMY)) | + // Ensure thread and has-children flags are set. + MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN, + 0); + // Update the content-bearing copy in the thread to match. (the root and + // first nodes in the thread should always be the same header.) + // Note: the guy who used to be the root will still exist. If our list of + // nodes was [A A], a new node B is introduced which sorts to be the first + // node, giving us [B A A], our copy makes that [B B A], and things are + // right in the world (since we want the first two headers to be the same + // since one is our dummy and one is real.) + if (GroupViewUsesDummyRow()) { + // Replace the old duplicate dummy header. + // We do not update the content-bearing copy in the view to match; we + // leave that up to OnNewHeader, which is the piece of code who gets to + // care about whether the thread's children are shown or not (elided). + foundThread->SetMsgHdrAt(1, msgHdr); + } + } + + return foundThread; +} + +NS_IMETHODIMP +nsMsgGroupView::OpenWithHdrs(nsIMsgEnumerator* aHeaders, + nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, + nsMsgViewFlagsTypeValue aViewFlags, + int32_t* aCount) { + nsresult rv = NS_OK; + + m_groupsTable.Clear(); + if (aSortType == nsMsgViewSortType::byThread || + aSortType == nsMsgViewSortType::byId || + aSortType == nsMsgViewSortType::byNone || + aSortType == nsMsgViewSortType::bySize) + return NS_ERROR_INVALID_ARG; + + m_sortType = aSortType; + m_sortOrder = aSortOrder; + m_viewFlags = aViewFlags | nsMsgViewFlagsType::kThreadedDisplay | + nsMsgViewFlagsType::kGroupBySort; + SaveSortInfo(m_sortType, m_sortOrder); + + if (m_sortType == nsMsgViewSortType::byCustom) { + // If the desired sort is a custom column and there is no handler found, + // it hasn't been registered yet; after the custom column observer is + // notified with MsgCreateDBView and registers the handler, it will come + // back and build the view. + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + if (!colHandler) return rv; + } + + bool hasMore; + nsCOMPtr<nsISupports> supports; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + while (NS_SUCCEEDED(rv) && + NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) { + rv = aHeaders->GetNext(getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv) && msgHdr) { + bool notUsed; + AddHdrToThread(msgHdr, ¬Used); + } + } + uint32_t expandFlags = 0; + bool expandAll = m_viewFlags & nsMsgViewFlagsType::kExpandAll; + uint32_t viewFlag = + (m_sortType == nsMsgViewSortType::byDate) ? MSG_VIEW_FLAG_DUMMY : 0; + if (viewFlag && m_db) { + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + nsresult rv = m_db->GetDBFolderInfo(getter_AddRefs(dbFolderInfo)); + NS_ENSURE_SUCCESS(rv, rv); + if (dbFolderInfo) + dbFolderInfo->GetUint32Property("dateGroupFlags", 0, &expandFlags); + } + // Go through the view updating the flags for threads with more than one + // message, and if grouped by date, expanding threads that were expanded + // before. + for (uint32_t viewIndex = 0; viewIndex < m_keys.Length(); viewIndex++) { + nsCOMPtr<nsIMsgThread> thread; + GetThreadContainingIndex(viewIndex, getter_AddRefs(thread)); + if (thread) { + uint32_t numChildren; + thread->GetNumChildren(&numChildren); + if (numChildren > 1 || viewFlag) + OrExtraFlag(viewIndex, viewFlag | MSG_VIEW_FLAG_HASCHILDREN); + if (expandAll || expandFlags) { + nsMsgGroupThread* groupThread = + static_cast<nsMsgGroupThread*>((nsIMsgThread*)thread); + if (expandAll || expandFlags & (1 << groupThread->m_threadKey)) { + uint32_t numExpanded; + ExpandByIndex(viewIndex, &numExpanded); + viewIndex += numExpanded; + } + } + } + } + *aCount = m_keys.Length(); + return rv; +} + +// We wouldn't need this if we never instantiated this directly, +// but instead used nsMsgThreadedDBView with the grouping flag set. +// Or, we could get rid of the nsMsgThreadedDBView impl of this method. +NS_IMETHODIMP +nsMsgGroupView::GetViewType(nsMsgViewTypeValue* aViewType) { + NS_ENSURE_ARG_POINTER(aViewType); + *aViewType = nsMsgViewType::eShowAllThreads; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgGroupView::CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) { + nsMsgDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow, + aCmdUpdater); + nsMsgGroupView* newMsgDBView = (nsMsgGroupView*)aNewMsgDBView; + + // If grouped, we need to clone the group thread hash table. + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) { + for (auto iter = m_groupsTable.Iter(); !iter.Done(); iter.Next()) { + newMsgDBView->m_groupsTable.InsertOrUpdate(iter.Key(), iter.UserData()); + } + } + return NS_OK; +} + +// E.g., if the day has changed, we need to close and re-open the view. +// Or, if we're switching between grouping and threading in a cross-folder +// saved search. In that case, we needed to build an enumerator based on the +// old view type, and internally close the view based on its old type, but +// rebuild the new view based on the new view type. So we pass the new +// view flags to OpenWithHdrs. +nsresult nsMsgGroupView::RebuildView(nsMsgViewFlagsTypeValue newFlags) { + nsCOMPtr<nsIMsgEnumerator> headers; + if (NS_SUCCEEDED(GetMessageEnumerator(getter_AddRefs(headers)))) { + int32_t count; + m_dayChanged = false; + AutoTArray<nsMsgKey, 1> preservedSelection; + nsMsgKey curSelectedKey; + SaveAndClearSelection(&curSelectedKey, preservedSelection); + InternalClose(); + int32_t oldSize = GetSize(); + // This is important, because the tree will ask us for our row count, + // which gets determined from the number of keys. + m_keys.Clear(); + // Be consistent. + m_flags.Clear(); + m_levels.Clear(); + + // This needs to happen after we remove all the keys, since + // RowCountChanged() will call our GetRowCount(). + if (mTree) mTree->RowCountChanged(0, -oldSize); + if (mJSTree) mJSTree->RowCountChanged(0, -oldSize); + + SetSuppressChangeNotifications(true); + nsresult rv = + OpenWithHdrs(headers, m_sortType, m_sortOrder, newFlags, &count); + SetSuppressChangeNotifications(false); + if (mTree) mTree->RowCountChanged(0, GetSize()); + if (mJSTree) mJSTree->RowCountChanged(0, GetSize()); + + NS_ENSURE_SUCCESS(rv, rv); + + // Now, restore our desired selection. + AutoTArray<nsMsgKey, 1> keyArray; + keyArray.AppendElement(curSelectedKey); + + return RestoreSelection(curSelectedKey, keyArray); + } + return NS_OK; +} + +nsresult nsMsgGroupView::OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey, + bool ensureListed) { + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::OnNewHeader(newHdr, aParentKey, ensureListed); + + // Check if we're adding a header, and the current day has changed. + // If it has, we're just going to close and re-open the view so things + // will be correctly categorized. + if (m_dayChanged) return RebuildView(m_viewFlags); + + bool newThread; + nsMsgGroupThread* thread = AddHdrToThread(newHdr, &newThread); + if (thread) { + // Find the view index of (the root node of) the thread. + nsMsgViewIndex threadIndex = ThreadIndexOfMsgHdr(newHdr); + // May need to fix thread counts. + if (threadIndex != nsMsgViewIndex_None) { + if (newThread) { + // AddHdrToThread creates the header elided, so we need to un-elide it + // if we want it expanded. + if (m_viewFlags & nsMsgViewFlagsType::kExpandAll) + m_flags[threadIndex] &= ~nsMsgMessageFlags::Elided; + } else { + m_flags[threadIndex] |= + MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD; + } + + int32_t numRowsToInvalidate = 1; + // If the thread is expanded (not elided), we should add the header to + // the view. + if (!(m_flags[threadIndex] & nsMsgMessageFlags::Elided)) { + uint32_t msgIndexInThread = thread->FindMsgHdr(newHdr); + bool insertedAtThreadRoot = !msgIndexInThread; + // Add any new display node and potentially fix-up changes in the root. + // (If this is a new thread and we are not using a dummy row, the only + // node to display is the root node which has already been added by + // AddHdrToThread. And since there is just the one, no change in root + // could have occurred, so we have nothing to do.) + if (!newThread || GroupViewUsesDummyRow()) { + // We never want to insert/update the root node, because + // AddHdrToThread has already done that for us (in all cases). + if (insertedAtThreadRoot) msgIndexInThread++; + // If this header is the new parent of the thread... AND + // If we are not using a dummy row, this means we need to append our + // old node as the first child of the new root. + // (If we are using a dummy row, the old node's "content" node already + // exists (at position threadIndex + 1) and we need to insert the + // "content" copy of the new root node there, pushing our old + // "content" node down.) + // Example mini-diagrams, wrapping the to-add thing with () + // No dummy row; we had: [A], now we have [B], we want [B (A)]. + // Dummy row; we had: [A A], now we have [B A], we want [B (B) A]. + // (Coming into this we're adding 'B') + if (!newThread && insertedAtThreadRoot && !GroupViewUsesDummyRow()) { + // Grab a copy of the old root node ('A') from the thread so we can + // insert it. (offset msgIndexInThread=1 is the right thing; we are + // non-dummy.) + thread->GetChildHdrAt(msgIndexInThread, &newHdr); + } + // Nothing to do for dummy case, we're already inserting 'B'. + + nsMsgKey msgKey; + uint32_t msgFlags; + newHdr->GetMessageKey(&msgKey); + newHdr->GetFlags(&msgFlags); + InsertMsgHdrAt(threadIndex + msgIndexInThread, newHdr, msgKey, + msgFlags, 1); + } + // The call to NoteChange() has to happen after we add the key + // as NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + // (msgIndexInThread states - new thread: 0, old thread at root: 1). + if (newThread && GroupViewUsesDummyRow()) + NoteChange(threadIndex, 2, nsMsgViewNotificationCode::insertOrDelete); + else + NoteChange(threadIndex + msgIndexInThread, 1, + nsMsgViewNotificationCode::insertOrDelete); + + numRowsToInvalidate = msgIndexInThread; + } else if (newThread) { + // We still need the addition notification for new threads when elided. + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::insertOrDelete); + } + + NoteChange(threadIndex, numRowsToInvalidate, + nsMsgViewNotificationCode::changed); + } + } + + // If thread is expanded, we need to add hdr to view... + return NS_OK; +} + +NS_IMETHODIMP +nsMsgGroupView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, + uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) { + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, + aInstigator); + + nsCOMPtr<nsIMsgThread> thread; + + // Check if we're adding a header, and the current day has changed. + // If it has, we're just going to close and re-open the view so things + // will be correctly categorized. + if (m_dayChanged) return RebuildView(m_viewFlags); + + nsresult rv = GetThreadContainingMsgHdr(aHdrChanged, getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t deltaFlags = (aOldFlags ^ aNewFlags); + if (deltaFlags & nsMsgMessageFlags::Read) + thread->MarkChildRead(aNewFlags & nsMsgMessageFlags::Read); + + return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, + aInstigator); +} + +NS_IMETHODIMP +nsMsgGroupView::OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, + int32_t aFlags, nsIDBChangeListener* aInstigator) { + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags, + aInstigator); + + // Check if we're adding a header, and the current day has changed. + // If it has, we're just going to close and re-open the view so things + // will be correctly categorized. + if (m_dayChanged) return RebuildView(m_viewFlags); + + nsCOMPtr<nsIMsgThread> thread; + nsMsgKey keyDeleted; + aHdrDeleted->GetMessageKey(&keyDeleted); + + nsresult rv = GetThreadContainingMsgHdr(aHdrDeleted, getter_AddRefs(thread)); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgViewIndex viewIndexOfThread = + GetIndexOfFirstDisplayedKeyInThread(thread, true); // Yes to dummy node. + + thread->RemoveChildHdr(aHdrDeleted, nullptr); + + nsMsgGroupThread* groupThread = + static_cast<nsMsgGroupThread*>((nsIMsgThread*)thread); + + bool rootDeleted = viewIndexOfThread != nsMsgKey_None && + m_keys[viewIndexOfThread] == keyDeleted; + rv = nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags, aInstigator); + if (groupThread->m_dummy) { + if (!groupThread->NumRealChildren()) { + // Get rid of dummy. + thread->RemoveChildAt(0); + if (viewIndexOfThread != nsMsgKey_None) { + RemoveByIndex(viewIndexOfThread); + if (m_deletingRows) + mIndicesToNoteChange.AppendElement(viewIndexOfThread); + } + } else if (rootDeleted) { + // Reflect new thread root into view.dummy row. + nsCOMPtr<nsIMsgDBHdr> hdr; + thread->GetChildHdrAt(0, getter_AddRefs(hdr)); + if (hdr) { + nsMsgKey msgKey; + hdr->GetMessageKey(&msgKey); + SetMsgHdrAt(hdr, viewIndexOfThread, msgKey, m_flags[viewIndexOfThread], + 0); + } + } + } + if (!groupThread->m_keys.Length()) { + nsString hashKey; + rv = HashHdr(aHdrDeleted, hashKey); + if (NS_SUCCEEDED(rv)) m_groupsTable.Remove(hashKey); + } + return rv; +} + +NS_IMETHODIMP +nsMsgGroupView::GetRowProperties(int32_t aRow, nsAString& aProperties) { + if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) { + aProperties.AssignLiteral("dummy"); + return NS_OK; + } + + return nsMsgDBView::GetRowProperties(aRow, aProperties); +} + +NS_IMETHODIMP +nsMsgGroupView::GetCellProperties(int32_t aRow, nsTreeColumn* aCol, + nsAString& aProperties) { + if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) { + aProperties.AssignLiteral("dummy read"); + + if (!(m_flags[aRow] & nsMsgMessageFlags::Elided)) return NS_OK; + + // Set unread property if a collapsed group thread has unread. + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsString hashKey; + rv = HashHdr(msgHdr, hashKey); + if (NS_FAILED(rv)) return NS_OK; + + nsCOMPtr<nsIMsgThread> msgThread; + m_groupsTable.Get(hashKey, getter_AddRefs(msgThread)); + nsMsgGroupThread* groupThread = + static_cast<nsMsgGroupThread*>(msgThread.get()); + if (!groupThread) return NS_OK; + + uint32_t numUnrMsg = 0; + groupThread->GetNumUnreadChildren(&numUnrMsg); + if (numUnrMsg > 0) aProperties.AppendLiteral(" hasUnread"); + + return NS_OK; + } + + return nsMsgDBView::GetCellProperties(aRow, aCol, aProperties); +} + +NS_IMETHODIMP +nsMsgGroupView::CellTextForColumn(int32_t aRow, const nsAString& aColumnName, + nsAString& aValue) { + if (!IsValidIndex(aRow)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (!(m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) || + aColumnName.EqualsLiteral("unreadCol")) + return nsMsgDBView::CellTextForColumn(aRow, aColumnName, aValue); + + // We only treat "subject" and "total" here. + bool isSubject; + if (!(isSubject = aColumnName.EqualsLiteral("subjectCol")) && + !aColumnName.EqualsLiteral("totalCol")) + return NS_OK; + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsresult rv = GetMsgHdrForViewIndex(aRow, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsString hashKey; + rv = HashHdr(msgHdr, hashKey); + if (NS_FAILED(rv)) return NS_OK; + nsCOMPtr<nsIMsgThread> msgThread; + m_groupsTable.Get(hashKey, getter_AddRefs(msgThread)); + nsMsgGroupThread* groupThread = + static_cast<nsMsgGroupThread*>(msgThread.get()); + if (isSubject) { + uint32_t flags; + bool rcvDate = false; + msgHdr->GetFlags(&flags); + aValue.Truncate(); + switch (m_sortType) { + case nsMsgViewSortType::byReceived: + rcvDate = true; + [[fallthrough]]; + case nsMsgViewSortType::byDate: { + uint32_t ageBucket = 0; + GetAgeBucketValue(msgHdr, &ageBucket, rcvDate); + switch (ageBucket) { + case 1: + aValue.Assign(nsMsgDBView::kTodayString); + break; + case 2: + aValue.Assign(nsMsgDBView::kYesterdayString); + break; + case 3: + aValue.Assign(nsMsgDBView::kLastWeekString); + break; + case 4: + aValue.Assign(nsMsgDBView::kTwoWeeksAgoString); + break; + case 5: + aValue.Assign(nsMsgDBView::kOldMailString); + break; + default: + // Future date, error/spoofed. + aValue.Assign(nsMsgDBView::kFutureDateString); + break; + } + break; + } + case nsMsgViewSortType::bySubject: + FetchSubject(msgHdr, m_flags[aRow], aValue); + break; + case nsMsgViewSortType::byAuthor: + FetchAuthor(msgHdr, aValue); + break; + case nsMsgViewSortType::byStatus: + rv = FetchStatus(m_flags[aRow], aValue); + if (aValue.IsEmpty()) { + GetString(u"messagesWithNoStatus", aValue); + } + break; + case nsMsgViewSortType::byTags: + rv = FetchTags(msgHdr, aValue); + if (aValue.IsEmpty()) { + GetString(u"untaggedMessages", aValue); + } + break; + case nsMsgViewSortType::byPriority: + FetchPriority(msgHdr, aValue); + if (aValue.IsEmpty()) { + GetString(u"noPriority", aValue); + } + break; + case nsMsgViewSortType::byAccount: + FetchAccount(msgHdr, aValue); + break; + case nsMsgViewSortType::byRecipient: + FetchRecipients(msgHdr, aValue); + break; + case nsMsgViewSortType::byAttachments: + GetString(flags & nsMsgMessageFlags::Attachment ? u"attachments" + : u"noAttachments", + aValue); + break; + case nsMsgViewSortType::byFlagged: + GetString( + flags & nsMsgMessageFlags::Marked ? u"groupFlagged" : u"notFlagged", + aValue); + break; + // byLocation is a special case; we don't want to have duplicate + // all this logic in nsMsgSearchDBView, and its hash key is what we + // want anyways, so just copy it across. + case nsMsgViewSortType::byLocation: + case nsMsgViewSortType::byCorrespondent: + aValue = hashKey; + break; + case nsMsgViewSortType::byCustom: { + nsIMsgCustomColumnHandler* colHandler = GetCurColumnHandler(); + if (colHandler) { + bool isString; + colHandler->IsString(&isString); + if (isString) { + rv = colHandler->GetSortStringForRow(msgHdr.get(), aValue); + } else { + uint32_t intKey; + rv = colHandler->GetSortLongForRow(msgHdr.get(), &intKey); + aValue.AppendInt(intKey); + } + } + if (aValue.IsEmpty()) aValue.Assign('*'); + break; + } + + default: + NS_ASSERTION(false, "we don't sort by group for this type"); + break; + } + + if (groupThread) { + // Get number of messages in group. + nsAutoString formattedCountMsg; + uint32_t numMsg = groupThread->NumRealChildren(); + formattedCountMsg.AppendInt(numMsg); + + // Get number of unread messages. + nsAutoString formattedCountUnrMsg; + uint32_t numUnrMsg = 0; + groupThread->GetNumUnreadChildren(&numUnrMsg); + formattedCountUnrMsg.AppendInt(numUnrMsg); + + // Add text to header. + aValue.AppendLiteral(u" ("); + if (numUnrMsg) { + aValue.Append(formattedCountUnrMsg); + aValue.Append(u'/'); + } + + aValue.Append(formattedCountMsg); + aValue.Append(u')'); + } + } else { + nsAutoString formattedCountString; + uint32_t numChildren = (groupThread) ? groupThread->NumRealChildren() : 0; + formattedCountString.AppendInt(numChildren); + aValue.Assign(formattedCountString); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgGroupView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr, + nsIMsgThread** pThread) { + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::GetThreadContainingMsgHdr(msgHdr, pThread); + + nsString hashKey; + nsresult rv = HashHdr(msgHdr, hashKey); + *pThread = nullptr; + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgThread> thread; + m_groupsTable.Get(hashKey, getter_AddRefs(thread)); + thread.forget(pThread); + } + + return (*pThread) ? NS_OK : NS_ERROR_FAILURE; +} + +int32_t nsMsgGroupView::FindLevelInThread(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startOfThread, + nsMsgViewIndex viewIndex) { + if (!(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) + return nsMsgDBView::FindLevelInThread(msgHdr, startOfThread, viewIndex); + + return (startOfThread == viewIndex) ? 0 : 1; +} + +bool nsMsgGroupView::GroupViewUsesDummyRow() { + // Return true to always use a header row as root grouped parent row. + return true; +} + +NS_IMETHODIMP +nsMsgGroupView::AddColumnHandler(const nsAString& column, + nsIMsgCustomColumnHandler* handler) { + nsMsgDBView::AddColumnHandler(column, handler); + + // If the sortType is byCustom and the desired custom column is the one just + // registered, build the view. + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort && + m_sortType == nsMsgViewSortType::byCustom) { + nsAutoString curCustomColumn; + GetCurCustomColumn(curCustomColumn); + if (curCustomColumn == column) RebuildView(m_viewFlags); + } + + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgGroupView.h b/comm/mailnews/base/src/nsMsgGroupView.h new file mode 100644 index 0000000000..3b646f159b --- /dev/null +++ b/comm/mailnews/base/src/nsMsgGroupView.h @@ -0,0 +1,78 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgGroupView_H_ +#define _nsMsgGroupView_H_ + +#include "mozilla/Attributes.h" +#include "nsMsgDBView.h" +#include "nsInterfaceHashtable.h" + +class nsIMsgThread; +class nsMsgGroupThread; + +// Please note that if you override a method of nsMsgDBView, +// you will most likely want to check the m_viewFlags to see if +// we're grouping, and if not, call the base class implementation. +class nsMsgGroupView : public nsMsgDBView { + public: + nsMsgGroupView(); + virtual ~nsMsgGroupView(); + + NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override; + NS_IMETHOD OpenWithHdrs(nsIMsgEnumerator* aHeaders, + nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, + nsMsgViewFlagsTypeValue aViewFlags, + int32_t* aCount) override; + NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override; + NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater); + NS_IMETHOD Close() override; + NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, + int32_t aFlags, + nsIDBChangeListener* aInstigator) override; + NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, + uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) override; + + NS_IMETHOD GetCellProperties(int32_t aRow, nsTreeColumn* aCol, + nsAString& aProperties) override; + NS_IMETHOD GetRowProperties(int32_t aRow, nsAString& aProperties) override; + NS_IMETHOD CellTextForColumn(int32_t aRow, const nsAString& aColumnName, + nsAString& aValue) override; + NS_IMETHOD GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr, + nsIMsgThread** pThread) override; + NS_IMETHOD AddColumnHandler(const nsAString& column, + nsIMsgCustomColumnHandler* handler) override; + + protected: + virtual void InternalClose(); + nsMsgGroupThread* AddHdrToThread(nsIMsgDBHdr* msgHdr, bool* pNewThread); + virtual nsresult HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey); + // Helper function to get age bucket for a hdr, useful when grouped by date. + nsresult GetAgeBucketValue(nsIMsgDBHdr* aMsgHdr, uint32_t* aAgeBucket, + bool rcvDate = false); + nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey, + bool /*ensureListed*/) override; + virtual int32_t FindLevelInThread(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startOfThread, + nsMsgViewIndex viewIndex) override; + + // Returns true if we are grouped by a sort attribute that uses a dummy row. + bool GroupViewUsesDummyRow(); + nsresult RebuildView(nsMsgViewFlagsTypeValue viewFlags); + virtual nsMsgGroupThread* CreateGroupThread(nsIMsgDatabase* db); + + nsInterfaceHashtable<nsStringHashKey, nsIMsgThread> m_groupsTable; + PRExplodedTime m_lastCurExplodedTime{0}; + bool m_dayChanged; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgI18N.cpp b/comm/mailnews/base/src/nsMsgI18N.cpp new file mode 100644 index 0000000000..1c81456403 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgI18N.cpp @@ -0,0 +1,403 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsICharsetConverterManager.h" +#include "mozilla/Utf8.h" +#include "nsIServiceManager.h" + +#include "nsISupports.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsIMimeConverter.h" +#include "nsMsgUtils.h" +#include "nsMsgI18N.h" +#include "nsILineInputStream.h" +#include "nsMimeTypes.h" +#include "nsString.h" +#include "prmem.h" +#include "plstr.h" +#include "nsUTF8Utils.h" +#include "nsNetUtil.h" +#include "nsCRTGlue.h" +#include "nsComponentManagerUtils.h" +#include "nsUnicharUtils.h" +#include "nsIFileStreams.h" +#include "../../intl/nsUTF7ToUnicode.h" +#include "../../intl/nsMUTF7ToUnicode.h" +#include "../../intl/nsUnicodeToMUTF7.h" + +#include <stdlib.h> +#include <tuple> + +// +// International functions necessary for composition +// + +nsresult nsMsgI18NConvertFromUnicode(const nsACString& aCharset, + const nsAString& inString, + nsACString& outString, + bool aReportUencNoMapping) { + if (inString.IsEmpty()) { + outString.Truncate(); + return NS_OK; + } + + auto encoding = mozilla::Encoding::ForLabelNoReplacement(aCharset); + if (!encoding) { + return NS_ERROR_UCONV_NOCONV; + } else if (encoding == UTF_16LE_ENCODING || encoding == UTF_16BE_ENCODING) { + // We shouldn't ever ship anything in these encodings. + return NS_ERROR_UCONV_NOCONV; + } + + nsresult rv; + std::tie(rv, std::ignore) = encoding->Encode(inString, outString); + + if (rv == NS_OK_HAD_REPLACEMENTS) { + rv = aReportUencNoMapping ? NS_ERROR_UENC_NOMAPPING : NS_OK; + } + + return rv; +} + +nsresult nsMsgI18NConvertToUnicode(const nsACString& aCharset, + const nsACString& inString, + nsAString& outString) { + if (inString.IsEmpty()) { + outString.Truncate(); + return NS_OK; + } + if (aCharset.IsEmpty()) { + // Despite its name, it also works for Latin-1. + CopyASCIItoUTF16(inString, outString); + return NS_OK; + } + + if (aCharset.Equals("UTF-8", nsCaseInsensitiveCStringComparator)) { + return UTF_8_ENCODING->DecodeWithBOMRemoval(inString, outString); + } + + // Look up Thunderbird's special aliases from charsetalias.properties. + nsresult rv; + nsCOMPtr<nsICharsetConverterManager> ccm = + do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString newCharset; + rv = ccm->GetCharsetAlias(PromiseFlatCString(aCharset).get(), newCharset); + NS_ENSURE_SUCCESS(rv, rv); + + if (newCharset.Equals("UTF-7", nsCaseInsensitiveCStringComparator)) { + // Special treatment for decoding UTF-7 since it's not handled by + // encoding_rs. + return CopyUTF7toUTF16(inString, outString); + } + + auto encoding = mozilla::Encoding::ForLabelNoReplacement(newCharset); + if (!encoding) return NS_ERROR_UCONV_NOCONV; + return encoding->DecodeWithoutBOMHandling(inString, outString); +} + +// This is used to decode UTF-7. No support for encoding in UTF-7. +nsresult CopyUTF7toUTF16(const nsACString& aSrc, nsAString& aDest) { + // UTF-7 encoding size cannot be larger than the size in UTF-16. + nsUTF7ToUnicode converter; + int32_t inLen = aSrc.Length(); + int32_t outLen = inLen; + aDest.SetLength(outLen); + converter.ConvertNoBuff(aSrc.BeginReading(), &inLen, aDest.BeginWriting(), + &outLen); + MOZ_ASSERT(inLen == (int32_t)aSrc.Length(), + "UTF-7 should not produce a longer output"); + aDest.SetLength(outLen); + return NS_OK; +} + +nsresult CopyUTF16toMUTF7(const nsAString& aSrc, nsACString& aDest) { +#define IMAP_UTF7_BUF_LENGTH 100 + nsUnicodeToMUTF7 converter; + static char buffer[IMAP_UTF7_BUF_LENGTH]; + const char16_t* in = aSrc.BeginReading(); + int32_t inLen = aSrc.Length(); + int32_t outLen; + aDest.Truncate(); + while (inLen > 0) { + outLen = IMAP_UTF7_BUF_LENGTH; + int32_t remaining = inLen; + converter.ConvertNoBuffNoErr(in, &remaining, buffer, &outLen); + aDest.Append(buffer, outLen); + in += remaining; + inLen -= remaining; + } + outLen = IMAP_UTF7_BUF_LENGTH; + converter.FinishNoBuff(buffer, &outLen); + if (outLen > 0) aDest.Append(buffer, outLen); + return NS_OK; +} + +// Hacky function to use for IMAP folders where the name can be in +// MUTF-7 or UTF-8. +nsresult CopyFolderNameToUTF16(const nsACString& aSrc, nsAString& aDest) { + if (NS_IsAscii(aSrc.BeginReading(), aSrc.Length())) { + // An ASCII string may not be valid MUTF-7. For example, it may contain an + // ampersand not immediately followed by a dash which is invalid MUTF-7. + // Check for validity by converting to UTF-16 and then back to MUTF-7 and + // the result should be unchanged. If the MUTF-7 is invalid, treat it as + // UTF-8. + if (NS_SUCCEEDED(CopyMUTF7toUTF16(aSrc, aDest))) { + nsAutoCString tmp; + CopyUTF16toMUTF7(aDest, tmp); + if (aSrc.Equals(tmp)) return NS_OK; + } + } + // Do if aSrc non-ASCII or if ASCII but invalid MUTF-7. + CopyUTF8toUTF16(aSrc, aDest); + return NS_OK; +} + +nsresult CopyMUTF7toUTF16(const nsACString& aSrc, nsAString& aDest) { + // MUTF-7 encoding size cannot be larger than the size in UTF-16. + nsMUTF7ToUnicode converter; + int32_t inLen = aSrc.Length(); + int32_t outLen = inLen; + aDest.SetLength(outLen); + converter.ConvertNoBuff(aSrc.BeginReading(), &inLen, aDest.BeginWriting(), + &outLen); + MOZ_ASSERT(inLen == (int32_t)aSrc.Length(), + "MUTF-7 should not produce a longer output"); + aDest.SetLength(outLen); + return NS_OK; +} + +// MIME encoder, output string should be freed by PR_FREE +// XXX : fix callers later to avoid allocation and copy +char* nsMsgI18NEncodeMimePartIIStr(const char* header, bool structured, + const char* charset, int32_t fieldnamelen, + bool usemime) { + // No MIME, convert to the outgoing mail charset. + if (!usemime) { + nsAutoCString convertedStr; + if (NS_SUCCEEDED(nsMsgI18NConvertFromUnicode( + charset ? nsDependentCString(charset) : EmptyCString(), + NS_ConvertUTF8toUTF16(header), convertedStr))) + return PL_strdup(convertedStr.get()); + else + return PL_strdup(header); + } + + nsAutoCString encodedString; + nsresult res; + nsCOMPtr<nsIMimeConverter> converter = + do_GetService("@mozilla.org/messenger/mimeconverter;1", &res); + if (NS_SUCCEEDED(res) && nullptr != converter) { + res = converter->EncodeMimePartIIStr_UTF8( + nsDependentCString(header), structured, fieldnamelen, + nsIMimeConverter::MIME_ENCODED_WORD_SIZE, encodedString); + } + + return NS_SUCCEEDED(res) ? PL_strdup(encodedString.get()) : nullptr; +} + +// Return True if a charset is stateful (e.g. JIS). +bool nsMsgI18Nstateful_charset(const char* charset) { + // TODO: use charset manager's service + return (PL_strcasecmp(charset, "ISO-2022-JP") == 0); +} + +bool nsMsgI18Nmultibyte_charset(const char* charset) { + nsresult res; + nsCOMPtr<nsICharsetConverterManager> ccm = + do_GetService(NS_CHARSETCONVERTERMANAGER_CONTRACTID, &res); + bool result = false; + + if (NS_SUCCEEDED(res)) { + nsAutoString charsetData; + res = ccm->GetCharsetData(charset, u".isMultibyte", charsetData); + if (NS_SUCCEEDED(res)) { + result = charsetData.LowerCaseEqualsLiteral("true"); + } + } + + return result; +} + +bool nsMsgI18Ncheck_data_in_charset_range(const char* charset, + const char16_t* inString) { + if (!charset || !*charset || !inString || !*inString) return true; + + bool res = true; + + auto encoding = + mozilla::Encoding::ForLabelNoReplacement(nsDependentCString(charset)); + if (!encoding) return false; + auto encoder = encoding->NewEncoder(); + + uint8_t buffer[512]; + auto src = mozilla::MakeStringSpan(inString); + auto dst = mozilla::Span(buffer); + while (true) { + uint32_t result; + size_t read; + size_t written; + std::tie(result, read, written) = + encoder->EncodeFromUTF16WithoutReplacement(src, dst, false); + if (result == mozilla::kInputEmpty) { + // All converted successfully. + break; + } else if (result != mozilla::kOutputFull) { + // Didn't use all the input but the output isn't full, hence + // there was an unencodable character. + res = false; + break; + } + src = src.From(read); + // dst = dst.From(written); // Just overwrite output since we don't need it. + } + + return res; +} + +// Simple parser to parse META charset. +// It only supports the case when the description is within one line. +const char* nsMsgI18NParseMetaCharset(nsIFile* file) { + static char charset[nsIMimeConverter::MAX_CHARSET_NAME_LENGTH + 1]; + + *charset = '\0'; + + bool isDirectory = false; + file->IsDirectory(&isDirectory); + if (isDirectory) { + NS_ERROR("file is a directory"); + return charset; + } + + nsresult rv; + nsCOMPtr<nsIFileInputStream> fileStream = + do_CreateInstance(NS_LOCALFILEINPUTSTREAM_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, charset); + + rv = fileStream->Init(file, PR_RDONLY, 0664, false); + nsCOMPtr<nsILineInputStream> lineStream = do_QueryInterface(fileStream, &rv); + + nsCString curLine; + bool more = true; + while (NS_SUCCEEDED(rv) && more) { + rv = lineStream->ReadLine(curLine, &more); + if (curLine.IsEmpty()) continue; + + ToUpperCase(curLine); + + if (curLine.Find("/HEAD") != -1) break; + + if (curLine.Find("META") != -1 && curLine.Find("HTTP-EQUIV") != -1 && + curLine.Find("CONTENT-TYPE") != -1 && curLine.Find("CHARSET") != -1) { + char* cp = (char*)PL_strchr(PL_strstr(curLine.get(), "CHARSET"), '='); + char* token = nullptr; + if (cp) { + char* newStr = cp + 1; + token = NS_strtok(" \"\'", &newStr); + } + if (token) { + PL_strncpy(charset, token, sizeof(charset)); + charset[sizeof(charset) - 1] = '\0'; + + // this function cannot parse a file if it is really + // encoded by one of the following charsets + // so we can say that the charset label must be incorrect for + // the .html if we actually see those charsets parsed + // and we should ignore them + if (!PL_strncasecmp("UTF-16", charset, sizeof("UTF-16") - 1) || + !PL_strncasecmp("UTF-32", charset, sizeof("UTF-32") - 1)) + charset[0] = '\0'; + + break; + } + } + } + + return charset; +} + +nsresult nsMsgI18NShrinkUTF8Str(const nsCString& inString, uint32_t aMaxLength, + nsACString& outString) { + if (inString.IsEmpty()) { + outString.Truncate(); + return NS_OK; + } + if (inString.Length() < aMaxLength) { + outString.Assign(inString); + return NS_OK; + } + NS_ASSERTION(mozilla::IsUtf8(inString), "Invalid UTF-8 string is inputted"); + const char* start = inString.get(); + const char* end = start + inString.Length(); + const char* last = start + aMaxLength; + const char* cur = start; + const char* prev = nullptr; + bool err = false; + while (cur < last) { + prev = cur; + if (!UTF8CharEnumerator::NextChar(&cur, end, &err) || err) break; + } + if (!prev || err) { + outString.Truncate(); + return NS_OK; + } + uint32_t len = prev - start; + outString.Assign(Substring(inString, 0, len)); + return NS_OK; +} + +void nsMsgI18NConvertRawBytesToUTF16(const nsCString& inString, + const nsACString& charset, + nsAString& outString) { + if (mozilla::IsUtf8(inString)) { + CopyUTF8toUTF16(inString, outString); + return; + } + + nsresult rv = nsMsgI18NConvertToUnicode(charset, inString, outString); + if (NS_SUCCEEDED(rv)) return; + + const char* cur = inString.BeginReading(); + const char* end = inString.EndReading(); + outString.Truncate(); + while (cur < end) { + char c = *cur++; + if (c & char(0x80)) + outString.Append(UCS2_REPLACEMENT_CHAR); + else + outString.Append(c); + } +} + +void nsMsgI18NConvertRawBytesToUTF8(const nsCString& inString, + const nsACString& charset, + nsACString& outString) { + if (mozilla::IsUtf8(inString)) { + outString.Assign(inString); + return; + } + + nsAutoString utf16Text; + nsresult rv = nsMsgI18NConvertToUnicode(charset, inString, utf16Text); + if (NS_SUCCEEDED(rv)) { + CopyUTF16toUTF8(utf16Text, outString); + return; + } + + // EF BF BD (UTF-8 encoding of U+FFFD) + constexpr auto utf8ReplacementChar = "\357\277\275"_ns; + const char* cur = inString.BeginReading(); + const char* end = inString.EndReading(); + outString.Truncate(); + while (cur < end) { + char c = *cur++; + if (c & char(0x80)) + outString.Append(utf8ReplacementChar); + else + outString.Append(c); + } +} diff --git a/comm/mailnews/base/src/nsMsgI18N.h b/comm/mailnews/base/src/nsMsgI18N.h new file mode 100644 index 0000000000..2268b64e26 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgI18N.h @@ -0,0 +1,138 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgI18N_H_ +#define _nsMsgI18N_H_ + +#include "nscore.h" +#include "msgCore.h" +#include "nsString.h" +class nsIFile; + +/** + * Encode an input string into RFC 2047 form. + * + * @param header [IN] A header to encode. + * @param structured [IN] Specify the header is structured or non-structured + * field (See RFC-822). + * @param charset [IN] Charset name to convert. + * @param fieldnamelen [IN] Header field name length. (e.g. "From: " -> 6) + * @param usemime [IN] If false then apply charset conversion only no MIME + * encoding. + * @return Encoded buffer (in C string) or NULL in case of error. + */ +NS_MSG_BASE char* nsMsgI18NEncodeMimePartIIStr(const char* header, + bool structured, + const char* charset, + int32_t fieldnamelen, + bool usemime); + +/** + * Check if given charset is stateful (e.g. ISO-2022-JP). + * + * @param charset [IN] Charset name. + * @return True if stateful + */ +NS_MSG_BASE bool nsMsgI18Nstateful_charset(const char* charset); + +/** + * Check if given charset is multibyte (e.g. Shift_JIS, Big5). + * + * @param charset [IN] Charset name. + * @return True if multibyte + */ +NS_MSG_BASE bool nsMsgI18Nmultibyte_charset(const char* charset); + +/** + * Check the input (unicode) string is in a range of the given charset after the + * conversion. Note, do not use this for large string (e.g. message body) since + * this actually applies the conversion to the buffer. + * + * @param charset [IN] Charset to be converted. + * @param inString [IN] Input unicode string to be examined. + * @return True if the string can be converted within the charset range. + * False if one or more characters cannot be converted to the + * target charset. + */ +NS_MSG_BASE bool nsMsgI18Ncheck_data_in_charset_range(const char* charset, + const char16_t* inString); +/** + * Convert from unicode to target charset. + * + * @param charset [IN] Charset name. + * @param inString [IN] Unicode string to convert. + * @param outString [OUT] Converted output string. + * @param aReportUencNoMapping [IN] Set encoder to report (instead of using + * replacement char on errors). Set to true + * to receive NS_ERROR_UENC_NOMAPPING when + * that happens. Note that + * NS_ERROR_UENC_NOMAPPING is a success code! + * @return nsresult. + */ +NS_MSG_BASE nsresult nsMsgI18NConvertFromUnicode( + const nsACString& aCharset, const nsAString& inString, + nsACString& outString, bool reportUencNoMapping = false); +/** + * Convert from charset to unicode. + * + * @param charset [IN] Charset name. + * @param inString [IN] Input string to convert. + * @param outString [OUT] Output unicode string. + * @return nsresult. + */ +NS_MSG_BASE nsresult nsMsgI18NConvertToUnicode(const nsACString& aCharset, + const nsACString& inString, + nsAString& outString); +/** + * Parse for META charset. + * + * @param file [IN] A nsIFile. + * @return A charset name or empty string if not found. + */ +NS_MSG_BASE const char* nsMsgI18NParseMetaCharset(nsIFile* file); + +/** + * Shrink the aStr to aMaxLength bytes. Note that this doesn't check whether + * the aUTF8Str is valid UTF-8 string. + * + * @param inString [IN] Input UTF-8 string (it must be valid UTF-8 string) + * @param aMaxLength [IN] Shrink to this length (it means bytes) + * @param outString [OUT] Shrunken UTF-8 string + * @return nsresult + */ +NS_MSG_BASE nsresult nsMsgI18NShrinkUTF8Str(const nsCString& inString, + uint32_t aMaxLength, + nsACString& outString); + +/* + * Convert raw bytes in header to UTF-16 + * + * @param inString [IN] Input raw octets + * @param outString [OUT] Output UTF-16 string + */ +NS_MSG_BASE void nsMsgI18NConvertRawBytesToUTF16(const nsCString& inString, + const nsACString& charset, + nsAString& outString); + +/* + * Convert raw bytes in header to UTF-8 + * + * @param inString [IN] Input raw octets + * @param outString [OUT] Output UTF-8 string + */ +NS_MSG_BASE void nsMsgI18NConvertRawBytesToUTF8(const nsCString& inString, + const nsACString& charset, + nsACString& outString); + +// Decode UTF-7 to UTF-16. No encoding supported. +NS_MSG_BASE nsresult CopyUTF7toUTF16(const nsACString& aSrc, nsAString& aDest); + +// Convert between UTF-16 and modified UTF-7 used for IMAP. +NS_MSG_BASE nsresult CopyFolderNameToUTF16(const nsACString& aSrc, + nsAString& aDest); +NS_MSG_BASE nsresult CopyUTF16toMUTF7(const nsAString& aSrc, nsACString& aDest); +NS_MSG_BASE nsresult CopyMUTF7toUTF16(const nsACString& aSrc, nsAString& aDest); + +#endif /* _nsMsgI18N_H_ */ diff --git a/comm/mailnews/base/src/nsMsgIdentity.cpp b/comm/mailnews/base/src/nsMsgIdentity.cpp new file mode 100644 index 0000000000..a36107203d --- /dev/null +++ b/comm/mailnews/base/src/nsMsgIdentity.cpp @@ -0,0 +1,645 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // for pre-compiled headers +#include "nsMsgIdentity.h" +#include "nsIPrefService.h" +#include "nsString.h" +#include "nsMsgFolderFlags.h" +#include "nsIMsgFolder.h" +#include "nsIMsgIncomingServer.h" +#include "nsIMsgAccountManager.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "nsIMsgHeaderParser.h" +#include "prprf.h" +#include "nsISupportsPrimitives.h" +#include "nsMsgUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsIUUIDGenerator.h" +#include "mozilla/Components.h" + +#define REL_FILE_PREF_SUFFIX "-rel" + +NS_IMPL_ISUPPORTS(nsMsgIdentity, nsIMsgIdentity) + +/* + * accessors for pulling values directly out of preferences + * instead of member variables, etc + */ + +NS_IMETHODIMP +nsMsgIdentity::GetKey(nsACString& aKey) { + aKey = mKey; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIdentity::SetKey(const nsACString& identityKey) { + mKey = identityKey; + nsresult rv; + nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_FAILED(rv)) return rv; + + nsAutoCString branchName; + branchName.AssignLiteral("mail.identity."); + branchName += mKey; + branchName.Append('.'); + rv = prefs->GetBranch(branchName.get(), getter_AddRefs(mPrefBranch)); + if (NS_FAILED(rv)) return rv; + + rv = prefs->GetBranch("mail.identity.default.", + getter_AddRefs(mDefPrefBranch)); + return rv; +} + +NS_IMETHODIMP +nsMsgIdentity::GetUID(nsACString& uid) { + bool hasValue; + nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue); + NS_ENSURE_SUCCESS(rv, rv); + if (hasValue) { + return mPrefBranch->GetCharPref("uid", uid); + } + + nsCOMPtr<nsIUUIDGenerator> uuidgen = + mozilla::components::UUIDGenerator::Service(); + NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE); + + nsID id; + rv = uuidgen->GenerateUUIDInPlace(&id); + NS_ENSURE_SUCCESS(rv, rv); + + char idString[NSID_LENGTH]; + id.ToProvidedString(idString); + + uid.AppendASCII(idString + 1, NSID_LENGTH - 3); + return SetUID(uid); +} + +NS_IMETHODIMP +nsMsgIdentity::SetUID(const nsACString& uid) { + bool hasValue; + nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue); + NS_ENSURE_SUCCESS(rv, rv); + if (hasValue) { + return NS_ERROR_ABORT; + } + return SetCharAttribute("uid", uid); +} + +nsresult nsMsgIdentity::GetIdentityName(nsAString& idName) { + idName.AssignLiteral(""); + // Try to use "fullname <email>" as the name. + nsresult rv = GetFullAddress(idName); + NS_ENSURE_SUCCESS(rv, rv); + + // If a non-empty label exists, append it. + nsString label; + rv = GetLabel(label); + if (NS_SUCCEEDED(rv) && + !label.IsEmpty()) { // TODO: this should be localizable + idName.AppendLiteral(" ("); + idName.Append(label); + idName.Append(')'); + } + + if (!idName.IsEmpty()) return NS_OK; + + // If we still found nothing to use, use our key. + return ToString(idName); +} + +nsresult nsMsgIdentity::GetFullAddress(nsAString& fullAddress) { + nsAutoString fullName; + nsresult rv = GetFullName(fullName); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString email; + rv = GetEmail(email); + NS_ENSURE_SUCCESS(rv, rv); + + if (fullName.IsEmpty() && email.IsEmpty()) { + fullAddress.Truncate(); + } else { + nsCOMPtr<msgIAddressObject> mailbox; + nsCOMPtr<nsIMsgHeaderParser> headerParser( + mozilla::components::HeaderParser::Service()); + NS_ENSURE_TRUE(headerParser, NS_ERROR_UNEXPECTED); + headerParser->MakeMailboxObject(fullName, NS_ConvertUTF8toUTF16(email), + getter_AddRefs(mailbox)); + mailbox->ToString(fullAddress); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIdentity::ToString(nsAString& aResult) { + aResult.AssignLiteral("[nsIMsgIdentity: "); + aResult.Append(NS_ConvertASCIItoUTF16(mKey)); + aResult.Append(']'); + return NS_OK; +} + +/* Identity attribute accessors */ + +NS_IMETHODIMP +nsMsgIdentity::GetSignature(nsIFile** sig) { + bool gotRelPref; + nsresult rv = + NS_GetPersistentFile("sig_file" REL_FILE_PREF_SUFFIX, "sig_file", nullptr, + gotRelPref, sig, mPrefBranch); + if (NS_SUCCEEDED(rv) && !gotRelPref) { + rv = NS_SetPersistentFile("sig_file" REL_FILE_PREF_SUFFIX, "sig_file", *sig, + mPrefBranch); + NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to write signature file pref."); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIdentity::SetSignature(nsIFile* sig) { + nsresult rv = NS_OK; + if (sig) + rv = NS_SetPersistentFile("sig_file" REL_FILE_PREF_SUFFIX, "sig_file", sig, + mPrefBranch); + return rv; +} + +NS_IMETHODIMP +nsMsgIdentity::ClearAllValues() { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsTArray<nsCString> prefNames; + nsresult rv = mPrefBranch->GetChildList("", prefNames); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto& prefName : prefNames) { + mPrefBranch->ClearUserPref(prefName.get()); + } + + return NS_OK; +} + +NS_IMPL_IDPREF_STR(EscapedVCard, "escapedVCard") +NS_IMPL_IDPREF_STR(SmtpServerKey, "smtpServer") +NS_IMPL_IDPREF_WSTR(FullName, "fullName") +NS_IMPL_IDPREF_STR(Email, "useremail") +NS_IMPL_IDPREF_BOOL(CatchAll, "catchAll") +NS_IMPL_IDPREF_STR(CatchAllHint, "catchAllHint") +NS_IMPL_IDPREF_WSTR(Label, "label") +NS_IMPL_IDPREF_STR(ReplyTo, "reply_to") +NS_IMPL_IDPREF_WSTR(Organization, "organization") +NS_IMPL_IDPREF_BOOL(ComposeHtml, "compose_html") +NS_IMPL_IDPREF_BOOL(AttachVCard, "attach_vcard") +NS_IMPL_IDPREF_BOOL(AttachSignature, "attach_signature") +NS_IMPL_IDPREF_WSTR(HtmlSigText, "htmlSigText") +NS_IMPL_IDPREF_BOOL(HtmlSigFormat, "htmlSigFormat") + +NS_IMPL_IDPREF_BOOL(AutoQuote, "auto_quote") +NS_IMPL_IDPREF_INT(ReplyOnTop, "reply_on_top") +NS_IMPL_IDPREF_BOOL(SigBottom, "sig_bottom") +NS_IMPL_IDPREF_BOOL(SigOnForward, "sig_on_fwd") +NS_IMPL_IDPREF_BOOL(SigOnReply, "sig_on_reply") + +NS_IMPL_IDPREF_INT(SignatureDate, "sig_date") + +NS_IMPL_IDPREF_BOOL(DoFcc, "fcc") + +NS_IMPL_FOLDERPREF_STR(FccFolder, "fcc_folder", "Sent"_ns, + nsMsgFolderFlags::SentMail) +NS_IMPL_IDPREF_STR(FccFolderPickerMode, "fcc_folder_picker_mode") +NS_IMPL_IDPREF_BOOL(FccReplyFollowsParent, "fcc_reply_follows_parent") +NS_IMPL_IDPREF_STR(DraftsFolderPickerMode, "drafts_folder_picker_mode") +NS_IMPL_IDPREF_STR(ArchivesFolderPickerMode, "archives_folder_picker_mode") +NS_IMPL_IDPREF_STR(TmplFolderPickerMode, "tmpl_folder_picker_mode") + +NS_IMPL_IDPREF_BOOL(BccSelf, "bcc_self") +NS_IMPL_IDPREF_BOOL(BccOthers, "bcc_other") +NS_IMPL_IDPREF_STR(BccList, "bcc_other_list") + +NS_IMPL_IDPREF_BOOL(SuppressSigSep, "suppress_signature_separator") + +NS_IMPL_IDPREF_BOOL(DoCc, "doCc") +NS_IMPL_IDPREF_STR(DoCcList, "doCcList") + +NS_IMPL_IDPREF_BOOL(AttachPgpKey, "attachPgpKey") +NS_IMPL_IDPREF_BOOL(SendAutocryptHeaders, "sendAutocryptHeaders") +NS_IMPL_IDPREF_BOOL(AutoEncryptDrafts, "autoEncryptDrafts") +NS_IMPL_IDPREF_BOOL(ProtectSubject, "protectSubject") +NS_IMPL_IDPREF_INT(EncryptionPolicy, "encryptionpolicy") +NS_IMPL_IDPREF_BOOL(SignMail, "sign_mail") + +NS_IMETHODIMP +nsMsgIdentity::GetDoBcc(bool* aValue) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv = mPrefBranch->GetBoolPref("doBcc", aValue); + if (NS_SUCCEEDED(rv)) return rv; + + bool bccSelf = false; + GetBccSelf(&bccSelf); + + bool bccOthers = false; + GetBccOthers(&bccOthers); + + nsCString others; + GetBccList(others); + + *aValue = bccSelf || (bccOthers && !others.IsEmpty()); + + return SetDoBcc(*aValue); +} + +NS_IMETHODIMP +nsMsgIdentity::SetDoBcc(bool aValue) { + return SetBoolAttribute("doBcc", aValue); +} + +NS_IMETHODIMP +nsMsgIdentity::GetDoBccList(nsACString& aValue) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsCString val; + nsresult rv = mPrefBranch->GetCharPref("doBccList", val); + aValue = val; + if (NS_SUCCEEDED(rv)) return rv; + + bool bccSelf = false; + rv = GetBccSelf(&bccSelf); + NS_ENSURE_SUCCESS(rv, rv); + + if (bccSelf) GetEmail(aValue); + + bool bccOthers = false; + rv = GetBccOthers(&bccOthers); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString others; + rv = GetBccList(others); + NS_ENSURE_SUCCESS(rv, rv); + + if (bccOthers && !others.IsEmpty()) { + if (bccSelf) aValue.Append(','); + aValue.Append(others); + } + + return SetDoBccList(aValue); +} + +NS_IMETHODIMP +nsMsgIdentity::SetDoBccList(const nsACString& aValue) { + return SetCharAttribute("doBccList", aValue); +} + +NS_IMPL_FOLDERPREF_STR(DraftFolder, "draft_folder", "Drafts"_ns, + nsMsgFolderFlags::Drafts) +NS_IMPL_FOLDERPREF_STR(ArchiveFolder, "archive_folder", "Archives"_ns, + nsMsgFolderFlags::Archive) +NS_IMPL_FOLDERPREF_STR(StationeryFolder, "stationery_folder", "Templates"_ns, + nsMsgFolderFlags::Templates) + +NS_IMPL_IDPREF_BOOL(ArchiveEnabled, "archive_enabled") +NS_IMPL_IDPREF_INT(ArchiveGranularity, "archive_granularity") +NS_IMPL_IDPREF_BOOL(ArchiveKeepFolderStructure, "archive_keep_folder_structure") + +NS_IMPL_IDPREF_BOOL(ShowSaveMsgDlg, "showSaveMsgDlg") +NS_IMPL_IDPREF_STR(DirectoryServer, "directoryServer") +NS_IMPL_IDPREF_BOOL(OverrideGlobalPref, "overrideGlobal_Pref") +NS_IMPL_IDPREF_BOOL(AutocompleteToMyDomain, "autocompleteToMyDomain") + +NS_IMPL_IDPREF_BOOL(Valid, "valid") + +nsresult nsMsgIdentity::getFolderPref(const char* prefname, nsACString& retval, + const nsACString& folderName, + uint32_t folderflag) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv = mPrefBranch->GetStringPref(prefname, EmptyCString(), 0, retval); + if (NS_SUCCEEDED(rv) && !retval.IsEmpty()) { + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(retval, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgIncomingServer> server; + // Make sure that folder hierarchy is built so that legitimate parent-child + // relationship is established. + folder->GetServer(getter_AddRefs(server)); + if (server) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsCOMPtr<nsIMsgFolder> deferredToRootFolder; + server->GetRootFolder(getter_AddRefs(rootFolder)); + server->GetRootMsgFolder(getter_AddRefs(deferredToRootFolder)); + // check if we're using a deferred account - if not, use the uri; + // otherwise, fall through to code that will fix this pref. + if (rootFolder == deferredToRootFolder) { + nsCOMPtr<nsIMsgFolder> msgFolder; + rv = server->GetMsgFolderFromURI(folder, retval, + getter_AddRefs(msgFolder)); + return NS_SUCCEEDED(rv) ? msgFolder->GetURI(retval) : rv; + } + } + } + + // if the server doesn't exist, fall back to the default pref. + rv = mDefPrefBranch->GetStringPref(prefname, EmptyCString(), 0, retval); + if (NS_SUCCEEDED(rv) && !retval.IsEmpty()) + return setFolderPref(prefname, retval, folderflag); + + // here I think we need to create a uri for the folder on the + // default server for this identity. + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgIncomingServer>> servers; + rv = accountManager->GetServersForIdentity(this, servers); + NS_ENSURE_SUCCESS(rv, rv); + if (servers.IsEmpty()) { + // if there are no servers for this identity, return generic failure. + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIMsgIncomingServer> server(servers[0]); + bool defaultToServer; + server->GetDefaultCopiesAndFoldersPrefsToServer(&defaultToServer); + // if we should default to special folders on the server, + // use the local folders server + if (!defaultToServer) { + rv = accountManager->GetLocalFoldersServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + } + nsCOMPtr<nsIMsgFolder> rootFolder; + // this will get the deferred to server's root folder, if "server" + // is deferred, e.g., using the pop3 global inbox. + rv = server->GetRootMsgFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + if (rootFolder) { + rv = rootFolder->GetURI(retval); + NS_ENSURE_SUCCESS(rv, rv); + retval.Append('/'); + retval.Append(folderName); + return setFolderPref(prefname, retval, folderflag); + } + return NS_ERROR_FAILURE; +} + +nsresult nsMsgIdentity::setFolderPref(const char* prefname, + const nsACString& value, + uint32_t folderflag) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsCString oldpref; + nsresult rv; + nsCOMPtr<nsIMsgFolder> folder; + + if (folderflag == nsMsgFolderFlags::SentMail) { + // Clear the temporary return receipt filter so that the new filter + // rule can be recreated (by ConfigureTemporaryFilters()). + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgIncomingServer>> servers; + rv = accountManager->GetServersForIdentity(this, servers); + NS_ENSURE_SUCCESS(rv, rv); + if (!servers.IsEmpty()) { + servers[0]->ClearTemporaryReturnReceiptsFilter(); + // okay to fail; no need to check for return code + } + } + + // get the old folder, and clear the special folder flag on it + rv = mPrefBranch->GetStringPref(prefname, EmptyCString(), 0, oldpref); + if (NS_SUCCEEDED(rv) && !oldpref.IsEmpty()) { + rv = GetOrCreateFolder(oldpref, getter_AddRefs(folder)); + if (NS_SUCCEEDED(rv)) { + rv = folder->ClearFlag(folderflag); + } + } + + // set the new folder, and set the special folder flags on it + rv = SetUnicharAttribute(prefname, NS_ConvertUTF8toUTF16(value)); + if (NS_SUCCEEDED(rv) && !value.IsEmpty()) { + rv = GetOrCreateFolder(value, getter_AddRefs(folder)); + if (NS_SUCCEEDED(rv)) rv = folder->SetFlag(folderflag); + } + return rv; +} + +NS_IMETHODIMP nsMsgIdentity::SetUnicharAttribute(const char* aName, + const nsAString& val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + if (!val.IsEmpty()) + return mPrefBranch->SetStringPref(aName, NS_ConvertUTF16toUTF8(val)); + + mPrefBranch->ClearUserPref(aName); + return NS_OK; +} + +NS_IMETHODIMP nsMsgIdentity::GetUnicharAttribute(const char* aName, + nsAString& val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsCString valueUtf8; + if (NS_FAILED( + mPrefBranch->GetStringPref(aName, EmptyCString(), 0, valueUtf8))) + mDefPrefBranch->GetStringPref(aName, EmptyCString(), 0, valueUtf8); + CopyUTF8toUTF16(valueUtf8, val); + return NS_OK; +} + +NS_IMETHODIMP nsMsgIdentity::SetCharAttribute(const char* aName, + const nsACString& val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + if (!val.IsEmpty()) return mPrefBranch->SetCharPref(aName, val); + + mPrefBranch->ClearUserPref(aName); + return NS_OK; +} + +NS_IMETHODIMP nsMsgIdentity::GetCharAttribute(const char* aName, + nsACString& val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsCString tmpVal; + if (NS_FAILED(mPrefBranch->GetCharPref(aName, tmpVal))) + mDefPrefBranch->GetCharPref(aName, tmpVal); + val = tmpVal; + return NS_OK; +} + +NS_IMETHODIMP nsMsgIdentity::SetBoolAttribute(const char* aName, bool val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + return mPrefBranch->SetBoolPref(aName, val); +} + +NS_IMETHODIMP nsMsgIdentity::GetBoolAttribute(const char* aName, bool* val) { + NS_ENSURE_ARG_POINTER(val); + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + *val = false; + + if (NS_FAILED(mPrefBranch->GetBoolPref(aName, val))) + mDefPrefBranch->GetBoolPref(aName, val); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgIdentity::SetIntAttribute(const char* aName, int32_t val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + return mPrefBranch->SetIntPref(aName, val); +} + +NS_IMETHODIMP nsMsgIdentity::GetIntAttribute(const char* aName, int32_t* val) { + NS_ENSURE_ARG_POINTER(val); + + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + *val = 0; + + if (NS_FAILED(mPrefBranch->GetIntPref(aName, val))) + mDefPrefBranch->GetIntPref(aName, val); + + return NS_OK; +} + +#define COPY_IDENTITY_FILE_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \ + { \ + nsresult macro_rv; \ + nsCOMPtr<nsIFile> macro_spec; \ + macro_rv = SRC_ID->MACRO_GETTER(getter_AddRefs(macro_spec)); \ + if (NS_SUCCEEDED(macro_rv)) this->MACRO_SETTER(macro_spec); \ + } + +#define COPY_IDENTITY_INT_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \ + { \ + nsresult macro_rv; \ + int32_t macro_oldInt; \ + macro_rv = SRC_ID->MACRO_GETTER(¯o_oldInt); \ + if (NS_SUCCEEDED(macro_rv)) this->MACRO_SETTER(macro_oldInt); \ + } + +#define COPY_IDENTITY_BOOL_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \ + { \ + nsresult macro_rv; \ + bool macro_oldBool; \ + macro_rv = SRC_ID->MACRO_GETTER(¯o_oldBool); \ + if (NS_SUCCEEDED(macro_rv)) this->MACRO_SETTER(macro_oldBool); \ + } + +#define COPY_IDENTITY_STR_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \ + { \ + nsCString macro_oldStr; \ + nsresult macro_rv; \ + macro_rv = SRC_ID->MACRO_GETTER(macro_oldStr); \ + if (NS_SUCCEEDED(macro_rv)) { \ + this->MACRO_SETTER(macro_oldStr); \ + } \ + } + +#define COPY_IDENTITY_WSTR_VALUE(SRC_ID, MACRO_GETTER, MACRO_SETTER) \ + { \ + nsString macro_oldStr; \ + nsresult macro_rv; \ + macro_rv = SRC_ID->MACRO_GETTER(macro_oldStr); \ + if (NS_SUCCEEDED(macro_rv)) { \ + this->MACRO_SETTER(macro_oldStr); \ + } \ + } + +NS_IMETHODIMP +nsMsgIdentity::Copy(nsIMsgIdentity* identity) { + NS_ENSURE_ARG_POINTER(identity); + + COPY_IDENTITY_BOOL_VALUE(identity, GetComposeHtml, SetComposeHtml) + COPY_IDENTITY_STR_VALUE(identity, GetEmail, SetEmail) + COPY_IDENTITY_BOOL_VALUE(identity, GetCatchAll, SetCatchAll) + COPY_IDENTITY_WSTR_VALUE(identity, GetLabel, SetLabel) + COPY_IDENTITY_STR_VALUE(identity, GetReplyTo, SetReplyTo) + COPY_IDENTITY_WSTR_VALUE(identity, GetFullName, SetFullName) + COPY_IDENTITY_WSTR_VALUE(identity, GetOrganization, SetOrganization) + COPY_IDENTITY_STR_VALUE(identity, GetDraftFolder, SetDraftFolder) + COPY_IDENTITY_STR_VALUE(identity, GetArchiveFolder, SetArchiveFolder) + COPY_IDENTITY_STR_VALUE(identity, GetFccFolder, SetFccFolder) + COPY_IDENTITY_BOOL_VALUE(identity, GetFccReplyFollowsParent, + SetFccReplyFollowsParent) + COPY_IDENTITY_STR_VALUE(identity, GetStationeryFolder, SetStationeryFolder) + COPY_IDENTITY_BOOL_VALUE(identity, GetArchiveEnabled, SetArchiveEnabled) + COPY_IDENTITY_INT_VALUE(identity, GetArchiveGranularity, + SetArchiveGranularity) + COPY_IDENTITY_BOOL_VALUE(identity, GetArchiveKeepFolderStructure, + SetArchiveKeepFolderStructure) + COPY_IDENTITY_BOOL_VALUE(identity, GetAttachSignature, SetAttachSignature) + COPY_IDENTITY_FILE_VALUE(identity, GetSignature, SetSignature) + COPY_IDENTITY_WSTR_VALUE(identity, GetHtmlSigText, SetHtmlSigText) + COPY_IDENTITY_BOOL_VALUE(identity, GetHtmlSigFormat, SetHtmlSigFormat) + COPY_IDENTITY_BOOL_VALUE(identity, GetAutoQuote, SetAutoQuote) + COPY_IDENTITY_INT_VALUE(identity, GetReplyOnTop, SetReplyOnTop) + COPY_IDENTITY_BOOL_VALUE(identity, GetSigBottom, SetSigBottom) + COPY_IDENTITY_BOOL_VALUE(identity, GetSigOnForward, SetSigOnForward) + COPY_IDENTITY_BOOL_VALUE(identity, GetSigOnReply, SetSigOnReply) + COPY_IDENTITY_INT_VALUE(identity, GetSignatureDate, SetSignatureDate) + COPY_IDENTITY_BOOL_VALUE(identity, GetAttachVCard, SetAttachVCard) + COPY_IDENTITY_STR_VALUE(identity, GetEscapedVCard, SetEscapedVCard) + COPY_IDENTITY_STR_VALUE(identity, GetSmtpServerKey, SetSmtpServerKey) + COPY_IDENTITY_BOOL_VALUE(identity, GetSuppressSigSep, SetSuppressSigSep) + + COPY_IDENTITY_BOOL_VALUE(identity, GetAttachPgpKey, SetAttachPgpKey) + COPY_IDENTITY_BOOL_VALUE(identity, GetSendAutocryptHeaders, + SetSendAutocryptHeaders) + COPY_IDENTITY_BOOL_VALUE(identity, GetAutoEncryptDrafts, SetAutoEncryptDrafts) + COPY_IDENTITY_BOOL_VALUE(identity, GetProtectSubject, SetProtectSubject) + COPY_IDENTITY_INT_VALUE(identity, GetEncryptionPolicy, SetEncryptionPolicy) + COPY_IDENTITY_BOOL_VALUE(identity, GetSignMail, SetSignMail) + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIdentity::GetRequestReturnReceipt(bool* aVal) { + NS_ENSURE_ARG_POINTER(aVal); + + bool useCustomPrefs = false; + nsresult rv = GetBoolAttribute("use_custom_prefs", &useCustomPrefs); + NS_ENSURE_SUCCESS(rv, rv); + if (useCustomPrefs) + return GetBoolAttribute("request_return_receipt_on", aVal); + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return prefs->GetBoolPref("mail.receipt.request_return_receipt_on", aVal); +} + +NS_IMETHODIMP +nsMsgIdentity::GetReceiptHeaderType(int32_t* aType) { + NS_ENSURE_ARG_POINTER(aType); + + bool useCustomPrefs = false; + nsresult rv = GetBoolAttribute("use_custom_prefs", &useCustomPrefs); + NS_ENSURE_SUCCESS(rv, rv); + if (useCustomPrefs) + return GetIntAttribute("request_receipt_header_type", aType); + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return prefs->GetIntPref("mail.receipt.request_header_type", aType); +} + +NS_IMETHODIMP +nsMsgIdentity::GetRequestDSN(bool* aVal) { + NS_ENSURE_ARG_POINTER(aVal); + + bool useCustomPrefs = false; + nsresult rv = GetBoolAttribute("dsn_use_custom_prefs", &useCustomPrefs); + NS_ENSURE_SUCCESS(rv, rv); + if (useCustomPrefs) return GetBoolAttribute("dsn_always_request_on", aVal); + + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return prefs->GetBoolPref("mail.dsn.always_request_on", aVal); +} diff --git a/comm/mailnews/base/src/nsMsgIdentity.h b/comm/mailnews/base/src/nsMsgIdentity.h new file mode 100644 index 0000000000..1b9e0f1635 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgIdentity.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgIdentity_h___ +#define nsMsgIdentity_h___ + +#include "nsIMsgIdentity.h" +#include "nsIPrefBranch.h" +#include "msgCore.h" +#include "nsCOMPtr.h" +#include "nsString.h" + +class nsMsgIdentity final : public nsIMsgIdentity { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMSGIDENTITY + + private: + ~nsMsgIdentity() {} + nsCString mKey; + nsCOMPtr<nsIPrefBranch> mPrefBranch; + nsCOMPtr<nsIPrefBranch> mDefPrefBranch; + + protected: + nsresult getFolderPref(const char* pref, nsACString& retval, + const nsACString& folderName, uint32_t folderFlag); + nsresult setFolderPref(const char* pref, const nsACString& retval, + uint32_t folderFlag); +}; + +#define NS_IMPL_IDPREF_STR(_postfix, _prefname) \ + NS_IMETHODIMP \ + nsMsgIdentity::Get##_postfix(nsACString& retval) { \ + return GetCharAttribute(_prefname, retval); \ + } \ + NS_IMETHODIMP \ + nsMsgIdentity::Set##_postfix(const nsACString& value) { \ + return SetCharAttribute(_prefname, value); \ + } + +#define NS_IMPL_IDPREF_WSTR(_postfix, _prefname) \ + NS_IMETHODIMP \ + nsMsgIdentity::Get##_postfix(nsAString& retval) { \ + return GetUnicharAttribute(_prefname, retval); \ + } \ + NS_IMETHODIMP \ + nsMsgIdentity::Set##_postfix(const nsAString& value) { \ + return SetUnicharAttribute(_prefname, value); \ + } + +#define NS_IMPL_IDPREF_BOOL(_postfix, _prefname) \ + NS_IMETHODIMP \ + nsMsgIdentity::Get##_postfix(bool* retval) { \ + return GetBoolAttribute(_prefname, retval); \ + } \ + NS_IMETHODIMP \ + nsMsgIdentity::Set##_postfix(bool value) { \ + return mPrefBranch->SetBoolPref(_prefname, value); \ + } + +#define NS_IMPL_IDPREF_INT(_postfix, _prefname) \ + NS_IMETHODIMP \ + nsMsgIdentity::Get##_postfix(int32_t* retval) { \ + return GetIntAttribute(_prefname, retval); \ + } \ + NS_IMETHODIMP \ + nsMsgIdentity::Set##_postfix(int32_t value) { \ + return mPrefBranch->SetIntPref(_prefname, value); \ + } + +#define NS_IMPL_FOLDERPREF_STR(_postfix, _prefname, _foldername, _flag) \ + NS_IMETHODIMP \ + nsMsgIdentity::Get##_postfix(nsACString& retval) { \ + nsresult rv; \ + nsCString folderPref; \ + rv = getFolderPref(_prefname, folderPref, _foldername, _flag); \ + retval = folderPref; \ + return rv; \ + } \ + NS_IMETHODIMP \ + nsMsgIdentity::Set##_postfix(const nsACString& value) { \ + return setFolderPref(_prefname, value, _flag); \ + } + +#endif /* nsMsgIdentity_h___ */ diff --git a/comm/mailnews/base/src/nsMsgIncomingServer.cpp b/comm/mailnews/base/src/nsMsgIncomingServer.cpp new file mode 100644 index 0000000000..0d914ffbd9 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgIncomingServer.cpp @@ -0,0 +1,2142 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgIncomingServer.h" +#include "nscore.h" +#include "plstr.h" +#include "prmem.h" +#include "prprf.h" + +#include "nsIServiceManager.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsMemory.h" +#include "nsISupportsPrimitives.h" + +#include "nsIMsgBiffManager.h" +#include "nsIMsgFolder.h" +#include "nsMsgDBFolder.h" +#include "nsIMsgFolderCache.h" +#include "nsIMsgPluggableStore.h" +#include "nsIMsgFolderCacheElement.h" +#include "nsIMsgWindow.h" +#include "nsIMsgFilterService.h" +#include "nsIMsgProtocolInfo.h" +#include "nsIPrefService.h" +#include "nsIRelativeFilePref.h" +#include "mozilla/nsRelativeFilePref.h" +#include "nsIDocShell.h" +#include "nsIAuthPrompt.h" +#include "nsNetUtil.h" +#include "nsIWindowWatcher.h" +#include "nsIStringBundle.h" +#include "nsIMsgHdr.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsILoginInfo.h" +#include "nsILoginManager.h" +#include "nsIMsgAccountManager.h" +#include "nsIMsgMdnGenerator.h" +#include "nsMsgUtils.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgSearchTerm.h" +#include "nsAppDirectoryServiceDefs.h" +#include "mozilla/Components.h" +#include "mozilla/Services.h" +#include "nsIMsgFilter.h" +#include "nsIObserverService.h" +#include "mozilla/Unused.h" +#include "nsIUUIDGenerator.h" +#include "nsArrayUtils.h" + +#define PORT_NOT_SET -1 + +nsMsgIncomingServer::nsMsgIncomingServer() + : m_rootFolder(nullptr), + m_downloadedHdrs(50), + m_numMsgsDownloaded(0), + m_biffState(nsIMsgFolder::nsMsgBiffState_Unknown), + m_serverBusy(false), + m_canHaveFilters(true), + m_displayStartupPage(true), + mPerformingBiff(false) {} + +nsresult nsMsgIncomingServer::Init() { + // We need to know when the password manager changes. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED); + + observerService->AddObserver(this, "passwordmgr-storage-changed", false); + observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + return NS_OK; +} + +nsMsgIncomingServer::~nsMsgIncomingServer() {} + +NS_IMPL_ISUPPORTS(nsMsgIncomingServer, nsIMsgIncomingServer, + nsISupportsWeakReference, nsIObserver) + +/** + * Observe() receives notifications for all accounts, not just this server's + * account. So we ignore all notifications not intended for this server. + * When the state of the password manager changes we need to clear the + * this server's password from the cache in case the user just changed or + * removed the password or username. + * Oauth2 servers often automatically change the password manager's stored + * password (the token). + */ +NS_IMETHODIMP +nsMsgIncomingServer::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + nsresult rv; + if (strcmp(aTopic, "passwordmgr-storage-changed") == 0) { + nsAutoString otherFullName; + nsAutoString otherUserName; + // Check that the notification is for this server. + nsCOMPtr<nsILoginInfo> loginInfo = do_QueryInterface(aSubject); + if (loginInfo) { + // The login info for this server has been removed with aData being + // "removeLogin" or "removeAllLogins". + loginInfo->GetOrigin(otherFullName); + loginInfo->GetUsername(otherUserName); + } else { + // Probably a 2 element array containing old and new login info due to + // aData being "modifyLogin". E.g., a user has modified password or + // username in the password manager or an OAuth2 token string has + // automatically changed. + nsCOMPtr<nsIArray> logins = do_QueryInterface(aSubject); + if (logins) { + // Only need to look at names in first array element (login info before + // any modification) since the user might have changed the username as + // found in the 2nd elements. (The hostname can't be modified in the + // password manager.) + nsCOMPtr<nsILoginInfo> login; + logins->QueryElementAt(0, NS_GET_IID(nsILoginInfo), + getter_AddRefs(login)); + if (login) { + login->GetOrigin(otherFullName); + login->GetUsername(otherUserName); + } + } + } + if (!otherFullName.IsEmpty()) { + nsAutoCString thisHostname; + nsAutoCString thisUsername; + GetHostName(thisHostname); + GetUsername(thisUsername); + nsAutoCString thisFullName; + GetType(thisFullName); + if (thisFullName.EqualsLiteral("pop3")) { + // Note: POP3 now handled by MsgIncomingServer.jsm so does not occur. + MOZ_ASSERT_UNREACHABLE("pop3 should not use nsMsgIncomingServer"); + thisFullName = "mailbox://"_ns + thisHostname; + } else { + thisFullName += "://"_ns + thisHostname; + } + if (!thisFullName.Equals(NS_ConvertUTF16toUTF8(otherFullName)) || + !thisUsername.Equals(NS_ConvertUTF16toUTF8(otherUserName))) { + // Not for this server; keep this server's cached password. + return NS_OK; + } + } else if (NS_strcmp(aData, u"hostSavingDisabled") != 0) { + // "hostSavingDisabled" only occurs during test_smtpServer.js and + // expects the password to be removed from memory cache. Otherwise, we + // don't have enough information to decide to remove the cached + // password, so keep it. + return NS_OK; + } + // When nsMsgImapIncomingServer::ForgetSessionPassword called with + // parameter modifyLogin true and if the server uses OAuth2, it causes the + // password to not be cleared from cache. This is needed by autosync. When + // the aData paremater of Observe() is not "modifyLogin" but is + // e.g., "removeLogin" or "removeAllLogins", ForgetSessionPassword(false) + // will still clear the cached password regardless of authentication method. + rv = ForgetSessionPassword(NS_strcmp(aData, u"modifyLogin") == 0); + NS_ENSURE_SUCCESS(rv, rv); + } else if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) { + // Now remove ourselves from the observer service as well. + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(observerService, NS_ERROR_UNEXPECTED); + + observerService->RemoveObserver(this, "passwordmgr-storage-changed"); + observerService->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetServerBusy(bool aServerBusy) { + m_serverBusy = aServerBusy; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetServerBusy(bool* aServerBusy) { + NS_ENSURE_ARG_POINTER(aServerBusy); + *aServerBusy = m_serverBusy; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetKey(nsACString& serverKey) { + serverKey = m_serverKey; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetKey(const nsACString& serverKey) { + m_serverKey.Assign(serverKey); + + // in order to actually make use of the key, we need the prefs + nsresult rv; + nsCOMPtr<nsIPrefService> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString branchName; + branchName.AssignLiteral("mail.server."); + branchName.Append(m_serverKey); + branchName.Append('.'); + rv = prefs->GetBranch(branchName.get(), getter_AddRefs(mPrefBranch)); + NS_ENSURE_SUCCESS(rv, rv); + + return prefs->GetBranch("mail.server.default.", + getter_AddRefs(mDefPrefBranch)); +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetUID(nsACString& uid) { + bool hasValue; + nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue); + NS_ENSURE_SUCCESS(rv, rv); + if (hasValue) { + return GetCharValue("uid", uid); + } + + nsCOMPtr<nsIUUIDGenerator> uuidgen = + mozilla::components::UUIDGenerator::Service(); + NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE); + + nsID id; + rv = uuidgen->GenerateUUIDInPlace(&id); + NS_ENSURE_SUCCESS(rv, rv); + + char idString[NSID_LENGTH]; + id.ToProvidedString(idString); + + uid.AppendASCII(idString + 1, NSID_LENGTH - 3); + return SetUID(uid); +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetUID(const nsACString& uid) { + bool hasValue; + nsresult rv = mPrefBranch->PrefHasUserValue("uid", &hasValue); + NS_ENSURE_SUCCESS(rv, rv); + if (hasValue) { + return NS_ERROR_ABORT; + } + return SetCharValue("uid", uid); +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetRootFolder(nsIMsgFolder* aRootFolder) { + m_rootFolder = aRootFolder; + return NS_OK; +} + +// this will return the root folder of this account, +// even if this server is deferred. +NS_IMETHODIMP +nsMsgIncomingServer::GetRootFolder(nsIMsgFolder** aRootFolder) { + NS_ENSURE_ARG_POINTER(aRootFolder); + if (!m_rootFolder) { + nsresult rv = CreateRootFolder(); + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_IF_ADDREF(*aRootFolder = m_rootFolder); + return NS_OK; +} + +// this will return the root folder of the deferred to account, +// if this server is deferred. +NS_IMETHODIMP +nsMsgIncomingServer::GetRootMsgFolder(nsIMsgFolder** aRootMsgFolder) { + return GetRootFolder(aRootMsgFolder); +} + +NS_IMETHODIMP +nsMsgIncomingServer::PerformExpand(nsIMsgWindow* aMsgWindow) { return NS_OK; } + +NS_IMETHODIMP +nsMsgIncomingServer::VerifyLogon(nsIUrlListener* aUrlListener, + nsIMsgWindow* aMsgWindow, nsIURI** aURL) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgIncomingServer::PerformBiff(nsIMsgWindow* aMsgWindow) { + // This has to be implemented in the derived class, but in case someone + // doesn't implement it just return not implemented. + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetNewMessages(nsIMsgFolder* aFolder, + nsIMsgWindow* aMsgWindow, + nsIUrlListener* aUrlListener) { + NS_ENSURE_ARG_POINTER(aFolder); + return aFolder->GetNewMessages(aMsgWindow, aUrlListener); +} + +NS_IMETHODIMP nsMsgIncomingServer::GetPerformingBiff(bool* aPerformingBiff) { + NS_ENSURE_ARG_POINTER(aPerformingBiff); + *aPerformingBiff = mPerformingBiff; + return NS_OK; +} + +NS_IMETHODIMP nsMsgIncomingServer::SetPerformingBiff(bool aPerformingBiff) { + mPerformingBiff = aPerformingBiff; + return NS_OK; +} + +NS_IMPL_GETSET(nsMsgIncomingServer, BiffState, uint32_t, m_biffState) + +NS_IMETHODIMP nsMsgIncomingServer::WriteToFolderCache( + nsIMsgFolderCache* folderCache) { + nsresult rv = NS_OK; + if (m_rootFolder) { + rv = m_rootFolder->WriteToFolderCache(folderCache, true /* deep */); + } + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::Shutdown() { + nsresult rv = CloseCachedConnections(); + mFilterPlugin = nullptr; + NS_ENSURE_SUCCESS(rv, rv); + + if (mFilterList) { + // close the filter log stream + rv = mFilterList->SetLogStream(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + mFilterList = nullptr; + } + + if (mSpamSettings) { + // close the spam log stream + rv = mSpamSettings->SetLogStream(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + mSpamSettings = nullptr; + } + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::CloseCachedConnections() { + // derived class should override if they cache connections. + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetDownloadMessagesAtStartup(bool* getMessagesAtStartup) { + // derived class should override if they need to do this. + *getMessagesAtStartup = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetCanHaveFilters(bool* canHaveFilters) { + NS_ENSURE_ARG_POINTER(canHaveFilters); + *canHaveFilters = m_canHaveFilters; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetCanHaveFilters(bool aCanHaveFilters) { + m_canHaveFilters = aCanHaveFilters; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetCanBeDefaultServer(bool* canBeDefaultServer) { + // derived class should override if they need to do this. + *canBeDefaultServer = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetCanSearchMessages(bool* canSearchMessages) { + // derived class should override if they need to do this. + NS_ENSURE_ARG_POINTER(canSearchMessages); + *canSearchMessages = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetCanCompactFoldersOnServer( + bool* canCompactFoldersOnServer) { + // derived class should override if they need to do this. + NS_ENSURE_ARG_POINTER(canCompactFoldersOnServer); + *canCompactFoldersOnServer = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetCanUndoDeleteOnServer(bool* canUndoDeleteOnServer) { + // derived class should override if they need to do this. + NS_ENSURE_ARG_POINTER(canUndoDeleteOnServer); + *canUndoDeleteOnServer = true; + return NS_OK; +} + +// construct <localStoreType>://[<username>@]<hostname +NS_IMETHODIMP +nsMsgIncomingServer::GetServerURI(nsACString& aResult) { + nsresult rv; + rv = GetLocalStoreType(aResult); + NS_ENSURE_SUCCESS(rv, rv); + aResult.AppendLiteral("://"); + + nsCString username; + rv = GetUsername(username); + if (NS_SUCCEEDED(rv) && !username.IsEmpty()) { + nsCString escapedUsername; + MsgEscapeString(username, nsINetUtil::ESCAPE_XALPHAS, escapedUsername); + // not all servers have a username + aResult.Append(escapedUsername); + aResult.Append('@'); + } + + nsCString hostname; + rv = GetHostName(hostname); + if (NS_SUCCEEDED(rv) && !hostname.IsEmpty()) { + nsCString escapedHostname; + MsgEscapeString(hostname, nsINetUtil::ESCAPE_URL_PATH, escapedHostname); + // not all servers have a hostname + aResult.Append(escapedHostname); + } + return NS_OK; +} + +// helper routine to create local folder on disk, if it doesn't exist. +nsresult nsMsgIncomingServer::CreateLocalFolder(const nsAString& folderName) { + nsCOMPtr<nsIMsgFolder> rootFolder; + nsresult rv = GetRootFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgFolder> child; + rv = rootFolder->GetChildNamed(folderName, getter_AddRefs(child)); + if (child) return NS_OK; + nsCOMPtr<nsIMsgPluggableStore> msgStore; + rv = GetMsgStore(getter_AddRefs(msgStore)); + NS_ENSURE_SUCCESS(rv, rv); + return msgStore->CreateFolder(rootFolder, folderName, getter_AddRefs(child)); +} + +nsresult nsMsgIncomingServer::CreateRootFolder() { + nsresult rv; + // get the URI from the incoming server + nsCString serverUri; + rv = GetServerURI(serverUri); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetOrCreateFolder(serverUri, getter_AddRefs(m_rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetBoolValue(const char* prefname, bool* val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + NS_ENSURE_ARG_POINTER(val); + *val = false; + + if (NS_FAILED(mPrefBranch->GetBoolPref(prefname, val))) + mDefPrefBranch->GetBoolPref(prefname, val); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetBoolValue(const char* prefname, bool val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + bool defaultValue; + nsresult rv = mDefPrefBranch->GetBoolPref(prefname, &defaultValue); + + if (NS_SUCCEEDED(rv) && val == defaultValue) + mPrefBranch->ClearUserPref(prefname); + else + rv = mPrefBranch->SetBoolPref(prefname, val); + + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetIntValue(const char* prefname, int32_t* val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + NS_ENSURE_ARG_POINTER(val); + *val = 0; + + if (NS_FAILED(mPrefBranch->GetIntPref(prefname, val))) + mDefPrefBranch->GetIntPref(prefname, val); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetFileValue(const char* aRelPrefName, + const char* aAbsPrefName, + nsIFile** aLocalFile) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + // Get the relative first + nsCOMPtr<nsIRelativeFilePref> relFilePref; + nsresult rv = mPrefBranch->GetComplexValue(aRelPrefName, + NS_GET_IID(nsIRelativeFilePref), + getter_AddRefs(relFilePref)); + if (relFilePref) { + rv = relFilePref->GetFile(aLocalFile); + NS_ASSERTION(*aLocalFile, "An nsIRelativeFilePref has no file."); + if (NS_SUCCEEDED(rv)) (*aLocalFile)->Normalize(); + } else { + rv = mPrefBranch->GetComplexValue(aAbsPrefName, NS_GET_IID(nsIFile), + reinterpret_cast<void**>(aLocalFile)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIRelativeFilePref> relFilePref = + new mozilla::nsRelativeFilePref(); + mozilla::Unused << relFilePref->SetFile(*aLocalFile); + mozilla::Unused << relFilePref->SetRelativeToKey( + nsLiteralCString(NS_APP_USER_PROFILE_50_DIR)); + + rv = mPrefBranch->SetComplexValue( + aRelPrefName, NS_GET_IID(nsIRelativeFilePref), relFilePref); + } + + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetFileValue(const char* aRelPrefName, + const char* aAbsPrefName, + nsIFile* aLocalFile) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + // Write the relative path. + nsCOMPtr<nsIRelativeFilePref> relFilePref = new mozilla::nsRelativeFilePref(); + mozilla::Unused << relFilePref->SetFile(aLocalFile); + mozilla::Unused << relFilePref->SetRelativeToKey( + nsLiteralCString(NS_APP_USER_PROFILE_50_DIR)); + + nsresult rv = mPrefBranch->SetComplexValue( + aRelPrefName, NS_GET_IID(nsIRelativeFilePref), relFilePref); + if (NS_FAILED(rv)) return rv; + + return mPrefBranch->SetComplexValue(aAbsPrefName, NS_GET_IID(nsIFile), + aLocalFile); +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetIntValue(const char* prefname, int32_t val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + int32_t defaultVal; + nsresult rv = mDefPrefBranch->GetIntPref(prefname, &defaultVal); + + if (NS_SUCCEEDED(rv) && defaultVal == val) + mPrefBranch->ClearUserPref(prefname); + else + rv = mPrefBranch->SetIntPref(prefname, val); + + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetCharValue(const char* prefname, nsACString& val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsCString tmpVal; + if (NS_FAILED(mPrefBranch->GetCharPref(prefname, tmpVal))) + mDefPrefBranch->GetCharPref(prefname, tmpVal); + val = tmpVal; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetUnicharValue(const char* prefname, nsAString& val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsCString valueUtf8; + if (NS_FAILED( + mPrefBranch->GetStringPref(prefname, EmptyCString(), 0, valueUtf8))) + mDefPrefBranch->GetStringPref(prefname, EmptyCString(), 0, valueUtf8); + CopyUTF8toUTF16(valueUtf8, val); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetCharValue(const char* prefname, const nsACString& val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + if (val.IsEmpty()) { + mPrefBranch->ClearUserPref(prefname); + return NS_OK; + } + + nsCString defaultVal; + nsresult rv = mDefPrefBranch->GetCharPref(prefname, defaultVal); + + if (NS_SUCCEEDED(rv) && defaultVal.Equals(val)) + mPrefBranch->ClearUserPref(prefname); + else + rv = mPrefBranch->SetCharPref(prefname, val); + + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetUnicharValue(const char* prefname, + const nsAString& val) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + if (val.IsEmpty()) { + mPrefBranch->ClearUserPref(prefname); + return NS_OK; + } + + nsCString defaultVal; + nsresult rv = + mDefPrefBranch->GetStringPref(prefname, EmptyCString(), 0, defaultVal); + + if (NS_SUCCEEDED(rv) && defaultVal.Equals(NS_ConvertUTF16toUTF8(val))) + mPrefBranch->ClearUserPref(prefname); + else + rv = mPrefBranch->SetStringPref(prefname, NS_ConvertUTF16toUTF8(val)); + + return rv; +} + +// pretty name is the display name to show to the user +NS_IMETHODIMP +nsMsgIncomingServer::GetPrettyName(nsAString& retval) { + nsresult rv = GetUnicharValue("name", retval); + NS_ENSURE_SUCCESS(rv, rv); + + // if there's no name, then just return the hostname + return retval.IsEmpty() ? GetConstructedPrettyName(retval) : rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetPrettyName(const nsAString& value) { + SetUnicharValue("name", value); + nsCOMPtr<nsIMsgFolder> rootFolder; + GetRootFolder(getter_AddRefs(rootFolder)); + if (rootFolder) rootFolder->SetPrettyName(value); + return NS_OK; +} + +// construct the pretty name to show to the user if they haven't +// specified one. This should be overridden for news and mail. +NS_IMETHODIMP +nsMsgIncomingServer::GetConstructedPrettyName(nsAString& retval) { + nsCString username; + nsresult rv = GetUsername(username); + NS_ENSURE_SUCCESS(rv, rv); + if (!username.IsEmpty()) { + CopyASCIItoUTF16(username, retval); + retval.AppendLiteral(" on "); + } + + nsCString hostname; + rv = GetHostName(hostname); + NS_ENSURE_SUCCESS(rv, rv); + + retval.Append(NS_ConvertASCIItoUTF16(hostname)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::ToString(nsAString& aResult) { + aResult.AssignLiteral("[nsIMsgIncomingServer: "); + aResult.Append(NS_ConvertASCIItoUTF16(m_serverKey)); + aResult.Append(']'); + return NS_OK; +} + +NS_IMETHODIMP nsMsgIncomingServer::SetPassword(const nsAString& aPassword) { + m_password = aPassword; + return NS_OK; +} + +NS_IMETHODIMP nsMsgIncomingServer::GetPassword(nsAString& aPassword) { + aPassword = m_password; + return NS_OK; +} + +NS_IMETHODIMP nsMsgIncomingServer::GetServerRequiresPasswordForBiff( + bool* aServerRequiresPasswordForBiff) { + NS_ENSURE_ARG_POINTER(aServerRequiresPasswordForBiff); + *aServerRequiresPasswordForBiff = true; + return NS_OK; +} + +// This sets m_password if we find a password in the pw mgr. +nsresult nsMsgIncomingServer::GetPasswordWithoutUI() { + nsresult rv; + nsCOMPtr<nsILoginManager> loginMgr( + do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the current server URI + nsCString currServerUri; + rv = GetLocalStoreType(currServerUri); + NS_ENSURE_SUCCESS(rv, rv); + + currServerUri.AppendLiteral("://"); + + nsCString temp; + rv = GetHostName(temp); + NS_ENSURE_SUCCESS(rv, rv); + + currServerUri.Append(temp); + + NS_ConvertUTF8toUTF16 currServer(currServerUri); + + nsTArray<RefPtr<nsILoginInfo>> logins; + rv = loginMgr->FindLogins(currServer, EmptyString(), currServer, logins); + + // Login manager can produce valid fails, e.g. NS_ERROR_ABORT when a user + // cancels the master password dialog. Therefore handle that here, but don't + // warn about it. + if (NS_FAILED(rv)) return rv; + uint32_t numLogins = logins.Length(); + + // Don't abort here, if we didn't find any or failed, then we'll just have + // to prompt. + if (numLogins > 0) { + nsCString serverCUsername; + rv = GetUsername(serverCUsername); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ConvertUTF8toUTF16 serverUsername(serverCUsername); + + nsString username; + for (uint32_t i = 0; i < numLogins; ++i) { + rv = logins[i]->GetUsername(username); + NS_ENSURE_SUCCESS(rv, rv); + + if (username.Equals(serverUsername)) { + nsString password; + rv = logins[i]->GetPassword(password); + NS_ENSURE_SUCCESS(rv, rv); + + m_password = password; + break; + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetPasswordWithUI(const nsAString& aPromptMessage, + const nsAString& aPromptTitle, + nsAString& aPassword) { + nsresult rv = NS_OK; + + if (m_password.IsEmpty()) { + // let's see if we have the password in the password manager and + // can avoid this prompting thing. This makes it easier to get embedders + // to get up and running w/o a password prompting UI. + rv = GetPasswordWithoutUI(); + // If GetPasswordWithoutUI returns NS_ERROR_ABORT, the most likely case + // is the user canceled getting the master password, so just return + // straight away, as they won't want to get prompted again. + if (rv == NS_ERROR_ABORT) return NS_MSG_PASSWORD_PROMPT_CANCELLED; + } + if (m_password.IsEmpty()) { + nsCOMPtr<nsIAuthPrompt> authPrompt = + do_GetService("@mozilla.org/messenger/msgAuthPrompt;1"); + if (authPrompt) { + // prompt the user for the password + nsCString serverUri; + rv = GetLocalStoreType(serverUri); + NS_ENSURE_SUCCESS(rv, rv); + + serverUri.AppendLiteral("://"); + nsCString temp; + rv = GetUsername(temp); + NS_ENSURE_SUCCESS(rv, rv); + + if (!temp.IsEmpty()) { + nsCString escapedUsername; + MsgEscapeString(temp, nsINetUtil::ESCAPE_XALPHAS, escapedUsername); + serverUri.Append(escapedUsername); + serverUri.Append('@'); + } + + rv = GetHostName(temp); + NS_ENSURE_SUCCESS(rv, rv); + + serverUri.Append(temp); + + // we pass in the previously used password, if any, into PromptPassword + // so that it will appear as ******. This means we can't use an nsString + // and getter_Copies. + char16_t* uniPassword = nullptr; + if (!aPassword.IsEmpty()) uniPassword = ToNewUnicode(aPassword); + + bool okayValue = true; + rv = authPrompt->PromptPassword(PromiseFlatString(aPromptTitle).get(), + PromiseFlatString(aPromptMessage).get(), + NS_ConvertASCIItoUTF16(serverUri).get(), + nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY, + &uniPassword, &okayValue); + NS_ENSURE_SUCCESS(rv, rv); + + if (!okayValue) // if the user pressed cancel, just return an empty + // string; + { + aPassword.Truncate(); + return NS_MSG_PASSWORD_PROMPT_CANCELLED; + } + + // we got a password back...so remember it + rv = SetPassword(nsDependentString(uniPassword)); + NS_ENSURE_SUCCESS(rv, rv); + + PR_FREEIF(uniPassword); + } // if we got a prompt dialog + else + return NS_ERROR_FAILURE; + } // if the password is empty + return GetPassword(aPassword); +} + +NS_IMETHODIMP +nsMsgIncomingServer::ForgetPassword() { + nsresult rv; + nsCOMPtr<nsILoginManager> loginMgr = + do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Get the current server URI + nsCString currServerUri; + rv = GetLocalStoreType(currServerUri); + NS_ENSURE_SUCCESS(rv, rv); + + currServerUri.AppendLiteral("://"); + + nsCString temp; + rv = GetHostName(temp); + NS_ENSURE_SUCCESS(rv, rv); + + currServerUri.Append(temp); + + NS_ConvertUTF8toUTF16 currServer(currServerUri); + + nsCString serverCUsername; + rv = GetUsername(serverCUsername); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ConvertUTF8toUTF16 serverUsername(serverCUsername); + + nsTArray<RefPtr<nsILoginInfo>> logins; + rv = loginMgr->FindLogins(currServer, EmptyString(), currServer, logins); + NS_ENSURE_SUCCESS(rv, rv); + + // There should only be one-login stored for this url, however just in case + // there isn't. + nsString username; + for (uint32_t i = 0; i < logins.Length(); ++i) { + rv = logins[i]->GetUsername(username); + int32_t atPos = serverUsername.FindChar('@'); + if (NS_SUCCEEDED(rv) && + (username.Equals(serverUsername) || + StringHead(serverUsername, atPos).Equals(username))) { + // If this fails, just continue, we'll still want to remove the password + // from our local cache. + loginMgr->RemoveLogin(logins[i]); + } + } + + return SetPassword(EmptyString()); +} + +NS_IMETHODIMP +nsMsgIncomingServer::ForgetSessionPassword(bool modifyLogin) { + m_password.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetDefaultLocalPath(nsIFile* aDefaultLocalPath) { + nsresult rv; + nsCOMPtr<nsIMsgProtocolInfo> protocolInfo; + rv = GetProtocolInfo(getter_AddRefs(protocolInfo)); + NS_ENSURE_SUCCESS(rv, rv); + return protocolInfo->SetDefaultLocalPath(aDefaultLocalPath); +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetLocalPath(nsIFile** aLocalPath) { + nsresult rv; + + // if the local path has already been set, use it + rv = GetFileValue("directory-rel", "directory", aLocalPath); + if (NS_SUCCEEDED(rv) && *aLocalPath) return rv; + + // otherwise, create the path using the protocol info. + // note we are using the + // hostname, unless that directory exists. + // this should prevent all collisions. + nsCOMPtr<nsIMsgProtocolInfo> protocolInfo; + rv = GetProtocolInfo(getter_AddRefs(protocolInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> localPath; + rv = protocolInfo->GetDefaultLocalPath(getter_AddRefs(localPath)); + NS_ENSURE_SUCCESS(rv, rv); + rv = localPath->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) rv = NS_OK; + NS_ENSURE_SUCCESS(rv, rv); + + nsCString hostname; + rv = GetHostName(hostname); + NS_ENSURE_SUCCESS(rv, rv); + + // set the leaf name to "dummy", and then call MakeUnique with a suggested + // leaf name + rv = localPath->AppendNative(hostname); + NS_ENSURE_SUCCESS(rv, rv); + rv = localPath->CreateUnique(nsIFile::DIRECTORY_TYPE, 0755); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetLocalPath(localPath); + NS_ENSURE_SUCCESS(rv, rv); + + localPath.forget(aLocalPath); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetMsgStore(nsIMsgPluggableStore** aMsgStore) { + NS_ENSURE_ARG_POINTER(aMsgStore); + if (!m_msgStore) { + nsCString storeContractID; + nsresult rv; + // We don't want there to be a default pref, I think, since + // we can't change the default. We may want no pref to mean + // berkeley store, and then set the store pref off of some sort + // of default when creating a server. But we need to make sure + // that we do always write a store pref. + GetCharValue("storeContractID", storeContractID); + if (storeContractID.IsEmpty()) { + storeContractID.AssignLiteral("@mozilla.org/msgstore/berkeleystore;1"); + SetCharValue("storeContractID", storeContractID); + } + + // After someone starts using the pluggable store, we can no longer + // change the value. + SetBoolValue("canChangeStoreType", false); + + // Right now, we just have one pluggable store per server. If we want + // to support multiple, this pref could be a list of pluggable store + // contract id's. + m_msgStore = do_CreateInstance(storeContractID.get(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + NS_IF_ADDREF(*aMsgStore = m_msgStore); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetLocalPath(nsIFile* aLocalPath) { + NS_ENSURE_ARG_POINTER(aLocalPath); + nsresult rv = aLocalPath->Create(nsIFile::DIRECTORY_TYPE, 0755); + if (rv == NS_ERROR_FILE_ALREADY_EXISTS) rv = NS_OK; + NS_ENSURE_SUCCESS(rv, rv); + return SetFileValue("directory-rel", "directory", aLocalPath); +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetLocalStoreType(nsACString& aResult) { + MOZ_ASSERT_UNREACHABLE( + "nsMsgIncomingServer superclass not implementing GetLocalStoreType!"); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetLocalDatabaseType(nsACString& aResult) { + MOZ_ASSERT_UNREACHABLE( + "nsMsgIncomingServer superclass not implementing GetLocalDatabaseType!"); + return NS_ERROR_UNEXPECTED; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetAccountManagerChrome(nsAString& aResult) { + aResult.AssignLiteral("am-main.xhtml"); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::Equals(nsIMsgIncomingServer* server, bool* _retval) { + nsresult rv; + + NS_ENSURE_ARG_POINTER(server); + NS_ENSURE_ARG_POINTER(_retval); + + nsCString key1; + nsCString key2; + + rv = GetKey(key1); + NS_ENSURE_SUCCESS(rv, rv); + + rv = server->GetKey(key2); + NS_ENSURE_SUCCESS(rv, rv); + + // compare the server keys + *_retval = key1.Equals(key2, nsCaseInsensitiveCStringComparator); + + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::ClearAllValues() { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsTArray<nsCString> prefNames; + nsresult rv = mPrefBranch->GetChildList("", prefNames); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto& prefName : prefNames) { + mPrefBranch->ClearUserPref(prefName.get()); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::RemoveFiles() { + // IMPORTANT, see bug #77652 + // TODO: Decide what to do for deferred accounts. + nsCString deferredToAccount; + GetCharValue("deferred_to_account", deferredToAccount); + bool isDeferredTo = true; + GetIsDeferredTo(&isDeferredTo); + if (!deferredToAccount.IsEmpty() || isDeferredTo) { + NS_ASSERTION(false, "shouldn't remove files for a deferred account"); + return NS_ERROR_FAILURE; + } + nsCOMPtr<nsIFile> localPath; + nsresult rv = GetLocalPath(getter_AddRefs(localPath)); + NS_ENSURE_SUCCESS(rv, rv); + return localPath->Remove(true); +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetFilterList(nsIMsgFilterList* aFilterList) { + mFilterList = aFilterList; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetFilterList(nsIMsgWindow* aMsgWindow, + nsIMsgFilterList** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + if (!mFilterList) { + nsCOMPtr<nsIMsgFolder> msgFolder; + // use GetRootFolder so for deferred pop3 accounts, we'll get the filters + // file from the deferred account, not the deferred to account, + // so that filters will still be per-server. + nsresult rv = GetRootFolder(getter_AddRefs(msgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString filterType; + rv = GetCharValue("filter.type", filterType); + NS_ENSURE_SUCCESS(rv, rv); + + if (!filterType.IsEmpty() && !filterType.EqualsLiteral("default")) { + nsAutoCString contractID("@mozilla.org/filterlist;1?type="); + contractID += filterType; + ToLowerCase(contractID); + mFilterList = do_CreateInstance(contractID.get(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mFilterList->SetFolder(msgFolder); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ADDREF(*aResult = mFilterList); + return NS_OK; + } + + // The default case, a local folder, is a bit special. It requires + // more initialization. + + nsCOMPtr<nsIFile> thisFolder; + rv = msgFolder->GetFilePath(getter_AddRefs(thisFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + mFilterFile = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = mFilterFile->InitWithFile(thisFolder); + NS_ENSURE_SUCCESS(rv, rv); + + mFilterFile->AppendNative("msgFilterRules.dat"_ns); + + bool fileExists; + mFilterFile->Exists(&fileExists); + if (!fileExists) { + nsCOMPtr<nsIFile> oldFilterFile = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = oldFilterFile->InitWithFile(thisFolder); + NS_ENSURE_SUCCESS(rv, rv); + oldFilterFile->AppendNative("rules.dat"_ns); + + oldFilterFile->Exists(&fileExists); + if (fileExists) // copy rules.dat --> msgFilterRules.dat + { + rv = oldFilterFile->CopyToNative(thisFolder, "msgFilterRules.dat"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + } + nsCOMPtr<nsIMsgFilterService> filterService = + do_GetService("@mozilla.org/messenger/services/filters;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = filterService->OpenFilterList(mFilterFile, msgFolder, aMsgWindow, + getter_AddRefs(mFilterList)); + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_IF_ADDREF(*aResult = mFilterList); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetEditableFilterList( + nsIMsgFilterList* aEditableFilterList) { + mEditableFilterList = aEditableFilterList; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetEditableFilterList(nsIMsgWindow* aMsgWindow, + nsIMsgFilterList** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + if (!mEditableFilterList) { + bool editSeparate; + nsresult rv = GetBoolValue("filter.editable.separate", &editSeparate); + if (NS_FAILED(rv) || !editSeparate) + return GetFilterList(aMsgWindow, aResult); + + nsCString filterType; + rv = GetCharValue("filter.editable.type", filterType); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString contractID("@mozilla.org/filterlist;1?type="); + contractID += filterType; + ToLowerCase(contractID); + mEditableFilterList = do_CreateInstance(contractID.get(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> msgFolder; + // use GetRootFolder so for deferred pop3 accounts, we'll get the filters + // file from the deferred account, not the deferred to account, + // so that filters will still be per-server. + rv = GetRootFolder(getter_AddRefs(msgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mEditableFilterList->SetFolder(msgFolder); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ADDREF(*aResult = mEditableFilterList); + return NS_OK; + } + + NS_IF_ADDREF(*aResult = mEditableFilterList); + return NS_OK; +} + +// If the hostname contains ':' (like hostname:1431) +// then parse and set the port number. +nsresult nsMsgIncomingServer::InternalSetHostName(const nsACString& aHostname, + const char* prefName) { + nsCString hostname; + hostname = aHostname; + if (hostname.CountChar(':') == 1) { + int32_t colonPos = hostname.FindChar(':'); + nsAutoCString portString(Substring(hostname, colonPos)); + hostname.SetLength(colonPos); + nsresult err; + int32_t port = portString.ToInteger(&err); + if (NS_SUCCEEDED(err)) SetPort(port); + } + return SetCharValue(prefName, hostname); +} + +NS_IMETHODIMP +nsMsgIncomingServer::OnUserOrHostNameChanged(const nsACString& oldName, + const nsACString& newName, + bool hostnameChanged) { + nsresult rv; + + // 1. Reset password so that users are prompted for new password for the new + // user/host. + int32_t atPos = newName.FindChar('@'); + if (hostnameChanged) { + ForgetPassword(); + } + + // 2. Replace all occurrences of old name in the acct name with the new one. + nsString acctName; + rv = GetPrettyName(acctName); + NS_ENSURE_SUCCESS(rv, rv); + + // 3. Clear the clientid because the user or host have changed. + SetClientid(EmptyCString()); + + // Will be generated again when used. + mPrefBranch->ClearUserPref("spamActionTargetAccount"); + + // If new username contains @ then better do not update the account name. + if (acctName.IsEmpty() || (!hostnameChanged && (atPos != kNotFound))) + return NS_OK; + + atPos = acctName.FindChar('@'); + + // get previous username and hostname + nsCString userName, hostName; + if (hostnameChanged) { + rv = GetUsername(userName); + NS_ENSURE_SUCCESS(rv, rv); + hostName.Assign(oldName); + } else { + userName.Assign(oldName); + rv = GetHostName(hostName); + NS_ENSURE_SUCCESS(rv, rv); + } + + // switch corresponding part of the account name to the new name... + if (!hostnameChanged && (atPos != kNotFound)) { + // ...if username changed and the previous username was equal to the part + // of the account name before @ + if (StringHead(acctName, atPos).Equals(NS_ConvertASCIItoUTF16(userName))) + acctName.Replace(0, userName.Length(), NS_ConvertASCIItoUTF16(newName)); + } + if (hostnameChanged) { + // ...if hostname changed and the previous hostname was equal to the part + // of the account name after @, or to the whole account name + if (atPos == kNotFound) + atPos = 0; + else + atPos += 1; + if (Substring(acctName, atPos).Equals(NS_ConvertASCIItoUTF16(hostName))) { + acctName.Replace(atPos, acctName.Length() - atPos, + NS_ConvertASCIItoUTF16(newName)); + } + } + + return SetPrettyName(acctName); +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetHostName(const nsACString& aHostname) { + nsCString oldName; + nsresult rv = GetHostName(oldName); + NS_ENSURE_SUCCESS(rv, rv); + rv = InternalSetHostName(aHostname, "hostname"); + + if (!oldName.IsEmpty() && + !aHostname.Equals(oldName, nsCaseInsensitiveCStringComparator)) + rv = OnUserOrHostNameChanged(oldName, aHostname, true); + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetHostName(nsACString& aResult) { + nsresult rv = GetCharValue("hostname", aResult); + if (aResult.CountChar(':') == 1) { + // gack, we need to reformat the hostname - SetHostName will do that + SetHostName(aResult); + rv = GetCharValue("hostname", aResult); + } + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetUsername(const nsACString& aUsername) { + nsCString oldName; + nsresult rv = GetUsername(oldName); + NS_ENSURE_SUCCESS(rv, rv); + + if (!oldName.IsEmpty() && !oldName.Equals(aUsername)) { + // If only username changed and the new name just added a domain we can keep + // the password. + int32_t atPos = aUsername.FindChar('@'); + if ((atPos == kNotFound) || + !StringHead(NS_ConvertASCIItoUTF16(aUsername), atPos) + .Equals(NS_ConvertASCIItoUTF16(oldName))) { + ForgetPassword(); + } + rv = SetCharValue("userName", aUsername); + NS_ENSURE_SUCCESS(rv, rv); + rv = OnUserOrHostNameChanged(oldName, aUsername, false); + } else { + rv = SetCharValue("userName", aUsername); + } + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetUsername(nsACString& aResult) { + return GetCharValue("userName", aResult); +} + +#define BIFF_PREF_NAME "check_new_mail" + +NS_IMETHODIMP +nsMsgIncomingServer::GetDoBiff(bool* aDoBiff) { + NS_ENSURE_ARG_POINTER(aDoBiff); + + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv; + + rv = mPrefBranch->GetBoolPref(BIFF_PREF_NAME, aDoBiff); + if (NS_SUCCEEDED(rv)) return rv; + + // if the pref isn't set, use the default + // value based on the protocol + nsCOMPtr<nsIMsgProtocolInfo> protocolInfo; + rv = GetProtocolInfo(getter_AddRefs(protocolInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = protocolInfo->GetDefaultDoBiff(aDoBiff); + // note, don't call SetDoBiff() + // since we keep changing our minds on + // if biff should be on or off, let's keep the ability + // to change the default in future builds. + // if we call SetDoBiff() here, it will be in the users prefs. + // and we can't do anything after that. + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetDoBiff(bool aDoBiff) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + // Update biffManager immediately, no restart required. Adding/removing + // existing/non-existing server is handled without error checking. + nsresult rv; + nsCOMPtr<nsIMsgBiffManager> biffService = + do_GetService("@mozilla.org/messenger/biffManager;1", &rv); + if (NS_SUCCEEDED(rv) && biffService) { + if (aDoBiff) + (void)biffService->AddServerBiff(this); + else + (void)biffService->RemoveServerBiff(this); + } + + return mPrefBranch->SetBoolPref(BIFF_PREF_NAME, aDoBiff); +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetPort(int32_t* aPort) { + NS_ENSURE_ARG_POINTER(aPort); + + nsresult rv; + rv = GetIntValue("port", aPort); + // We can't use a port of 0, because the URI parsing code fails. + if (*aPort != PORT_NOT_SET && *aPort) return rv; + + // if the port isn't set, use the default + // port based on the protocol + nsCOMPtr<nsIMsgProtocolInfo> protocolInfo; + rv = GetProtocolInfo(getter_AddRefs(protocolInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t socketType; + rv = GetSocketType(&socketType); + NS_ENSURE_SUCCESS(rv, rv); + bool useSSLPort = (socketType == nsMsgSocketType::SSL); + return protocolInfo->GetDefaultServerPort(useSSLPort, aPort); +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetPort(int32_t aPort) { + nsresult rv; + + nsCOMPtr<nsIMsgProtocolInfo> protocolInfo; + rv = GetProtocolInfo(getter_AddRefs(protocolInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t socketType; + rv = GetSocketType(&socketType); + NS_ENSURE_SUCCESS(rv, rv); + bool useSSLPort = (socketType == nsMsgSocketType::SSL); + + int32_t defaultPort; + protocolInfo->GetDefaultServerPort(useSSLPort, &defaultPort); + return SetIntValue("port", aPort == defaultPort ? PORT_NOT_SET : aPort); +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetProtocolInfo(nsIMsgProtocolInfo** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + nsCString type; + nsresult rv = GetType(type); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString contractid(NS_MSGPROTOCOLINFO_CONTRACTID_PREFIX); + contractid.Append(type); + + nsCOMPtr<nsIMsgProtocolInfo> protocolInfo = + do_GetService(contractid.get(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + + protocolInfo.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP nsMsgIncomingServer::GetRetentionSettings( + nsIMsgRetentionSettings** settings) { + NS_ENSURE_ARG_POINTER(settings); + nsMsgRetainByPreference retainByPreference; + int32_t daysToKeepHdrs = 0; + int32_t numHeadersToKeep = 0; + int32_t daysToKeepBodies = 0; + bool cleanupBodiesByDays = false; + bool applyToFlaggedMessages = false; + nsresult rv = NS_OK; + // Create an empty retention settings object, + // get the settings from the server prefs, and init the object from the prefs. + nsCOMPtr<nsIMsgRetentionSettings> retentionSettings = + do_CreateInstance("@mozilla.org/msgDatabase/retentionSettings;1"); + if (retentionSettings) { + rv = GetIntValue("retainBy", (int32_t*)&retainByPreference); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetIntValue("numHdrsToKeep", &numHeadersToKeep); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetIntValue("daysToKeepHdrs", &daysToKeepHdrs); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetIntValue("daysToKeepBodies", &daysToKeepBodies); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetBoolValue("cleanupBodies", &cleanupBodiesByDays); + NS_ENSURE_SUCCESS(rv, rv); + rv = GetBoolValue("applyToFlaggedMessages", &applyToFlaggedMessages); + NS_ENSURE_SUCCESS(rv, rv); + retentionSettings->SetRetainByPreference(retainByPreference); + retentionSettings->SetNumHeadersToKeep((uint32_t)numHeadersToKeep); + retentionSettings->SetDaysToKeepBodies(daysToKeepBodies); + retentionSettings->SetDaysToKeepHdrs(daysToKeepHdrs); + retentionSettings->SetCleanupBodiesByDays(cleanupBodiesByDays); + retentionSettings->SetApplyToFlaggedMessages(applyToFlaggedMessages); + } else + rv = NS_ERROR_OUT_OF_MEMORY; + NS_IF_ADDREF(*settings = retentionSettings); + return rv; +} + +NS_IMETHODIMP nsMsgIncomingServer::SetRetentionSettings( + nsIMsgRetentionSettings* settings) { + nsMsgRetainByPreference retainByPreference; + uint32_t daysToKeepHdrs = 0; + uint32_t numHeadersToKeep = 0; + uint32_t daysToKeepBodies = 0; + bool cleanupBodiesByDays = false; + bool applyToFlaggedMessages = false; + settings->GetRetainByPreference(&retainByPreference); + settings->GetNumHeadersToKeep(&numHeadersToKeep); + settings->GetDaysToKeepBodies(&daysToKeepBodies); + settings->GetDaysToKeepHdrs(&daysToKeepHdrs); + settings->GetCleanupBodiesByDays(&cleanupBodiesByDays); + settings->GetApplyToFlaggedMessages(&applyToFlaggedMessages); + nsresult rv = SetIntValue("retainBy", retainByPreference); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetIntValue("numHdrsToKeep", numHeadersToKeep); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetIntValue("daysToKeepHdrs", daysToKeepHdrs); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetIntValue("daysToKeepBodies", daysToKeepBodies); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetBoolValue("cleanupBodies", cleanupBodiesByDays); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetBoolValue("applyToFlaggedMessages", applyToFlaggedMessages); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetDisplayStartupPage(bool* displayStartupPage) { + NS_ENSURE_ARG_POINTER(displayStartupPage); + *displayStartupPage = m_displayStartupPage; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetDisplayStartupPage(bool displayStartupPage) { + m_displayStartupPage = displayStartupPage; + return NS_OK; +} + +NS_IMETHODIMP nsMsgIncomingServer::GetDownloadSettings( + nsIMsgDownloadSettings** settings) { + NS_ENSURE_ARG_POINTER(settings); + bool downloadUnreadOnly = false; + bool downloadByDate = false; + uint32_t ageLimitOfMsgsToDownload = 0; + nsresult rv = NS_OK; + if (!m_downloadSettings) { + m_downloadSettings = + do_CreateInstance("@mozilla.org/msgDatabase/downloadSettings;1"); + if (m_downloadSettings) { + rv = GetBoolValue("downloadUnreadOnly", &downloadUnreadOnly); + rv = GetBoolValue("downloadByDate", &downloadByDate); + rv = GetIntValue("ageLimit", (int32_t*)&ageLimitOfMsgsToDownload); + m_downloadSettings->SetDownloadUnreadOnly(downloadUnreadOnly); + m_downloadSettings->SetDownloadByDate(downloadByDate); + m_downloadSettings->SetAgeLimitOfMsgsToDownload(ageLimitOfMsgsToDownload); + } else + rv = NS_ERROR_OUT_OF_MEMORY; + // Create an empty download settings object, + // get the settings from the server prefs, and init the object from the + // prefs. + } + NS_IF_ADDREF(*settings = m_downloadSettings); + return rv; +} + +NS_IMETHODIMP nsMsgIncomingServer::SetDownloadSettings( + nsIMsgDownloadSettings* settings) { + m_downloadSettings = settings; + bool downloadUnreadOnly = false; + bool downloadByDate = false; + uint32_t ageLimitOfMsgsToDownload = 0; + m_downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly); + m_downloadSettings->GetDownloadByDate(&downloadByDate); + m_downloadSettings->GetAgeLimitOfMsgsToDownload(&ageLimitOfMsgsToDownload); + nsresult rv = SetBoolValue("downloadUnreadOnly", downloadUnreadOnly); + NS_ENSURE_SUCCESS(rv, rv); + SetBoolValue("downloadByDate", downloadByDate); + return SetIntValue("ageLimit", ageLimitOfMsgsToDownload); +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetSupportsDiskSpace(bool* aSupportsDiskSpace) { + NS_ENSURE_ARG_POINTER(aSupportsDiskSpace); + *aSupportsDiskSpace = true; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetOfflineSupportLevel(int32_t* aSupportLevel) { + NS_ENSURE_ARG_POINTER(aSupportLevel); + + nsresult rv = GetIntValue("offline_support_level", aSupportLevel); + NS_ENSURE_SUCCESS(rv, rv); + + if (*aSupportLevel == OFFLINE_SUPPORT_LEVEL_UNDEFINED) + *aSupportLevel = OFFLINE_SUPPORT_LEVEL_NONE; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetOfflineSupportLevel(int32_t aSupportLevel) { + SetIntValue("offline_support_level", aSupportLevel); + return NS_OK; +} + +// Called only during the migration process. A unique name is generated for the +// migrated account. +NS_IMETHODIMP +nsMsgIncomingServer::GeneratePrettyNameForMigration(nsAString& aPrettyName) { + /** + * 4.x had provisions for multiple imap servers to be maintained under + * single identity. So, when migrated each of those server accounts need + * to be represented by unique account name. nsImapIncomingServer will + * override the implementation for this to do the right thing. + */ + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetFilterScope(nsMsgSearchScopeValue* filterScope) { + NS_ENSURE_ARG_POINTER(filterScope); + *filterScope = nsMsgSearchScope::offlineMailFilter; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetSearchScope(nsMsgSearchScopeValue* searchScope) { + NS_ENSURE_ARG_POINTER(searchScope); + *searchScope = nsMsgSearchScope::offlineMail; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetIsSecure(bool* aIsSecure) { + NS_ENSURE_ARG_POINTER(aIsSecure); + int32_t socketType; + nsresult rv = GetSocketType(&socketType); + NS_ENSURE_SUCCESS(rv, rv); + *aIsSecure = (socketType == nsMsgSocketType::alwaysSTARTTLS || + socketType == nsMsgSocketType::SSL); + return NS_OK; +} + +// use the convenience macros to implement the accessors +NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, AuthMethod, "authMethod") +NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, BiffMinutes, "check_time") +NS_IMPL_SERVERPREF_STR(nsMsgIncomingServer, Type, "type") +NS_IMPL_SERVERPREF_STR(nsMsgIncomingServer, Clientid, "clientid") +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, ClientidEnabled, "clientidEnabled") +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, DownloadOnBiff, "download_on_biff") +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, Valid, "valid") +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, EmptyTrashOnExit, + "empty_trash_on_exit") +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanDelete, "canDelete") +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, LoginAtStartUp, "login_at_startup") +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, + DefaultCopiesAndFoldersPrefsToServer, + "allows_specialfolders_usage") + +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanCreateFoldersOnServer, + "canCreateFolders") + +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, CanFileMessagesOnServer, + "canFileMessages") + +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, LimitOfflineMessageSize, + "limit_offline_message_size") + +NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, MaxMessageSize, "max_size") + +NS_IMPL_SERVERPREF_INT(nsMsgIncomingServer, IncomingDuplicateAction, + "dup_action") + +NS_IMPL_SERVERPREF_BOOL(nsMsgIncomingServer, Hidden, "hidden") + +NS_IMETHODIMP nsMsgIncomingServer::GetSocketType(int32_t* aSocketType) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + nsresult rv = mPrefBranch->GetIntPref("socketType", aSocketType); + + // socketType is set to default value. Look at isSecure setting + if (NS_FAILED(rv)) { + bool isSecure; + rv = mPrefBranch->GetBoolPref("isSecure", &isSecure); + if (NS_SUCCEEDED(rv) && isSecure) { + *aSocketType = nsMsgSocketType::SSL; + // don't call virtual method in case overrides call GetSocketType + nsMsgIncomingServer::SetSocketType(*aSocketType); + } else { + if (!mDefPrefBranch) return NS_ERROR_NOT_INITIALIZED; + rv = mDefPrefBranch->GetIntPref("socketType", aSocketType); + if (NS_FAILED(rv)) *aSocketType = nsMsgSocketType::plain; + } + } + return rv; +} + +NS_IMETHODIMP nsMsgIncomingServer::SetSocketType(int32_t aSocketType) { + if (!mPrefBranch) return NS_ERROR_NOT_INITIALIZED; + + int32_t socketType = nsMsgSocketType::plain; + mPrefBranch->GetIntPref("socketType", &socketType); + + nsresult rv = mPrefBranch->SetIntPref("socketType", aSocketType); + NS_ENSURE_SUCCESS(rv, rv); + + bool isSecureOld = (socketType == nsMsgSocketType::alwaysSTARTTLS || + socketType == nsMsgSocketType::SSL); + bool isSecureNew = (aSocketType == nsMsgSocketType::alwaysSTARTTLS || + aSocketType == nsMsgSocketType::SSL); + if ((isSecureOld != isSecureNew) && m_rootFolder) { + m_rootFolder->NotifyBoolPropertyChanged(kIsSecure, isSecureOld, + isSecureNew); + } + return NS_OK; +} + +// Check if the password is available and return a boolean indicating whether +// it is being authenticated or not. +NS_IMETHODIMP +nsMsgIncomingServer::GetPasswordPromptRequired(bool* aPasswordIsRequired) { + NS_ENSURE_ARG_POINTER(aPasswordIsRequired); + *aPasswordIsRequired = true; + + // If the password is not even required for biff we don't need to check any + // further + nsresult rv = GetServerRequiresPasswordForBiff(aPasswordIsRequired); + NS_ENSURE_SUCCESS(rv, rv); + if (!*aPasswordIsRequired) return NS_OK; + + // If the password is empty, check to see if it is stored and to be retrieved + if (m_password.IsEmpty()) (void)GetPasswordWithoutUI(); + + *aPasswordIsRequired = m_password.IsEmpty(); + if (*aPasswordIsRequired) { + // Set *aPasswordIsRequired false if authMethod is oauth2. + int32_t authMethod = 0; + rv = GetAuthMethod(&authMethod); + if (NS_SUCCEEDED(rv) && authMethod == nsMsgAuthMethod::OAuth2) { + *aPasswordIsRequired = false; + } + } + return rv; +} + +NS_IMETHODIMP nsMsgIncomingServer::ConfigureTemporaryFilters( + nsIMsgFilterList* aFilterList) { + nsresult rv = ConfigureTemporaryReturnReceiptsFilter(aFilterList); + if (NS_FAILED(rv)) // shut up warnings... + return rv; + return ConfigureTemporaryServerSpamFilters(aFilterList); +} + +nsresult nsMsgIncomingServer::ConfigureTemporaryServerSpamFilters( + nsIMsgFilterList* filterList) { + nsCOMPtr<nsISpamSettings> spamSettings; + nsresult rv = GetSpamSettings(getter_AddRefs(spamSettings)); + NS_ENSURE_SUCCESS(rv, rv); + + bool useServerFilter; + rv = spamSettings->GetUseServerFilter(&useServerFilter); + NS_ENSURE_SUCCESS(rv, rv); + + // if we aren't configured to use server filters, then return early. + if (!useServerFilter) return NS_OK; + + // For performance reasons, we'll handle clearing of filters if the user turns + // off the server-side filters from the junk mail controls, in the junk mail + // controls. + nsAutoCString serverFilterName; + spamSettings->GetServerFilterName(serverFilterName); + if (serverFilterName.IsEmpty()) return NS_OK; + int32_t serverFilterTrustFlags = 0; + (void)spamSettings->GetServerFilterTrustFlags(&serverFilterTrustFlags); + if (!serverFilterTrustFlags) return NS_OK; + // check if filters have been setup already. + nsAutoString yesFilterName, noFilterName; + CopyASCIItoUTF16(serverFilterName, yesFilterName); + yesFilterName.AppendLiteral("Yes"); + + CopyASCIItoUTF16(serverFilterName, noFilterName); + noFilterName.AppendLiteral("No"); + + nsCOMPtr<nsIMsgFilter> newFilter; + (void)filterList->GetFilterNamed(yesFilterName, getter_AddRefs(newFilter)); + + if (!newFilter) + (void)filterList->GetFilterNamed(noFilterName, getter_AddRefs(newFilter)); + if (newFilter) return NS_OK; + + nsCOMPtr<nsIFile> file; + spamSettings->GetServerFilterFile(getter_AddRefs(file)); + + // it's possible that we can no longer find the sfd file (i.e. the user + // disabled an extnsion that was supplying the .sfd file. + if (!file) return NS_OK; + + nsCOMPtr<nsIMsgFilterService> filterService = + do_GetService("@mozilla.org/messenger/services/filters;1", &rv); + nsCOMPtr<nsIMsgFilterList> serverFilterList; + + rv = filterService->OpenFilterList(file, NULL, NULL, + getter_AddRefs(serverFilterList)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = serverFilterList->GetFilterNamed(yesFilterName, + getter_AddRefs(newFilter)); + if (newFilter && serverFilterTrustFlags & nsISpamSettings::TRUST_POSITIVES) { + newFilter->SetTemporary(true); + // check if we're supposed to move junk mail to junk folder; if so, + // add filter action to do so. + + /* + * We don't want this filter to activate on messages that have + * been marked by the user as not spam. This occurs when messages that + * were marked as good are moved back into the inbox. But to + * do this with a filter, we have to add a boolean term. That requires + * that we rewrite the existing filter search terms to group them. + */ + + // get the list of search terms from the filter + nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms; + rv = newFilter->GetSearchTerms(searchTerms); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t count = searchTerms.Length(); + if (count > 1) // don't need to group a single term + { + // beginGrouping the first term, and endGrouping the last term + searchTerms[0]->SetBeginsGrouping(true); + searchTerms[count - 1]->SetEndsGrouping(true); + } + + // Create a new term, checking if the user set junk status. The term will + // search for junkscoreorigin != "user" + nsCOMPtr<nsIMsgSearchTerm> searchTerm; + rv = newFilter->CreateTerm(getter_AddRefs(searchTerm)); + NS_ENSURE_SUCCESS(rv, rv); + + searchTerm->SetAttrib(nsMsgSearchAttrib::JunkScoreOrigin); + searchTerm->SetOp(nsMsgSearchOp::Isnt); + searchTerm->SetBooleanAnd(true); + + nsCOMPtr<nsIMsgSearchValue> searchValue; + searchTerm->GetValue(getter_AddRefs(searchValue)); + NS_ENSURE_SUCCESS(rv, rv); + searchValue->SetAttrib(nsMsgSearchAttrib::JunkScoreOrigin); + searchValue->SetStr(u"user"_ns); + searchTerm->SetValue(searchValue); + + newFilter->AppendTerm(searchTerm); + + bool moveOnSpam, markAsReadOnSpam; + spamSettings->GetMoveOnSpam(&moveOnSpam); + if (moveOnSpam) { + nsCString spamFolderURI; + rv = spamSettings->GetSpamFolderURI(spamFolderURI); + if (NS_SUCCEEDED(rv) && (!spamFolderURI.IsEmpty())) { + nsCOMPtr<nsIMsgRuleAction> moveAction; + rv = newFilter->CreateAction(getter_AddRefs(moveAction)); + if (NS_SUCCEEDED(rv)) { + moveAction->SetType(nsMsgFilterAction::MoveToFolder); + moveAction->SetTargetFolderUri(spamFolderURI); + newFilter->AppendAction(moveAction); + } + } + } + spamSettings->GetMarkAsReadOnSpam(&markAsReadOnSpam); + if (markAsReadOnSpam) { + nsCOMPtr<nsIMsgRuleAction> markAsReadAction; + rv = newFilter->CreateAction(getter_AddRefs(markAsReadAction)); + if (NS_SUCCEEDED(rv)) { + markAsReadAction->SetType(nsMsgFilterAction::MarkRead); + newFilter->AppendAction(markAsReadAction); + } + } + filterList->InsertFilterAt(0, newFilter); + } + + rv = + serverFilterList->GetFilterNamed(noFilterName, getter_AddRefs(newFilter)); + if (newFilter && serverFilterTrustFlags & nsISpamSettings::TRUST_NEGATIVES) { + newFilter->SetTemporary(true); + filterList->InsertFilterAt(0, newFilter); + } + + return rv; +} + +nsresult nsMsgIncomingServer::ConfigureTemporaryReturnReceiptsFilter( + nsIMsgFilterList* filterList) { + nsresult rv; + + nsCOMPtr<nsIMsgAccountManager> accountMgr = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgIdentity> identity; + rv = accountMgr->GetFirstIdentityForServer(this, getter_AddRefs(identity)); + NS_ENSURE_SUCCESS(rv, rv); + // this can return success and a null identity... + + bool useCustomPrefs = false; + int32_t incorp = nsIMsgMdnGenerator::eIncorporateInbox; + NS_ENSURE_TRUE(identity, NS_ERROR_NULL_POINTER); + + identity->GetBoolAttribute("use_custom_prefs", &useCustomPrefs); + if (useCustomPrefs) + rv = GetIntValue("incorporate_return_receipt", &incorp); + else { + nsCOMPtr<nsIPrefBranch> prefs(do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefs) prefs->GetIntPref("mail.incorporate.return_receipt", &incorp); + } + + bool enable = (incorp == nsIMsgMdnGenerator::eIncorporateSent); + + // this is a temporary, internal mozilla filter + // it will not show up in the UI, it will not be written to disk + constexpr auto internalReturnReceiptFilterName = + u"mozilla-temporary-internal-MDN-receipt-filter"_ns; + + nsCOMPtr<nsIMsgFilter> newFilter; + rv = filterList->GetFilterNamed(internalReturnReceiptFilterName, + getter_AddRefs(newFilter)); + if (newFilter) + newFilter->SetEnabled(enable); + else if (enable) { + nsCString actionTargetFolderUri; + rv = identity->GetFccFolder(actionTargetFolderUri); + if (!actionTargetFolderUri.IsEmpty()) { + filterList->CreateFilter(internalReturnReceiptFilterName, + getter_AddRefs(newFilter)); + if (newFilter) { + newFilter->SetEnabled(true); + // this internal filter is temporary + // and should not show up in the UI or be written to disk + newFilter->SetTemporary(true); + + nsCOMPtr<nsIMsgSearchTerm> term; + nsCOMPtr<nsIMsgSearchValue> value; + + rv = newFilter->CreateTerm(getter_AddRefs(term)); + if (NS_SUCCEEDED(rv)) { + rv = term->GetValue(getter_AddRefs(value)); + if (NS_SUCCEEDED(rv)) { + // we need to use OtherHeader + 1 so nsMsgFilter::GetTerm will + // return our custom header. + value->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1); + value->SetStr(u"multipart/report"_ns); + term->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1); + term->SetOp(nsMsgSearchOp::Contains); + term->SetBooleanAnd(true); + term->SetArbitraryHeader("Content-Type"_ns); + term->SetValue(value); + newFilter->AppendTerm(term); + } + } + rv = newFilter->CreateTerm(getter_AddRefs(term)); + if (NS_SUCCEEDED(rv)) { + rv = term->GetValue(getter_AddRefs(value)); + if (NS_SUCCEEDED(rv)) { + // XXX todo + // determine if ::OtherHeader is the best way to do this. + // see nsMsgSearchOfflineMail::MatchTerms() + value->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1); + value->SetStr(u"disposition-notification"_ns); + term->SetAttrib(nsMsgSearchAttrib::OtherHeader + 1); + term->SetOp(nsMsgSearchOp::Contains); + term->SetBooleanAnd(true); + term->SetArbitraryHeader("Content-Type"_ns); + term->SetValue(value); + newFilter->AppendTerm(term); + } + } + nsCOMPtr<nsIMsgRuleAction> filterAction; + rv = newFilter->CreateAction(getter_AddRefs(filterAction)); + if (NS_SUCCEEDED(rv)) { + filterAction->SetType(nsMsgFilterAction::MoveToFolder); + filterAction->SetTargetFolderUri(actionTargetFolderUri); + newFilter->AppendAction(filterAction); + filterList->InsertFilterAt(0, newFilter); + } + } + } + } + return rv; +} + +NS_IMETHODIMP +nsMsgIncomingServer::ClearTemporaryReturnReceiptsFilter() { + if (mFilterList) { + nsCOMPtr<nsIMsgFilter> mdnFilter; + nsresult rv = mFilterList->GetFilterNamed( + u"mozilla-temporary-internal-MDN-receipt-filter"_ns, + getter_AddRefs(mdnFilter)); + if (NS_SUCCEEDED(rv) && mdnFilter) + return mFilterList->RemoveFilter(mdnFilter); + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetMsgFolderFromURI(nsIMsgFolder* aFolderResource, + const nsACString& aURI, + nsIMsgFolder** aFolder) { + nsCOMPtr<nsIMsgFolder> rootMsgFolder; + nsresult rv = GetRootMsgFolder(getter_AddRefs(rootMsgFolder)); + NS_ENSURE_TRUE(rootMsgFolder, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIMsgFolder> msgFolder; + rv = rootMsgFolder->GetChildWithURI(aURI, true, true /*caseInsensitive*/, + getter_AddRefs(msgFolder)); + if (NS_FAILED(rv) || !msgFolder) msgFolder = aFolderResource; + NS_IF_ADDREF(*aFolder = msgFolder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetSpamSettings(nsISpamSettings** aSpamSettings) { + NS_ENSURE_ARG_POINTER(aSpamSettings); + + nsAutoCString spamActionTargetAccount; + GetCharValue("spamActionTargetAccount", spamActionTargetAccount); + if (spamActionTargetAccount.IsEmpty()) { + GetServerURI(spamActionTargetAccount); + SetCharValue("spamActionTargetAccount", spamActionTargetAccount); + } + + if (!mSpamSettings) { + nsresult rv; + mSpamSettings = + do_CreateInstance("@mozilla.org/messenger/spamsettings;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + mSpamSettings->Initialize(this); + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_ADDREF(*aSpamSettings = mSpamSettings); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetSpamFilterPlugin(nsIMsgFilterPlugin** aFilterPlugin) { + NS_ENSURE_ARG_POINTER(aFilterPlugin); + if (!mFilterPlugin) { + nsresult rv; + mFilterPlugin = do_GetService( + "@mozilla.org/messenger/filter-plugin;1?name=bayesianfilter", &rv); + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_IF_ADDREF(*aFilterPlugin = mFilterPlugin); + return NS_OK; +} + +// get all the servers that defer to the account for the passed in server. Note +// that destServer may not be "this" +nsresult nsMsgIncomingServer::GetDeferredServers( + nsIMsgIncomingServer* destServer, + nsTArray<RefPtr<nsIPop3IncomingServer>>& aServers) { + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgAccount> thisAccount; + accountManager->FindAccountForServer(destServer, getter_AddRefs(thisAccount)); + if (thisAccount) { + nsCString accountKey; + thisAccount->GetKey(accountKey); + nsTArray<RefPtr<nsIMsgIncomingServer>> allServers; + accountManager->GetAllServers(allServers); + for (auto server : allServers) { + nsCOMPtr<nsIPop3IncomingServer> popServer(do_QueryInterface(server)); + if (popServer) { + nsCString deferredToAccount; + popServer->GetDeferredToAccount(deferredToAccount); + if (deferredToAccount.Equals(accountKey)) + aServers.AppendElement(popServer); + } + } + } + return rv; +} + +NS_IMETHODIMP nsMsgIncomingServer::GetIsDeferredTo(bool* aIsDeferredTo) { + NS_ENSURE_ARG_POINTER(aIsDeferredTo); + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1"); + if (accountManager) { + nsCOMPtr<nsIMsgAccount> thisAccount; + accountManager->FindAccountForServer(this, getter_AddRefs(thisAccount)); + if (thisAccount) { + nsCString accountKey; + thisAccount->GetKey(accountKey); + nsTArray<RefPtr<nsIMsgIncomingServer>> allServers; + accountManager->GetAllServers(allServers); + for (auto server : allServers) { + if (server) { + nsCString deferredToAccount; + server->GetCharValue("deferred_to_account", deferredToAccount); + if (deferredToAccount.Equals(accountKey)) { + *aIsDeferredTo = true; + return NS_OK; + } + } + } + } + } + *aIsDeferredTo = false; + return NS_OK; +} + +const long kMaxDownloadTableSize = 500; + +// hash the concatenation of the message-id and subject as the hash table key, +// and store the arrival index as the value. To limit the size of the hash +// table, we just throw out ones with a lower ordinal value than the cut-off +// point. +NS_IMETHODIMP nsMsgIncomingServer::IsNewHdrDuplicate(nsIMsgDBHdr* aNewHdr, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + NS_ENSURE_ARG_POINTER(aNewHdr); + *aResult = false; + + // If the message has been partially downloaded, the message should not + // be considered a duplicated message. See bug 714090. + uint32_t flags; + aNewHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::Partial) return NS_OK; + + nsAutoCString strHashKey; + nsCString messageId, subject; + aNewHdr->GetMessageId(getter_Copies(messageId)); + strHashKey.Append(messageId); + aNewHdr->GetSubject(subject); + // err on the side of caution and ignore messages w/o subject or messageid. + if (subject.IsEmpty() || messageId.IsEmpty()) return NS_OK; + strHashKey.Append(subject); + int32_t hashValue = m_downloadedHdrs.Get(strHashKey); + if (hashValue) + *aResult = true; + else { + // we store the current size of the hash table as the hash + // value - this allows us to delete older entries. + m_downloadedHdrs.InsertOrUpdate(strHashKey, ++m_numMsgsDownloaded); + // Check if hash table is larger than some reasonable size + // and if is it, iterate over hash table deleting messages + // with an arrival index < number of msgs downloaded - half the reasonable + // size. + if (m_downloadedHdrs.Count() >= kMaxDownloadTableSize) { + for (auto iter = m_downloadedHdrs.Iter(); !iter.Done(); iter.Next()) { + if (iter.Data() < m_numMsgsDownloaded - kMaxDownloadTableSize / 2) { + iter.Remove(); + } else if (m_downloadedHdrs.Count() <= kMaxDownloadTableSize / 2) { + break; + } + } + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetForcePropertyEmpty(const char* aPropertyName, + bool* _retval) { + NS_ENSURE_ARG_POINTER(_retval); + nsAutoCString nameEmpty(aPropertyName); + nameEmpty.AppendLiteral(".empty"); + nsCString value; + GetCharValue(nameEmpty.get(), value); + *_retval = value.EqualsLiteral("true"); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgIncomingServer::SetForcePropertyEmpty(const char* aPropertyName, + bool aValue) { + nsAutoCString nameEmpty(aPropertyName); + nameEmpty.AppendLiteral(".empty"); + return SetCharValue(nameEmpty.get(), aValue ? "true"_ns : ""_ns); +} + +NS_IMETHODIMP +nsMsgIncomingServer::GetSortOrder(int32_t* aSortOrder) { + NS_ENSURE_ARG_POINTER(aSortOrder); + *aSortOrder = 100000000; + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgIncomingServer.h b/comm/mailnews/base/src/nsMsgIncomingServer.h new file mode 100644 index 0000000000..c75c1f07c9 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgIncomingServer.h @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgIncomingServer_h__ +#define nsMsgIncomingServer_h__ + +#include "nsIMsgIncomingServer.h" +#include "nsIPrefBranch.h" +#include "nsIMsgFilterList.h" +#include "msgCore.h" +#include "nsIMsgFolder.h" +#include "nsIFile.h" +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "nsIPop3IncomingServer.h" +#include "nsWeakReference.h" +#include "nsIMsgDatabase.h" +#include "nsISpamSettings.h" +#include "nsIMsgFilterPlugin.h" +#include "nsTHashMap.h" +#include "nsIMsgPluggableStore.h" +#include "nsIObserver.h" + +class nsIMsgFolderCache; +class nsIMsgProtocolInfo; + +/* + * base class for nsIMsgIncomingServer - derive your class from here + * if you want to get some free implementation + * + * this particular implementation is not meant to be used directly. + */ + +class nsMsgIncomingServer : public nsIMsgIncomingServer, + public nsSupportsWeakReference, + public nsIObserver { + public: + nsMsgIncomingServer(); + nsresult Init(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMSGINCOMINGSERVER + NS_DECL_NSIOBSERVER + + protected: + virtual ~nsMsgIncomingServer(); + nsCString m_serverKey; + + // Sets m_password, if password found. Can return NS_ERROR_ABORT if the + // user cancels the master password dialog. + nsresult GetPasswordWithoutUI(); + + nsresult ConfigureTemporaryReturnReceiptsFilter(nsIMsgFilterList* filterList); + nsresult ConfigureTemporaryServerSpamFilters(nsIMsgFilterList* filterList); + + nsCOMPtr<nsIMsgFolder> m_rootFolder; + nsCOMPtr<nsIMsgDownloadSettings> m_downloadSettings; + + // For local servers, where we put messages. For imap/pop3, where we store + // offline messages. + nsCOMPtr<nsIMsgPluggableStore> m_msgStore; + + /// Helper routine to create local folder on disk if it doesn't exist + /// under the account's rootFolder. + nsresult CreateLocalFolder(const nsAString& folderName); + + static nsresult GetDeferredServers( + nsIMsgIncomingServer* destServer, + nsTArray<RefPtr<nsIPop3IncomingServer>>& aServers); + + nsresult CreateRootFolder(); + virtual nsresult CreateRootFolderFromUri(const nsACString& serverUri, + nsIMsgFolder** rootFolder) = 0; + + nsresult InternalSetHostName(const nsACString& aHostname, + const char* prefName); + + nsCOMPtr<nsIFile> mFilterFile; + nsCOMPtr<nsIMsgFilterList> mFilterList; + nsCOMPtr<nsIMsgFilterList> mEditableFilterList; + nsCOMPtr<nsIPrefBranch> mPrefBranch; + nsCOMPtr<nsIPrefBranch> mDefPrefBranch; + + // these allow us to handle duplicate incoming messages, e.g. delete them. + nsTHashMap<nsCStringHashKey, int32_t> m_downloadedHdrs; + int32_t m_numMsgsDownloaded; + + private: + uint32_t m_biffState; + bool m_serverBusy; + nsCOMPtr<nsISpamSettings> mSpamSettings; + nsCOMPtr<nsIMsgFilterPlugin> mFilterPlugin; // XXX should be a list + + protected: + nsString m_password; + bool m_canHaveFilters; + bool m_displayStartupPage; + bool mPerformingBiff; +}; + +#endif // nsMsgIncomingServer_h__ diff --git a/comm/mailnews/base/src/nsMsgKeySet.cpp b/comm/mailnews/base/src/nsMsgKeySet.cpp new file mode 100644 index 0000000000..7423e2560a --- /dev/null +++ b/comm/mailnews/base/src/nsMsgKeySet.cpp @@ -0,0 +1,1412 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // precompiled header... +#include "prlog.h" + +#include "MailNewsTypes.h" +#include "nsMsgKeySet.h" +#include "prprf.h" +#include "prmem.h" +#include "nsTArray.h" +#include "nsMemory.h" +#include <ctype.h> + +#if defined(DEBUG_seth_) || defined(DEBUG_sspitzer_) +# define DEBUG_MSGKEYSET 1 +#endif + +/* A compressed encoding for sets of article. This is usually for lines from + the newsrc, which have article lists like + + 1-29627,29635,29658,32861-32863 + + so the data has these properties: + + - strictly increasing + - large subsequences of monotonically increasing ranges + - gaps in the set are usually small, but not always + - consecutive ranges tend to be large + + The biggest win is to run-length encode the data, storing ranges as two + numbers (start+length or start,end). We could also store each number as a + delta from the previous number for further compression, but that gets kind + of tricky, since there are no guarantees about the sizes of the gaps, and + we'd have to store variable-length words. + + Current data format: + + DATA := SIZE [ CHUNK ]* + CHUNK := [ RANGE | VALUE ] + RANGE := -LENGTH START + START := VALUE + LENGTH := int32_t + VALUE := a literal positive integer, for now + it could also be an offset from the previous value. + LENGTH could also perhaps be a less-than-32-bit quantity, + at least most of the time. + + Lengths of CHUNKs are stored negative to distinguish the beginning of + a chunk from a literal: negative means two-word sequence, positive + means one-word sequence. + + 0 represents a literal 0, but should not occur, and should never occur + except in the first position. + + A length of -1 won't occur either, except temporarily - a sequence of + two elements is represented as two literals, since they take up the same + space. + + Another optimization we make is to notice that we typically ask the + question ``is N a member of the set'' for increasing values of N. So the + set holds a cache of the last value asked for, and can simply resume the + search from there. */ + +nsMsgKeySet::nsMsgKeySet(/* MSG_NewsHost* host*/) { + m_cached_value = -1; + m_cached_value_index = 0; + m_length = 0; + m_data_size = 10; + m_data = (int32_t*)PR_Malloc(sizeof(int32_t) * m_data_size); +#ifdef NEWSRC_DOES_HOST_STUFF + m_host = host; +#endif +} + +nsMsgKeySet::~nsMsgKeySet() { PR_FREEIF(m_data); } + +bool nsMsgKeySet::Grow() { + int32_t new_size; + int32_t* new_data; + new_size = m_data_size * 2; + new_data = (int32_t*)PR_REALLOC(m_data, sizeof(int32_t) * new_size); + if (!new_data) return false; + m_data_size = new_size; + m_data = new_data; + return true; +} + +nsMsgKeySet::nsMsgKeySet(const char* numbers /* , MSG_NewsHost* host */) { + int32_t *head, *tail, *end; + +#ifdef NEWSRC_DOES_HOST_STUFF + m_host = host; +#endif + m_cached_value = -1; + m_cached_value_index = 0; + m_length = 0; + m_data_size = 10; + m_data = (int32_t*)PR_Malloc(sizeof(int32_t) * m_data_size); + if (!m_data) return; + + head = m_data; + tail = head; + end = head + m_data_size; + + if (!numbers) { + return; + } + + while (isspace(*numbers)) numbers++; + while (*numbers) { + int32_t from = 0; + int32_t to; + + if (tail >= end - 4) { + /* out of room! */ + int32_t tailo = tail - head; + if (!Grow()) { + PR_FREEIF(m_data); + return; + } + /* data may have been relocated */ + head = m_data; + tail = head + tailo; + end = head + m_data_size; + } + + while (isspace(*numbers)) numbers++; + if (*numbers && !isdigit(*numbers)) { + break; /* illegal character */ + } + while (isdigit(*numbers)) { + from = (from * 10) + (*numbers++ - '0'); + } + while (isspace(*numbers)) numbers++; + if (*numbers != '-') { + to = from; + } else { + to = 0; + numbers++; + while (*numbers >= '0' && *numbers <= '9') + to = (to * 10) + (*numbers++ - '0'); + while (isspace(*numbers)) numbers++; + } + + if (to < from) to = from; /* illegal */ + + /* This is a hack - if the newsrc file specifies a range 1-x as + being read, we internally pretend that article 0 is read as well. + (But if only 2-x are read, then 0 is not read.) This is needed + because some servers think that article 0 is an article (I think) + but some news readers (including Netscape 1.1) choke if the .newsrc + file has lines beginning with 0... ### */ + if (from == 1) from = 0; + + if (to == from) { + /* Write it as a literal */ + *tail = from; + tail++; + } else /* Write it as a range. */ { + *tail = -(to - from); + tail++; + *tail = from; + tail++; + } + + while (*numbers == ',' || isspace(*numbers)) { + numbers++; + } + } + + m_length = tail - head; /* size of data */ +} + +nsMsgKeySet* nsMsgKeySet::Create(/*MSG_NewsHost* host*/) { + nsMsgKeySet* set = new nsMsgKeySet(/* host */); + if (set && set->m_data == NULL) { + delete set; + set = NULL; + } + return set; +} + +nsMsgKeySet* nsMsgKeySet::Create(const char* value /* , MSG_NewsHost* host */) { +#ifdef DEBUG_MSGKEYSET + printf("create from %s\n", value); +#endif + + nsMsgKeySet* set = new nsMsgKeySet(value /* , host */); + if (set && set->m_data == NULL) { + delete set; + set = NULL; + } + return set; +} + +/* Returns the lowest non-member of the set greater than 0. + */ +int32_t nsMsgKeySet::FirstNonMember() { + if (m_length <= 0) { + return 1; + } else if (m_data[0] < 0 && m_data[1] != 1 && m_data[1] != 0) { + /* first range not equal to 0 or 1, always return 1 */ + return 1; + } else if (m_data[0] < 0) { + /* it's a range */ + /* If there is a range [N-M] we can presume that M+1 is not in the + set. */ + return (m_data[1] - m_data[0] + 1); + } else { + /* it's a literal */ + if (m_data[0] == 1) { + /* handle "1,..." */ + if (m_length > 1 && m_data[1] == 2) { + /* This is "1,2,M-N,..." or "1,2,M,..." where M >= 4. Note + that M will never be 3, because in that case we would have + started with a range: "1-3,..." */ + return 3; + } else { + return 2; /* handle "1,M-N,.." or "1,M,..." + where M >= 3; */ + } + } else if (m_data[0] == 0) { + /* handle "0,..." */ + if (m_length > 1 && m_data[1] == 1) { + /* this is 0,1, (see above) */ + return 2; + } else { + return 1; + } + + } else { + /* handle "M,..." where M >= 2. */ + return 1; + } + } +} + +nsresult nsMsgKeySet::Output(char** outputStr) { + NS_ENSURE_ARG(outputStr); + int32_t size; + int32_t* head; + int32_t* tail; + int32_t* end; + int32_t s_size; + char* s_head; + char *s, *s_end; + int32_t last_art = -1; + + *outputStr = nullptr; + + size = m_length; + head = m_data; + tail = head; + end = head + size; + + s_size = (size * 12) + + 10; // dmb - try to make this allocation get used at least once. + s_head = (char*)moz_xmalloc(s_size); + if (!s_head) return NS_ERROR_OUT_OF_MEMORY; + + s_head[0] = '\0'; // otherwise, s_head will contain garbage. + s = s_head; + s_end = s + s_size; + + while (tail < end) { + int32_t from; + int32_t to; + + if (s > (s_end - (12 * 2 + 10))) { /* 12 bytes for each number (enough + for "2147483647" aka 2^31-1), + plus 10 bytes of slop. */ + int32_t so = s - s_head; + s_size += 200; + char* tmp = (char*)moz_xmalloc(s_size); + if (tmp) PL_strcpy(tmp, s_head); + free(s_head); + s_head = tmp; + if (!s_head) return NS_ERROR_OUT_OF_MEMORY; + s = s_head + so; + s_end = s_head + s_size; + } + + if (*tail < 0) { + /* it's a range */ + from = tail[1]; + to = from + (-(tail[0])); + tail += 2; + } else /* it's a literal */ + { + from = *tail; + to = from; + tail++; + } + if (from == 0) { + from = 1; /* See 'hack' comment above ### */ + } + if (from <= last_art) from = last_art + 1; + if (from <= to) { + if (from < to) { + PR_snprintf(s, s_end - s, "%lu-%lu,", from, to); + } else { + PR_snprintf(s, s_end - s, "%lu,", from); + } + s += PL_strlen(s); + last_art = to; + } + } + if (last_art >= 0) { + s--; /* Strip off the last ',' */ + } + + *s = 0; + + *outputStr = s_head; + return NS_OK; +} + +int32_t nsMsgKeySet::GetLastMember() { + if (m_length > 1) { + int32_t nextToLast = m_data[m_length - 2]; + if (nextToLast < 0) // is range at end? + { + int32_t last = m_data[m_length - 1]; + return (-nextToLast + last - 1); + } else // no, so last number must be last member + { + return m_data[m_length - 1]; + } + } else if (m_length == 1) + return m_data[0]; // must be only 1 read. + else + return 0; +} + +void nsMsgKeySet::SetLastMember(int32_t newHighWaterMark) { + if (newHighWaterMark < GetLastMember()) { + while (true) { + if (m_length > 1) { + int32_t nextToLast = m_data[m_length - 2]; + int32_t curHighWater; + if (nextToLast < 0) // is range at end? + { + int32_t rangeStart = m_data[m_length - 1]; + int32_t rangeLength = -nextToLast; + curHighWater = (rangeLength + rangeStart - 1); + if (curHighWater > newHighWaterMark) { + if (rangeStart > newHighWaterMark) { + m_length -= 2; // throw away whole range + } else if (rangeStart == newHighWaterMark) { + // turn range into single element. + m_data[m_length - 2] = newHighWaterMark; + m_length--; + break; + } else // just shorten range + { + m_data[m_length - 2] = -(newHighWaterMark - rangeStart); + break; + } + } else { + // prevent the infinite loop + // see bug #13062 + break; + } + } else if (m_data[m_length - 1] > + newHighWaterMark) // no, so last number must be last member + { + m_length--; + } else + break; + } else + break; + } + // well, the whole range is probably invalid, because the server probably + // re-ordered ids, but what can you do? +#ifdef NEWSRC_DOES_HOST_STUFF + if (m_host) m_host->MarkDirty(); +#endif + } +} + +int32_t nsMsgKeySet::GetFirstMember() { + if (m_length > 1) { + int32_t first = m_data[0]; + if (first < 0) // is range at start? + { + int32_t second = m_data[1]; + return (second); + } else // no, so first number must be first member + { + return m_data[0]; + } + } else if (m_length == 1) + return m_data[0]; // must be only 1 read. + else + return 0; +} + +/* Re-compresses a `nsMsgKeySet' object. + + The assumption is made that the `nsMsgKeySet' is syntactically correct + (all ranges have a length of at least 1, and all values are non- + decreasing) but will optimize the compression, for example, merging + consecutive literals or ranges into one range. + + Returns true if successful, false if there wasn't enough memory to + allocate scratch space. + + #### This should be changed to modify the buffer in place. + + Also note that we never call Optimize() unless we actually changed + something, so it's a great place to tell the MSG_NewsHost* that something + changed. + */ +bool nsMsgKeySet::Optimize() { + int32_t input_size; + int32_t output_size; + int32_t* input_tail; + int32_t* output_data; + int32_t* output_tail; + int32_t* input_end; + int32_t* output_end; + + input_size = m_length; + output_size = input_size + 1; + input_tail = m_data; + output_data = (int32_t*)PR_Malloc(sizeof(int32_t) * output_size); + if (!output_data) return false; + + output_tail = output_data; + input_end = input_tail + input_size; + output_end = output_data + output_size; + + /* We're going to modify the set, so invalidate the cache. */ + m_cached_value = -1; + + while (input_tail < input_end) { + int32_t from, to; + bool range_p = (*input_tail < 0); + + if (range_p) { + /* it's a range */ + from = input_tail[1]; + to = from + (-(input_tail[0])); + + /* Copy it over */ + *output_tail++ = *input_tail++; + *output_tail++ = *input_tail++; + } else { + /* it's a literal */ + from = *input_tail; + to = from; + + /* Copy it over */ + *output_tail++ = *input_tail++; + } + NS_ASSERTION(output_tail < output_end, "invalid end of output string"); + if (output_tail >= output_end) { + PR_Free(output_data); + return false; + } + + /* As long as this chunk is followed by consecutive chunks, + keep extending it. */ + while (input_tail < input_end && + ((*input_tail > 0 && /* literal... */ + *input_tail == to + 1) || /* ...and consecutive, or */ + (*input_tail <= 0 && /* range... */ + input_tail[1] == to + 1)) /* ...and consecutive. */ + ) { + if (!range_p) { + /* convert the literal to a range. */ + output_tail++; + output_tail[-2] = 0; + output_tail[-1] = from; + range_p = true; + } + + if (*input_tail > 0) { /* literal */ + output_tail[-2]--; /* increase length by 1 */ + to++; + input_tail++; + } else { + int32_t L2 = (-*input_tail) + 1; + output_tail[-2] -= L2; /* increase length by N */ + to += L2; + input_tail += 2; + } + } + } + + PR_Free(m_data); + m_data = output_data; + m_data_size = output_size; + m_length = output_tail - output_data; + + /* One last pass to turn [N - N+1] into [N, N+1]. */ + output_tail = output_data; + output_end = output_tail + m_length; + while (output_tail < output_end) { + if (*output_tail < 0) { + /* it's a range */ + if (output_tail[0] == -1) { + output_tail[0] = output_tail[1]; + output_tail[1]++; + } + output_tail += 2; + } else { + /* it's a literal */ + output_tail++; + } + } + +#ifdef NEWSRC_DOES_HOST_STUFF + if (m_host) m_host->MarkDirty(); +#endif + return true; +} + +bool nsMsgKeySet::IsMember(int32_t number) { + bool value = false; + int32_t size; + int32_t* head; + int32_t* tail; + int32_t* end; + + size = m_length; + head = m_data; + tail = head; + end = head + size; + + /* If there is a value cached, and that value is smaller than the + value we're looking for, skip forward that far. */ + if (m_cached_value > 0 && m_cached_value < number) { + tail += m_cached_value_index; + } + + while (tail < end) { + if (*tail < 0) { + /* it's a range */ + int32_t from = tail[1]; + int32_t to = from + (-(tail[0])); + if (from > number) { + /* This range begins after the number - we've passed it. */ + value = false; + goto DONE; + } else if (to >= number) { + /* In range. */ + value = true; + goto DONE; + } else { + tail += 2; + } + } else { + /* it's a literal */ + if (*tail == number) { + /* bang */ + value = true; + goto DONE; + } else if (*tail > number) { + /* This literal is after the number - we've passed it. */ + value = false; + goto DONE; + } else { + tail++; + } + } + } + +DONE: + /* Store the position of this chunk for next time. */ + m_cached_value = number; + m_cached_value_index = tail - head; + + return value; +} + +int nsMsgKeySet::Add(int32_t number) { + int32_t size; + int32_t* head; + int32_t* tail; + int32_t* end; + +#ifdef DEBUG_MSGKEYSET + printf("add %d\n", number); +#endif + + size = m_length; + head = m_data; + tail = head; + end = head + size; + + NS_ASSERTION(number >= 0, "can't have negative items"); + if (number < 0) return 0; + + /* We're going to modify the set, so invalidate the cache. */ + m_cached_value = -1; + + while (tail < end) { + if (*tail < 0) { + /* it's a range */ + int32_t from = tail[1]; + int32_t to = from + (-(tail[0])); + + if (from <= number && to >= number) { + /* This number is already present - we don't need to do + anything. */ + return 0; + } + + if (to > number) { + /* We have found the point before which the new number + should be inserted. */ + break; + } + + tail += 2; + } else { + /* it's a literal */ + if (*tail == number) { + /* This number is already present - we don't need to do + anything. */ + return 0; + } + + if (*tail > number) { + /* We have found the point before which the new number + should be inserted. */ + break; + } + + tail++; + } + } + + /* At this point, `tail' points to a position in the set which represents + a value greater than `new'; or it is at `end'. In the interest of + avoiding massive duplication of code, simply insert a literal here and + then run the optimizer. + */ + int mid = (tail - head); + + if (m_data_size <= m_length + 1) { + int endo = end - head; + if (!Grow()) { + // out of memory + return -1; + } + head = m_data; + end = head + endo; + } + + if (tail == end) { + /* at the end */ + /* Add a literal to the end. */ + m_data[m_length++] = number; + } else { + /* need to insert (or edit) in the middle */ + int32_t i; + for (i = size; i > mid; i--) { + m_data[i] = m_data[i - 1]; + } + m_data[i] = number; + m_length++; + } + + Optimize(); + return 1; +} + +int nsMsgKeySet::Remove(int32_t number) { + int32_t size; + int32_t* head; + int32_t* tail; + int32_t* end; +#ifdef DEBUG_MSGKEYSET + printf("remove %d\n", number); +#endif + + size = m_length; + head = m_data; + tail = head; + end = head + size; + + // **** I am not sure this is a right thing to comment the following + // statements out. The reason for this is due to the implementation of + // offline save draft and template. We use faked UIDs (negative ids) for + // offline draft and template in order to distinguish them from real + // UID. David I need your help here. **** jt + + // PR_ASSERT(number >= 0); + // if (number < 0) { + // return -1; + /// } + + /* We're going to modify the set, so invalidate the cache. */ + m_cached_value = -1; + + while (tail < end) { + int32_t mid = (tail - m_data); + + if (*tail < 0) { + /* it's a range */ + int32_t from = tail[1]; + int32_t to = from + (-(tail[0])); + + if (number < from || number > to) { + /* Not this range */ + tail += 2; + continue; + } + + if (to == from + 1) { + /* If this is a range [N - N+1] and we are removing M + (which must be either N or N+1) replace it with a + literal. This reduces the length by 1. */ + m_data[mid] = (number == from ? to : from); + while (++mid < m_length) { + m_data[mid] = m_data[mid + 1]; + } + m_length--; + Optimize(); + return 1; + } else if (to == from + 2) { + /* If this is a range [N - N+2] and we are removing M, + replace it with the literals L,M (that is, either + (N, N+1), (N, N+2), or (N+1, N+2). The overall + length remains the same. */ + m_data[mid] = from; + m_data[mid + 1] = to; + if (from == number) { + m_data[mid] = from + 1; + } else if (to == number) { + m_data[mid + 1] = to - 1; + } + Optimize(); + return 1; + } else if (from == number) { + /* This number is at the beginning of a long range (meaning a + range which will still be long enough to remain a range.) + Increase start and reduce length of the range. */ + m_data[mid]++; + m_data[mid + 1]++; + Optimize(); + return 1; + } else if (to == number) { + /* This number is at the end of a long range (meaning a range + which will still be long enough to remain a range.) + Just decrease the length of the range. */ + m_data[mid]++; + Optimize(); + return 1; + } else { + /* The number being deleted is in the middle of a range which + must be split. This increases overall length by 2. + */ + int32_t i; + int endo = end - head; + if (m_data_size - m_length <= 2) { + if (!Grow()) + // out of memory + return -1; + } + head = m_data; + end = head + endo; + + for (i = m_length + 2; i > mid + 2; i--) { + m_data[i] = m_data[i - 2]; + } + + m_data[mid] = (-(number - from - 1)); + m_data[mid + 1] = from; + m_data[mid + 2] = (-(to - number - 1)); + m_data[mid + 3] = number + 1; + m_length += 2; + + /* Oops, if we've ended up with a range with a 0 length, + which is illegal, convert it to a literal, which reduces + the overall length by 1. */ + if (m_data[mid] == 0) { + /* first range */ + m_data[mid] = m_data[mid + 1]; + for (i = mid + 1; i < m_length; i++) { + m_data[i] = m_data[i + 1]; + } + m_length--; + } + if (m_data[mid + 2] == 0) { + /* second range */ + m_data[mid + 2] = m_data[mid + 3]; + for (i = mid + 3; i < m_length; i++) { + m_data[i] = m_data[i + 1]; + } + m_length--; + } + Optimize(); + return 1; + } + } else { + /* it's a literal */ + if (*tail != number) { + /* Not this literal */ + tail++; + continue; + } + + /* Excise this literal. */ + m_length--; + while (mid < m_length) { + m_data[mid] = m_data[mid + 1]; + mid++; + } + Optimize(); + return 1; + } + } + + /* It wasn't here at all. */ + return 0; +} + +static int32_t* msg_emit_range(int32_t* tmp, int32_t a, int32_t b) { + if (a == b) { + *tmp++ = a; + } else { + NS_ASSERTION(a < b && a >= 0, "range is out of order"); + *tmp++ = -(b - a); + *tmp++ = a; + } + return tmp; +} + +int nsMsgKeySet::AddRange(int32_t start, int32_t end) { + int32_t tmplength; + int32_t* tmp; + int32_t* in; + int32_t* out; + int32_t* tail; + int32_t a; + int32_t b; + bool didit = false; + + /* We're going to modify the set, so invalidate the cache. */ + m_cached_value = -1; + + NS_ASSERTION(start <= end, "invalid range"); + if (start > end) return -1; + + if (start == end) { + return Add(start); + } + + tmplength = m_length + 2; + tmp = (int32_t*)PR_Malloc(sizeof(int32_t) * tmplength); + + if (!tmp) + // out of memory + return -1; + + in = m_data; + out = tmp; + tail = in + m_length; + +#define EMIT(x, y) out = msg_emit_range(out, x, y) + + while (in < tail) { + // Set [a,b] to be this range. + if (*in < 0) { + b = -*in++; + a = *in++; + b += a; + } else { + a = b = *in++; + } + + if (a <= start && b >= end) { + // We already have the entire range marked. + PR_Free(tmp); + return 0; + } + if (start > b + 1) { + // No overlap yet. + EMIT(a, b); + } else if (end < a - 1) { + // No overlap, and we passed it. + EMIT(start, end); + EMIT(a, b); + didit = true; + break; + } else { + // The ranges overlap. Suck this range into our new range, and + // keep looking for other ranges that might overlap. + start = start < a ? start : a; + end = end > b ? end : b; + } + } + if (!didit) EMIT(start, end); + while (in < tail) { + *out++ = *in++; + } + +#undef EMIT + + PR_Free(m_data); + m_data = tmp; + m_length = out - tmp; + m_data_size = tmplength; +#ifdef NEWSRC_DOES_HOST_STUFF + if (m_host) m_host->MarkDirty(); +#endif + return 1; +} + +int32_t nsMsgKeySet::CountMissingInRange(int32_t range_start, + int32_t range_end) { + int32_t count; + int32_t* head; + int32_t* tail; + int32_t* end; + + NS_ASSERTION(range_start >= 0 && range_end >= 0 && range_end >= range_start, + "invalid range"); + if (range_start < 0 || range_end < 0 || range_end < range_start) return -1; + + head = m_data; + tail = head; + end = head + m_length; + + count = range_end - range_start + 1; + + while (tail < end) { + if (*tail < 0) { + /* it's a range */ + int32_t from = tail[1]; + int32_t to = from + (-(tail[0])); + if (from < range_start) from = range_start; + if (to > range_end) to = range_end; + + if (to >= from) count -= (to - from + 1); + + tail += 2; + } else { + /* it's a literal */ + if (*tail >= range_start && *tail <= range_end) count--; + tail++; + } + NS_ASSERTION(count >= 0, "invalid count"); + } + return count; +} + +int nsMsgKeySet::FirstMissingRange(int32_t min, int32_t max, int32_t* first, + int32_t* last) { + int32_t size; + int32_t* head; + int32_t* tail; + int32_t* end; + int32_t from = 0; + int32_t to = 0; + int32_t a; + int32_t b; + + NS_ASSERTION(first && last, "invalid parameter"); + if (!first || !last) return -1; + + *first = *last = 0; + + NS_ASSERTION(min <= max && min > 0, "invalid min or max param"); + if (min > max || min <= 0) return -1; + + size = m_length; + head = m_data; + tail = head; + end = head + size; + + while (tail < end) { + a = to + 1; + if (*tail < 0) { /* We got a range. */ + from = tail[1]; + to = from + (-(tail[0])); + tail += 2; + } else { + from = to = tail[0]; + tail++; + } + b = from - 1; + /* At this point, [a,b] is the range of unread articles just before + the current range of read articles [from,to]. See if this range + intersects the [min,max] range we were given. */ + if (a > max) return 0; /* It's hopeless; there are none. */ + if (a <= b && b >= min) { + /* Ah-hah! We found an intersection. */ + *first = a > min ? a : min; + *last = b < max ? b : max; + return 0; + } + } + /* We found no holes in the newsrc that overlaps the range, nor did we hit + something read beyond the end of the range. So, the great infinite + range of unread articles at the end of any newsrc line intersects the + range we want, and we just need to return that. */ + a = to + 1; + *first = a > min ? a : min; + *last = max; + return 0; +} + +// I'm guessing we didn't include this because we didn't think we're going +// to need it. I'm not so sure. I'm putting it in for now. +int nsMsgKeySet::LastMissingRange(int32_t min, int32_t max, int32_t* first, + int32_t* last) { + int32_t size; + int32_t* head; + int32_t* tail; + int32_t* end; + int32_t from = 0; + int32_t to = 0; + int32_t a; + int32_t b; + + NS_ASSERTION(first && last, "invalid null param"); + if (!first || !last) return -1; + + *first = *last = 0; + + NS_ASSERTION(min <= max && min > 0, "invalid min or max"); + if (min > max || min <= 0) return -1; + + size = m_length; + head = m_data; + tail = head; + end = head + size; + + while (tail < end) { + a = to + 1; + if (*tail < 0) { /* We got a range. */ + from = tail[1]; + to = from + (-(tail[0])); + tail += 2; + } else { + from = to = tail[0]; + tail++; + } + b = from - 1; + /* At this point, [a,b] is the range of unread articles just before + the current range of read articles [from,to]. See if this range + intersects the [min,max] range we were given. */ + if (a > max) + return 0; /* We're done. If we found something, it's already + sitting in [*first,*last]. */ + if (a <= b && b >= min) { + /* Ah-hah! We found an intersection. */ + *first = a > min ? a : min; + *last = b < max ? b : max; + /* Continue on, looking for a later range. */ + } + } + if (to < max) { + /* The great infinite range of unread articles at the end of any newsrc + line intersects the range we want, and we just need to return that. */ + a = to + 1; + *first = a > min ? a : min; + *last = max; + } + return 0; +} + +/** + * Fill the passed in aArray with the keys in the message key set. + */ +nsresult nsMsgKeySet::ToMsgKeyArray(nsTArray<nsMsgKey>& aArray) { + int32_t size; + int32_t* head; + int32_t* tail; + int32_t* end; + int32_t last_art = -1; + + size = m_length; + head = m_data; + tail = head; + end = head + size; + + while (tail < end) { + int32_t from; + int32_t to; + + if (*tail < 0) { + /* it's a range */ + from = tail[1]; + to = from + (-(tail[0])); + tail += 2; + } else /* it's a literal */ + { + from = *tail; + to = from; + tail++; + } + // The horrible news-hack used to adjust from to 1 if it was zero right + // here, but there is no longer a consumer of this method with that + // broken use-case. + if (from <= last_art) from = last_art + 1; + if (from <= to) { + if (from < to) { + for (int32_t i = from; i <= to; ++i) { + aArray.AppendElement(i); + } + } else { + aArray.AppendElement(from); + } + last_art = to; + } + } + + return NS_OK; +} + +#ifdef DEBUG /* A lot of test cases for the above */ + +# define countof(x) (sizeof(x) / sizeof(*(x))) + +void nsMsgKeySet::test_decoder(const char* string) { + nsMsgKeySet set(string /* , NULL */); + char* tmp; + set.Output(&tmp); + printf("\t\"%s\"\t--> \"%s\"\n", string, tmp); + free(tmp); +} + +# define START(STRING) \ + string = STRING; \ + if (!(set = nsMsgKeySet::Create(string))) abort() + +# define FROB(N, PUSHP) \ + i = N; \ + if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \ + printf("%3lu: %-58s %c %3lu =\n", (unsigned long)set->m_length, s, \ + (PUSHP ? '+' : '-'), (unsigned long)i); \ + free(s); \ + if (PUSHP ? set->Add(i) < 0 : set->Remove(i) < 0) abort(); \ + if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \ + printf("%3lu: %-58s optimized =\n", (unsigned long)set->m_length, s); \ + free(s); + +# define END() \ + if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \ + printf("%3lu: %s\n\n", (unsigned long)set->m_length, s); \ + free(s); \ + delete set; + +void nsMsgKeySet::test_adder(void) { + const char* string; + nsMsgKeySet* set; + char* s; + int32_t i; + + START("0-70,72-99,105,107,110-111,117-200"); + + FROB(205, true); + FROB(206, true); + FROB(207, true); + FROB(208, true); + FROB(208, true); + FROB(109, true); + FROB(72, true); + + FROB(205, false); + FROB(206, false); + FROB(207, false); + FROB(208, false); + FROB(208, false); + FROB(109, false); + FROB(72, false); + + FROB(72, true); + FROB(109, true); + FROB(208, true); + FROB(208, true); + FROB(207, true); + FROB(206, true); + FROB(205, true); + + FROB(205, false); + FROB(206, false); + FROB(207, false); + FROB(208, false); + FROB(208, false); + FROB(109, false); + FROB(72, false); + + FROB(100, true); + FROB(101, true); + FROB(102, true); + FROB(103, true); + FROB(106, true); + FROB(104, true); + FROB(109, true); + FROB(108, true); + END(); + + // clang-format off + START("1-6"); FROB(7, false); END(); + START("1-6"); FROB(6, false); END(); + START("1-6"); FROB(5, false); END(); + START("1-6"); FROB(4, false); END(); + START("1-6"); FROB(3, false); END(); + START("1-6"); FROB(2, false); END(); + START("1-6"); FROB(1, false); END(); + START("1-6"); FROB(0, false); END(); + + START("1-3"); FROB(1, false); END(); + START("1-3"); FROB(2, false); END(); + START("1-3"); FROB(3, false); END(); + + START("1,3,5-7,9,10"); FROB(5, false); END(); + START("1,3,5-7,9,10"); FROB(6, false); END(); + START("1,3,5-7,9,10"); FROB(7, false); FROB(7, true); FROB(8, true); + FROB (4, true); FROB (2, false); FROB (2, true); + + FROB (4, false); FROB (5, false); FROB (6, false); FROB (7, false); + FROB (8, false); FROB (9, false); FROB (10, false); FROB (3, false); + FROB (2, false); FROB (1, false); FROB (1, false); FROB (0, false); + END(); + // clang-format on +} + +# undef START +# undef FROB +# undef END + +# define START(STRING) \ + string = STRING; \ + if (!(set = nsMsgKeySet::Create(string))) abort() + +# define FROB(N, M) \ + i = N; \ + j = M; \ + if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \ + printf("%3lu: %-58s + %3lu-%3lu =\n", (unsigned long)set->m_length, s, \ + (unsigned long)i, (unsigned long)j); \ + free(s); \ + switch (set->AddRange(i, j)) { \ + case 0: \ + printf("(no-op)\n"); \ + break; \ + case 1: \ + break; \ + default: \ + abort(); \ + } \ + if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \ + printf("%3lu: %-58s\n", (unsigned long)set->m_length, s); \ + free(s); + +# define END() \ + if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \ + printf("%3lu: %s\n\n", (unsigned long)set->m_length, s); \ + free(s); \ + delete set; + +void nsMsgKeySet::test_ranges(void) { + const char* string; + nsMsgKeySet* set; + char* s; + int32_t i; + int32_t j; + + START("20-40,72-99,105,107,110-111,117-200"); + + FROB(205, 208); + FROB(50, 70); + FROB(0, 10); + FROB(112, 113); + FROB(101, 101); + FROB(5, 75); + FROB(103, 109); + FROB(2, 20); + FROB(1, 9999); + + END(); + +# undef START +# undef FROB +# undef END +} + +# define TEST(N) \ + if (!with_cache) set->m_cached_value = -1; \ + if (!(NS_SUCCEEDED(set->Output(&s)))) abort(); \ + printf(" %3d = %s\n", N, (set->IsMember(N) ? "true" : "false")); \ + free(s); + +void nsMsgKeySet::test_member(bool with_cache) { + nsMsgKeySet* set; + char* s; + + const char* st1 = "1-70,72-99,105,107,110-111,117-200"; + printf("\n\nTesting %s (with%s cache)\n", st1, with_cache ? "" : "out"); + if (!(set = Create(st1))) { + abort(); + } + + TEST(-1); + TEST(0); + TEST(1); + TEST(20); + + delete set; + const char* st2 = "0-70,72-99,105,107,110-111,117-200"; + printf("\n\nTesting %s (with%s cache)\n", st2, with_cache ? "" : "out"); + if (!(set = Create(st2))) { + abort(); + } + + TEST(-1); + TEST(0); + TEST(1); + TEST(20); + TEST(69); + TEST(70); + TEST(71); + TEST(72); + TEST(73); + TEST(74); + TEST(104); + TEST(105); + TEST(106); + TEST(107); + TEST(108); + TEST(109); + TEST(110); + TEST(111); + TEST(112); + TEST(116); + TEST(117); + TEST(118); + TEST(119); + TEST(200); + TEST(201); + TEST(65535); + + delete set; +} + +# undef TEST + +// static void +// test_newsrc (char *file) +// { +// FILE *fp = fopen (file, "r"); +// char buf [1024]; +// if (! fp) abort (); +// while (fgets (buf, sizeof (buf), fp)) +// { +// if (!strncmp (buf, "options ", 8)) +// fwrite (buf, 1, strlen (buf), stdout); +// else +// { +// char *sep = buf; +// while (*sep != 0 && *sep != ':' && *sep != '!') +// sep++; +// if (*sep) sep++; +// while (isspace (*sep)) sep++; +// fwrite (buf, 1, sep - buf, stdout); +// if (*sep) +// { +// char *s; +// msg_NewsRCSet *set = msg_parse_newsrc_set (sep, &allocinfo); +// if (! set) +// abort (); +// if (! msg_OptimizeNewsRCSet (set)) +// abort (); +// if (! ((s = msg_format_newsrc_set (set)))) +// abort (); +// msg_free_newsrc_set (set, &allocinfo); +// fwrite (s, 1, strlen (s), stdout); +// free (s); +// fwrite ("\n", 1, 1, stdout); +// } +// } +// } +// fclose (fp); +// } + +void nsMsgKeySet::RunTests() { + test_decoder(""); + test_decoder(" "); + test_decoder("0"); + test_decoder("1"); + test_decoder("123"); + test_decoder(" 123 "); + test_decoder(" 123 4"); + test_decoder(" 1,2, 3, 4"); + test_decoder("0-70,72-99,100,101"); + test_decoder(" 0-70 , 72 - 99 ,100,101 "); + test_decoder("0 - 268435455"); + /* This one overflows - we can't help it. + test_decoder ("0 - 4294967295"); */ + + test_adder(); + + test_ranges(); + + test_member(false); + test_member(true); + + // test_newsrc ("/u/montulli/.newsrc"); + /* test_newsrc ("/u/jwz/.newsrc");*/ +} + +#endif /* DEBUG */ diff --git a/comm/mailnews/base/src/nsMsgKeySet.h b/comm/mailnews/base/src/nsMsgKeySet.h new file mode 100644 index 0000000000..7390645870 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgKeySet.h @@ -0,0 +1,108 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgKeySet_H_ +#define _nsMsgKeySet_H_ + +#include "msgCore.h" +#include "nsTArray.h" + +// nsMsgKeySet represents a set of articles. Typically, it is the set of +// read articles from a .newsrc file, but it can be used for other purposes +// too. + +#if 0 +// If a MSG_NewsHost* is supplied to the creation routine, then that +// MSG_NewsHost will be notified whenever a change is made to set. +class MSG_NewsHost; +#endif + +class nsMsgKeySet { + public: + NS_INLINE_DECL_REFCOUNTING(nsMsgKeySet); + // Creates an empty set. + static nsMsgKeySet* Create(/* MSG_NewsHost* host = NULL*/); + + // Creates a set from the list of numbers, as might be found in a + // newsrc file. + static nsMsgKeySet* Create(const char* str /* , MSG_NewsHost* host = NULL*/); + + // FirstNonMember() returns the lowest non-member of the set that is + // greater than 0. + int32_t FirstNonMember(); + + // Output() converts to a string representation suitable for writing to a + // .newsrc file. + nsresult Output(char** outputStr); + + // IsMember() returns whether the given article is a member of this set. + bool IsMember(int32_t art); + + // Add() adds the given article to the set. (Returns 1 if a change was + // made, 0 if it was already there, and negative on error.) + int Add(int32_t art); + + // Remove() removes the given article from the set. + int Remove(int32_t art); + + // AddRange() adds the (inclusive) given range of articles to the set. + int AddRange(int32_t first, int32_t last); + + // CountMissingInRange() takes an inclusive range of articles and returns + // the number of articles in that range which are not in the set. + int32_t CountMissingInRange(int32_t start, int32_t end); + + // FirstMissingRange() takes an inclusive range and finds the first range + // of articles that are not in the set. If none, return zeros. + int FirstMissingRange(int32_t min, int32_t max, int32_t* first, + int32_t* last); + + // LastMissingRange() takes an inclusive range and finds the last range + // of articles that are not in the set. If none, return zeros. + int LastMissingRange(int32_t min, int32_t max, int32_t* first, int32_t* last); + + int32_t GetLastMember(); + int32_t GetFirstMember(); + void SetLastMember(int32_t highWaterMark); + // For debugging only... + int32_t getLength() { return m_length; } + + /** + * Fill the passed in aArray with the keys in the message key set. + */ + nsresult ToMsgKeyArray(nsTArray<nsMsgKey>& aArray); + +#ifdef DEBUG + static void RunTests(); +#endif + + protected: + nsMsgKeySet(/* MSG_NewsHost* host */); + explicit nsMsgKeySet(const char* /* , MSG_NewsHost* host */); + bool Grow(); + bool Optimize(); + +#ifdef DEBUG + static void test_decoder(const char*); + static void test_adder(); + static void test_ranges(); + static void test_member(bool with_cache); +#endif + + int32_t* m_data; /* the numbers composing the `chunks' */ + int32_t m_data_size; /* size of that malloc'ed block */ + int32_t m_length; /* active area */ + + int32_t m_cached_value; /* a potential set member, or -1 if unset*/ + int32_t m_cached_value_index; /* the index into `data' at which a search + to determine whether `cached_value' was + a member of the set ended. */ +#ifdef NEWSRC_DOES_HOST_STUFF + MSG_NewsHost* m_host; +#endif + ~nsMsgKeySet(); +}; + +#endif /* _nsMsgKeySet_H_ */ diff --git a/comm/mailnews/base/src/nsMsgLineBuffer.cpp b/comm/mailnews/base/src/nsMsgLineBuffer.cpp new file mode 100644 index 0000000000..60d38b5b8f --- /dev/null +++ b/comm/mailnews/base/src/nsMsgLineBuffer.cpp @@ -0,0 +1,351 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "prlog.h" +#include "prmem.h" +#include "nsMsgLineBuffer.h" +#include "nsMsgUtils.h" +#include "nsIInputStream.h" // used by nsMsgLineStreamBuffer + +nsByteArray::nsByteArray() { + MOZ_COUNT_CTOR(nsByteArray); + m_buffer = NULL; + m_bufferSize = 0; + m_bufferPos = 0; +} + +nsByteArray::~nsByteArray() { + MOZ_COUNT_DTOR(nsByteArray); + PR_FREEIF(m_buffer); +} + +nsresult nsByteArray::GrowBuffer(uint64_t desired_size, uint32_t quantum) { + if (desired_size > PR_UINT32_MAX) { + return NS_ERROR_OUT_OF_MEMORY; + } + if (m_bufferSize < desired_size) { + char* new_buf; + uint32_t increment = desired_size - m_bufferSize; + if (increment < quantum) /* always grow by a minimum of N bytes */ + increment = quantum; + + new_buf = + (m_buffer ? (char*)PR_REALLOC(m_buffer, (m_bufferSize + increment)) + : (char*)PR_MALLOC(m_bufferSize + increment)); + if (!new_buf) return NS_ERROR_OUT_OF_MEMORY; + m_buffer = new_buf; + m_bufferSize += increment; + } + return NS_OK; +} + +nsresult nsByteArray::AppendString(const char* string) { + uint32_t strLength = (string) ? PL_strlen(string) : 0; + return AppendBuffer(string, strLength); +} + +nsresult nsByteArray::AppendBuffer(const char* buffer, uint32_t length) { + nsresult ret = NS_OK; + if (m_bufferPos + length > m_bufferSize) + ret = GrowBuffer(m_bufferPos + length, 1024); + if (NS_SUCCEEDED(ret)) { + memcpy(m_buffer + m_bufferPos, buffer, length); + m_bufferPos += length; + } + return ret; +} + +nsMsgLineBuffer::nsMsgLineBuffer() { MOZ_COUNT_CTOR(nsMsgLineBuffer); } + +nsMsgLineBuffer::~nsMsgLineBuffer() { MOZ_COUNT_DTOR(nsMsgLineBuffer); } + +nsresult nsMsgLineBuffer::BufferInput(const char* net_buffer, + int32_t net_buffer_size) { + if (net_buffer_size < 0) { + return NS_ERROR_INVALID_ARG; + } + nsresult status = NS_OK; + if (m_bufferPos > 0 && m_buffer && m_buffer[m_bufferPos - 1] == '\r' && + net_buffer_size > 0 && net_buffer[0] != '\n') { + /* The last buffer ended with a CR. The new buffer does not start + with a LF. This old buffer should be shipped out and discarded. */ + PR_ASSERT(m_bufferSize > m_bufferPos); + if (m_bufferSize <= m_bufferPos) { + return NS_ERROR_UNEXPECTED; + } + if (NS_FAILED(HandleLine(m_buffer, m_bufferPos))) { + return NS_ERROR_FAILURE; + } + m_bufferPos = 0; + } + while (net_buffer_size > 0) { + const char* net_buffer_end = net_buffer + net_buffer_size; + const char* newline = 0; + const char* s; + + for (s = net_buffer; s < net_buffer_end; s++) { + /* Move forward in the buffer until the first newline. + Stop when we see CRLF, CR, or LF, or the end of the buffer. + *But*, if we see a lone CR at the *very end* of the buffer, + treat this as if we had reached the end of the buffer without + seeing a line terminator. This is to catch the case of the + buffers splitting a CRLF pair, as in "FOO\r\nBAR\r" "\nBAZ\r\n". + */ + if (*s == '\r' || *s == '\n') { + newline = s; + if (newline[0] == '\r') { + if (s == net_buffer_end - 1) { + /* CR at end - wait for the next character. */ + newline = 0; + break; + } else if (newline[1] == '\n') { + /* CRLF seen; swallow both. */ + newline++; + } + } + newline++; + break; + } + } + + /* Ensure room in the net_buffer and append some or all of the current + chunk of data to it. */ + { + const char* end = (newline ? newline : net_buffer_end); + uint64_t desired_size = (end - net_buffer) + (uint64_t)m_bufferPos + 1; + if (desired_size >= PR_INT32_MAX) { + // We're not willing to buffer more than 2GB data without seeing + // a newline, something is wrong with the input. + // Using this limit prevents us from overflowing. + return NS_ERROR_UNEXPECTED; + } + + if (desired_size >= m_bufferSize) { + status = GrowBuffer(desired_size, 1024); + if (NS_FAILED(status)) return status; + } + memcpy(m_buffer + m_bufferPos, net_buffer, (end - net_buffer)); + m_bufferPos += (end - net_buffer); + } + + /* Now m_buffer contains either a complete line, or as complete + a line as we have read so far. + + If we have a line, process it, and then remove it from `m_buffer'. + Then go around the loop again, until we drain the incoming data. + */ + if (!newline) return NS_OK; + + if (NS_FAILED(HandleLine(m_buffer, m_bufferPos))) { + return NS_ERROR_FAILURE; + } + + net_buffer_size -= (newline - net_buffer); + net_buffer = newline; + m_bufferPos = 0; + } + return NS_OK; +} + +// If there's still some data (non CRLF terminated) flush it out +nsresult nsMsgLineBuffer::Flush() { + nsresult rv = NS_OK; + if (m_bufferPos > 0) { + rv = HandleLine(m_buffer, m_bufferPos); + m_bufferPos = 0; + } + return rv; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// This is a utility class used to efficiently extract lines from an input +// stream by buffering read but unprocessed stream data in a buffer. +/////////////////////////////////////////////////////////////////////////////////////////////////// + +nsMsgLineStreamBuffer::nsMsgLineStreamBuffer(uint32_t aBufferSize, + bool aAllocateNewLines, + bool aEatCRLFs, char aLineToken) + : m_eatCRLFs(aEatCRLFs), + m_allocateNewLines(aAllocateNewLines), + m_lineToken(aLineToken) { + NS_ASSERTION(aBufferSize > 0, "invalid buffer size!!!"); + m_dataBuffer = nullptr; + m_startPos = 0; + m_numBytesInBuffer = 0; + + // used to buffer incoming data by ReadNextLineFromInput + if (aBufferSize > 0) { + m_dataBuffer = (char*)PR_CALLOC(sizeof(char) * aBufferSize); + } + + m_dataBufferSize = aBufferSize; +} + +nsMsgLineStreamBuffer::~nsMsgLineStreamBuffer() { + PR_FREEIF(m_dataBuffer); // release our buffer... +} + +nsresult nsMsgLineStreamBuffer::GrowBuffer(uint32_t desiredSize) { + char* newBuffer = (char*)PR_REALLOC(m_dataBuffer, desiredSize); + NS_ENSURE_TRUE(newBuffer, NS_ERROR_OUT_OF_MEMORY); + m_dataBuffer = newBuffer; + m_dataBufferSize = desiredSize; + return NS_OK; +} + +void nsMsgLineStreamBuffer::ClearBuffer() { + m_startPos = 0; + m_numBytesInBuffer = 0; +} + +// aInputStream - the input stream we want to read a line from +// aPauseForMoreData is returned as true if the stream does not yet contain a +// line and we must wait for more data to come into the stream. Note to people +// wishing to modify this function: Be *VERY CAREFUL* this is a critical +// function used by all of our mail protocols including imap, nntp, and pop. If +// you screw it up, you could break a lot of stuff..... + +char* nsMsgLineStreamBuffer::ReadNextLine(nsIInputStream* aInputStream, + uint32_t& aNumBytesInLine, + bool& aPauseForMoreData, + nsresult* prv, + bool addLineTerminator) { + // try to extract a line from m_inputBuffer. If we don't have an entire line, + // then read more bytes out from the stream. If the stream is empty then wait + // on the monitor for more data to come in. + + NS_ASSERTION(m_dataBuffer && m_dataBufferSize > 0, + "invalid input arguments for read next line from input"); + + if (prv) *prv = NS_OK; + // initialize out values + aPauseForMoreData = false; + aNumBytesInLine = 0; + char* endOfLine = nullptr; + char* startOfLine = m_dataBuffer + m_startPos; + + if (m_numBytesInBuffer > 0) // any data in our internal buffer? + endOfLine = PL_strchr(startOfLine, m_lineToken); // see if we already + // have a line ending... + + // it's possible that we got here before the first time we receive data from + // the server so aInputStream will be nullptr... + if (!endOfLine && aInputStream) // get some more data from the server + { + nsresult rv; + uint64_t numBytesInStream = 0; + uint32_t numBytesCopied = 0; + bool nonBlockingStream; + aInputStream->IsNonBlocking(&nonBlockingStream); + rv = aInputStream->Available(&numBytesInStream); + if (NS_FAILED(rv)) { + if (prv) *prv = rv; + aNumBytesInLine = 0; + return nullptr; + } + if (!nonBlockingStream && numBytesInStream == 0) // if no data available, + numBytesInStream = m_dataBufferSize / 2; // ask for half the data + // buffer size. + + // if the number of bytes we want to read from the stream, is greater than + // the number of bytes left in our buffer, then we need to shift the start + // pos and its contents down to the beginning of m_dataBuffer... + uint32_t numFreeBytesInBuffer = + m_dataBufferSize - m_startPos - m_numBytesInBuffer; + if (numBytesInStream >= numFreeBytesInBuffer) { + if (m_startPos) { + memmove(m_dataBuffer, startOfLine, m_numBytesInBuffer); + // make sure the end of the buffer is terminated + m_dataBuffer[m_numBytesInBuffer] = '\0'; + m_startPos = 0; + startOfLine = m_dataBuffer; + numFreeBytesInBuffer = m_dataBufferSize - m_numBytesInBuffer; + } + // If we didn't make enough space (or any), grow the buffer + if (numBytesInStream >= numFreeBytesInBuffer) { + int64_t growBy = (numBytesInStream - numFreeBytesInBuffer) * 2 + 1; + // GrowBuffer cannot handle over 4GB size. + if (m_dataBufferSize + growBy > PR_UINT32_MAX) return nullptr; + // try growing buffer by twice as much as we need. + nsresult rv = GrowBuffer(m_dataBufferSize + growBy); + // if we can't grow the buffer, we have to bail. + if (NS_FAILED(rv)) return nullptr; + startOfLine = m_dataBuffer; + numFreeBytesInBuffer += growBy; + } + NS_ASSERTION(m_startPos == 0, "m_startPos should be 0 ....."); + } + + uint32_t numBytesToCopy = /* leave one for a null terminator */ + std::min(uint64_t(numFreeBytesInBuffer - 1), numBytesInStream); + if (numBytesToCopy > 0) { + // read the data into the end of our data buffer + char* startOfNewData = startOfLine + m_numBytesInBuffer; + rv = aInputStream->Read(startOfNewData, numBytesToCopy, &numBytesCopied); + if (prv) *prv = rv; + uint32_t i; + for (i = 0; i < numBytesCopied; i++) // replace nulls with spaces + { + if (!startOfNewData[i]) startOfNewData[i] = ' '; + } + m_numBytesInBuffer += numBytesCopied; + m_dataBuffer[m_startPos + m_numBytesInBuffer] = '\0'; + + // okay, now that we've tried to read in more data from the stream, + // look for another end of line character in the new data + endOfLine = PL_strchr(startOfNewData, m_lineToken); + } + } + + // okay, now check again for endOfLine. + if (endOfLine) { + if (!m_eatCRLFs) endOfLine += 1; // count for LF or CR + + aNumBytesInLine = endOfLine - startOfLine; + + if (m_eatCRLFs && aNumBytesInLine > 0 && + startOfLine[aNumBytesInLine - 1] == '\r') + aNumBytesInLine--; // Remove the CR in a CRLF sequence. + + // PR_CALLOC zeros out the allocated line + char* newLine = (char*)PR_CALLOC( + aNumBytesInLine + (addLineTerminator ? MSG_LINEBREAK_LEN : 0) + 1); + if (!newLine) { + aNumBytesInLine = 0; + aPauseForMoreData = true; + return nullptr; + } + + memcpy(newLine, startOfLine, + aNumBytesInLine); // copy the string into the new line buffer + if (addLineTerminator) { + memcpy(newLine + aNumBytesInLine, MSG_LINEBREAK, MSG_LINEBREAK_LEN); + aNumBytesInLine += MSG_LINEBREAK_LEN; + } + + if (m_eatCRLFs) + endOfLine += 1; // advance past LF or CR if we haven't already done so... + + // now we need to update the data buffer to go past the line we just read + // out. + m_numBytesInBuffer -= (endOfLine - startOfLine); + if (m_numBytesInBuffer) + m_startPos = endOfLine - m_dataBuffer; + else + m_startPos = 0; + + return newLine; + } + + aPauseForMoreData = true; + return nullptr; // if we somehow got here. we don't have another line in the + // buffer yet...need to wait for more data... +} + +bool nsMsgLineStreamBuffer::NextLineAvailable() { + return (m_numBytesInBuffer > 0 && + PL_strchr(m_dataBuffer + m_startPos, m_lineToken)); +} diff --git a/comm/mailnews/base/src/nsMsgLineBuffer.h b/comm/mailnews/base/src/nsMsgLineBuffer.h new file mode 100644 index 0000000000..2ac579be3b --- /dev/null +++ b/comm/mailnews/base/src/nsMsgLineBuffer.h @@ -0,0 +1,123 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef _nsMsgLineBuffer_H +#define _nsMsgLineBuffer_H + +#include "msgCore.h" // precompiled header... + +// I can't believe I have to have this stupid class, but I can't find +// anything suitable (nsStrImpl might be, when it's done). nsIByteBuffer +// would do, if I had a stream for input, which I don't. + +class nsByteArray { + public: + nsByteArray(); + virtual ~nsByteArray(); + uint32_t GetSize() { return m_bufferSize; } + uint32_t GetBufferPos() { return m_bufferPos; } + nsresult GrowBuffer(uint64_t desired_size, uint32_t quantum = 1024); + nsresult AppendString(const char* string); + nsresult AppendBuffer(const char* buffer, uint32_t length); + void ResetWritePos() { m_bufferPos = 0; } + char* GetBuffer() { return m_buffer; } + + protected: + char* m_buffer; + uint32_t m_bufferSize; + uint32_t + m_bufferPos; // write Pos in m_buffer - where the next byte should go. +}; + +/** + * nsMsgLineBuffer breaks up incoming data into lines. + * It accepts CRLF, CR or LF line endings. + * + * Data is fed in via BufferInput(). The virtual HandleLine() will be + * invoked for each line. The data passed to HandleLine() is verbatim, + * and will include whatever line endings were in the source data. + * + * Flush() should be called when the data is exhausted, to handle any + * leftover bytes in the buffer (e.g. if the data doesn't end with an EOL). + */ +class nsMsgLineBuffer : private nsByteArray { + public: + nsMsgLineBuffer(); + virtual ~nsMsgLineBuffer(); + nsresult BufferInput(const char* net_buffer, int32_t net_buffer_size); + + /** + * HandleLine should be implemented by derived classes, to handle a line. + * The line will have whatever end-of-line characters were present in the + * source data (potentially none, if the data ends mid-line). + */ + virtual nsresult HandleLine(const char* line, uint32_t line_length) = 0; + + /** + * Flush processes any unprocessed data currently in the buffer. Should + * be called when the source data is exhausted. + */ + nsresult Flush(); +}; + +// I'm adding this utility class here for lack of a better place. This utility +// class is similar to nsMsgLineBuffer except it works from an input stream. It +// is geared towards efficiently parsing new lines out of a stream by storing +// read but unprocessed bytes in a buffer. I envision the primary use of this to +// be our mail protocols such as imap, news and pop which need to process line +// by line data being returned in the form of a proxied stream from the server. + +class nsIInputStream; + +class nsMsgLineStreamBuffer { + public: + NS_INLINE_DECL_REFCOUNTING(nsMsgLineStreamBuffer) + + // aBufferSize -- size of the buffer you want us to use for buffering stream + // data + // aEndOfLinetoken -- The delimiter string to be used for determining the end + // of line. This allows us to parse platform specific end of + // line endings by making it a parameter. + // aAllocateNewLines -- true if you want calls to ReadNextLine to allocate new + // memory for the line. + // if false, the char * returned is just a ptr into the buffer. + // Subsequent calls to ReadNextLine will alter the data so your + // ptr only has a life time of a per call. + // aEatCRLFs -- true if you don't want to see the CRLFs on the lines + // returned by ReadNextLine. + // false if you do want to see them. + // aLineToken -- Specify the line token to look for, by default is LF ('\n') + // which cover as well CRLF. If lines are terminated with a CR + // only, you need to set aLineToken to CR ('\r') + nsMsgLineStreamBuffer( + uint32_t aBufferSize, bool aAllocateNewLines, bool aEatCRLFs = true, + char aLineToken = '\n'); // specify the size of the buffer you want the + // class to use.... + + // Caller must free the line returned using PR_Free + // aEndOfLinetoken -- delimiter used to denote the end of a line. + // aNumBytesInLine -- The number of bytes in the line returned + // aPauseForMoreData -- There is not enough data in the stream to make a line + // at this time... + char* ReadNextLine(nsIInputStream* aInputStream, uint32_t& aNumBytesInLine, + bool& aPauseForMoreData, nsresult* rv = nullptr, + bool addLineTerminator = false); + nsresult GrowBuffer(uint32_t desiredSize); + void ClearBuffer(); + bool NextLineAvailable(); + + private: + virtual ~nsMsgLineStreamBuffer(); + + protected: + bool m_eatCRLFs; + bool m_allocateNewLines; + char* m_dataBuffer; + uint32_t m_dataBufferSize; + uint32_t m_startPos; + uint32_t m_numBytesInBuffer; + char m_lineToken; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgMailNewsUrl.cpp b/comm/mailnews/base/src/nsMsgMailNewsUrl.cpp new file mode 100644 index 0000000000..3796aeaef2 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgMailNewsUrl.cpp @@ -0,0 +1,1070 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgMailNewsUrl.h" +#include "nsIMsgAccountManager.h" +#include "nsString.h" +#include "nsILoadGroup.h" +#include "nsIDocShell.h" +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIIOService.h" +#include "nsNetCID.h" +#include "nsIStreamListener.h" +#include "nsIOutputStream.h" +#include "nsIInputStream.h" +#include "nsNetUtil.h" +#include "nsIFile.h" +#include "prmem.h" +#include <time.h> +#include "nsMsgUtils.h" +#include "mozilla/Components.h" +#include "nsProxyRelease.h" +#include "mozilla/Encoding.h" +#include "nsDocShellLoadState.h" +#include "nsContentUtils.h" +#include "nsIObjectInputStream.h" +#include "nsIObjectOutputStream.h" +#include "nsIChannel.h" + +nsMsgMailNewsUrl::nsMsgMailNewsUrl() { + // nsIURI specific state + m_runningUrl = false; + m_updatingFolder = false; + m_msgIsInLocalCache = false; + m_suppressErrorMsgs = false; + m_hasNormalizedOrigin = false; // SetSpecInternal() will set this correctly. + mMaxProgress = -1; +} + +#define NOTIFY_URL_LISTENERS(propertyfunc_, params_) \ + PR_BEGIN_MACRO \ + nsTObserverArray<nsCOMPtr<nsIUrlListener>>::ForwardIterator iter( \ + mUrlListeners); \ + while (iter.HasMore()) { \ + nsCOMPtr<nsIUrlListener> listener = iter.GetNext(); \ + listener->propertyfunc_ params_; \ + } \ + PR_END_MACRO + +nsMsgMailNewsUrl::~nsMsgMailNewsUrl() { + // In IMAP this URL is created and destroyed on the imap thread, + // so we must ensure that releases of XPCOM objects (which might be + // implemented by non-threadsafe JS components) are released on the + // main thread. + NS_ReleaseOnMainThread("nsMsgMailNewsUrl::m_baseURL", m_baseURL.forget()); + NS_ReleaseOnMainThread("nsMsgMailNewsUrl::mMimeHeaders", + mMimeHeaders.forget()); + NS_ReleaseOnMainThread("nsMsgMailNewsUrl::m_searchSession", + m_searchSession.forget()); + + nsTObserverArray<nsCOMPtr<nsIUrlListener>>::ForwardIterator iter( + mUrlListeners); + while (iter.HasMore()) { + nsCOMPtr<nsIUrlListener> listener = iter.GetNext(); + if (listener) + NS_ReleaseOnMainThread("nsMsgMailNewsUrl::mUrlListeners", + listener.forget()); + } +} + +NS_IMPL_ADDREF(nsMsgMailNewsUrl) +NS_IMPL_RELEASE(nsMsgMailNewsUrl) + +// We want part URLs to QI to nsIURIWithSpecialOrigin so we can give +// them a "normalized" origin. URLs that already have a "normalized" +// origin should not QI to nsIURIWithSpecialOrigin. +NS_INTERFACE_MAP_BEGIN(nsMsgMailNewsUrl) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIMsgMailNewsUrl) + NS_INTERFACE_MAP_ENTRY(nsIMsgMailNewsUrl) + NS_INTERFACE_MAP_ENTRY(nsIURL) + NS_INTERFACE_MAP_ENTRY(nsIURI) + NS_INTERFACE_MAP_ENTRY(nsISerializable) + NS_INTERFACE_MAP_ENTRY(nsIClassInfo) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIURIWithSpecialOrigin, + m_hasNormalizedOrigin) +NS_INTERFACE_MAP_END + +//-------------------------- +// Support for serialization +//-------------------------- +// nsMsgMailNewsUrl is only partly serialized by serializing the "base URL" +// which is an nsStandardURL, or by only serializing the Spec. This may +// cause problems in the future. See bug 1512356 and bug 1515337 for details, +// follow-up in bug 1512698. + +NS_IMETHODIMP_(void) +nsMsgMailNewsUrl::Serialize(mozilla::ipc::URIParams& aParams) { + m_baseURL->Serialize(aParams); +} + +//---------------------------- +// Support for nsISerializable +//---------------------------- +NS_IMETHODIMP nsMsgMailNewsUrl::Read(nsIObjectInputStream* stream) { + nsAutoCString urlstr; + nsresult rv = NS_ReadOptionalCString(stream, urlstr); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service(); + NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED); + nsCOMPtr<nsIURI> url; + rv = ioService->NewURI(urlstr, nullptr, nullptr, getter_AddRefs(url)); + NS_ENSURE_SUCCESS(rv, rv); + m_baseURL = do_QueryInterface(url); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::Write(nsIObjectOutputStream* stream) { + nsAutoCString urlstr; + nsresult rv = m_baseURL->GetSpec(urlstr); + NS_ENSURE_SUCCESS(rv, rv); + return NS_WriteOptionalStringZ(stream, urlstr.get()); +} + +//------------------------- +// Support for nsIClassInfo +//------------------------- +NS_IMETHODIMP nsMsgMailNewsUrl::GetInterfaces(nsTArray<nsIID>& array) { + array.Clear(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetScriptableHelper( + nsIXPCScriptable** _retval) { + *_retval = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetContractID(nsACString& aContractID) { + aContractID.SetIsVoid(true); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetClassDescription( + nsACString& aClassDescription) { + aClassDescription.SetIsVoid(true); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetClassID(nsCID** aClassID) { + *aClassID = (nsCID*)moz_xmalloc(sizeof(nsCID)); + return GetClassIDNoAlloc(*aClassID); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetFlags(uint32_t* aFlags) { + *aFlags = 0; + return NS_OK; +} + +#define NS_MSGMAILNEWSURL_CID \ + { \ + 0x3fdae3ab, 0x4ac1, 0x4ad4, { \ + 0xb2, 0x8a, 0x28, 0xd0, 0xfa, 0x36, 0x39, 0x29 \ + } \ + } +static NS_DEFINE_CID(kNS_MSGMAILNEWSURL_CID, NS_MSGMAILNEWSURL_CID); +NS_IMETHODIMP nsMsgMailNewsUrl::GetClassIDNoAlloc(nsCID* aClassIDNoAlloc) { + *aClassIDNoAlloc = kNS_MSGMAILNEWSURL_CID; + return NS_OK; +} + +//------------------------------------ +// Support for nsIURIWithSpecialOrigin +//------------------------------------ +NS_IMETHODIMP nsMsgMailNewsUrl::GetOrigin(nsIURI** aOrigin) { + MOZ_ASSERT(m_hasNormalizedOrigin, + "nsMsgMailNewsUrl::GetOrigin() can only be called for URLs with " + "normalized spec"); + + if (!m_normalizedOrigin) { + nsCOMPtr<nsIMsgMessageUrl> msgUrl; + QueryInterface(NS_GET_IID(nsIMsgMessageUrl), getter_AddRefs(msgUrl)); + + nsAutoCString spec; + if (!msgUrl || NS_FAILED(msgUrl->GetNormalizedSpec(spec))) { + MOZ_ASSERT(false, "Can't get normalized spec"); + // just use the normal spec. + GetSpec(spec); + } + + nsresult rv = NS_NewURI(getter_AddRefs(m_normalizedOrigin), spec); + NS_ENSURE_SUCCESS(rv, rv); + } + + NS_IF_ADDREF(*aOrigin = m_normalizedOrigin); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////////// +// Begin nsIMsgMailNewsUrl specific support +//////////////////////////////////////////////////////////////////////////////////// + +nsresult nsMsgMailNewsUrl::GetUrlState(bool* aRunningUrl) { + if (aRunningUrl) *aRunningUrl = m_runningUrl; + + return NS_OK; +} + +nsresult nsMsgMailNewsUrl::SetUrlState(bool aRunningUrl, nsresult aExitCode) { + // if we already knew this running state, return, unless the url was aborted + if (m_runningUrl == aRunningUrl && aExitCode != NS_MSG_ERROR_URL_ABORTED) { + return NS_OK; + } + m_runningUrl = aRunningUrl; + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + + // put this back - we need it for urls that don't run through the doc loader + if (NS_SUCCEEDED(GetStatusFeedback(getter_AddRefs(statusFeedback))) && + statusFeedback) { + if (m_runningUrl) + statusFeedback->StartMeteors(); + else { + statusFeedback->ShowProgress(0); + statusFeedback->StopMeteors(); + } + } + + if (m_runningUrl) { + NOTIFY_URL_LISTENERS(OnStartRunningUrl, (this)); + } else { + NOTIFY_URL_LISTENERS(OnStopRunningUrl, (this, aExitCode)); + mUrlListeners.Clear(); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::RegisterListener(nsIUrlListener* aUrlListener) { + NS_ENSURE_ARG_POINTER(aUrlListener); + mUrlListeners.AppendElement(aUrlListener); + return NS_OK; +} + +nsresult nsMsgMailNewsUrl::UnRegisterListener(nsIUrlListener* aUrlListener) { + NS_ENSURE_ARG_POINTER(aUrlListener); + + // Due to the way mailnews is structured, some listeners attempt to remove + // themselves twice. This may in fact be an error in the coding, however + // if they didn't do it as they do currently, then they could fail to remove + // their listeners. + mUrlListeners.RemoveElement(aUrlListener); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetServer( + nsIMsgIncomingServer** aIncomingServer) { + // mscott --> we could cache a copy of the server here....but if we did, we + // run the risk of leaking the server if any single url gets leaked....of + // course that shouldn't happen...but it could. so i'm going to look it up + // every time and we can look at caching it later. + + nsresult rv; + + nsAutoCString urlstr; + rv = m_baseURL->GetSpec(urlstr); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIURL> url; + rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(urlstr) + .Finalize(url); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString scheme; + rv = GetScheme(scheme); + if (NS_SUCCEEDED(rv)) { + if (scheme.EqualsLiteral("pop")) scheme.AssignLiteral("pop3"); + // we use "nntp" in the server list so translate it here. + if (scheme.EqualsLiteral("news")) scheme.AssignLiteral("nntp"); + rv = NS_MutateURI(url).SetScheme(scheme).Finalize(url); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = accountManager->FindServerByURI(url, aIncomingServer); + if (!*aIncomingServer && scheme.EqualsLiteral("imap")) { + // look for any imap server with this host name so clicking on + // other users folder urls will work. We could override this method + // for imap urls, or we could make caching of servers work and + // just set the server in the imap code for this case. + rv = NS_MutateURI(url).SetUserPass(EmptyCString()).Finalize(url); + NS_ENSURE_SUCCESS(rv, rv); + rv = accountManager->FindServerByURI(url, aIncomingServer); + } + } + + return rv; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetMsgWindow(nsIMsgWindow** aMsgWindow) { + NS_ENSURE_ARG_POINTER(aMsgWindow); + *aMsgWindow = nullptr; + + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak)); + msgWindow.forget(aMsgWindow); + return *aMsgWindow ? NS_OK : NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetMsgWindow(nsIMsgWindow* aMsgWindow) { +#ifdef DEBUG_David_Bienvenu + NS_ASSERTION(aMsgWindow || !m_msgWindowWeak, + "someone crunching non-null msg window"); +#endif + m_msgWindowWeak = do_GetWeakReference(aMsgWindow); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetStatusFeedback( + nsIMsgStatusFeedback** aMsgFeedback) { + // note: it is okay to return a null status feedback and not return an error + // it's possible the url really doesn't have status feedback + *aMsgFeedback = nullptr; + if (!m_statusFeedbackWeak) { + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak)); + if (msgWindow) msgWindow->GetStatusFeedback(aMsgFeedback); + } else { + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback( + do_QueryReferent(m_statusFeedbackWeak)); + statusFeedback.forget(aMsgFeedback); + } + return *aMsgFeedback ? NS_OK : NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetStatusFeedback( + nsIMsgStatusFeedback* aMsgFeedback) { + if (aMsgFeedback) m_statusFeedbackWeak = do_GetWeakReference(aMsgFeedback); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetMaxProgress(int64_t* aMaxProgress) { + *aMaxProgress = mMaxProgress; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetMaxProgress(int64_t aMaxProgress) { + mMaxProgress = aMaxProgress; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetLoadGroup(nsILoadGroup** aLoadGroup) { + *aLoadGroup = nullptr; + // note: it is okay to return a null load group and not return an error + // it's possible the url really doesn't have load group + nsCOMPtr<nsILoadGroup> loadGroup(do_QueryReferent(m_loadGroupWeak)); + if (!loadGroup) { + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak)); + if (msgWindow) { + // XXXbz This is really weird... why are we getting some + // random loadgroup we're not really a part of? + nsCOMPtr<nsIDocShell> docShell; + msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + loadGroup = do_GetInterface(docShell); + m_loadGroupWeak = do_GetWeakReference(loadGroup); + } + } + loadGroup.forget(aLoadGroup); + return *aLoadGroup ? NS_OK : NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetUpdatingFolder(bool* aResult) { + NS_ENSURE_ARG(aResult); + *aResult = m_updatingFolder; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetUpdatingFolder(bool updatingFolder) { + m_updatingFolder = updatingFolder; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetMsgIsInLocalCache(bool* aMsgIsInLocalCache) { + NS_ENSURE_ARG(aMsgIsInLocalCache); + *aMsgIsInLocalCache = m_msgIsInLocalCache; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetMsgIsInLocalCache(bool aMsgIsInLocalCache) { + m_msgIsInLocalCache = aMsgIsInLocalCache; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetSuppressErrorMsgs(bool* aSuppressErrorMsgs) { + NS_ENSURE_ARG(aSuppressErrorMsgs); + *aSuppressErrorMsgs = m_suppressErrorMsgs; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetSuppressErrorMsgs(bool aSuppressErrorMsgs) { + m_suppressErrorMsgs = aSuppressErrorMsgs; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetErrorCode(nsACString& aErrorCode) { + aErrorCode = m_errorCode; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetErrorCode(const nsACString& aErrorCode) { + m_errorCode.Assign(aErrorCode); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetErrorMessage(nsAString& aErrorMessage) { + aErrorMessage = m_errorMessage; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetErrorMessage( + const nsAString& aErrorMessage) { + m_errorMessage.Assign(aErrorMessage); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetSeeOtherURI(const nsACString& aSeeOtherURI) { + m_seeOtherURI.Assign(aSeeOtherURI); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetSeeOtherURI(nsACString& aSeeOtherURI) { + aSeeOtherURI = m_seeOtherURI; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::IsUrlType(uint32_t type, bool* isType) { + // base class doesn't know about any specific types + NS_ENSURE_ARG(isType); + *isType = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetSearchSession( + nsIMsgSearchSession* aSearchSession) { + if (aSearchSession) m_searchSession = aSearchSession; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetSearchSession( + nsIMsgSearchSession** aSearchSession) { + NS_ENSURE_ARG(aSearchSession); + NS_IF_ADDREF(*aSearchSession = m_searchSession); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////////// +// End nsIMsgMailNewsUrl specific support +//////////////////////////////////////////////////////////////////////////////////// + +//////////////////////////////////////////////////////////////////////////////////// +// Begin nsIURI support +//////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsMsgMailNewsUrl::GetSpec(nsACString& aSpec) { + return m_baseURL->GetSpec(aSpec); +} + +nsresult nsMsgMailNewsUrl::CreateURL(const nsACString& aSpec, nsIURL** aURL) { + nsCOMPtr<nsIURL> url; + nsresult rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID) + .SetSpec(aSpec) + .Finalize(url); + NS_ENSURE_SUCCESS(rv, rv); + url.forget(aURL); + return NS_OK; +} + +#define FILENAME_PART_LEN 10 + +nsresult nsMsgMailNewsUrl::SetSpecInternal(const nsACString& aSpec) { + nsAutoCString spec(aSpec); + // Parse out "filename" attribute if present. + char *start, *end; + start = PL_strcasestr(spec.BeginWriting(), "?filename="); + if (!start) start = PL_strcasestr(spec.BeginWriting(), "&filename="); + if (start) { // Make sure we only get our own value. + end = PL_strcasestr((char*)(start + FILENAME_PART_LEN), "&"); + if (end) { + *end = 0; + mAttachmentFileName = start + FILENAME_PART_LEN; + *end = '&'; + } else + mAttachmentFileName = start + FILENAME_PART_LEN; + } + + // Now, set the rest. + nsresult rv = CreateURL(aSpec, getter_AddRefs(m_baseURL)); + NS_ENSURE_SUCCESS(rv, rv); + + // Check whether the URL is in normalized form. + nsCOMPtr<nsIMsgMessageUrl> msgUrl; + QueryInterface(NS_GET_IID(nsIMsgMessageUrl), getter_AddRefs(msgUrl)); + + nsAutoCString normalizedSpec; + if (!msgUrl || NS_FAILED(msgUrl->GetNormalizedSpec(normalizedSpec))) { + // If we can't get the normalized spec, never QI this to + // nsIURIWithSpecialOrigin. + m_hasNormalizedOrigin = false; + } else { + m_hasNormalizedOrigin = !spec.Equals(normalizedSpec); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetPrePath(nsACString& aPrePath) { + return m_baseURL->GetPrePath(aPrePath); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetScheme(nsACString& aScheme) { + return m_baseURL->GetScheme(aScheme); +} + +nsresult nsMsgMailNewsUrl::SetScheme(const nsACString& aScheme) { + return NS_MutateURI(m_baseURL).SetScheme(aScheme).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetUserPass(nsACString& aUserPass) { + return m_baseURL->GetUserPass(aUserPass); +} + +nsresult nsMsgMailNewsUrl::SetUserPass(const nsACString& aUserPass) { + return NS_MutateURI(m_baseURL).SetUserPass(aUserPass).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetUsername(nsACString& aUsername) { + /* note: this will return an escaped string */ + return m_baseURL->GetUsername(aUsername); +} + +nsresult nsMsgMailNewsUrl::SetUsername(const nsACString& aUsername) { + return NS_MutateURI(m_baseURL).SetUsername(aUsername).Finalize(m_baseURL); +} + +nsresult nsMsgMailNewsUrl::SetUsernameInternal(const nsACString& aUsername) { + return NS_MutateURI(m_baseURL).SetUsername(aUsername).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetPassword(nsACString& aPassword) { + return m_baseURL->GetPassword(aPassword); +} + +nsresult nsMsgMailNewsUrl::SetPassword(const nsACString& aPassword) { + return NS_MutateURI(m_baseURL).SetPassword(aPassword).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetHostPort(nsACString& aHostPort) { + return m_baseURL->GetHostPort(aHostPort); +} + +nsresult nsMsgMailNewsUrl::SetHostPort(const nsACString& aHostPort) { + return NS_MutateURI(m_baseURL).SetHostPort(aHostPort).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetHost(nsACString& aHost) { + return m_baseURL->GetHost(aHost); +} + +nsresult nsMsgMailNewsUrl::SetHost(const nsACString& aHost) { + return NS_MutateURI(m_baseURL).SetHost(aHost).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetPort(int32_t* aPort) { + return m_baseURL->GetPort(aPort); +} + +nsresult nsMsgMailNewsUrl::SetPort(int32_t aPort) { + return NS_MutateURI(m_baseURL).SetPort(aPort).Finalize(m_baseURL); +} + +nsresult nsMsgMailNewsUrl::SetPortInternal(int32_t aPort) { + return NS_MutateURI(m_baseURL).SetPort(aPort).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetPathQueryRef(nsACString& aPath) { + return m_baseURL->GetPathQueryRef(aPath); +} + +nsresult nsMsgMailNewsUrl::SetPathQueryRef(const nsACString& aPath) { + return NS_MutateURI(m_baseURL).SetPathQueryRef(aPath).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetAsciiHost(nsACString& aHostA) { + return m_baseURL->GetAsciiHost(aHostA); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetAsciiHostPort(nsACString& aHostPortA) { + return m_baseURL->GetAsciiHostPort(aHostPortA); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetAsciiSpec(nsACString& aSpecA) { + return m_baseURL->GetAsciiSpec(aSpecA); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetBaseURI(nsIURI** aBaseURI) { + NS_ENSURE_ARG_POINTER(aBaseURI); + return m_baseURL->QueryInterface(NS_GET_IID(nsIURI), (void**)aBaseURI); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::Equals(nsIURI* other, bool* _retval) { + // The passed-in URI might be a mail news url. Pass our inner URL to its + // Equals method. The other mail news url will then pass its inner URL to + // to the Equals method of our inner URL. Other URIs will return false. + if (other) return other->Equals(m_baseURL, _retval); + + return m_baseURL->Equals(other, _retval); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::EqualsExceptRef(nsIURI* other, bool* result) { + // The passed-in URI might be a mail news url. Pass our inner URL to its + // Equals method. The other mail news url will then pass its inner URL to + // to the Equals method of our inner URL. Other URIs will return false. + if (other) return other->EqualsExceptRef(m_baseURL, result); + + return m_baseURL->EqualsExceptRef(other, result); +} + +NS_IMETHODIMP +nsMsgMailNewsUrl::GetSpecIgnoringRef(nsACString& result) { + return m_baseURL->GetSpecIgnoringRef(result); +} + +NS_IMETHODIMP +nsMsgMailNewsUrl::GetDisplaySpec(nsACString& aUnicodeSpec) { + return m_baseURL->GetDisplaySpec(aUnicodeSpec); +} + +NS_IMETHODIMP +nsMsgMailNewsUrl::GetDisplayHostPort(nsACString& aHostPort) { + return m_baseURL->GetDisplayHostPort(aHostPort); +} + +NS_IMETHODIMP +nsMsgMailNewsUrl::GetDisplayHost(nsACString& aHost) { + return m_baseURL->GetDisplayHost(aHost); +} + +NS_IMETHODIMP +nsMsgMailNewsUrl::GetDisplayPrePath(nsACString& aPrePath) { + return m_baseURL->GetDisplayPrePath(aPrePath); +} + +NS_IMETHODIMP +nsMsgMailNewsUrl::GetHasRef(bool* result) { + return m_baseURL->GetHasRef(result); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SchemeIs(const char* aScheme, bool* _retval) { + return m_baseURL->SchemeIs(aScheme, _retval); +} + +nsresult nsMsgMailNewsUrl::Clone(nsIURI** _retval) { + nsresult rv; + nsAutoCString urlSpec; + nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service(); + NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED); + rv = GetSpec(urlSpec); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> newUri; + rv = ioService->NewURI(urlSpec, nullptr, nullptr, getter_AddRefs(newUri)); + NS_ENSURE_SUCCESS(rv, rv); + + // add the msg window to the cloned url + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindowWeak)); + if (msgWindow) { + nsCOMPtr<nsIMsgMailNewsUrl> msgMailNewsUrl = do_QueryInterface(newUri, &rv); + NS_ENSURE_SUCCESS(rv, rv); + msgMailNewsUrl->SetMsgWindow(msgWindow); + } + + newUri.forget(_retval); + return rv; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::Resolve(const nsACString& relativePath, + nsACString& result) { + // only resolve anchor urls....i.e. urls which start with '#' against the + // mailnews url... everything else shouldn't be resolved against mailnews + // urls. + nsresult rv = NS_OK; + + if (relativePath.IsEmpty()) { + // Return base URL. + rv = GetSpec(result); + } else if (!relativePath.IsEmpty() && + relativePath.First() == '#') // an anchor + { + rv = m_baseURL->Resolve(relativePath, result); + } else { + // if relativePath is a complete url with it's own scheme then allow it... + nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service(); + NS_ENSURE_TRUE(ioService, NS_ERROR_UNEXPECTED); + nsAutoCString scheme; + + rv = ioService->ExtractScheme(relativePath, scheme); + // if we have a fully qualified scheme then pass the relative path back as + // the result + if (NS_SUCCEEDED(rv) && !scheme.IsEmpty()) { + result = relativePath; + rv = NS_OK; + } else { + result.Truncate(); + rv = NS_ERROR_FAILURE; + } + } + + return rv; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetDirectory(nsACString& aDirectory) { + return m_baseURL->GetDirectory(aDirectory); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetFileName(nsACString& aFileName) { + if (!mAttachmentFileName.IsEmpty()) { + aFileName = mAttachmentFileName; + return NS_OK; + } + return m_baseURL->GetFileName(aFileName); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetFileBaseName(nsACString& aFileBaseName) { + return m_baseURL->GetFileBaseName(aFileBaseName); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetFileExtension(nsACString& aFileExtension) { + if (!mAttachmentFileName.IsEmpty()) { + int32_t pos = mAttachmentFileName.RFindChar(char16_t('.')); + if (pos > 0) + aFileExtension = + Substring(mAttachmentFileName, pos + 1 /* skip the '.' */); + return NS_OK; + } + return m_baseURL->GetFileExtension(aFileExtension); +} + +nsresult nsMsgMailNewsUrl::SetFileNameInternal(const nsACString& aFileName) { + mAttachmentFileName = aFileName; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetQuery(nsACString& aQuery) { + return m_baseURL->GetQuery(aQuery); +} + +nsresult nsMsgMailNewsUrl::SetQuery(const nsACString& aQuery) { + return NS_MutateURI(m_baseURL).SetQuery(aQuery).Finalize(m_baseURL); +} + +nsresult nsMsgMailNewsUrl::SetQueryInternal(const nsACString& aQuery) { + return NS_MutateURI(m_baseURL).SetQuery(aQuery).Finalize(m_baseURL); +} + +nsresult nsMsgMailNewsUrl::SetQueryWithEncoding( + const nsACString& aQuery, const mozilla::Encoding* aEncoding) { + return NS_MutateURI(m_baseURL) + .SetQueryWithEncoding(aQuery, aEncoding) + .Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetRef(nsACString& aRef) { + return m_baseURL->GetRef(aRef); +} + +nsresult nsMsgMailNewsUrl::SetRef(const nsACString& aRef) { + return NS_MutateURI(m_baseURL).SetRef(aRef).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetFilePath(nsACString& o_DirFile) { + return m_baseURL->GetFilePath(o_DirFile); +} + +nsresult nsMsgMailNewsUrl::SetFilePath(const nsACString& i_DirFile) { + return NS_MutateURI(m_baseURL).SetFilePath(i_DirFile).Finalize(m_baseURL); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetCommonBaseSpec(nsIURI* uri2, + nsACString& result) { + return m_baseURL->GetCommonBaseSpec(uri2, result); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetRelativeSpec(nsIURI* uri2, + nsACString& result) { + return m_baseURL->GetRelativeSpec(uri2, result); +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetMemCacheEntry(nsICacheEntry* memCacheEntry) { + m_memCacheEntry = memCacheEntry; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetMemCacheEntry( + nsICacheEntry** memCacheEntry) { + NS_ENSURE_ARG(memCacheEntry); + nsresult rv = NS_OK; + + if (m_memCacheEntry) { + NS_ADDREF(*memCacheEntry = m_memCacheEntry); + } else { + *memCacheEntry = nullptr; + return NS_ERROR_NULL_POINTER; + } + + return rv; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetMimeHeaders(nsIMimeHeaders** mimeHeaders) { + NS_ENSURE_ARG_POINTER(mimeHeaders); + NS_IF_ADDREF(*mimeHeaders = mMimeHeaders); + return (mMimeHeaders) ? NS_OK : NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetMimeHeaders(nsIMimeHeaders* mimeHeaders) { + mMimeHeaders = mimeHeaders; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::LoadURI(nsIDocShell* docShell, + uint32_t aLoadFlags) { + NS_ENSURE_ARG_POINTER(docShell); + RefPtr<nsDocShellLoadState> loadState = new nsDocShellLoadState(this); + loadState->SetLoadFlags(aLoadFlags); + loadState->SetLoadType(MAKE_LOAD_TYPE(LOAD_NORMAL, aLoadFlags)); + loadState->SetFirstParty(false); + loadState->SetTriggeringPrincipal(nsContentUtils::GetSystemPrincipal()); + return docShell->LoadURI(loadState, false); +} + +#define SAVE_BUF_SIZE FILE_IO_BUFFER_SIZE +class nsMsgSaveAsListener : public nsIStreamListener { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + nsMsgSaveAsListener(nsIFile* aFile, bool addDummyEnvelope); + nsresult SetupMsgWriteStream(nsIFile* aFile, bool addDummyEnvelope); + + protected: + virtual ~nsMsgSaveAsListener(); + nsCOMPtr<nsIOutputStream> m_outputStream; + nsCOMPtr<nsIFile> m_outputFile; + bool m_addDummyEnvelope; + bool m_writtenData; + uint32_t m_leftOver; + char m_dataBuffer[SAVE_BUF_SIZE + + 1]; // temporary buffer for this save operation +}; + +NS_IMPL_ISUPPORTS(nsMsgSaveAsListener, nsIStreamListener, nsIRequestObserver) + +nsMsgSaveAsListener::nsMsgSaveAsListener(nsIFile* aFile, + bool addDummyEnvelope) { + m_outputFile = aFile; + m_writtenData = false; + m_addDummyEnvelope = addDummyEnvelope; + m_leftOver = 0; +} + +nsMsgSaveAsListener::~nsMsgSaveAsListener() {} + +NS_IMETHODIMP nsMsgSaveAsListener::OnStartRequest(nsIRequest* request) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSaveAsListener::OnStopRequest(nsIRequest* request, nsresult aStatus) { + if (m_outputStream) { + m_outputStream->Flush(); + m_outputStream->Close(); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgSaveAsListener::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStream, + uint64_t srcOffset, + uint32_t count) { + nsresult rv; + uint64_t available; + rv = inStream->Available(&available); + if (!m_writtenData) { + m_writtenData = true; + rv = SetupMsgWriteStream(m_outputFile, m_addDummyEnvelope); + NS_ENSURE_SUCCESS(rv, rv); + } + + bool useCanonicalEnding = false; + // We know the request is an nsIChannel we can get a URI from, but this is + // probably bad form. See Bug 1528662. + nsCOMPtr<nsIChannel> channel = do_QueryInterface(request, &rv); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "error QI nsIRequest to nsIChannel failed"); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIURI> uri; + rv = channel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(uri); + if (msgUrl) msgUrl->GetCanonicalLineEnding(&useCanonicalEnding); + + const char* lineEnding = (useCanonicalEnding) ? CRLF : MSG_LINEBREAK; + uint32_t lineEndingLength = (useCanonicalEnding) ? 2 : MSG_LINEBREAK_LEN; + + uint32_t readCount, maxReadCount = SAVE_BUF_SIZE - m_leftOver; + uint32_t writeCount; + char *start, *end, lastCharInPrevBuf = '\0'; + uint32_t linebreak_len = 0; + + while (count > 0) { + if (count < maxReadCount) maxReadCount = count; + rv = inStream->Read(m_dataBuffer + m_leftOver, maxReadCount, &readCount); + if (NS_FAILED(rv)) return rv; + + m_leftOver += readCount; + m_dataBuffer[m_leftOver] = '\0'; + + start = m_dataBuffer; + // make sure we don't insert another LF, accidentally, by ignoring + // second half of CRLF spanning blocks. + if (lastCharInPrevBuf == '\r' && *start == '\n') start++; + + end = PL_strpbrk(start, "\r\n"); + if (end) linebreak_len = (end[0] == '\r' && end[1] == '\n') ? 2 : 1; + + count -= readCount; + maxReadCount = SAVE_BUF_SIZE - m_leftOver; + + if (!end && count > maxReadCount) + // must be a very very long line; sorry cannot handle it + return NS_ERROR_FAILURE; + + while (start && end) { + if (m_outputStream && PL_strncasecmp(start, "X-Mozilla-Status:", 17) && + PL_strncasecmp(start, "X-Mozilla-Status2:", 18) && + PL_strncmp(start, "From - ", 7)) { + rv = m_outputStream->Write(start, end - start, &writeCount); + nsresult tmp = + m_outputStream->Write(lineEnding, lineEndingLength, &writeCount); + if (NS_FAILED(tmp)) { + rv = tmp; + } + } + start = end + linebreak_len; + if (start >= m_dataBuffer + m_leftOver) { + maxReadCount = SAVE_BUF_SIZE; + m_leftOver = 0; + break; + } + end = PL_strpbrk(start, "\r\n"); + if (end) linebreak_len = (end[0] == '\r' && end[1] == '\n') ? 2 : 1; + if (start && !end) { + m_leftOver -= (start - m_dataBuffer); + memcpy(m_dataBuffer, start, + m_leftOver + 1); // including null + maxReadCount = SAVE_BUF_SIZE - m_leftOver; + } + } + if (NS_FAILED(rv)) return rv; + if (end) lastCharInPrevBuf = *end; + } + return rv; + + // rv = m_outputStream->WriteFrom(inStream, std::min(available, count), + // &bytesWritten); +} + +nsresult nsMsgSaveAsListener::SetupMsgWriteStream(nsIFile* aFile, + bool addDummyEnvelope) { + // If the file already exists, delete it, but do this before + // getting the outputstream. + // Due to bug 328027, the nsSaveMsgListener created in + // nsMessenger::SaveAs now opens the stream on the nsIFile + // object, thus creating an empty file. Actual save operations for + // IMAP and NNTP use this nsMsgSaveAsListener here, though, so we + // have to close the stream before deleting the file, else data + // would still be written happily into a now non-existing file. + // (Windows doesn't care, btw, just unixoids do...) + aFile->Remove(false); + + nsresult rv = MsgNewBufferedFileOutputStream(getter_AddRefs(m_outputStream), + aFile, -1, 0666); + NS_ENSURE_SUCCESS(rv, rv); + + if (m_outputStream && addDummyEnvelope) { + nsAutoCString result; + uint32_t writeCount; + + time_t now = time((time_t*)0); + char* ct = ctime(&now); + // Remove the ending new-line character. + ct[24] = '\0'; + result = "From - "; + result += ct; + result += MSG_LINEBREAK; + m_outputStream->Write(result.get(), result.Length(), &writeCount); + + result = "X-Mozilla-Status: 0001"; + result += MSG_LINEBREAK; + result += "X-Mozilla-Status2: 00000000"; + result += MSG_LINEBREAK; + m_outputStream->Write(result.get(), result.Length(), &writeCount); + } + + return rv; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetSaveAsListener( + bool addDummyEnvelope, nsIFile* aFile, nsIStreamListener** aSaveListener) { + NS_ENSURE_ARG_POINTER(aSaveListener); + nsMsgSaveAsListener* saveAsListener = + new nsMsgSaveAsListener(aFile, addDummyEnvelope); + return saveAsListener->QueryInterface(NS_GET_IID(nsIStreamListener), + (void**)aSaveListener); +} + +NS_IMETHODIMP +nsMsgMailNewsUrl::SetFailedSecInfo(nsITransportSecurityInfo* secInfo) { + mFailedSecInfo = secInfo; + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetFailedSecInfo( + nsITransportSecurityInfo** secInfo) { + NS_ENSURE_ARG_POINTER(secInfo); + NS_IF_ADDREF(*secInfo = mFailedSecInfo); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::SetFolder(nsIMsgFolder* /* aFolder */) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetFolder(nsIMsgFolder** /* aFolder */) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgMailNewsUrl::GetIsMessageUri(bool* aIsMessageUri) { + NS_ENSURE_ARG(aIsMessageUri); + nsAutoCString scheme; + m_baseURL->GetScheme(scheme); + *aIsMessageUri = StringEndsWith(scheme, "-message"_ns); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsMsgMailNewsUrl::Mutator, nsIURISetters, nsIURIMutator) + +NS_IMETHODIMP +nsMsgMailNewsUrl::Mutate(nsIURIMutator** aMutator) { + RefPtr<nsMsgMailNewsUrl::Mutator> mutator = new nsMsgMailNewsUrl::Mutator(); + nsresult rv = mutator->InitFromURI(this); + if (NS_FAILED(rv)) { + return rv; + } + mutator.forget(aMutator); + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgMailNewsUrl.h b/comm/mailnews/base/src/nsMsgMailNewsUrl.h new file mode 100644 index 0000000000..21d56e8e65 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgMailNewsUrl.h @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgMailNewsUrl_h___ +#define nsMsgMailNewsUrl_h___ + +#include "msgCore.h" +#include "nscore.h" +#include "nsISupports.h" +#include "nsIUrlListener.h" +#include "nsTObserverArray.h" +#include "nsIMsgWindow.h" +#include "nsIMsgStatusFeedback.h" +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "nsIMimeHeaders.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIURL.h" +#include "nsIURIWithSpecialOrigin.h" +#include "nsILoadGroup.h" +#include "nsIMsgSearchSession.h" +#include "nsICacheEntry.h" +#include "nsIWeakReferenceUtils.h" +#include "nsString.h" +#include "nsIURIMutator.h" +#include "nsISerializable.h" +#include "nsIClassInfo.h" +#include "nsITransportSecurityInfo.h" + +/////////////////////////////////////////////////////////////////////////////////// +// Okay, I found that all of the mail and news url interfaces needed to support +// several common interfaces (in addition to those provided through nsIURI). +// So I decided to group them all in this implementation so we don't have to +// duplicate the code. +// +////////////////////////////////////////////////////////////////////////////////// + +class NS_MSG_BASE nsMsgMailNewsUrl : public nsIMsgMailNewsUrl, + public nsIURIWithSpecialOrigin, + public nsISerializable, + public nsIClassInfo { + public: + nsMsgMailNewsUrl(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMSGMAILNEWSURL + NS_DECL_NSIURI + NS_DECL_NSIURL + NS_DECL_NSIURIWITHSPECIALORIGIN + NS_DECL_NSISERIALIZABLE + NS_DECL_NSICLASSINFO + + protected: + virtual nsresult Clone(nsIURI** _retval); + virtual nsresult SetScheme(const nsACString& aScheme); + virtual nsresult SetUserPass(const nsACString& aUserPass); + virtual nsresult SetUsername(const nsACString& aUsername); + virtual nsresult SetPassword(const nsACString& aPassword); + virtual nsresult SetHostPort(const nsACString& aHostPort); + virtual nsresult SetHost(const nsACString& aHost); + virtual nsresult SetPort(int32_t aPort); + virtual nsresult SetPathQueryRef(const nsACString& aPath); + virtual nsresult SetRef(const nsACString& aRef); + virtual nsresult SetFilePath(const nsACString& aFilePath); + virtual nsresult SetQuery(const nsACString& aQuery); + virtual nsresult SetQueryWithEncoding(const nsACString& aQuery, + const mozilla::Encoding* aEncoding); + virtual nsresult CreateURL(const nsACString& aSpec, + nsIURL** aURL); // nsMailboxUrl overrides this. + + public: + class Mutator : public nsIURIMutator, + public BaseURIMutator<nsMsgMailNewsUrl> { + NS_DECL_ISUPPORTS + NS_FORWARD_SAFE_NSIURISETTERS_RET(mURI) + + NS_IMETHOD Deserialize(const mozilla::ipc::URIParams& aParams) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD Finalize(nsIURI** aURI) override { + mURI.forget(aURI); + return NS_OK; + } + + NS_IMETHOD SetSpec(const nsACString& aSpec, + nsIURIMutator** aMutator) override { + if (aMutator) NS_ADDREF(*aMutator = this); + return InitFromSpec(aSpec); + } + + explicit Mutator() {} + + private: + virtual ~Mutator() {} + + friend class nsMsgMailNewsUrl; + }; + friend BaseURIMutator<nsMsgMailNewsUrl>; + + protected: + virtual ~nsMsgMailNewsUrl(); + + nsCOMPtr<nsIURL> m_baseURL; + nsCOMPtr<nsIURI> m_normalizedOrigin; + nsWeakPtr m_statusFeedbackWeak; + nsWeakPtr m_msgWindowWeak; + nsWeakPtr m_loadGroupWeak; + nsCOMPtr<nsIMimeHeaders> mMimeHeaders; + nsCOMPtr<nsIMsgSearchSession> m_searchSession; + nsCOMPtr<nsICacheEntry> m_memCacheEntry; + nsCString m_errorCode; + nsCString m_seeOtherURI; + nsString m_errorMessage; + nsString m_errorParameters; + int64_t mMaxProgress; + bool m_runningUrl; + bool m_updatingFolder; + bool m_msgIsInLocalCache; + bool m_suppressErrorMsgs; + bool m_hasNormalizedOrigin; + + // the following field is really a bit of a hack to make + // open attachments work. The external applications code sometimes tries to + // figure out the right handler to use by looking at the file extension of the + // url we are trying to load. Unfortunately, the attachment file name really + // isn't part of the url string....so we'll store it here...and if the url we + // are running is an attachment url, we'll set it here. Then when the helper + // apps code asks us for it, we'll return the right value. + nsCString mAttachmentFileName; + + nsTObserverArray<nsCOMPtr<nsIUrlListener> > mUrlListeners; + + // Security info from the socket transport (if any), after a failed operation. + // Here so that urlListeners can access and handle bad certificates in + // their OnStopRunningUrl() callback. + nsCOMPtr<nsITransportSecurityInfo> mFailedSecInfo; +}; + +#endif /* nsMsgMailNewsUrl_h___ */ diff --git a/comm/mailnews/base/src/nsMsgMailSession.cpp b/comm/mailnews/base/src/nsMsgMailSession.cpp new file mode 100644 index 0000000000..ac34d5d487 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgMailSession.cpp @@ -0,0 +1,671 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" // for pre-compiled headers +#include "nsMsgMailSession.h" +#include "nsIMsgMessageService.h" +#include "nsMsgUtils.h" +#include "nsIMsgAccountManager.h" +#include "nsIChromeRegistry.h" +#include "nsIDirectoryService.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsPIDOMWindow.h" +#include "nsIDocShell.h" +#include "mozilla/dom/Document.h" +#include "nsIObserverService.h" +#include "nsIAppStartup.h" +#include "nsISupportsPrimitives.h" +#include "nsIAppShellService.h" +#include "nsAppShellCID.h" +#include "nsIWindowMediator.h" +#include "nsIWindowWatcher.h" +#include "nsIMsgMailNewsUrl.h" +#include "prcmon.h" +#include "nsThreadUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIProperties.h" +#include "mozilla/Services.h" +#include "mozilla/dom/Element.h" +#include "mozilla/Components.h" +#include "nsFocusManager.h" +#include "nsIPromptService.h" +#include "nsEmbedCID.h" + +NS_IMPL_ISUPPORTS(nsMsgMailSession, nsIMsgMailSession, nsIFolderListener) + +nsMsgMailSession::nsMsgMailSession() {} + +nsMsgMailSession::~nsMsgMailSession() { Shutdown(); } + +nsresult nsMsgMailSession::Init() { + // Ensures the shutdown service is initialised + nsresult rv; + nsCOMPtr<nsIMsgShutdownService> shutdownService = + do_GetService("@mozilla.org/messenger/msgshutdownservice;1", &rv); + return rv; +} + +nsresult nsMsgMailSession::Shutdown() { return NS_OK; } + +NS_IMETHODIMP nsMsgMailSession::AddFolderListener(nsIFolderListener* aListener, + uint32_t aNotifyFlags) { + NS_ENSURE_ARG_POINTER(aListener); + + // we don't care about the notification flags for equivalence purposes + size_t index = mListeners.IndexOf(aListener); + NS_ASSERTION(index == size_t(-1), "tried to add duplicate listener"); + if (index == size_t(-1)) { + folderListener newListener(aListener, aNotifyFlags); + mListeners.AppendElement(newListener); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailSession::RemoveFolderListener( + nsIFolderListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + + mListeners.RemoveElement(aListener); + return NS_OK; +} + +#define NOTIFY_FOLDER_LISTENERS(propertyflag_, propertyfunc_, params_) \ + PR_BEGIN_MACRO \ + nsTObserverArray<folderListener>::ForwardIterator iter(mListeners); \ + while (iter.HasMore()) { \ + const folderListener& fL = iter.GetNext(); \ + if (fL.mNotifyFlags & nsIFolderListener::propertyflag_) \ + fL.mListener->propertyfunc_ params_; \ + } \ + PR_END_MACRO + +NS_IMETHODIMP +nsMsgMailSession::OnFolderPropertyChanged(nsIMsgFolder* aItem, + const nsACString& aProperty, + const nsACString& aOldValue, + const nsACString& aNewValue) { + NOTIFY_FOLDER_LISTENERS(propertyChanged, OnFolderPropertyChanged, + (aItem, aProperty, aOldValue, aNewValue)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgMailSession::OnFolderUnicharPropertyChanged(nsIMsgFolder* aItem, + const nsACString& aProperty, + const nsAString& aOldValue, + const nsAString& aNewValue) { + NOTIFY_FOLDER_LISTENERS(unicharPropertyChanged, + OnFolderUnicharPropertyChanged, + (aItem, aProperty, aOldValue, aNewValue)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgMailSession::OnFolderIntPropertyChanged(nsIMsgFolder* aItem, + const nsACString& aProperty, + int64_t aOldValue, + int64_t aNewValue) { + NOTIFY_FOLDER_LISTENERS(intPropertyChanged, OnFolderIntPropertyChanged, + (aItem, aProperty, aOldValue, aNewValue)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgMailSession::OnFolderBoolPropertyChanged(nsIMsgFolder* aItem, + const nsACString& aProperty, + bool aOldValue, bool aNewValue) { + NOTIFY_FOLDER_LISTENERS(boolPropertyChanged, OnFolderBoolPropertyChanged, + (aItem, aProperty, aOldValue, aNewValue)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgMailSession::OnFolderPropertyFlagChanged(nsIMsgDBHdr* aItem, + const nsACString& aProperty, + uint32_t aOldValue, + uint32_t aNewValue) { + NOTIFY_FOLDER_LISTENERS(propertyFlagChanged, OnFolderPropertyFlagChanged, + (aItem, aProperty, aOldValue, aNewValue)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailSession::OnFolderAdded(nsIMsgFolder* parent, + nsIMsgFolder* child) { + NOTIFY_FOLDER_LISTENERS(added, OnFolderAdded, (parent, child)); + return NS_OK; +} +NS_IMETHODIMP nsMsgMailSession::OnMessageAdded(nsIMsgFolder* parent, + nsIMsgDBHdr* msg) { + NOTIFY_FOLDER_LISTENERS(added, OnMessageAdded, (parent, msg)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailSession::OnFolderRemoved(nsIMsgFolder* parent, + nsIMsgFolder* child) { + NOTIFY_FOLDER_LISTENERS(removed, OnFolderRemoved, (parent, child)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailSession::OnMessageRemoved(nsIMsgFolder* parent, + nsIMsgDBHdr* msg) { + NOTIFY_FOLDER_LISTENERS(removed, OnMessageRemoved, (parent, msg)); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailSession::OnFolderEvent(nsIMsgFolder* aFolder, + const nsACString& aEvent) { + NOTIFY_FOLDER_LISTENERS(event, OnFolderEvent, (aFolder, aEvent)); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgMailSession::AddUserFeedbackListener( + nsIMsgUserFeedbackListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + + size_t index = mFeedbackListeners.IndexOf(aListener); + NS_ASSERTION(index == size_t(-1), "tried to add duplicate listener"); + if (index == size_t(-1)) mFeedbackListeners.AppendElement(aListener); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgMailSession::RemoveUserFeedbackListener( + nsIMsgUserFeedbackListener* aListener) { + NS_ENSURE_ARG_POINTER(aListener); + + mFeedbackListeners.RemoveElement(aListener); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgMailSession::AlertUser(const nsAString& aMessage, + nsIMsgMailNewsUrl* aUrl) { + bool listenersNotified = false; + nsTObserverArray<nsCOMPtr<nsIMsgUserFeedbackListener>>::ForwardIterator iter( + mFeedbackListeners); + nsCOMPtr<nsIMsgUserFeedbackListener> listener; + + while (iter.HasMore()) { + bool notified = false; + listener = iter.GetNext(); + listener->OnAlert(aMessage, aUrl, ¬ified); + listenersNotified = listenersNotified || notified; + } + + // If the listeners notified the user, then we don't need to. Also exit if + // aUrl is null because we won't have a nsIMsgWindow in that case. + if (listenersNotified || !aUrl) return NS_OK; + + // If the url hasn't got a message window, then the error was a generated as a + // result of background activity (e.g. autosync, biff, etc), and hence we + // shouldn't prompt either. + nsCOMPtr<nsIMsgWindow> msgWindow; + aUrl->GetMsgWindow(getter_AddRefs(msgWindow)); + + if (!msgWindow) return NS_OK; + + nsCOMPtr<mozIDOMWindowProxy> domWindow; + msgWindow->GetDomWindow(getter_AddRefs(domWindow)); + + nsresult rv; + nsCOMPtr<nsIPromptService> dlgService( + do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + dlgService->Alert(domWindow, nullptr, PromiseFlatString(aMessage).get()); + + return NS_OK; +} + +nsresult nsMsgMailSession::GetTopmostMsgWindow(nsIMsgWindow** aMsgWindow) { + NS_ENSURE_ARG_POINTER(aMsgWindow); + + *aMsgWindow = nullptr; + + uint32_t count = mWindows.Count(); + + if (count == 1) { + NS_ADDREF(*aMsgWindow = mWindows[0]); + return (*aMsgWindow) ? NS_OK : NS_ERROR_FAILURE; + } else if (count > 1) { + // If multiple message windows then we have lots more work. + nsresult rv; + + // The msgWindows array does not hold z-order info. Use mediator to get + // the top most window then match that with the msgWindows array. + nsCOMPtr<nsIWindowMediator> windowMediator = + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISimpleEnumerator> windowEnum; + + rv = windowMediator->GetEnumerator(nullptr, getter_AddRefs(windowEnum)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> windowSupports; + nsCOMPtr<nsPIDOMWindowOuter> topMostWindow; + nsAutoString windowType; + bool more; + + // loop to get the top most with attribute "mail:3pane" or + // "mail:messageWindow" + windowEnum->HasMoreElements(&more); + while (more) { + rv = windowEnum->GetNext(getter_AddRefs(windowSupports)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(windowSupports, NS_ERROR_FAILURE); + + nsCOMPtr<nsPIDOMWindowOuter> window = + do_QueryInterface(windowSupports, &rv); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(window, NS_ERROR_FAILURE); + + mozilla::dom::Document* domDocument = window->GetDoc(); + NS_ENSURE_TRUE(domDocument, NS_ERROR_FAILURE); + + mozilla::dom::Element* domElement = domDocument->GetDocumentElement(); + NS_ENSURE_TRUE(domElement, NS_ERROR_FAILURE); + + domElement->GetAttribute(u"windowtype"_ns, windowType); + if (windowType.EqualsLiteral("mail:3pane") || + windowType.EqualsLiteral("mail:messageWindow")) { + // topMostWindow is the last 3pane/messageWindow found, not necessarily + // the top most. + topMostWindow = window; + RefPtr<nsFocusManager> fm = nsFocusManager::GetFocusManager(); + nsCOMPtr<mozIDOMWindowProxy> currentWindow = + do_QueryInterface(windowSupports, &rv); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<mozIDOMWindowProxy> activeWindow; + rv = fm->GetActiveWindow(getter_AddRefs(activeWindow)); + NS_ENSURE_SUCCESS(rv, rv); + if (currentWindow == activeWindow) { + // We are sure topMostWindow is really the top most now. + break; + } + } + + windowEnum->HasMoreElements(&more); + } + + // identified the top most window + if (topMostWindow) { + // use this for the match + nsIDocShell* topDocShell = topMostWindow->GetDocShell(); + + // loop for the msgWindow array to find the match + nsCOMPtr<nsIDocShell> docShell; + + while (count) { + nsIMsgWindow* msgWindow = mWindows[--count]; + + rv = msgWindow->GetRootDocShell(getter_AddRefs(docShell)); + NS_ENSURE_SUCCESS(rv, rv); + + if (topDocShell == docShell) { + NS_IF_ADDREF(*aMsgWindow = msgWindow); + break; + } + } + } + } + + return (*aMsgWindow) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgMailSession::AddMsgWindow(nsIMsgWindow* msgWindow) { + NS_ENSURE_ARG_POINTER(msgWindow); + + mWindows.AppendObject(msgWindow); + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailSession::RemoveMsgWindow(nsIMsgWindow* msgWindow) { + mWindows.RemoveObject(msgWindow); + // Mac keeps a hidden window open so the app doesn't shut down when + // the last window is closed. So don't shutdown the account manager in that + // case. Similarly, for suite, we don't want to disable mailnews when the + // last mail window is closed. +#if !defined(XP_MACOSX) && !defined(MOZ_SUITE) + if (!mWindows.Count()) { + nsresult rv; + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + if (NS_FAILED(rv)) return rv; + accountManager->CleanupOnExit(); + } +#endif + return NS_OK; +} + +NS_IMETHODIMP nsMsgMailSession::IsFolderOpenInWindow(nsIMsgFolder* folder, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + + *aResult = false; + + uint32_t count = mWindows.Count(); + + for (uint32_t i = 0; i < count; i++) { + nsCOMPtr<nsIMsgFolder> openFolder; + mWindows[i]->GetOpenFolder(getter_AddRefs(openFolder)); + if (folder == openFolder.get()) { + *aResult = true; + break; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgMailSession::ConvertMsgURIToMsgURL(const nsACString& aURI, + nsIMsgWindow* aMsgWindow, + nsACString& aURL) { + // convert the rdf msg uri into a url that represents the message... + nsCOMPtr<nsIMsgMessageService> msgService; + nsresult rv = GetMessageServiceFromURI(aURI, getter_AddRefs(msgService)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_NULL_POINTER); + + nsCOMPtr<nsIURI> tURI; + rv = msgService->GetUrlForUri(aURI, aMsgWindow, getter_AddRefs(tURI)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_NULL_POINTER); + + rv = tURI->GetSpec(aURL); + return rv; +} + +//------------------------------------------------------------------------- +// GetSelectedLocaleDataDir - If a locale is selected, appends the selected +// locale to the defaults data dir and returns +// that new defaults data dir +//------------------------------------------------------------------------- +nsresult nsMsgMailSession::GetSelectedLocaleDataDir(nsIFile* defaultsDir) { + NS_ENSURE_ARG_POINTER(defaultsDir); + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// GetDataFilesDir - Gets the application's default folder and then appends the +// subdirectory named passed in as param dirName. If there is +// a selected locale, will append that to the dir path before +// returning the value +//----------------------------------------------------------------------------- +NS_IMETHODIMP +nsMsgMailSession::GetDataFilesDir(const char* dirName, nsIFile** dataFilesDir) { + NS_ENSURE_ARG_POINTER(dirName); + NS_ENSURE_ARG_POINTER(dataFilesDir); + + nsresult rv; + nsCOMPtr<nsIProperties> directoryService = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> defaultsDir; + rv = directoryService->Get(NS_APP_DEFAULTS_50_DIR, NS_GET_IID(nsIFile), + getter_AddRefs(defaultsDir)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = defaultsDir->AppendNative(nsDependentCString(dirName)); + if (NS_SUCCEEDED(rv)) rv = GetSelectedLocaleDataDir(defaultsDir); + + defaultsDir.forget(dataFilesDir); + + return rv; +} + +/********************************************************************************/ + +NS_IMPL_ISUPPORTS(nsMsgShutdownService, nsIMsgShutdownService, nsIUrlListener, + nsIObserver) + +nsMsgShutdownService::nsMsgShutdownService() + : mTaskIndex(0), + mQuitMode(nsIAppStartup::eAttemptQuit), + mProcessedShutdown(false), + mQuitForced(false), + mReadyToQuit(false) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(this, "quit-application-requested", false); + observerService->AddObserver(this, "quit-application-granted", false); + observerService->AddObserver(this, "quit-application", false); + } +} + +nsMsgShutdownService::~nsMsgShutdownService() { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->RemoveObserver(this, "quit-application-requested"); + observerService->RemoveObserver(this, "quit-application-granted"); + observerService->RemoveObserver(this, "quit-application"); + } +} + +nsresult nsMsgShutdownService::ProcessNextTask() { + bool shutdownTasksDone = true; + + uint32_t count = mShutdownTasks.Length(); + if (mTaskIndex < count) { + shutdownTasksDone = false; + + nsCOMPtr<nsIMsgShutdownTask> curTask = mShutdownTasks[mTaskIndex]; + nsString taskName; + curTask->GetCurrentTaskName(taskName); + SetStatusText(taskName); + + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1"); + NS_ENSURE_TRUE(mailSession, NS_ERROR_FAILURE); + + nsCOMPtr<nsIMsgWindow> topMsgWindow; + mailSession->GetTopmostMsgWindow(getter_AddRefs(topMsgWindow)); + + bool taskIsRunning = true; + nsresult rv = curTask->DoShutdownTask(this, topMsgWindow, &taskIsRunning); + if (NS_FAILED(rv) || !taskIsRunning) { + // We have failed, let's go on to the next task. + mTaskIndex++; + mMsgProgress->OnProgressChange(nullptr, nullptr, 0, 0, + (int32_t)mTaskIndex, count); + ProcessNextTask(); + } + } + + if (shutdownTasksDone) { + if (mMsgProgress) + mMsgProgress->OnStateChange(nullptr, nullptr, + nsIWebProgressListener::STATE_STOP, NS_OK); + AttemptShutdown(); + } + + return NS_OK; +} + +void nsMsgShutdownService::AttemptShutdown() { + if (mQuitForced) { + PR_CEnterMonitor(this); + mReadyToQuit = true; + PR_CNotifyAll(this); + PR_CExitMonitor(this); + } else { + nsCOMPtr<nsIAppStartup> appStartup = + mozilla::components::AppStartup::Service(); + NS_ENSURE_TRUE_VOID(appStartup); + bool userAllowedQuit = true; + NS_ENSURE_SUCCESS_VOID(appStartup->Quit(mQuitMode, 0, &userAllowedQuit)); + } +} + +NS_IMETHODIMP nsMsgShutdownService::SetShutdownListener( + nsIWebProgressListener* inListener) { + NS_ENSURE_TRUE(mMsgProgress, NS_ERROR_FAILURE); + mMsgProgress->RegisterListener(inListener); + return NS_OK; +} + +NS_IMETHODIMP nsMsgShutdownService::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + // Due to bug 459376 we don't always get quit-application-requested and + // quit-application-granted. quit-application-requested is preferred, but if + // we don't then we have to hook onto quit-application, but we don't want + // to do the checking twice so we set some flags to prevent that. + if (!strcmp(aTopic, "quit-application-granted")) { + // Quit application has been requested and granted, therefore we will shut + // down. + mProcessedShutdown = true; + return NS_OK; + } + + // If we've already processed a shutdown notification, no need to do it again. + if (!strcmp(aTopic, "quit-application")) { + if (mProcessedShutdown) + return NS_OK; + else + mQuitForced = true; + } + + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + NS_ENSURE_STATE(observerService); + + nsCOMPtr<nsISimpleEnumerator> listenerEnum; + nsresult rv = observerService->EnumerateObservers( + "msg-shutdown", getter_AddRefs(listenerEnum)); + if (NS_SUCCEEDED(rv) && listenerEnum) { + bool hasMore; + listenerEnum->HasMoreElements(&hasMore); + if (!hasMore) return NS_OK; + + while (hasMore) { + nsCOMPtr<nsISupports> curObject; + listenerEnum->GetNext(getter_AddRefs(curObject)); + + nsCOMPtr<nsIMsgShutdownTask> curTask = do_QueryInterface(curObject); + if (curTask) { + bool shouldRunTask; + curTask->GetNeedsToRunTask(&shouldRunTask); + if (shouldRunTask) mShutdownTasks.AppendObject(curTask); + } + + listenerEnum->HasMoreElements(&hasMore); + } + + if (mShutdownTasks.Count() < 1) return NS_ERROR_FAILURE; + + mTaskIndex = 0; + + mMsgProgress = do_CreateInstance("@mozilla.org/messenger/progress;1"); + NS_ENSURE_TRUE(mMsgProgress, NS_ERROR_FAILURE); + + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1"); + NS_ENSURE_TRUE(mailSession, NS_ERROR_FAILURE); + + nsCOMPtr<nsIMsgWindow> topMsgWindow; + mailSession->GetTopmostMsgWindow(getter_AddRefs(topMsgWindow)); + + nsCOMPtr<mozIDOMWindowProxy> internalDomWin; + if (topMsgWindow) + topMsgWindow->GetDomWindow(getter_AddRefs(internalDomWin)); + + if (!internalDomWin) { + // First see if there is a window open. + nsCOMPtr<nsIWindowMediator> winMed = + do_GetService(NS_WINDOWMEDIATOR_CONTRACTID); + winMed->GetMostRecentWindow(nullptr, getter_AddRefs(internalDomWin)); + + // If not use the hidden window. + if (!internalDomWin) { + nsCOMPtr<nsIAppShellService> appShell( + do_GetService(NS_APPSHELLSERVICE_CONTRACTID)); + appShell->GetHiddenDOMWindow(getter_AddRefs(internalDomWin)); + NS_ENSURE_TRUE(internalDomWin, + NS_ERROR_FAILURE); // bail if we don't get a window. + } + } + + if (!mQuitForced) { + nsCOMPtr<nsISupportsPRBool> stopShutdown = do_QueryInterface(aSubject); + stopShutdown->SetData(true); + + // If the attempted quit was a restart, be sure to restart the app once + // the tasks have been run. This is usually the case when addons or + // updates are going to be installed. + if (aData && nsDependentString(aData).EqualsLiteral("restart")) + mQuitMode |= nsIAppStartup::eRestart; + } + + mMsgProgress->OpenProgressDialog( + internalDomWin, topMsgWindow, + "chrome://messenger/content/shutdownWindow.xhtml", false, nullptr); + + if (mQuitForced) { + nsCOMPtr<nsIThread> thread(do_GetCurrentThread()); + + mReadyToQuit = false; + while (!mReadyToQuit) { + PR_CEnterMonitor(this); + // Waiting for 50 milliseconds + PR_CWait(this, PR_MicrosecondsToInterval(50000UL)); + PR_CExitMonitor(this); + NS_ProcessPendingEvents(thread); + } + } + } + + return NS_OK; +} + +// nsIUrlListener +NS_IMETHODIMP nsMsgShutdownService::OnStartRunningUrl(nsIURI* url) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgShutdownService::OnStopRunningUrl(nsIURI* url, + nsresult aExitCode) { + mTaskIndex++; + + if (mMsgProgress) { + int32_t numTasks = mShutdownTasks.Count(); + mMsgProgress->OnProgressChange(nullptr, nullptr, 0, 0, (int32_t)mTaskIndex, + numTasks); + } + + ProcessNextTask(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgShutdownService::GetNumTasks(int32_t* inNumTasks) { + *inNumTasks = mShutdownTasks.Count(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgShutdownService::StartShutdownTasks() { + ProcessNextTask(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgShutdownService::CancelShutdownTasks() { + AttemptShutdown(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgShutdownService::SetStatusText( + const nsAString& inStatusString) { + nsString statusString(inStatusString); + if (mMsgProgress) + mMsgProgress->OnStatusChange(nullptr, nullptr, NS_OK, + nsString(statusString).get()); + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgMailSession.h b/comm/mailnews/base/src/nsMsgMailSession.h new file mode 100644 index 0000000000..09cea632e3 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgMailSession.h @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgMailSession_h___ +#define nsMsgMailSession_h___ + +#include "nsIMsgMailSession.h" +#include "nsISupports.h" +#include "nsCOMPtr.h" +#include "nsIMsgStatusFeedback.h" +#include "nsIMsgWindow.h" +#include "nsCOMArray.h" +#include "nsIMsgShutdown.h" +#include "nsIObserver.h" +#include "nsIMsgProgress.h" +#include "nsTObserverArray.h" +#include "nsIMsgUserFeedbackListener.h" +#include "nsIUrlListener.h" + +/////////////////////////////////////////////////////////////////////////////////// +// The mail session is a replacement for the old 4.x MSG_Master object. It +// contains mail session generic information such as the user's current mail +// identity, .... I'm starting this off as an empty interface and as people feel +// they need to add more information to it, they can. I think this is a better +// approach than trying to port over the old MSG_Master in its entirety as that +// had a lot of cruft in it.... +////////////////////////////////////////////////////////////////////////////////// + +// nsMsgMailSession also implements nsIFolderListener, in order to relay +// notifications to its registered listeners. +// Calling a method on the MailSession causes that method to be invoked upon +// all the registered listeners (but not listeners directly attached to +// folders!) +// In normal operation, most notifications will originate from the +// nsIMsgFolder.Notify*() functions, which invoke both folder-local +// listeners, and the global MailSession-registered ones). +class nsMsgMailSession : public nsIMsgMailSession, public nsIFolderListener { + public: + nsMsgMailSession(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMSGMAILSESSION + NS_DECL_NSIFOLDERLISTENER + + nsresult Init(); + nsresult GetSelectedLocaleDataDir(nsIFile* defaultsDir); + + protected: + virtual ~nsMsgMailSession(); + + struct folderListener { + nsCOMPtr<nsIFolderListener> mListener; + uint32_t mNotifyFlags; + + folderListener(nsIFolderListener* aListener, uint32_t aNotifyFlags) + : mListener(aListener), mNotifyFlags(aNotifyFlags) {} + folderListener(const folderListener& aListener) + : mListener(aListener.mListener), + mNotifyFlags(aListener.mNotifyFlags) {} + ~folderListener() {} + + int operator==(nsIFolderListener* aListener) const { + return mListener == aListener; + } + int operator==(const folderListener& aListener) const { + return mListener == aListener.mListener && + mNotifyFlags == aListener.mNotifyFlags; + } + }; + + nsTObserverArray<folderListener> mListeners; + nsTObserverArray<nsCOMPtr<nsIMsgUserFeedbackListener> > mFeedbackListeners; + + nsCOMArray<nsIMsgWindow> mWindows; + // stick this here temporarily + nsCOMPtr<nsIMsgWindow> m_temporaryMsgWindow; +}; + +/********************************************************************************/ + +class nsMsgShutdownService : public nsIMsgShutdownService, + public nsIUrlListener, + public nsIObserver { + public: + nsMsgShutdownService(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGSHUTDOWNSERVICE + NS_DECL_NSIURLLISTENER + NS_DECL_NSIOBSERVER + + protected: + nsresult ProcessNextTask(); + void AttemptShutdown(); + + private: + virtual ~nsMsgShutdownService(); + + nsCOMArray<nsIMsgShutdownTask> mShutdownTasks; + nsCOMPtr<nsIMsgProgress> mMsgProgress; + uint32_t mTaskIndex; + uint32_t mQuitMode; + bool mProcessedShutdown; + bool mQuitForced; + bool mReadyToQuit; +}; + +#endif /* nsMsgMailSession_h__ */ diff --git a/comm/mailnews/base/src/nsMsgOfflineManager.cpp b/comm/mailnews/base/src/nsMsgOfflineManager.cpp new file mode 100644 index 0000000000..7d6935fc89 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgOfflineManager.cpp @@ -0,0 +1,352 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * The offline manager service - manages going online and offline, and + * synchronization + */ +#include "msgCore.h" +#include "netCore.h" +#include "nsMsgOfflineManager.h" +#include "nsIServiceManager.h" +#include "nsIImapService.h" +#include "nsIMsgSendLater.h" +#include "nsIMsgAccountManager.h" +#include "nsIIOService.h" +#include "nsNetCID.h" +#include "nsINntpService.h" +#include "nsIMsgStatusFeedback.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Components.h" + +#define NS_MSGSENDLATER_CID \ + { /* E15C83F1-1CF4-11d3-8EF0-00A024A7D144 */ \ + 0xe15c83f1, 0x1cf4, 0x11d3, { \ + 0x8e, 0xf0, 0x0, 0xa0, 0x24, 0xa7, 0xd1, 0x44 \ + } \ + } +static NS_DEFINE_CID(kMsgSendLaterCID, NS_MSGSENDLATER_CID); + +NS_IMPL_ISUPPORTS(nsMsgOfflineManager, nsIMsgOfflineManager, + nsIMsgSendLaterListener, nsIObserver, + nsISupportsWeakReference, nsIUrlListener) + +nsMsgOfflineManager::nsMsgOfflineManager() + : m_inProgress(false), + m_sendUnsentMessages(false), + m_downloadNews(false), + m_downloadMail(false), + m_playbackOfflineImapOps(false), + m_goOfflineWhenDone(false), + m_curState(eNoState), + m_curOperation(eNoOp) {} + +nsMsgOfflineManager::~nsMsgOfflineManager() {} + +/* attribute nsIMsgWindow window; */ +NS_IMETHODIMP nsMsgOfflineManager::GetWindow(nsIMsgWindow** aWindow) { + NS_ENSURE_ARG(aWindow); + NS_IF_ADDREF(*aWindow = m_window); + return NS_OK; +} +NS_IMETHODIMP nsMsgOfflineManager::SetWindow(nsIMsgWindow* aWindow) { + m_window = aWindow; + if (m_window) + m_window->GetStatusFeedback(getter_AddRefs(m_statusFeedback)); + else + m_statusFeedback = nullptr; + return NS_OK; +} + +/* attribute boolean inProgress; */ +NS_IMETHODIMP nsMsgOfflineManager::GetInProgress(bool* aInProgress) { + NS_ENSURE_ARG(aInProgress); + *aInProgress = m_inProgress; + return NS_OK; +} + +NS_IMETHODIMP nsMsgOfflineManager::SetInProgress(bool aInProgress) { + m_inProgress = aInProgress; + return NS_OK; +} + +nsresult nsMsgOfflineManager::StopRunning(nsresult exitStatus) { + m_inProgress = false; + return exitStatus; +} + +nsresult nsMsgOfflineManager::AdvanceToNextState(nsresult exitStatus) { + // NS_BINDING_ABORTED is used for the user pressing stop, which + // should cause us to abort the offline process. Other errors + // should allow us to continue. + if (exitStatus == NS_BINDING_ABORTED) { + return StopRunning(exitStatus); + } + if (m_curOperation == eGoingOnline) { + switch (m_curState) { + case eNoState: + + m_curState = eSendingUnsent; + if (m_sendUnsentMessages) { + SendUnsentMessages(); + } else + AdvanceToNextState(NS_OK); + break; + case eSendingUnsent: + + m_curState = eSynchronizingOfflineImapChanges; + if (m_playbackOfflineImapOps) + return SynchronizeOfflineImapChanges(); + else + AdvanceToNextState(NS_OK); // recurse to next state. + break; + case eSynchronizingOfflineImapChanges: + m_curState = eDone; + return StopRunning(exitStatus); + default: + NS_ASSERTION(false, "unhandled current state when going online"); + } + } else if (m_curOperation == eDownloadingForOffline) { + switch (m_curState) { + case eNoState: + m_curState = eDownloadingNews; + if (m_downloadNews) + DownloadOfflineNewsgroups(); + else + AdvanceToNextState(NS_OK); + break; + case eSendingUnsent: + if (m_goOfflineWhenDone) { + SetOnlineState(false); + } + break; + case eDownloadingNews: + m_curState = eDownloadingMail; + if (m_downloadMail) + DownloadMail(); + else + AdvanceToNextState(NS_OK); + break; + case eDownloadingMail: + m_curState = eSendingUnsent; + if (m_sendUnsentMessages) + SendUnsentMessages(); + else + AdvanceToNextState(NS_OK); + break; + default: + NS_ASSERTION(false, + "unhandled current state when downloading for offline"); + } + } + return NS_OK; +} + +nsresult nsMsgOfflineManager::SynchronizeOfflineImapChanges() { + nsresult rv = NS_OK; + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return imapService->PlaybackAllOfflineOperations( + m_window, this, getter_AddRefs(mOfflineImapSync)); +} + +nsresult nsMsgOfflineManager::SendUnsentMessages() { + nsresult rv; + nsCOMPtr<nsIMsgSendLater> pMsgSendLater(do_GetService(kMsgSendLaterCID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + // now we have to iterate over the identities, finding the *unique* unsent + // messages folder for each one, determine if they have unsent messages, and + // if so, add them to the list of identities to send unsent messages from. + // However, I think there's only ever one unsent messages folder at the + // moment, so I think we'll go with that for now. + nsTArray<RefPtr<nsIMsgIdentity>> identities; + + if (NS_SUCCEEDED(rv) && accountManager) { + rv = accountManager->GetAllIdentities(identities); + NS_ENSURE_SUCCESS(rv, rv); + } + nsCOMPtr<nsIMsgIdentity> identityToUse; + for (auto thisIdentity : identities) { + if (thisIdentity) { + nsCOMPtr<nsIMsgFolder> outboxFolder; + pMsgSendLater->GetUnsentMessagesFolder(thisIdentity, + getter_AddRefs(outboxFolder)); + if (outboxFolder) { + int32_t numMessages; + outboxFolder->GetTotalMessages(false, &numMessages); + if (numMessages > 0) { + identityToUse = thisIdentity; + break; + } + } + } + } + if (identityToUse) { +#ifdef MOZ_SUITE + if (m_statusFeedback) pMsgSendLater->SetStatusFeedback(m_statusFeedback); +#endif + + pMsgSendLater->AddListener(this); + rv = pMsgSendLater->SendUnsentMessages(identityToUse); + ShowStatus("sendingUnsent"); + // if we succeeded, return - we'll run the next operation when the + // send finishes. Otherwise, advance to the next state. + if (NS_SUCCEEDED(rv)) return rv; + } + return AdvanceToNextState(rv); +} + +#define MESSENGER_STRING_URL "chrome://messenger/locale/messenger.properties" + +nsresult nsMsgOfflineManager::ShowStatus(const char* statusMsgName) { + if (!mStringBundle) { + nsCOMPtr<nsIStringBundleService> sBundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sBundleService, NS_ERROR_UNEXPECTED); + sBundleService->CreateBundle(MESSENGER_STRING_URL, + getter_AddRefs(mStringBundle)); + return NS_OK; + } + + nsString statusString; + nsresult res = mStringBundle->GetStringFromName(statusMsgName, statusString); + + if (NS_SUCCEEDED(res) && m_statusFeedback) + m_statusFeedback->ShowStatusString(statusString); + + return res; +} + +nsresult nsMsgOfflineManager::DownloadOfflineNewsgroups() { + nsresult rv; + ShowStatus("downloadingNewsgroups"); + nsCOMPtr<nsINntpService> nntpService( + do_GetService("@mozilla.org/messenger/nntpservice;1", &rv)); + if (NS_SUCCEEDED(rv) && nntpService) + rv = nntpService->DownloadNewsgroupsForOffline(m_window, this); + + if (NS_FAILED(rv)) return AdvanceToNextState(rv); + return rv; +} + +nsresult nsMsgOfflineManager::DownloadMail() { + nsresult rv = NS_OK; + ShowStatus("downloadingMail"); + nsCOMPtr<nsIImapService> imapService = + do_GetService("@mozilla.org/messenger/imapservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + return imapService->DownloadAllOffineImapFolders(m_window, this); + // ### we should do get new mail on pop servers, and download imap messages + // for offline use. +} + +NS_IMETHODIMP nsMsgOfflineManager::GoOnline(bool sendUnsentMessages, + bool playbackOfflineImapOperations, + nsIMsgWindow* aMsgWindow) { + m_sendUnsentMessages = sendUnsentMessages; + m_playbackOfflineImapOps = playbackOfflineImapOperations; + m_curOperation = eGoingOnline; + m_curState = eNoState; + SetWindow(aMsgWindow); + SetOnlineState(true); + if (!m_sendUnsentMessages && !playbackOfflineImapOperations) + return NS_OK; + else + AdvanceToNextState(NS_OK); + return NS_OK; +} + +NS_IMETHODIMP nsMsgOfflineManager::SynchronizeForOffline( + bool downloadNews, bool downloadMail, bool sendUnsentMessages, + bool goOfflineWhenDone, nsIMsgWindow* aMsgWindow) { + m_curOperation = eDownloadingForOffline; + m_downloadNews = downloadNews; + m_downloadMail = downloadMail; + m_sendUnsentMessages = sendUnsentMessages; + SetWindow(aMsgWindow); + m_goOfflineWhenDone = goOfflineWhenDone; + m_curState = eNoState; + if (!downloadNews && !downloadMail && !sendUnsentMessages) { + if (goOfflineWhenDone) return SetOnlineState(false); + } else + return AdvanceToNextState(NS_OK); + return NS_OK; +} + +nsresult nsMsgOfflineManager::SetOnlineState(bool online) { + nsCOMPtr<nsIIOService> netService = mozilla::components::IO::Service(); + NS_ENSURE_TRUE(netService, NS_ERROR_UNEXPECTED); + return netService->SetOffline(!online); +} + +// nsIUrlListener methods + +NS_IMETHODIMP +nsMsgOfflineManager::OnStartRunningUrl(nsIURI* aUrl) { return NS_OK; } + +NS_IMETHODIMP +nsMsgOfflineManager::OnStopRunningUrl(nsIURI* aUrl, nsresult aExitCode) { + mOfflineImapSync = nullptr; + + AdvanceToNextState(aExitCode); + return NS_OK; +} + +NS_IMETHODIMP nsMsgOfflineManager::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* someData) { + return NS_OK; +} + +// nsIMsgSendLaterListener implementation +NS_IMETHODIMP +nsMsgOfflineManager::OnStartSending(uint32_t aTotalMessageCount) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgOfflineManager::OnMessageStartSending(uint32_t aCurrentMessage, + uint32_t aTotalMessageCount, + nsIMsgDBHdr* aMessageHeader, + nsIMsgIdentity* aIdentity) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgOfflineManager::OnMessageSendProgress(uint32_t aCurrentMessage, + uint32_t aTotalMessageCount, + uint32_t aMessageSendPercent, + uint32_t aMessageCopyPercent) { + if (m_statusFeedback && aTotalMessageCount) + return m_statusFeedback->ShowProgress((100 * aCurrentMessage) / + aTotalMessageCount); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgOfflineManager::OnMessageSendError(uint32_t aCurrentMessage, + nsIMsgDBHdr* aMessageHeader, + nsresult aStatus, + const char16_t* aMsg) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgOfflineManager::OnStopSending(nsresult aStatus, const char16_t* aMsg, + uint32_t aTotalTried, uint32_t aSuccessful) { +#ifdef NS_DEBUG + if (NS_SUCCEEDED(aStatus)) + printf( + "SendLaterListener::OnStopSending: Tried to send %d messages. %d " + "successful.\n", + aTotalTried, aSuccessful); +#endif + return AdvanceToNextState(aStatus); +} diff --git a/comm/mailnews/base/src/nsMsgOfflineManager.h b/comm/mailnews/base/src/nsMsgOfflineManager.h new file mode 100644 index 0000000000..f340ee0fd1 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgOfflineManager.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgOfflineManager_h__ +#define nsMsgOfflineManager_h__ + +#include "nscore.h" +#include "nsIMsgOfflineManager.h" +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "nsWeakReference.h" +#include "nsIUrlListener.h" +#include "nsIMsgWindow.h" +#include "nsIMsgSendLaterListener.h" +#include "nsIStringBundle.h" + +class nsMsgOfflineManager : public nsIMsgOfflineManager, + public nsIObserver, + public nsSupportsWeakReference, + public nsIMsgSendLaterListener, + public nsIUrlListener { + public: + nsMsgOfflineManager(); + + NS_DECL_THREADSAFE_ISUPPORTS + + /* nsIMsgOfflineManager methods */ + + NS_DECL_NSIMSGOFFLINEMANAGER + NS_DECL_NSIOBSERVER + NS_DECL_NSIURLLISTENER + NS_DECL_NSIMSGSENDLATERLISTENER + + typedef enum { + eStarting = 0, + eSynchronizingOfflineImapChanges = 1, + eDownloadingNews = 2, + eDownloadingMail = 3, + eSendingUnsent = 4, + eDone = 5, + eNoState = 6 // we're not doing anything + } offlineManagerState; + + typedef enum { + eGoingOnline = 0, + eDownloadingForOffline = 1, + eNoOp = 2 // no operation in progress + } offlineManagerOperation; + + private: + virtual ~nsMsgOfflineManager(); + + nsresult AdvanceToNextState(nsresult exitStatus); + nsresult SynchronizeOfflineImapChanges(); + nsresult StopRunning(nsresult exitStatus); + nsresult SendUnsentMessages(); + nsresult DownloadOfflineNewsgroups(); + nsresult DownloadMail(); + + nsresult SetOnlineState(bool online); + nsresult ShowStatus(const char* statusMsgName); + + bool m_inProgress; + bool m_sendUnsentMessages; + bool m_downloadNews; + bool m_downloadMail; + bool m_playbackOfflineImapOps; + bool m_goOfflineWhenDone; + offlineManagerState m_curState; + offlineManagerOperation m_curOperation; + nsCOMPtr<nsIMsgWindow> m_window; + nsCOMPtr<nsIMsgStatusFeedback> m_statusFeedback; + nsCOMPtr<nsIStringBundle> mStringBundle; + nsCOMPtr<nsISupports> mOfflineImapSync; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgProgress.cpp b/comm/mailnews/base/src/nsMsgProgress.cpp new file mode 100644 index 0000000000..a6349b5add --- /dev/null +++ b/comm/mailnews/base/src/nsMsgProgress.cpp @@ -0,0 +1,250 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgProgress.h" + +#include "nsIBaseWindow.h" +#include "nsXPCOM.h" +#include "nsIMutableArray.h" +#include "nsISupportsPrimitives.h" +#include "nsIComponentManager.h" +#include "nsError.h" +#include "nsIWindowWatcher.h" +#include "nsPIDOMWindow.h" +#include "mozIDOMWindow.h" +#include "nsServiceManagerUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsMsgUtils.h" +#include "mozilla/Components.h" +#include "mozilla/dom/BrowsingContext.h" + +NS_IMPL_ISUPPORTS(nsMsgProgress, nsIMsgStatusFeedback, nsIMsgProgress, + nsIWebProgressListener, nsIProgressEventSink, + nsISupportsWeakReference) + +nsMsgProgress::nsMsgProgress() { + m_closeProgress = false; + m_processCanceled = false; + m_pendingStateFlags = -1; + m_pendingStateValue = NS_OK; +} + +nsMsgProgress::~nsMsgProgress() { (void)ReleaseListeners(); } + +NS_IMETHODIMP nsMsgProgress::OpenProgressDialog( + mozIDOMWindowProxy* parentDOMWindow, nsIMsgWindow* aMsgWindow, + const char* dialogURL, bool inDisplayModal, nsISupports* parameters) { + nsresult rv; + + if (aMsgWindow) { + SetMsgWindow(aMsgWindow); + aMsgWindow->SetStatusFeedback(this); + } + + NS_ENSURE_ARG_POINTER(dialogURL); + NS_ENSURE_ARG_POINTER(parentDOMWindow); + nsCOMPtr<nsPIDOMWindowOuter> parent = + nsPIDOMWindowOuter::From(parentDOMWindow); + + // Set up window.arguments[0]... + nsCOMPtr<nsIMutableArray> array(do_CreateInstance(NS_ARRAY_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupportsInterfacePointer> ifptr = + do_CreateInstance(NS_SUPPORTS_INTERFACE_POINTER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + ifptr->SetData(static_cast<nsIMsgProgress*>(this)); + ifptr->SetDataIID(&NS_GET_IID(nsIMsgProgress)); + + array->AppendElement(ifptr); + array->AppendElement(parameters); + + // Open the dialog. + RefPtr<mozilla::dom::BrowsingContext> newWindow; + + nsString chromeOptions(u"chrome,dependent,centerscreen"_ns); + if (inDisplayModal) chromeOptions.AppendLiteral(",modal"); + + return parent->OpenDialog(NS_ConvertASCIItoUTF16(dialogURL), u"_blank"_ns, + chromeOptions, array, getter_AddRefs(newWindow)); +} + +NS_IMETHODIMP nsMsgProgress::CloseProgressDialog(bool forceClose) { + m_closeProgress = true; + return OnStateChange(nullptr, nullptr, nsIWebProgressListener::STATE_STOP, + forceClose ? NS_ERROR_FAILURE : NS_OK); +} + +NS_IMETHODIMP nsMsgProgress::GetProcessCanceledByUser( + bool* aProcessCanceledByUser) { + NS_ENSURE_ARG_POINTER(aProcessCanceledByUser); + *aProcessCanceledByUser = m_processCanceled; + return NS_OK; +} +NS_IMETHODIMP nsMsgProgress::SetProcessCanceledByUser( + bool aProcessCanceledByUser) { + m_processCanceled = aProcessCanceledByUser; + OnStateChange(nullptr, nullptr, nsIWebProgressListener::STATE_STOP, + NS_BINDING_ABORTED); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::RegisterListener( + nsIWebProgressListener* listener) { + if (!listener) // Nothing to do with a null listener! + return NS_OK; + + NS_ENSURE_ARG(this != listener); // Check for self-reference (see bug 271700) + + m_listenerList.AppendObject(listener); + if (m_closeProgress || m_processCanceled) + listener->OnStateChange(nullptr, nullptr, + nsIWebProgressListener::STATE_STOP, NS_OK); + else { + listener->OnStatusChange(nullptr, nullptr, NS_OK, m_pendingStatus.get()); + if (m_pendingStateFlags != -1) + listener->OnStateChange(nullptr, nullptr, m_pendingStateFlags, + m_pendingStateValue); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::UnregisterListener( + nsIWebProgressListener* listener) { + if (listener) m_listenerList.RemoveObject(listener); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aStateFlags, + nsresult aStatus) { + m_pendingStateFlags = aStateFlags; + m_pendingStateValue = aStatus; + + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(m_msgWindow)); + if (aStateFlags == nsIWebProgressListener::STATE_STOP && msgWindow && + NS_FAILED(aStatus)) { + msgWindow->StopUrls(); + msgWindow->SetStatusFeedback(nullptr); + } + + for (int32_t i = m_listenerList.Count() - 1; i >= 0; i--) + m_listenerList[i]->OnStateChange(aWebProgress, aRequest, aStateFlags, + aStatus); + + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + for (int32_t i = m_listenerList.Count() - 1; i >= 0; i--) + m_listenerList[i]->OnProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsIURI* location, + uint32_t aFlags) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + nsresult aStatus, + const char16_t* aMessage) { + if (aMessage && *aMessage) m_pendingStatus = aMessage; + for (int32_t i = m_listenerList.Count() - 1; i >= 0; i--) + m_listenerList[i]->OnStatusChange(aWebProgress, aRequest, aStatus, + aMessage); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t state) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgProgress::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aEvent) { + return NS_OK; +} + +nsresult nsMsgProgress::ReleaseListeners() { + m_listenerList.Clear(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::ShowStatusString(const nsAString& aStatus) { + return OnStatusChange(nullptr, nullptr, NS_OK, + PromiseFlatString(aStatus).get()); +} + +NS_IMETHODIMP nsMsgProgress::SetStatusString(const nsAString& aStatus) { + return OnStatusChange(nullptr, nullptr, NS_OK, + PromiseFlatString(aStatus).get()); +} + +NS_IMETHODIMP nsMsgProgress::StartMeteors() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP nsMsgProgress::StopMeteors() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP nsMsgProgress::ShowProgress(int32_t percent) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgProgress::SetWrappedStatusFeedback( + nsIMsgStatusFeedback* aJSStatusFeedback) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsMsgProgress::SetMsgWindow(nsIMsgWindow* aMsgWindow) { + m_msgWindow = do_GetWeakReference(aMsgWindow); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::GetMsgWindow(nsIMsgWindow** aMsgWindow) { + NS_ENSURE_ARG_POINTER(aMsgWindow); + + if (m_msgWindow) + CallQueryReferent(m_msgWindow.get(), aMsgWindow); + else + *aMsgWindow = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP nsMsgProgress::OnProgress(nsIRequest* request, int64_t aProgress, + int64_t aProgressMax) { + // XXX: What should the nsIWebProgress be? + // XXX: This truncates 64-bit to 32-bit + return OnProgressChange(nullptr, request, int32_t(aProgress), + int32_t(aProgressMax), + int32_t(aProgress) /* current total progress */, + int32_t(aProgressMax) /* max total progress */); +} + +NS_IMETHODIMP nsMsgProgress::OnStatus(nsIRequest* request, nsresult aStatus, + const char16_t* aStatusArg) { + nsresult rv; + nsCOMPtr<nsIStringBundleService> sbs = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sbs, NS_ERROR_UNEXPECTED); + nsString str; + rv = sbs->FormatStatusMessage(aStatus, aStatusArg, str); + NS_ENSURE_SUCCESS(rv, rv); + return ShowStatusString(str); +} diff --git a/comm/mailnews/base/src/nsMsgProgress.h b/comm/mailnews/base/src/nsMsgProgress.h new file mode 100644 index 0000000000..57fae38aa9 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgProgress.h @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgProgress_h_ +#define nsMsgProgress_h_ + +#include "nsIMsgProgress.h" +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "nsIMsgStatusFeedback.h" +#include "nsString.h" +#include "nsIMsgWindow.h" +#include "nsIProgressEventSink.h" +#include "nsIStringBundle.h" +#include "nsWeakReference.h" + +class nsMsgProgress : public nsIMsgProgress, + public nsIMsgStatusFeedback, + public nsIProgressEventSink, + public nsSupportsWeakReference { + public: + nsMsgProgress(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMSGPROGRESS + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIMSGSTATUSFEEDBACK + NS_DECL_NSIPROGRESSEVENTSINK + + private: + virtual ~nsMsgProgress(); + nsresult ReleaseListeners(void); + + bool m_closeProgress; + bool m_processCanceled; + nsString m_pendingStatus; + int32_t m_pendingStateFlags; + nsresult m_pendingStateValue; + nsWeakPtr m_msgWindow; + nsCOMArray<nsIWebProgressListener> m_listenerList; +}; + +#endif // nsMsgProgress_h_ diff --git a/comm/mailnews/base/src/nsMsgProtocol.cpp b/comm/mailnews/base/src/nsMsgProtocol.cpp new file mode 100644 index 0000000000..7fc832758c --- /dev/null +++ b/comm/mailnews/base/src/nsMsgProtocol.cpp @@ -0,0 +1,1512 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsString.h" +#include "nsMemory.h" +#include "nsMsgProtocol.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIMsgMailSession.h" +#include "nsIStreamTransportService.h" +#include "nsISocketTransportService.h" +#include "nsISocketTransport.h" +#include "nsITLSSocketControl.h" +#include "nsITransportSecurityInfo.h" +#include "nsILoadGroup.h" +#include "nsILoadInfo.h" +#include "nsIIOService.h" +#include "nsNetUtil.h" +#include "nsIFileURL.h" +#include "nsIMsgWindow.h" +#include "nsIMsgStatusFeedback.h" +#include "nsIWebProgressListener.h" +#include "nsIPipe.h" +#include "nsIPrompt.h" +#include "prprf.h" +#include "plbase64.h" +#include "nsIStringBundle.h" +#include "nsIProxyInfo.h" +#include "nsThreadUtils.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "nsDirectoryServiceDefs.h" +#include "nsMsgUtils.h" +#include "nsILineInputStream.h" +#include "nsIAsyncInputStream.h" +#include "nsIMsgIncomingServer.h" +#include "nsIInputStreamPump.h" +#include "nsICancelable.h" +#include "nsMimeTypes.h" +#include "mozilla/Components.h" +#include "mozilla/SlicedInputStream.h" +#include "nsContentSecurityManager.h" +#include "nsPrintfCString.h" + +#undef PostMessage // avoid to collision with WinUser.h + +using namespace mozilla; + +NS_IMPL_ISUPPORTS_INHERITED(nsMsgProtocol, nsHashPropertyBag, nsIMailChannel, + nsIChannel, nsIStreamListener, nsIRequestObserver, + nsIRequest, nsITransportEventSink) + +static char16_t* FormatStringWithHostNameByName(const char16_t* stringName, + nsIMsgMailNewsUrl* msgUri); + +nsMsgProtocol::nsMsgProtocol(nsIURI* aURL) { + m_flags = 0; + m_readCount = 0; + mLoadFlags = 0; + m_socketIsOpen = false; + mContentLength = -1; + m_isChannel = false; + mContentDisposition = nsIChannel::DISPOSITION_INLINE; + + GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, "tempMessage.eml", + getter_AddRefs(m_tempMsgFile)); + + mSuppressListenerNotifications = false; + InitFromURI(aURL); +} + +nsresult nsMsgProtocol::InitFromURI(nsIURI* aUrl) { + m_url = aUrl; + + nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(aUrl); + if (mailUrl) { + mailUrl->GetLoadGroup(getter_AddRefs(m_loadGroup)); + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + mailUrl->GetStatusFeedback(getter_AddRefs(statusFeedback)); + mProgressEventSink = do_QueryInterface(statusFeedback); + } + + // Reset channel data in case the object is reused and initialised again. + mCharset.Truncate(); + + return NS_OK; +} + +nsMsgProtocol::~nsMsgProtocol() {} + +static bool gGotTimeoutPref; +static int32_t gSocketTimeout = 60; + +nsresult nsMsgProtocol::GetQoSBits(uint8_t* aQoSBits) { + NS_ENSURE_ARG_POINTER(aQoSBits); + const char* protocol = GetType(); + + if (!protocol) return NS_ERROR_NOT_IMPLEMENTED; + + nsAutoCString prefName("mail."); + prefName.Append(protocol); + prefName.AppendLiteral(".qos"); + + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t val; + rv = prefBranch->GetIntPref(prefName.get(), &val); + NS_ENSURE_SUCCESS(rv, rv); + *aQoSBits = (uint8_t)clamped(val, 0, 0xff); + return NS_OK; +} + +nsresult nsMsgProtocol::OpenNetworkSocketWithInfo( + const char* aHostName, int32_t aGetPort, const char* connectionType, + nsIProxyInfo* aProxyInfo, nsIInterfaceRequestor* callbacks) { + NS_ENSURE_ARG(aHostName); + + nsresult rv = NS_OK; + nsCOMPtr<nsISocketTransportService> socketService( + do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID)); + NS_ENSURE_TRUE(socketService, NS_ERROR_FAILURE); + + // with socket connections we want to read as much data as arrives + m_readCount = -1; + + nsCOMPtr<nsISocketTransport> strans; + AutoTArray<nsCString, 1> connectionTypeArray; + if (connectionType) connectionTypeArray.AppendElement(connectionType); + rv = socketService->CreateTransport( + connectionTypeArray, nsDependentCString(aHostName), aGetPort, aProxyInfo, + nullptr, getter_AddRefs(strans)); + if (NS_FAILED(rv)) return rv; + + strans->SetSecurityCallbacks(callbacks); + + // creates cyclic reference! + nsCOMPtr<nsIThread> currentThread(do_GetCurrentThread()); + strans->SetEventSink(this, currentThread); + + m_socketIsOpen = false; + m_transport = strans; + + if (!gGotTimeoutPref) { + nsCOMPtr<nsIPrefBranch> prefBranch = + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv); + if (prefBranch) { + prefBranch->GetIntPref("mailnews.tcptimeout", &gSocketTimeout); + gGotTimeoutPref = true; + } + } + strans->SetTimeout(nsISocketTransport::TIMEOUT_CONNECT, gSocketTimeout + 60); + strans->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, gSocketTimeout); + + uint8_t qos; + rv = GetQoSBits(&qos); + if (NS_SUCCEEDED(rv)) strans->SetQoSBits(qos); + + return SetupTransportState(); +} + +nsresult nsMsgProtocol::GetFileFromURL(nsIURI* aURL, nsIFile** aResult) { + NS_ENSURE_ARG_POINTER(aURL); + NS_ENSURE_ARG_POINTER(aResult); + // extract the file path from the uri... + nsAutoCString urlSpec; + aURL->GetPathQueryRef(urlSpec); + urlSpec.InsertLiteral("file://", 0); + nsresult rv; + + // dougt - there should be an easier way! + nsCOMPtr<nsIURI> uri; + if (NS_FAILED(rv = NS_NewURI(getter_AddRefs(uri), urlSpec.get()))) return rv; + + nsCOMPtr<nsIFileURL> fileURL = do_QueryInterface(uri); + if (!fileURL) return NS_ERROR_FAILURE; + + return fileURL->GetFile(aResult); + // dougt +} + +nsresult nsMsgProtocol::OpenFileSocket(nsIURI* aURL, uint64_t aStartPosition, + int64_t aReadCount) { + // mscott - file needs to be encoded directly into aURL. I should be able to + // get rid of this method completely. + + nsresult rv = NS_OK; + m_readCount = aReadCount; + nsCOMPtr<nsIFile> file; + + rv = GetFileFromURL(aURL, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIInputStream> stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file); + if (NS_FAILED(rv)) return rv; + + // create input stream transport + nsCOMPtr<nsIStreamTransportService> sts = + do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID, &rv); + if (NS_FAILED(rv)) return rv; + + // This can be called with aReadCount == -1 which means "read as much as we + // can". We pass this on as UINT64_MAX, which is in fact uint64_t(-1). + RefPtr<SlicedInputStream> slicedStream = new SlicedInputStream( + stream.forget(), aStartPosition, + aReadCount == -1 ? UINT64_MAX : uint64_t(aReadCount)); + rv = sts->CreateInputTransport(slicedStream, true, + getter_AddRefs(m_transport)); + + m_socketIsOpen = false; + return rv; +} + +nsresult nsMsgProtocol::GetTopmostMsgWindow(nsIMsgWindow** aWindow) { + nsresult rv; + nsCOMPtr<nsIMsgMailSession> mailSession( + do_GetService("@mozilla.org/messenger/services/session;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return mailSession->GetTopmostMsgWindow(aWindow); +} + +nsresult nsMsgProtocol::SetupTransportState() { + if (!m_socketIsOpen && m_transport) { + nsresult rv; + + // open buffered, blocking output stream + rv = m_transport->OpenOutputStream(nsITransport::OPEN_BLOCKING, 0, 0, + getter_AddRefs(m_outputStream)); + if (NS_FAILED(rv)) return rv; + // we want to open the stream + } // if m_transport + + return NS_OK; +} + +nsresult nsMsgProtocol::CloseSocket() { + nsresult rv = NS_OK; + // release all of our socket state + m_socketIsOpen = false; + m_outputStream = nullptr; + if (m_transport) { + nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(m_transport); + if (strans) { + strans->SetEventSink(nullptr, nullptr); // break cyclic reference! + } + } + // we need to call Cancel so that we remove the socket transport from the + // mActiveTransportList. see bug #30648 + if (m_request) { + rv = m_request->Cancel(NS_BINDING_ABORTED); + } + m_request = nullptr; + if (m_transport) { + m_transport->Close(NS_BINDING_ABORTED); + m_transport = nullptr; + } + + return rv; +} + +/* + * Writes the data contained in dataBuffer into the current output stream. It + * also informs the transport layer that this data is now available for + * transmission. Returns a positive number for success, 0 for failure (not all + * the bytes were written to the stream, etc). We need to make another pass + * through this file to install an error system (mscott) + * + * No logging is done in the base implementation, so aSuppressLogging is + * ignored. + */ + +nsresult nsMsgProtocol::SendData(const char* dataBuffer, + bool aSuppressLogging) { + uint32_t writeCount = 0; + + if (dataBuffer && m_outputStream) + return m_outputStream->Write(dataBuffer, PL_strlen(dataBuffer), + &writeCount); + // TODO make sure all the bytes in PL_strlen(dataBuffer) were written + else + return NS_ERROR_INVALID_ARG; +} + +// Whenever data arrives from the connection, core netlib notifices the protocol +// by calling OnDataAvailable. We then read and process the incoming data from +// the input stream. +NS_IMETHODIMP nsMsgProtocol::OnDataAvailable(nsIRequest* request, + nsIInputStream* inStr, + uint64_t sourceOffset, + uint32_t count) { + // right now, this really just means turn around and churn through the state + // machine + nsCOMPtr<nsIURI> uri; + GetURI(getter_AddRefs(uri)); + + return ProcessProtocolState(uri, inStr, sourceOffset, count); +} + +NS_IMETHODIMP nsMsgProtocol::OnStartRequest(nsIRequest* request) { + nsresult rv = NS_OK; + nsCOMPtr<nsIURI> uri; + GetURI(getter_AddRefs(uri)); + + if (uri) { + nsCOMPtr<nsIMsgMailNewsUrl> aMsgUrl = do_QueryInterface(uri); + rv = aMsgUrl->SetUrlState(true, NS_OK); + if (m_loadGroup) + m_loadGroup->AddRequest(static_cast<nsIRequest*>(this), + nullptr /* context isupports */); + } + + // if we are set up as a channel, we should notify our channel listener that + // we are starting... so pass in ourself as the channel and not the underlying + // socket or file channel the protocol happens to be using + if (!mSuppressListenerNotifications && m_channelListener) { + m_isChannel = true; + rv = m_channelListener->OnStartRequest(this); + } + + nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(m_transport); + + if (strans) + strans->SetTimeout(nsISocketTransport::TIMEOUT_READ_WRITE, gSocketTimeout); + + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +void nsMsgProtocol::ShowAlertMessage(nsIMsgMailNewsUrl* aMsgUrl, + nsresult aStatus) { + const char16_t* errorString = nullptr; + switch (aStatus) { + case NS_ERROR_UNKNOWN_HOST: + case NS_ERROR_UNKNOWN_PROXY_HOST: + errorString = u"unknownHostError"; + break; + case NS_ERROR_CONNECTION_REFUSED: + case NS_ERROR_PROXY_CONNECTION_REFUSED: + errorString = u"connectionRefusedError"; + break; + case NS_ERROR_NET_TIMEOUT: + errorString = u"netTimeoutError"; + break; + case NS_ERROR_NET_RESET: + errorString = u"netResetError"; + break; + case NS_ERROR_NET_INTERRUPT: + errorString = u"netInterruptError"; + break; + case NS_ERROR_OFFLINE: + // Don't alert when offline as that is already displayed in the UI. + return; + default: + nsPrintfCString msg( + "Unexpected status passed to ShowAlertMessage: %" PRIx32, + static_cast<uint32_t>(aStatus)); + NS_WARNING(msg.get()); + return; + } + + nsString errorMsg; + errorMsg.Adopt(FormatStringWithHostNameByName(errorString, aMsgUrl)); + if (errorMsg.IsEmpty()) { + errorMsg.AssignLiteral(u"[StringID "); + errorMsg.Append(errorString); + errorMsg.AppendLiteral(u"?]"); + } + + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1"); + if (mailSession) mailSession->AlertUser(errorMsg, aMsgUrl); +} + +// stop binding is a "notification" informing us that the stream associated with +// aURL is going away. +NS_IMETHODIMP nsMsgProtocol::OnStopRequest(nsIRequest* request, + nsresult aStatus) { + nsresult rv = NS_OK; + + // if we are set up as a channel, we should notify our channel listener that + // we are starting... so pass in ourself as the channel and not the underlying + // socket or file channel the protocol happens to be using + if (!mSuppressListenerNotifications && m_channelListener) + rv = m_channelListener->OnStopRequest(this, aStatus); + + nsCOMPtr<nsIURI> uri; + GetURI(getter_AddRefs(uri)); + + if (uri) { + nsCOMPtr<nsIMsgMailNewsUrl> msgUrl = do_QueryInterface(uri); + rv = msgUrl->SetUrlState(false, aStatus); // Always returns NS_OK. + if (m_loadGroup) + m_loadGroup->RemoveRequest(static_cast<nsIRequest*>(this), nullptr, + aStatus); + + // !m_isChannel because if we're set up as a channel, then the remove + // request above will handle alerting the user, so we don't need to. + // + // !NS_BINDING_ABORTED because we don't want to see an alert if the user + // cancelled the operation. also, we'll get here because we call Cancel() + // to force removal of the nsSocketTransport. see CloseSocket() + // bugs #30775 and #30648 relate to this + if (!m_isChannel && NS_FAILED(aStatus) && (aStatus != NS_BINDING_ABORTED)) + ShowAlertMessage(msgUrl, aStatus); + } // if we have a mailnews url. + + // Drop notification callbacks to prevent cycles. + mCallbacks = nullptr; + mProgressEventSink = nullptr; + // Call CloseSocket(), in case we got here because the server dropped the + // connection while reading, and we never get a chance to get back into + // the protocol state machine via OnDataAvailable. + if (m_socketIsOpen) CloseSocket(); + + return rv; +} + +nsresult nsMsgProtocol::LoadUrl(nsIURI* aURL, nsISupports* aConsumer) { + // nsMsgProtocol implements nsIChannel, and all channels are required to + // have non-null loadInfo. So if it's still unset, we've not been correctly + // initialised. + MOZ_ASSERT(m_loadInfo); + + // okay now kick us off to the next state... + // our first state is a process state so drive the state machine... + nsresult rv = NS_OK; + nsCOMPtr<nsIMsgMailNewsUrl> aMsgUrl = do_QueryInterface(aURL, &rv); + + if (NS_SUCCEEDED(rv) && aMsgUrl) { + bool msgIsInLocalCache; + aMsgUrl->GetMsgIsInLocalCache(&msgIsInLocalCache); + + // Set the url as a url currently being run... + rv = aMsgUrl->SetUrlState(true, NS_OK); + + // if the url is given a stream consumer then we should use it to forward + // calls to... + if (!m_channelListener && + aConsumer) // if we don't have a registered listener already + { + m_channelListener = do_QueryInterface(aConsumer); + m_isChannel = true; + } + + if (!m_socketIsOpen) { + if (m_transport) { + // open buffered, asynchronous input stream + nsCOMPtr<nsIInputStream> stream; + rv = m_transport->OpenInputStream(0, 0, 0, getter_AddRefs(stream)); + if (NS_FAILED(rv)) return rv; + + // m_readCount can be -1 which means "read as much as we can". + // We pass this on as UINT64_MAX, which is in fact uint64_t(-1). + // We don't clone m_inputStream here, we simply give up ownership + // since otherwise the original would never be closed. + RefPtr<SlicedInputStream> slicedStream = new SlicedInputStream( + stream.forget(), 0, + m_readCount == -1 ? UINT64_MAX : uint64_t(m_readCount)); + nsCOMPtr<nsIInputStreamPump> pump; + rv = NS_NewInputStreamPump(getter_AddRefs(pump), slicedStream.forget()); + if (NS_FAILED(rv)) return rv; + + m_request = pump; // keep a reference to the pump so we can cancel it + + // Put us in a state where we are always notified of incoming data. + // OnDataAvailable() will be called when that happens, which will + // pass that data into ProcessProtocolState(). + rv = pump->AsyncRead(this); + NS_ASSERTION(NS_SUCCEEDED(rv), "AsyncRead failed"); + m_socketIsOpen = true; // mark the channel as open + } + } else if (!msgIsInLocalCache) { + // The connection is already open so we should begin processing our url. + rv = ProcessProtocolState(aURL, nullptr, 0, 0); + } + } + + return rv; +} + +/////////////////////////////////////////////////////////////////////// +// The rest of this file is mostly nsIChannel mumbo jumbo stuff +/////////////////////////////////////////////////////////////////////// + +nsresult nsMsgProtocol::SetUrl(nsIURI* aURL) { + m_url = aURL; + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::SetLoadGroup(nsILoadGroup* aLoadGroup) { + m_loadGroup = aLoadGroup; + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::GetTRRMode(nsIRequest::TRRMode* aTRRMode) { + return GetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP nsMsgProtocol::SetTRRMode(nsIRequest::TRRMode aTRRMode) { + return SetTRRModeImpl(aTRRMode); +} + +NS_IMETHODIMP nsMsgProtocol::GetOriginalURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = m_originalUrl ? m_originalUrl : m_url); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::SetOriginalURI(nsIURI* aURI) { + m_originalUrl = aURI; + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::GetURI(nsIURI** aURI) { + NS_IF_ADDREF(*aURI = m_url); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::Open(nsIInputStream** _retval) { + nsCOMPtr<nsIStreamListener> listener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + return NS_ImplementChannelOpen(this, _retval); +} + +NS_IMETHODIMP nsMsgProtocol::AsyncOpen(nsIStreamListener* aListener) { + nsCOMPtr<nsIStreamListener> listener = aListener; + nsresult rv = + nsContentSecurityManager::doContentSecurityCheck(this, listener); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t port; + rv = m_url->GetPort(&port); + if (NS_FAILED(rv)) return rv; + + nsAutoCString scheme; + rv = m_url->GetScheme(scheme); + if (NS_FAILED(rv)) return rv; + + rv = NS_CheckPortSafety(port, scheme.get()); + if (NS_FAILED(rv)) return rv; + + // set the stream listener and then load the url + m_isChannel = true; + + m_channelListener = listener; + return LoadUrl(m_url, nullptr); +} + +NS_IMETHODIMP nsMsgProtocol::GetLoadFlags(nsLoadFlags* aLoadFlags) { + *aLoadFlags = mLoadFlags; + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::SetLoadFlags(nsLoadFlags aLoadFlags) { + mLoadFlags = aLoadFlags; + return NS_OK; // don't fail when trying to set this +} + +NS_IMETHODIMP nsMsgProtocol::GetContentType(nsACString& aContentType) { + // as url dispatching matures, we'll be intelligent and actually start + // opening the url before specifying the content type. This will allow + // us to optimize the case where the message url actual refers to + // a part in the message that has a content type that is not message/rfc822 + + if (mContentType.IsEmpty()) + aContentType.AssignLiteral("message/rfc822"); + else + aContentType = mContentType; + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::SetContentType(const nsACString& aContentType) { + nsAutoCString charset; + nsresult rv = + NS_ParseResponseContentType(aContentType, mContentType, charset); + if (NS_FAILED(rv) || mContentType.IsEmpty()) + mContentType.AssignLiteral(UNKNOWN_CONTENT_TYPE); + return rv; +} + +NS_IMETHODIMP nsMsgProtocol::GetContentCharset(nsACString& aContentCharset) { + aContentCharset.Assign(mCharset); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::SetContentCharset( + const nsACString& aContentCharset) { + mCharset.Assign(aContentCharset); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgProtocol::GetContentDisposition(uint32_t* aContentDisposition) { + *aContentDisposition = mContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgProtocol::SetContentDisposition(uint32_t aContentDisposition) { + mContentDisposition = aContentDisposition; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgProtocol::GetContentDispositionFilename( + nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsMsgProtocol::SetContentDispositionFilename( + const nsAString& aContentDispositionFilename) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsMsgProtocol::GetContentDispositionHeader( + nsACString& aContentDispositionHeader) { + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsMsgProtocol::GetContentLength(int64_t* aContentLength) { + *aContentLength = mContentLength; + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::SetContentLength(int64_t aContentLength) { + mContentLength = aContentLength; + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::GetSecurityInfo( + nsITransportSecurityInfo** secInfo) { + *secInfo = nullptr; + if (m_transport) { + nsCOMPtr<nsISocketTransport> strans = do_QueryInterface(m_transport); + if (strans) { + nsCOMPtr<nsITLSSocketControl> tlsSocketControl; + if (NS_SUCCEEDED( + strans->GetTlsSocketControl(getter_AddRefs(tlsSocketControl)))) { + nsCOMPtr<nsITransportSecurityInfo> transportSecInfo; + if (NS_SUCCEEDED(tlsSocketControl->GetSecurityInfo( + getter_AddRefs(transportSecInfo)))) { + transportSecInfo.forget(secInfo); + } + } + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::GetName(nsACString& result) { + if (m_url) return m_url->GetSpec(result); + result.Truncate(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::GetOwner(nsISupports** aPrincipal) { + NS_IF_ADDREF(*aPrincipal = mOwner); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::SetOwner(nsISupports* aPrincipal) { + mOwner = aPrincipal; + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::GetLoadGroup(nsILoadGroup** aLoadGroup) { + NS_IF_ADDREF(*aLoadGroup = m_loadGroup); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::GetLoadInfo(nsILoadInfo** aLoadInfo) { + NS_IF_ADDREF(*aLoadInfo = m_loadInfo); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::SetLoadInfo(nsILoadInfo* aLoadInfo) { + m_loadInfo = aLoadInfo; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgProtocol::GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) { + NS_IF_ADDREF(*aNotificationCallbacks = mCallbacks.get()); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgProtocol::SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) { + mCallbacks = aNotificationCallbacks; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgProtocol::OnTransportStatus(nsITransport* transport, nsresult status, + int64_t progress, int64_t progressMax) { + if ((mLoadFlags & LOAD_BACKGROUND) || !m_url) return NS_OK; + + // these transport events should not generate any status messages + if (status == NS_NET_STATUS_RECEIVING_FROM || + status == NS_NET_STATUS_SENDING_TO) + return NS_OK; + + if (!mProgressEventSink) { + NS_QueryNotificationCallbacks(mCallbacks, m_loadGroup, mProgressEventSink); + if (!mProgressEventSink) return NS_OK; + } + + nsAutoCString host; + m_url->GetHost(host); + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(m_url); + if (mailnewsUrl) { + nsCOMPtr<nsIMsgIncomingServer> server; + mailnewsUrl->GetServer(getter_AddRefs(server)); + if (server) server->GetHostName(host); + } + mProgressEventSink->OnStatus(this, status, NS_ConvertUTF8toUTF16(host).get()); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgProtocol::GetIsDocument(bool* aIsDocument) { + return NS_GetIsDocumentChannel(this, aIsDocument); +} + +//////////////////////////////////////////////////////////////////////////////// +// From nsIRequest +//////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP nsMsgProtocol::IsPending(bool* result) { + *result = m_channelListener != nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::GetStatus(nsresult* status) { + if (m_request) return m_request->GetStatus(status); + + *status = NS_OK; + return *status; +} + +NS_IMETHODIMP nsMsgProtocol::SetCanceledReason(const nsACString& aReason) { + return SetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsMsgProtocol::GetCanceledReason(nsACString& aReason) { + return GetCanceledReasonImpl(aReason); +} + +NS_IMETHODIMP nsMsgProtocol::CancelWithReason(nsresult aStatus, + const nsACString& aReason) { + return CancelWithReasonImpl(aStatus, aReason); +} + +NS_IMETHODIMP nsMsgProtocol::Cancel(nsresult status) { + if (m_proxyRequest) m_proxyRequest->Cancel(status); + + if (m_request) return m_request->Cancel(status); + + NS_WARNING("no request to cancel"); + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsMsgProtocol::GetCanceled(bool* aCanceled) { + nsresult status = NS_ERROR_FAILURE; + GetStatus(&status); + *aCanceled = NS_FAILED(status); + return NS_OK; +} + +NS_IMETHODIMP nsMsgProtocol::Suspend() { + if (m_request) return m_request->Suspend(); + + NS_WARNING("no request to suspend"); + return NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP nsMsgProtocol::Resume() { + if (m_request) return m_request->Resume(); + + NS_WARNING("no request to resume"); + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult nsMsgProtocol::PostMessage(nsIURI* url, nsIFile* postFile) { + if (!url || !postFile) return NS_ERROR_NULL_POINTER; + +#define POST_DATA_BUFFER_SIZE 2048 + + // mscott -- this function should be re-written to use the file url code + // so it can be asynch + nsCOMPtr<nsIInputStream> inputStream; + nsresult rv = + NS_NewLocalFileInputStream(getter_AddRefs(inputStream), postFile); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsILineInputStream> lineInputStream( + do_QueryInterface(inputStream, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + bool more = true; + nsCString line; + nsCString outputBuffer; + + do { + lineInputStream->ReadLine(line, &more); + + /* escape starting periods + */ + if (line.CharAt(0) == '.') line.Insert('.', 0); + line.AppendLiteral(CRLF); + outputBuffer.Append(line); + // test hack by mscott. If our buffer is almost full, then + // send it off & reset ourselves + // to make more room. + if (outputBuffer.Length() > POST_DATA_BUFFER_SIZE || !more) { + rv = SendData(outputBuffer.get()); + NS_ENSURE_SUCCESS(rv, rv); + // does this keep the buffer around? That would be best. + // Maybe SetLength(0) instead? + outputBuffer.Truncate(); + } + } while (more); + + return NS_OK; +} + +nsresult nsMsgProtocol::DoGSSAPIStep1(const nsACString& service, + const char* username, + nsCString& response) { + nsresult rv; +#ifdef DEBUG_BenB + printf("GSSAPI step 1 for service %s, username %s\n", service, username); +#endif + + // if this fails, then it means that we cannot do GSSAPI SASL. + m_authModule = nsIAuthModule::CreateInstance("sasl-gssapi"); + + m_authModule->Init(service, nsIAuthModule::REQ_DEFAULT, u""_ns, + NS_ConvertUTF8toUTF16(username), u""_ns); + + void* outBuf; + uint32_t outBufLen; + rv = m_authModule->GetNextToken((void*)nullptr, 0, &outBuf, &outBufLen); + if (NS_SUCCEEDED(rv) && outBuf) { + char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr); + if (base64Str) + response.Adopt(base64Str); + else + rv = NS_ERROR_OUT_OF_MEMORY; + free(outBuf); + } + +#ifdef DEBUG_BenB + printf("GSSAPI step 1 succeeded\n"); +#endif + return rv; +} + +nsresult nsMsgProtocol::DoGSSAPIStep2(nsCString& commandResponse, + nsCString& response) { +#ifdef DEBUG_BenB + printf("GSSAPI step 2\n"); +#endif + nsresult rv; + void *inBuf, *outBuf; + uint32_t inBufLen, outBufLen; + uint32_t len = commandResponse.Length(); + + // Cyrus SASL may send us zero length tokens (grrrr) + if (len > 0) { + // decode into the input secbuffer + inBufLen = (len * 3) / 4; // sufficient size (see plbase64.h) + inBuf = moz_xmalloc(inBufLen); + if (!inBuf) return NS_ERROR_OUT_OF_MEMORY; + + // strip off any padding (see bug 230351) + const char* challenge = commandResponse.get(); + while (challenge[len - 1] == '=') len--; + + // We need to know the exact length of the decoded string to give to + // the GSSAPI libraries. But NSPR's base64 routine doesn't seem capable + // of telling us that. So, we figure it out for ourselves. + + // For every 4 characters, add 3 to the destination + // If there are 3 remaining, add 2 + // If there are 2 remaining, add 1 + // 1 remaining is an error + inBufLen = + (len / 4) * 3 + ((len % 4 == 3) ? 2 : 0) + ((len % 4 == 2) ? 1 : 0); + + rv = (PL_Base64Decode(challenge, len, (char*)inBuf)) + ? m_authModule->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen) + : NS_ERROR_FAILURE; + + free(inBuf); + } else { + rv = m_authModule->GetNextToken(NULL, 0, &outBuf, &outBufLen); + } + if (NS_SUCCEEDED(rv)) { + // And in return, we may need to send Cyrus zero length tokens back + if (outBuf) { + char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr); + if (base64Str) + response.Adopt(base64Str); + else + rv = NS_ERROR_OUT_OF_MEMORY; + } else + response.Adopt((char*)moz_xmemdup("", 1)); + } + +#ifdef DEBUG_BenB + printf(NS_SUCCEEDED(rv) ? "GSSAPI step 2 succeeded\n" + : "GSSAPI step 2 failed\n"); +#endif + return rv; +} + +nsresult nsMsgProtocol::DoNtlmStep1(const nsACString& username, + const nsAString& password, + nsCString& response) { + nsresult rv; + + m_authModule = nsIAuthModule::CreateInstance("ntlm"); + + m_authModule->Init(""_ns, 0, u""_ns, NS_ConvertUTF8toUTF16(username), + PromiseFlatString(password)); + + void* outBuf; + uint32_t outBufLen; + rv = m_authModule->GetNextToken((void*)nullptr, 0, &outBuf, &outBufLen); + if (NS_SUCCEEDED(rv) && outBuf) { + char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr); + if (base64Str) + response.Adopt(base64Str); + else + rv = NS_ERROR_OUT_OF_MEMORY; + free(outBuf); + } + + return rv; +} + +nsresult nsMsgProtocol::DoNtlmStep2(nsCString& commandResponse, + nsCString& response) { + nsresult rv; + void *inBuf, *outBuf; + uint32_t inBufLen, outBufLen; + uint32_t len = commandResponse.Length(); + + // decode into the input secbuffer + inBufLen = (len * 3) / 4; // sufficient size (see plbase64.h) + inBuf = moz_xmalloc(inBufLen); + if (!inBuf) return NS_ERROR_OUT_OF_MEMORY; + + // strip off any padding (see bug 230351) + const char* challenge = commandResponse.get(); + while (challenge[len - 1] == '=') len--; + + rv = (PL_Base64Decode(challenge, len, (char*)inBuf)) + ? m_authModule->GetNextToken(inBuf, inBufLen, &outBuf, &outBufLen) + : NS_ERROR_FAILURE; + + free(inBuf); + if (NS_SUCCEEDED(rv) && outBuf) { + char* base64Str = PL_Base64Encode((char*)outBuf, outBufLen, nullptr); + if (base64Str) + response.Adopt(base64Str); + else + rv = NS_ERROR_OUT_OF_MEMORY; + } + + if (NS_FAILED(rv)) response = "*"; + + return rv; +} + +///////////////////////////////////////////////////////////////////// +// nsMsgAsyncWriteProtocol subclass and related helper classes +///////////////////////////////////////////////////////////////////// + +class nsMsgProtocolStreamProvider : public nsIOutputStreamCallback { + public: + // XXX this probably doesn't need to be threadsafe + NS_DECL_THREADSAFE_ISUPPORTS + + nsMsgProtocolStreamProvider() {} + + void Init(nsMsgAsyncWriteProtocol* aProtInstance, + nsIInputStream* aInputStream) { + mMsgProtocol = + do_GetWeakReference(static_cast<nsIStreamListener*>(aProtInstance)); + mInStream = aInputStream; + } + + // + // nsIOutputStreamCallback implementation ... + // + NS_IMETHODIMP OnOutputStreamReady(nsIAsyncOutputStream* aOutStream) override { + NS_ASSERTION(mInStream, "not initialized"); + + nsresult rv; + uint64_t avail; + + // Write whatever is available in the pipe. If the pipe is empty, then + // return NS_BASE_STREAM_WOULD_BLOCK; we will resume the write when there + // is more data. + + rv = mInStream->Available(&avail); + if (NS_FAILED(rv)) return rv; + + nsMsgAsyncWriteProtocol* protInst = nullptr; + nsCOMPtr<nsIStreamListener> callback = do_QueryReferent(mMsgProtocol); + if (!callback) return NS_ERROR_FAILURE; + protInst = static_cast<nsMsgAsyncWriteProtocol*>(callback.get()); + + if (avail == 0 && !protInst->mAsyncBuffer.Length()) { + // ok, stop writing... + protInst->mSuspendedWrite = true; + return NS_OK; + } + protInst->mSuspendedWrite = false; + + uint32_t bytesWritten; + + if (avail) { + rv = aOutStream->WriteFrom(mInStream, + std::min(avail, uint64_t(FILE_IO_BUFFER_SIZE)), + &bytesWritten); + // if were full at the time, the input stream may be backed up and we need + // to read any remains from the last ODA call before we'll get more ODA + // calls + if (protInst->mSuspendedRead) protInst->UnblockPostReader(); + } else { + rv = aOutStream->Write(protInst->mAsyncBuffer.get(), + protInst->mAsyncBuffer.Length(), &bytesWritten); + protInst->mAsyncBuffer.Cut(0, bytesWritten); + } + + protInst->UpdateProgress(bytesWritten); + + // try to write again... + if (NS_SUCCEEDED(rv)) + rv = aOutStream->AsyncWait(this, 0, 0, protInst->mProviderThread); + + NS_ASSERTION(NS_SUCCEEDED(rv) || rv == NS_BINDING_ABORTED, + "unexpected error writing stream"); + return NS_OK; + } + + protected: + virtual ~nsMsgProtocolStreamProvider() {} + + nsWeakPtr mMsgProtocol; + nsCOMPtr<nsIInputStream> mInStream; +}; + +NS_IMPL_ISUPPORTS(nsMsgProtocolStreamProvider, nsIOutputStreamCallback) + +class nsMsgFilePostHelper : public nsIStreamListener { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + + nsMsgFilePostHelper() { mSuspendedPostFileRead = false; } + nsresult Init(nsIOutputStream* aOutStream, + nsMsgAsyncWriteProtocol* aProtInstance, nsIFile* aFileToPost); + nsCOMPtr<nsIRequest> mPostFileRequest; + bool mSuspendedPostFileRead; + void CloseSocket() { mProtInstance = nullptr; } + + protected: + virtual ~nsMsgFilePostHelper() {} + nsCOMPtr<nsIOutputStream> mOutStream; + nsWeakPtr mProtInstance; +}; + +NS_IMPL_ISUPPORTS(nsMsgFilePostHelper, nsIStreamListener, nsIRequestObserver) + +nsresult nsMsgFilePostHelper::Init(nsIOutputStream* aOutStream, + nsMsgAsyncWriteProtocol* aProtInstance, + nsIFile* aFileToPost) { + nsresult rv = NS_OK; + mOutStream = aOutStream; + mProtInstance = + do_GetWeakReference(static_cast<nsIStreamListener*>(aProtInstance)); + + nsCOMPtr<nsIInputStream> stream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), aFileToPost); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr<nsIInputStreamPump> pump; + rv = NS_NewInputStreamPump(getter_AddRefs(pump), stream.forget()); + if (NS_FAILED(rv)) return rv; + + rv = pump->AsyncRead(this); + if (NS_FAILED(rv)) return rv; + + mPostFileRequest = pump; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilePostHelper::OnStartRequest(nsIRequest* aChannel) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilePostHelper::OnStopRequest(nsIRequest* aChannel, + nsresult aStatus) { + nsMsgAsyncWriteProtocol* protInst = nullptr; + nsCOMPtr<nsIStreamListener> callback = do_QueryReferent(mProtInstance); + if (!callback) return NS_OK; + protInst = static_cast<nsMsgAsyncWriteProtocol*>(callback.get()); + + if (!mSuspendedPostFileRead) protInst->PostDataFinished(); + + mSuspendedPostFileRead = false; + protInst->mFilePostHelper = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgFilePostHelper::OnDataAvailable(nsIRequest* /* aChannel */, + nsIInputStream* inStr, + uint64_t sourceOffset, + uint32_t count) { + nsMsgAsyncWriteProtocol* protInst = nullptr; + nsCOMPtr<nsIStreamListener> callback = do_QueryReferent(mProtInstance); + if (!callback) return NS_OK; + + protInst = static_cast<nsMsgAsyncWriteProtocol*>(callback.get()); + + if (mSuspendedPostFileRead) { + protInst->UpdateSuspendedReadBytes(count, protInst->mInsertPeriodRequired); + return NS_OK; + } + + protInst->ProcessIncomingPostData(inStr, count); + + if (protInst->mSuspendedWrite) { + // if we got here then we had suspended the write 'cause we didn't have + // anymore data to write (i.e. the pipe went empty). So resume the channel + // to kick things off again. + protInst->mSuspendedWrite = false; + protInst->mAsyncOutStream->AsyncWait(protInst->mProvider, 0, 0, + protInst->mProviderThread); + } + + return NS_OK; +} + +NS_IMPL_ADDREF_INHERITED(nsMsgAsyncWriteProtocol, nsMsgProtocol) +NS_IMPL_RELEASE_INHERITED(nsMsgAsyncWriteProtocol, nsMsgProtocol) + +NS_INTERFACE_MAP_BEGIN(nsMsgAsyncWriteProtocol) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) +NS_INTERFACE_MAP_END_INHERITING(nsMsgProtocol) + +nsMsgAsyncWriteProtocol::nsMsgAsyncWriteProtocol(nsIURI* aURL) + : nsMsgProtocol(aURL) { + mSuspendedWrite = false; + mSuspendedReadBytes = 0; + mSuspendedRead = false; + mInsertPeriodRequired = false; + mGenerateProgressNotifications = false; + mSuspendedReadBytesPostPeriod = 0; + mFilePostHelper = nullptr; + mNumBytesPosted = 0; + mFilePostSize = 0; +} + +nsMsgAsyncWriteProtocol::~nsMsgAsyncWriteProtocol() {} + +NS_IMETHODIMP nsMsgAsyncWriteProtocol::Cancel(nsresult status) { + mGenerateProgressNotifications = false; + + if (m_proxyRequest) { + m_proxyRequest->Cancel(status); + } + + if (m_request) m_request->Cancel(status); + + if (mAsyncOutStream) mAsyncOutStream->CloseWithStatus(status); + + return NS_OK; +} + +nsresult nsMsgAsyncWriteProtocol::PostMessage(nsIURI* url, nsIFile* file) { + nsCOMPtr<nsIStreamListener> listener = new nsMsgFilePostHelper(); + + if (!listener) return NS_ERROR_OUT_OF_MEMORY; + + // be sure to initialize some state before posting + mSuspendedReadBytes = 0; + mNumBytesPosted = 0; + file->GetFileSize(&mFilePostSize); + mSuspendedRead = false; + mInsertPeriodRequired = false; + mSuspendedReadBytesPostPeriod = 0; + mGenerateProgressNotifications = true; + + mFilePostHelper = static_cast<nsMsgFilePostHelper*>( + static_cast<nsIStreamListener*>(listener)); + + static_cast<nsMsgFilePostHelper*>(static_cast<nsIStreamListener*>(listener)) + ->Init(m_outputStream, this, file); + + return NS_OK; +} + +nsresult nsMsgAsyncWriteProtocol::SuspendPostFileRead() { + if (mFilePostHelper && !mFilePostHelper->mSuspendedPostFileRead) { + // uhoh we need to pause reading in the file until we get unblocked... + mFilePostHelper->mPostFileRequest->Suspend(); + mFilePostHelper->mSuspendedPostFileRead = true; + } + + return NS_OK; +} + +nsresult nsMsgAsyncWriteProtocol::ResumePostFileRead() { + if (mFilePostHelper) { + if (mFilePostHelper->mSuspendedPostFileRead) { + mFilePostHelper->mPostFileRequest->Resume(); + mFilePostHelper->mSuspendedPostFileRead = false; + } + } else // we must be done with the download so send the '.' + { + PostDataFinished(); + } + + return NS_OK; +} + +nsresult nsMsgAsyncWriteProtocol::UpdateSuspendedReadBytes( + uint32_t aNewBytes, bool aAddToPostPeriodByteCount) { + // depending on our current state, we'll either add aNewBytes to + // mSuspendedReadBytes or mSuspendedReadBytesAfterPeriod. + + mSuspendedRead = true; + if (aAddToPostPeriodByteCount) + mSuspendedReadBytesPostPeriod += aNewBytes; + else + mSuspendedReadBytes += aNewBytes; + + return NS_OK; +} + +nsresult nsMsgAsyncWriteProtocol::PostDataFinished() { + nsresult rv = SendData("." CRLF); + if (NS_FAILED(rv)) return rv; + mGenerateProgressNotifications = false; + mPostDataStream = nullptr; + return NS_OK; +} + +nsresult nsMsgAsyncWriteProtocol::ProcessIncomingPostData(nsIInputStream* inStr, + uint32_t count) { + if (!m_socketIsOpen) return NS_OK; // kick out if the socket was canceled + + // We need to quote any '.' that occur at the beginning of a line. + // but I don't want to waste time reading out the data into a buffer and + // searching let's try to leverage nsIBufferedInputStream and see if we can + // "peek" into the current contents for this particular case. + + nsCOMPtr<nsISearchableInputStream> bufferInputStr = do_QueryInterface(inStr); + NS_ASSERTION( + bufferInputStr, + "i made a wrong assumption about the type of stream we are getting"); + NS_ASSERTION(mSuspendedReadBytes == 0, "oops, I missed something"); + + if (!mPostDataStream) mPostDataStream = inStr; + + if (bufferInputStr) { + uint32_t amountWritten; + + while (count > 0) { + bool found = false; + uint32_t offset = 0; + bufferInputStr->Search("\012.", true, &found, &offset); // LF. + + if (!found || offset > count) { + // push this data into the output stream + m_outputStream->WriteFrom(inStr, count, &amountWritten); + // store any remains which need read out at a later date + if (count > amountWritten) // stream will block + { + UpdateSuspendedReadBytes(count - amountWritten, false); + SuspendPostFileRead(); + } + break; + } else { + // count points to the LF in a LF followed by a '.' + // go ahead and write up to offset.. + m_outputStream->WriteFrom(inStr, offset + 1, &amountWritten); + count -= amountWritten; + if (offset + 1 > amountWritten) { + UpdateSuspendedReadBytes(offset + 1 - amountWritten, false); + mInsertPeriodRequired = true; + UpdateSuspendedReadBytes(count, mInsertPeriodRequired); + SuspendPostFileRead(); + break; + } + + // write out the extra '.' + m_outputStream->Write(".", 1, &amountWritten); + if (amountWritten != 1) { + mInsertPeriodRequired = true; + // once we do write out the '.', if we are now blocked we need to + // remember the remaining count that comes after the '.' so we can + // perform processing on that once we become unblocked. + UpdateSuspendedReadBytes(count, mInsertPeriodRequired); + SuspendPostFileRead(); + break; + } + } + } // while count > 0 + } + + return NS_OK; +} +nsresult nsMsgAsyncWriteProtocol::UnblockPostReader() { + uint32_t amountWritten = 0; + + if (!m_socketIsOpen) return NS_OK; // kick out if the socket was canceled + + if (mSuspendedRead) { + // (1) attempt to write out any remaining read bytes we need in order to + // unblock the reader + if (mSuspendedReadBytes > 0 && mPostDataStream) { + uint64_t avail = 0; + mPostDataStream->Available(&avail); + + m_outputStream->WriteFrom(mPostDataStream, + std::min(avail, uint64_t(mSuspendedReadBytes)), + &amountWritten); + // hmm sometimes my mSuspendedReadBytes is getting out of whack...so for + // now, reset it if necessary. + if (mSuspendedReadBytes > avail) mSuspendedReadBytes = avail; + + if (mSuspendedReadBytes > amountWritten) + mSuspendedReadBytes -= amountWritten; + else + mSuspendedReadBytes = 0; + } + + // (2) if we are now unblocked, and we need to insert a '.' then do so + // now... + if (mInsertPeriodRequired && mSuspendedReadBytes == 0) { + amountWritten = 0; + m_outputStream->Write(".", 1, &amountWritten); + if (amountWritten == 1) // if we succeeded then clear pending '.' flag + mInsertPeriodRequired = false; + } + + // (3) if we inserted a '.' and we still have bytes after the '.' which need + // processed before the stream is unblocked then fake an ODA call to handle + // this now... + if (!mInsertPeriodRequired && mSuspendedReadBytesPostPeriod > 0) { + // these bytes actually need processed for extra '.''s..... + uint32_t postbytes = mSuspendedReadBytesPostPeriod; + mSuspendedReadBytesPostPeriod = 0; + ProcessIncomingPostData(mPostDataStream, postbytes); + } + + // (4) determine if we are out of the suspended read state... + if (mSuspendedReadBytes == 0 && !mInsertPeriodRequired && + mSuspendedReadBytesPostPeriod == 0) { + mSuspendedRead = false; + ResumePostFileRead(); + } + + } // if we are in the suspended read state + + return NS_OK; +} + +nsresult nsMsgAsyncWriteProtocol::SetupTransportState() { + nsresult rv = NS_OK; + + if (!m_outputStream && m_transport) { + // first create a pipe which we'll use to write the data we want to send + // into. + nsCOMPtr<nsIPipe> pipe = do_CreateInstance("@mozilla.org/pipe;1"); + rv = pipe->Init(true, true, 1024, 8); + NS_ENSURE_SUCCESS(rv, rv); + + nsIAsyncInputStream* inputStream = nullptr; + // This always succeeds because the pipe is initialized above. + MOZ_ALWAYS_SUCCEEDS(pipe->GetInputStream(&inputStream)); + mInStream = dont_AddRef(static_cast<nsIInputStream*>(inputStream)); + + nsIAsyncOutputStream* outputStream = nullptr; + // This always succeeds because the pipe is initialized above. + MOZ_ALWAYS_SUCCEEDS(pipe->GetOutputStream(&outputStream)); + m_outputStream = dont_AddRef(static_cast<nsIOutputStream*>(outputStream)); + + mProviderThread = do_GetCurrentThread(); + + nsMsgProtocolStreamProvider* provider = new nsMsgProtocolStreamProvider(); + + if (!provider) return NS_ERROR_OUT_OF_MEMORY; + + provider->Init(this, mInStream); + mProvider = provider; // ADDREF + + nsCOMPtr<nsIOutputStream> stream; + rv = m_transport->OpenOutputStream(0, 0, 0, getter_AddRefs(stream)); + if (NS_FAILED(rv)) return rv; + + mAsyncOutStream = do_QueryInterface(stream, &rv); + if (NS_FAILED(rv)) return rv; + + // wait for the output stream to become writable + rv = mAsyncOutStream->AsyncWait(mProvider, 0, 0, mProviderThread); + } // if m_transport + + return rv; +} + +nsresult nsMsgAsyncWriteProtocol::CloseSocket() { + nsresult rv = NS_OK; + if (mAsyncOutStream) mAsyncOutStream->CloseWithStatus(NS_BINDING_ABORTED); + + nsMsgProtocol::CloseSocket(); + + if (mFilePostHelper) { + mFilePostHelper->CloseSocket(); + mFilePostHelper = nullptr; + } + + mAsyncOutStream = nullptr; + mProvider = nullptr; + mProviderThread = nullptr; + mAsyncBuffer.Truncate(); + return rv; +} + +void nsMsgAsyncWriteProtocol::UpdateProgress(uint32_t aNewBytes) { + if (!mGenerateProgressNotifications) return; + + mNumBytesPosted += aNewBytes; + if (mFilePostSize > 0) { + nsCOMPtr<nsIMsgMailNewsUrl> mailUrl = do_QueryInterface(m_url); + if (!mailUrl) return; + + nsCOMPtr<nsIMsgStatusFeedback> statusFeedback; + mailUrl->GetStatusFeedback(getter_AddRefs(statusFeedback)); + if (!statusFeedback) return; + + nsCOMPtr<nsIWebProgressListener> webProgressListener( + do_QueryInterface(statusFeedback)); + if (!webProgressListener) return; + + // XXX not sure if m_request is correct here + webProgressListener->OnProgressChange(nullptr, m_request, mNumBytesPosted, + static_cast<uint32_t>(mFilePostSize), + mNumBytesPosted, mFilePostSize); + } + + return; +} + +nsresult nsMsgAsyncWriteProtocol::SendData(const char* dataBuffer, + bool aSuppressLogging) { + this->mAsyncBuffer.Append(dataBuffer); + if (!mAsyncOutStream) return NS_ERROR_FAILURE; + return mAsyncOutStream->AsyncWait(mProvider, 0, 0, mProviderThread); +} + +char16_t* FormatStringWithHostNameByName(const char16_t* stringName, + nsIMsgMailNewsUrl* msgUri) { + if (!msgUri) return nullptr; + + nsresult rv; + + nsCOMPtr<nsIStringBundleService> sBundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sBundleService, nullptr); + + nsCOMPtr<nsIStringBundle> sBundle; + rv = sBundleService->CreateBundle(MSGS_URL, getter_AddRefs(sBundle)); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = msgUri->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, nullptr); + + nsCString hostName; + rv = server->GetHostName(hostName); + NS_ENSURE_SUCCESS(rv, nullptr); + + AutoTArray<nsString, 1> params; + CopyASCIItoUTF16(hostName, *params.AppendElement()); + nsAutoString str; + rv = sBundle->FormatStringFromName(NS_ConvertUTF16toUTF8(stringName).get(), + params, str); + NS_ENSURE_SUCCESS(rv, nullptr); + + return ToNewUnicode(str); +} + +// vim: ts=2 sw=2 diff --git a/comm/mailnews/base/src/nsMsgProtocol.h b/comm/mailnews/base/src/nsMsgProtocol.h new file mode 100644 index 0000000000..c4aa707300 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgProtocol.h @@ -0,0 +1,263 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgProtocol_h__ +#define nsMsgProtocol_h__ + +#include "mozilla/Attributes.h" +#include "nsIStreamListener.h" +#include "nsIInputStream.h" +#include "nsIOutputStream.h" +#include "nsIChannel.h" +#include "nsIURL.h" +#include "nsIThread.h" +#include "nsILoadGroup.h" +#include "nsIFile.h" +#include "nsCOMPtr.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIProgressEventSink.h" +#include "nsITransport.h" +#include "nsIAsyncOutputStream.h" +#include "nsIAuthModule.h" +#include "nsString.h" +#include "nsWeakReference.h" +#include "nsHashPropertyBag.h" +#include "nsMailChannel.h" + +class nsIMsgWindow; +class nsIPrompt; +class nsIMsgMailNewsUrl; +class nsMsgFilePostHelper; +class nsIProxyInfo; +class nsICancelable; + +// This is a helper class used to encapsulate code shared between all of the +// mailnews protocol objects (imap, news, pop, smtp, etc.) In particular, +// it unifies the core networking code for the protocols. My hope is that +// this will make unification with Necko easier as we'll only have to change +// this class and not all of the mailnews protocols. +class nsMsgProtocol : public nsIStreamListener, + public nsIChannel, + public nsITransportEventSink, + public nsMailChannel, + public nsHashPropertyBag { + public: + nsMsgProtocol(nsIURI* aURL); + + NS_DECL_ISUPPORTS_INHERITED + // nsIChannel support + NS_DECL_NSICHANNEL + NS_DECL_NSIREQUEST + + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSITRANSPORTEVENTSINK + + // LoadUrl -- A protocol typically overrides this function, sets up any local + // state for the url and then calls the base class which opens the socket if + // it needs opened. If the socket is already opened then we just call + // ProcessProtocolState to start the churning process. aConsumer is the + // consumer for the url. It can be null if this argument is not appropriate + virtual nsresult LoadUrl(nsIURI* aURL, nsISupports* aConsumer = nullptr); + + virtual nsresult SetUrl( + nsIURI* aURL); // sometimes we want to set the url before we load it + void ShowAlertMessage(nsIMsgMailNewsUrl* aMsgUrl, nsresult aStatus); + + // Flag manipulators + virtual bool TestFlag(uint32_t flag) { return flag & m_flags; } + virtual void SetFlag(uint32_t flag) { m_flags |= flag; } + virtual void ClearFlag(uint32_t flag) { m_flags &= ~flag; } + + protected: + virtual ~nsMsgProtocol(); + + // methods for opening and closing a socket with core netlib.... + // mscott -okay this is lame. I should break this up into a file protocol and + // a socket based protocool class instead of cheating and putting both methods + // here... + + // open a connection with a specific host and port + // aHostName must be UTF-8 encoded. + virtual nsresult OpenNetworkSocketWithInfo(const char* aHostName, + int32_t aGetPort, + const char* connectionType, + nsIProxyInfo* aProxyInfo, + nsIInterfaceRequestor* callbacks); + // helper routine + nsresult GetFileFromURL(nsIURI* aURL, nsIFile** aResult); + virtual nsresult OpenFileSocket( + nsIURI* aURL, uint64_t aStartPosition, + int64_t aReadCount); // used to open a file socket connection + + nsresult GetTopmostMsgWindow(nsIMsgWindow** aWindow); + + virtual const char* GetType() { return nullptr; } + nsresult GetQoSBits(uint8_t* aQoSBits); + + // a Protocol typically overrides this method. They free any of their own + // connection state and then they call up into the base class to free the + // generic connection objects + virtual nsresult CloseSocket(); + + virtual nsresult + SetupTransportState(); // private method used by OpenNetworkSocket and + // OpenFileSocket + + // ProcessProtocolState - This is the function that gets churned by calls to + // OnDataAvailable. As data arrives on the socket, OnDataAvailable calls + // ProcessProtocolState. + + virtual nsresult ProcessProtocolState(nsIURI* url, + nsIInputStream* inputStream, + uint64_t sourceOffset, + uint32_t length) = 0; + + // SendData -- Writes the data contained in dataBuffer into the current output + // stream. It also informs the transport layer that this data is now available + // for transmission. Returns a positive number for success, 0 for failure (not + // all the bytes were written to the stream, etc). aSuppressLogging is a hint + // that sensitive data is being sent and should not be logged + virtual nsresult SendData(const char* dataBuffer, + bool aSuppressLogging = false); + + virtual nsresult PostMessage(nsIURI* url, nsIFile* aPostFile); + + virtual nsresult InitFromURI(nsIURI* aUrl); + + nsresult DoNtlmStep1(const nsACString& username, const nsAString& password, + nsCString& response); + nsresult DoNtlmStep2(nsCString& commandResponse, nsCString& response); + + nsresult DoGSSAPIStep1(const nsACString& service, const char* username, + nsCString& response); + nsresult DoGSSAPIStep2(nsCString& commandResponse, nsCString& response); + // Output stream for writing commands to the socket + nsCOMPtr<nsIOutputStream> + m_outputStream; // this will be obtained from the transport interface + + // Output stream for writing commands to the socket + nsCOMPtr<nsITransport> m_transport; + nsCOMPtr<nsIRequest> m_request; + nsCOMPtr<nsICancelable> m_proxyRequest; + + bool m_socketIsOpen; // mscott: we should look into keeping this state in the + // nsSocketTransport... I'm using it to make sure I open + // the socket the first time a URL is loaded into the + // connection + uint32_t m_flags; // used to store flag information + // uint32_t m_startPosition; + int64_t m_readCount; + + nsCOMPtr<nsIFile> + m_tempMsgFile; // we currently have a hack where displaying a msg + // involves writing it to a temp file first + + // auth module for access to NTLM functions + nsCOMPtr<nsIAuthModule> m_authModule; + + // the following is a catch all for nsIChannel related data + nsCOMPtr<nsIURI> m_originalUrl; // the original url + nsCOMPtr<nsIURI> m_url; // the running url + nsCOMPtr<nsISupports> m_consumer; + nsCOMPtr<nsIStreamListener> m_channelListener; + bool m_isChannel; + nsCOMPtr<nsILoadGroup> m_loadGroup; + nsLoadFlags mLoadFlags; + nsCOMPtr<nsIProgressEventSink> mProgressEventSink; + nsCOMPtr<nsIInterfaceRequestor> mCallbacks; + nsCOMPtr<nsISupports> mOwner; + nsCString mContentType; + nsCString mCharset; + int64_t mContentLength; + nsCOMPtr<nsILoadInfo> m_loadInfo; + + nsString m_lastPasswordSent; // used to prefill the password prompt + + // if a url isn't going to result in any content then we want to suppress + // calls to OnStartRequest, OnDataAvailable and OnStopRequest + bool mSuppressListenerNotifications; + + uint32_t mContentDisposition; +}; + +// This is is a subclass of nsMsgProtocol extends the parent class with +// AsyncWrite support. Protocols like smtp and news want to leverage async +// write. We don't want everyone who inherits from nsMsgProtocol to have to pick +// up the extra overhead. +class nsMsgAsyncWriteProtocol : public nsMsgProtocol, + public nsSupportsWeakReference { + public: + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD Cancel(nsresult status) override; + + nsMsgAsyncWriteProtocol(nsIURI* aURL); + + // temporary over ride... + virtual nsresult PostMessage(nsIURI* url, nsIFile* postFile) override; + + // over ride the following methods from the base class + virtual nsresult SetupTransportState() override; + virtual nsresult SendData(const char* dataBuffer, + bool aSuppressLogging = false) override; + nsCString mAsyncBuffer; + + // if we suspended the asynch write while waiting for more data to write then + // this will be TRUE + bool mSuspendedWrite; + nsCOMPtr<nsIRequest> m_WriteRequest; + nsCOMPtr<nsIAsyncOutputStream> mAsyncOutStream; + nsCOMPtr<nsIOutputStreamCallback> mProvider; + nsCOMPtr<nsIThread> mProviderThread; + + // because we are reading the post data in asynchronously, it's possible that + // we aren't sending it out fast enough and the reading gets blocked. The + // following set of state variables are used to track this. + bool mSuspendedRead; + bool mInsertPeriodRequired; // do we need to insert a '.' as part of the + // unblocking process + + nsresult ProcessIncomingPostData(nsIInputStream* inStr, uint32_t count); + nsresult UnblockPostReader(); + nsresult UpdateSuspendedReadBytes(uint32_t aNewBytes, + bool aAddToPostPeriodByteCount); + nsresult PostDataFinished(); // this is so we'll send out a closing '.' and + // release any state related to the post + + // these two routines are used to pause and resume our loading of the file + // containing the contents we are trying to post. We call these routines when + // we aren't sending the bits out fast enough to keep up with the file read. + nsresult SuspendPostFileRead(); + nsresult ResumePostFileRead(); + nsresult UpdateSuspendedReadBytes(uint32_t aNewBytes); + void UpdateProgress(uint32_t aNewBytes); + nsMsgFilePostHelper* mFilePostHelper; // needs to be a weak reference + protected: + virtual ~nsMsgAsyncWriteProtocol(); + + // the streams for the pipe used to queue up data for the async write calls to + // the server. we actually re-use the same mOutStream variable in our parent + // class for the output stream to the socket channel. So no need for a new + // variable here. + nsCOMPtr<nsIInputStream> mInStream; + nsCOMPtr<nsIInputStream> mPostDataStream; + uint32_t mSuspendedReadBytes; // remaining # of bytes we need to read before + // the input stream becomes unblocked + uint32_t mSuspendedReadBytesPostPeriod; // # of bytes which need processed + // after we insert a '.' before the + // input stream becomes unblocked. + int64_t + mFilePostSize; // used for file size, we post a single message in a file + uint32_t mNumBytesPosted; // used for determining progress on posting files + bool + mGenerateProgressNotifications; // set during a post operation after + // we've started sending the post data... + + virtual nsresult CloseSocket() override; +}; + +#endif /* nsMsgProtocol_h__ */ diff --git a/comm/mailnews/base/src/nsMsgPurgeService.cpp b/comm/mailnews/base/src/nsMsgPurgeService.cpp new file mode 100644 index 0000000000..0f72416c7b --- /dev/null +++ b/comm/mailnews/base/src/nsMsgPurgeService.cpp @@ -0,0 +1,496 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgPurgeService.h" +#include "nsIMsgAccountManager.h" +#include "nsMsgUtils.h" +#include "nsMsgSearchCore.h" +#include "msgCore.h" +#include "nsISpamSettings.h" +#include "nsIMsgSearchTerm.h" +#include "nsIMsgHdr.h" +#include "nsIMsgProtocolInfo.h" +#include "nsIMsgFilterPlugin.h" +#include "nsIPrefBranch.h" +#include "nsIPrefService.h" +#include "mozilla/Logging.h" +#include "nsMsgFolderFlags.h" +#include "nsITimer.h" +#include <stdlib.h> +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" + +static mozilla::LazyLogModule MsgPurgeLogModule("MsgPurge"); + +NS_IMPL_ISUPPORTS(nsMsgPurgeService, nsIMsgPurgeService, nsIMsgSearchNotify) + +void OnPurgeTimer(nsITimer* timer, void* aPurgeService) { + nsMsgPurgeService* purgeService = (nsMsgPurgeService*)aPurgeService; + purgeService->PerformPurge(); +} + +nsMsgPurgeService::nsMsgPurgeService() { + mHaveShutdown = false; + // never purge a folder more than once every 8 hours (60 min/hour * 8 hours. + mMinDelayBetweenPurges = 480; + // fire the purge timer every 5 minutes, starting 5 minutes after the service + // is created (when we load accounts). + mPurgeTimerInterval = 5; +} + +nsMsgPurgeService::~nsMsgPurgeService() { + if (mPurgeTimer) mPurgeTimer->Cancel(); + + if (!mHaveShutdown) Shutdown(); +} + +NS_IMETHODIMP nsMsgPurgeService::Init() { + nsresult rv; + + // these prefs are here to help QA test this feature + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (NS_SUCCEEDED(rv)) { + int32_t min_delay; + rv = prefBranch->GetIntPref("mail.purge.min_delay", &min_delay); + if (NS_SUCCEEDED(rv) && min_delay) mMinDelayBetweenPurges = min_delay; + + int32_t purge_timer_interval; + rv = prefBranch->GetIntPref("mail.purge.timer_interval", + &purge_timer_interval); + if (NS_SUCCEEDED(rv) && purge_timer_interval) + mPurgeTimerInterval = purge_timer_interval; + } + + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("mail.purge.min_delay=%d minutes", mMinDelayBetweenPurges)); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("mail.purge.timer_interval=%d minutes", mPurgeTimerInterval)); + + // don't start purging right away. + // because the accounts aren't loaded and because the user might be trying to + // sign in or startup, etc. + SetupNextPurge(); + + mHaveShutdown = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgPurgeService::Shutdown() { + if (mPurgeTimer) { + mPurgeTimer->Cancel(); + mPurgeTimer = nullptr; + } + + mHaveShutdown = true; + return NS_OK; +} + +nsresult nsMsgPurgeService::SetupNextPurge() { + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("setting to check again in %d minutes", mPurgeTimerInterval)); + + // Convert mPurgeTimerInterval into milliseconds + uint32_t timeInMSUint32 = mPurgeTimerInterval * 60000; + + // Can't currently reset a timer when it's in the process of + // calling Notify. So, just release the timer here and create a new one. + if (mPurgeTimer) mPurgeTimer->Cancel(); + + nsresult rv = NS_NewTimerWithFuncCallback( + getter_AddRefs(mPurgeTimer), OnPurgeTimer, (void*)this, timeInMSUint32, + nsITimer::TYPE_ONE_SHOT, "nsMsgPurgeService::OnPurgeTimer", nullptr); + if (NS_FAILED(rv)) { + NS_WARNING("Could not start mPurgeTimer timer"); + } + + return NS_OK; +} + +// This is the function that looks for the first folder to purge. It also +// applies retention settings to any folder that hasn't had retention settings +// applied in mMinDelayBetweenPurges minutes (default, 8 hours). +// However, if we've spent more than .5 seconds in this loop, don't +// apply any more retention settings because it might lock up the UI. +// This might starve folders later on in the hierarchy, since we always +// start at the top, but since we also apply retention settings when you +// open a folder, or when you compact all folders, I think this will do +// for now, until we have a cleanup on shutdown architecture. +nsresult nsMsgPurgeService::PerformPurge() { + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("performing purge")); + + nsresult rv; + + nsCOMPtr<nsIMsgAccountManager> accountManager = + do_GetService("@mozilla.org/messenger/account-manager;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + bool keepApplyingRetentionSettings = true; + + nsTArray<RefPtr<nsIMsgIncomingServer>> allServers; + rv = accountManager->GetAllServers(allServers); + if (NS_SUCCEEDED(rv)) { + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("%d servers", (int)allServers.Length())); + nsCOMPtr<nsIMsgFolder> folderToPurge; + PRIntervalTime startTime = PR_IntervalNow(); + int32_t purgeIntervalToUse = 0; + PRTime oldestPurgeTime = + 0; // we're going to pick the least-recently purged folder + + // apply retention settings to folders that haven't had retention settings + // applied in mMinDelayBetweenPurges minutes (default 8 hours) + // Because we get last purge time from the folder cache, + // this code won't open db's for folders until it decides it needs + // to apply retention settings, and since + // nsIMsgFolder::ApplyRetentionSettings will close any db's it opens, this + // code won't leave db's open. + for (uint32_t serverIndex = 0; serverIndex < allServers.Length(); + serverIndex++) { + nsCOMPtr<nsIMsgIncomingServer> server(allServers[serverIndex]); + if (server) { + if (keepApplyingRetentionSettings) { + nsCOMPtr<nsIMsgFolder> rootFolder; + rv = server->GetRootFolder(getter_AddRefs(rootFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<RefPtr<nsIMsgFolder>> childFolders; + rv = rootFolder->GetDescendants(childFolders); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto childFolder : childFolders) { + uint32_t folderFlags; + (void)childFolder->GetFlags(&folderFlags); + if (folderFlags & nsMsgFolderFlags::Virtual) continue; + PRTime curFolderLastPurgeTime = 0; + nsCString curFolderLastPurgeTimeString, curFolderUri; + rv = childFolder->GetStringProperty("LastPurgeTime", + curFolderLastPurgeTimeString); + if (NS_FAILED(rv)) + continue; // it is ok to fail, go on to next folder + + if (!curFolderLastPurgeTimeString.IsEmpty()) { + PRTime theTime; + PR_ParseTimeString(curFolderLastPurgeTimeString.get(), false, + &theTime); + curFolderLastPurgeTime = theTime; + } + + childFolder->GetURI(curFolderUri); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("%s curFolderLastPurgeTime=%s (if blank, then never)", + curFolderUri.get(), curFolderLastPurgeTimeString.get())); + + // check if this folder is due to purge + // has to have been purged at least mMinDelayBetweenPurges minutes + // ago we don't want to purge the folders all the time - once a + // day is good enough + int64_t minDelayBetweenPurges(mMinDelayBetweenPurges); + int64_t microSecondsPerMinute(60000000); + PRTime nextPurgeTime = + curFolderLastPurgeTime + + (minDelayBetweenPurges * microSecondsPerMinute); + if (nextPurgeTime < PR_Now()) { + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("purging %s", curFolderUri.get())); + childFolder->ApplyRetentionSettings(); + } + PRIntervalTime elapsedTime = PR_IntervalNow() - startTime; + // check if more than 500 milliseconds have elapsed in this purge + // process + if (PR_IntervalToMilliseconds(elapsedTime) > 500) { + keepApplyingRetentionSettings = false; + break; + } + } + } + nsCString type; + nsresult rv = server->GetType(type); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString hostName; + server->GetHostName(hostName); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] %s (%s)", serverIndex, hostName.get(), type.get())); + + nsCOMPtr<nsISpamSettings> spamSettings; + rv = server->GetSpamSettings(getter_AddRefs(spamSettings)); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t spamLevel; + spamSettings->GetLevel(&spamLevel); + // clang-format off + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] spamLevel=%d (if 0, don't purge)", serverIndex, spamLevel)); + // clang-format on + if (!spamLevel) continue; + + // check if we are set up to purge for this server + // if not, skip it. + bool purgeSpam; + spamSettings->GetPurge(&purgeSpam); + + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] purgeSpam=%s (if false, don't purge)", serverIndex, + purgeSpam ? "true" : "false")); + if (!purgeSpam) continue; + + // check if the spam folder uri is set for this server + // if not skip it. + nsCString junkFolderURI; + rv = spamSettings->GetSpamFolderURI(junkFolderURI); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] junkFolderURI=%s (if empty, don't purge)", serverIndex, + junkFolderURI.get())); + if (junkFolderURI.IsEmpty()) continue; + + // if the junk folder doesn't exist + // because the folder pane isn't built yet, for example + // skip this account + nsCOMPtr<nsIMsgFolder> junkFolder; + rv = FindFolder(junkFolderURI, getter_AddRefs(junkFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + // clang-format off + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] %s exists? %s (if doesn't exist, don't purge)", serverIndex, + junkFolderURI.get(), junkFolder ? "true" : "false")); + // clang-format on + if (!junkFolder) continue; + + PRTime curJunkFolderLastPurgeTime = 0; + nsCString curJunkFolderLastPurgeTimeString; + rv = junkFolder->GetStringProperty("curJunkFolderLastPurgeTime", + curJunkFolderLastPurgeTimeString); + if (NS_FAILED(rv)) + continue; // it is ok to fail, junk folder may not exist + + if (!curJunkFolderLastPurgeTimeString.IsEmpty()) { + PRTime theTime; + PR_ParseTimeString(curJunkFolderLastPurgeTimeString.get(), false, + &theTime); + curJunkFolderLastPurgeTime = theTime; + } + + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] %s curJunkFolderLastPurgeTime=%s (if blank, then never)", + serverIndex, junkFolderURI.get(), + curJunkFolderLastPurgeTimeString.get())); + + // check if this account is due to purge + // has to have been purged at least mMinDelayBetweenPurges minutes ago + // we don't want to purge the folders all the time + PRTime nextPurgeTime = + curJunkFolderLastPurgeTime + + mMinDelayBetweenPurges * 60000000 // convert to microseconds. + ; + if (nextPurgeTime < PR_Now()) { + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] last purge greater than min delay", serverIndex)); + + nsCOMPtr<nsIMsgIncomingServer> junkFolderServer; + rv = junkFolder->GetServer(getter_AddRefs(junkFolderServer)); + NS_ENSURE_SUCCESS(rv, rv); + + bool serverBusy = false; + bool serverRequiresPassword = true; + bool passwordPromptRequired; + bool canSearchMessages = false; + junkFolderServer->GetPasswordPromptRequired(&passwordPromptRequired); + junkFolderServer->GetServerBusy(&serverBusy); + junkFolderServer->GetServerRequiresPasswordForBiff( + &serverRequiresPassword); + junkFolderServer->GetCanSearchMessages(&canSearchMessages); + // Make sure we're logged on before doing the search (assuming we need + // to be) and make sure the server isn't already in the middle of + // downloading new messages and make sure a search isn't already going + // on + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] (search in progress? %s)", serverIndex, + mSearchSession ? "true" : "false")); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] (server busy? %s)", serverIndex, + serverBusy ? "true" : "false")); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] (serverRequiresPassword? %s)", serverIndex, + serverRequiresPassword ? "true" : "false")); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] (passwordPromptRequired? %s)", serverIndex, + passwordPromptRequired ? "true" : "false")); + if (canSearchMessages && !mSearchSession && !serverBusy && + (!serverRequiresPassword || !passwordPromptRequired)) { + int32_t purgeInterval; + spamSettings->GetPurgeInterval(&purgeInterval); + + if ((oldestPurgeTime == 0) || + (curJunkFolderLastPurgeTime < oldestPurgeTime)) { + // clang-format off + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] purging! searching for messages older than %d days", + serverIndex, purgeInterval)); + // clang-format on + oldestPurgeTime = curJunkFolderLastPurgeTime; + purgeIntervalToUse = purgeInterval; + folderToPurge = junkFolder; + // if we've never purged this folder, do it... + if (curJunkFolderLastPurgeTime == 0) break; + } + } else { + NS_ASSERTION(canSearchMessages, + "unexpected, you should be able to search"); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] not a good time for this server, try again later", + serverIndex)); + } + } else { + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("[%d] last purge too recent", serverIndex)); + } + } + } + if (folderToPurge && purgeIntervalToUse != 0) + rv = SearchFolderToPurge(folderToPurge, purgeIntervalToUse); + } + + // set up timer to check accounts again + SetupNextPurge(); + return rv; +} + +nsresult nsMsgPurgeService::SearchFolderToPurge(nsIMsgFolder* folder, + int32_t purgeInterval) { + nsresult rv; + mSearchSession = + do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + mSearchSession->RegisterListener(this, nsIMsgSearchSession::allNotifications); + + // update the time we attempted to purge this folder + char dateBuf[100]; + dateBuf[0] = '\0'; + PRExplodedTime exploded; + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded); + PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y", + &exploded); + folder->SetStringProperty("curJunkFolderLastPurgeTime", + nsDependentCString(dateBuf)); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("curJunkFolderLastPurgeTime is now %s", dateBuf)); + + nsCOMPtr<nsIMsgIncomingServer> server; + // We need to get the folder's server scope because imap can have + // local junk folder. + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgSearchScopeValue searchScope; + server->GetSearchScope(&searchScope); + + mSearchSession->AddScopeTerm(searchScope, folder); + + // look for messages older than the cutoff + // you can't also search by junk status, see + // nsMsgPurgeService::OnSearchHit() + nsCOMPtr<nsIMsgSearchTerm> searchTerm; + mSearchSession->CreateTerm(getter_AddRefs(searchTerm)); + if (searchTerm) { + searchTerm->SetAttrib(nsMsgSearchAttrib::AgeInDays); + searchTerm->SetOp(nsMsgSearchOp::IsGreaterThan); + nsCOMPtr<nsIMsgSearchValue> searchValue; + searchTerm->GetValue(getter_AddRefs(searchValue)); + if (searchValue) { + searchValue->SetAttrib(nsMsgSearchAttrib::AgeInDays); + searchValue->SetAge((uint32_t)purgeInterval); + searchTerm->SetValue(searchValue); + } + searchTerm->SetBooleanAnd(false); + mSearchSession->AppendTerm(searchTerm); + } + + // we are about to search + // create mHdrsToDelete array (if not previously created) + NS_ASSERTION(mHdrsToDelete.IsEmpty(), "mHdrsToDelete is not empty"); + + mSearchFolder = folder; + return mSearchSession->Search(nullptr); +} + +NS_IMETHODIMP nsMsgPurgeService::OnNewSearch() { + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("on new search")); + return NS_OK; +} + +NS_IMETHODIMP nsMsgPurgeService::OnSearchHit(nsIMsgDBHdr* aMsgHdr, + nsIMsgFolder* aFolder) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + + nsCString messageId; + nsCString author; + nsCString subject; + + aMsgHdr->GetMessageId(getter_Copies(messageId)); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("messageId=%s", messageId.get())); + aMsgHdr->GetSubject(subject); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("subject=%s", subject.get())); + aMsgHdr->GetAuthor(getter_Copies(author)); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("author=%s", author.get())); + + // double check that the message is junk before adding to + // the list of messages to delete + // + // note, we can't just search for messages that are junk + // because not all imap server support keywords + // (which we use for the junk score) + // so the junk status would be in the message db. + // + // see bug #194090 + nsCString junkScoreStr; + nsresult rv = aMsgHdr->GetStringProperty("junkscore", junkScoreStr); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("junkScore=%s (if empty or != nsIJunkMailPlugin::IS_SPAM_SCORE, " + "don't add to list delete)", + junkScoreStr.get())); + + // if "junkscore" is not set, don't delete the message + if (junkScoreStr.IsEmpty()) return NS_OK; + + if (atoi(junkScoreStr.get()) == nsIJunkMailPlugin::IS_SPAM_SCORE) { + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("added message to delete")); + mHdrsToDelete.AppendElement(aMsgHdr); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgPurgeService::OnSearchDone(nsresult status) { + if (NS_SUCCEEDED(status)) { + uint32_t count = mHdrsToDelete.Length(); + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, + ("%d messages to delete", count)); + + if (count > 0) { + MOZ_LOG(MsgPurgeLogModule, mozilla::LogLevel::Info, ("delete messages")); + if (mSearchFolder) + mSearchFolder->DeleteMessages( + mHdrsToDelete, nullptr, false /*delete storage*/, false /*isMove*/, + nullptr, false /*allowUndo*/); + } + } + mHdrsToDelete.Clear(); + if (mSearchSession) mSearchSession->UnregisterListener(this); + // don't cache the session + // just create another search session next time we search, rather than + // clearing scopes, terms etc. we also use mSearchSession to determine if the + // purge service is "busy" + mSearchSession = nullptr; + mSearchFolder = nullptr; + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgPurgeService.h b/comm/mailnews/base/src/nsMsgPurgeService.h new file mode 100644 index 0000000000..30643624d7 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgPurgeService.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NSMSGPURGESERVICE_H +#define NSMSGPURGESERVICE_H + +#include "msgCore.h" +#include "nsIMsgPurgeService.h" +#include "nsIMsgSearchSession.h" +#include "nsITimer.h" +#include "nsCOMPtr.h" +#include "nsIMsgSearchNotify.h" +#include "nsIMsgFolder.h" +#include "nsIMsgFolderCache.h" +#include "nsIMsgFolderCacheElement.h" + +class nsMsgPurgeService : public nsIMsgPurgeService, public nsIMsgSearchNotify { + public: + nsMsgPurgeService(); + + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGPURGESERVICE + NS_DECL_NSIMSGSEARCHNOTIFY + + nsresult PerformPurge(); + + protected: + virtual ~nsMsgPurgeService(); + int32_t FindServer(nsIMsgIncomingServer* server); + nsresult SetupNextPurge(); + nsresult PurgeSurver(nsIMsgIncomingServer* server); + nsresult SearchFolderToPurge(nsIMsgFolder* folder, int32_t purgeInterval); + + protected: + nsCOMPtr<nsITimer> mPurgeTimer; + nsCOMPtr<nsIMsgSearchSession> mSearchSession; + nsCOMPtr<nsIMsgFolder> mSearchFolder; + nsTArray<RefPtr<nsIMsgDBHdr>> mHdrsToDelete; + bool mHaveShutdown; + + private: + // in minutes, how long must pass between two consecutive purges on the + // same junk folder? + int32_t mMinDelayBetweenPurges; + // in minutes, how often to check if we need to purge one of the junk folders? + int32_t mPurgeTimerInterval; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp b/comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp new file mode 100644 index 0000000000..97d412acc4 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgQuickSearchDBView.cpp @@ -0,0 +1,806 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgQuickSearchDBView.h" +#include "nsMsgFolderFlags.h" +#include "nsIMsgHdr.h" +#include "nsIMsgImapMailFolder.h" +#include "nsImapCore.h" +#include "nsIMsgHdr.h" +#include "nsIDBFolderInfo.h" +#include "nsMsgMessageFlags.h" +#include "nsMsgUtils.h" + +nsMsgQuickSearchDBView::nsMsgQuickSearchDBView() { + m_usingCachedHits = false; + m_cacheEmpty = true; +} + +nsMsgQuickSearchDBView::~nsMsgQuickSearchDBView() {} + +NS_IMPL_ISUPPORTS_INHERITED(nsMsgQuickSearchDBView, nsMsgDBView, nsIMsgDBView, + nsIMsgSearchNotify) + +NS_IMETHODIMP nsMsgQuickSearchDBView::Open(nsIMsgFolder* folder, + nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, + int32_t* pCount) { + nsresult rv = + nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount); + NS_ENSURE_SUCCESS(rv, rv); + + if (!m_db) return NS_ERROR_NULL_POINTER; + m_viewFolder = nullptr; + + int32_t count; + rv = InitThreadedView(count); + if (pCount) *pCount = count; + return rv; +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, + nsIMsgDBView** _retval) { + nsMsgQuickSearchDBView* newMsgDBView = new nsMsgQuickSearchDBView(); + nsresult rv = + CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*_retval = newMsgDBView); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) { + nsMsgThreadedDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow, + aCmdUpdater); + nsMsgQuickSearchDBView* newMsgDBView = (nsMsgQuickSearchDBView*)aNewMsgDBView; + + // now copy all of our private member data + newMsgDBView->m_origKeys = m_origKeys.Clone(); + return NS_OK; +} + +nsresult nsMsgQuickSearchDBView::DeleteMessages( + nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection, + bool deleteStorage) { + for (nsMsgViewIndex viewIndex : selection) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + (void)GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr)); + if (msgHdr) { + RememberDeletedMsgHdr(msgHdr); + } + } + return nsMsgDBView::DeleteMessages(window, selection, deleteStorage); +} + +NS_IMETHODIMP nsMsgQuickSearchDBView::DoCommand( + nsMsgViewCommandTypeValue aCommand) { + if (aCommand == nsMsgViewCommandType::markAllRead) { + nsresult rv = NS_OK; + m_folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, + false); + + for (uint32_t i = 0; NS_SUCCEEDED(rv) && i < GetSize(); i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + m_db->GetMsgHdrForKey(m_keys[i], getter_AddRefs(msgHdr)); + rv = m_db->MarkHdrRead(msgHdr, true, nullptr); + } + + m_folder->EnableNotifications(nsIMsgFolder::allMessageCountNotifications, + true); + + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_folder); + if (NS_SUCCEEDED(rv) && imapFolder) + rv = imapFolder->StoreImapFlags(kImapMsgSeenFlag, true, m_keys, nullptr); + + m_db->SetSummaryValid(true); + return rv; + } else + return nsMsgDBView::DoCommand(aCommand); +} + +NS_IMETHODIMP nsMsgQuickSearchDBView::GetViewType( + nsMsgViewTypeValue* aViewType) { + NS_ENSURE_ARG_POINTER(aViewType); + *aViewType = nsMsgViewType::eShowQuickSearchResults; + return NS_OK; +} + +nsresult nsMsgQuickSearchDBView::AddHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex* resultIndex) { + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + // protect against duplication. + if (m_origKeys.BinaryIndexOf(msgKey) == m_origKeys.NoIndex) { + nsMsgViewIndex insertIndex = GetInsertIndexHelper( + msgHdr, m_origKeys, nullptr, nsMsgViewSortOrder::ascending, + nsMsgViewSortType::byId); + m_origKeys.InsertElementAt(insertIndex, msgKey); + } + if (m_viewFlags & (nsMsgViewFlagsType::kGroupBySort | + nsMsgViewFlagsType::kThreadedDisplay)) { + nsMsgKey parentKey; + msgHdr->GetThreadParent(&parentKey); + return nsMsgThreadedDBView::OnNewHeader(msgHdr, parentKey, true); + } else + return nsMsgDBView::AddHdr(msgHdr, resultIndex); +} + +nsresult nsMsgQuickSearchDBView::OnNewHeader(nsIMsgDBHdr* newHdr, + nsMsgKey aParentKey, + bool ensureListed) { + if (newHdr) { + bool match = false; + nsCOMPtr<nsIMsgSearchSession> searchSession = + do_QueryReferent(m_searchSession); + if (searchSession) searchSession->MatchHdr(newHdr, m_db, &match); + if (match) { + // put the new header in m_origKeys, so that expanding a thread will + // show the newly added header. + nsMsgKey newKey; + (void)newHdr->GetMessageKey(&newKey); + nsMsgViewIndex insertIndex = GetInsertIndexHelper( + newHdr, m_origKeys, nullptr, nsMsgViewSortOrder::ascending, + nsMsgViewSortType::byId); + m_origKeys.InsertElementAt(insertIndex, newKey); + nsMsgThreadedDBView::OnNewHeader( + newHdr, aParentKey, + ensureListed); // do not add a new message if there isn't a match. + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgQuickSearchDBView::OnHdrFlagsChanged( + nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) { + nsresult rv = nsMsgGroupView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, + aNewFlags, aInstigator); + + if (m_viewFolder && (m_viewFolder != m_folder) && + (aOldFlags & nsMsgMessageFlags::Read) != + (aNewFlags & nsMsgMessageFlags::Read)) { + // if we're displaying a single folder virtual folder for an imap folder, + // the search criteria might be on message body, and we might not have the + // message body offline, in which case we can't tell if the message + // matched or not. But if the unread flag changed, we need to update the + // unread counts. Normally, VirtualFolderChangeListener::OnHdrFlagsChanged + // will handle this, but it won't work for body criteria when we don't have + // the body offline. + nsCOMPtr<nsIMsgImapMailFolder> imapFolder = do_QueryInterface(m_viewFolder); + if (imapFolder) { + nsMsgViewIndex hdrIndex = FindHdr(aHdrChanged); + if (hdrIndex != nsMsgViewIndex_None) { + nsCOMPtr<nsIMsgSearchSession> searchSession = + do_QueryReferent(m_searchSession); + if (searchSession) { + bool oldMatch, newMatch; + rv = searchSession->MatchHdr(aHdrChanged, m_db, &newMatch); + aHdrChanged->SetFlags(aOldFlags); + rv = searchSession->MatchHdr(aHdrChanged, m_db, &oldMatch); + aHdrChanged->SetFlags(aNewFlags); + // if it doesn't match the criteria, + // VirtualFolderChangeListener::OnHdrFlagsChanged won't tweak the + // read/unread counts. So do it here: + if (!oldMatch && !newMatch) { + nsCOMPtr<nsIMsgDatabase> virtDatabase; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + + rv = m_viewFolder->GetDBFolderInfoAndDB( + getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase)); + NS_ENSURE_SUCCESS(rv, rv); + dbFolderInfo->ChangeNumUnreadMessages( + (aOldFlags & nsMsgMessageFlags::Read) ? 1 : -1); + m_viewFolder->UpdateSummaryTotals(true); // force update from db. + virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } + } + } + } + } + return rv; +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::OnHdrPropertyChanged(nsIMsgDBHdr* aHdrChanged, + const nsACString& property, + bool aPreChange, uint32_t* aStatus, + nsIDBChangeListener* aInstigator) { + // If the junk mail plugin just activated on a message, then + // we'll allow filters to remove from view. + // Otherwise, just update the view line. + // + // Note this will not add newly matched headers to the view. This is + // probably a bug that needs fixing. + + NS_ENSURE_ARG_POINTER(aStatus); + NS_ENSURE_ARG_POINTER(aHdrChanged); + + nsMsgViewIndex index = FindHdr(aHdrChanged); + if (index == nsMsgViewIndex_None) // message does not appear in view + return NS_OK; + + nsCString originStr; + (void)aHdrChanged->GetStringProperty("junkscoreorigin", originStr); + // check for "plugin" with only first character for performance + bool plugin = (originStr.get()[0] == 'p'); + + if (aPreChange) { + // first call, done prior to the change + *aStatus = plugin; + return NS_OK; + } + + // second call, done after the change + bool wasPlugin = *aStatus; + + bool match = true; + nsCOMPtr<nsIMsgSearchSession> searchSession( + do_QueryReferent(m_searchSession)); + if (searchSession) searchSession->MatchHdr(aHdrChanged, m_db, &match); + + if (!match && plugin && !wasPlugin) + RemoveByIndex(index); // remove hdr from view + else + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::GetSearchSession(nsIMsgSearchSession** aSession) { + NS_ASSERTION(false, "GetSearchSession method is not implemented"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::SetSearchSession(nsIMsgSearchSession* aSession) { + m_searchSession = do_GetWeakReference(aSession); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::OnSearchHit(nsIMsgDBHdr* aMsgHdr, + nsIMsgFolder* folder) { + NS_ENSURE_ARG(aMsgHdr); + if (!m_db) return NS_ERROR_NULL_POINTER; + // remember search hit and when search is done, reconcile cache + // with new hits; + m_hdrHits.AppendObject(aMsgHdr); + nsMsgKey key; + aMsgHdr->GetMessageKey(&key); + // Is FindKey going to be expensive here? A lot of hits could make + // it a little bit slow to search through the view for every hit. + if (m_cacheEmpty || FindKey(key, false) == nsMsgViewIndex_None) + return AddHdr(aMsgHdr); + else + return NS_OK; +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::OnSearchDone(nsresult status) { + // This batch began in OnNewSearch. + if (mJSTree) mJSTree->EndUpdateBatch(); + // We're a single-folder virtual folder if viewFolder != folder, and that is + // the only case in which we want to be messing about with a results cache + // or unread counts. + if (m_db && m_viewFolder && m_viewFolder != m_folder) { + nsTArray<nsMsgKey> keyArray; + nsCString searchUri; + m_viewFolder->GetURI(searchUri); + uint32_t count = m_hdrHits.Count(); + // Build up message keys. The cache expects them to be sorted. + for (uint32_t i = 0; i < count; i++) { + nsMsgKey key; + m_hdrHits[i]->GetMessageKey(&key); + keyArray.AppendElement(key); + } + keyArray.Sort(); + nsTArray<nsMsgKey> staleHits; + nsresult rv = m_db->RefreshCache(searchUri, keyArray, staleHits); + NS_ENSURE_SUCCESS(rv, rv); + for (nsMsgKey staleKey : staleHits) { + nsCOMPtr<nsIMsgDBHdr> hdrDeleted; + m_db->GetMsgHdrForKey(staleKey, getter_AddRefs(hdrDeleted)); + if (hdrDeleted) OnHdrDeleted(hdrDeleted, nsMsgKey_None, 0, this); + } + + nsCOMPtr<nsIMsgDatabase> virtDatabase; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + rv = m_viewFolder->GetDBFolderInfoAndDB(getter_AddRefs(dbFolderInfo), + getter_AddRefs(virtDatabase)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numUnread = 0; + size_t numTotal = m_origKeys.Length(); + + for (size_t i = 0; i < m_origKeys.Length(); i++) { + bool isRead; + m_db->IsRead(m_origKeys[i], &isRead); + if (!isRead) numUnread++; + } + dbFolderInfo->SetNumUnreadMessages(numUnread); + dbFolderInfo->SetNumMessages(numTotal); + m_viewFolder->UpdateSummaryTotals(true); // force update from db. + virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + } + if (m_sortType != + nsMsgViewSortType::byThread) // we do not find levels for the results. + { + m_sortValid = false; // sort the results + Sort(m_sortType, m_sortOrder); + } + if (m_viewFolder && (m_viewFolder != m_folder)) + SetMRUTimeForFolder(m_viewFolder); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::OnNewSearch() { + int32_t oldSize = GetSize(); + + m_keys.Clear(); + m_levels.Clear(); + m_flags.Clear(); + m_hdrHits.Clear(); + // this needs to happen after we remove all the keys, since RowCountChanged() + // will call our GetRowCount() + if (mTree) mTree->RowCountChanged(0, -oldSize); + uint32_t folderFlags = 0; + if (m_viewFolder) m_viewFolder->GetFlags(&folderFlags); + // check if it's a virtual folder - if so, we should get the cached hits + // from the db, and set a flag saying that we're using cached values. + if (folderFlags & nsMsgFolderFlags::Virtual) { + nsCOMPtr<nsIMsgEnumerator> cachedHits; + nsCString searchUri; + m_viewFolder->GetURI(searchUri); + m_db->GetCachedHits(searchUri, getter_AddRefs(cachedHits)); + if (cachedHits) { + bool hasMore; + + m_usingCachedHits = true; + cachedHits->HasMoreElements(&hasMore); + m_cacheEmpty = !hasMore; + if (mTree) mTree->BeginUpdateBatch(); + if (mJSTree) mJSTree->BeginUpdateBatch(); + while (hasMore) { + nsCOMPtr<nsIMsgDBHdr> header; + nsresult rv = cachedHits->GetNext(getter_AddRefs(header)); + if (header && NS_SUCCEEDED(rv)) + AddHdr(header); + else + break; + cachedHits->HasMoreElements(&hasMore); + } + if (mTree) mTree->EndUpdateBatch(); + if (mJSTree) mJSTree->EndUpdateBatch(); + } + } + + // Prevent updates for every message found. This batch ends in OnSearchDone. + if (mJSTree) mJSTree->BeginUpdateBatch(); + + return NS_OK; +} + +nsresult nsMsgQuickSearchDBView::GetFirstMessageHdrToDisplayInThread( + nsIMsgThread* threadHdr, nsIMsgDBHdr** result) { + uint32_t numChildren; + nsresult rv = NS_OK; + uint8_t minLevel = 0xff; + threadHdr->GetNumChildren(&numChildren); + nsMsgKey threadRootKey; + nsCOMPtr<nsIMsgDBHdr> rootParent; + threadHdr->GetRootHdr(getter_AddRefs(rootParent)); + if (rootParent) + rootParent->GetMessageKey(&threadRootKey); + else + threadHdr->GetThreadKey(&threadRootKey); + + nsCOMPtr<nsIMsgDBHdr> retHdr; + + // iterate over thread, finding mgsHdr in view with the lowest level. + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> child; + rv = threadHdr->GetChildHdrAt(childIndex, getter_AddRefs(child)); + if (NS_SUCCEEDED(rv) && child) { + nsMsgKey msgKey; + child->GetMessageKey(&msgKey); + + // this works because we've already sorted m_keys by id. + nsMsgViewIndex keyIndex = m_origKeys.BinaryIndexOf(msgKey); + if (keyIndex != nsMsgViewIndex_None) { + // this is the root, so it's the best we're going to do. + if (msgKey == threadRootKey) { + retHdr = child; + break; + } + uint8_t level = 0; + nsMsgKey parentId; + child->GetThreadParent(&parentId); + nsCOMPtr<nsIMsgDBHdr> parent; + // count number of ancestors - that's our level + while (parentId != nsMsgKey_None) { + m_db->GetMsgHdrForKey(parentId, getter_AddRefs(parent)); + if (parent) { + nsMsgKey saveParentId = parentId; + parent->GetThreadParent(&parentId); + // message is it's own parent - bad, let's break out of here. + // Or we've got some circular ancestry. + if (parentId == saveParentId || level > numChildren) break; + level++; + } else // if we can't find the parent, don't loop forever. + break; + } + if (level < minLevel) { + minLevel = level; + retHdr = child; + } + } + } + } + retHdr.forget(result); + return NS_OK; +} + +nsresult nsMsgQuickSearchDBView::SortThreads( + nsMsgViewSortTypeValue sortType, nsMsgViewSortOrderValue sortOrder) { + // don't need to sort by threads for group view. + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) return NS_OK; + // iterate over the messages in the view, getting the thread id's + // sort m_keys so we can quickly find if a key is in the view. + m_keys.Sort(); + // array of the threads' root hdr keys. + nsTArray<nsMsgKey> threadRootIds; + nsCOMPtr<nsIMsgDBHdr> rootHdr; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgThread> threadHdr; + for (uint32_t i = 0; i < m_keys.Length(); i++) { + GetMsgHdrForViewIndex(i, getter_AddRefs(msgHdr)); + m_db->GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(threadHdr)); + if (threadHdr) { + nsMsgKey rootKey; + threadHdr->GetChildKeyAt(0, &rootKey); + nsMsgViewIndex threadRootIndex = threadRootIds.BinaryIndexOf(rootKey); + // if we already have that id in top level threads, ignore this msg. + if (threadRootIndex != nsMsgViewIndex_None) continue; + // it would be nice if GetInsertIndexHelper always found the hdr, but it + // doesn't. + threadHdr->GetChildHdrAt(0, getter_AddRefs(rootHdr)); + if (!rootHdr) continue; + threadRootIndex = GetInsertIndexHelper(rootHdr, threadRootIds, nullptr, + nsMsgViewSortOrder::ascending, + nsMsgViewSortType::byId); + threadRootIds.InsertElementAt(threadRootIndex, rootKey); + } + } + + m_sortType = nsMsgViewSortType::byNone; // sort from scratch + // need to sort the top level threads now by sort order, if it's not by id + // and ascending (which is the order per above). + if (!(sortType == nsMsgViewSortType::byId && + sortOrder == nsMsgViewSortOrder::ascending)) { + m_keys.SwapElements(threadRootIds); + nsMsgDBView::Sort(sortType, sortOrder); + threadRootIds.SwapElements(m_keys); + } + m_keys.Clear(); + m_levels.Clear(); + m_flags.Clear(); + // now we've build up the list of thread ids - need to build the view + // from that. So for each thread id, we need to list the messages in the + // thread. + uint32_t numThreads = threadRootIds.Length(); + for (uint32_t threadIndex = 0; threadIndex < numThreads; threadIndex++) { + m_db->GetMsgHdrForKey(threadRootIds[threadIndex], getter_AddRefs(rootHdr)); + if (rootHdr) { + nsCOMPtr<nsIMsgDBHdr> displayRootHdr; + m_db->GetThreadContainingMsgHdr(rootHdr, getter_AddRefs(threadHdr)); + if (threadHdr) { + nsMsgKey rootKey; + uint32_t rootFlags; + GetFirstMessageHdrToDisplayInThread(threadHdr, + getter_AddRefs(displayRootHdr)); + if (!displayRootHdr) continue; + displayRootHdr->GetMessageKey(&rootKey); + displayRootHdr->GetFlags(&rootFlags); + rootFlags |= MSG_VIEW_FLAG_ISTHREAD; + m_keys.AppendElement(rootKey); + m_flags.AppendElement(rootFlags); + m_levels.AppendElement(0); + + nsMsgViewIndex startOfThreadViewIndex = m_keys.Length(); + nsMsgViewIndex rootIndex = startOfThreadViewIndex - 1; + uint32_t numListed = 0; + ListIdsInThreadOrder(threadHdr, rootKey, 1, &startOfThreadViewIndex, + &numListed); + if (numListed > 0) + m_flags[rootIndex] = rootFlags | MSG_VIEW_FLAG_HASCHILDREN; + } + } + } + + // The thread state is left expanded (despite viewFlags) so at least reflect + // the correct state. + m_viewFlags |= nsMsgViewFlagsType::kExpandAll; + + return NS_OK; +} + +nsresult nsMsgQuickSearchDBView::ListCollapsedChildren( + nsMsgViewIndex viewIndex, nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) { + nsCOMPtr<nsIMsgThread> threadHdr; + nsresult rv = GetThreadContainingIndex(viewIndex, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + nsCOMPtr<nsIMsgDBHdr> rootHdr; + nsMsgKey rootKey; + GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(rootHdr)); + rootHdr->GetMessageKey(&rootKey); + // group threads can have the root key twice, one for the dummy row. + bool rootKeySkipped = false; + for (uint32_t i = 0; i < numChildren; i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + if (msgHdr) { + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + if (msgKey != rootKey || (GroupViewUsesDummyRow() && rootKeySkipped)) { + // if this hdr is in the original view, add it to new view. + if (m_origKeys.BinaryIndexOf(msgKey) != m_origKeys.NoIndex) + messageArray.AppendElement(msgHdr); + } else { + rootKeySkipped = true; + } + } + } + return NS_OK; +} + +nsresult nsMsgQuickSearchDBView::ListIdsInThread( + nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex, + uint32_t* pNumListed) { + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort)) { + nsMsgKey parentKey = m_keys[startOfThreadViewIndex++]; + return ListIdsInThreadOrder(threadHdr, parentKey, 1, + &startOfThreadViewIndex, pNumListed); + } + + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + uint32_t i; + uint32_t viewIndex = startOfThreadViewIndex + 1; + nsCOMPtr<nsIMsgDBHdr> rootHdr; + nsMsgKey rootKey; + uint32_t rootFlags = m_flags[startOfThreadViewIndex]; + *pNumListed = 0; + GetMsgHdrForViewIndex(startOfThreadViewIndex, getter_AddRefs(rootHdr)); + rootHdr->GetMessageKey(&rootKey); + // group threads can have the root key twice, one for the dummy row. + bool rootKeySkipped = false; + for (i = 0; i < numChildren; i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + if (msgHdr != nullptr) { + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + if (msgKey != rootKey || (GroupViewUsesDummyRow() && rootKeySkipped)) { + nsMsgViewIndex threadRootIndex = m_origKeys.BinaryIndexOf(msgKey); + // if this hdr is in the original view, add it to new view. + if (threadRootIndex != nsMsgViewIndex_None) { + uint32_t childFlags; + msgHdr->GetFlags(&childFlags); + InsertMsgHdrAt( + viewIndex, msgHdr, msgKey, childFlags, + FindLevelInThread(msgHdr, startOfThreadViewIndex, viewIndex)); + if (!(rootFlags & MSG_VIEW_FLAG_HASCHILDREN)) + m_flags[startOfThreadViewIndex] = + rootFlags | MSG_VIEW_FLAG_HASCHILDREN; + + viewIndex++; + (*pNumListed)++; + } + } else { + rootKeySkipped = true; + } + } + } + return NS_OK; +} + +nsresult nsMsgQuickSearchDBView::ListIdsInThreadOrder( + nsIMsgThread* threadHdr, nsMsgKey parentKey, uint32_t level, + uint32_t callLevel, nsMsgKey keyToSkip, nsMsgViewIndex* viewIndex, + uint32_t* pNumListed) { + nsCOMPtr<nsIMsgEnumerator> msgEnumerator; + nsresult rv = + threadHdr->EnumerateMessages(parentKey, getter_AddRefs(msgEnumerator)); + NS_ENSURE_SUCCESS(rv, rv); + + // We use the numChildren as a sanity check on the thread structure. + uint32_t numChildren; + (void)threadHdr->GetNumChildren(&numChildren); + bool hasMore; + while (NS_SUCCEEDED(rv = msgEnumerator->HasMoreElements(&hasMore)) && + hasMore) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = msgEnumerator->GetNext(getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + if (msgKey == keyToSkip) continue; + + // If we discover depths of more than numChildren, it means we have + // some sort of circular thread relationship and we bail out of the + // while loop before overflowing the stack with recursive calls. + // Technically, this is an error, but forcing a database rebuild + // is too destructive so we just return. + if (*pNumListed > numChildren || callLevel > numChildren) { + NS_ERROR("loop in message threading while listing children"); + return NS_OK; + } + + int32_t childLevel = level; + if (m_origKeys.BinaryIndexOf(msgKey) != m_origKeys.NoIndex) { + uint32_t msgFlags; + msgHdr->GetFlags(&msgFlags); + InsertMsgHdrAt(*viewIndex, msgHdr, msgKey, msgFlags & ~MSG_VIEW_FLAGS, + level); + (*pNumListed)++; + (*viewIndex)++; + childLevel++; + } + rv = ListIdsInThreadOrder(threadHdr, msgKey, childLevel, callLevel + 1, + keyToSkip, viewIndex, pNumListed); + NS_ENSURE_SUCCESS(rv, rv); + } + return rv; +} + +nsresult nsMsgQuickSearchDBView::ListIdsInThreadOrder(nsIMsgThread* threadHdr, + nsMsgKey parentKey, + uint32_t level, + nsMsgViewIndex* viewIndex, + uint32_t* pNumListed) { + nsresult rv = ListIdsInThreadOrder(threadHdr, parentKey, level, level, + nsMsgKey_None, viewIndex, pNumListed); + // Because a quick search view might not have the actual thread root + // as its root, and thus might have a message that potentially has siblings + // as its root, and the enumerator will miss the siblings, we might need to + // make a pass looking for the siblings of the non-root root. We'll put + // those after the potential children of the root. So we will list the + // children of the faux root's parent, ignoring the faux root. + if (level == 1) { + nsCOMPtr<nsIMsgDBHdr> root; + nsCOMPtr<nsIMsgDBHdr> rootParent; + nsMsgKey rootKey; + threadHdr->GetRootHdr(getter_AddRefs(rootParent)); + if (rootParent) { + rootParent->GetMessageKey(&rootKey); + if (rootKey != parentKey) + rv = ListIdsInThreadOrder(threadHdr, rootKey, level, level, parentKey, + viewIndex, pNumListed); + } + } + return rv; +} + +nsresult nsMsgQuickSearchDBView::ExpansionDelta(nsMsgViewIndex index, + int32_t* expansionDelta) { + *expansionDelta = 0; + if (index >= ((nsMsgViewIndex)m_keys.Length())) + return NS_MSG_MESSAGE_NOT_FOUND; + + char flags = m_flags[index]; + + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) return NS_OK; + + nsCOMPtr<nsIMsgThread> threadHdr; + nsresult rv = GetThreadContainingIndex(index, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + nsCOMPtr<nsIMsgDBHdr> rootHdr; + nsMsgKey rootKey; + GetMsgHdrForViewIndex(index, getter_AddRefs(rootHdr)); + rootHdr->GetMessageKey(&rootKey); + // group threads can have the root key twice, one for the dummy row. + bool rootKeySkipped = false; + for (uint32_t i = 0; i < numChildren; i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + if (msgHdr) { + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + if (msgKey != rootKey || (GroupViewUsesDummyRow() && rootKeySkipped)) { + // if this hdr is in the original view, add it to new view. + if (m_origKeys.BinaryIndexOf(msgKey) != m_origKeys.NoIndex) + (*expansionDelta)++; + } else { + rootKeySkipped = true; + } + } + } + if (!(flags & nsMsgMessageFlags::Elided)) + *expansionDelta = -(*expansionDelta); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders, + nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, + nsMsgViewFlagsTypeValue aViewFlags, + int32_t* aCount) { + if (aViewFlags & nsMsgViewFlagsType::kGroupBySort) + return nsMsgGroupView::OpenWithHdrs(aHeaders, aSortType, aSortOrder, + aViewFlags, aCount); + + m_sortType = aSortType; + m_sortOrder = aSortOrder; + m_viewFlags = aViewFlags; + + bool hasMore; + nsCOMPtr<nsISupports> supports; + nsresult rv = NS_OK; + while (NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = aHeaders->GetNext(getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv) && msgHdr) { + AddHdr(msgHdr); + } else { + break; + } + } + *aCount = m_keys.Length(); + return rv; +} + +NS_IMETHODIMP nsMsgQuickSearchDBView::SetViewFlags( + nsMsgViewFlagsTypeValue aViewFlags) { + nsresult rv = NS_OK; + // if the grouping has changed, rebuild the view + if ((m_viewFlags & nsMsgViewFlagsType::kGroupBySort) ^ + (aViewFlags & nsMsgViewFlagsType::kGroupBySort)) + rv = RebuildView(aViewFlags); + nsMsgDBView::SetViewFlags(aViewFlags); + + return rv; +} + +nsresult nsMsgQuickSearchDBView::GetMessageEnumerator( + nsIMsgEnumerator** enumerator) { + return GetViewEnumerator(enumerator); +} + +NS_IMETHODIMP +nsMsgQuickSearchDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, + nsMsgKey aParentKey, int32_t aFlags, + nsIDBChangeListener* aInstigator) { + NS_ENSURE_ARG_POINTER(aHdrDeleted); + nsMsgKey msgKey; + aHdrDeleted->GetMessageKey(&msgKey); + size_t keyIndex = m_origKeys.BinaryIndexOf(msgKey); + if (keyIndex != m_origKeys.NoIndex) m_origKeys.RemoveElementAt(keyIndex); + return nsMsgThreadedDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags, + aInstigator); +} + +NS_IMETHODIMP nsMsgQuickSearchDBView::GetNumMsgsInView(int32_t* aNumMsgs) { + NS_ENSURE_ARG_POINTER(aNumMsgs); + *aNumMsgs = m_origKeys.Length(); + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgQuickSearchDBView.h b/comm/mailnews/base/src/nsMsgQuickSearchDBView.h new file mode 100644 index 0000000000..da10a3d9e1 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgQuickSearchDBView.h @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgQuickSearchDBView_H_ +#define _nsMsgQuickSearchDBView_H_ + +#include "mozilla/Attributes.h" +#include "nsMsgThreadedDBView.h" +#include "nsIMsgSearchNotify.h" +#include "nsIMsgSearchSession.h" +#include "nsCOMArray.h" +#include "nsIMsgHdr.h" +#include "nsIWeakReferenceUtils.h" + +class nsMsgQuickSearchDBView : public nsMsgThreadedDBView, + public nsIMsgSearchNotify { + public: + nsMsgQuickSearchDBView(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIMSGSEARCHNOTIFY + + virtual const char* GetViewName(void) override { return "QuickSearchView"; } + NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override; + NS_IMETHOD OpenWithHdrs(nsIMsgEnumerator* aHeaders, + nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, + nsMsgViewFlagsTypeValue aViewFlags, + int32_t* aCount) override; + NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCommandUpdater, + nsIMsgDBView** _retval) override; + NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) override; + NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue aCommand) override; + NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override; + NS_IMETHOD SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) override; + NS_IMETHOD SetSearchSession(nsIMsgSearchSession* aSearchSession) override; + NS_IMETHOD GetSearchSession(nsIMsgSearchSession** aSearchSession) override; + NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, + uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) override; + NS_IMETHOD OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange, + const nsACString& property, bool aPreChange, + uint32_t* aStatus, + nsIDBChangeListener* aInstigator) override; + NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, + int32_t aFlags, + nsIDBChangeListener* aInstigator) override; + NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override; + + protected: + virtual ~nsMsgQuickSearchDBView(); + nsWeakPtr m_searchSession; + nsTArray<nsMsgKey> m_origKeys; + bool m_usingCachedHits; + bool m_cacheEmpty; + nsCOMArray<nsIMsgDBHdr> m_hdrHits; + virtual nsresult AddHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex* resultIndex = nullptr) override; + virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey, + bool ensureListed) override; + virtual nsresult DeleteMessages(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection, + bool deleteStorage) override; + virtual nsresult SortThreads(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) override; + virtual nsresult GetFirstMessageHdrToDisplayInThread( + nsIMsgThread* threadHdr, nsIMsgDBHdr** result) override; + virtual nsresult ExpansionDelta(nsMsgViewIndex index, + int32_t* expansionDelta) override; + virtual nsresult ListCollapsedChildren( + nsMsgViewIndex viewIndex, + nsTArray<RefPtr<nsIMsgDBHdr>>& messageArray) override; + virtual nsresult ListIdsInThread(nsIMsgThread* threadHdr, + nsMsgViewIndex startOfThreadViewIndex, + uint32_t* pNumListed) override; + virtual nsresult ListIdsInThreadOrder(nsIMsgThread* threadHdr, + nsMsgKey parentKey, uint32_t level, + nsMsgViewIndex* viewIndex, + uint32_t* pNumListed) override; + virtual nsresult ListIdsInThreadOrder(nsIMsgThread* threadHdr, + nsMsgKey parentKey, uint32_t level, + uint32_t callLevel, nsMsgKey keyToSkip, + nsMsgViewIndex* viewIndex, + uint32_t* pNumListed); + virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator) override; + void SavePreSearchInfo(); + void ClearPreSearchInfo(); +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgReadStateTxn.cpp b/comm/mailnews/base/src/nsMsgReadStateTxn.cpp new file mode 100644 index 0000000000..80cd0d0bd1 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgReadStateTxn.cpp @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgReadStateTxn.h" + +#include "nsIMsgHdr.h" +#include "nsComponentManagerUtils.h" + +nsMsgReadStateTxn::nsMsgReadStateTxn() {} + +nsMsgReadStateTxn::~nsMsgReadStateTxn() {} + +nsresult nsMsgReadStateTxn::Init(nsIMsgFolder* aParentFolder, uint32_t aNumKeys, + nsMsgKey* aMsgKeyArray) { + NS_ENSURE_ARG_POINTER(aParentFolder); + + mParentFolder = aParentFolder; + mMarkedMessages.AppendElements(aMsgKeyArray, aNumKeys); + + return nsMsgTxn::Init(); +} + +NS_IMETHODIMP +nsMsgReadStateTxn::UndoTransaction() { return MarkMessages(false); } + +NS_IMETHODIMP +nsMsgReadStateTxn::RedoTransaction() { return MarkMessages(true); } + +NS_IMETHODIMP +nsMsgReadStateTxn::MarkMessages(bool aAsRead) { + nsTArray<RefPtr<nsIMsgDBHdr>> messages(mMarkedMessages.Length()); + for (auto msgKey : mMarkedMessages) { + nsCOMPtr<nsIMsgDBHdr> curMsgHdr; + nsresult rv = + mParentFolder->GetMessageHeader(msgKey, getter_AddRefs(curMsgHdr)); + if (NS_SUCCEEDED(rv) && curMsgHdr) { + messages.AppendElement(curMsgHdr); + } + } + return mParentFolder->MarkMessagesRead(messages, aAsRead); +} diff --git a/comm/mailnews/base/src/nsMsgReadStateTxn.h b/comm/mailnews/base/src/nsMsgReadStateTxn.h new file mode 100644 index 0000000000..07bd4c8670 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgReadStateTxn.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgBaseUndoTxn_h_ +#define nsMsgBaseUndoTxn_h_ + +#include "mozilla/Attributes.h" +#include "nsMsgTxn.h" +#include "nsTArray.h" +#include "nsCOMPtr.h" +#include "MailNewsTypes.h" +#include "nsIMsgFolder.h" + +#define NS_MSGREADSTATETXN_IID \ + { /* 121FCE4A-3EA1-455C-8161-839E1557D0CF */ \ + 0x121FCE4A, 0x3EA1, 0x455C, { \ + 0x81, 0x61, 0x83, 0x9E, 0x15, 0x57, 0xD0, 0xCF \ + } \ + } + +//------------------------------------------------------------------------------ +// A mark-all transaction handler. Helper for redo/undo of message read states. +//------------------------------------------------------------------------------ +class nsMsgReadStateTxn : public nsMsgTxn { + public: + nsMsgReadStateTxn(); + virtual ~nsMsgReadStateTxn(); + + nsresult Init(nsIMsgFolder* aParentFolder, uint32_t aNumKeys, + nsMsgKey* aMsgKeyArray); + NS_IMETHOD UndoTransaction() override; + NS_IMETHOD RedoTransaction() override; + + protected: + NS_IMETHOD MarkMessages(bool aAsRead); + + private: + nsCOMPtr<nsIMsgFolder> mParentFolder; + nsTArray<nsMsgKey> mMarkedMessages; +}; + +#endif // nsMsgBaseUndoTxn_h_ diff --git a/comm/mailnews/base/src/nsMsgSearchDBView.cpp b/comm/mailnews/base/src/nsMsgSearchDBView.cpp new file mode 100644 index 0000000000..f036d0d304 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgSearchDBView.cpp @@ -0,0 +1,1344 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgSearchDBView.h" +#include "nsIMsgHdr.h" +#include "nsIMsgThread.h" +#include "nsQuickSort.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgCopyService.h" +#include "nsMsgUtils.h" +#include "nsTreeColumns.h" +#include "nsIMsgMessageService.h" +#include "nsMsgGroupThread.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgSearchSession.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" + +static bool gReferenceOnlyThreading; + +nsMsgSearchDBView::nsMsgSearchDBView() { + // Don't try to display messages for the search pane. + mSuppressMsgDisplay = true; + m_totalMessagesInView = 0; + m_nextThreadId = 1; + mCurIndex = 0; + mTotalIndices = 0; + mCommand = -1; +} + +nsMsgSearchDBView::~nsMsgSearchDBView() {} + +NS_IMPL_ISUPPORTS_INHERITED(nsMsgSearchDBView, nsMsgDBView, nsIMsgDBView, + nsIMsgCopyServiceListener, nsIMsgSearchNotify) + +NS_IMETHODIMP +nsMsgSearchDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) { + // DBViewWrapper.jsm likes to create search views with a sort order + // of byNone, in order to have the order be the order the search results + // are returned. But this doesn't work with threaded view, so make the + // sort order be byDate if we're threaded. + + if (viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + sortType == nsMsgViewSortType::byNone) + sortType = nsMsgViewSortType::byDate; + + nsresult rv = + nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + prefBranch->GetBoolPref("mail.strict_threading", &gReferenceOnlyThreading); + + // Our sort is automatically valid because we have no contents at this point! + m_sortValid = true; + + if (pCount) *pCount = 0; + + m_folder = nullptr; + return rv; +} + +NS_IMETHODIMP +nsMsgSearchDBView::CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, + nsIMsgDBView** _retval) { + nsMsgSearchDBView* newMsgDBView = new nsMsgSearchDBView(); + nsresult rv = + CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*_retval = newMsgDBView); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchDBView::CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) { + nsMsgGroupView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow, + aCmdUpdater); + nsMsgSearchDBView* newMsgDBView = (nsMsgSearchDBView*)aNewMsgDBView; + + // Now copy all of our private member data. + newMsgDBView->mDestFolder = mDestFolder; + newMsgDBView->mCommand = mCommand; + newMsgDBView->mTotalIndices = mTotalIndices; + newMsgDBView->mCurIndex = mCurIndex; + newMsgDBView->m_folders.InsertObjectsAt(m_folders, 0); + newMsgDBView->m_curCustomColumn = m_curCustomColumn; + for (auto const& hdrs : m_hdrsForEachFolder) { + newMsgDBView->m_hdrsForEachFolder.AppendElement(hdrs.Clone()); + } + newMsgDBView->m_uniqueFoldersSelected.InsertObjectsAt(m_uniqueFoldersSelected, + 0); + + int32_t count = m_dbToUseList.Count(); + for (int32_t i = 0; i < count; i++) { + newMsgDBView->m_dbToUseList.AppendObject(m_dbToUseList[i]); + // Register the new view with the database so it gets notifications. + m_dbToUseList[i]->AddListener(newMsgDBView); + } + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + // We need to clone the thread and msg hdr hash tables. + for (auto iter = m_threadsTable.Iter(); !iter.Done(); iter.Next()) { + newMsgDBView->m_threadsTable.InsertOrUpdate(iter.Key(), iter.UserData()); + } + for (auto iter = m_hdrsTable.Iter(); !iter.Done(); iter.Next()) { + newMsgDBView->m_hdrsTable.InsertOrUpdate(iter.Key(), iter.UserData()); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchDBView::Close() { + int32_t count = m_dbToUseList.Count(); + + for (int32_t i = 0; i < count; i++) m_dbToUseList[i]->RemoveListener(this); + + m_dbToUseList.Clear(); + + return nsMsgGroupView::Close(); +} + +void nsMsgSearchDBView::InternalClose() { + m_threadsTable.Clear(); + m_hdrsTable.Clear(); + nsMsgGroupView::InternalClose(); + m_folders.Clear(); +} + +NS_IMETHODIMP +nsMsgSearchDBView::GetCellText(int32_t aRow, nsTreeColumn* aCol, + nsAString& aValue) { + NS_ENSURE_TRUE(IsValidIndex(aRow), NS_MSG_INVALID_DBVIEW_INDEX); + NS_ENSURE_ARG_POINTER(aCol); + + const nsAString& colID = aCol->GetId(); + // The only thing we contribute is location; dummy rows have no location, so + // bail in that case. Otherwise, check if we are dealing with 'location'. + // 'location', need to check for "lo" not just "l" to avoid "label" column. + if (!(m_flags[aRow] & MSG_VIEW_FLAG_DUMMY) && colID.Length() >= 2 && + colID.First() == 'l' && colID.CharAt(1) == 'o') + return FetchLocation(aRow, aValue); + else + return nsMsgGroupView::GetCellText(aRow, aCol, aValue); +} + +nsresult nsMsgSearchDBView::HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey) { + if (m_sortType == nsMsgViewSortType::byLocation) { + aHashKey.Truncate(); + nsCOMPtr<nsIMsgFolder> folder; + msgHdr->GetFolder(getter_AddRefs(folder)); + return folder->GetPrettyName(aHashKey); + } + + return nsMsgGroupView::HashHdr(msgHdr, aHashKey); +} + +nsresult nsMsgSearchDBView::FetchLocation(int32_t aRow, + nsAString& aLocationString) { + nsCOMPtr<nsIMsgFolder> folder; + nsresult rv = GetFolderForViewIndex(aRow, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + return folder->GetPrettyName(aLocationString); +} + +nsresult nsMsgSearchDBView::OnNewHeader(nsIMsgDBHdr* newHdr, + nsMsgKey aParentKey, + bool /*ensureListed*/) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchDBView::OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, + int32_t aFlags, + nsIDBChangeListener* aInstigator) { + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) + return nsMsgGroupView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags, + aInstigator); + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + nsMsgViewIndex deletedIndex = FindHdr(aHdrDeleted); + uint32_t savedFlags = 0; + if (deletedIndex != nsMsgViewIndex_None) { + // Check if this message is currently selected. If it is, tell the front + // end to be prepared for a delete. + nsCOMPtr<nsIMsgDBViewCommandUpdater> commandUpdater( + do_QueryReferent(mCommandUpdater)); + bool isMsgSelected = false; + if (mTreeSelection && commandUpdater) { + mTreeSelection->IsSelected(deletedIndex, &isMsgSelected); + if (isMsgSelected) commandUpdater->UpdateNextMessageAfterDelete(); + } + + savedFlags = m_flags[deletedIndex]; + RemoveByIndex(deletedIndex); + + if (isMsgSelected) { + // Now tell the front end that the delete happened. + commandUpdater->SelectedMessageRemoved(); + } + } + + nsCOMPtr<nsIMsgThread> thread; + GetXFThreadFromMsgHdr(aHdrDeleted, getter_AddRefs(thread)); + if (thread) { + nsMsgXFViewThread* viewThread = + static_cast<nsMsgXFViewThread*>(thread.get()); + viewThread->RemoveChildHdr(aHdrDeleted, nullptr); + if (deletedIndex == nsMsgViewIndex_None && viewThread->MsgCount() == 1) { + // Remove the last child of a collapsed thread. Need to find the root, + // and remove the thread flags on it. + nsCOMPtr<nsIMsgDBHdr> rootHdr; + thread->GetRootHdr(getter_AddRefs(rootHdr)); + if (rootHdr) { + nsMsgViewIndex threadIndex = GetThreadRootIndex(rootHdr); + if (IsValidIndex(threadIndex)) + AndExtraFlag(threadIndex, + ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided | + MSG_VIEW_FLAG_HASCHILDREN)); + } + } else if (savedFlags & MSG_VIEW_FLAG_HASCHILDREN) { + if (savedFlags & nsMsgMessageFlags::Elided) { + nsCOMPtr<nsIMsgDBHdr> rootHdr; + nsresult rv = thread->GetRootHdr(getter_AddRefs(rootHdr)); + NS_ENSURE_SUCCESS(rv, rv); + nsMsgKey msgKey; + uint32_t msgFlags; + rootHdr->GetMessageKey(&msgKey); + rootHdr->GetFlags(&msgFlags); + // Promote the new thread root. + if (viewThread->MsgCount() > 1) + msgFlags |= MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided | + MSG_VIEW_FLAG_HASCHILDREN; + InsertMsgHdrAt(deletedIndex, rootHdr, msgKey, msgFlags, 0); + if (!m_deletingRows) + NoteChange(deletedIndex, 1, + nsMsgViewNotificationCode::insertOrDelete); + } else if (viewThread->MsgCount() > 1) { + OrExtraFlag(deletedIndex, + MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN); + } + } + } + } else { + return nsMsgDBView::OnHdrDeleted(aHdrDeleted, aParentKey, aFlags, + aInstigator); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchDBView::OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, + uint32_t aOldFlags, uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) { + // Defer to base class if we're grouped or not threaded at all. + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort || + !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + return nsMsgGroupView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, + aInstigator); + } + + nsCOMPtr<nsIMsgThread> thread; + bool foundMessageId; + // Check if the hdr that changed is in a xf thread, and if the read flag + // changed, update the thread unread count. GetXFThreadFromMsgHdr returns + // the thread the header does or would belong to, so we need to also + // check that the header is actually in the thread. + GetXFThreadFromMsgHdr(aHdrChanged, getter_AddRefs(thread), &foundMessageId); + if (foundMessageId) { + nsMsgXFViewThread* viewThread = + static_cast<nsMsgXFViewThread*>(thread.get()); + if (viewThread->HdrIndex(aHdrChanged) != -1) { + uint32_t deltaFlags = (aOldFlags ^ aNewFlags); + if (deltaFlags & nsMsgMessageFlags::Read) + thread->MarkChildRead(aNewFlags & nsMsgMessageFlags::Read); + } + } + + return nsMsgDBView::OnHdrFlagsChanged(aHdrChanged, aOldFlags, aNewFlags, + aInstigator); +} + +void nsMsgSearchDBView::InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr, + nsMsgKey msgKey, uint32_t flags, + uint32_t level) { + if ((int32_t)index < 0) { + NS_ERROR("invalid insert index"); + index = 0; + level = 0; + } else if (index > m_keys.Length()) { + NS_ERROR("inserting past end of array"); + index = m_keys.Length(); + } + + m_keys.InsertElementAt(index, msgKey); + m_flags.InsertElementAt(index, flags); + m_levels.InsertElementAt(index, level); + nsCOMPtr<nsIMsgFolder> folder; + hdr->GetFolder(getter_AddRefs(folder)); + m_folders.InsertObjectAt(folder, index); +} + +void nsMsgSearchDBView::SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index, + nsMsgKey msgKey, uint32_t flags, + uint32_t level) { + m_keys[index] = msgKey; + m_flags[index] = flags; + m_levels[index] = level; + nsCOMPtr<nsIMsgFolder> folder; + hdr->GetFolder(getter_AddRefs(folder)); + m_folders.ReplaceObjectAt(folder, index); +} + +void nsMsgSearchDBView::InsertEmptyRows(nsMsgViewIndex viewIndex, + int32_t numRows) { + for (int32_t i = 0; i < numRows; i++) { + m_folders.InsertObjectAt(nullptr, viewIndex + i); + } + + return nsMsgDBView::InsertEmptyRows(viewIndex, numRows); +} + +void nsMsgSearchDBView::RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) { + nsMsgDBView::RemoveRows(viewIndex, numRows); + for (int32_t i = 0; i < numRows; i++) m_folders.RemoveObjectAt(viewIndex); +} + +nsresult nsMsgSearchDBView::GetMsgHdrForViewIndex(nsMsgViewIndex index, + nsIMsgDBHdr** msgHdr) { + if (index == nsMsgViewIndex_None || index >= (uint32_t)m_folders.Count()) { + return NS_MSG_INVALID_DBVIEW_INDEX; + } + + nsIMsgFolder* folder = m_folders[index]; + if (folder) { + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = folder->GetMsgDatabase(getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + if (db) { + return db->GetMsgHdrForKey(m_keys[index], msgHdr); + } + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMsgSearchDBView::GetFolderForViewIndex(nsMsgViewIndex index, + nsIMsgFolder** aFolder) { + NS_ENSURE_ARG_POINTER(aFolder); + + if (index == nsMsgViewIndex_None || index >= (uint32_t)m_folders.Count()) + return NS_MSG_INVALID_DBVIEW_INDEX; + + NS_IF_ADDREF(*aFolder = m_folders[index]); + return *aFolder ? NS_OK : NS_ERROR_NULL_POINTER; +} + +nsresult nsMsgSearchDBView::GetDBForViewIndex(nsMsgViewIndex index, + nsIMsgDatabase** db) { + nsCOMPtr<nsIMsgFolder> aFolder; + nsresult rv = GetFolderForViewIndex(index, getter_AddRefs(aFolder)); + NS_ENSURE_SUCCESS(rv, rv); + return aFolder->GetMsgDatabase(db); +} + +nsresult nsMsgSearchDBView::AddHdrFromFolder(nsIMsgDBHdr* msgHdr, + nsIMsgFolder* folder) { + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) + return nsMsgGroupView::OnNewHeader(msgHdr, nsMsgKey_None, true); + + nsMsgKey msgKey; + uint32_t msgFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + nsCOMPtr<nsIMsgThread> thread; + nsCOMPtr<nsIMsgDBHdr> threadRoot; + // If we find an xf thread in the hash table corresponding to the new msg's + // message id, a previous header must be a reference child of the new + // message, which means we need to reparent later. + bool msgIsReferredTo; + GetXFThreadFromMsgHdr(msgHdr, getter_AddRefs(thread), &msgIsReferredTo); + bool newThread = !thread; + nsMsgXFViewThread* viewThread; + if (!thread) { + viewThread = new nsMsgXFViewThread(this, m_nextThreadId++); + if (!viewThread) return NS_ERROR_OUT_OF_MEMORY; + + thread = viewThread; + } else { + viewThread = static_cast<nsMsgXFViewThread*>(thread.get()); + thread->GetChildHdrAt(0, getter_AddRefs(threadRoot)); + } + + AddMsgToHashTables(msgHdr, thread); + nsCOMPtr<nsIMsgDBHdr> parent; + uint32_t posInThread; + // We need to move threads in order to keep ourselves sorted + // correctly. We want the index of the original thread...we can do this by + // getting the root header before we add the new header, and finding that. + if (newThread || !viewThread->MsgCount()) { + viewThread->AddHdr(msgHdr, false, posInThread, getter_AddRefs(parent)); + nsMsgViewIndex insertIndex = GetIndexForThread(msgHdr); + NS_ASSERTION(insertIndex == m_levels.Length() || + (IsValidIndex(insertIndex) && !m_levels[insertIndex]), + "inserting into middle of thread"); + if (insertIndex == nsMsgViewIndex_None) + return NS_MSG_INVALID_DBVIEW_INDEX; + + if (!(m_viewFlags & nsMsgViewFlagsType::kExpandAll)) + msgFlags |= nsMsgMessageFlags::Elided; + + InsertMsgHdrAt(insertIndex, msgHdr, msgKey, msgFlags, 0); + NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete); + } else { + // Get the thread root index before we add the header, because adding + // the header can change the sort position. + nsMsgViewIndex threadIndex = GetThreadRootIndex(threadRoot); + viewThread->AddHdr(msgHdr, msgIsReferredTo, posInThread, + getter_AddRefs(parent)); + if (!IsValidIndex(threadIndex)) { + NS_ERROR("couldn't find thread index for newly inserted header"); + // Not really OK, but not failure exactly. + return NS_OK; + } + + NS_ASSERTION(!m_levels[threadIndex], + "threadRoot incorrect, or level incorrect"); + + bool moveThread = false; + if (m_sortType == nsMsgViewSortType::byDate) { + uint32_t newestMsgInThread = 0, msgDate = 0; + viewThread->GetNewestMsgDate(&newestMsgInThread); + msgHdr->GetDateInSeconds(&msgDate); + moveThread = (msgDate == newestMsgInThread); + } + + OrExtraFlag(threadIndex, + MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD); + if (!(m_flags[threadIndex] & nsMsgMessageFlags::Elided)) { + if (parent) { + // Since we know posInThread, we just want to insert the new hdr + // at threadIndex + posInThread, and then rebuild the view until we + // get to a sibling of the new hdr. + uint8_t newMsgLevel = viewThread->ChildLevelAt(posInThread); + InsertMsgHdrAt(threadIndex + posInThread, msgHdr, msgKey, msgFlags, + newMsgLevel); + + NoteChange(threadIndex + posInThread, 1, + nsMsgViewNotificationCode::insertOrDelete); + for (nsMsgViewIndex viewIndex = threadIndex + ++posInThread; + posInThread < viewThread->MsgCount() && + viewThread->ChildLevelAt(posInThread) > newMsgLevel; + viewIndex++) { + m_levels[viewIndex] = viewThread->ChildLevelAt(posInThread++); + } + + } else { + // The new header is the root, so we need to adjust all the children. + InsertMsgHdrAt(threadIndex, msgHdr, msgKey, msgFlags, 0); + + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::insertOrDelete); + nsMsgViewIndex i; + for (i = threadIndex + 1; + i < m_keys.Length() && (i == threadIndex + 1 || m_levels[i]); + i++) + m_levels[i] = m_levels[i] + 1; + // Turn off thread flags on old root. + AndExtraFlag(threadIndex + 1, + ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided | + MSG_VIEW_FLAG_HASCHILDREN)); + + NoteChange(threadIndex + 1, i - threadIndex + 1, + nsMsgViewNotificationCode::changed); + } + } else if (!parent) { + // New parent came into collapsed thread. + nsCOMPtr<nsIMsgFolder> msgFolder; + msgHdr->GetFolder(getter_AddRefs(msgFolder)); + m_keys[threadIndex] = msgKey; + m_folders.ReplaceObjectAt(msgFolder, threadIndex); + m_flags[threadIndex] = msgFlags | MSG_VIEW_FLAG_ISTHREAD | + nsMsgMessageFlags::Elided | + MSG_VIEW_FLAG_HASCHILDREN; + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + } + + if (moveThread) MoveThreadAt(threadIndex); + } + } else { + m_folders.AppendObject(folder); + // nsMsgKey_None means it's not a valid hdr. + if (msgKey != nsMsgKey_None) { + msgHdr->GetFlags(&msgFlags); + m_keys.AppendElement(msgKey); + m_levels.AppendElement(0); + m_flags.AppendElement(msgFlags); + NoteChange(GetSize() - 1, 1, nsMsgViewNotificationCode::insertOrDelete); + } + } + + return NS_OK; +} + +// This method removes the thread at threadIndex from the view +// and puts it back in its new position, determined by the sort order. +// And, if the selection is affected, save and restore the selection. +void nsMsgSearchDBView::MoveThreadAt(nsMsgViewIndex threadIndex) { + bool updatesSuppressed = mSuppressChangeNotification; + // Turn off tree notifications so that we don't reload the current message. + if (!updatesSuppressed) SetSuppressChangeNotifications(true); + + nsCOMPtr<nsIMsgDBHdr> threadHdr; + GetMsgHdrForViewIndex(threadIndex, getter_AddRefs(threadHdr)); + + uint32_t saveFlags = m_flags[threadIndex]; + bool threadIsExpanded = !(saveFlags & nsMsgMessageFlags::Elided); + int32_t childCount = 0; + nsMsgKey preservedKey; + AutoTArray<nsMsgKey, 1> preservedSelection; + int32_t selectionCount; + int32_t currentIndex; + bool hasSelection = + mTreeSelection && + ((NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(¤tIndex)) && + currentIndex >= 0 && (uint32_t)currentIndex < GetSize()) || + (NS_SUCCEEDED(mTreeSelection->GetRangeCount(&selectionCount)) && + selectionCount > 0)); + if (hasSelection) SaveAndClearSelection(&preservedKey, preservedSelection); + + if (threadIsExpanded) { + ExpansionDelta(threadIndex, &childCount); + childCount = -childCount; + } + + nsTArray<nsMsgKey> threadKeys; + nsTArray<uint32_t> threadFlags; + nsTArray<uint8_t> threadLevels; + nsCOMArray<nsIMsgFolder> threadFolders; + + if (threadIsExpanded) { + threadKeys.SetCapacity(childCount); + threadFlags.SetCapacity(childCount); + threadLevels.SetCapacity(childCount); + threadFolders.SetCapacity(childCount); + for (nsMsgViewIndex index = threadIndex + 1; + index < (nsMsgViewIndex)GetSize() && m_levels[index]; index++) { + threadKeys.AppendElement(m_keys[index]); + threadFlags.AppendElement(m_flags[index]); + threadLevels.AppendElement(m_levels[index]); + threadFolders.AppendObject(m_folders[index]); + } + + uint32_t collapseCount; + CollapseByIndex(threadIndex, &collapseCount); + } + + nsMsgDBView::RemoveByIndex(threadIndex); + m_folders.RemoveObjectAt(threadIndex); + nsMsgViewIndex newIndex = GetIndexForThread(threadHdr); + NS_ASSERTION(newIndex == m_levels.Length() || + (IsValidIndex(newIndex) && !m_levels[newIndex]), + "inserting into middle of thread"); + if (newIndex == nsMsgViewIndex_None) newIndex = 0; + + nsMsgKey msgKey; + uint32_t msgFlags; + threadHdr->GetMessageKey(&msgKey); + threadHdr->GetFlags(&msgFlags); + InsertMsgHdrAt(newIndex, threadHdr, msgKey, msgFlags, 0); + + if (threadIsExpanded) { + m_keys.InsertElementsAt(newIndex + 1, threadKeys); + m_flags.InsertElementsAt(newIndex + 1, threadFlags); + m_levels.InsertElementsAt(newIndex + 1, threadLevels); + m_folders.InsertObjectsAt(threadFolders, newIndex + 1); + } + + m_flags[newIndex] = saveFlags; + // Unfreeze selection. + if (hasSelection) RestoreSelection(preservedKey, preservedSelection); + + if (!updatesSuppressed) SetSuppressChangeNotifications(false); + + nsMsgViewIndex lowIndex = threadIndex < newIndex ? threadIndex : newIndex; + nsMsgViewIndex highIndex = lowIndex == threadIndex ? newIndex : threadIndex; + NoteChange(lowIndex, highIndex - lowIndex + childCount + 1, + nsMsgViewNotificationCode::changed); +} + +nsresult nsMsgSearchDBView::GetMessageEnumerator( + nsIMsgEnumerator** enumerator) { + // We do not have an m_db, so the default behavior (in nsMsgDBView) is not + // what we want (it will crash). We just want someone to enumerate the + // headers that we already have. Conveniently, nsMsgDBView already knows + // how to do this with its view enumerator, so we just use that. + return nsMsgDBView::GetViewEnumerator(enumerator); +} + +nsresult nsMsgSearchDBView::InsertHdrFromFolder(nsIMsgDBHdr* msgHdr, + nsIMsgFolder* folder) { + nsMsgViewIndex insertIndex = nsMsgViewIndex_None; + // Threaded view always needs to go through AddHdrFromFolder since + // it handles the xf view thread object creation. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) + insertIndex = GetInsertIndex(msgHdr); + + if (insertIndex == nsMsgViewIndex_None) + return AddHdrFromFolder(msgHdr, folder); + + nsMsgKey msgKey; + uint32_t msgFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + InsertMsgHdrAt(insertIndex, msgHdr, msgKey, msgFlags, 0); + + // The call to NoteChange() has to happen after we add the key as + // NoteChange() will call RowCountChanged() which will call our GetRowCount(). + NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchDBView::OnSearchHit(nsIMsgDBHdr* aMsgHdr, nsIMsgFolder* folder) { + NS_ENSURE_ARG(aMsgHdr); + NS_ENSURE_ARG(folder); + + if (m_folders.IndexOf(folder) < 0) + // Do this just for new folder. + { + nsCOMPtr<nsIMsgDatabase> dbToUse; + nsCOMPtr<nsIDBFolderInfo> folderInfo; + folder->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo), + getter_AddRefs(dbToUse)); + if (dbToUse) { + dbToUse->AddListener(this); + m_dbToUseList.AppendObject(dbToUse); + } + } + + m_totalMessagesInView++; + if (m_sortValid) + return InsertHdrFromFolder(aMsgHdr, folder); + else + return AddHdrFromFolder(aMsgHdr, folder); +} + +NS_IMETHODIMP +nsMsgSearchDBView::OnSearchDone(nsresult status) { + // We want to set imap delete model once the search is over because setting + // next message after deletion will happen before deleting the message and + // search scope can change with every search. + + // Set to default in case it is non-imap folder. + mDeleteModel = nsMsgImapDeleteModels::MoveToTrash; + nsIMsgFolder* curFolder = m_folders.SafeObjectAt(0); + if (curFolder) GetImapDeleteModel(curFolder); + + return NS_OK; +} + +// For now also acts as a way of resetting the search datasource. +NS_IMETHODIMP +nsMsgSearchDBView::OnNewSearch() { + int32_t oldSize = GetSize(); + + int32_t count = m_dbToUseList.Count(); + for (int32_t j = 0; j < count; j++) m_dbToUseList[j]->RemoveListener(this); + + m_dbToUseList.Clear(); + m_folders.Clear(); + m_keys.Clear(); + m_levels.Clear(); + m_flags.Clear(); + m_totalMessagesInView = 0; + + // Needs to happen after we remove the keys, since RowCountChanged() will + // call our GetRowCount(). + if (mTree) mTree->RowCountChanged(0, -oldSize); + if (mJSTree) mJSTree->RowCountChanged(0, -oldSize); + + // mSearchResults->Clear(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchDBView::GetViewType(nsMsgViewTypeValue* aViewType) { + NS_ENSURE_ARG_POINTER(aViewType); + *aViewType = nsMsgViewType::eShowSearch; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchDBView::SetSearchSession(nsIMsgSearchSession* aSession) { + m_searchSession = do_GetWeakReference(aSession); + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchDBView::OnAnnouncerGoingAway( + nsIDBChangeAnnouncer* instigator) { + nsIMsgDatabase* db = static_cast<nsIMsgDatabase*>(instigator); + if (db) { + db->RemoveListener(this); + m_dbToUseList.RemoveObject(db); + } + + return NS_OK; +} + +nsCOMArray<nsIMsgFolder>* nsMsgSearchDBView::GetFolders() { return &m_folders; } + +NS_IMETHODIMP +nsMsgSearchDBView::GetCommandStatus( + nsMsgViewCommandTypeValue command, bool* selectable_p, + nsMsgViewCommandCheckStateValue* selected_p) { + if (command != nsMsgViewCommandType::runJunkControls) + return nsMsgDBView::GetCommandStatus(command, selectable_p, selected_p); + + *selectable_p = false; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchDBView::DoCommandWithFolder(nsMsgViewCommandTypeValue command, + nsIMsgFolder* destFolder) { + mCommand = command; + mDestFolder = destFolder; + return nsMsgDBView::DoCommandWithFolder(command, destFolder); +} + +NS_IMETHODIMP nsMsgSearchDBView::DoCommand(nsMsgViewCommandTypeValue command) { + mCommand = command; + if (command == nsMsgViewCommandType::deleteMsg || + command == nsMsgViewCommandType::deleteNoTrash || + command == nsMsgViewCommandType::selectAll || + command == nsMsgViewCommandType::selectThread || + command == nsMsgViewCommandType::selectFlagged || + command == nsMsgViewCommandType::expandAll || + command == nsMsgViewCommandType::collapseAll) + return nsMsgDBView::DoCommand(command); + + nsresult rv = NS_OK; + nsMsgViewIndexArray selection; + GetIndicesForSelection(selection); + + // We need to break apart the selection by folders, and then call + // ApplyCommandToIndices with the command and the indices in the + // selection that are from that folder. + + mozilla::UniquePtr<nsTArray<nsMsgViewIndex>[]> indexArrays; + int32_t numArrays; + rv = PartitionSelectionByFolder(selection, indexArrays, &numArrays); + NS_ENSURE_SUCCESS(rv, rv); + for (int32_t folderIndex = 0; folderIndex < numArrays; folderIndex++) { + rv = ApplyCommandToIndices(command, (indexArrays.get())[folderIndex]); + NS_ENSURE_SUCCESS(rv, rv); + } + + return rv; +} + +// This method removes the specified line from the view, and adjusts the +// various flags and levels of affected messages. +nsresult nsMsgSearchDBView::RemoveByIndex(nsMsgViewIndex index) { + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgThread> thread; + nsresult rv = GetMsgHdrForViewIndex(index, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + GetXFThreadFromMsgHdr(msgHdr, getter_AddRefs(thread)); + if (thread) { + nsMsgXFViewThread* viewThread = + static_cast<nsMsgXFViewThread*>(thread.get()); + if (viewThread->MsgCount() == 2) { + // If we removed the next to last message in the thread, + // we need to adjust the flags on the first message in the thread. + nsMsgViewIndex threadIndex = m_levels[index] ? index - 1 : index; + if (threadIndex != nsMsgViewIndex_None) { + AndExtraFlag(threadIndex, + ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided | + MSG_VIEW_FLAG_HASCHILDREN)); + m_levels[threadIndex] = 0; + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + } + } + + // Bump up the level of all the descendents of the message + // that was removed, if the thread was expanded. + uint8_t removedLevel = m_levels[index]; + nsMsgViewIndex i = index + 1; + if (i < m_levels.Length() && m_levels[i] > removedLevel) { + // Promote the child of the removed message. + uint8_t promotedLevel = m_levels[i]; + m_levels[i] = promotedLevel - 1; + i++; + // Now promote all the children of the promoted message. + for (; i < m_levels.Length() && m_levels[i] > promotedLevel; i++) + m_levels[i] = m_levels[i] - 1; + } + } + } + + m_folders.RemoveObjectAt(index); + return nsMsgDBView::RemoveByIndex(index); +} + +NS_IMETHODIMP nsMsgSearchDBView::ApplyCommandToIndices( + nsMsgViewCommandTypeValue command, + nsTArray<nsMsgViewIndex> const& selection) { + mCommand = command; + return nsMsgDBView::ApplyCommandToIndices(command, selection); +} + +nsresult nsMsgSearchDBView::DeleteMessages( + nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection, + bool deleteStorage) { + nsresult rv = GetFoldersAndHdrsForSelection(selection); + NS_ENSURE_SUCCESS(rv, rv); + if (mDeleteModel != nsMsgImapDeleteModels::MoveToTrash) deleteStorage = true; + + if (mDeleteModel != nsMsgImapDeleteModels::IMAPDelete) m_deletingRows = true; + + // Remember the deleted messages in case the user undoes the delete, + // and we want to restore the hdr to the view, even if it no + // longer matches the search criteria. + for (nsMsgViewIndex viewIndex : selection) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + (void)GetMsgHdrForViewIndex(viewIndex, getter_AddRefs(msgHdr)); + if (msgHdr) { + RememberDeletedMsgHdr(msgHdr); + } + + // If we are deleting rows, save off the view indices. + if (m_deletingRows) { + mIndicesToNoteChange.AppendElement(viewIndex); + } + } + rv = deleteStorage ? ProcessRequestsInAllFolders(window) + : ProcessRequestsInOneFolder(window); + if (NS_FAILED(rv)) m_deletingRows = false; + + return rv; +} + +nsresult nsMsgSearchDBView::CopyMessages( + nsIMsgWindow* window, nsTArray<nsMsgViewIndex> const& selection, + bool isMove, nsIMsgFolder* destFolder) { + GetFoldersAndHdrsForSelection(selection); + return ProcessRequestsInOneFolder(window); +} + +nsresult nsMsgSearchDBView::PartitionSelectionByFolder( + nsTArray<nsMsgViewIndex> const& selection, + mozilla::UniquePtr<nsTArray<nsMsgViewIndex>[]>& indexArrays, + int32_t* numArrays) { + nsCOMArray<nsIMsgFolder> uniqueFoldersSelected; + nsTArray<uint32_t> numIndicesSelected; + mCurIndex = 0; + + // Build unique folder list based on headers selected by the user. + for (nsMsgViewIndex viewIndex : selection) { + nsIMsgFolder* curFolder = m_folders[viewIndex]; + int32_t folderIndex = uniqueFoldersSelected.IndexOf(curFolder); + if (folderIndex < 0) { + uniqueFoldersSelected.AppendObject(curFolder); + numIndicesSelected.AppendElement(1); + } else { + numIndicesSelected[folderIndex]++; + } + } + + int32_t numFolders = uniqueFoldersSelected.Count(); + indexArrays = mozilla::MakeUnique<nsTArray<nsMsgViewIndex>[]>(numFolders); + *numArrays = numFolders; + NS_ENSURE_TRUE(indexArrays, NS_ERROR_OUT_OF_MEMORY); + for (int32_t folderIndex = 0; folderIndex < numFolders; folderIndex++) { + (indexArrays.get())[folderIndex].SetCapacity( + numIndicesSelected[folderIndex]); + } + for (nsMsgViewIndex viewIndex : selection) { + nsIMsgFolder* curFolder = m_folders[viewIndex]; + int32_t folderIndex = uniqueFoldersSelected.IndexOf(curFolder); + (indexArrays.get())[folderIndex].AppendElement(viewIndex); + } + return NS_OK; +} + +nsresult nsMsgSearchDBView::GetFoldersAndHdrsForSelection( + nsTArray<nsMsgViewIndex> const& selection) { + nsresult rv = NS_OK; + mCurIndex = 0; + m_uniqueFoldersSelected.Clear(); + m_hdrsForEachFolder.Clear(); + + AutoTArray<RefPtr<nsIMsgDBHdr>, 1> messages; + rv = GetHeadersFromSelection(selection, messages); + NS_ENSURE_SUCCESS(rv, rv); + + // Build unique folder list based on headers selected by the user. + for (nsIMsgDBHdr* hdr : messages) { + nsCOMPtr<nsIMsgFolder> curFolder; + hdr->GetFolder(getter_AddRefs(curFolder)); + if (m_uniqueFoldersSelected.IndexOf(curFolder) < 0) { + m_uniqueFoldersSelected.AppendObject(curFolder); + } + } + + // Group the headers selected by each folder. + uint32_t numFolders = m_uniqueFoldersSelected.Count(); + for (uint32_t folderIndex = 0; folderIndex < numFolders; folderIndex++) { + nsIMsgFolder* curFolder = m_uniqueFoldersSelected[folderIndex]; + nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrsForOneFolder; + for (nsIMsgDBHdr* hdr : messages) { + nsCOMPtr<nsIMsgFolder> msgFolder; + hdr->GetFolder(getter_AddRefs(msgFolder)); + if (NS_SUCCEEDED(rv) && msgFolder && msgFolder == curFolder) { + msgHdrsForOneFolder.AppendElement(hdr); + } + } + + m_hdrsForEachFolder.AppendElement(msgHdrsForOneFolder.Clone()); + } + + return rv; +} + +nsresult nsMsgSearchDBView::ApplyCommandToIndicesWithFolder( + nsMsgViewCommandTypeValue command, + nsTArray<nsMsgViewIndex> const& selection, nsIMsgFolder* destFolder) { + mCommand = command; + mDestFolder = destFolder; + return nsMsgDBView::ApplyCommandToIndicesWithFolder(command, selection, + destFolder); +} + +// nsIMsgCopyServiceListener methods + +NS_IMETHODIMP +nsMsgSearchDBView::OnStartCopy() { return NS_OK; } + +NS_IMETHODIMP +nsMsgSearchDBView::OnProgress(uint32_t aProgress, uint32_t aProgressMax) { + return NS_OK; +} + +// Believe it or not, these next two are msgcopyservice listener methods! +NS_IMETHODIMP +nsMsgSearchDBView::SetMessageKey(nsMsgKey aMessageKey) { return NS_OK; } + +NS_IMETHODIMP +nsMsgSearchDBView::GetMessageId(nsACString& messageId) { return NS_OK; } + +NS_IMETHODIMP +nsMsgSearchDBView::OnStopCopy(nsresult aStatus) { + if (NS_SUCCEEDED(aStatus)) { + mCurIndex++; + if ((int32_t)mCurIndex < m_uniqueFoldersSelected.Count()) { + nsCOMPtr<nsIMsgWindow> msgWindow(do_QueryReferent(mMsgWindowWeak)); + ProcessRequestsInOneFolder(msgWindow); + } + } + + return NS_OK; +} + +// End nsIMsgCopyServiceListener methods. + +nsresult nsMsgSearchDBView::ProcessRequestsInOneFolder(nsIMsgWindow* window) { + nsresult rv = NS_OK; + + // Folder operations like copy/move are not implemented for .eml files. + if (m_uniqueFoldersSelected.Count() == 0) return NS_ERROR_NOT_IMPLEMENTED; + + nsIMsgFolder* curFolder = m_uniqueFoldersSelected[mCurIndex]; + NS_ASSERTION(curFolder, "curFolder is null"); + nsTArray<RefPtr<nsIMsgDBHdr>> const& msgs = m_hdrsForEachFolder[mCurIndex]; + + // called for delete with trash, copy and move + if (mCommand == nsMsgViewCommandType::deleteMsg) + curFolder->DeleteMessages(msgs, window, false /* delete storage */, + false /* is move*/, this, true /*allowUndo*/); + else { + NS_ASSERTION(!(curFolder == mDestFolder), + "The source folder and the destination folder are the same"); + if (NS_SUCCEEDED(rv) && curFolder != mDestFolder) { + nsCOMPtr<nsIMsgCopyService> copyService = + do_GetService("@mozilla.org/messenger/messagecopyservice;1", &rv); + if (NS_SUCCEEDED(rv)) { + if (mCommand == nsMsgViewCommandType::moveMessages) + copyService->CopyMessages(curFolder, msgs, mDestFolder, + true /* isMove */, this, window, + true /*allowUndo*/); + else if (mCommand == nsMsgViewCommandType::copyMessages) + copyService->CopyMessages(curFolder, msgs, mDestFolder, + false /* isMove */, this, window, + true /*allowUndo*/); + } + } + } + + return rv; +} + +nsresult nsMsgSearchDBView::ProcessRequestsInAllFolders(nsIMsgWindow* window) { + uint32_t numFolders = m_uniqueFoldersSelected.Count(); + for (uint32_t folderIndex = 0; folderIndex < numFolders; folderIndex++) { + nsIMsgFolder* curFolder = m_uniqueFoldersSelected[folderIndex]; + NS_ASSERTION(curFolder, "curFolder is null"); + curFolder->DeleteMessages( + m_hdrsForEachFolder[folderIndex], window, true /* delete storage */, + false /* is move*/, nullptr /*copyServListener*/, false /*allowUndo*/); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgSearchDBView::Sort(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) { + if (!m_checkedCustomColumns && CustomColumnsInSortAndNotRegistered()) + return NS_OK; + + int32_t rowCountBeforeSort = GetSize(); + + if (!rowCountBeforeSort) return NS_OK; + + if (m_viewFlags & (nsMsgViewFlagsType::kThreadedDisplay | + nsMsgViewFlagsType::kGroupBySort)) { + // ### This forgets which threads were expanded, and is sub-optimal + // since it rebuilds the thread objects. + m_sortType = sortType; + m_sortOrder = sortOrder; + return RebuildView(m_viewFlags); + } + + nsMsgKey preservedKey; + AutoTArray<nsMsgKey, 1> preservedSelection; + SaveAndClearSelection(&preservedKey, preservedSelection); + + nsresult rv = nsMsgDBView::Sort(sortType, sortOrder); + // The sort may have changed the number of rows before we restore the + // selection, tell the tree do this before we call restore selection. + // This is safe when there is no selection. + rv = AdjustRowCount(rowCountBeforeSort, GetSize()); + + RestoreSelection(preservedKey, preservedSelection); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +// If nothing selected, return an NS_ERROR. +NS_IMETHODIMP +nsMsgSearchDBView::GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) { + NS_ENSURE_ARG_POINTER(hdr); + nsMsgViewIndex index; + nsresult rv = GetViewIndexForFirstSelectedMsg(&index); + NS_ENSURE_SUCCESS(rv, rv); + return GetMsgHdrForViewIndex(index, hdr); +} + +NS_IMETHODIMP +nsMsgSearchDBView::OpenWithHdrs(nsIMsgEnumerator* aHeaders, + nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, + nsMsgViewFlagsTypeValue aViewFlags, + int32_t* aCount) { + if (aViewFlags & nsMsgViewFlagsType::kGroupBySort) + return nsMsgGroupView::OpenWithHdrs(aHeaders, aSortType, aSortOrder, + aViewFlags, aCount); + + m_sortType = aSortType; + m_sortOrder = aSortOrder; + m_viewFlags = aViewFlags; + SaveSortInfo(m_sortType, m_sortOrder); + + bool hasMore; + nsresult rv = NS_OK; + while (NS_SUCCEEDED(rv) && + NS_SUCCEEDED(rv = aHeaders->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgFolder> folder; + rv = aHeaders->GetNext(getter_AddRefs(msgHdr)); + if (NS_SUCCEEDED(rv) && msgHdr) { + msgHdr->GetFolder(getter_AddRefs(folder)); + AddHdrFromFolder(msgHdr, folder); + } + } + + *aCount = m_keys.Length(); + return rv; +} + +nsresult nsMsgSearchDBView::GetFolderFromMsgURI(const nsACString& aMsgURI, + nsIMsgFolder** aFolder) { + nsCOMPtr<nsIMsgMessageService> msgMessageService; + nsresult rv = + GetMessageServiceFromURI(aMsgURI, getter_AddRefs(msgMessageService)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = msgMessageService->MessageURIToMsgHdr(aMsgURI, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + return msgHdr->GetFolder(aFolder); +} + +nsMsgViewIndex nsMsgSearchDBView::FindHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startIndex, + bool allowDummy) { + nsCOMPtr<nsIMsgDBHdr> curHdr; + uint32_t index; + // It would be nice to take advantage of sorted views when possible. + for (index = startIndex; index < GetSize(); index++) { + GetMsgHdrForViewIndex(index, getter_AddRefs(curHdr)); + if (curHdr == msgHdr && + (allowDummy || !(m_flags[index] & MSG_VIEW_FLAG_DUMMY) || + (m_flags[index] & nsMsgMessageFlags::Elided))) + break; + } + + return index < GetSize() ? index : nsMsgViewIndex_None; +} + +// This method looks for the XF thread that corresponds to this message hdr, +// first by looking up the message id, then references, and finally, if subject +// threading is turned on, the subject. +nsresult nsMsgSearchDBView::GetXFThreadFromMsgHdr(nsIMsgDBHdr* msgHdr, + nsIMsgThread** pThread, + bool* foundByMessageId) { + NS_ENSURE_ARG_POINTER(msgHdr); + NS_ENSURE_ARG_POINTER(pThread); + + nsAutoCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + *pThread = nullptr; + m_threadsTable.Get(messageId, pThread); + // The caller may want to know if we found the thread by the msgHdr's + // messageId. + if (foundByMessageId) *foundByMessageId = *pThread != nullptr; + + if (!*pThread) { + uint16_t numReferences = 0; + msgHdr->GetNumReferences(&numReferences); + for (int32_t i = numReferences - 1; i >= 0 && !*pThread; i--) { + nsAutoCString reference; + msgHdr->GetStringReference(i, reference); + if (reference.IsEmpty()) break; + + m_threadsTable.Get(reference, pThread); + } + } + + // If we're threading by subject, and we couldn't find the thread by ref, + // just treat subject as an other ref. + if (!*pThread && !gReferenceOnlyThreading) { + nsCString subject; + msgHdr->GetSubject(subject); + // This is the raw rfc822 subject header, so this is OK. + m_threadsTable.Get(subject, pThread); + } + + return (*pThread) ? NS_OK : NS_ERROR_FAILURE; +} + +bool nsMsgSearchDBView::GetMsgHdrFromHash(nsCString& reference, + nsIMsgDBHdr** hdr) { + return m_hdrsTable.Get(reference, hdr); +} + +bool nsMsgSearchDBView::GetThreadFromHash(nsCString& reference, + nsIMsgThread** thread) { + return m_threadsTable.Get(reference, thread); +} + +nsresult nsMsgSearchDBView::AddRefToHash(nsCString& reference, + nsIMsgThread* thread) { + // Check if this reference is already is associated with a thread; + // If so, don't overwrite that association. + nsCOMPtr<nsIMsgThread> oldThread; + m_threadsTable.Get(reference, getter_AddRefs(oldThread)); + if (oldThread) return NS_OK; + + m_threadsTable.InsertOrUpdate(reference, thread); + return NS_OK; +} + +nsresult nsMsgSearchDBView::AddMsgToHashTables(nsIMsgDBHdr* msgHdr, + nsIMsgThread* thread) { + NS_ENSURE_ARG_POINTER(msgHdr); + + uint16_t numReferences = 0; + nsresult rv; + + msgHdr->GetNumReferences(&numReferences); + for (int32_t i = 0; i < numReferences; i++) { + nsAutoCString reference; + msgHdr->GetStringReference(i, reference); + if (reference.IsEmpty()) break; + + rv = AddRefToHash(reference, thread); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + m_hdrsTable.InsertOrUpdate(messageId, msgHdr); + if (!gReferenceOnlyThreading) { + nsCString subject; + msgHdr->GetSubject(subject); + // if we're threading by subject, just treat subject as an other ref. + AddRefToHash(subject, thread); + } + + return AddRefToHash(messageId, thread); +} + +nsresult nsMsgSearchDBView::RemoveRefFromHash(nsCString& reference) { + m_threadsTable.Remove(reference); + return NS_OK; +} + +nsresult nsMsgSearchDBView::RemoveMsgFromHashTables(nsIMsgDBHdr* msgHdr) { + NS_ENSURE_ARG_POINTER(msgHdr); + + uint16_t numReferences = 0; + nsresult rv = NS_OK; + + msgHdr->GetNumReferences(&numReferences); + + for (int32_t i = 0; i < numReferences; i++) { + nsAutoCString reference; + msgHdr->GetStringReference(i, reference); + if (reference.IsEmpty()) break; + + rv = RemoveRefFromHash(reference); + if (NS_FAILED(rv)) break; + } + + nsCString messageId; + msgHdr->GetMessageId(getter_Copies(messageId)); + m_hdrsTable.Remove(messageId); + RemoveRefFromHash(messageId); + if (!gReferenceOnlyThreading) { + nsCString subject; + msgHdr->GetSubject(subject); + // If we're threading by subject, just treat subject as an other ref. + RemoveRefFromHash(subject); + } + + return rv; +} + +nsMsgGroupThread* nsMsgSearchDBView::CreateGroupThread( + nsIMsgDatabase* /* db */) { + return new nsMsgXFGroupThread(); +} + +NS_IMETHODIMP +nsMsgSearchDBView::GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr, + nsIMsgThread** pThread) { + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) + return nsMsgGroupView::GetThreadContainingMsgHdr(msgHdr, pThread); + else if (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) + return GetXFThreadFromMsgHdr(msgHdr, pThread); + + // If not threaded, use the real thread. + nsCOMPtr<nsIMsgDatabase> msgDB; + nsresult rv = GetDBForHeader(msgHdr, getter_AddRefs(msgDB)); + NS_ENSURE_SUCCESS(rv, rv); + return msgDB->GetThreadContainingMsgHdr(msgHdr, pThread); +} + +nsresult nsMsgSearchDBView::ListIdsInThread( + nsIMsgThread* threadHdr, nsMsgViewIndex startOfThreadViewIndex, + uint32_t* pNumListed) { + NS_ENSURE_ARG_POINTER(threadHdr); + NS_ENSURE_ARG_POINTER(pNumListed); + + // These children ids should be in thread order. + uint32_t i; + nsMsgViewIndex viewIndex = startOfThreadViewIndex + 1; + *pNumListed = 0; + + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + NS_ASSERTION(numChildren, "Empty thread in view/db"); + if (!numChildren) return NS_OK; + + // Account for the existing thread root. + numChildren--; + InsertEmptyRows(viewIndex, numChildren); + + bool threadedView = m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay && + !(m_viewFlags & nsMsgViewFlagsType::kGroupBySort); + nsMsgXFViewThread* viewThread; + if (threadedView) viewThread = static_cast<nsMsgXFViewThread*>(threadHdr); + + for (i = 1; i <= numChildren; i++) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + threadHdr->GetChildHdrAt(i, getter_AddRefs(msgHdr)); + + if (msgHdr) { + nsMsgKey msgKey; + uint32_t msgFlags; + msgHdr->GetMessageKey(&msgKey); + msgHdr->GetFlags(&msgFlags); + uint8_t level = (threadedView) ? viewThread->ChildLevelAt(i) : 1; + SetMsgHdrAt(msgHdr, viewIndex, msgKey, msgFlags & ~MSG_VIEW_FLAGS, level); + (*pNumListed)++; + viewIndex++; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgSearchDBView::GetNumMsgsInView(int32_t* aNumMsgs) { + NS_ENSURE_ARG_POINTER(aNumMsgs); + *aNumMsgs = m_totalMessagesInView; + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgSearchDBView.h b/comm/mailnews/base/src/nsMsgSearchDBView.h new file mode 100644 index 0000000000..3dd363b58f --- /dev/null +++ b/comm/mailnews/base/src/nsMsgSearchDBView.h @@ -0,0 +1,182 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgSearchDBViews_H_ +#define _nsMsgSearchDBViews_H_ + +#include "mozilla/Attributes.h" +#include "nsMsgGroupView.h" +#include "nsIMsgCopyServiceListener.h" +#include "nsIMsgSearchNotify.h" +#include "nsMsgXFViewThread.h" +#include "nsCOMArray.h" +#include "mozilla/UniquePtr.h" +#include "nsIWeakReferenceUtils.h" + +class nsMsgSearchDBView : public nsMsgGroupView, + public nsIMsgCopyServiceListener, + public nsIMsgSearchNotify { + public: + nsMsgSearchDBView(); + + // these are tied together pretty intimately + friend class nsMsgXFViewThread; + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIMSGSEARCHNOTIFY + NS_DECL_NSIMSGCOPYSERVICELISTENER + + NS_IMETHOD SetSearchSession(nsIMsgSearchSession* aSearchSession) override; + + virtual const char* GetViewName(void) override { return "SearchView"; } + NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override; + NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, + nsIMsgDBView** _retval) override; + NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) override; + NS_IMETHOD Close() override; + NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override; + NS_IMETHOD Sort(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) override; + NS_IMETHOD GetCommandStatus( + nsMsgViewCommandTypeValue command, bool* selectable_p, + nsMsgViewCommandCheckStateValue* selected_p) override; + NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue command) override; + NS_IMETHOD DoCommandWithFolder(nsMsgViewCommandTypeValue command, + nsIMsgFolder* destFolder) override; + NS_IMETHOD GetHdrForFirstSelectedMessage(nsIMsgDBHdr** hdr) override; + NS_IMETHOD OpenWithHdrs(nsIMsgEnumerator* aHeaders, + nsMsgViewSortTypeValue aSortType, + nsMsgViewSortOrderValue aSortOrder, + nsMsgViewFlagsTypeValue aViewFlags, + int32_t* aCount) override; + NS_IMETHOD OnHdrDeleted(nsIMsgDBHdr* aHdrDeleted, nsMsgKey aParentKey, + int32_t aFlags, + nsIDBChangeListener* aInstigator) override; + NS_IMETHOD OnHdrFlagsChanged(nsIMsgDBHdr* aHdrChanged, uint32_t aOldFlags, + uint32_t aNewFlags, + nsIDBChangeListener* aInstigator) override; + NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override; + // override to get location + NS_IMETHOD GetCellText(int32_t aRow, nsTreeColumn* aCol, + nsAString& aValue) override; + virtual nsresult GetMsgHdrForViewIndex(nsMsgViewIndex index, + nsIMsgDBHdr** msgHdr) override; + virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey parentKey, + bool ensureListed) override; + NS_IMETHOD GetFolderForViewIndex(nsMsgViewIndex index, + nsIMsgFolder** folder) override; + + NS_IMETHOD OnAnnouncerGoingAway(nsIDBChangeAnnouncer* instigator) override; + + virtual nsCOMArray<nsIMsgFolder>* GetFolders() override; + virtual nsresult GetFolderFromMsgURI(const nsACString& aMsgURI, + nsIMsgFolder** aFolder) override; + + NS_IMETHOD GetThreadContainingMsgHdr(nsIMsgDBHdr* msgHdr, + nsIMsgThread** pThread) override; + + NS_IMETHOD ApplyCommandToIndices( + nsMsgViewCommandTypeValue command, + nsTArray<nsMsgViewIndex> const& selection) override; + + protected: + virtual ~nsMsgSearchDBView(); + virtual void InternalClose() override; + virtual nsresult HashHdr(nsIMsgDBHdr* msgHdr, nsString& aHashKey) override; + virtual nsresult ListIdsInThread(nsIMsgThread* threadHdr, + nsMsgViewIndex startOfThreadViewIndex, + uint32_t* pNumListed) override; + nsresult FetchLocation(int32_t aRow, nsAString& aLocationString); + virtual nsresult AddHdrFromFolder(nsIMsgDBHdr* msgHdr, nsIMsgFolder* folder); + virtual nsresult GetDBForViewIndex(nsMsgViewIndex index, + nsIMsgDatabase** db) override; + virtual nsresult RemoveByIndex(nsMsgViewIndex index) override; + virtual nsresult CopyMessages(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection, + bool isMove, nsIMsgFolder* destFolder) override; + virtual nsresult DeleteMessages(nsIMsgWindow* window, + nsTArray<nsMsgViewIndex> const& selection, + bool deleteStorage) override; + virtual void InsertMsgHdrAt(nsMsgViewIndex index, nsIMsgDBHdr* hdr, + nsMsgKey msgKey, uint32_t flags, + uint32_t level) override; + virtual void SetMsgHdrAt(nsIMsgDBHdr* hdr, nsMsgViewIndex index, + nsMsgKey msgKey, uint32_t flags, + uint32_t level) override; + virtual void InsertEmptyRows(nsMsgViewIndex viewIndex, + int32_t numRows) override; + virtual void RemoveRows(nsMsgViewIndex viewIndex, int32_t numRows) override; + virtual nsMsgViewIndex FindHdr(nsIMsgDBHdr* msgHdr, + nsMsgViewIndex startIndex = 0, + bool allowDummy = false) override; + nsresult GetFoldersAndHdrsForSelection( + nsTArray<nsMsgViewIndex> const& selection); + nsresult GroupSearchResultsByFolder(); + nsresult PartitionSelectionByFolder( + nsTArray<nsMsgViewIndex> const& selection, + mozilla::UniquePtr<nsTArray<uint32_t>[]>& indexArrays, + int32_t* numArrays); + + virtual nsresult ApplyCommandToIndicesWithFolder( + nsMsgViewCommandTypeValue command, + nsTArray<nsMsgViewIndex> const& selection, + nsIMsgFolder* destFolder) override; + void MoveThreadAt(nsMsgViewIndex threadIndex); + + virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator) override; + virtual nsresult InsertHdrFromFolder(nsIMsgDBHdr* msgHdr, + nsIMsgFolder* folder); + + // Holds the original folder of each message in this view. + // Augments the existing arrays in nsMsgDBView (m_keys, m_flags and m_levels), + // and is kept in sync with them. + nsCOMArray<nsIMsgFolder> m_folders; + + nsTArray<nsTArray<RefPtr<nsIMsgDBHdr>>> m_hdrsForEachFolder; + nsCOMArray<nsIMsgFolder> m_uniqueFoldersSelected; + uint32_t mCurIndex; + + int32_t mTotalIndices; + nsCOMArray<nsIMsgDatabase> m_dbToUseList; + nsMsgViewCommandTypeValue mCommand; + nsCOMPtr<nsIMsgFolder> mDestFolder; + nsWeakPtr m_searchSession; + + nsresult ProcessRequestsInOneFolder(nsIMsgWindow* window); + nsresult ProcessRequestsInAllFolders(nsIMsgWindow* window); + // these are for doing threading of the search hits + + // used for assigning thread id's to xfview threads. + nsMsgKey m_nextThreadId; + // this maps message-ids and reference message ids to + // the corresponding nsMsgXFViewThread object. If we're + // doing subject threading, we would throw subjects + // into the same table. + nsInterfaceHashtable<nsCStringHashKey, nsIMsgThread> m_threadsTable; + + // map message-ids to msg hdrs in the view, used for threading. + nsInterfaceHashtable<nsCStringHashKey, nsIMsgDBHdr> m_hdrsTable; + uint32_t m_totalMessagesInView; + + virtual nsMsgGroupThread* CreateGroupThread(nsIMsgDatabase* db) override; + nsresult GetXFThreadFromMsgHdr(nsIMsgDBHdr* msgHdr, nsIMsgThread** pThread, + bool* foundByMessageId = nullptr); + bool GetThreadFromHash(nsCString& reference, nsIMsgThread** thread); + bool GetMsgHdrFromHash(nsCString& reference, nsIMsgDBHdr** hdr); + nsresult AddRefToHash(nsCString& reference, nsIMsgThread* thread); + nsresult AddMsgToHashTables(nsIMsgDBHdr* msgHdr, nsIMsgThread* thread); + nsresult RemoveRefFromHash(nsCString& reference); + nsresult RemoveMsgFromHashTables(nsIMsgDBHdr* msgHdr); + nsresult InitRefHash(); +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgSpecialViews.cpp b/comm/mailnews/base/src/nsMsgSpecialViews.cpp new file mode 100644 index 0000000000..6bd42176e0 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgSpecialViews.cpp @@ -0,0 +1,163 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgSpecialViews.h" +#include "nsIMsgThread.h" +#include "nsMsgMessageFlags.h" + +nsMsgThreadsWithUnreadDBView::nsMsgThreadsWithUnreadDBView() + : m_totalUnwantedMessagesInView(0) {} + +nsMsgThreadsWithUnreadDBView::~nsMsgThreadsWithUnreadDBView() {} + +NS_IMETHODIMP nsMsgThreadsWithUnreadDBView::GetViewType( + nsMsgViewTypeValue* aViewType) { + NS_ENSURE_ARG_POINTER(aViewType); + *aViewType = nsMsgViewType::eShowThreadsWithUnread; + return NS_OK; +} + +bool nsMsgThreadsWithUnreadDBView::WantsThisThread(nsIMsgThread* threadHdr) { + if (threadHdr) { + uint32_t numNewChildren; + + threadHdr->GetNumUnreadChildren(&numNewChildren); + if (numNewChildren > 0) return true; + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + m_totalUnwantedMessagesInView += numChildren; + } + return false; +} + +nsresult nsMsgThreadsWithUnreadDBView::AddMsgToThreadNotInView( + nsIMsgThread* threadHdr, nsIMsgDBHdr* msgHdr, bool ensureListed) { + nsresult rv = NS_OK; + + nsCOMPtr<nsIMsgDBHdr> parentHdr; + uint32_t msgFlags; + msgHdr->GetFlags(&msgFlags); + GetFirstMessageHdrToDisplayInThread(threadHdr, getter_AddRefs(parentHdr)); + if (parentHdr && (ensureListed || !(msgFlags & nsMsgMessageFlags::Read))) { + nsMsgKey key; + uint32_t numMsgsInThread; + rv = AddHdr(parentHdr); + threadHdr->GetNumChildren(&numMsgsInThread); + if (numMsgsInThread > 1) { + parentHdr->GetMessageKey(&key); + nsMsgViewIndex viewIndex = FindViewIndex(key); + if (viewIndex != nsMsgViewIndex_None) + OrExtraFlag(viewIndex, + nsMsgMessageFlags::Elided | MSG_VIEW_FLAG_HASCHILDREN); + } + m_totalUnwantedMessagesInView -= numMsgsInThread; + } else + m_totalUnwantedMessagesInView++; + return rv; +} + +NS_IMETHODIMP +nsMsgThreadsWithUnreadDBView::CloneDBView( + nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, nsIMsgDBView** _retval) { + nsMsgThreadsWithUnreadDBView* newMsgDBView = + new nsMsgThreadsWithUnreadDBView(); + nsresult rv = + CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*_retval = newMsgDBView); + return NS_OK; +} + +NS_IMETHODIMP nsMsgThreadsWithUnreadDBView::GetNumMsgsInView( + int32_t* aNumMsgs) { + nsresult rv = nsMsgDBView::GetNumMsgsInView(aNumMsgs); + NS_ENSURE_SUCCESS(rv, rv); + *aNumMsgs = *aNumMsgs - m_totalUnwantedMessagesInView; + return rv; +} + +nsMsgWatchedThreadsWithUnreadDBView::nsMsgWatchedThreadsWithUnreadDBView() + : m_totalUnwantedMessagesInView(0) {} + +NS_IMETHODIMP nsMsgWatchedThreadsWithUnreadDBView::GetViewType( + nsMsgViewTypeValue* aViewType) { + NS_ENSURE_ARG_POINTER(aViewType); + *aViewType = nsMsgViewType::eShowWatchedThreadsWithUnread; + return NS_OK; +} + +bool nsMsgWatchedThreadsWithUnreadDBView::WantsThisThread( + nsIMsgThread* threadHdr) { + if (threadHdr) { + uint32_t numNewChildren; + uint32_t threadFlags; + + threadHdr->GetNumUnreadChildren(&numNewChildren); + threadHdr->GetFlags(&threadFlags); + if (numNewChildren > 0 && (threadFlags & nsMsgMessageFlags::Watched) != 0) + return true; + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + m_totalUnwantedMessagesInView += numChildren; + } + return false; +} + +nsresult nsMsgWatchedThreadsWithUnreadDBView::AddMsgToThreadNotInView( + nsIMsgThread* threadHdr, nsIMsgDBHdr* msgHdr, bool ensureListed) { + nsresult rv = NS_OK; + uint32_t threadFlags; + uint32_t msgFlags; + msgHdr->GetFlags(&msgFlags); + threadHdr->GetFlags(&threadFlags); + if (threadFlags & nsMsgMessageFlags::Watched) { + nsCOMPtr<nsIMsgDBHdr> parentHdr; + GetFirstMessageHdrToDisplayInThread(threadHdr, getter_AddRefs(parentHdr)); + if (parentHdr && (ensureListed || !(msgFlags & nsMsgMessageFlags::Read))) { + uint32_t numChildren; + threadHdr->GetNumChildren(&numChildren); + rv = AddHdr(parentHdr); + if (numChildren > 1) { + nsMsgKey key; + parentHdr->GetMessageKey(&key); + nsMsgViewIndex viewIndex = FindViewIndex(key); + if (viewIndex != nsMsgViewIndex_None) + OrExtraFlag(viewIndex, nsMsgMessageFlags::Elided | + MSG_VIEW_FLAG_ISTHREAD | + MSG_VIEW_FLAG_HASCHILDREN | + nsMsgMessageFlags::Watched); + } + m_totalUnwantedMessagesInView -= numChildren; + return rv; + } + } + m_totalUnwantedMessagesInView++; + return rv; +} + +NS_IMETHODIMP +nsMsgWatchedThreadsWithUnreadDBView::CloneDBView( + nsIMessenger* aMessengerInstance, nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, nsIMsgDBView** _retval) { + nsMsgWatchedThreadsWithUnreadDBView* newMsgDBView = + new nsMsgWatchedThreadsWithUnreadDBView(); + nsresult rv = + CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*_retval = newMsgDBView); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgWatchedThreadsWithUnreadDBView::GetNumMsgsInView(int32_t* aNumMsgs) { + nsresult rv = nsMsgDBView::GetNumMsgsInView(aNumMsgs); + NS_ENSURE_SUCCESS(rv, rv); + *aNumMsgs = *aNumMsgs - m_totalUnwantedMessagesInView; + return rv; +} diff --git a/comm/mailnews/base/src/nsMsgSpecialViews.h b/comm/mailnews/base/src/nsMsgSpecialViews.h new file mode 100644 index 0000000000..1503eec123 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgSpecialViews.h @@ -0,0 +1,82 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgSpecialViews_H_ +#define _nsMsgSpecialViews_H_ + +#include "mozilla/Attributes.h" +#include "nsMsgThreadedDBView.h" + +class nsMsgThreadsWithUnreadDBView : public nsMsgThreadedDBView { + public: + nsMsgThreadsWithUnreadDBView(); + virtual ~nsMsgThreadsWithUnreadDBView(); + virtual const char* GetViewName(void) override { + return "ThreadsWithUnreadView"; + } + NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCommandUpdater, + nsIMsgDBView** _retval) override; + NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override; + NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override; + virtual bool WantsThisThread(nsIMsgThread* threadHdr) override; + + protected: + virtual nsresult AddMsgToThreadNotInView(nsIMsgThread* threadHdr, + nsIMsgDBHdr* msgHdr, + bool ensureListed) override; + uint32_t m_totalUnwantedMessagesInView; +}; + +class nsMsgWatchedThreadsWithUnreadDBView : public nsMsgThreadedDBView { + public: + nsMsgWatchedThreadsWithUnreadDBView(); + NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override; + NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCommandUpdater, + nsIMsgDBView** _retval) override; + NS_IMETHOD GetNumMsgsInView(int32_t* aNumMsgs) override; + virtual const char* GetViewName(void) override { + return "WatchedThreadsWithUnreadView"; + } + virtual bool WantsThisThread(nsIMsgThread* threadHdr) override; + + protected: + virtual nsresult AddMsgToThreadNotInView(nsIMsgThread* threadHdr, + nsIMsgDBHdr* msgHdr, + bool ensureListed) override; + uint32_t m_totalUnwantedMessagesInView; +}; +#ifdef DOING_CACHELESS_VIEW +// This view will initially be used for cacheless IMAP. +class nsMsgCachelessView : public nsMsgDBView { + public: + nsMsgCachelessView(); + NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType); + virtual ~nsMsgCachelessView(); + virtual const char* GetViewName(void) { return "nsMsgCachelessView"; } + NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue viewType, + int32_t* count); + nsresult SetViewSize(int32_t setSize); // Override + virtual nsresult AddNewMessages(); + virtual nsresult AddHdr(nsIMsgDBHdr* msgHdr); + // for news, xover line, potentially, for IMAP, imap line... + virtual nsresult AddHdrFromServerLine(char* line, nsMsgKey* msgId); + virtual void SetInitialSortState(void); + virtual nsresult Init(uint32_t* pCount); + + protected: + void ClearPendingIds(); + + nsIMsgFolder* m_folder; + nsMsgViewIndex m_curStartSeq; + nsMsgViewIndex m_curEndSeq; + bool m_sizeInitialized; +}; + +#endif /* DOING_CACHELESS_VIEW */ +#endif diff --git a/comm/mailnews/base/src/nsMsgStatusFeedback.cpp b/comm/mailnews/base/src/nsMsgStatusFeedback.cpp new file mode 100644 index 0000000000..3afa14de87 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgStatusFeedback.cpp @@ -0,0 +1,261 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" + +#include "nsIWebProgress.h" +#include "nsIXULBrowserWindow.h" +#include "nsMsgStatusFeedback.h" +#include "mozilla/dom/Document.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "nsIChannel.h" +#include "prinrval.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIMsgWindow.h" +#include "nsMsgUtils.h" +#include "nsIMsgHdr.h" +#include "nsIMsgFolder.h" +#include "nsMsgDBFolder.h" +#include "nsServiceManagerUtils.h" +#include "mozilla/Components.h" +#include "nsMsgUtils.h" + +#define MSGFEEDBACK_TIMER_INTERVAL 500 + +nsMsgStatusFeedback::nsMsgStatusFeedback() + : m_meteorsSpinning(false), m_lastPercent(0), m_lastProgressTime(0) { + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + + if (bundleService) + bundleService->CreateBundle( + "chrome://messenger/locale/messenger.properties", + getter_AddRefs(mBundle)); +} + +nsMsgStatusFeedback::~nsMsgStatusFeedback() { mBundle = nullptr; } + +NS_IMPL_ISUPPORTS(nsMsgStatusFeedback, nsIMsgStatusFeedback, + nsIProgressEventSink, nsIWebProgressListener, + nsISupportsWeakReference) + +////////////////////////////////////////////////////////////////////////////////// +// nsMsgStatusFeedback::nsIWebProgressListener +////////////////////////////////////////////////////////////////////////////////// + +NS_IMETHODIMP +nsMsgStatusFeedback::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + int32_t percentage = 0; + if (aMaxTotalProgress > 0) { + percentage = (aCurTotalProgress * 100) / aMaxTotalProgress; + if (percentage) ShowProgress(percentage); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgStatusFeedback::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aProgressStateFlags, + nsresult aStatus) { + nsresult rv; + + NS_ENSURE_TRUE(mBundle, NS_ERROR_NULL_POINTER); + if (aProgressStateFlags & STATE_IS_NETWORK) { + if (aProgressStateFlags & STATE_START) { + m_lastPercent = 0; + StartMeteors(); + nsString loadingDocument; + rv = mBundle->GetStringFromName("documentLoading", loadingDocument); + if (NS_SUCCEEDED(rv)) ShowStatusString(loadingDocument); + } else if (aProgressStateFlags & STATE_STOP) { + // if we are loading message for display purposes, this STATE_STOP + // notification is the only notification we get when layout is actually + // done rendering the message. We need to fire the appropriate msgHdrSink + // notification in this particular case. + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + if (channel) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(uri)); + if (mailnewsUrl) { + // get the url type + bool messageDisplayUrl; + mailnewsUrl->IsUrlType(nsIMsgMailNewsUrl::eDisplay, + &messageDisplayUrl); + + if (messageDisplayUrl) { + // get the folder and notify that the msg has been loaded. We're + // using NotifyPropertyFlagChanged. To be completely consistent, + // we'd send a similar notification that the old message was + // unloaded. + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgFolder> msgFolder; + mailnewsUrl->GetFolder(getter_AddRefs(msgFolder)); + nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(mailnewsUrl); + if (msgUrl) { + // not sending this notification is not a fatal error... + (void)msgUrl->GetMessageHeader(getter_AddRefs(msgHdr)); + if (msgFolder && msgHdr) + msgFolder->NotifyPropertyFlagChanged(msgHdr, kMsgLoaded, 0, 1); + } + } + } + } + StopMeteors(); + nsString documentDone; + rv = mBundle->GetStringFromName("documentDone", documentDone); + if (NS_SUCCEEDED(rv)) ShowStatusString(documentDone); + } + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgStatusFeedback::OnLocationChange( + nsIWebProgress* aWebProgress, nsIRequest* aRequest, nsIURI* aLocation, + uint32_t aFlags) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgStatusFeedback::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgStatusFeedback::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t state) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgStatusFeedback::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + return NS_OK; +} + +NS_IMETHODIMP +nsMsgStatusFeedback::ShowStatusString(const nsAString& aStatus) { + nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback( + do_QueryReferent(mJSStatusFeedbackWeak)); + if (jsStatusFeedback) jsStatusFeedback->ShowStatusString(aStatus); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgStatusFeedback::SetStatusString(const nsAString& aStatus) { + nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback( + do_QueryReferent(mJSStatusFeedbackWeak)); + if (jsStatusFeedback) jsStatusFeedback->SetStatusString(aStatus); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgStatusFeedback::ShowProgress(int32_t aPercentage) { + // if the percentage hasn't changed...OR if we are going from 0 to 100% in one + // step then don't bother....just fall out.... + if (aPercentage == m_lastPercent || + (m_lastPercent == 0 && aPercentage >= 100)) + return NS_OK; + + m_lastPercent = aPercentage; + + int64_t nowMS = 0; + if (aPercentage < 100) // always need to do 100% + { + nowMS = PR_IntervalToMilliseconds(PR_IntervalNow()); + if (nowMS < m_lastProgressTime + 250) return NS_OK; + } + + m_lastProgressTime = nowMS; + nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback( + do_QueryReferent(mJSStatusFeedbackWeak)); + if (jsStatusFeedback) jsStatusFeedback->ShowProgress(aPercentage); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgStatusFeedback::StartMeteors() { + nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback( + do_QueryReferent(mJSStatusFeedbackWeak)); + if (jsStatusFeedback) jsStatusFeedback->StartMeteors(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgStatusFeedback::StopMeteors() { + nsCOMPtr<nsIMsgStatusFeedback> jsStatusFeedback( + do_QueryReferent(mJSStatusFeedbackWeak)); + if (jsStatusFeedback) jsStatusFeedback->StopMeteors(); + return NS_OK; +} + +NS_IMETHODIMP nsMsgStatusFeedback::SetWrappedStatusFeedback( + nsIMsgStatusFeedback* aJSStatusFeedback) { + NS_ENSURE_ARG_POINTER(aJSStatusFeedback); + mJSStatusFeedbackWeak = do_GetWeakReference(aJSStatusFeedback); + return NS_OK; +} + +NS_IMETHODIMP nsMsgStatusFeedback::OnProgress(nsIRequest* request, + int64_t aProgress, + int64_t aProgressMax) { + // XXX: What should the nsIWebProgress be? + // XXX: this truncates 64-bit to 32-bit + return OnProgressChange(nullptr, request, int32_t(aProgress), + int32_t(aProgressMax), + int32_t(aProgress) /* current total progress */, + int32_t(aProgressMax) /* max total progress */); +} + +NS_IMETHODIMP nsMsgStatusFeedback::OnStatus(nsIRequest* request, + nsresult aStatus, + const char16_t* aStatusArg) { + nsresult rv; + nsCOMPtr<nsIURI> uri; + nsString accountName; + // fetching account name from nsIRequest + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request); + rv = aChannel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIMsgMailNewsUrl> url(do_QueryInterface(uri)); + if (url) { + nsCOMPtr<nsIMsgIncomingServer> server; + url->GetServer(getter_AddRefs(server)); + if (server) server->GetPrettyName(accountName); + } + + // forming the status message + nsCOMPtr<nsIStringBundleService> sbs = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(sbs, NS_ERROR_UNEXPECTED); + nsString str; + rv = sbs->FormatStatusMessage(aStatus, aStatusArg, str); + NS_ENSURE_SUCCESS(rv, rv); + + // prefixing the account name to the status message if status message isn't + // blank and doesn't already contain the account name. + nsString statusMessage; + if (!str.IsEmpty() && str.Find(accountName) == kNotFound) { + nsCOMPtr<nsIStringBundle> bundle; + rv = sbs->CreateBundle(MSGS_URL, getter_AddRefs(bundle)); + AutoTArray<nsString, 2> params = {accountName, str}; + rv = bundle->FormatStringFromName("statusMessage", params, statusMessage); + NS_ENSURE_SUCCESS(rv, rv); + } else { + statusMessage.Assign(str); + } + return ShowStatusString(statusMessage); +} diff --git a/comm/mailnews/base/src/nsMsgStatusFeedback.h b/comm/mailnews/base/src/nsMsgStatusFeedback.h new file mode 100644 index 0000000000..6713eaea4c --- /dev/null +++ b/comm/mailnews/base/src/nsMsgStatusFeedback.h @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgStatusFeedback_h +#define _nsMsgStatusFeedback_h + +#include "nsIWebProgressListener.h" +#include "nsIObserver.h" +#include "nsITimer.h" +#include "nsCOMPtr.h" +#include "nsIMsgStatusFeedback.h" +#include "nsIProgressEventSink.h" +#include "nsIStringBundle.h" +#include "nsWeakReference.h" +#include "nsIWeakReferenceUtils.h" + +class nsMsgStatusFeedback : public nsIMsgStatusFeedback, + public nsIProgressEventSink, + public nsIWebProgressListener, + public nsSupportsWeakReference { + public: + nsMsgStatusFeedback(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIMSGSTATUSFEEDBACK + NS_DECL_NSIWEBPROGRESSLISTENER + NS_DECL_NSIPROGRESSEVENTSINK + + protected: + virtual ~nsMsgStatusFeedback(); + + bool m_meteorsSpinning; + int32_t m_lastPercent; + int64_t m_lastProgressTime; + + void BeginObserving(); + void EndObserving(); + + // the JS status feedback implementation object...eventually this object + // will replace this very C++ class you are looking at. + nsWeakPtr mJSStatusFeedbackWeak; + + nsCOMPtr<nsIStringBundle> mBundle; +}; + +#endif // _nsMsgStatusFeedback_h diff --git a/comm/mailnews/base/src/nsMsgTagService.cpp b/comm/mailnews/base/src/nsMsgTagService.cpp new file mode 100644 index 0000000000..83ced5fdeb --- /dev/null +++ b/comm/mailnews/base/src/nsMsgTagService.cpp @@ -0,0 +1,458 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgTagService.h" +#include "nsIPrefService.h" +#include "nsISupportsPrimitives.h" +#include "nsMsgI18N.h" +#include "nsIPrefLocalizedString.h" +#include "nsMsgDBView.h" // for labels migration +#include "nsQuickSort.h" +#include "nsMsgUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsMemory.h" + +#define STRLEN(s) (sizeof(s) - 1) + +#define TAG_PREF_VERSION "version" +#define TAG_PREF_SUFFIX_TAG ".tag" +#define TAG_PREF_SUFFIX_COLOR ".color" +#define TAG_PREF_SUFFIX_ORDINAL ".ordinal" + +static bool gMigratingKeys = false; + +// Comparator to set sort order in GetAllTags(). +struct CompareMsgTags { + private: + int cmp(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const { + // Sort nsMsgTag objects by ascending order, using their ordinal or key. + // The "smallest" value will be first in the sorted array, + // thus being the most important element. + + // Only use the key if the ordinal is not defined or empty. + nsAutoCString value1, value2; + element1->GetOrdinal(value1); + if (value1.IsEmpty()) element1->GetKey(value1); + element2->GetOrdinal(value2); + if (value2.IsEmpty()) element2->GetKey(value2); + + return strcmp(value1.get(), value2.get()); + } + + public: + bool Equals(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const { + return cmp(element1, element2) == 0; + } + bool LessThan(RefPtr<nsIMsgTag> element1, RefPtr<nsIMsgTag> element2) const { + return cmp(element1, element2) < 0; + } +}; + +// +// nsMsgTag +// +NS_IMPL_ISUPPORTS(nsMsgTag, nsIMsgTag) + +nsMsgTag::nsMsgTag(const nsACString& aKey, const nsAString& aTag, + const nsACString& aColor, const nsACString& aOrdinal) + : mTag(aTag), mKey(aKey), mColor(aColor), mOrdinal(aOrdinal) {} + +nsMsgTag::~nsMsgTag() {} + +/* readonly attribute ACString key; */ +NS_IMETHODIMP nsMsgTag::GetKey(nsACString& aKey) { + aKey = mKey; + return NS_OK; +} + +/* readonly attribute AString tag; */ +NS_IMETHODIMP nsMsgTag::GetTag(nsAString& aTag) { + aTag = mTag; + return NS_OK; +} + +/* readonly attribute ACString color; */ +NS_IMETHODIMP nsMsgTag::GetColor(nsACString& aColor) { + aColor = mColor; + return NS_OK; +} + +/* readonly attribute ACString ordinal; */ +NS_IMETHODIMP nsMsgTag::GetOrdinal(nsACString& aOrdinal) { + aOrdinal = mOrdinal; + return NS_OK; +} + +// +// nsMsgTagService +// +NS_IMPL_ISUPPORTS(nsMsgTagService, nsIMsgTagService) + +nsMsgTagService::nsMsgTagService() { + m_tagPrefBranch = nullptr; + nsCOMPtr<nsIPrefService> prefService( + do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (prefService) + prefService->GetBranch("mailnews.tags.", getter_AddRefs(m_tagPrefBranch)); + SetupLabelTags(); + RefreshKeyCache(); +} + +nsMsgTagService::~nsMsgTagService() {} /* destructor code */ + +/* wstring getTagForKey (in string key); */ +NS_IMETHODIMP nsMsgTagService::GetTagForKey(const nsACString& key, + nsAString& _retval) { + nsAutoCString prefName(key); + if (!gMigratingKeys) ToLowerCase(prefName); + prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG); + return GetUnicharPref(prefName.get(), _retval); +} + +/* void setTagForKey (in string key); */ +NS_IMETHODIMP nsMsgTagService::SetTagForKey(const nsACString& key, + const nsAString& tag) { + nsAutoCString prefName(key); + ToLowerCase(prefName); + prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG); + return SetUnicharPref(prefName.get(), tag); +} + +/* void getKeyForTag (in wstring tag); */ +NS_IMETHODIMP nsMsgTagService::GetKeyForTag(const nsAString& aTag, + nsACString& aKey) { + nsTArray<nsCString> prefList; + nsresult rv = m_tagPrefBranch->GetChildList("", prefList); + NS_ENSURE_SUCCESS(rv, rv); + // traverse the list, and look for a pref with the desired tag value. + // XXXbz is there a good reason to reverse the list here, or did the + // old code do it just to be clever and save some characters in the + // for loop header? + for (auto& prefName : mozilla::Reversed(prefList)) { + // We are returned the tag prefs in the form "<key>.<tag_data_type>", but + // since we only want the tags, just check that the string ends with "tag". + if (StringEndsWith(prefName, nsLiteralCString(TAG_PREF_SUFFIX_TAG))) { + nsAutoString curTag; + GetUnicharPref(prefName.get(), curTag); + if (aTag.Equals(curTag)) { + aKey = Substring(prefName, 0, + prefName.Length() - STRLEN(TAG_PREF_SUFFIX_TAG)); + break; + } + } + } + ToLowerCase(aKey); + return NS_OK; +} + +/* ACString getTopKey (in ACString keylist); */ +NS_IMETHODIMP nsMsgTagService::GetTopKey(const nsACString& keyList, + nsACString& _retval) { + _retval.Truncate(); + // find the most important key + nsTArray<nsCString> keyArray; + ParseString(keyList, ' ', keyArray); + uint32_t keyCount = keyArray.Length(); + nsCString *topKey = nullptr, *key, topOrdinal, ordinal; + for (uint32_t i = 0; i < keyCount; ++i) { + key = &keyArray[i]; + if (key->IsEmpty()) continue; + + // ignore unknown keywords + nsAutoString tagValue; + nsresult rv = GetTagForKey(*key, tagValue); + if (NS_FAILED(rv) || tagValue.IsEmpty()) continue; + + // new top key, judged by ordinal order? + rv = GetOrdinalForKey(*key, ordinal); + if (NS_FAILED(rv) || ordinal.IsEmpty()) ordinal = *key; + if ((ordinal < topOrdinal) || topOrdinal.IsEmpty()) { + topOrdinal = ordinal; + topKey = key; // copy actual result key only once - later + } + } + // return the most important key - if any + if (topKey) _retval = *topKey; + return NS_OK; +} + +/* void addTagForKey (in string key, in wstring tag, in string color, in string + * ordinal); */ +NS_IMETHODIMP nsMsgTagService::AddTagForKey(const nsACString& key, + const nsAString& tag, + const nsACString& color, + const nsACString& ordinal) { + nsAutoCString prefName(key); + ToLowerCase(prefName); + prefName.AppendLiteral(TAG_PREF_SUFFIX_TAG); + nsresult rv = SetUnicharPref(prefName.get(), tag); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetColorForKey(key, color); + NS_ENSURE_SUCCESS(rv, rv); + rv = RefreshKeyCache(); + NS_ENSURE_SUCCESS(rv, rv); + return SetOrdinalForKey(key, ordinal); +} + +/* void addTag (in wstring tag, in long color); */ +NS_IMETHODIMP nsMsgTagService::AddTag(const nsAString& tag, + const nsACString& color, + const nsACString& ordinal) { + // figure out key from tag. Apply transformation stripping out + // illegal characters like <SP> and then convert to imap mod utf7. + // Then, check if we have a tag with that key yet, and if so, + // make it unique by appending A, AA, etc. + // Should we use an iterator? + nsAutoString transformedTag(tag); + transformedTag.ReplaceChar(u" ()/{%*<>\\\"", u'_'); + nsAutoCString key; + CopyUTF16toMUTF7(transformedTag, key); + // We have an imap server that converts keys to upper case so we're going + // to normalize all keys to lower case (upper case looks ugly in prefs.js) + ToLowerCase(key); + nsAutoCString prefName(key); + while (true) { + nsAutoString tagValue; + nsresult rv = GetTagForKey(prefName, tagValue); + if (NS_FAILED(rv) || tagValue.IsEmpty() || tagValue.Equals(tag)) + return AddTagForKey(prefName, tag, color, ordinal); + prefName.Append('A'); + } + NS_ASSERTION(false, "can't get here"); + return NS_ERROR_FAILURE; +} + +/* long getColorForKey (in string key); */ +NS_IMETHODIMP nsMsgTagService::GetColorForKey(const nsACString& key, + nsACString& _retval) { + nsAutoCString prefName(key); + if (!gMigratingKeys) ToLowerCase(prefName); + prefName.AppendLiteral(TAG_PREF_SUFFIX_COLOR); + nsCString color; + nsresult rv = m_tagPrefBranch->GetCharPref(prefName.get(), color); + if (NS_SUCCEEDED(rv)) _retval = color; + return NS_OK; +} + +/* long getSelectorForKey (in ACString key, out AString selector); */ +NS_IMETHODIMP nsMsgTagService::GetSelectorForKey(const nsACString& key, + nsAString& _retval) { + // Our keys are the result of MUTF-7 encoding. For CSS selectors we need + // to reduce this to 0-9A-Za-z_ with a leading alpha character. + // We encode non-alphanumeric characters using _ as an escape character + // and start with a leading T in all cases. This way users defining tags + // "selected" or "focus" don't collide with inbuilt "selected" or "focus". + + // Calculate length of selector string. + const char* in = key.BeginReading(); + size_t outLen = 1; + while (*in) { + if (('0' <= *in && *in <= '9') || ('A' <= *in && *in <= 'Z') || + ('a' <= *in && *in <= 'z')) { + outLen++; + } else { + outLen += 3; + } + in++; + } + + // Now fill selector string. + _retval.SetCapacity(outLen); + _retval.Assign('T'); + in = key.BeginReading(); + while (*in) { + if (('0' <= *in && *in <= '9') || ('A' <= *in && *in <= 'Z') || + ('a' <= *in && *in <= 'z')) { + _retval.Append(*in); + } else { + _retval.AppendPrintf("_%02x", *in); + } + in++; + } + + return NS_OK; +} + +/* void setColorForKey (in ACString key, in ACString color); */ +NS_IMETHODIMP nsMsgTagService::SetColorForKey(const nsACString& key, + const nsACString& color) { + nsAutoCString prefName(key); + ToLowerCase(prefName); + prefName.AppendLiteral(TAG_PREF_SUFFIX_COLOR); + if (color.IsEmpty()) { + m_tagPrefBranch->ClearUserPref(prefName.get()); + return NS_OK; + } + return m_tagPrefBranch->SetCharPref(prefName.get(), color); +} + +/* ACString getOrdinalForKey (in ACString key); */ +NS_IMETHODIMP nsMsgTagService::GetOrdinalForKey(const nsACString& key, + nsACString& _retval) { + nsAutoCString prefName(key); + if (!gMigratingKeys) ToLowerCase(prefName); + prefName.AppendLiteral(TAG_PREF_SUFFIX_ORDINAL); + nsCString ordinal; + nsresult rv = m_tagPrefBranch->GetCharPref(prefName.get(), ordinal); + _retval = ordinal; + return rv; +} + +/* void setOrdinalForKey (in ACString key, in ACString ordinal); */ +NS_IMETHODIMP nsMsgTagService::SetOrdinalForKey(const nsACString& key, + const nsACString& ordinal) { + nsAutoCString prefName(key); + ToLowerCase(prefName); + prefName.AppendLiteral(TAG_PREF_SUFFIX_ORDINAL); + if (ordinal.IsEmpty()) { + m_tagPrefBranch->ClearUserPref(prefName.get()); + return NS_OK; + } + return m_tagPrefBranch->SetCharPref(prefName.get(), ordinal); +} + +/* void deleteTag (in wstring tag); */ +NS_IMETHODIMP nsMsgTagService::DeleteKey(const nsACString& key) { + // clear the associated prefs + nsAutoCString prefName(key); + if (!gMigratingKeys) ToLowerCase(prefName); + prefName.Append('.'); + + nsTArray<nsCString> prefNames; + nsresult rv = m_tagPrefBranch->GetChildList(prefName.get(), prefNames); + NS_ENSURE_SUCCESS(rv, rv); + + for (auto& prefName : prefNames) { + m_tagPrefBranch->ClearUserPref(prefName.get()); + } + + return RefreshKeyCache(); +} + +/* Array<nsIMsgTag> getAllTags(); */ +NS_IMETHODIMP nsMsgTagService::GetAllTags( + nsTArray<RefPtr<nsIMsgTag>>& aTagArray) { + aTagArray.Clear(); + + // get the actual tag definitions + nsresult rv; + nsTArray<nsCString> prefList; + rv = m_tagPrefBranch->GetChildList("", prefList); + NS_ENSURE_SUCCESS(rv, rv); + // sort them by key for ease of processing + prefList.Sort(); + + nsString tag; + nsCString lastKey, color, ordinal; + for (auto& pref : mozilla::Reversed(prefList)) { + // extract just the key from <key>.<info=tag|color|ordinal> + int32_t dotLoc = pref.RFindChar('.'); + if (dotLoc != kNotFound) { + auto& key = Substring(pref, 0, dotLoc); + if (key != lastKey) { + if (!key.IsEmpty()) { + // .tag MUST exist (but may be empty) + rv = GetTagForKey(key, tag); + if (NS_SUCCEEDED(rv)) { + // .color MAY exist + color.Truncate(); + GetColorForKey(key, color); + // .ordinal MAY exist + rv = GetOrdinalForKey(key, ordinal); + if (NS_FAILED(rv)) ordinal.Truncate(); + // store the tag info in our array + aTagArray.AppendElement(new nsMsgTag(key, tag, color, ordinal)); + } + } + lastKey = key; + } + } + } + + // sort the non-null entries by ordinal + aTagArray.Sort(CompareMsgTags()); + return NS_OK; +} + +nsresult nsMsgTagService::SetUnicharPref(const char* prefName, + const nsAString& val) { + nsresult rv = NS_OK; + if (!val.IsEmpty()) { + rv = m_tagPrefBranch->SetStringPref(prefName, NS_ConvertUTF16toUTF8(val)); + } else { + m_tagPrefBranch->ClearUserPref(prefName); + } + return rv; +} + +nsresult nsMsgTagService::GetUnicharPref(const char* prefName, + nsAString& prefValue) { + nsCString valueUtf8; + nsresult rv = + m_tagPrefBranch->GetStringPref(prefName, EmptyCString(), 0, valueUtf8); + CopyUTF8toUTF16(valueUtf8, prefValue); + return rv; +} + +nsresult nsMsgTagService::SetupLabelTags() { + nsCString prefString; + + int32_t prefVersion = 0; + nsresult rv = m_tagPrefBranch->GetIntPref(TAG_PREF_VERSION, &prefVersion); + if (NS_SUCCEEDED(rv) && prefVersion > 1) { + return rv; + } + nsCOMPtr<nsIPrefBranch> prefRoot(do_GetService(NS_PREFSERVICE_CONTRACTID)); + nsCOMPtr<nsIPrefLocalizedString> pls; + nsString ucsval; + nsAutoCString labelKey("$label1"); + for (int32_t i = 0; i < 5;) { + prefString.AssignLiteral("mailnews.labels.description."); + prefString.AppendInt(i + 1); + rv = prefRoot->GetComplexValue(prefString.get(), + NS_GET_IID(nsIPrefLocalizedString), + getter_AddRefs(pls)); + NS_ENSURE_SUCCESS(rv, rv); + pls->ToString(getter_Copies(ucsval)); + + prefString.AssignLiteral("mailnews.labels.color."); + prefString.AppendInt(i + 1); + nsCString csval; + rv = prefRoot->GetCharPref(prefString.get(), csval); + NS_ENSURE_SUCCESS(rv, rv); + + rv = AddTagForKey(labelKey, ucsval, csval, EmptyCString()); + NS_ENSURE_SUCCESS(rv, rv); + labelKey.SetCharAt(++i + '1', 6); + } + m_tagPrefBranch->SetIntPref(TAG_PREF_VERSION, 2); + return rv; +} + +NS_IMETHODIMP nsMsgTagService::IsValidKey(const nsACString& aKey, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = m_keys.Contains(aKey); + return NS_OK; +} + +// refresh the local tag key array m_keys from preferences +nsresult nsMsgTagService::RefreshKeyCache() { + nsTArray<RefPtr<nsIMsgTag>> tagArray; + nsresult rv = GetAllTags(tagArray); + NS_ENSURE_SUCCESS(rv, rv); + m_keys.Clear(); + + uint32_t numTags = tagArray.Length(); + m_keys.SetCapacity(numTags); + for (uint32_t tagIndex = 0; tagIndex < numTags; tagIndex++) { + nsAutoCString key; + tagArray[tagIndex]->GetKey(key); + m_keys.InsertElementAt(tagIndex, key); + } + return rv; +} diff --git a/comm/mailnews/base/src/nsMsgTagService.h b/comm/mailnews/base/src/nsMsgTagService.h new file mode 100644 index 0000000000..7eee648271 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgTagService.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgTagService_h__ +#define nsMsgTagService_h__ + +#include "nsIMsgTagService.h" +#include "nsIPrefBranch.h" +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsTArray.h" + +class nsMsgTag final : public nsIMsgTag { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGTAG + + nsMsgTag(const nsACString& aKey, const nsAString& aTag, + const nsACString& aColor, const nsACString& aOrdinal); + + protected: + ~nsMsgTag(); + + nsString mTag; + nsCString mKey, mColor, mOrdinal; +}; + +class nsMsgTagService final : public nsIMsgTagService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIMSGTAGSERVICE + + nsMsgTagService(); + + private: + ~nsMsgTagService(); + + protected: + nsresult SetUnicharPref(const char* prefName, const nsAString& prefValue); + nsresult GetUnicharPref(const char* prefName, nsAString& prefValue); + nsresult SetupLabelTags(); + nsresult RefreshKeyCache(); + + nsCOMPtr<nsIPrefBranch> m_tagPrefBranch; + nsTArray<nsCString> m_keys; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgThreadedDBView.cpp b/comm/mailnews/base/src/nsMsgThreadedDBView.cpp new file mode 100644 index 0000000000..84105bc4fc --- /dev/null +++ b/comm/mailnews/base/src/nsMsgThreadedDBView.cpp @@ -0,0 +1,899 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgThreadedDBView.h" +#include "nsIMsgHdr.h" +#include "nsIMsgThread.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgSearchSession.h" +#include "nsMsgMessageFlags.h" + +// Allocate this more to avoid reallocation on new mail. +#define MSGHDR_CACHE_LOOK_AHEAD_SIZE 25 +// Max msghdr cache entries. +#define MSGHDR_CACHE_MAX_SIZE 8192 +#define MSGHDR_CACHE_DEFAULT_SIZE 100 + +nsMsgThreadedDBView::nsMsgThreadedDBView() { + /* member initializers and constructor code */ + m_havePrevView = false; +} + +nsMsgThreadedDBView::~nsMsgThreadedDBView() {} /* destructor code */ + +NS_IMETHODIMP +nsMsgThreadedDBView::Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) { + nsresult rv = + nsMsgDBView::Open(folder, sortType, sortOrder, viewFlags, pCount); + NS_ENSURE_SUCCESS(rv, rv); + + if (!m_db) return NS_ERROR_NULL_POINTER; + + // Preset msg hdr cache size for performance reason. + int32_t totalMessages, unreadMessages; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + PersistFolderInfo(getter_AddRefs(dbFolderInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + // Save off sort type and order, view type and flags. + dbFolderInfo->GetNumUnreadMessages(&unreadMessages); + dbFolderInfo->GetNumMessages(&totalMessages); + if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly) { + // Set unread msg size + extra entries to avoid reallocation on new mail. + totalMessages = (uint32_t)unreadMessages + MSGHDR_CACHE_LOOK_AHEAD_SIZE; + } else { + if (totalMessages > MSGHDR_CACHE_MAX_SIZE) + // Use max default. + totalMessages = MSGHDR_CACHE_MAX_SIZE; + else if (totalMessages > 0) + // Allocate extra entries to avoid reallocation on new mail. + totalMessages += MSGHDR_CACHE_LOOK_AHEAD_SIZE; + } + + // If total messages is 0, then we probably don't have any idea how many + // headers are in the db so we have no business setting the cache size. + if (totalMessages > 0) m_db->SetMsgHdrCacheSize((uint32_t)totalMessages); + + int32_t count; + rv = InitThreadedView(count); + if (pCount) *pCount = count; + + // This is a hack, but we're trying to find a way to correct + // incorrect total and unread msg counts w/o paying a big + // performance penalty. So, if we're not threaded, just add + // up the total and unread messages in the view and see if that + // matches what the db totals say. Except ignored threads are + // going to throw us off...hmm. Unless we just look at the + // unread counts which is what mostly tweaks people anyway... + int32_t unreadMsgsInView = 0; + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + for (uint32_t i = m_flags.Length(); i > 0;) { + if (!(m_flags[--i] & nsMsgMessageFlags::Read)) ++unreadMsgsInView; + } + + if (unreadMessages != unreadMsgsInView) m_db->SyncCounts(); + } + + m_db->SetMsgHdrCacheSize(MSGHDR_CACHE_DEFAULT_SIZE); + + return rv; +} + +NS_IMETHODIMP +nsMsgThreadedDBView::Close() { return nsMsgDBView::Close(); } + +// Populate the view with the ids of the first message in each thread. +nsresult nsMsgThreadedDBView::InitThreadedView(int32_t& count) { + count = 0; + m_keys.Clear(); + m_flags.Clear(); + m_levels.Clear(); + m_prevKeys.Clear(); + m_prevFlags.Clear(); + m_prevLevels.Clear(); + m_havePrevView = false; + + bool unreadOnly = (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly); + + nsCOMPtr<nsIMsgThreadEnumerator> threads; + nsresult rv = m_db->EnumerateThreads(getter_AddRefs(threads)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore = false; + while (NS_SUCCEEDED(rv = threads->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsIMsgThread> threadHdr; + rv = threads->GetNext(getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t numChildren; + if (unreadOnly) + threadHdr->GetNumUnreadChildren(&numChildren); + else + threadHdr->GetNumChildren(&numChildren); + + if (numChildren == 0) { + continue; // An empty thread. + } + + nsCOMPtr<nsIMsgDBHdr> msgHdr; + if (unreadOnly) { + rv = threadHdr->GetFirstUnreadChild(getter_AddRefs(msgHdr)); + } else { + rv = threadHdr->GetRootHdr(getter_AddRefs(msgHdr)); + } + NS_ENSURE_SUCCESS(rv, rv); + + // Hook to allow derived classes to filter out unwanted threads. + if (!WantsThisThread(threadHdr)) { + continue; + } + + uint32_t msgFlags; + msgHdr->GetFlags(&msgFlags); + // Turn off high byte of msg flags - used for view flags. + msgFlags &= ~MSG_VIEW_FLAGS; + // Turn off these flags on msg hdr - they belong in thread. + uint32_t newMsgFlagsUnused; + msgHdr->AndFlags(~(nsMsgMessageFlags::Watched), &newMsgFlagsUnused); + AdjustReadFlag(msgHdr, &msgFlags); + // Try adding in MSG_VIEW_FLAG_ISTHREAD flag for unreadonly view. + uint32_t threadFlags; + threadHdr->GetFlags(&threadFlags); + msgFlags |= MSG_VIEW_FLAG_ISTHREAD | threadFlags; + if (numChildren > 1) { + msgFlags |= MSG_VIEW_FLAG_HASCHILDREN; + } + + if (!(m_viewFlags & nsMsgViewFlagsType::kShowIgnored)) { + // Skip ignored threads. + if (msgFlags & nsMsgMessageFlags::Ignored) { + continue; + } + // Skip ignored subthreads + bool killed; + msgHdr->GetIsKilled(&killed); + if (killed) { + continue; + } + } + + // By default, make threads collapsed unless we're only viewing new msgs. + if (msgFlags & MSG_VIEW_FLAG_HASCHILDREN) { + msgFlags |= nsMsgMessageFlags::Elided; + } + + // OK, now add it to the view! + nsMsgKey msgKey; + msgHdr->GetMessageKey(&msgKey); + m_keys.AppendElement(msgKey); + m_flags.AppendElement(msgFlags); + m_levels.AppendElement(0); + + // We expand as we build the view, which allows us to insert at the end + // of the key array, instead of the middle, and is much faster. + if ((!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) || + m_viewFlags & nsMsgViewFlagsType::kExpandAll) && + msgFlags & nsMsgMessageFlags::Elided) { + ExpandByIndex(m_keys.Length() - 1, nullptr); + } + + count++; + } + + rv = InitSort(m_sortType, m_sortOrder); + SaveSortInfo(m_sortType, m_sortOrder); + return rv; +} + +nsresult nsMsgThreadedDBView::SortThreads(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) { + NS_ASSERTION(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay, + "trying to sort unthreaded threads"); + + uint32_t numThreads = 0; + // The idea here is that copy the current view, then build up an m_keys and + // m_flags array of just the top level messages in the view, and then call + // nsMsgDBView::Sort(sortType, sortOrder). + // Then, we expand the threads in the result array that were expanded in the + // original view (perhaps by copying from the original view, but more likely + // just be calling expand). + for (uint32_t i = 0; i < m_keys.Length(); i++) { + if (m_flags[i] & MSG_VIEW_FLAG_ISTHREAD) { + if (numThreads < i) { + m_keys[numThreads] = m_keys[i]; + m_flags[numThreads] = m_flags[i]; + } + + m_levels[numThreads] = 0; + numThreads++; + } + } + + m_keys.SetLength(numThreads); + m_flags.SetLength(numThreads); + m_levels.SetLength(numThreads); + // m_viewFlags &= ~nsMsgViewFlagsType::kThreadedDisplay; + m_sortType = nsMsgViewSortType::byNone; // sort from scratch + nsMsgDBView::Sort(sortType, sortOrder); + m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay; + SetSuppressChangeNotifications(true); + + // Loop through the original array, for each thread that's expanded, + // find it in the new array and expand the thread. We have to update + // MSG_VIEW_FLAG_HAS_CHILDREN because we may be going from a flat sort, + // which doesn't maintain that flag, to a threaded sort, which requires + // that flag. + for (uint32_t j = 0; j < m_keys.Length(); j++) { + uint32_t flags = m_flags[j]; + if ((flags & (MSG_VIEW_FLAG_HASCHILDREN | nsMsgMessageFlags::Elided)) == + MSG_VIEW_FLAG_HASCHILDREN) { + uint32_t numExpanded; + m_flags[j] = flags | nsMsgMessageFlags::Elided; + ExpandByIndex(j, &numExpanded); + j += numExpanded; + if (numExpanded > 0) + m_flags[j - numExpanded] = flags | MSG_VIEW_FLAG_HASCHILDREN; + } else if (flags & MSG_VIEW_FLAG_ISTHREAD && + !(flags & MSG_VIEW_FLAG_HASCHILDREN)) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + nsCOMPtr<nsIMsgThread> pThread; + m_db->GetMsgHdrForKey(m_keys[j], getter_AddRefs(msgHdr)); + if (msgHdr) { + m_db->GetThreadContainingMsgHdr(msgHdr, getter_AddRefs(pThread)); + if (pThread) { + uint32_t numChildren; + pThread->GetNumChildren(&numChildren); + if (numChildren > 1) + m_flags[j] = + flags | MSG_VIEW_FLAG_HASCHILDREN | nsMsgMessageFlags::Elided; + } + } + } + } + + SetSuppressChangeNotifications(false); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgThreadedDBView::Sort(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) { + nsresult rv; + + int32_t rowCountBeforeSort = GetSize(); + + if (!rowCountBeforeSort) { + // Still need to setup our flags even when no articles - bug 98183. + m_sortType = sortType; + m_sortOrder = sortOrder; + if (sortType == nsMsgViewSortType::byThread && + !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + SetViewFlags(m_viewFlags | nsMsgViewFlagsType::kThreadedDisplay); + } + + SaveSortInfo(sortType, sortOrder); + return NS_OK; + } + + if (!m_checkedCustomColumns && CustomColumnsInSortAndNotRegistered()) + return NS_OK; + + // Sort threads by sort order. + bool sortThreads = m_viewFlags & (nsMsgViewFlagsType::kThreadedDisplay | + nsMsgViewFlagsType::kGroupBySort); + + // If sort type is by thread, and we're already threaded, change sort type + // to byId. + if (sortType == nsMsgViewSortType::byThread && + (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) != 0) { + sortType = nsMsgViewSortType::byId; + } + + nsMsgKey preservedKey; + AutoTArray<nsMsgKey, 1> preservedSelection; + SaveAndClearSelection(&preservedKey, preservedSelection); + // If the client wants us to forget our cached id arrays, they + // should build a new view. If this isn't good enough, we + // need a method to do that. + if (sortType != m_sortType || !m_sortValid || sortThreads) { + SaveSortInfo(sortType, sortOrder); + if (sortType == nsMsgViewSortType::byThread) { + m_sortType = sortType; + m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay; + m_viewFlags &= ~nsMsgViewFlagsType::kGroupBySort; + if (m_havePrevView) { + // Restore saved id array and flags array. + m_keys = m_prevKeys.Clone(); + m_flags = m_prevFlags.Clone(); + m_levels = m_prevLevels.Clone(); + m_sortValid = true; + + // The sort may have changed the number of rows + // before we restore the selection, tell the tree + // do this before we call restore selection + // this is safe when there is no selection. + rv = AdjustRowCount(rowCountBeforeSort, GetSize()); + + RestoreSelection(preservedKey, preservedSelection); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + + return NS_OK; + } else { + // Set sort info in anticipation of what Init will do. + // Build up thread list. + int32_t unused; // count. + InitThreadedView(unused); + if (sortOrder != nsMsgViewSortOrder::ascending) + Sort(sortType, sortOrder); + + // The sort may have changed the number of rows + // before we update the selection, tell the tree + // do this before we call restore selection + // this is safe when there is no selection. + rv = AdjustRowCount(rowCountBeforeSort, GetSize()); + + RestoreSelection(preservedKey, preservedSelection); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + + return NS_OK; + } + } else if (sortType != nsMsgViewSortType::byThread && + (m_sortType == nsMsgViewSortType::byThread || sortThreads) + /* && !m_havePrevView*/) { + if (sortThreads) { + SortThreads(sortType, sortOrder); + // Hack so base class won't do anything. + sortType = nsMsgViewSortType::byThread; + } else { + // Going from SortByThread to non-thread sort - must build new key, + // level, and flags arrays. + m_prevKeys = m_keys.Clone(); + m_prevFlags = m_flags.Clone(); + m_prevLevels = m_levels.Clone(); + // Do this before we sort, so that we'll use the cheap method + // of expanding. + m_viewFlags &= ~(nsMsgViewFlagsType::kThreadedDisplay | + nsMsgViewFlagsType::kGroupBySort); + ExpandAll(); + // m_idArray.RemoveAll(); + // m_flags.Clear(); + m_havePrevView = true; + } + } + } else if (m_sortOrder != sortOrder) { + // Check for toggling the sort. + nsMsgDBView::Sort(sortType, sortOrder); + } + + if (!sortThreads) { + // Call the base class in case we're not sorting by thread. + rv = nsMsgDBView::Sort(sortType, sortOrder); + NS_ENSURE_SUCCESS(rv, rv); + SaveSortInfo(sortType, sortOrder); + } + + // The sort may have changed the number of rows + // before we restore the selection, tell the tree + // do this before we call restore selection + // this is safe when there is no selection. + rv = AdjustRowCount(rowCountBeforeSort, GetSize()); + + RestoreSelection(preservedKey, preservedSelection); + if (mTree) mTree->Invalidate(); + if (mJSTree) mJSTree->Invalidate(); + + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; +} + +void nsMsgThreadedDBView::OnExtraFlagChanged(nsMsgViewIndex index, + uint32_t extraFlag) { + if (IsValidIndex(index)) { + if (m_havePrevView) { + nsMsgKey keyChanged = m_keys[index]; + nsMsgViewIndex prevViewIndex = m_prevKeys.IndexOf(keyChanged); + if (prevViewIndex != nsMsgViewIndex_None) { + uint32_t prevFlag = m_prevFlags[prevViewIndex]; + // Don't want to change the elided bit, or has children or is thread. + if (prevFlag & nsMsgMessageFlags::Elided) + extraFlag |= nsMsgMessageFlags::Elided; + else + extraFlag &= ~nsMsgMessageFlags::Elided; + + if (prevFlag & MSG_VIEW_FLAG_ISTHREAD) + extraFlag |= MSG_VIEW_FLAG_ISTHREAD; + else + extraFlag &= ~MSG_VIEW_FLAG_ISTHREAD; + + if (prevFlag & MSG_VIEW_FLAG_HASCHILDREN) + extraFlag |= MSG_VIEW_FLAG_HASCHILDREN; + else + extraFlag &= ~MSG_VIEW_FLAG_HASCHILDREN; + + // Will this be right? + m_prevFlags[prevViewIndex] = extraFlag; + } + } + } + + // We don't really know what's changed, but to be on the safe side, set the + // sort invalid so that reverse sort will pick it up. + if (m_sortType == nsMsgViewSortType::byStatus || + m_sortType == nsMsgViewSortType::byFlagged || + m_sortType == nsMsgViewSortType::byUnread || + m_sortType == nsMsgViewSortType::byPriority) { + m_sortValid = false; + } +} + +void nsMsgThreadedDBView::OnHeaderAddedOrDeleted() { ClearPrevIdArray(); } + +void nsMsgThreadedDBView::ClearPrevIdArray() { + m_prevKeys.Clear(); + m_prevLevels.Clear(); + m_prevFlags.Clear(); + m_havePrevView = false; +} + +nsresult nsMsgThreadedDBView::InitSort(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) { + // Nothing to do. + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) return NS_OK; + + if (sortType == nsMsgViewSortType::byThread) { + // Sort top level threads by id. + nsMsgDBView::Sort(nsMsgViewSortType::byId, sortOrder); + m_sortType = nsMsgViewSortType::byThread; + m_viewFlags |= nsMsgViewFlagsType::kThreadedDisplay; + m_viewFlags &= ~nsMsgViewFlagsType::kGroupBySort; + // Persist the view flags. + SetViewFlags(m_viewFlags); + // m_db->SetSortInfo(m_sortType, sortOrder); + } + // else + // m_viewFlags &= ~nsMsgViewFlagsType::kThreadedDisplay; + + // By default, the unread only view should have all threads expanded. + if ((m_viewFlags & + (nsMsgViewFlagsType::kUnreadOnly | nsMsgViewFlagsType::kExpandAll)) && + (m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + ExpandAll(); + } + + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + // For now, expand all and do a flat sort. + ExpandAll(); + } + + Sort(sortType, sortOrder); + if (sortType != nsMsgViewSortType::byThread) { + // Forget prev view, since it has everything expanded. + ClearPrevIdArray(); + } + + return NS_OK; +} + +nsresult nsMsgThreadedDBView::OnNewHeader(nsIMsgDBHdr* newHdr, + nsMsgKey aParentKey, + bool ensureListed) { + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) + return nsMsgGroupView::OnNewHeader(newHdr, aParentKey, ensureListed); + + NS_ENSURE_TRUE(newHdr, NS_MSG_MESSAGE_NOT_FOUND); + + nsMsgKey newKey; + newHdr->GetMessageKey(&newKey); + + // Views can override this behaviour, which is to append to view. + // This is the mail behaviour, but threaded views want + // to insert in order... + uint32_t msgFlags; + newHdr->GetFlags(&msgFlags); + if (m_viewFlags & nsMsgViewFlagsType::kUnreadOnly && !ensureListed && + msgFlags & nsMsgMessageFlags::Read) { + return NS_OK; + } + + // Currently, we only add the header in a threaded view if it's a thread. + // We used to check if this was the first header in the thread, but that's + // a bit harder in the unreadOnly view. But we'll catch it below. + + // If not threaded display just add it to the view. + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) + return AddHdr(newHdr); + + // Need to find the thread we added this to so we can change the hasnew flag + // added message to existing thread, but not to view. + // Fix flags on thread header. + int32_t threadCount; + uint32_t threadFlags; + bool moveThread = false; + nsMsgViewIndex threadIndex = + ThreadIndexOfMsg(newKey, nsMsgViewIndex_None, &threadCount, &threadFlags); + bool threadRootIsDisplayed = false; + + nsCOMPtr<nsIMsgThread> threadHdr; + m_db->GetThreadContainingMsgHdr(newHdr, getter_AddRefs(threadHdr)); + if (threadHdr && m_sortType == nsMsgViewSortType::byDate) { + uint32_t newestMsgInThread = 0, msgDate = 0; + threadHdr->GetNewestMsgDate(&newestMsgInThread); + newHdr->GetDateInSeconds(&msgDate); + moveThread = (msgDate == newestMsgInThread); + } + + if (threadIndex != nsMsgViewIndex_None) { + threadRootIsDisplayed = (m_currentlyDisplayedViewIndex == threadIndex); + uint32_t flags = m_flags[threadIndex]; + if (!(flags & MSG_VIEW_FLAG_HASCHILDREN)) { + flags |= MSG_VIEW_FLAG_HASCHILDREN | MSG_VIEW_FLAG_ISTHREAD; + if (!(m_viewFlags & nsMsgViewFlagsType::kUnreadOnly)) + flags |= nsMsgMessageFlags::Elided; + + m_flags[threadIndex] = flags; + } + + if (!(flags & nsMsgMessageFlags::Elided)) { + // Thread is expanded. + // Insert child into thread. + // Levels of other hdrs may have changed! + uint32_t newFlags = msgFlags; + int32_t level = 0; + nsMsgViewIndex insertIndex = threadIndex; + if (aParentKey == nsMsgKey_None) { + newFlags |= MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN; + } else { + nsMsgViewIndex parentIndex = + FindParentInThread(aParentKey, threadIndex); + level = m_levels[parentIndex] + 1; + insertIndex = GetInsertInfoForNewHdr(newHdr, parentIndex, level); + } + + InsertMsgHdrAt(insertIndex, newHdr, newKey, newFlags, level); + // The call to NoteChange() has to happen after we add the key as + // NoteChange() will call RowCountChanged() which will call our + // GetRowCount(). + NoteChange(insertIndex, 1, nsMsgViewNotificationCode::insertOrDelete); + + if (aParentKey == nsMsgKey_None) { + // this header is the new king! try collapsing the existing thread, + // removing it, installing this header as king, and expanding it. + CollapseByIndex(threadIndex, nullptr); + // call base class, so child won't get promoted. + // nsMsgDBView::RemoveByIndex(threadIndex); + ExpandByIndex(threadIndex, nullptr); + } + } else if (aParentKey == nsMsgKey_None) { + // if we have a collapsed thread which just got a new + // top of thread, change the keys array. + m_keys[threadIndex] = newKey; + } + + // If this message is new, the thread is collapsed, it is the + // root and it was displayed, expand it so that the user does + // not find that their message has magically turned into a summary. + if (msgFlags & nsMsgMessageFlags::New && + m_flags[threadIndex] & nsMsgMessageFlags::Elided && + threadRootIsDisplayed) + ExpandByIndex(threadIndex, nullptr); + + if (moveThread) + MoveThreadAt(threadIndex); + else + // note change, to update the parent thread's unread and total counts + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + } else if (threadHdr) { + // Adding msg to thread that's not in view. + AddMsgToThreadNotInView(threadHdr, newHdr, ensureListed); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgThreadedDBView::OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent, + nsMsgKey newParent, + nsIDBChangeListener* aInstigator) { + // We need to adjust the level of the hdr whose parent changed, and + // invalidate that row, iff we're in threaded mode. +#if 0 + // This code never runs due to the if (false) and Clang complains about it + // so it is ifdefed out for now. + if (false && m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay) + { + nsMsgViewIndex childIndex = FindViewIndex(aKeyChanged); + if (childIndex != nsMsgViewIndex_None) + { + nsMsgViewIndex parentIndex = FindViewIndex(newParent); + int32_t newParentLevel = + (parentIndex == nsMsgViewIndex_None) ? -1 : m_levels[parentIndex]; + + nsMsgViewIndex oldParentIndex = FindViewIndex(oldParent); + + int32_t oldParentLevel = + (oldParentIndex != nsMsgViewIndex_None || + newParent == nsMsgKey_None) ? m_levels[oldParentIndex] : -1 ; + + int32_t levelChanged = m_levels[childIndex]; + int32_t parentDelta = oldParentLevel - newParentLevel; + m_levels[childIndex] = (newParent == nsMsgKey_None) ? 0 : newParentLevel + 1; + if (parentDelta > 0) + { + for (nsMsgViewIndex viewIndex = childIndex + 1; + viewIndex < GetSize() && m_levels[viewIndex] > levelChanged; + viewIndex++) + { + m_levels[viewIndex] = m_levels[viewIndex] - parentDelta; + NoteChange(viewIndex, 1, nsMsgViewNotificationCode::changed); + } + } + + NoteChange(childIndex, 1, nsMsgViewNotificationCode::changed); + } + } +#endif + return NS_OK; +} + +nsMsgViewIndex nsMsgThreadedDBView::GetInsertInfoForNewHdr( + nsIMsgDBHdr* newHdr, nsMsgViewIndex parentIndex, int32_t targetLevel) { + uint32_t viewSize = GetSize(); + while (++parentIndex < viewSize) { + // Loop until we find a message at a level less than or equal to the + // parent level + if (m_levels[parentIndex] < targetLevel) break; + } + + return parentIndex; +} + +// This method removes the thread at threadIndex from the view +// and puts it back in its new position, determined by the sort order. +// And, if the selection is affected, save and restore the selection. +void nsMsgThreadedDBView::MoveThreadAt(nsMsgViewIndex threadIndex) { + // We need to check if the thread is collapsed or not... + // We want to turn off tree notifications so that we don't + // reload the current message. + // We also need to invalidate the range between where the thread was + // and where it ended up. + bool changesDisabled = mSuppressChangeNotification; + if (!changesDisabled) SetSuppressChangeNotifications(true); + + nsCOMPtr<nsIMsgDBHdr> threadHdr; + + GetMsgHdrForViewIndex(threadIndex, getter_AddRefs(threadHdr)); + int32_t childCount = 0; + + nsMsgKey preservedKey; + AutoTArray<nsMsgKey, 1> preservedSelection; + int32_t selectionCount; + int32_t currentIndex; + bool hasSelection = + mTreeSelection && + ((NS_SUCCEEDED(mTreeSelection->GetCurrentIndex(¤tIndex)) && + currentIndex >= 0 && (uint32_t)currentIndex < GetSize()) || + (NS_SUCCEEDED(mTreeSelection->GetRangeCount(&selectionCount)) && + selectionCount > 0)); + if (hasSelection) SaveAndClearSelection(&preservedKey, preservedSelection); + + uint32_t saveFlags = m_flags[threadIndex]; + bool threadIsExpanded = !(saveFlags & nsMsgMessageFlags::Elided); + + if (threadIsExpanded) { + ExpansionDelta(threadIndex, &childCount); + childCount = -childCount; + } + + nsTArray<nsMsgKey> threadKeys; + nsTArray<uint32_t> threadFlags; + nsTArray<uint8_t> threadLevels; + + if (threadIsExpanded) { + threadKeys.SetCapacity(childCount); + threadFlags.SetCapacity(childCount); + threadLevels.SetCapacity(childCount); + for (nsMsgViewIndex index = threadIndex + 1; + index < GetSize() && m_levels[index]; index++) { + threadKeys.AppendElement(m_keys[index]); + threadFlags.AppendElement(m_flags[index]); + threadLevels.AppendElement(m_levels[index]); + } + + uint32_t collapseCount; + CollapseByIndex(threadIndex, &collapseCount); + } + + nsMsgDBView::RemoveByIndex(threadIndex); + nsMsgViewIndex newIndex = nsMsgViewIndex_None; + AddHdr(threadHdr, &newIndex); + + // AddHdr doesn't always set newIndex, and getting it to do so + // is going to require some refactoring. + if (newIndex == nsMsgViewIndex_None) newIndex = FindHdr(threadHdr); + + if (threadIsExpanded) { + m_keys.InsertElementsAt(newIndex + 1, threadKeys); + m_flags.InsertElementsAt(newIndex + 1, threadFlags); + m_levels.InsertElementsAt(newIndex + 1, threadLevels); + } + + if (newIndex == nsMsgViewIndex_None) { + NS_WARNING("newIndex=-1 in MoveThreadAt"); + newIndex = 0; + } + + m_flags[newIndex] = saveFlags; + // Unfreeze selection. + if (hasSelection) RestoreSelection(preservedKey, preservedSelection); + + if (!changesDisabled) SetSuppressChangeNotifications(false); + + nsMsgViewIndex lowIndex = threadIndex < newIndex ? threadIndex : newIndex; + nsMsgViewIndex highIndex = lowIndex == threadIndex ? newIndex : threadIndex; + + NoteChange(lowIndex, highIndex - lowIndex + childCount + 1, + nsMsgViewNotificationCode::changed); +} + +nsresult nsMsgThreadedDBView::AddMsgToThreadNotInView(nsIMsgThread* threadHdr, + nsIMsgDBHdr* msgHdr, + bool ensureListed) { + nsresult rv = NS_OK; + uint32_t threadFlags; + threadHdr->GetFlags(&threadFlags); + if (!(threadFlags & nsMsgMessageFlags::Ignored)) { + bool msgKilled; + msgHdr->GetIsKilled(&msgKilled); + if (!msgKilled) rv = nsMsgDBView::AddHdr(msgHdr); + } + + return rv; +} + +// This method just removes the specified line from the view. It does +// NOT delete it from the database. +nsresult nsMsgThreadedDBView::RemoveByIndex(nsMsgViewIndex index) { + nsresult rv = NS_OK; + int32_t flags; + + if (!IsValidIndex(index)) return NS_MSG_INVALID_DBVIEW_INDEX; + + OnHeaderAddedOrDeleted(); + + flags = m_flags[index]; + + if (!(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) + return nsMsgDBView::RemoveByIndex(index); + + nsCOMPtr<nsIMsgThread> threadHdr; + GetThreadContainingIndex(index, getter_AddRefs(threadHdr)); + NS_ENSURE_SUCCESS(rv, rv); + uint32_t numThreadChildren = 0; + // If we can't get a thread, it's already deleted and thus has 0 children. + if (threadHdr) threadHdr->GetNumChildren(&numThreadChildren); + + // Check if we're the top level msg in the thread, and we're not collapsed. + if ((flags & MSG_VIEW_FLAG_ISTHREAD) && + !(flags & nsMsgMessageFlags::Elided) && + (flags & MSG_VIEW_FLAG_HASCHILDREN)) { + // Fix flags on thread header - newly promoted message should have + // flags set correctly. + if (threadHdr) { + nsMsgDBView::RemoveByIndex(index); + nsCOMPtr<nsIMsgThread> nextThreadHdr; + // Above RemoveByIndex may now make index out of bounds. + if (IsValidIndex(index) && numThreadChildren > 0) { + // unreadOnly + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(msgHdr)); + if (msgHdr != nullptr) { + uint32_t flag = 0; + msgHdr->GetFlags(&flag); + if (numThreadChildren > 1) + flag |= MSG_VIEW_FLAG_ISTHREAD | MSG_VIEW_FLAG_HASCHILDREN; + + m_flags[index] = flag; + m_levels[index] = 0; + } + } + } + + return rv; + } else if (!(flags & MSG_VIEW_FLAG_ISTHREAD)) { + // We're not deleting the top level msg, but top level msg might be the + // only msg in thread now. + if (threadHdr && numThreadChildren == 1) { + nsMsgKey msgKey; + rv = threadHdr->GetChildKeyAt(0, &msgKey); + if (NS_SUCCEEDED(rv)) { + nsMsgViewIndex threadIndex = FindViewIndex(msgKey); + if (IsValidIndex(threadIndex)) { + uint32_t flags = m_flags[threadIndex]; + flags &= ~(MSG_VIEW_FLAG_ISTHREAD | nsMsgMessageFlags::Elided | + MSG_VIEW_FLAG_HASCHILDREN); + m_flags[threadIndex] = flags; + NoteChange(threadIndex, 1, nsMsgViewNotificationCode::changed); + } + } + } + + return nsMsgDBView::RemoveByIndex(index); + } + + // Deleting collapsed thread header is special case. Child will be promoted, + // so just tell FE that line changed, not that it was deleted. + // Header has already been deleted from thread. + if (threadHdr && numThreadChildren > 0) { + // Change the id array and flags array to reflect the child header. + // If we're not deleting the header, we want the second header, + // Otherwise, the first one (which just got promoted). + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = threadHdr->GetChildHdrAt(0, getter_AddRefs(msgHdr)); + if (msgHdr != nullptr) { + msgHdr->GetMessageKey(&m_keys[index]); + uint32_t flag = 0; + msgHdr->GetFlags(&flag); + flag |= MSG_VIEW_FLAG_ISTHREAD; + + // If only hdr in thread (with one about to be deleted). + if (numThreadChildren == 1) { + // Adjust flags. + flag &= ~MSG_VIEW_FLAG_HASCHILDREN; + flag &= ~nsMsgMessageFlags::Elided; + // Tell FE that thread header needs to be repainted. + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + } else { + flag |= MSG_VIEW_FLAG_HASCHILDREN; + flag |= nsMsgMessageFlags::Elided; + } + + m_flags[index] = flag; + mIndicesToNoteChange.RemoveElement(index); + } else { + NS_ASSERTION(false, "couldn't find thread child"); + } + + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + } else { + // We may have deleted a whole, collapsed thread - if so, + // ensure that the current index will be noted as changed. + if (!mIndicesToNoteChange.Contains(index)) + mIndicesToNoteChange.AppendElement(index); + + rv = nsMsgDBView::RemoveByIndex(index); + } + + return rv; +} + +NS_IMETHODIMP +nsMsgThreadedDBView::GetViewType(nsMsgViewTypeValue* aViewType) { + NS_ENSURE_ARG_POINTER(aViewType); + *aViewType = nsMsgViewType::eShowAllThreads; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgThreadedDBView::CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, + nsIMsgDBView** _retval) { + nsMsgThreadedDBView* newMsgDBView = new nsMsgThreadedDBView(); + + if (!newMsgDBView) return NS_ERROR_OUT_OF_MEMORY; + + nsresult rv = + CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*_retval = newMsgDBView); + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsMsgThreadedDBView.h b/comm/mailnews/base/src/nsMsgThreadedDBView.h new file mode 100644 index 0000000000..a33c98a3bf --- /dev/null +++ b/comm/mailnews/base/src/nsMsgThreadedDBView.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgThreadedDBView_H_ +#define _nsMsgThreadedDBView_H_ + +#include "mozilla/Attributes.h" +#include "nsMsgGroupView.h" + +class nsMsgThreadedDBView : public nsMsgGroupView { + public: + nsMsgThreadedDBView(); + virtual ~nsMsgThreadedDBView(); + + NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override; + NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCommandUpdater, + nsIMsgDBView** _retval) override; + NS_IMETHOD Close() override; + NS_IMETHOD Sort(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder) override; + NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override; + NS_IMETHOD OnParentChanged(nsMsgKey aKeyChanged, nsMsgKey oldParent, + nsMsgKey newParent, + nsIDBChangeListener* aInstigator) override; + + protected: + virtual const char* GetViewName(void) override { return "ThreadedDBView"; } + nsresult InitThreadedView(int32_t& count); + virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey aParentKey, + bool ensureListed) override; + virtual nsresult AddMsgToThreadNotInView(nsIMsgThread* threadHdr, + nsIMsgDBHdr* msgHdr, + bool ensureListed); + nsresult InitSort(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder); + virtual nsresult SortThreads(nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder); + virtual void OnExtraFlagChanged(nsMsgViewIndex index, + uint32_t extraFlag) override; + virtual void OnHeaderAddedOrDeleted() override; + void ClearPrevIdArray(); + virtual nsresult RemoveByIndex(nsMsgViewIndex index) override; + nsMsgViewIndex GetInsertInfoForNewHdr(nsIMsgDBHdr* newHdr, + nsMsgViewIndex threadIndex, + int32_t targetLevel); + void MoveThreadAt(nsMsgViewIndex threadIndex); + + // these are used to save off the previous view so that bopping back and forth + // between two views is quick (e.g., threaded and flat sorted by date). + bool m_havePrevView; + nsTArray<nsMsgKey> m_prevKeys; // this is used for caching non-threaded view. + nsTArray<uint32_t> m_prevFlags; + nsTArray<uint8_t> m_prevLevels; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgTxn.cpp b/comm/mailnews/base/src/nsMsgTxn.cpp new file mode 100644 index 0000000000..0074fc1960 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgTxn.cpp @@ -0,0 +1,247 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgTxn.h" +#include "nsIMsgHdr.h" +#include "nsIMsgDatabase.h" +#include "nsCOMArray.h" +#include "nsArrayEnumerator.h" +#include "nsComponentManagerUtils.h" +#include "nsVariant.h" +#include "nsIProperty.h" +#include "nsMsgMessageFlags.h" +#include "nsIMsgFolder.h" + +NS_IMPL_ADDREF(nsMsgTxn) +NS_IMPL_RELEASE(nsMsgTxn) +NS_INTERFACE_MAP_BEGIN(nsMsgTxn) + NS_INTERFACE_MAP_ENTRY(nsIWritablePropertyBag) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsIPropertyBag, nsIWritablePropertyBag) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWritablePropertyBag) + NS_INTERFACE_MAP_ENTRY(nsITransaction) + NS_INTERFACE_MAP_ENTRY(nsIPropertyBag2) + NS_INTERFACE_MAP_ENTRY(nsIWritablePropertyBag2) +NS_INTERFACE_MAP_END + +nsMsgTxn::nsMsgTxn() { m_txnType = 0; } + +nsMsgTxn::~nsMsgTxn() {} + +nsresult nsMsgTxn::Init() { return NS_OK; } + +NS_IMETHODIMP nsMsgTxn::HasKey(const nsAString& name, bool* aResult) { + *aResult = mPropertyHash.Get(name, nullptr); + return NS_OK; +} + +NS_IMETHODIMP nsMsgTxn::Get(const nsAString& name, nsIVariant** _retval) { + mPropertyHash.Get(name, _retval); + return NS_OK; +} + +NS_IMETHODIMP nsMsgTxn::GetProperty(const nsAString& name, + nsIVariant** _retval) { + return mPropertyHash.Get(name, _retval) ? NS_OK : NS_ERROR_FAILURE; +} + +NS_IMETHODIMP nsMsgTxn::SetProperty(const nsAString& name, nsIVariant* value) { + NS_ENSURE_ARG_POINTER(value); + mPropertyHash.InsertOrUpdate(name, value); + return NS_OK; +} + +NS_IMETHODIMP nsMsgTxn::DeleteProperty(const nsAString& name) { + if (!mPropertyHash.Get(name, nullptr)) return NS_ERROR_FAILURE; + + mPropertyHash.Remove(name); + return mPropertyHash.Get(name, nullptr) ? NS_ERROR_FAILURE : NS_OK; +} + +// +// nsMailSimpleProperty class and impl; used for GetEnumerator +// This is same as nsSimpleProperty but for external API use. +// + +class nsMailSimpleProperty final : public nsIProperty { + public: + nsMailSimpleProperty(const nsAString& aName, nsIVariant* aValue) + : mName(aName), mValue(aValue) {} + + NS_DECL_ISUPPORTS + NS_DECL_NSIPROPERTY + protected: + ~nsMailSimpleProperty() {} + + nsString mName; + nsCOMPtr<nsIVariant> mValue; +}; + +NS_IMPL_ISUPPORTS(nsMailSimpleProperty, nsIProperty) + +NS_IMETHODIMP nsMailSimpleProperty::GetName(nsAString& aName) { + aName.Assign(mName); + return NS_OK; +} + +NS_IMETHODIMP nsMailSimpleProperty::GetValue(nsIVariant** aValue) { + NS_IF_ADDREF(*aValue = mValue); + return NS_OK; +} + +// end nsMailSimpleProperty + +NS_IMETHODIMP nsMsgTxn::GetEnumerator(nsISimpleEnumerator** _retval) { + nsCOMArray<nsIProperty> propertyArray; + for (auto iter = mPropertyHash.Iter(); !iter.Done(); iter.Next()) { + nsMailSimpleProperty* sprop = + new nsMailSimpleProperty(iter.Key(), iter.Data()); + propertyArray.AppendObject(sprop); + } + return NS_NewArrayEnumerator(_retval, propertyArray, NS_GET_IID(nsIProperty)); +} + +#define IMPL_GETSETPROPERTY_AS(Name, Type) \ + NS_IMETHODIMP \ + nsMsgTxn::GetPropertyAs##Name(const nsAString& prop, Type* _retval) { \ + nsIVariant* v = mPropertyHash.GetWeak(prop); \ + if (!v) return NS_ERROR_NOT_AVAILABLE; \ + return v->GetAs##Name(_retval); \ + } \ + \ + NS_IMETHODIMP \ + nsMsgTxn::SetPropertyAs##Name(const nsAString& prop, Type value) { \ + nsCOMPtr<nsIWritableVariant> var = new nsVariant(); \ + var->SetAs##Name(value); \ + return SetProperty(prop, var); \ + } + +IMPL_GETSETPROPERTY_AS(Int32, int32_t) +IMPL_GETSETPROPERTY_AS(Uint32, uint32_t) +IMPL_GETSETPROPERTY_AS(Int64, int64_t) +IMPL_GETSETPROPERTY_AS(Uint64, uint64_t) +IMPL_GETSETPROPERTY_AS(Double, double) +IMPL_GETSETPROPERTY_AS(Bool, bool) + +NS_IMETHODIMP nsMsgTxn::GetPropertyAsAString(const nsAString& prop, + nsAString& _retval) { + nsIVariant* v = mPropertyHash.GetWeak(prop); + if (!v) return NS_ERROR_NOT_AVAILABLE; + return v->GetAsAString(_retval); +} + +NS_IMETHODIMP nsMsgTxn::GetPropertyAsACString(const nsAString& prop, + nsACString& _retval) { + nsIVariant* v = mPropertyHash.GetWeak(prop); + if (!v) return NS_ERROR_NOT_AVAILABLE; + return v->GetAsACString(_retval); +} + +NS_IMETHODIMP nsMsgTxn::GetPropertyAsAUTF8String(const nsAString& prop, + nsACString& _retval) { + nsIVariant* v = mPropertyHash.GetWeak(prop); + if (!v) return NS_ERROR_NOT_AVAILABLE; + return v->GetAsAUTF8String(_retval); +} + +NS_IMETHODIMP nsMsgTxn::GetPropertyAsInterface(const nsAString& prop, + const nsIID& aIID, + void** _retval) { + nsIVariant* v = mPropertyHash.GetWeak(prop); + if (!v) return NS_ERROR_NOT_AVAILABLE; + nsCOMPtr<nsISupports> val; + nsresult rv = v->GetAsISupports(getter_AddRefs(val)); + if (NS_FAILED(rv)) return rv; + if (!val) { + // We have a value, but it's null + *_retval = nullptr; + return NS_OK; + } + return val->QueryInterface(aIID, _retval); +} + +NS_IMETHODIMP nsMsgTxn::SetPropertyAsAString(const nsAString& prop, + const nsAString& value) { + nsCOMPtr<nsIWritableVariant> var = new nsVariant(); + var->SetAsAString(value); + return SetProperty(prop, var); +} + +NS_IMETHODIMP nsMsgTxn::SetPropertyAsACString(const nsAString& prop, + const nsACString& value) { + nsCOMPtr<nsIWritableVariant> var = new nsVariant(); + var->SetAsACString(value); + return SetProperty(prop, var); +} + +NS_IMETHODIMP nsMsgTxn::SetPropertyAsAUTF8String(const nsAString& prop, + const nsACString& value) { + nsCOMPtr<nsIWritableVariant> var = new nsVariant(); + var->SetAsAUTF8String(value); + return SetProperty(prop, var); +} + +NS_IMETHODIMP nsMsgTxn::SetPropertyAsInterface(const nsAString& prop, + nsISupports* value) { + nsCOMPtr<nsIWritableVariant> var = new nsVariant(); + var->SetAsISupports(value); + return SetProperty(prop, var); +} + +/////////////////////// Transaction Stuff ////////////////// +NS_IMETHODIMP nsMsgTxn::DoTransaction(void) { return NS_OK; } + +NS_IMETHODIMP nsMsgTxn::GetIsTransient(bool* aIsTransient) { + if (nullptr != aIsTransient) + *aIsTransient = false; + else + return NS_ERROR_NULL_POINTER; + return NS_OK; +} + +NS_IMETHODIMP nsMsgTxn::Merge(nsITransaction* aTransaction, bool* aDidMerge) { + *aDidMerge = false; + return NS_OK; +} + +nsresult nsMsgTxn::GetMsgWindow(nsIMsgWindow** msgWindow) { + if (!msgWindow || !m_msgWindow) return NS_ERROR_NULL_POINTER; + NS_ADDREF(*msgWindow = m_msgWindow); + return NS_OK; +} + +nsresult nsMsgTxn::SetMsgWindow(nsIMsgWindow* msgWindow) { + m_msgWindow = msgWindow; + return NS_OK; +} + +nsresult nsMsgTxn::SetTransactionType(uint32_t txnType) { + return SetPropertyAsUint32(u"type"_ns, txnType); +} + +/*none of the callers pass null aFolder, + we always initialize aResult (before we pass in) for the case where the key is + not in the db*/ +nsresult nsMsgTxn::CheckForToggleDelete(nsIMsgFolder* aFolder, + const nsMsgKey& aMsgKey, + bool* aResult) { + NS_ENSURE_ARG(aResult); + nsCOMPtr<nsIMsgDBHdr> message; + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = aFolder->GetMsgDatabase(getter_AddRefs(db)); + if (db) { + bool containsKey; + rv = db->ContainsKey(aMsgKey, &containsKey); + if (NS_FAILED(rv) || !containsKey) // the message has been deleted from db, + // so we cannot do toggle here + return NS_OK; + rv = db->GetMsgHdrForKey(aMsgKey, getter_AddRefs(message)); + uint32_t flags; + if (NS_SUCCEEDED(rv) && message) { + message->GetFlags(&flags); + *aResult = (flags & nsMsgMessageFlags::IMAPDeleted) != 0; + } + } + return rv; +} diff --git a/comm/mailnews/base/src/nsMsgTxn.h b/comm/mailnews/base/src/nsMsgTxn.h new file mode 100644 index 0000000000..824b26993b --- /dev/null +++ b/comm/mailnews/base/src/nsMsgTxn.h @@ -0,0 +1,77 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsMsgTxn_h__ +#define nsMsgTxn_h__ + +#include "mozilla/Attributes.h" +#include "nsITransaction.h" +#include "msgCore.h" +#include "nsCOMPtr.h" +#include "nsIMsgWindow.h" +#include "nsInterfaceHashtable.h" +#include "MailNewsTypes2.h" +#include "nsIVariant.h" +#include "nsIWritablePropertyBag.h" +#include "nsIWritablePropertyBag2.h" + +#include "mozilla/EditTransactionBase.h" + +using mozilla::EditTransactionBase; + +#define NS_MESSAGETRANSACTION_IID \ + { /* da621b30-1efc-11d3-abe4-00805f8ac968 */ \ + 0xda621b30, 0x1efc, 0x11d3, { \ + 0xab, 0xe4, 0x00, 0x80, 0x5f, 0x8a, 0xc9, 0x68 \ + } \ + } +/** + * base class for all message undo/redo transactions. + */ + +class nsMsgTxn : public nsITransaction, + public nsIWritablePropertyBag, + public nsIWritablePropertyBag2 { + public: + nsMsgTxn(); + + nsresult Init(); + + NS_IMETHOD DoTransaction(void) override; + + NS_IMETHOD UndoTransaction(void) override = 0; + + NS_IMETHOD RedoTransaction(void) override = 0; + + NS_IMETHOD GetIsTransient(bool* aIsTransient) override; + + NS_IMETHOD Merge(nsITransaction* aTransaction, bool* aDidMerge) override; + + nsresult GetMsgWindow(nsIMsgWindow** msgWindow); + nsresult SetMsgWindow(nsIMsgWindow* msgWindow); + nsresult SetTransactionType(uint32_t txnType); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROPERTYBAG + NS_DECL_NSIPROPERTYBAG2 + NS_DECL_NSIWRITABLEPROPERTYBAG + NS_DECL_NSIWRITABLEPROPERTYBAG2 + + NS_IMETHOD GetAsEditTransactionBase(EditTransactionBase**) final { + return NS_ERROR_NOT_IMPLEMENTED; + } + + protected: + virtual ~nsMsgTxn(); + + // a hash table of string -> nsIVariant + nsInterfaceHashtable<nsStringHashKey, nsIVariant> mPropertyHash; + nsCOMPtr<nsIMsgWindow> m_msgWindow; + uint32_t m_txnType; + nsresult CheckForToggleDelete(nsIMsgFolder* aFolder, const nsMsgKey& aMsgKey, + bool* aResult); +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgUtils.cpp b/comm/mailnews/base/src/nsMsgUtils.cpp new file mode 100644 index 0000000000..661b4fb219 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgUtils.cpp @@ -0,0 +1,1926 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsIMsgHdr.h" +#include "nsMsgUtils.h" +#include "nsMsgFolderFlags.h" +#include "nsMsgMessageFlags.h" +#include "nsString.h" +#include "nsIServiceManager.h" +#include "nsCOMPtr.h" +#include "nsIFolderLookupService.h" +#include "nsIImapUrl.h" +#include "nsIMailboxUrl.h" +#include "nsINntpUrl.h" +#include "nsMsgI18N.h" +#include "nsNativeCharsetUtils.h" +#include "nsCharTraits.h" +#include "prprf.h" +#include "prmem.h" +#include "nsNetCID.h" +#include "nsIIOService.h" +#include "nsIMimeConverter.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsISupportsPrimitives.h" +#include "nsIPrefLocalizedString.h" +#include "nsIRelativeFilePref.h" +#include "mozilla/nsRelativeFilePref.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsISpamSettings.h" +#include "nsICryptoHash.h" +#include "nsNativeCharsetUtils.h" +#include "nsDirectoryServiceUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIRssIncomingServer.h" +#include "nsIMsgFolder.h" +#include "nsIMsgProtocolInfo.h" +#include "nsIMsgMessageService.h" +#include "nsIMsgAccountManager.h" +#include "nsIOutputStream.h" +#include "nsMsgFileStream.h" +#include "nsIFileURL.h" +#include "nsNetUtil.h" +#include "nsProtocolProxyService.h" +#include "nsIProtocolProxyCallback.h" +#include "nsICancelable.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIStringBundle.h" +#include "nsIMsgWindow.h" +#include "nsIWindowWatcher.h" +#include "nsIPrompt.h" +#include "nsIMsgSearchTerm.h" +#include "nsTextFormatter.h" +#include "nsIStreamListener.h" +#include "nsReadLine.h" +#include "nsILineInputStream.h" +#include "nsIParserUtils.h" +#include "nsICharsetConverterManager.h" +#include "nsIDocumentEncoder.h" +#include "mozilla/Components.h" +#include "locale.h" +#include "nsStringStream.h" +#include "nsIInputStreamPump.h" +#include "nsIInputStream.h" +#include "nsIChannel.h" +#include "nsIURIMutator.h" +#include "mozilla/Unused.h" +#include "mozilla/Preferences.h" +#include "mozilla/Encoding.h" +#include "mozilla/EncodingDetector.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/Utf8.h" +#include "mozilla/Buffer.h" +#include "nsIPromptService.h" +#include "nsEmbedCID.h" + +/* for logging to Error Console */ +#include "nsIScriptError.h" +#include "nsIConsoleService.h" + +// Log an error string to the error console +// (adapted from nsContentUtils::LogSimpleConsoleError). +// Flag can indicate error, warning or info. +NS_MSG_BASE void MsgLogToConsole4(const nsAString& aErrorText, + const nsAString& aFilename, + uint32_t aLinenumber, uint32_t aFlag) { + nsCOMPtr<nsIScriptError> scriptError = + do_CreateInstance(NS_SCRIPTERROR_CONTRACTID); + if (NS_WARN_IF(!scriptError)) return; + nsCOMPtr<nsIConsoleService> console = + do_GetService(NS_CONSOLESERVICE_CONTRACTID); + if (NS_WARN_IF(!console)) return; + if (NS_FAILED(scriptError->Init(aErrorText, aFilename, EmptyString(), + aLinenumber, 0, aFlag, "mailnews"_ns, false, + false))) + return; + console->LogMessage(scriptError); + return; +} + +using namespace mozilla; +using namespace mozilla::net; + +#define ILLEGAL_FOLDER_CHARS ";#" +#define ILLEGAL_FOLDER_CHARS_AS_FIRST_LETTER "." +#define ILLEGAL_FOLDER_CHARS_AS_LAST_LETTER ".~ " + +nsresult GetMessageServiceContractIDForURI(const char* uri, + nsCString& contractID) { + nsresult rv = NS_OK; + // Find protocol + nsAutoCString uriStr(uri); + int32_t pos = uriStr.FindChar(':'); + if (pos == -1) return NS_ERROR_FAILURE; + + nsAutoCString protocol(StringHead(uriStr, pos)); + + if (protocol.EqualsLiteral("file")) { + protocol.AssignLiteral("mailbox"); + } + // Build message service contractid + contractID = "@mozilla.org/messenger/messageservice;1?type="; + contractID += protocol.get(); + + return rv; +} + +// Note: This function is also implemented in JS, see MailServices.jsm. +nsresult GetMessageServiceFromURI(const nsACString& uri, + nsIMsgMessageService** aMessageService) { + nsresult rv; + + nsAutoCString contractID; + rv = GetMessageServiceContractIDForURI(PromiseFlatCString(uri).get(), + contractID); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgMessageService> msgService = + do_GetService(contractID.get(), &rv); + NS_ENSURE_SUCCESS(rv, rv); + + msgService.forget(aMessageService); + return rv; +} + +nsresult GetMsgDBHdrFromURI(const nsACString& uri, nsIMsgDBHdr** msgHdr) { + nsCOMPtr<nsIMsgMessageService> msgMessageService; + nsresult rv = + GetMessageServiceFromURI(uri, getter_AddRefs(msgMessageService)); + NS_ENSURE_SUCCESS(rv, rv); + if (!msgMessageService) return NS_ERROR_FAILURE; + + return msgMessageService->MessageURIToMsgHdr(uri, msgHdr); +} + +// Where should this live? It's a utility used to convert a string priority, +// e.g., "High, Low, Normal" to an enum. +// Perhaps we should have an interface that groups together all these +// utilities... +nsresult NS_MsgGetPriorityFromString(const char* const priority, + nsMsgPriorityValue& outPriority) { + if (!priority) return NS_ERROR_NULL_POINTER; + + // Note: Checking the values separately and _before_ the names, + // hoping for a much faster match; + // Only _drawback_, as "priority" handling is not truly specified: + // some software may have the number meanings reversed (1=Lowest) !? + if (PL_strchr(priority, '1')) + outPriority = nsMsgPriority::highest; + else if (PL_strchr(priority, '2')) + outPriority = nsMsgPriority::high; + else if (PL_strchr(priority, '3')) + outPriority = nsMsgPriority::normal; + else if (PL_strchr(priority, '4')) + outPriority = nsMsgPriority::low; + else if (PL_strchr(priority, '5')) + outPriority = nsMsgPriority::lowest; + else if (PL_strcasestr(priority, "Highest")) + outPriority = nsMsgPriority::highest; + // Important: "High" must be tested after "Highest" ! + else if (PL_strcasestr(priority, "High") || PL_strcasestr(priority, "Urgent")) + outPriority = nsMsgPriority::high; + else if (PL_strcasestr(priority, "Normal")) + outPriority = nsMsgPriority::normal; + else if (PL_strcasestr(priority, "Lowest")) + outPriority = nsMsgPriority::lowest; + // Important: "Low" must be tested after "Lowest" ! + else if (PL_strcasestr(priority, "Low") || + PL_strcasestr(priority, "Non-urgent")) + outPriority = nsMsgPriority::low; + else + // "Default" case gets default value. + outPriority = nsMsgPriority::Default; + + return NS_OK; +} + +nsresult NS_MsgGetPriorityValueString(const nsMsgPriorityValue p, + nsACString& outValueString) { + switch (p) { + case nsMsgPriority::highest: + outValueString.Assign('1'); + break; + case nsMsgPriority::high: + outValueString.Assign('2'); + break; + case nsMsgPriority::normal: + outValueString.Assign('3'); + break; + case nsMsgPriority::low: + outValueString.Assign('4'); + break; + case nsMsgPriority::lowest: + outValueString.Assign('5'); + break; + case nsMsgPriority::none: + case nsMsgPriority::notSet: + // Note: '0' is a "fake" value; we expect to never be in this case. + outValueString.Assign('0'); + break; + default: + NS_ASSERTION(false, "invalid priority value"); + } + + return NS_OK; +} + +nsresult NS_MsgGetUntranslatedPriorityName(const nsMsgPriorityValue p, + nsACString& outName) { + switch (p) { + case nsMsgPriority::highest: + outName.AssignLiteral("Highest"); + break; + case nsMsgPriority::high: + outName.AssignLiteral("High"); + break; + case nsMsgPriority::normal: + outName.AssignLiteral("Normal"); + break; + case nsMsgPriority::low: + outName.AssignLiteral("Low"); + break; + case nsMsgPriority::lowest: + outName.AssignLiteral("Lowest"); + break; + case nsMsgPriority::none: + case nsMsgPriority::notSet: + // Note: 'None' is a "fake" value; we expect to never be in this case. + outName.AssignLiteral("None"); + break; + default: + NS_ASSERTION(false, "invalid priority value"); + } + + return NS_OK; +} + +/* this used to be XP_StringHash2 from xp_hash.c */ +/* phong's linear congruential hash */ +static uint32_t StringHash(const char* ubuf, int32_t len = -1) { + unsigned char* buf = (unsigned char*)ubuf; + uint32_t h = 1; + unsigned char* end = buf + (len == -1 ? strlen(ubuf) : len); + while (buf < end) { + h = 0x63c63cd9 * h + 0x9c39c33d + (int32_t)*buf; + buf++; + } + return h; +} + +inline uint32_t StringHash(const nsAutoString& str) { + const char16_t* strbuf = str.get(); + return StringHash(reinterpret_cast<const char*>(strbuf), str.Length() * 2); +} + +/* Utility functions used in a few places in mailnews */ +int32_t MsgFindCharInSet(const nsCString& aString, const char* aChars, + uint32_t aOffset) { + return aString.FindCharInSet(aChars, aOffset); +} + +int32_t MsgFindCharInSet(const nsString& aString, const char16_t* aChars, + uint32_t aOffset) { + return aString.FindCharInSet(aChars, aOffset); +} + +static bool ConvertibleToNative(const nsAutoString& str) { + nsAutoCString native; + nsAutoString roundTripped; + NS_CopyUnicodeToNative(str, native); + NS_CopyNativeToUnicode(native, roundTripped); + return str.Equals(roundTripped); +} + +#if defined(XP_UNIX) +const static uint32_t MAX_LEN = 55; +#elif defined(XP_WIN) +const static uint32_t MAX_LEN = 55; +#else +# error need_to_define_your_max_filename_length +#endif + +nsresult NS_MsgHashIfNecessary(nsAutoCString& name) { + if (name.IsEmpty()) return NS_OK; // Nothing to do. + nsAutoCString str(name); + + // Given a filename, make it safe for filesystem + // certain filenames require hashing because they + // are too long or contain illegal characters + int32_t illegalCharacterIndex = MsgFindCharInSet( + str, FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS ILLEGAL_FOLDER_CHARS, 0); + + // Need to check the first ('.') and last ('.', '~' and ' ') char + if (illegalCharacterIndex == -1) { + int32_t lastIndex = str.Length() - 1; + if (nsLiteralCString(ILLEGAL_FOLDER_CHARS_AS_FIRST_LETTER) + .FindChar(str[0]) != -1) + illegalCharacterIndex = 0; + else if (nsLiteralCString(ILLEGAL_FOLDER_CHARS_AS_LAST_LETTER) + .FindChar(str[lastIndex]) != -1) + illegalCharacterIndex = lastIndex; + else + illegalCharacterIndex = -1; + } + + char hashedname[MAX_LEN + 1]; + if (illegalCharacterIndex == -1) { + // no illegal chars, it's just too long + // keep the initial part of the string, but hash to make it fit + if (str.Length() > MAX_LEN) { + PL_strncpy(hashedname, str.get(), MAX_LEN + 1); + PR_snprintf(hashedname + MAX_LEN - 8, 9, "%08lx", + (unsigned long)StringHash(str.get())); + name = hashedname; + } + } else { + // found illegal chars, hash the whole thing + // if we do substitution, then hash, two strings + // could hash to the same value. + // for example, on mac: "foo__bar", "foo:_bar", "foo::bar" + // would map to "foo_bar". this way, all three will map to + // different values + PR_snprintf(hashedname, 9, "%08lx", (unsigned long)StringHash(str.get())); + name = hashedname; + } + + return NS_OK; +} + +// XXX : The number of UTF-16 2byte code units are half the number of +// bytes in legacy encodings for CJK strings and non-Latin1 in UTF-8. +// The ratio can be 1/3 for CJK strings in UTF-8. However, we can +// get away with using the same MAX_LEN for nsCString and nsString +// because MAX_LEN is defined rather conservatively in the first place. +nsresult NS_MsgHashIfNecessary(nsAutoString& name) { + if (name.IsEmpty()) return NS_OK; // Nothing to do. + int32_t illegalCharacterIndex = MsgFindCharInSet( + name, + u"" FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS ILLEGAL_FOLDER_CHARS, 0); + + // Need to check the first ('.') and last ('.', '~' and ' ') char + if (illegalCharacterIndex == -1) { + int32_t lastIndex = name.Length() - 1; + if (NS_LITERAL_STRING_FROM_CSTRING(ILLEGAL_FOLDER_CHARS_AS_FIRST_LETTER) + .FindChar(name[0]) != -1) + illegalCharacterIndex = 0; + else if (NS_LITERAL_STRING_FROM_CSTRING(ILLEGAL_FOLDER_CHARS_AS_LAST_LETTER) + .FindChar(name[lastIndex]) != -1) + illegalCharacterIndex = lastIndex; + else + illegalCharacterIndex = -1; + } + + char hashedname[9]; + int32_t keptLength = -1; + if (illegalCharacterIndex != -1) + keptLength = illegalCharacterIndex; + else if (!ConvertibleToNative(name)) + keptLength = 0; + else if (name.Length() > MAX_LEN) { + keptLength = MAX_LEN - 8; + // To avoid keeping only the high surrogate of a surrogate pair + if (NS_IS_HIGH_SURROGATE(name.CharAt(keptLength - 1))) --keptLength; + } + + if (keptLength >= 0) { + PR_snprintf(hashedname, 9, "%08lx", (unsigned long)StringHash(name)); + name.SetLength(keptLength); + name.Append(NS_ConvertASCIItoUTF16(hashedname)); + } + + return NS_OK; +} + +nsresult FormatFileSize(int64_t size, bool useKB, nsAString& formattedSize) { + const char* sizeAbbrNames[] = { + "byteAbbreviation2", "kiloByteAbbreviation2", "megaByteAbbreviation2", + "gigaByteAbbreviation2", "teraByteAbbreviation2", "petaByteAbbreviation2", + }; + + nsresult rv; + + nsCOMPtr<nsIStringBundleService> bundleSvc = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleSvc, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIStringBundle> bundle; + rv = bundleSvc->CreateBundle("chrome://messenger/locale/messenger.properties", + getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + double unitSize = size < 0 ? 0.0 : size; + uint32_t unitIndex = 0; + + if (useKB) { + // Start by formatting in kilobytes + unitSize /= 1024; + if (unitSize < 0.1 && unitSize != 0) unitSize = 0.1; + unitIndex++; + } + + // Convert to next unit if it needs 4 digits (after rounding), but only if + // we know the name of the next unit + while ((unitSize >= 999.5) && (unitIndex < ArrayLength(sizeAbbrNames) - 1)) { + unitSize /= 1024; + unitIndex++; + } + + // Grab the string for the appropriate unit + nsString sizeAbbr; + rv = bundle->GetStringFromName(sizeAbbrNames[unitIndex], sizeAbbr); + NS_ENSURE_SUCCESS(rv, rv); + + // Get rid of insignificant bits by truncating to 1 or 0 decimal points + // 0.1 -> 0.1; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 + nsTextFormatter::ssprintf( + formattedSize, sizeAbbr.get(), + (unitIndex != 0) && (unitSize < 99.95 && unitSize != 0) ? 1 : 0, + unitSize); + + int32_t separatorPos = formattedSize.FindChar('.'); + if (separatorPos != kNotFound) { + // The ssprintf returned a decimal number using a dot (.) as the decimal + // separator. Now we try to localize the separator. + // Try to get the decimal separator from the system's locale. + char* decimalPoint; +#ifdef HAVE_LOCALECONV + struct lconv* locale = localeconv(); + decimalPoint = locale->decimal_point; +#else + decimalPoint = getenv("LOCALE_DECIMAL_POINT"); +#endif + NS_ConvertUTF8toUTF16 decimalSeparator(decimalPoint); + if (decimalSeparator.IsEmpty()) decimalSeparator.Assign('.'); + + formattedSize.Replace(separatorPos, 1, decimalSeparator); + } + + return NS_OK; +} + +nsresult NS_MsgCreatePathStringFromFolderURI(const char* aFolderURI, + nsCString& aPathCString, + const nsCString& aScheme, + bool aIsNewsFolder) { + // A file name has to be in native charset. Here we convert + // to UTF-16 and check for 'unsafe' characters before converting + // to native charset. + NS_ENSURE_TRUE(mozilla::IsUtf8(nsDependentCString(aFolderURI)), + NS_ERROR_UNEXPECTED); + NS_ConvertUTF8toUTF16 oldPath(aFolderURI); + + nsAutoString pathPiece, path; + + int32_t startSlashPos = oldPath.FindChar('/'); + int32_t endSlashPos = (startSlashPos >= 0) + ? oldPath.FindChar('/', startSlashPos + 1) - 1 + : oldPath.Length() - 1; + if (endSlashPos < 0) endSlashPos = oldPath.Length(); +#if defined(XP_UNIX) || defined(XP_MACOSX) + bool isLocalUri = aScheme.EqualsLiteral("none") || + aScheme.EqualsLiteral("pop3") || + aScheme.EqualsLiteral("rss"); +#endif + // trick to make sure we only add the path to the first n-1 folders + bool haveFirst = false; + while (startSlashPos != -1) { + pathPiece.Assign( + Substring(oldPath, startSlashPos + 1, endSlashPos - startSlashPos)); + // skip leading '/' (and other // style things) + if (!pathPiece.IsEmpty()) { + // add .sbd onto the previous path + if (haveFirst) { + path.AppendLiteral(FOLDER_SUFFIX "/"); + } + + if (aIsNewsFolder) { + nsAutoCString tmp; + CopyUTF16toMUTF7(pathPiece, tmp); + CopyASCIItoUTF16(tmp, pathPiece); + } +#if defined(XP_UNIX) || defined(XP_MACOSX) + // Don't hash path pieces because local mail folder uri's have already + // been hashed. We're only doing this on the mac to limit potential + // regressions. + if (!isLocalUri) +#endif + NS_MsgHashIfNecessary(pathPiece); + path += pathPiece; + haveFirst = true; + } + // look for the next slash + startSlashPos = endSlashPos + 1; + + endSlashPos = (startSlashPos >= 0) + ? oldPath.FindChar('/', startSlashPos + 1) - 1 + : oldPath.Length() - 1; + if (endSlashPos < 0) endSlashPos = oldPath.Length(); + + if (startSlashPos >= endSlashPos) break; + } + return NS_CopyUnicodeToNative(path, aPathCString); +} + +bool NS_MsgStripRE(const nsCString& subject, nsCString& modifiedSubject) { + bool result = false; + + // Get localizedRe pref. + nsresult rv; + nsString utf16LocalizedRe; + NS_GetLocalizedUnicharPreferenceWithDefault(nullptr, "mailnews.localizedRe", + EmptyString(), utf16LocalizedRe); + NS_ConvertUTF16toUTF8 localizedRe(utf16LocalizedRe); + + // Hardcoded "Re" so that no one can configure Mozilla standards incompatible. + nsAutoCString checkString("Re,RE,re,rE"); + if (!localizedRe.IsEmpty()) { + checkString.Append(','); + checkString.Append(localizedRe); + } + + // Decode the string. + nsCString decodedString; + nsCOMPtr<nsIMimeConverter> mimeConverter; + // We cannot strip "Re:" for RFC2047-encoded subject without modifying the + // original. + if (subject.Find("=?") != kNotFound) { + mimeConverter = + do_GetService("@mozilla.org/messenger/mimeconverter;1", &rv); + if (NS_SUCCEEDED(rv)) + rv = mimeConverter->DecodeMimeHeaderToUTF8(subject, nullptr, false, true, + decodedString); + } + + const char *s, *s_end; + if (decodedString.IsEmpty()) { + s = subject.BeginReading(); + s_end = s + subject.Length(); + } else { + s = decodedString.BeginReading(); + s_end = s + decodedString.Length(); + } + +AGAIN: + while (s < s_end && IS_SPACE(*s)) s++; + + const char* tokPtr = checkString.get(); + while (*tokPtr) { + // Tokenize the comma separated list. + size_t tokenLength = 0; + while (*tokPtr && *tokPtr != ',') { + tokenLength++; + tokPtr++; + } + // Check if the beginning of s is the actual token. + if (tokenLength && !strncmp(s, tokPtr - tokenLength, tokenLength)) { + if (s[tokenLength] == ':') { + s = s + tokenLength + 1; /* Skip over "Re:" */ + result = true; /* Yes, we stripped it. */ + goto AGAIN; /* Skip whitespace and try again. */ + } else if (s[tokenLength] == '[' || s[tokenLength] == '(') { + const char* s2 = s + tokenLength + 1; /* Skip over "Re[" */ + + // Skip forward over digits after the "[". + while (s2 < (s_end - 2) && isdigit((unsigned char)*s2)) s2++; + + // Now ensure that the following thing is "]:". + // Only if it is do we alter `s`. + if ((s2[0] == ']' || s2[0] == ')') && s2[1] == ':') { + s = s2 + 2; /* Skip over "]:" */ + result = true; /* Yes, we stripped it. */ + goto AGAIN; /* Skip whitespace and try again. */ + } + } + } + if (*tokPtr) tokPtr++; + } + + // If we didn't strip anything, we can return here. + if (!result) return false; + + if (decodedString.IsEmpty()) { + // We didn't decode anything, so just return a new string. + modifiedSubject.Assign(s); + return true; + } + + // We decoded the string, so we need to encode it again. We always encode in + // UTF-8. + mimeConverter->EncodeMimePartIIStr_UTF8( + nsDependentCString(s), false, sizeof("Subject:"), + nsIMimeConverter::MIME_ENCODED_WORD_SIZE, modifiedSubject); + return true; +} + +/* Very similar to strdup except it free's too + */ +char* NS_MsgSACopy(char** destination, const char* source) { + if (*destination) { + PR_Free(*destination); + *destination = 0; + } + if (!source) + *destination = nullptr; + else { + *destination = (char*)PR_Malloc(PL_strlen(source) + 1); + if (*destination == nullptr) return (nullptr); + + PL_strcpy(*destination, source); + } + return *destination; +} + +/* Again like strdup but it concatenates and free's and uses Realloc. + */ +char* NS_MsgSACat(char** destination, const char* source) { + if (source && *source) { + int destLength = *destination ? PL_strlen(*destination) : 0; + char* newDestination = + (char*)PR_Realloc(*destination, destLength + PL_strlen(source) + 1); + if (newDestination == nullptr) return nullptr; + + *destination = newDestination; + PL_strcpy(*destination + destLength, source); + } + return *destination; +} + +nsresult NS_MsgEscapeEncodeURLPath(const nsAString& aStr, nsCString& aResult) { + return MsgEscapeString(NS_ConvertUTF16toUTF8(aStr), + nsINetUtil::ESCAPE_URL_PATH, aResult); +} + +nsresult NS_MsgDecodeUnescapeURLPath(const nsACString& aPath, + nsAString& aResult) { + nsAutoCString unescapedName; + MsgUnescapeString( + aPath, + nsINetUtil::ESCAPE_URL_FILE_BASENAME | nsINetUtil::ESCAPE_URL_FORCED, + unescapedName); + CopyUTF8toUTF16(unescapedName, aResult); + return NS_OK; +} + +bool WeAreOffline() { + bool offline = false; + + nsCOMPtr<nsIIOService> ioService = mozilla::components::IO::Service(); + if (ioService) ioService->GetOffline(&offline); + + return offline; +} + +// Find a folder by URL. If it doesn't exist, null will be returned +// via aFolder. +nsresult FindFolder(const nsACString& aFolderURI, nsIMsgFolder** aFolder) { + NS_ENSURE_ARG_POINTER(aFolder); + + *aFolder = nullptr; + + nsresult rv; + nsCOMPtr<nsIFolderLookupService> fls(do_GetService(NSIFLS_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // GetFolderForURL returns NS_OK and null for non-existent folders + rv = fls->GetFolderForURL(aFolderURI, aFolder); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +// Fetch an existing folder by URL +// The returned aFolder will be non-null if and only if result is NS_OK. +// NS_OK - folder was found +// NS_MSG_FOLDER_MISSING - if aFolderURI not found +nsresult GetExistingFolder(const nsACString& aFolderURI, + nsIMsgFolder** aFolder) { + nsresult rv = FindFolder(aFolderURI, aFolder); + NS_ENSURE_SUCCESS(rv, rv); + return *aFolder ? NS_OK : NS_MSG_ERROR_FOLDER_MISSING; +} + +nsresult GetOrCreateFolder(const nsACString& aFolderURI, + nsIMsgFolder** aFolder) { + NS_ENSURE_ARG_POINTER(aFolder); + + *aFolder = nullptr; + + nsresult rv; + nsCOMPtr<nsIFolderLookupService> fls(do_GetService(NSIFLS_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = fls->GetOrCreateFolderForURL(aFolderURI, aFolder); + NS_ENSURE_SUCCESS(rv, rv); + + return *aFolder ? NS_OK : NS_ERROR_FAILURE; +} + +bool IsAFromSpaceLine(char* start, const char* end) { + bool rv = false; + while ((start < end) && (*start == '>')) start++; + // If the leading '>'s are followed by an 'F' then we have a possible case + // here. + if ((*start == 'F') && (end - start > 4) && !strncmp(start, "From ", 5)) + rv = true; + return rv; +} + +// +// This function finds all lines starting with "From " or "From " preceding +// with one or more '>' (ie, ">From", ">>From", etc) in the input buffer +// (between 'start' and 'end') and prefix them with a ">" . +// +nsresult EscapeFromSpaceLine(nsIOutputStream* outputStream, char* start, + const char* end) { + nsresult rv; + char* pChar; + uint32_t written; + + pChar = start; + while (start < end) { + while ((pChar < end) && (*pChar != '\r') && ((pChar + 1) < end) && + (*(pChar + 1) != '\n')) + pChar++; + if ((pChar + 1) == end) pChar++; + + if (pChar < end) { + // Found a line so check if it's a qualified "From " line. + if (IsAFromSpaceLine(start, pChar)) { + rv = outputStream->Write(">", 1, &written); + NS_ENSURE_SUCCESS(rv, rv); + } + int32_t lineTerminatorCount = (*(pChar + 1) == '\n') ? 2 : 1; + rv = outputStream->Write(start, pChar - start + lineTerminatorCount, + &written); + NS_ENSURE_SUCCESS(rv, rv); + pChar += lineTerminatorCount; + start = pChar; + } else if (start < end) { + // Check and flush out the remaining data and we're done. + if (IsAFromSpaceLine(start, end)) { + rv = outputStream->Write(">", 1, &written); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = outputStream->Write(start, end - start, &written); + NS_ENSURE_SUCCESS(rv, rv); + break; + } + } + return NS_OK; +} + +nsresult IsRFC822HeaderFieldName(const char* aHdr, bool* aResult) { + NS_ENSURE_ARG_POINTER(aHdr); + NS_ENSURE_ARG_POINTER(aResult); + uint32_t length = strlen(aHdr); + for (uint32_t i = 0; i < length; i++) { + char c = aHdr[i]; + if (c < '!' || c == ':' || c > '~') { + *aResult = false; + return NS_OK; + } + } + *aResult = true; + return NS_OK; +} + +// Warning, currently this routine only works for the Junk Folder +nsresult GetOrCreateJunkFolder(const nsACString& aURI, + nsIUrlListener* aListener) { + nsresult rv; + + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(aURI, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + + // don't check validity of folder - caller will handle creating it + nsCOMPtr<nsIMsgIncomingServer> server; + // make sure that folder hierarchy is built so that legitimate parent-child + // relationship is established + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + if (!server) return NS_ERROR_UNEXPECTED; + + nsCOMPtr<nsIMsgFolder> msgFolder; + rv = server->GetMsgFolderFromURI(folder, aURI, getter_AddRefs(msgFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgFolder> parent; + rv = msgFolder->GetParent(getter_AddRefs(parent)); + if (NS_FAILED(rv) || !parent) { + nsCOMPtr<nsIFile> folderPath; + // for local folders, path is to the berkeley mailbox. + // for imap folders, path needs to have .msf appended to the name + msgFolder->GetFilePath(getter_AddRefs(folderPath)); + + nsCOMPtr<nsIMsgProtocolInfo> protocolInfo; + rv = server->GetProtocolInfo(getter_AddRefs(protocolInfo)); + NS_ENSURE_SUCCESS(rv, rv); + + bool isAsyncFolder; + rv = protocolInfo->GetFoldersCreatedAsync(&isAsyncFolder); + NS_ENSURE_SUCCESS(rv, rv); + + // if we can't get the path from the folder, then try to create the storage. + // for imap, it doesn't matter if the .msf file exists - it still might not + // exist on the server, so we should try to create it + bool exists = false; + if (!isAsyncFolder && folderPath) folderPath->Exists(&exists); + if (!exists) { + // Hack to work around a localization bug with the Junk Folder. + // Please see Bug #270261 for more information... + nsString localizedJunkName; + msgFolder->GetName(localizedJunkName); + + // force the junk folder name to be Junk so it gets created on disk + // correctly... + msgFolder->SetName(u"Junk"_ns); + msgFolder->SetFlag(nsMsgFolderFlags::Junk); + rv = msgFolder->CreateStorageIfMissing(aListener); + NS_ENSURE_SUCCESS(rv, rv); + + // now restore the localized folder name... + msgFolder->SetName(localizedJunkName); + + // XXX TODO + // JUNK MAIL RELATED + // ugh, I hate this hack + // we have to do this (for now) + // because imap and local are different (one creates folder asynch, the + // other synch) one will notify the listener, one will not. I blame + // nsMsgCopy. we should look into making it so no matter what the folder + // type we always call the listener this code should move into local + // folder's version of CreateStorageIfMissing() + if (!isAsyncFolder && aListener) { + rv = aListener->OnStartRunningUrl(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aListener->OnStopRunningUrl(nullptr, NS_OK); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } else { + // if the folder exists, we should set the junk flag on it + // which is what the listener will do + if (aListener) { + rv = aListener->OnStartRunningUrl(nullptr); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aListener->OnStopRunningUrl(nullptr, NS_OK); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + return NS_OK; +} + +nsresult IsRSSArticle(nsIURI* aMsgURI, bool* aIsRSSArticle) { + nsresult rv; + *aIsRSSArticle = false; + + nsCOMPtr<nsIMsgMessageUrl> msgUrl = do_QueryInterface(aMsgURI, &rv); + if (NS_FAILED(rv)) return rv; + + nsCString resourceURI; + msgUrl->GetUri(resourceURI); + + // get the msg service for this URI + nsCOMPtr<nsIMsgMessageService> msgService; + rv = GetMessageServiceFromURI(resourceURI, getter_AddRefs(msgService)); + NS_ENSURE_SUCCESS(rv, rv); + + // Check if the message is a feed message, regardless of folder. + uint32_t flags; + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = msgService->MessageURIToMsgHdr(resourceURI, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + msgHdr->GetFlags(&flags); + if (flags & nsMsgMessageFlags::FeedMsg) { + *aIsRSSArticle = true; + return rv; + } + + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl = do_QueryInterface(aMsgURI, &rv); + mozilla::Unused << mailnewsUrl; + NS_ENSURE_SUCCESS(rv, rv); + + // get the folder and the server from the msghdr + nsCOMPtr<nsIMsgFolder> folder; + rv = msgHdr->GetFolder(getter_AddRefs(folder)); + if (NS_SUCCEEDED(rv) && folder) { + nsCOMPtr<nsIMsgIncomingServer> server; + folder->GetServer(getter_AddRefs(server)); + nsCOMPtr<nsIRssIncomingServer> rssServer = do_QueryInterface(server); + + if (rssServer) *aIsRSSArticle = true; + } + + return rv; +} + +// digest needs to be a pointer to a DIGEST_LENGTH (16) byte buffer +nsresult MSGCramMD5(const char* text, int32_t text_len, const char* key, + int32_t key_len, unsigned char* digest) { + nsresult rv; + + nsAutoCString hash; + nsCOMPtr<nsICryptoHash> hasher = + do_CreateInstance("@mozilla.org/security/hash;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // this code adapted from + // http://www.cis.ohio-state.edu/cgi-bin/rfc/rfc2104.html + + char innerPad[65]; /* inner padding - key XORd with innerPad */ + char outerPad[65]; /* outer padding - key XORd with outerPad */ + int i; + /* if key is longer than 64 bytes reset it to key=MD5(key) */ + if (key_len > 64) { + rv = hasher->Init(nsICryptoHash::MD5); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hasher->Update((const uint8_t*)key, key_len); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hasher->Finish(false, hash); + NS_ENSURE_SUCCESS(rv, rv); + + key = hash.get(); + key_len = DIGEST_LENGTH; + } + + /* + * the HMAC_MD5 transform looks like: + * + * MD5(K XOR outerPad, MD5(K XOR innerPad, text)) + * + * where K is an n byte key + * innerPad is the byte 0x36 repeated 64 times + * outerPad is the byte 0x5c repeated 64 times + * and text is the data being protected + */ + + /* start out by storing key in pads */ + memset(innerPad, 0, sizeof innerPad); + memset(outerPad, 0, sizeof outerPad); + memcpy(innerPad, key, key_len); + memcpy(outerPad, key, key_len); + + /* XOR key with innerPad and outerPad values */ + for (i = 0; i < 64; i++) { + innerPad[i] ^= 0x36; + outerPad[i] ^= 0x5c; + } + /* + * perform inner MD5 + */ + nsAutoCString result; + rv = hasher->Init(nsICryptoHash::MD5); /* init context for 1st pass */ + rv = hasher->Update((const uint8_t*)innerPad, 64); /* start with inner pad */ + rv = hasher->Update((const uint8_t*)text, + text_len); /* then text of datagram */ + rv = hasher->Finish(false, result); /* finish up 1st pass */ + + /* + * perform outer MD5 + */ + hasher->Init(nsICryptoHash::MD5); /* init context for 2nd pass */ + rv = hasher->Update((const uint8_t*)outerPad, 64); /* start with outer pad */ + rv = hasher->Update((const uint8_t*)result.get(), + 16); /* then results of 1st hash */ + rv = hasher->Finish(false, result); /* finish up 2nd pass */ + + if (result.Length() != DIGEST_LENGTH) return NS_ERROR_UNEXPECTED; + + memcpy(digest, result.get(), DIGEST_LENGTH); + + return rv; +} + +// digest needs to be a pointer to a DIGEST_LENGTH (16) byte buffer +nsresult MSGApopMD5(const char* text, int32_t text_len, const char* password, + int32_t password_len, unsigned char* digest) { + nsresult rv; + nsAutoCString result; + + nsCOMPtr<nsICryptoHash> hasher = + do_CreateInstance("@mozilla.org/security/hash;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hasher->Init(nsICryptoHash::MD5); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hasher->Update((const uint8_t*)text, text_len); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hasher->Update((const uint8_t*)password, password_len); + NS_ENSURE_SUCCESS(rv, rv); + + rv = hasher->Finish(false, result); + NS_ENSURE_SUCCESS(rv, rv); + + if (result.Length() != DIGEST_LENGTH) return NS_ERROR_UNEXPECTED; + + memcpy(digest, result.get(), DIGEST_LENGTH); + return rv; +} + +NS_MSG_BASE nsresult NS_GetPersistentFile(const char* relPrefName, + const char* absPrefName, + const char* dirServiceProp, + bool& gotRelPref, nsIFile** aFile, + nsIPrefBranch* prefBranch) { + NS_ENSURE_ARG_POINTER(aFile); + *aFile = nullptr; + NS_ENSURE_ARG(relPrefName); + NS_ENSURE_ARG(absPrefName); + gotRelPref = false; + + nsCOMPtr<nsIPrefBranch> mainBranch; + if (!prefBranch) { + nsCOMPtr<nsIPrefService> prefService( + do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (!prefService) return NS_ERROR_FAILURE; + prefService->GetBranch(nullptr, getter_AddRefs(mainBranch)); + if (!mainBranch) return NS_ERROR_FAILURE; + prefBranch = mainBranch; + } + + nsCOMPtr<nsIFile> localFile; + + // Get the relative first + nsCOMPtr<nsIRelativeFilePref> relFilePref; + prefBranch->GetComplexValue(relPrefName, NS_GET_IID(nsIRelativeFilePref), + getter_AddRefs(relFilePref)); + if (relFilePref) { + relFilePref->GetFile(getter_AddRefs(localFile)); + NS_ASSERTION(localFile, "An nsIRelativeFilePref has no file."); + if (localFile) gotRelPref = true; + } + + // If not, get the old absolute + if (!localFile) { + prefBranch->GetComplexValue(absPrefName, NS_GET_IID(nsIFile), + getter_AddRefs(localFile)); + + // If not, and given a dirServiceProp, use directory service. + if (!localFile && dirServiceProp) { + nsCOMPtr<nsIProperties> dirService( + do_GetService("@mozilla.org/file/directory_service;1")); + if (!dirService) return NS_ERROR_FAILURE; + dirService->Get(dirServiceProp, NS_GET_IID(nsIFile), + getter_AddRefs(localFile)); + if (!localFile) return NS_ERROR_FAILURE; + } + } + + if (localFile) { + localFile->Normalize(); + localFile.forget(aFile); + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +NS_MSG_BASE nsresult NS_SetPersistentFile(const char* relPrefName, + const char* absPrefName, + nsIFile* aFile, + nsIPrefBranch* prefBranch) { + NS_ENSURE_ARG(relPrefName); + NS_ENSURE_ARG(absPrefName); + NS_ENSURE_ARG(aFile); + + nsCOMPtr<nsIPrefBranch> mainBranch; + if (!prefBranch) { + nsCOMPtr<nsIPrefService> prefService( + do_GetService(NS_PREFSERVICE_CONTRACTID)); + if (!prefService) return NS_ERROR_FAILURE; + prefService->GetBranch(nullptr, getter_AddRefs(mainBranch)); + if (!mainBranch) return NS_ERROR_FAILURE; + prefBranch = mainBranch; + } + + // Write the absolute for backwards compatibilty's sake. + // Or, if aPath is on a different drive than the profile dir. + nsresult rv = + prefBranch->SetComplexValue(absPrefName, NS_GET_IID(nsIFile), aFile); + + // Write the relative path. + nsCOMPtr<nsIRelativeFilePref> relFilePref = new nsRelativeFilePref(); + mozilla::Unused << relFilePref->SetFile(aFile); + mozilla::Unused << relFilePref->SetRelativeToKey( + nsLiteralCString(NS_APP_USER_PROFILE_50_DIR)); + + nsresult rv2 = prefBranch->SetComplexValue( + relPrefName, NS_GET_IID(nsIRelativeFilePref), relFilePref); + if (NS_FAILED(rv2) && NS_SUCCEEDED(rv)) + prefBranch->ClearUserPref(relPrefName); + + return rv; +} + +NS_MSG_BASE nsresult NS_GetUnicharPreferenceWithDefault( + nsIPrefBranch* prefBranch, // can be null, if so uses the root branch + const char* prefName, const nsAString& defValue, nsAString& prefValue) { + NS_ENSURE_ARG(prefName); + + nsCOMPtr<nsIPrefBranch> pbr; + if (!prefBranch) { + pbr = do_GetService(NS_PREFSERVICE_CONTRACTID); + prefBranch = pbr; + } + + nsCString valueUtf8; + nsresult rv = + prefBranch->GetStringPref(prefName, EmptyCString(), 0, valueUtf8); + if (NS_SUCCEEDED(rv)) + CopyUTF8toUTF16(valueUtf8, prefValue); + else + prefValue = defValue; + return NS_OK; +} + +NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreferenceWithDefault( + nsIPrefBranch* prefBranch, // can be null, if so uses the root branch + const char* prefName, const nsAString& defValue, nsAString& prefValue) { + NS_ENSURE_ARG(prefName); + + nsCOMPtr<nsIPrefBranch> pbr; + if (!prefBranch) { + pbr = do_GetService(NS_PREFSERVICE_CONTRACTID); + prefBranch = pbr; + } + + nsCOMPtr<nsIPrefLocalizedString> str; + nsresult rv = prefBranch->GetComplexValue( + prefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(str)); + if (NS_SUCCEEDED(rv)) { + nsString tmpValue; + str->ToString(getter_Copies(tmpValue)); + prefValue.Assign(tmpValue); + } else + prefValue = defValue; + return NS_OK; +} + +NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreference( + nsIPrefBranch* prefBranch, // can be null, if so uses the root branch + const char* prefName, nsAString& prefValue) { + NS_ENSURE_ARG_POINTER(prefName); + + nsCOMPtr<nsIPrefBranch> pbr; + if (!prefBranch) { + pbr = do_GetService(NS_PREFSERVICE_CONTRACTID); + prefBranch = pbr; + } + + nsCOMPtr<nsIPrefLocalizedString> str; + nsresult rv = prefBranch->GetComplexValue( + prefName, NS_GET_IID(nsIPrefLocalizedString), getter_AddRefs(str)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString tmpValue; + str->ToString(getter_Copies(tmpValue)); + prefValue.Assign(tmpValue); + return NS_OK; +} + +void PRTime2Seconds(PRTime prTime, uint32_t* seconds) { + *seconds = (uint32_t)(prTime / PR_USEC_PER_SEC); +} + +void PRTime2Seconds(PRTime prTime, int32_t* seconds) { + *seconds = (int32_t)(prTime / PR_USEC_PER_SEC); +} + +void Seconds2PRTime(uint32_t seconds, PRTime* prTime) { + *prTime = (PRTime)seconds * PR_USEC_PER_SEC; +} + +nsresult GetSummaryFileLocation(nsIFile* fileLocation, + nsIFile** summaryLocation) { + nsresult rv; + nsCOMPtr<nsIFile> newSummaryLocation = + do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + newSummaryLocation->InitWithFile(fileLocation); + nsString fileName; + + rv = newSummaryLocation->GetLeafName(fileName); + if (NS_FAILED(rv)) return rv; + + fileName.AppendLiteral(SUMMARY_SUFFIX); + rv = newSummaryLocation->SetLeafName(fileName); + NS_ENSURE_SUCCESS(rv, rv); + + newSummaryLocation.forget(summaryLocation); + return NS_OK; +} + +void MsgGenerateNowStr(nsACString& nowStr) { + char dateBuf[100]; + dateBuf[0] = '\0'; + PRExplodedTime exploded; + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded); + PR_FormatTimeUSEnglish(dateBuf, sizeof(dateBuf), "%a %b %d %H:%M:%S %Y", + &exploded); + nowStr.Assign(dateBuf); +} + +// Gets a special directory and appends the supplied file name onto it. +nsresult GetSpecialDirectoryWithFileName(const char* specialDirName, + const char* fileName, + nsIFile** result) { + nsresult rv = NS_GetSpecialDirectory(specialDirName, result); + NS_ENSURE_SUCCESS(rv, rv); + + return (*result)->AppendNative(nsDependentCString(fileName)); +} + +// Cleans up temp files with matching names +nsresult MsgCleanupTempFiles(const char* fileName, const char* extension) { + nsCOMPtr<nsIFile> tmpFile; + nsCString rootName(fileName); + rootName.Append('.'); + rootName.Append(extension); + nsresult rv = GetSpecialDirectoryWithFileName(NS_OS_TEMP_DIR, rootName.get(), + getter_AddRefs(tmpFile)); + + NS_ENSURE_SUCCESS(rv, rv); + int index = 1; + bool exists; + do { + tmpFile->Exists(&exists); + if (exists) { + tmpFile->Remove(false); + nsCString leafName(fileName); + leafName.Append('-'); + leafName.AppendInt(index); + leafName.Append('.'); + leafName.Append(extension); + // start with "Picture-1.jpg" after "Picture.jpg" exists + tmpFile->SetNativeLeafName(leafName); + } + } while (exists && index++ < 10000); + return NS_OK; +} + +nsresult MsgGetFileStream(nsIFile* file, nsIOutputStream** fileStream) { + RefPtr<nsMsgFileStream> newFileStream = new nsMsgFileStream; + nsresult rv = newFileStream->InitWithFile(file); + NS_ENSURE_SUCCESS(rv, rv); + newFileStream.forget(fileStream); + return NS_OK; +} + +nsresult MsgNewBufferedFileOutputStream(nsIOutputStream** aResult, + nsIFile* aFile, int32_t aIOFlags, + int32_t aPerm) { + nsCOMPtr<nsIOutputStream> stream; + nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), aFile, + aIOFlags, aPerm); + if (NS_SUCCEEDED(rv)) + rv = NS_NewBufferedOutputStream(aResult, stream.forget(), + FILE_IO_BUFFER_SIZE); + return rv; +} + +nsresult MsgNewSafeBufferedFileOutputStream(nsIOutputStream** aResult, + nsIFile* aFile, int32_t aIOFlags, + int32_t aPerm) { + nsCOMPtr<nsIOutputStream> stream; + nsresult rv = NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), aFile, + aIOFlags, aPerm); + if (NS_SUCCEEDED(rv)) + rv = NS_NewBufferedOutputStream(aResult, stream.forget(), + FILE_IO_BUFFER_SIZE); + return rv; +} + +bool MsgFindKeyword(const nsCString& keyword, nsCString& keywords, + int32_t* aStartOfKeyword, int32_t* aLength) { +// nsTString_CharT::Find(const nsCString& aString, +// bool aIgnoreCase=false, +// int32_t aOffset=0, +// int32_t aCount=-1 ) const; +#define FIND_KEYWORD(keywords, keyword, offset) \ + ((keywords).Find((keyword), (offset))) + // 'keyword' is the single keyword we're looking for + // 'keywords' is a space delimited list of keywords to be searched, + // which may be just a single keyword or even be empty + const int32_t kKeywordLen = keyword.Length(); + const char* start = keywords.BeginReading(); + const char* end = keywords.EndReading(); + *aStartOfKeyword = FIND_KEYWORD(keywords, keyword, 0); + while (*aStartOfKeyword >= 0) { + const char* matchStart = start + *aStartOfKeyword; + const char* matchEnd = matchStart + kKeywordLen; + // For a real match, matchStart must be the start of keywords or preceded + // by a space and matchEnd must be the end of keywords or point to a space. + if ((matchStart == start || *(matchStart - 1) == ' ') && + (matchEnd == end || *matchEnd == ' ')) { + *aLength = kKeywordLen; + return true; + } + *aStartOfKeyword = + FIND_KEYWORD(keywords, keyword, *aStartOfKeyword + kKeywordLen); + } + + *aLength = 0; + return false; +#undef FIND_KEYWORD +} + +bool MsgHostDomainIsTrusted(nsCString& host, nsCString& trustedMailDomains) { + const char* end; + uint32_t hostLen, domainLen; + bool domainIsTrusted = false; + + const char* domain = trustedMailDomains.BeginReading(); + const char* domainEnd = trustedMailDomains.EndReading(); + const char* hostStart = host.BeginReading(); + hostLen = host.Length(); + + do { + // skip any whitespace + while (*domain == ' ' || *domain == '\t') ++domain; + + // find end of this domain in the string + end = strchr(domain, ','); + if (!end) end = domainEnd; + + // to see if the hostname is in the domain, check if the domain + // matches the end of the hostname. + domainLen = end - domain; + if (domainLen && hostLen >= domainLen) { + const char* hostTail = hostStart + hostLen - domainLen; + if (PL_strncasecmp(domain, hostTail, domainLen) == 0) { + // now, make sure either that the hostname is a direct match or + // that the hostname begins with a dot. + if (hostLen == domainLen || *hostTail == '.' || + *(hostTail - 1) == '.') { + domainIsTrusted = true; + break; + } + } + } + + domain = end + 1; + } while (*end); + return domainIsTrusted; +} + +nsresult MsgGetLocalFileFromURI(const nsACString& aUTF8Path, nsIFile** aFile) { + nsresult rv; + nsCOMPtr<nsIURI> argURI; + rv = NS_NewURI(getter_AddRefs(argURI), aUTF8Path); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIFileURL> argFileURL(do_QueryInterface(argURI, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIFile> argFile; + rv = argFileURL->GetFile(getter_AddRefs(argFile)); + NS_ENSURE_SUCCESS(rv, rv); + + argFile.forget(aFile); + return NS_OK; +} + +NS_MSG_BASE void MsgStripQuotedPrintable(nsCString& aSrc) { + // decode quoted printable text in place + + if (aSrc.IsEmpty()) return; + + char* src = aSrc.BeginWriting(); + char* dest = src; + int srcIdx = 0, destIdx = 0; + + while (src[srcIdx] != 0) { + // Decode sequence of '=XY' into a character with code XY. + if (src[srcIdx] == '=') { + if (MsgIsHex((const char*)src + srcIdx + 1, 2)) { + // If we got here, we successfully decoded a quoted printable sequence, + // so bump each pointer past it and move on to the next char. + dest[destIdx++] = MsgUnhex((const char*)src + srcIdx + 1, 2); + srcIdx += 3; + } else { + // If first char after '=' isn't hex check if it's a normal char + // or a soft line break. If it's a soft line break, eat the + // CR/LF/CRLF. + if (src[srcIdx + 1] == '\r' || src[srcIdx + 1] == '\n') { + srcIdx++; // soft line break, ignore the '='; + if (src[srcIdx] == '\r' || src[srcIdx] == '\n') { + srcIdx++; + if (src[srcIdx] == '\n') srcIdx++; + } + } else // The first or second char after '=' isn't hex, just copy the + // '='. + { + dest[destIdx++] = src[srcIdx++]; + } + continue; + } + } else + dest[destIdx++] = src[srcIdx++]; + } + + dest[destIdx] = src[srcIdx]; // null terminate + aSrc.SetLength(destIdx); +} + +NS_MSG_BASE nsresult MsgEscapeString(const nsACString& aStr, uint32_t aType, + nsACString& aResult) { + nsresult rv; + nsCOMPtr<nsINetUtil> nu = do_GetService(NS_NETUTIL_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return nu->EscapeString(aStr, aType, aResult); +} + +NS_MSG_BASE nsresult MsgUnescapeString(const nsACString& aStr, uint32_t aFlags, + nsACString& aResult) { + nsresult rv; + nsCOMPtr<nsINetUtil> nu = do_GetService(NS_NETUTIL_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return nu->UnescapeString(aStr, aFlags, aResult); +} + +NS_MSG_BASE nsresult MsgEscapeURL(const nsACString& aStr, uint32_t aFlags, + nsACString& aResult) { + nsresult rv; + nsCOMPtr<nsINetUtil> nu = do_GetService(NS_NETUTIL_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return nu->EscapeURL(aStr, aFlags, aResult); +} + +NS_MSG_BASE nsresult +MsgGetHeadersFromKeys(nsIMsgDatabase* aDB, const nsTArray<nsMsgKey>& aMsgKeys, + nsTArray<RefPtr<nsIMsgDBHdr>>& aHeaders) { + NS_ENSURE_ARG_POINTER(aDB); + aHeaders.Clear(); + aHeaders.SetCapacity(aMsgKeys.Length()); + + for (auto key : aMsgKeys) { + // This function silently skips when the key is not found. This is an + // expected case. + bool hasKey; + nsresult rv = aDB->ContainsKey(key, &hasKey); + NS_ENSURE_SUCCESS(rv, rv); + if (hasKey) { + nsCOMPtr<nsIMsgDBHdr> msgHdr; + rv = aDB->GetMsgHdrForKey(key, getter_AddRefs(msgHdr)); + NS_ENSURE_SUCCESS(rv, rv); + aHeaders.AppendElement(msgHdr); + } + } + return NS_OK; +} + +bool MsgAdvanceToNextLine(const char* buffer, uint32_t& bufferOffset, + uint32_t maxBufferOffset) { + bool result = false; + for (; bufferOffset < maxBufferOffset; bufferOffset++) { + if (buffer[bufferOffset] == '\r' || buffer[bufferOffset] == '\n') { + bufferOffset++; + if (buffer[bufferOffset - 1] == '\r' && buffer[bufferOffset] == '\n') + bufferOffset++; + result = true; + break; + } + } + return result; +} + +NS_MSG_BASE nsresult MsgExamineForProxyAsync(nsIChannel* channel, + nsIProtocolProxyCallback* listener, + nsICancelable** result) { + nsresult rv; + +#ifdef DEBUG + nsCOMPtr<nsIURI> uri; + rv = channel->GetURI(getter_AddRefs(uri)); + NS_ASSERTION(NS_SUCCEEDED(rv) && uri, + "The URI needs to be set before calling the proxy service"); +#endif + + nsCOMPtr<nsIProtocolProxyService> pps = + do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + return pps->AsyncResolve(channel, 0, listener, nullptr, result); +} + +NS_MSG_BASE nsresult MsgPromptLoginFailed(nsIMsgWindow* aMsgWindow, + const nsACString& aHostname, + const nsACString& aUsername, + const nsAString& aAccountname, + int32_t* aResult) { + nsCOMPtr<mozIDOMWindowProxy> domWindow; + if (aMsgWindow) { + aMsgWindow->GetDomWindow(getter_AddRefs(domWindow)); + } + + nsresult rv; + nsCOMPtr<nsIPromptService> dlgService( + do_GetService(NS_PROMPTSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIStringBundleService> bundleSvc = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleSvc, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIStringBundle> bundle; + rv = bundleSvc->CreateBundle("chrome://messenger/locale/messenger.properties", + getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + nsString message; + AutoTArray<nsString, 2> formatStrings; + CopyUTF8toUTF16(aHostname, *formatStrings.AppendElement()); + CopyUTF8toUTF16(aUsername, *formatStrings.AppendElement()); + + rv = bundle->FormatStringFromName("mailServerLoginFailed2", formatStrings, + message); + NS_ENSURE_SUCCESS(rv, rv); + + nsString title; + if (aAccountname.IsEmpty()) { + // Account name may be empty e.g. on a SMTP server. + rv = bundle->GetStringFromName("mailServerLoginFailedTitle", title); + } else { + AutoTArray<nsString, 1> formatStrings = {nsString(aAccountname)}; + rv = bundle->FormatStringFromName("mailServerLoginFailedTitleWithAccount", + formatStrings, title); + } + NS_ENSURE_SUCCESS(rv, rv); + + nsString button0; + rv = bundle->GetStringFromName("mailServerLoginFailedRetryButton", button0); + NS_ENSURE_SUCCESS(rv, rv); + + nsString button2; + rv = bundle->GetStringFromName("mailServerLoginFailedEnterNewPasswordButton", + button2); + NS_ENSURE_SUCCESS(rv, rv); + + bool dummyValue = false; + return dlgService->ConfirmEx( + domWindow, title.get(), message.get(), + (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_0) + + (nsIPrompt::BUTTON_TITLE_CANCEL * nsIPrompt::BUTTON_POS_1) + + (nsIPrompt::BUTTON_TITLE_IS_STRING * nsIPrompt::BUTTON_POS_2), + button0.get(), nullptr, button2.get(), nullptr, &dummyValue, aResult); +} + +NS_MSG_BASE PRTime MsgConvertAgeInDaysToCutoffDate(int32_t ageInDays) { + PRTime now = PR_Now(); + + return now - PR_USEC_PER_DAY * ageInDays; +} + +NS_MSG_BASE nsresult +MsgTermListToString(nsTArray<RefPtr<nsIMsgSearchTerm>> const& aTermList, + nsCString& aOutString) { + nsresult rv = NS_OK; + for (nsIMsgSearchTerm* term : aTermList) { + nsAutoCString stream; + + if (aOutString.Length() > 1) aOutString += ' '; + + bool booleanAnd; + bool matchAll; + term->GetBooleanAnd(&booleanAnd); + term->GetMatchAll(&matchAll); + if (matchAll) { + aOutString += "ALL"; + continue; + } else if (booleanAnd) + aOutString += "AND ("; + else + aOutString += "OR ("; + + rv = term->GetTermAsString(stream); + NS_ENSURE_SUCCESS(rv, rv); + + aOutString += stream; + aOutString += ')'; + } + return rv; +} + +NS_MSG_BASE uint64_t ParseUint64Str(const char* str) { +#ifdef XP_WIN + { + char* endPtr; + return _strtoui64(str, &endPtr, 10); + } +#else + return strtoull(str, nullptr, 10); +#endif +} + +NS_MSG_BASE uint64_t MsgUnhex(const char* aHexString, size_t aNumChars) { + // Large numbers will not fit into uint64_t. + NS_ASSERTION(aNumChars <= 16, "Hex literal too long to convert!"); + + uint64_t result = 0; + for (size_t i = 0; i < aNumChars; i++) { + unsigned char c = aHexString[i]; + uint8_t digit; + if ((c >= '0') && (c <= '9')) + digit = (c - '0'); + else if ((c >= 'a') && (c <= 'f')) + digit = ((c - 'a') + 10); + else if ((c >= 'A') && (c <= 'F')) + digit = ((c - 'A') + 10); + else + break; + + result = (result << 4) | digit; + } + + return result; +} + +NS_MSG_BASE bool MsgIsHex(const char* aHexString, size_t aNumChars) { + for (size_t i = 0; i < aNumChars; i++) { + if (!isxdigit(aHexString[i])) return false; + } + return true; +} + +NS_MSG_BASE nsresult MsgStreamMsgHeaders(nsIInputStream* aInputStream, + nsIStreamListener* aConsumer) { + mozilla::UniquePtr<nsLineBuffer<char>> lineBuffer(new nsLineBuffer<char>); + + nsresult rv; + + nsAutoCString msgHeaders; + nsAutoCString curLine; + + bool more = true; + + // We want to NS_ReadLine until we get to a blank line (the end of the + // headers) + while (more) { + rv = NS_ReadLine(aInputStream, lineBuffer.get(), curLine, &more); + NS_ENSURE_SUCCESS(rv, rv); + if (curLine.IsEmpty()) break; + msgHeaders.Append(curLine); + msgHeaders.AppendLiteral("\r\n"); + } + lineBuffer.reset(); + nsCOMPtr<nsIStringInputStream> hdrsStream = + do_CreateInstance("@mozilla.org/io/string-input-stream;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + hdrsStream->SetData(msgHeaders.get(), msgHeaders.Length()); + + nsCOMPtr<nsIInputStreamPump> pump; + rv = NS_NewInputStreamPump(getter_AddRefs(pump), hdrsStream.forget()); + NS_ENSURE_SUCCESS(rv, rv); + + return pump->AsyncRead(aConsumer); +} + +NS_MSG_BASE nsresult MsgDetectCharsetFromFile(nsIFile* aFile, + nsACString& aCharset) { + // We do the detection in this order: + // Check BOM. + // If no BOM, run localized detection (Russian, Ukrainian or Japanese). + // We need to run this first, since ISO-2022-JP is 7bit ASCII and would be + // detected as UTF-8. If ISO-2022-JP not detected, check for UTF-8. If no + // UTF-8, but detector detected something, use that, otherwise return an + // error. + aCharset.Truncate(); + + nsresult rv; + nsCOMPtr<nsIInputStream> inputStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(inputStream), aFile); + NS_ENSURE_SUCCESS(rv, rv); + + // Check the BOM. + char sniffBuf[3]; + uint32_t numRead; + rv = inputStream->Read(sniffBuf, sizeof(sniffBuf), &numRead); + + if (numRead >= 2 && sniffBuf[0] == (char)0xfe && sniffBuf[1] == (char)0xff) { + aCharset = "UTF-16BE"; + } else if (numRead >= 2 && sniffBuf[0] == (char)0xff && + sniffBuf[1] == (char)0xfe) { + aCharset = "UTF-16LE"; + } else if (numRead >= 3 && sniffBuf[0] == (char)0xef && + sniffBuf[1] == (char)0xbb && sniffBuf[2] == (char)0xbf) { + aCharset = "UTF-8"; + } + if (!aCharset.IsEmpty()) return NS_OK; + + // Position back to the beginning. + nsCOMPtr<nsISeekableStream> seekStream = do_QueryInterface(inputStream); + if (seekStream) seekStream->Seek(nsISeekableStream::NS_SEEK_SET, 0); + + // Use detector. + mozilla::UniquePtr<mozilla::EncodingDetector> detector = + mozilla::EncodingDetector::Create(); + char buffer[1024]; + numRead = 0; + while (NS_SUCCEEDED(inputStream->Read(buffer, sizeof(buffer), &numRead))) { + mozilla::Span<const uint8_t> src = + mozilla::AsBytes(mozilla::Span(buffer, numRead)); + Unused << detector->Feed(src, false); + if (numRead == 0) { + break; + } + } + Unused << detector->Feed(nullptr, true); + auto encoding = detector->Guess(nullptr, true); + encoding->Name(aCharset); + return NS_OK; +} + +/* + * Converts a buffer to plain text. Some conversions may + * or may not work with certain end charsets which is why we + * need that as an argument to the function. If charset is + * unknown or deemed of no importance NULL could be passed. + */ +NS_MSG_BASE nsresult ConvertBufToPlainText(nsString& aConBuf, bool formatFlowed, + bool formatOutput, + bool disallowBreaks) { + if (aConBuf.IsEmpty()) return NS_OK; + + int32_t wrapWidth = 72; + nsCOMPtr<nsIPrefBranch> pPrefBranch(do_GetService(NS_PREFSERVICE_CONTRACTID)); + + if (pPrefBranch) { + pPrefBranch->GetIntPref("mailnews.wraplength", &wrapWidth); + // Let sanity reign! + if (wrapWidth == 0 || wrapWidth > 990) + wrapWidth = 990; + else if (wrapWidth < 10) + wrapWidth = 10; + } + + uint32_t converterFlags = nsIDocumentEncoder::OutputPersistNBSP; + if (formatFlowed) converterFlags |= nsIDocumentEncoder::OutputFormatFlowed; + if (formatOutput) converterFlags |= nsIDocumentEncoder::OutputFormatted; + if (disallowBreaks) + converterFlags |= nsIDocumentEncoder::OutputDisallowLineBreaking; + + nsCOMPtr<nsIParserUtils> utils = do_GetService(NS_PARSERUTILS_CONTRACTID); + return utils->ConvertToPlainText(aConBuf, converterFlags, wrapWidth, aConBuf); +} + +NS_MSG_BASE nsMsgKey msgKeyFromInt(uint32_t aValue) { return aValue; } + +NS_MSG_BASE nsMsgKey msgKeyFromInt(uint64_t aValue) { + NS_ASSERTION(aValue <= PR_UINT32_MAX, "Msg key value too big!"); + return aValue; +} + +NS_MSG_BASE uint32_t msgKeyToInt(nsMsgKey aMsgKey) { return (uint32_t)aMsgKey; } + +// Helper function to extract a query qualifier. +nsCString MsgExtractQueryPart(const nsACString& spec, + const char* queryToExtract) { + nsCString queryPart; + int32_t queryIndex = PromiseFlatCString(spec).Find(queryToExtract); + if (queryIndex == kNotFound) return queryPart; + + int32_t queryEnd = spec.FindChar('&', queryIndex + 1); + if (queryEnd == kNotFound) queryEnd = spec.FindChar('?', queryIndex + 1); + if (queryEnd == kNotFound) { + // Nothing follows, so return from where the query qualifier started. + queryPart.Assign(Substring(spec, queryIndex)); + } else { + // Return the substring that represents the query qualifier. + queryPart.Assign(Substring(spec, queryIndex, queryEnd - queryIndex)); + } + return queryPart; +} + +// Helper function to remove query part from URL spec or path. +void MsgRemoveQueryPart(nsCString& aSpec) { + // Sadly the query part can have different forms, these were seen + // "in the wild", even with two ?: + // /;section=2?part=1.2&filename=A01.JPG + // ?section=2?part=1.2&filename=A01.JPG&type=image/jpeg&filename=A01.JPG + // ?header=quotebody/;section=2.2?part=1.2.2&filename=lijbmghmkilicioj.png + // ?part=1.2&type=image/jpeg&filename=IMG_C0030.jpg + // ?header=quotebody&part=1.2&filename=lijbmghmkilicioj.png + + // Truncate path at the first of /; or ? + int32_t ind = aSpec.FindChar('?'); + if (ind != kNotFound) aSpec.SetLength(ind); + ind = aSpec.Find("/;"); + if (ind != kNotFound) aSpec.SetLength(ind); +} + +// Perform C-style string escaping. +// e.g. "foo\r\n" => "foo\\r\\n" +// (See also CEscape(), in protobuf, for similar function). +nsCString CEscapeString(nsACString const& s) { + nsCString out; + for (size_t i = 0; i < s.Length(); ++i) { + char c = s[i]; + if (c & 0x80) { + out.AppendPrintf("\\x%02x", (uint8_t)c); + continue; + } + switch (c) { + case '\a': + out += "\\a"; + break; + case '\b': + out += "\\b"; + break; + case '\f': + out += "\\f"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + case '\v': + out += "\\v"; + break; + default: + if (c < ' ') { + out.AppendPrintf("\\x%02x", (uint8_t)c); + } else { + out += c; + } + break; + } + } + return out; +} + +nsresult SyncCopyStream(nsIInputStream* src, nsIOutputStream* dest, + uint64_t& bytesCopied, size_t bufSize) { + mozilla::Buffer<char> buf(bufSize); + nsresult rv; + + bytesCopied = 0; + while (1) { + uint32_t numRead; + rv = src->Read(buf.Elements(), buf.Length(), &numRead); + NS_ENSURE_SUCCESS(rv, rv); + if (numRead == 0) { + break; // EOF. + } + uint32_t pos = 0; + while (pos < numRead) { + uint32_t n; + rv = dest->Write(&buf[pos], numRead - pos, &n); + NS_ENSURE_SUCCESS(rv, rv); + pos += n; + bytesCopied += n; + } + } + return NS_OK; +} + +// Used for "@mozilla.org/network/sync-stream-listener;1". +already_AddRefed<nsIStreamListener> SyncStreamListenerCreate() { + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr<nsIStreamListener> listener; + nsCOMPtr<nsIInputStream> stream; + nsresult rv = NS_NewSyncStreamListener(getter_AddRefs(listener), + getter_AddRefs(stream)); + NS_ENSURE_SUCCESS(rv, nullptr); + return listener.forget(); +} + +// Determine if folder1 and folder2 reside on the same server +nsresult IsOnSameServer(nsIMsgFolder* folder1, nsIMsgFolder* folder2, + bool* sameServer) { + NS_ENSURE_ARG_POINTER(folder1); + NS_ENSURE_ARG_POINTER(folder2); + NS_ENSURE_ARG_POINTER(sameServer); + + nsCOMPtr<nsIMsgIncomingServer> server1; + nsresult rv = folder1->GetServer(getter_AddRefs(server1)); + NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER); + + nsCOMPtr<nsIMsgIncomingServer> server2; + rv = folder2->GetServer(getter_AddRefs(server2)); + NS_ENSURE_SUCCESS(rv, NS_MSG_INVALID_OR_MISSING_SERVER); + + NS_ENSURE_TRUE(server2, NS_ERROR_NULL_POINTER); + return server2->Equals(server1, sameServer); +} diff --git a/comm/mailnews/base/src/nsMsgUtils.h b/comm/mailnews/base/src/nsMsgUtils.h new file mode 100644 index 0000000000..6f289d4b37 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgUtils.h @@ -0,0 +1,462 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _NSMSGUTILS_H +#define _NSMSGUTILS_H + +#include "nsIURL.h" +#include "nsString.h" +#include "msgCore.h" +#include "nsCOMPtr.h" +#include "MailNewsTypes2.h" +#include "nsTArray.h" +#include "nsInterfaceRequestorAgg.h" +#include "nsILoadGroup.h" +#include "nsINetUtil.h" +#include "nsIRequest.h" +#include "nsILoadInfo.h" +#include "nsServiceManagerUtils.h" +#include "nsUnicharUtils.h" +#include "nsIFile.h" + +class nsIChannel; +class nsIFile; +class nsIPrefBranch; +class nsIMsgFolder; +class nsIMsgMessageService; +class nsIUrlListener; +class nsIOutputStream; +class nsIInputStream; +class nsIMsgDatabase; +class nsIProxyInfo; +class nsIMsgWindow; +class nsIStreamListener; +class nsICancelable; +class nsIProtocolProxyCallback; +class nsIMsgSearchTerm; + +#define FILE_IO_BUFFER_SIZE (16 * 1024) +#define MSGS_URL "chrome://messenger/locale/messenger.properties" + +enum nsDateFormatSelectorComm : long { + kDateFormatNone = 0, + kDateFormatLong = 1, + kDateFormatShort = 2, + kDateFormatUnused = 3, + kDateFormatWeekday = 4 +}; + +// These are utility functions that can used throughout the mailnews code + +NS_MSG_BASE nsresult GetMessageServiceContractIDForURI(const char* uri, + nsCString& contractID); + +NS_MSG_BASE nsresult GetMessageServiceFromURI( + const nsACString& uri, nsIMsgMessageService** aMessageService); + +NS_MSG_BASE nsresult GetMsgDBHdrFromURI(const nsACString& uri, + nsIMsgDBHdr** msgHdr); + +NS_MSG_BASE nsresult NS_MsgGetPriorityFromString( + const char* const priority, nsMsgPriorityValue& outPriority); + +NS_MSG_BASE nsresult NS_MsgGetPriorityValueString(const nsMsgPriorityValue p, + nsACString& outValueString); + +NS_MSG_BASE nsresult NS_MsgGetUntranslatedPriorityName( + const nsMsgPriorityValue p, nsACString& outName); + +NS_MSG_BASE nsresult NS_MsgHashIfNecessary(nsAutoString& name); +NS_MSG_BASE nsresult NS_MsgHashIfNecessary(nsAutoCString& name); + +NS_MSG_BASE nsresult FormatFileSize(int64_t size, bool useKB, + nsAString& formattedSize); + +/** + * given a folder uri, return the path to folder in the user profile directory. + * + * @param aFolderURI uri of folder we want the path to, without the scheme + * @param[out] aPathString result path string + * @param aScheme scheme of the uri + * @param[optional] aIsNewsFolder is this a news folder? + */ +NS_MSG_BASE nsresult NS_MsgCreatePathStringFromFolderURI( + const char* aFolderURI, nsCString& aPathString, const nsCString& aScheme, + bool aIsNewsFolder = false); + +/** + * Given a string and a length, removes any "Re:" strings from the front. + * It also deals with that dumbass "Re[2]:" thing that some losing mailers do. + * + * If mailnews.localizedRe is set, it will also remove localized "Re:" strings. + * + * @return true if it made a change (in which case the caller should look to + * modifiedSubject for the result) and false otherwise (in which + * case the caller should look at subject for the result) + */ +NS_MSG_BASE bool NS_MsgStripRE(const nsCString& subject, + nsCString& modifiedSubject); + +NS_MSG_BASE char* NS_MsgSACopy(char** destination, const char* source); + +NS_MSG_BASE char* NS_MsgSACat(char** destination, const char* source); + +NS_MSG_BASE nsresult NS_MsgEscapeEncodeURLPath(const nsAString& aStr, + nsCString& aResult); + +NS_MSG_BASE nsresult NS_MsgDecodeUnescapeURLPath(const nsACString& aPath, + nsAString& aResult); + +NS_MSG_BASE bool WeAreOffline(); + +// Get a folder by Uri, returning null if it doesn't exist (or if some +// error occurs). A missing folder is not considered an error. +NS_MSG_BASE nsresult FindFolder(const nsACString& aFolderURI, + nsIMsgFolder** aFolder); + +// Get a folder by Uri. +// A missing folder is considered to be an error. +// Returns a non-null folder if and only if result is NS_OK. +NS_MSG_BASE nsresult GetExistingFolder(const nsACString& aFolderURI, + nsIMsgFolder** aFolder); + +// Get a folder by Uri, creating it if it doesn't already exist. +// An error is returned if a folder cannot be found or created. +// Created folders will be 'dangling' folders (ie not connected to a +// parent). +NS_MSG_BASE nsresult GetOrCreateFolder(const nsACString& aFolderURI, + nsIMsgFolder** aFolder); + +// Escape lines starting with "From ", ">From ", etc. in a buffer. +NS_MSG_BASE nsresult EscapeFromSpaceLine(nsIOutputStream* ouputStream, + char* start, const char* end); +NS_MSG_BASE bool IsAFromSpaceLine(char* start, const char* end); + +NS_MSG_BASE nsresult NS_GetPersistentFile( + const char* relPrefName, const char* absPrefName, + const char* dirServiceProp, // Can be NULL + bool& gotRelPref, nsIFile** aFile, nsIPrefBranch* prefBranch = nullptr); + +NS_MSG_BASE nsresult NS_SetPersistentFile(const char* relPrefName, + const char* absPrefName, + nsIFile* aFile, + nsIPrefBranch* prefBranch = nullptr); + +NS_MSG_BASE nsresult IsRFC822HeaderFieldName(const char* aHdr, bool* aResult); + +NS_MSG_BASE nsresult NS_GetUnicharPreferenceWithDefault( + nsIPrefBranch* prefBranch, // can be null, if so uses the root branch + const char* prefName, const nsAString& defValue, nsAString& prefValue); + +NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreferenceWithDefault( + nsIPrefBranch* prefBranch, // can be null, if so uses the root branch + const char* prefName, const nsAString& defValue, nsAString& prefValue); + +NS_MSG_BASE nsresult NS_GetLocalizedUnicharPreference( + nsIPrefBranch* prefBranch, // can be null, if so uses the root branch + const char* prefName, nsAString& prefValue); + +/** + * this needs a listener, because we might have to create the folder + * on the server, and that is asynchronous + */ +NS_MSG_BASE nsresult GetOrCreateJunkFolder(const nsACString& aURI, + nsIUrlListener* aListener); + +// Returns true if the nsIURI is a message under an RSS account +NS_MSG_BASE nsresult IsRSSArticle(nsIURI* aMsgURI, bool* aIsRSSArticle); + +// digest needs to be a pointer to a 16 byte buffer +#define DIGEST_LENGTH 16 + +NS_MSG_BASE nsresult MSGCramMD5(const char* text, int32_t text_len, + const char* key, int32_t key_len, + unsigned char* digest); +NS_MSG_BASE nsresult MSGApopMD5(const char* text, int32_t text_len, + const char* password, int32_t password_len, + unsigned char* digest); + +// helper functions to convert a 64bits PRTime into a 32bits value (compatible +// time_t) and vice versa. +NS_MSG_BASE void PRTime2Seconds(PRTime prTime, uint32_t* seconds); +NS_MSG_BASE void PRTime2Seconds(PRTime prTime, int32_t* seconds); +NS_MSG_BASE void Seconds2PRTime(uint32_t seconds, PRTime* prTime); +// helper function to generate current date+time as a string +NS_MSG_BASE void MsgGenerateNowStr(nsACString& nowStr); + +// Appends the correct summary file extension onto the supplied fileLocation +// and returns it in summaryLocation. +NS_MSG_BASE nsresult GetSummaryFileLocation(nsIFile* fileLocation, + nsIFile** summaryLocation); + +// Gets a special directory and appends the supplied file name onto it. +NS_MSG_BASE nsresult GetSpecialDirectoryWithFileName(const char* specialDirName, + const char* fileName, + nsIFile** result); + +// cleanup temp files with the given filename and extension, including +// the consecutive -NNNN ones that we can find. If there are holes, e.g., +// <filename>-1-10,12.<extension> exist, but <filename>-11.<extension> does not +// we'll clean up 1-10. If the leaks are common, I think the gaps will tend to +// be filled. +NS_MSG_BASE nsresult MsgCleanupTempFiles(const char* fileName, + const char* extension); + +NS_MSG_BASE nsresult MsgGetFileStream(nsIFile* file, + nsIOutputStream** fileStream); + +// Automatically creates an output stream with a suitable buffer +NS_MSG_BASE nsresult MsgNewBufferedFileOutputStream(nsIOutputStream** aResult, + nsIFile* aFile, + int32_t aIOFlags = -1, + int32_t aPerm = -1); + +// Automatically creates an output stream with a suitable buffer, but write to a +// temporary file first, then rename to aFile +NS_MSG_BASE nsresult +MsgNewSafeBufferedFileOutputStream(nsIOutputStream** aResult, nsIFile* aFile, + int32_t aIOFlags = -1, int32_t aPerm = -1); + +// fills in the position of the passed in keyword in the passed in keyword list +// and returns false if the keyword isn't present +NS_MSG_BASE bool MsgFindKeyword(const nsCString& keyword, nsCString& keywords, + int32_t* aStartOfKeyword, int32_t* aLength); + +NS_MSG_BASE bool MsgHostDomainIsTrusted(nsCString& host, + nsCString& trustedMailDomains); + +// gets an nsIFile from a UTF-8 file:// path +NS_MSG_BASE nsresult MsgGetLocalFileFromURI(const nsACString& aUTF8Path, + nsIFile** aFile); + +NS_MSG_BASE void MsgStripQuotedPrintable(nsCString& aSrc); + +/* + * Utility functions that call functions from nsINetUtil + */ + +NS_MSG_BASE nsresult MsgEscapeString(const nsACString& aStr, uint32_t aType, + nsACString& aResult); + +NS_MSG_BASE nsresult MsgUnescapeString(const nsACString& aStr, uint32_t aFlags, + nsACString& aResult); + +NS_MSG_BASE nsresult MsgEscapeURL(const nsACString& aStr, uint32_t aFlags, + nsACString& aResult); + +// Given a message db and a set of keys, fetch the corresponding message +// headers. +NS_MSG_BASE nsresult +MsgGetHeadersFromKeys(nsIMsgDatabase* aDB, const nsTArray<nsMsgKey>& aKeys, + nsTArray<RefPtr<nsIMsgDBHdr>>& aHeaders); + +NS_MSG_BASE nsresult MsgExamineForProxyAsync(nsIChannel* channel, + nsIProtocolProxyCallback* listener, + nsICancelable** result); + +NS_MSG_BASE int32_t MsgFindCharInSet(const nsCString& aString, + const char* aChars, uint32_t aOffset = 0); +NS_MSG_BASE int32_t MsgFindCharInSet(const nsString& aString, + const char16_t* aChars, + uint32_t aOffset = 0); + +// advances bufferOffset to the beginning of the next line, if we don't +// get to maxBufferOffset first. Returns false if we didn't get to the +// next line. +NS_MSG_BASE bool MsgAdvanceToNextLine(const char* buffer, + uint32_t& bufferOffset, + uint32_t maxBufferOffset); + +/** + * Alerts the user that the login to the server failed. Asks whether the + * connection should: retry, cancel, or request a new password. + * + * @param aMsgWindow The message window associated with this action (cannot + * be null). + * @param aHostname The hostname of the server for which the login failed. + * @param aResult The button pressed. 0 for retry, 1 for cancel, + * 2 for enter a new password. + * @return NS_OK for success, NS_ERROR_* if there was a failure in + * creating the dialog. + */ +NS_MSG_BASE nsresult MsgPromptLoginFailed(nsIMsgWindow* aMsgWindow, + const nsACString& aHostname, + const nsACString& aUsername, + const nsAString& aAccountname, + int32_t* aResult); + +/** + * Calculate a PRTime value used to determine if a date is XX + * days ago. This is used by various retention setting algorithms. + */ +NS_MSG_BASE PRTime MsgConvertAgeInDaysToCutoffDate(int32_t ageInDays); + +/** + * Converts the passed in term list to its string representation. + * + * @param aTermList Array of nsIMsgSearchTerms + * @param[out] aOutString result representation of search terms. + * + */ +NS_MSG_BASE nsresult MsgTermListToString( + nsTArray<RefPtr<nsIMsgSearchTerm>> const& aTermList, nsCString& aOutString); + +NS_MSG_BASE nsresult MsgStreamMsgHeaders(nsIInputStream* aInputStream, + nsIStreamListener* aConsumer); + +/** + * convert string to uint64_t + * + * @param str converted string + * @returns uint64_t value for success, 0 for parse failure + */ +NS_MSG_BASE uint64_t ParseUint64Str(const char* str); + +/** + * Detect charset of file + * + * @param aFile The target of nsIFile + * @param[out] aCharset The charset string + */ +NS_MSG_BASE nsresult MsgDetectCharsetFromFile(nsIFile* aFile, + nsACString& aCharset); + +/* + * Converts a buffer to plain text. Some conversions may + * or may not work with certain end charsets which is why we + * need that as an argument to the function. If charset is + * unknown or deemed of no importance NULL could be passed. + * @param[in/out] aConBuf Variable with the text to convert + * @param formatFlowed Use format flowed? + * @param formatOutput Reformat the output? + & @param disallowBreaks Disallow breaks when formatting + */ +NS_MSG_BASE nsresult ConvertBufToPlainText(nsString& aConBuf, bool formatFlowed, + bool formatOutput, + bool disallowBreaks); + +#include "nsEscape.h" + +/** + * Converts a hex string into an integer. + * Processes up to aNumChars characters or the first non-hex char. + * It is not an error if less than aNumChars valid hex digits are found. + */ +NS_MSG_BASE uint64_t MsgUnhex(const char* aHexString, size_t aNumChars); + +/** + * Checks if a string is a valid hex literal containing at least aNumChars + * digits. + */ +NS_MSG_BASE bool MsgIsHex(const char* aHexString, size_t aNumChars); + +/** + * Convert an uint32_t to a nsMsgKey. + * Currently they are mostly the same but we need to preserve the notion that + * nsMsgKey is an opaque value that can't be treated as a generic integer + * (except when storing it into the database). It enables type safety checks and + * may prevent coding errors. + */ +NS_MSG_BASE nsMsgKey msgKeyFromInt(uint32_t aValue); + +NS_MSG_BASE nsMsgKey msgKeyFromInt(uint64_t aValue); + +NS_MSG_BASE uint32_t msgKeyToInt(nsMsgKey aMsgKey); + +/** + * Helper function to extract query part from URL spec. + */ +nsCString MsgExtractQueryPart(const nsACString& spec, + const char* queryToExtract); +/** + * Helper function to remove query part from URL spec or path. + */ +void MsgRemoveQueryPart(nsCString& aSpec); + +/** + * Helper macro for defining getter/setters. Ported from nsISupportsObsolete.h + */ +#define NS_IMPL_GETSET(clazz, attr, type, member) \ + NS_IMETHODIMP clazz::Get##attr(type* result) { \ + NS_ENSURE_ARG_POINTER(result); \ + *result = member; \ + return NS_OK; \ + } \ + NS_IMETHODIMP clazz::Set##attr(type aValue) { \ + member = aValue; \ + return NS_OK; \ + } + +/** + * Macro and helper function for reporting an error, warning or + * informational message to the Error Console + * + * This will require the inclusion of the following files in the source file + * #include "nsIScriptError.h" + * #include "nsIConsoleService.h" + * + */ + +NS_MSG_BASE +void MsgLogToConsole4(const nsAString& aErrorText, const nsAString& aFilename, + uint32_t aLine, uint32_t flags); + +// Macro with filename and line number +#define MSG_LOG_TO_CONSOLE(_text, _flag) \ + MsgLogToConsole4(NS_LITERAL_STRING_FROM_CSTRING(_text), \ + NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, _flag) +#define MSG_LOG_ERR_TO_CONSOLE(_text) \ + MSG_LOG_TO_CONSOLE(_text, nsIScriptError::errorFlag) +#define MSG_LOG_WARN_TO_CONSOLE(_text) \ + MSG_LOG_TO_CONSOLE(_text, nsIScriptError::warningFlag) +#define MSG_LOG_INFO_TO_CONSOLE(_text) \ + MSG_LOG_TO_CONSOLE(_text, nsIScriptError::infoFlag) + +// Helper macros to cope with shoddy I/O error reporting (or lack thereof) +#define MSG_NS_ERROR(_txt) \ + do { \ + NS_ERROR(_txt); \ + MSG_LOG_ERR_TO_CONSOLE(_txt); \ + } while (0) +#define MSG_NS_WARNING(_txt) \ + do { \ + NS_WARNING(_txt); \ + MSG_LOG_WARN_TO_CONSOLE(_txt); \ + } while (0) +#define MSG_NS_WARN_IF_FALSE(_val, _txt) \ + do { \ + if (!(_val)) { \ + NS_WARNING(_txt); \ + MSG_LOG_WARN_TO_CONSOLE(_txt); \ + } \ + } while (0) +#define MSG_NS_INFO(_txt) \ + do { \ + MSG_LOCAL_INFO_TO_CONSOLE(_txt); \ + fprintf(stderr, "(info) %s (%s:%d)\n", _txt, __FILE__, __LINE__); \ + } while (0) + +/** + * Perform C-style string escaping. E.g. "foo\r\n" => "foo\\r\\n" + * This is primarily intended for debuggin purposes. + */ +nsCString CEscapeString(nsACString const& s); + +/** + * Synchronously copy the contents of src to dest, until EOF is encountered + * or an error occurs. + * The total number of bytes copied is returned in bytesCopied. + */ +nsresult SyncCopyStream(nsIInputStream* src, nsIOutputStream* dest, + uint64_t& bytesCopied, + size_t bufSize = FILE_IO_BUFFER_SIZE); + +// Used for "@mozilla.org/network/sync-stream-listener;1". +already_AddRefed<nsIStreamListener> SyncStreamListenerCreate(); + +nsresult IsOnSameServer(nsIMsgFolder* folder1, nsIMsgFolder* folder2, + bool* sameServer); + +#endif diff --git a/comm/mailnews/base/src/nsMsgWindow.cpp b/comm/mailnews/base/src/nsMsgWindow.cpp new file mode 100644 index 0000000000..1b0129d8f8 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgWindow.cpp @@ -0,0 +1,327 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsMsgWindow.h" +#include "nsIURILoader.h" +#include "nsCURILoader.h" +#include "nsIDocShell.h" +#include "nsIDocShellTreeItem.h" +#include "mozIDOMWindow.h" +#include "nsTransactionManagerCID.h" +#include "nsIComponentManager.h" +#include "nsILoadGroup.h" +#include "nsIMsgMailNewsUrl.h" +#include "nsIInterfaceRequestor.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIWebProgress.h" +#include "nsIWebProgressListener.h" +#include "nsPIDOMWindow.h" +#include "nsIPrompt.h" +#include "nsICharsetConverterManager.h" +#include "nsIChannel.h" +#include "nsIRequestObserver.h" +#include "netCore.h" +#include "prmem.h" +#include "plbase64.h" +#include "nsMsgI18N.h" +#include "nsIWebNavigation.h" +#include "nsContentUtils.h" +#include "nsComponentManagerUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIAuthPrompt.h" +#include "nsMsgUtils.h" +#include "mozilla/dom/Document.h" +#include "mozilla/TransactionManager.h" +#include "mozilla/dom/LoadURIOptionsBinding.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/XULFrameElement.h" +#include "nsFrameLoader.h" + +NS_IMPL_ISUPPORTS(nsMsgWindow, nsIMsgWindow, nsIURIContentListener, + nsISupportsWeakReference) + +nsMsgWindow::nsMsgWindow() { + mCharsetOverride = false; + m_stopped = false; +} + +nsMsgWindow::~nsMsgWindow() { CloseWindow(); } + +nsresult nsMsgWindow::Init() { + // create Undo/Redo Transaction Manager + mTransactionManager = new mozilla::TransactionManager(); + return mTransactionManager->SetMaxTransactionCount(-1); +} + +NS_IMETHODIMP nsMsgWindow::GetMessageWindowDocShell(nsIDocShell** aDocShell) { + *aDocShell = nullptr; + + nsCOMPtr<nsIDocShell> docShell(do_QueryReferent(mMessageWindowDocShellWeak)); + nsCOMPtr<nsIDocShell> rootShell(do_QueryReferent(mRootDocShellWeak)); + if (rootShell) { + // There seem to be some issues with shutdown (see Bug 1610406). + // This workaround should prevent the GetElementById() call dying horribly + // but really, we shouldn't even get here in such cases. + bool doomed; + rootShell->IsBeingDestroyed(&doomed); + if (doomed) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + + RefPtr<mozilla::dom::Element> el = + rootShell->GetDocument()->GetElementById(u"messagepane"_ns); + RefPtr<mozilla::dom::XULFrameElement> frame = + mozilla::dom::XULFrameElement::FromNodeOrNull(el); + NS_ENSURE_TRUE(frame, NS_ERROR_FAILURE); + RefPtr<mozilla::dom::Document> doc = frame->GetContentDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + docShell = doc->GetDocShell(); + NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); + + // we don't own mMessageWindowDocShell so don't try to keep a reference to + // it! + mMessageWindowDocShellWeak = do_GetWeakReference(docShell); + } + + NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); + docShell.forget(aDocShell); + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::CloseWindow() { + mStatusFeedback = nullptr; + + StopUrls(); + + nsCOMPtr<nsIDocShell> messagePaneDocShell( + do_QueryReferent(mMessageWindowDocShellWeak)); + if (messagePaneDocShell) { + nsCOMPtr<nsIURIContentListener> listener( + do_GetInterface(messagePaneDocShell)); + if (listener) listener->SetParentContentListener(nullptr); + SetRootDocShell(nullptr); + mMessageWindowDocShellWeak = nullptr; + } + + // in case nsMsgWindow leaks, make sure other stuff doesn't leak. + mTransactionManager = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::GetStatusFeedback( + nsIMsgStatusFeedback** aStatusFeedback) { + NS_ENSURE_ARG_POINTER(aStatusFeedback); + NS_IF_ADDREF(*aStatusFeedback = mStatusFeedback); + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::SetStatusFeedback( + nsIMsgStatusFeedback* aStatusFeedback) { + mStatusFeedback = aStatusFeedback; + nsCOMPtr<nsIDocShell> messageWindowDocShell; + GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell)); + + // register our status feedback object as a web progress listener + nsCOMPtr<nsIWebProgress> webProgress(do_GetInterface(messageWindowDocShell)); + if (webProgress && mStatusFeedback && messageWindowDocShell) { + nsCOMPtr<nsIWebProgressListener> webProgressListener = + do_QueryInterface(mStatusFeedback); + webProgress->AddProgressListener(webProgressListener, + nsIWebProgress::NOTIFY_ALL); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::GetTransactionManager( + nsITransactionManager** aTransactionManager) { + NS_ENSURE_ARG_POINTER(aTransactionManager); + NS_IF_ADDREF(*aTransactionManager = mTransactionManager); + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::SetTransactionManager( + nsITransactionManager* aTransactionManager) { + mTransactionManager = aTransactionManager; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::GetOpenFolder(nsIMsgFolder** aOpenFolder) { + NS_ENSURE_ARG_POINTER(aOpenFolder); + NS_IF_ADDREF(*aOpenFolder = mOpenFolder); + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::SetOpenFolder(nsIMsgFolder* aOpenFolder) { + mOpenFolder = aOpenFolder; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::GetRootDocShell(nsIDocShell** aDocShell) { + if (mRootDocShellWeak) + CallQueryReferent(mRootDocShellWeak.get(), aDocShell); + else + *aDocShell = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::SetRootDocShell(nsIDocShell* aDocShell) { + // Query for the doc shell and release it + mRootDocShellWeak = nullptr; + if (aDocShell) { + mRootDocShellWeak = do_GetWeakReference(aDocShell); + + nsCOMPtr<nsIDocShell> messagePaneDocShell; + GetMessageWindowDocShell(getter_AddRefs(messagePaneDocShell)); + nsCOMPtr<nsIURIContentListener> listener( + do_GetInterface(messagePaneDocShell)); + if (listener) listener->SetParentContentListener(this); + } + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::GetDomWindow(mozIDOMWindowProxy** aWindow) { + NS_ENSURE_ARG_POINTER(aWindow); + if (mDomWindow) + CallQueryReferent(mDomWindow.get(), aWindow); + else + *aWindow = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::SetDomWindow(mozIDOMWindowProxy* aWindow) { + NS_ENSURE_ARG_POINTER(aWindow); + mDomWindow = do_GetWeakReference(aWindow); + + nsCOMPtr<nsPIDOMWindowOuter> win = nsPIDOMWindowOuter::From(aWindow); + nsIDocShell* docShell = nullptr; + if (win) docShell = win->GetDocShell(); + + nsCOMPtr<nsIDocShellTreeItem> docShellAsItem(docShell); + + if (docShellAsItem) { + nsCOMPtr<nsIDocShellTreeItem> rootAsItem; + docShellAsItem->GetInProcessSameTypeRootTreeItem( + getter_AddRefs(rootAsItem)); + + nsCOMPtr<nsIDocShell> rootAsShell(do_QueryInterface(rootAsItem)); + SetRootDocShell(rootAsShell); + + // force ourselves to figure out the message pane + nsCOMPtr<nsIDocShell> messageWindowDocShell; + GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell)); + } + + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::SetNotificationCallbacks( + nsIInterfaceRequestor* aNotificationCallbacks) { + mNotificationCallbacks = aNotificationCallbacks; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::GetNotificationCallbacks( + nsIInterfaceRequestor** aNotificationCallbacks) { + NS_ENSURE_ARG_POINTER(aNotificationCallbacks); + NS_IF_ADDREF(*aNotificationCallbacks = mNotificationCallbacks); + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::StopUrls() { + m_stopped = true; + nsCOMPtr<nsIWebNavigation> webnav(do_QueryReferent(mRootDocShellWeak)); + return webnav ? webnav->Stop(nsIWebNavigation::STOP_NETWORK) + : NS_ERROR_FAILURE; +} + +// nsIURIContentListener support + +NS_IMETHODIMP nsMsgWindow::DoContent(const nsACString& aContentType, + bool aIsContentPreferred, + nsIRequest* request, + nsIStreamListener** aContentHandler, + bool* aAbortProcess) { + if (!aContentType.IsEmpty()) { + // forward the DoContent call to our docshell + nsCOMPtr<nsIDocShell> messageWindowDocShell; + GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell)); + nsCOMPtr<nsIURIContentListener> ctnListener = + do_QueryInterface(messageWindowDocShell); + if (ctnListener) { + nsCOMPtr<nsIChannel> aChannel = do_QueryInterface(request); + if (!aChannel) return NS_ERROR_FAILURE; + + // get the url for the channel...let's hope it is a mailnews url so we can + // set our msg hdr sink on it.. right now, this is the only way I can + // think of to force the msg hdr sink into the mime converter so it can + // get too it later... + nsCOMPtr<nsIURI> uri; + aChannel->GetURI(getter_AddRefs(uri)); + if (uri) { + nsCOMPtr<nsIMsgMailNewsUrl> mailnewsUrl(do_QueryInterface(uri)); + if (mailnewsUrl) mailnewsUrl->SetMsgWindow(this); + } + return ctnListener->DoContent(aContentType, aIsContentPreferred, request, + aContentHandler, aAbortProcess); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgWindow::IsPreferred(const char* aContentType, char** aDesiredContentType, + bool* aCanHandleContent) { + // We don't want to handle opening any attachments inside the + // message pane, but want to let nsIExternalHelperAppService take care. + *aCanHandleContent = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::CanHandleContent(const char* aContentType, + bool aIsContentPreferred, + char** aDesiredContentType, + bool* aCanHandleContent) + +{ + // the mail window knows nothing about the default content types + // its docshell can handle...ask the content area if it can handle + // the content type... + + nsCOMPtr<nsIDocShell> messageWindowDocShell; + GetMessageWindowDocShell(getter_AddRefs(messageWindowDocShell)); + nsCOMPtr<nsIURIContentListener> ctnListener( + do_GetInterface(messageWindowDocShell)); + if (ctnListener) + return ctnListener->CanHandleContent(aContentType, aIsContentPreferred, + aDesiredContentType, + aCanHandleContent); + else + *aCanHandleContent = false; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::GetParentContentListener( + nsIURIContentListener** aParent) { + NS_ENSURE_ARG_POINTER(aParent); + *aParent = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::SetParentContentListener( + nsIURIContentListener* aParent) { + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::GetLoadCookie(nsISupports** aLoadCookie) { + NS_ENSURE_ARG_POINTER(aLoadCookie); + *aLoadCookie = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsMsgWindow::SetLoadCookie(nsISupports* aLoadCookie) { + return NS_OK; +} + +NS_IMPL_GETSET(nsMsgWindow, Stopped, bool, m_stopped) diff --git a/comm/mailnews/base/src/nsMsgWindow.h b/comm/mailnews/base/src/nsMsgWindow.h new file mode 100644 index 0000000000..1ca2370552 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgWindow.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgWindow_h +#define _nsMsgWindow_h + +#include "nsIMsgWindow.h" +#include "nsIMsgStatusFeedback.h" +#include "nsITransactionManager.h" +#include "nsIMsgFolder.h" +#include "nsCOMPtr.h" +#include "nsIDocShell.h" +#include "nsIURIContentListener.h" +#include "nsWeakReference.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIInterfaceRequestor.h" + +class nsMsgWindow : public nsIMsgWindow, + public nsIURIContentListener, + public nsSupportsWeakReference { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + nsMsgWindow(); + nsresult Init(); + NS_DECL_NSIMSGWINDOW + NS_DECL_NSIURICONTENTLISTENER + + protected: + virtual ~nsMsgWindow(); + nsCOMPtr<nsIMsgStatusFeedback> mStatusFeedback; + nsCOMPtr<nsITransactionManager> mTransactionManager; + nsCOMPtr<nsIMsgFolder> mOpenFolder; + // These are used by the backend protocol code to attach + // notification callbacks to channels, e.g., nsIBadCertListner2. + nsCOMPtr<nsIInterfaceRequestor> mNotificationCallbacks; + // authorization prompt used during testing only + nsCOMPtr<nsIAuthPrompt> mAuthPrompt; + + // let's not make this a strong ref - we don't own it. + nsWeakPtr mRootDocShellWeak; + nsWeakPtr mMessageWindowDocShellWeak; + nsWeakPtr mDomWindow; + + nsCString mMailCharacterSet; + bool mCharsetOverride; + bool m_stopped; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgXFViewThread.cpp b/comm/mailnews/base/src/nsMsgXFViewThread.cpp new file mode 100644 index 0000000000..0180a7c910 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgXFViewThread.cpp @@ -0,0 +1,444 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgXFViewThread.h" +#include "nsMsgSearchDBView.h" +#include "nsMsgMessageFlags.h" + +NS_IMPL_ISUPPORTS(nsMsgXFViewThread, nsIMsgThread) + +nsMsgXFViewThread::nsMsgXFViewThread(nsMsgSearchDBView* view, + nsMsgKey threadId) { + m_numUnreadChildren = 0; + m_numChildren = 0; + m_flags = 0; + m_newestMsgDate = 0; + m_view = view; + m_threadId = threadId; +} + +nsMsgXFViewThread::~nsMsgXFViewThread() {} + +NS_IMETHODIMP +nsMsgXFViewThread::SetThreadKey(nsMsgKey threadKey) { + m_threadId = threadKey; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetThreadKey(nsMsgKey* aResult) { + NS_ENSURE_ARG_POINTER(aResult); + *aResult = m_threadId; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetFlags(uint32_t* aFlags) { + NS_ENSURE_ARG_POINTER(aFlags); + *aFlags = m_flags; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::SetFlags(uint32_t aFlags) { + m_flags = aFlags; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::SetSubject(const nsACString& aSubject) { + NS_ASSERTION(false, "shouldn't call this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetSubject(nsACString& result) { + NS_ASSERTION(false, "shouldn't call this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetNumChildren(uint32_t* aNumChildren) { + NS_ENSURE_ARG_POINTER(aNumChildren); + *aNumChildren = m_keys.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetNumUnreadChildren(uint32_t* aNumUnreadChildren) { + NS_ENSURE_ARG_POINTER(aNumUnreadChildren); + *aNumUnreadChildren = m_numUnreadChildren; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::AddChild(nsIMsgDBHdr* aNewHdr, nsIMsgDBHdr* aInReplyTo, + bool aThreadInThread, + nsIDBChangeAnnouncer* aAnnouncer) { + uint32_t whereInserted; + return AddHdr(aNewHdr, false, whereInserted, nullptr); +} + +// Returns the parent of the newly added header. If reparentChildren +// is true, we believe that the new header is a parent of an existing +// header, and we should find it, and reparent it. +nsresult nsMsgXFViewThread::AddHdr(nsIMsgDBHdr* newHdr, bool reparentChildren, + uint32_t& whereInserted, + nsIMsgDBHdr** outParent) { + nsCOMPtr<nsIMsgFolder> newHdrFolder; + newHdr->GetFolder(getter_AddRefs(newHdrFolder)); + + uint32_t newHdrFlags = 0; + uint32_t msgDate; + nsMsgKey newHdrKey = 0; + + newHdr->GetMessageKey(&newHdrKey); + newHdr->GetDateInSeconds(&msgDate); + newHdr->GetFlags(&newHdrFlags); + if (msgDate > m_newestMsgDate) SetNewestMsgDate(msgDate); + + if (newHdrFlags & nsMsgMessageFlags::Watched) + SetFlags(m_flags | nsMsgMessageFlags::Watched); + + ChangeChildCount(1); + if (!(newHdrFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(1); + + if (m_numChildren == 1) { + m_keys.InsertElementAt(0, newHdrKey); + m_levels.InsertElementAt(0, 0); + m_folders.InsertObjectAt(newHdrFolder, 0); + if (outParent) *outParent = nullptr; + + whereInserted = 0; + return NS_OK; + } + + // Find our parent, if any, in the thread. Starting at the newest + // reference, and working our way back, see if we've mapped that reference + // to this thread. + uint16_t numReferences; + newHdr->GetNumReferences(&numReferences); + nsCOMPtr<nsIMsgDBHdr> parent; + int32_t parentIndex = -1; + + for (int32_t i = numReferences - 1; i >= 0; i--) { + nsAutoCString reference; + newHdr->GetStringReference(i, reference); + if (reference.IsEmpty()) break; + + // I could look for the thread from the reference, but getting + // the header directly should be fine. If it's not, that means + // that the parent isn't in this thread, though it should be. + m_view->GetMsgHdrFromHash(reference, getter_AddRefs(parent)); + if (parent) { + parentIndex = HdrIndex(parent); + if (parentIndex == -1) { + NS_ERROR("how did we get in the wrong thread?"); + parent = nullptr; + } + + break; + } + } + + if (parent) { + uint32_t parentLevel = m_levels[parentIndex]; + nsMsgKey parentKey; + parent->GetMessageKey(&parentKey); + nsCOMPtr<nsIMsgFolder> parentFolder; + parent->GetFolder(getter_AddRefs(parentFolder)); + + if (outParent) parent.forget(outParent); + + // Iterate over our parents' children until we find one we're older than, + // and insert ourselves before it, or as the last child. In other words, + // insert, sorted by date. + uint32_t msgDate, childDate; + newHdr->GetDateInSeconds(&msgDate); + nsCOMPtr<nsIMsgDBHdr> child; + nsMsgViewIndex i; + nsMsgViewIndex insertIndex = m_keys.Length(); + uint32_t insertLevel = parentLevel + 1; + for (i = parentIndex; + i < m_keys.Length() && + (i == (nsMsgViewIndex)parentIndex || m_levels[i] >= parentLevel); + i++) { + GetChildHdrAt(i, getter_AddRefs(child)); + if (child) { + if (reparentChildren && IsHdrParentOf(newHdr, child)) { + insertIndex = i; + // Bump all the children of the current child, and the child. + nsMsgViewIndex j = insertIndex; + uint8_t childLevel = m_levels[insertIndex]; + do { + m_levels[j] = m_levels[j] + 1; + j++; + } while (j < m_keys.Length() && m_levels[j] > childLevel); + break; + } else if (m_levels[i] == parentLevel + 1) { + // Possible sibling. + child->GetDateInSeconds(&childDate); + if (msgDate < childDate) { + // If we think we need to reparent, remember this insert index, + // but keep looking for children. + insertIndex = i; + insertLevel = m_levels[i]; + // If the sibling we're inserting after has children, we need + // to go after the children. + while (insertIndex + 1 < m_keys.Length() && + m_levels[insertIndex + 1] > insertLevel) { + insertIndex++; + } + + if (!reparentChildren) break; + } + } + } + } + + m_keys.InsertElementAt(insertIndex, newHdrKey); + m_levels.InsertElementAt(insertIndex, insertLevel); + m_folders.InsertObjectAt(newHdrFolder, insertIndex); + whereInserted = insertIndex; + } else { + if (outParent) *outParent = nullptr; + + nsCOMPtr<nsIMsgDBHdr> rootHdr; + GetChildHdrAt(0, getter_AddRefs(rootHdr)); + // If the new header is a parent of the root then it should be promoted. + if (rootHdr && IsHdrParentOf(newHdr, rootHdr)) { + m_keys.InsertElementAt(0, newHdrKey); + m_levels.InsertElementAt(0, 0); + m_folders.InsertObjectAt(newHdrFolder, 0); + whereInserted = 0; + // Adjust level of old root hdr and its children + for (nsMsgViewIndex i = 1; i < m_keys.Length(); i++) + m_levels[i] = m_levels[1] + 1; + } else { + m_keys.AppendElement(newHdrKey); + m_levels.AppendElement(1); + m_folders.AppendObject(newHdrFolder); + if (outParent) rootHdr.forget(outParent); + + whereInserted = m_keys.Length() - 1; + } + } + + // ### TODO handle the case where the root header starts + // with Re, and the new one doesn't, and is earlier. In that + // case, we want to promote the new header to root. + // PRTime newHdrDate; + // newHdr->GetDate(&newHdrDate); + // if (numChildren > 0 && !(newHdrFlags & nsMsgMessageFlags::HasRe)) { + // PRTime topLevelHdrDate; + // nsCOMPtr<nsIMsgDBHdr> topLevelHdr; + // rv = GetRootHdr(getter_AddRefs(topLevelHdr)); + // if (NS_SUCCEEDED(rv) && topLevelHdr) { + // topLevelHdr->GetDate(&topLevelHdrDate); + // if (newHdrDate < topLevelHdrDate) ?? and now ?? + // } + // } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetChildHdrAt(uint32_t aIndex, nsIMsgDBHdr** aResult) { + if (aIndex >= m_keys.Length()) return NS_MSG_MESSAGE_NOT_FOUND; + + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = m_folders[aIndex]->GetMsgDatabase(getter_AddRefs(db)); + NS_ENSURE_SUCCESS(rv, rv); + return db->GetMsgHdrForKey(m_keys[aIndex], aResult); +} + +NS_IMETHODIMP +nsMsgXFViewThread::RemoveChildAt(uint32_t aIndex) { + m_keys.RemoveElementAt(aIndex); + m_levels.RemoveElementAt(aIndex); + m_folders.RemoveObjectAt(aIndex); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::RemoveChildHdr(nsIMsgDBHdr* child, + nsIDBChangeAnnouncer* announcer) { + NS_ENSURE_ARG_POINTER(child); + nsMsgKey msgKey; + uint32_t msgFlags; + child->GetMessageKey(&msgKey); + child->GetFlags(&msgFlags); + nsCOMPtr<nsIMsgFolder> msgFolder; + child->GetFolder(getter_AddRefs(msgFolder)); + // If this was the newest msg, clear the newest msg date so we'll recalc. + uint32_t date; + child->GetDateInSeconds(&date); + if (date == m_newestMsgDate) SetNewestMsgDate(0); + + for (uint32_t childIndex = 0; childIndex < m_keys.Length(); childIndex++) { + if (m_keys[childIndex] == msgKey && m_folders[childIndex] == msgFolder) { + uint8_t levelRemoved = m_keys[childIndex]; + // Adjust the levels of all the children of this header. + nsMsgViewIndex i; + for (i = childIndex + 1; + i < m_keys.Length() && m_levels[i] > levelRemoved; i++) { + m_levels[i] = m_levels[i] - 1; + } + + m_view->NoteChange(childIndex + 1, i - childIndex + 1, + nsMsgViewNotificationCode::changed); + m_keys.RemoveElementAt(childIndex); + m_levels.RemoveElementAt(childIndex); + m_folders.RemoveObjectAt(childIndex); + if (!(msgFlags & nsMsgMessageFlags::Read)) ChangeUnreadChildCount(-1); + + ChangeChildCount(-1); + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetRootHdr(nsIMsgDBHdr** aResult) { + NS_ENSURE_ARG_POINTER(aResult); + return GetChildHdrAt(0, aResult); +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetChildKeyAt(uint32_t aIndex, nsMsgKey* aResult) { + NS_ASSERTION(false, "shouldn't call this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetChild(nsMsgKey msgKey, nsIMsgDBHdr** aResult) { + NS_ASSERTION(false, "shouldn't call this"); + return NS_ERROR_NOT_IMPLEMENTED; +} + +int32_t nsMsgXFViewThread::HdrIndex(nsIMsgDBHdr* hdr) { + nsMsgKey msgKey; + nsCOMPtr<nsIMsgFolder> folder; + hdr->GetMessageKey(&msgKey); + hdr->GetFolder(getter_AddRefs(folder)); + for (uint32_t i = 0; i < m_keys.Length(); i++) { + if (m_keys[i] == msgKey && m_folders[i] == folder) return i; + } + + return -1; +} + +void nsMsgXFViewThread::ChangeUnreadChildCount(int32_t delta) { + m_numUnreadChildren += delta; +} + +void nsMsgXFViewThread::ChangeChildCount(int32_t delta) { + m_numChildren += delta; +} + +bool nsMsgXFViewThread::IsHdrParentOf(nsIMsgDBHdr* possibleParent, + nsIMsgDBHdr* possibleChild) { + uint16_t referenceToCheck = 0; + possibleChild->GetNumReferences(&referenceToCheck); + nsAutoCString reference; + + nsCString messageId; + possibleParent->GetMessageId(getter_Copies(messageId)); + + while (referenceToCheck > 0) { + possibleChild->GetStringReference(referenceToCheck - 1, reference); + + if (reference.Equals(messageId)) return true; + + // If reference didn't match, check if this ref is for a non-existent + // header. If it is, continue looking at ancestors. + nsCOMPtr<nsIMsgDBHdr> refHdr; + m_view->GetMsgHdrFromHash(reference, getter_AddRefs(refHdr)); + if (refHdr) break; + + referenceToCheck--; + } + + return false; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetNewestMsgDate(uint32_t* aResult) { + // If this hasn't been set, figure it out by enumerating the msgs in the + // thread. + if (!m_newestMsgDate) { + uint32_t numChildren; + nsresult rv = NS_OK; + + GetNumChildren(&numChildren); + + if ((int32_t)numChildren < 0) numChildren = 0; + + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> child; + rv = GetChildHdrAt(childIndex, getter_AddRefs(child)); + if (NS_SUCCEEDED(rv) && child) { + uint32_t msgDate; + child->GetDateInSeconds(&msgDate); + if (msgDate > m_newestMsgDate) m_newestMsgDate = msgDate; + } + } + } + + *aResult = m_newestMsgDate; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::SetNewestMsgDate(uint32_t aNewestMsgDate) { + m_newestMsgDate = aNewestMsgDate; + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::MarkChildRead(bool aRead) { + ChangeUnreadChildCount(aRead ? -1 : 1); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFViewThread::GetFirstUnreadChild(nsIMsgDBHdr** aResult) { + NS_ENSURE_ARG(aResult); + uint32_t numChildren; + GetNumChildren(&numChildren); + + if ((int32_t)numChildren < 0) numChildren = 0; + + for (uint32_t childIndex = 0; childIndex < numChildren; childIndex++) { + nsCOMPtr<nsIMsgDBHdr> child; + nsresult rv = GetChildHdrAt(childIndex, getter_AddRefs(child)); + if (NS_SUCCEEDED(rv) && child) { + nsMsgKey msgKey; + child->GetMessageKey(&msgKey); + + bool isRead; + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = m_folders[childIndex]->GetMsgDatabase(getter_AddRefs(db)); + if (NS_SUCCEEDED(rv)) rv = db->IsRead(msgKey, &isRead); + + if (NS_SUCCEEDED(rv) && !isRead) { + child.forget(aResult); + break; + } + } + } + + return (*aResult) ? NS_OK : NS_ERROR_NULL_POINTER; +} + +NS_IMETHODIMP +nsMsgXFViewThread::EnumerateMessages(nsMsgKey aParentKey, + nsIMsgEnumerator** aResult) { + NS_ERROR("shouldn't call this"); + return NS_ERROR_NOT_IMPLEMENTED; +} diff --git a/comm/mailnews/base/src/nsMsgXFViewThread.h b/comm/mailnews/base/src/nsMsgXFViewThread.h new file mode 100644 index 0000000000..ff3276bf3f --- /dev/null +++ b/comm/mailnews/base/src/nsMsgXFViewThread.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef nsMsgXFViewThread_h__ +#define nsMsgXFViewThread_h__ + +#include "msgCore.h" +#include "nsCOMArray.h" +#include "nsIMsgThread.h" +#include "MailNewsTypes.h" +#include "nsTArray.h" +#include "nsIMsgDatabase.h" +#include "nsIMsgHdr.h" +#include "nsMsgDBView.h" + +class nsMsgSearchDBView; + +class nsMsgXFViewThread : public nsIMsgThread { + public: + nsMsgXFViewThread(nsMsgSearchDBView* view, nsMsgKey threadId); + + NS_DECL_NSIMSGTHREAD + NS_DECL_ISUPPORTS + + bool IsHdrParentOf(nsIMsgDBHdr* possibleParent, nsIMsgDBHdr* possibleChild); + + void ChangeUnreadChildCount(int32_t delta); + void ChangeChildCount(int32_t delta); + + nsresult AddHdr(nsIMsgDBHdr* newHdr, bool reparentChildren, + uint32_t& whereInserted, nsIMsgDBHdr** outParent); + int32_t HdrIndex(nsIMsgDBHdr* hdr); + uint32_t ChildLevelAt(uint32_t msgIndex) { return m_levels[msgIndex]; } + uint32_t MsgCount() { return m_numChildren; }; + + protected: + virtual ~nsMsgXFViewThread(); + + nsMsgSearchDBView* m_view; + uint32_t m_numUnreadChildren; + uint32_t m_numChildren; + uint32_t m_flags; + uint32_t m_newestMsgDate; + nsMsgKey m_threadId; + nsTArray<nsMsgKey> m_keys; + nsCOMArray<nsIMsgFolder> m_folders; + nsTArray<uint8_t> m_levels; +}; + +#endif diff --git a/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp new file mode 100644 index 0000000000..fc2a1886eb --- /dev/null +++ b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.cpp @@ -0,0 +1,514 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "msgCore.h" +#include "nsMsgXFVirtualFolderDBView.h" +#include "nsIMsgHdr.h" +#include "nsIMsgThread.h" +#include "nsQuickSort.h" +#include "nsIDBFolderInfo.h" +#include "nsIMsgCopyService.h" +#include "nsMsgUtils.h" +#include "nsIMsgSearchSession.h" +#include "nsIMsgSearchTerm.h" +#include "nsMsgMessageFlags.h" +#include "nsServiceManagerUtils.h" + +nsMsgXFVirtualFolderDBView::nsMsgXFVirtualFolderDBView() { + mSuppressMsgDisplay = false; + m_doingSearch = false; + m_doingQuickSearch = false; + m_totalMessagesInView = 0; + m_curFolderHasCachedHits = false; +} + +nsMsgXFVirtualFolderDBView::~nsMsgXFVirtualFolderDBView() {} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::Open(nsIMsgFolder* folder, + nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, + int32_t* pCount) { + m_viewFolder = folder; + return nsMsgSearchDBView::Open(folder, sortType, sortOrder, viewFlags, + pCount); +} + +void nsMsgXFVirtualFolderDBView::RemovePendingDBListeners() { + nsresult rv; + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + + // UnregisterPendingListener will return an error when there are no more + // instances of this object registered as pending listeners. + while (NS_SUCCEEDED(rv)) rv = msgDBService->UnregisterPendingListener(this); +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::Close() { + RemovePendingDBListeners(); + return nsMsgSearchDBView::Close(); +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, + nsIMsgDBView** _retval) { + nsMsgXFVirtualFolderDBView* newMsgDBView = new nsMsgXFVirtualFolderDBView(); + nsresult rv = + CopyDBView(newMsgDBView, aMessengerInstance, aMsgWindow, aCmdUpdater); + NS_ENSURE_SUCCESS(rv, rv); + + NS_IF_ADDREF(*_retval = newMsgDBView); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::CopyDBView( + nsMsgDBView* aNewMsgDBView, nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, nsIMsgDBViewCommandUpdater* aCmdUpdater) { + nsMsgSearchDBView::CopyDBView(aNewMsgDBView, aMessengerInstance, aMsgWindow, + aCmdUpdater); + + nsMsgXFVirtualFolderDBView* newMsgDBView = + (nsMsgXFVirtualFolderDBView*)aNewMsgDBView; + + newMsgDBView->m_viewFolder = m_viewFolder; + newMsgDBView->m_searchSession = m_searchSession; + + int32_t scopeCount; + nsresult rv; + nsCOMPtr<nsIMsgSearchSession> searchSession = + do_QueryReferent(m_searchSession, &rv); + // It's OK not to have a search session. + NS_ENSURE_SUCCESS(rv, NS_OK); + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + searchSession->CountSearchScopes(&scopeCount); + for (int32_t i = 0; i < scopeCount; i++) { + nsMsgSearchScopeValue scopeId; + nsCOMPtr<nsIMsgFolder> searchFolder; + searchSession->GetNthSearchScope(i, &scopeId, getter_AddRefs(searchFolder)); + if (searchFolder) + msgDBService->RegisterPendingListener(searchFolder, newMsgDBView); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::GetViewType(nsMsgViewTypeValue* aViewType) { + NS_ENSURE_ARG_POINTER(aViewType); + *aViewType = nsMsgViewType::eShowVirtualFolderResults; + return NS_OK; +} + +nsresult nsMsgXFVirtualFolderDBView::OnNewHeader(nsIMsgDBHdr* newHdr, + nsMsgKey aParentKey, + bool /*ensureListed*/) { + if (newHdr) { + bool match = false; + nsCOMPtr<nsIMsgSearchSession> searchSession = + do_QueryReferent(m_searchSession); + + if (searchSession) searchSession->MatchHdr(newHdr, m_db, &match); + + if (!match) match = WasHdrRecentlyDeleted(newHdr); + + if (match) { + nsCOMPtr<nsIMsgFolder> folder; + newHdr->GetFolder(getter_AddRefs(folder)); + bool saveDoingSearch = m_doingSearch; + m_doingSearch = false; + OnSearchHit(newHdr, folder); + m_doingSearch = saveDoingSearch; + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::OnHdrPropertyChanged( + nsIMsgDBHdr* aHdrChanged, const nsACString& property, bool aPreChange, + uint32_t* aStatus, nsIDBChangeListener* aInstigator) { + // If the junk mail plugin just activated on a message, then + // we'll allow filters to remove from view. + // Otherwise, just update the view line. + // + // Note this will not add newly matched headers to the view. This is + // probably a bug that needs fixing. + + NS_ENSURE_ARG_POINTER(aStatus); + NS_ENSURE_ARG_POINTER(aHdrChanged); + + nsMsgViewIndex index = FindHdr(aHdrChanged); + // Message does not appear in view. + if (index == nsMsgViewIndex_None) return NS_OK; + + nsCString originStr; + (void)aHdrChanged->GetStringProperty("junkscoreorigin", originStr); + // Check for "plugin" with only first character for performance. + bool plugin = (originStr.get()[0] == 'p'); + + if (aPreChange) { + // First call, done prior to the change. + *aStatus = plugin; + return NS_OK; + } + + // Second call, done after the change. + bool wasPlugin = *aStatus; + + bool match = true; + nsCOMPtr<nsIMsgSearchSession> searchSession( + do_QueryReferent(m_searchSession)); + if (searchSession) searchSession->MatchHdr(aHdrChanged, m_db, &match); + + if (!match && plugin && !wasPlugin) + // Remove hdr from view. + RemoveByIndex(index); + else + NoteChange(index, 1, nsMsgViewNotificationCode::changed); + + return NS_OK; +} + +void nsMsgXFVirtualFolderDBView::UpdateCacheAndViewForFolder( + nsIMsgFolder* folder, nsTArray<nsMsgKey> const& newHits) { + nsCOMPtr<nsIMsgDatabase> db; + nsresult rv = folder->GetMsgDatabase(getter_AddRefs(db)); + if (NS_SUCCEEDED(rv) && db) { + nsCString searchUri; + m_viewFolder->GetURI(searchUri); + nsTArray<nsMsgKey> badHits; + rv = db->RefreshCache(searchUri, newHits, badHits); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIMsgDBHdr> badHdr; + for (nsMsgKey badKey : badHits) { + // ### of course, this isn't quite right, since we should be + // using FindHdr, and we shouldn't be expanding the threads. + db->GetMsgHdrForKey(badKey, getter_AddRefs(badHdr)); + // Let nsMsgSearchDBView decide what to do about this header + // getting removed. + if (badHdr) OnHdrDeleted(badHdr, nsMsgKey_None, 0, this); + } + } + } +} + +void nsMsgXFVirtualFolderDBView::UpdateCacheAndViewForPrevSearchedFolders( + nsIMsgFolder* curSearchFolder) { + // Handle the most recent folder with hits, if any. + if (m_curFolderGettingHits) { + uint32_t count = m_hdrHits.Count(); + nsTArray<nsMsgKey> newHits; + newHits.SetLength(count); + for (uint32_t i = 0; i < count; i++) + m_hdrHits[i]->GetMessageKey(&newHits[i]); + + newHits.Sort(); + UpdateCacheAndViewForFolder(m_curFolderGettingHits, newHits); + m_foldersSearchingOver.RemoveObject(m_curFolderGettingHits); + } + + while (m_foldersSearchingOver.Count() > 0) { + // This new folder has cached hits. + if (m_foldersSearchingOver[0] == curSearchFolder) { + m_curFolderHasCachedHits = true; + m_foldersSearchingOver.RemoveObjectAt(0); + break; + } else { + // This must be a folder that had no hits with the current search. + // So all cached hits, if any, need to be removed. + nsTArray<nsMsgKey> noHits; + UpdateCacheAndViewForFolder(m_foldersSearchingOver[0], noHits); + m_foldersSearchingOver.RemoveObjectAt(0); + } + } +} +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::OnSearchHit(nsIMsgDBHdr* aMsgHdr, + nsIMsgFolder* aFolder) { + NS_ENSURE_ARG(aMsgHdr); + NS_ENSURE_ARG(aFolder); + + if (m_curFolderGettingHits != aFolder && m_doingSearch && + !m_doingQuickSearch) { + m_curFolderHasCachedHits = false; + // Since we've gotten a hit for a new folder, the searches for + // any previous folders are done, so deal with stale cached hits + // for those folders now. + UpdateCacheAndViewForPrevSearchedFolders(aFolder); + m_curFolderGettingHits = aFolder; + m_hdrHits.Clear(); + m_curFolderStartKeyIndex = m_keys.Length(); + } + + bool hdrInCache = false; + if (!m_doingQuickSearch) { + nsCOMPtr<nsIMsgDatabase> dbToUse; + nsCOMPtr<nsIDBFolderInfo> dummyInfo; + nsresult rv = aFolder->GetDBFolderInfoAndDB(getter_AddRefs(dummyInfo), + getter_AddRefs(dbToUse)); + if (NS_SUCCEEDED(rv)) { + nsCString searchUri; + m_viewFolder->GetURI(searchUri); + dbToUse->HdrIsInCache(searchUri, aMsgHdr, &hdrInCache); + } + } + + if (!m_doingSearch || !m_curFolderHasCachedHits || !hdrInCache) { + if (m_viewFlags & nsMsgViewFlagsType::kGroupBySort) + nsMsgGroupView::OnNewHeader(aMsgHdr, nsMsgKey_None, true); + else if (m_sortValid) + InsertHdrFromFolder(aMsgHdr, aFolder); + else + AddHdrFromFolder(aMsgHdr, aFolder); + } + + m_hdrHits.AppendObject(aMsgHdr); + m_totalMessagesInView++; + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::OnSearchDone(nsresult status) { + // This batch began in OnNewSearch. + if (mJSTree) mJSTree->EndUpdateBatch(); + + NS_ENSURE_TRUE(m_viewFolder, NS_ERROR_NOT_INITIALIZED); + + // Handle any non verified hits we haven't handled yet. + if (NS_SUCCEEDED(status) && !m_doingQuickSearch && + status != NS_MSG_SEARCH_INTERRUPTED) + UpdateCacheAndViewForPrevSearchedFolders(nullptr); + + m_doingSearch = false; + // We want to set imap delete model once the search is over because setting + // next message after deletion will happen before deleting the message and + // search scope can change with every search. + + // Set to default in case it is non-imap folder. + mDeleteModel = nsMsgImapDeleteModels::MoveToTrash; + nsIMsgFolder* curFolder = m_folders.SafeObjectAt(0); + if (curFolder) GetImapDeleteModel(curFolder); + + nsCOMPtr<nsIMsgDatabase> virtDatabase; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB( + getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase)); + NS_ENSURE_SUCCESS(rv, rv); + // Count up the number of unread and total messages from the view, and set + // those in the folder - easier than trying to keep the count up to date in + // the face of search hits coming in while the user is reading/deleting + // messages. + uint32_t numUnread = 0; + for (uint32_t i = 0; i < m_flags.Length(); i++) { + if (m_flags[i] & nsMsgMessageFlags::Elided) { + nsCOMPtr<nsIMsgThread> thread; + GetThreadContainingIndex(i, getter_AddRefs(thread)); + if (thread) { + uint32_t unreadInThread; + thread->GetNumUnreadChildren(&unreadInThread); + numUnread += unreadInThread; + } + } else { + if (!(m_flags[i] & nsMsgMessageFlags::Read)) numUnread++; + } + } + + dbFolderInfo->SetNumUnreadMessages(numUnread); + dbFolderInfo->SetNumMessages(m_totalMessagesInView); + // Force update from db. + m_viewFolder->UpdateSummaryTotals(true); + virtDatabase->Commit(nsMsgDBCommitType::kLargeCommit); + if (!m_sortValid && m_sortType != nsMsgViewSortType::byThread && + !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + // Sort the results. + m_sortValid = false; + Sort(m_sortType, m_sortOrder); + } + + m_foldersSearchingOver.Clear(); + m_curFolderGettingHits = nullptr; + return rv; +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::OnNewSearch() { + int32_t oldSize = GetSize(); + + RemovePendingDBListeners(); + m_doingSearch = true; + m_totalMessagesInView = 0; + m_folders.Clear(); + m_keys.Clear(); + m_levels.Clear(); + m_flags.Clear(); + + // Needs to happen after we remove the keys, since RowCountChanged() will + // call our GetRowCount(). + if (mTree) mTree->RowCountChanged(0, -oldSize); + if (mJSTree) mJSTree->RowCountChanged(0, -oldSize); + + // To use the search results cache, we'll need to iterate over the scopes + // in the search session, calling getNthSearchScope + // for i = 0; i < searchSession.countSearchScopes; i++ + // and for each folder, then open the db and pull out the cached hits, + // add them to the view. For each hit in a new folder, we'll then clean up + // the stale hits from the previous folder(s). + + int32_t scopeCount; + nsCOMPtr<nsIMsgSearchSession> searchSession = + do_QueryReferent(m_searchSession); + // Just ignore. + NS_ENSURE_TRUE(searchSession, NS_OK); + nsCOMPtr<nsIMsgDBService> msgDBService = + do_GetService("@mozilla.org/msgDatabase/msgDBService;1"); + searchSession->CountSearchScopes(&scopeCount); + + // Figure out how many search terms the virtual folder has. + nsCOMPtr<nsIMsgDatabase> virtDatabase; + nsCOMPtr<nsIDBFolderInfo> dbFolderInfo; + nsresult rv = m_viewFolder->GetDBFolderInfoAndDB( + getter_AddRefs(dbFolderInfo), getter_AddRefs(virtDatabase)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString terms; + dbFolderInfo->GetCharProperty("searchStr", terms); + nsTArray<RefPtr<nsIMsgSearchTerm>> searchTerms; + rv = searchSession->GetSearchTerms(searchTerms); + NS_ENSURE_SUCCESS(rv, rv); + nsCString curSearchAsString; + + rv = MsgTermListToString(searchTerms, curSearchAsString); + // Trim off the initial AND/OR, which is irrelevant and inconsistent between + // what SearchSpec.jsm generates, and what's in virtualFolders.dat. + curSearchAsString.Cut(0, + StringBeginsWith(curSearchAsString, "AND"_ns) ? 3 : 2); + terms.Cut(0, StringBeginsWith(terms, "AND"_ns) ? 3 : 2); + + NS_ENSURE_SUCCESS(rv, rv); + // If the search session search string doesn't match the vf search str, + // then we're doing quick search, which means we don't want to invalidate + // cached results, or used cached results. + m_doingQuickSearch = !curSearchAsString.Equals(terms); + + if (!m_doingQuickSearch) { + if (mTree) mTree->BeginUpdateBatch(); + if (mJSTree) mJSTree->BeginUpdateBatch(); + } + + for (int32_t i = 0; i < scopeCount; i++) { + nsMsgSearchScopeValue scopeId; + nsCOMPtr<nsIMsgFolder> searchFolder; + searchSession->GetNthSearchScope(i, &scopeId, getter_AddRefs(searchFolder)); + if (searchFolder) { + nsCOMPtr<nsIMsgDatabase> searchDB; + nsCString searchUri; + m_viewFolder->GetURI(searchUri); + nsresult rv = searchFolder->GetMsgDatabase(getter_AddRefs(searchDB)); + if (NS_SUCCEEDED(rv) && searchDB) { + if (msgDBService) + msgDBService->RegisterPendingListener(searchFolder, this); + + m_foldersSearchingOver.AppendObject(searchFolder); + // Ignore cached hits in quick search case. + if (m_doingQuickSearch) continue; + + nsCOMPtr<nsIMsgEnumerator> cachedHits; + searchDB->GetCachedHits(searchUri, getter_AddRefs(cachedHits)); + bool hasMore; + if (cachedHits) { + cachedHits->HasMoreElements(&hasMore); + if (hasMore) { + mozilla::DebugOnly<nsMsgKey> prevKey = nsMsgKey_None; + while (hasMore) { + nsCOMPtr<nsIMsgDBHdr> header; + nsresult rv = cachedHits->GetNext(getter_AddRefs(header)); + if (header && NS_SUCCEEDED(rv)) { + nsMsgKey msgKey; + header->GetMessageKey(&msgKey); + NS_ASSERTION(prevKey == nsMsgKey_None || msgKey > prevKey, + "cached Hits not sorted"); +#ifdef DEBUG + prevKey = msgKey; +#endif + AddHdrFromFolder(header, searchFolder); + } else { + break; + } + + cachedHits->HasMoreElements(&hasMore); + } + } + } + } + } + } + + if (!m_doingQuickSearch) { + if (mTree) mTree->EndUpdateBatch(); + if (mJSTree) mJSTree->EndUpdateBatch(); + } + + m_curFolderStartKeyIndex = 0; + m_curFolderGettingHits = nullptr; + m_curFolderHasCachedHits = false; + + // If we have cached hits, sort them. + if (GetSize() > 0) { + // Currently, we keep threaded views sorted while we build them. + if (m_sortType != nsMsgViewSortType::byThread && + !(m_viewFlags & nsMsgViewFlagsType::kThreadedDisplay)) { + // Sort the results. + m_sortValid = false; + Sort(m_sortType, m_sortOrder); + } else if (mJSTree) { + mJSTree->Invalidate(); + } + } + + // Prevent updates for every message found. This batch ends in OnSearchDone. + if (mJSTree) mJSTree->BeginUpdateBatch(); + + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::DoCommand(nsMsgViewCommandTypeValue command) { + return nsMsgSearchDBView::DoCommand(command); +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::GetMsgFolder(nsIMsgFolder** aMsgFolder) { + NS_ENSURE_ARG_POINTER(aMsgFolder); + NS_IF_ADDREF(*aMsgFolder = m_viewFolder); + return NS_OK; +} + +NS_IMETHODIMP +nsMsgXFVirtualFolderDBView::SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) { + nsresult rv = NS_OK; + // If the grouping/threading has changed, rebuild the view. + if ((m_viewFlags & (nsMsgViewFlagsType::kGroupBySort | + nsMsgViewFlagsType::kThreadedDisplay)) != + (aViewFlags & (nsMsgViewFlagsType::kGroupBySort | + nsMsgViewFlagsType::kThreadedDisplay))) { + rv = RebuildView(aViewFlags); + } + + nsMsgDBView::SetViewFlags(aViewFlags); + return rv; +} + +nsresult nsMsgXFVirtualFolderDBView::GetMessageEnumerator( + nsIMsgEnumerator** enumerator) { + return GetViewEnumerator(enumerator); +} diff --git a/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h new file mode 100644 index 0000000000..5b8b627347 --- /dev/null +++ b/comm/mailnews/base/src/nsMsgXFVirtualFolderDBView.h @@ -0,0 +1,70 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsMsgXFVirtualFolderDBView_H_ +#define _nsMsgXFVirtualFolderDBView_H_ + +#include "mozilla/Attributes.h" +#include "nsMsgSearchDBView.h" +#include "nsIMsgCopyServiceListener.h" +#include "nsIMsgSearchNotify.h" +#include "nsCOMArray.h" + +class nsMsgGroupThread; + +class nsMsgXFVirtualFolderDBView : public nsMsgSearchDBView { + public: + nsMsgXFVirtualFolderDBView(); + virtual ~nsMsgXFVirtualFolderDBView(); + + // we override all the methods, currently. Might change... + NS_DECL_NSIMSGSEARCHNOTIFY + + virtual const char* GetViewName(void) override { + return "XFVirtualFolderView"; + } + NS_IMETHOD Open(nsIMsgFolder* folder, nsMsgViewSortTypeValue sortType, + nsMsgViewSortOrderValue sortOrder, + nsMsgViewFlagsTypeValue viewFlags, int32_t* pCount) override; + NS_IMETHOD CloneDBView(nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater, + nsIMsgDBView** _retval) override; + NS_IMETHOD CopyDBView(nsMsgDBView* aNewMsgDBView, + nsIMessenger* aMessengerInstance, + nsIMsgWindow* aMsgWindow, + nsIMsgDBViewCommandUpdater* aCmdUpdater) override; + NS_IMETHOD Close() override; + NS_IMETHOD GetViewType(nsMsgViewTypeValue* aViewType) override; + NS_IMETHOD DoCommand(nsMsgViewCommandTypeValue command) override; + NS_IMETHOD SetViewFlags(nsMsgViewFlagsTypeValue aViewFlags) override; + NS_IMETHOD OnHdrPropertyChanged(nsIMsgDBHdr* aHdrToChange, + const nsACString& property, bool aPreChange, + uint32_t* aStatus, + nsIDBChangeListener* aInstigator) override; + NS_IMETHOD GetMsgFolder(nsIMsgFolder** aMsgFolder) override; + + virtual nsresult OnNewHeader(nsIMsgDBHdr* newHdr, nsMsgKey parentKey, + bool ensureListed) override; + void UpdateCacheAndViewForPrevSearchedFolders(nsIMsgFolder* curSearchFolder); + void UpdateCacheAndViewForFolder(nsIMsgFolder* folder, + nsTArray<nsMsgKey> const& newHits); + void RemovePendingDBListeners(); + + protected: + virtual nsresult GetMessageEnumerator(nsIMsgEnumerator** enumerator) override; + + nsCOMArray<nsIMsgFolder> m_foldersSearchingOver; + nsCOMArray<nsIMsgDBHdr> m_hdrHits; + nsCOMPtr<nsIMsgFolder> m_curFolderGettingHits; + // keeps track of the index of the first hit from the cur folder + uint32_t m_curFolderStartKeyIndex; + bool m_curFolderHasCachedHits; + bool m_doingSearch; + // Are we doing a quick search on top of the virtual folder search? + bool m_doingQuickSearch; +}; + +#endif diff --git a/comm/mailnews/base/src/nsNewMailnewsURI.cpp b/comm/mailnews/base/src/nsNewMailnewsURI.cpp new file mode 100644 index 0000000000..305e9273a4 --- /dev/null +++ b/comm/mailnews/base/src/nsNewMailnewsURI.cpp @@ -0,0 +1,155 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsNewMailnewsURI.h" +#include "nsURLHelper.h" +#include "nsSimpleURI.h" +#include "nsStandardURL.h" +#include "nsThreadUtils.h" +#include "MainThreadUtils.h" +#include "mozilla/SyncRunnable.h" +#include "nsIMsgProtocolHandler.h" +#include "nsIComponentRegistrar.h" +#include "nsXULAppAPI.h" + +#include "../../local/src/nsPop3URL.h" +#include "../../local/src/nsMailboxService.h" +#include "../../compose/src/nsSmtpUrl.h" +#include "../../addrbook/src/nsLDAPURL.h" +#include "../../imap/src/nsImapService.h" +#include "../../news/src/nsNntpUrl.h" +#include "../src/nsCidProtocolHandler.h" + +nsresult NS_NewMailnewsURI(nsIURI** aURI, const nsACString& aSpec, + const char* aCharset /* = nullptr */, + nsIURI* aBaseURI /* = nullptr */) { + // Mailnews URIs aren't allowed in child processes. + if (!XRE_IsParentProcess()) { + return NS_ERROR_UNKNOWN_PROTOCOL; + } + + nsAutoCString scheme; + nsresult rv = net_ExtractURLScheme(aSpec, scheme); + if (NS_FAILED(rv)) { + // then aSpec is relative + if (!aBaseURI) { + return NS_ERROR_MALFORMED_URI; + } + + rv = aBaseURI->GetScheme(scheme); + if (NS_FAILED(rv)) return rv; + } + + // Creating IMAP/mailbox URIs off the main thread can lead to crashes. + // Seems to happen when viewing PDFs. + if (scheme.EqualsLiteral("mailbox") || + scheme.EqualsLiteral("mailbox-message")) { + if (NS_IsMainThread()) { + return nsMailboxService::NewURI(aSpec, aCharset, aBaseURI, aURI); + } + auto NewURI = [&aSpec, &aCharset, &aBaseURI, aURI, &rv ]() -> auto{ + rv = nsMailboxService::NewURI(aSpec, aCharset, aBaseURI, aURI); + }; + nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI); + mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), task); + return rv; + } + if (scheme.EqualsLiteral("imap") || scheme.EqualsLiteral("imap-message")) { + if (NS_IsMainThread()) { + return nsImapService::NewURI(aSpec, aCharset, aBaseURI, aURI); + } + auto NewURI = [&aSpec, &aCharset, &aBaseURI, aURI, &rv ]() -> auto{ + rv = nsImapService::NewURI(aSpec, aCharset, aBaseURI, aURI); + }; + nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI); + mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), task); + return rv; + } + if (scheme.EqualsLiteral("smtp") || scheme.EqualsLiteral("smtps")) { + return nsSmtpUrl::NewSmtpURI(aSpec, aBaseURI, aURI); + } + if (scheme.EqualsLiteral("mailto")) { + if (NS_IsMainThread()) { + return nsMailtoUrl::NewMailtoURI(aSpec, aBaseURI, aURI); + } + // If we're for some reason not on the main thread, dispatch to main + // or else we'll crash. + auto NewURI = [&aSpec, &aBaseURI, aURI, &rv ]() -> auto{ + rv = nsMailtoUrl::NewMailtoURI(aSpec, aBaseURI, aURI); + }; + nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI); + mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), task); + return rv; + } + if (scheme.EqualsLiteral("pop") || scheme.EqualsLiteral("pop3")) { + return nsPop3URL::NewURI(aSpec, aBaseURI, aURI); + } + if (scheme.EqualsLiteral("news") || scheme.EqualsLiteral("snews") || + scheme.EqualsLiteral("news-message") || scheme.EqualsLiteral("nntp")) { + return nsNntpUrl::NewURI(aSpec, aBaseURI, aURI); + } + if (scheme.EqualsLiteral("cid")) { + return nsCidProtocolHandler::NewURI(aSpec, aCharset, aBaseURI, aURI); + } + if (scheme.EqualsLiteral("ldap") || scheme.EqualsLiteral("ldaps")) { + nsCOMPtr<nsILDAPURL> url = do_CreateInstance(NS_LDAPURL_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + rv = url->Init(nsIStandardURL::URLTYPE_STANDARD, + scheme.EqualsLiteral("ldap") ? 389 : 636, aSpec, aCharset, + aBaseURI); + NS_ENSURE_SUCCESS(rv, rv); + url.forget(aURI); + return NS_OK; + } + if (scheme.EqualsLiteral("smile")) { + return NS_MutateURI(new mozilla::net::nsSimpleURI::Mutator()) + .SetSpec(aSpec) + .Finalize(aURI); + } + if (scheme.EqualsLiteral("moz-cal-handle-itip")) { + return NS_MutateURI(new mozilla::net::nsStandardURL::Mutator()) + .SetSpec(aSpec) + .Finalize(aURI); + } + if (scheme.EqualsLiteral("webcal") || scheme.EqualsLiteral("webcals")) { + return NS_MutateURI(new mozilla::net::nsStandardURL::Mutator()) + .SetSpec(aSpec) + .Finalize(aURI); + } + + rv = NS_ERROR_UNKNOWN_PROTOCOL; // Let M-C handle it by default. + + nsCOMPtr<nsIComponentRegistrar> compMgr; + NS_GetComponentRegistrar(getter_AddRefs(compMgr)); + if (compMgr) { + nsAutoCString contractID(NS_NETWORK_PROTOCOL_CONTRACTID_PREFIX); + contractID += scheme; + bool isRegistered = false; + compMgr->IsContractIDRegistered(contractID.get(), &isRegistered); + if (isRegistered) { + auto NewURI = + [&aSpec, &aCharset, &aBaseURI, aURI, &contractID, &rv ]() -> auto{ + nsCOMPtr<nsIMsgProtocolHandler> handler( + do_GetService(contractID.get())); + if (handler) { + // We recognise this URI. Use the protocol handler's result. + rv = handler->NewURI(aSpec, aCharset, aBaseURI, aURI); + } + }; + if (NS_IsMainThread()) { + NewURI(); + } else { + nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction("NewURI", NewURI); + mozilla::SyncRunnable::DispatchToThread( + mozilla::GetMainThreadSerialEventTarget(), task); + } + } + } + + return rv; +} diff --git a/comm/mailnews/base/src/nsNewMailnewsURI.h b/comm/mailnews/base/src/nsNewMailnewsURI.h new file mode 100644 index 0000000000..a5775d1978 --- /dev/null +++ b/comm/mailnews/base/src/nsNewMailnewsURI.h @@ -0,0 +1,15 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsNewMailnewsURI_h__ +#define nsNewMailnewsURI_h__ + +#include "nsIURI.h" +#include "nsNetUtil.h" + +nsresult NS_NewMailnewsURI(nsIURI** aURI, const nsACString& aSpec, + const char* aCharset /* = nullptr */, + nsIURI* aBaseURI /* = nullptr */); +#endif diff --git a/comm/mailnews/base/src/nsQuarantinedOutputStream.cpp b/comm/mailnews/base/src/nsQuarantinedOutputStream.cpp new file mode 100644 index 0000000000..1f325edadf --- /dev/null +++ b/comm/mailnews/base/src/nsQuarantinedOutputStream.cpp @@ -0,0 +1,234 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsQuarantinedOutputStream.h" +#include "nsDirectoryServiceDefs.h" +#include "nsIInputStream.h" +#include "nsISeekableStream.h" +#include "nsIFile.h" +#include "nsNetUtil.h" +#include "mozilla/UniquePtr.h" + +NS_IMPL_ISUPPORTS(nsQuarantinedOutputStream, nsIOutputStream, + nsISafeOutputStream) + +nsQuarantinedOutputStream::~nsQuarantinedOutputStream() { Close(); } + +// Initialise mTempFile and open it for writing (mTempStream). +nsresult nsQuarantinedOutputStream::InitTemp() { + MOZ_ASSERT(mState == eUninitialized); + MOZ_ASSERT(!mTempFile); + MOZ_ASSERT(!mTempStream); + // Create a unique temp file. + { + nsCOMPtr<nsIFile> file; + nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(file)); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->Append(u"newmsg"_ns); + NS_ENSURE_SUCCESS(rv, rv); + rv = file->CreateUnique(nsIFile::NORMAL_FILE_TYPE, 0600); + NS_ENSURE_SUCCESS(rv, rv); + mTempFile = std::move(file); + } + + // Open the temp file for writing. + { + nsCOMPtr<nsIOutputStream> stream; + nsresult rv = NS_NewLocalFileOutputStream(getter_AddRefs(stream), mTempFile, + -1, 0600); + NS_ENSURE_SUCCESS(rv, rv); + mTempStream = std::move(stream); + } + + return NS_OK; +} + +// Put us into the error state and clean up (by deleting the temp file +// if it exists). +void nsQuarantinedOutputStream::EnterErrorState(nsresult status) { + mState = eError; + mError = status; + mTarget = nullptr; + + if (mTempStream) { + mTempStream = nullptr; + } + if (mTempFile) { + mTempFile->Remove(false); + mTempFile = nullptr; + } +} + +// copyStream copies all the data in the input stream to the output stream. +// It keeps going until it sees an EOF on the input. +static nsresult copyStream(nsIInputStream* in, nsIOutputStream* out) { + constexpr uint32_t BUFSIZE = 8192; + auto buf = mozilla::MakeUnique<char[]>(BUFSIZE); + while (true) { + // Read input stream into buf. + uint32_t bufCnt; + nsresult rv = in->Read(buf.get(), BUFSIZE, &bufCnt); + NS_ENSURE_SUCCESS(rv, rv); + if (bufCnt == 0) { + break; // EOF. We're all done! + } + // Write buf to output stream. + uint32_t pos = 0; + while (pos < bufCnt) { + uint32_t writeCnt; + rv = out->Write(buf.get() + pos, bufCnt - pos, &writeCnt); + NS_ENSURE_SUCCESS(rv, rv); + pos += writeCnt; + } + } + return NS_OK; +} + +// copyStreamSafely() wraps copyStream(). If the output stream is seekable, +// it will try to roll it back if an error occurs during the copy. +static nsresult copyStreamSafely(nsIInputStream* in, nsIOutputStream* out) { + nsCOMPtr<nsISeekableStream> outSeekable = do_QueryInterface(out); + if (!outSeekable) { + // It's not seekable, so we jump out without a parachute. + return copyStream(in, out); + } + int64_t initialOffset; + nsresult rv = outSeekable->Tell(&initialOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = copyStream(in, out); + if (NS_FAILED(rv)) { + // Uhoh... the copy failed! Try to remove the partially-written data. + rv = outSeekable->Seek(nsISeekableStream::NS_SEEK_SET, initialOffset); + NS_ENSURE_SUCCESS(rv, rv); + rv = outSeekable->SetEOF(); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +NS_IMETHODIMP nsQuarantinedOutputStream::Close() { + if (mState != eOpen) { + // Already failed or closed or no data written. That's OK. + return NS_OK; + } + nsresult rv = NS_OK; + if (mTempStream) { + rv = mTempStream->Close(); + mTempStream = nullptr; + } + if (mTempFile) { + mTempFile->Remove(false); + mTempFile = nullptr; + } + mTarget->Close(); + mTarget = nullptr; + mState = eClosed; + return rv; +} + +NS_IMETHODIMP nsQuarantinedOutputStream::Finish() { + // Fail here if there was a previous error. + if (mState == eError) { + return mError; + } + if (mState != eOpen) { + // Already closed or no data written. That's OK. + return NS_OK; + } + + // Flush and close the temp file. Hopefully any virus checker will now act + // and prevent us reopening any suspicious-looking file. + MOZ_ASSERT(mTempStream); + MOZ_ASSERT(mTempFile); + mTempStream->Flush(); + nsresult rv = mTempStream->Close(); + if (NS_FAILED(rv)) { + EnterErrorState(rv); + return rv; + } + mTempStream = nullptr; + + // Write the tempfile out to the target stream + { + nsCOMPtr<nsIInputStream> ins; + // If a virus checker smells something bad, it should show up here as a + // failure to (re)open the temp file. + rv = NS_NewLocalFileInputStream(getter_AddRefs(ins), mTempFile); + if (NS_FAILED(rv)) { + EnterErrorState(rv); + return rv; + } + rv = copyStreamSafely(ins, mTarget); + if (NS_FAILED(rv)) { + EnterErrorState(rv); + return rv; + } + } + + // All done! + mTarget->Close(); + mTempFile->Remove(false); + mTempFile = nullptr; + mState = eClosed; + mTarget = nullptr; + return NS_OK; +} + +NS_IMETHODIMP nsQuarantinedOutputStream::Flush() { + if (mState != eOpen) { + return NS_OK; // Don't rock the boat. + } + nsresult rv = mTempStream->Flush(); + if (NS_FAILED(rv)) { + EnterErrorState(rv); + } + return rv; +} + +NS_IMETHODIMP nsQuarantinedOutputStream::Write(const char* buf, uint32_t count, + uint32_t* result) { + if (mState == eUninitialized) { + // Lazy open. + nsresult rv = InitTemp(); + if NS_FAILED (rv) { + EnterErrorState(rv); + return rv; + } + mState = eOpen; + } + + if (mState != eOpen) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = mTempStream->Write(buf, count, result); + if (NS_FAILED(rv)) { + EnterErrorState(rv); + return rv; + } + return NS_OK; +} + +NS_IMETHODIMP nsQuarantinedOutputStream::WriteFrom(nsIInputStream* fromStream, + uint32_t count, + uint32_t* retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsQuarantinedOutputStream::WriteSegments(nsReadSegmentFun reader, + void* closure, + uint32_t count, + uint32_t* retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP nsQuarantinedOutputStream::IsNonBlocking(bool* nonBlocking) { + *nonBlocking = false; + return NS_OK; +} + +NS_IMETHODIMP nsQuarantinedOutputStream::StreamStatus() { + return mState == eOpen ? NS_OK : NS_BASE_STREAM_CLOSED; +} diff --git a/comm/mailnews/base/src/nsQuarantinedOutputStream.h b/comm/mailnews/base/src/nsQuarantinedOutputStream.h new file mode 100644 index 0000000000..515440494b --- /dev/null +++ b/comm/mailnews/base/src/nsQuarantinedOutputStream.h @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsQuarantinedOutputStream_h__ +#define nsQuarantinedOutputStream_h__ + +// #include "nsISupports.h" +#include "nsIOutputStream.h" +#include "nsISafeOutputStream.h" +#include "nsCOMPtr.h" +class nsIFile; + +/** + * nsQuarantinedOutputStream layers on top of an existing target output stream. + * The idea is to let an OS virus checker quarantine individual messages + * _before_ they hit the mbox. You don't want entire mboxes embargoed if + * you can avoid it. + * + * It works by buffering all writes to a temporary file. + * When finish() is called the temporary file is closed, reopened, + * then copied into a pre-existing target stream. There's no special OS + * virus-checker integration - the assumption is that the checker will hook + * into the filesystem and prevent us from opening a file it has flagged as + * dodgy. Hence the temp file close/reopen before the final write. + * + * If the nsQuarantinedOutputStream is closed (or released) without calling + * finish(), the write is discarded (as per nsISafeOutputStream requirements). + * + * Upon close() or finish(), the underlying target file is also closed. + */ +class nsQuarantinedOutputStream : public nsIOutputStream, nsISafeOutputStream { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + NS_DECL_NSISAFEOUTPUTSTREAM + + /** + * Pass the target output stream in during construction. Upon Close(), + * the written data will be copied here. + */ + explicit nsQuarantinedOutputStream(nsIOutputStream* target) + : mTarget(target) {} + nsQuarantinedOutputStream() = delete; + + protected: + virtual ~nsQuarantinedOutputStream(); + + // Set up mTempFile and mTempStream (called at + // (lazily set up, upon first write). + nsresult InitTemp(); + nsresult PerformAppend(); + void EnterErrorState(nsresult status); + + // The temporary file and stream we're writing to. + nsCOMPtr<nsIFile> mTempFile; + nsCOMPtr<nsIOutputStream> mTempStream; + + // The stream we'll be appending to if it all succeeds. + nsCOMPtr<nsIOutputStream> mTarget; + + enum { + eUninitialized, // No temp file yet. + eOpen, // We're up and running. + eClosed, // The file has been closed. + eError // An error has occurred (stored in mError). + } mState{eUninitialized}; + nsresult mError{NS_OK}; +}; + +#endif // nsQuarantinedOutputStream_h__ diff --git a/comm/mailnews/base/src/nsSpamSettings.cpp b/comm/mailnews/base/src/nsSpamSettings.cpp new file mode 100644 index 0000000000..61dc1e8f43 --- /dev/null +++ b/comm/mailnews/base/src/nsSpamSettings.cpp @@ -0,0 +1,806 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSpamSettings.h" +#include "nsIFile.h" +#include "plstr.h" +#include "prmem.h" +#include "nsIMsgHdr.h" +#include "nsNetUtil.h" +#include "nsIMsgFolder.h" +#include "nsMsgUtils.h" +#include "nsMsgFolderFlags.h" +#include "nsImapCore.h" +#include "nsIImapIncomingServer.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIStringBundle.h" +#include "mozilla/Components.h" +#include "mozilla/mailnews/MimeHeaderParser.h" +#include "nsMailDirServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsISimpleEnumerator.h" +#include "nsIAbCard.h" +#include "nsIAbManager.h" +#include "nsIMsgAccountManager.h" +#include "mozilla/intl/AppDateTimeFormat.h" + +using namespace mozilla::mailnews; + +nsSpamSettings::nsSpamSettings() { + mLevel = 0; + mMoveOnSpam = false; + mMoveTargetMode = nsISpamSettings::MOVE_TARGET_MODE_ACCOUNT; + mPurge = false; + mPurgeInterval = 14; // 14 days + + mServerFilterTrustFlags = 0; + mInhibitWhiteListingIdentityUser = false; + mInhibitWhiteListingIdentityDomain = false; + mUseWhiteList = false; + mUseServerFilter = false; + + nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(mLogFile)); + if (NS_SUCCEEDED(rv)) mLogFile->Append(u"junklog.html"_ns); +} + +nsSpamSettings::~nsSpamSettings() {} + +NS_IMPL_ISUPPORTS(nsSpamSettings, nsISpamSettings, nsIUrlListener) + +NS_IMETHODIMP +nsSpamSettings::GetLevel(int32_t* aLevel) { + NS_ENSURE_ARG_POINTER(aLevel); + *aLevel = mLevel; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::SetLevel(int32_t aLevel) { + NS_ASSERTION((aLevel >= 0 && aLevel <= 100), "bad level"); + mLevel = aLevel; + return NS_OK; +} + +NS_IMETHODIMP +nsSpamSettings::GetMoveTargetMode(int32_t* aMoveTargetMode) { + NS_ENSURE_ARG_POINTER(aMoveTargetMode); + *aMoveTargetMode = mMoveTargetMode; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::SetMoveTargetMode(int32_t aMoveTargetMode) { + NS_ASSERTION((aMoveTargetMode == nsISpamSettings::MOVE_TARGET_MODE_FOLDER || + aMoveTargetMode == nsISpamSettings::MOVE_TARGET_MODE_ACCOUNT), + "bad move target mode"); + mMoveTargetMode = aMoveTargetMode; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::GetManualMark(bool* aManualMark) { + NS_ENSURE_ARG_POINTER(aManualMark); + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return prefBranch->GetBoolPref("mail.spam.manualMark", aManualMark); +} + +NS_IMETHODIMP nsSpamSettings::GetManualMarkMode(int32_t* aManualMarkMode) { + NS_ENSURE_ARG_POINTER(aManualMarkMode); + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return prefBranch->GetIntPref("mail.spam.manualMarkMode", aManualMarkMode); +} + +NS_IMETHODIMP nsSpamSettings::GetLoggingEnabled(bool* aLoggingEnabled) { + NS_ENSURE_ARG_POINTER(aLoggingEnabled); + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return prefBranch->GetBoolPref("mail.spam.logging.enabled", aLoggingEnabled); +} + +NS_IMETHODIMP nsSpamSettings::GetMarkAsReadOnSpam(bool* aMarkAsReadOnSpam) { + NS_ENSURE_ARG_POINTER(aMarkAsReadOnSpam); + nsresult rv; + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + return prefBranch->GetBoolPref("mail.spam.markAsReadOnSpam", + aMarkAsReadOnSpam); +} + +NS_IMPL_GETSET(nsSpamSettings, MoveOnSpam, bool, mMoveOnSpam) +NS_IMPL_GETSET(nsSpamSettings, Purge, bool, mPurge) +NS_IMPL_GETSET(nsSpamSettings, UseWhiteList, bool, mUseWhiteList) +NS_IMPL_GETSET(nsSpamSettings, UseServerFilter, bool, mUseServerFilter) + +NS_IMETHODIMP nsSpamSettings::GetWhiteListAbURI(nsACString& aWhiteListAbURI) { + aWhiteListAbURI = mWhiteListAbURI; + return NS_OK; +} +NS_IMETHODIMP nsSpamSettings::SetWhiteListAbURI( + const nsACString& aWhiteListAbURI) { + mWhiteListAbURI = aWhiteListAbURI; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::GetActionTargetAccount( + nsACString& aActionTargetAccount) { + aActionTargetAccount = mActionTargetAccount; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::SetActionTargetAccount( + const nsACString& aActionTargetAccount) { + mActionTargetAccount = aActionTargetAccount; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::GetActionTargetFolder( + nsACString& aActionTargetFolder) { + aActionTargetFolder = mActionTargetFolder; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::SetActionTargetFolder( + const nsACString& aActionTargetFolder) { + mActionTargetFolder = aActionTargetFolder; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::GetPurgeInterval(int32_t* aPurgeInterval) { + NS_ENSURE_ARG_POINTER(aPurgeInterval); + *aPurgeInterval = mPurgeInterval; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::SetPurgeInterval(int32_t aPurgeInterval) { + NS_ASSERTION(aPurgeInterval >= 0, "bad purge interval"); + mPurgeInterval = aPurgeInterval; + return NS_OK; +} + +NS_IMETHODIMP +nsSpamSettings::SetLogStream(nsIOutputStream* aLogStream) { + // if there is a log stream already, close it + if (mLogStream) { + // will flush + nsresult rv = mLogStream->Close(); + NS_ENSURE_SUCCESS(rv, rv); + } + + mLogStream = aLogStream; + return NS_OK; +} + +#define LOG_HEADER \ + "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"UTF-8\">\n<style " \ + "type=\"text/css\">body{font-family:Consolas,\"Lucida " \ + "Console\",Monaco,\"Courier " \ + "New\",Courier,monospace;font-size:small}</style>\n</head>\n<body>\n" +#define LOG_HEADER_LEN (strlen(LOG_HEADER)) + +NS_IMETHODIMP +nsSpamSettings::GetLogStream(nsIOutputStream** aLogStream) { + NS_ENSURE_ARG_POINTER(aLogStream); + + nsresult rv; + + if (!mLogStream) { + // append to the end of the log file + rv = MsgNewBufferedFileOutputStream(getter_AddRefs(mLogStream), mLogFile, + PR_CREATE_FILE | PR_WRONLY | PR_APPEND, + 0600); + NS_ENSURE_SUCCESS(rv, rv); + + int64_t fileSize; + rv = mLogFile->GetFileSize(&fileSize); + NS_ENSURE_SUCCESS(rv, rv); + + // write the header at the start + if (fileSize == 0) { + uint32_t writeCount; + + rv = mLogStream->Write(LOG_HEADER, LOG_HEADER_LEN, &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(writeCount == LOG_HEADER_LEN, + "failed to write out log header"); + } + } + + NS_ADDREF(*aLogStream = mLogStream); + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::Initialize(nsIMsgIncomingServer* aServer) { + NS_ENSURE_ARG_POINTER(aServer); + nsresult rv; + int32_t spamLevel; + rv = aServer->GetIntValue("spamLevel", &spamLevel); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetLevel(spamLevel); + NS_ENSURE_SUCCESS(rv, rv); + + bool moveOnSpam; + rv = aServer->GetBoolValue("moveOnSpam", &moveOnSpam); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetMoveOnSpam(moveOnSpam); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t moveTargetMode; + rv = aServer->GetIntValue("moveTargetMode", &moveTargetMode); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetMoveTargetMode(moveTargetMode); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString spamActionTargetAccount; + rv = + aServer->GetCharValue("spamActionTargetAccount", spamActionTargetAccount); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetActionTargetAccount(spamActionTargetAccount); + NS_ENSURE_SUCCESS(rv, rv); + + nsString spamActionTargetFolder; + rv = aServer->GetUnicharValue("spamActionTargetFolder", + spamActionTargetFolder); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetActionTargetFolder(NS_ConvertUTF16toUTF8(spamActionTargetFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + bool useWhiteList; + rv = aServer->GetBoolValue("useWhiteList", &useWhiteList); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetUseWhiteList(useWhiteList); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString whiteListAbURI; + rv = aServer->GetCharValue("whiteListAbURI", whiteListAbURI); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetWhiteListAbURI(whiteListAbURI); + NS_ENSURE_SUCCESS(rv, rv); + + bool purgeSpam; + rv = aServer->GetBoolValue("purgeSpam", &purgeSpam); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetPurge(purgeSpam); + NS_ENSURE_SUCCESS(rv, rv); + + int32_t purgeSpamInterval; + rv = aServer->GetIntValue("purgeSpamInterval", &purgeSpamInterval); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetPurgeInterval(purgeSpamInterval); + NS_ENSURE_SUCCESS(rv, rv); + + bool useServerFilter; + rv = aServer->GetBoolValue("useServerFilter", &useServerFilter); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetUseServerFilter(useServerFilter); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString serverFilterName; + rv = aServer->GetCharValue("serverFilterName", serverFilterName); + if (NS_SUCCEEDED(rv)) SetServerFilterName(serverFilterName); + int32_t serverFilterTrustFlags = 0; + rv = aServer->GetIntValue("serverFilterTrustFlags", &serverFilterTrustFlags); + NS_ENSURE_SUCCESS(rv, rv); + rv = SetServerFilterTrustFlags(serverFilterTrustFlags); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIPrefBranch> prefBranch( + do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + if (prefBranch) + prefBranch->GetCharPref("mail.trusteddomains", mTrustedMailDomains); + + mWhiteListDirArray.Clear(); + if (!mWhiteListAbURI.IsEmpty()) { + nsCOMPtr<nsIAbManager> abManager( + do_GetService("@mozilla.org/abmanager;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray<nsCString> whiteListArray; + ParseString(mWhiteListAbURI, ' ', whiteListArray); + + for (uint32_t index = 0; index < whiteListArray.Length(); index++) { + nsCOMPtr<nsIAbDirectory> directory; + rv = abManager->GetDirectory(whiteListArray[index], + getter_AddRefs(directory)); + NS_ENSURE_SUCCESS(rv, rv); + + if (directory) mWhiteListDirArray.AppendObject(directory); + } + } + + // the next two preferences affect whether we try to whitelist our own + // address or domain. Spammers send emails with spoofed from address matching + // either the email address of the recipient, or the recipient's domain, + // hoping to get whitelisted. + // + // The terms to describe this get wrapped up in chains of negatives. A full + // definition of the boolean inhibitWhiteListingIdentityUser is "Suppress + // address book whitelisting if the sender matches an identity's email + // address" + + rv = aServer->GetBoolValue("inhibitWhiteListingIdentityUser", + &mInhibitWhiteListingIdentityUser); + NS_ENSURE_SUCCESS(rv, rv); + rv = aServer->GetBoolValue("inhibitWhiteListingIdentityDomain", + &mInhibitWhiteListingIdentityDomain); + NS_ENSURE_SUCCESS(rv, rv); + + // collect lists of identity users if needed + if (mInhibitWhiteListingIdentityDomain || mInhibitWhiteListingIdentityUser) { + nsCOMPtr<nsIMsgAccountManager> accountManager( + do_GetService("@mozilla.org/messenger/account-manager;1", &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgAccount> account; + rv = accountManager->FindAccountForServer(aServer, getter_AddRefs(account)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString accountKey; + if (account) account->GetKey(accountKey); + + // Loop through all accounts, adding emails from this account, as well as + // from any accounts that defer to this account. + mEmails.Clear(); + nsTArray<RefPtr<nsIMsgAccount>> accounts; + // No sense scanning accounts if we've nothing to match. + if (account) { + rv = accountManager->GetAccounts(accounts); + NS_ENSURE_SUCCESS(rv, rv); + } + + for (auto loopAccount : accounts) { + if (!loopAccount) continue; + nsAutoCString loopAccountKey; + loopAccount->GetKey(loopAccountKey); + nsCOMPtr<nsIMsgIncomingServer> loopServer; + loopAccount->GetIncomingServer(getter_AddRefs(loopServer)); + nsAutoCString deferredToAccountKey; + if (loopServer) + loopServer->GetCharValue("deferred_to_account", deferredToAccountKey); + + // Add the emails for any account that defers to this one, or for the + // account itself. + if (accountKey.Equals(deferredToAccountKey) || + accountKey.Equals(loopAccountKey)) { + nsTArray<RefPtr<nsIMsgIdentity>> identities; + loopAccount->GetIdentities(identities); + for (auto identity : identities) { + nsAutoCString email; + identity->GetEmail(email); + if (!email.IsEmpty()) mEmails.AppendElement(email); + } + } + } + } + + return UpdateJunkFolderState(); +} + +nsresult nsSpamSettings::UpdateJunkFolderState() { + nsresult rv; + + // if the spam folder uri changed on us, we need to unset the junk flag + // on the old spam folder + nsCString newJunkFolderURI; + rv = GetSpamFolderURI(newJunkFolderURI); + NS_ENSURE_SUCCESS(rv, rv); + + if (!mCurrentJunkFolderURI.IsEmpty() && + !mCurrentJunkFolderURI.Equals(newJunkFolderURI)) { + nsCOMPtr<nsIMsgFolder> oldJunkFolder; + rv = FindFolder(mCurrentJunkFolderURI, getter_AddRefs(oldJunkFolder)); + NS_ENSURE_SUCCESS(rv, rv); + if (oldJunkFolder) { + // remove the nsMsgFolderFlags::Junk on the old junk folder + // XXX TODO + // JUNK MAIL RELATED + // (in ClearFlag?) we need to make sure that this folder + // is not the junk folder for another account + // the same goes for set flag. have fun with all that. + oldJunkFolder->ClearFlag(nsMsgFolderFlags::Junk); + } + } + + mCurrentJunkFolderURI = newJunkFolderURI; + + // only try to create the junk folder if we are moving junk + // and we have a non-empty uri + if (mMoveOnSpam && !mCurrentJunkFolderURI.IsEmpty()) { + // as the url listener, the spam settings will set the + // nsMsgFolderFlags::Junk folder flag on the junk mail folder, after it is + // created + rv = GetOrCreateJunkFolder(mCurrentJunkFolderURI, this); + } + + return rv; +} + +NS_IMETHODIMP nsSpamSettings::Clone(nsISpamSettings* aSpamSettings) { + NS_ENSURE_ARG_POINTER(aSpamSettings); + + nsresult rv = aSpamSettings->GetUseWhiteList(&mUseWhiteList); + NS_ENSURE_SUCCESS(rv, rv); + + (void)aSpamSettings->GetMoveOnSpam(&mMoveOnSpam); + (void)aSpamSettings->GetPurge(&mPurge); + (void)aSpamSettings->GetUseServerFilter(&mUseServerFilter); + + rv = aSpamSettings->GetPurgeInterval(&mPurgeInterval); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aSpamSettings->GetLevel(&mLevel); + NS_ENSURE_SUCCESS(rv, rv); + + rv = aSpamSettings->GetMoveTargetMode(&mMoveTargetMode); + NS_ENSURE_SUCCESS(rv, rv); + + nsCString actionTargetAccount; + rv = aSpamSettings->GetActionTargetAccount(actionTargetAccount); + NS_ENSURE_SUCCESS(rv, rv); + mActionTargetAccount = actionTargetAccount; + + nsCString actionTargetFolder; + rv = aSpamSettings->GetActionTargetFolder(actionTargetFolder); + NS_ENSURE_SUCCESS(rv, rv); + mActionTargetFolder = actionTargetFolder; + + nsCString whiteListAbURI; + rv = aSpamSettings->GetWhiteListAbURI(whiteListAbURI); + NS_ENSURE_SUCCESS(rv, rv); + mWhiteListAbURI = whiteListAbURI; + + aSpamSettings->GetServerFilterName(mServerFilterName); + aSpamSettings->GetServerFilterTrustFlags(&mServerFilterTrustFlags); + + return rv; +} + +NS_IMETHODIMP nsSpamSettings::GetSpamFolderURI(nsACString& aSpamFolderURI) { + if (mMoveTargetMode == nsISpamSettings::MOVE_TARGET_MODE_FOLDER) + return GetActionTargetFolder(aSpamFolderURI); + + // if the mode is nsISpamSettings::MOVE_TARGET_MODE_ACCOUNT + // the spam folder URI = account uri + "/Junk" + nsCString folderURI; + nsresult rv = GetActionTargetAccount(folderURI); + NS_ENSURE_SUCCESS(rv, rv); + + // we might be trying to get the old spam folder uri + // in order to clear the flag + // if we didn't have one, bail out. + if (folderURI.IsEmpty()) return NS_OK; + + nsCOMPtr<nsIMsgFolder> folder; + rv = GetOrCreateFolder(folderURI, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIMsgIncomingServer> server; + rv = folder->GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + // see nsMsgFolder::SetPrettyName() for where the pretty name is set. + + // Check for an existing junk folder - this will do a case-insensitive + // search by URI - if we find a junk folder, use its URI. + nsCOMPtr<nsIMsgFolder> junkFolder; + folderURI.AppendLiteral("/Junk"); + if (NS_SUCCEEDED(server->GetMsgFolderFromURI(nullptr, folderURI, + getter_AddRefs(junkFolder))) && + junkFolder) + junkFolder->GetURI(folderURI); + + // XXX todo + // better not to make base depend in imap + // but doing it here, like in nsMsgCopy.cpp + // one day, we'll fix this (and nsMsgCopy.cpp) to use GetMsgFolderFromURI() + nsCOMPtr<nsIImapIncomingServer> imapServer = do_QueryInterface(server); + if (imapServer) { + // Make sure an specific IMAP folder has correct personal namespace + // see bug #197043 + nsCString folderUriWithNamespace; + (void)imapServer->GetUriWithNamespacePrefixIfNecessary( + kPersonalNamespace, folderURI, folderUriWithNamespace); + if (!folderUriWithNamespace.IsEmpty()) folderURI = folderUriWithNamespace; + } + + aSpamFolderURI = folderURI; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::GetServerFilterName(nsACString& aFilterName) { + aFilterName = mServerFilterName; + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::SetServerFilterName( + const nsACString& aFilterName) { + mServerFilterName = aFilterName; + mServerFilterFile = nullptr; // clear out our stored location value + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::GetServerFilterFile(nsIFile** aFile) { + NS_ENSURE_ARG_POINTER(aFile); + if (!mServerFilterFile) { + nsresult rv; + nsAutoCString serverFilterFileName; + GetServerFilterName(serverFilterFileName); + serverFilterFileName.AppendLiteral(".sfd"); + + nsCOMPtr<nsIProperties> dirSvc = + do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // Walk through the list of isp directories + nsCOMPtr<nsISimpleEnumerator> ispDirectories; + rv = dirSvc->Get(ISP_DIRECTORY_LIST, NS_GET_IID(nsISimpleEnumerator), + getter_AddRefs(ispDirectories)); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasMore; + nsCOMPtr<nsIFile> file; + while (NS_SUCCEEDED(ispDirectories->HasMoreElements(&hasMore)) && hasMore) { + nsCOMPtr<nsISupports> elem; + ispDirectories->GetNext(getter_AddRefs(elem)); + file = do_QueryInterface(elem); + + if (file) { + // append our desired leaf name then test to see if the file exists. If + // it does, we've found mServerFilterFile. + file->AppendNative(serverFilterFileName); + bool exists; + if (NS_SUCCEEDED(file->Exists(&exists)) && exists) { + file.swap(mServerFilterFile); + break; + } + } // if file + } // until we find the location of mServerFilterName + } // if we haven't already stored mServerFilterFile + + NS_IF_ADDREF(*aFile = mServerFilterFile); + return NS_OK; +} + +NS_IMPL_GETSET(nsSpamSettings, ServerFilterTrustFlags, int32_t, + mServerFilterTrustFlags) + +#define LOG_ENTRY_START_TAG "<p>\n" +#define LOG_ENTRY_START_TAG_LEN (strlen(LOG_ENTRY_START_TAG)) +#define LOG_ENTRY_END_TAG "</p>\n" +#define LOG_ENTRY_END_TAG_LEN (strlen(LOG_ENTRY_END_TAG)) +// Does this need to be localizable? +#define LOG_ENTRY_TIMESTAMP "[$S] " + +NS_IMETHODIMP nsSpamSettings::LogJunkHit(nsIMsgDBHdr* aMsgHdr, + bool aMoveMessage) { + bool loggingEnabled; + nsresult rv = GetLoggingEnabled(&loggingEnabled); + NS_ENSURE_SUCCESS(rv, rv); + + if (!loggingEnabled) return NS_OK; + + PRTime date; + + nsString authorValue; + nsString subjectValue; + nsString dateValue; + + (void)aMsgHdr->GetDate(&date); + PRExplodedTime exploded; + PR_ExplodeTime(date, PR_LocalTimeParameters, &exploded); + + mozilla::intl::DateTimeFormat::StyleBag style; + style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long); + mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue); + + (void)aMsgHdr->GetMime2DecodedAuthor(authorValue); + (void)aMsgHdr->GetMime2DecodedSubject(subjectValue); + + nsCString buffer; + // this is big enough to hold a log entry. + // do this so we avoid growing and copying as we append to the log. + buffer.SetCapacity(512); + + nsCOMPtr<nsIStringBundleService> bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + + nsCOMPtr<nsIStringBundle> bundle; + rv = bundleService->CreateBundle( + "chrome://messenger/locale/filter.properties", getter_AddRefs(bundle)); + NS_ENSURE_SUCCESS(rv, rv); + + AutoTArray<nsString, 3> junkLogDetectFormatStrings = { + authorValue, subjectValue, dateValue}; + nsString junkLogDetectStr; + rv = bundle->FormatStringFromName( + "junkLogDetectStr", junkLogDetectFormatStrings, junkLogDetectStr); + NS_ENSURE_SUCCESS(rv, rv); + + buffer += NS_ConvertUTF16toUTF8(junkLogDetectStr); + buffer += "\n"; + + if (aMoveMessage) { + nsCString msgId; + aMsgHdr->GetMessageId(getter_Copies(msgId)); + + nsCString junkFolderURI; + rv = GetSpamFolderURI(junkFolderURI); + NS_ENSURE_SUCCESS(rv, rv); + + AutoTArray<nsString, 2> logMoveFormatStrings; + CopyASCIItoUTF16(msgId, *logMoveFormatStrings.AppendElement()); + CopyASCIItoUTF16(junkFolderURI, *logMoveFormatStrings.AppendElement()); + nsString logMoveStr; + rv = bundle->FormatStringFromName("logMoveStr", logMoveFormatStrings, + logMoveStr); + NS_ENSURE_SUCCESS(rv, rv); + + buffer += NS_ConvertUTF16toUTF8(logMoveStr); + buffer += "\n"; + } + + return LogJunkString(buffer.get()); +} + +NS_IMETHODIMP nsSpamSettings::LogJunkString(const char* string) { + bool loggingEnabled; + nsresult rv = GetLoggingEnabled(&loggingEnabled); + NS_ENSURE_SUCCESS(rv, rv); + + if (!loggingEnabled) return NS_OK; + + nsString dateValue; + PRExplodedTime exploded; + PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &exploded); + + mozilla::intl::DateTimeFormat::StyleBag style; + style.date = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Short); + style.time = mozilla::Some(mozilla::intl::DateTimeFormat::Style::Long); + mozilla::intl::AppDateTimeFormat::Format(style, &exploded, dateValue); + + nsCString timestampString(LOG_ENTRY_TIMESTAMP); + timestampString.ReplaceSubstring("$S", + NS_ConvertUTF16toUTF8(dateValue).get()); + + nsCOMPtr<nsIOutputStream> logStream; + rv = GetLogStream(getter_AddRefs(logStream)); + NS_ENSURE_SUCCESS(rv, rv); + + uint32_t writeCount; + + rv = logStream->Write(LOG_ENTRY_START_TAG, LOG_ENTRY_START_TAG_LEN, + &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(writeCount == LOG_ENTRY_START_TAG_LEN, + "failed to write out start log tag"); + + rv = logStream->Write(timestampString.get(), timestampString.Length(), + &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(writeCount == timestampString.Length(), + "failed to write out timestamp"); + + // HTML-escape the log for security reasons. + // We don't want someone to send us a message with a subject with + // HTML tags, especially <script>. + nsCString escapedBuffer; + nsAppendEscapedHTML(nsDependentCString(string), escapedBuffer); + + uint32_t escapedBufferLen = escapedBuffer.Length(); + rv = logStream->Write(escapedBuffer.get(), escapedBufferLen, &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(writeCount == escapedBufferLen, "failed to write out log hit"); + + rv = logStream->Write(LOG_ENTRY_END_TAG, LOG_ENTRY_END_TAG_LEN, &writeCount); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(writeCount == LOG_ENTRY_END_TAG_LEN, + "failed to write out end log tag"); + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::OnStartRunningUrl(nsIURI* aURL) { + // do nothing + // all the action happens in OnStopRunningUrl() + return NS_OK; +} + +NS_IMETHODIMP nsSpamSettings::OnStopRunningUrl(nsIURI* aURL, + nsresult exitCode) { + nsCString junkFolderURI; + nsresult rv = GetSpamFolderURI(junkFolderURI); + NS_ENSURE_SUCCESS(rv, rv); + + if (junkFolderURI.IsEmpty()) return NS_ERROR_UNEXPECTED; + + // when we get here, the folder should exist. + nsCOMPtr<nsIMsgFolder> junkFolder; + rv = GetExistingFolder(junkFolderURI, getter_AddRefs(junkFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = junkFolder->SetFlag(nsMsgFolderFlags::Junk); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +NS_IMETHODIMP nsSpamSettings::CheckWhiteList(nsIMsgDBHdr* aMsgHdr, + bool* aResult) { + NS_ENSURE_ARG_POINTER(aMsgHdr); + NS_ENSURE_ARG_POINTER(aResult); + *aResult = false; // default in case of error or no whitelisting + + if (!mUseWhiteList || + (!mWhiteListDirArray.Count() && mTrustedMailDomains.IsEmpty())) + return NS_OK; + + // do per-message processing + + nsCString author; + aMsgHdr->GetAuthor(getter_Copies(author)); + + nsAutoCString authorEmailAddress; + ExtractEmail(EncodedHeader(author), authorEmailAddress); + + if (authorEmailAddress.IsEmpty()) return NS_OK; + + // should we skip whitelisting for the identity email? + if (mInhibitWhiteListingIdentityUser) { + for (uint32_t i = 0; i < mEmails.Length(); ++i) { + if (mEmails[i].Equals(authorEmailAddress, + nsCaseInsensitiveCStringComparator)) + return NS_OK; + } + } + + if (!mTrustedMailDomains.IsEmpty() || mInhibitWhiteListingIdentityDomain) { + nsAutoCString domain; + int32_t atPos = authorEmailAddress.FindChar('@'); + if (atPos >= 0) domain = Substring(authorEmailAddress, atPos + 1); + if (!domain.IsEmpty()) { + if (!mTrustedMailDomains.IsEmpty() && + MsgHostDomainIsTrusted(domain, mTrustedMailDomains)) { + *aResult = true; + return NS_OK; + } + + if (mInhibitWhiteListingIdentityDomain) { + for (uint32_t i = 0; i < mEmails.Length(); ++i) { + nsAutoCString identityDomain; + int32_t atPos = mEmails[i].FindChar('@'); + if (atPos >= 0) { + identityDomain = Substring(mEmails[i], atPos + 1); + if (identityDomain.Equals(domain, + nsCaseInsensitiveCStringComparator)) + return NS_OK; // don't whitelist + } + } + } + } + } + + if (mWhiteListDirArray.Count()) { + nsCOMPtr<nsIAbCard> cardForAddress; + for (int32_t index = 0; + index < mWhiteListDirArray.Count() && !cardForAddress; index++) { + mWhiteListDirArray[index]->CardForEmailAddress( + authorEmailAddress, getter_AddRefs(cardForAddress)); + } + if (cardForAddress) { + *aResult = true; + return NS_OK; + } + } + return NS_OK; // default return is false +} diff --git a/comm/mailnews/base/src/nsSpamSettings.h b/comm/mailnews/base/src/nsSpamSettings.h new file mode 100644 index 0000000000..56725f7fe4 --- /dev/null +++ b/comm/mailnews/base/src/nsSpamSettings.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsSpamSettings_h__ +#define nsSpamSettings_h__ + +#include "nsCOMPtr.h" +#include "nsISpamSettings.h" +#include "nsString.h" +#include "nsIOutputStream.h" +#include "nsIMsgIncomingServer.h" +#include "nsIUrlListener.h" +#include "nsCOMArray.h" +#include "nsIAbDirectory.h" +#include "nsTArray.h" + +class nsSpamSettings : public nsISpamSettings, public nsIUrlListener { + public: + nsSpamSettings(); + + NS_DECL_ISUPPORTS + NS_DECL_NSISPAMSETTINGS + NS_DECL_NSIURLLISTENER + + private: + virtual ~nsSpamSettings(); + + nsCOMPtr<nsIOutputStream> mLogStream; + nsCOMPtr<nsIFile> mLogFile; + + int32_t mLevel; + int32_t mPurgeInterval; + int32_t mMoveTargetMode; + + bool mPurge; + bool mUseWhiteList; + bool mMoveOnSpam; + bool mUseServerFilter; + + nsCString mActionTargetAccount; + nsCString mActionTargetFolder; + nsCString mWhiteListAbURI; + // used to detect changes to the spam folder in ::initialize + nsCString mCurrentJunkFolderURI; + + nsCString mServerFilterName; + nsCOMPtr<nsIFile> mServerFilterFile; + int32_t mServerFilterTrustFlags; + + // array of address directories to use in junk whitelisting + nsCOMArray<nsIAbDirectory> mWhiteListDirArray; + // mail domains to use in junk whitelisting + nsCString mTrustedMailDomains; + // should we inhibit whitelisting address of identity? + bool mInhibitWhiteListingIdentityUser; + // should we inhibit whitelisting domain of identity? + bool mInhibitWhiteListingIdentityDomain; + // email addresses associated with this server + nsTArray<nsCString> mEmails; + + // helper routine used by Initialize which unsets the junk flag on the + // previous junk folder for this account, and sets it on the new junk folder. + nsresult UpdateJunkFolderState(); +}; + +#endif /* nsSpamSettings_h__ */ diff --git a/comm/mailnews/base/src/nsStatusBarBiffManager.cpp b/comm/mailnews/base/src/nsStatusBarBiffManager.cpp new file mode 100644 index 0000000000..df85a7bcee --- /dev/null +++ b/comm/mailnews/base/src/nsStatusBarBiffManager.cpp @@ -0,0 +1,254 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsStatusBarBiffManager.h" +#include "nsMsgBiffManager.h" +#include "nsIMsgMailSession.h" +#include "nsIMsgAccountManager.h" +#include "nsIObserverService.h" +#include "nsIWindowMediator.h" +#include "nsIMsgMailSession.h" +#include "MailNewsTypes.h" +#include "nsIMsgFolder.h" // TO include biffState enum. Change to bool later... +#include "nsMsgDBFolder.h" +#include "nsIFileChannel.h" +#include "nsIPrefService.h" +#include "nsIPrefBranch.h" +#include "nsIURL.h" +#include "nsNetUtil.h" +#include "nsIFileURL.h" +#include "nsIFile.h" +#include "nsMsgUtils.h" +#include "mozilla/Services.h" + +// QueryInterface, AddRef, and Release +// +NS_IMPL_ISUPPORTS(nsStatusBarBiffManager, nsIStatusBarBiffManager, + nsIFolderListener, nsIObserver) + +nsStatusBarBiffManager::nsStatusBarBiffManager() + : mInitialized(false), + mCurrentBiffState(nsIMsgFolder::nsMsgBiffState_Unknown) {} + +nsStatusBarBiffManager::~nsStatusBarBiffManager() {} + +#define NEW_MAIL_PREF_BRANCH "mail.biff." +#define CHAT_PREF_BRANCH "mail.chat." +#define FEED_PREF_BRANCH "mail.feed." +#define PREF_PLAY_SOUND "play_sound" +#define PREF_SOUND_URL "play_sound.url" +#define PREF_SOUND_TYPE "play_sound.type" +#define SYSTEM_SOUND_TYPE 0 +#define CUSTOM_SOUND_TYPE 1 +#define PREF_CHAT_ENABLED "mail.chat.enabled" +#define PLAY_CHAT_NOTIFICATION_SOUND "play-chat-notification-sound" + +nsresult nsStatusBarBiffManager::Init() { + if (mInitialized) return NS_ERROR_ALREADY_INITIALIZED; + + nsresult rv; + + nsCOMPtr<nsIMsgMailSession> mailSession = + do_GetService("@mozilla.org/messenger/services/session;1", &rv); + if (NS_SUCCEEDED(rv)) + mailSession->AddFolderListener(this, nsIFolderListener::intPropertyChanged); + + nsCOMPtr<nsIPrefBranch> pref(do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + bool chatEnabled = false; + if (NS_SUCCEEDED(rv)) rv = pref->GetBoolPref(PREF_CHAT_ENABLED, &chatEnabled); + if (NS_SUCCEEDED(rv) && chatEnabled) { + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + if (observerService) + observerService->AddObserver(this, PLAY_CHAT_NOTIFICATION_SOUND, false); + } + + mInitialized = true; + return NS_OK; +} + +nsresult nsStatusBarBiffManager::PlayBiffSound(const char* aPrefBranch) { + nsresult rv; + nsCOMPtr<nsIPrefService> prefSvc = + (do_GetService(NS_PREFSERVICE_CONTRACTID, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIPrefBranch> pref; + rv = prefSvc->GetBranch(aPrefBranch, getter_AddRefs(pref)); + NS_ENSURE_SUCCESS(rv, rv); + + bool playSound; + if (mServerType.EqualsLiteral("rss")) { + nsCOMPtr<nsIPrefBranch> prefFeed; + rv = prefSvc->GetBranch(FEED_PREF_BRANCH, getter_AddRefs(prefFeed)); + NS_ENSURE_SUCCESS(rv, rv); + rv = prefFeed->GetBoolPref(PREF_PLAY_SOUND, &playSound); + } else { + rv = pref->GetBoolPref(PREF_PLAY_SOUND, &playSound); + } + NS_ENSURE_SUCCESS(rv, rv); + + if (!playSound) return NS_OK; + + // lazily create the sound instance + if (!mSound) mSound = do_CreateInstance("@mozilla.org/sound;1"); + + int32_t soundType = SYSTEM_SOUND_TYPE; + rv = pref->GetIntPref(PREF_SOUND_TYPE, &soundType); + NS_ENSURE_SUCCESS(rv, rv); + +#ifndef XP_MACOSX + bool customSoundPlayed = false; +#endif + + if (soundType == CUSTOM_SOUND_TYPE) { + nsCString soundURLSpec; + rv = pref->GetCharPref(PREF_SOUND_URL, soundURLSpec); + + if (NS_SUCCEEDED(rv) && !soundURLSpec.IsEmpty()) { + if (!strncmp(soundURLSpec.get(), "file://", 7)) { + nsCOMPtr<nsIURI> fileURI; + rv = NS_NewURI(getter_AddRefs(fileURI), soundURLSpec); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr<nsIFileURL> soundURL = do_QueryInterface(fileURI, &rv); + if (NS_SUCCEEDED(rv)) { + nsCOMPtr<nsIFile> soundFile; + rv = soundURL->GetFile(getter_AddRefs(soundFile)); + if (NS_SUCCEEDED(rv)) { + bool soundFileExists = false; + rv = soundFile->Exists(&soundFileExists); + if (NS_SUCCEEDED(rv) && soundFileExists) { + rv = mSound->Play(soundURL); +#ifndef XP_MACOSX + if (NS_SUCCEEDED(rv)) customSoundPlayed = true; +#endif + } + } + } + } + // XXX TODO: See if we can create a nsIFile using the string as a native + // path. + } + } +#ifndef XP_MACOSX + // if nothing played, play the default system sound + if (!customSoundPlayed) { + rv = mSound->PlayEventSound(nsISound::EVENT_NEW_MAIL_RECEIVED); + NS_ENSURE_SUCCESS(rv, rv); + } +#endif + return rv; +} + +// nsIFolderListener methods.... +NS_IMETHODIMP +nsStatusBarBiffManager::OnFolderAdded(nsIMsgFolder* parent, + nsIMsgFolder* child) { + return NS_OK; +} + +NS_IMETHODIMP +nsStatusBarBiffManager::OnMessageAdded(nsIMsgFolder* parent, nsIMsgDBHdr* msg) { + return NS_OK; +} + +NS_IMETHODIMP +nsStatusBarBiffManager::OnFolderRemoved(nsIMsgFolder* parent, + nsIMsgFolder* child) { + return NS_OK; +} + +NS_IMETHODIMP +nsStatusBarBiffManager::OnMessageRemoved(nsIMsgFolder* parent, + nsIMsgDBHdr* msg) { + return NS_OK; +} + +NS_IMETHODIMP +nsStatusBarBiffManager::OnFolderPropertyChanged(nsIMsgFolder* folder, + const nsACString& property, + const nsACString& oldValue, + const nsACString& newValue) { + return NS_OK; +} + +NS_IMETHODIMP +nsStatusBarBiffManager::OnFolderIntPropertyChanged(nsIMsgFolder* folder, + const nsACString& property, + int64_t oldValue, + int64_t newValue) { + if (property.Equals(kBiffState) && mCurrentBiffState != newValue) { + // if we got new mail, attempt to play a sound. + // if we fail along the way, don't return. + // we still need to update the UI. + if (newValue == nsIMsgFolder::nsMsgBiffState_NewMail) { + // Get the folder's server type. + nsCOMPtr<nsIMsgIncomingServer> server; + nsresult rv = folder->GetServer(getter_AddRefs(server)); + if (NS_SUCCEEDED(rv) && server) server->GetType(mServerType); + + // if we fail to play the biff sound, keep going. + (void)PlayBiffSound(NEW_MAIL_PREF_BRANCH); + } + mCurrentBiffState = newValue; + + // don't care if notification fails + nsCOMPtr<nsIObserverService> observerService = + mozilla::services::GetObserverService(); + + if (observerService) + observerService->NotifyObservers( + static_cast<nsIStatusBarBiffManager*>(this), + "mail:biff-state-changed", nullptr); + } else if (property.Equals(kNewMailReceived)) { + (void)PlayBiffSound(NEW_MAIL_PREF_BRANCH); + } + return NS_OK; +} + +NS_IMETHODIMP +nsStatusBarBiffManager::OnFolderBoolPropertyChanged(nsIMsgFolder* folder, + const nsACString& property, + bool oldValue, + bool newValue) { + return NS_OK; +} + +NS_IMETHODIMP +nsStatusBarBiffManager::OnFolderUnicharPropertyChanged( + nsIMsgFolder* folder, const nsACString& property, const nsAString& oldValue, + const nsAString& newValue) { + return NS_OK; +} + +NS_IMETHODIMP +nsStatusBarBiffManager::OnFolderPropertyFlagChanged(nsIMsgDBHdr* msg, + const nsACString& property, + uint32_t oldFlag, + uint32_t newFlag) { + return NS_OK; +} + +NS_IMETHODIMP +nsStatusBarBiffManager::OnFolderEvent(nsIMsgFolder* folder, + const nsACString& event) { + return NS_OK; +} + +// nsIObserver implementation +NS_IMETHODIMP +nsStatusBarBiffManager::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + return PlayBiffSound(CHAT_PREF_BRANCH); +} + +// nsIStatusBarBiffManager method.... +NS_IMETHODIMP +nsStatusBarBiffManager::GetBiffState(int32_t* aBiffState) { + NS_ENSURE_ARG_POINTER(aBiffState); + *aBiffState = mCurrentBiffState; + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsStatusBarBiffManager.h b/comm/mailnews/base/src/nsStatusBarBiffManager.h new file mode 100644 index 0000000000..bd4d5d5bed --- /dev/null +++ b/comm/mailnews/base/src/nsStatusBarBiffManager.h @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsStatusBarBiffManager_h__ +#define nsStatusBarBiffManager_h__ + +#include "nsIStatusBarBiffManager.h" + +#include "msgCore.h" +#include "nsCOMPtr.h" +#include "nsISound.h" +#include "nsIObserver.h" + +class nsStatusBarBiffManager : public nsIStatusBarBiffManager, + public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIFOLDERLISTENER + NS_DECL_NSISTATUSBARBIFFMANAGER + NS_DECL_NSIOBSERVER + + nsStatusBarBiffManager(); + nsresult Init(); + + private: + virtual ~nsStatusBarBiffManager(); + + bool mInitialized; + int32_t mCurrentBiffState; + nsCString mServerType; + nsCOMPtr<nsISound> mSound; + nsresult PlayBiffSound(const char* aPrefBranch); +}; + +#endif // nsStatusBarBiffManager_h__ diff --git a/comm/mailnews/base/src/nsStopwatch.cpp b/comm/mailnews/base/src/nsStopwatch.cpp new file mode 100644 index 0000000000..585450d113 --- /dev/null +++ b/comm/mailnews/base/src/nsStopwatch.cpp @@ -0,0 +1,164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsStopwatch.h" + +#include <stdio.h> +#include <time.h> +#if defined(XP_UNIX) +# include <unistd.h> +# include <sys/times.h> +# include <sys/time.h> +# include <errno.h> +#elif defined(XP_WIN) +# include "windows.h" +#endif // elif defined(XP_WIN) + +#include "nsMemory.h" +/* + * This basis for the logic in this file comes from (will used to come from): + * (mozilla/)modules/libutil/public/stopwatch.cpp. + * + * It was no longer used in the mozilla tree, and is being migrated to + * comm-central where we actually have a need for it. ("Being" in the sense + * that it will not be removed immediately from mozilla-central.) + * + * Simplification and general clean-up has been performed and the fix for + * bug 96669 has been integrated. + */ + +NS_IMPL_ISUPPORTS(nsStopwatch, nsIStopwatch) + +#if defined(XP_UNIX) +/** the number of ticks per second */ +static double gTicks = 0; +# define MICRO_SECONDS_TO_SECONDS_MULT static_cast<double>(1.0e-6) +#elif defined(WIN32) +# ifdef DEBUG +# include "nsPrintfCString.h" +# endif +// 1 tick per 100ns = 10 per us = 10 * 1,000 per ms = 10 * 1,000 * 1,000 per +// sec. +# define WIN32_TICK_RESOLUTION static_cast<double>(1.0e-7) +// subtract off to get to the unix epoch +# define UNIX_EPOCH_IN_FILE_TIME 116444736000000000L +#endif // elif defined(WIN32) + +nsStopwatch::nsStopwatch() + : fTotalRealTimeSecs(0.0), fTotalCpuTimeSecs(0.0), fRunning(false) { +#if defined(XP_UNIX) + // idempotent in the event of a race under all coherency models + if (!gTicks) { + // we need to clear errno because sysconf's spec says it leaves it the same + // on success and only sets it on failure. + errno = 0; + gTicks = (clock_t)sysconf(_SC_CLK_TCK); + // in event of failure, pick an arbitrary value so we don't divide by zero. + if (errno) gTicks = 1000000L; + } +#endif +} + +nsStopwatch::~nsStopwatch() {} + +NS_IMETHODIMP nsStopwatch::Start() { + fTotalRealTimeSecs = 0.0; + fTotalCpuTimeSecs = 0.0; + return Resume(); +} + +NS_IMETHODIMP nsStopwatch::Stop() { + fStopRealTimeSecs = GetRealTime(); + fStopCpuTimeSecs = GetCPUTime(); + if (fRunning) { + fTotalCpuTimeSecs += fStopCpuTimeSecs - fStartCpuTimeSecs; + fTotalRealTimeSecs += fStopRealTimeSecs - fStartRealTimeSecs; + } + fRunning = false; + return NS_OK; +} + +NS_IMETHODIMP nsStopwatch::Resume() { + if (!fRunning) { + fStartRealTimeSecs = GetRealTime(); + fStartCpuTimeSecs = GetCPUTime(); + } + fRunning = true; + return NS_OK; +} + +NS_IMETHODIMP nsStopwatch::GetCpuTimeSeconds(double* result) { + NS_ENSURE_ARG_POINTER(result); + *result = fTotalCpuTimeSecs; + return NS_OK; +} + +NS_IMETHODIMP nsStopwatch::GetRealTimeSeconds(double* result) { + NS_ENSURE_ARG_POINTER(result); + *result = fTotalRealTimeSecs; + return NS_OK; +} + +double nsStopwatch::GetRealTime() { +#if defined(XP_UNIX) + struct timeval t; + gettimeofday(&t, NULL); + return t.tv_sec + t.tv_usec * MICRO_SECONDS_TO_SECONDS_MULT; +#elif defined(WIN32) + union { + FILETIME ftFileTime; + __int64 ftInt64; + } ftRealTime; // time the process has spent in kernel mode + SYSTEMTIME st; + GetSystemTime(&st); + SystemTimeToFileTime(&st, &ftRealTime.ftFileTime); + return (ftRealTime.ftInt64 - UNIX_EPOCH_IN_FILE_TIME) * WIN32_TICK_RESOLUTION; +#else +# error "nsStopwatch not supported on this platform." +#endif +} + +double nsStopwatch::GetCPUTime() { +#if defined(XP_UNIX) + struct tms cpt; + times(&cpt); + return (double)(cpt.tms_utime + cpt.tms_stime) / gTicks; +#elif defined(WIN32) + FILETIME ftCreate, // when the process was created + ftExit; // when the process exited + + union { + FILETIME ftFileTime; + __int64 ftInt64; + } ftKernel; // time the process has spent in kernel mode + + union { + FILETIME ftFileTime; + __int64 ftInt64; + } ftUser; // time the process has spent in user mode + + HANDLE hProcess = GetCurrentProcess(); +# ifdef DEBUG + BOOL ret = +# endif + GetProcessTimes(hProcess, &ftCreate, &ftExit, &ftKernel.ftFileTime, + &ftUser.ftFileTime); +# ifdef DEBUG + if (!ret) + NS_ERROR(nsPrintfCString("GetProcessTimes() failed, error=0x%lx.", + GetLastError()) + .get()); +# endif + + /* + * Process times are returned in a 64-bit structure, as the number of + * 100 nanosecond ticks since 1 January 1601. User mode and kernel mode + * times for this process are in separate 64-bit structures. + * Add them and convert the result to seconds. + */ + return (ftKernel.ftInt64 + ftUser.ftInt64) * WIN32_TICK_RESOLUTION; +#else +# error "nsStopwatch not supported on this platform." +#endif +} diff --git a/comm/mailnews/base/src/nsStopwatch.h b/comm/mailnews/base/src/nsStopwatch.h new file mode 100644 index 0000000000..9873519809 --- /dev/null +++ b/comm/mailnews/base/src/nsStopwatch.h @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef _nsStopwatch_h_ +#define _nsStopwatch_h_ + +#include "nsIStopwatch.h" + +#include "msgCore.h" + +#define NS_STOPWATCH_CID \ + { \ + 0x6ef7eafd, 0x72d0, 0x4c56, { \ + 0x94, 0x09, 0x67, 0xe1, 0x6d, 0x0f, 0x25, 0x5b \ + } \ + } + +#define NS_STOPWATCH_CONTRACTID "@mozilla.org/stopwatch;1" + +class nsStopwatch : public nsIStopwatch { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSISTOPWATCH + + nsStopwatch(); + + private: + virtual ~nsStopwatch(); + + /// Wall-clock start time in seconds since unix epoch. + double fStartRealTimeSecs; + /// Wall-clock stop time in seconds since unix epoch. + double fStopRealTimeSecs; + /// CPU-clock start time in seconds (of CPU time used since app start) + double fStartCpuTimeSecs; + /// CPU-clock stop time in seconds (of CPU time used since app start) + double fStopCpuTimeSecs; + /// Total wall-clock time elapsed in seconds. + double fTotalRealTimeSecs; + /// Total CPU time elapsed in seconds. + double fTotalCpuTimeSecs; + + /// Is the timer running? + bool fRunning; + + static double GetRealTime(); + static double GetCPUTime(); +}; + +#endif // _nsStopwatch_h_ diff --git a/comm/mailnews/base/src/nsSubscribableServer.cpp b/comm/mailnews/base/src/nsSubscribableServer.cpp new file mode 100644 index 0000000000..043ef2e4d3 --- /dev/null +++ b/comm/mailnews/base/src/nsSubscribableServer.cpp @@ -0,0 +1,867 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSubscribableServer.h" +#include "nsIMsgIncomingServer.h" +#include "nsIServiceManager.h" +#include "nsMsgI18N.h" +#include "nsMsgUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsTreeColumns.h" +#include "mozilla/dom/DataTransfer.h" + +/** + * The basic structure for the tree of the implementation. + * + * These elements are stored in reverse alphabetical order. + * Each node owns its children and subsequent siblings. + */ +struct SubscribeTreeNode { + nsCString name; + nsCString path; + bool isSubscribed; + SubscribeTreeNode* prevSibling; + SubscribeTreeNode* nextSibling; + SubscribeTreeNode* firstChild; + SubscribeTreeNode* lastChild; + SubscribeTreeNode* parent; + // Stores the child considered most likely to be next searched for - usually + // the most recently-added child. If names match the search can early-out. + SubscribeTreeNode* cachedChild; + bool isSubscribable; + bool isOpen; +}; + +nsSubscribableServer::nsSubscribableServer(void) { + mDelimiter = '.'; + mShowFullName = true; + mTreeRoot = nullptr; + mStopped = false; +} + +nsresult nsSubscribableServer::Init() { return NS_OK; } + +nsSubscribableServer::~nsSubscribableServer(void) { + FreeRows(); + if (mTreeRoot) { + FreeSubtree(mTreeRoot); + } +} + +NS_IMPL_ISUPPORTS(nsSubscribableServer, nsISubscribableServer, nsITreeView) + +NS_IMETHODIMP +nsSubscribableServer::SetIncomingServer(nsIMsgIncomingServer* aServer) { + if (!aServer) { + mIncomingServerUri.AssignLiteral(""); + mServerType.Truncate(); + return NS_OK; + } + aServer->GetType(mServerType); + + // We intentionally do not store a pointer to the aServer here + // as it would create reference loops, because nsIImapIncomingServer + // and nsINntpIncomingServer keep a reference to an internal + // nsISubscribableServer object. + // We only need the URI of the server anyway. + return aServer->GetServerURI(mIncomingServerUri); +} + +NS_IMETHODIMP +nsSubscribableServer::GetDelimiter(char* aDelimiter) { + NS_ENSURE_ARG_POINTER(aDelimiter); + *aDelimiter = mDelimiter; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::SetDelimiter(char aDelimiter) { + mDelimiter = aDelimiter; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::SetAsSubscribed(const nsACString& path) { + nsresult rv = NS_OK; + + SubscribeTreeNode* node = nullptr; + rv = FindAndCreateNode(path, &node); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(node, "didn't find the node"); + if (!node) return NS_ERROR_FAILURE; + node->isSubscribable = true; + node->isSubscribed = true; + + return rv; +} + +NS_IMETHODIMP +nsSubscribableServer::AddTo(const nsACString& aName, bool aAddAsSubscribed, + bool aSubscribable, bool aChangeIfExists) { + nsresult rv = NS_OK; + + if (mStopped) { + return NS_ERROR_FAILURE; + } + + SubscribeTreeNode* node = nullptr; + + // todo, shouldn't we pass in aAddAsSubscribed, for the + // default value if we create it? + rv = FindAndCreateNode(aName, &node); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(node, "didn't find the node"); + if (!node) return NS_ERROR_FAILURE; + + if (aChangeIfExists) { + node->isSubscribed = aAddAsSubscribed; + } + + node->isSubscribable = aSubscribable; + + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::SetState(const nsACString& aPath, bool aState, + bool* aStateChanged) { + nsresult rv = NS_OK; + NS_ASSERTION(!aPath.IsEmpty() && aStateChanged, "no path or stateChanged"); + if (aPath.IsEmpty() || !aStateChanged) return NS_ERROR_NULL_POINTER; + + NS_ASSERTION(mozilla::IsUtf8(aPath), "aPath is not in UTF-8"); + + *aStateChanged = false; + + SubscribeTreeNode* node = nullptr; + rv = FindAndCreateNode(aPath, &node); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(node, "didn't find the node"); + if (!node) return NS_ERROR_FAILURE; + + NS_ASSERTION(node->isSubscribable, "fix this"); + if (!node->isSubscribable) { + return NS_OK; + } + + if (node->isSubscribed == aState) { + return NS_OK; + } else { + node->isSubscribed = aState; + *aStateChanged = true; + + // Repaint the tree row to show/clear the check mark. + if (mTree) { + bool dummy; + int32_t index = GetRow(node, &dummy); + if (index >= 0) mTree->InvalidateRow(index); + } + } + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::SetSubscribeListener(nsISubscribeListener* aListener) { + mSubscribeListener = aListener; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetSubscribeListener(nsISubscribeListener** aListener) { + NS_ENSURE_ARG_POINTER(aListener); + NS_IF_ADDREF(*aListener = mSubscribeListener); + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::SubscribeCleanup() { + NS_ASSERTION(false, "override this."); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsSubscribableServer::StartPopulatingWithUri(nsIMsgWindow* aMsgWindow, + bool aForceToServer, + const nsACString& uri) { + mStopped = false; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::StartPopulating(nsIMsgWindow* aMsgWindow, + bool aForceToServer, + bool aGetOnlyNew /*ignored*/) { + mStopped = false; + + FreeRows(); + if (mTreeRoot) { + FreeSubtree(mTreeRoot); + mTreeRoot = nullptr; + } + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::StopPopulating(nsIMsgWindow* aMsgWindow) { + mStopped = true; + + FreeRows(); + + if (mTreeRoot) { + SubscribeTreeNode* node = mTreeRoot->lastChild; + // Add top level items as closed. + while (node) { + node->isOpen = false; + mRowMap.AppendElement(node); + node = node->prevSibling; + } + + // Invalidate the whole thing. + if (mTree) mTree->RowCountChanged(0, mRowMap.Length()); + + // Open all the top level items if they are containers. + uint32_t topRows = mRowMap.Length(); + for (int32_t i = topRows - 1; i >= 0; i--) { + bool isContainer = false; + IsContainer(i, &isContainer); + if (isContainer) ToggleOpenState(i); + } + } + + if (mSubscribeListener) mSubscribeListener->OnDonePopulating(); + + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::UpdateSubscribed() { + NS_ASSERTION(false, "override this."); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsSubscribableServer::Subscribe(const char16_t* aName) { + NS_ASSERTION(false, "override this."); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsSubscribableServer::Unsubscribe(const char16_t* aName) { + NS_ASSERTION(false, "override this."); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsSubscribableServer::SetShowFullName(bool showFullName) { + mShowFullName = showFullName; + return NS_OK; +} + +void nsSubscribableServer::FreeSubtree(SubscribeTreeNode* node) { + // NOTE: this doesn't cleanly detach each node, but that's not an issue + // here. This is a nuking, not a surgical removal. + + // recursively free the children + if (node->firstChild) { + // will free node->firstChild + FreeSubtree(node->firstChild); + node->firstChild = nullptr; + } + + // recursively free the siblings + if (node->nextSibling) { + FreeSubtree(node->nextSibling); + node->nextSibling = nullptr; + } + + delete node; +} + +void nsSubscribableServer::FreeRows() { + int32_t rowCount = mRowMap.Length(); + mRowMap.Clear(); + if (mTree) mTree->RowCountChanged(0, -rowCount); +} + +SubscribeTreeNode* nsSubscribableServer::CreateNode(SubscribeTreeNode* parent, + nsACString const& name, + nsACString const& path) { + SubscribeTreeNode* node = new SubscribeTreeNode(); + + node->name.Assign(name); + node->path.Assign(path); + node->parent = parent; + node->prevSibling = nullptr; + node->nextSibling = nullptr; + node->firstChild = nullptr; + node->lastChild = nullptr; + node->isSubscribed = false; + node->isSubscribable = false; + node->isOpen = true; + node->cachedChild = nullptr; + if (parent) { + parent->cachedChild = node; + } + return node; +} + +nsresult nsSubscribableServer::AddChildNode(SubscribeTreeNode* parent, + nsACString const& name, + const nsACString& path, + SubscribeTreeNode** child) { + nsresult rv = NS_OK; + NS_ASSERTION(parent && child && !name.IsEmpty() && !path.IsEmpty(), + "parent, child or name is null"); + if (!parent || !child || name.IsEmpty() || path.IsEmpty()) + return NS_ERROR_NULL_POINTER; + + // Adding to a node with no children? + if (!parent->firstChild) { + // CreateNode will set the parent->cachedChild + *child = CreateNode(parent, name, path); + parent->firstChild = *child; + parent->lastChild = *child; + return NS_OK; + } + + // Did we just add a child of this name? + if (parent->cachedChild) { + if (parent->cachedChild->name.Equals(name)) { + *child = parent->cachedChild; + return NS_OK; + } + } + + SubscribeTreeNode* current = parent->firstChild; + + /* + * Insert in reverse alphabetical order. + * This will reduce the # of strcmps since this is faster assuming: + * 1) the hostinfo.dat feeds us the groups in alphabetical order + * since we control the hostinfo.dat file, we can guarantee this. + * 2) the server gives us the groups in alphabetical order + * we can't guarantee this, but it seems to be a common thing. + * + * Because we have firstChild, lastChild, nextSibling, prevSibling, + * we can efficiently reverse the order when dumping to hostinfo.dat + * or to GetTargets(). + */ + int32_t compare = Compare(current->name, name); + + while (current && (compare != 0)) { + if (compare < 0) { + // CreateNode will set the parent->cachedChild + *child = CreateNode(parent, name, path); + NS_ENSURE_SUCCESS(rv, rv); + + (*child)->nextSibling = current; + (*child)->prevSibling = current->prevSibling; + current->prevSibling = (*child); + if (!(*child)->prevSibling) { + parent->firstChild = (*child); + } else { + (*child)->prevSibling->nextSibling = (*child); + } + + return NS_OK; + } + current = current->nextSibling; + if (current) { + NS_ASSERTION(!current->name.IsEmpty(), "no name!"); + compare = Compare(current->name, name); + } else { + compare = -1; // anything but 0, since that would be a match + } + } + + if (compare == 0) { + // already exists; + *child = current; + parent->cachedChild = *child; + return NS_OK; + } + + // CreateNode will set the parent->cachedChild + *child = CreateNode(parent, name, path); + + (*child)->prevSibling = parent->lastChild; + (*child)->nextSibling = nullptr; + parent->lastChild->nextSibling = *child; + parent->lastChild = *child; + + return NS_OK; +} + +nsresult nsSubscribableServer::FindAndCreateNode(const nsACString& aPath, + SubscribeTreeNode** aResult) { + nsresult rv = NS_OK; + NS_ASSERTION(aResult, "no result"); + NS_ENSURE_ARG_POINTER(aResult); + + if (!mTreeRoot) { + // the root has no parent, and its name is server uri + mTreeRoot = CreateNode(nullptr, mIncomingServerUri, EmptyCString()); + } + + if (aPath.IsEmpty()) { + *aResult = mTreeRoot; + return NS_OK; + } + + *aResult = nullptr; + + SubscribeTreeNode* parent = mTreeRoot; + SubscribeTreeNode* child = nullptr; + + uint32_t tokenStart = 0; + // Special case paths that start with the hierarchy delimiter. + // We want to include that delimiter in the first token name. + // So start from position 1. + int32_t tokenEnd = aPath.FindChar(mDelimiter, tokenStart + 1); + while (true) { + if (tokenEnd == kNotFound) { + if (tokenStart >= aPath.Length()) break; + tokenEnd = aPath.Length(); + } + nsCString token(Substring(aPath, tokenStart, tokenEnd - tokenStart)); + rv = AddChildNode(parent, token, Substring(aPath, 0, tokenEnd), &child); + if (NS_FAILED(rv)) return rv; + tokenStart = tokenEnd + 1; + tokenEnd = aPath.FindChar(mDelimiter, tokenStart); + parent = child; + } + + // the last child we add is the result + *aResult = child; + return rv; +} + +NS_IMETHODIMP +nsSubscribableServer::HasChildren(const nsACString& aPath, bool* aHasChildren) { + NS_ASSERTION(aHasChildren, "no hasChildren"); + NS_ENSURE_ARG_POINTER(aHasChildren); + + *aHasChildren = false; + + SubscribeTreeNode* node = nullptr; + nsresult rv = FindAndCreateNode(aPath, &node); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(node, "didn't find the node"); + if (!node) return NS_ERROR_FAILURE; + + *aHasChildren = (node->firstChild != nullptr); + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::IsSubscribed(const nsACString& aPath, + bool* aIsSubscribed) { + NS_ENSURE_ARG_POINTER(aIsSubscribed); + + *aIsSubscribed = false; + + SubscribeTreeNode* node = nullptr; + nsresult rv = FindAndCreateNode(aPath, &node); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(node, "didn't find the node"); + if (!node) return NS_ERROR_FAILURE; + + *aIsSubscribed = node->isSubscribed; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::IsSubscribable(const nsACString& aPath, + bool* aIsSubscribable) { + NS_ENSURE_ARG_POINTER(aIsSubscribable); + + *aIsSubscribable = false; + + SubscribeTreeNode* node = nullptr; + nsresult rv = FindAndCreateNode(aPath, &node); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(node, "didn't find the node"); + if (!node) return NS_ERROR_FAILURE; + + *aIsSubscribable = node->isSubscribable; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetLeafName(const nsACString& aPath, + nsAString& aLeafName) { + SubscribeTreeNode* node = nullptr; + nsresult rv = FindAndCreateNode(aPath, &node); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(node, "didn't find the node"); + if (!node) return NS_ERROR_FAILURE; + + // XXX TODO FIXME + // I'm assuming that mShowFullName is true for NNTP, false for IMAP. + // For imap, the node name is in MUTF-7; for news, the path is escaped UTF-8. + // When we switch to using the tree, this hack will go away. + if (mShowFullName) { + return NS_MsgDecodeUnescapeURLPath(aPath, aLeafName); + } + + return CopyFolderNameToUTF16(node->name, aLeafName); +} + +NS_IMETHODIMP +nsSubscribableServer::GetFirstChildURI(const nsACString& aPath, + nsACString& aResult) { + aResult.Truncate(); + + SubscribeTreeNode* node = nullptr; + nsresult rv = FindAndCreateNode(aPath, &node); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(node, "didn't find the node"); + if (!node) return NS_ERROR_FAILURE; + + // no children + if (!node->firstChild) return NS_ERROR_FAILURE; + + aResult.Assign(node->firstChild->path); + + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetChildURIs(const nsACString& aPath, + nsTArray<nsCString>& aResult) { + aResult.Clear(); + SubscribeTreeNode* node = nullptr; + nsresult rv = FindAndCreateNode(aPath, &node); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(node, "didn't find the node"); + if (!node) return NS_ERROR_FAILURE; + + NS_ASSERTION(mTreeRoot, "no tree root!"); + if (!mTreeRoot) return NS_ERROR_UNEXPECTED; + + // We inserted them in reverse alphabetical order. + // So pull them out in reverse to get the right order + // in the subscribe dialog. + SubscribeTreeNode* current = node->lastChild; + // return failure if there are no children. + if (!current) return NS_ERROR_FAILURE; + + while (current) { + NS_ASSERTION(!current->name.IsEmpty(), "no name"); + if (current->name.IsEmpty()) { + return NS_ERROR_FAILURE; + } + aResult.AppendElement(current->path); + current = current->prevSibling; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::CommitSubscribeChanges() { + NS_ASSERTION(false, "override this."); + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsSubscribableServer::SetSearchValue(const nsAString& aSearchValue) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsSubscribableServer::GetSupportsSubscribeSearch(bool* retVal) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsSubscribableServer::GetFolderView(nsITreeView** aView) { + NS_ENSURE_ARG_POINTER(aView); + return this->QueryInterface(NS_GET_IID(nsITreeView), (void**)aView); +} + +int32_t nsSubscribableServer::GetRow(SubscribeTreeNode* node, bool* open) { + int32_t parentRow = -1; + if (node->parent) parentRow = GetRow(node->parent, open); + + // If the parent wasn't opened, we're not in the row map + if (open && *open == false) return -1; + + if (open) *open = node->isOpen; + + for (uint32_t row = parentRow + 1; row < mRowMap.Length(); row++) { + if (mRowMap[row] == node) return row; + } + + // Apparently, we're not in the map + return -1; +} + +NS_IMETHODIMP +nsSubscribableServer::GetSelection(nsITreeSelection** selection) { + NS_IF_ADDREF(*selection = mSelection); + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::SetSelection(nsITreeSelection* selection) { + mSelection = selection; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetRowCount(int32_t* rowCount) { + *rowCount = mRowMap.Length(); + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::SetTree(mozilla::dom::XULTreeElement* aTree) { + mTree = aTree; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::IsContainer(int32_t aIndex, bool* retval) { + *retval = !!mRowMap[aIndex]->firstChild; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::IsContainerEmpty(int32_t aIndex, bool* retval) { + *retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::IsContainerOpen(int32_t aIndex, bool* retval) { + *retval = mRowMap[aIndex]->isOpen; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetParentIndex(int32_t aIndex, int32_t* retval) { + SubscribeTreeNode* parent = mRowMap[aIndex]->parent; + if (!parent) { + *retval = -1; + return NS_OK; + } + + int32_t index; + for (index = aIndex - 1; index >= 0; index--) { + if (mRowMap[index] == parent) { + *retval = index; + return NS_OK; + } + } + *retval = -1; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::HasNextSibling(int32_t aRowIndex, int32_t aAfterIndex, + bool* retval) { + // This looks odd, but is correct. Using ->nextSibling gives a bad tree. + *retval = !!mRowMap[aRowIndex]->prevSibling; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetLevel(int32_t aIndex, int32_t* retval) { + // When starting with -2, we increase twice and return 0 for a top level node. + int32_t level = -2; + SubscribeTreeNode* node = mRowMap[aIndex]; + while (node) { + node = node->parent; + level++; + } + + *retval = level; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::ToggleOpenState(int32_t aIndex) { + SubscribeTreeNode* node = mRowMap[aIndex]; + if (node->isOpen) { + node->isOpen = false; + + // Remove subtree by deleting elements from array up to next sibling. + int32_t count = 0; + do { + // Find next sibling or the beginning of the next subtree. + if (node->prevSibling) { + count = mRowMap.IndexOf(node->prevSibling, aIndex) - aIndex - 1; + } else { + node = node->parent; + // When node reaches the root, delete the rest of the array. + if (!node->parent) { + count = mRowMap.Length() - aIndex - 1; + } + } + } while (!count && node->parent); + mRowMap.RemoveElementsAt(aIndex + 1, count); + if (mTree) { + mTree->RowCountChanged(aIndex + 1, -count); + mTree->InvalidateRow(aIndex); + } + } else { + // Recursively add the children nodes (i.e., remember open) + node->isOpen = true; + int32_t total = 0; + node = node->lastChild; + while (node) { + total += AddSubtree(node, aIndex + 1 + total); + node = node->prevSibling; + } + if (mTree) { + mTree->RowCountChanged(aIndex + 1, total); + mTree->InvalidateRow(aIndex); + } + } + return NS_OK; +} + +int32_t nsSubscribableServer::AddSubtree(SubscribeTreeNode* node, + int32_t index) { + mRowMap.InsertElementAt(index, node); + int32_t total = 1; + if (node->isOpen) { + node = node->lastChild; + while (node) { + total += AddSubtree(node, index + total); + node = node->prevSibling; + } + } + return total; +} + +NS_IMETHODIMP +nsSubscribableServer::GetCellText(int32_t aRow, nsTreeColumn* aCol, + nsAString& retval) { + nsString colId; + aCol->GetId(colId); + if (colId.EqualsLiteral("nameColumn")) { + nsCString path(mRowMap[aRow]->path); + GetLeafName(path, retval); + } + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetCellValue(int32_t aRow, nsTreeColumn* aCol, + nsAString& retval) { + nsString colId; + aCol->GetId(colId); + if (colId.EqualsLiteral("nameColumn")) + retval = NS_ConvertUTF8toUTF16(mRowMap[aRow]->path); + if (colId.EqualsLiteral("subscribedColumn")) { + retval = mRowMap[aRow]->isSubscribed ? u"true"_ns : u"false"_ns; + } + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::SetCellText(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aText) { + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::SetCellValue(int32_t aRow, nsTreeColumn* aCol, + const nsAString& aText) { + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetCellProperties(int32_t aRow, nsTreeColumn* aCol, + nsAString& aProps) { + SubscribeTreeNode* node = mRowMap[aRow]; + if (node->isSubscribable) + aProps.AssignLiteral("subscribable-true"); + else + aProps.AssignLiteral("subscribable-false"); + + nsString colId; + aCol->GetId(colId); + if (colId.EqualsLiteral("subscribedColumn")) { + if (node->isSubscribed) + aProps.AppendLiteral(" subscribed-true"); + else + aProps.AppendLiteral(" subscribed-false"); + } else if (colId.EqualsLiteral("nameColumn")) { + aProps.AppendLiteral(" serverType-"); + aProps.Append(NS_ConvertUTF8toUTF16(mServerType)); + } + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetRowProperties(int32_t aRow, nsAString& aProps) { + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetColumnProperties(nsTreeColumn* aCol, + nsAString& aProps) { + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::IsEditable(int32_t aRow, nsTreeColumn* aCol, + bool* retval) { + *retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::IsSeparator(int32_t aRowIndex, bool* retval) { + *retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::IsSorted(bool* retval) { + *retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::CanDrop(int32_t aIndex, int32_t aOrientation, + mozilla::dom::DataTransfer* aData, bool* retval) { + *retval = false; + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::Drop(int32_t aRow, int32_t aOrientation, + mozilla::dom::DataTransfer* aData) { + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::GetImageSrc(int32_t aRow, nsTreeColumn* aCol, + nsAString& retval) { + return NS_OK; +} + +NS_IMETHODIMP +nsSubscribableServer::CycleHeader(nsTreeColumn* aCol) { return NS_OK; } + +NS_IMETHODIMP +nsSubscribableServer::SelectionChangedXPCOM() { return NS_OK; } + +NS_IMETHODIMP +nsSubscribableServer::CycleCell(int32_t aRow, nsTreeColumn* aCol) { + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsSubscribableServer.h b/comm/mailnews/base/src/nsSubscribableServer.h new file mode 100644 index 0000000000..7fbafd0838 --- /dev/null +++ b/comm/mailnews/base/src/nsSubscribableServer.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsSubscribableServer_h__ +#define nsSubscribableServer_h__ + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "mozilla/dom/XULTreeElement.h" +#include "nsITreeSelection.h" +#include "nsITreeView.h" +#include "nsISubscribableServer.h" +#include "nsTArray.h" + +struct SubscribeTreeNode; + +class nsSubscribableServer : public nsISubscribableServer, public nsITreeView { + public: + nsSubscribableServer(); + + nsresult Init(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSISUBSCRIBABLESERVER + NS_DECL_NSITREEVIEW + + private: + virtual ~nsSubscribableServer(); + + nsresult ConvertNameToUnichar(const char* inStr, char16_t** outStr); + nsCOMPtr<nsISubscribeListener> mSubscribeListener; + nsCString mIncomingServerUri; + char mDelimiter; + bool mShowFullName; + bool mStopped; + nsCString mServerType; + + // root of the folder tree while items are discovered on the server + SubscribeTreeNode* mTreeRoot; + // array of nodes representing the rows for the tree element + nsTArray<SubscribeTreeNode*> mRowMap; + nsCOMPtr<nsITreeSelection> mSelection; + RefPtr<mozilla::dom::XULTreeElement> mTree; + void FreeSubtree(SubscribeTreeNode* node); + void FreeRows(); + SubscribeTreeNode* CreateNode(SubscribeTreeNode* parent, + nsACString const& name, nsACString const& path); + nsresult AddChildNode(SubscribeTreeNode* parent, nsACString const& name, + const nsACString& aPath, SubscribeTreeNode** child); + nsresult FindAndCreateNode(const nsACString& aPath, + SubscribeTreeNode** aResult); + + int32_t GetRow(SubscribeTreeNode* node, bool* open); + int32_t AddSubtree(SubscribeTreeNode* node, int32_t index); +}; + +#endif // nsSubscribableServer_h__ diff --git a/comm/mailnews/base/src/nsUserInfo.h b/comm/mailnews/base/src/nsUserInfo.h new file mode 100644 index 0000000000..df3f81adef --- /dev/null +++ b/comm/mailnews/base/src/nsUserInfo.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +#ifndef __nsUserInfo_h +#define __nsUserInfo_h + +#include "nsIUserInfo.h" + +class nsUserInfo : public nsIUserInfo + +{ + public: + nsUserInfo(void); + + NS_DECL_ISUPPORTS + NS_DECL_NSIUSERINFO + + protected: + virtual ~nsUserInfo(); +}; + +#endif /* __nsUserInfo_h */ diff --git a/comm/mailnews/base/src/nsUserInfoMac.mm b/comm/mailnews/base/src/nsUserInfoMac.mm new file mode 100644 index 0000000000..a98d1f72ea --- /dev/null +++ b/comm/mailnews/base/src/nsUserInfoMac.mm @@ -0,0 +1,70 @@ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsUserInfo.h" +#include "nsObjCExceptions.h" +#include "nsString.h" +#include "nsCocoaUtils.h" + +#import <Cocoa/Cocoa.h> +#import <AddressBook/AddressBook.h> + +NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo) + +nsUserInfo::nsUserInfo() {} + +nsUserInfo::~nsUserInfo() {} + +NS_IMETHODIMP +nsUserInfo::GetFullname(nsAString& aFullname) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK + + nsCocoaUtils::GetStringForNSString(NSFullUserName(), aFullname); + + NS_OBJC_END_TRY_IGNORE_BLOCK + return NS_OK; +} + +NS_IMETHODIMP +nsUserInfo::GetUsername(nsAString& aUsername) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK + + nsCocoaUtils::GetStringForNSString(NSUserName(), aUsername); + + NS_OBJC_END_TRY_IGNORE_BLOCK + return NS_OK; +} + +NS_IMETHODIMP +nsUserInfo::GetEmailAddress(nsAString& aEmailAddress) { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK + + aEmailAddress.Truncate(); + // Try to get this user's primary email from the system addressbook's "me card" + // (if they've filled it) + ABPerson* me = [[ABAddressBook sharedAddressBook] me]; + ABMultiValue* emailAddresses = [me valueForProperty:kABEmailProperty]; + if ([emailAddresses count] > 0) { + // get the index of the primary email, in case there are more than one + int primaryEmailIndex = [emailAddresses indexForIdentifier:[emailAddresses primaryIdentifier]]; + nsCocoaUtils::GetStringForNSString([emailAddresses valueAtIndex:primaryEmailIndex], + aEmailAddress); + } + + NS_OBJC_END_TRY_IGNORE_BLOCK + + return NS_OK; +} + +NS_IMETHODIMP +nsUserInfo::GetDomain(nsAString& aDomain) { + GetEmailAddress(aDomain); + int32_t index = aDomain.FindChar('@'); + if (index != -1) { + // chop off everything before, and including the '@' + aDomain.Cut(0, index + 1); + } + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsUserInfoUnix.cpp b/comm/mailnews/base/src/nsUserInfoUnix.cpp new file mode 100644 index 0000000000..559b68f062 --- /dev/null +++ b/comm/mailnews/base/src/nsUserInfoUnix.cpp @@ -0,0 +1,124 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsUserInfo.h" +#include "nsCRT.h" + +#include <pwd.h> +#include <sys/types.h> +#include <unistd.h> +#include <sys/utsname.h> + +#include "nsString.h" +#include "nsReadableUtils.h" +#include "nsNativeCharsetUtils.h" + +nsUserInfo::nsUserInfo() {} + +nsUserInfo::~nsUserInfo() {} + +NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo) + +NS_IMETHODIMP +nsUserInfo::GetFullname(nsAString& aFullname) { + aFullname.Truncate(); + struct passwd* pw = nullptr; + + pw = getpwuid(geteuid()); + + if (!pw || !pw->pw_gecos) return NS_OK; + + nsAutoCString fullname(pw->pw_gecos); + + // now try to parse the GECOS information, which will be in the form + // Full Name, <other stuff> - eliminate the ", <other stuff> + // also, sometimes GECOS uses "&" to mean "the user name" so do + // the appropriate substitution + + // truncate at first comma (field delimiter) + int32_t index; + if ((index = fullname.Find(",")) != kNotFound) fullname.Truncate(index); + + // replace ampersand with username + if (pw->pw_name) { + nsAutoCString username(pw->pw_name); + if (!username.IsEmpty()) + username.SetCharAt(nsCRT::ToUpper(username.CharAt(0)), 0); + + fullname.ReplaceSubstring("&", username.get()); + } + + CopyUTF8toUTF16(fullname, aFullname); + + return NS_OK; +} + +NS_IMETHODIMP +nsUserInfo::GetUsername(nsAString& aUsername) { + aUsername.Truncate(); + struct passwd* pw = nullptr; + + // is this portable? those are POSIX compliant calls, but I need to check + pw = getpwuid(geteuid()); + + if (!pw || !pw->pw_name) return NS_OK; + + CopyUTF8toUTF16(mozilla::MakeStringSpan(pw->pw_name), aUsername); + + return NS_OK; +} + +NS_IMETHODIMP +nsUserInfo::GetDomain(nsAString& aDomain) { + aDomain.Truncate(); + struct utsname buf; + char* domainname = nullptr; + + if (uname(&buf) < 0) { + return NS_OK; + } + +#if defined(__linux__) + domainname = buf.domainname; +#endif + + if (domainname && domainname[0]) { + CopyUTF8toUTF16(mozilla::MakeStringSpan(domainname), aDomain); + } else { + // try to get the hostname from the nodename + // on machines that use DHCP, domainname may not be set + // but the nodename might. + if (buf.nodename[0]) { + // if the nodename is foo.bar.org, use bar.org as the domain + char* pos = strchr(buf.nodename, '.'); + if (pos) { + CopyUTF8toUTF16(mozilla::MakeStringSpan(pos + 1), aDomain); + } + } + } + + return NS_OK; +} + +NS_IMETHODIMP +nsUserInfo::GetEmailAddress(nsAString& aEmailAddress) { + // use username + "@" + domain for the email address + + nsString username; + nsString domain; + + GetUsername(username); + GetDomain(domain); + + if (!username.IsEmpty() && !domain.IsEmpty()) { + aEmailAddress = username; + aEmailAddress.Append('@'); + aEmailAddress += domain; + } else { + aEmailAddress.Truncate(); + } + + return NS_OK; +} diff --git a/comm/mailnews/base/src/nsUserInfoWin.cpp b/comm/mailnews/base/src/nsUserInfoWin.cpp new file mode 100644 index 0000000000..a5b69cf9a2 --- /dev/null +++ b/comm/mailnews/base/src/nsUserInfoWin.cpp @@ -0,0 +1,99 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsUserInfo.h" + +#include "mozilla/ArrayUtils.h" // ArrayLength +#include "nsString.h" +#include "windows.h" +#include "nsCRT.h" + +#define SECURITY_WIN32 +#include "lm.h" +#include "security.h" + +nsUserInfo::nsUserInfo() {} + +nsUserInfo::~nsUserInfo() {} + +NS_IMPL_ISUPPORTS(nsUserInfo, nsIUserInfo) + +NS_IMETHODIMP +nsUserInfo::GetUsername(nsAString& aUsername) { + aUsername.Truncate(); + + // UNLEN is the max username length as defined in lmcons.h + wchar_t username[UNLEN + 1]; + DWORD size = mozilla::ArrayLength(username); + if (!GetUserNameW(username, &size)) return NS_OK; + + aUsername.Assign(username); + return NS_OK; +} + +NS_IMETHODIMP +nsUserInfo::GetFullname(nsAString& aFullname) { + aFullname.Truncate(); + + wchar_t fullName[512]; + DWORD size = mozilla::ArrayLength(fullName); + + if (GetUserNameExW(NameDisplay, fullName, &size)) { + aFullname.Assign(fullName); + } else { + // Try to use the net APIs regardless of the error because it may be + // able to obtain the information. + wchar_t username[UNLEN + 1]; + size = mozilla::ArrayLength(username); + if (!GetUserNameW(username, &size)) { + return NS_OK; + } + + const DWORD level = 2; + LPBYTE info; + // If the NetUserGetInfo function has no full name info it will return + // success with an empty string. + NET_API_STATUS status = NetUserGetInfo(nullptr, username, level, &info); + if (status != NERR_Success) { + return NS_OK; + } + + aFullname.Assign(reinterpret_cast<USER_INFO_2*>(info)->usri2_full_name); + NetApiBufferFree(info); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsUserInfo::GetDomain(nsAString& aDomain) { + aDomain.Truncate(); + + const DWORD level = 100; + LPBYTE info; + NET_API_STATUS status = NetWkstaGetInfo(nullptr, level, &info); + if (status == NERR_Success) { + aDomain.Assign(reinterpret_cast<WKSTA_INFO_100*>(info)->wki100_langroup); + NetApiBufferFree(info); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsUserInfo::GetEmailAddress(nsAString& aEmailAddress) { + aEmailAddress.Truncate(); + + // RFC3696 says max length of an email address is 254 + wchar_t emailAddress[255]; + DWORD size = mozilla::ArrayLength(emailAddress); + + if (!GetUserNameExW(NameUserPrincipal, emailAddress, &size)) { + return NS_OK; + } + + aEmailAddress.Assign(emailAddress); + return NS_OK; +} |