smtplib – 簡易メール転送プロトコルクライアント

目的:メール送信を含めて SMTP サーバと通信する
利用できるバージョン:1.5.2 以上

smtplib はメールの送信や SMTP サーバとの通信に便利な SMTP クラスを提供します。

Note

本稿で紹介するサンプルコードのメールアドレス、ホスト名、IP アドレスは隠蔽されていますが、そのログはコマンドの流れと正確なレスポンスを説明します。

メールを送信する

SMTP の一般的な用途はメールサーバへ接続してメールを送信することです。メールサーバのホスト名とポート番号は引数としてコンストラクタに渡すか、明示的に connect() メソッドで指定します。接続した時点でエンベロープパラメータとメッセージ本文と共に sendmail() を呼び出してください。smtplib はコンテンツやヘッダを全く変更しないので、メッセージのテキストは RFC 2882 完全準拠のメッセージであるべきです。つまり自分で FromTo をヘッダに追加するということを意味します。

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

# メッセージを作成する
msg = MIMEText('This is the body of the message.')
msg['To'] = email.utils.formataddr(('Recipient', 'recipient@example.com'))
msg['From'] = email.utils.formataddr(('Author', 'author@example.com'))
msg['Subject'] = 'Simple test message'

server = smtplib.SMTP('mail')
server.set_debuglevel(True) # サーバとの通信内容を表示する
try:
    server.sendmail('author@example.com', ['recipient@example.com'], msg.as_string())
finally:
    server.quit()

このサンプルではクライアントとサーバ間の通信を表示するためにデバッグも有効にしています。そうしないと、このサンプルは何も出力しません。

$ python smtplib_sendmail.py
send: 'ehlo localhost.local\r\n'
reply: '250-mail.example.com Hello [192.168.1.17], pleased to meet you\r\n'
reply: '250-ENHANCEDSTATUSCODES\r\n'
reply: '250-PIPELINING\r\n'
reply: '250-8BITMIME\r\n'
reply: '250-SIZE\r\n'
reply: '250-DSN\r\n'
reply: '250-ETRN\r\n'
reply: '250-AUTH GSSAPI DIGEST-MD5 CRAM-MD5\r\n'
reply: '250-DELIVERBY\r\n'
reply: '250 HELP\r\n'
reply: retcode (250); Msg: mail.example.com Hello [192.168.1.17], pleased to meet you
ENHANCEDSTATUSCODES
PIPELINING
8BITMIME
SIZE
DSN
ETRN
AUTH GSSAPI DIGEST-MD5 CRAM-MD5
DELIVERBY
HELP
send: 'mail FROM:<author@example.com> size=266\r\n'
reply: '250 2.1.0 <author@example.com>... Sender ok\r\n'
reply: retcode (250); Msg: 2.1.0 <author@example.com>... Sender ok
send: 'rcpt TO:<recipient@example.com>\r\n'
reply: '250 2.1.5 <recipient@example.com>... Recipient ok\r\n'
reply: retcode (250); Msg: 2.1.5 <recipient@example.com>... Recipient ok
send: 'data\r\n'
reply: '354 Enter mail, end with "." on a line by itself\r\n'
reply: retcode (354); Msg: Enter mail, end with "." on a line by itself
data: (354, 'Enter mail, end with "." on a line by itself')
send: 'From nobody Sun Sep 28 10:02:48 2008\r\nContent-Type: text/plain; charset="us-ascii"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 7bit\r\nTo: Recipient <recipient@example.com>\r\nFrom: Author <author@example.com>\r\nSubject: Simple test message\r\n\r\nThis is the body of the message.\r\n.\r\n'
reply: '250 2.0.0 m8SE2mpc015614 Message accepted for delivery\r\n'
reply: retcode (250); Msg: 2.0.0 m8SE2mpc015614 Message accepted for delivery
data: (250, '2.0.0 m8SE2mpc015614 Message accepted for delivery')
send: 'quit\r\n'
reply: '221 2.0.0 mail.example.com closing connection\r\n'
reply: retcode (221); Msg: 2.0.0 mail.example.com closing connection

sendmail() の2番目の引数、受取人がリストとして渡されていることに注意してください。次々にメッセージを配送するために受取人のリストへ幾つでもメールアドレスを追加することができます。エンベロープ情報はメッセージヘッダから分離されるので BCC で指定するメールアドレスは sendmail() メソッドの引数に追加しますが、メッセージヘッダには追加しません。

認証と暗号化

SMTP クラスはサーバが認証と TLS (transport layer security) 暗号化をサポートするときにそういった機能も扱えます。サーバが TLS をサポートする場合、サーバに対してあなたのコンピュータを識別してどのような機能が有効かを問い合わせるために、直接 ehlo() を呼び出してください。それからその応答を確認するために has_extn() を呼び出してください。TLS が開始されると、認証の前に再度 ehlo() を呼び出さなければなりません。

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

# ユーザに接続情報を入力させる
to_email = raw_input('Recipient: ')
servername = raw_input('Mail server name: ')
username = raw_input('Mail user name: ')
password = getpass.getpass("%s's password: " % username)

# メッセージを作成する
msg = MIMEText('Test message from PyMOTW.')
msg.set_unixfrom('author')
msg['To'] = email.utils.formataddr(('Recipient', to_email))
msg['From'] = email.utils.formataddr(('Author', 'author@example.com'))
msg['Subject'] = 'Test from PyMOTW'

server = smtplib.SMTP(servername)
try:
    server.set_debuglevel(True)

    # 自分自身を識別してサポートしている機能をサーバに入力させる
    server.ehlo()

    # このセッションを暗号化できるならそうする
    if server.has_extn('STARTTLS'):
        server.starttls()
        server.ehlo() # TLS コネクションで自分自身を再識別する

    server.login(username, password)
    server.sendmail('author@example.com', [to_email], msg.as_string())
finally:
    server.quit()

TLS が有効になった時点で STARTTLS は ( EHLO の応答の) 機能リストに現れないことに注意してください。

$ python smtplib_authenticated.py
Recipient: recipient@example.com
Mail server name: smtpauth.isp.net
Mail user name: user@isp.net
user@isp.net's password:
send: 'ehlo localhost.local\r\n'
reply: '250-elasmtp-isp.net Hello localhost.local [<your IP here>]\r\n'
reply: '250-SIZE 14680064\r\n'
reply: '250-PIPELINING\r\n'
reply: '250-AUTH PLAIN LOGIN CRAM-MD5\r\n'
reply: '250-STARTTLS\r\n'
reply: '250 HELP\r\n'
reply: retcode (250); Msg: elasmtp-isp.net Hello localhost.local [<your IP here>]
SIZE 14680064
PIPELINING
AUTH PLAIN LOGIN CRAM-MD5
STARTTLS
HELP
send: 'STARTTLS\r\n'
reply: '220 TLS go ahead\r\n'
reply: retcode (220); Msg: TLS go ahead
send: 'ehlo localhost.local\r\n'
reply: '250-elasmtp-isp.net Hello localhost.local [<your IP here>]\r\n'
reply: '250-SIZE 14680064\r\n'
reply: '250-PIPELINING\r\n'
reply: '250-AUTH PLAIN LOGIN CRAM-MD5\r\n'
reply: '250 HELP\r\n'
reply: retcode (250); Msg: elasmtp-isp.net Hello farnsworth.local [<your IP here>]
SIZE 14680064
PIPELINING
AUTH PLAIN LOGIN CRAM-MD5
HELP
send: 'AUTH CRAM-MD5\r\n'
reply: '334 PDExNjkyLjEyMjI2MTI1NzlAZWxhc210cC1tZWFseS5hdGwuc2EuZWFydGhsaW5rLm5ldD4=\r\n'
reply: retcode (334); Msg: PDExNjkyLjEyMjI2MTI1NzlAZWxhc210cC1tZWFseS5hdGwuc2EuZWFydGhsaW5rLm5ldD4=
send: 'ZGhlbGxtYW5uQGVhcnRobGluay5uZXQgN2Q1YjAyYTRmMGQ1YzZjM2NjOTNjZDc1MDQxN2ViYjg=\r\n'
reply: '235 Authentication succeeded\r\n'
reply: retcode (235); Msg: Authentication succeeded
send: 'mail FROM:<author@example.com> size=221\r\n'
reply: '250 OK\r\n'
reply: retcode (250); Msg: OK
send: 'rcpt TO:<recipient@example.com>\r\n'
reply: '250 Accepted\r\n'
reply: retcode (250); Msg: Accepted
send: 'data\r\n'
reply: '354 Enter message, ending with "." on a line by itself\r\n'
reply: retcode (354); Msg: Enter message, ending with "." on a line by itself
data: (354, 'Enter message, ending with "." on a line by itself')
send: 'Content-Type: text/plain; charset="us-ascii"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 7bit\r\nTo: Recipient <recipient@example.com>\r\nFrom: Author <author@example.com>\r\nSubject: Test from PyMOTW\r\n\r\nTest message from PyMOTW.\r\n.\r\n'
reply: '250 OK id=1KjxNj-00032a-Ux\r\n'
reply: retcode (250); Msg: OK id=1KjxNj-00032a-Ux
data: (250, 'OK id=1KjxNj-00032a-Ux')
send: 'quit\r\n'
reply: '221 elasmtp-isp.net closing connection\r\n'
reply: retcode (221); Msg: elasmtp-isp.net closing connection

メールアドレスを検証する

SMTP プロトコルにはメールアドレスが有効かどうかをサーバへ問い合わせるコマンドがあります。通常 VRFY は正規のメールアドレスをスパマーが見つけられないように無効にしていますが、もしそのコマンドが有効な場合、サーバへ問い合わせて完全なユーザ名と有効かどうかを表すステータスコードを受信します。

import smtplib

server = smtplib.SMTP('mail')
server.set_debuglevel(True) # サーバとの通信内容を表示する
try:
    dhellmann_result = server.verify('dhellmann')
    notthere_result = server.verify('notthere')
finally:
    server.quit()

print 'dhellmann:', dhellmann_result
print 'notthere :', notthere_result

結果出力の最後の2行を見ると、メールアドレス dhellmann は有効ですが notthere は存在しません。

$ python smtplib_verify.py
send: 'vrfy <dhellmann>\r\n'
reply: '250 2.1.5 Doug Hellmann <dhellmann@mail.example.com>\r\n'
reply: retcode (250); Msg: 2.1.5 Doug Hellmann <dhellmann@mail.example.com>
send: 'vrfy <notthere>\r\n'
reply: '550 5.1.1 <notthere>... User unknown\r\n'
reply: retcode (550); Msg: 5.1.1 <notthere>... User unknown
send: 'quit\r\n'
reply: '221 2.0.0 mail.example.com closing connection\r\n'
reply: retcode (221); Msg: 2.0.0 mail.example.com closing connection
dhellmann: (250, '2.1.5 Doug Hellmann <dhellmann@mail.example.com>')
notthere : (550, '5.1.1 <notthere>... User unknown')

See also

smtplib
本モジュールの標準ライブラリドキュメント
RFC 821
簡易メール転送プロトコル (SMTP) 仕様
RFC 1869
ベースプロトコルに対する SMTP サービス拡張
RFC 822
“APRA インターネットテキストメッセージフォーマットの標準仕様”
RFC 2822
“インターネットメッセージフォーマット”、メールメッセージフォーマットの更新版
email
メールを構文解析する標準モジュール
smtpd
シンプルな SMTP サーバを実装する
Bookmark and Share