Core のメール送信
VehicleVision.PleasanterTools.Core.Mail は、ツールが通知や自動返信のメールを送るための共通部品です。設定の読み取り、3 つの送信経路、1 通ぶんの形と検証、失敗の分類を持ちます。対象は 0.1.0-preview.9 です。メールの送信待ち(アウトボックス)の保管先は Core.Data 側にあります(共有ストア)。
プレビュー版・未確認の範囲
プレビュー版の間は API が変わり得ます(概要)。Core.Mail の機能は preview.1 から変わっていません(変更履歴)。このページはソースの読み取りに基づきます。SMTP サーバ・Amazon SES・Azure Communication Services に実際に送って確かめた内容ではありません。
設定(MailOptions)
MailOptions.FromConfiguration(configuration, prefix) が、IConfiguration(環境変数など)から設定を読みます。環境変数の接頭辞はアプリごとに決めて渡します。 規約は <ID>_MAIL_(<ID> はアプリ ID の大文字でハイフンは下線)で、たとえばアプリ ID が sample-app なら接頭辞は SAMPLE_APP_MAIL_ です。次の表の名前は、この接頭辞を付けたものを指します。
| 設定名(接頭辞の後) | 既定 | 内容 |
|---|---|---|
ENABLED | 無効 | true で有効。既定は無効で、設定しなければメールは 1 通も出ません。無効なら他の値は読まない(使わない設定の不備で起動を止めない) |
TRANSPORT | Smtp | 送信経路。Smtp / AmazonSes / AzureCommunicationServices |
SMTP_HOST | - | SMTP サーバ(経路が Smtp のとき必須) |
SMTP_PORT | 587 | ポート(1〜65535) |
SMTP_SECURITY | StartTls | StartTls(587 番向け)/ ImplicitTls(465 番向け)/ None(暗号化しない。検証用で、本番では使わない) |
SMTP_USER | 空 | 認証のユーザ名。空なら認証しない |
SMTP_PASSWORD | 空 | 認証のパスワード。ログにも画面にも出さない |
SES_REGION | - | Amazon SES の地域(経路が AmazonSes のとき必須。例 ap-northeast-1) |
ACS_ENDPOINT | - | Azure Communication Services の窓口の URL(経路が AzureCommunicationServices のとき必須。https の絶対 URL) |
FROM_ADDRESS | - | 差出人のアドレス(有効にするとき必須) |
FROM_NAME | 空 | 差出人の表示名 |
REPLYTO_ADDRESS | 空 | 返信先。空なら差出人と同じ |
BASEURL | 空 | メールに載せる URL の起点(例 https://survey.example.jp)。http:// か https:// の絶対 URL |
TIMEOUT_SECONDS | 30 | 1 通あたりの待ち時間の上限(1 以上) |
検証の挙動
- 読めない値を黙って既定へ落としません。
ENABLEDがtrue/falseでない、SMTP_PORTが整数でない、TRANSPORTやSMTP_SECURITYが定義外の値、といった場合はInvalidOperationExceptionで止まります。設定したつもりが効いていない状態を作らないためです。 - 有効にしたのに必須の値が欠けているときも、起動時に止まります(
SMTP_HOST・SES_REGION・ACS_ENDPOINT・FROM_ADDRESSのうち、経路に必要なもの)。 BASEURLがhttp://かhttps://の絶対 URL でなければ、止まります。MailOptions.IsReadyは、有効で、差出人があり、経路に必要な値が揃っているかを返します。送信経路は、IsReadyでなければ「メールの設定が有効になっていない」(再送しても直らない失敗)として送りません。BASEURLは、リンクを載せるメールの組み立てに使う値です。 要求のHostヘッダから URL を組み立てると、偽の宛先を載せたメールをこちらの名前で送らされる(host header injection)ため、設定から取ります。設定が無ければ、リンクを載せるメールは送らない設計です(MailOptions.Linkは起点が無ければnullを返します)。
送信経路
どの経路でも、同じ 1 通が出ます。違うのは資格情報をどこに置くかです。
| 経路 | 資格情報 | 備考 |
|---|---|---|
SMTP(SmtpMailTransport、既定) | SMTP_USER / SMTP_PASSWORD を保管する | 認証付きリレーの 587 番は Azure でブロックされないので、SendGrid・Amazon SES・Azure Communication Services・Microsoft 365 など、どの送信元にも届く。1 通ごとに接続して切る |
Amazon SES(SesMailTransport) | 設定に持たない | 地域だけを設定から取り、資格情報は AWS SDK の既定の探索順(マネージド ID や IAM ロール、環境変数)に任せる。組み立てた生のメールをそのまま渡す |
Azure Communication Services(AcsMailTransport) | 設定に持たない | 窓口の URL だけを設定から取り、Entra ID のマネージド ID(DefaultAzureCredential)で通る。接続文字列は受け取らない。送信の完了を待たず、受け付けられた時点で送れたとする(届かなかったことは ACS 側の記録で追う) |
SES と ACS の経路は、鍵を設定へ書ける口を作らないために加えられたものです(ソースのコメント)。SMTP は、必ずパスワードを 1 つ保管します。
SMTP で既定を 587 番にしているのは、25 番の外向き接続が Azure で塞がれる場合があるためです(サブスクリプションの種別による。ソースのコメント)。
3 つの経路を使い分ける
運用の観点では、次のように選びます(ソースの設計意図に沿った整理で、このマニュアルが推奨を決めるものではありません)。
- 秘密を保管したくなければ、SES か ACS(マネージド ID)。
- 送信元を問わず届かせたければ SMTP。パスワードを環境変数か Key Vault で守る必要があります。
1 通の形(OutgoingMail)と守っていること
OutgoingMail は、宛先・件名・本文と、任意の差出人の表示名・返信先・BCC を持つ record です。
- 本文は平文(
text/plain)だけです。管理者が書いた文面と回答者自身の回答が入るため、HTML にすると逃がし漏れがそのまま差し込みになるからです。 - 1 通 1 宛先です。複数宛先は作りません(誰に届いたかを混ぜない)。
- 件名・宛先・差出人の表示名・返信先・BCC に改行を含められません(
Validateが弾く)。ヘッダの途中で改行を差し込まれると、ヘッダを 1 本増やせてしまう(メールヘッダインジェクション)ためです。 - 全通に
Auto-Submitted: auto-generatedヘッダ(RFC 3834)を付けます。 自動返信に不在通知が返ると無限に往復するため、まともな不在通知がこのヘッダで返信を止められるようにします。 - アドレスの形が壊れているときは、再送しても直らない失敗として扱います(失敗の文に、アドレスは入れません)。
- 差出人の表示名・返信先は、1 通ごとの指定があればそれを使い、無ければ全体設定を使います。BCC は 1 通ごとの指定だけです。
MailMessageFactory が、通信を含まずに MIME メッセージを組み立てます。網が無くても試験できる作りです。
送信待ちへ置くための形(MailPayload・IMailPayloadProtector)
メールの本文には回答者のメールアドレスなど、個人を指す値が入ります。送信待ちの表(共有ストア の MailOutbox)へ置くときは、次の部品を使います。
MailPayload.ToJson/FromJsonは、1 通ぶんを 1 つの文字列(JSON)にまとめます。宛先・件名・本文を別の列に持つと、暗号化の掛け忘れが列ごとに起き得るため、まとめて守る形です。壊れた文字列や宛先が空のものは、例外ではなくnullを返します(呼ぶ側が読めない行をデッドレターへ回します)。IMailPayloadProtectorは、1 通を暗号化して保存する形にする(Protect)・戻す(Unprotect。鍵が違う・壊れている・読めないならnull)ための口です。実装は Core には無く、アプリが用意します(鍵の管理はアプリの責務)。
失敗の分類(MailDeliveryException)
送信経路は、失敗を MailDeliveryException にして投げます。IsTransient が、時間を置けば通る見込みがあるかを表します。送信ワーカーが SMTP の応答コードなどを解釈しなくて済むようにするためです。再送は送信経路ではしません。 「1 回試して、駄目なら理由を付けて投げる」だけで、再送は送信待ちの表とワーカーの役割です。
| 経路 | 一時(再送する) | 恒久(再送しても同じ) |
|---|---|---|
| SMTP | 繋がらない・時間切れ・4xx の一時拒否・手順の不一致 | 5xx の恒久拒否・認証が通らない・TLS を確立できない |
| Amazon SES | 流量の上限(TooManyRequests / LimitExceeded)・5xx・繋がらない・時間切れ | 送信ドメインが未検証・アカウント停止・送信停止・不正な要求・その他の 4xx |
| ACS | 429(流量の上限)・5xx・繋がらない・時間切れ | 401・403(認証)・資格情報を取得できない・その他の 4xx |
停止の合図(CancellationToken による中止)は失敗として扱わず、そのまま投げ直します(次回また拾わせるため)。判定できない失敗は、一時として扱います。
失敗の文に宛先を入れない
MailDeliveryException の文は、デッドレターの行や管理画面に残ります。宛先と本文を入れない作りです。ログにも、宛先・本文・パスワードは出さず、送れたことだけを残します。
関連ページ
- 共有ストア(メール送信箱
MailOutbox) - DB の接続・暗号化・排他(
<ID>_DB_の設定名) - Core の概要とバージョン別の機能