This commit is contained in:
s2
2017-11-10 11:20:35 +01:00
commit 734c1e9397
4 changed files with 1054 additions and 0 deletions

19
LICENSE Normal file
View File

@@ -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.

195
README.md Normal file
View File

@@ -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.

774
lib/nntp.js Normal file
View File

@@ -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<len; ++i)
ret = pad + ret;
}
return ret;
}
function makeError(msg, code) {
var err = new Error(msg);
err.code = code;
return err;
}
function ReadStream(sock) {
var self = this;
this.readable = true;
this.paused = false;
this._buffer = [];
this._sock = sock;
this._decoder = undefined;
sock.once('end', function() {
self.readable = false;
});
sock.once('close', function(had_err) {
self.readable = false;
});
}
inherits(ReadStream, Stream);
ReadStream.prototype._emitData = function(d) {
if (d === undefined) {
if (this._buffer && this._buffer.length) {
this._emitData(this._buffer.shift());
return true;
} else
return false;
} else if (this.paused)
this._buffer.push(d);
else if (this._decoder) {
var string = this._decoder.write(d);
if (string.length)
this.emit('data', string);
} else
this.emit('data', d);
};
ReadStream.prototype.pause = function() {
this.paused = true;
this._sock.pause();
};
ReadStream.prototype.resume = function() {
if (this._buffer && this._buffer.length)
while (this._emitData());
this.paused = false;
this._sock.resume();
};
ReadStream.prototype.destroy = function(cb) {
this._decoder = undefined;
if (!this.readable) {
cb && process.nextTick(cb);
return;
}
this.readable = false;
this._buffer = [];
cb && cb();
this.emit('close');
};
ReadStream.prototype.setEncoding = function(encoding) {
var StringDecoder = require('string_decoder').StringDecoder; // lazy load
this._decoder = new StringDecoder(encoding);
};

66
package.json Normal file
View File

@@ -0,0 +1,66 @@
{
"_args": [
[
"nntp@0.3.1",
"/home/s2/tmp/n3wz/backend"
]
],
"_from": "nntp@0.3.1",
"_id": "nntp@0.3.1",
"_inBundle": false,
"_integrity": "sha1-88HxsV/vY+WTBAccs0HiMkQNfB0=",
"_location": "/nntp",
"_phantomChildren": {},
"_requested": {
"type": "version",
"registry": true,
"raw": "nntp@0.3.1",
"name": "nntp",
"escapedName": "nntp",
"rawSpec": "0.3.1",
"saveSpec": null,
"fetchSpec": "0.3.1"
},
"_requiredBy": [
"/"
],
"_resolved": "https://registry.npmjs.org/nntp/-/nntp-0.3.1.tgz",
"_spec": "0.3.1",
"_where": "/home/s2/tmp/n3wz/backend",
"author": {
"name": "Brian White",
"email": "mscdex@mscdex.net"
},
"bugs": {
"url": "https://github.com/mscdex/node-nntp/issues"
},
"dependencies": {
"streamsearch": "*"
},
"description": "An NNTP client module for node.js",
"engines": {
"node": ">=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"
}