EY-Office ブログ

GmailのPOPサポート終了に対応する為のツールをAIに作ってもらった

GmailがPOPを使用したサードパーティのアカウントからGmailアカウントへのメールの取得サービスを2026年1月より終了するそうです。→ Google公式情報

実はこのサービスを使っていたので、早速対応しました。

Gmail_stop_pop.jpg Nano Banana Proが生成した画像を使っています

なぜGmailのPOPメール取得を使っているのか

以下のグラフは全メールに占めるスパムメールの率のグラフです(Claudeに作ってもらいました)。これがGmailと大きく関わっています。

EY-Officeを設立した当時

EY-Officeを創設した2000年頃は上のグラフなあるように、まだスパムは少なくMac等のメールアプリを使いホスティングサービスから直接メールを取得していました。平和な時代でした 😄

スパムメールが増えてきた

2005年を過ぎるとスパムメールだらけになりました。ホスティングサービスやメールアプリもスパムフィルターが内蔵されだしましたがレベルが低く大変な時代になりました。 そんな時代にGmailが登場しました❗ そしてGmailは高度なスパムフィルターが付いていました。

そこで、EY-OfficeのメールをGmailに転送し、Gmailで読むことにしました。

メールの送信ドメイン認証

数年前くらいから一部のメールがGmailに届かない事が起きだしました。調べてみるとホスティングサービスまでは来ていますがGmailには転送されていませんでした。原因は送信ドメイン認証でした。wikipediaによると、

迷惑メール対策の一つとして使われており、受信側のメールサーバで送信者情報(reverse-path)を検証することで、差出人メールアドレス(ヘッダFrom)のなりすましを検出している。 SPFとDKIMがよく使われている。

この問題をホスティングサービスに問い合わせたところ、転送を止めてGmailのPOPメール取得を使うようにしたらどうだろうと言われ、そうしました。

そして現在に至ります。

GmailのPOPメール取得サービル停止にどう対応したか

GmailのPOPメール取得サービル停止に対応するのにはいくつかの方法があると思います。

  1. POPメール取得を止め転送に戻す
  2. Gmail以外の高性能なスパムフィルターを内蔵したメールソフトを使う
  3. Google Workspaceを契約しEY-Office宛てメールをGmailで受信する

利用しているホスティングサービスを調べたところ、さくらのレンタルサーバから外部メールサービスにメール転送されないにあるように、メールを転送した際の送信ドメイン認証の問題は解決しているようなので、1.の「POPメール取得を止め転送に戻す」を採用しました。

ただし、本当に大丈夫なのだろうか? という疑問が拭えないのでホスティングサービスから直接メールを受信し、1日分のサマリーを作り人が(私が)確認するツールを作成しました。

メール確認ツール

Pythonの勉強を兼ねてPythonで作りました。もちろんコードはAI(Claude)に生成してもらったコードを修正して使いまいました。

メールは昨日に受信したメールの一覧で、時間 送信者 件名が以下のように完璧ではありませんが等倍フォントなら送信者 件名が揃うようにしています。このへんのコードもAIに作ってもらいました。
現在は自社サーバーで動かしていますが、将来的にはAWS Lambdaで動かそうと思っています。コードは最後に添付しました。

Date: 2025-12-16
        ・・・
11:32:52 楽天みんなのレビュー<hyouka@raku  【楽天市場】お買い物へのレビューをお願いします
11:33:13 生成AIフォーラム 2025 REPLAY事務  【見逃し再配信】みずほFGが解説「AIエージェントが変える金融の未来図」/ ライ
11:46:19 日経クロステック ウイークリー・  地下インフラに救世主、光ファイバーや人工衛星で異常検知/楽天で生損保一体型基幹シ
11:51:10 support@sakura.ad.jp               2要素認証通過のお知らせ
11:56:08 Yuumi Yoshida <yy@ey-office.com>  Test1
12:03:11 ビジネス+ITメール<special@ml3.sb  【抽選でデル・テクノロジーズ製品が当たる!】PC導入に関するアンケート:ビジネス
12:04:08 connpass <no-reply@connpass.com>  connpass グループ管理者からのメッセージ 「Forkwell Commu
        ・・・

まとめ

しばらくツールを運用し全てのメールがGmailへ転送されているようなら、ツールは止めても良いかなと思います。
このような使い捨てのツール作成にはAIは適していますね。💕

  
  


メール確認ツールのコード
#!/usr/bin/env python3

import imaplib
import email
from email.header import decode_header
from email.utils import parsedate_to_datetime
import smtplib
from email.mime.text import MIMEText
from datetime import timezone, timedelta, date
import unicodedata
import os

# IMAP/SMTP接続設定
IMAP_SERVER = os.getenv("IMAP_SERVER")
SMTP_SERVER = os.getenv("SMTP_SERVER")
IMAP_PORT = int(os.getenv("IMAP_PORT"))
SMTP_PORT = int(os.getenv("SMTP_PORT"))
EMAIL_ACCOUNT = os.getenv("EMAIL_ACCOUNT")
EMAIL_PASSWORD = os.getenv("EMAIL_PASSWORD")

def connect_to_imap():
    """IMAPサーバーに接続"""
    mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
    mail.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
    return mail

def decode_mime_words(s):
    """MIMEエンコードされた文字列をデコード"""
    if s is None:
        return ''
    decoded_fragments = decode_header(s)
    fragments = []
    for fragment, encoding in decoded_fragments:
        if isinstance(fragment, bytes):
            # エンコーディングを明示的に処理
            if encoding:
                try:
                    fragment = fragment.decode(encoding, errors='replace')
                except:
                    fragment = fragment.decode('utf-8', errors='replace')
            else:
                # エンコーディングが不明な場合、複数試行
                for enc in ['iso-2022-jp', 'utf-8', 'shift_jis', 'euc-jp']:
                    try:
                        fragment = fragment.decode(enc)
                        break
                    except:
                        continue
                else:
                    fragment = fragment.decode('utf-8', errors='replace')
        fragments.append(str(fragment))
    return ''.join(fragments)

def convert_to_jst(date_str):
    """メールの日付を日本時間(JST)に変換"""
    try:
        # メールの日付文字列をdatetimeオブジェクトに変換
        dt = parsedate_to_datetime(date_str)

        # 日本時間(JST = UTC+9)に変換
        jst = timezone(timedelta(hours=9))
        dt_jst = dt.astimezone(jst)

        # yyyy-mm-dd HH:MM:SS 形式で返す
        return dt_jst.strftime('%Y-%m-%d %H:%M:%S')
    except:
        return date_str  # 変換できない場合は元の文字列を返す

def fetch_latest_emails(mail, mailbox='INBOX', num_emails=10):
    """最新のメールを取得"""
    # メールボックスを選択
    mail.select(mailbox)

    # すべてのメールIDを検索
    status, messages = mail.search(None, 'ALL')
    email_ids = messages[0].split()
    # 最新のメールIDを取得(昇順で取得)
    latest_email_ids = email_ids[-num_emails:]  # [::-1]

    emails = []

    for email_id in latest_email_ids:
        # メールを取得
        status, msg_data = mail.fetch(email_id, '(RFC822)')

        for response_part in msg_data:
            if isinstance(response_part, tuple):
                # メールメッセージをパース
                msg = email.message_from_bytes(response_part[1])

                # 件名を取得
                subject = decode_mime_words(msg['Subject']) if msg['Subject'] else 'No Subject'

                # 送信者を取得
                from_ = decode_mime_words(msg['From']) if msg['From'] else 'Unknown'

                # 日付を取得して日本時間に変換
                date = convert_to_jst(msg['Date']) if msg['Date'] else 'Unknown'

                # 本文を取得
                body = ""
                if msg.is_multipart():
                    for part in msg.walk():
                        content_type = part.get_content_type()
                        content_disposition = str(part.get("Content-Disposition"))

                        if content_type == "text/plain" and "attachment" not in content_disposition:
                            payload = part.get_payload(decode=True)
                            if payload:
                                # 文字エンコーディングを取得
                                charset = part.get_content_charset()
                                if charset:
                                    try:
                                        body = payload.decode(charset, errors='replace')
                                    except:
                                        body = payload.decode('utf-8', errors='replace')
                                else:
                                    # charsetが指定されていない場合、複数試行
                                    for enc in ['iso-2022-jp', 'utf-8', 'shift_jis', 'euc-jp']:
                                        try:
                                            body = payload.decode(enc)
                                            break
                                        except:
                                            continue
                                    else:
                                        body = payload.decode('utf-8', errors='replace')
                            break
                else:
                    payload = msg.get_payload(decode=True)
                    if payload:
                        charset = msg.get_content_charset()
                        if charset:
                            try:
                                body = payload.decode(charset, errors='replace')
                            except:
                                body = payload.decode('utf-8', errors='replace')
                        else:
                            # charsetが指定されていない場合、複数試行
                            for enc in ['iso-2022-jp', 'utf-8', 'shift_jis', 'euc-jp']:
                                try:
                                    body = payload.decode(enc)
                                    break
                                except:
                                    continue
                            else:
                                body = payload.decode('utf-8', errors='replace')

                emails.append({
                    'id': email_id.decode(),
                    'subject': subject,
                    'from': from_,
                    'date': date,
                    'body': body[:200]  # 本文の最初の200文字のみ
                })

    return emails

def get_display_width(text):
    """文字列の表示幅を取得(全角=2、半角=1)"""
    width = 0
    for char in text:
        if unicodedata.east_asian_width(char) in ('F', 'W', 'A'):
            # F: Fullwidth (全角), W: Wide (広), A: Ambiguous (曖昧)
            width += 2
        else:
            # N: Neutral, H: Halfwidth, Na: Narrow
            width += 1
    return width

def truncate_by_width(text, max_width):
    """指定した表示幅で文字列を切る(全角=2、半角=1)"""
    current_width = 0
    result = []

    for char in text:
        char_width = 2 if unicodedata.east_asian_width(char) in ('F', 'W', 'A') else 1

        if current_width + char_width > max_width:
            break

        result.append(char)
        current_width += char_width

    return ''.join(result)

def send_email(to_email, subject, body):
    """
    メールを送信する

    Args:
        to_email: 送信先メールアドレス(文字列またはリスト)
        subject: 件名
        body: 本文
    """
    try:
        # メッセージオブジェクトを作成
        msg = MIMEText(body, "plain", "utf-8")
        msg['From'] = EMAIL_ACCOUNT
        msg['To'] = to_email if isinstance(to_email, str) else ', '.join(to_email)
        msg['Subject'] = subject

         # SMTPサーバーに接続
        server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
        server.starttls()  # TLS暗号化を開始

        # ログイン
        server.login(EMAIL_ACCOUNT, EMAIL_PASSWORD)
        # メールを送信
        server.sendmail(EMAIL_ACCOUNT, to_email, msg.as_string())
        # 接続を閉じる
        server.quit()

        print(f"-- Send email: {to_email}")
        return True

    except smtplib.SMTPException as e:
        print(f"SMTP Error : {e}")
        return False
    except Exception as e:
        print(f"Error : {e}")
        return False

def main():
    try:
        # IMAPに接続
        mail = connect_to_imap()
        print("-- connected to IMAP server --")

        # 最新10件のメールを取得
        emails = fetch_latest_emails(mail, num_emails=100)
        yesterday_str = (date.today() - timedelta(days=1)).strftime('%Y-%m-%d')

        message = ""
        message += f"Date: {yesterday_str}\n"
        # まだ昨日のメールがありそうなら省略記号を追加
        if emails[0]['date'].startswith(yesterday_str):
          message += "..............\n"

        # メール情報を整形して追加
        for i, email in enumerate(emails, 1):
           if email['date'].startswith(yesterday_str):
                message += f"{email['date'][11:]} {truncate_by_width(email['from'].ljust(32), 32)}  {email['subject'][:40]}\n"


        # メール送信
        print(message)
        send_email(EMAIL_ACCOUNT, f"メール一覧 {yesterday_str}", message)

        # 接続を閉じる
        mail.close()
        mail.logout()
        print("-- disconnected from IMAP server --")

    except imaplib.IMAP4.error as e:
        print(f"IMAP error : {e}")
    except Exception as e:
        print(f"Error : {e}")

if __name__ == "__main__":
    main()

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ