From 734c1e93972e196f6791ad0cb73954c3bc3798e9 Mon Sep 17 00:00:00 2001 From: s2 Date: Fri, 10 Nov 2017 11:20:35 +0100 Subject: [PATCH] add lib --- LICENSE | 19 ++ README.md | 195 +++++++++++++ lib/nntp.js | 774 +++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 66 +++++ 4 files changed, 1054 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 lib/nntp.js create mode 100644 package.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..290762e --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright Brian White. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6960239 --- /dev/null +++ b/README.md @@ -0,0 +1,195 @@ +Description +=========== + +node-nntp is an NNTP (usenet/newsgroups) client module for [node.js](http://nodejs.org/). + + +Requirements +============ + +* [node.js](http://nodejs.org/) -- v0.8.0 or newer + + +Examples +======== + +* Get the headers and body of the first message in 'misc.test' + +```javascript + var NNTP = require('nntp'), + inspect = require('util').inspect; + + var c = new NNTP(); + c.on('ready', function() { + c.group('misc.test', function(err, count, low, high) { + if (err) throw err; + }); + c.article(function(err, n, id, headers, body) { + if (err) throw err; + console.log('Article #' + n); + console.log('Article ID: ' + id); + console.log('Article headers: ' + inspect(headers)); + console.log('Article body: ' + inspect(body.toString())); + }); + }); + c.on('error', function(err) { + console.log('Error: ' + err); + }); + c.on('close', function(had_err) { + console.log('Connection closed'); + }); + c.connect({ + host: 'example.org', + user: 'foo', + password: 'bar' + }); +``` + +* Get a list of all newsgroups beginning with 'alt.binaries.' + +```javascript + var NNTP = require('nntp'), + inspect = require('util').inspect; + + var c = new NNTP(); + c.on('ready', function() { + c.groups('alt.binaries.*', function(err, list) { + if (err) throw err; + console.dir(list); + }); + }); + c.on('error', function(err) { + console.log('Error: ' + err); + }); + c.on('close', function(had_err) { + console.log('Connection closed'); + }); + c.connect({ + host: 'example.org', + user: 'foo', + password: 'bar' + }); +``` + +* Post a message to alt.test: + +```javascript + var NNTP = require('nntp'), + inspect = require('util').inspect; + + var c = new NNTP(); + c.on('ready', function() { + var msg = { + from: { name: 'Node User', email: 'user@example.com' }, + groups: 'alt.test', + subject: 'Just testing, do not mind me', + body: 'node.js rules!' + }; + c.post(msg, function(err) { + if (err) throw err; + }); + }); + c.on('error', function(err) { + console.log('Error: ' + err); + }); + c.on('close', function(had_err) { + console.log('Connection closed'); + }); + c.connect({ + host: 'example.org', + user: 'foo', + password: 'bar' + }); +``` + + +API +=== + +Events +------ + +* **ready**() - Emitted when connection and authentication were successful. + +* **close**(< _boolean_ >hadErr) - Emitted when the connection has fully closed. + +* **end**() - Emitted when the connection has ended. + +* **error**(< _Error_ >err) - Emitted when an error occurs. In case of protocol-level errors, `err` contains a 'code' property that references the related NNTP response code. + + +Methods +------- + +* **(constructor)**() - Creates and returns a new NNTP client instance. + +* **connect**(< _object_ >config) - _(void)_ - Attempts to connect to a server. Valid `config` properties are: + + * **host** - < _string_ > - Hostname or IP address of the server. **Default:** 'localhost' + + * **port** - < _integer_ > - Port number of the server. **Default:** 119 + + * **secure** - < _boolean_ > - Will this be a secure (TLS) connection? **Default:** false + + * **user** - < _string_ > - Username for authentication. **Default:** (none) + + * **password** - < _string_ > - Password for password-based user authentication. **Default:** (none) + + * **connTimeout** - < _integer_ > - Connection timeout in milliseconds. **Default:** 60000 + +* **end**() - _(void)_ - Ends the connection with the server. + +### Mandatory/Common protocol commands + +* **dateTime**(< _function_ >callback) - _(void)_ - Retrieves the server's UTC date and time in YYYYMMDDHHMMSS format. `callback` has 2 parameters: < _Error_ >err, < _string_ >datetime. + +* **stat**([< _string_ >which, ]< _function_ >callback) - _(void)_ - Retrieves the article number and message ID for the current article if `which` is not given or for the article whose number or message ID is `what`. `callback` has 3 parameters: < _Error_ >err, < _integer_ >articleNum, < _string_ >msgID. + +* **group**(< _string_ >group, < _function_ >callback) - _(void)_ - Sets the current newsgroup to `group`. `callback` has 4 parameters: < _Error_ >err, < _integer_ >estimatedArticleCount, < _integer_ >firstArticleNum, < _integer_ >lastArticleNum. + +* **next**(< _function_ >callback) - _(void)_ - Attempts to move to the next article in the current newsgroup. `callback` has 3 parameters: < _Error_ >err, < _integer_ >articleNum, < _string_ >msgID. + +* **prev**(< _function_ >callback) - _(void)_ - Attempts to move to the previous article in the current newsgroup. `callback` has 3 parameters: < _Error_ >err, < _integer_ >articleNum, < _string_ >msgID. + +* **headers**([< _string_ >which, ]< _function_ >callback) - _(void)_ - Retrieves the headers of the current article if `which` is not given or for the article whose number or message ID is `what`. `callback` has 4 parameters: < _Error_ >err, < _integer_ >articleNum, < _string_ >msgID, < _object_ >headers. `headers` values are always arrays (of strings). + +* **body**([< _string_ >which, ]< _function_ >callback) - _(void)_ - Retrieves the body of the current article if `which` is not given or for the article whose number or message ID is `what`. `callback` has 4 parameters: < _Error_ >err, < _integer_ >articleNum, < _string_ >msgID, < _Buffer_ >body. + +* **article**([< _string_ >which, ]< _function_ >callback) - _(void)_ - Retrieves the headers and body of the current article if `which` is not given or for the article whose number or message ID is `what`. `callback` has 5 parameters: < _Error_ >err, < _integer_ >articleNum, < _string_ >msgID, < _object_ >headers, < _Buffer_ >body. `headers` values are always arrays (of strings). + +### Extended protocol commands -- these _may not_ be implemented or enabled on all servers + +**\* Note: A `filter` parameter is a single (or Array of) wildcard-capable newsgroup name filter string(s) ([information on the wildcard format](http://tools.ietf.org/html/rfc3977#section-4.2) and [wildcard examples](http://tools.ietf.org/html/rfc3977#section-4.4)).** + +* **newNews**(< _mixed_ >filter, < _mixed_ >date, [< _string_ >time, ] < _function_ >callback) - _(void)_ - Retrieves the message ID of articles in group(s) matching `filter` on or after a date. This date can be specified with `date` being a Date object, or `date` being a 'YYYYMMDD'-formatted string and `time` being a 'HHMMSS'-formatted string (defaults to midnight) in UTC/GMT. `callback` has 2 parameters: < _Error_ >err, < _array_ >msgIDs. + +* **groups**(< _mixed_ >filter, < _function_ >callback) - _(void)_ - Retrieves a list of groups matching `filter`. `callback` has 2 parameters: < _Error_ >err, < _array_ >groupsInfo. `groupsInfo` is an array of `[groupName, firstArticleNum, lastArticleNum, status]` rows. Valid statuses are documented [here](http://tools.ietf.org/html/rfc6048#section-3.1). + +* **groupsDesc**(< _mixed_ >filter, < _function_ >callback) - _(void)_ - Retrieves a list of group descriptions matching `filter`. `callback` has 2 parameters: < _Error_ >err, < _array_ >groups. `groups` is an array of `[groupName, groupDesc]` rows. + +* **post**(< _object_ >msg, < _function_ >callback) - _(void)_ - Posts the given `msg` (as defined below) to the current newsgroup. `callback` has 1 parameter: < _Error_ >err. + + * **from** - < _object_ > - Who the message is from. + + * **name** - < _string_ > - Example: 'User'. + + * **email** - < _string_ > - Example: 'user@example.com'. + + * **groups** - < _mixed_ > - A single newsgroup or array of newsgroups to post this message to. + + * **subject** - < _string_ > - The subject line. + + * **body** - < _mixed_ > - The body content -- a string or a Buffer (will be converted to UTF-8 string). + + + +* For methods that return first and last article numbers, the RFC says a group is empty if one of the following is true: + + * The last article number will be one less than the first article number, and + the estimated article count will be zero. This is the only time that the + last article number can be less than the first article number. + + * First and last article numbers (and estimated article count where applicable) are all 0. + + * The last article number is equal to the first article number. The + estimated article count might be zero or non-zero. diff --git a/lib/nntp.js b/lib/nntp.js new file mode 100644 index 0000000..052e5cd --- /dev/null +++ b/lib/nntp.js @@ -0,0 +1,774 @@ +/* + * TODO: - keepalive timer (< 3 min intervals) + */ + +var tls = require('tls'), + Socket = require('net').Socket, + EventEmitter = require('events').EventEmitter, + Stream = require('stream'), + util = require('util'), + SBMH = require('streamsearch'), + inherits = util.inherits, + inspect = util.inspect, + RE_CRLF = /\r\n/g, + RE_LIST_ACTIVE = /^(.+)\s+(\d+)\s+(\d+)\s+(.+)$/, + RE_GROUP_DESC = /^([^\s]+)\s+(.+)$/, + RE_STAT = /^(\d+)\s+(.+)$/, + RE_GROUP = /^(\d+)\s+(\d+)\s+(\d+)\s/, + RE_HDR = /^([^:]+):[ \t]?(.+)?$/, + RES_CODE_ML = [100, 101, 215, 220, 221, 222, 224, 225, 230, 231], + RES_CODE_ARGS = [111, 211, 220, 221, 222, 223, 401], + B_CRLF = new Buffer([13, 10]), + B_ML_TERM = new Buffer([13, 10, 46, 13, 10]), + TYPE = { + CONNECTION: 0, + GROUP: 1, + ARTICLE: 2, + DISTRIBUTION: 3, + POST: 4, + AUTH: 8, + PRIVATE: 9 + }, + RETVAL = { + INFO: 1, + OK: 2, + WAITING: 3, + ERR_NONSYN: 4, + ERR_OTHER: 5 + }, + ERRORS = { + 400: 'Service not available or no longer available', + 401: 'Server is in the wrong mode', + 403: 'Internal fault', + 411: 'No such newsgroup', + 412: 'No newsgroup selected', + 420: 'Current article number is invalid', + 421: 'No next article in this group', + 422: 'No previous article in this group', + 423: 'No article with that number or in that range', + 430: 'No article with that message-id', + 435: 'Article not wanted', + 436: 'Transfer not possible or failed; try again later', + 437: 'Transfer rejected; do not retry', + 440: 'Posting not permitted', + 441: 'Posting failed', + 480: 'Authentication required', + 481: 'Authentication failed/rejected', // RFC 4643 + 483: 'Command unavailable until suitable privacy has been arranged', + 500: 'Unknown command', + 501: 'Syntax error', + 502: 'Service/command not permitted', + 503: 'Feature not supported', + 504: 'Invalid base64-encoded argument' + }; + +function NNTP() { + this._sbmhML = new SBMH(B_ML_TERM); + this._sbmhML.maxMatches = 1; + this._sbmhCRLF = new SBMH(B_CRLF); + this._sbmhCRLF.maxMatches = 1; + + this._socket = undefined; + this._state = undefined; + this._caps = undefined; + this._queue = undefined; + this._curReq = undefined; + this._stream = undefined; + this._buffer = ''; + this._bufferEnc = undefined; + this._debug = false; + this.options = { + host: undefined, + port: undefined, + secure: undefined, + user: undefined, + password: undefined, + connTimeout: undefined + }; + this.connected = false; +}; +inherits(NNTP, EventEmitter); + +NNTP.prototype.reset = function() { + this._sbmhML.reset(); + this._sbmhCRLF.reset(); + this._socket = undefined; + this._state = undefined; + this._caps = undefined; + this._queue = undefined; + this._curReq = undefined; + this._stream = undefined; + this._buffer = ''; + this._bufferEnc = undefined; + this.connected = false; +}; + +function readCode(chunk, code) { + var ret = code, more, + left = chunk.length - chunk.p; + if (left >= 3 && code === undefined) { + ret = parseInt(chunk.toString('ascii', chunk.p, chunk.p + 3), 10); + chunk.p += 3; + } else { + if (code === undefined) { + ret = chunk.toString('ascii', chunk.p); + chunk.p = chunk.length; + } else { + more = 3 - ret.length; + if (left >= more) { + ret += chunk.toString('ascii', chunk.p, chunk.p + more); + chunk.p += more; + } else { + ret += chunk.toString('ascii', chunk.p); + chunk.p = chunk.length; + } + + if (ret.length === 3) + ret = parseInt(ret, 10); + } + } + return ret; +} + +NNTP.prototype.connect = function(options) { + var self = this; + + this.options.host = options.host || 'localhost'; + this.options.port = options.port || 119; + this.options.secure = options.secure || false; + this.options.user = options.user || ''; + this.options.password = options.password || ''; + this.options.connTimeout = options.connTimeout || 60000; // in ms + var debug; + if (typeof options.debug === 'function') + debug = this._debug = options.debug; + else + debug = this._debug = false; + + this.reset(); + this._caps = {}; + this._queue = []; + this._state = 'connecting'; + this.connected = false; + + var isML = false, code, type, retval, isErr, sbmh; + + var connTimeout = setTimeout(function() { + self._socket.destroy(); + self._socket = undefined; + self.emit('error', new Error('Connection timeout')); + }, this.options.connTimeout); + + var socket = this._socket = new Socket(); + this._socket.setTimeout(0); + if (this.options.secure) + socket = tls.connect({ socket: this._socket }, onconnect); + else + this._socket.once('connect', onconnect); + function onconnect() { + self._socket = socket; // re-assign for secure connections + self._state = 'connected'; + self.connected = true; + clearTimeout(connTimeout); + + var cmd, params; + self._curReq = { + cmd: '', + cb: function reentry(err, code) { + // many? servers don't support the *mandatory* CAPABILITIES command :-( + if (err && cmd !== 'CAPABILITIES') { + self.emit('error', err); + return self._socket.end(); + } + // TODO: try sending CAPABILITIES first thing + if (!cmd) { + if (self.options.user) { + cmd = 'AUTHINFO'; + params = 'USER ' + self.options.user; + } else { + cmd = 'CAPABILITIES'; + params = undefined; + } + } else if (cmd === 'AUTHINFO') { + if (params.substr(0, 4) === 'USER') { + if (code === 381) { // password required + if (!self.options.password) { + self.emit('error', makeError('Password required', code)); + return self._socket.end(); + } + params = 'PASS ' + self.options.password; + } + } else if (params.substr(0, 4) === 'PASS') { + cmd = 'CAPABILITIES'; + params = undefined; + } + } else if (cmd === 'CAPABILITIES') { + //self._parseCaps(); + return self.emit('ready'); + } + self._send(cmd, params, reentry); + } + }; + } + this._socket.once('end', function() { + clearTimeout(connTimeout); + self.connected = false; + self._state = 'disconnected'; + self.emit('end'); + }); + this._socket.once('close', function(had_err) { + clearTimeout(connTimeout); + self.connected = false; + self._state = 'disconnected'; + self.emit('close', had_err); + }); + this._socket.once('error', function(err) { + self.emit('error', err); + }); + socket.on('data', function(chunk) { + chunk.p = 0; + var chlen = chunk.length, r = 0; + debug&&debug('< ' + inspect(chunk.toString('binary'))); + while (r < chlen) { + if (typeof code !== 'number') { + code = readCode(chunk, code); + if (typeof code !== 'number') + return; + if (isNaN(code)) { + self.reset(); + self.emit('error', new Error('Parse error')); + return socket.end(); + } + retval = code / 100 >> 0; + type = (code % 100) / 10 >> 0; + isErr = (retval === RETVAL.ERR_NONSYN || retval === RETVAL.ERR_OTHER); + if (code === 211) + isML = (self._curReq.cmd !== 'GROUP'); + else + isML = (RES_CODE_ML.indexOf(code) > -1); + sbmh = (isML ? self._sbmhML : self._sbmhCRLF); + sbmh.reset(); + r = chunk.p; + } else { + r = sbmh.push(chunk, r); + + if (sbmh.matches === 1) { + if (self._stream) { + if (isErr) + self._stream.emit('error', makeError(ERRORS[code], code)); + else + self._stream.emit('end'); + self._stream.emit('close', isErr); + } else if (isErr) + self._curReq.cb(makeError(ERRORS[code], code)); + else { + self._curReq.cb(undefined, code, retval, type); + self._buffer = ''; + } + code = undefined; + self._curReq = undefined; + self._send(); + } + } + } + }); + + function responseHandler(isMatch, chunk, start, end) { + if (isErr || !chunk) + return; + if (self._stream === undefined) + self._buffer += chunk.toString(self._bufferEnc || 'utf8', start, end); + else + self._stream.emit('data', chunk.slice(start, end)); + } + this._sbmhML.on('info', responseHandler); + this._sbmhCRLF.on('info', responseHandler); + + this._socket.connect(this.options.port, this.options.host); +}; + +NNTP.prototype.end = function() { + if (this._socket && this._socket.writable) + this._socket.end(); + + this._socket = undefined; +}; + + +// Mandatory/Common features +NNTP.prototype.dateTime = function(cb) { + var self = this; + this._send('DATE', undefined, function(err, code, r, type) { + if (err) + return cb(err); + // server UTC date/time in YYYYMMDDHHMMSS format + cb(undefined, self._buffer.trim()); + }); +}; + +NNTP.prototype.stat = function(id, cb) { + var self = this; + if (typeof id === 'function') { + cb = id; + id = undefined; + } + this._send('STAT', id, function(err, code, r, type) { + if (err) + return cb(err); + var m = RE_STAT.exec(self._buffer.trim()); + // article number, message id + cb(undefined, parseInt(m[1], 10), m[2]); + }); +}; + +NNTP.prototype.group = function(group, cb) { + var self = this; + this._send('GROUP', group, function(err, code, r, type) { + if (err) + return cb(err); + + // est. article count, low mark, high mark + var m = RE_GROUP.exec(self._buffer.trim()); + cb(undefined, parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)); + }); +}; + +NNTP.prototype.next = function(cb) { + var self = this; + this._send('NEXT', undefined, function(err, code, r, type) { + if (err) + return cb(err); + var m = RE_STAT.exec(self._buffer.trim()); + // article number, message id + cb(undefined, parseInt(m[1], 10), m[2]); + }); +}; + +NNTP.prototype.prev = function(cb) { + var self = this; + this._send('LAST', undefined, function(err, code, r, type) { + if (err) + return cb(err); + var m = RE_STAT.exec(self._buffer.trim()); + // article number, message id + cb(undefined, parseInt(m[1], 10), m[2]); + }); +}; + +NNTP.prototype.headers = function(what, cb) { + var self = this; + + if (typeof what === 'function') { + cb = what; + what = undefined; + } + + this._send('HEAD', what, function(err, code, r, type) { + if (err) + return cb(err); + + var list = self._buffer.split(RE_CRLF), + info = list.shift().trim(), + headers = {}, m; + + for (var i = 0, h, len = list.length; i < len; ++i) { + if (list[i].length === 0) + continue; + if (list[i][0] === '\t' || list[i][0] === ' ') { + // folded header content + // RFC2822 says to just remove the CRLF and not the whitespace following + // it, so we follow the RFC and include the leading whitespace ... + headers[h][headers[h].length - 1] += list[i]; + } else { + m = RE_HDR.exec(list[i]); + h = m[1].toLowerCase(); + if (m[2]) { + if (headers[h] === undefined) + headers[h] = [m[2]]; + else + headers[h].push(m[2]); + } else + headers[h] = ''; + } + } + + m = RE_STAT.exec(info); + // article number, message id, headers + cb(undefined, parseInt(m[1], 10), m[2], headers); + }); +}; + +NNTP.prototype.body = function(what, cb) { + var self = this; + + /*if (typeof what === 'function') { + // body(function() {}) + cb = what; + doBuffer = false; + what = undefined; + } else if (typeof doBuffer === 'function') { + cb = doBuffer; + if (typeof what === 'boolean') { + // body(true, function() {}); + doBuffer = what; + what = undefined; + } else { + // body(100, function() {}); + doBuffer = false; + } + }*/ + + if (typeof what === 'function') { + cb = what; + what = undefined; + } + + this._bufferEnc = 'binary'; + + this._send('BODY', what, function(err, code, r, type) { + self._bufferEnc = undefined; + if (err) + return cb(err); + + var idxCRLF = self._buffer.indexOf('\r\n'), m, body = ''; + + if (idxCRLF > -1) { + body = self._buffer.substring(idxCRLF + 2); + m = RE_STAT.exec(self._buffer.substring(0, idxCRLF).trim()); + } else { + // empty body + m = RE_STAT.exec(self._buffer.trim()); + } + + body = new Buffer(body, 'binary'); + + // article number, message id, string body + cb(undefined, parseInt(m[1], 10), m[2], body); + }); +}; + +NNTP.prototype.article = function(what, cb) { + var self = this; + + /*if (typeof what === 'function') { + // body(function() {}) + cb = what; + doBuffer = false; + what = undefined; + } else if (typeof doBuffer === 'function') { + cb = doBuffer; + if (typeof what === 'boolean') { + // body(true, function() {}); + doBuffer = what; + what = undefined; + } else { + // body(100, function() {}); + doBuffer = false; + } + }*/ + + if (typeof what === 'function') { + cb = what; + what = undefined; + } + + this._bufferEnc = 'binary'; + + this._send('ARTICLE', what, function(err, code, r, type) { + self._bufferEnc = undefined; + if (err) + return cb(err); + + var idxDCRLF = self._buffer.indexOf('\r\n\r\n'), m, list, + headers = {}, body, info, sheaders; + + sheaders = self._buffer.substring(0, idxDCRLF); + list = sheaders.split(RE_CRLF); + info = list.shift().trim(); + for (var i = 0, h, len = list.length; i < len; ++i) { + if (list[i].length === 0) + continue; + if (list[i][0] === '\t' || list[i][0] === ' ') { + // folded header content + // RFC2822 says to just remove the CRLF and not the whitespace following + // it, so we follow the RFC and include the leading whitespace ... + headers[h][headers[h].length - 1] += list[i]; + } else { + m = RE_HDR.exec(list[i]); + h = m[1].toLowerCase(); + if (m[2]) { + if (headers[h] === undefined) + headers[h] = [m[2]]; + else + headers[h].push(m[2]); + } else + headers[h] = ''; + } + } + + body = new Buffer(self._buffer.substring(idxDCRLF + 4), 'binary'); + + m = RE_STAT.exec(info); + + // article number, message id, headers, string body + if (m != null) { + cb(undefined, parseInt(m[1], 10), m[2], headers, body); + } else { + cb(undefined, undefined, undefined, undefined, undefined); + } + + }); +}; + + +// Extended features -- these may not be implemented or enabled on all servers +NNTP.prototype.newNews = function(search, date8, time6, cb) { + if (typeof search !== 'string') + throw new Error('Expected search string'); + /*if (typeof date8 === 'function' + || (typeof time6 === 'function' && !util.isDate(date8))) + throw new Error('Expected Date instance');*/ + + var self = this; + + if (typeof time6 === 'function') { + cb = time6; + if (util.isDate(date8)) { + time6 = padLeft(''+date8.getUTCHours(), 2, '0') + + padLeft(''+date8.getUTCMinutes(), 2, '0') + + padLeft(''+date8.getUTCSeconds(), 2, '0'); + date8 = ''+date8.getUTCFullYear() + + padLeft(''+date8.getUTCMonth(), 2, '0') + + padLeft(''+date8.getUTCDate(), 2, '0'); + } else + time6 = '000000'; + } + + if (Array.isArray(search)) + search = search.join(','); + search = (search ? search : ''); + + this._send('NEWNEWS', search + ' ' + date8 + ' ' + time6 + ' GMT', + function(err, code, r, type) { + if (err) + return cb(err); + var list = self._buffer.split(RE_CRLF); + list.shift(); // remove initial response line + cb(undefined, list); + } + ); +}; + +NNTP.prototype.groups = function(search, cb) { + var self = this; + if (typeof search === 'function') { + cb = search; + search = ''; + } + if (Array.isArray(search)) + search = search.join(','); + search = (search ? ' ' + search : ''); + this._send('LIST', 'ACTIVE' + search, function(err, code, r, type) { + if (err) + return cb(err); + var list = self._buffer.split(RE_CRLF); + list.shift(); // remove initial response line + for (var i = 0, m, len = list.length; i < len; ++i) { + m = RE_LIST_ACTIVE.exec(list[i]); + // short name, low mark, high mark, status + list[i] = [ m[1], parseInt(m[3], 10), parseInt(m[2], 10), m[4] ]; + } + cb(undefined, list); + }); +}; + +NNTP.prototype.groupsDesc = function(search, cb) { + var self = this; + if (typeof search === 'function') { + cb = search; + search = ''; + } else if (Array.isArray(search)) + search = search.join(','); + search = (search ? ' ' + search : ''); + + // According to the RFC: + // The description SHOULD be in UTF-8. However, servers often obtain the + // information from external sources. These sources may have used different + // encodings (ones that use octets in the range 128 to 255 in some other + // manner) and, in that case, the server MAY pass it on unchanged. + // Therefore, clients MUST be prepared to receive such descriptions. + this._bufferEnc = 'binary'; + + this._send('LIST', 'NEWSGROUPS' + search, function(err, code, r, type) { + self._bufferEnc = undefined; + if (err) + return cb(err); + var list = self._buffer.split(RE_CRLF); + list.shift(); // remove initial response line + for (var i = 0, m, len = list.length; i < len; ++i) { + m = RE_GROUP_DESC.exec(list[i]); + // short name, description + list[i] = [ m[1], m[2] ]; + } + cb(undefined, list); + }); +}; + +NNTP.prototype.post = function(msg, cb) { + var self = this, composing = true; + this._send('POST', undefined, function reentry(err, code, r, type) { + if (err || !composing) + return cb(err); + + var CRLF = '\r\n', + text; + + text = 'From: "'; + text += msg.from.name; + text += '" <'; + text += msg.from.email; + text += '>'; + text += CRLF; + + text += 'Newsgroups: '; + text += (Array.isArray(msg.groups) ? msg.groups.join(',') : msg.groups); + text += CRLF; + + text += 'Subject: '; + text += msg.subject; + text += CRLF; + + if (msg.references) { + text += 'References: '; + text += msg.references; + text += CRLF; + } + + if (msg.trace) { + text += 'fcku-trace: '; + text += msg.trace; + text += CRLF; + } + + text += 'Content-Type: text/plain; charset=utf-8'; + text += CRLF; + + text += CRLF; + + text += (Buffer.isBuffer(msg.body) + ? msg.body.toString('utf8') + : msg.body + ).replace(/\r\n/g, '\n') + .replace(/\r/g, '\n') + .replace(/\n/g, '\r\n') + .replace(/^\.([^.]*?)/gm, '..$1'); + + // _send always appends CRLF to the end of every cmd + text += '\r\n.'; + + composing = false; + self._send(text, undefined, reentry); + }); +}; + +// Private methods +NNTP.prototype._send = function(cmd, params, cb) { + if (cmd !== undefined) + this._queue.push({ cmd: cmd, params: params, cb: cb }); + if (!this._curReq && this._queue.length) { + this._curReq = this._queue.shift(); + this._socket.write(this._curReq.cmd); + if (this._curReq.params !== undefined) { + this._socket.write(' '); + this._socket.write(''+this._curReq.params); + } else if (this._debug) + this._debug('> ' + this._curReq.cmd); + + this._socket.write(B_CRLF); + } +}; + +NNTP.prototype._parseCaps = function() { + // TODO +}; + +module.exports = NNTP; + +function padLeft(str, size, pad) { + var ret = str; + if (str.length < size) { + for (var i=0,len=(size-str.length); i=0.8.0" + }, + "homepage": "https://github.com/mscdex/node-nntp#readme", + "keywords": [ + "nntp", + "client", + "usenet", + "newsreader", + "newsgroups", + "news" + ], + "licenses": [ + { + "type": "MIT", + "url": "http://github.com/mscdex/node-nntp/raw/master/LICENSE" + } + ], + "main": "./lib/nntp", + "name": "nntp", + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/mscdex/node-nntp.git" + }, + "version": "0.3.1" +}