# = Net::NNTP class # Author:: Anton Bangratz require 'socket' require 'thread' require 'timeout' # :nodoc: require 'net/nntp_group' require 'net/nntp_article' require 'net/nntp/version.rb' require 'rubygems' require 'log4r' module Net class NNTP include Timeout # :nodoc: # Statuses of one-line responses ONELINE_STATUSES = %w( 111 200 201 223 281 381 411 412 422 480 500 501 ).freeze # Statuses of multiline responses MULTILINE_STATUSES = %w( 100 215 220 221 222 224 ).freeze # Error to indicate that NNTP Command failed gracefully class CommandFailedError < StandardError end # Error to indicate that a Protocol Error occured during NNTP command execution class ProtocolError < StandardError end attr_reader :socket, :grouplist, :overview_format def self.logger=(logger) @@logger = logger end def self.logger @@logger end def logger @@logger end def debug(message) @@logger.debug(message) end # initializes NNTP class with host and port def initialize(host, port = 119) @host = host @port = port @socket_class = TCPSocket @group = nil @@logger ||= Log4r::Logger['net::nntp'] || Log4r::Logger.new('net::nntp') end # Actually connects to NNTP host and port given in new() def connect @socket = @socket_class.new(@host, @port) @welcome = read_response() debug "Welcome: #{@welcome[0]} " @welcome end # Closes connection. If not reconnected, subsequent calls of commands raise exceptions def close debug 'closing connection per request' @socket.close unless socket.closed? end def has_xover? !help.select {|i| i =~ /\bxover\b/i}.empty? end def has_over? !help.select {|i| i =~ /\bover\b/i}.empty? end # Uses authinfo commands to authenticate. Timeout for first command is set to 10 seconds. # # Returns true on success, false on failure. def authenticate(user, pass) cmd = "authinfo user #{user}" debug "Authenticating: Sending #{cmd}" send_cmd cmd response_array = read_response() response = response_array[0] debug "Authenticating: Response #{response}" if response[0..2] == '381' then cmd = "authinfo pass #{pass}" debug "Authenticating: Sending #{cmd}" send_cmd cmd response_array = read_response() response = response_array[0] debug "Authenticating: Response #{response}" end return response && response[0..2] == '281' end # Issues 'GROUP' command to NNTP Server and creates new active group from returning data. # # Throws CommandFailedError # def group(group) send_cmd "group #{group}" response = read_response(true) responsecode, cnt, first, last, name = response[0].split if responsecode == '211' @group = Group.new(name) @group.article_info = [cnt, first, last] @group else raise CommandFailedError, response[0] end end # Issues 'HELP' command to NNTP Server, and returns raw response. def help send_cmd("help") read_response() end # Issues 'XHDR' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group # otherwise. # # TODO:: Implement XHDR header def xhdr(groupname, header, rest) group(groupname) unless (@group && @group.name == groupname) cmd = "xhdr #{header}" suffix = numbers_or_id(rest) send_cmd([cmd, suffix].join(' ')) read_response() end # Issues 'XOVER' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group # otherwise. If no group is selected nor given, raises error. # Parameter 'rest' can be in the form of :from => number, :to => number or :messageid => 'messageid', # if not set, a 'next' command is issued to the server prior to the xover command # def xover(groupname=nil, rest=nil) raise CommandFailedError, 'No group selected' unless @group || groupname debug "Selected Group: #{@group.name}" group(groupname) unless (@group || @group.name == groupname) self.next unless rest prefix = "xover" suffix = numbers_or_id(rest) cmd = [prefix, suffix].join ' ' send_cmd(cmd) response = nil timeout(10) do response = read_response() end response end # Issues 'LISTGROUP' command to NNTP Server, and returns raw response. Checks if group has been selected, selects group # otherwise. # def listgroup(groupname) group(groupname) unless (@group && @group.name == groupname) send_cmd('listgroup') read_response() end # Issues 'LIST' command to NNTP Server. Depending on server capabilities and given keyword, can either set overview # format (if called with 'overview.fmt') or create a grouplist (see Attributes) # # Throws CommandFailedError def list(keyword=nil, pattern=nil) cmd = ['list', keyword, pattern].join ' ' send_cmd(cmd) list = read_response() responsecode = list[0][0..2] case responsecode when '215' case keyword when /overview.fmt/ @overview_format_raw = list @overview_format = Net::NNTP.parse_overview_format list.join else create_grouplist(list) list end when '501', '503', '500' raise CommandFailedError, list end end # prepares overview format as read from server, used by Net::NNTP::Article and list() def self.parse_overview_format(format) overview_format = %w{id} format.split(/\r?\n/).each do |line| next if line[0] == ?2 || line[0] == ?. ident = line.scan(/\w/).join.downcase unless ident[0..3] == 'xref' overview_format << ident else overview_format << 'xref' end end overview_format end # TODO: complete implementation def stat end # Issues 'HEAD' command to NNTP server, returning raw response # # Throws CommandFailedError def head(args=nil) suffix = numbers_or_id(args) cmd = 'head' cmd = ['head', suffix].join " " if suffix send_cmd(cmd) response = read_response() case response[0][0..2] when '221' return response else raise CommandFailedError, response end end # Issues 'BODY' command to NNTP server, returning raw response. # options:: messageid|id # # Throws CommandFailedError def body(args=nil) suffix = args cmd = 'body' cmd = ['body', suffix].join " " if suffix send_cmd(cmd) response = read_response() case response[0][0..2] when '222' return response else raise CommandFailedError, response end end # Issues 'ARTICLE' command to NNTP server, returning raw response. # options:: messageid|id # # Throws CommandFailedError def article(args=nil) suffix = args cmd = 'article' cmd = ['article', suffix].join " " if suffix send_cmd(cmd) response = read_response() case response[0][0..2] when '220' return response else raise CommandFailedError, response end end def last_or_next(cmd) raise ProtocolError, "No group selected" unless @group send_cmd(cmd) response = read_response()[0] code = response[0..2] article = @group.articles.create case code when '223' code, id, msgid, what = response.split article.id = id article.messageid = msgid else raise CommandFailedError, response end response end # Issues the LAST command to the NNTP server, returning the raw response def last last_or_next("last") end def next last_or_next("next") end def create_grouplist(response) @grouplist = {} response.each_with_index do |line, idx| next if idx == 0 break if line =~ /^\.\r\n$/ groupinfo = line.split group = Group.new groupinfo.shift group.article_info = groupinfo @grouplist[group.name] = group end @grouplist end def send_cmd(cmd) debug "Sending: '#{cmd}'" @socket.write(cmd+"\r\n") end def read_response(force_oneline=false) response = '' str = '' ra = [] loop do str = @socket.readline ra << str break if force_oneline || (str == ".\r\n" || !str || ONELINE_STATUSES.include?(str[0..2]) ) end debug "Response: '#{ra}'" ra end def numbers_or_id(hash) return nil unless hash suffix = '' from = hash[:from] to = hash[:to] msgid = hash[:message_id] if from suffix = "#{from}-" suffix += "#{to}" if to elsif msgid suffix = "#{msgid}" end suffix end private :read_response, :numbers_or_id, :send_cmd, :last_or_next, :create_grouplist end end # vim:sts=2:ts=2:sw=2:sta:noet