SQLiteでメール重複防止:永続化とクエリ効率化のテクニック

はじめに

メール監視システムを15分間隔で自動実行すると、同じメールを何度も通知してしまう問題が発生します。この記事では、SQLiteを使った重複防止の実装を紹介します。

重複問題の本質

Gmail APIでメールを取得すると、過去50件などの範囲で取得します。Cronジョブで15分ごとに実行すると:

  • 前回チェック時のメールが再度取得される
  • 同じメールを繰り返し「重要」と判定してしまう
  • 通知がスパム化する

解決策:SQLite永続化

データベース設計

メールごとにユニークIDを記録し、通知履歴を保存します。

CREATE TABLE IF NOT EXISTS emails (
    id TEXT PRIMARY KEY,
    account TEXT NOT NULL,
    subject TEXT,
    sender TEXT,
    received_at TIMESTAMP,
    importance_level TEXT,
    score INTEGER,
    first_detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    last_notified_at TIMESTAMP,
    notification_count INTEGER DEFAULT 0
);

CREATE INDEX idx_account_received ON emails(account, received_at);
CREATE INDEX idx_importance ON emails(importance_level);

初期化

import sqlite3
from datetime import datetime

def init_database(db_path='emails.db'):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    cursor.execute('''
        CREATE TABLE IF NOT EXISTS emails (
            id TEXT PRIMARY KEY,
            account TEXT NOT NULL,
            subject TEXT,
            sender TEXT,
            received_at TIMESTAMP,
            importance_level TEXT,
            score INTEGER,
            first_detected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            last_notified_at TIMESTAMP,
            notification_count INTEGER DEFAULT 0
        )
    ''')

    cursor.execute('CREATE INDEX IF NOT EXISTS idx_account_received ON emails(account, received_at)')
    cursor.execute('CREATE INDEX IF NOT EXISTS idx_importance ON emails(importance_level)')

    conn.commit()
    conn.close()

重複チェック

新規メールかどうかを判定し、重複なら既存レコードを返します。

def is_duplicate(email_id, db_path='emails.db'):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    cursor.execute('SELECT * FROM emails WHERE id = ?', (email_id,))
    result = cursor.fetchone()

    conn.close()
    return result is not None

メール登録

新規メールをデータベースに保存します。

def save_email(email_data, db_path='emails.db'):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    cursor.execute('''
        INSERT OR REPLACE INTO emails 
        (id, account, subject, sender, received_at, importance_level, score, last_notified_at, notification_count)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?, 
                COALESCE((SELECT notification_count FROM emails WHERE id = ?), 0) + 1)
    ''', (
        email_data['id'],
        email_data['account'],
        email_data['subject'],
        email_data['from'],
        email_data['received_at'],
        email_data['priority'],
        email_data['score'],
        datetime.now().isoformat(),
        email_data['id']  # サブクエリ用
    ))

    conn.commit()
    conn.close()

再通知ロジック

重要メールは一定時間経過後に再通知します。

再通知判定

from datetime import datetime, timedelta

def should_renotify(email_id, priority, db_path='emails.db'):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    cursor.execute('SELECT last_notified_at FROM emails WHERE id = ?', (email_id,))
    result = cursor.fetchone()
    conn.close()

    if not result or not result[0]:
        return True  # 初回通知

    last_notified = datetime.fromisoformat(result[0])
    now = datetime.now()

    # 優先度別の再通知間隔
    intervals = {
        'HIGH': timedelta(hours=2),
        'MEDIUM': timedelta(hours=24)
    }

    interval = intervals.get(priority, timedelta(days=999))  # LOWは再通知なし

    return (now - last_notified) >= interval

統合フロー

def process_emails(emails, account, db_path='emails.db'):
    new_important = []

    for email in emails:
        email_id = email['id']
        priority = email['priority']

        if is_duplicate(email_id, db_path):
            if should_renotify(email_id, priority, db_path):
                save_email(email, db_path)  # 通知カウント更新
                new_important.append(email)
        else:
            save_email(email, db_path)
            new_important.append(email)

    return new_important

パフォーマンス最適化

インデックス活用

頻繁に使うクエリにインデックスを設定します。

-- アカウント + 受信日時での検索
CREATE INDEX idx_account_received ON emails(account, received_at);

-- 重要度での絞り込み
CREATE INDEX idx_importance ON emails(importance_level);

-- 再通知判定
CREATE INDEX idx_last_notified ON emails(last_notified_at);

バッチ処理

複数メールを1トランザクションで処理します。

def save_emails_batch(emails, db_path='emails.db'):
    conn = sqlite3.connect(db_path)
    cursor = conn.cursor()

    data = [
        (e['id'], e['account'], e['subject'], e['from'], 
         e['received_at'], e['priority'], e['score'])
        for e in emails
    ]

    cursor.executemany('''
        INSERT OR IGNORE INTO emails 
        (id, account, subject, sender, received_at, importance_level, score)
        VALUES (?, ?, ?, ?, ?, ?, ?)
    ''', data)

    conn.commit()
    conn.close()

運用結果

  • 初回チェック: 64通 → 7通の重要メール検出
  • 2回目以降: 重複除外により通知0件(正常動作)
  • 2時間後: High優先度メール1件を再通知

まとめ

SQLiteを使った永続化により、重複通知を完全に防止できます。再通知ロジックと組み合わせることで、重要メールを適切なタイミングで再度知らせる仕組みも実現できます。

関連記事:
– Gmail重要メール監視システム構築
– 複数Googleアカウント統合管理
– OpenClaw Cronジョブ活用術