Python smtplibとは?メールを送信する方法

smtplibは、組み立てたメールをSMTPサーバーへ送る標準ライブラリです。

メールの中身を作るのはemail モジュールの役割です。作る係と送る係が分かれていると覚えると混乱しません。


送信の最短コード

with文で開き、send_message()に渡すだけです。

import smtplib
from email.message import EmailMessage

m = EmailMessage()
m["Subject"] = "通知"
m["From"] = "me@example.com"
m["To"] = "you@example.com"
m.set_content("本文")

with smtplib.SMTP("127.0.0.1", 25, timeout=10) as s:
    result = s.send_message(m)

print(result)

実行結果:

{}

空の辞書が返ったら全員に送れたという意味です。ここが直感に反するところで、Trueが返るわけではありません。

一部の宛先だけ拒否された場合は、拒否された宛先が辞書に入って返ります。つまりif result:が真なら失敗した宛先があるということです。


Gmailから送る

GmailのSMTPはポート587のSTARTTLSか、ポート465のSSLを使います。

import smtplib
from email.message import EmailMessage

m = EmailMessage()
m["Subject"] = "テスト"
m["From"] = "you@gmail.com"
m["To"] = "dest@example.com"
m.set_content("本文")

with smtplib.SMTP("smtp.gmail.com", 587, timeout=30) as s:
    s.starttls()                       # ここから暗号化
    s.login("you@gmail.com", "アプリパスワード")
    s.send_message(m)

通常のGoogleアカウントのパスワードでは接続できません。2段階認証を有効にしてから発行するアプリパスワード(16文字)を使います。

SMTP_SSLなら最初から暗号化された接続になり、starttls()は不要です。

with smtplib.SMTP_SSL("smtp.gmail.com", 465, timeout=30) as s:
    s.login("you@gmail.com", "アプリパスワード")
    s.send_message(m)

パスワードをコードに書かない

import os, smtplib

user = os.environ["GMAIL_USER"]
app_pass = os.environ["GMAIL_APP_PASSWORD"]

with smtplib.SMTP("smtp.gmail.com", 587, timeout=30) as s:
    s.starttls()
    s.login(user, app_pass)

環境変数に置けば、ソースコードを共有しても鍵は漏れません。設定ファイルで管理するならPython configparserとは?設定ファイルを読み書きする方法が使えます。


宛先が拒否されたとき

存在しない宛先だけを指定すると例外になります。検証用のSMTPサーバーを立てて、550を返させた結果です。

try:
    with smtplib.SMTP("127.0.0.1", 2526, timeout=10) as s:
        s.send_message(m)
except smtplib.SMTPRecipientsRefused as e:
    print(type(e).__name__)
    print("recipients:", e.recipients)

実行結果:

SMTPRecipientsRefused
recipients: {'ng@example.com': (550, b'No such user here')}

recipientsに宛先ごとの応答コードと理由が入ります。ログに残すならここを見ます。


接続できないとき

try:
    smtplib.SMTP("127.0.0.1", 2, timeout=3)
except Exception as e:
    print(type(e).__name__, "->", e)

実行結果:

ConnectionRefusedError -> [Errno 61] Connection refused

接続の失敗は smtplib の例外ではなく OSError 系で飛んできます。smtplib.SMTPExceptionだけを捕まえていると取り逃がします。


よく出る例外の見分け方

  • SMTPAuthenticationError … ユーザー名かパスワードが違う。Gmailならアプリパスワードを確認
  • SMTPRecipientsRefused … 宛先がすべて拒否された
  • SMTPSenderRefused … 差出人が拒否された。Fromと認証アカウントの不一致が多い
  • SMTPServerDisconnected … 暗号化前に認証した、タイムアウトなど
  • ConnectionRefusedError・socket.timeout … そもそも繋がっていない

暗号化の確認

starttls()は既定でSSLとTLSの違いとは?の検証を行います。中身は次の設定です。

import ssl
ctx = ssl.create_default_context()
print("verify_mode:", ctx.verify_mode)
print("check_hostname:", ctx.check_hostname)

実行結果:

verify_mode: VerifyMode.CERT_REQUIRED
check_hostname: True

証明書の検証を切る設定をネットで見かけても真似しないでください。暗号化している意味がなくなります。


まとめ

  • send_message()が空の辞書を返したら成功
  • Gmailはアプリパスワード。通常のパスワードでは繋がらない
  • 587ならstarttls()、465ならSMTP_SSL
  • 接続失敗はOSError系。SMTPExceptionだけでは足りない

関連記事

あわせて読むと理解が深まります。

対面・無料|東京・永田町で毎週土曜日13時

1回の参加で、プログラミングの構造がわかる

ゼロからはじめるPython入門講座

8年間・累計600回開催/これまで全国で2万人以上が参加

「やはり対面で聞けるのはとても気づきがあり、価値があると思います」

参加者の声

  • 本当に初心者からの講座です。プログラミングに触ったことがなくて大丈夫です
  • 対面だから、その場で聞けます。つまずいた所を持ち帰らずに解決できます
  • 講座は1時間。そのあとも残って学習を続けられ、講師にも質問できます
  • 独学で挫折した方も。糸口が見つかって、講座のあと会員になる方が多くいます

無料で参加する|開催日程を見る

参加費は無料です