# 通知設計書 1-会員サインインメール

## 概要

本ドキュメントは、Ghost CMSにおける会員サインインメール（Magic Link認証メール）の設計仕様を記載する。既存会員がサイトにサインインする際に送信されるマジックリンクメールの送信ロジック、テンプレート構造、およびデータフローを定義する。

### 本通知の処理概要

本通知は、既存会員がGhostサイトへサインインを要求した際に、パスワードレス認証のためのマジックリンクを含むメールを送信する機能である。

**業務上の目的・背景**：パスワードレス認証を実現することで、会員がパスワードを記憶・管理する必要がなくなり、セキュリティリスク（パスワード漏洩、使い回し等）を軽減する。また、サインインの障壁を下げることで会員のエンゲージメント向上を図る。Ghost CMSの会員管理システムの中核機能として位置づけられる。

**通知の送信タイミング**：会員がサイトのログインフォーム（Portal等）でメールアドレスを入力し、サインインボタンをクリックした時点でトリガーされる。APIエンドポイント `POST /members/api/send-magic-link` が呼び出されることで送信処理が開始される。

**通知の受信者**：サインインを要求した既存会員本人のメールアドレスに送信される。メールアドレスはリクエストボディから取得され、membersテーブルに登録済みの会員であることが確認される。

**通知内容の概要**：メールにはワンタイムコード（OTC）とマジックリンクURLの両方が含まれる。OTCは6桁の数字コードで、ユーザーはコードを入力するか、リンクをクリックすることでサインインを完了できる。リンクは24時間で有効期限が切れる。

**期待されるアクション**：受信者はメール内のOTCをサイトのコード入力欄に入力するか、「Sign in now」ボタンをクリックしてサインインを完了する。どちらのアクションでも同じ認証結果が得られる。

## 通知種別

メール通知（トランザクションメール）

## 送信仕様

### 基本情報

| 項目 | 内容 |
|-----|------|
| 送信方式 | 同期（API応答を待つ） |
| 優先度 | 高 |
| リトライ | 無（送信失敗時はエラーレスポンスを返却） |

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

1. クライアントからのリクエストボディに含まれるメールアドレスを取得
2. メールアドレスの正規化（normalizeEmail関数）を実行
3. `members`テーブルでメールアドレスの存在確認を実施
4. 会員が存在する場合のみサインインメールを送信（存在しない場合はエラー）

## 通知テンプレート

### メール通知の場合

| 項目 | 内容 |
|-----|------|
| 送信元アドレス | サイト設定の`members_support_address`または`noreply@[サイトドメイン]` |
| 送信元名称 | サイトタイトル（settingsCache.get('title')） |
| 件名 | OTCあり: `Sign in to {siteTitle} with code {otc}` / OTCなし: `Secure sign in link for {siteTitle}` |
| 形式 | HTML + テキスト（マルチパート） |

### 本文テンプレート

```html
<!-- OTCあり版 -->
件名: Sign in to {siteTitle} with code {otc}

本文:
Hey there,

Welcome back! Here's your code to sign in to {siteTitle}:

[OTCコード（6桁）]

Or, skip the code and sign in directly:

[Sign in now ボタン]

You can also copy & paste this URL into your browser:
{magicLinkUrl}

---
If you did not make this request, you can safely ignore this email.
This message was sent from {siteDomain} to {email}.
```

```html
<!-- OTCなし版 -->
件名: Secure sign in link for {siteTitle}

本文:
Hey there,

Welcome back! Use this link to securely sign in to your {siteTitle} account:

[Sign in to {siteTitle} ボタン]

For your security, the link will expire in 24 hours time.

See you soon!

---
You can also copy & paste this URL into your browser:
{magicLinkUrl}

If you did not make this request, you can safely ignore this email.
This message was sent from {siteDomain} to {email}.
```

### 添付ファイル

| ファイル名 | 形式 | 条件 | 説明 |
|----------|------|------|------|
| なし | - | - | 本通知には添付ファイルはない |

## テンプレート変数

| 変数名 | 説明 | データ取得元 | 必須 |
|--------|------|-------------|-----|
| siteTitle | サイト名 | settingsCache.get('title') | Yes |
| email | 送信先メールアドレス | リクエストボディ | Yes |
| url | マジックリンクURL | tokenProvider.create() + getSigninURL() | Yes |
| otc | ワンタイムコード（6桁） | tokenProvider.deriveOTC() | No |
| accentColor | サイトのアクセントカラー | settingsCache.get('accent_color') | No（デフォルト: #15212A） |
| siteDomain | サイトドメイン | urlUtils.getSiteUrl()から抽出 | Yes |
| siteUrl | サイトURL | urlUtils.getSiteUrl() | Yes |

## 送信トリガー・条件

### トリガー一覧

| トリガー種別 | トリガーイベント | 送信条件 | 説明 |
|------------|----------------|---------|------|
| API呼び出し | POST /members/api/send-magic-link | emailType='signin' かつ会員が存在 | 会員向けログインフォームからの送信 |
| API呼び出し | POST /members/api/send-magic-link | emailType未指定かつ会員が存在 | デフォルトでsigninとして処理 |

### 送信抑止条件

| 条件 | 説明 |
|-----|------|
| 会員未登録 | 指定メールアドレスがmembersテーブルに存在しない場合はエラー |
| 無効なメールアドレス | isEmail()バリデーションを通過しない場合 |
| ハニーポット検出 | honeypotフィールドに値がある場合（ボット対策） |
| ブロックドメイン | `all_blocked_email_domains`設定でブロックされたドメインの場合 |

## 処理フロー

### 送信フロー

```mermaid
flowchart TD
    A[POST /members/api/send-magic-link] --> B[メールアドレス正規化]
    B --> C{バリデーション}
    C -->|失敗| D[400 Bad Request]
    C -->|成功| E{会員存在確認}
    E -->|存在しない| F[会員未登録エラー]
    E -->|存在する| G[トークン生成]
    G --> H{OTC含む?}
    H -->|Yes| I[OTC導出]
    H -->|No| J[OTCなし]
    I --> K[マジックリンクURL生成]
    J --> K
    K --> L[HTMLテンプレート展開]
    L --> M[メール送信]
    M --> N{送信結果}
    N -->|成功| O[201 Created + sniperLinks]
    N -->|失敗| P[エラーレスポンス]
```

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

### 参照テーブル一覧

| テーブル名 | 用途 | 備考 |
|-----------|------|------|
| members | 会員存在確認 | email列で検索 |
| settings | サイト設定取得 | settingsCacheから取得 |

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

#### members

| 参照項目（カラム名） | 用途 | 取得条件 |
|-------------------|------|---------|
| email | 会員存在確認 | WHERE email = [入力メールアドレス] |
| id | 会員ID（トークンデータ用） | 上記条件で取得 |

### 更新テーブル一覧

| テーブル名 | 操作 | 概要 |
|-----------|------|------|
| tokens | INSERT | マジックリンクトークンの保存（token_provider実装による） |

#### 送信ログテーブル

本機能では明示的な送信ログテーブルへの書き込みは行われない。メール送信はGhostMailerを通じて実行され、送信結果はAPIレスポンスとして返却される。

## エラー処理

### エラーケース一覧

| エラー種別 | 発生条件 | 対処方法 |
|----------|---------|---------|
| BadRequestError | メールアドレスが未入力または不正形式 | 400エラーを返却、クライアント側でメッセージ表示 |
| BadRequestError | 会員が存在しない | エラーメッセージ「No member exists with this e-mail address」を返却 |
| EENVELOPE | メール送信時のエンベロープエラー | 400エラーを返却 |
| 送信失敗 | SMTPエラー等 | エラーをログ出力し、例外をthrow |

### リトライ仕様

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

## 配信設定

### レート制限

| 項目 | 内容 |
|-----|------|
| 1分あたり上限 | 設定なし（SMTPサーバー側の制限に依存） |
| 1日あたり上限 | 設定なし |

### 配信時間帯

特定の配信時間帯制限はない。24時間いつでも送信可能。

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

- マジックリンクトークンは24時間で有効期限切れ
- トークンは暗号学的にセキュアな方法で生成（crypto.randomBytes相当）
- OTCは6桁の数字コードで、トークンIDから派生して生成
- OTC検証時はタイムスタンプベースのリプレイ攻撃対策（5分のウィンドウ）を実装
- ハニーポットフィールドによるボット対策
- メールアドレスの正規化により、ホモグラフ攻撃を軽減
- 存在しない会員へのサインインリクエストはエラーを返す（列挙攻撃への対策は限定的）

## 備考

- OTCは`includeOTC=true`がリクエストに含まれる場合のみ生成される
- Portalコンポーネントは通常OTCを含むリクエストを送信する
- サインアップ済みの会員がサインアップフローに入った場合も、自動的にサインインメールにフォールバックする
- `sniperLinks`としてメールクライアント（Gmail, Outlook等）への直接リンクがレスポンスに含まれる場合がある

---

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

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

### 推奨読解順序

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

まず、メールテンプレートに渡されるデータ構造と、トークンの形式を理解する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 1-1 | signin.js | `ghost/core/core/server/services/members/emails/signin.js` | テンプレート関数のシグネチャ（t, siteTitle, email, url, otc等）を確認。HTMLテンプレートの構造を把握 |
| 1-2 | magic-link.js | `ghost/core/core/server/services/lib/magic-link/magic-link.js` | TokenProvider型定義（create, validate, getRefByToken, deriveOTC）を確認 |

**読解のコツ**: signin.jsはテンプレートリテラル関数で、引数がそのままテンプレート変数となる。条件分岐（otcの有無）でUIが変わる点に注目。

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

処理の起点となるAPIコントローラーを特定する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 2-1 | router-controller.js | `ghost/core/core/server/services/members/members-api/controllers/router-controller.js` | sendMagicLink()メソッド（676行目〜）がAPIエンドポイントの実装 |

**主要処理フロー**:
1. **676-678行目**: リクエストからemail, honeypot, emailTypeを取得
2. **694-700行目**: メールアドレスの正規化とバリデーション
3. **721-755行目**: emailTypeに応じてsignup/signinハンドラーを呼び出し
4. **857-876行目**: _handleSignin()で会員存在確認とメール送信

#### Step 3: メール送信サービスを理解する

マジックリンク生成とメール送信の実装を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 3-1 | members-api.js | `ghost/core/core/server/services/members/members-api/members-api.js` | sendEmailWithMagicLink()関数（223-234行目）の実装。typeの決定ロジック |
| 3-2 | magic-link.js | `ghost/core/core/server/services/lib/magic-link/magic-link.js` | sendMagicLink()メソッド（72-119行目）。トークン生成、OTC導出、メール送信の流れ |

**主要処理フロー**:
- **80-81行目**: tokenProvider.create()でトークン生成
- **88-95行目**: includeOTCがtrueの場合、getOTCFromToken()でOTC生成
- **97-102行目**: transporter.sendMail()でメール送信

#### Step 4: 設定とユーティリティを理解する

サイト設定の取得方法とメール送信基盤を確認する。

| 順序 | ファイル | パス | 読解ポイント |
|-----|---------|------|-------------|
| 4-1 | settings-helpers | `ghost/core/core/server/services/settings-helpers/` | getMembersSupportAddress()等のヘルパー関数 |
| 4-2 | normalize-email.js | `ghost/core/core/server/services/members/members-api/utils/normalize-email.js` | メールアドレス正規化ロジック |

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

```
POST /members/api/send-magic-link
    │
    ├─ RouterController.sendMagicLink()
    │      │
    │      ├─ normalizeEmail() - メールアドレス正規化
    │      │
    │      ├─ _handleSignin()
    │      │      └─ memberRepository.get({email}) - 会員存在確認
    │      │
    │      └─ sendEmailWithMagicLink()
    │             │
    │             └─ MagicLink.sendMagicLink()
    │                    │
    │                    ├─ tokenProvider.create() - トークン生成
    │                    │
    │                    ├─ getOTCFromToken() - OTC導出（オプション）
    │                    │
    │                    ├─ getSigninURL() - マジックリンクURL生成
    │                    │
    │                    └─ transporter.sendMail()
    │                           │
    │                           └─ signin.js テンプレート関数
    │
    └─ getSniperLinks() - メールクライアントリンク生成
```

### データフロー図

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

リクエストボディ ───────▶ RouterController
  - email                      │
  - emailType                  ▼
  - includeOTC           メールアドレス正規化
                               │
                               ▼
membersテーブル ◀────── 会員存在確認
                               │
                               ▼
                         トークン生成
                               │
settings ─────────────▶  テンプレート展開
  - title                      │
  - accent_color               ▼
                         メール送信 ─────────▶ 会員のメールボックス
                               │
                               ▼
                         APIレスポンス
                           - sniperLinks
                           - otc_ref
```

### 関連ファイル一覧

| ファイル | パス | 種別 | 役割 |
|---------|------|------|------|
| signin.js | `ghost/core/core/server/services/members/emails/signin.js` | テンプレート | サインインメールのHTMLテンプレート関数 |
| router-controller.js | `ghost/core/core/server/services/members/members-api/controllers/router-controller.js` | コントローラー | send-magic-link APIエンドポイントの実装 |
| members-api.js | `ghost/core/core/server/services/members/members-api/members-api.js` | サービス | Members API全体のファクトリー関数、sendEmailWithMagicLink定義 |
| magic-link.js | `ghost/core/core/server/services/lib/magic-link/magic-link.js` | ライブラリ | マジックリンク生成・送信のコアロジック |
| normalize-email.js | `ghost/core/core/server/services/members/members-api/utils/normalize-email.js` | ユーティリティ | メールアドレス正規化関数 |
| member-repository.js | `ghost/core/core/server/services/members/members-api/repositories/member-repository.js` | リポジトリ | 会員データのCRUD操作 |
| GhostMailer | `ghost/core/core/server/services/mail/` | サービス | メール送信基盤 |
