summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/news/src/NntpIncomingServer.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mailnews/news/src/NntpIncomingServer.jsm')
-rw-r--r--comm/mailnews/news/src/NntpIncomingServer.jsm624
1 files changed, 624 insertions, 0 deletions
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}"
+);