#########################################################################################################
# 
# nntp.rb - an NNTP client implementing RFC 977 - the Network News Transfer Protocol
# ported from the Python code by Jefferson Heard
#
# this software is released under the terms of the GNU Library General Public License verson 2 or greater
# or whatever license our benevolent dictatorm Matsumoto-san, chooses.
#
# (C) 2001, Jefferson Heard
#
# Revision History:
#   11-7-2001: Initial revision
#   11-9-2001: made getresp, getlongresp, shortcmd, longcmd protected
#              and made getline, startparse, and putline private
#
##########################################################################################################

require 'socket'

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)
    
    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

  private :shortcmd, :startparse, :getline, :putline
  protected :shortcmd, :longcmd, :getresp, :getlongresp
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