Newer
Older
after5 / after5.rb
@HIROSE Yuuji HIROSE Yuuji on 5 Apr 2023 127 KB Update copyright line
#!/usr/bin/env ruby27
# -*- coding: euc-jp -*-
#
# Associative Scheduling Table - after5
# (C)2003, 2004, 2006, 2008, 2012-2023 by HIROSE Yuuji [yuuji<at>gentei.org]
# $Id: after5.rb,v 1.20 2012/12/03 15:54:20 yuuji Exp $
# Last modified Wed Apr  5 10:42:36 2023 on firestorm
# See http://www.gentei.org/~yuuji/software/after5/
# このスクリプトはEUCで保存してください。
$hgid = <<_HGID_.split[1..-2].join(" ")
$HGid$
_HGID_
$myurl = "https://www.gentei.org/~yuuji/software/after5/"

require 'kconv'
require 'nkf'

$charset = 'EUC-JP'

class HTMLout
  def contenttype(type = "text/html", charset = $charset)
    sprintf "Content-type: %s; charset=%s\n\n", type, charset
  end
  def initialize(title = "Document")
    @title = title
    @eltstack = []
  end
  def resetstack()
    @eltstack = []
  end
  def head(title = @title, css = "style.css")
    sprintf <<__EOS__, title, css
<html>
<head>
<title>%s</title>
<link rel="stylesheet" type="text/css" href="%s">
</head>
__EOS__
  end

  def startelement(elt, attrs = {}, nl = true)
    attr = ""
    if attrs.is_a?(Hash)
      for k in attrs.keys
	attr += " %s=\"%s\"" % [k, attrs[k]]
      end
    end
    @eltstack.push(elt)
    sprintf "<%s%s>%s", elt, attr, nl ? "\n" : ""
  end
  def endelement(elt = nil, nl = true)
    if elt
      x = elt
      @eltstack.pop
    else
      x = @eltstack.pop
    end
    sprintf "</%s>%s", x, nl ? "\n" : ""
  end
  def element(elt, attrs = nil, nl = nil)
    attr = ""
    lf = nl ? "\n" : ""
    if attrs.is_a?(Hash)
      for k in attrs.keys
        attr += " %s=\"%s\"" % [k, attrs[k]]
      end
    end
    body = yield
    sprintf "<%s%s>%s%s%s</%s>%s", elt, attr, lf, body, lf, elt, lf
  end
  def elementln(elt, attr=nil)
    body = yield
    element(elt, attr, true){body}
  end
  def a(href, anchor = nil, attrs = {})
    attr = attrs
    attr['href'] = href
    element("a", attr){
      anchor or href
    }
  end
  def p(msg, attrs=nil)
    element("p", attrs){msg}
  end
  def text(name, value='', size=nil, maxlength=nil)
    sprintf "<input type=\"text\" name=\"%s\" value=\"%s\"%s%s>",
      name, value,
      size ? " size=\"%s\""%size.to_s : '',
      maxlength ? " maxlength=\"%s\""%maxlength.to_s : ''
  end
  def hidden(name, value='')
    sprintf "<input type=\"hidden\" name=\"%s\" value=\"%s\">", name, value
  end
  def radio(name, value, text='', checked=nil)
    sprintf "%s<input type=\"radio\" name=\"%s\" value=\"%s\"%s>%s%s",
      "<label>", name, value, checked ? " checked" : "", text, "</label>"
  end
  def checkbox(name, value, text='', checked=nil)
    sprintf "%s<input type=\"checkbox\" name=\"%s\" value=\"%s\"%s>%s%s",
      "<label>", name, value, checked ? " checked" : "", text, "</label>"
  end
  def submit(name, value, text='')
    sprintf "<input type=\"submit\" name=\"%s\" value=\"%s\">%s\n",
      name, value, text
  end
  def reset(name, value, text='')
    sprintf "<input type=\"reset\" name=\"%s\" value=\"%s\">\n",
      name, value, text
  end
  def submit_reset(name)
    submit(name, "OK")+reset(name, "Reset")
  end

  def select(name, range, selected=nil)
    #start = (b<e ? b : e)
    #last  = (b>e ? b : e)
    c=0
    "<select name=\"#{name}\">\n" + \
    range.collect{|i|
      value = (i.is_a?(Array) ? i[1] : i).to_s
      sprintf "<option%s%s>%s%s</option>",
	(selected.to_s==value.to_s) ? " selected" : "", 
	i.is_a?(Array) ? " value=\"%s\"" % value : '',
	i.is_a?(Array) ? i[0] : i.to_s,
	(c+=1)%6==0 ? "\n" : ''
    }.join + \
    "\n</select>\n"
  end
end
class TEXTout
  def isBlock(elt)
    /\b(tr|[udo]l|p|div)\b/i =~ elt
  end
  def isEOC(elt)
    /\bt[dh]\b/i =~ elt
  end
  def eoelem(elt)
    r = ""
    r << "\n" if isBlock(elt)
    r << " " if isEOC(elt)
    r
  end
  def contenttype(type = "text/plain", charset = $charset)
    ### sprintf "Content-type: %s; charset=%s\n\n", type, charset
    ""
  end
  def initialize(title = "Document")
    @title = title
    @eltstack = []
  end
  def resetstack()
    @eltstack = []
  end
  def head(title = @title, css = "style.css")
    sprintf <<__EOS__, title, css
===== [[[ %s  ]]] =====
__EOS__
  end

  def startelement(elt, attrs = {}, nl = true)
    attr = ""
    x = sprintf "%s", " "*@eltstack.length
    @eltstack.push(elt)
    x
  end
  def endelement(elt = nil, nl = true)
    if elt
      x = elt
      @eltstack.pop
    else
      x = @eltstack.pop
    end
    eoelem(x)
  end
  def element(elt, attrs = nil, nl = nil)
    attr = ""
    lf = nl ? "\n" : ""
    body = yield
    #sprintf "<%s%s>%s%s%s</%s>%s", elt, attr, lf, body, lf, elt, lf
    sprintf "%s%s", body, eoelem(elt)
  end
  def elementln(elt, attr=nil)
    body = yield
    sprintf "%s\n", body
  end
  def a(href, anchor = nil, attrs = {})
    attr = attrs
    attr['href'] = href
    # sprintf "%s\n", href
    anchor
  end
  def p(msg, attrs=nil)
    element("p", attrs){msg}
  end
  def text(name, value='', size=nil, maxlength=nil)
    ""
  end
  def hidden(name, value='')
    ""
  end
  def radio(name, value, text='', checked=nil)
    ""
  end
  def checkbox(name, value, text='', checked=nil)
    ""
  end
  def submit(name, value, text='')
    ""
  end
  def reset(name, value, text='')
    ""
  end
  def submit_reset(name)
    ""
  end
  def select(name, range, selected=nil)
    ""
  end
end

class PasswdMgr
  def initialize(name, mode=0640)
    require 'dbm'
    @pdb = DBM.open(name, mode)
  end
  def checkpasswd(user, passwd)
    if @pdb[user] then
      @pdb[user] == passwd.crypt(@pdb[user])
    end
  end
  def setpasswd(user, passwd)
    salt = [rand(64),rand(64)].pack("C*").tr("\x00-\x3f","A-Za-z0-9./")
    @pdb[user] = passwd.crypt(salt)
  end
  def userexist?(user)
    @pdb[user] ? true : false
  end
  def getpasswd(user)
    @pdb[user]
  end
  def delete(user)
    @pdb.delete(user)
  end
  def close()
    @pdb.close()
  end
  def newpasswd(length)
    srand()
    left	= "qazxswedcvfrtgb12345"
    right	= "yhnmjuik.lop;/67890-"
    array	= [left, right]
    (1..length).collect{|i|
      a = array[i%array.length]
      a[rand(a.length), 1]
    }.join('')
  end
  def users()
    @pdb.keys.collect{|u| u.toeuc} # toeuc is for 1.9 :(
  end
  private :newpasswd
  def setnewpasswd(user, length=8)
    length = length.to_i
    length = 4 if length < 4
    newp = newpasswd(length)
    setpasswd(user, newp)
    newp
  end
end
  
class ScheduleDir
  def initialize(dir = "s")
    @dir = dir
    @schedulefile = "sched"
    @usermapdir = File.join(@dir, "usermap")
    @usermap = mkusermap()
    @groupmapdir = File.join(@dir, "groupmap")
    @groupmap = mkgroupmap()
    @crondir = File.join(@dir, "crondir")
    
  end
  def mkusermap()
    map = {}
    unless test(?d, @usermapdir)
      mkdir_p(@usermapdir, 0750)
    end
    Dir.foreach(@usermapdir){|u|
      next if /^\./ =~ u
      newu = ''
      u.split('').each{|c|	# for security wrapping
	newu << c[0].chr if %r,[-A-Z0-9/+_.@],i =~ c
      }
      u = newu
      map[u] = {}
      d = File.join(@usermapdir, u).untaint
      next unless test(?d, d)
      Dir.foreach(d){|attr|
	next if /^\./ =~ attr
	attr.untaint if /^[A-Z_][-A-Z_0-9]*$/i =~ attr
	file = File.join(@usermapdir, u, attr).untaint
	next unless test(?s, file) && test(?r, file)
	map[u][attr] = IO.readlines(file).join.toeuc.strip
      }
    }
    map
  end
  def ismembersemail(email, grp = nil)
    @usermap.keys.each {|u|
      return u if u==email
      return u if mailaddress(u, grp).split(/,\s*|\s+/).collect{|m|
        m.sub(/^(skip|off):/i, "")
      }.grep(email)[0]
    }
    nil
  end
  def putuserattr(user, attr, text)
    # if text==nil, remove it
    d = File.join(@usermapdir, user)
    Dir.mkdir(d) unless test(?d, d)
    file = File.join(d, attr)
    begin
      unless @usermap[user]
	@usermap[user] = {}
	mkdir_p(d) unless test(?d, d)
      end
      @usermap[user][attr] = text
      if text==nil
	File.unlink(file)
      else
	open(file, "w"){|w| w.puts @usermap[user][attr]}
      end
    rescue
      return nil
    end
    return {attr => text}
  end
  def getuserattr(user, attr)
    # Should we distinguish between attribute is nil and "" ?
    if @usermap.has_key?(user) && @usermap[user][attr].is_a?(String) &&
	@usermap[user][attr] > ''
      return @usermap[user][attr].untaint
    else
      return nil
    end
  end

  def nickname(user)
    if @usermap.has_key?(user) && @usermap[user]['name'].is_a?(String) &&
	@usermap[user]['name'] > ''
      return @usermap[user]['name']
    else
      return user.sub(/@.*/, '')
    end
  end
  def mailaddress(user, grp = nil)
    grp ? \
    mail4grp(user, grp) : \
    (getuserattr(user, 'email') || user)
  end
  def setnickname(user, nickname)
    putuserattr(user, 'name', nickname)
  end

  #
  # make group map
  def collectmembers(gname)
    @visitedgroup=[] unless @visitedgroup
    return [] unless @visitedgroup.grep(gname).empty?
    @visitedgroup.push(gname)
    mdir = File.join(@groupmapdir, gname, 'members').untaint
    return [] unless test(?d, mdir)
    members = []
    Dir.foreach(mdir){|item|
      next if /^\./ =~ item
      item.untaint
      next unless test(?f, File.join(mdir, item))
      if /.+@.+/ =~ item
	members << item
      else
	members += collectmembers(item)
      end
    }
    @visitedgroup.pop
    members.uniq
  end
  def mkgroupmap()
    map = {}
    return map unless test(?d, @groupmapdir)
    @visitedgroup = []
    Dir.foreach(@groupmapdir){|g|
      next if /^\./ =~ g
      newg = ''
      next unless /^[-a-z0-9_.]+$/i =~ g
      #g.untaint ## untaintじゃだめだ。map{g} のkeyがtaintedになっちゃうよ
      gg = ''			# for security wrapping
      g.split('').each{|c| gg << c[0].chr if c != '`'}
      g = gg
      map[gg] = {}
      d = File.join(@groupmapdir, g).untaint
      next unless test(?d, d)
      # get group name
      gnf = File.join(d, 'name').untaint
      if test(?r, gnf) && test(?s, gnf)
	n = IO.readlines(gnf)[0].to_s.toeuc.strip
	map[g]['name'] = if n > '' then n else g end
      else
	map[g]['name'] = g
      end
      # get administrators
      #
      gad = File.join(d, 'admin').untaint
      map[g]['admin'] = []
      if test(?d, gad)
	Dir.foreach(gad){|a|
	  # administrator should be a person (not group)
	  next unless /@/ =~ a
	  map[g]['admin'] << a
	}
      end
      # collect members
      #map[g]['members'] = collectmembers(g)
      memd = File.join(d, 'members').untaint
      map[g]['members'] = []
      if test(?d, memd)
	Dir.foreach(memd){|a|
	  next if /^\./ =~ a
	  map[g]['members'] << a.untaint
	}
      end
      # get other attributes
      Dir.foreach(d) {|attr|
        next if /^\./ =~ attr
        next unless /^[-_a-z]+$/i =~ attr
        next if attr == "name"   # already collected
        attr.untaint
        file = File.join(d, attr) #.untaint
        next if test(?d, file)
        next unless test(?s, file) && test(?r, file)
        map[g][attr] = IO.readlines(file).join.toeuc.strip
      }
    }
    map
  end
  def putgroupattr(group, attr, value)
    d = File.join(@groupmapdir, group).untaint
    Dir.mkdir(d) unless test(?d, d)
    file = File.join(d, attr)
    begin
      unless @groupmap[group]
        @groupmap[group] = {}
      end
      @groupmap[group][attr] = value
      if value == nil
        File.unlink(file)
      else
        open(file, "w"){|w| w.puts @groupmap[group][attr]}
      end
    rescue
      return nil
    end
    return {attr => value}
  end
  def getgroupattr(group, attr)
    if @groupmap.has_key?(group) && @groupmap[group][attr].is_a?(String) &&
	@groupmap[group][attr] > ''
      return @groupmap[group][attr].untaint
    else
      return nil
    end
  end
  def groupmap()
    @groupmap
  end
  def groups()
    @groupmap.keys
  end
  def addgroup(group, users, remove=nil, role='members')
    grp = groups.grep(group)[0]	# group may be tainted, using kept name
    return nil unless grp
    for u in users
      m = nil
      u, m = u if u.is_a?(Array) # ["user", "mailto"]
      m = nil if mailaddress(u)==m || /@/ !~ m
      next unless account_exists(u)
      mdir = File.join(@groupmapdir, grp, role).untaint
      file = File.join(mdir, u).untaint
      if remove
	@groupmap[grp][role].delete(u)
	File.unlink(file) if test(?e, file)
      else
	@groupmap[grp][role] << u
	@groupmap[grp][role].uniq
	Dir.mkdir(file) unless test(?d, mdir)
	open(file, "w"){|x|x.puts m if m}
      end
    end
    grp
  end
  def setgroupname(grp, name)
    return nil unless @groupmap[grp]
    mdir = File.join(@groupmapdir, grp).untaint
    nfile = File.join(mdir, 'name').untaint
    @groupmap[grp]['name'] = name
    if grp == name
      # remove the name file because it is default name
      File.unlink(nfile) if test(?e, nfile)
    else
      Dir.mkdir(mdir) unless test(?d, mdir)
      open(nfile, "w"){|n| n.puts name.to_s.strip}
    end
    name
  end
  def creategroup(grp, grpname="", admin=[])
    grpptnOK = /^[-A-Z0-9._:!$%,]+$/i
    return nil unless grpptnOK =~ grp
    gg = ''
    grp.split('').each{|c| gg << c[0].chr if c =~ grpptnOK}
    grp = gg
    gdir = File.join(@groupmapdir, grp)
    mkdir_p(gdir)		# Should not care errors here
    Dir.mkdir(File.join(gdir, "admin"))
    Dir.mkdir(File.join(gdir, "members"))
    @groupmap[grp] = {}
    if grpname == ''
      @groupmap[grp]['name'] = grp
    else
      setgroupname(grp, grpname)
    end
    @groupmap[grp]['members'] = []
    @groupmap[grp]['admin'] = []
    addgroup(grp, admin)
    addgroup(grp, admin, nil, 'admin')
    return @groupmap[grp]
  end
  def createuser(user, email = nil)
    return nil unless /@/ =~ user
    return nil if %r@[\/()\;|,$\%^!\#&\'\"]@ =~ user
    email = email || user
    @usermap[user] = {}
    dir = File.join(@usermapdir, user).untaint
    test(?d, dir) || Dir.mkdir(dir)
    putuserattr(user, 'email', email)
  end
  def deleteuser(user)
    return nil unless @usermap[user]
    begin
      @usermap[user]		# return value
    ensure
      @usermap.delete(user)
      rm_rf(File.join(@usermapdir, user))
      rm_rf(File.join(@groupmapdir, "*/members/#{user}"))
      rm_rf(File.join(@crondir, "[1-9]*-*-*/#{user}"))
      rm_rf(File.join(@dir, "[1-9]*/[0-9][0-9]/[0-9][0-9]/[0-9]???/#{user}"))
    end
  end
  def destroygroup(grp)
    return nil unless @groupmap[grp]
    begin
      @groupmap[grp]		# return value
    ensure
      @groupmap.delete(grp)
      rm_rf(File.join(@groupmapdir, grp))
      rm_rf(File.join(@groupmapdir, "*/members/#{grp}"))
      rm_rf(File.join(@crondir, "[1-9]*-*-*/#{grp}"))
      rm_rf(File.join(@dir, "[1-9]*/[0-9][0-9]/[0-9][0-9]/[0-9]???/#{grp}"))
    end
  end
  def rm_rf(path)
    path.untaint
    if (list = Dir.glob(path))[0]
      for p in list
	p.untaint
	system "/bin/rm -rf \"#{p}\""
      end
      cleanup_files(list)
    end
  end
  def account_exists(instance)
    if /@/ =~ instance
      true
    else
      ! @groupmap.select{|k, v| k==instance}.empty?
    end
  end
  def mail4grp(usr, group)
    # If members/user file contains only "skip:" keyword,
    # return "skip:email@add.re.ss"
    default = mailaddress(usr)
    file = File.expand_path((group+"/members/"+usr).untaint, @groupmapdir)
    if test(?s, file.untaint)
      rcpt = open(file, "r"){|f|f.gets.chomp}.untaint
      if /^(off|skip):/ =~ rcpt && /@/ !~ rcpt
        rcpt = "skip:"+default
      end
      return rcpt
    end
    default
  end
  def delivergrpmail(user, grp)

  end
  def ismember(user, grouporuser)
    return user if user==grouporuser
    if @groupmap[grouporuser]
      @groupmap[grouporuser]['members'].grep(user)[0] &&
        mail4grp(user, grouporuser)
    end
  end
  def isuser(user)
    @usermap[user] && @usermap.keys.grep(user)[0]
  end
  def isgroup(grp)
    @groupmap[grp]
  end
  def isadmin(user, group)
    @groupmap[group] and @groupmap[group]['admin'].grep(user)[0]
  end
  def members(grp)
    @groupmap[grp] and ####################@groupmap[grp]['members']
      collectmembers(grp)
  end
  def membernames(grp)
    if isgroup(grp)
      members(grp).collect{|u| nickname(u)}
    else
      [nickname(grp)]
    end
  end
  def admins(grp)
    @groupmap[grp] and @groupmap[grp]['admin']
  end
  def groupname(grp)
    @groupmap[grp] && @groupmap[grp]['name']
  end
  def name2group(name)
    @groupmap.find{|g, v| v.is_a?(Hash) && v['name']==name}
  end
  def day_all(d, user=nil, personalonly = nil, filter)
    y, m, d = d.scan(%r,(\d\d\d\d+)/(\d+)/(\d+),)[0]
    #daydir = File.join(@dir, "%04d"%y.to_i, "%02d"%m.to_i, "%02d"%d.to_i)
    daydir = File.join("s", "%04d"%y.to_i, "%02d"%m.to_i, "%02d"%d.to_i)
    sched = {}
    grep = if filter && filter > ''
             Regexp.new(filter.split(/\s+/).join("|"))
           end
    return sched unless test(?d, daydir)
    Dir.foreach(daydir) {|time|
      next if /^\./ =~ time
      next unless /^\d\d\d\d$/ =~ time
      time.untaint
      t = File.join(daydir, time)
      next unless test(?d, t)
      sched[time] = {}
      Dir.foreach(t){|who|
	next if /^\./ =~ who

	visible = false
	#next unless /@/ =~ who	# user must be as user@do.ma.in
	next unless account_exists(who)
	## next if private && who != user  #2004/1/16
	who.untaint
	dir = File.join(t, who)
	next unless test(?d, dir) && test(?x, dir)
	pub = File.join(dir, 'pub')
	if test(?f, pub) && test(?r, pub) && test(?s, pub) &&
	    !personalonly # unneccessary if personalonly mode
	  if IO.readlines(pub)[0].to_i > 0
	    visible = true
	  end
	end
        next if grep && (grep !~ who && grep !~ nickname(who)) # 2013/12/19

	if ismember(user, who) || visible
	  sched[time][who] = {}
	  file = File.join(dir, @schedulefile)
	  if test(?s, file) && test(?r, file) && test(?s, file)
	    sched[time][who]['sched'] = IO.readlines(file).join.toeuc.chomp!
	    sched[time][who]['regtime'] = File.stat(file).mtime
	  end
	  sched[time][who]['pub'] = visible
	end
      } #|who|
      sched.delete(time) if sched[time].empty?
    }
    sched
  end

  def scheduledir(user, y, m, d, time)
    sprintf("%s/%04d/%02d/%02d/%04d/%s",
	    @dir, y.to_i, m.to_i, d.to_i, time.to_i, user).untaint
  end
  def schedulefile(user, y, m, d, time)
    File.join(scheduledir(user, y, m, d, time), @schedulefile)
  end
  def mkdir_p(path, mode=0777)
    # Do not mkdir `path' for
    #	absolute paths
    #   those paths which contains `../'
    # for the sake of security reason
    return false if %r,\.\./|^/, =~ path
    path = path.untaint
    p = 0
    i=0
    while p=path.index("/", p)
      dir = path[0..p].chop
      p += 1
      break if i > 10	# overprotecting
      next if test(?d, dir)
      Dir.mkdir(dir, mode)
      i += 1
    end
    Dir.mkdir(path, mode) unless test(?d, path)
  end

  #
  # register schedule for user
  #
  def register(user, year, month, day, time, text, replace=nil)
    # return code: 0 = succesfull new registration
    #              1 = succesfull appending registration
    dir = scheduledir(user, year, month, day, time)
    file = schedulefile(user, year, month, day, time)
    ret = 0
    um = File.umask(027)
    begin
      if !replace && test(?s, file)
	ret = 1
      else
	mkdir_p(dir, 0777)
      end
    ensure
      File.umask(um)
    end
    open(file, replace ? "w" : "a"){|out|out.print text}
    return ret
  end
  def getschedule(user, year, month, day, time)
    file = schedulefile(user, year, month, day, time)
    if test(?r, file) && test(?s, file)
      return IO.readlines(file).join.toeuc
    end
    return nil
  end
  def remove(user, year, month, day, time)
    file = schedulefile(user, year, month, day, time)
    dir = File.dirname(file)
    if test(?r, file) && test(?s, file)
      File.unlink(file)
    end
    for f in Dir.glob(File.join(dir, "*"))
      f.untaint
      File.unlink(f)
    end
    Dir.rmdir(dir) if test(?d, dir)
    begin
      Dir.rmdir(File.dirname(dir))
    rescue
    end
  end
  #
  # register file
  #
  def putfile(user, year, month, day, time, file, contents)
    scback = @schedulefile
    begin
      @schedulefile = File.basename(file)
      register(user, year, month, day, time, contents, true)
    ensure
      @schedulefile = scback
    end
  end
  def getfile(user, year, month, day, time, file)
    scback = @schedulefile
    begin
      @schedulefile = File.basename(file)
      getschedule(user, year, month, day, time)
    ensure
      @schedulefile = scback
    end
  end
  def removefile(user, year, month, day, time, file)
    dir = scheduledir(user, year, month, day, time)
    file = File.join(dir, file)
    if test(?e, file)
      File.unlink(file)
    end
  end
  #
  # registration to crondir
  #
  def cronlink_file(nt_time, user, y, m, d, time)
    subdir = nt_time.strftime("%Y-%m-%d-%H%M/#{user}")
    cdir = File.join(@crondir, subdir)
    File.join(cdir, sprintf("%04d-%02d-%02d-%04d", y, m, d, time))
  end
  def register_crondir(nt_time, user, y, m, d, time)
    linkfile = cronlink_file(nt_time, user, y, m, d, time)
    mkdir_p(File.dirname(linkfile))
    scfile = schedulefile(user, y, m, d, time)
    if test(?s, scfile)
      sclink = File.join("../../..", scfile.sub!(Regexp.quote(@dir+'/'), ''))
      File.symlink(sclink, linkfile) unless test(?e, linkfile)
      return linkfile
    end
    return false
  end
  def remove_crondir(nt_time, user, y, m, d, time)
    linkfile = cronlink_file(nt_time, user, y, m, d, time)
    scfile = schedulefile(user, y, m, d, time)
    if test(?e, linkfile)
      File.unlink(linkfile)
      begin
	dir = linkfile
	2.times {|x|
	  dir = File.dirname(dir)
	  if Dir.open(dir).collect.length <= 2  # is empty dir
	    Dir.rmdir(dir)
	  else
	    break
	  end
	}
      rescue
      end
      return linkfile
    end
    return false
  end

  #
  # return the Hash of crondir {user => files}
  def notify_list(asof)
    slack = 5*60
    gomifiles = []
    ntl = {}
    return ntl unless test(?d, @crondir)
    Dir.foreach(@crondir){|datedir|
      next unless /(\d\d\d\d+)-(\d+)-(\d+)-(\d\d\d\d)/ =~ datedir
      ##datedir = sprintf("%04d-%02d-%02d-%04d",
      ## $1.to_i, $2.to_i, $3.to_i, $4.to_i)
      datedir.untaint
      dd = File.join(@crondir, datedir)
      next unless test(?d, dd)
      y, m, d, hm = $1.to_i, $2.to_i, $3.to_i, $4.to_i
      hh = hm/100 % 60
      mm = (hm%100) % 60
      t = Time.mktime(y, m, d, hh, mm)
      next if t-slack > asof
      #
      # collect them
      Dir.foreach(dd){|user|
	# next unless /@/ =~ user || isgroup(user)
	next if /^\./ =~ user
	if isgroup(user)
	  user = @groupmap.keys.grep(user)[0]
	else
	  user = @usermap.keys.grep(user)[0]
	end
	next unless user
	ud = File.join(dd, user).untaint
	next unless test(?d, ud)
	ntl[user] = {} unless ntl.has_key?(user)
	Dir.foreach(ud){|date|
	  next if /^\./ =~ date
	  unless /(\d\d\d\d+)-(\d+)-(\d+)-(\d\d\d\d)/ =~ date
	    gomifiles << File.join(ud, date)
	    next
	  end
	  #date = sprintf("%04d-%02d-%02d-%04d",
	  #		 $1.to_i, $2.to_i, $3.to_i, $4.to_i)
	  date.untaint
	  f = File.join(ud, date)
	  if test(?s, f)
	    ntl[user][date] = {}
	    ntl[user][date]['file'] = f
	    ntl[user][date]['text'] = 
              IO.readlines(f).collect{|l| l.toeuc}	# ...why? :-(
	  else
	    File.unlink(f)	# symlink points to nonexistent file
	  end
	}
	if ntl[user].empty?
	  # if ud does not contain valid cron symlinks,
	  # ud had been left badly.  Remove it.
	  ntl.delete(user) 
	  cleanup_files(gomifiles)
	end
      }
    }
    ntl
  end
  # 
  # cleanup file and directories
  def cleanup_crondir(time)
    Dir.foreach(@crnondir){|datedir|
      dd = File.join(@crondir, datedir)
      next unless test(?d, dd)
      next unless /(\d\d\d\d+)-(\d+)-(\d+)-(\d\d\d\d)/ =~ dd
      y, m, d, hm = $1.to_i, $2.to_i, $3.to_i, $4.to_i
      hh = hm/100 % 60
      mm = (hm%100) % 60
      t = Time.mktime(y, m, d, hh, mm)
      if t < time
	system "rm -rf #{dd}"
      end
    }
  end
  #
  # remove files in FILES, and remove parent directory if possible
  def cleanup_files(files)
    sentinel = File.stat(@dir).ino
    me = $0.dup.untaint
    scriptsuid = File.stat(me).uid
    for f in files
      if $SAFE > 0
	f.untaint
	if test(?e, f) && File.stat(f).uid != scriptsuid
	  f.taint
	end
      end
      printf "Removing %s\n", f if $DEBUG
      File.unlink(f) if test(?e, f)
      d = f
      loop {
	d = File.dirname(d)
	break if d.length < 2
	break if File.stat(d).ino == sentinel
	begin
	  puts "rmdir #{d}" if $DEBUG
	  Dir.rmdir(d)
	rescue
	  break
	end
      }
    end
  end
end

class StringIO<IO
  def initialize()
    @str=""
  end
  def foo=(str)
    @str = str
  end
  def append(str)
    @str = str+@str
  end
  def print(str)
    @str << str
  end
  def puts(str)
    @str << str+"\n"
  end
  def printf(*args)
    @str << sprintf(*args)
  end
  def write(bytes)
    print(bytes)
  end
  def gets()
    return nil if @str == ''
    p = @str.index(?\n)
    if p
      r = @str[0..p]
      @str=@str[p+1..-1]
    else
      r = @str
    end
    return r
  end
  def readline()
    this.gets()
  end
  def readlines()
    r = @str
    @str=''
    r
  end
    
  def p(*obj)
    STDOUT.p(*obj)
  end
end

class CMDTimeout < Exception
  def initialize()
    @pw = IO.pipe
    @pr = IO.pipe
    @pe = IO.pipe
    @timeout = false
  end
  def start(cmd, timeout, mixstderr=false)
    if @pid=fork
      @pw[0].close
      @pr[1].close
      @pe[1].close
      # puts "parent!"
      if @tk=fork
	# main
      else
	@pw[1].close
	@pr[0].close
	@pe[0].close
	trap(:INT){exit 0}
	sleep timeout
	begin
	  @timeout = true
	  STDERR.puts "TIMEOUT"
	  Process.kill :INT, @pid
	rescue
	  #puts "Already done"
	end
	exit 0
      end
    else
      # Running this block with pid=@pid
      trap(:INT){@timeout = true; exit 0}
      @pw[1].close
      STDIN.reopen(@pw[0])
      @pw[0].close

      @pr[0].close
      STDOUT.reopen(@pr[1])
      if mixstderr
	STDERR.reopen(@pr[1])
      else
	STDERR.reopen(@pe[1])
      end
      @pr[1].close
      @pe[0].close
      @pe[1].close

      exec(*cmd)
      exit 0
    end
    return [@pw[1], @pr[0], @pe[0]]
  end
  def wait()
    Process.waitpid(@pid, nil)
  end
  def close()
    @pr.each{|p| p.close unless p.closed?}
    @pw.each{|p| p.close unless p.closed?}
    @pe.each{|p| p.close unless p.closed?}
    begin
      Process.kill :INT, @tk
    rescue
    end
  end
  def timeout()
    @timeout
  end
end

class Holiday
  def initialize(dir = ".")
    @@dir = dir
    defined?(@@holiday) || setupHoliday
  end
  def setupHoliday(file = "holiday")
    @@holiday = {}
    return unless test(?f, file) && test(?s, file)
    IO.foreach(file){|line|
      line = line.toeuc.strip
      next if /^#/ =~ line
      date, what = line.scan(/(\S+)\s+(.*)/)[0]
      if %r,(\d+)/(\d+)/(\d+), =~ date
	cdate = sprintf("%d/%d/%d", $1.to_i, $2.to_i, $3.to_i)
	@@holiday[cdate] || @@holiday[cdate] = []
	@@holiday[cdate] << what
      elsif %r,(\d+)/(\d+), =~ date
	cdate = sprintf("%d/%d", $1.to_i, $2.to_i)
	@@holiday[cdate] || @@holiday[cdate] = []
	@@holiday[cdate] << what
      elsif %r,(\d+)/(\w+), =~ date
	cdate = sprintf("%d/%s", $1.to_i, $2.downcase)
	@@holiday[cdate] || @@holiday[cdate] = []
	@@holiday[cdate] << what
      end
    }
  end
  def isHoliday(y, m, d, wday=nil)
    y, m, d = y.to_i, m.to_i, d.to_i
    wname = %w[sun mon tue wed thu fri sat]
    wday = wname[wday || Time.mktime(y, m, d).wday]
    holiday = @@holiday[sprintf("%d/%d/%d", y, m, d)] ||
      @@holiday[sprintf("%d/%d", m, d)]
    unless holiday
      nthweek = (d-1)/7+1
      holiday = @@holiday[sprintf("%d/w%d%s", m, nthweek, wday)]
    end
    if !holiday && wday == "mon" && d > 0 # d<1 when column is before 1th
      # holiday in lieu
      yesterday = Time.mktime(y, m, d)-3600*24
      holiday = ["振替休日"] if
        isHoliday(yesterday.year, yesterday.mon, yesterday.day)
    end
    holiday
  end
  def holidays()
    @@holiday
  end
end

class After5
  def initialize()
    @me = File.expand_path($0)
    @mydir, @myname = File.dirname(@me), File.basename(@me)
    @mybase = @myname.sub(/\.\w+$/, '')
    @mydir.untaint
    @mybase.untaint
    Dir.chdir @mydir
    @myname='a5.cgi' if test(?f, "a5.cgi")
    @conf = nil
    @schedulearea = {'rows'=>'4', 'cols'=>'60', 'name'=>'schedule'}
    @oldagent = (%r,Mozilla/4, =~ ENV['HTTP_USER_AGENT'])
    @lang = 0
    @mlbasedir = "ml"
    @attachmentdir = "a"
    @attachmentmax = 8*1024**2
    @mailmode = nil
    @mailadmdelimiter = "/"
    @mailadmsuffix = @mailadmdelimiter + "adm"
    @saveprefsregexp = /^(display(mode|days|filter)$|nt|headline)/
    @opt = {
      'conf'		=> @mybase+".cf",
      'css'		=> @mybase+".css",
      'logfile'		=> 's/'+@mybase+".log",
      "sendmail"	=> "/usr/sbin/sendmail",
      'hostcmd'		=> '/usr/bin/host',
      'nslookup'	=> '/usr/sbin/nsookup',
      'bg'		=> 'ivory',
      'name'		=> nil,
      'at_bsd'		=> '%H:%M %b %d %Y',
      'at_solaris'	=> '%H:%M %b %d,%Y',
      'schedir'		=> 's',
      'tdskip'		=> '<br>',
      'forgot'		=> 'wasureta',
      'size'		=> @oldagent ? '15' : '40',
      'morning'		=> '6',
      'night'		=> '22',
      'alldaydir'	=> '3000',
      'pswdlen'		=> 4,
      'pswddb'		=> 's/a5pswd',
      'lang'		=> 'j',
      'notifymail'	=> true,
      'mailbracket'	=> '[%n-ML]',
    }
    @subjtags = [['[GroupID:#]', "[%i:%c]"],
                 ['[GroupID:#####]', "[%i:%5c]"],
                 ['[GroupName:#]', "[%n:%c]"],
                 ['[GroupName:#####]', "[%n:%5c]"],
                 ['(GroupID:#)', "(%i:%c)"],
                 ['(GroupID:#####)', "(%i:%5c)"],
                 ['(GroupName:#)', "(%n:%c)"],
                 ['(GroupName:#####)', "(%n:%5c)"],
                 ['NONE', "NONE"]]
    ##@job = "today"
    @wnames = %w[sun mon tue wed thu fri sat]
    @job = "login"
    @sc = ScheduleDir.new
    @O = StringIO.new
    @H = HTMLout.new()
    @umback = File.umask
    @author = 'yuuji@gentei.org'
    @after5url = 'https://www.gentei.org/~yuuji/software/after5/'
    File.umask(007)
  end
  def doit()
    @params = getarg()
    @cookie = getcookie()
    importcookie()
    @lang = (/^j/i =~ @opt['lang'] ? 0 : 1)
    @ntlist = [                 # this shoud be set after @lang
      ['nt10m', "10"+msg('minutes', 'before')],
      ['nt30m',	"30"+msg('minutes', 'before')],
      ['nt60m',	"60"+msg('minutes', 'before')],
      ['nttoday', msg('theday')],
      ['nt1d',	"1"+msg('days', 'before')],
      ['nt2d',	"2"+msg('days', 'before')],
      ['nt3d',	"3"+msg('days', 'before')],
      ['nt7d',	"7"+msg('days', 'before')],
      ['nt30d',	"30"+msg('days', 'before')],
    ]
    p @cookie if $DEBUG
    p @params if $DEBUG

    ### @params['displaymode'] = @params['displaymode'] || @cookie['displaymode']
    personal = /^personal/i =~ @params['displaymode']
    bodyclass = if personal then {'class'=>'personal'} end

    ## x = {"align"=>'center'}
    ## @H.element("p", x, "hoge", nil)
    ## @H.element("p", nil, "buha", nil)

    if nil
    if !@params['passwd'] && @cookie['passwd']
      @params['passwd'] = @cookie['passwd']
    end
    if !@params['user'] && @cookie['user']
      @params['user'] = @cookie['user']
    end
    end
    @params['user'] = safecopy(@params['user'])

    ######eval @job
    a5name = if @opt['name'] && @opt['name'] > ''
	       sprintf("(%s)", @opt['name'])
	     else
	       ""
	     end
    @O.append(@H.contenttype() +
	      @H.head(a5name+"After 5"+@job.sub(/\s*/, ' '), @opt['css']))
    @O.print @H.startelement("body", bodyclass, true)
    # @job should be here because its output shoud go after <body>.
    eval @job
    @O.print @H.endelement(nil, true) # body
    @O.print @H.endelement("html", true)	# html
    setcookie()

    print @O.readlines
  end
  def msg(*keyword)
    unless defined?(@msg)
      @msg = {
	'title'		=> ['みんなの予定表 <img src="after5.png" alt="「アフター5」">', 'Schedule table for us all <img src="after5.png" alt="After 5">'],
	'login'	=> ['ログイン', 'Login'],
	'loginfirst'	=> ['最初にログインすべし', 'Login first'],
	'autherror'	=> ['認証エラーがあったと管理者に伝えてくれっす',
	  'Unexpected authentication error. Please tell this to the administrator'],
	 'yourmail'	=> ['あなたのメイルアドレス', 'Your email address'],
	'passwd'	=> ['パスワード<br>(初回時は空欄)',
	  'Passowrd<br>Left blank, first time'],
	'error'		=> ['エラー:', 'Error: '],
	'mailerror'	=> ['メイルアドレスが違います', 'Invalid email address'],
	'pswderror'	=> ['パスワードが違います', 'Password incorrect'],
	'forgotguide'	=> ['忘れた場合は %s と入力するよろし',
 "Put \`%s' when you forgot password."],
	'fmtdaysschedule'=> ['%s〜の予定', 'Schedule from %s'],
        'schedtable'	=> ['予定表', 'Schedule Table'],
	'noplan'	=> ['登録されている予定はありません', 'No plans'],
	'allday'	=> ['全日', 'whole day'],
	'addsched'	=> ['新規予定項目の登録', 'Register new schedule'],
	'defthisday'	=> ['デフォルトの日付はこの日になってま', ''],
	'24hour'	=> ['24時間制4桁でね<br>(0000〜2359)<br>%sは時刻指定なし', 'in 24-hour<br>(0000-2359)<br>%s for whole day'],
	'24hourtxt'	=> ['24時間制4桁でね(0000〜2359), %sは時刻指定なし', 'in 24-hour(0000-2359), %s for whole day'],
	'reqnotify'	=> ['通知メイルいるけ?', 'Previous notification'],
	'rightnow'	=> ['登録時にすぐ', 'Right now on registration'],
	'immediatenote'	=> ['に以下の予定を登録しました',
	  ", Your schedule has been registered as follows;"],
	'registerer_is'	=> ['登録名義: ', 'Register as '],
	'registerer'	=> ['登録者: ', 'registerer: '],
	'about'		=> ['約', 'about'],
	'minutes'	=> ['分', 'minutes'],
	'hours'		=> ['時間', 'hour(s)'],
	'days'		=> ['日', 'day(s)'],
	'daystodisplay'	=> ['日分表示', 'days to display'],
	'before'	=> ['前', 'before'],
	'precedingday'	=> ['前日', 'Preceding day'],
	'theday'	=> ['当日朝', "the day's morning"],
	'night'		=> ['(夜)', '(night)'],
	'publicok'	=> ['アカウント保持者全員<br>に見せてもええね?',
	  'visible to anyone who has account of this board?'],
	'public'	=> ['公', 'pub'],
	'nonpublic'	=> ['非', 'sec'],
	'through'	=> ['〜', '=&gt;'],
	'yes'		=> ['はいな', 'yes'],
	'no'		=> ['やだ', 'nope'],
	'wnames'	=> [%w[日 月 火 水 木 金 土],
	  %w[sun mon tue wed thu fri sat]],
	'whichday'	=> ['<small>(まとめ登録の場合)</small><br>期間中のどの日に?',
	  '<small>(On multiple registration)</small><br>Which days in the term?'],
	'singleday'	=> ['一日分だけ登録', '1day regist'],
	'everyday'	=> ['毎日', 'everyday'],
	'invaliddate'	=> ['日付指定が変みたい', 'Invalid time string'],
	'past'		=> ['それはもう過去の話ね', 'It had Pasted'],
	'putsomething'	=> ['何か書こうや', 'Write some message please'],
	'appended'	=> ['既存の予定に追加しました', 'Appended'],
	'append'	=> ['追加', 'append'],
	'join'		=> ['参加', 'join'],
	'regist'	=> ['登録', 'register'],
	'remove'	=> ['削除', 'remove'],
	'move'		=> ['移動', 'move'],
	'newdate'	=> ['移動先時刻', 'New date'],
	'deletion'	=> ['完全消去', 'deletion'],
	'deletionwarn'	=> ['OK押したら即消去。確認とらないぞ',
	  'Hitting OK immediately delets this group, be carefully!'],
	'deluser'	=> ['%s ユーザ消してええかの?', "Delete the user `%s'"],
	'delgroup'	=> ['%s グループ消してええかの?', "Delete the group `%s'"],
	'really?'	=> ['ほんまにええけ?', 'Really?'],
	'chicken'	=> ['ふっ、腰抜けめ', 'Hey, chicken boy'],
	'modify'	=> ['修正', 'modify'],
	'done'		=> ['完了', 'done'],
	'success'	=> ['成功', 'success'],
	'failure'	=> ['失敗', 'failure'],
	'tomonthlist'	=> ['%s の一覧', 'all %s table'],
	'notifysubj'	=> @mybase+"'s reminder for your plan",
	'introduce'	=> ['はいこんにちは、'+@mybase+'ですよ〜。',
	  "Hi, this is #{@mybase}'s notification."],
	'notifymail'	=> ['こんな予定がありまっせ。',
	  "You have some eschedule below;"],
	'notification'	=> ['の通知', 'notification'],
	'newaccount'	=> ["新しいアカウントを作りました。\n"+
      			   "パスワードは %s さん宛に送信しておきました。\n",
	  "You got new account for #{@mybase}\n" +
	  "Password was sent to %s.\nThank you.\n"],
	'accessfrom'	=> ["%s からのアクセスによる送信\n",
	  "This mail was sent by the access from %s\n"],
	'newpassword'	=> ["%s さんのパスワードは %s です。\n",
	  "The password of %s is %s\n"],
	'mischief'	=> ["身に覚えのない場合はMLへの代理登録の可能性があります。
上記URLが見慣れたものならばアクセスしてみるか、
このURLの管理人(%s さん)に問い合わせてみて下さい。
それらも心当たりのない場合はいたずらです。対処しますので管理人まで御連絡下さい。",
	  'If you have no idea of the reason for getting this message, '+
	  'it may be a invitation of mailing list from your friend.
Please try to access URL above if it is familiar one or
contact to the administrator of the site(is %s).
If you have completely no clue for this invitation,
it might be a mischief by someone else.  Please notice the fact
to the administrator.  Thank you.'],
	'user'		=> ['ユーザ', 'user'],
	'group'		=> ['グループ', 'group'],
        'mladdress'	=> ['公開MLアドレス(%s以外にしたい場合設定する)',
"Public ML address(if you set to diffrent address than `%s')"],
        'fromhack'	=> ['ML配送時のFrom:を常にMLのアドレスにする
(From:にしか返さないケータイ参加者が多いときにオススメ)',
'Set From: address of all ML messages to ML address, which is convenient
to keep responses from cellular phones surely to ML address.  Most cellular
phones tend to return only from: address.'],
        'inviteonly'	=> ['管理者のみ加入操作可能',
                            'Only administrators can add new members.'],
        'invite-error'	=> ['%s への加入はグループ管理者のみが操作できます。',
"Only administrator of this group(`%s') can add you."],
        'limitsender'	=> ['アカウント保持者のみ送信可能',
                            'Allow only account holders to post to ML'],
        'xmlname'	=> ['X-ML-Nameヘッダの値("%s" 以外にしたい場合設定する)',
                            'Value of X-ML-Name header ("%s" for default)'],
	'personal'	=> ['個人で', 'personal'],
	'registas'	=> ['グループ予定として登録?', 'Register as group?'],
        'headsched'	=> ['下の枠内に予定を記入: 1行以内で短めに。
長くなるときは2行目以降に詳細を。',
'Put shortest sentence as possible within 1 line.
Or, put short subject in the first line, details in latter lines.'],
	'joinquit'	=> ['入退会', 'joining/quiting'],
        'operation'	=> ['操作', 'operation'],
	'of'		=> ['の', "'s"],
	'id'		=> ['ID(英単語かローマ字の分かりやすい1単語半角空白なしで)', 'ID(without spaces)'],
	'name'		=> ['名前', 'name'],
	'anystring'	=> ['(日本語OK)', '(any length, any characters)'],
	'setto'		=> ['を設定 → ', 'set to '],
	'dupname'	=> ['あー、%sってグループ名は既にあるん素。別のにして.',
	  "Group name `%s' already exists, choose another name."],
	'management'	=> ['管理', 'management'],
	'administrator'	=> ['管理者', 'Administrator'],
	'newgroup'	=> ['新規グループ作成', 'Create new group'],
	'adminop'	=> ['管理<br>操作', "Administrative<br>operation"],
	'sendall'	=> ['一斉送信', "write to members"],
        'sendall_err'	=> ["%s ファイルで mailprefix と maildomain を定義しとかないと送れまへん。
例: mailprefix=yuuji-after5
    maildomain=gentei.org

さらに、.qmail-$mailprefix-default も以下のように用意しておこね。
| ./#{@myname} -list",
"You should define `mailprefix' and `maildomain' in %s file before
sending message to all.
(ex.) mailprefix=yuuji-after5
      maildomain=gentei.org

And then prepare .qmail-$mailprefix-default file as below.
| ./#{@myname} -list"],
        'sendall_head'	=> ['「%s」宛のメイル送信', "Send message to `%s'"],
        'sendmem_head'	=> ['「%s」さん宛のメイル送信', "Send message to `%s'"],
        'sendall_note'	=> ['メンバーへの連絡だけでなく、グループ非加入者がこれから加入する旨の通知などにも有用。',
                            "Send this message to all of group."],
        'sendall_done'	=> ['送信完了', "sending message done"],
        'body'		=> ['本文', 'Body'],
	'rcptto'	=> ['宛先', 'Recipients'],
	'member'	=> ['メンバー', 'Member'],
	'personalmode'	=> ['自分のだけ表示モード', 'Display Personal Only'],
	'normalmode'	=> ['全員分表示モード', "Display Everyone's"],
	'display'	=> ['予定表示行: ', 'Display schedule of: '],
	'nameonly'	=> ['名前のみ', 'Name Only'],
	'head5char'	=> ['先頭5文字', 'Head 5 chars'],
	'headline'	=> ['先頭1行', 'Headline only'],
	'whole'		=> ['長くても全部', 'Whole text'],
        'filter'	=> ['登録者絞込', 'Selection by Registerer'],
        'filterhelp'	=> ['表示する予定を登録者アカウント名(の一部)で絞り込みできる。
2つ以上の名前で絞りたいときはスペースで区切ればOK。', 'Can filter by (a part of)account name.
Multiple patterns delimited by spaces are acceptable.'],
        'filterreset'	=> ['絞込解除', 'Reset Selection'],
	'hldays'	=> ['最新X日分強調', 'Hilight Recent X-days'],
	'addedtogroup'	=> ['をグループに追加 →', 'added to the group:'],
	'removedfromgp'	=> ['をグループから削除:', 'removed from the group:'],
	'soleadmin'	=> ['%s は %s の唯一の管理者なのでやめられないのだ',
	  "%s is sole administrator of %s.  Cannot retire."],
	'recursewarn'	=> ['個人では加入してないが、別の加入グループがこのグループに入っているので実質参加していることになっている。',
	  'Though this member does not join to this group, it is assumed to be joining this group because other group where one joins is joined to this group.'],
	'regaddress'	=> ['登録アカウント名', 'Account id'],
	'existent'	=> ['既にあるんすよ → ', 'Already exists: '],
	'mailaddress'	=> ['通知送付先アドレス', 'Notification email address'],
        'multipleok'	=> ['<br>(スペースで区切って複数指定可)',
'<br>(Two or more addresses are OK by delimiting with space.)'],
	'weburl'	=> ['ゲストブックとかURL<br><small>(予定への反応を書いて欲しい場所)</small>', 'Your guest book URL'],
	'usermodwarn'	=> ['いちいち yes/no とか確認取らないから押したら最後、気いつけて。',
	  'This is the final decision.  Make sure and sure.'],
	'joinmyself'	=> ['自分自身が既存のグループに対して入る(IN)か出る(OUT)かを決めるのがここ。自分管理のグループに誰かを足すなら「管理操作」、新たにグループを作るなら',
	  'In this page, you can decide put yourself IN or OUT of the existing groups.  If you want to manage the member of your own group, go to'],
        'aboutgroup'	=> ['グループ %s の操作', "Operations on group `%s'"],
	'groupwarn'	=> ['自分が参加してないグループAに、自分が参加しているグループBが含まれている場合、グループAにも加入していると見なされるので気をつけよう。管理者はグループのニックネームを変えられるよ。',
	  'Though you are not member of group A, you are treated as a member of A, if you join to the group B, which is a member of A.  Think the nesting of groups carefully, please.  Group administrator can change the group nickname.'],
	'address2send'	=> ['自分が参加しているグループのメンバーリストの先頭が自分。その直後にある入力欄には、そのML宛メッセージをどの宛先に配送するかを入れられる。そう、MLごとに自分への配送先を変えられるよ。',
                            'The first entry of member list of a group to which you belongs, is you.  Entry box just after your name is for address list you want to deliver messages to that ML.  Thus, you can define different addresses for each ML.'],
        'skip:'		=> ['MLへの送信専用メイルアドレスは、アドレスの前に空白入れずに skip: を付けて登録できるよ(例: skip:hoge@example.com。ML登録メンバーのみの投稿を許すMLに送信して拒否メッセージを食らったときには、その送信アドレスを skip: つきで登録しとくとええよ。', "You can prefix `skip:' without any blanks to email address to register POST-ONLY address for the ML(eg. skip:you@example.com).  When you get rejecting message from ML which allows only members to post, try to add POST-ONLY address to your email addresses entry of that group."],
	'wholemembers'	=> ['グループ内グループを考慮した上で、現在グループ %s への通知は以下のメンバーに送られる。',
	  "Consiering the groups registered in another group, notification to the group `%s' is send to members as follows."],
	'noadmingroup'	=> ['管理できるグループはないっす',
	 "'There's no groups under your administration."],
	'nickname'	=> ['ニックネーム', 'nickname'],
	'shortnameplz'	=> ['表が崩れるほど長すぎるニックネームは嫌われるよ。短めにね。',
	  'Because nickname is displayed many times in table, shorter name is prefered.'],
	'nicknamenote'	=> ['ニックネームを消去するとデフォルト名になりんす.',
	  'Default name is displayed if you remove nickname.'],
	'nothingtodo'	=> ['って何もやることあらへんかったで',
	  'Nothing to do for this transaction.'],
	'schedlist'	=> [' and %d days Schedule list',
	  'から%d日間の予定一覧'],
	'nothing'	=> ['なんもないす', 'Nothing'],
	'sessionpswd'	=> ['セッションパスワード(これはいじらないでね)',
	  'Session Password(Do not modify this)'],
	'date'		=> ['日付', 'Date'],
	'time'		=> ['時刻指定', 'Time'],
	'publicp'	=> ['公開=yes、非公開=no', 'Public?'],
	'neednotify'	=> ['通知メイル(要らないのは消してね)',
	  'Leave lines for notification timing'],
	'schedulehere'	=> ['以下登録内容', 'Your Schedule below']
      }
    end
    keyword.collect{|k|
      if @msg[k].is_a?(Array)
	@msg[k][@lang]
      elsif @msg[k].is_a?(String)
	@msg[k]
      else
	''
      end
    }.join(['', ' '][@lang])
  end

  def importcookie()
    @cookie.keys.grep(@saveprefsregexp){|v|
      @params[v] = @params[v] || @cookie[v]
    }
    for v in %w[user passwd]
      @params[v] = @params[v] || @cookie[v]
    end
  end
  def setcookie()
    a = {}
    a['user'] = @params['user'] if @params['user']
    a['passwd'] = @params['passwd'] if @params['passwd']
    ac = gencookie("value", a, 3600*6*1)
    printf "Set-Cookie: %s\n", ac if ac
    p = {}
    @params.keys.grep(@saveprefsregexp){|v|
      p[v] = @params[v].to_s.strip if @params[v] && @params[v] > ''
    }
    c = gencookie("prefs", p, 3600*24*7)
    str = [ac, c].select{|x|x}.join("; ")
    # printf "Set-Cookie: %s\n", str if str>''
    printf "Set-Cookie: %s\n", c if c
  end

  def encode(string)		# borrowed from cgi.rb
    string.gsub(/([^ a-zA-Z0-9_.-]+)/n) do
      '%' + $1.unpack('H2' * $1.size).join('%').upcase
    end.tr(' ', '+')
  end
  def purify(string)
    string.gsub(/[\040-\177]/) {encode($&)}
  end
  def decode!(string)
    string.gsub!(/\+/, ' ')
    string.gsub!(/%(..)/){[$1.hex].pack("c")}
  end
  def decode(string)
    string.gsub(/\+/, ' ').gsub(/%(..)/){[$1.hex].pack("c")}
  end
  def escape(string)
    string.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').
      gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
  end
  def quoted(string)
    NKF.nkf('-eMQ', string)
  end
  def unquoted(string)
    NKF.nkf('-emQ', string)
  end

  def gencookie(name, a, expire)
    x = a.collect{|k, v|
      sprintf("%s=%s", k, encode(v)) if v
    }
    x.delete(nil)
    return nil if x.empty?
    str = x.join('&')
    ex = (Time.new+expire).to_s
    sprintf "%s=%s; expires=%s", name, encode(str), ex
  end

  def login(altaction = nil)
    @O.print @H.elementln("h1", nil){msg('title')}
    @O.print @H.elementln("h2", nil){msg('login')}
    format = {'method'=>'POST',
      'action'=> @myname+"?" +(altaction || "-today")}
    @O.print @H.elementln("form", format){
      @H.elementln("table", nil){
	@H.elementln("tr", nil){
	  @H.element("td", nil){msg('yourmail')} + \
	  @H.element("td", nil){
	    sprintf '<input type="text" size="%s" name="user">', @opt['size']
	  }
	} + \
	@H.elementln("tr", nil){
	  @H.element("td", nil){msg('passwd')} + \
	  @H.element("td", nil){
	    sprintf '<input type="password" size="%s" name="passwd">', @opt['size']
	  }
	}
      } + '<input type="submit" value="LOGIN">'
    }
    @O.print footer2()
  end
  def open_pm()
    begin
      PasswdMgr.new(@opt['pswddb'])
    rescue
      STDERR.printf "Cannot open pswd file [%s]\n", @opt['pswddb']
      STDERR.printf "euid=#{Process.euid}, uid=#{Process.uid}\n", @opt['pswddb']
      nil
    end
  end
  def outputError(*msg)
    @O.print @H.p(msg('error')+sprintf(*msg))
  end
  def mailaddress(user, grp=nil)
    @sc.mailaddress(user, grp)
  end
  def webpage(user)
    @sc.getuserattr(user, "webpage")
  end
  def checkauth_mail()
    return true			# temporary
  end
  def checkauth()
    if @mailmode && @params['sessionpw']
      return checkauth_mail
    end
    auth = catch(:auth) {
      unless @params['user']
	#outputError(@H.a(@myname, msg('loginfirst')))
        login(@oldargv.join('+'))
	throw :auth, nil 
      end
      unless pm=open_pm()
	outputError(msg('autherror'))
	throw :auth, nil
      end	
      user, passwd = @params['user'], @params['passwd']
      email = mailaddress(user)
      if !checkmail(user)
	outputError(msg('mailerror'))
	throw :auth, nil
      end
      if pm.userexist?(user)
	if pm.checkpasswd(user, passwd)
	  throw :auth, true
	elsif passwd == @opt['forgot']
	  newp = pm.setnewpasswd(user, @opt['pswdlen'].to_i)
	  sendMail(email, "#{@mybase} password",
		   "(#{ENV['REMOTE_ADDR']} からのアクセスによる送信)\n" +
		   @opt['url'] + "\n" +
		   "#{@mybase} 用の #{user} さんのパスワードは\n" +
		   (newp || "未定義") + "\nです。\n")
	  @O.print @H.p("#{email} 宛に送信しておきました")
	  throw :auth, nil
	else
	  outputError(msg('pswderror'))
	  @O.print @H.p(sprintf(msg('forgotguide'), @opt['forgot']))
	  throw :auth, nil
	end
      elsif passwd == ''
        # Create new user from Web-UI
	newp = pm.setnewpasswd(user, @opt['pswdlen'])
	@sc.createuser(user, user)
        putLog("New user [#{user}] created\n")
        sendMail(@opt['maintainer'], "After5 New User",
                 sprintf("URL=%s\nREMOTE_ADDR=%s\nuser=%s",
                         @opt['url'], ENV['REMOTE_ADDR'], user))
	sendMail(email, "#{@mybase} new account",
		 sprintf(msg('accessfrom'), ENV['REMOTE_ADDR']) +
		 sprintf(@opt['url']) + "\n" +
		 sprintf(msg('newpassword'), user, newp) +
		 sprintf(msg('mischief'), @opt['maintainer']) + "\n")
	@O.print @H.p(sprintf(msg('newaccount'), user))
	@O.print @H.p(@H.a(@myname, msg('login')))
	throw :auth, nil
      else
	outputError(msg('pswderror'))
	throw :auth, nil
      end
    }
    if auth
      return true
    else
      return false
    end
  end
  def safecopy(string)
    return nil unless string
    if $SAFE > 0
      cpy=''
      string.split('').each{|c|
	cpy << c[0].chr if c[0] != ?`  # `
      }
      cpy.untaint
    else
      string
    end
  end
  def checkmail(mail)
    account, domain = mail.scan(/(.*)@(.*)/)[0]
    return false unless account != nil && domain != nil
    return false unless /^[-0-9a-z_.]+$/oi =~ domain.toeuc
    domain = safecopy(domain)
    require 'socket'
    begin
      TCPSocket.gethostbyname(domain)
      return true
    rescue
      if test(?x, @opt["hostcmd"])
	open("| #{@opt['hostcmd']} -t mx #{domain}.", "r") {|ns|
	  #p ns.readlines.grep(/\d,\s*mail exchanger/)
	  return ! ns.readlines.grep(/is handled .*(by |=)\d+/).empty?
	}
      elsif test(?x, @opt["nslookup"])
	open("| #{@opt['nslookup']} -type=mx #{domain}.", "r") {|ns|
	  #p ns.readlines.grep(/\d,\s*mail exchanger/)
	  return ! ns.readlines.grep(/\d,\s*mail exchanger/).empty?
	}
      end
      return false
    end
  end # checkmail

  # Logging
  #
  def putLog(msg)
    msg += "\n" unless /\n/ =~ msg
    open(@opt["logfile"], "a+") {|lp|
      lp.print Time.now.to_s + " " + msg
    }
    msg
  end

  def sendnotify(whom, subj, body)
    users = users()
    if grepgroup(whom)
      recipients = @sc.members(whom)
    else
      recipients=[whom]
    end
    for u in recipients
      if users.grep(u)[0]
	sendMail(mailaddress(u), subj, body)
      end
    end
  end

  def dospool(dir, outhandle)
    seq=1
    seqfile=File.expand_path("seq", dir).untaint
    spooldir=File.expand_path("spool", dir).untaint
    test(?d, spooldir) or Dir.mkdir(spooldir)
    if test(?s, seqfile)
      seq=open(seqfile, "r"){|s|s.gets.to_i}
    end
    seq+=1 while test(?s, (newfile=sprintf("%s/%d", spooldir, seq)))
    open(newfile, "w") do |spoolfile|
      countdone = nil
      while line=STDIN.gets
        if !countdone && /^X-ML-Name: / =~ line.toeuc
          line += sprintf("X-Mail-Count: %d\n", seq) 
          coutndone=true
        end
        spoolfile.print line
        outhandle.print line
      end
    end
    open(seqfile, "w"){|s| s.puts seq.to_s} # update `seq' file
  end
  def mlseq(dir)
    test(?s, (seqfile = dir+"/seq")) ?
    open(seqfile, "r"){|s|s.gets.to_i+1}
    : 1
  end
  def sendMail(to, subject, body, from=nil, rcptto=nil, header={},
               thru=nil, spoolto=false)
    # rcptto should be an Array or nil
    body = NKF.nkf("-j", body) unless thru
    subject = NKF.nkf("-jM", (subject||"No subject").strip)
    to = safecopy(to)		# cleanup tainted address
    subject.gsub!(/\n/, '')
    rcptto.reject!{|i| /^(skip|off):/i =~ i} if rcptto.is_a?(Array)
    begin
      if (m=open("|-", "w"))
        header.each do |h, v|
          m.printf("%s: %s\n", h.strip, v.strip)
        end
        unless thru
          m.print "To: #{to}\n"
          from and m.print "From: #{from}\n"
          m.print "Subject: #{subject}\n"
          m.puts "Mime-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Type: Text/Plain; charset=iso-2022-jp"
          # m.puts "Date: #{Time.now.strftime("%a, %d %b %Y %T %z")}"
          m.print "\n"
        end
	m.write body
	m.close
      else
	# exec(@attr['mail'], "-s", subject, to)
        recipient = rcptto || to.split(/,\s*|\s+/)
        #p recipient
        File.umask(027)
        if spoolto && spoolto.is_a?(String) &&
            proc {
            require 'fileutils'
            begin
              test(?d, spoolto) or FileUtils.mkdir_p(spoolto, :mode => 0750)
              test(?w, spoolto)
            rescue
              nil
            end}.call &&
            (tee=open("|-", "w")) # popen should be done in if-condition
          dospool(spoolto, tee)
        else  
          if ENV['MAILCMD']
            #exec("qmail-inject", "yuuji@gentei.org", "yuuji@koeki-u.ac.jp")
            open("/tmp/body", "w") {|w| w.print STDIN.readlines.join
              w.puts "---"
              w.puts recipient.join(",\n")
              w.puts header.inspect
              w.puts "ENV: #{ENV.inspect}"
            }
            exit 0
          elsif header['Return-path'] && /-@/ =~ header['Return-path']
            # if VERP is requested
            mailcmd = ENV['MAILCMD'] || @opt['sendmail']
            contents = STDIN.readlines
            recipient.uniq.each {|r|
              local, domain = header['Return-path'].split("@")
              newlocal = local+r.sub("@", "=")
              verp = newlocal+"@"+domain
              verp = safecopy(verp)
              open("| #{mailcmd} -f#{verp} -- #{r}", "w") {|m|
                m.write contents.join
              }
            }
          else
            recipient.unshift "-f"+header['Return-path'] if header['Return-path']
            exec(ENV['MAILCMD'] || @opt['sendmail'], *recipient)
          end
        end
	exit 0;
      end
      putLog("Sent '#{subject.toeuc}' to #{to}\n")
      return true
    rescue
      putLog("FAILED! - Sent '#{subject}' to #{to}\n")
      return nil
    end
  end # sendMail

  def today()
    today = Time.now
    showtable(today)
  end
  def isleap?(y)
    if y%400 == 0
      true
    elsif y%100 == 0 || y%4 != 0
      false
    else
      true
    end
  end
  def daysofmonth(year, month)
    dl = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
    if month != 2 || !isleap?(year)
      dl[month-1]
    else
      29
    end
  end
  #
  # Return the Time object at the last day of last month
  def lastmonth(today)
    Time.at(Time.mktime(today.year, today.month)-3600*24)
  end
  #
  # Return the Time object at the first day of next month
  def nextmonth(today)
    y, m = today.year, today.month
    Time.at(Time.mktime(y, m, daysofmonth(y, m))+3600*24)
  end

  def month(month)
    y, m = month.scan(%r,(\d\d\d\d+)/(\d+),)[0]
    if y && m
      showtable(Time.mktime(y, m, 1))
    else
      outputError "%s %s", msg('invaliddate'), month
      return nil
    end
  end
  def footer1()
    "<br>" + \
    @H.element("p"){
      me = @myname+"?-"; delim = " / "
      @H.a(me+'userman', msg('user', 'management')) + delim + \
      @H.a(me+'groupman', msg('group', 'management')) + delim + \
      if /^personal/i =~ @params['displaymode']
	@H.a(me+'today_n', msg('normalmode'))
      else
	@H.a(me+'today_p', msg('personalmode'))
      end
    }
  end

  def footer2()
    "<hr>" + \
    @H.element("code") {
      "This " + \
      @H.a(@after5url, "After5") + \
      " board is maintained by " + \
      @opt['maintainer'].gsub(".", "<span>.</span>").
      sub('@', "&#x40;") + \
      '<span style="display: none;">.cut.here</span>' + \
      "."
    }
  end
  def footer()
    footer1+footer2
  end
  def header_filter()
    filter = @params['displayfilter']
    if filter && filter > ""
      myarg = (ENV['REQUEST_URI']||"-today").sub(/.*\?/, "")
      @H.elementln("form", {'action'=>@myname+"?"+myarg, "method"=>"POST"}) {
        @H.elementln("p", {"class"=>"filter"}) {
          @H.element("span") {
            msg('filter')+"="+filter
          } + \
          @H.hidden("displayfilter", "") + \
          @H.submit("GO", msg('filterreset'))
        }
      }
    else
      ""
    end
  end
  def nickname(userORgroup)
    if grepgroup(userORgroup)
      @sc.groupname(userORgroup)
    else
      @sc.nickname(userORgroup)
    end
  end
  #
  # show specified month's calendar
  def showtable(day)
    if !checkauth
      return nil
    end

    month = day.month.to_s
    first = Time.mktime(day.year, day.month, 1)
    last  = daysofmonth(day.year, day.month)
    wday1 = first.wday
    start = 1-wday1
    wname = @wnames
    today = Time.now
    todayy = today.year
    todaym = today.month
    todayd = today.day
    tdclass = {}
    tdclass["width"] = "64px" if @oldagent # workaround for NN4
    personal = /personal/ =~ @params['displaymode']
    headline = @params['headline']
    headlinehl = @params['headlinehl']
    filter = @params['displayfilter']
    hldays = headlinehl.to_i * 3600*24
    recent = {'class'=>'recent'}
    monthstr = sprintf "%d/%d", day.year, day.month
    

    holiday = Holiday.new
    # create dayofweek header
    @O.print @H.elementln("h1", nil){monthstr}
    # which mode?
    @O.print @H.p(msg(personal ? 'personalmode' : 'normalmode'))
    # @O.print @H.p(sprintf("filter=%s", filter))
    @O.print header_filter()
    # display table
    @O.print @H.startelement("table", {'border'=>"1", 'class'=>'main'})

    # day of week
    @O.print @H.startelement("tr")
    for w in wname
      @O.print @H.element("th", {'class'=>w}){w.capitalize}
    end
    @O.print "\n"+@H.endelement(nil, true)

    # create day table
    column = start
    ## p day, last
    while column <= last
      @O.print @H.elementln("tr", nil){
	(column..column+6).collect{|d|
	  todayp = (day.year==todayy && day.month==todaym && d==todayd)
	  wd=d-column
          thisday = first+(d-1)*3600*24
	  hd = holiday.isHoliday(thisday.year, thisday.month, thisday.day, wd)
	  tdclass['class'] = (hd ? 'holiday' : wname[wd])
	  @H.element("td", tdclass){
	    if d>0 
	      #date = "%d/%d/%d"%[day.year, day.month, d]
	      date = "%d/%d/%d"%[thisday.year, thisday.month, thisday.day]
	      @H.element("p", {'class'=>todayp ? 'todayline' : 'dayline'}){
		##@H.a(@myname+"?-show+"+date, "%4d"%d)
		@H.a(@myname+"?-show+"+date, "%4d"%thisday.day)
	      } + \
	      # isHoliday?
	      if hd
		@H.element("small"){hd.join("<br>")}
	      end.to_s + \
	      @H.element("p", {'class'=>'topic'}){
		s = @sc.day_all(date, @params['user'], personal, filter)
		if !s.empty?
		  s.keys.sort.collect{|time|
		    s[time].keys.sort.collect{|who|
		      text = escape(s[time][who]['sched'])
		      topic = sprintf "%s%s",
			time == @opt['alldaydir'] ? '' : time+":",
			if personal
			  (@params['user'] == who ? "" : nickname(who)+"=") +
			    text ## .split("\n")  ##[0]
			else
			  nickname(who) + \
			    if headline == 'whole'
			      '=' + text
			    elsif headline == 'head5char'
			      '=' + text.gsub(/\n/, '').sub(/(.{5}).*/, '\1')
			    elsif headline == 'headline'
			      '=' + text.split("\n")[0]
			    end.to_s
			end
		      if hldays > 0 &&
			  (today - s[time][who]['regtime']) < hldays
			topic = @H.element("span", recent){topic}
		      end
		      topic
		    }.join("<br>")
		  }.join("<br>\n")
		else
		  @opt['tdskip']
		end
	      }
	    else
	      @opt['tdskip']
	    end
	  }
	}.join
      }
      column += 7
    end

    # month-link
    @O.print @H.elementln("tr", {'class'=>'monthlink'}){
      lm1 = lastmonth(day)
      lm2 = lastmonth(lm1)
      lm3 = lastmonth(lm2)
      nm1 = nextmonth(day)
      nm2 = nextmonth(nm1)
      nm3 = nextmonth(nm2)
      [lm3, lm2, lm1, nil, nm1, nm2, nm3].collect{|t|
	@H.element("td"){
	  if t.is_a?(Time)
	    ym = sprintf("%d/%d", t.year, t.month)
	    @H.a(sprintf("%s?-month+%s", @myname, ym), ym)
	  else
	    sprintf "%d/%d", day.year, day.month
	  end
	}
      }.join("\n")
    }
    @O.print "\n"+@H.endelement(nil, true)
    
    @O.print "showtable" if @params['user'] == @author
    @O.print @H.elementln("form", {'action'=>@myname+"?-month+#{monthstr}", 'method'=>'POST'}){
      choice = [
	[msg('nameonly'), 'name'],
	[msg('head5char'), 'head5char'],
	[msg('headline'), 'headline'],
	[msg('whole'), 'whole']]
      msg('display') + \
      @H.select('headline', choice, headline) + "/" + \
      msg('hldays') + \
      @H.select('headlinehl', 0..30, headlinehl) + \
      @H.element("abbr", {"title"=>msg('filterhelp')}) {msg('filter')} + \
      @H.text('displayfilter', @params['displayfilter'], 10, 80) +\
      @H.submit("GO", "GO")
    }
    @O.print footer
    ##schedule.day_all("2003/12/22")
    # @O.print @H.endelement()
  end

  #
  # Put carrying values
  def hiddenvalues()
    h = %w[user displaymode].collect{|v|
      if @params[v]
	sprintf "<input type=\"hidden\" name=\"%s\" value=\"%s\">\n",
	  v, @params[v]
      end
    }
    h.delete(nil)
    h.join
  end
  def date2ymd(date)
    %r,(\d\d\d\d+)/(\d\d?)/(\d\d?), =~ date and
      [$1.to_i, $2.to_i, $3.to_i]
  end
  #
  # Return the string of table
  def dayTableString(user, datestr, range, personal = nil, filter = nil)
    #s = @sc.day_all(date, user, personal)
    #return '' if s.empty?
    r = ''
    header = @H.startelement("table", {'border'=>'1'}, true)

    day = Time.mktime(*date2ymd(datestr))
    i = -1
    while (i+=1) < range
      d = Time.at(day+i*3600*24)
      date = sprintf("%04d/%02d/%02d", d.year, d.month, d.day)
      datewn = @H.element("span", {'class'=>@wnames[d.wday]}){
	sprintf("%s(%s)", date, @msg['wnames'][@lang][d.wday])
      }
      s = @sc.day_all(date, user, personal, filter)
      next if s.empty?

      r << @H.element("tr", nil){
	@H.element("th", {'class'=>'time'}){'TIME'} + \
	@H.element("th", nil){'Who - '+datewn+' - What'}
      }
      for time in s.keys.sort
	tstr = case time
	       when @opt['alldaydir']
		 msg('allday')
	       else
		 sprintf "%02d:%02d", time.to_i/100, time.to_i%100
	       end
	r << @H.startelement("tr", nil, true)
	r << @H.element("th", {'class'=>'time'}){tstr}
	r << @H.element("td"){
	  @H.elementln("table"){
	    s[time].keys.collect{|who|
	      editable = (user==who || @sc.ismember(user, who))
	      groupp   = grepgroup(who)
	      @H.element("tr"){
		@H.element("td", {'class'=>groupp ? 'group' : 'who'}){
		  if !groupp && webpage(who)
		    @H.a(webpage(who), nickname(who))
		  else
		    nickname(who)
		  end
		} + \
		@H.element("td"){
		  if editable
		    s[time][who]['pub'] ? msg('public') :
		      msg('nonpublic')
		  else
		    @opt['tdskip']
		  end
		} + \
		@H.element("td"){
		  if editable
		    @H.a(@myname+"?-modify+#{date}/#{time}/#{who}",
			 msg('modify'))
		  else
		    @opt['tdskip']
		  end
		} + \
		@H.element("td"){
		  if editable
		    @H.a(@myname+"?-remove+#{date}/#{time}/#{who}",
			 msg('remove'))
		  else
		    @opt['tdskip']
		  end
		} + \
		@H.element("td"){
		  if editable
		    @H.a(@myname+"?-move+#{date}/#{time}/#{who}",
			 msg('move'))
		  else
		    @opt['tdskip']
		  end
		} + \
		@H.element("td"){escape(s[time][who]['sched'])}
	      }
	    }.join("\n")
	  }
	}
	r << @H.endelement()
      end
    end
    footer = @H.endelement()
    if r > ''
      header + r + footer
    else
      ''
    end
  end
  def dayTextString(user, datestr, range, personal = nil, filter = nil)
    r = ''
    cols = 20
    header = "-" * cols + "\n"

    day = Time.mktime(*date2ymd(datestr))
    i = -1
    while (i+=1) < range
      d = Time.at(day+i*3600*24)
      date = sprintf("%04d/%02d/%02d", d.year, d.month, d.day)
      datewn = sprintf("%s(%s)", date, @msg['wnames'][@lang][d.wday])
      s = @sc.day_all(date, user, personal, filter)
      next if s.empty?

      r << sprintf("TIME Who %s - What\n", datewn)

      for time in s.keys.sort
	tstr = case time
	       when @opt['alldaydir']
		 msg('allday')
	       else
		 sprintf "%02d:%02d", time.to_i/100, time.to_i%100
	       end
	r << s[time].keys.collect{|who|
	  editable = (user==who || @sc.ismember(user, who))
	  groupp   = grepgroup(who)
	  sprintf("%-5s %-10s %s",
                  tstr, nickname(who), escape(s[time][who]['sched']))
	}.join("\n") + "\n"
      end
      r << "-" * cols + "\n"
    end
    footer = "That's all\n"
    if r > ''
      header + r + footer
    else
      ''
    end

  end
  #
  # new form
  def displayRegistForm(date, multiple = true)
    #
    # Link button to add new plan
    #now = Time.now+3600*24
    thisyear, thismonth, thisday = date.scan(%r,(\d\d\d\d+)/(\d+)/(\d+),)[0]
    user = @params['user']
    now = Time.mktime(thisyear, thismonth, thisday.to_i, Time.now.hour)
    y, m, d, h, min = now.year, now.month, now.day, now.hour, now.min
    nextweek = Time.at(now+3600*24*7)
    ey, em, ed = nextweek.year, nextweek.month, nextweek.day
    rcsp = (multiple ? {'colspan'=>'2'} : nil)
    wnames = @msg['wnames'][@lang]
    wnames << @msg['everyday'][@lang]

    @O.print @H.element('h2', nil, true){msg('addsched')}
    @O.print @H.element('p', nil){msg('defthisday')}

    @O.print @H.element("form", {'action'=>@myname+"?-addsched", 'method'=>'POST'}){
      border1 = {'border'=>'1'}
      border1c = {'border'=>'1', 'class'=>'c'}
      mygroup = @sc.groups().select{|g|@sc.ismember(user, g)}
      @H.elementln('table', border1){
	@H.elementln('tr'){
	  @H.element('th'){'Name'} + \
	  @H.element('td', rcsp){
	    hiddenvalues() + @sc.nickname(user)
	  }
	} + \
	@H.elementln('tr'){
	  @H.element('th'){'Year'} + \
	  @H.element('td'){@H.select("year", y..y+5, y)} + \
	  if multiple
	    @H.element('td'){
	      d1 = msg('singleday')
	      msg('through')+@H.select("endyear", [d1]+(y..y+5).to_a, d1)
	    }
	  end
	} + \
	@H.elementln('tr'){
	  @H.element('th'){'Month'} + \
	  @H.element('td'){@H.select("month", 1..12, m)} + \
	  if multiple
	    @H.element('td'){
	      msg('through')+@H.select("endmonth", 1..12, em)
	    }
	  end
	} + \
	@H.elementln('tr'){
	  @H.element('th'){'Day'} + \
	  @H.element('td'){@H.select("day", 1..31, d)} + \
	  if multiple
	    @H.element('td'){
	      msg('through')+@H.select("endday", 1..31, ed)
	    }
	  end
	} + \
	if multiple
	  @H.elementln('tr'){
	    @H.element('th'){
	      msg('whichday')
	    } + \
	    @H.element('td', rcsp){
	      @H.elementln('table', border1c){
		@H.element('tr'){
		  i=-1
		  wnames.collect{|w|
		    @H.element('td'){
		      i+=1
		      @H.radio('whichday', i.to_s, '', i==wnames.length-1)
		    }
		  }.join("\n")
		} + \
		@H.element('tr'){
		  i=-1
		  wnames.collect{|w|
		    @H.element('td'){w}
		  }.join
		}
	      }
	    }
	  }
	end + \
	@H.elementln('tr'){
	  @H.element('th'){'Time<br>'+ \
	    sprintf(msg('24hour'), @opt['alldaydir'])} + \
	  @H.element('td', rcsp){
	    '<input type=text name="time" value="3000" size=8 maxlength="4">'
	  }
	} + \
	@H.elementln('tr'){
	  @H.element('th'){msg('publicok')} + \
	  @H.element('td', rcsp){
	    @H.radio('pub', 'yes', msg('yes')+'<br>', true) + \
	    @H.radio('pub', 'no', msg('no'))
	  }
	}
	## table
      } + \
      @H.elementln("p"){	# put notify mail checkbox
	msg('reqnotify') + '<br>' + \
	@ntlist.collect{|n, v|
	  # Actual variables of notifylist for submitting is "sub_"+n
	  @H.checkbox("sub_"+n, 'yes', v, @params[n])
	}.join("\n") + \
	" " + @H.checkbox('rightnow', 'yes', msg('rightnow'), true) + \
	"\n"
      } + \
      if mygroup[0]
	@H.elementln("p"){	# put "register as"
	  msg('registas') + "<br>\n" + \
	  mygroup.collect{|g|
	    @H.radio('registas', g, @sc.groupname(g))
	  }.join(' ') + "\n/ " + \
	  @H.radio('registas', 'no', msg('personal'))
	}
      end.to_s + "\n" + \
      @H.radio('editmode', 'remove', 'Delete?') + " / " + \
      @H.radio('editmode', 'modify', 'Overwrite?') + " / " + \
      @H.radio('editmode', 'append', 'Append?', true) + "<br>\n" + \
      @H.element("p"){msg('headsched') + "<br>\n" + \
        @H.element("textarea", @schedulearea){}} + 	# textarea
      @H.submit_reset("GO")
    } #form
  end
  #
  # show the schedule list of specified date
  #
  def show(date)
    if !checkauth
      return nil
    end
    user = safecopy(@params['user'])
    personal = (/^personal/i =~ @params['displaymode'])
    @params['displaydays'] = @params['displaydays'] || @cookie['displaydays']
    filter = @params['displayfilter']
    days = @params['displaydays'].to_i
    days = (days > 0 ? days : 3)

    outstr = dayTableString(user, date, days, personal, filter)

    @O.print @H.element("h1", nil){
      sprintf msg('fmtdaysschedule'), date
    }
    @O.print @H.element("h2"){msg('schedtable')}
    ## @O.print @H.p()
    @O.print header_filter()
    @O.print @H.elementln("form", {'action'=>@myname+"?-show+#{date}", 'method'=>'POST'}){
      @H.elementln("p"){
	msg(personal ? 'personalmode' : 'normalmode') + "<br>" + \
	@H.select("displaydays", 1..30, days) + msg('daystodisplay') + \
	@H.submit("GO", "GO")
      }
    }
    if outstr > ''
      @O.print outstr
    else
      @O.print @H.p(msg('noplan'))
    end #is_empty?
    thisyear, thismonth, thisday = date.scan(%r,(\d\d\d\d+)/(\d+)/(\d+),)[0]
    mstr = sprintf "%04d/%02d", thisyear.to_i, thismonth.to_i
    @O.print @H.a(@myname+"?-month+"+mstr,
		  sprintf(msg('tomonthlist'), mstr))


    #
    # Display registration form
    displayRegistForm(date)
    @O.print "show" if user == @author
  end

  #
  # call process
  def call_process(cmd, input=nil, timeout=10)
    prc = CMDTimeout.new
    fds = prc.start(cmd, timeout, true)
    if input
      Thread.start {
	fds[0].sync = true
	fds[0].print.input
	fds[0]
      }
    end
    begin
      fds[1].readlines
    ensure
      prc.close()
    end
  end
  #
  # notification registerer
  def notify_time(year, month, day, time, symbol)
    if (t = time.to_i) > 2359
      hh = mm = 0
    else
      hh, mm = t/100, t%100
    end
    base = Time.mktime(year.to_i, month.to_i, day.to_i, hh, mm)
    if /nt(\d+)([mh])$/ =~ symbol
      return nil if t > 2359
      num, unit = $1.to_i, $2.downcase
      rate = {'h'=>3600, 'm'=>60}[unit] || 3600
      return Time.at(base-rate*num)
    elsif /nt(\d+)d/ =~ symbol
      seconds = $1.to_i*3600*24
      tday= Time.at(base-seconds)
      target = [tday.year, tday.month, tday.day, @opt['night'].to_i]
      targetnight = Time.mktime(*target)
    elsif "nttoday" == symbol
      Time.mktime(year.to_i, month.to_i, day.to_i, @opt['morning'])
    end
  end
  def reg_notify(user, year, month, day, time, text, cancelall = nil)
    return nil unless @opt['notifymail']
    threshold = 5*60		# Omit notifycation within 30min future

    y, m, d, t, = year.to_i, month.to_i, day.to_i, time.to_i
    if t > 2359
      hh = mm = 0
    else
      hh = t/100
      mm = t%100
    end
    now = Time.now
    
    filearg = [user, year, month, day, t]
    @ntlist.each{|k, v|
      # @params[k]s are always defined in cookies, so we use @params["sub_"+k]
      @params[k] = @params["sub_"+k]
      nt_time = notify_time(year, month, day, t, k)
      if !nt_time
	# do nothing for allday schedule's notification before some minutes
      elsif cancelall || nt_time < now+threshold ||
	  /yes|on|true|1/ !~ @params[k] || !@params[k]
	# cancel
	uf = @sc.remove_crondir(nt_time, user, year, month, day, t)
	@sc.removefile(*(filearg+[k]))
      else
	# register
	lf = @sc.register_crondir(nt_time, user, year, month, day, t)
	@sc.putfile(*(filearg+[k, lf]))
      end
    }
  end
  def cancel_notify(user, year, month, day, time)
    reg_notify(user, year, month, day, time, 'dummy', true)
  end

  def commit_schedule(who, y, m, d, timedir, text, repl, pub)

  end

  def regulate_time(y, m, d, tm)
    if tm > 2399
      sh, smin = 23, 59
      timedir=@opt['alldaydir']
      tmstr = msg('allday')
    else
      sh = (tm/100).to_i
      smin = (tm%100).to_i
      timedir = sprintf("%04d", tm)
      tmstr = sprintf("%d:%02d", sh, smin)
    end
    time = nil
    begin
      time = Time.mktime(y, m, d, sh, smin)
    rescue
      outputError "%s<br>\nyear=%s<br>month=%s<br>day=%s<br>time=%s\n",
	msg('invaliddate'),
	@params['year'], @params['month'], @params['day'], @params['time']
      return nil
    end
    [time, timedir, tmstr]
  end

  #
  # add or remove a schedule
  #
  def add_remove(remove = nil)
    if !checkauth
      return nil
    end
    user = registerer = @params['user']
    as = @params['registas']
    if as && as > '' && /^no$/ !~ as && @sc.ismember(user, as)
      if (gr=grepgroup(as))
	registerer = gr
      end
    end
    now = Time.now
    #y, m, d, h, min = now.year, now.month, now.day, now.hour, now.min

    $KCODE='e' if $DEBUG
    @O.print @params.inspect if $DEBUG
    #
    # Check the validity of specified time
    sy = @params['year'].to_i
    sm = @params['month'].to_i
    sd = @params['day'].to_i
    tm = @params['time'].to_i

    time, timedir, tmstr = regulate_time(sy, sm, sd, tm)

    #
    # Check continuous schedule registration
    wwday = @params['whichday'].to_i
    if @params['endyear'] && @params['endmonth'] && @params['endday'] &&
	(ey=@params['endyear'].to_i) > 0 &&
	(em=@params['endmonth'].to_i) > 0 &&
	(ed=@params['endday'].to_i) > 0
      daylist = []
      endtime = Time.mktime(ey, em, ed, 23, 59)
      ti = time
      begin
	if wwday==7 || wwday==ti.wday
	  daylist << [ti.year, ti.month, ti.day]
	end
      end while (ti=Time.at(ti+3600*24)) <= endtime
    else
      daylist = [[sy, sm, sd]]
    end

    if !remove && !(@params['schedule'] && @params['schedule'].strip > '')
      outputError msg('putsomething')
      return nil
    end


    for y, m, d in daylist
      # do remove or addition
      if remove
	cancel_notify(registerer, y, m, d, timedir)
	begin
	  @sc.remove(registerer, y, m, d, timedir)
	  #########@O.print @H.p(msg('remove')+msg('done'))
	rescue
	  outputError("Failed"+$!)
	end
      else
	if time < now
	  outputError(msg('past'))
	  return nil
	end
	begin

	  (text = @params['schedule'].toeuc.strip.gsub(/\r+\n/, $/)) << "\n"
	  # text = purify(text)
          STDERR.print text
	  replace = (/modify/i =~ @params['editmode'])
	  rc = @sc.register(registerer, y, m, d, timedir, text, replace)
	  if @params['pub'] && /yes/ =~ @params['pub']
	    @sc.putfile(registerer, y, m, d, timedir, 'pub', "1\n")
	  else
	    @sc.removefile(registerer, y, m, d, timedir, 'pub')
	  end
	  ########  @O.print @H.p(msg('appended')) if rc == 1
	rescue
	  outputError("Failed"+$!)
	end
	text = @sc.getschedule(registerer, y, m, d, timedir)
	reg_notify(registerer, y, m, d, timedir, text)

      end

    end

    if !remove && @params['rightnow'] && /yes/i =~ @params['rightnow']
      header = sprintf("%s\n%s/%s/%s%s %s %s\n%s%s%s\n%s\n",
		       @opt['url'],
		       sy, sm, sd,
		       if daylist.length > 1
			 "-%s/%s/%s" % daylist[-1]
		       end,
		       tmstr, msg('immediatenote'),
		       msg('registerer_is'), nickname(registerer),
		       if user!=registerer
			 sprintf(" (%s%s)",
				 msg('registerer'), nickname(user))
		       else
			 ""
		       end,
		       "-"*70)
      sendnotify(registerer, "Registration completed", header+text)
    end
    unless @mailmode
      show(sprintf("%04d/%02d/%02d", sy, sm, sd))
      @O.print "add_remove" if user == @author
    end
  end

  # add
  def addsched()
    if "move" == @params['editmode']
      add_remove(:remove)
      for p in %w(year month day time) do
	@params[p] = @params["new"+p]
      end
    end
    add_remove(/^remove/i =~ @params['editmode'])
  end

  #
  # Display remove or modify screen
  def remove_modify(datetime, editmode)
    if !checkauth
      return nil
    end

    user = @params['user']
    y, m, d, time, dummy, as =
      datetime.scan(%r,(\d\d\d\d+)/(\d+)/(\d+)/(\d+)(/(.+))?,)[0]
    # datetime always contains trailing slash generated by parsedate
    # but if the trailing part is a user(not a group), it is removed
    # because it filtered out by grepgroup() function
    if ! (y && m && d && time)
      outputError "Invalid time specification"
      return nil
    elsif as && as > ''
      unless @sc.ismember(user, as)
	outputError "You have no permission to edit group %s's schedule", as
	return nil
      end
      user = as
    end
    unless text=@sc.getschedule(user, y, m, d, time)
      outputError "%s %s", datetime, msg('noplan')
      return nil
    end
    ## text = decode(text)
    @O.print @H.elementln("h1"){
      sprintf "%s %s", datetime, msg(editmode)
    }
    @O.print @H.elementln("form", {'action'=>@myname+"?-addsched", 'method'=>'POST'}){
      pubp=(@sc.getfile(user, y, m, d, time, 'pub').to_i > 0)
      if as
	@H.hidden("registas", as)
      end.to_s + \
      "<input type=\"hidden\" name=\"year\" value=\"%04d\">\n" % y.to_i + \
      "<input type=\"hidden\" name=\"month\" value=\"%02d\">\n" % m.to_i + \
      "<input type=\"hidden\" name=\"day\" value=\"%02d\">\n" % d.to_i + \
      "<input type=\"hidden\" name=\"time\" value=\"%04d\">\n" % time.to_i + \
      if editmode=="move"
	@H.elementln("table") {
	  @H.elementln("tr", {"colspan" => "2"}) {msg('newdate')} + \
	  @H.elementln("tr") {
	    @H.element("th"){"Year"} + \
	    @H.element("td"){@H.select("newyear", y.to_i..y.to_i+5, y)}
	  } + \
	  @H.elementln("tr") {
	    @H.element("th"){"Month"} + \
	    @H.element("td"){@H.select("newmonth", 1..12, m)}
	  } + \
	  @H.elementln("tr") {
	    @H.element("th"){"Day"} + \
	    @H.element("td"){@H.select("newday", 1..31, d)}
	  } + \
	  @H.elementln("tr") {
	    @H.element("th"){"Time"} + \
	    @H.element("td"){
	      "<input type=text name=\"newtime\" value=\"#{time}\" " + \
	      "size=\"8\" maxlength=\"4\">"
	    }
	  }
	}
      end.to_s + \
      @H.elementln("div", {"style" =>
		     "visibility: " +
		     (editmode=="move" ? "hidden" : "show") + "\""}) {
	msg('reqnotify') + "<br>\n" + \
	@ntlist.collect{|nt, v|
	  cronp = @sc.getfile(user, y, m, d, time, nt)
	  sprintf "<input type=\"checkbox\" name=\"sub_%s\"%s>%s \n",
	    nt, (cronp ? " checked" : ""), v
	}.join + "<br>"
      } + \
      @H.element("textarea", @schedulearea) {text} + "<br>" + \
      @H.radio("editmode", "append", msg('append')) + ' / ' + \
      @H.radio("editmode", "modify", msg('modify'), editmode=="modify")+' / '+\
      @H.radio("editmode", "remove", msg('remove'), editmode=="remove")+' / '+\
      @H.radio("editmode", "move", msg('move'), editmode=="move") + ' / ' + \
      "<br>\n" + \
      msg('publicok') + \
      @H.radio("pub", "yes", msg('yes'), pubp) + \
      @H.radio("pub", "no", msg('no'), !pubp) + \
      '<br>' + \
      @H.submit_reset("GO")
    }
    @O.print "remove_modify" if user == @author
  end
  def remove(datetime)
    remove_modify(datetime, "remove")
  end
  def modify(datetime)
    remove_modify(datetime, "modify")
  end
  def move(datetime)
    remove_modify(datetime, "move")
  end

  def prohibitviahttp()
    %w[REMOTE_ADDR REMOTE_HOST SERVER_NAME].each{|v|
      if ENV[v]
	print "Content-type: text/plain\n\n"
	print "Do not call this via CGI"
	exit 0
      end
    }
  end
  #
  # notify: call via cron
  def notify()
    prohibitviahttp()
    unless @opt['maintainer']
      STDERR.printf "Set maintainer(email-address) in %s\n", @opt['conf']
      STDERR.print "(ex.)  maintainer=yuuji@gentei.org\n"
      exit 0
    end
    Dir.chdir @mydir
    line = "-"*25
    indent = "    "
    now = Time.now
    ntlist = @sc.notify_list(now)
    p "notifylist", ntlist if $DEBUG
    ntlist.each{|u, datehash|
      dellist = []
      content = datehash.sort.collect{|date, filehash|
	next unless /(\d\d\d\d+)-(\d+)-(\d+)-(\d\d\d\d)/ =~ date
	y, m, d, t = $1.to_i, $2.to_i, $3.to_i, $4.to_i
	ddiff=(Time.mktime(y, m, d) \
	       - Time.mktime(now.year, now.month, now.day))/3600/24
	if t > 2359
	  hhmm = msg('allday')
	  if ddiff > 1
	    comment = "%d%s" % [ddiff, msg('days', 'before')]
	  else
	    comment = msg(now.hour > 18 ? 'precedingday' : 'theday')
	  end
	else
	  hhmm = sprintf "%02d:%02d", t/100, t%100
	  diff = Time.mktime(y, m, d, t/100, t%100) - now
	  if diff < 7200
	    comment = "%d%s" % [diff/60, msg('minutes', 'before')]
	  elsif (ddiff == 0)
	    comment = "%s%d%s" %
	      [msg('about'), diff/3600, msg('hours', 'before')]
	  else
	    comment = "%d%s" % [ddiff, msg('days', 'before')]
	  end
	end
	dellist << filehash['file']
	sprintf("%s[[[%d/%d/%d %s]]]%s\n", line, y, m, d, hhmm, line) + \
	sprintf("(%s %s)\n", comment, msg('notification')) + \
	indent+filehash['text'].join(indent) + "\n\n"
      }
      # content.delete(nil)
      if content
	if $DEBUG
	  print content
	else
	  content.unshift(msg('introduce')+"\n"+msg('notifymail')+"\n")
	  content.unshift(@opt['url'].to_s+"\n")
	  if sendnotify(u, msg('notifysubj'), content.join)
	    # send mail completed
	    begin
	      @sc.cleanup_files(dellist)
	    rescue
	    end
	  end
	end
      end
    }
    if !(list = @sc.notify_list(now)).empty?
      subj = @mybase+": Undeleted old cron files detected"
      files = list.collect{|who, whash|
	whash.sort.collect{|date, fhash| fhash['file']}.join("\n")
      }.join("\n")
      sendMail(@opt['maintainer'], subj,
	       "This is `#{@mybase}' in #{@mydir}\n" +
	       "You'd better check and remove these files.\n\n"+files)
    end
    
    exit 0
  end
  # ML functions
  def parseaddress(spec)        # from catchup.rb
    # Return [email, comment]
    # nil if comment does not exitst.
    if /(.*)\s*<(.*)>/ =~ spec then
      [$2, $1.strip]
    elsif /(.*)\s*\((.*)\)/ =~ spec then
      [$1.strip, $2]
    else
      [spec.strip, nil]
    end
  end
  def rewritefrom(email, comment, newseed) # from catchup.rb
    # no need to setcomment here because if comment set, it's enough
    comment.sub!(/(\"?)(.*)\1/, '\2')
    quote = $1
    comment += "/" if comment>""
    return quote +
           comment.gsub(/([^\x00-\x7f]+)/){NKF.nkf('-jM', $1)} +
           email.sub("@", "=") +
           quote + " <"+newseed+">"
  end

  def tagify_subj(body, tag, removeregexp, fromhack = nil)
    # This method should be generic for other headers than `Subject'?
    hold = []
    ret = []
    skip = false
    mailcountheader = 0
    while line = body.shift
      case line.toeuc
  # #Below does not work correctly when (from|subject): is final line
  #    when /^$/
  #      hold << "\n"
  #      break
  #    ## when /^(subject|from): /i
      when /^(\S+): /i, /^$/            # if new header comes or header ends
        if /^subject:/i =~ hold[0] # check previous header in hold space
          sj = hold.join.toeuc.sub("Subject: ", "").gsub(tag, "").strip
          removeregexp && sj && sj.gsub!(removeregexp, "")
          sj = sj.sub(/^(re: *)+/i, "Re: ").gsub("\n", "")
          hold = ["Subject: "+NKF.nkf('-jM', tag+" "+sj).strip+"\n"]
        elsif /^from/i =~ hold[0] && fromhack.is_a?(String)
          from = hold.join.toeuc.sub(/From: */i, "").strip
          email, comment = parseaddress(from)
          if (!comment || comment=="")
            if comment = @sc.ismembersemail(email)
              # Reverse conversion of uname<->email
              comment = @sc.nickname(comment) || "whoareyou"
            else
              comment = email		# email from alien
            end
          end
          hold = ["From: "+rewritefrom(email, comment, fromhack)+"\n"]
        elsif /^x-mail-count:/i =~ line
          if (mailcountheader += 1) > 2
            # Maybe loop!  Silently finish
            STDERR.puts("Too many X-Mail-Count(maybe loop)")
            exit 0			# XXXXXXXX
          end
        elsif /^received:/i =~ line
          # Put original received headers aside to reduce hop count
          line = "X-pre"+line
        end
        if /^$/ =~ line.toeuc
          hold << line
          break
        end
        ret += hold
        hold = [line]
      when /^\s/                # continued line
        hold << line
      end
    end
    ret + hold + body
  end
  def recompose_multipart(string)
    /(\r?)\n\r?\n/ =~ string or return string
    $1 > "" ? eol = "\r\n" : eol = "\n"
    head, body = $`, $'
    r = Hash.new
    if %r,^Content-type:\s*text/plain,i =~ head
      # open("/tmp/a", "a"){|a| a.printf("str=<<<%s>>>\n\n", string)}
      r["name"] = ":TEXT"
      r["value"] = body.sub(Regexp.new(".*?"+eol+eol, Regexp::MULTILINE), "")
      return [head, r]
    elsif %r,^Content-type:\s*(multipart/(.*));\s*boundary=([\'\"])?(.*)\3,mi =~
        head
      ct, boundary = $1, $4
      [head] + body.split(eol+"--"+boundary).collect {|pc|
        recompose_multipart(pc)
      }
    elsif %r,^Content-type:\s*(.*);\s*name=([\'\"])?(\S*)\2,mi =~ head
      r["name"] = $3
      %r,filename=([\'\"])?(\S+)\1,mi =~ head
      r["filename"] = $2
      content = body.sub(Regexp.new(".*?"+eol+eol, Regexp::MULTILINE), "")
      r['value'] = content
      return [head, r]
    end
  end
  def extract_attachment(attachment)
    # Must return [text, href] strings to attached files
    # Argument ATTACHMENT is a array of attached files.
    # Each element is an Hash of a form of:
    #	{'filename'=>name, 'value'=>content, 'content-type'=>contentType}
    href = ""; text = ""
    dir = @attachmentdir + Time.now.strftime("/%Y%m")
    if %r,(https?://[^/]+), =~ @opt['url']
      server = $1
    else
      msg = "`url' not set in after5.cf"
      return [msg, msg]
    end
    kakasi = @opt['kakasi'] && @opt['kakasi']+" -Ha -Ka -Ja -Ea -ka"
    urlbase = sprintf("%s%s/%s",
                      server, File.dirname(ENV['SCRIPT_NAME']), dir)
    count=0
    attachment.each {|a|
      basename = safecopy(File.basename(a['filename']))
      encname = (kakasi ?
                 IO.popen(kakasi, "r+") do |k|
                   k.puts basename
                   k.close_write         # force flush
                   k.gets.chomp.downcase
                 end
                 :
                 basename
                 ).gsub(/([^-_0-9a-z=,.@])/i){"~"+$1.unpack("H*")[0].to_s}
      filename = safecopy(dir+"/"+encname)
      umask = File.umask(022)
      sz = a['value'].bytesize
      next if sz == 0
      count += 1
      if sz > @attachmentmax
        msg = sprintf("%d: %s is too large(%dMB), skipped\n",
                        count, basename, sz/1024**2)
        text += msg; href += msg
        next
      end
      begin
        require 'fileutils'
        (test(?d, dir) && test(?w, dir)) or FileUtils.mkdir_p(dir)
        open(filename, "w") {|x| x.write a['value']}
        File.chmod(0664, filename)
        text += sprintf("%d: %s/%s\n", count, urlbase, encname)
        href += sprintf("%d: <a href=\"%s\">%s/%s</a>\n",
                        count, filename, urlbase, encname)
      rescue
      ensure
        File.umask(umask)
      end
    }
    if count>0
      text="\n\n[[Attached files below]]\n"+text
      href="[[Attached files]]\n"+href
    end
    [text.chomp, href.chomp]
  end
  def defaultmladdress(name)
    prefix = (@opt['mailprefix'] || "")
    dash = prefix > '' ? "-" : ""
    sprintf("%s%s%s@%s", prefix, dash, name, @opt['maildomain'])
  end
  def list()
    # For debug:
    #  LOCAL=1 DEFAULT=name ./after5.rb -list
    # $DEFAULT is ML name
    viamail = ENV['LOCAL'] && ENV['DEFAULT'] # called via mail
    from = toadmin = groupmode = fromhack = extract_report = nil
    unless @opt['mailprefix'] && @opt['maildomain']
      if viamail
        STDERR.print msg('sendall_err') % [@opt['conf']]
        exit 0
      else
        @O.print @H.elementln("pre"){msg('sendall_err') % [@opt['conf']]}
        return true
      end
    end
    if viamail then
      prohibitviahttp()
      name = unquoted(ENV['DEFAULT'])
      user = @sc.ismembersemail(ENV['SENDER']) # should here be (,name)??
      if Regexp.new("(.*)("+Regexp.quote(@mailadmsuffix)+")") =~ name
        # To: GROUP/adm*@domain
        #   -> Forward to group administrator(s)
        name, toadmin = $1, $2
        sendMail("dummy", 'dummy', # Original To: and Subject: go through
                 STDIN.readlines.join, nil, @sc.admins(name),
                 {"Return-path" => @opt['maintainer']}, :thru)
        exit 0
      end
      if @sc.isuser(name)	# groupmode = nil
        # First, compare with username
        # groupmode = nil
        # Then, check group name
      elsif grepgroup(name)
        groupmode = true
      else                      # not found
        sendMail(@opt['maintainer'], "no group",
                 sprintf("Invalid group address: %s(%s@%s)\nSent by %s\n" +
                         "URL: %s\n------------\n",
                         name, ENV['LOCAL'], ENV['HOST'], ENV['SENDER'],
                         @opt['url']) +
                 "> "+STDIN.readlines.join("> "))
        exit 0                  # should exit 0 in mail mode
      end
    else                        # via http
      return nil unless checkauth
      # HERE CGI Params name, subject, attachment and body
      # comes with enctype=multipart/form-data,
      # So we get them via Array
      name = unquoted(@params['name'][0]['value'].untaint)
      user = @params['user']
      if @sc.isuser(name)
        # groupmode = nil
      elsif grepgroup(name)
        groupmode = true
      else
        @O.print @H.p("No such group: #{name}")
        return true
      end
      nick = @sc.nickname(user)
      from = sprintf("%s <%s>", nick, user)
      subj = @params['subject'][0]['value'].toeuc || "Message from "+@myname
      body = @params['body'][0]['value'].gsub("\r", "").untaint
      # Extract attachment file
      if @params['attachment'].is_a?(Array)
        body += (extract_report = extract_attachment(@params['attachment']))[0]
      end
    end
    # Variables here
    #  name:		Destination group/user name
    #  user:		Member account or nil(not member)
    
    # Set values for header rewriting
    # We have to setup these variables:
    #  to:		Header To:
    #  subj:		Header Subject:
    #  from:		Header From:
    #  rcpts:		Array of recipients addresses
    #  header:		Hash of additional header values
    #  body:		String of mail body
    #  spooling:	Flag if spool ML files nor not
    #  mldir:		spooling directory name
    if groupmode                # Run as ML
      # To: should be ML address
      # Return-path: should be verp of PREFIX-RCPT@DOMAIN
      bracket = @sc.getgroupattr(name, 'subjtag') || @opt['mailbracket']
      xmlname = @sc.getgroupattr(name, 'xmlname') || name
      mldir = @mlbasedir+"/"+name
      to = @sc.getgroupattr(name, 'mladdress') || defaultmladdress(name)
      if @sc.getgroupattr(name, 'fromhack')
        fromhack = to
      end
      spooling = @opt['mlspooling']
      returnpath = to.sub("@", @mailadmsuffix+"-@")
    else                        # Run as p2p mail
      # To: should be Destination user name
      # Return-path: should be PREFIX-USER@DOMAIN or $SENDER(not member)
      bracket = "NONE"          # Through Subject
      if user
        returnpath = defaultmladdress(quoted(user))
      else
        returnpath = ENV['SENDER']
      end
      ###fromhack = sprintf("%s <%s>", nick, sender)
      fromhack = returnpath
      
      xmlname = name
      to = name
      spooling = mldir = nil
    end
    adminaddr  = to.sub("@", @mailadmsuffix+"@")
    if bracket == "NONE"
      sjtag = ""
      tagre = nil
    else
      sjtag = bracket.gsub("%n", nickname(name)).
        gsub("%i", name).
        gsub(/%(\d*)c/){("%0"+$1+"d") % [mlseq(mldir)]}
      tagpt = Regexp.quote(bracket). # compute bracket pattern
        gsub("%n", Regexp.quote(nickname(name))).
        gsub("%i", Regexp.quote(name)).
        gsub(/%(\d*)c/, '\d+')
      tagre = Regexp.new(tagpt)
      if !viamail
        subj = sjtag.strip+" "+subj.gsub(Regexp.new(tagpt), "")
      end
    end
    if viamail then
      body = tagify_subj(STDIN.readlines, sjtag, tagre, fromhack).join
    elsif fromhack              # via http
      from = rewritefrom(user, nick, groupmode ? to : returnpath)
    end
    header = {
      "X-ML-Driver" => ($hgid || @myname),
      "X-ML-Driver-URI" => $myurl,
      "Return-path" => returnpath
    }
    if groupmode
      header["Reply-to"] = to
      header["X-ML-Name"] = xmlname
      header["X-ML-URI"] = sprintf("%s?-groupman+%s", @opt['url'], name)
    end
    Dir.chdir @mydir
    if groupmode                # (#includeself)
      rcpts = if viamail
                @sc.members(name)
              else
                @sc.members(name) + [user]
              end.collect {|u|
        mailaddress(u, name).split(/,\s*|\s+/)}.flatten.uniq
    else
      rcpts = @sc.mailaddress(name).split(/,\s*|\s+/).flatten
      if user && !viamail	# HTTP mode
        rcpts += @sc.mailaddress(user).split(/,\s*|\s+/).flatten # +sender
      end
    end
    # ENV["QMAILINJECT"] = "r"    # for ML mode, use verp
    # For vodafone, QMAILINJECT=r doesn't work correctly
    # On mail mode, check if sender can send message to list.
    if viamail && @sc.getgroupattr(name, 'limitsender')
      s = ENV['SENDER']
      if !catch(:senderok) {
          throw :senderok, true if rcpts.grep(s)[0]
          throw :senderok, true if @sc.ismembersemail(s, name)
        }
        # sender is not allowed to send to ML
        sendMail(s, "You are not allowed to send to this ML",
                 ("Before posting to this list(%s),\n"+
                  "subscribe to %s") % [to, @opt['url']],
                 adminaddr, nil, {"Return-path" => returnpath})
        exit 0
      end
    end
    # 
    # OK to send, go ahead
    sendMail(to, subj, body, from, rcpts,
             header,
             ENV['SENDER'],
             spooling ? mldir : nil)
    if !viamail then
      @O.print @H.elementln("h1"){msg('sendall_done')}
      @O.print @H.p(sprintf(msg(groupmode ? 'sendall_head' : 'sendmem_head'),
                            nickname(name))+" "+msg('done'))
      @O.print @H.elementln("pre"){extract_report[1].toeuc}
      link2home()
      @O.print footer()
      return true
    end
    exit 0
  end
  def listdraft(name)
    return nil unless checkauth
    return nil unless name
    unless @opt['mailprefix'] && @opt['maildomain']
      @O.print @H.elementln("pre"){msg('sendall_err') % [@opt['conf']]}
      return true
    end
    
    user=@params['user']
    nickname = @sc.nickname(user)
    groupmode = @sc.isgroup(name)
    @O.print @H.elementln("h1") {
      @mybase+' '+msg('sendall').sub("<br>", " ")
    }
    @O.print @H.elementln("h2") {
      sprintf(msg(groupmode ? 'sendall_head' : 'sendmem_head'), nickname(name))
    }
    if groupmode                # (#includeself)
      list = @sc.members(name)	# +user
    else
      list = [name, user]
    end
    @O.print @H.p(sprintf("%s: %s", msg('rcptto'),
                          list.collect {|u|
                            @H.element("abbr", "title"=>u){
                              @sc.nickname(u)
                            }
                          }.join(",\n")))
    @O.print @H.p(sprintf("(total %d)", list.length))+"\n"
    @O.print \
    @H.elementln("form", {
                   'action' => @myname+'?-list', 'method'=>"POST",
                   'enctype' => "multipart/form-data"}) {
      @H.elementln("table"){
        @H.elementln("tr"){
          @H.element("td"){"To"} + \
          @H.element("td"){
            @H.element("code"){
              groupmode ? defaultmladdress(quoted(name)) : @sc.nickname(name)
            }
          }
        } + \
        @H.elementln("tr"){
          @H.element("td"){"Subject"} + \
          @H.element("td"){
            @H.text("subject", "", 40, 128)
          }
        } + \
        @H.elementln("tr"){
          @H.element("td"){"Attachment"} + \
          @H.element("td"){
            "<input name=\"attachment\" type=\"file\" multiple>"
          }
        } + \
        @H.elementln("tr"){
          @H.element("td"){
            msg('body')
          } + \
          @H.element("td"){
            @H.element("textarea", @schedulearea.merge({"name"=>"body"})){}
          }
        }
      } +	# </table>
      @H.hidden("name", name) +
      @H.submit("send", "SEND") +
      @H.reset("clear", "Clear")
    }
    @O.print @H.p(msg('sendall_note'))
  end

  # put Link to home
  def link2home()
    @O.print @H.p("-&gt; " + @H.a(@myname+"?-today", "Home"))
  end

  #
  # user management
  def userman()
    if !checkauth
      return nil
    end
    user=@params['user']
    nickname = @sc.nickname(user)
    tdclass = {}
    tdclass["width"] = "80px" if @oldagent # workaround for NN4

    @O.print @H.elementln("h1"){
      @mybase+' '+msg('user', 'management')
    }
    @O.print @H.p(@sc.mkusermap.inspect) if $DEBUG
    @O.print @H.p(msg('usermodwarn'))
    @O.print \
    @H.elementln("form", {'action'=>@myname+"?-usermod", 'method'=>'POST'}){
      @H.elementln("table", {"class" => "border"}){
	@H.elementln("tr"){
	  @H.element("td", tdclass) {msg('regaddress')} + \
	  @H.element("td") {
	    @H.element("code"){user}
	  }
	} + \
	@H.elementln("tr"){
	  @H.element("td", tdclass) {msg('mailaddress', 'multipleok')} + \
	  @H.element("td") {
	    @H.text("newmail", mailaddress(user), @opt['size'], 180)
	  }
	} + \
	@H.elementln("tr"){
	  @H.element("td", tdclass) {msg('weburl')} + \
	  @H.element("td") {
	    @H.text("webpage", webpage(user), @opt['size'], 80)
	  }
	} + \
	@H.elementln("tr"){
	  @H.element("td") {msg('nickname')} + \
	  @H.element("td") {
	    @H.text("nickname", nickname, @opt['size'], 10)
	  }
	}
      } + \
      @H.elementln("p"){msg('shortnameplz')} + \
      '<br>' + \
      @H.submit_reset("GO")
    } # form

    #
    # Next section, REMOVE USER!
    @O.print @H.elementln("h2"){
      sprintf "%s %s %s", msg('user'), user, msg('deletion')
    }
    @O.print @H.p(msg('deletionwarn'))+"\n"
    @O.print @H.elementln("form", {'action'=>@myname+"?-delusersub+#{user}", 'method'=>'POST'}){
      @H.hidden("user", user) + "\n" + \
      @H.elementln("table"){
	@H.elementln("tr"){
	  @H.elementln("td"){
	    sprintf msg('deluser'), user
	  } + \
	  @H.elementln("td"){
	    @H.radio("delete", "yes", msg('yes')) + ' ' + \
	    @H.radio("delete", "no", msg('no'), true)
	  }
	} + \
	@H.elementln("tr"){
	  @H.elementln("td"){
	    sprintf msg('really?'), user
	  } + \
	  @H.elementln("td"){
	    @H.radio("delete2", "yes", msg('yes')) + ' ' + \
	    @H.radio("delete2", "no", msg('no'), true)
	  }
	}
      } + \
      "<br>\n" + @H.element("span", "class"=>"danger"){@H.submit_reset("OK")}
    }

    @O.print footer()
  end
  def usermod()
    if !checkauth
      return nil
    end
    @O.print @H.elementln("h1"){
      msg('user', 'management')+" "+msg('done')
    }
    user=@params['user']
    email = mailaddress(user)
    newmail = @params['newmail']
    nickname = @sc.nickname(user)
    newnn = @params['nickname'].to_s.strip
    webpage = webpage(user)
    newweb  = @params['webpage']
    if email != newmail
      # change user's address
      if newmail == user
	newvalue = nil
      elsif checkmail(newmail)
	newvalue = newmail
      else
	@O.print @H.elementln("pre"){"Invalid mail address"}
      end
      @O.print @H.elementln("pre"){
	if @sc.putuserattr(user, 'email', newvalue)
	  sprintf "new mail address=\"%s\"", mailaddress(user)
	else
	  sprintf "Setting new mail address to \"%s\" failed", newvalue
	end
      }
    end
    if nickname != newnn
      if @sc.setnickname(user, newnn)
	@O.print @H.p(msg('success'))
	@O.print @H.elementln("pre"){
	  sprintf "user=\"%s\"\nnickname=\"%s\"", user, @sc.nickname(user)
	}
	@O.print @H.p(msg('nicknamenote')) if newnn == ''
      else
	@O.print @H.p(msg('failure'))
      end
    end
    if newweb > '' && webpage != newweb
      if @sc.putuserattr(user, "webpage", newweb)
	@O.print @H.p(msg('success'))
	@O.print @H.elementln("pre"){
	  sprintf "user=\"%s\"\nwebpage=\"%s\"", user, webpage(user)
	}
      else
	@O.print @H.p("Update webpage"+msg('failure'))
      end
    end
    link2home
  end
  #
  # Display form of group management
  def groupman(grp = nil)
    if !checkauth
      return nil
    end
    user=@params['user']
    nickname = @sc.nickname(user)
    tdclass = {}
    tdclass["width"] = "80px" if @oldagent # workaround for NN4
    admclass = {'class'=>'admin'}
    grmap = @sc.groupmap

    @O.print @H.elementln("h1"){
      @mybase+' '+msg('group', 'management')
    }
    $KCODE='e' if $DEBUG
    if grp && group = grepgroup(grp)
      @O.print @H.elementln("h2"){
        sprintf(msg('aboutgroup'), group)
      }
      grmap = {group => grmap[group]}
    end
    @O.print grmap.inspect if $DEBUG
    @O.print @H.p(msg('joinmyself')+
		  @H.a(@myname+"?-newgroup", msg('newgroup')))
    @O.print @H.p(msg('usermodwarn'))
    @O.print \
    @H.elementln("form", {'action'=>@myname+"?-groupmod", 'method'=>'POST'}){
      @H.elementln("table", {'class'=>'border'}){
	grmap.sort.collect{|g, ghash|
          memberp = @sc.ismember(user, g)
          adminp = @sc.isadmin(user, g)
	  @H.elementln("tr"){
	    @H.element("td", adminp ? admclass : nil){
	      g + "<br>("+@sc.members(g).length.to_s+")"
	    } + \
	    @H.element("td"){
	      @H.element("div", {'class'=>'c'}) {
		if adminp
		  @H.a(@myname+"?-admgroup+#{g}", msg('adminop'))
		else
		  '--'
		end
	      }
	    } + \
	    @H.element("td"){
	      if ghash['admin'].grep(user)[0]
		@H.text("groupname-#{g}", ghash['name'], nil, 30)
	      else
		ghash['name']
	      end + '<br>' + \
              # If this group is inviteonly and the user is not a member,
              # one cannot join.
              if memberp && adminp || !@sc.getgroupattr(g, 'inviteonly')
                @H.radio("groupadd-#{g}", "yes", "IN", memberp) + " / " + \
                @H.radio("groupadd-#{g}", "no", "OUT", !memberp)
              else
                @H.element("small"){"("+msg('inviteonly')+")"}
              end
	    } + \
	    @H.element("td"){
              @H.element("div", {'class'=>'memlist5'}){
                memlist = ghash['members']
                if memberp        # move this user to the beginning of list
                  memlist.delete(user)
                  memlist.unshift(user)
                end
                memlist.collect{|u|
                  if u == user
                    @sc.nickname(u) + \
                    "("+@H.text("mail4-#{g}", memberp, 30, 180)+")"
                  else
                    @H.a(@myname+"?-listdraft+#{u}",
                         @H.element("abbr", "title"=>u) {
                           @sc.nickname(u)
                         })
                  end
                }.join(", ")
              }
            } + \
            @H.element("td"){
              @H.a(@myname+"?-listdraft+#{g}", msg('sendall'))
            }
	  }
	}.join("\n")
      } + \
      '' + \
      @H.p(msg('address2send')) + \
      @H.p(msg('skip:')) + \
      @H.p(msg('groupwarn', 'shortnameplz')) + \
      @H.submit_reset("GO")
    } # form
    link2home()
    @O.print footer()
  end
  def groupnamesString()
    @H.elementln("p", {'class'=>'listup'}){
	@sc.groups().collect{|g|@sc.groupname(g)}.join(", ")
    }
  end
  def groupmod()
    if !checkauth
      return nil
    end
    @O.print @H.elementln("h1"){
      msg('group', 'management')+" "+msg('done')
    }
    user=@params['user']
    @O.print @params.inspect if $DEBUG

    for grp in @sc.groups()
      #
      # As a member, participate or retire

      key = "groupadd-#{grp}"
      memberp = @sc.ismember(user, grp)
      removep = (/no/i =~ @params[key])
      if @params[key]
        #
        # Check the group is invitation-only mode.
        if !removep && !memberp \
          && @sc.getgroupattr(grp, 'inviteonly') \
          && !@sc.isadmin(user, grp)
          @O.print @H.elementln("p") {
            sprintf(msg('invite-error'), grp) + "<br>\n" + \
            @sc.admins(grp).join(", ")
          }
          sendMail(defaultmladdress(grp).sub("@", @mailadmsuffix+"@"),
                   "Group paticipation attempt to #{grp}",
                   putLog("User `%s' tried to join `%s' from %s" %
                          [user, grp, ENV['REMOTE_ADDR']]),
                   nil,
                   @sc.admins(grp))
          next
        end
        #
        # OK to join/retire
	if (!removep) ^ memberp
	  @sc.addgroup(grp, [user], removep)
	  @O.print @H.elementln("p"){
	    sprintf "%s [%s] %s %s", msg('user'), user,
	      removep ? msg('removedfromgp') : msg('addedtogroup'), grp
	  }
	end
      end
      #
      # As a member, change group-specific mailto address.
      key = "mail4-#{grp}"
      if memberp && @params[key] && memberp != @params[key]
        @sc.addgroup(grp, [[user, @params[key]]])
        newmemp = @sc.ismember(user, grp)
        @O.print @H.elementln("p") {
          sprintf("%s `%s' %s =&gt; %s%s",
                  msg('group'), grp, msg('mailaddress'), @params[key],
                  @params[key]==mailaddress(user) ? "(same)" : "")
        }
      end
      #
      # as an owner, change the name of group
      if @sc.isadmin(user, grp) &&
	  (newname = @params["groupname-#{grp}"]) &&
	  @sc.groupname(grp) != newname
@O.printf "@sc.name2group=%s<br>\n", @sc.name2group(newname)
	if dupl=@sc.name2group(newname)
	  @O.print @H.p(sprintf(msg('dupname'), newname))
	  @O.print groupnamesString()

	else
	  @sc.setgroupname(grp, newname)
	  @O.print @H.elementln("p"){
	    sprintf "%s %s%s %s",
	      msg('group'), grp, msg('of', 'name', 'setto'), newname
	  }
	end
      end
    end
    link2home
  end
  def users()
    unless pm=open_pm()
      outputError(msg('autherror'))
      return nil
    end	
    pm.users
  end
  def grepgroup(gname)
    gr = @sc.groups.grep(gname)[0]
  end
  def admgroup(group = nil)
    # if group==nil, create new
    if !checkauth
      return nil
    end
    @O.print @H.elementln("h1"){
      msg('group', 'management')
    }
    user=@params['user']

    # Check the existent group's validity
    if group
      unless (gr=grepgroup(group))
      @O.print @H.p("No such group #{group}")
	return nil
      end
      group = gr
      unless @sc.isadmin(user, group)
	@O.print @H.p("You are not administrator of #{group}.")
	return nil
      end
      @O.print @H.elementln("h2"){
	msg('group')+" #{group}" +
	  if group != @sc.groupname(group)
	    " (#{@sc.groupname(group)})"
	  end.to_s
      }
      actionmethod={'action'=>@myname+"?-admgroupsub", 'method'=>'POST'}
    else
      # New group creation
      @O.print @H.elementln("h2"){
	msg('newgroup')
      }
      actionmethod={'action'=>@myname+"?-newgroupsub", 'method'=>'POST'}
    end

    userlist = ([user] + users()).uniq.sort
    myselfclass = {'class'=>'admin'}
    yesclass = {'class' => 'yes'}
    colspan2 = {'colspan'=>'2'}
    colspan3 = {'colspan'=>'3'}
    warnclass = {'class'=>'warn'}
    warnp = nil

    @O.print @H.elementln("form", actionmethod){
      @H.hidden('group', group) + "\n" + \
      if group
        # Non symmetric job: Move the current users above.
        gmemlist = userlist.select{|u| @sc.ismember(u, group)}
        userlist = (gmemlist + userlist).uniq
        # In this context, should return simply "".
	""
      else
	# new group creation
	grps = @sc.groups()
	i=1
	defname = "group%03d"%i
	while grps.grep(defname)[0]
	  defname = "group%03d"%(i+=1)
	end
	@H.element("pre"){
	  msg('group', 'of', 'id')+"\n"+@H.text("group", defname) + "\n" + \
	  msg('group', 'of', 'name', 'anystring')+"\n"+ \
	  @H.text("gname", '') + "\n"
	}
      end + \
      @H.elementln("div", {'class'=>'memlist'}){
        @H.elementln("table", {'border'=>'1'}){
          @H.elementln("tr") {
            @H.elementln("th", colspan3) {
              msg('member', 'of', 'joinquit', 'operation')}
          } + \
          @H.elementln("tr"){
            @H.element("th"){msg('join')} + \
            @H.element("th"){msg('administrator')} + \
            @H.element("th"){msg('member')}
          } + \
          userlist.collect{|u|
            recursememp = nil
            if group
              memberp = (@sc.ismember(u, group) && true)
              adminp = (@sc.isadmin(u, group) && true)
              if !memberp && @sc.members(group).grep(u)[0]
                recursememp = true
              end
            else
              memberp = adminp = (u == user)
            end
            @H.elementln("tr", (u==user ? myselfclass : nil)){
              @H.element("td", memberp && yesclass){
                yes = memberp ? 'YES' : 'yes'
                @H.radio('mem-'+u, 'yes', yes+' / ', memberp) + \
                @H.radio('mem-'+u, 'no', 'no', !memberp)
              } + \
              @H.element("td"){
                @H.radio('adm-'+u, 'yes', 'Admin / ', adminp) + \
                @H.radio('adm-'+u, 'no', 'no', !adminp)
              } + \
              @H.element("td"){
                @H.element("abbr", "title"=>mailaddress(u)) {
                  @sc.nickname(u)
                } + \
                if recursememp
                  warnp = true
                  @H.element("span", warnclass){"(*)"}
                end.to_s
              }
            }
          }.join + \
          # group names
          @H.elementln("tr") {
            @H.elementln("th", colspan3) {
              msg('group', 'of', 'joinquit', 'operation')}
          } + \
          @H.elementln("tr"){
            @H.element("th", colspan2){msg('join')} + \
            @H.element("th"){msg('group')}
          } + \
          @sc.groups().sort.collect{|g|
            next if group == g
            memberp = @sc.ismember(g, group)
            @H.element("tr"){
              @H.element("td", colspan2){
                @H.radio('mem-'+g, 'yes', 'YES / ', memberp) + \
                @H.radio('mem-'+g, 'no', 'NO', !memberp)
              } + \
              @H.element("td"){
                if @sc.isadmin(user, g)
                  @H.a(@myname+"?-admgroup+#{g}", @sc.groupname(g))
                else
                  @sc.groupname(g)
                end
              }
            }
          }.join
        }
      } + \
      ["fromhack", "inviteonly", "limitsender"].collect do |param|
        @H.checkbox(param, "yes", msg(param),
                    @sc.getgroupattr(group, param)) + "<br>\n"
      end.join + \
      (group ? @H.elementln("p") {
         sprintf(msg('mladdress'), defaultmladdress(group)) + \
         @H.text("mladdress", @sc.getgroupattr(group, 'mladdress'),
                 @opt['size'], 80)
       } + \
       @H.elementln('p') {
         sprintf(msg('xmlname'), group) + \
         @H.text("xmlname", @sc.getgroupattr(group, 'xmlname'),
                 @opt['size'], 80)
       } : "") + \
      @H.elementln('p') {
        n = -1
        curtag = @sc.getgroupattr(group, 'subjtag')
        values = @subjtags.collect {|x|
          sprintf('	 <option value="%d"%s>%s</option>', n+=1,
                  curtag==@subjtags[n][1] ? ' selected' : "",
                  x[0])
        }.join("\n")
        "Subject tag: " + \
        <<-_EOF_
	<select name="subjtag">
	 <option value="">DEFAULT</option>
	 #{values}
	</select>
	_EOF_
      } + \
      @H.submit_reset("GO")
    } # form
    @O.print @H.p(@H.element("span", warnclass){"(*)"}+
		  msg('recursewarn')) if warnp
    if group && (members = @sc.members(group))[0]
      @O.print @H.p(sprintf(msg('wholemembers'), group))
      @O.print @H.elementln("p", {'class'=>'listup'}){
	members.collect{|u|@sc.nickname(u)}.join(", ")}
    end

    #
    # Next section, REMOVE GROUP!
    return nil unless group
    @O.print @H.elementln("h2"){
      sprintf "%s %s %s", msg('group'), group, msg('deletion')
    }
    @O.print @H.p(msg('deletionwarn'))+"\n"
    @O.print @H.elementln("form", {'action'=>@myname+"?-delgroupsub+#{group}", 'method'=>'POST'}){
      @H.hidden("group", group) + "\n" + \
      @H.elementln("table"){
	@H.elementln("tr"){
	  @H.elementln("td"){
	    sprintf msg('delgroup'), group
	  } + \
	  @H.elementln("td"){
	    @H.radio("delete", "yes", msg('yes')) + ' ' + \
	    @H.radio("delete", "no", msg('no'), true)
	  }
	} + \
	@H.elementln("tr"){
	  @H.elementln("td"){
	    sprintf msg('really?'), group
	  } + \
	  @H.elementln("td"){
	    @H.radio("delete2", "yes", msg('yes')) + ' ' + \
	    @H.radio("delete2", "no", msg('no'), true)
	  }
	}
      } + \
      "<br>\n" + @H.element("span", "class"=>"danger"){@H.submit_reset("OK")}
    }

    @O.print footer()
  end
  def newgroup()
    admgroup(nil)
  end

  def delgroupsub(group)
    if !checkauth
      return nil
    end
    user = @params['user']
    if group != @params['group']
      @O.print @H.p("Group mismatch")
      return nil
    end
    unless (gr=grepgroup(group))
      @O.print @H.p("No such group #{group}")
      return nil
    end
    group = safecopy(gr)
    unless @sc.isadmin(user, group)
      @O.print @H.p("You are not administrator of #{group}.")
      return nil
    end
    unless @params['delete'] && /yes/i =~ @params['delete'] \
      && @params['delete2'] && /yes/i =~ @params['delete2']
      @O.print @H.p(msg('chicken'))
      return nil
    end
    @O.print @H.elementln("h1"){
      msg('group')+" #{group} "+msg('deletion')
    }
    if @sc.destroygroup(group)
      system(sprintf("rm -r ml/%s", group))
      resmsg = msg("done")
    else
      resmsg = Omsg("failure")
    end
    @O.print @H.p(resmsg)
    putLog("Delete group '#{group}' #{resmsg}\n")

    @O.print footer()
  end

  def deleteuser(user)
    @sc.deleteuser(user) &&
      begin
	pm = open_pm
	pm.delete(user)
	pm.close()
	true
      rescue
	nil
      end
  end
  def delusersub(user)
    if !checkauth
      return nil
    end
    user = @params['user']
    if user != @params['user']
      @O.print @H.p("User mismatch")
      return nil
    end
    unless (us=users().grep(user)[0])
      @O.print @H.p("No such user #{user}")
      return nil
    end
    user = us
    unless @params['delete'] && /yes/i =~ @params['delete'] \
      && @params['delete2'] && /yes/i =~ @params['delete2']
      @O.print @H.p(msg('chicken'))
      return nil
    end
    @O.print @H.elementln("h1"){
      msg('user')+" #{user} "+msg('deletion')
    }
    resmsg = deleteuser(user) ? msg("done") : msg("failure")
    @O.print @H.p(resmsg)
    putLog("Delete user '#{user}' #{resmsg}\n")

    @O.print @H.p(@H.a(@myname, msg('login')))
  end

  def admgroupsub()
    if !checkauth
      return nil
    end
    user = @params['user']
    group = @params['group']
    unless (gr=grepgroup(group))
      @O.print @H.element("pre"){"No such group #{group.inspect}"}
      return nil
    end
    unless @sc.isadmin(user, group)
      @O.print @H.p("You are not administrator of #{group}.")
      return nil
    end
    gorup = gr
    @O.print @H.elementln("h1"){
      msg('group', 'management', 'done')
    }
    @O.print @H.elementln("h2"){
      msg('group')+" #{group}" +
	if group != @sc.groupname(group)
	  " (#{@sc.groupname(group)})"
	end.to_s
    }
    somethingdone = nil
    for u in users()
      u = @sc.isuser(u)		# users() value is considered tainted.
      next unless u		# Use registered value in @sc.
      for var, kind in {
	  "mem"=>['members', 'member'], 'adm'=>['admin', 'administrator']}
	memv = "#{var}-#{u}"
	if @params[memv]
	  joinp = ((/^yes/i =~ @params[memv]) && true)
	  membp = if var=='mem'
		    @sc.ismember(u, group)
		  else		# admin
		    @sc.isadmin(u, group)
		  end && true
	  if var=='adm' && @sc.admins(group).length == 1 && membp && !joinp
	    @O.print @H.p(sprintf(msg('soleadmin'), u, group))
	  elsif joinp ^ membp
	    somethingdone = true
	    @sc.addgroup(group, [u], !joinp, kind[0])
	    @O.print @H.elementln("p"){
	      putLog(sprintf "%s [%s](%s) %s %s", msg('user'), u,
                     msg(kind[1]),
                     joinp ?  msg('addedtogroup'): msg('removedfromgp'), group)
	    }
	  end
	end
      end
    end # users()

    # add or remove for group in groups
    for g in @sc.groups()
      next if g == group
      memv = "mem-#{g}"
      if @params[memv]
	joinp = ((/^yes/i =~ @params[memv]) && true)
	membp = (@sc.ismember(g, group) && true)
	if joinp ^ membp
	  somethingdone = true
	  @sc.addgroup(group, [g], !joinp)
	  @O.print @H.elementln("p"){
	    putLog(sprintf("%s [%s] %s %s",
                           msg('group'), g,
                           joinp ? 
                           msg('addedtogroup')
                           : msg('removedfromgp'), group))
          }
	end
      end
    end # groups
    # Change parameter(s)
    # To be more generic...
    ["fromhack", "inviteonly", "limitsender"].each {|param|
      parsetp = (@params[param] && /^yes/i =~ @params[param])
      cursetp = (@sc.getgroupattr(group, param)!=nil)
      if cursetp ^ parsetp
        @sc.putgroupattr(group, param, @params[param])
        @O.print @H.elementln("p") {
        putLog(sprintf("group: %s[%s] -&gt; %s",
                       group, param, @params[param].inspect))
        }
        somethingdone = true
      end
    }
    # mladdress
    newmladdress = @params['mladdress']
    newmladdress = nil if newmladdress == ""
    curmladdress = @sc.getgroupattr(group, 'mladdress')
    if newmladdress != curmladdress
      defmladdress = defaultmladdress(group)
      @sc.putgroupattr(group, 'mladdress', newmladdress)
      @O.print @H.elementln("p") {
        putLog(sprintf("group: %s[mladdress] &lt;%s&gt; -> &lt;%s&gt;",
                       group,
                       curmladdress || defmladdress,
                       newmladdress || defmladdress))
      }
      somethingdone = true
    end
    # Subject tag bracket
    newtag = @params['subjtag']
    if newtag == ''
      newtag = nil 
    else
      newtag = @subjtags[newtag.to_i % @subjtags.length][1]
    end
    @sc.putgroupattr(group, 'subjtag', newtag)
    if newtag && newtag > ""
      @O.print @H.elementln("p") {
        putLog(sprintf("group: %s[subjtag] set to '%s'", group, newtag))
      }
      somethingdone = true
    end
    # X-ML-Name: Header value
    xmlname = @params['xmlname']
    xmlname = nil if xmlname == ""
    curxmlname = @sc.getgroupattr(group, 'xmlname')
    if xmlname != curxmlname && /^[-A-Z_a-z\/0-9+@(),.<>]+$/ =~ xmlname 
      @sc.putgroupattr(group, 'xmlname', xmlname)
      @O.print @H.elementln("p") {
        putLog(sprintf("X-ML-Name: Set to %s", xmlname))
      }
      somethingdone = true
    end
    unless somethingdone
      # @O.print @H.p(msg('nothingtodo'))
    end
    # @O.print footer()
    link2home
  end
  def newgroupsub()
    return nil unless checkauth
    user = @params['user']
    newgroup = @params['group']
    newgname = @params['gname']

    
    if @sc.groups.grep(newgroup)[0]
      @O.print @H.p(msg('existent')+newgroup)
      return nil
    end
    if dupl=@sc.name2group(newgname)
      @O.print @H.p(sprintf(msg('dupname'), newgname))
      @O.print groupnamesString()
      return nil
    end
    @sc.creategroup(newgroup, newgname, [user]) &&
      putLog("New group '#{newgroup}'(#{newgname}) created\n")
    admgroupsub()
  end

  #
  # Methods Related to viaMail functions
  def gen_sessionpswd()
    
  end
  def viamail_registform()
    c = "# "
    nl = "\n"
    user = @params['user']
    msg('addsched') + "-" * 20 + nl*2 + \
    c + msg('user') + nl + \
    "user=" + user + nl*2 + \
    c + msg('sessionpswd') + nl + \
    "sp=hoge" + nl*2 + \
    c + msg('date') + nl + \
    "date="+Time.now.strftime("%Y/%m/%d") + nl*2 + \
    c + msg('time') + sprintf(msg('24hourtxt'), @opt['alldaydir']) + nl + \
    "time=3000"+nl*2 + \
    c + msg('publicp') + nl + \
    "public=yes" + nl*2 + \
    c + msg('neednotify') + nl + \
    "nt10m=yes (%s)
nttoday=yes (%s)
nt1d=yes (%s)
nt7d=yes (%s)" % ["10"+msg('minutes')+msg('before'),
      msg('theday'), msg('precedingday'),
      "7"+msg('days')+msg('before')] + nl*2 + \

    c + msg('schedulehere')
  end
  def viamail_footer()
    viamail_registform()
  end
  def show_by_text(date, days)
    user = @params['user']
    personal = true
    sched = dayTextString(user, date, days, personal)
    # @O.print outstr

    sendMail(mailaddress(user),
	     "After5 Schedule",
	     @opt['url'] + "\n" + \
	     Time.now.strftime("%Y/%m/%d") + \
	     sprintf(msg('schedlist'), days) + "\n\n" + \
	     if sched > ''
	       sched
	     else
	       msg('noplan')+"\n"
	     end + \
	     viamail_footer
	     )
    
  end
  def parseHeader
    contline=nil
    header=Hash.new
    text=Array.new

    field=nil
    # header
    while line=STDIN.gets
      text << line
      break if /^$/ =~ line
      
      if /^\s+/ =~ line
	if field
	  header[field][-1] << line
	end
      else
	if /^([^:]+):\s*(.*)/ =~ line
	  field=$1.downcase
	  header[field] or header[field] = []
	  header[field] << $2
	end
      end
    end
    header
  end
  def mail_regsched()
    @params = Hash.new		# Reset

    reqparams = %w[user sp date time public]
    otherparams = %w[nt10m nttoday nt1d nt7d]
    setall = lambda{
      reqparams.each{|key| return false unless @params.has_key?(key)}
      return true
    }
    stack = ""
    while line=gets # !setall.call && line=gets
      if /^(\S+)=(.*)/ =~ line
	next unless reqparams.index($1) || otherparams.index($1)
	@params[$1] = $2
	#if reqparams.index($1)
	STDERR.print "Set #{$1} to #{$2}\n"
	#end
	buf = ""
      elsif /^\s*\#|^$/ =~ line
	# skip comments
      else
	buf += line
      end
    end
    unless setall.call
      STDERR.print "Insufficient variables\n"
      exit 1
    end
    p buf
    
    y, m, d = date2ymd(@params["date"])
    @params["year"] = y
    @params["month"] = m
    @params["day"] = d
    @params["schedule"] = buf
    @params["editmode"] = "modify"
    @params["sessionpw"] = @params["sp"]
    p @params
    add_remove()
  end
  def mail_getsched()
    user = nil
    while bline=gets
      if /(\S+@\S+)/ =~ bline
	break if user=@sc.isuser($1)
      end
    end
    unless user
      sendMail(@opt['maintainer'], "viaMail Request Error",
	       "This is `#{@mybase}' in #{@mydir}\n" +
	       "Invalid schedule request from #{ENV['SENDER']}.\n\n")
      exit 1
    end
    today = Time.now.strftime("%Y/%m/%d")
    days = 7
    if bline=gets
      if /\d+/ =~ bline
	days = bline.to_i
      end
    end
    # Send user to schedules of today and near future
    @params['user'] = user
    show_by_text(today, days)
  end
  def doMail()
    days = 7
    # Confirm `via Mail'
    prohibitviahttp()
    @H = TEXTout.new
    unless ENV['RECIPIENT'] && ENV['SENDER']
      STDERR.print "Call me via qmail\n"
      exit 1
    end
    @mailmode = true
    header = parseHeader	# is this necessary?
    if /regist/ =~ ENV["EXT"]
      mail_regsched()
    else
      mail_getsched()
    end
  end

  #
  # Password related Methos
  def setpasswd(user)
    prohibitviahttp()
    pm = open_pm()
    exit 1 unless pm
    if pm.userexist?(user) then
      begin
	system "stty -echo"
	STDERR.print "New passwd: "
	p1 = STDIN.gets
	STDERR.print "\nAgain: "
	p2 = STDIN.gets
      ensure
	system "stty echo"
      end
      if (p1 == p2) then
	pm.setpasswd(user, p1.chop!)
      end
      STDERR.print "New password for #{user} set successfully\n"
    else
      STDERR.print "User #{user} not found\n"
    end
    pm.close()
    exit 0
  end
  def adduser(user)
    prohibitviahttp()
    pm = open_pm()
    exit 1 unless pm
    if pm.userexist?(user) && !ENV['PWRESET']
      printf("User %s already exists.  Skip.\n", user)
      printf("If you reset passwd to new one, PWRESET=1 #{$0} ...\n")
      exit 1
    end
    email = nil
    if /(.*@.*)=(.*@.*)/ =~ user
      user, email = $1, $2
    end
    newpwd = pm.setnewpasswd(user, 4)
    @sc.createuser(user, email)
    print "#{user}'s password is #{newpwd}\n"
    pm.close()
    exit 0
  end
  def deluser(user)
    prohibitviahttp()
    pm = open_pm()
    exit 1 unless pm
    pm.delete(user)
    @sc.deleteuser(user)
    pm.close()
    exit 0
  end

  # read configuratoin file
  def readconf(conf)
    cf =  "after5.cf" #conf # || @opt['conf']
    cf = File.join(@mydir, cf) unless test(?s, cf)
    cf = File.join(ENV["HOME"], cf) unless test(?s, cf)
    return unless test(?s, cf)
    begin
      IO.foreach(cf){|line|
        line = line.toeuc
	next if /^\s *#/ =~ line
	line.chop!
	line.sub!(/^\s*#.*/, '')
	next if /^$/ =~ line
	case line
	  # title, type = line.split(/\t+/)
	when /^([a-z]+)=(.*)/oi
	  key, value = $1, $2
	  case value
	  when /^(no|none|null|nil|false|0|off)$/io
	    @opt[key] = nil
	  else
	    @opt[key] = value.untaint
	  end
	  print "#{key} set to #{value}\n" if $DEBUG
	end
      }
    rescue
      STDERR.printf("Configuration file %s not readable\n", cf)
    end
  end

  def parsedate(string)
    if %r,^(\d\d\d\d+)/(\d+)/(\d+)/(\d+)/([^/]+)$, =~ string
      sprintf "%04d/%02d/%02d/%04d/%s", $1.to_i, $2.to_i, $3.to_i, $4.to_i,
	grepgroup($5)
    elsif %r,^(\d\d\d\d+)/(\d+)/(\d+)/(\d+), =~ string
      sprintf "%04d/%02d/%02d/%04d", $1.to_i, $2.to_i, $3.to_i, $4.to_i
    elsif %r,^(\d\d\d\d+)/(\d+)/(\d+), =~ string
      sprintf "%04d/%02d/%02d", $1.to_i, $2.to_i, $3.to_i
    elsif %r,^(\d\d\d\d+)/(\d+), =~ string
      sprintf "%04d/%02d", $1.to_i, $2.to_i
    elsif %r,^(\d\d\d\d+)/(\d+), =~ string
      sprintf "%04d", $1.to_i
    end
  end

  def getarg()
    argument = {}
    @oldargv = ARGV.dup

    while /^-/ =~ ARGV[0]
      case ARGV[0]
      when '-f'
	conf = ARGV[1]
	ARGV.shift
      when "-d"
	$DEBUG = true
      when "-install"
      when "-stream"
	# ARGV.shift
	# @job = 'show_by_text "2005/1/18"'
	@job = 'doMail'
      when "-addsched"
	@job = "addsched"
      when "-today"
	@job = "today"
      when "-today_p"
	argument['displaymode'] = 'personal'
	@job = "today"
      when "-today_n"
	argument['displaymode'] = 'normal'
	@job = "today"
      when "-remove"
	ARGV.shift
	@job = 'remove "'+parsedate(ARGV[0])+'"'
      when "-move"
	ARGV.shift
	@job = 'move "'+parsedate(ARGV[0])+'"'
      when "-modify"
	ARGV.shift
	@job = 'modify "'+parsedate(ARGV[0])+'"'
      when "-month"
	ARGV.shift
	@job = 'month "'+parsedate(ARGV[0])+'"'
      when "-show"
	ARGV.shift
	# @job = "show '"+ARGV[0]+"'"
	@job = "show '"+parsedate(ARGV[0])+"'"
      when "-login"
	@job = "login"
      when "-userman"
	@job = "userman"
      when "-usermod"
	@job = "usermod"
      when "-groupinout"
	@job = "groupinout"
      when "-groupsubmit"
	@job = "groupsubmit"
      when "-groupman"
        ARGV.shift
        x=ARGV[0]
	@job = "groupman("+(x ? "'#{x.dup.untaint}'" : "") + ")"
      when "-groupmod"
	@job = "groupmod"
      when "-notify"
	@job = 'notify'	# + exit
      when "-list"
	@job = 'list'	# + exit
      when "-newgroup"
	@job = 'newgroup'
      when /^-(admgroup|listdraft)$/
	ARGV.shift
	#gr = safecopy(grepgroup(ARGV[0]))
	gr = safecopy(ARGV[0]) # -listdraft can be called with user 2013/12/12
	##gr.untaint
	@job = safecopy($1)+' "'+gr+'"'
      when "-admgroupsub"
	@job = 'admgroupsub'
      when "-newgroupsub"
	@job = 'newgroupsub'
      when "-delusersub"
	ARGV.shift
	usr = safecopy(users().grep(ARGV[0])[0])
	@job = 'delusersub "'+usr+'"'
      when "-delgroupsub"
	ARGV.shift
	gr = safecopy(grepgroup(ARGV[0]))
	@job = 'delgroupsub "'+gr+'"'
      when /-(setpasswd|deluser|adduser)$/
	ARGV.shift
	@job = $1+ " '#{ARGV[0]}'" # + exit
      when "-mp"
        ARGV.shift
        r = recompose_multipart(ARGF.readlines.join)
        open("/tmp/a", "w"){|a|
          
          a.printf("R:(((%s)))\n", r)
        }

        exit 0
      when ""
      end
      ARGV.shift
    end

    readconf(@conf)

    query = ''
    method = ENV["REQUEST_METHOD"]
    if /POST/i =~ method then
      length = ENV["CONTENT_LENGTH"].to_i
      query = STDIN.read(length)
    elsif /GET/i =~ method then
      query = ENV["QUERY_STRING"]
    else                            # executed from command line
      query = ARGV.join("&")
    end

    if ENV['CONTENT_TYPE'] &&
        %r,multipart/form-data.*boundary=(.*),i =~ ENV['CONTENT_TYPE']
      boundary = $1
      query.split("\r\n--"+boundary).each {|unit|
        if /Content-Disposition.*\bname=([\'\"])?(\S*)\1/i =~ unit
          argument.has_key?(key=$2) or argument[key]=[]
          newvalue = Hash.new
          if /^Content.*filename=([\'\"])?(\S*)\1/i =~ unit
            newvalue['filename'] = $2
          end
          newvalue['value'] = unit.sub(/.*?\r\n\r\n/m, "") # Shortest match
          if /^Content-type:\s*(\S*)/i =~ unit
            newvalue['content-type'] = $1
          else
            newvalue['value'].gsub!("\r\n", "\n")
          end
          argument[key] << newvalue
        end
      }
      if $DEBUG
        open("/tmp/body", "w"){|x|
          x.write query.split("\r\n--"+boundary+"\r\n")[-1]
          argument.each{|k,v|
            x.printf(" %s => %s\n", k.inspect, v.inspect)}
          x.printf("boundary=\n%s\n",boundary)
          x.printf("ENV=%s\n",ENV.inspect)
          x.write query}
      end
    else
      for unit in query.split(/\&/)
        if /^([a-z][-_0-9@%a-z.]*)=(.*)/i =~ unit
          key, value = $1, $2
          #value.gsub!(/%(..)/){[$1.hex].pack("c")} # これでURLデコードが一発
          decode!(value)
          decode!(key)
          value = Kconv::toeuc(value) # EUCに変換
          printf "[%s] = %s\n", key, value if $DEBUG
          argument[key] = value
        end
      end
    end
    argument
  end
  def getcookie()
    cookie = {}
    return cookie unless ENV['HTTP_COOKIE']
    #if /value=(.*)/ =~ ENV['HTTP_COOKIE']
    for cv in ENV['HTTP_COOKIE'].split(/[\; ]+/).grep(/(value|prefs)=(.*)/)
      # value=$1.gsub!(/%(..)/){[$1.hex].pack("c")}
      next unless /\w+=(.*)/ =~ cv
      value=decode!($1)
      next unless value
      for line in value.split("&")
	if /(\w+)=(.*)/ =~ line
	  key, value = $1, $2
	  #value.gsub!(/%(..)/){[$1.hex].pack("c")} # これでURLデコードが一発
	  decode!(value)
	  value = Kconv::toeuc(value) # EUCに変換
	  printf "cookie[%s] = %s\n", key, value if $DEBUG
	  cookie[key] = value
	end
      end
    end
    cookie
  end
end

$KCODE='e' if RUBY_VERSION < "1.9"
After5.new.doit

if __FILE__ == $0
end


# Local variables:
# buffer-file-coding-system: euc-jp
# End: