Bug 1719414 - Init NntpClient.jsm to interact with NNTP server. r=mkmelin

Depends on D119594.

Differential Revision: https://phabricator.services.mozilla.com/D119595

--HG--
extra : amend_source : ef03e9baa305539684da74e34296e64ad9f39bf1
This commit is contained in:
Ping Chen 2021-07-14 13:54:10 +03:00
Родитель 03830341a1
Коммит c1c11fbd23
3 изменённых файлов: 225 добавлений и 1 удалений

Просмотреть файл

@ -0,0 +1,208 @@
/* 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 { CommonUtils } = ChromeUtils.import("resource://services-common/utils.js");
/**
* 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, uri) {
this.onOpen = () => {};
this.onError = () => {};
this._server = server;
let matches = /.+:\/\/(.+)\/(.+)/.exec(uri);
this._host = matches[1];
this._group = matches[2];
this._logger = console.createInstance({
prefix: "mailnews.nntp",
maxLogLevel: "Warn",
maxLogLevelPref: "mailnews.nntp.loglevel",
});
}
/**
* Initiate a connection to the server
*/
connect() {
let port = this._server.port;
let useSecureTransport = this._server.isSecure;
this._logger.debug(
`Connecting to ${useSecureTransport ? "snews" : "news"}://${
this._host
}:${port}`
);
this._socket = new TCPSocket(this._host, port, {
binaryType: "arraybuffer",
useSecureTransport,
});
this._socket.onopen = this._onOpen;
this._socket.onerror = this._onError;
}
/**
* The open event handler.
*/
_onOpen = () => {
this._logger.debug("Connected");
this._socket.ondata = this._onData;
this._socket.onclose = this._onClose;
this.onOpen();
};
/**
* 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);
this._nextAction?.(res);
};
/**
* Parse the server response.
* @param {string} str - Response received from the server.
* @returns {NntpResponse}
*/
_parse(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 string to the socket.
* @param {string} str - The string to send.
*/
_sendString(str) {
this._logger.debug(`C: ${str}`);
this._socket.send(CommonUtils.byteStringToArrayBuffer(str + "\r\n").buffer);
}
/**
* Get new articles.
* @param {boolean} getOld - Get old articles as well.
* @param {nsIUrlListener} urlListener - Callback for the request.
* @param {nsIMsgWindow} msgWindow - The associated msg window.
*/
getNewNews(getOld, urlListener, msgWindow) {
this._nextAction = this._actionModeReader;
}
/**
* Send `MODE READER` request to the server.
*/
_actionModeReader() {
this._sendString("MODE READER");
this._nextAction = this._actionGroup;
}
/**
* Send `GROUP` request to the server.
*/
_actionGroup() {
this._sendString(`GROUP ${this._group}`);
this._nextAction = this._actionXOver;
}
/**
* Send `XOVER` request to the server.
*/
_actionXOver(res) {
let [, , high] = res.statusText.split(" ");
// TODO figure out the range, which depends on the folder state and user
// settings.
this._sendString(`XOVER ${high - 10}-${high}`);
this._nextAction = this._actionXOverResponse;
}
/**
* A transient action to consume the status line of XOVER response.
* @param {NntpResponse} res - XOVER response received from the server.
*/
_actionXOverResponse(res) {
this._actionReadXOver(res);
this._nextAction = this._actionReadXOver;
}
/**
* Handle XOVER response.
* @param {NntpResponse} res - XOVER response received from the server.
*/
_actionReadXOver({ data }) {
// XOVER response can span multiple ondata events, an event's leftover data
// is saved in this._xoverData.
if (this._xoverData) {
data = this._xoverData + data;
this._xoverData = null;
}
while (data) {
let index = data.indexOf("\r\n");
if (index == -1) {
// Not enough data, save it for the next round.
this._xoverData = data;
break;
}
if (data == ".\r\n") {
// Finished reading XOVER response.
this._nextAction = null;
break;
}
let parts = data.slice(0, index).split("\t");
if (parts.length >= 8) {
let [
articleNumber,
subject,
from,
date,
messageId,
references,
bytes,
lines,
...extra
] = parts;
this._logger.debug({
articleNumber,
subject,
from,
date,
messageId,
references,
bytes,
lines,
extra,
});
}
data = data.slice(index + 2);
}
}
}

Просмотреть файл

@ -4,6 +4,14 @@
const EXPORTED_SYMBOLS = ["NntpService"];
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
NntpClient: "resource:///modules/NntpClient.jsm",
});
/**
* Set the mailnews.nntp.jsmodule pref to true to use this module.
*
@ -12,7 +20,14 @@ const EXPORTED_SYMBOLS = ["NntpService"];
class NntpService {
QueryInterface = ChromeUtils.generateQI(["nsINntpService"]);
getNewNews(server, uri, getOld, urlListener, msgWindow) {}
getNewNews(server, uri, getOld, urlListener, msgWindow) {
let client = new NntpClient(server, uri);
client.connect();
client.onOpen = () => {
client.getNewNews(getOld, urlListener, msgWindow);
};
}
}
NntpService.prototype.classID = Components.ID(

Просмотреть файл

@ -20,6 +20,7 @@ SOURCES += [
EXTRA_JS_MODULES += [
"NewsAutoCompleteSearch.jsm",
"NntpClient.jsm",
"NntpModuleLoader.jsm",
"NntpService.jsm",
]