簡易メーリングリストマネージャ

メーリングリストを運用したいので、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 だとfooの方を見てしまう)
・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 を記入。