# 機能設計書 23-メール抑制リスト

## 概要

本ドキュメントは、Ghostのメール抑制リスト（Email Suppression List）機能の設計仕様を記述する。メール抑制リスト機能は、バウンス（配信失敗）やスパム報告されたメールアドレスを管理し、該当アドレスへの再送信を防止することでメール配信の品質を維持する。

### 本機能の処理概要

**業務上の目的・背景**：メール配信において、バウンスしたアドレスへの継続的な送信や、スパム報告者への送信は、メール配信プロバイダー（Mailgun）からのドメイン評価低下やブロックリスト登録のリスクを高める。メール抑制リスト機能は、問題のあるアドレスを自動的に抑制リストに登録し、以降の配信から除外することで、ドメインレピュテーションを保護し、正常なメンバーへの配信到達率を維持する。

**機能の利用シーン**：
- バウンスメール発生時の自動抑制登録
- スパム報告発生時の自動抑制登録
- メンバーのメール配信停止状態の確認
- 抑制リストからの手動削除（アドレス修正後の再有効化）

**主要な処理内容**：
1. メールバウンスイベント（永続的失敗）の検知と抑制登録
2. スパム報告イベントの検知と抑制登録
3. 抑制状態の照会（単一/一括）
4. Mailgun APIからの抑制削除（バウンス/スパム/購読解除）
5. ローカルデータベースからの抑制削除

**関連システム・外部連携**：
- Mailgun API（抑制リスト管理）
- DomainEvents（イベント駆動処理）

**権限による制御**：管理者のみが抑制リストの参照・削除が可能。メンバーは自身の配信停止状態をPortalで確認可能。

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 89 | メール停止通知ページ | 主機能 | メール配信停止状態の通知表示 |

## 機能種別

イベント駆動処理 / データ管理 / 外部API連携

## 入力仕様

### 入力パラメータ

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| email | string | Yes | 対象メールアドレス | 有効なメールアドレス形式 |
| emails | string[] | No | 一括照会用メールアドレス配列 | 各要素が有効なメールアドレス形式 |

### 入力データソース

- **EmailBouncedEvent**: メールバウンスイベント（email-analytics経由）
- **SpamComplaintEvent**: スパム報告イベント（email-analytics経由）
- **suppressions テーブル**: 既存の抑制データ

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| suppressed | boolean | 抑制状態（true: 抑制中） |
| info.reason | string | 抑制理由（'spam' または 'fail'） |
| info.timestamp | Date | 抑制登録日時 |

### 出力先

- **suppressions テーブル**: 抑制データの保存
- **Mailgun Suppression API**: 抑制解除リクエスト
- **DomainEvents**: EmailSuppressedEvent の発行

## 処理フロー

### 処理シーケンス

```
1. イベント受信
   └─ EmailBouncedEvent または SpamComplaintEvent を DomainEvents 経由で受信

2. 抑制条件の判定
   └─ バウンスの場合: エラーコード 605 または 607 のみ抑制対象
   └─ スパム報告の場合: 無条件で抑制対象

3. 抑制データの登録
   └─ suppressions テーブルに INSERT
   └─ 重複登録（ER_DUP_ENTRY）は無視

4. 抑制イベントの発行
   └─ EmailSuppressedEvent を dispatch

5. 抑制解除（手動操作時）
   └─ Mailgun API からバウンス/スパム/購読解除を削除
   └─ suppressions テーブルから削除
```

### フローチャート

```mermaid
flowchart TD
    A[開始] --> B{イベント種別}
    B -->|EmailBouncedEvent| C{エラーコード確認}
    B -->|SpamComplaintEvent| D[スパム報告処理]
    C -->|605 or 607| E[バウンス処理]
    C -->|その他| F[無視]
    D --> G[suppressions にINSERT]
    E --> G
    G --> H{重複エラー?}
    H -->|Yes| I[無視して続行]
    H -->|No| J[EmailSuppressedEvent発行]
    I --> K[終了]
    J --> K
    F --> K

    subgraph 抑制解除フロー
    L[解除リクエスト] --> M[Mailgun API呼び出し]
    M --> N[removeBounce]
    M --> O[removeComplaint]
    M --> P[removeUnsubscribe]
    N --> Q[suppressions から DELETE]
    O --> Q
    P --> Q
    Q --> R[完了]
    end
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-23-01 | バウンスコード制限 | エラーコード 605（メールボックス無効）または 607（メールアドレス無効）のバウンスのみ抑制 | EmailBouncedEvent 受信時 |
| BR-23-02 | スパム報告即時抑制 | スパム報告は無条件で即座に抑制登録 | SpamComplaintEvent 受信時 |
| BR-23-03 | 重複登録許容 | 同一アドレスの重複登録試行はエラーとせず無視 | INSERT 時の ER_DUP_ENTRY |
| BR-23-04 | 完全削除 | 解除時は Mailgun とローカル DB の両方から削除 | removeEmail 呼び出し時 |

### 計算ロジック

特になし

## データベース操作仕様

### 操作別データベース影響一覧

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| 抑制登録 | suppressions | INSERT | 抑制データの登録 |
| 抑制照会 | suppressions | SELECT | 抑制状態の取得 |
| 抑制解除 | suppressions | DELETE | 抑制データの削除 |

### テーブル別操作詳細

#### suppressions

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | email | 対象メールアドレス | UNIQUE 制約あり |
| INSERT | email_id | 関連するメールID | NULL許容 |
| INSERT | reason | 'spam' または 'bounce' | 抑制理由 |
| INSERT | created_at | イベント発生日時 | タイムスタンプ |
| SELECT | * | email で検索 | 単一照会 |
| SELECT | * | email IN (...) | 一括照会 |
| DELETE | - | email で指定 | destroyBy 使用 |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| ER_DUP_ENTRY | DB重複エラー | 同一メールアドレスの重複登録 | 無視して処理続行 |
| - | API Error | Mailgun API呼び出し失敗 | ログ出力、false 返却 |

### リトライ仕様

- Mailgun API呼び出し失敗時はリトライなし（ログ出力のみ）
- DB操作失敗時もリトライなし

## トランザクション仕様

- 抑制登録は単一レコードのINSERTのため、暗黙的トランザクション
- 抑制解除時のMailgun API呼び出しとDB削除は個別処理（一方が失敗しても他方は実行）

## パフォーマンス要件

- 単一照会: 即時応答
- 一括照会: findAll で効率的に取得

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

- メールアドレスは個人情報として適切に管理
- Mailgun APIキーはサーバーサイドでのみ使用
- 抑制リストへのアクセスは管理者権限で制限

## 備考

- Mailgun のエラーコード 605/607 は永続的なアドレス無効を示す
- 一時的なエラー（メールボックス満杯等）は抑制対象外

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | email-suppression-list.js | `ghost/core/core/server/services/email-suppression-list/email-suppression-list.js` | EmailSuppressionData クラスと AbstractEmailSuppressionList インターフェース |
| 1-2 | schema.js | `ghost/core/core/server/data/schema/schema.js` | suppressions テーブル（1032-1048行）のスキーマ定義 |

**読解のコツ**: EmailSuppressionData は suppressed（boolean）と info（reason, timestamp）を持つシンプルな構造。reason は 'spam' または 'fail' の2種類。

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | mailgun-email-suppression-list.js | `ghost/core/core/server/services/email-suppression-list/mailgun-email-suppression-list.js` | MailgunEmailSuppressionList クラスがメイン実装 |

**主要処理フロー**:
1. **27-48行目**: removeEmail() - Mailgun API と DB からの削除処理
2. **60-78行目**: getSuppressionData() - 単一アドレスの抑制状態取得
3. **80-106行目**: getBulkSuppressionData() - 一括照会
4. **108-139行目**: init() - イベントハンドラの登録

#### Step 3: イベントハンドリングを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | email-bounced-event.js | `ghost/core/core/server/services/email-service/events/email-bounced-event.js` | バウンスイベントの構造 |
| 3-2 | spam-complaint-event.js | `ghost/core/core/server/services/email-service/events/spam-complaint-event.js` | スパム報告イベントの構造 |

**主要処理フロー**:
- **110-138行目**: init() 内の handleEvent - イベント種別（bounce/spam）に応じた処理分岐
- **112-118行目**: バウンスの場合のエラーコード判定（605, 607のみ）

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

```
EmailAnalyticsService (email-analytics)
    │
    └─ EmailEventProcessor.handlePermanentFailed()
           │
           └─ DomainEvents.dispatch(EmailBouncedEvent)
                  │
                  └─ MailgunEmailSuppressionList.init() [handler]
                         │
                         ├─ Suppression.add()
                         │      └─ suppressions テーブル INSERT
                         │
                         └─ DomainEvents.dispatch(EmailSuppressedEvent)

SpamComplaintEvent
    │
    └─ MailgunEmailSuppressionList.init() [handler]
           │
           ├─ Suppression.add()
           │
           └─ DomainEvents.dispatch(EmailSuppressedEvent)
```

### データフロー図

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

EmailBouncedEvent ────────▶ handleEvent('bounce')
  (error.code: 605/607)          │
                                 ├─ コード判定
                                 │
SpamComplaintEvent ───────▶ handleEvent('spam')
                                 │
                                 ▼
                          Suppression.add() ───────────▶ suppressions テーブル
                                 │
                                 ▼
                          EmailSuppressedEvent ────────▶ DomainEvents

[抑制解除フロー]
removeEmail(email) ──────▶ Mailgun API
                                 │
                                 ├─ removeBounce()
                                 ├─ removeComplaint()
                                 └─ removeUnsubscribe()
                                 │
                                 ▼
                          Suppression.destroy() ───────▶ suppressions テーブル DELETE
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| email-suppression-list.js | `ghost/core/core/server/services/email-suppression-list/email-suppression-list.js` | ソース | 基底クラス・データ構造定義 |
| mailgun-email-suppression-list.js | `ghost/core/core/server/services/email-suppression-list/mailgun-email-suppression-list.js` | ソース | Mailgun実装 |
| in-memory-email-suppression-list.js | `ghost/core/core/server/services/email-suppression-list/in-memory-email-suppression-list.js` | ソース | インメモリ実装（テスト用） |
| service.js | `ghost/core/core/server/services/email-suppression-list/service.js` | ソース | サービスファクトリ |
| index.js | `ghost/core/core/server/services/email-suppression-list/index.js` | ソース | モジュールエントリーポイント |
| email-bounced-event.js | `ghost/core/core/server/services/email-service/events/email-bounced-event.js` | ソース | バウンスイベント定義 |
| spam-complaint-event.js | `ghost/core/core/server/services/email-service/events/spam-complaint-event.js` | ソース | スパム報告イベント定義 |
| schema.js | `ghost/core/core/server/data/schema/schema.js` | 設定 | DBスキーマ定義 |
