- 設計
メール送信の設計とメール設計書|通知メール一覧テンプレート
メール送信の設計とは、システムが送るメールを1通ずつ洗い出し、「いつ・誰に・どんな件名と文面で・どの方式で送り、届かなかったらどうするか」を一覧表と文面テンプレートに書き切る作業です。その成果物がメール設計書で、中心になるのはメール一覧表、文面テンプレート、差し込み変数の定義表の3つです。
この記事では、架空の「備品予約システム」を題材に、そのまま書き換えて使えるメール一覧表と、通知メール10種の件名・文面テンプレートを全文で載せます。テンプレートは Python の string.Template で実際に値を差し込み、変数の過不足を検査した結果も付けています。
確認環境:Ubuntu 24.04 / Python 3.11.15(string.Template、email、sqlite3 は標準ライブラリ。SQLite 3.45.1)、PostgreSQL 16.15。確認日 2026-10-05。
この記事の結論
- メール設計書は、メール一覧表(何を送るか)・文面テンプレート(何と書くか)・差し込み変数の定義表(何を埋めるか)の3点で構成します。
- 送信方式は、画面の処理とメール送信を切り離す「キュー経由の非同期送信」を基本にし、再送の回数・間隔・上限後の扱いをメールごとに決めます。
- 二重送信は、「メールID+出来事を表すキー」を冪等キーにして一意制約で防ぎます。
- 送信ドメインの認証(SPF・DKIM・DMARC)は、アプリを作る前の設計段階で確認項目に入れます。
- 通知メールと一斉配信メールは、同意の要否・配信停止・送信経路が違うので、設計書も送信の仕組みも分けます。
メール設計書に書く項目の早見表
| 成果物 | 書く項目 | 決める人の目安 | この記事の該当箇所 |
|---|---|---|---|
| メール一覧表 | メールID、メール名、契機、送信タイミング、宛先、件名、差し込み項目、送信方式、再送と上限後の扱い | 設計担当 | メール一覧表テンプレート |
| 文面テンプレート | 件名、本文、共通フッター | 設計担当と利用部門 | 件名と文面テンプレート10種 |
| 差し込み変数の定義表 | 変数名、意味、型・書式、例、取得元、値が空のときの扱い | 設計担当 | 差し込み変数の定義表 |
| 送信方式の設計 | 同期か非同期か、キューの置き場所、優先度 | 設計担当と基盤担当 | 同期送信と非同期送信の違い |
| 再送の設計 | 再送する条件、回数、間隔、上限後の扱い | 設計担当と運用担当 | 再送の回数と間隔 |
| 二重送信の防止 | 冪等キーの作り方、一意制約 | 設計担当 | 二重送信を防ぐ冪等キー |
| 送信ドメインの設定 | 差出人アドレス、SPF、DKIM、DMARC | 基盤担当 | SPF・DKIM・DMARC の確認項目 |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「メール送信の設計とメール設計書|通知メール一覧テンプレート」
メール一覧表テンプレート(記入例10件)
メール一覧表は、1行が1種類のメールです。画面設計や機能一覧から「メールを送る」と書かれた箇所を拾い、漏れなく行にします。列は次の9つを基本にします。
| 列 | 書くこと | 書き方のポイント |
|---|---|---|
| メールID | M-001 のような通し番号 | テンプレートのファイル名・ログ・冪等キーで同じIDを使う |
| メール名 | 何のメールかが分かる名前 | 「〜の確認」「〜の通知」のように用件で付ける |
| 契機 | 送るきっかけになる操作や処理 | 画面の操作か、バッチかを書き分ける |
| 送信タイミング | 即時か定時か、目標時間 | 「即時(目標1分以内)」「前日 9:00」のように数字で書く |
| 宛先 | To、CC、BCC | 役割(申請者、承認者)で書き、アドレスの取得元は変数の定義表に書く |
| 件名 | 件名のテンプレート | 差し込み後の最長の長さも確かめる |
| 差し込み項目 | 使う変数の一覧 | 定義表の変数名と一致させる |
| 送信方式 | 同期、非同期(キュー)、バッチ | 理由が特別なときは備考に書く |
| 再送と上限後の扱い | 再送の回数、上限に達したあとの対応 | 「記録のみ」「運用担当へ通知」「画面から再発行」など |
記入例です。件名と差し込み項目は、後述のテンプレートから機械的に取り出しています。
| メールID | メール名 | 契機 | 送信タイミング | 宛先 | 件名 | 差し込み項目 | 送信方式 | 再送と上限後の扱い |
|---|---|---|---|---|---|---|---|---|
| M-001 | 会員登録の確認 | 会員登録フォームの送信 | 即時(目標1分以内) | 登録者 | 【備品予約】会員登録のご確認(${expire_hours}時間以内にお手続きください) |
expire_hours, user_name, confirm_url, expire_at |
非同期(キュー) | 再送5回。届かないときは画面の「再送する」で再発行 |
| M-002 | 会員登録の完了 | 確認URLへのアクセス | 即時 | 登録者 | 【備品予約】会員登録が完了しました |
user_name, login_url, login_id |
非同期(キュー) | 再送5回。上限後は記録のみ(画面で完了が分かるため) |
| M-003 | パスワード再設定の案内 | 再設定フォームの送信 | 即時(目標1分以内) | 申請した会員 | 【備品予約】パスワード再設定のご案内(${expire_minutes}分以内) |
expire_minutes, user_name, reset_url, expire_at |
非同期(キュー・優先) | 再送3回。有効期限が短いため上限後は破棄し、画面から再申請 |
| M-004 | パスワード変更の完了 | パスワードの変更 | 即時 | 変更した会員 | 【備品予約】パスワードを変更しました |
user_name, changed_at, reset_form_url |
非同期(キュー) | 再送5回。上限後は運用担当へ通知 |
| M-005 | 予約申請の受付 | 予約申請の登録 | 即時 | 申請者 | 【備品予約】申請を受け付けました(${apply_no} ${item_name}) |
apply_no, item_name, user_name, use_from, use_to, approver_name, apply_url |
非同期(キュー) | 再送5回。上限後は記録のみ |
| M-006 | 承認の依頼 | 予約申請の登録 | 即時 | 承認者 | 【備品予約】承認のお願い(${apply_no} ${applicant_name})期限 ${approve_due} |
apply_no, applicant_name, approve_due, approver_name, item_name, use_from, use_to, purpose, apply_url |
非同期(キュー) | 再送5回。上限後は運用担当へ通知し、代理承認者へ切り替え |
| M-007 | 承認の通知 | 承認者による承認 | 即時 | 申請者 | 【備品予約】申請が承認されました(${apply_no} ${item_name}) |
apply_no, item_name, user_name, use_from, use_to, pickup_place, apply_url |
非同期(キュー) | 再送5回。上限後は記録のみ(画面で状態が分かるため) |
| M-008 | 差し戻しの通知 | 承認者による差し戻し | 即時 | 申請者 | 【備品予約】申請が差し戻されました(${apply_no})内容をご確認ください |
apply_no, user_name, item_name, reject_reason, apply_url |
非同期(キュー) | 再送5回。上限後は運用担当へ通知 |
| M-009 | 利用前日のお知らせ | 日次バッチ(前日 9:00) | 定時(利用日の前日 9:00) | 予約者 | 【備品予約】明日 ${use_from} から ${item_name} のご利用です |
use_from, item_name, user_name, apply_no, use_to, pickup_place, apply_url |
非同期(バッチが一括で登録) | 再送3回。当日 0:00 を過ぎたら送らない |
| M-010 | 返却期限のお知らせ | 日次バッチ(返却期限の翌日 9:00) | 定時(期限の翌日 9:00。未返却の間は毎日) | 予約者(CC:承認者) | 【備品予約】${item_name} の返却をお願いします(期限 ${use_to}) |
item_name, use_to, user_name, apply_no, pickup_place, apply_url |
非同期(バッチが一括で登録) | 再送3回。上限後は運用担当へ通知 |
表計算ソフトに貼るときは、次のタブ区切りを使ってください。
メールID メール名 契機 送信タイミング 宛先 件名 差し込み項目 送信方式 再送と上限後の扱い
M-001 会員登録の確認 会員登録フォームの送信 即時(目標1分以内) 登録者 【備品予約】会員登録のご確認(${expire_hours}時間以内にお手続きください) expire_hours, user_name, confirm_url, expire_at 非同期(キュー) 再送5回。届かないときは画面の「再送する」で再発行
M-002 会員登録の完了 確認URLへのアクセス 即時 登録者 【備品予約】会員登録が完了しました user_name, login_url, login_id 非同期(キュー) 再送5回。上限後は記録のみ(画面で完了が分かるため)
M-003 パスワード再設定の案内 再設定フォームの送信 即時(目標1分以内) 申請した会員 【備品予約】パスワード再設定のご案内(${expire_minutes}分以内) expire_minutes, user_name, reset_url, expire_at 非同期(キュー・優先) 再送3回。有効期限が短いため上限後は破棄し、画面から再申請
M-004 パスワード変更の完了 パスワードの変更 即時 変更した会員 【備品予約】パスワードを変更しました user_name, changed_at, reset_form_url 非同期(キュー) 再送5回。上限後は運用担当へ通知
M-005 予約申請の受付 予約申請の登録 即時 申請者 【備品予約】申請を受け付けました(${apply_no} ${item_name}) apply_no, item_name, user_name, use_from, use_to, approver_name, apply_url 非同期(キュー) 再送5回。上限後は記録のみ
M-006 承認の依頼 予約申請の登録 即時 承認者 【備品予約】承認のお願い(${apply_no} ${applicant_name})期限 ${approve_due} apply_no, applicant_name, approve_due, approver_name, item_name, use_from, use_to, purpose, apply_url 非同期(キュー) 再送5回。上限後は運用担当へ通知し、代理承認者へ切り替え
M-007 承認の通知 承認者による承認 即時 申請者 【備品予約】申請が承認されました(${apply_no} ${item_name}) apply_no, item_name, user_name, use_from, use_to, pickup_place, apply_url 非同期(キュー) 再送5回。上限後は記録のみ(画面で状態が分かるため)
M-008 差し戻しの通知 承認者による差し戻し 即時 申請者 【備品予約】申請が差し戻されました(${apply_no})内容をご確認ください apply_no, user_name, item_name, reject_reason, apply_url 非同期(キュー) 再送5回。上限後は運用担当へ通知
M-009 利用前日のお知らせ 日次バッチ(前日 9:00) 定時(利用日の前日 9:00) 予約者 【備品予約】明日 ${use_from} から ${item_name} のご利用です use_from, item_name, user_name, apply_no, use_to, pickup_place, apply_url 非同期(バッチが一括で登録) 再送3回。当日 0:00 を過ぎたら送らない
M-010 返却期限のお知らせ 日次バッチ(返却期限の翌日 9:00) 定時(期限の翌日 9:00。未返却の間は毎日) 予約者(CC:承認者) 【備品予約】${item_name} の返却をお願いします(期限 ${use_to}) item_name, use_to, user_name, apply_no, pickup_place, apply_url 非同期(バッチが一括で登録) 再送3回。上限後は運用担当へ通知
件名と文面テンプレート10種
文面の共通ルール
| 項目 | ルール | 例 |
|---|---|---|
| 件名の先頭 | 【サービス名】を付ける | 【備品予約】 |
| 件名の内容 | 用件と、相手にしてほしいことを書く | 承認のお願い、内容をご確認ください |
| 件名の期限 | 期限のあるメールは件名にも期限を入れる | 30分以内、期限 10/7(水) |
| 本文の1行目 | 宛名(氏名+様) | 山田 太郎 様 |
| 本文の2〜3行目 | 何が起きたか、何をしてほしいか | 予約申請が差し戻されました。内容を修正して… |
| 日時 | 「明日」だけにせず日付と曜日を添える | 10/8(木)13:00 |
| URL | 1通につき行動用のURLは1つ。前後に空行を入れる | 申請の確認 |
| 心当たりがない場合 | 本人の操作で送るメールには対応を書く | 何もせずに破棄してください |
| フッター | サービス名、URL、送信専用であること、問い合わせ先 | 下の共通フッター |
件名は差し込み後の長さも確かめます。今回の10種に例の値を入れたところ、最短17字、最長48字でした(後述の検査スクリプトの出力)。備品名のように長さが変わる値を件名に入れるときは、用件を前に、変わる値を後ろに置くと、受信一覧で後ろが省略されても用件が読めます。
共通フッターは、全メールの本文の末尾に付けます。
--
備品予約システム
${service_url}
このメールは送信専用です。返信いただいても確認できません。
お問い合わせ:${support_url}
以下の10種は、${変数名} の部分を差し込む前提のテンプレートです。サービス名と変数を自分のシステムに合わせて書き換えて使えます。
M-001 会員登録の確認
件名:【備品予約】会員登録のご確認(${expire_hours}時間以内にお手続きください)
${user_name} 様
備品予約システムへの会員登録を受け付けました。
次のURLを開くと、登録が完了します。
${confirm_url}
有効期限:${expire_at}(発行から${expire_hours}時間)
期限を過ぎた場合は、登録フォームからもう一度お手続きください。
このメールに心当たりがない場合は、何もせずにこのメールを破棄してください。
M-002 会員登録の完了
件名:【備品予約】会員登録が完了しました
${user_name} 様
会員登録が完了しました。
次のURLからログインして、備品を予約できます。
${login_url}
ログインID:${login_id}
M-003 パスワード再設定の案内
件名:【備品予約】パスワード再設定のご案内(${expire_minutes}分以内)
${user_name} 様
パスワード再設定の申請を受け付けました。
次のURLを開いて、新しいパスワードを設定してください。
${reset_url}
有効期限:${expire_at}(発行から${expire_minutes}分)
このメールに心当たりがない場合は、何もせずにこのメールを破棄してください。
パスワードは変更されません。
M-004 パスワード変更の完了
件名:【備品予約】パスワードを変更しました
${user_name} 様
${changed_at} にパスワードを変更しました。
ご自身で変更した場合、対応は不要です。
心当たりがない場合は、次のURLからパスワードを再設定してください。
${reset_form_url}
M-005 予約申請の受付
件名:【備品予約】申請を受け付けました(${apply_no} ${item_name})
${user_name} 様
次の内容で予約申請を受け付けました。
承認されると、あらためてメールでお知らせします。
申請番号:${apply_no}
備品:${item_name}
利用期間:${use_from} ~ ${use_to}
承認者:${approver_name}
申請内容の確認・取り消し
${apply_url}
M-006 承認の依頼
件名:【備品予約】承認のお願い(${apply_no} ${applicant_name})期限 ${approve_due}
${approver_name} 様
${applicant_name} さんから予約申請が届いています。
${approve_due} までに、承認または差し戻しをお願いします。
申請番号:${apply_no}
備品:${item_name}
利用期間:${use_from} ~ ${use_to}
利用目的:${purpose}
申請の確認
${apply_url}
M-007 承認の通知
件名:【備品予約】申請が承認されました(${apply_no} ${item_name})
${user_name} 様
予約申請が承認されました。
申請番号:${apply_no}
備品:${item_name}
利用期間:${use_from} ~ ${use_to}
受け取り場所:${pickup_place}
予約の確認
${apply_url}
M-008 差し戻しの通知
件名:【備品予約】申請が差し戻されました(${apply_no})内容をご確認ください
${user_name} 様
予約申請が差し戻されました。
内容を修正して、もう一度申請してください。
申請番号:${apply_no}
備品:${item_name}
差し戻しの理由:
${reject_reason}
申請の修正
${apply_url}
M-009 利用前日のお知らせ
件名:【備品予約】明日 ${use_from} から ${item_name} のご利用です
${user_name} 様
明日のご予約をお知らせします。
申請番号:${apply_no}
備品:${item_name}
利用期間:${use_from} ~ ${use_to}
受け取り場所:${pickup_place}
予定が変わった場合は、次のURLから取り消しをお願いします。
${apply_url}
M-010 返却期限のお知らせ
件名:【備品予約】${item_name} の返却をお願いします(期限 ${use_to})
${user_name} 様
次の備品の返却期限を過ぎています。
お手数ですが、返却の手続きをお願いします。
申請番号:${apply_no}
備品:${item_name}
返却期限:${use_to}
返却場所:${pickup_place}
すでに返却済みの場合は、行き違いですのでご容赦ください。
返却の手続き
${apply_url}
差し込み変数の定義表
差し込み変数は、メールごとではなくシステム全体で1つの表にまとめます。同じ意味の値に別の名前が付くのを防げます。「値が空のとき」の列は見落としやすいので必ず埋めます。URLや番号のように欠けると用をなさない値は「送信しない」、氏名のように代わりの言葉で通じる値は代わりの表示を決めます。
| 変数名 | 意味 | 型・書式 | 例 | 取得元 | 値が空のとき |
|---|---|---|---|---|---|
user_name |
宛先の氏名 | 文字列(最大50字) | 山田 太郎 | 会員.氏名 | 「ご利用者」と表示 |
login_id |
ログインID | 文字列 | taro@example.com | 会員.メールアドレス | 送信しない |
confirm_url |
登録確認URL | URL(1回限りのトークン付き) | https://reserve.example.com/confirm?token=3f9a |
登録トークン | 送信しない |
login_url |
ログイン画面のURL | URL | https://reserve.example.com/login |
設定値 | 送信しない |
reset_url |
パスワード再設定URL | URL(1回限りのトークン付き) | https://reserve.example.com/reset?token=7c21 |
再設定トークン | 送信しない |
reset_form_url |
再設定の申請画面のURL | URL | https://reserve.example.com/reset |
設定値 | 送信しない |
expire_at |
有効期限 | 日時 YYYY-MM-DD HH:MM(日本時間) | 2026-10-06 10:00 | トークン.有効期限 | 送信しない |
expire_hours |
有効期間(時間) | 整数 | 24 | 設定値 | 送信しない |
expire_minutes |
有効期間(分) | 整数 | 30 | 設定値 | 送信しない |
changed_at |
変更した日時 | 日時 YYYY-MM-DD HH:MM(日本時間) | 2026-10-05 14:32 | 会員.パスワード更新日時 | 送信しない |
apply_no |
申請番号 | 文字列 A-9999 | A-0042 | 申請.申請番号 | 送信しない |
item_name |
備品名 | 文字列(最大40字) | プロジェクター(大会議室用) | 備品.名称 | 送信しない |
use_from |
利用開始 | 日時 M/D(曜)HH:MM | 10/8(木)13:00 | 申請.利用開始日時 | 送信しない |
use_to |
利用終了(返却期限) | 日時 M/D(曜)HH:MM | 10/8(木)17:00 | 申請.利用終了日時 | 送信しない |
applicant_name |
申請者の氏名 | 文字列 | 山田 太郎 | 会員.氏名 | 送信しない |
approver_name |
承認者の氏名 | 文字列 | 佐藤 花子 | 会員.氏名(承認者) | 「承認者」と表示 |
approve_due |
承認の期限 | 日付 M/D(曜) | 10/7(水) | 申請.利用開始日の前日 | 送信しない |
purpose |
利用目的 | 文字列(最大200字、改行あり) | 部内の勉強会 | 申請.利用目的 | 「(記入なし)」と表示 |
pickup_place |
受け取り・返却の場所 | 文字列 | 3階 総務カウンター | 備品.保管場所 | 「総務カウンター」と表示 |
reject_reason |
差し戻しの理由 | 文字列(最大500字、改行あり) | 利用時間が別の予約と重なっています。15:00 以降でご検討ください。 | 承認履歴.コメント | 「(理由の記入はありません)」と表示 |
apply_url |
申請の詳細画面のURL | URL | https://reserve.example.com/applies/A-0042 |
申請.申請番号から組み立て | 送信しない |
service_url |
サービスのトップURL | URL | https://reserve.example.com/ |
設定値 | 送信しない |
support_url |
問い合わせ先のURL | URL | https://reserve.example.com/support |
設定値 | 送信しない |
日時は、表示用の書式(曜日を付けるか、年を付けるか)とタイムゾーンを定義表で固定します。同じ「利用開始」でもメールごとに書式が変わると、読み手が見比べにくくなります。
Python の string.Template で差し込みを確かめる
テンプレートの ${変数名} という書き方は、Python 標準ライブラリの string.Template でそのまま差し込めます。設計段階で文面を動かしてみると、変数の抜けや件名の長さにすぐ気づけます。
基本:件名と本文に値を差し込む
from string import Template
# メール一覧表の1行ぶん(メールID、件名、本文、差し込み変数の定義)
MAILS = {
"M-001": {
"subject": "【備品予約】会員登録のご確認(${expire_hours}時間以内に手続きをお願いします)",
"body": (
"${user_name} 様\n\n"
"備品予約システムへの会員登録を受け付けました。\n"
"次のURLを開いて、登録を完了してください。\n\n"
"${confirm_url}\n\n"
"有効期限:${expire_at}(${expire_hours}時間)\n"
"料金の目安:1回 $$0(無料)\n"
),
"vars": {"user_name", "confirm_url", "expire_at", "expire_hours"},
},
}
def render(mail_id: str, values: dict) -> tuple[str, str]:
mail = MAILS[mail_id]
subject = Template(mail["subject"]).substitute(values)
body = Template(mail["body"]).substitute(values)
return subject, body
values = {
"user_name": "山田 太郎",
"confirm_url": "https://reserve.example.com/confirm?token=3f9a",
"expire_at": "2026-10-06 10:00",
"expire_hours": 24,
}
subject, body = render("M-001", values)
print("件名:", subject)
print("----")
print(body)
実行結果です。
件名: 【備品予約】会員登録のご確認(24時間以内に手続きをお願いします)
----
山田 太郎 様
備品予約システムへの会員登録を受け付けました。
次のURLを開いて、登録を完了してください。
https://reserve.example.com/confirm?token=3f9a
有効期限:2026-10-06 10:00(24時間)
料金の目安:1回 $0(無料)
$ そのものを文面に書きたいときは $$ と書きます(上の「$$0」が「$0」になっています)。
値が足りないとき・書き方を誤ったときの動き
from string import Template
subject = Template("【備品予約】${item_name}の予約を受け付けました")
body = Template("${user_name} 様\n予約番号:${reserve_no}\n利用日:${use_date}\n")
# 1) 値が足りないとき substitute は例外で止まる
try:
body.substitute(user_name="山田 太郎", reserve_no="R-000123")
except KeyError as e:
print("KeyError:", e)
# 2) safe_substitute は止まらず、プレースホルダーをそのまま残す
print(body.safe_substitute(user_name="山田 太郎", reserve_no="R-000123"))
# 3) 送信前の検査:テンプレート内の変数名と、設計書の定義を突き合わせる
defined = {"user_name", "reserve_no", "use_date", "item_name"}
used = set(subject.get_identifiers()) | set(body.get_identifiers())
print("テンプレートで使う変数:", sorted(used))
print("定義に無い変数:", sorted(used - defined))
print("定義だけにある変数:", sorted(defined - used))
# 4) 書き方の誤りを見つける
bad = Template("合計 $ 円、担当 ${staff name}")
print("is_valid:", bad.is_valid())
try:
bad.substitute(staff="佐藤")
except ValueError as e:
print("ValueError:", e)
# 5) 利用者が入力した値に $ が含まれていても、再度の置き換えは起きない
t = Template("件名:${title}")
print(t.substitute(title="${user_name} の $100 クーポン"))
実行結果です。
KeyError: 'use_date'
山田 太郎 様
予約番号:R-000123
利用日:${use_date}
テンプレートで使う変数: ['item_name', 'reserve_no', 'use_date', 'user_name']
定義に無い変数: []
定義だけにある変数: []
is_valid: False
ValueError: Invalid placeholder in string: line 1, col 4
件名:${user_name} の $100 クーポン
| メソッド | 値が足りないとき | 向いている場面 |
|---|---|---|
| substitute | KeyError で止まる | 送信処理。穴の空いたメールを送らずに済む |
| safe_substitute | プレースホルダーを残して続行 | 文面のプレビュー、レビュー用の出力 |
| get_identifiers | テンプレート内の変数名を一覧で返す | 設計書の定義表との突き合わせ |
| is_valid | 書き方の誤りを True / False で返す | テンプレート登録時の検査 |
get_identifiers と is_valid は Python 3.11 で追加されたメソッドです。
10種のテンプレートをまとめて検査する
この記事の10種は、メールID・件名・本文を並べた MAILS と、変数名をキーにした定義表 VARS を mails.py に書き、次のスクリプトで検査しました。mails.py の中身は、上の文面テンプレートと定義表をそのまま Python のリストと辞書にしたものです。
"""メール一覧(mails.py)を検査し、全メールを例の値で組み立てる。"""
from string import Template
from mails import MAILS, VARS, FOOTER
sample = {name: spec[2] for name, spec in VARS.items()}
problems = 0
used_all = set()
for (mail_id, name, *_rest, subject, body) in MAILS:
s, b = Template(subject), Template(body + FOOTER)
if not (s.is_valid() and b.is_valid()):
print(mail_id, "書き方に誤りがあります"); problems += 1
used = set(s.get_identifiers()) | set(b.get_identifiers())
used_all |= used
undefined = used - VARS.keys()
if undefined:
print(mail_id, "定義に無い変数:", sorted(undefined)); problems += 1
rendered = s.substitute(sample)
b.substitute(sample)
print(f"{mail_id} {name}: 変数{len(used):>2}個 件名{len(rendered):>2}字 → {rendered}")
print("使われていない定義:", sorted(VARS.keys() - used_all))
print("メール数:", len(MAILS), "/ 変数の定義数:", len(VARS), "/ 指摘:", problems)
実行結果です。
M-001 会員登録の確認: 変数 6個 件名31字 → 【備品予約】会員登録のご確認(24時間以内にお手続きください)
M-002 会員登録の完了: 変数 5個 件名17字 → 【備品予約】会員登録が完了しました
M-003 パスワード再設定の案内: 変数 6個 件名25字 → 【備品予約】パスワード再設定のご案内(30分以内)
M-004 パスワード変更の完了: 変数 5個 件名18字 → 【備品予約】パスワードを変更しました
M-005 予約申請の受付: 変数 9個 件名39字 → 【備品予約】申請を受け付けました(A-0042 プロジェクター(大会議室用))
M-006 承認の依頼: 変数11個 件名36字 → 【備品予約】承認のお願い(A-0042 山田 太郎)期限 10/7(水)
M-007 承認の通知: 変数 9個 件名39字 → 【備品予約】申請が承認されました(A-0042 プロジェクター(大会議室用))
M-008 差し戻しの通知: 変数 7個 件名35字 → 【備品予約】申請が差し戻されました(A-0042)内容をご確認ください
M-009 利用前日のお知らせ: 変数 9個 件名46字 → 【備品予約】明日 10/8(木)13:00 から プロジェクター(大会議室用) のご利用です
M-010 返却期限のお知らせ: 変数 8個 件名48字 → 【備品予約】プロジェクター(大会議室用) の返却をお願いします(期限 10/8(木)17:00)
使われていない定義: []
メール数: 10 / 変数の定義数: 23 / 指摘: 0
M-006 を例の値で組み立てた全文は次のとおりです。
件名:【備品予約】承認のお願い(A-0042 山田 太郎)期限 10/7(水)
佐藤 花子 様
山田 太郎 さんから予約申請が届いています。
10/7(水) までに、承認または差し戻しをお願いします。
申請番号:A-0042
備品:プロジェクター(大会議室用)
利用期間:10/8(木)13:00 ~ 10/8(木)17:00
利用目的:部内の勉強会
申請の確認
https://reserve.example.com/applies/A-0042
--
備品予約システム
https://reserve.example.com/
このメールは送信専用です。返信いただいても確認できません。
お問い合わせ:https://reserve.example.com/support
件名と差出人の日本語はヘッダーで符号化される
日本語の件名や差出人名は、メールのヘッダーでは符号化された形になります。標準ライブラリの email で組み立てると自動で行われます。
from email.message import EmailMessage
from email.utils import formataddr
msg = EmailMessage()
msg["From"] = formataddr(("備品予約システム", "no-reply@reserve.example.com"))
msg["To"] = "taro@example.com"
msg["Subject"] = "【備品予約】会員登録のご確認"
msg.set_content("山田 太郎 様\n\n会員登録を受け付けました。\n")
print(msg.as_string())
実行結果です。
From:
=?utf-8?b?5YKZ5ZOB5LqI57SE44K344K544OG44Og?= <no-reply@reserve.example.com>
To: taro@example.com
Subject: =?utf-8?b?44CQ5YKZ5ZOB5LqI57SE44CR5Lya5ZOh55m76Yyy44Gu44GU56K66KqN?=
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: base64
MIME-Version: 1.0
5bGx55SwIOWkqumDjiDmp5gKCuS8muWToeeZu+mMsuOCkuWPl+OBkeS7mOOBkeOBvuOBl+OBn+OA
ggo=
Subject の行が、設計書に書いた件名とは違う見た目になっています。送信ログやメールサーバーのログで件名を探すときは、この形で記録されていることがあります。ログから特定のメールを追えるよう、メールIDと冪等キーを別の項目としてログに残す設計にしておくと探しやすくなります。
同期送信と非同期送信の違い
同期送信は、画面の処理の中でメールサーバーへ送り終えるまで待つ方式です。非同期送信は、画面の処理では「送信依頼」を表やキューに積むだけにして、別のプロセスが取り出して送る方式です。
| 観点 | 同期送信 | 非同期送信(キュー経由) |
|---|---|---|
| 画面の応答 | メールサーバーの応答を待つ | 送信依頼を積んだらすぐ返る |
| メールサーバーが応答しないとき | 画面の処理も待たされる、またはエラーになる | 依頼は残り、あとで再送できる |
| 業務データとの整合 | 登録は取り消されたのにメールは送られた、が起こり得る | 送信依頼を業務データと同じトランザクションで登録すれば揃う |
| 再送 | 利用者にやり直してもらう | 仕組みで自動的に再送できる |
| 送信までの時間 | 最短 | キューの取り出し間隔ぶん遅れる |
| 作るもの | 少ない | 送信依頼の表(またはキュー)と送信プロセス |
| 状況の確認 | アプリのログ | 送信依頼の状態(待ち・送信済み・再送待ち・停止)で追える |
選ぶときの判断基準です。
| 条件 | 選ぶ方式 |
|---|---|
| 画面の操作をきっかけに送る通知メール全般 | 非同期(キュー) |
| 登録とメールを必ず揃えたい(承認依頼、受付の通知) | 非同期(送信依頼を業務データと同じトランザクションで登録) |
| 有効期限が短い(パスワード再設定) | 非同期(優先度を上げたキュー) |
| 夜間や定時にまとめて送る(前日のお知らせ) | バッチが送信依頼を一括で登録し、送信は同じキューを使う |
| 社内の小さなツールで、送れなければ画面にエラーを出せば足りる | 同期でも可。タイムアウトを短く設定する |
非同期送信の処理の流れは次のとおりです。
- 利用者が申請を登録する。
- アプリが、申請と送信依頼を同じトランザクションで DB に登録する。
- アプリが完了画面を返す(メールの送信は待たない)。
- 送信プロセスが、待ちの送信依頼を取り出す。
- 送信プロセスがテンプレートに値を差し込み、メールサーバーへ送る。
- 応答に応じて、送信依頼の状態を「送信済み」または「再送待ち」に更新する。
再送の回数と間隔
再送は、再送してよい場合とそうでない場合を分けることから決めます。SMTP の応答コードは、RFC 5321 で 4 から始まるものが一時的な不成立、5 から始まるものが恒久的な不成立と定められています。
| 起きたこと | 再送 | 対応 |
|---|---|---|
| メールサーバーへ接続できない、タイムアウト | する | 間隔を空けて再送 |
| 応答コードが 4 から始まる(一時的) | する | 間隔を空けて再送 |
| 応答コードが 5 から始まる(恒久的) | しない | 状態を「停止」にして、宛先の誤りなどを確認 |
| テンプレートの変数が足りない(KeyError) | しない | 設計・データの問題なので運用担当へ通知 |
| 有効期限を過ぎた | しない | 破棄し、画面からの再申請に任せる |
間隔は、回を重ねるごとに広げます。1回目を1分後、以降4倍ずつにした例を計算します。
from datetime import datetime, timedelta
BASE_SECONDS = 60 # 1回目の再送までの間隔
FACTOR = 4 # 回を重ねるごとに何倍にするか
MAX_RETRY = 5 # 再送の上限回数
first = datetime(2026, 10, 5, 10, 0, 0)
t = first
print("回 間隔 実行時刻 初回からの経過")
print(f"初回 - {t:%Y-%m-%d %H:%M} 0:00:00")
for n in range(1, MAX_RETRY + 1):
wait = BASE_SECONDS * FACTOR ** (n - 1)
t += timedelta(seconds=wait)
print(f"再送{n} {str(timedelta(seconds=wait)):>8} {t:%Y-%m-%d %H:%M} {t - first}")
実行結果です。
回 間隔 実行時刻 初回からの経過
初回 - 2026-10-05 10:00 0:00:00
再送1 0:01:00 2026-10-05 10:01 0:01:00
再送2 0:04:00 2026-10-05 10:05 0:05:00
再送3 0:16:00 2026-10-05 10:21 0:21:00
再送4 1:04:00 2026-10-05 11:25 1:25:00
再送5 4:16:00 2026-10-05 15:41 5:41:00
5回の再送で、初回から5時間41分後まで試すことになります。この数字とメールの性質を見比べて回数を決めます。
| メールの性質 | 回数と間隔の例 | 理由 |
|---|---|---|
| 有効期限が短い(パスワード再設定:30分) | 3回(1分・4分・16分後) | 3回目が初回から21分後。期限内に届く範囲で止める |
| 相手の行動が必要(承認依頼、差し戻し) | 5回 | 届かないと業務が止まるので長めに試し、上限後は運用担当へ通知 |
| 画面でも分かる(登録完了、承認の通知) | 5回 | 上限後は記録のみでよい |
| 定時のお知らせ(前日のお知らせ) | 3回 | 当日になったら意味がなくなるので、送らない時刻を決める |
上の回数と間隔は一例です。アプリからメールサーバーへ渡したあとは、メールサーバー自身も相手先へ再送を試みます。RFC 5321 は、この再送について少なくとも30分の間隔を空けることを推奨しています。アプリ側の再送は「メールサーバーへ渡すまで」を対象にし、渡したあとの不達は、メールサーバーや配信サービスの不達通知で把握する、と役割を分けて設計書に書きます。
二重送信を防ぐ冪等キー
冪等キーとは、同じ処理が2回動いても結果が1回ぶんになるように付ける目印です。ボタンの二度押し、タイムアウト後の再実行、バッチの再実行で、同じメールが2通届くのを防ぎます。
キーは「メールID+そのメールを送る理由になった出来事」で作ります。
| メール | 冪等キーの例 | 考え方 |
|---|---|---|
| 承認の通知 | M-007:apply:A-0042:approved |
1つの申請の承認は1回 |
| 差し戻しの通知 | M-008:apply:A-0042:rejected:2 |
差し戻しは複数回あり得るので回数を入れる |
| パスワード再設定 | M-003:token:7c21 |
発行したトークンごとに1通 |
| 前日のお知らせ | M-009:apply:A-0042:2026-10-07 |
送信日を入れる。バッチを再実行しても1通 |
| 返却期限のお知らせ | M-010:apply:A-0042:2026-10-09 |
毎日送る設計なので日付で区別 |
送信依頼の表に一意制約を付け、同じキーの2回目は登録しない実装例です(SQLite で実行。同じ ON CONFLICT … DO NOTHING の文は PostgreSQL 16 でも2回目が登録されないことを確かめました)。
import sqlite3
con = sqlite3.connect(":memory:")
con.execute("""
CREATE TABLE mail_outbox (
id INTEGER PRIMARY KEY,
idempotency_key TEXT NOT NULL UNIQUE, -- 二重送信を防ぐキー
mail_id TEXT NOT NULL,
to_address TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', -- pending / sent / retry / dead
attempts INTEGER NOT NULL DEFAULT 0,
next_attempt_at TEXT
)""")
def enqueue(mail_id: str, to: str, event_key: str) -> bool:
"""送信依頼を積む。同じキーの2回目は何もしない。"""
key = f"{mail_id}:{event_key}"
cur = con.execute(
"INSERT INTO mail_outbox (idempotency_key, mail_id, to_address) "
"VALUES (?, ?, ?) ON CONFLICT (idempotency_key) DO NOTHING",
(key, mail_id, to),
)
return cur.rowcount == 1
# 申請 A-0042 の承認ボタンが2回押された(または処理が再実行された)場合
print("1回目:", enqueue("M-006", "taro@example.com", "apply:A-0042:approved"))
print("2回目:", enqueue("M-006", "taro@example.com", "apply:A-0042:approved"))
# 別の出来事(差し戻し)は別のキーなので積まれる
print("差し戻し:", enqueue("M-007", "taro@example.com", "apply:A-0042:rejected:1"))
for row in con.execute("SELECT id, idempotency_key, status FROM mail_outbox"):
print(row)
print("SQLite", sqlite3.sqlite_version)
実行結果です。
1回目: True
2回目: False
差し戻し: True
(1, 'M-006:apply:A-0042:approved', 'pending')
(2, 'M-007:apply:A-0042:rejected:1', 'pending')
2回目は登録されず、表には1行だけ残ります。キーに現在時刻や乱数を入れると毎回違う値になり、目印の役目を果たさなくなるので、出来事そのものから決まる値だけで組み立てます。
通知メールと一斉配信メールの違い
通知メールは、利用者の操作やシステムの出来事をきっかけに、その人だけに送るメールです。一斉配信メールは、お知らせや案内を多くの宛先へまとめて送るメールです。
| 観点 | 通知メール | 一斉配信メール |
|---|---|---|
| きっかけ | 個々の操作・出来事 | 送る側の予定 |
| 宛先 | 1人(+関係者) | 多数 |
| 内容 | その人の手続きに必要な情報 | お知らせ、案内 |
| 受け取る側の期待 | すぐ届く | 急がない |
| 受信の同意・配信停止 | 手続きに必要なものは止められない設計が多い | 同意の取得と、配信停止の手段が要る |
| 送信の仕組み | アプリからキュー経由 | 配信用の仕組みや配信サービス |
| 送信経路 | 通知用のドメイン・経路 | 通知とは別のサブドメイン・経路にすることが多い |
| 設計書 | メール一覧表と文面テンプレート | 配信計画(対象の抽出条件、配信日時、停止の扱い) |
Google の「メール送信者のガイドライン」では、1日あたり5,000件を超えるメールを送る送信者に対し、マーケティング目的のメールと配信登録されたメールでワンクリックの登録解除に対応することを求めています。通知と一斉配信で求められることが違うので、どちらに当たるかを先に決めます。
| 条件 | 扱い |
|---|---|
| 利用者の操作への応答(登録確認、再設定、受付) | 通知メール |
| 手続きを進めるのに必要(承認依頼、差し戻し、期限のお知らせ) | 通知メール |
| 全利用者への案内(機能追加、メンテナンス予定) | 一斉配信メール |
| 利用を促す内容(おすすめ、キャンペーン) | 一斉配信メール。同意と配信停止を用意 |
| 通知に案内文を足したい | 分ける。通知メールは用件だけにする |
SPF・DKIM・DMARC の設計時確認項目
SPF・DKIM・DMARC は、差出人のドメインが正しいことを受信側が確かめるための仕組みです。設定するのは DNS とメールサーバーですが、差出人アドレスをどのドメインにするかはメール設計で決めるので、確認項目として設計書に入れておきます。
| 仕組み | 何を確かめるか | DNS に置くもの | 設計時に確認すること |
|---|---|---|---|
| SPF(RFC 7208) | 送信元サーバーが、そのドメインから送ってよいサーバーか(MAIL FROM と HELO のドメインが対象) | TXT レコード | 送信に使うサーバー・配信サービスをすべて挙げたか。DNS 参照を伴う記述(include、a、mx、ptr、exists、redirect)は合計10回までに収まるか |
| DKIM(RFC 6376) | ヘッダーと本文に付けた署名が、公開鍵で検証できるか | セレクタ名._domainkey.ドメイン の TXT レコード(公開鍵) | 署名するドメイン(d=)とセレクタ(s=)を決めたか。鍵の入れ替え手順はあるか |
| DMARC(RFC 7489) | SPF か DKIM で確認できたドメインが、From ヘッダーのドメインと揃っているか。揃わないメールをどう扱ってほしいか | _dmarc.ドメイン の TXT レコード | ポリシー(p=none / quarantine / reject)をどの段階から始めるか。集計レポートの送り先(rua=)を決めたか |
DMARC の「揃っている(アライメント)」には、組織ドメインが同じならよい relaxed と、完全一致を求める strict があり、既定は relaxed です。
設計書に入れる確認項目です。
| 番号 | 確認項目 | 記入例 |
|---|---|---|
| 1 | 差出人(From)のアドレスと表示名 | 備品予約システム、no-reply@reserve.example.com |
| 2 | エンベロープの送信元(MAIL FROM)のドメイン | bounce.reserve.example.com |
| 3 | 送信に使うサーバー・配信サービスの一覧 | 社内 SMTP、配信サービスA |
| 4 | SPF レコードに 3 がすべて入っているか | 済 |
| 5 | DKIM の署名ドメインとセレクタ | d=reserve.example.com、s=s2026a |
| 6 | From のドメインと、SPF または DKIM のドメインが揃うか | DKIM で揃う |
| 7 | DMARC のポリシーとレポートの送り先 | p=none から開始、rua は基盤担当の受信箱 |
| 8 | 送信に TLS を使うか | 使う |
| 9 | 1日の送信数の見込み | 通知 約800通 |
| 10 | 一斉配信を同じドメインで送るか | 送らない(別のサブドメイン) |
| 11 | 返信を受けるか(Reply-To、送信専用の明記) | 受けない。フッターに明記 |
| 12 | 開発・検証用の環境から利用者へ届かない仕組み | 宛先を許可リストに限定 |
Google の「メール送信者のガイドライン」は、すべての送信者に SPF または DKIM の設定、送信時の TLS、迷惑メール率を 0.3% 未満に保つことなどを求め、1日あたり5,000件を超える送信者には SPF と DKIM の両方、DMARC の設定(ポリシーは none でも可)、From ヘッダーのドメインと SPF または DKIM のドメインの一致を求めています。9 の送信数の見込みは、この区分を判断する材料になります。
確かめて分かったこと
- テンプレートを動かして初めて、定義表の抜けが見つかりました。10種を検査スクリプトに通した最初の実行では、本文で使っている
apply_urlが定義表に無く、M-005 で指摘が出ました。目で見るレビューでは通り過ぎていた箇所です。 - substitute は値が1つ足りないだけで KeyError になり、safe_substitute は
${use_date}を残したまま文面を返します。送信処理に safe_substitute を使うと、穴の空いたメールがそのまま届く形になります。 - 差し込む値に
${user_name}や$100という文字が入っていても、もう一度置き換えられることはありません。利用者が入力した文字列を差し込んでも、別の変数の値が出ることはありませんでした。 - 件名は差し込み後に17〜48字と、テンプレートの見た目より大きく幅が出ました。備品名と日時を両方入れた M-009 と M-010 が長くなっています。
- 日本語の件名は、ヘッダーでは
=?utf-8?b?…?=の形になりました。ログを件名の文字列で検索する設計は当てにできないので、メールIDを別項目で残します。 - 4倍ずつ広げる再送は、5回で初回から5時間41分後に達しました。有効期限30分のメールに同じ設定を当てると期限後に届くので、メールごとに回数を変える列が一覧表に要ります。
よくある質問
Q. メール設計書とは何ですか? A. システムが送るメールの種類・契機・宛先・件名・文面・送信方式・届かないときの扱いをまとめた設計書です。メール一覧表、文面テンプレート、差し込み変数の定義表の3つで構成すると、設計・実装・テスト・運用のどの担当も同じ表を見て話せます。
Q. 通知メールと一斉配信メールの違いは何ですか? A. 通知メールは個々の操作や出来事をきっかけにその人へ送るメール、一斉配信メールは送る側の予定で多数へ送るメールです。同意や配信停止の要否、求められる速さ、送信の仕組みが違うので、設計書と送信経路を分けます。
Q. 同じメールが2通届くときは、どこを見直しますか? A. 送信依頼に冪等キーと一意制約があるかを確かめます。キーは「メールID+出来事」で作り、時刻や乱数を含めないようにします。バッチの再実行で起きる場合は、キーに対象日が入っているかを見ます。
Q. 再送は何回にすればよいですか? A. 決まった正解はなく、メールの有効期限と、届かないと困る度合いで決めます。間隔を広げながら3〜5回を出発点にして、初回から最後の再送までの時間を計算し、期限のあるメールはその時間が期限内に収まる回数にします。
Q. 差し込み変数が空のときはどうしますか? A. 変数ごとに「送信しない」か「代わりの表示にする」かを定義表で決めておきます。URLや番号は送信しない、氏名や任意入力の欄は代わりの言葉を出す、という分け方が扱いやすいです。
まとめ
- メール設計書は、メール一覧表・文面テンプレート・差し込み変数の定義表の3点で作ります。
- メール一覧表は1行1メールで、契機・送信タイミング・宛先・件名・差し込み項目・送信方式・再送と上限後の扱いを書きます。
- 文面はテンプレートを実際に動かして検査すると、変数の抜けと件名の長さが分かります。
- 送信はキュー経由の非同期を基本にし、再送してよい場合とそうでない場合を分けます。
- 冪等キーは「メールID+出来事」で作り、一意制約で二重送信を防ぎます。
- 通知メールと一斉配信メールは分けて設計し、差出人ドメインの SPF・DKIM・DMARC を設計段階で確認します。
参考・出典
- Python ドキュメント「string — 一般的な文字列操作」テンプレート文字列(https://docs.python.org/ja/3.11/library/string.html)
- RFC 5321 Simple Mail Transfer Protocol(https://www.rfc-editor.org/rfc/rfc5321.html)
- RFC 7208 Sender Policy Framework (SPF)(https://www.rfc-editor.org/rfc/rfc7208.html)
- RFC 6376 DomainKeys Identified Mail (DKIM) Signatures(https://www.rfc-editor.org/rfc/rfc6376.html)
- RFC 7489 Domain-based Message Authentication, Reporting, and Conformance (DMARC)(https://www.rfc-editor.org/rfc/rfc7489.html)
- Google Workspace 管理者ヘルプ「メール送信者のガイドライン」(https://support.google.com/a/answer/81126?hl=ja)
いずれも 2026-10-05 に内容を確認しました。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「メール送信の設計とメール設計書|通知メール一覧テンプレート」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。