RobinvdVleuten/node-nntp

View on GitHub
lib/nntp.js

Summary

Maintainability
D
2 days
Test Coverage
'use strict';

var domain = require('domain'),
    xtend = require('xtend'),
    Response = require('./response'),
    ResponseStream = require('./streams/response'),
    MultilineStream = require('./streams/multiline'),
    CompressedStream = require('./streams/compressed');

var defaults = {
  host: 'localhost',
  port: 119,
  secure: false,
};

function NNTP (options) {
  this.options = xtend(defaults, options || {});

  var socket, self = this;

  this.connect = function (callback) {
    socket = require(self.options.secure ? 'tls' : 'net').connect(self.options.port, self.options.host);

    getResponse(false, false, function (error, response) {
      self.isConnected = true;
      callback(error, response);
    });
  };

  /**
   * Shorthand function for connect and authenticate in one call.
   */
  this.connectAndAuthenticate = function (callback) {
    this.connect(function (error, response) {
      if (error) {
        return callback(error);
      }

      if (undefined === self.options.username) {
        return callback(null, response);
      }

      self.authenticate(callback);
    });
  };

  this.disconnect = function (callback) {
    socket.once('end', function () {
      self.isConnected = false;
      callback();
    });

    socket.end();
  };

  this.authenticate = function (callback) {
    this.authInfo('USER', this.options.username, function (error, response) {
      if (error) {
        return callback(error);
      }

      if (response.status === 381) {
        if (undefined === self.options.password) {
          return callback(new Error('Password is required'));
        }

        return self.authInfo('PASS', self.options.password, callback);
      }

      callback(null, response);
    });
  };

  this.authInfo = function (type, value, callback) {
    getResponse(false, false, callback);
    socket.write('AUTHINFO ' + type + ' ' + value + '\r\n');
  };

  this.article = function (messageId, callback) {
    getResponse(true, false, function (error, response) {
      if (error) {
        return callback(error);
      }

      if (Response.NO_SUCH_ARTICLE === response.status) {
        return callback(new Error('No such article'));
      }

      if (Response.ARTICLE_RETRIEVED !== response.status) {
        return callback(new Error('Unexpected response received: ' + JSON.stringify(response)));
      }

      var inBody = false;
      var body = [];
      var headers = [];
      response.lines.forEach(function (line) {
        if (line.trim().length === 0 && inBody === false) {
          inBody = true;
        } else {
          if (inBody){
            body.push(line);
          } else {
            headers.push(line);
          }
        }
      });

      callback(null, { headers: headers, body: body });
    });

    socket.write('ARTICLE ' + messageId + '\r\n');
  };

  this.group = function (group, callback) {
    getResponse(false, false, function (error, response) {
      if (error) {
        return callback(error);
      }

      if (Response.NO_SUCH_GROUP === response.status) {
        return callback(new Error('No such group'));
      }

      if (Response.GROUP_SELECTED !== response.status) {
        return callback(new Error('Unexpected response received: ' + JSON.stringify(response)));
      }

      var messageParts = response.message.split(' ');

      callback(null, {
        name:  messageParts[3],
        count: parseInt(messageParts[0], 10),
        first: parseInt(messageParts[1], 10),
        last:  parseInt(messageParts[2], 10),
      });
    });

    socket.write('GROUP ' + group + '\r\n');
  };

  this.overviewFormat = function (callback) {
    getResponse(true, false, function (error, response) {
      if (error) {
        return callback(error);
      }

      var format = [];
      response.lines.forEach(function (line) {
        if (line.substr(-5, 5).toLowerCase() === ':full') {
          format[line.slice(0, -5).toLowerCase()] = true;
        }
        else {
          format[line.slice(0, -1).toLowerCase()] = false;
        }
      });

      callback(null, format);
    });

    socket.write('LIST OVERVIEW.FMT\r\n');
  };

  this.xover = function (range, format, callback) {
    format = xtend({number: false}, format);

    getResponse(true, false, function (error, response) {
      if (error) {
        return callback(error);
      }

      callback(null, parseOverview(response.lines, format));
    });

    socket.write('XOVER ' + range + '\r\n');
  };

  this.xzver = function (range, format, callback) {
    format = xtend({number: false}, format);

    getResponse(true, true, function (error, response) {
      if (error) {
        return callback(error);
      }

      callback(null, parseOverview(response.lines, format));
    });

    socket.write('XZVER ' + range + '\r\n');
  };

  function getResponse(multiline, compressed, callback) {
    var d = domain.create();

    function done(error, response) {
      socket.unpipe();

      socket.removeAllListeners('data');
      socket.removeAllListeners('error');

      d.exit();

      callback(error, response);
    }

    d.on('error', function (error) {
      done(error, null);
    });

    d.run(function () {
      socket.on('error', function (error) {
        done(error, null);
      });

      var pipeable = socket;

      if (compressed) {
        pipeable = pipeable.pipe(new CompressedStream());
      }

      if (multiline) {
        pipeable = pipeable.pipe(new MultilineStream());
      }

      pipeable = pipeable.pipe(new ResponseStream(multiline));

      var response;
      pipeable.on('data', function (data) {
        response = data;
      });

      pipeable.on('end', function () {
        done(null, response);
      });
    });
  }

  function parseOverview(overview, format) {
    var messages = [];

    overview.forEach(function (line) {
      var messageParts = line.toString().split('\t'),
          message = {},
          messagePart;

      for (var field in format) {
        if (format.hasOwnProperty(field)) {
          messagePart = messageParts.shift();
          message[field] = format[field] ? messagePart.substring(messagePart.indexOf(':') + 1).trim() : messagePart;
        }
      }

      messages.push(message);
    });

    return messages;
  }
}

module.exports = NNTP;