# = Net::NNTP class # Author:: Anton Bangratz require 'socket' require 'thread' require 'timeout' # :nodoc: require 'net/nntp_group' require 'net/nntp_article' module Net class NNTP VERSION = "0.0.2" include Timeout # :nodoc: # 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 # initializes NNTP class with host and port def initialize(host, port = 119) @host = host @port = port @socket_class = TCPSocket @group = nil end # Actually connects to NNTP host and port given in new() def connect @socket = @socket_class.new(@host, @port) @welcome = @socket.recv 1024 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}" send_cmd cmd response = nil timeout(10) do response = @socket.recv 1024 end if response[0..2] == '381' then cmd = "authinfo pass #{pass}" send_cmd cmd response = @socket.recv 1024 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 = @socket.readline responsecode, cnt, first, last, name = response.split if responsecode == '211' @group = Group.new(name) @group.article_info = [cnt, first, last] @group else raise CommandFailedError, response 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 <message-id> 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. # def xover(groupname, rest) group(groupname) unless (@group && @group.name == groupname) prefix = "xover" suffix = numbers_or_id(rest) cmd = [prefix, suffix].join ' ' send_cmd(cmd) read_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) 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 do |line| next if line[0..2] == '215' 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) @socket.write(cmd+"\r\n") end def read_response response = '' queue = Queue.new str = '' ra = [] loop do str = @socket.readline queue << str ra << str break if str == ".\r\n" || !str end 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