################################# # # nntp.rb - an NNTP client implementing RFC 977 # ported from the Python code by Jefferson Heard # this software is released under the terms of the GNU Library General Public License # (C) 2001, Jefferson Heard # # Contributors: Jefferson Heard, Ward Wouts # # Release History # 0.1: 11.7.2001 - Initial revision. # 0.2: 11-9-2001 - fixed regexp bugs, # fixed XHDR bugs, # made internal methods private, # changed constructor default arg # 0.3: 11-14-2001 - Fixed numerous bugs and made things a little cleaner # as per the suggestions of Ward Wouts # 0.4: 11-15-2001 - Fixed statcmd bug - Ward Wouts # 0.5: 12-06-2001 - Fixed post buf - Ozawa, Sakuro ################################# require 'socket' require 'net/protocol' module Net # Exceptions raised by NNTP class NNTPError < RuntimeError; end class NNTPReplyError < NNTPError; end class NNTPTemporaryError < NNTPError; end class NNTPPermanentError < NNTPError; end class NNTPDataError < NNTPError; end class NNTP NNTP_PORT = 119 LONGRESP = ['100', '215', '220', '221', '222', '224', '230', '231', '282'] CRLF = "\r\n" def initialize(host, port=NNTP_PORT, user=nil, password=nil, readermode=nil) @debuglevel = 2 @host = host if port then @port = port else @port = NNTP_PORT end @socket = TCPSocket.new @host, @port @welcome = getresp readermode_afterauth = false if readermode begin @welcome = shortcmd('mode reader') rescue NNTPPermanentError rescue NNTPTemporaryError if user and $!.response[0...3] == '480' readermode_afterauth = true else raise end end end if user resp = shortcmd "authinfo user #{user}" if resp[0...3] == '381' # then we need a password raise NNTPReplyError, resp, caller unless password resp = shortcmd "authinfo pass #{password}" raise NNTPPermanentError, resp, caller unless resp[0...3] == '281' end end if readermode_afterauth begin @welcome = shortcmd('mode reader') rescue NNTPPermanentError end end end def welcome puts "*welcome*, #{@welcome}" if @debuglevel > 0 return @welcome end attr_writer :debuglevel def putline(line) puts '*put* '+line+'\r\n' if @debuglevel > 1 @socket.send "#{line}\r\n", 0 end def putcmd(cmd) puts "*cmd* #{cmd}" if @debuglevel > 0 putline cmd end def getline line = '' line.concat @socket.recv 1 until line.length > 2 and line[-1] == "\n" or line[-2..-1] == "\r\n" puts '*getline* '+line if @debuglevel > 0 line = line[0...-2] if line[-2..-1] == "\r\n" line = line[0...-1] if "\r\n".include? line[-1].to_s return line end def getresp resp = getline puts "*getresp* #{resp}" if @debuglevel > 0 c = resp[0] case c when c == '4' then raise NNTPTemporaryError, resp, caller when c == '5' then raise NNTPPermanentError, resp, caller when '123'.include?(c) then raise NNTPProtocolError, resp, caller end return resp end def getlongresp resp = getresp raise NNTPReplyError, resp, caller unless LONGRESP.include? resp[0...3] list = [] while true line = getline break if line == '.' line = line[1..-1] if line.to_s[0...2] == '..' list << line end return resp, list end def shortcmd(line) putcmd line return getresp end def longcmd(line) putcmd line return getlongresp end def newgroups(date, time) return longcmd "NEWGROUPS #{date.to_s} #{time.to_s}" end def newnews(group, date, time) return longcmd "NEWNEWS #{group} #{date.to_s} #{time.to_s}" end def list resp, list = longcmd "LIST" list.each_index {|ix| list[ix] = list[ix].split " " } return resp, list end def group(name) resp = shortcmd "GROUP #{name}" raise NNTPReplyError, resp, caller unless resp[0...3] == '211' words = resp.split " " count, first, last = 0 n = words.length if n>1 count = words[1] if n>2 first = words[2] if n>3 last = words[3] if n>4 name = words[4].downcase end end end end return resp, count, first, last, name end def help return longcmd "HELP" end def statparse(resp) raise NNTPReplyError, resp, caller unless resp[0...2] == '22' words = resp.split " " nr = 0 id = '' n = words.length if n>1 nr = words[1] if n>2 id = words[2] end end return resp, nr, id end def statcmd(line) resp = shortcmd line return statparse(resp) end def stat(id) return statcmd "STAT #{id}" end def next return statcmd "NEXT" end def last return statcmd "LAST" end def articlecmd(line) resp, list = longcmd line resp, nr, id = statparse(resp) return resp, nr, id, list end def head(id) return articlecmd "HEAD #{id}" end def body(id) return articlecmd "BODY #{id}" end def article(id) return articlecmd "ARTICLE #{id}" end def slave(id) return shortcmd "SLAVE" end def xhdr(hdr, str) pat = Regexp.new '^([0-9]+) ?(.*)\n?' resp, lines = longcmd "XHDR #{hdr} #{str}" lines.each_index {|ix| line = lines[ix] m = pat.match line lines[ix] = m[1..2] if m } return resp, lines end def xover(start, ed) begin resp, lines = longcmd "XOVER #{start}-#{ed}" xover_lines = [] lines.each {|line| elements = line.split "\t" elements[5].split! " " 0.upto(7) {|ix| xover_lines << element[ix]} } return resp, xover_lines rescue RuntimeError raise NNTPDataError line, caller end end def xgtitle(group) line_pat = Regexp.new "^([^\t]+)[\t]+(.*)$" resp, raw_lines = longcmd "XGTITLE #{group}" lines = [] raw_lines.each {|line| match = line_pat.match line.strip lines << match[1..2] if match } return resp, lines end def date resp = shortcmd "DATE" raise NNTPReplyError unless resp[0...3] == '111' resp.split! " " raise NNTPDataError unless resp.length == 2 date = resp[1][2...8] time = resp[1][-6..-1] raise NNTPDataError resp, caller unless date.length == 6 and time.length == 6 return resp, date, time end def post(f) resp = shortcmd "POST" raise NNTPReplyError unless resp =~ /^3/ #[0] == 3 lines = f.readlines lines.each {|line| line.chop! line = '.' + line if line[0] == '.' putline line } putline '.' return getresp end def quit resp = shortcmd "QUIT" @socket.close_read @socket.close_write return resp end private :statparse, :getline, :putline, :articlecmd, :statcmd protected :getresp, :getlongresp end end if __FILE__ == $0 s = Net::NNTP.new('news') resp, count, first, last, name = s.group('comp.lang.ruby') puts resp puts "group #{name} has #{count} articles, range #{first} to #{last}" resp, subs = s.xhdr('subject', "#{first}-#{last}") puts resp subs.each do |sub| puts sub end resp = s.quit puts resp end