perl の import 関数、Storable パッケージを使ってメール転送用ルール管理ツールを作ってみる修行

飛んできたメールを、その送信元アドレスにしたがって転送するツールを perl で作ってみた。言ってみれば、会社の代表番号にかかってきた電話を、「××商事の○○と申します。」と聞いただけで、「××商事の○○様ですね。△△課の☆☆におつなぎします。」と転送する人みたいなもの。というか、発信元電話番号にしたがって自動的に該当の人の内線に切り替えるようなものか。ともかくそんなもの。
たとえば、「abc@example.com」からきたメールは基本的に「abc@example.org」と「デフォルトドメインの test-abc」の二つのアドレスに転送する、んだけど、拡張部分が「j」の場合は「abc@example.co.jp」に、「u」の場合は「abc@example.co.us」に、「org」の場合は「abc@example.org」に、それぞれ転送してね、なんてのを一つのルールとする。わかりやすく(ないかもしれないけどともかく)書いてみると、こんなのが一つのルール。

abc@example.com => abc@example.org,test-abc
 j   -> abc@example.co.jp
 u   -> abc@example.co.us
 etc -> abc@example.org

でもって、こんなルールが複数ある。これが何に便利か、という話はまた今度。

ハッシュべた書き実装

ともかく、このようなツールを作ってみたのだが、最初は特に何のパッケージも使わず、クラスの概念も用いず、さくっと書いた。ルールはハッシュをべた書き。まあ、こんな感じ。

$rules{'abc@example.com'} = {
    0     => 'abc@example.org,test-abc', # デフォルトね
    'j'   => 'abc@example.co.jp',
    'u'   => 'abc@example.co.us',
    'etc' => 'abc@example.org',
};

でも、使われ続けるうちに、やっぱりルールは別に管理して、他の人でもメンテナンスしやすくできるようにしたいと思った。

求められるもの

ということで、まずはルール周りだけを取り出して実装することにした。こんな要件か。

  • メールをさばくからあんまり重いのはダメだよね
    • 更新はともかく読込みは速くないと
  • 人間が読みにくいのはダメだね
    • /etc/aliases みたいに更新時は追加とかじゃなくてまるごと置換えってのが面倒ないよね

で、こんな形式でルールを書くようにしてみた。

$ cat green.txt
abc@example.com => abc@example.org,test-abc
 j   -> abc@example.co.jp
 u   -> abc@example.co.us
 etc -> abc@example.org
test@why.not  => test@why.not
# test2@why.not => test@why.not
test@trash.box.com => trash-box@example.org
 1 -> trash-box-1@example.org
 2 -> trash-box-1@example.org
# 3 -> trash-box-1@example.org
 4 -> trash-box-1@example.org

(green ってのは特に意味ないけど、ルールセットの識別子。)更新時は newaliases みたいな感じでこれを食わすとデータベース化され、読込み時はハッシュとして取り出せるイメージ。あ、# で始まる行はコメントってことで。

Storable パッケージ

いろいろ調べているうちに Storable パッケージに行き着いた。MLDBM がよさそうだったけど、メールサーバに入ってなかった。メールサーバいじるのは怖いので、パッケージのインストールはなしの方向で。昔、たぶん 13 年前くらいに GDBM_File を tie/untie したとき以来。perlRDBMS 以外のデータベースアクセスをするのは。

できたもの

で、できたものがこんな Avelio::SympleRuleStorage パッケージ。あ、simple のスペル間違ってる。しかも、simple じゃないし。もっと適切な名前考えなきゃ。

package Avelio::SympleRuleStorage;
use Storable qw(store retrieve);

my $BASE_DIR = q{};

sub import($) {
    my ($package_name, $base_dir) = @_;
    if ($base_dir) {
        $BASE_DIR = $base_dir;
    }
    else {
        $BASE_DIR = '/etc/';
    }

}

sub new($$) {
    my ($clazz, $prefix) = @_;
    bless {
        table  => {},
        prefix => $prefix,
        path   => "$BASE_DIR/$prefix.rules",
    };
}

sub updateRulesWithSTDIN($$$) {
    my ($self) = @_;
    my %rules;
    my $mainkey = q{};
    while (my $line = <STDIN>) {
        next if ($line =~ /^\#/o);
        chomp($line);
        my ($key, $value) = split(/[=-]>/, $line);
        $key   =~ s/\s+//og;
        $value =~ s/\s+//og;
        if ($line !~ /^ /o) {
            $mainkey = $key;
            $key = '0';
        }
        $rules{$mainkey}{$key} = $value;
    }
    store(\%rules, $self->{path}) or die;
}

sub showRules($) {
    my ($self) = @_;
    my $storage = $self->loadRules();
    foreach my $key (keys %{$storage}) {
        my $rule = $storage->{$key};
        print "$key => ". $rule->{0}."\n";
        foreach my $cond (keys %{$rule}) {
            next if $cond eq '0';
            print " $cond -> ". $rule->{$cond}."\n";
        }
    }
}

sub loadRules($) {
    my ($self) = @_;
    $self->{storage} = retrieve($self->{path});
}

1;
__END__

どうでもいいけど、はじめて import 関数使った。これを使っている管理ツール maintrule.pl は、こんな感じ。

#!/usr/bin/perl
BEGIN {
    unshift(@INC, '/home/gorbachev/lib');
}
use Avelio::SympleRuleStorage qw(/home/gorbachev/rules);

my ($action, $prefix) = @ARGV;
my $storage = new Avelio::SympleRuleStorage($prefix);

if (   $action eq 'show')   {    $storage->showRules();              }
elsif ($action eq 'update') {    $storage->updateRulesWithSTDIN();   }
elsif ($action eq 'test')   {                                        }
else                        {    exit;                               }

# end of file

使い方

ルールを更新するときは、こんな感じで。

$ ./maintrule.pl update green < green.txt

そうすると /home/gorbachev/rules/green.rules が更新される。なかったときには新規に作られる。
で、中身を確認するときは、こんな感じで。

$ ./maintrule.pl show green
abc@example.com => abc@example.org,test-abc
 u -> abc@example.co.us
 j -> abc@example.co.jp
 etc -> abc@example.org
test@trash.box.com => trash-box@example.org
 1 -> trash-box-1@example.org
 4 -> trash-box-1@example.org
 2 -> trash-box-1@example.org

一応、こんなこともできるはず。

$ ./maintrule.pl show green |head -20| ./maintrule.pl update red

今後

単体テスト

テストスイートを用意しておいて、

$ ./maintrule.pl test green

とかやると、テストしてくれたりすると嬉しいかな。テストスイートは green.suite とかいうディレクトリを掘って、その中にテストケースのファイルを入れておくみたいな感じかな。

メール転送ツールへの導入

これを本来の目的のメール転送ツールに導入しないと。とりあえず、本番に使っているルールセットを blue とした場合、こんなイメージ。


入れ替える予定のルールセットは skyblue とかにして、

$ ./maintrule.pl update skyblue < skyblue.txt

やら

$ ./maintrule.pl test skyblue

を繰り返して skyblue をさんざん叩く。で、大丈夫かな、と思ったら、

$ ./maintrule.pl show blue | diff - skyblue.txt

とかで変更点を確認して、問題なければさくっと

$ cp -vi /home/gorbachev/rules/skyblue.rules /home/gorbachev/rules/blue.rules

って上書きしちゃう、みたいな。

実際にメール転送ツールに導入し終わったら、それはそれで公開の予定。