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=nil, 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)

     print "*put* #{line}" if @debuglevel > 1
     @socket.send "#{line}\r\n", 0
   end

   def putcmd(cmd)
     puts "*cmd* #{cmd}" if @debuglevel > 0
     @socket.send "#{cmd}\r\n", 0
   end

   def getline
     puts "*getline*" if @debuglevel > 1
     line = ''
     line.concat @socket.recv 1 until line.length > 2 and line[-1] == 
"\n" or line[-2..-1] == CRLF
     line = line[0...-2] if line[-2..-1] == CRLF
     line = line[0...-1] if CRLF.include? line[-1]
     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[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[0]
       if n>2
         first = words[1]
         if n>3
           last = words[2]
           if n>4
             name = words[3].downcase
           end
        end
      end
     end
     return resp, count, first, last, name
   end

   def help
     return longcmd "HELP"
   end

   def startparse(resp)
     raise NNTPReplyError, resp, caller if resp[0...2] == '22'
     words = resp.split " "
     nr = 0
     id = ''
     n = words.length
     if n>1
       nr = words[0]
       if n>2
         id = words[1]
       end
     end
     return resp, nr, id
   end

   def statcmd(line)
     resp = shortcmd line
     return startparse.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 = startparse(resp)
     return resp, nr, id, list
   end

   def head(id)
     return artcmd "HEAD #{id}"
   end

   def body(id)
     return artcmd "BODY #{id}"
   end

   def article(id)
     return artcmd "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[i] = m[1..2]
     }
     return resp, lines
   end

   def xover(start, ed)
     begin
     resp, lines = lingcmd "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[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
end

end

if __FILE__ == $0
    s = Net::NNTP.new('news.prodigy.net', 119, 'giddybug', 'sycamore')
    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