# 通知設計書 3-フォーム送信通知

## 概要

本ドキュメントは、Payload CMSのplugin-form-builderプラグインにおけるフォーム送信通知の設計仕様を記述する。フォーム送信時にフォーム設定に基づいてメールを送信する機能の詳細を定義する。

### 本通知の処理概要

この通知は、plugin-form-builderを使用して構築されたフォームが送信された際に、フォーム設定で定義された宛先にメールを送信する機能を提供する。フォーム送信データ（FormSubmission）のafterChangeフックで発火し、複数の宛先（to/cc/bcc）への送信、動的な変数置換、リッチテキストメッセージ（Lexical/Slate）に対応している。

**業務上の目的・背景**：Webサイト上のお問い合わせフォームや申込フォームなどの送信内容を、担当者やシステム管理者にリアルタイムで通知することが目的である。これにより、迅速な顧客対応や業務処理が可能となる。また、送信者への自動返信メールとしても利用でき、フォーム送信の確認として機能する。

**通知の送信タイミング**：form-submissionsコレクションに新規ドキュメントが作成された直後（afterChangeフック、operation === 'create'時）に送信される。更新時（operation !== 'create'）は送信されない。

**通知の受信者**：フォーム設定（formsコレクション）のemails配列で定義された宛先に送信される。各メール設定でto/cc/bccを個別に指定可能。宛先にはフォームフィールドの値を変数として埋め込むことができる（例：`{{email}}`）。

**通知内容の概要**：フォームに設定されたメッセージ（Lexical/Slateリッチテキスト）とフォーム送信データが含まれる。件名・本文・宛先すべてに変数置換が適用される。

**期待されるアクション**：受信者はフォーム送信内容を確認し、必要に応じて返信や業務処理を行うことが期待される。

## 通知種別

メール

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（Promise.all） |
| 優先度 | 中 |
| リトライ | なし（エラーログのみ記録） |

### 送信先決定ロジック

1. FormSubmissionドキュメントからformID（関連するフォームのID）を取得
2. payload.findByIDでフォームドキュメントを取得
3. フォームのemails配列から各メール設定を取得
4. emailToFromConfig または defaultToEmail または payload.email.defaultFromAddress の優先順位で宛先決定
5. replaceDoubleCurlysで宛先文字列内の`{{変数名}}`を実際の値に置換

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | フォーム設定のemailFrom（変数置換後） |
| 送信元名称 | emailFromに含める必要あり |
| 件名 | フォーム設定のsubject（変数置換後） |
| 形式 | HTML |

### 本文テンプレート

```html
<div>
  ${serializedMessage}
</div>
```

- `serializedMessage`はLexicalまたはSlateのリッチテキストをHTML変換したもの
- メッセージ内の`{{変数名}}`は送信データの値に置換される
- `{{*}}`で全フィールドを「フィールド名 : 値」形式で出力
- `{{*:table}}`で全フィールドをHTMLテーブル形式で出力

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 添付ファイルなし（標準機能として） |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| `{{フィールド名}}` | フォームフィールドの値 | submissionData配列 | No |
| `{{*}}` | 全フィールドの値（改行区切り） | submissionData配列 | No |
| `{{*:table}}` | 全フィールドの値（HTMLテーブル） | submissionData配列 | No |
| `{{formSubmissionID}}` | フォーム送信ID | doc.id | Yes（自動追加） |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| API | POST `/api/form-submissions` | operation === 'create' かつ emails配列が存在 | フォーム送信コレクションへの新規作成 |
| ローカルAPI | payload.create({collection: 'form-submissions'}) | 同上 | サーバーサイドでの新規作成 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| operation !== 'create' | 更新時はメール送信しない |
| emails配列が空 | 送信先が未設定の場合はスキップ（ログ出力のみ） |
| beforeEmailフックでemailsToSendを空配列にした場合 | フック処理で送信抑止 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[form-submissionsドキュメント作成] --> B{operation === 'create'?}
    B -->|No| C[処理終了]
    B -->|Yes| D[関連フォームをfindByIDで取得]
    D --> E{emails配列が存在?}
    E -->|No| F[ログ出力して終了]
    E -->|Yes| G[submissionData準備]
    G --> H[formSubmissionIDを追加]
    H --> I[各メール設定をループ]
    I --> J[to/cc/bcc/from/replyTo変数置換]
    J --> K{messageはLexical?}
    K -->|Yes| L[serializeLexicalでHTML変換]
    K -->|No| M[serializeSlateでHTML変換]
    L --> N[FormattedEmail配列生成]
    M --> N
    N --> O{beforeEmailフックが設定?}
    O -->|Yes| P[beforeEmailフック実行]
    O -->|No| Q[メール送信処理]
    P --> Q
    Q --> R[Promise.allで並列送信]
    R --> S{送信成功?}
    S -->|Yes| T[正常終了]
    S -->|No| U[エラーログ出力]
    U --> T
    F --> C
```

## データベース参照・更新仕様

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| forms（またはformOverrides.slug） | フォーム設定（メールテンプレート）取得 | payload.findByIDで取得 |
| form-submissions | 送信データ取得 | afterChangeフックの引数として渡される |

### テーブル別参照項目詳細

#### forms

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| emails | メール設定配列 | WHERE id = :formID |
| emails[].emailTo | 送信先アドレス | - |
| emails[].emailFrom | 送信元アドレス | - |
| emails[].cc | CCアドレス | - |
| emails[].bcc | BCCアドレス | - |
| emails[].replyTo | 返信先アドレス | - |
| emails[].subject | 件名 | - |
| emails[].message | 本文（Lexical/Slate） | - |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| なし | - | 送信通知処理ではDB更新なし |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| フォーム取得失敗 | formIDに対応するフォームが存在しない | エラーログ出力（送信処理は継続しない） |
| 送信失敗 | メールサーバー接続エラー | エラーログ出力（他のメールは継続して送信） |
| テンプレートエラー | Lexical/Slateシリアライズエラー | エラーログ出力 |

### リトライ仕様

| 項目 | 内容 |
|-----|------|
| リトライ回数 | 0（リトライなし） |
| リトライ間隔 | - |
| リトライ対象エラー | - |

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | Payload設定に依存（デフォルトなし） |
| 1日あたり上限 | Payload設定に依存（デフォルトなし） |

### 配信時間帯

制限なし（24時間送信可能）

## セキュリティ考慮事項

1. **入力値の検証**: フォームフィールドの値がそのまま使用されるため、XSS対策としてHTMLエスケープが必要
2. **メールインジェクション対策**: 宛先フィールドに改行コードが含まれないよう注意
3. **機密情報**: フォーム送信データに機密情報が含まれる場合、メール送信の是非を検討
4. **変数置換**: `{{変数名}}`形式の置換により、任意のフィールド値がメールに挿入可能

## 備考

- `beforeEmail`フックにより、送信前にメール内容をカスタマイズ可能
- `defaultToEmail`設定により、フォームにemailToが未設定の場合のフォールバック先を指定可能
- Lexical形式とSlate形式の両方のリッチテキストに対応
- 支払いフィールド（payment blockType）が存在する場合、beforeChangeフックでcreateChargeが実行される

---

## コードリーディングガイド

本通知を理解するために参照すべきファイルと、推奨する読み解き順序を以下に示す。

### 推奨読解順序

#### Step 1: データ構造を理解する

まず、フォームとフォーム送信の型定義を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | types.ts | `packages/plugin-form-builder/src/types.ts` | Email型（209-217行目）、FormattedEmail型（219-227行目）、Form型（238-247行目） |

**読解のコツ**: Email型はフォーム設定側の構造、FormattedEmail型は送信処理用に整形された構造。messageフィールドはLexical/Slateのリッチテキストオブジェクト。

#### Step 2: エントリーポイントを理解する

FormSubmissionsコレクションの設定を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | index.ts | `packages/plugin-form-builder/src/collections/FormSubmissions/index.ts` | afterChangeフックの設定（103-106行目） |

**主要処理フロー**:
1. **103行目**: afterChangeフック配列にsendEmail関数を登録
2. **104行目**: `(data) => sendEmail(data, formConfig)` で呼び出し
3. **107-108行目**: beforeChangeフックにcreateChargeを登録（支払い処理）

#### Step 3: メール送信処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | sendEmail.ts | `packages/plugin-form-builder/src/collections/FormSubmissions/hooks/sendEmail.ts` | メール送信の中核ロジック |

**主要処理フロー**:
- **15行目**: operation === 'create' のチェック
- **27-32行目**: 関連フォームをpayload.findByIDで取得
- **34行目**: emails配列を取得
- **44-81行目**: 各メール設定をFormattedEmailに変換
- **59行目**: to宛先の変数置換
- **65-69行目**: Lexical/Slate判定とシリアライズ
- **85-87行目**: beforeEmailフックの実行
- **89-101行目**: Promise.allで並列メール送信
- **96-99行目**: 送信エラー時のログ出力

#### Step 4: 変数置換処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | replaceDoubleCurlys.ts | `packages/plugin-form-builder/src/utilities/replaceDoubleCurlys.ts` | `{{変数名}}`形式の置換処理 |

**主要処理フロー**:
- **11行目**: 正規表現 `/\{\{(.+?)\}\}/g` でマッチ
- **14-24行目**: `*` または `*:table` の特殊変数処理
- **26-31行目**: 通常の変数置換

### プログラム呼び出し階層図

```
FormSubmission afterChange hook
    │
    └─ sendEmail (hooks/sendEmail.ts)
           │
           ├─ payload.findByID (フォーム設定取得)
           │
           ├─ replaceDoubleCurlys (宛先・件名の変数置換)
           │
           ├─ serializeLexical または serializeSlate (本文HTML化)
           │      │
           │      └─ replaceDoubleCurlys (本文内変数置換)
           │
           ├─ beforeEmail フック (オプション)
           │
           └─ payload.sendEmail (並列送信)
                  │
                  └─ (各宛先へ個別送信)
```

### データフロー図

```
[入力]                    [処理]                              [出力]

FormSubmission ───▶ sendEmail ───▶ メール送信
   │                     │
   ├─ form (ID)          ├─▶ payload.findByID(form)
   │                     │       └─▶ emails配列取得
   │                     │
   └─ submissionData ────┼─▶ replaceDoubleCurlys
                         │       └─▶ 変数置換
                         │
                         └─▶ serializeLexical/serializeSlate
                                 └─▶ HTML本文生成
                                         │
                                         └─▶ 各宛先へメール送信
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| sendEmail.ts | `packages/plugin-form-builder/src/collections/FormSubmissions/hooks/sendEmail.ts` | ソース | メール送信フック |
| index.ts | `packages/plugin-form-builder/src/collections/FormSubmissions/index.ts` | ソース | FormSubmissionsコレクション定義 |
| types.ts | `packages/plugin-form-builder/src/types.ts` | ソース | 型定義 |
| replaceDoubleCurlys.ts | `packages/plugin-form-builder/src/utilities/replaceDoubleCurlys.ts` | ソース | 変数置換ユーティリティ |
| serializeLexical.ts | `packages/plugin-form-builder/src/utilities/lexical/serializeLexical.ts` | ソース | Lexicalシリアライザー |
| serializeSlate.ts | `packages/plugin-form-builder/src/utilities/slate/serializeSlate.ts` | ソース | Slateシリアライザー |
| keyValuePairToHtmlTable.ts | `packages/plugin-form-builder/src/utilities/keyValuePairToHtmlTable.ts` | ソース | HTMLテーブル生成 |
