view after5.rb @ 72:6a0c2f85faa3 draft

Canonicalize arguments for isHoliday(), which will be sent to Time.mktime().
author HIROSE Yuuji <yuuji@gentei.org>
date Thu, 29 Nov 2012 10:21:21 +0900
parents 156f315f52b8
children 8b8cb30b78fa
line wrap: on
line source

#!/usr/local/bin/ruby19
# -*- coding: euc-jp -*-
#
# Associative Scheduling Table - after5
# (C)2003, 2004, 2006, 2008, 2012 by HIROSE Yuuji [yuuji<at>gentei.org]
# $Id: after5.rb,v 1.19 2012/04/01 11:52:25 yuuji Exp yuuji $
# Last modified Thu Nov 29 10:17:51 2012 on firestorm
# See http://www.gentei.org/~yuuji/software/after5/
# このスクリプトはEUCで保存してください。
$hgid = <<_HGID_.split[1..-2].join(" ")
$HGid$
_HGID_
$myurl = "http://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 "<input type=\"radio\" name=\"%s\" value=\"%s\"%s>%s",
      name, value, checked ? " checked" : "", text
  end
  def checkbox(name, value, text='', checked=nil)
    sprintf "<input type=\"checkbox\" name=\"%s\" value=\"%s\"%s>%s",
      name, value, checked ? " checked" : "", text
  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, "GO")+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)
    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)
    @usermap.keys.each {|u|
      return u if u==email
      return u if mailaddress(u).split(/,\s*|\s+/).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
  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)
    file = File.expand_path((group+"/members/"+usr).untaint, @groupmapdir)
    if test(?s, file.untaint)
      open(file, "r"){|f|f.gets.chomp}.untaint
    else
      mailaddress(usr)
    end
  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)
    members(grp).collect{|u| nickname(u)}
  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)
    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 = {}
    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 personalonly && 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 personal mode
	  if IO.readlines(pub)[0].to_i > 0
	    visible = true
	  end
	end

      
	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] = {}
	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
    @mailmode = nil
    @mailadmdelimiter = "/"
    @mailadmsuffix = @mailadmdelimiter + "adm"
    @saveprefsregexp = /^(display(mode|days)$|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)"]]
    ##@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 = 'http://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'],
	'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'"],
        'sendall_note'	=> ['メンバーへの連絡だけでなく、グループ非加入者がこれから加入する旨の通知などにも有用。',
                            "Send this message to all of group."],
        'sendall_done'	=> ['送信完了', "sending message done"],
        'body'		=> ['本文', 'Body'],
	'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'],
	'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.'],
	'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 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'])
	  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')))
	@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
          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)
    body = NKF.nkf("-j", body) unless thru
    subject = NKF.nkf("-jM", subject.strip)
    to = safecopy(to)		# cleanup tainted address
    subject.gsub!(/\n/, '')
    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 %Y %T %z")}"
          m.print "\n"
        end
	m.print body, "\n"
	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")
            }
            exit 0
          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 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']
    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'))
    #
    # 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){
        sunday = first + (column-1)*3600
	(column..column+6).collect{|d|
	  todayp = (day.year==todayy && day.month==todaym && d==todayd)
	  wd=d-column
          thisday = sunday+3600*wd
	  hd = holiday.isHoliday(thisday.year, thisday.month, thisday.day, wd)
	  tdclass['class'] = (hd ? 'holiday' : wname[wd])
	  @H.element("td", tdclass){
	    if d>0 
	      thisday = first+(d-1)*3600*24
	      #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)
		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.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)
    #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)
      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)
    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)
      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']
    days = @params['displaydays'].to_i
    days = (days > 0 ? days : 3)

    # str = @sc.day_all(date, user, personal)
    outstr = dayTableString(user, date, days, personal)

    @O.print @H.element("h1", nil){
      sprintf msg('fmtdaysschedule'), date
    }
    @O.print @H.element("h2"){msg('schedtable')}
    ## @O.print @H.p()
    @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=\"%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
    p "notifylist", @sc.notify_list(now) if $DEBUG
    @sc.notify_list(now).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')
    comment += "/" if comment>""
    return comment.gsub(/([^\x00-\x7f]+)/){NKF.nkf('-jM', $1)} +
      email.sub("@", "=")+" <"+newseed+">"
    #  end
  end

  def tagify_subj(body, tag, removeregexp, fromhack = nil)
    # This method should be generic for other headers than `Subject'?
    hold = []
    ret = []
    skip = false
    while line = body.shift
      case line.toeuc
      when /^$/
        hold << "\n"
        break
      ## when /^(subject|from): /i
      when /^(\S+): /i             # if new header comes
        if /^subject:/i =~ hold[0] # check previous header in hold space
          sj = hold.join.toeuc.sub("Subject: ", "").gsub(tag, "").strip
          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)
          hold = ["From: "+rewritefrom(email, comment, fromhack)+"\n"]
        end
        ret += hold
        hold = [line]
      when /^\s/                # continued line
        hold << line
      end
    end
    ret + hold + body
  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 = 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 = ENV['DEFAULT']
      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
      unless grepgroup(name)
        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
      name = @params['name'].untaint
      unless grepgroup(name)
        @O.print @H.p("No such group: #{name}")
        return true
      end
      nick = @sc.nickname(@params['user'])
      from = sprintf("%s <%s>", nick, @params['user'])
      body = @params['body'].gsub("\r", "").untaint
    end
    bracket = @sc.getgroupattr(name, 'subjtag') || @opt['mailbracket']
    fromhack = @sc.getgroupattr(name, 'fromhack')
    mldir = "ml/"+name
    to = @sc.getgroupattr(name, 'mladdress') || defaultmladdress(name)
    returnpath = to.sub("@", @mailadmsuffix+"-@")
    adminaddr  = to.sub("@", @mailadmsuffix+"@")
    subj = @params['subject'] || "Message from "+@myname
    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)
    subj = sjtag.strip+" "+subj.gsub(Regexp.new(tagpt), "")
    if viamail then
      body = tagify_subj(STDIN.readlines, sjtag, tagre,
                         fromhack ? to : nil).join
    elsif fromhack
      from = rewritefrom(@params['user'], nick, to)
    end
    header = {
      "Reply-to" => to,
      "X-ML-Driver" => ($hgid || @myname),
      "X-ML-Driver-URI" => $myurl,
      "X-ML-Name" => name,
      "X-ML-URI" => sprintf("%s?-groupman+%s", @opt['url'], name),
      "Return-path" => returnpath}
    Dir.chdir @mydir
    rcpts = if grepgroup(name)
              @sc.members(name)
            else
              [name]
            end.collect {|u| mailaddress(u, name).split(/,\s*|\s+/)}.flatten
    ENV["QMAILINJECT"] = "r"    # for ML mode, use verp
    #
    # 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)
        }
        # 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'],
             @opt['mlspooling'] ? mldir : nil)
    if !viamail then
      @O.print @H.elementln("h1"){msg('sendall_done')}
      @O.print @H.p(sprintf(msg('sendall_head'),
                            nickname(name))+" "+msg('done'))
      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)
    @O.print @H.elementln("h1") {
      @mybase+' '+msg('sendall').sub("<br>", " ")
    }
    @O.print @H.elementln("h2") {
      sprintf(msg('sendall_head'), nickname(name))
    }
    @O.print @H.p(sprintf("(%s: %s)", msg('member'),
                          @sc.membernames(name).join(", ")))
    @O.print \
    @H.elementln("form", {'action' => @myname+'?-list', 'method'=>"POST"}) {
      @H.elementln("table"){
        @H.elementln("tr"){
          @H.element("td"){"Subject"} + \
          @H.element("td"){
            @H.text("subject", "", 40, 128)
          }
        } + \
        @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.submit_reset("GO")
    }


  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, 12)
	      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|
                  (adminp \
                   ? @H.element("abbr", "title"=>mailaddress(u)) {
                     @sc.nickname(u)
                   }
                   : @sc.nickname(u)) + \
                  ((u == user) ? ("("+@H.text("mail4-#{g}", memberp, 30, 180)+")") : "")
                }.join(", ")
              }
            } + \
            @H.element("td"){
              @H.a(@myname+"?-listdraft+#{g}", msg('sendall'))
            }
	  }
	}.join("\n")
      } + \
      '' + \
      @H.p(msg('groupwarn', 'shortnameplz')) + \
      @H.submit_reset("GO")
    } # form
  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.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') {
        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.submit_reset("GO")
    }

    @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 = 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')
    }
    resmsg = @sc.destroygroup(group) ? msg("done") : msg("failure")
    @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
    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.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 ""
      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

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

yatex.org