ガイド · SMTP Python
プロダクトチームはPythonでSMTPをどのように安全に実装すべきか
PythonでのSMTPは、Webリクエストから直接ではなく、認可されたバックグラウンドのワーカーから実装します。メッセージはEmailMessageで組み立て、暗黙的TLSにはSMTP_SSLを使うか、プロバイダーの現在の仕様で求められる場合はSTARTTLSで明示的にアップグレードし、サーバーサイドの秘密情報で認証し、上限を設けたタイムアウトでsend_messageを呼び出します。接続する前にジョブを保存し、受信者ごとの拒否の証拠を記録し、あいまいな切断を突き合わせ、SMTPでの受け付けを、その後の配信や受信トレイへの到達と区別してください。
SMTPの前に送信を認可して保存する
領収書、セキュリティ通知、要求された確認メール、アカウントに関するお知らせなど、正当なアプリケーションのイベントから始めます。呼び出し元を認証し、テナント、メッセージの種類、表示されるFromのID、受信者、テンプレートのリビジョンを認可します。SMTP接続を開く前に、安定したビジネスイベントのキーを含む永続的な送信ジョブを書き込みます。このキーによって、2つのワーカーが同じ論理的なメッセージをそれぞれ作成してしまうのを防ぎます。ブラウザからの入力で、SMTPのホスト、ポート、ユーザー名、エンベロープ送信者、任意の受信者、ヘッダー、TLSのポリシーを選べてはいけません。これらの値は、レビュー済みのサーバーの設定に保持してください。キューワーカーは1つのジョブを取得し、送信時にサプレッションと認可を再確認し、各試行を記録し、明示的な状態を通じてジョブを解放または確定すべきです。PythonのSMTPライブラリは準備されたメッセージを転送するもので、テナントの認可、同意、冪等性、サプレッションのポリシーを提供するものではありません。
EmailMessageでメッセージを組み立てる
生のヘッダーと本文を連結するのではなく、email.message.EmailMessageを使ってください。アプリケーションで承認されたモデルに従って、From、To、Subject、Date、生成したMessage-IDを設定し、テキストにはset_contentを、必要に応じてHTMLにはadd_alternativeを使います。アドレスオブジェクトを検証し、受信者と添付ファイルの数を制限し、値に含まれる改行インジェクションを拒否し、テンプレートのデータは出力先のコンテキストに合わせてエスケープします。テキストとHTMLの両方を、変更不可の1つのテンプレートのリビジョンから生成します。件名、カスタムヘッダー、ファイル名、診断用のフィールド、ログに、秘密情報や不要な個人データを含めないでください。認証とバウンス処理は異なる送信者IDに依存することがあるため、表示されるFromヘッダーとSMTPのエンベロープ送信者は意図して分けておきます。明確な必要性なしにメッセージ本文全体を保持するのではなく、監査のためにコンテンツのリビジョンやプライバシーに配慮したハッシュを保存してください。
暗黙的TLSかSTARTTLSかを明示的に選ぶ
Pythonのドキュメントでは、最初から暗号化する接続にはSMTP_SSLを、確立済みの接続をアップグレードするにはSMTP.starttlsを使うよう説明しています。一般的なポートの一覧から推測するのではなく、プロバイダーの現在のホスト名、ポート、証明書、サブミッションの仕様に従ってください。検証を行うデフォルトのSSLコンテキストを作成し、証明書やホスト名のチェックを無効にしないでください。STARTTLSの場合は、接続して必要に応じてEHLOを送り、コンテキストを指定してstarttlsを呼び出し、アップグレード後はサーバーが提示する拡張機能が変わることがあるため、もう一度EHLOを送ります。認証情報や顧客のメッセージの内容を平文の接続で送信しないでください。RFC 8314は、TLSで保護されたサブミッションを推奨し、平文でのアクセスを非推奨としています。証明書の失敗、ホスト名の不一致、必須のSTARTTLSがない、予期しない機能の変化などは、黙ってフォールバックするのではなく、調査が必要な致命的な失敗として扱ってください。
SMTPの認証情報を狭い秘密情報の境界内に保つ
ユーザー名とパスワード、またはトークンは、実行時にマネージドなサーバーサイドのシークレット機能から読み込みます。認証情報を、ソースコード、クライアントのバンドル、環境変数のダンプ、URL、例外のトレース、分析ツール、ノートブック、スクリーンショット、プロンプト、コミットされたテストデータに含めないでください。各認証情報は、プロバイダーが対応する最小の環境とワークロードに限定し、開発環境と本番環境を分けます。認証は、必要なTLSの状態が確立されてから行います。管理された受信者でローテーションを実践してください。承認された管理手順で代わりの認証情報を用意し、ワーカーを更新し、認証とイベントのライフサイクル全体を確認してから、古い値を失効させます。認証の失敗が繰り返される場合は、すばやい再試行のループに入るのではなく、該当する経路を一時停止すべきです。Pythonのloginメソッドは、サーバーが通知するメカニズムの中から交渉しますが、プロバイダーが実際に使うメカニズム、アカウントのポリシー、トークンの権限、ローテーションの挙動については、最新の証拠が必要です。
上限を設けたPythonの送信関数を使う
プロバイダーのアダプターは小さく保ち、ジョブの状態機械に構造化された証拠を返します。暗黙的TLSの典型的な流れでは、SSLコンテキストを作成し、SMTP_SSL(host, port, timeout=10)をsmtpとして開き、smtp.login(username, secret)を呼び出してから、smtp.send_message(message, from_addr=envelope_from, to_addrs=recipients)を呼び出します。明示的なアップグレードが必要なプロバイダーでは、タイムアウトを指定したSMTPを使い、ehlo、starttls(context=context)、ehloの順に呼び出してからloginします。例として示したホスト名やポートを、普遍的なデフォルト値として示さないでください。信頼できないヘッダーの解析に頼るのではなく、正規化した受信者のリストを渡します。例外のクラス、SMTPの応答コード、利用可能な場合は長さを制限した診断テキストを記録しますが、アドレス、認証情報、メッセージの内容は伏せてください。運用上の失敗を診断できるよう、接続、TLS、認証、エンベロープ、データ、終了の各フェーズを別々に計測します。
send_messageの受信者ごとの結果を正確に解釈する
Pythonのドキュメントによると、sendmailとsend_messageは、少なくとも1人の受信者についてメールが受け付けられると正常に戻り、拒否された受信者の辞書を返します。空の辞書は、その段階で拒否された受信者がいなかったことを意味します。ジョブ全体を配信済みにするのではなく、この受信者ごとの結果を保持してください。すべての受信者が拒否された場合、ライブラリはSMTPRecipientsRefused例外を送出します。その他の例外によって、送信者の拒否、DATAの拒否、認証、接続、プロトコル、および関連するエラーを区別できます。正確な証拠を、サブミッションサーバーによる受け付け、恒久的な拒否、一時的な拒否、不明、といったアプリケーションの状態に対応づけます。正常に戻ったことが証明するのは、その範囲でのSMTPサブミッションの結果だけです。宛先サーバーによる受け付け、最終的なメールボックスでの振り分け、閲覧、エンゲージメントは立証されません。その後の配信状態通知やプロバイダーのイベントは、別途突き合わせる必要があります。
重複のリスクを制御できる場合にのみ再試行する
次の試行をスケジュールする前に、失敗を分類します。アドレス、送信者、認証、ポリシー、内容に関する恒久的な失敗には、通常、自動的な繰り返しではなく修正やサプレッションが必要です。一時的な4xxの応答は、指数バックオフ、ジッター、試行回数の上限、有効期限、宛先ごとの予算を設けて再試行できます。メッセージのデータを送ったあとの接続リセットやタイムアウトは、あいまいな場合があります。クライアントが最終的な応答を受け取れなかっただけで、サーバーはメッセージを受け付けていた可能性があります。その試行は不明のままにし、プライバシーに配慮した相関づけによってプロバイダーのアクティビティや後から届くイベントを確認し、すぐにやみくもに再送することは避けてください。SMTPには普遍的なアプリケーションの冪等キーがありません。耐久性のあるビジネスイベントのキーはアプリケーションの同時試行を防ぎますが、リモートのSMTPサーバーに、受け付け済みの2つのサブミッションの重複を排除させることはできません。あいまいな結果が繰り返される場合はエスカレーションし、判断に使った正確な証拠を保存してください。
受信者の一部だけの受け付けとサプレッションに対処する
メッセージに複数の受信者がいる場合、SMTPは一部を受け付け、それ以外を拒否することがあります。各受信者の応答を保存し、受け付けられた受信者だけを次の状態に進めます。1つのアドレスが一時的に拒否されたというだけで、元のリスト全体に再送しないでください。恒久的なバウンス、苦情、配信停止、法的な理由、テナント、管理者によるサプレッションを、再試行も含めて各試行の前に適用します。メッセージの種類は、明示的に文書化されたポリシーによってのみ区別してください。メッセージをトランザクションと分類しても、受信者の安全やプロバイダーの制限がなくなるわけではありません。プライバシーと個別の状態管理がコストに見合う場合は、機密性の高いワークフローでは受信者1人ずつのジョブを優先します。ToやCcで受信者のリストを露出させないようにし、Bccの挙動を認可の代わりに使わないでください。SMTPの応答には受信者のアドレスや受信側固有の詳細が含まれることがあるため、診断テキストは長さを制限し、伏せ字にしてください。
管理されたシステムで失敗の経路をテストする
メッセージの組み立て、Unicode、テキストとHTMLの代替パート、添付ファイル、ヘッダーの拒否、受信者の正規化、TLSの検証、STARTTLSがない場合、無効な認証情報、送信者の拒否、一部の受信者の拒否とすべての受信者の拒否、DATAの拒否、受け付けられた可能性のある時点の前後でのタイムアウト、切断、レート制限の応答、再試行の期限切れ、ワーカーの重複、サプレッションの変更、秘密情報のローテーションをテストします。決定論的な単体テストと結合テストには、管理されたテスト用SMTPサービスかローカルのフェイクを使い、下位の環境のトラフィックが誤って顧客のアドレスに送られることが絶対にないようにしてください。本番のカナリアでは、許可された受信者を使い、生のヘッダーで、表示されるFrom、エンベロープの経路、Message-ID、DKIM、SPF、DMARCのアライメント、プロバイダーの証拠を確認します。ログと指標に、認証情報やメッセージ本文が漏れていないことを確認してください。ワーカーがテナントの認可を回避できる、TLSをダウングレードできる、上限なく再試行する、一部の拒否を無視する、送信経路を一時停止できない、といった場合はリリースを見送ってください。
SendHQの位置付け
SendHQは、想定されたプロダクトコミュニケーション向けのワークスペース単位のメールAPIです。ドキュメントでは、送信、検証済みドメイン、配信イベント、サプレッションを扱っています。PythonでSendHQを連携する場合は、文書化されたHTTP APIを使用してください。
よくある質問
PythonではSMTP_SSLとSTARTTLSのどちらを使うべきですか?
プロバイダーの現在のサブミッションの仕様で求められるモードを使ってください。SMTP_SSLは接続の開始時から暗号化します。STARTTLSは明示的にアップグレードするもので、検証済みのTLSと、改めてのEHLOが必要です。
send_messageが正常に戻れば、配信が証明されますか?
いいえ。そのSMTPサブミッションの段階で、少なくとも1人の受信者が受け付けられたという意味です。宛先による受け付け、メールボックスでの振り分け、エンゲージメントには、後から得られる範囲を絞った証拠が必要です。
send_messageが返す辞書は何を意味しますか?
SMTPサーバーに拒否された受信者と、その応答の証拠を対応づけたものです。空の辞書は、その段階で拒否された受信者がいなかったことを意味し、すべてのメッセージが受信トレイに届いたことを意味するものではありません。
タイムアウトはすぐに再試行できますか?
送信が行われた可能性がある後に発生した場合は、安全には再試行できません。その試行はあいまいなものとして保持し、プロバイダーや後から届くイベントの証拠と突き合わせ、重複のリスクに上限を設けたポリシーのもとでのみ再送してください。
SMTPのパスワードはどこに保管すべきですか?
ワークロードと環境ごとにアクセスを絞り、取得が監査され、ローテーションがテスト済みで、クライアント、ログ、プロンプト、テストデータに露出しない、マネージドなサーバーサイドのシークレット機能を使ってください。
本番環境で証明書の検証を無効にしてもよい場合はありますか?
いいえ。証明書やホスト名の失敗は、安全でない設定や誤った設定の証拠です。TLSの検証を黙って弱めるのではなく、その経路を止めて診断してください。
一部の受信者が拒否された場合はどのように処理すべきですか?
受信者ごとの結果を保存し、受け付けられた受信者だけを先に進め、対象となる一時的な拒否だけを再試行します。元のリスト全体を使って、すでに受け付けられた受信者に再送しないでください。
このページはSendHQがSMTPに対応していることを証明していますか?
いいえ。このガイドはPython SMTPを一般的に扱っています。メールAPIについては、SendHQの最新ドキュメントを参照してください。
出典
- smtplib — SMTPプロトコルクライアント — Python Software Foundation
- email.message:電子メールメッセージの表現 — Python Software Foundation
- Pythonのemailの使用例 — Python Software Foundation
- RFC 5321:簡易メール転送プロトコル(SMTP) — RFC Editor
- RFC 4954:認証のためのSMTPサービス拡張 — RFC Editor
- RFC 8314:平文は廃止扱い:メールのサブミッションとアクセスにおけるトランスポート層セキュリティ(TLS)の使用 — RFC Editor