HOME 備忘録 書き込む

日本語の Subject でメールを振り分ける


Jan 28, 2012

1. はじめに

紫藤は受信したメールを maildrop を使ってサーバー上で振り分けていますが、 最近は日本語が MIME format で表記されるようになったので、maildrop 単体では 日本語でのメールの振り分けはできなくなりました。

そこで、maildrop に振り分けフォルダーを渡す 簡単な python スクリプトを書きました。 このスクリプトは、標準入力からメールをうけとり、振り分け先フォルダーを標準出力に吐き出します。 maildrop はその結果をうけとり、それに基づいてメールを振り分けます。

このスクリプトでは、日本語に変換した Subject と他のヘッダー部分を、あらかじめ登録しておいた振り分けルールと比較することによって 振り分け先フォルダーを決めています。

2. 振り分けフォルダを決める python スクリプト

振り分けフォルダーを決めるスクリプト (suggest_folder.py) を以下に示します。 大まかな動作は以下のとおりです。
  1. 登録してある正規表現データ (drop_rules.RULES) を読み込む。(14 行目: import drop_rules)

    drop_rules.py には、キーがヘッダ項目で、値が正規表現である辞書のリストが登録されています。 また、各振り分けルールの辞書にはヘッダー項目のほかに 'folder' というキーがあり、 ここの値が振り分け先フォルダーです。
    例えば drop_rules.py の 6 行目は、

    という意味であり、7 行目は という意味です。

    [drop_rules.py]

    001:   import re
    002:   
    003:   RULES = [ \
    004:   {'Subject':re.compile(r'【楽天カード(通信|ニュース)】', re.I), 'From':re.compile(r'@mail\.rakuten-card\.co\.jp', re.I), 'folder':'INBOX.sale'},\
    005:   {'Subject':re.compile(r'【出光カード(モール|まいどプラス)】', re.I), 'From':re.compile(r'confirm@ml\.idemitsucard\.com', re.I), 'folder':'INBOX.sale'},\
    006:   {'Subject':re.compile(r'お年玉|キャンペーン|ボーナス|クリスマス|ポイント|off|ショッピング|ローン', re.I),
             'From':re.compile(r'idemitsucard|rakuten-card|amazon|yahoo', re.I), 'folder':'INBOX.sale'},\
    007:   {'Subject':re.compile(r'カード.?利用|請求|明細|引き落とし|注文|発送|パスワード|password|月.*料金', re.I), 'folder':'INBOX'}\
    008:   ]
    
  2. 標準出力から email を読み込み、それを email オブジェクトに変換する。 (29--31 行。 get_email() )
    なお、email.parser.BytesParser は Python 3.2 から導入されたクラスである。
  3. その email オブジェクトと drop_rules.RULES の各ルールを比較し、ルールにある全てのヘッダフィールドにおいて、 受信したメールのヘッダフィールドがルールの正規表現にマッチした場合にはルールにあるフォルダーを返す。 どのルールともマッチしなかったら、'*** No Idea ***' を返す。
    比較する際、email の Subject を デーコードして Unicode 文字列に変換しておく。
    Unicode 文字列への変換は 25--26 行目の decode_mime_header() でおこなう。
  4. get_folder() が返した文字列を標準出力に書き出す。
[suggest_folder.py]
001:   #!/usr/local/bin/python3.2
002:   
003:   '''
004:   The program writes the distination folder to sys.stdout for the e-mail read from sys.stdin
005:   according to drop_rules.RULES which is a list of dictionaries containing header elements and regular expressions.
006:   
007:   The program is designed to be used with maildrop.
008:   
009:   The goal of this program is to deal with Japanese headers, 
010:   which maildrop cannot handle.
011:   '''
012:   
013:   
014:   import email.parser, sys, email.header, drop_rules
015:   
016:   def get_folder(em, ls_rules):
017:       sbj = decode_mime_header(em['Subject']) if em['Subject'] else ''
018:       for h in ls_rules:
019:           if all( em[k] and h[k].search(sbj if k=='Subject' else em[k]) for k in h if k!='folder') : 
020:               return h['folder']
021:       else:
022:           return '*** No Idea ***'
023:   
024:   
025:   def decode_mime_header(s0):
026:       return ''.join( str(s, c or 'ascii') if isinstance(s, (bytes,)) else s for s,c in email.header.decode_header(s0) )
027:   
028:   
029:   def get_email(fp):
030:       p=email.parser.BytesParser()
031:       return p.parse(fp, True)
032:   
033:   
034:   
035:   
036:   if __name__=='__main__':
037:       sys.stdout.write(  \
038:          get_folder(get_email(sys.stdin.detach()), \
039:                     drop_rules.RULES))

3. 振り分けルール作成スクリプト (make_drop_rules.py)

drop_rules.RULES を作成するためのスクリプトです。drop_rules.txt から drop_rules.py を作ります。 drop_rules.txt には各行に ヘッダー要素、正規表現が書かれており、空行でルールを区切っています。

[make_drop_rules.py]

001:   #! /usr/local/bin/python3.2
002:   
003:   '''
004:   The program is to make a python code for e-mail filtering rules which is invoced from suggest_folder.py
005:   from an input file that consisting of sets of [header elements] -- [regular expression] pairs and distination folders. 
006:   '''
007:   
008:   
009:   import re, py_compile, os.path, sys
010:   
011:   FIN='/home/takafumi/drop_rule/drop_rules.txt'
012:   FOUT='/home/takafumi/bin/drop_rules.py'
013:   
014:   #FIN='drop_rules.txt'
015:   #FOUT='drop_rules.py'
016:   
017:   RE_COL=re.compile(r'^[-\w]+:', re.ASCII)
018:   RE_RECSEP = re.compile('([ \t]*\n){2,}')
019:   
020:   def make_a_record(s0):
021:       return [ line for line in s0.splitlines() if RE_COL.search(line) ]
022:   
023:   
024:   def get_records(s0):
025:       return (rec for rec in ( make_a_record(s) for s in RE_RECSEP.split(s0) ) \
026:         if any(line.startswith('folder:') for line in rec) )
027:   
028:   
029:   def format_record(ls0):
030:       return '{' + ', '.join(format_col(s) for s in ls0) +'}'
031:   
032:   
033:   def format_col(s_col):
034:       k,v=s_col.split(':', 1)
035:       s_fmt="'{0}':'{1}'" if k=='folder' else "'{0}':re.compile(r'{1}', re.I)" 
036:       return s_fmt.format(k, v.strip()) 
037:   
038:   
039:   def format_rules(s0):
040:       return \
041:       'import re\n\nRULES = [ \\\n' + \
042:           ',\\\n'.join( format_record(rec) for rec in get_records(s0) ) + \
043:       '\\\n]\n'
044:   
045:   
046:   def make_drop_rules(fname_in, fname_out):
047:       with open(fname_out, 'w', encoding='utf8') as fout:
048:           with open(fname_in, encoding='utf8') as fin:
049:               fout.write(format_rules(fin.read()))
050:   
051:       py_compile.compile(fname_out)
052:   
053:   
054:   if __name__=='__main__':
055:       if (len(sys.argv)>1 and sys.argv[1]=='force')    \
056:           or not os.path.isfile(FOUT)                  \
057:           or (os.path.getmtime(FOUT) < os.path.getmtime(FIN)):
058:           make_drop_rules(FIN, FOUT)

[drop_rules.txt]

Subject:【楽天カード(通信|ニュース)】
From:@mail\.rakuten-card\.co\.jp
folder:INBOX.sale

Subject:【出光カード(モール|まいどプラス)】
From:confirm@ml\.idemitsucard\.com
folder:INBOX.sale

Subject:お年玉|キャンペーン|ボーナス|クリスマス|ポイント|off|ショッピング|ローン
From:idemitsucard|rakuten-card|amazon|yahoo
folder:INBOX.sale



Subject:カード.?利用|請求|明細|引き落とし|注文|発送|パスワード|password|月.*料金
folder:INBOX

4. 紫藤の自宅サーバーの .mailfilter

紫藤の自宅サーバの .mailfilter を以下に示します。
  1. 33 行目で suggest_folder.py の結果を DIR に保持する。
  2. 34 -- 37 行目で $DIR が INBOX の時には $MAILDIR に振り分ける。
  3. 38 -- 42 行目で $DIR が /^INBOX/ の時には、$DIR から INBOX. を取り除いた $SUBDIR をつくり ${MAILDIR}${SUBDIR}/ に振り分ける。
  4. suggest_folder.py が '*** No Idea ***' を返したものについては、From あるいは Return-Path に基づいて振り分ける。
[.mailfilter]
001:   MAILDIR="/home/takafumi/Maildir/"
002:   DEFAULT=$MAILDIR
003:   NKF="/usr/local/bin/nkf"
004:   NKF_OPTION="-Z -m -j"
005:   WAKATI="/usr/local/bin/kakasi -w"
006:   BOGOFILTER="/usr/local/bin/bogofilter"
007:   BOGOFILTER_OPTION="-u -e -p"
008:   FORMAT_HEADER="/home/takafumi/bin/format_header.py"
009:   BOGOJP="${FORMAT_HEADER} | ${WAKATI} | ${BOGOFILTER} ${BOGOFILTER_OPTION}"
010:   SENDMAIL="/usr/sbin/sendmail"
011:   SUGGEST_FOLDER="/home/takafumi/bin/suggest_folder.py"
012:   
013:   xfilter "${BOGOJP}"
014:   
015:   if (/^X-Bogosity: Spam, tests=bogofilter/:h)
016:   {
017:       to "${MAILDIR}.Junk/"
018:   }
019:   
020:   
021:   if (/^Subject: Undelivered Mail Returned to Sender/:h)
022:   {
023:       to "${MAILDIR}"
024:   }
025:   
026:   if (/^Return-Path: <(root|daemon)@www\.shido\.info>$/:h)
027:   {
028:       to "${MAILDIR}.log_of_shido2/"
029:   }
030:   
031:   
032:   
033:   DIR = `${SUGGEST_FOLDER}`
034:   if($DIR eq "INBOX")
035:   {
036:       to "${MAILDIR}"
037:   }
038:   if ($DIR=~/^INBOX/)
039:   {
040:       SUBDIR=substr($DIR, 5)
041:       to "${MAILDIR}${SUBDIR}/"
042:   }
043:   
044:   
045:   if (/^From:\s*(.*)/:h)
046:   {
047:       FADDR=getaddr($MATCH1)
048:       if (lookup($FADDR, "drop_rule/inbox.txt"))
049:       {
050:           to "${MAILDIR}"
051:       }
052:       if (lookup($FADDR, "drop_rule/sale.txt"))
053:       {
054:           to "${MAILDIR}.sale/"
055:       }
056:       if (lookup($FADDR, "drop_rule/magazines.txt"))
057:       {
058:           to "${MAILDIR}.magazines/"
059:       }
060:   }
061:   
062:   
063:   if (/Return-Path:\s*(.*)/:h && lookup($MATCH1, "drop_rule/mailing_lists.txt"))
064:   {
065:       to "${MAILDIR}.mailing_lists/"
066:   }
067:   
068:   to "${MAILDIR}"
069:   

5. 終わりに

maildrop が日本語を読めないので、仕方なく From を使って振り分けていましたが、 宣伝用のメールと重要なメールが同じメールアドレスから来ることがあるので、限界がありました。

日本語の Subject を使って振り分けるようにしたところ、同じアドレスから来る重要なメールと宣伝用メールを 振り分けられるようになり、大変便利に使っています。 たとえば、注文の確認、発送の知らせ、カード利用の知らせなどが INBOX に振り分けられるので、宣伝用メールに埋もれてしまうことがなくなりました。