迷惑メールをローカルLLMに判別させてみた

迷惑メールが増えているとのこと。前からな気もしますが、ドメインが明らかに偽装されたメールがフィルタをすり抜けてきます。*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回実行して多数決を取っています。判断の根拠はログファイルに記録されます。

#!/bin/zsh
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
			-- 不正なメッセージ一覧が渡されてしまうため、少し待って非同期でINBOX内のメールに対して実行
			-- tell me to evaluate(theMessages)
			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 -- INBOXを持たないアカウントは無視
						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"' などというヘッダがついているので、中国からメールを受け取る機会がないのならこれでフィルタしてしまうのが簡単だったりします。

*1:こんなインターネット黎明期の原始的な仕組みをいまだに使っているのが悪い気がする

*2:Gmailの方がフィルタリングは優秀なので転送してしまうという手もあるんでしょうが…