きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Gmailでテキストメールを受信して確認する(クライアント問わない方法)

Gmailでテキストメールを送信する方法はたくさん見つかったのですが、受信してテキストメールを確認する方法は見つからなかったのでブログにします。

なお、GmailではHTMLメールとテキストメールを選んで受信はできません。そのため、受信するではなく、受信してテキストメールを確認するが本題です。

環境

  • Gmail
    • 20240320時点

確認方法

テキストメールを確認したいメールのソースを確認する

確認したいメールを開き、「︙」をクリックしてメッセージのソースを確認を選択します。

デコードしてテキストメールを確認する

メッセージのソースからテキストメールの箇所を探します。text/plainのブロックを探してください。画像では6LO86Kqから始まり、ZS5qcC8Kで終わるブロックです。

今回の場合、Content-Transfer-Encoding: base64というメールヘッダがbase64でencodeされていることを示しているので、こちらをbase64でdecodeするとテキストメールの内容を確認できます。

なお、base64でのencode, decodeが必要ない言語ではそのまま確認できます。

そもそもテキストメールとHTMLメールを両立できるのか

マルチパートメールという仕組みによって、テキストメールとHTMLメールを両立できます。ファイル添付ができるのもこちらの仕組みです。

詳細はSendgrid様のページで解説してくれています。

終わりに

メールについて詳しい仕様を知らなかったので、この方法で確認できるのを最近知りました。GmailがHTMLメールしか受信できないから、受信時にテキストメールを切り捨てているのかと思っていました。

基本的にはHTMLメールの検証だけでよいのですが、ごくまれにテキストメールの検証も必要だったのでこれで検証できると分かってよかったです。

参考情報

testcontainers-pythonでMinioを扱う

testcontainers-pythonでminioを使う際に少々手間どったのでメモに残します。

なお、この記事ではtestcontainers自体のメリット等については記載いたしません。

環境

  • Python
    • 3.12
  • boto3
    • 1.34.69
  • minio
    • 7.2.5
  • testcontainers
    • 4.1.1

ゴール

boto3minioに対してファイルアップロードできたことを検証します。minioについてはtestcontainersでセットアップします。

下準備

testcontainersminiodevDependenciesに加えます。各々のパッケージマネジャで読み替えてください。

rye add --dev minio
rye add --dev testcontainers

minioの起動

まずは、access_keysecret_keyを指定します。指定しない場合のデフォルトはminioadminです。

また、localの検証で使用しているminioと同じimageを指定しましょう。理由はわかりませんが、ライブラリではminio/minio:RELEASE.2022-12-02T19-19-22Z"を指定しています。

起動ポートを修正したい場合はportを指定しましょう。基本的にはデフォルトの9000で問題ないはずです。

from testcontainers.minio import MinioContainer

async def test_01(self):
    config = MinioContainer(
        access_key="minio", secret_key="minio1234", image="minio/minio"
    )

次に、get_client()で起動後のインスタンスを取得します。commandは簡単には渡せなさそうですので、このインスタンスを元に必要なバケットを作成します。今回はtmp.localバケットを作成しました。

次にDocker環境でテストする場合は不要ですが、それ以外の環境でテストする場合はTestContainersを外部からアクセスすることになるので、外部ポートを知る必要があります。minio.get_config()["endpoint"]でエンドポイントの情報を取得してください。今回の私は環境変数のS3_URLに格納しています。

with config as minio:
    minio_client = minio.get_client()
    minio_client.make_bucket("tmp.local")
    os.environ["S3_URL"] = minio.get_config()["endpoint"]W

そのほかに取得できる情報としては次のとおりです。

{
  'endpoint': 'localhost:32841',
  'access_key': 'minio',
  'secret_key': 'minio1234'
}

boto3をセットアップする

今まで設定したaccess_keysecret_keyS3_URLboto3の作成時に渡してください。

import os

import boto3  # type: ignore

boto3.client(
    "s3",
    endpoint_url=f"http://{os.getenv('S3_URL', 'localhost:9000')}",
    aws_access_key_id="minio",
    aws_secret_access_key="minio1234",
)

ファイルアップロードする

ファイルアップロードするロジックはboto3の使い方なので省略します。詳細はソースコードを見てください。

アップロードしたファイルを検証する

minioのインスタンスを操作すればアップロードしたファイルを確認できます。

minio_client.get_object("任意のbucket", "任意のキー").data

ソースコード

終わりに

miniotestcontainersのexampleとしては問題ないのですが、実際にboto3からアクセスしようとしたときにポートが分からなくて困りました。最終的な本番環境ではS3を使用する人が多いと思うので、boto3の方がexampleとしては嬉しいんですがね…。

ただ、今回testcontainersを使用してみましたが、正直な感想としてはテストが遅くなるのでちゃんとユースケースは絞って利用したいですね。並列実行のテストをするときに、今回のtestcontainersは役立つと思われます。

参考情報

【Scrap】pytestについて

ZennのScrapsと同じような感覚の記事。間違っている可能性は十分にあります。

今回の記事ではpytestについて自分が調べたことをまとめます。

環境

  • Python
    • 3.7.9
  • pytest
    • 7.4.3
  • pytest-check
    • 2.2.2

Pytestについて

処理順番

pytestではヒットした順番で順次テストするのではなく、collectionというテスト対象のメソッドを集めた過程を経てテストを実行します。この実行前のcollectionで先頭にTestが付いているクラス、test_がついているメソッドを探しています。感覚的には、200テストを探すのに1秒くらいかかるので、テストケースが増えれば増えるほど起動までに時間がかかります。

--last-failedというオプションを付けて実行すると、前回のセッションで失敗したテストだけを実行してくれます。しかし地味にcollectionに時間がかかるので、失敗したテストが複数ファイルに跨っていない限りは、失敗したファイルだけで実行したほうが早く済みます。このオプションを付与しているときは、collection対象を減らすというコントリビュートチャンスかも。

--continue-on-collection-errorsというオプションをつけると、通常はcollection処理中に一部テストで構文エラーがあった時にはテストを実行しませんが、付与すると構文エラーがあっても他のテストを実行してくれます。基本的にはIDEやプラグインでしか使わないオプションだと思います。

テストレポートについて

メインのテストレポートについては、hook機能を使用すると書き換えられます。

@pytest.hookimpl(hookwrapper=True, trylast=True)
def pytest_runtest_makereport(item, call):
  pass

使い方は別のライブラリであるpytest-checkを見るのがいいかも。

厄介なのがshort test summary infoという箇所については、一苦労が必要です。次の処理箇所がpytestshort test summary infoのメッセージを作成している箇所です。メインのエラーメッセージをstrで渡すこともできますが、その場合はAttributeErrorの処理に入ってしまって値が取得できません。

try:
    # Type ignored intentionally -- possible AttributeError expected.
    msg = rep.longrepr.reprcrash.message  # type: ignore[union-attr]
except AttributeError:
    pass

そのため、pytestに合わせて次のように値を渡す必要がありました。

# ASIS
try:
    raise AssertionError(report.longrepr)
except AssertionError:
    excinfo = ExceptionInfo.from_current()
call.excinfo = excinfo
# TOBE
from _pytest.reports import ExceptionChainRepr
from _pytest._code.code import ExceptionRepr, ReprFileLocation

try:
    raise AssertionError(report.longrepr)
except AssertionError as e:
    excinfo = ExceptionInfo.from_current()
    reprcrash = ReprFileLocation(item.nodeid, 0, str(e))
    reprtraceback = ExceptionRepr(reprcrash, excinfo)
    chain_repr = ExceptionChainRepr([(reprtraceback, reprcrash, str(e))])
    report.longrepr = chain_repr

call.excinfo = excinfo

もうちょっと頭のいいやり方があったかもしれないのですが、上のようにしてlogreprstrではなくExceptionChainReprで渡せばshort test summary infoに値を渡せました。

その件はこちらのPull Requestに入れています。

オプションについて

とりあえず実行時につけているオプション。このセクションには中身は無いです。

# -p no:warnings はPython側の機能です
addopts = ["-p no:warnings", "-p no:logging", "--last-failed", "-s"]

もっと便利になりそうなオプションがあったら追加します。

量が多くて読み切れていないです。

Pytest-Checkについて

PythonAssertion Rouletteにならないように回避するライブラリ。

JavaでのSoftAssertionsと同じような処理をします。

こちらの紹介記事に関しては、PullRequestがmergeされたら記載する予定です。

ソースコード

なし

終わりに

正直。pytest-checkへのPullRequestを作成したときに色々と調べたメモです。今後もPytest関連のライブラリにコントリビュートするかは置いておいて、一瞬だけ使用する知識としておくのももったいないのでScrapという形で記事化しました。

参考文献