メール中の URL を用いたスパムフィルタリング

スパムメール対策に某社製エンタープライズ向けスパムゲートウェイアプライアンスを試す機会がありました。手許のスパムメール約 3,000 件を使ったテストでは、その 75% を メール中の URL によってフィルタリングする機能によりブロックすることができました。 ベイジアンフィルタに比べるとやってることは単純な気がするのですが、効果は大きいですね。

というわけで公開 URI ブラックリストである URIBL/SURBL を使うとそれなりの効果があるかも、という期待はあるのですが、SpamAssassin にこれらを参照する機能はあっても自分の使っている bogofilter にはないのでした…

まあ、その製品はこれらのリストではなく、メーカー自身が提供するリストを使用するので同程度の効果となるかは何とも言えないところではありますが、ベイジアンが全てではないということで。


追記
厳密に言うと URL データベースへの登録タイミングが問題になってきますが、テスト当時において最新~約1ヶ月前迄のスパムメールを用いました。 集計した数字はないですが、実環境においてもかなりの効果が得られています。

bogofilter のデータベースのエンコーディングについて

bogofilter は何も考えずに使い始めると unicode でデータベースを作ってくれます。

$ bogoutil -w ~/.bogofilter/wordlist.db .ENCODING
spam   good
.ENCODING                           2      0

ここで spam の下の値は 1=raw、2=unicode を意味します。 この例では unicode のデータベースになっていることがわかります。

しかし、これまで紹介した手順では EUC テキストを bogofilter に喰わせているわけで、 bogofilter はそれを iso-8859-1 と信じて unicode に変換してデータベースに格納してしまうのです。

ですので、FAQ 等に載っているように "bogoutil -d" でデータベースを見ようとしても日本語が化けて困ってしまうのです。 とりあえず以下のコマンドを打てば Cygwin 環境ではきちんと日本語も SJIS で出力され、その内容を確認できます。 (最後の nkf で出力エンコーディングを調整してください)

$ bogoutil -d ~/.bogofilter/wordlist.db | iconv -c -f utf-8 -t iso-8859-1 | nkf -Es

たまにこうしてデータベースの内容を見てみると色々な発見ができるのではないでしょうか?

さて、わざわざ変換に手間をかけるならば raw でいいではないか、ということになります。 unicode から raw にデータベースを変換するやり方は先の FAQ に出ているのでこれを参考に raw encoding のデータベースファイルを作ります。

$ cd ~/.bogofilter
$ bogoutil --unicode=no -m wordlist.db
$ bogoutil -w wordlist.db .ENCODING
spam   good
.ENCODING                           1      0

これで "bogoutil -d" の結果が EUC で出力されるようになりました。

なお、iconv を使った変換もしてみましたが、.ENCODING の値は 2 のままでした。 ただし、データベースの内容は raw (=EUC) になってはいるようです。 それ以上突っ込んだ確認はしていません。

bogofilter を正しく使おう – 2007年 6月版

さて、bogofilter 使用環境での spam 登録/ham 登録/判定のためのスクリプトの改訂版です。 このスクリプトは元々 Mew のマーク付けに呼び出して使うために書いています。 何故このスクリプトが必要かというと、nkf では本文が base64 等でエンコードされたメールをきちんとデコードできないからです。 Ruby で書かれた前処理のフィルタ (その1その2) は見つかったのですが、perl のものが見当たらないので自分で書いてみたのです。外部コマンド実行のオーバーヘッド削減を目的として Text::Kakasi や NKF モジュールを用いています。

前回から何が変わったかというと、MIME の階層をきちんと辿るようにしました。前回のものはこれが不十分でした。 また、コンテンツタイプがテキスト以外のものは Content-Type フィールドを表示して、学習に生かせるようにしました。実行時の各種オプションも新設しました。

#!/usr/bin/perl
use Email::MIME;
use NKF;
use Text::Kakasi;
use Getopt::Std;
my $nkfopt  = "-e";
my $bogoopt = "";
my $printmode = 0;
sub mime_body {
my $o = shift;
my @p = $o->subparts;
my $m = "";
if (@p > 0) {
foreach my $so (@p) {
$m = $m . mime_body($so);
}
}
elsif (!defined($o->content_type) || $o->content_type =~ /text/i) {
$m = $m . nkf($nkfopt, $o->body);
}
else {
$m = $m . $o->content_type . "\n";
}
return $m;
}
local $/;
my $k = Text::Kakasi->new;
$k->set("-w");
getopt('snp');
if (defined($opt_s)) { $bogoopt = "-s"; }
if (defined($opt_n)) { $bogoopt = "-n"; }
if (defined($opt_p)) { $printmode = 1;  }
LOOP: foreach $filename (@ARGV)  {
open(I, $filename) || next LOOP;
my $message = <I>;
my $parsed = Email::MIME->new($message);
$message = nkf($nkfopt, $parsed->header_obj->as_string . "\n") . mime_body($parsed);
my $tokenized = $k->get($message);
if ($printmode) {
print $tokenized;
}
else {
open(O, "| bogofilter $bogoopt") || die("can't execute bogofilter");
print O $tokenized;
close(O) && print "$filename\n";
}
close(I);
}

このスクリプトを bogo として、使い方を簡単に説明します。 そのフォルダのメールをスパムとしてデータベースに登録するには次のように呼び出します。

bogo -s -- *

そのフォルダのメールをハムとしてデータベースに登録するには次のように呼び出します。

bogo -n -- *

"-p" オプションは bogofilter に喰わせる前のメールデータを標準出力に出力します。 kakai による分かち書きの結果も確認できます。

bogo -p -- *

引数でオプションをつけずファイル名のみ渡すと、指定したファイルのうち spam と判定されたメールのファイル名のみ標準出力に出力します。 これを Mew のマークに使います。

bogo *

利用している perl のメール関連パッケージは以下の通りです。この順で "perl Makefile.PL; make; make install" を実行しインストールしました。

  • MIME-Types-1.20
  • MIME-Base64-3.07
  • Email-MIME-ContentType-1.014
  • Email-MIME-Encoding-1.311
  • Email-Simple-1.999
  • Email-MIME-1.859

この他に Text::Kakasi と NKF を用いています。

メール関連のこの辺りのモジュールは使いやすいと思います。 日本語のサイトで取り上げている例は少ないようですが、このコードが処理の参考になればと思います。

Cygwin 上で bogofilter を再インストール

ハードディスク換装に伴い Cygwin もインストールし直しとなり、bogofilter を最新のソース再コンパイルしました。 前回紹介した時とバージョンやら configure のオプションやらが若干異なるので記録しておきます。導入したバージョンは bogofilter-1.1.5 です。

まず、Cygwin でデフォルトではインストールされない以下のパッケージをインストールしておきます。

db4.3
libdb4.3
libdb4.3-devel
gsl
gcc
make

私の場合、ソースをコンパイルする前にリンクをいくつか張る必要がありました。

cd /bin
ln -s /bin/bash sh
cd /lib
ln -s libdb-4.3.a libdb.a
ln -s libdb-4.3.dll.a libdb.dll.a
ln -s libdb-4.3.la libdb.la
ln -s libdb_cxx-4.3.a libdb_cxx.a
ln -s libdb_cxx-4.3.dll.a libdb_cxx.dll.a
ln -s libdb_cxx-4.3.la libdb_cxx.la

さて次はコンパイル、インストールですが、configure で db パッケージのヘッダファイルを明示的に指定する必要があります。

./configure CPPFLAGS=-I/usr/include/db4.3
make
make install

これで無事 bogofilter がインストールされます。最近は /usr/local/etc/bogofilter.cf に次のように書いて判定時のしきい値を調整しています。

spam_cutoff = 0.80

bogofilter を呼び出すスクリプトもいろいろ手を入れていますが、これは次の記事に書きます。

Mew 用 bogofilter 呼び出しスクリプトの書き直し

以前の記事、「bogofilter を正しく使おう」で書いた Mew 用判定スクリプトですが、高速化のために書き直しました。kakasi、nkf を perl のモジュールとして使用して全て perl で処理するようにしたのです。すると何と実行時間が 1/3 に短縮されました。

perl からこれらを使うために kakasi は Text::Kakasi を、nkf はソースを展開した後の NKF.mod サブディレクトリ以下にあるモジュールをインストールする必要があります。cygwin 上で特に特別なオプションをつけることなくコンパイル、インストールができました。

#!/usr/bin/perl
use Email::MIME;
use NKF;
use Text::Kakasi;
local $/;
my $k = Text::Kakasi->new;
$k->set("-w");
LOOP: foreach $filename (@ARGV)  {
open(I, $filename) || next LOOP;
my $message = <I>;
my $parsed = Email::MIME->new($message);
my @parts = $parsed->parts;
$message = nkf("-e", $parsed->_headers_as_string . "\n");
foreach my $p (@parts) {
if ($p->content_type =~ /text/i) {
$message = $message . nkf("-e", $p->body);
}
}
my $tokenized = $k->get($message);
open(O, "| bogofilter") || die("can't execute bogofilter");
print O $tokenized;
close(O) && print "$filename\n";
close(I);
}

稀におかしな形式のメールを処理するときに Email::MIME モジュールから Carp::croak が呼ばれて die してしまいやり残しのメールが出てしまうのですが、どうしたのものか考え中です。


2007. 2. 14 追記:

eval を使えば die をトラップできるというところまで調べました。ただ、綺麗に書ける自信なしです。


2007. 6. 16 追記:

内容を修正して改訂版を作っていますのでこちらを参照ください。

Email::MIME モジュール

先のエントリに書いた通り bogofilter を使う前処理のために Email::MIME を使いましたが、このモジュールのインストールについて説明しておきます。perl は cygwin の 5.8.7 を使用しているのですが、Email::MIME モジュールを動かすために以下のモジュールを追加インストールしました。


・MIME-Types-1.17
・MIME-Base64-3.07
・Email-MIME-ContentType-1.01
・Email-MIME-Encodings-1.3
・Email-Simple-1.980
・Email-MIME-1.851


依存関係があるのでこの順でインストールします。それぞれ CPAN から探して取得し、ソースを展開後 “perl Makefile.PL; make; make install” を実行します。


それとインストールが終わってこのモジュールを使ったプログラムを実行する時の話なのですが、次のような Content-Type フィールドを含むメールの処理中に “Illegal Content-Type parameter at /usr/lib/perl5/site_perl/5.8/Email/MIME.pm line 13” というエラーメッセージが出ます。


Content-Type: text/plain;
Content-Transfer-Encoding: base64


どうやら “text/plain” の後に “;” が入っているのがまずいようです。”;” がないメールや “;” の後に属性の指定があるメールについてはエラーメッセージが出ません。ただ、エラーメッセージが表示されてもきちんとデコード処理等は行われているので気にしないことにしました。


そうそう、細かい話ですがドキュメントに、


my Email::MIME @parts = $parsed->parts;


とあるのは、


my @parts = $parsed->parts;


としなければ動きませんでした。

bogofilter を正しく使おう

cygwin を更新すると bogofilter が動かなくなったので最新のソースからインストールし直しました。更に、プライベートではあまりメールを使わないのでこれまで真剣に取り組まなかったのですが、最近 bogofilter の判定精度がいま一つのようなので原因を調べてみました。するといろいろなことがわかりました。


・MIME エンコードされた本文がきちんと処理できていない
・データベースがいまひとつ


これまでのやり方はここに書いた通りです。
それぞれの問題の詳細を見てみましょう。
1) MIME エンコードされた本文
本文が MIME エンコードされたスパムが多くなっているのですが、これを nkf や kakasi に喰わせる前にデコードしなければなりません。あちこちの Web に nkf の “-m” で MIME デコードと書いてあったのでそれをそのまま信用していたのですが、実は “-m” だけでは何も起きていないことがわかりました。


参考までにnkf のソース(最新は 2.0.7) でオプションを解析する部分を見ると、


case ‘m’: /* MIME support */
/* mime_decode_f = TRUE; */ /* this has too large side effects… */
if (*cp==’B’||*cp==’Q’) {
mime_decode_mode = *cp++;
mimebuf_f = FIXED_MIME;
} else if (*cp==’N’) {
mime_f = TRUE; cp++;
} else if (*cp==’S’) {
mime_f = STRICT_MIME; cp++;
} else if (*cp==’0′) {
mime_decode_f = FALSE;
mime_f = FALSE; cp++;
}
continue;
(nkf.c より)


のようになっています。”-mB” とすると base64 のデコードをしますが、メール全体をデコードしてしまいます。


仕方ないので perl の Email::MIME を利用して MIME 形式のメールをデコードするスクリプトを作成しました。と言ってもとても簡単です。


#!/usr/bin/perl
# prebogo
use Email::MIME;

local $/;
my $message = <>;
my $parsed = Email::MIME->new($message);
my @parts = $parsed->parts;

print $parsed->_headers_as_string . “\n”;
foreach my $p (@parts) {
if ($p->content_type =~ /text/i) {
print $p->body;
}
}


これ (prebogo とする) を nkf の前に入れます。例えば newham スクリプトは次のようになります。


#!/bin/sh
prebogo | nkf -e | kakasi -w | bogofilter -n -v


判定用の bogo スクリプトは次の通りです。


#!/bin/sh
PATH=/bin:/usr/bin:/usr/local/bin:$PATH

for i in $*; do
prebogo $i | nkf -e | kakasi -w | bogofilter
if [ $? -eq 0 ]; then
echo $i
fi
done


「判定に用いるスクリプトのパフォーマンスが悪い」、「パートごとに charset が違うときはどうする?」、「content-type に “application/octet-stream” を指定してテキスト入れてくるスパムも除外したいなあ」等の問題はありますが、とりあえず今の段階ではここまでにしておきます。
2) データベースの内容
まずはどの程度の数のメッセージを学習させたか確認しようとしました。


bogoutil -w ~/.bogofilter/wordlist.db .MSG_COUNT


を実行すると学習させたメッセージの数が出力されるのですが、思ったほど数が増えていません。実は spam/ham の登録は、 mew の summary モードより以前の記事で作成した newspam/newham スクリプトを呼び出していたのですが、このときに bogofilter コマンドのオプションに “-N”/”-S” を指定して登録するのはまずいようです。


これらのオプションはメッセージを間違えて登録してしまった時にそれを取り消す場合のみ指定するもので、例えば “-S” については bogofilter のマニュアルに以下のような記載があります。


If -S is used for a message that wasn’t registered as spam, the counts will still be decremented.


ということは間違って登録したわけではないのにこれらのオプションを使ってしまうとカウントが減ってしまいます。


そこで newspam/newham スクリプト中の bogofilter 実行オプションを “-s -N” → “-s” 、”-n -S” → “-n” と変更しました。これも某所で見かけたオプションを深く考えず使っていました。newham は先の通りですが、newspam もついでに載せておきます。


#!/bin/sh
prebogo | nkf -e | kakasi -w | bogofilter -s -v


これらを改善してデータベースを作り直してから使うと大分判定が良い感じになりました。前の記事では書いていませんが、スパムだけでなくハム(正常なメール)も学習させておかなければなりません。特に nkf の “-m” オプションは、間違って使って「bogofilter は使えない」と思ってしまった人が結構いるのではないでしょうか。
教訓: きちんと自らオプションの意味を確認しなければならない。

2007. 6. 16 追記:

内容を修正して改訂版を作っていますのでこちらを参照ください。

(更に追記)
nkf はデフォルトの動作で、

Subject: =?iso-2022-jp?B?GyRCJUYlOSVIJWEhPCVrGyhC?=

のような行はデコードしてくれますね。いずれにせよ、

Content-Transfer-Encoding: base64

となっているパートをデコードしてくれるわけではないです。

Bogofilter の導入 (Meadow, Mew, Cygwin 環境)

某プロバイダのメールアドレスを持っているのだが、ありがちなユーザ名なので、スパムが大量に来る。と言うか、来るメールのほとんどがスパムと言ってよい。ちょっと前までザウルスで受信していたのだが、スパム処理が馬鹿らしくなったので、パソコンで受けてフィルタリングソフトを導入することにした。ちなみに某プロバイダではメールアドレスを変えるのに \3,000 かかるのでケチくさいが大した使っていないアドレスを変えるのにお金をかけるのはもったいないし、スパムの傾向を見るのも社会勉強だろう。


導入したソフトは BogofilterMeadow + Mew + Cygwin の環境に導入した。ついでに Mew も 4.2 へバージョンアップ。ネタとしては古いかも知れないけれど仕方ない。


ちなみに職場のメールは MS-Outlook + Exchange という環境。これはこれでいいけど。(本当か?)


以下、導入について。


cygwin の setup.exe で、


libdb4.2
libdb4.2-devel (いずれも category は database)
gsl (category は libs)


をインストールしておく。


bogofilter-0.92.8 を展開。README の通り、


./autogen.sh
make
make install


を実行。t.env と t.lock3 のテストが FAIL するが、気にしないことにする。前者は空白混じりのパスが問題。後者はよくわからないが、


test “x‘grep -v 0 $TMPDIR/exits‘” = x


の行でコケているようだ。


Mew のドキュメントにもスパム用の設定が載っているが、それを参考にしつつ、日本語スパムへの対策強化のため、nkf と kakasi を使うように変えてみる。kakasi は Win32 用バイナリ使用。nkf は以前より cygwin 用にコンパイルしてインストールしてあった。


次のような newspam, newham という2つのスクリプトを作って cygwin の /usr/local/bin の下におく。


#!/bin/sh
# newspam

nkf -m -e | kakasi -w | bogofilter -s -N -v

#!/bin/sh
# newham

nkf -m -e | kakasi -w | bogofilter -n -S -v

nkf の -m オプションは知らなかったが、MIME デコードするオプション。このあたりは先人達の知恵をそのまま借用。


.emacs に次の行を入れる。これで ‘ls’, ‘lh’ コマンドでスパム/ハムを学習することができる。


(setq mew-spam-prog “bash”)
(setq mew-spam-prog-args ‘(“–login” “-c” “newspam”))
(setq mew-ham-prog “bash”)
(setq mew-ham-prog-args ‘(“–login” “-c” “newham”))


次にスパムをマークするためのスクリプトを作成する。次の内容で bogo という名のファイルを /usr/local/bin の下に作成する。


#!/bin/sh
# bogo

PATH=/bin:/usr/bin:/usr/local/bin:$PATH

for i in $*; do
nkf -e -m $i | kakasi -w | bogofilter
if [ $? -eq 0 ]; then
echo $i
fi
done


これで、'”‘ コマンドで ‘bash /usr/local/bin/bogo’ と入力するとスパムにマークがつく。多分もっと美しいやり方があるのだろうけど、とりあえず用が足りればオッケーとする。


おっと、実際はその前にスパムを学習させねばならない。スパムのメールファイルが存在するディレクトリで、


for i in *; do
nkf -e -m $i | kakasi -w | bogofilter -s
done


などとやっておけば良い。


さて現在の状況だがスパムの処理が楽になったものの、だからといって普通のメールが増えるわけではない。何か虚しいような気もするが、スパムを受けて処理するのも社会勉強ということで。