# -*- ruby -*- # nntp-lib.rb -- # NNTP Client Library # # Modified at: <1999/4/15 01:56:16 by ttate> # # Norio Suzuki # 1999-02-18 ## QUIT ## LIST ## HELP ## GROUP ## ARTICLE ## STAT ## HEAD ## BODY ## NEXT ## LAST ## POST ## NEWNEWS ## NEWGROUPS ## XOVER # message-id: add or del # From INN # AUTHINFO USER name|PASS password # Orig(same as APOP) # server greeting # 200 ... # AUTHINFO MD5 name respons # respons <- MD5.digest("" + secret) # resond within 30 sec. require "socket" require "kconv" class NNTPError < Exception ; end class NNTP PORT = 119 HOST = "localhost" def initialize(host="", port=PORT) port = port.to_i if port.type == String raise NNTPError, "port must be String or Fixnum class" unless port.type == Fixnum raise NNTPError, "port must be > 0" unless port > 0 raise NNTPError, "host must be String class" unless host.type == String @del_msg_id = TRUE @data = [] @stat = "new" open(host, port) if host != "" end def open(host=HOST, port=PORT) port = port.to_i if port.type == String raise NNTPError, "port must be String or Fixnum class" unless port.type == Fixnum raise NNTPError, "port must be > 0" unless port > 0 raise NNTPError, "host must be String class" unless host.type == String @socket = TCPsocket.open(host, port) check_reply end def close command("QUIT") @socket.close end alias quit close def auth(pass) raise NNTPError, "pass must be String class" unless auth.type == String challenge = $1 if @reply =~ /(<.+>)/ response = md5_digest(challenge + pass) command("AUTHINFO MD5 #{response}") end def reply? return @reply.gsub(/[\r\n]/,"") end def help @socket.write("HELP\r\n") reply = @socket.readline if reply[0] == ?1 read_to_end else return FALSE end end def list # newsgroup をキーとした連想配列を返す。 # 内容は、[first, last, "mode"] list = {} if command("LIST") reply = read_to_end reply.each{|line| if line =~ /(\S+)\s+(\d+)\s+(\d+)\s+(\S+)/ list[$1] = [$3.to_i, $4.to_i, $4] end } return list else return FALSE end end def group(str) # [first, last] raise NNTPError, "group must be String class" unless str.type == String if command("GROUP #{str}") @reply =~ /\d+\s+\d+\s+(\d+)\s+(\d+)/ return [$1.to_i, $2.to_i] else return FALSE end end def stat(nnn = "") nnn = nnn.to_s if nnn.type == Fixnum raise NNTPError, "article number must be String or Fixnum class" unless nnn.type == String nnn = "" if nnn == "0" if command("STAT #{nnn}") return $1 if @reply =~ /(<.+>)/ return FALSE else return FALSE end end def article(nnn = "") nnn = nnn.to_s if nnn.type == Fixnum raise NNTPError, "article number must be String or Fixnum class" unless nnn.type == String nnn = "" if nnn == "0" if command("ARTICLE #{nnn}") read_to_end else return FALSE end end def head(nnn = "") nnn = nnn.to_s if nnn.type == Fixnum raise NNTPError, "article number must be String or Fixnum class" unless nnn.type == String nnn = "" if nnn == "0" if command("HEAD #{nnn}") read_to_end else return FALSE end end def body(nnn = "") nnn = nnn.to_s if nnn.type == Fixnum raise NNTPError, "article number must be String or Fixnum class" unless nnn.type == String nnn = "" if nnn == "0" if command("BODY #{nnn}") read_to_end else return FALSE end end def last # [pointer, msg-id] if command("LAST") return [$1.to_i, $2] if @reply =~ /\d+\s+(\d+)\s+(<.+>)/ return FALSE else return FALSE end end def next # [pointer, msg-id] if command("NEXT") return [$1.to_i, $2] if @reply =~ /\d+\s+(\d+)\s+(<.+>)/ return FALSE else return FALSE end end def post(msg) id = "" data = [] if msg.type == File # File の時 begin msg.pos =0 msg.readlines.each{|line| line.gsub!(/[\r\n]/,"") line.sub!(/^\./,"..") data << line } rescue raise NNTPError, "File Read Error" end elsif msg.type == String if msg == "" data << "" else msg.split(/\n/).each {|line| line.gsub!(/\r/,"") line.sub!(/^\./,"..") data << line } end elsif msg.type == Array msg.each {|line| line.gsub!(/[\r\n]/,"") line.sub!(/^\./,"..") data << line } else raise NNTPError, "post message must be Array, String, or File class" end @socket.write("POST\r\n") reply = @socket.readline if reply[0] == ?3 data.each {|line| if line =~ /^Message-Id:\s*(<.*>)/i next if @del_msg_id end line_jis = Kconv::tojis(line) @socket.write("#{line_jis}\r\n") } @socket.write(".\r\n") check_reply else return FALSE end end def newgroups(date, time = "000000") raise NNTPError, "time must be String class" unless time.type == String if date.type == Time tmp = date date = tmp.strftime("%y%m%d") time = tmp.strftime("%H%M%S") end raise NNTPError, "date must String or Time class" unless date.type == String raise NNTPError, "date must be 6 digit" unless date =~ /\d{6,6}/ raise NNTPError, "time must be 6 digit" unless time =~ /\d{6,6}/ if command("NEWGROUPS #{date} #{time}") array = read_to_end new_groups = [] array.each{|group| new_groups << $1 if group =~ /^(\S+)/ } return new_groups end end def newnews(group, date, time = "000000") raise NNTPError, "group must be String class" unless group.type == String raise NNTPError, "time must be String class" unless time.type == String if date.type == Time tmp = date date = tmp.strftime("%y%m%d") time = tmp.strftime("%H%M%S") end raise NNTPError, "date must String or Time class" unless date.type == String raise NNTPError, "date must be 6 digit" unless date =~ /\d{6,6}/ raise NNTPError, "time must be 6 digit" unless time =~ /\d{6,6}/ if command("NEWNEWS #{group} #{date} #{time}") array = read_to_end new_news = [] array.each{|group| new_news << $1 if group =~ /^(<.+>)/ } return new_news end end def del_msg_id @del_msg_id = TRUE end def no_check_msg_id @del_msg_id = FALSE end def del_msg_id? return @del_msg_id end # XOVER does NOT described in RFC977 def xover(min = 0, max = 0) min = min.to_i if min.type == String max = min.to_i if max.type == String raise NNTPError, "min must be String or Fixnum class" unless min.type == Fixnum raise NNTPError, "max must be String or Fixnum class" unless max.type == Fixnum if min <= 0 range = "" elsif max <= 0 range = min.to_s + "-" else range = min.to_s + "-" + max.to_s end return FALSE unless command("XOVER #{range}") reply = read_to_end list = {} reply.each{|line| tmp = {} line =~ /(\d+)\s+(.*)/ article = $1 fields = $2 tag = fields.split(/\t/) tmp["subject"] = tag[0] tmp["from"] = tag[1] tmp["date"] = tag[2] tmp["id"] = tag[3] tmp["ref"] = tag[4] tmp["bytes"] = tag[5] tmp["lines"] = tag[6] tmp["opt"] = "" if tag.size > 7 tmp["opt"] = tag[7] counter = 8 while counter < tag.size tmp["opt"] = tmp["opt"] + "\t" + tag[counter] counter = counter +1 end end list[article] = tmp } return list end def xover? return FALSE unless command("LIST OVERVIEW.FMT") return read_to_end end # ここから下は、private です。 private def check_reply @reply = @socket.readline #p @reply if @reply[0] == ?2 return TRUE else return FALSE end end def read_to_end reply = [] while true line = @socket.readline line.sub!(/\r/, "") return reply if line =~ /^\.$/ line.sub!(/^\.\./, ".") reply = reply << line end end def command(str) @socket.write(str + "\r\n") check_reply end def md5_digest(str) if str.type == String md5 = MD5.new(str) digest = md5.digest.unpack("H*")[0] return digest else return "" end end end