# 機能設計書 20-ニュースレター管理

## 概要

本ドキュメントは、Ghostにおけるニュースレター管理機能の設計仕様を記載する。本機能は、複数ニュースレターの作成・設定・購読者管理を提供する。

### 本機能の処理概要

**業務上の目的・背景**：
コンテンツクリエイターが異なるテーマやターゲット層に合わせて複数のニュースレターを運営するため、ニュースレターの作成・編集・管理機能が必要である。本機能により、各ニュースレターの送信元設定、デザインカスタマイズ、購読者管理を行い、効果的なメールマーケティングを実現する。

**機能の利用シーン**：
- 新規ニュースレターを作成する場合
- ニュースレターのデザイン設定を変更する場合
- 送信元メールアドレスを変更・検証する場合
- ニュースレターをアーカイブ/アクティブ化する場合
- 既存メンバーを新規ニュースレターに一括登録する場合

**主要な処理内容**：
1. ニュースレターのCRUD操作
2. メール設定（送信元・返信先）の検証フロー
3. デザイン設定（フォント、カラー、レイアウト）
4. 購読者の一括オプトイン
5. ニュースレターの表示順序管理

**関連システム・外部連携**：
- メンバー管理（購読者管理）
- メール配信（ニュースレター配信機能）
- MagicLink（メールアドレス検証）

**権限による制御**：
- ニュースレター管理: Administrator以上

## 関連画面

| 画面No | 画面名 | 関連種別 | 関連する操作・処理 |
|--------|--------|----------|------------------|
| 46 | ニュースレター設定画面 | 主機能 | ニュースレターの作成・編集 |

## 機能種別

CRUD操作 / メール検証

## 入力仕様

### 入力パラメータ（ニュースレター作成）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| name | string | Yes | ニュースレター名 | 一意性チェック |
| slug | string | No | スラッグ | 自動生成可 |
| description | string | No | 説明 | - |
| status | string | No | ステータス | active/archived |
| visibility | string | No | 公開範囲 | members等 |
| subscribe_on_signup | boolean | No | 新規登録時自動購読 | デフォルトtrue |
| sender_name | string | No | 送信者名 | - |
| sender_email | string | No | 送信元メールアドレス | 検証必要 |
| sender_reply_to | string | No | 返信先設定 | newsletter/support/カスタム |
| header_image | string | No | ヘッダー画像URL | - |
| show_header_icon | boolean | No | ヘッダーアイコン表示 | デフォルトtrue |
| show_header_title | boolean | No | ヘッダータイトル表示 | デフォルトtrue |
| show_header_name | boolean | No | ヘッダー名表示 | デフォルトtrue |
| title_font_category | string | No | タイトルフォント | sans_serif/serif |
| title_alignment | string | No | タイトル配置 | center/left |
| show_feature_image | boolean | No | アイキャッチ画像表示 | デフォルトtrue |
| body_font_category | string | No | 本文フォント | sans_serif/serif |
| footer_content | string | No | フッターコンテンツ | - |
| show_badge | boolean | No | Ghostバッジ表示 | デフォルトtrue |
| feedback_enabled | boolean | No | フィードバック有効化 | Labs依存 |
| background_color | string | No | 背景色 | light/dark等 |

### 入力パラメータ（ニュースレター編集）

| パラメータ名 | 型 | 必須 | 説明 | バリデーション |
|-------------|-----|-----|------|---------------|
| id | string | Yes | ニュースレターID | 存在チェック |
| (上記作成パラメータ) | - | No | 編集対象属性 | - |

### オプション

| オプション名 | 型 | 説明 |
|-------------|-----|------|
| opt_in_existing | boolean | 既存メンバーを自動購読 |
| include | string | count.posts, count.members, count.active_members |

### 入力データソース

- 管理画面からのAPI呼び出し
- Ghost Admin設定画面

## 出力仕様

### 出力データ

| 項目名 | 型 | 説明 |
|--------|-----|------|
| id | string | ニュースレターID |
| uuid | string | UUID（公開識別子） |
| name | string | ニュースレター名 |
| slug | string | スラッグ |
| description | string | 説明 |
| status | string | active/archived |
| visibility | string | 公開範囲 |
| subscribe_on_signup | boolean | 新規登録時自動購読 |
| sender_name | string | 送信者名 |
| sender_email | string | 送信元メール |
| sender_reply_to | string | 返信先設定 |
| sort_order | number | 表示順序 |
| header_image | string | ヘッダー画像URL |
| (デザイン設定) | - | 各種表示設定 |
| created_at | Date | 作成日時 |
| updated_at | Date | 更新日時 |
| count.posts | number | 関連記事数（include時） |
| count.members | number | 購読者数（include時） |
| count.active_members | number | アクティブ購読者数（include時） |
| meta.sent_email_verification | array | 検証メール送信済みプロパティ |
| meta.opted_in_member_count | number | オプトインしたメンバー数 |

### 出力先

- REST API レスポンス（JSON形式）
- DBテーブル（newsletters）

## 処理フロー

### 処理シーケンス

```
1. APIリクエスト受信
   └─ newsletters エンドポイントで処理
2. 入力検証
   └─ 名前の一意性、ステータス制限チェック
3. メールアドレス検証判定
   └─ sender_email, sender_reply_to の変更検出
4. アクティブ制限チェック
   └─ active状態への変更時にlimitService確認
5. ニュースレター保存
   └─ DB操作（INSERT/UPDATE）
6. オプトイン処理（作成時）
   └─ opt_in_existing=trueなら既存メンバー登録
7. 検証メール送信
   └─ 必要なプロパティに検証メール送信
8. レスポンス返却
   └─ metaに検証情報を付加して返却
```

### フローチャート

```mermaid
flowchart TD
    A[APIリクエスト] --> B{操作種別}
    B -->|add| C[名前一意性チェック]
    B -->|edit| D[既存ニュースレター取得]
    C --> E{status=active?}
    D --> E
    E -->|Yes| F[limitServiceチェック]
    E -->|No| G[メールアドレス検証判定]
    F --> G
    G --> H{検証必要?}
    H -->|Yes| I[検証用属性をクリア]
    H -->|No| J[DB保存]
    I --> J
    J --> K{opt_in_existing?}
    K -->|Yes| L[既存メンバー一括登録]
    K -->|No| M{検証メール?}
    L --> M
    M -->|Yes| N[MagicLink送信]
    M -->|No| O[レスポンス返却]
    N --> O
```

## ビジネスルール

### 業務ルール

| ルールNo | ルール名 | 内容 | 適用条件 |
|---------|---------|------|---------|
| BR-20-01 | 名前一意性 | 同名のニュースレターは作成不可 | add/edit時 |
| BR-20-02 | アクティブ制限 | ホスティングプランによりアクティブ数に制限あり | status=active時 |
| BR-20-03 | メール検証 | sender_email/sender_reply_toの変更は検証必要 | カスタムメール設定時 |
| BR-20-04 | 返信先プリセット | newsletter/supportは検証不要 | sender_reply_to設定時 |
| BR-20-05 | 自動スラッグ | 未指定時はnameから自動生成 | add時 |
| BR-20-06 | sort_order自動設定 | 新規作成時は最後尾に追加 | add時 |
| BR-20-07 | オプトイン | opt_in_existing時は購読者をコピー | add時 |
| BR-20-08 | フィードバック機能 | feedback_enabledはLabs依存 | Labs未有効時は強制false |
| BR-20-09 | Content API制限 | 公開APIではactive状態のみ取得可能 | enforcedFilters |

### 計算ロジック

- sort_order: 最大sort_order + 1を自動設定
- count.active_members: email_disabled=falseのメンバーのみカウント

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

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

| 操作 | 対象テーブル | 操作種別 | 概要 |
|-----|-------------|---------|------|
| ニュースレター作成 | newsletters | INSERT | 新規レコード作成 |
| ニュースレター編集 | newsletters | UPDATE | レコード更新 |
| オプトイン | members_newsletters | INSERT（バッチ） | 購読関連付け |

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

#### newsletters

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | ObjectID | 自動生成 |
| INSERT | uuid | crypto.randomUUID() | 自動生成 |
| INSERT | name | 入力値 | 一意制約 |
| INSERT | slug | 入力値または自動生成 | 一意制約 |
| INSERT | status | active（デフォルト） | - |
| INSERT | visibility | members（デフォルト） | - |
| INSERT | subscribe_on_signup | true（デフォルト） | - |
| INSERT | sender_reply_to | newsletter（デフォルト） | - |
| INSERT | sort_order | 自動計算 | - |
| INSERT | title_font_category | sans_serif（デフォルト） | - |
| INSERT | title_alignment | center（デフォルト） | - |
| INSERT | show_feature_image | true（デフォルト） | - |
| INSERT | body_font_category | sans_serif（デフォルト） | - |
| INSERT | show_badge | true（デフォルト） | - |
| INSERT | background_color | light（デフォルト） | - |
| INSERT | feedback_enabled | false（デフォルト） | - |
| UPDATE | (各属性) | 入力値 | 変更された属性のみ |

#### members_newsletters

| 操作 | 項目（カラム名） | 更新値・取得条件 | 備考 |
|-----|-----------------|-----------------|------|
| INSERT | id | ObjectID | 自動生成 |
| INSERT | member_id | メンバーID | FK |
| INSERT | newsletter_id | ニュースレターID | FK |

## エラー処理

### エラーケース一覧

| エラーコード | エラー種別 | 発生条件 | 対処方法 |
|------------|----------|---------|---------|
| ValidationError | 400 | 同名ニュースレター存在 | 別名を指定 |
| ValidationError | 400 | 許可されていないメールアドレス | 許可ドメインを確認 |
| NotFoundError | 404 | ニュースレターが見つからない | 正しいIDを指定 |
| HostLimitError | 403 | アクティブニュースレター上限超過 | プラン変更またはアーカイブ |

### リトライ仕様

- 検証メール送信失敗時: 再度編集操作で再送信可能

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

- ニュースレター作成 + オプトイン: 同一トランザクション内で実行
- opt_in_existing時: MemberModel.fetchAllSubscribed → subscribeMembersById

## パフォーマンス要件

- count.active_members: JOINクエリで計算
- batchInsert: オプトイン時の一括挿入

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

- メールアドレス検証: MagicLinkによるトークンベース検証
- 24時間でトークン失効
- 許可ドメインのバリデーション（emailAddressService）

## 備考

- header_image: urlUtils.toTransformReady/transformReadyToAbsoluteで変換
- enforcedFilters: Content API（公開API）ではstatus:activeのみ取得可能
- Labs.audienceFeedback: フィードバック機能はフラグ依存

---

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

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

### 推奨読解順序

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

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | newsletter.js | `ghost/core/core/server/models/newsletter.js` | Bookshelfモデル定義 |

**読解のコツ**:
- tableName: newsletters でテーブル名確認
- defaults() でデフォルト値を確認
- members(), posts() でリレーションを確認
- countRelations() でカウントクエリを確認

**主要処理フロー**:
- **6-7行目**: モデル定義、テーブル名
- **9-41行目**: defaults() - 全デフォルト値定義
- **43-50行目**: members() - メンバーとの多対多リレーション
- **52-54行目**: posts() - 記事との一対多リレーション
- **57-59行目**: enforcedFilters() - Content API用フィルタ
- **61-79行目**: onSaving() - 保存前処理（name trim、slug生成）
- **81-98行目**: subscribeMembersById() - メンバー一括購読
- **144-172行目**: countRelations() - 関連カウントクエリ
- **187-204行目**: getDefaultNewsletter() - デフォルトニュースレター取得
- **206-224行目**: getNextAvailableSortOrder() - 次のsort_order取得

#### Step 2: サービス層を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | newsletters-service.js | `ghost/core/core/server/services/newsletters/newsletters-service.js` | ビジネスロジック |

**読解のコツ**:
- constructor()でDI（依存性注入）を確認
- MagicLinkServiceでメール検証フローを確認
- prepAttrsForEmailVerification()でメール検証判定ロジックを確認

**主要処理フロー**:
- **31-92行目**: constructor() - 依存関係注入とMagicLink設定
- **100-109行目**: read() - 単一取得
- **116-118行目**: browse() - 一覧取得
- **133-196行目**: add() - 新規作成（制限チェック、検証、オプトイン）
- **206-238行目**: edit() - 編集（検証、制限チェック）
- **245-258行目**: verifyPropertyUpdate() - トークンによるプロパティ検証
- **265-326行目**: prepAttrsForEmailVerification() - メール検証前処理
- **331-342行目**: respondWithEmailVerification() - 検証メール送信・メタ追加
- **347-367行目**: sendEmailVerificationMagicLink() - MagicLink送信

#### Step 3: APIエンドポイントを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | newsletters.js | `ghost/core/core/server/api/endpoints/newsletters.js` | APIコントローラ |

**主要処理フロー**:
- **1行目**: allowedIncludes定義（count.posts, count.members, count.active_members）
- **9-32行目**: browse - 一覧取得API
- **34-62行目**: read - 単一取得API
- **64-84行目**: add - 作成API（opt_in_existingオプション）
- **86-108行目**: edit - 編集API
- **110-123行目**: verifyPropertyUpdate - メール検証API

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

```
newsletters.js (API Endpoint)
    │
    ├─ browse()
    │      └─ newslettersService.browse()
    │             └─ NewsletterModel.findPage()
    │
    ├─ read()
    │      └─ newslettersService.read()
    │             └─ NewsletterModel.findOne()
    │
    ├─ add()
    │      └─ newslettersService.add()
    │             ├─ limitService.errorIfWouldGoOverLimit()
    │             ├─ prepAttrsForEmailVerification()
    │             ├─ getNextAvailableSortOrder()
    │             ├─ NewsletterModel.add()
    │             ├─ MemberModel.fetchAllSubscribed()
    │             ├─ newsletter.subscribeMembersById()
    │             └─ respondWithEmailVerification()
    │                    └─ sendEmailVerificationMagicLink()
    │
    ├─ edit()
    │      └─ newslettersService.edit()
    │             ├─ NewsletterModel.findOne() (original)
    │             ├─ prepAttrsForEmailVerification()
    │             ├─ limitService.errorIfWouldGoOverLimit()
    │             ├─ NewsletterModel.edit()
    │             └─ respondWithEmailVerification()
    │
    └─ verifyPropertyUpdate()
           └─ newslettersService.verifyPropertyUpdate()
                  ├─ magicLinkService.getDataFromToken()
                  └─ NewsletterModel.edit()
```

### データフロー図

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

管理画面               newsletters.js         newslettersService
リクエスト ────▶ (API Endpoint) ────▶ (Service Layer)
   │                   │                    │
   │                   │                    ▼
   │                   │            ┌───────────────────┐
   │                   │            │ prepAttrsForEmail │
   │                   │            │ Verification()    │
   │                   │            └───────────────────┘
   │                   │                    │
   │                   │                    ▼
   │                   │            ┌───────────────────┐
   │                   │            │ NewsletterModel   │
   │                   │            │ add()/edit()      │
   │                   │            └───────────────────┘
   │                   │                    │
   │                   │                    ▼
   │                   │            newsletters テーブル
   │                   │                    │
   │                   │    (opt_in_existing時)
   │                   │                    ▼
   │                   │            members_newsletters
   │                   │                    │
   │                   │    (検証必要時)
   │                   │                    ▼
   │                   │            ┌───────────────────┐
   │                   │            │ MagicLink送信     │
   │                   │            └───────────────────┘
   │                   │
   └── JSON ◀───────── レスポンス (meta.sent_email_verification含む)

[メール検証フロー]

検証メール ─────▶ ユーザークリック ─────▶ verifyPropertyUpdate()
                                               │
                                               ▼
                                        トークン検証
                                               │
                                               ▼
                                        Newsletter更新
                                        (sender_email等)
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| newsletter.js | `ghost/core/core/server/models/newsletter.js` | ソース | Bookshelfモデル |
| newsletters-service.js | `ghost/core/core/server/services/newsletters/newsletters-service.js` | ソース | ビジネスロジック |
| newsletters.js | `ghost/core/core/server/api/endpoints/newsletters.js` | ソース | APIエンドポイント |
| verify-email.js | `ghost/core/core/server/services/newsletters/emails/verify-email.js` | ソース | 検証メールテンプレート |
| magic-link.js | `ghost/core/core/server/services/lib/magic-link/magic-link.js` | ソース | MagicLinkサービス |
| email-address-service.js | `ghost/core/core/server/services/email-address/email-address-service.js` | ソース | メールアドレス検証 |
