【Gemini API × docker-mailserver】AIで受信メールのフォルダ分けを自動化してみた

受信メールのフォルダ分け自動化やりたい…やりたくない?

docker-mailserverでメールサーバー構築がとっても簡単になり、
スパムメールの自動判定は簡単に行うことができる反面、
通常のメールも含めて目的のフォルダに自動分類するツールはメジャーではなくあまりないそう

今回できるのかの実証実験の意味も込めてdocker-mailserverのメール自動分類をGemini APIを使って試してみた

ぜったい需要あると思うのにそういう機能欲しいと思ってるのわたしだけ?
探したけど良いのないので仕方なく自分で作ってみたのお話

実装して使ってみた感想としては、たくさんの文章を食ったAIに分類してもらえるので精度が高くて非常に便利です

対象の読者

  • docker-mailserverのメールサーバーを構築済みの方 (分類スクリプトを動かすだけならGmailでもできるかもしれない)
  • AIで受信メールの分類を自動化したい方 (通常メール/迷惑メールの分類をどちらかもしくはどっちも高度にやりたい方)

概要

Docker 環境に構築したdocker-mailserverにおいて、受信したメールをGemini APIを使ったPythonスクリプトで自動分類し、Maildir の指定フォルダに格納するシステムを構築した記録です

  • メール受信時に Python スクリプトを自動実行
  • Gemini API でメールの内容を解析・ジャンル別に分類(例 : “就活", “金融取引", “迷惑メール", “その他")
  • 分類結果に対応するフォルダにメール保存し、どのジャンルにも当てはまらない場合(その他の場合)は通常フォルダにメールを保存

ちなみに、Gemini APIは一定以下のリクエストは無料枠で使えますが、課金アカウントは作る必要があるそうなので、調べてあらかじめ用意しておいてください

構成

  • Docker Mailserver(Postfix + Dovecot)
  • Python 3.11 + venv
  • Generative Language API(Gemini API)

↓ システムイメージ

ChatGPTにつくってもらったシステムイメージ

ワークフロー

  1. 分類メール保存先のディレクトリ作成
  2. Pythonスクリプト作成
  3. PostfixでPythonを実行する設定追加

分類メール保存先のディレクトリ作成 (以下すべてDockerコンテナ内での作業)

まず最初に、目的別に分類したメールの保存先フォルダ作ります
今回は請求(Invoices),就活(JobHunting),迷惑メール(Junk),で作ります
フォルダ名は自由に変えてください。分類先のフォルダは何個でも作って大丈夫です

user: メールのユーザー名
example:ドメイン

Bash
mkdir -p /var/mail/example.com/user/.Junk/{cur,new,tmp}
mkdir -p /var/mail/example.com/user/.Invoices/{cur,new,tmp}
mkdir -p /var/mail/example.com/user/.JobHunting/{cur,new,tmp}
chown -R docker:docker /var/mail/example.com/

Python分類スクリプト実行環境の準備

Pythonのスクリプトを実行する環境を作ります
Pythonは環境はvenv等お好みの方法で用意してください
私はガサツなのでシステムのPythonを使いました

以下の2つのライブラリが必要です

Bash
pip install google-cloud-aiplatform 
pip install requests

また、Gemini APIキーをGoogle Cloudを以下から発行しておいてください
https://aistudio.google.com/apikey

Pythonスクリプト mail-classifier.py の設置

メールを分類するPythonスクリプトです
/opt/classifierというフォルダを作り、そこにスクリプトを置く前提で説明します

Bash
mkdir /opt/classifier
vi mail-classifier.py
Python
import sys
import os
import email
import mailbox
import requests
import json
from email import message_from_bytes

# --- API キーとエンドポイント ---
API_KEY = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"  # <-- Google AI Studio から発行
ENDPOINT = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={API_KEY}"

# --- Gemini でカテゴリ分類 ---
def classify_email(subject, body):
    prompt = f"""以下のメールを目的別に分類してください。

件名: {subject}
本文: {body}

分類カテゴリ:
- 金融取引
- 就活
- ログイン・アカウント
- 迷惑メール
- その他

カテゴリ名だけ1語で返してください。
"""

    headers = {"Content-Type": "application/json"}
    data = {
        "contents": [
            {
                "parts": [{"text": prompt}]
            }
        ]
    }

    response = requests.post(ENDPOINT, headers=headers, data=json.dumps(data))
    if response.status_code == 200:
        result = response.json()
        text = result["candidates"][0]["content"]["parts"][0]["text"].strip()
        return text if text in ["金融取引", "就活","ログイン・アカウント", "迷惑メール", "その他"] else "その他"
    else:
        print(f"Error: {response.status_code} {response.text}")
        return "その他"

# --- スパム判定 (Rspamd) ---
def is_spam(msg):
    spam_status = msg.get('X-Spam', 'no').lower()
    return spam_status == 'yes'

# --- 本文抽出 ---
def get_email_body(msg):
    body = ""
    if msg.is_multipart():
        for part in msg.walk():
            if part.get_content_type() == "text/plain":
                charset = part.get_content_charset() or "utf-8"
                body += part.get_payload(decode=True).decode(charset, errors="ignore")
    else:
        charset = msg.get_content_charset() or "utf-8"
        body = msg.get_payload(decode=True).decode(charset, errors="ignore")
    return body

# --- Maildir へ保存 ---
def move_to_mailbox(category, raw_email_bytes, recipient):
    mailbox_mapping = {
        "迷惑メール": ".Junk",
        "金融取引": ".Transactions",
        "就活": ".JobHunting",
        "ログイン・アカウント": ".Login",
        "その他": ""
    }

    local_part, domain = recipient.split("@")
    mailbox_name = mailbox_mapping.get(category, "")
    mailbox_path = f"/var/mail/{domain}/{local_part}"
    if mailbox_name:
        mailbox_path = f"{mailbox_path}/{mailbox_name}"

    msg_obj = message_from_bytes(raw_email_bytes)
    mbox = mailbox.Maildir(mailbox_path, factory=None, create=False)
    mbox.add(msg_obj)
    mbox.flush()

# --- メイン処理 ---
def main():
    raw_email_bytes = sys.stdin.buffer.read()
    msg = message_from_bytes(raw_email_bytes)

    if is_spam(msg):
        category = "迷惑メール"
    else:
        subject = msg.get("Subject", "")
        body = get_email_body(msg)
        category = classify_email(subject, body)

    recipient = sys.argv[1] if len(sys.argv) > 1 else "[email protected]"
    move_to_mailbox(category, raw_email_bytes, recipient)

if __name__ == "__main__":
    main()

Rspamdでのスパム判定をプログラムで確認していますが、Rspamdを使ってなくても動きます
入れていればRspamd+Geminiのダブルチェックになるだけです

このプログラムで変更するべき所は以下のとおりです

  • APIキー
  • エンドポイント(ENDPOINT) … 使うモデルで無料枠の制限が変わるので好きなものを使ってください
    gemini-2.0-flashの文字列を変更することで別のモデルが使えます
  • 分類カテゴリ … Geminiのプロンプト部分ですので、分類したいカテゴリを指定してください
    それに伴って [“金融取引", “就活","ログイン・アカウント", “迷惑メール", “その他"]のリストも合わせて変更してください
  • mailbox_mapping … コードを参考に、作ったカテゴリと、分類先のフォルダを対応付けてください

使うモデルで無料枠の回数制限が違うようですが、無料で使い続けてあまりにも頻繁にメールが来るわけでなければgemini-2.0-flashで十分だと思います

APIキーはハードコーディングされていて運用上あまりよろしくないので適宜変更してください

以下の(内容は任意の)サンプルメールを作り、スクリプトを実行してメールが正しく分類されるか確認します
引数に送信先のメールアドレスを指定してください

Bash
cat <<EOF > sample_email.eml
From: [email protected]
To: [email protected]
Subject: ご請求書の送付について

お世話になっております。

以下の通り、今月分のご請求書をお送りします。

----
請求書番号: INV-202505
金額: ¥123,456
期日: 2025年5月31日
----

何卒よろしくお願いいたします。

株式会社サンプル
経理部
EOF

cat sample_email.eml | python3 mail-classifier.py [email protected]

Postfix設定

[email protected]にメールを受信した際に、Pythonスクリプトを自動実行する設定です

Bash
## Postfix に transport 設定
echo "[email protected] classifier:" >> /etc/postfix/transport
postmap /etc/postfix/transport

## /etc/postfix/master.cf に classifier サービス追加
echo "classifier unix - n n - - pipe user=docker argv=/opt/classifier/mail-classifier.py \${recipient}" >> /etc/postfix/master.cf

## main.cf に transport 設定
echo "transport_maps = hash:/etc/postfix/transport" >> /etc/postfix/main.cf

## Postfix再起動
postfix reload

テスト

Gmail やローカルから[email protected]宛に次のようなメールを送って、振り分けが動くかを確認します

件名: 「面接のご案内」 → JobHunting に分類

件名: 「ご請求書の送付について」→ Invoices に分類

どれにも該当しない → 受信箱(cur)に保存

まとめ

これによって、メール内容を AI で柔軟に分類し、[email protected]宛のメールをフォルダに自動で振り分けできます

メールが大量に届く環境でも、手動で仕分けする手間を省くことができ大変便利です