2006年06月30日

届いたメールの料理の仕方 (3) 〜qmail編〜

はてなブックマークに登録

前回の後半で、dot-qmailで実行したコマンドの終了コードによって、次の配送を行うかどうかを制御できると書きました。

今回は、この仕組みを活用した例を2つ紹介したいと思います。

mobile-valve



携帯電話からのメールをあるプログラム(process-ktai-mailとします)で処理したいとします。

携帯メールのenvelope fromのドメインパートはキャリア毎に決まっているので、ドメインパートを見れば携帯メールかどうかを判断することができます。

しかし、この判断ロジックを同じようなプログラムのそれぞれに実装するのは面倒ですしDRY (Don't Repeat Yourself)の原則からも外れるので、こんな風にしてみましょう。


  • mobile-valveというプログラムが、携帯メールかどうかの判断を行う。
  • mobile-valveは、携帯メールの場合はexit 0(後続の配送命令を実行する)し、そうでない場合はexit 99(後続の配送命令を実行しない)する。
  • process-ktai-mailは、dot-qmailでmobile-valuveの次の行に書く。


例えばこんなdot-qmailにした場合、
| mobile-valve
| process-ktai-mail


携帯メールの場合はmobile-valveがexit 0するので次のprocess-ktai-mailが実行され、携帯メールではない場合はmobile-valveがexit 99するのでそこで配送命令は終わり、process-ktai-mailは実行されません。

このようにすれば、mobile-valveは他のdot-qmailでも使い回せますし、process-ktai-mailに渡るのは携帯メールだけに限定できます。

実はひとつ抜け道があるのですが、それは後半で紹介します。

spf-valve



次の例はSPFの判定をするspf-valveです。

SPFについては末尾の参考資料を参照して欲しいのですが、簡単にいうと、メールの差出人の詐称を防ぐための枠組みです。SPFパッチを当てたqmailの場合、「Received-SPF」というヘッダが追加され、そこにSPFの判定結果が記されています。

SPFの判定結果にはいくつか種類があるのですが、「fail」の場合は詐称されているとみなしてよいでしょう。また、「softfail」の場合も状況によっては詐称とみなせます。

というわけで、spf-valveは以下のようにしてみましょう。


  • SPFを使って差出人が詐称されているかどうかの判断を行う。
  • SPFの判定結果が「fail」か「softfail」の場合はexit 99(後続の配送命令を実行しない)し、そうでない場合はexit 0(後続の配送命令を実行する)する。
  • オプションによって、「softfail」を詐称されていない(exit 0)とみなすこともできる。


例えばこんなdot-qmailにした場合、
| spf-valve
| process-mail


差出人を詐称したメールの場合は、spf-valveがexit 99するので次のprocess-mailは実行されません。

最近の迷惑メールはきちんとSPFをpassするように送ってくるため、迷惑メールの排除にSPFを使うのはあまり効果がないでしょう。あくまでSPFは詐称を防ぐものです。


mobile-valve + spf-valve



さて、mobile-valveのところで「抜け道がある」と書きました。

それはこんな抜け道です。

mobile-valveはenvelope fromのドメインパートを元に携帯メールかどうかを判断するのですが、このenvelope fromは詐称することができます。つまり、envelope fromがXXX@docomo.ne.jpとなるようにPCから送ったメールは、mobile-valveで「携帯メール」と判定されてしまいます。

これを回避するには詐称を検出できればよいわけで、ここでSPFの登場です。

幸い、各携帯キャリア(DoCoMo、au、Vodafone)はSPFに対応しています。

$ dig +noall +answer -t txt docomo.ne.jp
docomo.ne.jp. 85570 IN TXT "v=spf1 +ip4:203.138.203.0/24 ~all"

$ dig +noall +answer -t txt ezweb.ne.jp
ezweb.ne.jp. 3600 IN TXT "v=spf1 include:spf-im1.ezweb.ne.jp include:spf-im2.ezweb.ne.jp include:spf-im3.ezweb.ne.jp include:spf-im4.ezweb.ne.jp include:spf-im5.ezweb.ne.jp include:spf-im6.ezweb.ne.jp include:spf-nm.ezweb.ne.jp include:spf-az.ezweb.ne.jp ~all"

$ dig +noall +answer -t txt t.vodafone.ne.jp
t.vodafone.ne.jp. 1800 IN TXT "v=spf1 include:spf01.vodafone.ne.jp include:spf02.vodafone.ne.jp include:spf99.vodafone.ne.jp ~all"


つまり、mobile-valveとspf-valveを組み合わせれば、携帯メール以外を完全に排除することができるわけです。

なお、各キャリアのSPFの設定を見ると、「~all」となっているのでキャリアゲートウェイ以外からのメールはSPFの判定結果がsoftfailとなります。ですので携帯メールに限った用途では「softfail」の場合もspf-valveで詐称とみなした方がよいでしょう。

さて、例えばこんなdot-qmailの場合、

| mobile-valve
| spf-valve
| process-ktai-mail


非携帯メールならば、mobile-valveで配送処理が終わります。次に携帯メールでenvelope fromを詐称している場合はspf-valveで配送処理が終わります。結果的に、process-ktai-mailに渡るのは、詐称されていない携帯メールのみ、となります。

参考資料





ソースコード



mobile-valve



#!/bin/bash
# Copyright (c) 2006-2008 KLab Inc. All rights reserved.

usage() {
echo 'usage:'
echo ' mobile-valve [ -d ] [ -e ] [ -v ] [ -l LOGLABEL ]'
exit 111
}

LABEL=
AT_DOCOMO='*@docomo.ne.jp'
PAT_VODAFONE='*@?.vodafone.ne.jp *@softbank.ne.jp *@disney.ne.jp'
PAT_EZWEB='*@ezweb.ne.jp *@ido.ne.jp'
PAT_WILLCOM='*@pdx.ne.jp'
PATTERN=

while getopts "devl:" opt; do
case "$opt" in
l)
LABEL=$OPTARG
;;
d)
PATTERN="$PATTERN $PAT_DOCOMO"
;;
v)
PATTERN="$PATTERN $PAT_VODAFONE"
;;
e)
PATTERN="$PATTERN $PAT_EZWEB"
;;
*)
usage
;;
esac
done
shift $(($OPTIND - 1))

if [ -z "$PATTERN" ]; then
PATTERN="$PAT_DOCOMO $PAT_VODAFONE $PAT_EZWEB $PAT_WILLCOM"
fi
# echo ">$PATTERN<"

EUSER=$(id -n -u)
shopt -s extglob
case "$LABEL" in
+([a-zA-Z0-9_-]))
TAG="mobile/${EUSER}/${LABEL}"
;;
*)
TAG="mobile/${EUSER}/${EUSER}"
;;
esac

log() {
logger -i -p mail.info -t "$TAG" -- "sender=$SENDER $@"
}

if [ -z "$SENDER" -o "$SENDER" = '#@[]' ]; then
log "empty SENDER"
exit 99
fi

for p in $PATTERN; do
case "$SENDER" in
$p)
exit 0
;;
esac
done

log "result=reject"
exit 99

# Local Variables:
# sh-basic-offset: 2
# tab-width: 2
# coding: euc-jp
# End:

# vi: set ts=2 sw=2 sts=0 :


spv-valve



#!/usr/bin/env perl
#
# Copyright (c) 2005-2008 KLab Inc. All rights reserved.
#

use strict;
use warnings;
use Carp;
use Getopt::Long;
use Sys::Syslog qw(:DEFAULT setlogsock);
use POSIX qw(strftime setlocale LC_TIME);

use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Deepcopy = 1;

my $RCSID = q$Id: spf-valve,v 1.8 2008/02/13 11:22:32 KBoraKun Exp $;
my $REVISION = $RCSID =~ /,v ([\d.]+)/ ? $1 : 'unknown';
my $PROG = substr($0, rindex($0, '/')+1);

my $Verbose = 0;
my $Logger_Label = '';

# exit code
my $OK = 0;
my $OK_IGNORE_NEXT_DELIVER = 99;
my $FAIL_RETRY = 111;

sub usage(@) {
my $mesg = shift;

print "[ERROR] $mesg\n" if $mesg;
print "usage:\n";
print " $PROG [ -v ] [ -s ] [ -k ] [ -d DOMAIN ] [ -l LOGLABEL ]\n";
print "
v$REVISION
";
exit $FAIL_RETRY;
}

# --------------------------------------------------------------------
# M A I N
# --------------------------------------------------------------------

MAIN: {
my %opt;
Getopt::Long::Configure("bundling");
GetOptions(\%opt,
'verbose|v+' => \$Verbose,
'ktai|k',
'domain|d=s@',
'label|l=s',
'allow_softfail|s',
'help|h|?') or &usage();
&usage() if exists $opt{'help'};

my $euser = (getpwuid($<) or 'unknown');
my $label = "spf/$euser";
if ($opt{'label'} and $opt{'label'} =~ /^[a-zA-Z0-9_-]+$/) {
$label .= "/$opt{'label'}";
} else {
$label .= "/$euser";
}
$Logger_Label = $label;

if ($Verbose >= 1) {
setlogmask &Sys::Syslog::LOG_UPTO(&Sys::Syslog::LOG_INFO);
} else {
setlogmask &Sys::Syslog::LOG_UPTO(&Sys::Syslog::LOG_NOTICE);
}

my @domain;
if ($opt{'ktai'}) {
push @domain, 'docomo.ne.jp', 'ezweb.ne.jp', 'vodafone.ne.jp', 'softbank.ne.jp', 'disney.ne.jp';
} elsif ($opt{'domain'}) {
push @domain, @{ $opt{'domain'} };
}
logger2('domain='.join(',',@domain));

my %reject = ('fail' => 1, 'softfail' => 1);
$reject{'softfail'} = 0 if $opt{'allow_softfail'};

my $envelope_from = '';
my $spf = ''; # pass none fail unknown neutral softfail
my $spf_mesg = '';

if (exists $ENV{'SENDER'}) {
$envelope_from = $ENV{'SENDER'} if $ENV{'SENDER'};
} else {
logger("missing environment variable 'SENDER'.");
exit $OK_IGNORE_NEXT_DELIVER;
}

while (<>) {
s/[\r\n]+$//;
if (/^$/) {
last;
} elsif (/^Received-SPF:\s*(\S+)\s*(.+)/i) {
$spf = $1;
$spf_mesg = $2;
last;
}
}

if (@domain and $envelope_from) {
my $need_check = 0;
foreach my $d (@domain) {
if ($envelope_from =~ /${d}$/i) {
$need_check = 1;
last;
}
}
unless ($need_check) {
logger2("sender=$envelope_from spf=$spf spf_mesg=$spf_mesg result=$OK (no check)");
exit $OK; # no check.
}
}

if ($reject{lc($spf)}) {
logger("sender=$envelope_from spf=$spf spf_mesg=$spf_mesg result=$OK_IGNORE_NEXT_DELIVER (fail or softfail)");
exit $OK_IGNORE_NEXT_DELIVER;
} else {
logger2("sender=$envelope_from spf=$spf spf_mesg=$spf_mesg result=$OK (checked)");
exit $OK;
}
}

# --------------------------------------------------------------------
# S U B R O U T I N E
# --------------------------------------------------------------------

sub _logger {
my $prio = shift;

if ($Sys::Syslog::VERSION <= 0.10) {
my $oldlocale = setlocale(LC_TIME);
setlocale(LC_TIME, 'C');
my $timestamp = strftime "%b %e %T", localtime;
setlocale(LC_TIME, $oldlocale);
openlog "$timestamp $Logger_Label", 'pid', 'mail';
} else {
openlog "$Logger_Label", 'pid', 'mail';
}
syslog $prio, '%s', join(' ', @_);
closelog;
}

sub logger {
_logger('notice', @_);
}

sub logger2 {
_logger('info', @_);
}

__END__

# for Emacsen
# Local Variables:
# tab-width: 4
# cperl-indent-level: 4
# coding: euc-jp
# End:

# vi: set ts=4 sw=4 sts=0 :

klab_gijutsu2 at 08:30│Comments(0)TrackBack(0)network | mail

トラックバックURL

この記事にコメントする

名前:
URL:
  情報を記憶: 評価: 顔   
 
 
 
Blog内検索
Archives
このブログについて
DSASとは、KLab が構築し運用しているコンテンツサービス用のLinuxベースのインフラです。現在5ヶ所のデータセンタにて構築し、運用していますが、我々はDSASをより使いやすく、より安全に、そしてより省力で運用できることを目指して、日々改良に勤しんでいます。
このブログでは、そんな DSAS で使っている技術の紹介や、実験してみた結果の報告、トラブルに巻き込まれた時の経験談など、広く深く、色々な話題を織りまぜて紹介していきたいと思います。
最新コメント