#!/usr/bin/env ruby19 # coding: euc-jp # Catch up with conversation on email # - Dynamic mailing list and more for qmail - # Last modified Mon Sep 19 10:42:36 2022 on firestorm # Update count: 788 # (c)2008, 2009, 2011, 2016 by HIROSE, Yuuji [yuuji(at)yatex.org] hgid = <<_HGID_.split[1..-2].join(" ") $HGid: catchup.rb,v 1129:ae05d338fa83 2022-09-19 10:43 +0900 yuuji $ _HGID_ myurl = "http://www.gentei.org/~yuuji/software/catchup/" # Ruby 1.9 hack if defined?(Encoding::default_external) then Encoding::default_internal = Encoding::default_external = 'binary' end ENV["PATH"] = "/var/qmail/bin:/usr/sbin:"+ENV["PATH"]+":/usr/lib" $prefix = "#:" $repl = nil $default= ENV['DEFAULT'] && ENV['DEFAULT'] > "" && ENV['DEFAULT'] $rcpt = ENV['RECIPIENT'] $header = {} # defined later as {'Reply-to' => $rcpt} $confdir= "~/."+File.basename($0).sub(".rb", "") $expire = 7*24*3600 $sender = ENV["SENDER"] $subject='' $fromhack = nil $subjecthack = nil $verpsep = '=' $dotqmaildir = nil $commandmode = nil $lotmode = nil $unsubscribe = nil $staticmember = nil $grouptag = "grp_" $openlist = nil # nil = ML is invitation mode class String if defined?("".force_encoding) def tobin self.force_encoding("binary") end def tojisbin self.tojis.tobin end else def tobin self end def tojisbin self.tojis end end end require 'kconv' require 'nkf' # for MIME encoding class GuestDB def initialize(dir) @dir = File.expand_path(dir) @modemask = 0100 @gprefix = "grp:" parent = File.dirname(@dir) Dir.mkdir(parent, 0750) unless test(?d, parent) Dir.mkdir(@dir, 0750) unless test(?d, @dir) end def item2file(item) File.expand_path(item, @dir) end def mkitem(item, comment = nil) file=item2file(item) File.unlink(file) if test(?f, file) open(file, "w"){|fp| fp.puts comment if comment&&comment>""} end def delitem(item) file=item2file(item) File.unlink(file) end def listall() Dir.entries(@dir).select{|f| /@/=~f}.sort end def list() listall.select{|f| file = @dir+"/"+f File.stat(file).mode&@modemask == 0 } end def turnoff(file) # File.unlink(file) mode = File.stat(file).mode printf("%s: %o -> %o\n", file, mode, mode|@modemask) if $DEBUG File.chmod(mode|@modemask, file) end def postonly(expire) limit = Time.now-expire list.select {|f| file = @dir+"/"+f if File.mtime(file) < limit turnoff(file) f end } end def update(item) #mkitem(item) file = item2file(item) mode = File.stat(file).mode newmode = mode&~@modemask printf("%s: %o -> %o\n", file, mode, newmode) File.chmod(newmode, file) # touch guest file File.utime(Time.now, Time.now, file) end def downdate(item) turnoff(@dir+"/"+item) end def delete(item) file = item2file(item) File.unlink(file) if test(?f, file) end def escape(string) # borrowed from cgi.rb string.gsub(/([^a-z0-9_.-]+)/ni) do '%' + $1.unpack('H2' * $1.size).join('%').upcase end end def unescape(string) string.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n) do [$1.delete('%')].pack('H*') end end def setvalue(k, v) newk, newv = escape(k), escape(v) file = item2file(newk) File.unlink(file) if test(?f, file) open(file, "w"){|f| f.print newv} v # return itself end def getvalue(k) newk = escape(k) file = item2file(newk) if test(?s, file) then unescape(IO.readlines(file).join.split("\n").join(" ")) else "" end end def setheader(header, value) setvalue(header.capitalize, value) end def getheader(header) getvalue(header.capitalize) end def setcomment(item, comment) mkitem(item, comment) end def getcomment(item) file=item2file(item) comment = nil if test(?s, file) then open(file, "r"){|fp| comment=fp.gets.chomp} return comment if comment && comment > "" end return nil end def list_comment() # List all guests' comment list().collect {|s| getcomment(s) }.select{|s|s} end # subgroup def newgroup(memlist, name=nil) # memlist should be [["email0", "gecos0"], ["email1", "gecos1"], ...] gbase = name || memlist.collect{|m| m[0][0].chr.downcase}.join dir = name = nil cmember = memlist.collect{|x| x[0]}.sort.uniq ["", *(2..99)].each {|suff| gname = gbase+suff.to_s gdir = @gprefix+gname if !test(?d, File.expand_path(gdir, @dir)) then dir=gdir; name=gname break end # if it exists, check the list consists of the same members. if getgroup(gname).sort == cmember then # using it is touching it File.utime(Time.now, Time.now, File.expand_path(gdir, @dir)) return gname end } if dir then d = File.expand_path(dir, @dir) Dir.mkdir(d, 0750) memlist.each {|email, cmt| open(File.expand_path(email, d), "w") {|gf| gf.puts((cmt||email))} } return name end end def getgroup(name) gdir = File.expand_path(@gprefix+name, @dir) if test(?d, gdir) then Dir.entries(gdir).select{|f| /@/=~f}.sort end end def addtogroup(name, newlist) # newlist := [[email, name], ...] gdir = File.expand_path(@gprefix+name, @dir) newlist.each {|email, cmt| open(File.expand_path(email, gdir), "w"){|gf| gf.puts((cmt||email))} } end end class Dotqmail def initialize(local, domain) @c = "/var/qmail/control" @user = local @dash = "-" @pre = "" vd = File.expand_path("virtualdomains", @c) if ! test(?s, vd) \ || ! IO.readlines(File.expand_path("locals", @c)).select{|x| x.chomp! domain == x || Regexp.new("^"+Regexp.quote(x)+'$') =~ domain # '$ }.empty? then if ENV['POSTFIX'] || test(?d, "/etc/postfix") d = (ENV['POSTFIX'] || "+") @user, @ext = @user.split(d, 2) @ext = "-"+@ext require 'etc' @home = Etc.getpwnam(@user).dir return end elsif test(?s, vd) vdoms = IO.readlines(vd) d = domain u = nil while d > "" match = vdoms.select{|x| /^#{d}:/ =~ x}[0] if match then d, u = match.chomp.split(":") break end d.sub!(/\.?[^.]+/, "") end if u @user = u+"-"+local end end @home = gethome(@user) end def gethome(user) return "" unless user home = nil if test(?r, asg="/var/qmail/users/assign") then assignlist = IO.readlines(asg) u = user.dup while u > "" @ext = user[u.length..-1] ux = Regexp.quote(u) match = assignlist.select{|l| /^\+#{ux}-:/ =~ l}[0] if match then ms = match.chomp!.split(":", -1) home = ms[4] @dash = ms[5] @pre = ms[6] break end u.sub!(/-?[^-]+$/, "") end return home if home end u = user.dup require 'etc' while u > "" @ext = user[u.length..-1].to_s begin home = Etc.getpwnam(u).dir rescue end u.sub!(/-?[^-]+$/, "") end # if a user is found, return its home directory if home then return home else # no users found, then it's covered by user `alias' begin home = Etc.getpwnam("alias").dir @ext = "-"+user rescue end end return home || "" end def homedir() @home end def dotqmail() @home+"/.qmail"+@pre+@ext end end def install() e = STDERR while true e.print <<_EOF_ 連絡先として使いたいアドレスを入れて下さい(例: taro-renraku@example.com) _EOF_ e.print "Mail Address for broadcasting: " address = gets.chomp! local, domain = address.split("@") local and domain and break end dq = Dotqmail.new(local, domain).dotqmail homedir = File.dirname(dq) # p dq.homedir, dq.dotqmail() if test(?f, dq) then e.print "#{dq} ファイルは既に存在します。上書きしますか?(y/n)\n" e.print "#{dq} alread exists. Continue(y/n): " abort "中止します。" if /^y/i !~ gets end e.puts "このアドレスのメンバーにしたい宛先を順次入力して下さい(xで終了)。" e.puts "Enter members' email addresses for this address." require 'resolv' dns = Resolv::DNS.new members = [] while true e.print "Address(enter `x' for break): " email = gets.chomp! break if /^(x|\`x')$/i =~ email redo unless /@/ =~ email local, domain = parseaddress(email)[0].split("@") begin dns.getresource(domain, Resolv::DNS::Resource::IN::ANY) rescue e.print "#{e} というメイルドメインはみつかりません.\n" e.print "#{e} is nonexistent mail domain.\n" redo end e.print "[#{email}] added\n" members << $prefix+" "+email end e.puts "必ず返事が全員に戻るようなFrom:ヘッダの書き換えをしますか?" e.puts "Do you need From:-header hack to ensure reply comes back to all?" e.print "(y/n): " fromopt = (/^y/i =~ gets ? "-F " : "") e.puts "Subjectをまともなものに保つよう努力させますか??" e.puts "Shall I try to keep Subject: sane even if they drop it?" e.print "(y/n): " subjopt = (/^y/i =~ gets ? "-S " : "") e.puts "Cc自動登録モードを利用しますか??" e.puts "Use auto-subscription for Cc address?" e.print "(y/n): " staticopt = (/^y/i =~ gets ? "" : "-s ") if %r,/, !~ $0 then # no slashes myname = `which $0`.chomp myname.sub!(homedir, ".") else myname = File.expand_path($0) myname.sub!(homedir, ".") end while true if $dotqmaildir && test(?d, $dotqmaildir) dir = $dotqmaildir else e.print "書き出すファイル名は?\n" e.print "Specify the file name of dot-qmail.\n" e.printf("(Default %s): ", dq) newdq = gets.chomp break if newdq == "" end if test(?d, File.dirname(newdq)) && test(?w, File.dirname(newdq)) dq = newdq break else e.puts "存在するディレクトリのファイル名を指定して下さい" e.puts "Input file name in the existing directory." end end content = ["| #{myname} #{fromopt}#{subjopt}#{staticopt}-r #{address}"] output = (content+members).join("\n") e.puts "\n以下の内容で #{dq} を作成しました。" e.puts "-"*78+"\n"+output+"\n"+"-"*78+"\n" dqbase, dqdir = File.basename(dq), File.dirname(dq) open(dq, "w") do |dqf| dqf.puts output end # Create symlink .qmail-LIST-default -> .qmail-LIST dflt_dq = dq+"-default" File.unlink(dflt_dq) if test(?e, dflt_dq) File.symlink(dq, dflt_dq) # Return admindq = File.expand_path(dqbase+"-adm-default", dqdir) adminline = "| #{myname} -r #{address} -u" # printf("ln -s %s %s\n", dqbase, admindq) if $DEBUG open(admindq, "w") do |adq| adq.puts(adminline) end e.puts "\nまた以下の内容で #{admindq} を作成しました。" e.puts "-"*78+"\n"+adminline+"\n"+"-"*78+"\n" e.print "#{dq} ファイルの1行目の\n#{$0}\n" e.print "が適切でない場合は正しいパスに直しておいて下さい\n" e.print "At the 1st line of #{dq},\nyou see a filename as follows;\n" e.print " #{$0}\n" e.print "You may have to replace this with correct pathname.\n" exit 0 end def convunit(s) case s when /y$/ s.to_i * 365*24*3600 when /m$/ s.to_i * 30*24*3600 when /w$/ s.to_i * 7*24*3600 when /d$/ s.to_i * 24*3600 when /h$/ s.to_i * 3600 else s.to_i end end def splitaddresses(line) list = [] l = 0 inquote = nil while l/ =~ spec then [$2, $1.strip] elsif /(.*)\s*\((.*)\)/ =~ spec then [$1.strip, $2] else [spec.strip, nil] end end def extractrcpt(line, addresswithoutVERP) local, domain = addresswithoutVERP.split("@") lrx = Regexp.new("^"+Regexp.quote(local)+'-') drx = Regexp.new("@"+Regexp.quote(domain)+'$') # ' info = {} splitaddresses(line).each {|a| e, n = parseaddress(a) # If extracted address seems to be a VERP address of this address # it should be rewritten to my address without VERP. e = addresswithoutVERP if lrx =~ e && drx =~ e info[e] = n } info end # strip DEFAULT extension if $default then $rcpt = ENV["LOCAL"].sub("-"+$default, "") + "@" + ENV["HOST"] end while /^-.+/ =~ ($_=ARGV[0]) $_=ARGV.shift.dup break if ~/^--$/ while ~/^-[A-z]/ case $_ when "-install" install when "-t" extractrcpt(gets.chomp, "yuuji-all@gentei.org") when "-e" $expire = convunit(ARGV.shift) when "-F" $fromhack = true when "-s" $staticmember = true when "-S" $subjecthack = true when "-d" $dotqmaildir = ARGV.shift when "-o" $openlist = true when "-h" if /([^=]+)=(.*)/ =~ ARGV.shift then $header[$1] = $2 else STDERR.print "Value for -h options should be the form of\n" STDERR.print "header=value\n" exit 1 end break when "-r" $rcpt = ARGV.shift; break when "-f" $repl = true when "-u" $unsubscribe = true else ARGV.shift; break end $_.sub!(/^-.(.*)/, "-\\1") end end # Now $rcpt points to this correct address, prepare header hash. $header = { 'Reply-to' => $rcpt, 'X-ML-Driver' => hgid + " on Ruby #{RUBY_VERSION}", 'X-ML-Driver-URI' => myurl, 'X-ML-Info' => <<_EOM_, To get help, send empty mail with \"Subject: help\" to #{$rcpt}. _EOM_ } g = GuestDB.new($confdir+"/"+$rcpt) if !$rcpt then STDERR.print "Recipient unkown.\nUse -r RecipientAddress\n" exit 1 end def headervalue(line) line.sub(/^[^:]+:\s*/, "").chomp end class HeaderOP # This class is awkward workaround for using common db. def initialize(db = GuestDB.new) @db = db end def cachesubject(new) @db.setheader("Subject", new) end def oldsubject() o = @db.getheader("Subject").sub(/^(re([\[0-9\]:]) ?)+/i, "") o = "Re: "+o if o > "" o end def bettersubject(current) oldsbj = oldsubject().toeuc # remove superfluous re:re:re:... or Re[3]:... newsbj = current.sub(/^(re(\[?[0-9]?\]?) ?: ?)+/i, "Re: ") if /^$|^(re(\[[0-9]\]| )?:? ?)+$/i =~ newsbj # if Subject is empty or meaningless, use cached Subject. newsbj = oldsbj end newsbj = Time.now.strftime("%m-%d") if newsbj == "" cachesubject(newsbj) if oldsbj != newsbj newsbj.tobin end end hop = HeaderOP.new(g) def mkverp(rcpt, myaddress) local, domain = myaddress.split("@") local+"-"+rcpt.sub("@", $verpsep)+"@"+domain end def unverp(rcpt, myaddress) rlocal, rdomain = rcpt.split("@") mylocal, mydomain = myaddress.split("@") rlocal.sub("^"+Regexp.quote(mylocal)+"-", "") + mydomain end def replat(address) address.sub("@", $verpsep) end def rewritefrom(email, comment, newseed, g) # Assume from header has only one address spec # case orig # when /(\"?)(.*)(\1)<(.*)>/ # ## return $1+"<"+mkverp($2, newseed)+">" # if $2 then # comment, email, quote = $2, $4, $1 # return quote+comment+" "+replat(email)+quote+"<"+newseed+">" # else # email = $4 # /(\"?)(.*)(\1)/ =~ g.getcomment(email) # return $1+$2+" "+replat(email)+$3+"<"+newseed+">" # end # when /(.*) \((.*)\)/ # ## return mkverp($1, newseed)+" (#{$2})" # comment, email = $1, $2 # return "\"#{comment} #{replat(email)}\" <"+newseed+">" # else ## return mkverp(orig, newseed) comment = comment||g.getcomment(email)||"" # 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)} + replat(email)+" <"+newseed+">" # end end def getrcpt() info = {} dq = ".qmail" dq += "-"+ENV["EXT"] if ENV["EXT"] if $default && $default > "" dq.sub!("-"+$default, "") end dq = File.expand_path(dq, ENV["HOME"]) if test(?s, dq) then IO.readlines(dq).select {|x| /^#{$prefix}/ =~ x }.each {|x| # strip prefix and remove trailing comment string e, c = parseaddress(x.sub(/^#{$prefix}\s*/, "").sub(/\s*\#.*$/, "")) info[e] = c } end info end ### # Send message ### def sendmail(sender, rcpt, subject, body, header={}) # iso-2022-jp! if sender.is_a?(Array) and sender[1].is_a?(String) then from=sprintf("%s <%s>", NKF::nkf('-jM', sender[1]), sender[0]) sender=sender[0] else from=sender end if rcpt.is_a?(Array) and rcpt[1].is_a?(String) then to=sprintf("%s <%s>", rcpt[1], rcpt[0]) rcpt=rcpt[0] else to=rcpt end h=header.collect{|k, v| sprintf("%s: %s\n", k, v)}.join open("| sendmail -f #{sender} #{rcpt}", "w") {|s| s.print <<_EOF_.tojisbin From: #{from} To: #{to} Subject: #{subject.gsub("\n", " ")} Date: #{Time.now.to_s} #{h}Mime-Version: 1.0 Content-type: text/plain; charset=iso-2022-jp _EOF_ s.print body.to_s.tojisbin } end def unbase64(body) if /content-transfer-encoding:\s+base64/mi =~ body then # text/plain; charset=iso-2022-jp + base64 sc = (/charset=([\"\']?)utf-8/i =~ body ? "W" : "") body.sub!(/^content-transfer-encoding.*base64/mi, "") body.sub!(/^.*?\n\n\n/m, "\n\n") #body.sub!(/(\n\n)(.+)/mi, '\1'+NKF::nkf('-jmB', '\2')) body.sub!(/(\n\n)(.+)/mi){$1+NKF::nkf("-d#{sc}jmB", $2).tojisbin} #[body] body.split("\n") else # text/plain; charset=iso-2022-jp body = body.split("\n") body.reject!{|x| %r,^content-type: *text/plain,i =~ x} body.unshift("Content-type: text/plain; charset=iso-2022-jp") end end ### # Split multipart and restore ### def split_multipart(header, body) if header.find{|x| /^content-type: (.*);.*boundary=(['"]?)(\S*)\2/mi =~ x} then require 'nkf' ct, boundary = $1, "--"+$3+"\n" # +"\n" is essential! terminator = "--"+$3+"--\n" newct = "Content-Type: text/plain; charset=iso-2022-jp\n" m = body.join.split(boundary) # m = [leader, 1st-part, 2nd-part, ..., "--"] if %r,multipart/(alternative|mixed),i =~ ct then type = $1 if /alternative/i =~ type # purge alternative HTML m.reject!{|x| %r,\Acontent-type: *text/html;,i =~ x} end before = [] newbody = m.find{|x| %r,^content-type: *text/plain,im =~ x} trailing = m.index(newbody)+1 newbody = unbase64(newbody) while newbody[0] l = newbody.shift.tojisbin before << l+"\n" break if /^$/ =~ l end body = newbody.collect{|l| l+"\n"} if /alternative/i =~ type # strip alternative HTML header.reject!{|x| /^content-type:/i =~ x} header.unshift("Content-type: text/plain; charset=iso-2022-jp\n") return [header, [], body, []] end # else, content-type == text/mixed body.unshift("=== 添付ファイルに御注意 ===\n".tojisbin) tail = [boundary, m[trailing..-1].join(boundary), terminator] return [header, [boundary]+before, body, tail] end elsif header.find{|x| /^content-transfer-encoding:\s*(\S+)/mi =~ x} then #open("/tmp/header.txt", "w"){|x| x.print header.inspect} #open("/tmp/body.txt", "w"){|x| x.print body.join} case (enc=$1) when /base64|quoted-printable/i header.reject!{|x| /^content-transfer-encoding:\s*\S+/mi =~ x} header.reject!{|x| /^content-type:\s*\S+/mi =~ x} header.unshift("Content-Transfer-Encoding: 7bit\n") header.unshift("Content-type: text/plain; charset=iso-2022-jp\n") body = if /base64/i =~ enc require 'base64' NKF::nkf("-jd", Base64.decode64(body.join)) else NKF::nkf("-jmQ",body.join) end.tojisbin.split("\n").collect{|s| s+"\n"} end #open("/tmp/header2.txt", "w"){|x| x.print header.inspect} #open("/tmp/body2.txt", "w"){|x| x.print body.inspect} end [header, [], body, []] end ### # Main procedure ### if $unsubscribe default = ENV["DEFAULT"] user = default.sub($verpsep, "@") g.delete(user) if (owner=g.getvalue("Owner")) then sendmail($rcpt, owner, "Member Removed: #{user}", <<_EOF_) Removed #{user} from #{$rcpt} because of delivery failure. _EOF_ end exit 0 end body = [] before_body = [] after_body = [] hold = [] msghead = [ "Delivered-To: #{$rcpt}\n", "Delivered-To: #{ENV['RECIPIENT']}\n" ] header='' rcptinheader = [] userinfo = {} # all info. of .qmail-LIST and header. userinheader = {} # users in header $header["Subject"] = hop.oldsubject if ARGV[0] then regularmember = ARGV else userinfo = getrcpt() regularmember = userinfo.keys end while line=STDIN.gets # break if /^$/ =~ line if /^([a-z][-a-z]*):|^$/i =~ line cur = $1 if !hold.empty? then header = hold[0].split(":")[0] if /^(to|cc)$/i =~ header then newinfo = extractrcpt(headervalue(hold.join), $rcpt) newinfo.delete($rcpt) #XXX? userinfo.update(newinfo) userinheader.update(newinfo) rcptinheader += newinfo.keys if $fromhack then end elsif /^subject$/i =~ header then # bye|off|chaddr subj = headervalue(hold.join).chomp if /^(help|member|who|off|bye)$/i =~ subj.strip $commandmode = $1 elsif /^(123)$/i =~ subj.strip $lotmode = $1 else subj = hop.bettersubject(subj) if $subjecthack subj = NKF::nkf('-jM', subj) $subject = subj # for latter use hold = ["Subject: "+subj+"\n"] end elsif $fromhack && /^from$/i =~ header then # From should be 1 entry. email, comment = parseaddress(splitaddresses(headervalue(hold.join))[0]) userinfo[email] = comment if comment && comment != "" userinheader[email] = comment hold = ["From: "+rewritefrom(email, userinfo[email], $rcpt, g)+"\n"] end for h in $header.keys.sort if h.downcase == header.downcase then hold = ["#{h}: #{$header[h].chomp}\n"] if $repl $header.delete(h) break end end if !cur # end of header (/^$/) for h in $header.keys.sort hold << "#{h}: #{$header[h].chomp}\n" end hold << "\n" # delimiter with mail body end end msghead += hold break unless cur #hold = [] #hold << line hold = [line] else # Continuing line hold[-1] += line end end skipped = g.postonly($expire) # guest := all recipients in header except static member and this list guest = rcptinheader-regularmember-[$rcpt] if $commandmode recipients = [$sender] hrule = "-"*60+"\n" msghead = [ "To: #{$sender} Subject: #{$commandmode} result from #{$rcpt} From: Command result <#{$rcpt}> Date: #{Time.now.to_s} Reply-To: #{$rcpt}\n" ] case $commandmode when /help/ body = ["Send empty message putting command in subject.\n", "Subject にコマンドを入れて ${$rcpt} に送ると操作ができます。\n\n", "Commands are as follows: コマンド一覧:\n", "who check who are registered(登録メンバー一覧を取得)\n", "bye retire from list(自分のアドレスを登録解除)\n", "123 Number lot(番号くじを全員に発送.あみだくじ代わりに使える)\n", ].collect{|x| x.tojisbin} when /members?|who/ body = ["Member(s) of #{$rcpt}\n", hrule] + regularmember.collect{|n| "* "+n+"\n"} + g.list.collect{|n| "- "+n+"\n"} + [hrule, "* Core member (固定メンバー)\n".tojisbin, g.list.empty? ? "" : "- Guest(自動登録メンバー)\n".tojisbin ] when /off|bye/ if $default then # subgroup mode # not yet body = [ "Not yet implemented. Try to create new subgroup.\n", "サブグループからの解除はできないので、新しくグループを\n", "作り直して下さい。\n" ].collect{|x| x.tojisbin} elsif g.list.index($sender) then if /off/ =~ $commandmode then g.downdate($sender) body = [ "Stopped auto-mailing until you send to #{$rcpt}.\n", "次にあなたが #{$rcpt} にメイルを送るまで配送を停止します。\n" ].collect{|x| x.tojisbin} else g.delete($sender) body = [ "Remove your address from the list #{$rcpt}.\n", "#{$rcpt} から登録解除しました。\n" ].collect{|x| x.tojisbin} end else body = [ "You are not guest of #{$rcpt}.\n", "あなたは自動登録メンバーではありません。\n".tojisbin ] end end else # not command mode g.update($sender) if g.listall.index($sender) recipients = regularmember + g.list body = STDIN.readlines # Scrap and rebuild multipart message msghead, before_body, body, after_body = *split_multipart(msghead, body) if !skipped.empty? then body << "\n"+"="*30+"\n" body << "Old Member marked skip: "+skipped.join(", ")+"\n" body << "="*30+"\n" end if !$default || $default == "" # Update gecos of existing guests, if necessary guest.each{|i| if userinfo[i] && userinfo[i] > "" && g.list.index(i) then g.setcomment(i, userinfo[i]) end } if g.list.index($sender) && userinfo[$sender] && userinfo[$sender] > "" g.setcomment($sender, userinfo[$sender]) # update senders gecos end # Send participation guidance, if the $sender belongs to list. unregistered_guest = guest - g.list if !unregistered_guest.empty? && !$staticmember && !guest.empty? && ($openlist || recipients.index($sender)) then timeout = if $expire > 3600*24 then ($expire/3600/24).to_s + " days" elsif $expire > 3600 then ($expire/3600).to_s + " hours" else $expire.to_s + " seconds" end timeoutj = timeout.sub(" days", "日").sub(" hours", "時").sub(" seconds", "秒") unregistered_guest.each{|i| g.mkitem(i, userinfo[i]) # Add to dynamic member list # open("| qmail-inject -f #{$rcpt} -- #{i}", "w") {|w| sendmail($rcpt, i, "You are added to #{$rcpt}", <<_EOF_, {"Reply-to"=>$rcpt, "Delivered-to"=>$rcpt}) (Japanese below. 日本語の説明は下の方) You(#{i}) are added to mail list as a consequent of previous mail from "#{$sender}". Current member list is added at the bottom of this mail. You will be automatically unsubscribed from this list after #{timeout} without any response to the list. If you send email this address with Subject: member ... to get member list Subject: off ... to turn off mailing to you Thank you. 直前に届いた(はずの) "#{$sender}" さんからのメイルによってあなたのこの アドレス(#{i})は、 #{$rcpt} というアドレスのリストに追加されました。 以後、このアドレスに返事を送るとメンバー全員に届きます。ただし、 #{timeoutj}間あなたからの送信がなければ自動的にリストから解除されます。 この宛先に Subject(件名)を、 member にして送ると現在のメンバーリストが送られて来ます。 off にして送るとあなた宛の配送をOFFにします。 また、このリストに、現在メンバーでない人も追加したい場合は、 #{i} とその人両方宛にメイルを送って下さい。その人も自動登録され、 今の話題に参加できます。 詳しくは http://www.gentei.org/~yuuji/software/catchup/ を御覧あれ。 現在のメンバーは #{(recipients+guest).uniq.join("\n")} です。 _EOF_ } body << "\n"+"="*30+"\n" body << "New Member Added: "+guest.join(", ")+"\n" body << "="*30+"\n" end end end open("/tmp/raw2", "w") do |raw| raw.print((msghead+body).join) end if $DEBUG # filter recipients filtered = nil if $default then groupname = gn = nil plus = [] if Regexp.new("^"+$grouptag) =~ $default gn = $default[$grouptag.length..-1].downcase filtered = g.getgroup(gn) plus = userinheader.keys-filtered if !plus.empty? then g.addtogroup(gn, plus.collect{|x| [x, userinfo[x]]}) end end if filtered then groupname = gn elsif Regexp.new(".+"+$verpsep+".+") =~ $default newrcpt = $default.sub($verpsep, "@") filtered=[newrcpt] else sublist = nil filtered = [] $default.split("+").each {|word| pattern = Regexp.new(Regexp.quote(word), Regexp::IGNORECASE) # recipients is regularmember(==userinfo.keys) and g.list. sublist = userinfo.keys.select{|r| pattern =~ userinfo[r] || pattern =~ r } #if sublist.empty? then sublist += g.list.select{|r| (c=g.getcomment(r)) && pattern =~ c || pattern =~ r } # end filtered += sublist } filtered.uniq! if !filtered.empty? then # create new group filtered << $sender mem = filtered.collect{|m| [m, userinfo[m]||g.getcomment(m)]} # mem << [$sender, userinfo[$sender]] groupname = g.newgroup(mem) end end if filtered.empty? then body = [sprintf("[%s] にマッチするユーザはいませんでした\n", $default).tojisbin + "----- 現在のメンバー -----\n".tojisbin + userinfo.values.join("\n") + g.list_comment.join("\n") + "--------------------------\n"] + body recipients = [$sender] else # Prepend notification local, dom = $rcpt.split("@") ns = sprintf("%s-%s%s@%s", local, $grouptag, groupname, dom) body.unshift(sprintf("※%s さん発サブグループ配送\n"+ "%s=[%s]\n%s\n%s\n", userinfo[$sender], ns, filtered.collect {|f| userinfo[f] || g.getcomment(f) || f }.join(", "), plus.empty? ? "" : "NEW: "+plus.join(", "), "-"*20).tojisbin) plus.each {|em| sendmail(ns, em, "Subject: You are added to #{ns}", <<_EOF_) (Japanese after English. 日本語は下に) You(#{em}) are added to mailing list; #{ns} by the posting from #{$sender}. You may receive that message simultaneously, check it. The members are as follows. #{filtered.join("\n")} これは上記のメンバーからなるメイリングリストです。 ほぼ同時に届いた #{$sender} からのメイルによってあなたのアドレスが 自動的に追加されました。今後の話題進行はこの #{ns} のアドレスをご利用下さい。 _EOF_ } s = $sender recipients = filtered msghead.reject!{|x| /^(from|reply-to): /i =~ x} msghead.unshift("From: "+rewritefrom(s, userinfo[s], ns, g)+"\n") msghead.unshift("Reply-to: "+ns+"\n") end end if ENV["RPLINE"] then # ENV["QMAILINJECT"] = "r" tee = $DEBUG ? "tee /tmp/#{myname}-out |" : "" local, domain = $rcpt.split("@") vsender = local+"-"+ (filtered ? replat($sender) : "adm")+"@"+domain case $lotmode when "123" lot = (1..recipients.length).to_a before_body = [] after_body =[sprintf("(%s の%d人くじの結果です)\n", Time.now.strftime("%Y-%m-%d %H:%M:%S"), recipients.length).tojisbin] end for r in recipients verp = mkverp(r, vsender) # open("| #{tee}qmail-inject -f #{verp} -- "+r, "w") do |out| case $lotmode when "123" n = rand(lot.length) body = [sprintf("%d番です\n", lot[n]).tojisbin] lot.delete_at(n) end open("| #{tee}sendmail -f #{verp} -- "+r, "w") do |out| if /@(docomo|ezweb|softbank|(.*\.)?pdx)\.ne.jp/ =~ r # for foolish cellular MUA, hack To: address towards to itself. i = 0 while i