summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/news
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--comm/mailnews/news/content/downloadheaders.js92
-rw-r--r--comm/mailnews/news/content/downloadheaders.xhtml82
-rw-r--r--comm/mailnews/news/moz.build11
-rw-r--r--comm/mailnews/news/public/moz.build18
-rw-r--r--comm/mailnews/news/public/nsIMsgNewsFolder.idl151
-rw-r--r--comm/mailnews/news/public/nsIMsgOfflineNewsState.idl22
-rw-r--r--comm/mailnews/news/public/nsINNTPNewsgroupPost.idl51
-rw-r--r--comm/mailnews/news/public/nsINewsDownloadDialogArgs.idl19
-rw-r--r--comm/mailnews/news/public/nsINntpIncomingServer.idl136
-rw-r--r--comm/mailnews/news/public/nsINntpService.idl50
-rw-r--r--comm/mailnews/news/public/nsINntpUrl.idl102
-rw-r--r--comm/mailnews/news/src/NewsAutoCompleteSearch.jsm134
-rw-r--r--comm/mailnews/news/src/NewsDownloader.sys.mjs158
-rw-r--r--comm/mailnews/news/src/NntpChannel.jsm402
-rw-r--r--comm/mailnews/news/src/NntpClient.jsm981
-rw-r--r--comm/mailnews/news/src/NntpIncomingServer.jsm624
-rw-r--r--comm/mailnews/news/src/NntpMessageService.jsm272
-rw-r--r--comm/mailnews/news/src/NntpNewsGroup.jsm420
-rw-r--r--comm/mailnews/news/src/NntpProtocolHandler.jsm46
-rw-r--r--comm/mailnews/news/src/NntpProtocolInfo.jsm44
-rw-r--r--comm/mailnews/news/src/NntpService.jsm250
-rw-r--r--comm/mailnews/news/src/NntpUtils.jsm63
-rw-r--r--comm/mailnews/news/src/components.conf98
-rw-r--r--comm/mailnews/news/src/moz.build32
-rw-r--r--comm/mailnews/news/src/nntpCore.h165
-rw-r--r--comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp79
-rw-r--r--comm/mailnews/news/src/nsNewsDownloadDialogArgs.h29
-rw-r--r--comm/mailnews/news/src/nsNewsDownloader.cpp507
-rw-r--r--comm/mailnews/news/src/nsNewsDownloader.h136
-rw-r--r--comm/mailnews/news/src/nsNewsFolder.cpp1645
-rw-r--r--comm/mailnews/news/src/nsNewsFolder.h144
-rw-r--r--comm/mailnews/news/src/nsNewsUtils.cpp57
-rw-r--r--comm/mailnews/news/src/nsNewsUtils.h30
-rw-r--r--comm/mailnews/news/src/nsNntpUrl.cpp476
-rw-r--r--comm/mailnews/news/src/nsNntpUrl.h68
-rw-r--r--comm/mailnews/news/test/moz.build6
-rw-r--r--comm/mailnews/news/test/unit/head_server_setup.js244
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post1.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post2.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post3.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post4.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post5.eml50
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post6.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post7.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/auto-add/post8.eml14
-rw-r--r--comm/mailnews/news/test/unit/postings/bug403242.eml25
-rw-r--r--comm/mailnews/news/test/unit/postings/bug670935.eml7
-rw-r--r--comm/mailnews/news/test/unit/postings/post1.eml7
-rw-r--r--comm/mailnews/news/test/unit/postings/post2.eml6
-rw-r--r--comm/mailnews/news/test/unit/postings/post3.eml10
-rw-r--r--comm/mailnews/news/test/unit/test_NntpChannel.js74
-rw-r--r--comm/mailnews/news/test/unit/test_biff.js57
-rw-r--r--comm/mailnews/news/test/unit/test_bug170727.js61
-rw-r--r--comm/mailnews/news/test/unit/test_bug37465.js49
-rw-r--r--comm/mailnews/news/test/unit/test_bug403242.js55
-rw-r--r--comm/mailnews/news/test/unit/test_bug540288.js114
-rw-r--r--comm/mailnews/news/test/unit/test_bug695309.js121
-rw-r--r--comm/mailnews/news/test/unit/test_cancelPasswordDialog.js59
-rw-r--r--comm/mailnews/news/test/unit/test_filter.js179
-rw-r--r--comm/mailnews/news/test/unit/test_getNewsMessage.js101
-rw-r--r--comm/mailnews/news/test/unit/test_internalUris.js305
-rw-r--r--comm/mailnews/news/test/unit/test_newsAutocomplete.js107
-rw-r--r--comm/mailnews/news/test/unit/test_nntpContentLength.js80
-rw-r--r--comm/mailnews/news/test/unit/test_nntpGroupPassword.js99
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPassword.js54
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPassword2.js106
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPassword3.js46
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPasswordFailure.js196
-rw-r--r--comm/mailnews/news/test/unit/test_nntpPost.js37
-rw-r--r--comm/mailnews/news/test/unit/test_nntpProtocols.js55
-rw-r--r--comm/mailnews/news/test/unit/test_nntpProxy.js38
-rw-r--r--comm/mailnews/news/test/unit/test_nntpUrl.js30
-rw-r--r--comm/mailnews/news/test/unit/test_server.js179
-rw-r--r--comm/mailnews/news/test/unit/test_uriParser.js221
-rw-r--r--comm/mailnews/news/test/unit/test_xover.js42
-rw-r--r--comm/mailnews/news/test/unit/xpcshell.ini35
76 files changed, 10547 insertions, 0 deletions
diff --git a/comm/mailnews/news/content/downloadheaders.js b/comm/mailnews/news/content/downloadheaders.js
new file mode 100644
index 0000000000..80d31935b8
--- /dev/null
+++ b/comm/mailnews/news/content/downloadheaders.js
@@ -0,0 +1,92 @@
+/* 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 markreadElement = null;
+var numberElement = null;
+
+var nntpServer = null;
+var args = null;
+
+document.addEventListener("dialogaccept", OkButtonCallback);
+document.addEventListener("dialogcancel", CancelButtonCallback);
+
+function OnLoad() {
+ let newsBundle = document.getElementById("bundle_news");
+
+ if ("arguments" in window && window.arguments[0]) {
+ args = window.arguments[0].QueryInterface(Ci.nsINewsDownloadDialogArgs);
+ /* by default, act like the user hit cancel */
+ args.hitOK = false;
+ /* by default, act like the user did not select download all */
+ args.downloadAll = false;
+
+ nntpServer = MailServices.accounts
+ .getIncomingServer(args.serverKey)
+ .QueryInterface(Ci.nsINntpIncomingServer);
+
+ document.title = newsBundle.getString("downloadHeadersTitlePrefix");
+
+ let infotext = newsBundle.getFormattedString("downloadHeadersInfoText", [
+ args.articleCount,
+ ]);
+ setText("info", infotext);
+ let okButtonText = newsBundle.getString("okButtonText");
+ let okbutton = document.querySelector("dialog").getButton("accept");
+ okbutton.setAttribute("label", okButtonText);
+ okbutton.focus();
+ setText("newsgroupLabel", args.groupName);
+ }
+
+ numberElement = document.getElementById("number");
+ numberElement.value = nntpServer.maxArticles;
+
+ markreadElement = document.getElementById("markread");
+ markreadElement.checked = nntpServer.markOldRead;
+
+ setupDownloadUI(true);
+
+ return true;
+}
+
+function setText(id, value) {
+ let element = document.getElementById(id);
+ if (!element) {
+ return;
+ }
+
+ while (element.lastChild) {
+ element.lastChild.remove();
+ }
+ let textNode = document.createTextNode(value);
+ element.appendChild(textNode);
+}
+
+function OkButtonCallback() {
+ nntpServer.maxArticles = numberElement.value;
+ nntpServer.markOldRead = markreadElement.checked;
+
+ let radio = document.getElementById("all");
+ if (radio) {
+ args.downloadAll = radio.selected;
+ }
+
+ args.hitOK = true;
+}
+
+function CancelButtonCallback() {
+ args.hitOK = false;
+}
+
+function setupDownloadUI(enable) {
+ let checkbox = document.getElementById("markread");
+ let numberFld = document.getElementById("number");
+
+ checkbox.disabled = !enable;
+ numberFld.disabled = !enable;
+ numberFld.select();
+}
diff --git a/comm/mailnews/news/content/downloadheaders.xhtml b/comm/mailnews/news/content/downloadheaders.xhtml
new file mode 100644
index 0000000000..b212d5d288
--- /dev/null
+++ b/comm/mailnews/news/content/downloadheaders.xhtml
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://messenger/skin/messenger.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css" type="text/css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://messenger/locale/downloadheaders.dtd">
+
+<dialog
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ style="max-width: 27em; padding: 10px"
+ buttonpack="center"
+ lightweightthemes="true"
+ onload="OnLoad();"
+>
+ <stringbundle
+ id="bundle_news"
+ src="chrome://messenger/locale/news.properties"
+ />
+ <script src="chrome://messenger/content/downloadheaders.js" />
+ <script src="chrome://messenger/content/dialogShadowDom.js" />
+
+ <label
+ id="newsgroupLabel"
+ control="downloadGroup"
+ class="header"
+ style="width: 25em; max-width: 25em; margin-inline-start: 6px"
+ />
+ <description
+ style="width: 25em; max-width: 25em"
+ id="info"
+ control="downloadGroup"
+ />
+ <separator class="thin" />
+ <vbox class="indent">
+ <radiogroup id="downloadGroup">
+ <radio
+ id="all"
+ label="&all.label;"
+ accesskey="&all.accesskey;"
+ oncommand="setupDownloadUI(false);"
+ value="all"
+ />
+ <separator class="thin" />
+ <hbox align="center">
+ <radio
+ id="some"
+ selected="true"
+ label="&download.label;"
+ accesskey="&download.accesskey;"
+ oncommand="setupDownloadUI(true);"
+ aria-labelledby="some number headers"
+ value="some"
+ />
+ <html:input
+ id="number"
+ type="number"
+ min="1"
+ aria-labelledby="some number headers"
+ />
+ <label
+ id="headers"
+ control="number"
+ value="&headers.label;"
+ accesskey="&headers.accesskey;"
+ />
+ </hbox>
+ </radiogroup>
+
+ <hbox class="indent" align="start">
+ <checkbox
+ id="markread"
+ label="&mark.label;"
+ accesskey="&mark.accesskey;"
+ />
+ </hbox>
+ </vbox>
+</dialog>
diff --git a/comm/mailnews/news/moz.build b/comm/mailnews/news/moz.build
new file mode 100644
index 0000000000..a49689ab64
--- /dev/null
+++ b/comm/mailnews/news/moz.build
@@ -0,0 +1,11 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += [
+ "public",
+ "src",
+]
+
+TEST_DIRS += ["test"]
diff --git a/comm/mailnews/news/public/moz.build b/comm/mailnews/news/public/moz.build
new file mode 100644
index 0000000000..c6c6b27c6f
--- /dev/null
+++ b/comm/mailnews/news/public/moz.build
@@ -0,0 +1,18 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "nsIMsgNewsFolder.idl",
+ "nsIMsgOfflineNewsState.idl",
+ "nsINewsDownloadDialogArgs.idl",
+ "nsINntpIncomingServer.idl",
+ "nsINNTPNewsgroupPost.idl",
+ "nsINntpService.idl",
+ "nsINntpUrl.idl",
+]
+
+XPIDL_MODULE = "msgnews"
+
+EXPORTS += []
diff --git a/comm/mailnews/news/public/nsIMsgNewsFolder.idl b/comm/mailnews/news/public/nsIMsgNewsFolder.idl
new file mode 100644
index 0000000000..683c566595
--- /dev/null
+++ b/comm/mailnews/news/public/nsIMsgNewsFolder.idl
@@ -0,0 +1,151 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIMsgFolder.idl"
+
+%{C++
+#include "nsTArray.h"
+%}
+
+interface nsIMsgWindow;
+interface nsINntpIncomingServer;
+
+[scriptable, uuid(9a12c3a5-9de5-4c57-ace3-d51802b525a9)]
+interface nsIMsgNewsFolder : nsISupports {
+ readonly attribute AString unicodeName;
+ /**|rawName| is an 8-bit string to represent the name of a newsgroup used by
+ * a news server. It's offered for the convenience of callers so that they
+ * don't have to convert |unicodeName| to the server-side name when
+ * communicating with a news server. It's US-ASCII except for some
+ * 'stand-alone' Chinese news servers that use GB2312 for newsgroup names
+ * violating RFC 1036. For those servers, it's GB2312. However, it can be any
+ * other single and multibyte encoding in principle. The encoding of this
+ * string is stored in |nsINntpIncomingServer| because that's a server-wide
+ * property.
+ **/
+ readonly attribute ACString rawName;
+ readonly attribute nsINntpIncomingServer nntpServer;
+ attribute boolean saveArticleOffline;
+
+ /**
+ * @name Authentication methods
+ * NNTP authentication is slightly wonky, due to edge cases that are not seen
+ * in other protocols. Authentication is not necessary; if authentication is
+ * used, it could be configured on a per-group basis or even require only a
+ * username and not a password.
+ *
+ * Since passwords could be per-group, it is necessary to refer to passwords
+ * using the methods on this interface and not nsIMsgIncomingServer. Passwords
+ * for the server as a whole are found via the root folder. If the server is
+ * configured to use single sign-on (the default), asking any group for its
+ * password will result in the server's password, otherwise, each group stores
+ * its password individually.
+ *
+ * Due to this setup, most of the password management functions on
+ * nsIMsgIncomingServer do not correctly work. The only one that would affect
+ * the passwords stored on folders correctly is forgetPassword; using any
+ * other on a news server would result in inconsistent state.
+ *
+ * Before requesting either the username or password for authentication, it is
+ * first necessary to call getAuthenticationCredentials. If the method returns
+ * true, then groupUsername and groupPassword are appropriately set up for
+ * necessary authentication; if not, then authentication must be stopped.
+ */
+ /// @{
+
+ /**
+ * Gets the authentication credentials, returning if the results are valid.
+ *
+ * If mustPrompt is true, then the user will always be asked for the
+ * credentials. Otherwise, if mayPrompt is true, then the user will be asked
+ * for credentials if there are no saved credentials. If mayPrompt is false,
+ * then no prompt will be shown, even if there are no saved credentials.
+ *
+ * If this method returns true, then groupUsername and groupPassword will
+ * contain non-empty results that could be used for authentication. If this
+ * method returns false, then the values of groupUsername and groupPassword
+ * will be cleared if they had previously been set. This could happen if
+ * mustPrompt were true and the user decided to cancel the authentication
+ * prompt.
+ *
+ * Note that this method will be executed synchronously; if an async prompt
+ * is wanted, it is the responsibility of the caller to manage it explicitly
+ * with nsIMsgAsyncPrompter.
+ */
+ bool getAuthenticationCredentials(in nsIMsgWindow aMsgWindow,
+ in bool mayPrompt, in bool mustPrompt);
+
+ /// The username that should be used for this group
+ attribute ACString groupUsername;
+
+ /// The password that should be used for this group
+ attribute ACString groupPassword;
+
+ /// Forgets saved authentication credentials permanently.
+ void forgetAuthenticationCredentials();
+ /// @}
+
+ void reorderGroup(in nsIMsgFolder aNewsgroupToMove, in nsIMsgFolder aRefNewsgroup);
+
+ nsIMsgFolder addNewsgroup(in AUTF8String newsgroupName, in ACString setStr);
+
+ void setReadSetFromStr(in ACString setStr);
+
+ /// returns the server's default charset.
+ readonly attribute ACString charset;
+
+ readonly attribute AUTF8String newsrcLine;
+ readonly attribute ACString optionLines;
+ readonly attribute ACString unsubscribedNewsgroupLines;
+ void SetNewsrcHasChanged(in boolean newsrcHasChanged);
+ void updateSummaryFromNNTPInfo(in long oldest, in long youngest, in long total);
+ void removeMessage(in nsMsgKey key);
+ void removeMessages(in Array<nsMsgKey> keys);
+ void cancelComplete();
+ void cancelFailed();
+
+ ACString getMessageIdForKey(in nsMsgKey key);
+
+ void getNextNMessages(in nsIMsgWindow aMsgWindow);
+
+ /**
+ * Begin loading a message into the folder's offline store.
+ * @param key - the key of the message being loaded (the folder must
+ * already have the message in it's DB)
+ */
+ void notifyDownloadBegin(in nsMsgKey key);
+
+ /**
+ * Feed the next line of a message into the folder, to be invoked multiple
+ * times between notifyDownloadBegin() and notifyDownloadEnd().
+ *
+ * @param line - a single line of data to append to the message, including
+ * end-of-line terminator.
+ */
+ void notifyDownloadedLine(in ACString line);
+
+ /**
+ * Finish loading a message into the folder. If an error occurs, the
+ * folder hears about it via this function, and should abort the message.
+ *
+ * @param status - NS_OK if the operation was completed, an error code
+ * otherwise.
+ */
+ void notifyDownloadEnd(in nsresult status);
+
+ void notifyFinishedDownloadinghdrs();
+
+ /**
+ * Requests that a message be canceled.
+ *
+ * Note that, before sending the news cancel, this method will check to make
+ * sure that the user has proper permission to cancel the message.
+ *
+ * @param aMsgHdr The header of the message to be canceled.
+ * @param aMsgWindow The standard message window object, for error dialogs.
+ */
+ void cancelMessage(in nsIMsgDBHdr aMsgHdr, in nsIMsgWindow aMsgWindow);
+};
diff --git a/comm/mailnews/news/public/nsIMsgOfflineNewsState.idl b/comm/mailnews/news/public/nsIMsgOfflineNewsState.idl
new file mode 100644
index 0000000000..eef1e88db6
--- /dev/null
+++ b/comm/mailnews/news/public/nsIMsgOfflineNewsState.idl
@@ -0,0 +1,22 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * offline news message state. Interface for old MSG_OfflineNewsArtState
+ */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(921AC210-96B5-11d2-B7EB-00805F05FFA5)]
+interface nsIMsgOfflineNewsState : nsISupports {
+
+ /* outputBuffer is actually
+ * a buffer to dump data into, but we normally pass it NET_Socket_Buffer,
+ * which is constant. The implementation should only allocate a new
+ * buffer if *outputBuffer is NULL.
+ */
+ long Process(out string outputBuffer, in long bufferSize);
+ long Interrupt();
+};
diff --git a/comm/mailnews/news/public/nsINNTPNewsgroupPost.idl b/comm/mailnews/news/public/nsINNTPNewsgroupPost.idl
new file mode 100644
index 0000000000..95f3cbb2bf
--- /dev/null
+++ b/comm/mailnews/news/public/nsINNTPNewsgroupPost.idl
@@ -0,0 +1,51 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* This object represents the stream of data which will be sent to an
+ NNTP server. You basically set up all the RFC850 required headers, etc,
+ then pass it to something that reads off the nsIInputStream interface.
+*/
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+
+[scriptable, uuid(9979a2cb-a4e6-45e6-bfeb-b08e704c5a2b)]
+interface nsINNTPNewsgroupPost : nsISupports {
+
+ /* from RFC850 */
+ /* section 2.1 - required headers */
+ attribute string relayVersion;
+ attribute string postingVersion;
+ attribute string from;
+ attribute string date;
+
+ void AddNewsgroup(in string newsgroupName);
+ readonly attribute string newsgroups;
+
+ attribute string subject;
+ attribute string path;
+
+ /* Section 2.2 - optional headers */
+ attribute string replyTo;
+ attribute string sender;
+ attribute string followupTo;
+ attribute string dateReceived;
+ attribute string expires;
+
+ readonly attribute string references;
+
+ attribute string control;
+ attribute string distribution;
+ attribute string organization;
+
+ /* the message itself */
+ attribute string body;
+
+ /* is this a control message? */
+ readonly attribute boolean isControl;
+
+ attribute nsIFile postMessageFile;
+};
diff --git a/comm/mailnews/news/public/nsINewsDownloadDialogArgs.idl b/comm/mailnews/news/public/nsINewsDownloadDialogArgs.idl
new file mode 100644
index 0000000000..8641a56034
--- /dev/null
+++ b/comm/mailnews/news/public/nsINewsDownloadDialogArgs.idl
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(3634327c-392b-4686-adf5-576e6cef9196)]
+interface nsINewsDownloadDialogArgs: nsISupports {
+ attribute AString groupName;
+ attribute long articleCount;
+ attribute string serverKey;
+ attribute boolean hitOK;
+ attribute boolean downloadAll;
+};
+
+%{ C++
+#define DOWNLOAD_HEADERS_URL "chrome://messenger/content/downloadheaders.xhtml"
+%}
diff --git a/comm/mailnews/news/public/nsINntpIncomingServer.idl b/comm/mailnews/news/public/nsINntpIncomingServer.idl
new file mode 100644
index 0000000000..548b62da24
--- /dev/null
+++ b/comm/mailnews/news/public/nsINntpIncomingServer.idl
@@ -0,0 +1,136 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIFile;
+interface nsIMsgNewsFolder;
+interface nsIChannel;
+interface nsIURI;
+interface nsIMsgWindow;
+
+[scriptable, uuid(077620ed-c6c4-4d4d-bed5-4d041f924002)]
+interface nsINntpIncomingServer : nsISupports {
+ /* the on-disk path to the newsrc file for this server */
+ attribute nsIFile newsrcFilePath;
+
+ /* the newsrc root path (the directories all the newsrc files live) */
+ attribute nsIFile newsrcRootPath;
+
+ /* ask the user before downloading more than maxArticles? */
+ attribute boolean notifyOn;
+
+ /* the max articles to download */
+ attribute long maxArticles;
+
+ /* when we don't download all, do we mark the rest read? */
+ attribute boolean markOldRead;
+
+ /* abbreviate the newsgroup names in the folder pane? */
+ attribute boolean abbreviate;
+
+ /* do we use a single login per server or do we login per group */
+ attribute boolean singleSignon;
+
+ /** the server charset and it may be needed to display newsgroup folder
+ * names correctly
+ **/
+ attribute ACString charset;
+
+ /* the server keeps track of all the newsgroups we are subscribed to */
+ void addNewsgroup(in AString name);
+ void removeNewsgroup(in AString name);
+
+ void writeNewsrcFile();
+
+ attribute boolean newsrcHasChanged;
+
+ /**
+ * The maximum number of connections to make to the server.
+ *
+ * This preference (internally max_cached_connections) controls how many
+ * connections we can make. A negative connection count is treated as only
+ * one connection, while 0 (the default) loads the default number of
+ * connections, presently 2.
+ */
+ attribute long maximumConnectionsNumber;
+
+ void displaySubscribedGroup(in nsIMsgNewsFolder msgFolder,
+ in long firstMessage, in long lastMessage,
+ in long totalMessages);
+
+
+ /**
+ * Get a new NNTP channel to run the URI.
+ *
+ * If the server has used up all of its connections, this will place the URI
+ * in the queue to be run when one is freed.
+ *
+ * @param uri The URI to run.
+ * @param window The standard message window object.
+ */
+ nsIChannel getNntpChannel(in nsIURI uri, in nsIMsgWindow window);
+ /**
+ * Enqueues a URI to be run when we have a free connection.
+ *
+ * If there is one already free, it will be immediately started.
+ *
+ * @param uri The URI to run.
+ * @param window The standard message window object.
+ * @param consumer A listener for the response data.
+ */
+ void loadNewsUrl(in nsIURI uri, in nsIMsgWindow window,
+ in nsISupports consumer);
+
+ /**
+ * Returns whether or not the server has subscribed to the given newsgroup.
+ *
+ * Note that the name here is intended to be escaped; however, since `%' is
+ * not a legal newsgroup name, it is possibly safe to pass in an unescaped
+ * newsgroup name.
+ */
+ boolean containsNewsgroup(in AUTF8String escapedName);
+
+ void subscribeToNewsgroup(in AUTF8String name);
+
+ /* used for the subscribe dialog.
+ name is encoded in |charset| (attribute declared above) */
+ void addNewsgroupToList(in string name);
+
+ attribute boolean supportsExtensions;
+ void addExtension(in string extension);
+ boolean queryExtension(in string extension);
+
+ attribute boolean postingAllowed;
+ attribute boolean pushAuth;
+ attribute unsigned long lastUpdatedTime;
+
+ void addPropertyForGet(in string name, in string value);
+ string queryPropertyForGet(in string name);
+
+ void addSearchableGroup(in AString name);
+ boolean querySearchableGroup(in AString name);
+
+ void addSearchableHeader(in string headerName);
+ boolean querySearchableHeader(in string headerName);
+
+ /**
+ * Returns the folder corresponding to the given group.
+ *
+ * Note that this name is expected to be unescaped.
+ * @note If the group does not exist, a bogus news folder will be returned.
+ * DO NOT call this method unless you are sure that the newsgroup
+ * is subscribed to (e.g., by containsNewsgroup)
+ */
+ nsIMsgNewsFolder findGroup(in AUTF8String name);
+
+ readonly attribute AUTF8String firstGroupNeedingExtraInfo;
+ void setGroupNeedsExtraInfo(in AUTF8String name, in boolean needsExtraInfo);
+
+ void groupNotFound(in nsIMsgWindow window, in AString group,
+ in boolean opening);
+
+ void setPrettyNameForGroup(in AString name, in AString prettyName);
+};
diff --git a/comm/mailnews/news/public/nsINntpService.idl b/comm/mailnews/news/public/nsINntpService.idl
new file mode 100644
index 0000000000..ec1b0b3efe
--- /dev/null
+++ b/comm/mailnews/news/public/nsINntpService.idl
@@ -0,0 +1,50 @@
+/* -*- Mode: IDL; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIUrlListener.idl"
+#include "nsINntpIncomingServer.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsIURI;
+interface nsIFile;
+interface nsIMsgWindow;
+interface nsIMsgFolder;
+interface nsICacheStorage;
+
+[scriptable, uuid(dc5cadb0-966c-4ef1-a4c8-cc1e48d1ac61)]
+interface nsINntpService : nsISupports {
+
+ /* newsgroupsList is a comma separated list of newsgroups, which may be
+ * in news://host/group or group form
+ * "news://host/group1,news://host/group2" or "group1,group2"
+ *
+ * newsgroupsHeaderVal is a comma separated list of groups in the group form
+ * "group1,group2"
+ *
+ * newshostHeaderVal is a single hostname.
+ * "host"
+ */
+ void generateNewsHeaderValsForPosting(in ACString newsgroupsList, out string newsgroupsHeaderVal, out string newshostHeaderVal);
+
+ nsIURI postMessage(in nsIFile aFileToPost, in string newsgroupNames, in string aAccountKey, in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ nsIURI getNewNews(in nsINntpIncomingServer nntpServer, in AUTF8String uri, in boolean getOld, in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ nsIURI cancelMessage(in AUTF8String cancelURL, in AUTF8String messageURI, in nsISupports aConsumer, in nsIUrlListener aUrlListener, in nsIMsgWindow aMsgWindow);
+
+ void getListOfGroupsOnServer(in nsINntpIncomingServer nntpServer, in nsIMsgWindow aMsgWindow, in boolean getOnlyNew);
+
+ nsIURI fetchMessage(in nsIMsgFolder newsFolder, in nsMsgKey key, in nsIMsgWindow aMsgWindow, in nsISupports aConsumer, in nsIUrlListener aUrlListener);
+
+ void downloadNewsgroupsForOffline(in nsIMsgWindow aMsgWindow, in nsIUrlListener aListener);
+ /**
+ * can handle news-message:// and news://
+ */
+ void decomposeNewsURI(in AUTF8String uri, out nsIMsgFolder folder, out nsMsgKey key);
+
+ // handle to the cache session used by news....
+ readonly attribute nsICacheStorage cacheStorage;
+};
diff --git a/comm/mailnews/news/public/nsINntpUrl.idl b/comm/mailnews/news/public/nsINntpUrl.idl
new file mode 100644
index 0000000000..33b82b6687
--- /dev/null
+++ b/comm/mailnews/news/public/nsINntpUrl.idl
@@ -0,0 +1,102 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "MailNewsTypes2.idl"
+
+interface nsINNTPNewsgroupPost;
+
+typedef long nsNewsAction;
+
+/**
+ * Represents specific attributes to a URL for news usage.
+ *
+ * Note that the urls represented by this interface can be one of five schemes:
+ * [s]news, nntp[s], or news-message. Any URI that is valid under RFC 5538 will
+ * be accepted. However, it is possible for some queries to be invalid. There
+ * are also a few important things to note:
+ *
+ * - Missing authorities in [s]news: URIs cause nsIMsgMailNewsUrl::server and
+ * nsIMsgMessageUrl::folder to be null.
+ * - nsIMsgMailNewsUrl::server and nsIMsgMessageUrl::folder will be null if the
+ * specified server does not actually exist. In addition, the folder is null
+ * if the group is not currently subscribed on that server.
+ * - Although news-message URIs are parsable, there is no protocol handler
+ * associated with this url. To run these, you should convert these to the
+ * corresponding [s]news or nntp URL, and set the original one in
+ * nsIMsgMessageUrl::uri and ::originalSpec.
+ * - A URI that results in an ActionUnknown will not be run.
+ * - Cancel URIs require the original spec to also be set, so it can find both
+ * the message ID and the group/key combination.
+ * * Some actions require either a group or a message id. Since actions can be
+ * set after the fact, these conditions are not verified.
+ */
+[scriptable, uuid(ef920ca3-9c46-48b8-9fa3-cb430d3681ea)]
+interface nsINntpUrl : nsISupports {
+ /// For ActionPostArticle URIs, the message to be posted.
+ attribute nsINNTPNewsgroupPost messageToPost;
+
+ /**
+ * The action that this URL will take when run.
+ *
+ * Most actions can be automatically determined from the URL spec as follows:
+ *
+ * 1. The query string is searched for the appropriate action.
+ *
+ * 2. A non-empty message ID or key is found (sets ActionFetchArticle).
+ *
+ * 3. A non-empty group is found (ActionGetNewNews or ActionListGroups).
+ */
+ attribute nsNewsAction newsAction;
+
+ /// For ActionGetNewNews URIs, whether or not to get older messages.
+ attribute boolean getOldMessages;
+
+ /**
+ * The group portion of the URI, if one is present.
+ *
+ * This group name is fully unescaped; if you need to construct news URLs with
+ * this value, be sure to escape it first.
+ */
+ readonly attribute ACString group;
+
+ /// The message ID portion of the URI, if one is present
+ readonly attribute ACString messageID;
+
+ /// The message key portion of the URI or nsMsgKey_None if not present
+ readonly attribute nsMsgKey key;
+
+ /// returns the server's default charset.
+ readonly attribute ACString charset;
+
+ /// The action of this news URI could not be determined
+ const nsNewsAction ActionUnknown = 0;
+ /// Fetch the contents of an article
+ const nsNewsAction ActionFetchArticle = 1;
+ /// Fetch the part of an article (requires ?part=)
+ const nsNewsAction ActionFetchPart = 2;
+ /// Save the contents of an article to disk
+ const nsNewsAction ActionSaveMessageToDisk = 3;
+ /// Cancel the article (requires ?cancel)
+ const nsNewsAction ActionCancelArticle = 4;
+ /// Post an article
+ const nsNewsAction ActionPostArticle = 5;
+ /// List the non-expired ids in the newsgroup (requires ?list-ids)
+ const nsNewsAction ActionListIds = 6;
+ /// Do an online newsgroup search (requires ?search)
+ const nsNewsAction ActionSearch = 7;
+ /// Retrieve new messages from the server
+ const nsNewsAction ActionGetNewNews = 8;
+ /// List groups for subscribe
+ const nsNewsAction ActionListGroups = 9;
+ /// List new groups for subscribe (requires ?new-groups)
+ const nsNewsAction ActionListNewGroups = 10;
+
+ /// Constant for the default NNTP over ssl port number
+ const int32_t DEFAULT_NNTP_PORT = 119;
+
+ /// Constant for the default NNTP over ssl port number
+ const int32_t DEFAULT_NNTPS_PORT = 563;
+};
diff --git a/comm/mailnews/news/src/NewsAutoCompleteSearch.jsm b/comm/mailnews/news/src/NewsAutoCompleteSearch.jsm
new file mode 100644
index 0000000000..88716e01bc
--- /dev/null
+++ b/comm/mailnews/news/src/NewsAutoCompleteSearch.jsm
@@ -0,0 +1,134 @@
+/* -*- Mode: Javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["NewsAutoCompleteSearch"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var kACR = Ci.nsIAutoCompleteResult;
+var kSupportedTypes = new Set(["addr_newsgroups", "addr_followup"]);
+
+function NewsAutoCompleteResult(aSearchString) {
+ // Can't create this in the prototype as we'd get the same array for
+ // all instances
+ this._searchResults = [];
+ this.searchString = aSearchString;
+}
+
+NewsAutoCompleteResult.prototype = {
+ _searchResults: null,
+
+ // nsIAutoCompleteResult
+
+ searchString: null,
+ searchResult: kACR.RESULT_NOMATCH,
+ defaultIndex: -1,
+ errorDescription: null,
+
+ get matchCount() {
+ return this._searchResults.length;
+ },
+
+ getValueAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getLabelAt(aIndex) {
+ return this._searchResults[aIndex].value;
+ },
+
+ getCommentAt(aIndex) {
+ return this._searchResults[aIndex].comment;
+ },
+
+ getStyleAt(aIndex) {
+ return "subscribed-news-abook";
+ },
+
+ getImageAt(aIndex) {
+ return "";
+ },
+
+ getFinalCompleteValueAt(aIndex) {
+ return this.getValueAt(aIndex);
+ },
+
+ removeValueAt(aRowIndex, aRemoveFromDB) {},
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]),
+};
+
+function NewsAutoCompleteSearch() {}
+
+NewsAutoCompleteSearch.prototype = {
+ // For component registration
+ classDescription: "Newsgroup Autocomplete",
+
+ cachedAccountKey: "",
+ cachedServer: null,
+
+ /**
+ * Find the newsgroup server associated with the given accountKey.
+ *
+ * @param accountKey The key of the account.
+ * @returns The incoming news server (or null if one does not exist).
+ */
+ _findServer(accountKey) {
+ let account = MailServices.accounts.getAccount(accountKey);
+
+ if (account.incomingServer.type == "nntp") {
+ return account.incomingServer;
+ }
+ return null;
+ },
+
+ // nsIAutoCompleteSearch
+ startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) {
+ let params = aSearchParam ? JSON.parse(aSearchParam) : {};
+ let result = new NewsAutoCompleteResult(aSearchString);
+ if (
+ !("type" in params) ||
+ !("accountKey" in params) ||
+ !kSupportedTypes.has(params.type)
+ ) {
+ result.searchResult = kACR.RESULT_IGNORED;
+ aListener.onSearchResult(this, result);
+ return;
+ }
+
+ if ("accountKey" in params && params.accountKey != this.cachedAccountKey) {
+ this.cachedAccountKey = params.accountKey;
+ this.cachedServer = this._findServer(params.accountKey);
+ }
+
+ if (this.cachedServer) {
+ for (let curr of this.cachedServer.rootFolder.subFolders) {
+ if (curr.prettyName.includes(aSearchString)) {
+ result._searchResults.push({
+ value: curr.prettyName,
+ comment: this.cachedServer.prettyName,
+ });
+ }
+ }
+ }
+
+ if (result.matchCount) {
+ result.searchResult = kACR.RESULT_SUCCESS;
+ // If the user does not select anything, use the first entry:
+ result.defaultIndex = 0;
+ }
+ aListener.onSearchResult(this, result);
+ },
+
+ stopSearch() {},
+
+ // nsISupports
+
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteSearch"]),
+};
diff --git a/comm/mailnews/news/src/NewsDownloader.sys.mjs b/comm/mailnews/news/src/NewsDownloader.sys.mjs
new file mode 100644
index 0000000000..be94dfb96e
--- /dev/null
+++ b/comm/mailnews/news/src/NewsDownloader.sys.mjs
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { NntpUtils } = ChromeUtils.import("resource:///modules/NntpUtils.jsm");
+
+/**
+ * Download articles in all subscribed newsgroups for offline use.
+ */
+export class NewsDownloader {
+ _logger = NntpUtils.logger;
+
+ /**
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIUrlListener} urlListener - Callback for the request.
+ */
+ constructor(msgWindow, urlListener) {
+ this._msgWindow = msgWindow;
+ this._urlListener = urlListener;
+
+ this._bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ }
+
+ /**
+ * Actually start the download process.
+ */
+ async start() {
+ this._logger.debug("Start downloading articles for offline use");
+ let servers = MailServices.accounts.allServers.filter(
+ x => x.type == "nntp"
+ );
+ // Download all servers concurrently.
+ await Promise.all(
+ servers.map(async server => {
+ let folders = server.rootFolder.descendants;
+ for (let folder of folders) {
+ if (folder.flags & Ci.nsMsgFolderFlags.Offline) {
+ // Download newsgroups set for offline use in a server one by one.
+ await this._downloadFolder(folder);
+ }
+ }
+ })
+ );
+
+ this._urlListener.OnStopRunningUrl(null, Cr.NS_OK);
+
+ this._logger.debug("Finished downloading articles for offline use");
+ this._msgWindow.statusFeedback.showStatusString("");
+ }
+
+ /**
+ * Download articles in a newsgroup one by one.
+ *
+ * @param {nsIMsgFolder} folder - The newsgroup folder.
+ */
+ async _downloadFolder(folder) {
+ this._logger.debug(`Start downloading ${folder.URI}`);
+
+ folder.QueryInterface(Ci.nsIMsgNewsFolder).saveArticleOffline = true;
+ let keysToDownload = await this._getKeysToDownload(folder);
+
+ let i = 0;
+ let total = keysToDownload.size;
+ for (let key of keysToDownload) {
+ await new Promise(resolve => {
+ MailServices.nntp.fetchMessage(folder, key, this._msgWindow, null, {
+ OnStartRunningUrl() {},
+ OnStopRunningUrl() {
+ resolve();
+ },
+ });
+ });
+ this._msgWindow.statusFeedback.showStatusString(
+ this._bundle.formatStringFromName("downloadingArticlesForOffline", [
+ ++i,
+ total,
+ folder.prettyName,
+ ])
+ );
+ }
+
+ folder.saveArticleOffline = false;
+ }
+
+ /**
+ * Use a search session to find articles that match the download settings
+ * and we don't already have.
+ *
+ * @param {nsIMsgFolder} folder - The newsgroup folder.
+ * @returns {Set<number>}
+ */
+ async _getKeysToDownload(folder) {
+ let searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ let termValue = searchSession.createTerm().value;
+
+ let downloadSettings = folder.downloadSettings;
+ if (downloadSettings.downloadUnreadOnly) {
+ termValue.attrib = Ci.nsMsgSearchAttrib.MsgStatus;
+ termValue.status = Ci.nsMsgMessageFlags.Read;
+ searchSession.addSearchTerm(
+ Ci.nsMsgSearchAttrib.MsgStatus,
+ Ci.nsMsgSearchOp.Isnt,
+ termValue,
+ true,
+ null
+ );
+ }
+ if (downloadSettings.downloadByDate) {
+ termValue.attrib = Ci.nsMsgSearchAttrib.AgeInDays;
+ termValue.age = downloadSettings.ageLimitOfMsgsToDownload;
+ searchSession.addSearchTerm(
+ Ci.nsMsgSearchAttrib.AgeInDays,
+ Ci.nsMsgSearchOp.IsLessThan,
+ termValue,
+ true,
+ null
+ );
+ }
+ termValue.attrib = Ci.nsMsgSearchAttrib.MsgStatus;
+ termValue.status = Ci.nsMsgMessageFlags.Offline;
+ searchSession.addSearchTerm(
+ Ci.nsMsgSearchAttrib.MsgStatus,
+ Ci.nsMsgSearchOp.Isnt,
+ termValue,
+ true,
+ null
+ );
+
+ let keysToDownload = new Set();
+ await new Promise(resolve => {
+ searchSession.registerListener(
+ {
+ onSearchHit(hdr, folder) {
+ if (!(hdr.flags & Ci.nsMsgMessageFlags.Offline)) {
+ // Only need to download articles we don't already have.
+ keysToDownload.add(hdr.messageKey);
+ }
+ },
+ onSearchDone: status => {
+ resolve();
+ },
+ },
+ Ci.nsIMsgSearchSession.allNotifications
+ );
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.localNews, folder);
+ searchSession.search(this._msgWindow);
+ });
+
+ return keysToDownload;
+ }
+}
diff --git a/comm/mailnews/news/src/NntpChannel.jsm b/comm/mailnews/news/src/NntpChannel.jsm
new file mode 100644
index 0000000000..4e20fca7bc
--- /dev/null
+++ b/comm/mailnews/news/src/NntpChannel.jsm
@@ -0,0 +1,402 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["NntpChannel"];
+
+const { MailChannel } = ChromeUtils.importESModule(
+ "resource:///modules/MailChannel.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ MailUtils: "resource:///modules/MailUtils.jsm",
+ NntpUtils: "resource:///modules/NntpUtils.jsm",
+});
+
+/**
+ * A channel to interact with NNTP server.
+ *
+ * @implements {nsIChannel}
+ * @implements {nsIRequest}
+ * @implements {nsICacheEntryOpenCallback}
+ */
+class NntpChannel extends MailChannel {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMailChannel",
+ "nsIChannel",
+ "nsIRequest",
+ "nsICacheEntryOpenCallback",
+ ]);
+
+ _logger = lazy.NntpUtils.logger;
+ _status = Cr.NS_OK;
+
+ /**
+ * @param {nsIURI} uri - The uri to construct the channel from.
+ * @param {nsILoadInfo} [loadInfo] - The loadInfo associated with the channel.
+ */
+ constructor(uri, loadInfo) {
+ super();
+ this._server = lazy.NntpUtils.findServer(uri.asciiHost);
+ if (!this._server) {
+ this._server = MailServices.accounts
+ .createIncomingServer("", uri.asciiHost, "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ this._server.port = uri.port;
+ }
+
+ if (uri.port < 1) {
+ // Ensure the uri has a port so that memory cache works.
+ uri = uri.mutate().setPort(this._server.port).finalize();
+ }
+
+ // Two forms of the uri:
+ // - news://news.mozilla.org:119/mailman.30.1608649442.1056.accessibility%40lists.mozilla.org?group=mozilla.accessibility&key=378
+ // - news://news.mozilla.org:119/id@mozilla.org
+ let url = new URL(uri.spec);
+ this._groupName = url.searchParams.get("group");
+ if (this._groupName) {
+ this._newsFolder = this._server.rootFolder.getChildNamed(
+ decodeURIComponent(url.searchParams.get("group"))
+ );
+ this._articleNumber = url.searchParams.get("key");
+ } else {
+ this._messageId = decodeURIComponent(url.pathname.slice(1));
+ if (!this._messageId.includes("@")) {
+ this._groupName = this._messageId;
+ this._messageId = null;
+ }
+ }
+
+ // nsIChannel attributes.
+ this.originalURI = uri;
+ this.URI = uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ this.loadInfo = loadInfo || {
+ QueryInterface: ChromeUtils.generateQI(["nsILoadInfo"]),
+ loadingPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ internalContentPolicy: Ci.nsIContentPolicy.TYPE_OTHER,
+ };
+ this.contentLength = 0;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {string}
+ */
+ get name() {
+ return this.URI?.spec;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {boolean}
+ */
+ isPending() {
+ return !!this._pending;
+ }
+
+ /**
+ * @see nsIRequest
+ * @returns {nsresult}
+ */
+ get status() {
+ return this._status;
+ }
+
+ /**
+ * @see nsICacheEntryOpenCallback
+ */
+ onCacheEntryAvailable(entry, isNew, status) {
+ if (!Components.isSuccessCode(status)) {
+ // If memory cache doesn't work, read from the server.
+ this._readFromServer();
+ return;
+ }
+
+ if (isNew) {
+ if (Services.io.offline) {
+ this._status = Cr.NS_ERROR_OFFLINE;
+ return;
+ }
+ // It's a new entry, needs to read from the server.
+ let tee = Cc["@mozilla.org/network/stream-listener-tee;1"].createInstance(
+ Ci.nsIStreamListenerTee
+ );
+ let outStream = entry.openOutputStream(0, -1);
+ // When the tee stream receives data from the server, it writes to both
+ // the original listener and outStream (memory cache).
+ tee.init(this._listener, outStream, null);
+ this._listener = tee;
+ this._cacheEntry = entry;
+ this._readFromServer();
+ return;
+ }
+
+ // It's an old entry, read from the memory cache.
+ this._readFromCacheStream(entry.openInputStream(0));
+ }
+
+ onCacheEntryCheck(entry) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ }
+
+ /**
+ * @see nsIChannel
+ */
+ get contentType() {
+ return this._contentType || "message/rfc822";
+ }
+
+ set contentType(value) {
+ this._contentType = value;
+ }
+
+ get isDocument() {
+ return true;
+ }
+
+ open() {
+ throw Components.Exception(
+ `${this.constructor.name}.open not implemented`,
+ Cr.NS_ERROR_NOT_IMPLEMENTED
+ );
+ }
+
+ asyncOpen(listener) {
+ this._logger.debug("asyncOpen", this.URI.spec);
+ let url = new URL(this.URI.spec);
+ this._listener = listener;
+ if (url.searchParams.has("list-ids")) {
+ // Triggered by newsError.js.
+ this._removeExpired(decodeURIComponent(url.pathname.slice(1)));
+ return;
+ }
+
+ if (this._groupName && !this._server.containsNewsgroup(this._groupName)) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let win = Services.wm.getMostRecentWindow("mail:3pane");
+ let result = Services.prompt.confirm(
+ win,
+ null,
+ bundle.formatStringFromName("autoSubscribeText", [this._groupName])
+ );
+ if (!result) {
+ return;
+ }
+ this._server.subscribeToNewsgroup(this._groupName);
+ let folder = this._server.findGroup(this._groupName);
+ lazy.MailUtils.displayFolderIn3Pane(folder.URI);
+ }
+
+ if (this._groupName && !this._articleNumber && !this._messageId) {
+ let folder = this._server.findGroup(this._groupName);
+ lazy.MailUtils.displayFolderIn3Pane(folder.URI);
+ return;
+ }
+
+ if (url.searchParams.has("part")) {
+ let converter = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ this._listener = converter.asyncConvertData(
+ "message/rfc822",
+ "*/*",
+ listener,
+ this
+ );
+ }
+ try {
+ // Attempt to get the message from the offline storage.
+ try {
+ if (this._readFromOfflineStorage()) {
+ return;
+ }
+ } catch (e) {
+ this._logger.warn(e);
+ }
+
+ let uri = this.URI;
+ if (url.search) {
+ // A full news url may look like
+ // news://<host>:119/<Msg-ID>?group=<name>&key=<key>&header=quotebody.
+ // Remove any query strings to keep the cache key stable.
+ uri = uri.mutate().setQuery("").finalize();
+ }
+
+ // Check if a memory cache is available for the current URI.
+ MailServices.nntp.cacheStorage.asyncOpenURI(
+ uri,
+ "",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ this
+ );
+ } catch (e) {
+ this._logger.warn(e);
+ this._readFromServer();
+ }
+ if (this._status == Cr.NS_ERROR_OFFLINE) {
+ throw new Components.Exception(
+ "The requested action could not be completed in the offline state",
+ Cr.NS_ERROR_OFFLINE
+ );
+ }
+ }
+
+ /**
+ * Try to read the article from the offline storage.
+ *
+ * @returns {boolean} True if successfully read from the offline storage.
+ */
+ _readFromOfflineStorage() {
+ if (!this._newsFolder) {
+ return false;
+ }
+ if (!this._newsFolder.hasMsgOffline(this._articleNumber)) {
+ return false;
+ }
+ let hdr = this._newsFolder.GetMessageHeader(this._articleNumber);
+ let stream = this._newsFolder.getLocalMsgStream(hdr);
+ this._readFromCacheStream(stream);
+ return true;
+ }
+
+ /**
+ * Read the article from the a stream.
+ *
+ * @param {nsIInputStream} cacheStream - The input stream to read.
+ */
+ _readFromCacheStream(cacheStream) {
+ let pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(
+ Ci.nsIInputStreamPump
+ );
+ this.contentLength = 0;
+ this._contentType = "";
+ pump.init(cacheStream, 0, 0, true);
+ pump.asyncRead({
+ onStartRequest: () => {
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ },
+ onStopRequest: (request, status) => {
+ this._listener.onStopRequest(this, status);
+ try {
+ this.loadGroup?.removeRequest(this, null, Cr.NS_OK);
+ } catch (e) {}
+ this._pending = false;
+ },
+ onDataAvailable: (request, stream, offset, count) => {
+ this.contentLength += count;
+ this._listener.onDataAvailable(this, stream, offset, count);
+ try {
+ if (!cacheStream.available()) {
+ cacheStream.close();
+ }
+ } catch (e) {}
+ },
+ });
+ }
+
+ /**
+ * Retrieve the article from the server.
+ */
+ _readFromServer() {
+ this._logger.debug("Read from server");
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ let inputStream = pipe.inputStream;
+ let outputStream = pipe.outputStream;
+ if (this._newsFolder) {
+ this._newsFolder.QueryInterface(Ci.nsIMsgNewsFolder).saveArticleOffline =
+ this._newsFolder.shouldStoreMsgOffline(this._articleNumber);
+ }
+
+ this._server.wrappedJSObject.withClient(client => {
+ let msgWindow;
+ try {
+ msgWindow = this.URI.msgWindow;
+ } catch (e) {}
+ client.startRunningUrl(null, msgWindow, this.URI);
+ client.channel = this;
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ client.onOpen = () => {
+ if (this._messageId) {
+ client.getArticleByMessageId(this._messageId);
+ } else {
+ client.getArticleByArticleNumber(
+ this._groupName,
+ this._articleNumber
+ );
+ }
+ };
+
+ client.onData = data => {
+ this.contentLength += data.length;
+ outputStream.write(data, data.length);
+ this._listener.onDataAvailable(this, inputStream, 0, data.length);
+ };
+
+ client.onDone = status => {
+ try {
+ this.loadGroup?.removeRequest(this, null, Cr.NS_OK);
+ } catch (e) {}
+ if (status != Cr.NS_OK) {
+ // Prevent marking a message as read.
+ this.URI.errorCode = status;
+ // Remove the invalid cache.
+ this._cacheEntry?.asyncDoom(null);
+ }
+ this._listener.onStopRequest(this, status);
+ this._newsFolder?.msgDatabase.commit(
+ Ci.nsMsgDBCommitType.kSessionCommit
+ );
+ this._pending = false;
+ };
+ });
+ }
+
+ /**
+ * Fetch all the article keys on the server, then remove expired keys from the
+ * local folder.
+ *
+ * @param {string} groupName - The group to check.
+ */
+ _removeExpired(groupName) {
+ this._logger.debug("_removeExpired", groupName);
+ let newsFolder = this._server.findGroup(groupName);
+ let allKeys = new Set(newsFolder.msgDatabase.listAllKeys());
+ this._server.wrappedJSObject.withClient(client => {
+ let msgWindow;
+ try {
+ msgWindow = this.URI.msgWindow;
+ } catch (e) {}
+ client.startRunningUrl(null, msgWindow, this.URI);
+ this._listener.onStartRequest(this);
+ this._pending = true;
+ client.onOpen = () => {
+ client.listgroup(groupName);
+ };
+
+ client.onData = data => {
+ allKeys.delete(+data);
+ };
+
+ client.onDone = status => {
+ newsFolder.removeMessages([...allKeys]);
+ this._listener.onStopRequest(this, status);
+ this._pending = false;
+ };
+ });
+ }
+}
diff --git a/comm/mailnews/news/src/NntpClient.jsm b/comm/mailnews/news/src/NntpClient.jsm
new file mode 100644
index 0000000000..dfbd9fde10
--- /dev/null
+++ b/comm/mailnews/news/src/NntpClient.jsm
@@ -0,0 +1,981 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["NntpClient"];
+
+var { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+var { CommonUtils } = ChromeUtils.importESModule(
+ "resource://services-common/utils.sys.mjs"
+);
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { LineReader } = ChromeUtils.import("resource:///modules/LineReader.jsm");
+var { NntpNewsGroup } = ChromeUtils.import(
+ "resource:///modules/NntpNewsGroup.jsm"
+);
+
+// Server response code.
+const AUTH_ACCEPTED = 281;
+const AUTH_PASSWORD_REQUIRED = 381;
+const AUTH_REQUIRED = 480;
+const AUTH_FAILED = 481;
+const SERVICE_UNAVAILABLE = 502;
+const NOT_SUPPORTED = 503;
+const XPAT_OK = 221;
+
+const NNTP_ERROR_MESSAGE = -304;
+
+/**
+ * A structure to represent a response received from the server. A response can
+ * be a single status line of a multi-line data block.
+ *
+ * @typedef {object} NntpResponse
+ * @property {number} status - The status code of the response.
+ * @property {string} statusText - The status line of the response excluding the
+ * status code.
+ * @property {string} data - The part of a multi-line data block excluding the
+ * status line.
+ */
+
+/**
+ * A class to interact with NNTP server.
+ */
+class NntpClient {
+ /**
+ * @param {nsINntpIncomingServer} server - The associated server instance.
+ * @param {string} uri - The server uri.
+ */
+ constructor(server) {
+ this._server = server;
+ this._lineReader = new LineReader();
+
+ this._reset();
+ this._logger = console.createInstance({
+ prefix: "mailnews.nntp",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.nntp.loglevel",
+ });
+ }
+
+ /**
+ * @type {NntpAuthenticator} - An authentication helper.
+ */
+ get _authenticator() {
+ if (!this._nntpAuthenticator) {
+ var { NntpAuthenticator } = ChromeUtils.import(
+ "resource:///modules/MailAuthenticator.jsm"
+ );
+ this._nntpAuthenticator = new NntpAuthenticator(this._server);
+ }
+ return this._nntpAuthenticator;
+ }
+
+ /**
+ * Reset some internal states to be safely reused.
+ */
+ _reset() {
+ this.onOpen = () => {};
+ this.onError = () => {};
+ this.onData = () => {};
+ this.onDone = () => {};
+
+ this.runningUri = null;
+ this.urlListener = null;
+ this._msgWindow = null;
+ this._newsFolder = null;
+ }
+
+ /**
+ * Initiate a connection to the server
+ */
+ connect() {
+ this._done = false;
+ if (this._socket?.readyState == "open") {
+ // Reuse the connection.
+ this.onOpen();
+ } else {
+ // Start a new connection.
+ this._authenticated = false;
+ let hostname = this._server.hostName.toLowerCase();
+ let useSecureTransport = this._server.isSecure;
+ this._logger.debug(
+ `Connecting to ${useSecureTransport ? "snews" : "news"}://${hostname}:${
+ this._server.port
+ }`
+ );
+ this._socket = new TCPSocket(hostname, this._server.port, {
+ binaryType: "arraybuffer",
+ useSecureTransport,
+ });
+ this._socket.onopen = this._onOpen;
+ this._socket.onerror = this._onError;
+ this._showNetworkStatus(Ci.nsISocketTransport.STATUS_CONNECTING_TO);
+ }
+ }
+
+ /**
+ * Construct an nsIMsgMailNewsUrl instance, setup urlListener to notify when
+ * the current request is finished.
+ *
+ * @param {nsIUrlListener} urlListener - Callback for the request.
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIMsgMailNewsUrl} [runningUrl] - The url to run, if provided.
+ * @returns {nsIMsgMailNewsUrl}
+ */
+ startRunningUrl(urlListener, msgWindow, runningUri) {
+ this.urlListener = urlListener;
+ this._msgWindow = msgWindow;
+ this.runningUri = runningUri;
+ if (!this.runningUri) {
+ this.runningUri = Services.io
+ .newURI(`news://${this._server.hostName}:${this._server.port}`)
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ }
+ if (msgWindow) {
+ this.runningUri.msgWindow = msgWindow;
+ }
+ this.urlListener?.OnStartRunningUrl(this.runningUri, Cr.NS_OK);
+ this.runningUri.SetUrlState(true, Cr.NS_OK);
+ return this.runningUri;
+ }
+
+ /**
+ * The open event handler.
+ */
+ _onOpen = () => {
+ this._logger.debug("Connected");
+ this._socket.ondata = this._onData;
+ this._socket.onclose = this._onClose;
+ this._inReadingMode = false;
+ this._currentGroupName = null;
+ this._nextAction = ({ status }) => {
+ if ([200, 201].includes(status)) {
+ this._nextAction = null;
+ this.onOpen();
+ } else {
+ this.quit(Cr.NS_ERROR_FAILURE);
+ }
+ };
+ this._showNetworkStatus(Ci.nsISocketTransport.STATUS_CONNECTED_TO);
+ };
+
+ /**
+ * The data event handler.
+ *
+ * @param {TCPSocketEvent} event - The data event.
+ */
+ _onData = event => {
+ let stringPayload = CommonUtils.arrayBufferToByteString(
+ new Uint8Array(event.data)
+ );
+ this._logger.debug(`S: ${stringPayload}`);
+
+ let res = this._parse(stringPayload);
+ switch (res.status) {
+ case AUTH_REQUIRED:
+ this._currentGroupName = null;
+ this._actionAuthUser();
+ return;
+ case SERVICE_UNAVAILABLE:
+ this._actionError(NNTP_ERROR_MESSAGE, res.statusText);
+ return;
+ default:
+ if (
+ res.status != AUTH_FAILED &&
+ res.status >= 400 &&
+ res.status < 500
+ ) {
+ if (this._messageId || this._articleNumber) {
+ let uri = `about:newserror?r=${res.statusText}`;
+
+ if (this._messageId) {
+ uri += `&m=${encodeURIComponent(this._messageId)}`;
+ } else {
+ let msgId = this._newsFolder?.getMessageIdForKey(
+ this._articleNumber
+ );
+ if (msgId) {
+ uri += `&m=${encodeURIComponent(msgId)}`;
+ }
+ uri += `&k=${this._articleNumber}`;
+ }
+ if (this._newsFolder) {
+ uri += `&f=${this._newsFolder.URI}`;
+ }
+ // Store the uri to display. The registered uriListener will get
+ // notified when we stop running the uri, and can act on this data.
+ this.runningUri.seeOtherURI = uri;
+ }
+ this._actionError(NNTP_ERROR_MESSAGE, res.statusText);
+ return;
+ }
+ }
+
+ try {
+ this._nextAction?.(res);
+ } catch (e) {
+ this._logger.error(`Failed to process server response ${res}.`, e);
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ }
+ };
+
+ /**
+ * The error event handler.
+ *
+ * @param {TCPSocketErrorEvent} event - The error event.
+ */
+ _onError = event => {
+ this._logger.error(event, event.name, event.message, event.errorCode);
+ let errorName;
+ let uri;
+ switch (event.errorCode) {
+ case Cr.NS_ERROR_UNKNOWN_HOST:
+ case Cr.NS_ERROR_UNKNOWN_PROXY_HOST:
+ errorName = "unknownHostError";
+ uri = "about:neterror?e=dnsNotFound";
+ break;
+ case Cr.NS_ERROR_CONNECTION_REFUSED:
+ errorName = "connectionRefusedError";
+ uri = "about:neterror?e=connectionFailure";
+ break;
+ case Cr.NS_ERROR_PROXY_CONNECTION_REFUSED:
+ errorName = "connectionRefusedError";
+ uri = "about:neterror?e=proxyConnectFailure";
+ break;
+ case Cr.NS_ERROR_NET_TIMEOUT:
+ errorName = "netTimeoutError";
+ uri = "about:neterror?e=netTimeout";
+ break;
+ case Cr.NS_ERROR_NET_RESET:
+ errorName = "netResetError";
+ uri = "about:neterror?e=netReset";
+ break;
+ case Cr.NS_ERROR_NET_INTERRUPT:
+ errorName = "netInterruptError";
+ uri = "about:neterror?e=netInterrupt";
+ break;
+ }
+ if (errorName && uri) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ let errorMessage = bundle.formatStringFromName(errorName, [
+ this._server.hostName,
+ ]);
+ MailServices.mailSession.alertUser(errorMessage, this.runningUri);
+
+ // If we were going to display an article, instead show an error page.
+ this.runningUri.seeOtherURI = uri;
+ }
+
+ this._msgWindow?.statusFeedback?.showStatusString("");
+ this.quit(event.errorCode);
+ };
+
+ /**
+ * The close event handler.
+ */
+ _onClose = () => {
+ this._logger.debug("Connection closed.");
+ };
+
+ /**
+ * Parse the server response.
+ *
+ * @param {string} str - Response received from the server.
+ * @returns {NntpResponse}
+ */
+ _parse(str) {
+ if (this._lineReader.processingMultiLineResponse) {
+ // When processing multi-line response, no parsing should happen.
+ return { data: str };
+ }
+ let matches = /^(\d{3}) (.+)\r\n([^]*)/.exec(str);
+ if (matches) {
+ let [, status, statusText, data] = matches;
+ return { status: Number(status), statusText, data };
+ }
+ return { data: str };
+ }
+
+ /**
+ * Send a command to the socket.
+ *
+ * @param {string} str - The command string to send.
+ * @param {boolean} [suppressLogging=false] - Whether to suppress logging the str.
+ */
+ _sendCommand(str, suppressLogging) {
+ if (this._socket.readyState != "open") {
+ if (str != "QUIT") {
+ this._logger.warn(
+ `Failed to send "${str}" because socket state is ${this._socket.readyState}`
+ );
+ }
+ return;
+ }
+ if (suppressLogging && AppConstants.MOZ_UPDATE_CHANNEL != "default") {
+ this._logger.debug(
+ "C: Logging suppressed (it probably contained auth information)"
+ );
+ } else {
+ // Do not suppress for non-release builds, so that debugging auth problems
+ // is easier.
+ this._logger.debug(`C: ${str}`);
+ }
+ this.send(str + "\r\n");
+ }
+
+ /**
+ * Send a string to the socket.
+ *
+ * @param {string} str - The string to send.
+ */
+ send(str) {
+ this._socket.send(CommonUtils.byteStringToArrayBuffer(str).buffer);
+ }
+
+ /**
+ * Send a LIST or NEWGROUPS command to get groups in the current server.
+ *
+ * @param {boolean} getOnlyNew - List only new groups.
+ */
+ getListOfGroups(getOnlyNew) {
+ if (!getOnlyNew) {
+ this._actionModeReader(this._actionList);
+ } else {
+ this._actionModeReader(this._actionNewgroups);
+ }
+ this.urlListener = this._server.QueryInterface(Ci.nsIUrlListener);
+ }
+
+ /**
+ * Get new articles.
+ *
+ * @param {string} groupName - The group to get new articles.
+ * @param {boolean} getOld - Get old articles as well.
+ */
+ getNewNews(groupName, getOld) {
+ this._currentGroupName = null;
+ this._newsFolder = this._getNewsFolder(groupName);
+ this._newsGroup = new NntpNewsGroup(this._server, this._newsFolder);
+ this._newsGroup.getOldMessages = getOld;
+ this._nextGroupName = this._newsFolder.rawName;
+ this.runningUri.updatingFolder = true;
+ this._firstGroupCommand = this._actionXOver;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Get a single article by group name and article number.
+ *
+ * @param {string} groupName - The group name.
+ * @param {integer} articleNumber - The article number.
+ */
+ getArticleByArticleNumber(groupName, articleNumber) {
+ this._newsFolder = this._server.rootFolder.getChildNamed(groupName);
+ this._nextGroupName = this._getNextGroupName(groupName);
+ this._articleNumber = articleNumber;
+ this._messageId = "";
+ this._firstGroupCommand = this._actionArticle;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Get a single article by the message id.
+ *
+ * @param {string} messageId - The message id.
+ */
+ getArticleByMessageId(messageId) {
+ this._messageId = `<${messageId}>`;
+ this._articleNumber = 0;
+ this._actionModeReader(this._actionArticle);
+ }
+
+ /**
+ * Send a `Control: cancel <msg-id>` message to cancel an article, not every
+ * server supports it, see rfc5537.
+ *
+ * @param {string} groupName - The group name.
+ */
+ cancelArticle(groupName) {
+ this._nextGroupName = this._getNextGroupName(groupName);
+ this._firstGroupCommand = this.post;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Send a `XPAT <header> <message-id> <pattern>` message, not every server
+ * supports it, see rfc2980.
+ *
+ * @param {string} groupName - The group name.
+ * @param {string[]} xpatLines - An array of xpat lines to send.
+ */
+ search(groupName, xpatLines) {
+ this._nextGroupName = this._getNextGroupName(groupName);
+ this._xpatLines = xpatLines;
+ this._firstGroupCommand = this._actionXPat;
+ this._actionModeReader(this._actionGroup);
+ }
+
+ /**
+ * Load a news uri directly, see rfc5538 about supported news uri.
+ *
+ * @param {string} uri - The news uri to load.
+ * @param {nsIMsgWindow} msgWindow - The associated msg window.
+ * @param {nsIStreamListener} streamListener - The listener for the request.
+ */
+ loadNewsUrl(uri, msgWindow, streamListener) {
+ this._logger.debug(`Loading ${uri}`);
+ let url = new URL(uri);
+ let path = url.pathname.slice(1);
+ let action;
+ if (path == "*") {
+ action = () => this.getListOfGroups();
+ } else if (path.includes("@")) {
+ action = () => this.getArticleByMessageId(path);
+ } else {
+ this._newsFolder = this._getNewsFolder(path);
+ this._newsGroup = new NntpNewsGroup(this._server, this._newsFolder);
+ this._nextGroupName = this._newsFolder.rawName;
+ action = () => this._actionModeReader(this._actionGroup);
+ }
+ if (!action) {
+ return;
+ }
+ this._msgWindow = msgWindow;
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ let inputStream = pipe.inputStream;
+ let outputStream = pipe.outputStream;
+ this.onOpen = () => {
+ streamListener.onStartRequest(null, Cr.NS_OK);
+ action();
+ };
+ this.onData = data => {
+ outputStream.write(data, data.length);
+ streamListener.onDataAvailable(null, inputStream, 0, data.length);
+ };
+ this.onDone = status => {
+ streamListener.onStopRequest(null, status);
+ };
+ }
+
+ /**
+ * Send LISTGROUP request to the server.
+ *
+ * @param {string} groupName - The group to request.
+ */
+ listgroup(groupName) {
+ this._actionModeReader(() => {
+ this._nextAction = this._actionListgroupResponse;
+ this._sendCommand(`LISTGROUP ${groupName}`);
+ });
+ }
+
+ /**
+ * Send `POST` request to the server.
+ */
+ post() {
+ let action = () => {
+ this._nextAction = this._actionHandlePost;
+ this._sendCommand("POST");
+ };
+ if (this._server.pushAuth && !this._authenticated) {
+ this._currentAction = action;
+ this._actionAuthUser();
+ } else {
+ action();
+ }
+ }
+
+ /**
+ * Send `QUIT` request to the server.
+ */
+ quit(status = Cr.NS_OK) {
+ this._sendCommand("QUIT");
+ this._nextAction = this.close;
+ this.close();
+ this._actionDone(status);
+ }
+
+ /**
+ * Close the socket.
+ */
+ close() {
+ this._socket.close();
+ }
+
+ /**
+ * Get the news folder corresponding to a group name.
+ *
+ * @param {string} groupName - The group name.
+ * @returns {nsIMsgNewsFolder}
+ */
+ _getNewsFolder(groupName) {
+ return this._server.rootFolder
+ .getChildNamed(groupName)
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ }
+
+ /**
+ * Given a UTF-8 group name, return the underlying group name used by the server.
+ *
+ * @param {string} groupName - The UTF-8 group name.
+ * @returns {BinaryString} - The group name that can be sent to the server.
+ */
+ _getNextGroupName(groupName) {
+ return this._getNewsFolder(groupName).rawName;
+ }
+
+ /**
+ * Send `MODE READER` request to the server.
+ */
+ _actionModeReader(nextAction) {
+ if (this._inReadingMode) {
+ nextAction();
+ } else {
+ this._currentAction = () => {
+ this._inReadingMode = false;
+ this._actionModeReader(nextAction);
+ };
+ this._sendCommand("MODE READER");
+ this._inReadingMode = true;
+ this._nextAction = () => {
+ if (this._server.pushAuth && !this._authenticated) {
+ this._currentAction = nextAction;
+ this._actionAuthUser();
+ } else {
+ nextAction();
+ }
+ };
+ }
+ }
+
+ /**
+ * Send `LIST` request to the server.
+ */
+ _actionList = () => {
+ this._sendCommand("LIST");
+ this._currentAction = this._actionList;
+ this._nextAction = this._actionReadData;
+ };
+
+ /**
+ * Send `NEWGROUPS` request to the server.
+ * @see rfc3977#section-7.3
+ */
+ _actionNewgroups = () => {
+ const days = Services.prefs.getIntPref("news.newgroups_for_num_days", 180);
+ const dateTime = new Date(Date.now() - 86400000 * days)
+ .toISOString()
+ .replace(
+ /(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}).*/,
+ "$1$2$3 $4$5$6"
+ );
+ this._sendCommand("NEWGROUPS " + dateTime + " GMT");
+ this._currentAction = this._actionNewgroups;
+ this._nextAction = this._actionReadData;
+ };
+
+ /**
+ * Send `GROUP` request to the server.
+ */
+ _actionGroup = () => {
+ this._firstGroupCommand = this._firstGroupCommand || this._actionXOver;
+ if (this._nextGroupName == this._currentGroupName) {
+ this._firstGroupCommand();
+ } else {
+ this._sendCommand(`GROUP ${this._nextGroupName}`);
+ this._currentAction = this._actionGroup;
+ this._currentGroupName = this._nextGroupName;
+ this._nextAction = this._actionGroupResponse;
+ }
+ };
+
+ /**
+ * Handle GROUP response.
+ *
+ * @param {NntpResponse} res - GROUP response received from the server.
+ */
+ _actionGroupResponse = res => {
+ if (res.status == 411) {
+ this._server.groupNotFound(null, this._currentGroupName, true);
+ return;
+ }
+ this._firstGroupCommand(res);
+ };
+
+ /**
+ * Consume the status line of LISTGROUP response.
+ */
+ _actionListgroupResponse = res => {
+ this._nextAction = this._actionListgroupDataResponse;
+ if (res.data) {
+ this._actionListgroupDataResponse(res);
+ }
+ };
+
+ /**
+ * Consume the multi-line data of LISTGROUP response.
+ *
+ * @param {NntpResponse} res - The server response.
+ */
+ _actionListgroupDataResponse = ({ data }) => {
+ this._lineReader.read(
+ data,
+ line => {
+ this.onData(line);
+ },
+ () => {
+ this._actionDone();
+ }
+ );
+ };
+
+ /**
+ * Send `XOVER` request to the server.
+ */
+ _actionXOver = res => {
+ let [count, low, high] = res.statusText.split(" ");
+ this._newsFolder.updateSummaryFromNNTPInfo(low, high, count);
+ let [start, end] = this._newsGroup.getArticlesRangeToFetch(
+ this._msgWindow,
+ Number(low),
+ Number(high)
+ );
+ if (start && end) {
+ this._startArticle = start;
+ this._endArticle = end;
+ this._nextAction = this._actionXOverResponse;
+ this._sendCommand(`XOVER ${start}-${end}`);
+ } else {
+ this._actionDone();
+ }
+ };
+
+ /**
+ * A transient action to consume the status line of XOVER response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionXOverResponse(res) {
+ if (res.status == 224) {
+ this._nextAction = this._actionReadXOver;
+ this._newsGroup.addKnownArticles(this._startArticle, this._endArticle);
+ this._actionReadXOver(res);
+ } else {
+ // Somehow XOVER is not supported by the server, fallback to use HEAD to
+ // fetch one by one.
+ this._actionHead();
+ }
+ }
+
+ /**
+ * Handle XOVER response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionReadXOver({ data }) {
+ this._lineReader.read(
+ data,
+ line => {
+ this._newsGroup.processXOverLine(line);
+ },
+ () => {
+ // Fetch extra headers used by filters, but not returned in XOVER response.
+ this._xhdrFields = this._newsGroup.getXHdrFields();
+ this._actionXHdr();
+ }
+ );
+ }
+
+ /**
+ * Send `XHDR` request to the server.
+ */
+ _actionXHdr = () => {
+ this._curXHdrHeader = this._xhdrFields.shift();
+ if (this._curXHdrHeader) {
+ this._nextAction = this._actionXHdrResponse;
+ this._sendCommand(
+ `XHDR ${this._curXHdrHeader} ${this._startArticle}-${this._endArticle}`
+ );
+ } else {
+ this._newsGroup.finishProcessingXOver();
+ this._actionDone();
+ }
+ };
+
+ /**
+ * Handle XHDR response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionXHdrResponse({ status, data }) {
+ if (status == NOT_SUPPORTED) {
+ // Fallback to HEAD request.
+ this._actionHead();
+ return;
+ }
+
+ this._lineReader.read(
+ data,
+ line => {
+ this._newsGroup.processXHdrLine(this._curXHdrHeader, line);
+ },
+ this._actionXHdr
+ );
+ }
+
+ /**
+ * Send `HEAD` request to the server.
+ */
+ _actionHead = () => {
+ if (this._startArticle <= this._endArticle) {
+ this._nextAction = this._actionReadHead;
+ this._sendCommand(`HEAD ${this._startArticle}`);
+ this._newsGroup.initHdr(this._startArticle);
+ this._startArticle++;
+ } else {
+ this._newsGroup.finishProcessingXOver();
+ this._actionDone();
+ }
+ };
+
+ /**
+ * Handle HEAD response.
+ *
+ * @param {NntpResponse} res - XOVER response received from the server.
+ */
+ _actionReadHead({ data }) {
+ this._lineReader.read(
+ data,
+ line => {
+ this._newsGroup.processHeadLine(line);
+ },
+ () => {
+ this._newsGroup.initHdr(-1);
+ this._actionHead();
+ }
+ );
+ }
+
+ /**
+ * Send `ARTICLE` request to the server.
+ * @see {@link https://www.rfc-editor.org/rfc/rfc3977#section-6.2.1|RFC 3977 §6.2.1}
+ */
+ _actionArticle = () => {
+ this._sendCommand(`ARTICLE ${this._articleNumber || this._messageId}`);
+ this._nextAction = this._actionArticleResponse;
+ this._newsFolder?.notifyDownloadBegin(this._articleNumber);
+ this._downloadingToFolder = true;
+ };
+
+ /**
+ * Handle `ARTICLE` response.
+ *
+ * @param {NntpResponse} res - ARTICLE response received from the server.
+ */
+ _actionArticleResponse = ({ data }) => {
+ let lineSeparator = AppConstants.platform == "win" ? "\r\n" : "\n";
+
+ this._lineReader.read(
+ data,
+ line => {
+ // NewsFolder will decide whether to save it to the offline storage.
+ this._newsFolder?.notifyDownloadedLine(
+ line.slice(0, -2) + lineSeparator
+ );
+ this.onData(line);
+ },
+ () => {
+ this._newsFolder?.notifyDownloadEnd(Cr.NS_OK);
+ this._downloadingToFolder = false;
+ this._actionDone();
+ }
+ );
+ };
+
+ /**
+ * Handle multi-line data blocks response, e.g. ARTICLE/LIST response. Emit
+ * each line through onData.
+ *
+ * @param {NntpResponse} res - Response received from the server.
+ */
+ _actionReadData({ data }) {
+ this._lineReader.read(data, this.onData, this._actionDone);
+ }
+
+ /**
+ * Handle POST response.
+ *
+ * @param {NntpResponse} res - POST response received from the server.
+ */
+ _actionHandlePost({ status, statusText }) {
+ if (status == 340) {
+ this.onReadyToPost();
+ } else if (status == 240) {
+ this._actionDone();
+ } else {
+ this._actionError(NNTP_ERROR_MESSAGE, statusText);
+ }
+ }
+
+ /**
+ * Send `AUTHINFO user <name>` to the server.
+ *
+ * @param {boolean} [forcePrompt=false] - Whether to force showing an auth prompt.
+ */
+ _actionAuthUser(forcePrompt = false) {
+ if (!this._newsFolder) {
+ this._newsFolder = this._server.rootFolder.QueryInterface(
+ Ci.nsIMsgNewsFolder
+ );
+ }
+ if (!this._newsFolder.groupUsername) {
+ let gotPassword = this._newsFolder.getAuthenticationCredentials(
+ this._msgWindow,
+ true,
+ forcePrompt
+ );
+ if (!gotPassword) {
+ this._actionDone(Cr.NS_ERROR_ABORT);
+ return;
+ }
+ }
+ this._sendCommand(`AUTHINFO user ${this._newsFolder.groupUsername}`, true);
+ this._nextAction = this._actionAuthResult;
+ this._authenticator.username = this._newsFolder.groupUsername;
+ }
+
+ /**
+ * Send `AUTHINFO pass <password>` to the server.
+ */
+ _actionAuthPassword() {
+ this._sendCommand(`AUTHINFO pass ${this._newsFolder.groupPassword}`, true);
+ this._nextAction = this._actionAuthResult;
+ }
+
+ /**
+ * Decide the next step according to the auth response.
+ *
+ * @param {NntpResponse} res - Auth response received from the server.
+ */
+ _actionAuthResult({ status }) {
+ switch (status) {
+ case AUTH_ACCEPTED:
+ this._authenticated = true;
+ this._currentAction?.();
+ return;
+ case AUTH_PASSWORD_REQUIRED:
+ this._actionAuthPassword();
+ return;
+ case AUTH_FAILED:
+ let action = this._authenticator.promptAuthFailed();
+ if (action == 1) {
+ // Cancel button pressed.
+ this._actionDone();
+ return;
+ }
+ if (action == 2) {
+ // 'New password' button pressed.
+ this._newsFolder.forgetAuthenticationCredentials();
+ }
+ // Retry.
+ this._actionAuthUser();
+ }
+ }
+
+ /**
+ * Send `XPAT <header> <message-id> <pattern>` to the server.
+ */
+ _actionXPat = () => {
+ let xptLine = this._xpatLines.shift();
+ if (!xptLine) {
+ this._actionDone();
+ return;
+ }
+ this._sendCommand(xptLine);
+ this._nextAction = this._actionXPatResponse;
+ };
+
+ /**
+ * Handle XPAT response.
+ *
+ * @param {NntpResponse} res - XPAT response received from the server.
+ */
+ _actionXPatResponse({ status, statusText, data }) {
+ if (status && status != XPAT_OK) {
+ this._actionError(NNTP_ERROR_MESSAGE, statusText);
+ return;
+ }
+ this._lineReader.read(data, this.onData, this._actionXPat);
+ }
+
+ /**
+ * Show network status in the status bar.
+ *
+ * @param {number} status - See NS_NET_STATUS_* in nsISocketTransport.idl.
+ */
+ _showNetworkStatus(status) {
+ let statusMessage = Services.strings.formatStatusMessage(
+ status,
+ this._server.hostName
+ );
+ this._msgWindow?.statusFeedback?.showStatusString(statusMessage);
+ }
+
+ /**
+ * Show an error prompt.
+ *
+ * @param {number} errorId - An error name corresponds to an entry of
+ * news.properties.
+ * @param {string} serverErrorMsg - Error message returned by the server.
+ */
+ _actionError(errorId, serverErrorMsg) {
+ this._logger.error(`Got an error id=${errorId}`);
+ let msgWindow = this._msgWindow;
+
+ if (!msgWindow) {
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ return;
+ }
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let errorMsg = bundle.GetStringFromID(errorId);
+ if (serverErrorMsg) {
+ errorMsg += " " + serverErrorMsg;
+ }
+ Services.prompt.alert(msgWindow?.domWindow, null, errorMsg);
+
+ this._actionDone(Cr.NS_ERROR_FAILURE);
+ }
+
+ /**
+ * Close the connection and do necessary cleanup.
+ */
+ _actionDone = (status = Cr.NS_OK) => {
+ if (this._done) {
+ return;
+ }
+ if (this._downloadingToFolder) {
+ // If we're in the middle of sending a message to the folder, make sure
+ // the folder knows we're aborting.
+ this._newsFolder?.notifyDownloadEnd(Cr.NS_ERROR_FAILURE);
+ this._downloadingToFolder = false;
+ }
+ this._done = true;
+ this._logger.debug(`Done with status=${status}`);
+ this.onDone(status);
+ this._newsGroup?.cleanUp();
+ this._newsFolder?.OnStopRunningUrl?.(this.runningUri, status);
+ this.urlListener?.OnStopRunningUrl(this.runningUri, status);
+ this.runningUri.SetUrlState(false, status);
+ this._reset();
+ this.onIdle?.();
+ };
+}
diff --git a/comm/mailnews/news/src/NntpIncomingServer.jsm b/comm/mailnews/news/src/NntpIncomingServer.jsm
new file mode 100644
index 0000000000..3497cbae77
--- /dev/null
+++ b/comm/mailnews/news/src/NntpIncomingServer.jsm
@@ -0,0 +1,624 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["NntpIncomingServer"];
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { MsgIncomingServer } = ChromeUtils.import(
+ "resource:///modules/MsgIncomingServer.jsm"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CommonUtils: "resource://services-common/utils.sys.mjs",
+ clearInterval: "resource://gre/modules/Timer.sys.mjs",
+ setInterval: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NntpClient: "resource:///modules/NntpClient.jsm",
+});
+
+/**
+ * A class to represent a NNTP server.
+ *
+ * @implements {nsINntpIncomingServer}
+ * @implements {nsIMsgIncomingServer}
+ * @implements {nsISupportsWeakReference}
+ * @implements {nsISubscribableServer}
+ * @implements {nsITreeView}
+ * @implements {nsIUrlListener}
+ */
+class NntpIncomingServer extends MsgIncomingServer {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsINntpIncomingServer",
+ "nsIMsgIncomingServer",
+ "nsISupportsWeakReference",
+ "nsISubscribableServer",
+ "nsITreeView",
+ "nsIUrlListener",
+ ]);
+
+ constructor() {
+ super();
+
+ this._subscribed = new Set();
+ this._groups = [];
+
+ // @type {NntpClient[]} - An array of connections can be used.
+ this._idleConnections = [];
+ // @type {NntpClient[]} - An array of connections in use.
+ this._busyConnections = [];
+ // @type {Function[]} - An array of Promise.resolve functions.
+ this._connectionWaitingQueue = [];
+
+ Services.obs.addObserver(this, "profile-before-change");
+ // Update newsrc every 5 minutes.
+ this._newsrcTimer = lazy.setInterval(() => this.writeNewsrcFile(), 300000);
+
+ // nsIMsgIncomingServer attributes.
+ this.localStoreType = "news";
+ this.localDatabaseType = "news";
+ this.canSearchMessages = true;
+ this.canCompactFoldersOnServer = false;
+ this.sortOrder = 500000000;
+
+ Object.defineProperty(this, "defaultCopiesAndFoldersPrefsToServer", {
+ // No Draft/Sent folder on news servers, will point to "Local Folders".
+ get: () => false,
+ });
+ Object.defineProperty(this, "canCreateFoldersOnServer", {
+ // No folder creation on news servers.
+ get: () => false,
+ });
+ Object.defineProperty(this, "canFileMessagesOnServer", {
+ get: () => false,
+ });
+
+ // nsISubscribableServer attributes.
+ this.supportsSubscribeSearch = true;
+
+ // nsINntpIncomingServer attributes.
+ this.newsrcHasChanged = false;
+
+ // nsINntpIncomingServer attributes that map directly to pref values.
+ this._mapAttrsToPrefs([
+ ["Bool", "notifyOn", "notify.on"],
+ ["Bool", "markOldRead", "mark_old_read"],
+ ["Bool", "abbreviate", "abbreviate"],
+ ["Bool", "pushAuth", "always_authenticate"],
+ ["Bool", "singleSignon"],
+ ["Int", "maxArticles", "max_articles"],
+ ]);
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "profile-before-change":
+ lazy.clearInterval(this._newsrcTimer);
+ this.writeNewsrcFile();
+ }
+ }
+
+ /**
+ * Most of nsISubscribableServer interfaces are delegated to
+ * this._subscribable.
+ */
+ get _subscribable() {
+ if (!this._subscribableServer) {
+ this._subscribableServer = Cc[
+ "@mozilla.org/messenger/subscribableserver;1"
+ ].createInstance(Ci.nsISubscribableServer);
+ this._subscribableServer.setIncomingServer(this);
+ }
+ return this._subscribableServer;
+ }
+
+ /** @see nsISubscribableServer */
+ get folderView() {
+ return this._subscribable.folderView;
+ }
+
+ get subscribeListener() {
+ return this._subscribable.subscribeListener;
+ }
+
+ set subscribeListener(value) {
+ this._subscribable.subscribeListener = value;
+ }
+
+ subscribeCleanup() {
+ this._subscribableServer = null;
+ }
+
+ startPopulating(msgWindow, forceToServer, getOnlyNew) {
+ this._startPopulating(msgWindow, forceToServer, getOnlyNew);
+ }
+
+ stopPopulating(msgWindow) {
+ this._subscribable.stopPopulating(msgWindow);
+ if (!this._hostInfoLoaded) {
+ this._saveHostInfo();
+ }
+ this.updateSubscribed();
+ }
+
+ addTo(name, addAsSubscribed, subscribale, changeIfExists) {
+ try {
+ this._subscribable.addTo(
+ name,
+ addAsSubscribed,
+ subscribale,
+ changeIfExists
+ );
+ this._groups.push(name);
+ } catch (e) {
+ // Group names with double dot, like alt.binaries.sounds..mp3.zappa are
+ // not working. Bug 1788572.
+ console.error(`Failed to add group ${name}. ${e}`);
+ }
+ }
+
+ subscribe(name) {
+ this.subscribeToNewsgroup(name);
+ }
+
+ unsubscribe(name) {
+ this.rootMsgFolder.propagateDelete(
+ this.rootMsgFolder.getChildNamed(name),
+ true // delete storage
+ );
+ this.newsrcHasChanged = true;
+ }
+
+ commitSubscribeChanges() {
+ this.newsrcHasChanged = true;
+ this.writeNewsrcFile();
+ }
+
+ setAsSubscribed(path) {
+ this._tmpSubscribed.add(path);
+ this._subscribable.setAsSubscribed(path);
+ }
+
+ updateSubscribed() {
+ this._tmpSubscribed = new Set();
+ this._subscribed.forEach(path => this.setAsSubscribed(path));
+ }
+
+ setState(path, state) {
+ let changed = this._subscribable.setState(path, state);
+ if (changed) {
+ if (state) {
+ this._tmpSubscribed.add(path);
+ } else {
+ this._tmpSubscribed.delete(path);
+ }
+ }
+ return changed;
+ }
+
+ hasChildren(path) {
+ return this._subscribable.hasChildren(path);
+ }
+
+ isSubscribed(path) {
+ return this._subscribable.isSubscribed(path);
+ }
+
+ isSubscribable(path) {
+ return this._subscribable.isSubscribable(path);
+ }
+
+ setSearchValue(value) {
+ this._tree?.beginUpdateBatch();
+ this._tree?.rowCountChanged(0, -this._searchResult.length);
+
+ let terms = value.toLowerCase().split(" ");
+ this._searchResult = this._groups
+ .filter(name => {
+ name = name.toLowerCase();
+ // The group name should contain all the search terms.
+ return terms.every(term => name.includes(term));
+ })
+ .sort();
+
+ this._tree?.rowCountChanged(0, this._searchResult.length);
+ this._tree?.endUpdateBatch();
+ }
+
+ getLeafName(path) {
+ return this._subscribable.getLeafName(path);
+ }
+
+ getFirstChildURI(path) {
+ return this._subscribable.getFirstChildURI(path);
+ }
+
+ getChildURIs(path) {
+ return this._subscribable.getChildURIs(path);
+ }
+
+ /** @see nsITreeView */
+ get rowCount() {
+ return this._searchResult.length;
+ }
+
+ isContainer(index) {
+ return false;
+ }
+
+ getCellProperties(row, col) {
+ if (
+ col.id == "subscribedColumn2" &&
+ this._tmpSubscribed.has(this._searchResult[row])
+ ) {
+ return "subscribed-true";
+ }
+ if (col.id == "nameColumn2") {
+ // Show the news folder icon in the search view.
+ return "serverType-nntp";
+ }
+ return "";
+ }
+
+ getCellValue(row, col) {
+ if (col.id == "nameColumn2") {
+ return this._searchResult[row];
+ }
+ return "";
+ }
+
+ getCellText(row, col) {
+ if (col.id == "nameColumn2") {
+ return this._searchResult[row];
+ }
+ return "";
+ }
+
+ setTree(tree) {
+ this._tree = tree;
+ }
+
+ /** @see nsIUrlListener */
+ OnStartRunningUrl() {}
+
+ OnStopRunningUrl() {
+ this.stopPopulating(this._msgWindow);
+ }
+
+ /** @see nsIMsgIncomingServer */
+ get serverRequiresPasswordForBiff() {
+ return false;
+ }
+
+ get filterScope() {
+ return Ci.nsMsgSearchScope.newsFilter;
+ }
+
+ get searchScope() {
+ return Services.io.offline
+ ? Ci.nsMsgSearchScope.localNewsBody
+ : Ci.nsMsgSearchScope.news;
+ }
+
+ get offlineSupportLevel() {
+ const OFFLINE_SUPPORT_LEVEL_UNDEFINED = -1;
+ const OFFLINE_SUPPORT_LEVEL_EXTENDED = 20;
+ let level = this.getIntValue("offline_support_level");
+ return level != OFFLINE_SUPPORT_LEVEL_UNDEFINED
+ ? level
+ : OFFLINE_SUPPORT_LEVEL_EXTENDED;
+ }
+
+ performExpand(msgWindow) {
+ if (!Services.prefs.getBoolPref("news.update_unread_on_expand", false)) {
+ return;
+ }
+
+ for (let folder of this.rootFolder.subFolders) {
+ folder.getNewMessages(msgWindow, null);
+ }
+ }
+
+ performBiff(msgWindow) {
+ this.performExpand(msgWindow);
+ }
+
+ closeCachedConnections() {
+ for (let client of [...this._idleConnections, ...this._busyConnections]) {
+ client.quit();
+ }
+ this._idleConnections = [];
+ this._busyConnections = [];
+ }
+
+ /** @see nsINntpIncomingServer */
+ get charset() {
+ return this.getCharValue("charset") || "UTF-8";
+ }
+
+ set charset(value) {
+ this.setCharValue("charset", value);
+ }
+
+ get maximumConnectionsNumber() {
+ let maxConnections = this.getIntValue("max_cached_connections", 0);
+ if (maxConnections > 0) {
+ return maxConnections;
+ }
+ // The default is 2 connections, if the pref value is 0, we use the default.
+ // If it's negative, treat it as 1.
+ maxConnections = maxConnections == 0 ? 2 : 1;
+ this.maximumConnectionsNumber = maxConnections;
+ return maxConnections;
+ }
+
+ set maximumConnectionsNumber(value) {
+ this.setIntValue("max_cached_connections", value);
+ }
+
+ get newsrcRootPath() {
+ let file = this.getFileValue("mail.newsrc_root-rel", "mail.newsrc_root");
+ if (!file) {
+ file = Services.dirsvc.get("NewsD", Ci.nsIFile);
+ this.setFileValue("mail.newsrc_root-rel", "mail.newsrc_root", file);
+ }
+ return file;
+ }
+
+ set newsrcRootPath(value) {
+ this.setFileValue("mail.newsrc_root-rel", "mail.newsrc_root", value);
+ }
+
+ get newsrcFilePath() {
+ if (!this._newsrcFilePath) {
+ this._newsrcFilePath = this.getFileValue(
+ "newsrc.file-rel",
+ "newsrc.file"
+ );
+ }
+ if (!this._newsrcFilePath) {
+ let prefix = "newsrc-";
+ let suffix = "";
+ if (AppConstants.platform == "win") {
+ prefix = "";
+ suffix = ".rc";
+ }
+ this._newsrcFilePath = this.newsrcRootPath;
+ this._newsrcFilePath.append(`${prefix}${this.hostName}${suffix}`);
+ this._newsrcFilePath.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ this.newsrcFilePath = this._newsrcFilePath;
+ }
+ return this._newsrcFilePath;
+ }
+
+ set newsrcFilePath(value) {
+ this._newsrcFilePath = value;
+ if (!this._newsrcFilePath.exists) {
+ this._newsrcFilePath.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ }
+ this.setFileValue("newsrc.file-rel", "newsrc.file", this._newsrcFilePath);
+ }
+
+ addNewsgroupToList(name) {
+ name = new TextDecoder(this.charset).decode(
+ lazy.CommonUtils.byteStringToArrayBuffer(name)
+ );
+ this.addTo(name, false, true, true);
+ }
+
+ addNewsgroup(name) {
+ this._subscribed.add(name);
+ }
+
+ removeNewsgroup(name) {
+ this._subscribed.delete(name);
+ }
+
+ containsNewsgroup(name) {
+ // Get subFolders triggers populating _subscribed if it wasn't set already.
+ if (this._subscribed.size == 0) {
+ this.rootFolder.QueryInterface(Ci.nsIMsgNewsFolder).subFolders;
+ }
+ return this._subscribed.has(name);
+ }
+
+ subscribeToNewsgroup(name) {
+ if (this.containsNewsgroup(name)) {
+ return;
+ }
+ this.rootMsgFolder.createSubfolder(name, null);
+ }
+
+ writeNewsrcFile() {
+ if (!this.newsrcHasChanged) {
+ return;
+ }
+
+ let newsFolder = this.rootFolder.QueryInterface(Ci.nsIMsgNewsFolder);
+ let lines = [];
+ for (let folder of newsFolder.subFolders) {
+ folder = folder.QueryInterface(Ci.nsIMsgNewsFolder);
+ if (folder.newsrcLine) {
+ lines.push(folder.newsrcLine);
+ }
+ }
+ IOUtils.writeUTF8(this.newsrcFilePath.path, lines.join(""));
+ }
+
+ findGroup(name) {
+ return this.rootMsgFolder
+ .findSubFolder(name)
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ }
+
+ loadNewsUrl(uri, msgWindow, consumer) {
+ if (consumer instanceof Ci.nsIStreamListener) {
+ this.withClient(client => {
+ client.loadNewsUrl(uri.spec, msgWindow, consumer);
+ });
+ }
+ }
+
+ forgetPassword() {
+ let newsFolder = this.rootFolder.QueryInterface(Ci.nsIMsgNewsFolder);
+ // Clear password of root folder.
+ newsFolder.forgetAuthenticationCredentials();
+
+ // Clear password of all sub folders.
+ for (let folder of newsFolder.subFolders) {
+ folder.QueryInterface(Ci.nsIMsgNewsFolder);
+ folder.forgetAuthenticationCredentials();
+ }
+ }
+
+ groupNotFound(msgWindow, groupName, opening) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let result = Services.prompt.confirm(
+ msgWindow,
+ null,
+ bundle.formatStringFromName("autoUnsubscribeText", [
+ groupName,
+ this.hostName,
+ ])
+ );
+ if (result) {
+ this.unsubscribe(groupName);
+ }
+ }
+
+ _lineSeparator = AppConstants.platform == "win" ? "\r\n" : "\n";
+
+ /**
+ * startPopulating as an async function.
+ *
+ * @see startPopulating
+ */
+ async _startPopulating(msgWindow, forceToServer, getOnlyNew) {
+ this._msgWindow = msgWindow;
+ this._subscribable.startPopulating(msgWindow, forceToServer, getOnlyNew);
+ this._groups = [];
+
+ this._hostInfoLoaded = false;
+ if (!forceToServer) {
+ this._hostInfoLoaded = await this._loadHostInfo();
+ if (this._hostInfoLoaded) {
+ this.stopPopulating(msgWindow);
+ return;
+ }
+ }
+ this._hostInfoChanged = !getOnlyNew;
+ MailServices.nntp.getListOfGroupsOnServer(this, msgWindow, getOnlyNew);
+ }
+
+ /**
+ * Try to load groups from hostinfo.dat.
+ *
+ * @returns {boolean} Returns false if hostinfo.dat doesn't exist or doesn't
+ * contain any group.
+ */
+ async _loadHostInfo() {
+ this._hostInfoFile = this.localPath;
+ this._hostInfoFile.append("hostinfo.dat");
+ if (!this._hostInfoFile.exists()) {
+ return false;
+ }
+ let content = await IOUtils.readUTF8(this._hostInfoFile.path);
+ let groupLine = false;
+ for (let line of content.split(this._lineSeparator)) {
+ if (groupLine) {
+ this.addTo(line, false, true, true);
+ } else if (line == "begingroups") {
+ groupLine = true;
+ }
+ }
+ return this._groups.length;
+ }
+
+ /**
+ * Save this._groups to hostinfo.dat.
+ */
+ async _saveHostInfo() {
+ if (!this._hostInfoChanged) {
+ return;
+ }
+
+ let lines = [
+ "# News host information file.",
+ "# This is a generated file! Do not edit.",
+ "",
+ "version=2",
+ `newsrcname=${this.hostName}`,
+ `lastgroupdate=${Math.floor(Date.now() / 1000)}`,
+ "uniqueid=0",
+ "",
+ "begingroups",
+ ...this._groups,
+ ];
+ await IOUtils.writeUTF8(
+ this._hostInfoFile.path,
+ lines.join(this._lineSeparator) + this._lineSeparator
+ );
+ }
+
+ /**
+ * Get an idle connection that can be used.
+ *
+ * @returns {NntpClient}
+ */
+ async _getNextClient() {
+ // The newest connection is the least likely to have timed out.
+ let client = this._idleConnections.pop();
+ if (client) {
+ this._busyConnections.push(client);
+ return client;
+ }
+ if (
+ this._idleConnections.length + this._busyConnections.length <
+ this.maximumConnectionsNumber
+ ) {
+ // Create a new client if the pool is not full.
+ client = new lazy.NntpClient(this);
+ this._busyConnections.push(client);
+ return client;
+ }
+ // Wait until a connection is available.
+ await new Promise(resolve => this._connectionWaitingQueue.push(resolve));
+ return this._getNextClient();
+ }
+
+ /**
+ * Do some actions with a connection.
+ *
+ * @param {Function} handler - A callback function to take a NntpClient
+ * instance, and do some actions.
+ */
+ async withClient(handler) {
+ let client = await this._getNextClient();
+ client.onIdle = () => {
+ this._busyConnections = this._busyConnections.filter(c => c != client);
+ this._idleConnections.push(client);
+ // Resovle the first waiting in queue.
+ this._connectionWaitingQueue.shift()?.();
+ };
+ handler(client);
+ client.connect();
+ }
+}
+
+NntpIncomingServer.prototype.classID = Components.ID(
+ "{dc4ad42f-bc98-4193-a469-0cfa95ed9bcb}"
+);
diff --git a/comm/mailnews/news/src/NntpMessageService.jsm b/comm/mailnews/news/src/NntpMessageService.jsm
new file mode 100644
index 0000000000..dcfac7570a
--- /dev/null
+++ b/comm/mailnews/news/src/NntpMessageService.jsm
@@ -0,0 +1,272 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["NntpMessageService", "NewsMessageService"];
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const lazy = {};
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ NntpChannel: "resource:///modules/NntpChannel.jsm",
+ NntpUtils: "resource:///modules/NntpUtils.jsm",
+});
+
+/**
+ * A message service for news-message://, mainly used for displaying messages.
+ *
+ * @implements {nsIMsgMessageService}
+ * @implements {nsIMsgMessageFetchPartService}
+ */
+class BaseMessageService {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMsgMessageService",
+ "nsIMsgMessageFetchPartService",
+ ]);
+
+ _logger = lazy.NntpUtils.logger;
+
+ /** @see nsIMsgMessageService */
+ loadMessage(
+ messageURI,
+ displayConsumer,
+ msgWindow,
+ urlListener,
+ autodetectCharset
+ ) {
+ this._logger.debug("loadMessage", messageURI);
+
+ let uri = this.getUrlForUri(messageURI, msgWindow);
+ if (urlListener) {
+ uri.RegisterListener(urlListener);
+ }
+ if (displayConsumer instanceof Ci.nsIDocShell) {
+ uri.loadURI(
+ displayConsumer.QueryInterface(Ci.nsIDocShell),
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE
+ );
+ } else {
+ let streamListener = displayConsumer.QueryInterface(Ci.nsIStreamListener);
+ let channel = new lazy.NntpChannel(uri);
+ channel.asyncOpen(streamListener);
+ }
+ }
+
+ /**
+ * @param {string} messageURI - Message URI.
+ * @param {?nsIMsgWindow} [msgWindow] - Message window.
+ * @returns {nsIURI}
+ */
+ getUrlForUri(messageURI, msgWindow) {
+ let uri = Services.io
+ .newURI(this._createMessageIdUrl(messageURI))
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ uri.msgWindow = msgWindow;
+ uri.QueryInterface(Ci.nsIMsgMessageUrl).originalSpec = messageURI;
+ uri.QueryInterface(Ci.nsINntpUrl).newsAction =
+ Ci.nsINntpUrl.ActionFetchArticle;
+ return uri;
+ }
+
+ /**
+ * @param {string} uri - The message URI.
+ * @returns {?nsIMsgDBHdr} The message for the URI, or null.
+ */
+ messageURIToMsgHdr(uri) {
+ let [folder, key] = this._decomposeNewsMessageURI(uri);
+ return folder?.GetMessageHeader(key);
+ }
+
+ copyMessage(messageUri, copyListener, moveMessage, urlListener, msgWindow) {
+ this._logger.debug("copyMessage", messageUri);
+ this.loadMessage(messageUri, copyListener, msgWindow, urlListener, false);
+ }
+
+ SaveMessageToDisk(
+ messageUri,
+ file,
+ addDummyEnvelope,
+ urlListener,
+ outUrl,
+ canonicalLineEnding,
+ msgWindow
+ ) {
+ this._logger.debug("SaveMessageToDisk", messageUri);
+ let url = this.getUrlForUri(messageUri, msgWindow);
+ if (urlListener) {
+ url.RegisterListener(urlListener);
+ }
+ url.newsAction = Ci.nsINntpUrl.ActionSaveMessageToDisk;
+ url.AddDummyEnvelope = addDummyEnvelope;
+ url.canonicalLineEnding = canonicalLineEnding;
+
+ let [folder, key] = this._decomposeNewsMessageURI(messageUri);
+ if (folder && folder.QueryInterface(Ci.nsIMsgNewsFolder)) {
+ url.msgIsInLocalCache = folder.hasMsgOffline(key);
+ }
+
+ this.loadMessage(
+ messageUri,
+ url.getSaveAsListener(addDummyEnvelope, file),
+ msgWindow,
+ urlListener,
+ false
+ );
+ }
+
+ Search(searchSession, msgWindow, msgFolder, searchUri) {
+ let slashIndex = searchUri.indexOf("/");
+ let xpatLines = searchUri.slice(slashIndex + 1).split("/");
+ let server = msgFolder.server.QueryInterface(Ci.nsINntpIncomingServer);
+
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(
+ searchSession.QueryInterface(Ci.nsIUrlListener),
+ msgWindow
+ );
+ client.onOpen = () => {
+ client.search(msgFolder.name, xpatLines);
+ };
+
+ client.onData = line => {
+ searchSession.runningAdapter.AddHit(line.split(" ")[0]);
+ };
+ });
+ }
+
+ streamMessage(
+ messageUri,
+ consumer,
+ msgWindow,
+ urlListener,
+ convertData,
+ additionalHeader
+ ) {
+ this._logger.debug("streamMessage", messageUri);
+ let [folder, key] = this._decomposeNewsMessageURI(messageUri);
+
+ let uri = this.getUrlForUri(messageUri, msgWindow);
+ if (additionalHeader) {
+ // NOTE: jsmimeemitter relies on this.
+ let url = new URL(uri.spec);
+ let params = new URLSearchParams(`?header=${additionalHeader}`);
+ for (let [key, value] of params.entries()) {
+ url.searchParams.set(key, value);
+ }
+ uri = uri.mutate().setQuery(url.search).finalize();
+ }
+
+ uri = uri.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ uri.msgIsInLocalCache = folder.hasMsgOffline(key);
+ if (urlListener) {
+ uri.RegisterListener(urlListener);
+ }
+
+ let streamListener = consumer.QueryInterface(Ci.nsIStreamListener);
+ let channel = new lazy.NntpChannel(uri.QueryInterface(Ci.nsINntpUrl));
+ let listener = streamListener;
+ if (convertData) {
+ let converter = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ listener = converter.asyncConvertData(
+ "message/rfc822",
+ "*/*",
+ streamListener,
+ channel
+ );
+ }
+ channel.asyncOpen(listener);
+ return uri;
+ }
+
+ /**
+ * Parse a message uri to folder and message key.
+ *
+ * @param {string} uri - The news-message:// url to parse.
+ * @returns {[nsIMsgFolder, string]} - The folder and message key.
+ */
+ _decomposeNewsMessageURI(uri) {
+ let host, groupName, key;
+ if (uri.startsWith("news-message://")) {
+ let matches = /news-message:\/\/([^:]+)\/(.+)#(\d+)/.exec(uri);
+ if (!matches) {
+ throw Components.Exception(
+ `Failed to parse message url: ${uri}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ [, host, groupName, key] = matches;
+ if (host.includes("@")) {
+ host = host.slice(host.indexOf("@") + 1);
+ }
+ } else {
+ let url = new URL(uri);
+ host = url.hostname;
+ groupName = url.searchParams.get("group");
+ key = url.searchParams.get("key");
+ }
+ groupName = groupName ? decodeURIComponent(groupName) : null;
+ let server = MailServices.accounts
+ .findServer("", host, "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ let folder;
+ if (groupName) {
+ folder = server.rootFolder
+ .getChildNamed(groupName)
+ .QueryInterface(Ci.nsIMsgNewsFolder);
+ }
+ return [folder, key];
+ }
+
+ /**
+ * Create a news:// url from a news-message:// url.
+ *
+ * @param {string} messageURI - The news-message:// url.
+ * @returns {string} The news:// url.
+ */
+ _createMessageIdUrl(messageURI) {
+ if (messageURI.startsWith("news://")) {
+ return messageURI;
+ }
+ let [folder, key] = this._decomposeNewsMessageURI(messageURI);
+ let host = folder.rootFolder.URI;
+ let messageId = folder.getMessageIdForKey(key);
+ let url = new URL(`${host}/${encodeURIComponent(messageId)}`);
+ url.searchParams.set("group", folder.name);
+ url.searchParams.set("key", key);
+ if (!url.port) {
+ url.port = folder.server.port;
+ }
+ return url.toString();
+ }
+
+ /** @see nsIMsgMessageFetchPartService */
+ fetchMimePart(uri, messageUri, displayConsumer, msgWindow, urlListener) {
+ this._logger.debug("fetchMimePart", uri.spec);
+ this.loadMessage(uri.spec, displayConsumer, msgWindow, urlListener, false);
+ }
+}
+
+/**
+ * A message service for news-message://, mainly for displaying messages.
+ */
+class NntpMessageService extends BaseMessageService {}
+
+NntpMessageService.prototype.classID = Components.ID(
+ "{9cefbe67-5966-4f8a-b7b0-cedd60a02c8e}"
+);
+
+/**
+ * A message service for news://, mainly for handling attachments.
+ */
+class NewsMessageService extends BaseMessageService {}
+
+NewsMessageService.prototype.classID = Components.ID(
+ "{4cae5569-2c72-4910-9f3d-774f9e939df8}"
+);
diff --git a/comm/mailnews/news/src/NntpNewsGroup.jsm b/comm/mailnews/news/src/NntpNewsGroup.jsm
new file mode 100644
index 0000000000..e4df659802
--- /dev/null
+++ b/comm/mailnews/news/src/NntpNewsGroup.jsm
@@ -0,0 +1,420 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["NntpNewsGroup"];
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { MsgKeySet } = ChromeUtils.import("resource:///modules/MsgKeySet.jsm");
+
+/**
+ * A helper class for NntpClient to deal with msg db and folders.
+ */
+class NntpNewsGroup {
+ /**
+ * @param {nsINntpIncomingServer} server - The associated server instance.
+ * @param {nsIMsgNewsFolder} folder - The associated news folder.
+ */
+ constructor(server, folder) {
+ this._server = server;
+ this._folder = folder;
+ this._db = this._folder.msgDatabase;
+ this._msgHdrs = [];
+ }
+
+ /**
+ * @type {boolean} value - Whether to fetch old messages.
+ */
+ set getOldMessages(value) {
+ this._getOldMessages = value;
+ }
+
+ /**
+ * Get the articles range to fetch, depending on server setting and user
+ * selection.
+ *
+ * @type {nsIMsgWindow} msgWindow - The associated msg window.
+ * @type {number} firstPossible - The first article that can be fetched.
+ * @type {number} lastPossible - The last article that can be fetched.
+ * @returns {[number, number]} A tuple of the first and last article to fetch.
+ */
+ getArticlesRangeToFetch(msgWindow, firstPossible, lastPossible) {
+ this._msgWindow = msgWindow;
+ if (!this._msgWindow) {
+ try {
+ this._msgWindow = MailServices.mailSession.topmostMsgWindow;
+ } catch (e) {}
+ }
+
+ this._folderFilterList = this._folder.getFilterList(this._msgWindow);
+ this._serverFilterList = this._server.getFilterList(this._msgWindow);
+ this._filterHeaders = new Set(
+ (
+ this._folderFilterList.arbitraryHeaders +
+ " " +
+ this._serverFilterList.arbitraryHeaders
+ )
+ .split(" ")
+ .filter(Boolean)
+ );
+
+ let groupInfo = this._db.dBFolderInfo;
+ if (groupInfo) {
+ if (lastPossible < groupInfo.highWater) {
+ groupInfo.highWater = lastPossible;
+ }
+ this._knownKeySet = new MsgKeySet(groupInfo.knownArtsSet);
+ } else {
+ this._knownKeySet = new MsgKeySet();
+ this._knownKeySet.addRange(
+ this._db.lowWaterArticleNum,
+ this._db.highWaterArticleNum
+ );
+ }
+ if (this._knownKeySet.has(lastPossible)) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let messengerBundle = Services.strings.createBundle(
+ "chrome://messenger/locale/messenger.properties"
+ );
+ msgWindow?.statusFeedback.showStatusString(
+ messengerBundle.formatStringFromName("statusMessage", [
+ this._server.prettyName,
+ bundle.GetStringFromName("noNewMessages"),
+ ])
+ );
+ }
+
+ if (this._getOldMessages || !this._knownKeySet.has(lastPossible)) {
+ let [start, end] = this._knownKeySet.getLastMissingRange(
+ firstPossible,
+ lastPossible
+ );
+ if (this._getOldMessages) {
+ return [Math.max(start, end - this._server.maxArticles + 1), end];
+ }
+ if (
+ start &&
+ end - start > this._server.maxArticles &&
+ this._server.notifyOn
+ ) {
+ // Show a dialog to let user decide how many articles to download.
+ let args = Cc[
+ "@mozilla.org/messenger/newsdownloaddialogargs;1"
+ ].createInstance(Ci.nsINewsDownloadDialogArgs);
+ args.articleCount = end - start + 1;
+ args.groupName = this._folder.unicodeName;
+ args.serverKey = this._server.key;
+ this._msgWindow.domWindow.openDialog(
+ "chrome://messenger/content/downloadheaders.xhtml",
+ "_blank",
+ "centerscreen,chrome,modal,titlebar",
+ args
+ );
+ if (!args.hitOK) {
+ return [];
+ }
+ start = args.downloadAll ? start : end - this._server.maxArticles + 1;
+ if (this._server.markOldRead) {
+ this._readKeySet = new MsgKeySet(
+ this._folder.newsrcLine.split(":")[1].trim()
+ );
+ this._readKeySet.addRange(firstPossible, start - 1);
+ }
+ }
+ return [start, end];
+ }
+ return [];
+ }
+
+ /**
+ * Strip multiple localized Re: prefixes and set the subject and the hasRe
+ * flag. This emulates NS_MsgStripRE()
+ *
+ * @param {nsIMsgDBHdr} msgHdr - The nsIMsgDBHdr to update
+ * @param {string} subject - The unprocessed subject
+ */
+ setSubject(msgHdr, subject) {
+ let prefixes = Services.prefs
+ .getComplexValue("mailnews.localizedRe", Ci.nsIPrefLocalizedString)
+ .data.split(",")
+ .filter(Boolean);
+ if (!prefixes.includes("Re")) {
+ prefixes.push("Re");
+ }
+ // Construct a regular expression like this: ^(Re: |Aw: )+
+ let newSubject = subject.replace(
+ new RegExp(`^(${prefixes.join(": |")}: )+`, "i"),
+ ""
+ );
+ msgHdr.subject = newSubject;
+ if (newSubject != subject) {
+ msgHdr.orFlags(Ci.nsMsgMessageFlags.HasRe);
+ }
+ }
+
+ /**
+ * Parse an XOVER line to a msg hdr.
+ *
+ * @param {string} line - An XOVER response line.
+ */
+ processXOverLine(line) {
+ let parts = line.split("\t");
+ if (parts.length < 8) {
+ return;
+ }
+ let [
+ articleNumber,
+ subject,
+ from,
+ date,
+ messageId,
+ references,
+ bytes,
+ lines,
+ ] = parts;
+ let msgHdr = this._db.createNewHdr(articleNumber);
+ msgHdr.orFlags(Ci.nsMsgMessageFlags.New);
+ this.setSubject(msgHdr, subject);
+ msgHdr.author = from;
+ msgHdr.date = new Date(date).valueOf() * 1000;
+ msgHdr.messageId = messageId;
+ msgHdr.setReferences(references);
+ msgHdr.messageSize = bytes;
+ msgHdr.lineCount = lines;
+ this._msgHdrs.push(msgHdr);
+ }
+
+ /**
+ * Add a range (usually XOVER range) to the known key set.
+ */
+ addKnownArticles(start, end) {
+ this._knownKeySet.addRange(start, end);
+ }
+
+ /**
+ * Finish processing XOVER responses.
+ */
+ finishProcessingXOver() {
+ this._runFilters();
+ let groupInfo = this._db.dBFolderInfo;
+ if (groupInfo) {
+ groupInfo.knownArtsSet = this._knownKeySet.toString();
+ }
+ }
+
+ /**
+ * Extra headers needed by filters, but not returned in XOVER response.
+ */
+ getXHdrFields() {
+ return [...this._filterHeaders].filter(
+ x => !["message-id", "references"].includes(x)
+ );
+ }
+
+ /**
+ * Update msgHdr according to XHDR line.
+ *
+ * @param {string} header - The requested header.
+ * @param {string} line - A XHDR response line.
+ */
+ processXHdrLine(header, line) {
+ let spaceIndex = line.indexOf(" ");
+ let articleNumber = line.slice(0, spaceIndex);
+ let value = line.slice(spaceIndex).trim();
+ let msgHdr = this._db.getMsgHdrForKey(articleNumber);
+ msgHdr.setStringProperty(header, value);
+ }
+
+ /**
+ * Init a msgHdr to prepare to take HEAD response.
+ *
+ * @param {number} articleNumber - The article number.
+ */
+ initHdr(articleNumber) {
+ if (this._msgHdr) {
+ this._msgHdrs.push(this._msgHdr);
+ }
+
+ if (articleNumber >= 0) {
+ this._msgHdr = this._db.createNewHdr(articleNumber);
+ }
+ }
+
+ /**
+ * Update msgHdr according to HEAD line.
+ *
+ * @param {string} line - A HEAD response line.
+ */
+ processHeadLine(line) {
+ let colonIndex = line.indexOf(":");
+ let name = line.slice(0, colonIndex);
+ let value = line.slice(colonIndex + 1).trim();
+ switch (name) {
+ case "from":
+ this._msgHdr.author = value;
+ break;
+ case "date":
+ this._msgHdr.date = new Date(value).valueOf() * 1000;
+ break;
+ case "subject":
+ this.setSubject(this._msgHdr, value);
+ this._msgHdr.orFlags(Ci.nsMsgMessageFlags.New);
+ break;
+ case "message-id":
+ this._msgHdr.messageId = value;
+ break;
+ case "references":
+ this._msgHdr.setReferences(value);
+ break;
+ case "bytes":
+ this._msgHdr.messageSize = value;
+ break;
+ case "lines":
+ this._msgHdr.lineCount = value;
+ break;
+ default:
+ if (this._filterHeaders.has(name)) {
+ this._msgHdr.setStringProperty(name, value);
+ }
+ }
+ }
+
+ /**
+ * Run filters to all newly added msg hdrs.
+ */
+ _runFilters() {
+ let folderFilterCount = this._folderFilterList.filterCount;
+ let serverFilterCount = this._serverFilterList.filterCount;
+
+ for (let msgHdr of this._msgHdrs) {
+ this._filteringHdr = msgHdr;
+ this._addHdrToDB = true;
+ let headers = "";
+ if (folderFilterCount || serverFilterCount) {
+ let author = this._filteringHdr.author;
+ let subject = this._filteringHdr.subject;
+ if (author) {
+ headers += `From: ${author}\0`;
+ }
+ if (subject) {
+ headers += `Subject: ${subject}\0`;
+ }
+ }
+ if (folderFilterCount) {
+ this._folderFilterList.applyFiltersToHdr(
+ Ci.nsMsgFilterType.NewsRule,
+ msgHdr,
+ this._folder,
+ this._db,
+ headers,
+ this,
+ this._msgWindow
+ );
+ }
+ if (serverFilterCount) {
+ this._serverFilterList.applyFiltersToHdr(
+ Ci.nsMsgFilterType.NewsRule,
+ msgHdr,
+ this._folder,
+ this._db,
+ headers,
+ this,
+ this._msgWindow
+ );
+ }
+ if (this._addHdrToDB && !this._db.containsKey(msgHdr.messageKey)) {
+ this._db.addNewHdrToDB(msgHdr, true);
+ MailServices.mfn.notifyMsgAdded(msgHdr);
+ this._folder.orProcessingFlags(
+ msgHdr.messageKey,
+ Ci.nsMsgProcessingFlags.NotReportedClassified
+ );
+ }
+ }
+ }
+
+ /**
+ * Callback of nsIMsgFilterList.applyFiltersToHdr.
+ *
+ * @see nsIMsgFilterHitNotify
+ */
+ applyFilterHit(filter, msgWindow) {
+ let loggingEnabled = filter.filterList.loggingEnabled;
+ let applyMore = true;
+
+ for (let action of filter.sortedActionList) {
+ if (loggingEnabled) {
+ filter.logRuleHit(action, this._filteringHdr);
+ }
+ switch (action.type) {
+ case Ci.nsMsgFilterAction.Delete:
+ this._addHdrToDB = false;
+ break;
+ case Ci.nsMsgFilterAction.MarkRead:
+ this._db.markHdrRead(this._filteringHdr, true, null);
+ break;
+ case Ci.nsMsgFilterAction.MarkUnread:
+ this._db.markHdrRead(this._filteringHdr, false, null);
+ break;
+ case Ci.nsMsgFilterAction.KillThread:
+ this._filteringHdr.setUint32Property(
+ "ProtoThreadFlags",
+ Ci.nsMsgMessageFlags.Ignored
+ );
+ break;
+ case Ci.nsMsgFilterAction.KillSubthread:
+ this._filteringHdr.orFlags(Ci.nsMsgMessageFlags.Ignored);
+ break;
+ case Ci.nsMsgFilterAction.WatchThread:
+ this._filteringHdr.orFlags(Ci.nsMsgMessageFlags.Watched);
+ break;
+ case Ci.nsMsgFilterAction.MarkFlagged:
+ this._filteringHdr.markFlagged(true);
+ break;
+ case Ci.nsMsgFilterAction.ChangePriority:
+ this._filteringHdr.priority = action.priority;
+ break;
+ case Ci.nsMsgFilterAction.AddTag:
+ this._folder.addKeywordsToMessages(
+ [this._filteringHdr],
+ action.strValue
+ );
+ break;
+ case Ci.nsMsgFilterAction.StopExecution:
+ applyMore = false;
+ break;
+ case Ci.nsMsgFilterAction.Custom:
+ action.customAction.applyAction(
+ [this._filteringHdr],
+ action.strValue,
+ null,
+ Ci.nsMsgFilterType.NewsRule,
+ msgWindow
+ );
+ break;
+ default:
+ throw Components.Exception(
+ `Unexpected filter action type=${action.type}`,
+ Cr.NS_ERROR_UNEXPECTED
+ );
+ }
+ }
+ return applyMore;
+ }
+
+ /**
+ * Commit changes to msg db.
+ */
+ cleanUp() {
+ if (this._readKeySet) {
+ this._folder.setReadSetFromStr(this._readKeySet);
+ }
+ this._folder.notifyFinishedDownloadinghdrs();
+ this._db.commit(Ci.nsMsgDBCommitType.kSessionCommit);
+ this._db.close(true);
+ }
+}
diff --git a/comm/mailnews/news/src/NntpProtocolHandler.jsm b/comm/mailnews/news/src/NntpProtocolHandler.jsm
new file mode 100644
index 0000000000..00e5dc224b
--- /dev/null
+++ b/comm/mailnews/news/src/NntpProtocolHandler.jsm
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["NewsProtocolHandler", "SnewsProtocolHandler"];
+
+var { NntpChannel } = ChromeUtils.import("resource:///modules/NntpChannel.jsm");
+
+/**
+ * @implements {nsIProtocolHandler}
+ */
+class NewsProtocolHandler {
+ QueryInterface = ChromeUtils.generateQI(["nsIProtocolHandler"]);
+
+ scheme = "news";
+
+ newChannel(uri, loadInfo) {
+ let channel = new NntpChannel(uri, loadInfo);
+ let spec = uri.spec;
+ if (
+ spec.includes("part=") &&
+ !spec.includes("type=message/rfc822") &&
+ !spec.includes("type=application/x-message-display") &&
+ !spec.includes("type=application/pdf")
+ ) {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_ATTACHMENT;
+ } else {
+ channel.contentDisposition = Ci.nsIChannel.DISPOSITION_INLINE;
+ }
+ return channel;
+ }
+
+ allowPort(port, scheme) {
+ return true;
+ }
+}
+NewsProtocolHandler.prototype.classID = Components.ID(
+ "{24220ecd-cb05-4676-8a47-fa1da7b86e6e}"
+);
+
+class SnewsProtocolHandler extends NewsProtocolHandler {
+ scheme = "snews";
+}
+SnewsProtocolHandler.prototype.classID = Components.ID(
+ "{1895016d-5302-46a9-b3f5-9c47694d9eca}"
+);
diff --git a/comm/mailnews/news/src/NntpProtocolInfo.jsm b/comm/mailnews/news/src/NntpProtocolInfo.jsm
new file mode 100644
index 0000000000..3a1bfeb887
--- /dev/null
+++ b/comm/mailnews/news/src/NntpProtocolInfo.jsm
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["NntpProtocolInfo"];
+
+var { MsgProtocolInfo } = ChromeUtils.importESModule(
+ "resource:///modules/MsgProtocolInfo.sys.mjs"
+);
+
+/**
+ * @implements {nsIMsgProtocolInfo}
+ */
+class NntpProtocolInfo extends MsgProtocolInfo {
+ QueryInterface = ChromeUtils.generateQI(["nsIMsgProtocolInfo"]);
+
+ serverIID = Components.ID("{dc4ad42f-bc98-4193-a469-0cfa95ed9bcb}");
+
+ requiresUsername = false;
+ preflightPrettyNameWithEmailAddress = false;
+ canDelete = true;
+ canLoginAtStartUp = true;
+ canDuplicate = true;
+ canGetMessages = true;
+ canGetIncomingMessages = false;
+ defaultDoBiff = false;
+ showComposeMsgLink = false;
+ foldersCreatedAsync = false;
+
+ getDefaultServerPort(isSecure) {
+ return isSecure
+ ? Ci.nsINntpUrl.DEFAULT_NNTPS_PORT
+ : Ci.nsINntpUrl.DEFAULT_NNTP_PORT;
+ }
+
+ // @see MsgProtocolInfo.sys.mjs
+ RELATIVE_PREF = "mail.root.nntp-rel";
+ ABSOLUTE_PREF = "mail.root.nntp";
+ DIR_SERVICE_PROP = "NewsD";
+}
+
+NntpProtocolInfo.prototype.classID = Components.ID(
+ "{7d71db22-0624-4c9f-8d70-dea6ab3ff076}"
+);
diff --git a/comm/mailnews/news/src/NntpService.jsm b/comm/mailnews/news/src/NntpService.jsm
new file mode 100644
index 0000000000..cae1cd9002
--- /dev/null
+++ b/comm/mailnews/news/src/NntpService.jsm
@@ -0,0 +1,250 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["NntpService"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * @implements {nsINntpService}
+ */
+class NntpService {
+ QueryInterface = ChromeUtils.generateQI(["nsINntpService"]);
+
+ get cacheStorage() {
+ if (!this._cacheStorage) {
+ this._cacheStorage = Services.cache2.memoryCacheStorage(
+ Services.loadContextInfo.custom(false, {})
+ );
+ }
+ return this._cacheStorage;
+ }
+
+ generateNewsHeaderValsForPosting(
+ newsgroupsList,
+ outNewsgroupsHeader,
+ outNewsHostHeader
+ ) {
+ let groups = newsgroupsList.split(",");
+ outNewsgroupsHeader.value = newsgroupsList;
+ let hosts = groups.map(name => this._findHostFromGroupName(name));
+ hosts = [...new Set(hosts)].filter(Boolean);
+ let host = hosts[0];
+ if (!host) {
+ outNewsHostHeader.value = "";
+ return;
+ }
+ if (hosts.length > 1) {
+ throw Components.Exception(
+ `Cross posting not allowed, hosts=${hosts.join(",")}`,
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ outNewsHostHeader.value = host;
+ }
+
+ postMessage(messageFile, groupNames, accountKey, urlListener, msgWindow) {
+ let server = MailServices.accounts.getAccount(accountKey)?.incomingServer;
+ if (!server) {
+ // If no matching server, find the first news server and use it.
+ server = MailServices.accounts.findServer("", "", "nntp");
+ }
+ server = server.QueryInterface(Ci.nsINntpIncomingServer);
+
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(urlListener, msgWindow);
+
+ client.onOpen = () => {
+ client.post();
+ };
+
+ client.onReadyToPost = () => {
+ let fstream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ // PR_RDONLY
+ fstream.init(messageFile, 0x01, 0, 0);
+ let lineInputStream = fstream.QueryInterface(Ci.nsILineInputStream);
+ let hasMore;
+ do {
+ let outLine = {};
+ hasMore = lineInputStream.readLine(outLine);
+ let line = outLine.value;
+ if (line.startsWith(".")) {
+ // Dot stuffing, see rfc3977#section-3.1.1.
+ line = "." + line;
+ }
+ client.send(line + "\r\n");
+ } while (hasMore);
+ fstream.close();
+ client.send(".\r\n");
+ };
+ });
+ }
+
+ getNewNews(server, uri, getOld, urlListener, msgWindow) {
+ if (Services.io.offline) {
+ const NS_MSG_ERROR_OFFLINE = 0x80550014;
+ // @see nsMsgNewsFolder::UpdateFolder
+ throw Components.Exception(
+ "Cannot get news while offline",
+ NS_MSG_ERROR_OFFLINE
+ );
+ }
+ // The uri is in the form of news://news.mozilla.org/mozilla.accessibility
+ let matches = /.+:\/\/([^:]+):?(\d+)?\/(.+)?/.exec(uri);
+ let groupName = decodeURIComponent(matches[3]);
+
+ let runningUri = Services.io
+ .newURI(uri)
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(urlListener, msgWindow, runningUri);
+ client.onOpen = () => {
+ client.getNewNews(groupName, getOld);
+ };
+ });
+
+ return runningUri;
+ }
+
+ getListOfGroupsOnServer(server, msgWindow, getOnlyNew) {
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(null, msgWindow);
+ client.onOpen = () => {
+ client.getListOfGroups(getOnlyNew);
+ };
+
+ client.onData = data => {
+ server.addNewsgroupToList(data.split(" ")[0]);
+ };
+ });
+ }
+
+ fetchMessage(folder, key, msgWindow, consumer, urlListener) {
+ let streamListener, inputStream, outputStream;
+ if (consumer instanceof Ci.nsIStreamListener) {
+ streamListener = consumer;
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(true, true, 0, 0);
+ inputStream = pipe.inputStream;
+ outputStream = pipe.outputStream;
+ }
+
+ let server = folder.server.QueryInterface(Ci.nsINntpIncomingServer);
+ server.wrappedJSObject.withClient(client => {
+ client.startRunningUrl(urlListener, msgWindow);
+
+ client.onOpen = () => {
+ client.getArticleByArticleNumber(folder.name, key);
+ streamListener?.onStartRequest(null);
+ };
+ client.onData = data => {
+ outputStream?.write(data, data.length);
+ streamListener?.onDataAvailable(null, inputStream, 0, data.length);
+ };
+ client.onDone = () => {
+ streamListener?.onStopRequest(null, Cr.NS_OK);
+ };
+ });
+ }
+
+ cancelMessage(cancelUrl, messageUri, consumer, urlListener, msgWindow) {
+ if (Services.prefs.getBoolPref("news.cancel.confirm")) {
+ let bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/news.properties"
+ );
+ let result = Services.prompt.confirmEx(
+ msgWindow?.domWindow,
+ null,
+ bundle.GetStringFromName("cancelConfirm"),
+ Ci.nsIPrompt.STD_YES_NO_BUTTONS,
+ null,
+ null,
+ null,
+ null,
+ { value: false }
+ );
+ if (result != 0) {
+ // Cancelled.
+ return;
+ }
+ }
+ // The cancelUrl is in the form of "news://host/message-id?cancel"
+ let url = new URL(cancelUrl);
+ let messageId = "<" + decodeURIComponent(url.pathname.slice(1)) + ">";
+ let server = MailServices.accounts
+ .findServer("", url.host, "nntp")
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ let groupName = new URL(messageUri).pathname.slice(1);
+ let messageKey = messageUri.split("#")[1];
+ let newsFolder = server.findGroup(groupName);
+ let from = MailServices.accounts.getFirstIdentityForServer(server).email;
+ let bundle = Services.strings.createBundle(
+ "chrome://branding/locale/brand.properties"
+ );
+
+ server.wrappedJSObject.withClient(client => {
+ let runningUrl = client.startRunningUrl(urlListener, msgWindow);
+ runningUrl.msgWindow = msgWindow;
+
+ client.onOpen = () => {
+ client.cancelArticle(groupName);
+ };
+
+ client.onReadyToPost = () => {
+ let content = [
+ `From: ${from}`,
+ `Newsgroups: ${groupName}`,
+ `Subject: cancel ${messageId}`,
+ `References: ${messageId}`,
+ `Control: cancel ${messageId}`,
+ "MIME-Version: 1.0",
+ "Content-Type: text/plain",
+ "", // body separator
+ `This message was cancelled from within ${bundle.GetStringFromName(
+ "brandFullName"
+ )}`,
+ ];
+ client.send(content.join("\r\n"));
+ client.send("\r\n.\r\n");
+
+ newsFolder.removeMessage(messageKey);
+ newsFolder.cancelComplete();
+ };
+ });
+ }
+
+ downloadNewsgroupsForOffline(msgWindow, urlListener) {
+ let { NewsDownloader } = ChromeUtils.importESModule(
+ "resource:///modules/NewsDownloader.sys.mjs"
+ );
+ let downloader = new NewsDownloader(msgWindow, urlListener);
+ downloader.start();
+ }
+
+ /**
+ * Find the hostname of a NNTP server from a group name.
+ *
+ * @param {string} groupName - The group name.
+ * @returns {string} The corresponding server host.
+ */
+ _findHostFromGroupName(groupName) {
+ for (let server of MailServices.accounts.allServers) {
+ if (
+ server instanceof Ci.nsINntpIncomingServer &&
+ server.containsNewsgroup(groupName)
+ ) {
+ return server.hostName;
+ }
+ }
+ return "";
+ }
+}
+
+NntpService.prototype.classID = Components.ID(
+ "{b13db263-a219-4168-aeaf-8266f001087e}"
+);
diff --git a/comm/mailnews/news/src/NntpUtils.jsm b/comm/mailnews/news/src/NntpUtils.jsm
new file mode 100644
index 0000000000..40cc51b993
--- /dev/null
+++ b/comm/mailnews/news/src/NntpUtils.jsm
@@ -0,0 +1,63 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["NntpUtils"];
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/**
+ * Collection of helper functions for NNTP.
+ */
+var NntpUtils = {
+ logger: console.createInstance({
+ prefix: "mailnews.nntp",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mailnews.nntp.loglevel",
+ }),
+
+ /**
+ * Find a server instance by its hostname.
+ *
+ * Sometimes we create a server instance to load a news url, this server is
+ * written to the prefs but not associated with any account. Different from
+ * nsIMsgAccountManager.findServer which can only find servers associated
+ * with accounts, this function looks for NNTP server in the mail.server.
+ * branch directly.
+ *
+ * @param {string} hostname - The hostname of the server.
+ * @returns {nsINntpIncomingServer|null}
+ */
+ findServer(hostname) {
+ let branch = Services.prefs.getBranch("mail.server.");
+
+ // Collect all the server keys.
+ let keySet = new Set();
+ for (let name of branch.getChildList("")) {
+ keySet.add(name.split(".")[0]);
+ }
+
+ // Find the NNTP server that matches the hostname.
+ hostname = hostname.toLowerCase();
+ for (let key of keySet) {
+ let type = branch.getCharPref(`${key}.type`, "");
+ let hostnameValue = branch
+ .getCharPref(`${key}.hostname`, "")
+ .toLowerCase();
+ if (type == "nntp" && hostnameValue == hostname) {
+ try {
+ return MailServices.accounts
+ .getIncomingServer(key)
+ .QueryInterface(Ci.nsINntpIncomingServer);
+ } catch (e) {
+ // In some profiles, two servers have the same hostname, but only one
+ // can be loaded into AccountManager. Catch the error here and the
+ // already loaded server will be found.
+ }
+ }
+ }
+ return null;
+ },
+};
diff --git a/comm/mailnews/news/src/components.conf b/comm/mailnews/news/src/components.conf
new file mode 100644
index 0000000000..502e3cd271
--- /dev/null
+++ b/comm/mailnews/news/src/components.conf
@@ -0,0 +1,98 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ "cid": "{e9bb3330-ac7e-11de-8a39-0800200c9a66}",
+ "contract_ids": ["@mozilla.org/autocomplete/search;1?name=news"],
+ "jsm": "resource:///modules/NewsAutoCompleteSearch.jsm",
+ "constructor": "NewsAutoCompleteSearch",
+ },
+ {
+ "cid": "{dc4ad42f-bc98-4193-a469-0cfa95ed9bcb}",
+ "contract_ids": ["@mozilla.org/messenger/server;1?type=nntp"],
+ "jsm": "resource:///modules/NntpIncomingServer.jsm",
+ "constructor": "NntpIncomingServer",
+ },
+ {
+ "cid": "{7d71db22-0624-4c9f-8d70-dea6ab3ff076}",
+ "contract_ids": ["@mozilla.org/messenger/protocol/info;1?type=nntp"],
+ "jsm": "resource:///modules/NntpProtocolInfo.jsm",
+ "constructor": "NntpProtocolInfo",
+ },
+ {
+ "cid": "{b13db263-a219-4168-aeaf-8266f001087e}",
+ "contract_ids": ["@mozilla.org/messenger/nntpservice;1"],
+ "jsm": "resource:///modules/NntpService.jsm",
+ "constructor": "NntpService",
+ },
+ {
+ "cid": "{9cefbe67-5966-4f8a-b7b0-cedd60a02c8e}",
+ "contract_ids": ["@mozilla.org/messenger/messageservice;1?type=news-message"],
+ "jsm": "resource:///modules/NntpMessageService.jsm",
+ "constructor": "NntpMessageService",
+ },
+ {
+ "cid": "{4cae5569-2c72-4910-9f3d-774f9e939df8}",
+ "contract_ids": ["@mozilla.org/messenger/messageservice;1?type=news"],
+ "jsm": "resource:///modules/NntpMessageService.jsm",
+ "constructor": "NewsMessageService",
+ },
+ {
+ "cid": "{24220ecd-cb05-4676-8a47-fa1da7b86e6e}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=news"],
+ "jsm": "resource:///modules/NntpProtocolHandler.jsm",
+ "constructor": "NewsProtocolHandler",
+ "protocol_config": {
+ "scheme": "news",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ "URI_LOADABLE_BY_ANYONE",
+ "ALLOWS_PROXY",
+ "URI_FORBIDS_COOKIE_ACCESS",
+ "ORIGIN_IS_FULL_SPEC",
+ ],
+ "default_port": 119,
+ },
+ },
+ {
+ "cid": "{1895016d-5302-46a9-b3f5-9c47694d9eca}",
+ "contract_ids": ["@mozilla.org/network/protocol;1?name=snews"],
+ "jsm": "resource:///modules/NntpProtocolHandler.jsm",
+ "constructor": "SnewsProtocolHandler",
+ "protocol_config": {
+ "scheme": "snews",
+ "flags": [
+ "URI_NORELATIVE",
+ "URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT",
+ "URI_LOADABLE_BY_ANYONE",
+ "ALLOWS_PROXY",
+ "URI_FORBIDS_COOKIE_ACCESS",
+ "ORIGIN_IS_FULL_SPEC",
+ ],
+ "default_port": 563,
+ },
+ },
+ {
+ "cid": "{196b4b30-e18c-11d2-806e-006008128c4e}",
+ "contract_ids": ["@mozilla.org/messenger/nntpurl;1"],
+ "type": "nsNntpUrl",
+ "headers": ["/comm/mailnews/news/src/nsNntpUrl.h"],
+ },
+ {
+ "cid": "{4ace448a-f6d4-11d2-880d-004005263078}",
+ "contract_ids": ["@mozilla.org/mail/folder-factory;1?name=news"],
+ "type": "nsMsgNewsFolder",
+ "headers": ["/comm/mailnews/news/src/nsNewsFolder.h"],
+ },
+ {
+ "cid": "{1540689e-1dd2-11b2-933d-f0d1e460ef4a}",
+ "contract_ids": ["@mozilla.org/messenger/newsdownloaddialogargs;1"],
+ "type": "nsNewsDownloadDialogArgs",
+ "headers": ["/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h"],
+ },
+]
diff --git a/comm/mailnews/news/src/moz.build b/comm/mailnews/news/src/moz.build
new file mode 100644
index 0000000000..a5f792b2cf
--- /dev/null
+++ b/comm/mailnews/news/src/moz.build
@@ -0,0 +1,32 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SOURCES += [
+ "nsNewsDownloadDialogArgs.cpp",
+ "nsNewsDownloader.cpp",
+ "nsNewsFolder.cpp",
+ "nsNewsUtils.cpp",
+ "nsNntpUrl.cpp",
+]
+
+EXTRA_JS_MODULES += [
+ "NewsAutoCompleteSearch.jsm",
+ "NewsDownloader.sys.mjs",
+ "NntpChannel.jsm",
+ "NntpClient.jsm",
+ "NntpIncomingServer.jsm",
+ "NntpMessageService.jsm",
+ "NntpNewsGroup.jsm",
+ "NntpProtocolHandler.jsm",
+ "NntpProtocolInfo.jsm",
+ "NntpService.jsm",
+ "NntpUtils.jsm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "mail"
diff --git a/comm/mailnews/news/src/nntpCore.h b/comm/mailnews/news/src/nntpCore.h
new file mode 100644
index 0000000000..52230c6931
--- /dev/null
+++ b/comm/mailnews/news/src/nntpCore.h
@@ -0,0 +1,165 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _NNTPCore_H__
+#define _NNTPCore_H__
+
+#define NEWS_MSGS_URL "chrome://messenger/locale/news.properties"
+
+// The following string constants are protocol strings. I'm defining them as
+// macros here so I don't have to sprinkle all of the strings throughout the
+// protocol.
+#define NNTP_CMD_LIST_EXTENSIONS "LIST EXTENSIONS" CRLF
+#define NNTP_CMD_MODE_READER "MODE READER" CRLF
+#define NNTP_CMD_LIST_SEARCHES "LIST SEARCHES" CRLF
+#define NNTP_CMD_LIST_SEARCH_FIELDS "LIST SRCHFIELDS" CRLF
+#define NNTP_CMD_GET_PROPERTIES "GET" CRLF
+#define NNTP_CMD_LIST_SUBSCRIPTIONS "LIST SUBSCRIPTIONS" CRLF
+#define NNTP_CMD_POST "POST" CRLF
+#define NNTP_CMD_QUIT "QUIT" CRLF
+
+// end of protocol strings
+
+#define MK_NNTP_RESPONSE_HELP 100
+
+#define MK_NNTP_RESPONSE_POSTING_ALLOWED 200
+#define MK_NNTP_RESPONSE_POSTING_DENIED 201
+
+#define MK_NNTP_RESPONSE_DISCONTINUED 400
+
+#define MK_NNTP_RESPONSE_COMMAND_UNKNOWN 500
+#define MK_NNTP_RESPONSE_SYNTAX_ERROR 501
+#define MK_NNTP_RESPONSE_PERMISSION_DENIED 502
+#define MK_NNTP_RESPONSE_SERVER_ERROR 503
+
+#define MK_NNTP_RESPONSE_ARTICLE_BOTH 220
+#define MK_NNTP_RESPONSE_ARTICLE_HEAD 221
+#define MK_NNTP_RESPONSE_ARTICLE_BODY 222
+#define MK_NNTP_RESPONSE_ARTICLE_NONE 223
+#define MK_NNTP_RESPONSE_ARTICLE_NO_GROUP 412
+#define MK_NNTP_RESPONSE_ARTICLE_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_ARTICLE_NONEXIST 423
+#define MK_NNTP_RESPONSE_ARTICLE_NOTFOUND 430
+
+#define MK_NNTP_RESPONSE_GROUP_SELECTED 211
+#define MK_NNTP_RESPONSE_GROUP_NO_GROUP 411
+
+#define MK_NNTP_RESPONSE_IHAVE_OK 235
+#define MK_NNTP_RESPONSE_IHAVE_ARTICLE 335
+#define MK_NNTP_RESPONSE_IHAVE_NOT_WANTED 435
+#define MK_NNTP_RESPONSE_IHAVE_FAILED 436
+#define MK_NNTP_RESPONSE_IHAVE_REJECTED 437
+
+#define MK_NNTP_RESPONSE_LAST_OK 223
+#define MK_NNTP_RESPONSE_LAST_NO_GROUP 412
+#define MK_NNTP_RESPONSE_LAST_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_LAST_NO_ARTICLE 422
+
+#define MK_NNTP_RESPONSE_LIST_OK 215
+
+#define MK_NNTP_RESPONSE_NEWGROUPS_OK 231
+
+#define MK_NNTP_RESPONSE_NEWNEWS_OK 230
+
+#define MK_NNTP_RESPONSE_NEXT_OK 223
+#define MK_NNTP_RESPONSE_NEXT_NO_GROUP 412
+#define MK_NNTP_RESPONSE_NEXT_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_NEXT_NO_ARTICLE 421
+
+#define MK_NNTP_RESPONSE_POST_OK 240
+#define MK_NNTP_RESPONSE_POST_SEND_NOW 340
+#define MK_NNTP_RESPONSE_POST_DENIED 440
+#define MK_NNTP_RESPONSE_POST_FAILED 441
+
+#define MK_NNTP_RESPONSE_QUIT_OK 205
+
+#define MK_NNTP_RESPONSE_SLAVE_OK 202
+
+#define MK_NNTP_RESPONSE_CHECK_NO_ARTICLE 238
+#define MK_NNTP_RESPONSE_CHECK_NO_ACCEPT 400
+#define MK_NNTP_RESPONSE_CHECK_LATER 431
+#define MK_NNTP_RESPONSE_CHECK_DONT_SEND 438
+#define MK_NNTP_RESPONSE_CHECK_DENIED 480
+#define MK_NNTP_RESPONSE_CHECK_ERROR 500
+
+#define MK_NNTP_RESPONSE_XHDR_OK 221
+#define MK_NNTP_RESPONSE_XHDR_NO_GROUP 412
+#define MK_NNTP_RESPONSE_XHDR_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_XHDR_NO_ARTICLE 430
+#define MK_NNTP_RESPONSE_XHDR_DENIED 502
+
+#define MK_NNTP_RESPONSE_XOVER_OK 224
+#define MK_NNTP_RESPONSE_XOVER_NO_GROUP 412
+#define MK_NNTP_RESPONSE_XOVER_NO_CURRENT 420
+#define MK_NNTP_RESPONSE_XOVER_DENIED 502
+
+#define MK_NNTP_RESPONSE_XPAT_OK 221
+#define MK_NNTP_RESPONSE_XPAT_NO_ARTICLE 430
+#define MK_NNTP_RESPONSE_XPAT_DENIED 502
+
+#define MK_NNTP_RESPONSE_AUTHINFO_OK 281
+#define MK_NNTP_RESPONSE_AUTHINFO_CONT 381
+#define MK_NNTP_RESPONSE_AUTHINFO_REQUIRE 480
+#define MK_NNTP_RESPONSE_AUTHINFO_REJECT 482
+#define MK_NNTP_RESPONSE_AUTHINFO_DENIED 502
+
+#define MK_NNTP_RESPONSE_
+
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_OK 250
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_CONT 350
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_REQUIRE 450
+#define MK_NNTP_RESPONSE_AUTHINFO_SIMPLE_REJECT 452
+
+#define MK_NNTP_RESPONSE_TYPE_INFO 1
+#define MK_NNTP_RESPONSE_TYPE_OK 2
+#define MK_NNTP_RESPONSE_TYPE_CONT 3
+#define MK_NNTP_RESPONSE_TYPE_CANNOT 4
+#define MK_NNTP_RESPONSE_TYPE_ERROR 5
+
+#define MK_NNTP_RESPONSE_TYPE(x) (x / 100)
+
+// the following used to be defined in allxpstr.h. Until we find a new values
+// for these, I'm defining them here because I don't want to link against
+// xplib.lib...(mscott)
+
+#define MK_DATA_LOADED 1
+#define MK_EMPTY_NEWS_LIST -227
+#define MK_INTERRUPTED -201
+#define MK_MALFORMED_URL_ERROR -209
+#define MK_NEWS_ERROR_FMT -430
+#define MK_NNTP_CANCEL_CONFIRM -426
+#define MK_NNTP_CANCEL_DISALLOWED -427
+#define MK_NNTP_NOT_CANCELLED -429
+#define MK_OUT_OF_MEMORY -207
+#define XP_CONFIRM_SAVE_NEWSGROUPS -1
+#define XP_HTML_ARTICLE_EXPIRED -1
+#define XP_HTML_NEWS_ERROR -1
+#define XP_PROGRESS_READ_NEWSGROUPINFO 1
+#define XP_PROGRESS_RECEIVE_ARTICLE 1
+#define XP_PROGRESS_RECEIVE_LISTARTICLES 1
+#define XP_PROGRESS_RECEIVE_NEWSGROUP 1
+#define XP_PROGRESS_SORT_ARTICLES 1
+#define XP_PROGRESS_READ_NEWSGROUP_COUNTS 1
+#define XP_THERMO_PERCENT_FORM 1
+#define XP_PROMPT_ENTER_USERNAME 1
+#define MK_NNTP_AUTH_FAILED -260
+#define MK_NNTP_ERROR_MESSAGE -304
+#define MK_NNTP_NEWSGROUP_SCAN_ERROR -305
+#define MK_NNTP_SERVER_ERROR -217
+#define MK_NNTP_SERVER_NOT_CONFIGURED -307
+#define MK_TCP_READ_ERROR -252
+#define MK_TCP_WRITE_ERROR -236
+#define MK_NNTP_CANCEL_ERROR -428
+#define XP_CONNECT_NEWS_HOST_CONTACTED_WAITING_FOR_REPLY 1
+#define XP_PLEASE_ENTER_A_PASSWORD_FOR_NEWS_SERVER_ACCESS 1
+#define XP_GARBAGE_COLLECTING 1
+#define XP_MESSAGE_SENT_WAITING_NEWS_REPLY 1
+#define MK_MSG_DELIV_NEWS 1
+#define MK_MSG_COLLABRA_DISABLED 1
+#define MK_MSG_EXPIRE_NEWS_ARTICLES 1
+#define MK_MSG_HTML_IMAP_NO_CACHED_BODY 1
+#define MK_MSG_CANT_MOVE_FOLDER 1
+
+#endif /* NNTPCore_H__ */
diff --git a/comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp
new file mode 100644
index 0000000000..3b91407598
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsNewsDownloadDialogArgs.h"
+
+nsNewsDownloadDialogArgs::nsNewsDownloadDialogArgs() {
+ mArticleCount = 0;
+ mServerKey = "";
+ mHitOK = false;
+ mDownloadAll = false;
+}
+
+nsNewsDownloadDialogArgs::~nsNewsDownloadDialogArgs() {}
+
+NS_IMPL_ISUPPORTS(nsNewsDownloadDialogArgs, nsINewsDownloadDialogArgs)
+
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetGroupName(nsAString& aGroupName) {
+ aGroupName = mGroupName;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetGroupName(
+ const nsAString& aGroupName) {
+ mGroupName = aGroupName;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetArticleCount(
+ int32_t* aArticleCount) {
+ NS_ENSURE_ARG_POINTER(aArticleCount);
+
+ *aArticleCount = mArticleCount;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetArticleCount(int32_t aArticleCount) {
+ mArticleCount = aArticleCount;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetServerKey(char** aServerKey) {
+ NS_ENSURE_ARG_POINTER(aServerKey);
+
+ *aServerKey = ToNewCString(mServerKey);
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetServerKey(const char* aServerKey) {
+ NS_ENSURE_ARG_POINTER(aServerKey);
+
+ mServerKey = aServerKey;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetHitOK(bool* aHitOK) {
+ NS_ENSURE_ARG_POINTER(aHitOK);
+
+ *aHitOK = mHitOK;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetHitOK(bool aHitOK) {
+ mHitOK = aHitOK;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::GetDownloadAll(bool* aDownloadAll) {
+ NS_ENSURE_ARG_POINTER(aDownloadAll);
+
+ *aDownloadAll = mDownloadAll;
+
+ return NS_OK;
+}
+NS_IMETHODIMP nsNewsDownloadDialogArgs::SetDownloadAll(bool aDownloadAll) {
+ mDownloadAll = aDownloadAll;
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h
new file mode 100644
index 0000000000..7a43523072
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloadDialogArgs.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsNewsDownloadDialogArgs_h__
+#define nsNewsDownloadDialogArgs_h__
+
+#include "nsINewsDownloadDialogArgs.h"
+#include "nsString.h"
+
+class nsNewsDownloadDialogArgs : public nsINewsDownloadDialogArgs {
+ public:
+ nsNewsDownloadDialogArgs();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINEWSDOWNLOADDIALOGARGS
+
+ private:
+ virtual ~nsNewsDownloadDialogArgs();
+
+ nsString mGroupName;
+ int32_t mArticleCount;
+ nsCString mServerKey;
+ bool mHitOK;
+ bool mDownloadAll;
+};
+
+#endif // nsNewsDownloadDialogArgs_h__
diff --git a/comm/mailnews/news/src/nsNewsDownloader.cpp b/comm/mailnews/news/src/nsNewsDownloader.cpp
new file mode 100644
index 0000000000..945e1bd084
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloader.cpp
@@ -0,0 +1,507 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nntpCore.h"
+#include "netCore.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsIStringBundle.h"
+#include "nsNewsDownloader.h"
+#include "nsINntpService.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgSearchTerm.h"
+#include "nsIMsgAccountManager.h"
+#include "nsMsgFolderFlags.h"
+#include "nsIMsgMailSession.h"
+#include "nsMsgMessageFlags.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsMsgUtils.h"
+#include "mozilla/Components.h"
+
+// This file contains the news article download state machine.
+
+// if pIds is not null, download the articles whose id's are passed in.
+// Otherwise, which articles to download is determined by nsNewsDownloader
+// object, or subclasses thereof. News can download marked objects, for example.
+nsresult nsNewsDownloader::DownloadArticles(nsIMsgWindow* window,
+ nsIMsgFolder* folder,
+ nsTArray<nsMsgKey>* pIds) {
+ if (pIds != nullptr)
+ m_keysToDownload.InsertElementsAt(0, pIds->Elements(), pIds->Length());
+
+ if (!m_keysToDownload.IsEmpty()) m_downloadFromKeys = true;
+
+ m_folder = folder;
+ m_window = window;
+ m_numwrote = 0;
+
+ bool headersToDownload = GetNextHdrToRetrieve();
+ // should we have a special error code for failure here?
+ return (headersToDownload) ? DownloadNext(true) : NS_ERROR_FAILURE;
+}
+
+/* Saving news messages
+ */
+
+NS_IMPL_ISUPPORTS(nsNewsDownloader, nsIUrlListener, nsIMsgSearchNotify)
+
+nsNewsDownloader::nsNewsDownloader(nsIMsgWindow* window, nsIMsgDatabase* msgDB,
+ nsIUrlListener* listener) {
+ m_numwrote = 0;
+ m_downloadFromKeys = false;
+ m_newsDB = msgDB;
+ m_abort = false;
+ m_listener = listener;
+ m_window = window;
+ m_lastPercent = -1;
+ m_lastProgressTime = 0;
+ // not the perfect place for this, but I think it will work.
+ if (m_window) m_window->SetStopped(false);
+}
+
+nsNewsDownloader::~nsNewsDownloader() {
+ if (m_listener)
+ m_listener->OnStopRunningUrl(/* don't have a url */ nullptr, m_status);
+ if (m_newsDB) {
+ m_newsDB->Commit(nsMsgDBCommitType::kLargeCommit);
+ m_newsDB = nullptr;
+ }
+}
+
+NS_IMETHODIMP nsNewsDownloader::OnStartRunningUrl(nsIURI* url) { return NS_OK; }
+
+NS_IMETHODIMP nsNewsDownloader::OnStopRunningUrl(nsIURI* url,
+ nsresult exitCode) {
+ bool stopped = false;
+ if (m_window) m_window->GetStopped(&stopped);
+ if (stopped) exitCode = NS_BINDING_ABORTED;
+
+ nsresult rv = exitCode;
+ if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND)
+ rv = DownloadNext(false);
+
+ return rv;
+}
+
+nsresult nsNewsDownloader::DownloadNext(bool firstTimeP) {
+ nsresult rv;
+ if (!firstTimeP) {
+ bool moreHeaders = GetNextHdrToRetrieve();
+ if (!moreHeaders) {
+ if (m_listener) m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ return NS_OK;
+ }
+ }
+ StartDownload();
+ m_wroteAnyP = false;
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> uri;
+ return nntpService->FetchMessage(m_folder, m_keyToDownload, m_window, nullptr,
+ this, getter_AddRefs(uri));
+}
+
+bool DownloadNewsArticlesToOfflineStore::GetNextHdrToRetrieve() {
+ nsresult rv;
+
+ if (m_downloadFromKeys) return nsNewsDownloader::GetNextHdrToRetrieve();
+
+ if (m_headerEnumerator == nullptr)
+ rv = m_newsDB->EnumerateMessages(getter_AddRefs(m_headerEnumerator));
+
+ bool hasMore = false;
+
+ while (NS_SUCCEEDED(rv = m_headerEnumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ rv = m_headerEnumerator->GetNext(getter_AddRefs(m_newsHeader));
+ NS_ENSURE_SUCCESS(rv, false);
+ uint32_t hdrFlags;
+ m_newsHeader->GetFlags(&hdrFlags);
+ if (hdrFlags & nsMsgMessageFlags::Marked) {
+ m_newsHeader->GetMessageKey(&m_keyToDownload);
+ break;
+ } else {
+ m_newsHeader = nullptr;
+ }
+ }
+ return hasMore;
+}
+
+void nsNewsDownloader::Abort() {}
+void nsNewsDownloader::Complete() {}
+
+bool nsNewsDownloader::GetNextHdrToRetrieve() {
+ nsresult rv;
+ if (m_downloadFromKeys) {
+ if (m_numwrote >= (int32_t)m_keysToDownload.Length()) return false;
+
+ m_keyToDownload = m_keysToDownload[m_numwrote++];
+ int32_t percent;
+ percent = (100 * m_numwrote) / (int32_t)m_keysToDownload.Length();
+
+ int64_t nowMS = 0;
+ if (percent < 100) // always need to do 100%
+ {
+ nowMS = PR_IntervalToMilliseconds(PR_IntervalNow());
+ if (nowMS - m_lastProgressTime < 750) return true;
+ }
+
+ m_lastProgressTime = nowMS;
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, false);
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(NEWS_MSGS_URL, getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ nsAutoString firstStr;
+ firstStr.AppendInt(m_numwrote);
+ nsAutoString totalStr;
+ totalStr.AppendInt(int(m_keysToDownload.Length()));
+ nsString prettyName;
+ nsString statusString;
+
+ m_folder->GetPrettyName(prettyName);
+
+ AutoTArray<nsString, 3> formatStrings = {firstStr, totalStr, prettyName};
+ rv = bundle->FormatStringFromName("downloadingArticlesForOffline",
+ formatStrings, statusString);
+ NS_ENSURE_SUCCESS(rv, false);
+ ShowProgress(statusString.get(), percent);
+ return true;
+ }
+ NS_ASSERTION(false, "shouldn't get here if we're not downloading from keys.");
+ return false; // shouldn't get here if we're not downloading from keys.
+}
+
+nsresult nsNewsDownloader::ShowProgress(const char16_t* progressString,
+ int32_t percent) {
+ if (!m_statusFeedback) {
+ if (m_window) m_window->GetStatusFeedback(getter_AddRefs(m_statusFeedback));
+ }
+ if (m_statusFeedback) {
+ m_statusFeedback->ShowStatusString(nsDependentString(progressString));
+ if (percent != m_lastPercent) {
+ m_statusFeedback->ShowProgress(percent);
+ m_lastPercent = percent;
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStartRunningUrl(
+ nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP DownloadNewsArticlesToOfflineStore::OnStopRunningUrl(
+ nsIURI* url, nsresult exitCode) {
+ m_status = exitCode;
+ if (m_newsHeader != nullptr) {
+#ifdef DEBUG_bienvenu
+ // XP_Trace("finished retrieving %ld\n", m_newsHeader->GetMessageKey());
+#endif
+ if (m_newsDB) {
+ nsMsgKey msgKey;
+ m_newsHeader->GetMessageKey(&msgKey);
+ m_newsDB->MarkMarked(msgKey, false, nullptr);
+ }
+ }
+ m_newsHeader = nullptr;
+ return nsNewsDownloader::OnStopRunningUrl(url, exitCode);
+}
+
+int DownloadNewsArticlesToOfflineStore::FinishDownload() { return 0; }
+
+NS_IMETHODIMP nsNewsDownloader::OnSearchHit(nsIMsgDBHdr* header,
+ nsIMsgFolder* folder) {
+ NS_ENSURE_ARG(header);
+
+ uint32_t msgFlags;
+ header->GetFlags(&msgFlags);
+ // only need to download articles we don't already have...
+ if (!(msgFlags & nsMsgMessageFlags::Offline)) {
+ nsMsgKey key;
+ header->GetMessageKey(&key);
+ m_keysToDownload.AppendElement(key);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNewsDownloader::OnSearchDone(nsresult status) {
+ if (m_keysToDownload.IsEmpty()) {
+ if (m_listener) return m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ }
+ nsresult rv = DownloadArticles(
+ m_window, m_folder,
+ /* we've already set m_keysToDownload, so don't pass it in */ nullptr);
+ if (NS_FAILED(rv))
+ if (m_listener) m_listener->OnStopRunningUrl(nullptr, rv);
+
+ return rv;
+}
+NS_IMETHODIMP nsNewsDownloader::OnNewSearch() { return NS_OK; }
+
+int DownloadNewsArticlesToOfflineStore::StartDownload() {
+ m_newsDB->GetMsgHdrForKey(m_keyToDownload, getter_AddRefs(m_newsHeader));
+ return 0;
+}
+
+DownloadNewsArticlesToOfflineStore::DownloadNewsArticlesToOfflineStore(
+ nsIMsgWindow* window, nsIMsgDatabase* db, nsIUrlListener* listener)
+ : nsNewsDownloader(window, db, listener) {
+ m_newsDB = db;
+}
+
+DownloadNewsArticlesToOfflineStore::~DownloadNewsArticlesToOfflineStore() {}
+
+DownloadMatchingNewsArticlesToNewsDB::DownloadMatchingNewsArticlesToNewsDB(
+ nsIMsgWindow* window, nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
+ nsIUrlListener* listener)
+ : DownloadNewsArticlesToOfflineStore(window, newsDB, listener) {
+ m_window = window;
+ m_folder = folder;
+ m_newsDB = newsDB;
+ m_downloadFromKeys = true; // search term matching means downloadFromKeys.
+}
+
+DownloadMatchingNewsArticlesToNewsDB::~DownloadMatchingNewsArticlesToNewsDB() {}
+
+NS_IMPL_ISUPPORTS(nsMsgDownloadAllNewsgroups, nsIUrlListener)
+
+nsMsgDownloadAllNewsgroups::nsMsgDownloadAllNewsgroups(
+ nsIMsgWindow* window, nsIUrlListener* listener) {
+ m_window = window;
+ m_listener = listener;
+ m_downloaderForGroup =
+ new DownloadMatchingNewsArticlesToNewsDB(window, nullptr, nullptr, this);
+ m_downloadedHdrsForCurGroup = false;
+}
+
+nsMsgDownloadAllNewsgroups::~nsMsgDownloadAllNewsgroups() {}
+
+NS_IMETHODIMP nsMsgDownloadAllNewsgroups::OnStartRunningUrl(nsIURI* url) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgDownloadAllNewsgroups::OnStopRunningUrl(nsIURI* url, nsresult exitCode) {
+ nsresult rv = exitCode;
+ if (NS_SUCCEEDED(exitCode) || exitCode == NS_MSG_NEWS_ARTICLE_NOT_FOUND) {
+ if (m_downloadedHdrsForCurGroup) {
+ bool savingArticlesOffline = false;
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder =
+ do_QueryInterface(m_currentFolder);
+ if (newsFolder) newsFolder->GetSaveArticleOffline(&savingArticlesOffline);
+
+ m_downloadedHdrsForCurGroup = false;
+ if (savingArticlesOffline) // skip this group - we're saving to it
+ // already
+ rv = ProcessNextGroup();
+ else
+ rv = DownloadMsgsForCurrentGroup();
+ } else {
+ rv = ProcessNextGroup();
+ }
+ } else if (m_listener) // notify main observer.
+ m_listener->OnStopRunningUrl(url, exitCode);
+
+ return rv;
+}
+
+/**
+ * Leaves m_currentServer at the next nntp "server" that
+ * might have folders to download for offline use. If no more servers,
+ * m_currentServer will be left at nullptr and the function returns false.
+ * Also, sets up m_folderQueue to hold a (reversed) list of all the folders
+ * to consider for the current server.
+ * If no servers found, returns false.
+ */
+bool nsMsgDownloadAllNewsgroups::AdvanceToNextServer() {
+ nsresult rv;
+
+ if (m_allServers.IsEmpty()) {
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ASSERTION(accountManager && NS_SUCCEEDED(rv),
+ "couldn't get account mgr");
+ if (!accountManager || NS_FAILED(rv)) return false;
+
+ rv = accountManager->GetAllServers(m_allServers);
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+ size_t serverIndex = 0;
+ if (m_currentServer) {
+ serverIndex = m_allServers.IndexOf(m_currentServer);
+ if (serverIndex == m_allServers.NoIndex) {
+ serverIndex = 0;
+ } else {
+ ++serverIndex;
+ }
+ }
+ m_currentServer = nullptr;
+ uint32_t numServers = m_allServers.Length();
+ nsCOMPtr<nsIMsgFolder> rootFolder;
+
+ while (serverIndex < numServers) {
+ nsCOMPtr<nsIMsgIncomingServer> server(m_allServers[serverIndex]);
+ serverIndex++;
+
+ nsCOMPtr<nsINntpIncomingServer> newsServer = do_QueryInterface(server);
+ if (!newsServer) // we're only looking for news servers
+ continue;
+
+ if (server) {
+ m_currentServer = server;
+ server->GetRootFolder(getter_AddRefs(rootFolder));
+ if (rootFolder) {
+ rv = rootFolder->GetDescendants(m_folderQueue);
+ if (NS_SUCCEEDED(rv)) {
+ if (!m_folderQueue.IsEmpty()) {
+ // We'll be popping folders from the end of the queue as we go.
+ m_folderQueue.Reverse();
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+}
+
+/**
+ * Sets m_currentFolder to the next usable folder.
+ *
+ * @return False if no more folders found, otherwise true.
+ */
+bool nsMsgDownloadAllNewsgroups::AdvanceToNextGroup() {
+ nsresult rv = NS_OK;
+
+ if (m_currentFolder) {
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
+ if (newsFolder) newsFolder->SetSaveArticleOffline(false);
+
+ nsCOMPtr<nsIMsgMailSession> session =
+ do_GetService("@mozilla.org/messenger/services/session;1", &rv);
+ if (NS_SUCCEEDED(rv) && session) {
+ bool folderOpen;
+ uint32_t folderFlags;
+ m_currentFolder->GetFlags(&folderFlags);
+ session->IsFolderOpenInWindow(m_currentFolder, &folderOpen);
+ if (!folderOpen &&
+ !(folderFlags & (nsMsgFolderFlags::Trash | nsMsgFolderFlags::Inbox)))
+ m_currentFolder->SetMsgDatabase(nullptr);
+ }
+ m_currentFolder = nullptr;
+ }
+
+ bool hasMore = false;
+ if (m_currentServer) {
+ hasMore = !m_folderQueue.IsEmpty();
+ }
+ if (!hasMore) {
+ hasMore = AdvanceToNextServer();
+ }
+
+ if (hasMore) {
+ m_currentFolder = m_folderQueue.PopLastElement();
+ }
+ return m_currentFolder;
+}
+
+nsresult DownloadMatchingNewsArticlesToNewsDB::RunSearch(
+ nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
+ nsIMsgSearchSession* searchSession) {
+ m_folder = folder;
+ m_newsDB = newsDB;
+ m_searchSession = searchSession;
+
+ m_keysToDownload.Clear();
+
+ NS_ENSURE_ARG(searchSession);
+ NS_ENSURE_ARG(folder);
+
+ searchSession->RegisterListener(this, nsIMsgSearchSession::allNotifications);
+ nsresult rv =
+ searchSession->AddScopeTerm(nsMsgSearchScope::localNews, folder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return searchSession->Search(m_window);
+}
+
+nsresult nsMsgDownloadAllNewsgroups::ProcessNextGroup() {
+ bool done = false;
+
+ while (!done) {
+ done = !AdvanceToNextGroup();
+ if (!done && m_currentFolder) {
+ uint32_t folderFlags;
+ m_currentFolder->GetFlags(&folderFlags);
+ if (folderFlags & nsMsgFolderFlags::Offline) break;
+ }
+ }
+ if (done) {
+ if (m_listener) return m_listener->OnStopRunningUrl(nullptr, NS_OK);
+ }
+ m_downloadedHdrsForCurGroup = true;
+ return m_currentFolder ? m_currentFolder->GetNewMessages(m_window, this)
+ : NS_ERROR_NOT_INITIALIZED;
+}
+
+nsresult nsMsgDownloadAllNewsgroups::DownloadMsgsForCurrentGroup() {
+ NS_ENSURE_TRUE(m_downloaderForGroup, NS_ERROR_OUT_OF_MEMORY);
+ nsCOMPtr<nsIMsgDatabase> db;
+ nsCOMPtr<nsIMsgDownloadSettings> downloadSettings;
+ m_currentFolder->GetMsgDatabase(getter_AddRefs(db));
+ nsresult rv =
+ m_currentFolder->GetDownloadSettings(getter_AddRefs(downloadSettings));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder = do_QueryInterface(m_currentFolder);
+ if (newsFolder) newsFolder->SetSaveArticleOffline(true);
+
+ nsCOMPtr<nsIMsgSearchSession> searchSession =
+ do_CreateInstance("@mozilla.org/messenger/searchSession;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool downloadByDate, downloadUnreadOnly;
+ uint32_t ageLimitOfMsgsToDownload;
+
+ downloadSettings->GetDownloadByDate(&downloadByDate);
+ downloadSettings->GetDownloadUnreadOnly(&downloadUnreadOnly);
+ downloadSettings->GetAgeLimitOfMsgsToDownload(&ageLimitOfMsgsToDownload);
+
+ nsCOMPtr<nsIMsgSearchTerm> term;
+ nsCOMPtr<nsIMsgSearchValue> value;
+
+ rv = searchSession->CreateTerm(getter_AddRefs(term));
+ NS_ENSURE_SUCCESS(rv, rv);
+ term->GetValue(getter_AddRefs(value));
+
+ if (downloadUnreadOnly) {
+ value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
+ value->SetStatus(nsMsgMessageFlags::Read);
+ searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus,
+ nsMsgSearchOp::Isnt, value, true, nullptr);
+ }
+ if (downloadByDate) {
+ value->SetAttrib(nsMsgSearchAttrib::AgeInDays);
+ value->SetAge(ageLimitOfMsgsToDownload);
+ searchSession->AddSearchTerm(nsMsgSearchAttrib::AgeInDays,
+ nsMsgSearchOp::IsLessThan, value,
+ nsMsgSearchBooleanOp::BooleanAND, nullptr);
+ }
+ value->SetAttrib(nsMsgSearchAttrib::MsgStatus);
+ value->SetStatus(nsMsgMessageFlags::Offline);
+ searchSession->AddSearchTerm(nsMsgSearchAttrib::MsgStatus,
+ nsMsgSearchOp::Isnt, value,
+ nsMsgSearchBooleanOp::BooleanAND, nullptr);
+
+ m_downloaderForGroup->RunSearch(m_currentFolder, db, searchSession);
+ return rv;
+}
diff --git a/comm/mailnews/news/src/nsNewsDownloader.h b/comm/mailnews/news/src/nsNewsDownloader.h
new file mode 100644
index 0000000000..c1e68eb77d
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsDownloader.h
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _nsNewsDownloader_H_
+#define _nsNewsDownloader_H_
+
+#include "nsIMsgDatabase.h"
+#include "nsIUrlListener.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgHdr.h"
+#include "nsIMsgWindow.h"
+#include "nsIMsgSearchNotify.h"
+#include "nsIMsgSearchSession.h"
+#include "nsIMsgStatusFeedback.h"
+#include "nsTArray.h"
+
+// base class for downloading articles in a single newsgroup. Keys to download
+// are passed in to DownloadArticles method.
+class nsNewsDownloader : public nsIUrlListener, public nsIMsgSearchNotify {
+ public:
+ nsNewsDownloader(nsIMsgWindow* window, nsIMsgDatabase* db,
+ nsIUrlListener* listener);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+ NS_DECL_NSIMSGSEARCHNOTIFY
+
+ virtual nsresult DownloadArticles(nsIMsgWindow* window, nsIMsgFolder* folder,
+ nsTArray<nsMsgKey>* pKeyArray);
+
+ bool ShouldAbort() const { return m_abort; }
+
+ protected:
+ virtual ~nsNewsDownloader();
+
+ virtual int32_t Write(const char* /*block*/, int32_t length) {
+ return length;
+ }
+ virtual void Abort();
+ virtual void Complete();
+ virtual bool GetNextHdrToRetrieve();
+ virtual nsresult DownloadNext(bool firstTimeP);
+ virtual int32_t FinishDownload() { return 0; }
+ virtual int32_t StartDownload() { return 0; }
+ virtual nsresult ShowProgress(const char16_t* progressString,
+ int32_t percent);
+
+ nsTArray<nsMsgKey> m_keysToDownload;
+ nsCOMPtr<nsIMsgFolder> m_folder;
+ nsCOMPtr<nsIMsgDatabase> m_newsDB;
+ nsCOMPtr<nsIUrlListener> m_listener;
+ bool m_downloadFromKeys;
+ bool m_existedP;
+ bool m_wroteAnyP;
+ bool m_summaryValidP;
+ bool m_abort;
+ int32_t m_numwrote;
+ nsMsgKey m_keyToDownload;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsCOMPtr<nsIMsgStatusFeedback> m_statusFeedback;
+ nsCOMPtr<nsIMsgSearchSession> m_searchSession;
+ int32_t m_lastPercent;
+ int64_t m_lastProgressTime;
+ nsresult m_status;
+};
+
+// class for downloading articles in a single newsgroup to the offline store.
+class DownloadNewsArticlesToOfflineStore : public nsNewsDownloader {
+ public:
+ DownloadNewsArticlesToOfflineStore(nsIMsgWindow* window, nsIMsgDatabase* db,
+ nsIUrlListener* listener);
+ virtual ~DownloadNewsArticlesToOfflineStore();
+
+ NS_IMETHOD OnStartRunningUrl(nsIURI* url);
+ NS_IMETHOD OnStopRunningUrl(nsIURI* url, nsresult exitCode);
+
+ protected:
+ virtual int32_t StartDownload();
+ virtual int32_t FinishDownload();
+ virtual bool GetNextHdrToRetrieve();
+
+ nsCOMPtr<nsIMsgEnumerator> m_headerEnumerator;
+ nsCOMPtr<nsIMsgDBHdr> m_newsHeader;
+};
+
+// class for downloading all the articles that match the passed in search
+// criteria for a single newsgroup.
+class DownloadMatchingNewsArticlesToNewsDB
+ : public DownloadNewsArticlesToOfflineStore {
+ public:
+ DownloadMatchingNewsArticlesToNewsDB(nsIMsgWindow* window,
+ nsIMsgFolder* folder,
+ nsIMsgDatabase* newsDB,
+ nsIUrlListener* listener);
+ virtual ~DownloadMatchingNewsArticlesToNewsDB();
+ nsresult RunSearch(nsIMsgFolder* folder, nsIMsgDatabase* newsDB,
+ nsIMsgSearchSession* searchSession);
+
+ protected:
+};
+
+// this class iterates all the news servers for each group on the server that's
+// configured for offline use, downloads the messages that meet the download
+// criteria for that newsgroup/server
+class nsMsgDownloadAllNewsgroups : public nsIUrlListener {
+ public:
+ nsMsgDownloadAllNewsgroups(nsIMsgWindow* window, nsIUrlListener* listener);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIURLLISTENER
+
+ nsresult ProcessNextGroup();
+
+ protected:
+ virtual ~nsMsgDownloadAllNewsgroups();
+
+ bool AdvanceToNextServer();
+ bool AdvanceToNextGroup();
+ nsresult DownloadMsgsForCurrentGroup();
+
+ RefPtr<DownloadMatchingNewsArticlesToNewsDB> m_downloaderForGroup;
+
+ nsCOMPtr<nsIMsgFolder> m_currentFolder;
+ nsCOMPtr<nsIMsgWindow> m_window;
+ nsTArray<RefPtr<nsIMsgIncomingServer>> m_allServers;
+ nsCOMPtr<nsIMsgIncomingServer> m_currentServer;
+ // Folders still to process for the current server.
+ nsTArray<RefPtr<nsIMsgFolder>> m_folderQueue;
+ nsCOMPtr<nsIUrlListener> m_listener;
+
+ bool m_downloadedHdrsForCurGroup;
+};
+
+#endif
diff --git a/comm/mailnews/news/src/nsNewsFolder.cpp b/comm/mailnews/news/src/nsNewsFolder.cpp
new file mode 100644
index 0000000000..f23b54e273
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsFolder.cpp
@@ -0,0 +1,1645 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "prlog.h"
+
+#include "msgCore.h" // precompiled header...
+#include "nntpCore.h"
+#include "nsIMsgMailNewsUrl.h"
+#include "nsNewsFolder.h"
+#include "nsMsgFolderFlags.h"
+#include "MailNewsTypes.h"
+#include "prprf.h"
+#include "prsystem.h"
+#include "nsTArray.h"
+#include "nsINntpService.h"
+#include "nsIMsgFilterService.h"
+#include "nsCOMPtr.h"
+#include "nsMsgUtils.h"
+#include "nsNewsUtils.h"
+
+#include "nsIMsgIncomingServer.h"
+#include "nsINntpIncomingServer.h"
+#include "nsINewsDatabase.h"
+#include "nsILineInputStream.h"
+
+#include "nsIMsgWindow.h"
+#include "nsIWindowWatcher.h"
+
+#include "nsNetUtil.h"
+#include "nsIAuthPrompt.h"
+#include "nsIURL.h"
+#include "nsNetCID.h"
+#include "nsINntpUrl.h"
+
+#include "nsNewsDownloader.h"
+#include "nsIStringBundle.h"
+#include "nsMsgI18N.h"
+#include "nsNativeCharsetUtils.h"
+
+#include "nsIMsgFolderNotificationService.h"
+#include "nsILoginInfo.h"
+#include "nsILoginManager.h"
+#include "nsEmbedCID.h"
+#include "mozilla/Components.h"
+#include "mozilla/SlicedInputStream.h"
+#include "nsIInputStream.h"
+#include "nsMemory.h"
+#include "nsIURIMutator.h"
+
+#define kNewsSortOffset 9000
+
+#define NEWS_SCHEME "news:"
+#define SNEWS_SCHEME "snews:"
+
+////////////////////////////////////////////////////////////////////////////////
+
+nsMsgNewsFolder::nsMsgNewsFolder(void)
+ : mExpungedBytes(0),
+ mGettingNews(false),
+ mInitialized(false),
+ m_downloadMessageForOfflineUse(false),
+ mReadSet(nullptr),
+ mSortOrder(kNewsSortOffset) {
+ mFolderSize = kSizeUnknown;
+}
+
+nsMsgNewsFolder::~nsMsgNewsFolder(void) {}
+
+NS_IMPL_ADDREF_INHERITED(nsMsgNewsFolder, nsMsgDBFolder)
+NS_IMPL_RELEASE_INHERITED(nsMsgNewsFolder, nsMsgDBFolder)
+
+NS_IMETHODIMP nsMsgNewsFolder::QueryInterface(REFNSIID aIID,
+ void** aInstancePtr) {
+ if (!aInstancePtr) return NS_ERROR_NULL_POINTER;
+ *aInstancePtr = nullptr;
+
+ if (aIID.Equals(NS_GET_IID(nsIMsgNewsFolder)))
+ *aInstancePtr = static_cast<nsIMsgNewsFolder*>(this);
+ if (*aInstancePtr) {
+ AddRef();
+ return NS_OK;
+ }
+
+ return nsMsgDBFolder::QueryInterface(aIID, aInstancePtr);
+}
+
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsMsgNewsFolder::CreateSubFolders(nsIFile* path) {
+ nsresult rv;
+ bool isNewsServer = false;
+ rv = GetIsServer(&isNewsServer);
+ if (NS_FAILED(rv)) return rv;
+
+ if (isNewsServer) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->GetNewsrcFilePath(getter_AddRefs(mNewsrcFilePath));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = LoadNewsrcFileAndCreateNewsgroups();
+ } else // is not a host, so it has no newsgroups. (what about categories??)
+ rv = NS_OK;
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::AddNewsgroup(const nsACString& name, const nsACString& setStr,
+ nsIMsgFolder** child) {
+ NS_ENSURE_ARG_POINTER(child);
+ nsresult rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoCString uri(mURI);
+ uri.Append('/');
+ // URI should use UTF-8
+ // (see RFC2396 Uniform Resource Identifiers (URI): Generic Syntax)
+
+ // we are handling newsgroup names in UTF-8
+ NS_ConvertUTF8toUTF16 nameUtf16(name);
+
+ nsAutoCString escapedName;
+ rv = NS_MsgEscapeEncodeURLPath(nameUtf16, escapedName);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = nntpServer->AddNewsgroup(nameUtf16);
+ if (NS_FAILED(rv)) return rv;
+
+ uri.Append(escapedName);
+
+ nsCOMPtr<nsIMsgFolder> folder;
+ rv = GetOrCreateFolder(uri, getter_AddRefs(folder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder(do_QueryInterface(folder, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Ensure any containing .sdb dir exists.
+ nsCOMPtr<nsIFile> path;
+ rv = CreateDirectoryForFolder(getter_AddRefs(path));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // cache this for when we open the db
+ rv = newsFolder->SetReadSetFromStr(setStr);
+
+ rv = folder->SetParent(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // this what shows up in the UI
+ rv = folder->SetName(nameUtf16);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = folder->SetFlag(nsMsgFolderFlags::Newsgroup);
+ if (NS_FAILED(rv)) return rv;
+
+ int32_t numExistingGroups = mSubFolders.Count();
+
+ // add kNewsSortOffset (9000) to prevent this problem: 1,10,11,2,3,4,5
+ // We use 9000 instead of 1000 so newsgroups will sort to bottom of flat
+ // folder views
+ rv = folder->SetSortOrder(numExistingGroups + kNewsSortOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mSubFolders.AppendObject(folder);
+ folder->SetParent(this);
+ folder.forget(child);
+ return rv;
+}
+
+nsresult nsMsgNewsFolder::ParseFolder(nsIFile* path) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+nsresult nsMsgNewsFolder::AddDirectorySeparator(nsIFile* path) {
+ // don't concat the full separator with .sbd
+ return (mURI.Equals(kNewsRootURI))
+ ? NS_OK
+ : nsMsgDBFolder::AddDirectorySeparator(path);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) {
+ if (!mInitialized) {
+ // do this first, so we make sure to do it, even on failure.
+ // see bug #70494
+ mInitialized = true;
+
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = GetFilePath(getter_AddRefs(path));
+ if (NS_FAILED(rv)) return rv;
+
+ rv = CreateSubFolders(path);
+ if (NS_FAILED(rv)) return rv;
+
+ // force ourselves to get initialized from cache
+ // Don't care if it fails. this will fail the first time after
+ // migration, but we continue on. see #66018
+ (void)UpdateSummaryTotals(false);
+ }
+
+ return nsMsgDBFolder::GetSubFolders(folders);
+}
+
+// Makes sure the database is open and exists. If the database is valid then
+// returns NS_OK. Otherwise returns a failure error value.
+nsresult nsMsgNewsFolder::GetDatabase() {
+ nsresult rv;
+ if (!mDatabase) {
+ nsCOMPtr<nsIMsgDBService> msgDBService =
+ do_GetService("@mozilla.org/msgDatabase/msgDBService;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Get the database, blowing it away if it's out of date.
+ rv = msgDBService->OpenFolderDB(this, false, getter_AddRefs(mDatabase));
+ if (NS_FAILED(rv))
+ rv = msgDBService->CreateNewDB(this, getter_AddRefs(mDatabase));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mAddListener) rv = mDatabase->AddListener(this);
+
+ nsCOMPtr<nsINewsDatabase> db = do_QueryInterface(mDatabase, &rv);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = db->SetReadSet(mReadSet);
+ if (NS_FAILED(rv)) return rv;
+
+ rv = UpdateSummaryTotals(true);
+ if (NS_FAILED(rv)) return rv;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::UpdateFolder(nsIMsgWindow* aWindow) {
+ // Get news.get_messages_on_select pref
+ nsresult rv;
+ nsCOMPtr<nsIPrefBranch> prefBranch =
+ do_GetService(NS_PREFSERVICE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool getMessagesOnSelect = true;
+ prefBranch->GetBoolPref("news.get_messages_on_select", &getMessagesOnSelect);
+
+ // Only if news.get_messages_on_select is true do we get new messages
+ // automatically
+ if (getMessagesOnSelect) {
+ rv = GetDatabase(); // want this cached...
+ if (NS_SUCCEEDED(rv)) {
+ if (mDatabase) {
+ nsCOMPtr<nsIMsgRetentionSettings> retentionSettings;
+ nsresult rv = GetRetentionSettings(getter_AddRefs(retentionSettings));
+ if (NS_SUCCEEDED(rv))
+ rv = mDatabase->ApplyRetentionSettings(retentionSettings, false);
+ }
+ rv = AutoCompact(aWindow);
+ NS_ENSURE_SUCCESS(rv, rv);
+ // GetNewMessages has to be the last rv set before we get to the next
+ // check, so that we'll have rv set to NS_MSG_ERROR_OFFLINE when offline
+ // and send a folder loaded notification to the front end.
+ rv = GetNewMessages(aWindow, nullptr);
+ }
+ if (rv != NS_MSG_ERROR_OFFLINE) return rv;
+ }
+ // We're not getting messages because either get_messages_on_select is
+ // false or we're offline. Send an immediate folder loaded notification.
+ NotifyFolderEvent(kFolderLoaded);
+ (void)RefreshSizeOnDisk();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanSubscribe(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+
+ bool isNewsServer = false;
+ nsresult rv = GetIsServer(&isNewsServer);
+ if (NS_FAILED(rv)) return rv;
+
+ // you can only subscribe to news servers, not news groups
+ *aResult = isNewsServer;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanFileMessages(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ // you can't file messages into a news server or news group
+ *aResult = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanCreateSubfolders(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // you can't create subfolders on a news server or a news group
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanRename(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // you can't rename a news server or a news group
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetCanCompact(bool* aResult) {
+ NS_ENSURE_ARG_POINTER(aResult);
+ *aResult = false;
+ // you can't compact a news server or a news group
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetFolderURL(nsACString& aUrl) {
+ nsCString hostName;
+ nsresult rv = GetHostname(hostName);
+ nsString groupName;
+ rv = GetName(groupName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ rv = server->GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t port;
+ rv = server->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, rv);
+ const char* newsScheme =
+ (socketType == nsMsgSocketType::SSL) ? SNEWS_SCHEME : NEWS_SCHEME;
+ nsCString escapedName;
+ rv = NS_MsgEscapeEncodeURLPath(groupName, escapedName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString tmpStr;
+ tmpStr.Adopt(PR_smprintf("%s//%s:%ld/%s", newsScheme, hostName.get(), port,
+ escapedName.get()));
+ aUrl.Assign(tmpStr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetNewsrcHasChanged(bool newsrcHasChanged) {
+ nsresult rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+ return nntpServer->SetNewsrcHasChanged(newsrcHasChanged);
+}
+
+nsresult nsMsgNewsFolder::CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) {
+ nsMsgNewsFolder* newFolder = new nsMsgNewsFolder;
+ NS_ADDREF(*folder = newFolder);
+ newFolder->Init(uri);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CreateSubfolder(const nsAString& newsgroupName,
+ nsIMsgWindow* msgWindow) {
+ nsresult rv = NS_OK;
+ if (newsgroupName.IsEmpty()) return NS_MSG_ERROR_INVALID_FOLDER_NAME;
+
+ nsCOMPtr<nsIMsgFolder> child;
+ // Now let's create the actual new folder
+ rv = AddNewsgroup(NS_ConvertUTF16toUTF8(newsgroupName), EmptyCString(),
+ getter_AddRefs(child));
+
+ if (NS_SUCCEEDED(rv))
+ SetNewsrcHasChanged(true); // subscribe UI does this - but maybe we got
+ // here through auto-subscribe
+
+ if (NS_SUCCEEDED(rv) && child) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsIDBFolderInfo> folderInfo;
+ nsCOMPtr<nsIMsgDatabase> db;
+ // Used to init some folder status of child.
+ rv = child->GetDBFolderInfoAndDB(getter_AddRefs(folderInfo),
+ getter_AddRefs(db));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NotifyFolderAdded(child);
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyFolderAdded(child);
+ }
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::DeleteStorage() {
+ nsresult rv = nsMsgDBFolder::DeleteStorage();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ nsAutoString name;
+ rv = GetUnicodeName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->RemoveNewsgroup(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (void)RefreshSizeOnDisk();
+
+ return SetNewsrcHasChanged(true);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::Rename(const nsAString& newName,
+ nsIMsgWindow* msgWindow) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetAbbreviatedName(nsAString& aAbbreviatedName) {
+ nsresult rv;
+
+ rv = nsMsgDBFolder::GetPrettyName(aAbbreviatedName);
+ if (NS_FAILED(rv)) return rv;
+
+ // only do this for newsgroup names, not for newsgroup hosts.
+ bool isNewsServer = false;
+ rv = GetIsServer(&isNewsServer);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!isNewsServer) {
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ bool abbreviate = true;
+ rv = nntpServer->GetAbbreviate(&abbreviate);
+ if (NS_FAILED(rv)) return rv;
+
+ if (abbreviate)
+ rv = AbbreviatePrettyName(aAbbreviatedName, 1 /* hardcoded for now */);
+ }
+ return rv;
+}
+
+// original code from Oleg Rekutin
+// rekusha@asan.com
+// Public domain, created by Oleg Rekutin
+//
+// takes a newsgroup name, number of words from the end to leave unabberviated
+// the newsgroup name, will get reset to the following format:
+// x.x.x, where x is the first letter of each word and with the
+// exception of last 'fullwords' words, which are left intact.
+// If a word has a dash in it, it is abbreviated as a-b, where
+// 'a' is the first letter of the part of the word before the
+// dash and 'b' is the first letter of the part of the word after
+// the dash
+nsresult nsMsgNewsFolder::AbbreviatePrettyName(nsAString& prettyName,
+ int32_t fullwords) {
+ nsAutoString name(prettyName);
+ int32_t totalwords = 0; // total no. of words
+
+ // get the total no. of words
+ int32_t pos = 0;
+ while (1) {
+ pos = name.FindChar('.', pos);
+ if (pos == -1) {
+ totalwords++;
+ break;
+ } else {
+ totalwords++;
+ pos++;
+ }
+ }
+
+ // get the no. of words to abbreviate
+ int32_t abbrevnum = totalwords - fullwords;
+ if (abbrevnum < 1) return NS_OK; // nothing to abbreviate
+
+ // build the ellipsis
+ nsAutoString out;
+ out += name[0];
+
+ int32_t length = name.Length();
+ int32_t newword = 0; // == 2 if done with all abbreviated words
+
+ fullwords = 0;
+ char16_t currentChar;
+ for (int32_t i = 1; i < length; i++) {
+ // this temporary assignment is needed to fix an intel mac compiler bug.
+ // See Bug #327037 for details.
+ currentChar = name[i];
+ if (newword < 2) {
+ switch (currentChar) {
+ case '.':
+ fullwords++;
+ // check if done with all abbreviated words...
+ if (fullwords == abbrevnum)
+ newword = 2;
+ else
+ newword = 1;
+ break;
+ case '-':
+ newword = 1;
+ break;
+ default:
+ if (newword)
+ newword = 0;
+ else
+ continue;
+ }
+ }
+ out.Append(currentChar);
+ }
+ prettyName = out;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) {
+ NS_ENSURE_ARG_POINTER(folderInfo);
+ NS_ENSURE_ARG_POINTER(db);
+ nsresult openErr;
+ openErr = GetDatabase();
+ if (!mDatabase) {
+ *db = nullptr;
+ return openErr;
+ }
+
+ NS_ADDREF(*db = mDatabase);
+
+ if (NS_SUCCEEDED(openErr)) openErr = (*db)->GetDBFolderInfo(folderInfo);
+ return openErr;
+}
+
+/* this used to be MSG_FolderInfoNews::UpdateSummaryFromNNTPInfo() */
+NS_IMETHODIMP
+nsMsgNewsFolder::UpdateSummaryFromNNTPInfo(int32_t oldest, int32_t youngest,
+ int32_t total) {
+ NS_ENSURE_STATE(mReadSet);
+ /* First, mark all of the articles now known to be expired as read. */
+ if (oldest > 1) {
+ nsCString oldSet;
+ nsCString newSet;
+ mReadSet->Output(getter_Copies(oldSet));
+ mReadSet->AddRange(1, oldest - 1);
+ mReadSet->Output(getter_Copies(newSet));
+ }
+
+ /* Now search the newsrc line and figure out how many of these messages are
+ * marked as unread. */
+
+ /* make sure youngest is a least 1. MSNews seems to return a youngest of 0. */
+ if (youngest == 0) youngest = 1;
+
+ int32_t unread = mReadSet->CountMissingInRange(oldest, youngest);
+ NS_ASSERTION(unread >= 0, "CountMissingInRange reported unread < 0");
+ if (unread < 0)
+ // servers can send us stuff like "211 0 41 40 nz.netstatus"
+ // we should handle it gracefully.
+ unread = 0;
+
+ if (unread > total) {
+ /* This can happen when the newsrc file shows more unread than exist in the
+ * group (total is not necessarily `end - start'.) */
+ unread = total;
+ int32_t deltaInDB = mNumTotalMessages - mNumUnreadMessages;
+ // int32_t deltaInDB = m_totalInDB - m_unreadInDB;
+ /* if we know there are read messages in the db, subtract that from the
+ * unread total */
+ if (deltaInDB > 0) unread -= deltaInDB;
+ }
+
+ bool dbWasOpen = mDatabase != nullptr;
+ int32_t pendingUnreadDelta =
+ unread - mNumUnreadMessages - mNumPendingUnreadMessages;
+ int32_t pendingTotalDelta =
+ total - mNumTotalMessages - mNumPendingTotalMessages;
+ ChangeNumPendingUnread(pendingUnreadDelta);
+ ChangeNumPendingTotalMessages(pendingTotalDelta);
+ if (!dbWasOpen && mDatabase) {
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ mDatabase->RemoveListener(this);
+ mDatabase = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetExpungedBytesCount(int64_t* count) {
+ NS_ENSURE_ARG_POINTER(count);
+ *count = mExpungedBytes;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetDeletable(bool* deletable) {
+ NS_ENSURE_ARG_POINTER(deletable);
+
+ *deletable = false;
+ // For legacy reasons, there can be Saved search folders under news accounts.
+ // Allow deleting those.
+ GetFlag(nsMsgFolderFlags::Virtual, deletable);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::RefreshSizeOnDisk() {
+ uint64_t oldFolderSize = mFolderSize;
+ // We set size to unknown to force it to get recalculated from disk.
+ mFolderSize = kSizeUnknown;
+ if (NS_SUCCEEDED(GetSizeOnDisk(&mFolderSize)))
+ NotifyIntPropertyChanged(kFolderSize, oldFolderSize, mFolderSize);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetSizeOnDisk(int64_t* size) {
+ NS_ENSURE_ARG_POINTER(size);
+
+ bool isServer = false;
+ nsresult rv = GetIsServer(&isServer);
+ // If this is the rootFolder, return 0 as a safe value.
+ if (NS_FAILED(rv) || isServer) mFolderSize = 0;
+
+ // 0 is a valid folder size (meaning empty file with no offline messages),
+ // but 1 is not. So use -1 as a special value meaning no file size was fetched
+ // from disk yet.
+ if (mFolderSize == kSizeUnknown) {
+ nsCOMPtr<nsIFile> diskFile;
+ nsresult rv = GetFilePath(getter_AddRefs(diskFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If there were no news messages downloaded for offline use, the folder
+ // file may not exist yet. In that case size is 0.
+ bool exists = false;
+ rv = diskFile->Exists(&exists);
+ if (NS_FAILED(rv) || !exists) {
+ mFolderSize = 0;
+ } else {
+ int64_t fileSize;
+ rv = diskFile->GetFileSize(&fileSize);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mFolderSize = fileSize;
+ }
+ }
+
+ *size = mFolderSize;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& msgHdrs,
+ nsIMsgWindow* aMsgWindow, bool deleteStorage,
+ bool isMove,
+ nsIMsgCopyServiceListener* listener,
+ bool allowUndo) {
+ nsresult rv = NS_OK;
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ if (!isMove) {
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) notifier->NotifyMsgsDeleted(msgHdrs);
+ }
+
+ rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = EnableNotifications(allMessageCountNotifications, false);
+ if (NS_SUCCEEDED(rv)) {
+ for (auto msgHdr : msgHdrs) {
+ rv = mDatabase->DeleteHeader(msgHdr, nullptr, true, true);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ }
+ EnableNotifications(allMessageCountNotifications, true);
+ }
+
+ if (!isMove)
+ NotifyFolderEvent(NS_SUCCEEDED(rv) ? kDeleteOrMoveMsgCompleted
+ : kDeleteOrMoveMsgFailed);
+
+ if (listener) {
+ listener->OnStartCopy();
+ listener->OnStopCopy(NS_OK);
+ }
+
+ (void)RefreshSizeOnDisk();
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CancelMessage(nsIMsgDBHdr* msgHdr,
+ nsIMsgWindow* aMsgWindow) {
+ NS_ENSURE_ARG_POINTER(msgHdr);
+ NS_ENSURE_ARG_POINTER(aMsgWindow);
+
+ nsresult rv;
+
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // for cancel, we need to
+ // turn "newsmessage://sspitzer@news.mozilla.org/netscape.test#5428"
+ // into "news://sspitzer@news.mozilla.org/23423@netscape.com"
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString serverURI;
+ rv = server->GetServerURI(serverURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCString messageID;
+ rv = msgHdr->GetMessageId(getter_Copies(messageID));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // we need to escape the message ID,
+ // it might contain characters which will mess us up later, like #
+ // see bug #120502
+ nsCString escapedMessageID;
+ MsgEscapeString(messageID, nsINetUtil::ESCAPE_URL_PATH, escapedMessageID);
+
+ nsAutoCString cancelURL(serverURI.get());
+ cancelURL += '/';
+ cancelURL += escapedMessageID;
+ cancelURL += "?cancel";
+
+ nsCString messageURI;
+ rv = GetUriForMsg(msgHdr, messageURI);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> resultUri;
+ return nntpService->CancelMessage(cancelURL, messageURI,
+ nullptr /* consumer */, nullptr, aMsgWindow,
+ getter_AddRefs(resultUri));
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetNewMessages(nsIMsgWindow* aMsgWindow,
+ nsIUrlListener* aListener) {
+ return GetNewsMessages(aMsgWindow, false, aListener);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetNextNMessages(nsIMsgWindow* aMsgWindow) {
+ return GetNewsMessages(aMsgWindow, true, nullptr);
+}
+
+nsresult nsMsgNewsFolder::GetNewsMessages(nsIMsgWindow* aMsgWindow,
+ bool aGetOld,
+ nsIUrlListener* aUrlListener) {
+ nsresult rv = NS_OK;
+
+ bool isNewsServer = false;
+ rv = GetIsServer(&isNewsServer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (isNewsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return server->PerformExpand(aMsgWindow);
+ }
+
+ nsCOMPtr<nsINntpService> nntpService =
+ do_GetService("@mozilla.org/messenger/nntpservice;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIURI> resultUri;
+ rv = nntpService->GetNewNews(nntpServer, mURI, aGetOld, this, aMsgWindow,
+ getter_AddRefs(resultUri));
+ if (aUrlListener && NS_SUCCEEDED(rv) && resultUri) {
+ nsCOMPtr<nsIMsgMailNewsUrl> msgUrl(do_QueryInterface(resultUri));
+ if (msgUrl) msgUrl->RegisterListener(aUrlListener);
+ }
+ return rv;
+}
+
+nsresult nsMsgNewsFolder::LoadNewsrcFileAndCreateNewsgroups() {
+ nsresult rv = NS_OK;
+ if (!mNewsrcFilePath) return NS_ERROR_FAILURE;
+
+ bool exists;
+ rv = mNewsrcFilePath->Exists(&exists);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!exists)
+ // it is ok for the newsrc file to not exist yet
+ return NS_OK;
+
+ nsCOMPtr<nsIInputStream> fileStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(fileStream), mNewsrcFilePath);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILineInputStream> lineInputStream(
+ do_QueryInterface(fileStream, &rv));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool more = true;
+ nsCString line;
+
+ while (more && NS_SUCCEEDED(rv)) {
+ rv = lineInputStream->ReadLine(line, &more);
+ if (line.IsEmpty()) continue;
+ HandleNewsrcLine(line.get(), line.Length());
+ }
+
+ fileStream->Close();
+ return rv;
+}
+
+int32_t nsMsgNewsFolder::HandleNewsrcLine(const char* line,
+ uint32_t line_size) {
+ nsresult rv;
+
+ /* guard against blank line lossage */
+ if (line[0] == '#' || line[0] == '\r' || line[0] == '\n') return 0;
+
+ if ((line[0] == 'o' || line[0] == 'O') && !PL_strncasecmp(line, "options", 7))
+ return RememberLine(nsDependentCString(line));
+
+ const char* s = nullptr;
+ const char* setStr = nullptr;
+ const char* end = line + line_size;
+
+ for (s = line; s < end; s++)
+ if ((*s == ':') || (*s == '!')) break;
+
+ if (*s == 0) /* What is this?? Well, don't just throw it away... */
+ return RememberLine(nsDependentCString(line));
+
+ bool subscribed = (*s == ':');
+ setStr = s + 1;
+
+ if (*line == '\0') return 0;
+
+ // previous versions of Communicator polluted the
+ // newsrc files with articles
+ // (this would happen when you clicked on a link like
+ // news://news.mozilla.org/3746EF3F.6080309@netscape.com)
+ //
+ // legal newsgroup names can't contain @ or %
+ //
+ // News group names are structured into parts separated by dots,
+ // for example "netscape.public.mozilla.mail-news".
+ // Each part may be up to 14 characters long, and should consist
+ // only of letters, digits, "+" and "-", with at least one letter
+ //
+ // @ indicates an article and %40 is @ escaped.
+ // previous versions of Communicator also dumped
+ // the escaped version into the newsrc file
+ //
+ // So lines like this in a newsrc file should be ignored:
+ // 3746EF3F.6080309@netscape.com:
+ // 3746EF3F.6080309%40netscape.com:
+ if (PL_strchr(line, '@') || PL_strstr(line, "%40"))
+ // skipping, it contains @ or %40
+ subscribed = false;
+
+ if (subscribed) {
+ // we're subscribed, so add it
+ nsCOMPtr<nsIMsgFolder> child;
+
+ rv = AddNewsgroup(Substring(line, s), nsDependentCString(setStr),
+ getter_AddRefs(child));
+ if (NS_FAILED(rv)) return -1;
+ } else {
+ rv = RememberUnsubscribedGroup(nsDependentCString(line),
+ nsDependentCString(setStr));
+ if (NS_FAILED(rv)) return -1;
+ }
+
+ return 0;
+}
+
+nsresult nsMsgNewsFolder::RememberUnsubscribedGroup(const nsACString& newsgroup,
+ const nsACString& setStr) {
+ mUnsubscribedNewsgroupLines.Append(newsgroup);
+ mUnsubscribedNewsgroupLines.AppendLiteral("! ");
+ if (!setStr.IsEmpty())
+ mUnsubscribedNewsgroupLines.Append(setStr);
+ else
+ mUnsubscribedNewsgroupLines.Append(MSG_LINEBREAK);
+ return NS_OK;
+}
+
+int32_t nsMsgNewsFolder::RememberLine(const nsACString& line) {
+ mOptionLines = line;
+ mOptionLines.Append(MSG_LINEBREAK);
+ return 0;
+}
+
+nsresult nsMsgNewsFolder::ForgetLine() {
+ mOptionLines.Truncate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetGroupUsername(nsACString& aGroupUsername) {
+ aGroupUsername = mGroupUsername;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetGroupUsername(
+ const nsACString& aGroupUsername) {
+ mGroupUsername = aGroupUsername;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetGroupPassword(nsACString& aGroupPassword) {
+ aGroupPassword = mGroupPassword;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetGroupPassword(
+ const nsACString& aGroupPassword) {
+ mGroupPassword = aGroupPassword;
+ return NS_OK;
+}
+
+nsresult nsMsgNewsFolder::CreateNewsgroupUrlForSignon(const char* ref,
+ nsAString& result) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ if (NS_FAILED(rv)) return rv;
+
+ bool singleSignon = true;
+ rv = nntpServer->GetSingleSignon(&singleSignon);
+
+ nsCOMPtr<nsIURL> url;
+ if (singleSignon) {
+ // Do not include username in the url when interacting with LoginManager.
+ nsCString serverURI = "news://"_ns;
+ nsCString hostName;
+ rv = server->GetHostName(hostName);
+ NS_ENSURE_SUCCESS(rv, rv);
+ serverURI.Append(hostName);
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(serverURI)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(mURI)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ int32_t port = 0;
+ rv = url->GetPort(&port);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (port <= 0) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ int32_t socketType;
+ nsresult rv = server->GetSocketType(&socketType);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only set this for ssl newsgroups as for non-ssl connections, we don't
+ // need to specify the port as it is the default for the protocol and
+ // password manager "blanks" those out.
+ if (socketType == nsMsgSocketType::SSL) {
+ rv = NS_MutateURI(url)
+ .SetPort(nsINntpUrl::DEFAULT_NNTPS_PORT)
+ .Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ nsCString rawResult;
+ if (ref) {
+ rv = NS_MutateURI(url).SetRef(nsDependentCString(ref)).Finalize(url);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = url->GetSpec(rawResult);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else {
+ // If the url doesn't have a path, make sure we don't get a '/' on the end
+ // as that will confuse searching in password manager.
+ nsCString spec;
+ rv = url->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!spec.IsEmpty() && spec[spec.Length() - 1] == '/')
+ rawResult = StringHead(spec, spec.Length() - 1);
+ else
+ rawResult = spec;
+ }
+ result = NS_ConvertASCIItoUTF16(rawResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetAuthenticationCredentials(nsIMsgWindow* aMsgWindow,
+ bool mayPrompt, bool mustPrompt,
+ bool* validCredentials) {
+ // Not strictly necessary, but it would help consumers to realize that this is
+ // a rather nonsensical combination.
+ NS_ENSURE_FALSE(mustPrompt && !mayPrompt, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_ARG_POINTER(validCredentials);
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ mozilla::components::StringBundle::Service();
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_UNEXPECTED);
+
+ nsresult rv;
+ nsCOMPtr<nsIStringBundle> bundle;
+ rv = bundleService->CreateBundle(NEWS_MSGS_URL, getter_AddRefs(bundle));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString signonUrl;
+ rv = CreateNewsgroupUrlForSignon(nullptr, signonUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If we don't have a username or password, try to load it via the login mgr.
+ // Do this even if mustPrompt is true, to prefill the dialog.
+ if (mGroupUsername.IsEmpty() || mGroupPassword.IsEmpty()) {
+ nsCOMPtr<nsILoginManager> loginMgr =
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(signonUrl, EmptyString(), signonUrl, logins);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (logins.Length() > 0) {
+ nsString uniUsername, uniPassword;
+ logins[0]->GetUsername(uniUsername);
+ logins[0]->GetPassword(uniPassword);
+ mGroupUsername = NS_LossyConvertUTF16toASCII(uniUsername);
+ mGroupPassword = NS_LossyConvertUTF16toASCII(uniPassword);
+
+ *validCredentials = true;
+ }
+ }
+
+ // Show the prompt if we need to
+ if (mustPrompt ||
+ (mayPrompt && (mGroupUsername.IsEmpty() || mGroupPassword.IsEmpty()))) {
+ nsCOMPtr<nsIAuthPrompt> authPrompt =
+ do_GetService("@mozilla.org/messenger/msgAuthPrompt;1");
+ if (!authPrompt) {
+ nsCOMPtr<nsIWindowWatcher> wwatch(
+ do_GetService(NS_WINDOWWATCHER_CONTRACTID));
+ if (wwatch) wwatch->GetNewAuthPrompter(0, getter_AddRefs(authPrompt));
+ if (!authPrompt) return NS_ERROR_FAILURE;
+ }
+
+ if (authPrompt) {
+ // Format the prompt text strings
+ nsString promptTitle, promptText;
+ bundle->GetStringFromName("enterUserPassTitle", promptTitle);
+
+ nsString serverName;
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ server->GetPrettyName(serverName);
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool singleSignon = true;
+ nntpServer->GetSingleSignon(&singleSignon);
+
+ if (singleSignon) {
+ AutoTArray<nsString, 1> params = {serverName};
+ bundle->FormatStringFromName("enterUserPassServer", params, promptText);
+ } else {
+ AutoTArray<nsString, 2> params = {mName, serverName};
+ bundle->FormatStringFromName("enterUserPassGroup", params, promptText);
+ }
+
+ // Fill the signon url for the dialog
+ nsString signonURL;
+ rv = CreateNewsgroupUrlForSignon(nullptr, signonURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Prefill saved username/password
+ char16_t* uniGroupUsername =
+ ToNewUnicode(NS_ConvertASCIItoUTF16(mGroupUsername));
+ char16_t* uniGroupPassword =
+ ToNewUnicode(NS_ConvertASCIItoUTF16(mGroupPassword));
+
+ // Prompt for the dialog
+ rv = authPrompt->PromptUsernameAndPassword(
+ promptTitle.get(), promptText.get(), signonURL.get(),
+ nsIAuthPrompt::SAVE_PASSWORD_PERMANENTLY, &uniGroupUsername,
+ &uniGroupPassword, validCredentials);
+
+ nsAutoString uniPasswordAdopted, uniUsernameAdopted;
+ uniPasswordAdopted.Adopt(uniGroupPassword);
+ uniUsernameAdopted.Adopt(uniGroupUsername);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Only use the username/password if the user didn't cancel.
+ if (*validCredentials) {
+ SetGroupUsername(NS_LossyConvertUTF16toASCII(uniUsernameAdopted));
+ SetGroupPassword(NS_LossyConvertUTF16toASCII(uniPasswordAdopted));
+ } else {
+ mGroupUsername.Truncate();
+ mGroupPassword.Truncate();
+ }
+ }
+ }
+
+ *validCredentials = !(mGroupUsername.IsEmpty() || mGroupPassword.IsEmpty());
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::ForgetAuthenticationCredentials() {
+ nsString signonUrl;
+ nsresult rv = CreateNewsgroupUrlForSignon(nullptr, signonUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILoginManager> loginMgr =
+ do_GetService(NS_LOGINMANAGER_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsTArray<RefPtr<nsILoginInfo>> logins;
+ rv = loginMgr->FindLogins(signonUrl, EmptyString(), signonUrl, logins);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // There should only be one-login stored for this url, however just in case
+ // there isn't.
+ for (uint32_t i = 0; i < logins.Length(); ++i)
+ loginMgr->RemoveLogin(logins[i]);
+
+ // Clear out the saved passwords for anyone else who tries to call.
+ mGroupUsername.Truncate();
+ mGroupPassword.Truncate();
+
+ return NS_OK;
+}
+
+// change order of subfolders (newsgroups)
+NS_IMETHODIMP nsMsgNewsFolder::ReorderGroup(nsIMsgFolder* aNewsgroupToMove,
+ nsIMsgFolder* aRefNewsgroup) {
+ // if folders are identical do nothing
+ if (aNewsgroupToMove == aRefNewsgroup) return NS_OK;
+
+ nsresult rv = NS_OK;
+
+ // get index for aNewsgroupToMove
+ int32_t indexNewsgroupToMove = mSubFolders.IndexOf(aNewsgroupToMove);
+ if (indexNewsgroupToMove == -1)
+ // aNewsgroupToMove is no subfolder of this folder
+ return NS_ERROR_INVALID_ARG;
+
+ // get index for aRefNewsgroup
+ int32_t indexRefNewsgroup = mSubFolders.IndexOf(aRefNewsgroup);
+ if (indexRefNewsgroup == -1)
+ // aRefNewsgroup is no subfolder of this folder
+ return NS_ERROR_INVALID_ARG;
+
+ // Move NewsgroupToMove to new index and set new sort order.
+
+ nsCOMPtr<nsIMsgFolder> newsgroup = mSubFolders[indexNewsgroupToMove];
+
+ mSubFolders.RemoveObjectAt(indexNewsgroupToMove);
+ mSubFolders.InsertObjectAt(newsgroup, indexRefNewsgroup);
+
+ for (uint32_t i = 0; i < mSubFolders.Length(); i++) {
+ mSubFolders[i]->SetSortOrder(kNewsSortOffset + i);
+ nsAutoString name;
+ mSubFolders[i]->GetName(name);
+ NotifyFolderRemoved(mSubFolders[i]);
+ NotifyFolderAdded(mSubFolders[i]);
+ }
+
+ // write changes back to file
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->SetNewsrcHasChanged(true);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = nntpServer->WriteNewsrcFile();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return rv;
+}
+
+nsresult nsMsgNewsFolder::CreateBaseMessageURI(const nsACString& aURI) {
+ return nsCreateNewsBaseMessageURI(aURI, mBaseMessageURI);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetCharset(nsACString& charset) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsINntpIncomingServer> nserver(do_QueryInterface(server));
+ NS_ENSURE_TRUE(nserver, NS_ERROR_NULL_POINTER);
+ return nserver->GetCharset(charset);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetNewsrcLine(nsACString& newsrcLine) {
+ nsresult rv;
+ nsString newsgroupNameUtf16;
+ rv = GetName(newsgroupNameUtf16);
+ if (NS_FAILED(rv)) return rv;
+ NS_ConvertUTF16toUTF8 newsgroupName(newsgroupNameUtf16);
+
+ newsrcLine = newsgroupName;
+ newsrcLine.Append(':');
+
+ if (mReadSet) {
+ nsCString setStr;
+ mReadSet->Output(getter_Copies(setStr));
+ if (NS_SUCCEEDED(rv)) {
+ newsrcLine.Append(' ');
+ newsrcLine.Append(setStr);
+ newsrcLine.AppendLiteral(MSG_LINEBREAK);
+ }
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetReadSetFromStr(const nsACString& newsrcLine) {
+ mReadSet = nsMsgKeySet::Create(PromiseFlatCString(newsrcLine).get());
+ NS_ENSURE_TRUE(mReadSet, NS_ERROR_OUT_OF_MEMORY);
+
+ // Now that mReadSet is recreated, make sure it's stored in the db as well.
+ nsCOMPtr<nsINewsDatabase> db = do_QueryInterface(mDatabase);
+ if (db) // it's ok not to have a db here.
+ db->SetReadSet(mReadSet);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetUnsubscribedNewsgroupLines(
+ nsACString& aUnsubscribedNewsgroupLines) {
+ aUnsubscribedNewsgroupLines = mUnsubscribedNewsgroupLines;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetOptionLines(nsACString& optionLines) {
+ optionLines = mOptionLines;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::OnReadChanged(nsIDBChangeListener* aInstigator) {
+ return SetNewsrcHasChanged(true);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetUnicodeName(nsAString& aName) { return GetName(aName); }
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetRawName(nsACString& aRawName) {
+ nsresult rv;
+ if (mRawName.IsEmpty()) {
+ nsString name;
+ rv = GetName(name);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // convert to the server-side encoding
+ nsCOMPtr<nsINntpIncomingServer> nntpServer;
+ rv = GetNntpServer(getter_AddRefs(nntpServer));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString dataCharset;
+ rv = nntpServer->GetCharset(dataCharset);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsMsgI18NConvertFromUnicode(dataCharset, name, mRawName);
+
+ if (NS_FAILED(rv)) LossyCopyUTF16toASCII(name, mRawName);
+ }
+ aRawName = mRawName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetNntpServer(nsINntpIncomingServer** result) {
+ nsresult rv;
+ NS_ENSURE_ARG_POINTER(result);
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ if (NS_FAILED(rv)) return rv;
+
+ nsCOMPtr<nsINntpIncomingServer> nntpServer = do_QueryInterface(server, &rv);
+ if (NS_FAILED(rv)) return rv;
+ nntpServer.forget(result);
+ return NS_OK;
+}
+
+// this gets called after the message actually gets cancelled
+// it removes the cancelled message from the db
+NS_IMETHODIMP nsMsgNewsFolder::RemoveMessage(nsMsgKey key) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv,
+ rv); // if GetDatabase succeeds, mDatabase will be non-null
+
+ // Notify listeners of a delete for a single message
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+ if (notifier) {
+ nsCOMPtr<nsIMsgDBHdr> msgHdr;
+ rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(msgHdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyMsgsDeleted({msgHdr.get()});
+ }
+ return mDatabase->DeleteMessage(key, nullptr, false);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::RemoveMessages(
+ const nsTArray<nsMsgKey>& aMsgKeys) {
+ nsresult rv = GetDatabase();
+ NS_ENSURE_SUCCESS(rv,
+ rv); // if GetDatabase succeeds, mDatabase will be non-null
+
+ // Notify listeners of a multiple message delete
+ nsCOMPtr<nsIMsgFolderNotificationService> notifier(
+ do_GetService("@mozilla.org/messenger/msgnotificationservice;1"));
+
+ if (notifier) {
+ nsTArray<RefPtr<nsIMsgDBHdr>> msgHdrs;
+ rv = MsgGetHeadersFromKeys(mDatabase, aMsgKeys, msgHdrs);
+ NS_ENSURE_SUCCESS(rv, rv);
+ notifier->NotifyMsgsDeleted(msgHdrs);
+ }
+
+ return mDatabase->DeleteMessages(aMsgKeys, nullptr);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CancelComplete() {
+ NotifyFolderEvent(kDeleteOrMoveMsgCompleted);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::CancelFailed() {
+ NotifyFolderEvent(kDeleteOrMoveMsgFailed);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetSaveArticleOffline(bool* aBool) {
+ NS_ENSURE_ARG(aBool);
+ *aBool = m_downloadMessageForOfflineUse;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetSaveArticleOffline(bool aBool) {
+ m_downloadMessageForOfflineUse = aBool;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) {
+ nsTArray<nsMsgKey> srcKeyArray;
+ SetSaveArticleOffline(true);
+ nsresult rv = NS_OK;
+
+ // build up message keys.
+ if (mDatabase) {
+ nsCOMPtr<nsIMsgEnumerator> enumerator;
+ rv = mDatabase->EnumerateMessages(getter_AddRefs(enumerator));
+ if (NS_SUCCEEDED(rv) && enumerator) {
+ bool hasMore;
+ while (NS_SUCCEEDED(rv = enumerator->HasMoreElements(&hasMore)) &&
+ hasMore) {
+ nsCOMPtr<nsIMsgDBHdr> header;
+ rv = enumerator->GetNext(getter_AddRefs(header));
+ if (header && NS_SUCCEEDED(rv)) {
+ bool shouldStoreMsgOffline = false;
+ nsMsgKey msgKey;
+ header->GetMessageKey(&msgKey);
+ MsgFitsDownloadCriteria(msgKey, &shouldStoreMsgOffline);
+ if (shouldStoreMsgOffline) srcKeyArray.AppendElement(msgKey);
+ }
+ }
+ }
+ }
+ RefPtr<DownloadNewsArticlesToOfflineStore> downloadState =
+ new DownloadNewsArticlesToOfflineStore(msgWindow, mDatabase, this);
+ rv = downloadState->DownloadArticles(msgWindow, this, &srcKeyArray);
+ (void)RefreshSizeOnDisk();
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages, nsIMsgWindow* window) {
+ nsresult rv;
+ SetSaveArticleOffline(
+ true); // ### TODO need to clear this when we've finished
+ // build up message keys.
+ nsTArray<nsMsgKey> srcKeyArray(messages.Length());
+ for (nsIMsgDBHdr* hdr : messages) {
+ nsMsgKey key;
+ rv = hdr->GetMessageKey(&key);
+ if (NS_SUCCEEDED(rv)) srcKeyArray.AppendElement(key);
+ }
+ RefPtr<DownloadNewsArticlesToOfflineStore> downloadState =
+ new DownloadNewsArticlesToOfflineStore(window, mDatabase, this);
+
+ rv = downloadState->DownloadArticles(window, this, &srcKeyArray);
+ (void)RefreshSizeOnDisk();
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) {
+ nsMsgKey key;
+ hdr->GetMessageKey(&key);
+
+ uint64_t offset = 0;
+ uint32_t size = 0;
+ nsCOMPtr<nsIInputStream> rawStream;
+ nsresult rv =
+ GetOfflineFileStream(key, &offset, &size, getter_AddRefs(rawStream));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ RefPtr<mozilla::SlicedInputStream> slicedStream =
+ new mozilla::SlicedInputStream(rawStream.forget(), offset,
+ uint64_t(size));
+ slicedStream.forget(stream);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyDownloadBegin(nsMsgKey key) {
+ if (!m_downloadMessageForOfflineUse) {
+ return NS_OK;
+ }
+ nsresult rv = GetMessageHeader(key, getter_AddRefs(m_offlineHeader));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return StartNewOfflineMessage(); // Sets up m_tempMessageStream et al.
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyDownloadedLine(nsACString const& line) {
+ nsresult rv = NS_OK;
+ if (m_tempMessageStream) {
+ m_numOfflineMsgLines++;
+ uint32_t count = 0;
+ rv = m_tempMessageStream->Write(line.BeginReading(), line.Length(), &count);
+ NS_ENSURE_SUCCESS(rv, rv);
+ m_tempMessageStreamBytesWritten += count;
+ }
+
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyDownloadEnd(nsresult status) {
+ if (m_tempMessageStream) {
+ return EndNewOfflineMessage(status);
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::NotifyFinishedDownloadinghdrs() {
+ bool wasCached = !!mDatabase;
+ ChangeNumPendingTotalMessages(-mNumPendingTotalMessages);
+ ChangeNumPendingUnread(-mNumPendingUnreadMessages);
+ bool filtersRun;
+ // run the bayesian spam filters, if enabled.
+ CallFilterPlugins(nullptr, &filtersRun);
+
+ // If the DB was not open before, close our reference to it now.
+ if (!wasCached && mDatabase) {
+ mDatabase->Commit(nsMsgDBCommitType::kLargeCommit);
+ mDatabase->RemoveListener(this);
+ // This also clears all of the cached headers that may have been added while
+ // we were downloading messages (and those clearing refcount cycles in the
+ // database).
+ mDatabase->ClearCachedHdrs();
+ mDatabase = nullptr;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) {
+ nsresult rv;
+ rv = GetDatabase();
+ if (mDatabase) ApplyRetentionSettings();
+ (void)RefreshSizeOnDisk();
+ return rv;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::ApplyRetentionSettings() {
+ return nsMsgDBFolder::ApplyRetentionSettings(false);
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetMessageIdForKey(nsMsgKey key,
+ nsACString& result) {
+ nsresult rv = GetDatabase();
+ if (!mDatabase) return rv;
+ nsCOMPtr<nsIMsgDBHdr> hdr;
+ rv = mDatabase->GetMsgHdrForKey(key, getter_AddRefs(hdr));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCString id;
+ rv = hdr->GetMessageId(getter_Copies(id));
+ result.Assign(id);
+ return rv;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::SetSortOrder(int32_t order) {
+ int32_t oldOrder = mSortOrder;
+ mSortOrder = order;
+
+ NotifyIntPropertyChanged(kSortOrder, oldOrder, order);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::GetSortOrder(int32_t* order) {
+ NS_ENSURE_ARG_POINTER(order);
+ *order = mSortOrder;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsMsgNewsFolder::Shutdown(bool shutdownChildren) {
+ if (mFilterList) {
+ // close the filter log stream
+ nsresult rv = mFilterList->SetLogStream(nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mFilterList = nullptr;
+ }
+
+ mInitialized = false;
+ if (mReadSet) {
+ // the nsINewsDatabase holds a weak ref to the readset,
+ // and we outlive the db, so it's safe to delete it here.
+ nsCOMPtr<nsINewsDatabase> db = do_QueryInterface(mDatabase);
+ if (db) db->SetReadSet(nullptr);
+ mReadSet = nullptr;
+ }
+ return nsMsgDBFolder::Shutdown(shutdownChildren);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::SetFilterList(nsIMsgFilterList* aFilterList) {
+ if (mIsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->SetFilterList(aFilterList);
+ }
+
+ mFilterList = aFilterList;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ if (mIsServer) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ return server->GetFilterList(aMsgWindow, aResult);
+ }
+
+ if (!mFilterList) {
+ nsCOMPtr<nsIFile> thisFolder;
+ nsresult rv = GetFilePath(getter_AddRefs(thisFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIFile> filterFile =
+ do_CreateInstance(NS_LOCAL_FILE_CONTRACTID, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+ ;
+ rv = filterFile->InitWithFile(thisFolder);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // in 4.x, the news filter file was
+ // C:\Program
+ // Files\Netscape\Users\meer\News\host-news.mcom.com\mcom.test.dat where the
+ // summary file was C:\Program
+ // Files\Netscape\Users\meer\News\host-news.mcom.com\mcom.test.snm we make
+ // the rules file ".dat" in mozilla, so that migration works.
+
+ // NOTE:
+ // we don't we need to call NS_MsgHashIfNecessary()
+ // it's already been hashed, if necessary
+ nsCString filterFileName;
+ rv = filterFile->GetNativeLeafName(filterFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ filterFileName.AppendLiteral(".dat");
+
+ rv = filterFile->SetNativeLeafName(filterFileName);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgFilterService> filterService =
+ do_GetService("@mozilla.org/messenger/services/filters;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = filterService->OpenFilterList(filterFile, this, aMsgWindow,
+ getter_AddRefs(mFilterList));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ NS_IF_ADDREF(*aResult = mFilterList);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aResult) {
+ // We don't support pluggable filter list types for news.
+ return GetFilterList(aMsgWindow, aResult);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::SetEditableFilterList(nsIMsgFilterList* aFilterList) {
+ return SetFilterList(aFilterList);
+}
+
+NS_IMETHODIMP
+nsMsgNewsFolder::GetIncomingServerType(nsACString& serverType) {
+ serverType.AssignLiteral("nntp");
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNewsFolder.h b/comm/mailnews/news/src/nsNewsFolder.h
new file mode 100644
index 0000000000..e18a768375
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsFolder.h
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ Interface for representing News folders.
+*/
+
+#ifndef nsMsgNewsFolder_h__
+#define nsMsgNewsFolder_h__
+
+#include "mozilla/Attributes.h"
+#include "nsMsgDBFolder.h"
+#include "nsIFile.h"
+#include "nsNewsUtils.h"
+#include "nsMsgKeySet.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsCOMPtr.h"
+#include "nsIMsgFilterList.h"
+
+class nsMsgNewsFolder : public nsMsgDBFolder, public nsIMsgNewsFolder {
+ public:
+ nsMsgNewsFolder(void);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIMSGNEWSFOLDER
+
+ // nsIMsgFolder methods:
+ NS_IMETHOD GetSubFolders(nsTArray<RefPtr<nsIMsgFolder>>& folders) override;
+
+ NS_IMETHOD UpdateFolder(nsIMsgWindow* aWindow) override;
+
+ NS_IMETHOD CreateSubfolder(const nsAString& folderName,
+ nsIMsgWindow* msgWindow) override;
+
+ NS_IMETHOD DeleteStorage() override;
+ NS_IMETHOD Rename(const nsAString& newName, nsIMsgWindow* msgWindow) override;
+
+ NS_IMETHOD GetAbbreviatedName(nsAString& aAbbreviatedName) override;
+
+ NS_IMETHOD GetFolderURL(nsACString& url) override;
+
+ NS_IMETHOD GetExpungedBytesCount(int64_t* count);
+ NS_IMETHOD GetDeletable(bool* deletable) override;
+
+ NS_IMETHOD RefreshSizeOnDisk();
+
+ NS_IMETHOD GetSizeOnDisk(int64_t* size) override;
+
+ NS_IMETHOD GetDBFolderInfoAndDB(nsIDBFolderInfo** folderInfo,
+ nsIMsgDatabase** db) override;
+
+ NS_IMETHOD DeleteMessages(nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* msgWindow, bool deleteStorage,
+ bool isMove, nsIMsgCopyServiceListener* listener,
+ bool allowUndo) override;
+ NS_IMETHOD GetNewMessages(nsIMsgWindow* aWindow,
+ nsIUrlListener* aListener) override;
+
+ NS_IMETHOD GetCanSubscribe(bool* aResult) override;
+ NS_IMETHOD GetCanFileMessages(bool* aResult) override;
+ NS_IMETHOD GetCanCreateSubfolders(bool* aResult) override;
+ NS_IMETHOD GetCanRename(bool* aResult) override;
+ NS_IMETHOD GetCanCompact(bool* aResult) override;
+ NS_IMETHOD OnReadChanged(nsIDBChangeListener* aInstigator) override;
+
+ NS_IMETHOD DownloadMessagesForOffline(
+ nsTArray<RefPtr<nsIMsgDBHdr>> const& messages,
+ nsIMsgWindow* window) override;
+ NS_IMETHOD GetLocalMsgStream(nsIMsgDBHdr* hdr,
+ nsIInputStream** stream) override;
+ NS_IMETHOD Compact(nsIUrlListener* aListener,
+ nsIMsgWindow* aMsgWindow) override;
+ NS_IMETHOD DownloadAllForOffline(nsIUrlListener* listener,
+ nsIMsgWindow* msgWindow) override;
+ NS_IMETHOD GetSortOrder(int32_t* order) override;
+ NS_IMETHOD SetSortOrder(int32_t order) override;
+
+ NS_IMETHOD Shutdown(bool shutdownChildren) override;
+
+ NS_IMETHOD GetFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aFilterList) override;
+ NS_IMETHOD GetEditableFilterList(nsIMsgWindow* aMsgWindow,
+ nsIMsgFilterList** aFilterList) override;
+ NS_IMETHOD SetFilterList(nsIMsgFilterList* aFilterList) override;
+ NS_IMETHOD SetEditableFilterList(nsIMsgFilterList* aFilterList) override;
+ NS_IMETHOD ApplyRetentionSettings() override;
+ NS_IMETHOD GetIncomingServerType(nsACString& serverType) override;
+
+ protected:
+ virtual ~nsMsgNewsFolder();
+ // helper routine to parse the URI and update member variables
+ nsresult AbbreviatePrettyName(nsAString& prettyName, int32_t fullwords);
+ nsresult ParseFolder(nsIFile* path);
+ nsresult CreateSubFolders(nsIFile* path);
+ nsresult AddDirectorySeparator(nsIFile* path);
+ nsresult GetDatabase() override;
+ virtual nsresult CreateChildFromURI(const nsACString& uri,
+ nsIMsgFolder** folder) override;
+
+ nsresult LoadNewsrcFileAndCreateNewsgroups();
+ int32_t RememberLine(const nsACString& line);
+ nsresult RememberUnsubscribedGroup(const nsACString& newsgroup,
+ const nsACString& setStr);
+ nsresult ForgetLine(void);
+ nsresult GetNewsMessages(nsIMsgWindow* aMsgWindow, bool getOld,
+ nsIUrlListener* aListener);
+
+ int32_t HandleNewsrcLine(const char* line, uint32_t line_size);
+ virtual nsresult CreateBaseMessageURI(const nsACString& aURI) override;
+
+ protected:
+ int64_t mExpungedBytes;
+ bool mGettingNews;
+ bool mInitialized;
+ bool m_downloadMessageForOfflineUse;
+
+ nsCString mOptionLines;
+ nsCString mUnsubscribedNewsgroupLines;
+ RefPtr<nsMsgKeySet> mReadSet;
+
+ nsCOMPtr<nsIFile> mNewsrcFilePath;
+
+ // used for auth news
+ nsCString mGroupUsername;
+ nsCString mGroupPassword;
+
+ // the name of the newsgroup.
+ nsCString mRawName;
+ int32_t mSortOrder;
+
+ private:
+ /**
+ * Constructs a signon url for use in login manager.
+ *
+ * @param ref The URI ref (should be null unless working with legacy).
+ * @param result The result of the string
+ */
+ nsresult CreateNewsgroupUrlForSignon(const char* ref, nsAString& result);
+ nsCOMPtr<nsIMsgFilterList> mFilterList;
+};
+
+#endif // nsMsgNewsFolder_h__
diff --git a/comm/mailnews/news/src/nsNewsUtils.cpp b/comm/mailnews/news/src/nsNewsUtils.cpp
new file mode 100644
index 0000000000..b9afed9c95
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsUtils.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h"
+#include "nntpCore.h"
+#include "nsNewsUtils.h"
+#include "nsMsgUtils.h"
+
+/* parses NewsMessageURI */
+nsresult nsParseNewsMessageURI(const nsACString& uri, nsCString& group,
+ nsMsgKey* key) {
+ NS_ENSURE_ARG_POINTER(key);
+
+ const nsPromiseFlatCString& uriStr = PromiseFlatCString(uri);
+ int32_t keySeparator = uriStr.FindChar('#');
+ if (keySeparator != -1) {
+ int32_t keyEndSeparator = MsgFindCharInSet(uriStr, "?&", keySeparator);
+
+ // Grab between the last '/' and the '#' for the key
+ group = StringHead(uriStr, keySeparator);
+ int32_t groupSeparator = group.RFind("/");
+ if (groupSeparator == -1) return NS_ERROR_FAILURE;
+
+ // Our string APIs don't let us unescape into the same buffer from earlier,
+ // so escape into a temporary
+ nsAutoCString unescapedGroup;
+ MsgUnescapeString(Substring(group, groupSeparator + 1), 0, unescapedGroup);
+ group = unescapedGroup;
+
+ nsAutoCString keyStr;
+ if (keyEndSeparator != -1)
+ keyStr = Substring(uriStr, keySeparator + 1,
+ keyEndSeparator - (keySeparator + 1));
+ else
+ keyStr = Substring(uriStr, keySeparator + 1);
+ nsresult errorCode;
+ *key = keyStr.ToInteger(&errorCode);
+
+ return errorCode;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult nsCreateNewsBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI) {
+ nsAutoCString tailURI(baseURI);
+
+ // chop off news:/
+ if (tailURI.Find(kNewsRootURI) == 0) tailURI.Cut(0, PL_strlen(kNewsRootURI));
+
+ baseMessageURI = kNewsMessageRootURI;
+ baseMessageURI += tailURI;
+
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNewsUtils.h b/comm/mailnews/news/src/nsNewsUtils.h
new file mode 100644
index 0000000000..601858de3f
--- /dev/null
+++ b/comm/mailnews/news/src/nsNewsUtils.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef NS_NEWSUTILS_H
+#define NS_NEWSUTILS_H
+
+#include "nsString.h"
+#include "MailNewsTypes2.h"
+
+#define kNewsRootURI "news:/"
+#define kNntpRootURI "nntp:/"
+#define kNewsMessageRootURI "news-message:/"
+#define kNewsURIGroupQuery "?group="
+#define kNewsURIKeyQuery "&key="
+
+#define kNewsRootURILen 6
+#define kNntpRootURILen 6
+#define kNewsMessageRootURILen 14
+#define kNewsURIGroupQueryLen 7
+#define kNewsURIKeyQueryLen 5
+
+extern nsresult nsParseNewsMessageURI(const nsACString& uri, nsCString& group,
+ nsMsgKey* key);
+
+extern nsresult nsCreateNewsBaseMessageURI(const nsACString& baseURI,
+ nsCString& baseMessageURI);
+
+#endif // NS_NEWSUTILS_H
diff --git a/comm/mailnews/news/src/nsNntpUrl.cpp b/comm/mailnews/news/src/nsNntpUrl.cpp
new file mode 100644
index 0000000000..ab7dcc08ab
--- /dev/null
+++ b/comm/mailnews/news/src/nsNntpUrl.cpp
@@ -0,0 +1,476 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "msgCore.h" // precompiled header...
+
+#include "nsNntpUrl.h"
+
+#include "nsString.h"
+#include "nsNewsUtils.h"
+#include "nsMsgUtils.h"
+
+#include "nntpCore.h"
+
+#include "nsCOMPtr.h"
+#include "nsIMsgFolder.h"
+#include "nsIMsgNewsFolder.h"
+#include "nsINntpService.h"
+#include "nsIMsgMessageService.h"
+#include "nsIMsgAccountManager.h"
+#include "nsServiceManagerUtils.h"
+
+nsNntpUrl::nsNntpUrl() {
+ m_newsgroupPost = nullptr;
+ m_newsAction = nsINntpUrl::ActionUnknown;
+ m_addDummyEnvelope = false;
+ m_canonicalLineEnding = false;
+ m_filePath = nullptr;
+ m_getOldMessages = false;
+ m_key = nsMsgKey_None;
+ mAutodetectCharset = false;
+}
+
+nsNntpUrl::~nsNntpUrl() {}
+
+NS_IMPL_ADDREF_INHERITED(nsNntpUrl, nsMsgMailNewsUrl)
+NS_IMPL_RELEASE_INHERITED(nsNntpUrl, nsMsgMailNewsUrl)
+
+NS_INTERFACE_MAP_BEGIN(nsNntpUrl)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsINntpUrl)
+ NS_INTERFACE_MAP_ENTRY(nsINntpUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgMessageUrl)
+ NS_INTERFACE_MAP_ENTRY(nsIMsgI18NUrl)
+NS_INTERFACE_MAP_END_INHERITING(nsMsgMailNewsUrl)
+
+////////////////////////////////////////////////////////////////////////////////
+// Begin nsINntpUrl specific support
+////////////////////////////////////////////////////////////////////////////////
+
+/* News URI parsing explanation:
+ * We support 3 different news URI schemes, essentially boiling down to 8
+ * different formats:
+ * news://host/group
+ * news://host/message
+ * news://host/
+ * news:group
+ * news:message
+ * nntp://host/group
+ * nntp://host/group/key
+ * news-message://host/group#key
+ *
+ * In addition, we use queries on the news URIs with authorities for internal
+ * NNTP processing. The most important one is ?group=group&key=key, for cache
+ * canonicalization.
+ */
+
+nsresult nsNntpUrl::SetSpecInternal(const nsACString& aSpec) {
+ // For [s]news: URIs, we need to munge the spec if it is no authority, because
+ // the URI parser guesses the wrong thing otherwise
+ nsCString parseSpec(aSpec);
+ int32_t colon = parseSpec.Find(":");
+
+ // Our smallest scheme is 4 characters long, so colon must be at least 4
+ if (colon < 4 || colon + 1 == (int32_t)parseSpec.Length())
+ return NS_ERROR_MALFORMED_URI;
+
+ if (Substring(parseSpec, colon - 4, 4).EqualsLiteral("news") &&
+ parseSpec[colon + 1] != '/') {
+ // To make this parse properly, we add in three slashes, which convinces the
+ // parser that the authority component is empty.
+ parseSpec = Substring(aSpec, 0, colon + 1);
+ parseSpec.AppendLiteral("///");
+ parseSpec += Substring(aSpec, colon + 1);
+ }
+
+ nsresult rv = nsMsgMailNewsUrl::SetSpecInternal(parseSpec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString scheme;
+ rv = GetScheme(scheme);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (scheme.EqualsLiteral("news") || scheme.EqualsLiteral("snews"))
+ rv = ParseNewsURL();
+ else if (scheme.EqualsLiteral("nntp") || scheme.EqualsLiteral("nntps"))
+ rv = ParseNntpURL();
+ else if (scheme.EqualsLiteral("news-message")) {
+ nsAutoCString spec;
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = nsParseNewsMessageURI(spec, m_group, &m_key);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+ } else
+ return NS_ERROR_MALFORMED_URI;
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = DetermineNewsAction();
+ NS_ENSURE_SUCCESS(rv, rv);
+ return rv;
+}
+
+nsresult nsNntpUrl::ParseNewsURL() {
+ // The path here is the group/msgid portion
+ nsAutoCString path;
+ nsresult rv = GetFilePath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Drop the potential beginning from the path
+ if (path.Length() && path[0] == '/') path = Substring(path, 1);
+
+ // The presence of an `@' is a sign we have a msgid
+ if (path.Find("@") != -1 || path.Find("%40") != -1) {
+ MsgUnescapeString(path, 0, m_messageID);
+
+ // Set group, key for ?group=foo&key=123 uris
+ nsAutoCString spec;
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ int32_t groupPos = spec.Find(kNewsURIGroupQuery); // find ?group=
+ int32_t keyPos = spec.Find(kNewsURIKeyQuery); // find &key=
+ if (groupPos != kNotFound && keyPos != kNotFound) {
+ // get group name and message key
+ m_group = Substring(spec, groupPos + kNewsURIGroupQueryLen,
+ keyPos - groupPos - kNewsURIGroupQueryLen);
+ nsCString keyStr(Substring(spec, keyPos + kNewsURIKeyQueryLen));
+ m_key = keyStr.ToInteger(&rv, 10);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+ }
+ } else
+ MsgUnescapeString(path, 0, m_group);
+
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::ParseNntpURL() {
+ nsAutoCString path;
+ nsresult rv = GetFilePath(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (path.Length() > 0 && path[0] == '/') path = Substring(path, 1);
+
+ if (path.IsEmpty()) return NS_ERROR_MALFORMED_URI;
+
+ int32_t slash = path.FindChar('/');
+ if (slash == -1) {
+ m_group = path;
+ m_key = nsMsgKey_None;
+ } else {
+ m_group = Substring(path, 0, slash);
+ nsAutoCString keyStr;
+ keyStr = Substring(path, slash + 1);
+ m_key = keyStr.ToInteger(&rv, 10);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_MALFORMED_URI);
+
+ // Keys must be at least one
+ if (m_key == 0) return NS_ERROR_MALFORMED_URI;
+ }
+
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::DetermineNewsAction() {
+ nsAutoCString path;
+ nsresult rv = nsMsgMailNewsUrl::GetPathQueryRef(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString query;
+ rv = GetQuery(query);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (query.EqualsLiteral("cancel")) {
+ m_newsAction = nsINntpUrl::ActionCancelArticle;
+ return NS_OK;
+ }
+ if (query.EqualsLiteral("list-ids")) {
+ m_newsAction = nsINntpUrl::ActionListIds;
+ return NS_OK;
+ }
+ if (query.EqualsLiteral("newgroups")) {
+ m_newsAction = nsINntpUrl::ActionListNewGroups;
+ return NS_OK;
+ }
+ if (StringBeginsWith(query, "search"_ns)) {
+ m_newsAction = nsINntpUrl::ActionSearch;
+ return NS_OK;
+ }
+ if (StringBeginsWith(query, "part="_ns) || query.Find("&part=") > 0) {
+ // news://news.mozilla.org:119/3B98D201.3020100%40cs.com?part=1
+ // news://news.mozilla.org:119/b58dme%24aia2%40ripley.netscape.com?header=print&part=1.2&type=image/jpeg&filename=Pole.jpg
+ m_newsAction = nsINntpUrl::ActionFetchPart;
+ return NS_OK;
+ }
+
+ if (!m_messageID.IsEmpty() || m_key != nsMsgKey_None) {
+ m_newsAction = nsINntpUrl::ActionFetchArticle;
+ return NS_OK;
+ }
+
+ if (m_group.Find("*") >= 0) {
+ // If the group is a wildmat, list groups instead of grabbing a group.
+ m_newsAction = nsINntpUrl::ActionListGroups;
+ return NS_OK;
+ }
+ if (!m_group.IsEmpty()) {
+ m_newsAction = nsINntpUrl::ActionGetNewNews;
+ return NS_OK;
+ }
+
+ // At this point, we have a URI that contains neither a query, a group, nor a
+ // message ID. Ergo, we don't know what it is.
+ m_newsAction = nsINntpUrl::ActionUnknown;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetGetOldMessages(bool aGetOldMessages) {
+ m_getOldMessages = aGetOldMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetGetOldMessages(bool* aGetOldMessages) {
+ NS_ENSURE_ARG(aGetOldMessages);
+ *aGetOldMessages = m_getOldMessages;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetNewsAction(nsNewsAction* aNewsAction) {
+ if (aNewsAction) *aNewsAction = m_newsAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetNewsAction(nsNewsAction aNewsAction) {
+ m_newsAction = aNewsAction;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetGroup(nsACString& group) {
+ group = m_group;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetMessageID(nsACString& messageID) {
+ messageID = m_messageID;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetKey(nsMsgKey* key) {
+ NS_ENSURE_ARG_POINTER(key);
+ *key = m_key;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetCharset(nsACString& charset) {
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ nsresult rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+ nsCOMPtr<nsINntpIncomingServer> nserver(do_QueryInterface(server));
+ NS_ENSURE_TRUE(nserver, NS_ERROR_NULL_POINTER);
+ return nserver->GetCharset(charset);
+}
+
+NS_IMETHODIMP nsNntpUrl::GetNormalizedSpec(nsACString& aPrincipalSpec) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetUri(const nsACString& aURI) {
+ mURI = aURI;
+ return NS_OK;
+}
+
+// from nsIMsgMessageUrl
+NS_IMETHODIMP nsNntpUrl::GetUri(nsACString& aURI) {
+ nsresult rv = NS_OK;
+
+ // if we have been given a uri to associate with this url, then use it
+ // otherwise try to reconstruct a URI on the fly....
+ if (mURI.IsEmpty()) {
+ nsAutoCString spec;
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ mURI = spec;
+ }
+
+ aURI = mURI;
+ return rv;
+}
+
+NS_IMPL_GETSET(nsNntpUrl, AddDummyEnvelope, bool, m_addDummyEnvelope)
+NS_IMPL_GETSET(nsNntpUrl, CanonicalLineEnding, bool, m_canonicalLineEnding)
+
+NS_IMETHODIMP nsNntpUrl::SetMessageFile(nsIFile* aFile) {
+ m_messageFile = aFile;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetMessageFile(nsIFile** aFile) {
+ if (aFile) NS_IF_ADDREF(*aFile = m_messageFile);
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////////////
+// End nsINntpUrl specific support
+////////////////////////////////////////////////////////////////////////////////
+
+nsresult nsNntpUrl::SetMessageToPost(nsINNTPNewsgroupPost* post) {
+ m_newsgroupPost = post;
+ if (post) SetNewsAction(nsINntpUrl::ActionPostArticle);
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::GetMessageToPost(nsINNTPNewsgroupPost** aPost) {
+ NS_ENSURE_ARG_POINTER(aPost);
+ NS_IF_ADDREF(*aPost = m_newsgroupPost);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetMessageHeader(nsIMsgDBHdr** aMsgHdr) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMessageService> msgService =
+ do_GetService("@mozilla.org/messenger/messageservice;1?type=news", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsAutoCString spec(mOriginalSpec);
+ if (spec.IsEmpty()) {
+ // Handle the case where necko directly runs an internal news:// URL,
+ // one that looks like news://host/message-id?group=mozilla.announce&key=15
+ // Other sorts of URLs -- e.g. news://host/message-id -- will not succeed.
+ rv = GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ return msgService->MessageURIToMsgHdr(spec, aMsgHdr);
+}
+
+NS_IMETHODIMP nsNntpUrl::IsUrlType(uint32_t type, bool* isType) {
+ NS_ENSURE_ARG(isType);
+
+ switch (type) {
+ case nsIMsgMailNewsUrl::eDisplay:
+ *isType = (m_newsAction == nsINntpUrl::ActionFetchArticle);
+ break;
+ default:
+ *isType = false;
+ };
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNntpUrl::GetOriginalSpec(nsACString& aSpec) {
+ aSpec = mOriginalSpec;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNntpUrl::SetOriginalSpec(const nsACString& aSpec) {
+ mOriginalSpec = aSpec;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsNntpUrl::GetServer(nsIMsgIncomingServer** aServer) {
+ NS_ENSURE_ARG_POINTER(aServer);
+
+ nsresult rv;
+ nsAutoCString scheme, user, host;
+
+ GetScheme(scheme);
+ GetUsername(user);
+ GetHost(host);
+
+ // No authority -> no server
+ if (host.IsEmpty()) {
+ *aServer = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgAccountManager> accountManager =
+ do_GetService("@mozilla.org/messenger/account-manager;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ *aServer = nullptr;
+ accountManager->FindServer(user, host, "nntp"_ns, 0, aServer);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::GetFolder(nsIMsgFolder** msgFolder) {
+ NS_ENSURE_ARG_POINTER(msgFolder);
+
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgIncomingServer> server;
+ rv = GetServer(getter_AddRefs(server));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Need a server and a group to get the folder
+ if (!server || m_group.IsEmpty()) {
+ *msgFolder = nullptr;
+ return NS_OK;
+ }
+
+ // Find the group on the server
+ nsCOMPtr<nsINntpIncomingServer> nntpServer = do_QueryInterface(server, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ bool hasGroup = false;
+ rv = nntpServer->ContainsNewsgroup(m_group, &hasGroup);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (!hasGroup) {
+ *msgFolder = nullptr;
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIMsgNewsFolder> newsFolder;
+ rv = nntpServer->FindGroup(m_group, getter_AddRefs(newsFolder));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return newsFolder->QueryInterface(NS_GET_IID(nsIMsgFolder),
+ (void**)msgFolder);
+}
+
+NS_IMETHODIMP nsNntpUrl::GetAutodetectCharset(bool* aAutodetectCharset) {
+ *aAutodetectCharset = mAutodetectCharset;
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNntpUrl::SetAutodetectCharset(bool aAutodetectCharset) {
+ mAutodetectCharset = aAutodetectCharset;
+ return NS_OK;
+}
+
+nsresult nsNntpUrl::Clone(nsIURI** _retval) {
+ nsresult rv;
+ rv = nsMsgMailNewsUrl::Clone(_retval);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsIMsgMessageUrl> newsurl = do_QueryInterface(*_retval, &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return newsurl->SetUri(mURI);
+}
+
+nsresult nsNntpUrl::NewURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval) {
+ nsresult rv;
+
+ nsCOMPtr<nsIMsgMailNewsUrl> nntpUri =
+ do_CreateInstance("@mozilla.org/messenger/nntpurl;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (aBaseURI) {
+ nsAutoCString newSpec;
+ aBaseURI->Resolve(aSpec, newSpec);
+ rv = nntpUri->SetSpecInternal(newSpec);
+ // XXX Consider: rv = NS_MutateURI(new
+ // nsNntpUrl::Mutator()).SetSpec(newSpec).Finalize(nntpUri);
+ } else {
+ rv = nntpUri->SetSpecInternal(aSpec);
+ }
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nntpUri.forget(_retval);
+ return NS_OK;
+}
diff --git a/comm/mailnews/news/src/nsNntpUrl.h b/comm/mailnews/news/src/nsNntpUrl.h
new file mode 100644
index 0000000000..eab52cb1c5
--- /dev/null
+++ b/comm/mailnews/news/src/nsNntpUrl.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsNntpUrl_h__
+#define nsNntpUrl_h__
+
+#include "nsINntpUrl.h"
+#include "nsMsgMailNewsUrl.h"
+#include "nsINNTPNewsgroupPost.h"
+#include "nsIFile.h"
+
+class nsNntpUrl : public nsINntpUrl,
+ public nsMsgMailNewsUrl,
+ public nsIMsgMessageUrl,
+ public nsIMsgI18NUrl {
+ public:
+ NS_DECL_NSINNTPURL
+ NS_DECL_NSIMSGMESSAGEURL
+ NS_DECL_NSIMSGI18NURL
+
+ // nsMsgMailNewsUrl overrides
+ nsresult SetSpecInternal(const nsACString& aSpec) override;
+ nsresult Clone(nsIURI** _retval) override;
+
+ NS_IMETHOD IsUrlType(uint32_t type, bool* isType) override;
+
+ // nsIMsgMailNewsUrl overrides
+ NS_IMETHOD GetServer(nsIMsgIncomingServer** server) override;
+ NS_IMETHOD GetFolder(nsIMsgFolder** msgFolder) override;
+
+ // nsNntpUrl
+ nsNntpUrl();
+ static nsresult NewURI(const nsACString& aSpec, nsIURI* aBaseURI,
+ nsIURI** _retval);
+
+ NS_DECL_ISUPPORTS_INHERITED
+
+ private:
+ virtual ~nsNntpUrl();
+ nsresult DetermineNewsAction();
+ nsresult ParseNewsURL();
+ nsresult ParseNntpURL();
+
+ nsCOMPtr<nsINNTPNewsgroupPost> m_newsgroupPost;
+ nsNewsAction m_newsAction; // the action this url represents...parse mailbox,
+ // display messages, etc.
+
+ nsCString mURI; // the RDF URI associated with this url.
+ bool mAutodetectCharset; // used by nsIMsgI18NUrl...
+
+ nsCString mOriginalSpec;
+ nsCOMPtr<nsIFile> m_filePath;
+
+ // used by save message to disk
+ nsCOMPtr<nsIFile> m_messageFile;
+
+ bool m_addDummyEnvelope;
+ bool m_canonicalLineEnding;
+ bool m_getOldMessages;
+
+ nsCString m_group;
+ nsCString m_messageID;
+ nsMsgKey m_key;
+};
+
+#endif // nsNntpUrl_h__
diff --git a/comm/mailnews/news/test/moz.build b/comm/mailnews/news/test/moz.build
new file mode 100644
index 0000000000..6b37fdbe09
--- /dev/null
+++ b/comm/mailnews/news/test/moz.build
@@ -0,0 +1,6 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.ini"]
diff --git a/comm/mailnews/news/test/unit/head_server_setup.js b/comm/mailnews/news/test/unit/head_server_setup.js
new file mode 100644
index 0000000000..6a5bcbda9f
--- /dev/null
+++ b/comm/mailnews/news/test/unit/head_server_setup.js
@@ -0,0 +1,244 @@
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+var { localAccountUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/LocalAccountUtils.jsm"
+);
+
+var test = null;
+
+// WebApps.jsm called by ProxyAutoConfig (PAC) requires a valid nsIXULAppInfo.
+var { getAppInfo, newAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+// Ensure the profile directory is set up
+do_get_profile();
+
+var gDEPTH = "../../../../";
+
+// Import the servers
+var { fsDebugAll, gThreadManager, nsMailServer } = ChromeUtils.import(
+ "resource://testing-common/mailnews/Maild.jsm"
+);
+var {
+ NewsArticle,
+ NNTP_Giganews_handler,
+ NNTP_RFC2980_handler,
+ NNTP_RFC4643_extension,
+ NNTP_RFC977_handler,
+ NntpDaemon,
+} = ChromeUtils.import("resource://testing-common/mailnews/Nntpd.jsm");
+
+var kSimpleNewsArticle =
+ "From: John Doe <john.doe@example.com>\n" +
+ "Date: Sat, 24 Mar 1990 10:59:24 -0500\n" +
+ "Newsgroups: test.subscribe.simple\n" +
+ "Subject: H2G2 -- What does it mean?\n" +
+ "Message-ID: <TSS1@nntp.invalid>\n" +
+ "\n" +
+ "What does the acronym H2G2 stand for? I've seen it before...\n";
+
+// The groups to set up on the fake server.
+// It is an array of tuples, where the first element is the group name and the
+// second element is whether or not we should subscribe to it.
+var groups = [
+ ["misc.test", false],
+ ["test.empty", false],
+ ["test.subscribe.empty", true],
+ ["test.subscribe.simple", true],
+ ["test.filter", true],
+];
+// Sets up the NNTP daemon object for use in fake server
+function setupNNTPDaemon() {
+ var daemon = new NntpDaemon();
+
+ groups.forEach(function (element) {
+ daemon.addGroup(element[0]);
+ });
+
+ var auto_add = do_get_file("postings/auto-add/");
+ var files = [...auto_add.directoryEntries];
+
+ files.sort(function (a, b) {
+ if (a.leafName == b.leafName) {
+ return 0;
+ }
+ return a.leafName < b.leafName ? -1 : 1;
+ });
+
+ files.forEach(function (file) {
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ sstream.init(fstream);
+
+ var post = "";
+ let part = sstream.read(4096);
+ while (part.length > 0) {
+ post += part;
+ part = sstream.read(4096);
+ }
+ sstream.close();
+ fstream.close();
+ daemon.addArticle(new NewsArticle(post));
+ });
+
+ var article = new NewsArticle(kSimpleNewsArticle);
+ daemon.addArticleToGroup(article, "test.subscribe.simple", 1);
+
+ return daemon;
+}
+
+function makeServer(handler, daemon) {
+ function createHandler(d) {
+ return new handler(d);
+ }
+ return new nsMailServer(createHandler, daemon);
+}
+
+// Enable strict threading
+Services.prefs.setBoolPref("mail.strict_threading", true);
+
+// Make sure we don't try to use a protected port. I like adding 1024 to the
+// default port when doing so...
+var NNTP_PORT = 1024 + 119;
+
+var _server = null;
+var _account = null;
+
+function subscribeServer(incomingServer) {
+ // Subscribe to newsgroups
+ incomingServer.QueryInterface(Ci.nsINntpIncomingServer);
+ groups.forEach(function (element) {
+ if (element[1]) {
+ incomingServer.subscribeToNewsgroup(element[0]);
+ }
+ });
+ // Only allow one connection
+ incomingServer.maximumConnectionsNumber = 1;
+}
+
+// Sets up the client-side portion of fakeserver
+function setupLocalServer(port, host = "localhost") {
+ if (_server != null) {
+ return _server;
+ }
+ let serverAndAccount = localAccountUtils.create_incoming_server_and_account(
+ "nntp",
+ port,
+ null,
+ null,
+ host
+ );
+ let server = serverAndAccount.server;
+ subscribeServer(server);
+
+ _server = server;
+ _account = serverAndAccount.account;
+
+ return server;
+}
+
+// Sets up a protocol object and prepares to run the test for the news url
+function setupProtocolTest(port, newsUrl, incomingServer) {
+ var url;
+ if (newsUrl instanceof Ci.nsIMsgMailNewsUrl) {
+ url = newsUrl;
+ } else {
+ url = Services.io.newURI(newsUrl);
+ }
+
+ var newsServer = incomingServer;
+ if (!newsServer) {
+ newsServer = setupLocalServer(port);
+ }
+
+ var listener = {
+ onStartRequest() {},
+ onStopRequest() {
+ if (!this.called) {
+ this.called = true;
+ newsServer.closeCachedConnections();
+ this.called = false;
+ }
+ },
+ onDataAvailable() {},
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ };
+ listener.called = false;
+ newsServer.loadNewsUrl(url, null, listener);
+}
+
+function create_post(baseURL, file) {
+ var url = Services.io.newURI(baseURL);
+ url.QueryInterface(Ci.nsINntpUrl);
+
+ var post = Cc["@mozilla.org/messenger/nntpnewsgrouppost;1"].createInstance(
+ Ci.nsINNTPNewsgroupPost
+ );
+ post.postMessageFile = do_get_file(file);
+ url.messageToPost = post;
+ return url;
+}
+
+function resetFolder(folder) {
+ var headers = [...folder.messages];
+
+ var db = folder.msgDatabase;
+ db.dBFolderInfo.knownArtsSet = "";
+ for (var header of headers) {
+ db.deleteHeader(header, null, true, false);
+ }
+ dump("resetting folder\n");
+ folder.msgDatabase = null;
+}
+
+function do_check_transaction(real, expected) {
+ if (Array.isArray(real)) {
+ real = real.at(-1);
+ }
+
+ // real.them may have an extra QUIT on the end, where the stream is only
+ // closed after we have a chance to process it and not them. We therefore
+ // excise this from the list
+ if (real.them[real.them.length - 1] == "QUIT") {
+ real.them.pop();
+ }
+
+ Assert.equal(real.them.join(","), expected.join(","));
+ dump("Passed test " + test + "\n");
+}
+
+function make_article(file) {
+ var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ fstream.init(file, -1, 0, 0);
+ sstream.init(fstream);
+
+ var post = "";
+ let part = sstream.read(4096);
+ while (part.length > 0) {
+ post += part;
+ part = sstream.read(4096);
+ }
+ sstream.close();
+ fstream.close();
+ return new NewsArticle(post);
+}
+
+registerCleanupFunction(function () {
+ load("../../../resources/mailShutdown.js");
+});
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post1.eml b/comm/mailnews/news/test/unit/postings/auto-add/post1.eml
new file mode 100644
index 0000000000..9e1f3e7265
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post1.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: First post!
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <1@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:1
+
+This is the first body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post2.eml b/comm/mailnews/news/test/unit/postings/auto-add/post2.eml
new file mode 100644
index 0000000000..d5a4591bb3
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post2.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Odd Subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <2@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:2
+
+This is the second body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post3.eml b/comm/mailnews/news/test/unit/postings/auto-add/post3.eml
new file mode 100644
index 0000000000..fdfc2d69c1
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post3.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Odd Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <3@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:3
+
+This is the third body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post4.eml b/comm/mailnews/news/test/unit/postings/auto-add/post4.eml
new file mode 100644
index 0000000000..11a53b7035
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post4.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Sat, 1 Jan 2000 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <4@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:4
+
+This is the fourth body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post5.eml b/comm/mailnews/news/test/unit/postings/auto-add/post5.eml
new file mode 100644
index 0000000000..55ba89c022
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post5.eml
@@ -0,0 +1,50 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <5@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:5
+Bytes: 2057
+
+This is the fifth body post, with extra special padding to make sure
+that no one else has the same length as it. You see, we have to ensure
+that we have at least two KB of data because search is stupid and
+searches in terms of KB. If we didn't, we'd be stuck with a lot of
+messages merely matching 1, so we use this to pad the size.
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
+veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+occaecat cupidatat non proident, sunt in culpa qui officia deserunt
+mollit anim id est laborum.
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
+veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+occaecat cupidatat non proident, sunt in culpa qui officia deserunt
+mollit anim id est laborum.
+
+Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
+veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
+commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
+velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
+occaecat cupidatat non proident, sunt in culpa qui officia deserunt
+mollit anim id est laborum.
+
+.
+
+But that wasn't all :-)
+
+--
+A signature for the heck of it.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post6.eml b/comm/mailnews/news/test/unit/postings/auto-add/post6.eml
new file mode 100644
index 0000000000..fe9d536203
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post6.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Program/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <6.odd@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:6
+
+This is the sixth body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post7.eml b/comm/mailnews/news/test/unit/postings/auto-add/post7.eml
new file mode 100644
index 0000000000..f12b708a9d
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post7.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Odd/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <7@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:7
+
+This is the seventh body post.
diff --git a/comm/mailnews/news/test/unit/postings/auto-add/post8.eml b/comm/mailnews/news/test/unit/postings/auto-add/post8.eml
new file mode 100644
index 0000000000..265b1f5a4a
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/auto-add/post8.eml
@@ -0,0 +1,14 @@
+Path: border1.nntp.dca.giganews.com!nntp.giganews.com!local02.nntp.dca.giganews.com!nntp.mozilla.org!news.mozilla.org.POSTED!not-for-mail
+Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+User-Agent: Odd/1.0
+MIME-Version: 1.0
+Newsgroups: test.filter
+Subject: Regular subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <8.unread@regular.invalid>
+NNTP-Posting-Host: 127.0.0.1
+Xref: test.filter:8
+
+This is the eight body post.
diff --git a/comm/mailnews/news/test/unit/postings/bug403242.eml b/comm/mailnews/news/test/unit/postings/bug403242.eml
new file mode 100644
index 0000000000..741ccf8624
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/bug403242.eml
@@ -0,0 +1,25 @@
+Message-ID: <89550684-f0df-4e9a-aea6-07645e238dd2>
+Subject: Attachment test
+From: =?ISO-8859-1?Q?Marcin_Miko=3Fajczak?= <marcin.mikolajczak@hotmail.invalid>
+Date: 2007-11-09 18:37:09
+Newsgroups: test1
+Content-Type: multipart/mixed; boundary="------------060805070701070202060104"
+MIME-Version: 1.0
+User-Agent: Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.8.1.6) Gecko/20070728 Thunderbird/2.0.0.6 Mnenhy/0.7.5.666
+
+This is a multi-part message in MIME format.
+--------------060805070701070202060104
+Content-Type: text/plain; charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+
+This message has an attachment.
+
+--------------060805070701070202060104
+Content-Type: text/plain;
+ name="Test.txt"
+Content-Transfer-Encoding: base64
+Content-Disposition: inline;
+ filename="Test.txt"
+
+VGhpcyBpcyBhIHRlc3QgYXR0YWNobWVudC4=
+--------------060805070701070202060104--
diff --git a/comm/mailnews/news/test/unit/postings/bug670935.eml b/comm/mailnews/news/test/unit/postings/bug670935.eml
new file mode 100644
index 0000000000..7762586a2f
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/bug670935.eml
@@ -0,0 +1,7 @@
+From: Robert Kagan <Robert.Kagan@test.invalid>
+Date: Fri, 24 Mar 1989 00:04:18 -0900
+Newsgroups: test.malformed&name
+Subject: I think we have a problem
+
+This is an automated report. There may be some structural damage in
+sectors 10 and 9. Please respond to confirm.
diff --git a/comm/mailnews/news/test/unit/postings/post1.eml b/comm/mailnews/news/test/unit/postings/post1.eml
new file mode 100644
index 0000000000..b767aca453
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/post1.eml
@@ -0,0 +1,7 @@
+From: Robert Kagan <Robert.Kagan@test.invalid>
+Date: Fri, 24 Mar 1989 00:04:18 -0900
+Newsgroups: test.news.problems test.news.fictional
+Subject: I think we have a problem
+
+This is an automated report. There may be some structural damage in
+sectors 10 and 9. Please respond to confirm.
diff --git a/comm/mailnews/news/test/unit/postings/post2.eml b/comm/mailnews/news/test/unit/postings/post2.eml
new file mode 100644
index 0000000000..c64818c6df
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/post2.eml
@@ -0,0 +1,6 @@
+From: "Demo User" <nobody@example.net>
+Newsgroups: misc.test
+Subject: I am just a test article
+Organization: An Example Net
+
+This is just a test article.
diff --git a/comm/mailnews/news/test/unit/postings/post3.eml b/comm/mailnews/news/test/unit/postings/post3.eml
new file mode 100644
index 0000000000..7769fbf3d7
--- /dev/null
+++ b/comm/mailnews/news/test/unit/postings/post3.eml
@@ -0,0 +1,10 @@
+From: "Demo User" <nobody@example.net>
+Message-ID: <2@dot.invalid>
+Newsgroups: dot.test
+Subject: Bug 170727: Test article with dots
+
+. 1 dot
+.. 2 dots
+ .indented dot
+.
+no - that wasn't the end of the message!
diff --git a/comm/mailnews/news/test/unit/test_NntpChannel.js b/comm/mailnews/news/test/unit/test_NntpChannel.js
new file mode 100644
index 0000000000..0da7963c78
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_NntpChannel.js
@@ -0,0 +1,74 @@
+/* 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 { NntpChannel } = ChromeUtils.import("resource:///modules/NntpChannel.jsm");
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+let server;
+
+add_setup(function setup() {
+ let daemon = setupNNTPDaemon();
+ server = new nsMailServer(() => {
+ let handler = new NNTP_RFC977_handler(daemon);
+ // Test NntpClient works with 201 response.
+ handler.onStartup = () => {
+ return "201 posting prohibited";
+ };
+ return handler;
+ }, daemon);
+ server.start(NNTP_PORT);
+ registerCleanupFunction(() => {
+ server.stop();
+ });
+
+ setupLocalServer(NNTP_PORT);
+});
+
+/**
+ * Test a ?list-ids news url will trigger LISTGROUP request.
+ */
+add_task(async function test_listIds() {
+ // Init the uri and streamListener.
+ let uri = Services.io.newURI(
+ `news://localhost:${NNTP_PORT}/test.filter?list-ids`
+ );
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+
+ // Run the uri with NntpChannel.
+ let channel = new NntpChannel(uri);
+ channel.asyncOpen(streamListener);
+ await streamListener.promise;
+
+ // Test LISTGROUP request was sent correctly.
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, ["MODE READER", "LISTGROUP test.filter"]);
+});
+
+/**
+ * Test a ?group=name&key=x news url will trigger ARTICLE request.
+ */
+add_task(async function test_fetchArticle() {
+ _server.closeCachedConnections();
+
+ // Init the uri and streamListener.
+ let uri = Services.io.newURI(
+ `news://localhost:${NNTP_PORT}?group=test.filter&key=1`
+ );
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+
+ // Run the uri with NntpChannel.
+ let channel = new NntpChannel(uri);
+ channel.asyncOpen(streamListener);
+ await streamListener.promise;
+
+ // Test ARTICLE request was sent correctly.
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.filter",
+ "ARTICLE 1",
+ ]);
+});
diff --git a/comm/mailnews/news/test/unit/test_biff.js b/comm/mailnews/news/test/unit/test_biff.js
new file mode 100644
index 0000000000..ba6266ef25
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_biff.js
@@ -0,0 +1,57 @@
+// This tests that we can execute biff properly, specifically that filters are
+// run during biff, producing correct counts.
+
+/* import-globals-from ../../../test/resources/filterTestUtils.js */
+load("../../../resources/filterTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function run_test() {
+ // Set up the server and add in filters
+ let daemon = setupNNTPDaemon();
+ let server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ let localserver = setupLocalServer(server.port);
+ // Remove all but the test.filter folder
+ let rootFolder = localserver.rootFolder;
+ for (let folder of rootFolder.subFolders) {
+ if (folder.name != "test.filter") {
+ rootFolder.propagateDelete(folder, true);
+ }
+ }
+
+ // Create a filter to mark one message read.
+ let filters = localserver.getFilterList(null);
+ filters.loggingEnabled = true;
+ createFilter(filters, "subject", "Odd", "read");
+ localserver.setFilterList(filters);
+
+ // This is a bit hackish, but we don't have any really functional callbacks
+ // for biff. Instead, we use the notifier to look for all 7 messages to be
+ // added and take that as our sign that the download is finished.
+ let expectCount = 7,
+ seen = 0;
+ let listener = {
+ msgAdded() {
+ if (++seen == expectCount) {
+ localserver.closeCachedConnections();
+ }
+ },
+ };
+ MailServices.mfn.addListener(
+ listener,
+ Ci.nsIMsgFolderNotificationService.msgAdded
+ );
+ localserver.performBiff(null);
+ server.performTest();
+ MailServices.mfn.removeListener(listener);
+
+ // We marked, via our filters, one of the messages read. So if we do not
+ // have 1 read message, either we're not running the filters on biff, or the
+ // filters aren't working. This is disambiguated by the test_filter.js test.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ Assert.equal(folder.getTotalMessages(false), folder.getNumUnread(false) + 1);
+ server.stop();
+}
diff --git a/comm/mailnews/news/test/unit/test_bug170727.js b/comm/mailnews/news/test/unit/test_bug170727.js
new file mode 100644
index 0000000000..50bda7b096
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug170727.js
@@ -0,0 +1,61 @@
+// Bug 170727 - Remove the escaped dot from body lines before saving in the offline store.
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_task(async function testloadMessage() {
+ let daemon = setupNNTPDaemon();
+ daemon.addGroup("dot.test");
+ daemon.addArticle(make_article(do_get_file("postings/post3.eml")));
+
+ let server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ let localserver = setupLocalServer(server.port);
+ localserver.subscribeToNewsgroup("dot.test");
+
+ let folder = localserver.rootFolder.getChildNamed("dot.test");
+ folder.setFlag(Ci.nsMsgFolderFlags.Offline);
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ let uri = folder.generateMessageURI(1);
+ let msgService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=news"
+ ].getService(Ci.nsIMsgMessageService);
+
+ // Pretend to display the message: During the first run, the article is downloaded,
+ // displayed directly and simultaneously saved in the offline storage.
+ {
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ msgService.loadMessage(uri, listener, null, null, false);
+ let msgText = await listener.promise;
+ localserver.closeCachedConnections();
+
+ // Correct text? (original file uses LF only, so strip CR)
+ Assert.equal(
+ msgText.replaceAll("\r", ""),
+ daemon.getArticle("<2@dot.invalid>").fullText
+ );
+ }
+
+ // In the second run, the offline store serves as the source of the article.
+ {
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ msgService.loadMessage(uri, listener, null, null, false);
+ let msgText = await listener.promise;
+ localserver.closeCachedConnections();
+
+ // Correct text? (original file uses LF only, so strip CR)
+ Assert.equal(
+ msgText.replaceAll("\r", ""),
+ daemon.getArticle("<2@dot.invalid>").fullText
+ );
+ }
+
+ server.stop();
+});
diff --git a/comm/mailnews/news/test/unit/test_bug37465.js b/comm/mailnews/news/test/unit/test_bug37465.js
new file mode 100644
index 0000000000..e3d61c9dbe
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug37465.js
@@ -0,0 +1,49 @@
+// Bug 37465 -- assertions with no accounts
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+add_task(async function textChannelAsync() {
+ let daemon = setupNNTPDaemon();
+ let server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+
+ // Correct URI?
+ let uri = Services.io.newURI(
+ "news://localhost:" + server.port + "/1@regular.invalid"
+ );
+ let newsUri = uri
+ .QueryInterface(Ci.nsINntpUrl)
+ .QueryInterface(Ci.nsIMsgMailNewsUrl);
+ Assert.equal(uri.port, server.port);
+ Assert.equal(newsUri.server, null);
+ Assert.equal(newsUri.messageID, "1@regular.invalid");
+ Assert.equal(newsUri.folder, null);
+
+ // Run the URI and make sure we get the message
+ let channel = Services.io.newChannelFromURI(
+ uri,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ channel.asyncOpen(listener, null);
+ let msgText = await listener.promise;
+ // Correct text? (original file uses LF only, so strip CR)
+ Assert.equal(
+ msgText.replaceAll("\r", ""),
+ daemon.getArticle("<1@regular.invalid>").fullText
+ );
+
+ // Shut down connections
+ MailServices.accounts.closeCachedConnections();
+ server.stop();
+});
diff --git a/comm/mailnews/news/test/unit/test_bug403242.js b/comm/mailnews/news/test/unit/test_bug403242.js
new file mode 100644
index 0000000000..54a385da78
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug403242.js
@@ -0,0 +1,55 @@
+// Bug 403242 stems from invalid message ids
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+add_task(async function test403242() {
+ let daemon = setupNNTPDaemon();
+ daemon.addGroup("test1");
+ daemon.addArticle(make_article(do_get_file("postings/bug403242.eml")));
+ let server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ let localserver = setupLocalServer(server.port);
+ localserver.subscribeToNewsgroup("test1");
+
+ let folder = localserver.rootFolder.getChildNamed("test1");
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ // Fetch the message
+ let uri = folder.generateMessageURI(1);
+ let msgService = Cc[
+ "@mozilla.org/messenger/messageservice;1?type=news"
+ ].getService(Ci.nsIMsgMessageService);
+
+ // Does the URL lie to us?
+ let neckoUrl = msgService.getUrlForUri(uri).QueryInterface(Ci.nsINntpUrl);
+ Assert.equal(neckoUrl.newsAction, Ci.nsINntpUrl.ActionFetchArticle);
+
+ // Pretend to display the message
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ msgService.loadMessage(uri, listener, null, null, false);
+ let msgText = await listener.promise;
+ localserver.closeCachedConnections();
+ server.stop();
+
+ // Correct text? (original file uses LF only, so strip CR)
+ Assert.equal(
+ msgText.replaceAll("\r", ""),
+ daemon.getGroup("test1")[1].fullText
+ );
+
+ // No illegal commands?
+ test = "bug 403242";
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction[transaction.length - 1], [
+ "MODE READER",
+ "GROUP test1",
+ "ARTICLE 1",
+ ]);
+});
diff --git a/comm/mailnews/news/test/unit/test_bug540288.js b/comm/mailnews/news/test/unit/test_bug540288.js
new file mode 100644
index 0000000000..9ffe8a7616
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug540288.js
@@ -0,0 +1,114 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Tests that an empty cache entry doesn't return an empty message for news. */
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+var server;
+var localserver;
+
+var streamListener = {
+ _data: "",
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ // nsIRequestObserver
+ onStartRequest(aRequest) {},
+ onStopRequest(aRequest, aStatusCode) {
+ Assert.equal(aStatusCode, 0);
+
+ // Reduce any \r\n to just \n so we can do a good comparison on any
+ // platform.
+ var reduced = this._data.replace(/\r\n/g, "\n");
+ Assert.equal(reduced, kSimpleNewsArticle);
+
+ // We must finish closing connections and tidying up after a timeout
+ // so that the thread has time to unwrap itself.
+ do_timeout(0, doTestFinished);
+ },
+
+ // nsIStreamListener
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
+ let scriptStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+
+ scriptStream.init(aInputStream);
+
+ this._data += scriptStream.read(aCount);
+ },
+};
+
+function doTestFinished() {
+ localserver.closeCachedConnections();
+
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+}
+
+function run_test() {
+ server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+ var uri = Services.io.newURI(
+ "news://localhost:" + server.port + "/TSS1%40nntp.test"
+ );
+
+ try {
+ // Add an empty message to the cache
+ MailServices.nntp.cacheStorage.asyncOpenURI(
+ uri,
+ "",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ {
+ onCacheEntryAvailable(cacheEntry, isNew, status) {
+ Assert.equal(status, Cr.NS_OK);
+
+ cacheEntry.markValid();
+
+ // Get the folder and new mail
+ var folder = localserver.rootFolder.getChildNamed(
+ "test.subscribe.simple"
+ );
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ Assert.equal(folder.getTotalMessages(false), 1);
+ Assert.ok(folder.hasNewMessages);
+
+ server.resetTest();
+
+ var message = folder.firstNewMessage;
+
+ var messageUri = folder.getUriForMsg(message);
+
+ Cc["@mozilla.org/messenger/messageservice;1?type=news"]
+ .getService(Ci.nsIMsgMessageService)
+ .loadMessage(messageUri, streamListener, null, null, false);
+
+ // Get the server to run
+ server.performTest();
+ },
+ }
+ );
+
+ do_test_pending();
+ } catch (e) {
+ server.stop();
+ do_throw(e);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_bug695309.js b/comm/mailnews/news/test/unit/test_bug695309.js
new file mode 100644
index 0000000000..d51587ba08
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_bug695309.js
@@ -0,0 +1,121 @@
+/* Tests the connection mayhem found by bug 695309 */
+
+// The full bug requires several things to fall into place:
+// 1. Cause the connections to timeout, while keeping them in the cache.
+// 2. Enqueue enough requests to cause things to be placed in the pending queue.
+// 3. Commands try to run but die instead.
+// 4. Enqueue more requests to open up new connections.
+// 5. When loading, the connection ends up pulling somebody from the queue and
+// ends up treating the response for the prior command as the current
+// response.
+// 6. This causes, in particular, GROUP to read the logon string as the response
+// (where sprintf clears everything to 0), and AUTHINFO to think credentials
+// are wrong. The bug's description is then caused by the next read seeing
+// a large number of (not really) new messages.
+// For the purposes of this test, we read enough to see if the group command is
+// being misread or not, as it is complicated enough.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+const { NetworkTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/NetworkTestUtils.jsm"
+);
+
+var daemon, localserver, server;
+var highWater = 0;
+
+add_setup(async function () {
+ daemon = setupNNTPDaemon();
+ server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+
+ // Bug 1050840:
+ // Check if invalid value of the max_cached_connections pref
+ // is properly folded into a sane value.
+ localserver.maximumConnectionsNumber = -5;
+ Assert.equal(localserver.maximumConnectionsNumber, 1);
+
+ localserver.maximumConnectionsNumber = 0;
+ Assert.equal(localserver.maximumConnectionsNumber, 2);
+
+ localserver.maximumConnectionsNumber = 2;
+});
+
+add_task(async function test_newMsgs() {
+ // Start by initializing the folder, and mark some messages as read.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ Assert.equal(folder.getTotalMessages(false), 0);
+ let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.getNewMessages(null, asyncUrlListener);
+ await asyncUrlListener.promise;
+ // Do another folder to use up both connections
+ localserver.rootFolder
+ .getChildNamed("test.subscribe.simple")
+ .getNewMessages(null, asyncUrlListener);
+ await asyncUrlListener.promise;
+ folder.QueryInterface(Ci.nsIMsgNewsFolder).setReadSetFromStr("1-3");
+ Assert.equal(folder.getTotalMessages(false) - folder.getNumUnread(false), 3);
+ highWater = folder.getTotalMessages(false);
+ Assert.equal(folder.msgDatabase.dBFolderInfo.highWater, highWater);
+});
+
+add_task(async function trigger_bug() {
+ // Kill the connection and start it up again.
+ server.stop();
+ server.start();
+
+ // Get new messages for all folders. Once we've seen one folder, trigger a
+ // load of the folder in question. This second load should, if the bug is
+ // present, be overwritten with one from the load queue that causes the
+ // confusion. It then loads it again, and should (before the patch that fixes
+ // this) read the 200 logon instead of the 211 group.
+ let testFolder = localserver.rootFolder.getChildNamed("test.filter");
+ let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ let promiseFolderEvent = function (folder, event) {
+ return new Promise((resolve, reject) => {
+ let folderListener = {
+ QueryInterface: ChromeUtils.generateQI(["nsIFolderListener"]),
+ onFolderEvent(aEventFolder, aEvent) {
+ if (
+ aEvent == "FolderLoaded" &&
+ aEventFolder.prettyName == "test.subscribe.simple"
+ ) {
+ aEventFolder.getNewMessages(null, asyncUrlListener);
+ return;
+ }
+
+ if (folder === aEventFolder && event == aEvent) {
+ MailServices.mailSession.RemoveFolderListener(folderListener);
+ resolve();
+ }
+ },
+ };
+ MailServices.mailSession.AddFolderListener(
+ folderListener,
+ Ci.nsIFolderListener.event
+ );
+ });
+ };
+ let folderLoadedPromise = promiseFolderEvent(testFolder, "FolderLoaded");
+
+ localserver.performExpand(null);
+
+ // Wait for test.subscribe.simple to load. That will trigger getNewMessages.
+ await folderLoadedPromise;
+ // Wait for the new messages to be loaded.
+ await asyncUrlListener.promise;
+
+ Assert.equal(testFolder.msgDatabase.dBFolderInfo.highWater, highWater);
+});
+
+add_task(async function cleanUp() {
+ NetworkTestUtils.shutdownServers();
+ localserver.closeCachedConnections();
+});
diff --git a/comm/mailnews/news/test/unit/test_cancelPasswordDialog.js b/comm/mailnews/news/test/unit/test_cancelPasswordDialog.js
new file mode 100644
index 0000000000..5212614b3a
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_cancelPasswordDialog.js
@@ -0,0 +1,59 @@
+/* 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 { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+let daemon = setupNNTPDaemon();
+let server = makeServer(NNTP_RFC4643_extension, daemon);
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+/**
+ * Test connection should be closed after canceling the password dialog.
+ */
+add_task(async function cancelPasswordDialog() {
+ // Mock the password prompt.
+ registerAlertTestUtils();
+
+ // Enforce server auth and trigger a list group request.
+ let incomingServer = setupLocalServer(server.port);
+ incomingServer.pushAuth = true;
+ let listener = new PromiseTestUtils.PromiseStreamListener();
+ incomingServer.loadNewsUrl(
+ Services.io.newURI(`news://localhost:${server.port}/*`),
+ null,
+ listener
+ );
+
+ // The request should be aborted.
+ try {
+ await listener.promise;
+ } catch (e) {
+ equal(e, Cr.NS_ERROR_ABORT);
+ }
+
+ // Should send nothing after canceling the password dialog.
+ let transaction = server.playTransaction();
+ do_check_transaction(transaction, ["MODE READER"]);
+});
+
+function promptUsernameAndPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ // Cancel the password dialog.
+ return false;
+}
diff --git a/comm/mailnews/news/test/unit/test_filter.js b/comm/mailnews/news/test/unit/test_filter.js
new file mode 100644
index 0000000000..9e450499e7
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_filter.js
@@ -0,0 +1,179 @@
+// Tests for the filtering code of NNTP. The same tests are run for each of the
+// different NNTP setups, to test code in a variety of cases.
+//
+// Different suites:
+// * Perfect 3977 compliance (not tested)
+// * Perfect 2980 compliance (XOVER and XHDR work)
+// * Giganews compliance (XHDR doesn't work for practical purposes)
+// * Only 977 compliance (no XOVER support)
+// Basic operations:
+// * Test that the following headers trigger:
+// - Subject
+// - From
+// - Date
+// - Size
+// - Message-ID (header retrievable by XOVER)
+// - User-Agent (header not retrievable by XHDR)
+// * Test all actions
+
+/* import-globals-from ../../../test/resources/filterTestUtils.js */
+load("../../../resources/filterTestUtils.js");
+
+// These are the expected results for testing filter triggers
+var attribResults = {
+ "1@regular.invalid": ["isRead", false],
+ "2@regular.invalid": ["isRead", true],
+ "3@regular.invalid": ["isRead", true],
+ "4@regular.invalid": ["isRead", true],
+ "5@regular.invalid": ["isRead", true],
+ "6.odd@regular.invalid": ["isRead", true],
+ "7@regular.invalid": ["isRead", true],
+ "8.unread@regular.invalid": ["isRead", true],
+};
+function testAttrib(handler, daemon, localserver) {
+ var server = makeServer(handler, daemon);
+ server.setDebugLevel(fsDebugAll);
+ server.start();
+ localserver.port = server.port;
+
+ // Get the folder and force filters to run
+ var folder = localserver.rootFolder.getChildNamed("test.filter");
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ var headers = [...folder.messages];
+
+ try {
+ Assert.equal(headers.length, 8);
+ for (var header of headers) {
+ var id = header.messageId;
+ dump("Testing message " + id + "\n");
+ Assert.equal(header[attribResults[id][0]], attribResults[id][1]);
+ }
+ } catch (e) {
+ print(server.playTransaction().them);
+ throw e;
+ } finally {
+ server.stop();
+ }
+
+ resetFolder(folder);
+}
+
+// These are the results for testing actual actions
+var actionResults = {
+ "1@regular.invalid": ["priority", 6],
+ // "2@regular.invalid" should not be in database
+ "3@regular.invalid": function (header, folder) {
+ var flags = folder.msgDatabase.getThreadContainingMsgHdr(header).flags;
+ var ignored = Ci.nsMsgMessageFlags.Ignored;
+ // This is checking the thread's kill flag
+ return (flags & ignored) == ignored;
+ },
+ "4@regular.invalid": function (header, folder) {
+ var flags = folder.msgDatabase.getThreadContainingMsgHdr(header).flags;
+ var watched = Ci.nsMsgMessageFlags.Watched;
+ // This is checking the thread's watch flag
+ return (flags & watched) == watched;
+ },
+ "5@regular.invalid": ["isFlagged", true],
+ "6.odd@regular.invalid": ["isRead", false],
+ "7@regular.invalid": function (header, folder) {
+ return header.getStringProperty("keywords") == "tag";
+ },
+ "8.unread@regular.invalid": ["isRead", false],
+};
+function testAction(handler, daemon, localserver) {
+ var server = makeServer(handler, daemon);
+ server.start();
+ localserver.port = server.port;
+
+ // Get the folder and force filters to run
+ var folder = localserver.rootFolder.getChildNamed("test.filter");
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ let headers = [...folder.messages];
+
+ try {
+ Assert.equal(headers.length, 7);
+ for (var header of headers) {
+ var id = header.messageId;
+ dump("Testing message " + id + "\n");
+ if (actionResults[id] instanceof Array) {
+ Assert.equal(header[actionResults[id][0]], actionResults[id][1]);
+ } else {
+ Assert.ok(actionResults[id](header, folder));
+ }
+ }
+ } catch (e) {
+ print(server.playTransaction().them);
+ throw e;
+ } finally {
+ server.stop();
+ }
+
+ resetFolder(folder);
+}
+
+// These are the various server handlers
+var handlers = [
+ NNTP_RFC977_handler,
+ NNTP_Giganews_handler,
+ NNTP_RFC2980_handler,
+];
+function run_test() {
+ // Set up the server and add in filters
+ var daemon = setupNNTPDaemon();
+ var localserver = setupLocalServer(NNTP_PORT);
+ var serverFilters = localserver.getFilterList(null);
+
+ createFilter(serverFilters, "subject", "Odd", "read");
+ createFilter(serverFilters, "from", "Odd Person", "read");
+ // A PRTime is the time in μs, but a JS date is time in ms.
+ createFilter(serverFilters, "date", new Date(2000, 0, 1) * 1000, "read");
+ createFilter(serverFilters, "size", 2, "read");
+ createFilter(serverFilters, "message-id", "odd", "read");
+ createFilter(serverFilters, "user-agent", "Odd/1.0", "read");
+ createFilter(serverFilters, "message-id", "8.unread", "read");
+ localserver.setFilterList(serverFilters);
+
+ handlers.forEach(function (handler) {
+ testAttrib(handler, daemon, localserver);
+ });
+
+ // Now we test folder-filters... and actions
+ // Clear out the server filters
+ while (serverFilters.filterCount > 0) {
+ serverFilters.removeFilterAt(0);
+ }
+ localserver.setFilterList(serverFilters);
+
+ var folder = localserver.rootFolder.getChildNamed("test.filter");
+ var folderFilters = folder.getFilterList(null);
+ createFilter(folderFilters, "subject", "First", "priority");
+ createFilter(folderFilters, "subject", "Odd", "delete");
+ createFilter(folderFilters, "from", "Odd Person", "kill");
+ createFilter(folderFilters, "date", new Date(2000, 0, 1) * 1000, "watch");
+ createFilter(folderFilters, "size", 2, "flag");
+ createFilter(folderFilters, "message-id", "odd", "stop");
+ // This shouldn't be hit, because of the previous filter
+ createFilter(folderFilters, "message-id", "6.odd", "read");
+ createFilter(folderFilters, "user-agent", "Odd/1.0", "tag");
+ createFilter(folderFilters, "message-id", "8.unread", "read");
+ createFilter(folderFilters, "message-id", "8.unread", "unread");
+ folderFilters.loggingEnabled = true;
+ folder.setFilterList(folderFilters);
+
+ handlers.forEach(function (handler) {
+ testAction(handler, daemon, localserver);
+ });
+}
diff --git a/comm/mailnews/news/test/unit/test_getNewsMessage.js b/comm/mailnews/news/test/unit/test_getNewsMessage.js
new file mode 100644
index 0000000000..94fa82aece
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_getNewsMessage.js
@@ -0,0 +1,101 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Tests:
+ * - getNewMessages for a newsgroup folder (single message).
+ * - loadMessage for a newsgroup message
+ * - Downloading a single message and checking content in stream is correct.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+var server;
+var localserver;
+
+var streamListener = {
+ _data: "",
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIStreamListener",
+ "nsIRequestObserver",
+ ]),
+
+ // nsIRequestObserver
+ onStartRequest(aRequest) {},
+ onStopRequest(aRequest, aStatusCode) {
+ Assert.equal(aStatusCode, 0);
+
+ // Reduce any \r\n to just \n so we can do a good comparison on any
+ // platform.
+ var reduced = this._data.replace(/\r\n/g, "\n");
+ Assert.equal(reduced, kSimpleNewsArticle);
+
+ // We must finish closing connections and tidying up after a timeout
+ // so that the thread has time to unwrap itself.
+ do_timeout(0, doTestFinished);
+ },
+
+ // nsIStreamListener
+ onDataAvailable(aRequest, aInputStream, aOffset, aCount) {
+ let scriptStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+
+ scriptStream.init(aInputStream);
+
+ this._data += scriptStream.read(aCount);
+ },
+};
+
+function doTestFinished() {
+ localserver.closeCachedConnections();
+
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+
+ do_test_finished();
+}
+
+function run_test() {
+ server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+
+ try {
+ // Get the folder and new mail
+ var folder = localserver.rootFolder.getChildNamed("test.subscribe.simple");
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ Assert.equal(folder.getTotalMessages(false), 1);
+ Assert.ok(folder.hasNewMessages);
+
+ server.resetTest();
+
+ var message = folder.firstNewMessage;
+
+ var messageUri = folder.getUriForMsg(message);
+
+ do_test_pending();
+
+ Cc["@mozilla.org/messenger/messageservice;1?type=news"]
+ .getService(Ci.nsIMsgMessageService)
+ .loadMessage(messageUri, streamListener, null, null, false);
+ } catch (e) {
+ server.stop();
+ do_throw(e);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_internalUris.js b/comm/mailnews/news/test/unit/test_internalUris.js
new file mode 100644
index 0000000000..4be0402f0e
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_internalUris.js
@@ -0,0 +1,305 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Tests internal URIs generated by various methods in the code base.
+ * If you manually generate a news URI somewhere, please add it to this test.
+ */
+
+Cu.importGlobalProperties(["crypto"]);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+load("../../../resources/alertTestUtils.js");
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+var { PromiseUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseUtils.sys.mjs"
+);
+
+var daemon, localserver, server;
+
+var kCancelArticle =
+ "From: fake@acme.invalid\n" +
+ "Newsgroups: test.filter\n" +
+ "Subject: cancel <4@regular.invalid>\n" +
+ "References: <4@regular.invalid>\n" +
+ "Control: cancel <4@regular.invalid>\n" +
+ "MIME-Version: 1.0\n" +
+ "Content-Type: text/plain\n" +
+ "\n" +
+ "This message was cancelled from within ";
+
+var dummyMsgWindow;
+
+add_setup(function setupTest() {
+ registerAlertTestUtils();
+
+ daemon = setupNNTPDaemon();
+ server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+
+ // Set up an identity for posting.
+ let identity = MailServices.accounts.createIdentity();
+ identity.fullName = "Normal Person";
+ identity.email = "fake@acme.invalid";
+ MailServices.accounts.FindAccountForServer(localserver).addIdentity(identity);
+
+ dummyMsgWindow = new DummyMsgWindow();
+});
+
+add_task(async function test_newMsgs() {
+ // This tests nsMsgNewsFolder::GetNewsMessages via getNewMessages.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ Assert.equal(folder.getTotalMessages(false), 0);
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.getNewMessages(null, urlListener);
+ await urlListener.promise;
+ Assert.equal(folder.getTotalMessages(false), 8);
+});
+
+add_task(async function test_cancel() {
+ // This tests nsMsgNewsFolder::CancelMessage.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ let db = folder.msgDatabase;
+ let hdr = db.getMsgHdrForKey(4);
+
+ folder.QueryInterface(Ci.nsIMsgNewsFolder).cancelMessage(hdr, dummyMsgWindow);
+ await dummyMsgWindow.promise;
+
+ // Reset promise state.
+ dummyMsgWindow.deferPromise();
+
+ Assert.equal(folder.getTotalMessages(false), 7);
+
+ // Check the content of the CancelMessage itself.
+ let article = daemon.getGroup("test.filter")[9];
+ // Since the cancel message includes the brand name (Daily, Thunderbird), we
+ // only check the beginning of the string.
+ Assert.ok(article.fullText.startsWith(kCancelArticle));
+});
+
+function generateLongArticle() {
+ // After converting to base64, the message body will be 65536 * 4 = 256KB.
+ let arr = new Uint8Array(65536);
+ crypto.getRandomValues(arr);
+ return `Date: Mon, 23 Jun 2008 19:58:07 +0400
+From: Normal Person <fake@acme.invalid>
+MIME-Version: 1.0
+Subject: Odd Subject
+Content-Type: text/plain; charset=ISO-8859-1; format=flowed
+Content-Transfer-Encoding: 7bit
+Message-ID: <2@regular.invalid>
+
+${btoa(arr)}
+${btoa(arr)}
+${btoa(arr)}
+`;
+}
+
+add_task(async function test_fetchMessage() {
+ // Replace the second article with a large message.
+ daemon.addArticleToGroup(
+ new NewsArticle(generateLongArticle()),
+ "test.filter",
+ 2
+ );
+
+ // Tests nsNntpService::CreateMessageIDURL via FetchMessage.
+ let streamListener = new PromiseTestUtils.PromiseStreamListener();
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ MailServices.nntp.fetchMessage(folder, 2, null, streamListener, urlListener);
+ await urlListener.promise;
+ let data = await streamListener.promise;
+ // To point out that the streamListener Promise shouldn't reject.
+ Assert.ok(data);
+});
+
+add_task(async function test_fetchMessageNoStreamListener() {
+ // Tests nsNntpService::CreateMessageIDURL via FetchMessage.
+ let streamListener = null;
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ MailServices.nntp.fetchMessage(folder, 2, null, streamListener, urlListener);
+ await urlListener.promise;
+});
+
+add_task(async function test_search() {
+ // This tests nsNntpService::Search.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ var searchSession = Cc[
+ "@mozilla.org/messenger/searchSession;1"
+ ].createInstance(Ci.nsIMsgSearchSession);
+ searchSession.addScopeTerm(Ci.nsMsgSearchScope.news, folder);
+
+ let searchTerm = searchSession.createTerm();
+ searchTerm.attrib = Ci.nsMsgSearchAttrib.Subject;
+ let value = searchTerm.value;
+ value.str = "First";
+ searchTerm.value = value;
+ searchTerm.op = Ci.nsMsgSearchOp.Contains;
+ searchTerm.booleanAnd = false;
+ searchSession.appendTerm(searchTerm);
+
+ let hitCount;
+ let searchListener = new PromiseTestUtils.PromiseSearchNotify(searchSession, {
+ onSearchHit() {
+ hitCount++;
+ },
+ onNewSearch() {
+ hitCount = 0;
+ },
+ });
+
+ searchSession.search(null);
+ await searchListener.promise;
+
+ Assert.equal(hitCount, 1);
+});
+
+add_task(async function test_grouplist() {
+ // This tests nsNntpService::GetListOfGroupsOnServer.
+ let subserver = localserver.QueryInterface(Ci.nsISubscribableServer);
+ let subscribablePromise = PromiseUtils.defer();
+ let subscribeListener = {
+ OnDonePopulating() {
+ subscribablePromise.resolve();
+ },
+ };
+ subserver.subscribeListener = subscribeListener;
+
+ function enumGroups(rootUri) {
+ let hierarchy = subserver.getChildURIs(rootUri);
+ let groups = [];
+ for (let name of hierarchy) {
+ if (subserver.isSubscribable(name)) {
+ groups.push(name);
+ }
+ if (subserver.hasChildren(name)) {
+ groups = groups.concat(enumGroups(name));
+ }
+ }
+ return groups;
+ }
+
+ MailServices.nntp.getListOfGroupsOnServer(localserver, null, false);
+ await subscribablePromise.promise;
+
+ let groups = enumGroups("");
+ Assert.equal(groups.length, Object.keys(daemon._groups).length);
+ for (let group in daemon._groups) {
+ Assert.ok(groups.includes(group));
+ }
+
+ // First node in the group list, even though it is not subscribable,
+ // parent of "test.empty" group.
+ Assert.equal(subserver.getFirstChildURI(""), "test");
+
+ // Release reference, somehow impedes GC of 'subserver'.
+ subserver.subscribeListener = null;
+});
+
+add_task(async function test_postMessage() {
+ // This tests nsNntpService::SetUpNntpUrlForPosting via PostMessage.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.nntp.postMessage(
+ do_get_file("postings/post2.eml"),
+ "misc.test",
+ localserver.key,
+ urlListener,
+ null
+ );
+ await urlListener.promise;
+ Assert.equal(daemon.getGroup("misc.test").keys.length, 1);
+});
+
+// Not tested because it requires UI, and this is insufficient, I think.
+// function test_forwardInline() {
+// // This tests mime_parse_stream_complete via forwarding inline
+// let folder = localserver.rootFolder.getChildNamed("test.filter");
+// let hdr = folder.msgDatabase.getMsgHdrForKey(1);
+// MailServices.compose.forwardMessage("a@b.invalid", hdr, null,
+// localserver, Ci.nsIMsgComposeService.kForwardInline);
+// }
+
+add_task(async function test_escapedName() {
+ // This does a few tests to make sure our internal URIs work for newsgroups
+ // with names that need escaping.
+ let evilName = "test.malformed&name";
+ daemon.addGroup(evilName);
+ daemon.addArticle(make_article(do_get_file("postings/bug670935.eml")));
+ localserver.subscribeToNewsgroup(evilName);
+
+ // Can we access it?
+ let folder = localserver.rootFolder.getChildNamed(evilName);
+ let newMessageUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.getNewMessages(null, newMessageUrlListener);
+ await newMessageUrlListener.promise;
+
+ // If we get here, we didn't crash--newsgroups unescape properly.
+ // Load a message, to test news-message: URI unescaping.
+ let streamlistener = new PromiseTestUtils.PromiseStreamListener();
+ let fetchMessageUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.nntp.fetchMessage(
+ folder,
+ 1,
+ null,
+ streamlistener,
+ fetchMessageUrlListener
+ );
+ await fetchMessageUrlListener.promise;
+ let data = await streamlistener.promise;
+ // To point out that the streamListener Promise shouldn't reject.
+ Assert.ok(data);
+});
+
+add_task(function cleanUp() {
+ localserver.closeCachedConnections();
+});
+
+class DummyMsgWindow {
+ QueryInterface = ChromeUtils.generateQI([
+ "nsIMsgWindow",
+ "nsISupportsWeakReference",
+ ]);
+
+ constructor() {
+ this._deferredPromise = PromiseUtils.defer();
+ }
+
+ get statusFeedback() {
+ let scopedThis = this;
+ return {
+ startMeteors() {},
+ stopMeteors() {
+ scopedThis._deferredPromise.resolve(true);
+ },
+ showProgress() {},
+ };
+ }
+
+ get promptDialog() {
+ return alertUtilsPrompts;
+ }
+
+ deferPromise() {
+ this._deferredPromise = PromiseUtils.defer();
+ }
+
+ get promise() {
+ return this._deferredPromise.promise;
+ }
+}
+
+/* exported alert, confirmEx */
+// Prompts for cancel.
+function alertPS(parent, title, text) {}
+function confirmExPS(parent, title, text, flags) {
+ return 0;
+}
diff --git a/comm/mailnews/news/test/unit/test_newsAutocomplete.js b/comm/mailnews/news/test/unit/test_newsAutocomplete.js
new file mode 100644
index 0000000000..29fecb3d9d
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_newsAutocomplete.js
@@ -0,0 +1,107 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function acObserver() {}
+
+acObserver.prototype = {
+ _search: null,
+ _result: null,
+
+ onSearchResult(aSearch, aResult) {
+ this._search = aSearch;
+ this._result = aResult;
+ },
+};
+
+function run_test() {
+ setupLocalServer(119);
+
+ // create identity
+ let identity = MailServices.accounts.createIdentity();
+ _account.addIdentity(identity);
+
+ let acs = Cc["@mozilla.org/autocomplete/search;1?name=news"].getService(
+ Ci.nsIAutoCompleteSearch
+ );
+ let obs;
+
+ let paramsN = JSON.stringify({
+ idKey: identity.key,
+ accountKey: _account.key,
+ type: "addr_newsgroups",
+ });
+ let paramsF = JSON.stringify({
+ idKey: identity.key,
+ accountKey: _account.key,
+ type: "addr_followup",
+ });
+ let paramsMail = JSON.stringify({
+ idKey: identity.key,
+ accountKey: _account.key,
+ type: "addr_to",
+ });
+
+ // misc.test is not subscribed
+ obs = new acObserver();
+ acs.startSearch("misc", paramsN, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ obs = new acObserver();
+ acs.startSearch("misc", paramsF, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ obs = new acObserver();
+ acs.startSearch("misc", paramsMail, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ // test.filter is subscribed
+ obs = new acObserver();
+ acs.startSearch("filter", paramsN, null, obs);
+ Assert.equal(obs._result.matchCount, 1);
+
+ obs = new acObserver();
+ acs.startSearch("filter", paramsF, null, obs);
+ Assert.equal(obs._result.matchCount, 1);
+
+ // ... but no auto-complete should occur for addr_to
+ obs = new acObserver();
+ acs.startSearch("filter", paramsMail, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ // test.subscribe.empty and test.subscribe.simple are subscribed
+ obs = new acObserver();
+ acs.startSearch("subscribe", paramsN, null, obs);
+ Assert.equal(obs._result.matchCount, 2);
+
+ obs = new acObserver();
+ acs.startSearch("subscribe", paramsF, null, obs);
+ Assert.equal(obs._result.matchCount, 2);
+
+ // ... but no auto-complete should occur for addr_to
+ obs = new acObserver();
+ acs.startSearch("subscribe", paramsMail, null, obs);
+ Assert.ok(obs._result == null || obs._result.matchCount == 0);
+
+ // test.subscribe.empty is subscribed, test.empty is not
+ obs = new acObserver();
+ acs.startSearch("empty", paramsN, null, obs);
+ Assert.equal(obs._result.matchCount, 1);
+
+ obs = new acObserver();
+ acs.startSearch("empty", paramsF, null, obs);
+ Assert.equal(obs._result.matchCount, 1);
+
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_nntpContentLength.js b/comm/mailnews/news/test/unit/test_nntpContentLength.js
new file mode 100644
index 0000000000..f9f780011d
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpContentLength.js
@@ -0,0 +1,80 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Test content length for the news protocol. This focuses on necko URLs
+ * that are run externally.
+ */
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+var server;
+var localserver;
+
+function run_test() {
+ server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start();
+ localserver = setupLocalServer(server.port);
+
+ try {
+ // Get the folder and new mail
+ let folder = localserver.rootFolder.getChildNamed("test.subscribe.simple");
+ folder.clearFlag(Ci.nsMsgFolderFlags.Offline);
+ folder.getNewMessages(null, {
+ OnStopRunningUrl() {
+ localserver.closeCachedConnections();
+ },
+ });
+ server.performTest();
+
+ Assert.equal(folder.getTotalMessages(false), 1);
+ Assert.ok(folder.hasNewMessages);
+
+ server.resetTest();
+
+ // Get the message URI
+ let msgHdr = folder.firstNewMessage;
+ let messageUri = folder.getUriForMsg(msgHdr);
+ // Convert this to a URI that necko can run
+ let messageService = MailServices.messageServiceFromURI(messageUri);
+ let neckoURL = messageService.getUrlForUri(messageUri);
+ // Don't use the necko URL directly. Instead, get the spec and create a new
+ // URL using the IO service
+ let urlToRun = Services.io.newURI(neckoURL.spec);
+
+ // Get a channel from this URI, and check its content length
+ let channel = Services.io.newChannelFromURI(
+ urlToRun,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ Assert.equal(channel.contentLength, kSimpleNewsArticle.length);
+
+ // Now try an attachment. &part=1.2
+ // XXX the message doesn't really have an attachment
+ let attachmentURL = Services.io.newURI(neckoURL.spec + "&part=1.2");
+ Services.io.newChannelFromURI(
+ attachmentURL,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ null,
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ Ci.nsIContentPolicy.TYPE_OTHER
+ );
+ // Currently attachments have their content length set to the length of the
+ // entire message
+ Assert.equal(channel.contentLength, kSimpleNewsArticle.length);
+ } catch (e) {
+ server.stop();
+ do_throw(e);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_nntpGroupPassword.js b/comm/mailnews/news/test/unit/test_nntpGroupPassword.js
new file mode 100644
index 0000000000..6bf9be53cc
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpGroupPassword.js
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Authentication tests for NNTP (based on RFC4643).
+ */
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+add_task(async function () {
+ await Services.logins.initializationPromise;
+
+ daemon.groupCredentials = {
+ "test.subscribe.empty": ["group1", "pass1"],
+ "test.filter": ["group2", "pass2"],
+ };
+
+ var server = makeServer(NNTP_RFC4643_extension, daemon);
+ server.start();
+ var localserver = setupLocalServer(server.port);
+ localserver.singleSignon = false;
+
+ // Add passwords to the manager
+ var serverURI = "news://localhost/";
+ var loginInfo1 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ loginInfo1.init(
+ serverURI + "test.subscribe.empty",
+ null,
+ serverURI + "test.subscribe.empty",
+ "group1",
+ "pass1",
+ "",
+ ""
+ );
+ Services.logins.addLogin(loginInfo1);
+ var loginInfo2 = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(
+ Ci.nsILoginInfo
+ );
+ loginInfo2.init(
+ serverURI + "test.filter",
+ null,
+ serverURI + "test.filter",
+ "group2",
+ "pass2",
+ "",
+ ""
+ );
+ Services.logins.addLogin(loginInfo2);
+ try {
+ var prefix = "news://localhost:" + server.port + "/";
+ var transaction;
+
+ test = "per-group password part 1";
+ setupProtocolTest(
+ server.port,
+ prefix + "test.subscribe.empty",
+ localserver
+ );
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.subscribe.empty",
+ "AUTHINFO user group1",
+ "AUTHINFO pass pass1",
+ "GROUP test.subscribe.empty",
+ ]);
+
+ test = "per-group password part 2";
+ server.resetTest();
+ setupProtocolTest(server.port, prefix + "test.filter", localserver);
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.filter",
+ "AUTHINFO user group2",
+ "AUTHINFO pass pass2",
+ "GROUP test.filter",
+ "XOVER 1-8",
+ ]);
+ } catch (e) {
+ dump("NNTP Protocol test " + test + " failed for type RFC 977:\n");
+ try {
+ var trans = server.playTransaction();
+ if (trans) {
+ dump("Commands called: " + uneval(trans) + "\n");
+ }
+ } catch (exp) {}
+ do_throw(e);
+ }
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpPassword.js b/comm/mailnews/news/test/unit/test_nntpPassword.js
new file mode 100644
index 0000000000..dbccf01f74
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPassword.js
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Authentication tests for NNTP (based on RFC4643).
+ *
+ * Note: Logins for newsgroup servers for 1.8 were stored with either the
+ * default port or the SSL default port. Nothing else!
+ */
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+add_task(async function () {
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ var server = makeServer(NNTP_RFC4643_extension, daemon);
+ server.start();
+
+ try {
+ var prefix = "news://localhost:" + server.port + "/";
+ var transaction;
+
+ // Test - group subscribe listing
+ test = "news:*";
+ setupProtocolTest(server.port, prefix + "*");
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "LIST",
+ "AUTHINFO user testnews",
+ "AUTHINFO pass newstest",
+ "LIST",
+ ]);
+ } catch (e) {
+ dump("NNTP Protocol test " + test + " failed for type RFC 977:\n");
+ try {
+ var trans = server.playTransaction();
+ if (trans) {
+ dump("Commands called: " + trans.them + "\n");
+ }
+ } catch (exp) {}
+ do_throw(e);
+ }
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpPassword2.js b/comm/mailnews/news/test/unit/test_nntpPassword2.js
new file mode 100644
index 0000000000..8fcf7d7e0b
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPassword2.js
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Authentication tests for NNTP (based on RFC4643) - checks for servers whose
+ * details have changed (e.g. realhostname is different from hostname).
+ *
+ * Note: Logins for newsgroup servers for 1.8 were stored with either the
+ * default port or the SSL default port. Nothing else!
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+add_task(async function () {
+ let server = makeServer(NNTP_RFC4643_extension, daemon);
+ server.start();
+
+ // These preferences set up a local news server that has had its hostname
+ // and username changed from the original settings. We can't do this by
+ // function calls for this test as they would cause the password to be
+ // forgotten when changing the hostname/username and this breaks the test.
+ Services.prefs.setCharPref("mail.account.account1.server", "server1");
+ Services.prefs.setCharPref("mail.account.account2.server", "server2");
+ Services.prefs.setCharPref("mail.account.account2.identities", "id1");
+ Services.prefs.setCharPref(
+ "mail.accountmanager.accounts",
+ "account1,account2"
+ );
+ Services.prefs.setCharPref(
+ "mail.accountmanager.localfoldersserver",
+ "server1"
+ );
+ Services.prefs.setCharPref("mail.accountmanager.defaultaccount", "account2");
+ Services.prefs.setCharPref("mail.identity.id1.fullName", "testnntp");
+ Services.prefs.setCharPref(
+ "mail.identity.id1.useremail",
+ "testnntp@localhost"
+ );
+ Services.prefs.setBoolPref("mail.identity.id1.valid", true);
+ Services.prefs.setCharPref("mail.server.server1.hostname", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.name", "Local Folders");
+ Services.prefs.setCharPref("mail.server.server1.type", "none");
+ Services.prefs.setCharPref("mail.server.server1.userName", "nobody");
+ Services.prefs.setCharPref("mail.server.server2.hostname", "invalid");
+ Services.prefs.setCharPref(
+ "mail.server.server2.name",
+ "testnntp on localhost"
+ );
+ Services.prefs.setIntPref("mail.server.server2.port", server.port);
+ Services.prefs.setCharPref("mail.server.server2.realhostname", "localhost");
+ Services.prefs.setCharPref("mail.server.server2.type", "nntp");
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8-alt.json");
+
+ try {
+ // Note, the uri is for hostname "invalid" which is the original uri. See
+ // setupProtocolTest parameters.
+ var prefix = "news://invalid:" + server.port + "/";
+
+ // Test - group subscribe listing
+ test = "news:*";
+
+ // Get the existing incoming server
+ MailServices.accounts.loadAccounts();
+
+ // Create the incoming server with "original" details.
+ var incomingServer = MailServices.accounts.getIncomingServer("server2");
+
+ subscribeServer(incomingServer);
+
+ // Now set up and run the tests
+ setupProtocolTest(server.port, prefix + "*", incomingServer);
+ server.performTest();
+ var transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "LIST",
+ "AUTHINFO user testnews",
+ "AUTHINFO pass newstest",
+ "LIST",
+ ]);
+ incomingServer.QueryInterface(Ci.nsISubscribableServer).subscribeCleanup();
+ } catch (e) {
+ dump("NNTP Protocol test " + test + " failed for type RFC 977:\n");
+ try {
+ var trans = server.playTransaction();
+ if (trans) {
+ dump("Commands called: " + trans.them + "\n");
+ }
+ } catch (exp) {}
+ do_throw(e);
+ }
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpPassword3.js b/comm/mailnews/news/test/unit/test_nntpPassword3.js
new file mode 100644
index 0000000000..a4c5e0ac76
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPassword3.js
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/**
+ * Extra tests for forgetting newsgroup usernames and passwords.
+ */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/passwordStorage.js");
+
+var kUsername = "testnews";
+var kPassword = "newstest";
+var kProtocol = "nntp";
+var kHostname = "localhost";
+var kServerUrl = "news://" + kHostname;
+
+add_task(async function () {
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ await setupForPassword("signons-mailnews1.8.json");
+
+ // Set up the basic accounts and folders.
+ localAccountUtils.loadLocalMailAccount();
+
+ var incomingServer = MailServices.accounts.createIncomingServer(
+ null,
+ kHostname,
+ kProtocol
+ );
+
+ // Test - Check there is a password to begin with...
+ var logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUsername);
+ Assert.equal(logins[0].password, kPassword);
+
+ // Test - Remove the news password login via the incoming server
+ incomingServer.forgetPassword();
+
+ logins = Services.logins.findLogins(kServerUrl, null, kServerUrl);
+
+ // should be no passwords left...
+ Assert.equal(logins.length, 0);
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpPasswordFailure.js b/comm/mailnews/news/test/unit/test_nntpPasswordFailure.js
new file mode 100644
index 0000000000..994a701c8e
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPasswordFailure.js
@@ -0,0 +1,196 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This test checks to see if the nntp password failure is handled correctly.
+ * The steps are:
+ * - Have an invalid password in the password database.
+ * - Check we get a prompt asking what to do.
+ * - Check retry does what it should do.
+ * - Check cancel does what it should do.
+ * - Re-initiate connection, this time select enter new password, check that
+ * we get a new password prompt and can enter the password.
+ */
+
+var { mailTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/MailTestUtils.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/* import-globals-from ../../../test/resources/alertTestUtils.js */
+/* import-globals-from ../../../test/resources/passwordStorage.js */
+load("../../../resources/alertTestUtils.js");
+load("../../../resources/passwordStorage.js");
+
+var server;
+var daemon;
+var incomingServer;
+var folder;
+var attempt = 0;
+var logins;
+
+var kUserName = "testnews";
+var kInvalidPassword = "newstest";
+var kValidPassword = "notallama";
+
+add_setup(function () {
+ // Disable new mail notifications
+ Services.prefs.setBoolPref("mail.biff.play_sound", false);
+ Services.prefs.setBoolPref("mail.biff.show_alert", false);
+ Services.prefs.setBoolPref("mail.biff.show_tray_icon", false);
+ Services.prefs.setBoolPref("mail.biff.animate_dock_icon", false);
+ Services.prefs.setBoolPref("signon.debug", true);
+
+ // Prepare files for passwords (generated by a script in bug 1018624).
+ setupForPassword("signons-mailnews1.8.json");
+
+ registerAlertTestUtils();
+
+ // Set up the server
+ daemon = setupNNTPDaemon();
+ function createHandler(d) {
+ var handler = new NNTP_RFC4643_extension(d);
+ handler.expectedPassword = kValidPassword;
+ return handler;
+ }
+ server = new nsMailServer(createHandler, daemon);
+ server.start();
+ incomingServer = setupLocalServer(server.port);
+ folder = incomingServer.rootFolder.getChildNamed("test.subscribe.simple");
+
+ // Check that we haven't got any messages in the folder, if we have its a test
+ // setup issue.
+ Assert.equal(folder.getTotalMessages(false), 0);
+});
+
+add_task(async function getMail1() {
+ // Now get mail.
+ let urlListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl(url, result) {
+ // On the last attempt, we should have successfully got one mail.
+ Assert.equal(folder.getTotalMessages(false), attempt == 4 ? 1 : 0);
+
+ // If we've just cancelled, expect failure rather than success
+ // because the server dropped the connection.
+ dump("in onStopRunning, result = " + result + "\n");
+ // do_check_eq(result, attempt == 2 ? Cr.NS_ERROR_FAILURE : 0);
+ },
+ });
+ folder.getNewMessages(gDummyMsgWindow, urlListener);
+ await urlListener.promise;
+
+ Assert.equal(attempt, 2);
+
+ // Check that we haven't forgotten the login even though we've retried and cancelled.
+ logins = Services.logins.findLogins(
+ "news://localhost",
+ null,
+ "news://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kInvalidPassword);
+
+ server.resetTest();
+});
+
+add_task(async function getMail2() {
+ let urlListener = new PromiseTestUtils.PromiseUrlListener({
+ OnStopRunningUrl(url, result) {
+ // On the last attempt, we should have successfully got one mail.
+ Assert.equal(folder.getTotalMessages(false), attempt == 4 ? 1 : 0);
+
+ // If we've just cancelled, expect failure rather than success
+ // because the server dropped the connection.
+ dump("in onStopRunning, result = " + result + "\n");
+ // do_check_eq(result, attempt == 2 ? Cr.NS_ERROR_FAILURE : 0);
+ },
+ });
+ folder.getNewMessages(gDummyMsgWindow, urlListener);
+ await urlListener.promise;
+ // Now check the new one has been saved.
+ logins = Services.logins.findLogins(
+ "news://localhost",
+ null,
+ "news://localhost"
+ );
+
+ Assert.equal(logins.length, 1);
+ Assert.equal(logins[0].username, kUserName);
+ Assert.equal(logins[0].password, kValidPassword);
+});
+
+add_task(function endTest() {
+ // Clean up nicely the test.
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+});
+
+/* exported alert, confirmEx, promptUsernameAndPasswordPS */
+function alertPS(parent, aDialogText, aText) {
+ // The first few attempts may prompt about the password problem, the last
+ // attempt shouldn't.
+ Assert.ok(attempt < 4);
+
+ // Log the fact we've got an alert, but we don't need to test anything here.
+ dump("Alert Title: " + aDialogText + "\nAlert Text: " + aText + "\n");
+}
+
+function confirmExPS(
+ parent,
+ aDialogTitle,
+ aText,
+ aButtonFlags,
+ aButton0Title,
+ aButton1Title,
+ aButton2Title,
+ aCheckMsg,
+ aCheckState
+) {
+ switch (++attempt) {
+ // First attempt, retry.
+ case 1:
+ dump("\nAttempting retry\n");
+ return 0;
+ // Second attempt, cancel.
+ case 2:
+ dump("\nCancelling login attempt\n");
+ return 1;
+ // Third attempt, retry.
+ case 3:
+ dump("\nAttempting Retry\n");
+ return 0;
+ // Fourth attempt, enter a new password.
+ case 4:
+ dump("\nEnter new password\n");
+ return 2;
+ default:
+ throw new Error("unexpected attempt number " + attempt);
+ }
+}
+
+function promptUsernameAndPasswordPS(
+ aParent,
+ aDialogTitle,
+ aText,
+ aUsername,
+ aPassword,
+ aCheckMsg,
+ aCheckState
+) {
+ if (attempt == 4) {
+ aUsername.value = kUserName;
+ aPassword.value = kValidPassword;
+ aCheckState.value = true;
+ return true;
+ }
+ return false;
+}
diff --git a/comm/mailnews/news/test/unit/test_nntpPost.js b/comm/mailnews/news/test/unit/test_nntpPost.js
new file mode 100644
index 0000000000..e49aa81d92
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpPost.js
@@ -0,0 +1,37 @@
+// Tests that the news can correctly post messages
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+var { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+/**
+ * Test dot is stuffed correctly when posting an article.
+ */
+add_task(async function test_nntpPost() {
+ // Setup test server.
+ let daemon = setupNNTPDaemon();
+ let handler = new NNTP_RFC977_handler(daemon);
+ let server = new nsMailServer(() => handler, daemon);
+ server.start();
+ registerCleanupFunction(() => server.stop());
+
+ // Send post3.eml to the server.
+ let localServer = setupLocalServer(server.port);
+ let testFile = do_get_file("postings/post3.eml");
+ let urlListener = new PromiseTestUtils.PromiseUrlListener();
+ MailServices.nntp.postMessage(
+ testFile,
+ "test.empty",
+ localServer.key,
+ urlListener,
+ null
+ );
+ await urlListener.promise;
+
+ // Because Nntpd.jsm undone the dot-stuffing, handler.post should be the same
+ // as the original post.
+ equal(handler.post, await IOUtils.readUTF8(testFile.path));
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpProtocols.js b/comm/mailnews/news/test/unit/test_nntpProtocols.js
new file mode 100644
index 0000000000..48f13095fd
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpProtocols.js
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/*
+ * Test suite for getting news urls via the protocol handler.
+ */
+
+var defaultProtocolFlags =
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE |
+ Ci.nsIProtocolHandler.ALLOWS_PROXY |
+ Ci.nsIProtocolHandler.URI_FORBIDS_AUTOMATIC_DOCUMENT_REPLACEMENT |
+ Ci.nsIProtocolHandler.URI_FORBIDS_COOKIE_ACCESS |
+ Ci.nsIProtocolHandler.ORIGIN_IS_FULL_SPEC;
+
+var protocols = [
+ {
+ protocol: "news",
+ urlSpec: "news://user@localhost/",
+ defaultPort: Ci.nsINntpUrl.DEFAULT_NNTP_PORT,
+ },
+ // XXX News secure protocol not working yet.
+ /* { protocol: "snews",
+ urlSpec: "snews://user@localhost/",
+ defaultPort: Ci.nsINntpUrl.DEFAULT_NNTPS_PORT
+} */
+];
+
+function run_test() {
+ for (var part = 0; part < protocols.length; ++part) {
+ print("protocol: " + protocols[part].protocol);
+
+ var pH = Cc[
+ "@mozilla.org/network/protocol;1?name=" + protocols[part].protocol
+ ].createInstance(Ci.nsIProtocolHandler);
+
+ Assert.equal(pH.scheme, protocols[part].protocol);
+ Assert.equal(
+ Services.io.getDefaultPort(pH.scheme),
+ protocols[part].defaultPort
+ );
+ Assert.equal(Services.io.getProtocolFlags(pH.scheme), defaultProtocolFlags);
+
+ // Whip through some of the ports to check we get the right results.
+ // NEWS allows connecting to any port.
+ for (let i = 0; i < 1024; ++i) {
+ Assert.ok(pH.allowPort(i, ""));
+ }
+
+ // Check we get a URI when we ask for one
+ var uri = Services.io.newURI(protocols[part].urlSpec);
+
+ uri.QueryInterface(Ci.nsINntpUrl);
+
+ Assert.equal(uri.spec, protocols[part].urlSpec);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_nntpProxy.js b/comm/mailnews/news/test/unit/test_nntpProxy.js
new file mode 100644
index 0000000000..826a057c9d
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpProxy.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+// Tests that NNTP over a SOCKS proxy works.
+
+const { NetworkTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/NetworkTestUtils.jsm"
+);
+const { PromiseTestUtils } = ChromeUtils.import(
+ "resource://testing-common/mailnews/PromiseTestUtils.jsm"
+);
+
+const PORT = 119;
+
+var daemon, localserver, server;
+
+add_setup(async function () {
+ daemon = setupNNTPDaemon();
+ server = makeServer(NNTP_RFC2980_handler, daemon);
+ server.start();
+ NetworkTestUtils.configureProxy("news.tinderbox.invalid", PORT, server.port);
+ localserver = setupLocalServer(PORT, "news.tinderbox.invalid");
+});
+
+add_task(async function findMessages() {
+ // This is a trivial check that makes sure that we actually do some network
+ // traffic without caring about the exact network traffic.
+ let folder = localserver.rootFolder.getChildNamed("test.filter");
+ equal(folder.getTotalMessages(false), 0);
+ let asyncUrlListener = new PromiseTestUtils.PromiseUrlListener();
+ folder.getNewMessages(null, asyncUrlListener);
+ await asyncUrlListener.promise;
+ equal(folder.getTotalMessages(false), 8);
+});
+
+add_task(async function cleanUp() {
+ NetworkTestUtils.shutdownServers();
+ localserver.closeCachedConnections();
+});
diff --git a/comm/mailnews/news/test/unit/test_nntpUrl.js b/comm/mailnews/news/test/unit/test_nntpUrl.js
new file mode 100644
index 0000000000..93c51334bd
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_nntpUrl.js
@@ -0,0 +1,30 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/licenses/publicdomain/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+function getMessageHeaderFromUrl(aUrl) {
+ let msgUrl = Services.io.newURI(aUrl).QueryInterface(Ci.nsIMsgMessageUrl);
+ return msgUrl.messageHeader;
+}
+
+function run_test() {
+ // This is crash test for Bug 392729
+ try {
+ // msgkey is invalid for news:// protocol
+ getMessageHeaderFromUrl(
+ "news://localhost:119" +
+ "/123@example.invalid?group=test.subscribe.simple&key=abcdefghijk"
+ );
+ Assert.ok(false);
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_MALFORMED_URI);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_server.js b/comm/mailnews/news/test/unit/test_server.js
new file mode 100644
index 0000000000..f0954add1c
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_server.js
@@ -0,0 +1,179 @@
+// Protocol tests for NNTP. These actually aren't too important, but their main
+// purpose is to make sure that maild is working properly and to provide
+// examples for how using maild. They also help make sure that I coded Nntpd.jsm
+// right, both logically and for RFC compliance.
+// TODO:
+// * We need to hook up mochitest,
+// * TLS negotiation.
+
+// The basic daemon to use for testing Nntpd.jsm implementations
+var daemon = setupNNTPDaemon();
+
+// NNTP SERVER TESTS
+// -----------------
+// Functions in order as defined in Nntpd.jsm. Each function tests the URLs
+// that are located over the implementation of nsNNTPProtocol::LoadURL and
+// added in bug 400331. Furthermore, they are tested in rough order as they
+// would be expected to be used in a session. If more URL types are modified,
+// please add a corresponding type to the following tests.
+// When adding new servers, only test the commands that become different for
+// each specified server, to keep down redudant tests.
+
+function testRFC977() {
+ var server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start(NNTP_PORT);
+
+ try {
+ var prefix = "news://localhost:" + NNTP_PORT + "/";
+ var transaction;
+
+ // Test - group subscribe listing
+ test = "news:*";
+ setupProtocolTest(NNTP_PORT, prefix + "*");
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, ["MODE READER", "LIST"]);
+
+ // Test - getting group headers
+ test = "news:test.subscribe.empty";
+ server.resetTest();
+ setupProtocolTest(NNTP_PORT, prefix + "test.subscribe.empty");
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.subscribe.empty",
+ ]);
+
+ // Test - getting an article
+ test = "news:MESSAGE_ID";
+ server.resetTest();
+ setupProtocolTest(NNTP_PORT, prefix + "TSS1@nntp.invalid");
+ server.performTest();
+ transaction = server.playTransaction();
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "ARTICLE <TSS1@nntp.invalid>",
+ ]);
+ } catch (e) {
+ dump("NNTP Protocol test " + test + " failed for type RFC 977:\n");
+ try {
+ var trans = server.playTransaction();
+ if (trans) {
+ dump("Commands called: " + trans.them + "\n");
+ }
+ } catch (exp) {}
+ do_throw(e);
+ }
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
+
+function testConnectionLimit() {
+ var server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start(NNTP_PORT);
+ // 1 is the default, but other tests do change it, so let's be explicit.
+ _server.maximumConnectionsNumber = 1;
+
+ var prefix = "news://localhost:" + NNTP_PORT + "/";
+
+ // To test make connections limit, we run two URIs simultaneously.
+ var url = Services.io.newURI(prefix + "*");
+ _server.loadNewsUrl(url, null, null);
+ setupProtocolTest(NNTP_PORT, prefix + "TSS1@nntp.invalid");
+ server.performTest();
+ // We should have length one... which means this must be a transaction object,
+ // containing only us and them
+ // (playTransactions() returns an array of transaction objects if there is
+ // more than one of them, so this assert will fail in that case).
+ Assert.ok("us" in server.playTransaction());
+ server.stop();
+
+ var thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
+
+function testReentrantClose() {
+ // What we are testing is that a CloseConnection that spins the event loop
+ // does not cause a crash.
+ var server = makeServer(NNTP_RFC977_handler, daemon);
+ server.start(NNTP_PORT);
+
+ var listener = {
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, rv) {
+ // Spin the event loop (entering nsNNTPProtocol::ProcessProtocolState)
+ let thread = gThreadManager.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+ },
+ };
+ // Nice multi-step command--we can close while executing this URL if we are
+ // careful.
+ var url = Services.io.newURI(
+ "news://localhost:" + NNTP_PORT + "/test.filter"
+ );
+ url.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ url.RegisterListener(listener);
+
+ _server.loadNewsUrl(url, null, {
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+ onStartRequest() {},
+ onStopRequest() {},
+ });
+ server.performTest("GROUP");
+ dump("Stopping server\n");
+ gThreadManager.currentThread.dispatch(
+ {
+ run() {
+ _server.closeCachedConnections();
+ },
+ },
+ Ci.nsIEventTarget.DISPATCH_NORMAL
+ );
+ server.performTest();
+ server.stop();
+
+ // Break refcnt loops
+ listener = url = null;
+}
+
+function testManyConnections() {
+ // Start up 2 connections at once and make sure that they don't conflict
+ var server = makeServer(NNTP_RFC2980_handler, daemon);
+ setupLocalServer(NNTP_PORT);
+ server.start(NNTP_PORT);
+ _server.maximumConnectionsNumber = 3;
+ var listener = {
+ ran: 0,
+ OnStartRunningUrl(url) {},
+ OnStopRunningUrl(url, rv) {
+ if (--this.ran == 0) {
+ _server.closeCachedConnections();
+ }
+ },
+ };
+ for (let group of _server.rootFolder.subFolders) {
+ group.getNewMessages(null, listener);
+ listener.ran++;
+ }
+ server.performTest();
+ // The last one that is processed is test.filter, so make sure that
+ // test.subscribed.simple is not retrieving the data meant for test.filter
+ let folder = _server.rootFolder.getChildNamed("test.subscribe.simple");
+ Assert.equal(folder.getTotalMessages(false), 1);
+}
+
+function run_test() {
+ testRFC977();
+ testConnectionLimit();
+ testReentrantClose();
+ testManyConnections();
+}
diff --git a/comm/mailnews/news/test/unit/test_uriParser.js b/comm/mailnews/news/test/unit/test_uriParser.js
new file mode 100644
index 0000000000..a152076333
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_uriParser.js
@@ -0,0 +1,221 @@
+// Tests nsINntpUrl parsing.
+
+var { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+var localserver;
+var tests = [
+ // news://host/-based URIs
+ {
+ uri: "news://localhost/?newgroups",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionListNewGroups,
+ },
+ // news://host/group-based
+ {
+ uri: "news://news.server.example/example.group.this",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "example.group.this",
+ },
+ {
+ uri: "news://news.server.example/*",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionListGroups,
+ },
+ {
+ uri: "news://news.server.example/news.*",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionListGroups,
+ },
+ {
+ uri: "news://localhost/test.filter?list-ids",
+ get server() {
+ return localserver;
+ },
+ get folder() {
+ return localserver.rootFolder.getChildNamed("test.filter");
+ },
+ newsAction: Ci.nsINntpUrl.ActionListIds,
+ group: "test.filter",
+ },
+ {
+ uri: "news://localhost/some.group?search/XPAT From 1-5 [Ww][Hh][Oo]",
+ get server() {
+ return localserver;
+ },
+ newsAction: Ci.nsINntpUrl.ActionSearch,
+ group: "some.group",
+ },
+
+ // news://host/message-based URIs
+ {
+ uri: "news://localhost/message-id@some-host.invalid",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ messageID: "message-id@some-host.invalid",
+ group: "",
+ key: 0xffffffff,
+ },
+ {
+ uri: "news://localhost/message-id@some-host.invalid?part=1.4",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchPart,
+ messageID: "message-id@some-host.invalid",
+ },
+ {
+ uri: "news://localhost/message-id@some-host.invalid?cancel",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionCancelArticle,
+ messageID: "message-id@some-host.invalid",
+ },
+ {
+ uri: "news://localhost/message-id@some-host.invalid?group=foo&key=123",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ messageID: "message-id@some-host.invalid",
+ group: "foo",
+ key: 123,
+ },
+
+ // No-authority uris
+ {
+ uri: "news:rec.games.pinball",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "rec.games.pinball",
+ host: "",
+ },
+ {
+ uri: "news:message-id@some-host.invalid",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ messageID: "message-id@some-host.invalid",
+ group: "",
+ key: 0xffffffff,
+ },
+
+ // news-message://host/group#key
+ {
+ uri: "news-message://localhost/test.simple.subscribe#1",
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ group: "test.simple.subscribe",
+ key: 1,
+ },
+
+ // nntp://host/group
+ {
+ uri: "nntp://localhost/test.filter",
+ get server() {
+ return localserver;
+ },
+ get folder() {
+ return localserver.rootFolder.getChildNamed("test.filter");
+ },
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "test.filter",
+ },
+ {
+ uri: "nntp://localhost/i.dont.exist",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "i.dont.exist",
+ },
+ {
+ uri: "nntp://news.example.invalid/i.dont.exist",
+ server: null,
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionGetNewNews,
+ group: "i.dont.exist",
+ },
+
+ // nntp://host/group/key
+ {
+ uri: "nntp://localhost/test.filter/123",
+ get server() {
+ return localserver;
+ },
+ get folder() {
+ return localserver.rootFolder.getChildNamed("test.filter");
+ },
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ group: "test.filter",
+ key: 123,
+ },
+ {
+ uri: "nntp://localhost/i.dont.exist/123",
+ get server() {
+ return localserver;
+ },
+ folder: null,
+ newsAction: Ci.nsINntpUrl.ActionFetchArticle,
+ group: "i.dont.exist",
+ key: 123,
+ },
+];
+
+var invalid_uris = [
+ "news-message://localhost/test.simple.subscribe#hello",
+ "nntp://localhost/",
+ "nntp://localhost/a.group/hello",
+ "nntp://localhost/a.group/0",
+ "nntp:a.group",
+];
+
+function run_test() {
+ // We're not running the server, just setting it up
+ localserver = setupLocalServer(119);
+ for (let test of tests) {
+ dump("Checking URL " + test.uri + "\n");
+ let url = Services.io.newURI(test.uri);
+ url.QueryInterface(Ci.nsIMsgMailNewsUrl);
+ url.QueryInterface(Ci.nsINntpUrl);
+ for (let prop in test) {
+ if (prop == "uri") {
+ continue;
+ }
+ Assert.equal(url[prop], test[prop]);
+ }
+ }
+
+ for (let fail of invalid_uris) {
+ try {
+ dump("Checking URL " + fail + " for failure\n");
+ Services.io.newURI(fail);
+ Assert.ok(false);
+ } catch (e) {
+ Assert.equal(e.result, Cr.NS_ERROR_MALFORMED_URI);
+ }
+ }
+
+ // The password migration is async, so trigger an event to prevent the logon
+ // manager from trying to migrate after shutdown has started.
+ let thread = Services.tm.currentThread;
+ while (thread.hasPendingEvents()) {
+ thread.processNextEvent(true);
+ }
+}
diff --git a/comm/mailnews/news/test/unit/test_xover.js b/comm/mailnews/news/test/unit/test_xover.js
new file mode 100644
index 0000000000..48cc6f3e50
--- /dev/null
+++ b/comm/mailnews/news/test/unit/test_xover.js
@@ -0,0 +1,42 @@
+/* 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/. */
+
+let daemon = setupNNTPDaemon();
+let server = makeServer(NNTP_RFC2980_handler, daemon);
+server.start();
+registerCleanupFunction(() => {
+ server.stop();
+});
+
+let incomingServer = setupLocalServer(server.port);
+
+/**
+ * Test nsIDBFolderInfo.knownArtsSet is correctly updated after XOVER response.
+ * knownArtsSet depends on the XOVER range requested, it doesn't matter if
+ * articles in that range don't exist on the server.
+ */
+add_task(function test_updateKnownKeySetAfterXOver() {
+ // setupNNTPDaemon inited test.filter with 8 messages, delete the 5th, 6th here.
+ daemon.removeArticleFromGroup("test.filter", 5);
+ daemon.removeArticleFromGroup("test.filter", 6);
+
+ // Trigger a get new messages request.
+ let prefix = "news://localhost:" + server.port + "/";
+ setupProtocolTest(server.port, prefix + "test.filter", incomingServer);
+ server.performTest();
+ let transaction = server.playTransaction();
+
+ // Test XOVER was sent correctly.
+ do_check_transaction(transaction, [
+ "MODE READER",
+ "GROUP test.filter",
+ "XOVER 1-8",
+ ]);
+
+ // Test knownArtsSet was updated correctly.
+ let folder = incomingServer.rootFolder.getChildNamed("test.filter");
+ let groupInfo = folder.msgDatabase.dBFolderInfo;
+ // knownArtsSet should be "1-8", not "1-4,7-8".
+ equal(groupInfo.knownArtsSet, "1-8");
+});
diff --git a/comm/mailnews/news/test/unit/xpcshell.ini b/comm/mailnews/news/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..e997e00987
--- /dev/null
+++ b/comm/mailnews/news/test/unit/xpcshell.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+head = head_server_setup.js
+tail =
+support-files = postings/*
+
+[test_biff.js]
+[test_bug37465.js]
+[test_bug170727.js]
+run-sequentially = Restarts server twice--may work but dangerous
+[test_bug403242.js]
+[test_bug540288.js]
+[test_bug695309.js]
+[test_cancelPasswordDialog.js]
+[test_filter.js]
+[test_getNewsMessage.js]
+[test_internalUris.js]
+[test_newsAutocomplete.js]
+[test_NntpChannel.js]
+[test_nntpContentLength.js]
+# The server doesn't support returning sizes! (bug 782629)
+skip-if = true
+[test_nntpGroupPassword.js]
+[test_nntpPassword.js]
+[test_nntpPassword2.js]
+skip-if = true # realhostname and realuserName don't exist anymore
+[test_nntpPassword3.js]
+[test_nntpPasswordFailure.js]
+[test_nntpPost.js]
+[test_nntpProtocols.js]
+[test_nntpProxy.js]
+[test_nntpUrl.js]
+[test_server.js]
+run-sequentially = Uses fixed NNTP_PORT
+[test_uriParser.js]
+[test_xover.js]