Daydreaming in Brookline, MA

Pythonでemail送受信テストしてみる

1 テストSMTPサーバーを立てる

emailのテストをするには、何はともあれSMTPサーバーが必要ですが、Pythonではとても簡単にテスト用のSMTPサーバーを立てることができます。

まずはpipでモジュールをインストールします。

pip install aiosmtpd

そして実行します。

python -m aiosmtpd -n

なんと、これだけ。aiosmtpd公式ドキュメントによるとlocalhostのポート8025をリッスンするようです。変えるには -l <[HOST][:PORT]> オプションを与えます。

pipするのが面倒な方は、標準ライブラリにsmtpdがありますが、公式ドキュメントによるとdeprecatedなのでaiosmtpdを使うように書いてありました。以下で使えます。

python -m smtpd -n -c DebuggingServer <IP address>:<port>

2 テストメールを送る

SMTPサーバーを立てるほどではありませんが、テストメールの送信も簡単にできます。

import smtplib
from email import utils
from email.mime.text import MIMEText

msg = MIMEText('This is the body')  # メール本文
msg['To'] = utils.formataddr((
    'Recipient', 'test_recv@example.com'))
msg['From'] = utils.formataddr((
    'Author', 'test_send@example.com'))
msg['Subject'] = 'Test email subject'

server = smtplib.SMTP('localhost', 8025)
# server.set_debuglevel(True)  # デバッグ用詳細メッセージ出力
try:
    server.sendmail('test_send@example.com',
                    ['test_recv@example.com'],
                    msg.as_string())
finally:
    server.quit()

3 テストしてみる

ターミナル上でテスト用のSMTPサーバーを走らせている状態で、上記のテストメール送信スクリプトを別ターミナルから実行してみます。

python emtest.py

あ、来ました。

$ python -m aiosmtpd -n
^[---------- MESSAGE FOLLOWS ----------
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
To: Recipient <test_recv@example.com>
From: Author <test_send@example.com>
Subject: Simple test message
X-Peer: ('::1', 52132, 0, 0)

This is the body.
------------ END MESSAGE ------------

Pythonを使うとemailのテストがとても簡単にできますね。

上記スクリプトのデバッグ用詳細メッセージ出力の行のコメントを外すと、SMTPサーバーとの詳細なやりとりが見られます。(一部情報を伏せています)

$ python emtest.py 
send: 'ehlo localhost.localdomain\r\n'
reply: b'250-localhost.localdomain\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250 HELP\r\n'
reply: retcode (250); Msg: b'localhost.localdomain\n8BITMIME\nHELP'
send: 'mail FROM:<test_send@example.com>\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'rcpt TO:<test_recv@example.com>\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'data\r\n'
reply: b'354 End data with <CR><LF>.<CR><LF>\r\n'
reply: retcode (354); Msg: b'End data with <CR><LF>.<CR><LF>'
data: (354, b'End data with <CR><LF>.<CR><LF>')
send: b'Content-Type: text/plain; charset="us-ascii"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 7bit\r\nTo: Recipient <test_recv@example.com>\r\nFrom: Author <test_send@example.com>\r\nSubject: Simple test message\r\n\r\nThis is the body.\r\n.\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
data: (250, b'OK')
send: 'quit\r\n'
reply: b'221 Bye\r\n'
reply: retcode (221); Msg: b'Bye'

4 宛先が表示されない?

あることに気がつきました。上で紹介した簡易smtpサーバーで表示される情報に、宛先(To/Cc/Bcc)が表示されないことがあるのです。調べてみたところ、宛先の情報はSMTP envelopeに含まれるRCPT TOが使われることがわかりました。実際の宛先が知りたい場合には、上記smtpサーバーで出力される情報では不足です。

そこで、aiosmtpdのドキュメントを参考(ほぼ丸写しとも言う)に、SMTPサーバー側も簡単なスクリプトを作成してRCPT TO情報を出力するようにしました。

import asyncio

class MyHandler:
    async def handle_DATA(self, server, session, envelope):
        print(f'Msg from {envelope.mail_from}')
        # envelopeのRCPT TO情報を表示する
        print(f'Msg for {envelope.rcpt_tos}')
        print(f'Msg data:\n')
        for ln in envelope.content.decode('utf8', errors='replace').splitlines():
            print(f'> {ln}'.strip())
        print()
        print('End of msg')
        return '250 Message accepted for delivery'

from aiosmtpd.controller import Controller
ctrlr = Controller(MyHandler(), hostname='<IPアドレス>')
ctrlr.start()

さて、どうでしょうか。To/Cc/Bccを含めない送信元からメールを出してみます。

Msg from <送り主emailアドレス>
Msg for ['<宛先emailアドレス>']  # RCPT TOsアドレスのリスト
Msg data:

> Date: Fri, 5 Feb 2021 18:12:38 -0500 (EST)
> From: <送り主emailアドレス>
> Message-ID: <XXXXX2588.7.XXXXXXXX.JavaMail.root@XXXXX>
> Subject: <タイトル文字列>
> MIME-Version: 1.0
> Content-Type: text/plain; charset=us-ascii
> Content-Transfer-Encoding: 7bit
>
> This is email contents.
>

End of msg

無事、宛先アドレスがわかるようになりました。(引用では伏せてあります)

なお、送信側を以下のように変えて、RCPT TOにToとは異なるアドレス(複数可能)を指定することができます。

try:
    server.sendmail('send@example.com',  # mail fromアドレス
                    ['recv1@example.com', 'recv2@example.com'],  # RCPT TOsアドレスのリスト
                    msg.as_string())