生成AIを駆使して作成したGmailの転送スクリプト

# 生成AIを駆使して作成したGmailの転送スクリプト

## 1. 背景

Gmailの容量が一杯になってしまい（何とフリー15GBのところ60GB超え）、別のアカウントに転送して保存する必要に迫られました。とはいえ、転送する際に元の日付が変わってしまうのは避けたい…。そのような状況において、生成AIを駆使して転送スクリプトを作成しました。本記事では、その背景や苦労した点、設定方法を紹介します。

## 2. 大変だったこと

Gmailの転送スクリプトを作成するにあたり、いくつかの難題に直面しました。

### 2-1. Gmailの仕様変更に翻弄される

GmailのIMAP転送を行う際、セキュリティ上の理由で二段階認証を有効にしなければならないことが判明しました。さらに、アプリパスワードの発行が必要だったのですが、現在アプリパスワードは非推奨のため、設定方法を探すのに一苦労しました。標準では表示されていないため、Googleのアカウント設定ページで細かい検索が必要です。

### 2-2. 文字化け対策

転送の過程でメールが文字化けしました。エンコードの問題を特定し、スクリプト内で対応する必要がありましたが、これは時間と労力を要しました。
（ここは生成AIが解決してくれたので、私は完全な理解は出来ていません。）

### 2-3. Gmail側の容量制限

Gmailには送受信メールの容量制限があるため、一度に大量のメールを転送することができませんでした。そこで、転送処理を1週間ごとに区切って行うようにしました。こうすることで、Gmailのサーバー負荷やエラーを最小限に抑えることができました。

## 3. 生成AIの使い方
私は現在ChatGPTとClaudeの有料サービスを使っています。ChatGPTで躓いたら、Claudeに聞いて、Claudeで躓いたらChatGPTに聞いてと繰り返しながらスクリプトを完成させていきました。どちらの生成AIでも躓いた場合には、検索ワードを生成AIに作成させて、それでWeb検索します。検索して有用な情報があれば、その内容を生成AIに教えて、更にコードを改善していくという作業工程を取りました。

## 4. 設定方法・使い方

ここでは、Gmail転送スクリプトを使用するための基本的な設定手順を紹介します。

### 4-1. Gmail転送元・転送先のIMAP設定

まず、Gmailの転送元と転送先の両方でIMAPを有効にする必要があります。Gmailの「設定」から「転送とPOP/IMAP」タブに移動し、IMAPを有効にしてください。

### 4-2. アプリパスワードの設定

次に、Gmail転送元・転送先の両方でアプリパスワードを設定します。本記事では具体的な設定方法は割愛しますが、「Gmail IMAP アプリパスワード設定」と検索することで、詳しい手順を見つけられるでしょう。

重要なのは、Gmailの設定ではなく、「Googleアカウント管理ページ」からセキュリティ設定を行うことです（[myaccount.google.com](https://myaccount.google.com/)）。アプリパスワードは標準で表示されないため、「その他の情報」セクション内の検索窓で検索する必要があります。

### 4-3. Pythonコードの実行
ここまで設定できれば以下のPythonコードをお好きな方法で実行すれば大丈夫だと思います。転送容量などがあるために、ゆっくりですが、確実な転送が可能です。

```Python3
import imaplib
import email
import ssl
from datetime import datetime, timedelta
from email.header import decode_header
import time
import logging
import signal
import sys
import os

# アカウントの設定
SOURCE_EMAIL = "source_email@gmail.com"  # 送信元のメールアドレス
SOURCE_PASSWORD = "source_password"  # 送信元のメールアカウントのアプリパスワード
DEST_EMAIL = "dest_email@gmail.com"  # 送信先のメールアドレス
DEST_PASSWORD = "dest_password"  # 送信先のメールアカウントのアプリパスワード

# 日付範囲の設定
START_DATE = datetime(2017, 1, 1)  # 開始日
END_DATE = datetime(2018, 12, 31)  # 終了日

# ログの設定
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
log_records = []

# グローバル変数
total_processed = 0
is_interrupted = False

def signal_handler(signum, frame):
    global is_interrupted
    is_interrupted = True
    logging.info("プログラムの中断が要求されました。現在の処理が完了次第、安全に終了します。")

signal.signal(signal.SIGINT, signal_handler)

def connect_to_imap(email_address, password):
    try:
        context = ssl.create_default_context()
        mail = imaplib.IMAP4_SSL("imap.gmail.com", port=993, ssl_context=context)
        mail.login(email_address, password)
        return mail
    except imaplib.IMAP4.error as e:
        logging.error(f"{email_address} のIMAP接続エラー: {e}")
        return None

def fetch_email(mail, email_id):
    try:
        result, data = mail.fetch(email_id, "(RFC822)")
        if result == "OK":
            return data[0][1]
        else:
            return None
    except Exception:
        return None

def copy_and_delete_email(source_mail, dest_mail, email_id):
    raw_email = fetch_email(source_mail, email_id)
    if raw_email:
        try:
            msg = email.message_from_bytes(raw_email)
            date_tuple = email.utils.parsedate_tz(msg['Date'])
            date_str = imaplib.Time2Internaldate(time.mktime(date_tuple[:9]) if date_tuple else time.time())
            dest_mail.append('INBOX', None, date_str, raw_email)
            source_mail.store(email_id, '+FLAGS', '\\Deleted')
            return True
        except Exception:
            pass
    return False

def process_emails(source_mail, dest_mail, start_date, end_date):
    global total_processed
    try:
        source_mail.select('INBOX')
        date_criterion = f'(SINCE "{start_date.strftime("%d-%b-%Y")}" BEFORE "{end_date.strftime("%d-%b-%Y")}")'
        result, messages = source_mail.search(None, date_criterion)
        
        if result != "OK":
            return 0

        email_ids = messages[0].split()
        processed_count = 0

        for email_id in email_ids:
            if is_interrupted:
                break
            if copy_and_delete_email(source_mail, dest_mail, email_id):
                processed_count += 1

        source_mail.expunge()
        total_processed += processed_count
        return processed_count

    except Exception as e:
        logging.error(f"エラーが発生しました: {e}")
        return 0

def process_emails_in_batches(source_mail, dest_mail, start_date, end_date):
    current_start = start_date
    while current_start < end_date and not is_interrupted:
        current_end = min(current_start + timedelta(days=7), end_date)
        processed = process_emails(source_mail, dest_mail, current_start, current_end)
        log_message = f"{current_start.strftime('%Y-%m-%d')} から {current_end.strftime('%Y-%m-%d')} まで: {processed} 件のメールを転送しました。"
        logging.info(log_message)
        log_records.append(log_message)
        current_start = current_end

def save_log_to_file():
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"email_transfer_log_{timestamp}.txt"
    with open(filename, "w") as f:
        f.write(f"処理期間: {START_DATE.strftime('%Y-%m-%d')} から {END_DATE.strftime('%Y-%m-%d')}\n\n")
        for record in log_records:
            f.write(record + "\n")
        f.write(f"\n合計 {total_processed} 件のメールが処理されました。")
    logging.info(f"ログが {filename} に保存されました。")

def main():
    global total_processed
    logging.info(f"{START_DATE.strftime('%Y-%m-%d')} から {END_DATE.strftime('%Y-%m-%d')} までのメールを処理します。")

    source_mail = connect_to_imap(SOURCE_EMAIL, SOURCE_PASSWORD)
    dest_mail = connect_to_imap(DEST_EMAIL, DEST_PASSWORD)

    if not source_mail or not dest_mail:
        logging.error("IMAPサーバーへの接続に失敗しました。")
        return

    try:
        process_emails_in_batches(source_mail, dest_mail, START_DATE, END_DATE)
    finally:
        if source_mail:
            source_mail.close()
            source_mail.logout()
        if dest_mail:
            dest_mail.close()
            dest_mail.logout()
        
        if is_interrupted:
            logging.info("プログラムは中断されました。")
        logging.info(f"合計 {total_processed} 件のメールが処理されました。")
        save_log_to_file()

if __name__ == "__main__":
    main()
```

## 5. やっと成功した方法ですが…

こうしてやっとの思いでGmailの転送スクリプトを成功させましたが、再びGoogleの仕様変更が予定されています。アプリパスワードも近々廃止になる模様で、この方法もいずれ使えなくなるかもしれません。その際はまた新しい手段を模索しなければなりませんが、現状の解決策として参考になれば幸いです。

## 6. 生成AIの使い方などFeedbackを下さい
今回、スクリプトの紹介も一つの目的でしたが、もう一つは生成AIの有効な使い方です。何か良い使い方のアイデアがあればお教え下さい。
スクリプトについても改善点などありましたらご指摘頂ければと思います。