迷惑メールが増えているとのこと。前からな気もしますが、ドメインが明らかに偽装されたメールがフィルタをすり抜けてきます。*1
cf. 迷惑メールが爆増中。Yahoo!メールでも対策へ | マイナビニュース
それならというわけでMac上で動くローカルLLMに判別させてみることにしました。*2。人間なら送信元(ドメイン)と件名を見れば判別可能なので、高速化・複雑化抑止のため、入力はこれだけとします。
モデルは Gemma-4-E4B を使用しました。(最初 1-bit Bonsai を試しましたが日本に関する知識が乏しすぎてプロンプトの工夫レベルではどうにもならなさそうでした。)
実行には llama.cpp を利用。セットアップは以下の手順に従うだけです。
unsloth.ai
以下コマンドでサーバーを起動しておきます。高速化のため reasoning はオフにして代わりにプロンプトで根拠を述べさせています(現在の説明によると --chat-template-kwargs '{"enable_thinking":false}' とすべきかもです)。後で述べますが複数回実行させており、その際に観点が少し変わることがあるので temperature は少し高めを指定しました。
export LLAMA_CACHE="unsloth/gemma-4-E4B-it-GGUF"
./build/bin/llama-server -hf unsloth/gemma-4-E4B-it-GGUF:Q8_0 --temp 0.5 --top-p 0.95 --top-k 64 --reasoning off &
evaluate_mail_sender.sh として、送信元とタイトルを引数に渡すと OK/BAD のどちらかを結果として返すコマンドを用意します。自信がないのか回答がブレることもあるため、最大3回実行して多数決を取っています。判断の根拠はログファイルに記録されます。
set -e
prompt='次のメールで、FROM の名称("名前 <...@...>"の"名前"部分)もしくはSubjectに含まれる送信元名称と、FROMのドメインを比較して、全く関係ない送信元を偽装したメールかを判定して。特にFROMに記述された名称とドメインの不一致は問題のある可能性が高い。Subjectは「【送信元】〜」のように名称であることを強調している場合には重視する。名称かが曖昧な場合や、確実でない場合は偽装だと判断してはならない。根拠を述べた後、最後の行に、送信元偽装の可能性が高い場合には「BAD」と、問題ない場合・不確かな場合は「OK」とプレインテキストで出力して。'
target="From: $from / Subject: \"$subj\""
bad=0
ok=0
for i in {{1..3}}; do
result=$(curl -fs http://127.0.0.1:8080/v1/chat/completions -H "Content-Type: application/json" -d '{"messages":[{"role":"system","content":"'"${prompt//\"/\\\"}"'"},{"role":"user","content":"'"${target//\"/\\\"}"'"}],"temperature":0.5}' | jq -r '.choices[].message.content')
result_str=$(tail -1 <<<"$result")
echo "$target\n$result\n" >>/tmp/llm-result.log
grep -q BAD <<<"$result_str" && bad=$(($bad+1))
if [[ "$bad" -eq 2 ]]; then
echo BAD
exit
fi
grep -q OK <<<"$result_str" && ok=$(($ok+1))
if [[ "$ok" -eq 2 ]]; then
echo OK
exit
fi
done
あとはこれを使ってメールのフィルタリングをする設定をします。なお、メールアプリとしては Mac の Mail.app で動作させることにします。
まずはスクリプトエディタで次の AppleScript を作成し、 ~/Library/Application\ Scripts/com.apple.mail/ に eval-with-llm.scpt として保存します(形式はスクリプト)。
これは受信メールに対してルール評価時に先ほどのコマンドを使って結果が BAD なら迷惑メールに移動させるものです。
以前の Mail.app ではもっと簡単に動いていたんですが、どこかのバージョンから Mail.app の AppleScript フィルタ機能がバグってしまっているらしく、perform mail action に正しい受信メールが渡ってきません。。。仕方がないので、適当に少し待ってから自分自身を run し直し、INBOX内の全メールを調べて、未チェックのもの(チェック済みの問題ないメールには背景色をつけて区別)に対してフィルタしています。
色々テキスト置換とかしているのは実行コマンド引数をサニタイズしているためです。
using terms from application "Mail"
on perform mail action with messages theMessages for rule theRule
try
do shell script "zsh -c 'sleep 3; osascript ~/Library/Containers/com.apple.mail/Data/Library/Application\\ Scripts/com.apple.mail/eval-with-llm.scpt' &"
on error e number n
display dialog "eval-with-llm Error: " & e & " (" & n & ")" buttons "OK" default button 1
end try
end perform mail action with messages
on run
tell application "Mail"
repeat with theAccount in every account
try
try
set theMessages to messages of mailbox "INBOX" of theAccount
end try
my evaluate(theMessages)
on error e number n
display dialog "eval-with-llm Error: " & e & " (" & n & ")" buttons "OK" default button 1
end try
end repeat
end tell
end run
on evaluate(message)
tell application "Mail"
repeat with eachMessage in theMessages
if background color of eachMessage is none then
set theSubject to subject of eachMessage
set theSender to sender of eachMessage
set theSubject to my replaceText(theSubject, return, " ")
set theSender to my replaceText(theSender, return, " ")
set theResult to (do shell script "~/bin/evaluate_mail_sender.sh " & (quoted form of theSender) & " " & (quoted form of theSubject))
if theResult contains "BAD" then
move eachMessage to junk mailbox
else
set background color of eachMessage to blue
end if
end if
end repeat
end tell
end evaluate
on replaceText(target, a, b)
set oldDelimiters to AppleScript's text item delimiters
set AppleScript's text item delimiters to a
set target to text items of target
set AppleScript's text item delimiters to b
set target to target as string
set AppleScript's text item delimiters to oldDelimiters
return target
end replaceText
end using terms from
Mac の Mail.app で設定から 「ルール > ルールを追加」 ですべてのメッセージに対して AppleScript を実行を選び、先ほどのスクリプト名を選ぶことで、受信時に実行されるようになります。
実行時間は1回につき5秒弱 (多数決のため計10-15秒) くらいのようです。精度としてはそこそこの様子。多数決させてもたまに素通りすることもあります。まあ "強い確信がないならOKに倒せ" と指示していますからそんなものでしょう。
なお今多数来ているメールは大体 'Content-Type: text/html; charset="GBK"' などというヘッダがついているので、中国からメールを受け取る機会がないのならこれでフィルタしてしまうのが簡単だったりします。