################################ # # 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 = 0 @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 if ! @socket puts '*put* '+line+'\r\n' end @socket.send "#{line}\r\n", 0 end def putcmd(cmd) puts "*cmd* #{cmd}" if @debuglevel > 0 putline cmd end def getline line = @socket.readline print "*getline* '#{line}'" if @debuglevel > 0 line.chomp!("\r\n") return line end def getresp resp = getline puts "*getresp* #{resp}" if @debuglevel > 0 c = resp[0] case c when c.nil? then raise NNTPProtocolError, resp, caller 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.slice!(0) 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 mode_reader() return shortcmd("MODE READER") 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] = elements[5].split(" ") xover_lines << elements } return resp, xover_lines rescue RuntimeError raise(NNTPDataError, lines, 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