summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/news/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/mailnews/news/src/NewsAutoCompleteSearch.jsm134
-rw-r--r--comm/mailnews/news/src/NewsDownloader.sys.mjs158
-rw-r--r--comm/mailnews/news/src/NntpChannel.jsm402
-rw-r--r--comm/mailnews/news/src/NntpClient.jsm981
-rw-r--r--comm/mailnews/news/src/NntpIncomingServer.jsm624
-rw-r--r--comm/mailnews/news/src/NntpMessageService.jsm272
-rw-r--r--comm/mailnews/news/src/NntpNewsGroup.jsm420
-rw-r--r--comm/mailnews/news/src/NntpProtocolHandler.jsm46
-rw-r--r--comm/mailnews/news/src/NntpProtocolInfo.jsm44
-rw-r--r--comm/mailnews/news/src/NntpService.jsm250
-rw-r--r--comm/mailnews/news/src/NntpUtils.jsm63
-rw-r--r--comm/mailnews/news/src/components.conf98
-rw-r--r--comm/mailnews/news/src/moz.build32
-rw-r--r--comm/mailnews/news/src/nntpCore.h165
-rw-r--r--comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp79
-rw-r--r--comm/mailnews/news/src/nsNewsDownloadDialogArgs.h29
-rw-r--r--comm/mailnews/news/src/nsNewsDownloader.cpp507
-rw-r--r--comm/mailnews/news/src/nsNewsDownloader.h136
-rw-r--r--comm/mailnews/news/src/nsNewsFolder.cpp1645
-rw-r--r--comm/mailnews/news/src/nsNewsFolder.h144
-rw-r--r--comm/mailnews/news/src/nsNewsUtils.cpp57
-rw-r--r--comm/mailnews/news/src/nsNewsUtils.h30
-rw-r--r--comm/mailnews/news/src/nsNntpUrl.cpp476
-rw-r--r--comm/mailnews/news/src/nsNntpUrl.h68
24 files changed, 6860 insertions, 0 deletions
diff --git a/comm/mailnews/news/src/NewsAutoCompleteSearch.jsm b/comm/mailnews/news/src/NewsAutoCompleteSearch.jsm
new file mode 100644
index 0000000000..88716e01bc
--- /dev/null
+++ b/comm/mailnews/news/src/NewsAutoCompleteSearch.jsm
@@ -0,0 +1,134 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["NewsAutoCompleteSearch"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var kACR = Ci.nsIAutoCompleteResult;
+var kSupportedTypes = new Set(["addr_newsgroups", "addr_followup"]);
+
+function NewsAutoCompleteResult(aSearchString) {
+ // Can't create this in the prototype as we'd get the same array for
+ // all instances
+ this._searchResults = [];
+ this.searchString = aSearchString;
+}
+
+NewsAutoCompleteResult.prototype = {
+ _searchResults: null,
+
+ // nsIAutoCompleteResult
+
+ searchString: null,
+ searchResult: kACR.RESULT_NOMATCH,
+ defaultIndex: -1,
+ errorDescription: null,
+
+ get matchCount() {
+ return this._searchResults.length;
+ },
+
+ getValueAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getLabelAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getCommentAt(aIndex) {
+ return this._searchResults[aIndex].comment;
+ },
+
+ getStyleAt(aIndex) {
+ return "subscribed-news-abook";
+ },
+
+ getImageAt(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt(aRowIndex, aRemoveFromDB) {},
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]),
+};
+
+function NewsAutoCompleteSearch() {}
+
+NewsAutoCompleteSearch.prototype = {
+ // For component registration
+ classDescription: "Newsgroup Autocomplete",
+
+ cachedAccountKey: "",
+ cachedServer: null,
+
+ /**
+ * Find the newsgroup server associated with the given accountKey.
+ *
+ * @param accountKey The key of the account.
+ * @returns The incoming news server (or null if one does not exist).
+ */
+ _findServer(accountKey) {
+ let account = MailServices.accounts.getAccount(accountKey);
+
+ if (account.incomingServer.type == "nntp") {
+ return account.incomingServer;
+ }
+ return null;
+ },
+
+ // nsIAutoCompleteSearch
+ startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) {
+ let params = aSearchParam ? JSON.parse(aSearchParam) : {};
+ let result = new NewsAutoCompleteResult(aSearchString);
+ if (
+ !("type" in params) ||
+ !("accountKey" in params) ||
+ !kSupportedTypes.has(params.type)
+ ) {
+ result.searchResult = kACR.RESULT_IGNORED;
+ aListener.onSearchResult(this, result);
+ return;
+ }
+
+ if ("accountKey" in params && params.accountKey != this.cachedAccountKey) {
+ this.cachedAccountKey = params.accountKey;
+ this.cachedServer = this._findServer(params.accountKey);
+ }
+
+ if (this.cachedServer) {
+ for (let curr of this.cachedServer.rootFolder.subFolders) {
+ if (curr.prettyName.includes(aSearchString)) {
+ result._searchResults.push({
+ value: curr.prettyName,
+ comment: this.cachedServer.prettyName,
+ });
+ }
+ }
+ }
+
+ if (result.matchCount) {
+ result.searchResult = kACR.RESULT_SUCCESS;
+ // If the user does not select anything, use the first entry:
+ result.defaultIndex = 0;
+ }
+ aListener.onSearchResult(this, result);
+ },
+
+ stopSearch() {},
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]),
+};
diff --git a/comm/mailnews/news/src/NewsDownloader.sys.mjs b/comm/mailnews/news/src/NewsDownloader.sys.mjs
new file mode 100644
index 0000000000..be94dfb96e
--- /dev/null
+++ b/comm/mailnews/news/src/NewsDownloader.sys.mjs
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { NntpUtils } = ChromeUtils.import("resource:///modules/NntpUtils.jsm");
+
+/**
+ * Download articles in all subscribed newsgroups for offline use.
+ */
+export class NewsDownloader {
+ _logger = NntpUtils.logger;
+
+ /**
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIUrlListener} urlListener - Callback for the request.
+ */
+ constructor(msgWindow, urlListener) {
+ this._msgWindow = msgWindow;
+ this._urlListener = urlListener;
+
+ this._bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ }
+
+ /**
+ * Actually start the download process.
+ */
+ async start() {
+ this._logger.debug("Start downloading articles for offline use");
+ let servers = MailServices.accounts.allServers.filter(
+ x => x.type == "nntp"
+ );
+ // Download all servers concurrently.
+ await Promise.all(
+ servers.map(async server => {
+ let folders = server.rootFolder.descendants;
+ for (let folder of folders) {
+ if (folder.flags & Ci.nsMsgFolderFlags.Offline) {
+ // Download newsgroups set for offline use in a server one by one.
+ await this._downloadFolder(folder);
+ }
+ }
+ })
+ );
+
+ this._urlListener.OnStopRunningUrl(null, Cr.NS_OK);
+
+ this._logger.debug("Finished downloading articles for offline use");
+ this._msgWindow.statusFeedback.showStatusString("");
+ }
+
+ /**
+ * Download articles in a newsgroup one by one.
+ *
+ * @param {nsIMsgFolder} folder - The newsgroup folder.
+ */
+ async _downloadFolder(folder) {
+ this._logger.debug(`Start downloading ${folder.URI}`);
+
+ folder.QueryInterface(Ci.nsIMsgNewsFolder).saveArticleOffline = true;
+ let keysToDownload = await this._getKeysToDownload(folder);
+
+ let i = 0;
+ let total = keysToDownload.size;
+ for (let key of keysToDownload) {
+ await new Promise(resolve => {
+ MailServices.nntp.fetchMessage(folder, key, this._msgWindow, null, {
+ OnStartRunningUrl() {},
+ OnStopRunningUrl() {
+ resolve();
+ },
+ });
+ });
+ this._msgWindow.statusFeedback.showStatusString(
+ this._bundle.formatStringFromName("downloadingArticlesForOffline", [
+ ++i,
+ total,
+ folder.prettyName,
+ ])
+ );
+ }
+
+ folder.saveArticleOffline = false;
+ }
+
+ /**
+ * Use a search session to find articles that match the download settings
+ * and we don't already have.
+ *
+ * @param {nsIMsgFolder} folder - The newsgroup folder.
+ * @returns {Set<number>}
+ */
+ async _getKeysToDownload(folder) {
+ let searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ let termValue = searchSession.createTerm().value;
+
+ let downloadSettings = folder.downloadSettings;
+ if (downloadSettings.downloadUnreadOnly) {
+ termValue.attrib = Ci.nsMsgSearchAttrib.MsgStatus;
+ termValue.status = Ci.nsMsgMessageFlags.Read;
+ searchSession.addSearchTerm(
+ Ci.nsMsgSearchAttrib.MsgStatus,
+ Ci.nsMsgSearchOp.Isnt,
+ termValue,
+ true,
+ null
+ );
+ }
+ if (downloadSettings.downloadByDate) {
+ termValue.attrib = Ci.nsMsgSearchAttrib.AgeInDays;
+ termValue.age = downloadSettings.ageLimitOfMsgsToDownload;
+ searchSession.addSearchTerm(
+ Ci.nsMsgSearchAttrib.AgeInDays,
+ Ci.nsMsgSearchOp.IsLessThan,
+ termValue,
+ true,
+ null
+ );
+ }
+ termValue.attrib = Ci.nsMsgSearchAttrib.MsgStatus;
+ termValue.status = Ci.nsMsgMessageFlags.Offline;
+ searchSession.addSearchTerm(
+ Ci.nsMsgSearchAttrib.MsgStatus,
+ Ci.nsMsgSearchOp.Isnt,
+ termValue,
+ true,
+ null
+ );
+
+ let keysToDownload = new Set();
+ await new Promise(resolve => {
+ searchSession.registerListener(
+ {
+ onSearchHit(hdr, folder) {
+ if (!(hdr.flags & Ci.nsMsgMessageFlags.Offline)) {
+ // Only need to download articles we don't already have.
+ keysToDownload.add(hdr.messageKey);
+ }
+ },
+ onSearchDone: status => {
+ resolve();
+ },
+ },
+ Ci.nsIMsgSearchSession.allNotifications
+ );
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.localNews, folder);
+ searchSession.search(this._msgWindow);
+ });
+
+ return keysToDownload;
+ }
+}
diff --git a/comm/mailnews/news/src/NntpChannel.jsm b/comm/mailnews/news/src/NntpChannel.jsm
new file mode 100644
index 0000000000..4e20fca7bc
--- /dev/null
+++ b/comm/mailnews/news/src/NntpChannel.jsm
@@ -0,0 +1,402 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = ["NntpChannel"];
+
+const { MailChannel } = ChromeUtils.importESModule(
+ "resource:///modules/MailChannel.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",
+ NntpUtils: "resource:///modules/NntpUtils.jsm",
+});
+
+/**
+ * A channel to interact with NNTP server.
+ *
+ * @implements {nsIChannel}
+ * @implements {nsIRequest}
+ * @implements {nsICacheEntryOpenCallback}
+ */
+class NntpChannel extends MailChannel {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMailChannel",
+ "nsIChannel",
+ "nsIRequest",
+ "nsICacheEntryOpenCallback",
+ ]);
+
+ _logger = lazy.NntpUtils.logger;
+ _status = Cr.NS_OK;
+
+ /**
+ * @param {nsIURI} uri - The uri to construct the channel from.
+ * @param {nsILoadInfo} [loadInfo] - The loadInfo associated with the channel.
+ */
+ constructor(uri, loadInfo) {
+ super();
+ this._server = lazy.NntpUtils.findServer(uri.asciiHost);
+ if (!this._server) {
+ this._server = MailServices.accounts
+ .createIncomingServer("", uri.asciiHost, "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ this._server.port = uri.port;
+ }
+
+ if (uri.port < 1) {
+ // Ensure the uri has a port so that memory cache works.
+ uri = uri.mutate().setPort(this._server.port).finalize();
+ }
+
+ // Two forms of the uri:
+ // - news://news.mozilla.org:119/mailman.30.1608649442.1056.accessibility%40lists.mozilla.org?group=mozilla.accessibility&key=378
+ // - news://news.mozilla.org:119/id@mozilla.org
+ let url = new URL(uri.spec);
+ this._groupName = url.searchParams.get("group");
+ if (this._groupName) {
+ this._newsFolder = this._server.rootFolder.getChildNamed(
+ decodeURIComponent(url.searchParams.get("group"))
+ );
+ this._articleNumber = url.searchParams.get("key");
+ } else {
+ this._messageId = decodeURIComponent(url.pathname.slice(1));
+ if (!this._messageId.includes("@")) {
+ this._groupName = this._messageId;
+ this._messageId = null;
+ }
+ }
+
+ // nsIChannel attributes.
+ this.originalURI = uri;
+ this.URI = uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ this.loadInfo = loadInfo || {
+ QueryInterface: ChromeUtils.generateQI(["nsILoadInfo"]),
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ internalContentPolicy: Ci.nsIContentPolicy.TYPE_OTHER,
+ };
+ this.contentLength = 0;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {string}
+ */
+ get name() {
+ return this.URI?.spec;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {boolean}
+ */
+ isPending() {
+ return !!this._pending;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {nsresult}
+ */
+ get status() {
+ return this._status;
+ }
+
+ /**
+ * @see nsICacheEntryOpenCallback
+ */
+ onCacheEntryAvailable(entry, isNew, status) {
+ if (!Components.isSuccessCode(status)) {
+ // If memory cache doesn't work, read from the server.
+ this._readFromServer();
+ return;
+ }
+
+ if (isNew) {
+ if (Services.io.offline) {
+ this._status = Cr.NS_ERROR_OFFLINE;
+ return;
+ }
+ // It's a new entry, needs to read from the server.
+ let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance(
+ Ci.nsIStreamListenerTee
+ );
+ let outStream = entry.openOutputStream(0, -1);
+ // When the tee stream receives data from the server, it writes to both
+ // the original listener and outStream (memory cache).
+ tee.init(this._listener, outStream, null);
+ this._listener = tee;
+ this._cacheEntry = entry;
+ this._readFromServer();
+ return;
+ }
+
+ // It's an old entry, read from the memory cache.
+ this._readFromCacheStream(entry.openInputStream(0));
+ }
+
+ onCacheEntryCheck(entry) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ }
+
+ /**
+ * @see nsIChannel
+ */
+ get contentType() {
+ return this._contentType || "message/rfc822";
+ }
+
+ set contentType(value) {
+ this._contentType = value;
+ }
+
+ get isDocument() {
+ return true;
+ }
+
+ open() {
+ throw Components.Exception(
+ `${this.constructor.name}.open not implemented`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ asyncOpen(listener) {
+ this._logger.debug("asyncOpen", this.URI.spec);
+ let url = new URL(this.URI.spec);
+ this._listener = listener;
+ if (url.searchParams.has("list-ids")) {
+ // Triggered by newsError.js.
+ this._removeExpired(decodeURIComponent(url.pathname.slice(1)));
+ return;
+ }
+
+ if (this._groupName && !this._server.containsNewsgroup(this._groupName)) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ let result = Services.prompt.confirm(
+ win,
+ null,
+ bundle.formatStringFromName("autoSubscribeText", [this._groupName])
+ );
+ if (!result) {
+ return;
+ }
+ this._server.subscribeToNewsgroup(this._groupName);
+ let folder = this._server.findGroup(this._groupName);
+ lazy.MailUtils.displayFolderIn3Pane(folder.URI);
+ }
+
+ if (this._groupName && !this._articleNumber && !this._messageId) {
+ let folder = this._server.findGroup(this._groupName);
+ lazy.MailUtils.displayFolderIn3Pane(folder.URI);
+ return;
+ }
+
+ if (url.searchParams.has("part")) {
+ let converter = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ this._listener = converter.asyncConvertData(
+ "message/rfc822",
+ "*/*",
+ listener,
+ this
+ );
+ }
+ try {
+ // Attempt to get the message from the offline storage.
+ try {
+ if (this._readFromOfflineStorage()) {
+ return;
+ }
+ } catch (e) {
+ this._logger.warn(e);
+ }
+
+ let uri = this.URI;
+ if (url.search) {
+ // A full news url may look like
+ // news://<host>:119/<Msg-ID>?group=<name>&key=<key>&header=quotebody.
+ // Remove any query strings to keep the cache key stable.
+ uri = uri.mutate().setQuery("").finalize();
+ }
+
+ // Check if a memory cache is available for the current URI.
+ MailServices.nntp.cacheStorage.asyncOpenURI(
+ uri,
+ "",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ this
+ );
+ } catch (e) {
+ this._logger.warn(e);
+ this._readFromServer();
+ }
+ if (this._status == Cr.NS_ERROR_OFFLINE) {
+ throw new Components.Exception(
+ "The requested action could not be completed in the offline state",
+ Cr.NS_ERROR_OFFLINE
+ );
+ }
+ }
+
+ /**
+ * Try to read the article from the offline storage.
+ *
+ * @returns {boolean} True if successfully read from the offline storage.
+ */
+ _readFromOfflineStorage() {
+ if (!this._newsFolder) {
+ return false;
+ }
+ if (!this._newsFolder.hasMsgOffline(this._articleNumber)) {
+ return false;
+ }
+ let hdr = this._newsFolder.GetMessageHeader(this._articleNumber);
+ let stream = this._newsFolder.getLocalMsgStream(hdr);
+ this._readFromCacheStream(stream);
+ return true;
+ }
+
+ /**
+ * Read the article from the a stream.
+ *
+ * @param {nsIInputStream} cacheStream - The input stream to read.
+ */
+ _readFromCacheStream(cacheStream) {
+ let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ this.contentLength = 0;
+ this._contentType = "";
+ pump.init(cacheStream, 0, 0, true);
+ pump.asyncRead({
+ onStartRequest: () => {
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ },
+ onStopRequest: (request, status) => {
+ this._listener.onStopRequest(this, status);
+ try {
+ this.loadGroup?.removeRequest(this, null, Cr.NS_OK);
+ } catch (e) {}
+ this._pending = false;
+ },
+ onDataAvailable: (request, stream, offset, count) => {
+ this.contentLength += count;
+ this._listener.onDataAvailable(this, stream, offset, count);
+ try {
+ if (!cacheStream.available()) {
+ cacheStream.close();
+ }
+ } catch (e) {}
+ },
+ });
+ }
+
+ /**
+ * Retrieve the article from the server.
+ */
+ _readFromServer() {
+ this._logger.debug("Read from server");
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ let inputStream = pipe.inputStream;
+ let outputStream = pipe.outputStream;
+ if (this._newsFolder) {
+ this._newsFolder.QueryInterface(Ci.nsIMsgNewsFolder).saveArticleOffline =
+ this._newsFolder.shouldStoreMsgOffline(this._articleNumber);
+ }
+
+ this._server.wrappedJSObject.withClient(client => {
+ let msgWindow;
+ try {
+ msgWindow = this.URI.msgWindow;
+ } catch (e) {}
+ client.startRunningUrl(null, msgWindow, this.URI);
+ client.channel = this;
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ client.onOpen = () => {
+ if (this._messageId) {
+ client.getArticleByMessageId(this._messageId);
+ } else {
+ client.getArticleByArticleNumber(
+ this._groupName,
+ this._articleNumber
+ );
+ }
+ };
+
+ client.onData = data => {
+ this.contentLength += data.length;
+ outputStream.write(data, data.length);
+ this._listener.onDataAvailable(this, inputStream, 0, data.length);
+ };
+
+ client.onDone = status => {
+ try {
+ this.loadGroup?.removeRequest(this, null, Cr.NS_OK);
+ } catch (e) {}
+ if (status != Cr.NS_OK) {
+ // Prevent marking a message as read.
+ this.URI.errorCode = status;
+ // Remove the invalid cache.
+ this._cacheEntry?.asyncDoom(null);
+ }
+ this._listener.onStopRequest(this, status);
+ this._newsFolder?.msgDatabase.commit(
+ Ci.nsMsgDBCommitType.kSessionCommit
+ );
+ this._pending = false;
+ };
+ });
+ }
+
+ /**
+ * Fetch all the article keys on the server, then remove expired keys from the
+ * local folder.
+ *
+ * @param {string} groupName - The group to check.
+ */
+ _removeExpired(groupName) {
+ this._logger.debug("_removeExpired", groupName);
+ let newsFolder = this._server.findGroup(groupName);
+ let allKeys = new Set(newsFolder.msgDatabase.listAllKeys());
+ this._server.wrappedJSObject.withClient(client => {
+ let msgWindow;
+ try {
+ msgWindow = this.URI.msgWindow;
+ } catch (e) {}
+ client.startRunningUrl(null, msgWindow, this.URI);
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ client.onOpen = () => {
+ client.listgroup(groupName);
+ };
+
+ client.onData = data => {
+ allKeys.delete(+data);
+ };
+
+ client.onDone = status => {
+ newsFolder.removeMessages([...allKeys]);
+ this._listener.onStopRequest(this, status);
+ this._pending = false;
+ };
+ });
+ }
+}
diff --git a/comm/mailnews/news/src/NntpClient.jsm b/comm/mailnews/news/src/NntpClient.jsm
new file mode 100644
index 0000000000..dfbd9fde10
--- /dev/null
+++ b/comm/mailnews/news/src/NntpClient.jsm
@@ -0,0 +1,981 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = ["NntpClient"];
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { LineReader } = ChromeUtils.import("resource:///modules/LineReader.jsm");
+var { NntpNewsGroup } = ChromeUtils.import(
+ "resource:///modules/NntpNewsGroup.jsm"
+);
+
+// Server response code.
+const AUTH_ACCEPTED = 281;
+const AUTH_PASSWORD_REQUIRED = 381;
+const AUTH_REQUIRED = 480;
+const AUTH_FAILED = 481;
+const SERVICE_UNAVAILABLE = 502;
+const NOT_SUPPORTED = 503;
+const XPAT_OK = 221;
+
+const NNTP_ERROR_MESSAGE = -304;
+
+/**
+ * A structure to represent a response received from the server. A response can
+ * be a single status line of a multi-line data block.
+ *
+ * @typedef {object} NntpResponse
+ * @property {number} status - The status code of the response.
+ * @property {string} statusText - The status line of the response excluding the
+ * status code.
+ * @property {string} data - The part of a multi-line data block excluding the
+ * status line.
+ */
+
+/**
+ * A class to interact with NNTP server.
+ */
+class NntpClient {
+ /**
+ * @param {nsINntpIncomingServer} server - The associated server instance.
+ * @param {string} uri - The server uri.
+ */
+ constructor(server) {
+ this._server = server;
+ this._lineReader = new LineReader();
+
+ this._reset();
+ this._logger = console.createInstance({
+ prefix: "mailnews.nntp",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.nntp.loglevel",
+ });
+ }
+
+ /**
+ * @type {NntpAuthenticator} - An authentication helper.
+ */
+ get _authenticator() {
+ if (!this._nntpAuthenticator) {
+ var { NntpAuthenticator } = ChromeUtils.import(
+ "resource:///modules/MailAuthenticator.jsm"
+ );
+ this._nntpAuthenticator = new NntpAuthenticator(this._server);
+ }
+ return this._nntpAuthenticator;
+ }
+
+ /**
+ * Reset some internal states to be safely reused.
+ */
+ _reset() {
+ this.onOpen = () => {};
+ this.onError = () => {};
+ this.onData = () => {};
+ this.onDone = () => {};
+
+ this.runningUri = null;
+ this.urlListener = null;
+ this._msgWindow = null;
+ this._newsFolder = null;
+ }
+
+ /**
+ * Initiate a connection to the server
+ */
+ connect() {
+ this._done = false;
+ if (this._socket?.readyState == "open") {
+ // Reuse the connection.
+ this.onOpen();
+ } else {
+ // Start a new connection.
+ this._authenticated = false;
+ let hostname = this._server.hostName.toLowerCase();
+ let useSecureTransport = this._server.isSecure;
+ this._logger.debug(
+ `Connecting to ${useSecureTransport ? "snews" : "news"}://${hostname}:${
+ this._server.port
+ }`
+ );
+ this._socket = new TCPSocket(hostname, this._server.port, {
+ binaryType: "arraybuffer",
+ useSecureTransport,
+ });
+ this._socket.onopen = this._onOpen;
+ this._socket.onerror = this._onError;
+ this._showNetworkStatus(Ci.nsISocketTransport.STATUS_CONNECTING_TO);
+ }
+ }
+
+ /**
+ * Construct an nsIMsgMailNewsUrl instance, setup urlListener to notify when
+ * the current request is finished.
+ *
+ * @param {nsIUrlListener} urlListener - Callback for the request.
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIMsgMailNewsUrl} [runningUrl] - The url to run, if provided.
+ * @returns {nsIMsgMailNewsUrl}
+ */
+ startRunningUrl(urlListener, msgWindow, runningUri) {
+ this.urlListener = urlListener;
+ this._msgWindow = msgWindow;
+ this.runningUri = runningUri;
+ if (!this.runningUri) {
+ this.runningUri = Services.io
+ .newURI(`news://${this._server.hostName}:${this._server.port}`)
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ }
+ if (msgWindow) {
+ this.runningUri.msgWindow = msgWindow;
+ }
+ this.urlListener?.OnStartRunningUrl(this.runningUri, Cr.NS_OK);
+ this.runningUri.SetUrlState(true, Cr.NS_OK);
+ return this.runningUri;
+ }
+
+ /**
+ * The open event handler.
+ */
+ _onOpen = () => {
+ this._logger.debug("Connected");
+ this._socket.ondata = this._onData;
+ this._socket.onclose = this._onClose;
+ this._inReadingMode = false;
+ this._currentGroupName = null;
+ this._nextAction = ({ status }) => {
+ if ([200, 201].includes(status)) {
+ this._nextAction = null;
+ this.onOpen();
+ } else {
+ this.quit(Cr.NS_ERROR_FAILURE);
+ }
+ };
+ this._showNetworkStatus(Ci.nsISocketTransport.STATUS_CONNECTED_TO);
+ };
+
+ /**
+ * The data event handler.
+ *
+ * @param {TCPSocketEvent} event - The data event.
+ */
+ _onData = event => {
+ let stringPayload = CommonUtils.arrayBufferToByteString(
+ new Uint8Array(event.data)
+ );
+ this._logger.debug(`S: ${stringPayload}`);
+
+ let res = this._parse(stringPayload);
+ switch (res.status) {
+ case AUTH_REQUIRED:
+ this._currentGroupName = null;
+ this._actionAuthUser();
+ return;
+ case SERVICE_UNAVAILABLE:
+ this._actionError(NNTP_ERROR_MESSAGE, res.statusText);
+ return;
+ default:
+ if (
+ res.status != AUTH_FAILED &&
+ res.status >= 400 &&
+ res.status < 500
+ ) {
+ if (this._messageId || this._articleNumber) {
+ let uri = `about:newserror?r=${res.statusText}`;
+
+ if (this._messageId) {
+ uri += `&m=${encodeURIComponent(this._messageId)}`;
+ } else {
+ let msgId = this._newsFolder?.getMessageIdForKey(
+ this._articleNumber
+ );
+ if (msgId) {
+ uri += `&m=${encodeURIComponent(msgId)}`;
+ }
+ uri += `&k=${this._articleNumber}`;
+ }
+ if (this._newsFolder) {
+ uri += `&f=${this._newsFolder.URI}`;
+ }
+ // Store the uri to display. The registered uriListener will get
+ // notified when we stop running the uri, and can act on this data.
+ this.runningUri.seeOtherURI = uri;
+ }
+ this._actionError(NNTP_ERROR_MESSAGE, res.statusText);
+ return;
+ }
+ }
+
+ try {
+ this._nextAction?.(res);
+ } catch (e) {
+ this._logger.error(`Failed to process server response ${res}.`, e);
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ }
+ };
+
+ /**
+ * The error event handler.
+ *
+ * @param {TCPSocketErrorEvent} event - The error event.
+ */
+ _onError = event => {
+ this._logger.error(event, event.name, event.message, event.errorCode);
+ let errorName;
+ let uri;
+ switch (event.errorCode) {
+ case Cr.NS_ERROR_UNKNOWN_HOST:
+ case Cr.NS_ERROR_UNKNOWN_PROXY_HOST:
+ errorName = "unknownHostError";
+ uri = "about:neterror?e=dnsNotFound";
+ break;
+ case Cr.NS_ERROR_CONNECTION_REFUSED:
+ errorName = "connectionRefusedError";
+ uri = "about:neterror?e=connectionFailure";
+ break;
+ case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED:
+ errorName = "connectionRefusedError";
+ uri = "about:neterror?e=proxyConnectFailure";
+ break;
+ case Cr.NS_ERROR_NET_TIMEOUT:
+ errorName = "netTimeoutError";
+ uri = "about:neterror?e=netTimeout";
+ break;
+ case Cr.NS_ERROR_NET_RESET:
+ errorName = "netResetError";
+ uri = "about:neterror?e=netReset";
+ break;
+ case Cr.NS_ERROR_NET_INTERRUPT:
+ errorName = "netInterruptError";
+ uri = "about:neterror?e=netInterrupt";
+ break;
+ }
+ if (errorName && uri) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ let errorMessage = bundle.formatStringFromName(errorName, [
+ this._server.hostName,
+ ]);
+ MailServices.mailSession.alertUser(errorMessage, this.runningUri);
+
+ // If we were going to display an article, instead show an error page.
+ this.runningUri.seeOtherURI = uri;
+ }
+
+ this._msgWindow?.statusFeedback?.showStatusString("");
+ this.quit(event.errorCode);
+ };
+
+ /**
+ * The close event handler.
+ */
+ _onClose = () => {
+ this._logger.debug("Connection closed.");
+ };
+
+ /**
+ * Parse the server response.
+ *
+ * @param {string} str - Response received from the server.
+ * @returns {NntpResponse}
+ */
+ _parse(str) {
+ if (this._lineReader.processingMultiLineResponse) {
+ // When processing multi-line response, no parsing should happen.
+ return { data: str };
+ }
+ let matches = /^(\d{3}) (.+)\r\n([^]*)/.exec(str);
+ if (matches) {
+ let [, status, statusText, data] = matches;
+ return { status: Number(status), statusText, data };
+ }
+ return { data: str };
+ }
+
+ /**
+ * Send a command to the socket.
+ *
+ * @param {string} str - The command string to send.
+ * @param {boolean} [suppressLogging=false] - Whether to suppress logging the str.
+ */
+ _sendCommand(str, suppressLogging) {
+ if (this._socket.readyState != "open") {
+ if (str != "QUIT") {
+ this._logger.warn(
+ `Failed to send "${str}" because socket state is ${this._socket.readyState}`
+ );
+ }
+ return;
+ }
+ if (suppressLogging && AppConstants.MOZ_UPDATE_CHANNEL != "default") {
+ this._logger.debug(
+ "C: Logging suppressed (it probably contained auth information)"
+ );
+ } else {
+ // Do not suppress for non-release builds, so that debugging auth problems
+ // is easier.
+ this._logger.debug(`C: ${str}`);
+ }
+ this.send(str + "\r\n");
+ }
+
+ /**
+ * Send a string to the socket.
+ *
+ * @param {string} str - The string to send.
+ */
+ send(str) {
+ this._socket.send(CommonUtils.byteStringToArrayBuffer(str).buffer);
+ }
+
+ /**
+ * Send a LIST or NEWGROUPS command to get groups in the current server.
+ *
+ * @param {boolean} getOnlyNew - List only new groups.
+ */
+ getListOfGroups(getOnlyNew) {
+ if (!getOnlyNew) {
+ this._actionModeReader(this._actionList);
+ } else {
+ this._actionModeReader(this._actionNewgroups);
+ }
+ this.urlListener = this._server.QueryInterface(Ci.nsIUrlListener);
+ }
+
+ /**
+ * Get new articles.
+ *
+ * @param {string} groupName - The group to get new articles.
+ * @param {boolean} getOld - Get old articles as well.
+ */
+ getNewNews(groupName, getOld) {
+ this._currentGroupName = null;
+ this._newsFolder = this._getNewsFolder(groupName);
+ this._newsGroup = new NntpNewsGroup(this._server, this._newsFolder);
+ this._newsGroup.getOldMessages = getOld;
+ this._nextGroupName = this._newsFolder.rawName;
+ this.runningUri.updatingFolder = true;
+ this._firstGroupCommand = this._actionXOver;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Get a single article by group name and article number.
+ *
+ * @param {string} groupName - The group name.
+ * @param {integer} articleNumber - The article number.
+ */
+ getArticleByArticleNumber(groupName, articleNumber) {
+ this._newsFolder = this._server.rootFolder.getChildNamed(groupName);
+ this._nextGroupName = this._getNextGroupName(groupName);
+ this._articleNumber = articleNumber;
+ this._messageId = "";
+ this._firstGroupCommand = this._actionArticle;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Get a single article by the message id.
+ *
+ * @param {string} messageId - The message id.
+ */
+ getArticleByMessageId(messageId) {
+ this._messageId = `<${messageId}>`;
+ this._articleNumber = 0;
+ this._actionModeReader(this._actionArticle);
+ }
+
+ /**
+ * Send a `Control: cancel <msg-id>` message to cancel an article, not every
+ * server supports it, see rfc5537.
+ *
+ * @param {string} groupName - The group name.
+ */
+ cancelArticle(groupName) {
+ this._nextGroupName = this._getNextGroupName(groupName);
+ this._firstGroupCommand = this.post;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Send a `XPAT <header> <message-id> <pattern>` message, not every server
+ * supports it, see rfc2980.
+ *
+ * @param {string} groupName - The group name.
+ * @param {string[]} xpatLines - An array of xpat lines to send.
+ */
+ search(groupName, xpatLines) {
+ this._nextGroupName = this._getNextGroupName(groupName);
+ this._xpatLines = xpatLines;
+ this._firstGroupCommand = this._actionXPat;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Load a news uri directly, see rfc5538 about supported news uri.
+ *
+ * @param {string} uri - The news uri to load.
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIStreamListener} streamListener - The listener for the request.
+ */
+ loadNewsUrl(uri, msgWindow, streamListener) {
+ this._logger.debug(`Loading ${uri}`);
+ let url = new URL(uri);
+ let path = url.pathname.slice(1);
+ let action;
+ if (path == "*") {
+ action = () => this.getListOfGroups();
+ } else if (path.includes("@")) {
+ action = () => this.getArticleByMessageId(path);
+ } else {
+ this._newsFolder = this._getNewsFolder(path);
+ this._newsGroup = new NntpNewsGroup(this._server, this._newsFolder);
+ this._nextGroupName = this._newsFolder.rawName;
+ action = () => this._actionModeReader(this._actionGroup);
+ }
+ if (!action) {
+ return;
+ }
+ this._msgWindow = msgWindow;
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ let inputStream = pipe.inputStream;
+ let outputStream = pipe.outputStream;
+ this.onOpen = () => {
+ streamListener.onStartRequest(null, Cr.NS_OK);
+ action();
+ };
+ this.onData = data => {
+ outputStream.write(data, data.length);
+ streamListener.onDataAvailable(null, inputStream, 0, data.length);
+ };
+ this.onDone = status => {
+ streamListener.onStopRequest(null, status);
+ };
+ }
+
+ /**
+ * Send LISTGROUP request to the server.
+ *
+ * @param {string} groupName - The group to request.
+ */
+ listgroup(groupName) {
+ this._actionModeReader(() => {
+ this._nextAction = this._actionListgroupResponse;
+ this._sendCommand(`LISTGROUP ${groupName}`);
+ });
+ }
+
+ /**
+ * Send `POST` request to the server.
+ */
+ post() {
+ let action = () => {
+ this._nextAction = this._actionHandlePost;
+ this._sendCommand("POST");
+ };
+ if (this._server.pushAuth && !this._authenticated) {
+ this._currentAction = action;
+ this._actionAuthUser();
+ } else {
+ action();
+ }
+ }
+
+ /**
+ * Send `QUIT` request to the server.
+ */
+ quit(status = Cr.NS_OK) {
+ this._sendCommand("QUIT");
+ this._nextAction = this.close;
+ this.close();
+ this._actionDone(status);
+ }
+
+ /**
+ * Close the socket.
+ */
+ close() {
+ this._socket.close();
+ }
+
+ /**
+ * Get the news folder corresponding to a group name.
+ *
+ * @param {string} groupName - The group name.
+ * @returns {nsIMsgNewsFolder}
+ */
+ _getNewsFolder(groupName) {
+ return this._server.rootFolder
+ .getChildNamed(groupName)
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ }
+
+ /**
+ * Given a UTF-8 group name, return the underlying group name used by the server.
+ *
+ * @param {string} groupName - The UTF-8 group name.
+ * @returns {BinaryString} - The group name that can be sent to the server.
+ */
+ _getNextGroupName(groupName) {
+ return this._getNewsFolder(groupName).rawName;
+ }
+
+ /**
+ * Send `MODE READER` request to the server.
+ */
+ _actionModeReader(nextAction) {
+ if (this._inReadingMode) {
+ nextAction();
+ } else {
+ this._currentAction = () => {
+ this._inReadingMode = false;
+ this._actionModeReader(nextAction);
+ };
+ this._sendCommand("MODE READER");
+ this._inReadingMode = true;
+ this._nextAction = () => {
+ if (this._server.pushAuth && !this._authenticated) {
+ this._currentAction = nextAction;
+ this._actionAuthUser();
+ } else {
+ nextAction();
+ }
+ };
+ }
+ }
+
+ /**
+ * Send `LIST` request to the server.
+ */
+ _actionList = () => {
+ this._sendCommand("LIST");
+ this._currentAction = this._actionList;
+ this._nextAction = this._actionReadData;
+ };
+
+ /**
+ * Send `NEWGROUPS` request to the server.
+ * @see rfc3977#section-7.3
+ */
+ _actionNewgroups = () => {
+ const days = Services.prefs.getIntPref("news.newgroups_for_num_days", 180);
+ const dateTime = new Date(Date.now() - 86400000 * days)
+ .toISOString()
+ .replace(
+ /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).*/,
+ "$1$2$3 $4$5$6"
+ );
+ this._sendCommand("NEWGROUPS " + dateTime + " GMT");
+ this._currentAction = this._actionNewgroups;
+ this._nextAction = this._actionReadData;
+ };
+
+ /**
+ * Send `GROUP` request to the server.
+ */
+ _actionGroup = () => {
+ this._firstGroupCommand = this._firstGroupCommand || this._actionXOver;
+ if (this._nextGroupName == this._currentGroupName) {
+ this._firstGroupCommand();
+ } else {
+ this._sendCommand(`GROUP ${this._nextGroupName}`);
+ this._currentAction = this._actionGroup;
+ this._currentGroupName = this._nextGroupName;
+ this._nextAction = this._actionGroupResponse;
+ }
+ };
+
+ /**
+ * Handle GROUP response.
+ *
+ * @param {NntpResponse} res - GROUP response received from the server.
+ */
+ _actionGroupResponse = res => {
+ if (res.status == 411) {
+ this._server.groupNotFound(null, this._currentGroupName, true);
+ return;
+ }
+ this._firstGroupCommand(res);
+ };
+
+ /**
+ * Consume the status line of LISTGROUP response.
+ */
+ _actionListgroupResponse = res => {
+ this._nextAction = this._actionListgroupDataResponse;
+ if (res.data) {
+ this._actionListgroupDataResponse(res);
+ }
+ };
+
+ /**
+ * Consume the multi-line data of LISTGROUP response.
+ *
+ * @param {NntpResponse} res - The server response.
+ */
+ _actionListgroupDataResponse = ({ data }) => {
+ this._lineReader.read(
+ data,
+ line => {
+ this.onData(line);
+ },
+ () => {
+ this._actionDone();
+ }
+ );
+ };
+
+ /**
+ * Send `XOVER` request to the server.
+ */
+ _actionXOver = res => {
+ let [count, low, high] = res.statusText.split(" ");
+ this._newsFolder.updateSummaryFromNNTPInfo(low, high, count);
+ let [start, end] = this._newsGroup.getArticlesRangeToFetch(
+ this._msgWindow,
+ Number(low),
+ Number(high)
+ );
+ if (start && end) {
+ this._startArticle = start;
+ this._endArticle = end;
+ this._nextAction = this._actionXOverResponse;
+ this._sendCommand(`XOVER ${start}-${end}`);
+ } else {
+ this._actionDone();
+ }
+ };
+
+ /**
+ * A transient action to consume the status line of XOVER response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionXOverResponse(res) {
+ if (res.status == 224) {
+ this._nextAction = this._actionReadXOver;
+ this._newsGroup.addKnownArticles(this._startArticle, this._endArticle);
+ this._actionReadXOver(res);
+ } else {
+ // Somehow XOVER is not supported by the server, fallback to use HEAD to
+ // fetch one by one.
+ this._actionHead();
+ }
+ }
+
+ /**
+ * Handle XOVER response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionReadXOver({ data }) {
+ this._lineReader.read(
+ data,
+ line => {
+ this._newsGroup.processXOverLine(line);
+ },
+ () => {
+ // Fetch extra headers used by filters, but not returned in XOVER response.
+ this._xhdrFields = this._newsGroup.getXHdrFields();
+ this._actionXHdr();
+ }
+ );
+ }
+
+ /**
+ * Send `XHDR` request to the server.
+ */
+ _actionXHdr = () => {
+ this._curXHdrHeader = this._xhdrFields.shift();
+ if (this._curXHdrHeader) {
+ this._nextAction = this._actionXHdrResponse;
+ this._sendCommand(
+ `XHDR ${this._curXHdrHeader} ${this._startArticle}-${this._endArticle}`
+ );
+ } else {
+ this._newsGroup.finishProcessingXOver();
+ this._actionDone();
+ }
+ };
+
+ /**
+ * Handle XHDR response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionXHdrResponse({ status, data }) {
+ if (status == NOT_SUPPORTED) {
+ // Fallback to HEAD request.
+ this._actionHead();
+ return;
+ }
+
+ this._lineReader.read(
+ data,
+ line => {
+ this._newsGroup.processXHdrLine(this._curXHdrHeader, line);
+ },
+ this._actionXHdr
+ );
+ }
+
+ /**
+ * Send `HEAD` request to the server.
+ */
+ _actionHead = () => {
+ if (this._startArticle <= this._endArticle) {
+ this._nextAction = this._actionReadHead;
+ this._sendCommand(`HEAD ${this._startArticle}`);
+ this._newsGroup.initHdr(this._startArticle);
+ this._startArticle++;
+ } else {
+ this._newsGroup.finishProcessingXOver();
+ this._actionDone();
+ }
+ };
+
+ /**
+ * Handle HEAD response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionReadHead({ data }) {
+ this._lineReader.read(
+ data,
+ line => {
+ this._newsGroup.processHeadLine(line);
+ },
+ () => {
+ this._newsGroup.initHdr(-1);
+ this._actionHead();
+ }
+ );
+ }
+
+ /**
+ * Send `ARTICLE` request to the server.
+ * @see {@link https://www.rfc-editor.org/rfc/rfc3977#section-6.2.1|RFC 3977 §6.2.1}
+ */
+ _actionArticle = () => {
+ this._sendCommand(`ARTICLE ${this._articleNumber || this._messageId}`);
+ this._nextAction = this._actionArticleResponse;
+ this._newsFolder?.notifyDownloadBegin(this._articleNumber);
+ this._downloadingToFolder = true;
+ };
+
+ /**
+ * Handle `ARTICLE` response.
+ *
+ * @param {NntpResponse} res - ARTICLE response received from the server.
+ */
+ _actionArticleResponse = ({ data }) => {
+ let lineSeparator = AppConstants.platform == "win" ? "\r\n" : "\n";
+
+ this._lineReader.read(
+ data,
+ line => {
+ // NewsFolder will decide whether to save it to the offline storage.
+ this._newsFolder?.notifyDownloadedLine(
+ line.slice(0, -2) + lineSeparator
+ );
+ this.onData(line);
+ },
+ () => {
+ this._newsFolder?.notifyDownloadEnd(Cr.NS_OK);
+ this._downloadingToFolder = false;
+ this._actionDone();
+ }
+ );
+ };
+
+ /**
+ * Handle multi-line data blocks response, e.g. ARTICLE/LIST response. Emit
+ * each line through onData.
+ *
+ * @param {NntpResponse} res - Response received from the server.
+ */
+ _actionReadData({ data }) {
+ this._lineReader.read(data, this.onData, this._actionDone);
+ }
+
+ /**
+ * Handle POST response.
+ *
+ * @param {NntpResponse} res - POST response received from the server.
+ */
+ _actionHandlePost({ status, statusText }) {
+ if (status == 340) {
+ this.onReadyToPost();
+ } else if (status == 240) {
+ this._actionDone();
+ } else {
+ this._actionError(NNTP_ERROR_MESSAGE, statusText);
+ }
+ }
+
+ /**
+ * Send `AUTHINFO user <name>` to the server.
+ *
+ * @param {boolean} [forcePrompt=false] - Whether to force showing an auth prompt.
+ */
+ _actionAuthUser(forcePrompt = false) {
+ if (!this._newsFolder) {
+ this._newsFolder = this._server.rootFolder.QueryInterface(
+ Ci.nsIMsgNewsFolder
+ );
+ }
+ if (!this._newsFolder.groupUsername) {
+ let gotPassword = this._newsFolder.getAuthenticationCredentials(
+ this._msgWindow,
+ true,
+ forcePrompt
+ );
+ if (!gotPassword) {
+ this._actionDone(Cr.NS_ERROR_ABORT);
+ return;
+ }
+ }
+ this._sendCommand(`AUTHINFO user ${this._newsFolder.groupUsername}`, true);
+ this._nextAction = this._actionAuthResult;
+ this._authenticator.username = this._newsFolder.groupUsername;
+ }
+
+ /**
+ * Send `AUTHINFO pass <password>` to the server.
+ */
+ _actionAuthPassword() {
+ this._sendCommand(`AUTHINFO pass ${this._newsFolder.groupPassword}`, true);
+ this._nextAction = this._actionAuthResult;
+ }
+
+ /**
+ * Decide the next step according to the auth response.
+ *
+ * @param {NntpResponse} res - Auth response received from the server.
+ */
+ _actionAuthResult({ status }) {
+ switch (status) {
+ case AUTH_ACCEPTED:
+ this._authenticated = true;
+ this._currentAction?.();
+ return;
+ case AUTH_PASSWORD_REQUIRED:
+ this._actionAuthPassword();
+ return;
+ case AUTH_FAILED:
+ let action = this._authenticator.promptAuthFailed();
+ if (action == 1) {
+ // Cancel button pressed.
+ this._actionDone();
+ return;
+ }
+ if (action == 2) {
+ // 'New password' button pressed.
+ this._newsFolder.forgetAuthenticationCredentials();
+ }
+ // Retry.
+ this._actionAuthUser();
+ }
+ }
+
+ /**
+ * Send `XPAT <header> <message-id> <pattern>` to the server.
+ */
+ _actionXPat = () => {
+ let xptLine = this._xpatLines.shift();
+ if (!xptLine) {
+ this._actionDone();
+ return;
+ }
+ this._sendCommand(xptLine);
+ this._nextAction = this._actionXPatResponse;
+ };
+
+ /**
+ * Handle XPAT response.
+ *
+ * @param {NntpResponse} res - XPAT response received from the server.
+ */
+ _actionXPatResponse({ status, statusText, data }) {
+ if (status && status != XPAT_OK) {
+ this._actionError(NNTP_ERROR_MESSAGE, statusText);
+ return;
+ }
+ this._lineReader.read(data, this.onData, this._actionXPat);
+ }
+
+ /**
+ * Show network status in the status bar.
+ *
+ * @param {number} status - See NS_NET_STATUS_* in nsISocketTransport.idl.
+ */
+ _showNetworkStatus(status) {
+ let statusMessage = Services.strings.formatStatusMessage(
+ status,
+ this._server.hostName
+ );
+ this._msgWindow?.statusFeedback?.showStatusString(statusMessage);
+ }
+
+ /**
+ * Show an error prompt.
+ *
+ * @param {number} errorId - An error name corresponds to an entry of
+ * news.properties.
+ * @param {string} serverErrorMsg - Error message returned by the server.
+ */
+ _actionError(errorId, serverErrorMsg) {
+ this._logger.error(`Got an error id=${errorId}`);
+ let msgWindow = this._msgWindow;
+
+ if (!msgWindow) {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let errorMsg = bundle.GetStringFromID(errorId);
+ if (serverErrorMsg) {
+ errorMsg += " " + serverErrorMsg;
+ }
+ Services.prompt.alert(msgWindow?.domWindow, null, errorMsg);
+
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ }
+
+ /**
+ * Close the connection and do necessary cleanup.
+ */
+ _actionDone = (status = Cr.NS_OK) => {
+ if (this._done) {
+ return;
+ }
+ if (this._downloadingToFolder) {
+ // If we're in the middle of sending a message to the folder, make sure
+ // the folder knows we're aborting.
+ this._newsFolder?.notifyDownloadEnd(Cr.NS_ERROR_FAILURE);
+ this._downloadingToFolder = false;
+ }
+ this._done = true;
+ this._logger.debug(`Done with status=${status}`);
+ this.onDone(status);
+ this._newsGroup?.cleanUp();
+ this._newsFolder?.OnStopRunningUrl?.(this.runningUri, status);
+ this.urlListener?.OnStopRunningUrl(this.runningUri, status);
+ this.runningUri.SetUrlState(false, status);
+ this._reset();
+ this.onIdle?.();
+ };
+}
diff --git a/comm/mailnews/news/src/NntpIncomingServer.jsm b/comm/mailnews/news/src/NntpIncomingServer.jsm
new file mode 100644
index 0000000000..3497cbae77
--- /dev/null
+++ b/comm/mailnews/news/src/NntpIncomingServer.jsm
@@ -0,0 +1,624 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = ["NntpIncomingServer"];
+
+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"
+);
+var { MsgIncomingServer } = ChromeUtils.import(
+ "resource:///modules/MsgIncomingServer.jsm"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CommonUtils: "resource://services-common/utils.sys.mjs",
+ clearInterval: "resource://gre/modules/Timer.sys.mjs",
+ setInterval: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NntpClient: "resource:///modules/NntpClient.jsm",
+});
+
+/**
+ * A class to represent a NNTP server.
+ *
+ * @implements {nsINntpIncomingServer}
+ * @implements {nsIMsgIncomingServer}
+ * @implements {nsISupportsWeakReference}
+ * @implements {nsISubscribableServer}
+ * @implements {nsITreeView}
+ * @implements {nsIUrlListener}
+ */
+class NntpIncomingServer extends MsgIncomingServer {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsINntpIncomingServer",
+ "nsIMsgIncomingServer",
+ "nsISupportsWeakReference",
+ "nsISubscribableServer",
+ "nsITreeView",
+ "nsIUrlListener",
+ ]);
+
+ constructor() {
+ super();
+
+ this._subscribed = new Set();
+ this._groups = [];
+
+ // @type {NntpClient[]} - An array of connections can be used.
+ this._idleConnections = [];
+ // @type {NntpClient[]} - An array of connections in use.
+ this._busyConnections = [];
+ // @type {Function[]} - An array of Promise.resolve functions.
+ this._connectionWaitingQueue = [];
+
+ Services.obs.addObserver(this, "profile-before-change");
+ // Update newsrc every 5 minutes.
+ this._newsrcTimer = lazy.setInterval(() => this.writeNewsrcFile(), 300000);
+
+ // nsIMsgIncomingServer attributes.
+ this.localStoreType = "news";
+ this.localDatabaseType = "news";
+ this.canSearchMessages = true;
+ this.canCompactFoldersOnServer = false;
+ this.sortOrder = 500000000;
+
+ Object.defineProperty(this, "defaultCopiesAndFoldersPrefsToServer", {
+ // No Draft/Sent folder on news servers, will point to "Local Folders".
+ get: () => false,
+ });
+ Object.defineProperty(this, "canCreateFoldersOnServer", {
+ // No folder creation on news servers.
+ get: () => false,
+ });
+ Object.defineProperty(this, "canFileMessagesOnServer", {
+ get: () => false,
+ });
+
+ // nsISubscribableServer attributes.
+ this.supportsSubscribeSearch = true;
+
+ // nsINntpIncomingServer attributes.
+ this.newsrcHasChanged = false;
+
+ // nsINntpIncomingServer attributes that map directly to pref values.
+ this._mapAttrsToPrefs([
+ ["Bool", "notifyOn", "notify.on"],
+ ["Bool", "markOldRead", "mark_old_read"],
+ ["Bool", "abbreviate", "abbreviate"],
+ ["Bool", "pushAuth", "always_authenticate"],
+ ["Bool", "singleSignon"],
+ ["Int", "maxArticles", "max_articles"],
+ ]);
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "profile-before-change":
+ lazy.clearInterval(this._newsrcTimer);
+ this.writeNewsrcFile();
+ }
+ }
+
+ /**
+ * Most of nsISubscribableServer interfaces are delegated to
+ * this._subscribable.
+ */
+ get _subscribable() {
+ if (!this._subscribableServer) {
+ this._subscribableServer = Cc[
+ "@mozilla.org/messenger/subscribableserver;1"
+ ].createInstance(Ci.nsISubscribableServer);
+ this._subscribableServer.setIncomingServer(this);
+ }
+ return this._subscribableServer;
+ }
+
+ /** @see nsISubscribableServer */
+ get folderView() {
+ return this._subscribable.folderView;
+ }
+
+ get subscribeListener() {
+ return this._subscribable.subscribeListener;
+ }
+
+ set subscribeListener(value) {
+ this._subscribable.subscribeListener = value;
+ }
+
+ subscribeCleanup() {
+ this._subscribableServer = null;
+ }
+
+ startPopulating(msgWindow, forceToServer, getOnlyNew) {
+ this._startPopulating(msgWindow, forceToServer, getOnlyNew);
+ }
+
+ stopPopulating(msgWindow) {
+ this._subscribable.stopPopulating(msgWindow);
+ if (!this._hostInfoLoaded) {
+ this._saveHostInfo();
+ }
+ this.updateSubscribed();
+ }
+
+ addTo(name, addAsSubscribed, subscribale, changeIfExists) {
+ try {
+ this._subscribable.addTo(
+ name,
+ addAsSubscribed,
+ subscribale,
+ changeIfExists
+ );
+ this._groups.push(name);
+ } catch (e) {
+ // Group names with double dot, like alt.binaries.sounds..mp3.zappa are
+ // not working. Bug 1788572.
+ console.error(`Failed to add group ${name}. ${e}`);
+ }
+ }
+
+ subscribe(name) {
+ this.subscribeToNewsgroup(name);
+ }
+
+ unsubscribe(name) {
+ this.rootMsgFolder.propagateDelete(
+ this.rootMsgFolder.getChildNamed(name),
+ true // delete storage
+ );
+ this.newsrcHasChanged = true;
+ }
+
+ commitSubscribeChanges() {
+ this.newsrcHasChanged = true;
+ this.writeNewsrcFile();
+ }
+
+ setAsSubscribed(path) {
+ this._tmpSubscribed.add(path);
+ this._subscribable.setAsSubscribed(path);
+ }
+
+ updateSubscribed() {
+ this._tmpSubscribed = new Set();
+ this._subscribed.forEach(path => this.setAsSubscribed(path));
+ }
+
+ setState(path, state) {
+ let changed = this._subscribable.setState(path, state);
+ if (changed) {
+ if (state) {
+ this._tmpSubscribed.add(path);
+ } else {
+ this._tmpSubscribed.delete(path);
+ }
+ }
+ return changed;
+ }
+
+ hasChildren(path) {
+ return this._subscribable.hasChildren(path);
+ }
+
+ isSubscribed(path) {
+ return this._subscribable.isSubscribed(path);
+ }
+
+ isSubscribable(path) {
+ return this._subscribable.isSubscribable(path);
+ }
+
+ setSearchValue(value) {
+ this._tree?.beginUpdateBatch();
+ this._tree?.rowCountChanged(0, -this._searchResult.length);
+
+ let terms = value.toLowerCase().split(" ");
+ this._searchResult = this._groups
+ .filter(name => {
+ name = name.toLowerCase();
+ // The group name should contain all the search terms.
+ return terms.every(term => name.includes(term));
+ })
+ .sort();
+
+ this._tree?.rowCountChanged(0, this._searchResult.length);
+ this._tree?.endUpdateBatch();
+ }
+
+ getLeafName(path) {
+ return this._subscribable.getLeafName(path);
+ }
+
+ getFirstChildURI(path) {
+ return this._subscribable.getFirstChildURI(path);
+ }
+
+ getChildURIs(path) {
+ return this._subscribable.getChildURIs(path);
+ }
+
+ /** @see nsITreeView */
+ get rowCount() {
+ return this._searchResult.length;
+ }
+
+ isContainer(index) {
+ return false;
+ }
+
+ getCellProperties(row, col) {
+ if (
+ col.id == "subscribedColumn2" &&
+ this._tmpSubscribed.has(this._searchResult[row])
+ ) {
+ return "subscribed-true";
+ }
+ if (col.id == "nameColumn2") {
+ // Show the news folder icon in the search view.
+ return "serverType-nntp";
+ }
+ return "";
+ }
+
+ getCellValue(row, col) {
+ if (col.id == "nameColumn2") {
+ return this._searchResult[row];
+ }
+ return "";
+ }
+
+ getCellText(row, col) {
+ if (col.id == "nameColumn2") {
+ return this._searchResult[row];
+ }
+ return "";
+ }
+
+ setTree(tree) {
+ this._tree = tree;
+ }
+
+ /** @see nsIUrlListener */
+ OnStartRunningUrl() {}
+
+ OnStopRunningUrl() {
+ this.stopPopulating(this._msgWindow);
+ }
+
+ /** @see nsIMsgIncomingServer */
+ get serverRequiresPasswordForBiff() {
+ return false;
+ }
+
+ get filterScope() {
+ return Ci.nsMsgSearchScope.newsFilter;
+ }
+
+ get searchScope() {
+ return Services.io.offline
+ ? Ci.nsMsgSearchScope.localNewsBody
+ : Ci.nsMsgSearchScope.news;
+ }
+
+ get offlineSupportLevel() {
+ const OFFLINE_SUPPORT_LEVEL_UNDEFINED = -1;
+ const OFFLINE_SUPPORT_LEVEL_EXTENDED = 20;
+ let level = this.getIntValue("offline_support_level");
+ return level != OFFLINE_SUPPORT_LEVEL_UNDEFINED
+ ? level
+ : OFFLINE_SUPPORT_LEVEL_EXTENDED;
+ }
+
+ performExpand(msgWindow) {
+ if (!Services.prefs.getBoolPref("news.update_unread_on_expand", false)) {
+ return;
+ }
+
+ for (let folder of this.rootFolder.subFolders) {
+ folder.getNewMessages(msgWindow, null);
+ }
+ }
+
+ performBiff(msgWindow) {
+ this.performExpand(msgWindow);
+ }
+
+ closeCachedConnections() {
+ for (let client of [...this._idleConnections, ...this._busyConnections]) {
+ client.quit();
+ }
+ this._idleConnections = [];
+ this._busyConnections = [];
+ }
+
+ /** @see nsINntpIncomingServer */
+ get charset() {
+ return this.getCharValue("charset") || "UTF-8";
+ }
+
+ set charset(value) {
+ this.setCharValue("charset", value);
+ }
+
+ get maximumConnectionsNumber() {
+ let maxConnections = this.getIntValue("max_cached_connections", 0);
+ if (maxConnections > 0) {
+ return maxConnections;
+ }
+ // The default is 2 connections, if the pref value is 0, we use the default.
+ // If it's negative, treat it as 1.
+ maxConnections = maxConnections == 0 ? 2 : 1;
+ this.maximumConnectionsNumber = maxConnections;
+ return maxConnections;
+ }
+
+ set maximumConnectionsNumber(value) {
+ this.setIntValue("max_cached_connections", value);
+ }
+
+ get newsrcRootPath() {
+ let file = this.getFileValue("mail.newsrc_root-rel", "mail.newsrc_root");
+ if (!file) {
+ file = Services.dirsvc.get("NewsD", Ci.nsIFile);
+ this.setFileValue("mail.newsrc_root-rel", "mail.newsrc_root", file);
+ }
+ return file;
+ }
+
+ set newsrcRootPath(value) {
+ this.setFileValue("mail.newsrc_root-rel", "mail.newsrc_root", value);
+ }
+
+ get newsrcFilePath() {
+ if (!this._newsrcFilePath) {
+ this._newsrcFilePath = this.getFileValue(
+ "newsrc.file-rel",
+ "newsrc.file"
+ );
+ }
+ if (!this._newsrcFilePath) {
+ let prefix = "newsrc-";
+ let suffix = "";
+ if (AppConstants.platform == "win") {
+ prefix = "";
+ suffix = ".rc";
+ }
+ this._newsrcFilePath = this.newsrcRootPath;
+ this._newsrcFilePath.append(`${prefix}${this.hostName}${suffix}`);
+ this._newsrcFilePath.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ this.newsrcFilePath = this._newsrcFilePath;
+ }
+ return this._newsrcFilePath;
+ }
+
+ set newsrcFilePath(value) {
+ this._newsrcFilePath = value;
+ if (!this._newsrcFilePath.exists) {
+ this._newsrcFilePath.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ }
+ this.setFileValue("newsrc.file-rel", "newsrc.file", this._newsrcFilePath);
+ }
+
+ addNewsgroupToList(name) {
+ name = new TextDecoder(this.charset).decode(
+ lazy.CommonUtils.byteStringToArrayBuffer(name)
+ );
+ this.addTo(name, false, true, true);
+ }
+
+ addNewsgroup(name) {
+ this._subscribed.add(name);
+ }
+
+ removeNewsgroup(name) {
+ this._subscribed.delete(name);
+ }
+
+ containsNewsgroup(name) {
+ // Get subFolders triggers populating _subscribed if it wasn't set already.
+ if (this._subscribed.size == 0) {
+ this.rootFolder.QueryInterface(Ci.nsIMsgNewsFolder).subFolders;
+ }
+ return this._subscribed.has(name);
+ }
+
+ subscribeToNewsgroup(name) {
+ if (this.containsNewsgroup(name)) {
+ return;
+ }
+ this.rootMsgFolder.createSubfolder(name, null);
+ }
+
+ writeNewsrcFile() {
+ if (!this.newsrcHasChanged) {
+ return;
+ }
+
+ let newsFolder = this.rootFolder.QueryInterface(Ci.nsIMsgNewsFolder);
+ let lines = [];
+ for (let folder of newsFolder.subFolders) {
+ folder = folder.QueryInterface(Ci.nsIMsgNewsFolder);
+ if (folder.newsrcLine) {
+ lines.push(folder.newsrcLine);
+ }
+ }
+ IOUtils.writeUTF8(this.newsrcFilePath.path, lines.join(""));
+ }
+
+ findGroup(name) {
+ return this.rootMsgFolder
+ .findSubFolder(name)
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ }
+
+ loadNewsUrl(uri, msgWindow, consumer) {
+ if (consumer instanceof Ci.nsIStreamListener) {
+ this.withClient(client => {
+ client.loadNewsUrl(uri.spec, msgWindow, consumer);
+ });
+ }
+ }
+
+ forgetPassword() {
+ let newsFolder = this.rootFolder.QueryInterface(Ci.nsIMsgNewsFolder);
+ // Clear password of root folder.
+ newsFolder.forgetAuthenticationCredentials();
+
+ // Clear password of all sub folders.
+ for (let folder of newsFolder.subFolders) {
+ folder.QueryInterface(Ci.nsIMsgNewsFolder);
+ folder.forgetAuthenticationCredentials();
+ }
+ }
+
+ groupNotFound(msgWindow, groupName, opening) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let result = Services.prompt.confirm(
+ msgWindow,
+ null,
+ bundle.formatStringFromName("autoUnsubscribeText", [
+ groupName,
+ this.hostName,
+ ])
+ );
+ if (result) {
+ this.unsubscribe(groupName);
+ }
+ }
+
+ _lineSeparator = AppConstants.platform == "win" ? "\r\n" : "\n";
+
+ /**
+ * startPopulating as an async function.
+ *
+ * @see startPopulating
+ */
+ async _startPopulating(msgWindow, forceToServer, getOnlyNew) {
+ this._msgWindow = msgWindow;
+ this._subscribable.startPopulating(msgWindow, forceToServer, getOnlyNew);
+ this._groups = [];
+
+ this._hostInfoLoaded = false;
+ if (!forceToServer) {
+ this._hostInfoLoaded = await this._loadHostInfo();
+ if (this._hostInfoLoaded) {
+ this.stopPopulating(msgWindow);
+ return;
+ }
+ }
+ this._hostInfoChanged = !getOnlyNew;
+ MailServices.nntp.getListOfGroupsOnServer(this, msgWindow, getOnlyNew);
+ }
+
+ /**
+ * Try to load groups from hostinfo.dat.
+ *
+ * @returns {boolean} Returns false if hostinfo.dat doesn't exist or doesn't
+ * contain any group.
+ */
+ async _loadHostInfo() {
+ this._hostInfoFile = this.localPath;
+ this._hostInfoFile.append("hostinfo.dat");
+ if (!this._hostInfoFile.exists()) {
+ return false;
+ }
+ let content = await IOUtils.readUTF8(this._hostInfoFile.path);
+ let groupLine = false;
+ for (let line of content.split(this._lineSeparator)) {
+ if (groupLine) {
+ this.addTo(line, false, true, true);
+ } else if (line == "begingroups") {
+ groupLine = true;
+ }
+ }
+ return this._groups.length;
+ }
+
+ /**
+ * Save this._groups to hostinfo.dat.
+ */
+ async _saveHostInfo() {
+ if (!this._hostInfoChanged) {
+ return;
+ }
+
+ let lines = [
+ "# News host information file.",
+ "# This is a generated file! Do not edit.",
+ "",
+ "version=2",
+ `newsrcname=${this.hostName}`,
+ `lastgroupdate=${Math.floor(Date.now() / 1000)}`,
+ "uniqueid=0",
+ "",
+ "begingroups",
+ ...this._groups,
+ ];
+ await IOUtils.writeUTF8(
+ this._hostInfoFile.path,
+ lines.join(this._lineSeparator) + this._lineSeparator
+ );
+ }
+
+ /**
+ * Get an idle connection that can be used.
+ *
+ * @returns {NntpClient}
+ */
+ async _getNextClient() {
+ // The newest connection is the least likely to have timed out.
+ let client = this._idleConnections.pop();
+ if (client) {
+ this._busyConnections.push(client);
+ return client;
+ }
+ if (
+ this._idleConnections.length + this._busyConnections.length <
+ this.maximumConnectionsNumber
+ ) {
+ // Create a new client if the pool is not full.
+ client = new lazy.NntpClient(this);
+ this._busyConnections.push(client);
+ return client;
+ }
+ // Wait until a connection is available.
+ await new Promise(resolve => this._connectionWaitingQueue.push(resolve));
+ return this._getNextClient();
+ }
+
+ /**
+ * Do some actions with a connection.
+ *
+ * @param {Function} handler - A callback function to take a NntpClient
+ * instance, and do some actions.
+ */
+ async withClient(handler) {
+ let client = await this._getNextClient();
+ client.onIdle = () => {
+ this._busyConnections = this._busyConnections.filter(c => c != client);
+ this._idleConnections.push(client);
+ // Resovle the first waiting in queue.
+ this._connectionWaitingQueue.shift()?.();
+ };
+ handler(client);
+ client.connect();
+ }
+}
+
+NntpIncomingServer.prototype.classID = Components.ID(
+ "{dc4ad42f-bc98-4193-a469-0cfa95ed9bcb}"
+);
diff --git a/comm/mailnews/news/src/NntpMessageService.jsm b/comm/mailnews/news/src/NntpMessageService.jsm
new file mode 100644
index 0000000000..dcfac7570a
--- /dev/null
+++ b/comm/mailnews/news/src/NntpMessageService.jsm
@@ -0,0 +1,272 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = ["NntpMessageService", "NewsMessageService"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NntpChannel: "resource:///modules/NntpChannel.jsm",
+ NntpUtils: "resource:///modules/NntpUtils.jsm",
+});
+
+/**
+ * A message service for news-message://, mainly used for displaying messages.
+ *
+ * @implements {nsIMsgMessageService}
+ * @implements {nsIMsgMessageFetchPartService}
+ */
+class BaseMessageService {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMsgMessageService",
+ "nsIMsgMessageFetchPartService",
+ ]);
+
+ _logger = lazy.NntpUtils.logger;
+
+ /** @see nsIMsgMessageService */
+ loadMessage(
+ messageURI,
+ displayConsumer,
+ msgWindow,
+ urlListener,
+ autodetectCharset
+ ) {
+ this._logger.debug("loadMessage", messageURI);
+
+ let uri = this.getUrlForUri(messageURI, msgWindow);
+ if (urlListener) {
+ uri.RegisterListener(urlListener);
+ }
+ if (displayConsumer instanceof Ci.nsIDocShell) {
+ uri.loadURI(
+ displayConsumer.QueryInterface(Ci.nsIDocShell),
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE
+ );
+ } else {
+ let streamListener = displayConsumer.QueryInterface(Ci.nsIStreamListener);
+ let channel = new lazy.NntpChannel(uri);
+ channel.asyncOpen(streamListener);
+ }
+ }
+
+ /**
+ * @param {string} messageURI - Message URI.
+ * @param {?nsIMsgWindow} [msgWindow] - Message window.
+ * @returns {nsIURI}
+ */
+ getUrlForUri(messageURI, msgWindow) {
+ let uri = Services.io
+ .newURI(this._createMessageIdUrl(messageURI))
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ uri.msgWindow = msgWindow;
+ uri.QueryInterface(Ci.nsIMsgMessageUrl).originalSpec = messageURI;
+ uri.QueryInterface(Ci.nsINntpUrl).newsAction =
+ Ci.nsINntpUrl.ActionFetchArticle;
+ return uri;
+ }
+
+ /**
+ * @param {string} uri - The message URI.
+ * @returns {?nsIMsgDBHdr} The message for the URI, or null.
+ */
+ messageURIToMsgHdr(uri) {
+ let [folder, key] = this._decomposeNewsMessageURI(uri);
+ return folder?.GetMessageHeader(key);
+ }
+
+ copyMessage(messageUri, copyListener, moveMessage, urlListener, msgWindow) {
+ this._logger.debug("copyMessage", messageUri);
+ this.loadMessage(messageUri, copyListener, msgWindow, urlListener, false);
+ }
+
+ SaveMessageToDisk(
+ messageUri,
+ file,
+ addDummyEnvelope,
+ urlListener,
+ outUrl,
+ canonicalLineEnding,
+ msgWindow
+ ) {
+ this._logger.debug("SaveMessageToDisk", messageUri);
+ let url = this.getUrlForUri(messageUri, msgWindow);
+ if (urlListener) {
+ url.RegisterListener(urlListener);
+ }
+ url.newsAction = Ci.nsINntpUrl.ActionSaveMessageToDisk;
+ url.AddDummyEnvelope = addDummyEnvelope;
+ url.canonicalLineEnding = canonicalLineEnding;
+
+ let [folder, key] = this._decomposeNewsMessageURI(messageUri);
+ if (folder && folder.QueryInterface(Ci.nsIMsgNewsFolder)) {
+ url.msgIsInLocalCache = folder.hasMsgOffline(key);
+ }
+
+ this.loadMessage(
+ messageUri,
+ url.getSaveAsListener(addDummyEnvelope, file),
+ msgWindow,
+ urlListener,
+ false
+ );
+ }
+
+ Search(searchSession, msgWindow, msgFolder, searchUri) {
+ let slashIndex = searchUri.indexOf("/");
+ let xpatLines = searchUri.slice(slashIndex + 1).split("/");
+ let server = msgFolder.server.QueryInterface(Ci.nsINntpIncomingServer);
+
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(
+ searchSession.QueryInterface(Ci.nsIUrlListener),
+ msgWindow
+ );
+ client.onOpen = () => {
+ client.search(msgFolder.name, xpatLines);
+ };
+
+ client.onData = line => {
+ searchSession.runningAdapter.AddHit(line.split(" ")[0]);
+ };
+ });
+ }
+
+ streamMessage(
+ messageUri,
+ consumer,
+ msgWindow,
+ urlListener,
+ convertData,
+ additionalHeader
+ ) {
+ this._logger.debug("streamMessage", messageUri);
+ let [folder, key] = this._decomposeNewsMessageURI(messageUri);
+
+ let uri = this.getUrlForUri(messageUri, msgWindow);
+ if (additionalHeader) {
+ // NOTE: jsmimeemitter relies on this.
+ let url = new URL(uri.spec);
+ let params = new URLSearchParams(`?header=${additionalHeader}`);
+ for (let [key, value] of params.entries()) {
+ url.searchParams.set(key, value);
+ }
+ uri = uri.mutate().setQuery(url.search).finalize();
+ }
+
+ uri = uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ uri.msgIsInLocalCache = folder.hasMsgOffline(key);
+ if (urlListener) {
+ uri.RegisterListener(urlListener);
+ }
+
+ let streamListener = consumer.QueryInterface(Ci.nsIStreamListener);
+ let channel = new lazy.NntpChannel(uri.QueryInterface(Ci.nsINntpUrl));
+ let listener = streamListener;
+ if (convertData) {
+ let converter = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ listener = converter.asyncConvertData(
+ "message/rfc822",
+ "*/*",
+ streamListener,
+ channel
+ );
+ }
+ channel.asyncOpen(listener);
+ return uri;
+ }
+
+ /**
+ * Parse a message uri to folder and message key.
+ *
+ * @param {string} uri - The news-message:// url to parse.
+ * @returns {[nsIMsgFolder, string]} - The folder and message key.
+ */
+ _decomposeNewsMessageURI(uri) {
+ let host, groupName, key;
+ if (uri.startsWith("news-message://")) {
+ let matches = /news-message:\/\/([^:]+)\/(.+)#(\d+)/.exec(uri);
+ if (!matches) {
+ throw Components.Exception(
+ `Failed to parse message url: ${uri}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ [, host, groupName, key] = matches;
+ if (host.includes("@")) {
+ host = host.slice(host.indexOf("@") + 1);
+ }
+ } else {
+ let url = new URL(uri);
+ host = url.hostname;
+ groupName = url.searchParams.get("group");
+ key = url.searchParams.get("key");
+ }
+ groupName = groupName ? decodeURIComponent(groupName) : null;
+ let server = MailServices.accounts
+ .findServer("", host, "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ let folder;
+ if (groupName) {
+ folder = server.rootFolder
+ .getChildNamed(groupName)
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ }
+ return [folder, key];
+ }
+
+ /**
+ * Create a news:// url from a news-message:// url.
+ *
+ * @param {string} messageURI - The news-message:// url.
+ * @returns {string} The news:// url.
+ */
+ _createMessageIdUrl(messageURI) {
+ if (messageURI.startsWith("news://")) {
+ return messageURI;
+ }
+ let [folder, key] = this._decomposeNewsMessageURI(messageURI);
+ let host = folder.rootFolder.URI;
+ let messageId = folder.getMessageIdForKey(key);
+ let url = new URL(`${host}/${encodeURIComponent(messageId)}`);
+ url.searchParams.set("group", folder.name);
+ url.searchParams.set("key", key);
+ if (!url.port) {
+ url.port = folder.server.port;
+ }
+ return url.toString();
+ }
+
+ /** @see nsIMsgMessageFetchPartService */
+ fetchMimePart(uri, messageUri, displayConsumer, msgWindow, urlListener) {
+ this._logger.debug("fetchMimePart", uri.spec);
+ this.loadMessage(uri.spec, displayConsumer, msgWindow, urlListener, false);
+ }
+}
+
+/**
+ * A message service for news-message://, mainly for displaying messages.
+ */
+class NntpMessageService extends BaseMessageService {}
+
+NntpMessageService.prototype.classID = Components.ID(
+ "{9cefbe67-5966-4f8a-b7b0-cedd60a02c8e}"
+);
+
+/**
+ * A message service for news://, mainly for handling attachments.
+ */
+class NewsMessageService extends BaseMessageService {}
+
+NewsMessageService.prototype.classID = Components.ID(
+ "{4cae5569-2c72-4910-9f3d-774f9e939df8}"
+);
diff --git a/comm/mailnews/news/src/NntpNewsGroup.jsm b/comm/mailnews/news/src/NntpNewsGroup.jsm
new file mode 100644
index 0000000000..e4df659802
--- /dev/null
+++ b/comm/mailnews/news/src/NntpNewsGroup.jsm
@@ -0,0 +1,420 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = ["NntpNewsGroup"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MsgKeySet } = ChromeUtils.import("resource:///modules/MsgKeySet.jsm");
+
+/**
+ * A helper class for NntpClient to deal with msg db and folders.
+ */
+class NntpNewsGroup {
+ /**
+ * @param {nsINntpIncomingServer} server - The associated server instance.
+ * @param {nsIMsgNewsFolder} folder - The associated news folder.
+ */
+ constructor(server, folder) {
+ this._server = server;
+ this._folder = folder;
+ this._db = this._folder.msgDatabase;
+ this._msgHdrs = [];
+ }
+
+ /**
+ * @type {boolean} value - Whether to fetch old messages.
+ */
+ set getOldMessages(value) {
+ this._getOldMessages = value;
+ }
+
+ /**
+ * Get the articles range to fetch, depending on server setting and user
+ * selection.
+ *
+ * @type {nsIMsgWindow} msgWindow - The associated msg window.
+ * @type {number} firstPossible - The first article that can be fetched.
+ * @type {number} lastPossible - The last article that can be fetched.
+ * @returns {[number, number]} A tuple of the first and last article to fetch.
+ */
+ getArticlesRangeToFetch(msgWindow, firstPossible, lastPossible) {
+ this._msgWindow = msgWindow;
+ if (!this._msgWindow) {
+ try {
+ this._msgWindow = MailServices.mailSession.topmostMsgWindow;
+ } catch (e) {}
+ }
+
+ this._folderFilterList = this._folder.getFilterList(this._msgWindow);
+ this._serverFilterList = this._server.getFilterList(this._msgWindow);
+ this._filterHeaders = new Set(
+ (
+ this._folderFilterList.arbitraryHeaders +
+ " " +
+ this._serverFilterList.arbitraryHeaders
+ )
+ .split(" ")
+ .filter(Boolean)
+ );
+
+ let groupInfo = this._db.dBFolderInfo;
+ if (groupInfo) {
+ if (lastPossible < groupInfo.highWater) {
+ groupInfo.highWater = lastPossible;
+ }
+ this._knownKeySet = new MsgKeySet(groupInfo.knownArtsSet);
+ } else {
+ this._knownKeySet = new MsgKeySet();
+ this._knownKeySet.addRange(
+ this._db.lowWaterArticleNum,
+ this._db.highWaterArticleNum
+ );
+ }
+ if (this._knownKeySet.has(lastPossible)) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let messengerBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ msgWindow?.statusFeedback.showStatusString(
+ messengerBundle.formatStringFromName("statusMessage", [
+ this._server.prettyName,
+ bundle.GetStringFromName("noNewMessages"),
+ ])
+ );
+ }
+
+ if (this._getOldMessages || !this._knownKeySet.has(lastPossible)) {
+ let [start, end] = this._knownKeySet.getLastMissingRange(
+ firstPossible,
+ lastPossible
+ );
+ if (this._getOldMessages) {
+ return [Math.max(start, end - this._server.maxArticles + 1), end];
+ }
+ if (
+ start &&
+ end - start > this._server.maxArticles &&
+ this._server.notifyOn
+ ) {
+ // Show a dialog to let user decide how many articles to download.
+ let args = Cc[
+ "@mozilla.org/messenger/newsdownloaddialogargs;1"
+ ].createInstance(Ci.nsINewsDownloadDialogArgs);
+ args.articleCount = end - start + 1;
+ args.groupName = this._folder.unicodeName;
+ args.serverKey = this._server.key;
+ this._msgWindow.domWindow.openDialog(
+ "chrome://messenger/content/downloadheaders.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ args
+ );
+ if (!args.hitOK) {
+ return [];
+ }
+ start = args.downloadAll ? start : end - this._server.maxArticles + 1;
+ if (this._server.markOldRead) {
+ this._readKeySet = new MsgKeySet(
+ this._folder.newsrcLine.split(":")[1].trim()
+ );
+ this._readKeySet.addRange(firstPossible, start - 1);
+ }
+ }
+ return [start, end];
+ }
+ return [];
+ }
+
+ /**
+ * Strip multiple localized Re: prefixes and set the subject and the hasRe
+ * flag. This emulates NS_MsgStripRE()
+ *
+ * @param {nsIMsgDBHdr} msgHdr - The nsIMsgDBHdr to update
+ * @param {string} subject - The unprocessed subject
+ */
+ setSubject(msgHdr, subject) {
+ let prefixes = Services.prefs
+ .getComplexValue("mailnews.localizedRe", Ci.nsIPrefLocalizedString)
+ .data.split(",")
+ .filter(Boolean);
+ if (!prefixes.includes("Re")) {
+ prefixes.push("Re");
+ }
+ // Construct a regular expression like this: ^(Re: |Aw: )+
+ let newSubject = subject.replace(
+ new RegExp(`^(${prefixes.join(": |")}: )+`, "i"),
+ ""
+ );
+ msgHdr.subject = newSubject;
+ if (newSubject != subject) {
+ msgHdr.orFlags(Ci.nsMsgMessageFlags.HasRe);
+ }
+ }
+
+ /**
+ * Parse an XOVER line to a msg hdr.
+ *
+ * @param {string} line - An XOVER response line.
+ */
+ processXOverLine(line) {
+ let parts = line.split("\t");
+ if (parts.length < 8) {
+ return;
+ }
+ let [
+ articleNumber,
+ subject,
+ from,
+ date,
+ messageId,
+ references,
+ bytes,
+ lines,
+ ] = parts;
+ let msgHdr = this._db.createNewHdr(articleNumber);
+ msgHdr.orFlags(Ci.nsMsgMessageFlags.New);
+ this.setSubject(msgHdr, subject);
+ msgHdr.author = from;
+ msgHdr.date = new Date(date).valueOf() * 1000;
+ msgHdr.messageId = messageId;
+ msgHdr.setReferences(references);
+ msgHdr.messageSize = bytes;
+ msgHdr.lineCount = lines;
+ this._msgHdrs.push(msgHdr);
+ }
+
+ /**
+ * Add a range (usually XOVER range) to the known key set.
+ */
+ addKnownArticles(start, end) {
+ this._knownKeySet.addRange(start, end);
+ }
+
+ /**
+ * Finish processing XOVER responses.
+ */
+ finishProcessingXOver() {
+ this._runFilters();
+ let groupInfo = this._db.dBFolderInfo;
+ if (groupInfo) {
+ groupInfo.knownArtsSet = this._knownKeySet.toString();
+ }
+ }
+
+ /**
+ * Extra headers needed by filters, but not returned in XOVER response.
+ */
+ getXHdrFields() {
+ return [...this._filterHeaders].filter(
+ x => !["message-id", "references"].includes(x)
+ );
+ }
+
+ /**
+ * Update msgHdr according to XHDR line.
+ *
+ * @param {string} header - The requested header.
+ * @param {string} line - A XHDR response line.
+ */
+ processXHdrLine(header, line) {
+ let spaceIndex = line.indexOf(" ");
+ let articleNumber = line.slice(0, spaceIndex);
+ let value = line.slice(spaceIndex).trim();
+ let msgHdr = this._db.getMsgHdrForKey(articleNumber);
+ msgHdr.setStringProperty(header, value);
+ }
+
+ /**
+ * Init a msgHdr to prepare to take HEAD response.
+ *
+ * @param {number} articleNumber - The article number.
+ */
+ initHdr(articleNumber) {
+ if (this._msgHdr) {
+ this._msgHdrs.push(this._msgHdr);
+ }
+
+ if (articleNumber >= 0) {
+ this._msgHdr = this._db.createNewHdr(articleNumber);
+ }
+ }
+
+ /**
+ * Update msgHdr according to HEAD line.
+ *
+ * @param {string} line - A HEAD response line.
+ */
+ processHeadLine(line) {
+ let colonIndex = line.indexOf(":");
+ let name = line.slice(0, colonIndex);
+ let value = line.slice(colonIndex + 1).trim();
+ switch (name) {
+ case "from":
+ this._msgHdr.author = value;
+ break;
+ case "date":
+ this._msgHdr.date = new Date(value).valueOf() * 1000;
+ break;
+ case "subject":
+ this.setSubject(this._msgHdr, value);
+ this._msgHdr.orFlags(Ci.nsMsgMessageFlags.New);
+ break;
+ case "message-id":
+ this._msgHdr.messageId = value;
+ break;
+ case "references":
+ this._msgHdr.setReferences(value);
+ break;
+ case "bytes":
+ this._msgHdr.messageSize = value;
+ break;
+ case "lines":
+ this._msgHdr.lineCount = value;
+ break;
+ default:
+ if (this._filterHeaders.has(name)) {
+ this._msgHdr.setStringProperty(name, value);
+ }
+ }
+ }
+
+ /**
+ * Run filters to all newly added msg hdrs.
+ */
+ _runFilters() {
+ let folderFilterCount = this._folderFilterList.filterCount;
+ let serverFilterCount = this._serverFilterList.filterCount;
+
+ for (let msgHdr of this._msgHdrs) {
+ this._filteringHdr = msgHdr;
+ this._addHdrToDB = true;
+ let headers = "";
+ if (folderFilterCount || serverFilterCount) {
+ let author = this._filteringHdr.author;
+ let subject = this._filteringHdr.subject;
+ if (author) {
+ headers += `From: ${author}\0`;
+ }
+ if (subject) {
+ headers += `Subject: ${subject}\0`;
+ }
+ }
+ if (folderFilterCount) {
+ this._folderFilterList.applyFiltersToHdr(
+ Ci.nsMsgFilterType.NewsRule,
+ msgHdr,
+ this._folder,
+ this._db,
+ headers,
+ this,
+ this._msgWindow
+ );
+ }
+ if (serverFilterCount) {
+ this._serverFilterList.applyFiltersToHdr(
+ Ci.nsMsgFilterType.NewsRule,
+ msgHdr,
+ this._folder,
+ this._db,
+ headers,
+ this,
+ this._msgWindow
+ );
+ }
+ if (this._addHdrToDB && !this._db.containsKey(msgHdr.messageKey)) {
+ this._db.addNewHdrToDB(msgHdr, true);
+ MailServices.mfn.notifyMsgAdded(msgHdr);
+ this._folder.orProcessingFlags(
+ msgHdr.messageKey,
+ Ci.nsMsgProcessingFlags.NotReportedClassified
+ );
+ }
+ }
+ }
+
+ /**
+ * Callback of nsIMsgFilterList.applyFiltersToHdr.
+ *
+ * @see nsIMsgFilterHitNotify
+ */
+ applyFilterHit(filter, msgWindow) {
+ let loggingEnabled = filter.filterList.loggingEnabled;
+ let applyMore = true;
+
+ for (let action of filter.sortedActionList) {
+ if (loggingEnabled) {
+ filter.logRuleHit(action, this._filteringHdr);
+ }
+ switch (action.type) {
+ case Ci.nsMsgFilterAction.Delete:
+ this._addHdrToDB = false;
+ break;
+ case Ci.nsMsgFilterAction.MarkRead:
+ this._db.markHdrRead(this._filteringHdr, true, null);
+ break;
+ case Ci.nsMsgFilterAction.MarkUnread:
+ this._db.markHdrRead(this._filteringHdr, false, null);
+ break;
+ case Ci.nsMsgFilterAction.KillThread:
+ this._filteringHdr.setUint32Property(
+ "ProtoThreadFlags",
+ Ci.nsMsgMessageFlags.Ignored
+ );
+ break;
+ case Ci.nsMsgFilterAction.KillSubthread:
+ this._filteringHdr.orFlags(Ci.nsMsgMessageFlags.Ignored);
+ break;
+ case Ci.nsMsgFilterAction.WatchThread:
+ this._filteringHdr.orFlags(Ci.nsMsgMessageFlags.Watched);
+ break;
+ case Ci.nsMsgFilterAction.MarkFlagged:
+ this._filteringHdr.markFlagged(true);
+ break;
+ case Ci.nsMsgFilterAction.ChangePriority:
+ this._filteringHdr.priority = action.priority;
+ break;
+ case Ci.nsMsgFilterAction.AddTag:
+ this._folder.addKeywordsToMessages(
+ [this._filteringHdr],
+ action.strValue
+ );
+ break;
+ case Ci.nsMsgFilterAction.StopExecution:
+ applyMore = false;
+ break;
+ case Ci.nsMsgFilterAction.Custom:
+ action.customAction.applyAction(
+ [this._filteringHdr],
+ action.strValue,
+ null,
+ Ci.nsMsgFilterType.NewsRule,
+ msgWindow
+ );
+ break;
+ default:
+ throw Components.Exception(
+ `Unexpected filter action type=${action.type}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+ }
+ return applyMore;
+ }
+
+ /**
+ * Commit changes to msg db.
+ */
+ cleanUp() {
+ if (this._readKeySet) {
+ this._folder.setReadSetFromStr(this._readKeySet);
+ }
+ this._folder.notifyFinishedDownloadinghdrs();
+ this._db.commit(Ci.nsMsgDBCommitType.kSessionCommit);
+ this._db.close(true);
+ }
+}
diff --git a/comm/mailnews/news/src/NntpProtocolHandler.jsm b/comm/mailnews/news/src/NntpProtocolHandler.jsm
new file mode 100644
index 0000000000..00e5dc224b
--- /dev/null
+++ b/comm/mailnews/news/src/NntpProtocolHandler.jsm
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = ["NewsProtocolHandler", "SnewsProtocolHandler"];
+
+var { NntpChannel } = ChromeUtils.import("resource:///modules/NntpChannel.jsm");
+
+/**
+ * @implements {nsIProtocolHandler}
+ */
+class NewsProtocolHandler {
+ QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
+
+ scheme = "news";
+
+ newChannel(uri, loadInfo) {
+ let channel = new NntpChannel(uri, loadInfo);
+ let spec = uri.spec;
+ if (
+ spec.includes("part=") &&
+ !spec.includes("type=message/rfc822") &&
+ !spec.includes("type=application/x-message-display") &&
+ !spec.includes("type=application/pdf")
+ ) {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
+ } else {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_INLINE;
+ }
+ return channel;
+ }
+
+ allowPort(port, scheme) {
+ return true;
+ }
+}
+NewsProtocolHandler.prototype.classID = Components.ID(
+ "{24220ecd-cb05-4676-8a47-fa1da7b86e6e}"
+);
+
+class SnewsProtocolHandler extends NewsProtocolHandler {
+ scheme = "snews";
+}
+SnewsProtocolHandler.prototype.classID = Components.ID(
+ "{1895016d-5302-46a9-b3f5-9c47694d9eca}"
+);
diff --git a/comm/mailnews/news/src/NntpProtocolInfo.jsm b/comm/mailnews/news/src/NntpProtocolInfo.jsm
new file mode 100644
index 0000000000..3a1bfeb887
--- /dev/null
+++ b/comm/mailnews/news/src/NntpProtocolInfo.jsm
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["NntpProtocolInfo"];
+
+var { MsgProtocolInfo } = ChromeUtils.importESModule(
+ "resource:///modules/MsgProtocolInfo.sys.mjs"
+);
+
+/**
+ * @implements {nsIMsgProtocolInfo}
+ */
+class NntpProtocolInfo extends MsgProtocolInfo {
+ QueryInterface = ChromeUtils.generateQI(["nsIMsgProtocolInfo"]);
+
+ serverIID = Components.ID("{dc4ad42f-bc98-4193-a469-0cfa95ed9bcb}");
+
+ requiresUsername = false;
+ preflightPrettyNameWithEmailAddress = false;
+ canDelete = true;
+ canLoginAtStartUp = true;
+ canDuplicate = true;
+ canGetMessages = true;
+ canGetIncomingMessages = false;
+ defaultDoBiff = false;
+ showComposeMsgLink = false;
+ foldersCreatedAsync = false;
+
+ getDefaultServerPort(isSecure) {
+ return isSecure
+ ? Ci.nsINntpUrl.DEFAULT_NNTPS_PORT
+ : Ci.nsINntpUrl.DEFAULT_NNTP_PORT;
+ }
+
+ // @see MsgProtocolInfo.sys.mjs
+ RELATIVE_PREF = "mail.root.nntp-rel";
+ ABSOLUTE_PREF = "mail.root.nntp";
+ DIR_SERVICE_PROP = "NewsD";
+}
+
+NntpProtocolInfo.prototype.classID = Components.ID(
+ "{7d71db22-0624-4c9f-8d70-dea6ab3ff076}"
+);
diff --git a/comm/mailnews/news/src/NntpService.jsm b/comm/mailnews/news/src/NntpService.jsm
new file mode 100644
index 0000000000..cae1cd9002
--- /dev/null
+++ b/comm/mailnews/news/src/NntpService.jsm
@@ -0,0 +1,250 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = ["NntpService"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * @implements {nsINntpService}
+ */
+class NntpService {
+ QueryInterface = ChromeUtils.generateQI(["nsINntpService"]);
+
+ get cacheStorage() {
+ if (!this._cacheStorage) {
+ this._cacheStorage = Services.cache2.memoryCacheStorage(
+ Services.loadContextInfo.custom(false, {})
+ );
+ }
+ return this._cacheStorage;
+ }
+
+ generateNewsHeaderValsForPosting(
+ newsgroupsList,
+ outNewsgroupsHeader,
+ outNewsHostHeader
+ ) {
+ let groups = newsgroupsList.split(",");
+ outNewsgroupsHeader.value = newsgroupsList;
+ let hosts = groups.map(name => this._findHostFromGroupName(name));
+ hosts = [...new Set(hosts)].filter(Boolean);
+ let host = hosts[0];
+ if (!host) {
+ outNewsHostHeader.value = "";
+ return;
+ }
+ if (hosts.length > 1) {
+ throw Components.Exception(
+ `Cross posting not allowed, hosts=${hosts.join(",")}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ outNewsHostHeader.value = host;
+ }
+
+ postMessage(messageFile, groupNames, accountKey, urlListener, msgWindow) {
+ let server = MailServices.accounts.getAccount(accountKey)?.incomingServer;
+ if (!server) {
+ // If no matching server, find the first news server and use it.
+ server = MailServices.accounts.findServer("", "", "nntp");
+ }
+ server = server.QueryInterface(Ci.nsINntpIncomingServer);
+
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(urlListener, msgWindow);
+
+ client.onOpen = () => {
+ client.post();
+ };
+
+ client.onReadyToPost = () => {
+ let fstream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ // PR_RDONLY
+ fstream.init(messageFile, 0x01, 0, 0);
+ let lineInputStream = fstream.QueryInterface(Ci.nsILineInputStream);
+ let hasMore;
+ do {
+ let outLine = {};
+ hasMore = lineInputStream.readLine(outLine);
+ let line = outLine.value;
+ if (line.startsWith(".")) {
+ // Dot stuffing, see rfc3977#section-3.1.1.
+ line = "." + line;
+ }
+ client.send(line + "\r\n");
+ } while (hasMore);
+ fstream.close();
+ client.send(".\r\n");
+ };
+ });
+ }
+
+ getNewNews(server, uri, getOld, urlListener, msgWindow) {
+ if (Services.io.offline) {
+ const NS_MSG_ERROR_OFFLINE = 0x80550014;
+ // @see nsMsgNewsFolder::UpdateFolder
+ throw Components.Exception(
+ "Cannot get news while offline",
+ NS_MSG_ERROR_OFFLINE
+ );
+ }
+ // The uri is in the form of news://news.mozilla.org/mozilla.accessibility
+ let matches = /.+:\/\/([^:]+):?(\d+)?\/(.+)?/.exec(uri);
+ let groupName = decodeURIComponent(matches[3]);
+
+ let runningUri = Services.io
+ .newURI(uri)
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(urlListener, msgWindow, runningUri);
+ client.onOpen = () => {
+ client.getNewNews(groupName, getOld);
+ };
+ });
+
+ return runningUri;
+ }
+
+ getListOfGroupsOnServer(server, msgWindow, getOnlyNew) {
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(null, msgWindow);
+ client.onOpen = () => {
+ client.getListOfGroups(getOnlyNew);
+ };
+
+ client.onData = data => {
+ server.addNewsgroupToList(data.split(" ")[0]);
+ };
+ });
+ }
+
+ fetchMessage(folder, key, msgWindow, consumer, urlListener) {
+ let streamListener, inputStream, outputStream;
+ if (consumer instanceof Ci.nsIStreamListener) {
+ streamListener = consumer;
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ inputStream = pipe.inputStream;
+ outputStream = pipe.outputStream;
+ }
+
+ let server = folder.server.QueryInterface(Ci.nsINntpIncomingServer);
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(urlListener, msgWindow);
+
+ client.onOpen = () => {
+ client.getArticleByArticleNumber(folder.name, key);
+ streamListener?.onStartRequest(null);
+ };
+ client.onData = data => {
+ outputStream?.write(data, data.length);
+ streamListener?.onDataAvailable(null, inputStream, 0, data.length);
+ };
+ client.onDone = () => {
+ streamListener?.onStopRequest(null, Cr.NS_OK);
+ };
+ });
+ }
+
+ cancelMessage(cancelUrl, messageUri, consumer, urlListener, msgWindow) {
+ if (Services.prefs.getBoolPref("news.cancel.confirm")) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let result = Services.prompt.confirmEx(
+ msgWindow?.domWindow,
+ null,
+ bundle.GetStringFromName("cancelConfirm"),
+ Ci.nsIPrompt.STD_YES_NO_BUTTONS,
+ null,
+ null,
+ null,
+ null,
+ { value: false }
+ );
+ if (result != 0) {
+ // Cancelled.
+ return;
+ }
+ }
+ // The cancelUrl is in the form of "news://host/message-id?cancel"
+ let url = new URL(cancelUrl);
+ let messageId = "<" + decodeURIComponent(url.pathname.slice(1)) + ">";
+ let server = MailServices.accounts
+ .findServer("", url.host, "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ let groupName = new URL(messageUri).pathname.slice(1);
+ let messageKey = messageUri.split("#")[1];
+ let newsFolder = server.findGroup(groupName);
+ let from = MailServices.accounts.getFirstIdentityForServer(server).email;
+ let bundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+
+ server.wrappedJSObject.withClient(client => {
+ let runningUrl = client.startRunningUrl(urlListener, msgWindow);
+ runningUrl.msgWindow = msgWindow;
+
+ client.onOpen = () => {
+ client.cancelArticle(groupName);
+ };
+
+ client.onReadyToPost = () => {
+ let content = [
+ `From: ${from}`,
+ `Newsgroups: ${groupName}`,
+ `Subject: cancel ${messageId}`,
+ `References: ${messageId}`,
+ `Control: cancel ${messageId}`,
+ "MIME-Version: 1.0",
+ "Content-Type: text/plain",
+ "", // body separator
+ `This message was cancelled from within ${bundle.GetStringFromName(
+ "brandFullName"
+ )}`,
+ ];
+ client.send(content.join("\r\n"));
+ client.send("\r\n.\r\n");
+
+ newsFolder.removeMessage(messageKey);
+ newsFolder.cancelComplete();
+ };
+ });
+ }
+
+ downloadNewsgroupsForOffline(msgWindow, urlListener) {
+ let { NewsDownloader } = ChromeUtils.importESModule(
+ "resource:///modules/NewsDownloader.sys.mjs"
+ );
+ let downloader = new NewsDownloader(msgWindow, urlListener);
+ downloader.start();
+ }
+
+ /**
+ * Find the hostname of a NNTP server from a group name.
+ *
+ * @param {string} groupName - The group name.
+ * @returns {string} The corresponding server host.
+ */
+ _findHostFromGroupName(groupName) {
+ for (let server of MailServices.accounts.allServers) {
+ if (
+ server instanceof Ci.nsINntpIncomingServer &&
+ server.containsNewsgroup(groupName)
+ ) {
+ return server.hostName;
+ }
+ }
+ return "";
+ }
+}
+
+NntpService.prototype.classID = Components.ID(
+ "{b13db263-a219-4168-aeaf-8266f001087e}"
+);
diff --git a/comm/mailnews/news/src/NntpUtils.jsm b/comm/mailnews/news/src/NntpUtils.jsm
new file mode 100644
index 0000000000..40cc51b993
--- /dev/null
+++ b/comm/mailnews/news/src/NntpUtils.jsm
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.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 = ["NntpUtils"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Collection of helper functions for NNTP.
+ */
+var NntpUtils = {
+ logger: console.createInstance({
+ prefix: "mailnews.nntp",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.nntp.loglevel",
+ }),
+
+ /**
+ * Find a server instance by its hostname.
+ *
+ * Sometimes we create a server instance to load a news url, this server is
+ * written to the prefs but not associated with any account. Different from
+ * nsIMsgAccountManager.findServer which can only find servers associated
+ * with accounts, this function looks for NNTP server in the mail.server.
+ * branch directly.
+ *
+ * @param {string} hostname - The hostname of the server.
+ * @returns {nsINntpIncomingServer|null}
+ */
+ findServer(hostname) {
+ 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]);
+ }
+
+ // Find the NNTP server that matches the hostname.
+ hostname = hostname.toLowerCase();
+ for (let key of keySet) {
+ let type = branch.getCharPref(`${key}.type`, "");
+ let hostnameValue = branch
+ .getCharPref(`${key}.hostname`, "")
+ .toLowerCase();
+ if (type == "nntp" && hostnameValue == hostname) {
+ try {
+ return MailServices.accounts
+ .getIncomingServer(key)
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ } catch (e) {
+ // In some profiles, two servers have the same hostname, but only one
+ // can be loaded into AccountManager. Catch the error here and the
+ // already loaded server will be found.
+ }
+ }
+ }
+ return null;
+ },
+};
diff --git a/comm/mailnews/news/src/components.conf b/comm/mailnews/news/src/components.conf
new file mode 100644
index 0000000000..502e3cd271
--- /dev/null
+++ b/comm/mailnews/news/src/components.conf
@@ -0,0 +1,98 @@
+# -*- 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": "{e9bb3330-ac7e-11de-8a39-0800200c9a66}",
+ "contract_ids": ["@mozilla.org/autocomplete/search;1?name=news"],
+ "jsm": "resource:///modules/NewsAutoCompleteSearch.jsm",
+ "constructor": "NewsAutoCompleteSearch",
+ },
+ {
+ "cid": "{dc4ad42f-bc98-4193-a469-0cfa95ed9bcb}",
+ "contract_ids": ["@mozilla.org/messenger/server;1?type=nntp"],
+ "jsm": "resource:///modules/NntpIncomingServer.jsm",
+ "constructor": "NntpIncomingServer",
+ },
+ {
+ "cid": "{7d71db22-0624-4c9f-8d70-dea6ab3ff076}",
+ "contract_ids": ["@mozilla.org/messenger/protocol/info;1?type=nntp"],
+ "jsm": "resource:///modules/NntpProtocolInfo.jsm",
+ "constructor": "NntpProtocolInfo",
+ },
+ {
+ "cid": "{b13db263-a219-4168-aeaf-8266f001087e}",
+ "contract_ids": ["@mozilla.org/messenger/nntpservice;1"],
+ "jsm": "resource:///modules/NntpService.jsm",
+ "constructor": "NntpService",
+ },
+ {
+ "cid": "{9cefbe67-5966-4f8a-b7b0-cedd60a02c8e}",
+ "contract_ids": ["@mozilla.org/messenger/messageservice;1?type=news-message"],
+ "jsm": "resource:///modules/NntpMessageService.jsm",
+ "constructor": "NntpMessageService",
+ },
+ {
+ "cid": "{4cae5569-2c72-4910-9f3d-774f9e939df8}",
+ "contract_ids": ["@mozilla.org/messenger/messageservice;1?type=news"],
+ "jsm": "resource:///modules/NntpMessageService.jsm",
+ "constructor": "NewsMessageService",
+ },
+ {
+ "cid": "{24220ecd-cb05-4676-8a47-fa1da7b86e6e}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=news"],
+ "jsm": "resource:///modules/NntpProtocolHandler.jsm",
+ "constructor": "NewsProtocolHandler",
+ "protocol_config": {
+ "scheme": "news",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ "URI_LOADABLE_BY_ANYONE",
+ "ALLOWS_PROXY",
+ "URI_FORBIDS_COOKIE_ACCESS",
+ "ORIGIN_IS_FULL_SPEC",
+ ],
+ "default_port": 119,
+ },
+ },
+ {
+ "cid": "{1895016d-5302-46a9-b3f5-9c47694d9eca}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=snews"],
+ "jsm": "resource:///modules/NntpProtocolHandler.jsm",
+ "constructor": "SnewsProtocolHandler",
+ "protocol_config": {
+ "scheme": "snews",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ "URI_LOADABLE_BY_ANYONE",
+ "ALLOWS_PROXY",
+ "URI_FORBIDS_COOKIE_ACCESS",
+ "ORIGIN_IS_FULL_SPEC",
+ ],
+ "default_port": 563,
+ },
+ },
+ {
+ "cid": "{196b4b30-e18c-11d2-806e-006008128c4e}",
+ "contract_ids": ["@mozilla.org/messenger/nntpurl;1"],
+ "type": "nsNntpUrl",
+ "headers": ["/comm/mailnews/news/src/nsNntpUrl.h"],
+ },
+ {
+ "cid": "{4ace448a-f6d4-11d2-880d-004005263078}",
+ "contract_ids": ["@mozilla.org/mail/folder-factory;1?name=news"],
+ "type": "nsMsgNewsFolder",
+ "headers": ["/comm/mailnews/news/src/nsNewsFolder.h"],
+ },
+ {
+ "cid": "{1540689e-1dd2-11b2-933d-f0d1e460ef4a}",
+ "contract_ids": ["@mozilla.org/messenger/newsdownloaddialogargs;1"],
+ "type": "nsNewsDownloadDialogArgs",
+ "headers": ["/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h"],
+ },
+]
diff --git a/comm/mailnews/news/src/moz.build b/comm/mailnews/news/src/moz.build
new file mode 100644
index 0000000000..a5f792b2cf
--- /dev/null
+++ b/comm/mailnews/news/src/moz.build
@@ -0,0 +1,32 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SOURCES += [
+ "nsNewsDownloadDialogArgs.cpp",
+ "nsNewsDownloader.cpp",
+ "nsNewsFolder.cpp",
+ "nsNewsUtils.cpp",
+ "nsNntpUrl.cpp",
+]
+
+EXTRA_JS_MODULES += [
+ "NewsAutoCompleteSearch.jsm",
+ "NewsDownloader.sys.mjs",
+ "NntpChannel.jsm",
+ "NntpClient.jsm",
+ "NntpIncomingServer.jsm",
+ "NntpMessageService.jsm",
+ "NntpNewsGroup.jsm",
+ "NntpProtocolHandler.jsm",
+ "NntpProtocolInfo.jsm",
+ "NntpService.jsm",
+ "NntpUtils.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "mail"
diff --git a/comm/mailnews/news/src/nntpCore.h b/comm/mailnews/news/src/nntpCore.h
new file mode 100644
index 0000000000..52230c6931
--- /dev/null
+++ b/comm/mailnews/news/src/nntpCore.h
@@ -0,0 +1,165 @@
+/* -*- 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 _NNTPCore_H__
+#define _NNTPCore_H__
+
+#define NEWS_MSGS_URL "chrome://messenger/locale/news.properties"
+
+// The following string constants are protocol strings. I'm defining them as
+// macros here so I don't have to sprinkle all of the strings throughout the
+// protocol.
+#define NNTP_CMD_LIST_EXTENSIONS "LIST EXTENSIONS" CRLF
+#define NNTP_CMD_MODE_READER "MODE READER" CRLF
+#define NNTP_CMD_LIST_SEARCHES "LIST SEARCHES" CRLF
+#define NNTP_CMD_LIST_SEARCH_FIELDS "LIST SRCHFIELDS" CRLF
+#define NNTP_CMD_GET_PROPERTIES "GET" CRLF
+#define NNTP_CMD_LIST_SUBSCRIPTIONS "LIST SUBSCRIPTIONS" CRLF
+#define NNTP_CMD_POST "POST" CRLF
+#define NNTP_CMD_QUIT "QUIT" CRLF
+
+// end of protocol strings
+
+#define MK_NNTP_RESPONSE_HELP 100
+
+#define MK_NNTP_RESPONSE_POSTING_ALLOWED 200
+#define MK_NNTP_RESPONSE_POSTING_DENIED 201
+
+#define MK_NNTP_RESPONSE_DISCONTINUED 400
+
+#define MK_NNTP_RESPONSE_COMMAND_UNKNOWN 500
+#define MK_NNTP_RESPONSE_SYNTAX_ERROR 501
+#define MK_NNTP_RESPONSE_PERMISSION_DENIED 502
+#define MK_NNTP_RESPONSE_SERVER_ERROR 503
+
+#define MK_NNTP_RESPONSE_ARTICLE_BOTH 220
+#define MK_NNTP_RESPONSE_ARTICLE_HEAD 221
+#define MK_NNTP_RESPONSE_ARTICLE_BODY 222
+#define MK_NNTP_RESPONSE_ARTICLE_NONE 223
+#define MK_NNTP_RESPONSE_ARTICLE_NO_GROUP 412
+#define MK_NNTP_RESPONSE_ARTICLE_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_ARTICLE_NONEXIST 423
+#define MK_NNTP_RESPONSE_ARTICLE_NOTFOUND 430
+
+#define MK_NNTP_RESPONSE_GROUP_SELECTED 211
+#define MK_NNTP_RESPONSE_GROUP_NO_GROUP 411
+
+#define MK_NNTP_RESPONSE_IHAVE_OK 235
+#define MK_NNTP_RESPONSE_IHAVE_ARTICLE 335
+#define MK_NNTP_RESPONSE_IHAVE_NOT_WANTED 435
+#define MK_NNTP_RESPONSE_IHAVE_FAILED 436
+#define MK_NNTP_RESPONSE_IHAVE_REJECTED 437
+
+#define MK_NNTP_RESPONSE_LAST_OK 223
+#define MK_NNTP_RESPONSE_LAST_NO_GROUP 412
+#define MK_NNTP_RESPONSE_LAST_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_LAST_NO_ARTICLE 422
+
+#define MK_NNTP_RESPONSE_LIST_OK 215
+
+#define MK_NNTP_RESPONSE_NEWGROUPS_OK 231
+
+#define MK_NNTP_RESPONSE_NEWNEWS_OK 230
+
+#define MK_NNTP_RESPONSE_NEXT_OK 223
+#define MK_NNTP_RESPONSE_NEXT_NO_GROUP 412
+#define MK_NNTP_RESPONSE_NEXT_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_NEXT_NO_ARTICLE 421
+
+#define MK_NNTP_RESPONSE_POST_OK 240
+#define MK_NNTP_RESPONSE_POST_SEND_NOW 340
+#define MK_NNTP_RESPONSE_POST_DENIED 440
+#define MK_NNTP_RESPONSE_POST_FAILED 441
+
+#define MK_NNTP_RESPONSE_QUIT_OK 205
+
+#define MK_NNTP_RESPONSE_SLAVE_OK 202
+
+#define MK_NNTP_RESPONSE_CHECK_NO_ARTICLE 238
+#define MK_NNTP_RESPONSE_CHECK_NO_ACCEPT 400
+#define MK_NNTP_RESPONSE_CHECK_LATER 431
+#define MK_NNTP_RESPONSE_CHECK_DONT_SEND 438
+#define MK_NNTP_RESPONSE_CHECK_DENIED 480
+#define MK_NNTP_RESPONSE_CHECK_ERROR 500
+
+#define MK_NNTP_RESPONSE_XHDR_OK 221
+#define MK_NNTP_RESPONSE_XHDR_NO_GROUP 412
+#define MK_NNTP_RESPONSE_XHDR_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_XHDR_NO_ARTICLE 430
+#define MK_NNTP_RESPONSE_XHDR_DENIED 502
+
+#define MK_NNTP_RESPONSE_XOVER_OK 224
+#define MK_NNTP_RESPONSE_XOVER_NO_GROUP 412
+#define MK_NNTP_RESPONSE_XOVER_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_XOVER_DENIED 502
+
+#define MK_NNTP_RESPONSE_XPAT_OK 221
+#define MK_NNTP_RESPONSE_XPAT_NO_ARTICLE 430
+#define MK_NNTP_RESPONSE_XPAT_DENIED 502
+
+#define MK_NNTP_RESPONSE_AUTHINFO_OK 281
+#define MK_NNTP_RESPONSE_AUTHINFO_CONT 381
+#define MK_NNTP_RESPONSE_AUTHINFO_REQUIRE 480
+#define MK_NNTP_RESPONSE_AUTHINFO_REJECT 482
+#define MK_NNTP_RESPONSE_AUTHINFO_DENIED 502
+
+#define MK_NNTP_RESPONSE_
+
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_OK 250
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_CONT 350
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_REQUIRE 450
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_REJECT 452
+
+#define MK_NNTP_RESPONSE_TYPE_INFO 1
+#define MK_NNTP_RESPONSE_TYPE_OK 2
+#define MK_NNTP_RESPONSE_TYPE_CONT 3
+#define MK_NNTP_RESPONSE_TYPE_CANNOT 4
+#define MK_NNTP_RESPONSE_TYPE_ERROR 5
+
+#define MK_NNTP_RESPONSE_TYPE(x) (x / 100)
+
+// the following used to be defined in allxpstr.h. Until we find a new values
+// for these, I'm defining them here because I don't want to link against
+// xplib.lib...(mscott)
+
+#define MK_DATA_LOADED 1
+#define MK_EMPTY_NEWS_LIST -227
+#define MK_INTERRUPTED -201
+#define MK_MALFORMED_URL_ERROR -209
+#define MK_NEWS_ERROR_FMT -430
+#define MK_NNTP_CANCEL_CONFIRM -426
+#define MK_NNTP_CANCEL_DISALLOWED -427
+#define MK_NNTP_NOT_CANCELLED -429
+#define MK_OUT_OF_MEMORY -207
+#define XP_CONFIRM_SAVE_NEWSGROUPS -1
+#define XP_HTML_ARTICLE_EXPIRED -1
+#define XP_HTML_NEWS_ERROR -1
+#define XP_PROGRESS_READ_NEWSGROUPINFO 1
+#define XP_PROGRESS_RECEIVE_ARTICLE 1
+#define XP_PROGRESS_RECEIVE_LISTARTICLES 1
+#define XP_PROGRESS_RECEIVE_NEWSGROUP 1
+#define XP_PROGRESS_SORT_ARTICLES 1
+#define XP_PROGRESS_READ_NEWSGROUP_COUNTS 1
+#define XP_THERMO_PERCENT_FORM 1
+#define XP_PROMPT_ENTER_USERNAME 1
+#define MK_NNTP_AUTH_FAILED -260
+#define MK_NNTP_ERROR_MESSAGE -304
+#define MK_NNTP_NEWSGROUP_SCAN_ERROR -305
+#define MK_NNTP_SERVER_ERROR -217
+#define MK_NNTP_SERVER_NOT_CONFIGURED -307
+#define MK_TCP_READ_ERROR -252
+#define MK_TCP_WRITE_ERROR -236
+#define MK_NNTP_CANCEL_ERROR -428
+#define XP_CONNECT_NEWS_HOST_CONTACTED_WAITING_FOR_REPLY 1
+#define XP_PLEASE_ENTER_A_PASSWORD_FOR_NEWS_SERVER_ACCESS 1
+#define XP_GARBAGE_COLLECTING 1
+#define XP_MESSAGE_SENT_WAITING_NEWS_REPLY 1
+#define MK_MSG_DELIV_NEWS 1
+#define MK_MSG_COLLABRA_DISABLED 1
+#define MK_MSG_EXPIRE_NEWS_ARTICLES 1
+#define MK_MSG_HTML_IMAP_NO_CACHED_BODY 1
+#define MK_MSG_CANT_MOVE_FOLDER 1
+
+#endif /* NNTPCore_H__ */
diff --git a/comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp
new file mode 100644
index 0000000000..3b91407598
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp
@@ -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/. */
+
+#include "nsNewsDownloadDialogArgs.h"
+
+nsNewsDownloadDialogArgs::nsNewsDownloadDialogArgs() {
+ mArticleCount = 0;
+ mServerKey = "";
+ mHitOK = false;
+ mDownloadAll = false;
+}
+
+nsNewsDownloadDialogArgs::~nsNewsDownloadDialogArgs() {}
+
+NS_IMPL_ISUPPORTS(nsNewsDownloadDialogArgs, nsINewsDownloadDialogArgs)
+
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetGroupName(nsAString& aGroupName) {
+ aGroupName = mGroupName;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetGroupName(
+ const nsAString& aGroupName) {
+ mGroupName = aGroupName;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetArticleCount(
+ int32_t* aArticleCount) {
+ NS_ENSURE_ARG_POINTER(aArticleCount);
+
+ *aArticleCount = mArticleCount;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetArticleCount(int32_t aArticleCount) {
+ mArticleCount = aArticleCount;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetServerKey(char** aServerKey) {
+ NS_ENSURE_ARG_POINTER(aServerKey);
+
+ *aServerKey = ToNewCString(mServerKey);
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetServerKey(const char* aServerKey) {
+ NS_ENSURE_ARG_POINTER(aServerKey);
+
+ mServerKey = aServerKey;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetHitOK(bool* aHitOK) {
+ NS_ENSURE_ARG_POINTER(aHitOK);
+
+ *aHitOK = mHitOK;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetHitOK(bool aHitOK) {
+ mHitOK = aHitOK;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetDownloadAll(bool* aDownloadAll) {
+ NS_ENSURE_ARG_POINTER(aDownloadAll);
+
+ *aDownloadAll = mDownloadAll;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetDownloadAll(bool aDownloadAll) {
+ mDownloadAll = aDownloadAll;
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h
new file mode 100644
index 0000000000..7a43523072
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h
@@ -0,0 +1,29 @@
+/* -*- 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 nsNewsDownloadDialogArgs_h__
+#define nsNewsDownloadDialogArgs_h__
+
+#include "nsINewsDownloadDialogArgs.h"
+#include "nsString.h"
+
+class nsNewsDownloadDialogArgs : public nsINewsDownloadDialogArgs {
+ public:
+ nsNewsDownloadDialogArgs();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINEWSDOWNLOADDIALOGARGS
+
+ private:
+ virtual ~nsNewsDownloadDialogArgs();
+
+ nsString mGroupName;
+ int32_t mArticleCount;
+ nsCString mServerKey;
+ bool mHitOK;
+ bool mDownloadAll;
+};
+
+#endif // nsNewsDownloadDialogArgs_h__
diff --git a/comm/mailnews/news/src/nsNewsDownloader.cpp b/comm/mailnews/news/src/nsNewsDownloader.cpp
new file mode 100644
index 0000000000..945e1bd084
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloader.cpp
@@ -0,0 +1,507 @@
+/* -*- 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 "nntpCore.h"
+#include "netCore.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsIStringBundle.h"
+#include "nsNewsDownloader.h"
+#include "nsINntpService.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIMsgAccountManager.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgMailSession.h"
+#include "nsMsgMessageFlags.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+
+// This file contains the news article download state machine.
+
+// if pIds is not null, download the articles whose id's are passed in.
+// Otherwise, which articles to download is determined by nsNewsDownloader
+// object, or subclasses thereof. News can download marked objects, for example.
+nsresult nsNewsDownloader::DownloadArticles(nsIMsgWindow* window,
+ nsIMsgFolder* folder,
+ nsTArray<nsMsgKey>* pIds) {
+ if (pIds != nullptr)
+ m_keysToDownload.InsertElementsAt(0, pIds->Elements(), pIds->Length());
+
+ if (!m_keysToDownload.IsEmpty()) m_downloadFromKeys = true;
+
+ m_folder = folder;
+ m_window = window;
+ m_numwrote = 0;
+
+ bool headersToDownload = GetNextHdrToRetrieve();
+ // should we have a special error code for failure here?
+ return (headersToDownload) ? DownloadNext(true) : NS_ERROR_FAILURE;
+}
+
+/* Saving news messages
+ */
+
+NS_IMPL_ISUPPORTS(nsNewsDownloader, nsIUrlListener, nsIMsgSearchNotify)
+
+nsNewsDownloader::nsNewsDownloader(nsIMsgWindow* window, nsIMsgDatabase* msgDB,
+ nsIUrlListener* listener) {
+ m_numwrote = 0;
+ m_downloadFromKeys = false;
+ m_newsDB = msgDB;
+ m_abort = false;
+ m_listener = listener;
+ m_window = window;
+ m_lastPercent = -1;
+ m_lastProgressTime = 0;
+ // not the perfect place for this, but I think it will work.
+ if (m_window) m_window->SetStopped(false);
+}
+
+nsNewsDownloader::~nsNewsDownloader() {
+ if (m_listener)
+ m_listener->OnStopRunningUrl(/* don't have a url */ nullptr, m_status);
+ if (m_newsDB) {
+ m_newsDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ m_newsDB = nullptr;
+ }
+}
+
+NS_IMETHODIMP nsNewsDownloader::OnStartRunningUrl(nsIURI* url) { return NS_OK; }
+
+NS_IMETHODIMP nsNewsDownloader::OnStopRunningUrl(nsIURI* url,
+ nsresult exitCode) {
+ bool stopped = false;
+ if (m_window) m_window->GetStopped(&stopped);
+ if (stopped) exitCode = NS_BINDING_ABORTED;
+
+ nsresult rv = exitCode;
+ if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND)
+ rv = DownloadNext(false);
+
+ return rv;
+}
+
+nsresult nsNewsDownloader::DownloadNext(bool firstTimeP) {
+ nsresult rv;
+ if (!firstTimeP) {
+ bool moreHeaders = GetNextHdrToRetrieve();
+ if (!moreHeaders) {
+ if (m_listener) m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ return NS_OK;
+ }
+ }
+ StartDownload();
+ m_wroteAnyP = false;
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ return nntpService->FetchMessage(m_folder, m_keyToDownload, m_window, nullptr,
+ this, getter_AddRefs(uri));
+}
+
+bool DownloadNewsArticlesToOfflineStore::GetNextHdrToRetrieve() {
+ nsresult rv;
+
+ if (m_downloadFromKeys) return nsNewsDownloader::GetNextHdrToRetrieve();
+
+ if (m_headerEnumerator == nullptr)
+ rv = m_newsDB->EnumerateMessages(getter_AddRefs(m_headerEnumerator));
+
+ bool hasMore = false;
+
+ while (NS_SUCCEEDED(rv = m_headerEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ rv = m_headerEnumerator->GetNext(getter_AddRefs(m_newsHeader));
+ NS_ENSURE_SUCCESS(rv, false);
+ uint32_t hdrFlags;
+ m_newsHeader->GetFlags(&hdrFlags);
+ if (hdrFlags & nsMsgMessageFlags::Marked) {
+ m_newsHeader->GetMessageKey(&m_keyToDownload);
+ break;
+ } else {
+ m_newsHeader = nullptr;
+ }
+ }
+ return hasMore;
+}
+
+void nsNewsDownloader::Abort() {}
+void nsNewsDownloader::Complete() {}
+
+bool nsNewsDownloader::GetNextHdrToRetrieve() {
+ nsresult rv;
+ if (m_downloadFromKeys) {
+ if (m_numwrote >= (int32_t)m_keysToDownload.Length()) return false;
+
+ m_keyToDownload = m_keysToDownload[m_numwrote++];
+ int32_t percent;
+ percent = (100 * m_numwrote) / (int32_t)m_keysToDownload.Length();
+
+ int64_t nowMS = 0;
+ if (percent < 100) // always need to do 100%
+ {
+ nowMS = PR_IntervalToMilliseconds(PR_IntervalNow());
+ if (nowMS - m_lastProgressTime < 750) return true;
+ }
+
+ m_lastProgressTime = nowMS;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, false);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(NEWS_MSGS_URL, getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoString firstStr;
+ firstStr.AppendInt(m_numwrote);
+ nsAutoString totalStr;
+ totalStr.AppendInt(int(m_keysToDownload.Length()));
+ nsString prettyName;
+ nsString statusString;
+
+ m_folder->GetPrettyName(prettyName);
+
+ AutoTArray<nsString, 3> formatStrings = {firstStr, totalStr, prettyName};
+ rv = bundle->FormatStringFromName("downloadingArticlesForOffline",
+ formatStrings, statusString);
+ NS_ENSURE_SUCCESS(rv, false);
+ ShowProgress(statusString.get(), percent);
+ return true;
+ }
+ NS_ASSERTION(false, "shouldn't get here if we're not downloading from keys.");
+ return false; // shouldn't get here if we're not downloading from keys.
+}
+
+nsresult nsNewsDownloader::ShowProgress(const char16_t* progressString,
+ int32_t percent) {
+ if (!m_statusFeedback) {
+ if (m_window) m_window->GetStatusFeedback(getter_AddRefs(m_statusFeedback));
+ }
+ if (m_statusFeedback) {
+ m_statusFeedback->ShowStatusString(nsDependentString(progressString));
+ if (percent != m_lastPercent) {
+ m_statusFeedback->ShowProgress(percent);
+ m_lastPercent = percent;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStartRunningUrl(
+ nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStopRunningUrl(
+ nsIURI* url, nsresult exitCode) {
+ m_status = exitCode;
+ if (m_newsHeader != nullptr) {
+#ifdef DEBUG_bienvenu
+ // XP_Trace("finished retrieving %ld\n", m_newsHeader->GetMessageKey());
+#endif
+ if (m_newsDB) {
+ nsMsgKey msgKey;
+ m_newsHeader->GetMessageKey(&msgKey);
+ m_newsDB->MarkMarked(msgKey, false, nullptr);
+ }
+ }
+ m_newsHeader = nullptr;
+ return nsNewsDownloader::OnStopRunningUrl(url, exitCode);
+}
+
+int DownloadNewsArticlesToOfflineStore::FinishDownload() { return 0; }
+
+NS_IMETHODIMP nsNewsDownloader::OnSearchHit(nsIMsgDBHdr* header,
+ nsIMsgFolder* folder) {
+ NS_ENSURE_ARG(header);
+
+ uint32_t msgFlags;
+ header->GetFlags(&msgFlags);
+ // only need to download articles we don't already have...
+ if (!(msgFlags & nsMsgMessageFlags::Offline)) {
+ nsMsgKey key;
+ header->GetMessageKey(&key);
+ m_keysToDownload.AppendElement(key);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNewsDownloader::OnSearchDone(nsresult status) {
+ if (m_keysToDownload.IsEmpty()) {
+ if (m_listener) return m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ }
+ nsresult rv = DownloadArticles(
+ m_window, m_folder,
+ /* we've already set m_keysToDownload, so don't pass it in */ nullptr);
+ if (NS_FAILED(rv))
+ if (m_listener) m_listener->OnStopRunningUrl(nullptr, rv);
+
+ return rv;
+}
+NS_IMETHODIMP nsNewsDownloader::OnNewSearch() { return NS_OK; }
+
+int DownloadNewsArticlesToOfflineStore::StartDownload() {
+ m_newsDB->GetMsgHdrForKey(m_keyToDownload, getter_AddRefs(m_newsHeader));
+ return 0;
+}
+
+DownloadNewsArticlesToOfflineStore::DownloadNewsArticlesToOfflineStore(
+ nsIMsgWindow* window, nsIMsgDatabase* db, nsIUrlListener* listener)
+ : nsNewsDownloader(window, db, listener) {
+ m_newsDB = db;
+}
+
+DownloadNewsArticlesToOfflineStore::~DownloadNewsArticlesToOfflineStore() {}
+
+DownloadMatchingNewsArticlesToNewsDB::DownloadMatchingNewsArticlesToNewsDB(
+ nsIMsgWindow* window, nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
+ nsIUrlListener* listener)
+ : DownloadNewsArticlesToOfflineStore(window, newsDB, listener) {
+ m_window = window;
+ m_folder = folder;
+ m_newsDB = newsDB;
+ m_downloadFromKeys = true; // search term matching means downloadFromKeys.
+}
+
+DownloadMatchingNewsArticlesToNewsDB::~DownloadMatchingNewsArticlesToNewsDB() {}
+
+NS_IMPL_ISUPPORTS(nsMsgDownloadAllNewsgroups, nsIUrlListener)
+
+nsMsgDownloadAllNewsgroups::nsMsgDownloadAllNewsgroups(
+ nsIMsgWindow* window, nsIUrlListener* listener) {
+ m_window = window;
+ m_listener = listener;
+ m_downloaderForGroup =
+ new DownloadMatchingNewsArticlesToNewsDB(window, nullptr, nullptr, this);
+ m_downloadedHdrsForCurGroup = false;
+}
+
+nsMsgDownloadAllNewsgroups::~nsMsgDownloadAllNewsgroups() {}
+
+NS_IMETHODIMP nsMsgDownloadAllNewsgroups::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDownloadAllNewsgroups::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
+ nsresult rv = exitCode;
+ if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND) {
+ if (m_downloadedHdrsForCurGroup) {
+ bool savingArticlesOffline = false;
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder =
+ do_QueryInterface(m_currentFolder);
+ if (newsFolder) newsFolder->GetSaveArticleOffline(&savingArticlesOffline);
+
+ m_downloadedHdrsForCurGroup = false;
+ if (savingArticlesOffline) // skip this group - we're saving to it
+ // already
+ rv = ProcessNextGroup();
+ else
+ rv = DownloadMsgsForCurrentGroup();
+ } else {
+ rv = ProcessNextGroup();
+ }
+ } else if (m_listener) // notify main observer.
+ m_listener->OnStopRunningUrl(url, exitCode);
+
+ return rv;
+}
+
+/**
+ * Leaves m_currentServer at the next nntp "server" that
+ * might have folders to download for offline use. If no more servers,
+ * m_currentServer will be left at nullptr and the function returns false.
+ * Also, sets up m_folderQueue to hold a (reversed) list of all the folders
+ * to consider for the current server.
+ * If no servers found, returns false.
+ */
+bool nsMsgDownloadAllNewsgroups::AdvanceToNextServer() {
+ nsresult rv;
+
+ if (m_allServers.IsEmpty()) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ASSERTION(accountManager && NS_SUCCEEDED(rv),
+ "couldn't get account mgr");
+ if (!accountManager || NS_FAILED(rv)) return false;
+
+ rv = accountManager->GetAllServers(m_allServers);
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+ size_t serverIndex = 0;
+ if (m_currentServer) {
+ serverIndex = m_allServers.IndexOf(m_currentServer);
+ if (serverIndex == m_allServers.NoIndex) {
+ serverIndex = 0;
+ } else {
+ ++serverIndex;
+ }
+ }
+ m_currentServer = nullptr;
+ uint32_t numServers = m_allServers.Length();
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+
+ while (serverIndex < numServers) {
+ nsCOMPtr<nsIMsgIncomingServer> server(m_allServers[serverIndex]);
+ serverIndex++;
+
+ nsCOMPtr<nsINntpIncomingServer> newsServer = do_QueryInterface(server);
+ if (!newsServer) // we're only looking for news servers
+ continue;
+
+ if (server) {
+ m_currentServer = server;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ rv = rootFolder->GetDescendants(m_folderQueue);
+ if (NS_SUCCEEDED(rv)) {
+ if (!m_folderQueue.IsEmpty()) {
+ // We'll be popping folders from the end of the queue as we go.
+ m_folderQueue.Reverse();
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Sets m_currentFolder to the next usable folder.
+ *
+ * @return False if no more folders found, otherwise true.
+ */
+bool nsMsgDownloadAllNewsgroups::AdvanceToNextGroup() {
+ nsresult rv = NS_OK;
+
+ if (m_currentFolder) {
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
+ if (newsFolder) newsFolder->SetSaveArticleOffline(false);
+
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ if (NS_SUCCEEDED(rv) && session) {
+ bool folderOpen;
+ uint32_t folderFlags;
+ m_currentFolder->GetFlags(&folderFlags);
+ session->IsFolderOpenInWindow(m_currentFolder, &folderOpen);
+ if (!folderOpen &&
+ !(folderFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox)))
+ m_currentFolder->SetMsgDatabase(nullptr);
+ }
+ m_currentFolder = nullptr;
+ }
+
+ bool hasMore = false;
+ if (m_currentServer) {
+ hasMore = !m_folderQueue.IsEmpty();
+ }
+ if (!hasMore) {
+ hasMore = AdvanceToNextServer();
+ }
+
+ if (hasMore) {
+ m_currentFolder = m_folderQueue.PopLastElement();
+ }
+ return m_currentFolder;
+}
+
+nsresult DownloadMatchingNewsArticlesToNewsDB::RunSearch(
+ nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
+ nsIMsgSearchSession* searchSession) {
+ m_folder = folder;
+ m_newsDB = newsDB;
+ m_searchSession = searchSession;
+
+ m_keysToDownload.Clear();
+
+ NS_ENSURE_ARG(searchSession);
+ NS_ENSURE_ARG(folder);
+
+ searchSession->RegisterListener(this, nsIMsgSearchSession::allNotifications);
+ nsresult rv =
+ searchSession->AddScopeTerm(nsMsgSearchScope::localNews, folder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return searchSession->Search(m_window);
+}
+
+nsresult nsMsgDownloadAllNewsgroups::ProcessNextGroup() {
+ bool done = false;
+
+ while (!done) {
+ done = !AdvanceToNextGroup();
+ if (!done && m_currentFolder) {
+ uint32_t folderFlags;
+ m_currentFolder->GetFlags(&folderFlags);
+ if (folderFlags & nsMsgFolderFlags::Offline) break;
+ }
+ }
+ if (done) {
+ if (m_listener) return m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ }
+ m_downloadedHdrsForCurGroup = true;
+ return m_currentFolder ? m_currentFolder->GetNewMessages(m_window, this)
+ : NS_ERROR_NOT_INITIALIZED;
+}
+
+nsresult nsMsgDownloadAllNewsgroups::DownloadMsgsForCurrentGroup() {
+ NS_ENSURE_TRUE(m_downloaderForGroup, NS_ERROR_OUT_OF_MEMORY);
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIMsgDownloadSettings> downloadSettings;
+ m_currentFolder->GetMsgDatabase(getter_AddRefs(db));
+ nsresult rv =
+ m_currentFolder->GetDownloadSettings(getter_AddRefs(downloadSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
+ if (newsFolder) newsFolder->SetSaveArticleOffline(true);
+
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool downloadByDate, downloadUnreadOnly;
+ uint32_t ageLimitOfMsgsToDownload;
+
+ downloadSettings->GetDownloadByDate(&downloadByDate);
+ downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly);
+ downloadSettings->GetAgeLimitOfMsgsToDownload(&ageLimitOfMsgsToDownload);
+
+ nsCOMPtr<nsIMsgSearchTerm> term;
+ nsCOMPtr<nsIMsgSearchValue> value;
+
+ rv = searchSession->CreateTerm(getter_AddRefs(term));
+ NS_ENSURE_SUCCESS(rv, rv);
+ term->GetValue(getter_AddRefs(value));
+
+ if (downloadUnreadOnly) {
+ value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
+ value->SetStatus(nsMsgMessageFlags::Read);
+ searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus,
+ nsMsgSearchOp::Isnt, value, true, nullptr);
+ }
+ if (downloadByDate) {
+ value->SetAttrib(nsMsgSearchAttrib::AgeInDays);
+ value->SetAge(ageLimitOfMsgsToDownload);
+ searchSession->AddSearchTerm(nsMsgSearchAttrib::AgeInDays,
+ nsMsgSearchOp::IsLessThan, value,
+ nsMsgSearchBooleanOp::BooleanAND, nullptr);
+ }
+ value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
+ value->SetStatus(nsMsgMessageFlags::Offline);
+ searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus,
+ nsMsgSearchOp::Isnt, value,
+ nsMsgSearchBooleanOp::BooleanAND, nullptr);
+
+ m_downloaderForGroup->RunSearch(m_currentFolder, db, searchSession);
+ return rv;
+}
diff --git a/comm/mailnews/news/src/nsNewsDownloader.h b/comm/mailnews/news/src/nsNewsDownloader.h
new file mode 100644
index 0000000000..c1e68eb77d
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloader.h
@@ -0,0 +1,136 @@
+/* -*- 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 _nsNewsDownloader_H_
+#define _nsNewsDownloader_H_
+
+#include "nsIMsgDatabase.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsTArray.h"
+
+// base class for downloading articles in a single newsgroup. Keys to download
+// are passed in to DownloadArticles method.
+class nsNewsDownloader : public nsIUrlListener, public nsIMsgSearchNotify {
+ public:
+ nsNewsDownloader(nsIMsgWindow* window, nsIMsgDatabase* db,
+ nsIUrlListener* listener);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGSEARCHNOTIFY
+
+ virtual nsresult DownloadArticles(nsIMsgWindow* window, nsIMsgFolder* folder,
+ nsTArray<nsMsgKey>* pKeyArray);
+
+ bool ShouldAbort() const { return m_abort; }
+
+ protected:
+ virtual ~nsNewsDownloader();
+
+ virtual int32_t Write(const char* /*block*/, int32_t length) {
+ return length;
+ }
+ virtual void Abort();
+ virtual void Complete();
+ virtual bool GetNextHdrToRetrieve();
+ virtual nsresult DownloadNext(bool firstTimeP);
+ virtual int32_t FinishDownload() { return 0; }
+ virtual int32_t StartDownload() { return 0; }
+ virtual nsresult ShowProgress(const char16_t* progressString,
+ int32_t percent);
+
+ nsTArray<nsMsgKey> m_keysToDownload;
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsCOMPtr<nsIMsgDatabase> m_newsDB;
+ nsCOMPtr<nsIUrlListener> m_listener;
+ bool m_downloadFromKeys;
+ bool m_existedP;
+ bool m_wroteAnyP;
+ bool m_summaryValidP;
+ bool m_abort;
+ int32_t m_numwrote;
+ nsMsgKey m_keyToDownload;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsCOMPtr<nsIMsgStatusFeedback> m_statusFeedback;
+ nsCOMPtr<nsIMsgSearchSession> m_searchSession;
+ int32_t m_lastPercent;
+ int64_t m_lastProgressTime;
+ nsresult m_status;
+};
+
+// class for downloading articles in a single newsgroup to the offline store.
+class DownloadNewsArticlesToOfflineStore : public nsNewsDownloader {
+ public:
+ DownloadNewsArticlesToOfflineStore(nsIMsgWindow* window, nsIMsgDatabase* db,
+ nsIUrlListener* listener);
+ virtual ~DownloadNewsArticlesToOfflineStore();
+
+ NS_IMETHOD OnStartRunningUrl(nsIURI* url);
+ NS_IMETHOD OnStopRunningUrl(nsIURI* url, nsresult exitCode);
+
+ protected:
+ virtual int32_t StartDownload();
+ virtual int32_t FinishDownload();
+ virtual bool GetNextHdrToRetrieve();
+
+ nsCOMPtr<nsIMsgEnumerator> m_headerEnumerator;
+ nsCOMPtr<nsIMsgDBHdr> m_newsHeader;
+};
+
+// class for downloading all the articles that match the passed in search
+// criteria for a single newsgroup.
+class DownloadMatchingNewsArticlesToNewsDB
+ : public DownloadNewsArticlesToOfflineStore {
+ public:
+ DownloadMatchingNewsArticlesToNewsDB(nsIMsgWindow* window,
+ nsIMsgFolder* folder,
+ nsIMsgDatabase* newsDB,
+ nsIUrlListener* listener);
+ virtual ~DownloadMatchingNewsArticlesToNewsDB();
+ nsresult RunSearch(nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
+ nsIMsgSearchSession* searchSession);
+
+ protected:
+};
+
+// this class iterates all the news servers for each group on the server that's
+// configured for offline use, downloads the messages that meet the download
+// criteria for that newsgroup/server
+class nsMsgDownloadAllNewsgroups : public nsIUrlListener {
+ public:
+ nsMsgDownloadAllNewsgroups(nsIMsgWindow* window, nsIUrlListener* listener);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+
+ nsresult ProcessNextGroup();
+
+ protected:
+ virtual ~nsMsgDownloadAllNewsgroups();
+
+ bool AdvanceToNextServer();
+ bool AdvanceToNextGroup();
+ nsresult DownloadMsgsForCurrentGroup();
+
+ RefPtr<DownloadMatchingNewsArticlesToNewsDB> m_downloaderForGroup;
+
+ nsCOMPtr<nsIMsgFolder> m_currentFolder;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsTArray<RefPtr<nsIMsgIncomingServer>> m_allServers;
+ nsCOMPtr<nsIMsgIncomingServer> m_currentServer;
+ // Folders still to process for the current server.
+ nsTArray<RefPtr<nsIMsgFolder>> m_folderQueue;
+ nsCOMPtr<nsIUrlListener> m_listener;
+
+ bool m_downloadedHdrsForCurGroup;
+};
+
+#endif
diff --git a/comm/mailnews/news/src/nsNewsFolder.cpp b/comm/mailnews/news/src/nsNewsFolder.cpp
new file mode 100644
index 0000000000..f23b54e273
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsFolder.cpp
@@ -0,0 +1,1645 @@
+/* -*- 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 "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "prlog.h"
+
+#include "msgCore.h" // precompiled header...
+#include "nntpCore.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsNewsFolder.h"
+#include "nsMsgFolderFlags.h"
+#include "MailNewsTypes.h"
+#include "prprf.h"
+#include "prsystem.h"
+#include "nsTArray.h"
+#include "nsINntpService.h"
+#include "nsIMsgFilterService.h"
+#include "nsCOMPtr.h"
+#include "nsMsgUtils.h"
+#include "nsNewsUtils.h"
+
+#include "nsIMsgIncomingServer.h"
+#include "nsINntpIncomingServer.h"
+#include "nsINewsDatabase.h"
+#include "nsILineInputStream.h"
+
+#include "nsIMsgWindow.h"
+#include "nsIWindowWatcher.h"
+
+#include "nsNetUtil.h"
+#include "nsIAuthPrompt.h"
+#include "nsIURL.h"
+#include "nsNetCID.h"
+#include "nsINntpUrl.h"
+
+#include "nsNewsDownloader.h"
+#include "nsIStringBundle.h"
+#include "nsMsgI18N.h"
+#include "nsNativeCharsetUtils.h"
+
+#include "nsIMsgFolderNotificationService.h"
+#include "nsILoginInfo.h"
+#include "nsILoginManager.h"
+#include "nsEmbedCID.h"
+#include "mozilla/Components.h"
+#include "mozilla/SlicedInputStream.h"
+#include "nsIInputStream.h"
+#include "nsMemory.h"
+#include "nsIURIMutator.h"
+
+#define kNewsSortOffset 9000
+
+#define NEWS_SCHEME "news:"
+#define SNEWS_SCHEME "snews:"
+
+////////////////////////////////////////////////////////////////////////////////
+
+nsMsgNewsFolder::nsMsgNewsFolder(void)
+ : mExpungedBytes(0),
+ mGettingNews(false),
+ mInitialized(false),
+ m_downloadMessageForOfflineUse(false),
+ mReadSet(nullptr),
+ mSortOrder(kNewsSortOffset) {
+ mFolderSize = kSizeUnknown;
+}
+
+nsMsgNewsFolder::~nsMsgNewsFolder(void) {}
+
+NS_IMPL_ADDREF_INHERITED(nsMsgNewsFolder, nsMsgDBFolder)
+NS_IMPL_RELEASE_INHERITED(nsMsgNewsFolder, nsMsgDBFolder)
+
+NS_IMETHODIMP nsMsgNewsFolder::QueryInterface(REFNSIID aIID,
+ void** aInstancePtr) {
+ if (!aInstancePtr) return NS_ERROR_NULL_POINTER;
+ *aInstancePtr = nullptr;
+
+ if (aIID.Equals(NS_GET_IID(nsIMsgNewsFolder)))
+ *aInstancePtr = static_cast<nsIMsgNewsFolder*>(this);
+ if (*aInstancePtr) {
+ AddRef();
+ return NS_OK;
+ }
+
+ return nsMsgDBFolder::QueryInterface(aIID, aInstancePtr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsMsgNewsFolder::CreateSubFolders(nsIFile* path) {
+ nsresult rv;
+ bool isNewsServer = false;
+ rv = GetIsServer(&isNewsServer);
+ if (NS_FAILED(rv)) return rv;
+
+ if (isNewsServer) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->GetNewsrcFilePath(getter_AddRefs(mNewsrcFilePath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = LoadNewsrcFileAndCreateNewsgroups();
+ } else // is not a host, so it has no newsgroups. (what about categories??)
+ rv = NS_OK;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::AddNewsgroup(const nsACString& name, const nsACString& setStr,
+ nsIMsgFolder** child) {
+ NS_ENSURE_ARG_POINTER(child);
+ nsresult rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString uri(mURI);
+ uri.Append('/');
+ // URI should use UTF-8
+ // (see RFC2396 Uniform Resource Identifiers (URI): Generic Syntax)
+
+ // we are handling newsgroup names in UTF-8
+ NS_ConvertUTF8toUTF16 nameUtf16(name);
+
+ nsAutoCString escapedName;
+ rv = NS_MsgEscapeEncodeURLPath(nameUtf16, escapedName);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = nntpServer->AddNewsgroup(nameUtf16);
+ if (NS_FAILED(rv)) return rv;
+
+ uri.Append(escapedName);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(uri, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder(do_QueryInterface(folder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensure any containing .sdb dir exists.
+ nsCOMPtr<nsIFile> path;
+ rv = CreateDirectoryForFolder(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // cache this for when we open the db
+ rv = newsFolder->SetReadSetFromStr(setStr);
+
+ rv = folder->SetParent(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this what shows up in the UI
+ rv = folder->SetName(nameUtf16);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = folder->SetFlag(nsMsgFolderFlags::Newsgroup);
+ if (NS_FAILED(rv)) return rv;
+
+ int32_t numExistingGroups = mSubFolders.Count();
+
+ // add kNewsSortOffset (9000) to prevent this problem: 1,10,11,2,3,4,5
+ // We use 9000 instead of 1000 so newsgroups will sort to bottom of flat
+ // folder views
+ rv = folder->SetSortOrder(numExistingGroups + kNewsSortOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mSubFolders.AppendObject(folder);
+ folder->SetParent(this);
+ folder.forget(child);
+ return rv;
+}
+
+nsresult nsMsgNewsFolder::ParseFolder(nsIFile* path) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsMsgNewsFolder::AddDirectorySeparator(nsIFile* path) {
+ // don't concat the full separator with .sbd
+ return (mURI.Equals(kNewsRootURI))
+ ? NS_OK
+ : nsMsgDBFolder::AddDirectorySeparator(path);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) {
+ if (!mInitialized) {
+ // do this first, so we make sure to do it, even on failure.
+ // see bug #70494
+ mInitialized = true;
+
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = GetFilePath(getter_AddRefs(path));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = CreateSubFolders(path);
+ if (NS_FAILED(rv)) return rv;
+
+ // force ourselves to get initialized from cache
+ // Don't care if it fails. this will fail the first time after
+ // migration, but we continue on. see #66018
+ (void)UpdateSummaryTotals(false);
+ }
+
+ return nsMsgDBFolder::GetSubFolders(folders);
+}
+
+// Makes sure the database is open and exists. If the database is valid then
+// returns NS_OK. Otherwise returns a failure error value.
+nsresult nsMsgNewsFolder::GetDatabase() {
+ nsresult rv;
+ if (!mDatabase) {
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the database, blowing it away if it's out of date.
+ rv = msgDBService->OpenFolderDB(this, false, getter_AddRefs(mDatabase));
+ if (NS_FAILED(rv))
+ rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mAddListener) rv = mDatabase->AddListener(this);
+
+ nsCOMPtr<nsINewsDatabase> db = do_QueryInterface(mDatabase, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = db->SetReadSet(mReadSet);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = UpdateSummaryTotals(true);
+ if (NS_FAILED(rv)) return rv;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::UpdateFolder(nsIMsgWindow* aWindow) {
+ // Get news.get_messages_on_select pref
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool getMessagesOnSelect = true;
+ prefBranch->GetBoolPref("news.get_messages_on_select", &getMessagesOnSelect);
+
+ // Only if news.get_messages_on_select is true do we get new messages
+ // automatically
+ if (getMessagesOnSelect) {
+ rv = GetDatabase(); // want this cached...
+ if (NS_SUCCEEDED(rv)) {
+ if (mDatabase) {
+ nsCOMPtr<nsIMsgRetentionSettings> retentionSettings;
+ nsresult rv = GetRetentionSettings(getter_AddRefs(retentionSettings));
+ if (NS_SUCCEEDED(rv))
+ rv = mDatabase->ApplyRetentionSettings(retentionSettings, false);
+ }
+ rv = AutoCompact(aWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // GetNewMessages has to be the last rv set before we get to the next
+ // check, so that we'll have rv set to NS_MSG_ERROR_OFFLINE when offline
+ // and send a folder loaded notification to the front end.
+ rv = GetNewMessages(aWindow, nullptr);
+ }
+ if (rv != NS_MSG_ERROR_OFFLINE) return rv;
+ }
+ // We're not getting messages because either get_messages_on_select is
+ // false or we're offline. Send an immediate folder loaded notification.
+ NotifyFolderEvent(kFolderLoaded);
+ (void)RefreshSizeOnDisk();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanSubscribe(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+
+ bool isNewsServer = false;
+ nsresult rv = GetIsServer(&isNewsServer);
+ if (NS_FAILED(rv)) return rv;
+
+ // you can only subscribe to news servers, not news groups
+ *aResult = isNewsServer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanFileMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ // you can't file messages into a news server or news group
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanCreateSubfolders(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // you can't create subfolders on a news server or a news group
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanRename(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // you can't rename a news server or a news group
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanCompact(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // you can't compact a news server or a news group
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetFolderURL(nsACString& aUrl) {
+ nsCString hostName;
+ nsresult rv = GetHostname(hostName);
+ nsString groupName;
+ rv = GetName(groupName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ rv = server->GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t port;
+ rv = server->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, rv);
+ const char* newsScheme =
+ (socketType == nsMsgSocketType::SSL) ? SNEWS_SCHEME : NEWS_SCHEME;
+ nsCString escapedName;
+ rv = NS_MsgEscapeEncodeURLPath(groupName, escapedName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString tmpStr;
+ tmpStr.Adopt(PR_smprintf("%s//%s:%ld/%s", newsScheme, hostName.get(), port,
+ escapedName.get()));
+ aUrl.Assign(tmpStr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetNewsrcHasChanged(bool newsrcHasChanged) {
+ nsresult rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+ return nntpServer->SetNewsrcHasChanged(newsrcHasChanged);
+}
+
+nsresult nsMsgNewsFolder::CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) {
+ nsMsgNewsFolder* newFolder = new nsMsgNewsFolder;
+ NS_ADDREF(*folder = newFolder);
+ newFolder->Init(uri);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CreateSubfolder(const nsAString& newsgroupName,
+ nsIMsgWindow* msgWindow) {
+ nsresult rv = NS_OK;
+ if (newsgroupName.IsEmpty()) return NS_MSG_ERROR_INVALID_FOLDER_NAME;
+
+ nsCOMPtr<nsIMsgFolder> child;
+ // Now let's create the actual new folder
+ rv = AddNewsgroup(NS_ConvertUTF16toUTF8(newsgroupName), EmptyCString(),
+ getter_AddRefs(child));
+
+ if (NS_SUCCEEDED(rv))
+ SetNewsrcHasChanged(true); // subscribe UI does this - but maybe we got
+ // here through auto-subscribe
+
+ if (NS_SUCCEEDED(rv) && child) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // Used to init some folder status of child.
+ rv = child->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NotifyFolderAdded(child);
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderAdded(child);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::DeleteStorage() {
+ nsresult rv = nsMsgDBFolder::DeleteStorage();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoString name;
+ rv = GetUnicodeName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->RemoveNewsgroup(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (void)RefreshSizeOnDisk();
+
+ return SetNewsrcHasChanged(true);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::Rename(const nsAString& newName,
+ nsIMsgWindow* msgWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetAbbreviatedName(nsAString& aAbbreviatedName) {
+ nsresult rv;
+
+ rv = nsMsgDBFolder::GetPrettyName(aAbbreviatedName);
+ if (NS_FAILED(rv)) return rv;
+
+ // only do this for newsgroup names, not for newsgroup hosts.
+ bool isNewsServer = false;
+ rv = GetIsServer(&isNewsServer);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!isNewsServer) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ bool abbreviate = true;
+ rv = nntpServer->GetAbbreviate(&abbreviate);
+ if (NS_FAILED(rv)) return rv;
+
+ if (abbreviate)
+ rv = AbbreviatePrettyName(aAbbreviatedName, 1 /* hardcoded for now */);
+ }
+ return rv;
+}
+
+// original code from Oleg Rekutin
+// rekusha@asan.com
+// Public domain, created by Oleg Rekutin
+//
+// takes a newsgroup name, number of words from the end to leave unabberviated
+// the newsgroup name, will get reset to the following format:
+// x.x.x, where x is the first letter of each word and with the
+// exception of last 'fullwords' words, which are left intact.
+// If a word has a dash in it, it is abbreviated as a-b, where
+// 'a' is the first letter of the part of the word before the
+// dash and 'b' is the first letter of the part of the word after
+// the dash
+nsresult nsMsgNewsFolder::AbbreviatePrettyName(nsAString& prettyName,
+ int32_t fullwords) {
+ nsAutoString name(prettyName);
+ int32_t totalwords = 0; // total no. of words
+
+ // get the total no. of words
+ int32_t pos = 0;
+ while (1) {
+ pos = name.FindChar('.', pos);
+ if (pos == -1) {
+ totalwords++;
+ break;
+ } else {
+ totalwords++;
+ pos++;
+ }
+ }
+
+ // get the no. of words to abbreviate
+ int32_t abbrevnum = totalwords - fullwords;
+ if (abbrevnum < 1) return NS_OK; // nothing to abbreviate
+
+ // build the ellipsis
+ nsAutoString out;
+ out += name[0];
+
+ int32_t length = name.Length();
+ int32_t newword = 0; // == 2 if done with all abbreviated words
+
+ fullwords = 0;
+ char16_t currentChar;
+ for (int32_t i = 1; i < length; i++) {
+ // this temporary assignment is needed to fix an intel mac compiler bug.
+ // See Bug #327037 for details.
+ currentChar = name[i];
+ if (newword < 2) {
+ switch (currentChar) {
+ case '.':
+ fullwords++;
+ // check if done with all abbreviated words...
+ if (fullwords == abbrevnum)
+ newword = 2;
+ else
+ newword = 1;
+ break;
+ case '-':
+ newword = 1;
+ break;
+ default:
+ if (newword)
+ newword = 0;
+ else
+ continue;
+ }
+ }
+ out.Append(currentChar);
+ }
+ prettyName = out;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) {
+ NS_ENSURE_ARG_POINTER(folderInfo);
+ NS_ENSURE_ARG_POINTER(db);
+ nsresult openErr;
+ openErr = GetDatabase();
+ if (!mDatabase) {
+ *db = nullptr;
+ return openErr;
+ }
+
+ NS_ADDREF(*db = mDatabase);
+
+ if (NS_SUCCEEDED(openErr)) openErr = (*db)->GetDBFolderInfo(folderInfo);
+ return openErr;
+}
+
+/* this used to be MSG_FolderInfoNews::UpdateSummaryFromNNTPInfo() */
+NS_IMETHODIMP
+nsMsgNewsFolder::UpdateSummaryFromNNTPInfo(int32_t oldest, int32_t youngest,
+ int32_t total) {
+ NS_ENSURE_STATE(mReadSet);
+ /* First, mark all of the articles now known to be expired as read. */
+ if (oldest > 1) {
+ nsCString oldSet;
+ nsCString newSet;
+ mReadSet->Output(getter_Copies(oldSet));
+ mReadSet->AddRange(1, oldest - 1);
+ mReadSet->Output(getter_Copies(newSet));
+ }
+
+ /* Now search the newsrc line and figure out how many of these messages are
+ * marked as unread. */
+
+ /* make sure youngest is a least 1. MSNews seems to return a youngest of 0. */
+ if (youngest == 0) youngest = 1;
+
+ int32_t unread = mReadSet->CountMissingInRange(oldest, youngest);
+ NS_ASSERTION(unread >= 0, "CountMissingInRange reported unread < 0");
+ if (unread < 0)
+ // servers can send us stuff like "211 0 41 40 nz.netstatus"
+ // we should handle it gracefully.
+ unread = 0;
+
+ if (unread > total) {
+ /* This can happen when the newsrc file shows more unread than exist in the
+ * group (total is not necessarily `end - start'.) */
+ unread = total;
+ int32_t deltaInDB = mNumTotalMessages - mNumUnreadMessages;
+ // int32_t deltaInDB = m_totalInDB - m_unreadInDB;
+ /* if we know there are read messages in the db, subtract that from the
+ * unread total */
+ if (deltaInDB > 0) unread -= deltaInDB;
+ }
+
+ bool dbWasOpen = mDatabase != nullptr;
+ int32_t pendingUnreadDelta =
+ unread - mNumUnreadMessages - mNumPendingUnreadMessages;
+ int32_t pendingTotalDelta =
+ total - mNumTotalMessages - mNumPendingTotalMessages;
+ ChangeNumPendingUnread(pendingUnreadDelta);
+ ChangeNumPendingTotalMessages(pendingTotalDelta);
+ if (!dbWasOpen && mDatabase) {
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ mDatabase->RemoveListener(this);
+ mDatabase = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetExpungedBytesCount(int64_t* count) {
+ NS_ENSURE_ARG_POINTER(count);
+ *count = mExpungedBytes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetDeletable(bool* deletable) {
+ NS_ENSURE_ARG_POINTER(deletable);
+
+ *deletable = false;
+ // For legacy reasons, there can be Saved search folders under news accounts.
+ // Allow deleting those.
+ GetFlag(nsMsgFolderFlags::Virtual, deletable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::RefreshSizeOnDisk() {
+ uint64_t oldFolderSize = mFolderSize;
+ // We set size to unknown to force it to get recalculated from disk.
+ mFolderSize = kSizeUnknown;
+ if (NS_SUCCEEDED(GetSizeOnDisk(&mFolderSize)))
+ NotifyIntPropertyChanged(kFolderSize, oldFolderSize, mFolderSize);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetSizeOnDisk(int64_t* size) {
+ NS_ENSURE_ARG_POINTER(size);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ // If this is the rootFolder, return 0 as a safe value.
+ if (NS_FAILED(rv) || isServer) mFolderSize = 0;
+
+ // 0 is a valid folder size (meaning empty file with no offline messages),
+ // but 1 is not. So use -1 as a special value meaning no file size was fetched
+ // from disk yet.
+ if (mFolderSize == kSizeUnknown) {
+ nsCOMPtr<nsIFile> diskFile;
+ nsresult rv = GetFilePath(getter_AddRefs(diskFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If there were no news messages downloaded for offline use, the folder
+ // file may not exist yet. In that case size is 0.
+ bool exists = false;
+ rv = diskFile->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) {
+ mFolderSize = 0;
+ } else {
+ int64_t fileSize;
+ rv = diskFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mFolderSize = fileSize;
+ }
+ }
+
+ *size = mFolderSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& msgHdrs,
+ nsIMsgWindow* aMsgWindow, bool deleteStorage,
+ bool isMove,
+ nsIMsgCopyServiceListener* listener,
+ bool allowUndo) {
+ nsresult rv = NS_OK;
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ if (!isMove) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyMsgsDeleted(msgHdrs);
+ }
+
+ rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableNotifications(allMessageCountNotifications, false);
+ if (NS_SUCCEEDED(rv)) {
+ for (auto msgHdr : msgHdrs) {
+ rv = mDatabase->DeleteHeader(msgHdr, nullptr, true, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ EnableNotifications(allMessageCountNotifications, true);
+ }
+
+ if (!isMove)
+ NotifyFolderEvent(NS_SUCCEEDED(rv) ? kDeleteOrMoveMsgCompleted
+ : kDeleteOrMoveMsgFailed);
+
+ if (listener) {
+ listener->OnStartCopy();
+ listener->OnStopCopy(NS_OK);
+ }
+
+ (void)RefreshSizeOnDisk();
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CancelMessage(nsIMsgDBHdr* msgHdr,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ nsresult rv;
+
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // for cancel, we need to
+ // turn "newsmessage://sspitzer@news.mozilla.org/netscape.test#5428"
+ // into "news://sspitzer@news.mozilla.org/23423@netscape.com"
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverURI;
+ rv = server->GetServerURI(serverURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString messageID;
+ rv = msgHdr->GetMessageId(getter_Copies(messageID));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we need to escape the message ID,
+ // it might contain characters which will mess us up later, like #
+ // see bug #120502
+ nsCString escapedMessageID;
+ MsgEscapeString(messageID, nsINetUtil::ESCAPE_URL_PATH, escapedMessageID);
+
+ nsAutoCString cancelURL(serverURI.get());
+ cancelURL += '/';
+ cancelURL += escapedMessageID;
+ cancelURL += "?cancel";
+
+ nsCString messageURI;
+ rv = GetUriForMsg(msgHdr, messageURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> resultUri;
+ return nntpService->CancelMessage(cancelURL, messageURI,
+ nullptr /* consumer */, nullptr, aMsgWindow,
+ getter_AddRefs(resultUri));
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetNewMessages(nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aListener) {
+ return GetNewsMessages(aMsgWindow, false, aListener);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetNextNMessages(nsIMsgWindow* aMsgWindow) {
+ return GetNewsMessages(aMsgWindow, true, nullptr);
+}
+
+nsresult nsMsgNewsFolder::GetNewsMessages(nsIMsgWindow* aMsgWindow,
+ bool aGetOld,
+ nsIUrlListener* aUrlListener) {
+ nsresult rv = NS_OK;
+
+ bool isNewsServer = false;
+ rv = GetIsServer(&isNewsServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isNewsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return server->PerformExpand(aMsgWindow);
+ }
+
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> resultUri;
+ rv = nntpService->GetNewNews(nntpServer, mURI, aGetOld, this, aMsgWindow,
+ getter_AddRefs(resultUri));
+ if (aUrlListener && NS_SUCCEEDED(rv) && resultUri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl(do_QueryInterface(resultUri));
+ if (msgUrl) msgUrl->RegisterListener(aUrlListener);
+ }
+ return rv;
+}
+
+nsresult nsMsgNewsFolder::LoadNewsrcFileAndCreateNewsgroups() {
+ nsresult rv = NS_OK;
+ if (!mNewsrcFilePath) return NS_ERROR_FAILURE;
+
+ bool exists;
+ rv = mNewsrcFilePath->Exists(&exists);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!exists)
+ // it is ok for the newsrc file to not exist yet
+ return NS_OK;
+
+ nsCOMPtr<nsIInputStream> fileStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), mNewsrcFilePath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILineInputStream> lineInputStream(
+ do_QueryInterface(fileStream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more = true;
+ nsCString line;
+
+ while (more && NS_SUCCEEDED(rv)) {
+ rv = lineInputStream->ReadLine(line, &more);
+ if (line.IsEmpty()) continue;
+ HandleNewsrcLine(line.get(), line.Length());
+ }
+
+ fileStream->Close();
+ return rv;
+}
+
+int32_t nsMsgNewsFolder::HandleNewsrcLine(const char* line,
+ uint32_t line_size) {
+ nsresult rv;
+
+ /* guard against blank line lossage */
+ if (line[0] == '#' || line[0] == '\r' || line[0] == '\n') return 0;
+
+ if ((line[0] == 'o' || line[0] == 'O') && !PL_strncasecmp(line, "options", 7))
+ return RememberLine(nsDependentCString(line));
+
+ const char* s = nullptr;
+ const char* setStr = nullptr;
+ const char* end = line + line_size;
+
+ for (s = line; s < end; s++)
+ if ((*s == ':') || (*s == '!')) break;
+
+ if (*s == 0) /* What is this?? Well, don't just throw it away... */
+ return RememberLine(nsDependentCString(line));
+
+ bool subscribed = (*s == ':');
+ setStr = s + 1;
+
+ if (*line == '\0') return 0;
+
+ // previous versions of Communicator polluted the
+ // newsrc files with articles
+ // (this would happen when you clicked on a link like
+ // news://news.mozilla.org/3746EF3F.6080309@netscape.com)
+ //
+ // legal newsgroup names can't contain @ or %
+ //
+ // News group names are structured into parts separated by dots,
+ // for example "netscape.public.mozilla.mail-news".
+ // Each part may be up to 14 characters long, and should consist
+ // only of letters, digits, "+" and "-", with at least one letter
+ //
+ // @ indicates an article and %40 is @ escaped.
+ // previous versions of Communicator also dumped
+ // the escaped version into the newsrc file
+ //
+ // So lines like this in a newsrc file should be ignored:
+ // 3746EF3F.6080309@netscape.com:
+ // 3746EF3F.6080309%40netscape.com:
+ if (PL_strchr(line, '@') || PL_strstr(line, "%40"))
+ // skipping, it contains @ or %40
+ subscribed = false;
+
+ if (subscribed) {
+ // we're subscribed, so add it
+ nsCOMPtr<nsIMsgFolder> child;
+
+ rv = AddNewsgroup(Substring(line, s), nsDependentCString(setStr),
+ getter_AddRefs(child));
+ if (NS_FAILED(rv)) return -1;
+ } else {
+ rv = RememberUnsubscribedGroup(nsDependentCString(line),
+ nsDependentCString(setStr));
+ if (NS_FAILED(rv)) return -1;
+ }
+
+ return 0;
+}
+
+nsresult nsMsgNewsFolder::RememberUnsubscribedGroup(const nsACString& newsgroup,
+ const nsACString& setStr) {
+ mUnsubscribedNewsgroupLines.Append(newsgroup);
+ mUnsubscribedNewsgroupLines.AppendLiteral("! ");
+ if (!setStr.IsEmpty())
+ mUnsubscribedNewsgroupLines.Append(setStr);
+ else
+ mUnsubscribedNewsgroupLines.Append(MSG_LINEBREAK);
+ return NS_OK;
+}
+
+int32_t nsMsgNewsFolder::RememberLine(const nsACString& line) {
+ mOptionLines = line;
+ mOptionLines.Append(MSG_LINEBREAK);
+ return 0;
+}
+
+nsresult nsMsgNewsFolder::ForgetLine() {
+ mOptionLines.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetGroupUsername(nsACString& aGroupUsername) {
+ aGroupUsername = mGroupUsername;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetGroupUsername(
+ const nsACString& aGroupUsername) {
+ mGroupUsername = aGroupUsername;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetGroupPassword(nsACString& aGroupPassword) {
+ aGroupPassword = mGroupPassword;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetGroupPassword(
+ const nsACString& aGroupPassword) {
+ mGroupPassword = aGroupPassword;
+ return NS_OK;
+}
+
+nsresult nsMsgNewsFolder::CreateNewsgroupUrlForSignon(const char* ref,
+ nsAString& result) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ bool singleSignon = true;
+ rv = nntpServer->GetSingleSignon(&singleSignon);
+
+ nsCOMPtr<nsIURL> url;
+ if (singleSignon) {
+ // Do not include username in the url when interacting with LoginManager.
+ nsCString serverURI = "news://"_ns;
+ nsCString hostName;
+ rv = server->GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ serverURI.Append(hostName);
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(serverURI)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(mURI)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ int32_t port = 0;
+ rv = url->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (port <= 0) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ nsresult rv = server->GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only set this for ssl newsgroups as for non-ssl connections, we don't
+ // need to specify the port as it is the default for the protocol and
+ // password manager "blanks" those out.
+ if (socketType == nsMsgSocketType::SSL) {
+ rv = NS_MutateURI(url)
+ .SetPort(nsINntpUrl::DEFAULT_NNTPS_PORT)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ nsCString rawResult;
+ if (ref) {
+ rv = NS_MutateURI(url).SetRef(nsDependentCString(ref)).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = url->GetSpec(rawResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // If the url doesn't have a path, make sure we don't get a '/' on the end
+ // as that will confuse searching in password manager.
+ nsCString spec;
+ rv = url->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!spec.IsEmpty() && spec[spec.Length() - 1] == '/')
+ rawResult = StringHead(spec, spec.Length() - 1);
+ else
+ rawResult = spec;
+ }
+ result = NS_ConvertASCIItoUTF16(rawResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetAuthenticationCredentials(nsIMsgWindow* aMsgWindow,
+ bool mayPrompt, bool mustPrompt,
+ bool* validCredentials) {
+ // Not strictly necessary, but it would help consumers to realize that this is
+ // a rather nonsensical combination.
+ NS_ENSURE_FALSE(mustPrompt && !mayPrompt, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_ARG_POINTER(validCredentials);
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsresult rv;
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(NEWS_MSGS_URL, getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString signonUrl;
+ rv = CreateNewsgroupUrlForSignon(nullptr, signonUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we don't have a username or password, try to load it via the login mgr.
+ // Do this even if mustPrompt is true, to prefill the dialog.
+ if (mGroupUsername.IsEmpty() || mGroupPassword.IsEmpty()) {
+ nsCOMPtr<nsILoginManager> loginMgr =
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(signonUrl, EmptyString(), signonUrl, logins);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (logins.Length() > 0) {
+ nsString uniUsername, uniPassword;
+ logins[0]->GetUsername(uniUsername);
+ logins[0]->GetPassword(uniPassword);
+ mGroupUsername = NS_LossyConvertUTF16toASCII(uniUsername);
+ mGroupPassword = NS_LossyConvertUTF16toASCII(uniPassword);
+
+ *validCredentials = true;
+ }
+ }
+
+ // Show the prompt if we need to
+ if (mustPrompt ||
+ (mayPrompt && (mGroupUsername.IsEmpty() || mGroupPassword.IsEmpty()))) {
+ nsCOMPtr<nsIAuthPrompt> authPrompt =
+ do_GetService("@mozilla.org/messenger/msgAuthPrompt;1");
+ if (!authPrompt) {
+ nsCOMPtr<nsIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ if (wwatch) wwatch->GetNewAuthPrompter(0, getter_AddRefs(authPrompt));
+ if (!authPrompt) return NS_ERROR_FAILURE;
+ }
+
+ if (authPrompt) {
+ // Format the prompt text strings
+ nsString promptTitle, promptText;
+ bundle->GetStringFromName("enterUserPassTitle", promptTitle);
+
+ nsString serverName;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ server->GetPrettyName(serverName);
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool singleSignon = true;
+ nntpServer->GetSingleSignon(&singleSignon);
+
+ if (singleSignon) {
+ AutoTArray<nsString, 1> params = {serverName};
+ bundle->FormatStringFromName("enterUserPassServer", params, promptText);
+ } else {
+ AutoTArray<nsString, 2> params = {mName, serverName};
+ bundle->FormatStringFromName("enterUserPassGroup", params, promptText);
+ }
+
+ // Fill the signon url for the dialog
+ nsString signonURL;
+ rv = CreateNewsgroupUrlForSignon(nullptr, signonURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Prefill saved username/password
+ char16_t* uniGroupUsername =
+ ToNewUnicode(NS_ConvertASCIItoUTF16(mGroupUsername));
+ char16_t* uniGroupPassword =
+ ToNewUnicode(NS_ConvertASCIItoUTF16(mGroupPassword));
+
+ // Prompt for the dialog
+ rv = authPrompt->PromptUsernameAndPassword(
+ promptTitle.get(), promptText.get(), signonURL.get(),
+ nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY, &uniGroupUsername,
+ &uniGroupPassword, validCredentials);
+
+ nsAutoString uniPasswordAdopted, uniUsernameAdopted;
+ uniPasswordAdopted.Adopt(uniGroupPassword);
+ uniUsernameAdopted.Adopt(uniGroupUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only use the username/password if the user didn't cancel.
+ if (*validCredentials) {
+ SetGroupUsername(NS_LossyConvertUTF16toASCII(uniUsernameAdopted));
+ SetGroupPassword(NS_LossyConvertUTF16toASCII(uniPasswordAdopted));
+ } else {
+ mGroupUsername.Truncate();
+ mGroupPassword.Truncate();
+ }
+ }
+ }
+
+ *validCredentials = !(mGroupUsername.IsEmpty() || mGroupPassword.IsEmpty());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::ForgetAuthenticationCredentials() {
+ nsString signonUrl;
+ nsresult rv = CreateNewsgroupUrlForSignon(nullptr, signonUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILoginManager> loginMgr =
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(signonUrl, EmptyString(), signonUrl, logins);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // There should only be one-login stored for this url, however just in case
+ // there isn't.
+ for (uint32_t i = 0; i < logins.Length(); ++i)
+ loginMgr->RemoveLogin(logins[i]);
+
+ // Clear out the saved passwords for anyone else who tries to call.
+ mGroupUsername.Truncate();
+ mGroupPassword.Truncate();
+
+ return NS_OK;
+}
+
+// change order of subfolders (newsgroups)
+NS_IMETHODIMP nsMsgNewsFolder::ReorderGroup(nsIMsgFolder* aNewsgroupToMove,
+ nsIMsgFolder* aRefNewsgroup) {
+ // if folders are identical do nothing
+ if (aNewsgroupToMove == aRefNewsgroup) return NS_OK;
+
+ nsresult rv = NS_OK;
+
+ // get index for aNewsgroupToMove
+ int32_t indexNewsgroupToMove = mSubFolders.IndexOf(aNewsgroupToMove);
+ if (indexNewsgroupToMove == -1)
+ // aNewsgroupToMove is no subfolder of this folder
+ return NS_ERROR_INVALID_ARG;
+
+ // get index for aRefNewsgroup
+ int32_t indexRefNewsgroup = mSubFolders.IndexOf(aRefNewsgroup);
+ if (indexRefNewsgroup == -1)
+ // aRefNewsgroup is no subfolder of this folder
+ return NS_ERROR_INVALID_ARG;
+
+ // Move NewsgroupToMove to new index and set new sort order.
+
+ nsCOMPtr<nsIMsgFolder> newsgroup = mSubFolders[indexNewsgroupToMove];
+
+ mSubFolders.RemoveObjectAt(indexNewsgroupToMove);
+ mSubFolders.InsertObjectAt(newsgroup, indexRefNewsgroup);
+
+ for (uint32_t i = 0; i < mSubFolders.Length(); i++) {
+ mSubFolders[i]->SetSortOrder(kNewsSortOffset + i);
+ nsAutoString name;
+ mSubFolders[i]->GetName(name);
+ NotifyFolderRemoved(mSubFolders[i]);
+ NotifyFolderAdded(mSubFolders[i]);
+ }
+
+ // write changes back to file
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->SetNewsrcHasChanged(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->WriteNewsrcFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
+
+nsresult nsMsgNewsFolder::CreateBaseMessageURI(const nsACString& aURI) {
+ return nsCreateNewsBaseMessageURI(aURI, mBaseMessageURI);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetCharset(nsACString& charset) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsINntpIncomingServer> nserver(do_QueryInterface(server));
+ NS_ENSURE_TRUE(nserver, NS_ERROR_NULL_POINTER);
+ return nserver->GetCharset(charset);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetNewsrcLine(nsACString& newsrcLine) {
+ nsresult rv;
+ nsString newsgroupNameUtf16;
+ rv = GetName(newsgroupNameUtf16);
+ if (NS_FAILED(rv)) return rv;
+ NS_ConvertUTF16toUTF8 newsgroupName(newsgroupNameUtf16);
+
+ newsrcLine = newsgroupName;
+ newsrcLine.Append(':');
+
+ if (mReadSet) {
+ nsCString setStr;
+ mReadSet->Output(getter_Copies(setStr));
+ if (NS_SUCCEEDED(rv)) {
+ newsrcLine.Append(' ');
+ newsrcLine.Append(setStr);
+ newsrcLine.AppendLiteral(MSG_LINEBREAK);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetReadSetFromStr(const nsACString& newsrcLine) {
+ mReadSet = nsMsgKeySet::Create(PromiseFlatCString(newsrcLine).get());
+ NS_ENSURE_TRUE(mReadSet, NS_ERROR_OUT_OF_MEMORY);
+
+ // Now that mReadSet is recreated, make sure it's stored in the db as well.
+ nsCOMPtr<nsINewsDatabase> db = do_QueryInterface(mDatabase);
+ if (db) // it's ok not to have a db here.
+ db->SetReadSet(mReadSet);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetUnsubscribedNewsgroupLines(
+ nsACString& aUnsubscribedNewsgroupLines) {
+ aUnsubscribedNewsgroupLines = mUnsubscribedNewsgroupLines;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetOptionLines(nsACString& optionLines) {
+ optionLines = mOptionLines;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::OnReadChanged(nsIDBChangeListener* aInstigator) {
+ return SetNewsrcHasChanged(true);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetUnicodeName(nsAString& aName) { return GetName(aName); }
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetRawName(nsACString& aRawName) {
+ nsresult rv;
+ if (mRawName.IsEmpty()) {
+ nsString name;
+ rv = GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // convert to the server-side encoding
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString dataCharset;
+ rv = nntpServer->GetCharset(dataCharset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsMsgI18NConvertFromUnicode(dataCharset, name, mRawName);
+
+ if (NS_FAILED(rv)) LossyCopyUTF16toASCII(name, mRawName);
+ }
+ aRawName = mRawName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetNntpServer(nsINntpIncomingServer** result) {
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer = do_QueryInterface(server, &rv);
+ if (NS_FAILED(rv)) return rv;
+ nntpServer.forget(result);
+ return NS_OK;
+}
+
+// this gets called after the message actually gets cancelled
+// it removes the cancelled message from the db
+NS_IMETHODIMP nsMsgNewsFolder::RemoveMessage(nsMsgKey key) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv,
+ rv); // if GetDatabase succeeds, mDatabase will be non-null
+
+ // Notify listeners of a delete for a single message
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyMsgsDeleted({msgHdr.get()});
+ }
+ return mDatabase->DeleteMessage(key, nullptr, false);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::RemoveMessages(
+ const nsTArray<nsMsgKey>& aMsgKeys) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv,
+ rv); // if GetDatabase succeeds, mDatabase will be non-null
+
+ // Notify listeners of a multiple message delete
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+
+ if (notifier) {
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrs;
+ rv = MsgGetHeadersFromKeys(mDatabase, aMsgKeys, msgHdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyMsgsDeleted(msgHdrs);
+ }
+
+ return mDatabase->DeleteMessages(aMsgKeys, nullptr);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CancelComplete() {
+ NotifyFolderEvent(kDeleteOrMoveMsgCompleted);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CancelFailed() {
+ NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetSaveArticleOffline(bool* aBool) {
+ NS_ENSURE_ARG(aBool);
+ *aBool = m_downloadMessageForOfflineUse;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetSaveArticleOffline(bool aBool) {
+ m_downloadMessageForOfflineUse = aBool;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) {
+ nsTArray<nsMsgKey> srcKeyArray;
+ SetSaveArticleOffline(true);
+ nsresult rv = NS_OK;
+
+ // build up message keys.
+ if (mDatabase) {
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ rv = mDatabase->EnumerateMessages(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)) {
+ bool shouldStoreMsgOffline = false;
+ nsMsgKey msgKey;
+ header->GetMessageKey(&msgKey);
+ MsgFitsDownloadCriteria(msgKey, &shouldStoreMsgOffline);
+ if (shouldStoreMsgOffline) srcKeyArray.AppendElement(msgKey);
+ }
+ }
+ }
+ }
+ RefPtr<DownloadNewsArticlesToOfflineStore> downloadState =
+ new DownloadNewsArticlesToOfflineStore(msgWindow, mDatabase, this);
+ rv = downloadState->DownloadArticles(msgWindow, this, &srcKeyArray);
+ (void)RefreshSizeOnDisk();
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgWindow* window) {
+ nsresult rv;
+ SetSaveArticleOffline(
+ true); // ### TODO need to clear this when we've finished
+ // build up message keys.
+ nsTArray<nsMsgKey> srcKeyArray(messages.Length());
+ for (nsIMsgDBHdr* hdr : messages) {
+ nsMsgKey key;
+ rv = hdr->GetMessageKey(&key);
+ if (NS_SUCCEEDED(rv)) srcKeyArray.AppendElement(key);
+ }
+ RefPtr<DownloadNewsArticlesToOfflineStore> downloadState =
+ new DownloadNewsArticlesToOfflineStore(window, mDatabase, this);
+
+ rv = downloadState->DownloadArticles(window, this, &srcKeyArray);
+ (void)RefreshSizeOnDisk();
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) {
+ nsMsgKey key;
+ hdr->GetMessageKey(&key);
+
+ uint64_t offset = 0;
+ uint32_t size = 0;
+ nsCOMPtr<nsIInputStream> rawStream;
+ nsresult rv =
+ GetOfflineFileStream(key, &offset, &size, getter_AddRefs(rawStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozilla::SlicedInputStream> slicedStream =
+ new mozilla::SlicedInputStream(rawStream.forget(), offset,
+ uint64_t(size));
+ slicedStream.forget(stream);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyDownloadBegin(nsMsgKey key) {
+ if (!m_downloadMessageForOfflineUse) {
+ return NS_OK;
+ }
+ nsresult rv = GetMessageHeader(key, getter_AddRefs(m_offlineHeader));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return StartNewOfflineMessage(); // Sets up m_tempMessageStream et al.
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyDownloadedLine(nsACString const& line) {
+ nsresult rv = NS_OK;
+ if (m_tempMessageStream) {
+ m_numOfflineMsgLines++;
+ uint32_t count = 0;
+ rv = m_tempMessageStream->Write(line.BeginReading(), line.Length(), &count);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += count;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyDownloadEnd(nsresult status) {
+ if (m_tempMessageStream) {
+ return EndNewOfflineMessage(status);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyFinishedDownloadinghdrs() {
+ bool wasCached = !!mDatabase;
+ ChangeNumPendingTotalMessages(-mNumPendingTotalMessages);
+ ChangeNumPendingUnread(-mNumPendingUnreadMessages);
+ bool filtersRun;
+ // run the bayesian spam filters, if enabled.
+ CallFilterPlugins(nullptr, &filtersRun);
+
+ // If the DB was not open before, close our reference to it now.
+ if (!wasCached && mDatabase) {
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ mDatabase->RemoveListener(this);
+ // This also clears all of the cached headers that may have been added while
+ // we were downloading messages (and those clearing refcount cycles in the
+ // database).
+ mDatabase->ClearCachedHdrs();
+ mDatabase = nullptr;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+ rv = GetDatabase();
+ if (mDatabase) ApplyRetentionSettings();
+ (void)RefreshSizeOnDisk();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::ApplyRetentionSettings() {
+ return nsMsgDBFolder::ApplyRetentionSettings(false);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetMessageIdForKey(nsMsgKey key,
+ nsACString& result) {
+ nsresult rv = GetDatabase();
+ if (!mDatabase) return rv;
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString id;
+ rv = hdr->GetMessageId(getter_Copies(id));
+ result.Assign(id);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetSortOrder(int32_t order) {
+ int32_t oldOrder = mSortOrder;
+ mSortOrder = order;
+
+ NotifyIntPropertyChanged(kSortOrder, oldOrder, order);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetSortOrder(int32_t* order) {
+ NS_ENSURE_ARG_POINTER(order);
+ *order = mSortOrder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::Shutdown(bool shutdownChildren) {
+ if (mFilterList) {
+ // close the filter log stream
+ nsresult rv = mFilterList->SetLogStream(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mFilterList = nullptr;
+ }
+
+ mInitialized = false;
+ if (mReadSet) {
+ // the nsINewsDatabase holds a weak ref to the readset,
+ // and we outlive the db, so it's safe to delete it here.
+ nsCOMPtr<nsINewsDatabase> db = do_QueryInterface(mDatabase);
+ if (db) db->SetReadSet(nullptr);
+ mReadSet = nullptr;
+ }
+ return nsMsgDBFolder::Shutdown(shutdownChildren);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::SetFilterList(nsIMsgFilterList* aFilterList) {
+ if (mIsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->SetFilterList(aFilterList);
+ }
+
+ mFilterList = aFilterList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ if (mIsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetFilterList(aMsgWindow, aResult);
+ }
+
+ if (!mFilterList) {
+ nsCOMPtr<nsIFile> thisFolder;
+ nsresult rv = GetFilePath(getter_AddRefs(thisFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> filterFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ ;
+ rv = filterFile->InitWithFile(thisFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // in 4.x, the news filter file was
+ // C:\Program
+ // Files\Netscape\Users\meer\News\host-news.mcom.com\mcom.test.dat where the
+ // summary file was C:\Program
+ // Files\Netscape\Users\meer\News\host-news.mcom.com\mcom.test.snm we make
+ // the rules file ".dat" in mozilla, so that migration works.
+
+ // NOTE:
+ // we don't we need to call NS_MsgHashIfNecessary()
+ // it's already been hashed, if necessary
+ nsCString filterFileName;
+ rv = filterFile->GetNativeLeafName(filterFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ filterFileName.AppendLiteral(".dat");
+
+ rv = filterFile->SetNativeLeafName(filterFileName);
+ 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(filterFile, this, aMsgWindow,
+ getter_AddRefs(mFilterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aResult = mFilterList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ // We don't support pluggable filter list types for news.
+ return GetFilterList(aMsgWindow, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::SetEditableFilterList(nsIMsgFilterList* aFilterList) {
+ return SetFilterList(aFilterList);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetIncomingServerType(nsACString& serverType) {
+ serverType.AssignLiteral("nntp");
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNewsFolder.h b/comm/mailnews/news/src/nsNewsFolder.h
new file mode 100644
index 0000000000..e18a768375
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsFolder.h
@@ -0,0 +1,144 @@
+/* -*- 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/. */
+
+/**
+ Interface for representing News folders.
+*/
+
+#ifndef nsMsgNewsFolder_h__
+#define nsMsgNewsFolder_h__
+
+#include "mozilla/Attributes.h"
+#include "nsMsgDBFolder.h"
+#include "nsIFile.h"
+#include "nsNewsUtils.h"
+#include "nsMsgKeySet.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgFilterList.h"
+
+class nsMsgNewsFolder : public nsMsgDBFolder, public nsIMsgNewsFolder {
+ public:
+ nsMsgNewsFolder(void);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMSGNEWSFOLDER
+
+ // nsIMsgFolder methods:
+ NS_IMETHOD GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) override;
+
+ NS_IMETHOD UpdateFolder(nsIMsgWindow* aWindow) override;
+
+ NS_IMETHOD CreateSubfolder(const nsAString& folderName,
+ nsIMsgWindow* msgWindow) override;
+
+ NS_IMETHOD DeleteStorage() override;
+ NS_IMETHOD Rename(const nsAString& newName, nsIMsgWindow* msgWindow) override;
+
+ NS_IMETHOD GetAbbreviatedName(nsAString& aAbbreviatedName) override;
+
+ NS_IMETHOD GetFolderURL(nsACString& url) override;
+
+ NS_IMETHOD GetExpungedBytesCount(int64_t* count);
+ NS_IMETHOD GetDeletable(bool* deletable) override;
+
+ NS_IMETHOD RefreshSizeOnDisk();
+
+ NS_IMETHOD GetSizeOnDisk(int64_t* size) override;
+
+ NS_IMETHOD GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) override;
+
+ NS_IMETHOD DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* msgWindow, bool deleteStorage,
+ bool isMove, nsIMsgCopyServiceListener* listener,
+ bool allowUndo) override;
+ NS_IMETHOD GetNewMessages(nsIMsgWindow* aWindow,
+ nsIUrlListener* aListener) override;
+
+ NS_IMETHOD GetCanSubscribe(bool* aResult) override;
+ NS_IMETHOD GetCanFileMessages(bool* aResult) override;
+ NS_IMETHOD GetCanCreateSubfolders(bool* aResult) override;
+ NS_IMETHOD GetCanRename(bool* aResult) override;
+ NS_IMETHOD GetCanCompact(bool* aResult) override;
+ NS_IMETHOD OnReadChanged(nsIDBChangeListener* aInstigator) override;
+
+ NS_IMETHOD DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* window) override;
+ NS_IMETHOD GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) override;
+ NS_IMETHOD Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) override;
+ NS_IMETHOD GetSortOrder(int32_t* order) override;
+ NS_IMETHOD SetSortOrder(int32_t order) override;
+
+ NS_IMETHOD Shutdown(bool shutdownChildren) override;
+
+ NS_IMETHOD GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aFilterList) override;
+ NS_IMETHOD GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aFilterList) override;
+ NS_IMETHOD SetFilterList(nsIMsgFilterList* aFilterList) override;
+ NS_IMETHOD SetEditableFilterList(nsIMsgFilterList* aFilterList) override;
+ NS_IMETHOD ApplyRetentionSettings() override;
+ NS_IMETHOD GetIncomingServerType(nsACString& serverType) override;
+
+ protected:
+ virtual ~nsMsgNewsFolder();
+ // helper routine to parse the URI and update member variables
+ nsresult AbbreviatePrettyName(nsAString& prettyName, int32_t fullwords);
+ nsresult ParseFolder(nsIFile* path);
+ nsresult CreateSubFolders(nsIFile* path);
+ nsresult AddDirectorySeparator(nsIFile* path);
+ nsresult GetDatabase() override;
+ virtual nsresult CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) override;
+
+ nsresult LoadNewsrcFileAndCreateNewsgroups();
+ int32_t RememberLine(const nsACString& line);
+ nsresult RememberUnsubscribedGroup(const nsACString& newsgroup,
+ const nsACString& setStr);
+ nsresult ForgetLine(void);
+ nsresult GetNewsMessages(nsIMsgWindow* aMsgWindow, bool getOld,
+ nsIUrlListener* aListener);
+
+ int32_t HandleNewsrcLine(const char* line, uint32_t line_size);
+ virtual nsresult CreateBaseMessageURI(const nsACString& aURI) override;
+
+ protected:
+ int64_t mExpungedBytes;
+ bool mGettingNews;
+ bool mInitialized;
+ bool m_downloadMessageForOfflineUse;
+
+ nsCString mOptionLines;
+ nsCString mUnsubscribedNewsgroupLines;
+ RefPtr<nsMsgKeySet> mReadSet;
+
+ nsCOMPtr<nsIFile> mNewsrcFilePath;
+
+ // used for auth news
+ nsCString mGroupUsername;
+ nsCString mGroupPassword;
+
+ // the name of the newsgroup.
+ nsCString mRawName;
+ int32_t mSortOrder;
+
+ private:
+ /**
+ * Constructs a signon url for use in login manager.
+ *
+ * @param ref The URI ref (should be null unless working with legacy).
+ * @param result The result of the string
+ */
+ nsresult CreateNewsgroupUrlForSignon(const char* ref, nsAString& result);
+ nsCOMPtr<nsIMsgFilterList> mFilterList;
+};
+
+#endif // nsMsgNewsFolder_h__
diff --git a/comm/mailnews/news/src/nsNewsUtils.cpp b/comm/mailnews/news/src/nsNewsUtils.cpp
new file mode 100644
index 0000000000..b9afed9c95
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsUtils.cpp
@@ -0,0 +1,57 @@
+/* -*- 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 "nntpCore.h"
+#include "nsNewsUtils.h"
+#include "nsMsgUtils.h"
+
+/* parses NewsMessageURI */
+nsresult nsParseNewsMessageURI(const nsACString& uri, nsCString& group,
+ nsMsgKey* key) {
+ NS_ENSURE_ARG_POINTER(key);
+
+ const nsPromiseFlatCString& uriStr = PromiseFlatCString(uri);
+ int32_t keySeparator = uriStr.FindChar('#');
+ if (keySeparator != -1) {
+ int32_t keyEndSeparator = MsgFindCharInSet(uriStr, "?&", keySeparator);
+
+ // Grab between the last '/' and the '#' for the key
+ group = StringHead(uriStr, keySeparator);
+ int32_t groupSeparator = group.RFind("/");
+ if (groupSeparator == -1) return NS_ERROR_FAILURE;
+
+ // Our string APIs don't let us unescape into the same buffer from earlier,
+ // so escape into a temporary
+ nsAutoCString unescapedGroup;
+ MsgUnescapeString(Substring(group, groupSeparator + 1), 0, unescapedGroup);
+ group = unescapedGroup;
+
+ nsAutoCString keyStr;
+ if (keyEndSeparator != -1)
+ keyStr = Substring(uriStr, keySeparator + 1,
+ keyEndSeparator - (keySeparator + 1));
+ else
+ keyStr = Substring(uriStr, keySeparator + 1);
+ nsresult errorCode;
+ *key = keyStr.ToInteger(&errorCode);
+
+ return errorCode;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsCreateNewsBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI) {
+ nsAutoCString tailURI(baseURI);
+
+ // chop off news:/
+ if (tailURI.Find(kNewsRootURI) == 0) tailURI.Cut(0, PL_strlen(kNewsRootURI));
+
+ baseMessageURI = kNewsMessageRootURI;
+ baseMessageURI += tailURI;
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNewsUtils.h b/comm/mailnews/news/src/nsNewsUtils.h
new file mode 100644
index 0000000000..601858de3f
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsUtils.h
@@ -0,0 +1,30 @@
+/* -*- 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 NS_NEWSUTILS_H
+#define NS_NEWSUTILS_H
+
+#include "nsString.h"
+#include "MailNewsTypes2.h"
+
+#define kNewsRootURI "news:/"
+#define kNntpRootURI "nntp:/"
+#define kNewsMessageRootURI "news-message:/"
+#define kNewsURIGroupQuery "?group="
+#define kNewsURIKeyQuery "&key="
+
+#define kNewsRootURILen 6
+#define kNntpRootURILen 6
+#define kNewsMessageRootURILen 14
+#define kNewsURIGroupQueryLen 7
+#define kNewsURIKeyQueryLen 5
+
+extern nsresult nsParseNewsMessageURI(const nsACString& uri, nsCString& group,
+ nsMsgKey* key);
+
+extern nsresult nsCreateNewsBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI);
+
+#endif // NS_NEWSUTILS_H
diff --git a/comm/mailnews/news/src/nsNntpUrl.cpp b/comm/mailnews/news/src/nsNntpUrl.cpp
new file mode 100644
index 0000000000..ab7dcc08ab
--- /dev/null
+++ b/comm/mailnews/news/src/nsNntpUrl.cpp
@@ -0,0 +1,476 @@
+/* -*- 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 "nsNntpUrl.h"
+
+#include "nsString.h"
+#include "nsNewsUtils.h"
+#include "nsMsgUtils.h"
+
+#include "nntpCore.h"
+
+#include "nsCOMPtr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsINntpService.h"
+#include "nsIMsgMessageService.h"
+#include "nsIMsgAccountManager.h"
+#include "nsServiceManagerUtils.h"
+
+nsNntpUrl::nsNntpUrl() {
+ m_newsgroupPost = nullptr;
+ m_newsAction = nsINntpUrl::ActionUnknown;
+ m_addDummyEnvelope = false;
+ m_canonicalLineEnding = false;
+ m_filePath = nullptr;
+ m_getOldMessages = false;
+ m_key = nsMsgKey_None;
+ mAutodetectCharset = false;
+}
+
+nsNntpUrl::~nsNntpUrl() {}
+
+NS_IMPL_ADDREF_INHERITED(nsNntpUrl, nsMsgMailNewsUrl)
+NS_IMPL_RELEASE_INHERITED(nsNntpUrl, nsMsgMailNewsUrl)
+
+NS_INTERFACE_MAP_BEGIN(nsNntpUrl)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINntpUrl)
+ NS_INTERFACE_MAP_ENTRY(nsINntpUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgMessageUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgI18NUrl)
+NS_INTERFACE_MAP_END_INHERITING(nsMsgMailNewsUrl)
+
+////////////////////////////////////////////////////////////////////////////////
+// Begin nsINntpUrl specific support
+////////////////////////////////////////////////////////////////////////////////
+
+/* News URI parsing explanation:
+ * We support 3 different news URI schemes, essentially boiling down to 8
+ * different formats:
+ * news://host/group
+ * news://host/message
+ * news://host/
+ * news:group
+ * news:message
+ * nntp://host/group
+ * nntp://host/group/key
+ * news-message://host/group#key
+ *
+ * In addition, we use queries on the news URIs with authorities for internal
+ * NNTP processing. The most important one is ?group=group&key=key, for cache
+ * canonicalization.
+ */
+
+nsresult nsNntpUrl::SetSpecInternal(const nsACString& aSpec) {
+ // For [s]news: URIs, we need to munge the spec if it is no authority, because
+ // the URI parser guesses the wrong thing otherwise
+ nsCString parseSpec(aSpec);
+ int32_t colon = parseSpec.Find(":");
+
+ // Our smallest scheme is 4 characters long, so colon must be at least 4
+ if (colon < 4 || colon + 1 == (int32_t)parseSpec.Length())
+ return NS_ERROR_MALFORMED_URI;
+
+ if (Substring(parseSpec, colon - 4, 4).EqualsLiteral("news") &&
+ parseSpec[colon + 1] != '/') {
+ // To make this parse properly, we add in three slashes, which convinces the
+ // parser that the authority component is empty.
+ parseSpec = Substring(aSpec, 0, colon + 1);
+ parseSpec.AppendLiteral("///");
+ parseSpec += Substring(aSpec, colon + 1);
+ }
+
+ nsresult rv = nsMsgMailNewsUrl::SetSpecInternal(parseSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString scheme;
+ rv = GetScheme(scheme);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (scheme.EqualsLiteral("news") || scheme.EqualsLiteral("snews"))
+ rv = ParseNewsURL();
+ else if (scheme.EqualsLiteral("nntp") || scheme.EqualsLiteral("nntps"))
+ rv = ParseNntpURL();
+ else if (scheme.EqualsLiteral("news-message")) {
+ nsAutoCString spec;
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsParseNewsMessageURI(spec, m_group, &m_key);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+ } else
+ return NS_ERROR_MALFORMED_URI;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = DetermineNewsAction();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+nsresult nsNntpUrl::ParseNewsURL() {
+ // The path here is the group/msgid portion
+ nsAutoCString path;
+ nsresult rv = GetFilePath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Drop the potential beginning from the path
+ if (path.Length() && path[0] == '/') path = Substring(path, 1);
+
+ // The presence of an `@' is a sign we have a msgid
+ if (path.Find("@") != -1 || path.Find("%40") != -1) {
+ MsgUnescapeString(path, 0, m_messageID);
+
+ // Set group, key for ?group=foo&key=123 uris
+ nsAutoCString spec;
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t groupPos = spec.Find(kNewsURIGroupQuery); // find ?group=
+ int32_t keyPos = spec.Find(kNewsURIKeyQuery); // find &key=
+ if (groupPos != kNotFound && keyPos != kNotFound) {
+ // get group name and message key
+ m_group = Substring(spec, groupPos + kNewsURIGroupQueryLen,
+ keyPos - groupPos - kNewsURIGroupQueryLen);
+ nsCString keyStr(Substring(spec, keyPos + kNewsURIKeyQueryLen));
+ m_key = keyStr.ToInteger(&rv, 10);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+ }
+ } else
+ MsgUnescapeString(path, 0, m_group);
+
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::ParseNntpURL() {
+ nsAutoCString path;
+ nsresult rv = GetFilePath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (path.Length() > 0 && path[0] == '/') path = Substring(path, 1);
+
+ if (path.IsEmpty()) return NS_ERROR_MALFORMED_URI;
+
+ int32_t slash = path.FindChar('/');
+ if (slash == -1) {
+ m_group = path;
+ m_key = nsMsgKey_None;
+ } else {
+ m_group = Substring(path, 0, slash);
+ nsAutoCString keyStr;
+ keyStr = Substring(path, slash + 1);
+ m_key = keyStr.ToInteger(&rv, 10);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+
+ // Keys must be at least one
+ if (m_key == 0) return NS_ERROR_MALFORMED_URI;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::DetermineNewsAction() {
+ nsAutoCString path;
+ nsresult rv = nsMsgMailNewsUrl::GetPathQueryRef(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString query;
+ rv = GetQuery(query);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (query.EqualsLiteral("cancel")) {
+ m_newsAction = nsINntpUrl::ActionCancelArticle;
+ return NS_OK;
+ }
+ if (query.EqualsLiteral("list-ids")) {
+ m_newsAction = nsINntpUrl::ActionListIds;
+ return NS_OK;
+ }
+ if (query.EqualsLiteral("newgroups")) {
+ m_newsAction = nsINntpUrl::ActionListNewGroups;
+ return NS_OK;
+ }
+ if (StringBeginsWith(query, "search"_ns)) {
+ m_newsAction = nsINntpUrl::ActionSearch;
+ return NS_OK;
+ }
+ if (StringBeginsWith(query, "part="_ns) || query.Find("&part=") > 0) {
+ // news://news.mozilla.org:119/3B98D201.3020100%40cs.com?part=1
+ // news://news.mozilla.org:119/b58dme%24aia2%40ripley.netscape.com?header=print&part=1.2&type=image/jpeg&filename=Pole.jpg
+ m_newsAction = nsINntpUrl::ActionFetchPart;
+ return NS_OK;
+ }
+
+ if (!m_messageID.IsEmpty() || m_key != nsMsgKey_None) {
+ m_newsAction = nsINntpUrl::ActionFetchArticle;
+ return NS_OK;
+ }
+
+ if (m_group.Find("*") >= 0) {
+ // If the group is a wildmat, list groups instead of grabbing a group.
+ m_newsAction = nsINntpUrl::ActionListGroups;
+ return NS_OK;
+ }
+ if (!m_group.IsEmpty()) {
+ m_newsAction = nsINntpUrl::ActionGetNewNews;
+ return NS_OK;
+ }
+
+ // At this point, we have a URI that contains neither a query, a group, nor a
+ // message ID. Ergo, we don't know what it is.
+ m_newsAction = nsINntpUrl::ActionUnknown;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetGetOldMessages(bool aGetOldMessages) {
+ m_getOldMessages = aGetOldMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetGetOldMessages(bool* aGetOldMessages) {
+ NS_ENSURE_ARG(aGetOldMessages);
+ *aGetOldMessages = m_getOldMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetNewsAction(nsNewsAction* aNewsAction) {
+ if (aNewsAction) *aNewsAction = m_newsAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetNewsAction(nsNewsAction aNewsAction) {
+ m_newsAction = aNewsAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetGroup(nsACString& group) {
+ group = m_group;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetMessageID(nsACString& messageID) {
+ messageID = m_messageID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetKey(nsMsgKey* key) {
+ NS_ENSURE_ARG_POINTER(key);
+ *key = m_key;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetCharset(nsACString& charset) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsINntpIncomingServer> nserver(do_QueryInterface(server));
+ NS_ENSURE_TRUE(nserver, NS_ERROR_NULL_POINTER);
+ return nserver->GetCharset(charset);
+}
+
+NS_IMETHODIMP nsNntpUrl::GetNormalizedSpec(nsACString& aPrincipalSpec) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetUri(const nsACString& aURI) {
+ mURI = aURI;
+ return NS_OK;
+}
+
+// from nsIMsgMessageUrl
+NS_IMETHODIMP nsNntpUrl::GetUri(nsACString& aURI) {
+ nsresult rv = NS_OK;
+
+ // if we have been given a uri to associate with this url, then use it
+ // otherwise try to reconstruct a URI on the fly....
+ if (mURI.IsEmpty()) {
+ nsAutoCString spec;
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mURI = spec;
+ }
+
+ aURI = mURI;
+ return rv;
+}
+
+NS_IMPL_GETSET(nsNntpUrl, AddDummyEnvelope, bool, m_addDummyEnvelope)
+NS_IMPL_GETSET(nsNntpUrl, CanonicalLineEnding, bool, m_canonicalLineEnding)
+
+NS_IMETHODIMP nsNntpUrl::SetMessageFile(nsIFile* aFile) {
+ m_messageFile = aFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetMessageFile(nsIFile** aFile) {
+ if (aFile) NS_IF_ADDREF(*aFile = m_messageFile);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// End nsINntpUrl specific support
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsNntpUrl::SetMessageToPost(nsINNTPNewsgroupPost* post) {
+ m_newsgroupPost = post;
+ if (post) SetNewsAction(nsINntpUrl::ActionPostArticle);
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::GetMessageToPost(nsINNTPNewsgroupPost** aPost) {
+ NS_ENSURE_ARG_POINTER(aPost);
+ NS_IF_ADDREF(*aPost = m_newsgroupPost);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetMessageHeader(nsIMsgDBHdr** aMsgHdr) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMessageService> msgService =
+ do_GetService("@mozilla.org/messenger/messageservice;1?type=news", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString spec(mOriginalSpec);
+ if (spec.IsEmpty()) {
+ // Handle the case where necko directly runs an internal news:// URL,
+ // one that looks like news://host/message-id?group=mozilla.announce&key=15
+ // Other sorts of URLs -- e.g. news://host/message-id -- will not succeed.
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return msgService->MessageURIToMsgHdr(spec, aMsgHdr);
+}
+
+NS_IMETHODIMP nsNntpUrl::IsUrlType(uint32_t type, bool* isType) {
+ NS_ENSURE_ARG(isType);
+
+ switch (type) {
+ case nsIMsgMailNewsUrl::eDisplay:
+ *isType = (m_newsAction == nsINntpUrl::ActionFetchArticle);
+ break;
+ default:
+ *isType = false;
+ };
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNntpUrl::GetOriginalSpec(nsACString& aSpec) {
+ aSpec = mOriginalSpec;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNntpUrl::SetOriginalSpec(const nsACString& aSpec) {
+ mOriginalSpec = aSpec;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNntpUrl::GetServer(nsIMsgIncomingServer** aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+
+ nsresult rv;
+ nsAutoCString scheme, user, host;
+
+ GetScheme(scheme);
+ GetUsername(user);
+ GetHost(host);
+
+ // No authority -> no server
+ if (host.IsEmpty()) {
+ *aServer = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aServer = nullptr;
+ accountManager->FindServer(user, host, "nntp"_ns, 0, aServer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetFolder(nsIMsgFolder** msgFolder) {
+ NS_ENSURE_ARG_POINTER(msgFolder);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Need a server and a group to get the folder
+ if (!server || m_group.IsEmpty()) {
+ *msgFolder = nullptr;
+ return NS_OK;
+ }
+
+ // Find the group on the server
+ nsCOMPtr<nsINntpIncomingServer> nntpServer = do_QueryInterface(server, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasGroup = false;
+ rv = nntpServer->ContainsNewsgroup(m_group, &hasGroup);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!hasGroup) {
+ *msgFolder = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder;
+ rv = nntpServer->FindGroup(m_group, getter_AddRefs(newsFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return newsFolder->QueryInterface(NS_GET_IID(nsIMsgFolder),
+ (void**)msgFolder);
+}
+
+NS_IMETHODIMP nsNntpUrl::GetAutodetectCharset(bool* aAutodetectCharset) {
+ *aAutodetectCharset = mAutodetectCharset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetAutodetectCharset(bool aAutodetectCharset) {
+ mAutodetectCharset = aAutodetectCharset;
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::Clone(nsIURI** _retval) {
+ nsresult rv;
+ rv = nsMsgMailNewsUrl::Clone(_retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMessageUrl> newsurl = do_QueryInterface(*_retval, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return newsurl->SetUri(mURI);
+}
+
+nsresult nsNntpUrl::NewURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> nntpUri =
+ do_CreateInstance("@mozilla.org/messenger/nntpurl;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aBaseURI) {
+ nsAutoCString newSpec;
+ aBaseURI->Resolve(aSpec, newSpec);
+ rv = nntpUri->SetSpecInternal(newSpec);
+ // XXX Consider: rv = NS_MutateURI(new
+ // nsNntpUrl::Mutator()).SetSpec(newSpec).Finalize(nntpUri);
+ } else {
+ rv = nntpUri->SetSpecInternal(aSpec);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nntpUri.forget(_retval);
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNntpUrl.h b/comm/mailnews/news/src/nsNntpUrl.h
new file mode 100644
index 0000000000..eab52cb1c5
--- /dev/null
+++ b/comm/mailnews/news/src/nsNntpUrl.h
@@ -0,0 +1,68 @@
+/* -*- 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 nsNntpUrl_h__
+#define nsNntpUrl_h__
+
+#include "nsINntpUrl.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsINNTPNewsgroupPost.h"
+#include "nsIFile.h"
+
+class nsNntpUrl : public nsINntpUrl,
+ public nsMsgMailNewsUrl,
+ public nsIMsgMessageUrl,
+ public nsIMsgI18NUrl {
+ public:
+ NS_DECL_NSINNTPURL
+ NS_DECL_NSIMSGMESSAGEURL
+ NS_DECL_NSIMSGI18NURL
+
+ // nsMsgMailNewsUrl overrides
+ nsresult SetSpecInternal(const nsACString& aSpec) override;
+ nsresult Clone(nsIURI** _retval) override;
+
+ NS_IMETHOD IsUrlType(uint32_t type, bool* isType) override;
+
+ // nsIMsgMailNewsUrl overrides
+ NS_IMETHOD GetServer(nsIMsgIncomingServer** server) override;
+ NS_IMETHOD GetFolder(nsIMsgFolder** msgFolder) override;
+
+ // nsNntpUrl
+ nsNntpUrl();
+ static nsresult NewURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ private:
+ virtual ~nsNntpUrl();
+ nsresult DetermineNewsAction();
+ nsresult ParseNewsURL();
+ nsresult ParseNntpURL();
+
+ nsCOMPtr<nsINNTPNewsgroupPost> m_newsgroupPost;
+ nsNewsAction m_newsAction; // the action this url represents...parse mailbox,
+ // display messages, etc.
+
+ nsCString mURI; // the RDF URI associated with this url.
+ bool mAutodetectCharset; // used by nsIMsgI18NUrl...
+
+ nsCString mOriginalSpec;
+ nsCOMPtr<nsIFile> m_filePath;
+
+ // used by save message to disk
+ nsCOMPtr<nsIFile> m_messageFile;
+
+ bool m_addDummyEnvelope;
+ bool m_canonicalLineEnding;
+ bool m_getOldMessages;
+
+ nsCString m_group;
+ nsCString m_messageID;
+ nsMsgKey m_key;
+};
+
+#endif // nsNntpUrl_h__