メーリングリストを運用したいので、FML を使おうとしたのだが、機能が多すぎでいまいち使いにくい。
ということで、最小限の機能を持ったマネージャを作ってみた。
機能としては、これくらい。
・1メーリングリスト1スクリプト
・メンバー以外からの投稿は拒否する(弱気にFromヘッダで判断)
・サブジェクトは連番を入れて書き換える
・Reply-To: や Sender: は設定する
・それ以外は素通し(文字コードや添付ファイルもそのまま)
・送った物を一応保存しておく
・管理機能は無く、メンバー管理は、直接ファイルを編集する
gems mail を使おうかとも思ったが、Subjectのエンコードを変更してしまうみたいで、やめた。
厳密にやるのはめんどくさいので、手を抜いた。
#!/usr/local/bin/ruby require "net/smtp" require "base64" ML = "foo" DOMAIN = "example.jp" MLADDR = "#{ML}@#{DOMAIN}" OWNER = "#{ML}-owner@#{DOMAIN}" SUBJ = "[#{ML.upcase} %06d] %s" NOTMEMBER = <<EOT From: #{OWNER} To: %<dest>s Subject: You %<dest>s are not member #{ML} ML Mime-Version: 1.0 Content-Type: text/plain; charset="iso-2022-jp" あなたはこのメーリングリスト <#{MLADDR}> のメンバーではありません。 メーリングリストへの登録は、 #{OWNER} にメールしてください。 EOT DIR = "/var/spool/ml/" MAILDIR = File.join(DIR,ML,"spool") #メール保存ディレクトリ SEQFILE = File.join(DIR,ML,"seq") #連番管理ファイル MEMBERS = File.join(DIR,ML,"members") #投稿可能アドレス群 RECIPIENTS = File.join(DIR,ML,"recipients") #配布先アドレス群 def get_seq File.open(SEQFILE, File::RDWR|File::CREAT, 0644) do |f| f.flock(File::LOCK_EX) seq = f.read.to_i + 1 f.rewind f.puts seq f.flush f.truncate(f.pos) seq end end def read_addrs(file) IO.readlines(file).map{|x| x.sub(/#.*/,"").strip}.grep(Header::MAILre) end def not_member(dest) if dest Net::SMTP.start("localhost", 25, OWNER) do |smtp| smtp.send_message(sprintf(NOTMEMBER,dest: dest) .encode(Encoding::ISO_2022_JP).force_encoding(Encoding::ASCII_8BIT), OWNER, [dest,OWNER]) end end end class Header < String MAILre = %r<[A-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Z0-9-]+(?:\.[A-Z0-9-]+)*>i SUBJre = %r<^\s*(Re:|Fw:)?(\s*\[#{ML} \d+\]\s*((Re:|Fw:)\s*)*)+>i ENCODEre = %r<=\?([A-Z0-9_-]+)\?B\?([A-Z0-9/+=]+)\?=>i ENCODE = "=\?%s\?B\?%s\?=" def from if /^From:.*\n(\s+.*\n)*/i =~ self if MAILre =~ $& $& end end end def check_from read_addrs(MEMBERS).include?(self.from) end def del(hdr) self.gsub!(/^#{hdr}:.*\n(\s+.*\n)*/i,"") end def add(hdr,content) self << "#{hdr}: #{content}\n" end def subject(seq) unless self.sub!(/^Subject:\s*(.*)/i) do subj = $1.sub(SUBJre,"\\1 ") if ENCODEre =~ subj subj.sub!(ENCODEre) do |s; enc| enc = $1 ENCODE % [enc, Base64.strict_encode64( Base64.strict_decode64($2).sub(SUBJre,"\\1 "))] end end "Subject: "+(SUBJ % [seq, subj]) end add("Subject", SUBJ % [seq, ""]) end end end header, nl, body = STDIN.read.split(/(\r?\n){2}/,2) header = Header.new(header << nl) unless header.check_from not_member(header.from) exit end header.del("Received") header.del("Reply-To") header.add("Reply-To",MLADDR) header.del("Sender") header.add("Sender",OWNER) seq = get_seq header.subject(seq) data = header+nl+body Net::SMTP.start("localhost", 25, OWNER) do |smtp| smtp.send_message(data, OWNER, read_addrs(RECIPIENTS)) end open(File.join(MAILDIR,seq.to_s),"w") { |f| f.write data }
手を抜いた点。
・Fromアドレスを抜き出す方法 (From: foo@example.jp
・Subject書き換えの時、1行目しか見ない
このあたりは、gemsを使えばちゃんと出来るのだが。
使い方としては、/etc/aliases に、
foo: :include:/usr/local/lib/ml/foo foo-owner: 管理者メールアドレス
と追記して、newaliases。
/usr/local/lib/ml/fooには、
"|/usr/local/bin/このスクリプト ; exit 0"
(弱気ならば exit 0 を付けて、強気ならば付けない。デバッグ時はもちろん付けない)
と書いて、このファイルの所有者が /var/spool/ml/foo 以下を書けるようにディレクトリを作成して、
members と recipients と seq を記入。