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

2025年5月13日

※2025/05/19 dockerコンテナ再立ち上げの際に自動で本環境が再構築されるように手順を修正

広告

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

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

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

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

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

対象の読者

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

概要

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

構成

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

■本アプリ構成で行えること

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

↓ システムイメージ

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

ワークフロー

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

分類メール保存先のフォルダ作成
(以下gitリポジトリからクローンしたdocker-mailserverディレクトリ内での作業)

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

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

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

Pythonスクリプト実行の準備

Pythonスクリプト設置

Pythonスクリプトは、ホストに配置したのちコンテナにマウントする形で配置します

まず、ホストのdocker-mailserverディレクトリの配下にclassifierディレクトリを配置し、
その配下に少し下にあるPythonスクリプトをemail_classifier.pyとして作成してください

Bash
mkdir classifier
vi classifier/email_classifier.py

また、プログラムで利用するGemini APIキーをGoogle Cloudを以下から発行しておいてください
https://aistudio.google.com/apikey

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

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で十分だと思います

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

compose.yaml設定

スクリプトをコンテナで参照するため、compose.yamlのvolumes:の欄に、最下段のマウント設定を追記してください
ホストのdocker-mailserver/classifier/ディレクトリがコンテナの/opt/classifier/にマウントされます

YAML
    volumes:
      - ./docker-data/dms/mail-data/:/var/mail/
      - ./docker-data/dms/mail-state/:/var/mail-state/
      - ./docker-data/dms/mail-logs/:/var/log/mail/
      - ./docker-data/dms/config/:/tmp/docker-mailserver/
      - /etc/localtime:/etc/localtime:ro
      - /etc/letsencrypt:/etc/letsencrypt
      - ./classifier:/opt/classifier
      ## ↑ 追記

user-patches.sh設定

次に、docker compose時にPythonの実行環境(venv)とPostfix設定を一括で行うスクリプトを設定します

docker-mailserverでは、docker-data/dms/config/user-patches.shというファイルを作成すると、Dockerコンテナ作成後にスクリプトの内容を自動実行するため、ここに環境構築スクリプトを配置します

以下の内容を、user-patches.shとしてdocker-data/dms/config配下に保存してください
なお、Transportマップ登録の[email protected]を自分のメールアカウントに変更する必要があります

Bash
#!/bin/bash

# Postfix 設定ファイルの追記
TRANSPORT_FILE="/etc/postfix/transport"
MASTER_CF="/etc/postfix/master.cf"
MAIN_CF="/etc/postfix/main.cf"

# Transportマップ登録
if ! grep -q "[email protected] classifier:" "$TRANSPORT_FILE"; then
  echo "[email protected] classifier:" >> "$TRANSPORT_FILE"
  postmap "$TRANSPORT_FILE"
fi

# master.cf に pipe サービス追加
if ! grep -q "classifier unix" "$MASTER_CF"; then
  echo "classifier unix - n n - - pipe user=docker argv=/opt/venv/bin/python3 /opt/classifier/email_classifier.py \${recipient}" >> "$MASTER_CF"
fi

# main.cf に transport_maps 追加
if ! grep -q "transport_maps" "$MAIN_CF"; then
  echo "transport_maps = hash:/etc/postfix/transport" >> "$MAIN_CF"
fi

postmap /etc/postfix/transport

# postfix 再起動
postfix reload

## PythonとGeminiAPI環境
apt-get update
apt-get install -y python3.11 python3.11-venv python3.11-distutils
python3 -m venv /opt/venv
source /opt/venv/bin/activate
pip3 install google-cloud-aiplatform requests

echo "Default user-patches.sh successfully executed"

これが設置できたら、Dockerコンテナを再作成して、環境が正しく作られているか確認してください

Bash
docker compose down && docker compose up -d
docker logs -f mailserver

動作確認&テスト

Gmail やローカルから自分のメールアドレス([email protected])宛に次のようなテストメールを送って、振り分けが正しく行われるかを確認します

TypeScript
From: billing@example.com
To: recipient@example.com
Subject: ご請求書の送付について

お世話になっております

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

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

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

株式会社サンプル
経理部

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

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

迷惑メール → Junkフォルダに分類

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

まとめ

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

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

広告