From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- comm/mailnews/news/src/NewsAutoCompleteSearch.jsm | 134 ++ comm/mailnews/news/src/NewsDownloader.sys.mjs | 158 ++ comm/mailnews/news/src/NntpChannel.jsm | 402 +++++ comm/mailnews/news/src/NntpClient.jsm | 981 ++++++++++++ comm/mailnews/news/src/NntpIncomingServer.jsm | 624 ++++++++ comm/mailnews/news/src/NntpMessageService.jsm | 272 ++++ comm/mailnews/news/src/NntpNewsGroup.jsm | 420 +++++ comm/mailnews/news/src/NntpProtocolHandler.jsm | 46 + comm/mailnews/news/src/NntpProtocolInfo.jsm | 44 + comm/mailnews/news/src/NntpService.jsm | 250 +++ comm/mailnews/news/src/NntpUtils.jsm | 63 + comm/mailnews/news/src/components.conf | 98 ++ comm/mailnews/news/src/moz.build | 32 + comm/mailnews/news/src/nntpCore.h | 165 ++ .../mailnews/news/src/nsNewsDownloadDialogArgs.cpp | 79 + comm/mailnews/news/src/nsNewsDownloadDialogArgs.h | 29 + comm/mailnews/news/src/nsNewsDownloader.cpp | 507 ++++++ comm/mailnews/news/src/nsNewsDownloader.h | 136 ++ comm/mailnews/news/src/nsNewsFolder.cpp | 1645 ++++++++++++++++++++ comm/mailnews/news/src/nsNewsFolder.h | 144 ++ comm/mailnews/news/src/nsNewsUtils.cpp | 57 + comm/mailnews/news/src/nsNewsUtils.h | 30 + comm/mailnews/news/src/nsNntpUrl.cpp | 476 ++++++ comm/mailnews/news/src/nsNntpUrl.h | 68 + 24 files changed, 6860 insertions(+) create mode 100644 comm/mailnews/news/src/NewsAutoCompleteSearch.jsm create mode 100644 comm/mailnews/news/src/NewsDownloader.sys.mjs create mode 100644 comm/mailnews/news/src/NntpChannel.jsm create mode 100644 comm/mailnews/news/src/NntpClient.jsm create mode 100644 comm/mailnews/news/src/NntpIncomingServer.jsm create mode 100644 comm/mailnews/news/src/NntpMessageService.jsm create mode 100644 comm/mailnews/news/src/NntpNewsGroup.jsm create mode 100644 comm/mailnews/news/src/NntpProtocolHandler.jsm create mode 100644 comm/mailnews/news/src/NntpProtocolInfo.jsm create mode 100644 comm/mailnews/news/src/NntpService.jsm create mode 100644 comm/mailnews/news/src/NntpUtils.jsm create mode 100644 comm/mailnews/news/src/components.conf create mode 100644 comm/mailnews/news/src/moz.build create mode 100644 comm/mailnews/news/src/nntpCore.h create mode 100644 comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp create mode 100644 comm/mailnews/news/src/nsNewsDownloadDialogArgs.h create mode 100644 comm/mailnews/news/src/nsNewsDownloader.cpp create mode 100644 comm/mailnews/news/src/nsNewsDownloader.h create mode 100644 comm/mailnews/news/src/nsNewsFolder.cpp create mode 100644 comm/mailnews/news/src/nsNewsFolder.h create mode 100644 comm/mailnews/news/src/nsNewsUtils.cpp create mode 100644 comm/mailnews/news/src/nsNewsUtils.h create mode 100644 comm/mailnews/news/src/nsNntpUrl.cpp create mode 100644 comm/mailnews/news/src/nsNntpUrl.h (limited to 'comm/mailnews/news/src') 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} + */ + 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://:119/?group=&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 ` 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
` 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 ` 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 ` 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
` 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* 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 nntpService = + do_GetService("@mozilla.org/messenger/nntpservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, false); + nsCOMPtr 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 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 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 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 rootFolder; + + while (serverIndex < numServers) { + nsCOMPtr server(m_allServers[serverIndex]); + serverIndex++; + + nsCOMPtr 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 newsFolder = do_QueryInterface(m_currentFolder); + if (newsFolder) newsFolder->SetSaveArticleOffline(false); + + nsCOMPtr 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 db; + nsCOMPtr downloadSettings; + m_currentFolder->GetMsgDatabase(getter_AddRefs(db)); + nsresult rv = + m_currentFolder->GetDownloadSettings(getter_AddRefs(downloadSettings)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr newsFolder = do_QueryInterface(m_currentFolder); + if (newsFolder) newsFolder->SetSaveArticleOffline(true); + + nsCOMPtr 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 term; + nsCOMPtr 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* 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 m_keysToDownload; + nsCOMPtr m_folder; + nsCOMPtr m_newsDB; + nsCOMPtr 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 m_window; + nsCOMPtr m_statusFeedback; + nsCOMPtr 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 m_headerEnumerator; + nsCOMPtr 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 m_downloaderForGroup; + + nsCOMPtr m_currentFolder; + nsCOMPtr m_window; + nsTArray> m_allServers; + nsCOMPtr m_currentServer; + // Folders still to process for the current server. + nsTArray> m_folderQueue; + nsCOMPtr 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(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 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 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 folder; + rv = GetOrCreateFolder(uri, getter_AddRefs(folder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr newsFolder(do_QueryInterface(folder, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + // Ensure any containing .sdb dir exists. + nsCOMPtr 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>& folders) { + if (!mInitialized) { + // do this first, so we make sure to do it, even on failure. + // see bug #70494 + mInitialized = true; + + nsCOMPtr 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 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 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 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 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 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 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 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 nntpServer; + rv = GetNntpServer(getter_AddRefs(nntpServer)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr folderInfo; + nsCOMPtr 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 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 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 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 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> const& msgHdrs, + nsIMsgWindow* aMsgWindow, bool deleteStorage, + bool isMove, + nsIMsgCopyServiceListener* listener, + bool allowUndo) { + nsresult rv = NS_OK; + NS_ENSURE_ARG_POINTER(aMsgWindow); + + if (!isMove) { + nsCOMPtr 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 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 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 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 server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + return server->PerformExpand(aMsgWindow); + } + + nsCOMPtr nntpService = + do_GetService("@mozilla.org/messenger/nntpservice;1", &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr nntpServer; + rv = GetNntpServer(getter_AddRefs(nntpServer)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr resultUri; + rv = nntpService->GetNewNews(nntpServer, mURI, aGetOld, this, aMsgWindow, + getter_AddRefs(resultUri)); + if (aUrlListener && NS_SUCCEEDED(rv) && resultUri) { + nsCOMPtr 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 fileStream; + rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), mNewsrcFilePath); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 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 server; + rv = GetServer(getter_AddRefs(server)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr nntpServer; + rv = GetNntpServer(getter_AddRefs(nntpServer)); + if (NS_FAILED(rv)) return rv; + + bool singleSignon = true; + rv = nntpServer->GetSingleSignon(&singleSignon); + + nsCOMPtr 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 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 bundleService = + mozilla::components::StringBundle::Service(); + NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED); + + nsresult rv; + nsCOMPtr 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 loginMgr = + do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray> 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 authPrompt = + do_GetService("@mozilla.org/messenger/msgAuthPrompt;1"); + if (!authPrompt) { + nsCOMPtr 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 server; + rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + + server->GetPrettyName(serverName); + + nsCOMPtr nntpServer; + rv = GetNntpServer(getter_AddRefs(nntpServer)); + NS_ENSURE_SUCCESS(rv, rv); + + bool singleSignon = true; + nntpServer->GetSingleSignon(&singleSignon); + + if (singleSignon) { + AutoTArray params = {serverName}; + bundle->FormatStringFromName("enterUserPassServer", params, promptText); + } else { + AutoTArray 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 loginMgr = + do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + nsTArray> 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 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 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 server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr 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 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 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 server; + rv = GetServer(getter_AddRefs(server)); + if (NS_FAILED(rv)) return rv; + + nsCOMPtr 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 notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + if (notifier) { + nsCOMPtr 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& 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 notifier( + do_GetService("@mozilla.org/messenger/msgnotificationservice;1")); + + if (notifier) { + nsTArray> 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 srcKeyArray; + SetSaveArticleOffline(true); + nsresult rv = NS_OK; + + // build up message keys. + if (mDatabase) { + nsCOMPtr enumerator; + rv = mDatabase->EnumerateMessages(getter_AddRefs(enumerator)); + if (NS_SUCCEEDED(rv) && enumerator) { + bool hasMore; + while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) && + hasMore) { + nsCOMPtr 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 downloadState = + new DownloadNewsArticlesToOfflineStore(msgWindow, mDatabase, this); + rv = downloadState->DownloadArticles(msgWindow, this, &srcKeyArray); + (void)RefreshSizeOnDisk(); + return rv; +} + +NS_IMETHODIMP nsMsgNewsFolder::DownloadMessagesForOffline( + nsTArray> const& messages, nsIMsgWindow* window) { + nsresult rv; + SetSaveArticleOffline( + true); // ### TODO need to clear this when we've finished + // build up message keys. + nsTArray srcKeyArray(messages.Length()); + for (nsIMsgDBHdr* hdr : messages) { + nsMsgKey key; + rv = hdr->GetMessageKey(&key); + if (NS_SUCCEEDED(rv)) srcKeyArray.AppendElement(key); + } + RefPtr 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 rawStream; + nsresult rv = + GetOfflineFileStream(key, &offset, &size, getter_AddRefs(rawStream)); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr 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 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 db = do_QueryInterface(mDatabase); + if (db) db->SetReadSet(nullptr); + mReadSet = nullptr; + } + return nsMsgDBFolder::Shutdown(shutdownChildren); +} + +NS_IMETHODIMP +nsMsgNewsFolder::SetFilterList(nsIMsgFilterList* aFilterList) { + if (mIsServer) { + nsCOMPtr 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 server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + return server->GetFilterList(aMsgWindow, aResult); + } + + if (!mFilterList) { + nsCOMPtr thisFolder; + nsresult rv = GetFilePath(getter_AddRefs(thisFolder)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr 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 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>& 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> 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> 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 mReadSet; + + nsCOMPtr 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 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 server; + nsresult rv = GetServer(getter_AddRefs(server)); + NS_ENSURE_SUCCESS(rv, rv); + nsCOMPtr 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 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 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 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 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 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 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 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 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 m_filePath; + + // used by save message to disk + nsCOMPtr m_messageFile; + + bool m_addDummyEnvelope; + bool m_canonicalLineEnding; + bool m_getOldMessages; + + nsCString m_group; + nsCString m_messageID; + nsMsgKey m_key; +}; + +#endif // nsNntpUrl_h__ -- cgit v1.2.3