【python】Gmailで外部メール受信遅延の対処法

python logo Python3

Gmailをメーラとして、自作メールサーバからメール取得して表示しているとき、実際にメールサーバが受信してから1時間以上待たなければいけない場合があります。
GmailでのPOP3ポーリング間隔が遅いため、スクレイピングをするためのメモです。

概要

私は自作メールサーバをPostfixとDovecotで作成しており、メールを見る媒体(メーラー)でGmailを使用しています。
メールサーバ構築については以下記事を参照

【AlmaLinux】メールサーバ構築(Postfix+Dovecot)
AlmaLinuxを使用して、メールサーバーを構築する方法を解説します。PostfixやDovecotを利用し、メール送信や受信の設定を詳しく説明します。

目的

メーラーとして設定しているGmailから、メールサーバへのPOP3ポーリングは数十分~1時間程度の間隔があり、非常に不安定です。

GmailからメールサーバへのPOP3ポーリング間隔を「短く」+「一定」にして、常に受信したメールをすぐ確認可能にする

課題と解決策

Q. APIなどで公式的な自動更新の手段がない
Ans. pythonのスクレイピング(Selenium)を使用してWeb画面から手動更新ボタンを定期的にクリックする

Q. Seleniumを使用してGoogleアカウントにログインすると自動制御判定されてログイン不可
Ans. Seleniumを拡張したライブラリであるSeleniumBaseを使用する

Q. 定期実行の手段
Ans. AWS Lambdaを使用して定期実行をする
ー>∵ SeleniumBaseがAWS Lambdaでサポートされていない
Ans. 個人サーバで定期実行をする

設計

環境

  • AlmaLinux release 9.1 (Lime Lynx)
  • Python 3.9.14(venv使用)
  • Google Chrome 113.0.5672.92
  • pip 23.1.2
# cat /etc/redhat-releasecat /etc/redhat-release
AlmaLinux release 9.1 (Lime Lynx)

# python -V
Python 3.9.14

# google-chrome --version
Google Chrome 113.0.5672.92

# pip -V
pip 23.1.2 from <venv path>/lib64/python3.9/site-packages/pip (python 3.9)

更新の流れ

Google(Gmail)にログインをする
メールアドレスを記入して、[次へ]をクリック

パスワードを入力して、[次へ]をクリック

ログインできたことを確認
今回は[受信トレイ]タブが確認できることで同義とする

[設定] > [アカウントとインポート] > [他のアカウントのメールを確認]から
各ユーザメールアドレスにある[メールを今すぐ確認する]をクリックする

スクリプト実行について

  • Cronで定期実行を行う(間隔はサーバに負荷がかかりすぎない程度にする)
  • 実行結果をログとして溜めていく
  • ログはログローテートされるようにする
  • 実行が失敗した場合は、Gmailにエラー内容とともにメールを送信する

手順

事前準備

Gmailアカウントが2つ必要になる。

理由は、メールサーバーからメール送信を行う際はOP25B対応で、Gmail経由の設定をしている。
その際に、2段階認証プロセスの有効時に利用可能な「アプリパスワード」を使用して実装しています。

しかし、スクレイピングでGoogleアカウントにログインする際は、2段階認証プロセスを無効にしてパスワードのみのログインを使用します。

つまり、2つのGmailアカウントが必要になるのです。

  1. 自作メールサーバーのOP25B対応用の2段階認証プロセス有効にしているGmailアカウント(以降Gmailアカウント①とする)
  2. スクレイピング用の2段階認証プロセス無効にしているGmailアカウント(以降Gmailアカウント②とする)

Gmailアカウント②をメールサーバのメーラーとして使用することになります。

Python開発環境

Google Chrome Driverのインストール

Google chrome RPMパッケージのダウンロード

# wget https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm

# ll
-rw-r--r--  1 root root 96605932 May  6 07:53 google-chrome-stable_current_x86_64.rpm

インストール実行

# dnf install google-chrome-stable_current_x86_64.rpm
Complete!

バージョン確認

# google-chrome --version
Google Chrome 113.0.5672.92
venv設定
venvとは

venvはPythonの仮想環境を作成するためのツールです。

仮想環境は、プロジェクトごとに独立したPythonの実行環境を作ることができます。
これにより、プロジェクトごとに必要なライブラリのバージョンや依存関係を管理することができます。
つまり、異なるプロジェクト間でのパッケージの衝突を回避することができます。

venvを作成すると、新しいディレクトリが作成され、その中に独立したPython実行環境が含まれます。
仮想環境を有効化すると、システム全体ではなく、その仮想環境内でPythonプログラムが実行されます。

venvはPythonの標準ライブラリとして提供されており、Python3.3以降ではデフォルトで利用可能です。

手順
  1. 新環境の作成

    [user@localhost ~]$ cd [project dir]
    [user@localhost <project dir>]$ python3 -m venv [new env name]

    EX

    [user@localhost ~]$ cd GmailScraping
    [user@localhost GmailScraping]$ python3 -m venv GmailScraping
  2. Activate(仮想環境を有効化)

    [user@localhost ~]$ cd [project dir]
    [user@localhost <project dir>]$ source [new env name]/bin/activate
    (<new env name>) [user@localhost <project dir>]$

    EX

    [user@localhost ~]$ cd GmailScraping
    [user@localhost GmailScraping]$ source GmailScraping/bin/activate
    (GmailScraping) [user@localhost GmailScraping]$
  3. deactivate(仮想環境を無効化)

    $ deactivate

SeleniumBaseインストール

$ pip install seleniumbase
SeleniumBaseについての説明

SeleniumBaseは、Seleniumを拡張したテストフレームワークです。
この便利なツールを使用すると、開発者はシンプルなコードでテストスクリプトを作成し、
ブラウザの制御やテストの実行、結果の記録を効率的に行うことができます。
さらに、テストの作成や管理を容易にし、テスト結果の報告書を生成する機能も提供されています。

SeleniumBaseの利点は、開発者が短いコードでテストを作成できることです。これにより、テストの効率化や品質向上が図れます。
テスト結果の自動記録や報告書の生成も手間を省くことが可能です。

ただし、AWS等の特定の環境での使用には制約があることに注意が必要です。

requirements.txt

requirements.txtを記載しておきます。

$ pip freeze > requirements.txt
async-generator==1.10
attrs==23.1.0
beautifulsoup4==4.12.2
behave==1.2.6
certifi==2023.5.7
cffi==1.15.1
chardet==5.1.0
charset-normalizer==3.1.0
colorama==0.4.6
cryptography==40.0.2
cssselect==1.2.0
exceptiongroup==1.1.1
execnet==1.9.0
fasteners==0.18
filelock==3.12.0
h11==0.14.0
idna==3.4
importlib-metadata==6.6.0
iniconfig==2.0.0
jaraco.classes==3.2.3
jeepney==0.8.0
keyring==23.13.1
markdown-it-py==2.2.0
mdurl==0.1.2
more-itertools==9.1.0
outcome==1.2.0
packaging==23.1
parameterized==0.9.0
parse==1.19.0
parse-type==0.6.0
pdbp==1.4.0
platformdirs==3.5.1
pluggy==1.0.0
py==1.11.0
pycparser==2.21
Pygments==2.15.1
pynose==1.4.4
pyOpenSSL==23.1.1
pyotp==2.8.0
pyparsing==3.0.9
PySocks==1.7.1
pytest==7.3.1
pytest-forked==1.6.0
pytest-html==2.0.1
pytest-metadata==2.0.4
pytest-ordering==0.6
pytest-rerunfailures==11.1.2
pytest-xdist==3.2.1
PyYAML==6.0
requests==2.30.0
requests-toolbelt==1.0.0
rich==13.3.5
sbvirtualdisplay==1.2.0
SecretStorage==3.3.3
selenium==4.9.1
seleniumbase==4.14.9
six==1.16.0
sniffio==1.3.0
sortedcontainers==2.4.0
soupsieve==2.4.1
tabcompleter==1.2.0
tomli==2.0.1
tqdm==4.65.0
trio==0.22.0
trio-websocket==0.10.2
urllib3==2.0.2
wsproto==1.2.0
zipp==3.15.0

Pythonコード

$ vi gmail_pop3_check.py
# -*- coding: utf_8 -*-
import time
from seleniumbase import SB
import datetime
from email.mime.text import MIMEText
import smtplib
from email import policy
import sys

with SB(browser="chrome", uc=True, headless=True) as driver:
    # Googleアカウントログインしてから、手動POP3ポーリングを行う
    try:
        ######################################################
        ##########    Googleアカウントログイン     #############
        ######################################################
        # Googleのアカウントログインページに遷移
        driver.open("https://accounts.google.com/v3/signin/identifier?dsh=S1224780406%3A1684001458695011&ifkv=Af_xneEIqjygE2F9eIZcMy2afA1qmll65ORxiWQTQTVepOdWxFneQJEnvArrzXlg1Lxvauy7OnJ-Iw&service=mail&flowName=GlifWebSignIn&flowEntry=ServiceLogin")

        # [メールアドレスまたは電話番号]を入力
        driver.type("#identifierId", "<Gmailアカウント②>@gmail.com")
        # [次へ]をクリック
        driver.click("#identifierNext > div > button")

        # パスワードのテキストボックスが表示されるまで待機
        driver.wait_for_element("#password > div.aCsJod.oJeWuf > div > div.Xb9hP > input")
        # [パスワードを入力]にパスワードを入力
        driver.type(
            "#password > div.aCsJod.oJeWuf > div > div.Xb9hP > input", "<Gmailアカウント②>のアプリパスワード")
        # [次へ]をクリック
        driver.click("#passwordNext > div > button")

        # ログインできたことを確認するために、[受信トレイ]の存在を確認する
        driver.wait_for_element('a:contains("受信トレイ")', timeout=30)

        ######################################################
        ##########    [メールを今すぐ確認する]     #############
        ######################################################

        # 新しいWindowsを立ち上げる
        driver.open_new_window()
        # 新しいタブに切り替える
        driver.switch_to_window(1)

        # [Gmail>設定>アカウントとインポート]のページへ遷移
        driver.open("https://mail.google.com/mail/#settings/accounts")

        # [メールを今すぐ確認する]のIDをリスト化
        # メール受信をするアカウント数
        user_num = 3

        # [メールを今すぐ確認する]ボタンの表示を確認する
        driver.wait_for_element('span:contains("メールを今すぐ確認する")')
        # [メールを今すぐ確認する]ボタンのセレクタからidを取得
        button_id = driver.find_element('span:contains("メールを今すぐ確認する")').get_attribute('id')
        # idの末尾から1文字削る(∵末尾の数字がアカウント毎の番号になる。アカウントが3つあれば、[**1,**2,**3]となる)
        button_id = button_id[:-1]
        # CSSセレクタでは特殊文字でエスケープ表現(\)をする必要があるための対応
        escape_list = ['!','@','#','$','%','^','&','*','(',')','+','=',',','.','/',':'] # エスケープ表現のリスト
        # リストの文字をエスケープ表現に置換
        for escape_item in escape_list:
            button_id = button_id.replace(escape_item, '\\' + escape_item)
        # idであるため頭文字に[#]を追記+末尾にアカウント数に応じて文字を追加してリスト化
        click_list = ['#'+button_id+str(x+1) for x in range(user_num)]

        # [メールを今すぐ確認する]のボタンをすべてクリックする
        for click_item in click_list:
            # [メールを今すぐ確認する]の表示を確認する
            driver.wait_for_element(click_item)
            # [メールを今すぐ確認する]をクリック
            driver.click(click_item, timeout=30)
            # クリック後10秒待機する
            ## エラー[stale element reference: stale element not found]対策
            driver.sleep(10)

        # 成功したのでログに「[現在日時] Success!!」を出力
        print('['+str(datetime.datetime.now())+'] Success!!')

    # 例外処理:Gmailとしてエラー行とエラー内容を送信する
    except Exception as e:
        # エラー行を取得するための変数
        exc_type, exc_obj, exc_tb = sys.exc_info()

        # ログに「[現在日時] エラー行 エラー内容」を出力する
        print('['+str(datetime.datetime.now())+'] ',end="")
        print('error line:',str(exc_tb.tb_lineno))
        print(e)

        # Gmail送信に関する情報
        account = "<Gmailアカウント①>@gmail.com"         # ログインアカウントのGmail
        password = "<Gmailアカウント①>アプリパスワード"   # アプリパスワード
        to_email = "<Gmailアカウント②>@gmail.com"        # 宛先メールアドレス
        from_email = "<Gmailアカウント①>@gmail.com"      # 送信元メールアドレス
        smtp_host = "smtp.gmail.com"                    # SMPTホストにGmailを指定
        smtp_port = 587                                 # SMPTのポート番号
        subject = "An error occurred"                   # メールタイトル
        message = "An error occurred: Line "+str(exc_tb.tb_lineno)+"\r\n"+str(e) # メール本文

        # メール作成
        msg = MIMEText(message, "plain", policy=policy.default)
        msg["Subject"] = subject
        msg["To"] = to_email
        msg["From"] = from_email

        # メール送信
        with smtplib.SMTP(smtp_host, smtp_port) as smtp:
            smtp.starttls()
            smtp.login(account, password)
            smtp.send_message(msg)
実行

venvがActivete状態で、以下コマンドから実行します。

(<new env name>) [user@localhost <project dir>]$ python ファイル名.py

EX )

(GmailScraping) [user@localhost GmailScraping]$ python gmail_pop3_check.py

結果は以下のようになります。

########## 成功時 ##############
[2023-05-20 23:40:49.718902] Success!!

########## 失敗時 ##############
[2023-05-21 00:05:22.333920] error line: 47
Message: stale element reference: stale element not found
  (Session info: chrome=113.0.5672.92)
Stacktrace:
#0 0x562f311c6133 <unknown>
#1 0x562f30efa966 <unknown>
#2 0x562f30eff126 <unknown>
#3 0x562f30f01070 <unknown>
#4 0x562f30f6fe02 <unknown>
#18 0x7f49c4e5d802 start_thread

ログ設定

ログファイル作成

出力先のログファイルを作成します。

/var/log配下に新規にフォルダやファイルを作成するには、root権限が必要です。
root以外のユーザで作成する場合は注意してください。

今回は、以下の要件で設定していきます。

  • ユーザ名:test
  • ログファイルの絶対パス:/var/log/GmailScraping/gmail_pop3_check.log
$ sudo mkdir /var/log/GmailScraping
$ sudo touch /var/log/GmailScraping/gmail_pop3_check.log
$ sudo chmod 646 /var/log/GmailScraping/gmail_pop3_check.log
ログローテート設定

以下要件でログローテートをする設定を入れます。

  • ログローテート設定ファイルの絶対パス:/etc/logrotate.d/gmail_pop3_check
  • ログ容量が100KBを超えたらログローテート実施
  • 5世代まで保存
  • ログローテート時に圧縮
$ sudo vi /etc/logrotate.d/gmail_pop3_check
/var/log/GmailScraping/*.log {
    missingok
    notifempty
    size 500k
    compress
    rotate 5
}

Cron設定

Cronを設定して、定期実行を行います。

以下要件で定期実行の設定をします。

  • 5分おきに実行する
  • venv環境を使用しているので、実行の際venvの絶対パスで記述
  • 前項で作成したログファイルにprintの出力を記述する
  • ユーザ名:test
  • ログファイルの絶対パス:/var/log/GmailScraping/gmail_pop3_check.log
$ crontab -e
*/5 * * * * /home/<username>/<project dir>/<new env name>/bin/python /home/<username>/<project dir>/gmail_pop3_check.py >> <ログファイルの絶対パス> 2>&1

(EX)
*/5 * * * * /home/test/GmailScraping/GmailScraping/bin/python /home/test/GmailScraping/gmail_pop3_check.py >> /var/log/GmailScraping/gmail_pop3_check.log 2>&1

まとめ

GmailのPOP3ポーリングをするために、2つの学びがありました。

  1. Googleアカウントログインに利用可能かつ、Seleniumより簡易的に書けるSeleniumBaseを使用
  2. ログ出力とログローテート設定方法

今後の課題

上記手順で実行している場合、CPU使用率が5分ごとに上昇しています。
自作サーバであるため、できるだけリソースは空けたいです。

そのために実行間隔を変えずに考えられる対応は以下です。

  1. EC2の無料枠で、新たにLinuxサーバを立てて実行する
  2. AWS LambdaのDockerを利用するとできるかも(Twiiteで見かけた+Dockerの知識なし)
  3. 他のクラウドやレンタルサーバを検討(Heroku無料枠がなくなったのは痛いです、、)

今後、時間があるときに再検討して対応したいと考えています。

Follow me!

PAGE TOP
タイトルとURLをコピーしました