# = 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 205 223 235 240 281 335 340 381 400 401 403 411 412 420 421 422 423 430 435 436 437 440 441 480 483 500 501 502 503 504 ).freeze # Statuses of multiline responses MULTILINE_STATUSES = %w( 100 101 215 220 221 222 224 225 230 231 ).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(header, groupname=nil, rest=nil) raise CommandFailedError, 'No group selected' unless @group || groupname if @group.nil? group(groupname) elsif groupname.nil? else @group.name != groupname group(groupname) end 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 if @group.nil? group(groupname) elsif groupname.nil? else @group.name != groupname group(groupname) end debug "Selected Group: #{@group.name}" 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=nil) raise CommandFailedError, 'No group selected' unless @group || groupname if @group.nil? group(groupname) elsif @group.name != groupname group(groupname) end debug "Selected Group: #{@group.name}" 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 =~ /^\.$/ 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 = [] linecnt = 0 loop do str = @socket.readline ra << str.chomp if str break if force_oneline || (str == "." || !str || (linecnt == 0 && ONELINE_STATUSES.include?(str[0..2])) ) linecnt += 1 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:et