diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /comm/mailnews/news | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'comm/mailnews/news')
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] |