# 通知設計書 20-ニュースレターメールアドレス検証

## 概要

本ドキュメントは、Ghost CMSにおけるニュースレターメールアドレス検証機能の設計仕様を記載したものである。

### 本通知の処理概要

ニュースレターメールアドレス検証は、管理者がニュースレターの送信元メールアドレス（sender_email）または返信先メールアドレス（sender_reply_to）を変更した際に、そのメールアドレスの所有権を確認するために送信される検証メールである。

**業務上の目的・背景**：ニュースレターの送信元メールアドレスは、受信者から見て信頼性の指標となる重要な設定項目である。不正なメールアドレスの使用を防止し、メール配信のレピュテーションを保護するため、新しいメールアドレスを設定する際には所有権の検証が必要となる。これにより、なりすましメールの送信を防止し、メール到達率を維持できる。

**通知の送信タイミング**：管理者がニュースレター設定画面でsender_emailまたはsender_reply_toを変更し、そのメールアドレスが検証を必要とする場合に送信される。`emailAddressService.service.validate`が`verificationEmailRequired: true`を返した場合にトリガーされる。

**通知の受信者**：検証対象のメールアドレス（変更しようとしている新しいメールアドレス）。

**通知内容の概要**：メールアドレス確認用のMagicLink（トークンベースのURL）が含まれる。リンクの有効期限は24時間。

**期待されるアクション**：受信者は確認リンクをクリックし、Ghost管理画面に戻ることでメールアドレスの検証が完了する。検証が完了すると、ニュースレターの設定が更新される。

## 通知種別

メール通知（管理者向け/検証メール）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 非同期（MagicLinkサービス経由） |
| 優先度 | 高 |
| リトライ | なし |

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

検証対象のメールアドレス（`email`パラメータ）に直接送信する。これはニュースレター設定で変更しようとしている新しいメールアドレスである。

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | `emailAddressService.service.defaultFromAddress` |
| 送信元名称 | システム |
| 件名 | `Verify email address` |
| 形式 | HTML/テキスト両対応 |

### 本文テンプレート

`verify-email.js`テンプレート関数を使用（HTMLメール生成）。テキスト版はMagicLinkサービス内の`getText`関数で生成。

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 添付ファイルなし |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| url | 検証URL | `getSigninURL(token)` - adminUrl + hash付き | Yes |
| email | 受信者のメールアドレス | sendEmailVerificationMagicLink引数 | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| API呼び出し | NewslettersService.add() | sender_email/sender_reply_toが検証必要 | ニュースレター新規作成時 |
| API呼び出し | NewslettersService.edit() | sender_email/sender_reply_toが変更され検証必要 | ニュースレター編集時 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 検証不要 | emailAddressService.validateがverificationEmailRequired: falseを返す場合 |
| 空文字/null | sender_emailがemptyable=trueで空の場合 |
| 同一メール | sender_reply_toがsender_emailと同じ場合（既に検証済み） |
| 許可されないメール | emailAddressService.validateがallowed: falseを返す場合（エラーをスロー） |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[ニュースレター作成/編集] --> B[prepAttrsForEmailVerification]
    B --> C{sender_email/reply_to変更?}
    C -->|No| D[変更なしで処理続行]
    C -->|Yes| E[emailAddressService.validate]
    E --> F{allowed?}
    F -->|No| G[ValidationError]
    F -->|Yes| H{verificationEmailRequired?}
    H -->|No| I[そのまま属性を適用]
    H -->|Yes| J[属性をemailsToVerifyに追加]
    J --> K[cleanedAttrsから属性を削除]
    K --> L[ニュースレター保存]
    L --> M[respondWithEmailVerification]
    M --> N{emailsToVerify.length > 0?}
    N -->|No| O[ニュースレター返却]
    N -->|Yes| P[sendEmailVerificationMagicLink]
    P --> Q[MagicLink生成]
    Q --> R[ghostMailer.send]
    R --> S[meta.sent_email_verification設定]
    S --> O
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| newsletters | ニュースレター設定取得 | 変更前の値との比較 |

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

#### newsletters

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| sender_email | 変更前の送信元メール | ニュースレターID |
| sender_reply_to | 変更前の返信先メール | ニュースレターID |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| newsletters | UPDATE | 検証完了時にsender_email/sender_reply_toを更新 |

#### 検証完了時の更新

| 操作 | 項目（カラム名） | 更新値 | 備考 |
|-----|-----------------|-------|------|
| UPDATE | sender_email または sender_reply_to | 検証されたメールアドレス | verifyPropertyUpdate経由 |

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| ValidationError | メールアドレスが許可されていない | senderEmailNotAllowed/replyToNotAllowedメッセージ |
| BadRequestError | 無効なメールアドレス形式 | MagicLinkサービスでinvalidEmailエラー |
| 送信失敗 | SMTP接続エラー等 | エラーをスロー |

### リトライ仕様

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

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 制限なし |
| 1日あたり上限 | 制限なし |

### 配信時間帯

制限なし（API呼び出し時に即時送信）

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

- MagicLinkトークンはsingleUseTokenProviderで生成され、一度のみ使用可能
- トークンの有効期限は24時間
- トークンにはニュースレターID、プロパティ名、新しいメールアドレス値が含まれる
- 検証URLはGhost管理画面のハッシュフラグメントとして構成（`/settings/newsletters/?verifyEmail={token}`）
- 本番環境以外ではログに検証URLを出力（デバッグ用）

## 備考

### 検証対象のプロパティ

```javascript
const emailProperties = [
    {property: 'sender_email', type: 'from', emptyable: true, error: messages.senderEmailNotAllowed}
];
// sender_reply_toが'newsletter'/'support'以外の場合のみ検証対象に追加
if (!['newsletter', 'support'].includes(cleanedAttrs.sender_reply_to)) {
    emailProperties.push({property: 'sender_reply_to', type: 'replyTo', emptyable: false, error: messages.replyToNotAllowed});
}
```

### 検証URL生成

```javascript
getSigninURL(token) {
    const adminUrl = urlUtils.urlFor('admin', true);
    const signinURL = new URL(adminUrl);
    signinURL.hash = `/settings/newsletters/?verifyEmail=${token}`;
    return signinURL.href;
}
```

### トークンデータ構造

```javascript
tokenData: {
    id: newsletter.get('id'),  // ニュースレターID
    property: 'sender_email',   // 更新するプロパティ名
    value: email                // 新しいメールアドレス
}
```

### 検証完了処理

`verifyPropertyUpdate`メソッドでトークンからデータを取得し、ニュースレターの該当プロパティを更新する。

---

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

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

### 推奨読解順序

#### Step 1: メールアドレス検証の準備処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | newsletters-service.js | `ghost/core/core/server/services/newsletters/newsletters-service.js` | 265-326行目: `prepAttrsForEmailVerification`メソッド |

**読解のコツ**:
- **268-276行目**: 検証対象のemailPropertiesを定義
- **278-304行目**: 各プロパティをループして検証要否をチェック
- **287行目**: `emailAddressService.service.validate`で検証
- **295-301行目**: 検証が必要な場合はemailsToVerifyに追加

#### Step 2: 検証メール送信を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | newsletters-service.js | `ghost/core/core/server/services/newsletters/newsletters-service.js` | 331-342行目: `respondWithEmailVerification`メソッド |
| 2-2 | newsletters-service.js | `ghost/core/core/server/services/newsletters/newsletters-service.js` | 347-367行目: `sendEmailVerificationMagicLink`メソッド |

**主要処理フロー**:
- **332-335行目**: emailsToVerifyをループして検証メール送信
- **337-338行目**: meta.sent_email_verificationを設定
- **348行目**: defaultFromAddressを取得
- **351-364行目**: MagicLinkServiceのtransporterをオーバーライド
- **366行目**: `magicLinkService.sendMagicLink`を呼び出し

#### Step 3: MagicLinkサービスを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | magic-link.js | `ghost/core/core/server/services/lib/magic-link/magic-link.js` | 72-119行目: `sendMagicLink`メソッド |
| 3-2 | newsletters-service.js | `ghost/core/core/server/services/newsletters/newsletters-service.js` | 75-80行目: `getSigninURL`関数 |

**主要処理フロー**:
- **81行目**: `tokenProvider.create`でトークン生成
- **85行目**: `getSigninURL`でURL生成
- **97-102行目**: `transporter.sendMail`でメール送信
- **77-79行目**: 管理画面URLにハッシュフラグメントとしてトークンを付与

#### Step 4: メールテンプレートを理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | verify-email.js | `ghost/core/core/server/services/newsletters/emails/verify-email.js` | 1-172行目: HTMLテンプレート全体 |

**主要コンテンツ**:
- **119行目**: 挨拶文「Hey there,」
- **120行目**: 本文「Please confirm your email address with this link:」
- **128行目**: 確認ボタン「Confirm email address」
- **136行目**: 有効期限「For your security, the link will expire in 24 hours time.」
- **150行目**: 免責事項

#### Step 5: 検証完了処理を理解する

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 5-1 | newsletters-service.js | `ghost/core/core/server/services/newsletters/newsletters-service.js` | 245-258行目: `verifyPropertyUpdate`メソッド |

**主要処理フロー**:
- **246行目**: `magicLinkService.getDataFromToken`でトークンからデータ取得
- **247行目**: id, property, valueを抽出
- **252行目**: `NewsletterModel.edit`でプロパティを更新
- **255行目**: meta.email_verifiedに検証されたプロパティ名を設定

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

```
NewslettersService.add() / edit()
    │
    ├─ prepAttrsForEmailVerification(attrs, newsletter)
    │      ├─ emailAddressService.service.validate(email, type)
    │      └─ emailsToVerify配列に追加
    │
    ├─ NewsletterModel.add() / edit()
    │
    └─ respondWithEmailVerification(newsletter, emailsToVerify)
           │
           └─ sendEmailVerificationMagicLink({id, email, property})
                  │
                  ├─ emailAddressService.service.defaultFromAddress
                  │
                  ├─ magicLinkService.transporter設定
                  │      └─ ghostMailer.send(msg)
                  │
                  └─ magicLinkService.sendMagicLink({email, tokenData})
                         ├─ tokenProvider.create(tokenData)
                         ├─ getSigninURL(token) → adminUrl + hash
                         ├─ getHTML(url, type, email) → verify-email.js
                         ├─ getText(url, type, email)
                         └─ transporter.sendMail()

検証完了時:
NewslettersService.verifyPropertyUpdate(token)
    │
    ├─ magicLinkService.getDataFromToken(token)
    │      └─ tokenProvider.validate(token)
    │
    └─ NewsletterModel.edit({[property]: value}, {id})
```

### データフロー図

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

sender_email変更      ───▶ prepAttrsForEmailVerification ───▶ emailsToVerify
                              │
                              ├─ emailAddressService.validate
                              └─ cleanedAttrsから属性削除

emailsToVerify        ───▶ respondWithEmailVerification ───▶ 検証メール送信
                              │
                              └─ sendEmailVerificationMagicLink
                                    │
                                    ├─ MagicLink生成
                                    ├─ URL生成（adminUrl + hash）
                                    └─ ghostMailer.send

[出力メール]
  ├─ 件名: Verify email address
  ├─ 本文: 確認リンク（24時間有効）
  └─ 送信先: 検証対象メールアドレス

[検証完了]
token                 ───▶ verifyPropertyUpdate ───▶ ニュースレター更新
                              │
                              ├─ getDataFromToken
                              └─ NewsletterModel.edit
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| newsletters-service.js | `ghost/core/core/server/services/newsletters/newsletters-service.js` | ソース | ニュースレターサービスメイン |
| verify-email.js | `ghost/core/core/server/services/newsletters/emails/verify-email.js` | テンプレート | HTMLメールテンプレート |
| magic-link.js | `ghost/core/core/server/services/lib/magic-link/magic-link.js` | ソース | MagicLinkサービス |
| GhostMailer | mail serviceで管理 | ライブラリ | メール送信 |
| emailAddressService | 外部サービス | サービス | メールアドレス検証・デフォルトアドレス取得 |
| singleUseTokenProvider | 外部サービス | サービス | 一度限り使用可能なトークン生成 |
